ژرفا — خودآموز یادگیری عمیق و مهندسی هوش مصنوعی (متوسط)

فصل ۱ از ۹

پیشرفت ترم
۰٪

ترم ۴ · زبان: از `embedding` تا `transformer`

متن به عدد

فصل ۱پیش‌نمایش رایگان

در این فصل چه یاد می‌گیری#

سه ترم با عدد و تصویر کار کردی. متن مسئلهٔ متفاوتی است: طولش ثابت نیست، و هیچ ترتیبِ عددیِ طبیعی‌ای بینِ کلمه‌ها وجود ندارد.

در این فصل دو راهِ تبدیلِ متن به عدد را می‌بینی — شمردنِ کلمه و nn.Embedding — و یک خطِ پایه می‌گیری که کلِ ترم را می‌سنجد. و همان‌جا با یک نتیجهٔ ناخوشایند روبه‌رو می‌شوی: خطِ پایه مسئله را کاملاً حل می‌کند، که یعنی باید مسئلهٔ سخت‌تری پیدا کنیم تا بفهمیم شبکه‌ها چه چیزی اضافه می‌کنند.

دو راهِ متفاوت برای تبدیلِ یک برگهٔ نوشته به یک ستونِ عدد

آخر این فصل می‌توانی:

  • متن را به شناسهٔ عددی تبدیل کنی
  • nn.Embedding را به‌کار ببری و بگویی با TF-IDF چه فرقی دارد
  • یک خطِ پایهٔ درست بگیری قبل از هر شبکه‌ای
  • بگویی چرا مسئلهٔ آسان، معیارِ بدی برای مقایسه است

قبل از شروع#

از سرنخ ترمِ ۴ فصل ۵: embedding به‌عنوان بردارِ آمادهٔ یک مدل — اینجا لایه‌ای می‌سازیم که خودش یاد می‌گیرد.

از ترمِ ۱ فصل ۴: nn.Module و شمردنِ پارامترها.

دادهٔ این ترم: دو مجموعهٔ ساختگیِ فارسی که سلولِ راه‌اندازی می‌سازد — یکی «موضوع» و یکی «ترتیب». هر دو بدونِ اینترنت و روی CPU در چند ثانیه.

📓 نوت‌بوک: نوت‌بوک این فصل را در Colab باز کن — همهٔ کدهای این فصل آماده و به‌ترتیب داخلش هست.

۱. اول نگاه کن به داده#

for text, label in list(zip(train_texts, train_labels))[:4]:
    print(f"  [{TOPIC_NAMES[label]}] {text}")

words = sorted({w for s in sentences + order_texts for w in s.split()})
print("کلمه‌های یکتا:", len(words))
print("کل توکن‌های مجموعهٔ موضوع:", sum(len(s.split()) for s in sentences))
  [آشپزی] بدون خورش ساده نتیجه خوب نمی‌شود
  [آشپزی] بدون نان خوشمزه نتیجه خوب نمی‌شود
  [سفر] هر اقامتگاه طولانی به تمرین نیاز دارد
  [برنامه‌نویسی] متغیر را پیچیده انتخاب کن نه گران
کلمه‌های یکتا: 83
کل توکن‌های مجموعهٔ موضوع: 7488

قاعدهٔ اولِ سرنخ اینجا هم برقرار است: اول داده را ببین. و در همان چهار سطر یک چیز پیداست: هر موضوع کلمه‌های مخصوصِ خودش را دارد — «خورش» فقط در آشپزی می‌آید و «اقامتگاه» فقط در سفر.

این را یادت باشد؛ در بخشِ ۴ به کارت می‌آید.

۲. راهِ اول: شمردنِ کلمه#

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression

vectorizer = TfidfVectorizer()
X_train = vectorizer.fit_transform(train_texts)
X_val = vectorizer.transform(val_texts)

baseline = LogisticRegression(max_iter=1000).fit(X_train, train_labels)
print("اندازهٔ واژگان TF-IDF:", len(vectorizer.vocabulary_))
print("دقت TF-IDF روی اعتبارسنجی:", round(baseline.score(X_val, val_labels), 4))
اندازهٔ واژگان TF-IDF: 82
دقت TF-IDF روی اعتبارسنجی: 1.0

دقتِ کامل. صد درصد.

و این خبرِ خوبی نیست — خبرِ مهمی است. یعنی این مسئله برای مقایسهٔ روش‌ها بی‌فایده است: هر مدلی که بسازی، حداکثر می‌تواند با یک خطِ پایهٔ ده‌ثانیه‌ای مساوی شود.

📏 اندازه بگیر: خطِ پایه اولین کارِ هر پروژه است، نه آخرینش — و این دقیقاً همان درسی است که در سرنخ ترمِ ۳ فصلِ ۳ گرفتی. اگر خطِ پایه مسئله را حل کرد، یا مسئله را عوض کن یا پروژه را ببند. ساختنِ یک transformer برای مسئله‌ای که TF-IDF کاملاً حلش می‌کند، مهندسی نیست — تزیین است.

ولی قبل از رفتن، به یک ناهماهنگی دقت کن. بخشِ ۱ گفت ۸۳ کلمهٔ یکتا و اینجا واژگانِ TF-IDF شد ۸۲ — در حالی که این دو عدد اصلاً نباید نزدیک باشند: آن ۸۳ از هر دو مجموعه شمرده شد و TF-IDF فقط روی دادهٔ آموزشِ مجموعهٔ «موضوع» برازش شده، که خیلی کوچک‌تر است. پس واژگانش باید محسوس کمتر می‌شد. نشد. بشماریمش.

by_space = {w for s in train_texts for w in s.split()}
lost = sorted(by_space - set(vectorizer.vocabulary_))
print("کلمه‌های آموزش، با شمارشِ فاصله:", len(by_space))
print("اندازهٔ واژگان TF-IDF          :", len(vectorizer.vocabulary_))
print("چه چیزی از قلم افتاد:", lost)
print("تکه‌های تازه:", sorted(set(vectorizer.vocabulary_) - by_space))
کلمه‌های آموزش، با شمارشِ فاصله: 79
اندازهٔ واژگان TF-IDF          : 82
چه چیزی از قلم افتاد: ['می\u200cکند', 'نمی\u200cشود', 'پیاده\u200cروی']
تکه‌های تازه: ['روی', 'شود', 'می', 'نمی', 'پیاده', 'کند']

دادهٔ آموزش ۷۹ کلمهٔ یکتا دارد — کمتر از ۸۳، همان‌طور که انتظار داشتیم. ولی واژگانِ TF-IDF از آن هم بیشتر است: ۸۲.

سه کلمه ناپدید شدند و شش تکه به‌جایشان آمد — ۷۹ منهای ۳ به‌علاوهٔ ۶ می‌شود همان ۸۲.

و آن \u200c وسطِ سه کلمهٔ اول، خودِ متهم است: نیم‌فاصله. پایتون این نویسه را نمی‌تواند نشان بدهد چون دیدنی نیست، پس در repr کدش را چاپ می‌کند — و همین لوش داد. TfidfVectorizer پیش‌فرض کلمه‌ها را با الگوی \b\w\w+\b جدا می‌کند، و نیم‌فاصله جزو \w نیست — پس «نمی‌شود» از وسط نصف می‌شود به «نمی» و «شود».

پیامدش عملی است: واژگانت بزرگ‌تر می‌شود در حالی که کلمه‌های واقعی‌ات را از دست داده‌ای، و «نمی‌شود» و «می‌شود» تکهٔ مشترکِ «شود» پیدا می‌کنند که هرگز نمی‌خواستی.

درمانش یک آرگومان است، و در بخشِ ۴ عمداً همان را به‌کار برده‌ایم: token_pattern=r"\S+"، یعنی «هرچه بینِ دو فاصله است، یک توکن». فصلِ ۶ نشان می‌دهد که این هم کاملِ ماجرا نیست و tokenizerهای واقعی کارِ ظریف‌تری می‌کنند — ولی تا آنجا، همین یک آرگومان جلوی نصف شدنِ کلمه‌های فارسی را می‌گیرد.

⚠️ مواظب باش: این تلهٔ مخصوصِ فارسی است و هیچ خطایی نمی‌دهد. روی متنِ انگلیسی الگوی پیش‌فرض درست کار می‌کند، پس در هیچ آموزشِ انگلیسی‌ای دربارهٔ آن نمی‌خوانی. هر بار که یک ابزارِ متنیِ آماده را روی فارسی می‌گذاری، اولین کارت این باشد: تعدادِ توکن‌هایش را با split() خودت مقایسه کن. دو خط کد است.

۳. راهِ دوم: nn.Embedding#

import torch
import torch.nn as nn

VOCAB = {w: i + 1 for i, w in enumerate(words)}      # صفر برای padding رزرو است

torch.manual_seed(0)
embedding = nn.Embedding(len(VOCAB) + 1, 16, padding_idx=0)

