上上周的时候接了中文和英文语音实时识别两个榜单,中间又是遇到了大大小小的坑。这周两个榜单都上线供算法同学打榜 PK 了,因此得以有时间整理下。

项目背景

语音实时识别功能,一部分人应该用过,最常见的就是点击输入法的语音按钮,不停的说着话时,文本框上实时的显示识别的结果。除了输入法,在我司的对外产品中也有很多应用。前段时间有了一个智能耳机项目,其中一个功能就是将语音实时识别功能嵌入其中。另外,公司的式说平台,也要支持语音输入功能。因此,该榜单应运而生。

评估指标

当前语音实时识别榜单的评估指标可以分为以下几种,其中前两种也适用于非实时识别的语音识别。

字错率(CER)

字错率适用于中文、日文、韩文等语言,每个字符就是一个独立的单元。

在计算字错率的时候,首先需要基于正确答案和预测结果之间的编辑距离进行字的匹配。(匹配之前还应该进行文本归一化,后面讲述)

例如下面是一个标准答案和预测结果的示例:

label = "你好呀,今天天气不错呀"
pred = "你好,今天天气是不错的"

将两者基于编辑距离进行字的匹配后,得到的效果为:

label = ["你", "好", "呀", "今", "天", "天", "气", None, "不", "错", "呀"]
pred =  ["你", "好", None, "今", "天", "天", "气", "是", "不", "错", "的"]

其中,在进行字匹配的时候,会出现三种错误情况——多字、少字、错字,更官方的表示为插入错误(I)、删除错误(D)和替换错误(S)。

那么,CER 的计算方式就是 (I + D + S) / label_token_cnt.

词错率(WER)

词错率适用于英文等语言,一个词是一个最小单元,计算方式类同 CER。

RTF

RTF(Real time factor) 是一个判断语音识别实时率的指标,简单来说就是判断语音识别的延迟高低。

RTF 的计算方法是:语音识别的时长 / 语音输入时长。

然而,为了模拟更真实的场景,语音前后与中间可能包含静音片段,所以在榜单设计的时候,对 RTF 的计算方式进行一定的变更,从而成为榜单开发中最大的坑点 hhh。

句子切分正确率

在实际使用中,除了要查看字词识别的是否正确,还需要查看语音识别的时候能否合理的切分句子,毕竟用户也不想要一个不含标点的纯文本内容。

当前榜单在判断句子切分是否正确时,考察句子是否少切分了,或者多切分了。PS. 后来我觉得应该按照编辑距离匹配的方式判断 S+I+D 更合理。

举个例子,假设一段语音对应的句子是下面的形式:

label = "今天天气不错呀,昨天的就不好。"
pre = "今天天气,不错呀昨天的就不好,难受。"

那么,“天气”处的断句属于多切分了,“不错呀”处的断句属于少切分了,“不好”处的断句属于正确切分,“难受”处的断句属于多切分了。

至于实现句子切分正确率的方法可以简写为如下伪代码:

# 基于标点符号进行句子切分,当前使用的标点符号为中英文的句号、逗号、分号、冒号、感叹号、问号、省略号
label_sentence_split = split_sentence(label)  # ["今天天气不错呀", "昨天的就不好"]
pred_sentence_split = split_sentence(pred)  # ["今天天气", "不错呀昨天的就不好", "难受"]
 
# 对每一个句子进行归一化操作
lable_sentence_norm = [text_norm(sentence, tokenizer="char") for sentence in label_sentence_split]
pred_sentence_norm = [text_norm(sentence, tokenizer="char") for sentence in pred_sentence_split]
 
# 基于编辑距离进行字词的对齐
label_token_align, pred_token_align = token_align("".join(lable_sentence_norm), "".join(pred_sentence_norm))
 
# 获得每个句子结束字词对应的索引值
label_sentence_end_index = sentence_end_index(lable_sentence_norm, label_token_align)
pred_sentence_end_index = sentence_end_index(pred_sentence_norm, pred_token_align)
 
# 获取少切分和多切分的句子数目
miss_count = len(set(label_sentence_end_index) - set(pred_sentence_end_index))
more_count = len(set(pred_sentence_end_index) - set(label_sentence_end_index))

文本归一化

文本归一化的背景

借用 SpeechIO 中的示例,假设一段语音的识别答案是 “这块黄金重达 324.75 克”,而服务的识别结果是 “这块黄金重达三百二十四点七五克”。如果使用者只关注语音识别结果的内容是否正确,不关注是文字还是数字,那么服务的识别结果就可以看作是正确的。

因此需要进行文本归一化来避免直接匹配导致服务识别结果被看作部分字词是错误的。

在中英文进行语音识别时,采用了 SpeechIO 的中英文归一化的功能,具体如下。

中文文本归一化

仓库链接:chinese_text_normalization/python/cn_tn.py at master · speechio/chinese_text_normalization · GitHub 或者可以使用 Leaderboard/utils/textnorm_zh.py at master · SpeechColab/Leaderboard · GitHub

使用代码如下:

import cn_tn as textnorm
 
def text_norm(sentence)
	normalizer = textnorm.TextNorm(
		to_banjiao=True,
		to_upper=True,
		to_lower=False,
		remove_fillers=True,
		remove_erhua=False,
		check_chars=False,
		remove_space=False,
		cc_mode="",
	)
 
	return normalizer(sentence)

英文文本归一化

仓库链接: Leaderboard/utils/textnorm_en.py at master · SpeechColab/Leaderboard · GitHub

使用代码如下,当前只简单的适配了下,没有将其转换为一个合理的包来使用。

def text_norm(sentence_list):
	pwd = os.path.dirname(os.path.abspath(__file__))
	with open('./predict.txt', 'w', encoding='utf-8') as fp:
		for idx, sentence in enumerate(sentence_list):
			fp.write('%s\t%s\n' % (idx, sentence))
 
	subprocess.run(
		f'PYTHONPATH={pwd}/utils/speechio python {pwd}/utils/speechio/textnorm_en.py --has_key --to_upper ./predict.txt ./predict_norm.txt',
		shell=True,
		check=True,
	)
 
	sentence_norm = []
	with open('./predict_norm.txt', 'r', encoding='utf-8') as fp:
		for line in fp.readlines():
			line_split_result = line.strip().split('\t', 1)
			if len(line_split_result) >= 2:
				sentence_norm.append(line_split_result[1])
	return sentence_norm

RTF

TODO。