核心摘要
自然语言处理把语言转换成软件可以使用的标签、实体范围、检索结果、译文或回答。可靠系统先定义任务和评测数据,再选择模型。分词不等于切词,基础 BERT 不等于现成的实体识别器,表达流畅也不代表事实正确。
自然语言处理究竟输出什么
自然语言处理研究如何用计算方法分析和生成人类语言。自然语言理解(NLU)强调含义判断,自然语言生成(NLG)强调产生文本;它们是任务视角,并非互斥的软件架构。
例如,用户说“退款已经批准,但银行卡还没收到”。客服系统可能先判断这是退款进度咨询,再从对话中提取交易标识,检索到账规则,最后起草答复。这几步的正确答案、评测单位和错误代价各不相同。
| 任务 | 输入与输出 | 必须提前确定的约定 |
|---|---|---|
| 文本分类、意图识别 | 消息 → 一个或多个标签 | 标签范围、歧义处理、超出范围时怎么办 |
| 情感分析 | 评论 → 整体或分方面的情感 | 谁对什么表达情绪,是否允许中性与混合 |
| 词性标注 | 上下文中的词元 → 语法标签 | 切分边界与词性标签集 |
| 命名实体识别(NER) | 文本 → 带类型的实体范围 | 范围单位、实体分类、是否允许嵌套 |
| 语义搜索 | 查询 → 排序后的文档标识 | 相关性、权限范围、返回数量 |
| 机器翻译 | 原文 → 目标语言文本 | 语言方向、术语、人名、数字及原意保留 |
| 摘要与问答 | 来源或问题 → 文本回答 | 必须包含的事实、证据支持、遗漏与拒答条件 |
NER 找出实体提及;把“苹果”对应到某家公司 ID 是实体链接,判断“它”指代该公司是指代消解,提取两实体之间的关系则是关系抽取。把它们拆开定义,才能知道错误发生在哪一步。
语音识别也可以为 NLP 提供文本,但转写错误仍属于应用链路的一部分。如果语音中的金额已经转错,下游抽取得再准确,也只是准确地提取了错误内容。
先定义任务,再选择模型
任务约定需要说明系统返回什么,以及评审者如何判断对错。“理解客户反馈”无法直接验收;“按指定版本的标签集,为每条工单输出一个路由标签”则可以。
以客服路由为例,先定义 billing(账单)、refund(退款)和 shipping(物流),为每类准备正例与反例。遇到“快递还没到,我想退款”,可以按用户下一步诉求路由,也可以允许多标签或转人工。不同选择会产生不同的数据集和指标,不能训练后再临时解释。
标注规范应覆盖否定、引用别人的话、混合意图、上下文不足和新话题。训练前先检查标注分歧:相同含义却给出矛盾标签,模型很难学到稳定边界。真正存在歧义的样本应保留歧义记录,不能全部强行填上一个“标准答案”。
把标签版本、数据版本、预处理、模型、tokenizer 和决策阈值一起管理。正负两类情感任务与后来加入“中性、混合”的任务不是同一个问题,两次分数不能直接比较。
中文分词、模型 Token 与字符偏移量
Tokenization 是把文本转换为某个处理流程使用的单位。Tokenizer可能产生词、子词、字符或基于字节的单位。具体 Token ID 取决于模型词表和预处理规则,按空格切分不能替代模型 tokenizer。
中文分词关心语言上的词边界;子词切分关心模型词表中的表示单位。预训练模型应接收自身兼容的 tokenizer 输出。除非训练流程明确要求,否则不要先用另一套中文分词器插入空格,再假定模型表现会更好。
实体偏移量必须写清单位
实体范围需要定位到某一版本的输入文本。可以约定采用左闭右开的 [start, end),但还必须说明偏移量的计数单位。
| 单位 | 含义 | 退款🙂 的长度 |
|---|---|---|
| UTF-8 字节 | 存储位置,也是 Go 字符串索引单位 | 10 字节 |
| Unicode 码点 | 对有效 UTF-8,可由 Go rune 表示 | 3 个码点 |
| UTF-16 代码单元 | JavaScript 字符串常用的索引单位 | 4 个代码单元 |
| 字素簇 | 近似用户感知的字符单元 | 本例为 3 个字素簇 |
该表情占四个 UTF-8 字节、一个码点、两个 UTF-16 代码单元。如果后端返回字节偏移,前端直接当 JavaScript 索引高亮实体,位置就可能错位。
Unicode UAX #29分别定义字素、词和句子边界,并允许按需求定制。例如,字母 e 后接组合重音符号,包含两个码点,但通常组成一个字素簇。因此,统计 rune 数并没有实现字素分割。
规范化、HTML 解码、空白清理、脱敏都会影响位置。保留原文并维护映射,或者明确声明范围对应的是处理后的文本。训练数据与评测结果不能只有一边做规范化。
预处理不是越多越好
小写化、停用词删除对某些词项基线有帮助,却不是通用前置步骤。删除“不”会改变情感,删除标点可能破坏单据编号,小写化也可能丢掉实体线索。预训练模型通常应采用与训练设置一致的预处理。
序列标注还需要把子词位置映射回标注范围,并约定一个词拆成多个子词后如何分配标签。大量 Token 都是“非实体”时,Token 准确率很高也可能掩盖整个实体边界识别失败。
用同一组问题比较模型
规则、统计模型和神经模型都仍有适用场景。技术演进不能被简化成“越新的架构一定越好”。
| 方法 | 适合建立基线的条件 | 主要代价 |
|---|---|---|
| 规则与词典 | 格式稳定、业务模式明确 | 改写覆盖不足,例外容易积累 |
| TF-IDF 加线性分类器 | 标签稳定,有可用标注样本 | 词项信号强,复杂上下文理解有限 |
| 预训练编码器加任务头 | 分类或实体范围任务,有领域标签 | 需要微调、位置对齐和部署 |
| 编码器—解码器模型 | 翻译、摘要、文本到文本任务 | 必须检查含义保留与生成错误 |
| 指令式解码器模型 | 指令灵活,少样本演示,抽取与生成混合 | 需要控制输出格式、提示敏感性、延迟和成本 |
TF-IDF 根据词项在文档中的频率及其在文档集合中的稀有程度赋权。它能帮助你判断任务有多少信息可以直接从词项信号中解决。词表等需要学习的预处理只能在训练集上拟合;把测试文档也纳入统计会泄漏信息。
BERT、GPT 与 T5 的边界
BERT 原始论文研究双向表示预训练及下游任务适配。bert-base-chinese 是基础编码器,不能据此声称已经得到人名、地名、组织识别器。可用的 NER 模型还需要匹配的任务头、标签映射、tokenizer,以及在目标领域上的效果证据。
GPT 类解码器通过自回归方式,根据已有上下文预测后续 Token。它也可以生成一个标签或结构化对象来完成分类。把它限定为“只能生成”混淆了模型机制和任务能力;另一方面,生成一段流畅解释并不能证明分类正确。
T5 原始论文研究统一的文本输入、文本输出接口,覆盖多种任务。统一接口便于表达任务,但不会消除各任务独有的数据、校验和评测要求。
需要进一步理解内部架构,可阅读 Transformer 架构解析。如果问答需要外部知识与出处,RAG 入门解释了额外的检索和证据处理环节。
数据切分与评测要对应真实使用方式
评测需要估计系统处理未来独立输入的表现。如果同一会话、客户、文档模板或近重复样本横跨训练和测试两侧,随机按消息切分可能高估效果。
分组规则取决于你要回答的问题。测试新会话,就把整段会话放在同一分区;测试未来流量,可以使用时间留出,并说明哪些对象会合理地重复出现。按客户隔离适合测试未见过的客户,但它比“服务已有客户的未来消息”更严格,两者不是同一个评测目标。
训练集拟合参数,验证集选择提示、阈值和模型,测试集用于最终比较。如果反复查看测试错误并据此修改设计,这些样本已经成为开发数据,需要另外保留独立留出集。
分类指标如何理解
针对某一个类别,TP 是正确预测为该类的数量,FP 是误报为该类的数量,FN 是该类被漏掉的数量。应先检查原始混淆矩阵,再按不同分母解释精确率与召回率;F1 分数汇总一个阈值下的两项指标,ROC-AUC衡量跨阈值的分数排序,马修斯相关系数则提供使用二分类全部四个单元格的对称摘要。它们回答的问题不同,不能当作可互换的排行榜分数。
精确率 Precision = TP / (TP + FP)
召回率 Recall = TP / (TP + FN)
F1 = 2TP / (2TP + FP + FN)
宏平均 F1 = 约定标签集中各类别 F1 的算术平均
准确率 Accuracy = 正确预测数 / 全部样本数
scikit-learn F1 文档明确区分平均方法和未定义分数的处理。宏平均给每类相同权重,按支持数加权的 F1 则按真实样本数量赋权。必须固定标签集和分母为零时的策略,避免某个缺失类别悄悄从平均值中消失。
分类准确率让每次硬标签完全命中贡献一票,平衡准确率则对已声明类别的 Recall 做等权平均。特异度单独衡量真实负例有多少被正确排除。这些视角相互补充,任何一项都不能替代原始计数和错误成本。
假设测试集有 95 条普通消息和 5 条紧急投诉,全部预测为普通消息就能得到 95% 准确率,但紧急投诉召回率为零。这是构造数据的算术示例,不是任何模型的实测成绩。
F1 也不直接表达业务成本。根据业务选择需要报告的误报率或工作阈值下的精确率,同时给出召回率和样本数量。如果允许拒答,应同时报告覆盖率与已接受样本的质量;不能删掉所有难例后,把剩余分数当成无条件提升。
对输出连续分数的分类器,精确率-召回率曲线展示正向告警可靠性与覆盖率如何随阈值变化。当分数被解释为概率时,对数损失会评价分配给每个真实标签的概率,它回答的问题不同于硬标签 Accuracy、F1 或排序面积。
其他任务不能照搬分类准确率
| 任务 | 应检查什么 | 容易误导的简化 |
|---|---|---|
| NER | 实体边界和类型同时匹配的精确率、召回率、F1;按实体类型拆分 | 用大量非实体 Token 拉高准确率 |
| 搜索 | 指定截断位置的召回、排序质量、权限正确性 | 把余弦相似度当成相关性真值 |
| 翻译 | 人工含义评估、术语、人名与数字保留 | 只要读起来通顺就算正确 |
| 摘要 | 主张有依据、关键事实覆盖、重要遗漏 | 把词语重合度当作事实保证 |
| 情感分析 | 按类别、方面、否定和讽刺等切片分析 | 默认英文模型直接处理任意语言 |
不确定性估计也应与采样单位一致,例如按会话重采样,而不是把同一会话中高度相关的消息当作独立样本。一个极小切片即使满分,证据仍然有限。
用 Go 运行固定标签分类评测
下面的 Go 1.23 示例评估六条人为构造的预测,不调用模型,也不训练分类器。它把标签约定、混淆矩阵和平均策略变成可检查的代码,供接入真实预测前核对口径。
每条记录必须只有一个真实标签和一个预测标签。未知标签、重复标签定义、空数据集都会返回错误。某类完全缺失时,精确率、召回率和 F1 均记零,且仍纳入宏平均。线上报告还需单独记录无效输出率与拒答率,不能把这些请求丢弃。
保存为 main.go,执行 go run main.go。
package main
import (
"fmt"
"log"
"strings"
"unicode/utf16"
"unicode/utf8"
)
type Example struct {
Gold, Predicted string
}
type ClassScore struct {
Label string
Support int
Precision, Recall, F1 float64
}
type Report struct {
Matrix [][]int
Classes []ClassScore
Accuracy, MacroF1 float64
}
func ratio(n, d int) float64 {
if d == 0 {
return 0
}
return float64(n) / float64(d)
}
func evaluate(labels []string, rows []Example) (Report, error) {
var result Report
if len(labels) == 0 || len(rows) == 0 {
return result, fmt.Errorf("labels and examples must be nonempty")
}
index := make(map[string]int, len(labels))
for i, label := range labels {
if strings.TrimSpace(label) == "" {
return Report{}, fmt.Errorf("empty label")
}
if _, exists := index[label]; exists {
return Report{}, fmt.Errorf("duplicate label %q", label)
}
index[label] = i
result.Matrix = append(result.Matrix, make([]int, len(labels)))
}
correct := 0
for i, row := range rows {
gold, goldOK := index[row.Gold]
predicted, predictedOK := index[row.Predicted]
if !goldOK || !predictedOK {
return Report{}, fmt.Errorf("unknown label at row %d", i)
}
result.Matrix[gold][predicted]++
if gold == predicted {
correct++
}
}
for i, label := range labels {
tp, support, predicted := result.Matrix[i][i], 0, 0
for j := range labels {
support += result.Matrix[i][j]
predicted += result.Matrix[j][i]
}
score := ClassScore{
Label: label, Support: support,
Precision: ratio(tp, predicted),
Recall: ratio(tp, support),
F1: ratio(2*tp, support+predicted),
}
result.Classes = append(result.Classes, score)
result.MacroF1 += score.F1
}
result.MacroF1 /= float64(len(labels))
result.Accuracy = ratio(correct, len(rows))
return result, nil
}
func main() {
labels := []string{"billing", "refund", "shipping"}
rows := []Example{
{"billing", "billing"}, {"billing", "billing"},
{"billing", "refund"}, {"refund", "refund"},
{"refund", "billing"}, {"shipping", "billing"},
}
report, err := evaluate(labels, rows)
if err != nil {
log.Fatal(err)
}
fmt.Printf("accuracy=%.4f macro_f1=%.4f\n", report.Accuracy, report.MacroF1)
fmt.Printf("rows=gold columns=predicted labels=%v matrix=%v\n", labels, report.Matrix)
for _, score := range report.Classes {
fmt.Printf("%s support=%d precision=%.4f recall=%.4f f1=%.4f\n",
score.Label, score.Support, score.Precision, score.Recall, score.F1)
}
text := "退款🙂"
fmt.Printf("utf8_bytes=%d code_points=%d utf16_units=%d\n",
len(text), utf8.RuneCountInString(text), len(utf16.Encode([]rune(text))))
}
输出:
accuracy=0.5000 macro_f1=0.3571
rows=gold columns=predicted labels=[billing refund shipping] matrix=[[2 1 0] [1 1 0] [1 0 0]]
billing support=3 precision=0.5000 recall=0.6667 f1=0.5714
refund support=2 precision=0.5000 recall=0.5000 f1=0.5000
shipping support=1 precision=0.0000 recall=0.0000 f1=0.0000
utf8_bytes=10 code_points=3 utf16_units=4
矩阵暴露了具体问题:物流请求一条也没有识别出来。最后一行仅展示有效 UTF-8 文本的不同计数单位,没有实现偏移转换或字素分割。接入真实预测前,还应按稳定样本 ID 与标注一一关联并检查覆盖;两个数组长度相同,不代表行已经正确对齐。
上线的是完整流程
部署范围包括预处理、推理、校验及后续业务决策,延迟和错误也应按完整链路统计。
固定标签任务应拒绝 schema 外的输出;抽取任务应检查范围合法,并验证抽取文本等于来源中的对应片段;生成任务需要检查必需事实与证据支持。JSON 合法只说明结构可解析。
模型置信分数不等于校准后的概率。分数 0.9 不能自动解释为“90% 会正确”。根据验证集上的实际错误成本选择阈值,再检查不同语言、领域下的校准是否变化。
监测语言、文本长度、截断、类别分布、无效输出、拒答、延迟和人工复核后的业务结果。能够用脱敏失败记录及受控评测集定位问题时,就不应在普通日志中保存完整敏感文本。每个服务语言都要单独看,整体均值可能掩盖小语种失败。
实际发布时,可以先让候选模型在同一批流量上旁路运行,不改变用户可见决策。保留整套流程的版本化回退能力;只退模型却保留新 tokenizer 或标签映射,可能制造新的不兼容。
常见问题
NLP、NLU 和 NLG 有什么区别?
NLP 是处理人类语言的总称;NLU 侧重含义理解,例如意图分类;NLG 侧重语言生成,例如摘要。同一个应用可以同时包含两类任务,这些名称不规定必须使用哪一种模型架构。
中文 NLP 一定要先做中文分词吗?
不一定。基于词的流程需要分词,但预训练模型应使用与自身匹配的 tokenizer,其单位可能是字、子词或基于字节的 Token。词边界、模型 Token 和字符偏移量是三份不同的约定。
bert-base-chinese 能直接用于命名实体识别吗?
基础编码器输出的是表示,不是现成的人名、地名和组织标签。可靠的 NER 需要经过任务训练的输出头或其他训练好的抽取方法,并匹配 tokenizer、标签定义和目标语言、领域的评测数据。
情感分析准确率多少才算可用?
没有通用阈值。先确定领域、语言、标签体系、类别比例和独立测试样本,再与基线比较,报告各类精确率和召回率,并衡量误报和漏报的业务代价。
什么时候用小分类器,什么时候用大模型?
标签稳定、代表性数据充足、延迟或部署限制严格时,小分类器值得优先比较。任务指令经常变化或需要生成文本时,大模型更有吸引力。两者应使用同一独立测试集,同时比较错误、无效输出、延迟和总运行成本。
参考资料与延伸阅读
- Unicode UAX #29:字素、词与句子边界。
- BERT 原始论文:双向表示预训练与下游适配。
- T5 原始论文:文本到文本的迁移学习框架。
- scikit-learn F1 文档:平均方式、标签集合与未定义分数。
- 超越 ROUGE 和 BLEU 的生成评测:如何评估表面词语重合之外的质量。