به نام خدا


یادگیری ماشین
مقدمه و داده‌ی مورد استفاده
هدف این متن آموزش اصول کلی یادگیری ماشین و نحوه انجام یک پروژه یادگیری ماشین از ابتدا تا انتهای آن می‌باشد در این بخش ما از مجموعه‌ی داده‌ی(Dataset) قیمت‌ خانه‌های کالیفرنیا (California Housing Prices dataset) از مخزن StatLib استفاده می‌کنیم. برای مقاصد آموزشی، یک ویژگی دسته‌ای به این مجموعه‌ی داده اضافه کرده‌ایم و چند ویژگی حذف شده‌اند.
در مرحله‌ی اول باید یک مدل از قیمت خانه‌های کالیفرنیا، با استفاده از این مجموعه‌ی داده، بسازیم. این مجموعه‌ی داده حاوی مقادیرِ جمعیت، میانه‌ی درآمد، میانه‌ی قیمت خانه و … برای هر منطقه از کالیفرنیا است.
مدل شما باید با داده آموزش ببیند و باید بتواند با داشتن تمام مقادیر دیگر، میانه‌ی قیمت خانه در هر منطقه را پیش‌بینی کند.
نصب و راه‌اندازی
برای استفاده از کدهای این دفترچه شما نیاز دارید که پایتون 2 یا 3 را روی سیستم خود نصب کرده باشید. همچین از کتاب‌خانه های Numpy و Pandas و Scikit-learn در این پروژه استفاده خواهد شد. این سه کتاب‌خانه اصلی‌ترین کتاب‌خانه‌ها برای کارهای یادگیری ماشین در زبان پایتون می‌باشند. برای رسم نمودارها نیز از کتاب‌خانه Matplotlib استفاده می‌شود. برای دنبال کردن بهتر این دفترچه، بهتر است که اصول مقدماتی برنامه‌نویسی در زبان پایتون را بلد باشید اما نیاز به دانش خاصی در حوزه یادگیری ماشین ندارید زیرا سعی شده است همه چیز به ساده‌ترین شکل توضیح داده شود.
در صورتی که از Anaconda استفاده می‌کنید این کتاب‌خانه‌ها بر روی پایتون شما نصب شده‌اند در غیر این صورت با اجرای دستورات زیر می‌توانید این کتاب‌خانه‌ها را نصب کنید. که چون در سیستمی که در حال حاضر از آن استفاده می‌کنیم این کتاب‌خانه‌ها وجود دارد با پیام زیر روبرو شده‌ایم.
In [1]:
!pip install numpy
!pip install matplotlib
!pip install pandas
!pip install scikit-learn
Requirement already satisfied: numpy in /home/sthossein/anaconda3/lib/python3.7/site-packages (1.15.4)
Requirement already satisfied: matplotlib in /home/sthossein/anaconda3/lib/python3.7/site-packages (3.0.2)
Requirement already satisfied: numpy>=1.10.0 in /home/sthossein/anaconda3/lib/python3.7/site-packages (from matplotlib) (1.15.4)
Requirement already satisfied: cycler>=0.10 in /home/sthossein/anaconda3/lib/python3.7/site-packages (from matplotlib) (0.10.0)
Requirement already satisfied: kiwisolver>=1.0.1 in /home/sthossein/anaconda3/lib/python3.7/site-packages (from matplotlib) (1.0.1)
Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in /home/sthossein/anaconda3/lib/python3.7/site-packages (from matplotlib) (2.3.0)
Requirement already satisfied: python-dateutil>=2.1 in /home/sthossein/anaconda3/lib/python3.7/site-packages (from matplotlib) (2.7.5)
Requirement already satisfied: six in /home/sthossein/anaconda3/lib/python3.7/site-packages (from cycler>=0.10->matplotlib) (1.12.0)
Requirement already satisfied: setuptools in /home/sthossein/anaconda3/lib/python3.7/site-packages (from kiwisolver>=1.0.1->matplotlib) (40.6.3)
Requirement already satisfied: pandas in /home/sthossein/anaconda3/lib/python3.7/site-packages (0.23.4)
Requirement already satisfied: python-dateutil>=2.5.0 in /home/sthossein/anaconda3/lib/python3.7/site-packages (from pandas) (2.7.5)
Requirement already satisfied: pytz>=2011k in /home/sthossein/anaconda3/lib/python3.7/site-packages (from pandas) (2018.7)
Requirement already satisfied: numpy>=1.9.0 in /home/sthossein/anaconda3/lib/python3.7/site-packages (from pandas) (1.15.4)
Requirement already satisfied: six>=1.5 in /home/sthossein/anaconda3/lib/python3.7/site-packages (from python-dateutil>=2.5.0->pandas) (1.12.0)
Requirement already satisfied: scikit-learn in /home/sthossein/anaconda3/lib/python3.7/site-packages (0.20.1)
Requirement already satisfied: numpy>=1.8.2 in /home/sthossein/anaconda3/lib/python3.7/site-packages (from scikit-learn) (1.15.4)
Requirement already satisfied: scipy>=0.13.3 in /home/sthossein/anaconda3/lib/python3.7/site-packages (from scikit-learn) (1.1.0)
در تکه کد زیر کتاب‌خانه numpy را اضافه کرده‌ایم و seed تصادفی آن را تنظیم کرده‌ایم. دقت کنید که مشخص کردن seed در ابتدای کد نقش مهمی در reproducibility نتایج تولید شده توسط کد شما دارد. تعداد زیادی از الگوریتم‌های یادگیری ماشین از مقادیر تصادفی برای مقداردهیِ اولیه استفاده می‌کنند بنابراین اگر seed را در ابتدای کد مشخص نکرده باشید در هر بار از اجرای کدتان نتیجه‌ی متفاوتی را به دست خواهید آورد.
در ادامه کتاب‌خانه Matplotlib اضافه شده است و حالت آن به inline تغییر کرده است. در صورتی که بخواهید نمودارهایی که رسم میکنید داخل Jupyter Notebook نمایش داده شوند باید این خط را به کد خودتان اضافه کنید. تنظیمات دیگر برای مشخص کردن سایز متن‌های داخل نمودار است. برای ذخیره‌سازی نمودارها نیز یک تابع نوشته شده است. این تابع یک پارامتر به نام $\tt{tight\_layout}$ دارد. در تصاویر زیر می‌توانید فرق نمودارهایی که با اضافه کردن این ویژگی و بدون آن رسم شده‌اند را ببینید. استفاده از این ویژگی هنگامی که می‌خواهید چند نمودار را در قالب یک تصویر نشان دهید، عادت خوبی است!
In [2]:
# To support both python 2 and python 3
from __future__ import division, print_function, unicode_literals

# Common imports
import numpy as np
import os

# to make this notebook's output stable across runs
np.random.seed(42)

# To plot pretty figures
%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rc('axes', labelsize=14)
mpl.rc('xtick', labelsize=12)
mpl.rc('ytick', labelsize=12)

# Where to save the figures
PROJECT_ROOT_DIR = "./"
IMAGES_PATH = os.path.join(PROJECT_ROOT_DIR, "figs/")

def save_fig(fig_id, tight_layout=True, fig_extension="png", resolution=300):
    path = os.path.join(IMAGES_PATH, fig_id + "." + fig_extension)
    print("Saving figure", fig_id)
    if tight_layout:
        plt.tight_layout()
    plt.savefig(path, format=fig_extension, dpi=resolution)

# Ignore useless warnings (see SciPy issue #5998)
import warnings
warnings.filterwarnings(action="ignore", message="^internal gelsd")
بارگیری (download) داده‌ها
تکه کد زیر داده‌ها را از اینترنت بارگیری می‌کند و آن را در پوشه‌ی dataset در کنار کد شما قرار می‌دهد و همچنین داده بارگیری شده را از حالت فشرده خارج می‌کند.
In [3]:
import os
import tarfile
from six.moves import urllib

DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml/master/"
HOUSING_PATH = os.path.join(PROJECT_ROOT_DIR, "datasets", "housing")
HOUSING_URL = DOWNLOAD_ROOT + "datasets/housing/housing.tgz"


def fetch_housing_data(housing_url=HOUSING_URL, housing_path=HOUSING_PATH):
    if not os.path.isdir(housing_path):
        os.makedirs(housing_path)
    tgz_path = os.path.join(housing_path, "housing.tgz")
    urllib.request.urlretrieve(housing_url, tgz_path)
    housing_tgz = tarfile.open(tgz_path)
    housing_tgz.extractall(path=housing_path)
    housing_tgz.close()


fetch_housing_data()
بار کردن (load) و کار با داده‌ها
ما از کتاب‌خانه Pandas برای بار کردن داده‌ها استفاده می‌کنیم. تکه کد زیر مجموعه‌ی داده‌ی بارگیری شده را که به صورت یک فایل با پسوند csv می‌باشد می‌خواند و و خروجی آن یک شیء از نوع $\texttt{Pandas.DataFrame}$ است. دیتافریم کتابخانه Pandas قابلیت‌های زیادی را در اختیار ما قرار می‌دهد که در ادامه با برخی از آنها آشنا می‌شویم.
In [4]:
import pandas as pd


def load_housing_data(housing_path=HOUSING_PATH):
    csv_path = os.path.join(housing_path, "housing.csv")
    return pd.read_csv(csv_path)
