فاز 2 · درس 14

الگوریتم نایو بیز (Naive Bayes) در یادگیری ماشین

نوع
ساختنی
زمان
حدود 75 دقیقه
زبان‌ها
Python
ترازویی با دو کفه که نمادهای ریاضی قضیه بیز و کیسه‌های کلمات روی آن قرار دارند و توزیع ویژگی‌ها را دسته‌بندی می‌کنند
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. قضیه‌ی بیز (مرور سریع)
  5. فرض استقلال نایو
  6. چرا این مدل با وجود فرض غلط کار می‌کند؟
  7. گام‌به‌گام با فرمول‌های ریاضی
  8. سه گونه‌ی اصلی نایو بیز
  9. راهنمای انتخاب نسخه‌ی مناسب
  10. هموارسازی لاپلاس
  11. محاسبات در فضای لگاریتمی
  12. مقایسه‌ی نایو بیز و رگرسیون لجستیک
  13. خط لوله‌ی دسته‌بندی
  14. پیاده‌سازی از صفر
  15. مدل چندجمله‌ای (MultinomialNB)
  16. مدل گوسی (GaussianNB)
  17. آزمون عملی: دسته‌بندی متن
  18. آزمون عملی: ویژگی‌های پیوسته
  19. سرعت پیش‌بینی
  20. استفاده از ابزارهای آماده
  21. ترکیب TF-IDF با نایو بیز
  22. مدل برنولی برای متن‌های کوتاه
  23. کالیبره‌سازی احتمالات نایو بیز
  24. اشتباهات رایج
  25. تحویل نهایی
  26. نایو بیز چه زمانی شکست می‌خورد؟
  27. تمرین‌ها
  28. واژگان و اصطلاحات کلیدی
  29. منابع برای مطالعه‌ی بیشتر

دسته‌بندی متن از متداول‌ترین نیازها در محصولات نرم‌افزاری است؛ از فیلتر هرزنامه‌ها و تشخیص تیکت‌های پشتیبانی گرفته تا تحلیل احساسات نظرات کاربران. با این حال، در مسائل متنی با هزاران ویژگی تنک روبه‌رو هستیم و مدل‌های پیچیده معمولاً به داده‌های آموزشی حجیم و منابع پردازشی سنگین نیاز دارند یا دچار بیش‌برازش می‌شوند.

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

فرض «ساده‌لوحانه» از نظر ریاضی غلط است، اما در عمل کار می‌کند؛ زیبایی‌اش هم دقیقاً در همین است.

اهداف یادگیری

  • پیاده‌سازی نایو بیز چندجمله‌ای (Multinomial Naive Bayes) از صفر به‌همراه هموارسازی لاپلاس (Laplace smoothing) برای دسته‌بندی متن
  • توضیح این‌که چرا فرض استقلال نایو از نظر ریاضی غلط است، اما در عمل رتبه‌بندی درستی از کلاس‌ها ارائه می‌دهد
  • مقایسه‌ی نسخه‌های چندجمله‌ای، برنولی (Bernoulli) و گوسی (Gaussian) نایو بیز و انتخاب مدل مناسب بر اساس نوع ویژگی‌ها
  • ارزیابی نایو بیز در برابر رگرسیون لجستیک (logistic regression) روی داده‌های تنک با ابعاد بالا و توضیح موازنه‌ی بایاس-واریانس (bias-variance tradeoff) در عمل

صورت مسئله

می‌خواهید متنی را دسته‌بندی کنید: تشخیص ایمیل‌های اسپم از غیراسپم، تفکیک نظرات مثبت و منفی کاربران، یا دسته‌بندی تیکت‌های پشتیبانی. هزاران ویژگی پیش رو دارید (به‌ازای هر کلمه یکی)، درحالی‌که داده‌های آموزشی شما محدودند.

بیشتر دسته‌بندها در چنین شرایطی به زانو درمی‌آیند. رگرسیون لجستیک برای تخمین مطمئنِ هزاران وزن، به داده‌های زیادی نیاز دارد. درخت‌های تصمیم روی تک‌تک کلمات انشعاب می‌زنند و شدیداً دچار بیش‌برازش (overfitting) می‌شوند. الگوریتم KNN هم در فضای 10000 بعدی عملاً بی‌معنی است؛ چون در این ابعاد، فاصله‌ی هر نقطه از سایر نقاط تقریباً یکسان می‌شود.

نایو بیز این چالش را به‌خوبی حل می‌کند. این مدل فرضی می‌سازد که از نظر ریاضیاتی اشتباه است (این‌که به شرط مشخص بودن کلاس، همه‌ی ویژگی‌ها از یکدیگر مستقل‌اند)، با این حال در دسته‌بندی متن — به‌ویژه وقتی داده‌های آموزشی کمی دارید — از مدل‌های «باهوش‌تر» جلو می‌زند. آموزش آن تنها با یک بار مرور داده‌ها تمام می‌شود، تا میلیون‌ها ویژگی به‌راحتی مقیاس می‌پذیرد و برآوردی از احتمالات به دست می‌دهد (هرچند به دلیل فرض استقلال، این احتمالات معمولاً کالیبراسیون ضعیفی دارند).

درک این نکته که چرا فرضی نادرست به پیش‌بینی‌های خوبی ختم می‌شود، درس بنیادینی در یادگیری ماشین به شما می‌آموزد: بهترین مدل، مدلی نیست که از نظر تئوری بی‌نقص باشد؛ بلکه مدلی است که بهترین موازنه‌ی بایاس-واریانس را برای داده‌های شما رقم بزند.

مفاهیم پایه

قضیه‌ی بیز (مرور سریع)

