机器学习实用指南(二):一个完整的机器学习项目【上】
作者:LeonG
本文参考自:《Hands-On Machine Learning with Scikit-Learn & TensorFlow 机器学习实用指南》,感谢中文AI社区ApacheCN提供翻译。本文全部代码和数据集保存在我的github-----LeonG的github
1. 学习前提
建议掌握以下知识的基础内容:
- python编程
- numpy、matplotlib、pandas科学计算库
python是机器学习分析的编程基础,numpy、matplotlib、pandas是机器学习中常用的科学计算库,没有学过的话找个时间补一下,先接着往下看,不影响。
2.项目概述
在这一章,我们受好友王思聪的委托,临时参与万达集团的投资项目,完整的实践一次机器学习系统构建。
- 获取数据
- 分析数据规律
- 选择模型,进行训练
- 给出结论,维护系统
任务:王健林董事长与我们进行了亲切地会晤,并邀请我们参加晚宴。。。说正事,王总最近看上了大洋彼岸的加利福利亚州,打算利用加州的房地产数据,建立一个加州房价模型,用于预测该地区的房价。这个数据包含每个小区的人口、收入中位数、房价中位数等指标。
- 数据来源:加州房产普查
- 小区:每个区域的最小单位,我们以小区为单位来划分各个地区。
- 中位数:中位数不受极端值影响,可以代表该地区的大部分水平
一般来说,机器学习是一个完整的系统,我们的工作只是流水线上的一个节点。
机器学习系统流水线所以搞清楚数据的结构以及我们需要输出的形式是开始之前非常重要的准备。这一失手就是好几个亿,马虎不得。
我们跟王老板以及对接人进行详细的沟通后,得出以下结论:
- 项目用于预测地区的房价,预测结果用来评估是否进行投资开发。
- 以往的小区房价预测方法是专家用复杂的规则手工估计的,误差率为15%,需要降低误差。
- 数据中有房价中位数,所以这是一个监督学习的任务。
- 投资评估团队,也就是我们流水线的下游,需要房价数值而不是类别,所以这是一个回归任务。
OK,利用我们第一章学习的知识可以总结出,我们要做的是建立并训练一个有监督的回归预测算法模型。
3. 获取数据
首先引入必要的包numpy、matplotlib、pandas
注意:本项目主要使用Jupyter Notebook进行开发,Jupyter Notebook是一个简便易上手的python工作环境。建议没有学过的同学找个时间学习一下,源代码请使用Jupyter Notebook打开。
import numpy as np
import pandas as pd
%matplotlib inline #Jupyter Notebook的魔法变量,可以方便的展示图像
import matplotlib.pyplot as plt
为了便于测试,我对原来的代码进行了修改,你可以通过以下代码下载文件:
import os
import requests
DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml/master/"
#项目下的文件目录 datasets\housing
HOUSING_PATH = os.path.join("datasets", "housing")
#文件下载地址 https://raw.githubusercontent.com/ageron/handson-ml/master/datasets/housing/housing.csv
HOUSING_URL = DOWNLOAD_ROOT + "datasets/housing/housing.csv"
def download_data(housing_url=HOUSING_URL, housing_path=HOUSING_PATH):
os.makedirs(housing_path, exist_ok=True)
#文件地址 datasets\housing\housing.csv
csv_path = os.path.join(housing_path, "housing.csv")
r = requests.get(HOUSING_URL)
with open(csv_path,'wb') as f:
f.write(r.content)
r.close()
download_data()
代码运行之后可以看到生成了一个csv文件
如果报错,可以直接下载这个数据文件保存在你的项目目录/datasets/housing里面。
读取这个数据集并显示前五行数据:
#用pandas读取文件
housing = pd.read_csv("datasets\housing\housing.csv")
housing.head()
可以看到,该数据集总共包含10个属性,分别是:
经度、维度、房屋年龄中位数、总房间数、总卧室数、人口数、家庭数、收入中位数、房价中位数、离大海距离。
再看看这个数据集的分布情况
housing.hist(bins=50, figsize=(20,15))
plt.show()
观察这九张图,你会发现:
第二张图的房屋年龄和第五张图的房屋价值中位数在最右侧有异常数据,与数据采集团队交流之后才知道,这两个数据设置了上限,过高的值会限定到上限位置。
房屋价值中位数(房价)是数据的标签,这个上限可能会导致你的算法模型预测的房价也不会超出这个上限。这样会影响结果,建议移除这部分数据或者重新采集。
这个问题暂时放到一边,该开始建立模型了吧?
不对,我们应该先创建测试集,这一步很重要。
4.创建测试集
现在打算把数据集二八分,训练集占80%,测试集占20%。
最简单有效的方法就是随机分,我们借助Scikit-Learn工具来帮我们划分。
注意:Scikit-learn(sklearn)是机器学习中常用的第三方模块,对常用的机器学习方法进行了封装。
如果你不知道Scikit-Learn怎么使用,没关系,跟着做就是了
Scikit-Learn 提供了一些函数,可以用多种方式将数据集分割成多个子集。最简单的函数是 train_test_split。
from sklearn.model_selection import train_test_split
#test_size是分割的百分比,random_state是随机数种子
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
但是这种方法有一个很大的问题。当你的数据集很大时,这通常可行;但如果数据集不大,就会有采样偏差的风险。
比如从100个人中抽10个人出来,如果男生占60%,女生占40%,那么最好是男生抽6个,女生抽4个。
这种采样方法叫分层抽样,按照某一属性值的比例来抽取。
在这个数据集中,我们推测对房价影响比较大的是收入中位数,那么可以按照收入中位数对数据进行分组,然后从每个组中抽取对应比例的数据量,组合成为测试集。
先创建一个分组属性“income_cat”
,将收入中位数除以 1.5 向上取整,然后将所有大于5的值都改为5,代码如下:
#np.ceil向上取整
housing["income_cat"] = np.ceil(housing["median_income"] / 1.5)
#除小于5的值以外都改为5
housing["income_cat"].where(housing["income_cat"] < 5, 5.0, inplace=True)
通过这个操作我们把数据集按照“income_cat”
属性进行了分组->【1 2 3 4 5】。
现在,就可以根据收入分组进行分层采样。我们使用Scikit-Learn 的 StratifiedShuffleSplit 类:
from sklearn.model_selection import StratifiedShuffleSplit
#n_splits为切割次数,我们按照"income_cat"属性进行切分,所以只用切分一次
#test_size是测试集比例,random_state是随机数种子
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]
分类完成后得到了两个数据集 strat_train_set、strat_test_set,然后删除 “income_cat"
属性,使数据回到初始状态。
for set in (strat_train_set, strat_test_set):
set.drop(["income_cat"], axis=1, inplace=True)
根据下图,我们对比了总数据集和全随机采样、分层采样的收入分组比例。
可以看到,分层采样测试集的收入比例与总数据集几乎相同,而随机采样数据集偏差严重。
5.分析数据
现在我们把测试集放一边,仔细探索一下数据的规律。创建一个副本,以免污染训练集。
housing = strat_train_set.copy()
5.1地理数据可视化
因为存在地理信息(纬度和经度),创建一个所有小区的散点图来数据可视化是一个不错的主意:
housing.plot(kind="scatter", x="longitude", y="latitude")
王思聪表示除了能看得出是加州地图以外也看不出什么特别的规律,那我们再进行一些优化。
- 设置透明度为0.4,展示数据密度
- 圆圈的半径表示人口数量
- 根据价格高低显示不同的颜色
#s表示半径,指定为人口数/100
#c表示颜色,指定根据房价变化
#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()
现在可以看出距离海岸线越近的地方房价越高,但是北边的价格较低,说明我们不能简单的按照某一个规则来预测房价。
5.2查找关联
先来看一下相关系数,这个值表示两个属性之间的关联程度。
查看其他属性和房价(标签)的相关系数:
#corr函数计算每对属性之间的标准相关系数
corr_matrix = housing.corr()
corr_matrix["median_house_value"].sort_values(ascending=False)
相关系数的范围是 -1 到 1。当接近 1 时,意味强正相关;例如,当收入中位数增加时,房价中位数也会增加。当相关系数接近 -1 时,意味强负相关;比如纬度和房价中位数有轻微的负相关性(即,越往北,房价越可能降低)。最后,相关系数接近 0,意味没有线性相关性。
根据相关系数,我们可以进一步查看收入中位数和房价之间的关系,做出二维点图。
housing.plot(kind="scatter", x="median_income",y="median_house_value",alpha=0.1)
从这张图可以看出以下几点:
- 相关度很高,随着收入增加,房价也随之增加,数据点比较集中
- 房价有上限500000,而且在350000和450000的位置有几条水平线,这是收集数据时做的简化处理导致的,最好去掉。
有一些属性单独列出来可能没有太大的意义,像房间数和卧室数。但是将属性组合之后会出现一些关联性,比如 卧室数/房间数 比例越低,房价越高(意味着房间多而且卧室少的房子更贵)。
6.数据整理
进行了简单的数据分析之后,作为一个严谨的算法工程师(国家一级函数调用大师),还需要处理一下数据中的问题,比如空值,异常值,没必要的属性。
作为一个严谨值MAX的人,在项目中最好能将这些工作流程化,如果有新的数据要处理,可以一键调用。
首先回到我们之前的训练集(上一步为了防止污染训练集单独复制了一份,这里再将训练集复制一次)。并且把房价也就是标签单独分离。
housing = strat_train_set.drop("median_house_value", axis=1)#删除房价列后复制给新的变量
housing_labels = strat_train_set["median_house_value"].copy()
6.1数据清洗
先看一下数据中是否有缺失值:
housing.info()#查看数据的信息
数据有16512条,但是total_bedrooms
只有16354条,说明total_bedrooms
有缺失值。
虽然只有total_bedrooms
有缺失值,但是我们希望这个工具最好能把所有的属性缺失都进行替换,方便以后使用。
先把非数值类型的ocean_proximity
去掉:
#删除非数据类型的属性
housing_num = housing.drop("ocean_proximity", axis=1)
翻阅了LeonG写的机器学习指南总结之后,发现一般可以用中位数来替换缺失值,我们用Scikit-Learn提供Imputer类试试:
from sklearn.preprocessing import Imputer
#建立一个填补缺失的工具imputer,策略是使用中位数
imputer = Imputer(strategy="median")
#将imputer拟合到数据集
imputer.fit(housing_num)
#将缺失值替换为中位数
X = imputer.transform(housing_num)
#将X(这是numpy数组)转换为Pandas的Dataframe数据集类型
housing_tr = pd.DataFrame(X, columns=housing_num.columns)
6.2数据转换
再看看housing_tr的信息,发现缺失值补上了,但是缺少了一个属性ocean_proximity
,因为这个属性是文字类型的。如何处理文本和类别属性呢,最好的方法是把它转为数字。
一种比较通用的方法是将类型转为数字,比如A类型转为1,B类型转为2,但是文本之间没有大小和距离的关系,而数字之间存在大小和距离的关系,所以采用独热编码更合适。
独热编码就是将分类转为一串编码,举个例子就懂了
地区特征:["北京","上海,"深圳"](这里N=3):
北京 => [1,0,0]
上海 => [0,1,0]
深圳 => [0,0,1]
使用Scikit-Learn里的LabelBinarizer类,可以直接把文本分类转换到独热编码。
from sklearn.preprocessing import LabelBinarizer
#建立转换器encoder,参数为False时为Numpy数组,参数为True时为稀疏矩阵节约空间
encoder = LabelBinarizer(sparse_output=False)
#取出文本类型ocean_proximity
housing_cat = housing["ocean_proximity"]
#将文本转换为独热编码(numpy数组形式)
housing_cat_1hot = encoder.fit_transform(housing_cat)
housing_cat_1hot
结果:(原始数据有五类,所以输出五列)
6.3自定义工具
上面讲到的替换和转换操作是可以自定义的,自定义工具的细节了解更多点这里
这里做两个自定义工具(这两段代码直接用,能看懂就看,看不懂也没关系)
第一个是将DataFrames类型转为numpy类型
#sklearn是不能直接处理DataFrames的,那么我们需要自定义一个处理的方法将之转化为numpy类型
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 #返回的为numpy array
第二个是在数据分析时说到的属性组合,卧室数/房间数(每个房子的平均卧室数)。还有房间数/家庭数(每个家庭的平均房间数)、人口数/家庭数(每个家庭的平均人口数),这些属性都影响到房价。
# 添加一个特征组合的装换器
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
def transform(self, X, y=None):
rooms_per_household = X[:, rooms_ix] / X[:, household_ix] # X[:,3]表示的是第4列所有数据
population_per_household = X[:, population_ix] / X[:, household_ix]
if self.add_bedrooms_per_room:
bedrooms_per_room = X[:, bedrooms_ix] / X[:, rooms_ix]
# np.c_表示的是拼接数组。
return np.c_[X, rooms_per_household, population_per_household,bedrooms_per_room]
else:
return np.c_[X, rooms_per_household, population_per_household]
6.4标准化处理
数据的量度不同时,系统的性能会变差。
就像身高和体重,身高普遍在100~200之间,体重在40~80之间,但是两者对健康程度的影响可能差不多,所以我们有必要统一不同数据间的量度。
常见的两种方法是线性函数归一化和标准化:
线性函数归一化:通过减去最小值,然后再除以最大值与最小值的差值,使数据范围变成 0 到 1。
标准化:首先减去平均值,然后除以方差,使得到的分布具有单位方差。与归一化不同,标准化不会限定值到某个特定的范围。但是,标准化受到异常值的影响很小。
我们采用标准化的方式处理数据,Scikit-Learn 提供了一个转换器 StandardScaler 来进行标准化。
代码。。代码没有,用法都一样,直接看下一节
6.5工具组合
这些工具可以组装到一个函数里面,对相同的数据只用调用该函数就可以完成所有需要的处理:
根据下表从上往下进行工具的拼接:
我们使用FeatureUnion和Pipeline两个类进行拼接:
from sklearn.pipeline import FeatureUnion
from sklearn.pipeline import Pipeline #流水线组合工具
from sklearn.preprocessing import StandardScaler #标准化工具类
#其他的工具类都在上面,提示没找到类的话,先把6.3节的两个自定义工具类加上,再把下面两行注释去掉
#from sklearn.preprocessing import Imputer #替换工具类
#from sklearn.preprocessing import LabelBinarizer #独热编码工具类
num_attribs = list(housing_num)#数字处理
cat_attribs = ["ocean_proximity"]#文字处理(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', LabelBinarizer()),
#如果label_binarizer因版本原因用不了,把上一行删除用下面这个工具
#('cat_encoder', CategoricalEncoder(encoding="onehot-dense")),
])
#将两个处理工具放在一起
full_pipeline = FeatureUnion(transformer_list=[
("num_pipeline", num_pipeline),
("cat_pipeline", cat_pipeline),
])
#使用一个函数就能完成所有的数据预处理
housing_prepared = full_pipeline.fit_transform(housing)
现在看一下效果:
终于将数据整理好了,下一步进行训练。
本章的内容相较于上一章有点硬核,大家细细研究,源代码地址:LeonG的github,搞懂之后看下一章。
欢迎来我的博客留言讨论,我的博客主页:LeonG的博客,本文作者LeonG
本文参考自:《Hands-On Machine Learning with Scikit-Learn & TensorFlow机器学习实用指南》,感谢中文AI社区ApacheCN提供翻译。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
网友评论