پس از فراخوانی تابع، با استفاده از تابع head پنچ سطر اول از جدول داده‌ها را مشاهده می‌کنیم.
In [5]:
housing = load_housing_data()
housing.head()
Out[5]:
longitude latitude housing_median_age total_rooms total_bedrooms population households median_income median_house_value ocean_proximity
0 -122.23 37.88 41.0 880.0 129.0 322.0 126.0 8.3252 452600.0 NEAR BAY
1 -122.22 37.86 21.0 7099.0 1106.0 2401.0 1138.0 8.3014 358500.0 NEAR BAY
2 -122.24 37.85 52.0 1467.0 190.0 496.0 177.0 7.2574 352100.0 NEAR BAY
3 -122.25 37.85 52.0 1274.0 235.0 558.0 219.0 5.6431 341300.0 NEAR BAY
4 -122.25 37.85 52.0 1627.0 280.0 565.0 259.0 3.8462 342200.0 NEAR BAY
همچنین با استفاده از تابع info می‌توان به اطلاعات کلی از قبیل تعداد سطر‌ها و ستون‌ها و ویژگی‌های هر ستون پی برد.
In [6]:
housing.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 20640 entries, 0 to 20639
Data columns (total 10 columns):
longitude             20640 non-null float64
latitude              20640 non-null float64
housing_median_age    20640 non-null float64
total_rooms           20640 non-null float64
total_bedrooms        20433 non-null float64
population            20640 non-null float64
households            20640 non-null float64
median_income         20640 non-null float64
median_house_value    20640 non-null float64
ocean_proximity       20640 non-null object
dtypes: float64(9), object(1)
memory usage: 1.6+ MB
با دانستن نام ستون می‌توان فقط آن ستون دلخواه را از کل داده‌ها انتخاب کرده و به کمک تابع $\tt{value\_counts}$ درباره مقادیر ممکن برای آن و تعداد تکرار هر کدام از آنها اطلاع پیدا کرد.
In [7]:
housing["ocean_proximity"].value_counts()
Out[7]:
<1H OCEAN     9136
INLAND        6551
NEAR OCEAN    2658
NEAR BAY      2290
ISLAND           5
Name: ocean_proximity, dtype: int64
با فراخوانی تابع $\tt{describe}$ آماره‌های مفیدی مانند میانگین و انحراف معیار و میانه و ... روی ستون‌های مختلف محاسبه خواهد‌شد.
In [8]:
housing.describe()
Out[8]:
longitude latitude housing_median_age total_rooms total_bedrooms population households median_income median_house_value
count 20640.000000 20640.000000 20640.000000 20640.000000 20433.000000 20640.000000 20640.000000 20640.000000 20640.000000
mean -119.569704 35.631861 28.639486 2635.763081 537.870553 1425.476744 499.539680 3.870671 206855.816909
std 2.003532 2.135952 12.585558 2181.615252 421.385070 1132.462122 382.329753 1.899822 115395.615874
min -124.350000 32.540000 1.000000 2.000000 1.000000 3.000000 1.000000 0.499900 14999.000000
25% -121.800000 33.930000 18.000000 1447.750000 296.000000 787.000000 280.000000 2.563400 119600.000000
50% -118.490000 34.260000 29.000000 2127.000000 435.000000 1166.000000 409.000000 3.534800 179700.000000
75% -118.010000 37.710000 37.000000 3148.000000 647.000000 1725.000000 605.000000 4.743250 264725.000000
max -114.310000 41.950000 52.000000 39320.000000 6445.000000 35682.000000 6082.000000 15.000100 500001.000000
هر Dataframe ای تابعی به نام $\tt{hist}$ دارد که با گرفتن پارامتر‌هایی مانند تعداد bin و ... نمودار هیستوگرام هر یک از ستون‌ها را رسم می‌کند. این نمودا با استفاده از تابع $\tt{save\_fig}$ که در ابتدا تعریف شد، ذخیره می‌شوند.
In [9]:
%matplotlib inline
import matplotlib.pyplot as plt
housing.hist(bins=50, figsize=(20,15))
save_fig("attribute_histogram_plots")
plt.show()
Saving figure attribute_histogram_plots
یکی از مهم‌ترین مفاهیم در یادگیری ماشین مفهوم Generalization است. ما برای آموزش دادن مدل خود از یک مجموعه داده استفاده میکنیم که Train Set ما است. اما ما برای ارزیابی مدل خود نمی‌توانیم از Train Set استفاده کنیم زیرا مدل ما قبلا این داده‌ها را دیده است و به آن ها برازش شده است. بنابراین عمل‌کرد مدلی که با Train Set آموزش دیده شده است روی داده‌هایی که تا به حال آن‌ها را ندیده است با احتمال خوبی بدتر از عمل‌کردش روی Train Set خواهد بود. به این پدیده که مدل ما عملکرد خوبی روی داده‌های آموزش دارد اما روی داده‌های جدید عمل‌کرد بدتری دارد بیش‌برازش یا Overfitting می‌گویند.(مثل دانش‌آموزی که درسهای خود را مفهومی نمی‌خواند و آن‌ها را حفظ می‌کند و به هنگام روربرو شدن با مساله‌ی جدید، ناتوان می‌شود :دی)
بنابراین اگر ما بخواهیم متوجه شویم که مدل ما روی داده هایی که تا به حال با آن ها روبرو نشده است چه عمل‌کردی دارد باید از قبل یک قسمت از داده‌هایمان را جدا کرده باشیم و فرآیند آموزش مدل را فقط با باقی داده‌ها انجام دهیم. بنابراین ما داده‌هایمان را به دو قسمت Train Set و Test Set تقسیم میکنیم. برای اینکه Test Set نمایان‌گر داده ای باشد که مدل ما تا به حال با آن روبرو نشده است باید به اصطلاح بعد از جداکردن Test Set آن را در یک گاوصندوق قرار دهیم و تا مشخص نشدن مدل نهایی ارزیابی نهایی را روی این مجموعه انجام ندهیم!

با استفاده از تابع $\tt{train\_test\_split}$ که در کتابخانه sklearn قرار دارد می‌توانیم داده‌هایمان را به دو مجموعه مجزای Train و Test تفکیک کنیم. ورودی $\tt{test\_size}$ در این تابع مشخص می‌کند که چه کسری از داده‌ها به عنوان داده تست در نظر گرفته شود. این تابع داده‌ها را به صورت تصادفی به دو مجموعه Train و Test تقسیم می‌کند. برای اینکه خروجی تابع در دفعات مختلف اجرا یکسان باقی بماند باید $\tt{random\_state}$ را به عنوان ورودی تابع مشخص کنیم.
In [10]:
from sklearn.model_selection import train_test_split

train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
در زیر 5 سطر اول داده‌های تست را می‌بینیم. مشاهده می‌کنیم که اندیس سطرها دیگر به ترتیب نیست و به صورت تصادفی است.
In [11]:
test_set.head()
Out[11]:
longitude latitude housing_median_age total_rooms total_bedrooms population households median_income median_house_value ocean_proximity
20046 -119.01 36.06 25.0 1505.0 NaN 1392.0 359.0 1.6812 47700.0 INLAND
3024 -119.46 35.14 30.0 2943.0 NaN 1565.0 584.0 2.5313 45800.0 INLAND
15663 -122.44 37.80 52.0 3830.0 NaN 1310.0 963.0 3.4801 500001.0 NEAR BAY
20484 -118.72 34.28 17.0 3051.0 NaN 1705.0 495.0 5.7376 218600.0 <1H OCEAN
9814 -121.93 36.62 34.0 2351.0 NaN 1063.0 428.0 3.7250 278000.0 NEAR OCEAN
فرض کنید که از دانش پیشین خود می‌دانیم که ویژگی $\tt{median\_income}$ ویژگی بسیار مهمی برای پیش بینی میانه قیمت خانه ها در هر ناحیه است. بنابراین میخواهیم که این ویژگی در داده های Train و Test به صورت متعادل تقسیم شده باشد.
In [12]:
housing["median_income"].hist()
Out[12]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f679461f400>
فرض کنید در یک مسئله ویژگی مشخصی که می‌خواهیم به صورت متعادل بین مجموعه Train و Test تقسیم شود، ویژگی جنسیت افراد باشد. در این صورت کافی است درصد تعداد مردان و زنان در مجموعه‌های Train و Test یکسان باشد. اما ویژگی $\tt{median\_income}$ یک ویژگی با مقادیر پیوسته است. بنابراین برای این که بتوانیم به صورت متعادل این ویژگی را بین مجموعه‌های Trainو Test تقسیم کنیم ابتدا یک ویژگی گسسته به نام $\tt{income\_cat}$ از روی این ویژگی می‌سازیم و سپس با استفاده از آن مجموعه Train و Test را درست می‌کنیم. ویژگی $\tt{income\_cat}$ مقادیر بین 1 تا 5 را اختیار می‌کند.
In [13]:
# Divide by 1.5 to limit the number of income categories
housing["income_cat"] = np.ceil(housing["median_income"] / 1.5)
# Label those above 5 as 5
housing["income_cat"].where(housing["income_cat"] < 5, 5.0, inplace=True)
housing["income_cat"].value_counts()
Out[13]:
3.0    7236
2.0    6581
4.0    3639
5.0    2362
1.0     822
Name: income_cat, dtype: int64
درصد تکرار هر کدام از این مقادیر در مجموعه‌ی تمام داده‌ها در زیر آمده است.
In [14]:
housing["income_cat"].value_counts() / len(housing)
Out[14]:
3.0    0.350581
2.0    0.318847
4.0    0.176308
5.0    0.114438
1.0    0.039826
Name: income_cat, dtype: float64
با مشخص کردن ورودی $\tt{stratify}$ در تابع $\tt{train\_test\_split}$ میتوانیم ویژگی ای را مشخص کنیم که به صورت متعادل بین Train و Test تقسیم شوند. در زیر برای هر دو حالت (تقسیم متعادل و تقسیم معمولی) می‌توانیم درصد تکرار هر کدام از مقادیر ۱ تا ۵ را در داده تست مشاهده کنیم. می‌بینیم که در حالتی که از $\tt{stratify}$ استفاده کرده باشیم درصد هر یک از ویژگی‌ها در مجموعه Test به درصد هرکدام از ویژگی‌ها در مجموعه داده‌های اولیه نزدیک‌تر است.
In [15]:
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
strat_train_set, strat_test_set = train_test_split(housing, test_size=0.2, random_state=42, stratify=housing['income_cat'])
In [16]:
test_set["income_cat"].value_counts() / len(test_set)
Out[16]:
3.0    0.358527
2.0    0.324370
4.0    0.167393
5.0    0.109496
1.0    0.040213
Name: income_cat, dtype: float64
In [17]:
strat_test_set["income_cat"].value_counts() / len(strat_test_set)
Out[17]:
3.0    0.350533
2.0    0.318798
4.0    0.176357
5.0    0.114583
1.0    0.039729
Name: income_cat, dtype: float64
حال می‌توانیم ستون $\tt{income\_cat}$ را که به صورت موقتی به داده‌ها اضافه کرده‌ایم را حذف کنیم.
In [18]:
strat_train_set.drop("income_cat", axis=1, inplace=True)
strat_test_set.drop("income_cat", axis=1, inplace=True)
/home/sthossein/anaconda3/lib/python3.7/site-packages/pandas/core/frame.py:3697: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
  errors=errors)
