核心摘要

自然语言处理把语言转换成软件可以使用的标签、实体范围、检索结果、译文或回答。可靠系统先定义任务和评测数据,再选择模型。分词不等于切词,基础 BERT 不等于现成的实体识别器,表达流畅也不代表事实正确。

自然语言处理究竟输出什么

自然语言处理研究如何用计算方法分析和生成人类语言。自然语言理解(NLU)强调含义判断,自然语言生成(NLG)强调产生文本;它们是任务视角,并非互斥的软件架构。

例如,用户说“退款已经批准,但银行卡还没收到”。客服系统可能先判断这是退款进度咨询,再从对话中提取交易标识,检索到账规则,最后起草答复。这几步的正确答案、评测单位和错误代价各不相同。

任务 输入与输出 必须提前确定的约定
文本分类、意图识别 消息 → 一个或多个标签 标签范围、歧义处理、超出范围时怎么办
情感分析 评论 → 整体或分方面的情感 谁对什么表达情绪,是否允许中性与混合
词性标注 上下文中的词元 → 语法标签 切分边界与词性标签集
命名实体识别(NER) 文本 → 带类型的实体范围 范围单位、实体分类、是否允许嵌套
语义搜索 查询 → 排序后的文档标识 相关性、权限范围、返回数量
机器翻译 原文 → 目标语言文本 语言方向、术语、人名、数字及原意保留
摘要与问答 来源或问题 → 文本回答 必须包含的事实、证据支持、遗漏与拒答条件

NER 找出实体提及;把“苹果”对应到某家公司 ID 是实体链接,判断“它”指代该公司是指代消解,提取两实体之间的关系则是关系抽取。把它们拆开定义,才能知道错误发生在哪一步。

语音识别也可以为 NLP 提供文本,但转写错误仍属于应用链路的一部分。如果语音中的金额已经转错,下游抽取得再准确,也只是准确地提取了错误内容。

先定义任务,再选择模型

任务约定需要说明系统返回什么,以及评审者如何判断对错。“理解客户反馈”无法直接验收;“按指定版本的标签集,为每条工单输出一个路由标签”则可以。

以客服路由为例,先定义 billing(账单)、refund(退款)和 shipping(物流),为每类准备正例与反例。遇到“快递还没到,我想退款”,可以按用户下一步诉求路由,也可以允许多标签或转人工。不同选择会产生不同的数据集和指标,不能训练后再临时解释。

flowchart LR A["任务与标签约定"] --> B["采集与标注数据"] B --> C["隔离训练、验证与测试分组"] C --> D["建立基线"] D --> E["比较模型并分析错误"] E --> F["上线并校验输出"] F --> G["监测漂移与复核失败"] G --> B

标注规范应覆盖否定、引用别人的话、混合意图、上下文不足和新话题。训练前先检查标注分歧:相同含义却给出矛盾标签,模型很难学到稳定边界。真正存在歧义的样本应保留歧义记录,不能全部强行填上一个“标准答案”。

把标签版本、数据版本、预处理、模型、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衡量跨阈值的分数排序,马修斯相关系数则提供使用二分类全部四个单元格的对称摘要。它们回答的问题不同,不能当作可互换的排行榜分数。

text
精确率 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。

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))))
}

输出:

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、标签定义和目标语言、领域的评测数据。

情感分析准确率多少才算可用?

没有通用阈值。先确定领域、语言、标签体系、类别比例和独立测试样本,再与基线比较,报告各类精确率和召回率,并衡量误报和漏报的业务代价。

什么时候用小分类器,什么时候用大模型?

标签稳定、代表性数据充足、延迟或部署限制严格时,小分类器值得优先比较。任务指令经常变化或需要生成文本时,大模型更有吸引力。两者应使用同一独立测试集,同时比较错误、无效输出、延迟和总运行成本。

参考资料与延伸阅读