قضیه‌ی بیز احتمال‌های شرطی را برعکس می‌کند:

P(class∣features)=P(features∣class)⋅P(class)P(features)P(\text{class} \mid \text{features}) = \frac{P(\text{features} \mid \text{class}) \cdot P(\text{class})}{P(\text{features})}

هدف ما محاسبه‌ی P(class | features) است؛ یعنی احتمال تعلق یک سند به کلاسی مشخص، با فرض حضور کلمات آن سند. این مقدار را می‌توان از این سه بخش حساب کرد:

  • P(features | class) — درست‌نمایی (likelihood): احتمال دیدن این کلمات در اسنادِ این کلاس
  • P(class) — پیشین (prior): احتمال پیشین کلاس (مثلاً به‌طور کلی چقدر ایمیل اسپم داریم؟)
  • P(features) — شواهد (evidence): مقداری که برای همه‌ی کلاس‌ها یکسان است، بنابراین در مقایسه و انتخاب کلاس برتر می‌توان آن را نادیده گرفت

کلاسی که بالاترین مقدار P(class | features) را داشته باشد، برنده است.

فرض استقلال نایو

محاسبه‌ی دقیق P(features | class) مستلزم تخمین توزیع توأم تمام ویژگی‌ها در کنار هم است. اگر دایره‌ی واژگان شما شامل 10000 کلمه باشد، باید توزیعی روی 210,0002^{10{,}000} حالت ممکن را تخمین بزنید؛ کاری که عملاً ناممکن است.

فرض ساده‌لوحانه (نایو) این است: به شرط مشخص بودن کلاس، تمام ویژگی‌ها از یکدیگر مستقل شرطی هستند.

P(w1,w2,…,wn∣class)=P(w1∣class)⋅P(w2∣class)⋯P(wn∣class)P(w_1, w_2, \ldots, w_n \mid \text{class}) = P(w_1 \mid \text{class}) \cdot P(w_2 \mid \text{class}) \cdots P(w_n \mid \text{class})

حالا به‌جای یک توزیع توأم ناممکن، کافی است nn توزیع ساده را به‌ازای هر ویژگی تخمین بزنید؛ که هر کدام از آن‌ها هم فقط با یک شمارش ساده به دست می‌آید.

این فرض مسلماً اشتباه است. در هیچ سندی کلمات «machine» و «learning» مستقل از هم ظاهر نمی‌شوند. اما دسته‌بند نیازی به احتمالات دقیق ندارد؛ بلکه فقط رتبه‌بندی درست را می‌خواهد — یعنی مشخص کند کدام کلاس بیشترین احتمال را دارد. فرض استقلال خطاهایی سیستماتیک ایجاد می‌کند، اما این خطاها روی تمام کلاس‌ها اثر مشابهی می‌گذارند؛ بنابراین ترتیب و رتبه‌ی کلاس‌ها درست باقی می‌ماند.

چرا این مدل با وجود فرض غلط کار می‌کند؟

به سه دلیل:

  1. اولویت رتبه‌بندی بر کالیبراسیون. در دسته‌بندی، فقط درست بودنِ کلاسی که بالاترین رتبه را می‌آورد مهم است. حتی اگر مدل خروجی P(spam)=0.99999P(\text{spam}) = 0.99999 بدهد ولی احتمال واقعی 0.7 باشد، باز هم دسته‌بند کلاس اسپم را درست انتخاب کرده است. ما نیازی به احتمالات واقعی نداریم؛ فقط کاندیدای پیروزِ درست را می‌خواهیم.

  2. بایاس بالا، واریانس کم. فرض استقلال، در واقع یک پیشین بسیار قوی است که قیدوبندهای سفت‌وسختی روی مدل می‌گذارد و جلوی بیش‌برازش را می‌گیرد. وقتی داده‌های آموزشی محدودند، مدلی با خطای ساختاری کم اما باثبات، بسیار بهتر از مدلی عمل می‌کند که از نظر تئوری درست ولی شدیداً ناپایدار است. این یعنی نمایش عینی موازنه‌ی بایاس-واریانس.

  3. خنثی‌شدن افزونگی ویژگی‌ها. ویژگی‌های همبسته شواهد تکراری فراهم می‌کنند. دسته‌بند این شواهد را دو بار می‌شمارد، ولی این دوباره شمردن به نفع همان کلاس درست تمام می‌شود. اگر «machine» و «learning» همیشه با هم بیایند، هر دو شاهدی برای کلاس «tech» هستند. مدل نایو بیز آن‌ها را دو بار محاسبه می‌کند، اما خوشبختانه این امتیاز مضاعف برای کلاسِ درست ثبت می‌شود.

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

گام‌به‌گام با فرمول‌های ریاضی

بیایید یک مثال ملموس را دنبال کنیم. فرض کنید دو کلاس داریم: اسپم (spam) و غیراسپم (not-spam). دایره‌ی واژگان ما شامل سه کلمه است: «free»، «money» و «meeting».

داده‌های آموزشی:

  • در ایمیل‌های اسپم: 80 بار کلمه‌ی «free»، 60 بار «money» و 10 بار «meeting» آمده است (مجموعاً 150 کلمه).
  • در ایمیل‌های غیراسپم: 5 بار کلمه‌ی «free»، 10 بار «money» و 100 بار «meeting» آمده است (مجموعاً 115 کلمه).
  • 40% ایمیل‌ها اسپم و 60% غیراسپم هستند.

با اعمال هموارسازی لاپلاس (α=1\alpha=1):

