پیش از ادامه، دوباره با کپیکردن strat_train_set به یک مجموعهٔ آموزشی پاک برگردید. همچنین بهتر است پیشبینها را از برچسبها جدا کنید، زیرا لزوماً نمیخواهید همان تبدیلهایی را که روی ورودیها اعمال میکنید روی مقادیر هدف نیز اجرا کنید. توجه کنید drop() یک نسخهٔ جدید میسازد و strat_train_set را تغییر نمیدهد:
housing = strat_train_set.drop("median_house_value", axis=1)
housing_labels = strat_train_set["median_house_value"].copy()
پاکسازی داده
بیشتر الگوریتمهای یادگیری ماشین نمیتوانند با ویژگیهای گمشده کار کنند؛ بنابراین باید آنها را مدیریت کنید. قبلاً دیدید که ویژگی total_bedrooms تعدادی مقدار گمشده دارد. سه راه اصلی وجود دارد:
- ناحیههای متناظر را حذف کنید.
- کل ویژگی را حذف کنید.
- مقادیر گمشده را با مقداری دیگر - صفر، میانگین، میانه و مانند آن - جایگزین کنید. این کار «جایگذاری مقدار گمشده» یا imputation نام دارد.
این گزینهها با متدهای dropna()، drop() و fillna() در Pandas DataFrame بهسادگی قابل اجرا هستند:
housing.dropna(subset=["total_bedrooms"], inplace=True) # option 1
housing.drop("total_bedrooms", axis=1) # option 2
median = housing["total_bedrooms"].median() # option 3
housing["total_bedrooms"].fillna(median, inplace=True)
گزینهٔ سوم کمترین تخریب را دارد، اما بهجای کد بالا از کلاس کاربردی SimpleImputer در Scikit-Learn استفاده میکنیم. مزیت این کلاس آن است که مقدار میانهٔ هر ویژگی را نگه میدارد؛ بنابراین میتوانید مقادیر گمشده را نهفقط در مجموعهٔ آموزشی، بلکه در مجموعهٔ اعتبارسنجی، مجموعهٔ آزمون و هر دادهٔ جدیدی که بعداً به مدل میرسد نیز جایگزین کنید.
ابتدا یک نمونهٔ SimpleImputer بسازید و مشخص کنید که مقادیر گمشدهٔ هر ویژگی با میانهٔ همان ویژگی جایگزین شوند:
from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy="median")
چون میانه فقط روی ویژگیهای عددی قابل محاسبه است، نسخهای از داده بسازید که فقط ویژگیهای عددی را داشته باشد و در نتیجه ویژگی متنی ocean_proximity را کنار بگذارد:
housing_num = housing.select_dtypes(include=[np.number])
اکنون با متد fit() نمونهٔ Imputer را روی دادهٔ آموزشی برازش دهید:
imputer.fit(housing_num)
Imputer فقط میانهٔ هر ویژگی را محاسبه کرده و نتیجه را در متغیر نمونهای statistics_ ذخیره کرده است. در دادهٔ فعلی فقط total_bedrooms مقدار گمشده داشت، اما هیچ تضمینی وجود ندارد که پس از عملیاتیشدن سامانه، دادهٔ جدید در ویژگیهای دیگر مقدار گمشده نداشته باشد؛ بنابراین امنتر است Imputer را روی همهٔ ویژگیهای عددی اعمال کنید:
>>> imputer.statistics_
array([-118.51 , 34.26 , 29. , 2125. , 434. , 1167. ,
408. , 3.5385])
>>> housing_num.median().values
array([-118.51 , 34.26 , 29. , 2125. , 434. , 1167. ,
408. , 3.5385])
حالا با Imputer «آموزشدیده» مجموعهٔ آموزشی را تبدیل کنید و مقادیر گمشده را با میانههای آموختهشده جایگزین نمایید:
X = imputer.transform(housing_num)
مقادیر گمشده میتوانند با میانگین - strategy="mean" -، پرتکرارترین مقدار - strategy="most_frequent" - یا مقدار ثابت - strategy="constant" همراه با fill_value=... - نیز جایگزین شوند. دو راهبرد آخر از دادههای غیرعددی هم پشتیبانی میکنند.
طراحی Scikit-Learn
API کتابخانهٔ Scikit-Learn بسیار خوب طراحی شده است. اصول اصلی طراحی آن به شرح زیرند:۹
سازگاری
همهٔ اشیا رابطی ساده و سازگار دارند:
- Estimatorها
- هر شیئی که بتواند بر پایهٔ یک مجموعهداده پارامترهایی را برآورد کند «Estimator» نامیده میشود؛ برای نمونه
SimpleImputer یک Estimator است. خود برآورد با متد fit() انجام میشود. این متد یک مجموعهداده میگیرد، یا در الگوریتمهای یادگیری نظارتشده دو مجموعهداده میگیرد که دومی برچسبها را نگه میدارد. هر پارامتر دیگری که برای هدایت فرایند برآورد لازم باشد یک «فراپارامتر» محسوب میشود؛ مانند strategy در SimpleImputer. فراپارامتر باید بهعنوان متغیر نمونه، معمولاً از طریق پارامتر سازنده، تنظیم شود.
- Transformerها
- بعضی Estimatorها، مانند
SimpleImputer، میتوانند مجموعهداده را نیز تبدیل کنند؛ اینها Transformer نامیده میشوند. رابط باز هم ساده است: تبدیل با متد transform() و مجموعهدادهای که باید تبدیل شود انجام میشود و نتیجه مجموعهدادهٔ تبدیلشده است. این تبدیل معمولاً به پارامترهای آموختهشده متکی است، همانطور که در SimpleImputer رخ میدهد. همهٔ Transformerها متد کمکی fit_transform() هم دارند که معادل فراخوانی fit() و سپس transform() است؛ گاهی fit_transform() بهینه شده و بسیار سریعتر اجرا میشود.
- Predictorها
- برخی Estimatorها پس از دریافت یک مجموعهداده میتوانند پیشبینی انجام دهند؛ اینها Predictor نامیده میشوند. برای نمونه مدل
LinearRegression فصل قبل یک Predictor بود: با دریافت سرانهٔ تولید ناخالص داخلی یک کشور، رضایت از زندگی را پیشبینی میکرد. Predictor متد predict() دارد که مجموعهای از نمونههای جدید را میگیرد و مجموعهای از پیشبینیهای متناظر بازمیگرداند. همچنین متد score() کیفیت پیشبینی را روی مجموعهٔ آزمون، و در یادگیری نظارتشده همراه برچسبهای متناظر، اندازه میگیرد.۱۰
بازرسی
تمام فراپارامترهای Estimator مستقیماً از طریق متغیرهای نمونهٔ عمومی قابل دسترسیاند؛ مثلاً imputer.strategy. تمام پارامترهای آموختهشدهٔ Estimator نیز از طریق متغیرهای نمونهٔ عمومی با پسوند زیرخط در دسترس قرار میگیرند؛ مثل imputer.statistics_.
جلوگیری از تکثیر کلاسها
مجموعهدادهها بهجای کلاسهای اختصاصی ساختهشده در Scikit-Learn، با آرایههای NumPy یا ماتریسهای Sparse در SciPy نمایش داده میشوند. فراپارامترها نیز رشتهها و اعداد معمول Python هستند.
ترکیبپذیری
تا حد ممکن از بلوکهای موجود دوباره استفاده میشود. برای نمونه، میتوان بهسادگی یک Estimator از نوع Pipeline را از دنبالهای دلخواه از Transformerها و سپس یک Estimator نهایی ساخت؛ کمی جلوتر این کار را خواهید دید.
مقادیر پیشفرض معقول
Scikit-Learn برای بیشتر پارامترها مقادیر پیشفرض منطقی فراهم میکند و همین امر ساخت سریع یک سامانهٔ پایهٔ قابل کار را آسان میسازد.
Transformerهای Scikit-Learn حتی وقتی ورودیشان Pandas DataFrame باشد، معمولاً آرایهٔ NumPy و گاهی ماتریس Sparse از SciPy برمیگردانند.۱۱ بنابراین خروجی imputer.transform(housing_num) یک آرایهٔ NumPy است؛ X دیگر نام ستون یا اندیس ندارد. خوشبختانه میتوان بهراحتی آن را دوباره در DataFrame پیچید و نام ستونها و اندیس را از housing_num بازیابی کرد:
housing_tr = pd.DataFrame(X, columns=housing_num.columns,
index=housing_num.index)
مدیریت ویژگیهای متنی و دستهای
تا اینجا فقط با ویژگیهای عددی کار کردهایم، اما داده میتواند ویژگی متنی هم داشته باشد. در این مجموعهداده فقط یک مورد وجود دارد: ocean_proximity. چند مقدار نخست را ببینید:
>>> housing_cat = housing[["ocean_proximity"]]
>>> housing_cat.head(8)
ocean_proximity
13096 NEAR BAY
14973 <1H OCEAN
3785 INLAND
14689 INLAND
20507 NEAR OCEAN
1286 INLAND
18078 <1H OCEAN
4396 NEAR BAY
این متن دلخواه نیست: فقط تعداد محدودی مقدار ممکن وجود دارد و هر مقدار یک دسته را نشان میدهد. پس ocean_proximity یک ویژگی دستهای است. بیشتر الگوریتمهای یادگیری ماشین ترجیح میدهند با عدد کار کنند؛ بنابراین دستهها را از متن به عدد تبدیل میکنیم. برای این کار میتوان از کلاس رسمی OrdinalEncoder در Scikit-Learn استفاده کرد:
from sklearn.preprocessing import OrdinalEncoder
ordinal_encoder = OrdinalEncoder()
housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
چند مقدار نخست کدگذاریشده چنیناند:
>>> housing_cat_encoded[:8]
array([[3.],
[0.],
[1.],
[1.],
[4.],
[1.],
[0.],
[3.]])
فهرست دستهها از متغیر نمونهٔ categories_ قابل دریافت است. این متغیر فهرستی شامل یک آرایهٔ یکبعدی از دستهها برای هر ویژگی دستهای است؛ در اینجا چون فقط یک ویژگی دستهای داریم، فهرست تنها یک آرایه دارد:
>>> ordinal_encoder.categories_
[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
dtype=object)]
یک مشکل این نمایش آن است که الگوریتمهای یادگیری ماشین فرض میکنند دو مقدار عددی نزدیک، از دو مقدار دورتر شبیهترند. این فرض برای دستههای ترتیبی مانند «بد»، «متوسط»، «خوب» و «عالی» میتواند منطقی باشد، اما دربارهٔ ocean_proximity روشن است که درست نیست؛ برای مثال دستههای عددی ۰ و ۴ در واقع شبیهتر از ۰ و ۱ هستند.
راهحل رایج ایجاد یک ویژگی دودویی برای هر دسته است: یک ویژگی وقتی دسته <1H OCEAN باشد ۱ و در غیر این صورت ۰ است، ویژگی دیگری برای INLAND و همینطور ادامه پیدا میکند. این روش «One-Hot Encoding (کدگذاری تکداغ)» نام دارد، زیرا فقط یک ویژگی برابر ۱ - «داغ» - است و بقیه صفر - «سرد» - هستند. ویژگیهای جدید را گاهی «ویژگیهای Dummy» نیز مینامند. Scikit-Learn برای تبدیل مقادیر دستهای به بردارهای One-Hot، کلاس OneHotEncoder را فراهم میکند:
from sklearn.preprocessing import OneHotEncoder
cat_encoder = OneHotEncoder()
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
بهطور پیشفرض خروجی OneHotEncoder یک ماتریس Sparse از SciPy است، نه آرایهٔ NumPy:
>>> housing_cat_1hot
<16512x5 sparse matrix of type '<class 'numpy.float64'>'
with 16512 stored elements in Compressed Sparse Row format>
ماتریس Sparse نمایش بسیار کارآمدی برای ماتریسهایی است که بیشتر خانههایشان صفر است؛ درون حافظه فقط مقادیر ناصفر و موقعیت آنها ذخیره میشود. وقتی یک ویژگی دستهای صدها یا هزاران دسته داشته باشد، One-Hot Encoding ماتریسی بسیار بزرگ و تقریباً سراسر صفر با تنها یک «۱» در هر سطر ایجاد میکند. ماتریس Sparse دقیقاً برای چنین حالتی مناسب است: حافظهٔ زیادی ذخیره میکند و محاسبات را سرعت میدهد. میتوانید تقریباً مانند یک آرایهٔ دوبعدی عادی از آن استفاده کنید.۱۲ اگر به آرایهٔ متراکم NumPy نیاز داشتید، متد toarray() را فراخوانی کنید:
>>> housing_cat_1hot.toarray()
array([[0., 0., 0., 1., 0.],
[1., 0., 0., 0., 0.],
[0., 1., 0., 0., 0.],
...,
[0., 0., 0., 0., 1.],
[1., 0., 0., 0., 0.],
[0., 0., 0., 0., 1.]])
روش جایگزین این است که هنگام ساخت OneHotEncoder گزینهٔ sparse=False را تنظیم کنید؛ در این حالت متد transform() مستقیماً آرایهٔ متراکم NumPy برمیگرداند. همانند OrdinalEncoder فهرست دستهها از categories_ در دسترس است:
>>> cat_encoder.categories_
[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
dtype=object)]
Pandas نیز تابع get_dummies() دارد که هر ویژگی دستهای را به نمایش One-Hot با یک ویژگی دودویی برای هر دسته تبدیل میکند:
>>> df_test = pd.DataFrame({"ocean_proximity": ["INLAND", "NEAR BAY"]})
>>> pd.get_dummies(df_test)
ocean_proximity_INLAND ocean_proximity_NEAR BAY
0 1 0
1 0 1
ظاهر ساده و خوبی دارد، اما چرا بهجای OneHotEncoder از آن استفاده نکنیم؟ مزیت مهم OneHotEncoder این است که دستههایی را که با آنها آموزش دیده به خاطر میسپارد. این موضوع در محیط عملیاتی حیاتی است، زیرا مدل پس از استقرار باید دقیقاً همان ویژگیهایی را دریافت کند که هنگام آموزش دیده است؛ نه بیشتر و نه کمتر. اگر cat_encoder آموزشدیده را با transform() روی همان df_test اجرا کنیم:
>>> cat_encoder.transform(df_test)
array([[0., 1., 0., 0., 0.],
[0., 0., 0., 1., 0.]])
تفاوت روشن است: get_dummies() فقط دو دسته را در دادهٔ ورودی دیده بود، پس دو ستون ساخت؛ درحالیکه OneHotEncoder یک ستون برای هر دستهٔ آموختهشده و با ترتیب درست تولید کرد.
افزون بر این، اگر DataFrameی با یک دستهٔ ناشناخته، مثلاً <2H OCEAN، به get_dummies() بدهید، بدون اعتراض ستونی برای آن میسازد:
>>> df_test_unknown = pd.DataFrame({"ocean_proximity": ["<2H OCEAN", "ISLAND"]})
>>> pd.get_dummies(df_test_unknown)
ocean_proximity_<2H OCEAN ocean_proximity_ISLAND
0 1 0
1 0 1
اما OneHotEncoder دستهٔ ناشناخته را تشخیص میدهد و Exception ایجاد میکند. اگر ترجیح دهید میتوانید فراپارامتر handle_unknown را روی "ignore" بگذارید؛ در آن صورت دستهٔ ناشناخته با صفرها نمایش داده میشود:
>>> cat_encoder.handle_unknown = "ignore"
>>> cat_encoder.transform(df_test_unknown)
array([[0., 0., 0., 0., 0.],
[0., 0., 1., 0., 0.]])
هنگامی که هر Estimator در Scikit-Learn را با DataFrame برازش میدهید، Estimator نام ستونها را در ویژگی feature_names_in_ نگه میدارد. پس از آن Scikit-Learn بررسی میکند هر DataFrameی که مثلاً به transform() یا predict() میدهید همان نام ستونها را داشته باشد. Transformerها همچنین متد get_feature_names_out() دارند که برای ساخت DataFrame پیرامون خروجی Transformer مفید است:
>>> cat_encoder.feature_names_in_
array(['ocean_proximity'], dtype=object)
>>> cat_encoder.get_feature_names_out()
array(['ocean_proximity_<1H OCEAN', 'ocean_proximity_INLAND',
'ocean_proximity_ISLAND', 'ocean_proximity_NEAR BAY',
'ocean_proximity_NEAR OCEAN'], dtype=object)
>>> df_output = pd.DataFrame(cat_encoder.transform(df_test_unknown),
... columns=cat_encoder.get_feature_names_out(),
... index=df_test_unknown.index)
...
مقیاسبندی و تبدیل ویژگیها
یکی از مهمترین تبدیلهایی که باید روی داده انجام دهید «مقیاسبندی ویژگی» است. بهجز چند استثنا، الگوریتمهای یادگیری ماشین وقتی ویژگیهای عددی ورودی مقیاسهای بسیار متفاوت دارند خوب عمل نمیکنند. دادهٔ مسکن چنین وضعی دارد: تعداد کل اتاقها تقریباً از ۶ تا ۳۹٬۳۲۰ تغییر میکند، درحالیکه درآمد میانه فقط در بازهٔ صفر تا ۱۵ است. بدون مقیاسبندی، بسیاری از مدلها به سمت نادیدهگرفتن درآمد میانه و تمرکز بیش از حد بر تعداد اتاقها سوگیری پیدا میکنند.
دو روش رایج برای رساندن همهٔ ویژگیها به مقیاس مشابه وجود دارد: «مقیاسبندی کمینه-بیشینه» و «استانداردسازی».
«Min-Max Scaling (مقیاسبندی کمینه-بیشینه)»، که بسیاری آن را Normalization (نرمالسازی) مینامند، سادهترین روش است. برای هر ویژگی، مقادیر جابهجا و دوباره مقیاسبندی میشوند تا در بازهٔ صفر تا ۱ قرار گیرند. برای این کار کمینه از مقدار کم میشود و نتیجه بر تفاوت بیشینه و کمینه تقسیم میگردد. Scikit-Learn Transformer رسمی MinMaxScaler را برای این کار ارائه میکند. فراپارامتر feature_range اجازه میدهد بازه را تغییر دهید؛ مثلاً شبکههای عصبی معمولاً با ورودیهای با میانگین صفر بهتر کار میکنند، بنابراین بازهٔ ۱- تا ۱ میتواند ترجیح داده شود:
from sklearn.preprocessing import MinMaxScaler
min_max_scaler = MinMaxScaler(feature_range=(-1, 1))
housing_num_min_max_scaled = min_max_scaler.fit_transform(housing_num)
«Standardization (استانداردسازی)» متفاوت است: ابتدا میانگین را از مقدار کم میکند تا مقادیر استانداردشده میانگین صفر داشته باشند، سپس نتیجه را بر انحراف معیار تقسیم میکند تا انحراف معیار برابر ۱ شود. برخلاف مقیاسبندی کمینه-بیشینه، استانداردسازی مقادیر را به یک بازهٔ مشخص محدود نمیکند، اما نسبت به دادههای پرت بسیار کمتر حساس است.
برای نمونه، فرض کنید به اشتباه درآمد میانهٔ یک ناحیه ۱۰۰ ثبت شده باشد، درحالیکه بازهٔ معمول صفر تا ۱۵ است. مقیاسبندی کمینه-بیشینه در بازهٔ صفر تا ۱ این مقدار پرت را روی ۱ قرار میدهد و تمام مقادیر دیگر را در بازهٔ فشردهٔ صفر تا ۰٫۱۵ له میکند؛ اما استانداردسازی به این شدت تحت تأثیر قرار نمیگیرد. Scikit-Learn کلاس StandardScaler را برای استانداردسازی ارائه میکند:
from sklearn.preprocessing import StandardScaler
std_scaler = StandardScaler()
housing_num_std_scaled = std_scaler.fit_transform(housing_num)
وقتی توزیع یک ویژگی دنبالهٔ سنگین دارد - یعنی مقادیر دور از میانگین بهصورت نمایی نادر نیستند - هر دو روش کمینه-بیشینه و استانداردسازی بخش عمدهٔ مقادیر را در بازهای کوچک فشرده میکنند. مدلهای یادگیری ماشین عموماً چنین وضعی را دوست ندارند. بنابراین پیش از مقیاسبندی، بهتر است ابتدا ویژگی را تبدیل کنید تا دنبالهٔ سنگین کوتاهتر و در صورت امکان توزیع تقریباً متقارن شود.
برای ویژگیهای مثبت با دنبالهٔ سنگین سمت راست، روش رایج جایگزینکردن مقدار با جذر آن - یا بهتوانرساندن با توانی میان صفر و ۱ - است. اگر دنباله واقعاً بسیار بلند و سنگین، مانند توزیع قانون توانی، باشد، جایگزینکردن ویژگی با لگاریتمش میتواند کمک کند. برای نمونه، ویژگی جمعیت تقریباً از قانون توانی پیروی میکند: ناحیههای دارای ۱۰ هزار نفر فقط حدود ۱۰ برابر کمتر از ناحیههای دارای ۱۰۰۰ نفر دیده میشوند، نه بهصورت نمایی کمتر. در بخش بعدی خواهید دید محاسبهٔ لگاریتم چگونه توزیع این ویژگی را بسیار به توزیع Gaussian زنگولهای نزدیک میکند.