这篇文章是本人学习 《Hands-On-Machine-Learning-with-Scikit-Learn-and-TensorFlow》的读书笔记第二篇。整理出来是希望在巩固自己的学习效果的同时,希望能够帮助到同样想学习的人。本人也是小白,可能很多地方理解和翻译不是很到位,希望大家多多谅解和提意见。
这一章将会在一个实际的地产项目中完成一个完整的机器学习项目,其中包括以下步骤:
纵观整个项目收集数据通过可视化发现数据规律为机器学习算法准备数据选择一个模型并开始训练微调模型展示结果上线、监控、维护系统建立一个模型预测加利福利亚的房价,给定的数据中包括人口、收入中位数、房价中位数等特征。我们的模型应该通过从这些数据中学习,在给定其他特征的情况下能够预测出任一区的房价中位数。
首先我们知道这是一个监督学习的问题,因为我们的数据是有标签的。另外这也是一个回归问题,因为我们需要的是预测一个数值。更具体地,这是一个多元回归的问题,因为系统需要使用多个特征去做预测。最后因为我们面对的不是连续的数据流,不需要经常改变系统适应数据的变化。同时数据量也足够小,可以一次读入内存,所以批量学习就足够了。
对于回归问题,常见的性能度量指标是 均方根误差(Root Mean Square Error,RMSE)。它测量的是系统在做预测时错误的标准差。 Equation 2-1. Root Mean Square Error (RMSE) RMSE ( X , h ) = 1 m ∑ i = 1 m ( h ( x ( i ) ) − y ( i ) ) 2 \operatorname{RMSE}(\mathbf{X}, h)=\sqrt{\frac{1}{m} \sum_{i=1}^{m}\left(h\left(\mathbf{x}^{(i)}\right)-y^{(i)}\right)^{2}} RMSE(X,h)=m1∑i=1m(h(x(i))−y(i))2
本书中会用到的一些符号:
m m m 表示的是要计算 RMSE的数据集的大小; x ( i ) \mathbf{x}^{(i)} x(i)代表数据集中第 i i i个例子的所有特征值组成的项目; y ( i ) y^{(i)} y(i) 代表数据集中第 i i i个例子的标签; X \mathbf{X} X是一个矩阵,由数据集中所有数据的全部特征组成,每一行代表一个数据, i t h i^{t h} ith 等于 x ( i ) \mathbf{x}^{(i)} x(i)的转置; h h h是系统的预测函数,也称为假设。当给定一个数据的特征矩阵 x ( i ) \mathbf{x}^{(i)} x(i),它给出该数据一个预测值 y ^ ( i ) = h ( x ( i ) ) \hat{y}^{(i)}=h\left(\mathbf{x}^{(i)}\right) y^(i)=h(x(i)); RMSE ( X , h ) \operatorname{RMSE}(\mathbf{X}, h) RMSE(X,h) 是使用假设 h h h在数据集上计算的损失函数。当数据中存在离群社区是,我们可以考虑平均绝对误差。 Equation 2-2. Mean Absolute Error MAE ( X , h ) = 1 m ∑ i = 1 m ∣ h ( x ( i ) ) − y ( i ) ∣ \operatorname{MAE}(\mathbf{X}, h)=\frac{1}{m} \sum_{i=1}^{m}\left|h\left(\mathbf{x}^{(i)}\right)-y^{(i)}\right| MAE(X,h)=m1∑i=1m∣∣h(x(i))−y(i)∣∣ R M S E RMSE RMSE 和 M A E MAE MAE 都是计算两个向量距离的方法:预测的向量和真实的向量。
计算 R M S E RMSE RMSE 相当于计算的是欧几里得范数,也称为 ℓ 2 \ell_{2} ℓ2范数,记为 ∥ ⋅ ∥ 2 \|\cdot\|_{2} ∥⋅∥2。计算 M A E MAE MAE 相当于计算的是 ℓ 1 \ell_{1} ℓ1范数,记为 ∥ ⋅ ∥ 1 \|\cdot\|_{1} ∥⋅∥1。这也被称为曼哈顿距离,因为它计算的是当你只能按照正交街区的方法行走时 城市中两个点的距离。包含 n n n 个元素的向量 v v v 的 ℓ k \ell_{k} ℓk范数定义为 ∥ v ∥ k = ( ∣ v 0 ∣ k + ∣ v 1 ∣ k + ⋯ + ∣ v n ∣ k ) 1 / k \|\mathbf{v}\|_{k}=\left(\left|v_{0}\right|^{k}+\left|v_{1}\right|^{k}+\cdots+\left|v_{n}\right|^{k}\right)^{1/k} ∥v∥k=(∣v0∣k+∣v1∣k+⋯+∣vn∣k)1/k范数的索引越高的话,它越侧重于数值大的数而忽略数值小的数。这也是为什么 R M S E RMSE RMSE 比 M A E MAE MAE 对异常值更敏感。但是当异常值是指数稀少时(像一个钟型曲线), R M S E RMSE RMSE 表现的更好。通过程序从网上下载数据并解压到 datasets/housing 文件夹。
import numpy as np import os import pandas as pd import tarfile from six.moves import urllib DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml/master/" HOUSING_PATH = os.path.join("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_url下载文件到 tgz_path housing_tgz = tarfile.open(tgz_path) housing_tgz.extractall(path=housing_path) #解压文件 housing_tgz.close() fetch_housing_data()使用 Pandas 来读取数据
def load_housing_data(housing_path=HOUSING_PATH): csv_path = os.path.join(housing_path, "housing.csv") return pd.read_csv(csv_path)使用 i n f o ( ) info() info()的方法来观察数据,看看数据的行数,各属性的类别,非Null值的个数。 可以看出数据集中有20640个数据,其中 total_bedrooms 中只有20433个非空值,表示其中还有207个空值,后期处理需要注意这个问题。另外 ocean_proximity 数据类型为 object,通过查看数据知为类别型。通过 value_counts() 方法查看类别数及每一类的数量。 通过 describe() 方法对所有数值型特征做个汇总。 另一个观察数据的好方法就是直方图,直方图能够表示出每个数值变量在给定范围的数据个数。 通过观察直方图,发现以下几个问题:
median_income不像是用美元表示的,实际上是被缩放过了,被限定在0.5-15之间。housing_median_age 和 median_house_value 都被覆盖了顶部(大于某个值的数据被该值覆盖)。为了处理这种情况,可以考虑: -给这些被覆盖的数据找到合适的标签 -把这些数据从训练数据中删除不同的属性有不一样的尺度,后面我们会谈到特征放缩大部分特征都是重尾的:他们更偏向于中位数的右边而不是左边。我们会尝试进行特征转换将这些数据转换成更像正态分布。创建一个测试集,理论上是很简单的。只需要从数据集中随机地抽出20%的数据即可。
def split_train_test(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]这样创建会遇到一个问题,就是每次运行这个函数得到的结果都不一样。可以在 np.random.permutation() 之前使用 np.random.seed(42)固定随机数的种子。但是当我们的数据集有更新时,这两种方法得到的数据集都会被打乱。可以通过计算每个数据标识符的哈希值,把哈希值的最后一个字节小于51(20%*256)的数据划入测试集。这样即使下次数据集有更新,我们的测试集也不会有变化。
import hashlib #hash值的最后一个字节小于51的划入测试集 def test_set_check(identifier, test_ratio, hash): return hash(np.int64(identifier)).digest()[-1] < 256 * test_ratio def split_train_test_by_id(data, test_ratio, id_column, hash=hashlib.md5): ids = data[id_column] in_test_set = ids.apply(lambda id_: test_set_check(id_, test_ratio, hash)) return data.loc[~in_test_set], data.loc[in_test_set]但是我们的数据中没有可作为标识符的列,可以利用行号来创建一个。
housing_with_id = housing.reset_index() # 加入 'index' 列 train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, 'index')如果使用行号作为标识符的话,那新加入的数据必须附加在原数据之后且不能够删除数据。如果不能保证这些的话,可以引入新的方法产生唯一的标识符。
housing_with_id['id'] = housing['longitude']*1000 + housing['latitude'] train_set, test_set = split_train_test_by_id(housing_with_id, 0.2,'id')也可以使用 Scikit-Learn自带的函数 train_test_split()
from sklearn.model_selection import train_test_split train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)考虑到 median_income 对预测房价比较重要,为了避免随机创建测试集带来的样本偏差,可以考虑分层抽样。为此需要对数据做一些整理。
housing['income_cat'] = np.ceil(housing['median_income'] / 1.5) housing['income_cat'].where(housing['income_cat'] < 5, 5.0,inplace=True)根据收入的类别,使用Scikit-Learn’s StratifiedShuffleSplit()来进行分层抽样。
from sklearn.model_selection import StratifiedShuffleSplit #Provides train/test indices to split data in train/test sets. split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42) for train_index, test_index in split.split(housing,housing['income_cat']): strat_train_set = housing.loc[train_index] strat_test_set = housing.loc[test_index]为了使数据恢复原来的样子,我们需要删除 income_cat 这一列。
for data in (strat_train_set, strat_test_set): data.drop(['income_cat'],axis=1,inplace=True)每个圆圈的半径代表该区域的人口数量,颜色代表价格,我们使用cmap来绘制色彩表,颜色从蓝到红表示数值从高到低。
housing.plot(kind='scatter', x='longitude', y='latitude', alpha=0.4, s=housing['population']/100, label='population', c='median_house_value', cmap=plt.get_cmap('jet'), colorbar=True) plt.legend()分析几个可能和房价存在相关性的特征
from pandas.tools.plotting import scatter_matrix attributes = ["median_house_value", "median_income", "total_rooms", "housing_median_age"] scatter_matrix(housing[attributes],figsize=(12,8))从图形中可以看出,这两个变量存在正相关;数据在500,000的时候被明显截顶了,且大概在450,000和380,000的位置也存在一条水平线。在训练的时候可能要注意删除这些数据,防止模型学到这些怪异的特征。
bedrooms_per_room 比 total_bedrooms相关性更强。