P(free∣spam)=(80+1)/(150+3)=81/153=0.529P(money∣spam)=(60+1)/(150+3)=61/153=0.399P(meeting∣spam)=(10+1)/(150+3)=11/153=0.072P(free∣not-spam)=(5+1)/(115+3)=6/118=0.051P(money∣not-spam)=(10+1)/(115+3)=11/118=0.093P(meeting∣not-spam)=(100+1)/(115+3)=101/118=0.856\begin{aligned} P(\text{free} \mid \text{spam}) &= (80 + 1) / (150 + 3) = 81/153 = 0.529 \\ P(\text{money} \mid \text{spam}) &= (60 + 1) / (150 + 3) = 61/153 = 0.399 \\ P(\text{meeting} \mid \text{spam}) &= (10 + 1) / (150 + 3) = 11/153 = 0.072 \\[6pt] P(\text{free} \mid \text{not-spam}) &= (5 + 1) / (115 + 3) = 6/118 = 0.051 \\ P(\text{money} \mid \text{not-spam}) &= (10 + 1) / (115 + 3) = 11/118 = 0.093 \\ P(\text{meeting} \mid \text{not-spam}) &= (100 + 1) / (115 + 3) = 101/118 = 0.856 \end{aligned}

ایمیل جدیدی دریافت شده که شامل این کلمات است: «free» (2 بار)، «money» (1 بار)، «meeting» (0 بار).

log⁡P(spam∣email)=log⁡(0.4)+2log⁡(0.529)+1log⁡(0.399)+0log⁡(0.072)=−0.916+2(−0.637)+(−0.919)+0=−3.109log⁡P(not-spam∣email)=log⁡(0.6)+2log⁡(0.051)+1log⁡(0.093)+0log⁡(0.856)=−0.511+2(−2.976)+(−2.375)+0=−8.838\begin{aligned} \log P(\text{spam} \mid \text{email}) &= \log(0.4) + 2 \log(0.529) + 1 \log(0.399) + 0 \log(0.072) \\ &= -0.916 + 2(-0.637) + (-0.919) + 0 \\ &= -3.109 \\[6pt] \log P(\text{not-spam} \mid \text{email}) &= \log(0.6) + 2 \log(0.051) + 1 \log(0.093) + 0 \log(0.856) \\ &= -0.511 + 2(-2.976) + (-2.375) + 0 \\ &= -8.838 \end{aligned}

کلاس اسپم با اختلاف زیاد برنده می‌شود. دو بار تکرار کلمه‌ی «free» شاهدی محکم بر اسپم بودن است. دقت کنید نیامدن کلمه‌ی «meeting» در هر دو مجموعِ لگاریتمی سهمی معادل صفر دارد (0⋅log⁡P0 \cdot \log P) — در نایو بیز چندجمله‌ای، غیبت کلمات هیچ اثری نمی‌گذارد. در مقابل، این نایو بیز برنولی است که غیبت کلمات را صراحتاً مدل می‌کند.

سه گونه‌ی اصلی نایو بیز

نایو بیز در سه نسخه ارائه می‌شود که هر کدام P(feature | class) را به شکل متفاوتی مدل می‌کنند:

نایو بیز چندجمله‌ای (Multinomial Naive Bayes)

هر ویژگی را به چشم یک شمارش می‌بیند. بهترین انتخاب برای داده‌های متنی است که در آن‌ها ویژگی‌ها فراوانی کلمات یا مقادیر TF-IDF هستند.

P(wordi∣class)=count of wordi in class+αtotal words in class+α⋅vocab_sizeP(\text{word}_i \mid \text{class}) = \frac{\text{count of } \text{word}_i \text{ in class} + \alpha}{\text{total words in class} + \alpha \cdot \text{vocab\_size}}

پارامتر alpha همان هموارسازی لاپلاس است (که در ادامه توضیح می‌دهیم). این نسخه اسب کاری و گزینه‌ی اصلی برای دسته‌بندی متن است.

نایو بیز گوسی (Gaussian Naive Bayes)

هر ویژگی را با یک توزیع نرمال مدل می‌کند و برای ویژگی‌های پیوسته بهترین انتخاب است.

P(xi∣class)=12π⋅var⋅exp⁡(−(xi−mean)22⋅var)P(x_i \mid \text{class}) = \frac{1}{\sqrt{2 \pi \cdot \text{var}}} \cdot \exp\left(-\frac{(x_i - \text{mean})^2}{2 \cdot \text{var}}\right)

به‌ازای هر ویژگی در هر کلاس، یک میانگین و یک واریانس مجزا در نظر گرفته می‌شود. این روش زمانی عالی جواب می‌دهد که ویژگی‌ها درون هر کلاس واقعاً توزیعی زنگوله‌ای داشته باشند.

نایو بیز برنولی (Bernoulli Naive Bayes)

هر ویژگی را به‌صورت باینری (حضور یا غیبت) مدل می‌کند و برای متن‌های کوتاه یا بردارهای ویژگی دودویی ایده‌آل است.

P(wordi∣class)=docs in class containing wordi+αtotal docs in class+2αP(\text{word}_i \mid \text{class}) = \frac{\text{docs in class containing } \text{word}_i + \alpha}{\text{total docs in class} + 2 \alpha}

برخلاف نوع چندجمله‌ای، مدل برنولی غیبت یک کلمه را جریمه می‌کند. اگر کلمه‌ی «free» معمولاً در اسپم بیاید اما در ایمیل فعلی نباشد، برنولی آن را شاهدی علیه اسپم بودن در نظر می‌گیرد.

راهنمای انتخاب نسخه‌ی مناسب

