Sklearn 机器学习应用实用指南(二)
四、回归的预测建模
分类是预测一个实例的离散类别标签的问题,而回归预测建模(或只是回归)是学习自变量(或特征)和连续因变量(或结果)之间的关联强度的问题。一个连续输出变量是一个实值,比如一个整数或浮点值,通常被量化为数量和大小。
简单地说,回归试图了解特征和结果之间的关系有多强。从形式上讲,回归近似于从输入变量(X)到连续输出变量(y)的映射函数(f)。能够学习回归预测模型的算法称为回归算法。由于回归预测一个量,性能必须在那些预测中作为误差来测量。
回归的性能可以用许多方法来衡量,但最常见的是计算均方根误差(RMSE)。 RMSE 的一个好处是误差分数的单位与预测值相同。虽然回归预测可以使用 RMSE 进行评估,但分类预测却不能。
回归数据集
我们专注于四个数据集:小费、波士顿和葡萄酒(红和白)。小费数据由餐馆的服务员小费和相关因素组成,包括小费、餐费和时间。波士顿数据由波士顿不同地点的房价组成。葡萄酒数据由两个数据集(红色和白色)组成,这两个数据集由葡萄牙 Vinho Verde 葡萄酒的变体组成。
回归提示
清单 4-1 中所示的第一个代码示例从 CSV 文件加载 tips 数据,通过将分类特征转换为虚拟特征来实现特征工程,向现有数据集中添加新数据,估算新数据,显示特征重要性,使用线性回归算法训练数据,并进行预测。在这种情况下,我们从多元线性回归中学习,因为我们正在对多个特征和一个连续相关目标进行数据训练。
import numpy as np, pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
if __name__ == "__main__":
br = '\n'
tips = pd.read_csv('data/tips.csv')
print ('original data shape:', tips.shape, br)
target = tips['tip']
data = tips.drop(['tip'], axis=1)
data = pd.get_dummies(data, columns=['sex', 'smoker','day', 'time'])
d = {'sex_Male':'male', 'sex_Female':'female',
'smoker_Yes':'smoker', 'smoker_No':'non-smoker',
'day_Thur':'th', 'day_Fri':'fri', 'day_Sat':'sat',
'day_Sun':'sun', 'time_Lunch':'lunch',
'time_Dinner':'dinner'}
data = data.rename(index=str, columns=d)
X = data.values
y = target.values
print ('X and y shapes (post conversion):')
print (X.shape, y.shape, br)
X_vector = np.array([30.00, 'NaN', 1, 0, 1, 0, 0, 0, 0, 1, 1, 0])
y_vector = np.array([4.5])
X = np.vstack([X, X_vector])
y = np.append(y, y_vector)
print ('new X and y data point:')
print (X[244], y[244], br)
X_vectors = np.array([[24.99, 'NaN',0, 1, 0, 1, 1, 0, 0, 0, 0, 1],
[19.99, 'NaN',1, 0, 1, 0, 0, 0, 0, 1, 1, 0]])
y_vectors = np.array([[3.5], [2.0]])
X = np.vstack([X, X_vectors])
y = np.append(y, y_vectors)
print ('new X and y data points:')
print (X[245], y[245])
print (X[246], y[246], br)
imputer = SimpleImputer()
imputer.fit(X)
X = imputer.transform(X)
print ('new data shape:', X.shape, br)
print ('new records post imputation (features and targets):')
print (X[244], y[244])
print (X[245], y[245])
print (X[246], y[246], br)
rfr = RandomForestRegressor(random_state=0, n_estimators=100)
rfr.fit(X, y)
print ('feature importance (first 6 features):')
feature_importances = rfr.feature_importances_
features = list(data.columns.values)
importance = sorted(zip(feature_importances, features), reverse=True)
[print (row) for i, row in enumerate(importance) if i < 6]
print ()
X_train, X_test, y_train, y_test = train_test_split(
X, y, random_state=0)
model = LinearRegression()
model_name = model.__class__.__name__
print ('<<' + model_name + '>>', br)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
print (rmse, '(rmse)', br)
print ('predict from new data:')
p1 = [X[244]]
p2 = [X[245], X[246]]
y1, y2 = model.predict(p1), model.predict(p2)
print (y[244], y1[0])
print (y[245], y2[0])
print (y[246], y2[1])
X_file = 'data/X_tips'
y_file = 'data/y_tips'
np.save(X_file, X)
np.save(y_file, y)
Listing 4-1Predicting from tips with get_dummies encoding
继续执行清单 4-1 中的代码。请记住,您可以从本书的示例下载中找到示例。您不需要手动键入示例。更容易访问示例下载和复制/粘贴。
执行清单 4-1 的输出应该如下所示:
original data shape: (244, 7)
X and y shapes (post conversion):
(244, 12) (244,)
new X and y data point:
['30.0' 'NaN' '1' '0' '1' '0' '0' '0' '0' '1' '1' '0'] 4.5
new X and y data points:
['24.99' 'NaN' '0' '1' '0' '1' '1' '0' '0' '0' '0' '1'] 3.5
['19.99' 'NaN' '1' '0' '1' '0' '0' '0' '0' '1' '1' '0'] 2.0
new data shape: (247, 12)
new records post imputation (features and targets):
[30\. 2.56967213 1\. 0\. 1\. 0.
0\. 0\. 0\. 1\. 1\. 0\. ] 4.5
[24.99 2.56967213 0\. 1\. 0\. 1.
1\. 0\. 0\. 0\. 0\. 1\. ] 3.5
[19.99 2.56967213 1\. 0\. 1\. 0.
0\. 0\. 0\. 1\. 1\. 0\. ] 2.0
feature importance (first 6 features)
:
(0.7597845511444519, 'total_bill')
(0.0643775334380493, 'size')
(0.03663421916266647, 'non-smoker')
(0.033603977117169975, 'smoker')
(0.026410154999617023, 'sat')
(0.02186564474599064, 'sun')
<<LinearRegression>>
0.9474705746817206 (rmse)
predict from new data:
4.5 3.827512419066452
3.5 3.56649951075833
2.0 2.941595038244732
代码示例首先导入 RandomForestRegressor、SimpleImputer 和 mean_square_error 以及其他必需的包。主程序块从从 CSV 文件加载 tips 数据开始。它继续用 pandas get_dummies 函数对分类变量进行特征工程。提醒一下,特征工程正在使用数据集的领域知识来创建使机器学习算法更有效工作的特征。
熊猫得到 _dummies 一热默认编码。比如,性别要么是女要么是男。通过一键编码,女变成了【1,0】,而男【0,1】。在这种情况下必须进行特征工程,因为 Scikit-Learn 只处理数字数据。接下来,创建特征集 X 和目标 y。请注意,由于一次热编码,数据形状现在有 12 个特征。
代码的下一部分向数据集中添加了三条新记录。还要注意,我们添加了一个 NaN 特性,这意味着该特性没有可识别的值。Scikit-Learn 算法不能处理 Nan 特征,所以我们用简单估算器类估算特征平均值。插补是用替代值替换缺失数据的过程。我们接下来显示新记录。请注意,所有的 Nan 值都被替换为其特征平均值。
小费
插补是用替代数据替换缺失数据的常用技术。
代码的最后一部分首先在 RandomForestRegressor 的帮助下显示六个最重要的特性,RandomForestRegressor 是一个元估计器,适合对数据进行决策树分类,并使用平均来提高性能和减少过度拟合。接下来,数据被分成训练测试子集,并用线性回归进行训练,这样我们就可以计算 RMSE。线性回归模拟特征和目标之间的关系。最后,我们根据新数据进行预测,其中第一个 st 值是实际目标值,第二个是我们的预测值,并将 X 和 y 保存为 NumPy 文件以供将来处理。
小费
回归的目标是最小化(减少)RMSE。
清单 4-2 中显示的下一个代码示例使用 DictVectorizer 而不是 get_dummies 作为一次性编码分类数据的替代选项。
import pandas as pd, numpy as np
from sklearn.feature_extraction import DictVectorizer
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from random import randint
if __name__ == "__main__":
br = '\n'
tips = pd.read_csv('data/tips.csv')
data = tips.drop(['tip'], axis=1)
target = tips['tip']
v = ['sex', 'smoker', 'day', 'time']
ls = data[v].to_dict(orient='records')
vector = DictVectorizer(sparse=False, dtype=int)
d = vector.fit_transform(ls)
print ('one hot encoding:')
print (d[0:3], br)
print ('encoding order:')
encode_order = vector.get_feature_names()
print (encode_order, br)
data = data.drop(['sex', 'smoker', 'day', 'time'], axis=1)
X = data.values
print ('feature shape after removing categorical columns:')
print (X.shape, br)
Xls, dls = X.tolist(), d.tolist()
X = [np.array(row + dls[i]) for i, row in enumerate(Xls)]
X = np.array(X)
y = target.values
print ('feature shape after adding encoded data back:')
print (X.shape, br)
X_train, X_test, y_train, y_test = train_test_split(
X, y, random_state=0)
model = LinearRegression(fit_intercept=True)
model_name = model.__class__.__name__
print ('<<' + model_name + '>>', br)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
print (rmse, '(rmse)', br)
print ('predict 1st test set element (actual/prediction):')
print (y_test[0], y_pred[0], br)
rints = [randint(0, y.shape[0]-1) for row in range(3)]
print ('random integers:', rints, br)
p = [X[rints[0]], X[rints[1]], X[rints[2]]]
y_p = model.predict(p)
y_p = list(np.around(y_p, 2))
print (y_p, '(predicted)')
print ([y[rints[0]], y[rints[1]], y[rints[2]]], '(actual)')
Listing 4-2Predicting from tips with DictVectorizer encoding
执行清单 4-2 的输出应该如下所示:
One hot encoding:
[[0 0 1 0 1 0 1 0 1 0]
[0 0 1 0 0 1 1 0 1 0]
[0 0 1 0 0 1 1 0 1 0]]
encoding order:
['day=Fri', 'day=Sat', 'day=Sun', 'day=Thur', 'sex=Female', 'sex=Male', 'smoker=No','smoker=Yes', 'time=Dinner', 'time=Lunch']
feature shape after removing categorical columns:
(244, 2)
feature shape after adding encoded data back:
(244, 12)
<<LinearRegression>>
0.9636287548943022 (rmse)
predict 1st test set element (actual/prediction):
2.64 2.8121130438023094
random integers: [202, 13, 143]
[2.19, 3.21, 4.52] (predicted)
[2.0, 3.0, 5.0] (actual)
代码从导入 DictVectorizer 以及其他必备的包开始。主程序块加载 tips 数据,剥离特征 tip 并将剩余特征放入变量数据,并将特征 tip 放入变量目标。接下来,我们将需要编码的特征放入变量 v 中。
代码继续剥离需要编码的特性,并将结果放入变量 ls 中。然后创建一个 DictVectorizer 实例,并放入变量 vector 中。DictVectorizer 是一种 Scikit-Learn 技术,它将特征值映射列表转换为向量。
然后,向量中的数据被拟合(或训练)并转换为独热编码值,结果被放入变量 d 中。要查看编码顺序,请使用函数 get_feature_names。接下来,从变量数据中删除编码特征,这样我们就可以开始构建特征集 x 了。
代码继续创建一个基于 X 的列表和另一个基于 d 的列表,这样我们可以将 X 与包含在 d 中的独热码编码值连接起来。剩下的就是将 X 和 y 转换成 NumPy 值。代码结束时将 X 和 y 分成训练测试子集,用线性回归进行训练,计算 RMSE,并进行预测。RMSE 要高一点,因为我们没有像上一个例子那样添加新数据。
小费
在大多数情况下,使用 get_dummies 进行一键编码。
尽管 DictVectorizer 比 get_dummies 更难实现,但它具有稀疏的优势。也就是说,不存在的特征不需要被存储。此外,DictVectorizer 是一种有用的表示转换,用于在自然语言处理模型中训练序列分类器,该模型通常通过提取感兴趣的特定单词周围的特征窗口来工作。
清单 4-3 中的最后一个代码示例将工程数据(来自 NumPy 文件)加载到 X 和 y 中,并为实现正则化的几个回归算法计算 RMSE。正则化是一种通过在给定数据集上适当拟合函数(或算法)来减少误差的技术,以减轻过度拟合。
import numpy as np
from sklearn.linear_model import LinearRegression, Ridge,\
Lasso, ElasticNet, SGDRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import StandardScaler
def get_scores(model, Xtest, ytest):
y_pred = model.predict(Xtest)
return np.sqrt(mean_squared_error(ytest, y_pred)),\
model.__class__.__name__
if __name__ == "__main__":
br = '\n'
X = np.load('data/X_tips.npy')
y = np.load('data/y_tips.npy')
X_train, X_test, y_train, y_test = train_test_split(
X, y, random_state=0)
print ('rmse:')
lr = LinearRegression().fit(X_train, y_train)
rmse, lr_name = get_scores(lr, X_test, y_test)
print (rmse, '(' + lr_name + ')')
rr = Ridge(random_state=0).fit(X_train, y_train)
rmse, rr_name = get_scores(rr, X_test, y_test)
print (rmse, '(' + rr_name + ')')
lasso = Lasso(random_state=0).fit(X_train, y_train)
rmse, lasso_name = get_scores(lasso, X_test, y_test)
print (rmse, '(' + lasso_name + ')')
en = ElasticNet(random_state=0).fit(X_train, y_train)
rmse, en_name = get_scores(en, X_test, y_test)
print (rmse, '(' + en_name + ')')
sgdr = SGDRegressor(random_state=0, max_iter=1000, tol=0.001)
sgdr.fit(X_train, y_train)
rmse, sgdr_name = get_scores(sgdr, X_test, y_test)
print (rmse, '(' + sgdr_name + ')', br)
scaler = StandardScaler()
X_train_std = scaler.fit_transform(X_train)
X_test_std = scaler.fit_transform(X_test)
print ('rmse std:')
lr_std = LinearRegression().fit(X_train_std, y_train)
rmse, lr_name = get_scores(lr_std, X_test_std, y_test)
print (rmse, '(' + lr_name + ')')
rr_std = Ridge(random_state=0).fit(X_train_std, y_train)
rmse, rr_name = get_scores(rr_std, X_test_std, y_test)
print (rmse, '(' + rr_name + ')')
lasso_std = Lasso(random_state=0).fit(X_train_std, y_train)
rmse, lasso_name = get_scores(lasso_std, X_test_std, y_test)
print (rmse, '(' + lasso_name + ')')
en_std = ElasticNet(random_state=0)
en_std.fit(X_train_std, y_train)
rmse, en_name = get_scores(en_std, X_test_std, y_test)
print (rmse, '(' + en_name + ')')
sgdr_std = SGDRegressor(random_state=0, max_iter=1000, tol=0.001)
sgdr_std.fit(X_train_std, y_train)
rmse, sgdr_name = get_scores(sgdr_std, X_test_std, y_test)
print (rmse, '(' + sgdr_name + ')')
Listing 4-3Predicting from tips with regression regularization models
执行清单 4-3 的输出应该如下所示:
rmse
:
0.9474705746817206 (LinearRegression)
0.9469115898683899 (Ridge)
0.9439950256305224 (Lasso)
0.9307377813721578 (ElasticNet)
1.7005504977258326 (SGDRegressor)
rmse std:
0.9007751177881488 (LinearRegression)
0.9014055340745654 (Ridge)
1.333812899498391 (Lasso)
1.1310151423347359 (ElasticNet)
0.9021020134681715 (SGDRegressor)
代码示例从导入 Ridge、Lasso、ElasticNet、SGDRegressor 和其他必需的包开始。函数 get_scores 返回 RMSE。主程序块首先将 NumPy 文件中的数据加载到 X 和 y 中,然后将数据分割成训练测试子集。
代码的其余部分使用 LinearRegression 和几个实现正则化的回归模型来训练数据。Ridge、Lasso、ElasticNet 和 SGDRegressor 是流行的 Scikit-Learn 回归算法,用于调整线性回归。正则化通过在给定的训练集上适当地拟合模型来减少过度拟合,从而减少误差。也就是说,正则化不鼓励学习更复杂的模型来减轻过度拟合的风险。
小费
使用正则化来减少误差并最小化回归模型的过度拟合。
岭回归对系数的大小施加惩罚。拉索回归推导出具有较少参数值(或稀疏模型)的解决方案,有效减少了解决方案所依赖的变量数量。
Lasso 使用 L1 正则化,Ridge 使用 L2 正则化。
L1 和 L2 的主要区别在于刑期。Ridge 将系数的平方值作为惩罚添加到损失函数中,以减轻过拟合。Lasso(最小绝对收缩和选择算子)将系数的绝对值作为损失函数的惩罚,当我们有大量的特征时,它工作得很好。
损失(成本)函数是将一个或多个特征的事件(或值)映射到表示与该事件相关联的成本的实数上的函数。两种技术的主要区别在于,Lasso 将不太重要的特征的系数缩小到零,从而有效地将它们排除在考虑范围之外。Lasso 为密集和稀疏数据提供了相同的结果,对于稀疏数据,速度得到了提高。
回归包括 L1 和 L2 的惩罚作为正则项。结合 L1 和 L2 允许学习稀疏模型,其中很少有权重像 Lasso 一样非零,同时仍然保持岭的正则化属性。ElasticNet 最擅长的是多个相关的特性。利用 Lasso 和 Ridge 之间的权衡的一个实际优势是,它允许 ElasticNet 继承 Ridge 在旋转下的一些稳定性,同时仍然可以很好地处理稀疏模型。
SGDRegressor 通过最小化带有随机梯度下降(SGD)的正则化经验损失来执行。也就是说,对每个样本估计损失的梯度,然后用递减的强度时间表(或学习速率)来更新模型。正则项是添加到损失函数中的惩罚,它使用 L1(套索)或 L2(山脊)或两者的组合(弹性网)将参数向零收缩。
因此,正则化技术的选择高度依赖于数据的性质。稀疏数据表明从套索开始。创建过于复杂的模型将意味着转向 Ridge。ElasticNet 提供了一个折中方案。SGDRegressor 试图做到这一切。对于相对较小的数据集,尝试所有这些技术没有问题。但是,大型数据集是一个不同的故事,因为需要大量的处理时间(或高计算费用)。
请注意,有时扩展(或标准化)会提高性能,有时不会。例如,缩放对 LinearRegression、Ridge 和 SGDRegressor 有帮助,但对 Lasso 和 ElasticNet 不利。
小费
如果您有时间、耐心和计算资源,实验是提高性能的一个很好的方法。
回归波士顿
清单 4-4 中显示的第一个代码示例显示了波士顿数据集中的要素重要性,使用 RandomForestRegressor 进行训练,并计算有噪声和无噪声情况下的 RMSE。
import numpy as np, pandas as pd
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error
if __name__ == "__main__":
br = '\n'
boston = load_boston()
X = boston.data
y = boston.target
print ('feature shape', X.shape)
print ('target shape', y.shape, br)
keys = boston.keys()
rfr = RandomForestRegressor(random_state=0, n_estimators=100)
rfr.fit(X, y)
features = boston.feature_names
feature_importances = rfr.feature_importances_
importance = sorted(zip(feature_importances, features), reverse=True)
[print (row) for row in importance]
print ()
X_train, X_test, y_train, y_test = train_test_split(
X, y, random_state=0)
rfr = RandomForestRegressor(random_state=0, n_estimators=100)
rfr.fit(X_train, y_train)
rfr_name = rfr.__class__.__name__
y_pred = rfr.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
print (rfr_name + ' (rmse):', rmse, br)
cols = list(features) + ['target']
data = pd.DataFrame(data=np.c_[X, y], columns=cols)
print ('boston dataset sample:')
print (data[['RM', 'LSTAT', 'DIS', 'CRIM', 'NOX', 'PTRATIO', 'target']].head(3), br)
print ('data set before removing noise:', data.shape)
noise = data.loc[data['target'] >= 50]
data = data.drop(noise.index)
print ('data set without noise:', data.shape, br)
X = data.loc[:, data.columns != 'target'].values
y = data['target'].values
print ('cleansed feature shape:', X.shape)
print ('cleansed target shape:', y.shape, br)
X_train, X_test, y_train, y_test = train_test_split(
X, y, random_state=0)
rfr = RandomForestRegressor(random_state=0, n_estimators=100)
rfr.fit(X_train, y_train)
y_pred = rfr.predict(X_test)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
print (rfr_name + ' (rmse):', rmse)
X_file = 'data/X_boston'
y_file = 'data/y_boston'
np.save(X_file, X)
np.save(y_file, y)
Listing 4-4Exploring boston data with RandomForestRegressor
执行清单 4-4 的输出应该如下所示:
feature shape (506, 13)
target shape (506,)
(0.45730362625767496, 'RM')
(0.35008661885681375, 'LSTAT')
(0.06518862820215894, 'DIS')
(0.040989617257001, 'CRIM')
(0.02024797563034355, 'NOX')
(0.015576365835498516, 'PTRATIO')
(0.015524054184831321, 'TAX')
(0.011764308556043926, 'AGE')
(0.011324966974602932, 'B')
(0.005912139937999768, 'INDUS')
(0.003916064249793193, 'RAD')
(0.0011173446269339175, 'ZN')
(0.0010482894303040916, 'CHAS')
RandomForestRegressor (rmse): 4.091149842219918
boston dataset sample:
RM LSTAT DIS CRIM NOX PTRATIO target
0 6.575 4.98 4.0900 0.00632 0.538 15.3 24.0
1 6.421 9.14 4.9671 0.02731 0.469 17.8 21.6
2 7.185 4.03 4.9671 0.02729 0.469 17.8 34.7
data set before removing noise: (506, 14)
data set without noise: (490, 14)
cleansed feature shape: (490, 13)
cleansed target shape: (490,)
RandomForestRegressor (rmse): 3.37169151536684
该代码示例从导入必备包开始。主块将 sklearn.datasets 中的波士顿数据加载到 X 和 y 中,并显示形状。接下来,RandomForestRegressor 对完整数据集(X 和 y)进行训练,以创建和显示特征重要性。代码继续将数据分成训练测试子集,并用 RandomForestRegressor 进行训练(X_train,y_train)。计算并显示 RMSE。
随机森林是一种集成技术,能够通过使用多个决策树和 bagging 来执行回归和分类。Bagging 包括用替换的不同数据样本训练每个决策树。这个想法是结合多个决策树来确定结果,而不是依赖单个决策树。
代码继续将 X 和 y 读入熊猫数据帧,并显示前三条记录。然后从数据帧中去除噪声,并保存到 X 和 y 中。清理后的数据被分成训练测试子集,并使用 RandomForestRegressor 进行训练。请注意,清理后的数据的 RMSE 要低得多(误差更小)。代码最后将 X 和 y 保存为 NumPy 文件。
16 个数据点的 MEDV 值为 50.0,这可能包含缺失值或删减值,可视为噪声。所以,我们不再考虑它们。有关波士顿数据集中噪声的更多信息,请参考以下链接: https://www.ritchieng.com/machine-learning-project-boston-home-prices/ 。
清单 4-5 中显示的本节中的最后一个代码示例加载经过清理(去除噪声)的波士顿数据,并使用线性回归、正则化模型和 RandomForestRegressor 计算 RMSE。
import numpy as np
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression, Ridge,\
Lasso, ElasticNet, SGDRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import StandardScaler
def get_scores(model, Xtest, ytest):
y_pred = model.predict(Xtest)
return np.sqrt(mean_squared_error(ytest, y_pred)),\
model.__class__.__name__
if __name__ == "__main__":
br = '\n'
X = np.load('data/X_boston.npy')
y = np.load('data/y_boston.npy')
print ('feature shape', X.shape)
print ('target shape', y.shape, br)
X_train, X_test, y_train, y_test = train_test_split( X, y, random_state=0)
print ('rmse:')
rfr = RandomForestRegressor(random_state=0, n_estimators=100)
rfr.fit(X_train, y_train)
rmse, rfr_name = get_scores(rfr, X_test, y_test)
print (rmse, '(' + rfr_name + ')')
lr = LinearRegression().fit(X_train, y_train)
rmse, lr_name = get_scores(lr, X_test, y_test)
print (rmse, '(' + lr_name + ')')
ridge = Ridge(random_state=0).fit(X_train, y_train)
rmse, ridge_name = get_scores(ridge, X_test, y_test)
print (rmse, '(' + ridge_name + ')')
lasso = Lasso(random_state=0).fit(X_train, y_train)
rmse, lasso_name = get_scores(lasso, X_test, y_test)
print (rmse, '(' + lasso_name + ')')
en = ElasticNet(random_state=0).fit(X_train, y_train)
rmse, en_name = get_scores(en, X_test, y_test)
print (rmse, '(' + en_name + ')')
scaler = StandardScaler()
X_train_std = scaler.fit_transform(X_train)
X_test_std = scaler.fit_transform(X_test)
sgdr_std = SGDRegressor(random_state=0, max_iter=1000, tol=0.001)
sgdr_std.fit(X_train_std, y_train)
rmse, sgdr_name = get_scores(sgdr_std, X_test_std, y_test)
print (rmse, '(' + sgdr_name + ' - scaled)')
Listing 4-5Exploring boston data with regression algorithms
执行清单 4-5 的输出应该如下所示:
feature shape (490, 13)
target shape (490,)
rmse:
3.37169151536684 (RandomForestRegressor)
4.236710574387242 (LinearRegression)
4.2526986026173486 (Ridge)
5.097231463859832 (Lasso)
4.88844846745213 (ElasticNet)
4.410035683951274 (SGDRegressor - scaled)
代码从导入必需的包开始。函数 get_scores 返回 RMSE 和算法名。主程序块从加载 NumPy 文件中的数据开始,并将其分成训练测试子集。然后用 RandomForestRegressor、LinearRegression、Ridge、Lasso、ElasticNet 和 SGDRegressor 训练数据。请注意,RandomForestRegressor 优于所有正则化算法,因为它的 RMSE 最低。
回归葡萄酒数据
清单 4-6 中显示的第一个代码示例从 CSV 文件加载红酒数据,显示特性重要性,并将数据保存到 NumPy 文件。
import numpy as np, pandas as pd
from sklearn.ensemble import RandomForestRegressor
if __name__ == "__main__":
br = '\n'
f = 'data/redwine.csv'
red_wine = pd.read_csv(f)
X = red_wine.drop(['quality'], axis=1)
y = red_wine['quality']
print (X.shape)
print (y.shape, br)
features = list(X)
rfr = RandomForestRegressor(random_state=0, n_estimators=100)
rfr.fit(X, y)
feature_importances = rfr.feature_importances_
importance = sorted(zip(feature_importances, features), reverse=True)
for row in importance:
print (row)
print ()
print (red_wine[['alcohol', 'sulphates', 'volatile acidity',
'total sulfur dioxide', 'quality']]. head())
X_file = 'data/X_red'
y_file = 'data/y_red'
np.save(X_file, X)
np.save(y_file, y)
Listing 4-6Exploring and saving red wine data
执行清单 4-6 的输出应该如下所示:
(1599, 11)
(1599,)
(0.27432500255956216, 'alcohol')
(0.13700073893077233, 'sulphates')
(0.13053941311188708, 'volatile acidity')
(0.08068199773601588, 'total sulfur dioxide')
(0.06294612644261727, 'chlorides')
(0.057730976351602854, 'pH')
(0.055499749756166, 'residual sugar')
(0.05198192402458334, 'density')
(0.05114079873500658, 'fixed acidity')
(0.049730883807319035, 'free sulfur dioxide')
(0.04842238854446754, 'citric acid')
alcohol sulphates volatile acidity total sulfur dioxide quality
0 9.4 0.56 0.70 34.0 5.0
1 9.8 0.68 0.88 67.0 5.0
2 9.8 0.65 0.76 54.0 5.0
3 9.8 0.58 0.28 60.0 6.0
4 9.4 0.56 0.70 34.0 5.0
代码从导入必需的包开始。主块从 CSV 文件中加载红酒数据。接下来,通过从 Pandas 数据帧中剥离目标列质量来创建特征集 X,然后从质量列中创建目标 y。然后显示 x 和 y 形状。代码最后在 RandomForestRegressor 的帮助下显示特性的重要性,并将数据保存到 NumPy 文件中。
清单 4-7 中显示的下一个代码示例使用各种回归算法对红酒数据进行实验。
import numpy as np, pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression,\
Ridge, Lasso, ElasticNet, SGDRegressor
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline
import matplotlib.pyplot as plt, seaborn as sns
def get_scores(model, Xtest, ytest):
y_pred = model.predict(Xtest)
return np.sqrt(mean_squared_error(ytest, y_pred)),\
model.__class__.__name__
if __name__ == "__main__":
br = '\n'
d = dict()
X = np.load('data/X_red.npy')
y = np.load('data/y_red.npy')
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=0)
print ('rmse (unscaled):')
rfr = RandomForestRegressor(random_state=0, n_estimators=100)
rfr.fit(X_train, y_train)
rmse, rfr_name = get_scores(rfr, X_test, y_test)
d['rfr'] = [rmse]
print (rmse, '(' + rfr_name + ')')
lr = LinearRegression().fit(X_train, y_train)
rmse, lr_name = get_scores(lr, X_test, y_test)
d['lr'] = [rmse]
print (rmse, '(' + lr_name + ')')
ridge = Ridge(random_state=0).fit(X_train, y_train)
rmse, ridge_name = get_scores(ridge, X_test, y_test)
d['ridge'] = [rmse]
print (rmse, '(' + ridge_name + ')')
lasso = Lasso(random_state=0).fit(X_train, y_train)
rmse, lasso_name = get_scores(lasso, X_test, y_test)
d['lasso'] = [rmse]
print (rmse, '(' + lasso_name + ')')
en = ElasticNet(random_state=0).fit(X_train, y_train)
rmse, en_name = get_scores(en, X_test, y_test)
d['en'] = [rmse]
print (rmse, '(' + en_name + ')')
sgdr = SGDRegressor(random_state=0, max_iter=1000, tol=0.001)
sgdr.fit(X_train, y_train)
rmse, sgdr_name = get_scores(sgdr, X_test, y_test)
print (rmse, '(' + sgdr_name + ')', br)
scaler = StandardScaler()
X_train_std = scaler.fit_transform(X_train)
X_test_std = scaler.fit_transform(X_test)
print ('rmse scaled:')
lr_std = LinearRegression().fit(X_train_std, y_train)
rmse, lr_std_name = get_scores(lr_std, X_test_std, y_test)
print (rmse, '(' + lr_std_name + ')')
rr_std = Ridge(random_state=0).fit(X_train_std, y_train)
rmse, rr_std_name = get_scores(rr_std, X_test_std, y_test)
print (rmse, '(' + rr_std_name + ')')
lasso_std = Lasso(random_state=0).fit(X_train_std, y_train)
rmse, lasso_std_name = get_scores(lasso_std, X_test_std, y_test)
print (rmse, '(' + lasso_std_name + ')')
en_std = ElasticNet(random_state=0).fit(X_train_std, y_train)
rmse, en_std_name = get_scores(en_std, X_test_std, y_test)
print (rmse, '(' + en_std_name + ')')
sgdr_std = SGDRegressor(random_state=0, max_iter=1000, tol=0.001)
sgdr_std.fit(X_train_std, y_train)
rmse, sgdr_std_name = get_scores(sgdr_std, X_test_std, y_test)
d['sgdr_std'] = [rmse]
print (rmse, '(' + sgdr_std_name + ')', br)
pipe = Pipeline([('poly', PolynomialFeatures(degree=2)),
('linear', LinearRegression())])
model = pipe.fit(X_train, y_train)
rmse, poly_name = get_scores(model, X_test, y_test)
d['poly'] = [rmse]
print (PolynomialFeatures().__class__.__name__, '(rmse):')
print (rmse, '(' + poly_name + ')')
algo, rmse = [], []
for key, value in d.items():
algo.append(key)
rmse.append(value[0])
plt.figure('RMSE')
sns.set(style="whitegrid")
ax = sns.barplot(algo, rmse)
plt.title('Red Wine Algorithm Comparison')
plt.xlabel('regressor')
plt.ylabel('RMSE')
plt.show()
Listing 4-7Exploring red wine data with regression algorithms
执行清单 4-7 的输出应该如下所示:
rmse (unscaled):
0.5694654840286635 (RandomForestRegressor)
0.6200574149384266 (LinearRegression)
0.6185762657415644 (Ridge)
0.7455442007369433 (Lasso)
0.7450232657227877 (ElasticNet)
51120537008.37402 (SGDRegressor)
rmse scaled:
0.6216027053463463 (LinearRegression)
0.6215826846730879 (Ridge)
0.7584549718351333 (Lasso)
0.7584549718351333 (ElasticNet)
0.6234205584462227 (SGDRegressor)
PolynomialFeatures (rmse):
0.6382400985644077 (Pipeline)
列表 4-7 也显示图 4-1 。图 4-1 显示了本实验中使用的算法的 RMSE 分数。
图 4-1
红酒 RMSE 评分对比
代码从导入多项式特性、管道、seaborn 和其他必需的包开始。函数 get_scores 返回 RMSE 和型号名称。主程序块首先创建一个字典来存储来自训练实验的最佳 RMSE 分数。算法训练数据有无缩放,最好的分数保存在字典 d 中。接下来,数据被分成训练测试子集。
代码继续使用 LinearRegression、Ridge、Lasso、ElasticNet、SGDRegressor 和 RandomForestRegressor 训练未缩放的数据。在该数据集上性能最好的算法是 RandomForestRegressor,其 RMSE 约为 0.569。然后代码用线性回归、岭、套索、弹性网和 SGDRegressor 训练缩放的数据。在该数据集上,性能最好的算法是 RMSE 约为 0.622 的 Ridge 算法,这并不比其未缩放的 RMSE 更好。尽管 RMSE·斯格雷索不是表现最好的,但请注意缩放对算法的影响有多大!最后,用多项式特征训练未缩放的数据。
多项式特性提供了一个通过转换输入而不是改进模型来提高性能的机会。多项式回归允许不同程度的输入的线性组合。在本例中,我们对输入进行平方(度数=2)以探究对性能的影响。为了训练数据,我们将多项式特征转化为线性回归。
我们可以用 degree 做实验,看看性能会发生什么变化。我们可以立方体输入(度数=3),四倍输入(度数=4),等等。多项式模型对于非线性机器学习实验非常有用,但要小心高阶多项式模型,因为它们通常表现不佳。也就是说,它们会产生不必要的剧烈波动。正则化可以减轻多项式的不当行为。
多项式特征的训练是通过将转换后的输入(平方数据)转化为线性回归来完成的。该代码通过基于存储在字典 d 中的最佳 RMSE 分数创建可视化来结束。
清单 4-8 中显示的下一个代码示例用多项式拟合进行实验。在前面的示例中,我们对输入数据求平方,训练模型,并计算 RMSE。在这个模型中,我们采用输入数据的二次(平方)、三次和四次幂,训练每个模型,并计算和显示 RMSE 进行比较。与之前代码的另一个变化是,我们用多项式特征算法来转换训练和测试数据,而不是将多项式特征转化为线性回归。然后,我们用转换后的数据训练线性回归。一旦数据被训练,我们显示每个实验的 RMSE。
import numpy as np, pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import PolynomialFeatures
def get_scores(model, Xtest, ytest):
y_pred = model.predict(Xtest)
return np.sqrt(mean_squared_error(ytest, y_pred)),\
model.__class__.__name__
if __name__ == "__main__":
br = '\n'
d = dict()
X = np.load('data/X_red.npy')
y = np.load('data/y_red.npy')
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=0)
poly = PolynomialFeatures(degree=2)
poly.fit(X_train, y_train)
X_train_poly = poly.transform(X_train)
lr = LinearRegression().fit(X_train_poly, y_train)
X_test_poly = poly.transform(X_test)
rmse, lr_name = get_scores(lr, X_test_poly, y_test)
print (rmse, '(squared polynomial fitting)')
poly = PolynomialFeatures(degree=3)
poly.fit(X_train, y_train)
X_train_poly = poly.transform(X_train)
lr = LinearRegression().fit(X_train_poly, y_train)
X_test_poly = poly.transform(X_test)
rmse, lr_name = get_scores(lr, X_test_poly, y_test)
print (rmse, '(cubic polynomial fitting)')
poly = PolynomialFeatures(degree=4)
poly.fit(X_train, y_train)
X_train_poly = poly.transform(X_train)
lr = LinearRegression().fit(X_train_poly, y_train)
X_test_poly = poly.transform(X_test)
rmse, lr_name = get_scores(lr, X_test_poly, y_test)
print (rmse, '(quartic polynomial fitting)')
Listing 4-8Polynomial fitting with red wine data
执行清单 4-8 的输出应该如下所示:
0.6382400985644077 (squared polynomial fitting)
0.8284645679714848 (cubic polynomial fitting)
97.85391125320886 (quartic polynomial fitting)
代码导入必需的包。函数 get_scores 返回 RMSE 和型号名称。主程序块将数据加载到 X 和 y 中。它继续将数据分割成训练测试子集。接下来,用输入数据的平方(次数=2)用多项式特征拟合训练和测试数据。然后转换训练数据。线性回归对转换后的数据进行训练,并显示 RMSE。度=3 和度=4 时遵循相同的过程。对于此数据集,平方输入可提供最佳 RMSE。
小费
多项式特征对于非线性数据集建模是一种非常有用的技术,并且易于实现。
清单 4-9 中显示的下一个代码示例从 CSV 文件中加载白酒数据,显示特性重要性,并将数据保存到 NumPy 文件中。
import numpy as np, pandas as pd
from sklearn.ensemble import RandomForestRegressor
if __name__ == "__main__":
br = '\n'
f = 'data/whitewine.csv'
white_wine = pd.read_csv(f)
X = white_wine.drop(['quality'], axis=1)
y = white_wine['quality']
print (X.shape)
print (y.shape, br)
features = list(X)
rfr = RandomForestRegressor(random_state=0, n_estimators=100)
rfr.fit(X, y)
feature_importances = rfr.feature_importances_
importance = sorted(zip(feature_importances, features), reverse=True)
for row in importance:
print (row)
print ()
print (white_wine[['alcohol', 'sulphates', 'volatile acidity', 'total sulfur dioxide', 'quality']]. head())
X_file = 'data/X_white'
y_file = 'data/y_white'
np.save(X_file, X)
np.save(y_file, y)
Listing 4-9Exploring and saving white wine data
执行清单 4-9 的输出应该如下所示:
(4898, 11)
(4898,)
(0.24186185906056268, 'alcohol')
(0.1251626059551235, 'volatile acidity')
(0.11524332271725685, 'free sulfur dioxide')
(0.07170261049200727, 'pH')
(0.06940456299270928, 'total sulfur dioxide')
(0.06899334812486085, 'residual sugar')
(0.06259740092261244, 'chlorides')
(0.06227404207074219, 'sulphates')
(0.061557623671947746, 'density')
(0.060982526101159625, 'citric acid')
(0.060220097891017656, 'fixed acidity')
alcohol sulphates volatile acidity total sulfur dioxide quality
0 8.8 0.45 0.27 170.0 6.0
1 9.5 0.49 0.30 132.0 6.0
2 10.1 0.44 0.28 97.0 6.0
3 9.9 0.40 0.23 186.0 6.0
4 9.9 0.40 0.23 186.0 6.0
代码从导入必需的包开始。主程序块从 CSV 文件中加载白酒数据。接下来,通过从 Pandas 数据帧中剥离目标列质量来创建特征集 X,并且从质量列中创建目标 y。然后显示 x 和 y 形状。请注意,白葡萄酒数据集由 4898 个数据元素组成,而红葡萄酒数据集只有 1599 个。代码最后在 RandomForestRegressor 的帮助下显示特性的重要性,并将数据保存到 NumPy 文件中。
清单 4-10 中显示的最终代码示例使用各种回归算法对白酒数据进行了实验。
import numpy as np, pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression,\
Ridge, Lasso, ElasticNet, SGDRegressor
from sklearn.metrics import mean_squared_error
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import Pipeline
import matplotlib.pyplot as plt, seaborn as sns
def get_scores(model, Xtest, ytest):
y_pred = model.predict(Xtest)
return np.sqrt(mean_squared_error(ytest, y_pred)),\
model.__class__.__name__
if __name__ == "__main__":
br = '\n'
d = dict()
X = np.load('data/X_white.npy')
y = np.load('data/y_white.npy')
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=0)
print ('rmse (unscaled):')
rfr = RandomForestRegressor(random_state=0, n_estimators=100)
rfr.fit(X_train, y_train)
rmse, rfr_name = get_scores(rfr, X_test, y_test)
d['rfr'] = [rmse]
print (rmse, '(' + rfr_name + ')')
lr = LinearRegression().fit(X_train, y_train)
rmse, lr_name = get_scores(lr, X_test, y_test)
d['lr'] = [rmse]
print (rmse, '(' + lr_name + ')')
ridge = Ridge(random_state=0).fit(X_train, y_train)
rmse, ridge_name = get_scores(ridge, X_test, y_test)
d['ridge'] = [rmse]
print (rmse, '(' + ridge_name + ')')
lasso = Lasso(random_state=0).fit(X_train, y_train)
rmse, lasso_name = get_scores(lasso, X_test, y_test)
d['lasso'] = [rmse]
print (rmse, '(' + lasso_name + ')')
en = ElasticNet(random_state=0).fit(X_train, y_train)
rmse, en_name = get_scores(en, X_test, y_test)
d['en'] = [rmse]
print (rmse, '(' + en_name + ')', br)
scaler = StandardScaler()
X_train_std = scaler.fit_transform(X_train)
X_test_std = scaler.fit_transform(X_test)
print ('rmse scaled:')
sgd = SGDRegressor(max_iter=1000, tol=0.001, random_state=0)
sgd.fit(X_train_std, y_train)
rmse, sgd_name = get_scores(sgd, X_test_std, y_test)
d['sgd'] = [rmse]
print (rmse, '(' + sgd_name + ')', br)
pipe = Pipeline([('poly', PolynomialFeatures(degree=2)),
('linear', LinearRegression())])
model = pipe.fit(X_train, y_train)
rmse, pf_name = get_scores(model, X_test, y_test)
d['poly'] = [rmse]
print (PolynomialFeatures().__class__.__name__,'(rmse):')
print (rmse, '(' + pf_name + ')')
poly = PolynomialFeatures(degree=2)
poly.fit(X_train, y_train)
X_train_poly = poly.transform(X_train)
lr = LinearRegression().fit(X_train_poly, y_train)
X_test_poly = poly.transform(X_test)
rmse, lr_name = get_scores(lr, X_test_poly, y_test)
print (rmse, '(without Pipeline)')
algo, rmse = [], []
for key, value in d.items():
algo.append(key)
rmse.append(value[0])
plt.figure('RMSE')
sns.set(style="whitegrid")
ax = sns.barplot(algo, rmse)
plt.title('White Wine Algorithm Comparison')
plt.xlabel('regressor')
plt.ylabel('RMSE')
plt.show()
Listing 4-10Exploring white wine data with regression algorithms
执行清单 4-10 的输出应该如下所示:
rmse (unscaled):
0.687111151629689 (RandomForestRegressor)
0.8123086554972433 (LinearRegression)
0.8141615403447382 (Ridge)
0.9255803421282806 (Lasso)
0.9242810596011943 (ElasticNet)
rmse scaled:
0.8092835779827245 (SGDRegressor)
PolynomialFeatures (rmse):
0.7767527802246017 (Pipeline)
0.7767527802246017 (without Pipeline)
列表 4-10 也显示图 4-2 。图 4-2 显示了本实验中使用的算法的 RMSE 分数。
图 4-2
白葡萄酒 RMSE 评分对比
代码从导入必需的包开始。函数 get_scores 返回 RMSE 和型号名称。主程序块首先创建一个字典来存储来自训练实验的最佳 RMSE 分数。它继续将白葡萄酒数据从 NumPy 文件加载到 X 和 y 中。然后,数据被分成训练测试子集。接下来,算法在缩放和不缩放的情况下训练数据,最佳分数保存在字典 d 中。
代码继续使用 LinearRegression、Ridge、Lasso、ElasticNet、SGDRegressor 和 RandomForestRegressor 训练未缩放的数据。在该数据集上性能最好的算法是 RandomForestRegressor,其 RMSE 约为 0.687。然后代码用 SGDRegressor 训练缩放后的数据。我已经确定(通过此处未显示的实验)山脊、套索和弹性网的 RMSE 不会随着缩放而改进,所以我没有包含代码。最后,用有和没有流水线的多项式特征来训练未缩放的数据。因为两个 RMSE 分数是相同的,所以使用哪种技术并不重要。
五、简单训练集的分类器调优
调优是在不过度拟合、欠拟合或产生高方差的情况下最大化算法性能的过程。过度拟合是指算法训练数据过于精确,以至于可能无法拟合新数据或可靠地预测未来结果。当模型对于它试图训练的数据来说过于复杂时,通常会发生过度拟合。过于复杂的模型可以很好地训练数据,但也会拟合不属于数据的噪声。因此,当用于训练新数据时,会引入噪声,导致不可预测的结果。
欠拟合是指算法无法充分捕捉数据的底层结构。拟合不足的模型表现不佳,因为它不够复杂,无法捕捉数据的含义。高方差是指算法在预测中引入过多误差,导致性能不佳。
高性能调整是通过从模型中选择最佳超参数来实现的。模型超参数是模型外部的配置,其值无法从数据中估计。大多数机器学习算法都有一组超参数。有些算法很少,有些算法很多。超参数越少的算法越容易调优,因为需要考虑的调整越少。
调整机器学习算法非常困难,因为它通常是一个非直观、耗时且系统的试错过程。由于必须在训练开始前手动设置超参数,因此难度增加。通过阅读学术文章、行业书籍、在线文章(例如 Scikit-Learn 文档)、观看 YouTube 视频、数据和数据集经验、勤奋、努力工作以及简单的实践,可以增强调优专业知识。
为我们的调整示例选择的机器学习算法不是巧合。我选择它们是基于许多小时的实验、阅读和洞察力。对于给定的数据集,表现最好的算法被包括在内,表现差的算法则不包括在内。
Scikit-Learn 提供了两种优化超参数调优的工具:GridSearchCV 和 RandomizedSearchCV。 GridSearchCV 对估计器(或机器学习算法)的指定参数值进行彻底搜索,并返回最佳性能的超参数组合。
因此,我们需要做的就是指定我们想要试验的超参数及其值的范围,GridSearchCV 使用交叉验证来执行超参数值的所有可能组合。因此,我们自然会限制超参数的选择及其取值范围。理论上,我们可以为模型的所有超参数指定一组参数值,但是这种搜索会消耗大量的计算机资源和时间。
RandomizedSearchCV 基于超参数的预定子集进行评估,从给定域中随机选择选定数量的超参数对,并仅测试那些被选择的超参数对。RandomizedSearchCV 的计算成本和耗时更少,因为它不会评估每个可能的超参数组合。这种方法极大地简化了分析,而没有明显牺牲优化。对于高维数据,RandomizedSearchCV 通常是一个很好的选择,因为它可以非常快速地返回一个好的超参数组合。
小费
在给定足够的计算资源的情况下,使用 GridSearchCV 进行调优适合于彻底搜索性能最佳的超参数。使用 RandomizedSearchCV 进行调优适用于良好的搜索,或者如果调优高维数据。
学习调整分类器可以通过使用各种数据集和分类器的例子来加速。但是,我也建议遵循结构化流程:
-
总是从使用基线算法的默认超参数开始。
-
尝试训练和测试规模。
-
处理高维数据时使用降维。
-
处理大型数据集时,随机抽取样本。
-
缩放数据(在适当的情况下)以潜在地提高性能。
-
使用 GridSearchCV 或 RandomizedSearchCV 进行调整。
-
一旦使用基线算法进行了调整,就可以使用复杂的算法进行实验。
一个基线算法具有很少的超参数,因此很容易调整。它还允许我们在预测建模问题上建立基线性能。使用基线提供了一个与更高级算法的比较点,您可以在稍后的调优过程中对这些算法进行评估。
小费
从基线算法(及其默认的超参数)开始调优,以建立基线性能。
一旦在样本数据上创建了优化的算法,如果有足够的计算机资源可用,就可以对整个数据集进行实验。虽然这样的实验可能会很昂贵,但至少我们有一个很好的模型可以使用。想象一下没有采样的试错实验的代价!
因为调优是一项复杂的工作,所以通过处理简单的数据集来学习是一个好主意。简单来说,我们指的是低维的和小的数据集。首先,调整简单的数据允许在没有大量计算费用或时间的情况下进行实验。简单数据集上的试错调优实验消耗相对较少的计算机和人员时间。第二,简单的数据集易于处理和理解。第三,我们可以通过 GridSearchCV 或 RandomizedSearchCV 使用许多(如果不是全部)超参数。
调整数据集
我们专注于四个数据集:虹膜、数字、银行和葡萄酒。Iris 数据集由 150 个数据元素组成,代表三种 Iris。数字数据集由 1797 个数字图像组成。银行数据集由 41188 个代表客户订阅的数据元素组成。葡萄酒数据集由 178 个代表葡萄酒质量的数据元素组成。
调谐虹膜数据
清单 5-1 中所示的代码示例使用 KNeighborsClassifier、GridSearchCV 和 RandomizedSearchCV 来训练和调整 load_iris。因为我们在下面的代码片段中使用了人性化的软件包,并且它通常不是预装的,所以请按如下方式安装:
import numpy as np, humanfriendly as hf
import time
from sklearn.datasets import load_iris
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import RandomizedSearchCV
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.model_selection import cross_val_score
def get_scores(model, Xtrain, ytrain, Xtest, ytest):
y_pred = model.predict(Xtrain)
train = accuracy_score(ytrain, y_pred)
y_pred = model.predict(Xtest)
test = accuracy_score(ytest, y_pred)
return train, test, model.__class__.__name__
def get_cross(model, data, target, groups=10):
return cross_val_score(model, data, target, cv=groups)
def see_time(note):
end = time.perf_counter()
elapsed = end - start
print (note, hf.format_timespan(elapsed, detailed=True))
if __name__ == "__main__":
br = '\n'
iris = load_iris()
X = iris.data
y = iris.target
targets = iris.target_names
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
knn = KNeighborsClassifier()
print (knn, br)
distances = [1, 2, 3, 4, 5]
k_range = list(range(1, 31))
leaf = [10]
param_grid = dict(n_neighbors=k_range, p=distances, leaf_size=leaf)
start = time.perf_counter()
grid = GridSearchCV(knn, param_grid, cv=10, scoring="accuracy")
grid.fit(X, y)
see_time('GridSearchCV total tuning time:')
bp = grid.best_params_
print ()
print ('best parameters:')
print (bp, br)
knn_best = KNeighborsClassifier(**bp).fit(X_train, y_train)
train, test, name = get_scores(knn_best, X_train, y_train
, X_test, y_test)
print (name, 'train/test scores (GridSearchCV):')
print (train, test, br)
scores = get_cross(knn, X, y)
print ('cross-val scores:')
print (scores, br)
print ('avg cross-val score:', np.mean(scores), br)
d = grid.cv_results_
print ('mean grid score:', np.mean(d['mean_test_score']), br)
vector = [[3, 5, 4, 2]]
vectors = [[2, 5, 3, 5], [1, 4, 2, 1]]
y_pred = knn_best.predict(vector)
print (targets[y_pred])
y_preds = knn_best.predict(vectors)
print (targets[y_preds], br)
start = time.perf_counter()
rand = RandomizedSearchCV(knn, param_grid, cv=10, random_state=0, scoring="accuracy", n_iter=10)
rand.fit(X, y)
see_time('RandomizedSearchCV total tuning time:')
bp = rand.best_params_
print()
print ('best parameters:')
print (bp, br)
knn_best = KNeighborsClassifier(**bp).fit(X_train, y_train)
train, test, name = get_scores(knn_best, X_train, y_train, X_test, y_test)
print (name, 'train/test scores (RandomizedSearchCV):')
print (train, test)
Listing 5-1Tuning Iris data with KNeighborsClassifier
pip install humanfriendly
继续执行清单 5-1 中的代码。请记住,您可以从本书的示例下载中找到示例。您不需要手动键入示例。更容易访问示例下载和复制/粘贴。
执行清单 5-1 的输出应该如下所示:
KNeighborsClassifier(algorithm='auto', leaf_size=30, metric="minkowski", metric_params=None, n_jobs=None, n_neighbors=5, p=2, weights="uniform")
GridSearchCV total tuning time: 7 seconds and 388.94 milliseconds
best parameters:
{'leaf_size': 10, 'n_neighbors': 6, 'p': 3}
KNeighborsClassifier train/test scores (GridSearchCV):
0.9732142857142857 0.9736842105263158
cross-val scores:
[1\. 0.93333333 1\. 1\. 0.86666667 0.93333333
0.93333333 1\. 1\. 1\. ]
avg cross-val score: 0.9666666666666668
mean grid score: 0.9673333333333333
['versicolor']
['versicolor' 'setosa']
RandomizedSearchCV total tuning time: 473.88 milliseconds
best parameters:
{'p': 3, 'n_neighbors': 13, 'leaf_size': 10}
KNeighborsClassifier train/test scores (RandomizedSearchCV):
0.9642857142857143 0.9736842105263158
代码从导入 GridSearchCV、RandomizedSearchCV、cross_val_score 和其他必需的包开始。函数 get_scores 返回训练测试精度和算法名称。函数 get_cross 返回交叉验证分数。函数 see_time 返回经过的时间。主模块加载数据,并将其分成训练测试子集。然后显示 KNeighborsClassifier 的超参数。
小费
在调整之前显示算法的超参数总是一个好主意。
我们通过调整 p 、 leaf_size 和 n_neighbors 来调优 KNeighborsClassifier。
小费
Scikit-Learn 和其他在线文档是了解算法的超参数的好地方。
p 是用于调整距离的闵可夫斯基度量的功率参数。调整叶子 _ 大小以减少邻居的候选数量。调整 n_neighbors 控制邻居数量。
代码继续为 GridSearchCV 创建参数网格。我们想测试距离(或 p)从 1 到 5,n_neighbors 从 1 到 31,leaf_size 为 10。通过将列表[10]分配给 leaf_size,我们覆盖了它的默认值。
小费
如果搜索中不包括超参数,则使用其默认值。如果搜索中包含单个值(作为列表),则其值会覆盖默认值,但不会增加搜索的计算开销。
接下来,我们使用 GridSearchCV 进行调优。注意,我们使用 X 和 y,因为 GridSearchCV 自己对数据集进行交叉验证。我们继续显示最佳参数。然后,我们使用 KNeighborsClassifier 的最佳参数。
结果非常好,因为分数超过 97%,几乎完全符合。我们对训练数据进行交叉验证,以了解一个算法应该执行得有多好。交叉验证分数略低于 97%,这意味着我们的结果是可靠的。调整实验的准确度应该接近或超过交叉验证分数。
小费
交叉验证分数接近我们可以从算法中获得的最佳性能。所以,我们的表现应该接近或者有望更好。
然后我们计算并显示 GridSearchCV 的平均分数。用我们调整过的模型,我们做一些预测。代码以使用具有相同参数网格的 RandomizedSearchCV 进行调优结束。分数几乎相同,但是使用 RandomizedSearchCV 所用的时间要好得多!
调谐数字数据
清单 5-2 中所示的代码示例使用 KNeighborsClassifier、LogisticRegression 和 GridSearchCV 来训练和调整 load_digits。
import numpy as np, humanfriendly as hf
import time
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split,\
cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import GridSearchCV
def get_scores(model, Xtrain, ytrain, Xtest, ytest):
y_pred = model.predict(Xtrain)
train = accuracy_score(ytrain, y_pred)
y_pred = model.predict(Xtest)
test = accuracy_score(ytest, y_pred)
return train, test, model.__class__.__name__
def get_cross(model, data, target, groups=10):
return cross_val_score(model, data, target, cv=groups)
def see_time(note):
end = time.perf_counter()
elapsed = end - start
print (note, hf.format_timespan(elapsed, detailed=True))
if __name__ == "__main__":
br = '\n'
digits = load_digits()
X = digits.data
y = digits.target
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
knn = KNeighborsClassifier().fit(X_train, y_train)
print (knn, br)
train, test, name = get_scores(knn, X_train, y_train, X_test, y_test)
knn_name, acc1, acc2 = name, train, test
print (str(knn_name) + ':')
print ('train:', np.round(acc1, 2),
'test:', np.round(acc2, 2), br)
param_grid = {'n_neighbors': np.arange(1, 31, 2),
'metric': ['euclidean', 'cityblock']}
start = time.perf_counter()
grid = GridSearchCV(knn, param_grid, cv=5, n_jobs=-1)
grid.fit(X, y)
see_time('GridSearchCV total tuning time:')
best_params = grid.best_params_
print (best_params, br)
knn_tuned = KNeighborsClassifier(**best_params)
knn_tuned.fit(X_train, y_train)
train, test, name = get_scores(knn_tuned, X_train, y_train, X_test, y_test)
knn_name, acc1, acc2 = name, train, test
print (knn_name + ' (tuned):')
print ('train:', np.round(acc1, 2),
'test:', np.round(acc2, 2), br)
lr = LogisticRegression(random_state=0, max_iter=4000,
multi_class='auto'
, solver="lbfgs")
print (lr, br)
lr.fit(X_train, y_train)
train, test, name = get_scores(lr, X_train, y_train, X_test, y_test)
lr_name, acc1, acc2 = name, train, test
print (lr_name + ':')
print ('train:', np.round(acc1, 2),
'test:', np.round(acc2, 2), br)
param_grid = {'penalty': ['l2'],
'solver': ['newton-cg', 'lbfgs', 'sag'],
'max_iter': [4000], 'multi_class': ['auto'],
'C': [0.001, 0.01, 0.1]}
start = time.perf_counter()
grid = GridSearchCV(lr, param_grid, cv=5, n_jobs=-1)
grid.fit(X, y)
see_time('GridSearchCV total tuning time:')
bp = grid.best_params_
print (bp)
lr_tuned = LogisticRegression(**bp, random_state=0)
lr_tuned.fit(X_train, y_train)
train, test, name = get_scores(lr_tuned, X_train, y_train, X_test, y_test)
lr_name, acc1, acc2 = name, train, test
print (lr_name + ' (tuned):')
print ('train:', np.round(acc1, 2),
'test:', np.round(acc2, 2), br)
print ('cross-validation score knn:')
knn = KNeighborsClassifier()
scores = get_cross(knn, X, y)
print (np.mean(scores))
Listing 5-2Tuning digits data with two algorithms
执行清单 5-2 的输出应该如下所示:
KNeighborsClassifier(algorithm='auto', leaf_size=30,
metric='minkowski', metric_params=None, n_jobs=None,
n_neighbors=5, p=2, weights="uniform")
KNeighborsClassifier:
train: 0.99 test: 0.98
GridSearchCV total tuning time: 9 seconds and 609.98 milliseconds
{'metric': 'euclidean', 'n_neighbors': 3}
KNeighborsClassifier (tuned):
train: 0.99 test: 0.99
LogisticRegression(C=1.0, class_weight=None, dual=False,
fit_intercept=True, intercept_scaling=1, max_iter=4000,
multi_class='auto', n_jobs=None, penalty="l2",
random_state=0, solver="lbfgs", tol=0.0001, verbose=0,
warm_start=False)
LogisticRegression:
train: 1.0 test: 0.95
GridSearchCV total tuning time: 12 seconds and 708.79 milliseconds
{'C': 0.01, 'max_iter': 4000, 'multi_class': 'auto', 'penalty': 'l2', 'solver': 'lbfgs'}
LogisticRegression (tuned):
train: 0.99 test: 0.96
cross-validation score knn:
0.9739482872546906
代码从导入必需的包开始。函数 get_scores 返回精确度分数和模型名称。函数 see_time 返回经过的时间。主模块将数字数据加载到 X 和 y 中,并将其分成训练测试子集。接下来,KNeighborsClassifier(使用默认超参数)对数据进行训练,并显示结果。
代码继续使用 GridSearchCV 进行调优。对于调优实验,KNeighborsClassifier 是模型,我们调整 n_neighbors 和度量超参数。根据我的经验和研究,邻居的数量是 KNeighborsClassifier 需要调整的最重要的超参数。超参数度量是用于树的距离。
调整提高了性能,因为我们有一个理想的拟合。当然,load_digits 经过了大量的预处理,这使得它很容易调优。然而,调优是如此复杂,以至于从简单的数据集学习基础知识是一个好主意。
代码继续使用 LogisticRegression 进行调优。算法(及其默认参数)太复杂,因为结果显示过度拟合。也就是说,该算法完美地训练了数据,但是测试集精度相当低。
小费
当测试精度比训练精度低很多时,训练算法对于数据集来说太复杂,因此会发生过拟合。
用 GridSearchCV 调优 LogisticRegression 减少了过度拟合,但在数据上的表现不如 KNeighborsClassifier。本次调优实验调整的超参数包括罚值、解算器、 max_iter 和 C 。罚值涉及正则化的类型。解算器指定优化过程中使用的算法。 max_iter 超参数表示求解器收敛所需的最大迭代次数。最后, C 表示正则化强度的倒数。C 值越小,正则化越强。
代码以使用 KNeighborsClassifier 进行交叉验证结束。我们用这个算法进行了交叉验证,因为它在数据上表现最好。交叉验证是用于评估机器学习模型性能的重采样程序。在这种情况下,我们的性能接近 99%,比交叉验证分数要好。因此,我们确信我们的最佳模型(KNeighborsClassifier)的性能是最佳的。如果交叉验证与我们最好的建模实验非常不同,我们可能做错了什么,或者需要继续调整。
小费
如果有足够的计算资源,交叉验证是测试算法准确性的一种很好的技术。
虽然 LogisticRegression 的性能不如 KNeighborsClassifier,但调优确实提高了性能。也就是说,测试性能向上调整,而训练性能向下调整。当调整将训练和测试性能相互调整时,我们就取得了进展。但是,如果我们仍然对性能不满意,我们应该继续试验。但是,至少我们正朝着积极的方向前进。
小费
随着调优实验将训练和测试分数相互调整,我们知道我们的调优实验正在取得进展。
调谐库数据
清单 5-3 中显示的第一个代码示例使用 svm.SVC 调优从银行数据集中抽取的随机样本。
import numpy as np, humanfriendly as hf, random
import time
from sklearn.model_selection import train_test_split,\
RandomizedSearchCV, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
def get_scores(model, xtrain, ytrain, xtest, ytest):
ypred = model.predict(xtest)
train = model.score(xtrain, ytrain)
test = model.score(xtest, y_test)
name = model.__class__.__name__
return (name, train, test)
def get_cross(model, data, target, groups=10):
return cross_val_score(model, data, target, cv=groups)
def prep_data(data, target):
d = [data[i] for i, _ in enumerate(data)]
t = [target[i] for i, _ in enumerate(target)]
return list(zip(d, t))
def create_sample(d, n, replace="yes"):
if replace == 'yes': s = random.sample(d, n)
else: s = [random.choice(d)
for i, _ in enumerate(d) if i < n]
Xs = [row[0] for i, row in enumerate(s)]
ys = [row[1] for i, row in enumerate(s)]
return np.array(Xs), np.array(ys)
def see_time(note):
end = time.perf_counter()
elapsed = end - start
print (note, hf.format_timespan(elapsed, detailed=True))
if __name__ == "__main__":
br = '\n'
X = np.load('data/X_bank.npy')
y = np.load('data/y_bank.npy')
sample_size = 4000
data = prep_data(X, y)
Xs, ys = create_sample(data, sample_size, replace="no")
Xs = StandardScaler().fit_transform(Xs)
X_train, X_test, y_train, y_test = train_test_split\
(Xs, ys, random_state=0)
svm = SVC(gamma='scale', random_state=0)
print (svm, br)
svm.fit(X_train, y_train)
svm_scores = get_scores(svm, X_train, y_train, X_test, y_test)
print (svm_scores[0] + ' (train, test):')
print (svm_scores[1], svm_scores[2], br)
Cs = [0.0001, 0.001]
param_grid = {'C': Cs}
start = time.perf_counter()
rand = RandomizedSearchCV(svm, param_grid, cv=3, n_jobs = -1, random_state=0, verbose=2, n_iter=2)
rand.fit(X, y)
see_time('RandomizedSearchCV total tuning time:')
bp = rand.best_params_
print (bp, br)
svm_tuned = SVC(**bp, gamma="scale", random_state=0)
svm_tuned.fit(X_train, y_train)
svm_scores = get_scores(svm_tuned, X_train, y_train, X_test, y_test)
print (svm_scores[0] + ' (train, test):')
print (svm_scores[1], svm_scores[2], br)
print ('cross-validation score:')
svm = SVC(gamma='scale')
scores = get_cross(svm, Xs, ys)
print (np.mean(scores))
Listing 5-3Tuning a bank data random sample with svm.SVC
执行清单 5-3 的输出应该如下所示:
SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
decision_function_shape='ovr', degree=3, gamma="scale",
kernel='rbf', max_iter=-1, probability=False, random_state=0,
shrinking=True, tol=0.001, verbose=False)
SVC (train, test):
0.949 0.893
Fitting 3 folds for each of 2 candidates, totalling 6 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
[Parallel(n_jobs=-1)]: Done 3 out of 6 | elapsed: 55.4s remaining: 55.4s
[Parallel(n_jobs=-1)]: Done 6 out of 6 | elapsed: 57.0s finished
RandomizedSearchCV total tuning time: 1 minute, 31 seconds and 171.06 milliseconds
{'C': 0.0001}
SVC (train, test):
0.891 0.875
cross-validation score:
0.9102441546509665
代码从导入 RandomizedSearchCV,svm 开始。SVC 和其他必备的包。函数 get_scores 返回精确度分数和模型名称。函数 prep_data 为函数 create_sample 中的样本处理准备数据。函数 create_sample 创建一个随机样本。函数 see_time 返回经过的时间。
主程序块加载数据,创建 4000 个无替换的样本,并将数据分成训练测试子集。接下来,我们缩放数据,用 svm 进行训练。SVC(使用默认超参数),并显示结果。代码通过调整 svm 继续。随机搜索。
我们只调整 C 超参数,它是一个正则化参数,控制在训练数据上实现低误差和最小化权重范数之间的权衡。当我们增加 C 时,模型的复杂性增加,这增加了过度拟合的机会。另请注意,verbose 设置为 2(verbose = 2)。
详细参数(而非超参数)控制详细程度。我们设置的数字越高,收到的消息就越多。因此,在执行时,我们会注意到关于调优过程中发生了什么的消息。GridSearchCV 还有一个 verbosity 选项。
仅仅通过调整 C 的两个值,运行时间已经超过一分钟了!然而,我们似乎达到了很好的契合。交叉验证分数证实了我们在 svm 方面做得很好。SVC,但是通过更多的调优实验可以做得更好。也就是说,我们也许能够从 svm 中挤出更多的性能。SVC 与更多的实验。
小费
如果我们最好的模型测试分数接近交叉验证分数,我们就不需要继续调优了。
通过大量的调优实验,我能够大幅降低调优的复杂性。我不仅仅从两个 C 值开始,也不仅仅用 C 进行调优。此外,我最初尝试用整个数据集进行调优,但发现计算开销太大,所以我用一个样本进行了训练。
清单 5-4 中显示的下一个代码示例使用 KNeighborsClassifier 调优从银行数据集中抽取的随机样本。
import numpy as np, humanfriendly as hf, random
import time
from sklearn.model_selection import train_test_split,\
RandomizedSearchCV, cross_val_score
from sklearn.neighbors import KNeighborsClassifier
def get_scores(model, xtrain, ytrain, xtest, ytest):
ypred = model.predict(xtest)
train = model.score(xtrain, ytrain)
test = model.score(xtest, y_test)
name = model.__class__.__name__
return (name, train, test)
def get_cross(model, data, target, groups=10):
return cross_val_score(model, data, target, cv=groups)
def prep_data(data, target):
d = [data[i] for i, _ in enumerate(data)]
t = [target[i] for i, _ in enumerate(target)]
return list(zip(d, t))
def create_sample(d, n, replace="yes"):
if replace == 'yes': s = random.sample(d, n)
else: s = [random.choice(d)
for i, _ in enumerate(d) if i < n]
Xs = [row[0] for i, row in enumerate(s)]
ys = [row[1] for i, row in enumerate(s)]
return np.array(Xs), np.array(ys)
def see_time(note):
end = time.perf_counter()
elapsed = end - start
print (note, hf.format_timespan(elapsed, detailed=True))
if __name__ == "__main__":
br = '\n'
X = np.load('data/X_bank.npy')
y = np.load('data/y_bank.npy')
sample_size = 4000
data = prep_data(X, y)
Xs, ys = create_sample(data, sample_size, replace="no")
X_train, X_test, y_train, y_test = train_test_split\
(Xs, ys, random_state=0)
knn = KNeighborsClassifier()
print (knn, br)
knn.fit(X_train, y_train)
knn_scores = get_scores(knn, X_train, y_train, X_test, y_test)
print (knn_scores[0] + ' (train, test):')
print (knn_scores[1], knn_scores[2], br)
param_grid = {'n_neighbors': np.arange(1, 31, 2),
'metric': ['euclidean']}
start = time.perf_counter()
rand = RandomizedSearchCV(knn, param_grid, cv=3, n_jobs = -1,
random_state=0, verbose=2)
rand.fit(X, y)
see_time('RandomizedSearchCV total tuning time:')
bp = rand.best_params_
print (bp, br)
file = 'data/bp_bank'
np.save(file, bp)
knn_tuned = KNeighborsClassifier(**bp).fit(X_train
, y_train)
knn_scores = get_scores(knn_tuned, X_train, y_train, X_test, y_test)
print (knn_scores[0] + ' (train, test):')
print (knn_scores[1], knn_scores[2], br)
print ('cross-validation score:')
knn = KNeighborsClassifier()
scores = get_cross(knn, Xs, ys)
print (np.mean(scores))
Listing 5-4Tuning a bank data random sample with KNeighborsClassifier
执行清单 5-4 的输出应该如下所示:
KNeighborsClassifier(algorithm='auto', leaf_size=30,
metric='minkowski', metric_params=None, n_jobs=None,
n_neighbors=5, p=2, weights="uniform")
KNeighborsClassifier (train, test):
0.927 0.906
Fitting 3 folds for each of 10 candidates, totalling 30 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
[Parallel(n_jobs=-1)]: Done 30 out of 30 | elapsed: 59.6s finished
RandomizedSearchCV total tuning time: 1 minute and 654.85 milliseconds
{'n_neighbors': 29, 'metric': 'euclidean'}
KNeighborsClassifier (train, test):
0.913 0.91
cross-validation score:
0.9032489046806542
代码从导入必需的包开始。函数 get_scores 返回精确度分数和模型名称。函数 prep_data 为函数 create_sample 中的样本处理准备数据。函数 create_sample 创建一个随机样本。函数 see_time 返回经过的时间。
主程序块加载数据,创建 4000 个无替换的样本,并将数据分成训练测试子集。接下来,我们使用 KNeighborsClassifier(使用默认的超参数)进行训练,并显示结果。代码通过用 RandomizedSearchCV 调谐来继续。我们调整 n_neighbors 并强制度量为欧几里得。我们还保存了最佳参数,供下一个代码示例使用。
通过实验,我发现欧几里德效果最好。调整 KNeighborsClassifier 提供了比使用默认参数的模型更好的拟合。交叉验证分数证实了我们做得很好,因为它非常接近我们的测试分数。请注意,我们确实使用了 10 个折叠进行交叉验证。我的经验表明,5 或 10 的交叉验证似乎效果很好。但是,要小心,因为更多的交叉验证会增加处理时间。
清单 5-5 中显示的本节中的最终代码示例使用 KNeighborsClassifier 和从之前的调优练习中获得的最佳参数对整个银行数据集进行建模。
import numpy as np, humanfriendly as hf, random
import time
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
def get_scores(model, xtrain, ytrain, xtest, ytest):
ypred = model.predict(xtest)
train = model.score(xtrain, ytrain)
test = model.score(xtest, y_test)
name = model.__class__.__name__
return (name, train, test)
def see_time(note):
end = time.perf_counter()
elapsed = end - start
print (note, hf.format_timespan(elapsed, detailed=True))
if __name__ == "__main__":
br = '\n'
X = np.load('data/X_bank.npy')
y = np.load('data/y_bank.npy')
bp = np.load('data/bp_bank.npy')
bp = bp.tolist()
print ('best parameters:')
print (bp, br)
X_train, X_test, y_train, y_test = train_test_split\
(X, y, random_state=0)
start = time.perf_counter()
knn = KNeighborsClassifier(**bp)
knn.fit(X_train, y_train)
see_time('training time:')
start = time.perf_counter()
knn_scores = get_scores(knn, X_train, y_train, X_test, y_test)
see_time('scoring time:')
print ()
print (knn_scores[0] + ' (train, test):')
print (knn_scores[1], knn_scores[2])
Listing 5-5Tuning bank data with KNeighborsClassifier
执行清单 5-5 的输出应该如下所示:
best parameters:
{'n_neighbors': 29, 'metric': 'euclidean'}
training time: 461.58 milliseconds
scoring time: 10 seconds and 62.98 milliseconds
KNeighborsClassifier (train, test):
0.9154769997733968 0.9138584053607847
该代码示例导入必需的包。函数 get_scores 返回精确度分数和模型名称。函数 see_time 返回经过的时间。主程序块为 KNeighborsClassifier 加载银行数据和最佳参数。接下来,数据被分成训练测试子集。代码以使用最佳参数训练模型并显示结果结束。
结果表明,我们实现了非常好的拟合,整个处理时间不到 11 秒!因此,用随机样本进行调优是减少计算开销的一个好方法。
小费
随机采样是一种计算成本低廉的调整方式。
调整葡萄酒数据
清单 5-6 中显示的第一个代码示例利用了 load_wine 上的 SGDClassifier 和 LinearDiscriminantAnalysis。
import numpy as np, random
from sklearn.datasets import load_wine
from sklearn.preprocessing import StandardScaler
from sklearn.discriminant_analysis\
import LinearDiscriminantAnalysis as LDA
from sklearn.linear_model import SGDClassifier
from sklearn.model_selection import train_test_split,\
cross_val_score
from sklearn import metrics
def get_cross(model, data, target, groups=10):
return cross_val_score(model, data, target, cv=groups)
if __name__ == "__main__":
br = '\n'
wine = load_wine()
X = wine.data
y = wine.target
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
lda = LDA().fit(X_train, y_train)
print (lda, br)
lda_name = lda.__class__.__name__
y_pred = lda.predict(X_train)
accuracy = metrics.accuracy_score(y_train, y_pred)
accuracy = str(accuracy * 100) + '%'
print (lda_name + ':')
print ('train:', accuracy)
y_pred_test = lda.predict(X_test)
accuracy = metrics.accuracy_score(y_test, y_pred_test)
accuracy = str(round(accuracy * 100, 2)) + '%'
print ('test: ', accuracy, br)
print ('cross-validation:')
scores = get_cross(lda, X, y)
print (np.mean(scores), br)
n, ls = 100, []
for i, row in enumerate(range(n)):
rs = random.randint(1, 100)
sgd = LDA().fit(X_train, y_train)
y_pred = lda.predict(X_test)
accuracy = metrics.accuracy_score(y_test, y_pred)
ls.append(accuracy)
avg = sum(ls) / len(ls)
print ('MCS')
print (avg, br)
X = StandardScaler().fit_transform(X)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
sgd = SGDClassifier(max_iter=100, random_state=1)
print (sgd, br)
sgd.fit(X_train, y_train)
sgd_name = sgd.__class__.__name__
y_pred = sgd.predict(X_train)
y_pred_test = sgd.predict(X_test)
print (sgd_name + ':')
print('train: {:.2%}'.format(metrics.accuracy_score\
(y_train, y_pred)))
print('test: {:.2%}\n'.format(metrics.accuracy_score\
(y_test, y_pred_test)))
print ('cross-validation:')
scores = get_cross(sgd, X, y)
print (np.mean(scores), br)
n, ls = 100, []
for i, row in enumerate(range(n)):
rs = random.randint(1, 100)
sgd = SGDClassifier(max_iter=100).fit(X_train, y_train)
y_pred = sgd.predict(X_test)
accuracy = metrics.accuracy_score(y_test, y_pred)
ls.append(accuracy)
avg = sum(ls) / len(ls)
print ('MCS:')
print (avg)
Listing 5-6Exploring wine data with two classifiers
执行清单 5-6 的输出应该如下所示:
LinearDiscriminantAnalysis(n_components=None, priors=None,
shrinkage=None, solver="svd",
store_covariance=False, tol=0.0001)
LinearDiscriminantAnalysis:
train: 100.0%
test: 97.78%
cross-validation:
0.9832989336085312
MCS
0.9777777777777754
SGDClassifier(alpha=0.0001, average=False, class_weight=None,
early_stopping=False, epsilon=0.1, eta0=0.0,
fit_intercept=True, l1_ratio=0.15,
learning_rate='optimal', loss="hinge", max_iter=100,
n_iter=None, n_iter_no_change=5, n_jobs=None,
penalty='l2', power_t=0.5, random_state=1, shuffle=True,
tol=None, validation_fraction=0.1, verbose=0,
warm_start=False)
SGDClassifier:
train: 100.00%
test: 97.78%
cross-validation:
0.9616959064327485
MCS:
0.9966666666666663
代码从导入 LinearDiscriminantAnalysis、SGDClassifier 和其他必需的包开始。主模块加载 wine 数据,将其分成训练测试集,并使用线性判别分析进行训练。代码继续显示准确性分数、交叉验证和 MCS 分数。
交叉验证和 MCS 分数表明,调整很可能不会增加线性判别分析的测试准确性。所以,我们不会开始调谐实验。然而,这个例子确实证明了在不进行调整实验的情况下获得高精度分数是可能的。但是,请记住,load_wine 数据集是经过大量处理的。在工业中,数据很少如此干净或处理得如此漂亮。
代码的下一部分用 SGDClassifier 对数据进行定型。请注意,我们在将数据分成训练测试子集之前对其进行了缩放。我在没有缩放的情况下运行了一个实验,获得了非常差的结果。因此,SGDClassifier 往往会从数据缩放中受益匪浅。
线性判别分析的缩放数据不会改变结果,因此我们在该实验中没有使用缩放数据。同样,SGDClassifier 在没有调优的情况下,精度得分非常高。然而,在取得满分时要谨慎。我做了几个实验,调整了随机状态参数,结果分数变了。当然,变化不是很大,但是训练测试分数并不完美。
尽管计算成本很高,但 MCS 是一个很好的指标,可以反映算法在数据集上的表现。对于 load_wine 数据,MCS 不是问题,因为数据集很小,并且不是由高维数据组成的。交叉验证也是算法性能的一个优秀指标,但它往往比 MCS 更保守。它在计算上也比 MCS 便宜得多。
我们可以调整随机状态参数来修改结果。通过将 SGD 分类器上的随机状态从 0 改为 1,测试准确度下降到 97.78%。这是运行交叉验证和 MCS(给定足够的计算资源)的另一个原因,以了解数据集上给定算法的基线准确性。
小费
调整随机状态参数会改变评分结果,因此运行交叉验证以建立稳定的基线准确度分数始终是一个好主意。
清单 5-7 中显示的最后一个代码示例用各种分类器对 load_wine 进行了一个实验。我用这个例子来演示如何探索给定数据集的算法的可行性。当然,我们必须考虑计算开销。但是,考虑到进行这种实验的资源,从长远来看,我们可能会节省时间和金钱。
from sklearn.datasets import load_wine
from sklearn.neighbors import KNeighborsClassifier as knn
from sklearn.svm import SVC
from sklearn.gaussian_process import\
GaussianProcessClassifier as gpc
from sklearn.gaussian_process.kernels import RBF as rbf
from sklearn.tree import DecisionTreeClassifier as dt
from sklearn.ensemble import RandomForestClassifier as rf,\
AdaBoostClassifier as ada
from sklearn.naive_bayes import GaussianNB as gnb
from sklearn.discriminant_analysis import\
QuadraticDiscriminantAnalysis as qda,\
LinearDiscriminantAnalysis as lda
from sklearn.linear_model import SGDClassifier as sgd
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn import metrics
if __name__ == "__main__":
br = '\n'
wine = load_wine()
X = wine.data
y = wine.target
X = StandardScaler().fit_transform(X)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=.4, random_state=0)
classifiers = [knn(3), qda(), lda(), gnb(),
SVC(kernel='linear', gamma="scale",
random_state=0),
ada(random_state=0), dt(random_state=0),
sgd(max_iter=100, random_state=0),
gpc(1.0 * rbf(1.0), random_state=0),
rf(random_state=0, n_estimators=100)]
for clf in classifiers:
clf.fit(X_train, y_train)
train_score = clf.score(X_train, y_train)
test_score = clf.score(X_test, y_test)
name = clf.__class__.__name__
print (name + '(train/test scores):')
print (train_score, test_score)
Listing 5-7Exploring wine data with a variety of classifiers
执行清单 5-7 的输出应该如下所示:
KNeighborsClassifier(train/test scores):
0.9905660377358491 0.9027777777777778
QuadraticDiscriminantAnalysis(train/test scores):
0.9905660377358491 1.0
LinearDiscriminantAnalysis(train/test scores):
1.0 0.9722222222222222
GaussianNB(train/test scores):
0.9905660377358491 0.9444444444444444
SVC(train/test scores):
1.0 0.9722222222222222
AdaBoostClassifier(train/test scores):
1.0 0.9027777777777778
DecisionTreeClassifier(train/test scores):
1.0 0.9166666666666666
SGDClassifier(train/test scores):
1.0 0.9861111111111112
GaussianProcessClassifier(train/test scores):
1.0 0.9722222222222222
RandomForestClassifier(train/test scores):
1.0 0.9583333333333334
代码首先加载必要的包和各种分类器,包括 SGDClassifier 和 LinearDiscriminantAnalysis(在前面的示例中演示过)。请记住,这个例子只是一个给定适当计算资源的有趣实验。
主模块加载 wine 数据,对其进行缩放,并将其分成训练测试子集。代码继续创建分类器列表。该代码通过遍历分类器列表、用每个分类器训练数据并显示准确度得分来结束。
从结果来看,对于 load_wine 最可行的分类器是 SGD 分类器、线性判别分析、二次判别分析、svm。SVC 和 GaussianProcessClassifier。RandomForestClassifier 和 GaussianNB 也有潜力,但不如首先列出的那些。QuadraticDiscriminantAnalysis 产生了一个几乎完美的分数令人难以置信的拟合!
我们可以调整其他算法来提高它们的性能,但是我的经验和这个实验告诉我,我们应该使用最有前途的算法来节省时间和金钱。这个实验也是接触各种分类算法的好方法。
六、复杂训练集的分类器调优
既然我们已经练习了对低维(或简单)数据的调优,我们就可以开始实验对高维(或复杂)数据集的调优了。低维数据由有限数量的特征组成,而高维数据由非常多的特征组成。
在机器学习文献中,最常用来描述数据集维度的术语是特征空间。特征空间指用于表征数据集的特征集合。也就是说,特征空间指的是变量所在的 n 维空间(不包括目标变量,如果它存在的话)。
与优化低维数据一致,我们在优化高维数据时遵循一个结构化的过程:
-
总是从使用基线算法的默认超参数开始。
-
尝试训练和测试规模。
-
处理高维数据时使用降维。
-
处理大型数据集时,随机抽取样本。
-
缩放数据(在适当的情况下)以潜在地提高性能。
-
使用 GridSearchCV 或 RandomizedSearchCV 进行调整。
-
一旦使用基线算法进行了调整,就可以使用复杂的算法进行实验。
调整数据集
我们专注于三个数据集:fetch_1fw_people、MNIST 和 fetch_20newsgroups。fetch_1fw_people 数据集包含 1288 张人脸图像和 7 个目标。每个人脸图像由一个 50 × 37 的像素矩阵表示。MNIST 数据集包含 70000 个从 0 到 9 的手写数字图像。每个数字由一个 28 × 28 的矩阵表示。fetch_20newsgroups 数据集包含大约 18000 篇关于 20 个主题的帖子。数据被分成训练集和测试集。这种拆分基于特定日期前后发布的消息。
调整 fetch_1fw_people
人脸识别是机器学习中一个非常复杂的话题。但是,Scikit-Learn 提供了 fetch_1fw_people,这是一个很好的数据集,可以在其上进行实验和学习。通过经验和实验,我确定了两种 Scikit-Learn 算法——SGD classifier 和 svm。SVC——它对数据集的处理相对较好。
清单 6-1 中显示的第一个代码示例使用 SGDClassifier 调优数据。
import numpy as np, humanfriendly as hf, warnings
import time
from sklearn.decomposition import PCA
from sklearn.model_selection import train_test_split,\
GridSearchCV, cross_val_score
from sklearn.linear_model import SGDClassifier
from sklearn.metrics import classification_report
def see_time(note):
end = time.perf_counter()
elapsed = end - start
print (note, hf.format_timespan(elapsed, detailed=True))
def get_cross(model, data, target, groups=10):
return cross_val_score(model, data, target, cv=groups)
if __name__ == "__main__":
br = '\n'
warnings.filterwarnings("ignore", category=DeprecationWarning)
X = np.load('data/X_faces.npy')
y = np.load('data/y_faces.npy')
X_train, X_test, y_train, y_test = train_test_split(
X, y, random_state=0)
pca = PCA(n_components=0.95, whiten=True, random_state=1)
pca.fit(X_train)
X_train_pca = pca.transform(X_train)
X_test_pca = pca.transform(X_test)
pca_name = pca.__class__.__name__
print ('<<' + pca_name + '>>')
print ('features (before PCA):', X.shape[1])
print ('features (after PCA):', pca.n_components_, br)
sgd = SGDClassifier(max_iter=1000, tol=.001, random_state=0)
sgd.fit(X_train_pca, y_train)
y_pred = sgd.predict(X_test_pca)
cr = classification_report(y_test, y_pred)
print (cr)
sgd_name = sgd.__class__.__name__
param_grid = {'alpha': [1e-3, 1e-2, 1e-1, 1e0], 'max_iter': [1000], 'loss': ['log', 'perceptron'], 'penalty': ['l1'], 'tol': [.001]}
grid = GridSearchCV(sgd, param_grid, cv=5)
start = time.perf_counter()
grid.fit(X_train_pca, y_train)
see_time('training time:')
print ()
bp = grid.best_params_
print ('best parameters:')
print (bp, br)
sgd = SGDClassifier(**bp, random_state=1)
sgd.fit(X_train_pca, y_train)
y_pred = sgd.predict(X_test_pca)
cr = classification_report(y_test, y_pred)
print (cr)
print ('cross-validation:')
scores = get_cross(sgd, X_train_pca, y_train)
print (np.mean(scores))
Listing 6-1Tuning fetch_1fw_people with SGDClassifier
继续执行清单 6-1 中的代码。请记住,您可以从本书的示例下载中找到示例。您不需要手动键入示例。更容易访问示例下载和复制/粘贴。
执行清单 6-1 的输出应该如下所示:
<<PCA>>
features (before PCA): 1850
features (after PCA): 135
precision recall f1-score support
0 0.89 0.57 0.70 28
1 0.80 0.78 0.79 63
2 0.83 0.62 0.71 24
3 0.73 0.89 0.80 132
4 0.55 0.55 0.55 20
5 0.88 0.32 0.47 22
6 0.67 0.73 0.70 33
micro avg 0.74 0.74 0.74 322
macro avg 0.76 0.64 0.67 322
weighted avg 0.76 0.74 0.73 322
training time: 7 seconds and 745.7 milliseconds
best parameters:
{'alpha': 0.001, 'loss': 'log', 'max_iter': 1000, 'penalty': 'l1', 'tol': 0.001}
precision recall f1-score support
0 0.91 0.71 0.80 28
1 0.79 0.79 0.79 63
2 0.71 0.71 0.71 24
3 0.84 0.86 0.85 132
4 0.48 0.75 0.59 20
5 0.83 0.45 0.59 22
6 0.72 0.79 0.75 33
micro avg 0.78 0.78 0.78 322
macro avg 0.76 0.72 0.73 322
weighted avg 0.79 0.78 0.78 322
cross-validation:
0.7808966616425951
第一个代码示例从导入必备包开始。函数 see_time 返回经过的时间。主块将数据加载到 X 和 y 中,将其分成训练测试子集,并进行 PCA 以降低特征空间维度。
在调整高维数据时,PCA 至关重要,因为它极大地减少了计算开销,同时信息损失最小。然后,代码用 SGDClassifier 训练数据(以获得基线性能度量)并显示结果。接下来,从 GridSearchCV 开始调优。
小费
PCA 是一个重要的调优工具,因为它以最小的信息损失降低了高维数据集的维数,从而大大减少了调优时间(或计算开销)。
我们调整 alpha 、 max_iter 、损耗、惩罚和 tol 超参数。超参数 alpha 是乘以正则项的常数。超参数 max_iter 设置训练数据的最大通过次数(或时期)。一个时期是机器要学习的数据集的一个完整呈现。
超参数损失是指用于实验的损失函数。机器通过损失函数进行学习,这是一种评估算法对给定数据集建模程度的方法。超参数罚值指的是模型使用的正则化项。超参数 tol 是停止标准。
两个最重要的超参数是 alpha 和 penalty,因为它们与模型采用的正则化类型和数量直接相关。
接下来构建参数网格。请注意,α是本实验中调整的临界超参数。通过反复试验,我确定 l1 惩罚是最佳选择,因此我将其硬编码到网格中以减少调优时间。调整后,SGDClassifier 会使用最佳参数对数据进行训练,并显示结果。最后,进行交叉验证,以确保模型以最佳状态运行(确实如此)。
小费
通过一次改变一个或两个超参数,并通过硬编码它们的值来保持其他参数不变,可以更容易(也更快)地进行调优实验。
清单 6-2 中显示的第二个代码示例使用 svm.SVC 调优。SVC 的性能优于 SGDClassifier,但是我想通过在本章中包含第一个代码示例来演示至少一些实验性调优过程中固有的严格性。
import numpy as np, humanfriendly as hf
import time
from sklearn.decomposition import PCA
from sklearn.model_selection import train_test_split,\
GridSearchCV, cross_val_score
from sklearn.svm import SVC
from sklearn.metrics import classification_report
import matplotlib.pyplot as plt
def see_time(note):
end = time.perf_counter()
elapsed = end - start
print (note, hf.format_timespan(elapsed, detailed=True))
def get_cross(model, data, target, groups=10):
return cross_val_score(model, data, target, cv=groups)
if __name__ == "__main__":
br = '\n'
X = np.load('data/X_faces.npy')
y = np.load('data/y_faces.npy')
images = np.load('data/faces_images.npy')
targets = np.load('data/faces_targets.npy')
_, h, w = images.shape
n_images, n_features, n_classes = X.shape[0], X.shape[1],\
len(targets)
X_train, X_test, y_train, y_test = train_test_split(
X, y, random_state=0)
pca = PCA(n_components=0.95, whiten=True, random_state=0)
pca.fit(X_train)
components = pca.n_components_
eigenfaces = pca.components_.reshape((components, h, w))
X_train_pca = pca.transform(X_train)
pca_name = pca.__class__.__name__
print ('<<' + pca_name + '>>')
print ('features (before PCA):', n_features)
print ('features (after PCA):', components, br)
X_i = np.array(eigenfaces[0].reshape(h, w))
fig = plt.figure('eigenface')
ax = fig.subplots()
image = ax.imshow(X_i, cmap="bone")
svm = SVC(random_state=0, gamma="scale")
print (svm, br)
svm.fit(X_train_pca, y_train)
X_test_pca = pca.transform(X_test)
y_pred = svm.predict(X_test_pca)
cr = classification_report(y_test, y_pred)
print (cr)
svm_name = svm.__class__.__name__
param_grid = {'C': [1e2, 1e3, 5e3], 'gamma': [0.001, 0.005, 0.01, 0.1], 'kernel': ['rbf'], 'class_weight': ['balanced']}
grid = GridSearchCV(svm, param_grid, cv=5)
start = time.perf_counter()
grid.fit(X_train_pca, y_train)
see_time('training time:')
print ()
bp = grid.best_params_
print ('best parameters:')
print (bp, br)
svm = SVC(**bp)
svm.fit(X_train_pca, y_train)
y_pred = svm.predict(X_test_pca)
print ()
cr = classification_report(y_test, y_pred)
print (cr, br)
print ('cross-validation:')
scores = get_cross(svm, X_train_pca, y_train)
print (np.mean(scores), br)
file = 'data/bp_face'
np.save(file, bp)
bp = np.load('data/bp_face.npy')
bp = bp.tolist()
print ('best parameters:')
print (bp)
plt.show()
Listing 6-2Tuning fetch_1fw_people with svm.SVC
执行清单 6-2 的输出应该如下所示:
<<PCA>>
features (before PCA): 1850
features (after PCA): 135
SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
decision_function_shape='ovr', degree=3, gamma="scale",
kernel='rbf', max_iter=-1, probability=False, random_state=0,
shrinking=True, tol=0.001, verbose=False)
precision recall f1-score support
0 1.00 0.43 0.60 28
1 0.83 0.87 0.85 63
2 0.94 0.62 0.75 24
3 0.71 0.97 0.82 132
4 1.00 0.70 0.82 20
5 1.00 0.36 0.53 22
6 0.96 0.73 0.83 33
micro avg 0.80 0.80 0.80 322
macro avg 0.92 0.67 0.74 322
weighted avg 0.84 0.80 0.78 322
training time: 18 seconds
and 143.89 milliseconds
best parameters:
{'C': 100.0, 'class_weight': 'balanced', 'gamma': 0.005, 'kernel': 'rbf'}
precision recall f1-score support
0 1.00 0.64 0.78 28
1 0.76 0.92 0.83 63
2 0.91 0.88 0.89 24
3 0.88 0.92 0.90 132
4 0.74 0.85 0.79 20
5 1.00 0.64 0.78 22
6 0.90 0.85 0.88 33
micro avg 0.86 0.86 0.86 322
macro avg 0.89 0.81 0.84 322
weighted avg 0.87 0.86 0.86 322
cross-validation:
0.8393624737627647
best parameters:
{'C': 100.0, 'class_weight': 'balanced', 'gamma': 0.005, 'kernel': 'rbf'}
清单 6-2 还显示了图 6-1 ,这是 PCA 创建的第一个特征脸。
图 6-1
PCA 创建的第一个特征脸
代码从导入必需的包开始。函数 see_time 返回经过的时间。主模块将数据加载到 X 和 y 中,将其分成训练测试子集,并进行 PCA 降维。svm 的基准性能。显示 SVC,以便稍后与调整后的 svm 进行比较。SVC 分数。
通过用 C 、伽马、内核和 class_weight 超参数构建网格来开始调优。超参数 C 是误差项的惩罚参数,所以对于调优非常重要。超参数 gamma 是核系数。超参数内核指定算法使用的内核类型(如线性)。超参数 class_weight 用于设置每个类的权重(或强调)。通过实验,我发现 rbf 内核和平衡类权重是最好的,所以我把它们硬编码到网格里。
我的发现过程如下:首先,我保持所有其他超参数不变,并更改 kernel 以查看产生最佳性能的设置。第二,我保持内核不变,改变了类的权重。
正如您从网格中看到的,我们改变了 C 和 gamma 来提高性能。一旦确定了最佳参数,svm。SVC 用它们训练数据。结果与交叉验证度量一起显示。我们在 svm 方面做得很好。因为我们的表现明显好于交叉验证分数。为了完整性,我们显示降维后的第一个特征脸。最后,保存(并显示)最佳参数。
调优调优调优调优调优调优调优调优调优调优调优调优调优调优调优调优调优调优调优调优调优调优调优调优调
MNIST 不是一个拥有 70000 个样本的大型数据集,但它有一个由 784 个特征组成的高维特征空间。这种特征空间的复杂性增加了计算开销,因此我们在使用计算开销很大的算法(如 svm.SVC)进行实验时必须考虑到这一点。
清单 6-3 中的第一个代码示例使用 RandomForestClassifier 和 ExtraTreesClassifier 调优 MNIST。这些算法有许多超参数,但我们只调整了几个。根据我使用这些算法的经验,我能够极大地简化调优。您可以进一步试验,但是当您调整额外的超参数时,计算开销会大大增加。
import numpy as np, humanfriendly as hf, random
import time
from sklearn.model_selection import train_test_split
from sklearn.model_selection import RandomizedSearchCV,\
cross_val_score
from sklearn.ensemble import RandomForestClassifier,\
ExtraTreesClassifier
def get_scores(model, xtrain, ytrain, xtest, ytest):
ypred = model.predict(xtest)
train = model.score(xtrain, ytrain)
test = model.score(xtest, y_test)
name = model.__class__.__name__
return (name, train, test)
def get_cross(model, data, target, groups=10):
return cross_val_score(model, data, target, cv=groups)
def prep_data(data, target):
d = [data[i] for i, _ in enumerate(data)]
t = [target[i] for i, _ in enumerate(target)]
return list(zip(d, t))
def create_sample(d, n, replace="yes"):
if replace == 'yes': s = random.sample(d, n)
else: s = [random.choice(d) for i, _ in enumerate(d) if i < n]
Xs = [row[0] for i, row in enumerate(s)]
ys = [row[1] for i, row in enumerate(s)]
return np.array(Xs), np.array(ys)
def see_time(note):
end = time.perf_counter()
elapsed = end - start
print (note, hf.format_timespan(elapsed, detailed=True))
if __name__ == "__main__":
br = '\n'
X_file = 'data/X_mnist'
y_file = 'data/y_mnist'
X = np.load('data/X_mnist.npy')
y = np.load('data/y_mnist.npy')
X = X.astype(np.float32)
data = prep_data(X, y)
sample_size = 7000
Xs, ys = create_sample(data, sample_size)
rf = RandomForestClassifier(random_state=0, n_estimators=100)
print (rf, br)
params = {'class_weight': ['balanced'], 'max_depth': [10, 30]}
random = RandomizedSearchCV(rf, param_distributions = params,
cv=3, n_iter=2, random_state=0)
start = time.perf_counter()
random.fit(Xs, ys)
see_time('RandomizedSearchCV total tuning time:')
bp = random.best_params_
print (bp, br)
X_train, X_test, y_train, y_test = train_test_split(
X, y, random_state=0)
rf = RandomForestClassifier(**bp, random_state=0, n_estimators=100)
start = time.perf_counter()
rf.fit(X_train, y_train)
rf_scores = get_scores(rf, X_train, y_train, X_test, y_test)
see_time('total time:')
print (rf_scores[0] + ' (train, test):')
print (rf_scores[1], rf_scores[2], br)
et = ExtraTreesClassifier(random_state=0, n_estimators=200)
print (et, br)
params = {'class_weight': ['balanced'], 'max_depth': [10, 30]}
random = RandomizedSearchCV(et, param_distributions = params,
cv=3, n_iter=2, random_state=0)
start = time.perf_counter()
random.fit(Xs, ys)
see_time('RandomizedSearchCV total tuning time:')
bp = random.best_params_
print (bp, br)
X_train, X_test, y_train, y_test = train_test_split(
X, y, random_state=0)
et = ExtraTreesClassifier(**bp, random_state=0, n_estimators=200)
start = time.perf_counter()
et.fit(X_train, y_train)
et_scores = get_scores(et, X_train, y_train
, X_test, y_test)
see_time('total time:')
print (et_scores[0] + ' (train, test):')
print (et_scores[1], et_scores[2], br)
print ('cross-validation (et):')
start = time.perf_counter()
scores = get_cross(rf, X, y)
see_time('total time:')
print (np.mean(scores), br)
file = 'data/bp_mnist_et'
np.save(file, bp)
bp = np.load('data/bp_mnist_et.npy')
bp = bp.tolist()
print ('best parameters:')
print (bp)
Listing 6-3Tuning with RandomForestClassifier and ExtraTreesClassifier
执行清单 6-3 的输出应该如下所示:
RandomForestClassifier(bootstrap=True, class_weight=None,
criterion='gini', max_depth=None,
max_features='auto', max_leaf_nodes=None,
min_impurity_decrease=0.0, min_impurity_split=None,
min_samples_leaf='deprecated', min_samples_split=2,
min_weight_fraction_leaf='deprecated',
n_estimators=100, n_jobs=None, oob_score=False,
random_state=0, verbose=0, warm_start=False)
RandomizedSearchCV total tuning time: 13 seconds and 398.73 milliseconds
{'max_depth': 30, 'class_weight': 'balanced'}
total time: 32 seconds and 589.23 milliseconds
RandomForestClassifier (train, test):
0.9999809523809524 0.9701142857142857
ExtraTreesClassifier(bootstrap=False, class_weight=None,
criterion='gini', max_depth=None, max_features="auto",
max_leaf_nodes=None, min_impurity_decrease=0.0,
min_impurity_split=None,
min_samples_leaf='deprecated', min_samples_split=2,
min_weight_fraction_leaf='deprecated',
n_estimators=200, n_jobs=None, oob_score=False,
random_state=0, verbose=0, warm_start=False)
RandomizedSearchCV total tuning time: 23 seconds and 342.93 milliseconds
{'max_depth': 30, 'class_weight': 'balanced'}
total time: 1 minute, 8 seconds and 270.59 milliseconds
ExtraTreesClassifier (train, test):
1.0 0.9732
cross-validation (et):
total time: 5 minutes, 40 seconds and 788.07 milliseconds
0.9692001937716965
best parameters:
{'max_depth': 30, 'class_weight': 'balanced'}
代码从导入必需的包开始。函数 get_scores 返回精确度分数和模型名称。函数 get_cross 返回交叉验证分数。函数 prep_data 为函数 create_sample 准备数据。函数 create sample 创建一个有或没有替换的随机样本。函数 see_time 返回经过的时间。主块加载数据,创建随机样本,并实例化算法 RandomForestClassifier。
通过构建具有 class_weight 和 max_depth 超参数的网格开始调整。超参数 class_weight 用于设置每个类的权重(或强调)。超参数 max_depth 用于建立树的最大深度。通过许多小时的实验,我发现这两个参数是提高性能的关键。通过利用 RandomizedSearchCV 获得最佳参数来继续调优。注意,调优时间只有 13 秒多一点,因为网格非常简单。
现在我们可以用最佳参数测试 RandomForestClassifier。请注意,我们在算法中包括了超参数 n_estimators 以及最佳参数。超参数 n_estimators 代表森林中的树木数量,可能是提高性能的最重要的超参数。
出于两个原因,我们在算法中包括了 n 个估计量(而不是把它放在网格中)。首先,它是一个如此重要的超参数,我们可以通过在调整实验之外调整它来节省时间。也就是说,我们可以非常容易地调整它,而不会给调整实验增加计算开销。然而,增加它的值确实增加了处理算法的计算费用。第二,它必须包含在这个算法中,以避免令人讨厌的警告。
优化 ExtraTreesClassifier 遵循完全相同的逻辑,只有一点不同。我们将 n 估计量增加到 200 棵树。请注意,这种增加会导致处理时间增加一倍以上,但性能会更好。
最后,我们运行交叉验证(在 extractreesclassifier 上),并保存来自 extractreesclassifier 的最佳参数以供将来处理。从交叉验证分数中,我们知道我们的准确度分数是可靠的。但是,交叉验证需要超过 5 分钟的处理时间!如果你不想等待,你可以注释掉代码的交叉验证部分。
积极的一面是,交叉验证只需要在一个算法上执行一次。我建议您在开始调优实验之前运行交叉验证。然后,您可以运行试错实验,直到达到或超过交叉验证分数。
小费
交叉验证只需运行一次,因为它不可调整。
总体性能良好,准确率超过 97%,没有太多的过度拟合。但是,不要被我的调优实验误导而产生错误的安全感。调优耗费大量的时间和耐心。我只能给你一些例子和提示,帮助你成为一个更有成就的数据科学家。
我强烈建议进行定时调优实验,尤其是那些计算量很大的实验(比如在各种值范围内使用大量超参数进行调优)。否则,很难知道你的实验进展如何。当我第一次开始调整机器学习算法时,我没有计算实验的时间。我的进度很慢,因为当我不能通过运行时间区分调优实验时,我变得非常沮丧。
小费
总是调整实验时间来衡量进展。
清单 6-4 中显示的下一个代码示例用 svm.SVC 调优 MNIST
import numpy as np, humanfriendly as hf, random
import time
from sklearn.model_selection import train_test_split
from sklearn.model_selection import RandomizedSearchCV
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
def get_scores(model, xtrain, ytrain, xtest, ytest):
ypred = model.predict(xtest)
train = model.score(xtrain, ytrain)
test = model.score(xtest, y_test)
name = model.__class__.__name__
return (name, train, test)
def prep_data(data, target):
d = [data[i] for i, _ in enumerate(data)]
t = [target[i] for i, _ in enumerate(target)]
return list(zip(d, t))
def create_sample(d, n, replace="yes"):
if replace == 'yes': s = random.sample(d, n)
else: s = [random.choice(d) for i, _ in enumerate(d) if i < n]
Xs = [row[0] for i, row in enumerate(s)]
ys = [row[1] for i, row in enumerate(s)]
return np.array(Xs), np.array(ys)
def see_time(note):
end = time.perf_counter()
elapsed = end - start
print (note, hf.format_timespan(elapsed, detailed=True))
if __name__ == "__main__":
br = '\n'
X_file = 'data/X_mnist'
y_file = 'data/y_mnist'
X = np.load('data/X_mnist.npy')
y = np.load('data/y_mnist.npy')
X = X.astype(np.float32)
data = prep_data(X, y)
sample_size = 7000
Xs, ys = create_sample(data, sample_size)
pca = PCA(n_components=0.95, random_state=0)
Xs = StandardScaler().fit_transform(Xs)
Xs_reduced = pca.fit_transform(Xs)
X_train, X_test, y_train, y_test = train_test_split(
Xs_reduced, ys, random_state=0)
svm = SVC(gamma='scale', random_state=0)
print (svm, br)
start = time.perf_counter()
svm.fit(X_train, y_train)
svm_scores = get_scores(svm, X_train, y_train
, X_test, y_test)
print (svm_scores[0] + ' (train, test):')
print (svm_scores[1], svm_scores[2])
see_time('time:')
print ()
param_grid = {'C': [30, 35, 40], 'kernel': ['poly'], 'gamma': ['scale'], 'degree': [3], 'coef0': [0.1]}
start = time.perf_counter()
rand = RandomizedSearchCV(svm, param_grid, cv=3, n_jobs = -1,
random_state=0, n_iter=3, verbose=2)
rand.fit(X_train, y_train)
see_time('RandomizedSearchCV total tuning time:')
bp = rand.best_params_
print (bp, br)
svm = SVC(**bp, random_state=0)
start = time.perf_counter()
svm.fit(X_train, y_train)
svm_scores = get_scores(svm, X_train, y_train, X_test, y_test)
print (svm_scores[0] + ' (train, test):')
print (svm_scores[1], svm_scores[2])
see_time('total time:')
Listing 6-4Tuning MNIST with svm.SVC
执行清单 6-4 的输出应该如下所示:
SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
decision_function_shape='ovr', degree=3, gamma="scale",
kernel='rbf', max_iter=-1, probability=False, random_state=0,
shrinking=True, tol=0.001, verbose=False)
SVC (train, test):
0.9845714285714285 0.9228571428571428
time: 13 seconds and 129.03 milliseconds
Fitting 3 folds for each of 3 candidates, totalling 9 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
[Parallel(n_jobs=-1)]: Done 4 out of 9 | elapsed: 14.0s remaining: 17.6s
[Parallel(n_jobs=-1)]: Done 9 out of 9 | elapsed: 19.3s remaining: 0.0s
[Parallel(n_jobs=-1)]: Done 9 out of 9 | elapsed: 19.3s finished
RandomizedSearchCV total tuning time: 23 seconds and 824.72 milliseconds
{'kernel': 'poly', 'gamma': 'scale', 'degree': 3, 'coef0': 0.1, 'C': 30}
SVC (train, test):
1.0 0.9542857142857143
total time: 10 seconds and 810.06 milliseconds
像第一个 MNIST 调谐码的例子,我们采取随机抽样。但是,由于 svm.SVC 固有的巨大计算开销,我们也使用 PCA 进行降维。
小费
对于计算量大的算法,我们建议使用 PCA 抽取随机样本和进行降维,以加快处理速度。
代码从导入必需的包开始。上一个例子我们已经讲过函数了,这里就不需要讨论了。
主程序块加载数据并随机抽取 7000 个样本。PCA 用于降维,信息损失 5%。接下来,我们缩放训练数据,因为 svm。SVC 对缩放响应良好。代码继续将数据分割成训练测试子集。接下来,svm。用默认参数训练 SVC 以测量性能。
代码继续使用 RandomizedSearchCV 进行调整。我们用超参数 C 、内核、伽马、度数和 coef0 创建一个网格。我们已经讨论了超参数 C、kernel 和 gamma,所以我们不需要在这里重复。超参数次数表示多项式核函数的次数。我们包含它是因为我们选择了 poly 作为内核。超参数 coef0 与多项式内核的度结合使用。
通过实验,我发现超参数 C 是最需要调整的。因此,网格只改变 c 的值。
代码继续使用 svm.SVC 调优实验中的最佳参数。我们能够大幅提高测试性能,但我们仍然面临过度拟合的问题。
我们没有包括交叉验证有两个原因。第一,svm。SVC 的表现没有 ExtraTreeClassifier 好(那还有什么意义?).其次,在 svm 上运行交叉验证需要大量时间。SVC。
调整 fetch _ 新闻组
和人脸识别一样,文本探索是机器学习中一个非常复杂的课题。但是,Scikit-Learn 提供了 fetch_20newsgroups,这是一个很好的数据集,可以在其上进行实验和学习。
因为流水线模型(具有多项式 inb 和 TfidfVectorizer)包括两组超参数(每个算法一个),所以调整复杂性大大增加。
调整多项式 lNB 本身非常容易,因为只需调整 alpha 超参数。超参数阿尔法允许我们调整平滑。然而,调整 TfidfVectorizer 要困难得多,因为它包含大量的超参数。
当用 RandomizedSearchCV 调整流水线模型时,我们会遇到更大的困难,因为超参数的名称是不同的。来自流水线模型的每个超参数必须以算法名为前缀,以便 RandomizedSearchCV 可以正确解释。这是有意义的,因为算法可以共享相同的超参数。
清单 6-5 中显示的代码示例调优了一个流水线模型。
import numpy as np, humanfriendly as hf
import time
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import make_pipeline
from sklearn.metrics import f1_score
from sklearn.model_selection import RandomizedSearchCV,\
cross_val_score
def get_cross(model, data, target, groups=10):
return cross_val_score(model, data, target, cv=groups)
def see_time(note):
end = time.perf_counter()
elapsed = end - start
print (note, hf.format_timespan(elapsed, detailed=True))
if __name__ == "__main__":
br = '\n'
train = fetch_20newsgroups(subset='train')
test = fetch_20newsgroups(subset='test')
categories = ['rec.autos', 'rec.motorcycles', 'sci.space', 'sci.med']
train = fetch_20newsgroups(subset='train', categories=categories, remove=('headers', 'footers', 'quotes'))
test = fetch_20newsgroups(subset='test', categories=categories, remove=('headers', 'footers', 'quotes'))
targets = train.target_names
mnb = MultinomialNB()
tf = TfidfVectorizer()
print (mnb, br)
print (tf, br)
pipe = make_pipeline(tf, mnb)
pipe.fit(train.data, train.target)
labels = pipe.predict(test.data)
f1 = f1_score(test.target, labels, average="micro")
print ('f1_score', f1, br)
print (pipe.get_params().keys(), br)
param_grid = {'tfidfvectorizer__ngram_range': [(1, 1), (1, 2)],
'tfidfvectorizer__use_idf': [True, False],
'multinomialnb__alpha': [1e-2, 1e-3],
'multinomialnb__fit_prior': [True, False]}
start = time.perf_counter()
rand = RandomizedSearchCV(pipe, param_grid, cv=3, n_jobs = -1, random_state=0, n_iter=16, verbose=2)
rand.fit(train.data, train.target)
see_time('RandomizedSearchCV tuning time:')
bp = rand.best_params_
print ()
print ('best parameters:')
print (bp, br)
rbs = rand.best_score_
mnb = MultinomialNB(alpha=0.01)
tf = TfidfVectorizer(ngram_range=(1, 1), use_idf=False)
pipe = make_pipeline(tf, mnb)
pipe.fit(train.data, train.target)
labels = pipe.predict(test.data)
f1 = f1_score(test.target, labels, average="micro")
print ('f1_score', f1, br)
file = 'data/bp_news'
np.save(file, bp)
bp = np.load('data/bp_news.npy')
bp = bp.tolist()
print ('best parameters:')
print (bp, br)
start = time.perf_counter()
scores = get_cross(pipe, train.data, train.target)
see_time('cross-validation:')
print (np.mean(scores))
Listing 6-5Tuning fetch_20newsgroups with a pipelined model
执行清单 6-5 的输出应该如下所示:
MultinomialNB(alpha=1.0, class_prior=None, fit_prior=True)
TfidfVectorizer(analyzer='word', binary=False,
decode_error='strict', dtype=<class 'numpy.float64'>,
encoding='utf-8', input="content", lowercase=True,
max_df=1.0, max_features=None, min_df=1,
ngram_range=(1, 1), norm="l2", preprocessor=None,
smooth_idf=True, stop_words=None, strip_accents=None,
sublinear_tf=False, token_pattern='(?u)\\b\\w\\w+\\b',
tokenizer=None, use_idf=True, vocabulary=None)
f1_score 0.8440656565656567
dict_keys(['memory', 'steps', 'tfidfvectorizer', 'multinomialnb', 'tfidfvectorizer__analyzer', 'tfidfvectorizer__binary', 'tfidfvectorizer__decode_error', 'tfidfvectorizer__dtype', 'tfidfvectorizer__encoding', 'tfidfvectorizer__input', 'tfidfvectorizer__lowercase', 'tfidfvectorizer__max_df', 'tfidfvectorizer__max_features', 'tfidfvectorizer__min_df', 'tfidfvectorizer__ngram_range', 'tfidfvectorizer__norm', 'tfidfvectorizer__preprocessor', 'tfidfvectorizer__smooth_idf', 'tfidfvectorizer__stop_words', 'tfidfvectorizer__strip_accents', 'tfidfvectorizer__sublinear_tf', 'tfidfvectorizer__token_pattern', 'tfidfvectorizer__tokenizer', 'tfidfvectorizer__use_idf', 'tfidfvectorizer__vocabulary', 'multinomialnb__alpha', 'multinomialnb__class_prior', 'multinomialnb__fit_prior'])
Fitting 3 folds for each of 16 candidates, totalling 48 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
[Parallel(n_jobs=-1)]: Done 25 tasks | elapsed: 7.6s
[Parallel(n_jobs=-1)]: Done 48 out of 48 | elapsed: 12.4s finished
RandomizedSearchCV tuning time: 12 seconds and 747.04 milliseconds
best parameters:
{'tfidfvectorizer__use_idf': False, 'tfidfvectorizer__ngram_range': (1, 1), 'multinomialnb__fit_prior': False, 'multinomialnb__alpha': 0.01}
f1_score 0.8611111111111112
best parameters
:
{'tfidfvectorizer__use_idf': False, 'tfidfvectorizer__ngram_range': (1, 1), 'multinomialnb__fit_prior': False, 'multinomialnb__alpha': 0.01}
cross-validation: 2 seconds and 750.36 milliseconds
0.8735201157292913
代码从导入必需的包开始。接下来是函数 get_cross 和 see_time。主程序块首先从 fetch_20newsgroups 数据集创建训练集和测试集。接下来,我们创建子类别并将数据分成训练测试子集。代码继续创建一个基线管道模型,并显示 f1_score,以便稍后与调整后的模型进行比较。
流水线模型的可能超参数可以用 pipe.get_params()显示。按键()。这是重要的一步,因为我们必须包含 RandomizedSearchCV 调优的确切名称。
小费
您可以(也应该)用 model_name.get_params()显示流水线模型的超参数。按键()。
参数网格是用tfidf 矢量器 __ngram_range 、tfidf 矢量器 __use_idf 、多项式 b__alpha 和多项式 b__fit_prior 创建的。
超参数多项式 inb _ _ alpha与多项式 inb 中的 alpha 完全相同。唯一的区别是前缀 multinomialnb 被包含进来,以通知 RandomizedSearchCV 它所属的算法。超参数多项式 inb _ _ fit _ prior表示是否学习类别先验概率。
超参数tfidf 矢量器 __ngram_range 和tfidf 矢量器 __use_idf 属于算法 tfidf 矢量器,如其前缀所示。 ngram_range 表示要从文档中提取的不同 n 元语法的 n 值范围的上下边界。 use_idf 启用或禁用逆文档频率重新加权。
基于参数网格值,从 RandomizedSearchCV 开始调整。通过调整,我们能够将性能提高 86%以上。然而,交叉验证表明我们可以从我们的模型中挤出更多的性能。