这是我的第470篇原创文章。

一、引言

词错率(Word Error Rate, WER)是一项用于评价ASR性能的重要指标,用来评价预测文本与标准文本之间错误率,因此词错率最大的特点是越小越好。像英语、阿拉伯语语音转文本或语音识别任务中研究者常用WER衡量ASR效果好坏。

因为英文语句中句子的最小单位是单词,而中文语句中的最小单位是汉字,因此在中文语音转文本任务或中文语音识别任务中使用字错率(Character Error Rate, CER)来衡量中文ASR效果好坏。

两者计算方式相同,为行文统一,下文统一使用WER表示该性能。

它们的计算公式是:

图片

假设有一个参考例句Ref和一段ASR系统转写语音后生成的预测文本Hyp。带入上面公式,S表示将Hyp转化为Ref时发生的替换数量,D表示将Hyp转化为Ref时发生的删除数量,I代表将Hypo转化为Ref时发生的插入数量,N代表Ref句子中总的字数或者英文单词数。C代表Hyp句子中识别正确的字数。即原参考句子总字数N = S+ D + C。

再说一句,根据维基百科里面的说法,N就是原样例文本总的字数。

二、实现过程

函数:

def levenshtein_distance(hypothesis: list, reference: list):
    """编辑距离
    计算两个序列的levenshtein distance,可用于计算 WER/CER
    词错率(Word Error Rate, WER)是一项用于评价ASR性能的重要指标,用来评价预测文本与标准文本之间错误率,因此词错率最大的特点是越小越好。
    像英语、阿拉伯语语音转文本或语音识别任务中研究者常用WER衡量ASR效果好坏。
    因为英文语句中句子的最小单位是单词,而中文语句中的最小单位是汉字,
    因此在中文语音转文本任务或中文语音识别任务中使用字错率(Character Error Rate, CER)来衡量中文ASR效果好坏。
    两者计算方式相同,为行文统一,下文统一使用WER表示该性能。
    WER = (S+D+I)/N = (S+D+I)/(S+D+C)
    参考资料:
        https://www.cuelogic.com/blog/the-levenshtein-algorithm
        https://martin-thoma.com/word-error-rate-calculation/
    N: number         真实序列的字数
    C: correct        预测序列识别正确的字数
    W: wrong          预测序列识别错误的字数
    I: insert         相较于真实序列,预测序列错误插入的数量
    D: delete         相较于真实序列,预测序列错误删除的数量
    S: substitution   相较于真实序列,预测序列错误替换的数量
    :param hypothesis: 预测序列
    :param reference: 真实序列
    :return: 1: 错误操作,所需要的 S,D,I 操作的次数;
             2: ref 与 hyp 的所有对齐下标
             3: 返回N、C、W、S、D、I 各自的数量
    """
    len_hyp = len(hypothesis)
    len_ref = len(reference)
    cost_matrix = np.zeros((len_hyp + 1, len_ref + 1), dtype=np.int16)
    # 记录所有的操作,0-equal;1-insertion;2-deletion;3-substitution
    ops_matrix = np.zeros((len_hyp + 1, len_ref + 1), dtype=np.int8)
    for i in range(len_hyp + 1):
        cost_matrix[i][0] = i
    for j in range(len_ref + 1):
        cost_matrix[0][j] = j
    # 生成 cost 矩阵和 operation矩阵,i:外层hyp,j:内层ref
    for i in range(1, len_hyp + 1):
        for j in range(1, len_ref + 1):
            if hypothesis[i-1] == reference[j-1]:
                cost_matrix[i][j] = cost_matrix[i-1][j-1]
            else:
                substitution = cost_matrix[i-1][j-1] + 1
                insertion = cost_matrix[i-1][j] + 1
                deletion = cost_matrix[i][j-1] + 1
                # compare_val = [insertion, deletion, substitution]   # 优先级
                compare_val = [substitution, insertion, deletion]   # 优先级
                min_val = min(compare_val)
                operation_idx = compare_val.index(min_val) + 1
                cost_matrix[i][j] = min_val
                ops_matrix[i][j] = operation_idx
    match_idx = []  # 保存 hyp与ref 中所有对齐的元素下标
    i = len_hyp
    j = len_ref
    nb_map = {"N": len_ref, "C": 0, "W": 0, "I": 0, "D": 0, "S": 0}
    while i >= 0 or j >= 0:
        i_idx = max(0, i)
        j_idx = max(0, j)
        if ops_matrix[i_idx][j_idx] == 0:     # correct
            if i-1 >= 0 and j-1 >= 0:
                match_idx.append((j-1, i-1))
                nb_map['C'] += 1
            # 出边界后,这里仍然使用,应为第一行与第一列必然是全零的
            i -= 1
            j -= 1
        # elif ops_matrix[i_idx][j_idx] == 1:   # insert
        elif ops_matrix[i_idx][j_idx] == 2:   # insert
            i -= 1
            nb_map['I'] += 1
        # elif ops_matrix[i_idx][j_idx] == 2:   # delete
        elif ops_matrix[i_idx][j_idx] == 3:   # delete
            j -= 1
            nb_map['D'] += 1
        # elif ops_matrix[i_idx][j_idx] == 3:   # substitute
        elif ops_matrix[i_idx][j_idx] == 1:   # substitute
            i -= 1
            j -= 1
            nb_map['S'] += 1
        # 出边界处理
        if i < 0 and j >= 0:
            nb_map['D'] += 1
        elif j < 0 and i >= 0:
            nb_map['I'] += 1
    match_idx.reverse()
    wrong_cnt = cost_matrix[len_hyp][len_ref]
    nb_map["W"] = wrong_cnt
    try:
        wer = (nb_map["S"] + nb_map["D"] + nb_map["I"])/(nb_map["S"] + nb_map["D"] + nb_map["C"])
    except:
        wer = float('inf')
    # print("ref: %s" % " ".join(reference))
    # print("hyp: %s" % " ".join(hypothesis))
    # print(nb_map)
    # print("match_idx: %s" % str(match_idx))
    return wrong_cnt, match_idx, nb_map, wer

举例1:Ref: 你吃了吗 Hyp: 你吃了么

标准文本为“你吃了吗”,转写结果为“你吃了么”,上例发生了一次错误替换,Hyp将“吗”替换成了么,即S=1,D=0, I =0, 参考文本字数N=4, 因此本次转写结果WER= 1/4 = 25%.

代码:

wrong_cnt, match_idx, nb_map, wer = levenshtein_distance(list("你吃了么"), list("你吃了吗"))
print(wrong_cnt, match_idx, nb_map, wer)

结果:

图片

举例2:Ref: 今天天气很好 Hyp: 今天天气很好啊

举例2中,Hyp文本相比标准文本错误插入了一个“啊”,即S=0,D=0,I =1,N=6,因此字错率WER= 1/6 = 16.7%。

代码:​​​​​​​

wrong_cnt, match_idx, nb_map, wer = levenshtein_distance(list("今天天气很好啊"), list("今天天气很好"))
print(wrong_cnt, match_idx, nb_map, wer)

结果:

图片

三、小结

句错率,SER,SER表述为句子中如果有一个词识别错误,那么这个句子被认为识别错误,句子识别错误的的个数,除以总的句子个数即为SER。

作者简介:

读研期间发表6篇SCI数据挖掘相关论文,现在某研究院从事数据算法相关科研工作,结合自身科研实践经历不定期分享关于Python、机器学习、深度学习、人工智能系列基础知识与应用案例。致力于只做原创,以最简单的方式理解和学习,关注我一起交流成长。需要数据集和源码的小伙伴可以关注底部公众号添加作者微信。​​​​​​​

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