بگذارید با یک مثال کامل شروع کنیم. نگاهی میاندازیم به آنچه در پشت صحنه در اثر اجرای این قطعه کد در فصل اول رخ داد:
from transformers import pipeline
classifier = pipeline("sentiment-analysis")
classifier(
[
"I've been waiting for a HuggingFace course my whole life.",
"I hate this so much!",
]
)
این خروجی را دریافت کردیم:
[{'label': 'POSITIVE', 'score': 0.9598047137260437},
{'label': 'NEGATIVE', 'score': 0.9994558095932007}]
همان طور که در در فصل اول دیدیم، این خط تولید از سه مرحله تشکیل شده است: پیشپردازش، پردازش ورودیها در مدل و پسپردازش.
به صورت خلاصه هرکدام از این مراحل را بررسی میکنیم.
پیشپردازش با توکِنایزر
مثل شبکههای عصبی دیگر، مدلهای ترنسفورمر هم نمیتوانند نوشته خام را پردازش کنند. پس اولین قدم در خط تولید ما، تبدیل نوشته خام ورودی به اعدادی است که مدل قادر به فهم آنها باشد. برای این کار از یک توکِنایزر استفاده میکنیم، که مسئولیتهای زیر را بر عهده دارد:
- شکستن نوشته به کلمات، زیرکلمات و علامتها (مانند علائم نگارشی) که به آنها توکِن میگوییم.
- انتخاب عدد صحیح معادل برای هر توکِن.
- اضافهکردن ورودیهای دیگری که ممکن است برای مدل مفید باشند.
همه مراحل این پیشپردازش باید دقیقا همان طور که قبلا هنگام تعلیم مدل انجام شده، دنبال شوند. این اطلاعات در هاب مدلها موجود است و توسط تابع from_pretrained()
از کلاس AutoTokenizer
دانلود میشود. با استفاده از نام کامل مدل که شامل نقطه تعلیم است، این تابع به صورت خودکار دادههای توکِنایزر مدل را دریافت نموده و در سیستم شما ذخیره میکند. به این ترتیب این دادهها فقط بار اولی که کد زیر را اجرا میکنید دانلود میشوند.
خط تولید تحلیل احساسات
نقطه تعلیم پیشفرضی به نام distilbert-base-uncased-finetuned-sst-2-english
دارد. صفحه توضیحات این مدل را میتوانید در اینجا مشاهده کنید. با اجرای کد زیر آن را دانلود میکنیم:
from transformers import AutoTokenizer
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
پس از دریافت توکِنایزر، میتوانیم جملات خود را مستقیماً وارد آن کنیم و دیکشنری خروجی را دریافت کنیم که آماده است تا به عنوان ورودی مدل مورد استفاده قرار گیرد! تنها کار باقی مانده، تبدیل لیست شناسههای ورودی به تِنسور است.
شما میتوانید از ترنسفورمرهای هاگینگفِیس بدون اطلاع از اینکه کدام فریمورک یادگیری ماشین در پشت صحنه درگیر میشود استفاده کنید. ممکن است از پایتورچ، تِنسورفِلو یا حتی فلَکس برای بعضی مدلها استفاده شده باشد. با این وجود، مدلهای ترسفورمر فقط تِنسورها را به عنوان ورودی قبول میکنند. اگر این بار اولی است که کلمه تِنسور را میشنوید، تصور کنید مانند آرایههای NumPy هستند. این آرایهها میتوانند عددی (تک بُعدی)، برداری (یک بُعدی)، ماتریس (دو بُعدی) یا با ابعاد بیشتر باشند. آنها در واقع تِنسور هستند و تِنسورها در فریمورکهای یادگیری ماشین رفتاری شبیه به آرایههای NumPy دارند و به همان سادگی هم ساخته میشوند.
برای مشخص کردن نوع تِنسوری که میخواهیم به عنوان خروجی دریافت کنیم (پایتورچ، تِنسورفِلو یا NumPy ساده)، از آرگومان return_tensors
استفاده میکنیم:
raw_inputs = [
"I've been waiting for a HuggingFace course my whole life.",
"I hate this so much!",
]
inputs = tokenizer(raw_inputs, padding=True, truncation=True, return_tensors="pt")
print(inputs)
هنوز لازم نیست نگران آرگومانهای padding
و truncation
باشید؛ زیرا بعدتر به آنها خواهیم پرداخت. مسئله اصلی که باید به به خاطر بسپارید، امکان دادن جمله یا آرایهای از جملهها به عنوان ورودی و مشخص کردن نوع تِنسورهای خروجی است. اگر نوع خروجی را مشخص نکنید، لیستی از لیستها را دریافت خواهید کرد.
خروجی تِنسورهای پایتورچ به این شکل است:
{
'input_ids': tensor([
[ 101, 1045, 1005, 2310, 2042, 3403, 2005, 1037, 17662, 12172, 2607, 2026, 2878, 2166, 1012, 102],
[ 101, 1045, 5223, 2023, 2061, 2172, 999, 102, 0, 0, 0, 0, 0, 0, 0, 0]
]),
'attention_mask': tensor([
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
])
}
خروجی یک دیکشنری با دو کلید input_ids
و attention_mask
است. input_ids
دو ردیف عدد صحیح (یک ردیف برای هر جمله) است که شناسههای منحصر به فرد توکِنهای هر جمله هستند. attention_mask
را بعدتر در همین فصل توضیح خواهیم داد.
گذر از مدل
میتوانیم مدل از پیش تعلیم دیده را، همانند آن چه در مورد توکِنایزر انجام شد، دانلود کنیم. ترنسوفورمرهای هاگینگفِیس کلاس AutoModel
را ارائه میدهد که آن هم تابعی به نام from_pretrained()
دارد:
from transformers import AutoModel
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModel.from_pretrained(checkpoint)
در این قطعه کد، همان نقطه تعلیمی که قبلا در خط تولید استفاده کردیم را دانلود کرده و مدلی جدید بر اساس آن میسازیم. این نقطه تعلیم احتمالا قبلا دانلود شده و در سیستم شما موجود است؛ پس نیازی به دانلود مجدد ندارد.
این معماری تنها شامل ماژول پایهٔ ترنسفورمر است: با دریافت ورودی، تنها وضعیت پنهان را در خروجی تحویل میدهد. به این وضعیتهای پنهان، فیچر هم میگوییم. برای هر ورودی مدل، برداری با بُعد بالا دریافت میکنیم که معادل «درک کلی مدل ترنسفورمر از آن ورودی» است.
نگران نباشید اگر درک این مفاهیم سخت است. همه آنها را بعدتر توضیح خواهیم داد.
با وجود آنکه وضعیتهای پنهان به خودی خود هم مفید هستند، آنها معمولا ورودی بخش دیگری از مدل به نام سَر مدل هستند. در فصل اول، میتوانستیم همه مسائل مختلف را توسط تنها یک معماری حل کنیم، و سپس خروجی را به سر متفاوتی در ادامه مدل پاس بدهیم.
بردارهای با بُعد بالا؟
خروجی ماژول Transformer
معمولا تِنسوری بزرگ است که اکثراً سه بُعد دارد:
- اندازه بتچ: تعداد رشتههای مورد پردازش در یک دسته، که در مثال ما دو عدد است.
- طول رشته: تعداد بردارهای عددی معادل هر رشته، که در مثال ما ۱۶ است.
- اندازه پنهان: ابعاد بردار نماینده هر ورودی مدل.
به خاطر همین مقدار آخر به این تِنسور «بُعد بالا» میگوییم. اندازه پنهان میتواند بسیار بزرگ باشد (معمولا ۷۶۸ برای مدلهای کوچکتر، و در مدلهای بزرگتر این عدد به ۳۰۷۲ یا بیشتر هم میرسد).
با پاس دادن ورودیهای پیشپردازش شده به مدل خود میتوانیم این تِنسور را ببینیم:
outputs = model(**inputs)
print(outputs.last_hidden_state.shape)
torch.Size([2, 16, 768])
توجه کنید که خروجیهای ترنسفورمرهای هاگینگفِیس، رفتاری شبیه namedtuple
یا دیکشنری دارند. شما میتوانید به هر عضو، با استفاده از نامش (مانند آنچه ما انجام دادیم) یا با کلیدش (outputs["last_hidden_state"]
) یا حتی اگر دقیقاً از مکان آن اطلاع دارید با اندیساش (outputs[0]
) دسترسی پیدا کنید.
سَر مدل: درک اعداد درون مدل
قسمت سَر، بردارهای بُعد بالای وضعیت پنهان را به عنوان ورودی میپذیرد و آنها را به بُعدی دیگر میبرد. سَرها معمولا از یک یا چند لایه خطی تشکیل شدهاند.
خروجی مدل ترنسفورمر، مستقیماً به سَر مدل برای پردازش پاس داده میشود. در این نمودار، مدل ترنسفورمر به لایه embeddings و لایههای بعدی آن تقسیم شده است. لایه embeddings هر شناسه ورودی در ورودی توکِنشده را به یک بردار که نماینده آن توکِن است تبدیل میکند. لایههای بعدی با دستکاری در این بردارها توسط مکانیزم توجه، شکل پایانی بردار نماینده جملات را تولید میکنند.
تعداد زیادی از معماریهای مختلف در ترنسفورمرهای هاگینگفِیس موجود است و هرکدام برای حل یک مسئله خاص طراحی شدهاند. در اینجا فهرست کوتاهی از آنها را میآوریم:
*Model
(برای دسترسی به وضعیتهای پنهان)*ForCausalLM
*ForMaskedLM
*ForMultipleChoice
*ForQuestionAnswering
*ForSequenceClassification
*ForTokenClassification
- و نمونههای دیگر در هاگینگفِیس
برای این مثال، نیازمند مدلی با سَر مخصوص دستهبندی رشتهها (برای تشخیص منفی یا مثبت بودن جملات) هستیم. پس به جای کلاس AutoModel
از کلاس AutoModelForSequenceClassification
استفاده میکنیم:
from transformers import AutoModelForSequenceClassification
checkpoint = "distilbert-base-uncased-finetuned-sst-2-english"
model = AutoModelForSequenceClassification.from_pretrained(checkpoint)
outputs = model(**inputs)
اگر نگاهی به شکل ورودیها بیاندازیم، خواهیم دید که حالا تعداد ابعاد آنها بسیار کمتر است: قسمت سَر مدل، بردارهای بُعد بالایی که قبلا دیدیم را به عنوان ورودی دریافت کرده و در خروجی خود، بردارهایی با دو عضو (یکی به ازای هر برچسب دستهبندی) تولید میکند.
print(outputs.logits.shape)
torch.Size([2, 2])
از آنجا که ما تنها دو جمله و دو برچسب ممکن داشتیم، خروجی مدل ما شکل ۲ در ۲ دارد.
پسپردازش خروجی
مقادیری که به عنوان خروجی از مدل دریافت میکنیم به خودی خود قابل درک نیستند. بگذارید نگاهی به آنها بیندازیم:
print(outputs.logits)
tensor([[-1.5607, 1.6123],
[ 4.1692, -3.3464]], grad_fn=<AddmmBackward>)
پیشبینی مدل ما برای جمله اول [-1.5607, 1.6123]
و برای جمله دوم [4.1692, -3.3464]
است. این خروجیها مقادیر آماری نیستند. به این مقادیر لوجیت میگوییم. مقادیری خام و نرمالنشده که خروجی آخرین لایه مدل هستند. برای تبدیل به مقادیر آماری باید این مقادیر را از یک لایه سافتمکس بگذرانیم. تمام ترنسفورمرهای هاگینگفِیس در خروجی لوجیت تولید میکنند زیرا معمولا تابع هزینه مورد استفاده در تعلیم مدل، آخرین تابع فعالسازی (مانند سافتمکس) را با تابع هزینه مدل (مانند آنتروپی متقابل) ترکیب میکند.
import torch
predictions = torch.nn.functional.softmax(outputs.logits, dim=-1)
print(predictions)
tensor([[4.0195e-02, 9.5980e-01],
[9.9946e-01, 5.4418e-04]], grad_fn=<SoftmaxBackward>)
حالا میببینیم که پیشبینی مدل برای جمله اول [0.0402, 0.9598]
و برای جمله دوم [0.9995, 0.0005]
است. اینها مقادیر آشنای آماری (به فرم احتمال) هستند.
برای تبدیل این مقادیر به برچسب دسته تشخیص داده شده میتوانیم از ویژگی id2label
تنظیمات مدل استفاده کنیم (در بخش بعدی بیشتر در این مورد صحبت خواهیم کرد):
model.config.id2label
{0: 'NEGATIVE', 1: 'POSITIVE'}
اکنون مشخص است که پیشبینیهای مدل از این قرار هستند:
- جمله اول: NEGATIVE: 0.0402, POSITIVE: 0.9598
- جمله دوم: NEGATIVE: 0.9995, POSITIVE: 0.0005
ما با موفقیت سه مرحله خط تولید را در اینجا نشان دادیم: پیشپردازش توسط توکِنایزرها، گذر ورودیها از مدل و پسپردازش! اکنون زمان آن فرا رسیده که به شکلی عمیقتر وارد هر یک از این مراحل شویم.
✏️ خودتان امتحان کنید! دو نوشته از خودتان (یا حتی بیشتر) را از خط تولید sentiment-analysis
بگذرانید. سپس مراحلی که در اینجا دیدیم را تکرار کنید و بررسی کنید که نتایج همان هستند!