مصورسازی داده‌ها
در این قسمت با رسم نمودارهایی، اطلاعات کلی‌تری راجع به ساختار داده‌هایی که داریم به دست می‌آوریم. از این پس فقط با داده‌های Train ای که در قسمت قبل از داده‌های اصلی جدا کرده‌ایم کار می‌کنیم و دیگر به داده‌های Test دست نمی‌زنیم.
In [19]:
housing = strat_train_set.copy()
با فراخوانی تابع plot روی یک Dataframe میتوانیم یکی از ویژگی‌ها را بر حسب دیگری رسم کنیم.
In [20]:
housing.plot(kind="scatter", x="longitude", y="latitude")
save_fig("bad_visualization_plot")
Saving figure bad_visualization_plot
برای اینکه شهود بهتری از نحوه توزیع داده‌ها به دست بیاوریم می‌توانیم ورودی $\tt{alpha}$ را تنظیم کنیم تا جاهایی که داده های بیشتری وجود دارد پررنگ‌تر رسم شوند و بقیه جاها کمرنگ‌تر. این نمودار نحوه توزیع خانه‌ها را در طول و عرض‌های جغرافیایی مختلف نشان می‌دهد.
In [21]:
housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1)
save_fig("better_visualization_plot")
Saving figure better_visualization_plot
حال می‌خواهیم جمعیت ساکن هر منطقه و همچنین میانه‌ی قیمت خانه‌های هر منطقه را نیز به نموداری که در قسمت قبل رسم کردیم اضافه کنیم. برای اینکار سایز هرکدام از نقاطی که در نمودار رسم می‌کنیم را متناسب با ویژگی $\tt{population}$ در نظر می‌گیریم و رنگ آن را متناسب با $\tt{median\_house\_value}$ در نظر می‌گیریم. تکه کد پایین این نمودار را تولید می‌کند.
In [22]:
housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.4,
    s=housing["population"]/100, label="population", figsize=(10,7),
    c="median_house_value", cmap=plt.get_cmap("jet"), colorbar=True,
    sharex=False) # s is size and c is color
plt.legend()
save_fig("housing_prices_scatterplot")
Saving figure housing_prices_scatterplot
حال با اضافه کردن نقشه جغرافیایی کالیفرنیا در کنار نقشه بالا متوجه می‌شویم که ناحیه‌هایی که به دریا نزدیک‌تر هستند جمعیت ساکن بیشتری دارند و قیمت آن خانه‌ها نیز بیشتر است.
In [23]:
import matplotlib.image as mpimg
california_img=mpimg.imread(PROJECT_ROOT_DIR + '/figs/california.png')
ax = housing.plot(kind="scatter", x="longitude", y="latitude", figsize=(10,7),
                       s=housing['population']/100, label="Population",
                       c="median_house_value", cmap=plt.get_cmap("jet"),
                       colorbar=False, alpha=0.4,
                      )
plt.imshow(california_img, extent=[-124.55, -113.80, 32.45, 42.05], alpha=0.5,
           cmap=plt.get_cmap("jet"))
plt.ylabel("Latitude", fontsize=14)
plt.xlabel("Longitude", fontsize=14)

prices = housing["median_house_value"]
tick_values = np.linspace(prices.min(), prices.max(), 11)
cbar = plt.colorbar()
cbar.ax.set_yticklabels(["$%dk"%(round(v/1000)) for v in tick_values], fontsize=14)
cbar.set_label('Median House Value', fontsize=16)

plt.legend(fontsize=16)
save_fig("california_housing_prices_plot")
plt.show()
Saving figure california_housing_prices_plot
هدف پیش‌بینی $\tt{median\_house\_value}$ از روی بقیه ویژگی‌ها است. تکه کد پایین هم‌بستگی (correlation) هر کدام از ویژگی‌ها را با این ویژگی مشخص می‌کند و با استفاده از تابع $\tt{sort}$ آن‌ها را به ترتیب نزولی نمایش می‌دهد. مشاهده می‌کنیم که $\tt{median\_income}$ رابطه خطی نسبتا زیادی با $\tt{median\_house\_value}$ دارد.
In [24]:
corr_matrix = housing.corr()
corr_matrix["median_house_value"].sort_values(ascending=False)
Out[24]:
median_house_value    1.000000
median_income         0.687160
total_rooms           0.135097
housing_median_age    0.114110
households            0.064506
total_bedrooms        0.047689
population           -0.026920
longitude            -0.047432
latitude             -0.142724
Name: median_house_value, dtype: float64
با تابع $\tt{scatter\_matrix}$ می‌توانیم نمودار هر ویژگی بر حسب ویژگی دیگر را رسم کنیم. نمودارهایی که روی قطر قرار دارند هیستوگرام هریک از ویژگی‌ها است.
In [25]:
# from pandas.tools.plotting import scatter_matrix # For older versions of Pandas
from pandas.plotting import scatter_matrix

attributes = ["median_house_value", "median_income", "total_rooms",
              "housing_median_age"]
scatter_matrix(housing[attributes], figsize=(12, 8))
save_fig("scatter_matrix_plot")
Saving figure scatter_matrix_plot
حال اگر به نمودار $\tt{median\_income}$ بر حسب $\tt{median\_house\_value}$ نگاه کنیم متوجه می‌شویم که تعداد زیادی از ناحیه‌ها هست که $\tt{median\_income}$ های متفاوتی دارند اما $\tt{median\_house\_value}$ برای آنها مقدار 500000 دارد. از روی این نمودار متوجه می‌شویم که احتمالا ستون $\tt{median\_house\_value}$ که در داده‌ها وجود داشت قبلا از بالا بریده(clip) شده است و مقادیر بیشتر از 500000 را برابر با 500000 قرار داده اند.
In [26]:
housing.plot(kind="scatter", x="median_income", y="median_house_value",
             alpha=0.1)
plt.axis([0, 16, 0, 550000])
save_fig("income_vs_house_value_scatterplot")
Saving figure income_vs_house_value_scatterplot
احتمالا متوجه شده‌اید که ویژگی‌های $\tt{total\_rooms}$ و $\tt{population}$ و $\tt{total\_bedrooms}$ برای ناحیه‌های مختلف قابل مقایسه نیستند زیرا در هر ناحیه تعداد خانه‌های متفاوتی وجود دارد و هر چه تعداد خانه‌ها بیشتر باشد این مقادیر نیز بیشتر خواهند بود. به منظور داشتن اطلاعات مفیدتری از این ستون‌ها کد زیر ویژگی‌های $\tt{rooms\_per\_household}$ و $\tt{bedrooms\_per\_room}$ و $\tt{population\_per\_household}$ را به Dataframe اضافه می‌کند.
In [27]:
housing["rooms_per_household"] = housing["total_rooms"]/housing["households"]
housing["bedrooms_per_room"] = housing["total_bedrooms"]/housing["total_rooms"]
housing["population_per_household"]=housing["population"]/housing["households"]
حال بعد از اضافه کردن این ویژگی‌ها می‌توانیم دوباره هم‌بستگی‌ها را حساب کنیم. مشاهده می‌کنیم که ویژگی $\tt{bedrooms\_per\_room}$ که اضافه کرده‌ایم رابطه خطی معکوس نسبتا زیادی با $\tt{median\_house\_value}$ دارد در حالی که $\tt{total\_bedrooms}$ تقریبا هیچ رابطه‌ای خطی با $\tt{median\_house\_value}$ ندارد. یکی از کارهایی که تاثیر زیادی در کارآیی مدل شما دارد Feature Engineering یا انتخاب کردن و تغییر دادن ویژگی‌ها به گونه‌ای است که برای مدل قابل فهم‌تر باشند. کاری که پیش از این انجام دادیم نمونه‌ای از آن است.
In [28]:
corr_matrix = housing.corr()
corr_matrix["median_house_value"].sort_values(ascending=False)
Out[28]:
median_house_value          1.000000
median_income               0.687160
rooms_per_household         0.146285
total_rooms                 0.135097
housing_median_age          0.114110
households                  0.064506
total_bedrooms              0.047689
population_per_household   -0.021985
population                 -0.026920
longitude                  -0.047432
latitude                   -0.142724
bedrooms_per_room          -0.259984
Name: median_house_value, dtype: float64
In [29]:
housing.plot(kind="scatter", x="rooms_per_household", y="median_house_value",
             alpha=0.2)