ids = torch.tensor([VOCAB[w] for w in train_texts[0].split()])
print("جمله:", train_texts[0])
print("شناسه‌ها:", ids.tolist())
print("شکل خروجی:", tuple(embedding(ids).shape))
print("پارامترهای لایه:", sum(p.numel() for p in embedding.parameters()))
جمله: بدون خورش ساده نتیجه خوب نمی‌شود
شناسه‌ها: [12, 32, 41, 59, 31, 60]
شکل خروجی: (6, 16)
پارامترهای لایه: 1344

nn.Embedding در واقع یک جدولِ جست‌وجوست: برای هر کلمه یک سطرِ ۱۶تایی نگه می‌دارد، و با دادنِ شناسه همان سطر را برمی‌گرداند. هیچ ضربِ ماتریسی‌ای در کار نیست — فقط انتخابِ سطر، که خیلی سریع‌تر است.

و مهم‌ترین تفاوتش با TF-IDF: آن سطرها پارامترند و در آموزش یاد گرفته می‌شوند. TF-IDF عددهایش را از آمارِ متن حساب می‌کند و ثابت است؛ nn.Embedding عددهایش را از مسئله‌ای که داری حلش می‌کنی می‌گیرد.

🌱 ریشه‌اش کجاست: هر سطرِ آن جدول یک بردار است — و ریشه، ترم ۷ فصل ۱ — بردار: عدد که جهت دارد دقیقاً همان تصویری را می‌سازد که این فصل به آن نیاز دارد: بردار فقط فهرستی از عدد نیست، یک نقطه در فضاست. و به همین دلیل است که در فصلِ بعد می‌شود پرسید «کدام دو کلمه به هم نزدیک‌ترند؟» — سؤالی که دربارهٔ یک ستونِ TF-IDF اصلاً معنا ندارد.

TF-IDF nn.Embedding
عددها از کجا آمارِ تکرارِ کلمه یادگیری روی مسئلهٔ تو
طولِ بردار به‌اندازهٔ واژگان (هزاران) هرچه تعیین کنی (مثلاً ۱۶)
ترتیبِ کلمه‌ها از بین می‌رود حفظ می‌شود (یک بردار به‌ازای هر جایگاه)
کلمهٔ ناشناخته نادیده گرفته می‌شود باید <unk> داشته باشی
هزینه تقریباً هیچ آموزش لازم دارد

آن padding_idx=0 هم مهم است: جمله‌ها طولِ متفاوت دارند و برای ساختنِ batch باید کوتاه‌ترها را با صفر پُر کنی. padding_idx به PyTorch می‌گوید بردارِ آن شناسه همیشه صفر بماند و گرادیان نگیرد — وگرنه مدل چیزی دربارهٔ «هیچ» یاد می‌گیرد.

۴. مسئلهٔ سختی که لازم داریم#

اگر خطِ پایه صد درصد می‌گیرد، باید مسئله‌ای پیدا کنیم که نگیرد. سلولِ راه‌اندازی یکی ساخته:

for text, label in list(zip(otr_texts, otr_labels))[:3]:
    print(f"  [کلاس {label}] {text}")
  [کلاس 5] اول ماهی بعد برنج و آخر نان
  [کلاس 2] اول زعفران بعد سبزی و آخر نان
  [کلاس 6] اول سبزی بعد برنج و آخر زعفران

برچسب، شمارهٔ کلمه‌ای است که اول آمده. و به سطرِ اول و دوم نگاه کن: هر دو «نان» را در جایگاهِ آخر دارند و «سبزی» یا «برنج» در وسط — مجموعهٔ کلمه‌ها تقریباً یکی است و برچسب کاملاً متفاوت. تنها چیزی که تفکیکشان می‌کند، ترتیب است.

from sklearn.feature_extraction.text import CountVectorizer

counter = CountVectorizer(token_pattern=r"\S+")
order_train = counter.fit_transform(otr_texts)
order_val = counter.transform(ova_texts)

order_baseline = LogisticRegression(max_iter=2000).fit(order_train, otr_labels)
print("دقت bag-of-words روی ترتیب:", round(order_baseline.score(order_val, ova_labels), 4))
print("حدس تصادفی:", round(1 / 8, 4))
دقت bag-of-words روی ترتیب: 0.1
حدس تصادفی: 0.125

۰٫۱ در برابرِ حدسِ تصادفیِ ۰٫۱۲۵ — یعنی دقیقاً هیچ.

