深度学习任务到底分几类?这个问题别看简单,我面试过不少候选人、也带过不少新人,发现大部分人对任务分类的理解都停留在“分类和回归”这两个词上。真到做项目的时候,丢过来一个任务,连该用哪条技术路线、该定什么损失函数、该拿什么指标去评估都拎不清,最后模型是训出来了,但业务上根本不适用。
这篇系列文章我就是想把深度学习任务这件事拆开了揉碎了讲一遍。第一篇先把大框架搭起来,把最主流、最常见的几类任务讲清楚,每一类都给到可复现的示例和核心代码,保证你读完能直接对应到自己手头的问题上。这个系列适合正在入门深度学习、或者已经跑通了一些基础模型但缺乏全局视角的读者,也适合准备面试、需要系统梳理知识体系的同学。我不堆公式,不贴大段论文,全部以实际工程视角来讲。
先说说为什么要把“任务分类”单独拿出来讲。深度学习的本质是“用神经网络去拟合一个从输入到输出的映射”,但“映射”这件事有无数种形态。输入可以是一张图、一段文本、一条时序数据、一组结构化特征,甚至多个模态的混合;输出可以是一个离散的类别标签、一个连续的数值、一个序列、一个概率分布、或者一张生成的图像。不同的输入输出形态,直接决定了你用什么网络结构、什么损失函数、什么评价指标。任务分类真正决定的是后面所有技术选型。
1. 深度学习任务的总体分类框架
所谓的“任务分类”,最主要的分水岭在“训练时有没有标签”。有标签的走监督学习路线,没标签的走无监督或自监督路线,还有夹在中间的弱监督和半监督。在落地场景中,绝大多数业务问题都能被归入某个具体的任务类别,只是有时候需要把业务问题“翻译”成技术任务。
我给一个自己常用的分类框架,这个框架在带新人和做技术方案评审时反复用过:
| 大类 | 子类 | 输入形态 | 输出形态 | 典型应用 |
|---|---|---|---|---|
| 监督学习 | 分类 | 图像、文本、特征向量 | 离散类别 | 图像识别、垃圾邮件过滤、医疗诊断 |
| 监督学习 | 回归 | 图像、文本、特征向量 | 连续数值 | 房价预测、销量预估、年龄估计 |
| 监督学习 | 序列标注 | 序列数据 | 标签序列 | 命名实体识别、词性标注、语音识别对齐 |
| 监督学习 | 序列生成 | 序列数据 | 目标序列 | 机器翻译、文本摘要、语音合成 |
| 无监督学习 | 聚类 | 特征向量 | 簇标签 | 用户分群、异常检测、数据探索 |
| 无监督学习 | 降维/表征学习 | 高维数据 | 低维向量 | 可视化、特征压缩、预训练 |
| 无监督学习 | 生成 | 噪声/条件向量 | 新样本 | AIGC图片生成、数据增强、风格迁移 |
| 自监督学习 | 对比学习 | 无标签样本 | 表征向量 | 预训练、小样本场景下的特征提取 |
| 自监督学习 | 掩码重建 | 部分遮蔽样本 | 重建完整样本 | 大模型预训练(文本、图像、语音) |
| 强化学习 | 决策控制 | 状态观测 | 动作输出 | 游戏AI、机器人控制、自动驾驶决策 |
这个表看起来很基础,但真到了实际项目里,很多人会犯一个低级错误:拿分类任务的思路去做回归任务,拿生成任务的评估方式去衡量分类模型。之所以出现这种问题,是因为对任务本质的理解没到位。举个例子,给一组皮肤镜图像,判断是良性还是恶性,这是个二分类问题,但很多人上来就搭一个回归模型去预测“恶性概率数值”,然后再拿个阈值去卡。虽然行为上看起来差不多,但在损失函数设计和评估指标上完全不同,效果往往也会差不少。
如果按“技术框架的复杂度”再排一档,最核心、应用最广的是监督学习中的分类与回归,这是整个深度学习的基石。本文先从这两类讲透,再往前沿的序列任务和自监督任务展开。分类和回归拿捏住了,后面学任何其他任务都有底气。而且从实际工作来看,80%以上的深度学习落地场景,最终的输出不是类别就是数值,这两类任务理解到位了,已经能应付大多数需求。
2. 监督学习核心之一:分类任务全解析
分类任务的目标是:给定一个输入样本,预测它属于哪个预定义的类别。听起来很简单,但里面隐藏了三个关键问题:类别之间是否互斥?样本是否只有一个标签?类别数量是多少?这三个问题的答案不同,对应的技术方案就不同,这一点极其重要。
2.1 二分类、多分类、多标签:三个容易混淆的方向
先看一组我经常拿来给新人区分的例子:
- 二分类:判断一封邮件是垃圾邮件还是正常邮件。输出只有一个类别,且两个类别互斥。输出层只需要一个神经元,经过Sigmoid激活,输出一个0到1之间的概率值,以0.5为分界线。
- 多分类:判断一张图片里的物体是猫、狗还是鸟,类别数量大于2,且一个样本只能属于其中一个类别。输出层使用Softmax激活函数,输出一个概率分布,所有类别的概率之和为1,取最大概率对应的类别作为预测结果。
- 多标签分类:判断一张图片里同时包含哪些物体,比如一张照片里同时有一只猫和一只狗。对于一个样本,多个标签可以同时为1,不能用Softmax,而是每个输出节点独立使用Sigmoid,各自输出0到1之间的概率,每个维度独立进行二分类。
这里有一个非常经典的技术误区,值得单独强调:做多标签分类时不要用Softmax。Softmax强制所有类别概率之和为1,这等于默认“一个样本只能属于一个类别”,在多标签场景下会把低概率的类别强行压下去。我之前遇到过一个人,做医学影像的多标签分类,三种病变可能同时存在,他用了Softmax,结果模型只会输出单标签,后期发现这个bug排查了很久。换成独立Sigmoid之后,效果立刻恢复正常了。
2.2 分类任务的损失函数:为什么交叉熵是首选
分类任务最常用的损失函数是交叉熵(Cross Entropy)。很多新手会问:为什么不用均方误差(MSE)来做分类?这个问题里面藏着对分类本质的理解。
我从两个角度来解释这个问题。第一,分类任务的期望输出是“类别概率分布”,而交叉熵本身就是用来度量两个概率分布之间差异的。用均方误差去度量概率分布之间的差异,不符合分布本身的特性。第二,从梯度优化的角度来看,使用Sigmoid或Softmax激活函数之后,如果配合均方误差,在输出值接近0或1的饱和区时梯度会极小,模型参数几乎得不到有效更新,收敛速度会大幅减慢。而交叉熵配合Softmax,在预测错误时能够提供足够的梯度信号,让模型更快地学习到正确的分类边界。
具体在代码层面,PyTorch里有一个关键细节:nn.CrossEntropyLoss()这个类内部已经包含了Softmax计算,所以你在搭建模型时,分类层不需要手动加Softmax,直接把网络最后一层的原始输出(logits)传给损失函数即可。但如果是在推理阶段,想要输出“每个类别的概率值”,就需要单独对logits做一次Softmax。很多新手在这个地方容易搞混,导致训练和推理行为不一致。另外,如果是多标签分类,用nn.BCEWithLogitsLoss(),它内部也是把Sigmoid和BCE合并了,理由和上面一样,数值稳定性更好。
2.3 一个完整可复现的图像分类示例(PyTorch + ResNet)
我用一个标准的图像分类示例来演示从数据加载到模型训练评估的完整流程。这里用CIFAR-10数据集,类别数为10,输入图像尺寸为32×32。
import torch import torch.nn as nn import torch.optim as optim import torchvision import torchvision.transforms as transforms from torchvision.models import resnet18 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") transform_train = transforms.Compose([ transforms.RandomCrop(32, padding=4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), ]) transform_test = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), ]) train_set = torchvision.datasets.CIFAR10(root="./data", train=True, download=True, transform=transform_train) test_set = torchvision.datasets.CIFAR10(root="./data", train=False, download=True, transform=transform_test) train_loader = torch.utils.data.DataLoader(train_set, batch_size=128, shuffle=True, num_workers=4) test_loader = torch.utils.data.DataLoader(test_set, batch_size=256, shuffle=False, num_workers=4) model = resnet18(num_classes=10).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=200) for epoch in range(30): model.train() running_loss = 0.0 for inputs, labels in train_loader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * inputs.size(0) scheduler.step() model.eval() correct = 0 total = 0 with torch.no_grad(): for inputs, labels in test_loader: inputs, labels = inputs.to(device), labels.to(device) outputs = model(inputs) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() epoch_loss = running_loss / len(train_set) accuracy = 100.0 * correct / total print(f"Epoch {epoch+1:3d} | Loss: {epoch_loss:.4f} | Acc: {accuracy:.2f}%")这段代码是一个标准的监督分类流程,从里面能看到几个关键点。第一,使用预训练的ResNet-18并在CIFAR-10上微调,因为CIFAR-10的图像分辨率低,直接使用预训练权重时需要注意输入尺寸的适配,需要把模型的第一层卷积修改一下,或者直接从头训练。第二,数据增强用了随机裁剪和水平翻转,这属于图像分类的基本操作,目的是增加数据的多样性,提升模型的泛化能力。但要注意,增强过度反而会抑制性能,我之前试过对CIFAR-10加颜色抖动和旋转,结果准确率反而下降了,原因是32×32的小图像本身信息量少,过强的几何变换会引入无效信息。第三,SGD配合动量在CIFAR-10上一直是非常稳的组合,余弦退火学习率调度的效果也很稳定,较小数据集上我通常优先考虑这个组合而不是直接上Adam。
2.4 分类任务的评估指标:只看Accuracy是不够的
关于分类的评估,一个常见的认知坑是只看Accuracy。当类别分布不均衡时,Accuracy会骗人。比如一个异常检测场景,99%是正常样本,1%是异常样本,模型全部预测为正常就能拿到99%的准确率,但这显然是没有价值的模型。
在类别不平衡的场景下,我通常组合使用Precision、Recall、F1-Score和AUC-ROC。简单说:Precision衡量的是“预测为正的样本中真正为正的比例”,Recall衡量的是“真实为正的样本中被正确找出来的比例”,F1是两者的调和平均,AUC-ROC则反映模型对正负样本排序能力的优劣,它不依赖于具体的分类阈值。在医疗诊断之类“漏检代价远高于误报”的场景里,应优先提升Recall;在垃圾邮件过滤这类“误杀正常邮件代价高”的场景里,应优先保证Precision。这个取舍本质上是业务问题而不是技术问题,做模型的同学一定要和业务方确认清楚“更怕漏还是更怕错”。
另外,很多项目里样本不平衡的处理方式也是值得一说的。常用的有效手段有几类,第一是重采样,对少数类做上采样或对多数类做下采样;第二是调整类别权重,在损失函数里给少数类更高的权重,PyTorch里可以直接给CrossEntropyLoss传入weight参数实现;第三是使用Focal Loss这类特殊设计的损失函数,它在交叉熵的基础上引入了调制因子,让模型把注意力集中在难分类的样本上,减少大量易分类负样本对梯度的主导。这个损失在处理极度不平衡的检测任务中非常有用,我在真实项目里靠它解决过正负样本比超过1:500的问题。
3. 监督学习核心之二:回归任务全解析
回归任务的目标是:给定一个输入样本,预测一个连续的数值输出。看起来和分类只差最后一步,但模型设计、损失函数、评估方式全都不一样。比如预测房价、预测商品销量、预测用户点击率,这些都是回归任务。它们的共同点是:输出是一个连续的实数,而不是离散的类别。正是因为输出连续,所以评估“预测值跟真实值差了多少”成为核心问题。
3.1 回归任务的损失函数选型:MSE、MAE与Huber
回归任务里最常用的三个损失函数是均方误差(MSE)、平均绝对误差(MAE)和Huber Loss。我有一个自己的选择逻辑,在实际项目里反复验证过,分享出来。
MSE的表达式是(y - y_pred)^2,它的优势在于处处可导,梯度在误差较小的时候也比较顺滑,数学性质好,是回归任务默认的选择。但MSE有一个致命弱点:对离群点极度敏感。因为误差被平方了,一个离群点带来的损失会以平方级别放大,导致模型为了“照顾”这个离群点而对整体产生偏移。
MAE的表达式是|y - y_pred|,它对离群点的容忍度更高,损失随着误差线性增长,不会像MSE那样被异常值主导。但MAE在误差接近0的地方不可导(导数会跳变),靠近最优解时梯度恒定,收敛速度相对变慢。
Huber Loss是对二者的折中:在误差小于某个阈值δ时使用MSE,在误差大于δ时退化为MAE。实际效果是:模型在训练初期误差较大的阶段使用类似MAE的行为,对离群点不敏感;随着训练收敛、误差缩小到阈值以内,切换为MSE行为,保证收敛精度和梯度稳定性。阈值δ通常取1.0,也可以根据标签的值域做调整。
实际项目里怎么选?如果标签数据相对干净、没有明显离群点,优先选MSE;如果数据里有明显离群点且预测目标对离群点敏感度低,选MAE;如果你不想费心判断数据质量,直接用Huber Loss是相对稳妥的选择。比如做人体年龄估计,输入一张人脸图像预测年龄,数据集中有一些因为标注误差或特殊外观导致标签明显偏离的样本,用MSE训练时这些样本会强烈干扰模型,后来换成Huber Loss问题缓解了很多,验证集上的MAE直接下降了约8%。这就是活生生的经验。
3.2 回归任务的网络结构与输出层设计
分类任务的输出层是Softmax或Sigmoid,输出是概率值,有明确的取值范围。回归任务的输出层通常就是线性层,不加任何激活函数,输出一个无约束的实数。这个“不加激活函数”的决定经常被新手忽略,有人会在输出层加一个ReLU或Sigmoid,结果导致输出范围被限制住了,完全不符合回归的目标。
比如预测房价,范围可能从几十万到几千万,输出层必须能够输出任意范围内的数值,加Sigmoid把输出限制在0到1之间反而成了束缚。如果实际问题确实需要限制输出范围,比如预测的销量不能为负数,可以在线性输出后再接一个ReLU或Softplus,但这些属于针对业务约束的特殊设计,不能默认添加。
网络结构上,回归任务的特征提取器和分类任务可以共用,比如都是卷积网络提取图像特征、Transformer提取序列特征。区别在于损失函数和输出层。但当任务是回归时,最后一层特征向量的维度选择要更讲究。分类任务最后可能能把特征压缩到类别数个维度,回归任务通常需要保留一定信息量再映射到一维。我自己做回归时,一般会在最后一层加一个隐藏层(比如256维),再做线性映射到1维输出,这样能提高模型的表达能力。
3.3 回归任务评估:MAE还是RMSE?谁更贴近业务
回归模型常用的评估指标有MAE、MSE、RMSE(均方根误差)和MAPE(平均绝对百分比误差)。这些指标各有侧重。
MAE衡量的是平均绝对误差,单位与标签一致,直观易懂。RMSE是MSE开平方,同样在原始量纲下,但因为先平方再开方,RMSE会放大离群点的影响,所以RMSE永远大于等于MAE。如果RMSE显著大于MAE,说明预测中存在一些误差很大的样本,也就是离群点不少。MAPE衡量的是相对误差,排除了标签尺度的影响,适合标签量级差异大的场景,比如有些商品销量是几万、有些是几个,此时绝对误差无法横向比较。
实际项目中,我选择评估指标有一个经验标准:先看业务方关注什么,再选对应的评估指标。业务说“我关心预测值和真实值平均差多少”,就用MAE;业务说“我担心个别极端样本差太多,管控风险”,就用RMSE;业务说“不同品类销量差异很大,想统一看误差占比”,就用MAPE。评估指标一定要能翻译成业务语言,否则模型做得再漂亮,在业务那边也说不过去。
另外提一个在回归任务中经常被忽视但影响很大的细节:标签分布。当标签分布严重偏向某些区域时(比如大部分商品销量集中在低价区,少数高价商品销量也大),模型会被主导区间的样本带着跑,尾部区间的预测效果显著变差。一种有效手段是对标签做对数变换,即让模型预测log(y)而不是直接预测y。这能压缩极端值的尺度,让模型更容易拟合。预测时对输出做指数变换还原。我在处理销量预测任务时用过这个技巧,对高尾部分的预测精度提升非常明显。
4. 自监督学习:不靠人工标签也能学到好特征
前面讲的分类和回归都是典型的有监督学习,强依赖人工标注数据。但现实中标注成本极高,尤其在一些专业领域,需要标注者的专业知识门槛。自监督学习的核心思路是:不依赖人工标签,而是从数据本身构造监督信号,让模型学出好的表征,再在下游任务上做微调。这个方向已经成了大模型时代的标配路径,也是我认为每个深度学习从业者都必须理解的一类任务。
4.1 自监督为什么重要:从“给模型喂标签”到“让模型自己出题”
传统监督学习是有“标准答案”的,模型拿着输入去对答案,错了就更新参数。自监督学习没有这个标准答案,它自己从数据里构造一个辅助任务(pretext task),用数据自身的结构当标签。
举一个最直观的例子:文本模型的预训练,输入是一段话,把其中一部分单词给挖掉,让模型根据上下文预测被挖掉的词是什么。这不需要任何人标注,直接把原始文本拿来就能用。模型在完成这个任务的过程中,被迫学习词汇的语义、上下文的语法结构,最终得到的能力就是对语言本身的理解。图像领域同样如此,把一张图切成若干块,挖掉其中一块,让模型根据周围信息重建这一块的内容,模型在做的过程中学到了颜色、纹理、形状、物体边界等丰富的视觉概念。
自监督最大的价值在于:可以用海量无标注数据训练出一个强大的特征提取器,这个特征提取器在下游任务(分类、检测、分割等)中只需要少量标注数据做微调即可达到不错的性能。尤其在标注成本高的行业,这个策略几乎是唯一的可行路线。我参与过一个工业质检项目,缺陷样本只有几百张,刚开始直接用这些数据训练一个分类模型,怎么调准确率都上不去。后来换成先在无标注的产线图像上做自监督预训练,再把学到的参数迁移到缺陷分类任务上微调,效果直接提升了一大截。这让我彻底改变了对自监督学习的态度,它不是什么理论前沿,而是实际解决问题的重要工具。
4.2 对比学习范式:让相似样本靠近、不相似样本远离
对比学习是自监督学习中最有代表性的一条路线。它的核心思想很直观:同一张图片经过两种不同的数据增强变换(比如随机裁剪、色彩抖动、旋转),得到的两个版本被视为一对“正样本”,它们在特征空间里的表示应该彼此靠近;不同图片之间则被视为“负样本”,它们的表示应该彼此远离。
用一句话概括就是:学到一种特征空间,让相似的样本在空间里聚集,不相似的样本在空间里散开。
这里给出一个基于SimCLR思路的简化实现,在实际学习中,这个框架理解透了,再去看各种对比学习变体就会容易很多:
import torch import torch.nn as nn import torchvision.models as models class SimCLRModel(nn.Module): def __init__(self, feature_dim=128): super().__init__() # 使用ResNet-18作为编码器,输出原始特征 self.encoder = models.resnet18(pretrained=False, num_classes=512) # 投影头:将特征映射到对比学习用的低维空间 self.projection = nn.Sequential( nn.Linear(512, 256), nn.ReLU(inplace=True), nn.Linear(256, feature_dim) ) def forward(self, x): features = self.encoder(x) # L2归一化,约束特征在单位球面上 features = nn.functional.normalize(features, dim=1) project = self.projection(features) project = nn.functional.normalize(project, dim=1) return features, project def contrastive_loss(projected_a, projected_b, temperature=0.5): # 将两个分支的特征拼接成同一批次 batch_size = projected_a.size(0) # 这里最底层的实现使用NT-Xent损失 logits = torch.matmul(projected_a, projected_b.T) / temperature labels = torch.arange(batch_size, device=projected_a.device) # 对角线位置是正样本对,其余为负样本 loss = nn.functional.cross_entropy(logits, labels) return loss这里有一个很值得注意的细节:温度系数temperature。这个超参数控制着相似度分布的“锐利程度”。温度越低,特征分布越尖锐,模型对负样本的区分要求越高;温度越高,分布越平滑,对所有样本一视同仁。SimCLR原文和其他对比学习论文普遍发现较低的温度能显著提升效果,默认值常取0.5或0.1。我在实际调参过程中也发现这个参数对最终表征质量的影响非常大,甚至比模型结构本身还关键。建议重点调这个值而不是一上来就去换大模型。
除了温度系数,对比学习里还有几个公认的实操要点。一是batch size要足够大,因为负样本对来自同一个batch内,batch越大负样本越丰富,SimCLR原文甚至用了8192的batch size。在资源受限的情况下,可以借助梯度累积机制模拟大batch。二是数据增强是效果的关键,随机裁剪和色彩抖动是最常用的变换组合,增强强度过弱会让正样本对太简单,学不到好特征,过强会让正样本对难以识别出是同一个物体。三是投影头的设计,SimCLR专门实验证明了projection head对效果很重要,因为它把编码器得到的特征先映射到一个对比空间进行训练,训练完之后再丢弃投影头,使用编码器特征做下游任务。
4.3 掩码重建范式:大模型预训练的路子
比起对比学习,掩码重建的思路更直给:输入一部分已知信息,预测被遮住的部分。这个思想在NLP里已经存在了很久,BERT的Masked Language Model就是掩码重建的典范。在图像领域,MAE(Masked Autoencoder)把这一思路发扬光大。
MAE的做法是先把图像划分成若干Patch,随机遮蔽掉大部分Patch(比如75%),然后将可见的小部分Patch送入编码器,解码器负责重建被遮蔽的Patch。关键发现是:这种看似“自虐”的做法能逼着模型学到高级的语义特征,因为想要从25%的可见信息中重建完整图像,模型必须真正理解物体形状、纹理和上下文关系。这里有个反直觉的点:遮蔽比例越高,学习到的表征反而越好,即使重建的视觉质量变差了。原因是高遮蔽比例强迫模型去做真正的语义理解,而不能靠邻近像素直接插值填补。
掩码重建范式在图像和文本中已经成为大模型预训练的主流,几乎所有主流大语言模型和视觉Transformer模型都走这条路。它的优势在于:不需要构造正负样本对,训练更稳定;数据利用率高,任何无标注数据都可以直接用;学到的是深度语义特征,在下游任务上泛化能力强。
在工程落地时有一个实用建议:如果你的业务是垂直领域,比如医学影像、工业缺陷或特定类型的文本,强烈建议先用自己的领域数据做自监督预训练或领域继续预训练。通用预训练模型虽然强大,但未必熟悉你领域数据的分布特点。我在医学影像任务上做过一次实验:拿一批无标注的胸片做MAE预训练,再只给几百张有标注的胸片做微调,效果比直接使用通用ImageNet预训练模型高出不少。这个结果其实体现了自监督学习最重要的价值:让模型从你的数据里先学会“这一行长什么样”,再去解决具体问题。
5. 序列任务:不只是输入不同,而是对时序和语义的理解
序列数据在现实世界中随处可见:文本是一串词构成的序列,语音是一串帧构成的序列,传感器数据是一串时间点构成的序列。序列任务的特殊之处在于:数据点之间存在先后依赖关系,模型不能把它们当作独立样本来看待。分类和回归虽然也能处理序列输入,但当任务是“基于序列上文预测下文”、“判断序列中每个位置对应的标签”时,就进入了序列任务的范畴,技术路线也跟着变化。
5.1 序列标注任务:为每个位置打标签
序列标注是NLP和信息抽取里的基础任务。给定一串输入序列,模型需要为序列中的每个位置输出一个对应的标签。命名实体识别就是最典型的代表:一句话中的每个词,需要被标记为人名、地名、组织机构名或无关词。词性标注同样如此,每个词要标出它是名词、动词还是形容词。
这类任务的标准技术栈已经非常成熟。分词器先把文本切成Token队列,模型编码器(如BERT、LSTM等)提取每个位置的特征,最后每个位置接一个全连接层做分类。由于序列中的标签前后有依赖(比如“人名”后面很少直接跟“组织机构名”),业界普遍会再叠一个CRF层对输出约束做全局优化。CRF的作用是学习标签之间的转移规则,保证输出的标签序列整体合理。
用BERT做序列标注任务时,有一个关键工程细节很容易踩坑:标签对齐问题。BERT使用WordPiece分词器,一个英文单词可能被切成多个子词(subword),而标注标签通常是对“原始单词”级别标注的。处理办法是:第一个子词的标签保留原始标签,其后的子词全部标记为某种占位标签(比如“X”或“-1”),计算损失时忽略这些占位标签的预测结果。我见过好几个项目因为没处理好对齐,模型训练时loss异常震荡、评估效果非常差,排查了一圈才发现是标签错位导致的。
实体识别任务里有另一个几乎人人都会碰到的问题:嵌套实体。比如“北京大学人民医院”中,“北京大学”是一个组织机构实体,而“人民医院”整体又是另一个组织机构实体,但它们的位置是重叠的。常规序列标注只能处理不重叠的扁平实体,遇到嵌套实体就有问题。业界目前有几种方案,最常用的是“打平”后输出最长实体,或者用基于Span的方式预测所有可能的实体片段(起点+终点+类型),再配合全局解码。如果业务里嵌套实体比例不小,建议一开始就选择Span-based方案,而不是硬套序列标注。
5.2 序列生成任务:从翻译到摘要
与序列标注不同,序列生成任务的输入和输出都是序列,但长度可以不同,内容可以同构也可以异构。机器翻译是最典型的例子:输入一句中文,输出一句英文,输入输出是不同语言的同义表达。文本摘要也类似,输入一篇长文,输出一小段浓缩的要点。
当前做序列生成任务的标配方案是Transformer,配合“自回归解码”的策略:逐词生成输出,每次生成一个Token,把已生成的Token拼接在输入后面,继续生成下一个Token,直到生成结束标记。这个看似机械的过程,背后是一个核心逻辑:生成任务里“上下文依赖”非常强,当前生成的词依赖于之前所有已生成的词。这也是为什么自监督预训练(先学理解)加微调(再学生成)框架在NLP领域几乎统治了所有任务。
序列生成训练阶段最需要注意的问题是“训练与推理不一致”,即所谓的Exposure Bias问题。训练时模型每一步看到的是真实历史Token(Teacher Forcing),但推理时它看到的是自己之前生成的Token,上一步的错误会沿着生成过程累积。为了缓解这个问题,业界常用的手段是计划采样,就是训练前期多使用真实Token做输入,训练后期逐渐增加模型自己生成Token的比例,让模型适应自己的错误。听完原理可能觉得很简单,但实际训练时这个比例怎么安排很影响效果,我试过把采样概率均匀地从1降到0,效果不如在训练后段保持一个较小的概率长期存在来得好。
另一个在序列生成里极其影响体验的工程问题是重复生成。模型经常会出现“我我我我”、“你好你好你好”这种复读机现象,或者整段话绕圈重复。这本质上是因为解码策略选择的贪心解码(每次选概率最大的Token)容易陷入局部循环,或者因为训练表里的目标序列本身多样性不足。业界通用的做法是引入Beam Search(束搜索),维护多个候选序列,全局评分取最优,而不是每一步只看当前最优。同时还可以在解码时加入repitition penalty,对已经出现过的Token施加惩罚。我在长期做文本生成项目后有个心得:90%的重复问题靠调解码参数能解决,而不是换模型。
5.3 序列任务在非NLP领域的应用:以时序预测为例
序列任务绝不仅是NLP的专属领域。工业设备传感器的读数随时间变化,金融产品的价格走势,电商平台的日销量,这些都是时间序列数据。基于深度学习的时序建模已成为一个重要方向,与NLP有相通之处,但也有独特之处。
经典的时间序列预测模型有很多,从最早的LSTM到后来引入注意力机制的Transformer变体,再到专门设计的时序卷积网络。如果数据量不大或序列模式相对规律,LSTM其实效果就很不错。如果一个序列包含非常长期且复杂的依赖模式,或者你要同时预测多个变量、捕捉多个变量之间的交互关系,Transformer结构会更合适。
时序预测有个极其关键且容易被新手忽略的步骤:归一化。时间序列数据往往包含趋势项和季节性成分,不同变量的数值范围可能差异悬殊。如果不做归一化,模型训练时大量算力会被大数值变量主导,模型学不到小数值变量的规律。在工程实践中,我对每个滑动窗口内做标准化是一种常见且有效的处理方式。窗口内的标准化能保留局部相对变化,同时消除尺度差异。还有一个细节:有些时候需要对差分后的序列建模(即预测变化量而不是绝对值),这能大幅消除非平稳性,让预测准确率明显提升。
6. 面对新任务时如何选择方案:一个可复用的决策框架
到了这一步,理论框架和各类任务的实现方式都覆盖到了。现在面临一个更实际的问题:拿到一个业务需求,怎么判断它是哪类任务?怎么选择技术方案?我在带团队时总结了一个决策框架,用了几轮迭代,目前看下来很稳定。
第一个问题是输出是什么形态。如果输出是离散的有限类别标签,那就是分类问题;如果输出是连续数值,那就是回归问题;如果输出是一个序列,就要看输出序列和输入序列等不等长,等长且逐位对应,是序列标注,不等长或内容不逐位对应,是序列生成。这个判断基本锁定了任务大类。
第二个问题是有没有标注数据。如果完全没有标注,分类和回归就做不了,此时要么先去采集和标注数据,要么走自监督路线,先学特征,再在极小标注量下微调。如果有一定的标注数据但量不大,最有效路径是先做预训练或使用别人预训练好的模型,再做迁移学习微调。
第三个问题是业务到底关心什么。这一点很多人会忽略。评估标准直接决定了模型优化的方向。之前做推荐相关项目时,业务方说“用户点击率要尽可能高”,这就是二分类问题;但后来发现业务方真正的诉求是“每一条给用户看到的推荐内容都要尽量准确”,这句话则变成了对置信度和排序能力的考量。同样的数据,诉求不同,技术方案完全不同。拿到需求先别急着搭模型,先把业务关心的事情翻译成任务类型和评估指标。
最后一个问题是资源约束。同一个任务,从零训练一个模型和自己标数据,成本要准确评估。可现在很多人拿到任务第一反应就是“用大模型”,实际上在很多任务中,一个训练良好的小模型效果就能胜过通用大模型。自监督预训练加任务微调的策略就是在算力有限情况下兼顾效果和成本的选择。我在多个实际项目里反复确认了一条经验:先跑通弱智线(简单模型+基础特征),再上复杂模型,一步一步优化,往往比一开始就上最强模型更高效。
另一种常见情况是面对一个全新的数据集,无从下手。我推荐一个“三步走”策略。先用一个小型模型快速跑通流程,确认数据能正常加载、Loss能正常下降,这一步只求“能动”。然后分析模型输出,看错误都集中在哪些样本上,定位问题方向,是数据问题、标签问题还是模型容量问题。最后再针对性地换模型结构、加数据增强、调损失函数。跳过前三步直接上大模型,结果往往是训练半天发现数据有bug,浪费大量时间。
多任务学习对工程落地也非常有实际意义。很多业务天然就是多输出的,比如一个商品推荐系统,既要判断用户会不会点击(二分类),又要预测点击后下单的可能性(回归),还要预测用户看了之后会不会收藏(二分类)。与其做三个模型,不如共享底层特征、在上层分叉出多个输出头,这样能共享表示、减少重复计算。这个思路在工业界应用非常普遍,训练时需要注意:多个任务的Loss量级需要做平衡,否则大Loss的任务会主导梯度。我常用的做法是先给每个任务设置初始权重,再根据训练中Loss的量级动态调整权重,让多个任务的训练进度尽量同步。
7. 写在最后:从任务分类到真正的项目实战
整个系列的第一篇就在这里收个尾。我把监督学习的两大主力(分类和回归)讲透了,也把当前大模型时代绕不开的自监督学习、以及序列相关的典型任务做了系统梳理。回看整篇内容,我想强调一个被无数人忽视的认知:任务分类不是学术概念,而是工程方法论的起点。你在项目里看到的所有数据处理、模型选择、损失函数设计、评估指标确定,都由任务类型决定。选错了任务类型,就是在错误的赛道上努力,再好的模型和调参都救不回来。
在我个人做项目的过程中,最深的体会是:拿到一个需求,先别急着调包跑模型,把问题翻译成技术任务,把所有技术选项都摆出来,想清楚每种选择的代价和收益,再决定动手方向。这个习惯帮我避开了大量的无效劳动。深度学习技术的迭代速度很快,今天的最优模型结构,可能半年后就会被新的替代,但任务分类的逻辑框架相对稳定,它教你的是“面对一个未知问题时,如何找到正确的解决路径”。
下一篇我打算接着讲生成任务(图像生成、风格迁移、文本生成)的核心闭环和评估方法,这些都是当前AIGC实战中最常遇到的方向。如果你在阅读过程中有任何困惑,或者遇到某个具体任务不知道怎么归类,欢迎带着问题来交流。系列文章后面还会继续更新,我会保持这种“讲原理更讲实战”的风格,尽量让大家读完就能用到自己项目里去。