plt.axis([0, 5, 0, 520000])
plt.show()
پس از تغییراتی که صورت گرفت آماره‌های کلی درباره ستون‌های مختلف را به دست می‌آوریم.
In [30]:
housing.describe()
Out[30]:
longitude latitude housing_median_age total_rooms total_bedrooms population households median_income median_house_value rooms_per_household bedrooms_per_room population_per_household
count 16512.000000 16512.000000 16512.000000 16512.000000 16354.000000 16512.000000 16512.000000 16512.000000 16512.000000 16512.000000 16354.000000 16512.000000
mean -119.575834 35.639577 28.653101 2622.728319 534.973890 1419.790819 497.060380 3.875589 206990.920724 5.440341 0.212878 3.096437
std 2.001860 2.138058 12.574726 2138.458419 412.699041 1115.686241 375.720845 1.904950 115703.014830 2.611712 0.057379 11.584826
min -124.350000 32.540000 1.000000 6.000000 2.000000 3.000000 2.000000 0.499900 14999.000000 1.130435 0.100000 0.692308
25% -121.800000 33.940000 18.000000 1443.000000 295.000000 784.000000 279.000000 2.566775 119800.000000 4.442040 0.175304 2.431287
50% -118.510000 34.260000 29.000000 2119.500000 433.000000 1164.000000 408.000000 3.540900 179500.000000 5.232284 0.203031 2.817653
75% -118.010000 37.720000 37.000000 3141.000000 644.000000 1719.250000 602.000000 4.744475 263900.000000 6.056361 0.239831 3.281420
max -114.310000 41.950000 52.000000 39320.000000 6210.000000 35682.000000 5358.000000 15.000100 500001.000000 141.909091 1.000000 1243.333333
آماده‌سازی داده‌ها برای الگوریتم‌های یادگیری ماشین
حال با رسم نمودارهایی اطلاعات کلی‌تری در مورد ویژگی‌هایی که داریم به دست می‌آوریم. در این بخش برچسب داده‌ها را از داده‌ها جدا کرده و در متغیر دیگری نگه می‌داریم و روی بقیه ویژگی‌ها کار می‌کنیم.
In [31]:
housing = strat_train_set.drop("median_house_value", axis=1) # drop labels for training set
housing_labels = strat_train_set["median_house_value"].copy()
بیشتر الگوریتم‌های یادگیری ماشین نمی‌توانند با ویژگی‌هایی کار کنند که در بعضی از داده‌ها جا افتاده‌اند. از این رو پیش از آن که داده‌ها به الگوریتم داده شوند، می‌بایست تغییراتی در آن‌ها صورت گیرد. برای این کار روش‌های متعددی وجود دارد که مهم‌ترین آنها در ادامه آمده است. ابتدا تعدادی از داده‌ها را که ويژگی NaN دارند بدست می‌آوریم.
In [32]:
sample_incomplete_rows = housing[housing.isnull().any(axis=1)].head()
sample_incomplete_rows
Out[32]:
longitude latitude housing_median_age total_rooms total_bedrooms population households median_income ocean_proximity
4629 -118.30 34.07 18.0 3759.0 NaN 3296.0 1462.0 2.2708 <1H OCEAN
6068 -117.86 34.01 16.0 4632.0 NaN 3038.0 727.0 5.1762 <1H OCEAN
17923 -121.97 37.35 30.0 1955.0 NaN 999.0 386.0 4.6328 <1H OCEAN
13656 -117.30 34.05 6.0 2155.0 NaN 1039.0 391.0 1.6675 INLAND
19252 -122.79 38.48 7.0 6837.0 NaN 3468.0 1405.0 3.1662 <1H OCEAN
همان طور که دیده می‌شود ستون $\tt{total\_bedrooms}$ در مثال‌های بالا مقدار NaN دارد. در روش اول می‌توان تمام سطر‌هایی را که در یک یا چند ویژگی دلخواه مقدار مشخص ندارند، حذف کرد. پس از این کار می‌توان دید که تمام داده‌هایی که در ستون $\tt{total\_bedrooms}$ مقدار نامشخص داشتند حذف می‌شوند.
In [33]:
sample_incomplete_rows.dropna(subset=["total_bedrooms"])    # option 1
Out[33]:
longitude latitude housing_median_age total_rooms total_bedrooms population households median_income ocean_proximity
در روش دوم می‌توان کل ستون $\tt{total\_bedrooms}$ را از ویژگی‌هایی که در نظر گرفته می‌شود حذف کرد. باید توجه داشت که در بعضی موارد این کار می‌تواند موجب آن شود که ویژگی مهمی به علت مشخص نبودنش برای تعداد اندکی از داده‌ها حذف شده و فرایند یادگیری را سخت‌تر کند.
In [34]:
sample_incomplete_rows.drop("total_bedrooms", axis=1)       # option 2
Out[34]:
longitude latitude housing_median_age total_rooms population households median_income ocean_proximity
4629 -118.30 34.07 18.0 3759.0 3296.0 1462.0 2.2708 <1H OCEAN
6068 -117.86 34.01 16.0 4632.0 3038.0 727.0 5.1762 <1H OCEAN
17923 -121.97 37.35 30.0 1955.0 999.0 386.0 4.6328 <1H OCEAN
13656 -117.30 34.05 6.0 2155.0 1039.0 391.0 1.6675 INLAND
19252 -122.79 38.48 7.0 6837.0 3468.0 1405.0 3.1662 <1H OCEAN
روش سوم که بیشتر توصیه می‌شود آن است که مقادیر گم‌شده را با استفاده از داده‌هایی که از آن ویژگی داریم پر کنیم. به عنوان مثال می‌توان میانه داده‌ها را به جای مقادیری که مشخص نیستند قرار داد.
In [35]:
median = housing["total_bedrooms"].median()
sample_incomplete_rows["total_bedrooms"].fillna(median, inplace=True) # option 3
sample_incomplete_rows
Out[35]:
longitude latitude housing_median_age total_rooms total_bedrooms population households median_income ocean_proximity
4629 -118.30 34.07 18.0 3759.0 433.0 3296.0 1462.0 2.2708 <1H OCEAN
6068 -117.86 34.01 16.0 4632.0 433.0 3038.0 727.0 5.1762 <1H OCEAN
17923 -121.97 37.35 30.0 1955.0 433.0 999.0 386.0 4.6328 <1H OCEAN
13656 -117.30 34.05 6.0 2155.0 433.0 1039.0 391.0 1.6675 INLAND
19252 -122.79 38.48 7.0 6837.0 433.0 3468.0 1405.0 3.1662 <1H OCEAN
اخطار: از آنجا که از ورژن ۰.۲۰ کتابخانه Scikit-Learn به بعد sklearn.preprocessing.Imputer با sklearn.preprocessing.SimpleImupter جایگزین شده است به صورت زیر این کلاس را import می‌کنیم. در نمونه‌ای که از این کلاس می‌گیریم مشخص می‌کنیم که می‌خواهیم مقادیر گم‌شده را با استفاده از میانه جایگزین کنیم.
In [36]:
try:
    from sklearn.impute import SimpleImputer # Scikit-Learn 0.20+
except ImportError:
    from sklearn.preprocessing import Imputer as SimpleImputer

imputer = SimpleImputer(strategy="median")
از آنجا که میانه تنها برای داده‌های عددی قابل محاسبه است، یک کپی از داده‌ها درست می‌کنیم که داده از نوع متن $\tt{ocean\_proximity}$ را نداشته باشد.
In [37]:
housing_num = housing.drop('ocean_proximity', axis=1)
# alternatively: housing_num = housing.select_dtypes(include=[np.number])
سپس با استفاده از تابع $\tt{fit}$ و دادن مجموعه داده‌ها به عنوان ورودی، می‌توان میانه‌ها را به دست آورد که نتیجه در متغیر $\tt{statistics\underline{}}$ ذخیره شده است.
In [38]:
imputer.fit(housing_num)
Out[38]:
SimpleImputer(copy=True, fill_value=None, missing_values=nan,
       strategy='median', verbose=0)
In [39]:
imputer.statistics_
Out[39]:
array([-118.51  ,   34.26  ,   29.    , 2119.5   ,  433.    , 1164.    ,
        408.    ,    3.5409])
بررسی می‌کنیم که مقادیر محاسبه شده در بالا با مقادیری که از محاسبه‌ی مستقیم میانه‌ها به دست می‌آیند، یکی هستند یا خیر.
In [40]:
housing_num.median().values
Out[40]:
array([-118.51  ,   34.26  ,   29.    , 2119.5   ,  433.    , 1164.    ,
        408.    ,    3.5409])
حال با فراخوانی تابع $\tt{transform}$ روی imputerای که آموزش داده شده است، مقادیر گم‌شده را برای داده‌ی اولیه جایگزین می‌کنیم.
In [41]:
X = imputer.transform(housing_num)
از آن جا که نتیجه اجرای این تابع یک آرایه numpy است، آن را به حالت dataframe در می‌آوریم.
In [42]:
housing_tr = pd.DataFrame(X, columns=housing_num.columns,
                          index = list(housing.index.values))
حال پس از اعمال تغییرات، داده‌هایی را که مقادیر گم‌شده داشتند نمایش می‌دهیم. همان طور که در ادامه می‌بینید ستون $\tt{total\_bedrooms}$ برای همه آن‌ها با مقدار میانه، جایگزین شده است.
In [43]:
housing_tr.loc[sample_incomplete_rows.index.values]
Out[43]:
longitude latitude housing_median_age total_rooms total_bedrooms population households median_income
4629 -118.30 34.07 18.0 3759.0 433.0 3296.0 1462.0 2.2708
6068 -117.86 34.01 16.0 4632.0 433.0 3038.0 727.0 5.1762
17923 -121.97 37.35 30.0 1955.0 433.0 999.0 386.0 4.6328
13656 -117.30 34.05 6.0 2155.0 433.0 1039.0 391.0 1.6675
19252 -122.79 38.48 7.0 6837.0 433.0 3468.0 1405.0 3.1662
In [44]:
imputer.strategy
Out[44]:
'median'
In [45]:
housing_tr = pd.DataFrame(X, columns=housing_num.columns)
housing_tr.head()
Out[45]:
longitude latitude housing_median_age total_rooms total_bedrooms population households median_income
0 -121.89 37.29 38.0 1568.0 351.0 710.0 339.0 2.7042
1 -121.93 37.05 14.0 679.0 108.0 306.0 113.0 6.4214
2 -117.20 32.77 31.0 1952.0 471.0 936.0 462.0 2.8621
3 -119.61 36.31 25.0 1847.0 371.0 1460.0 353.0 1.8839
4 -118.59 34.23 17.0 6592.0 1525.0 4459.0 1463.0 3.0347
در ادامه می‌خواهیم با داده‌ی دسته‌ای $\tt{ocean\_proximity}$ کار کنیم. همان طور که پیش از این ذکر شد این ویژگی از نوع متن است و به همین دلیل نمی‌توان میانه‌اش را محاسبه کرد. بیشتر الگوریتم‌های یادگیری ماشین با مقادیر عددی کار می‌کنند پس بهتر است این نوع ویژگی‌ها را به عدد تبدیل کرد.
In [46]:
housing_cat = housing[['ocean_proximity']]
housing_cat.head(10)
Out[46]:
ocean_proximity
17606 <1H OCEAN
18632 <1H OCEAN
14650 NEAR OCEAN
3230 INLAND
3555 <1H OCEAN
19480 INLAND
8879 <1H OCEAN
13685 INLAND
4937 <1H OCEAN
4861 <1H OCEAN
اخطار: برای عددی کردن ویژگی‌های متنیِ دسته‌ای می‌توان از کلاس‌های $\tt{LabelEncoder}$ و یا تابع $\tt{Series.factorize()}$ مربوط به کتابخانه Pandas استفاده کرد. اما کلاس $\tt{OrdinalEncoder}$ که در ورژن ۰.۲۰ Scikit-Learn ارائه شده است به علت آنکه با ويژگی‌های ورودی کار می‌کند و عملکرد بهتری با Pipeline ( که در ادامه توضیح داده‌خواهد شده) دارد ارجح است. اگر ورژن قدیمی‌تری از Scikit-Learn را استفاده می‌کنید باید آن را از future_encoders.py بخوانید.
In [47]:
try:
    from sklearn.preprocessing import OrdinalEncoder
