机器学习项目从零到一:加州房价预测模型(PART 2)
一、食用指南
本系列文章将实现一个端到端的机器学习项目案例。假设你是一个A公司最近新雇用的数据科学家,以下是你将会经历的主要步骤:
- 观察大局。
- 获得数据。
- 从数据探索和可视化中获得洞见。
- 机器学习算法的数据准备。
- 选择并训练模型。
- 微调模型。
- 展示解决方案。
- 启动、监控和维护系统。
项目案例纯属虚构,目的仅仅是为了说明机器学习项目的主要步骤。
二、从数据探索和可视化中获得洞见
书接上文:机器学习项目从零到一:加州房价预测模型(PART 1),我们还只是在快速浏览数据,从而对手头上正在处理
的数据类型形成一个大致的了解,本阶段的目标是再深入一点。
首先,把测试集放在一边,我们只需要使用训练集。此外,如果训练集非常庞大,我们可以抽样一个探索集,这样后面的操作更简单快捷一些。不过我们这个案例的数据集非常小,完全可以直接在整个训练集上操作。让我们先创建一个副本,这样可以随便尝试而不改变原训练集数据:
# 拷贝一份分层抽样的训练集副本
housing = strat_train_set.copy()
1、将地理数据可视化
由于存在地理位置信息(经度和纬度),因此可以建立一个各区域的分布图以便于可视化数据:
housing.plot(kind="scatter", x="longitude", y="latitude")
没错,这除了看起来跟加州一样以外,很难再看出任何其他的模式:
将alpha
选项设置为0.1
,可以更清楚地看出高密度数据点的位置:
housing.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1)
现在好多了:可以清楚地看到高密度区域,也就是湾区、洛杉矶和圣地亚哥附近,同时在中央山谷有一条相当高密度的长线,特别是萨克拉门托和弗雷斯诺附近。
!我们的大脑非常善于从图片中发现模式,但是我们需要充分使用可视化参数才能让这些模式凸显出来。
现在,再来看看房价数据可视化:
import matplotlib.pyplot as plt
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,sharex=False)
plt.legend()
图中每个圆的半径大小代表了每个区域的人口数量(选项s),颜色代表价格(选项c)。我们使用一个名叫jet
的预定义颜色表(选项cmap)来进行可视化,颜色范围从蓝(低)到红(高)。
上图表明了房价与地理位置(例如靠近海)和人口密度息息相关。这一点在生活中很常见,例如海景房的价格往往会比较高。一个通常很有用的方法是使用聚类算法来检测主集群,然后再为各个集群中心添加一个新的衡量邻近距离的特征,海洋邻近度可能就是一个很有用的属性,不过在北加州,沿海地区的房价并不是太高,所以这个简单的规则也不是万能的。
2、寻找相关性
皮尔逊r
皮尔逊相关系数(Pearson’s r),也称为皮尔逊积差相关系数,是统计学中用于度量两个变量X和Y之间线性相关程度的一种方法。皮尔逊r的值介于-1到+1之间,其中:
- +1表示完全正相关,即当一个变量增加时,另一个变量也会相应地按比例增加。
- 0表示没有线性相关,即两个变量的变化彼此无关。
- -1表示完全负相关,即一个变量增加时,另一个变量会相应地减少。
由于数据集不大,我们可以使用corr
方法轻松计算出每对属性之间的标准相关系数(皮尔逊r):
# ocean_proximity 属性值是字符串不可用于计算,因此剔除
# drop 会创建一个数据副本,不会影响原数据集
housing_copy2 = housing.drop("ocean_proximity", axis=1)
corr_matrix = housing_copy2.corr()
corr_matrix["median_house_value"].sort_values(ascending=False)
median_house_value 1.000000
median_income 0.691513
total_rooms 0.140831
housing_median_age 0.105154
households 0.071182
total_bedrooms 0.055079
population -0.018445
longitude -0.042265
latitude -0.147418
Name: median_house_value, dtype: float64
相关系数的范围从-1变化到1。越接近1,表示有越强的正相关,例如,当收入中位数上升时,房价中位数也趋于上升。当系数接近于-1时,表示有较强的负相关,我们可以看到纬度和房价中位数之间呈现出轻微的负相关(也就是说,越往北走,房价倾向于下降)。最后,系数靠近0则说明二者之间没有线性相关性。
!相关系数仅测量线性相关性(“如果x上升,则y上升/下降”)。所以它有可能彻底遗漏非线性相关性(例如“如果x接近于0,则y会上升”)。
scatter_matrix
还有一种方法可以检测属性之间的相关性,就是使用 pandas
的 scatter_matrix
函数,它会绘制出每个数值属性相对于其他数值属性的相关性,这里我们仅关注那些与房价中位数属性最相关的,可算作是最有潜力的属性:
# from pandas.tools.plotting import scatter_matrix # For older versions of Pandas
from pandas.plotting import scatter_matrixattributes = ["median_house_value", "median_income", "total_rooms","housing_median_age"]
scatter_matrix(housing[attributes], figsize=(12, 8))
如果 pandas
绘制每个变量对自身的图像,那么主对角线(从左上到右下)将全都是直线,这样毫无意义。所以取而代之的方法是,pandas
在这几个图中显示了每个属性的直方图(还有其他选项可选,详情请参考 pandas
文档)
最有潜力能够预测房价中位数的属性是收入中位数,所以我们放大来看看其相关性的散点图:
housing.plot(kind="scatter", x="median_income", y="median_house_value", alpha=0.1)
plt.axis([0, 16, 0, 550000])
上图说明了几个问题:
- 房价中位数与收入中位数的相关性确实很强,我们可以清楚地看到上升的趋势,并且点也不是太分散
- 前面我们提到过50万美元的价格上限在图中是一条清晰的水平线
- 不明显的直线:45万美元附近有一条水平线,35万美元附近也有一条,28万美元附近似乎隐约也有一条,再往下可能还有一些
3、试验不同属性的组合
在准备给机器学习算法输入数据之前,我们要做的最后一件事应该是尝试各种属性的组合。例如,如果我们不知道一个区域有多少个家庭,那么知道一个区域的“房间总数”也没什么用,而我们真正想要知道的是一个家庭的房间数量。
我们来试着创建这些新属性:
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.drop("ocean_proximity", axis=1).corr()
corr_matrix["median_house_value"].sort_values(ascending=False)
median_house_value 1.000000
median_income 0.691513
rooms_per_household 0.146429
total_rooms 0.140831
housing_median_age 0.105154
households 0.071182
total_bedrooms 0.055079
population -0.018445
population_per_household -0.022362
longitude -0.042265
latitude -0.147418
bedrooms_per_room -0.260229
Name: median_house_value, dtype: float64
从皮尔逊r结果来看,新属性 bedrooms_per_room
比“房间总数”或“卧室总数”与房价中位数的相关性都要高得多,显然,卧室/房间
比例更低的房屋往往价格更贵;同样,“每个家庭的房间数量”
也比“房间总数”更具信息量:房屋越大,价格越贵
。
我们可以在图像中更直观的看到:
housing.plot(kind="scatter", x="rooms_per_household", y="median_house_value",alpha=0.2)
plt.axis([0, 5, 0, 520000])
plt.show()
4、要领
这一轮的探索不一定要多么彻底,关键是迈开第一步,快速获得洞见,这将有助于我们获得非常棒的第一个原型。这也是一个不断迭代的过程:一旦我们的原型产生并且开始运行,我们就可以分析它的输出以获得更多洞见,然后再次回到这个探索步骤。
三、机器学习算法的数据准备
现在,我们可以开始给机器学习算法准备数据了,首先再次拷贝一份训练集:
housing = strat_train_set.drop("median_house_value", axis=1) # drop labels for training set
housing_labels = strat_train_set["median_house_value"].copy()
1、数据清洗
大部分的机器学习算法无法在缺失的特征上工作,所以我们要创建一些函数来辅助它。前面我们已经注意到 total_bedrooms
属性有部分值缺失,所以我们要解决它。
sample_incomplete_rows = housing[housing.isnull().any(axis=1)].head()
sample_incomplete_rows
有以下三种选择:
- 放弃这些相应的区域。
- 放弃整个属性。
- 将缺失的值设置为某个值(0、平均数或者中位数等)。
通过 DataFrame
的 dropna
、drop
和 fillna
方法,可以轻松完成这些操作:
sample_incomplete_rows.dropna(subset=["total_bedrooms"]) # option 1
sample_incomplete_rows.drop("total_bedrooms", axis=1) # option 2
median = housing["total_bedrooms"].median()
sample_incomplete_rows["total_bedrooms"].fillna(median, inplace=True) # option 3
如果选择方法3,我们需要计算出训练集的中位数值,然后用它填充训练集中的缺失值,但也别忘了保存这个计算出来的中位数值,因为后面可能需要用到。
当重新评估系统时,我们需要更换测试集中的缺失值;或者在系统上线时,需要使用新数据替代缺失值。Scikit-Learn
提供了一个非常容易上手的类来处理缺失值:SimpleImputer
。使用方法如下:
from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy="median")
首先,我们创建了一个 SimpleImputer
实例,指定要用属性的中位数值替换该属性的缺失值。
housing_num = housing.drop("ocean_proximity", axis=1)
# alternatively: housing_num = housing.select_dtypes(include=[np.number])
由于中位数值只能在数值属性上计算,所以我们需要创建一个没有文本属性ocean_proximity
的数据副本。
imputer.fit(housing_num)
使用 fit
方法将 imputer
实例适配到训练数据,这里 imputer
仅仅只是计算了每个属性的中位数值,并将结果存储在其实例变量 statistics_
中。
imputer.statistics_
array([-118.48 , 34.25 , 29. , 2131. , 435. , 1168. ,410. , 3.5348])
housing_num.median().values
array([-118.48 , 34.25 , 29. , 2131. , 435. , 1168. ,410. , 3.5348])
虽然只有 total_bedrooms
这个属性存在缺失值,但是我们无法确认系统启动之后新数据中是否一定不存在任何缺失值,所以稳妥起见,还是将 imputer
应用于所有的数值属性。现在,我们可以使用这个“训练有素”的 imputer
将缺失值替换成中位数值从而完成训练集转换:
X = imputer.transform(housing_num)
结果是一个包含转换后特征的NumPy
数组,如果需要将它放回pandas DataFrame
,也很简单:
housing_tr = pd.DataFrame(X, columns=housing_num.columns,index=housing.index)
2、处理文本和分类属性
到目前为止,我们只处理数值属性,但现在让我们看一下文本属性。在此数据集中,只有一个:ocean_proximity
属性。我们看看前10
个实例的值:
housing_cat = housing[["ocean_proximity"]]
housing_cat.head(10)
它不是任意文本,而是有限个可能的取值,每个值代表一个类别。因此,此属性是分类属性。大多数机器学习算法更喜欢使用数字,因此让我们将这些类别从文本转到数字。为此,我们可以使用 Scikit-Learn
的 OrdinalEncoder
类:
from sklearn.preprocessing import OrdinalEncoderordinal_encoder = OrdinalEncoder()
housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
housing_cat_encoded[:10]
array([[0.],[0.],[4.],[1.],[0.],[1.],[0.],[1.],[0.],[0.]])
使用categories_
实例变量获取类别列表,这个列表包含每个类别属性的一维数组(在这种情况下,这个列表包含一个数组,因为只有一个类别属性):
ordinal_encoder.categories_
[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],dtype=object)]
这种表征方式产生的一个问题是:机器学习算法会认为两个相近的值比两个离得较远的值更为相似一些。在某些情况下这是对的(对一些有序类别,像“坏”“平均”“好”“优秀”),但是,对ocean_proximity
而言情况并非如此(例如,类别0和类别4之间就比类别0和类别1之间的相似度更高)。
常见的解决方案是给每个类别创建一个二进制的属性:当类别是“<1H OCEAN”时,一个属性为1(其他为0),当类别是“INLAND”时,另一个属性为1(其他为0),以此类推。
这就是独热编码,因为只有一个属性为1(热),其他均为0(冷)。新的属性有时候称为哑(dummy)属性。Scikit-Learn
提供了一个 OneHotEncoder
编码器,可以将整数类别值转换为独热向量,我们用它来将类别编码为独热向量。
from sklearn.preprocessing import OneHotEncodercat_encoder = OneHotEncoder()
housing_cat_1hot = cat_encoder.fit_transform(housing_cat)
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是一件非常浪费的事情,因此稀疏矩阵选择仅存储非零元素的位置。
而我们依旧可以像使用一个普通的二维数组那样来使用它,当然如果实在想把它转换成一个(密集的)NumPy数组,只需要调用 toarray
方法即可:
housing_cat_1hot.toarray()
array([[0., 0., 0., 1., 0.],[0., 0., 0., 0., 1.],[0., 0., 0., 1., 0.],...,[0., 1., 0., 0., 0.],[0., 1., 0., 0., 0.],[1., 0., 0., 0., 0.]])
我们可以再次使用编码器的 categories_
实例变量来得到类别列表:
cat_encoder.categories_
[array(['<1H OCEAN', 'INLAND', 'ISLAND', 'NEAR BAY', 'NEAR OCEAN'],dtype=object)]
!如果类别属性具有大量可能的类别(例如,国家代码、专业、物种),那么独热编码会导致大量的输入特征,这可能会减慢训练并降低性能。如果发生这种情况,你可能想要用相关的数字特征代替类别输入。例如,你可以用与海洋的距离来替换ocean_proximity特征(类似地,可以用该国家的人口和人均GDP来代替国家代码)。或者,你可以用可学习的低维向量(称为嵌入)来替换每个类别。每个类别的表征可以在训练期间学习,这是表征学习的示例。
3、自定义转换器
虽然 Scikit-Learn
提供了许多有用的转换器,但是我们仍然需要为一些诸如自定义清理操作或组合特定属性等任务编写自己的转换器。我们当然希望让自己的转换器与Scikit-Learn
自身的功能(比如流水线)无缝衔接,而由于 Scikit-Learn
依赖于鸭子类型的编译,而不是继承,所以我们所需要的只是创建一个类,然后应用以下三种方法:fit
(返回self)、transform
、fit_transform
。
!关于鸭子类型,后文有补充解释。如果这里不太理解,可以先往下阅读理解鸭子类型再回到这里。
我们可以通过添加 TransformerMixin
作为基类,直接得到最后一种方法 fit_transform
。同时,如果添加 BaseEstimator
作为基类(并在构造函数中避免*args和**kargs),我们还能额外获得两种非常有用的自动调整超参数的方法 get_params
和 set_params
。
TransformerMixin 的主要功能是为转换器提供 fit_transform 方法的默认实现。该方法结合了 fit 和 transform,允许用户在一步中完成模型的拟合和数据转换。
例如,我们前面讨论过的组合属性,这里有个简单的转换器类,用来添加组合后的属性:
from sklearn.base import BaseEstimator, TransformerMixin# column index
rooms_ix, bedrooms_ix, population_ix, households_ix = 3, 4, 5, 6class CombinedAttributesAdder(BaseEstimator, TransformerMixin):def __init__(self, add_bedrooms_per_room=True): # no *args or **kargsself.add_bedrooms_per_room = add_bedrooms_per_roomdef fit(self, X, y=None):return self # nothing else to dodef transform(self, X):rooms_per_household = X[:, rooms_ix] / X[:, households_ix]population_per_household = X[:, population_ix] / X[:, households_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)
注意到 rooms_ix, bedrooms_ix, population_ix, households_ix
这里我们使用了硬编码,一种更好的方式是使用字段名动态获取:
col_names = "total_rooms", "total_bedrooms", "population", "households"
rooms_ix, bedrooms_ix, population_ix, households_ix = [housing.columns.get_loc(c) for c in col_names] # get the column indices
在本例中,转换器有一个超参数 add_bedrooms_per_room
默认设置为 True
(提供合理的默认值通常是很有帮助的),这个超参数可以让我们轻松知晓添加这个属性是否有助于机器学习算法。
更一般地,如果我们对数据准备的步骤没有充分的信心,就可以添加这个超参数来进行把关。这些数据准备步骤的执行越自动化,我们自动尝试的组合也就越多,从而有更大可能从中找到一个重要的组合(还节省了大量时间)。
4、特征缩放
最重要也最需要应用到数据上的转换就是特征缩放。如果输入的数值属性具有非常大的比例差异,往往会导致机器学习算法的性能表现不佳,当然也有极少数特例。案例中的房屋数据就是这样:房间总数的范围从6~39320
,而收入中位数的范围是0~15
。注意,目标值通常不需要缩放。
同比例缩放所有属性的两种常用方法是最小-最大缩放和标准化。
最小-最大缩放(又叫作归一化)很简单:将值重新缩放使其最终范围归于0~1
之间。实现方法是将值减去最小值并除以最大值和最小值的差。对此,Scikit-Learn
提供了一个名为 MinMaxScaler
的转换器。如果出于某种原因,我们希望范围不是0~1
,那么可以通过调整超参数 feature_range
进行更改。
标准化则完全不一样:首先减去平均值(所以标准化值的均值总是零),然后除以方差,从而使得结果的分布具备单位方差。不同于最小-最大缩放的是,标准化不将值绑定到特定范围,对某些算法而言,这可能是个问题(例如,神经网络期望的输入值范围通常是0~1
)。但是标准化的方法受异常值的影响更小,例如,假设某个地区的平均收入为100(错误数据),最小-最大缩放会将所有其他值从0~15
降到0~0.15
,而标准化则不会受到很大影响。对此,Scikit-Learn
提供了一个标准化的转换器StandadScaler
。
!重要的是,跟所有转换一样,缩放器仅用来拟合训练集,而不是完整的数据集(包括测试集)。只有这样,才能使用它们来转换训练集和测试集(和新数据)。
5、转换流水线
许多数据转换的步骤需要以正确的顺序来执行,而 Scikit-Learn
正好提供了 Pipeline
类来支持这样的转换。下面是一个数值属性的流水线示例:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScalernum_pipeline = Pipeline([('imputer', SimpleImputer(strategy="median")),('attribs_adder', CombinedAttributesAdder()),('std_scaler', StandardScaler()),])housing_num_tr = num_pipeline.fit_transform(housing_num)
代码解析:
- 代码结构分析:
- 首先导入了Pipeline和StandardScaler
- 创建了一个名为num_pipeline的Pipeline对象
- 最后用
housing_num
数据拟合(fit)并转换(transform)这个pipeline
- Pipeline的三个步骤:
- imputer:使用中位数策略处理缺失值
- attribs_adder:自定义的特征组合转换器
- std_scaler:数据标准化(均值为0,方差为1)
- Pipeline的工作原理:
- 按顺序执行每个转换步骤
- 每个步骤的输出作为下一个步骤的输入
- fit_transform会依次调用每个步骤的相应方法
- 最终输出经过所有转换处理后的数据
- 执行过程(当调用
fit_transform
时):
- 先调用SimpleImputer的fit_transform处理缺失值
- 将结果传给CombinedAttributesAdder进行特征组合
- 最后用StandardScaler进行标准化
- 返回最终处理后的数据
housing_num_tr
array([[-1.26017908, 0.81900844, 0.03105702, ..., 0.33876264,0.04747783, 2.13831979],[ 0.50453401, -0.76487156, 0.82525471, ..., 0.34398974,-0.07168096, -0.58463955],[-1.42515226, 0.99239176, 1.46061287, ..., 0.01538999,0.05570578, 0.2552861 ],...,[ 0.76949093, -0.71332517, 0.5869954 , ..., -0.07849318,-0.03063158, -0.40096737],[ 0.73949581, -0.69926707, 0.82525471, ..., 0.08519798,-0.01060437, -0.77878766],[ 0.55952507, -0.74612742, -0.28662206, ..., -0.73999398,-0.06902297, 1.62213203]])
到目前为止,我们分别处理了类别列和数值列。拥有一个能够处理所有列的转换器会更方便,将适当的转换应用于每个列。在0.20版中,Scikit-Learn
为此引入了 ColumnTransformer
,好消息是它与 pandas DataFrames
一起使用时效果很好。让我们用它来将所有转换应用到房屋数据:
from sklearn.compose import ColumnTransformernum_attribs = list(housing_num)
cat_attribs = ["ocean_proximity"]full_pipeline = ColumnTransformer([("num", num_pipeline, num_attribs),("cat", OneHotEncoder(), cat_attribs),])housing_prepared = full_pipeline.fit_transform(housing)
首先导入 ColumnTransformer
类,接下来获得数值列名称列表和类别列名称列表,然后构造一个 ColumnTransformer
:构造函数需要一个元组列表,其中每个元组都包含一个名字、一个转换器以及一个该转换器能够应用的列名字(或索引)的列表。
在此示例中,我们指定数值列使用之前定义的 num_pipeline
进行转换,类别列使用 OneHotEncoder
进行转换,最后我们将ColumnTransformer
应用到房屋数据:它将每个转换器应用于适当的列,并沿第二个轴合并输出(转换器必须返回相同数量的行)。
请注意,OneHotEncoder
返回一个稀疏矩阵,而 num_pipeline
返回一个密集矩阵。当稀疏矩阵和密集矩阵混合在一起时,ColumnTransformer
会估算最终矩阵的密度(即单元格的非零比率),如果密度低于给定的阈值,则返回一个稀疏矩阵(通过默认值为sparse_threshold=0.3)。在此示例中,它返回一个密集矩阵。
现在我们有一个预处理流水线 housing_prepared
,该流水线可以获取全部房屋数据并对每一列进行适当的转换。
housing_prepared
array([[-1.26017908, 0.81900844, 0.03105702, ..., 0. ,1. , 0. ],[ 0.50453401, -0.76487156, 0.82525471, ..., 0. ,0. , 1. ],[-1.42515226, 0.99239176, 1.46061287, ..., 0. ,1. , 0. ],...,[ 0.76949093, -0.71332517, 0.5869954 , ..., 0. ,0. , 0. ],[ 0.73949581, -0.69926707, 0.82525471, ..., 0. ,0. , 0. ],[ 0.55952507, -0.74612742, -0.28662206, ..., 0. ,0. , 0. ]])
四、补充内容
补充1:鸭子类型
鸭子类型的概念
鸭子类型(Duck Typing)是动态编程语言中一种常见的类型系统设计理念。其核心思想是“如果它走起来像鸭子,叫起来像鸭子,那么它就是鸭子”。在编程中,这意味着对象的类型由它的行为(方法和属性)决定,而非显式的类或接口声明。
鸭子类型的特点
行为决定类型:不需要显式继承或实现接口,只要对象具有所需的方法或属性,就可以被视为某种类型。例如,一个对象只要有quack()
方法,就可以被视为“鸭子”。
动态性:在运行时检查对象是否支持特定操作,而非编译时。这使得代码更灵活,但可能增加运行时错误的风险。
多态性:不同类的对象只要行为一致,就可以互换使用。例如,for
循环不关心对象是否是列表,只要它是可迭代的。
鸭子类型的示例
以下是一个Python中的鸭子类型示例:
class Duck:def quack(self):print("Quack!")class Person:def quack(self):print("I'm quacking like a duck!")def make_it_quack(thing):thing.quack()duck = Duck()
person = Person()
make_it_quack(duck) # 输出: Quack!
make_it_quack(person) # 输出: I'm quacking like a duck!
这里make_it_quack
函数不关心参数的具体类型,只要它有quack()
方法。
鸭子类型的优缺点
优点:
- 代码更灵活,减少冗余的接口定义。
- 支持快速原型开发,无需复杂的类型层次结构。
- 更容易实现松耦合的设计。
缺点:
- 缺乏编译时类型检查,可能导致运行时错误。
- 代码可读性可能降低,因为类型依赖文档或约定。
- 不适合需要严格类型安全的场景。
鸭子类型与静态类型语言的对比
静态类型语言(如Java、C#)通常通过接口或抽象类实现类似功能,但需要显式声明。例如:
interface Quackable {void quack();
}class Duck implements Quackable {public void quack() { System.out.println("Quack!"); }
}class Person implements Quackable {public void quack() { System.out.println("I'm quacking like a duck!"); }
}public static void makeItQuack(Quackable thing) {thing.quack();
}
动态语言的鸭子类型省去了显式接口实现的步骤。
适用场景
- 快速开发或脚本编写。
- 需要高度灵活性的框架或库(如Python的迭代协议)。
- 测试中模拟对象行为。
注意事项
- 文档和命名约定非常重要,需明确对象需要支持的行为。
- 单元测试可以弥补缺乏编译时检查的问题。
- 现代动态语言(如Python 3.10+)支持类型提示,可在保留鸭子类型的同时提供类型检查。
补充2:Scikit-Learn 的设计
Scikit-Learn
的 API
设计得非常好。其主要的设计原则是:
估算器
能够根据数据集对某些参数进行估算的任意对象都可以称为估算器(例如,imputer
就是一个估算器)。估算由 fit
方法执行,它只需要一个数据集作为参数(或者两个——对于有监督学习算法,第二个数据集包含标签)。引导估算过程的任何其他参数都视为超参数(例如,imputer’s strategy
),它必须被设置为一个实例变量(一般通过构造函数参数)。
转换器
有些估算器(例如imputer
)也可以转换数据集,这些称为转换器。同样,API也非常简单:由 transform
方法和作为参数的待转换数据集一起执行转换,返回的结果就是转换后的数据集。这种转换的过程通常依赖于学习的参数,比如本例中的imputer
。所有的转换器都可以使用一个很方便的方法,即 fit_transform
,相当于先调用 fit
然后再调用 transform
(但是fit_transform
有时是被优化过的,所以运行得更快一些)。
预测器
最后,还有些估算器能够基于一个给定的数据集进行预测,这称为预测器。预测器的 predict
方法会接受一个新实例的数据集,然后返回一个包含相应预测的数据集。值得一提的还有一个 score
方法,可以用来衡量给定测试集的预测质量(以及在有监督学习算法里对应的标签)。
检查
所有估算器的超参数都可以通过公共实例变量(例如,imputer.strategy
)直接访问,并且所有估算器的学习参数也可以通过有下划线后缀的公共实例变量来访问(例如,imputer.statistics
)。
防止类扩散
数据集被表示为NumPy
数组或SciPy
稀疏矩阵,而不是自定义的类型。超参数只是普通的Python
字符串或者数字。
构成
现有的构件块尽最大可能重用。例如,任意序列的转换器最后加一个预测器就可以轻松创建一个Pipeline
估算器。
合理的默认值
Scikit-Learn
为大多数参数提供了合理的默认值,从而可以快速搭建起一个基本的工作系统。
五、选择和训练模型(更新中…)
我们将在下一篇文章中介绍选择机器学习模型并展开训练。