فاز 5 · درس 1

پردازش متن: توکن‌سازی، ریشه‌یابی و لماتایز کردن در NLP

نوع
ساختنی
زمان
حدود 45 دقیقه
زبان‌ها
Python
پیش‌نیاز
فاز 2 · 14 (Naive Bayes)
یک چاقوی ظریف در حال برش دادن نوار کاغذی متنی به قطعات مرتب روی میز آزمایشگاهی چوبی
فهرست مطالب
  1. صورت مسئله
  2. مفهوم
  3. پیاده‌سازی از صفر
  4. گام 1: یک توکنایزر کلمات مبتنی بر Regex
  5. گام 2: ریشه‌یاب پورتر (فقط گام 1a)
  6. گام 3: یک لماتایزر مبتنی بر جدول جستجو
  7. گام 4: اتصال اجزا در قالب یک پایپ‌لاین
  8. استفاده از ابزارهای واقعی
  9. NLTK
  10. spaCy
  11. چه زمانی سراغ کدام برویم؟
  12. دو نقطهٔ شکستی که هیچ‌کس درباره‌شان هشدار نمی‌دهد
  13. خروجی این درس
  14. تمرین‌ها
  15. اصطلاحات کلیدی
  16. برای مطالعهٔ بیشتر

مدل‌های یادگیری ماشین توان خواندن متن خام را ندارند و ورودی آن‌ها اعداد ریاضی است. پردازش زبان طبیعی (NLP) پلی میان متن‌های پیوسته و ساختارهای گسسته است؛ به همین دلیل اگر مراحل نخست پیش‌پردازش شامل توکن‌سازی، ریشه‌یابی و لماتایز کردن درست طراحی نشوند، داده‌های نهایی دچار واگرایی شدید خواهند شد.

در این درس ساختار پایپ‌لاین‌های آماده‌سازی متن را بررسی می‌کنیم و ضمن پیاده‌سازی الگوریتم‌ها از پایه، کاربرد و تفاوت‌های کتابخانه‌های NLTK و spaCy را مرور خواهیم کرد. همچنین به چالش‌های پنهان پروداکشن نظیر ناسازگاری میان نسخه‌ها و اختلاف ورودی آموزش با استنتاج پرداخته می‌شود.

اگر برای راه‌اندازی و اجرای ابزارها بسته‌های پایتون مانند NLTK یا spaCy و مدل‌های زبانی آن‌ها را دانلود می‌کنید، توجه داشته باشید که دریافت بسته‌ها از مخازن خارجی مانند PyPI گاهی از داخل ایران با کندی یا قطعی مواجه می‌شود و استفاده از آینه‌های داخلی یا ابزارهای مدیریت وابستگی مناسب راهکار پایداری برای این موارد است.

زبان پیوسته است. مدل‌ها گسسته‌اند. پیش‌پردازش پل میان این دو است.

صورت مسئله

مدل نمی‌تواند جملهٔ «.The cats were running» را بخواند؛ مدل فقط اعداد صحیح را می‌فهمد.

هر سیستم پردازش زبان طبیعی (NLP) با سه پرسش یکسان آغاز می‌شود: مرز کلمه کجاست؟ ریشهٔ کلمه چیست؟ و چطور کلمات «run»، «running» و «ran» را وقتی به کار می‌آید یکی بدانیم و وقتی فایده‌ای ندارد، متفاوت در نظر بگیریم؟

اگر توکن‌سازی (Tokenization) را اشتباه انجام دهید، مدل از داده‌های بی‌ارزش یاد می‌گیرد. اگر توکنایزر شما don't را یک توکن اما do n't را دو توکن بشناسد، توزیع داده‌های آموزشی دوپاره می‌شود. اگر ریشه‌یاب (Stemmer) شما دو کلمهٔ organization و organ را به یک ریشه تبدیل کند، مدل‌سازی موضوعی (Topic modeling) نابود می‌شود. اگر لماتایزر (Lemmatizer) شما به بافت نقش دستوری کلمه نیاز داشته باشد اما آن را ارسال نکنید، فعل‌ها به‌جای اسم در نظر گرفته می‌شوند.