except ImportError:
    from future_encoders import OrdinalEncoder # Scikit-Learn < 0.20
با فراخوانی تابع $\tt{fit\underline{}transform}$ از این کلاس، داده دسته‌ای ورودی به داده‌ی عددی کد می‌شود که ۱۰ خط اول آن را نمایش می‌دهیم. هم‌چنین با استفاده از ويژگی $\tt{categories\underline{}}$ می‌توان به رشته‌ی متنی دسته‌ها پی برد.
In [48]:
ordinal_encoder = OrdinalEncoder()
housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
housing_cat_encoded[:10]
Out[48]:
array([[0.],
       [0.],
       [4.],
       [1.],
       [0.],
       [1.],
       [0.],
       [1.],
       [0.],
       [0.]])
In [49]:
ordinal_encoder.categories_
Out[49]:
[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
       dtype=object)]
مشاهده می‌کنیم که به جای هر کدام از مقادیر متنی ویژگی $\tt{ocean\_proximity}$ یکی از اعداد 0 تا 4 قرار داده شده است. در واقع این نگاشت از متن به اعداد طبیعی را می‌توانستیم به ترتیب دیگری انجام دهیم؛ مثلا به جای اینکه به $\tt{INLAND}$ مقدار 1 را نسبت دهیم می‌توانستیم به آن مقدار 3 را نسبت دهیم. هم‌چنین می‌توانستیم به جای اینکه مقادیر 0, 1, 3, 4 را نسبت دهیم، از مقادیر 0, 100, 300, 1000, 50000 استفاده کنیم. برای این که این کار را انجام دهیم میتوانیم از One-hot Encoding استفاده کنیم. در این حالت مقادیر مختلفی که ویژگی های دسته‌ای می‌گیرند از یک‌دیگر مستقل خواهند شد. اگر مثلا یک ویژگی متنی داشته باشیم که 3 مقدار مختلف A, B, C را می‌گیرد آن را تبدیل به یک بردار 3 بعدی می‌کنیم به صورتی که:
A ---> [1, 0, 0]
B ---> [0, 1, 0]
C ---> [0, 0, 1]
In [50]:
try:
    from sklearn.preprocessing import OrdinalEncoder # just to raise an ImportError if Scikit-Learn < 0.20
    from sklearn.preprocessing import OneHotEncoder
except ImportError:
    from future_encoders import OneHotEncoder # Scikit-Learn < 0.20

cat_encoder = OneHotEncoder(sparse=False)
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
housing_cat_1hot
Out[50]:
array([[1., 0., 0., 0., 0.],
       [1., 0., 0., 0., 0.],
       [0., 0., 0., 0., 1.],
       ...,
       [0., 1., 0., 0., 0.],
       [1., 0., 0., 0., 0.],
       [0., 0., 0., 1., 0.]])
In [51]:
cat_encoder.categories_
Out[51]:
[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],
       dtype=object)]
حال می‌خواهیم مراحل مختلف پیش‌پردازشی را که روی داده‌ها انجام دادیم (پر کردن Nanها و اضافه کردن ویژگی هایی مانند $\tt{rooms\_per\_household}$ ) در قالب یک Pipeline انجام دهیم ابتدا ستون‌هایی را که مقادیرشان عددی است از ستون‌هایی که مقادیر غیرعددی دارند جدا می‌کنیم. چون که پیش‌پردازشی که روی این نوع ستون‌ها باید انجام بگیرد متفاوت است.
In [52]:
numerical_columns = housing.columns.drop('ocean_proximity')
categorical_columns = pd.Index(['ocean_proximity'])
housing_numerical = housing[numerical_columns]
housing_categorical = housing[categorical_columns]
به صورت کلی عملکرد $\tt{Pipeline}$ به این صورت است که یک ورودی می‌گیرد و روی آن پردازش‌هایی انجام می‌دهد و سپس یک خروجی می‌دهد. خط‌‌لوله‌ای که ساخته‌ایم از 3 مرحله تشکیل شده است. مرحله اول $\tt{Imputer}$ است که جاهایی از داده ها را که Nan است پر می‌کند. سپس خروجی مرحله اول وارد مرحله دوم می‌شود. کلاسِ مربوط به مرحله دوم را خودمان تعریف کرده‌ایم. این کلاس ویژگی های $\tt{rooms\_per\_household}$ و ... را به داده‌هایمان اضافه می‌کند. هم‌چنین می‌توانیم به عنوان ورودی تعیین کنیم که ویژگی $\tt{bedrooms\_per\_room}$ اضافه شود یا خیر. آخرین مرحله از Pipeline که $\tt{StandardScaler}$ است مسئولیت نرمال کردن ستون‌های عددی را برعهده دارد. این تابع به صورت پیش‌فرض هر ستون را با یک تبدیل خطی به گونه‌ای تغییر می‌دهد که میانگینش صفر شود و انحراف معیار آن برابر با ۱ شود.
In [53]:
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler


class CombinedAttributesAdder(BaseEstimator, TransformerMixin):
    def __init__(self, add_bedrooms_per_room=False): # no *args or **kargs
        self.add_bedrooms_per_room = add_bedrooms_per_room
    def fit(self, X, y=None):
        return self  # nothing else to do
    def transform(self, X, y=None):
        X_df = pd.DataFrame(X, columns=numerical_columns)
        X_df["rooms_per_household"] = X_df["total_rooms"]/X_df["households"]
        X_df["population_per_household"]=X_df["population"]/X_df["households"]
        if self.add_bedrooms_per_room:
            X_df["bedrooms_per_room"] = X_df["total_bedrooms"]/X_df["total_rooms"]
        return X_df

numerical_pipeline = Pipeline([
        ('imputer', SimpleImputer(strategy="median")),
        ('attribs_adder', CombinedAttributesAdder(add_bedrooms_per_room=True)),
        ('std_scaler', StandardScaler()),
    ])

housing_numerical_transformed = numerical_pipeline.fit_transform(housing_numerical)
housing_numerical_transformed
Out[53]:
array([[-1.15604281,  0.77194962,  0.74333089, ..., -0.31205452,
        -0.08649871,  0.15531753],
       [-1.17602483,  0.6596948 , -1.1653172 , ...,  0.21768338,
        -0.03353391, -0.83628902],
       [ 1.18684903, -1.34218285,  0.18664186, ..., -0.46531516,
        -0.09240499,  0.4222004 ],
       ...,
       [ 1.58648943, -0.72478134, -1.56295222, ...,  0.3469342 ,
        -0.03055414, -0.52177644],
       [ 0.78221312, -0.85106801,  0.18664186, ...,  0.02499488,
         0.06150916, -0.30340741],
       [-1.43579109,  0.99645926,  1.85670895, ..., -0.22852947,
        -0.09586294,  0.10180567]])
همان‌طور که مشاهده می‌کنیم میانگین ستون‌ها عددهای بسیار نزدیک به صفر و انحراف معیار ستون ها ۱ است.
In [54]:
np.mean(housing_numerical_transformed, axis=0)
Out[54]:
array([-4.35310702e-15,  2.28456358e-15, -4.70123509e-17,  7.58706190e-17,
        1.36061489e-16, -3.70074342e-17,  2.07897868e-17, -2.10210832e-16,
        8.01469141e-17, -1.62176474e-17, -4.87874168e-17])
In [55]:
np.std(housing_numerical_transformed, axis=0)
Out[55]:
array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.])
حال با استفاده از کلاس $\tt{ColumnTransformer}$ می‌توانیم تبدیل‌های مختلفی روی ستون‌های مختلف داده اولیه اعمال کنیم و در نهایت با کنار هم گذاشتن ستون‌های تبدیل یافته به داده نهایی برسیم.
In [56]:
try:
    from sklearn.compose import ColumnTransformer
except ImportError:
    from future_encoders import ColumnTransformer # Scikit-Learn < 0.20

num_attribs = list(housing_num)
cat_attribs = ["ocean_proximity"]

numerical_pipeline = Pipeline([
        ('imputer', SimpleImputer(strategy="median")),
        ('attribs_adder', CombinedAttributesAdder(add_bedrooms_per_room=True)),
        ('std_scaler', StandardScaler()),
    ])

categorical_pipeline = Pipeline([
        ('imputer', SimpleImputer(strategy="most_frequent")),
        ("onehot_encoder", OneHotEncoder())
])



full_pipeline = ColumnTransformer([
        ("num", numerical_pipeline, numerical_columns),
        ("cat", categorical_pipeline, categorical_columns),
    ])

housing_prepared = full_pipeline.fit_transform(housing)
housing_prepared
Out[56]:
array([[-1.15604281,  0.77194962,  0.74333089, ...,  0.        ,
         0.        ,  0.        ],
       [-1.17602483,  0.6596948 , -1.1653172 , ...,  0.        ,
         0.        ,  0.        ],
       [ 1.18684903, -1.34218285,  0.18664186, ...,  0.        ,
         0.        ,  1.        ],
       ...,
       [ 1.58648943, -0.72478134, -1.56295222, ...,  0.        ,
         0.        ,  0.        ],
       [ 0.78221312, -0.85106801,  0.18664186, ...,  0.        ,
         0.        ,  0.        ],
       [-1.43579109,  0.99645926,  1.85670895, ...,  0.        ,
         1.        ,  0.        ]])
مشاهده می‌کنیم که داده نهایی آموزش 16512 سطر دارد و 16 ستون که ستون‌هایی از آن را چاپ می‌کنیم.
In [57]:
housing_prepared.shape
Out[57]:
(16512, 16)
In [58]:
housing.columns
Out[58]:
Index(['longitude', 'latitude', 'housing_median_age', 'total_rooms',
       'total_bedrooms', 'population', 'households', 'median_income',
       'ocean_proximity'],
      dtype='object')
