ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

多分类与正则化:从Softmax到弹性网的工程实践

多分类与正则化:从Softmax到弹性网的工程实践 搞了这么多年机器学习我见过太多人卡在“二分类很熟一遇到多分类就懵”的状态。刚入门的时候大家接触最多的确实是二分类垃圾邮件是不是、用户会不会流失、故障要不要告警。可真实业务里大多数问题天生就是多分类——手写数字要识别10个类别电商商品要分到几十个类目文本要判情感倾向甚至细粒度情绪。与此同时只要类别一多模型参数跟着涨过拟合就像影子一样贴上来。所以我把多分类和正则化放在同一章讲这两件事在工程里总是一起出现多分类把任务变复杂正则化把模型拉回冷静。如果你是刚学完逻辑回归、想看清新旧知识怎么衔接的初学者或者已经在用现成模型但总被过拟合和准确率虚高坑到的从业者这一章都值得细看。我会从原理拆到代码再到排查技巧尽量把每一步“为什么这么设计”说清楚。1. 多分类问题的核心思路从“是/否”到“选一个”1.1 为什么二分类的思路不能直接套用二分类的逻辑回归输出的是一个概率 p表示“属于正类的概率”然后用 0.5 做阈值划界。到了多分类任务变成了“从 K 个候选中选一个”比如给图片分类成猫、狗、鸟给新闻分成体育、财经、娱乐。你当然可以拆成多个二分类来做但怎么拆、怎么合并里面有很多权衡。最常见的两种拆法是 OvROne vs Rest和 OvOOne vs One。OvR 对每个类别训练一个“是不是这个类”的分类器预测时看哪个分类器的得分最高OvO 则对每两个类别都训练一个分类器预测时让所有分类器投票。这两种方案简单直观适合类别不多、模型本身不带概率输出的情况但缺陷也很明显OvR 容易出现类别不平衡OvO 需要训练 K*(K-1)/2 个模型类别一多开销暴涨。真正更优雅的做法是让模型一次性输出一个 K 维的概率分布。这就是 softmax 回归它的核心思想是每个类别先得到一个原始得分然后通过 softmax 把这些得分转化成“谁是这一类的概率”。这种做法端到端、训练稳定、预测速度快也是神经网络里多分类的标准姿势。这一章我们就围绕 softmax 这条路展开。1.2 Softmax函数到底在做什么Softmax 的公式写出来很简单softmax(z_i) exp(z_i) / sum_j exp(z_j)其中 z_i 是模型给第 i 个类别的原始得分logits分母把所有类别的 exp 值加起来。这样处理后每个输出都在 0 到 1 之间并且总和等于 1天然就是一个概率分布。我用一个生活化的例子说明。假设期末评优有三个候选人评委打分分别是 2、1、0。直接看分数A 似乎最好但分数之间的差距并不直观。softmax 做了两件事第一指数函数 exp 会放大差距2 分的优势被放大得比 1 分明显得多第二除以总和后得到一个“A 占 70% 概率、B 占 20%、C 占 10%”的排序。换句话说softmax 把一条“谁分高谁赢”的规则变成了“好到什么程度”的概率语言。工程上有一个非常关键的细节如果 logits 很大比如 100exp(100) 会直接溢出变成无穷大。所以真正实现时都会先减去最大值。因为 exp(z_i - max) / sum_j exp(z_j - max) 和原始公式完全等价但数值范围被压到了安全区。这个“减最大值”的 trick 是我每次手写 softmax 必加的别等到 inf/NaN 出现才想起来。1.3 交叉熵损失分类任务的首选损失函数有了概率输出还需要一个损失函数来衡量“猜得好不好”。分类任务里最常用的是交叉熵cross entropy公式是L -sum_k y_k * log(p_k)其中 y_k 是真实标签的 one-hot 编码p_k 是模型预测的概率。因为真实标签只有一个位置是 1所以交叉熵实际只关注模型给正确类别的那一个概率正确类别的概率越接近 1损失越小。为什么多分类不用均方误差MSE我踩过这个坑。MSE 对概率输出的梯度在 softmax 叠加后会变得非常平缓模型学得很慢而且 MSE 会把“概率分布完全一致”作为目标这对分类任务来说太严格、也不必要。交叉熵直接拉大错误预测的惩罚梯度更明显收敛也快得多。所以只要是多分类第一选择就是交叉熵这在框架里都是标配。2. 正则化技术专治“背答案”的模型2.1 过拟合的本质模型开始背答案而不是学规律多分类模型参数比二分类多能力强的模型很容易记住训练集里所有样本包括噪声和偶然模式。这就导致训练集准确率越练越高测试集却不见涨甚至下跌。我把这种行为叫“背答案”考试前把题目和标准答案背得滚瓜烂熟但换一套题就露馅。从统计学习角度看这是典型的方差不降反升模型过于灵活对不同数据集的适应性太强。过拟合的典型症状就是训练集和验证集的准确率曲线出现明显剪刀差。正则化的本质就是给模型的复杂度加上约束逼它不要那么“自信满满地背答案”。2.2 L2正则化让权重别太嚣张L2 正则化也叫岭回归、weight decay是最常见的正则手段。它的思路简单粗暴在原始损失函数后面加一项“所有权重的平方和”乘上一个系数 λL_total L_original λ * sum(w^2)为什么这样能防止过拟合想象一下模型的决策边界如果特别扭曲通常意味着某些维度的权重特别大。加了平方惩罚后权重越大受到的压力就越大模型只能被迫把权重分散、压小。权重小了模型对个别特征的依赖就弱决策边界更平滑对新样本的适应性更强。梯度更新时L2 正则会额外产生一个 λ*w 的项相当于每次更新都在做“权重衰减”。这也是 PyTorch 里优化器中的 weight_decay 的由来。实际操作中我喜欢把 L2 系数设成 1e-4 到 1e-2 之间的量级具体看数据量数据越多需要的正则越弱。2.3 L1正则化自动做特征选择L1 正则化Lasso在损失函数里加入的是所有权重的绝对值之和λ * sum|w|。它的神奇之处在于会产生稀疏解——很多权重被直接压到 0。也就是说L1 自带“特征选择”功能会告诉你哪些特征对任务真正重要。为什么 L1 能压到 0我用几何直觉解释L1 的约束区域是菱形最优解很容易落在坐标轴上该维度的权重正好为 0L2 的约束区域是圆形最优解很少落在坐标轴上权重只会变小很少归零。所以在特征很多、你怀疑大部分特征无关的时候L1 更合适如果特征都挺有用、只是要控制幅度L2 更合适。2.4 弹性网L1与L2的组合拳“到底用 L1 还是 L2”这个问题经常让人犹豫。答案是成年人不做选择两个都要这就是弹性网Elastic NetL_total L_original λ1 * sum|w| λ2 * sum(w^2)弹性网把 L1 的稀疏性和 L2 的平滑性结合在一起尤其适合特征之间存在相关性、单纯 L1 只会随机选其中一个的情况。实际落地时我会先用少量特征快速试验搞清楚数据特性再决定要不要上弹性网。sklearn 的 SGDClassifier 里直接支持 penaltyelasticnet调两个系数就行。2.5 容易被忽略的正则化手段很多人一提正则化就只想到 L1/L2其实工程里还有一批非常有效的手段。Dropout 是深度学习的经典正则训练时随机让一部分神经元失活强迫模型不依赖单个节点。早停early stopping是我最常用的技巧监控验证集指标一旦开始变差就立刻停止训练简单又有效。数据增强对图片旋转、裁剪、加噪声本质也是正则化因为模型看到更多变化后自然就更鲁棒。还有一种叫一致性正则化的思想在半监督学习里特别常见让模型对同一输入的不同增强版本输出一致相当于用“稳定性”来约束模型减少对噪声的过度拟合。3. 实操Python里完整跑一遍多分类与正则化3.1 数据集准备与实验设计这一节我用 sklearn 自带的手写数字数据集共 10 类1797 个样本。这个数据集不大但非常适合观察多分类和正则化的效果因为它真实、有噪声又不至于让训练等太久。先导入库并加载数据import numpy as np import matplotlib.pyplot as plt from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns digits load_digits() X, y digits.data, digits.target print(X.shape, y.shape) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) scaler StandardScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test)注意stratifyy是做分层采样保证训练集和测试集里每个类别的比例一致。对于多分类任务这一步非常重要否则有些类别可能训练集里特别少测出来指标虚高。3.2 三行代码跑通Softmax多分类sklearn 的 LogisticRegression 默认就支持多分类关键是设置multi_classmultinomial再选一个能处理多分类的求解器比如 lbfgsmodel LogisticRegression( multi_classmultinomial, solverlbfgs, C1.0, max_iter2000 ) model.fit(X_train, y_train) train_acc model.score(X_train, y_train) test_acc model.score(X_test, y_test) print(fTrain acc: {train_acc:.4f}) print(fTest acc: {test_acc:.4f})我用这个数据跑出来的结果大约在训练 99% 左右、测试 97% 左右。到这里一个多分类模型已经能跑了。但如果只看到这一行结果很容易自欺欺人因为准确率是全局平均值不代表每个类都好。3.3 混淆矩阵与分类报告读懂模型真正错在哪准确率告诉你答对多少混淆矩阵告诉你错在哪一类。这是一个非常重要的诊断工具y_pred model.predict(X_test) cm confusion_matrix(y_test, y_pred) plt.figure(figsize(8, 6)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues) plt.xlabel(Predicted) plt.ylabel(True) plt.show() print(classification_report(y_test, y_pred))read 混淆矩阵时我看三个地方第一对角线是不是明显亮第二对角线之外哪些格子数值偏高那代表模型特别容易混淆某两个类别第三有没有某一行几乎全黑说明真实类别被系统性漏预测。分类报告里的 precision、recall、f1-score 每个类别都要看一眼。比如某个类别 recall 偏低说明它经常被辨认成其他类别如果 precision 偏低说明其他类别经常被误认成它。工程上这两个指标往往需要权衡不能只盯准确率。3.4 正则化强度对比实验画出一条调参曲线sklearn 里C是正则化强度的倒数C 越小正则化越强C 越大正则化越弱。我们做一组实验观察 C 从 0.001 到 1000 变化时训练集和测试集准确率的走势C_list [0.001, 0.01, 0.1, 1, 10, 100, 1000] train_scores [] test_scores [] for C in C_list: clf LogisticRegression( multi_classmultinomial, solverlbfgs, CC, max_iter2000 ) clf.fit(X_train, y_train) train_scores.append(clf.score(X_train, y_train)) test_scores.append(clf.score(X_test, y_test)) plt.figure(figsize(8, 5)) plt.plot(C_list, train_scores, markero, labelTrain) plt.plot(C_list, test_scores, markers, labelTest) plt.xscale(log) plt.xlabel(C (log scale)) plt.ylabel(Accuracy) plt.legend() plt.show()这条曲线就是过拟合和正则化最直观的证据。C 很小时模型被约束得太死训练集和测试集都低这是欠拟合C 很大时训练集接近满分但测试集可能开始回落这是过拟合。最佳 C 通常落在两者之间也就是测试集曲线最高的地方。3.5 手动实现弹性网正则化为了彻底理解正则化如何介入梯度更新我用 SGDClassifier 示范弹性网。它通过 loss、penalty、alpha 三个参数组合出不同的正则方式from sklearn.linear_model import SGDClassifier sgd_elastic SGDClassifier( losslog_loss, penaltyelasticnet, alpha1e-4, l1_ratio0.5, max_iter2000, random_state42 ) sgd_elastic.fit(X_train, y_train) print(fElastic Net test acc: {sgd_elastic.score(X_test, y_test):.4f})l1_ratio控制 L1 和 L2 的比例1 就是纯 L10 就是纯 L20.5 就是各一半。这个参数不用每次重头猜可以放进网格搜索里一起调。想手写实现一个带 L2 的 softmax 回归也不难核心就这么几步def softmax(z): z_shift z - np.max(z, axis-1, keepdimsTrue) exp_z np.exp(z_shift) return exp_z / np.sum(exp_z, axis-1, keepdimsTrue) def fit_softmax(X, y, lr0.1, epochs200, lam0.01): n_samples, n_features X.shape n_classes np.max(y) 1 W np.random.randn(n_features, n_classes) * 0.01 b np.zeros(n_classes) Y_onehot np.eye(n_classes)[y] for epoch in range(epochs): score X.dot(W) b prob softmax(score) grad_W X.T.dot(prob - Y_onehot) / n_samples lam * W grad_b np.mean(prob - Y_onehot, axis0) W - lr * grad_W b - lr * grad_b return W, b每一步梯度都清晰可见第一项是预测与真实标签的残差第二项就是 L2 正则带来的梯度衰减。这种手写实现跑不了大数据集但用来理解原理非常值。4. 常见问题与排查技巧实录4.1 准确率很高但某些类别完全没被预测到这类问题在多分类里非常常见尤其是类别数量多、样本不均衡的时候。模型为了降低整体损失会倾向于把所有样本都判给样本量大的类别导致小类别在预测结果里几乎消失。此时准确率看起来不低但一查混淆矩阵某一行全是零。解决办法有几种一是用class_weightbalanced给样本少的类别更高权重二是做重采样对小类别过采样三是在评估时多看 macro F1 而不是整体准确率。我自己的习惯是第一步永远先看 classification_report别只看准确率。4.2 Softmax输出全部挤在一起看不出置信度如果预测概率分布非常接近均匀分布比如每个类别概率都在 0.1 左右说明模型还没学会区分特征。常见原因包括特征没有标准化、学习率太低/太高、模型容量不足。另一个可能是 logits 本身太小softmax 的分母把所有概率摊平了。排查路径我一般是先检查数据预处理有没有做标准化再看训练损失是否下降如果 loss 一直不降大概率是学习率设置不合理最后确认模型是否训练了足够多轮次。碎片时间可以先跑一个小模型快速验证特征是否有效。4.3 正则化系数拉满后训练集也不准了有些同学一看到过拟合就猛加正则化结果训练集和测试集双双崩盘。这是因为正则化太强时模型的权重被压到接近 0决策边界变成一条几乎没有弯曲的直线连训练集里的基本模式都抓不住。这时候模型处于欠拟合状态。正则系数不是越大越好它是在“模型复杂度”和“数据拟合程度”之间找一个平衡点。我的经验是先不加正则把模型跑到过拟合记录训练/测试曲线再逐步增加正则强度观察测试集回升然后又回落的区间最后用交叉验证选择那个区间里的最优值。4.4 混淆矩阵对角线很亮但业务场景里还是没法用这种情况最扎心。模型在均衡测试集上表现优秀一旦部署到真实环境发现某些类别完全失效。原因通常是训练数据的类别分布和真实场景不一致测试集里每个类别各占 10%但真实业务里某一类可能占 90%。模型在高频类别上哪怕有 1% 的误差绝对错误量也会很大。解决办法是在评估阶段就模拟真实分布用带权重的指标来选模型。另外如果业务更在意某几类比如医疗筛检更在意“有病不能漏”那就不要用默认 0.5 的决策阈值可以换成自定义阈值优先保证高危类别的 recall。4.5 数值稳定性的坑exp溢出怎么办手写 softmax 时最容易遇到的坑就是溢出。logits 一旦接近 100exp(100) 变成天文数字轻则出 inf重则直接 NaN。解决方式就是之前提到的减最大值技巧。无论你用什么框架理解这个技巧背后的数值原因能帮你在排查 NaN 问题时少走很多弯路。顺便说一个容易混淆的知识点交叉熵和 softmax 在框架里经常组合使用比如 PyTorch 的 CrossEntropyLoss 已经在内部做了 log_softmax所以喂进去的是原始 logits 而不是概率。你在自己拼模型时千万别重复对概率再算一次交叉熵否则数值会很奇怪。5. 写在实验之后我的几点体会多次调参跑完这套实验我最大的体会是多分类和正则化从来都不是孤立的两个知识点它们是同一枚硬币的两面。多分类扩大了模型的表达能力正则化则负责给这种能力套上缰绳。真正重要的不是记住某个公式而是形成一套诊断习惯先看训练集和测试集的差距再看混淆矩阵的分布最后才动手调正则化系数。还有个实用小建议实验记录一定要留档尤其是每次改了什么 C、l1_ratio、数据预处理方式都记一笔。很多时候你回头看自己一个月前的实验会惊讶地发现折腾半天的参数其实和默认值效果差不多。把时间和精力花在数据清洗、特征理解、类别分布分析上往往比花在精细调正则化系数上更有回报。多分类的上手之路走通一次后面就顺了。
返回列表