迄今为止,我们只是把机器学习模型及其大多数训练算法视为黑盒。但是如果你做了前面几章的一些练习,你可能会惊讶于你可以在不知道任何关于背后原理的情况下完成很多工作:优化一个回归系统,改进一个数字图像分类器,甚至从头开始建立一个垃圾邮件分类器,所有这些都不知道它们的实际工作原理。事实上,在很多情况下,你其实并不需要知道这些实现细节。
然而,深刻理解事物的工作原理,可以帮助你快速找到合适的模型、正确的训练算法,以及一组适合目标任务的超参数。除此之外,也会帮助你更有效地调试问题和进行错误分析。最后,本章所讨论的大部分主题将对理解、构建和训练神经网络(将在本书第二部分中讨论)至关重要。
在本章中,我们将从线性回归模型开始,因为它是最简单的模型之一。我们将讨论两种非常不同的训练方法。
然后,我们将讨论一下多项式回归,这是一个比较复杂的模型,可以拟合非线性的数据集。由于这个模型的参数比线性回归更多,所以容易对训练数据过拟合,我们将讨论如何使用学习曲线来检测是否存在这种情况,以及几种可以降低过拟合风险的正则化方法。
最后,我们再着重讨论两个常用于分类任务的模型:Logistic回归和Softmax回归。
在第一章中,我们研究了一个简单的生活满意度回归模型:
life_satisfaction
该模型为单个输入特征GDP_per_capita的线性函数,其中
更一般地,线性模型是通过简单地计算输入特征的加权平均,并加上一个偏置项(也称为截距项)来进行预测,如公式4-1所示:
上述公式用矢量化的形式可以写得更简洁,如公式4-2所示:
但我们应该如何训练呢?回顾一下,训练模型意味着找出在训练集上最适合模型的参数。为此,我们首先需要一个目标函数,正如我们在第二章中所看到,回归模型最常用的度量标准是均方根误差(RMSE)。为了训练线性回归模型,我们需要寻找最小化RMSE的
在线性假设
为了找到最小化成本函数的
现在我们来生成一些类似线性关系的数据
import numpy as np
X = 2 * np.random.rand(100, 1)
y = 4 + 3 * X + np.random.randn(100, 1)
我们可以使用正规方程进行求解,只需使用NumPy
的线性代数模块(np.linalg
)中的inv()
函数来计算矩阵的逆,并使用dot()
方法进行矩阵乘法。
X_b = np.c_[np.ones((100, 1)), X] # add x0 = 1 to each instance
theta_best = np.linalg.inv(X_b.T.dot(X_b)).dot(X_b.T).dot(y)
我们用来生成数据的函数是
theta_best
# array([[4.21509616],
# [2.77011339]])
我们本来希望
我们来做些预测:
X_new = np.array([[0], [2]])
X_new_b = np.c_[np.ones((2, 1)), X_new] # add x0 = 1 to each instance
y_predict = X_new_b.dot(theta_best)
y_predict
# array([[4.21509616],
# [9.75532293]])
plt.plot(X_new, y_predict, "r-")
plt.plot(X, y, "b.")
plt.axis([0, 2, 0, 15])
plt.show()
使用Scikit-Learn
进行线性回归非常简单。
from sklearn.linear_model import LinearRegression
lin_reg = LinearRegression()
lin_reg.fit(X, y)
lin_reg.intercept_, lin_reg.coef_
# (array([4.21509616]), array([[2.77011339]]))
lin_reg.predict(X_new)
# array([[4.21509616],
# [9.75532293]])
LinearRegression
类是基于scipy.linalg.lstsq()
函数( "最小二乘法"),你可以直接调用它。
theta_best_svd, residuals, rank, s = np.linalg.lstsq(X_b, y, rcond=1e-6)
theta_best_svd
# array([[4.21509616],
# [2.77011339]])
这个函数计算
np.linalg.pinv()
直接计算。
np.linalg.pinv(X_b).dot(y)
# array([[4.21509616],
# [2.77011339]])
伪逆本身的计算是通过奇异值分解(SVD)的方法进行的,该方法将训练集矩阵
numpy.linalg.svd()
)。伪逆的计算公式为
现在我们来看看一种非常不同的训练线性回归模型的方法,这种方法更适合于有大量特征或训练实例太多,无法放入内存的情况。
梯度下降是一种通用的优化算法,能够为各种问题找到最优解,梯度下降的一般思想是反复调整参数来最小化成本函数。
假设你由于浓雾而在山中迷失,你只能感觉到脚下地面的坡度,要想快速到达谷底,一个好的策略就是沿着坡度最陡的方向下山。这正是 "梯度下降 "的思想:通过计算误差函数关于参数向量
具体来说,你先随机初始化参数
梯度下降法的一个重要参数是步长的大小,由超参数学习率所决定。如果学习率太小,那么算法就必须经过多次迭代才能收敛,也就意味着需要很长的时间进行训练(见图4-4)。
另一方面,如果学习率太高,你可能会跳过山谷,最后到了另一边,甚至可能比之前更高了,这可能会导致算法发散(见图4-5)。
最后需要注意的是,并不是所有的代价函数都是规则的碗状结构。代价函数的曲面可能会有洞、山脊、鞍部和各种不规则的地形,这使得收敛到最小值变得困难。图4-6显示了梯度下降的两个主要挑战。如果随机初始化从左边开始,那么它将收敛到局部最小值,这显然不如全局最小值。而如果从右边开始,那么它将需要很长的时间来跨越鞍部,并且如果你过早地停止训练,将永远无法到达全局最小值。
幸运的是,线性回归模型的MSE代价函数恰好是一个凸函数,这意味着如果你在曲线上选取任意两点,则连接它们的线段永远不会与曲线交叉,也就是说没有局部最小值,只有一个全局最小值。此外,它也是一个连续可导的函数,这两个特点可以保证梯度下降法会无限接近全局最小值。
事实上,即使代价函数具有碗的形状,如果特征具有非常不同的尺度大小,它可以是一个拉长的碗。图4-7显示了在特征1和2具有相同尺度的情况下,以及在特征1的取值比特征2小得多的情况下,梯度下降法在训练集上的迭代过程。
正如你所看到的,在左图中,梯度下降法直接向最小值运动,从而快速到达最小值,而在右图中,先是向着与全局最小值方向几乎正交的方向运动,最后沿着一个几乎平坦的山谷移动,虽然最终依然会到达最小值,但这需要很长的时间。
当使用梯度下降法时,你应该确保所有的特征都具有相似的尺度(例如,使用Scikit-Learn
的StandardScaler
类),否则会花费很长的时间来收敛。
这张图也说明了训练一个模型意味着寻找一个模型参数的组合来最小化训练集上的代价函数。而这个过程是在模型的参数空间中进行的:一个模型的参数越多,意味着这个空间的维度就越多,搜索的难度也就越大(大海捞针)。幸运的是,由于在线性回归的情况下,成本函数是凸的,搜索就变得容易多了。
为了实现梯度下降,你需要计算代价函数关于每个模型参数
与其单独计算这些偏导数,不如使用公式4-6一次性计算完梯度向量,记为 n
请注意,上述公式在梯度下降的每一步中都涉及整个训练集的计算!这就是为什么该算法被称为 "批量梯度下降"(Batch Gradient Descent):它在每一步都使用所有训练数据(实际上,完全梯度下降可能是一个更好的名字)。因此,在非常大的训练集上,它的速度会变得非常慢,但是,梯度下降法随着特征数量的增加具有良好的可扩展性:当面对几十万个特征的时侯,使用梯度下降法训练线性回归模型要比使用正规方程或SVD分解法快得多。
一旦有了梯度向量,其指向为上坡,我们所需要做的就是反方向下坡。这意味着从
我们来看看该算法的快速实现:
eta = 0.1 # learning rate
n_iterations = 1000
m = 100
theta = np.random.randn(2,1) # random initialization
for iteration in range(n_iterations):
gradients = 2/m * X_b.T.dot(X_b.dot(theta) - y)
theta = theta - eta * gradients
theta
# array([[4.21509616],
# [2.77011339]])
这正是正规方程所求的! 梯度下降法的效果非常好,但如果你使用了不同的学习率
为了找到一个好的学习率,你可以使用网格搜索(见第2章)。但是,您可能希望限制迭代次数,以便网格搜索可以淘汰那些需要太长时间才能收敛的模型。
你可能想知道如何设置迭代次数。如果它太低,当算法停止时,你仍然会离最优解很远;但如果它太高,你会浪费时间,而模型参数却不再改变。一个简单的解决方案是设置一个非常大的迭代次数,但当梯度向量变得很小,也就是说,当它的范数变得小于一个很小的数值
批量梯度下降的主要问题是其每一步都使用整个训练集来计算梯度,所有当训练集很大时,迭代的速度就会非常慢。相反地,随机梯度下降法在每一步都会在训练集中随机挑选一个实例,并仅基于该实例来计算梯度,显然,每次只处理一个实例会使算法的速度快得多。
另一方面,由于其随机性的特点,该算法比Batch Gradient Descent的规律性要差得多:代价函数不是缓缓下降到达最小值,而是上下跳动,总体上下降。随着时间的推移,它最终会非常接近最小值,但是就算到了最小值,也会继续跳动,永远不会稳定下来(见图4-9)。所以当算法停止时,最终得到的参数值是好的,但并不是最优的。
当代价函数非常不规则时(如图4-6),这实际上可以帮助算法跳出局部最小值,所以随机梯度下降比批量梯度下降有更好的机会找到全局最小值。
因此,随机性对于跳出局部最优值是有利的,但也意味着算法永远无法在最小值处安顿下来。解决该难题的一个办法是学习率衰减。步数一开始很大(有助于加速并跳出局部最小值),然后逐渐减小,让算法在全局最小值处稳定下来。这个过程类似于模拟退火,这种算法的灵感来自于冶金学中的退火过程,即熔融的金属被缓慢冷却。决定每次迭代时学习率的函数称为学习调度。如果学习率降低得太快,可能会卡在局部最小值,甚至中途停止,但如果学习率降低得太慢,可能会在最小值附近跳动很长时间,如果此时过早地停止训练,最终得到的将是一个次优的解。
下面这段代码使用了一个简单的学习调度实现了随机梯度下降。
n_epochs = 50
m = len(X_b)
t0, t1 = 5, 50 # learning schedule hyperparameters
def learning_schedule(t):
return t0 / (t + t1)
theta = np.random.randn(2,1) # random initialization
for epoch in range(n_epochs):
for i in range(m):
random_index = np.random.randint(m)
xi = X_b[random_index:random_index+1]
yi = y[random_index:random_index+1]
gradients = 2 * xi.T.dot(xi.dot(theta) - yi)
eta = learning_schedule(epoch * m + i)
theta = theta - eta * gradients
theta
# array([[4.21076011],
# [2.74856079]])
根据传统惯例,我们以
图4-10展示了训练的前20步:
Figure 4-10. The first 20 steps of Stochastic Gradient Descent注意到,由于实例是随机选取的,所以在每个epoch中, 有些实例可能会被选取几次,而有些实例可能根本不会被选取。如果你想确保训练过程使用到每一个实例,一种方法是对训练集进行洗牌(确保将输入特征和标签一起洗牌),然后逐个实例进行迭代。不过,这种方法一般收敛比较慢。
基于Scikit-Learn
的随机梯度下降进行线性回归,可以使用 SGDRegressor
类,该类默认为优化平方误差代价函数。
下面的代码运行最大epoch
数为1,000,或者直到损失下降小于0.001为止(max_iter=1000, tol=1e-3
),初始学习率为0.1(eta0=0.1
),使用默认的学习调度(与前面的不同),不使用任何正则化(penalty=None
)。
from sklearn.linear_model import SGDRegressor
sgd_reg = SGDRegressor(max_iter=1000, tol=1e-3, penalty=None, eta0=0.1, random_state=42)
sgd_reg.fit(X, y.ravel())
sgd_reg.intercept_, sgd_reg.coef_
# (array([4.24365286]), array([2.8250878]))
最后我们要讲的梯度下降算法叫做小批量梯度下降。如果你知道了批量梯度下降和随机梯度下降,应该就容易理解了:在每一步,小批量梯度下降并不是基于完整的训练集或仅仅基于单个实例来计算梯度,而是在小批量的随机实例集上计算梯度。与随机梯度下降相比,小批量梯度下降的主要优势在于,你可以从矩阵运算的硬件优化中获得性能提升,尤其是在使用GPU时。
此外,与随机梯度下降相比,小批量梯度下降在参数空间的进展并没有那么不稳定,尤其是在相当大的小批量下。因此,小批量梯度下降最终会比随机梯度下降更接近最小值,但同时它可能会更难脱离局部最小值。图4-11展示了三种梯度下降算法在训练过程中的路径,它们最终都在集中最小值附近,但批量梯度下降的路径实际上停在了最小值处,而随机梯度下降和小批量梯度下降都继续在最小值附近摆动。但是,别忘了,批量梯度下降每走一步都需要大量的时间,并且如果你使用一个合适的学习调度,随机梯度下降和小批量梯度下降也会达到最小值。
让我们比较一下到目前为止我们讨论过的线性回归的算法(回忆一下,
如果你的数据比直线更复杂怎么办?你可以使用线性模型来拟合非线性数据,一个简单的方法是将每个特征的幂级添加为新特征,然后在这个扩展的特征集上训练一个线性模型,这就是所谓的多项式回归。
我们来看一个例子,首先,我们根据一个简单的二次方程(加上一些噪声,见图4-12),生成一些非线性数据。
m = 100
X = 6 * np.random.rand(m, 1) - 3
y = 0.5 * X**2 + X + 2 + np.random.randn(m, 1)
显然,直线永远无法正确拟合这些数据,因此,我们使用 Scikit- Learn
的 PolynomialFeatures
类来转换我们的训练数据,并将训练集中每个特征的平方作为新的特征添加进来(本例中只有一个特征)。
from sklearn.preprocessing import PolynomialFeatures
poly_features = PolynomialFeatures(degree=2, include_bias=False)
X_poly = poly_features.fit_transform(X)
X[0]
# array([-0.75275929])
X_poly[0]
# array([-0.75275929, 0.56664654])
现在你可以对这个扩展的训练数据拟合一个LinearRegression
模型(图4-13)。
lin_reg = LinearRegression()
lin_reg.fit(X_poly, y)
lin_reg.intercept_, lin_reg.coef_
# (array([1.78134581]), array([[0.93366893, 0.56456263]]))
还不错,预测模型为:
需要注意的是,当数据具有多个特征时,多项式回归能够找到特征之间的交互关系(这是普通线性回归模型无法做到的)。这是因为PolynomialFeatures
还可以将所有特征的组合到给定的次数。例如,如果有两个特征
PolynomialFeatures
中的 degree=3
,则除了添加特征
PolynomialFeatures(degree=d)
将包含个特征的数组转换成包含个特征的数组。
如果你尝试高次的多项式回归,你可能会比普通线性回归更好地拟合训练数据。图4-14将300次的多项式模型应用于前面的训练数据,并将结果与单纯的线性模型和四元模型(二次多项式)进行比较。注意到300次的多项式模型是摇摆不定的,并尽可能接近训练实例。
高次多项式回归模型对训练数据严重过拟合,而线性模型则欠拟合。在这种情况下,最具泛化能力的模型是二次模型,这是合理的的,因为数据是用二次模型生成的。但是在一般情况下,你不会知道数据是由什么函数生成的,所以你该如何决定你的模型应该有多复杂?如何判断你的模型对数据是过拟合还是欠拟合?
在第2章中,我们使用交叉验证来获得模型泛化性能的估计。如果一个模型在训练数据上表现良好,但根据交叉验证的指标,它的泛化性能很差,那么你的模型就是过拟合。如果在这两方面都表现不佳,那么就是欠拟合,这是判断一个模型太简单或太复杂的一种方法。
另一种方法是观察学习曲线:这些曲线是模型在训练集和验证集上的性能相对于训练集大小(或迭代次数)的函数。下面的代码定义了一个函数,给定一些训练数据,绘制模型的学习曲线。
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
def plot_learning_curves(model, X, y):
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=10)
train_errors, val_errors = [], []
for m in range(1, len(X_train)):
model.fit(X_train[:m], y_train[:m])
y_train_predict = model.predict(X_train[:m])
y_val_predict = model.predict(X_val)
train_errors.append(mean_squared_error(y_train[:m], y_train_predict))
val_errors.append(mean_squared_error(y_val, y_val_predict))
plt.plot(np.sqrt(train_errors), "r-+", linewidth=2, label="train")
plt.plot(np.sqrt(val_errors), "b-", linewidth=3, label="val")
plt.legend(loc="upper right", fontsize=14) # not shown in the book
plt.xlabel("Training set size", fontsize=14) # not shown
plt.ylabel("RMSE", fontsize=14) # not shown
lin_reg = LinearRegression()
plot_learning_curves(lin_reg, X, y)
plt.axis([0, 80, 0, 3]) # not shown in the book
plt.show() # not shown
这是个欠拟合的模型,值得解释一下。首先,我们来看看在训练数据上的表现:当训练集中只有一两个实例时,模型可以完美地拟合它们,这就是为什么曲线从零开始。但是随着新的实例加入到训练集中,模型就不可能完美地拟合训练数据了,这既是因为数据是有噪声的,也是因为数据根本不是线性的。所以训练数据上的误差会一直上升,直到达到一个平稳的状态,这时向训练集添加新的实例并不会使平均误差有多大的变化。现在我们来看看模型在验证数据上的表现,当模型在极少的训练实例上进行训练时,它无法进行正确的泛化,这就是为什么最初的验证误差相当大。然后,当模型被展示更多的训练实例时,它就会学习,因此验证误差会慢慢下降。然而,直线又不能很好地对数据进行建模,所以误差也最终会达到一个平稳状态,非常接近另一条曲线。
这些学习曲线是典型的模型欠拟合,两条曲线都达到了一平缓,很接近,而且相当高。
如果你的模型对训练数据欠拟合,添加更多的训练实例也无济于事,你需要使用更复杂的模型或添加更好的特征。
我们来看看10次多项式模型在相同数据上的学习曲线(图4-16)。
from sklearn.pipeline import Pipeline
polynomial_regression = Pipeline([
("poly_features", PolynomialFeatures(degree=10, include_bias=False)),
("lin_reg", LinearRegression()),
])
plot_learning_curves(polynomial_regression, X, y)
plt.axis([0, 80, 0, 3]) # not shown
plt.show()
这些学习曲线看起来有点像之前的曲线,但有两个非常重要的区别。
偏差/方差权衡
改进过拟合模型的一种方法是给它输入更多的训练数据,直到验证误差逼近训练误差。
统计学和机器学习的一个重要理论成果是,一个模型的泛化误差可以用三个不同的误差之和来表示。 偏差:这部分泛化误差是由于错误的假设造成的,比如假设数据是线性的,而实际上是二次元的。高偏差的模型很有可能是对训练数据欠拟合。 方差:这部分是由于模型对训练数据的微小变化过于敏感。一个具有高自由度的模型(如高次多项式模型)很可能具有较高的方差,从而对训练数据过拟合。
随机误差:这部分是由于数据本身的噪声。减少这部分误差的唯一方法是清理数据(如修复数据源,如破损的传感器,或检测并去除异常值)。
增加一个模型的复杂性通常会增加其方差,减少其偏差。相反,降低一个模型的复杂性会增加它的偏差,减少它的方差。这就是为什么它被称为权衡。
正如我们在第1章和第2章中所看到的,减少过拟合的一个方法是对模型进行正则化(即约束模型):模型的自由度越少,它就越难对数据过拟合。正则化多项式模型的一个简单方法是减少多项式的次数。
对于线性模型来说,正则化通常是通过约束模型的权重来实现的。我们现在来看一下岭回归、拉索回归和弹性网络三种不同的权重约束方式。
岭回归(也叫Tikhonov正则化)是线性回归的正则化版本:在代价函数中加入
训练时使用的代价函数与测试时使用的性能度量标准往往是不同的。除了正则化之外,另一个原因是,一个好的代价函数应该具有良好优化的导数,而用于测试的性能度量应该尽可能地接近最终目标。例如,分类模型通常使用对数损失等代价函数进行训练,但是使用精度/召回率进行评估。
超参数
注意到偏置项
在进行岭回归之前,对数据进行缩放(例如,使用 StandardScaler
)是很重要的,因为它对输入特征的尺度很敏感,实际上大多数正则化模型都是如此。
图4-17展示了使用不同的惩罚系数
PolynomialFeatures(degree=10)
对数据进行扩展,然后使用 StandardScaler
对数据进行缩放,最后对得到的特征应用岭回归模型。注意到,增加
与线性回归一样,我们也可以通过计算闭式解或梯度下降来训练岭回归模型。方程4-9为对应的闭式解,其中
下面使用闭式解来训练Scikit-Learn
的岭回归:
from sklearn.linear_model import Ridge
ridge_reg = Ridge(alpha=1, solver="cholesky", random_state=42)
ridge_reg.fit(X, y)
ridge_reg.predict([[1.5]])
# array([[1.55071465]])
也可以使用随机梯度下降法:
ridge_reg = Ridge(alpha=1, solver="sag", random_state=42)
ridge_reg.fit(X, y)
ridge_reg.predict([[1.5]])
# array([[1.5507201]])
# SGD
sgd_reg = SGDRegressor(penalty="l2", max_iter=1000, tol=1e-3, random_state=42)
sgd_reg.fit(X, y.ravel())
sgd_reg.predict([[1.5]])
# array([1.47012588])
其中超参数penalty
设置要使用的正则化项的类型,指定 "l2 "
表示希望SGD
在代价函数中添加一个正则化项,取值为权重向量的
拉索回归是线性回归的另一个正则化版本:就像岭回归一样,它在代价函数中增加了一个正则化项,但它使用的是权重向量的
与图4-17类似,将岭模型替换为拉索模型,并使用较小的
拉索回归的一个重要特点是它倾向于淘汰不重要的特征的权重(即将其设置为零)。例如,图4-18中右图中的虚线(
你可以通过看图4-19来了解为什么会这样:轴代表模型的两个参数,背景轮廓代表不同的损失函数。在左上角的图中,等高线代表了
底部的两张图显示的是同样的情况,只是惩罚项换成了
在使用拉索回归时,为了避免梯度下降在最后绕着最优值跳动,你需要在训练中逐渐降低学习率(它仍然会绕着最优值跳动,但步子会越来越小,所以会收敛)。
拉索回归的代价函数在
from sklearn.linear_model import Lasso
lasso_reg = Lasso(alpha=0.1)
lasso_reg.fit(X, y)
lasso_reg.predict([[1.5]])
# array([1.53788174])
弹性网络是介于岭回归和拉索回归之间,正则化项是它们正则化项的简单混合,并通过混合比
那么,什么时候应该使用普通的线性回归(即不进行任何正则化)、Ridge、Lasso和Elastic Net?事实上最好还是要有一点正则化,即尽量避免使用纯线性回归。Ridge是一个很好的初始模型,但如果你怀疑只有几个特征是有用的,你应该更偏向于Lasso或Elastic Net,因为它们倾向于将无用特征的权重降到零。一般来说,Elastic Net比Lasso更适用,因为当特征的数量大于训练实例的数量,或者有几个特征具有很强的相关性时,Lasso可能会表现得很不稳定。
from sklearn.linear_model import ElasticNet
elastic_net = ElasticNet(alpha=0.1, l1_ratio=0.5, random_state=42)
elastic_net.fit(X, y)
elastic_net.predict([[1.5]])
# array([1.54333232])
对迭代学习算法(如梯度下降)进行正则化的另一个方法是:当验证误差达到最小值时,立即停止训练,这就是所谓的早停。图4-20展示了一个复杂的模型(在本例中,是一个高阶多项式回归模型)使用批量梯度下降法进行训练,随着时间的推移,模型在训练集和验证集的预测误差(RMSE)都在下降。然而过了一段时间,验证误差不再减少,开始回升,这说明模型已经开始对训练数据过拟合,你只要在验证误差达到最小值时就停止训练即可。就是这样一种简单高效的正则化技术,Geoffrey Hinton称其为 "美妙的免费午餐"。
对于随机和小批量的梯度下降,曲线并不是那么平滑,可能很难知道你是否已经达到了最小值。一个解决方案是,只有在验证误差超过最小值一段时间后(即当你确信模型不会有更好的表现时)才停止,然后返回验证误差最小的模型参数。
from sklearn.base import clone
poly_scaler = Pipeline([
("poly_features", PolynomialFeatures(degree=90, include_bias=False)),
("std_scaler", StandardScaler())
])
X_train_poly_scaled = poly_scaler.fit_transform(X_train)
X_val_poly_scaled = poly_scaler.transform(X_val)
sgd_reg = SGDRegressor(max_iter=1, tol=-np.infty, warm_start=True,
penalty=None, learning_rate="constant", eta0=0.0005, random_state=42)
minimum_val_error = float("inf")
best_epoch = None
best_model = None
for epoch in range(1000):
sgd_reg.fit(X_train_poly_scaled, y_train) # continues where it left off
y_val_predict = sgd_reg.predict(X_val_poly_scaled)
val_error = mean_squared_error(y_val, y_val_predict)
if val_error < minimum_val_error:
minimum_val_error = val_error
best_epoch = epoch
best_model = clone(sgd_reg)
注意到热重启参数warm_start=True
,当调用fit()
函数时,会接着上次训练的结果继续训练,而不是重新训练。
正如我们在第一章中所讨论的那样,一些回归算法可以用于分类(反之亦然)。逻辑回归通常用于估计一个实例属于特定类别的概率(例如,这封邮件是垃圾邮件的概率是多少?)如果估计的概率大于50%,那么该模型就预测该实例属于该类(称为正类,标签为 "1"),否则就预测它不属于该类(即属于负类,标签为 "0")。
那么,逻辑回归是如何工作的呢?如线性回归模型一样,逻辑回归模型计算的是输入特征的加权和(加上一个偏置项),但它并不像线性回归模型那样直接输出结果,而是输出这个结果的logistic(见公式4-13)。
只要逻辑回归模型估计出一个实例
我们知道了逻辑回归模型如何估计概率并进行预测了,但是如何训练呢?训练的目的是寻找参数向量
这个代价函数是合理的,因为当
整个训练集的代价函数是所有训练实例的代价的平均。它可以写成一个单一的表达式,即对数损失,如式4-17所示:
坏消息是,并没有已知的闭式方程来求解最小化这个代价函数的
这个式子看起来很像式4-5:对于每个实例,计算其预测误差,并将其乘以
我们用鸢尾花数据集来了解Logistic回归,该数据集包含了150朵三种不同品种(Iris setosa, Iris versicolor, Iris virginica)的鸢尾花的萼片和花瓣的长度与宽度(见图4-22)。
我们先尝试建立一个仅根据花瓣宽度来检测花朵是否为Iris virginica的二分类器,首先我们来加载数据:
from sklearn import datasets
iris = datasets.load_iris()
list(iris.keys())
# ['data', 'target', 'target_names', 'DESCR', 'feature_names', 'filename']
X = iris["data"][:, 3:] # petal width
y = (iris["target"] == 2).astype(np.int) # 1 if Iris virginica, else 0
然后我们训练一个逻辑回归模型:
from sklearn.linear_model import LogisticRegression
log_reg = LogisticRegression(solver="lbfgs", random_state=42)
log_reg.fit(X, y)
我们来看看模型对于花瓣宽度从0到3厘米不等的花朵的预测概率分布(图4-23)。
X_new = np.linspace(0, 3, 1000).reshape(-1, 1)
y_proba = log_reg.predict_proba(X_new)
decision_boundary = X_new[y_proba[:, 1] >= 0.5][0]
plt.figure(figsize=(8, 3))
plt.plot(X[y==0], y[y==0], "bs")
plt.plot(X[y==1], y[y==1], "g^")
plt.plot([decision_boundary, decision_boundary], [-1, 2], "k:", linewidth=2)
plt.plot(X_new, y_proba[:, 1], "g-", linewidth=2, label="Iris virginica")
plt.plot(X_new, y_proba[:, 0], "b--", linewidth=2, label="Not Iris virginica")
plt.text(decision_boundary+0.02, 0.15, "Decision boundary", fontsize=14, color="k", ha="center")
plt.arrow(decision_boundary, 0.08, -0.3, 0, head_width=0.05, head_length=0.1, fc='b', ec='b')
plt.arrow(decision_boundary, 0.92, 0.3, 0, head_width=0.05, head_length=0.1, fc='g', ec='g')
plt.xlabel("Petal width (cm)", fontsize=14)
plt.ylabel("Probability", fontsize=14)
plt.legend(loc="center left", fontsize=14)
plt.axis([0, 3, -0.02, 1.02])
plt.show()
Iris virginica(三角形)的花瓣宽度从
predict()
方法而不是 predict_proba()
方法),它将返回最有可能的类。因此,在
log_reg.predict([[1.7], [1.5]])
# array([1, 0])
图4-24展示了相同的数据集,但这次展示的是两个特征:花瓣宽度和长度。训练完成后,逻辑回归分类器就可以根据这两个特征来估计新样本是Iris virginica的概率。虚线代表模型估计概率为50%的点:这是模型的决策边界,并注意到,这是一个线性边界。每条平行线代表模型输出特定概率的点,从15%(左下)到90%(右上),根据模型,右上角线以外的所有花都有90%以上的概率是Iris virginica。
如同其他线性模型一样,逻辑回归模型可以使用
Scikit-Learn
默认会增加一个
控制Scikit-Learn
中LogisticRegression
模型正则化强度的超参数不是,而是其倒数:,的值越大,模型的正则化强度越低。
逻辑回归模型可以推广为支持多分类的情形,这就是所谓的Softmax回归。
其思想很简单:当给定一个实例
注意到,每个类都有自己的专用参数向量
只要计算出实例
其中:
与逻辑回归一样,Softmax回归也是预测估计概率最高的类(简单来说就是得分最高的类),如公式4-21所示:
我们的目标是训练一个对目标类具有高概率取值的模型。最小化式4-22所示的代价函数,即交叉熵,是可以达到目标的,因为当模型对目标类的估计概率较低时,它会对模型进行惩罚。交叉熵经常用来衡量预测类概率与目标类的匹配程度。
该代价函数关于
现在,你可以计算每个类的梯度向量,然后使用梯度下降(或任何其他优化算法)找到最小化代价函数的参数矩阵
X = iris["data"][:, (2, 3)] # petal length, petal width
y = iris["target"]
softmax_reg = LogisticRegression(multi_class="multinomial",solver="lbfgs", C=10, random_state=42)
softmax_reg.fit(X, y)
图4-25展示了由此产生的决策边界,用背景颜色表示。注意,任何两个类之间的决策边界都是线性的。图中还显示了Iris versicolor的概率,用曲线表示(例如,标有0.450的线代表45%的概率边界)。