انتخاب یک مدل و آموزش دادن آن
حال وقت آن است که یک مدل انتخاب کرده و آن را با استفاده از داده‌های آموزش تعلیم دهیم! به عنوان اولین مثال سعی می‌کنیم ویژگی $\tt{median\_house\_value}$ را برحسب سایر ویژگی‌ها پیش‌بینی کنیم.
ساده ترین مدلی که برای تخمین زدن یک مقدار حقیقی بر حسب تعدادی ویژگی وجود دارد، رگرسیون خطی است. کتابخانه Scikit-learn تعداد زیادی از مدل‌های یادگیری ماشین را به صورت آماده در خود جای داده است.
تکه کد زیر یک مدل رگرسیون خطی را تعریف می‌کند و سپس آن را به داده های آموزش می‌برازاند(!) و بعد از برازش مدل میتوانیم با استفاده از تابع $\tt{score}$ درکی از میزان نزدیک بودن مقادیر تخمینی و مقادیر واقعی به دست بیاوریم.
برای رگرسیون خطی این امتیاز هرچقدر به 1 یا -1 نزدیک تر باشد به معنی این است که تخمین مدل به واقعیت نزدیک‌تر است.
In [59]:
from sklearn.linear_model import LinearRegression

lin_reg = LinearRegression()
lin_reg.fit(housing_prepared, housing_labels)
lin_reg.score(housing_prepared, housing_labels)
Out[59]:
0.6481624842804428
حال می‌توانیم با استفاده از مدلی که آموزش داده‌ایم مقدار $\tt{median\_house\_value}$ را برای 5 داده آموزش اول محاسبه کنیم و آن‌ها را با مقادیر واقعی مقایسه کنیم.
In [60]:
# let's try the full preprocessing pipeline on a few training instances
some_data = housing.iloc[:5]
some_labels = housing_labels.iloc[:5]
some_data_prepared = full_pipeline.transform(some_data)

print("Predictions:", lin_reg.predict(some_data_prepared))
Predictions: [210644.60459286 317768.80697211 210956.43331178  59218.98886849
 189747.55849879]

Compare against the actual values:

In [61]:
print("Labels:", list(some_labels))
Labels: [286600.0, 340600.0, 196900.0, 46300.0, 254500.0]
یکی از معیار هایی که می‌توان با آن میزان برازش مدل های رگرسیون را بررسی کرد، MSE یا میانگین مربعات خطاها است.
این معیار به این صورت محاسبه می‌شود که به ازای هرکدام از داده ها اختلاف تخمین مدل و مقدار واقعی را به توان دو می‌رسانیم و این مقادیر را میانگین می‌گیریم.
هرچقدر تخمین مدل بیشتر به واقعیت نزدیک باشد این مقدار کوچکتر خواهد بود.
برای اینکه Scale خطای حاصل با Scale مقدار هدف یکسان شود در آخر از آن جذر می‌گیریم.
In [62]:
from sklearn.metrics import mean_squared_error

housing_predictions = lin_reg.predict(housing_prepared)
lin_mse = mean_squared_error(housing_labels, housing_predictions)
lin_rmse = np.sqrt(lin_mse)
lin_rmse
Out[62]:
68628.19819848923
معیار دیگری که برای مقایسه وجود دارد MAE است.
مشکلی که MSE دارد این است که به داده‌های پرت حساس است. یعنی اگر یکی از داده‌ها مقدار $\tt{median\_house\_value}$ اش به اشتباه عدد بسیار بزرگی وارد شده باشد، MSE مقدار بسیار زیادی می‌گیرد زیرا با توان 2 خطا رابطه دارد.
بجای استفاده از توان 2 می‌توانیم از قدرمطلق استفاده کنیم و این مشکل را حل کنیم.
In [63]:
from sklearn.metrics import mean_absolute_error

lin_mae = mean_absolute_error(housing_labels, housing_predictions)
lin_mae
Out[63]:
49439.89599001897
حال مدلی پیچیده‌تر از رگرسیون خطی را به داده‌های آموزش می‌برازانیم!
مشاهده می‌کنیم که Score برابر با 1 شده است و MSE نیز برابر با صفر شده است!
در واقع این مدل مقدار $\tt{median\_house\_value}$ را به ازای تمام داده‌های آموزش به صورت دقیق پیش‌بینی می‌کند آیا ما بهترین مدلی که می‌توانستیم را پیدا کرده‌ایم؟
In [64]:
from sklearn.tree import DecisionTreeRegressor

tree_reg = DecisionTreeRegressor(random_state=42)
tree_reg.fit(housing_prepared, housing_labels)
tree_reg.score(housing_prepared, housing_labels)
Out[64]:
1.0
In [65]:
housing_predictions = tree_reg.predict(housing_prepared)
tree_mse = mean_squared_error(housing_labels, housing_predictions)
tree_rmse = np.sqrt(tree_mse)
tree_rmse
Out[65]:
0.0
یافتن مدل مناسب
پیش‌تر دیدید که مدل $\tt{DecisionTreeRegressor}$ به صورت کاملا دقیق مقدار $\tt{median\_house\_value}$ را در داده‌های هدف پیش‌بینی می‌کرد. اما همان‌طور که قبل‌تر گفته شد، این مدل ممکن است به داده‌های آموزش بیش از حد برازیده باشد یا به اصطلاح Overfit شده باشد. برای اینکه متوجه شویم خطای واقعی این مدل رو داده‌ای که در هنگام آموزش با آن روبه‌رو نشده است تقریبا چقدر است، از Cross Validation استفاده می‌کنیم (دقت کنید که تا وقتی مدل نهایی خودمان را انتخاب نکرده‌ایم نمی‌توانیم از Test Set استفاده کنیم). تابع $\tt{cross\_val\_score}$ به این صورت عمل می‌کند که مثلا داده‌های آموزش را به ۱۰ قسمت مساوی تقسیم می‌کند. سپس از ۹تا از این قسمت‌ها برای آموزش مدل استفاده می‌کند و از یکی از این قسمت‌ها برای ارزیابی مدل و محاسبه MSE. به این صورت با انتخاب هریک از ۱۰ قسمت به عنوان Validation Set و انتخاب ۹ قسمت دیگر به عنوان Train Set، ده امتیاز مختلف به دست می‌آوریم. میانگین این امتیازها می‌تواند معیار مناسبی از خطای مدل روی داده‌هایی که مشاهده نکرده است باشد. با مقایسه این مقادیر برای مدل $\tt{DecisionTreeRegressor}$ و $\tt{LinearRegressor}$ متوجه می‌شویم که با اینکه مدل رگرسیون درختی روی داده‌های آموزش عملکرد بسیار خوبی دارد اما درواقع به آن داده‌ها Overfit شده است و روی داده‌های جدید عملکرد مناسبی ندارد.
In [66]:
from sklearn.model_selection import cross_val_score

scores = cross_val_score(tree_reg, housing_prepared, housing_labels,
                         scoring="neg_mean_squared_error", cv=10)
tree_rmse_scores = np.sqrt(-scores)
In [67]:
def display_scores(scores):
    print("Scores:", scores)
    print("Mean:", scores.mean())
    print("Standard deviation:", scores.std())

display_scores(tree_rmse_scores)
Scores: [70194.33680785 66855.16363941 72432.58244769 70758.73896782
 71115.88230639 75585.14172901 70262.86139133 70273.6325285
 75366.87952553 71231.65726027]
Mean: 71407.68766037929
Standard deviation: 2439.4345041191004
In [68]:
lin_scores = cross_val_score(lin_reg, housing_prepared, housing_labels,
                             scoring="neg_mean_squared_error", cv=10)
lin_rmse_scores = np.sqrt(-lin_scores)
display_scores(lin_rmse_scores)
Scores: [66782.73843989 66960.118071   70347.95244419 74739.57052552
 68031.13388938 71193.84183426 64969.63056405 68281.61137997
 71552.91566558 67665.10082067]
Mean: 69052.46136345083
Standard deviation: 2731.674001798349
برای این که مشکل Overfitting درخت رگرسیون را برطرف کنیم می‌توانیم عمق آن را کم کنیم و یا از $\tt{RandomForestRegressor}$ استفاده کنیم. این مدل از $\tt{n\_estimators}$ درخت رگرسیون استفاده می‌کند و نتایج آن‌ها را به گونه‌ای میانگین می‌گیرد. با محاسبه Cross Validation Score مشاهده می‌کنیم که این مدل در مقایسه با دو مدل قبلی عملکرد بهتری دارد.
In [69]:
from sklearn.ensemble import RandomForestRegressor

forest_reg = RandomForestRegressor(n_estimators=10, random_state=42)
forest_reg.fit(housing_prepared, housing_labels)
Out[69]:
RandomForestRegressor(bootstrap=True, criterion='mse', max_depth=None,
           max_features='auto', max_leaf_nodes=None,
           min_impurity_decrease=0.0, min_impurity_split=None,
           min_samples_leaf=1, min_samples_split=2,
           min_weight_fraction_leaf=0.0, n_estimators=10, n_jobs=None,
           oob_score=False, random_state=42, verbose=0, warm_start=False)
In [70]:
housing_predictions = forest_reg.predict(housing_prepared)
forest_mse = mean_squared_error(housing_labels, housing_predictions)
forest_rmse = np.sqrt(forest_mse)
forest_rmse
Out[70]:
21933.31414779769
In [71]:
from sklearn.model_selection import cross_val_score

forest_scores = cross_val_score(forest_reg, housing_prepared, housing_labels,
                                scoring="neg_mean_squared_error", cv=10)
forest_rmse_scores = np.sqrt(-forest_scores)
display_scores(forest_rmse_scores)
Scores: [51646.44545909 48940.60114882 53050.86323649 54408.98730149
 50922.14870785 56482.50703987 51864.52025526 49760.85037653
 55434.21627933 53326.10093303]
Mean: 52583.72407377466
Standard deviation: 2298.353351147122
به جای استفاده از تابع $\tt{display\_scores}$ که خودمان تعریف کردیم، می‌توانیم از کتابخانه Pandas استفاده کنیم که میانگین، انحراف معیار، کمینه ، بیشینه و چارک های یک سری عددی را حساب می‌کند.
In [72]:
scores = cross_val_score(lin_reg, housing_prepared, housing_labels, scoring="neg_mean_squared_error", cv=10)
pd.Series(np.sqrt(-scores)).describe()
Out[72]:
count       10.000000
mean     69052.461363
std       2879.437224
min      64969.630564
25%      67136.363758
50%      68156.372635
75%      70982.369487
max      74739.570526
dtype: float64
فرض کنید که با بررسی‌هایی که انجام دادیم نتیجه گرفتیم که $\tt{RandomForestRegressor}$ مدل مناسبی است. هر مدل تعدادی Hyperparameter دارد، برای مثال $\tt{n\_estimators}$ یکی از هایپرپارامترهای مدل $\tt{RandomForestRegressor}$ است. با تغییر دادن این هایپرپارامترها نتیجه‌های متفاوتی به دست می‌آیند. کتابخانه Scikit-learn توابعی فراهم کرده است که با آن‌ها می‌توان مقادیر مناسبی برای هایپرپارامترهای یک مدل پیدا کرد به طوری که خطای Cross Validation کمتر شود. تابع $\tt{GridSearchCV}$ به این صورت عمل می‌کند که یک Grid از هایپرپارامترها را به عنوان ورودی می‌گیرد و روی تمام حالت‌های مختلفی که وجود دارد $\tt{Cross\_val\_score}$ را محاسبه می‌کند. پارامتر cv مشخص می‌کند که به ازای هر حالتِ مقداردهی برای هایپرپارامترها، برای محاسبه امتیاز با Cross Validation، از چند fold استفاده شود.
In [73]:
from sklearn.model_selection import GridSearchCV