در این درس، این سه مرحلهٔ پیش‌پردازش را از صفر می‌سازیم؛ سپس پیاده‌سازی آن‌ها را در NLTK و spaCy بررسی می‌کنیم تا تفاوت‌ها و مصالحه‌های میانشان را به‌عینه ببینید.

مفهوم

سه عملیات؛ هر کدام با یک وظیفه و یک نقطهٔ شکست.

توکن‌سازی (Tokenization) رشتهٔ متنی را به توکن‌ها تقسیم می‌کند. واژهٔ «توکن» عمداً کلی تعریف شده است؛ زیرا میزان ریزدانگیِ مناسب به نوع کار بستگی دارد: سطح کلمه برای NLP کلاسیک، زیرکلمه (Subword) برای ترنسفورمرها، و کاراکتر برای زبان‌های بدون فاصله.

ریشه‌یابی صرفی (Stemming) پسوندها را بر اساس قواعد مشخصی حذف می‌کند؛ سریع، تهاجمی و ناآگاهانه: running -> run. و organization -> organ؛ که مورد دوم دقیقاً نقطهٔ شکست آن است.

ریشه‌یابی لغوی (Lemmatization) کلمه را با استفاده از دانش گرامری به شکل واژه‌نامه‌ایِ آن برمی‌گرداند؛ کندتر و دقیق‌تر است و به جدول جستجو یا تحلیل‌گر ساخت‌واژی (Morphological analyzer) نیاز دارد: ran -> run (باید بداند «ran» زمان گذشتهٔ «run» است). و better -> good (باید صورت‌های تفضیلی را بشناسد).

یک قاعدهٔ سرانگشتی: وقتی سرعت مهم است و می‌توانید نویز را تحمل کنید، سراغ Stemming بروید (نمایه‌سازی جستجو، دسته‌بندی کلی). وقتی معنا اهمیت دارد، از Lemmatization استفاده کنید (پاسخ‌دهی به سؤالات، جستجوی معنایی، و هر خروجی‌ای که کاربر نهایی قرار است بخواند).

فاصله ویرایشییک جفت را انتخاب کنید
EDIT DISTANCELevenshtein distance fills a table where each cell is the cheapest way to turn one prefix into another using insert, delete, and substitute. Every cell takes the minimum of its three neighbors, so the bottom-right corner is the distance for the whole strings. Tracing back the choices recovers the actual edit path, which is how spell-checkers and translation metrics align text.sittingkitten01234567112345672212345633212345443212345543223466543323
فاصله لون‌اشتاین جدولی را پر می‌کند که در آن هر خانه، کم‌هزینه‌ترین راه برای تبدیل یک پیشوند به پیشوند دیگر با استفاده از درج، حذف و جایگزینی است. هر خانه کمینهٔ 3 همسایهٔ خود را می‌گیرد، بنابراین گوشهٔ پایین سمت راست، فاصله کل دو رشته را نشان می‌دهد. ردیابی رو به عقب انتخاب‌ها، مسیر ویرایش واقعی را بازیابی می‌کند؛ این همان روشی است که غلط‌یاب‌های املایی و معیارهای ترجمه متن‌ها را تراز می‌کنند.

پیاده‌سازی از صفر

گام 1: یک توکنایزر کلمات مبتنی بر Regex

ساده‌ترین توکنایزر کاربردی، متن را بر اساس کاراکترهای غیرالفبایی-عددی می‌شکند و در عین حال علائم نگارشی را هم به‌شکل توکن‌های مستقل حفظ می‌کند. بی‌نقص نیست، نسخهٔ نهایی هم نیست، اما در یک خط اجرا می‌شود.

import re

def tokenize(text):
    return re.findall(r"[A-Za-z]+(?:'[A-Za-z]+)?|[0-9]+|[^\sA-Za-z0-9]", text)

