场景
我们将一个端到端的项目(一个从开始到结束包含了所有必要步骤和组件的完整项目)案例,步骤大概有:
1.观察大局。
2.获得数据。
3.从数据探索和可视化中获得洞见。
4.机器学习算法的数据准备。
5.选择和训练模型。
6.微调模型。
7.展示解决方案。
8.启动、监控和维护系统。
开始
准备数据
我们有一个csv,里面有许多房价指标,诸如每个街区的人口数量、收入中位数、房价中位数等。街区是美国人口普查局发布样本数据的最小地理单位(一个街区通常人口数为600~3000人)。这里,我们将其简称为“区域”。模型需要从这个数据中学习,从而能够根据所有其他指标,预测任意区域的房价中位数。
import pandas as pd
import os
def load_housing_data(housing_path=""):
csv_path = os.path.join(housing_path, "housing.csv")
return pd.read_csv(csv_path)
housing = load_housing_data()
通过info()方法可以快速获取数据集的简单描述,特别是总行数、每个属性的类型和非空值的数量
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 20640 entries, 0 to 20639
Data columns (total 10 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 longitude 20640 non-null float64
1 latitude 20640 non-null float64
2 housing_median_age 20640 non-null float64
3 total_rooms 20640 non-null float64
4 total_bedrooms 20433 non-null float64
5 population 20640 non-null float64
6 households 20640 non-null float64
7 median_income 20640 non-null float64
8 median_house_value 20640 non-null float64
9 ocean_proximity 20640 non-null object
dtypes: float64(9), object(1)
memory usage: 1.6+ MB
total_bed这个属性只有20433个非空值,这意味着有207个区域缺失这个特征。所有属性的字段都是数字,除了ocean_proximity。它可能是一个分类系数,看一下共有几种
<1H OCEAN 9136
INLAND 6551
NEAR OCEAN 2658
NEAR BAY 2290
ISLAND 5
过describe()方法可以显示数值属性的摘要
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
count、mean、min以及max行的意思很清楚。需要注意的是,这里的空值会被忽略(因此本例中,total_bedrooms的count是20433而不是20640)。std行显示的是标准差(用来测量数值的离散程度)。25%、50%和75%行显示相应的百分位数:百分位数表示一组观测值中给定百分比的观测值都低于该值。例如,对于housing_median_age的值,25%的区域低于18,50%的区域低于29,以及75%的区域低于37。这些通常被称为:百分之二十五分位数(或者第一四分位数)、中位数以及百分之七十五分位数(或者第三四分位数)。
我们也可以快速的绘制数据的直方图查看数据的情况:
import matplotlib.pyplot as plt
housing.hist(bins=50, figsize=(20,15))
plt.show()
首先
收入中位数的单位一定不是美元,数据已经按比例缩小
其次
.房龄中位数和房价中位数也被设定了上限。而后者正是你的目标属性(标签),这可是个大问题。因为你的机器学习算法很可能会学习到价格永远不会超过这个限制。所以这一点需要确认。解决方案:
a.对那些标签值被设置了上限的地区,重新收集标签值。
b.或是将这些地区的数据从训练集中移除(包括从测试集中移除,因为如果预测值超过500000,系统不应被评估为不良)。
3.这些属性值被缩放的程度各不相同。
4.许多直方图都表现出重尾(一个分布如果在其尾部的概率质量比标准的正态分布要大,那么这个分布就被认为是重尾的)
创建测试集
理论上,创建测试集非常简单:只需要随机选择一些实例,通常是数据集的20%,然后将它们放在一边:但是如果你再运行一遍,它又会产生一个不同的数据集!这样下去,将会看到整个完整的数据集,而这正是创建测试集时需要避免的。解决方案之一是在第一次运行程序后即保存测试集,随后的运行只是加载它而已。另一种方法是在调用np.random.permutation()之前设置一个随机数生成器的种子,从而让它始终生成相同的随机索引。常见的解决办法是每个实例都使用一个标识符(identifier)来决定是否进入测试集(假定每个实例都有一个唯一且不变的标识符)。举例来说,你可以计算每个实例标识符的hash值,只取hash的最后一个字节,如果该值小于等于51(约256的20%),则将该实例放入测试集。这样可以确保测试集在多个运行里都是一致的,即便更新数据集仍然一致。新实例的20%将被放入新的测试集,而之前训练集中的实例也不会被放入新测试集。
Scikit-Learn提供了一些函数,可以通过多种方式将数据集分成多个子集。最简单的函数是train_test_split,它与前面定义的函数split_train_test几乎相同,除了几个额外特征。首先,它也有
random_state参数,让你可以像之前提到过的那样设置随机生成器种子;其次,你可以把行数相同的多个数据集一次性发送给它,它会根据相同的索引将其拆分.
from sklearn.model_selection import train_test_split
data_set = load_housing_data("")
print(train_test_split(data_set, test_size=0.2, random_state=42))
当你为其random_state参数设置一个固定的值时,可以保证每次分割得到的测试集和训练集是一样的。random_state参数控制了数据分割时的随机性,为其设置相同的值确保了每次运行代码时随机数生成器的种子相同,从而使得结果可复现。无论你运行多少次这段代码,只要data_set内容没有变化,你每次得到的测试集和训练集的分割都会是相同的。
如果你的数据集足够大,这可能的确是个不错的方案。如果不是,则有可能会导致明显的抽样偏差。随机分割可能不代表总体:尽管随机分割旨在从总体数据集中公平地选取训练集和测试集,但在某些情况下,特别是在数据量较小或分布不均匀的情况下,随机选择的样本可能无法充分代表总体数据的特征。这可能会导致模型的训练效果不佳或评估结果不准确。
类别不平衡:在处理分类问题时,如果数据集中某些类别的样本数量远少于其他类别,纯随机的数据分割可能导致训练集和测试集中的类别分布不平衡。这种不平衡可能会对模型的训练和评估产生负面影响。
要预测房价平均值,收入中位数是一个非常重要的属性。确保在收入属性上,测试集能够代表整个数据集中各种不同类型的收入。查看直方图
data_set["income_cat"] = pd.cut(data_set["median_income"],
bins=[0., 1.5, 3.0, 4.5, 6., np.inf],
labels=[1, 2, 3, 4, 5])
data_set["income_cat"].hist()
直方图如下:
大多数收入中值是集中在 1.5 至 6 左右(即 15,000 美元至 60,000 美元),但一些收入中位数远远超过 6。已准备好根据收入类别进行分层抽样。可以使用 Scikit-Learn 的 StratifiedShuffleSplit 类
StratifiedShuffleSplit
]分层采样:StratifiedShuffleSplit 是一种分层采样方法,它确保在每次划分时,训练集和测试集中各类别的比例与完整数据集中的比例相同。这种方法对于处理不平衡数据集尤其重要,因为它可以防止在训练集或测试集中某一类别的样本数量不成比例。
多次划分:StratifiedShuffleSplit 提供了进行多次随机划分的能力,生成多个训练/测试集对。这在交叉验证中非常有用,可以提供多个不同的训练/测试数据集,以便进行更全面的模型评估。
使用之:
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(data_set,data_set["income_cat"]):
strat_train_set = data_set.loc[train_index]
strat_test_set = data_set.loc[test_index]
每个标签占比:
strat_test_set["income_cat"].value_counts() / len(strat_test_set)
income_cat
3 0.350533
2 0.318798
4 0.176357
5 0.114341
1 0.039971
Name: count, dtype: float64
恢复数据集并且复制副本
for set_ in (strat_train_set, strat_test_set):
set_.drop("income_cat", axis=1, inplace=True)
housing = strat_train_set.copy()
将地理数据可视化
housing.plot(kind="scatter", x="longitude", y="latitude")
经纬度的散点图难看到任何东西,设置散点图中点的透明度为 0.1。alpha 参数的取值范围是 [0,1],其中0表示完全透明,1 表示完全不透明。
housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1)
这可以一目了然,哪些地区数据点较密集,哪些地区较稀疏。
现在我们看看房价,每个圆的半径大小代表了每个地区的人口数量(选项s),颜色代表价格(选项c)。我们使用一个名叫jet的预定义颜色表(选项cmap)来进行可视化,颜色范围从蓝(低)到红(高):
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,)
plt.legend()
人口密度:点的大小表示人口规模,大点表示人口密集,小点表示人口稀少。
房价分布:颜色深浅表示房价的高低,从蓝色的低价值过渡到红色的高价值。
地理集群:通过颜色和点的大小可以看出,人口密集区域和房价高的地区往往是集中的。比如,在大城市或海岸线附近,我们可以看到更大的点以及更暖色调的颜色,表明那里的房价较高和/或人口较多。
趋势:沿海岸线的地区,特别是大都市区如旧金山湾区,房价显得尤为高。而内陆地区的房价相对较低。
寻找相关性
由于数据集不大,可以使用corr()方法轻松计算出每对属性之间的标准相关系数(也称为皮尔逊相关系数):
numeric_housing = housing.select_dtypes(include=[np.number]) # 只保留数值型列
corr_matrix = numeric_housing.corr()
corr_matrix["median_house_value"].sort_values(ascending=False)
结果是:
median_house_value 1.000000
median_income 0.687151
total_rooms 0.135140
housing_median_age 0.114146
households 0.064590
total_bedrooms 0.047781
population -0.026882
longitude -0.047466
latitude -0.142673
Name: median_house_value, dtype: float64
当收入中位数上升时,房价中位数也趋于上升。当系数接近于-1,则表示有强烈的负相关;注意看纬度和房价中位数之间呈现出轻微的负相关(也就是说,越往北走,房价倾向于下降)。最后,系数靠近0则说明二者之间没有线性相关性。相关系数仅测量线性相关性(“如果x上升,则y上升/下降”)。所以它有可能彻底遗漏非线性相关性(例如“如果x接近于零,则y会上升”)。注意上图最下面一排的图像,它们的相关性系数都是0,但是显然我们可以看出横轴和纵轴之间的关系并不是彼此完全独立的:这是非线性关系的例子。
from pandas.plotting import scatter_matrix
attributes = ["median_house_value", "median_income", "total_rooms", "housing_median_age"]
scatter_matrix(housing[attributes], figsize=(12, 8))
得出图像
每个图展示了四个属性之间的双变量关系:“median_house_value”, “median_income”, “total_rooms”, 和 “housing_median_age”。图中对角线上的图是各属性自己的直方图,它展示了单个变量的分布。其他图是不同变量对的散点图,展示了变量间的关系。根据这个矩阵,我们可以得出以下结论:
median_income 和 median_house_value:
这对属性似乎有较强的正相关性,即收入中位数较高的地区房屋中位价值也较高。这是可以预期的,因为收入通常是决定房价的一个重要因素。
total_rooms 和 median_house_value:
整体上,房间总数似乎与房屋价值有一定的正相关性,但不如收入中位数那么强。这可能是因为房间总数并不能直接反映出房屋的质量或大小,因为它没有考虑房屋数量或每户的房间数。
housing_median_age 和 median_house_value:
房屋年龄的中位数似乎没有显示出与房屋中位价值的强烈线性关系。尽管如此,某些年龄较高的房屋区域可能会有较高的价值,这可能与其历史价值或维护状况有关。
试验不同属性的组合
在准备给机器学习算法输入数据之前,你要做的最后一件事应该是尝试各种属性的组合。比如,如果你不知道一个地区有多少个家庭,那么知道一个地区的“房间总数”也没什么用。你真正想要知道的是一个家庭的房间数量。同样地,单看“卧室总数”这个属性本身,也没什么意义,你可能是想拿它和“房间总数”来对比,或者拿来同“每个家庭的人口数”这个属性结合也似乎挺有意思。我们来试着创建这些新属性:
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"]
numeric_housing = housing.select_dtypes(include=[np.number]) # 只保留数值型列
corr_matrix = numeric_housing.corr()
corr_matrix["median_house_value"].sort_values(ascending=False)
median_house_value 1.000000
median_income 0.687151
rooms_per_household 0.146255
total_rooms 0.135140
housing_median_age 0.114146
households 0.064590
total_bedrooms 0.047781
population_per_household -0.021991
population -0.026882
longitude -0.047466
latitude -0.142673
bedrooms_per_room -0.259952
Name: median_house_value, dtype: float64
新的属性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"]) # option 1
housing.drop("total_bedrooms", axis=1) # option 2
median = housing["total_bedrooms"].median() # option 3
housing["total_bedrooms"].fillna(median, inplace=True)
如果选择选项 3,应该计算训练集的中值并用它来填充训练集中的缺失值。不要忘记保存计算出的中值。您稍后将需要它来替换丢失的。当想评估系统时,以及一旦系统上线以替换新数据中的缺失值
Scikit-Learn提供了一个非常容易上手的教程来处理缺失值:imputer。使用方法如下,首先,需要创建一个imputer实例,指定要用属性的中位数值替换该属性的缺失值:
from sklearn.impute import SimpleImputer
import numpy as np
import pandas as pd
# 首先,创建一个 SimpleImputer 实例,指定您希望用于替换缺失值的策略,比如 'mean', 'median', 或 'most_frequent'。
imputer = SimpleImputer(strategy="median")
# 如果数据集中有非数值型数据列,您需要先删除这些列,因为中位数只能在数值型数据上计算。
housing_num = housing.select_dtypes(include=[np.number]) # 仅选择数值列
# 计算每列的中位数,并将结果存储在 imputer 对象中。
imputer.fit(housing_num)
# 查看每列的中位数
print(imputer.statistics_)
# 使用中位数来填充数据集中的缺失值
X = imputer.transform(housing_num)
# 将返回的 NumPy 数组转换回 pandas DataFrame
housing_tr = pd.DataFrame(X, columns=housing_num.columns)
print(housing_tr)
Scikit-Learn的设计
Scikit-Learn的API设计得非常好。其主要的设计原则是:
·一致性。所有对象共享一个简单一致的界面
·估算器。能够根据数据集对某些参数进行估算的任意对象都可以被称为估算器(例如,imputer就是一个估算器)。估算由fit()方法执行,它只需要一个数据集作为参数(或者两个——对于监督式学习算法,第二个数据集包含标签)。引导估算过程的任何其他参数都算作是超参数,它必须被设置为一个实例变量(一般是构造函数参数)
·转换器。有些估算器(例如imputer)也可以转换数据集,这些被称为转换器。同样,API也非常简单:由transform()方法和作为参数的待转换数据集一起执行转换,返回的结果就是转换后的数据集。这种转换的过程通常依赖于学习的参数,比如本例中的imputer。所有的转换器都可以使用一个很方便的方法,即fit_transform(),相当于先调用fit()然后再调用transform()(但是fit_transform()有时是被优化过的,所以运行得要更快一些)。
·预测器。最后,还有些估算器能够基于一个给定的数据集进行预测,这被称为预测器。
·检查。所有估算器的超参数都可以通过公共实例变量(例如,imputer.strategy)直接访问,并且所有估算器的学习参数也可以通过有下划线后缀的公共实例变量来访问(例如,imputer.strategy_)。
处理文本和分类属性
之前我们排除了分类属性ocean_proximity,因为它是一个文本属性,我们无法计算它的中位数值。大部分的机器学习算法都更易于跟数字打交道,所以我们先将这些文本标签转化为数字。Scikit-Learn为这类任务提供了一个转换器LabelEncoder:
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, 4, 1, ..., 0, 0, 1])
现在好了:我们可以将这套数值数据应用于任意机器学习算法。可以使用classes_属性来查看这个编码器已学习的映射(“<1HOCEAN”对应为0,“INLAND”对应为1,等等
查看所有分类
print(encoder.classes_)
结果:
['<1H OCEAN' 'INLAND' 'ISLAND' 'NEAR BAY' 'NEAR OCEAN']
直接使用数字标签进行编码可能会误导模型认为两个类别之间存在某种数值上的距离关系。因为机器学习算法会假设数值上相近的两个值在含义上也是相近的,但实际上地理位置的分类并没有这样的顺序关系,例如分类 0 和 4 可能比分类 0 和 1 更为相似。如果我们直接将这些类别编码为数字(例如,将 <1H OCEAN 编码为 0,INLAND 编码为 1,以此类推),模型可能会错误地解释这些数字。在这种编码方式下,模型可能会认为 INLAND(1)与 <1H OCEAN(0)的相似度高于 INLAND(1)与 ISLAND(4)的相似度,因为数值上 1 更接近于 0 而不是 4。然而,在实际情况中,这些类别是没有内在数值距离的,它们应该被视为彼此独立的类别。
一种解决方案是使用独热编码(One-Hot Encoding)。对于 ocean_proximity 特征的每个可能的类别,我们创建一个新的二进制特征。在对应于某个数据点的类别的特征中,我们设置值为 1;在所有其他新特征中,我们设置值为 0。
对于上面的例子,我们将创建 5 个新的特征:
ocean_proximity_<1H OCEAN
ocean_proximity_INLAND
ocean_proximity_NEAR OCEAN
ocean_proximity_NEAR BAY
ocean_proximity_ISLAND
如果一个房产的 ocean_proximity 是 INLAND,那么在 ocean_proximity_INLAND 特征中,我们将其值设置为 1,而在其他四个特征中,我们将值设置为 0。
Scikit-Learn提供了一个OneHotEncoder编码器,可以将整数分类值转换为独热向量。我们用它来将类别编码为独热向量。
from sklearn.preprocessing import OneHotEncoder
cat_encoder = OneHotEncoder()
housing_cat_1hot = cat_encoder.fit_transform(housing_cat_encoded.reshape(-1, 1))
housing_cat_1hot
结果是:
<16512x5 sparse matrix of type '<class 'numpy.float64'>'
with 16512 stored elements in Compressed Sparse Row format>
注意到这里的输出是一个SciPy稀疏矩阵,而不是一个NumPy数组。当你有成千上万种类别的分类属性时,这个函数会非常有用。因为当独热编码完成之后,我们会得到一个几千列的矩阵,并且全是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, 0, 0, 0, 1],
[0, 1, 0, 0, 0],
...,
[1, 0, 0, 0, 0],
[1, 0, 0, 0, 0],
[0, 1, 0, 0, 0]])
encoder = LabelBinarizer(sparse_output=True)可以返回稀疏矩阵
区别
OneHotEncoder 和 LabelBinarizer 都是 scikit-learn 中用于处理分类特征的编码器,但它们在处理数据和返回结果方面有一些差异:
OneHotEncoder 旨在将分类特征,尤其是非数值类别特征,转换成一热(One-Hot)编码格式。
默认情况下,OneHotEncoder 返回一个稀疏矩阵。这在处理包含大量类别的特征时非常有效,因为稀疏矩阵可以节省大量内存。OneHotEncoder 在较新版本的 scikit-learn 中得到了改进,现在可以直接处理字符串类别数据,无需先将其转换为整数。
LabelBinarizer 主要用于将目标标签(y)转换成二进制形式。虽然它也可以用于处理特征(X),但更常见的用途是为二分类或多标签分类问题准备目标变量。
默认情况下,LabelBinarizer 返回一个密集数组(numpy array),即使在处理具有多个类别的特征时也是如此。LabelBinarizer 更适用于处理目标变量,而不是作为一个通用的特征编码器。因此,当用于特征编码时,它可能不像 OneHotEncoder 那样灵活。
主要区别
返回类型:OneHotEncoder 默认返回稀疏矩阵,而 LabelBinarizer 返回密集数组。
使用场景:OneHotEncoder 设计用于处理特征,支持直接处理字符串类别数据;LabelBinarizer 主要用于处理二分类或多标签的目标变量。
处理字符串类别:在较新版本的 scikit-learn 中,OneHotEncoder 可以直接处理包含字符串的列,而 LabelBinarizer 则在处理特征时可能需要额外的步骤来先将字符串转换成数值形式(尽管它可以直接处理字符串目标标签)。
自定义转换器
自定义转换器在机器学习数据预处理和特征工程中扮演着重要的角色。它们提供了一种灵活的方法来扩展 Scikit-Learn 的预处理功能,能够整合自定义的数据清洗步骤、属性添加、以及任何其他计算或转换操作,进而适应特定数据科学任务。
如之前说过的:
假设我们正在处理一个房价预测模型,我们的数据集包含以下几个特征:
total_rooms:区域内的总房间数
total_bedrooms:区域内的总卧室数
population:区域内的人口数
households:区域内的家庭户数
我们注意到,原始特征可能不足以给出足够的信息来进行准确的房价预测。比如,知道一个区域的总房间数对于预测房价来说信息不够充分,我们可能还想知道每户的平均房间数或者卧室占房间的比例等更具代表性的信息。
基于上述理由,我们决定添加以下几个组合特征:
每户的平均房间数(rooms_per_household)
每户的平均卧室数(bedrooms_per_room)
每户的平均人口数(population_per_household)
为了实现这个目标,我们将创建一个自定义的转换器 CombinedAttributesAdder:
from sklearn.base import BaseEstimator, TransformerMixin
import numpy as np
class CombinedAttributesAdder(BaseEstimator, TransformerMixin):
def fit(self, X, y=None):
return self # 不需要进一步计算
def transform(self, X, y=None):
rooms_per_household = X[:, rooms_ix] / X[:, households_ix]
bedrooms_per_room = X[:, bedrooms_ix] / X[:, rooms_ix]
population_per_household = X[:, population_ix] / X[:, households_ix]
# 将新计算的特征附加到原始数据矩阵上
return np.c_[X, rooms_per_household, bedrooms_per_room, population_per_household]
rooms_ix, bedrooms_ix, population_ix, households_ix = 3, 4, 5, 6
使用:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
num_pipeline = Pipeline([
('attribs_adder', CombinedAttributesAdder()),
('std_scaler', StandardScaler()),
])
housing_prepared = num_pipeline.fit_transform(housing_num)
在这个管道中,CombinedAttributesAdder 首先被应用于数值数据,以添加新的组合特征,然后使用 StandardScaler 进行特征缩放。
特征缩放
最重要也最需要应用到数据上的转换器,就是特征缩放。如果输入的数值属性具有非常大的比例差异,往往导致机器学习算法的性能表现不佳,当然也有极少数特例。案例中的房屋数据就是这样:房间总数的范围从6到39320,而收入中位数的范围是0到15。注意,目标值通常不需要缩放。
同比例缩放所有属性,常用的两种方法是:最小-最大缩放和标准化。
最小-最大缩放(又叫作归一化)很简单:将值重新缩放使其最终范围归于0到1之间。实现方法是将值减去最小值并除以最大值和最小值的差。对此,Scikit-Learn提供了一个名为MinMaxScaler的转换器。如果出于某种原因,你希望范围不是0~1,你可以通过调整超参数feature_range进行更改。
标准化则完全不一样:首先减去平均值(所以标准化值的均值总是零),然后除以方差,从而使得结果的分布具备单位方差。不同于最小-最大缩放的是,标准化不将值绑定到特定范围,对某些算法而言,这可能是个问题(例如,神经网络期望的输入值范围通常是0到1)。但是标准化的方法受异常的影响更小。例如,假设某个地区的平均收入等于100(错误数据)。最小-最大缩放会将所有其他值从0~15降到0~0.15,而标准化则不会受到很大影响。Scikit-Learn提供了一个标准化的转换器StandadScaler。
重要的是,跟所有转换一样,缩放器仅用来拟合训练集,而不是完整的数据集(包括测试集)。只有这样,才能使用它们来进行转换。
转换流水线
许多数据转换的步骤需要以正确的顺序来执行。而Scikit-Learn正好提供了Pipeline来支持这样的转换。
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import FeatureUnion
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from machine_learning.combined_attributes_adder import CombinedAttributesAdder
# 预处理
# 创建流水线
housing_list = pd.read_csv("housing.csv")
print(housing_list)
# 预处理
# 创建流水线
num_pipeline = Pipeline([
('imputer', SimpleImputer(strategy="median")), # 填充缺失值
('attribs_adder', CombinedAttributesAdder()), # 添加自定义特征
('std_scaler', StandardScaler()), # 数据标准化
])
housing_num = housing_list.drop("ocean_proximity", axis=1)
ocean_proximity = housing_list['ocean_proximity']
housing_num_tr = num_pipeline.fit_transform(housing_num)
class_pipeline = Pipeline([
('one_hot_encoder', OneHotEncoder())
])
# 将数值和分类处理流水线合并
column_pipeline = ColumnTransformer([
("num_pipeline", num_pipeline,list(housing_list)),
("cat_pipeline", class_pipeline,ocean_proximity),
])
housing_prepared = column_pipeline.fit_transform(housing_list)
tip:
ColumnTransformer 和 FeatureUnion 都用于构建机器学习流水线中的特征处理部分,但它们的用途和工作方式有所不同:
ColumnTransformer:用于同时对数据集中的不同列应用不同的转换。这对于同时处理数值和分类数据非常有用,因为它可以针对每种类型的数据指定不同的预处理步骤。例如,对数值数据使用标准化,而对分类数据使用独热编码。
FeatureUnion:用于并行地应用多个转换器,并将它们的输出合并成一个更大的特征集。这适用于当你想要结合多种特征提取方法的特征时,如同时使用文本向量化和统计特征。
简而言之,ColumnTransformer按列区分对待数据,针对不同类型的数据列使用不同的处理,而FeatureUnion则是并行地运行多个转换器,并将它们的输出合并起来。
Pipeline接受一个由步骤名称和估计器(或变换器)组成的列表,用于定义数据处理的序列。在这个序列中,最后一个估计器可以是一个模型(比如线性回归模型),而序列中的其他估计器都必须是变换器,即具有fit_transform()方法的对象。每个步骤的名称是自定义的,但需要保证唯一性,并且不能包含双下划线__,这些名称后续可以用于超参数调优。一个完整的处理数值和分类属性的流水线可能如下所示:
运行整条流水线:
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import StratifiedShuffleSplit
from machine_learning.combined_attributes_adder import CombinedAttributesAdder
# 加载数据
housing_list = pd.read_csv("housing.csv")
# 使用分层抽样方法进行数据分割,确保测试集能够代表数据的整体分布
split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42)
for train_index, test_index in split.split(housing_list, housing_list["ocean_proximity"]):
strat_train_set = housing_list.loc[train_index]
strat_test_set = housing_list.loc[test_index]
# 准备数据
housing = strat_train_set.drop("median_house_value", axis=1)
housing_labels = strat_train_set["median_house_value"].copy()
# 数字数据处理管道
num_pipeline = Pipeline([
('imputer', SimpleImputer(strategy="median")), # 中值填充缺失数据
('attribs_adder', CombinedAttributesAdder()), # 添加自定义特征
('std_scaler', StandardScaler()), # 数据标准化处理
])
housing_num = housing.drop("ocean_proximity", axis=1)
# 分类数据处理管道
cat_pipeline = Pipeline([
('one_hot_encoder', OneHotEncoder()) # 独热编码处理分类数据
])
# 组合不同的处理管道
column_pipeline = ColumnTransformer([
("num_pipeline", num_pipeline, list(housing_num.columns)),
("cat_pipeline", cat_pipeline, ["ocean_proximity"]),
])
# 转换数据
housing_prepared = column_pipeline.fit_transform(housing)
# 训练线性回归模型
lin_reg = LinearRegression()
lin_reg.fit(housing_prepared, housing_labels)
测试集的使用和结果评估
和测试集比较
some_data = housing.iloc[:5]
some_labels = housing_labels.iloc[:5]
some_data_prepared = column_pipeline.transform(some_data)
print("Predictions:\t", lin_reg.predict(some_data_prepared))
print("Labels:\t\t", list(some_labels))
housing_test = strat_test_set.drop("median_house_value", axis=1)
housing_test_prepared = column_pipeline.transform(housing_test)
test_predictions = lin_reg.predict(housing_test_prepared)
test_mse = mean_squared_error(strat_test_set["median_house_value"], test_predictions)
test_rmse = np.sqrt(test_mse)
print("Test RMSE: ", test_rmse)
结果
Predictions: [163365.47485065 156823.62208331 235699.27887731 117177.88558729
273801.89883058]
Labels: [96500.0, 166700.0, 242000.0, 150000.0, 243600.0]
Test RMSE: 68237.72687841511
测误差达到68237美元只能算是差强人意。这就是一个典型的模型对训练数据拟合不足的案例。
我们来训练一个DecisionTreeRegressor。这是一个非常强大的模型,它能够从数据中找到复杂的非线性关系(决策树)
tree_reg = DecisionTreeRegressor()
tree_reg.fit(housing_prepared, housing_labels)
你会发现结果
Test RMSE: 69490.84408972049
结果似乎更拉胯了。
我们再来试试最后一个模型:RandomForestRegressor。
微调模型
一般使用随机法和网格法(之前有介绍过)
启动、监控和维护系统
系统获准启动了!需要为生产环境做好准备,特别是将生产数据源接入系统,并编写测试。还需要编写监控代码,以定期检查系统的实时性能,同时在性能下降时触发警报。重要的是,这里需要捕捉的不仅只是突然的系统崩溃,系统性能的退化也值得关注。这个问题很常见,因为随着时间的推移,数据不断进化,模型会渐渐“腐坏”,除非定期使用新数据训练模型。评估系统性能,需要对系统的预测结果进行抽样并评估。通常这一步需要人工分析。分析师可能是领域专家,或者是众包平台的工作人员(例如Amazon Mechanical Turk或CrowdFlower)。不管怎么说,都需要将人工评估的流水线接入你的系统。还需要评估输入系统的数据的质量。质量较差的数据(比如用来发送随机值的传感器故障,或者是其他团队的输出变得过时)会导致性能略微下降,但是要降到触发警报还需要一段时间。所以如果监控系统的输入,就可以更快地捕捉到这个信号。对于在线学习来说,监控系统输入尤其重要。一般来说,最后要使用新鲜数据定期训练你的模型。这个过程要尽可能自动化。如若不然,很有可能每6个月(最多)需要更新一次模型,久而久之,系统性能也会发生严重波动。如果是在线学习系统,应当定期保存系统状态的快速备份,以便于轻松回滚到之前的工作状态。
结束
从数据准备到模型评估的过程几乎都走了一遍,后续会进入更加深入的学习。Updating…