1. 多分类到底在解决什么问题:从“是不是猫”到“是哪一种”
我估摸着看到这一篇的朋友,应该已经跟完了前面的几篇文章。前头我们聊过神经元、激活函数、损失函数、梯度下降,也动手搭建过最简单的全连接网络,甚至可能拿它做过二分类——比如判断一张图里“是猫”还是“不是猫”。但现实世界的图像分类问题,几乎都不是二选一。你面对的更可能是:这张图是猫、是狗、是鸟、是鱼,还是别的什么?这才叫“多分类”。
1.1 一张图里到底有几种“类”
图像多分类,通俗点说,就是给定一张图片,让神经网络从若干个预先定义好的类别中,选出它属于哪一个。这里的“若干个”,可能是10类(比如手写数字0到9),可能是100类(比如CIFAR-100里的100种物体),也可能是1000类(比如ImageNet的1000类常见物体)。类别数量越多,问题越复杂,但思维框架是一样的。
我经常打的一个比方:二分类像是“这个人是好人还是坏人”,多分类则像是“这个人的职业是医生、老师、厨师还是司机”。后者显然更贴近真实世界。你在实际项目里,几乎找不到只有两个类别还值得用神经网络去做的场景——就算有,多半也只是多分类的一个特例罢了。
理解多分类,先记住一句话:多分类的本质,是让模型在多个候选答案之间做“打分”,最后选出分数最高的那个。所以不要把它想得多么神秘,它跟人做选择题的流程很像——先快速扫一眼题目,脑子里的各科知识给出候选答案信心,然后选择信心最高的选项。神经网络做多分类,也是这个路子。
1.2 多分类和二分类的边界在哪里
很多新手一开始会把多分类误解成“多个二分类”。比如有人想:既然二分类是输出一个0到1之间的数,那我有三个类,是不是训练三个二分类器,分别判断“是不是猫”“是不是狗”“是不是鸟”,然后拼起来?
这个思路不能算全错,早期确实有人这么干过(叫 one-vs-all),但它有个绕不开的问题:三个分类器各管各的,彼此之间的分数没有可比性。猫分类器打出0.9的高分,狗分类器打出0.8的高分,到底该信谁?你没法从两个独立的网络里推出一个统一的结论。更麻烦的是,如果某张图既不像猫又不像狗,三个分类器全都给出低分,那你连“它到底是什么”的答案都拿不到。
神经网络的正确答案是:别搞三个独立的分类器,而是让一个网络同时输出三个分数,再用一个统一的规则把这几个分数转成概率。这个统一的规则,就是本篇文章的核心——softmax。
2. 网络的“出口”是怎么设计的:softmax和交叉熵的作用
2.1 为什么最后一层要换成softmax
先看一个具体的网络结构变化。二分类的时候,我们习惯在最后一层放一个神经元,用 sigmoid 激活函数,输出一个0到1之间的值,代表“是正类的概率”。到了多分类,最后一层就要换成类别数量个神经元,每个神经元对应一个类别。比如10类问题,最后一层就是10个神经元。
问题来了:这10个神经元各自输出一个数值,可能有的神经元输出2.5,有的输出-1.3,有的输出0.8。这些值本身没有明确的意义——既不是概率,互相之间还可能有数量级差异。我们需要一个东西,把这10个原始分数(叫logits)转成10个加起来恰好等于1的正概率值。
softmax就是干这个的。它的公式长这样:
[ \text{softmax}(z_i) = \frac{e^{z_i}}{\sum_{j=1}^{C} e^{z_j}} ]
其中 ( C ) 是类别总数,( z_i ) 是第 ( i ) 个类别的原始分数。每个分数取指数,再除以所有分数指数之和。取指数的好处有两个:第一,不管原始分数是负还是正,指数之后都变成正数;第二,指数运算会放大分数之间的差距——在原始分数下差0.5,在指数之后可能差出好几倍,这样模型做决策时会更“笃定”,不会模棱两可。
实操里的一个细节:因为 ( e^{z_i} ) 在 ( z_i ) 比较大的时候会迅速膨胀,容易产生数值溢出(超出计算机浮点数能表示的范围)。所以工程实现上通常会先对所有logits做一次“减去最大值”的平移,再算softmax。数学上这不会改变结果,因为分子分母同时缩放了相同的倍数。你要是自己写代码,记得把这个细节带上。
2.2 交叉熵损失:教模型“认错”的尺子
网络结构改完之后,损失函数也要跟着换。二分类时候用的二元交叉熵(binary crossentropy),到了多分类要用多分类交叉熵(categorical crossentropy)。
交叉熵衡量的是两个概率分布之间的差异。放在这里,就是模型预测的概率分布(比如[0.1, 0.6, 0.2, 0.1])和真实标签的概率分布(比如[0, 1, 0, 0],因为真实类别是第2类)之间到底差多远。公式如下:
[ L = -\sum_{i=1}^{C} y_i \log(\hat{y}_i) ]
其中 ( y_i ) 是真实标签的one-hot编码(只有真实类别那一项为1,其余为0),( \hat{y}_i ) 是模型预测的概率。
注意看,因为 one-hot 编码里绝大部分 ( y_i ) 都是0,所以求和真正起作用的只有真实类别那一项,也就是最终只算 ( -\log(\hat{y}_{\text{true}}) )。当模型给真实类别打的概率越接近1,损失越小;越接近0,损失越大。这跟人考试是一个道理:你做错了某道题,不会因为其他题蒙对了就少扣分——交叉熵只看你在“正确的那道题”上有多大把握。
还有一个小变种值得提一下:如果你的标签不是one-hot编码,而是一个个类别编号(比如数字3代表第3类),那么 TensorFlow 里的sparse_categorical_crossentropy可以直接吃整数标签,省去手动转one-hot的麻烦。但概念上它和普通交叉熵是等价的,只是实现上替你省了一步。
3. 搭建一个真正的图像多分类网络:数据和结构两手抓
理论和结构都说完了,现在进入动手环节。多分类的图像任务,通常包含三类预处理环节:数据准备、网络结构设计、训练配置。我一个个讲。
3.1 数据准备的三个关键动作
第一,图像归一化。像素值范围是0到255,直接丢进网络,数值太大,会让权重更新的梯度不稳定。常见做法是除以255,缩放到0到1之间;更讲究一点,还可以算数据集的均值和标准差做标准化。我用下来感觉,对简单数据集(比如MNIST、CIFAR-10),除255已经够用;但如果做更复杂的任务,标准化效果更好,收敛也更稳。
第二,标签处理。多分类的标签有两种常见形态——整数编号(比如0到9)和one-hot向量(比如10维向量里只有一个1)。TensorFlow 的两种交叉熵分别对应这两种标签,选的时候要匹配好。最常见的报错就是整数标签配了普通交叉熵,或者反过来,维度对不上,一跑就错。
第三,数据集划分。千万不能只用一份数据既训练又测试,那样模型“背答案”背得好,换个新图就露馅。稳妥的做法是划出训练集、验证集(和测试集)。验证集用来调参和选模型,测试集用来最终考察泛化能力。很多初学者图省事,只留训练集和测试集,结果调参的时候反复看测试集,等于把测试集信息间接泄露给了模型,最终报告的准确率有水分。
3.2 网络结构:卷积和全连接的配合
图像多分类,业界标准答案是卷积神经网络(CNN)。如果你已经很懂卷积、池化这些概念,可以直接跳过这段;如果还不太熟悉,我给你一个直觉理解:图像跟普通表格数据的区别在于,像素之间存在强烈的空间关系——相邻的像素往往属于同一个物体。全连接网络看不到这种空间关系,它把每个像素当独立特征处理,数据量一大就学不动。CNN则通过卷积核在图像上滑动,每次只看一个小局部,提取边缘、纹理、形状等基础特征,再层层组合成高级语义特征。
一个典型的手写数字识别网络长这样:
第一层是卷积层,一般用32个3×3的卷积核,激活函数选ReLU,然后用池化层把特征图尺寸减半。第二层再上64个卷积核,再池化。最后把二维特征图展平成一位向量,接一个或两个全连接层,最后一层输出10个类别分数。
卷积层的设计有几个经验值:卷积核尺寸用3×3就够,堆两个3×3比一个5×5效果更好而参数更少;激活函数默认ReLU,如果不放心可以试试Leaky ReLU,防止某些神经元“死掉”。池化用2×2最大池化,基本不会出大错。
全连接层的神经元数量也不用太夸张,128到512之间都很多人用。参数量主要被全连接层吃掉,全连接层太大不仅训练慢,还容易过拟合。你可以观察一下:同样一个网络,全连接层从512砍到128,准确率可能几乎不掉,但训练速度快了不止一倍。
3.3 训练配置:批量大小、迭代轮数和优化器
训练配置上,多分类跟二分类大同小异,但有一个点我要特别强调:类别不均衡。
假设你的数据里猫有9000张,鸟只有500张,模型只要永远猜猫,就能拿到90%的准确率。这不是本事,这是偷懒。处理办法通常是给损失函数的每个类别加权,让少数类别的样本在损失中占更大比重,逼迫模型真正去学鸟的特征。这招叫类别加权,Keras 里fit()函数有个class_weight参数,填进去就行。
优化器方面,Adam 几乎是最省心的默认选择,它的自适应学习率机制让你不用太费心思去调学习率,大部分情况下能稳稳收敛。我个人的习惯是:先用 Adam + 默认学习率(0.001)跑通流程,看loss的下降趋势;如果收敛太慢,再把学习率调大;如果收敛到后期震荡,再用 ReduceLROnPlateau 回调让学习率自动衰减。这个“先跑通、再优化”的顺序,比一开始就追求最优配置更高效。
4. 拿代码说话:用CIFAR-10跑一个完整的多分类流程
光讲理论不写代码,等于纸上谈兵。我用 TensorFlow/Keras 写一个完整的 CIFAR-10 多分类示例。CIFAR-10 是10类彩色图像,32×32像素,包含飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车。它是一个很适合练手的入门数据集——比MNIST难,能让你体会真实的图像多分类挑战。
4.1 代码框架怎么搭
import tensorflow as tf from tensorflow.keras import layers, models, datasets # 1. 加载数据 (train_images, train_labels), (test_images, test_labels) = datasets.cifar10.load_data() # 2. 归一化 train_images = train_images.astype('float32') / 255.0 test_images = test_images.astype('float32') / 255.0 # 3. 标签转 one-hot(其实用 sparse_categorical_crossentropy 可以不用转) train_labels_onehot = tf.keras.utils.to_categorical(train_labels, 10) test_labels_onehot = tf.keras.utils.to_categorical(test_labels, 10) # 4. 构建模型 model = models.Sequential([ layers.Conv2D(32, (3, 3), activation='relu', padding='same', input_shape=(32, 32, 3)), layers.MaxPooling2D((2, 2)), layers.Conv2D(64, (3, 3), activation='relu', padding='same'), layers.MaxPooling2D((2, 2)), layers.Conv2D(128, (3, 3), activation='relu', padding='same'), layers.MaxPooling2D((2, 2)), layers.Flatten(), layers.Dense(256, activation='relu'), layers.Dropout(0.5), layers.Dense(10, activation='softmax') # 最后一层:10个类别,softmax ]) # 5. 编译 model.compile( optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'] ) # 6. 训练 history = model.fit( train_images, train_labels_onehot, validation_split=0.2, batch_size=64, epochs=20, verbose=1 ) # 7. 测试 test_loss, test_acc = model.evaluate(test_images, test_labels_onehot, verbose=0) print(f'测试集准确率: {test_acc:.4f}')这段代码流程很标准,我拆开讲几个容易踩坑的点。
padding='same'的作用是让卷积不改变特征图尺寸,省得你为尺寸计算操心;如果你不加这个参数,特征图每经过一层卷积都会变小,到最后一层时尺寸可能已经小于卷积核,直接报错。
Dropout(0.5)放在全连接层之前,让训练时随机丢弃一半神经元。这是对抗过拟合的经典手段,CIFAR-10 上不加 Dropout,训练集准确率会冲向95%以上而测试集卡在70%左右,加上之后测试集能明显提升。
批量大小设了64,Epochs设了20。这个数据量下,20轮训练在普通GPU上大概三到五分钟,CPU上也就十几分钟,等得起。跑完看一眼输出,你会发现训练集准确率最后涨到95%以上,但验证集和测试集大概在70%上下。这个差距,就是过拟合的味道。
4.2 训练过程发生了什么
训练时你会发现 loss 一开始很高(接近2.3),然后一段一段往下掉。开始的那一下,其实是模型刚初始化还没学会任何东西,每个类别的预测概率都接近0.1,交叉熵自然就是 ( -\log(0.1) \approx 2.3 )。如果你看到起点不是这个值,那大概率是数据或标签出了问题,这点可以用来做训练前的 sanity check。
跑完20轮,一个典型的输出是:训练集准确率98%,验证集准确率72%。这个差值看着心疼,但它非常正常——模型把训练集的噪声和细节都记下来了,却没有泛化到新数据上。要让准确率继续往上走,光加训练轮数没用,得从数据增强、模型结构、正则化这些方向下手。
4.3 数据增强:多分类的“免费午餐”
CIFAR-10 上最有效的提点手段,不是换网络结构,而是数据增强。所谓数据增强,是在训练时对图像做随机变换——左右翻转、小幅旋转、裁剪、颜色抖动——让模型每次看到的都是略有不同的版本。相当于把一份数据变出多份,而且这些新样本还免费给模型上了“这个物体换个角度、换个姿势仍然属于同一类”的课。
Keras 里用tf.keras.layers.RandomFlip、RandomRotation、RandomZoom这些层就能现成地实现。注意一点:数据增强只应该在训练时启用,验证和测试时要保持原始图像,否则你验证集的分数也会被随机性污染,不稳定。
我用几次实际对比下来,只是加上随机左右翻转和随机裁剪这两项,CIFAR-10 准确率就能从72%涨到80%以上,训练时间几乎不变。这是性价比最高的一步,强烈建议先做。
5. 训练多分类模型最容易踩的坑:过拟合、学习率和标签陷阱
5.1 过拟合:模型“背答案”而不是“学方法”
在图像多分类里,过拟合是最常见的现象,没有之一。特征很明显:训练集准确率一路飙升,验证集准确率涨到某个点后不涨甚至开始掉,两条曲线之间的缝隙越拉越大。
应对手段我按优先级排序:数据增强、Dropout、降低模型容量(减少卷积核数量或全连接层大小)、早停(EarlyStopping)。前两个能解决大部分问题。如果你做了数据增强和 Dropout 之后验证集和训练集之间仍然有10个点以上的差距,再考虑砍模型。
还有一个很多人忽视的细节:训练时观察损失曲线比准确率曲线更灵敏。准确率是“离散”的,可能连续好几轮都是同一档数值,让你看不出趋势;损失是“连续”的,哪怕只差0.01都看得清清楚楚。我习惯用 TensorBoard 或者直接在 Keras 的history回调里看 loss 曲线,以它为判断依据。
5.2 学习率和批量大小之间的拉扯
学习率设置不当,会让多分类训练出现各种奇奇怪怪的现象。学习率太高,loss 可能直接变成NaN(梯度爆炸,数值溢出);学习率太低,loss 像蜗牛一样挪,500个epoch都下不去。
我自己的经验是先按默认的 0.001 跑,观察前三个epoch的loss:如果第一条loss下降的幅度超过0.5,说明学习率偏大,调到0.0003;如果每个epoch掉不到0.05,说明学习率偏小,调到0.003。这个“看前三个epoch反应再调”的方法,比纯看论文给的经验值实用得多。
批量大小也有讲究。大批量(比如256或512)会让梯度更平稳,但容易收敛到“尖锐”的极小值,泛化可能变差;小批量(16或32)噪声大,但反而可能跳到更好的谷底。CIFAR-10 这类中小数据集,64是最常用的起点。如果你显存紧张,直接降到32也行,训练会更波折一点但结果不会差太多。
5.3 标签处理的隐蔽陷阱
多分类的标签处理表面上很简单,但我见过太多人在这一步栽跟头。最典型的情况:标签编号从1开始而不是从0开始。神经网络最后一层的输出维度如果是10,标签就得是0到9;如果你的数据标注是1到10,没减1,模型训练时在最后一类上永远学不会。
另一个暗坑是 one-hot 编码维度和类别数不一致。有些数据集自带 one-hot 标签,但可能只覆盖了其中部分类别;如果把没覆盖到的类别样本也丢进网络,维度一错,立刻报错。我的建议是:不管数据源是什么,都先打印一下train_labels.shape和最大值、最小值,确认跟你的类别数量对得上再开工。
还有一点:使用sparse_categorical_crossentropy时,标签的 shape 必须是(样本数, 1)或(样本数,)。如果你传入的是(样本数, 1, 1),也就是多套了一层维度,Keras 有时候不报错,但loss曲线会非常奇怪,怎么都不下降。我排查过好几个这种情况,最后都是 reshape 一下就好。
6. 不只看准确率:怎么判断一个多分类模型真的“会分类了”
准确率是个很好用的指标,但对多分类来说,它会把很多重要信息藏起来。我举个例子:一个三分类问题,类别A占90%,B和C各占5%,一个“全猜A”的傻模型准确率也是90%,表面上精度惊人,实际上对B和C一无所知。所以光看准确率,不能判断模型是不是真的在“学会分类”。
6.1 混淆矩阵:把错误摊开看
混淆矩阵是多分类最通用的诊断工具。它是一个 ( C \times C ) 的表格,行代表真实类别,列代表预测类别。对角线上的数字是正确分类的样本数,非对角线则是“把某个类看成了另一个类”的错误。
看混淆矩阵最有价值的点是:错误不是均匀分布的,它常常有聚集性。比如 CIFAR-10 里,猫和狗之间经常互相认错,汽车和卡车之间也经常搞混——这完全合理,因为它们本身长得太像。但如果你的模型做出了一些毫无道理的混淆,比如把“飞机”认成“狗”,那就要怀疑训练数据里是不是有标签错误,或者特征提取层出了毛病。
TensorFlow/Keras 里可以直接用tf.math.confusion_matrix计算:
import numpy as np from sklearn.metrics import confusion_matrix predictions = model.predict(test_images) predicted_classes = np.argmax(predictions, axis=1) true_classes = np.argmax(test_labels_onehot, axis=1) cm = confusion_matrix(true_classes, predicted_classes) print(cm)argmax就是把softmax输出的10个概率里最大的那个类别序号取出来,恢复成我们真正关心的“这图是哪一类”的答案。打印出来的矩阵每一行加起来是那一类的样本总数,对角线数字越大越好。
6.2 每一类的准确率比总准确率更值得盯
我自己的习惯是:总准确率只看一眼,每类的准确率逐行盯着看。算起来很简单,混淆矩阵每一行的对角线除以该行所有样本之和,就是那一类的准确率。
如果某一类准确率明显低于其他类,我会先看这类样本量是多少。样本太少,学不好很正常,解决方案是收集更多数据或者做少数类过采样。如果样本量足够还是学不好,就去看混淆矩阵里它跟谁混淆——如果是跟临近的语义类别混淆,比如猫和狗,那是特征区分度不够,需要更深的网络或者更好的数据增强;如果是跟语义无关的类别混淆,那可能是标签噪声,建议人工抽查一批训练图看看是不是标错了。
还有一个对新手特别容易误导的现象:训练最后阶段的准确率可能在一两个点之间来回跳。不要被它干扰,更不要因此反复重启训练。把早停的耐心阈值调大一些(比如连续10个epoch没有改善才停),让训练多跑一会儿,往往能得到更稳的结果。
6.3 概率输出的实际用法:多分类模型不只是用来选最大值的
理论上,模型输出的是每一个类别的“概率”,而我们通常只取最大值对应的类别做最终判断。但实际应用里,这个概率分布本身就有价值。
举个安防或相册分类的典型场景:如果模型识别一张图是猫的概率是0.93,是狗的概率0.05,是狐狸的概率0.02,你可以放心归为猫;但如果猫的概率只有0.45,狗0.40,狐狸0.15,这时候机器自己也拿不准,更好的做法是标记为“不确定”,交给人工复核,而不是硬猜一个答案。设置一个置信度阈值(比如最高概率低于0.7就不做自动分类)能显著减少错误分类带来的后续麻烦。
这个思路,比单纯追求总准确率更能解决实际问题。做多分类项目落地的朋友,我建议把阈值调优也纳入你的评估流程里。
写到这里,多分类从理论到实践的关键环节都过了一遍。这套流程你完整跑下来,应该能独立处理大部分常见图像多分类任务。做项目的时候多留个心眼:模型真正宝贵的不是那个准确率数字,而是它在你自己的数据分布上是否稳定可靠。