第四节:逆文本正则化——把“两万五”变成“25000”

好,咱们继续往下走。这一节要聊的,是语音识别后处理里一个特别接地气的活儿——逆文本正则化(Inverse Text Normalization,简称 ITN)。

说白了,就是把用户嘴里说出来的那些口语化表达,转成机器和系统能直接用的书面格式。比如用户说“两万五”,你得输出“25000”;说“明天下午三点半”,你得输出“2024-03-15 15:30”。

我刚开始做语音助手那会儿,就吃过这个亏。用户说“帮我充一百块话费”,结果系统理解成“100块”是数量,直接给充了100次……嗯,从那以后我再也不敢小看 ITN 了。

4.1 为什么需要逆文本正则化?

你想想看,语音识别模型输出的文本,其实是“人说的话”。人说话有大量的口语习惯:

  • 数字会说“两万五”而不是“25000”
  • 时间会说“差一刻十二点”而不是“11:45”
  • 日期会说“大后天”而不是“2024-03-18”
  • 金额会说“三块五”而不是“3.5元”

但下游系统——比如搜索引擎、数据库、支付接口——它们只认标准格式。所以 ITN 就是那个“翻译官”,把口语翻译成机器语言。

核心原则:ITN 不是简单的字符串替换,而是基于语义理解的结构化转换。同一个“两万五”,在金额场景下是“25000元”,在数量场景下就是“25000个”。

4.2 常见的 ITN 场景分类

我在实际项目中,把 ITN 场景分成了这么几大类。每一类都有它的坑,咱们一个一个说。

类别 口语输入 书面输出 常见陷阱
数字 两万五、三百二、一千零一 25000、320、1001 “两万五”可能是25000,也可能是2.5万
日期 明天、后天、下周一、三月十五 2024-03-16、2024-03-17、2024-03-18 “下周一”需要知道当前日期才能算
时间 三点半、差五分八点、中午十二点 15:30、07:55、12:00 “中午十二点”是12:00还是00:00?
金额 三块五、一百二十元、八毛 3.5元、120元、0.8元 “三块五”是3.5元还是3.05元?
电话号码 幺三九零零八六...、一三九零零八六 1390086xxxx “幺”和“一”的歧义

4.3 技术实现方案

ITN 的实现,我一般推荐三种方案。根据你的场景和资源选就行。

方案一:基于规则的正则表达式

这是最直接的方法。适合场景固定、格式明确的业务。比如只处理数字和金额。

import re

def itn_rule_based(text):
    # 处理“两万五” -> 25000
    # 注意:这里只演示核心逻辑,实际要处理更多边界情况
    pattern = r'([一二三四五六七八九十百千万亿]+)万([一二三四五六七八九十百千万亿]*)'
    def replace_num(match):
        # 这里需要实现中文数字转阿拉伯数字的逻辑
        # 我习惯用字典映射 + 递归处理
        pass
    return re.sub(pattern, replace_num, text)

# 示例
print(itn_rule_based("帮我充两万五"))  # 期望输出:帮我充25000

我的经验:纯规则方案维护成本高。我曾经维护过一个包含2000多条正则的ITN模块,每次加新场景都像在雷区跳舞。建议规则数量控制在200条以内,超过就考虑方案二或三。

方案二:基于有限状态机(WFST)

这是工业界用得比较多的方法。用加权有限状态转换机(WFST)来建模口语到书面语的映射关系。

# 伪代码示例,实际使用需要 Pynini 或 Thrax 工具
# 定义数字的WFST
digit_fst = {
    "零": "0", "一": "1", "二": "2", "三": "3",
    "四": "4", "五": "5", "六": "6", "七": "7",
    "八": "8", "九": "9"
}

# 组合规则
# "两万五" -> 2 * 10000 + 5 * 1000? 还是 25000?
# 这里需要定义组合优先级

WFST 的好处是效率高、可组合。我曾在某大厂做过一个 WFST 的 ITN 模块,处理速度能达到每秒 10 万句。但缺点也很明显——学习曲线陡,调试困难。

方案三:基于序列到序列(Seq2Seq)模型

近几年随着大模型的发展,直接用 Transformer 做 ITN 也成了选项。输入口语文本,输出书面文本。

# 使用 HuggingFace 的 T5 模型做 ITN
from transformers import T5Tokenizer, T5ForConditionalGeneration

tokenizer = T5Tokenizer.from_pretrained("t5-small")
model = T5ForConditionalGeneration.from_pretrained("t5-small")

def itn_seq2seq(text):
    input_text = f"itn: {text}"
    inputs = tokenizer(input_text, return_tensors="pt")
    outputs = model.generate(**inputs, max_length=128)
    return tokenizer.decode(outputs[0], skip_special_tokens=True)

# 示例
print(itn_seq2seq("两万五"))  # 期望输出:25000

注意:模型方案虽然灵活,但需要大量标注数据。我见过一个团队花了3个月标注了50万条ITN数据,结果模型在“两万五”上还是偶尔出错。数据质量比数量更重要。

4.4 实战中的避坑指南

这部分是我最想跟你分享的。ITN 看起来简单,但坑特别多。

  • 歧义消解:“两万五”到底是 25000 还是 2.5 万?我建议结合上下文判断。如果前面有“约”、“大概”,可能是 2.5 万;如果是精确数字,就是 25000。
  • 单位粘连:“三块五”和“三块五毛”不一样。前者是 3.5 元,后者是 3.5 元(但“五毛”是 0.5 元)。我曾经在支付场景里搞混过,导致用户多付了钱……
  • 方言影响:“两”和“二”在不同方言里用法不同。北方人常说“两百五”,南方人可能说“二百五”。ITN 要能兼容。
  • 时间相对性:“明天”、“后天”需要知道当前日期。我习惯在 ITN 模块里传入一个 reference_date 参数。

我的建议:不要试图用一个方案解决所有问题。我现在的做法是:规则处理 80% 的常见场景,模型兜底处理 15% 的复杂场景,剩下的 5% 走人工审核。这样既保证了速度,又保证了准确率。

4.5 评估指标

ITN 做得好不好,不能光看准确率。我一般用这几个指标:

指标 说明 我的经验值
准确率 完全正确的比例 目标 > 98%
召回率 该转换的是否都转了 目标 > 95%
误转率 不该转的转错了 目标 < 0.5%
延迟 单句处理时间 目标 < 50ms

嗯,这一节的内容差不多就这些。ITN 看起来是个小模块,但做精了不容易。下一节咱们聊聊更复杂的语义解析——怎么让机器真正“听懂”用户的意思。