سه الگو بر اساس اولویت: کلماتی که احتمالاً آپاستروف میانی دارند (don't، it's)؛ اعداد خالص؛ و هر کاراکتر تکیِ غیرفاصله و غیرالفبایی-عددی به‌شکل یک توکن مستقل (علائم نگارشی).

>>> tokenize("The cats weren't running at 3pm.")
['The', 'cats', "weren't", 'running', 'at', '3', 'pm', '.']

نقاط شکستی که باید به آن‌ها دقت کنید: عبارت 3pm به ['3', 'pm'] شکسته می‌شود، چون الگو بین دنباله‌ای از حروف و دنباله‌ای از ارقام جابه‌جا شده است. این رفتار برای بیشتر کارها کافی است. آدرس‌های وب، ایمیل‌ها و هشتگ‌ها همگی به مشکل می‌خورند. برای محیط پروداکشن، باید الگوهای مربوط به آن‌ها را پیش از الگوهای عمومی اضافه کنید.

گام 2: ریشه‌یاب پورتر (فقط گام 1a)

الگوریتم کامل پورتر (Porter) پنج فازِ قاعده‌محور دارد. گام 1a به‌تنهایی پرتکرارترین پسوندهای انگلیسی را پوشش می‌دهد و منطق کلی این الگو را به شما می‌آموزد.

def stem_step_1a(word):
    if word.endswith("sses"):
        return word[:-2]
    if word.endswith("ies"):
        return word[:-2]
    if word.endswith("ss"):
        return word
    if word.endswith("s") and len(word) > 1:
        return word[:-1]
    return word
>>> [stem_step_1a(w) for w in ["caresses", "ponies", "caress", "cats"]]
['caress', 'poni', 'caress', 'cat']

قواعد را از بالا به پایین بخوانید. قاعدهٔ ies -> i دلیل این است که ponies به poni تبدیل می‌شود، نه به pony. در نسخهٔ کامل پورتر، گام 1b این مشکل را برطرف می‌کند. قواعد با هم رقابت می‌کنند و اولویت با قاعده‌ای است که زودتر آمده باشد. ترتیب قواعد حتی از خود تک‌تکِ قواعد مهم‌تر است.

گام 3: یک لماتایزر مبتنی بر جدول جستجو

ریشه‌یابی لغویِ اصولی به ساخت‌واژه (Morphology) نیاز دارد. نسخهٔ آموزشی و جمع‌وجورِ آن می‌تواند از یک جدول کوچک و یک راهکار جایگزین (Fallback) استفاده کند.

LEMMA_TABLE = {
    ("running", "VERB"): "run",
    ("ran", "VERB"): "run",
    ("runs", "VERB"): "run",
    ("better", "ADJ"): "good",
    ("best", "ADJ"): "good",
    ("cats", "NOUN"): "cat",
    ("cat", "NOUN"): "cat",
    ("were", "VERB"): "be",
    ("was", "VERB"): "be",
    ("is", "VERB"): "be",
}

def lemmatize(word, pos):
    key = (word.lower(), pos)
    if key in LEMMA_TABLE:
        return LEMMA_TABLE[key]
    if pos == "VERB" and word.endswith("ing"):
        return word[:-3]
    if pos == "NOUN" and word.endswith("s"):
        return word[:-1]
    return word.lower()
>>> lemmatize("running", "VERB")
'run'
>>> lemmatize("cats", "NOUN")
'cat'
>>> lemmatize("better", "ADJ")
'good'
>>> lemmatize("watched", "VERB")
'watched'

مورد آخر نکتهٔ آموزشی کلیدی است: watched در جدول ما نیست و راهکار جایگزین ما فقط ing را پوشش می‌دهد. لماتایزر واقعی مواردی مثل ed، فعل‌های بی‌قاعده، صفت‌های تفضیلی، و جمع‌های بی‌قاعده با تغییر واکه (children -> child) را مدیریت می‌کند. به همین دلیل سیستم‌های پروداکشن از WordNet، ماژول Morphologizer در spaCy یا یک تحلیل‌گر ساخت‌واژی کامل استفاده می‌کنند.

