سرنخ — خودآموز برنامه‌نویسی برای هوش مصنوعی (مقدماتی)

فصل ۱۱ از ۱۱

پیشرفت ترم
۰٪

ترم ۱ · پایتون از صفر، داخلِ Colab

پروژه: یک ابزارِ متنیِ واقعی

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

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

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

یک آسیابِ دستیِ برش‌خورده که از یک طرف نوارِ پیوسته می‌گیرد و از طرفِ دیگر دانه‌های جدا و دسته‌بندی‌شده بیرون می‌دهد

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

  • یک متنِ خام را برای پردازش تمیز کنی
  • کلمه‌ها را بشماری و پرتکرارترین‌ها را پیدا کنی
  • یک ابزار را به چند تابعِ کوچکِ قابلِ‌آزمون تقسیم کنی
  • بگویی چرا نتیجهٔ ابزارت به تصمیم‌هایی که موقعِ تمیزکردن گرفتی وابسته است

قبل از شروع#

از فصل ۳: split()، strip()، replace() و اینکه رشته تغییرناپذیر است.

از فصل ۴: dict و get() با مقدارِ پیش‌فرض، و tuple.

از فصل ۶ و ۷: حلقه، الگوی انباشتگر، تابع با return و رد کردنِ حالتِ خاص در خطِ اول.

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

۱. قدمِ اول: تمیزکردنِ متن#

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

PUNCTUATION = "،.؟!:؛()«»\"'-—…"

def clean_text(text):
    """متن را برای شمارش آماده می‌کند: «ی» و «ک» عربی را یکدست و نشانه‌ها را به فاصله تبدیل می‌کند."""
    text = text.replace("ي", "ی").replace("ك", "ک")
    for mark in PUNCTUATION:
        text = text.replace(mark, " ")
    return text.strip()

def word_list(text):
    """فهرستِ کلمه‌های متنِ تمیزشده."""
    return clean_text(text).split()

print(word_list("سلامِ  دنیا! این یک آزمایش است."))
['سلامِ', 'دنیا', 'این', 'یک', 'آزمایش', 'است']

سه نکته در همین چند خط:

  • PUNCTUATION را با حرفِ بزرگ نوشتیم. این یک قرارداد پایتونی است: یعنی «این یک مقدارِ ثابت است، عوضش نکن».
  • نشانه‌ها را با فاصله جایگزین کردیم نه با رشتهٔ خالی. اگر خالی می‌گذاشتیم، «سلام» و «دنیا» به یک کلمهٔ چسبیده تبدیل می‌شد.
  • split() بدونِ آرگومان، فاصله‌های پشتِ‌سرِهم را هم یکی حساب می‌کند. برای همین آن دو فاصلهٔ بینِ «سلامِ» و «دنیا» کلمهٔ خالی نساخت.

چک کن: خروجی باید دقیقاً شش کلمه باشد. اگر رشتهٔ خالی ('') در فهرست دیدی، یعنی به‌جای split() از split(" ") استفاده کرده‌ای — و آن دو یکی نیستند.

⚠️ مواظب باش: «سلامِ» با کسرهٔ آخر ماند و از «سلام» جدا شمرده می‌شود. اعرابِ فارسی، نیم‌فاصله و شکل‌های مختلفِ نوشتنِ یک کلمه هر کدام یک تصمیم می‌خواهند. اینجا عمداً ساده نگهش داشتیم؛ ابزارِ حرفه‌ایِ این کار را در ترمِ ۲ فصلِ ۹ می‌بینی.

۲. قدمِ دوم: شمردن#

def count_words(words):
    """دیکشنریِ «کلمه ← تعداد تکرار»."""
    counts = {}
    for word in words:
        counts[word] = counts.get(word, 0) + 1
    return counts

print(count_words(["داده", "سرنخ", "داده"]))
{'داده': 2, 'سرنخ': 1}

قلبِ این تابع یک خط است: counts[word] = counts.get(word, 0) + 1. آن get(word, 0) دقیقاً همان چیزی است که در فصلِ ۴ دیدی — «اگر این کلمه را قبلاً ندیده‌ای، صفر حساب کن» — و بدونِ آن، اولین باری که کلمه‌ای می‌آید KeyError می‌گرفتی.

۳. قدمِ سوم: پرتکرارترین‌ها#

def top_words(counts, n=5):
    """n کلمهٔ پرتکرار، به‌صورتِ فهرستی از (کلمه، تعداد)."""
    pairs = []
    for word in counts:
        pairs.append((counts[word], word))
    pairs.sort(reverse=True)
    top = []
    for count, word in pairs[:n]:
        top.append((word, count))
    return top

print(top_words({"داده": 2, "سرنخ": 5, "مدل": 1}, n=2))
[('سرنخ', 5), ('داده', 2)]

اینجا یک ترفندِ خوب هست. برای مرتب‌کردن بر اساسِ تعداد، اول هر جفت را به‌صورتِ (تعداد، کلمه) ساختیم — با تعداد اول. چون tupleها از روی عنصرِ اولشان مقایسه می‌شوند، sort خودبه‌خود بر اساسِ تعداد مرتب می‌کند. بعد در حلقهٔ دوم جایشان را برمی‌گردانیم تا خروجی خوانا باشد.

