در عصر یکهتازی مدلهای ترنسفورمر، شاید تصور کنید شبکههای کانولوشنی (CNN) و بازگشتی (RNN) منسوخ شدهاند؛ اما در واقعیت مهندسی، همیشه منابع نامحدود ابری یا کارتهای گرافیک گرانقیمت در دسترس نیستند. روشهای سنتی مثل TF-IDF یا Word2Vec ترتیب واژهها را نادیده میگرفتند و مدلهای زبانی عظیم نیز روی سختافزارهای ضعیف یا تلفنهای همراه برای کارهای سادهای چون دستهبندی متن بسیار سنگیناند.
این درس به شما نشان میدهد چطور با معماریهای سبکی مثل TextCNN و LSTM، الگوهای ترتیبی و n-gramها را با سرعت بالا پردازش کنید. مدل TextCNN به صورت کاملاً موازی روی CPU یا پردازندههای معمولی در چند دقیقه آموزش میبیند و انتخابی عالی برای ساخت خطوط مبنا (baseline) و سامانههای پردازش بلادرنگ (streaming) به شمار میرود.
کدهای این آموزش بر پایهی ماژولهای استاندارد PyTorch طراحی شدهاند و بخش اعظم تمرینها بدون نیاز به GPU قدرتمند یا پرداخت هزینههای گزاف سرورهای ابری، روی سیستم شخصی اجراپذیر هستند؛ این موضوع برای توسعهدهندگانی که با محدودیت دسترسی به زیرساختهای ابری بینالمللی مواجهاند اهمیت دوچندانی دارد.
شبکههای کانولوشنی n-gramها را یاد میگیرند. شبکههای بازگشتی به خاطر میسپارند. اتنشن (attention) جای هر دو را گرفت. با این حال، هر دو هنوز روی سختافزارهای محدود اهمیت دارند.
صورت مسئله
روشهای TF-IDF و Word2Vec بردارهایی تخت تولید میکردند که ترتیب کلمات را نادیده میگرفتند. مدلی که بر پایهی آنها آموزش میدید، نمیتوانست تفاوت dog bites man و man bites dog را تشخیص دهد؛ در حالی که گاهی پیام اصلی دقیقاً در ترتیب کلمات نهفته است.
پیش از ظهور ترنسفورمرها (transformers)، دو خانواده از معماریها این خلأ را پر کردند:
شبکههای عصبی کانولوشنی برای متن (TextCNN): کانولوشنهای یکبعدی (1D convolutions) را روی دنبالهای از امبدینگ (embedding) کلمات اعمال میکنند. فیلتری با عرض 3 در عمل یک شناساگر یادگیرندهی trigram است: روی 3 کلمه قرار میگیرد و یک امتیاز در خروجی میدهد. با چیدن فیلترهایی با عرضهای مختلف (2، 3، 4 و 5) در کنار هم، الگوهایی در مقیاسهای گوناگون شناسایی میشوند. در نهایت، با یک لایهی بیشینهاستخر (max-pool)، خروجی به بازنمایی با اندازهی ثابت تبدیل میشود؛ ساختاری تخت، موازی و سریع.
شبکههای عصبی بازگشتی (RNN ،LSTM ،GRU): توکنها را یکییکی پردازش میکنند و یک وضعیت پنهان (hidden state) نگه میدارند تا اطلاعات را رو به جلو منتقل کنند. ترتیبی هستند، حافظه دارند و ورودیهایی با طول متغیر را میپذیرند. این شبکهها از سال 2014 تا 2017 بر مدلسازی دنبالهها مسلط بودند؛ تا اینکه سروکلهی اتنشن پیدا شد.
در این درس هر دو معماری را میسازیم و سپس به نقصی اشاره میکنیم که انگیزهی خلق اتنشن شد.
مفاهیم کلیدی
مدل TextCNN (کیم، 2014): ابتدا توکنها امبد میشوند. سپس یک کانولوشن یکبعدی با عرض k فیلتر خود را روی k-gramهای متوالی از امبدینگها میلغزاند و یک نقشهی ویژگی (feature map) میسازد. عملیات max-pooling سراسری روی این نقشه، قویترین فعالسازی را برمیدارد. خروجیهای حاصل از max-pooling چند فیلتر با عرضهای مختلف به هم متصل (concatenate) و به سربرگ دستهبند (classifier head) داده میشوند.
چرا جواب میدهد؟ هر فیلتر در اصل یک n-gram یادگیرنده است. از آنجا که max-pooling نسبت به جایگاه مستقل عمل میکند (position-invariant)، عبارت «اصلاً خوب نیست» چه در ابتدای نقد بیاید و چه در میانهی آن، همان ویژگی را فعال میکند. اگر 3 عرض فیلتر با 100 فیلتر برای هر کدام داشته باشید، 300 شناساگر n-gram یادگرفتهشده در اختیار خواهید داشت. آموزش این شبکه کاملاً موازی انجام میشود و وابستگی ترتیبی ندارد.
شبکهی بازگشتی (RNN): در هر گام زمانی t، وضعیت پنهان برابر است با: h_t = f(W * x_t + U * h_{t-1} + b). مقادیر W و U و b در تمام طول زمان مشترکاند. وضعیت پنهان در زمان T خلاصهای از کل پیشوند ورودی است. برای دستهبندی، روی h_1 ... h_T عملیات تجمیع (pooling به صورت max ،mean یا برداشتن آخرین وضعیت) انجام میدهیم.
شبکههای RNN ساده با مشکل محو گرادیان (vanishing gradient) روبهرو هستند. LSTM گیتهایی اضافه میکند که تصمیم میگیرند چه چیزی فراموش شود، چه چیزی ذخیره گردد و چه مقداری در خروجی بیاید؛ به این ترتیب گرادیانها در طول دنبالههای طولانی پایدار میمانند. معماری GRU ساختار LSTM را به دو گیت ساده میکند و با پارامترهای کمتر، عملکردی مشابه ارائه میدهد.
شبکههای بازگشتی دوطرفه (Bidirectional RNNs): یک RNN را رو به جلو و دیگری را رو به عقب اجرا میکنند و وضعیتهای پنهان آنها را کنار هم میگذارند. بنابراین بازنمایی هر توکن هم بافت متنی سمت چپ را میبیند و هم سمت راست را. این ویژگی برای کارهای برچسبگذاری توکنها حیاتی است.
پیادهسازی
گام 1: پیادهسازی TextCNN در PyTorch
import torch
import torch.nn as nn
import torch.nn.functional as F
class TextCNN(nn.Module):
def __init__(self, vocab_size, embed_dim, n_classes, filter_widths=(2, 3, 4), n_filters=64, dropout=0.3):
super().__init__()
self.embed = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
self.convs = nn.ModuleList([
nn.Conv1d(embed_dim, n_filters, kernel_size=k)
for k in filter_widths
])
self.dropout = nn.Dropout(dropout)
self.fc = nn.Linear(n_filters * len(filter_widths), n_classes)
def forward(self, token_ids):
x = self.embed(token_ids).transpose(1, 2)
pooled = []
for conv in self.convs:
c = F.relu(conv(x))
p = F.max_pool1d(c, c.size(2)).squeeze(2)
pooled.append(p)
h = torch.cat(pooled, dim=1)
return self.fc(self.dropout(h))
دستور transpose(1, 2) ابعاد تانسور را از [batch, seq_len, embed_dim] به [batch, embed_dim, seq_len] تغییر میدهد؛ چون nn.Conv1d محور میانی را کانال در نظر میگیرد. خروجی حاصل از لایهی pooling فارغ از طول ورودی، همیشه اندازهای ثابت دارد.
گام 2: دستهبند LSTM
class LSTMClassifier(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, n_classes, bidirectional=True, dropout=0.3):
super().__init__()
self.embed = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
self.lstm = nn.LSTM(embed_dim, hidden_dim, batch_first=True, bidirectional=bidirectional)
factor = 2 if bidirectional else 1
self.dropout = nn.Dropout(dropout)
self.fc = nn.Linear(hidden_dim * factor, n_classes)
def forward(self, token_ids):
x = self.embed(token_ids)
out, _ = self.lstm(x)
pooled = out.max(dim=1).values
return self.fc(self.dropout(pooled))
در اینجا روی کل دنباله max-pooling میزنیم، نه اینکه فقط آخرین وضعیت را برداریم. در مسائل دستهبندی، معمولاً max-pooling بهتر از انتخاب صرفاً آخرین وضعیت پنهان عمل میکند؛ چرا که در دنبالههای طولانی، اطلاعات انتهای متن روی آخرین وضعیت مسلط میشوند.
گام 3: نمایش شهودی محو گرادیان
یک RNN ساده بدون مکانیزم گیت نمیتواند وابستگیهای دوربرد را یاد بگیرد. مسئلهای ساختگی را در نظر بگیرید: پیشبینی اینکه آیا توکن A اصلاً در دنباله ظاهر شده است یا نه. اگر A در جایگاه 1 باشد و طول دنباله 100 توکن باشد، گرادیانِ تابع زیان (loss) باید در مسیر برگشت از دلِ 99 بار ضرب پیاپی در وزن بازگشتی عبور کند. اگر مقدار این وزن کمتر از 1 باشد، گرادیان محو میشود؛ اگر بیشتر از 1 باشد، منفجر خواهد شد.
def vanishing_gradient_sim(seq_len, recurrent_weight=0.9):
import math
return math.pow(recurrent_weight, seq_len)
# At weight=0.9 over 100 steps:
# 0.9 ^ 100 ≈ 2.7e-5
# گرادیان از گام 100 به گام 1 عملاً صفر است.
شبکههای LSTM با معرفی وضعیت سلول (cell state) این مشکل را حل میکنند؛ دادهها در طول شبکه فقط از طریق عملیات جمعی جریان مییابند (گیت فراموشی مقادیر را با ضرب مقیاسبندی میکند، اما گرادیانها همچنان بدون مانع از این «بزرگراه» عبور میکنند). مدلهای GRU نیز با پارامترهای کمتر سازوکار مشابهی دارند. هر دو مدل آموزش پایداری را برای دنبالههای بیش از 100 گام فراهم میکنند.
گام 4: چرا این روشها هم کافی نبودند؟
حتی با وجود LSTMها، 3 مشکل اصلی پابرجا ماند:
- گلوگاه ترتیبی: آموزش یک RNN روی دنبالهای به طول 1000 توکن، به 1000 گام متوالیِ پیشرو و پسرو نیاز دارد. امکان موازیسازی در طول محور زمان وجود ندارد.
- بردار بافت با طول ثابت در معماریهای انکودر-دیکودر: دیکودر فقط آخرین وضعیت پنهان انکودر را میبیند که فشردهشدهی کل ورودی است؛ بنابراین در ورودیهای طولانی جزئیات از دست میروند. درس 09 مستقیماً به این موضوع میپردازد.
- سقف دقت در وابستگیهای طولانی: گرچه LSTMها بسیار بهتر از RNNهای ساده عمل میکنند، انتقال اطلاعات مشخص در فواصل بیش از 200 گام همچنان برایشان دشوار است.
اتنشن هر 3 مشکل را حل کرد. ترنسفورمرها اتصالات بازگشتی را کاملاً کنار گذاشتند. درس 10 نقطهی عطف این ماجراست.
استفاده از ابزارهای واقعی
ماژولهای nn.LSTM ،nn.GRU و nn.Conv1d در PyTorch کاملاً آمادهی استفاده در محیطهای عملیاتی هستند. کد آموزش آنها نیز استاندارد و سرراست است.
کتابخانهی Hugging Face امبدینگهای ازپیشآموزشدیدهای ارائه میدهد که میتوانید مستقیماً به عنوان لایهی ورودی متصل کنید:
from transformers import AutoModel
encoder = AutoModel.from_pretrained("bert-base-uncased")
for param in encoder.parameters():
param.requires_grad = False
class BertCNN(nn.Module):
def __init__(self, n_classes, filter_widths=(2, 3, 4), n_filters=64):
super().__init__()
self.encoder = encoder
self.convs = nn.ModuleList([nn.Conv1d(768, n_filters, kernel_size=k) for k in filter_widths])
self.fc = nn.Linear(n_filters * len(filter_widths), n_classes)
def forward(self, input_ids, attention_mask):
with torch.no_grad():
out = self.encoder(input_ids=input_ids, attention_mask=attention_mask).last_hidden_state
x = out.transpose(1, 2)
pooled = [F.max_pool1d(F.relu(conv(x)), kernel_size=conv(x).size(2)).squeeze(2) for conv in self.convs]
return self.fc(torch.cat(pooled, dim=1))
چکلیست «چه زمانی مدل با محدودیتهای مسئله همخوانی دارد»:
- استنتاج روی لبه یا دستگاه (Edge / on-device): ترکیب TextCNN با امبدینگهای GloVe بین 10 تا 100 برابر کوچکتر از ترنسفورمر است. اگر مقصد استقرار شما گوشی موبایل است، این پشته انتخاب اصلی شماست.
- دستهبندی بلادرنگ و استریمینگ (Streaming): شبکهی RNN توکنها را یکییکی پردازش میکند؛ در حالی که ترنسفورمرها به کل دنباله نیاز دارند. برای متنهای ورودیِ بلادرنگ، LSTMها هنوز دست بالا را دارند.
- مدلهای بسیار کوچک به عنوان خط مبنا (Baseline): تکرار و آزمایش سریع روی یک کار جدید؛ یک TextCNN را میتوانید ظرف 5 دقیقه روی CPU آموزش دهید.
- برچسبگذاری دنباله با دادههای محدود: معماری BiLSTM-CRF (درس 06) برای 1k تا 10k جملهی برچسبخورده در وظیفهی بازشناسی موجودیتهای نامدار (NER)، همچنان یک معماری سطح عملیاتی به شمار میرود.
برای هر کار دیگری، سراغ ترنسفورمر بروید.
تحویل خروجی
این محتوا را در مسیر outputs/prompt-text-encoder-picker.md ذخیره کنید:
---
name: text-encoder-picker
description: Pick a text encoder architecture for a given constraint set.
phase: 5
lesson: 08
---
Given constraints (task, data volume, latency budget, deploy target, compute budget), output:
1. Encoder architecture: TextCNN, BiLSTM, BiLSTM-CRF, transformer fine-tune, or "use a pretrained transformer as a frozen encoder + small head".
2. Embedding input: random init, GloVe / fastText frozen, or contextualized transformer embeddings.
3. Training recipe in 5 lines: optimizer, learning rate, batch size, epochs, regularization.
4. One monitoring signal. For RNN/CNN models: attention mechanism absence means they miss long-range deps; check per-length accuracy. For transformers: fine-tuning collapse if LR too high; check train loss.
Refuse to recommend fine-tuning a transformer when data is under ~500 labeled examples without showing that a TextCNN / BiLSTM baseline has plateaued. Flag edge deployment as needing architecture-before-everything.
تمرینها
- ساده: یک مدل TextCNN روی یک مجموعهدادهی ساختگیِ 3 کلاسه آموزش دهید (دادهها را خودتان بسازید). بررسی کنید که آیا استفاده از ترکیب عرض فیلترهای (2، 3، 4) میانگین F1 را نسبت به یک عرض تکی (3) بهتر میکند یا نه.
- متوسط: روشهای تجمیع max-pool ،mean-pool و last-state را برای دستهبند LSTM پیادهسازی کنید. آنها را روی یک مجموعهدادهی کوچک مقایسه کنید؛ ثبت کنید کدام روش برنده میشود و فرضیهی خود را دربارهی علت آن بنویسید.
- سخت: یک مدل برچسبگذار BiLSTM-CRF برای شناسایی موجودیتهای نامدار بسازید (با ترکیب درس 06 و این درس). آن را روی CoNLL-2003 آموزش دهید. نتایج را با خط مبنای CRF بهتنهایی (از درس 06) و نسخهی فاینتیونشدهی BERT مقایسه کنید. زمان آموزش، مصرف حافظه و امتیاز F1 را گزارش دهید.
اصطلاحات کلیدی
| اصطلاح | در گفتگوها چه میگویند | معنای واقعی آن |
|---|---|---|
| TextCNN | مدل CNN برای متن | پشتهای از کانولوشنهای 1D روی امبدینگ کلمات همراه با max-pool سراسری. ارائهشده توسط Kim (2014). |
| RNN | شبکهی بازگشتی | وضعیت پنهانی که در هر گام زمانی بهروزرسانی میشود: h_t = f(W x_t + U h_{t-1}). |
| LSTM | شبکهی بازگشتی مجهز به گیت | دارای گیتهای ورودی، فراموشی و خروجی به همراه یک وضعیت سلول؛ آموزش آن در طول دنبالههای طولانی پایدار میماند. |
| GRU | مدل سادهتر LSTM | دارای دو گیت به جای سه گیت؛ دقتی مشابه با تعداد پارامتر کمتر. |
| Bidirectional | دوطرفه | اتصال خروجیهای شبکهی بازگشتی رو به جلو و رو به عقب به یکدیگر. هر توکن هر دو سمت بافت متنی خود را میبیند. |
| Vanishing gradient | محو گرادیان (قطع سیگنال آموزش) | ضرب مکرر در وزنهای کوچکتر از 1 در RNNهای ساده که باعث میشود گرادیان گامهای اولیه عملاً صفر شود. |
مطالعهی بیشتر
- Kim, Y. (2014). Convolutional Neural Networks for Sentence Classification — مقالهی اصلی TextCNN؛ 8 صفحه و بسیار خواندنی.
- Hochreiter, S. and Schmidhuber, J. (1997). Long Short-Term Memory — مقالهی اصلی معرفی LSTM؛ برخلاف انتظار، بسیار شفاف و روان.
- Olah, C. (2015). Understanding LSTM Networks — نمودارهایی که درک شبکههای LSTM را برای همه ممکن ساختند.
