آموزش پردازش زبان طبیعی با اکوسیستم هاگینگ فیس ؛ تنظیم مدل ازپیش آموزشدیده (قسمت اول فصل سوم)
در فصل دوم دوره آموزش پردازش زبان طبیعی، نحوهی استفاده از توکنکنندهها و مدلهایِ از پیش آموزش یافته برای انجام پیشبینی بررسی شد. اما اگر بخواهید مدلِ از پیش آموزش دیدهای را برای دیتاست خودتان تنظیم کنید، چه رویکردی باید در پیش بگیرید؟ فصل جاری به طور مفصل به این پرسش خواهد پرداخت. موارد زیر را در این فصل یاد خواهید گرفت:
- نحوهی آمادهسازی دیتاست بزرگ از Hub
- نحوهی استفاده از کراس برای تنظیم دقیق یک مدل
- نحوهی استفاده از کراس برای انجام پیشبینی
- نحوهی استفاده از متریک یا ابزار سنجش اختصاصی
برای اینکه چکپوینتهای آموزش یافتهتان را در Hugging Face Hub بارگذاری کنید، به یک حساب کاربری huggingface.co نیاز دارید. در این لینک میتوانید حساب کاربری خود را ایجاد کنید.
در انتهای این مطلب میتوانید به لینک سایر قسمتهای دو فصل گذشته از دوره آموزشی پردازش زبان طبیعی دسترسی داشته باشید.
پردازش دادهها
میخواهیم کار را با مثالی از فصل گذشته ادامه دهیم. چگونگی آموزشِ کلاسیفایر توالی Sequence classifier در یک دسته یا بچ Batch را در تنسورفلوTensorflow ملاحظه میکنید:
import tensorflow as tf import numpy as np from transformers import AutoTokenizer, TFAutoModelForSequenceClassification # Same as before checkpoint = "bert-base-uncased" tokenizer = AutoTokenizer.from_pretrained(checkpoint) model = TFAutoModelForSequenceClassification.from_pretrained(checkpoint) sequences = [ "I've been waiting for a HuggingFace course my whole life.", "This course is amazing!", ] batch = dict(tokenizer(sequences, padding=True, truncation=True, return_tensors="tf")) # This is new model.compile(optimizer='adam', loss='sparse_categorical_crossentropy') labels = tf.convert_to_tensor([1, 1]) model.train_on_batch(batch, labels)
البته، اگر فقط به آموزش مدل با دو جمله بسنده کنید، نتایج خیلی خوبی به دست نخواهید آورد. برای اینکه نتایج بهتری کسب کنید، باید دیتاست بزرگتری آماده کنید.
در این بخش، از دیتاست MRPC (پیکرهی پارافریز تحقیقات مایکروسافتMicrosoft Research Paraphrase Corpus) به عنوان نمونه استفاده خواهیم کرد که در مقالهی ویلیام بی. دولان و کریس بروکت معرفی شده است. دیتاست از 5801 جفت جمله تشکیل یافته است؛ برچسبی هم وجود دارد که نشان میدهد جملات خلاصهنویسی Paraphrases شدهاند یا خیر (یعنی هر دو جمله معنی یکسانی دارند یا خیر). یکی از دلایلی که ما را به انتخاب این دیتاست مجاب کرده، اندازهی کوچک آن است و آموزش در آن به آسانی صورت میگیرد.
بارگذاری دیتاست از Hub
Hub فقط حاوی مدلها نیست، بلکه چندین و چند دیتاست به زبانهای مختلف دنیا نیز درون خود جای داده است. میتوانید در این لینک به بررسی این دیتاستها بپردازید. توصیه میکنیم به محض اینکه این بخش را به پایان رساندید، دیتاست جدیدی را بارگذاری و پردازش کنید (مستندات کلی در این لینک قرار داده شده است). اما اینک، باید روی دیتاست MRPC تمرکز کنیم. این یکی از 10 دیتاستی است که « بنچمارک GLUEGLUE benchmark » را تشکیل میدهد. این معیار آکادمیک برای اندازهگیری عملکرد مدلهای یادگیری ماشین در 10 مورد طبقهبندی متن مختلف استفاده میشود. کتابخانهی Datasets دستور بسیار سادهای برای دانلود دیتاست از Hub دارد. دیتاست MRPC به ترتیب زیر دانلود میشود:
from datasets import load_dataset raw_datasets = load_dataset("glue", "mrpc") raw_datasets
DatasetDict({ train: Dataset({ features: ['sentence1', 'sentence2', 'label', 'idx'], num_rows: 3668 }) validation: Dataset({ features: ['sentence1', 'sentence2', 'label', 'idx'], num_rows: 408 }) test: Dataset({ features: ['sentence1', 'sentence2', 'label', 'idx'], num_rows: 1725 }) })
همانطور که مشاهده میکنید، شیء DatasetDict به دست میآید که حاوی مجموعههای آموزش، اعتبارسنجیValidation و آزمایش است. هر یک از این مجموعهها دارای چند ستون (sentence1, sentence2, label, and idx) و چندین سطر هستند که تعداد عناصر موجود در هر مجموعه را نشان میدهد (پس 3668 جفت جمله در مجموعه آموزش، 408 جفت در مجموعه اعتبارسنجی و 1725 جفت در مجموعه آزمایش وجود دارد). این دستور دیتاست را دانلود کرده و طبق پیشفرض در ~/.cache/huggingface/dataset قرار میدهد. احتمالاً از فصل 2 به خاطر دارید که میتوانید پوشهی cache را با تنظیم متغیر محیط HF_HOME به طور اختصاصی تغییر دهید. با عمل indexing میتوان به هر جفت از جملات در raw_datasets دسترسی پیدا کرد، مثلاً با یک دیکشنری:
raw_train_dataset = raw_datasets["train"] raw_train_dataset[0]
{'idx': 0, 'label': 1, 'sentence1': 'Amrozi accused his brother , whom he called " the witness " , of deliberately distorting his evidence .', 'sentence2': 'Referring to him as only " the witness " , Amrozi accused his brother of deliberately distorting his evidence .'}
همانطور که ملاحظه میکنید، برچسبهاLabels اعداد صحیح هستند. پس نیازی به انجام پیشپردازش نیست. برای اینکه ببینیم کدام عدد صحیح به کدام برچسب تعلق دارد، میتوان features را در raw_train_dataset بررسی کرد. با این کار، نوع هر ستون مشخص میشود:
raw_train_dataset.features
{'sentence1': Value(dtype='string', id=None), 'sentence2': Value(dtype='string', id=None), 'label': ClassLabel(num_classes=2, names=['not_equivalent', 'equivalent'], names_file=None, id=None), 'idx': Value(dtype='int32', id=None)}
باید به این نکته اشاره کرد که برچسب (label) از نوعِ ClassLabel است و نگاشتMapping اعداد صحیح به نام برچسب در پوشه نامها (names) ذخیره شده است. 0 نشاندهندهی not_equivalent و 1 نشاندهندهی equivalent است.
حال سعی کنید به عنصر 15 مجموعه آموزش و عنصر 87 مجموعه اعتبارسنجی نگاه کرده و بررسی کنید چه برچسبهای برای آنها به کار رفته است؟
پیشپردازش دیتاست
پیشپردازش دیتاست مستلزم این است که متن را به اعدادی تبدیل کنیم که مدل بتواند از آن سر در بیاورد. همانگونه که در فصل پیشین ملاحظه کردید، این کار با توکنکننده انجام میگیرد. میتوان یک جمله یا لیستی از جملات را در اختیار توکنکننده قرار داد. بنابراین، میتوان همه جملات نخست و همه جملات دومِ هر جفت را به ترتیب زیر توکنسازی کرد:
from transformers import AutoTokenizer checkpoint = "bert-base-uncased" tokenizer = AutoTokenizer.from_pretrained(checkpoint) tokenized_sentences_1 = tokenizer(raw_datasets["train"]["sentence1"]) tokenized_sentences_2 = tokenizer(raw_datasets["train"]["sentence2"])
با این حال، این امکان وجود ندارد که دو توالی را به مدل ارسال و اینطور پیشبینی کنیم که دو جمله خلاصهنویسی شدهاند یا خیر. باید دو توالی را در قالب یک جفت بررسی کرد و از روش پیشپردازش مناسب استفاده کرد. خوشبختانه، توکنکننده نیز میتواند یک جفت از توالیها را بردارد و طبق انتظارات مدل برت به آمادهسازی آن بپردازد:
inputs = tokenizer("This is the first sentence.", "This is the second one.") inputs
{ 'input_ids': [101, 2023, 2003, 1996, 2034, 6251, 1012, 102, 2023, 2003, 1996, 2117, 2028, 1012, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }
در فصل 2 درباره input_ids و attention_mask صحبت کردیم، اما بحث درباره token_type_ids را به زمان دیگری موکول کردیم. در این مثال، مدل تشخیص میدهد که کدام بخش از ورودی جمله اول و کدام بخش جمله دوم است.
حالا عنصر 15 مجموعه آموزش را انتخاب کرده و دو جمله را به صورت جداگانه و جفت توکنسازی کنید. این دو نتیجه چه فرقی با یکدیگر دارند؟
اگر شناسههای (IDs) درون input_ids را با این دستور به واژه تبدیل کنیم:
tokenizer.convert_ids_to_tokens(inputs["input_ids"])
نتیجه زیر به دست میآید:
['[CLS]', 'this', 'is', 'the', 'first', 'sentence', '.', '[SEP]', 'this', 'is', 'the', 'second', 'one', '.', '[SEP]']
وقتی دو جمله وجود داشته باشد، مدل انتظار دارد ورودیها به شکل [CLS] sentence1 [SEP] sentence2 [SEP] باشند. نتیجهی مطابقت با token_type_ids در زیر نشان داده شده است:
['[CLS]', 'this', 'is', 'the', 'first', 'sentence', '.', '[SEP]', 'this', 'is', 'the', 'second', 'one', '.', '[SEP]'] [ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1]
همانطور که میبینید، بخشهایی از ورودی که با [CLS] sentence1 [SEP] مطابقت دارند، همگی دارای شناسه توکن نوع 0 هستند، اما بخشهای دیگری که مطابقتشان با sentence2 [SEP] اثبات شده، دارای شناسه توکن نوع 1 میباشند. توجه داشته باشید که اگر چکپوینت متفاوتی را انتخاب کنید، لزوماً token_type_ids را در ورودیهای توکنشده نخواهید داشت (برای مثال، اگر از مدل DistilBERT استفاده کنید، ورودیها برگردانده نخواهند شد). ورودیها تنها زمانی برگردانده میشوند که مدل بداند قرار است با آنها چه کار کند، چرا که در طی فرایند پیشآموزش آنها را دیده است.
در اینجا، برت با شناسههای نوع توکن آموزش داده میشود. افزون بر هدف مدلسازیِ زبان masked که در فصل 1 بررسی شد، هدف دیگری تحت عنوان پیشبینی جمله بعدیnext sentence prediction نیز وجود دارد. هدف این است که رابطه میان جفت
جملات مدلسازی شود.
با پیشبینی جمله بعدی، جفت جملات در اختیار مدل قرار گرفته و از آن درخواست میشود تا پیشبینی کند که آیا جمله دوم از اولی پیروی میکند یا خیر. برای اینکه اهمیت کار مضاعف شود، نیمی از مواقع جملات در سند اصلی به دنبال یکدیگر میآیند که از آن استخراج شدهاند. نیمی دیگر از مواقع نیز دو جمله از دو سند مختلف به دست میآیند.
عموماً، نیازی نیست نگران این موضوع باشید که token_type_ids در ورودیهای توکنشدهتان وجود دارد یا خیر. تا زمانی که از چکپوینت یکسانی برای توکنکننده و مدل استفاده کنید، همه کارها به خوبی پیش خواهد رفت زیرا توکنکننده از نیازهای مدل باخبر است.
حال که دیدید توکنکنندهها چگونه با یک جفت جمله برخورد میکند، میتوانیم از آنها برای توکنسازیِ کل دیتاستمان استفاده کنیم. به همان شیوهای که در فصل قبل عمل کردیم، میتوان لیستی از جفت جملات را در اختیار توکنکننده قرار داد. این کار با ارائهی لیست جملات اول و سپس لیست جملات دوم به انجام میرسد. این رویکرد با عمل پدینگ و کوتاهسازی که در فصل 2 توضیح داده شد، مطابقت دارد. از این رو، یکی از راههای پیشپردازش دیتاست آموزش، استفاده از رشته کدهای زیر است. توجه داشته باشید که آرگومان p‘ این بار به توکنکننده اعلام میکند که خروجی را در آرایههای NumPy میخواهیم.
tokenized_dataset = tokenizer( raw_datasets["train"]["sentence1"], raw_datasets["train"]["sentence2"], padding=True, truncation=True, )
برای اینکه دادهها در قالب دیتاست باقی بمانند، باید از روشDataset.map() استفاده کرد. اگر عملیات پیشپردازش بیشتری در دستور کار قرار گیرد، میتوان روی انعطافپذیری روش مذکور حساب کرد. روش map() تابعی را در تمامی عناصر دیتاست پیادهسازی میکند. بنابراین، باید تابعی را تعریف کرد که ورودیها را توکنسازی کند:
def tokenize_function(example): return tokenizer(example["sentence1"], example["sentence2"], truncation=True)
این تابع یک دیکشنری را برداشته و دیکشنری جدیدی با input_ids، attention_mask و token_type_ids به عنوان خروجی تحویل میدهد. توجه داشته باشید که تابع یاد شده زمانی کارساز واقع میشود که دیکشنری example حاوی چند نمونه باشد (هر کلید، لیستی از جملات در نظر گرفته شود) زیرا tokenizer لیستی از جفت جملات را بررسی میکند. این کارکرد پیشتر نیز توضیح داده شده است. بنابراین، امکانِ استفاده از گزینهی batched=True نیز فراهم میشود. این کار سرعت توکنسازی را به طرز قابل توجهی افزایش خواهد داد. توکنکنندهای که در Rust نوشته شده و در کتابخانهی ? Tokenizers قرار دارد، میتواند از tokenizer پشتیبانی کند. این توکنکننده میتواند از سرعت بسیار بالایی برخوردار باشد، اما اگر هر بار تعداد ورودی زیادی در اختیار آن قرار داده شود.
به این موضوع هم توجه داشته باشید که آرگومان padding در تابع توکنسازی به کار برده نمیشود زیرا اگر همه نمونهها دارای طول حداکثری باشند، کارایی افزایش نخواهد یافت. بهتر است عملیات پدینگ نمونهها را زمانی اجرا کرد که ساخت دسته در دستور کار باشد. بنابراین، تا آن زمان، باید طول حداکثری را در آن دسته لحاظ کرد. البته طول حداکثری در کل دیتاست مورد پذیرش نیست. اگر ورودیها طول متفاوتی داشته باشند، اقدام فوق میتواند در زمان و توان پردازشی صرفهجویی کند.
اکنون باید چگونگی بهکارگیریِ تابع توکنسازی را در تمامی دیتاستها توضیح داد. در این راستا، از batched=True استفاده شده و تابع در چندین عنصر از دیتاست به کار برده میشود. نباید تابع به صورت جداگانه برای تکتک عناصر در نظر گرفته شود. این کار میتواند باعث افزایش سرعت پیشپردازش شود.
tokenized_datasets = raw_datasets.map(tokenize_function, batched=True) tokenized_datasets
کتابخانهی ? Datasets این فرایند پردازش را با اضافه کردن فیلدهای جدید به دیتاستها انجام میدهد. همچنین، زمانی میتوان از فرایند چندپردازشی استفاده کرد که تابع پیشپردازش با map() در اولویت باشد. در این شرایط، آرگومان num_proc به کار میآید. این کار در بخش حاضر انجام نشد زیرا کتابخانهی ? Tokenizers از رشتههایthread بسیاری برای توکن کردن سریع نمونهها استفاده میکند. اما اگر از توکنکنندهی پرسرعتی استفاده نمیکنید که تحت پشتیبانی این کتابخانه باشد، عملیات فوق میتواند سرعت پیشپردازش را افزایش دهد.
تابع tokenize_function یک دیکشنری با کلیدهای input_ids، attention_mask و token_type_ids تحویل میدهد. پس، این سه فیلد به همه splitها در دیتاست افزوده میشوند. این نکته را به خاطر داشته باشید که اگر تابش پیشپردازش مقدار جدیدی برای کلید موجود در دیتاست ارائه میکرد، امکان تغییر فیلدهای موجود فراهم میشد. آخرین کاری که باید انجام داد، اجرای عملیات پدینگ در نمونهها است. این روش با عنوان پدینگ به صورت پویا نیز شناخته میشود.
پدینگ به صورت پویا
تابعی که مسئولیتِ کنار هم قرار دادن نمونهها در دسته را بر عهده دارد، collate function نام دارد. این تابع، نمونهها را به tf.Tensor تبدیل کرده و آنها را کنار یکدیگر میچیند. این کار در کارهای ما امکانپذیر نیست زیرا ورودیهای ما حجم متفاوتی دارند. از این رو، عملیات پدینگ به صورت عامدانه به زمان بعد موکول شده است تا فقط در صورت نیاز در هر یک از دستهها به کار برده شود. ورودیهای بسیار طویل با عملیات پیچیدهی پدینگ کنار گذاشته میشوند.
این کار میتواند قدری به فرایند آموزش سرعت بدهد، اما این مسئله را به یاد داشته باشید که آموزش در TPU انجام شود، امکان دارد مشکلاتی پیش آید. TPU شکلهای ثابت را ترجیح میدهد، حتی در صورتی که نیاز به عملیات پدینگ بیشتری باشد. برای انجام این کار، باید تابع collate function را تعریف کرد که بتواند مقدار مناسبی از عملیات پدینگ را در آیتمهای دیتاست اجرا کند. این دیتاست نیاز به دستهبندی دارد.
خوشبختانه، کتابخانهی ? Transformers میتواند چنین تابعی را با استفاده از DataCollatorWithPadding ارائه کند. راهاندازی آن با یک توکنکننده میسر میشود. باید دید که چه نوع عملیات پدینگی مورد نیاز است. این مسئله نیز نیاز به شفافسازی دارد که آیا مدل انتظار دارد پدینگ در سمت راست ورودیها اِعمال شود یا سمت چپ. بنابراین، تمامی خواستهها به مرحلهی اجرا در میآیند:
from transformers import DataCollatorWithPadding data_collator = DataCollatorWithPadding(tokenizer=tokenizer, return_tensors="tf")
برای آزمایش این ابزار جدید، باید چند نمونه از مجموعهی آموزش برداشت. این نمونهها نیاز به دستهبندی دارند. در این بخش، ستونهای idx ،sentence1 و sentence2 حذف میشوند چرا که نیازی به آنها نخواهد بود. این ستونها حاوی استرینگ هستند (و امکان ایجاد تنسور با استرینگ وجود ندارد). اکنون، نوبت به بررسی طول ورودیها در دسته رسیده است:
samples = tokenized_datasets["train"][:8] samples = {k: v for k, v in samples.items() if k not in ["idx", "sentence1", "sentence2"]} [len(x) for x in samples["input_ids"]]
[50, 59, 47, 67, 59, 50, 62, 32]
جای تعجب ندارد که نمونهها طول متغیری داشته باشند (از 32 تا 67). پدینگ به صورت پویا شرایطی را رقم میزند که طی آن، نمونههای موجود در این دسته باید از طول 67 برخوردار باشند (طول حداکثری در داخل دسته). بدون پدینگ به صورت پویا، میبایست همه نمونهها دارای طول حداکثری در کل دیتاست باشند یا طول حداکثری داشته باشند که مورد پذیرش مدل باشد. باید یک بار دیگر این موضوع را بررسی کرد که آیا data_collator عملیات پدینگ را به صورت پویا در دستهها اجرا میکند یا خیر:
batch = data_collator(samples) {k: v.shape for k, v in batch.items()}
{'attention_mask': TensorShape([8, 67]), 'input_ids': TensorShape([8, 67]), 'token_type_ids': TensorShape([8, 67]), 'labels': TensorShape([8])}
این روش به خوبی عمل میکند. بنابراین، یک دیکشنری به دست میآید. حال به چند مورد از عیبهای آن اشاره کنیم. اولاً، روش مذکور تنها زمانی کارساز خواهد بود که RAM کافی برای کل دیتاستتان در طول فرایند توکنسازی داشته باشید. دیتاستهای برگرفته شده از کتابخانه ? Datasets عبارتند از فایلهای Apache Arrow که در دیسک ذخیره شدهاند. بنابراین، فقط نمونههایی بارگذاری میشوند که میخواهید در مموری باشند. علاوه بر این، همه نمونهها در کل دیتاست عمل پدینگ را تجربه میکنند.
اگر با دیتاست بزرگتری سر و کار دارید که در مموری جای نمیگیرد، یا اگر میخواهید دستههایی با طول متغیر ایجاد کنید، بهتر است از tf.data.Dataset API برای کنترل دقیقِ آن از دیسک استفاده کنید. یا میتوانید از حلقه آموزش دستی نیز استفاده نمائید (به فصل 9 مراجعه شود). اما در حال حاضر، کارتان با روش فوق راه میافتد.
عمل پیشپردازش را در دیتاست GLUE SST-2 انجام دهید. این کار قدری متفاوت است زیرا به جای جفت جملات از جملات تکی تشکیل یافته است. بقیه کارها با روال قبلی انجام میشود. برای اینکه چالش سختتری را تجربه کنید، اقدام به نوشتن یک تابع پیشپردازش کنید که در هر کدام از امور GLUE به خوبی عمل کند.
حال که دیتاست و تابع data_collator در دسترس قرار گرفته است، باید به تلفیق آنها اقدام کرد. میتوان دستهها را به صورت دستی بارگذاری و تلفیق کرد، اما این کار با دردسرهای بسیاری همراه است و شاید موثر نباشد. در عوض، باید از روش سادهتری استفاده کرد که راهکار موثری برای این مسئله ارائه نماید: to_tf_dataset(). در این صورت، tf.data.Dataset در دیتاست به کار برده میشود. tf.data.Dataset یک فرمت تنسورفلو است که Keras میتواند از آن برای model.fit() استفاده کند. این روش ? Dataset را به فرمتی تبدیل میکند که آمادهی آموزش است.
tf_train_dataset = tokenized_datasets["train"].to_tf_dataset( columns=["attention_mask", "input_ids", "token_type_ids"], label_cols=["labels"], shuffle=True, collate_fn=data_collator, batch_size=8, ) tf_validation_dataset = tokenized_datasets["validation"].to_tf_dataset( columns=["attention_mask", "input_ids", "token_type_ids"], label_cols=["labels"], shuffle=False, collate_fn=data_collator, batch_size=8, )
خب، کار با موفقیت به پایان رسید. دیتاستهای حاصله در مراحل بعدی نیز به کار برده میشوند. پس از اتمام فرایند دشوارِ پیشپردازش داده، آموزش بسیار لذتبخش میشود.
شما میتوانید از طریق لینک زیر به دیگر قسمتهای این دوره آموزشی دسترسی داشته باشید.
[button href=”https://hooshio.com/%D8%B1%D8%B3%D8%A7%D9%86%D9%87-%D9%87%D8%A7/%D8%A2%D9%85%D9%88%D8%B2%D8%B4-%D9%BE%D8%B1%D8%AF%D8%A7%D8%B2%D8%B4-%D8%B2%D8%A8%D8%A7%D9%86-%D8%B7%D8%A8%DB%8C%D8%B9%DB%8C/” type=”btn-default” size=”btn-lg”]آموزش پردازش زبان طبیعی[/button]