انتخاب میان منحنی Precision/Recall و ROC
از آنجا که منحنی ROC بسیار شبیه منحنی Precision/Recall است، انتخاب میان آنها مهم است. یک قاعدهٔ عملی این است که وقتی کلاس مثبت نادر است یا مثبتهای کاذب برای شما مهمتر از منفیهای کاذباند، منحنی PR را ترجیح دهید؛ در غیر این صورت ROC گزینهٔ مناسبی است. در مثال تشخیص رقم ۵، نگاهکردن به ROC و ROC AUC ممکن است تصور بسیار خوبی از مدل بدهد، اما یکی از علتها این است که تعداد ۵ها نسبت به غیر ۵ها کم است. منحنی PR واضحتر نشان میدهد که هنوز جای پیشرفت وجود دارد و منحنی میتواند به گوشهٔ بالا-راست نزدیکتر شود.
مقایسه با RandomForestClassifier
اکنون یک RandomForestClassifier میسازیم تا منحنی PR و امتیاز F1 آن را با SGDClassifier مقایسه کنیم:
from sklearn.ensemble import RandomForestClassifier
forest_clf = RandomForestClassifier(random_state=42)
تابع precision_recall_curve() برای هر نمونه به برچسب و یک امتیاز نیاز دارد. RandomForestClassifier بهدلیل سازوکارش decision_function() ندارد، اما predict_proba() احتمال کلاسها را برمیگرداند. بنابراین میتوان احتمال کلاس مثبت را بهعنوان امتیاز استفاده کرد. با cross_val_predict() طبقهبند را در Cross-Validation آموزش میدهیم و برای همهٔ تصاویر احتمال کلاسها را میگیریم:
y_probas_forest = cross_val_predict(forest_clf, X_train, y_train_5, cv=3,
method="predict_proba")
احتمال دو تصویر نخست:
>>> y_probas_forest[:2]
array([[0.11, 0.89],
[0.99, 0.01]])
مدل برای تصویر نخست ۸۹ درصد احتمال مثبتبودن و برای تصویر دوم ۹۹ درصد احتمال منفیبودن تخمین زده است. چون هر تصویر یا مثبت است یا منفی، مجموع احتمالهای هر سطر ۱۰۰ درصد است.
ستون دوم احتمال کلاس مثبت است؛ آن را برای ساخت منحنی PR استفاده میکنیم:
y_scores_forest = y_probas_forest[:, 1]
precisions_forest, recalls_forest, thresholds_forest = precision_recall_curve(
y_train_5, y_scores_forest)
اکنون منحنی PR جنگل تصادفی و SGD را کنار هم رسم میکنیم:
plt.plot(recalls_forest, precisions_forest, "b-", linewidth=2,
label="Random Forest")
plt.plot(recalls, precisions, "--", linewidth=2, label="SGD")
[...] # beautify the figure: add labels, grid, and legend
plt.show()
شکل ۳-۸. مقایسهٔ منحنیهای PR؛ جنگل تصادفی بهتر است، زیرا منحنی آن به گوشهٔ بالا-راست نزدیکتر و سطح زیر منحنی آن بزرگتر است.
امتیاز F1 و ROC AUC جنگل تصادفی نیز بهطور محسوسی بهتر است:
>>> y_train_pred_forest = y_probas_forest[:, 1] >= 0.5 # positive proba ≥ 50%
>>> f1_score(y_train_5, y_train_pred_forest)
0.9242275142688446
>>> roc_auc_score(y_train_5, y_scores_forest)
0.9983436731328145
اگر Precision و Recall را نیز اندازه بگیرید، تقریباً به ۹۹٫۱ درصد Precision و ۸۶٫۶ درصد Recall میرسید. اکنون شیوهٔ آموزش طبقهبند دودویی، انتخاب معیار مناسب، ارزیابی با Cross-Validation، تنظیم موازنهٔ Precision/Recall و مقایسهٔ مدلها با چند معیار و منحنی را میدانید. وقت آن است که بیش از یک رقم را تشخیص دهیم.
طبقهبندی چندکلاسه
طبقهبند دودویی فقط دو کلاس را از هم جدا میکند، اما طبقهبند چندکلاسه - که گاهی Multinomial Classifier نیز نامیده میشود - قادر است بیش از دو کلاس را تشخیص دهد. بعضی طبقهبندهای Scikit-Learn مانند LogisticRegression، RandomForestClassifier و GaussianNB بهصورت بومی چندکلاسه هستند. بعضی دیگر، مانند SGDClassifier و SVC، اساساً دودوییاند، اما میتوان با چند راهبرد از چند طبقهبند دودویی یک سامانهٔ چندکلاسه ساخت.
راهبرد One-versus-the-Rest
برای تشخیص ارقام ۰ تا ۹ میتوان ۱۰ طبقهبند دودویی ساخت: تشخیصدهندهٔ ۰، تشخیصدهندهٔ ۱ و به همین ترتیب. هنگام طبقهبندی یک تصویر، امتیاز تصمیم همهٔ طبقهبندها گرفته میشود و کلاسی انتخاب میشود که طبقهبند مربوط به آن بیشترین امتیاز را داده است. این روش One-versus-the-Rest (OvR) یا گاهی One-versus-All (OvA) نام دارد.
راهبرد One-versus-One
روش دیگر آموزش یک طبقهبند دودویی برای هر جفت کلاس است: یکی برای ۰ در برابر ۱، دیگری برای ۰ در برابر ۲، یکی برای ۱ در برابر ۲ و ... . این روش One-versus-One (OvO) نام دارد. با N کلاس به N × (N − 1) / 2 طبقهبند نیاز است. برای MNIST یعنی ۴۵ طبقهبند دودویی. هنگام پیشبینی، تصویر از همهٔ ۴۵ طبقهبند عبور میکند و کلاسی انتخاب میشود که بیشترین «دوئل» را برده باشد.
مزیت اصلی OvO این است که هر طبقهبند فقط روی نمونههای دو کلاس مربوط آموزش میبیند. الگوریتمهایی مانند SVM با افزایش اندازهٔ مجموعهٔ آموزشی بهخوبی مقیاس نمیشوند، بنابراین OvO برای آنها میتواند سریعتر باشد. برای بیشتر الگوریتمهای دودویی دیگر، OvR معمولاً ترجیح داده میشود.
Scikit-Learn وقتی متوجه شود الگوریتم دودویی برای مسئلهٔ چندکلاسه استفاده شده، بسته به الگوریتم بهطور خودکار OvR یا OvO را اجرا میکند. مثلاً SVC را فقط روی ۲۰۰۰ تصویر نخست آموزش میدهیم تا زمان آموزش زیاد نشود:
from sklearn.svm import SVC
svm_clf = SVC(random_state=42)
svm_clf.fit(X_train[:2000], y_train[:2000]) # y_train, not y_train_5
در اینجا هدف اصلی ۰ تا ۹ به مدل داده شده است، نه هدف دودویی «۵ در برابر بقیه». چون ۱۰ کلاس داریم، Scikit-Learn از OvO استفاده کرده و ۴۵ طبقهبند ساخته است:
>>> svm_clf.predict([some_digit])
array(['5'], dtype=object)
این پیشبینی درست است. در پشت صحنه، ۴۵ پیشبینی انجام شده و کلاسی که بیشترین دوئل را برده انتخاب شده است. decision_function() برای هر نمونه ۱۰ امتیاز برمیگرداند، یکی برای هر کلاس. امتیاز هر کلاس تعداد دوئلهای بردهشده بهاضافه یا منهای اصلاح کوچکی - حداکثر حدود ۰٫۳۳ - برای شکستن تساوی است:
>>> some_digit_scores = svm_clf.decision_function([some_digit])
>>> some_digit_scores.round(2)
array([[ 3.79, 0.73, 6.06, 8.3 , -0.29, 9.3 , 1.75, 2.77, 7.21,
4.82]])
>>> class_id = some_digit_scores.argmax()
>>> class_id
5
بیشترین امتیاز ۹٫۳ است و به کلاس ۵ مربوط میشود. پس از آموزش، فهرست کلاسهای هدف در classes_ ذخیره میشود. در MNIST بهطور اتفاقی Index هر کلاس با مقدار خودش برابر است، اما در حالت عمومی باید برچسب را از آرایه پیدا کنید:
>>> svm_clf.classes_
array(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], dtype=object)
>>> svm_clf.classes_[class_id]
'5'
اگر بخواهید راهبرد را صریحاً تحمیل کنید میتوانید از OneVsOneClassifier یا OneVsRestClassifier استفاده کنید. نمونهٔ OvR با SVC:
from sklearn.multiclass import OneVsRestClassifier
ovr_clf = OneVsRestClassifier(SVC(random_state=42))
ovr_clf.fit(X_train[:2000], y_train[:2000])
>>> ovr_clf.predict([some_digit])
array(['5'], dtype='<U1')
>>> len(ovr_clf.estimators_)
10
SGDClassifier در مسئلهٔ چندکلاسه
آموزش SGDClassifier روی برچسبهای ۰ تا ۹ نیز مستقیم است:
>>> sgd_clf = SGDClassifier(random_state=42)
>>> sgd_clf.fit(X_train, y_train)
>>> sgd_clf.predict([some_digit])
array(['3'], dtype='<U1')
این بار پیشبینی اشتباه است؛ خطا طبیعی است. Scikit-Learn در این حالت OvR اجرا کرده و ۱۰ طبقهبند دودویی ساخته است. امتیازهای کلاسها:
>>> sgd_clf.decision_function([some_digit]).round()
array([[-31893., -34420., -9531., 1824., -22320., -1386., -26189.,
-16148., -4604., -12051.]])
مدل به پیشبینی خود اطمینان زیادی ندارد: تقریباً همهٔ امتیازها بسیار منفیاند، کلاس ۳ امتیاز مثبت ۱۸۲۴ دارد و کلاس ۵ با منفی ۱۳۸۶ چندان دور نیست. ارزیابی روی Cross-Validation سهتایی:
>>> cross_val_score(sgd_clf, X_train, y_train, cv=3, scoring="accuracy")
array([0.87365, 0.85835, 0.8689 ])
در همهٔ Foldها Accuracy بیش از ۸۵٫۸ درصد است؛ درحالیکه طبقهبند تصادفی برای ۱۰ کلاس حدود ۱۰ درصد میگیرد. فقط با Scale کردن ورودیها، Accuracy به بیش از ۸۹٫۱ درصد میرسد:
>>> from sklearn.preprocessing import StandardScaler
>>> scaler = StandardScaler()
>>> X_train_scaled = scaler.fit_transform(X_train.astype("float64"))
>>> cross_val_score(sgd_clf, X_train_scaled, y_train, cv=3, scoring="accuracy")
array([0.8983, 0.891 , 0.9018])
تحلیل خطا
در پروژهٔ واقعی، در این مرحله گزینههای آمادهسازی داده را بررسی میکردید، مدلهای متعدد را میآزمودید، بهترینها را در فهرست کوتاه قرار میدادید، فراپارامترها را با GridSearchCV تنظیم میکردید و تا حد امکان فرایند را خودکار میساختید. فرض میکنیم مدل امیدوارکنندهای پیدا شده و اکنون میخواهیم علت خطاهایش را بشناسیم.
ابتدا ماتریس درهمریختگی را بررسی میکنیم. با cross_val_predict() پیشبینیها را میسازیم و سپس ConfusionMatrixDisplay.from_predictions() را به کار میبریم:
from sklearn.metrics import ConfusionMatrixDisplay
y_train_pred = cross_val_predict(sgd_clf, X_train_scaled, y_train, cv=3)
ConfusionMatrixDisplay.from_predictions(y_train, y_train_pred)
plt.show()
ماتریس سمت چپ شکل ۳-۹ عمدتاً قطر روشنی دارد که نشاندهندهٔ طبقهبندی درست بسیاری از تصاویر است. خانهٔ ۵ روی قطر کمی متفاوت دیده میشود؛ این میتواند بهعلت خطای بیشتر روی رقم ۵ یا کمتر بودن تعداد ۵ها باشد. برای رفع ابهام، ماتریس را با تقسیم هر مقدار بر تعداد تصاویر کلاس واقعی همان سطر نرمال میکنیم:
ConfusionMatrixDisplay.from_predictions(y_train, y_train_pred,
normalize="true", values_format=".0%")
plt.show()
شکل ۳-۹. ماتریس درهمریختگی خام در سمت چپ و همان ماتریس نرمالشده بر اساس سطر در سمت راست
اکنون مشخص است که فقط ۸۲ درصد تصاویر ۵ درست طبقهبندی شدهاند. رایجترین خطا برای ۵ها، تشخیص آنها بهعنوان ۸ است و این برای ۱۰ درصد همهٔ ۵ها رخ میدهد. در مقابل فقط ۲ درصد ۸ها بهعنوان ۵ اشتباه شدهاند؛ ماتریس درهمریختگی لزوماً متقارن نیست.
برای برجستهکردن خطاها میتوان وزن پیشبینیهای درست را صفر کرد:
sample_weight = (y_train_pred != y_train)
ConfusionMatrixDisplay.from_predictions(y_train, y_train_pred,
sample_weight=sample_weight,
normalize="true", values_format=".0%")
plt.show()
و میتوان بهجای نرمالسازی بر اساس سطر، با normalize="pred" بر اساس ستون نرمال کرد.
شکل ۳-۱۰. فقط خطاها؛ نرمالشده بر اساس سطر در سمت چپ و بر اساس ستون در سمت راست
ستون کلاس ۸ در ماتریس خطاها بسیار روشن است؛ یعنی بسیاری از ارقام بهاشتباه ۸ تشخیص داده میشوند. دقت کنید درصدها در ماتریسی که فقط خطاها را نشان میدهد نسبت به کل نمونهها نیستند. برای مثال ۳۶ درصد در سطر ۷ و ستون ۹ یعنی ۳۶ درصد «خطاهای مربوط به ۷» به شکل ۹ بودهاند، نه اینکه ۳۶ درصد همهٔ ۷ها اشتباه شده باشند. در ماتریس کامل میبینیم فقط حدود ۳ درصد ۷ها بهعنوان ۹ اشتباه شدهاند. در نرمالسازی ستونی نیز میتوان دید چه سهمی از پیشبینیهای اشتباه یک کلاس واقعاً متعلق به کلاس دیگر بودهاند.
تحلیل ماتریس راههای بهبود را نشان میدهد. اینجا باید روی کاهش «۸های کاذب» تمرکز کرد؛ مثلاً دادهٔ آموزشی بیشتری از ارقامی جمع کرد که شبیه ۸ هستند اما ۸ نیستند، ویژگیهایی مانند تعداد حلقههای بسته ساخت - ۸ دو حلقه دارد، ۶ یک حلقه و ۵ هیچ حلقه - یا با ابزارهایی مانند Scikit-Image، Pillow یا OpenCV تصاویر را پیشپردازش کرد تا الگوهای مهم برجسته شوند.
تحلیل خطاهای منفرد
بررسی نمونههای خطا نیز برای فهم رفتار مدل مفید است. نمونههایی از ۳ و ۵ را در قالب ماتریس درهمریختگی دستهبندی میکنیم:
cl_a, cl_b = '3', '5'
X_aa = X_train[(y_train == cl_a) & (y_train_pred == cl_a)]
X_ab = X_train[(y_train == cl_a) & (y_train_pred == cl_b)]
X_ba = X_train[(y_train == cl_b) & (y_train_pred == cl_a)]
X_bb = X_train[(y_train == cl_b) & (y_train_pred == cl_b)]
[...] # plot all images in X_aa, X_ab, X_ba, X_bb in a confusion matrix style
شکل ۳-۱۱. برخی تصاویر ۳ و ۵، سازماندهیشده مانند ماتریس درهمریختگی
برخی نمونههای اشتباه آنقدر بد نوشته شدهاند که انسان نیز بهسختی آنها را تشخیص میدهد، اما بسیاری از اشتباهها برای انسان واضحاند. علت این تفاوت آن است که مغز انسان سامانهٔ بسیار پیچیدهای برای تشخیص الگو و پیشپردازش بینایی دارد. SGDClassifier استفادهشده یک مدل خطی ساده است: برای هر کلاس به هر پیکسل وزنی میدهد و هنگام دیدن تصویر تازه، شدت پیکسلها را با وزنها جمع میکند تا امتیاز هر کلاس را بسازد. چون ۳ و ۵ فقط در چند پیکسل تفاوت دارند، مدل بهراحتی آنها را اشتباه میکند.
تفاوت مهم ۳ و ۵ موقعیت خط کوچکی است که خط بالایی را به قوس پایینی وصل میکند. جابهجایی یا چرخش اندک تصویر میتواند پیشبینی را عوض کند. یک راه، مرکزکردن و تصحیح چرخش تصاویر در پیشپردازش است؛ راه سادهتر این است که نسخههای کمی جابهجا یا چرخاندهشده به مجموعهٔ آموزشی اضافه شوند تا مدل نسبت به این تغییرات مقاومتر شود. این روش Data Augmentation نام دارد.
طبقهبندی چندبرچسبی
تا اینجا هر نمونه فقط یک کلاس داشت، اما گاهی یک نمونه باید چند کلاس خروجی داشته باشد. مثلاً سامانهٔ تشخیص چهره اگر چند نفر را در یک عکس ببیند باید برای هر فردی که میشناسد برچسب جدا بدهد. اگر مدل سه چهرهٔ Alice، Bob و Charlie را بشناسد، تصویر Alice و Charlie باید خروجی [True, False, True] تولید کند. چنین سامانهای «طبقهبندی چندبرچسبی» است.
یک مثال ساده با MNIST میسازیم: برچسب نخست میگوید رقم بزرگ است - ۷، ۸ یا ۹ - و برچسب دوم میگوید رقم فرد است:
import numpy as np
from sklearn.neighbors import KNeighborsClassifier
y_train_large = (y_train >= '7')
y_train_odd = (y_train.astype('int8') % 2 == 1)
y_multilabel = np.c_[y_train_large, y_train_odd]
knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train, y_multilabel)
اکنون پیشبینی دو برچسب میدهد:
>>> knn_clf.predict([some_digit])
array([[False, True]])
برای رقم ۵ درست است: بزرگ نیست و فرد است. ارزیابی طبقهبند چندبرچسبی به نیاز پروژه وابسته است. یک روش محاسبهٔ F1 برای هر برچسب و سپس میانگین آنهاست:
>>> y_train_knn_pred = cross_val_predict(knn_clf, X_train, y_multilabel, cv=3)
>>> f1_score(y_multilabel, y_train_knn_pred, average="macro")
0.976410265560605
این روش فرض میکند همهٔ برچسبها اهمیت برابر دارند. اگر بعضی برچسبها نمونهٔ بیشتری دارند، میتوان با average="weighted" به هر برچسب متناسب با Support آن وزن داد.
اگر طبقهبندی مانند SVC چندبرچسبی را بومی پشتیبانی نکند، میتوان یک مدل مستقل برای هر برچسب آموزش داد، اما این کار وابستگی میان برچسبها را نادیده میگیرد. برای ثبت این وابستگیها میتوان مدلها را زنجیره کرد؛ هر مدل علاوه بر ویژگیهای ورودی، پیشبینی مدلهای قبلی را نیز دریافت میکند. Scikit-Learn این کار را با ClassifierChain انجام میدهد. اگر cv تنظیم شود، پیشبینیهای Out-of-Sample حاصل از Cross-Validation برای آموزش مدلهای بعدی زنجیره استفاده میشوند:
from sklearn.multioutput import ClassifierChain
chain_clf = ClassifierChain(SVC(), cv=3, random_state=42)
chain_clf.fit(X_train[:2000], y_multilabel[:2000])
>>> chain_clf.predict([some_digit])
array([[0., 1.]])
طبقهبندی چندخروجی
آخرین نوع طبقهبندی این فصل «چندخروجی-چندکلاسه» یا بهاختصار Multioutput Classification است؛ تعمیم طبقهبندی چندبرچسبی که در آن هر برچسب خودش میتواند چندکلاسه باشد.
برای نمونه سامانهای میسازیم که نویز تصویر را حذف کند. ورودی یک تصویر نویزی از رقم است و خروجی باید تصویر تمیز باشد؛ یعنی آرایهای از شدت پیکسلها. خروجی چندبرچسبی است چون برای هر پیکسل یک برچسب داریم، و هر برچسب چند مقدار ممکن - ۰ تا ۲۵۵ - دارد. بنابراین این یک مسئلهٔ چندخروجی است.
با افزودن نویز تصادفی به پیکسلهای تصاویر MNIST، مجموعههای آموزشی و آزمون را میسازیم؛ هدفها همان تصاویر اصلیاند:
np.random.seed(42) # to make this code example reproducible
noise = np.random.randint(0, 100, (len(X_train), 784))
X_train_mod = X_train + noise
noise = np.random.randint(0, 100, (len(X_test), 784))
X_test_mod = X_test + noise
y_train_mod = X_train
y_test_mod = X_test
شکل ۳-۱۲. تصویر نویزی در سمت چپ و تصویر تمیز هدف در سمت راست
حالا KNN را آموزش میدهیم و از آن میخواهیم تصویر نویزی را پاک کند:
knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train_mod, y_train_mod)
clean_digit = knn_clf.predict([X_test_mod[0]])
plot_digit(clean_digit)
plt.show()
شکل ۳-۱۳. تصویر پاکسازیشدهٔ خروجی مدل
نتیجه به تصویر هدف بسیار نزدیک است. با این مثال، مرور طبقهبندی پایان مییابد: اکنون انتخاب معیار مناسب برای طبقهبندی، تعیین موازنهٔ Precision/Recall، مقایسهٔ طبقهبندها و ساخت سامانههای دودویی، چندکلاسه، چندبرچسبی و چندخروجی را دیدهاید. فصلهای بعد سازوکار داخلی مدلهایی را که تاکنون استفاده شدهاند باز میکنند.
تمرینهای فصل ۳
- برای MNIST طبقهبندی بسازید که Accuracy آن روی مجموعهٔ آزمون بیش از ۹۷ درصد باشد.
KNeighborsClassifier برای این کار مناسب است؛ مقادیر خوب weights و n_neighbors را با Grid Search پیدا کنید.
- تابعی بنویسید که تصویر MNIST را یک پیکسل به چپ، راست، بالا یا پایین جابهجا کند. سپس برای هر تصویر آموزشی چهار نسخهٔ جابهجاشده بسازید و به مجموعهٔ آموزشی اضافه کنید. بهترین مدل را روی مجموعهٔ گسترشیافته آموزش دهید و Accuracy را روی آزمون بسنجید. این روش رشد مصنوعی دادهٔ آموزشی همان Data Augmentation یا Training Set Expansion است.
- روی مجموعهدادهٔ Titanic کار کنید. میتوانید از Kaggle شروع کنید یا فایل معرفیشده در منبع را دریافت و مانند دادهٔ مسکن فصل ۲ باز کنید. دو فایل
train.csv و test.csv در اختیار دارید و هدف این است که بر اساس ستونهای دیگر، ستون Survived را پیشبینی کنید.
- یک طبقهبند Spam بسازید: نمونههای Spam و Ham را از مجموعهدادههای عمومی Apache SpamAssassin دریافت کنید؛ قالب داده را بررسی کنید؛ داده را به آموزش و آزمون تقسیم کنید؛ Pipeline آمادهسازی بنویسید که هر ایمیل را به بردار ویژگی Sparse تبدیل کند و حضور یا تعداد وقوع واژهها را نشان دهد. فراپارامترهایی برای حذف Headerها، کوچککردن حروف، حذف نشانهگذاری، جایگزینی URLها با
URL، جایگزینی اعداد با NUMBER و حتی Stemming در نظر بگیرید. سپس چند طبقهبند را امتحان کنید تا هم Recall و هم Precision بالا به دست آورید.
راهحل تمرینها در انتهای Notebook این فصل در منبع تکمیلی کتاب ارائه شده است.