第四节:逆文本正则化——把“两万五”变成“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 看起来是个小模块,但做精了不容易。下一节咱们聊聊更复杂的语义解析——怎么让机器真正“听懂”用户的意思。