و این شکست، شکستِ پیاده‌سازی نیست؛ ریاضی است. شمردنِ کلمه‌ها فقط می‌گوید کدام کلمه‌ها آمده‌اند، نه به چه ترتیبی. دو جمله‌ای که کلمه‌هایشان یکی است، بردارِ کاملاً یکسان می‌گیرند — پس هیچ مدلی روی این بردارها نمی‌تواند بینشان فرق بگذارد، هرچقدر هم قدرتمند باشد.

چک کن: این عدد باید نزدیکِ ۰٫۱۲۵ باشد، نه بالاتر. اگر خیلی بالاتر بود، مسئله‌ات آن‌قدر که فکر می‌کنی به ترتیب وابسته نیست — و همان اولین چیزی است که در طراحیِ یک آزمایش باید ثابتش کنی، نه فرضش.

۵. نقشهٔ این ترم#

فصل چه چیزی اضافه می‌کند
۱ (همین‌جا) متن به عدد، و دو خطِ پایه
۲ فضای embedding: معنا و سوگیری
۳ چرا مدلِ بدونِ ترتیب کم می‌آورد — و چه چیزی حلش می‌کند
۴ attention: هر کلمه به کدام کلمه نگاه کند
۵ بلوکِ کاملِ transformer
۶ tokenizer و اینکه چرا فارسی گران است
۷ آموزشِ یک مدلِ زبانیِ خیلی کوچک
۸ و ۹ fine-tune یک مدلِ آمادهٔ واقعی

و معیارِ همه‌شان همان دو عدد است که امروز گرفتیم: ۱٫۰ روی موضوع و ۰٫۱ روی ترتیب. هر چیزی که بسازیم باید در برابرِ این دو ثابت کند که ارزشش را دارد.

⚠️ مواظب باش: وسوسه می‌شوی مسئلهٔ «موضوع» را رها کنی چون آسان است. نکن. در فصلِ ۹ همان را با یک مدلِ آمادهٔ واقعی حل می‌کنیم، و آنجا صد درصدِ خطِ پایه دقیقاً همان چیزی است که باید توضیح بدهی — چون مدلی که میلیون‌ها پارامتر دارد و از یک TfidfVectorizer بهتر نیست، انتخابِ اشتباهی بوده.

🔧 اگر کار نکرد: KeyError هنگامِ ساختنِ شناسه — کلمه‌ای در VOCAB نیست؛ واژگان را از هر دو مجموعه بساز، همان‌طور که اینجا کردیم. شکلِ خروجیِ Embedding عجیب است — ورودی باید long باشد نه float، و شکلش (batch, seq). index out of range in self — شناسه‌ای بزرگ‌تر از اندازهٔ واژگان داده‌ای؛ همیشه len(VOCAB) + 1 بگذار.

🤖 از دستیارت بپرس: «چه راه‌های دیگری برای تبدیلِ متن به عدد هست؟» (one-hot، word2vec، subword را خواهی دید.) بعد این را بپرس: «کدامشان ترتیبِ کلمه‌ها را نگه می‌دارند؟» — و جوابش را با آزمایشِ بخشِ ۴ بسنج. هر روشی که خروجی‌اش یک بردارِ ثابت برای کلِ جمله باشد، ترتیب را از دست می‌دهد، و این را خودت الان اندازه گرفته‌ای.

واژه‌های تازهٔ این فصل#

کلمه تلفظ به حروف فارسی یعنی چه
token توکن کوچک‌ترین واحدِ متن برای مدل
vocabulary وکبیولری مجموعهٔ توکن‌های شناخته‌شده
nn.Embedding ان‌ان امبدینگ جدولِ بردارهای آموختنی برای هر توکن
padding پدینگ پُر کردنِ جمله‌های کوتاه‌تر تا طولِ یکسان
bag-of-words بگ-آو-وردز نمایشِ متن با شمارشِ کلمه، بدونِ ترتیب

تمرین‌ها

اول خودت فکر کن یا امتحان کن — بعد اینجا را باز کن.

در فصل بعد#

بردارهای embedding را داری، ولی هنوز تصادفی‌اند. فصلِ بعد نشان می‌دهد بعد از آموزش چه شکلی می‌شوند — کلمه‌های هم‌معنا کجای فضا می‌نشینند، چطور خوشه می‌سازند، و یک چیزِ ناخوشایند: سوگیری‌ای که در همین بردارها قابلِ اندازه‌گیری است.

به آخر این فصل رسیدی!

اگر ساختی و جواب داد، این دکمه مال توست.