
简介面向语音识别结果标点恢复任务的PaddlePaddle模型资源包适合从事ASR后处理、文本可读性优化的开发者或算法工程师使用。包内共5个文件包括model.pdmodel与model.pdiparams组成的推理模型参数文件、info.json配置、vocab.txt词典以及模型信息文件压缩包整体约250.85MB。资源提供可直接加载的标点预测模型能够为ASR输出的无标点文本自动补全句读、逗号、问号等符号省去自行训练的时间成本也可基于PaddlePaddle加载后继续微调适配特定场景。目前已有4930人学习下载是快速验证或集成标点恢复能力的实用参考。 做语音识别相关项目的人应该都碰过同一个问题ASR 输出的文本是一大段没有标点的纯文字流读起来像在念紧箍咒。拿 Whisper 直接生成英文或中文内容标点还能凑合看但一旦切到自研声学模型加语言模型的路线或者做流式识别、电话信道识别输出文本就经常是整段没标点的状态。产品那边当时丢来一句话给语音识别结果加上标点符号模型做一个小工具能把 ASR 的一句话变成正常人话。仔细拆了一下这活儿其实是 NLP 里一个经典任务——标点恢复Punctuation Restoration本质上是要根据上下文预测每一个位置该出现哪些标点也就是一个序列标注模型的工作。这篇文章把我从数据准备到训练部署的完整经验记下来给同样被 ASR 纯文本困扰的人做个参考。1. 先搞清楚任务语音识别输出为什么没有标点1.1 ASR 丢掉标点不是 bug而是训练目标没管它很多做应用的人会误以为ASR 把标点丢了好像本该有但被遗漏了。实际上绝大多数语音识别模型在训练目标里就没有把标点当成一个必须输出的信息。传统混合系统里声学模型负责把帧变成音素或者状态语言模型负责把音素变成词整条链路优化的都是字词序列的概率。标点符号既没有稳定的声学对应也不在词典的必选路径里模型自然倾向于不输出。端到端模型更明显CTC 也好RNN-T 也好训练数据如果被剥掉标点模型就只能学出一个无标点的映射关系。哪怕训练数据里带标点模型也要权衡多输出一个字符是否值得因为标点没有对应音频能量很容易被当成噪声忽略。这里还要注意一个现象同样是端到端方案Whisper 这类生成式模型因为训练语料是网页级文本天然包含大量规范标点所以它能顺带输出逗号和句号。但这是语言模型层面的统计能力不是声学特征驱动的一旦句子很长或者遇到口语化表达它会一本正经地给出语义不匹配的标点。换句话说语音识别结果缺标点不是换一个 ASR 引擎就能彻底解决的得有一个专门模块来补位。1.2 标点恢复是序列标注问题不是文本生成问题第一次接下这个需求时我第一反应是能不能用文本生成模型直接重写一遍文本把标点补上。比如用 T5、ChatGPT 之类的模型输入无标点文本输出带标点文本。听起来合理实际做起来会遇到两个问题第一生成模型会改写原文轻则调整语序重则丢字、改词、补充连接词这对语音识别结果来说是绝对不能接受的因为用户需要的是原样文本加标点不是一份润色后的转写稿第二生成式推理延迟高尤其是在流式场景里一句话识别完再等几秒钟生成标点产品根本没法用。所以更合适的方式是把标点恢复当成 token 级分类任务给一段文本的每个 token字或者词打一个标签预测它后面应该接什么标点。模型结构可以直接用 BERT 这类 Transformer 模型的序列标注头输出层每个位置预测一个标点类别。这样速度比生成模型快一个数量级而且输出严格受标签集合约束不会出现模型把我不确定去不去改成我不确定是否去的问题。2. 方案选型规则、序列标注与生成式模型怎么选2.1 规则方案为什么撑不住标点恢复最早期的做法是拍脑袋写规则比如检测吗、呢、吧后面加问号因为、所以、但是前面加逗号。这类方案在简单句子上能跑通甚至在演示 Demo 里足够惊艳但一上真实语料就露馅长句的从句边界、并列结构、插入语、引用关系靠关键词完全判断不了。而且规则之间会互相打架。我在初版方案里加过一条句首大写词前加句号的规则结果遇上一堆人名缩写和专有名词整段的断句都乱了。最后维护成本变得很高每发现一种新句式就要在规则堆里补逻辑最终变成一个没人敢动的屎山。规则可以做但只能做安全网不能做主力模型。主力模型必须能从大量数据里自动学习上下文模式。2.2 Transformer 模型做 token 分类是主流标点恢复相关的公开研究里最稳定的方案就是把预训练语言模型BERT、RoBERTa、ELECTRA 等当作编码器在最顶层接一个线性分类层输出每个位置的标点标签。这类模型天然适合捕捉上下文信息比如逗号和句号的边界往往取决于前后十几个词的语法关系而这些信息恰好被 Transformer 的注意力机制编码进去了。选型上中文场景我用的是 bert-base-chinese它按字切分处理中文标点和分词都比较稳英文场景用 bert-base-uncased。如果追求更高精度可以参考中文 MacBERT、英文 RoBERTa-large但随之而来的是推理时间暴增。对大多数业务落地场景来说base 级别的模型加一套靠谱的数据清洗流程已经能把句级标点预测的 F1 做到 0.85 以上需求完全够用。要注意的是 BERT 输入长度上限是 512 token长文本必须切段这一段后面会专门讲。2.3 蒸馏和融合资源紧张时怎么救场如果线上环境对延迟特别敏感一个思路是先训一个大模型做 teacher再用 6 层小 Transformer 做 student 做模型蒸馏蒸馏后的模型体积能压缩一半以上推理速度翻一倍F1 只掉两三个点。另一条路是模型融合把 BERT 的预测结果和 CRF 约束层拼在一起让模型输出自动考律相邻标签的转移关系减少句号后面紧跟逗号这类明显不合理的标注序列。实际工程里我经常是小模型 CRF 少量规则三件套既保住速度又保证标点序列在整体结构上不会太离谱。3. 核心细节数据准备、标签体系与对齐逻辑3.1 语料从哪里来怎么清洗训练一个标点恢复模型关键不在模型结构多花哨而在数据。我需要的是同一句话的两种形态无标点版本作为输入有标点版本作为标签。最方便的做法是从中文新闻语料、维基百科、开源小说集里抽取带标点的句子然后把标点全部移除作为模型的输入。数据清洗这条要做得足够狠。我第一版模型训练时偷懒没有过滤英文混排和 URL结果模型在数字后面特别爱加逗号在英文缩写后面频繁加句号。后来我把清洗流程固定成四步去掉 HTML 标签和 URL统一全角半角过滤掉包含异常字符和过长超过 200 字的句子移除问答平台那种带 emoji 和特殊符号的句子。这样一套下来十万句里大概会砍掉三成但留下的数据干净很多模型收敛速度和最终指标都明显变好。3.2 标签集合怎么定标点集合不需要贪多。很多语音识别业务实际上只需要四种标点句号、逗号、问号、感叹号。顿号、分号、冒号在口语转写里极少出现而且预测错了比不预测更影响阅读所以往往不出现在标签集合里。英文场景可以加一个撇号的处理但一般也是放到后处理规则里不单独建模。我做标签时对每个 token 标注它后面的标点类型用一个 O 表示不需要加标点其余标签为 COMMA、PERIOD、QUESTION、EXCLAMATION。这里有个很微妙的地方标点符号是占一个字符位置的所以严格来说模型输出的是在这个 token 之后插入某个标点而不是用标点替换 token。很多第一次做的人会在这个细节上栽跟头后面代码部分会展示正确做法。3.3 对齐是最大的暗坑BERT 的分词器会把一个词切成多个 subword中文虽然大多一个字一个 token但英文、数字、特殊符号会被拆得七零八落。如果直接把原始文本里的标点位置映射到 token 位置几乎必然错位。我推荐用return_offsets_mappingTrue拿回每个 token 在原始文本里的字符偏移然后根据偏移量判断某个 token 的end位置是否是标点。这样不管是中英文混排、数字、还是奇怪的 Unicode 字符都能精确对齐。注意不要对已经去掉标点的输入文本做二次清洗那会破坏偏移量对应关系。4. 实操用 BERT 微调一个标点恢复模型4.1 训练流程与核心代码我直接用了 HuggingFace Transformers 库来做。核心思路是先把带标点的文本用 tokenizer 编码拿到 input_ids 和 offsets再根据 offsets 给每个 token 打 tag然后喂给 AutoModelForTokenClassification 训练。代码大致长这样from transformers import AutoTokenizer, AutoModelForTokenClassification, Trainer, TrainingArguments import torch tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) label2id {O: 0, COMMA: 1, PERIOD: 2, QUESTION: 3, EXCLAMATION: 4} id2label {v: k for k, v in label2id.items()} def encode_with_labels(text): # text 是带标点的原始句子 encoding tokenizer( text, truncationTrue, max_length256, return_offsets_mappingTrue, return_tensorsNone ) labels [] offset_mapping encoding[offset_mapping] # 逐个 token 找到它后面紧跟的标点 for idx, (start, end) in enumerate(offset_mapping): if start 0 and end 0: # 特殊 token 如 [CLS] [SEP] 直接标记为 -100忽略损失 labels.append(-100) continue # 看原始文本中 end 位置是否是标点 punct if end len(text): punct text[end] labels.append(label2id.get(PUNCT_MAP.get(punct, O), 0)) encoding[labels] labels return encoding这里PUNCT_MAP是一个从标点字符到标签名的映射比如{: COMMA, 。: PERIOD, : QUESTION, : EXCLAMATION}。有一点要特别提醒我只看紧随当前 token 后面的字符是不是标点所以必须确认 offset_mapping 里每个 token 的 end 正好是它在原文中的结束位置不能提前做过空白压缩。数据构造完成后直接用 Trainer 训练评估指标用 准确率加各类标点的 F1。损失函数用交叉熵即可但要给 ask 类标签加权重因为问号在语料里占比很低不加权重很容易被模型全部预测成 O。4.2 训练超参与评估指标标点恢复这种细粒度任务学习率不能太大。我用的是 2e-5 到 3e-5 之间batch size 32warmup 比例 0.1训练 3 到 5 个 epoch 就足够。再往后训练loss 虽然还在降验证集上的宏观 F1 反而会掉典型过拟合。中文数据大概几十万句就能训出不错的效果数据量再少比如只有几万句就建议加载一个已经在通用语料上训过的标点恢复模型做微调。评估时不要只看准确率因为绝大多数 token 后面是不需要标点的模型全预测成 O 也能有很高的准确率。我一般看三类指标句号 F1、逗号 F1、问号 F1以及一个整体宏观 F1。这三个值能很好地反映模型在停顿怎么划分问句有没有识别出来这些关键问题上的表现。4.3 推理阶段把标点插回原文模型预测结果是每个 token 的标签序列推理时要把标点按位置插回无标点文本。伪代码如下def restore_punctuation(text, model, tokenizer): encoding tokenizer(text, return_tensorspt, truncationTrue, max_length256) outputs model(**encoding) logits outputs.logits[0] preds torch.argmax(logits, dim-1).tolist() # 把 token 序列还原成字符序列遇到标点标签就插入符号 result [] for token_id, pred in zip(encoding[input_ids][0], preds): token_str tokenizer.decode([token_id], skip_special_tokensTrue) result.append(token_str) label id2label[pred] if label COMMA: result.append() elif label PERIOD: result.append(。) elif label QUESTION: result.append() elif label EXCLAMATION: result.append() return .join(result)实际工程里我会给每个标点标签加一个概率阈值比如只有句号概率大于 0.6 才真的插入。这样做能显著减少幻觉标点——模型有时候会以很高的置信度在语气词后面加句号但人工阅读是完全不通的。5. 接入 ASR Pipeline流式、长文本与 Whisper 同存5.1 哪些场景需要标点模型哪些直接用 Whisper 输出如果整个语音识别链路用的是 Whisper 语音识别模型并且是离线批量翻译、字幕生成这类场景我建议直接信任 Whisper 自带的标点输出不要再过一层标点恢复模型。原因很简单Whisper 在生成时已经利用语言模型做了全局规划它的标点和文本内容是一起生成的一致性更好后续强行二次标注反而可能把原本正确的标点位置改乱。但如果是自研 ASR、流式识别、电话信道录音或者 Whisper 输出被后端进一步做格式化处理比如去掉所有标点后存入数据库那标点恢复模型就必须上场。它和 ASR 是解耦的ASR 负责把音频变成文本标点模型负责把文本变成可读性内容。两者通过消息队列或者 HTTP 接口连接各改各的互不影响。5.2 长文本切分与在线推理延迟控制BERT 类模型最长只能处理 512 个 token而一段会议录音的转写文本可能有好几千字。我常用的做法是切段优先在句号附近切断让每段长度控制在 350 个 token 以内前后各留 50 个 token 的上下文重叠。推理时只取中间部分的预测结果丢弃边界处 50 个 token 的预测能明显减少边界附近的标点错误。在线场景延迟目标可以控制在单句 50ms 以内。BERT-base 在 CPU 上处理一句话大约 20 到 50msGPU 上更快。如果还要压主流做法是把模型转成 ONNX 格式然后用 onnxruntime 推理或者用 INT8 量化精度损失通常在 1 到 2 个百分点内。我上线时用过一次 6 层小模型加蒸馏效果是延迟压到 10ms 以下虽然句号 F1 掉了 2 个点左右但对实时会议系统来说流畅度优先级更高。6. 常见问题与排查技巧实录6.1 漏标、错位、多标三个高频头疼点漏标是最常见的表现为该断句的地方没有标点。这种多半是训练数据里逗号过多、句号过少造成偏见。我的处理办法是重新统计语料里各标点的分布把语料比例调均衡一点比如逗号和句号的比例控制在 2 比 1 左右。错位是标点位置偏了一个词比如实际是我觉得这个方案不错被标成我觉得这个方案不错。出现这个情况一般是训练语料里流行口语化断句和书面语断句混在一起模型学到了一种折中。排查时可以按文本来源分桶检查把小说和新闻分开训练多个适配模型效果比混合训练更好。多标是模型太激进在不需要停顿的地方硬插逗号。我线上遇到过最夸张的案例模型把一句话切成了十来个碎片。后来我加了两条硬限制标点标签概率低于 0.6 一律不插入同一句话最多插入 5 个标点。效果立竿见影。6.2 问号识别率低怎么办问号在自然语料里占比太小很多语料集不到 5%模型即使全部忽略也能保持很高的整体准确率。针对这个问题我做过几次数据增强把疑问句数据随机复制三份同时把一般疑问句中的语气词保留原样输入模型强制它根据句式特征学习。另一个技巧是推理时稍微降低问号的概率阈值比如从 0.6 调到 0.4代价是误报增加一部分但如果应用场景是会议纪要和客服质检用户更关心疑问句有没有被抓出来误报一点可接受。6.3 中英混排、数字、日期场景这类断句错误往往不是模型能力问题而是标签对齐错误。中英混排时 BERT tokenizer 会把英文切得很碎每个 subword 都预测一个标签插标点时很容易插错位置。我的做法是预处理阶段先把邮箱、URL、数字、纯英文单词替换成特殊占位符比如__NUM__、__ENG__训练和推理时都用占位符推理结束后再映射回原词。这样模型不需要理解具体英文内容也能稳定地判断是否需要标点。注意占位符方案只对标点位置插入有效如果任务还需要保留原词边界就得在替换前先记录好原词位置等预测完再按索引恢复。6.4 常见问题速查表现象可能原因解决方向该断句的地方没有句号语料中句号比例偏低均衡各类标点占比补充正式文本语料标点位置偏位一个词口语和书面语语料混训按来源分桶训练或调整各类数据量比例逗号过多碎句严重模型阈值太低预测太过激进提高插入阈值限制单句最大标点数问号召回率低问号样本少、权重低增加疑问句数据量或降问号阈值英文/数字后乱加标点token 被切碎导致对齐错误用占位符替换数字和英文推理后再恢复边界附近标点错误明显长文本被切段后丢失上下文切割时保留前后 50 token 重叠只取中间预测6.5 踩过几次坑之后这个标点恢复模型前前后后调了两周最深的感受是模型结构只占三成工作量剩下七成都在跟数据较劲。语料越脏模型表现越差而且这种差不是靠调参能救回来的。如果你也想给语音识别结果加上标点符号模型我建议第一个版本不要追求集合多全、结构多新先把逗号、句号、问号三类标点做到 85% 以上精确率再逐步扩展。标点恢复这种序列标注任务稳定可靠比花哨更重要毕竟用户拿到的是一份转写稿不是一篇模型炫技的样例。本文还有配套的精品资源点击获取