گام 4: اتصال اجزا در قالب یک پایپ‌لاین

def preprocess(text, pos_tagger=None):
    tokens = tokenize(text)
    stems = [stem_step_1a(t.lower()) for t in tokens]
    tags = pos_tagger(tokens) if pos_tagger else [(t, "NOUN") for t in tokens]
    lemmas = [lemmatize(word, pos) for word, pos in tags]
    return {"tokens": tokens, "stems": stems, "lemmas": lemmas}

تکهٔ مفقود در این زنجیره، برچسب‌زن نقش دستوری (POS tagger) است. در درس 5 · 07 (POS Tagging) یکی از آن‌ها را می‌سازیم. فعلاً به‌طور پیش‌فرض همه‌چیز را NOUN در نظر می‌گیریم و این محدودیت را می‌پذیریم.

استفاده از ابزارهای واقعی

کتابخانه‌های NLTK و spaCy نسخه‌های آماده برای پروداکشن را عرضه کرده‌اند؛ هر کدام تنها با چند خط کد.

NLTK

import nltk
nltk.download("punkt_tab")
nltk.download("wordnet")
nltk.download("averaged_perceptron_tagger_eng")

from nltk.tokenize import word_tokenize
from nltk.stem import PorterStemmer, WordNetLemmatizer
from nltk import pos_tag

text = "The cats were running."
tokens = word_tokenize(text)
stems = [PorterStemmer().stem(t) for t in tokens]
lemmatizer = WordNetLemmatizer()
tagged = pos_tag(tokens)


def nltk_pos_to_wordnet(tag):
    if tag.startswith("V"):
        return "v"
    if tag.startswith("J"):
        return "a"
    if tag.startswith("R"):
        return "r"
    return "n"


lemmas = [lemmatizer.lemmatize(t, nltk_pos_to_wordnet(tag)) for t, tag in tagged]

تابع word_tokenize مخفف‌ها، یونیکد و حالت‌های خاصی را که رجکس سادهٔ شما جا می‌اندازد مدیریت می‌کند. کلاس PorterStemmer تمام پنج فاز را اجرا می‌کند. کلاس WordNetLemmatizer نیاز دارد که برچسب نقش دستوری از ساختار Penn Treebank در NLTK به مجموعه اختصارات WordNet تبدیل شود. کد نگاشت بالا همان بخشی است که بیشتر آموزش‌ها از رویش می‌پرند.

spaCy

import spacy

nlp = spacy.load("en_core_web_sm")
doc = nlp("The cats were running.")

for token in doc:
    print(token.text, token.lemma_, token.pos_)
The      the     DET
cats     cat     NOUN
were     be      AUX
running  run     VERB
.        .       PUNCT

کتابخانهٔ spaCy کل پایپ‌لاین را پشت دستور nlp(text) پنهان می‌کند: توکن‌سازی، برچسب‌گذاری نقش دستوری و لماتایز کردن همگی هم‌زمان اجرا می‌شوند. در مقیاس‌های بزرگ، سریع‌تر از NLTK است و خروجی پیش‌فرض دقیق‌تری دارد. مصالحه در اینجاست که نمی‌توانید اجزای داخلی آن را به‌سادگی تعویض کنید.

چه زمانی سراغ کدام برویم؟

موقعیت انتخاب مناسب
آموزش، کارهای پژوهشی، نیاز به تعویض اجزای سیستم NLTK
پروداکشن، چندزبانه، اهمیت بالای سرعت spaCy
پایپ‌لاین ترنسفورمر (در هر صورت از توکنایزر اختصاصی مدل استفاده می‌کنید) استفاده از tokenizers یا transformers و کنار گذاشتن پیش‌پردازش سنتی

دو نقطهٔ شکستی که هیچ‌کس درباره‌شان هشدار نمی‌دهد

