1. 从“要不要”到“选哪个”:softmax回归解决的真问题
很多人刚接触机器学习时,最先学会的往往是逻辑回归那套二分类套路——判断一封邮件是垃圾邮件还是正常邮件,判断一个用户会不会流失,判断一张图片里有没有猫。这类问题都只有两个答案,输出一个0到1之间的概率,设个0.5的阈值,完事。
但现实世界没有那么多的“非此即彼”。手写数字识别要判断0到9共10个类别,新闻分类要区分体育、财经、科技、娱乐、军事,商品评论要打1星到5星,这些都叫“多分类问题”,结果的取值是离散的类别标签。这时候你会发现,逻辑回归那套sigmoid激活函数的逻辑没法直接搬过来用——你总不能让模型先算“是不是体育”,再算“是不是财经”,挨个跑十遍。
softmax回归就是为解决这个问题而生的。它在逻辑回归的基础上,把二分类的概率输出扩展到了多分类:输入一个样本,模型给出它在每个类别上的得分,经过softmax归一化后变成一组和为1的概率分布,哪个类别的概率最高,就预测为哪一类。整个过程一步到位,不需要拆成多个二分类来绕。
我见过不少初学者对softmax有个误解,觉得它是一个“新的算法”。其实严格来说,softmax回归本质上是多项式逻辑回归,是逻辑回归在多分类场景下的自然延伸。理解了这一点,你对整个模型的来龙去脉就清晰了一大半。
这篇内容适合的读者,是已经接触过线性回归或逻辑回归、但还没有系统整理过多分类思路的人。无论你是要搭一个图像分类Demo,还是做文本情感的多档位判断,或者处理工业场景里的故障类型识别,这篇文章都可以帮你把softmax回归的来龙去脉、数学原理、实操细节一次理清楚。我会先讲明白它为什么能解决“离散值多分类”的问题,再拆解损失函数和梯度推导,最后给出完整可跑的代码和问题排查经验。
2. 核心思路拆解:为什么softmax能处理离散的多分类
2.1 逻辑回归的“公平比较”困局
要理解softmax回归,得先回到它的前身逻辑回归。假设现在有三类:A、B、C。一个朴素的想法是,我训练三个独立的二分类器,分别判断“是不是A”“是不是B”“是不是C”,然后看哪个分类器的置信度最高,就选哪个。
这个方案听起来合理,但有一个致命问题:三个分类器各自独立训练,它们的输出尺度不在一个水平线上。甲分类器说“是A的概率0.6”,乙分类器说“是B的概率0.4”,你根本无法直接比较这两个数,因为它们的决策边界和权重体系完全是两套标准。打个比方,这就像三个人用三把刻度不同的尺子量同一个物体,甲量出60厘米,乙量出40厘米,你说60厘米更长——尺子都不一样,数字再大也没有意义。
softmax回归解决这个问题的思路非常优雅:它不训练三个独立的分类器,而是训练一个模型,同时输出在所有类别上的得分,然后用softmax函数把这组得分统一压缩到同一个概率空间里。所有类别用同一套比较标准,谁高谁低一目了然。
2.2 softmax函数的“挤压”机制
softmax函数的数学形式长这样:
对于第k个类别,它的输出概率为:
P(y=k|x) = exp(z_k) / Σ_j exp(z_j)
其中z_k是模型对第k个类别的线性得分(即权重w与特征x的点积加偏置b),分母是对所有类别得分取指数后的总和。
这个函数做的事情可以拆成两步理解。
第一步,指数化。每个得分z都取指数变成exp(z)。这一步有两个好处:一方面,exp函数的输出恒为正,保证概率非负;另一方面,指数运算放大了得分之间的差距——得分最高的类别在指数化后优势被进一步拉大,模型在做决策时更有“底气”。
第二步,归一化。所有类别指数化后的值相加得到总和,每个类别除以这个总和,得到的数落在0到1之间,并且所有类别加起来恰好等于1。这就把一串任意范围的实数,变成了一组合法的概率分布。
这里我插一句:为什么要用exp,而不是直接用z做归一化?除了保证非负和放大差异之外,还有一个数学上的便利——exp函数求导特别友好,导数就是自身,后面推导梯度的时候会省很多事情。这个细节在手工推导梯度时非常重要,但大多数教程不会点破。
2.3 和“一对多”方案的本质区别
前面提到了拆成三个二分类器的方案,这在工程上叫“一对多”(One-vs-Rest)。它的确能解决多分类问题,但softmax回归和它是两套完全不同的逻辑,区别主要有三点。
第一,训练目标不同。“一对多”训练三套独立的权重,每套权重只关心自己的那个类别;softmax回归训练一套权重矩阵,同时优化所有类别的预测结果,它是全局最优的。
第二,概率解释不同。“一对多”输出的三个概率不能直接比较,因为前面说了,尺度不一致;softmax输出的概率是严格归一化的,可以直接当作模型对各类别的置信度。
第三,决策边界不同。“一对多”的决策边界是多个超平面的组合,可能出现“模糊区域”——某个样本落入A分类器和B分类器的置信度都很低,谁都不管;softmax的决策边界把整个特征空间划分成了多个互斥的区域,任何一个样本点都会明确地落在某个类的区域内。
这也就是为什么在类别互斥的场景下,比如图像分类、文本分类、故障类型判别,softmax回归通常是首选方案。而“一对多”更适合那些类别本身不互斥的场景,比如一篇文章同时可以归类为“科技”和“深度”,标签可以多选。
2.4 softmax回归的适用边界
任何模型都有它的适用边界,softmax回归也不例外。你要判断自己的问题适不适合用softmax回归,可以从三个维度来看。
第一,标签必须是离散的类别。如果你的目标值是一个连续数值,应该用线性回归或其它回归模型;如果目标值虽然连续但可以分档,比如评分1到10,也可以做离散化后用softmax,但这等于放弃了数值之间的序关系,需要权衡。
第二,类别之间必须互斥。一个样本只能属于一个类别。如果你的任务里一条数据可以同时属于多个类别,那这不是多分类问题,而是多标签问题,应该用多个二分类器或其它专门的方法。把多标签问题硬套到softmax上,模型会学得很别扭,因为它被强迫“只能选一个”。
第三,特征与类别之间的关系大体是线性的。softmax回归的决策边界是线性的,如果你的数据分布呈现出强烈的非线性模式,比如同心圆、螺旋形,那么softmax回归的分类效果会很差。这时候你需要引入特征工程(比如核变换)、加深网络变成多层感知机,或者直接换用其它非线性模型。
一句话总结,softmax回归适合“类别互斥、标签离散、边界近似线性”的多分类场景。这是它最朴素的适用条件,也恰恰是它作为深度学习分类任务输出层被广泛使用的原因——即使前面的特征提取网络已经把非线性问题转化为线性可分的高维表示,最后一层仍然是一个softmax分类器。
3. 损失函数与梯度推导:为什么交叉熵是最佳搭档
3.1 为什么不沿用平方误差
很多从线性回归过来的人,第一反应是用平方误差(MSE)作为损失函数,因为线性回归里就是这么做的。但在softmax回归里,平方误差是个糟糕的选择,原因可以从两个角度理解。
从优化角度看,softmax函数是高度非线性的,配合平方误差做损失时,损失函数对参数的梯度容易出现饱和现象。当模型预测错误的置信度很高时,softmax的输出会变得非常“尖锐”,此时梯度反而会变得很小,模型更新缓慢,训练效率低下。这在直观上很反直觉——错得很离谱反而学不动。
从概率角度看,平方误差隐含的假设是误差服从高斯分布,而多分类问题的标签是一个one-hot的离散向量,本质上是类别分布。用一个为连续误差设计的损失函数去拟合离散分布,尺度上就对不上,结果自然不好。
3.2 交叉熵的构造逻辑
交叉熵损失函数衡量的是模型预测的概率分布与真实标签分布之间的差异。它的形式如下:
L = -Σ_k y_k · log(p_k)
其中y_k是真实标签的第k维,p_k是模型对第k类的预测概率。
这个式子看似抽象,但在单标签分类任务里可以大幅简化。真实标签经过one-hot编码后,只有真实类别那一维是1,其余全是0。所以求和项里只有真实类别那一项不为零,损失函数简化为:
L = -log(p_true)
p_true是模型给真实类别分配的概率。如果模型对真实类别的预测置信度很高,p_true接近1,损失趋近于0;如果模型认为真实类别的概率很低,比如只有0.1,损失就是-log(0.1)≈2.3026。惩罚力度随着置信度的下降而急剧增大,这正好符合我们的直觉。
3.3 梯度推导的关键步骤
现在到了这篇文章最硬核的部分:softmax回归的梯度推导。我不会跳过任何关键步骤,因为这是理解整个模型训练过程的核心。
先定义符号。假设有N个训练样本,每个样本有d维特征,总共有K个类别。模型的线性得分z = Wx + b,其中W是K×d的权重矩阵,b是K维偏置向量。softmax概率输出为p_k = exp(z_k) / Σ_j exp(z_j)。
对单个样本,交叉熵损失为L = -Σ_k y_k · log(p_k)。我们要计算L对权重矩阵W中某个元素W_ij的偏导数。W_ij连接的是第i个输出维度和第j个输入特征维度。
利用链式法则:
∂L/∂W_ij = (∂L/∂z_i) · (∂z_i/∂W_ij) = (∂L/∂z_i) · x_j
所以问题归结为求∂L/∂z_i,这一项是softmax回归梯度推导中最关键也最容易出错的地方。
由于p_i是z_i的函数,而L又是p的函数,需要再展开一层链式法则。但这里有个坑:L对p_i的依赖并不是直接的,因为L是通过对所有类别的p_k求和得到的,而每个p_k都依赖z_i(因为softmax的分母是全体得分之和)。所以不能只考虑L对p_i的偏导,还要考虑其它类别p_k(k≠i)对z_i的依赖。
分两步走。
第一步,对正类(i等于真实类别,即y_i=1)的情况。此时L = -log(p_i)(其它项因为y_k=0而消失)。而p_i = exp(z_i)/Σ_j exp(z_j)。利用商数法则求∂L/∂z_i,推出来结果是p_i - 1。
第二步,对负类(i不等于真实类别,即y_i=0)的情况。此时L = -log(p_true),但p_true的分母里含有exp(z_i),所以z_i的变化会影响p_true的值。通过链式法则推出来结果是p_i。
把两种情况合并,就得到一个极其简洁的表达式:
∂L/∂z_i = p_i - y_i
这个式子漂亮得不像是推出来的,但它千真万确。再乘以x_j,得到:
∂L/∂W_ij = (p_i - y_i) · x_j
对偏置b_i的梯度同理,等于p_i - y_i。
用作者经验总结一下这个推导结论,哪怕你不深究中间过程,也要把最终结果记牢:softmax回归的梯度形式是“预测概率减真实标签”再乘以输入特征。这和你在线性回归里“预测值减真实值”乘特征的形式如出一辙,非常容易记忆和验证。
3.4 数值稳定性:你可能会踩的第一个坑
把softmax和交叉熵分开实现看起来很简单,但在实际计算中会遇到一个极其常见的数值问题。
假设某个类别得分z_k非常大,比如500,那么exp(500)在双精度浮点数下直接溢出为inf,后面的计算全部崩溃。更隐蔽的是,即使不溢出,当所有得分都很大时,exp运算的精度也会下降,导致softmax输出出现不正常的分布。
解决办法是在计算softmax之前做一次平移:对所有得分统一减去其中的最大值。
p_k = exp(z_k - z_max) / Σ_j exp(z_j - z_max)
因为所有z都减去了同一个常数,softmax的结果在数学上完全不变。但exp(z_k - z_max)的最大值被限制在exp(0)=1,彻底避免了溢出风险。这是所有深度学习框架内部都在用的标准做法,但我见过不少手写实现的项目在这里栽过跟头,所以你要么直接用框架自带的交叉熵函数,要么自己动手时务必加上这一步。
4. 实操过程与核心实现:从零手写一个softmax回归
4.1 数据准备与处理流程
我以一个实际的模拟项目为例——某图像处理Demo里的手写数字分类任务。数据集包含若干张28×28的灰度图,每张图对应一个0到9的数字标签。这是softmax回归最经典的练手场景,数据规模适中,特征维度也不算太高。
整个实操流程可以按五步推进:
- 把每张28×28的图片展平成784维的向量,所有样本拼成一个形状为[N, 784]的特征矩阵X
- 对特征做标准化处理,将像素值从0到255缩放到0到1区间
- 把标签转成one-hot编码,形状为[N, 10]的矩阵Y
- 初始化权重矩阵W为[784, 10]的全零或小随机数矩阵,偏置b为长度为10的零向量
- 进入梯度下降迭代训练,更新W和b
标准化这一步经常被忽略,但它对收敛速度的影响非常大。原始像素值的范围太宽,会让梯度更新在刚开始时很不稳定。缩放到0到1之后,整个优化过程会平滑得多。如果你在实操中遇到loss下降非常缓慢的问题,优先检查特征有没有做标准化。
4.2 完整实现代码与参数解析
我用Python和NumPy手写一个最精简但功能完整的实现,不依赖任何深度学习框架,方便你把模型内部的每个细节都看清楚。
import numpy as np class SoftmaxRegression: def __init__(self, n_features, n_classes, lr=0.1): self.W = np.random.randn(n_features, n_classes) * 0.01 self.b = np.zeros(n_classes) self.lr = lr def softmax(self, z): # 减去最大值保证数值稳定性 z_shifted = z - np.max(z, axis=1, keepdims=True) exp_z = np.exp(z_shifted) return exp_z / np.sum(exp_z, axis=1, keepdims=True) def predict_proba(self, X): z = X @ self.W + self.b return self.softmax(z) def predict(self, X): proba = self.predict_proba(X) return np.argmax(proba, axis=1) def compute_loss(self, X, y_onehot): proba = self.predict_proba(X) # 防止log(0),加一个极小值 loss = -np.mean(np.sum(y_onehot * np.log(proba + 1e-12), axis=1)) return loss def fit(self, X, y_onehot, epochs=200): n = X.shape[0] for epoch in range(epochs): proba = self.predict_proba(X) # 梯度计算,核心就是这一行 grad_w = (X.T @ (proba - y_onehot)) / n grad_b = np.mean(proba - y_onehot, axis=0) self.W -= self.lr * grad_w self.b -= self.lr * grad_b if epoch % 20 == 0: loss = self.compute_loss(X, y_onehot) print(f"epoch {epoch}, loss = {loss:.4f}")代码的核心就是fit方法里的那一行grad_w = X.T @ (proba - y_onehot) / n。乍一看可能觉得不明所以,但如果你还记得前面的梯度推导结论——“预测概率减真实标签再乘特征”,这一行就是批量样本下的矩阵表达形式。proba是模型对每个样本在每个类别上的预测概率,y_onehot是真实标签,二者相减得到一个[N, K]的误差矩阵,转置乘X再除以样本数,就是权重的平均梯度。
有几个细节值得说说。
初始化权重使用的是标准差0.01的小随机数。为什么不用全零?如果W全是零,所有类别的得分相等,softmax输出是对称均匀分布,梯度为零,模型无法打破对称性,训练会卡死。小随机数的作用就是打破这种对称。
学习率lr设置为0.1。这个值对这类小型任务一般够用,但如果你发现loss震荡不降,可能是学习率偏大;如果收敛太慢,可以适当调大或使用学习率衰减策略。
log里加了1e-12的极小值,是为了防止预测概率恰好为0时log(0)导致NaN。这是一个防御性编程习惯,虽然实际数据里概率为0的情况不多,但一旦出现,整个训练过程都会崩掉。
4.3 梯度下降变体:从全量梯度到随机梯度
上面实现用的是全量梯度下降,也就是每个epoch都计算所有训练样本的梯度。这在样本量不大时没有问题,但一旦数据量膨胀到百万级,每次迭代都要扫一遍全量数据,计算成本会让人崩溃。
实际工程里更常用的是小批量梯度下降,每次随机抽取一小部分样本(比如64或256条)计算梯度并更新参数,既保证了计算效率,又引入了适当的随机性,有助于跳出局部最优。这种变体在deep learning里叫mini-batch gradient descent,是训练神经网络的事实标准。
在特征维度特别高、类别数特别多的场景下,还有另一个省事的技巧值得介绍——直接把这个问题看成是多组逻辑回归的同时求解。每个类别独自对应一组系数,但共享同一个softmax归一化分母。这听起来像废话,但实操里的确有不少人会把多分类拆成一个一个跑,白白丢失了共享统计信息。只要类别互斥,就应该用完整的softmax回归一把梭。
4.4 决策边界可视化
对二维特征或者降维后的特征,强烈建议把softmax回归的决策边界画出来看一看。决策边界其实就是每个点取argmax后的区域划分,当所有类别的得分z相等时,就是类别的交界线。
可视化最大的价值在于快速验证模型是否学对了。我经常在实践里看到loss已经降到很低,但画出来的决策边界歪歪扭扭,大部分样本落在错误区域——这通常说明特征工程出了问题,或者数据本身存在离群点干扰。只盯着loss曲线很难发现这类问题,但一张图画出来,问题无处遁形。
如果你用的是三维特征,也可以画3D散点图,用颜色标注真实类别,再叠加每个类的决策区域。把特征降到二维来观察,哪怕损失一点信息,也值得。
5. 常见问题与排查技巧实录
5.1 损失值不下降或下降极慢
这是最常见的状况。出现这个问题的原因一般有四个方向。
第一个方向是学习率太小。梯度下降的学习率决定了参数更新的步长,如果设成0.001甚至更低,在小数据集上可能要几千个epoch才能看到明显变化。先用一个较大的学习率(比如0.1或0.5)做个快速试验,看loss有没有下降趋势,再逐步调整。
第二个方向是特征没有标准化。前面提过,原始像素值0到255的尺度会让梯度更新方向不一,收敛异常缓慢。把特征缩放到0到1或标准化到均值为0方差为1后,问题通常会立刻缓解。
第三个方向是初始化不当。如果权重随机初始化的范围太大,初始得分会很大,softmax输出趋于极端,梯度接近饱和区,初期更新很小。用标准差0.01或Xavier初始化都可以规避。
第四个方向是标签编码错误。如果你手打了one-hot编码,一定要检查y_onehot的每一行是否恰好只有一个1。常见错误是标签从1开始计数而不是从0开始,导致one-hot错位,模型训练八九不离十但loss永远降不到理想水平。用代码打印前几个样本对比检查,是排查这类低级错误最快的方式。
5.2 出现NaN或inf
loss变成NaN通常只有两个原因。要么是log(0)直接出现,要么是exp溢出。
log(0)的防护方案前面讲过了,加上一个极小值即可。exp溢出的防护方案也讲过,softmax之前统一减去最大值。如果你直接调用框架自带的交叉熵损失,这些细节框架内部已经处理好了,但要小心的是,某些手写实现或者教学代码里不会做这些防护,一旦你用到极端数据,问题就会暴露。
还有一个容易忽视的来源:数据里本身有缺失值或无穷值。NumPy在做矩阵乘法时会把缺失值传播到整个结果矩阵,导致梯度变成NaN。做数据预处理时务必先检查数据质量。
5.3 过拟合与欠拟合的平衡
softmax回归是线性模型,它的拟合能力有限,一般不会出现特别严重的过拟合,但如果特征维度远大于样本量,仍然会出问题。一个典型场景是文本分类,词表可能有几万维,但训练样本只有几千条。
应对策略有三个。最常见的是L2正则化,在损失函数后面加上权重平方和乘以一个正则系数λ,梯度更新时多减一项2λW。其次是早停法,在验证集loss开始上升时提前终止训练。如果数据量实在太少,还有一个简单粗暴但有效的办法:减少特征维度,比如做PCA降维后再训练,模型泛化能力往往还有提升。
反过来,欠拟合的表现是loss在训练集上就居高不下。这时要审视特征是否包含足够信息,或者数据本身是否线性可分。如果数据分布是明显非线性的,softmax回归再调参也无济于事,这时候要么引入核函数做特征变换,要么升级为多层神经网络模型。
5.4 一个容易忽视的陷阱:类别不平衡
真实业务数据里,类别不平衡是常态。比如故障类型识别场景,正常运行状态可能占99%,故障A占0.5%,故障B占0.3%,其它故障加起来0.2%。如果你直接训练softmax回归,模型很快会发现“全部预测为正常”就能拿到99%的精度,但它对故障样本完全没有鉴别能力。
处理类别不平衡我一般会这么做:
第一,用混淆矩阵而不是总体精度来评估模型。精度在小类上没有任何参考意义,要分别看每一类的精确率和召回率。
第二,在训练时对损失做类别加权。给样本量少的类别一个更高的损失权重,迫使模型更重视这些类别的准确性。在框架里一般表现为class_weight参数。
第三,如果极端不平衡,考虑对少数类做过采样,或对多数类做欠采样。我个人的经验是,过采样(重复少数类样本)实现简单,在小数据集上效果更稳定;欠采样虽然能极大加速训练,但会浪费大量数据,只有数据量充足时才值得尝试。
5.5 常见问题速查表
| 问题表现 | 可能原因 | 解决思路 |
|---|---|---|
| loss不降 | 学习率过小 | 调大学习率,观察趋势 |
| loss不降 | 特征未标准化 | 归一化到0-1或Z-score |
| loss为NaN | log(0)或exp溢出 | 加极小值,softmax平移 |
| 精度上不去 | 数据线性不可分 | 特征工程或换非线性模型 |
| 小类全预测错 | 类别不平衡 | 加权损失、重采样 |
| 类别分不清 | 特征区分度低 | 增加特征、降噪、特征选择 |
| 收敛极慢 | 初始化不当 | 改用小随机数或Xavier |
| 预测概率集中于某类 | 特征存在主导维度 | 检查标准化是否生效 |
这个表是我在实际项目中踩过坑之后整理出来的,不一定覆盖所有情况,但覆盖面已经够日常项目排查使用了。遇到问题时先对着表快速过一遍,往往能省下大量试错时间。
6. 为什么深度学习分类任务最终都会指向softmax
6.1 从线性分类到神经网络输出层
如果你已经读过一些深度学习的内容,会发现一个现象:卷积神经网络做图像分类,最后一层通常就是一个softmax分类器;Transformer做文本分类,最后一层同样是一个线性层加softmax。这不完全是巧合。
神经网络前面的所有层——卷积层、池化层、各种注意力机制——本质上都在做一件事:特征提取。它们把原始输入逐步变换,让数据在新的特征空间里变得线性可分。网络最末尾的softmax层则负责在已经线性可分的特征表示上做最终决策。
这正是softmax回归的价值所在:它不必永远是那个孤零零的线性分类器,它完全可以作为复杂网络的金字塔塔尖。不管前面的特征网络有多深、多复杂,最终把特征映射到离散类别的概率分布这一步,softmax始终是首选。
6.2 softmax与温度参数
在真实项目中,你可能会接触到带温度参数T的softmax变体:
p_k = exp(z_k / T) / Σ_j exp(z_j / T)
T大于1时,概率分布变得更平滑,类别之间的差异被压缩,输出的熵变大;T小于1时,概率分布变得更尖锐,模型更加自信。温度参数的物理解释来自统计力学,但在工程上它只是一个很方便的旋钮。
在知识蒸馏之类的场景里,温度参数用来控制“软标签”的平滑程度——用一个高温度的softmax输出作为学生模型的训练目标,让模型学到类别之间的相似关系,而不只是硬性的0/1标签。在强化学习的策略网络里,温度参数调节探索与利用的平衡。你在做模型部署时如果发现某个类别的置信度普遍偏低,适当调低温度也能让预测更果断。
6.3 从softmax回归到多分类模型家族的扩展
理解softmax回归之后,你其实就握住了多分类模型家族的一个关键枢纽。顺着两条线可以继续深入。
一条线是概率化的模型扩展。softmax回归是生成式模型的基石之一,朴素贝叶斯搭配softmax就可以做带先验信息的分类;线性判别分析(LDA)在类别先验和协方差假设下可以推导出本质上是softmax形式的后验概率。
另一条线是结构化的多分类扩展。在自然语言处理里,序列标注问题需要在每个时间步上做softmax,形成softmax-over-time;在多标签分类里,可以用多个独立的sigmoid替代单个softmax,这恰好从反面印证了softmax的“类间互斥”特性。
我把softmax回归比作多分类任务的地基,这句话在实践里越来越有体会。地基不牢,地上盖多少层都容易塌;地基扎实了,后面无论学什么新模型都会顺很多。
6.4 工程实现上的一些补充提醒
如果你准备在真实项目中落地一个softmax模型,有几个工程层面的细节值得提前想清楚。
第一个是预测阶段的效率。softmax的分母是全部类别的得分之和,所以哪怕你只关心top-3,也得把所有类别的得分全部算一遍。当类别数特别大时(比如上百万类的商品识别),全量softmax的计算开销不可忽视,业界有各种近似方法,但那是另一个话题了。
第二个是模型文件的大小。权重矩阵的维度是[特征维度, 类别数],如果特征维度是几万,类别数是几千,模型文件轻松上GB。在移动端或边缘设备部署时,这个体积往往是瓶颈,需要做量化或裁剪。
第三个是保存模型时一定要一并保存特征处理和编码规则。我见过不止一次,模型文件部署上线了,但忘了保存标准化的均值和标准差,推理结果全错。这个错误很低级,但一旦发生代价非常大。
7. 用一个小技巧收尾:永远先跑通一个最小用例
我在反复使用softmax回归的过程中,最后的体会其实很简单:无论你的任务看起来多大、数据多复杂,动手写代码之前,先构建一个最小化的用例跑通整个流程。比如用10个样本、3个类别、5个特征,把训练跑起来,确认loss确实在下降,模型确实能学进去,再逐步放大到完整数据集。
这个方法帮我避开了无数次折腾。因为当一个问题在大数据集上报错时,你很难判断是数据的问题、模型的问题还是代码的问题。但当你手里有一个最小用例时,你能确定代码逻辑本身是正确的,问题一定出在数据或者参数配置上,排查范围瞬间缩小了一大半。
另外,如果你想测试自己对softmax回归的推导是否真的理解了,可以试试这个自检题目:手动推导softmax回归在二分类且K=2时的最终形式,化简之后你应该得到一个sigmoid函数。这一步如果能顺顺利利做出来,那么你对softmax回归的本质就真的吃透了。
softmax回归看起来只是一个函数一个损失,但它牵涉的概率归一化、梯度推导、数值稳定性、类别不均衡处理这些内容,会在你之后接触的几乎所有分类模型里反复出现。啃下这一块,往后的路会顺很多。