Github:https://github.com/wenet-e2e/WeTextProcessing
摘自:https://mp.weixin.qq.com/s/q_11lck78qcjylHCi6wVsQ
Funasr仓库:
- https://www.modelscope.cn/models/thuduj12/fst_itn_zh
- https://github.com/duj12/WeTextProcessing/tree/master
Motivation
文本正则化(Text Normalization,TN)和反正则化(Inverse Text Normalization,ITN)是构建一个完整的语音交互系统不可或缺的部分。前者广泛用于语音合成系统的前端处理,而后者则在语音识别系统的识别文本上屏显示时影响着字幕的观感体验。
当前学术界中被广泛研究的 TN / ITN 系统主要有三种类型:
- 基于语法规则的 WFST [1]:这种系统由大量特定于语言的语法组成,优点是准确可控,可以快速修 bug ,缺点是对于容易产生歧义的文本不够鲁棒。
- 基于神经网络的端到端模型 [2]:构建这种模型时,挑战从撰写更精确的语法规则变成了标注和收集覆盖范围更广的数据。端到端模型的一个主要缺点是会产生无法恢复的错误,这时经系统转换后的文字可能在语法上是合理的,但却与原始文本的语义大相径庭。此外,对于 badcase 的修复也不如规则的方式快捷。
- 同时使用规则语法和神经网络的混合系统 [3]:在混合框架中,只有当系统没有找到匹配的语法规则才会转用神经网络。这种方式比较好地权衡了规则和 NN 的优劣,但是对计算资源提出了更高的要求。
鉴于以上三种系统的优劣,WeTextProcessing 选择实现基于语法规则的WFST 方案。在全球范围内的开源TN/ITN 项目中,目前受众最广泛的是谷歌公司推出的C++ 框架 Sparrowhawk [4] 。该框架的不足之处是它仅仅是一个规则执行引擎,谷歌公司并没有开源相关语言的语法规则。此外,Sparrowhawk 的实现依赖了许多第三方开源库(包括 OpenFst 、Thrax 、re2 、protobuf ),导致整体框架不够简便、轻量化。另一个较为成熟的项目是英伟达公司开源的 nemo_text_processing [5],该项目依旧使用Sparrowhawk 作为生产环境下的部署工具。与谷歌不同的是,该项目还开源了诸如英语、德语、俄语等多种语言的规则语法。在中文 TN / ITN 规则领域,Jiayu 等第三方个人开发者曾开源出一套定制化的中文 TN / ITN 规则库 chinese_text_normalization [6]。
站在这些优秀开源项目的肩膀上,WeTextProcessing秉承 简单易用 和Production First & Production Ready 的原则,为中文专门设计和实现一款开源易用的 TN / ITN 工具,它不仅仅包含了包含一套完整的中文 TN / ITN 规则语法,同时也提供了一个可以一键 pip install 使用的 py工具包以及比Sparrowhawk 依赖项更少(生产环境下仅依赖 OpenFst )的整体更轻量化的 C++ 规则处理引擎。
快速上手
一键install,六行代码搞定文本处理!
# install
pip install WeTextProcessing
# tn usage >>> from tn.chinese.normalizer import Normalizer >>> normalizer = Normalizer() >>> normalizer.normalize("2.5平方电线") # itn usage >>> from itn.chinese.inverse_normalizer import InverseNormalizer >>> invnormalizer = InverseNormalizer() >>> invnormalizer.normalize("二点五平方电线")
技术细节
TN 和 ITN 的流程都是包含三个部分:Tagger, Reorder 和 Verbalizer。Tagger 负责对输入的文本进行解析,得到结构化的信息。Reorder 负责对结构化信息进行顺序的调整。最终 Verbalizer 负责将重排序之后的结构化信息拼接起来。
TN 流程

ITN 流程

语法规则设计
WeTextProcessing 使用 pynini [7] 来编写和编译规则语法,规则语法可以将一个字符串转换为另一个字符串。规则语法通常可以表示为一个 WFST,pynini 的底层使用了 OpenFst 来实现 WFST 相关的功能。使用 pynini 编写的规则语法示例如下图所示:

- digits = zero | digit 的 | 操作符表示 WFST 理论中的 union 操作;
- cross(‘十’, ‘1’) 表示 WFST 理论中弧上的输入是“十”,输出是“1”,WFST 从一个状态转到另一个状态时若经过该弧则说明系统匹配到了“十”并成功将其转换为了“1”;
- delete(‘十’) 表示弧上的输入是“十”,输出是空,即经过该弧时会删除“十”;
- digit + delete(‘十’) 中 + 表示WFST理论中的 concat 操作,它将两个fst连起来;
- accep(‘兆’) 表示弧的输入和输出都是“兆”,此时 WFST 相当于一个 FSA;
- addzero**2,addzero**3 分别表示将 addzero 重复两次和三次;
- digits.ques 和 digits.plus 则分别表示将 digits 重复零到一次 和 重复一到无穷次
此外还有一些语法特性,比如下图中:

- add_weight(Char().tagger, 100) 表示为 Char().tagger 这条路径赋予权重(路径长度)为 100。当有多条路径都可以匹配当前输入时,我们取最短路径作为终选结果。例如“一点零五分”最终会被 ITN 成 “1:05” 而不是 “1.05分”。
- insert(‘ ‘) 表示弧上的输入和输出分别是“”和“ ”,即经过该弧时会强制插入一个空格。
- processor @ tagger.optimize() 中 @ 表示将两个 fst 进行 compose 操作,optimize() 表示对 tagger 进行 epsilon-removal,determinization 以及 minimization [8]
- ‘[EOS]’ 表示正则表达式中匹配到的 string 的结尾,同理这里没有列出的 ‘[BOS]’ 则表示开头 [9]
更多详尽的说明请参考pynini 的相关文档 [7]。对于本文所构建的所有WFST,我们采用 OpenFst 中默认的热带半环作为其类型,做出这个选择的原因是此类型对求网格图中的最短路径的操作有效率优势,其路径权重的计算仅需对沿路径的所有弧的权重进行简单求和。
进阶用法
如何快速修 badcase
当遇到 badcase 的时候,我们首先需要确定 badcase 属于什么类型,日期?时间?还是分数等等?是没有转换,还是转换成了其他类型。然后再去相对应的 rules 中进行修复,可能需要改代码,也可能需要改 tsv 文件。
比如若 ITN 系统将 “三心二意” 错误转成了 “3心2意” 则有两种解决方案:
- 在 whitelist.tsv 添加相关的映射放弃相关词汇的转换
- 将enable_standalone_number设置为False,此时系统对不带单位的数字不会进行转换
值得注意的是,WeTextProcessing 大多数失败案例是由于上下文歧义或特殊案例造成的长尾问题。例如,“三点五分” 可以是时间 “3:05” 也可以是量词 “3.5 分” 表示运动员得分。编写语法时若考虑更多的上下文可以一定程度上缓解这种情况,例如,如果 “三点五分” 前面有单词 “得到” ,则将其检测为运动员得分。当然,这种打补丁的方式并不能适用于所有情况。出于这个原因,如果想要设计一个能够覆盖 100% 场景的系统,语法的数量将不可避免呈指数级增长。其他常见的失败案例是由于定义不完整。例如,如果没有预定义 “千瓦时” 到 “kwh” 的度量缩写转换,系统将无法转换 “两百千瓦时” 为 “200kwh” 。这个问题相对来说容易解决,仅需在已有的量词类中添加所需的转换规则。
生产环境部署
对于想要自己对规则进行DIY的用户,可以通过以下方式获得自己的规则文件并部署到不同的环境中。
git clone https://github.com/wenet-e2e/WeTextProcessing.git
cd WeTextProcessing
# `overwrite_cache` will rebuild all rules according to
# your modifications on tn/chinese/rules/xx.py (itn/chinese/rules/xx.py).
# After rebuild, you can find new far files at `$PWD/tn` and `$PWD/itn`.
python normalize.py --text "2.5平方电线" --overwrite_cache
python inverse_normalize.py --text "二点五平方电线" --overwrite_cache
在已经pip安装好的工具包中使用自己的规则:
# tn usage
>>> from tn.chinese.normalizer import Normalizer
>>> normalizer = Normalizer(cache_dir="PATH_TO_GIT_CLONED_WETEXTPROCESSING/tn")
>>> normalizer.normalize("2.5平方电线")# itn usage
>>> from itn.chinese.inverse_normalizer import InverseNormalizer
>>> invnormalizer = InverseNormalizer(cache_dir="PATH_TO_GIT_CLONED_WETEXTPROCESSING/itn")
>>> invnormalizer.normalize("二点五平方电线")
在C++中使用自己的规则:
cmake -B build -S runtime -DCMAKE_BUILD_TYPE=Releasecmake --build build
# tn usage
./build/bin/processor_main --far PATH_TO_GIT_CLONED_WETEXTPROCESSING/tn/zh_tn_normalizer.far --text "2.5平方电线"
# itn usage
./build/bin/processor_main --far PATH_TO_GIT_CLONED_WETEXTPROCESSING/itn/zh_itn_normalizer.far --text "二点五平方电线"
总结和展望
未来,WeTextProcessing 的工作将聚焦在对 Corner Case 的规则修补:相比于规则撰写,设计一套合理的测试集是一件更为困难的事情,这是因为实际生产过程中总会遇到数不清的 corner case 。WeTextProcessing 中虽然提供了一个简单的单元测试和示例测试,但其覆盖场景仍未能达到 100% 。在未来,WeTextProcessing 的重点方向之一就是越来越多地投入部署到真实的线上环境中,以身试错,case by case 分析当前规则存在的可能漏洞并加以弥补。
参考资料
[1] Peter Ebden and Richard Sproat, “The kestrel TTS text normalization system,” Nat. Lang. Eng., vol. 21, no. 3, pp. 333–353, 2015.
[2] Courtney Mansfield, Ming Sun, Yuzong Liu, Ankur Gandhe, and Björn Hoffmeister, “Neural text normalization with subword units,” in Proceedings of the 2019 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, NAACL-HLT 2019, Minneapolis, MN, USA, June 2-7, 2019, Volume 2 (Industry Papers), Anastassia Loukina, Michelle Morales, and Rohit Kumar, Eds. 2019, pp. 190–196, Association for Computational Linguistics.
[3] Richard Sproat and Navdeep Jaitly, “An RNN model of text normalization,” in Interspeech 2017, 18th Annual Conference of the International Speech Communication Association, Stockholm, Sweden, August 20-24, 2017, Francisco Lacerda, Ed. 2017, pp. 754–758, ISCA.
[4] Peter Ebden and Richar Sproat, “Sparrowhawk,” 2022, https://github.com/google/sparrowhawk.
[5] Yang Zhang, “nemo_text_processing,” 2022, https://github.com/NVIDIA/NeMo/tree/main/nemo_text_processing.
[6] Jiayu Du, “chinese_text_normalization,” 2022, https://github.com/speechio/chinese_text_normalization.
[7] K. Gorman. 2016. Pynini: A Python library for weighted finite-state grammar compilation. In Proceedings of the ACL Workshop on Statistical NLP and Weighted Automata, pages 75-80.
[8] https://www.opengrm.org/twiki/bin/view/GRM/PyniniOptimizeDoc
[9] https://www.openfst.org/twiki/bin/view/GRM/ThraxQuickTour