第六章是很多刚开始啃统计学习的人一道绕不过去的坎,因为这一章同时把两个模型放在一起讲:逻辑斯蒂回归(Logistic Regression)和最大熵模型(Maximum Entropy Model)。表面上看,一个是线性分类器,一个是约束最优化问题,但书里用“对数线性模型”这条线索把它们串成了同一个家族。学完这一章,你能彻底搞清楚两件事:为什么逻辑回归的损失函数长成那个样子、以及最大熵模型到底在“最大”什么。
这本书一贯的风格是先给模型定义,再讲策略,最后给算法。第六章也完全按照这个骨架来。我在实际读的时候发现,如果只看公式推导,很容易迷失在符号里;但一旦把“模型、策略、算法”三要素拉出来对照,整章的逻辑链条就清晰了。这篇笔记我就按这个思路,从骨架、公式、算法、代码、踩坑五个层面来拆。
1. 这一章到底在讲什么:第六章的骨架
1.1 章节定位:从感知机到概率模型
在整本书的叙事顺序里,第二章感知机是“硬分类”,直接输出sign(w·x),类别是离散的 0 或 1;第四章朴素贝叶斯开始引入概率视角,但假设特征之间强独立;到了第六章,模型终于学会“输出概率”了,而且是基于线性函数直接建模条件概率,不再需要独立性假设。
这个定位很重要。逻辑斯蒂回归没有做任何特征独立假设,它只是假设“对数几率是特征的线性函数”,剩下的交给参数去学习。相比朴素贝叶斯的生成式思路,逻辑回归是判别式模型,直接建模P(Y|X),所以它对特征之间的关系更鲁棒,在真实场景里往往比朴素贝叶斯表现更好。
最大熵模型则更狠,它不预设任何模型结构,而是通过“约束条件 + 最大熵原理”从所有可能的概率分布里挑一个最“公平”的。书里先讲逻辑回归,再讲最大熵,其实是故意安排的:前者是后者的特例,后者是前者的推广。学完第六章你会形成一种直觉——很多分类模型本质上都在做同一件事:用特征的线性组合去参数化条件概率。
1.2 为什么两个模型被放在同一章
从形式上看,最大熵模型的求解结果是一个指数族分布:
P(y|x) = (1 / Z(x)) * exp( sum_i w_i * f_i(x, y) )而二项逻辑斯蒂回归:
P(Y=1|x) = exp(w·x) / (1 + exp(w·x))如果把特征函数f_i(x,y)设计成“x 的某个分量乘以 y 的指示变量”,再归一化,逻辑斯蒂回归就恰好是最大熵模型的特例。这也就是书里 6.2 节末尾点明的结论:二项逻辑斯蒂回归是最大熵模型的一个特殊情形。
两个模型放在同一章还有一个教学上的好处:读者能对比两种建模哲学。逻辑斯蒂回归是“先假定线性结构,再估计参数”;最大熵模型是“只给约束,由熵最大原理反推出结构”。一个演绎,一个归纳,殊途同归。
1.3 学完这章你能解决什么问题
在实际工程里,逻辑回归可能是工业界使用最广泛的 baseline 模型:可解释性强、训练快、对稀疏特征友好。理解它的损失函数和正则来源,直接决定你调参的时候是在瞎试还是有的放矢。
最大熵模型现在很少被单独拿出来做生产模型,但它的特征函数、对偶优化、约束期望匹配这些概念,是理解条件随机场(CRF)和序列标注问题的地基。很多做 NLP 的朋友回头补这一章,就是为了后面啃 CRF 不费劲。
2. 逻辑斯蒂回归:从线性模型到分类模型的桥
2.1 模型定义与对数几率比
感知机的输出是w·x的符号,但符号只给类别,不给置信度。逻辑斯蒂回归的改进非常朴素:把w·x塞进 sigmoid 函数,压到 (0,1) 区间,把它解释成概率。
二项逻辑斯蒂回归的模型是:
P(Y=1|x) = exp(w·x) / (1 + exp(w·x)) P(Y=0|x) = 1 / (1 + exp(w·x))这里我把偏置项直接并进 w 了,也就是 x 里补了一列常数 1,这样写法更干净。
关键概念是对数几率(odds):
log( P(Y=1|x) / (1 - P(Y=1|x)) ) = w·x也就是说,模型不是在直接拟合概率,而是在拟合“对数几率”这个连续值关于 x 的线性函数。这也是“回归”二字的由来——它本质上做了回归,只不过回归目标是对数几率,再通过 sigmoid 映射回概率。理解了这一点,你就明白为什么逻辑回归虽然叫回归,却是个分类模型。
2.2 极大似然估计:损失函数为什么长那样
既然模型输出概率,最自然的参数估计方式就是极大似然。把两类合并写:
P(Y=y|x) = P(Y=1|x)^y * (1 - P(Y=1|x))^(1-y)对 N 个样本取对数似然,再取负号变成最小化问题:
L(w) = - sum_i [ y_i * (w·x_i) - log(1 + exp(w·x_i)) ]很多初学者会问:为什么不用平方损失?因为逻辑回归的输出是伯努利分布,平方损失不是这个分布的负对数似然,用它做优化目标会导致非凸问题,梯度下降容易卡在局部最优。而对数似然是关于 w 的凸函数,梯度表达式也非常漂亮:
grad = sum_i ( P(Y=1|x_i) - y_i ) * x_i这个形式和线性回归的最小二乘梯度(预测值 - 真实值) * x_i长得几乎一样,只是“预测值”换成了 sigmoid 输出。这也是逻辑回归实现起来特别简洁的原因。
2.3 多项逻辑斯蒂回归与 Softmax
把二分类推广到 K 类,就得到了多项逻辑斯蒂回归,也就是 softmax 回归:
P(Y=k|x) = exp(w_k·x) / sum_j exp(w_j·x)二分类时 softmax 会退化成 sigmoid,但参数化略有差异:sigmoid 只需要一组参数 w,softmax 需要 K 组参数。实际实现中会存在冗余参数,所以通常固定其中一组为 0,或者直接加正则。
写代码的时候,softmax 分母里的sum_j exp(w_j·x)在 w_j·x 比较大时会上溢。标准解法是 log-sum-exp 技巧:
log sum_j exp(z_j) = m + log sum_j exp(z_j - m),其中 m = max_j z_j这个坑我在第 6 节还会专门展开。
3. 最大熵模型:一个约束下的最优化问题
3.1 最大熵原理:为什么熵最大
最大熵原理说人话就是:在对未知情况做预测时,没有充分依据,就不该做任何主观假设,选择熵最大的那个分布。
举个例子,扔一个六面骰子,如果没有任何信息,你会默认每个面概率是 1/6,因为这是熵最大的分布。现在如果告诉你“点数的期望是 3.5”,均匀分布已经不满足约束了,这时最大熵原理会给出一个尽量“平”的分布,而不是某个极端分布。这个“尽量平”背后就是熵在起作用——熵度量的是分布的不确定性,熵越大,说明模型对未知保留的余地越多。
在分类模型里,我们手里有训练数据的经验分布P~(x,y),我们希望学到的P(y|x)在满足某些特征期望匹配的条件下,熵尽可能大。这样模型不会因为训练样本少而过拟合到某些偶然的模式上。
3.2 特征函数与约束条件的构造
最大熵模型里的“特征”不是 x 本身,而是特征函数f_i(x,y),通常是一个二值指示函数。比如在 NLP 里可以定义:
f_1(x, y) = 1 如果 x 是“银行”且 y 是“名词” 0 否则特征函数刻画的是“输入 x 和输出 y 是否共现了某种组合”。模型的约束条件是:模型关于特征的期望等于训练数据中特征的期望。
经验分布下的特征期望:
E_p~(f_i) = sum_{x,y} P~(x,y) * f_i(x,y)模型的条件期望:
E_P(f_i) = sum_{x,y} P~(x) * P(y|x) * f_i(x,y)令二者相等,就能保证模型在训练数据上至少不会遗漏这些“共现信息”。
这里有一个容易困惑的点:为什么要用P~(x)而不是P(x)?因为我们不知道真实的 P(x),只能用经验分布近似。约束的实质是让模型在“我们看到的数据”上对这些特征统计量做到无偏。
3.3 拉格朗日对偶:如何把约束优化转成无约束
最大熵模型的学习问题是一个带约束的优化问题:
max_P H(P) = - sum_{x,y} P~(x) * P(y|x) * log P(y|x) s.t. sum_y P(y|x) = 1 E_P(f_i) = E_p~(f_i), i = 1,...,n直接求解很麻烦。书上用拉格朗日对偶:把约束罚到目标函数里,构造成拉格朗日函数L(P, w),然后先对 P 求极小,再对 w 求极大。
之所以可以这样干,是因为原问题是凸的,满足强对偶条件。对 P 求极小之后,P 可以被 w 显式表达出来:
P_w(y|x) = (1 / Z_w(x)) * exp( sum_i w_i * f_i(x,y) )其中 Z_w(x) 是归一化因子。
看到这个形式,再回头看逻辑斯蒂回归的 sigmoid,你会发现它们都是“特征的线性组合 → 指数 → 归一化”。最大熵模型通过“约束 + 熵最大”自动推导出了这个形式,这就解释了为什么两者是同一个家族。
4. 模型学习的两种路径:IIS 与拟牛顿
4.1 改进的迭代尺度法:IIS 到底在迭代什么
最大熵模型的对偶函数是:
Ψ(w) = sum_{x,y} P~(x,y) * sum_i w_i * f_i(x,y) - sum_x P~(x) * log Z_w(x)目标是最大化 Ψ(w),但没有解析解,只能迭代。改进的迭代尺度法(IIS)是书里重点介绍的算法,核心思路是:每一轮对每个参数 w_i 求一个增量 δ_i,使得 Ψ(w+δ) 比 Ψ(w) 大。
推导的关键技法是利用log t <= t - 1这个不等式做放缩,然后把 δ_i 解出来。当特征总和f#(x,y) = sum_i f_i(x,y)是常数时,δ_i 有闭式解:
δ_i = (1 / f#) * log( E_p~(f_i) / E_P_w(f_i) )如果 f# 随样本变化,就需要用牛顿法迭代求解每个 δ_i。
我看书看到这里有一个体会:IIS 属于“单坐标上升”,每轮只更新一个参数,好处是不用算海森矩阵,坏处是收敛比较慢。它是个理论优雅的算法,但实际工程里很少直接用。
4.2 用 BFGS 求解最大熵模型,简单又稳
好消息是,最大熵模型的对偶函数 Ψ(w) 是光滑凸函数,梯度可以直接写出来:
∂Ψ / ∂w_i = E_p~(f_i) - E_P_w(f_i)这个梯度形式非常直观:它恰好是经验特征期望和模型特征期望的差。于是任何无约束优化算法都能用,其中最实用的就是拟牛顿法。
BFGS 的迭代形式是:
w^(t+1) = w^(t) - ρ_t * H_t * grad_t其中 Hessian 逆矩阵用一阶梯度差来近似更新,不需要算二阶导,收敛速度比梯度下降快得多。现代科学计算库都有 L-BFGS 实现,直接调用就行。
我在实际写最大熵模型的时候,第一版用 IIS,收敛慢得让人怀疑人生;后来换成 L-BFGS,几轮迭代就收敛了。所以我的建议是:理解 IIS 有助于理解对偶原理,但实现时优先选 L-BFGS。
4.3 收敛判据与正则化的处理
收敛判据一般看两点:对数似然的变化量小于阈值(比如 1e-5),或者梯度的范数足够小。工程上还会限制最大迭代次数,防止死循环。
正则化也很关键。最大熵模型的参数 w 在特征稀疏时容易变得很大,导致过拟合。加 L2 正则后,对偶函数变为:
Ψ(w) - λ * sum_i w_i^2梯度对应地变成E_p~(f_i) - E_P_w(f_i) - 2λ w_i,实现改动就一行。L1 正则也可以加,只是优化时要处理不可导点,通常用近端梯度。
5. 从公式到代码:用 NumPy 复现逻辑回归与最大熵
5.1 准备一份可直接复现的数据
为了演示,我用经典的鸢尾花数据集,取其中两个特征,保留两个类别,做成二分类任务。数据先做标准化,再划分训练集和测试集,比例 8:2。
import numpy as np from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler iris = load_iris() # 只取前两个特征,类别只取 0 和 1,方便二分类演示 X = iris.data[:100, :2] y = iris.target[:100] scaler = StandardScaler() X = scaler.fit_transform(X) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 )标准化这一步不能省,否则特征尺度差异大,梯度下降会很慢甚至不收敛。这是我一开始图省事跳过、后来吃了亏才加回来的。
5.2 逻辑斯蒂回归的梯度下降实现
逻辑回归实现起来非常短。核心就三步:sigmoid 计算概率、算梯度、更新参数。
def sigmoid(z): # 用 np.clip 防止 exp 溢出 z = np.clip(z, -500, 500) return 1.0 / (1.0 + np.exp(-z)) def train_logistic(X, y, lr=0.1, epochs=300, lam=1e-3): X = np.hstack([X, np.ones((X.shape[0], 1))]) # 补一列偏置 w = np.zeros(X.shape[1]) for _ in range(epochs): p = sigmoid(X @ w) grad = X.T @ (p - y) / len(y) + lam * w w -= lr * grad return w w = train_logistic(X_train, y_train)这里lr是学习率,lam是 L2 正则系数。梯度前面除以样本数len(y)是为了让梯度大小不受 batch 大小影响,这样学习率在不同数据集规模下不用频繁调整。
我在调试时发现,如果不做特征标准化,同样的学习率在标准化前后的收敛速度能差出几个量级。所以不管用什么模型,数值稳定性永远是第一位的。
5.3 最大熵模型的 IIS 实现要点
最大熵模型的实现核心在特征构建。这里为了演示,我把特征函数定义为(x 的某个特征连续值分桶后与 y 的组合)这种二值指示函数。实现时可以预先给每个样本生成一个特征索引列表,然后按 IIS 公式更新。
# 简化版特征构建:对每个特征做一个二值化阈值,构造 (feature_bucket, y) 组合 def build_features(X, y): # 返回 (样本索引 -> 特征索引列表), 特征总数 # 实际中这一步是最大熵模型最耗时的地方 pass def iis_train(X, y, max_iter=100): # 初始化 w 全零 # 每轮迭代: # 1. 计算每个样本在当前 w 下的 P_w(y|x) # 2. 统计每个特征的经验期望 E_p~(f_i) # 3. 统计每个特征的模型期望 E_P_w(f_i) # 4. 对每个特征 i 求 delta_i,更新 w_i passIIS 的更新公式是δ_i = log(E_p~(f_i) / E_P_w(f_i)) / f#,注意如果E_P_w(f_i)为 0 要加平滑,否则 log 会炸。我在自己写的时候遇到过一个麻烦:特征数量一大,内存里存不下所有样本的特征矩阵,后来改成稀疏表示才解决。工程上特征函数通常都是稀疏的,用 CSR 稀疏矩阵存储是标配。
5.4 实验对比:它们真的是同一类模型
在同样的数据上分别用逻辑回归和最大熵模型训练,最后得到的测试准确率会比较接近,这正是因为二者本质上是同一族模型。差异主要来自特征构造:逻辑回归直接用连续特征,最大熵模型用了分桶后的离散指示特征,信息有损失,所以性能会略低。
如果你把最大熵模型的特征函数改成“连续特征本身乘以 y”,那它的参数化和逻辑回归就几乎一模一样了。这就是书上说的“逻辑斯蒂回归是最大熵模型特例”的实证版本。我建议学到这里时亲手跑一遍这个对比,对这种“模型家族感”的理解会比只看公式深得多。
6. 我踩过的坑:数值稳定性与实践建议
6.1 log-sum-exp 与 sigmoid 溢出
这是我在写 softmax 和最大熵模型时踩过最狠的坑。exp(50)大概是 5e21,exp(1000)直接就 inf 了。逻辑回归的 sigmoid 虽然取值范围是 (0,1),但如果w·x算出来特别大,np.exp(-z)可能变成 0,导致概率直接等于 1,梯度变 NaN。
解决办法有两个:一是像上面代码那样对 z 做 clip;二是用 scipy 的expit函数,它内部处理了极端数值:
from scipy.special import expit p = expit(z)对于 softmax 分母和最大熵模型的 Z_w(x),标准做法是 log-sum-exp:
def logsumexp(z): m = np.max(z) return m + np.log(np.sum(np.exp(z - m)))原理很简单:所有 z_i 同时减去 m,数值范围被压到安全区间,最后再加回来。这个技巧在深度学习里也经常用,值得刻进肌肉记忆。
6.2 特征函数与稀疏特征的处理
最大熵模型的特征通常是高维稀疏的,比如 NLP 里的“当前词是否为某个词”这种指示特征。稀疏带来的问题有两个:模型期望E_P_w(f_i)的计算要遍历所有样本,但每个样本只涉及少量特征,所以应该用稀疏矩阵存储和计算;另外,如果一个特征在训练数据里出现次数太少,经验期望近似为 0,训练出的参数会极不稳定。
我的实操建议:对特征做频率过滤,出现次数少于 5 次的特征直接丢掉。这点在 CRF 工具里也是标配操作。特征工程上,连续特征要分桶或归一化后再构造指示函数,否则模型对离群点非常敏感。
6.3 正则项:不加就是过拟合
无论是逻辑回归还是最大熵模型,在高维稀疏特征下不加正则,训练到后期参数绝对值会越来越大,测试集表现反而变差。这是因为模型在用极端参数去“死记”训练数据里的偶发共现。
L2 正则的实现就一行改动:
grad = X.T @ (p - y) / len(y) + lam * w # 逻辑回归 grad_i = E_p(f_i) - E_P_w(f_i) + 2 * lam * w_i # 最大熵调参时 lam 从 1e-3 起步,每 10 倍一档搜索。如果数据维度特别高,可以试 L1 正则,它会直接把不重要的特征参数压成 0,相当于做了特征选择。
6.4 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 训练 Loss 出现 NaN | exp 溢出 / 学习率过大 | 用 expit、log-sum-exp;调低学习率 |
| 训练 Loss 下降慢 | 特征未标准化 | 对连续特征做标准化或归一化 |
| 测试指标远差于训练 | 过拟合 | 加 L2/L1 正则,减少迭代次数 |
| IIS 收敛极慢 | 单坐标更新效率低 | 换成 L-BFGS 或梯度下降 |
| 某个特征参数巨大 | 特征出现次数太少 | 做频率过滤,去掉低频特征 |
| 预测概率全为 0/1 | 特征尺度极端 | 检查数据是否有离群点,做 clip |
还有一个我用血的教训换来的经验:学习率不是越大越好。我之前用 1.0 的学习率跑逻辑回归,Loss 在前几步确实掉得飞快,随后直接爆炸成 NaN。后来统一从 0.1 起步,观察 Loss 前 50 轮的变化趋势再调整,稳定很多。如果 Loss 曲线呈锯齿状剧烈震荡,说明学习率偏大;如果曲线平缓得像在爬坡,说明偏小。
我个人读这一章最大的收获,是意识到很多模型本质上共享同一套“线性组合 + 指数族分布 + 归一化”的框架。逻辑回归、softmax 回归、最大熵模型、甚至条件随机场,都是这个家族的不同表达。搞懂第六章,再往后看支持向量机和提升方法时,你会带着“它们各自在优化什么损失函数”这个视角去对比,学习效率会高很多。如果你正在啃这本书,建议把第六章的公式推导亲手过一遍,再跑一遍上面的代码,比单纯看书来得实在。