NLP全链路知识地图:从文本预处理到模型部署
自然语言处理技术这块知识点,摊开来看像一张网,密密麻麻全是概念:分词、词向量、注意力、预训练、微调、评估、部署,每一块单拎出来都能写一本书。我这几年的体会是,大部分人卡住不是因为某个知识点太难,而是因为不知道这些知识点之间是怎么串起来的,学到哪一步该停下来去动手,哪一步可以先用个简化版本糊过去。这篇文章我把自然语言处理技术的知识点按我自己带人和做项目时的顺序重新排了一遍,从文本进模型前的地基处理,到表示、结构、任务范式、评估、工程落地,每一块都会说清楚"它解决什么问题""什么时候该用它""用错了会怎样"。适合刚入门想建立整体框架的朋友,也适合已经能跑通demo、但一到真实数据就各种翻车的同行,里面的参数计算、代码片段和排查经验都是可以直接抄走用的。
1. 先理清知识地图:我为什么按这条链路梳理
1.1 从"文本进、结果出"的完整链路倒推知识点
我见过太多人学NLP的方式是:先啃一遍注意力公式,再背一遍Transformer结构图,然后发现自己还是不知道拿一段客服对话该干什么。问题出在顺序上——先学工具,后学问题,知识点就成了孤岛。我的建议是反过来,从一条最小链路倒推:一段原始文本进来,先要做清洗和切分,变成模型能吃的最小单元;然后要把单元映射成向量,让数值计算有东西可算;接着选一个结构去建模单元之间的关系;再往上套一个任务头,把它变成分类、标注、问答或者生成;最后用指标判断好坏,把模型变成一个服务。这条链路上每一个环节都对应一批知识点,而且前后是有依赖的——你分词粒度选错了,后面的词表、序列长度、显存占用、甚至生成质量全都跟着歪。
按这条链路走还有个好处:你能清楚知道每个知识点是为哪一步服务的。比如子词切分是为了解决词表爆炸和未登录词,位置编码是为了让并行结构感知顺序,层归一化是为了训练稳定,这些"为了"就是我在后面每一节都要反复强调的东西。脱离问题去记知识点,记完三天就忘;带着问题去记,哪怕公式记不全,你也能在需要的时候查回来并用对。
1.2 三层知识地图与各层的学习优先级
我把NLP知识点分成三层,优先级从高到低。 基础层 :字符编码与文本规范化、分词/子词切分、词表构建、序列填充与截断、标签体系设计。这一层决定了你的数据能不能用好,投入产出比最高,但最枯燥,很多人跳过,结果后期所有问题都从这里冒出来。 模型层 :文本表示方法(稀疏与稠密)、序列建模结构、注意力与前馈网络、预训练目标、微调与参数高效方法。这一层概念密度最大,但它更新换代最快,学的时候要抓"为什么这么设计",而不是背某个具体结构的细节。 工程层 :评估指标、数据泄漏排查、推理性能、版本管理、线上监控。这一层几乎没人系统地教,但它决定了你的模型能不能真正跑起来。
优先级上,我给新人的建议是基础层和工程层先打通,模型层边用边补。理由很实在:一个清洗规则写对、评估集切干净、指标算明白的简单方案,经常能打赢一个模型很 fancy 但数据漏了、评估集混了、指标算错的复杂方案。我在实际项目里见过不止一次,把训练集和验证集的重叠样本去掉之后,原来那个"效果拔群"的模型指标直接掉十几个点——不是模型不行,是评估撒了谎。
1.3 一个贯穿全文的最小示例设定
为了让后面的知识点有落点,我虚构一个贯穿全文的场景:给一批用户反馈文本做多标签分类(比如"物流""质量""客服""价格"四类,一条文本可以命中多个),再叠加一个抽取需求,把反馈里提到的具体商品部件抽出来。这个场景麻雀虽小,五脏俱全:分类覆盖了文本表示、池化、损失函数、阈值调优;抽取覆盖了序列标注、标签对齐、边界处理。数据量我设定为两万条左右,这个量级很尴尬——大到不适合纯规则,小到不足以从头训练一个大模型,正好逼着你把预训练、微调、评估这些知识点全部用上。
提示:全文出现的代码都是可运行的最小片段,依赖只用通用的开源库,具体版本不做约束,接口层面基本稳定。参数数值是为了演示计算过程,实际项目请按自己的数据规模缩放。
2. 文本预处理与切分:最容易被低估的地基
2.1 中文分词与子词切分,到底该选哪个
分词这块最常见的争论是"中文到底要不要先分词再喂模型"。我的结论很明确: 如果你的下游是预训练模型体系,优先用子词切分,不要自己先切词 。原因在于子词切分(BPE、WordPiece、Unigram 这类算法)本质上是数据驱动的,它从语料里统计高频字符组合,把常见词保留成完整单元,把生僻词拆成碎片,从而在"词表大小"和"覆盖率"之间找平衡。你如果先按词典切好词再送进去,等于把外部分词器的错误固化成模型的输入,而且模型原有的子词词表跟你切出来的词对不上,大量词会退化成未登录标记。
那传统分词还有没有用?有,但用在 特征工程和规则兜底 上。比如做序列标注的边界修正、做关键词召回、做文本长度统计、做领域词典增强,这些场景下分词结果是有价值的辅助信号。真要用,注意两个坑:一是 歧义切分 ,比如"研究生命起源"这类经典例子,正向最大匹配和逆向最大匹配结果不同,需要双向比对加词频消歧;二是 未登录词 ,人名、型号、缩写这类词词典里没有,切出来就是一堆单字,会污染下游特征。我的做法是主链路走子词,分词只用于规则层和特征层,两边不混。
子词算法本身也要理解流程,不然调词表大小时完全是盲猜。以最常用的BPE为例:先把语料按字符拆开,统计相邻字符对的共现频次,反复合并最高频的对,直到词表达到目标大小。这个过程决定了词表大小是个超参——太小则常见词被拆碎,序列变长、计算量上升;太大则低频词占坑,嵌入矩阵膨胀且训练不充分。经验区间是:中文场景词表两三万到五万都很常见,英文三四万够用。词表大小对参数量的影响很直接,嵌入层参数约等于词表大小乘以隐藏维度,三万词表配768维就是约2300万参数,这还没算输出层如果做权重共享。
2.2 清洗与规范化:一份可以直接抄的检查清单
预处理这块我踩过最惨的一次坑,是线上反馈文本里混了大量全角标点、零宽字符和表情符号的变体,结果同一句话在不同渠道编码不一样,模型在训练集里学到的模式在线上完全对不上。从那以后我养成了一个习惯:清洗规则写成显式的、可测试的函数,而不是散落在 notebook 的各个单元格里。下面这份清单是我现在基本都会过一遍的。
- 编码统一 :全部转成统一编码,异常字节用替换策略而不是直接丢弃,丢弃会让句子断在半截。
- 全角半角归一 :数字、字母、标点统一形态,否则"100"和"100"在词表里是两个完全不同的单元。
- 不可见字符清理 :零宽空格、软连字符、方向控制符,这些肉眼看不见但会切碎子词。
- 空白与换行规范化 :连续空白压成一个,换行统一,避免序列里塞满无意义填充。
- 表情与符号策略 :保留、转义成文字描述、还是直接删,三种策略对情感类任务影响很大,必须显式决策并记录。
- 长度截断与分块 :超长文本按句子边界切块,别硬切在词中间。
import re
import unicodedata
INVISIBLE = dict.fromkeys(map(ord, "\u200b\u200c\u200d\ufeff\u00ad"), None)
def normalize(text: str, keep_emoji: bool = True) -> str:
if not isinstance(text, str):
return ""
# 1. 统一编码形态,兼容全角半角
text = unicodedata.normalize("NFKC", text)
# 2. 去掉不可见字符
text = text.translate(INVISIBLE)
# 3. 表情处理策略
if not keep_emoji:
text = re.sub(r"[\U0001F300-\U0001FAFF\u2600-\u27BF]", " ", text)
# 4. 空白与换行规范化
text = re.sub(r"[\r\n\t]+", " ", text)
text = re.sub(r"\s{2,}", " ", text)
return text.strip()
这段代码看着简单,但每一条都对应过真实事故。特别是 NFKC 归一这一步,很多人以为只是处理全角,其实它还会把一些连字、上标、罗马数字统一掉,对提升词表命中率有帮助。我一般会在清洗后加一步统计:随机抽两百条,人工看一眼清洗前后的差异,确认没有把有效信息洗掉。这个动作花十分钟,能省掉后面两天的排查。
2.3 词表、未登录词与填充截断的细节
词表构建这块,核心知识点是 覆盖率统计 。做法很朴素:拿你的领域语料跑一遍分词或子词切分,统计有多少个 token 落在了词表外。如果未登录比例超过百分之一二,说明词表和你的领域语料不匹配,要么扩充词表,要么换一个更贴合的子词模型。我一般会把未登录的 token 按频次排个序,看看是不是集中在型号、缩写、专业术语上,如果是,就往词表里补;如果是一堆乱码和噪声,那就该回头修清洗。
填充和截断这两个操作看似无脑,实际上埋了不少雷。 截断 方面,我习惯按分位数定长度:统计训练集 token 长度的分布,取百分之九十五分位作为最大长度,剩下的截掉或分块。为什么不用最大值?因为一条超长样本会把整个批次的显存拉满,而它对训练的贡献可能微乎其微。 填充 方面,注意填充位置要和注意力掩码严格对应,我曾经因为掩码没有覆盖填充位,模型把一堆填充符当成了有效上下文,验证集指标看着还行,线上一到长文本就崩。另外,动态填充(按批次内最长样本填充)比全局定长填充能省不少算力,尤其是长度分布很偏的场景。
注意:标签对齐。做序列标注时,如果你的切分粒度和标签粒度不一致(比如标签是按字的,模型输入是按子词的),必须显式做对齐:子词的首个片段继承标签,后续片段标记为忽略。不做这一步,损失函数会在错误的位上计算,指标看起来有数,实际全是噪声。
3. 文本表示:从稀疏权重到上下文向量
3.1 TF-IDF 依然是最强的基线,别急着跳过
很多人一入门就想上稠密向量,觉得 TF-IDF 太"古典"。我的实际经验恰恰相反: 在任何文本分类项目里,TF-IDF 加线性模型都应该是你第一个跑出来的基线 ,它的训练成本几乎为零,可解释性好,而且在小样本、强领域词特征的任务上经常打得过复杂模型。我做过好几次对比,两万条以内的短文本分类,清洗得当的 TF-IDF 加逻辑回归能到相当可用的水平,先把这个基线钉住,后面所有复杂方案都得跟它比,比不过就是白折腾。
TF-IDF 的知识点核心在权重的计算逻辑。词频部分常用的是对数缩放,避免长文本天然权重高;逆文档频率衡量词的区分度,出现在越少文档里的词权重越高。举个具体计算:假设语料有一千篇文档,某词在某篇文档里出现三次,而它总共出现在五十篇文档中,那么词频取对数缩放后约等于一加上三的自然对数,大约二点一;逆文档频率约等于一千除以五十取自然对数再加一,大约四;两者相乘约八点四。这个数值和"出现一次但只在一篇文档里出现"的词相比就低了不少,后者逆文档频率能到七以上。理解这个计算过程,你才能解释为什么某些关键词在模型里权重异常——通常是它在语料里太稀有,被过度加权了。
实践中要调的参数不多但都关键: 词表规模上限 (去掉极低频词,减少噪声维度)、 文档频率上下限 (太高的停用词和太低的稀有词都该过滤)、 是否使用子词 (中文场景可以按字或按词)、 是否做长度归一化 (余弦相似度或 L2 归一化)。还有一个常被忽略的点:TF-IDF 的统计量必须 只在训练集上拟合 ,再把同一个变换应用到验证集和测试集。我见过有人在全量数据上拟合向量化器,这属于典型的轻量数据泄漏,指标会虚高一点几个点,很难察觉。
3.2 静态词向量的原理、用途与它绕不过去的两个短板
静态词向量的知识点,重点是理解它到底学到了什么。以最经典的两种思路为例:一种是用中心词预测上下文,另一种是用上下文预测中心词;前者的训练目标更偏向语义相似,后者对低频词更友好,还衍生出了子词增强的版本,用字符片段组合来表示词,从而缓解未登录词问题。它们的共同产出是一张"词到向量"的查找表,维度常见一百到三百,维度太低表达力不够,太高则在小语料上训练不充分且存储膨胀。
静态词向量有两个绕不过去的短板,必须心里有数。 第一是一词多义 :同一个词在所有语境下只有一个向量,好坏、苹果这类词会被强行平均成一个模糊的点,做细粒度情感或实体区分时明显吃亏。 第二是未登录词 :词表外的词要么映射到统一的未知标记,要么被丢弃,领域新词一多就废。这两个短板正是上下文向量存在的理由,理解了这个"为什么",你就不会在需要区分词义的任务上硬用静态向量。
它的价值在哪里?在 冷启动和资源受限场景 。你要做一个离线端上的粗分类器,模型体积要压到几兆,静态向量加轻量网络是合理选择;你要做词级别的相似度召回、近义词扩展、聚类探索,静态向量的几何性质直观好用。我的做法是把它当成工具箱里的一把螺丝刀,不当成万能锤。
3.3 句向量与池化策略:向量塌缩这个坑要早点知道
从词向量到句向量,这一步的知识点最容易被轻视。最朴素的做法是对所有词向量取平均,问题在于它把语序和重要词权重全抹平了,"狗咬人"和"人咬狗"会得到非常接近的向量。稍微好一点的做法是加权平均,用词的区分度做权重;再往上是用模型编码后取特殊位置或做注意力池化。这些方法的取舍点是: 池化越简单,信息损失越大,但越稳定、越不容易过拟合;池化越复杂,表达能力越强,但对数据量要求越高 。
这里必须提一个我踩过的坑: 表示塌缩 。如果你拿一个预训练编码器直接取平均池化做句向量,然后算余弦相似度,会发现很多不相关句子的相似度都在一个很高的基线附近,区分度极差。原因是预训练目标并没有要求句向量在角度上均匀分布,高频方向占了主导。解决办法有两类:一是换用专门训练过的句向量模型;二是在自己的领域数据上做对比学习微调,用正负样本对把向量空间拉开。做对比学习时注意温度系数这个超参,它控制对难负样本的惩罚强度,设得太小训练不稳定,设得太大则学不到区分度,我一般从零点零五附近开始试。
import numpy as np
def mean_pool(hidden_states, attention_mask):
# hidden_states: [batch, seq_len, dim]
mask = attention_mask[..., None].astype(np.float32)
summed = (hidden_states * mask).sum(axis=1)
counts = np.clip(mask.sum(axis=1), 1e-6, None)
return summed / counts
这段池化代码的正确性完全依赖掩码,掩码错了池化结果就被填充位污染。我建议在写完之后加一个断言测试:构造两条内容相同但填充长度不同的输入,池化结果应该几乎一致。这个小测试能挡住一大类隐蔽 bug。
4. 模型结构:从序列建模到注意力
4.1 循环结构的梯度问题与序列标注里的 CRF 层
循环神经网络这一族的知识点,核心不是公式,而是 它为什么会被后来的结构取代 。逐时间步传递的机制决定了它难以并行,长序列上梯度要么衰减要么爆炸,门控机制(长短期记忆、门控循环单元)通过加法和门控缓解了衰减,但没有根治。我在实际项目里的判断标准很简单:序列长度在几百以内、需要严格的因果时序、算力极其受限,循环结构仍有位置;其余情况优先考虑并行结构。
序列标注任务里有一个知识点经常被跳过: 条件随机场层 。为什么在输出层加它?因为逐位独立分类会产出非法标签序列,比如标签体系是"开始、中间、结束、外部",模型完全可能输出一个"中间"开头却没有"开始"。条件随机场层建模的是相邻标签之间的转移约束,解码时用动态规划找全局最优路径,而不是贪心取每一位的最大值。这里的关键经验是: 转移矩阵的初始化很重要 ,如果数据量小,转移矩阵学不准,反而不如手工设定非法转移为极大负值。我一般会先把明显非法的转移(比如从外部直接跳到结束)硬性屏蔽掉,剩下的交给学习。
还有个实操细节:子词切分之后,一个词被拆成多个片段,标签只能落在第一个片段上,其余片段要在损失计算时忽略。这个"忽略索引"的设置如果漏了,模型会学到"片段中间应该是什么标签"这种根本不存在的模式,指标看着正常,实际抽取边界全歪。
4.2 注意力机制的直观理解与计算量估算
注意力的知识点,我用一句话概括: 它解决的是"在当前位置,我应该看输入的哪些部分,看多重" 。查询向量代表当前位置的需求,键向量代表各位置能提供什么,两者做点积得到匹配分数,归一化后作为权重去加权值向量。这套机制的好处是任意两个位置之间的路径长度都是一步,不像循环结构要走很多步,长距离依赖更容易捕捉。
自注意力的计算量必须会估,不然显存爆炸时你完全不知道该调哪个参数。单层自注意力的核心运算量大致正比于序列长度的平方乘以隐藏维度。举个数:序列长度五百一十二,隐藏维度七百六十八,仅查询与键的矩阵乘法一项,乘加次数约为二乘以五百一十二的平方再乘以七百六十八,算下来约四亿次浮点运算,也就是零点四吉。这个"平方项"是灾难的来源——序列长度翻倍,这部分运算量翻四倍。所以长文本场景下,第一优先是砍序列长度或做分块,第二是考虑稀疏化或低秩近似,而不是硬堆显存。
多头机制这里有个常见误解: 多头不增加总计算量 。因为每个头的维度被均分,头数乘以每头维度等于总维度,总运算量基本不变。多头的价值在于让不同的头关注不同的关系模式,比如有的头盯相邻词,有的头盯标点边界。头数不是越多越好,头太多会让单头维度太小,表达能力受限,常见配置是八到十六头。
4.3 位置编码、归一化位置与层数头数的选型经验
位置编码这个知识点,问"为什么需要"比问"公式是什么"重要得多。并行结构本身对顺序一无所知,你把输入顺序打乱,输出除了对应位置交换之外没有任何变化,这在语言任务里是致命的。位置编码把顺序信息注入进去,常见做法有两类:一类是用固定函数生成,优点是外推性相对可控且不占参数;一类是学习得到,优点是灵活,缺点是超出训练长度就无能为力。近年还有相对位置的做法,直接建模位置之间的偏移关系,长文本外推表现更好,代价是实现复杂度上升。
归一化放在哪里,是个影响训练稳定性的实际问题。放在子层之前(前置归一化)训练更稳,可以用更大的学习率,收敛稍慢一些;放在子层之后(后置归一化)是早期做法,深层网络容易不稳,需要精心调预热。我的建议是:如果没有历史包袱,用前置归一化,配合残差连接,能省掉大量调参时间。
层数和头数怎么定?我的经验公式很土: 先看数据量,再看延迟预算 。数据在十万条以内做微调,层数往小了选,十二层以内基本够用,再深就是过拟合加推理慢;延迟要求高的线上服务,层数和隐藏维度都要压,宁可牺牲一两个点指标。隐藏维度和头数要保持整除关系,隐藏维度除以头数得到的单头维度不要低于三十二,低于这个数注意力基本学不出东西。
4.4 预训练目标:三种范式决定了模型的擅长方向
预训练目标这个知识点,直接决定了你该选哪类模型,很多人是反过来的——先拿一个模型,再硬套任务,效果不好就怪数据。三种主流范式我这样理解:
| 预训练目标 | 核心做法 | 擅长任务 | 不适合的任务 |
|---|---|---|---|
| 掩码重建 | 随机遮住部分片段,让模型还原 | 理解类:分类、抽取、匹配 | 自由生成、长文本续写 |
| 因果语言建模 | 用前文预测下一个词 | 生成类:续写、对话、摘要 | 需要双向信息的精细理解 |
| 序列到序列 | 编码输入、解码输出 | 翻译、改写、结构化生成 | 超长输入的实时处理 |
选型的逻辑是"训练目标和下游目标越接近越好"。你做的是给一段文本打标签,那就是理解类任务,掩码重建出来的编码器天然合适;你要生成摘要,因果解码器或者序列到序列结构更顺。硬用生成模型做分类,只能靠提示词或者在外面套一个分类头,前者不稳定,后者等于浪费了模型一半的参数。
还有一个常被忽略的知识点: 预训练语料的领域和你的任务领域差多远 。差距大时,微调需要的数据量更多,或者需要做领域内的继续预训练。继续预训练的成本比想象中低,拿领域无标注文本跑一轮掩码重建,学习率设小一点,往往能带来明显提升,尤其是术语密集的垂直领域。
5. 任务范式与微调策略
5.1 四大任务范式的输入输出设计
NLP 的任务看着五花八门,归到输入输出的形式上其实就四类,把这四类的输入输出设计吃透,遇到新需求就能快速映射。
- 句级分类 :一段文本进,一个或多个标签出。知识点集中在池化方式(取特殊位还是平均)、多标签的阈值处理、类别不均衡的损失加权。多标签场景下不要用互斥的归一化,每个标签独立判断。
- 序列标注 :一段文本进,每个单元一个标签出。知识点在标签体系设计(边界标记方案的选择)、对齐、解码约束。标签体系设计得好,后面能省很多后处理代码。
- 抽取式问答 :文本加问题进,文本中的片段位置出。知识点在答案跨度的起止位置建模、最大答案长度、无答案样本的处理。无答案样本不给足,模型会强行在无关段落里抠出一个答案。
- 生成式 :文本进,文本出。知识点在解码策略(贪心、束搜索、采样)、长度控制、重复抑制。束搜索的束宽不是越大越好,宽到一定程度收益递减还拖慢推理。
我的实践建议是: 能用抽取就别用生成 。抽取式方案的可控性和可评估性强得多,生成式方案看着灵活,实际上把不确定性全留给了线上,出了问题很难定位是数据问题还是解码问题。
5.2 全量微调、参数高效微调与检索增强的取舍
微调策略这块,知识点要落到"花多少算力换多少效果"上。全量微调更新所有参数,效果上限最高,但显存和存储成本都大,多任务多版本时更痛苦——每个任务存一份完整权重,运维会疯。参数高效微调只训练一小部分新增参数,最典型的是低秩分解:在原有的权重矩阵旁边并联两个小矩阵,一个降维一个升维,只训练这两个。
参数量可以算得很清楚:对一个 d 乘 k 的权重矩阵,秩取 r,新增参数量是 r 乘以 d 加 k。以 d 等于 k 等于七百六十八、r 等于八为例,新增参数约为一万二千,而原矩阵有约五十九万参数,占比百分之二左右。这个比例意味着你可以同时挂十几个任务的适配器,存储成本仍然可控。r 怎么选?任务和预训练分布差得远,r 往大了取(十六到六十四);只是做风格适配或格式适配,r 取四到八就够。注意低秩适配通常只加在注意力的投影矩阵上,加到所有线性层收益不一定线性增长,反而拖慢推理。
检索增强是另一条路:不动模型参数,把外部知识做成索引,推理时先检索再拼进上下文。它的知识点在切块策略、召回质量、上下文长度预算。我一般把检索增强和微调看成互补关系—— 知识类问题走检索,行为类问题走微调 。你要模型知道最新的产品参数,检索;你要模型学会固定的输出格式和判断标准,微调。搞混了就会陷入"用微调记知识"和"用检索改行为"的双重低效。
5.3 数据构造:标注规范比标注数量更重要
数据这块我的观点很硬: 标注规范的质量决定模型上限,标注数量只决定你多快接近上限 。规范里必须写清楚三件事:边界怎么定(比如抽取"电池续航"这个部件,是抽"电池"还是"电池续航")、歧义怎么处理(同一段文本命中多个类别怎么办)、负样本怎么定义(不命中任何类别时怎么标)。这三件事不写清楚,五个标注员能标出五套标准,模型学出来就是一个四不像。
样本配比也有讲究。类别不均衡时,直接上重采样容易让模型对少数类过拟合,我更倾向于 损失加权加阈值调优 的组合:训练时给少数类更高权重,推理时对每个类别单独调阈值,用验证集上的 F1 最优来确定。阈值调优这一步经常被跳过,但对多标签任务来说,它带来的提升往往比换个模型架构更大。
负样本的构造是另一个知识点。做匹配或检索类任务时,随机负样本太容易区分,模型学不到细粒度差异;要用 难负样本 ,即字面相似但语义不同的样本,可以从召回的头部结果里挑。难负样本的比例不宜过高,太多会让训练不稳定,我一般控制在一半以内,并且随着训练进程逐步增加难度。
6. 评估与调优:指标背后的陷阱
6.1 分类、标注、生成三类任务的指标用法
指标这块最容易被误用。分类任务上,准确率在类别均衡时直观,但在不均衡时具有极强的欺骗性——百分之九十五的样本是负类,全预测负类就有九十五的准确率。所以我基本不看准确率,看 精确率、召回率和它们的调和平均 ,并且要区分宏平均和微平均。宏平均是每个类别算完再平均,少数类的表现会被放大;微平均是所有样本汇总后算,多数类主导。多标签场景下我习惯两个都报,宏平均看少数类有没有被放弃,微平均看整体水平。
序列标注任务上,指标必须 按实体级别算而不是按标签位算 。按位算的话,边界错一个字和整个实体错,惩罚差不多,但实际上前者只错了一点,后者全错。实体级评估要求起止和类型完全匹配才算对,更贴近实际使用。这里还有个坑:如果标签体系里有嵌套实体,普通的实体级评估会漏掉一部分,需要单独设计评估方式。
生成任务上,基于重叠度的指标(比如 n 元组匹配、最长公共子序列)只能反映字面相似,对语义等价但表述不同的输出惩罚过重。我的做法是 自动指标加人工抽检双轨 :自动指标用来做版本间的相对比较,人工抽检用来判断绝对质量。抽检样本量不用大,每个版本抽五十到一百条,按"完全正确、部分正确、错误"三档打分,就能看出趋势。
6.2 数据泄漏:指标虚高的头号元凶
我排查过的"模型效果突然变好"的案例里,超过一半是数据泄漏。常见的泄漏源有这么几个: 样本重复 ,同一条文本在不同时间被采了两次,一次进了训练一次进了验证; 近似重复 ,模板化文本只改了个数字,模型记住了模板; 时间穿越 ,用未来的数据预测过去; 预处理穿越 ,在全量数据上拟合了向量化器或做了目标编码; 标签泄漏 ,某个特征其实是标注结果的衍生。
排查方法很土但有效:算训练集和验证集的 文本相似度分布 ,取最高的一批人工看;对每个特征做单特征预测力检查,某个特征单独用就能到很高的指标,基本可以判定是泄漏;做分组切分,把同一用户、同一会话、同一模板的样本放在同一侧。我现在的默认操作是,切分完之后先跑一个"仅用最长公共子串"的基线,如果这个基线指标异常高,说明数据里有大量近似重复,得先做去重。
注意:去重不能只用完全匹配。文本归一化之后做哈希去重能挡掉一部分,再用字符级 n 元组的相似度做近重复检测,阈值设在零点八到零点九之间,具体按领域调。
6.3 常见问题排查速查表
下面这张表是我自己贴在显示器旁边的,遇到问题先按表扫一遍,能解决八成情况。
| 现象 | 最可能的原因 | 优先排查动作 |
|---|---|---|
| 训练损失不下降 | 学习率过大或过小、标签错位、掩码全零 | 用极小批量过拟合十条样本,看能否到接近零损失 |
| 损失下降但验证指标不涨 | 过拟合、评估集泄漏、指标算错 | 检查评估集与训练集重叠,手算一遍指标 |
| 指标虚高但线上一塌糊涂 | 训练与线上分布不一致、预处理不一致 | 对比两边预处理函数的输出,抽样比对 |
| 显存溢出 | 序列过长、批次过大、梯度累积配置错 | 打印实际序列长度分布,改用动态填充 |
| 推理延迟高 | 束宽过大、未做批处理、无缓存 | 压束宽,开批处理,检查是否有重复计算 |
| 中文输出乱码 | 编码不一致、分词器与模型不匹配 | 从头到尾统一编码,核对词表版本 |
| 生成内容重复 | 解码策略问题、训练数据本身重复 | 加重复惩罚,检查训练语料重复率 |
| 多标签阈值全用零点五 | 未做阈值调优 | 每个类别在验证集上单独搜阈值 |
我特别想强调表里第一行那个"过拟合十条样本"的方法。这是我调试时的第一动作,成本极低但信息量极大:如果十条样本都过拟合不到接近零的损失,说明问题出在模型结构、标签对齐或者学习率上,跟数据量、正则化完全无关,别再去调那些参数浪费时间。这个习惯帮我省下的时间,比我学过的任何一个技巧都多。
7. 工程落地:从能跑到能用的距离
7.1 推理性能:量化、批处理与缓存
模型在本地跑通和上线服务之间,隔着一整个性能工程。知识点按收益排序: 批处理 收益最大,把单条推理改成批量,吞吐能提升几倍到十几倍,代价是延迟略微上升,需要根据业务场景在吞吐和延迟之间找平衡点; 缓存 是最容易被忽略的,相同或高度相似的输入直接返回缓存结果,实际业务里重复请求的比例经常超出预期; 量化 把权重从高精度压到低精度,显存占用和带宽压力都能降下来,常见的是八位整数量化,精度损失通常在可接受范围内,但要记得用你的领域数据做校准,别用通用校准集。
分块和流式处理也是必备知识点。长文本输入要么分块后分别推理再聚合,要么用滑动窗口带重叠,重叠比例一般取窗口的百分之十到二十,太小会切断跨块的语义,太大则重复计算浪费算力。聚合策略要根据任务定:分类任务可以取各块概率的最大值或平均值,抽取任务要处理跨块实体的合并。
还有个容易漏的点: 预热 。服务刚启动时第一批请求的延迟往往特别高,因为要做图优化、内存分配、缓存填充。上线前跑一轮预热请求,把常见长度的输入都过一遍,能避免冷启动时的超时告警。
7.2 版本管理、回归测试与线上监控
工程层最容易被欠债的是版本管理。一个可用的最小方案是: 数据版本、模型权重版本、预处理代码版本三者绑定 ,任何一个变了就发一个新版本号,并且在配置文件里显式记录。我见过最痛苦的情况是,线上效果回退,但没人说得清当时用的是哪份预处理代码,最后只能全量重跑一遍。绑定版本这件事,花半天做一次,后面能省无数个通宵。
回归测试要建成固定流程。做法是维护一个 黄金测试集 ,几百条覆盖各类边界情况的样本,每次改动后跑一遍,指标下降超过阈值就拦下来。这个集合要包含:典型正例、典型负例、歧义样本、超长样本、异常输入(空串、纯符号、超长重复)。阈值我一般设在零点五个点,太严会被噪声频繁打断,太松则起不到保护作用。
线上监控除了常规的延迟、错误率、资源占用,我强烈建议加两类 数据侧监控 :输入长度分布和输入内容分布。模型效果下滑往往是数据先变了,比如上游改了个字段格式,或者渠道变了导致文本风格变化。监控输入分布能在效果指标反应过来之前就发出预警。另外要做 抽样人工复核 ,每天固定抽一小批线上请求人工看结果,这个动作看着原始,但它能发现所有自动指标发现不了的问题。
8. 我个人在这些知识点上踩出来的几条经验
最后说几条不太像知识点、但我觉得比知识点更值钱的经验。 第一,永远先建立基线再谈优化 。规则、TF-IDF 加线性模型、现成的小模型微调,这三层基线依次建立,每一层都记录指标。后面任何复杂方案,只跟当前最好的基线比。没有基线的情况下,你根本不知道一个新方案是真好还是运气好。我吃过一次亏,折腾了半个月的结构改进,最后发现还不如把清洗规则修一修。
第二,把"不知道"显式记录下来 。做项目时我会维护一个疑点清单:这条数据为什么标成这样、这个参数为什么取这个值、这个指标波动是不是噪声。清单不用长,但必须写。很多问题的答案会在两周后的某次实验里突然出现,如果你没记,那一刻你根本不会意识到自己找到了答案。
第三,评估集的神圣性要守住 。评估集只能用来评估,不能用来调参,调参用验证集。这个纪律说着简单,实际上人在指标不涨的时候特别容易破戒——"就看一眼评估集,不调"——然后就再也回不去了。我的做法是把评估集的调用写成一个需要显式确认的脚本,增加一点摩擦,让破戒变得麻烦。
第四,小数据量的项目不要迷信大结构 。两万条数据去微调一个很深的模型,大概率不如一个中等规模模型加精心设计的特征和数据增强。参数量和数据量要匹配,这个直觉需要在多次对比实验里才能建立起来,但一旦建立,你做技术选型的速度会快很多。
第五,能算的先算,别急着跑实验 。显存够不够、序列长度能开多大、批大小取多少、低秩适配的参数量占比多少,这些都能在纸面上算出来。算清楚再跑,能避免大量"跑到一半炸了"的浪费。我现在的习惯是,每个实验开跑前先写三行:预期指标、资源预算、验证方式。三行写不出来,说明这个实验还没想清楚,不该跑。
更多推荐
所有评论(0)