نسخه نوع ویژگی بهترین کاربرد مثال
چندجمله‌ای (Multinomial) شمارش یا بسامد دسته‌بندی متن، کیسه‌ی کلمات (bag-of-words) تشخیص ایمیل اسپم، دسته‌بندی موضوعی
گوسی (Gaussian) مقادیر پیوسته داده‌های جدولی با ویژگی‌های شبه‌نرمال دسته‌بندی گل‌های زنبق (Iris)، داده‌های حسگرها
برنولی (Bernoulli) باینری (0 و 1) متن‌های کوتاه، بردارهای ویژگی باینری پیامک‌های اسپم، ویژگی‌های مبتنی بر حضور/غیبت

هموارسازی لاپلاس

اگر کلمه‌ای در داده‌های آزمون ظاهر شود که هرگز در داده‌های آموزشی یک کلاس نیامده باشد، چه پیش می‌آید؟

بدون هموارسازی: P(word | class) = 0/N = 0. ضرب این صفر در کل عبارت، مقدار کل P(class | features) را صفر می‌کند؛ فارغ از این‌که کلمات دیگر چه شواهدی آورده باشند. یعنی تنها یک کلمه‌ی نادیده تمام پیش‌بینی را نابود می‌کند، بدون توجه به این‌که سایر شواهد چقدر محکم از آن کلاس حمایت می‌کنند.

هموارسازی لاپلاس مقدار اندکی به نام alpha (معمولاً 1) را به شمارشِ هر ویژگی اضافه می‌کند:

P(wordi∣class)=count(wordi,class)+αtotal_words_in_class+α⋅vocab_sizeP(\text{word}_i \mid \text{class}) = \frac{\text{count}(\text{word}_i, \text{class}) + \alpha}{\text{total\_words\_in\_class} + \alpha \cdot \text{vocab\_size}}

با در نظر گرفتن α=1\alpha=1، هر کلمه حداقل احتمال بسیار ناچیزی می‌گیرد. بنابراین کلمه‌ای ناشناخته مانند «discombobulate» در ایمیل جدید، دیگر شانس اسپم بودن را یک‌باره صفر نمی‌کند. این هموارسازی تفسیری بیزی هم دارد: معادلِ قرار دادن یک پیشین یکنواخت دیریشله (Dirichlet prior) روی توزیع کلمات است.

مقادیر بالاترِ alpha هموارسازی قوی‌تری ایجاد می‌کنند (توزیع‌ها یکنواخت‌تر می‌شوند) و مقادیر پایین‌تر نشان می‌دهند مدل بیشتر به خود داده‌ها اعتماد دارد. آلفا ابرپارامتری است که باید آن را تنظیم کنید.

اثر مقادیر مختلف آلفا:

مقدار Alpha اثر زمان استفاده
0.001 تقریباً بدون هموارسازی، اعتماد کامل به داده داده‌های آموزشی بسیار زیاد، بدون احتمال برخورد با ویژگی‌های ندیده
0.1 هموارسازی ملایم مجموعه‌داده‌های آموزشی بزرگ
1.0 هموارسازی استاندارد لاپلاس نقطه‌ی شروع پیش‌فرض
10.0 هموارسازی شدید، تخت کردن توزیع‌ها داده‌های آموزشی بسیار کم، با احتمال بالای مواجهه با ویژگی‌های ندیده

محاسبات در فضای لگاریتمی

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

راهکار: کار در فضای لگاریتمی. به‌جای ضرب احتمال‌ها، لگاریتم آن‌ها را با هم جمع می‌کنیم:

log⁡P(class∣x1,x2,…,xn)=log⁡P(class)+∑ilog⁡P(xi∣class)\log P(\text{class} \mid x_1, x_2, \ldots, x_n) = \log P(\text{class}) + \sum_i \log P(x_i \mid \text{class})

این کار پیش‌بینی را به یک ضرب داخلی (dot product) تبدیل می‌کند:

log_scores = X @ log_feature_probs.T + log_class_priors
prediction = argmax(log_scores)

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

مقایسه‌ی نایو بیز و رگرسیون لجستیک

هر دو، دسته‌بندهایی خطی برای متن هستند؛ اما تفاوت در ماهیت چیزی است که مدل می‌کنند:

ویژگی نایو بیز رگرسیون لجستیک
نوع مدل زایشی یا مولد (مدل‌سازی P(X∣Y)P(X \mid Y){}) تمایزگرا (مدل‌سازی P(Y∣X)P(Y \mid X){})
آموزش شمارش بسامدها بهینه‌سازی تابع زیان (loss)
داده‌های اندک عملکرد بهتر (کمکِ پیشین قوی) ضعیف‌تر (داده ناکافی برای تخمین وزن‌ها)
داده‌های حجیم ضعیف‌تر (اثر منفی فرض اشتباه) بهتر (مرز تصمیم‌گیری انعطاف‌پذیر)
ویژگی‌ها فرض بر استقلال دارد همبستگی‌ها را مدیریت می‌کند
سرعت یک‌گذره، بسیار سریع بهینه‌سازی تکرارشونده
کالیبراسیون احتمالات با کالیبراسیون ضعیف احتمالات دقیق‌تر و مطمئن‌تر

یک قاعده‌ی سرانگشتی: کار را با نایو بیز شروع کنید. اگر داده‌های کافی در اختیار داشتید و رشد دقت نایو بیز متوقف شد، به سراغ رگرسیون لجستیک بروید.

خط لوله‌ی دسته‌بندی

متن خامتوکن‌سازیساخت واژگانشمارش بسامد کلماتاعمال هموارسازیمحاسبه لگاریتم احتمالپیش‌بینی: argmax P کلاس با شرط کلمات

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

