مدلهای یادگیری ماشین توان پردازش مستقیم متن را ندارند و باید ورودی متنی را به بردارهای عددی تبدیل کنیم. گرچه امروزه مدلهای زبانی بزرگ و امبدینگهای چندصد میلیون پارامتری محبوبیت زیادی پیدا کردهاند، اما در بسیاری از تسکهای عملیاتی مانند تشخیص اسپم، دستهبندی موضوعی یا فیلتر لاگها، روشهای کلاسیک بهینهتر هستند.
روشهای کیسهی کلمات (Bag of Words) و TF-IDF به دلیل سرعت استنتاج بر حسب میکروثانیه، تفسیرپذیری بالا و عدم نیاز به زیرساخت پردازشی سنگین، همچنان یکی از ابزارهای اصلی مهندسی داده محسوب میشوند. در این درس، ابتدا این الگوریتمها را از صفر پیادهسازی میکنیم و سپس به نحوه استفاده استاندارد از آنها در scikit-learn میپردازیم.
کدها و محاسبات این درس کاملاً متکی به پردازنده (CPU) سیستم شخصی هستند و برای اجرای آنها به هیچ GPU یا اتصال به API خارجی نیاز نخواهید داشت؛ تنها نصب بستههای پایه پایتون از طریق pip برای اجرای آن کافی است.
اول بشمار، بعد فکر کن. TF-IDF در سال 2026 هنوز در تسکهای مشخص و محدود، مدلهای امبدینگ را شکست میدهد.
صورت مسئله
مدل به عدد نیاز دارد؛ شما فقط رشتههای متنی (String) دارید.
همهی پایپلاینهای پردازش زبان طبیعی (NLP) باید به یک پرسش پاسخ دهند: چطور جریانی از توکنها با طول متغیر را به یک بردار با اندازهی ثابت تبدیل کنیم تا کلاسیفایر بتواند آن را پردازش کند؟ اولین پاسخی که این حوزه به آن رسید، سادهترین روشی بود که کار میکرد: کلمات را بشمار و بردار بساز.
این بردار، بیشتر از هر مدل امبدینگی بار سیستمهای عملیاتیِ NLP را به دوش کشیده است: فیلترهای اسپم، دستهبندی موضوعی، تشخیص ناهنجاری در لاگها، رتبهبندی نتایج جستجو (قبل از BM25)، موج اول تحلیل احساسات و دههی اول بنچمارکهای دانشگاهی NLP. متخصصان در سال 2026 هنوز برای تسکهای کلاسیفیکیشنِ محدود، اول سراغ همین روش میروند؛ چون سریع است، تفسیرپذیری بالایی دارد و در تسکهایی که فقط «حضور کلمات» اهمیت دارد، خروجیاش اغلب با یک مدل امبدینگ 400 میلیون پارامتری فرقی نمیکند.
در این درس ابتدا کیسهی کلمات (Bag of Words) و سپس TF-IDF را از صفر پیادهسازی میکنیم. بعد میبینیم که scikit-learn چطور همین کار را در سه خط انجام میدهد؛ و در نهایت سراغ حالتهای نقصی میرویم که شما را مجبور میکنند سراغ امبدینگها بروید.
مفهوم
کیسهی کلمات (BoW) ترتیب کلمات را دور میریزد. برای هر سند، میشماریم که هر کلمهی فرهنگ لغت چند بار تکرار شده است. طول بردار برابر با اندازهی فرهنگ لغت است و جایگاه i، تعداد تکرار کلمهی i را نشان میدهد.
الگوریتم TF-IDF به خروجی BoW وزن دوباره میدهد. کلمهای که در همهی سندها آمده باشد اطلاعات مفیدی ندارد، پس وزنش را کم میکنیم. در مقابل، کلمهای که در کل اسناد کمیاب است ولی در یک سند خاص زیاد تکرار شده، حاوی سیگنال مهمی است؛ پس وزنش را بالا میبریم.
در اینجا TF بسامد واژه در سند، df بسامد سندی (تعداد اسنادی که این کلمه در آنها آمده) و N تعداد کل اسناد است. تابع log وزن کلمات همهجاحاضر را محدود نگه میدارد.
ویژگی کلیدی: هر دو روش، بردارهای خلوت (Sparse vectors) با محورهای تفسیرپذیر میسازند. میتوانید به وزنهای یک کلاسیفایرِ آموزشدیده نگاه کنید و بخوانید که کدام کلمات، سند را به سمت کدام کلاس push میکنند. چنین کاری را با امبدینگ 768 بعدی BERT هرگز نمیتوانید انجام دهید.
پیادهسازی از صفر
مرحلهی 1: ساخت فرهنگ لغت
def build_vocab(docs):
vocab = {}
for doc in docs:
for token in doc:
if token not in vocab:
vocab[token] = len(vocab)
return vocab
ورودی: لیستی از اسناد توکنایزشده (هر توکنایزر کلمهمحوری کار را راه میاندازد؛ فایل code/main.py در این درس از نسخهی سادهشدهی تبدیل به حروف کوچک استفاده میکند). خروجی: دیکشنری {word: index}. حفظ ترتیب درج (Insertion order) یعنی کلمهی اندیس 0، همان اولین کلمهای است که در سند اول دیده شده. این قرارداد متفاوت است؛ scikit-learn کلمات را بر اساس حروف الفبا مرتب میکند.
مرحلهی 2: کیسهی کلمات
def bag_of_words(docs, vocab):
matrix = [[0] * len(vocab) for _ in docs]
for i, doc in enumerate(docs):
for token in doc:
if token in vocab:
matrix[i][vocab[token]] += 1
return matrix
>>> docs = [["cat", "sat", "on", "mat"], ["cat", "cat", "ran"]]
>>> vocab = build_vocab(docs)
>>> bag_of_words(docs, vocab)
[[1, 1, 1, 1, 0], [2, 0, 0, 0, 1]]
سطرها نشاندهندهی اسناد و ستونها اندیسهای فرهنگ لغت هستند. درایهی [i][j] یعنی «کلمهی j چند بار در سند i آمده است». سند شمارهی 1 کلمهی cat را دو بار دارد چون در متن دو بار آمده؛ سند شمارهی 0 کلمهی ran را صفر بار دارد چون اصلاً در آن نیامده است.
مرحلهی 3: بسامد واژه و بسامد سندی
import math
def term_frequency(doc_bow, doc_length):
return [c / doc_length if doc_length else 0 for c in doc_bow]
def document_frequency(bow_matrix):
df = [0] * len(bow_matrix[0])
for row in bow_matrix:
for j, count in enumerate(row):
if count > 0:
df[j] += 1
return df
def inverse_document_frequency(df, n_docs):
return [math.log((n_docs + 1) / (d + 1)) + 1 for d in df]
دو ترفند هموارسازی (Smoothing) که ارزش نام بردن دارند: عبارت (n+1)/(d+1) جلوی log(x/0) را میگیرد؛ اضافهکردن +1 در انتها هم تضمین میکند کلمهای که در تمام سندها وجود دارد، همچنان IDF برابر با 1 (و نه 0) داشته باشد که با رفتار پیشفرض scikit-learn مطابقت دارد. پیادهسازیهای دیگر از فرمول خام log(N/df) استفاده میکنند؛ هر دو کار میکنند، اما نسخهی هموارشده رفتار سازگارتری دارد.
مرحلهی 4: الگوریتم TF-IDF
def tfidf(bow_matrix):
n_docs = len(bow_matrix)
df = document_frequency(bow_matrix)
idf = inverse_document_frequency(df, n_docs)
out = []
for row in bow_matrix:
length = sum(row)
tf = term_frequency(row, length)
out.append([tf_j * idf_j for tf_j, idf_j in zip(tf, idf)])
return out
>>> docs = [
... ["the", "cat", "sat"],
... ["the", "dog", "sat"],
... ["the", "cat", "ran"],
... ]
>>> vocab = build_vocab(docs)
>>> bow = bag_of_words(docs, vocab)
>>> tfidf(bow)
سه سند و پنج کلمه در فرهنگ لغت داریم (the، cat، sat، dog، ran). کلمهی the در هر سه سند آمده، پس IDF پایینی دارد. کلمهی dog فقط در یک سند آمده، پس IDF آن بالاست. در یک فرهنگ لغت واقعی با هزاران کلمه، این بردارها خلوت خواهند بود (بیشتر درایهها صفرند). حتی در این مثال اسباببازی هم کلمات تمایزبخش کاملاً به چشم میآیند.
مرحلهی 5: نرمالسازی L2 روی سطرها
def l2_normalize(matrix):
out = []
for row in matrix:
norm = math.sqrt(sum(x * x for x in row))
out.append([x / norm if norm else 0 for x in row])
return out
بدون نرمالسازی، سند طولانیتر بردار بزرگتری میگیرد و امتیاز شباهت را تسخیر میکند. نرمالسازی L2 همهی اسناد را روی یک اَبَرکرهی واحد قرار میدهد. حالا شباهت کسینوسی بین سطرها صرفاً همان ضرب داخلی است.
استفاده از ابزارهای واقعی
کتابخانهی scikit-learn نسخهی آماده برای پروداکشن را عرضه میکند.
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
docs = ["the cat sat on the mat", "the dog sat on the mat", "the cat ran"]
bow_vectorizer = CountVectorizer()
bow = bow_vectorizer.fit_transform(docs)
print(bow_vectorizer.get_feature_names_out())
print(bow.toarray())
tfidf_vectorizer = TfidfVectorizer()
tfidf = tfidf_vectorizer.fit_transform(docs)
print(tfidf.toarray().round(3))
کلاس CountVectorizer توکنایز، استخراج فرهنگ لغت و ساخت BoW را در یک فراخوانی انجام میدهد. کلاس TfidfVectorizer وزندهی IDF و نرمالسازی L2 را اضافه میکند. هر دو ماتریس خلوت برمیگردانند. برای 100 هزار سند، نسخهی متراکم (Dense) در حافظه جا نمیشود؛ پس تا زمانی که کلاسیفایر بردار متراکم نخواسته، روی فرمت ماتریس خلوت بمانید.
پیچهای تنظیمی که همهچیز را تغییر میدهند:
| آرگومان | اثر |
|---|---|
ngram_range=(1, 2) |
بایگرامها (ترکیبهای دو کلمهای) را اضافه میکند. معمولاً دقت کلاسیفیکیشن را بالا میبرد. |
min_df=2 |
کلماتی را که در کمتر از 2 سند آمدهاند حذف میکند. برای سبککردن فرهنگ لغت روی دادههای پرنویز مفید است. |
max_df=0.95 |
کلماتی را که در بیش از 95% اسناد آمدهاند حذف میکند. معادل حذف ایستواژهها (Stop words) بدون نیاز به لیست دستی و هاردکدشده. |
stop_words="english" |
لیست توکار ایستواژهها در scikit-learn. بستگی به تسک دارد؛ در تحلیل احساسات نباید کلمات منفیکننده حذف شوند. |
sublinear_tf=True |
از فرمول 1 + log(tf) به جای tf خام استفاده میکند. وقتی واژهای در یک سند خیلی تکرار میشود اثر مثبت دارد. |
جاهایی که TF-IDF هنوز برنده است (در سال 2026)
- تشخیص اسپم، برچسبگذاری موضوعی و پرچمگذاری ناهنجاری در لاگها. حضور کلمات مهم است، نه ظرافتهای معنایی.
- شرایط کمبود داده (چند صد نمونهی برچسبدار). ترکیب TF-IDF با رگرسیون لجستیک هیچ هزینهی پیشآموزشی ندارد.
- هر جایی که تأخیر (Latency) حیاتی است. ترکیب TF-IDF با یک مدل خطی در کسری از میکروثانیه جواب میدهد؛ امبدینگ یک سند از طریق یک ترنسفورمر 10 تا 100 میلیثانیه طول میکشد.
- سیستمهایی که باید پیشبینی خود را توجیه کنند. ضرایب کلاسیفایر را بررسی کنید؛ کلمات مثبت بالانشین دلیل تصمیم مدل هستند.
جاهایی که TF-IDF شکست میخورد
مشکل «نابینایی معنایی». این دو سند را در نظر بگیرید:
- "The movie was not good at all."
- "The movie was excellent."
یکی نظر منفی و دیگری مثبت است. همپوشانی TF-IDF آنها دقیقاً کلمات {the, movie, was} است. یک کلاسیفایر کیسهی کلمات باید حفظ کند که آمدن کلمهی not نزدیک good برچسب را برمیگرداند. مدل با دادهی کافی شاید این را یاد بگیرد، اما هرگز به ظرافت و زیبایی مدلی نخواهد بود که نحو زبان را میفهمد.
حالت شکست دیگر: کلمات خارج از دایرهی لغات (Out-of-vocabulary) در زمان استنتاج. مدل BoW آموزشدیده روی نظرات IMDb، اگر توکن Zoomer-approved در دادههای آموزشی نیامده باشد، هیچ ایدهای ندارد با آن چه کند. امبدینگهای زیرکلمهای (درس 04) از پس این کار برمیآیند؛ اما TF-IDF نمیتواند.
روش ترکیبی: امبدینگ با وزندهی TF-IDF
پیشفرض عملگرایانهی سال 2026 برای کلاسیفیکیشن روی دادههای متوسط: استفاده از وزنهای TF-IDF بهعنوان اتنشن روی امبدینگ کلمات.
def tfidf_weighted_embedding(doc, tfidf_scores, embedding_table, dim):
vec = [0.0] * dim
total_weight = 0.0
for token in doc:
if token not in embedding_table or token not in tfidf_scores:
continue
weight = tfidf_scores[token]
emb = embedding_table[token]
for i in range(dim):
vec[i] += weight * emb[i]
total_weight += weight
if total_weight == 0:
return vec
return [v / total_weight for v in vec]
توان معنایی را از امبدینگها میگیرید و تأکید روی کلمات نادر را از TF-IDF. کلاسیفایر روی این بردار تجمیعشده (Pooled vector) آموزش میبیند. در مسائلی مثل تحلیل احساسات، تشخیص موضوع و کلاسیفیکیشن نیت، در دامنهی زیر 50 هزار نمونهی برچسبدار، این روش از هر دوی آنها به تنهایی بهتر عمل میکند.
خروجی نهایی
این فایل را با نام outputs/prompt-vectorization-picker.md ذخیره کنید:
---
name: vectorization-picker
description: Given a text-classification task, recommend BoW, TF-IDF, embeddings, or a hybrid.
phase: 5
lesson: 02
---
You recommend a text-vectorization strategy. Given a task description, output:
1. Representation (BoW, TF-IDF, transformer embeddings, or a hybrid). Explain why in one sentence.
2. Specific vectorizer configuration. Name the library. Quote the arguments (`ngram_range`, `min_df`, `max_df`, `sublinear_tf`, `stop_words`).
3. One failure mode to test before shipping.
Refuse to recommend embeddings when the user has under 500 labeled examples unless they show evidence of semantic failure in a TF-IDF baseline. Refuse to remove stopwords for sentiment analysis (negations carry signal). Flag class imbalance as needing more than a vectorizer change.
Example input: "Classifying 30k customer support tickets into 12 categories. Most tickets are 2-3 sentences. English only. Need explainability for audit logs."
Example output:
- Representation: TF-IDF. 30k examples is not small; explainability requirement rules out dense embeddings.
- Config: `TfidfVectorizer(ngram_range=(1, 2), min_df=3, max_df=0.95, sublinear_tf=True, stop_words=None)`. Keep stopwords because category keywords sometimes are stopwords ("not working" vs "working").
- Failure to test: verify `min_df=3` does not drop rare category keywords. Run `get_feature_names_out` filtered by class and eyeball.
تمرینها
- ساده. تابع
cosine_similarity(doc_vec_a, doc_vec_b)را روی خروجی نرمالسازیشدهی L2 در TF-IDF پیادهسازی کنید. بررسی کنید که امتیاز اسناد یکسان 1.0 و اسنادی با واژگان کاملاً مجزا 0.0 شود. - متوسط. قابلیت پشتیبانی از
n-gramرا به تابعbag_of_wordsاضافه کنید. پارامترnباید شمارش را روی دنبالههایnتایی انجام دهد. بررسی کنید که اجرایn=2روی ورودی["the", "cat", "sat"]، شمارش بایگرامها را برای["the cat", "cat sat"]تولید کند. - سخت. روش ترکیبی «امبدینگ با وزندهی TF-IDF» را با بردارهای GloVe 100d بسازید (یک بار دانلود و کش کنید). دقت کلاسیفیکیشن را با TF-IDF ساده و امبدینگ میانگینگیریشدهی ساده (Mean-pooled) روی مجموعهدادهی 20 Newsgroups مقایسه کنید. گزارش دهید که هر روش در کجا برنده میشود.
اصطلاحات کلیدی
| اصطلاح | در گفتار رایج | معنای دقیق فنی |
|---|---|---|
| BoW | بردار بسامد کلمات | شمارش کلمات فرهنگ لغت در یک سند؛ ترتیب کلمات را دور میریزد. |
| TF | بسامد واژه | تعداد دفعات تکرار یک کلمه در سند، که بهدلخواه با طول سند نرمال میشود. |
| DF | بسامد سندی | تعداد اسنادی که این کلمه حداقل یک بار در آنها آمده است. |
| IDF | معکوس بسامد سندی | فرمول هموارشدهی log(N / df)؛ وزن کلماتی را که همهجا میآیند پایین میآورد. |
| بردار خلوت (Sparse vector) | اکثراً صفر | فرهنگ لغت معمولاً بین 10 تا 100 هزار کلمه دارد؛ بیشتر آنها در هر سند مشخصی غایباند. |
| شباهت کسینوسی | زاویهی بردارها | ضرب داخلی بردارهای نرمالسازیشده با L2؛ مقدار 1 یعنی کاملاً منطبق و 0 یعنی عمود بر هم (متعامد). |
برای مطالعهی بیشتر
- scikit-learn — استخراج ویژگی از متن — مستندات مرجع API، به همراه توضیحات کامل برای تکتک تنظیمات.
- Salton, G., & Buckley, C. (1988). رویکردهای وزندهی واژگان در بازیابی خودکار متن — مقالهای که TF-IDF را به مدت یک دهه به روش پیشفرض تبدیل کرد.
- «چرا TF-IDF هنوز امبدینگها را شکست میدهد» — Ashfaque Thonikkadavan (Medium) — نگاهی در سال 2026 به اینکه چرا و چه زمانی این روش قدیمی برنده است.
