中文文本纠错(Chinese Spell Checking, CSC)任务各个论文的评价指标

本文说明

本文汇总了中文文本纠错(Chinese Spell Checking)任务在各个开源项目中的评价指标,他们虽然写法不同,但大部分本质是相同的,但也有少部分论文的评价指标存在问题或其他论文不一致,本文对他们的指标代码进行了分析,并说明了其中的问题。

评价指标总结

中文文本纠错通常使用精准率(Precision)、召回率(Recall)和F1-Score作为评价指标,有如下四种:

  • Character-level Detection Metrics:少数论文使用了。意思是:按字为维度统计,能检测出错字的情况;就目前来看,大部分论文的该指标统计方式相同。
  • Character-level Correction Metrics:少数论文使用了。意思是:按字为维度统计,能正确纠正字的情况;目前找到有三篇论文使用了该指标,但多多少少都存在问题。(如果大家找到哪个开源项目使用了该指标,欢迎在评论区提醒,我会补充进来)
  • Sentence-level Detection Metrics:大部分论文使用了。意思是:按句子为维度统计,能检测出句子存在错字的情况。大部分论文对该指标统计方式相同。
  • Sentence-level Correction Metrics::几乎所有论文都使用了。意思是:按句子为维度,能完全正确修改句子的情况。大部分论文对该指标统计方式相同。

下面我将会使用混淆矩阵的方式给出这四种指标的定义,会用到的术语如下:①该纠:表示该句子(汉字)中存在错字。②不该纠:表示该句子(汉字)中不存在错字;③纠了:表示模型对该句子(汉字)进行了改错。④未纠:表示模型未对该句子(汉字)做任何修改

Character-level Detection Metrics:

实际值 / 预测值 P N
P (TP) 该纠的字,纠了,纠没纠对不管 (FN) 该纠的字,没纠
N (FP) 不该纠的字,纠了 (TN) 不该纠的字,未纠

Character-level Correction Metrics:

实际值 / 预测值 P N
P (TP) 该纠的字,纠了也纠对了。 (FN) 该纠的字,没纠或没纠对
N (FP) 不该纠的字,纠了。 (TN) 不该纠的字,没纠

目前我看到只有PLOME和Confusionset-guided Pointer Networks这两篇论文的开源项目用了这个指标,但它们好像都有点问题。具体可以看下面对论文指标的详细解析。

Sentence-level Detection Metrics:

实际值 / 预测值 P N
P (TP) 该纠,且该纠的字,都纠了,纠没纠对不管;不该纠的字,都没纠 (FN) 该纠,但未纠或把不该纠的字纠了
N (FP) 不该纠,但纠了 (TN) 不该纠,未纠

Sentence-level Correction Metrics:

实际值 / 预测值 P N
P (TP) 该纠,且纠对了 (FN)该纠,未纠或纠错了
N (FP) 不该纠,但纠了 (TN) 不该纠,未纠

顺手写一下指标公式:

  • 准确率(Accuracy): (TP+TN) / (TP+FP+TN+FN)
  • 精准率(Precision): TP / (TP + FP)
  • 召回率(Recall): TP / (TP+FN)
  • F1-Score: (2 * Precision * Recall) / (Precsion + Recall)

各开源项目使用的评价指标

SIGHAN(官方)

Sentence-level Detection Metrics:

实际值 / 预测值 P N
P (TP) 该纠,且该纠的字,都纠了,纠没纠对不管;不该纠的字,都没纠 (FN) 该纠,但未纠或把不该纠的字纠了
N (FP) 不该纠,但纠了 (TN) 不该纠,未纠

Sentence-level Correction Metrics:

实际值 / 预测值 P N
P (TP) 该纠,且纠对了 (FN)该纠,未纠或纠错了
N (FP) 不该纠,但纠了 (TN) 不该纠,未纠

SIGHAN官方提供的工具是Java的,反编译后的部分代码如下:

while(...) {
    
    
	String id = (String)var20.next();
	// gct为真实值,rct为预测值。
	// 数据格式为:(位置, 字)
	// 例如:[(1, 鸡), (5, 美)],表示1位置字应该改成鸡,5位置字应该改成美
    Map<Integer, String> gctMap = (Map)gtMap.get(id);
    Map<Integer, String> rctMap = (Map)rtMap.get(id);
    if (gctMap.size() == 0) {
    
      // 不该纠(句子没错)
        if (rctMap.size() == 0) {
    
     // 不该纠,没纠
            dtnSet.add(id);  // detect TN
            itnSet.add(id);  // correct TN (这里的i是Identification)
        } else {
    
      // 不该纠,但纠了
            dfpSet.add(id);  // detect FP
            ifpSet.add(id);  // correct FP
        }
    } else if (rctMap.size() == 0) {
    
      // 该纠,但没纠
        dfnSet.add(id); // detect FN
        ifnSet.add(id); // correct FN
    } else if (gctMap.keySet().containsAll(rctMap.keySet()) 
			   && gctMap.size() == rctMap.size()) {
    
     // 该纠,纠了,纠的位置也对。
        if (gctMap.values().containsAll(rctMap.values())) {
    
     // 该纠,纠对了
            dtpSet.add(id); // detect TP
            itpSet.add(id); // correct TP
        } else {
    
     // 该纠,且该纠的字,都纠了,但是有些字没纠对。
            dtpSet.add(id); // detect TP
            ifnSet.add(id); // correct FN
        }
    } else {
    
     // 该纠,纠了,但纠了不该纠的字。
        dfnSet.add(id); // detect FN
        ifnSet.add(id); // correct FN
    }
}

double fp = (double)dfpSet.size() / (double)(dfpSet.size() + dtnSet.size());
double daccuracy = ((double)dtpSet.size() + (double)dtnSet.size()) / (double)rtList.size();
double dprecision = (double)dtpSet.size() / (double)(dtpSet.size() + dfpSet.size());
double drecall = (double)dtpSet.size() / (double)(dtpSet.size() + dfnSet.size());
double df1Score = 2.0D * dprecision * drecall / (dprecision + drecall);
double iaccuracy = ((double)itpSet.size() + (double)itnSet.size()) / (double)rtList.size();
double iprecision = (double)itpSet.size() / (double)(itpSet.size() + ifpSet.size());
double irecall = (double)itpSet.size() / (double)(itpSet.size() + ifnSet.size());
double if1Score = 2.0D * iprecision * irecall / (iprecision + irecall);

Confusionset-guided Pointer Networks


Character-level Detection Metrics:

实际值 / 预测值 P N
P (TP) 该纠的字,纠了,纠没纠对不管 (FN) 该纠的字,没纠
N (FP) 不该纠的字,纠了 (TN) 不该纠的字,未纠
for ... in ...:
    gold_index = ... # 错字的位置。例如: [1, 3, 5]表示该句话1,3,5位置是错字
    predict_index = ... # 预测错字的位置。例如: [3, 5, 7]表示模型对3,5,7位置的字进行了纠错
    for i in predict_index:
        if i in gold_index:
            TP += 1  # 该纠的字,纠了,纠没纠对不管
        else:
            FP += 1  # 不该纠的字,纠了
    for i in gold_index:
        if i in predict_index:
            continue
        else:
            FN += 1	 # 该纠的字,但没纠

detection_precision = TP / (TP + FP) if (TP+FP) > 0 else 0
detection_recall = TP / (TP + FN) if (TP+FN) > 0 else 0
detection_f1 = 2 * (detection_precision * detection_recall) / (detection_precision + detection_recall) if (detection_precision + detection_recall) > 0 else 0

Character-level Correction Metrics:

作者与正常的Character-level Correction Metrics不一样,他只考虑了“该纠的字,纠了”这样的场景,其他场景不在该指标的考虑范围内。因此,本次不对该指标进行讨论。

for i in range(len(all_predict_true_index)):
    # we only detect those correctly detected location, which is a different from the common metrics since
    # we wanna to see the precision improve by using the confusionset
    if len(all_predict_true_index[i]) > 0:
        predict_words = []
        for j in all_predict_true_index[i]:
            predict_words.append(results[i][2][j])
            if results[i][1][j] == results[i][2][j]:
                TP += 1
            else:
                FP += 1
        for j in all_gold_index[i]:
            if results[i][1][j]  in predict_words:
                continue
            else:
                FN += 1

correction_precision = TP / (TP + FP) if (TP+FP) > 0 else 0
correction_recall = TP / (TP + FN) if (TP+FN) > 0 else 0
correction_f1 = 2 * (correction_precision * correction_recall) / (correction_precision + correction_recall) if (correction_precision + correction_recall) > 0 else 0

PLOME


Character-level Detection Metrics:

实际值 / 预测值 P N
P (TP) 该纠的字,纠了,纠没纠对不管 (FN) 该纠的字,没纠
N (FP) 不该纠的字,纠了 (TN) 不该纠的字,未纠

Character-level Correction Metrics:

实际值 / 预测值 P N
P (TP) 该纠的字,纠了也纠对了。 (FN) 该纠的字,没纠
N (FP) 该纠的字,纠了,但没纠对。 (TN) 不该纠的字,没纠

Character-level Correction Metrics这个指标应该有问题,这个FP应该不对。对于“不该纠的字,但纠了”这种场景没有被统计进去。例如:ori=‘张’, god=‘张’, prd=‘李’,这种场景没有被统计到Character-level Correction指标中。

for ... in ...:
     ori_txt = ... # 原字
     god_txt = ... # 正确字
     prd_txt = ... # 预测字
     
     # 不该纠的字,没纠。即TN
     if ori_txt == god_txt and ori_txt == prd_txt:
         continue
     if ori != god: # 该纠
         total_gold_err += 1 # 相当于(TP+FN)
     if prd != ori: # 纠了
         total_pred_err += 1 # 相当于(TP+FP)
     if (ori != god) and (prd != ori):  # 该纠,且纠了
         check_right_pred_err += 1  # 该纠,且纠了,不管对没对
         if god == prd: # 该纠,且纠对了
             right_pred_err += 1

# Detect P, R, F1
p = 1. * check_right_pred_err / (total_pred_err + 0.001)
r = 1. * check_right_pred_err / (total_gold_err + 0.001)
f = 2 * p * r / (p + r +  1e-13)

# Correct P, R, F1
pc = 1. * right_pred_err / (check_right_pred_err + 0.001) # TP/(TP+FP)
rc = 1. * right_pred_err / (total_gold_err + 0.001)	# TP/(TP+FN)
fc = 2 * pc * rc / (pc + rc + 1e-13) 

Sentence-level Detection Metrics:

实际值 / 预测值 P N
P (TP) 该纠,且该纠的字,都纠了,纠没纠对不管;不该纠的字,都没纠 (FN) 该纠,但未纠或把不该纠的字纠了
N (FP) 不该纠,但纠了 (TN) 不该纠,未纠

Sentence-level Correction Metrics

实际值 / 预测值 P N
P (TP) 该纠,且纠对了 (FN)该纠,未纠或纠错了
N (FP) 不该纠,但纠了 (TN) 不该纠,未纠
for ... in ...:
	# errs存的是错字位置,例如:[1, 5],表示1和5位置上有错字
    gold_errs = ... # Label
    pred_errs = ... # 预测结果
	# tags存的是错字位置即改正后的字。例如:[(1, 鸡), (5, 美)],表示1位置字应该改成鸡,5位置字应该改成美
    god_tags = ... # Label
	pred_tags = .. # 预测结果
    # 该纠
    if len(gold_errs) > 0:
        total_gold_err += 1  # 相当于TP+FN
    
    # 纠了
    if len(pred_errs) > 0:
        total_pred_err += 1 # 相当于TP+FP
        if gold_errs == pred_errs: # 该纠的字都纠了,不该纠的字都没纠,纠没纠对不管
            check_right_pred_err += 1
        if god_tags == prd_tags: # 该纠,纠对了
            right_pred_err += 1

# Sentence-level Detection Metrics
p = 1. * check_right_pred_err / total_pred_err
r = 1. * check_right_pred_err / total_gold_err
f = 2 * p * r / (p + r + 1e-13)

# Sentence-level Correction Metrics
p = 1. * right_pred_err / total_pred_err
r = 1. * right_pred_err / total_gold_err
f = 2 * p * r / (p + r + 1e-13)

ReaLiSe

ReaLiSe字段说明:

  • targ/pred:存的是错字位置即改正后的字。例如:[(1, 鸡), (5, 美)],表示1位置字应该改成鸡,5位置字应该改成美

Sentence-level Detection Metrics:

实际值 / 预测值 P N
P (TP) 该纠,且该纠的字,都纠了,纠没纠对不管;不该纠的字,都没纠 (FN) 该纠,但未纠或把不该纠的字纠了
N (FP) 不该纠,但纠了 (TN) 不该纠,未纠
for ... in ...:
	# 该纠的句子(句子有错字)
    if targ != []:
        targ_p += 1	# 相当于TP+FN
    # 纠了的句子(句子有没有错字不知道,模型认为有)
    if pred != []:
        pred_p += 1	# 相当于TP+FP
    # 不该纠,没纠;或 该纠,且该纠的字,都纠了,纠没纠对不管;不该纠的字,都没纠
    if len(pred) == len(targ) and all(p[0] == t[0] for p, t in zip(pred, targ)):
        hit += 1
    # 该纠,且该纠的字,都纠了,纠没纠对不管;不该纠的字,都没纠
    if pred != [] and len(pred) == len(targ) and all(p[0] == t[0] for p, t in zip(pred, targ)):
        tp += 1
     