بیشتر آموزش‌ها الگوریتم‌ها را توضیح می‌دهند و کار را تمام می‌کنند. اما دو مشکل عمده وجود دارد که پایپ‌لاین‌های پیش‌پردازش واقعی را به دردسر می‌اندازد و تقریباً هیچ جا پوشش داده نمی‌شوند:

انحراف در بازتولیدپذیری (Reproducibility drift): رفتار NLTK و spaCy در توکن‌سازی و لماتایز کردن از نسخه‌ای به نسخهٔ دیگر تغییر می‌کند. کلماتی که در spaCy 2.x به‌صورت ['do', "n't"] توکن می‌شدند، ممکن است در نسخهٔ 3.x خروجی ["don't"] بدهند. مدل شما روی یک توزیع داده آموزش دیده، اما حالا استنتاج (Inference) روی توزیع متفاوتی اجرا می‌شود. دقت مدل بی‌سر‌و‌صدا افت می‌کند و هیچ‌کس هم علتش را نمی‌فهمد. نسخهٔ کتابخانه‌ها را در requirements.txt پین (قفل) کنید. یک تست رگرسیون برای پیش‌پردازش بنویسید که توکن‌سازیِ مورد انتظار برای 20 جملهٔ نمونه را تثبیت کند و با هر ارتقای نسخه، آن را اجرا کنید.

ناهمخوانی میان آموزش و استنتاج (Training / inference mismatch): مدل را با پیش‌پردازش تهاجمی (حروف کوچک، حذف کلمات توقف، ریشه‌یابی به روش Stemming) آموزش دهید، آن را روی ورودی خام کاربر مستقر کنید و سقوط کارایی‌اش را تماشا کنید! این رایج‌ترین دلیل شکست پروژه‌های NLP در پروداکشن است. اگر در مرحلهٔ آموزش پیش‌پردازشی انجام می‌دهید، باید دقیقاً همان تابع را در زمان استنتاج هم اجرا کنید. توابع پیش‌پردازش را در قالب تابعی داخل پکیج نهایی مدل عرضه کنید، نه به‌شکل یک سلول ژوپیتر نوت‌بوک که تیم استقرار مجبور شود دوباره آن را بازنویسی کند.

خروجی این درس

یک پرامپت با قابلیت استفادهٔ مجدد که به مهندسان کمک می‌کند بدون نیاز به خواندن چندین کتاب مرجع، استراتژی پیش‌پردازش خود را انتخاب کنند.

این متن را در فایل outputs/prompt-preprocessing-advisor.md ذخیره کنید:

---
name: preprocessing-advisor
description: Recommends a tokenization, stemming, and lemmatization setup for an NLP task.
phase: 5
lesson: 01
---

You advise on classical NLP preprocessing. Given a task description, you output:

1. Tokenization choice (regex, NLTK word_tokenize, spaCy, or transformer tokenizer). Explain why.
2. Whether to stem, lemmatize, both, or neither. Explain why.
3. Specific library calls. Name the functions. Quote the POS-tag translation if NLTK is involved.
4. One failure mode the user should test for.

Refuse to recommend stemming for user-visible text. Refuse to recommend lemmatization without POS tags. Flag non-English input as needing a different pipeline.

تمرین‌ها

  1. ساده. تابع tokenize را طوری توسعه دهید که نشانی‌های اینترنتی (URL) را به‌شکل یک توکن واحد حفظ کند. آزمون: عبارت tokenize("Visit https://example.com today.") باید یک توکن واحد برای URL تولید کند.
  2. متوسط. گام 1b از الگوریتم پورتر را پیاده‌سازی کنید: اگر کلمه‌ای شامل حروف صدادار بود و به ed یا ing ختم می‌شد، این پسوندها را حذف کنید. قاعدهٔ همخوان مضاعف را نیز مدیریت کنید (hopping -> hop، نه hopp).
  3. سخت. لماتایزری بسازید که از WordNet به‌عنوان جدول جستجو استفاده کند، اما اگر کلمه‌ای در WordNet پیدا نشد، به ریشه‌یاب پورتر شما رجوع کند (Fallback). دقت آن را روی یک پیکرهٔ برچسب‌خورده در مقایسه با WordNet خالص و پورتر خالص بسنجید.