log⁡P(class∣features)=log⁡P(class)+∑ilog⁡P(featurei∣class)\log P(\text{class} \mid \text{features}) = \log P(\text{class}) + \sum_i \log P(\text{feature}_i \mid \text{class})
بیز سادهمقدار مشاهده‌شده را بکشید
NAIVE BAYESEach class has its own bell curve over the feature. The orange line is what you observed; the dots are how likely each class is to produce it. Bayes turns those likelihoods, weighted by the priors, into the posterior bar, the probability the point belongs to class B.
هر کلاس منحنی زنگوله‌ایِ مخصوص به خود را روی این ویژگی دارد. خط نارنجی همان چیزی است که مشاهده کرده‌اید؛ نقطه‌ها نشان می‌دهند تولید این مقدار توسط هر کلاس چقدر محتمل است. قضیه بیز این درست‌نمایی‌ها را با وزن‌دهی بر اساس احتمالات پیشین، به نوار احتمال پسین تبدیل می‌کند؛ یعنی احتمال اینکه نقطه متعلق به کلاس B باشد.

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

کد فایل code/naive_bayes.py هر دو کلاس MultinomialNB و GaussianNB را از صفر پیاده کرده است.

مدل چندجمله‌ای (MultinomialNB)

مراحل پیاده‌سازی از صفر:

  1. متد fit(X, y): به‌ازای هر کلاس، بسامد هر ویژگی شمرده می‌شود. هموارسازی لاپلاس اعمال می‌گردد. لگاریتم احتمالات محاسبه می‌شود. پیشین‌های کلاس‌ها (لگاریتم فراوانی هر کلاس) نیز ذخیره می‌شوند.

  2. متد predict_log_proba(X): برای هر نمونه، مقدار log⁡P(class)\log P(\text{class}) به‌علاوه‌ی مجموع log⁡P(featurei∣class)\log P(\text{feature}_i \mid \text{class}) روی تمام کلاس‌ها محاسبه می‌شود. این کار با یک ضرب ماتریسی انجام می‌گیرد: X @ log_probs.T + log_priors.

  3. متد predict(X): کلاسی که بیشترین لگاریتم احتمال را داشته باشد برمی‌گرداند.

class MultinomialNB:
    def __init__(self, alpha=1.0):
        self.alpha = alpha

    def fit(self, X, y):
        classes = np.unique(y)
        n_classes = len(classes)
        n_features = X.shape[1]

        self.classes_ = classes
        self.class_log_prior_ = np.zeros(n_classes)
        self.feature_log_prob_ = np.zeros((n_classes, n_features))

        for i, c in enumerate(classes):
            X_c = X[y == c]
            self.class_log_prior_[i] = np.log(X_c.shape[0] / X.shape[0])
            counts = X_c.sum(axis=0) + self.alpha
            self.feature_log_prob_[i] = np.log(counts / counts.sum())

        return self

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

مدل گوسی (GaussianNB)

برای ویژگی‌های پیوسته، میانگین و واریانس را به‌ازای هر کلاس و هر ویژگی تخمین می‌زنیم:

class GaussianNB:
    def __init__(self):
        pass

    def fit(self, X, y):
        classes = np.unique(y)
        self.classes_ = classes
        self.means_ = np.zeros((len(classes), X.shape[1]))
        self.vars_ = np.zeros((len(classes), X.shape[1]))
        self.priors_ = np.zeros(len(classes))

        for i, c in enumerate(classes):
            X_c = X[y == c]
            self.means_[i] = X_c.mean(axis=0)
            self.vars_[i] = X_c.var(axis=0) + 1e-9
            self.priors_[i] = X_c.shape[0] / X.shape[0]

        return self

پیش‌بینی با تابع چگالی احتمال گوسی (Gaussian PDF) به‌ازای هر ویژگی انجام می‌شود و مقادیر در بین ویژگی‌ها ضرب می‌شوند (که در فضای لگاریتمی جمع خواهند شد).

آزمون عملی: دسته‌بندی متن

کد، یک مجموعه‌داده‌ی مصنوعی به سبک کیسه‌ی کلمات تولید می‌کند که نمایانگر دو کلاس است (مقاله‌های فناوری در برابر مقاله‌های ورزشی). هر کلاس توزیع متفاوتی از بسامد کلمات دارد و مدل MultinomialNB با شمارش کلمات، آن‌ها را دسته‌بندی می‌کند.

سازوکار تولید این داده‌های مصنوعی چنین است: 200 «کلمه» (ستون ویژگی) می‌سازیم. کلمات 0 تا 39 بسامد بالایی در مقالات فناوری و بسامد پایینی در ورزش دارند. کلمات 80 تا 119 در ورزش پرتکرار و در فناوری کم‌تکرارند. کلمات 40 تا 79 هم در هر دو دسته با بسامد متوسط ظاهر می‌شوند. بدین ترتیب سناریویی واقعی شبیه‌سازی می‌شود که در آن برخی کلمات شاخص‌های قوی کلاس هستند و مابقی حکم نویز را دارند.

آزمون عملی: ویژگی‌های پیوسته

کد، داده‌هایی شبیه مجموعه‌داده‌ی زنبق (Iris) می‌سازد (3 کلاس، 4 ویژگی، خوشه‌های گوسی). مدل GaussianNB بر مبنای میانگین و واریانسِ هر کلاس، داده‌ها را دسته‌بندی می‌کند. هر کلاس یک مرکز (بردار میانگین) و یک پراکندگی (واریانس) مجزا دارد که کاملاً شبیه داده‌های واقعی است؛ جایی که اندازه‌گیری‌ها در دسته‌های مختلف به‌طور نظام‌مند فرق دارند.