param_grid = [
    # try 12 (3×4) combinations of hyperparameters
    {'n_estimators': [3, 10, 30], 'max_features': [2, 4, 6, 8]},
    # then try 6 (2×3) combinations with bootstrap set as False
    {'bootstrap': [False], 'n_estimators': [3, 10], 'max_features': [2, 3, 4]},
  ]

forest_reg = RandomForestRegressor(random_state=42)
# train across 5 folds, that's a total of (12+6)*5=90 rounds of training 
grid_search = GridSearchCV(forest_reg, param_grid, cv=5,
                           scoring='neg_mean_squared_error', return_train_score=True)
grid_search.fit(housing_prepared, housing_labels)
Out[73]:
GridSearchCV(cv=5, error_score='raise-deprecating',
       estimator=RandomForestRegressor(bootstrap=True, criterion='mse', max_depth=None,
           max_features='auto', max_leaf_nodes=None,
           min_impurity_decrease=0.0, min_impurity_split=None,
           min_samples_leaf=1, min_samples_split=2,
           min_weight_fraction_leaf=0.0, n_estimators='warn', n_jobs=None,
           oob_score=False, random_state=42, verbose=0, warm_start=False),
       fit_params=None, iid='warn', n_jobs=None,
       param_grid=[{'n_estimators': [3, 10, 30], 'max_features': [2, 4, 6, 8]}, {'bootstrap': [False], 'n_estimators': [3, 10], 'max_features': [2, 3, 4]}],
       pre_dispatch='2*n_jobs', refit=True, return_train_score=True,
       scoring='neg_mean_squared_error', verbose=0)
حال می‌توانیم بهترین هایپرپارامترهایی را که در جریان جستجو پیدا شده‌اند را مشاهده کنیم.
In [74]:
grid_search.best_params_
Out[74]:
{'max_features': 8, 'n_estimators': 30}
In [75]:
grid_search.best_estimator_
Out[75]:
RandomForestRegressor(bootstrap=True, criterion='mse', max_depth=None,
           max_features=8, max_leaf_nodes=None, min_impurity_decrease=0.0,
           min_impurity_split=None, min_samples_leaf=1,
           min_samples_split=2, min_weight_fraction_leaf=0.0,
           n_estimators=30, n_jobs=None, oob_score=False, random_state=42,
           verbose=0, warm_start=False)
کد زیر Cross Validation Score را به ازای هر کدام از حالت های مقداردهی هایپرپارامترها محاسبه می‌کند.
In [76]:
cvres = grid_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
    print(np.sqrt(-mean_score), params)
