在介绍了机器学习中相关的基本概念和技巧后,本章我们继续讲解参数化模型中的线性模型。有了前文的基础,我们可以先来对 KNN 算法和线性回归进行比较,进一步回答“什么是参数化模型”这一问题。对于机器学习算法来说,其目标通常可以抽象为得到某个从输入空间到输出空间的映射
相比于非参数化模型,参数化模型由于限制了
在线性回归中,我们利用参数化的线性假设解决了回归问题,而分类问题作为机器学习任务中的另一大类别,其与回归问题既有相似也有不同。通常来说,回归问题的输出是连续的,而分类问题的输出是离散的。设输入数据
与线性回归类似,对二分类问题,我们同样可以作线性假设。设学习到的映射为
似乎刚刚的两个问题都得到了解决,但是,这一方法又为模型训练带来了新的困难。在线性回归中我们已经介绍过,无论是解析方法还是梯度下降,都需要以函数的梯度为基础。然而,这样的分类方法太“硬”了,使得
我们不妨换一个角度来考虑二分类问题。如果把样本
至此我们已经解决了用连续函数给出离散分类和硬分类函数不可导的两个问题。但我们最开始已经提到,
该函数的图像如图 6-1 所示。
逻辑斯谛函数是一种常见的 sigmoid 函数。sigmoid 函数是一类形状类似于‘S’型的函数,但在实践中如无特殊说明,一般就指逻辑斯谛函数。逻辑斯谛函数有许多优秀的性质。首先,它关于
综上所述,我们只需要用逻辑斯谛函数对
确定了逻辑斯谛回归的数学模型之后,我们接下来还需要确定优化目标。对于有关概率分布的问题,我们常常使用最大似然估计(maximum likelihood estimation,MLE)的思想来优化模型,也即是寻找逻辑斯谛回归的参数
设共有
该函数也称为似然函数(likelihood function)。为了使模型的预测尽可能准确,我们需要寻找使似然函数最大的参数
由于对数函数是单调递增的,优化
对
再把
注意到我们的优化目标是最大化
需要注意,在实际计算时,我们通常会对样本取平均,来消除样本规模对学习率
从而梯度下降算法的矩阵形式为:
除此之外,我们再为模型加入
逻辑斯谛回归的损失函数也是凸函数,图 6-2 展示了在逻辑斯谛回归模型
由于逻辑斯谛函数只适用于二分类的情况,对于多分类任务,我们需要寻找新的映射函数。这时,我们通常采用柔性最大值(softmax)函数来将线性部分的预测
其中,
下面我们来考察 softmax 函数的一些性质。直观上看,在原始的向量
简单起见,在本节的后续部分我们仍然考虑二分类任务。softmax 函数的更多性质与多分类任务的损失函数留作习题供读者思考。
在动手实现逻辑斯谛回归之前,我们还需要解决一个问题:如何判断分类模型的好坏呢?对于一个二分类问题,最简单的评估模型好坏的方式就是测试模型的正确率。这时,无论是用直接给出类别的硬分类,还是给出不同分类概率的软分类,我们都需要考虑分类的阈值。例如,如果模型判断某样本的类别是正类
读者或许有疑问,为什么不能遵循最自然的大小关系,将 0.5 作为阈值呢?这与实际应用中分类任务的特点有关。例如,当前的任务是进行汽车的出厂检测,根据检测的数据判断汽车是否有质量问题,没有问题是负类,有问题是正类。在这样的任务中,如果模型把有问题的汽车错判为没有问题,就可能让质量不合格的汽车出场,其后果比把没有问题的汽车错判为有问题要严重得多。因此,我们需要把正类的阈值设置的很高,比如 0.99。可以发现,虽然抽象的分类问题中,正类负类之间的地位是对等的,因此将正类错判为负类和将负类错判为正类没有什么区别;但是实际场景中,这两者往往并不对称,我们必须根据任务的特点设置合适的阈值。并且,只有正确率这一标准是不够的,还需要分别考虑正负类分别被错判的比例。
在机器学习中,我们通常用图 6-3 的混淆矩阵(confusion matrix)来统计不同的分类结果。其中,真阳性(true positive,TP)表示将真实的正类判别为正类,真阴性(true negative,TN)表示将真实的负类判别为负类,这两者属于判断正确的结果。假阴性(false negative,FN)表示将真实的正类判别为负类,假阳性(false positive,FP)表示将真实的负类判别为正类,属于判断错误的结果。我们常说的精度(accuracy)就是判断正确的样本数量除以样本总数:
在上面汽车质检的例子中,我们更关心找出有问题的汽车占真正有问题汽车的比例,即真阳性率(true positive rate),也称查全率或召回率(recall):
此外,我们常常还希望假阳性尽可能少,即真阳性占模型判为阳性的样本比例尽可能高。这一指标称为查准率(precision):
由于样本总数固定,混淆矩阵的 4 个指标并非相互独立。因此,我们常用查全率和查准率这两个标准作为代表,其他指标的变化也可以通过它们来间接反映。
或许读者已经发现,混淆矩阵导出的数个统计指标是不可兼得的,它们都与预设的阈值有关。例如,如果我们将阈值设得很低,就会将大量样本判为正类,提高真阳性的数量,从而使召回率上升。然而,这种做法同时会使许多实际上为负类的样本被归为正类,造成大量假阳性,反而可能让精确率下降。因此,我们应当根据任务具体的特点,先确定不同指标的重要程度,再选取合适的阈值。为了同时反映各个指标的情况,达到平衡点,我们引入新的指标 F1 分数(F1 score),定义为精确率和召回率的调和平均值:
可以看出,F1 分数最小为 0,在精确率或召回率有一方为 0 时取到;最大为 1,在精确率和召回率相等时取到。因此,单方面增大或减小精确率和召回率中的一个无法使 F1 分数增高,必须要让两者得到平衡。如果在实际问题中,精确率和召回率的重要程度不同,我们还可以把 F1 分数扩展为 F-beta 分数:
上式相当于给精确率添加了权重
对于更普遍的情况来说,我们希望衡量模型在不同阈值下的整体表现,根据不同指标随阈值的变化来得出与阈值无关的模型本身的特性。因此,我们选择假阳性率(false positive rate,FPR)与真阳性率(召回率)这两个随阈值变化趋势相同的指标,把它们的值绘制成曲线。其中,FPR 定义为
我们来具体分析一下
那么,它们的变化趋势为何能反映模型的好坏呢?为了方便读者理解,我们在图 6-4 中绘制了
对于模型来说,我们期望它能将尽可能多的样本分类正确、同时还要少出错,也即 TPR 尽可能高、FPR 尽可能低。最理想的情况下,当阈值合适的时候,所有的正样本都归为正类,所有的负样本都归为负类,这时
“曲线偏向左上方”是一个定性的描述,为了定量衡量 ROC 曲线表示的模型好坏,我们通常计算 ROC 曲线与
AUC 除了 ROC 下的面积,还有另一层含义。我们以一个包含 5 个样本的数据集来说明,这些样本依次记为
表 6-1 可以完美分隔的情况
显然,如果我们任取一个负类、任取一个正类,负类排在正类前面的概率是
表 6-2 无法完美分隔的情况
同样任取一个负类
再来计算此时的 AUC。绘制出形如图 6-5 的 ROC 曲线,容易算出 AUC 为
与上面得到的负类排在正类前面的概率相等!事实上,这一结论对于更多样本的场合也是成立的。我们将样本离散且没有相同概率的情况留作习题由读者证明。从这里可以看出,模型将真实的正类和负类分得越清楚,就有越多的负类排在正类前面,AUC 就越大。如果模型分类完全随机,那么任取一个负类和一个正类,它们的位置也是随机的,负类排在正类前面的概率为 0.5,所以得到的 AUC 也是 0.5,同样与上面的结论相符。这一结论告诉我们,虽然我们是从阈值的变化中推导出 ROC 与 AUC,但是 AUC 的值事实上与阈值是的选取无关的,只与模型本身对正负类的预测结果有关。在下一节的动手应用中,我们就用本节讲述的各种标准来作为模型的评价指标。
此外需要提醒的是,本节介绍的分类指标大都是针对二分类问题的。对于多分类问题,准确率(
下面,我们动手实现梯度下降算法求解逻辑斯谛回归。本章所用的数据集 lr_dataset.csv 包含了二维平面上的一些点,这些点按位置的不同分为两类。表 6-3 展示了数据集中的几条样本,每条样本依次包含横坐标、纵坐标和类别标签。我们的任务是训练逻辑斯谛回归模型,使模型对点的类别预测尽可能准确。
表 6-3 逻辑斯谛回归数据集中的样本示例
横坐标 | 纵坐标 | 标签 |
---|---|---|
0.4304 | 0.2055 | 1 |
0.0898 | -0.1527 | 1 |
-0.8257 | -0.9596 | 0 |
首先,我们读入并处理数据集,并将其在平面上的分布展示出来。图中的每个红色圆圈代表一个正类,蓝色叉号代表一个负类。
import numpy as npimport matplotlib.pyplot as pltfrom matplotlib.ticker import MaxNLocator# 从源文件中读入数据并处理lines = np.loadtxt('lr_dataset.csv', delimiter=',', dtype=float)x_total = lines[:, 0:2]y_total = lines[:, 2]print('数据集大小:', len(x_total))# 将得到的数据在二维平面上制图,不同的类别染上不同的颜色以便于观察样本点的分布pos_index = np.where(y_total == 1)neg_index = np.where(y_total == 0)plt.scatter(x_total[pos_index, 0], x_total[pos_index, 1],marker='o', color='coral', s=10)plt.scatter(x_total[neg_index, 0], x_total[neg_index, 1],marker='x', color='blue', s=10)plt.xlabel('X1 axis')plt.ylabel('X2 axis')plt.show()# 划分训练集与测试集np.random.seed(0)ratio = 0.7split = int(len(x_total) * ratio)idx = np.random.permutation(len(x_total))x_total = x_total[idx]y_total = y_total[idx]x_train, y_train = x_total[:split], y_total[:split]x_test, y_test = x_total[split:], y_total[split:]
数据集大小: 1000
接下来,我们实现上节中所描述的几个评价指标。这里选用准确率和 AUC 两种,其他评价指标留给读者自行实践。对于 AUC 以外的指标,我们需要样本的真实标签以及某个阈值下模型的预测标签。而 AUC 中已经包含了阈值变化,所以需要的是模型原始的预测概率。
def acc(y_true, y_pred):return np.mean(y_true == y_pred)def auc(y_true, y_pred):# 按预测值从大到小排序,越靠前的样本预测正类概率越大idx = np.argsort(y_pred)[::-1]y_true = y_true[idx]y_pred = y_pred[idx]# 把y_pred中不重复的值当作阈值,依次计算FP样本和TP样本数量# 由于两个数组已经排序且位置对应,直接从前向后累加即可tp = np.cumsum(y_true)fp = np.cumsum(1 - y_true)tpr = tp / tp[-1]fpr = fp / fp[-1]# 依次枚举FPR,计算曲线下的面积# 方便起见,给FPR和TPR最开始添加(0,0)s = 0.0tpr = np.concatenate([[0], tpr])fpr = np.concatenate([[0], fpr])for i in range(1, len(fpr)):s += (fpr[i] - fpr[i - 1]) * tpr[i]return s
由于本章所用的数据集大小较小,我们不再每次取小批量进行迭代,而是直接用完整的训练集计算梯度。接下来,我们按照之前推导的梯度下降公式定义训练函数,并设置学习率和迭代次数,查看训练结果。除了训练曲线之外,我们还将模型参数
# 逻辑斯谛函数def logistic(z):return 1 / (1 + np.exp(-z))def GD(num_steps, learning_rate, l2_coef):# 初始化模型参数theta = np.random.normal(size=(X.shape[1],))train_losses = []test_losses = []train_acc = []test_acc = []train_auc = []test_auc = []for i in range(num_steps):pred = logistic(X @ theta)grad = -X.T @ (y_train - pred) + l2_coef * thetatheta -= learning_rate * grad# 记录损失函数train_loss = - y_train.T @ np.log(pred) \- (1 - y_train).T @ np.log(1 - pred) \+ l2_coef * np.linalg.norm(theta) ** 2 / 2train_losses.append(train_loss / len(X))test_pred = logistic(X_test @ theta)test_loss = - y_test.T @ np.log(test_pred) \- (1 - y_test).T @ np.log(1 - test_pred)test_losses.append(test_loss / len(X_test))# 记录各个评价指标,阈值采用0.5train_acc.append(acc(y_train, pred >= 0.5))test_acc.append(acc(y_test, test_pred >= 0.5))train_auc.append(auc(y_train, pred))test_auc.append(auc(y_test, test_pred))return theta, train_losses, test_losses, \train_acc, test_acc, train_auc, test_auc
最后,我们定义各个超参数,进行训练并绘制出训练损失、准确率和 AUC 随训练轮数的变化。
# 定义梯度下降迭代的次数,学习率,以及L2正则系数num_steps = 250learning_rate = 0.002l2_coef = 1.0np.random.seed(0)# 在x矩阵上拼接1X = np.concatenate([x_train, np.ones((x_train.shape[0], 1))], axis=1)X_test = np.concatenate([x_test, np.ones((x_test.shape[0], 1))], axis=1)theta, train_losses, test_losses, train_acc, test_acc, \train_auc, test_auc = GD(num_steps, learning_rate, l2_coef)# 计算测试集上的预测准确率y_pred = np.where(logistic(X_test @ theta) >= 0.5, 1, 0)final_acc = acc(y_test, y_pred)print('预测准确率:', final_acc)print('回归系数:', theta)plt.figure(figsize=(13, 9))xticks = np.arange(num_steps) + 1# 绘制训练曲线plt.subplot(221)plt.plot(xticks, train_losses, color='blue', label='train loss')plt.plot(xticks, test_losses, color='red', ls='--', label='test loss')plt.gca().xaxis.set_major_locator(MaxNLocator(integer=True))plt.xlabel('Epochs')plt.ylabel('Loss')plt.legend()# 绘制准确率plt.subplot(222)plt.plot(xticks, train_acc, color='blue', label='train accuracy')plt.plot(xticks, test_acc, color='red', ls='--', label='test accuracy')plt.gca().xaxis.set_major_locator(MaxNLocator(integer=True))plt.xlabel('Epochs')plt.ylabel('Accuracy')plt.legend()# 绘制AUCplt.subplot(223)plt.plot(xticks, train_auc, color='blue', label='train AUC')plt.plot(xticks, test_auc, color='red', ls='--', label='test AUC')plt.gca().xaxis.set_major_locator(MaxNLocator(integer=True))plt.xlabel('Epochs')plt.ylabel('AUC')plt.legend()# 绘制模型学到的分隔直线plt.subplot(224)plot_x = np.linspace(-1.1, 1.1, 100)# 直线方程:theta_0 * x_1 + theta_1 * x_2 + theta_2 = 0plot_y = -(theta[0] * plot_x + theta[2]) / theta[1]pos_index = np.where(y_total == 1)neg_index = np.where(y_total == 0)plt.scatter(x_total[pos_index, 0], x_total[pos_index, 1],marker='o', color='coral', s=10)plt.scatter(x_total[neg_index, 0], x_total[neg_index, 1],marker='x', color='blue', s=10)plt.plot(plot_x, plot_y, ls='-.', color='green')plt.xlim(-1.1, 1.1)plt.ylim(-1.1, 1.1)plt.xlabel('X1 axis')plt.ylabel('X2 axis')plt.show()
预测准确率: 0.8766666666666667回归系数: [3.13501827 2.90998799 0.55095127]
为了进一步展示模型学到的概率信息,我们把模型预测的每个数据点的概率展示在图 6-6 的左半部分中,其中的
与线性回归相似,sklearn 同样提供了封装好的逻辑斯谛回归模型LogisticRegression
。我们直接使用该工具求解逻辑斯谛回归问题,并与自己实现的梯度下降方法进行比较。sklearn 中的方法默认添加了正则系数
from sklearn.linear_model import LogisticRegression# 使用线性模型中的逻辑回归模型在数据集上训练# 其提供的liblinear优化算法适合在较小数据集上使用# 默认使用系数为1.0的L2正则化约束# 其他可选参数请参考官方文档lr_clf = LogisticRegression(solver='liblinear')lr_clf.fit(x_train, y_train)print('回归系数:', lr_clf.coef_[0], lr_clf.intercept_)# 在数据集上用计算得到的逻辑回归模型进行预测并计算准确度y_pred = lr_clf.predict(x_test)print('准确率为:',np.mean(y_pred == y_test))
回归系数: [3.14129907 2.91620111] [0.5518978]准确率为: 0.8766666666666667
在训练逻辑斯谛回归模型时,我们从概率分布出发,采用最大似然估计的思想得到了损失函数。而从信息论的角度,我们也能得到相同的结果。在信息论中,当一个随机事件发生时,它就会提供一定的信息。而事件发生概率越小,其发生时所提供的信息量也就越大。例如,连续抛一枚硬币
从上式中可以看出,确定事件发生不会提供任何信息,这也符合我们的直观感受。而当许多事件互相影响时,我们还需要对这些事件整体的性质进行研究。
下面,我们只考虑事件离散且有限的情况。设有
经过一些数学推导可以得到,当某个事件发生的概率为
更进一步,如果关于随机变量
KL 散度是一种比较特殊的距离度量。我们知道,判断两个数
我们以图 6-7 为例定性地说明这一点,图的左半部分展示了两个连续变量的概率密度函数
对于离散随机变量,KL 散度可以进一步拆分为:
其中,
如果再对所有样本的交叉熵求和,就得到总的交叉熵为:
可以发现,总交叉熵恰好等于负的对数似然函数。因此,逻辑回归问题中,最大化对数似然函数与最小化交叉熵是等价的。事实上,无论是离散情况还是连续情况这一结论都成立,所以我们也经常称交叉熵为逻辑回归的损失函数。交叉熵在涉及到概率分布的模型中十分重要,我们在后面的树模型中还会再次用到这一概念。
本章讲解了机器学习的其中一大类任务:分类任务。任何有限分类任务均可以转化为多次二分类任务,因此二分类是分类问题的基础。我们重点讲解了二分类问题的最简单的线性模型:逻辑斯谛回归。和回归问题中的连续值标签不同,分类问题的标签是离散不可导的类别标签。由于线性模型的取值范围与分类任务不相符,我们需要将线性模型的结果映射到类别上,同时保证该映射可导。因此,我们从直接建模分类转为建模不同类别的概率,再利用最大似然估计的学习目标,解决了模型优化与训练的问题,这其实是分类问题求解的基本思想。最后,我们在简单的数据集上分别用梯度下降法和 sklearn 中的工具进行了实践。此外,我们还从信息论的角度出发,推导了交叉熵的公式,得到了分类问题中交叉熵与最大似然估计的等价性。
逻辑斯谛回归,虽然其名字包含“回归”二字,但它是最具有代表性的机器学习分类模型,至今还在学术研究和工业落地场景中被广泛使用。逻辑斯谛回归具有较好的可解释性,其参数的绝对值大小和正负代表了对应的特征对于预测数据类别的重要性,在医学、营销学、金融学等领域广泛被用于目标归因。逻辑斯谛回归具有极好的可并行性,其优化目标相对参数是凸函数,具有全局唯一最优解,因此在工业实践中也时常使用分布式并行训练的逻辑斯谛回归方法。可以说,掌握了线性回归和逻辑斯谛回归方法,日常大部分机器学习预测任务都可以有一个基本解决方案了。
以下关于最大似然估计的表述中正确的是: A. 以概率为输出的模型常用最大似然估计得到损失函数。 B. 由于最大似然估计优化的是对数似然而非似然,得到的结果只是最优解的近似。 C. 最大似然估计与交叉熵的训练目标不等价。 D. 最大似然估计中引入了概率分布,所以不能采用梯度下降法来优化最大似然估计导出的损失函数。
以下关于分类问题的说法中不正确的是: A. 分类问题中,最后往往需要通过阈值来决定样本最后的标签。对于标签为 0 或 1 的二分类问题,当
以下关于分类问题评价指标的说法,不正确的是: A. 精确率是指分类正确的样本占全体样本的比例。 B. 准确率是指分类为正例的样本中标签为正例的比例。 C. 召回率是指标签为正例的样本中分类为正例的比例。 D. AUC 是由阈值从小到大增加过程中,模型分类的假阳性率以及真阳性率变化趋势进行绘制的。
逻辑斯谛回归虽然引入了非线性的逻辑斯谛函数,但通常仍然被视为线性模型,试从模型参数化假设的角度解释原因。
如果某模型的 AUC 低于 0.5,是否有办法立即得到一个 AUC 高于 0.5 的模型?
对于一个二分类任务,数据的标签和对于预测正例的概率如下表所示,试画出 ROC 曲线并计算模型的 AUC 值。
表 6-4 习题 6 概率表
设数据集中包含样本
(提示:考虑 ROC 曲线上每一段横线和竖线的意义。对于选出的负样本
对于
从二分类到多分类。
(1)推导 softmax 函数
(2)将
(3)利用(1)和(2)的结果实现多分类的梯度下降算法,并在multiclass.csv
数据集上测试。该数据集每一行包含 3 个数字,依次为样本的
本章介绍的逻辑斯谛回归模型的参数化假设为
在逻辑斯谛回归中,我们假设
其中,
通过选取不同的
就得到高斯分布。令
就得到伯努利分布。读者可以自行代入验证。
在假设了
接下来,我们建立参数化模型预测
得到的结果与线性回归模型相同。对于伯努利分布,有:
得到的结果与逻辑斯谛回归模型相同。可以发现,这里逻辑斯谛函数是由伯努利分布和 GLM 假设自然导出的。
由线性模型到广义线性模型是一个由具象到抽象的过程。普通的线性模型的应用场景非常有限,但将其中的内核抽象出来,得到广义线性模型后,就可以解决所有适用于指数分布族的问题。除了我们详细介绍的线性回归模型和逻辑斯谛回归模型之外,泊松分布、多项分布、分类分布等都可以导出不同的模型,可以在不同的任务中发挥作用。