بخش‌های دیگر این کد شامل این موارد است:

  • مقایسه‌ی هموارسازی: آموزش MultinomialNB با مقادیر مختلف آلفا برای مشاهده‌ی اثر شدت هموارسازی بر دقت مدل.
  • آزمایش حجم داده‌ی آموزشی: بررسی رشد دقت نایو بیز همگام با افزایش نمونه‌های آموزشی از 20 به 1600 نمونه. نایو بیز حتی با نمونه‌های بسیار کم به دقتی مطلوب می‌رسد؛ مزیتی که دستاورد اصلی آن محسوب می‌شود.
  • ماتریس درهم‌ریختگی (Confusion Matrix): محاسبه‌ی دقت (precision)، فراخوانی (recall) و امتیاز F1 به‌تفکیک هر کلاس برای کشف نقاط ضعف مدل.

سرعت پیش‌بینی

پیش‌بینی در نایو بیز معادل یک ضرب ماتریسی است. برای nn نمونه با dd ویژگی و kk کلاس:

  • مدل چندجمله‌ای: یک ضرب ماتریسی (n×d)@(d×k)=O(n⋅d⋅k)(n \times d) \mathbin{@} (d \times k) = O(n \cdot d \cdot k)
  • مدل گوسی: n⋅kn \cdot k بار ارزیابی تابع چگالی احتمال گوسی، هر بار روی dd ویژگی = O(n⋅d⋅k)O(n \cdot d \cdot k)

هر دو با تمام ابعاد مسئله رابطه‌ای خطی دارند. این را مقایسه کنید با الگوریتم KNN (که نیازمند محاسبه‌ی فاصله تا تک‌تک نقاط آموزشی است) یا SVM با کرنل RBF (که باید کرنل را در برابر تک‌تک بردارهای پشتیبان محاسبه کند). نایو بیز در زمان پیش‌بینی چندین مرتبه از آن‌ها سریع‌تر است.

استفاده از ابزارهای آماده

با استفاده از scikit-learn، اجرای هر دو مدل در یک خط کد خلاصه می‌شود:

from sklearn.naive_bayes import GaussianNB, MultinomialNB

gnb = GaussianNB()
gnb.fit(X_train, y_train)
print(f"GaussianNB accuracy: {gnb.score(X_test, y_test):.3f}")

mnb = MultinomialNB(alpha=1.0)
mnb.fit(X_train_counts, y_train)
print(f"MultinomialNB accuracy: {mnb.score(X_test_counts, y_test):.3f}")

برای دسته‌بندی متن با scikit-learn:

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline

text_clf = Pipeline([
    ("vectorizer", CountVectorizer()),
    ("classifier", MultinomialNB(alpha=1.0)),
])

text_clf.fit(train_texts, train_labels)
accuracy = text_clf.score(test_texts, test_labels)

کد داخل naive_bayes.py پیاده‌سازیِ دست‌نویس را با scikit-learn روی داده‌های یکسان مقایسه می‌کند تا از درستی آن اطمینان حاصل شود.

ترکیب TF-IDF با نایو بیز

شمارش خام کلمات، به ازای هر بار تکرار وزن یکسانی به همه‌ی واژه‌ها می‌دهد. اما واژه‌های پرکاربردی مثل «the» و «is» در همه‌ی کلاس‌ها مکرراً ظاهر می‌شوند و هیچ اطلاعات مفیدی ندارند. معیار TF-IDF (بسامد ترم - عکس بسامد سند) وزن کلمات رایج را کم می‌کند و به کلمات کمیاب و تمایزبخش وزن بالاتری می‌دهد.

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline

text_clf = Pipeline([
    ("tfidf", TfidfVectorizer()),
    ("classifier", MultinomialNB(alpha=0.1)),
])

مقادیر TF-IDF نامنفی هستند، پس بدون مشکل در MultinomialNB کار می‌کنند. ترکیب TF-IDF با MultinomialNB یکی از قدرتمندترین خطوط پایه (baselines) در دسته‌بندی متن است. این ترکیب روی مجموعه‌داده‌هایی با کمتر از 10000 نمونه‌ی آموزشی، در بسیاری از مواقع مدل‌های پیچیده‌تر را شکست می‌دهد.

مدل برنولی برای متن‌های کوتاه

برای متن‌های کوتاه (مانند توییت‌ها، پیامک‌ها یا پیام‌های چت)، مدل BernoulliNB می‌تواند بهتر از MultinomialNB عمل کند. متن‌های کوتاه واژه‌های کمی دارند، بنابراین بسامد کلمات که MultinomialNB بر آن استوار است پر از نویز خواهد بود. مدل برنولی فقط به حضور یا غیبت واژه توجه دارد که در متن‌های کوتاه شاخص قابل‌اعتماد‌تری است.

from sklearn.naive_bayes import BernoulliNB
from sklearn.feature_extraction.text import CountVectorizer

text_clf = Pipeline([
    ("vectorizer", CountVectorizer(binary=True)),
    ("classifier", BernoulliNB(alpha=1.0)),
])

فلگ binary=True در CountVectorizer همه‌ی مقادیر شمارش را به 0 یا 1 تبدیل می‌کند. بدون آن هم مدل برنولی کار خواهد کرد، اما مقادیری غیر از 0 و 1 می‌بیند که با هدف طراحی آن همخوانی ندارد.

کالیبره‌سازی احتمالات نایو بیز