اصطلاحات کلیدی

اصطلاح آنچه دیگران می‌گویند معنای دقیق آن
توکن (Token) یک کلمه هر واحدی که مدل مصرف می‌کند؛ می‌تواند کلمه، زیرکلمه، کاراکتر یا بایت باشد.
ریشه به روش Stemming (ساقه) ریشهٔ یک کلمه نتیجهٔ حذف قاعده‌محور پسوندها؛ همیشه یک کلمهٔ واقعی در زبان نیست.
ریشه به روش Lemmatization (لِما) شکل واژه‌نامه‌ای کلمه همان صورتی که در لغت‌نامه جستجو می‌کنید؛ برای محاسبهٔ درست به بافت گرامری نیاز دارد.
برچسب نقش دستوری (POS tag) نوع کلمه در جمله دسته‌هایی مانند NOUN، VERB، ADJ؛ برای لماتایز کردن دقیق ضروری است.
ساخت‌واژه (Morphology) قواعد شکل ظاهری کلمه چگونگی تغییر فرم کلمه بر اساس زمان، شمار، حالت؛ عملکرد Lemmatization کاملاً به آن وابسته است.

برای مطالعهٔ بیشتر

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسچرا مدل زبانی قبل از آموزش به پیش‌پردازش نیاز دارد؟
  2. پیش از درسکدام عملیات پیش‌پردازش، حذف پسوند بر پایهٔ قاعده (rule-based) است؟
  3. حین درسگام 1a الگوریتم ریشه‌یاب Porter برای واژهٔ 'ponies' چه خروجی می‌دهد؟
  4. حین درسچرا تکواژشناسی (Lemmatization) معمولاً به برچسب POS نیاز دارد؟
  5. حین درسهنگام ترجمه برچسب‌های POS مجموعه‌داده Penn Treebank در NLTK به برچسب‌های WordNet، برچسبی که با 'V' شروع می‌شود به چه چیزی مپ می‌شود؟
  6. پس از درسمنظور از «عدم تطابق آموزش و استنتاج» (training/inference mismatch) در پیش‌پردازش NLP چیست؟
  7. پس از درسبه جای پیش‌پردازش کلاسیک، در پایپ‌لاین ترنسفورمر از چه ابزاری استفاده می‌کنید؟
  8. پس از درسچرا باید نسخه‌های NLTK و spaCy را در نیازمندی‌ها (requirements) قفل (pin) کرد؟

سؤالات متداول

تفاوت اصلی ریشه‌یابی صرفی (Stemming) و لغوی (Lemmatization) چیست؟

روش Stemming با حذف سریع و قاعده‌محور پسوندها کار می‌کند و ممکن است خروجی واژه‌ای واقعی نباشد؛ اما Lemmatization بر پایه گرامر و فرهنگ لغت شکل استاندارد کلمه را برمی‌گرداند که دقیق‌تر اما کندتر است.

در چه شرایطی باید بین NLTK و spaCy انتخاب کنیم؟

برای تحقیقات، آموزش و نیاز به تعویض تک‌تک اجزای پایپ‌لاین NLTK مناسب است؛ اما برای محیط پروداکشن، چندزبانه بودن و پردازش با سرعت بالا، spaCy اولویت دارد.

چرا ناسازگاری پیش‌پردازش آموزش و استنتاج باعث خرابی مدل در پروداکشن می‌شود؟

اگر متنی که مدل روی آن آموزش دیده با قوانین متفاوتی (مثل پیش‌پردازش تهاجمی) پاک‌سازی شده باشد ولی در محیط زنده ورودی خام دریافت کند، توزیع داده‌ها برهم خورده و دقت مدل به‌شدت افت می‌کند.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.