acc = hit / len(targs)
p = tp / pred_p
r = tp / targ_p
f1 = 2 * p * r / (p + r) if p + r > 0 else 0.0

Sentence-level Correction Metrics:

实际值 / 预测值 P N
P (TP) 该纠,且纠对了 (FN)该纠,未纠或纠错了
N (FP) 不该纠,但纠了 (TN) 不该纠,未纠
for ... in ...:
	# 该纠的句子(句子有错字)
    if targ != []:
        targ_p += 1	# 相当于TP+FN
    # 纠了的句子(句子有没有错字不知道,模型认为有)
    if pred != []:
        pred_p += 1	# 相当于TP+FP
    # 不该纠,没纠;或 该纠,纠对了
    if pred == targ:
        hit += 1
    # 该纠,纠对了
    if pred != [] and pred == targ:
        tp += 1

acc = hit / len(targs)
p = tp / pred_p
r = tp / targ_p
f1 = 2 * p * r / (p + r) if p + r > 0 else 0.0

SpellGCN

SpellGCN字段说明:

  • detect_token:存的是错字所在的位置。例如: [1,5,7]表示1,5,7三个位置上的字存在错误。如果没有错字,则为[0]
  • correct_zip/correct_tokens:存的是错字位置即改正后的字。例如:[(1, 鸡), (5, 美)],表示1位置字应该改成鸡,5位置字应该改成美

Character-level Detection Metrics:

实际值 / 预测值 P N
P (TP) 该纠的字,纠了,纠没纠对不管 (FN) 该纠的字,没纠
N (FP) 不该纠的字,纠了 (TN) 不该纠的字,未纠
for ... in ...:
	# 该纠的句子(句子中存在错字)
	if detect_actual_tokens[0]!=0:
		# 该纠的字,纠了,纠没纠对不管。
		# 例如:label为(1, 3, 5), pred为(3, 5, 7),则TP+=2,因为对两个该纠的字纠了
        detect_TP += len(set(max_detect_pred_tokens) & set(detect_actual_tokens)) 
        # 该纠的字,没纠
        # 例如:label为(1, 3, 5), pred为(3, 5, 7),FN+=1,因为1位置该纠但没纠
        detect_FN += len(set(detect_actual_tokens) - set(max_detect_pred_tokens))
    
    # 不该纠的字,但纠了
    # 例如:label为(1, 3, 5), pred为(3, 5, 7),FP+=1,因为7位置不该纠,但纠了
    detect_FP += len(set(max_detect_pred_tokens) - set(detect_actual_tokens)) 
    
detect_precision = detect_TP * 1.0 / (detect_TP + detect_FP)
detect_recall = detect_TP * 1.0 / (detect_TP + detect_FN)
detect_F1 = 2. * detect_precision * detect_recall/ ((detect_precision + detect_recall) + 1e-8)

Character-level Correction Metrics

实际值 / 预测值 P N
P (TP) 该纠的字,纠了也纠对了。 (FN) 纠了,但没纠对的字。(包括这个字本身有错和没错两种情况)
N (FP) 该纠的字,没纠对或没纠。 (TN) 不该纠的字,没纠

这个指标好像有问题,因为“该纠,但没纠对的字”会被FN和FP重复计算。详情可以参考下面代码中的注释