احتمالات نایو بیز کالیبراسیون ضعیفی دارند. وقتی این مدل پیش‌بینی می‌کند P(spam)=0.95P(\text{spam}) = 0.95، شاید احتمال واقعی فقط 0.7 باشد. اگر به احتمالات قابل‌اعتماد نیاز دارید (مثلاً برای تعیین یک آستانه‌ی مشخص یا ترکیب با خروجی سایر مدل‌ها)، از ابزار CalibratedClassifierCV در scikit-learn استفاده کنید:

from sklearn.calibration import CalibratedClassifierCV

calibrated_nb = CalibratedClassifierCV(MultinomialNB(), cv=5, method="sigmoid")
calibrated_nb.fit(X_train, y_train)
proba = calibrated_nb.predict_proba(X_test)

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

اشتباهات رایج

  1. ویژگی‌های با مقادیر منفی. مدل MultinomialNB نیازمند ویژگی‌های نامنفی است. اگر مقادیر منفی دارید (مثلاً با تنظیماتی خاص در TF-IDF یا بعد از استانداردسازی ویژگی‌ها)، به‌جای آن از GaussianNB استفاده کنید، یا مقادیر را شیفت دهید تا همه مثبت شوند.

  2. ویژگی‌های با واریانس صفر. مدل GaussianNB مقادیر را بر واریانس تقسیم می‌کند. اگر یک ویژگی درون یک کلاس واریانس صفر داشته باشد (یعنی همه‌ی مقادیر یکسان باشند)، محاسبه‌ی احتمال با خطا روبه‌رو می‌شود. در کدهای ما برای حل این مسئله مقدار اندکی (1e-9) به تمام واریانس‌ها اضافه شده است.

  3. عدم تعادل کلاسی (Class imbalance). اگر 99% از ایمیل‌ها غیراسپم باشند، احتمال پیشین P(not-spam)=0.99P(\text{not-spam}) = 0.99 چنان سنگین می‌شود که اثر درست‌نماییِ ویژگی‌ها را کاملاً خنثی می‌کند. می‌توانید مقادیر پیشین را دستی بدهید یا از پارامتر class_prior در scikit-learn استفاده کنید.

  4. مقیاس‌بندی ویژگی‌ها (Feature scaling). مدل چندجمله‌ای نیازی به مقیاس‌بندی ندارد (چون بر مبنای شمارش کار می‌کند). مدل گوسی هم بی‌نیاز از آن است (چون آماره‌های هر ویژگی را جداگانه برآورد می‌کند). این ویژگی مزیت بزرگی در برابر رگرسیون لجستیک و SVM است که حساسیت بالایی به مقیاس ویژگی‌ها دارند.

تحویل نهایی

خروجی‌های این درس عبارتند از:

  • outputs/skill-naive-bayes-chooser.md — یک شیوه‌نامه‌ی تصمیم‌گیری برای انتخاب نسخه‌ی مناسب نایو بیز
  • code/naive_bayes.py — پیاده‌سازی MultinomialNB و GaussianNB از صفر، همراه با مقایسه با scikit-learn

نایو بیز چه زمانی شکست می‌خورد؟

شکست نایو بیز زمانی رقم می‌خورد که فرض استقلال نه فقط احتمالات، بلکه رتبه‌بندی صحیح کلاس‌ها را هم به هم بریزد. این وضعیت در شرایط زیر رخ می‌دهد:

  1. اثر متقابل شدید میان ویژگی‌ها. اگر کلاسی به ترکیب دو ویژگی وابسته باشد و نه حضور هر کدام به‌تنهایی (مشابه الگوی XOR)، نایو بیز آن را کاملاً نادیده خواهد گرفت. هر ویژگی به‌تنهایی هیچ شاهدی فراهم نمی‌کند و نایو بیز هم قادر به ترکیب غیرخطی آن‌ها نیست.

  2. ویژگی‌های شدیداً همبسته با شواهد متناقض. اگر ویژگی A بگوید «اسپم» و ویژگی B بگوید «غیراسپم»، اما A و B همبستگی کاملی داشته باشند (در واقعیت همیشه با هم رخ دهند)، نایو بیز گمان می‌کند شواهد متناقضی دریافت کرده است؛ درحالی‌که تناقضی در کار نیست.

  3. مجموعه‌داده‌های آموزشی بسیار حجیم. وقتی داده‌ها بسیار زیاد باشند، مدل‌های تمایزگرا مثل رگرسیون لجستیک مرز واقعی تصمیم را یاد می‌گیرند و نایو بیز را پشت سر می‌گذارند. در اینجا همان فرض استقلالی که در داده‌های اندک عصای دست بود، مانع پیشرفت مدل می‌شود.

در عمل، این حالت‌های شکست در دسته‌بندی متن به‌ندرت مشکل‌ساز می‌شوند؛ چراکه ویژگی‌های متنی بسیار پرشمار و به‌تنهایی ضعیف‌اند و خطاهای ناشی از فرض استقلال هم اثر یکدیگر را خنثی می‌کنند. اما برای داده‌های جدولی که ویژگی‌های معدود با همبستگی بالا دارند، بهتر است اول به سراغ رگرسیون لجستیک یا مدل‌های مبتنی بر درخت بروید.

