机器学习实战_一个完整的程序(一)
使用housing.csv 训练数据
两种常用性能评价指标:
- 均方根误差RSEM
- 平均绝对误差MAE
$$RESM = \sqrt{ \frac{1}{m}\sum_{i=1}^m(y'-y)^2}$$
$$MAE = \sqrt{ \frac{1}{m}\sum_{i=1}^m|y'-y|}$$
MSE 和 MAE 都是测量预测值和目标值两个向量距离的方法。有多种测量距离的方法,或范数,更一般的K 阶闵氏范数写成。p=0时,ℓ0(汉明范数)只显示了这个向量的基数(即,非零元素的个数),p趋于无穷时,ℓ∞(切比雪夫范数)是向量中最大的绝对值。
$$RESM = \sqrt{ \frac{1}{m}\sum_{i=1}^m|y'-y|^p}$$
拆分训练集与测试集:
import numpy as np 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] train_set, test_set = split_train_test(housing, 0.2) # housing数据二八拆分
这个方法可行,但是并不完美:如果再次运行程序,就会产生一个不同的测试集。多次运行之后,你(或你的机器学习算法)就会得到整个数据集,这是需要避免的。
一个通常的解决办法是使用每个实例的识别码,以判定是否这个实例是否应该放入测试集(假设实例有单一且不变的识别码)。例如,你可以计算出每个实例识别码的哈希值,只保留其最后一个字节,如果值小于等于 51(约为 256 的 20%),就将其放入测试集。这样可以保证在多次运行中,测试集保持不变,即使更新了数据集。新的测试集会包含新实例中的 20%,但不会有之前位于训练集的实例。下面是一种可用的方法:
import hashlib # 参数identifier为单一且不变的识别码,可以为索引id # hash(np.int64(identifier)).digest()[-1]返回识别码的哈希摘要值的最后一个字节 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]
不过,房产数据集没有识别码这一列。最简单的方法是使用行索引作为 ID:
housing_with_id = housing.reset_index() # 增加一个索引列,放在数据的第一列 train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "index")
如果使用行索引作为唯一识别码,你需要保证新数据放到现有数据的尾部,且没有行被深处。如果做不到,则可以用最稳定的特征来创建唯一识别码。例如,一个区的维度和经度在几百万年之内是不变的,所以可以将两者结合成一个 ID。
如果你想简单地拆分数据做预测模型示例,使用split_train_test进行拆分即可。Scikit-Learn 提供了一些函数,可以用多种方式将数据集分割成多个子集。最简单的函数是train_test_split,它的作用和之前的函数split_train_test很像,并带有其它一些功能。首先,它有一个random_state参数,可以设定前面讲过的随机生成器种子;第二,你可以将种子传递到多个行数相同的数据集,可以在相同的索引上分割数据集(这个功能非常有用,比如你有另一个DataFrame作为标签):
from sklearn.model_selection import train_test_split train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
另外一种拆分形式:分层采样
将人群分成均匀的子分组,称为分层,从每个分层去除合适数量的实例,以保证测试集对总人数有代表性。例如,美国人口的 51.3% 是女性,48.7% 是男性。所以在美国,严谨的调查需要保证样本也是这个比例:513 名女性,487 名男性作为数据样本。
数据集中的每个分层都要有足够的实例位于你的数据中,这点很重要。否则,对分层重要性的评估就会有偏差。这意味着,你不能有过多的分层,且每个分层都要足够大。后面的代码通过将收入中位数除以 1.5(以限制收入分类的数量),创建了一个收入类别属性,用ceil对值舍入(以产生离散的分类),然后将所有大于 5的分类归入到分类5 :
# 预处理,创建"income_cat"属性 # 凡是会对原数组作出修改并返回一个新数组的,往往都有一个 inplace可选参数 # inplace=True,原数组名对应的内存值直接改变;inplace=False,原数组名对应的内存值并不改变,新的结果赋给一个新的数组. housing["income_cat"] = np.ceil(housing["median_income"] / 1.5) housing["income_cat"].where(housing["income_cat"] < 5, 5.0, inplace=True) # 现在,就可以根据收入分类,进行分层采样。你可以使用 Scikit-Learn 的StratifiedShuffleSplit类 from sklearn.model_selection import StratifiedShuffleSplit # random_state为随机种子生成器,可以得到相同的随机结果 # n_splits是将训练数据分成train/test对的组数,这里汇总成一组数据 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 set in (strat_train_set, strat_test_set): set.drop(["income_cat"], axis=1, inplace=True)
可视化数据寻找规律:
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()
每个圈的半径表示街区的人口(选项s),颜色代表价格(选项c)。我们用预先定义的名为jet的颜色图(选项cmap),它的范围是从蓝色(低价)到红色(高价):
这张图说明房价和位置(比如,靠海)和人口密度联系密切。你还可以很容易地使用corr()方法计算出每对属性间的标准相关系数(也称作皮尔逊相关系数):
corr_matrix = housing.corr()
现在来看下每个属性和房价中位数的关联度:
>>> corr_matrix["median_house_value"].sort_values(ascending=False) median_house_value 1.000000 median_income 0.687170 total_rooms 0.135231 housing_median_age 0.114220 households 0.064702 total_bedrooms 0.047865 population -0.026699 longitude -0.047279 latitude -0.142826 Name: median_house_value, dtype: float64
相关系数的范围是 -1 到 1。当接近 1 时,意味强正相关;例如,当收入中位数增加时,房价中位数也会增加。当相关系数接近 -1 时,意味强负相关;你可以看到,纬度和房价中位数有轻微的负相关性(即,越往北,房价越可能降低)。最后,相关系数接近 0,意味没有线性相关性。(没有直接的线性关系,不是没有关系)
另一种检测属性间相关系数的方法是使用 Pandas 的scatter_matrix函数,它能画出每个数值属性对每个其它数值属性的图。因为现在共有 11 个数值属性,你可以得到11 ** 2 = 121张图。
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))
得到两个属性的散点图
为机器学习算法准备数据
- 函数可以让你在任何数据集上(比如,你下一次获取的是一个新的数据集)方便地进行重复数据转换。
- 你能慢慢建立一个转换函数库,可以在未来的项目中复用。
- 在将数据传给算法之前,你可以在实时系统中使用这些函数。
- 这可以让你方便地尝试多种数据转换,查看那些转换方法结合起来效果最好。
数据清洗
大多机器学习算法不能处理特征丢失,因此先创建一些函数来处理特征丢失的问题。前面,你应该注意到了属性total_bedrooms有一些缺失值。有三个解决选项:
- 去掉对应的街区;
- 去掉整个属性;
- 进行赋值(0、平均值、中位数等等)。
用DataFrame的dropna(),drop(),和fillna()方法,可以方便地实现:
housing.dropna(subset=["total_bedrooms"]) # 选项1 housing.drop("total_bedrooms", axis=1) # 选项2 axis=0对行操作,axis=1对列操作 median = housing["total_bedrooms"].median() housing["total_bedrooms"].fillna(median) # 选项3
如果选择选项 3,你需要计算训练集的中位数,用中位数填充训练集的缺失值,不要忘记保存该中位数。后面用测试集评估系统时,需要替换测试集中的缺失值,也可以用来实时替换新数据中的缺失值。
Scikit-Learn 提供了一个方便的类来处理缺失值:Imputer。下面是其使用方法:首先,需要创建一个Imputer实例,指定用该属性的中位数替换它的每个缺失值:
from sklearn.preprocessing import Imputer imputer = Imputer(strategy="median") # 进行中位数赋值
因为只有数值属性才能算出中位数,我们需要创建一份不包括文本属性ocean_proximity的数据副本:
housing_num = housing.drop("ocean_proximity", axis=1) # 去除ocean_proximity不为数值属性的特征
现在,就可以用fit()方法将imputer实例拟合到训练数据:
imputer.fit(housing_num)
imputer计算出了每个属性的中位数,并将结果保存在了实例变量statistics_中。只有属性total_bedrooms有缺失值,但是我们确保一旦系统运行起来,新的数据中没有缺失值,所以安全的做法是将imputer应用到每个数值:
>>> imputer.statistics_ # 实例变量statistics_和housing_num数值数据得到的中位数是一样的 array([ -118.51 , 34.26 , 29. , 2119. , 433. , 1164. , 408. , 3.5414]) >>> housing_num.median().values array([ -118.51 , 34.26 , 29. , 2119. , 433. , 1164. , 408. , 3.5414])
现在,你就可以使用这个“训练过的”imputer来对训练集进行转换,通过将缺失值替换为中位数:
X = imputer.transform(housing_num)
结果是一个普通的 Numpy 数组,包含有转换后的特征。如果你想将其放回到 PandasDataFrame中,也很简单:
housing_tr = pd.DataFrame(X, columns=housing_num.columns) # 得到处理缺失值后的DF数据
处理文本和类别属性
前面,我们丢弃了类别属性ocean_proximity,因为它是一个文本属性,不能计算出中位数。大多数机器学习算法跟喜欢和数字打交道,所以让我们把这些文本标签转换为数字。Scikit-Learn 为这个任务提供了一个转换器LabelEncoder:
# 简单来说 LabelEncoder 是对不连续的数字或者文本进行编号 # le.fit([1,5,67,100]) # le.transform([1,1,100,67,5]) # 输出: array([0,0,3,2,1]) >>> from sklearn.preprocessing import LabelEncoder >>> encoder = LabelEncoder() >>> housing_cat = housing["ocean_proximity"] >>> housing_cat_encoded = encoder.fit_transform(housing_cat) >>> housing_cat_encoded array([1, 1, 4, ..., 1, 0, 3])
处理离散特征这还不够,Scikit-Learn 提供了一个编码器OneHotEncoder,用于将整书分类值转变为独热向量。注意fit_transform()用于 2D 数组,而housing_cat_encoded是一个 1D 数组,所以需要将其变形:
>>> from sklearn.preprocessing import OneHotEncoder >>> encoder = OneHotEncoder() >>> housing_cat_1hot = encoder.fit_transform(housing_cat_encoded.reshape(-1,1)) >>> housing_cat_1hot <16513x5 sparse matrix of type '<class 'numpy.float64'>' with 16513 stored elements in Compressed Sparse Row format>
注意输出结果是一个 SciPy 稀疏矩阵,而不是 NumPy 数组。当类别属性有数千个分类时,这样非常有用。经过独热编码,我们得到了一个有数千列的矩阵,这个矩阵每行只有一个 1,其余都是 0。使用大量内存来存储这些 0 非常浪费,所以稀疏矩阵只存储非零元素的位置。你可以像一个 2D 数据那样进行使用,但是如果你真的想将其转变成一个(密集的)NumPy 数组,只需调用toarray()方法:
>>> housing_cat_1hot.toarray() array([[ 0., 1., 0., 0., 0.], [ 0., 1., 0., 0., 0.], [ 0., 0., 0., 0., 1.], ..., [ 0., 1., 0., 0., 0.], [ 1., 0., 0., 0., 0.], [ 0., 0., 0., 1., 0.]])
使用类LabelBinarizer,我们可以用一步执行这两个转换(从文本分类到整数分类,再从整数分类到独热向量):
>>> from sklearn.preprocessing import LabelBinarizer >>> encoder = LabelBinarizer() >>> housing_cat_1hot = encoder.fit_transform(housing_cat) >>> housing_cat_1hot array([[0, 1, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 0, 0, 1], ..., [0, 1, 0, 0, 0], [1, 0, 0, 0, 0], [0, 0, 0, 1, 0]])
注意默认返回的结果是一个密集 NumPy 数组。向构造器LabelBinarizer传递sparse_output=True,就可以得到一个稀疏矩阵。