63669.05791727153 {'max_features': 2, 'n_estimators': 3}
55627.16171305252 {'max_features': 2, 'n_estimators': 10}
53384.57867637289 {'max_features': 2, 'n_estimators': 30}
60965.99185930139 {'max_features': 4, 'n_estimators': 3}
52740.98248528835 {'max_features': 4, 'n_estimators': 10}
50377.344409590376 {'max_features': 4, 'n_estimators': 30}
58663.84733372485 {'max_features': 6, 'n_estimators': 3}
52006.15355973719 {'max_features': 6, 'n_estimators': 10}
50146.465964159885 {'max_features': 6, 'n_estimators': 30}
57869.25504027614 {'max_features': 8, 'n_estimators': 3}
51711.09443660957 {'max_features': 8, 'n_estimators': 10}
49682.25345942335 {'max_features': 8, 'n_estimators': 30}
62895.088889905004 {'bootstrap': False, 'max_features': 2, 'n_estimators': 3}
54658.14484390074 {'bootstrap': False, 'max_features': 2, 'n_estimators': 10}
59470.399594730654 {'bootstrap': False, 'max_features': 3, 'n_estimators': 3}
52725.01091081235 {'bootstrap': False, 'max_features': 3, 'n_estimators': 10}
57490.612956065226 {'bootstrap': False, 'max_features': 4, 'n_estimators': 3}
51009.51445842374 {'bootstrap': False, 'max_features': 4, 'n_estimators': 10}
در جدول زیر اطلاعات کامل‌تری را می‌توانیم مشاهده کنیم. اطلاعاتی مانند زمانی که طول می‌کشد تا هرکدام از مدل ها را آموزش دهیم، رتبه هرکدام از مدل‌ها بر حسب Cross Val Score آن‌ها، امتیاز هریک از مدل‌ها روی fold های مختلف Cross Validation و ....
In [77]:
pd.DataFrame(grid_search.cv_results_)
Out[77]:
mean_fit_time std_fit_time mean_score_time std_score_time param_max_features param_n_estimators param_bootstrap params split0_test_score split1_test_score ... mean_test_score std_test_score rank_test_score split0_train_score split1_train_score split2_train_score split3_train_score split4_train_score mean_train_score std_train_score
0 0.069789 0.005065 0.004235 0.000258 2 3 NaN {'max_features': 2, 'n_estimators': 3} -3.837622e+09 -4.147108e+09 ... -4.053749e+09 1.519609e+08 18 -1.064113e+09 -1.105142e+09 -1.116550e+09 -1.112342e+09 -1.129650e+09 -1.105559e+09 2.220402e+07
1 0.185158 0.026889 0.009324 0.001662 2 10 NaN {'max_features': 2, 'n_estimators': 10} -3.047771e+09 -3.254861e+09 ... -3.094381e+09 1.327046e+08 11 -5.927175e+08 -5.870952e+08 -5.776964e+08 -5.716332e+08 -5.802501e+08 -5.818785e+08 7.345821e+06
2 0.517390 0.034149 0.026153 0.005866 2 30 NaN {'max_features': 2, 'n_estimators': 30} -2.689185e+09 -3.021086e+09 ... -2.849913e+09 1.626879e+08 9 -4.381089e+08 -4.391272e+08 -4.371702e+08 -4.376955e+08 -4.452654e+08 -4.394734e+08 2.966320e+06
3 0.096557 0.013426 0.003770 0.000809 4 3 NaN {'max_features': 4, 'n_estimators': 3} -3.730181e+09 -3.786886e+09 ... -3.716852e+09 1.631421e+08 16 -9.865163e+08 -1.012565e+09 -9.169425e+08 -1.037400e+09 -9.707739e+08 -9.848396e+08 4.084607e+07
4 0.316611 0.031960 0.010716 0.002557 4 10 NaN {'max_features': 4, 'n_estimators': 10} -2.666283e+09 -2.784511e+09 ... -2.781611e+09 1.268562e+08 8 -5.097115e+08 -5.162820e+08 -4.962893e+08 -5.436192e+08 -5.160297e+08 -5.163863e+08 1.542862e+07
5 0.897460 0.035982 0.034683 0.004583 4 30 NaN {'max_features': 4, 'n_estimators': 30} -2.387153e+09 -2.588448e+09 ... -2.537877e+09 1.214603e+08 3 -3.838835e+08 -3.880268e+08 -3.790867e+08 -4.040957e+08 -3.845520e+08 -3.879289e+08 8.571233e+06
6 0.132096 0.020209 0.003166 0.000266 6 3 NaN {'max_features': 6, 'n_estimators': 3} -3.119657e+09 -3.586319e+09 ... -3.441447e+09 1.893141e+08 14 -9.245343e+08 -8.886939e+08 -9.353135e+08 -9.009801e+08 -8.624664e+08 -9.023976e+08 2.591445e+07
7 0.431753 0.002459 0.008284 0.000118 6 10 NaN {'max_features': 6, 'n_estimators': 10} -2.549663e+09 -2.782039e+09 ... -2.704640e+09 1.471542e+08 6 -4.980344e+08 -5.045869e+08 -4.994664e+08 -4.990325e+08 -5.055542e+08 -5.013349e+08 3.100456e+06
8 1.203825 0.067956 0.029797 0.008100 6 30 NaN {'max_features': 6, 'n_estimators': 30} -2.370010e+09 -2.583638e+09 ... -2.514668e+09 1.285063e+08 2 -3.838538e+08 -3.804711e+08 -3.805218e+08 -3.856095e+08 -3.901917e+08 -3.841296e+08 3.617057e+06
9 0.140877 0.000724 0.002781 0.000065 8 3 NaN {'max_features': 8, 'n_estimators': 3} -3.353504e+09 -3.348552e+09 ... -3.348851e+09 1.241864e+08 13 -9.228123e+08 -8.553031e+08 -8.603321e+08 -8.881964e+08 -9.151287e+08 -8.883545e+08 2.750227e+07
10 0.554289 0.058603 0.009625 0.001288 8 10 NaN {'max_features': 8, 'n_estimators': 10} -2.571970e+09 -2.718994e+09 ... -2.674037e+09 1.392720e+08 5 -4.932416e+08 -4.815238e+08 -4.730979e+08 -5.155367e+08 -4.985555e+08 -4.923911e+08 1.459294e+07
11 1.682905 0.126579 0.025204 0.003291 8 30 NaN {'max_features': 8, 'n_estimators': 30} -2.357390e+09 -2.546640e+09 ... -2.468326e+09 1.091647e+08 1 -3.841658e+08 -3.744500e+08 -3.773239e+08 -3.882250e+08 -3.810005e+08 -3.810330e+08 4.871017e+06
12 0.092068 0.012512 0.003968 0.000608 2 3 False {'bootstrap': False, 'max_features': 2, 'n_est... -3.785816e+09 -4.166012e+09 ... -3.955792e+09 1.900966e+08 17 -0.000000e+00 -0.000000e+00 -0.000000e+00 -0.000000e+00 -0.000000e+00 0.000000e+00 0.000000e+00
13 0.271931 0.008868 0.009864 0.000754 2 10 False {'bootstrap': False, 'max_features': 2, 'n_est... -2.810721e+09 -3.107789e+09 ... -2.987513e+09 1.539231e+08 10 -6.056477e-02 -0.000000e+00 -0.000000e+00 -0.000000e+00 -2.967449e+00 -6.056027e-01 1.181156e+00
14 0.143249 0.010117 0.004533 0.000812 3 3 False {'bootstrap': False, 'max_features': 3, 'n_est... -3.618324e+09 -3.441527e+09 ... -3.536728e+09 7.795196e+07 15 -0.000000e+00 -0.000000e+00 -0.000000e+00 -0.000000e+00 -6.072840e+01 -1.214568e+01 2.429136e+01
15 0.376029 0.039420 0.010389 0.001809 3 10 False {'bootstrap': False, 'max_features': 3, 'n_est... -2.757999e+09 -2.851737e+09 ... -2.779927e+09 6.286611e+07 7 -2.089484e+01 -0.000000e+00 -0.000000e+00 -0.000000e+00 -5.465556e+00 -5.272080e+00 8.093117e+00
16 0.151178 0.016692 0.003611 0.000502 4 3 False {'bootstrap': False, 'max_features': 4, 'n_est... -3.134040e+09 -3.559375e+09 ... -3.305171e+09 1.879203e+08 12 -0.000000e+00 -0.000000e+00 -0.000000e+00 -0.000000e+00 -0.000000e+00 0.000000e+00 0.000000e+00
17 0.443949 0.037043 0.009162 0.000130 4 10 False {'bootstrap': False, 'max_features': 4, 'n_est... -2.525578e+09 -2.710011e+09 ... -2.601971e+09 1.088031e+08 4 -0.000000e+00 -1.514119e-02 -0.000000e+00 -0.000000e+00 -0.000000e+00 -3.028238e-03 6.056477e-03

18 rows × 23 columns

استفاده از $\tt{GridSearchCV}$ زمانی خوب است که بدانیم چه مقادیری برای هر هایپرپارامتر مناسب است. در ابتدا ممکن است ایده‌ای از این که چه مقادیری برای هر هایپرپارامتر مناسب است نداشته باشیم. در این حالت می‌توانیم از $\tt{RandomizedSearchCV}$ استفاده کنیم. ورودی این تابع به جای این که یک Grid از هایپرپارامترهای مختلف باشد، یک توزیع احتمال برای هر هایپرپارامتر است. این تابع به تعداد $\tt{n\_iters}$ از این توزیع‌های احتمال نمونه تصادفی می‌گیرد و سپس Cross Val Score را برای هرکدام از این حالت‌های مقداردهی محاسبه می‌کند. در واقع بهترین روشی که برای انتخاب هایپرپارامتر وجود دارد این است که ابتدا از $\tt{RandomizedSearchCV}$ استفاده کنیم و بعد از این که فهمیدیم چه بازه‌ای از هر هایپرپارامتر مناسب است، از $\tt{GridSearchCV}$ استفاده کنیم تا به به صورت دقیق‌تر جستجو کنیم و مقدار مناسب را برای هر هایپرپارامتر به دست آوریم.
In [78]:
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint

param_distribs = {
        'n_estimators': randint(low=1, high=200),
        'max_features': randint(low=1, high=8),
    }

forest_reg = RandomForestRegressor(random_state=42)
rnd_search = RandomizedSearchCV(forest_reg, param_distributions=param_distribs,
                                n_iter=10, cv=5, scoring='neg_mean_squared_error', random_state=42)
rnd_search.fit(housing_prepared, housing_labels)
Out[78]:
RandomizedSearchCV(cv=5, error_score='raise-deprecating',
          estimator=RandomForestRegressor(bootstrap=True, criterion='mse', max_depth=None,
           max_features='auto', max_leaf_nodes=None,
           min_impurity_decrease=0.0, min_impurity_split=None,
           min_samples_leaf=1, min_samples_split=2,
           min_weight_fraction_leaf=0.0, n_estimators='warn', n_jobs=None,
           oob_score=False, random_state=42, verbose=0, warm_start=False),
          fit_params=None, iid='warn', n_iter=10, n_jobs=None,
          param_distributions={'n_estimators': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7f67948a3278>, 'max_features': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7f67948a3438>},
          pre_dispatch='2*n_jobs', random_state=42, refit=True,
          return_train_score='warn', scoring='neg_mean_squared_error',
          verbose=0)
In [79]:
cvres = rnd_search.cv_results_
for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]):
    print(np.sqrt(-mean_score), params)
49150.657232934034 {'max_features': 7, 'n_estimators': 180}
51389.85295710133 {'max_features': 5, 'n_estimators': 15}
50796.12045980556 {'max_features': 3, 'n_estimators': 72}
50835.09932039744 {'max_features': 5, 'n_estimators': 21}
49280.90117886215 {'max_features': 7, 'n_estimators': 122}
50774.86679035961 {'max_features': 3, 'n_estimators': 75}
50682.75001237282 {'max_features': 3, 'n_estimators': 88}
49608.94061293652 {'max_features': 5, 'n_estimators': 100}
50473.57642831875 {'max_features': 3, 'n_estimators': 150}
64429.763804893395 {'max_features': 5, 'n_estimators': 2}
برای این مدل نیز می‌توانیم بهترین هایپرپارامتر‌هایی را که در جریان جستجو پیدا شده‌اند را مشاهده کنیم.
In [80]:
rnd_search.best_params_
Out[80]:
{'max_features': 7, 'n_estimators': 180}
برای داشتن شهود بهتر نسبت به آن که متغیر‌های مدل ما چگونه روی پیش‌بینی نهایی تاثیر دارند، می‌توان از ویژگی $\tt{feature\underline{}importances\underline{}}$ استفاده کرد. برای مدل $\tt{RandomForestRegressor}$ای که با GridSearch به دست آوردیم اهمیت متغیر‌ها را بررسی می‌کنیم.
In [81]:
feature_importances = grid_search.best_estimator_.feature_importances_
feature_importances
Out[81]:
array([7.33442355e-02, 6.29090705e-02, 4.11437985e-02, 1.46726854e-02,
       1.41064835e-02, 1.48742809e-02, 1.42575993e-02, 3.66158981e-01,
       5.64191792e-02, 1.08792957e-01, 5.33510773e-02, 1.03114883e-02,
       1.64780994e-01, 6.02803867e-05, 1.96041560e-03, 2.85647464e-03])
برای بهتر فهمیدن اعداد بالا هر کدام را در کنار متغیر مربوطه‌اش قرار می‌دهیم:
In [82]:
extra_attribs = ["rooms_per_hhold", "pop_per_hhold", "bedrooms_per_room"]
cat_encoder = full_pipeline.named_transformers_["cat"].named_steps['onehot_encoder']
cat_one_hot_attribs = list(cat_encoder.categories_[0])
attributes = num_attribs + extra_attribs + cat_one_hot_attribs
sorted(zip(feature_importances, attributes), reverse=True)
Out[82]:
[(0.3661589806181342, 'median_income'),
 (0.1647809935615905, 'INLAND'),
 (0.10879295677551573, 'pop_per_hhold'),
 (0.07334423551601242, 'longitude'),
 (0.0629090704826203, 'latitude'),
 (0.05641917918195401, 'rooms_per_hhold'),
 (0.05335107734767581, 'bedrooms_per_room'),
 (0.041143798478729635, 'housing_median_age'),
 (0.014874280890402767, 'population'),
 (0.014672685420543237, 'total_rooms'),
 (0.014257599323407807, 'households'),
 (0.014106483453584102, 'total_bedrooms'),
 (0.010311488326303787, '<1H OCEAN'),
 (0.002856474637320158, 'NEAR OCEAN'),
 (0.00196041559947807, 'NEAR BAY'),
 (6.028038672736599e-05, 'ISLAND')]
حال با استفاده از بهترین مدلی که به دست آوردیم روی داده تست (پس از آماده‌سازی آن) پیش‌بینی انجام می‌دهیم و میزان خطا را چاپ می‌کنیم.
In [83]:
final_model = grid_search.best_estimator_

X_test = strat_test_set.drop("median_house_value", axis=1)
y_test = strat_test_set["median_house_value"].copy()

X_test_prepared = full_pipeline.transform(X_test)
final_predictions = final_model.predict(X_test_prepared)

final_mse = mean_squared_error(y_test, final_predictions)
final_rmse = np.sqrt(final_mse)
In [84]:
final_rmse
Out[84]:
47730.22690385927
می توان خط‌لوله مربوط به بخش آماده‌سازی داده را با خط‌لوله مربوط به مدل رگرسیون خطی در یک Pipeline قرار داد.
In [85]:
full_pipeline_with_predictor = Pipeline([
        ("preparation", full_pipeline),
        ("linear", LinearRegression())
    ])

full_pipeline_with_predictor.fit(housing, housing_labels)
full_pipeline_with_predictor.predict(some_data)
Out[85]:
array([210644.60459286, 317768.80697211, 210956.43331178,  59218.98886849,
       189747.55849879])
یک نکته بسیار کاربردی ذخیره کردن مدل برای استفاده از آن در آینده است. برای این کار می‌توان مدل خود را در فایلی با پسوند .pkl با استفاده از تابع $\tt{dump}$ ذخیره کرد و همان فایل با تابع $\tt{load}$ قابل بارگیری است.
In [86]:
my_model = full_pipeline_with_predictor
In [87]:
from sklearn.externals import joblib
joblib.dump(my_model, "my_model.pkl") # DIFF
#...
my_model_loaded = joblib.load("my_model.pkl") # DIFF
همان طور که در بالا ذکر شد می‌توان هایپرپارامترهای مدل را با استفاده از توزیع‌های مختلفی در روش $\tt{RandomizedSearchCV}$ مقداردهی کرد. به عنوان مثال از دو توزیع نمایی و هندسی با استفاده از کتابخانه scipy به صورت زیر می‌توان نمونه‌برداری کرد.
In [88]:
from scipy.stats import geom, expon
geom_distrib=geom(0.5).rvs(10000, random_state=42)
expon_distrib=expon(scale=1).rvs(10000, random_state=42)
plt.hist(geom_distrib, bins=50)
plt.show()
plt.hist(expon_distrib, bins=50)
plt.show()