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 混合方法:规则+模型

嗯,这里要重点说一下。我做了这么多项目,发现纯规则和纯模型都有各自的短板。最好的做法是两者结合。

我的做法是这样的:

  1. 先用规则做快速过滤:比如遇到「吗」「呢」直接加问号,这些确定性高的场景不用模型
  2. 再用模型处理复杂场景:比如长句子的逗号句号判断
  3. 最后用规则做后处理:比如检查标点是否重复、引号是否成对

实战经验:我在一个金融领域的语音识别项目中,发现模型在数字和日期后面经常加错标点。比如「2023年12月31日」后面,模型总喜欢加逗号。后来我加了一条规则:日期后面不加逗号,直接加句号。准确率提升了3个百分点。

3.3 评估指标与数据准备

标点恢复的评估,我一般用这几个指标:

指标 说明 我的经验值
准确率 所有标点预测正确的比例 一般能达到85%-90%
召回率 实际标点中被正确预测的比例 逗号召回率通常偏低
F1分数 准确率和召回率的调和平均 句号F1通常最高
BLEU 与人工标注的标点序列的相似度 0.8以上算不错

数据准备方面,我建议用新闻语料或者书籍语料来做训练。为什么?因为这些语料的标点标注质量高。我踩过一个坑:用社交媒体语料训练,结果模型学会了乱加感叹号和问号,因为网上的文本标点本来就不规范。

小技巧:如果你没有现成的标点恢复训练数据,可以用带标点的文本,去掉标点后作为输入,原文本作为标签。这样就能生成大量的训练数据。我在一个冷启动项目里就是这么干的,效果出奇的好。

好了,标点符号恢复这块就讲到这里。说白了,规则方法适合快速上线和兜底,模型方法适合处理复杂场景。我建议你从规则方法入手,先跑通流程,再逐步引入模型。下一章咱们聊聊语义解析,那个更有意思。