前言

Github:NLP相关Paper笔记和代码复现
说明:讲解时会对相关文章资料进行思想、结构、优缺点,内容进行提炼和记录,相关引用会标明出处,引用之处如有侵权,烦请告知删除。
转载请注明:DengBoCong

NLP的语音合成中,有一种关键技术是将文字拆解成音素,再去语音库里匹配相同音素的语音片段,来实现文字转换语音。音素是给定语言的语音,如果与另一个音素交换,则会改变单词的含义,同时,音素是绝对的,并不是特定于任何语言,但只能参考特定语言讨论音素。由于音素的特性,非常适合用于语音合成领域。

音素(phone),是语音中的最小的单位,依据音节里的发音动作来分析,一个动作构成一个音素。音素分为元音、辅音两大类。

说白了,音素其实就是人在说话时,能发出最最最最短小、简洁的不能再分割的发音,不同的音素就是不同的短发音,可以组成不同的长发音,再组成词句形成语言。
在这里插入图片描述

本篇文章就来讲讲中文和英文中,如何将文本转换为音素序列。

相关知识

众所周知,语音合成系统通常包含前端和后端两个模块。前端模块主要是对输入文本进行分析,提取后端模块所需要的语言学信息。前端模块一般包含文本正则化、分词、词性预测、多音字消歧、韵律预测等子模块。后端模块根据前端分析结果,通过一定的方法生成语音波形。后端模块一般分为基于统计参数建模的语音合成(Statistical Parameter Speech Synthesis,SPSS,以下简称参数合成),以及基于单元挑选和波形拼接的语音合成(以下简称拼接合成)两条技术主线。

而“端到端”架构的语音合成系统的出现,能够直接从字符文本合成语音,打破了各个传统组件之间的壁垒,使得我们可以从<文本,声谱>配对的数据集上,完全随机从头开始训练。最具代表性的端到端语音合成系统,就是2017年初,Google 提出的端到端的语音合成系统——Tacotron

从通俗一点的角度来讲,语音合成过程,需要处理两部分内容,分别是文本(Text)处理和音频(speech)处理,如下图是端到端的语音合成系统整体技术架构选型。我们文章要讲的就是属于TTS Frontend范畴的Text-to-Phoneme。
在这里插入图片描述

文本规范化

对文本进行预处理,主要是去掉无用字符,全半角字符转化等,对于中文而言,有时候普通话文本中会出现简略词、日期、公式、号码等文本信息,这就需要通过文本规范化,对这些文本块进行处理以正确发音,比如:

  • “小明体重是 128 斤”中的“128”应该规范为“一百二十八”,而“G128 次列车”中的“128” 应该规范为“一 二 八”
  • “2016-05-15”、“2016 年 5 月 15 号”、“2016/05/15”可以统一为一致的发音

对于英文而言,也需要将年份、货币、数字、字母等文本信息,转换为完整单词,比如:

  • 类别为年份(NYER): 2011 → twenty eleven
  • 类别为货币(MONEY): £100 → one hundred pounds
  • 类别为非单词,需要拟音(ASWD): IKEA → apply letter-to-sound
  • 类别为数字(NUM) : 100 NUM → one hundred
  • 类别为字母(LSEQ) : DVD → dee vee dee

这些文本规范化预处理不放在深度学习模型去学习,而是通过各种规则的正则表达式进行转换,所以涉及到的代码工作量还是比较大的,所以我将写好的代码更新至GitHub项目中,方便需要者使用(TensorFlow和PyTorch版本同步更新):NLP相关Paper笔记和代码复现,这里粘贴一个作为举例,方便大家知道是啥。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def _clean_number(text: str):
"""
对句子中的数字相关进行统一单词转换
:param text: 单个句子文本
:return: 转换后的句子文本
"""
comma_number_re = re.compile(r"([0-9][0-9\,]+[0-9])")
decimal_number_re = re.compile(r"([0-9]+\.[0-9]+)")
pounds_re = re.compile(r"£([0-9\,]*[0-9]+)")
dollars_re = re.compile(r"\$([0-9\.\,]*[0-9]+)")
ordinal_re = re.compile(r"[0-9]+(st|nd|rd|th)")
number_re = re.compile(r"[0-9]+")

text = re.sub(comma_number_re, lambda m: m.group(1).replace(',', ''), text)
text = re.sub(pounds_re, r"\1 pounds", text)
text = re.sub(dollars_re, _dollars_to_word, text)
text = re.sub(decimal_number_re, lambda m: m.group(1).replace('.', ' point '), text)
text = re.sub(ordinal_re, lambda m: inflect.engine().number_to_words(m.group(0)), text)
text = re.sub(number_re, _number_to_word, text)

return text

英文Text-to-Phoneme

我们先来讲讲英文的Text-to-Phoneme,想要将英文单词转换成音素,想必需要了解Arpabet,下面是WiKi上的解释:

ARPABET(也称为ARPAbet)是高级研究计划局(ARPA)在1970年代语音理解研究项目中开发的一组语音转录代码。 它代表具有不同ASCII字符序列的通用美国英语的音素和同音素。

可以理解为通过字母组合定义发音规则,英文发音是由元音辅音等组成的,同时,在元音后紧接着用数字表示压力, 辅助符号在1和2个字母的代码中相同, 在2个字母的符号中,段之间用空格隔开,大概如下这样:
在这里插入图片描述

我们将文本转换为ARPABET并不需要我们了解很多语言学的东西,我们只需要选择现有合适的发音字典就可以了,使用比较多的还是CMU的发音词典,官方网站,想了解原理的可以看这篇论文。可以先在官网体验一下是啥效果,如下:
在这里插入图片描述
自行从官网下载发音字典(也可以去我的github下载,链接在文章顶部),音素集,39个音素,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Phoneme Example Translation
------- ------- -----------
AA odd AA D
AE at AE T
AH hut HH AH T
AO ought AO T
AW cow K AW
AY hide HH AY D
B be B IY
CH cheese CH IY Z
D dee D IY
DH thee DH IY
EH Ed EH D
ER hurt HH ER T
EY ate EY T
F fee F IY
G green G R IY N
HH he HH IY
IH it IH T
IY eat IY T
JH gee JH IY
K key K IY
L lee L IY
M me M IY
N knee N IY
NG ping P IH NG
OW oat OW T
OY toy T OY
P pee P IY
R read R IY D
S sea S IY
SH she SH IY
T tea T IY
TH theta TH EY T AH
UH hood HH UH D
UW two T UW
V vee V IY
W we W IY
Y yield Y IY L D
Z zee Z IY
ZH seizure S IY ZH ER

有了音素集之后就可以使用脚本将文本转换为音素了,我的转换脚本如下:(完整代码同GitHub可找到)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def text_to_phonemes_converter(text: str, cmu_dict_path: str):
"""
将句子按照CMU音素字典进行分词切分
:param text: 单个句子文本
:param cmu_dict_path: cmu音素字典路径
:return: 按照音素分词好的数组
"""
_, symbols_set = get_phoneme_dict_symbols()

alt_re = re.compile(r'\([0-9]+\)')
cmu_dict = {}
text = _clean_text(text)
text = re.sub(r"([?.!,])", r" \1", text)

# 文件是从官网下载的,所以文件编码格式要用latin-1
with open(cmu_dict_path, 'r', encoding='latin-1') as cmu_file:
for line in cmu_file:
if len(line) and (line[0] >= "A" and line[0] <= "Z" or line[0] == "'"):
parts = line.split(' ')
word = re.sub(alt_re, '', parts[0])

# 这里要将非cmu音素的干扰排除
pronunciation = " "
temps = parts[1].strip().split(' ')
for temp in temps:
if temp not in symbols_set:
pronunciation = None
break
if pronunciation:
pronunciation = ' '.join(temps)
if word in cmu_dict:
cmu_dict[word].append(pronunciation)
else:
cmu_dict[word] = [pronunciation]

cmu_result = []
for word in text.split(' '):
# 因为同一个单词,它的发音音素可能不一样,所以存在多个
# 音素分词,我这里就单纯的取第一个,后面再改进和优化
cmu_word = cmu_dict.get(word.upper(), [word])[0]
if cmu_word != word:
cmu_result.append("{" + cmu_word + "}")
else:
cmu_result.append(cmu_word)

return " ".join(cmu_result)

中文Text-to-Phoneme

对于中文,其实我们再熟悉不过了,中文的音素其实就是汉语拼音的最小单元,包括声母,韵母,但是其中还会有一些整体认读音节,更详细的拼音标注文本分析,可以参考MTTS文本分析。同一个字在不同分词情况下的发音不同,所以导致数量级比较大,比较典型的可以参见清华大学的thchs30中文数据集,里面提供了分词好的音素字典,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SIL sil
<SPOKEN_NOISE> sil
啊 aa a1
啊 aa a2
啊 aa a4
啊 aa a5
啊啊啊 aa a2 aa a2 aa a2
啊啊啊 aa a5 aa a5 aa a5
阿 aa a1
阿 ee e1
阿尔 aa a1 ee er3
阿根廷 aa a1 g en1 t ing2
阿九 aa a1 j iu3
阿克 aa a1 k e4
阿拉伯数字 aa a1 l a1 b o2 sh u4 z iy4
阿拉法特 aa a1 l a1 f a3 t e4

对应脚本在这里,不过其实汉子转拼音还有更方便的方式,就是使用Python 的拼音库 PyPinyin,如下:

1
2
3
from pypinyin import pinyin
print(pinyin('朝阳'))
# [['zhāo'], ['yáng']]

PyPinyin可以用于汉字注音、排序、检索等等场合,是基于 hotto/pinyin 这个库开发的,一些站点链接如下: