WeTextProcessing-文本[逆]正则化

Github:https://github.com/wenet-e2e/WeTextProcessing

摘自:https://mp.weixin.qq.com/s/q_11lck78qcjylHCi6wVsQ

Funasr仓库:

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**2addzero**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意” 则有两种解决方案:

  1. 在 whitelist.tsv 添加相关的映射放弃相关词汇的转换
  2. 将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

向云端

向云端
山那边
海里面
真实的我应该走向哪边
日落前
风来临
石墩下我在盘腿坐着
人到底靠什么来定义丑恶
神啊你在哪
山啊我害怕
海啊也带不走
尽头到底有没有
如果你很难过
不如先收拾你的房间
别再辗转反侧
等会儿阳光会照在你的旁边
没洗的别再攒着
换个新的发型买个好看的包
压抑焦虑心情忐忑
就去养只爱你的猫
不是不如意
也许你并没围着自己的赛道走
你真的很美丽
为什么常说自己的外貌丑
或许你像我一样很胖
生活让你感到卑贱
那么当你听到这首歌的时候
选择与我共同蜕变
也许你现在很难过
也许正躺在被窝
也许你现在很迷茫
正在酒吧里坐着
也许你在工作
或者刚刚分手了
也许你在山脚下
会情不自禁的哼出这首歌
向云端
山那边
海里面
真实的你在于怎么选择
神啊你在哪
山啊我害怕
海啊带走哀愁
就像带走每条河流

郭源潮

你说你知道他们的世界
悲歌三首买一切
买昆仑落脚 蓬莱放思想
买人们的争执酿酒汤
买公主坟的乌鸦
事发之木和东窗之麻
买胭脂河里船行渔歌
黄金世界中万物法则
你我都一样 将被遗忘 郭源潮
你的病也和我的一样 风月难扯 离合不骚
层楼终究误少年 自由早晚乱余生
你我山前没相见 山后别相逢
买石灰街车站的海鸥
山水禽兽和年少一梦
买太平湖底陈年水墨
哥本哈根的童年传说
其实你我都一样 终将被遗忘 郭源潮
你的病也和我的一样 风月难扯 离合不骚
层楼终究误少年 自由早晚乱余生
你我山前没相见 山后别相逢
其实你我都一样 终将被遗忘 郭源潮
你的病也和我一样 风月难扯 离合不骚
层楼终究误少年 自由早晚乱余生
你我山前没相见 山后别相逢
你我山前没相见 山后别相逢
你我山前没相见 山后别相逢

等我们变成更好的人

等我们各自走出盛夏

淋过几场雪花

想念还会再发芽

等我们各自走出盛夏

淋过几场雪花

想念还会再发芽

等我们各自学会长大

愈合几道伤疤

心愿开出几朵花

那年的故事写进晚霞

难过时记得抬头看它

等我们变成更好的人

聊聊从前的大话

我多想一觉醒来回到有你的夏

还不用把你牵挂

那穿过小城的风拉着我们的手

说别急着去远方

那一片夜空的星听过太多太多

我们未来的计划

多年后想起了谁它就眨了一眨

好像在学你说话

多希望后来闭上眼

少年还站在榕树下

肩上蝶明天飞去哪

都舍不得把你丢下

多希望那时间的手

别擦去你天真的脸颊

青春醒过来的刹那

谢谢你陪我说过梦话

等我们各自走出盛夏

淋过几场雪花

想念还会再发芽

等我们各自学会长大

愈合几道伤疤

心愿开出几朵花

那年的故事写进晚霞

难过时记得抬头看它

等我们变成更好的人

聊聊从前的大话

多希望后来闭上眼

少年还站在榕树下

肩上蝶明天飞去哪

都舍不得把你丢下

多希望那时间的手

别擦去你天真的脸颊

青春醒过来的刹那

谢谢你陪我说过梦话

等我们各自走出盛夏

淋过几场雪花

想念还会再发芽

等我们各自学会长大

愈合几道伤疤

心愿开出几朵花

那年的故事写进晚霞

难过时记得抬头看它

等我们变成更好的人

聊聊从前的大话

我记得

我带着比身体重的行李 游入尼罗河底 经过几道闪电 看到一堆光圈 不确定是不是这里
我看到几个人站在一起 他们拿着剪刀摘走我的行李 擦拭我的脑袋 没有机会返回去

直到我听见一个声音 我确定是你
可你怎记得我
我带来了另界的消息 可我怎么告知你
注定失忆着相遇

我记得这里是片树林 后面有个山坡 山坡上的枣树每当秋天到来 我们把枣装满口袋
我记得除了朋友我还 做过你的叔父 你总喜欢跟在我的屁股后面 只是为了那几个铜钱
我记得我们曾是恋人 后来战争爆发 你上战场后就再也没有回来 直到收不到你的信
我们总这样重复分离 却要重新开始 相互送别对方 说着来世再见 再次失忆着相聚

呜 呜 呜 呜…
快来抱抱 快来抱抱我
呜 呜 呜 呜…
快来抱抱 快来抱抱我

在路上我遇到了一位故去多年的人 她是如此年轻 扎着过肩马尾 露出和你一样的笑
她和我讲了很多关于你成长的故事 在星空另一端 思念从未停止 如同墓碑上的名字
不要哭我最亲爱的人 我最好的玩伴 时空是个圆圈 直行或是转弯 我们最终都会相见
在城池的某个拐角处 在夕阳西下时 在万家灯火的某一扇窗纱里 人们失忆着相聚

呜 快来抱抱 快来抱抱我
呜 快来抱抱 快来抱抱我 我终于找到你

船长

赵雷

请告诉我寂寞的时长
那里是否铺满花儿的香
在海上漂了很久会觉得岸上很晃
雨打湿了衣裳就适应了海水的凉

海北的路上没有信号
海北的灯光比繁星亮
海北的机车妹匆匆忙
海北的空调像是冬天一样冷得让我
缩成一只小鸟 缩成一只小鸟
可是没有人知道
我是刚刚经过暴风雨回到岸上的船长
会到岸的船长
而我却迷失方向
像是气球一样四处撞

槟榔一定配香烟才够爽
海北的男孩讲话有些娘娘腔
尽管看上去他们更强壮
但是我有胆量去征服海洋

上天没有给我华丽的皮囊
至今学不会打领结穿西装
海北没有适合我的衣裳
我喜欢光着膀子露出胸膛

吃饱的乌鸦在叫
迟到的学生赛跑
妩媚的夜让我想念我的船和那些被雨淋湿的海鸟
哭泣的乌云在飘
沉默的大地在摇
我开始习惯热流里涌来的爆米花味道

我的心总有一些问号
我的心总有一些孤傲
我的心总有一些潦倒
但从来没有任何事能把我困扰

我的心总有一些荒谬
我的心总有一些寂寥
我的心总有一些征兆
所以我停靠海北
来到海北 在海北
这里的人也会因为爱情喝得烂醉
一样拥挤的海北
璀璨的海北
哦 baby
我无法入睡

听歌

何必管一片海
有多么澎湃
何必管那山岗
它高在什么地方
只愿这颗跳动不停的心
永远有慈爱
好让这世间冰冷的胸膛
如盛开的暖阳
旅人等在那里
虔诚仰望着云开
咏唱回荡那里
伴着寂寞的旅程
心中这一只鹰
在哪里翱翔
心中这一朵花
它开在那片草原

少年游

芦叶满汀洲,寒沙带浅流。二十年重过南楼。柳下系船犹未稳,能几日,又中秋。黄鹤断矶头,故人今在否?旧江山浑是新愁。欲买桂花同载酒,终不似,少年游。 《唐多令》宋·刘过

少年偏爱摇摇欲坠的日落黄,殊不知此刻正拥有的,是一生中最明媚的曙光。

春日游,杏花吹满头。陌上谁家年少,足风流。

韶华不为少年留,恨悠悠,几时休。

可是我现在依然不太会转弯
虽然孤单的人偶尔也想有个伴
冷风又吹的时候想说
这生活会不会有点难
难道是因为当初有话没讲完
堵在喉咙里却始终不敢大声喊
算了 别哭 ————-毛不易《呓语》