线性模型这一章,我在西瓜书上画的重点比哪一章都多。不是因为它难,恰恰是因为它“简单”——简单到几乎所有后续模型的推导都能在这里找到影子。很多人刷《机器学习》(西瓜书)第三章时容易犯一个毛病:觉得线性回归嘛,不就是拟合一条直线,公式一看就懂,于是匆匆翻过去。等看到神经网络、支持向量机的时候才发现,梯度下降、正则化、损失函数这些概念的直觉,其实在第三章就已经埋下了。这篇文章就当是我自己重刷第三章的一份深度笔记,把线性模型的来龙去脉、推导细节和实操中容易踩的坑一次说清楚。
1. 从“线性组合”说起:线性模型到底在学什么
1.1 西瓜书里那个最朴素的公式,后面藏了多少信息
如果用一句话概括线性模型,那就是:用属性的线性组合来做预测。形式就是那个最早出场的公式:
text复制f(x) = w1*x1 + w2*x2 + ... + wd*xd + b
写成向量形式就是 f(x) = w^T x + b。
第一次看这个公式,很多人会觉得,这有什么好学的?不就是多元一次方程嘛。但真正往里想一层,你会发现事情没那么简单。这里面的 w 是有物理意义的——它表示的是“每个属性对最终结果的贡献权重”。w 是正的,说明这个属性和结果正相关;w 是负的,说明负相关;w 的绝对值越大,说明这个属性的影响越大。
这个特性直接决定了线性模型最大的优势:可解释性。在实际业务里,尤其是风控、医疗、司法这类场景,你不能扔给决策者一个黑盒模型说“这是深度神经网络算出来的概率,你信我就行”。你得能说清楚“这个用户为什么被拒贷”——因为他的负债率这个特征的权重是 0.35,已经超出了阈值。这就是线性模型在工业界至今仍有一席之地的根本原因。
同时,线性模型也是理解“模型”这个概念最好的起点。搞明白“训练”到底在干什么——其实就是找到一组参数 w 和 b,让预测值 f(x) 和真实值 y 之间的误差最小——后面看任何复杂模型,你都能往这个框架里套。
1.2 线性不是“简单”的代名词:可解释性才是它的底牌
很多人误以为线性模型只能处理线性可分的数据,这其实是个大误解。线性模型的“线性”是指:在参数 w 上是线性的。也就是说,模型输出是参数的线性函数。至于输入特征 x 怎么变换,完全可以非线性。
西瓜书后面讲支持向量机的时候会提到“核技巧”,本质就是把数据映射到高维空间再线性分割。但在线性模型这里,你不需要那么高级的工具,直接对特征做变换就行。比如你想让模型能拟合一个抛物线趋势,不一定非要上多项式回归,你完全可以在特征里人为加一个 x2 = x^2 的字段,然后模型还是用线性回归去拟合。这个时候预测函数 w1*x + w2*x^2 + b 在 x 上不是线性的,但你在代码里其实依然是在解一个线性回归问题。
这一点在面试里经常被拿出来问,也是一个常见误区。理解了这个,你就明白了为什么经典的机器学习流程里,特征工程这么重要——因为在模型能力有限的情况下,你的输入特征决定了模型效果的上限。线性模型就是那个最典型的例子:它处理不了复杂的非线性关系,但你可以通过特征工程帮它把非线性关系“翻译”成线性关系。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 对数几率回归:披着回归外衣的分类器
2.1 为什么非得用对数几率函数,不能直接用阶跃函数
第三章第一个真正烧脑的地方,就是对数几率回归(Logistic Regression)。名字里带“回归”,干的却是分类的事。
它的引入逻辑非常自然:当你想要用线性模型做二分类时,最直接的思路是找一个“阈值函数”——输出大于某个值判为正类,小于某个值判为负类。最理想的是单位阶跃函数,也就是常说的符号函数 sign:
text复制y = 0, 若 z < 0; y = 0.5, 若 z = 0; y = 1, 若 z > 0
但问题来了:这个函数不可导。不可导意味着你没法用梯度下降这类基于导数的优化方法去求解参数 w 和 b。你可以说“那我不需要导数啊,我直接暴力枚举不就行了”——但高维参数空间里暴力枚举是不现实的。
于是需要找一个既能近似阶跃函数、又处处可导的替代品。这就是对数几率函数(logistic function)登场的原因:
text复制y = 1 / (1 + e^(-z))
它的图像是一条平滑的 S 形曲线,当 z → +∞ 时 y → 1,当 z → -∞ 时 y → 0。关键在于:它把线性模型的输出 z 映射到了 (0,1) 区间,这个区间天然可以解释成“正类的概率”。
我把这个函数的图像画出来过很多次,每次看到那条 S 曲线都觉得很妙——它在中间近似线性、在两端饱和。这种“两端饱和”的特性,既让它在分类边界附近有较强的区分能力,又保证了整个函数的可导性。
2.2 极大似然估计推导里的几个容易被绕晕的点
对数几率回归的推导过程,基本是顺着这条线走的:
- 把
y当成正类的后验概率P(y=1|x)。 - 由对数几率函数反推,得到
ln(y/(1-y)) = w^T x + b。 - 用极大似然估计来求解参数
w和b。
这里最容易绕晕的地方出现在第三步。很多人会问:为什么线性回归用的是最小二乘(即最小化均方误差),到这里却突然换成了极大似然?
我的理解是:线性回归假设了误差项服从高斯分布,在这个假设下,极大似然估计和最小二乘在数学上是等价的——最小二乘其实是高斯噪声下极大似然的一个特例。而对数几率回归做分类时,输出 y 服从伯努利分布(二项分布),这个时候再用均方误差就既不自然也不方便了,直接用极大似然才会导出那个标准的交叉熵损失。
具体的推导过程我就不从零写了,只把最关键的一步列出来。单个样本的对数似然是:
text复制ℓ = y_i * ln(ŷ_i) + (1 - y_i) * ln(1 - ŷ_i)
这其实就是信息论里的交叉熵。把所有样本加起来取负,就是我们要最小化的损失函数。如果你用过 PyTorch 或者 TensorFlow,里面的 BCELoss(二元交叉熵损失)本质上就是这个公式。所以可以从这里拉一条线:对数几率回归 → 极大似然估计 → 交叉熵损失 → 深度学习里的分类损失,整条线是通的。
另外还有一个面试高频点:为什么逻辑回归的损失函数不用均方误差(MSE)?原因有两个。第一是对数几率函数是 S 形的,如果套用到 MSE 上,损失函数是非凸的,有很多局部最小值,梯度下降很难找到全局最优解。第二,从信息论的角度看,负对数似然天然对应预测分布和真实分布之间的 KL 散度,本质上是衡量两个分布之间的差异,比数值层面的平方误差更合理。
2.3 实操:sklearn里LogisticRegression的默认参数陷阱
理论归理论,回头说实操。我用 sklearn 跑逻辑回归的时候,踩过一个特别隐蔽的坑——默认的 penalty 是 'l2'。
也就是说,即使你不做任何设置,LogisticRegression() 默认就会在损失函数后面加一个 L2 正则项。这本身没什么问题,问题是很多初学者拿它和别的库做对比时,会发现“为什么我手写梯度下降实现的逻辑回归和 sklearn 结果差那么多?”很大概率就是这个正则项造成的。
另外一个常见问题是收敛警告。如果你的数据量特别大、特征也多的那种场景,sklearn 默认的求解器 lbfgs 有时会在迭代次数到达上限前停止,控制台会打出 ConvergenceWarning。解决办法有两个:一是调大 max_iter,比如 LogisticRegression(max_iter=1000);二是对特征做标准化,因为 lbfgs 这类基于梯度的优化器对特征的尺度非常敏感,尺度差异太大会让收敛过程变得很慢。
我现在的处理习惯是:做逻辑回归之前,先对连续型特征做 StandardScaler 标准化,把离散型特征做 one-hot 编码,然后再丢进模型训练。这样即使不调参,效果也能保持一个不差的下限。
3. 线性判别分析(LDA):从“找方向”理解降维
3.1 类内散度与类间散度:两个矩阵怎么定义出来的
线性判别分析(Linear Discriminant Analysis,LDA)是第三章里另一个值得多看几遍的内容。它和逻辑回归的角度完全不同:逻辑回归是在“给定数据,直接预测类别”;LDA 是在“找一个投影方向,让投影之后不同类别的样本尽可能分得开”。
这个“找方向”的思路用一句话概括就是:类内离散度小,类间离散度大。你可以想象一群红点(正类)和一群蓝点(负类)散布在二维平面上。你要找一条直线,把所有点投射到这条直线上,希望红点和蓝点投射之后能清晰地分为两堆,不要混在一起。
为了实现这个目标,西瓜书定义了类内散度矩阵 S_w 和类间散度矩阵 S_b。我看到这两个矩阵的符号时,第一反应是:怎么又来了两个矩阵?但细看定义会发现它们其实很好理解。
- 类内散度矩阵
S_w:衡量的是每个类别内部样本的离散程度,也就是“每个点到该类均值向量的距离平方和”。这个值越小,说明同类样本越集中。 - 类间散度矩阵
S_b:衡量的是不同类别均值向量之间的离散程度,也就是“各类均值点(加权后)到全局均值点的距离平方和”。这个值越大,说明各个类别的中心隔得越远。
最后的目标函数是最大化广义瑞利商:
text复制J = (w^T S_b w) / (w^T S_w w)
用自然语言翻译就是:我找一个方向 w,让投影后的类间距离尽量大、类内离散尽量小。
3.2 LDA的完整求解思路与一个小例子
求解的过程其实也有套路。目标函数是 w 的广义瑞利商,最大化它等价于解一个广义特征值问题:
text复制S_b w = λ S_w w
对二分类问题来说,S_b w 的方向始终指向“两个类均值之差”的方向,化简到最后你会发现解出来的 w = S_w^(-1) * (μ0 - μ1)。这个公式好多教材直接甩出来,没有讲为什么,我当初对着推了两遍才确认:本质上就是因为 S_b 是秩为 1 的矩阵,它唯一的非零特征值对应的特征向量就是两类均值之差的方向。
我写过一个极小的二分类例子帮助自己理解。假设两类样本:
- 正类样本点:
(2, 3), (3, 4), (4, 3) - 负类样本点:
(6, 5), (7, 6), (6, 7)
先说结论:即使不做任何计算,你也能感觉到这两堆点的整体方向是从左下到右上的。LDA 找到的投影方向不会是和横轴平行的方向,而是顺着“两类中心连线”的那个方向,这样投影之后两类点的分布区间才能尽量错开。
如果手动计算,步骤是:
- 分别求两类样本的均值向量:正类
(3, 10/3),负类(19/3, 6)。 - 求类内散度矩阵
S_w(就是每个类内各点到均值的协方差矩阵之和)。 - 求
w = S_w^(-1) * (μ0 - μ1)。
我这个例子里算出来的 w 大约是 (-0.87, -0.49) 归一化后的方向,指向左下到右上的反方向。把两类点投影到这个方向上,正类的投影值集中在 -4.2 附近,负类集中在 -7.8 附近,明显分成了两堆。
这种手推小例子可能看起来“没什么用”,但它帮我建立了对 LDA 的直觉。以后我遇到高维数据,需要做有监督降维时,会第一时间想到 LDA——它比 PCA 多利用了类别信息,在分类场景下往往能给出比 PCA 更有判别力的低维表示。
3.3 LDA与PCA的边界感
初学者经常把 LDA 和 PCA 搞混,因为两者都做降维。我的理解是:
- PCA 是无监督的,它找的是“数据方差最大的方向”,不关心任何类别标签。它在意的是“怎么投影才能保留最多的原始信息”。
- LDA 是有监督的,它找的是“类别最可分的方向”,充分利用了类别标签。它在意的是“怎么投影才能让不同类别的数据最容易分辨”。
我见过不少人一上来就问“PCA 和 LDA 哪个好”,其实这俩压根就不是在回答同一个问题。如果只是做数据压缩、可视化、去除噪声,用 PCA;如果是为了提高分类性能,而且有标签可用,可以考虑 LDA。
但它们也可以结合使用。我在处理高维稀疏矩阵做文本分类时,先用 PCA 把维度从几万降到几百,再用 LDA 进一步降到几十维,效果比只用其中任何一种都要稳。原理上也说得通:PCA 去掉了一部分冗余噪声,让 LDA 的类内散度矩阵 S_w 更可靠——高维场景下 S_w 往往是奇异的(不可逆),LDA 根本求不出 w,所以先降维是必要的。
4. 多分类拆解与类别不平衡:工程上真正烦人的事
4.1 一对多、一对一、多对多:拆法的选择时机
实际任务里很少有真正只分两类的情况,更多是三分类乃至上百分类。线性模型本身是二分类器(逻辑回归输出两个类别的概率),那怎么把它扩展到多分类?西瓜书给了三个基本策略:OvO(一对一)、OvR(一对其余)、MvM(多对多)。
我最早学的时候总是记不住它们各自的开销,后来总结了一张表:
| 策略 | 分类器数量 | 训练开销 | 预测开销 | 适用场景 |
|---|---|---|---|---|
| OvO | C(k,2) | 每个分类器只用两类数据,较小 | 需跑所有分类器,较大 | 类别数小、每类样本量多 |
| OvR | k | 每个分类器用全部数据,较大 | 只需跑 k 个分类器 | 类别数较多、每类样本量均衡 |
| MvM | 取决于编码 | 中等 | 中等 | 类别多且有特定纠错需求 |
这个表有反直觉的地方:OvO 的分类器数量虽然多,但每个分类器的训练数据量只是两个类别的样本总和,训练反而快;OvR 只需要 k 个分类器,但每个分类器都要面对全部样本,规模化之后训练开销一点不小。
我的一次实际经历是做一个手写数字识别任务,10 个类别,每类 500 个样本。我试了 OvO 和 OvR 两种模式,最后发现 OvO 在精度上小优势,OvR 在速度上小优势。差别不太大。但在类别数特别多的场景(比如 100 类),OvO 需要跑 C(100,2)=4950 个分类器,预测时要全部过一遍,时间开销很感人。这种情况下我一般选 OvR。
4.2 纠错输出码(ECOC):为什么要“冗余”
多对多(MvM)策略里,西瓜书重点介绍了纠错输出码(ECOC)。这个思路我第一次看的时候觉得太妙了:把多分类问题转化成多个二分类问题,然后通过“冗余编码”来纠错。
举个例子。假设有 4 个类别,你可以设计一个 5 位的编码表:
text复制类1: 1 1 1 0 0
类2: 0 0 1 1 1
类3: 1 0 0 0 1
类4: 0 1 0 1 0
训练的时候,构造 5 个二分类器,每个分类器按照对应位置是 1 还是 0 把 4 个类别分成两组进行训练。预测的时候,把新样本喂给 5 个分类器,得到一串预测结果(比如 1 0 1 0 0),然后和每个类别的编码算海明距离,距离最小的那个类别就是预测结果。
关键在于:如果某个分类器判断错了,比如第 3 位本来是 1 被判成 0,但整体编码串仍然可能和正确类别的距离最短。这就是纠错能力的来源——冗余位越多,容错能力越强。
我在实际工作中其实很少直接用 ECOC,因为大多数库(比如 sklearn)已经内置了 OvO/OvR 策略,而且深度学习方法天然支持多分类。但 ECOC 的思想在面试里是高频考点,而且在你需要在自定义二分类器之上做多分类时,它就是那个最正统的思路。
4.3 类别不平衡:阈值移动比过采样更优雅
第三章最后一个大问题是类别不平衡。什么叫不平衡?正负样本比例悬殊,比如 1000 个样本里只有 10 个正类。这种情况下,哪怕你什么都不做,把所有样本都预测成负类,准确率也有 99%——模型看似“不错”,实际毫无用处。
西瓜书给出的核心解决思路是阈值移动(threshold moving)。逻辑回归输出的概率 y 在 0.5 处作为默认阈值,对应着“正类 vs 负类的几率”等于 1 的判断。但如果数据里正类只有 5%,那么理论上应该把“几率”的门槛调低,也就是在原本的几率上乘一个系数 m_负 / m_正:
text复制y' / (1 - y') = (y / (1 - y)) * (m_负 / m_正)
这就是“再缩放”(rescaling)。它的潜在前提是训练集是总体样本的无偏估计。如果这个前提不成立,实际的阈值偏移应该根据业务场景来定,这在很多风控场景里尤其明显。
我的实际做法不一定直接改模型,更常用的是在决策阶段调整阈值。比如在 sklearn 里,逻辑回归的 predict_proba 输出的概率不一定非要以 0.5 作为切分点,我可以计算不同阈值下的精确率、召回率,画 PR 曲线,选一个业务上最优的点。这样比单纯过采样/欠采样更能保留数据的原始分布信息。
当然,数据层面也可以做处理。欠采样和过采样各有各的问题:欠采样会丢弃大量样本,可能丢掉重要信息;过采样如果只是简单复制样本,容易过拟合。更稳的做法是用 SMOTE 这类合成少数类样本的方法,或者同时配合阈值移动一起使用。
5. 我刷这一章时踩过的坑和整理的记忆方法
5.1 三个最容易背混的公式
刷完这一章,最常被问到的一个问题就是:这章公式这么多,怎么背?
我的经验是不要死背,而是理解每个量在“做什么”。三个最容易混的公式我梳理一下:
-
线性回归的均方误差:
E(w,b) = Σ(yi - ŷi)²。它是“预测值和真实值之间距离的平方和”,是最直观的一个。对应几何意义是“找一条直线,让所有点到直线的竖直距离平方和最小”。 -
对数几率回归的交叉熵损失:
Loss = -Σ[yi * ln(ŷi) + (1-yi) * ln(1-ŷi)]。它是“预测概率分布和真实分布之间的差异”。当yi=1时,只剩-ln(ŷi),你预测得越离谱,惩罚越大;yi=0时同理。它的好处是梯度比较平稳,不会像 MSE 那样在 S 曲线两端梯度接近 0 导致学习停滞。 -
LDA 的广义瑞利商:
J = w^T Sb w / w^T Sw w。它是“类间距离 / 类内距离”的比值。记忆时想着“越大越好”就顺了。
一个特别实用的技巧是:把这几个公式和对应的中文直觉绑定。以后遇到任何新模型,先问自己三件事:这个模型的损失函数衡量的是什么?它的优化目标是什么?它和线性模型的关系是什么?带着这三个问题去读论文,效率会高很多。
5.2 面试常问的几个细节
这些年我面试候选人时,或者自己准备面试时,收集了几个和线性模型相关的高频问题,列在这里供参考:
-
线性回归能不能做分类?
可以,但不推荐。线性回归的预测值没有上下界,分类任务要求输出是概率(有界的),强行用线性回归做分类,异常点会把拟合直线拉偏。 -
为什么逻辑回归中特征要归一化/标准化?
因为正则化项对特征尺度敏感。如果某个特征的取值特别大,它在损失函数中占的权重就会异常大,导致模型性能下降。标准化之后,梯度下降的收敛速度也会快很多。 -
LDA 中可以有两个以上投影方向吗?
可以。把广义瑞利商从一维推广到多维,本质是解广义特征值问题,取前 k 个最大特征值对应的特征向量,投影到 k 维空间。这也是类别数比较多时可以做的降维方案。 -
逻辑回归的系数如何解释?
如果用的是标准化后的特征,系数的绝对值可以大致衡量特征的重要性;如果想更精确地解释,可以用exp(w)计算“几率比”(odds ratio)——特征每增加一个单位,正类的几率变为原来的exp(w)倍。这个在业务分析里非常实用。
5.3 从公式到代码:一个完整的复现练习
最后安利一个我每次带新人必用的练习:不借助 sklearn,用 numpy 手写一个线性回归和逻辑回归,然后在西瓜数据集(或者其他公开数据)上和 sklearn 的结果做对比。
核心代码可以很短。线性回归的闭式解就是正规方程:
python复制import numpy as np
# 假设 X 是形状为 (m, d+1) 的矩阵,最后一列为全 1(偏置项)
# y 是形状为 (m, 1) 的真实值
w = np.linalg.inv(X.T @ X) @ X.T @ y
逻辑回归就需要用梯度下降来迭代求解了,核心的梯度公式推导自交叉熵损失:
python复制# z = X @ w,sigmoid = 1 / (1 + exp(-z))
# 损失函数对 w 的梯度 = X.T @ (sigmoid(z) - y) / m
for epoch in range(num_epochs):
z = X @ w
h = 1 / (1 + np.exp(-z))
gradient = X.T @ (h - y) / len(y)
w -= lr * gradient
这几行代码看着简单,但当你看到手写模型的收敛曲线和 sklearn 的 coef_ 越来越接近时,你对“模型训练”这件事就有了一个完整的闭环感受。这也是我强烈推荐所有初学者做的一个练习——只有亲手推一遍、写一遍、调一遍,线性模型才真正变成你的底层直觉。
后面再去看神经网络、SVM、树模型,你会发现它们的起点其实都是同一个问题:怎么找到一组参数,让预测尽可能接近真实。而线性模型,就是这个“一切开始”的地方。
