ساخت مجموعهٔ آزمون
ممکن است در این مرحله کنارگذاشتن داوطلبانهٔ بخشی از داده عجیب به نظر برسد. تا اینجا فقط نگاهی سریع به داده انداختهاید و منطقی است فکر کنید پیش از انتخاب الگوریتم باید اطلاعات بسیار بیشتری دربارهٔ آن به دست آورید. این حرف درست است، اما مغز انسان سامانهای شگفتانگیز برای کشف الگو است و درست به همین دلیل به بیشبرازش نیز بسیار مستعد است. اگر مجموعهٔ آزمون را ببینید، ممکن است ناخواسته الگویی ظاهراً جالب در آن پیدا کنید و بر پایهٔ آن نوع خاصی از مدل یادگیری ماشین را انتخاب کنید. سپس وقتی خطای تعمیم را با همان مجموعهٔ آزمون اندازه میگیرید، برآوردتان بیش از حد خوشبینانه میشود و سامانهای را راهاندازی میکنید که در عمل به خوبی انتظار شما کار نمیکند. این پدیده «سوگیری ناشی از وارسی داده» یا data snooping bias نام دارد.
در نظریه، ساخت مجموعهٔ آزمون ساده است: تعدادی نمونه را بهصورت تصادفی، معمولاً حدود ۲۰ درصد کل مجموعهداده - یا کمتر اگر مجموعهداده بسیار بزرگ باشد - انتخاب و کنار بگذارید:
import numpy as np
def shuffle_and_split_data(data, test_ratio):
shuffled_indices = np.random.permutation(len(data))
test_set_size = int(len(data) * test_ratio)
test_indices = shuffled_indices[:test_set_size]
train_indices = shuffled_indices[test_set_size:]
return data.iloc[train_indices], data.iloc[test_indices]
اکنون میتوانید تابع را به این صورت استفاده کنید:
>>> train_set, test_set = shuffle_and_split_data(housing, 0.2)
>>> len(train_set)
16512
>>> len(test_set)
4128
این روش کار میکند، اما بینقص نیست: اگر برنامه را دوباره اجرا کنید، مجموعهٔ آزمون دیگری ساخته میشود. با گذشت زمان، شما یا الگوریتم یادگیری ماشین عملاً همهٔ مجموعهداده را خواهید دید؛ دقیقاً چیزی که میخواستید از آن جلوگیری کنید.
یک راه این است که مجموعهٔ آزمون را در نخستین اجرا ذخیره کنید و در اجراهای بعد همان را بارگذاری کنید. راه دیگر این است که پیش از فراخوانی np.random.permutation()، Seed مولد اعداد تصادفی را مثلاً با np.random.seed(42) تنظیم کنید تا همیشه همان اندیسهای برزدهشده تولید شوند.۶
بااینحال هر دو راه وقتی نسخهٔ تازهای از مجموعهداده دریافت کنید دچار مشکل میشوند. برای داشتن تقسیم آموزش/آزمون پایدار حتی پس از بهروزرسانی داده، یک روش رایج این است که از شناسهٔ هر نمونه برای تصمیمگیری دربارهٔ قرارگرفتن آن در مجموعهٔ آزمون استفاده کنید؛ به شرط آنکه نمونهها شناسههای یکتا و تغییرناپذیر داشته باشند. برای مثال میتوانید Hash شناسهٔ هر نمونه را محاسبه کنید و اگر مقدار Hash کمتر یا مساوی ۲۰ درصد بیشینهٔ مقدار Hash بود، نمونه را در مجموعهٔ آزمون قرار دهید. این روش تضمین میکند مجموعهٔ آزمون در اجراهای مختلف، حتی پس از تازهسازی مجموعهداده، سازگار بماند. مجموعهٔ آزمون جدید شامل ۲۰ درصد نمونههای جدید خواهد بود، اما هیچ نمونهای که قبلاً در مجموعهٔ آموزشی بوده به مجموعهٔ آزمون منتقل نمیشود.
یک پیادهسازی ممکن:
from zlib import crc32
def is_id_in_test_set(identifier, test_ratio):
return crc32(np.int64(identifier)) < test_ratio * 2**32
def split_data_with_id_hash(data, test_ratio, id_column):
ids = data[id_column]
in_test_set = ids.apply(lambda id_: is_id_in_test_set(id_, test_ratio))
return data.loc[~in_test_set], data.loc[in_test_set]
متأسفانه مجموعهدادهٔ مسکن ستون شناسه ندارد. سادهترین راه این است که از اندیس سطر بهعنوان شناسه استفاده کنید:
housing_with_id = housing.reset_index() # adds an `index` column
train_set, test_set = split_data_with_id_hash(housing_with_id, 0.2, "index")
اگر اندیس سطر را شناسهٔ یکتا در نظر بگیرید، باید مطمئن باشید دادهٔ جدید همیشه به انتهای مجموعهداده افزوده میشود و هیچ سطری هرگز حذف نمیگردد. اگر چنین تضمینی ندارید، میتوانید از پایدارترین ویژگیها برای ساخت یک شناسهٔ یکتا استفاده کنید. برای مثال، عرض و طول جغرافیایی یک ناحیه برای چند میلیون سال پایدار خواهند ماند، بنابراین میتوانید آنها را به شکل زیر ترکیب کنید:۷
housing_with_id["id"] = housing["longitude"] * 1000 + housing["latitude"]
train_set, test_set = split_data_with_id_hash(housing_with_id, 0.2, "id")
Scikit-Learn چند تابع برای تقسیم مجموعهداده به زیرمجموعههای گوناگون ارائه میکند. سادهترین آنها train_test_split() است که تقریباً همان کار تابع shuffle_and_split_data() را انجام میدهد، اما چند قابلیت اضافی دارد. نخست، پارامتر random_state اجازه میدهد Seed مولد تصادفی را تعیین کنید. دوم، میتوانید چند مجموعهداده با تعداد سطر یکسان به آن بدهید تا همه را روی اندیسهای یکسان تقسیم کند؛ این قابلیت وقتی برچسبها در DataFrame جداگانهای قرار دارند بسیار مفید است:
from sklearn.model_selection import train_test_split
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
خطر سوگیری نمونهگیری
تا اینجا فقط روشهای نمونهگیری کاملاً تصادفی را در نظر گرفتیم. اگر مجموعهداده به اندازهٔ کافی بزرگ باشد - بهویژه در مقایسه با تعداد ویژگیها - این روش عموماً مناسب است؛ اما در مجموعههای کوچکتر خطر ایجاد سوگیری نمونهگیری جدی وجود دارد.
وقتی کارکنان یک شرکت نظرسنجی میخواهند برای پرسیدن چند سؤال با ۱۰۰۰ نفر تماس بگیرند، صرفاً ۱۰۰۰ نام تصادفی از دفترچه تلفن انتخاب نمیکنند. آنها میکوشند این ۱۰۰۰ نفر، با توجه به پرسشهایی که مطرح میشود، نمایندهٔ کل جمعیت باشند. برای نمونه، جمعیت ایالات متحده ۵۱٫۱ درصد زن و ۴۸٫۹ درصد مرد است؛ بنابراین یک نظرسنجی درست تلاش میکند این نسبت را در نمونه حفظ کند: ۵۱۱ زن و ۴۸۹ مرد، دستکم اگر احتمال داده شود پاسخها با جنسیت تفاوت داشته باشند.
این روش «نمونهگیری طبقهبندیشده» نام دارد: جمعیت به زیرگروههای همگن، یا «طبقهها»، تقسیم میشود و از هر طبقه تعداد مناسبی نمونه انتخاب میشود تا مجموعهٔ آزمون نمایندهٔ کل جمعیت باشد. اگر مسئولان نظرسنجی از نمونهگیری صرفاً تصادفی استفاده کنند، حدود ۱۰٫۷ درصد احتمال دارد نمونهای نامتوازن با کمتر از ۴۸٫۵ درصد یا بیش از ۵۳٫۵ درصد شرکتکنندهٔ زن به دست آید؛ در هر دو حالت نتیجهٔ نظرسنجی احتمالاً سوگیری محسوسی خواهد داشت.
فرض کنید پس از گفتوگو با کارشناسان متوجه شدهاید درآمد میانه ویژگی بسیار مهمی برای پیشبینی قیمت میانهٔ مسکن است. در این صورت بهتر است مطمئن شوید مجموعهٔ آزمون نمایندهٔ دستههای مختلف درآمدی در کل مجموعهداده است. چون درآمد میانه یک ویژگی عددی پیوسته است، ابتدا باید یک ویژگی دستهای برای درآمد بسازید.
با نگاه دقیقتر به هیستوگرام درآمد میانه، بیشتر مقادیر در بازهٔ ۱٫۵ تا ۶ - یعنی تقریباً ۱۵ هزار تا ۶۰ هزار دلار - متمرکزند، اما بعضی درآمدها بسیار بالاتر از ۶ میروند. لازم است در هر طبقه تعداد کافی نمونه وجود داشته باشد؛ وگرنه برآورد اهمیت آن طبقه ممکن است سوگیری پیدا کند. بنابراین نباید تعداد طبقهها بیش از حد زیاد باشد و هر طبقه باید بهاندازهٔ کافی بزرگ بماند. کد زیر با pd.cut() ویژگی دستهٔ درآمد را با پنج دسته، برچسبخورده از ۱ تا ۵، میسازد. دستهٔ ۱ از صفر تا ۱٫۵ - کمتر از ۱۵ هزار دلار -، دستهٔ ۲ از ۱٫۵ تا ۳ و به همین ترتیب ادامه دارد:
housing["income_cat"] = pd.cut(housing["median_income"],
bins=[0., 1.5, 3.0, 4.5, 6., np.inf],
labels=[1, 2, 3, 4, 5])
دستههای درآمدی را میتوان با کد زیر بهصورت نمودار ستونی نمایش داد:
housing["income_cat"].value_counts().sort_index().plot.bar(rot=0, grid=True)
plt.xlabel("Income category")
plt.ylabel("Number of districts")
plt.show()
شکل ۲-۹. هیستوگرام دستههای درآمد
نمونهگیری طبقهبندیشده با Scikit-Learn
اکنون آمادهاید نمونهگیری طبقهبندیشده را بر اساس دستهٔ درآمد انجام دهید. Scikit-Learn در بستهٔ sklearn.model_selection چند کلاس تقسیمکننده دارد که راهبردهای گوناگونی برای تقسیم مجموعهداده به مجموعهٔ آموزشی و آزمون پیادهسازی میکنند. هر تقسیمکننده متد split() دارد که یک Iterator از تقسیمهای مختلف آموزش/آزمون روی همان داده بازمیگرداند.
دقیقتر بگوییم، split() اندیسهای آموزش و آزمون را تولید میکند، نه خود داده را. داشتن چند تقسیم وقتی بخواهید عملکرد مدل را دقیقتر برآورد کنید سودمند است؛ بعداً در همین فصل هنگام بحث دربارهٔ اعتبارسنجی متقابل این موضوع را خواهید دید. مثال زیر ده تقسیم طبقهبندیشدهٔ متفاوت از یک مجموعهداده میسازد:
from sklearn.model_selection import StratifiedShuffleSplit
splitter = StratifiedShuffleSplit(n_splits=10, test_size=0.2, random_state=42)
strat_splits = []
for train_index, test_index in splitter.split(housing, housing["income_cat"]):
strat_train_set_n = housing.iloc[train_index]
strat_test_set_n = housing.iloc[test_index]
strat_splits.append([strat_train_set_n, strat_test_set_n])
فعلاً فقط از نخستین تقسیم استفاده کنید:
strat_train_set, strat_test_set = strat_splits[0]
و چون نمونهگیری طبقهبندیشده بسیار رایج است، برای گرفتن فقط یک تقسیم میتوانید از روش کوتاهتر، یعنی train_test_split() همراه آرگومان stratify، استفاده کنید:
strat_train_set, strat_test_set = train_test_split(
housing, test_size=0.2, stratify=housing["income_cat"], random_state=42)
برای بررسی نتیجه، ابتدا نسبت دستههای درآمد را در مجموعهٔ آزمون ببینید:
>>> strat_test_set["income_cat"].value_counts() / len(strat_test_set)
3 0.350533
2 0.318798
4 0.176357
5 0.114341
1 0.039971
Name: income_cat, dtype: float64
با کدی مشابه میتوانید نسبت دستههای درآمد را در کل مجموعهداده نیز اندازه بگیرید. جدول شکل ۲-۱۰ نسبت دستههای درآمد را در کل داده، در مجموعهٔ آزمون ساختهشده با نمونهگیری طبقهبندیشده و در مجموعهٔ آزمون ساختهشده با نمونهگیری صرفاً تصادفی مقایسه میکند. همانطور که دیده میشود، نسبتها در نمونهٔ طبقهبندیشده تقریباً با کل مجموعهداده یکساناند، درحالیکه نمونهٔ تصادفی انحراف دارد.
شکل ۲-۱۰. مقایسهٔ سوگیری نمونهگیری: طبقهبندیشده در برابر کاملاً تصادفی
| دستهٔ درآمد | کل داده، ٪ | طبقهبندیشده، ٪ | تصادفی، ٪ | خطای طبقهبندیشده، ٪ | خطای تصادفی، ٪ |
| ۱ | 3.98 | 4.00 | 4.24 | 0.36 | 6.45 |
| ۲ | 31.88 | 31.88 | 30.74 | -0.02 | -3.59 |
| ۳ | 35.06 | 35.05 | 34.52 | -0.01 | -1.54 |
| ۴ | 17.63 | 17.64 | 18.41 | 0.03 | 4.42 |
| ۵ | 11.44 | 11.43 | 12.09 | -0.08 | 5.63 |
دیگر به ستون income_cat نیاز ندارید؛ بنابراین بهتر است آن را حذف کنید تا داده به حالت اصلی بازگردد:
for set_ in (strat_train_set, strat_test_set):
set_.drop("income_cat", axis=1, inplace=True)
برای ساخت مجموعهٔ آزمون زمان نسبتاً زیادی صرف کردیم و دلیل خوبی هم داشت: این بخش از پروژهٔ یادگیری ماشین اغلب نادیده گرفته میشود، درحالیکه بسیار حیاتی است. افزون بر این، بسیاری از این ایدهها هنگام بررسی اعتبارسنجی متقابل دوباره به کار خواهند آمد. حالا وقت رفتن به مرحلهٔ بعد است: کاوش داده.
کاوش و مصورسازی داده برای کسب بینش
تا اینجا فقط نگاهی سریع به داده انداختهاید تا تصویری کلی از نوع دادهای که با آن کار میکنید به دست آورید. اکنون هدف این است که کمی عمیقتر شوید. نخست مطمئن شوید مجموعهٔ آزمون را کنار گذاشتهاید و فقط مجموعهٔ آموزشی را کاوش میکنید. اگر مجموعهٔ آموزشی بسیار بزرگ باشد، شاید بهتر باشد برای مرحلهٔ کاوش نمونهٔ کوچکتری بردارید تا دستکاریها آسان و سریع باشند؛ اما در اینجا مجموعهٔ آموزشی آنقدر کوچک است که میتوانید مستقیماً روی کل آن کار کنید.