本次实验简述了最小二乘法、最大似然估计、逻辑回归、正则化、验证和学习曲线的基本概念,搭建了基于逻辑回归的线性模型并进行正则化,通过分析 IBMD 数据集的二元分类问题和一个 XOR 问题阐述逻辑回归的优缺点。
在开始学习线性模型之前,简要介绍一下线性回归,首先指定一个模型将因变量 $y$ 和特征联系起来,对线性模型而言,依赖函数的形式如下:
如果为每项观测加上一个虚维度 $x_0 = 1$(比如偏置),那么就可以把 $w_0$ 整合进求和项中,改写为一个略微紧凑的形式:
如果有一个特征观测矩阵,其中矩阵的行是数据集中的观测,那么需要在左边加上一列。由此,线性模型可以定义为:
其中:
上述表达式亦可这样写:
模型具有如下限制(否则它就不是线性回归了):
权重 $w_i$ 的估计 $\widehat{w}_i$ 满足如下条件时,称其为线性:
其中对于 $\forall\ k\ $,$\omega_{ki}$ 仅依赖于 $X$ 中的样本。由于寻求最佳权重的解是一个线性估计,这一模型被称为线性回归。
再引入一项定义:当期望值等于估计参数的真实值时,权重估计被称为无偏(unbiased):
计算这些权重的方法之一是普通最小二乘法(OLS)。OLS 可以最小化因变量实际值和模型给出的预测值之间的均方误差:
为了解决这一优化问题,需要计算模型参数的导数。将导数设为零,然后求解关于 $\textbf w$ 的等式,倘若不熟悉矩阵求导,可以参考下面的 4 个式子:
现在开始计算模型参数的导数:
基于上述的定义和条件,可以说,根据高斯-马尔可夫定理,模型参数的 OLS 估计是所有线性无偏估计中最优的,即通过 OLS 估计可以获得最低的方差。
有人可能会问,为何选择最小化均方误差而不是其他指标?因为若不选择最小化均方误差,那么就不满足高斯-马尔可夫定理的条件,得到的估计将不再是最佳的线性无偏估计。
最大似然估计是解决线性回归问题一种常用方法,下面介绍它的概念。
首先举一个简单的例子,我们想做一个试验判定人们是否记得简单的甲醇化学式 $CH_3OH$。首先调查了 400 人,发现只有 117 个人记得甲醇的化学式。那么,直接将 $\frac{117}{400} \approx 29\%$ 作为估计下一个受访者知道甲醇化学式的概率是较为合理的。这个直观的估计就是一个最大似然估计。为什么会这么估计呢?回忆下伯努利分布的定义:如果一个随机变量只有两个值(1 和 0,相应的概率为 $\theta$ 和 $1 - \theta$),那么该随机变量满足伯努利分布,遵循以下概率分布函数:
这一分布正是我们所需要的,分布参数 $\theta$ 就是「某个人知道甲醇化学式」的概率估计。在 400 个独立试验中,试验的结果记为 $\textbf{x} = \left(x_1, x_2, \ldots, x_{400}\right)$。写下数据的似然,即观测的可能性,比如正好观测到 117 个随机变量 $x = 1$ 和 283 个随机变量 $x = 0$ 的可能性:
$$ p(\textbf{x}; \theta) = \prod_{i=1}^{400} \theta^{x_i} \left(1 - \theta\right)^{\left(1 - x_i\right)} = \theta^{117} \left(1 - \theta\right)^{283}$$
接着,将最大化这一 $\theta$ 的表达式。一般而言,为了简化计算,并不最大化似然 $p(\textbf{x}; \theta)$,转而最大化其对数(这种变换不影响最终答案):
为了找到最大化上式的 $\theta$ 值,将上式对 $\theta$ 求导,并令其为零,求解所得等式:
由上可知,我们的直观估计正好是最大似然估计。现在将这一推理过程应用到线性回归问题上,尝试找出均方误差背后的道理。为此,需要从概率论的角度来看线性回归。我们的模型和之前是一样的:
不过,现在假定随机误差符合均值为零的 正态分布:
据此改写模型:
由于样本是独立抽取的(误差不相关是高斯-马尔可夫定理的条件之一),数据的似然看起来会是密度函数 $p\left(y_i\right)$ 的积。转化为对数形式:
想要找到最大似然假设,即需要最大化表达式 $p\left(\textbf{y} \mid \textbf X; \textbf{w}\right)$ 以得到 $\textbf{w}_{\text{ML}}$,这和最大化其对数是一回事。注意,当针对某个参数最大化函数时,可以丢弃所有不依赖这一参数的变量:
所以,当测量误差服从正态(高斯)分布的情况下, 最小二乘法等价于极大似然估计。
下面讨论线性回归预测的误差性质(可以推广到机器学习算法上),上文提到:
因此,点 $\textbf{x}$ 的误差可分解为:
为了简洁,省略函数的参数,分别考虑每个变量。根据公式 $\text{Var}\left(z\right) = \mathbb{E}\left[z^2\right] - \mathbb{E}\left[z\right]^2$ 可以分解前两项为:
注意:
接着处理和的最后一项。由于误差和目标变量相互独立,所以可以将它们分离,写为:
最后,将上述公式合并为:
由此,从上等式可知,任何线性模型的预测误差由三部分组成:
尽管无法消除 $\sigma^2$,但我们可以影响前两项。理想情况下,希望同时消除偏差和方差(见下图中左上),但是在实践中,常常需要在偏置和不稳定(高方差)间寻找平衡。
一般而言,当模型的计算量增加时(例如,自由参数的数量增加了),估计的方差(分散程度)也会增加,但偏置会下降,这可能会导致过拟合现象。另一方面,如果模型的计算量太少(例如,自由参数过低),这可能会导致欠拟合现象。
高斯-马尔可夫定理表明:在线性模型参数估计问题中,OLS 估计是最佳的线性无偏估计。这意味着,如果存在任何无偏线性模型 g,可以确信 $Var\left(\widehat{f}\right) \leq Var\left(g\right)$。
低偏置和低方差往往是不可兼得的,所以在一些情形下,会为了稳定性(降低模型的方差)而导致模型的偏置 $\text{Var}\left(\widehat{f}\right)$ 提高。高斯-马尔可夫定理成立的条件之一就是矩阵 $\textbf{X}$ 是满秩的,否则 OLS 的解 $\textbf{w} = \left(\textbf{X}^\text{T} \textbf{X}\right)^{-1} \textbf{X}^\text{T} \textbf{y}$ 就不存在,因为逆矩阵 $\left(\textbf{X}^\text{T} \textbf{X}\right)^{-1}$ 不存在,此时矩阵 $\textbf{X}^\text{T} \textbf{X}$ 被称为奇异矩阵或退化矩阵。这类问题被称为病态问题,必须加以矫正,也就是说,矩阵 $\textbf{X}^\text{T} \textbf{X}$ 需要变成非奇异矩阵(这正是这一过程叫做正则化的原因)。
我们常常能在这类数据中观察到所谓的多重共线性:两个或更多特征高度相关,也就是矩阵 $\textbf{X}$ 的列之间存在类似线性依赖的关系(又不完全是线性依赖)。例如,在「基于特征预测房价」这一问题中,属性「含阳台的面积」和「不含阳台的面积」会有一个接近线性依赖的关系。数学上,包含这类数据的矩阵 $\textbf{X}^\text{T} \textbf{X}$ 被称为可逆矩阵,但由于多重共线性,一些本征值(特征值)会接近零。在 $\textbf{X}^\text{T} \textbf{X}$ 的逆矩阵中,因为其本征值为 $\frac{1}{\lambda_i}$,所以有些本征值会变得特别大。本征值这种巨大的数值波动会导致模型参数估计的不稳定,即在训练数据中加入一组新的观测会导致完全不同的解。为了解决上述问题,有一种正则化的方法称为吉洪诺夫(Tikhonov)正则化,大致上是在均方误差中加上一个新变量:
吉洪诺夫矩阵常常表达为单位矩阵乘上一个系数:$\Gamma = \frac{\lambda}{2} E$。在这一情形下,最小化均方误差问题变为一个 L2 正则化问题。若对新的损失函数求导,设所得函数为零,据 $\textbf{w}$ 重整等式,便得到了这一问题的解:
这类回归被称为岭回归(ridge regression)。岭为对角矩阵,在 $\textbf{X}^\text{T} \textbf{X}$ 矩阵上加上这一对角矩阵,以确保能得到一个正则矩阵。
这样的解降低了方差,但增加了偏置,因为参数的正则向量也被最小化了,这导致解朝零移动。在下图中,OLS 解为白色虚线的交点,蓝点表示岭回归的不同解。可以看到,通过增加正则化参数 $\lambda$,使解朝零移动。
线性分类器背后的基本思路是,目标分类的值可以被特征空间中的一个超平面分开。如果这可以无误差地达成,那么训练集被称为线性可分。
上面已经介绍了线性回归和普通最小二乘法(OLS)。现在考虑一个二元分类问题,将目标分类记为「+1」(正面样本)和「-1」(负面样本)。最简单的线性分类器可以通过回归定义:
其中:
逻辑回归是线性分类器的一个特殊情形,但逻辑回归有一个额外的优点:它可以预测样本 $\textbf{x}_\text{i}$ 为分类「+」的概率 $p_+$:
逻辑回归不仅能够预测样本是「+1」还是「-1」,还能预测其分别是「+1」和「-1」的概率是多少。对于很多业务问题(比如,信用评分问题)而言,这是一个非常重要的优点。下面是一个预测贷款违约概率的例子。
银行选择一个阈值 $p_*$ 以预测贷款违约的概率(上图中阈值为0.15),超过阈值就不批准贷款。
为了预测概率 $p_+ \in [0,1]$,使用 OLS 构造线性预测:
为了将所得结果转换为 [0,1] 区间内的概率,逻辑回归使用下列函数进行转换:
使用 Matplotlib 库画出上面这个函数。
import warnings
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
%matplotlib inline
warnings.filterwarnings("ignore")
def sigma(z):
return 1.0 / (1 + np.exp(-z))
xx = np.linspace(-10, 10, 1000)
plt.plot(xx, [sigma(x) for x in xx])
plt.xlabel("z")
plt.ylabel("sigmoid(z)")
plt.title("Sigmoid function")
Text(0.5, 1.0, 'Sigmoid function')
事件 $X$ 的概率记为 $P(X)$,则比值比 $OR(X)$ 由式 $\frac{P(X)}{1-P(X)}$ 决定,比值比是某一事件是否发生的概率之比。显然,概率和比值比包含同样的信息,不过 $P(X)$ 的范围是 0 到 1,而 $OR(X)$ 的范围是 0 到 $\infty$。如果计算 $OR(X)$ 的对数,那么显然有 $\log{OR(X)} \in \mathbb{R}$,这在 OLS 中有用到。
让我们看看逻辑回归是如何做出预测的:
现在,假设已经通过某种方式得到了权重 $\textbf{w}$,即模型已经训练好了,逻辑回归预测的步骤如下:
步骤一 计算:
等式 $\textbf{w}^\text{T}\textbf{x} = 0$ 定义了一个超空间将样本分为两类。
步骤二 计算对数比值比 $OR_{+}$
步骤三 现在已经有了将一个样本分配到「+」分类的概率 $OR_{+}$,可以据此计算 $p_{+}$:
上式的右边就是 sigmoid 函数。
所以,逻辑回归预测一个样本分配为「+」分类的概率(假定已知模型的特征和权重),这一预测过程是通过对权重向量和特征向量的线性组合进行 sigmoid 变换完成的,公式如下:
下面介绍模型是如何被训练的,我们将再次通过最大似然估计训练模型。
现在,看下从最大似然估计(MLE)出发如何进行逻辑回归优化,也就是最小化逻辑损失函数。前面已经见过了将样本分配为「+」分类的逻辑回归模型:
「-」分类相应的表达式为:
这两个表达式可以组合成一个:
表达式 $M(\textbf{x}_\text{i}) = y_i\textbf{w}^T\textbf{x}_\text{i}$ 称为目标 $\textbf{x}_\text{i}$ 的分类边缘。如果边缘非负,则模型正确选择了目标 $\textbf{x}_\text{i}$ 的分类;如果边缘为负,则目标 $\textbf{x}_\text{i}$ 被错误分类了。注意,边缘仅针对训练集中的目标(即标签 $y_i$ 已知的目标)而言。
为了准确地理解为何有这一结论,需要理解向线性分类器的几何解释。首先,看下线性代数的一个经典入门问题「找出向径 $\textbf{x}_A$ 与平面 $\textbf{w}^\text{T}\textbf{x} = 0$ 的距离」,即:
答案:
从答案中,可以看到,表达式 $\textbf{w}^\text{T}\textbf{x}_\text{i}$ 的绝对值越大,点 $\textbf{x}_\text{i}$ 离平面 $\textbf{w}^\text{T}\textbf{x} = 0$ 的距离就越远。
因此,表达式 $M(\textbf{x}_\text{i}) = y_i\textbf{w}^\text{T}\textbf{x}_\text{i}$ 是模型对目标 $\textbf{x}_\text{i}$ 分类的肯定程度:
现在,计算数据集的似然,即基于数据集 $\textbf{x}$ 观测到给定向量 $\textbf{y}$ 的概率。假设目标来自一个独立分布,然后可建立如下公式:
其中,$\ell$ 为数据集 $\textbf{X}$ 的长度(行数)。
对这个表达式取对数,简化计算:
最大化似然等价于最小化以下表达式:
上式就是逻辑损失函数。用分类边缘 $M(\textbf{x}_\text{i})$ 改写逻辑损失函数,有 $L(M) = \log (1 + \exp^{-M})$
将这一函数的图像和 0-1 损失函数的图像绘制在一张图上。当错误分类发生时,0-1 损失函数只会以恒定的数值 1.0 惩罚模型,即 $L_{1/0}(M) = [M < 0]$。
上图体现了这样一个想法:如果不能够直接最小化分类问题的误差数量(至少无法通过梯度方法最小化,因为 0-1 损失函数在 0 的导数趋向无穷),那么可以转而最小化它的上界。对逻辑损失函数而言,以下公式是成立的:
其中 $\mathcal{L_{1/0}} (\textbf X, \textbf{y})$ 只是数据集$(\textbf X, \textbf{y})$ 上权重 $\textbf{w}$ 的逻辑回归误差。因此,可以通过降低分类误差数 $\mathcal{L_{log}}$ 的上限,降低分数误差数本身。
逻辑回归的 L2 正则化和岭回归的情况基本一样。代替 $\mathcal{L_{\log}} (\textbf X, \textbf{y}, \textbf{w})$,只用最小化下式:
在逻辑回归中,通常使用正则化系数的倒数 $C = \frac{1}{\lambda}$:
下面通过一个例子直观地理解正则化。
正则化是如何影响分类质量的呢?我们使用吴恩达机器学习课程中的「微芯片测试」数据集,运用基于多项式特征的逻辑回归方法,然后改变正则化参数 $C$。首先,看看正则化是如何影响分类器的分界,并查看欠拟合和过拟合的情况。接着,将通过交叉验证和网格搜索方法来选择接近最优值的正则化参数。
import numpy as np
import pandas as pd
import seaborn as sns
from matplotlib import pyplot as plt
from sklearn.linear_model import LogisticRegression, LogisticRegressionCV
from sklearn.model_selection import (GridSearchCV, StratifiedKFold,
cross_val_score)
from sklearn.preprocessing import PolynomialFeatures
%matplotlib inline
使用 Pandas 库的 read_csv()
方法加载数据。这个数据集内有 118 个微芯片(目标),其中有两项质量控制测试的结果(两个数值变量)和微芯片是否投产的信息。变量已经过归一化,即列中的值已经减去其均值。所以,微芯片的平均测试值为零。
# 读取数据集
data = pd.read_csv(
"../../data/microchip_tests.txt", header=None, names=("test1", "test2", "released")
)
# 查看数据集的一些信息
data.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 118 entries, 0 to 117 Data columns (total 3 columns): test1 118 non-null float64 test2 118 non-null float64 released 118 non-null int64 dtypes: float64(2), int64(1) memory usage: 2.8 KB
查看开始五行和最后五行的数据。
data.head(5)
test1 | test2 | released | |
---|---|---|---|
0 | 0.051267 | 0.69956 | 1 |
1 | -0.092742 | 0.68494 | 1 |
2 | -0.213710 | 0.69225 | 1 |
3 | -0.375000 | 0.50219 | 1 |
4 | -0.513250 | 0.46564 | 1 |
data.tail(5)
test1 | test2 | released | |
---|---|---|---|
113 | -0.720620 | 0.538740 | 0 |
114 | -0.593890 | 0.494880 | 0 |
115 | -0.484450 | 0.999270 | 0 |
116 | -0.006336 | 0.999270 | 0 |
117 | 0.632650 | -0.030612 | 0 |
分离训练集和目标分类标签。
X = data.iloc[:, :2].values
y = data.iloc[:, 2].values
绘制数据,橙点对应有缺陷的芯片,蓝点对应正常芯片。
plt.scatter(X[y == 1, 0], X[y == 1, 1], c="blue", label="Released")
plt.scatter(X[y == 0, 0], X[y == 0, 1], c="orange", label="Faulty")
plt.xlabel("Test 1")
plt.ylabel("Test 2")
plt.title("2 tests of microchips. Logit with C=1")
plt.legend()
<matplotlib.legend.Legend at 0x7fd917ecc2e8>
定义一个函数来显示分类器的分界线。
def plot_boundary(clf, X, y, grid_step=0.01, poly_featurizer=None):
x_min, x_max = X[:, 0].min() - 0.1, X[:, 0].max() + 0.1
y_min, y_max = X[:, 1].min() - 0.1, X[:, 1].max() + 0.1
xx, yy = np.meshgrid(
np.arange(x_min, x_max, grid_step), np.arange(y_min, y_max, grid_step)
)
# 在 [x_min, m_max]x[y_min, y_max] 的每一点都用它自己的颜色来对应
Z = clf.predict(poly_featurizer.transform(np.c_[xx.ravel(), yy.ravel()]))
Z = Z.reshape(xx.shape)
plt.contour(xx, yy, Z, cmap=plt.cm.Paired)
为两个变量 $x_1$ 和 $x_2$ 定义如下多形式特征:
例如,$d=3$ 时的特征如下:
特征的数量会呈指数型增长,为 100 个变量创建 d 较大(例如 $d=10$)的多项式特征会导致计算成本变得很高。
使用 sklearn 库来实现逻辑回归。创建一个对象,为矩阵 $X$ 加上多项式特征($d$ 不超过7)。
poly = PolynomialFeatures(degree=7)
X_poly = poly.fit_transform(X)
X_poly.shape
(118, 36)
训练逻辑回归模型,正则化系数 $C = 10^{-2}$。
C = 1e-2
logit = LogisticRegression(C=C, random_state=17)
logit.fit(X_poly, y)
plot_boundary(logit, X, y, grid_step=0.01, poly_featurizer=poly)
plt.scatter(X[y == 1, 0], X[y == 1, 1], c="blue", label="Released")
plt.scatter(X[y == 0, 0], X[y == 0, 1], c="orange", label="Faulty")
plt.xlabel("Test 1")
plt.ylabel("Test 2")
plt.title("2 tests of microchips. Logit with C=%s" % C)
plt.legend()
print("Accuracy on training set:", round(logit.score(X_poly, y), 3))
Accuracy on training set: 0.627
可以尝试减小正则化,即把 $C$ 增加到 1,现在的模型权重可以比之前有更大的值(绝对值更大),这使得分类器在训练集上的精确度提高到 0.831。
C = 1
logit = LogisticRegression(C=C, random_state=17)
logit.fit(X_poly, y)
plot_boundary(logit, X, y, grid_step=0.005, poly_featurizer=poly)
plt.scatter(X[y == 1, 0], X[y == 1, 1], c="blue", label="Released")
plt.scatter(X[y == 0, 0], X[y == 0, 1], c="orange", label="Faulty")
plt.xlabel("Test 1")
plt.ylabel("Test 2")
plt.title("2 tests of microchips. Logit with C=%s" % C)
plt.legend()
print("Accuracy on training set:", round(logit.score(X_poly, y), 3))
Accuracy on training set: 0.831
倘若继续增加 $C$ 到 10000 会如何?看下面结果,很明显正则化不够强导致了过拟合现象。
C = 1e4
logit = LogisticRegression(C=C, random_state=17)
logit.fit(X_poly, y)
plot_boundary(logit, X, y, grid_step=0.005, poly_featurizer=poly)
plt.scatter(X[y == 1, 0], X[y == 1, 1], c="blue", label="Released")
plt.scatter(X[y == 0, 0], X[y == 0, 1], c="orange", label="Faulty")
plt.xlabel("Test 1")
plt.ylabel("Test 2")
plt.title("2 tests of microchips. Logit with C=%s" % C)
plt.legend()
print("Accuracy on training set:", round(logit.score(X_poly, y), 3))
Accuracy on training set: 0.873
为了讨论上述的这些结果,改写一下逻辑回归的优化函数:
其中,
总结:
对上述例子中正则化参数 $C$ 进行调参。使用 LogisticRegressionCV()
方法进行网格搜索参数后再交叉验证,LogisticRegressionCV()
是专门为逻辑回归设计的。如果想对其他模型进行同样的操作,可以使用 GridSearchCV()
或 RandomizedSearchCV()
等超参数优化算法。
# 该单元格执行时间较长,请耐心等待
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=17)
# 下方结尾的切片为了在线上环境搜索更快,线下练习时可以删除
c_values = np.logspace(-2, 3, 500)[50:450:50]
logit_searcher = LogisticRegressionCV(Cs=c_values, cv=skf, verbose=1, n_jobs=-1)
logit_searcher.fit(X_poly, y)
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 2 concurrent workers. [Parallel(n_jobs=-1)]: Done 5 out of 5 | elapsed: 5.1s finished
LogisticRegressionCV(Cs=array([3.16958e-02, 1.00463e-01, 3.18424e-01, 1.00927e+00, 3.19897e+00, 1.01394e+01, 3.21376e+01, 1.01863e+02]), class_weight=None, cv=StratifiedKFold(n_splits=5, random_state=17, shuffle=True), dual=False, fit_intercept=True, intercept_scaling=1.0, max_iter=100, multi_class='warn', n_jobs=-1, penalty='l2', random_state=None, refit=True, scoring=None, solver='lbfgs', tol=0.0001, verbose=1)
logit_searcher.C_
array([10.13939458])
查看超参数 $C$ 是如何影响模型的质量的。
plt.plot(c_values, np.mean(logit_searcher.scores_[1], axis=0))
plt.xlabel("C")
plt.ylabel("Mean CV-accuracy")
Text(0, 0.5, 'Mean CV-accuracy')
最后,选择 $C$ 值「最佳」的区域,即在 Mean CV-accuracy 值达到较大值的前提下选择较小的 $C$。上图由于 $C$ 过大,无法辨认具体哪个较小的 $C$ 达到了较好的 Mean CV-accuracy 值,可以仅画出 $C$ 为 0 到 10 时的验证曲线。
plt.plot(c_values, np.mean(logit_searcher.scores_[1], axis=0))
plt.xlabel("C")
plt.ylabel("Mean CV-accuracy")
plt.xlim((0, 10))
(0, 10)
上图可见,$C=2$ 时就达到了较好的 Mean CV-accuracy 值。
通过分析 IMDB 影评的二元分类问题和 XOR 问题来简要说明逻辑回归的优缺点。
IMDB 数据集中的训练集包含标记好的影评,其中有 12500 条好评,12500 条差评。使用词袋模型构建输入矩阵 $X$ ,语料库包含所有用户影评,影评的特征将由整个语料库中每个词的出现情况来表示。下图展示了这一思路:
import os
import matplotlib.pyplot as plt
import numpy as np
from sklearn.datasets import load_files
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
载入 IMDB 数据集。首先,我们从实验楼服务器上下载并解压数据。
# 文件较多,请耐心等待解压完成
!wget -nc "../../data/aclImdb_v1.tar.gz"
!tar -zxvf "aclImdb_v1.tar.gz"
PATH_TO_IMDB = "aclImdb/"
reviews_train = load_files(
os.path.join(PATH_TO_IMDB, "train"), categories=["pos", "neg"]
)
text_train, y_train = reviews_train.data, reviews_train.target
reviews_test = load_files(os.path.join(PATH_TO_IMDB, "test"), categories=["pos", "neg"])
text_test, y_test = reviews_test.data, reviews_test.target
看看训练集和测试集中各有多少条数据。
print("Number of documents in training data: %d" % len(text_train))
print(np.bincount(y_train))
print("Number of documents in test data: %d" % len(text_test))
print(np.bincount(y_test))
Number of documents in training data: 25000 [12500 12500] Number of documents in test data: 25000 [12500 12500]
下面是该数据集中的一些影评。
text_train[1]
b'Words can\'t describe how bad this movie is. I can\'t explain it by writing only. You have too see it for yourself to get at grip of how horrible a movie really can be. Not that I recommend you to do that. There are so many clich\xc3\xa9s, mistakes (and all other negative things you can imagine) here that will just make you cry. To start with the technical first, there are a LOT of mistakes regarding the airplane. I won\'t list them here, but just mention the coloring of the plane. They didn\'t even manage to show an airliner in the colors of a fictional airline, but instead used a 747 painted in the original Boeing livery. Very bad. The plot is stupid and has been done many times before, only much, much better. There are so many ridiculous moments here that i lost count of it really early. Also, I was on the bad guys\' side all the time in the movie, because the good guys were so stupid. "Executive Decision" should without a doubt be you\'re choice over this one, even the "Turbulence"-movies are better. In fact, every other movie in the world is better than this one.'
查看一下上面这条影评是差评还是好评。
y_train[1]
0
y_train=0 表示影评是差评,y_train=1 表示影评是好评,上面这条影片是差评。
首先,使用 CountVectorizer()
创建包含所有单词的字典。
cv = CountVectorizer()
cv.fit(text_train)
len(cv.vocabulary_)
74849
查看创建后的「单词」样本,发现 IMDB 数据集已经自动进行了文本处理(自动化文本处理不在本实验讨论范围,如果感兴趣可以自行搜索)。
print(cv.get_feature_names()[:50])
print(cv.get_feature_names()[50000:50050])
['00', '000', '0000000000001', '00001', '00015', '000s', '001', '003830', '006', '007', '0079', '0080', '0083', '0093638', '00am', '00pm', '00s', '01', '01pm', '02', '020410', '029', '03', '04', '041', '05', '050', '06', '06th', '07', '08', '087', '089', '08th', '09', '0f', '0ne', '0r', '0s', '10', '100', '1000', '1000000', '10000000000000', '1000lb', '1000s', '1001', '100b', '100k', '100m'] ['pincher', 'pinchers', 'pinches', 'pinching', 'pinchot', 'pinciotti', 'pine', 'pineal', 'pineapple', 'pineapples', 'pines', 'pinet', 'pinetrees', 'pineyro', 'pinfall', 'pinfold', 'ping', 'pingo', 'pinhead', 'pinheads', 'pinho', 'pining', 'pinjar', 'pink', 'pinkerton', 'pinkett', 'pinkie', 'pinkins', 'pinkish', 'pinko', 'pinks', 'pinku', 'pinkus', 'pinky', 'pinnacle', 'pinnacles', 'pinned', 'pinning', 'pinnings', 'pinnochio', 'pinnocioesque', 'pino', 'pinocchio', 'pinochet', 'pinochets', 'pinoy', 'pinpoint', 'pinpoints', 'pins', 'pinsent']
接着,使用单词的索引编码训练集的句子,用稀疏矩阵保存。
X_train = cv.transform(text_train)
X_train
<25000x74849 sparse matrix of type '<class 'numpy.int64'>' with 3445861 stored elements in Compressed Sparse Row format>
让我们看看上述转换过程是如何进行的,首先查看需要转换的训练集句子。
text_train[19726]
b'This movie is terrible but it has some good effects.'
然后将每个单词转换成对应的单词索引。
X_train[19726].nonzero()[1]
array([ 9881, 21020, 28068, 29999, 34585, 34683, 44147, 61617, 66150, 66562], dtype=int32)
X_train[19726].nonzero()
(array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0], dtype=int32), array([ 9881, 21020, 28068, 29999, 34585, 34683, 44147, 61617, 66150, 66562], dtype=int32))
接下来,对测试集应用同样的操作。
X_test = cv.transform(text_test)
之后就可以使用逻辑回归来训练模型。
logit = LogisticRegression(solver="lbfgs", n_jobs=-1, random_state=7)
logit.fit(X_train, y_train)
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True, intercept_scaling=1, max_iter=100, multi_class='warn', n_jobs=-1, penalty='l2', random_state=7, solver='lbfgs', tol=0.0001, verbose=0, warm_start=False)
训练完成后,查看训练集和测试集上的准确率(Accuracy)。
round(logit.score(X_train, y_train), 3), round(logit.score(X_test, y_test), 3),
(0.981, 0.864)
可视化模型的系数。
def visualize_coefficients(classifier, feature_names, n_top_features=25):
# get coefficients with large absolute values
coef = classifier.coef_.ravel()
positive_coefficients = np.argsort(coef)[-n_top_features:]
negative_coefficients = np.argsort(coef)[:n_top_features]
interesting_coefficients = np.hstack([negative_coefficients, positive_coefficients])
# plot them
plt.figure(figsize=(15, 5))
colors = ["red" if c < 0 else "blue" for c in coef[interesting_coefficients]]
plt.bar(np.arange(2 * n_top_features), coef[interesting_coefficients], color=colors)
feature_names = np.array(feature_names)
plt.xticks(
np.arange(1, 1 + 2 * n_top_features),
feature_names[interesting_coefficients],
rotation=60,
ha="right",
)
def plot_grid_scores(grid, param_name):
plt.plot(
grid.param_grid[param_name],
grid.cv_results_["mean_train_score"],
color="green",
label="train",
)
plt.plot(
grid.param_grid[param_name],
grid.cv_results_["mean_test_score"],
color="red",
label="test",
)
plt.legend()
visualize_coefficients(logit, cv.get_feature_names())
对逻辑回归的正则化系数进行调参。make_pipeline()
确保的序列顺序,在训练数据上应用 CountVectorizer()
方法,然后训练逻辑回归模型。
from sklearn.pipeline import make_pipeline
# 该单元格执行时间较长,请耐心等待
text_pipe_logit = make_pipeline(
CountVectorizer(), LogisticRegression(solver="lbfgs", n_jobs=1, random_state=7)
)
text_pipe_logit.fit(text_train, y_train)
print(text_pipe_logit.score(text_test, y_test))
0.864
from sklearn.model_selection import GridSearchCV
# 该单元格执行时间较长,请耐心等待
param_grid_logit = {"logisticregression__C": np.logspace(-5, 0, 6)[4:5]}
grid_logit = GridSearchCV(
text_pipe_logit, param_grid_logit, return_train_score=True, cv=3, n_jobs=-1
)
grid_logit.fit(text_train, y_train)
GridSearchCV(cv=3, error_score='raise-deprecating', estimator=Pipeline(memory=None, steps=[('countvectorizer', CountVectorizer(analyzer='word', binary=False, decode_error='strict', dtype=<class 'numpy.int64'>, encoding='utf-8', input='content', lowercase=True, max_df=1.0, max_features=None, min_df=1, ngram_range=(1, 1), preprocessor=None, stop_words=None, ... penalty='l2', random_state=7, solver='lbfgs', tol=0.0001, verbose=0, warm_start=False))]), fit_params=None, iid='warn', n_jobs=-1, param_grid={'logisticregression__C': array([0.1])}, pre_dispatch='2*n_jobs', refit=True, return_train_score=True, scoring=None, verbose=0)
查看一下最佳 $C$,以及相应的交叉验证评分。
grid_logit.best_params_, grid_logit.best_score_
({'logisticregression__C': 0.1}, 0.8848)
plot_grid_scores(grid_logit, "logisticregression__C")
调优后的逻辑回归模型在验证集上的准确率。
grid_logit.score(text_test, y_test)
0.87812
现在换一种方法,使用随机森林来分类。
from sklearn.ensemble import RandomForestClassifier
forest = RandomForestClassifier(n_estimators=200, n_jobs=-1, random_state=17)
forest.fit(X_train, y_train)
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=1, min_samples_split=2, min_weight_fraction_leaf=0.0, n_estimators=200, n_jobs=-1, oob_score=False, random_state=17, verbose=0, warm_start=False)
round(forest.score(X_test, y_test), 3)
0.855
上述结果可见,相较于随机森林,逻辑回归在 IMDB 数据集上表现更优。
线性分类定义的是一个非常简单的分界平面:一个超平面,这导致线性模型在 XOR 问题上表现不佳。XOR 即异或,其真值表如下:
XOR 是一个简单的二元分类问题,其中两个分类呈对角交叉分布。下面创建数据集。
rng = np.random.RandomState(0)
X = rng.randn(200, 2)
y = np.logical_xor(X[:, 0] > 0, X[:, 1] > 0)
plt.scatter(X[:, 0], X[:, 1], s=30, c=y, cmap=plt.cm.Paired)
<matplotlib.collections.PathCollection at 0x7fd9033288d0>
显然,无法划出一条直线无误差地将两个分类分开。因此,逻辑回归在这一任务上的表现很差。
def plot_boundary(clf, X, y, plot_title):
xx, yy = np.meshgrid(np.linspace(-3, 3, 50), np.linspace(-3, 3, 50))
clf.fit(X, y)
# plot the decision function for each datapoint on the grid
Z = clf.predict_proba(np.vstack((xx.ravel(), yy.ravel())).T)[:, 1]
Z = Z.reshape(xx.shape)
image = plt.imshow(
Z,
interpolation="nearest",
extent=(xx.min(), xx.max(), yy.min(), yy.max()),
aspect="auto",
origin="lower",
cmap=plt.cm.PuOr_r,
)
contours = plt.contour(xx, yy, Z, levels=[0], linewidths=2, linetypes="--")
plt.scatter(X[:, 0], X[:, 1], s=30, c=y, cmap=plt.cm.Paired)
plt.xticks(())
plt.yticks(())
plt.xlabel(r"$x_1$")
plt.ylabel(r"$x_2$")
plt.axis([-3, 3, -3, 3])
plt.colorbar(image)
plt.title(plot_title, fontsize=12)
plot_boundary(
LogisticRegression(solver="lbfgs"), X, y, "Logistic Regression, XOR problem"
)
然而,如果将输入变为多项式特征(这里 $d$ = 2),那么这一任务就可以得到较好的解决。
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
logit_pipe = Pipeline(
[
("poly", PolynomialFeatures(degree=2)),
("logit", LogisticRegression(solver="lbfgs")),
]
)
plot_boundary(logit_pipe, X, y, "Logistic Regression + quadratic features. XOR problem")
通过将多项式特征作为输入,逻辑回归在 6 维特征空间($1,x_1,x_2,x_1^2,x_1x_2,x_2^2$)中生成了一个超平面。当这个超平面投影到原特征空间($x_1, x_2$)时,分界是非线性的。
在实际应用中,多项式特征确实有用,不过显式的创建它们会大大提升计算复杂度。使用核(kernel)函数的 SVM 方法相较逻辑回归要快很多,在 SVM 中,只计算高维空间中目标之间的距离(由核函数定义),而不用生成大量特征组合。
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
from sklearn.linear_model import (LogisticRegression, LogisticRegressionCV,
SGDClassifier)
from sklearn.model_selection import learning_curve, validation_curve
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
上文对模型验证、交叉验证、正则化做了简单介绍,现在考虑一个更大的问题:如果模型的质量不佳,该怎么办?针对这个问题,有很多猜想:
这些猜想的答案并不明显,比如有时候一个更复杂的模型会导致表现退化,有时候增加新的特征带来的变化并不直观。事实上,做出正确决定,选择正确方法,从而改进模型的能力是衡量一个人对机器学习知识掌握程度的重要指标。
让我们回头看看电信运营商的客户离网数据集。
data = pd.read_csv("../../data/telecom_churn.csv").drop("State", axis=1)
data["International plan"] = data["International plan"].map({"Yes": 1, "No": 0})
data["Voice mail plan"] = data["Voice mail plan"].map({"Yes": 1, "No": 0})
y = data["Churn"].astype("int").values
X = data.drop("Churn", axis=1).values
使用随机梯度下降训练逻辑回归(在之后的实验中将会专门讨论梯度下降)。
alphas = np.logspace(-2, 0, 20)
sgd_logit = SGDClassifier(loss="log", n_jobs=-1, random_state=17, max_iter=5)
logit_pipe = Pipeline(
[
("scaler", StandardScaler()),
("poly", PolynomialFeatures(degree=2)),
("sgd_logit", sgd_logit),
]
)
val_train, val_test = validation_curve(
logit_pipe, X, y, "sgd_logit__alpha", alphas, cv=5, scoring="roc_auc"
)
绘制 ROC-AUC 曲线,查看不同正则化参数下模型在训练集和测试集上的表现有何不同。
def plot_with_err(x, data, **kwargs):
mu, std = data.mean(1), data.std(1)
lines = plt.plot(x, mu, "-", **kwargs)
plt.fill_between(
x,
mu - std,
mu + std,
edgecolor="none",
facecolor=lines[0].get_color(),
alpha=0.2,
)
plot_with_err(alphas, val_train, label="training scores")
plot_with_err(alphas, val_test, label="validation scores")
plt.xlabel(r"$\alpha$")
plt.ylabel("ROC AUC")
plt.legend()
plt.grid(True)
上图的趋势表明:
上述结论可以推广到其他问题中。
一般而言,模型所用的数据越多越好。但新数据是否在任何情况下都有帮助呢?例如,为了评估特征 N ,而对数据集的数据进行加倍,这样做是否合理?
由于新数据可能难以取得,合理的做法是改变训练集的大小,然后看模型的质量与训练数据的数量之间的依赖关系,这就是「学习曲线」的概念。
这个想法很简单:将误差看作训练中所使用的样本数量的函数。模型的参数事先固定。
def plot_learning_curve(degree=2, alpha=0.01):
train_sizes = np.linspace(0.05, 1, 20)
logit_pipe = Pipeline(
[
("scaler", StandardScaler()),
("poly", PolynomialFeatures(degree=degree)),
(
"sgd_logit",
SGDClassifier(n_jobs=-1, random_state=17, alpha=alpha, max_iter=5),
),
]
)
N_train, val_train, val_test = learning_curve(
logit_pipe, X, y, train_sizes=train_sizes, cv=5, scoring="roc_auc"
)
plot_with_err(N_train, val_train, label="training scores")
plot_with_err(N_train, val_test, label="validation scores")
plt.xlabel("Training Set Size")
plt.ylabel("AUC")
plt.legend()
plt.grid(True)
把正则化系数设定为较大的数(alpha=10),查看线性模型的表现情况。
plot_learning_curve(degree=2, alpha=10)
上图表明:对于少量数据而言,训练集和交叉验证集之间的误差差别(方差)相当大,这暗示了过拟合。同样的模型,使用大量数据,误差「收敛」,暗示了欠拟合。加入更多数据,该训练集的误差不会增加,且该验证集上的误差也不会下降。所以,倘若误差「收敛」,如果不改变模型的复杂度,而是仅仅把数据集大小增大 10 倍,或许对最终的表现结果没有太大帮助。
如果将正则化系数 alpha 降低到 0.05,会怎么样?
plot_learning_curve(degree=2, alpha=0.05)
上图表明,降低正则化系数 alpha 至 0.05,曲线将逐渐收敛,如果加入更多数据,可以进一步改善模型在验证集上的表现。
如果把 alpha 设为 $10^{-4}$,让模型更复杂,会出现什么情况?
plot_learning_curve(degree=2, alpha=1e-4)
上图表明,与正则化系数 alpha=0.05 相比,在训练集和验证集上,AUC 都下降了,出现过拟合现象。
构建学习曲线和验证曲线可以帮助我们为新数据调整合适的模型复杂度。
关于验证曲线和学习曲线的结论:
训练集上的误差本身不能说明模型的质量。
交叉验证误差除了可以显示模型对数据的拟合程度外,还可以显示模型保留了多少对新数据的概括能力。
验证曲线是一条根据模型复杂度显示训练集和验证集结果的曲线:如果两条曲线彼此接近,且两者的误差都很大,这标志着欠拟合;如果两条曲线彼此距离很远,这标志着过拟合。
学习曲线是一个根据观测数量显示训练集和验证集结果的曲线:如果两条曲线收敛,那么增加新数据收益不大,有必要改变模型复杂度;如果两条曲线没有收敛,增加新数据可以改善结果。
本次实验主要使用逻辑回归的方法构建线性回归和线性分类模型,正则化、验证曲线、学习曲线方法可以帮助我们更好更快的构建模型。
相关链接