中文文本智能纠错项目
任务简介
给定一段文本,找出其中可能存在错误的地方。
这是我近期所参与的实验室里的项目,在这个项目里,个人主要负责算法方面的工作。最终目标是实现一个给后台调用的接口。
思路
首先查找相关资料,看看有木有现成的方案。
在文本纠错这一方面,github上有一个比较出名的开源项目——pycorrector。pycorrector里面集成了多个文本纠错模型,而且可以调用自带的模型文件,上手十分方便。这也是前期本人研究了挺久的一个项目。不得不说,这里面资料十分全面,特别适合我这种小白学习。
在一段时间测试后,发现pycorrector里面的模型在文本拼写纠错方面有着不错的能力,但是缺乏语法纠错方面的能力。对于一段文本,如果其中存在一些错别字,pycorrector里面的模型可以比较好地进行识别以及纠正,但如果出现词语残缺,冗余或者乱序等问题,pycreector里面的模型并不能识别。
为了完善pycorrector语法纠错方面的不足,还需要再找一个具有语法纠错能力的模型。于是,后续计划采用双模型结合,实现文本纠错。
准备工作
考虑到双模型在不同环境中可能会产生冲突,所以应当先确定一个环境再配置双模型。
经过各种卸载重装,最终版本采用的环境为 Ubuntu22.04 LTS系统 + python3.7解释器,其他依赖的配置按官方网站的介绍来就好。实测在这个环境下配置下面两个模型一路绿灯。因为个人电脑为Windows系统,所以通过WSL2运行Ubuntu子系统。这里吹一下WSL,配合本地Windows系统的VS Code的远程连接,在不使用桌面服务的情况下也能拥有跟本地一样的开发效率。另外多提一嘴,如果想要配置Ubuntu桌面的话,建议在安装完桌面服务后,下个xrdp,然后利用微软自带的rdp进行远程连接,画质清晰得跟本地一样而且很流畅。
拼写纠错
在pycorrector里面的多个文本纠错模型中,最后选择了采用MacBert模型。该模型具体使用可参考这里。
评估结果
因训练集中包含SIGHAN2015,故在SIGHAN2015上达到了SOTA效果
| 模型 | Backbone | 评估数据集 | Precision | Recall | F1 |
|---|---|---|---|---|---|
| *MacBert* | *macbert-base-chinese* | *SIGHAN2015* | *0.8489* | *0.7035* | *0.7694* |
| *MacBert* | *macbert-base-chinese* | *COUR500* | *0.9301* | *0.5786* | *0.7134* |
语法纠错
网络上可以简单上手的语法纠错模型比较少,最后采用modelscope里的BART文本纠错模型。具体使用可参考原网址。
评估结果
将双模型结合后,在CGED 2021 数据集上使用官方脚本评测结果如下:
Correct Units: 711 Units With Errors: 1583
Error Type Counts in All Units: 2360 Error Count: 3205 Errors Need Correction: 4165
=====================
System Correct Units: 1002 System Units With Errors: 1293
System Error Type Counts in All Units: 1566 System Error Count: 1900 System Corrected Erros:1525
=====================
False Positive Rate = 0.157524613220816 ( 112 / 711)
=====================
Detction Level
Pre = 0.912606341840681 (1180 / 1293)
Rec = 0.745420088439672 (1180 / 1583)
F1 = 0.820584144645341 (2* 0.912606341840681 * 0.745420088439672 /( 0.912606341840681 + 0.745420088439672 ))
=====================
Identification Level
Pre = 0.741379310344828 (1161 / 1566)
Rec = 0.491949152542373 (1161 / 2360)
F1 = 0.59144167091187 (2* 0.741379310344828 * 0.491949152542373 / ( 0.741379310344828+0.491949152542373 ))
=====================
Postion Level
Pre = 0.306842105263158 ( 583 / 1900 )
Rec = 0.181903276131045 ( 583 / 3205 )
F1 = 0.228403525954946 ( 2 * 0.306842105263158 * 0.181903276131045 /( 0.306842105263158+0.181903276131045 ) )
=====================
Correction Level
Pre = 0.237377049180328 ( 362 / 1525 )
Rec = 0.0869147659063625 ( 362 / 4165 )
F1 = 0.127240773286467 ( 2 * 0.237377049180328 * 0.0869147659063625 /( 0.237377049180328 + 0.0869147659063625 ) )
Comprehensive score = 0.402542528699656
END=
功能函数设计
为了更好实现纠错功能接口以及与前端交互,需要添加一些功能函数。
句子切分
在文本纠错模型中,纠错的单位一般是句子。故在实际使用中需要先完成句子切分。
'''
src_line:原文本,字符串类型
end_chars: 切分句子的标志
result_list: 切分后的句子,列表类型
'''
def get_textlist(src_line):
end_chars = ['。', '!', '!', '?', '?', '……', ';', ';','\n',' ']
result_list = []
str_list = []
for c in src_line:
if c in end_chars:
str_list.append(c)
result_list.append(''.join(str_list))
str_list.clear()
else:
str_list.append(c)
if str_list:
result_list.append(''.join(str_list))
str_list.clear()
return result_list文本差异比对
在bart模型中,纠错返回的结果是句子,为方便前端进行标注,需要实现一个比对功能,根据两个句子的不同返回标注所需要的信息。
'''
import Levenshtein
src: 原文本
tgt: 修改后的文本
result: 形式如(原文本中需要修改的起始位置,终止位置,错误类型,应当替换成的词)的列表
'''
def get_corrlist(src,tgt):
src_line = src.replace(',', ',')
tgt_line = tgt.replace(',', ',')
_edits = Levenshtein.opcodes(src_line[::-1], tgt_line[::-1])[::-1]
edits = []
src_len = len(src_line)
tgt_len = len(tgt_line)
for edit in _edits:
edits.append((edit[0], src_len - edit[2], src_len - edit[1], tgt_len - edit[4], tgt_len - edit[3]))
merged_edits = []
for edit in edits:
if edit[0] == 'equal':
continue
if len(merged_edits) > 0:
last_edit = merged_edits[-1]
if last_edit[0] == 'insert' and edit[0] == 'insert' and last_edit[2] == edit[1]:
new_edit = ('insert', last_edit[1], edit[2], last_edit[3], edit[4])
merged_edits[-1] = new_edit
elif last_edit[2] == edit[1]:
assert last_edit[4] == edit[3]
new_edit = ('hybrid', last_edit[1], edit[2], last_edit[3], edit[4])
merged_edits[-1] = new_edit
elif last_edit[0] == 'insert' and edit[0] == 'delete' \
and tgt_line[last_edit[3]:last_edit[4]] == src_line[edit[1]:edit[2]]:
new_edit = ('luanxu', last_edit[1], edit[2], last_edit[3], edit[4])
merged_edits[-1] = new_edit
elif last_edit[0] == 'delete' and edit[0] == 'insert':
if src_line[last_edit[1]:last_edit[2]] == tgt_line[edit[3]:edit[4]]:
new_edit = ('luanxu', last_edit[1], edit[2], last_edit[3], edit[4])
merged_edits[-1] = new_edit
elif edit[4] < len(tgt_line) and tgt_line[edit[3]] == tgt_line[edit[4]] and src_line[last_edit[1]:last_edit[2]] == tgt_line[edit[3]+1:edit[4]+1]:
new_edit = ('luanxu', last_edit[1], edit[2]+1, last_edit[3], edit[4])
merged_edits[-1] = new_edit
else:
merged_edits.append(edit)
else:
merged_edits.append(edit)
else:
merged_edits.append(edit)
merged_edits2 = []
for edit in merged_edits:
if edit[0] == 'equal':
continue
if len(merged_edits2) > 0:
last_edit = merged_edits2[-1]
if last_edit[0] == 'insert' and edit[0] == 'insert' and last_edit[2] == edit[1]:
new_edit = ('insert', last_edit[1], edit[2], last_edit[3], edit[4])
merged_edits2[-1] = new_edit
elif last_edit[2] == edit[1]:
assert last_edit[4] == edit[3]
new_edit = ('hybrid', last_edit[1], edit[2], last_edit[3], edit[4])
merged_edits2[-1] = new_edit
elif last_edit[0] == 'insert' and edit[0] == 'delete' \
and tgt_line[last_edit[3]:last_edit[4]] == src_line[edit[1]:edit[2]]:
new_edit = ('luanxu', last_edit[1], edit[2], last_edit[3], edit[4])
merged_edits2[-1] = new_edit
elif last_edit[0] == 'delete' and edit[0] == 'insert':
if src_line[last_edit[1]:last_edit[2]] == tgt_line[edit[3]:edit[4]]:
new_edit = ('luanxu', last_edit[1], edit[2], last_edit[3], edit[4])
merged_edits2[-1] = new_edit
elif edit[4] < len(tgt_line) and tgt_line[edit[3]] == tgt_line[edit[4]] and src_line[last_edit[1]:last_edit[2]] == tgt_line[edit[3]+1:edit[4]+1]:
new_edit = ('luanxu', last_edit[1], edit[2]+1, last_edit[3], edit[4])
merged_edits2[-1] = new_edit
else:
merged_edits2.append(edit)
else:
merged_edits2.append(edit)
else:
merged_edits2.append(edit)
result = []
for edit in merged_edits2:
if tgt_line[edit[3]:edit[4]] == '[UNK]':
continue
if edit[0] == "insert":
result.append((str(edit[1]+1), str(edit[1]+1), "insert", tgt_line[edit[3]:edit[4]]))
elif edit[0] == "replace":
result.append((str(edit[1]+1), str(edit[2]), "replace", tgt_line[edit[3]:edit[4]]))
elif edit[0] == "delete":
result.append((str(edit[1]+1), str(edit[2]), "delete"))
elif edit[0] == "hybrid":
result.append((str(edit[1]+1), str(edit[2]), "hybrid", tgt_line[edit[3]:edit[4]]))
elif edit[0] == "luanxu":
result.append((str(edit[1]+1), str(edit[2]) , "luanxu"))
return result纠错接口设计
这里采用flask框架设计接口
from flask import Flask,request,redirect
import json
import Levenshtein
from modelscope.pipelines import pipeline
from modelscope.utils.constant import Tasks
from pycorrector.macbert.macbert_corrector import MacBertCorrector
app=Flask(__name__)
model_id = 'damo/nlp_bart_text-error-correction_chinese'
pipeline = pipeline(Tasks.text_error_correction, model=model_id)
m = MacBertCorrector()
@app.route("/")
def home():
return redirect("/static/index.html")
@app.route("/getcorrection",methods=["POST"])
def check():
srctext = request.get_data()
srctext = str(srctext, 'utf-8')
result_list = get_textlist(srctext)
ans = ""
for seq in result_list:
if seq == '':
continue
if len(seq) > 100 or len(seq) <= 2:
ans += seq
else:
result = pipeline(seq)['output']
correct_sent, err = m.macbert_correct(result)
if len(correct_sent) != len(result):
correct_sent = result
result = correct_sent
ans += result
return get_corrlist(srctext,ans)
if __name__ == "__main__":
app.run(host='0.0.0.0',port=8080,debug=True) 最终的接口效果如下:
后记
虽然接口完成后我的本职工作就完成了,不过为了能更好地利用这台服务器,自己也独立地稍微做一些前端,写了个简单的页面。点此跳转。
因为双模型下对性能消耗过大,所以该页面暂时就只使用Macbert单模型进行纠错。
至此,关于这个项目的研究也告一段落了。