تمرین‌ها

  1. آزمایش هموارسازی. مدل MultinomialNB را با مقادیر آلفای 0.01، 0.1، 1.0، 10.0 و 100.0 روی داده‌های متنی آموزش دهید. نمودار دقت بر حسب آلفا را رسم کنید. نقطه‌ی اوج عملکرد کجاست؟ چرا مقادیر خیلی بزرگ آلفا باعث افت دقت می‌شوند؟

  2. آزمون استقلال ویژگی‌ها. یک مجموعه‌داده‌ی متنی واقعی بردارید. دو کلمه را که قطعاً با هم مرتبط‌اند انتخاب کنید (مثل «machine» و «learning»). مقدار P(word1∣class)⋅P(word2∣class)P(\text{word}_1 \mid \text{class}) \cdot P(\text{word}_2 \mid \text{class}) را محاسبه کرده و با P(word1 AND word2∣class)P(\text{word}_1 \text{ AND } \text{word}_2 \mid \text{class}) مقایسه نمایید. فرض استقلال تا چه حد با واقعیت فاصله دارد؟ آیا این موضوع تاثیری بر دقت دسته‌بندی می‌گذارد؟

  3. پیاده‌سازی برنولی. کد را گسترش دهید و کلاس BernoulliNB را بسازید. خروجی کیسه‌ی کلمات را به حالت باینری (حضور/غیبت) تبدیل کنید و دقت آن را با MultinomialNB روی داده‌های متنی مقایسه کنید. برنولی در چه شرایطی برنده می‌شود؟

  4. نایو بیز در برابر رگرسیون لجستیک. هر دو مدل را روی داده‌های متنی آموزش دهید. کار را با 100 نمونه‌ی آموزشی شروع کرده و اندازه را تا 10000 افزایش دهید. نمودار دقت بر حسب اندازه‌ی مجموعه‌ی داده را برای هر دو ترسیم کنید. رگرسیون لجستیک از چه نقطه‌ای نایو بیز را پشت سر می‌گذارد؟

  5. فیلتر اسپم. یک دسته‌بند کامل اسپم بسازید: متن خام ایمیل‌ها را به توکن (token) تبدیل کنید، واژگان را استخراج نمایید، ویژگی‌های کیسه‌ی کلمات را بسازید، مدل MultinomialNB را آموزش دهید و با استفاده از دقت و فراخوانی آن را ارزیابی کنید (چرا نباید فقط به ارزیابی دقت بسنده کرد؟).

واژگان و اصطلاحات کلیدی

اصطلاح در گفت‌وگوهای عامه معنای دقیق فنی
نایو بیز (Naive Bayes) «یک دسته‌بند احتمالاتی ساده» دسته‌بندی مبتنی بر قضیه‌ی بیز، با این فرض که ویژگی‌ها به شرط دانستن کلاس، از هم مستقل هستند
استقلال شرطی (Conditional independence) «ویژگی‌ها بر هم اثری نمی‌گذارند» P(A,B∣C)=P(A∣C)⋅P(B∣C)P(A, B \mid C) = P(A \mid C) \cdot P(B \mid C) — وقتی مقدار CC را بدانید، آگاهی از BB اطلاعات جدیدی درباره‌ی AA به شما نمی‌دهد
هموارسازی لاپلاس (Laplace smoothing) «هموارسازی با افزودن یک واحد» اضافه کردن مقداری کوچک به شمارش همه‌ی ویژگی‌ها تا احتمال‌های صفر مانع تصمیم‌گیری مدل نشوند
پیشین (Prior) «باوری که قبل از دیدن داده‌ها داشتیم» P(class)P(\text{class}) — احتمال هر کلاس قبل از این‌که ویژگی‌ها را بررسی کنیم
درست‌نمایی (Likelihood) «میزان همخوانی داده با کلاس» P(features∣class)P(\text{features} \mid \text{class}) — احتمال دیدن این ویژگی‌ها با فرض این‌که کلاس داده‌ها مشخص باشد
پسین (Posterior) «باوری که بعد از دیدن داده‌ها به دست می‌آید» P(class∣features)P(\text{class} \mid \text{features}) — احتمال به‌روزرسانی‌شده‌ی کلاس پس از مشاهده‌ی ویژگی‌ها
مدل زایشی / مولد (Generative model) «مدلی که سازوکار تولید داده را یاد می‌گیرد» مدلی که توزیع‌های P(X∣Y)P(X \mid Y) و P(Y)P(Y) را یاد می‌گیرد و بعد با قضیه‌ی بیز به P(Y∣X)P(Y \mid X) می‌رسد
مدل تمایزگرا (Discriminative model) «مدلی که مرز تصمیم‌گیری را یاد می‌گیرد» مدلی که مستقیماً P(Y∣X)P(Y \mid X) را بدون مدل‌سازی نحوه‌ی تولید XX یاد می‌گیرد
لگاریتم احتمال (Log probability) «روشی برای جلوگیری از کم‌ریز عددی» کار با log⁡P\log P به‌جای PP تا ضرب چند عدد کوچک باعث بروز خطای محاسباتی صفر در متغیرهای ممیز شناور نشود

منابع برای مطالعه‌ی بیشتر

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

چرا با وجود غلط بودن فرض استقلال شرطی، نایو بیز در عمل خوب کار می‌کند؟

چون در دسته‌بندی فقط رتبه‌بندی صحیح کلاس‌ها و انتخاب بالاترین احتمال اهمیت دارد، نه مقدار دقیق و کالیبره‌شده‌ی احتمالات؛ همچنین خطای ناشی از این فرض تقریباً به شکل یکنواخت روی تمام کلاس‌ها اثر می‌گذارد.

هموارسازی لاپلاس (Laplace smoothing) چه مشکلی را حل می‌کند؟

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

تفاوت MultinomialNB با BernoulliNB در چیست؟

نایو بیز چندجمله‌ای (Multinomial) بسامد یا تعداد تکرار واژه‌ها را در نظر می‌گیرد و برای متن‌های معمولی مناسب است، اما مدل برنولی (Bernoulli) فقط حضور یا عدم حضور (0 و 1) کلمه را لحاظ کرده و برای متن‌های خیلی کوتاه کاربرد بهتری دارد.

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

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