3、标点符号恢复:基于规则的标点预测、基于模型的标点恢复方法
标点符号恢复,说白了就是给一段光秃秃的文字加上逗号、句号、问号这些。你想想看,语音识别出来的结果,往往是一长串没有标点的文本,读起来特别费劲。我刚开始做这个方向时,觉得这事儿挺简单的,不就是加几个标点嘛。后来才发现,标点放错位置,整个句子的意思可能就完全变了。
举个例子:
输入:我喜欢吃饭妈妈做的饭很好吃
加标点后:我喜欢吃饭,妈妈做的饭很好吃。
加标点后(错误):我喜欢吃饭妈妈,做的饭很好吃。
嗯,这里要注意,标点恢复做不好,下游的语义理解、机器翻译都会跟着出错。我个人习惯把标点恢复方法分成两大类:基于规则的和基于模型的。咱们一个一个来看。
3.1 基于规则的标点预测
基于规则的方法,说白了就是靠人工总结的规律来加标点。我在项目中遇到过一些场景,比如特定领域的语音助手,规则方法反而比模型方法更稳定。
3.1.1 基于停顿和韵律的规则
语音识别通常会输出每个词的开始时间和结束时间,以及置信度。我们可以利用这些信息来做标点预测。
核心思路是这样的:
- 长停顿(比如超过300ms)→ 句号或段落结束
- 中等停顿(100ms-300ms)→ 逗号
- 语气上扬 → 问号
- 语气加重 → 感叹号
我曾经踩过一个坑:单纯靠停顿时间来判断,结果在语速快的场景下,停顿都很短,句号全变成了逗号。后来我加了一个动态阈值,根据说话人的平均语速来调整停顿判断标准。
避坑指南:我曾经在会议录音场景中,发现说话人换人时的停顿和句号停顿很难区分。后来我加了一个说话人识别模块,换人时自动加段落分隔,效果好了很多。
3.1.2 基于关键词和句法模式的规则
除了停顿,我们还可以利用一些语言规律。比如:
- 出现「但是」「然而」「不过」→ 前面加逗号或句号
- 出现「吗」「呢」「吧」→ 后面加问号
- 出现「啊」「呀」「啦」→ 后面加感叹号
- 出现「首先」「其次」「最后」→ 后面加逗号
我习惯把这些规则做成一个优先级列表,高优先级的规则先匹配。比如「但是」前面如果已经有句号了,就不再加逗号,避免标点重复。
# 一个简单的规则引擎示例
def rule_based_punctuation(text):
rules = [
(r'(但是|然而|不过)', ','),
(r'(吗|呢|吧)\s', '?'),
(r'(啊|呀|啦)\s', '!'),
(r'(首先|其次|最后)', ','),
]
for pattern, punct in rules:
text = re.sub(pattern, punct + pattern, text)
return text
小技巧:规则方法虽然简单,但胜在速度快、可解释性强。我建议在模型方法之前,先用规则做一层兜底,特别是那些确定性很高的场景。
3.2 基于模型的标点恢复方法
规则方法虽然好用,但遇到复杂句子就力不从心了。比如「我喜欢吃饭妈妈做的饭很好吃」这句话,规则方法很难判断「吃饭」后面该不该加逗号。这时候就需要模型出马了。
3.2.1 序列标注方法
标点恢复最常见的做法是把它当成一个序列标注任务。说白了,就是给每个词打一个标签,告诉它后面该加什么标点。
标签体系一般是这样的:
| 标签 | 含义 | 示例 |
|---|---|---|
| O | 不加标点 | 我 O 喜欢 O 你 |
| COMMA | 加逗号 | 今天 COMMA 天气不错 |
| PERIOD | 加句号 | 我到家了 PERIOD |
| QUESTION | 加问号 | 你吃饭了吗 QUESTION |
| EXCLAMATION | 加感叹号 | 太棒了 EXCLAMATION |
模型方面,我推荐用BERT或者RoBERTa这类预训练模型做微调。为什么?因为这些模型本身已经学了很多语言知识,微调起来数据量不用太大。
# 基于BERT的标点恢复模型示例
from transformers import BertTokenizer, BertForTokenClassification
import torch
class PunctuationRestorer:
def __init__(self, model_path):
self.tokenizer = BertTokenizer.from_pretrained(model_path)
self.model = BertForTokenClassification.from_pretrained(model_path)
self.label_map = {0: 'O', 1: 'COMMA', 2: 'PERIOD',
3: 'QUESTION', 4: 'EXCLAMATION'}
def predict(self, text):
tokens = self.tokenizer(text, return_tensors='pt')
with torch.no_grad():
outputs = self.model(**tokens)
predictions = torch.argmax(outputs.logits, dim=-1)
# 将预测结果映射回原始文本
result = []
for token, pred in zip(tokens['input_ids'][0], predictions[0]):
word = self.tokenizer.decode(token)
label = self.label_map[pred.item()]
result.append((word, label))
return result
注意:BERT的tokenizer会把词拆成子词,比如「吃饭」可能拆成「吃」和「##饭」。这时候标点标签要加在第一个子词上,后面的子词统一标O。我刚开始做的时候没注意这个,结果模型学得一塌糊涂。
3.2.2 基于语言模型的方法
除了序列标注,还有一种思路是用语言模型来做。说白了,就是让模型预测下一个词是什么,同时预测该加什么标点。
具体做法是这样的:
- 把标点符号当成特殊的token,加入到词表中
- 训练一个语言模型,让它学会在合适的位置生成标点
- 推理时,用beam search来生成带标点的文本
我个人习惯用这种方法来做流式标点恢复。为什么?因为语言模型可以一边识别一边加标点,延迟很低。我在车载语音助手的项目里就用过这种方法,效果还不错。
3.2.3 混合方法:规则+模型
嗯,这里要重点说一下。我做了这么多项目,发现纯规则和纯模型都有各自的短板。最好的做法是两者结合。
我的做法是这样的:
- 先用规则做快速过滤:比如遇到「吗」「呢」直接加问号,这些确定性高的场景不用模型
- 再用模型处理复杂场景:比如长句子的逗号句号判断
- 最后用规则做后处理:比如检查标点是否重复、引号是否成对
实战经验:我在一个金融领域的语音识别项目中,发现模型在数字和日期后面经常加错标点。比如「2023年12月31日」后面,模型总喜欢加逗号。后来我加了一条规则:日期后面不加逗号,直接加句号。准确率提升了3个百分点。
3.3 评估指标与数据准备
标点恢复的评估,我一般用这几个指标:
| 指标 | 说明 | 我的经验值 |
|---|---|---|
| 准确率 | 所有标点预测正确的比例 | 一般能达到85%-90% |
| 召回率 | 实际标点中被正确预测的比例 | 逗号召回率通常偏低 |
| F1分数 | 准确率和召回率的调和平均 | 句号F1通常最高 |
| BLEU | 与人工标注的标点序列的相似度 | 0.8以上算不错 |
数据准备方面,我建议用新闻语料或者书籍语料来做训练。为什么?因为这些语料的标点标注质量高。我踩过一个坑:用社交媒体语料训练,结果模型学会了乱加感叹号和问号,因为网上的文本标点本来就不规范。
小技巧:如果你没有现成的标点恢复训练数据,可以用带标点的文本,去掉标点后作为输入,原文本作为标签。这样就能生成大量的训练数据。我在一个冷启动项目里就是这么干的,效果出奇的好。
好了,标点符号恢复这块就讲到这里。说白了,规则方法适合快速上线和兜底,模型方法适合处理复杂场景。我建议你从规则方法入手,先跑通流程,再逐步引入模型。下一章咱们聊聊语义解析,那个更有意思。