for ... in ...:
	"""
	该纠的字,纠了也纠对了。
    例如:label为[(1, '鸡'), (3, '你'), (5, '太')], 
         pred为[(1, '坤'), (3, '你'), (5, '太'), (7, '美')]
         则TP+=2。  因为“你,太”两个字该纠且纠对了
	"""
	correct_TP += len(set(correct_pred_tokens) & set(zip(detect_actual_tokens,correct_actual_tokens))) 
	"""
	纠了,但没纠对的字。(包括这个字本身有错和没错两种情况)
    例如:label为[(1, '鸡'), (3, '你'), (5, '太')], 
         pred为[(1, '坤'), (3, '你'), (5, '太'), (7, '美')]
         则FP+=2。  因为'坤'字纠了,但没纠对。'美'字纠了,但7位置本身没错,所以也没纠对
	"""
    correct_FP += len(set(correct_pred_tokens) - set(zip(detect_actual_tokens,correct_actual_tokens)))
    """
	该纠的字,但没纠对或没纠。
    例如:label为[(1, '鸡'), (3, '你'), (5, '太'), (9, '美')], 
         pred为[(1, '坤'), (3, '你'), (5, '太')]
         则FN+=2。 因为'鸡'字没纠对,'美'字该纠但没纠
    这里这个指标好像出现了问题,对于'坤'字的错误预测,在FP和FN被重复计算了。
	"""
    correct_FN += len(set(zip(detect_actual_tokens,correct_actual_tokens)) - set(correct_pred_tokens)) 

correct_precision = correct_TP * 1.0 / (correct_TP + correct_FP)
correct_recall = correct_TP * 1.0 / (correct_TP + correct_FN)
correct_F1 = 2. * correct_precision * correct_recall/ ((correct_precision + correct_recall) + 1e-8)

Sentence-level Detection Metrics:

实际值 / 预测值 P N
P (TP) 该纠,且该纠的字,都纠了,纠没纠对不管;不该纠的字,都没纠 (FN) 该纠,但未纠或把不该纠的字纠了
N (FP) 不该纠,但纠了 (TN) 不该纠,未纠
for ... in ...:
	# 不管该不该纠,反正纠了
	if detect_pred_tokens[0] !=  0:
        sent_P += 1	# 相当于TP+FP
    # 该纠的
    if detect_actual_tokens[0] != 0:
        sent_N += 1  # 相当于(TP+FN)
        if sorted(detect_actual_tokens) == sorted(detect_pred_tokens): 
		    detect_sent_TP += 1

detect_sent_precision = detect_sent_TP * 1.0 / (sent_P)
detect_sent_recall = detect_sent_TP * 1.0 / (sent_N)
detect_sent_F1 = 2. * detect_sent_precision * detect_sent_recall/ ((detect_sent_precision + detect_sent_recall) + 1e-8)

Sentence-level Correction Metrics

实际值 / 预测值 P N
P (TP) 该纠,且纠对了 (FN)该纠,未纠或纠错了
N (FP) 不该纠,但纠了 (TN) 不该纠,未纠

代码如下:

for ... in ...:
	# 不管该不该纠,反正纠了
	if detect_pred_tokens[0] !=  0:	# 表示预测句子中存在错字
        sent_P += 1	 # 相当于TP+FP
        # 该纠的,且纠对了(因为纠了,且纠对了,说明句子该纠)
        if sorted(correct_pred_zip) == sorted(correct_actual_zip):
          correct_sent_TP += 1
    # 该纠的
    if detect_actual_tokens[0] != 0:
        sent_N += 1	# 相当于(TP+FN)
	
correct_sent_precision = correct_sent_TP * 1.0 / (sent_P)
correct_sent_recall = correct_sent_TP * 1.0 / (sent_N)
correct_sent_F1 = 2. * correct_sent_precision * correct_sent_recall/ ((correct_sent_precision + correct_sent_recall) + 1e-8)

PyCorrector


Sentence-level Correction Metrics

实际值 / 预测值 P N
P (TP) 该纠,且纠对了 (FN)该纠,未纠或纠错了
N (FP) 不该纠,但纠了 (TN) 不该纠,未纠

代码如下:

for ... in ...:
	# 负样本,不该纠的
	if src == tgt:
	    # 预测也为负
	    if tgt == tgt_pred:
	        TN += 1
	    # 预测为正
	    # 不该纠的,但是纠了,为FP
	    else:
	        FP += 1
	# 正样本,该纠错的
	else:
	    # 预测也为正
	    # 该纠错的句子,且纠对了,为TP
	    if tgt == tgt_pred:
	        TP += 1
	    # 预测为负
	    # 该纠的,没纠或者纠错了,为FN
	    else:
	        FN += 1
	
	total_num += 1
acc = (TP + TN) / total_num
precision = TP / (TP + FP) if TP > 0 else 0.0
recall = TP / (TP + FN) if TP > 0 else 0.0
f1 = 2 * precision * recall / (precision + recall) if precision + recall != 0 else 0

猜你喜欢

转载自blog.csdn.net/zhaohongfei_358/article/details/129079461