اجرای Pipeline کامل پیشپردازش
اگر ColumnTransformer ساختهشده در بخش قبل را اجرا کنید، همهٔ تبدیلها انجام میشوند و یک آرایهٔ NumPy با ۲۴ ویژگی تولید میشود:
>>> housing_prepared = preprocessing.fit_transform(housing)
>>> housing_prepared.shape
(16512, 24)
>>> preprocessing.get_feature_names_out()
array(['bedrooms__ratio', 'rooms_per_house__ratio',
'people_per_house__ratio', 'log__total_bedrooms',
'log__total_rooms', 'log__population', 'log__households',
'log__median_income', 'geo__Cluster 0 similarity', [...],
'geo__Cluster 9 similarity', 'cat__ocean_proximity_<1H OCEAN',
'cat__ocean_proximity_INLAND', 'cat__ocean_proximity_ISLAND',
'cat__ocean_proximity_NEAR BAY', 'cat__ocean_proximity_NEAR OCEAN',
'remainder__housing_median_age'], dtype=object)
انتخاب و آموزش مدل
بالاخره به این مرحله رسیدید. مسئله را صورتبندی کردهاید، داده را دریافت و کاوش کردهاید، مجموعهٔ آموزشی و آزمون ساختهاید و Pipeline پیشپردازشی نوشتهاید که داده را برای الگوریتمهای یادگیری ماشین بهصورت خودکار پاک و آماده میکند. اکنون آمادهاید یک مدل یادگیری ماشین را انتخاب و آموزش دهید.
آموزش و ارزیابی روی مجموعهٔ آموزشی
خبر خوب این است که بهلطف همهٔ کارهای قبلی، ادامهٔ کار بسیار آسانتر خواهد بود. برای شروع یک مدل بسیار سادهٔ رگرسیون خطی آموزش میدهید:
from sklearn.linear_model import LinearRegression
lin_reg = make_pipeline(preprocessing, LinearRegression())
lin_reg.fit(housing, housing_labels)
تمام شد؛ اکنون یک مدل رگرسیون خطی قابل اجرا دارید. آن را روی مجموعهٔ آموزشی امتحان کنید، پنج پیشبینی نخست را ببینید و با برچسبهای واقعی مقایسه کنید:
>>> housing_predictions = lin_reg.predict(housing)
>>> housing_predictions[:5].round(-2) # -2 = rounded to the nearest hundred
array([243700., 372400., 128800., 94400., 328300.])
>>> housing_labels.iloc[:5].values
array([458300., 483800., 101700., 96100., 361800.])
مدل کار میکند، اما نه همیشه خوب. پیشبینی نخست بیش از ۲۰۰ هزار دلار خطا دارد؛ سایر پیشبینیها بهترند: دو مورد حدود ۲۵ درصد و دو مورد کمتر از ۱۰ درصد خطا دارند. چون معیار عملکرد انتخابشده RMSE است، اکنون RMSE مدل را روی کل مجموعهٔ آموزشی با تابع mean_squared_error() در Scikit-Learn و با squared=False اندازه میگیریم:
>>> from sklearn.metrics import mean_squared_error
>>> lin_rmse = mean_squared_error(housing_labels, housing_predictions,
... squared=False)
...
>>> lin_rmse
68687.89176589991
این نتیجه بهتر از هیچ است، اما بهوضوح خوب نیست. قیمت میانهٔ خانه در بیشتر ناحیهها بین ۱۲۰ هزار تا ۲۶۵ هزار دلار است؛ بنابراین خطای معمول حدود ۶۸٬۶۲۸ دلار رضایتبخش نیست. این نمونهای از Underfitting (کمبرازش) مدل روی دادهٔ آموزشی است. چنین وضعی میتواند به این معنا باشد که ویژگیها اطلاعات کافی برای پیشبینی خوب ندارند یا مدل توانمند نیست.
همانطور که در فصل قبل دیدید، راههای اصلی رفع کمبرازش عبارتاند از انتخاب مدل قویتر، دادن ویژگیهای بهتر به الگوریتم آموزشی یا کاهش محدودیتهای مدل. مدل فعلی منظمسازی نشده است، پس گزینهٔ آخر مطرح نیست. میتوانید ویژگیهای بیشتری اضافه کنید، اما ابتدا بهتر است یک مدل پیچیدهتر امتحان شود.
مدل DecisionTreeRegressor را امتحان میکنیم؛ مدلی نسبتاً قدرتمند که میتواند روابط غیرخطی پیچیده در داده را پیدا کند. در فصل ۶ درختهای تصمیم با جزئیات بیشتری بررسی میشوند:
from sklearn.tree import DecisionTreeRegressor
tree_reg = make_pipeline(preprocessing,
DecisionTreeRegressor(random_state=42))
tree_reg.fit(housing, housing_labels)
پس از آموزش، آن را روی مجموعهٔ آموزشی ارزیابی میکنید:
>>> housing_predictions = tree_reg.predict(housing)
>>> tree_rmse = mean_squared_error(housing_labels, housing_predictions,
... squared=False)
...
>>> tree_rmse
0.0
صفر؟ یعنی هیچ خطایی وجود ندارد؟ آیا مدل واقعاً کاملاً بینقص است؟ بسیار محتملتر است که مدل داده را شدیداً Overfit (بیشبرازش) کرده باشد. چگونه مطمئن شویم؟ قبلاً گفتیم تا وقتی به مدل مناسبی برای راهاندازی نرسیدهاید نباید به مجموعهٔ آزمون دست بزنید؛ بنابراین باید بخشی از مجموعهٔ آموزشی برای آموزش و بخشی برای اعتبارسنجی مدل استفاده شود.
ارزیابی بهتر با Cross-Validation (اعتبارسنجی متقابل)
یک راه برای ارزیابی درخت تصمیم این است که با train_test_split() مجموعهٔ آموزشی را به مجموعهٔ آموزشی کوچکتر و مجموعهٔ اعتبارسنجی تقسیم کنید، مدل را روی بخش آموزشی کوچکتر آموزش دهید و روی اعتبارسنجی بسنجید. کمی کار دارد اما دشوار نیست و معمولاً خوب عمل میکند.
گزینهٔ بسیار خوب دیگر استفاده از قابلیت k-fold cross-validation در Scikit-Learn است. کد زیر مجموعهٔ آموزشی را بهصورت تصادفی به ۱۰ زیرمجموعهٔ بدون همپوشانی، موسوم به Fold، تقسیم میکند. سپس مدل درخت تصمیم ۱۰ بار آموزش و ارزیابی میشود؛ هر بار Fold متفاوتی برای ارزیابی و ۹ Fold دیگر برای آموزش استفاده میشوند. نتیجه آرایهای شامل ۱۰ امتیاز ارزیابی است:
from sklearn.model_selection import cross_val_score
tree_rmses = -cross_val_score(tree_reg, housing, housing_labels,
scoring="neg_root_mean_squared_error", cv=10)
نتایج:
>>> pd.Series(tree_rmses).describe()
count 10.000000
mean 66868.027288
std 2060.966425
min 63649.536493
25% 65338.078316
50% 66801.953094
75% 68229.934454
max 70094.778246
dtype: float64
اکنون درخت تصمیم به خوبی قبل به نظر نمیرسد؛ در واقع تقریباً به اندازهٔ رگرسیون خطی ضعیف است. Cross-Validation علاوه بر برآورد عملکرد مدل، معیاری از دقت این برآورد - یعنی انحراف معیار آن - نیز میدهد. RMSE درخت تصمیم حدود ۶۶٬۸۶۸ با انحراف معیار حدود ۲۰۶۱ است. اگر فقط یک مجموعهٔ اعتبارسنجی داشتید، این اطلاعات را در اختیار نداشتید. البته Cross-Validation هزینه دارد، زیرا مدل چند بار آموزش داده میشود و همیشه عملی نیست.
اگر همان معیار را برای رگرسیون خطی حساب کنید، میانگین RMSE حدود ۶۹٬۸۵۸ و انحراف معیار ۴۱۸۲ است. بنابراین درخت تصمیم اندکی بهتر از مدل خطی عمل میکند، اما تفاوت بهدلیل بیشبرازش شدید ناچیز است. وجود بیشبرازش را از اختلاف میان خطای آموزشی بسیار پایین - در اینجا دقیقاً صفر - و خطای اعتبارسنجی بالا میفهمیم.
RandomForestRegressor
یک مدل دیگر امتحان میکنیم: RandomForestRegressor. همانطور که در فصل ۷ خواهید دید، جنگل تصادفی تعداد زیادی درخت تصمیم را روی زیرمجموعههای تصادفی ویژگیها آموزش میدهد و سپس میانگین پیشبینیهای آنها را میگیرد. مدلهایی که از چند مدل دیگر تشکیل شدهاند «Ensemble (مدل تجمیعی)» نامیده میشوند و میتوانند عملکرد مدلهای پایه - در اینجا درخت تصمیم - را تقویت کنند.
from sklearn.ensemble import RandomForestRegressor
forest_reg = make_pipeline(preprocessing,
RandomForestRegressor(random_state=42))
forest_rmses = -cross_val_score(forest_reg, housing, housing_labels,
scoring="neg_root_mean_squared_error", cv=10)
امتیازها:
>>> pd.Series(forest_rmses).describe()
count 10.000000
mean 47019.561281
std 1033.957120
min 45458.112527
25% 46464.031184
50% 46967.596354
75% 47325.694987
max 49243.765795
dtype: float64
این نتیجه بسیار بهتر است و جنگل تصادفی برای این مسئله بسیار امیدوارکننده به نظر میرسد. بااینحال اگر یک RandomForest را آموزش دهید و RMSE آن را روی مجموعهٔ آموزشی اندازه بگیرید، تقریباً ۱۷٬۴۷۴ به دست میآید؛ بسیار کمتر از خطای اعتبارسنجی، یعنی همچنان بیشبرازش قابلتوجهی وجود دارد. راههای ممکن شامل سادهترکردن مدل، محدود یا منظمسازی آن، یا تهیهٔ دادهٔ آموزشی بسیار بیشتر است.
پیش از تمرکز عمیقتر روی جنگل تصادفی، بهتر است چند مدل دیگر از دستههای متفاوت الگوریتمها - برای نمونه چند ماشین بردار پشتیبان با Kernelهای مختلف و شاید یک شبکهٔ عصبی - آزمایش کنید، بدون آنکه فعلاً زمان زیادی برای تنظیم فراپارامترها صرف کنید. هدف ساخت فهرستی کوتاه از دو تا پنج مدل امیدوارکننده است.
تنظیم دقیق مدل
فرض کنیم اکنون چند مدل امیدوارکننده در فهرست کوتاه دارید. باید آنها را Fine-Tune (تنظیم دقیق) کنید. چند روش را بررسی میکنیم.
Grid Search (جستوجوی شبکهای)
یک راه این است که فراپارامترها را دستی دستکاری کنید تا ترکیب خوبی پیدا شود. این کار بسیار خستهکننده است و شاید فرصت بررسی ترکیبهای زیاد را نداشته باشید. بهجای آن میتوانید از کلاس رسمی GridSearchCV در Scikit-Learn استفاده کنید. کافی است بگویید کدام فراپارامترها و چه مقادیری باید آزمایش شوند؛ کلاس همهٔ ترکیبهای ممکن را با Cross-Validation ارزیابی میکند.
کد زیر بهترین ترکیب فراپارامترها را برای RandomForestRegressor جستوجو میکند:
from sklearn.model_selection import GridSearchCV
full_pipeline = Pipeline([
("preprocessing", preprocessing),
("random_forest", RandomForestRegressor(random_state=42)),
])
param_grid = [
{'preprocessing__geo__n_clusters': [5, 8, 10],
'random_forest__max_features': [4, 6, 8]},
{'preprocessing__geo__n_clusters': [10, 15],
'random_forest__max_features': [6, 8, 10]},
]
grid_search = GridSearchCV(full_pipeline, param_grid, cv=3,
scoring='neg_root_mean_squared_error')
grid_search.fit(housing, housing_labels)
میتوانید به فراپارامتر هر Estimator درون Pipeline ارجاع دهید، حتی اگر آن Estimator در چند لایه Pipeline و ColumnTransformer تو در تو باشد. وقتی Scikit-Learn رشتهٔ preprocessing__geo__n_clusters را میبیند، آن را در دو زیرخطها جدا میکند، ابتدا Estimator با نام preprocessing را در Pipeline پیدا میکند که همان ColumnTransformer پیشپردازش است؛ سپس Transformer با نام geo را در آن پیدا میکند که ClusterSimilarity اعمالشده بر عرض و طول جغرافیایی است؛ و در نهایت به فراپارامتر n_clusters میرسد. بههمین ترتیب random_forest__max_features به فراپارامتر max_features در Estimator با نام random_forest اشاره دارد. فراپارامتر max_features در فصل ۷ توضیح داده میشود.
در param_grid دو Dictionary وجود دارد. GridSearchCV ابتدا همهٔ 3 × 3 = 9 ترکیب n_clusters و max_features در Dictionary نخست را ارزیابی میکند، سپس 2 × 3 = 6 ترکیب Dictionary دوم را میآزماید. در مجموع 9 + 6 = 15 ترکیب بررسی میشود و چون Cross-Validation سهتایی است، Pipeline برای هر ترکیب سه بار آموزش میبیند؛ یعنی در مجموع 15 × 3 = 45 دور آموزش. ممکن است مدتی طول بکشد، اما در پایان بهترین ترکیب را چنین میگیرید:
>>> grid_search.best_params_
{'preprocessing__geo__n_clusters': 15, 'random_forest__max_features': 6}
در متن کتاب، مدل برتر این مثال با n_clusters=15 و max_features=8 توصیف شده است.
بهترین Estimator از grid_search.best_estimator_ در دسترس است. اگر GridSearchCV با refit=True ساخته شود - که پیشفرض است - پس از یافتن بهترین Estimator با Cross-Validation، آن را روی کل مجموعهٔ آموزشی دوباره آموزش میدهد. این معمولاً ایدهٔ خوبی است، چون دادهٔ بیشتر احتمالاً عملکرد را بهتر میکند.
امتیازهای ارزیابی در grid_search.cv_results_ قرار دارند. این شیء یک Dictionary است، اما با تبدیلش به DataFrame فهرست خوانایی از امتیاز آزمون هر ترکیب فراپارامتر در هر Split و میانگین امتیاز همهٔ Splitها میگیرید:
>>> cv_res = pd.DataFrame(grid_search.cv_results_)
>>> cv_res.sort_values(by="mean_test_score", ascending=False, inplace=True)
>>> [...] # change column names to fit on this page, and show rmse = -score
>>> cv_res.head() # note: the 1st column is the row ID
n_clusters max_features split0 split1 split2 mean_test_rmse
12 15 6 43460 43919 44748 44042
13 15 8 44132 44075 45010 44406
14 15 10 44374 44286 45316 44659
7 10 6 44683 44655 45657 44999
9 10 6 44683 44655 45657 44999
میانگین RMSE آزمون برای بهترین مدل ۴۴٬۰۴۲ است که از امتیاز ۴۷٬۰۱۹ حاصل از فراپارامترهای پیشفرض بهتر است. در نتیجه بهترین مدل با موفقیت تنظیم دقیق شده است.
Randomized Search (جستوجوی تصادفی)
Grid Search وقتی تعداد ترکیبها کم است مناسب است، اما بهویژه وقتی فضای جستوجوی فراپارامترها بزرگ باشد، RandomizedSearchCV اغلب ترجیح داده میشود. روش استفاده تقریباً شبیه GridSearchCV است، اما بهجای امتحان همهٔ ترکیبها، تعداد ثابتی ترکیب را ارزیابی میکند و در هر تکرار برای هر فراپارامتر مقدار تصادفی انتخاب میکند. این روش چند مزیت دارد:
- اگر بعضی فراپارامترها پیوسته یا گسسته با تعداد بسیار زیادی مقدار ممکن باشند و جستوجوی تصادفی را مثلاً ۱۰۰۰ تکرار اجرا کنید، ۱۰۰۰ مقدار متفاوت برای هر فراپارامتر بررسی میشود؛ Grid Search فقط چند مقداری را که صریحاً فهرست کردهاید میآزماید.
- فرض کنید فراپارامتری عملاً اثر زیادی ندارد اما شما هنوز نمیدانید. اگر ۱۰ مقدار ممکن برایش در Grid Search اضافه کنید، زمان آموزش ۱۰ برابر میشود. افزودن همان فراپارامتر به Random Search تعداد کل تکرارهای ازپیشتعیینشده را افزایش نمیدهد.
- اگر شش فراپارامتر داشته باشید و هرکدام ۱۰ مقدار ممکن، Grid Search باید یک میلیون بار مدل را آموزش دهد؛ اما Random Search را میتوانید با هر تعداد تکرار دلخواه اجرا کنید.
برای هر فراپارامتر باید یا فهرستی از مقادیر ممکن بدهید یا یک توزیع احتمال:
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint
param_distribs = {
'preprocessing__geo__n_clusters': randint(low=3, high=50),
'random_forest__max_features': randint(low=2, high=20)
}
rnd_search = RandomizedSearchCV(
full_pipeline, param_distributions=param_distribs, n_iter=10, cv=3,
scoring='neg_root_mean_squared_error', random_state=42)
rnd_search.fit(housing, housing_labels)
Scikit-Learn کلاسهای HalvingRandomSearchCV و HalvingGridSearchCV را نیز برای جستوجوی فراپارامترها دارد. هدف آنها استفادهٔ کارآمدتر از منابع محاسباتی است، تا آموزش سریعتر شود یا فضای بزرگتری از فراپارامترها بررسی شود. در دور نخست تعداد زیادی ترکیب فراپارامتر - «نامزد» - با روش شبکهای یا تصادفی ساخته میشوند و مانند معمول با Cross-Validation ارزیابی میشوند، اما آموزش با منابع محدود انجام میشود و بنابراین دور نخست بسیار سریعتر است. بهطور پیشفرض، منابع محدود یعنی مدلها فقط روی بخش کوچکی از مجموعهٔ آموزشی آموزش میبینند، هرچند محدودیتهای دیگری مانند کاهش تعداد تکرارهای آموزش نیز ممکن است. پس از ارزیابی همهٔ نامزدها، فقط بهترینها به دور دوم میروند و منابع بیشتری دریافت میکنند. پس از چند دور، نامزدهای نهایی با منابع کامل ارزیابی میشوند. این روش میتواند زمان تنظیم فراپارامترها را کاهش دهد.
روشهای Ensemble
راه دیگر برای Fine-Tune کردن سامانه ترکیب مدلهایی است که بهترین عملکرد را دارند. گروه یا Ensemble اغلب از بهترین مدل منفرد بهتر عمل میکند، همانطور که جنگل تصادفی از درختهای تصمیم منفرد تشکیلدهندهاش بهتر است؛ بهویژه وقتی مدلهای منفرد انواع متفاوتی از خطا ایجاد کنند. برای نمونه میتوانید یک مدل k نزدیکترین همسایه را آموزش و تنظیم کنید و سپس مدل Ensemble سادهای بسازید که میانگین پیشبینی جنگل تصادفی و آن مدل را برگرداند. این موضوع در فصل ۷ با جزئیات بیشتری بررسی میشود.
تحلیل بهترین مدلها و خطاهایشان
با بررسی بهترین مدلها اغلب بینش خوبی دربارهٔ مسئله به دست میآورید. برای نمونه RandomForestRegressor میتواند اهمیت نسبی هر ویژگی را برای پیشبینی دقیق نشان دهد:
>>> final_model = rnd_search.best_estimator_ # includes preprocessing
>>> feature_importances = final_model["random_forest"].feature_importances_
>>> feature_importances.round(2)
array([0.07, 0.05, 0.05, 0.01, 0.01, 0.01, 0.01, 0.19, [...], 0.01])
امتیازهای اهمیت را نزولی مرتب میکنیم و کنار نام ویژگی متناظر نمایش میدهیم:
>>> sorted(zip(feature_importances,
... final_model["preprocessing"].get_feature_names_out()),
... reverse=True)
...
[(0.18694559869103852, 'log__median_income'),
(0.0748194905715524, 'cat__ocean_proximity_INLAND'),
(0.06926417748515576, 'bedrooms__ratio'),
(0.05446998753775219, 'rooms_per_house__ratio'),
(0.05262301809680712, 'people_per_house__ratio'),
(0.03819415873915732, 'geo__Cluster 0 similarity'),
[...]
(0.00015061247730531558, 'cat__ocean_proximity_NEAR BAY'),
(7.301686597099842e-05, 'cat__ocean_proximity_ISLAND')]
با این اطلاعات ممکن است بخواهید بعضی ویژگیهای کمفایده را حذف کنید؛ مثلاً ظاهراً فقط یکی از دستههای ocean_proximity واقعاً مفید است، بنابراین میتوانید حذف بقیه را امتحان کنید.
همچنین باید خطاهای مشخصی را که سامانه ایجاد میکند بررسی کنید، علت آنها را بفهمید و ببینید چه چیزی مشکل را اصلاح میکند: افزودن ویژگیهای بیشتر، حذف ویژگیهای بیاطلاع، پاکسازی دادههای پرت و مانند آن.
اکنون زمان خوبی است مطمئن شوید مدل نهتنها بهطور میانگین، بلکه برای همهٔ دستههای ناحیهها نیز خوب کار میکند: روستایی یا شهری، ثروتمند یا فقیر، شمالی یا جنوبی، اقلیت یا غیره. ساخت زیرمجموعههای جدا از مجموعهٔ اعتبارسنجی برای هر دسته کمی کار میبرد، اما مهم است. اگر مدل برای یک دستهٔ کامل عملکرد ضعیفی دارد، احتمالاً تا رفع مسئله نباید مستقر شود، یا دستکم نباید برای آن دسته پیشبینی انجام دهد، زیرا ممکن است بیشتر از آنکه کمک کند آسیب برساند.
ارزیابی سامانه روی مجموعهٔ آزمون
پس از مدتی تنظیم مدلها، سرانجام سامانهای دارید که بهاندازهٔ کافی خوب کار میکند و آمادهاید مدل نهایی را روی مجموعهٔ آزمون بسنجید. کار خاصی لازم نیست: پیشبینها و برچسبها را از مجموعهٔ آزمون جدا کنید، مدل نهایی را برای تبدیل داده و پیشبینی اجرا کنید و سپس پیشبینیها را ارزیابی نمایید:
X_test = strat_test_set.drop("median_house_value", axis=1)
y_test = strat_test_set["median_house_value"].copy()
final_predictions = final_model.predict(X_test)
final_rmse = mean_squared_error(y_test, final_predictions, squared=False)
print(final_rmse) # prints 41424.40026462184
گاهی یک برآورد نقطهای از خطای تعمیم برای تصمیم به راهاندازی کافی نیست. مثلاً اگر مدل فقط ۰٫۱ درصد بهتر از مدل عملیاتی فعلی باشد، لازم است بدانید این برآورد چقدر دقیق است. میتوانید بازهٔ اطمینان ۹۵ درصدی خطای تعمیم را با scipy.stats.t.interval() محاسبه کنید. در این مثال بازهٔ نسبتاً بزرگی از ۳۹٬۲۷۵ تا ۴۳٬۴۶۷ به دست میآید و برآورد نقطهای قبلی، ۴۱٬۴۲۴، تقریباً در میانهٔ آن قرار دارد:
>>> from scipy import stats
>>> confidence = 0.95
>>> squared_errors = (final_predictions - y_test) ** 2
>>> np.sqrt(stats.t.interval(confidence, len(squared_errors) - 1,
... loc=squared_errors.mean(),
... scale=stats.sem(squared_errors)))
...
array([39275.40861216, 43467.27680583])