在学习《Scikit_Learn 与 TensorFlow机器学习实战指南》之前,也曾一直苦恼,不管是机器学习还是深度学习他的一个总体的流程是什么,头脑中没有一个大概的概念。在本书中,作者给出了答案:
- 项目概述
- 获取数据
- 发现并可视化数据,发现其中的规律
- 为机器学习算法准备数据(对数据的清洗等操作)
- 选择模型,进行训练
- 微调模型
- 给出解决方案
- 部署、监控、维护系统
一、项目概述
本章节以加州房产价格预测为例来讲解整个流程,数据集采用1990年加州普查的数据,数据是有点老,但根据本文所说,具有很多优点。毕竟,有免费真实的数据供自己学习练手也挺好。
目标:自己的模型要利用这个数据进行学习,然后根据其他指标,预测任何街区的房价中位数
(其他问题,比如划定问题(本项目的商业目标?)、选择性能指标(回归问题典型指标均方根误差)等在此略过)
二、获取数据
一般情况下,数据是存储于关系数据库(或其他常见数据库)中的多个表、文档、文件。
为防止数据变动频繁,最好的方法就是写个脚本,随时获取最新的数据(或创建一个定时任务来做),实现对获取数据集的自动化
1 import os 2 import tarfile 3 from six.moves import urllib 4 5 DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml/master/" 6 HOUSING_PATH = "datasets/housing" 7 HOUSING_URL = DOWNLOAD_ROOT + HOUSING_PATH + '/housing.tgz' 8 9 def fetch_housing_data(housing_url = HOUSING_URL,housing_path = HOUSING_PATH): 10 if not os.path.isdir(housing_path): 11 os.makedirs(housing_path) 12 tgz_path = os.path.join(housing_path,'housing.tgz') 13 urllib.request.urlretrieve(HOUSING_URL,tgz_path) 14 housing_tgz = tarfile.open(tgz_path) 15 housing_tgz.extractall(path=housing_path) 16 housing_tgz.close()
作为前脚刚踏进机器学习门槛,后脚还在门外的孩子,看到这代码有诸多不解
six模块是啥?可展开下面详解
运行数据集自动下载代码:
fetch_housing_data()
可以下载目录filename查看:
数据实现如下:
数据下载完成后,使用Pandas读取数据:
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)
pd.read_csv(filename)函数会返回一个包含所有数据的Pandas DataFrame对象
那如何查看读取数据呢?可以使用DataFrame对象的head()方法就能查看该数据集的前五行数据
1 housing = load_housing_data() 2 housing.head()
数据前五行数据如下所示:
每一行都代表一个街区,共有10个属性:经度、维度、房屋年龄中位数、总房间数、总卧室数、人口数、家庭数、收入中位数、房屋价值中位数、离大海距离
可以使用info()方法查看数据的描述,尤其是总行数、每个属性的类型以及非空值的数量
1 housing.info()
结果如下:
由上面的数据描述可得,总卧室数只有20433个非空值,这就意味着有207个街区缺少这个数据,我们也会在后期对它进行处理
所有的属性都是数值类型的,除了离大海距离,它的类型是object类型的,因此可以包含任意python对象,但因为该项是从csv文件加载的,所以必然是文本类型的。
在查看数据前五项的时候,注意到某列的值是由重复的,只意味着它可能是一项表示类别的属性,可以使用value_counts()方法查看该项中都有哪些类别,每个类别中都包含有多少个街区
1 housing['ocean_proximity'].value_counts()
结果如下:
describe()方法展示数值属性的概括
1 housing.describe()
结果如下:
( 因为只能查看数值属性的概述,所以离大海距离并没有在此显示)
count(总数据的个数)、mean(平均数)、std(标准差:揭示数值的分散度)、min(最小值)、25%、50%、75%展示对应的分位数:每个分位数指明小于这个值,且指定分组的百分比,比如,25%的街区的房屋年龄中位数小于18,而50%的小于29,75%的小于37。这些值通常称为第25个百分点(或第一个四分位数),中位数、第75个百分点(第三个四分位数)
现在对数据有了个大概的了解,如何使数据更加形象的展现出来呢?决定画出每个数值属性的柱状图。柱状图的纵轴展示了特定范围的实例的个数,横轴代表属性值。可以一次给一个属性画图,或对完整数据集调用hist()方法绘制每个数值属性的柱状图。
1 %matplotlib inline 2 import matplotlib.pyplot as plt 3 housing.hist(bins = 50,figsize = (20,20)) 4 plt.show()
再做进一步的数据查看时,需要创建测试集,将测试集放在一边,用于最后的模型测试
创建测试集
数据集的分割有以下几种思路:(参考:https://www.jianshu.com/p/1e203872c0c4?utm_campaign=maleskine)
- 自写函数通过随机直接分割数据
- 自写函数通过哈希编码+随机分割数据
- 调用sklearn中train_test_split()方法分割数据
- 分层抽样分割数据
自写函数通过随机直接分割数据
理论上,创建测试集很简单:只要随机挑选一些实例,一般是数据集的 20%,放到一边:
def split_train_test(data,test_ratio): #np.random.seed(42) 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.shuffle()或者是np.random.permutation()来完成,实现的功能是一样的,区别在于
- shuffle的参数只能是array_like,而permutation除了array_like还可以是int类型,如果是int类型,那随机打乱np.arange(int)
- shuffle返回值为None,也就是没有返回值,而permutation则返回打乱后的array
执行上述代码:
1 train_set,test_set = split_train_test(housing,0.2)
这个方法可行,但存在不足,当再次运行程序,就会产生一个不同的测试集,多次运行之后,就会得到整个数据集
解决方法 :(数据集不更新的条件下 )
- 可以保存第一次运行得到的测试集,并在随后的过程中进行加载
- 在调用np.random.permutation()之前,设置随机生成器的种子(比如:np.random.seed(42)),就可以总是产生相同的洗牌指数(shuffled indices)
自写函数通过哈希编码+随机分割数据
但如果数据集更新,上述的两个解决方案就会失效,一个通常的解决方法就是使用每个实例的ID来判定这个实例是否放入测试集(假设每个实例都有唯一并且不变的ID)。例如,你可以计算出每个实例ID的哈希值,只保留其最后一个字节,如果该值小于等于 51(约为 256 的20%),就将其放入测试集。这样可以保证在多次运行中,测试集保持不变,即使更新了数据集。新的测试集会包含新实例中的 20%,但不会有之前位于训练集的实例
import hashlib 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]
看到上面的描述,以及代码的书写,我产生了一个疑问???为什么是256的20%呢?,为什么不是512,128的20%呢,后来我找到了可以说服我的理由:
代码中,将唯一标识码转换成了int64类型的数据,这种数据类型相当于C++中long long 类型,它要占8位,那么2^8 = 256
执行上述代码:
#用行索引作为ID(条件:保证新数据都被放在现有数据的尾部,且没有行被删除) housing_with_id = housing.reset_index()#增加index列 train_set,test_set = split_train_test_by_id(housing_with_id,0.2,'index') train_set.size,test_set.size
运行结果:
#如果上述条件不能被满足,可以采用最稳定的特征来创建唯一识别码 housing_with_id['id'] = housing['longitude'] * 1000 + housing['latitude'] train_set,test_set = split_train_test_by_id(housing_with_id,0.2,'id') train_set.size,test_set.size
运行结果:
调用sklearn中train_test_split()方法分割数据
Scikit-Learn 提供了一些函数,可以用多种方式将数据集分割成多个子集。最简单的函数是 train_test_split(),它的作用和之前的函数 split_train_test() 很像,并带有其它一些功能。首先,它有一个 random_state 参数,可以设定前面讲过的随机生成器种子;第二,你可以将种子传递给多个行数相同的数据集,可以在相同的索引上分割数据集(这个功能非常有用,比如你的标签值是放在另一个 DataFrame 里的):
#也可以使用Scikit-Learn对数据集进行训练集和测试集分离 from sklearn.model_selection import train_test_split #参数random_state,可以设定前面设定随机生成器种子,还可以将种子传递给多个行数相同的数据集,可以在相同的索引上分割数据集 train_set,test_set = train_test_split(housing,test_size = 0.2,random_state = 42) train_set.size,test_set.size
至此,采用的都是纯随机的取样。当数据集很大时(尤其是和属性数相比),这通常是可行,但如果数据集不大,就会有采样偏差的风险
分层抽样分割数据
例如,美国人口的 51.3% 是女性,48.7% 是男性。所以在美国,严谨的调查需要保证样本也是这个比例:513 名女性,487 名男性。这称作分层采样(stratified sampling):将人群分成均匀的子分组,称为分层,从每个分层去取合适数量的实例,以保证测试集对总人数有代表性。如果调查公司采用纯随机采样,会有 12% 的概率导致采样偏差:女性人数少于 49%,或多于 54%。不管发生那种情况,调查结果都会严重偏差。
简而言之,分层抽样就是保证在测试集中每个等级的数据都存在,测试集中的数据分布式是完整的,不然,如果测试集中一些具有代表性的数据不存在,对最后的准确率将存在较大的影响。
现在,某专家说:收入中位数是预测房价中位数非常重要的属性,就需要保证测试集可以代表整个数据集中的多种收入分类。因为收入中位数是一个连续的数值属性,首先需要创建一个收入类别属性。(我们将收入中位数属性作为分类依据,进行分层)
首先来看下原始收入中位数的直方图表示:
housing['median_income'].hist()
结果如下:
由上图也可以看出,原始数据中的数据分布并不能作为一个良好的分类依据,因此,我们需要对原始数据需要稍微的进行修改,以限制分类的数量。
housing['income_cat'] = np.ceil(housing['median_income'] / 1.5)#除以1.5以限制分类的数量 housing['income_cat'].where(housing['income_cat'] < 5,5.0,inplace=True) housing
上面代码中有个数据筛查、替换的工作: data.where(cond, other=nan, inplace=False, axis=None, level=None, errors=’raise’, try_cast=False, raise_on_error=None) 该函数主要实现的功能就是:当条件cond满足时,数据不发生改变,当不满足条件cond时,会将数据data中的原有数据替换成other
运行结果如下所示:
查看数据各分类的数据量:
housing['income_cat'].value_counts() / len(housing)
在Pandas中也提供了能够实现上述相同效果的函数
pandas.cut():用来把一组数据分割成离散的区间。比如有一组年龄数据,可以使用pandas.cut
将年龄数据分割成不同的年龄段并打上标签。
函数原型: pandas.cut(x, bins, right=True, labels=None, retbins=False, precision=3, include_lowest=False, duplicates='raise') 参数含义: x:被切分的类数组(array-like)数据,必须是1维的(不能用DataFrame) bins:bins是被切割后的区间(或者叫“桶”、“箱”),有三种形式:一个int型的标量、标量序列(数组)或者pandas.IntervalIndex。 一个int型的标量 当bins为一个int型的标量时,代表将x平分成bins份。x的范围在每侧扩展0.1%,以包括x的最大值和最小值 标量序列 标量序列定义了被分割后每一个bin的区间边缘,此时x没有扩展 pandas.IntervalIndex 定义要使用的精确区间 right:bool型参数,默认为True,表示是否包含区间右部。比如,bins = [1,2,3],right= True,则区间为(1,2],(2,3],right= False,则区间为(1,2),(2,3) labels:给分割后的bins打标签,比如把年龄x分割成年龄段bins后,可以给年龄段打上诸如青年、中年的标签。labels的划分后的区间长度相等,比如bins = [1,2,3],划分后有2个区间(1,2],(2,3],则labels的长度必须为2;如果指定labels=False,则返回x中的数据在第几个bin中(从0开始)。 retbins:bool型的参数,表示是否将分割后的bins返回,当bins为一个int型的标量时比较有用,这样可以得到划分后的区间,默认为False。 precision:保留区间小数点的位数,默认为3. include_lowest:bool型的参数,表示区间的左边是开还是闭的,默认为false,也就是不包含区间左部(闭)。 duplicates:是否允许重复区间。有两种选择:raise:不允许,drop:允许。 返回值: out:一个pandas.Categorical, Series或者ndarray类型的值,代表分区后x中的每个值在哪个bin(区间)中,如果指定了labels,则返回对应的label。 bins:分隔后的区间,当指定retbins为True时返回。 (参考:https://www.cnblogs.com/sench/p/10128216.html)
函数也大概了解清楚了,来用np.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()
运行结果:
直方图显示:
housing['income_cat'].hist()
分类完成,现在进行正式工作:分层取样
#根据收入分类,进行分层采用 from sklearn.model_selection import StratifiedShuffleSplit 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]
StratifiedShuffleSplit交叉验证同样适用于这个划分函数
函数原型: StratifiedShuffleSplit(n_splits = 10,test_size = None ,train_size = None ,random_state = None) 参数含义: n_splits:将数据集分成train/test对的组数,默认是10组 test_size和train_size用来设置train/test对中train和test所占的比例 random_state:将样本随机打乱
split(X,y):按照y的值将数据集分为训练集和测试集
可以在完整的房产数据集中查看收入分类比例:
housing['income_cat'].value_counts() / len(housing)
现在已经测试集、训练集分割完成,需要删除income_cat属性,使属性回到原始状态
for set in (strat_train_set,strat_test_set): set.drop(['income_cat'],axis = 1,inplace = True) strat_test_set
查看测试集strat_test_set数据:
三、发现并可视化数据、发现其中规律
创建一个副本,以免损伤训练集:
housing = strat_train_set.copy()
地理数据可视化
因为存在地理信息(维度和经度),创建一个所有街区的散点图来数据可视化是一个不错的主意:
#创建一个街区的散点图来数据可视化 housing.plot(x = 'longitude',y = 'latitude',kind = 'scatter')
可视化效果如下:
上图看起来像是加州,但看不出来什么规律,将alpha设为0.1,可以更容易看到数据点的密度:
housing.plot(x = 'longitude',y = 'latitude',kind = 'scatter',alpha = 0.1)
(参数alpha:点的不透明度,当点的透明度很高时,单个点的颜色很浅。这样点越密集,对应区域颜色越深。通过颜色很浅就可以就看一看出数据的几种区域。alpha=0,无色,整个绘图区域无图,类似于[R, G, B, alpha]四通道中的alpha通道)(注:可参考:https://blog.csdn.net/dss_dssssd/article/details/82810630)
将分布图画到加州地图上:
import matplotlib.image as mpimg housing.plot(kind = 'scatter',x = 'longitude',y = 'latitude',alpha = 0.1) california_img = mping.imread('./california.png') plt.imshow(california_img,alpha = 0.5,cmap=plt.get_cmap('jet'),extent=[-124.55, -113.80, 32.45, 42.05])
效果如下:
plt.show()中显示的extent,它是控制图片的坐标轴的工具,为(left,right,bottom,top),就是控制x轴为left->right,y轴 :bottom -> top
示意图如下:
回归正题,现在可以清楚的看到高密度区域,湾区、洛杉矶和圣迭戈,以及中央谷,特别是从萨克拉门托和弗雷斯诺。
现在来看房价,每个圈的半径表示街区的人口(选项s),颜色代表价格(选项c),我们用预定义的名为jet的颜色图(选项cmap),它的范围是蓝色(低价)到红色(高价)(关于参数cmap可参考:https://www.cnblogs.com/denny402/p/5122594.html):
housing.plot(kind = 'scatter',x = 'longitude',y = 'latitude',alpha = 0.2, s = housing['population']/100,label = 'population', c = 'median_house_value',cmap = plt.get_cmap('jet'),colorbar=True) plt.legend()
将分布图画到加州地图上:
import matplotlib.image as mping california_img = mping.imread("./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)
#制作价格bar 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.show()
这张图也说明房价和位置(比如:靠海)和人口密度联系密切,可以使用聚类算法来检测主要的聚类,用一个新的特征值测量聚集中心距离。尽管北加州海岸区域的房价不是非常高,但离大海距离属性也可能很有用,所以这不是用一个简单的规则就可以定义的问题。
查找关联
由于数据集并不是太大,可以容易的使用corr()方法计算出每对属性间的标准相关系数(standard correlation coefficient,也称为皮尔逊相关系数):
corr_matrix = housing.corr()
现在查看下每个属性与房价中位数的关联度:
corr_matrix['median_house_value'].sort_values(ascending = False)
相关系数的范围是从-1到1,当接近1时,意味着强正相关,(根据上面的相关性检测,当收入中位数增加时,房价中位数也会增加)当相关系数接近-1时,意味着强负相关(维度和房价中位数有轻微的负相关),相关系数接近0,意味着没有线性相关性。
现在来介绍下另一种检测属性间相关系数的方法:使用Pandas的scatter_matrix函数,它能够画出每个数值属性对其他数值属性的图。由于现在housing中共有11个数值属性,因此将得到11 ^2 = 121张图,这里只关注其中和房价中位数最具有相关的属性。
#from pandas.tools.plotting import scatter_matrix from pandas.plotting import scatter_matrix #更新 attributes = ['median_house_value','median_income','total_rooms','housing_median_age'] scatter_matrix(housing[attributes],figsize=(12,12))
Pandas将每个变量都对自己作图,主对角线都会是直线,所以Pandas展示的是每个属性的柱状图。
由上图也可以得到:最有希望用来预测房价中位数的属性就是收入中位数了,将该图单独拿出
housing.plot(x = 'median_income',y = 'median_house_value',kind = 'scatter',alpha = 0.1)
这张图说明了几点。首先,相关性非常高;可以清晰地看到向上的趋势,并且数据点不是非常分散。第二,我们之前看到的最高价,清晰地呈现为一条位于 500000 美元的水平线。这张图也呈现了一些不是那么明显的直线:一条位于 450000 美元的直线,一条位于 350000 美元的直线,一条在 280000 美元的线,和一些更靠下的线。你可能希望去除对应的街区,以防止算法重复这些巧合。
属性组合实验
给算法准备数据之前,需要做的最后一件事是尝试多种属性组合
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"]
新增加每户有几个房间、每户有几个卧室以及每户人口等属性,与房价中位数的相关性分析
corr_matrix = housing.corr() corr_matrix['median_house_value'].sort_values(ascending = False)
与总房间数或卧室数相比,新的bedrooms_per_room属性与房价中位数的关联更加强。
这一步的数据探索不必非常完备,此处的目的是有一个正确的开始,快速发现规律,以得到一个合理的原型。但这是一个交互过程:一旦得到了一个原型,并运行起来,就可以分析它的输出,进而发现更多的规律,然后再回到数据探索这步。
作者建议在为机器学习算法准备数据时,不要手工来做,而是写一些函数,理由如下:
- 函数可以让你在任何数据集上(比如,下一次获取的是一个新的数据集)方便地进行重复数据转换
- 可以慢慢建立一个转换函数库,可以在未来的项目中使用
- 在将数据传给算法之前,可以在实时系统中使用这些函数
- 可以方便的尝试多种数据转换,查看哪些转换方法结合起来效果最好。
现在,对数据集中的数据已经有了初步的掌握,还是先回到干净的训练集(通过再次复制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有一些缺失值,有三个解决选项:
- 去掉对应的街区
- 去掉整个属性
- 进行赋值(0,平均值、中位数等)
用DataFrame的dropna(),drop()和fillna()方法,很方便的实现
housing.dropna(subset = ['total_bedrooms']) #选项1 housing.drop('totoal_bedrooms',axis = 1) #选项2 median = housing['total_bedrooms'].median() housing['total_bedrooms'].fillna(median) #选项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计算出每个属性的中位数,并将结果保存在了实例变量statistic_中,虽然此时只有属性 total_bedrooms 存在缺失值,但我们不能确定在以后的新的数据中会不会有其他属性也存在缺失值,所以安全的做法是将 imputer 应用到每个数值,也就是imputer会将每个属性的中位数全部都计算出来保存起来了:
imputer.statistics_
housing_num.median().values
现在用“训练过的”imputer来对训练集进行转换,将缺失值替换成中位数:
#将“训练过的”imputer来对训练集进行转换,将缺失值替换成中位数 #返回的结果是一个包含转换后特征的普通的numpy数组 X = imputer.transform(housing_num)
将返回的普通的numpy数组放回到Pandas DataFrame中:
#将返回的普通的numpy数组X放回到Pandas的DataFrame中 housing_tr = pd.DataFrame(X,columns=housing_num.columns)
housing_tr.info()
可以看到total_bedrooms数据已经补齐了
处理文本和类别属性
前面由于需要计算数值属性的中位数,丢弃了类别属性ocean_proximity,因为它是文本属性,不能计算出,又大多数机器学习算法喜欢和数字打交道,因此我们把这些文本标签转换成数字
当然,Scikit_Learn也为这个任务提供了一个转换器LabelEncoder
from sklearn.preprocessing import LabelEncoder encoder = LabelEncoder() housing_cat = housing['ocean_proximity'] housing_cat_encoder = encoder.fit_transform(housing_cat) housing_cat_encoder
译者注解道:
在原书中使用 LabelEncoder 转换器来转换文本特征列的方式是错误的,该转换器只能用 来转换标签(正如其名)。在这里使用 LabelEncoder 没有出错的原因是该数据只有一列 文本特征值,在有多个文本特征列的时候就会出错。应使用 factorize() 方法来进行操 作: housing_cat_encoded, housing_categories = housing_cat.factorize() housing_cat_encoded[:10]
输出对应的类型:
print(encoder.classes_)
但这样的做法使存在问题的,ML算法会认为两个临近的值比两个疏远的值更相似,这样显然是不对的。要解决这个问题,一个常见的方法就是给每个分类创建一个二元属性:当分类1h OCEAN,该属性为1(否则为0),当分类INLAND,另一个属性为1(否则为0),以此类推,这就是独热编码(One-Hot Encoding),因为只有一个属性会等于1(热),其余都是0(冷)
Scikit_Learn提供了一个编码器OneHotEncoder,用于将整数分类值转变成独热编码
注意:fit_transform()用于2D数组,而housing_cat_encoded是一维数组,所以需要将其变形
from sklearn.preprocessing import OneHotEncoder encoder = OneHotEncoder() housing_cat_1hot = encoder.fit_transform(housing_cat_encoder.reshape(-1,1)) housing_cat_1hot
#输出的结果是一个SciPy稀疏矩阵,而不是Numpy数组
要注意转换的输出结果是SciPy稀疏矩阵,而不是Numpy数组。当类别属性有数千个分类时,这非常有用。经过独热编码,会得到一个数千列的矩阵,这个矩阵每一行只由一个1,其余都是0。所以会非常浪费,但如果想将其转换成(密级的)Numpy数组,只需调用toarray()方法:
housing_cat_1hot.toarray()
其实呢,完全可以使用类LabelBinarizer,一步实现这两步操作(从文本转换成整数分类,再从整数分类到独热向量):
from sklearn.preprocessing import LabelBinarizer encoder = LabelBinarizer() housing_cat_1hot = encoder.fit_transform(housing_cat) housing_cat_1hot #默认返回的结果是一个密集的Numpy数组,向构造器LabelBinarizer传递sparse_output=True,就可以得到一个稀疏矩阵
译者注:
在原书中使用 LabelBinarizer 的方式也是错误的,该类也应用于标签列的转换。正确做法是使用sklearn即将提供的 CategoricalEncoder 类。
#from sklearn.preprocessing import CategoricalEncoder # in future versions of Sci
kit-Learn
cat_encoder = CategoricalEncoder()
housing_cat_reshaped = housing_cat.values.reshape(-1, 1)
housing_cat_1hot = cat_encoder.fit_transform(housing_cat_reshaped)
housing_cat_1hot
自定义转换器
尽管Scikit_Learn提供了许多有用的转换器,还是需要自己动手写转换器执行任务,比如自定义的清理操作,或属性组合。为了使自制的转换器与Scikit_Learn组件(比如流水线)无缝衔接,又由于Scikit_Learn是依赖鸭子类型的(而不是继承),因此创建的类必须执行三个方法:fit()(返回self),transform(),和fit_transform()。通过添加 TransformerMixin 作为基类,可以很容易地得到最后一个。另外,如果你添加 BaseEstimator 作为基类(且构造器中避免使用 *args 和 **kargs ),你就能得到两个额外的方法( get_params() 和 set_params() ),二者可以方便地进行超参数自动微调。
from sklearn.base import BaseEstimator,TransformerMixin rooms_ix,bedrooms_ix,population_ix,household_ix = 3,4,5,6 class CombinedAttributesAdder(BaseEstimator,TransformerMixin): def __init__(self,add_bedrooms_per_room = True): 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): rooms_per_household = X[:,rooms_ix] / X[:,household_ix] population_per_household = X[:,population_ix] / X[:,household_ix] if self.add_bedrooms_per_room: bedrooms_per_room = X[:,bedrooms_ix] / X[:,rooms_ix] return np.c_[X,rooms_per_household,population_per_household, bedrooms_per_room] else: return np.c_[X,rooms_per_household,population_per_household] attr_adder = CombinedAttributesAdder(add_bedrooms_per_room = False) housing_extra_attribs = attr_adder.transform(housing.values)
housing_extra_attribs
Note: np.c_[a,b]按行将两数组转换成矩阵,np.r_[a,b]按列将两数组转换成矩阵
示例:
特征缩放
数组要做的最重要的转换之一是特征缩放。
有两种常见的方法可以让所有的属性有相同的量度:线性函数归一化(Min_Max Scaling)和标准化(standardization)
线性函数归一化(归一化):值被转变、重新缩放,直到范围变成0到1,通过减去最小值,除以最大值与最小值的差值,来进行归一化。Scikit_Learn提供了转换器MinMaxScaler来实现这个功能,它有一个超参数feature_range,一个改变范围,如果不希望范围是0到1
标准化:首先减去平均值(所以标准化值的平均值总是0),然后除以方差,使得到分布具有单位方差。与归一化不同,标准化不会限定值到某个特定的范围,这对某些算法可能构成问题(比如,神经网络常需要输入值的范围是0到1),但标准化收到异常值的影响很小。例如,假设一个街区的收入中位数由于某种错误变成了100,归一化会将其它范围是 0 到 15 的值变为 0-0.15,但是标准化不会受什么影响。Scikit_Learn也提供了一个转换器StandardScaler来标准化。
Note:与所有转换一样,缩放只能对训练集拟合,而不是向完整的数据集(包括测试集)。只有这样,才能用缩放器来转换训练集和测试集(和新数据)
转换流水线
现在对数据的清理等工作初步完成,可以发现,在数据转换步骤中,是需要一定的顺序执行的,幸运的是,Scikit_Learn提供了类Pipeline,来执行这一系列的转换,下面是一个数值属性的小流水线:
from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler num_pipline = Pipeline([ ('imputer',Imputer(strategy = "median")),#补充空值 ('attribs_adder',CombinedAttributesAdder()),#属性结合 ('std_scaler',StandardScaler())#特征缩放 ]) housing_num_tr = num_pipline.fit_transform(housing_num)
Pipeline 构造器需要一个定义步骤顺序的名字/估计器对的列表。除了最后一个估计器,其余都要是转换器(即,它们都要有 fit_transform() 方法)。名字可以随意起。
当你调用流水线的 fit() 方法,就会对所有转换器顺序调用 fit_transform() 方法,将每次调用的输出作为参数传递给下一个调用,一直到最后一个估计器,它只执行 fit() 方法。
流水线暴露相同的方法作为最终的估计器。在这个例子中,最后的估计器是一个 StandardScaler ,它是一个转换器,因此这个流水线有一个 transform() 方法,可以顺序对数据做所有转换(它还有一个 fit_transform 方法可以使用,就不必先调用 fit() 再进行 transform() )。
你现在就有了一个对数值的流水线,你还需要对分类值应用 LabelBinarizer :如何将这些转换写成一个流水线呢?Scikit-Learn 提供了一个类 FeatureUnion 实现这个功能。你给它一列转换器(可以是所有的转换器),当调用它的 transform() 方法,每个转换器的 transform() 会被并行执行,等待输出,然后将输出合并起来,并返回结果(当然,调用它的 fit() 方法就会调用每个转换器的 fit() )。一个完整的处理数值和类别属性的流水线如下所示:
from sklearn.pipeline import FeatureUnion num_attribs = list(housing_num) cat_attribs = ["ocean_proximity"] num_pipeline = Pipeline([ ('selector', DataFrameSelector(num_attribs)), ('imputer', Imputer(strategy="median")), ('attribs_adder', CombinedAttributesAdder()), ('std_scaler', StandardScaler()), ]) cat_pipeline = Pipeline([ ('selector', DataFrameSelector(cat_attribs)), ('label_binarizer', MyLabelBinarizer()), ]) full_pipeline = FeatureUnion(transformer_list=[ ("num_pipeline", num_pipeline), ("cat_pipeline", cat_pipeline), ])
其中需要未实现的DataFrameSelector转换器,MyLabelBinarizer转换器
from sklearn.base import BaseEstimator, TransformerMixin class DataFrameSelector(BaseEstimator, TransformerMixin): def __init__(self, attribute_names): self.attribute_names = attribute_names def fit(self, X, y=None): return self def transform(self, X): return X[self.attribute_names].values
from sklearn.base import TransformerMixin #gives fit_transform method for free class MyLabelBinarizer(TransformerMixin): def __init__(self, *args, **kwargs): self.encoder = LabelBinarizer(*args, **kwargs) def fit(self, x, y = None): self.encoder.fit(x) return self def transform(self, x, y=None): return self.encoder.transform(x)
housing_prepared = full_pipeline.fit_transform(housing)
housing_prepared
最终执行结果如下:
五、选择模型,进行训练
尝试各种模型在训练集上训练和评估
1、线性回归模型
from sklearn.linear_model import LinearRegression lin_reg = LinearRegression() lin_reg.fit(housing_prepared,housing_labels)
现在训练完毕,先有了一个可用的线性回归模型,先在一些训练集中的实例中进行验证:
#现在训练集中的实例做下验证 some_data = housing.iloc[:5] some_labels = housing_labels[:5] some_data_prepared = full_pipeline.transform(some_data) print("Predictions: ",lin_reg.predict(some_data_prepared)) print("Labels: ",list(some_labels))
可以行的通,现在使用Scikit_Learn中的mean_squared_error函数,用全部训练集来计算下这个回归模型的RMSE:
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
线性回归模型的RMSE为68628,结果并不好:大多数街区的median_housing_values位于120000到265000美元之间,因此预测误差68628美元不能让人满意。这是一个模型欠拟合训练数据的例子。当这种情况发生时,意味着特征没有提供足够度的信息来做出一个好的预测,或模型并不强大。修复欠拟合的主要方法是选择一个更强大的模型,给训练算法提供更好的特征,或去掉模型上的限制。这个模型还没有正则化,所以排除最后一个选项。可以尝试添加更多特征(比如,人口的对数值),但首先打算尝试下一个更复杂的模型,看看效果。
2、DecisionTreeRegressor,这是个强大的模型,可以发现数据中复杂的非线性关系。代码如下:
from sklearn.tree import DecisionTreeRegressor tree_reg = DecisionTreeRegressor() tree_reg.fit(housing_prepared,housing_labels)
模型训练好后,同样先用训练集进行验证
housing_predictions = tree_reg.predict(housing_prepared) tree_mse = mean_squared_error(housing_labels,housing_predictions) tree_rmse = np.sqrt(tree_mse) tree_rmse
结果发现决策树的RMSE为0,难道这个模型完美了吗??更大的可能性是这个模型发生了严重的过拟合现象。但还是不要碰测试集,直到你准备运行一个具备足够信心的模型,因此需要使用训练集的部分数据来训练,一部分来做模型验证。
使用交叉验证做更佳的评估
评估决策树模型的一种方法是用函数train_test_split来分割训练集,得到一个更小的训练集和测试集,然后用更小的训练集来训练模型,用验证集来评估。
另一种更好的方法是使用Scikit_Learn的交叉验证功能。下面的代码采用K折交叉验证(K-fold cross-validation):它随机的将训练集分成是个不同的子集,称为“折”,然后训练评估决策树模型10次,每次选一个不用的折来评估,用其它9个来训练,结果是一个包含10个评分的数组:
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)
Note:Scikit_Learn交叉验证功能期望的是效用函数(越大越好)而不是损失函数(越低越好),因此得分函数实际上与MSE相反(即负值),这就是为什么前面的代码在计算平方根之前先计算-scores。
def display_scores(scores): print("Scores:",scores) print("Mean:",scores.mean()) print("Sandard deviation:",scores.std()) display_scores(tree_rmse_scores)
现在决策树看起来就不像前面的那么好了,实际上,它看起来比线性回归模型还糟。注意到交叉验证不仅可以让你得到模型性能的评估,还能测量评估的准确性(即,它的标准差),决策树的评分大约是70944,通常波动是正负2533,如果是只有一个验证集,就得不到这些信息,但交叉验证的代价是训练了模型多次。
计算下线性回归模型的相同分数,以确保
#计算线性回归回归模型相同的分数 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)
的确,决策树模型过拟合很严重,它的性能比线性回归模型还差
3、RandomForestRegressor
随机森林是通过特征的随机子集训练许多决策树。在其它多个模型之上建立模型称为集成学习(Ensemble Learning),它是推进ML算法的一种好方法。
from sklearn.ensemble import RandomForestRegressor forest_reg = RandomForestRegressor() forest_reg.fit(housing_prepared,housing_labels)
housing_predictions = forest_reg.predict(housing_prepared) forest_mse = mean_squared_error(housing_labels,housing_predictions) forest_rmse = np.sqrt(forest_mse) forest_rmse
forest_score = cross_val_score(forest_reg,housing_prepared,y = housing_labels, scoring = 'neg_mean_squared_error',cv = 10) foreset_rmse_scores = np.sqrt(-forest_score) display_scores(foreset_rmse_scores)
至此,随机森林看起来是最好的,最有希望的,但是,训练集的评分仍然比验证集的评分低很多。解决过拟合可以通过简化模型,给模型加限制(即,规整化),或用更多的训练数据。不要在调节超参数上花费太多时间,目标是列出一个可能模型的列表(两个到五个)。
六、模型微调
假设你先有了一个列表,列表中有几个有希望的模型,现在需要对他们进行微调。微调的方法:
网格搜索
微调的一种方法是手工调整超参数,知道找到一个号的超参数组合。这样做会非常冗长,也可能没有时间探索多种组合
另一种方法使用Scikit_Learn的GridSearchCV来做这项搜索工作。需要告诉GridSearchCV要实验有哪些超参数,要实验什么值,GridSearchCV就能用交叉验证实验所有可能超参数数值的组合。例如,下面的代码搜索RandomForestRegressor超参数值的最佳组合:
from sklearn.model_selection import GridSearchCV param_grid = [ {'n_estimators': [3, 10, 30], 'max_features': [2, 4, 6, 8]}, {'bootstrap': [False], 'n_estimators': [3, 10], 'max_features': [2, 3, 4]}, ] forest_reg = RandomForestRegressor() grid_search = GridSearchCV(forest_reg, param_grid, cv=5, scoring='neg_mean_squared_error') grid_search.fit(housing_prepared, housing_labels)
param_grid告诉Scikit_Learn首先评估所有的列在第一个dict中的n_estimators和max_features的3 * 4 = 12中组合,然后尝试第二个dict中超参数的2 * 3 = 6种组合,这次会将超参数bootstrap设为False而不是True
总之,网格搜索会参数12 +6 = 18种RandomForestRegressor的超参数组合,会训练每个模型五次(因为用的是五折交叉验证)。换句话说,训练总共有18 * 5 = 90轮!K折将花费大量的时间,完成后将能获得参数的最佳组合
grid_search.best_params_
Note:因为30是n_estimators的最大值,你也应该估计更高的值,因为评估的分数可能会随n_estimators的增大而持续提升
还可以直接得到最佳的估计器:
grid_search.best_estimator_
Note:如果GridSearchCV是以(默认值)refit= True开始运行,则一旦用交叉验证找到了最佳的估计器,就会在整个训练集上重新训练,这是一个好的方法,因为用更多数据训练会提高性能。
当然,也可以直接得到评估得分:
cvres = grid_search.cv_results_ for mean_score,params in zip(cvres['mean_test_score'],cvres['params']): print(np.sqrt(-mean_score),params)
在这个例子中,通过设定超参数max_features为6,n_estimators为30,得到了最佳方案。对这个组合,RMSE的值49959,这比之前使用的默认超参数的值(52657)要稍微好点。
随机搜索
当探索相对较少的组合时,网格搜索还可以。但当超参数的搜索空间很大,最好使用RandomizedSearchCV,这个类用法与类GridSearchCV相似,但它不能尝试所有可能的组合,而是通过选择每个超参数的一个随机值的特定数量的随机组合,这种方法有两个优点:
- 如果让随机搜索运行,比如1000次,它会探索每个超参数的1000个不同的值(而不是像网格搜索那样,只搜索每个超参数的几个值)
- 可以方便的通过设定搜索次数,控制超参数搜索的计算量
集成方法
另一种微调系统的方法是将表现最好的模型组合起来。组合(集成)之后的性能通常要比单独的模型摇号(就像随机森林要比单独的决策树要好),特别是当单独模型的误差类型不同时,
分析最佳模型和他们的误差
通过分析最佳模型,常常可以获得对问题的更深的了解,比如,RandomForestRegressor可以指出每个属性对于做出正确预测的相对重要性
#randomRorestRegressor可以指出每个属性对于做出准确预测的相对重要性 feature_importances = grid_search.best_estimator_.feature_importances_ feature_importances
将重要性分数和属性名放到一块:
#将重要性分数和属性名放在一块 extra_attribs = ['rooms_per_hhold','pop_per_hhold','bedroom_per_room'] cat_one_hot_attribs = list(encoder.classes_) attributes = num_attribs + extra_attribs + cat_one_hot_attribs sorted(zip(feature_importances,attributes),reverse=True)
用测试集评估系统
调节完系统之后,你终于有了一个性能足够好的系统。现在就可以用测试集评估最后的模型。这个过程没什么特殊,就是从测试集得到预测值和标签,运行pipeline转换数据(调用transform(),而不是fit_transform()),再用测试集评估最终模型。
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) final_rmse
四、给出解决方案
然后就是项目的预上线阶段:你需要展示你的方案(重点说明学到了什么、做了什么、没做什么、做过什么假设、系统的限制是什么,等等),记录下所有事情,用漂亮的图表和容易记住的表达(比如,“收入中位数是房价最重要的预测量”)做一次精彩的展示。
五、部署、监控、维护系统
这里不做介绍