pairs.sort(reverse=True) فهرست را جای خودش مرتب می‌کند (مثلِ append، چون فهرست تغییرپذیر است) و reverse=True یعنی از بزرگ به کوچک.

قبل از ادامه، جواب بده: اگر دو کلمه تعدادِ یکسانی داشته باشند، کدام اول می‌آید؟ جواب: وقتی عنصرِ اولِ دو tuple برابر باشد، مقایسه سراغِ عنصرِ دوم می‌رود — یعنی خودِ کلمه. و چون reverse=True روی کلِ مقایسه اثر می‌گذارد، آن کلمه‌ها هم برعکسِ ترتیبِ الفبا می‌آیند. در خروجیِ بخشِ بعد همین را می‌بینی: چهار کلمهٔ دوتکراری به‌ترتیبِ «یک، فقط، سوال، داده» آمده‌اند، که دقیقاً وارونهٔ الفباست. نکتهٔ مهم این نیست که کدام اول است؛ این است که هر بار همان ترتیب درمی‌آید — و همین چیزی است که نتیجه را تکرارپذیر می‌کند.

۴. قدمِ چهارم: سرِهم‌کردنِ گزارش#

def report(text):
    """گزارشِ کاملِ یک متن. برای متنِ خالی None برمی‌گرداند."""
    words = word_list(text)
    if not words:
        return None
    counts = count_words(words)
    total_length = 0
    for word in words:
        total_length = total_length + len(word)
    return {
        "total_words": len(words),
        "unique_words": len(counts),
        "average_length": total_length / len(words),
        "top": top_words(counts),
    }

SAMPLE = """
داده بدون سوال فقط عدد است. سوال خوب داده را به سرنخ تبدیل می‌کند.
هر سرنخ باید وارسی شود؛ سرنخ وارسی‌نشده فقط یک حدس است، نه یک نتیجه.
"""

result = report(SAMPLE)
for key in result:
    print(f"{key} = {result[key]}")
total_words = 28
unique_words = 21
average_length = 3.6785714285714284
top = [('سرنخ', 3), ('یک', 2), ('فقط', 2), ('سوال', 2), ('داده', 2)]

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

و حالتِ خاص، همان‌طور که در فصلِ ۷ یاد گرفتی، در خطِ اول رد شد:

print(report("   "))
print(report(""))
None
None

چک کن: عددِ ۲۸ را با دست بشمار — کلمه‌های متنِ نمونه را یکی‌یکی. اگر عددت فرق کرد، احتمالاً «می‌کند» را دو کلمه شمرده‌ای یا «؛» را نادیده گرفته‌ای. هر اختلافی که پیدا کنی، در واقع یک تصمیمِ تمیزکردن است که ابزارت گرفته و تو نمی‌دانستی.

📏 اندازه بگیر: average_length عددِ 3.6785714285714284 را داد. این عدد دقیق است ولی گمراه‌کننده، به دو دلیل. اول اینکه چهارده رقمِ اعشار برای «طولِ میانگینِ کلمه» بی‌معناست؛ دقتی را نشان می‌دهد که وجود ندارد. دوم و مهم‌تر: len تعدادِ نویسه را می‌شمارد، و «می‌کند» به‌خاطرِ نیم‌فاصله یک نویسهٔ نامرئی بیشتر دارد. پس این عدد به تصمیم‌های بخشِ ۱ وابسته است، نه فقط به متن. همیشه بپرس این عدد از کجا آمد — این اولین تمرینِ واقعیِ نخِ دوره است.

۵. خرابش کن، تا یاد بگیری#

سه تغییر بده و ببین چه می‌شود. هر کدام یک درسِ واقعی دارند:

  1. PUNCTUATION را خالی کن (PUNCTUATION = ""). حالا total_words چقدر عوض شد و unique_words چقدر؟ کدام‌شان بیشتر آسیب دید و چرا؟
  2. در clean_text نشانه‌ها را با "" عوض کن به‌جای " ". یک کلمهٔ جدید و عجیب در فهرستِ پرتکرارها پیدا می‌کنی؟
  3. در top_words مقدارِ reverse=True را بردار. خروجی چه می‌شود، و چرا این باگ در یک نگاه دیده نمی‌شود؟

🔧 اگر کار نکرد: TypeError: '<' not supported between instances of 'str' and 'int' یعنی موقعِ ساختنِ pairs جای کلمه و تعداد را عوض کرده‌ای. tuple وقتی عنصرِ اولش هم‌نوع نباشد نمی‌تواند مقایسه شود. ترتیب باید (تعداد، کلمه) باشد.

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

تمرین‌ها

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

در فصل بعد#

ترمِ یک تمام شد. حالا پایتون می‌نویسی، خطا را می‌خوانی، و می‌دانی نوت‌بوک کجا دروغ می‌گوید. ولی همهٔ داده‌هایی را که تا اینجا دیدی خودت با دست تایپ کردی — چهار محصول، هفت دما، یک پاراگراف.

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

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

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