├── tools ├── README.md ├── data_clean │ └── README.md ├── data_augment │ └── README.md └── segment │ ├── README.md │ ├── segment_bert.py │ ├── segment_pkunlp.py │ └── tokenization.py ├── models ├── seq2edit-based-CGEC │ ├── plm │ │ └── chinese-struct-bert-large │ │ │ ├── added_tokens.json │ │ │ ├── tokenizer_config.json │ │ │ ├── special_tokens_map.json │ │ │ └── config.json │ ├── data │ │ └── output_vocabulary_chinese_char_hsk+lang8_5 │ │ │ ├── .lock │ │ │ ├── non_padded_namespaces.txt │ │ │ └── d_tags.txt │ ├── pipeline.sh │ ├── gector │ │ ├── seq2labels_metric.py │ │ └── datareader.py │ ├── predict.py │ ├── utils │ │ └── helpers.py │ └── tokenization.py └── seq2seq-based-CGEC │ ├── pipeline.sh │ ├── predict.py │ ├── utils.py │ └── train.py ├── pics └── errors.PNG ├── guidelines ├── update_1.pdf ├── update_2.pdf └── CGEC_guidelines.pdf ├── scorers ├── ChERRANT │ ├── data │ │ └── cilin.txt │ ├── requirements.txt │ ├── modules │ │ ├── __pycache__ │ │ │ ├── merger.cpython-38.pyc │ │ │ ├── alignment.cpython-38.pyc │ │ │ ├── annotator.cpython-38.pyc │ │ │ ├── tokenizer.cpython-38.pyc │ │ │ └── classifier.cpython-38.pyc │ │ ├── annotator.py │ │ ├── tokenizer.py │ │ ├── classifier.py │ │ └── tokenization.py │ ├── utils │ │ └── __pycache__ │ │ │ └── char_smi.cpython-38.pyc │ ├── samples │ │ ├── demo.input │ │ ├── demo.hyp │ │ ├── demo.hyp.para │ │ ├── demo.hyp.m2.word │ │ ├── demo.hyp.m2 │ │ ├── demo.hyp.m2.char │ │ ├── demo.ref.m2.word │ │ └── demo.ref.m2.char │ ├── demo.sh │ ├── ensemble.sh │ ├── evaluate.sh │ ├── README.md │ ├── m2convertor.py │ ├── rule_ensemble.py │ ├── README.en.md │ └── parallel_to_m2.py └── README.md ├── requirements_seq2seq.txt ├── requirements_seq2edit.txt ├── data ├── README.md ├── utils.py └── MuCGEC │ ├── README.md │ └── README.en.md ├── README.md ├── README.en.md └── LICENSE /tools/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tools/data_clean/README.md: -------------------------------------------------------------------------------- 1 | In progress -------------------------------------------------------------------------------- /tools/data_augment/README.md: -------------------------------------------------------------------------------- 1 | In progress -------------------------------------------------------------------------------- /models/seq2edit-based-CGEC/plm/chinese-struct-bert-large/added_tokens.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /models/seq2edit-based-CGEC/data/output_vocabulary_chinese_char_hsk+lang8_5/.lock: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pics/errors.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HillZhang1999/MuCGEC/HEAD/pics/errors.PNG -------------------------------------------------------------------------------- /models/seq2edit-based-CGEC/plm/chinese-struct-bert-large/tokenizer_config.json: -------------------------------------------------------------------------------- 1 | {"init_inputs": []} -------------------------------------------------------------------------------- /guidelines/update_1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HillZhang1999/MuCGEC/HEAD/guidelines/update_1.pdf -------------------------------------------------------------------------------- /guidelines/update_2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HillZhang1999/MuCGEC/HEAD/guidelines/update_2.pdf -------------------------------------------------------------------------------- /guidelines/CGEC_guidelines.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HillZhang1999/MuCGEC/HEAD/guidelines/CGEC_guidelines.pdf -------------------------------------------------------------------------------- /scorers/ChERRANT/data/cilin.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HillZhang1999/MuCGEC/HEAD/scorers/ChERRANT/data/cilin.txt -------------------------------------------------------------------------------- /models/seq2edit-based-CGEC/data/output_vocabulary_chinese_char_hsk+lang8_5/non_padded_namespaces.txt: -------------------------------------------------------------------------------- 1 | *tags 2 | *labels 3 | -------------------------------------------------------------------------------- /scorers/ChERRANT/requirements.txt: -------------------------------------------------------------------------------- 1 | ltp==4.1.3.post1 2 | OpenCC==1.1.2 3 | python-Levenshtein==0.12.1 4 | pypinyin 5 | tqdm -------------------------------------------------------------------------------- /scorers/README.md: -------------------------------------------------------------------------------- 1 | M2Scorer下载链接:https://github.com/nusnlp/m2scorer 2 | 3 | Download link of M2Scorer: https://github.com/nusnlp/m2scorer -------------------------------------------------------------------------------- /models/seq2edit-based-CGEC/data/output_vocabulary_chinese_char_hsk+lang8_5/d_tags.txt: -------------------------------------------------------------------------------- 1 | CORRECT 2 | INCORRECT 3 | @@UNKNOWN@@ 4 | @@PADDING@@ 5 | -------------------------------------------------------------------------------- /scorers/ChERRANT/modules/__pycache__/merger.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HillZhang1999/MuCGEC/HEAD/scorers/ChERRANT/modules/__pycache__/merger.cpython-38.pyc -------------------------------------------------------------------------------- /scorers/ChERRANT/utils/__pycache__/char_smi.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HillZhang1999/MuCGEC/HEAD/scorers/ChERRANT/utils/__pycache__/char_smi.cpython-38.pyc -------------------------------------------------------------------------------- /scorers/ChERRANT/modules/__pycache__/alignment.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HillZhang1999/MuCGEC/HEAD/scorers/ChERRANT/modules/__pycache__/alignment.cpython-38.pyc -------------------------------------------------------------------------------- /scorers/ChERRANT/modules/__pycache__/annotator.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HillZhang1999/MuCGEC/HEAD/scorers/ChERRANT/modules/__pycache__/annotator.cpython-38.pyc -------------------------------------------------------------------------------- /scorers/ChERRANT/modules/__pycache__/tokenizer.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HillZhang1999/MuCGEC/HEAD/scorers/ChERRANT/modules/__pycache__/tokenizer.cpython-38.pyc -------------------------------------------------------------------------------- /requirements_seq2seq.txt: -------------------------------------------------------------------------------- 1 | datasets==1.4.1 2 | ltp==4.1.3.post1 3 | OpenCC==1.1.2 4 | pyarrow 5 | python-Levenshtein==0.12.1 6 | torch==1.8.1 7 | transformers==4.4.1 8 | numpy==1.21.4 -------------------------------------------------------------------------------- /scorers/ChERRANT/modules/__pycache__/classifier.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HillZhang1999/MuCGEC/HEAD/scorers/ChERRANT/modules/__pycache__/classifier.cpython-38.pyc -------------------------------------------------------------------------------- /requirements_seq2edit.txt: -------------------------------------------------------------------------------- 1 | allennlp==1.3.0 2 | ltp==4.1.3.post1 3 | OpenCC==1.1.2 4 | pyarrow 5 | python-Levenshtein==0.12.1 6 | torch==1.7.1 7 | transformers==4.0.1 8 | numpy==1.21.1 9 | -------------------------------------------------------------------------------- /tools/segment/README.md: -------------------------------------------------------------------------------- 1 | PKUNLP分词工具需要提前下载[[Link]](https://drive.google.com/file/d/1HEpN85XZarlnQmKYEMGg156384GQh9ZC/view?usp=sharing),下载完成后解压到当前目录下(即`./segger`)即可。NLPCC18的评测需要先对结果使用该工具进行分词。 -------------------------------------------------------------------------------- /models/seq2edit-based-CGEC/plm/chinese-struct-bert-large/special_tokens_map.json: -------------------------------------------------------------------------------- 1 | {"unk_token": "[UNK]", "sep_token": "[SEP]", "pad_token": "[PAD]", "cls_token": "[CLS]", "mask_token": "[MASK]"} -------------------------------------------------------------------------------- /models/seq2edit-based-CGEC/plm/chinese-struct-bert-large/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "attention_probs_dropout_prob": 0.1, 3 | "hidden_act": "gelu", 4 | "hidden_dropout_prob": 0.1, 5 | "hidden_size": 1024, 6 | "initializer_range": 0.02, 7 | "intermediate_size": 4096, 8 | "max_position_embeddings": 512, 9 | "num_attention_heads": 16, 10 | "num_hidden_layers": 24, 11 | "type_vocab_size": 2, 12 | "vocab_size": 21128, 13 | "attention_type": "self" 14 | } -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | 我们依托CCL2022会议举办了汉语学习者语法纠错评测(CLTC),并在阿里云天池平台放出了MuCGEC数据集供大家下载和打榜,欢迎大家积极参与评测,有丰厚的奖金等奖励。 2 | 3 | With the support of the [CCL-2022](http://cips-cl.org/static/CCL2022/index.html) conference, we held the Chinese learner text correction shared task (CLTC), and released the whole MuCGEC dataset on the Alibaba Tianchi platform [https://tianchi.aliyun.com/dataset/dataDetail?dataId=131328](https://tianchi.aliyun.com/dataset/dataDetail?dataId=131328) for everyone to download and test. You are welcome to participate in our shared task, with generous bonuses and other rewards. 4 | -------------------------------------------------------------------------------- /tools/segment/segment_bert.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import tokenization 3 | from tqdm import tqdm 4 | import os 5 | dir_path = os.path.dirname(os.path.realpath(__file__)) 6 | 7 | tokenizer = tokenization.FullTokenizer(vocab_file=os.path.join(dir_path, "vocab.txt"), do_lower_case=False) 8 | 9 | for line in tqdm(sys.stdin): 10 | line = line.strip() 11 | origin_line = line 12 | line = line.replace(" ", "") 13 | line = tokenization.convert_to_unicode(line) 14 | if not line: 15 | continue 16 | tokens = tokenizer.tokenize(line) 17 | print(' '.join(tokens)) -------------------------------------------------------------------------------- /tools/segment/segment_pkunlp.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import tokenization 4 | from tqdm import tqdm 5 | from segger.pkunlp import Segmentor 6 | 7 | file_path = os.path.dirname(os.path.abspath(__file__)) 8 | tokenizer = Segmentor(os.path.join(file_path, "segger/feature/segment.feat"), os.path.join(file_path, "segger/feature/segment.dic")) 9 | 10 | with open(sys.argv[1], "w", encoding="utf-8") as f: 11 | for line in tqdm(sys.stdin): 12 | line = "".join(line.strip().split()) 13 | origin_line = line 14 | if not line: 15 | continue 16 | tokens = tokenizer.seg_string(line) 17 | # print(" ".join(tokens)) 18 | f.write(' '.join(tokens) + "\n") 19 | -------------------------------------------------------------------------------- /scorers/ChERRANT/samples/demo.input: -------------------------------------------------------------------------------- 1 | 冬阴功是泰国最著名的菜之一,它虽然不是很豪华,但它的味确实让人上瘾,做法也不难、不复杂。 2 | 首先,我们得准备:大虾六到九只、盐一茶匙、已搾好的柠檬汁三汤匙、泰国柠檬叶三叶、柠檬香草一根、鱼酱两汤匙、辣椒6粒,纯净水4量杯、香菜半量杯和草菇10个。 3 | 这样,你就会尝到泰国人死爱的味道。 4 | 另外,冬阴功对外国人的喜爱不断地增加。 5 | 这部电影不仅是国内,在国外也很有名。 6 | 不管是真正的冬阴功还是电影的“冬阴功”,都在人们的心里刻骨铭心。 7 | 随着中国经济突飞猛近,建造工业与日俱增。 8 | 虽然工业的发展和城市规模的扩大对经济发展有积极作用,但是同时也对环境问题日益严重造成了空气污染问题。 9 | 那些空气污染也没有助于人生的身体建康。 10 | 由此可见,首先我们要了解一些关于空气污染对我们人生有什么危害的话题知道了这些常识以后对我们人类会有积极作用。以及要学会怎样应对和治理空气污染的问题。 11 | 任何事情都是各有利弊,众所周知越建立工业越对经济方面有所发展。 12 | 对我看来,曼古空气污染的问题与日俱增。 13 | 每天会有不少的毒气体泄漏从工厂里出来。 14 | 在工厂里的工作人员为了工作,而每天吸了不少的毒气体,经过了一年多的时间,连工作人员也得了严重的病。更不用说住在这家工厂近的家庭。 15 | 沙尘暴也是一类空气污染之一。 16 | 不官是从口、眼、鼻子进去这样会伤害身体的建康。 17 | 这样做会避免受到沙尘暴。 18 | 最后,要关主一些关于天气预报的新闻。 19 | 中国,悠久的历史,灿烂的文化,真是在历史上最难忘的国家。 20 | 对一个生名来说空气污染是很危害的问题,对身体不好。 -------------------------------------------------------------------------------- /scorers/ChERRANT/samples/demo.hyp: -------------------------------------------------------------------------------- 1 | 冬阴功是泰国最著名的菜之一,它虽然不是很豪华,但它的味道确实让人上瘾,做法也不难、不复杂。 2 | 首先,我们得准备:大虾六到九只、盐一茶匙、已搾好的柠檬汁三汤匙、泰国柠檬叶三叶、柠檬香草一根、鱼酱两汤匙、辣椒6粒,纯净水4量杯、香菜半量杯和草菇10个。 3 | 这样,你就会尝到泰国人死爱的味道。 4 | 另外,冬阴功对外国人的喜爱也不断地增加。 5 | 这部电影不仅是在国内,在国外也很有名。 6 | 不管是真正的冬阴功还是电影里的“冬阴功”,都在人们的心里刻骨铭刻。 7 | 随着中国经济突飞猛快,建造工业与日俱增。 8 | 虽然工业的发展和城市规模的扩大对经济发展有积极作用,但是同时也使环境问题日益严重造成了空气污染问题。 9 | 那些空气污染也没有助于人们的身体健康。 10 | 由此可见,首先我们要了解一些关于空气污染对我们人生有什么危害的话题,知道了这些常识以后对我们人类会有积极作用。并且要学会怎何应对和治理空气污染的问题。 11 | 任何事情都是各有利弊,众所周知越建立工业对经济方面越有发展。 12 | 在我看来,曼古空气污染的问题与日俱增。 13 | 每天会有不少的有毒气体从工厂里出来。 14 | 在工厂里的工作人员为了工作,而每天吸不少的毒气体,经过了一年多的时间,连工作人员也得了严重的病。更不用说住在这家工厂附近的家庭。 15 | 沙尘暴也是一类空气污染之一。 16 | 不管是从口、眼、鼻子进去,这样都会伤害身体的健康。 17 | 这样做会避免受到沙尘暴。 18 | 最后,要关注一些关于天气预报的新闻。 19 | 中国,悠久的历史,灿烂的文化,真是历史上最难忘的国家。 20 | 对一个生人来说空气污染是很严重的问题,对身体不好。 -------------------------------------------------------------------------------- /scorers/ChERRANT/demo.sh: -------------------------------------------------------------------------------- 1 | INPUT_FILE=./samples/demo.input 2 | OUTPUT_FILE=./samples/demo.hyp 3 | HYP_PARA_FILE=./samples/demo.hyp.para 4 | HYP_M2_FILE=./samples/demo.hyp.m2.char 5 | REF_M2_FILE=./samples/demo.ref.m2.char 6 | 7 | # Step1. extract edits from hypothesis file. 8 | 9 | paste $INPUT_FILE $OUTPUT_FILE | awk '{print NR"\t"$p}' > $HYP_PARA_FILE # only for single hypothesis situation 10 | 11 | python parallel_to_m2.py -f $HYP_PARA_FILE -o $HYP_M2_FILE -g char # char-level evaluation 12 | 13 | # Step2. compare hypothesis edits with reference edits. 14 | 15 | python compare_m2_for_evaluation.py -hyp $HYP_M2_FILE -ref $REF_M2_FILE 16 | 17 | # Note: you can also extract the reference edits yourself by using parallel_to_m2.py if you have reference sentences. 18 | # You need to process the data into the following format: id \t source \t reference1 \t reference2 \t ... \n 19 | 20 | # word-level evaluation 21 | HYP_M2_FILE=./samples/demo.hyp.m2.word 22 | REF_M2_FILE=./samples/demo.ref.m2.word 23 | python parallel_to_m2.py -f $HYP_PARA_FILE -o $HYP_M2_FILE -g word 24 | python compare_m2_for_evaluation.py -hyp $HYP_M2_FILE -ref $REF_M2_FILE 25 | -------------------------------------------------------------------------------- /scorers/ChERRANT/ensemble.sh: -------------------------------------------------------------------------------- 1 | # Seq2Edit的三个模型,更换了3个随机种子 2 | RESULT_FILE1=$1 3 | RESULT_FILE2=$2 4 | RESULT_FILE3=$3 5 | 6 | # Seq2Seq的三个模型,更换了3个随机种子 7 | RESULT_FILE4=$4 8 | RESULT_FILE5=$5 9 | RESULT_FILE6=$6 10 | 11 | # for ((i=1; i<=6; i++)) 12 | # do 13 | # THRESHOLD=$i 14 | # RESULT_DIR=./ensemble_results/3seq2edit_3seq2seq_threshold_$THRESHOLD 15 | # mkdir -p $RESULT_DIR 16 | 17 | # M2_OUTPUT_FILE=$RESULT_DIR/MuCGEC_test.m2_temp 18 | # OUTPUT_FILE=$RESULT_DIR/MuCGEC_test.output 19 | 20 | # python edit_ensemble.py --result_path $RESULT_FILE1 $RESULT_FILE2 $RESULT_FILE3 $RESULT_FILE4 $RESULT_FILE5 $RESULT_FILE6 --output_path $M2_OUTPUT_FILE --threshold $THRESHOLD 21 | 22 | # python m2convertor.py -f $M2_OUTPUT_FILE -o $OUTPUT_FILE 23 | # done 24 | 25 | THRESHOLD=4 # threshold 一般取 N/2+1,即当编辑出现次数大于等于该值时,才会保留 26 | RESULT_DIR=./ensemble_results/3seq2edit_3seq2seq_threshold_$THRESHOLD 27 | mkdir -p $RESULT_DIR 28 | 29 | M2_OUTPUT_FILE=$RESULT_DIR/MuCGEC_test.m2_temp 30 | OUTPUT_FILE=$RESULT_DIR/MuCGEC_test.output 31 | 32 | python rule_ensemble.py --result_path $RESULT_FILE1 $RESULT_FILE2 $RESULT_FILE3 $RESULT_FILE4 $RESULT_FILE5 $RESULT_FILE6 --output_path $M2_OUTPUT_FILE --threshold $THRESHOLD 33 | 34 | python m2convertor.py -f $M2_OUTPUT_FILE -o $OUTPUT_FILE 35 | -------------------------------------------------------------------------------- /scorers/ChERRANT/samples/demo.hyp.para: -------------------------------------------------------------------------------- 1 | 1 冬阴功是泰国最著名的菜之一,它虽然不是很豪华,但它的味确实让人上瘾,做法也不难、不复杂。 冬阴功是泰国最著名的菜之一,它虽然不是很豪华,但它的味道确实让人上瘾,做法也不难、不复杂。 2 | 2 首先,我们得准备:大虾六到九只、盐一茶匙、已搾好的柠檬汁三汤匙、泰国柠檬叶三叶、柠檬香草一根、鱼酱两汤匙、辣椒6粒,纯净水4量杯、香菜半量杯和草菇10个。 首先,我们得准备:大虾六到九只、盐一茶匙、已搾好的柠檬汁三汤匙、泰国柠檬叶三叶、柠檬香草一根、鱼酱两汤匙、辣椒6粒,纯净水4量杯、香菜半量杯和草菇10个。 3 | 3 这样,你就会尝到泰国人死爱的味道。 这样,你就会尝到泰国人死爱的味道。 4 | 4 另外,冬阴功对外国人的喜爱不断地增加。 另外,冬阴功对外国人的喜爱也不断地增加。 5 | 5 这部电影不仅是国内,在国外也很有名。 这部电影不仅是在国内,在国外也很有名。 6 | 6 不管是真正的冬阴功还是电影的“冬阴功”,都在人们的心里刻骨铭心。 不管是真正的冬阴功还是电影里的“冬阴功”,都在人们的心里刻骨铭刻。 7 | 7 随着中国经济突飞猛近,建造工业与日俱增。 随着中国经济突飞猛快,建造工业与日俱增。 8 | 8 虽然工业的发展和城市规模的扩大对经济发展有积极作用,但是同时也对环境问题日益严重造成了空气污染问题。 虽然工业的发展和城市规模的扩大对经济发展有积极作用,但是同时也使环境问题日益严重造成了空气污染问题。 9 | 9 那些空气污染也没有助于人生的身体建康。 那些空气污染也没有助于人们的身体健康。 10 | 10 由此可见,首先我们要了解一些关于空气污染对我们人生有什么危害的话题知道了这些常识以后对我们人类会有积极作用。以及要学会怎样应对和治理空气污染的问题。 由此可见,首先我们要了解一些关于空气污染对我们人生有什么危害的话题,知道了这些常识以后对我们人类会有积极作用。并且要学会怎何应对和治理空气污染的问题。 11 | 11 任何事情都是各有利弊,众所周知越建立工业越对经济方面有所发展。 任何事情都是各有利弊,众所周知越建立工业对经济方面越有发展。 12 | 12 对我看来,曼古空气污染的问题与日俱增。 在我看来,曼古空气污染的问题与日俱增。 13 | 13 每天会有不少的毒气体泄漏从工厂里出来。 每天会有不少的有毒气体从工厂里出来。 14 | 14 在工厂里的工作人员为了工作,而每天吸了不少的毒气体,经过了一年多的时间,连工作人员也得了严重的病。更不用说住在这家工厂近的家庭。 在工厂里的工作人员为了工作,而每天吸不少的毒气体,经过了一年多的时间,连工作人员也得了严重的病。更不用说住在这家工厂附近的家庭。 15 | 15 沙尘暴也是一类空气污染之一。 沙尘暴也是一类空气污染之一。 16 | 16 不官是从口、眼、鼻子进去这样会伤害身体的建康。 不管是从口、眼、鼻子进去,这样都会伤害身体的健康。 17 | 17 这样做会避免受到沙尘暴。 这样做会避免受到沙尘暴。 18 | 18 最后,要关主一些关于天气预报的新闻。 最后,要关注一些关于天气预报的新闻。 19 | 19 中国,悠久的历史,灿烂的文化,真是在历史上最难忘的国家。 中国,悠久的历史,灿烂的文化,真是历史上最难忘的国家。 20 | 20 对一个生名来说空气污染是很危害的问题,对身体不好。 对一个生人来说空气污染是很严重的问题,对身体不好。 21 | -------------------------------------------------------------------------------- /data/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | def remove_special_tags(): 4 | file = "./MuCGEC_CGED_Dev.para" 5 | src_file = "./MuCGEC_CGED_Dev.src" 6 | tgt_file = "./MuCGEC_CGED_Dev.tgt" 7 | 8 | with open(file, "r", encoding="utf-8") as f: 9 | with open(src_file, "w", encoding="utf-8") as o1: 10 | with open(tgt_file, "w", encoding="utf-8") as o2: 11 | for line in f: 12 | li = line.rstrip("\n").split("\t")[1:] 13 | src = li[0] 14 | for tgt in li[1:]: 15 | if tgt == "无法标注" or tgt == "没有错误": 16 | tgt = src 17 | o1.write(src + "\n") 18 | o2.write(tgt + "\n") 19 | 20 | def get_erroneous_pair(): 21 | src_file = "./train_data/lang8+hsk/train.src" 22 | tgt_file = "./train_data/lang8+hsk/train.tgt" 23 | with open(src_file, "r", encoding="utf-8") as f1: 24 | with open(tgt_file, "r", encoding="utf-8") as f2: 25 | with open(src_file + "_only_erroneous", "w", encoding="utf-8") as o1: 26 | with open(tgt_file + "_only_erroneous", "w", encoding="utf-8") as o2: 27 | src_lines = f1.readlines() 28 | tgt_lines = f2.readlines() 29 | for src_line, tgt_line in zip(src_lines, tgt_lines): 30 | if src_line != tgt_line: 31 | o1.write(src_line + "\n") 32 | o2.write(tgt_line + "\n") -------------------------------------------------------------------------------- /scorers/ChERRANT/evaluate.sh: -------------------------------------------------------------------------------- 1 | RESULT_DIR=$1 2 | 3 | INPUT_FILE=../../data/test_data/MuCGEC/MuCGEC-ALL/MuCGEC_ALL_Test.input 4 | OUTPUT_FILE=$RESULT_DIR"/MuCGEC_test.output" 5 | NLPCC18_OFFICIAL_M2=../../data/test_data/NLPCC18-orig/NLPCC18-orig.m2.pkunlp # m2scorer, word-based 6 | MuCGEC_ALL_M2=../../data/test_data/MuCGEC/MuCGEC-ALL/MuCGEC_ALL_Test.m2.char # ChERRANT, char-based 7 | 8 | if [ ! -f $RESULT_DIR"/MuCGEC_test.pred.m2.char" ]; then 9 | paste $INPUT_FILE $OUTPUT_FILE | awk '{print NR"\t"$p}' > $RESULT_DIR"/MuCGEC_test.pred.para" 10 | 11 | CUDA_VISIBLE_DEVICES=0 python parallel_to_m2.py -f $RESULT_DIR"/MuCGEC_test.pred.para" -o $RESULT_DIR"/MuCGEC_test.pred.m2.char" -g char -s all 12 | fi 13 | 14 | if [ ! -f $RESULT_DIR"/MuCGEC_test.output.pku_tok" ]; then 15 | head -n 2000 $OUTPUT_FILE | python ../../tools/segment/segment_pkunlp.py $RESULT_DIR"/MuCGEC_test.output.pku_tok" 16 | fi 17 | 18 | echo "****************NLPCC18-official-MaxMatch****************" 19 | 20 | python2 ../m2scorer/scripts/m2scorer.py $RESULT_DIR"/MuCGEC_test.output.pku_tok" $NLPCC18_OFFICIAL_M2 21 | 22 | echo "****************MuCGEC-NLPCC18-ChERRANT****************" 23 | 24 | python compare_m2_for_evaluation.py -hyp $RESULT_DIR"/MuCGEC_test.pred.m2.char" -ref $MuCGEC_ALL_M2 --start 0 --end 2000 25 | 26 | echo "****************MuCGEC-LANG8-ChERRANT****************" 27 | 28 | python compare_m2_for_evaluation.py -hyp $RESULT_DIR"/MuCGEC_test.pred.m2.char" -ref $MuCGEC_ALL_M2 --start 2000 --end 4000 29 | 30 | echo "****************MuCGEC-CGED-ChERRANT****************" 31 | 32 | python compare_m2_for_evaluation.py -hyp $RESULT_DIR"/MuCGEC_test.pred.m2.char" -ref $MuCGEC_ALL_M2 --start 4000 --end 6000 33 | 34 | echo "****************MuCGEC-ALL-ChERRANT****************" 35 | 36 | python compare_m2_for_evaluation.py -hyp $RESULT_DIR"/MuCGEC_test.pred.m2.char" -ref $MuCGEC_ALL_M2 37 | -------------------------------------------------------------------------------- /models/seq2seq-based-CGEC/pipeline.sh: -------------------------------------------------------------------------------- 1 | # Step 1. Data preprocessing 2 | DATA_DIR=./exp_data/lang8+hsk_only_erroneous 3 | mkdir -p $DATA_DIR 4 | 5 | TRAIN_SRC_FILE=../../data/train_data/lang8+hsk/train.src_only_erroneous # 每行一个病句 6 | TRAIN_TGT_FILE=../../data/train_data/lang8+hsk/train.tgt_only_erroneous # 每行一个正确句子,和病句一一对应 7 | if [ ! -f $DATA_DIR"/train.json" ]; then 8 | python ./utils.py $TRAIN_SRC_FILE $TRAIN_TGT_FILE $DATA_DIR"/train.json" 9 | fi 10 | 11 | VALID_SRC_FILE=../../data/valid_data/MuCGEC_CGED_Dev.src # 每行一个病句 12 | VALID_TGT_FILE=../../data/valid_data/MuCGEC_CGED_Dev.tgt # 每行一个正确句子,和病句一一对应 13 | if [ ! -f $DATA_DIR"/valid.json" ]; then 14 | python ./utils.py $VALID_SRC_FILE $VALID_TGT_FILE $DATA_DIR"/valid.json" 15 | fi 16 | 17 | # Step 2. Training 18 | SEED=2021 19 | PRETRAIN_MODEL=fnlp/bart-large-chinese 20 | MODEL_DIR=./exps/lang8+lang8_bart-large-chinese 21 | TASK_NAME=gec 22 | CUDA_DEVICE=0,1,2,3,4,5,6,7 23 | 24 | mkdir -p $MODEL_DIR/$TASK_NAME/run_$SEED/src_bak 25 | cp ./pipeline.sh $MODEL_DIR/$TASK_NAME/run_$SEED/src_bak 26 | cp train.py $MODEL_DIR/$TASK_NAME/run_$SEED/src_bak 27 | cp predict.py $MODEL_DIR/$TASK_NAME/run_$SEED/src_bak 28 | 29 | CUDA_VISIBLE_DEVICES=$CUDA_DEVICE python -u train.py \ 30 | --do_train \ 31 | --do_eval \ 32 | --model_path $PRETRAIN_MODEL \ 33 | --save_path $MODEL_DIR \ 34 | --task $TASK_NAME \ 35 | --data_dir $DATA_DIR \ 36 | --seed $SEED \ 37 | 38 | # Step 3. Inference 39 | MODEL_PATH=./exps/seq2seq_lang8 40 | RESULT_DIR=$MODEL_PATH/results 41 | mkdir -p $RESULT_DIR 42 | INPUT_FILE=../../data/test_data/MuCGEC/MuCGEC-ALL/MuCGEC_ALL_Test.input # 输入文件(无需分字) 43 | OUTPUT_FILE=$RESULT_DIR"/MuCGEC_test.output" # 输出文件 44 | 45 | echo "Generating..." 46 | SECONDS=0 47 | CUDA_VISIBLE_DEVICES=0 python -u predict.py \ 48 | --model_path $MODEL_PATH \ 49 | --input_path $INPUT_FILE \ 50 | --output_path $OUTPUT_FILE ; 51 | 52 | echo "Generating Finish!" 53 | duration=$SECONDS 54 | echo "$(($duration / 60)) minutes and $(($duration % 60)) seconds elapsed." 55 | 56 | -------------------------------------------------------------------------------- /scorers/ChERRANT/README.md: -------------------------------------------------------------------------------- 1 | # 使用说明 2 | 3 | 我们借鉴了英文上主流的GEC评估工具[ERRANT](https://github.com/chrisjbryant/errant),搭建了中文GEC评估工具ChERRANT(Chinese ERRANT)。ChERRANT的主要功能是通过对比预测编辑和标准编辑,计算预测结果的精确度、召回度、F值指标,从而评估语法纠错模型的性能。 4 | 5 | ## 环境 6 | 7 | `requirements.txt`包含了实验所需的主要环境,具体环境搭建流程如下所示: 8 | 9 | ``` 10 | conda create -n cherrant python==3.8 11 | conda activate cherrant 12 | pip install -r requirements.txt 13 | ``` 14 | 15 | ## 使用方式 16 | 17 | ### 总览 18 | 19 | #### 文件格式 20 | 21 | 预测文件格式为:`id \t 原句 \t 预测结果 `; 22 | 23 | 标准答案文件格式为:`id \t 原句 \t 标准答案1 \t 标准答案2 \t ... `; 24 | 25 | 编辑文件格式采用M2格式,如下所示: 26 | 27 | ``` 28 | S 冬 阴 功 是 泰 国 最 著 名 的 菜 之 一 , 它 虽 然 不 是 很 豪 华 , 但 它 的 味 确 实 让 人 上 瘾 , 做 法 也 不 难 、 不 复 杂 。 29 | T0-A0 冬 阴 功 是 泰 国 最 著 名 的 菜 之 一 , 它 虽 然 不 是 很 豪 华 , 但 它 的 味 道 确 实 让 人 上 瘾 , 做 法 也 不 难 、 不 复 杂 。 30 | A 27 27|||M|||道|||REQUIRED|||-NONE-|||0 31 | ``` 32 | 33 | + `S` 代表原句; 34 | + `T0-A0`代表第0个答案的第0个编辑序列(一个句子可能有多个答案,一个答案也可能有编辑距离相同的多个编辑序列); 35 | + `A `代表编辑,主要包括如下信息:错误的起始和结束位置(`27 27`);错误类型(`M`,Missing Error,缺失错误);错误的修改答案(`道`,即插入“道”);标注id(`0`); 36 | 37 | #### 评估过程 38 | 39 | 主要的评估步骤为: 40 | 41 | 1. 将标准答案平行文件通过`parallel_to_m2.py`转换成M2格式的编辑文件`gold.m2`(仅首次评估需要,之后可以复用); 42 | 2. 将预测答案平行文件通过`parallel_to_m2.py`转换成M2格式的编辑文件`hyp.m2`; 43 | 3. 使用`compare_m2_for_evaluation.py`对比`hyp.m2`和`gold.m2`,得到最终的评价指标。 44 | 45 | 完整流程的示例脚本可以参考`./demo.sh`。 46 | 47 | ### 抽取编辑 48 | 49 | 首先,将输入文件(每行一句)和输出文件(每行一句)合并,处理成平行格式: 50 | 51 | ``` 52 | INPUT_FILE=./samples/demo.input 53 | OUTPUT_FILE=./samples/demo.hyp 54 | HYP_PARA_FILE=./samples/demo.hyp.para 55 | 56 | paste $INPUT_FILE $OUTPUT_FILE | awk '{print NR"\t"$p}' > $HYP_PARA_FILE 57 | ``` 58 | 59 | 然后,通过如下命令抽取编辑: 60 | 61 | ``` 62 | HYP_M2_FILE=./samples/demo.hyp.m2.char 63 | 64 | python parallel_to_m2.py -f $HYP_PARA_FILE -o $HYP_M2_FILE -g char 65 | ``` 66 | 67 | 默认抽取的是字级别编辑。 68 | 69 | 通过将`-g`参数设为`word`,则可以抽取词级别编辑。虽然词级别编辑标注了更多的错误类型信息,但可能会受到分词错误影响,因此仅供参考。更多设置请参考命令行帮助文件: 70 | 71 | ``` 72 | python parallel_to_m2.py --help 73 | ``` 74 | 75 | ### 计算指标 76 | 77 | 使用如下脚本对比预测编辑文件和标准编辑文件,即可得到字级别的评测指标: 78 | 79 | ``` 80 | REF_M2_FILE=./samples/demo.ref.m2.char 81 | python compare_m2_for_evaluation.py -hyp $HYP_M2_FILE -ref $REF_M2_FILE 82 | ``` 83 | 84 | 字级别的F0.5指标是MuCGEC数据集采用的官方评测指标,评价结果如下所示: 85 | 86 | ``` 87 | =========== Span-Based Correction ============ 88 | TP FP FN Prec Rec F0.5 89 | 8 19 35 0.2963 0.186 0.2649 90 | ============================================== 91 | ``` 92 | 93 | 该程序也可支持更细粒度的信息展示,如展示检测指标和不同类型错误的纠正指标,请参考命令行帮助信息使用。 94 | 95 | ``` 96 | python compare_m2_for_evaluation.py --help 97 | ``` 98 | 99 | ## 引用 100 | 101 | 如果您使用了我们的评价程序,请引用我们的论文: 102 | 103 | #### MuCGEC: a Multi-Reference Multi-Source Evaluation Dataset for Chinese Grammatical Error Correction (Accepted by NAACL2022 main conference) [[PDF]](https://arxiv.org/pdf/2204.10994.pdf) 104 | 105 | ``` 106 | @inproceedings{zhang-etal-2022-mucgec, 107 | title = "{MuCGEC}: a Multi-Reference Multi-Source Evaluation Dataset for Chinese Grammatical Error Correction", 108 | author = "Zhang, Yue and Li, Zhenghua and Bao, Zuyi and Li, Jiacheng and Zhang, Bo and Li, Chen and Huang, Fei and Zhang, Min", 109 | booktitle = "Proceedings of NAACL-HLT", 110 | year = "2022", 111 | address = "Online", 112 | publisher = "Association for Computational Linguistics" 113 | ``` 114 | -------------------------------------------------------------------------------- /scorers/ChERRANT/modules/annotator.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | from modules.alignment import read_cilin, read_confusion, Alignment 3 | from modules.merger import Merger 4 | from modules.classifier import Classifier 5 | 6 | class Annotator: 7 | def __init__(self, 8 | align: Alignment, 9 | merger: Merger, 10 | classifier: Classifier, 11 | granularity: str = "word", 12 | strategy: str = "first"): 13 | self.align = align 14 | self.merger = merger 15 | self.classifier = classifier 16 | self.granularity = granularity 17 | self.strategy = strategy 18 | 19 | @classmethod 20 | def create_default(cls, granularity: str = "word", strategy: str = "first"): 21 | """ 22 | Default parameters used in the paper 23 | """ 24 | semantic_dict, semantic_class = read_cilin() 25 | confusion_dict = read_confusion() 26 | align = Alignment(semantic_dict, confusion_dict, granularity) 27 | merger = Merger(granularity) 28 | classifier = Classifier(granularity) 29 | return cls(align, merger, classifier, granularity, strategy) 30 | 31 | def __call__(self, 32 | src: List[Tuple], 33 | tgt: List[Tuple], 34 | annotator_id: int = 0, 35 | verbose: bool = False): 36 | """ 37 | Align sentences and annotate them with error type information 38 | """ 39 | src_tokens = [x[0] for x in src] 40 | tgt_tokens = [x[0] for x in tgt] 41 | src_str = "".join(src_tokens) 42 | tgt_str = "".join(tgt_tokens) 43 | # convert to text form 44 | annotations_out = ["S " + " ".join(src_tokens) + "\n"] 45 | if tgt_str == "没有错误" or src_str == tgt_str: # Error Free Case 46 | annotations_out.append(f"T{annotator_id} 没有错误\n") 47 | cors = [tgt_str] 48 | op, toks, inds = "noop", "-NONE-", (-1, -1) 49 | a_str = f"A {inds[0]} {inds[1]}|||{op}|||{toks}|||REQUIRED|||-NONE-|||{annotator_id}\n" 50 | annotations_out.append(a_str) 51 | elif tgt_str == "无法标注": # Not Annotatable Case 52 | annotations_out.append(f"T{annotator_id} 无法标注\n") 53 | cors = [tgt_str] 54 | op, toks, inds = "NA", "-NONE-", (-1, -1) 55 | a_str = f"A {inds[0]} {inds[1]}|||{op}|||{toks}|||REQUIRED|||-NONE-|||{annotator_id}\n" 56 | annotations_out.append(a_str) 57 | else: # Other 58 | align_objs = self.align(src, tgt) 59 | edit_objs = [] 60 | align_idx = 0 61 | if self.strategy == "first": 62 | align_objs = align_objs[:1] 63 | for align_obj in align_objs: 64 | edits = self.merger(align_obj, src, tgt, verbose) 65 | if edits not in edit_objs: 66 | edit_objs.append(edits) 67 | annotations_out.append(f"T{annotator_id}-A{align_idx} " + " ".join(tgt_tokens) + "\n") 68 | align_idx += 1 69 | cors = self.classifier(src, tgt, edits, verbose) 70 | # annotations_out = [] 71 | for cor in cors: 72 | op, toks, inds = cor.op, cor.toks, cor.inds 73 | a_str = f"A {inds[0]} {inds[1]}|||{op}|||{toks}|||REQUIRED|||-NONE-|||{annotator_id}\n" 74 | annotations_out.append(a_str) 75 | annotations_out.append("\n") 76 | return annotations_out, cors 77 | -------------------------------------------------------------------------------- /data/MuCGEC/README.md: -------------------------------------------------------------------------------- 1 | # MuCGEC数据集说明 2 | [English Version](https://github.com/HillZhang1999/MuCGEC/blob/main/data/MuCGEC/README.en.md) 3 | ## 基本信息 4 | 数据集名称:MuCGEC(Multi-Reference Multi-Source Evaluation Dataset for Chinese Grammatical Error Correction) 5 | 6 | 本次评测开放数据为: 7 | + 开发集:1137句(其中包含12句无法标注的句子) 8 | + 测试集:6000句(其中包含62句无法标注的句子) 9 | 数据集下载文件为:mucgec.zip,解压后包括: 10 | 11 | + MuCGEC_dev.txt:验证集,给出了每个句子的修改方式; 12 | + MuCGEC_test.txt:测试集,仅给出原句,但未给出修改方式,需要参赛人员自行预测; 13 | + example_pred_dev.txt:验证集预测结果示例; 14 | + filter_sentences.txt:需要参赛队员从训练集中筛除的句子; 15 | + README.md:数据集使用说明文件,请仔细阅读。 16 | 17 | **训练数据必须剔除在filter_sentences.txt中的句子。** 18 | 19 | ## 数据格式 20 | ### 开发集 21 | 22 | 我们提供了给定答案的开发集(`MuCGEC_dev.txt`)以供模型调优。开发集以txt文件的格式给出,基本数据格式为:`id,\t,原句,\t,标准答案1,\t,标准答案2,...`,如下所示: 23 | ``` 24 | 1 因为在冰箱里没什么东西也做很好吃的菜。 即使在冰箱里没什么东西也能做很好吃的菜。 即使在冰箱里没什么东西,也能做很好吃的菜。 25 | ``` 26 | 27 | ### 测试集 28 | 测试集同样以txt格式给出,每列之间以`\t`制表符隔开,但未给定标准答案,需要自行预测。如下所示: 29 | ``` 30 | 2 纳税和帐单小一典。 31 | ``` 32 | 33 | 参赛队员提交前需要预测出唯一的修改结果,并添加到每一行的末尾。如下所示: 34 | ``` 35 | 2 纳税和帐单小一典。 纳税和帐单小一点。 36 | ``` 37 | 38 | ## 使用说明 39 | ### 本地评测(开发集) 40 | 41 | 本次评测的排名依据是**字级别的F0.5指标**,指标计算工具地址为:https://github.com/HillZhang1999/MuCGEC/tree/main/scorers/ChERRANT 。 42 | 43 | 评测工具使用方法可以参考:https://github.com/HillZhang1999/MuCGEC/tree/main/scorers/ChERRANT/README.md 。 44 | 45 | 核心步骤为: 46 | 47 | 1. 将标准答案文件`MuCGEC_dev.txt`通过`parallel_to_m2.py`脚本转换成为M2格式`MuCGEC_dev.m2`(仅首次评测时需要,后续复用即可); 48 | ``` 49 | python parallel_to_m2.py -f MuCGEC_dev.txt -o MuCGEC_dev.m2 50 | ``` 51 | 2. 将预测结果文件`example_pred_dev.txt`通过`parallel_to_m2.py`脚本转换成为M2格式`example_pred_dev.m2`; 52 | ``` 53 | python parallel_to_m2.py -f example_pred_dev.txt -o example_pred_dev.m2 54 | ``` 55 | 3. 使用`compare_m2_for_evaluation.py`脚本对比`example_pred_dev.m2`和`MuCGEC_dev.m2`,得到最终的指标。 56 | ``` 57 | python compare_m2_for_evaluation.py -hyp example_pred_dev.m2 -ref MuCGEC_dev.m2 58 | ``` 59 | 60 | 所有预测结果在进行评测前均需要处理成和`example_pred_dev.txt`一致的格式。 61 | 62 | 如果评测过程无误,样例结果文件`example_pred_dev.txt`在开发集的字级别指标应为: 63 | 64 | ``` 65 | =========== Span-Based Correction ============ 66 | TP FP FN Prec Rec F0.5 67 | 1084 1635 3003 0.3987 0.2652 0.3622 68 | ============================================== 69 | ``` 70 | 71 | ### 在线提交(测试集) 72 | 73 | 对给定的测试集文件中的所有句子进行预测,所得结果添加到每行末尾,具体格式为:`id,\t,原句,\t,预测结果`。如: 74 | 75 | + 原文件: 76 | ``` 77 | 2 纳税和帐单小一典。 78 | ``` 79 | 80 | + 结果文件: 81 | ``` 82 | 2 纳税和帐单小一典。 纳税和帐单小一点。 83 | ``` 84 | 85 | 将所得结果文件仍命名为`MuCGEC_test.txt`,打包为zip文件上传至[天池平台](https://tianchi.aliyun.com/dataset/dataDetail?dataId=131328)。 86 | 87 | 需要注意: 88 | + zip文件内仅需要包含唯一的预测结果txt文件,不需要包含中间文件夹或其他文件。 89 | + 结果文件必须命名成`MuCGEC_test.txt`,且内容格式与`example_pred_dev.txt`一致。 90 | 91 | 不附合上述要求的提交,将无法正确返回测试集指标。 92 | 93 | ## 指标说明 94 | 95 | 在线提交后返回的评测指标包括: 96 | 97 | + TP:True Positive,真正例,即模型预测的编辑中正确的数目; 98 | + FP:False Positive,伪正例,即模型预测的编辑中错误的数目; 99 | + FN:False Negative,伪负例,即模型未预测到的正确编辑的数目; 100 | + Precision:精确度,等于TP/(TP+FP); 101 | + Recall:召回度,等于TP(TP+FN); 102 | + score:F0.5值。与传统的F1值相比,更看重模型的精确度,是GEC领域常用的指标。 103 | 104 | 上述指标中,score(即模型的F0.5值)是所有榜单排名的最终依据。 105 | 106 | ## 引用 107 | 108 | 如您使用了我们的数据集或基线模型,请引用我们的论文: 109 | ``` 110 | @inproceedings{zhang-etal-2022-mucgec, 111 | title = "{MuCGEC}: a Multi-Reference Multi-Source Evaluation Dataset for Chinese Grammatical Error Correction", 112 | author = "Zhang, Yue and Li, Zhenghua and Bao, Zuyi and Li, Jiacheng and Zhang, Bo and Li, Chen and Huang, Fei and Zhang, Min", 113 | booktitle = "Proceedings of NAACL-HLT", 114 | year = "2022", 115 | address = "Online", 116 | publisher = "Association for Computational Linguistics" 117 | ``` 118 | -------------------------------------------------------------------------------- /scorers/ChERRANT/modules/tokenizer.py: -------------------------------------------------------------------------------- 1 | from ltp import LTP 2 | from typing import List 3 | from pypinyin import pinyin, Style, lazy_pinyin 4 | import torch 5 | import os 6 | import functools 7 | 8 | class Tokenizer: 9 | """ 10 | 分词器 11 | """ 12 | 13 | def __init__(self, 14 | granularity: str = "word", 15 | device: str = "cpu", 16 | segmented: bool = False, 17 | bpe: bool = False, 18 | ) -> None: 19 | """ 20 | 构造函数 21 | :param mode: 分词模式,可选级别:字级别(char)、词级别(word) 22 | """ 23 | self.ltp = None 24 | if granularity == "word": 25 | self.ltp = LTP(device=torch.device(device) if torch.cuda.is_available() else torch.device("cpu")) 26 | self.ltp.add_words(words=["[缺失成分]"], max_window=6) 27 | self.segmented = segmented 28 | self.granularity = granularity 29 | if self.granularity == "word": 30 | self.tokenizer = self.split_word 31 | elif self.granularity == "char": 32 | self.tokenizer = functools.partial(self.split_char, bpe=bpe) 33 | else: 34 | raise NotImplementedError 35 | 36 | def __repr__(self) -> str: 37 | return "{:s}\nMode:{:s}\n}".format(str(self.__class__.__name__), self.mode) 38 | 39 | def __call__(self, 40 | input_strings: List[str] 41 | ) -> List: 42 | """ 43 | 分词函数 44 | :param input_strings: 需要分词的字符串列表 45 | :return: 分词后的结果列表,由元组组成,元组为(token,pos_tag,pinyin)的形式 46 | """ 47 | if not self.segmented: 48 | input_strings = ["".join(s.split(" ")) for s in input_strings] 49 | results = self.tokenizer(input_strings) 50 | return results 51 | 52 | def split_char(self, input_strings: List[str], bpe=False) -> List: 53 | """ 54 | 分字函数 55 | :param input_strings: 需要分字的字符串 56 | :return: 分字结果 57 | """ 58 | if bpe: 59 | from . import tokenization 60 | project_dir = os.path.dirname(os.path.dirname(__file__)) 61 | tokenizer = tokenization.FullTokenizer(vocab_file=os.path.join(project_dir,"data","chinese_vocab.txt"), do_lower_case=False) 62 | results = [] 63 | for input_string in input_strings: 64 | if not self.segmented: # 如果没有被分字,就按照每个字符隔开(不考虑英文标点的特殊处理,也不考虑BPE),否则遵循原分字结果 65 | segment_string = " ".join([char for char in input_string] if not bpe else tokenizer.tokenize(input_string)) 66 | else: 67 | segment_string = input_string 68 | # print(segment_string) 69 | segment_string = segment_string.replace("[ 缺 失 成 分 ]", "[缺失成分]").split(" ") # 缺失成分当成一个单独的token 70 | results.append([(char, "unk", pinyin(char, style=Style.NORMAL, heteronym=True)[0]) for char in segment_string]) 71 | return results 72 | 73 | def split_word(self, input_strings: List[str]) -> List: 74 | """ 75 | 分词函数 76 | :param input_strings: 需要分词的字符串 77 | :return: 分词结果 78 | """ 79 | if self.segmented: 80 | seg, hidden = self.ltp.seg([input_string.split(" ") for input_string in input_strings], is_preseged=True) 81 | else: 82 | seg, hidden = self.ltp.seg(input_strings) 83 | pos = self.ltp.pos(hidden) 84 | result = [] 85 | for s, p in zip(seg, pos): 86 | pinyin = [lazy_pinyin(word) for word in s] 87 | result.append(list(zip(s, p, pinyin))) 88 | return result 89 | 90 | if __name__ == "__main__": 91 | tokenizer = Tokenizer("word") 92 | print(tokenizer(["LAC是个优秀的分词工具", "百度是一家高科技公司"])) 93 | -------------------------------------------------------------------------------- /scorers/ChERRANT/samples/demo.hyp.m2.word: -------------------------------------------------------------------------------- 1 | S 冬阴功 是 泰国 最 著名 的 菜 之一 , 它 虽然 不 是 很 豪华 , 但 它 的 味 确实 让 人 上瘾 , 做法 也 不 难 、 不 复杂 。 2 | T0-A0 冬阴功 是 泰国 最 著名 的 菜 之一 , 它 虽然 不 是 很 豪华 , 但 它 的 味道 确实 让 人 上瘾 , 做法 也 不 难 、 不 复杂 。 3 | A 19 20|||S:NOUN|||味道|||REQUIRED|||-NONE-|||0 4 | 5 | S 首先 , 我们 得 准备 : 大虾 六 到 九 只 、 盐 一 茶匙 、 已 搾好 的 柠檬汁 三 汤匙 、 泰国 柠檬叶 三 叶 、 柠檬 香草 一 根 、 鱼 酱 两 汤匙 、 辣椒 6 粒 , 纯净水 4 量 杯 、 香菜 半量 杯 和 草菇 10 个 。 6 | T0 没有错误 7 | A -1 -1|||noop|||-NONE-|||REQUIRED|||-NONE-|||0 8 | 9 | S 这样 , 你 就 会 尝 到 泰国 人 死 爱 的 味道 。 10 | T0 没有错误 11 | A -1 -1|||noop|||-NONE-|||REQUIRED|||-NONE-|||0 12 | 13 | S 另外 , 冬阴功 对 外国人 的 喜爱 不断 地 增加 。 14 | T0-A0 另外 , 冬阴功 对 外国人 的 喜爱 也 不断 地 增加 。 15 | A 7 7|||M:ADV|||也|||REQUIRED|||-NONE-|||0 16 | 17 | S 这部 电影 不仅 是 国内 , 在 国外 也 很 有名 。 18 | T0-A0 这部 电影 不仅 是 在 国内 , 在 国外 也 很 有名 。 19 | A 4 4|||M:PREP|||在|||REQUIRED|||-NONE-|||0 20 | 21 | S 不管 是 真正 的 冬 阴功 还是 电影 的 “ 冬 阴功 ” , 都 在 人们 的 心里 刻骨铭心 。 22 | T0-A0 不管 是 真正 的 冬 阴功 还是 电影 里 的 “ 冬 阴功 ” , 都 在 人们 的 心里 刻骨铭刻 。 23 | A 8 8|||M:NOUN|||里|||REQUIRED|||-NONE-|||0 24 | A 19 20|||S:OTHER|||刻骨铭刻|||REQUIRED|||-NONE-|||0 25 | 26 | S 随着 中国 经济 突飞猛近 , 建造 工业 与日俱增 。 27 | T0-A0 随着 中国 经济 突飞猛快 , 建造 工业 与日俱增 。 28 | A 3 4|||S:OTHER|||突飞猛快|||REQUIRED|||-NONE-|||0 29 | 30 | S 虽然 工业 的 发展 和 城市 规模 的 扩大 对 经济 发展 有 积极 作用 , 但是 同时 也 对 环境 问题 日益 严重 造成 了 空气 污染 问题 。 31 | T0-A0 虽然 工业 的 发展 和 城市 规模 的 扩大 对 经济 发展 有 积极 作用 , 但是 同时 也 使 环境 问题 日益 严重 造成 了 空气 污染 问题 。 32 | A 19 20|||S:VERB|||使|||REQUIRED|||-NONE-|||0 33 | 34 | S 那些 空气 污染 也 没有 助于 人生 的 身体 建康 。 35 | T0-A0 那些 空气 污染 也 没有 助于 人们 的 身体 健康 。 36 | A 6 7|||S:NOUN|||人们|||REQUIRED|||-NONE-|||0 37 | A 9 10|||S:SPELL|||健康|||REQUIRED|||-NONE-|||0 38 | 39 | S 由此可见 , 首先 我们 要 了解 一些 关于 空气 污染 对 我们 人生 有 什么 危害 的 话题 知道 了 这些 常识 以后 对 我们 人类 会 有 积极 作用 。 以及 要 学会 怎样 应对 和 治理 空气 污染 的 问题 。 40 | T0-A0 由此可见 , 首先 我们 要 了解 一些 关于 空气 污染 对 我们 人生 有 什么 危害 的 话题 , 知道 了 这些 常识 以后 对 我们 人类 会 有 积极 作用 。 并且 要 学会 怎 何 应对 和 治理 空气 污染 的 问题 。 41 | A 18 18|||M:PUNCT|||,|||REQUIRED|||-NONE-|||0 42 | A 31 32|||S:CONJ|||并且|||REQUIRED|||-NONE-|||0 43 | A 34 35|||S:OTHER|||怎 何|||REQUIRED|||-NONE-|||0 44 | 45 | S 任何 事情 都 是 各 有 利弊 , 众所周知 越 建立 工业 越 对 经济 方面 有所 发展 。 46 | T0-A0 任何 事情 都 是 各 有 利弊 , 众所周知 越 建立 工业 对 经济 方面 越 有 发展 。 47 | A 12 13|||R:ADV|||-NONE-|||REQUIRED|||-NONE-|||0 48 | A 16 17|||S:OTHER|||越 有|||REQUIRED|||-NONE-|||0 49 | 50 | S 对 我 看来 , 曼古 空气 污染 的 问题 与日俱增 。 51 | T0-A0 在 我 看来 , 曼古 空气 污染 的 问题 与日俱增 。 52 | A 0 1|||S:PREP|||在|||REQUIRED|||-NONE-|||0 53 | 54 | S 每天 会 有 不少 的 毒 气体 泄漏 从 工厂 里 出来 。 55 | T0-A0 每天 会 有 不少 的 有毒 气体 从 工厂 里 出来 。 56 | A 5 6|||S:VERB|||有毒|||REQUIRED|||-NONE-|||0 57 | A 7 8|||R:VERB|||-NONE-|||REQUIRED|||-NONE-|||0 58 | 59 | S 在 工厂 里 的 工作 人员 为了 工作 , 而 每天 吸 了 不少 的 毒气体 , 经过 了 一 年 多 的 时间 , 连 工作 人员 也 得了 严重 的 病 。 更 不用 说 住 在 这家 工厂 近 的 家庭 。 60 | T0-A0 在 工厂 里 的 工作 人员 为了 工作 , 而 每天 吸 不少 的 毒 气体 , 经过 了 一 年 多 的 时间 , 连 工作 人员 也 得了 严重 的 病 。 更 不用 说 住 在 这家 工厂 附近 的 家庭 。 61 | A 12 13|||R:AUX|||-NONE-|||REQUIRED|||-NONE-|||0 62 | A 41 42|||S:NOUN|||附近|||REQUIRED|||-NONE-|||0 63 | 64 | S 沙尘暴 也 是 一 类 空气 污染 之一 。 65 | T0 没有错误 66 | A -1 -1|||noop|||-NONE-|||REQUIRED|||-NONE-|||0 67 | 68 | S 不 官 是 从 口 、 眼 、 鼻子 进去 这样 会 伤害 身体 的 建康 。 69 | T0-A0 不管 是 从 口 、 眼 、 鼻子 进去 , 这样 都 会 伤害 身体 的 健康 。 70 | A 0 2|||S:SPELL|||不管|||REQUIRED|||-NONE-|||0 71 | A 10 10|||M:PUNCT|||,|||REQUIRED|||-NONE-|||0 72 | A 11 11|||M:ADV|||都|||REQUIRED|||-NONE-|||0 73 | A 15 16|||S:SPELL|||健康|||REQUIRED|||-NONE-|||0 74 | 75 | S 这样 做 会 避免 受到 沙尘暴 。 76 | T0 没有错误 77 | A -1 -1|||noop|||-NONE-|||REQUIRED|||-NONE-|||0 78 | 79 | S 最后 , 要 关主 一些 关于 天气 预报 的 新闻 。 80 | T0-A0 最后 , 要 关注 一些 关于 天气 预报 的 新闻 。 81 | A 3 4|||S:SPELL|||关注|||REQUIRED|||-NONE-|||0 82 | 83 | S 中国 , 悠久 的 历史 , 灿烂 的 文化 , 真是 在 历史 上 最 难忘 的 国家 。 84 | T0-A0 中国 , 悠久 的 历史 , 灿烂 的 文化 , 真是 历史 上 最 难忘 的 国家 。 85 | A 11 12|||R:PREP|||-NONE-|||REQUIRED|||-NONE-|||0 86 | 87 | S 对 一个 生名 来说 空气 污染 是 很 危害 的 问题 , 对 身体 不好 。 88 | T0-A0 对 一个 生人 来说 空气 污染 是 很 严重 的 问题 , 对 身体 不好 。 89 | A 2 3|||S:NOUN|||生人|||REQUIRED|||-NONE-|||0 90 | A 8 9|||S:ADJ|||严重|||REQUIRED|||-NONE-|||0 91 | 92 | -------------------------------------------------------------------------------- /scorers/ChERRANT/samples/demo.hyp.m2: -------------------------------------------------------------------------------- 1 | S 冬 阴 功 是 泰 国 最 著 名 的 菜 之 一 , 它 虽 然 不 是 很 豪 华 , 但 它 的 味 确 实 让 人 上 瘾 , 做 法 也 不 难 、 不 复 杂 。 2 | T0-A0 冬 阴 功 是 泰 国 最 著 名 的 菜 之 一 , 它 虽 然 不 是 很 豪 华 , 但 它 的 味 道 确 实 让 人 上 瘾 , 做 法 也 不 难 、 不 复 杂 。 3 | A 27 27|||M|||道|||REQUIRED|||-NONE-|||0 4 | 5 | S 首 先 , 我 们 得 准 备 : 大 虾 六 到 九 只 、 盐 一 茶 匙 、 已 搾 好 的 柠 檬 汁 三 汤 匙 、 泰 国 柠 檬 叶 三 叶 、 柠 檬 香 草 一 根 、 鱼 酱 两 汤 匙 、 辣 椒 6 粒 , 纯 净 水 4 量 杯 、 香 菜 半 量 杯 和 草 菇 1 0 个 。 6 | T0 没有错误 7 | A -1 -1|||noop|||-NONE-|||REQUIRED|||-NONE-|||0 8 | 9 | S 这 样 , 你 就 会 尝 到 泰 国 人 死 爱 的 味 道 。 10 | T0 没有错误 11 | A -1 -1|||noop|||-NONE-|||REQUIRED|||-NONE-|||0 12 | 13 | S 另 外 , 冬 阴 功 对 外 国 人 的 喜 爱 不 断 地 增 加 。 14 | T0-A0 另 外 , 冬 阴 功 对 外 国 人 的 喜 爱 也 不 断 地 增 加 。 15 | A 13 13|||M|||也|||REQUIRED|||-NONE-|||0 16 | 17 | S 这 部 电 影 不 仅 是 国 内 , 在 国 外 也 很 有 名 。 18 | T0-A0 这 部 电 影 不 仅 是 在 国 内 , 在 国 外 也 很 有 名 。 19 | A 7 7|||M|||在|||REQUIRED|||-NONE-|||0 20 | 21 | S 不 管 是 真 正 的 冬 阴 功 还 是 电 影 的 “ 冬 阴 功 ” , 都 在 人 们 的 心 里 刻 骨 铭 心 。 22 | T0-A0 不 管 是 真 正 的 冬 阴 功 还 是 电 影 里 的 “ 冬 阴 功 ” , 都 在 人 们 的 心 里 刻 骨 铭 刻 。 23 | A 13 13|||M|||里|||REQUIRED|||-NONE-|||0 24 | A 30 31|||S|||刻|||REQUIRED|||-NONE-|||0 25 | 26 | S 随 着 中 国 经 济 突 飞 猛 近 , 建 造 工 业 与 日 俱 增 。 27 | T0-A0 随 着 中 国 经 济 突 飞 猛 快 , 建 造 工 业 与 日 俱 增 。 28 | A 9 10|||S|||快|||REQUIRED|||-NONE-|||0 29 | 30 | S 虽 然 工 业 的 发 展 和 城 市 规 模 的 扩 大 对 经 济 发 展 有 积 极 作 用 , 但 是 同 时 也 对 环 境 问 题 日 益 严 重 造 成 了 空 气 污 染 问 题 。 31 | T0-A0 虽 然 工 业 的 发 展 和 城 市 规 模 的 扩 大 对 经 济 发 展 有 积 极 作 用 , 但 是 同 时 也 使 环 境 问 题 日 益 严 重 造 成 了 空 气 污 染 问 题 。 32 | A 31 32|||S|||使|||REQUIRED|||-NONE-|||0 33 | 34 | S 那 些 空 气 污 染 也 没 有 助 于 人 生 的 身 体 建 康 。 35 | T0-A0 那 些 空 气 污 染 也 没 有 助 于 人 们 的 身 体 健 康 。 36 | A 12 13|||S|||们|||REQUIRED|||-NONE-|||0 37 | A 16 17|||S|||健|||REQUIRED|||-NONE-|||0 38 | 39 | S 由 此 可 见 , 首 先 我 们 要 了 解 一 些 关 于 空 气 污 染 对 我 们 人 生 有 什 么 危 害 的 话 题 知 道 了 这 些 常 识 以 后 对 我 们 人 类 会 有 积 极 作 用 。 以 及 要 学 会 怎 样 应 对 和 治 理 空 气 污 染 的 问 题 。 40 | T0-A0 由 此 可 见 , 首 先 我 们 要 了 解 一 些 关 于 空 气 污 染 对 我 们 人 生 有 什 么 危 害 的 话 题 , 知 道 了 这 些 常 识 以 后 对 我 们 人 类 会 有 积 极 作 用 。 并 且 要 学 会 怎 何 应 对 和 治 理 空 气 污 染 的 问 题 。 41 | A 33 33|||M|||,|||REQUIRED|||-NONE-|||0 42 | A 54 56|||S|||并 且|||REQUIRED|||-NONE-|||0 43 | A 60 61|||S|||何|||REQUIRED|||-NONE-|||0 44 | 45 | S 任 何 事 情 都 是 各 有 利 弊 , 众 所 周 知 越 建 立 工 业 越 对 经 济 方 面 有 所 发 展 。 46 | T0-A0 任 何 事 情 都 是 各 有 利 弊 , 众 所 周 知 越 建 立 工 业 对 经 济 方 面 越 有 发 展 。 47 | A 20 26|||W|||对 经 济 方 面 越|||REQUIRED|||-NONE-|||0 48 | A 27 28|||R|||-NONE-|||REQUIRED|||-NONE-|||0 49 | 50 | S 对 我 看 来 , 曼 古 空 气 污 染 的 问 题 与 日 俱 增 。 51 | T0-A0 在 我 看 来 , 曼 古 空 气 污 染 的 问 题 与 日 俱 增 。 52 | A 0 1|||S|||在|||REQUIRED|||-NONE-|||0 53 | 54 | S 每 天 会 有 不 少 的 毒 气 体 泄 漏 从 工 厂 里 出 来 。 55 | T0-A0 每 天 会 有 不 少 的 有 毒 气 体 从 工 厂 里 出 来 。 56 | A 7 7|||M|||有|||REQUIRED|||-NONE-|||0 57 | A 10 12|||R|||-NONE-|||REQUIRED|||-NONE-|||0 58 | 59 | S 在 工 厂 里 的 工 作 人 员 为 了 工 作 , 而 每 天 吸 了 不 少 的 毒 气 体 , 经 过 了 一 年 多 的 时 间 , 连 工 作 人 员 也 得 了 严 重 的 病 。 更 不 用 说 住 在 这 家 工 厂 近 的 家 庭 。 60 | T0-A0 在 工 厂 里 的 工 作 人 员 为 了 工 作 , 而 每 天 吸 不 少 的 毒 气 体 , 经 过 了 一 年 多 的 时 间 , 连 工 作 人 员 也 得 了 严 重 的 病 。 更 不 用 说 住 在 这 家 工 厂 附 近 的 家 庭 。 61 | A 18 19|||R|||-NONE-|||REQUIRED|||-NONE-|||0 62 | A 59 59|||M|||附|||REQUIRED|||-NONE-|||0 63 | 64 | S 沙 尘 暴 也 是 一 类 空 气 污 染 之 一 。 65 | T0 没有错误 66 | A -1 -1|||noop|||-NONE-|||REQUIRED|||-NONE-|||0 67 | 68 | S 不 官 是 从 口 、 眼 、 鼻 子 进 去 这 样 会 伤 害 身 体 的 建 康 。 69 | T0-A0 不 管 是 从 口 、 眼 、 鼻 子 进 去 , 这 样 都 会 伤 害 身 体 的 健 康 。 70 | A 1 2|||S|||管|||REQUIRED|||-NONE-|||0 71 | A 12 12|||M|||,|||REQUIRED|||-NONE-|||0 72 | A 14 14|||M|||都|||REQUIRED|||-NONE-|||0 73 | A 20 21|||S|||健|||REQUIRED|||-NONE-|||0 74 | 75 | S 这 样 做 会 避 免 受 到 沙 尘 暴 。 76 | T0 没有错误 77 | A -1 -1|||noop|||-NONE-|||REQUIRED|||-NONE-|||0 78 | 79 | S 最 后 , 要 关 主 一 些 关 于 天 气 预 报 的 新 闻 。 80 | T0-A0 最 后 , 要 关 注 一 些 关 于 天 气 预 报 的 新 闻 。 81 | A 5 6|||S|||注|||REQUIRED|||-NONE-|||0 82 | 83 | S 中 国 , 悠 久 的 历 史 , 灿 烂 的 文 化 , 真 是 在 历 史 上 最 难 忘 的 国 家 。 84 | T0-A0 中 国 , 悠 久 的 历 史 , 灿 烂 的 文 化 , 真 是 历 史 上 最 难 忘 的 国 家 。 85 | A 17 18|||R|||-NONE-|||REQUIRED|||-NONE-|||0 86 | 87 | S 对 一 个 生 名 来 说 空 气 污 染 是 很 危 害 的 问 题 , 对 身 体 不 好 。 88 | T0-A0 对 一 个 生 人 来 说 空 气 污 染 是 很 严 重 的 问 题 , 对 身 体 不 好 。 89 | A 4 5|||S|||人|||REQUIRED|||-NONE-|||0 90 | A 13 15|||S|||严 重|||REQUIRED|||-NONE-|||0 91 | 92 | -------------------------------------------------------------------------------- /scorers/ChERRANT/samples/demo.hyp.m2.char: -------------------------------------------------------------------------------- 1 | S 冬 阴 功 是 泰 国 最 著 名 的 菜 之 一 , 它 虽 然 不 是 很 豪 华 , 但 它 的 味 确 实 让 人 上 瘾 , 做 法 也 不 难 、 不 复 杂 。 2 | T0-A0 冬 阴 功 是 泰 国 最 著 名 的 菜 之 一 , 它 虽 然 不 是 很 豪 华 , 但 它 的 味 道 确 实 让 人 上 瘾 , 做 法 也 不 难 、 不 复 杂 。 3 | A 27 27|||M|||道|||REQUIRED|||-NONE-|||0 4 | 5 | S 首 先 , 我 们 得 准 备 : 大 虾 六 到 九 只 、 盐 一 茶 匙 、 已 搾 好 的 柠 檬 汁 三 汤 匙 、 泰 国 柠 檬 叶 三 叶 、 柠 檬 香 草 一 根 、 鱼 酱 两 汤 匙 、 辣 椒 6 粒 , 纯 净 水 4 量 杯 、 香 菜 半 量 杯 和 草 菇 1 0 个 。 6 | T0 没有错误 7 | A -1 -1|||noop|||-NONE-|||REQUIRED|||-NONE-|||0 8 | 9 | S 这 样 , 你 就 会 尝 到 泰 国 人 死 爱 的 味 道 。 10 | T0 没有错误 11 | A -1 -1|||noop|||-NONE-|||REQUIRED|||-NONE-|||0 12 | 13 | S 另 外 , 冬 阴 功 对 外 国 人 的 喜 爱 不 断 地 增 加 。 14 | T0-A0 另 外 , 冬 阴 功 对 外 国 人 的 喜 爱 也 不 断 地 增 加 。 15 | A 13 13|||M|||也|||REQUIRED|||-NONE-|||0 16 | 17 | S 这 部 电 影 不 仅 是 国 内 , 在 国 外 也 很 有 名 。 18 | T0-A0 这 部 电 影 不 仅 是 在 国 内 , 在 国 外 也 很 有 名 。 19 | A 7 7|||M|||在|||REQUIRED|||-NONE-|||0 20 | 21 | S 不 管 是 真 正 的 冬 阴 功 还 是 电 影 的 “ 冬 阴 功 ” , 都 在 人 们 的 心 里 刻 骨 铭 心 。 22 | T0-A0 不 管 是 真 正 的 冬 阴 功 还 是 电 影 里 的 “ 冬 阴 功 ” , 都 在 人 们 的 心 里 刻 骨 铭 刻 。 23 | A 13 13|||M|||里|||REQUIRED|||-NONE-|||0 24 | A 30 31|||S|||刻|||REQUIRED|||-NONE-|||0 25 | 26 | S 随 着 中 国 经 济 突 飞 猛 近 , 建 造 工 业 与 日 俱 增 。 27 | T0-A0 随 着 中 国 经 济 突 飞 猛 快 , 建 造 工 业 与 日 俱 增 。 28 | A 9 10|||S|||快|||REQUIRED|||-NONE-|||0 29 | 30 | S 虽 然 工 业 的 发 展 和 城 市 规 模 的 扩 大 对 经 济 发 展 有 积 极 作 用 , 但 是 同 时 也 对 环 境 问 题 日 益 严 重 造 成 了 空 气 污 染 问 题 。 31 | T0-A0 虽 然 工 业 的 发 展 和 城 市 规 模 的 扩 大 对 经 济 发 展 有 积 极 作 用 , 但 是 同 时 也 使 环 境 问 题 日 益 严 重 造 成 了 空 气 污 染 问 题 。 32 | A 31 32|||S|||使|||REQUIRED|||-NONE-|||0 33 | 34 | S 那 些 空 气 污 染 也 没 有 助 于 人 生 的 身 体 建 康 。 35 | T0-A0 那 些 空 气 污 染 也 没 有 助 于 人 们 的 身 体 健 康 。 36 | A 12 13|||S|||们|||REQUIRED|||-NONE-|||0 37 | A 16 17|||S|||健|||REQUIRED|||-NONE-|||0 38 | 39 | S 由 此 可 见 , 首 先 我 们 要 了 解 一 些 关 于 空 气 污 染 对 我 们 人 生 有 什 么 危 害 的 话 题 知 道 了 这 些 常 识 以 后 对 我 们 人 类 会 有 积 极 作 用 。 以 及 要 学 会 怎 样 应 对 和 治 理 空 气 污 染 的 问 题 。 40 | T0-A0 由 此 可 见 , 首 先 我 们 要 了 解 一 些 关 于 空 气 污 染 对 我 们 人 生 有 什 么 危 害 的 话 题 , 知 道 了 这 些 常 识 以 后 对 我 们 人 类 会 有 积 极 作 用 。 并 且 要 学 会 怎 何 应 对 和 治 理 空 气 污 染 的 问 题 。 41 | A 33 33|||M|||,|||REQUIRED|||-NONE-|||0 42 | A 54 56|||S|||并 且|||REQUIRED|||-NONE-|||0 43 | A 60 61|||S|||何|||REQUIRED|||-NONE-|||0 44 | 45 | S 任 何 事 情 都 是 各 有 利 弊 , 众 所 周 知 越 建 立 工 业 越 对 经 济 方 面 有 所 发 展 。 46 | T0-A0 任 何 事 情 都 是 各 有 利 弊 , 众 所 周 知 越 建 立 工 业 对 经 济 方 面 越 有 发 展 。 47 | A 20 26|||W|||对 经 济 方 面 越|||REQUIRED|||-NONE-|||0 48 | A 27 28|||R|||-NONE-|||REQUIRED|||-NONE-|||0 49 | 50 | S 对 我 看 来 , 曼 古 空 气 污 染 的 问 题 与 日 俱 增 。 51 | T0-A0 在 我 看 来 , 曼 古 空 气 污 染 的 问 题 与 日 俱 增 。 52 | A 0 1|||S|||在|||REQUIRED|||-NONE-|||0 53 | 54 | S 每 天 会 有 不 少 的 毒 气 体 泄 漏 从 工 厂 里 出 来 。 55 | T0-A0 每 天 会 有 不 少 的 有 毒 气 体 从 工 厂 里 出 来 。 56 | A 7 7|||M|||有|||REQUIRED|||-NONE-|||0 57 | A 10 12|||R|||-NONE-|||REQUIRED|||-NONE-|||0 58 | 59 | S 在 工 厂 里 的 工 作 人 员 为 了 工 作 , 而 每 天 吸 了 不 少 的 毒 气 体 , 经 过 了 一 年 多 的 时 间 , 连 工 作 人 员 也 得 了 严 重 的 病 。 更 不 用 说 住 在 这 家 工 厂 近 的 家 庭 。 60 | T0-A0 在 工 厂 里 的 工 作 人 员 为 了 工 作 , 而 每 天 吸 不 少 的 毒 气 体 , 经 过 了 一 年 多 的 时 间 , 连 工 作 人 员 也 得 了 严 重 的 病 。 更 不 用 说 住 在 这 家 工 厂 附 近 的 家 庭 。 61 | A 18 19|||R|||-NONE-|||REQUIRED|||-NONE-|||0 62 | A 59 59|||M|||附|||REQUIRED|||-NONE-|||0 63 | 64 | S 沙 尘 暴 也 是 一 类 空 气 污 染 之 一 。 65 | T0 没有错误 66 | A -1 -1|||noop|||-NONE-|||REQUIRED|||-NONE-|||0 67 | 68 | S 不 官 是 从 口 、 眼 、 鼻 子 进 去 这 样 会 伤 害 身 体 的 建 康 。 69 | T0-A0 不 管 是 从 口 、 眼 、 鼻 子 进 去 , 这 样 都 会 伤 害 身 体 的 健 康 。 70 | A 1 2|||S|||管|||REQUIRED|||-NONE-|||0 71 | A 12 12|||M|||,|||REQUIRED|||-NONE-|||0 72 | A 14 14|||M|||都|||REQUIRED|||-NONE-|||0 73 | A 20 21|||S|||健|||REQUIRED|||-NONE-|||0 74 | 75 | S 这 样 做 会 避 免 受 到 沙 尘 暴 。 76 | T0 没有错误 77 | A -1 -1|||noop|||-NONE-|||REQUIRED|||-NONE-|||0 78 | 79 | S 最 后 , 要 关 主 一 些 关 于 天 气 预 报 的 新 闻 。 80 | T0-A0 最 后 , 要 关 注 一 些 关 于 天 气 预 报 的 新 闻 。 81 | A 5 6|||S|||注|||REQUIRED|||-NONE-|||0 82 | 83 | S 中 国 , 悠 久 的 历 史 , 灿 烂 的 文 化 , 真 是 在 历 史 上 最 难 忘 的 国 家 。 84 | T0-A0 中 国 , 悠 久 的 历 史 , 灿 烂 的 文 化 , 真 是 历 史 上 最 难 忘 的 国 家 。 85 | A 17 18|||R|||-NONE-|||REQUIRED|||-NONE-|||0 86 | 87 | S 对 一 个 生 名 来 说 空 气 污 染 是 很 危 害 的 问 题 , 对 身 体 不 好 。 88 | T0-A0 对 一 个 生 人 来 说 空 气 污 染 是 很 严 重 的 问 题 , 对 身 体 不 好 。 89 | A 4 5|||S|||人|||REQUIRED|||-NONE-|||0 90 | A 13 15|||S|||严 重|||REQUIRED|||-NONE-|||0 91 | 92 | -------------------------------------------------------------------------------- /models/seq2edit-based-CGEC/pipeline.sh: -------------------------------------------------------------------------------- 1 | # Step1. Data Preprocessing 2 | 3 | ## Download Structbert 4 | if [ ! -f ./plm/chinese-struct-bert-large/pytorch_model.bin ]; then 5 | wget https://alice-open.oss-cn-zhangjiakou.aliyuncs.com/StructBERT/ch_model 6 | mv ch_model ./plm/chinese-struct-bert-large/pytorch_model.bin 7 | fi 8 | 9 | ## Tokenize 10 | SRC_FILE=../../data/train_data/lang8+hsk/train.src # 每行一个病句 11 | TGT_FILE=../../data/train_data/lang8+hsk/train.tgt # 每行一个正确句子,和病句一一对应 12 | if [ ! -f $SRC_FILE".char" ]; then 13 | python ../../tools/segment/segment_bert.py < $SRC_FILE > $SRC_FILE".char" # 分字 14 | fi 15 | if [ ! -f $TGT_FILE".char" ]; then 16 | python ../../tools/segment/segment_bert.py < $TGT_FILE > $TGT_FILE".char" # 分字 17 | fi 18 | 19 | ## Generate label file 20 | LABEL_FILE=../../data/train_data/lang8+hsk/train.label # 训练数据 21 | if [ ! -f $LABEL_FILE ]; then 22 | python ./utils/preprocess_data.py -s $SRC_FILE".char" -t $TGT_FILE".char" -o $LABEL_FILE --worker_num 32 23 | shuf $LABEL_FILE > $LABEL_FILE".shuf" 24 | fi 25 | 26 | # Step2. Training 27 | CUDA_DEVICE=0 28 | SEED=1 29 | 30 | DEV_SET=../../data/valid_data/MuCGEC_CGED_Dev.label 31 | MODEL_DIR=./exps/seq2edit_lang8 32 | if [ ! -d $MODEL_DIR ]; then 33 | mkdir -p $MODEL_DIR 34 | fi 35 | 36 | PRETRAIN_WEIGHTS_DIR=./plm/chinese-struct-bert-large 37 | 38 | mkdir ${MODEL_DIR}/src_bak 39 | cp ./pipeline.sh $MODEL_DIR/src_bak 40 | cp -r ./gector $MODEL_DIR/src_bak 41 | cp ./train.py $MODEL_DIR/src_bak 42 | cp ./predict.py $MODEL_DIR/src_bak 43 | 44 | VOCAB_PATH=./data/output_vocabulary_chinese_char_hsk+lang8_5 45 | 46 | ## Freeze encoder (Cold Step) 47 | COLD_LR=1e-3 48 | COLD_BATCH_SIZE=128 49 | COLD_MODEL_NAME=Best_Model_Stage_1 50 | COLD_EPOCH=2 51 | 52 | CUDA_VISIBLE_DEVICES=$CUDA_DEVICE python train.py --tune_bert 0\ 53 | --train_set $LABEL_FILE".shuf"\ 54 | --dev_set $DEV_SET\ 55 | --model_dir $MODEL_DIR\ 56 | --model_name $COLD_MODEL_NAME\ 57 | --vocab_path $VOCAB_PATH\ 58 | --batch_size $COLD_BATCH_SIZE\ 59 | --n_epoch $COLD_EPOCH\ 60 | --lr $COLD_LR\ 61 | --weights_name $PRETRAIN_WEIGHTS_DIR\ 62 | --seed $SEED 63 | 64 | ## Unfreeze encoder 65 | LR=1e-5 66 | BATCH_SIZE=32 67 | ACCUMULATION_SIZE=4 68 | MODEL_NAME=Best_Model_Stage_2 69 | EPOCH=20 70 | PATIENCE=3 71 | 72 | CUDA_VISIBLE_DEVICES=$CUDA_DEVICE python train.py --tune_bert 1\ 73 | --train_set $LABEL_FILE".shuf"\ 74 | --dev_set $DEV_SET\ 75 | --model_dir $MODEL_DIR\ 76 | --model_name $MODEL_NAME\ 77 | --vocab_path $VOCAB_PATH\ 78 | --batch_size $BATCH_SIZE\ 79 | --n_epoch $EPOCH\ 80 | --lr $LR\ 81 | --accumulation_size $ACCUMULATION_SIZE\ 82 | --patience $PATIENCE\ 83 | --weights_name $PRETRAIN_WEIGHTS_DIR\ 84 | --pretrain_folder $MODEL_DIR\ 85 | --pretrain "Temp_Model"\ 86 | --seed $SEED 87 | 88 | 89 | # Step3. Inference 90 | MODEL_PATH=$MODEL_DIR"/Best_Model_Stage_2.th" 91 | RESULT_DIR=$MODEL_DIR"/results" 92 | 93 | INPUT_FILE=../../data/test_data/MuCGEC/MuCGEC-ALL/MuCGEC_ALL_Test.input # 输入文件 94 | if [ ! -f $INPUT_FILE".char" ]; then 95 | python ../../tools/segment/segment_bert.py < $INPUT_FILE > $INPUT_FILE".char" # 分字 96 | fi 97 | if [ ! -d $RESULT_DIR ]; then 98 | mkdir -p $RESULT_DIR 99 | fi 100 | OUTPUT_FILE=$RESULT_DIR"/MuCGEC_test.output" 101 | 102 | echo "Generating..." 103 | SECONDS=0 104 | CUDA_VISIBLE_DEVICES=$CUDA_DEVICE python predict.py --model_path $MODEL_PATH\ 105 | --weights_name $PRETRAIN_WEIGHTS_DIR\ 106 | --vocab_path $VOCAB_PATH\ 107 | --input_file $INPUT_FILE".char"\ 108 | --output_file $OUTPUT_FILE --log 109 | 110 | echo "Generating Finish!" 111 | duration=$SECONDS 112 | echo "$(($duration / 60)) minutes and $(($duration % 60)) seconds elapsed." 113 | -------------------------------------------------------------------------------- /scorers/ChERRANT/m2convertor.py: -------------------------------------------------------------------------------- 1 | # -*- coding:UTF-8 -*- 2 | # @Author: Xuezhi Fang 3 | # @Date: 2020-06-19 4 | # @Email: jasonfang3900@gmail.com 5 | 6 | import argparse 7 | import re 8 | 9 | 10 | class M2Processor(): 11 | def __init__(self, src_sent, edit_lines): 12 | self.src_sent = src_sent 13 | self.edit_lines = edit_lines 14 | self.edits = {} 15 | self.trg_sents = [] 16 | 17 | def conv_edit(self, line): 18 | line = line.strip().split("|||") 19 | edit_span = line[0].split(" ") 20 | edit_span = (int(edit_span[0]), int(edit_span[1])) 21 | edit_res = line[2] 22 | editor = line[-1] 23 | if edit_span[0] == -1: 24 | return None 25 | if edit_span[0] == edit_span[1]: 26 | edit_tag = "ADD" 27 | elif edit_res == "-NONE-" or edit_res == "": 28 | edit_tag = "DEL" 29 | else: 30 | edit_tag = "REP" 31 | return editor, edit_tag, edit_span, edit_res 32 | 33 | def get_edits(self): 34 | for line in self.edit_lines: 35 | if line: 36 | edit_item = self.conv_edit(line) 37 | if not edit_item: 38 | continue 39 | editor, edit_tag, edit_span, edit_res = edit_item 40 | if editor not in self.edits: 41 | self.edits[editor] = [] 42 | self.edits[editor].append({"span": edit_span, "op": edit_tag, "res": edit_res}) 43 | 44 | def get_para(self): 45 | self.get_edits() 46 | if self.edits: 47 | for editor in self.edits: 48 | sent = self.src_sent.split(" ") 49 | for edit_item in self.edits[editor]: 50 | edit_span, edit_tag, trg_tokens = edit_item["span"], edit_item["op"], edit_item["res"] 51 | if edit_tag == "DEL": 52 | sent[edit_span[0]:edit_span[1]] = [" " for _ in range(edit_span[1] - edit_span[0])] 53 | else: 54 | if edit_tag == "ADD": 55 | if edit_span[0] != 0: 56 | sent[edit_span[0]-1] += " " + trg_tokens 57 | else: 58 | sent[edit_span[0]] = trg_tokens + " " + sent[edit_span[0]] 59 | elif edit_tag == "REP": 60 | src_tokens_len = len(sent[edit_span[0]:edit_span[1]]) 61 | sent[edit_span[0]:edit_span[1]] = [trg_tokens] + [" " for _ in range(src_tokens_len-1)] 62 | sent = " ".join(sent).strip() 63 | res_sent = re.sub(" +", " ", sent) 64 | self.trg_sents.append(res_sent) 65 | return self.trg_sents 66 | else: 67 | return [self.src_sent] 68 | 69 | 70 | def read_file(): 71 | src_sent = None 72 | edit_lines = [] 73 | with open(args.f, "r", encoding="utf8") as fr: 74 | for line in fr: 75 | if line: 76 | line = line.strip() 77 | if line.startswith("S "): 78 | src_sent = line.replace("S ", "", 1) 79 | elif line.startswith("A "): 80 | edit_lines.append(line.replace("A ", "", 1)) 81 | elif line == "": 82 | yield src_sent, edit_lines 83 | edit_lines.clear() 84 | 85 | 86 | def main(): 87 | counter = 0 88 | fw_trg = open(args.o, "w", encoding="utf8") 89 | for src_sent, edit_lines in read_file(): 90 | counter += 1 91 | m2_item = M2Processor(src_sent, edit_lines) 92 | trg_sents = m2_item.get_para() 93 | prefix_counter = 0 94 | fw_trg.write(trg_sents[0]+"\n") 95 | fw_trg.close() 96 | 97 | 98 | if __name__ == "__main__": 99 | parser = argparse.ArgumentParser() 100 | parser.add_argument("-f", help="m2 file") 101 | parser.add_argument("-o", help="output file") 102 | args = parser.parse_args() 103 | main() 104 | -------------------------------------------------------------------------------- /models/seq2edit-based-CGEC/gector/seq2labels_metric.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from allennlp.training.metrics.metric import Metric 3 | from overrides import overrides 4 | 5 | @Metric.register("Seq2LabelsMetric") 6 | class Seq2LabelsMetric(Metric): 7 | """ 8 | 计算评价指标 9 | """ 10 | 11 | def __init__(self, eps=1e-8): 12 | super().__init__() 13 | 14 | self.eps = eps 15 | self.count_token = 0 16 | self.labels_without_keep = 0 17 | self.labels_correct = 0 18 | self.d_tags_correct = 0 19 | self.labels_without_keep_correct = 0 20 | 21 | def __repr__(self): 22 | s = f"labels_accuracy: {self.Labels_Accuracy}, d_tags_accuracy: {self.Tags_Accuracy}, labels_accuracy_except_keep: {self.Labels_Accuracy_Except_Keep}.\n" 23 | return s 24 | 25 | @overrides 26 | def __call__(self, logits_labels, labels, logits_d, d_tags, mask=None, crf_result=None): 27 | logits_labels, labels, logits_d, d_tags, mask = self.detach_tensors(logits_labels, labels, logits_d, d_tags, 28 | mask) 29 | num_labels = logits_labels.size(-1) 30 | num_tags = logits_d.size(-1) 31 | 32 | logits_labels = logits_labels.view((-1, num_labels)) 33 | labels = labels.view(-1).long() 34 | if crf_result is not None: 35 | argmax_labels = crf_result.view(-1).unsqueeze(-1) 36 | else: 37 | argmax_labels = logits_labels.max(-1)[1].unsqueeze(-1) 38 | # print(argmax_labels.shape, labels.shape) 39 | correct_labels = argmax_labels.eq(labels.unsqueeze(-1)).float() 40 | labels_ueq_keep = labels.unsqueeze(-1) != 0 41 | correct_labels_ueq_keep = (argmax_labels.eq(labels.unsqueeze(-1)) & labels_ueq_keep).float() 42 | labels_ueq_keep = labels_ueq_keep.float() 43 | 44 | logits_d = logits_d.view((-1, num_tags)) 45 | d_tags = d_tags.view(-1).long() 46 | argmax_tags = logits_d.max(-1)[1].unsqueeze(-1) 47 | correct_tags = argmax_tags.eq(d_tags.unsqueeze(-1)).float() 48 | 49 | if mask is not None: 50 | correct_labels *= mask.view(-1, 1) 51 | correct_tags *= mask.view(-1, 1) 52 | correct_labels_ueq_keep *= mask.view(-1, 1) 53 | _total_count = mask.sum() 54 | labels_ueq_keep *= mask.view(-1, 1) 55 | _labels_without_keep = labels_ueq_keep.sum() 56 | else: 57 | _total_count = torch.tensor(labels.numel()) 58 | _labels_without_keep = labels_ueq_keep.sum() 59 | _correct_labels_count = correct_labels.sum() 60 | _correct_tags_count = correct_tags.sum() 61 | _correct_labels_ueq_keep_count = correct_labels_ueq_keep.sum() 62 | self.count_token += _total_count 63 | self.labels_without_keep += _labels_without_keep 64 | self.labels_correct += _correct_labels_count 65 | self.d_tags_correct += _correct_tags_count 66 | self.labels_without_keep_correct += _correct_labels_ueq_keep_count 67 | 68 | @overrides 69 | def reset(self): 70 | self.count_token = 0 71 | self.labels_without_keep = 0 72 | self.labels_correct = 0 73 | self.d_tags_correct = 0 74 | self.labels_without_keep_correct = 0 75 | 76 | @property 77 | def Labels_Accuracy(self): 78 | """ 79 | 编辑标签预测准确率 80 | """ 81 | return self.labels_correct / (self.count_token + self.eps) 82 | 83 | @property 84 | def Tags_Accuracy(self): 85 | """ 86 | token是否有误标签预测准确率 87 | """ 88 | return self.d_tags_correct / (self.count_token + self.eps) 89 | 90 | @property 91 | def Labels_Accuracy_Except_Keep(self): 92 | """ 93 | 编辑标签预测准确率(除$Keep标签外) 94 | """ 95 | return self.labels_without_keep_correct / (self.labels_without_keep + self.eps) 96 | 97 | @property 98 | def Total_Accuracy(self): 99 | """ 100 | 编辑标签预测准确率 101 | """ 102 | return self.Labels_Accuracy + self.Tags_Accuracy + self.Labels_Accuracy_Except_Keep 103 | 104 | def get_metric(self, reset: bool = False, model_object=None): 105 | ret = {"labels_accuracy": self.Labels_Accuracy, "d_tags_accuracy": self.Tags_Accuracy, "labels_accuracy_except_keep": self.Labels_Accuracy_Except_Keep, "total_accuracy": self.Total_Accuracy} 106 | if reset: 107 | self.reset() 108 | return ret 109 | -------------------------------------------------------------------------------- /models/seq2seq-based-CGEC/predict.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import torch 5 | import argparse 6 | from tqdm import tqdm 7 | from transformers import BartForConditionalGeneration, BertTokenizer 8 | from opencc import OpenCC 9 | import re 10 | 11 | parser = argparse.ArgumentParser() 12 | parser.add_argument('-m', '--model_path') 13 | parser.add_argument('-i', '--input_path') 14 | parser.add_argument('-o', '--output_path') 15 | parser.add_argument('-b', '--batch_size', default=50) 16 | args = parser.parse_args() 17 | 18 | cc = OpenCC("t2s") 19 | tokenizer=BertTokenizer.from_pretrained(args.model_path) 20 | model=BartForConditionalGeneration.from_pretrained(args.model_path) 21 | model.eval() 22 | model.half() 23 | model.cuda() 24 | 25 | def split_sentence(document: str, flag: str = "all", limit: int = 510): 26 | """ 27 | Args: 28 | document: 29 | flag: Type:str, "all" 中英文标点分句,"zh" 中文标点分句,"en" 英文标点分句 30 | limit: 默认单句最大长度为510个字符 31 | Returns: Type:list 32 | """ 33 | sent_list = [] 34 | try: 35 | if flag == "zh": 36 | document = re.sub('(?P([。?!](?![”’"\'])))', r'\g\n', document) # 单字符断句符 37 | document = re.sub('(?P([。?!])[”’"\'])', r'\g\n', document) # 特殊引号 38 | elif flag == "en": 39 | document = re.sub('(?P([.?!](?![”’"\'])))', r'\g\n', document) # 英文单字符断句符 40 | document = re.sub('(?P([?!.]["\']))', r'\g\n', document) # 特殊引号 41 | else: 42 | document = re.sub('(?P([。?!….?!](?![”’"\'])))', r'\g\n', document) # 单字符断句符 43 | document = re.sub('(?P(([。?!.!?]|…{1,2})[”’"\']))', r'\g\n', 44 | document) # 特殊引号 45 | 46 | sent_list_ori = document.splitlines() 47 | for sent in sent_list_ori: 48 | sent = sent.strip() 49 | if not sent: 50 | continue 51 | else: 52 | while len(sent) > limit: 53 | temp = sent[0:limit] 54 | sent_list.append(temp) 55 | sent = sent[limit:] 56 | sent_list.append(sent) 57 | except: 58 | sent_list.clear() 59 | sent_list.append(document) 60 | return sent_list 61 | 62 | def run_model(sents): 63 | num_ret_seqs = 1 64 | beam = 5 65 | inp_max_len = 100 66 | batch = [tokenizer(s, return_tensors='pt', padding='max_length', max_length=inp_max_len) for s in sents] 67 | oidx2bidx = {} #original index to final batch index 68 | final_batch = [] 69 | for oidx, elm in enumerate(batch): 70 | if elm['input_ids'].size(1) <= inp_max_len: 71 | oidx2bidx[oidx] = len(final_batch) 72 | final_batch.append(elm) 73 | batch = {key: torch.cat([elm[key] for elm in final_batch], dim=0) for key in final_batch[0]} 74 | with torch.no_grad(): 75 | generated_ids = model.generate(batch['input_ids'].cuda(), 76 | attention_mask=batch['attention_mask'].cuda(), 77 | num_beams=beam, num_return_sequences=num_ret_seqs, max_length=inp_max_len) 78 | _out = tokenizer.batch_decode(generated_ids.detach().cpu(), skip_special_tokens=True) 79 | outs = [] 80 | for i in range(0, len(_out), num_ret_seqs): 81 | outs.append(_out[i:i+num_ret_seqs]) 82 | final_outs = [[sents[oidx]] if oidx not in oidx2bidx else outs[oidx2bidx[oidx]] for oidx in range(len(sents))] 83 | return final_outs 84 | 85 | def predict(): 86 | sents = [l.strip() for l in open(args.input_path)] # 分句 87 | subsents = [] 88 | s_map = [] 89 | for i, sent in enumerate(sents): # 将篇章划分为子句,分句预测再合并 90 | subsent_list = split_sentence(sent, "zh") 91 | s_map.extend([i for _ in range(len(subsent_list))]) 92 | subsents.extend(subsent_list) 93 | assert len(subsents) == len(s_map) 94 | b_size = args.batch_size 95 | outs = [] 96 | for j in tqdm(range(0, len(subsents), b_size)): 97 | sents_batch = subsents[j:j+b_size] 98 | outs_batch = run_model(sents_batch) 99 | for sent, preds in zip(sents_batch, outs_batch): 100 | outs.append({'src': sent, 'preds': preds}) 101 | results = ["" for _ in range(len(sents))] 102 | with open(args.output_path, 'w') as outf: 103 | for i, out in enumerate(outs): 104 | results[s_map[i]] += cc.convert(out['preds'][0]) 105 | for res in results: 106 | outf.write(res + "\n") 107 | 108 | predict() -------------------------------------------------------------------------------- /scorers/ChERRANT/rule_ensemble.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from collections import Counter 3 | from modules.classifier import check_spell_error 4 | from tqdm import tqdm 5 | 6 | def parse_m2(filename): 7 | """解析m2格式文件 8 | 9 | Arguments: 10 | filename -- 文件名 11 | """ 12 | sources = [] 13 | edits = [] 14 | with open(filename, "r") as f: 15 | chunk = [] 16 | for line in f: 17 | if line == "\n": 18 | sources.append(chunk[0]) 19 | edit_list = [] 20 | for s in chunk[2:]: 21 | if s[0] != "A": break 22 | edit_list.append(s) 23 | edits.append(edit_list) 24 | chunk = [] 25 | else: 26 | chunk.append(line.rstrip("\n")) 27 | if chunk: 28 | sources.append(chunk[0]) 29 | edit_list = [] 30 | for s in chunk[2:]: 31 | if s[0] != "A": break 32 | edit_list.append(s) 33 | edits.append(edit_list) 34 | return sources, edits 35 | 36 | 37 | def validate(edits): 38 | edits_with_pos = [] 39 | for edit, times in edits: 40 | _, ss, se = edit.split("|||")[0].split(" ") 41 | ss, se = int(ss), int(se) 42 | edits_with_pos.append((ss, se, edit, times)) 43 | edits_with_pos.sort(key=lambda x: (x[0], -times)) # 按照起始位置从小到大排序,起始位置相同,按照编辑出现次数从大到小排序 44 | final_edits = [edits_with_pos[0][2]] 45 | for i in range(1, len(edits_with_pos)): 46 | if edits_with_pos[i][0] < edits_with_pos[i-1][1]: # 有重叠span 47 | edits_with_pos[i] = edits_with_pos[i-1] # 后续的span和前一个span比较 48 | continue 49 | if edits_with_pos[i][0] == edits_with_pos[i-1][0] and edits_with_pos[i][1] == edits_with_pos[i-1][1]: 50 | edits_with_pos[i] = edits_with_pos[i-1] # 后续的span和前一个span比较 51 | continue 52 | final_edits.append(edits_with_pos[i][-2]) 53 | final_final_edits = [] 54 | for e in final_edits: 55 | if len(final_final_edits) == 0 or e != final_final_edits[-1]: 56 | final_final_edits.append(e) 57 | return final_final_edits 58 | 59 | 60 | def main(args): 61 | total_edits = [] 62 | for f in args.result_path: 63 | sources, edits = parse_m2(f) 64 | total_edits.append(edits) 65 | with open(args.output_path, "w", encoding="utf-8") as o: 66 | for i in tqdm(range(len(sources))): 67 | src = sources[i] 68 | src_tokens = src.split(" ")[1:] 69 | edit_candidates = [] 70 | for edits in total_edits: 71 | edit_candidates.extend(edits[i]) 72 | final_edits = [] 73 | c = Counter(edit_candidates) 74 | if c["A -1 -1|||noop|||-NONE-|||REQUIRED|||-NONE-|||0"] > (6 - args.threshold): # 没有错误 75 | out = src + "\n" + "A -1 -1|||noop|||-NONE-|||REQUIRED|||-NONE-|||0" + "\n\n" 76 | o.write(out) 77 | continue 78 | for k, v in c.items(): 79 | if v >= args.threshold: 80 | if k != "A -1 -1|||noop|||-NONE-|||REQUIRED|||-NONE-|||0": 81 | final_edits.append((k, v)) 82 | if "|||W|||" in k and v >= args.threshold - 1: # 词序错误特殊阈值 83 | final_edits.append((k, v)) 84 | if "|||S|||" in k and v >= args.threshold - 1: # 拼写错误特殊阈值 85 | _, ss, se = k.split("|||")[0].split(" ") 86 | src_span = "".join(src_tokens[int(ss): int(se)]) 87 | tgt_span = k.split("|||")[2].replace(" ", "") 88 | if check_spell_error(src_span, tgt_span): 89 | final_edits.append((k, v)) 90 | if final_edits: 91 | final_edits = validate(final_edits) 92 | out = src + "\n" + "\n".join(final_edits) + "\n\n" 93 | else: 94 | out = src + "\n" + "A -1 -1|||noop|||-NONE-|||REQUIRED|||-NONE-|||0" + "\n\n" 95 | o.write(out) 96 | 97 | if __name__ == "__main__": 98 | parser = argparse.ArgumentParser() 99 | parser.add_argument('--result_path', 100 | help='Path to the result file.', nargs='+', 101 | required=True) 102 | parser.add_argument('--output_path', 103 | help='Path to the output file.', 104 | required=True) 105 | parser.add_argument('-T', '--threshold', 106 | help='Threshold.', 107 | type=int, 108 | default=2) 109 | args = parser.parse_args() 110 | main(args) -------------------------------------------------------------------------------- /scorers/ChERRANT/README.en.md: -------------------------------------------------------------------------------- 1 | # Instruction 2 | 3 | We have built a Chinese GEC evaluation tool, ChERRANT (Chinese ERRANT), following the mainstream English GEC evaluation tool [ERRANT](https://github.com/chrisjbryant/errant). The main function of ChERRANT is to calculate the Precision, Recall and F-value of prediction results by comparing hypothesis edits and golden edits, thereby evaluating the performance of GEC model. 4 | 5 | ## Environment 6 | 7 | `requirements.txt` contains the main environment required for the experiment. The specific environment construction process is as follows: 8 | 9 | ``` 10 | conda create -n cherrant python==3.8 11 | conda activate cherrant 12 | pip install -r requirements.txt 13 | ``` 14 | 15 | ## Usage 16 | 17 | ### Overview 18 | 19 | #### File Format 20 | 21 | The format of the hypothesis file is: `id \t Original sentence \t Forecast results `; 22 | 23 | The format of the reference file is: `id \t Original sentence \t Standard answer 1 \t Standard answer 2 \t ... `; 24 | 25 | The format of the edit file is M2, as follows: 26 | 27 | ``` 28 | S 冬 阴 功 是 泰 国 最 著 名 的 菜 之 一 , 它 虽 然 不 是 很 豪 华 , 但 它 的 味 确 实 让 人 上 瘾 , 做 法 也 不 难 、 不 复 杂 。 29 | T0-A0 冬 阴 功 是 泰 国 最 著 名 的 菜 之 一 , 它 虽 然 不 是 很 豪 华 , 但 它 的 味 道 确 实 让 人 上 瘾 , 做 法 也 不 难 、 不 复 杂 。 30 | A 27 27|||M|||道|||REQUIRED|||-NONE-|||0 31 | ``` 32 | 33 | + `S` represents the original sentence; 34 | + `T0-A0` represents the 0th edit sequence of the 0th answer (a sentence may have multiple answers, and an answer may also have multiple edit sequences with the same minimal edit distance); 35 | + `A` represents edits, mainly including the following information: The start and end positions of errors (`27 27`); Error type (`M`, Missing Error); Modification method of errors (`道`, i.e., insert "道"); Annotator ID (`0`); 36 | 37 | #### Evaluation Process 38 | 39 | The main evaluation steps are as follows: 40 | 41 | 1. Converting parallel reference file to edit file `gold.m2` through `parallel_to_m2.py` (it is only necessary for the first evaluation and can be reused later); 42 | 2. Converting parallel hypothesis file to edit file `hyp.m2` through `parallel_to_m2.py`; 43 | 3. Comparing `gold.m2` and `hyp.m2` using `compare_m2_for_evaluation.py` to get the final evaluation results; 44 | 45 | For example scripts of the complete process, please refer to `./demo.sh`. 46 | 47 | ### Extract Edit 48 | 49 | First, merging the input file (one sentence per line) and the output file (one sentence per line) into the parallel format: 50 | 51 | ``` 52 | INPUT_FILE=./samples/demo.input 53 | OUTPUT_FILE=./samples/demo.hyp 54 | HYP_PARA_FILE=./samples/demo.hyp.para 55 | 56 | paste $INPUT_FILE $OUTPUT_FILE | awk '{print NR"\t"$p}' > $HYP_PARA_FILE 57 | ``` 58 | 59 | Then, extracting the edits with the following command: 60 | 61 | ``` 62 | HYP_M2_FILE=./samples/demo.hyp.m2.char 63 | 64 | python parallel_to_m2.py -f $HYP_PARA_FILE -o $HYP_M2_FILE -g char 65 | ``` 66 | 67 | By default, char-level edits are extracted. 68 | 69 | By setting the `-g' parameter to 'word', you can extract word-level edits. 70 | 71 | For more functions, please refer to the command line help file: 72 | 73 | ``` 74 | python parallel_to_m2.py --help 75 | ``` 76 | 77 | ### Calculate Evaluation 78 | 79 | Use the following script to compare the hypothesis edit file with the reference edit file to get the char-level evaluation metrics: 80 | 81 | ``` 82 | REF_M2_FILE=./samples/demo.ref.m2.char 83 | python compare_m2_for_evaluation.py -hyp $HYP_M2_FILE -ref $REF_M2_FILE 84 | ``` 85 | 86 | The F0.5 value at the char-level is the official evaluation metric adopted by the MuCGEC dataset, and the evaluation results are as follows: 87 | 88 | ``` 89 | =========== Span-Based Correction ============ 90 | TP FP FN Prec Rec F0.5 91 | 8 19 35 0.2963 0.186 0.2649 92 | ============================================== 93 | ``` 94 | 95 | This program can also support more fine-grained information displays, such as displaying detection indicators and correction indicators of different types of errors. 96 | 97 | 98 | For more functions, please refer to the command line help file: 99 | 100 | ``` 101 | python compare_m2_for_evaluation.py --help 102 | ``` 103 | 104 | ## Citation 105 | 106 | If you find this work is useful for your research, please cite our paper: 107 | 108 | #### MuCGEC: a Multi-Reference Multi-Source Evaluation Dataset for Chinese Grammatical Error Correction (Accepted by NAACL2022 main conference) [[PDF]](https://arxiv.org/pdf/2204.10994.pdf) 109 | 110 | ``` 111 | @inproceedings{zhang-etal-2022-mucgec, 112 | title = "{MuCGEC}: a Multi-Reference Multi-Source Evaluation Dataset for Chinese Grammatical Error Correction", 113 | author = "Zhang, Yue and Li, Zhenghua and Bao, Zuyi and Li, Jiacheng and Zhang, Bo and Li, Chen and Huang, Fei and Zhang, Min", 114 | booktitle = "Proceedings of NAACL-HLT", 115 | year = "2022", 116 | address = "Online", 117 | publisher = "Association for Computational Linguistics" 118 | ``` 119 | -------------------------------------------------------------------------------- /scorers/ChERRANT/parallel_to_m2.py: -------------------------------------------------------------------------------- 1 | import os 2 | from modules.annotator import Annotator 3 | from modules.tokenizer import Tokenizer 4 | import argparse 5 | from collections import Counter 6 | from tqdm import tqdm 7 | import torch 8 | from collections import defaultdict 9 | from multiprocessing import Pool 10 | from opencc import OpenCC 11 | 12 | os.environ["TOKENIZERS_PARALLELISM"] = "false" 13 | 14 | annotator, sentence_to_tokenized = None, None 15 | cc = OpenCC("t2s") 16 | 17 | def annotate(line): 18 | """ 19 | :param line: 20 | :return: 21 | """ 22 | sent_list = line.split("\t")[1:] 23 | source = sent_list[0] 24 | if args.segmented: 25 | source = source.strip() 26 | else: 27 | source = "".join(source.strip().split()) 28 | output_str = "" 29 | for idx, target in enumerate(sent_list[1:]): 30 | try: 31 | if args.segmented: 32 | target = target.strip() 33 | else: 34 | target = "".join(target.strip().split()) 35 | if not args.no_simplified: 36 | target = cc.convert(target) 37 | source_tokenized, target_tokenized = sentence_to_tokenized[source], sentence_to_tokenized[target] 38 | out, cors = annotator(source_tokenized, target_tokenized, idx) 39 | if idx == 0: 40 | output_str += "".join(out[:-1]) 41 | else: 42 | output_str += "".join(out[1:-1]) 43 | except Exception: 44 | raise Exception 45 | return output_str 46 | 47 | def main(args): 48 | tokenizer = Tokenizer(args.granularity, args.device, args.segmented, args.bpe) 49 | global annotator, sentence_to_tokenized 50 | annotator = Annotator.create_default(args.granularity, args.multi_cheapest_strategy) 51 | lines = open(args.file, "r", encoding="utf-8").read().strip().split("\n") # format: id src tgt1 tgt2... 52 | # error_types = [] 53 | 54 | with open(args.output, "w", encoding="utf-8") as f: 55 | count = 0 56 | sentence_set = set() 57 | sentence_to_tokenized = {} 58 | for line in lines: 59 | sent_list = line.split("\t")[1:] 60 | for idx, sent in enumerate(sent_list): 61 | if args.segmented: 62 | # print(sent) 63 | sent = sent.strip() 64 | else: 65 | sent = "".join(sent.split()).strip() 66 | if idx >= 1: 67 | if not args.no_simplified: 68 | sentence_set.add(cc.convert(sent)) 69 | else: 70 | sentence_set.add(sent) 71 | else: 72 | sentence_set.add(sent) 73 | batch = [] 74 | for sent in tqdm(sentence_set): 75 | count += 1 76 | if sent: 77 | batch.append(sent) 78 | if count % args.batch_size == 0: 79 | results = tokenizer(batch) 80 | for s, r in zip(batch, results): 81 | sentence_to_tokenized[s] = r # Get tokenization map. 82 | batch = [] 83 | if batch: 84 | results = tokenizer(batch) 85 | for s, r in zip(batch, results): 86 | sentence_to_tokenized[s] = r # Get tokenization map. 87 | 88 | # 单进程模式 89 | for line in tqdm(lines): 90 | ret = annotate(line) 91 | f.write(ret) 92 | f.write("\n") 93 | 94 | # 多进程模式:仅在Linux环境下测试,建议在linux服务器上使用 95 | # with Pool(args.worker_num) as pool: 96 | # for ret in pool.imap(annotate, tqdm(lines), chunksize=8): 97 | # if ret: 98 | # f.write(ret) 99 | # f.write("\n") 100 | 101 | 102 | if __name__ == "__main__": 103 | parser = argparse.ArgumentParser(description="Choose input file to annotate") 104 | parser.add_argument("-f", "--file", type=str, required=True, help="Input parallel file") 105 | parser.add_argument("-o", "--output", type=str, help="Output file", required=True) 106 | parser.add_argument("-b", "--batch_size", type=int, help="The size of batch", default=128) 107 | parser.add_argument("-d", "--device", type=int, help="The ID of GPU", default=0) 108 | parser.add_argument("-w", "--worker_num", type=int, help="The number of workers", default=16) 109 | parser.add_argument("-g", "--granularity", type=str, help="Choose char-level or word-level evaluation", default="char") 110 | parser.add_argument("-m", "--merge", help="Whether merge continuous replacement/deletion/insertion", action="store_true") 111 | parser.add_argument("-s", "--multi_cheapest_strategy", type=str, choices=["first", "all"], default="all") 112 | parser.add_argument("--segmented", help="Whether tokens have been segmented", action="store_true") # 支持提前token化,用空格隔开 113 | parser.add_argument("--no_simplified", help="Whether simplifying chinese", action="store_true") # 将所有corrections转换为简体中文 114 | parser.add_argument("--bpe", help="Whether to use bpe", action="store_true") # 支持 bpe 切分英文单词 115 | args = parser.parse_args() 116 | main(args) 117 | -------------------------------------------------------------------------------- /data/MuCGEC/README.en.md: -------------------------------------------------------------------------------- 1 | # MuCGEC Dataset Description 2 | ## Basic Information 3 | The name of the dataset: MuCGEC(Multi-Reference Multi-Source Evaluation Dataset for Chinese Grammatical Error Correction) 4 | 5 | This dataset contains: 6 | + dev set: 1137 sentences(12 unannotatable sentences) 7 | + test set: 6000 sentences(62 unannotatable sentences) 8 | 9 | The dataset file: mucgec.zip. After you unzip the file, you should see: 10 | 11 | + MuCGEC_dev.txt: the dev set which contains the source sentences and the corresponding answers; 12 | + MuCGEC_test.txt: the test set which only contains the source sentences. The answers should be predicted by yourselfs; 13 | + example_pred_dev.txt: the example of the prediction results of the dev set; 14 | + filter_sentences.txt: the sentences that need to be filtered out from the train set (may overlap with test set); 15 | + README.md: Instructions of the dataset, please read it carefully。 16 | 17 | **The training data must filter out the sentences in filter_sentences.txt。** 18 | 19 | ## Data Format 20 | ### Dev Set 21 | 22 | We provide a dev set (`MuCGEC_dev.txt` ) with answers for hyper-parameter tuning. The dev set is given in the format of TXT file, and the basic data format is: `id,\t, source sentence,\t, standard answer 1,\t, standard answer 2,...`, as follows: 23 | ``` 24 | 1 因为在冰箱里没什么东西也做很好吃的菜。 即使在冰箱里没什么东西也能做很好吃的菜。 即使在冰箱里没什么东西,也能做很好吃的菜。 25 | ``` 26 | 27 | ### Test Set 28 | The test set shown below is also given in TXT format, and each column is separated by a `\t` symbol. However, the standard answers need to be predicted by the participants themselves. As follows: 29 | ``` 30 | 2 纳税和帐单小一典。 31 | ``` 32 | 33 | Participants need to predict the unique modification result before submitting it and add it to the end of each line. As follows: 34 | ``` 35 | 2 纳税和帐单小一典。 纳税和帐单小一点。 36 | ``` 37 | 38 | ## Instructions 39 | ### Local Evaluation(Dev Set) 40 | 41 | The official evaluation metric of MuCGEC is **the char-level F0.5 metric**. 42 | 43 | The link of our evaluation tool: [https://github.com/HillZhang1999/MuCGEC/tree/main/scorers/ChERRANT](https://github.com/HillZhang1999/MuCGEC/tree/main/scorers/ChERRANT). 44 | 45 | You can refer to the following link for the usage of our evaluation tool: [https://github.com/HillZhang1999/MuCGEC/tree/main/scorers/ChERRANT/README.md](https://github.com/HillZhang1999/MuCGEC/tree/main/scorers/ChERRANT/README.md). 46 | 47 | The core steps: 48 | 49 | 1. Converting the standard answer file `MuCGEC_dev.txt` to M2 file `MuCGEC_dev.m2` through `parallel_to_m2.py` (it is only necessary for the first time and can be reused later); 50 | 51 | ``` 52 | python parallel_to_m2.py -f MuCGEC_dev.txt -o MuCGEC_dev.m2 53 | ``` 54 | 55 | 2. Converting the prediction result file `example_pred_dev.txt` to M2 file `example_pred_dev.m2` through `parallel_to_m2.py`; 56 | 57 | ``` 58 | python parallel_to_m2.py -f example_pred_dev.txt -o example_pred_dev.m2 59 | ``` 60 | 61 | 3. Comparing `example_pred_dev.m2` and `MuCGEC_dev.m2` using `compare_m2_for_evaluation.py` to get the final evaluation results; 62 | ``` 63 | python compare_m2_for_evaluation.py -hyp example_pred_dev.m2 -ref MuCGEC_dev.m2 64 | ``` 65 | 66 | All prediction results need to be processed into the same format as `example_pred_dev.txt`. 67 | 68 | If the evaluation process is correct, the word level indicator of the sample result file `example_pred_dev.txt` in the dev set should be: 69 | 70 | ``` 71 | =========== Span-Based Correction ============ 72 | TP FP FN Prec Rec F0.5 73 | 1084 1635 3003 0.3987 0.2652 0.3622 74 | ============================================== 75 | ``` 76 | 77 | ### Online Submission (Test Set) 78 | 79 | Predict all sentences in the given test set file, and add the result to the end of each line. The specific format is `id, \t, source sentence, \t, predict result`. For example: 80 | 81 | + source file: 82 | ``` 83 | 2 纳税和帐单小一典。 84 | ``` 85 | 86 | + result file: 87 | ``` 88 | 2 纳税和帐单小一典。 纳税和帐单小一点。 89 | ``` 90 | 91 | Still name the result file as `MuCGEC_test.txt`, zip it, and upload it to the [Tianchi platform](https://tianchi.aliyun.com/dataset/dataDetail?dataId=131328)。 92 | 93 | Note: 94 | + The zip file only needs to contain the prediction result txt file, and does not need to contain intermediate folders or other files. 95 | + The result file must be named as `mucgec_Test.txt`, and the content format must be the same as `example_pred_Dev.txt `. 96 | 97 | ## Detailed Metric Description 98 | 99 | The indexes returned after online submission include: 100 | 101 | + TP: True Positive, which denotes the number of correct edits predicted by the model; 102 | + FP: False Positive, which denotes the number of wrong edits predicted by the model; 103 | + FN: False Negative, which denotes the number of correct edits that are not predicted by the model; 104 | + Precision: equal to TP/(TP+FP); 105 | + Recall: equal to TP/(TP+FN); 106 | + score: F0.5. Compared with the traditional F1 value, it pays more attention to the precision of the model and is a commonly used indicator in the GEC field. 107 | 108 | Among the above indexes, the score (i.e., f0.5 value of the model) is the final ranking criterion. 109 | 110 | 111 | ## Citation 112 | 113 | If you use any part of our work, please cite our paper: 114 | ``` 115 | @inproceedings{zhang-etal-2022-mucgec, 116 | title = "{MuCGEC}: a Multi-Reference Multi-Source Evaluation Dataset for Chinese Grammatical Error Correction", 117 | author = "Zhang, Yue and Li, Zhenghua and Bao, Zuyi and Li, Jiacheng and Zhang, Bo and Li, Chen and Huang, Fei and Zhang, Min", 118 | booktitle = "Proceedings of NAACL-HLT", 119 | year = "2022", 120 | address = "Online", 121 | publisher = "Association for Computational Linguistics" 122 | ``` 123 | -------------------------------------------------------------------------------- /scorers/ChERRANT/samples/demo.ref.m2.word: -------------------------------------------------------------------------------- 1 | S 冬阴功 是 泰国 最 著名 的 菜 之一 , 它 虽然 不 是 很 豪华 , 但 它 的 味 确实 让 人 上瘾 , 做法 也 不 难 、 不 复杂 。 2 | T0-A0 冬阴功 是 泰国 最 著名 的 菜 之一 , 虽然 它 不 是 很 豪华 , 但 它 的 味 确实 让 人 上瘾 , 做法 也 不 难 、 不 复杂 。 3 | A 9 11|||W|||虽然 它|||REQUIRED|||-NONE-|||0 4 | 5 | S 首先 , 我们 得 准备 : 大虾 六 到 九 只 、 盐 一 茶匙 、 已 搾好 的 柠檬汁 三 汤匙 、 泰国 柠檬叶 三 叶 、 柠檬 香草 一 根 、 鱼 酱 两 汤匙 、 辣椒 6 粒 , 纯净水 4 量 杯 、 香菜 半量 杯 和 草菇 10 个 。 6 | T0-A0 首先 , 我们 得 准备 : 大虾 六 到 九 只 、 盐 一 茶匙 、 已 榨 好 的 柠檬汁 三 汤匙 、 泰国 柠檬叶 三 叶 、 柠檬 香草 一 根 、 鱼 酱 两 汤匙 、 辣椒 六 粒 , 纯净水 四 量 杯 、 香菜 半量 杯 和 草菇 十 个 。 7 | A 17 18|||S:SPELL|||榨 好|||REQUIRED|||-NONE-|||0 8 | A 39 40|||S:NUM|||六|||REQUIRED|||-NONE-|||0 9 | A 43 44|||S:NUM|||四|||REQUIRED|||-NONE-|||0 10 | A 52 53|||S:NUM|||十|||REQUIRED|||-NONE-|||0 11 | 12 | S 这样 , 你 就 会 尝 到 泰国 人 死 爱 的 味道 。 13 | T0-A0 这样 , 你 就 会 尝 到 泰国 人 爱 死 的 味道 。 14 | A 9 11|||W|||爱 死|||REQUIRED|||-NONE-|||0 15 | 16 | S 另外 , 冬阴功 对 外国人 的 喜爱 不断 地 增加 。 17 | T0-A0 另外 , 外国人 对 冬阴功 的 喜爱 不断 地 增加 。 18 | A 2 5|||W|||外国人 对 冬阴功|||REQUIRED|||-NONE-|||0 19 | 20 | S 这部 电影 不仅 是 国内 , 在 国外 也 很 有名 。 21 | T0-A0 这部 电影 不仅 是 在 国内 , 在 国外 也 很 有名 。 22 | A 4 4|||M:PREP|||在|||REQUIRED|||-NONE-|||0 23 | T1-A0 这部 电影 不仅 在 国内 , 在 国外 也 很 有名 。 24 | A 3 4|||S:PREP|||在|||REQUIRED|||-NONE-|||1 25 | 26 | S 不管 是 真正 的 冬 阴功 还是 电影 的 “ 冬 阴功 ” , 都 在 人们 的 心里 刻骨铭心 。 27 | T0-A0 不管 是 真正 的 冬 阴功 还是 电影 的 “ 冬 阴功 ” , 人们 都 刻骨铭心 。 28 | A 14 16|||R:OTHER|||-NONE-|||REQUIRED|||-NONE-|||0 29 | A 17 19|||S:ADV|||都|||REQUIRED|||-NONE-|||0 30 | T1-A0 不管 是 真正 的 冬 阴功 还是 电影 中 的 “ 冬 阴功 ” , 都 使 人们 刻骨铭心 。 31 | A 8 8|||M:NOUN|||中|||REQUIRED|||-NONE-|||1 32 | A 15 16|||S:VERB|||使|||REQUIRED|||-NONE-|||1 33 | A 17 19|||R:OTHER|||-NONE-|||REQUIRED|||-NONE-|||1 34 | 35 | S 随着 中国 经济 突飞猛近 , 建造 工业 与日俱增 。 36 | T0-A0 随着 中国 经济 突飞猛进 , 建造 工业 与日俱增 。 37 | A 3 4|||S:SPELL|||突飞猛进|||REQUIRED|||-NONE-|||0 38 | 39 | S 虽然 工业 的 发展 和 城市 规模 的 扩大 对 经济 发展 有 积极 作用 , 但是 同时 也 对 环境 问题 日益 严重 造成 了 空气 污染 问题 。 40 | T0-A0 虽然 工业 的 发展 和 城市 规模 的 扩大 对 经济 发展 有 积极 作用 , 但是 同时 也 对 环境 造成 了 日益 严重 的 空气 污染 问题 。 41 | A 21 22|||S:OTHER|||造成 了|||REQUIRED|||-NONE-|||0 42 | A 24 26|||S:AUX|||的|||REQUIRED|||-NONE-|||0 43 | T1-A0 虽然 工业 的 发展 和 城市 规模 的 扩大 对 经济 发展 有 积极 作用 , 但是 同时 也 对 环境 造成 了 日益 严重 的 污染 问题 。 44 | A 21 22|||S:OTHER|||造成 了|||REQUIRED|||-NONE-|||1 45 | A 24 27|||S:AUX|||的|||REQUIRED|||-NONE-|||1 46 | 47 | S 那些 空气 污染 也 没有 助于 人生 的 身体 建康 。 48 | T0-A0 那些 空气 污染 也 无助于 人 的 身体 健康 。 49 | A 4 7|||S:OTHER|||无助于 人|||REQUIRED|||-NONE-|||0 50 | A 9 10|||S:SPELL|||健康|||REQUIRED|||-NONE-|||0 51 | 52 | S 由此可见 , 首先 我们 要 了解 一些 关于 空气 污染 对 我们 人生 有 什么 危害 的 话题 知道 了 这些 常识 以后 对 我们 人类 会 有 积极 作用 。 以及 要 学会 怎样 应对 和 治理 空气 污染 的 问题 。 53 | T0-A0 由此可见 , 首先 我们 要 了解 一些 关于 空气 污染 对 我们 人体 有 什么 危害 的 话题 —— 知道 这些 常识 对 我们 人类 会 有 积极 作用 , 以及 要 学会 怎样 应对 和 治理 空气 污染 的 问题 。 54 | A 12 13|||S:NOUN|||人体|||REQUIRED|||-NONE-|||0 55 | A 18 18|||M:PUNCT|||——|||REQUIRED|||-NONE-|||0 56 | A 19 20|||R:AUX|||-NONE-|||REQUIRED|||-NONE-|||0 57 | A 22 23|||R:NOUN|||-NONE-|||REQUIRED|||-NONE-|||0 58 | A 30 31|||S:PUNCT|||,|||REQUIRED|||-NONE-|||0 59 | T1-A0 由此可见 , 首先 我们 要 了解 一些 关于 空气 污染 对 我们 人体 有 什么 危害 的 话题 —— 知道 这些 常识 对 我们 人类 会 有 积极 作用 , 其次 要 学会 怎样 应对 和 治理 空气 污染 的 问题 。 60 | A 12 13|||S:NOUN|||人体|||REQUIRED|||-NONE-|||1 61 | A 18 18|||M:PUNCT|||——|||REQUIRED|||-NONE-|||1 62 | A 19 20|||R:AUX|||-NONE-|||REQUIRED|||-NONE-|||1 63 | A 22 23|||R:NOUN|||-NONE-|||REQUIRED|||-NONE-|||1 64 | A 30 31|||S:PUNCT|||,|||REQUIRED|||-NONE-|||1 65 | A 31 32|||S:CONJ|||其次|||REQUIRED|||-NONE-|||1 66 | 67 | S 任何 事情 都 是 各 有 利弊 , 众所周知 越 建立 工业 越 对 经济 方面 有所 发展 。 68 | T0-A0 任何 事情 都 是 各 有 利弊 , 众所周知 越 发展 工业 , 经济 方面 就 越 有所 发展 。 69 | A 10 11|||S:VERB|||发展|||REQUIRED|||-NONE-|||0 70 | A 12 14|||S:PUNCT|||,|||REQUIRED|||-NONE-|||0 71 | A 16 16|||M:OTHER|||就 越|||REQUIRED|||-NONE-|||0 72 | T1-A0 任何 事情 都 是 各 有 利弊 的 , 众所周知 越 发展 工业 , 经济 方面 就 越 有所 发展 。 73 | A 7 7|||M:AUX|||的|||REQUIRED|||-NONE-|||1 74 | A 10 11|||S:VERB|||发展|||REQUIRED|||-NONE-|||1 75 | A 12 14|||S:PUNCT|||,|||REQUIRED|||-NONE-|||1 76 | A 16 16|||M:OTHER|||就 越|||REQUIRED|||-NONE-|||1 77 | 78 | S 对 我 看来 , 曼古 空气 污染 的 问题 与日俱增 。 79 | T0-A0 在 我 看来 , 曼古 空气 污染 的 问题 与日俱增 。 80 | A 0 1|||S:PREP|||在|||REQUIRED|||-NONE-|||0 81 | 82 | S 每天 会 有 不少 的 毒 气体 泄漏 从 工厂 里 出来 。 83 | T0-A0 每天 会 有 不少 的 有毒 气体 从 工厂 里 泄漏 出来 。 84 | A 5 6|||S:VERB|||有毒|||REQUIRED|||-NONE-|||0 85 | A 7 11|||W|||从 工厂 里 泄漏|||REQUIRED|||-NONE-|||0 86 | T1-A0 每天 会 有 不少 的 有毒 气体 从 工厂 里 泄露 出来 。 87 | A 5 6|||S:VERB|||有毒|||REQUIRED|||-NONE-|||1 88 | A 7 11|||W|||从 工厂 里 泄露|||REQUIRED|||-NONE-|||1 89 | 90 | S 在 工厂 里 的 工作 人员 为了 工作 , 而 每天 吸 了 不少 的 毒气体 , 经过 了 一 年 多 的 时间 , 连 工作 人员 也 得了 严重 的 病 。 更 不用 说 住 在 这家 工厂 近 的 家庭 。 91 | T0-A0 在 工厂 里 的 工作 人员 为了 工作 , 每天 吸 了 不少 的 有毒 气体 , 经过 了 一 年 多 的 时间 , 连 工作 人员 也 得了 严重 的 病 , 更 不用 说 住 得 离 这家 工厂 近 的 家庭 。 92 | A 9 10|||R:CONJ|||-NONE-|||REQUIRED|||-NONE-|||0 93 | A 15 16|||S:OTHER|||有毒 气体|||REQUIRED|||-NONE-|||0 94 | A 33 34|||S:PUNCT|||,|||REQUIRED|||-NONE-|||0 95 | A 38 39|||S:OTHER|||得 离|||REQUIRED|||-NONE-|||0 96 | T1-A0 在 工厂 里 的 工作 人员 为了 工作 , 每天 吸 了 不少 的 有毒 气体 , 经过 了 一 年 多 的 时间 , 连 工作 人员 也 得了 严重 的 病 , 更 不用说 住 在 这家 工厂 附近 的 家庭 。 97 | A 9 10|||R:CONJ|||-NONE-|||REQUIRED|||-NONE-|||1 98 | A 15 16|||S:OTHER|||有毒 气体|||REQUIRED|||-NONE-|||1 99 | A 33 34|||S:PUNCT|||,|||REQUIRED|||-NONE-|||1 100 | A 41 42|||S:NOUN|||附近|||REQUIRED|||-NONE-|||1 101 | 102 | S 沙尘暴 也 是 一 类 空气 污染 之一 。 103 | T0-A0 沙尘暴 也 是 一 类 空气 污染 。 104 | A 7 8|||R:PRON|||-NONE-|||REQUIRED|||-NONE-|||0 105 | T1-A0 沙尘暴 也 是 空气 污染 之一 。 106 | A 3 5|||R:OTHER|||-NONE-|||REQUIRED|||-NONE-|||1 107 | 108 | S 不 官 是 从 口 、 眼 、 鼻子 进去 这样 会 伤害 身体 的 建康 。 109 | T0-A0 不管 是 从 口 、 眼 还是 鼻子 进去 , 都 会 伤害 身体 的 健康 。 110 | A 0 2|||S:SPELL|||不管|||REQUIRED|||-NONE-|||0 111 | A 7 8|||S:CONJ|||还是|||REQUIRED|||-NONE-|||0 112 | A 10 11|||S:OTHER|||, 都|||REQUIRED|||-NONE-|||0 113 | A 15 16|||S:SPELL|||健康|||REQUIRED|||-NONE-|||0 114 | 115 | S 这样 做 会 避免 受到 沙尘暴 。 116 | T0-A0 这样 做 会 避免 受到 沙尘暴 的 危害 。 117 | A 6 6|||M:OTHER|||的 危害|||REQUIRED|||-NONE-|||0 118 | 119 | S 最后 , 要 关主 一些 关于 天气 预报 的 新闻 。 120 | T0-A0 最后 , 要 关注 一些 关于 天气 预报 的 新闻 。 121 | A 3 4|||S:SPELL|||关注|||REQUIRED|||-NONE-|||0 122 | 123 | S 中国 , 悠久 的 历史 , 灿烂 的 文化 , 真是 在 历史 上 最 难忘 的 国家 。 124 | T0-A0 中国 , 拥有 悠久 的 历史 , 灿烂 的 文化 , 真是 在 历史 上 让 人 最 难忘 的 国家 。 125 | A 2 2|||M:VERB|||拥有|||REQUIRED|||-NONE-|||0 126 | A 14 14|||M:OTHER|||让 人|||REQUIRED|||-NONE-|||0 127 | 128 | S 对 一个 生名 来说 空气 污染 是 很 危害 的 问题 , 对 身体 不好 。 129 | T0-A0 对 一个 生命 来说 空气 污染 是 有 很 大 危害 的 问题 , 对 身体 不好 。 130 | A 2 3|||S:SPELL|||生命|||REQUIRED|||-NONE-|||0 131 | A 7 7|||M:VERB|||有|||REQUIRED|||-NONE-|||0 132 | A 8 8|||M:ADJ|||大|||REQUIRED|||-NONE-|||0 133 | T1-A0 对 一个 生命 来说 空气 污染 是 很 有 危害 的 , 对 身体 不好 。 134 | A 2 3|||S:SPELL|||生命|||REQUIRED|||-NONE-|||1 135 | A 8 8|||M:VERB|||有|||REQUIRED|||-NONE-|||1 136 | A 10 11|||R:NOUN|||-NONE-|||REQUIRED|||-NONE-|||1 137 | 138 | -------------------------------------------------------------------------------- /scorers/ChERRANT/samples/demo.ref.m2.char: -------------------------------------------------------------------------------- 1 | S 冬 阴 功 是 泰 国 最 著 名 的 菜 之 一 , 它 虽 然 不 是 很 豪 华 , 但 它 的 味 确 实 让 人 上 瘾 , 做 法 也 不 难 、 不 复 杂 。 2 | T0-A0 冬 阴 功 是 泰 国 最 著 名 的 菜 之 一 , 虽 然 它 不 是 很 豪 华 , 但 它 的 味 确 实 让 人 上 瘾 , 做 法 也 不 难 、 不 复 杂 。 3 | A 14 17|||W|||虽 然 它|||REQUIRED|||-NONE-|||0 4 | 5 | S 首 先 , 我 们 得 准 备 : 大 虾 六 到 九 只 、 盐 一 茶 匙 、 已 搾 好 的 柠 檬 汁 三 汤 匙 、 泰 国 柠 檬 叶 三 叶 、 柠 檬 香 草 一 根 、 鱼 酱 两 汤 匙 、 辣 椒 6 粒 , 纯 净 水 4 量 杯 、 香 菜 半 量 杯 和 草 菇 1 0 个 。 6 | T0-A0 首 先 , 我 们 得 准 备 : 大 虾 六 到 九 只 、 盐 一 茶 匙 、 已 榨 好 的 柠 檬 汁 三 汤 匙 、 泰 国 柠 檬 叶 三 叶 、 柠 檬 香 草 一 根 、 鱼 酱 两 汤 匙 、 辣 椒 六 粒 , 纯 净 水 四 量 杯 、 香 菜 半 量 杯 和 草 菇 十 个 。 7 | A 22 23|||S|||榨|||REQUIRED|||-NONE-|||0 8 | A 55 56|||S|||六|||REQUIRED|||-NONE-|||0 9 | A 61 62|||S|||四|||REQUIRED|||-NONE-|||0 10 | A 73 75|||S|||十|||REQUIRED|||-NONE-|||0 11 | 12 | S 这 样 , 你 就 会 尝 到 泰 国 人 死 爱 的 味 道 。 13 | T0-A0 这 样 , 你 就 会 尝 到 泰 国 人 爱 死 的 味 道 。 14 | A 11 13|||W|||爱 死|||REQUIRED|||-NONE-|||0 15 | 16 | S 另 外 , 冬 阴 功 对 外 国 人 的 喜 爱 不 断 地 增 加 。 17 | T0-A0 另 外 , 外 国 人 对 冬 阴 功 的 喜 爱 不 断 地 增 加 。 18 | A 3 10|||W|||外 国 人 对 冬 阴 功|||REQUIRED|||-NONE-|||0 19 | 20 | S 这 部 电 影 不 仅 是 国 内 , 在 国 外 也 很 有 名 。 21 | T0-A0 这 部 电 影 不 仅 是 在 国 内 , 在 国 外 也 很 有 名 。 22 | A 7 7|||M|||在|||REQUIRED|||-NONE-|||0 23 | T1-A0 这 部 电 影 不 仅 在 国 内 , 在 国 外 也 很 有 名 。 24 | A 6 7|||S|||在|||REQUIRED|||-NONE-|||1 25 | 26 | S 不 管 是 真 正 的 冬 阴 功 还 是 电 影 的 “ 冬 阴 功 ” , 都 在 人 们 的 心 里 刻 骨 铭 心 。 27 | T0-A0 不 管 是 真 正 的 冬 阴 功 还 是 电 影 的 “ 冬 阴 功 ” , 人 们 都 刻 骨 铭 心 。 28 | A 20 22|||R|||-NONE-|||REQUIRED|||-NONE-|||0 29 | A 24 27|||S|||都|||REQUIRED|||-NONE-|||0 30 | T1-A0 不 管 是 真 正 的 冬 阴 功 还 是 电 影 中 的 “ 冬 阴 功 ” , 都 使 人 们 刻 骨 铭 心 。 31 | A 13 13|||M|||中|||REQUIRED|||-NONE-|||1 32 | A 21 22|||S|||使|||REQUIRED|||-NONE-|||1 33 | A 24 27|||R|||-NONE-|||REQUIRED|||-NONE-|||1 34 | 35 | S 随 着 中 国 经 济 突 飞 猛 近 , 建 造 工 业 与 日 俱 增 。 36 | T0-A0 随 着 中 国 经 济 突 飞 猛 进 , 建 造 工 业 与 日 俱 增 。 37 | A 9 10|||S|||进|||REQUIRED|||-NONE-|||0 38 | 39 | S 虽 然 工 业 的 发 展 和 城 市 规 模 的 扩 大 对 经 济 发 展 有 积 极 作 用 , 但 是 同 时 也 对 环 境 问 题 日 益 严 重 造 成 了 空 气 污 染 问 题 。 40 | T0-A0 虽 然 工 业 的 发 展 和 城 市 规 模 的 扩 大 对 经 济 发 展 有 积 极 作 用 , 但 是 同 时 也 对 环 境 造 成 了 日 益 严 重 的 空 气 污 染 问 题 。 41 | A 34 36|||S|||造 成 了|||REQUIRED|||-NONE-|||0 42 | A 40 43|||S|||的|||REQUIRED|||-NONE-|||0 43 | T1-A0 虽 然 工 业 的 发 展 和 城 市 规 模 的 扩 大 对 经 济 发 展 有 积 极 作 用 , 但 是 同 时 也 对 环 境 造 成 了 日 益 严 重 的 污 染 问 题 。 44 | A 34 36|||S|||造 成 了|||REQUIRED|||-NONE-|||1 45 | A 40 45|||S|||的|||REQUIRED|||-NONE-|||1 46 | 47 | S 那 些 空 气 污 染 也 没 有 助 于 人 生 的 身 体 建 康 。 48 | T0-A0 那 些 空 气 污 染 也 无 助 于 人 的 身 体 健 康 。 49 | A 7 9|||S|||无|||REQUIRED|||-NONE-|||0 50 | A 12 13|||R|||-NONE-|||REQUIRED|||-NONE-|||0 51 | A 16 17|||S|||健|||REQUIRED|||-NONE-|||0 52 | 53 | S 由 此 可 见 , 首 先 我 们 要 了 解 一 些 关 于 空 气 污 染 对 我 们 人 生 有 什 么 危 害 的 话 题 知 道 了 这 些 常 识 以 后 对 我 们 人 类 会 有 积 极 作 用 。 以 及 要 学 会 怎 样 应 对 和 治 理 空 气 污 染 的 问 题 。 54 | T0-A0 由 此 可 见 , 首 先 我 们 要 了 解 一 些 关 于 空 气 污 染 对 我 们 人 体 有 什 么 危 害 的 话 题 — — 知 道 这 些 常 识 对 我 们 人 类 会 有 积 极 作 用 , 以 及 要 学 会 怎 样 应 对 和 治 理 空 气 污 染 的 问 题 。 55 | A 24 25|||S|||体|||REQUIRED|||-NONE-|||0 56 | A 33 33|||M|||— —|||REQUIRED|||-NONE-|||0 57 | A 35 36|||R|||-NONE-|||REQUIRED|||-NONE-|||0 58 | A 40 42|||R|||-NONE-|||REQUIRED|||-NONE-|||0 59 | A 53 54|||S|||,|||REQUIRED|||-NONE-|||0 60 | T1-A0 由 此 可 见 , 首 先 我 们 要 了 解 一 些 关 于 空 气 污 染 对 我 们 人 体 有 什 么 危 害 的 话 题 — — 知 道 这 些 常 识 对 我 们 人 类 会 有 积 极 作 用 , 其 次 要 学 会 怎 样 应 对 和 治 理 空 气 污 染 的 问 题 。 61 | A 24 25|||S|||体|||REQUIRED|||-NONE-|||1 62 | A 33 33|||M|||— —|||REQUIRED|||-NONE-|||1 63 | A 35 36|||R|||-NONE-|||REQUIRED|||-NONE-|||1 64 | A 40 42|||R|||-NONE-|||REQUIRED|||-NONE-|||1 65 | A 53 56|||S|||, 其 次|||REQUIRED|||-NONE-|||1 66 | 67 | S 任 何 事 情 都 是 各 有 利 弊 , 众 所 周 知 越 建 立 工 业 越 对 经 济 方 面 有 所 发 展 。 68 | T0-A0 任 何 事 情 都 是 各 有 利 弊 , 众 所 周 知 越 发 展 工 业 , 经 济 方 面 就 越 有 所 发 展 。 69 | A 16 18|||S|||发 展|||REQUIRED|||-NONE-|||0 70 | A 20 22|||S|||,|||REQUIRED|||-NONE-|||0 71 | A 26 26|||M|||就 越|||REQUIRED|||-NONE-|||0 72 | T1-A0 任 何 事 情 都 是 各 有 利 弊 的 , 众 所 周 知 越 发 展 工 业 , 经 济 方 面 就 越 有 所 发 展 。 73 | A 10 10|||M|||的|||REQUIRED|||-NONE-|||1 74 | A 16 18|||S|||发 展|||REQUIRED|||-NONE-|||1 75 | A 20 22|||S|||,|||REQUIRED|||-NONE-|||1 76 | A 26 26|||M|||就 越|||REQUIRED|||-NONE-|||1 77 | 78 | S 对 我 看 来 , 曼 古 空 气 污 染 的 问 题 与 日 俱 增 。 79 | T0-A0 在 我 看 来 , 曼 古 空 气 污 染 的 问 题 与 日 俱 增 。 80 | A 0 1|||S|||在|||REQUIRED|||-NONE-|||0 81 | 82 | S 每 天 会 有 不 少 的 毒 气 体 泄 漏 从 工 厂 里 出 来 。 83 | T0-A0 每 天 会 有 不 少 的 有 毒 气 体 从 工 厂 里 泄 漏 出 来 。 84 | A 7 7|||M|||有|||REQUIRED|||-NONE-|||0 85 | A 10 16|||W|||从 工 厂 里 泄 漏|||REQUIRED|||-NONE-|||0 86 | T1-A0 每 天 会 有 不 少 的 有 毒 气 体 从 工 厂 里 泄 露 出 来 。 87 | A 7 7|||M|||有|||REQUIRED|||-NONE-|||1 88 | A 10 16|||W|||从 工 厂 里 泄 露|||REQUIRED|||-NONE-|||1 89 | 90 | S 在 工 厂 里 的 工 作 人 员 为 了 工 作 , 而 每 天 吸 了 不 少 的 毒 气 体 , 经 过 了 一 年 多 的 时 间 , 连 工 作 人 员 也 得 了 严 重 的 病 。 更 不 用 说 住 在 这 家 工 厂 近 的 家 庭 。 91 | T0-A0 在 工 厂 里 的 工 作 人 员 为 了 工 作 , 每 天 吸 了 不 少 的 有 毒 气 体 , 经 过 了 一 年 多 的 时 间 , 连 工 作 人 员 也 得 了 严 重 的 病 , 更 不 用 说 住 得 离 这 家 工 厂 近 的 家 庭 。 92 | A 14 15|||R|||-NONE-|||REQUIRED|||-NONE-|||0 93 | A 22 22|||M|||有|||REQUIRED|||-NONE-|||0 94 | A 48 49|||S|||,|||REQUIRED|||-NONE-|||0 95 | A 54 55|||S|||得 离|||REQUIRED|||-NONE-|||0 96 | T1-A0 在 工 厂 里 的 工 作 人 员 为 了 工 作 , 每 天 吸 了 不 少 的 有 毒 气 体 , 经 过 了 一 年 多 的 时 间 , 连 工 作 人 员 也 得 了 严 重 的 病 , 更 不 用 说 住 在 这 家 工 厂 附 近 的 家 庭 。 97 | A 14 15|||R|||-NONE-|||REQUIRED|||-NONE-|||1 98 | A 22 22|||M|||有|||REQUIRED|||-NONE-|||1 99 | A 48 49|||S|||,|||REQUIRED|||-NONE-|||1 100 | A 59 59|||M|||附|||REQUIRED|||-NONE-|||1 101 | 102 | S 沙 尘 暴 也 是 一 类 空 气 污 染 之 一 。 103 | T0-A0 沙 尘 暴 也 是 一 类 空 气 污 染 。 104 | A 11 13|||R|||-NONE-|||REQUIRED|||-NONE-|||0 105 | T1-A0 沙 尘 暴 也 是 空 气 污 染 之 一 。 106 | A 5 7|||R|||-NONE-|||REQUIRED|||-NONE-|||1 107 | 108 | S 不 官 是 从 口 、 眼 、 鼻 子 进 去 这 样 会 伤 害 身 体 的 建 康 。 109 | T0-A0 不 管 是 从 口 、 眼 还 是 鼻 子 进 去 , 都 会 伤 害 身 体 的 健 康 。 110 | A 1 2|||S|||管|||REQUIRED|||-NONE-|||0 111 | A 7 8|||S|||还 是|||REQUIRED|||-NONE-|||0 112 | A 12 14|||S|||, 都|||REQUIRED|||-NONE-|||0 113 | A 20 21|||S|||健|||REQUIRED|||-NONE-|||0 114 | 115 | S 这 样 做 会 避 免 受 到 沙 尘 暴 。 116 | T0-A0 这 样 做 会 避 免 受 到 沙 尘 暴 的 危 害 。 117 | A 11 11|||M|||的 危 害|||REQUIRED|||-NONE-|||0 118 | 119 | S 最 后 , 要 关 主 一 些 关 于 天 气 预 报 的 新 闻 。 120 | T0-A0 最 后 , 要 关 注 一 些 关 于 天 气 预 报 的 新 闻 。 121 | A 5 6|||S|||注|||REQUIRED|||-NONE-|||0 122 | 123 | S 中 国 , 悠 久 的 历 史 , 灿 烂 的 文 化 , 真 是 在 历 史 上 最 难 忘 的 国 家 。 124 | T0-A0 中 国 , 拥 有 悠 久 的 历 史 , 灿 烂 的 文 化 , 真 是 在 历 史 上 让 人 最 难 忘 的 国 家 。 125 | A 3 3|||M|||拥 有|||REQUIRED|||-NONE-|||0 126 | A 21 21|||M|||让 人|||REQUIRED|||-NONE-|||0 127 | 128 | S 对 一 个 生 名 来 说 空 气 污 染 是 很 危 害 的 问 题 , 对 身 体 不 好 。 129 | T0-A0 对 一 个 生 命 来 说 空 气 污 染 是 有 很 大 危 害 的 问 题 , 对 身 体 不 好 。 130 | A 4 5|||S|||命|||REQUIRED|||-NONE-|||0 131 | A 12 12|||M|||有|||REQUIRED|||-NONE-|||0 132 | A 13 13|||M|||大|||REQUIRED|||-NONE-|||0 133 | T1-A0 对 一 个 生 命 来 说 空 气 污 染 是 很 有 危 害 的 , 对 身 体 不 好 。 134 | A 4 5|||S|||命|||REQUIRED|||-NONE-|||1 135 | A 13 13|||M|||有|||REQUIRED|||-NONE-|||1 136 | A 16 18|||R|||-NONE-|||REQUIRED|||-NONE-|||1 137 | 138 | -------------------------------------------------------------------------------- /scorers/ChERRANT/modules/classifier.py: -------------------------------------------------------------------------------- 1 | from utils.char_smi import CharFuncs 2 | from collections import namedtuple 3 | from pypinyin import pinyin, Style 4 | import os 5 | Correction = namedtuple( 6 | "Correction", 7 | [ 8 | "op", 9 | "toks", 10 | "inds", 11 | ], 12 | ) 13 | file_path = os.path.dirname(os.path.abspath(__file__)) 14 | char_smi = CharFuncs(os.path.join(file_path.replace("modules", ""), 'data/char_meta.txt')) 15 | 16 | def check_spell_error(src_span: str, 17 | tgt_span: str, 18 | threshold: float = 0.8) -> bool: 19 | if len(src_span) != len(tgt_span): 20 | return False 21 | src_chars = [ch for ch in src_span] 22 | tgt_chars = [ch for ch in tgt_span] 23 | if sorted(src_chars) == sorted(tgt_chars): # 词内部字符异位 24 | return True 25 | for src_char, tgt_char in zip(src_chars, tgt_chars): 26 | if src_char != tgt_char: 27 | if src_char not in char_smi.data or tgt_char not in char_smi.data: 28 | return False 29 | v_sim = char_smi.shape_similarity(src_char, tgt_char) 30 | p_sim = char_smi.pronunciation_similarity(src_char, tgt_char) 31 | if v_sim + p_sim < threshold and not ( 32 | set(pinyin(src_char, style=Style.NORMAL, heteronym=True)[0]) & set(pinyin(tgt_char, style=Style.NORMAL, heteronym=True)[0])): 33 | return False 34 | return True 35 | 36 | class Classifier: 37 | """ 38 | 错误类型分类器 39 | """ 40 | def __init__(self, 41 | granularity: str = "word"): 42 | 43 | self.granularity = granularity 44 | 45 | @staticmethod 46 | def get_pos_type(pos): 47 | if pos in {"n", "nd"}: 48 | return "NOUN" 49 | if pos in {"nh", "ni", "nl", "ns", "nt", "nz"}: 50 | return "NOUN-NE" 51 | if pos in {"v"}: 52 | return "VERB" 53 | if pos in {"a", "b"}: 54 | return "ADJ" 55 | if pos in {"c"}: 56 | return "CONJ" 57 | if pos in {"r"}: 58 | return "PRON" 59 | if pos in {"d"}: 60 | return "ADV" 61 | if pos in {"u"}: 62 | return "AUX" 63 | # if pos in {"k"}: # TODO 后缀词比例太少,暂且分入其它 64 | # return "SUFFIX" 65 | if pos in {"m"}: 66 | return "NUM" 67 | if pos in {"p"}: 68 | return "PREP" 69 | if pos in {"q"}: 70 | return "QUAN" 71 | if pos in {"wp"}: 72 | return "PUNCT" 73 | return "OTHER" 74 | 75 | def __call__(self, 76 | src, 77 | tgt, 78 | edits, 79 | verbose: bool = False): 80 | """ 81 | 为编辑操作划分错误类型 82 | :param src: 错误句子信息 83 | :param tgt: 正确句子信息 84 | :param edits: 编辑操作 85 | :param verbose: 是否打印信息 86 | :return: 划分完错误类型后的编辑操作 87 | """ 88 | results = [] 89 | src_tokens = [x[0] for x in src] 90 | tgt_tokens = [x[0] for x in tgt] 91 | for edit in edits: 92 | error_type = edit[0] 93 | src_span = " ".join(src_tokens[edit[1]: edit[2]]) 94 | tgt_span = " ".join(tgt_tokens[edit[3]: edit[4]]) 95 | # print(tgt_span) 96 | cor = None 97 | if error_type[0] == "T": 98 | cor = Correction("W", tgt_span, (edit[1], edit[2])) 99 | elif error_type[0] == "D": 100 | if self.granularity == "word": # 词级别可以细分错误类型 101 | if edit[2] - edit[1] > 1: # 词组冗余暂时分为OTHER 102 | cor = Correction("R:OTHER", "-NONE-", (edit[1], edit[2])) 103 | else: 104 | pos = self.get_pos_type(src[edit[1]][1]) 105 | pos = "NOUN" if pos == "NOUN-NE" else pos 106 | pos = "MC" if tgt_span == "[缺失成分]" else pos 107 | cor = Correction("R:{:s}".format(pos), "-NONE-", (edit[1], edit[2])) 108 | else: # 字级别可以只需要根据操作划分类型即可 109 | cor = Correction("R", "-NONE-", (edit[1], edit[2])) 110 | elif error_type[0] == "I": 111 | if self.granularity == "word": # 词级别可以细分错误类型 112 | if edit[4] - edit[3] > 1: # 词组丢失暂时分为OTHER 113 | cor = Correction("M:OTHER", tgt_span, (edit[1], edit[2])) 114 | else: 115 | pos = self.get_pos_type(tgt[edit[3]][1]) 116 | pos = "NOUN" if pos == "NOUN-NE" else pos 117 | pos = "MC" if tgt_span == "[缺失成分]" else pos 118 | cor = Correction("M:{:s}".format(pos), tgt_span, (edit[1], edit[2])) 119 | else: # 字级别可以只需要根据操作划分类型即可 120 | cor = Correction("M", tgt_span, (edit[1], edit[2])) 121 | elif error_type[0] == "S": 122 | if self.granularity == "word": # 词级别可以细分错误类型 123 | if check_spell_error(src_span.replace(" ", ""), tgt_span.replace(" ", "")): 124 | cor = Correction("S:SPELL", tgt_span, (edit[1], edit[2])) 125 | # Todo 暂且不单独区分命名实体拼写错误 126 | # if edit[4] - edit[3] > 1: 127 | # cor = Correction("S:SPELL:COMMON", tgt_span, (edit[1], edit[2])) 128 | # else: 129 | # pos = self.get_pos_type(tgt[edit[3]][1]) 130 | # if pos == "NOUN-NE": # 命名实体拼写有误 131 | # cor = Correction("S:SPELL:NE", tgt_span, (edit[1], edit[2])) 132 | # else: # 普通词语拼写有误 133 | # cor = Correction("S:SPELL:COMMON", tgt_span, (edit[1], edit[2])) 134 | else: 135 | if edit[4] - edit[3] > 1: # 词组被替换暂时分为OTHER 136 | cor = Correction("S:OTHER", tgt_span, (edit[1], edit[2])) 137 | else: 138 | pos = self.get_pos_type(tgt[edit[3]][1]) 139 | pos = "NOUN" if pos == "NOUN-NE" else pos 140 | pos = "MC" if tgt_span == "[缺失成分]" else pos 141 | cor = Correction("S:{:s}".format(pos), tgt_span, (edit[1], edit[2])) 142 | else: # 字级别可以只需要根据操作划分类型即可 143 | cor = Correction("S", tgt_span, (edit[1], edit[2])) 144 | results.append(cor) 145 | if verbose: 146 | print("========== Corrections ==========") 147 | for cor in results: 148 | print("Type: {:s}, Position: {:d} -> {:d}, Target: {:s}".format(cor.op, cor.inds[0], cor.inds[1], cor.toks)) 149 | return results 150 | 151 | # print(pinyin("朝", style=Style.NORMAL)) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MuCGEC: A Multi-Reference Multi-Source Evaluation Dataset for Chinese Grammatical Error Correction & SOTA Models 2 | 3 | 4 | [English](README.en.md) | 简体中文 5 | 6 | ## 最新消息 7 | + **2022.5.26** 我们的最新工作NaSGEC被ACL2023会议录用,在这篇文章中我们提出一个多领域中文母语纠错数据集,以及面向**社交媒体**、**学术写作**、**复杂病句**的定制化纠错模型。欢迎大家试用!链接:[[Link]](https://github.com/HillZhang1999/NaSGEC) 8 | 9 | + **2023.1.12** 我们在阿里巴巴魔搭社区开源了两个最新的SOTA纠错模型(基于BART),分别面向通用领域和法律领域,支持一键调用推理和Demo试玩,欢迎大家试用:[通用领域](https://modelscope.cn/models/damo/nlp_bart_text-error-correction_chinese/summary),[法律领域](https://modelscope.cn/models/damo/nlp_bart_text-error-correction_chinese-law/summary)。 10 | 11 | + **2022.10.18** 我们的最新工作SynGEC被EMNLP2022会议录用,在这篇文章中我们提出的融入适配句法的SynGEC模型可以在NLPCC-18和MuCGEC-Test上取得45.32/46.51的F值。欢迎大家试用!链接:[[Link]](https://github.com/HillZhang1999/SynGEC) 12 | 13 | + **2022.8.29** 我们上传了MuCGEC数据集(含开发集答案和测试集输入)[[Link](https://github.com/HillZhang1999/MuCGEC/tree/main/data/MuCGEC)],并且在天池平台开放长期测试集评测榜单[[Link](https://tianchi.aliyun.com/dataset/dataDetail?dataId=131328#4)),欢迎大家提交结果,提交方式可以参考[[Link](https://github.com/HillZhang1999/MuCGEC/tree/main/data/MuCGEC#%E5%9C%A8%E7%BA%BF%E6%8F%90%E4%BA%A4%E6%B5%8B%E8%AF%95%E9%9B%86)]。 14 | 15 | + **2022.6.23** 我们开源了CTC-2021评测中所搜集的语义纠错模板,请参考:[[Github](https://github.com/HillZhang1999/gec_error_template)],[[论文](https://arxiv.org/abs/2206.11569)] 16 | 17 | + **2022.6.5** MuCGEC数据集作为CCL2022-CLTC评测的Track 4在[阿里云天池平台](https://tianchi.aliyun.com/dataset/dataDetail?dataId=131328)开放,欢迎大家使用和打榜! 18 | 19 | ## 引用 20 | 如果您认为我们的工作对您的工作有帮助,请引用我们的论文: 21 | 22 | #### MuCGEC: a Multi-Reference Multi-Source Evaluation Dataset for Chinese Grammatical Error Correction (Accepted by NAACL2022 main conference) [[PDF]](https://aclanthology.org/2022.naacl-main.227.pdf) 23 | 24 | ``` 25 | @inproceedings{zhang-etal-2022-mucgec, 26 | title = "{M}u{CGEC}: a Multi-Reference Multi-Source Evaluation Dataset for {C}hinese Grammatical Error Correction", 27 | author = "Zhang, Yue and 28 | Li, Zhenghua and 29 | Bao, Zuyi and 30 | Li, Jiacheng and 31 | Zhang, Bo and 32 | Li, Chen and 33 | Huang, Fei and 34 | Zhang, Min", 35 | booktitle = "Proceedings of the 2022 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies", 36 | month = jul, 37 | year = "2022", 38 | address = "Seattle, United States", 39 | publisher = "Association for Computational Linguistics", 40 | url = "https://aclanthology.org/2022.naacl-main.227", 41 | pages = "3118--3130", 42 | } 43 | ``` 44 | 45 | ## 简介 46 | 47 | 给定一段中文文本,中文语法纠错(Chinese Grammatical Error Correction, CGEC)技术旨在对其中存在的拼写、词法、语法、语义等各类错误进行自动纠正。该技术在教育、新闻、通讯乃至搜索等领域都拥有着广阔的应用空间。 48 | 49 | 目前的中文语法纠错评测集存在着数据量小,答案少,领域单一等缺陷。为了提供更加合理的模型评估结果,本仓库提供了一个高质量、多答案CGEC评测数据集**MuCGEC**。与此同时,为了推动CGEC领域的发展,我们还额外提供了如下资源: 50 | 51 | + **中文语法纠错数据标注规范`./guidelines`**:我们详细定义了常见的中文语法错误的类别体系,针对每类错误,给出了对应的修改方案和丰富的修改样例,从而可以促进中文语法纠错数据标注领域的研究。 52 | + **中文语法纠错评测工具`./scorers`**: 53 | + `ChERRANT`:我们对目前英文上通用的可细分类别评估的评测工具[ERRANT](https://github.com/chrisjbryant/errant)进行了中文适配和修改,并命名为`ChERRANT`(**Ch**inese **ERRANT**)。ChERRANT支持字、词粒度的评估。字级别的ChERRANT指标是MuCGEC数据集主要使用的评测指标,缓解了中文上因为分词错误导致的评估不准确现象。词粒度的评估支持更细的错误类型(如拼写错误、名词错误、动词错误等),可供研究人员更好地分析模型。 54 | + **中文语法纠错基线模型`./models`**: 55 | + **Seq2Edit模型`./models/seq2edit-based-CGEC`**:设计编辑动作标签(如替换、删除、插入、调序等),从而将语法纠错任务视作序列标注任务进行解决。 56 | + 我们对英文上SOTA的Seq2Edit模型[GECToR](https://github.com/grammarly/gector)进行了一些修改,以使其支持中文。 57 | + **Seq2Seq模型`./models/seq2seq-based-CGEC`**:将语法纠错看做是一个从错误句子翻译为正确句子的过程,利用先进的神经机器翻译模型进行解决。 58 | + 我们微调了大规模Seq2Seq预训练语言模型[中文BART](https://github.com/fastnlp/CPT)用于语法纠错任务。 59 | + **集成模型`./scorers/ChERRANT/emsemble.sh`**:我们提供了一种简单的基于编辑的模型集成方法,支持异构模型(如Seq2Seq和Seq2Edit)的融合。 60 | + **中文语法纠错常用工具`./tools`**: 61 | + **分词工具** 62 | + **数据增强** (*Todo*) 63 | + **数据清洗** (*Todo*) 64 | 65 | ## MuCGEC数据集 66 | 67 | 我们的数据主要来自中文二语学习者,分别采样自以下数据集:`NLPCC18`测试集(来自于NLPCC18-shared Task2评测任务)、`CGED`测试集(来自于CGED18&20评测任务)以及中文`Lang8`训练集(来自于NLPCC18-shared Task2评测任务)。我们从三个数据来源各采样2000-3000句,采用三人随机标注加审核专家审核方式构建测试集。数据的整体统计如下表所示。 68 | 69 | | 数据集 | 句子数| 错误句子数(比例) | 平均字数 | 平均编辑数 | 平均答案数 | 70 | | :------- | :---------: | :---------: | :---------: | :---------: | :---------: | 71 | | **MuCGEC-NLPCC18** | 1996 | 1904(95.4%) | 29.7 | 2.5 | 2.5 | 72 | | **MuCGEC-CGED** | 3125 | 2988(95.6%) | 44.8 | 4.0 | 2.3 | 73 | | **MuCGEC-Lang8** | 1942 | 1652(85.1%) | 37.5 | 2.8 | 2.1 | 74 | | **MuCGEC-ALL** | 7063 | 6544(92.7%) | 38.5 | 3.2 | 2.3 | 75 | 76 | 相较于之前的CGEC评测集(如NLPCC18和CGED),MuCGEC拥有更丰富的答案和数据来源。此外,在标注过程中,我们还发现有74句句子因为句意不清等问题无法标注。 77 | 78 | 更多关于MuCGEC数据集的细节,请参考我们的论文。 79 | 80 | ### 数据下载链接 81 | 82 | **MuCGEC数据集目前开放了开发集,测试集以在线榜单形式给出,请参考链接[https://tianchi.aliyun.com/dataset/dataDetail?dataId=131328](https://tianchi.aliyun.com/dataset/dataDetail?dataId=131328) 使用。** 83 | 84 | 85 | ## CGEC基准模型 86 | 87 | ### 实验环境安装 88 | 89 | 我们采用Python 3.8进行实验,通过以下代码可以安装必要的依赖,考虑到Seq2Edit模型的环境和Seq2Seq模型存在一些冲突,需要分别安装两个环境: 90 | ``` 91 | # Seq2Edit模型 92 | pip install -r requirements_seq2edit.txt 93 | 94 | # Seq2Seq模型 95 | pip install -r requirements_seq2seq.txt 96 | ``` 97 | 98 | ### 训练数据 99 | 100 | 我们实验所用训练集为:`Lang8`数据集(来自外语学习网站Lang8)和`HSK`数据集(北语开发的汉语学习考试数据集)中的错误句子,并且对`HSK`数据集上采样5次,过滤掉和我们测试集重复的部分,共计约150万对。 101 | 102 | 下载方式:[Google Drive](https://drive.google.com/file/d/1l0A50z7fMXjQT3y2ct7TQsEHqOwlvg0_/view?usp=sharing) 103 | 104 | ### 模型使用 105 | 我们提供了使用模型的流水线脚本,包含预处理-训练-推理的流程,可参考`./models/seq2edit-based-CGEC/pipeline.sh`及`./models/seq2seq-based-CGEC/pipeline.sh` 106 | 107 | 与此同时,我们也提供了训练后的checkpoint以供测试(下列指标均为精确度/召回度/F0.5值): 108 | 109 | | 模型 | NLPCC18-Official(m2socrer)| MuCGEC(ChERRANT)| 110 | | :-------: | :---------:| :---------: | 111 | | **seq2seq_lang8**[[Link](https://drive.google.com/file/d/1Jras2Km4ScdVB0sx8ePg-PqCmDC4O8v5/view?usp=sharing)] | 37.78/29.91/35.89 | 40.44/26.71/36.67 | 112 | | **seq2seq_lang8+hsk**[[Link](https://drive.google.com/file/d/180CXiW7pDz0wcbeTgszVoBrvzRmXzeZ9/view?usp=sharing)] | 41.50/32.87/39.43 | 44.02/28.51/39.70| 113 | | **seq2edit_lang8**[[Link](https://drive.google.com/file/d/13OAJ9DSThqssl93bSn0vQetetLhQz5LA/view?usp=sharing)] | 37.43/26.29/34.50 | 38.08/22.90/33.62 | 114 | | **seq2edit_lang8+hsk**[[Link](https://drive.google.com/file/d/1ce7t8r3lLUeJ4eIxIg3EpXwHIUE99nk8/view?usp=sharing)] | 43.12/30.18/39.72 | 44.65/27.32/39.62| 115 | 116 | 下载后,分别解压放入`./models/seq2seq-based-CGEC/exps`和`./models/seq2edit-based-CGEC/exps`即可使用。其中,seq2seq模型基于`Chinese-BART-Large`预训练语言模型,seq2edit模型基于`StructBERT-Large`预训练语言模型。 117 | 118 | 我们在论文中使用的模型融合策略请参考`./scorers/ChERRANT/ensemble.sh`。 119 | 120 | ### Tips 121 | + 我们发现在英文上有用的一些trick,在中文上同样有效,例如GECToR的额外置信度trick和Seq2Seq的R2L-Reranking trick,如果您对模型性能要求较高,可以尝试这些trick。 122 | + 我们发现两阶段训练(先Lang8+Hsk再单独Hsk)所得模型效果相较于单阶段训练效果会有进一步提升,您感兴趣的话可以按照两阶段训练策略重新训练模型。 123 | + 我们发现基于[中文BART](https://huggingface.co/fnlp/bart-large-chinese)的Seq2Seq模型存在一些改进空间:1)原始中文BART的词表缺少一些常见的中文标点/字符;2)transformers库训练和推理速度较慢,所占显存也较大。我们最近基于[fairseq](https://github.com/pytorch/fairseq)重新实现了一版基于BART的Seq2Seq模型,并加入了一些额外的训练trick,使其效果有了大幅提升(4-5个F值),且训练/推理速度快了3-4倍。该工作后续也将整理并开源。 124 | + 我们目前提供的基线模型仅使用了公开训练集。关于数据增强技术,可以参考我们之前在CTC2021比赛中的方案[[Link]](https://github.com/HillZhang1999/CTC-Report),合理构建的人造数据对模型性能有着巨大的提升。 125 | 126 | ### 模型评估 127 | 128 | 针对[NLPCC18官方数据集](http://tcci.ccf.org.cn/conference/2018/taskdata.php),可使用我们的基准模型预测后,通过NLPCC18的官方工具[M2Scorer](https://github.com/nusnlp/m2scorer)进行计算指标。需要注意的是预测结果必须使用PKUNLP工具分词。 129 | 130 | 针对MuCGEC数据集的相关指标,可以采用我们提供的[ChERRANT](./scorers/ChERRANT)工具进行指标计算。 131 | ChERRANT的相关使用可参考`./scorers/ChERRANT/demo.sh`。对于字级别指标,我们部分参考了[ERRANT_zh](https://github.com/cehinson/ERRANT_ZH)仓库,词级别指标及错误类型划分我们则参考了[原始ERRANT](https://github.com/chrisjbryant/errant)。 132 | 133 | **错误类型** 134 | + 操作级别(字/词粒度): 135 | + M(missing):缺失错误,需要添加缺失字/词 136 | + R(redundant):冗余错误,需要删除冗余字/词 137 | + S(substitute):替换错误,需要替换错误字/词 138 | + W(word-order):词序错误,需要进行调序 139 | 140 | + 语言学级别(仅词粒度): 141 | + 我们设计了14种主要的语言学错误类型(基本上是根据词性),除拼写错误(SPELL)和词序错误(W)外,还可以根据替换/删除/冗余操作进一步划分,如`形容词冗余错误`可以表示为:R:ADJ 142 | 143 | ![error types](./pics/errors.PNG) 144 | 145 | 146 | ## 相关工作 147 | + 我们在CTC2021评测中使用了本仓库的一些技术,并且获得了Top1的成绩,相关技术报告可见:[CTC-report](https://github.com/HillZhang1999/CTC-Report)。 148 | + 我们的基线模型提供在线演示平台:[GEC demo](http://139.224.234.18:5002/) (校外访问可能较慢,请耐心等待)。 149 | + YACLC中文学习者语料库:[YACLC](https://github.com/blcuicall/YACLC)。 150 | + NLPCC18中文语法纠错数据集:[NLPCC18](https://github.com/zhaoyyoo/NLPCC2018_GEC)。 151 | 152 | ## 联系 153 | 如果您在使用我们的数据集及代码的过程中遇到了任何问题,可联系 hillzhang1999@qq.com。 154 | -------------------------------------------------------------------------------- /models/seq2edit-based-CGEC/gector/datareader.py: -------------------------------------------------------------------------------- 1 | """Tweaked AllenNLP dataset reader.""" 2 | import logging 3 | import re 4 | from random import random 5 | from typing import Dict, List 6 | from allennlp.common.file_utils import cached_path 7 | from allennlp.data.dataset_readers.dataset_reader import DatasetReader 8 | from allennlp.data.fields import TextField, SequenceLabelField, MetadataField, Field, ListField 9 | from allennlp.data.instance import Instance 10 | from allennlp.data.token_indexers import TokenIndexer, SingleIdTokenIndexer 11 | from allennlp.data.tokenizers import Token 12 | from overrides import overrides 13 | from utils.helpers import SEQ_DELIMETERS, START_TOKEN 14 | 15 | logger = logging.getLogger(__name__) # pylint: disable=invalid-name 16 | 17 | 18 | @DatasetReader.register("seq2labels_datareader") 19 | class Seq2LabelsDatasetReader(DatasetReader): 20 | """ 21 | Reads instances from a pretokenised file where each line is in the following format: 22 | 23 | WORD###TAG [TAB] WORD###TAG [TAB] ..... \n 24 | 25 | and converts it into a ``Dataset`` suitable for sequence tagging. You can also specify 26 | alternative delimiters in the constructor. 27 | 28 | Parameters 29 | ---------- 30 | delimiters: ``dict`` 31 | The dcitionary with all delimeters. 32 | token_indexers : ``Dict[str, TokenIndexer]``, optional (default=``{"tokens": SingleIdTokenIndexer()}``) 33 | We use this to define the input representation for the text. See :class:`TokenIndexer`. 34 | Note that the `output` tags will always correspond to single token IDs based on how they 35 | are pre-tokenised in the data file. 36 | max_len: if set than will truncate long sentences 37 | """ 38 | # fix broken sentences mostly in Lang8 39 | BROKEN_SENTENCES_REGEXP = re.compile(r'\.[a-zA-RT-Z]') 40 | 41 | def __init__(self, 42 | token_indexers: Dict[str, TokenIndexer] = None, 43 | delimeters: dict = SEQ_DELIMETERS, 44 | skip_correct: bool = False, 45 | skip_complex: int = 0, 46 | lazy: bool = False, 47 | max_len: int = None, 48 | test_mode: bool = False, 49 | tag_strategy: str = "keep_one", 50 | tn_prob: float = 0, 51 | tp_prob: float = 0, 52 | broken_dot_strategy: str = "keep") -> None: 53 | """ 54 | 数据读取器类 55 | :param token_indexers: token编号器 56 | :param delimeters: 分隔符 57 | :param skip_correct: 是否跳过正确的句子(在GECToR训练的第二阶段,只训练错误的句子) 58 | :param skip_complex: 是否跳过过于复杂的句子(是否含有编辑次数大于skip_complex的source_token) 59 | :param lazy: 是否为懒加载模式(节约内存) 60 | :param max_len: 是否对句子中长于max_len的token做截断 61 | :param test_mode: 是否测试模式 62 | :param tag_strategy: 两种抽取编辑label的策略:1)只保留各source token第一个的编辑label(GECToR的做法)。2)将各source token 所有的编辑label合并为一个(PIE的做法)。 63 | :param tn_prob: 按照某一概率阈值,跳过TN 64 | :param tp_prob: 按照某一概率阈值,跳过TP 65 | :param broken_dot_strategy: 针对破碎的句子的策略(尤其是lang-8中的数据) 66 | :param granularity: 模型的粒度 67 | """ 68 | super().__init__(lazy) 69 | self._token_indexers = token_indexers 70 | self._delimeters = delimeters 71 | self._max_len = max_len 72 | self._skip_correct = skip_correct 73 | self._skip_complex = skip_complex 74 | self._tag_strategy = tag_strategy 75 | self._broken_dot_strategy = broken_dot_strategy 76 | self._test_mode = test_mode 77 | self._tn_prob = tn_prob 78 | self._tp_prob = tp_prob 79 | 80 | @overrides 81 | def _read(self, file_path): 82 | # if `file_path` is a URL, redirect to the cache 83 | file_path = cached_path(file_path) 84 | with open(file_path, "r") as data_file: 85 | logger.info("Reading instances from lines in file at: %s", file_path) 86 | for line in data_file: 87 | line = line.strip("\n") 88 | # skip blank and broken lines 89 | if not line or (not self._test_mode and self._broken_dot_strategy == 'skip' 90 | and self.BROKEN_SENTENCES_REGEXP.search(line) is not None): 91 | continue 92 | tokens_and_tags = [pair.rsplit(self._delimeters['labels'], 1) 93 | for pair in line.split(self._delimeters['tokens'])] 94 | try: 95 | tokens = [Token(token) for token, tag in tokens_and_tags] 96 | tags = [tag for token, tag in tokens_and_tags] 97 | except ValueError: 98 | tokens = [Token(token) for token, tag in tokens_and_tags] 99 | tags = [tag for token, tag in tokens_and_tags] 100 | 101 | if tokens and tokens[0] != Token(START_TOKEN): 102 | tokens = [Token(START_TOKEN)] + tokens 103 | 104 | words = [x.text for x in tokens] 105 | if self._max_len is not None: 106 | tokens = tokens[:self._max_len] 107 | tags = None if tags is None else tags[:self._max_len] 108 | instance = self.text_to_instance(tokens, tags, words) 109 | if instance: 110 | yield instance 111 | 112 | def extract_tags(self, tags: List[str]): 113 | op_del = self._delimeters['operations'] 114 | 115 | labels = [x.split(op_del) for x in tags] # 将每个source token的所有编辑label分开 116 | labels_final = [] 117 | 118 | complex_flag_dict = {} 119 | # get flags 120 | for i in range(5): 121 | idx = i + 1 122 | complex_flag_dict[idx] = sum([len(x) > idx for x in labels]) # 统计编辑次数大于idx的source token个数 123 | 124 | if self._tag_strategy == "keep_one": # 两种抽取编辑label的策略:1)只保留各source token第一个的编辑label(GECToR的做法)。2)将各source token 所有的编辑label合并为一个(PIE的做法)。 125 | # get only first candidates for r_tags in right and the last for left 126 | for x in labels: # 按照论文中的做法,如果当前token有多个编辑,选择第一个不是$KEEP的 127 | if len(x) == 1: 128 | labels_final.append(x[0]) 129 | elif len(x) > 5: 130 | if self._skip_complex: 131 | labels_final.append("$KEEP") 132 | else: 133 | labels_final.append(x[1] if x[0] == "$KEEP" else x[0]) 134 | else: 135 | labels_final.append(x[1] if x[0] == "$KEEP" else x[0]) 136 | 137 | elif self._tag_strategy == "merge_all": 138 | # consider phrases as a words 139 | pass 140 | else: 141 | raise Exception("Incorrect tag strategy") 142 | 143 | detect_tags = ["CORRECT" if label == "$KEEP" else "INCORRECT" for label in labels_final] # 抽取一下当前token是否有误的标签,后面要用到。 144 | return labels_final, detect_tags, complex_flag_dict 145 | 146 | def text_to_instance(self, tokens: List[Token], 147 | tags: List[str] = None, 148 | words: List[str] = None) -> Instance: # type: ignore 149 | """ 150 | We take `pre-tokenized` input here, because we don't have a tokenizer in this class. 151 | """ 152 | # pylint: disable=arguments-differ 153 | fields: Dict[str, Field] = {} 154 | sequence = TextField(tokens, self._token_indexers) 155 | fields["tokens"] = sequence 156 | fields["metadata"] = MetadataField({"words": words}) 157 | if tags is not None: 158 | labels, detect_tags, complex_flag_dict = self.extract_tags(tags) 159 | if self._skip_complex: # 跳过过于复杂的句子(句子的复杂程度定义为是否有需要很多次编辑的source token) 160 | if detect_tags.count("INCORRECT") / len(detect_tags) > 0.8: 161 | return None 162 | rnd = random() 163 | # skip TN 164 | if self._skip_correct and all(x == "CORRECT" for x in detect_tags): 165 | if rnd > self._tn_prob: 166 | return None 167 | # skip TP 168 | else: 169 | if rnd > self._tp_prob: 170 | return None 171 | 172 | fields["labels"] = SequenceLabelField(labels, sequence, 173 | label_namespace="labels") 174 | fields["d_tags"] = SequenceLabelField(detect_tags, sequence, 175 | label_namespace="d_tags") 176 | return Instance(fields) 177 | -------------------------------------------------------------------------------- /models/seq2edit-based-CGEC/predict.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | import os 3 | from transformers import BertModel 4 | import torch 5 | import tokenization 6 | import argparse 7 | from gector.gec_model import GecBERTModel 8 | import re 9 | from opencc import OpenCC 10 | 11 | cc = OpenCC("t2s") 12 | 13 | def split_sentence(document: str, flag: str = "all", limit: int = 510): 14 | """ 15 | Args: 16 | document: 17 | flag: Type:str, "all" 中英文标点分句,"zh" 中文标点分句,"en" 英文标点分句 18 | limit: 默认单句最大长度为510个字符 19 | Returns: Type:list 20 | """ 21 | sent_list = [] 22 | try: 23 | if flag == "zh": 24 | document = re.sub('(?P([。?!](?![”’"\'])))', r'\g\n', document) # 单字符断句符 25 | document = re.sub('(?P([。?!])[”’"\'])', r'\g\n', document) # 特殊引号 26 | elif flag == "en": 27 | document = re.sub('(?P([.?!](?![”’"\'])))', r'\g\n', document) # 英文单字符断句符 28 | document = re.sub('(?P([?!.]["\']))', r'\g\n', document) # 特殊引号 29 | else: 30 | document = re.sub('(?P([。?!….?!](?![”’"\'])))', r'\g\n', document) # 单字符断句符 31 | document = re.sub('(?P(([。?!.!?]|…{1,2})[”’"\']))', r'\g\n', 32 | document) # 特殊引号 33 | 34 | sent_list_ori = document.splitlines() 35 | for sent in sent_list_ori: 36 | sent = sent.strip() 37 | if not sent: 38 | continue 39 | else: 40 | while len(sent) > limit: 41 | temp = sent[0:limit] 42 | sent_list.append(temp) 43 | sent = sent[limit:] 44 | sent_list.append(sent) 45 | except: 46 | sent_list.clear() 47 | sent_list.append(document) 48 | return sent_list 49 | 50 | 51 | def predict_for_file(input_file, output_file, model, batch_size, log=True, seg=False): 52 | with open(input_file, 'r', encoding='utf-8') as f: 53 | lines = f.readlines() 54 | sents = [s.strip() for s in lines] 55 | subsents = [] 56 | s_map = [] 57 | for i, sent in enumerate(sents): # 将篇章划分为子句,分句预测再合并 58 | if seg: 59 | subsent_list = split_sentence(sent, flag="zh") 60 | else: 61 | subsent_list = [sent] 62 | s_map.extend([i for _ in range(len(subsent_list))]) 63 | subsents.extend(subsent_list) 64 | assert len(subsents) == len(s_map) 65 | predictions = [] 66 | cnt_corrections = 0 67 | batch = [] 68 | for sent in subsents: 69 | batch.append(sent.split()) 70 | if len(batch) == batch_size: # 如果数据够了一个batch的话, 71 | preds, cnt = model.handle_batch(batch) 72 | assert len(preds) == len(batch) 73 | predictions.extend(preds) 74 | cnt_corrections += cnt 75 | if log: 76 | for z in zip(batch, preds): 77 | print("source: " + "".join(z[0])) 78 | print("target: " + "".join(z[1])) 79 | print() 80 | batch = [] 81 | 82 | if batch: 83 | preds, cnt = model.handle_batch(batch) 84 | assert len(preds) == len(batch) 85 | predictions.extend(preds) 86 | cnt_corrections += cnt 87 | if log: 88 | for z in zip(batch, preds): 89 | print("source: " + "".join(z[0])) 90 | print("target: " + "".join(z[1])) 91 | print() 92 | 93 | assert len(subsents) == len(predictions) 94 | if output_file: 95 | with open(output_file, 'w') as f1: 96 | with open(output_file + ".char", 'w') as f2: 97 | results = ["" for _ in range(len(sents))] 98 | for i, ret in enumerate(predictions): 99 | ret_new = [tok.lstrip("##") for tok in ret] 100 | ret = cc.convert("".join(ret_new)) 101 | results[s_map[i]] += cc.convert(ret) 102 | tokenizer = tokenization.FullTokenizer(vocab_file="vocab.txt", do_lower_case=False) 103 | for ret in results: 104 | f1.write(ret + "\n") 105 | line = tokenization.convert_to_unicode(ret) 106 | tokens = tokenizer.tokenize(line) 107 | f2.write(" ".join(tokens) + "\n") 108 | return cnt_corrections 109 | 110 | 111 | def main(args): 112 | # get all paths 113 | model = GecBERTModel(vocab_path=args.vocab_path, 114 | model_paths=args.model_path.split(','), 115 | weights_names=args.weights_name.split(','), 116 | max_len=args.max_len, min_len=args.min_len, 117 | iterations=args.iteration_count, 118 | min_error_probability=args.min_error_probability, 119 | min_probability=args.min_error_probability, 120 | log=False, 121 | confidence=args.additional_confidence, 122 | is_ensemble=args.is_ensemble, 123 | weigths=args.weights, 124 | cuda_device=args.cuda_device 125 | ) 126 | cnt_corrections = predict_for_file(args.input_file, args.output_file, model, 127 | batch_size=args.batch_size, log=args.log, seg=args.seg) 128 | print(f"Produced overall corrections: {cnt_corrections}") 129 | 130 | 131 | if __name__ == '__main__': 132 | # read parameters 133 | parser = argparse.ArgumentParser() 134 | parser.add_argument('--model_path', 135 | help='Path to the model file', 136 | required=True) # GECToR模型文件,多个模型集成的话,可以用逗号隔开 137 | parser.add_argument('--weights_name', 138 | help='Path to the pre-trained language model', 139 | default='chinese-struct-bert', 140 | required=True) # 预训练语言模型文件,多个模型集成的话,每个模型对应一个PLM,可以用逗号隔开 141 | parser.add_argument('--vocab_path', 142 | help='Path to the vocab file', 143 | default='./data/output_vocabulary_chinese_char_hsk+lang8_5', 144 | ) # 词表文件 145 | parser.add_argument('--input_file', 146 | help='Path to the input file', 147 | required=True) # 输入文件,要求:预先分好词/字 148 | parser.add_argument('--output_file', 149 | help='Path to the output file', 150 | required=True) # 输出结果文件 151 | parser.add_argument('--max_len', 152 | type=int, 153 | help='The max sentence length' 154 | '(all longer will be truncated)', 155 | default=200) # 最大输入长度(token数目),大于该长度的输入将被截断 156 | parser.add_argument('--min_len', 157 | type=int, 158 | help='The minimum sentence length' 159 | '(all longer will be returned w/o changes)', 160 | default=0) # 最小修改长度(token数目),小于该长度的输入将不被修改 161 | parser.add_argument('--batch_size', 162 | type=int, 163 | help='The number of sentences in a batch when predicting', 164 | default=128) # 预测时的batch大小(句子数目) 165 | parser.add_argument('--iteration_count', 166 | type=int, 167 | help='The number of iterations of the model', 168 | default=5) # 迭代修改轮数 169 | parser.add_argument('--additional_confidence', 170 | type=float, 171 | help='How many probability to add to $KEEP token', 172 | default=0.0) # Keep标签额外置信度 173 | parser.add_argument('--min_probability', 174 | type=float, 175 | default=0) # token级别最小修改阈值 176 | parser.add_argument('--min_error_probability', 177 | type=float, 178 | default=0.0) # 句子级别最小修改阈值 179 | parser.add_argument('--is_ensemble', 180 | type=int, 181 | help='Whether to do ensembling.', 182 | default=0) # 是否进行模型融合 183 | parser.add_argument('--weights', 184 | help='Used to calculate weighted average', nargs='+', 185 | default=None) # 不同模型的权重(加权集成) 186 | parser.add_argument('--cuda_device', 187 | help='The number of GPU', 188 | default=0) # 使用GPU编号 189 | parser.add_argument('--log', 190 | action='store_true') # 是否输出完整信息 191 | parser.add_argument('--seg', 192 | action='store_true') # 是否切分长句预测后再合并 193 | args = parser.parse_args() 194 | main(args) 195 | -------------------------------------------------------------------------------- /models/seq2seq-based-CGEC/utils.py: -------------------------------------------------------------------------------- 1 | 2 | from dataclasses import dataclass, field 3 | from typing import Optional 4 | import sys 5 | import json 6 | from datasets import Dataset 7 | 8 | def read_lines(file): 9 | lines = [] 10 | for line in file: 11 | line = line.rstrip("\n") 12 | lines.append(line.replace(" ", "")) 13 | return lines 14 | 15 | def load_json(file_path): 16 | results={'summarization':[],'article':[]} 17 | with open(file_path,encoding='utf-8') as f: 18 | content=json.load(f) 19 | for sample in content: 20 | results['summarization'].append(sample['summarization']) 21 | results['article'].append(sample['article']) 22 | results=Dataset.from_dict(results) 23 | return results 24 | 25 | def convert_parallel_data_to_json_file(source_data_file, target_data_file, json_data_file): 26 | json_list = [] 27 | with open(source_data_file, "r", encoding='utf-8') as f1: 28 | with open(target_data_file, "r", encoding='utf-8') as f2: 29 | src_lines, tgt_lines = read_lines(f1), read_lines(f2) 30 | for s, t in zip(src_lines, tgt_lines): 31 | if s != t: 32 | dic = {"summarization": t, "article": s} 33 | json_list.append(dic) 34 | json.dump(json_list, open(json_data_file, "w", encoding='utf-8'), ensure_ascii=False) 35 | 36 | def convert_input_data_to_json_file(input_data_file, json_data_file): 37 | json_list = [] 38 | with open(input_data_file, "r", encoding='utf-8') as f: 39 | src_lines = read_lines(f) 40 | for s in src_lines: 41 | dic = {"summarization": "", "article": s} 42 | json_list.append(dic) 43 | json.dump(json_list, open(json_data_file, "w", encoding='utf-8'), ensure_ascii=False) 44 | 45 | @dataclass 46 | class ModelArguments: 47 | """ 48 | Arguments pertaining to which model/config/tokenizer we are going to fine-tune from. 49 | """ 50 | 51 | model_name_or_path: str = field( 52 | metadata={"help": "Path to pretrained model or model identifier from huggingface.co/models"} 53 | ) 54 | config_name: Optional[str] = field( 55 | default=None, metadata={"help": "Pretrained config name or path if not the same as model_name"} 56 | ) 57 | tokenizer_name: Optional[str] = field( 58 | default=None, metadata={"help": "Pretrained tokenizer name or path if not the same as model_name"} 59 | ) 60 | cache_dir: Optional[str] = field( 61 | default=None, 62 | metadata={"help": "Where to store the pretrained models downloaded from huggingface.co"}, 63 | ) 64 | use_fast_tokenizer: bool = field( 65 | default=True, 66 | metadata={"help": "Whether to use one of the fast tokenizer (backed by the tokenizers library) or not."}, 67 | ) 68 | model_revision: str = field( 69 | default="main", 70 | metadata={"help": "The specific model version to use (can be a branch name, tag name or commit id)."}, 71 | ) 72 | use_auth_token: bool = field( 73 | default=False, 74 | metadata={ 75 | "help": "Will use the token generated when running `transformers-cli login` (necessary to use this script " 76 | "with private models)." 77 | }, 78 | ) 79 | 80 | 81 | @dataclass 82 | class DataTrainingArguments: 83 | """ 84 | Arguments pertaining to what data we are going to input our model for training and eval. 85 | """ 86 | 87 | dataset_name: Optional[str] = field( 88 | default=None, metadata={"help": "The name of the dataset to use (via the datasets library)."} 89 | ) 90 | dataset_config_name: Optional[str] = field( 91 | default=None, metadata={"help": "The configuration name of the dataset to use (via the datasets library)."} 92 | ) 93 | text_column: Optional[str] = field( 94 | default=None, 95 | metadata={"help": "The name of the column in the datasets containing the full texts (for summarization)."}, 96 | ) 97 | summary_column: Optional[str] = field( 98 | default=None, 99 | metadata={"help": "The name of the column in the datasets containing the summaries (for summarization)."}, 100 | ) 101 | train_file: Optional[str] = field( 102 | default=None, metadata={"help": "The input training data file (a jsonlines or csv file)."} 103 | ) 104 | validation_file: Optional[str] = field( 105 | default=None, 106 | metadata={ 107 | "help": "An optional input evaluation data file to evaluate the metrics (rouge) on " 108 | "(a jsonlines or csv file)." 109 | }, 110 | ) 111 | test_file: Optional[str] = field( 112 | default=None, 113 | metadata={ 114 | "help": "An optional input test data file to evaluate the metrics (rouge) on " "(a jsonlines or csv file)." 115 | }, 116 | ) 117 | overwrite_cache: bool = field( 118 | default=False, metadata={"help": "Overwrite the cached training and evaluation sets"} 119 | ) 120 | preprocessing_num_workers: Optional[int] = field( 121 | default=None, 122 | metadata={"help": "The number of processes to use for the preprocessing."}, 123 | ) 124 | max_source_length: Optional[int] = field( 125 | default=1024, 126 | metadata={ 127 | "help": "The maximum total input sequence length after tokenization. Sequences longer " 128 | "than this will be truncated, sequences shorter will be padded." 129 | }, 130 | ) 131 | max_target_length: Optional[int] = field( 132 | default=128, 133 | metadata={ 134 | "help": "The maximum total sequence length for target text after tokenization. Sequences longer " 135 | "than this will be truncated, sequences shorter will be padded." 136 | }, 137 | ) 138 | val_max_target_length: Optional[int] = field( 139 | default=None, 140 | metadata={ 141 | "help": "The maximum total sequence length for validation target text after tokenization. Sequences longer " 142 | "than this will be truncated, sequences shorter will be padded. Will default to `max_target_length`." 143 | "This argument is also used to override the ``max_length`` param of ``model.generate``, which is used " 144 | "during ``evaluate`` and ``predict``." 145 | }, 146 | ) 147 | pad_to_max_length: bool = field( 148 | default=False, 149 | metadata={ 150 | "help": "Whether to pad all samples to model maximum sentence length. " 151 | "If False, will pad the samples dynamically when batching to the maximum length in the batch. More " 152 | "efficient on GPU but very bad for TPU." 153 | }, 154 | ) 155 | max_train_samples: Optional[int] = field( 156 | default=None, 157 | metadata={ 158 | "help": "For debugging purposes or quicker training, truncate the number of training examples to this " 159 | "value if set." 160 | }, 161 | ) 162 | max_val_samples: Optional[int] = field( 163 | default=None, 164 | metadata={ 165 | "help": "For debugging purposes or quicker training, truncate the number of validation examples to this " 166 | "value if set." 167 | }, 168 | ) 169 | max_test_samples: Optional[int] = field( 170 | default=None, 171 | metadata={ 172 | "help": "For debugging purposes or quicker training, truncate the number of test examples to this " 173 | "value if set." 174 | }, 175 | ) 176 | num_beams: Optional[int] = field( 177 | default=None, 178 | metadata={ 179 | "help": "Number of beams to use for evaluation. This argument will be passed to ``model.generate``, " 180 | "which is used during ``evaluate`` and ``predict``." 181 | }, 182 | ) 183 | ignore_pad_token_for_loss: bool = field( 184 | default=True, 185 | metadata={ 186 | "help": "Whether to ignore the tokens corresponding to padded labels in the loss computation or not." 187 | }, 188 | ) 189 | source_prefix: Optional[str] = field( 190 | default=None, metadata={"help": "A prefix to add before every source text (useful for T5 models)."} 191 | ) 192 | 193 | def __post_init__(self): 194 | if self.dataset_name is None and self.train_file is None and self.validation_file is None: 195 | raise ValueError("Need either a dataset name or a training/validation file.") 196 | else: 197 | if self.train_file is not None: 198 | extension = self.train_file.split(".")[-1] 199 | assert extension in ["csv", "json"], "`train_file` should be a csv or a json file." 200 | if self.validation_file is not None: 201 | extension = self.validation_file.split(".")[-1] 202 | assert extension in ["csv", "json"], "`validation_file` should be a csv or a json file." 203 | if self.val_max_target_length is None: 204 | self.val_max_target_length = self.max_target_length 205 | 206 | if __name__ == "__main__": 207 | args = sys.argv[1:] 208 | if len(args) == 2: 209 | convert_input_data_to_json_file(args[0],args[1]) 210 | elif len(args) == 3: 211 | convert_parallel_data_to_json_file(args[0], args[1],args[2]) -------------------------------------------------------------------------------- /models/seq2edit-based-CGEC/utils/helpers.py: -------------------------------------------------------------------------------- 1 | import os 2 | from collections import defaultdict, Counter 3 | from pathlib import Path 4 | import random 5 | import string 6 | from tqdm import tqdm 7 | import json 8 | from string import punctuation 9 | 10 | chinese_punct = "……·——!―〉<>?。。"#$%&'()*+,-/:《》;<=>@[’.\]^_’`{|}~⦅⦆「」、、〃》「」『』【】〔〕〖〗〘〙〚〛〜〝〞〟〰〾〿–—‘'‛“”„‟…‧﹏" 11 | english_punct = punctuation 12 | letter = "123456789abcdefghijklmnopqrstuvwxyz" 13 | FILTER = ["\x7f", " ", "\uf0e0", "\uf0a7", "\u200e", "\x8b", "\uf0b7", "\ue415", "\u2060", "\ue528", "\ue529", "ᩘ", "\ue074", "\x8b", "\u200c", "\ue529", "\ufeff", "\u200b", "\ue817", "\xad", '\u200f', '️', '่', '︎'] 14 | VOCAB_DIR = Path(__file__).resolve().parent.parent / "data" 15 | PAD = "@@PADDING@@" 16 | UNK = "@@UNKNOWN@@" 17 | START_TOKEN = "$START" 18 | SEQ_DELIMETERS = {"tokens": " ", 19 | "labels": "SEPL|||SEPR", 20 | "operations": "SEPL__SEPR", 21 | "pos_tags": "SEPL---SEPR"} # 分隔符,其中,如果一个source token被多次编辑,那么这些编辑label之间用"SEPL__SEPR"相分割 22 | PUNCT = chinese_punct + english_punct + letter + letter.upper() 23 | 24 | def split_char(line): 25 | """ 26 | 将中文按照字分开,英文按照词分开 27 | :param line: 输入句子 28 | :return: 分词后的句子 29 | """ 30 | english = "abcdefghijklmnopqrstuvwxyz0123456789" 31 | output = [] 32 | buffer = "" 33 | chinese_punct = "!?。"#$%&'()*+,-/:;<=>@[\]^_`{|}~⦅⦆「」、、〃》「」『』【】〔〕〖〗〘〙〚〛〜〝〞〟〰〾〿–—‘'‛“”„‟…‧﹏." 34 | for s in line: 35 | if s in english or s in english.upper() or s in string.punctuation or s in chinese_punct: # 英文或数字或标点不分 36 | buffer += s 37 | else: # 中文或空格分 38 | if buffer and buffer != " ": 39 | output.append(buffer) 40 | buffer = "" 41 | if s != " ": 42 | output.append(s) 43 | if buffer: 44 | output.append(buffer) 45 | return output 46 | 47 | def get_verb_form_dicts(): 48 | """ 49 | 从词典verb-form-vocab.txt获得用于动词形式转换变形的encode和decode。 50 | verb-form-vocab.txt词典主要是存储了英文常见动词形式转换映射。 51 | :return: 52 | encode: key:单词形式转换, value:转换标签 likes_like:VBZ_VB 53 | decode: key:likes_VAZ_VB value:like likes_VAZ_VB:like 54 | """ 55 | path_to_dict = os.path.join(VOCAB_DIR, "verb-form-vocab.txt") 56 | encode, decode = {}, {} 57 | with open(path_to_dict, encoding="utf-8") as f: 58 | for line in f: 59 | words, tags = line.split(":") 60 | word1, word2 = words.split("_") 61 | tag1, tag2 = tags.split("_") 62 | decode_key = f"{word1}_{tag1}_{tag2.strip()}" 63 | if decode_key not in decode: 64 | encode[words] = tags 65 | decode[decode_key] = word2 66 | return encode, decode 67 | 68 | 69 | ENCODE_VERB_DICT, DECODE_VERB_DICT = get_verb_form_dicts() # 动词形式变换编码器、解码器 70 | 71 | 72 | def get_target_sent_by_edits(source_tokens, edits): 73 | """ 74 | 对源句子token列表应用编辑操作(Span-level),得到目标句子token列表 75 | :param source_tokens: 源句子token列表 76 | :param edits: 编辑序列 77 | :return:目标句子token列表 78 | """ 79 | target_tokens = source_tokens[:] 80 | shift_idx = 0 81 | for edit in edits: 82 | start, end, label, _ = edit 83 | target_pos = start + shift_idx 84 | source_token = target_tokens[target_pos] \ 85 | if len(target_tokens) > target_pos >= 0 else '' 86 | if label == "": 87 | if target_tokens: 88 | del target_tokens[target_pos] 89 | shift_idx -= 1 90 | elif start == end: 91 | word = label.replace("$APPEND_", "") # 添加操作 92 | target_tokens[target_pos: target_pos] = [word] 93 | shift_idx += 1 94 | elif label.startswith("$TRANSFORM_"): # 变形操作 95 | word = apply_reverse_transformation(source_token, label) 96 | if word is None: 97 | word = source_token 98 | target_tokens[target_pos] = word 99 | elif start == end - 1: # 替换操作 100 | word = label.replace("$REPLACE_", "") 101 | target_tokens[target_pos] = word 102 | elif label.startswith("$MERGE_"): # 合并操作 103 | target_tokens[target_pos + 1: target_pos + 1] = [label] 104 | shift_idx += 1 105 | 106 | return replace_merge_transforms(target_tokens) # 将Merge操作应用到目标句子token列表(当前只是用$Merge标签标记了需要合并的地方) 107 | 108 | 109 | def replace_merge_transforms(tokens): 110 | """ 111 | 对序列应用Merge变形编辑(将当前token与下一个token合并) 112 | :param tokens: 词序列列表 113 | :return: Merge完成后的序列列表 114 | """ 115 | if all(not x.startswith("$MERGE_") for x in tokens): 116 | return tokens 117 | target_tokens = tokens[:] 118 | allowed_range = range(1, len(tokens) - 1) 119 | for i in range(len(tokens)): 120 | target_token = tokens[i] 121 | if target_token.startswith("$MERGE"): 122 | if target_token.startswith("$MERGE_SWAP") and i in allowed_range: 123 | target_tokens[i - 1] = tokens[i + 1] 124 | target_tokens[i + 1] = tokens[i - 1] 125 | target_line = " ".join(target_tokens) 126 | target_line = target_line.replace(" $MERGE_HYPHEN ", "-") 127 | target_line = target_line.replace(" $MERGE_SPACE ", "") 128 | target_line = target_line.replace(" $MERGE_SWAP ", " ") 129 | return target_line.split() 130 | 131 | 132 | def convert_using_case(token, smart_action): 133 | """ 134 | 对当前token进行大小写变换 135 | :param token: 当前token 136 | :param smart_action: 编辑操作标签 137 | :return: 编辑完成后的token 138 | """ 139 | if not smart_action.startswith("$TRANSFORM_CASE_"): 140 | return token 141 | if smart_action.endswith("LOWER"): 142 | return token.lower() 143 | elif smart_action.endswith("UPPER"): 144 | return token.upper() 145 | elif smart_action.endswith("CAPITAL"): 146 | return token.capitalize() 147 | elif smart_action.endswith("CAPITAL_1"): 148 | return token[0] + token[1:].capitalize() 149 | elif smart_action.endswith("UPPER_-1"): 150 | return token[:-1].upper() + token[-1] 151 | else: 152 | return token 153 | 154 | 155 | def convert_using_verb(token, smart_action): 156 | """ 157 | 对当前token进行动词时形式变换(人称、时态等) 158 | :param token: 当前token 159 | :param smart_action: 编辑操作标签 160 | :return: 编辑完成后的token 161 | """ 162 | key_word = "$TRANSFORM_VERB_" 163 | if not smart_action.startswith(key_word): 164 | raise Exception(f"Unknown action type {smart_action}") 165 | encoding_part = f"{token}_{smart_action[len(key_word):]}" 166 | decoded_target_word = decode_verb_form(encoding_part) 167 | return decoded_target_word 168 | 169 | 170 | def convert_using_split(token, smart_action): 171 | """ 172 | 对当前token进行切分(去掉连字符-) 173 | :param token: 当前token 174 | :param smart_action: 编辑操作标签 175 | :return: 编辑完成后的token 176 | """ 177 | key_word = "$TRANSFORM_SPLIT" 178 | if not smart_action.startswith(key_word): 179 | raise Exception(f"Unknown action type {smart_action}") 180 | target_words = token.split("-") 181 | return " ".join(target_words) 182 | 183 | 184 | # TODO 单复数变换不止有加s,还有加es的情况? 185 | def convert_using_plural(token, smart_action): 186 | """ 187 | 对当前token进行单复数变换 188 | :param token: 当前token 189 | :param smart_action: 编辑操作标签 190 | :return: 编辑完成后的token 191 | """ 192 | if smart_action.endswith("PLURAL"): 193 | return token + "s" 194 | elif smart_action.endswith("SINGULAR"): 195 | return token[:-1] 196 | else: 197 | raise Exception(f"Unknown action type {smart_action}") 198 | 199 | 200 | def apply_reverse_transformation(source_token, transform): 201 | """ 202 | 对token进行转换操作 203 | :param source_token: 204 | :param transform: 205 | :return: 206 | """ 207 | if transform.startswith("$TRANSFORM"): 208 | # deal with equal 209 | if transform == "$KEEP": # 冗余? 210 | return source_token 211 | # deal with case 212 | elif transform.startswith("$TRANSFORM_CASE"): 213 | return convert_using_case(source_token, transform) 214 | # deal with verb 215 | elif transform.startswith("$TRANSFORM_VERB"): 216 | return convert_using_verb(source_token, transform) 217 | # deal with split 218 | elif transform.startswith("$TRANSFORM_SPLIT"): 219 | return convert_using_split(source_token, transform) 220 | # deal with single/plural 221 | elif transform.startswith("$TRANSFORM_AGREEMENT"): 222 | return convert_using_plural(source_token, transform) 223 | # raise exception if not find correct type 224 | raise Exception(f"Unknown action type {transform}") 225 | else: 226 | return source_token 227 | 228 | 229 | def read_parallel_lines(fn1, fn2): 230 | """ 231 | 读取平行语料文件 232 | :param fn1: 源句子文件(纠错前) 233 | :param fn2: 目标句子文件(纠错后) 234 | :return: 分别包含源句子和目标句子的两个列表 235 | """ 236 | lines1 = read_lines(fn1, skip_strip=True) 237 | lines2 = read_lines(fn2, skip_strip=True) 238 | assert len(lines1) == len(lines2), print(len(lines1), len(lines2)) 239 | out_lines1, out_lines2 = [], [] 240 | for line1, line2 in zip(lines1, lines2): 241 | if not line1.strip() or not line2.strip(): 242 | continue 243 | else: 244 | out_lines1.append(line1) 245 | out_lines2.append(line2) 246 | return out_lines1, out_lines2 247 | 248 | 249 | def read_lines(fn, skip_strip=False): 250 | """ 251 | 从文件中读取每一行 252 | :param fn: 文件路径 253 | :param skip_strip: 是否跳过空行 254 | :return: 包含文件中每一行的列表 255 | """ 256 | if not os.path.exists(fn): 257 | return [] 258 | with open(fn, 'r', encoding='utf-8') as f: 259 | lines = f.readlines() 260 | return [s.strip() for s in lines if s.strip() or skip_strip] 261 | 262 | def write_lines(fn, lines, mode='w'): 263 | """ 264 | 将数据写入到文件中 265 | :param fn: 输出文件路径 266 | :param lines: 需要写入的数据 267 | :param mode: 写入的模式(w、a等) 268 | :return: 269 | """ 270 | if mode == 'w' and os.path.exists(fn): 271 | os.remove(fn) 272 | with open(fn, encoding='utf-8', mode=mode) as f: 273 | f.writelines(['%s\n' % s for s in lines]) 274 | 275 | 276 | def decode_verb_form(original): 277 | return DECODE_VERB_DICT.get(original) 278 | 279 | 280 | def encode_verb_form(original_word, corrected_word): 281 | decoding_request = original_word + "_" + corrected_word 282 | decoding_response = ENCODE_VERB_DICT.get(decoding_request, "").strip() 283 | if original_word and decoding_response: 284 | answer = decoding_response 285 | else: 286 | answer = None 287 | return answer -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # MuCGEC: A Multi-Reference Multi-Source Evaluation Dataset for Chinese Grammatical Error Correction & SOTA Models 2 | 3 | 4 | English | [简体中文](README.md) 5 | 6 | ## News 7 | + Our new work SynGEC has been accepted by EMNLP2022. In this paper, we propose a SynGEC model that can utilize GEC-oriented tailored syntax and get SOTA performance (e.g., 45.32/46.51 F0.5 scores on NLPCC-18 and MuCGEC-Test), respectively. Welcome to test it! Link: [[Link]](https://github.com/HillZhang1999/SynGEC) 8 | 9 | + We have uploaded the full MuCGEC dataset to this [link](https://github.com/HillZhang1999/MuCGEC/tree/main/data/MuCGEC) and opened a leaderboard at [Tianchi Platform](https://tianchi.aliyun.com/dataset/dataDetail?dataId=131328#4), welcome to submit your results! 10 | 11 | + We have released a part of MuCGEC dataset at [Tianchi Platform](https://tianchi.aliyun.com/dataset/dataDetail?dataId=131328), welcome to download and test! 12 | 13 | ## Citation 14 | If you find this work is useful for your research, please cite our paper: 15 | 16 | #### MuCGEC: a Multi-Reference Multi-Source Evaluation Dataset for Chinese Grammatical Error Correction (Accepted by NAACL2022 main conference) [[PDF]](https://aclanthology.org/2022.naacl-main.227.pdf) 17 | 18 | ``` 19 | @inproceedings{zhang-etal-2022-mucgec, 20 | title = "{M}u{CGEC}: a Multi-Reference Multi-Source Evaluation Dataset for {C}hinese Grammatical Error Correction", 21 | author = "Zhang, Yue and 22 | Li, Zhenghua and 23 | Bao, Zuyi and 24 | Li, Jiacheng and 25 | Zhang, Bo and 26 | Li, Chen and 27 | Huang, Fei and 28 | Zhang, Min", 29 | booktitle = "Proceedings of the 2022 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies", 30 | month = jul, 31 | year = "2022", 32 | address = "Seattle, United States", 33 | publisher = "Association for Computational Linguistics", 34 | url = "https://aclanthology.org/2022.naacl-main.227", 35 | pages = "3118--3130", 36 | } 37 | ``` 38 | 39 | ## Introduction 40 | 41 | Chinese Grammatical Error Correction (CGEC) technology aims to automatically correct spelling, grammatical, semantical, and other errors in a Chinese sentence. This technology is very useful in various scenarios. 42 | 43 | Current CGEC evaluation datasets have some flaws, such as small amounts of data, single reference, single text source, etc. In order to better evaluate CGEC models, this repository provide a new multi-reference multi-source CGEC evaluation dataset named **MuCGEC**. Meanwhile, in order to promote the development of CGEC, we also provide the following additional resources: 44 | 45 | + **CGEC data annotation guidelines`./guidelines`** 46 | 47 | + **CGEC evaluation tools`./scorers`**: 48 | + `ChERRANT`:We extend the English GEC evaluation tool [ERRANT](https://github.com/chrisjbryant/errant) to accomondate Chinese, and name it `ChERRANT`(**Ch**inese **ERRANT**). ChERRANT supports the CGEC model evaluation at both word and character granularity. 49 | 50 | + **CGEC benchmark models`./models`**: 51 | + **Seq2Edit model`./models/seq2edit-based-CGEC`**: This kind of models treats GEC as a sequence labeling task and performs error corrections via a sequence of token-level edits, including insertion, deletion, and substitution. 52 | + With minor modifications to accommodate Chinese, we adopt [GECToR](https://github.com/grammarly/gector), which achieves the SOTA performance on English GEC datasets. 53 | + **Seq2Seq model`./models/seq2seq-based-CGEC`**:This kind of models straightforwardly treats GEC as a monolingual translation task 54 | + We fine-tune the recently-proposed Seq2Seq pretrained model [Chinese-BART](https://github.com/fastnlp/CPT) and use it in CGEC. 55 | + **Ensemble model`./scorers/ChERRANT/emsemble.sh`**:We adopt a simple edit-wise vote mechanism, which can support the ensemble of heterogeneous models (such as Seq2Seq and Seq2Edit) and lead to significant performance boosts. 56 | + **CGEC tools`./tools`**: 57 | + **Tokenization tools** 58 | + **Data augmentation tools** (*Todo*) 59 | + **Data cleaning tools** (*Todo*) 60 | 61 | ## MuCGEC Dataset 62 | 63 | Our dataset consists of texts written by Chinese learners. We select data from the following three sources: `NLPCC18` corpus, `CGED` corpus, and Chinese `Lang8` corpus. Each sentence has been corrected by three 64 | annotators, and their corrections are meticulously reviewed by an expert, resulting in 2.3 references per sentence. The detailed statistics are shown below: 65 | 66 | | Subset | #Sents | %Errors | Chars/sent | Edits/sent | Refs/sent | 67 | | :------- | :---------: | :---------: | :---------: | :---------: | :---------: | 68 | | **MuCGEC-NLPCC18** | 1996 | 1904(95.4%) | 29.7 | 2.5 | 2.5 | 69 | | **MuCGEC-CGED** | 3125 | 2988(95.6%) | 44.8 | 4.0 | 2.3 | 70 | | **MuCGEC-Lang8** | 1942 | 1652(85.1%) | 37.5 | 2.8 | 2.1 | 71 | | **MuCGEC-ALL** | 7063 | 6544(92.7%) | 38.5 | 3.2 | 2.3 | 72 | 73 | Compared with previous CGEC evaluation sets (such as NLPCC18-orig and CGED-orig), MuCGEC has richer references and data sources. In addition, during the annotation procedure, we also found that 74 sentences could not be annotated since their meanings are unclear. 74 | 75 | For more details about MuCGEC, please kindly refer to our paper. 76 | 77 | ### Download link 78 | We have released MuCGEC at [https://tianchi.aliyun.com/dataset?spm=5176.12281949.J_3941670930.15.493e2448TJV71D](https://tianchi.aliyun.com/dataset?spm=5176.12281949.J_3941670930.15.493e2448TJV71D). 79 | 80 | 81 | ## CGEC benchmark models 82 | 83 | ### Experimental enviroment 84 | 85 | We use Python 3.8 to experiment, and the necessary dependencies can be installed through the following code. Considering that there are some conflicts between the environments of Seq2Seq and Seq2Edit models, two environments need to be installed separately: 86 | ``` 87 | # Seq2Edit environment 88 | pip install -r requirements_seq2edit.txt 89 | 90 | # Seq2Seq environment 91 | pip install -r requirements_seq2seq.txt 92 | ``` 93 | 94 | ### Training data 95 | 96 | The training data used in our experiment is composed of: 1) Chinese `Lang8` corpus; 2)`HSK` corpus. We upsampling `HSK` corpus 5 times. We only use the erroneous part of the training data. 97 | 98 | Download Link: [Google Drive](https://drive.google.com/file/d/1l0A50z7fMXjQT3y2ct7TQsEHqOwlvg0_/view?usp=sharing) 99 | 100 | 101 | 102 | ### Usage 103 | We provide pipeline scripts to use our model, including the process of preprocessing->training->inference. Please refer to 104 | `./models/seq2edit-based-CGEC/pipeline.sh` and `./models/seq2seq-based-CGEC/pipeline.sh` 105 | 106 | Besides, we also provide converged checkpoints for testing (the following metrics are precision/recall/F0.5): 107 | 108 | | Model | NLPCC18-Official(m2socrer)| MuCGEC(ChERRANT)| 109 | | :-------: | :---------:| :---------: | 110 | | **seq2seq_lang8**[[Link](https://drive.google.com/file/d/1Jras2Km4ScdVB0sx8ePg-PqCmDC4O8v5/view?usp=sharing)] | 37.78/29.91/35.89 | 40.44/26.71/36.67 | 111 | | **seq2seq_lang8+hsk**[[Link](https://drive.google.com/file/d/180CXiW7pDz0wcbeTgszVoBrvzRmXzeZ9/view?usp=sharing)] | 41.50/32.87/39.43 | 44.02/28.51/39.70 | 112 | | **seq2edit_lang8**[[Link](https://drive.google.com/file/d/13OAJ9DSThqssl93bSn0vQetetLhQz5LA/view?usp=sharing)] | 37.43/26.29/34.50 | 38.08/22.90/33.62 | 113 | | **seq2edit_lang8+hsk**[[Link](https://drive.google.com/file/d/1ce7t8r3lLUeJ4eIxIg3EpXwHIUE99nk8/view?usp=sharing)] | 43.12/30.18/39.72 | 44.65/27.32/39.62 | 114 | 115 | The ensemble strategy used in our paper can be found in `./scorers/ChERRANT/ensemble.sh`. Please kindly note that above seq2edit checkpoints are based on `Chinese-StructBERT-large` and seq2seq checkpoints are based on `Chinese-BART-Large`. 116 | 117 | ### Tips 118 | + We found that some useful tricks in English are still effective in Chinese, such as the confidence bias trick in GECToR and R2L reranking trick in Seq2Seq models. You can try them yourself. 119 | + We found that the decomposition of the training procedure (firstly train on HSL+Lang8, then fine-tune on HSK) can lead to further improvement. You can re-train our models following this two-stage training strategy. 120 | + Our Seq2Seq models based on [Chinese-BART](https://huggingface.co/fnlp/bart-large-chinese) can be improved: 1) the original vocabulary of Chinese-BART lacks some common Chinese punctuation / characters; 2)The training and generation speed of transformers library is relatively slow. We recently re-implement our Seq2Seq model based on [fairseq](https://github.com/pytorch/fairseq) and use some additional tricks, which greatly improved its performance (4-5 F0.5) and accelerated its speed. We will also release the improved version in the future. 121 | + None of the baselines we provided used pseudo data. For data augmentation, please refer to our paper in CTC-2021 CGEC competition [[Link]](https://github.com/HillZhang1999/CTC-Report). 122 | 123 | ### Evaluation 124 | 125 | To get the evaluation results on the [Official NLPCC18 dataset](http://tcci.ccf.org.cn/conference/2018/taskdata.php), you can make predictions by using our benchmark models, and evaluate the results through [M2Scorer](https://github.com/nusnlp/m2scorer). It is important to note that the predicted results must be segmented using the PKUSEG tool. 126 | 127 | To get the evaluation results on MuCGEC, you can evaluate the results through our proposed [ChERRANT](./scorers/ChERRANT). Please refer to `./scorers/ChERRANT/demo.sh` for the use of ChERRANT. 128 | 129 | **Error types in ChERRANT** 130 | + Operation tier (word/char granularity): 131 | + M(missing): Missing error, which means tokens need to be inserted; 132 | + R(redundant): Redundant error, which means tokens need to be deleted; 133 | + S(substitute): Substitued error , which means tokens need to be replaced; 134 | + W(word-order): Word-error error, which means tokens need to be re-ordered. 135 | 136 | + Linguistic tier (word granularity): 137 | + We define 14 main types of linguistic errors (basically based on part-of-speech tags): 138 | 139 | ![error types](./pics/errors.PNG) 140 | 141 | 142 | ## Related works 143 | + We have used some technologies from this repo in the CTC-2021 evaluation task, and obtained the top-1 score. Please see: [CTC-report](https://github.com/HillZhang1999/CTC-Report). 144 | + Online demonstration platform of the baseline models: [GEC demo](http://139.224.234.18:5002/)。 145 | 146 | ## Contact 147 | If you have any problems, feel free to contact me at hillzhang1999@qq.com. 148 | -------------------------------------------------------------------------------- /models/seq2seq-based-CGEC/train.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import logging 4 | import os 5 | import random 6 | import sys 7 | import nltk 8 | 9 | import numpy as np 10 | import torch 11 | import transformers 12 | from transformers import (AutoConfig, AutoModel, BertTokenizer,BertForTokenClassification, 13 | DataCollatorForTokenClassification, HfArgumentParser,DataCollatorForSeq2Seq,Seq2SeqTrainer, 14 | Seq2SeqTrainingArguments, Trainer, TrainerCallback,AutoModelForSeq2SeqLM,BartForConditionalGeneration) 15 | from transformers.trainer_utils import is_main_process 16 | from datasets import load_metric,Dataset 17 | from utils import DataTrainingArguments, ModelArguments, load_json 18 | 19 | import sys 20 | sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) 21 | 22 | 23 | parser = argparse.ArgumentParser() 24 | parser.add_argument("--do_train", action="store_true") 25 | parser.add_argument("--do_eval", action="store_true") 26 | parser.add_argument("--do_predict", action="store_true") 27 | parser.add_argument("--model_path",default='/path/to/model',type=str) 28 | parser.add_argument("--save_path", default="lcsts",type=str) 29 | parser.add_argument("--task", default="lcsts",type=str) 30 | parser.add_argument("--gradient_accumulation_steps", default="4",type=str) 31 | parser.add_argument("--lr",default=3e-6,type=float) 32 | parser.add_argument("--batch_size",default='32',type=str) 33 | parser.add_argument("--epoch",default='10',type=str) 34 | # parser.add_argument("--warmup_steps",default='1000',type=str) 35 | parser.add_argument("--lr_scheduler",default='polynomial',type=str) 36 | parser.add_argument("--save_strategy",default='epoch',type=str) 37 | parser.add_argument("--data_dir",default="/path/to/dataset/",type=str) 38 | parser.add_argument("--seed",default='2021',type=str) 39 | parser.add_argument("--predict_file",default='results',type=str) 40 | args = parser.parse_args() 41 | arg_dict=args.__dict__ 42 | 43 | logger = logging.getLogger(__name__) 44 | 45 | dataset_name=arg_dict['task'] 46 | outdir_1=arg_dict['save_path'] 47 | if not os.path.exists(outdir_1): 48 | os.mkdir(outdir_1) 49 | 50 | outdir=outdir_1+'/'+dataset_name 51 | if not os.path.exists(outdir): 52 | os.mkdir(outdir) 53 | 54 | seed=arg_dict['seed'] 55 | outdir=outdir+'/run_'+seed 56 | length_map={'lcsts':'30','csl':'50','adgen':'128', 'gec': '100'} 57 | 58 | 59 | args_list=[ 60 | '--model_name_or_path',arg_dict['model_path'], 61 | '--train_file',os.path.join(arg_dict['data_dir'],'train.json'), 62 | '--validation_file',os.path.join(arg_dict['data_dir'],'valid.json'), 63 | # '--test_file',os.path.join(arg_dict['data_dir'], 'test.json'), 64 | '--output_dir',outdir, 65 | '--per_device_train_batch_size',arg_dict['batch_size'], 66 | '--per_device_eval_batch_size',arg_dict['batch_size'], 67 | '--overwrite_output_dir', 68 | '--max_source_length=100', 69 | '--val_max_target_length='+length_map[arg_dict['task']], 70 | '--seed',seed, 71 | '--num_train_epochs',arg_dict['epoch'], 72 | '--save_strategy','no', 73 | '--evaluation_strategy','epoch', 74 | '--learning_rate',str(arg_dict['lr']), 75 | '--fp16', 76 | '--label_smoothing_factor', '0.1', 77 | '--predict_with_generate', 78 | '--group_by_length', 79 | '--gradient_accumulation_steps', arg_dict['gradient_accumulation_steps'], 80 | '--lr_scheduler', arg_dict['lr_scheduler'], 81 | '--save_strategy', arg_dict['save_strategy'] 82 | ] 83 | 84 | if args.do_train: 85 | args_list.append('--do_train') 86 | if args.do_eval: 87 | args_list.append('--do_eval') 88 | if args.do_predict: 89 | args_list.append('--do_predict') 90 | 91 | parser = HfArgumentParser((ModelArguments, DataTrainingArguments, Seq2SeqTrainingArguments)) 92 | model_args, data_args, training_args = parser.parse_args_into_dataclasses(args_list) 93 | 94 | def set_seed(seed): 95 | random.seed(seed) 96 | np.random.seed(seed) 97 | torch.manual_seed(seed) 98 | if torch.cuda.is_available(): 99 | torch.cuda.manual_seed_all(seed) 100 | 101 | set_seed(training_args.seed) 102 | 103 | datasets={} 104 | data_files = {} 105 | if data_args.train_file is not None: 106 | data_files["train"] = data_args.train_file 107 | if data_args.validation_file is not None: 108 | data_files["validation"] = data_args.validation_file 109 | if data_args.test_file is not None: 110 | data_files["test"] = data_args.test_file 111 | for key in data_files: 112 | datasets[key]=load_json(data_files[key]) 113 | 114 | logging.basicConfig( 115 | format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", 116 | datefmt="%m/%d/%Y %H:%M:%S", 117 | handlers=[logging.StreamHandler(sys.stdout)], 118 | ) 119 | logger.setLevel(logging.INFO if is_main_process(training_args.local_rank) else logging.WARN) 120 | 121 | # Log on each process the small summary: 122 | logger.warning( 123 | f"Process rank: {training_args.local_rank}, device: {training_args.device}, n_gpu: {training_args.n_gpu}" 124 | + f"distributed training: {bool(training_args.local_rank != -1)}, 16-bits training: {training_args.fp16}" 125 | ) 126 | # Set the verbosity to info of the Transformers logger (on main process only): 127 | if is_main_process(training_args.local_rank): 128 | transformers.utils.logging.set_verbosity_info() 129 | logger.info("Training/evaluation parameters %s", training_args) 130 | 131 | tokenizer=BertTokenizer.from_pretrained(model_args.model_name_or_path) 132 | model=BartForConditionalGeneration.from_pretrained(model_args.model_name_or_path) 133 | model.config.max_length=data_args.val_max_target_length 134 | 135 | text_column='article' 136 | summary_column='summarization' 137 | column_names = datasets["train"].column_names 138 | max_target_length = data_args.val_max_target_length 139 | padding=False 140 | 141 | def preprocess_function(examples): 142 | inputs = examples[text_column] 143 | targets = examples[summary_column] 144 | model_inputs = tokenizer(inputs, max_length=data_args.max_source_length, padding=padding, truncation=True) 145 | 146 | # Setup the tokenizer for targets 147 | with tokenizer.as_target_tokenizer(): 148 | labels = tokenizer(targets, max_length=max_target_length, padding=padding, truncation=True) 149 | 150 | model_inputs["labels"] = labels["input_ids"] 151 | return model_inputs 152 | 153 | if training_args.do_train: 154 | train_dataset = datasets["train"] 155 | if "train" not in datasets: 156 | raise ValueError("--do_train requires a train dataset") 157 | if data_args.max_train_samples is not None: 158 | train_dataset = train_dataset.select(range(data_args.max_train_samples)) 159 | train_dataset = train_dataset.map( 160 | preprocess_function, 161 | batched=True, 162 | num_proc=data_args.preprocessing_num_workers, 163 | remove_columns=column_names, 164 | load_from_cache_file=not data_args.overwrite_cache, 165 | ) 166 | 167 | if training_args.do_eval: 168 | max_target_length = data_args.val_max_target_length 169 | if "validation" not in datasets: 170 | raise ValueError("--do_eval requires a validation dataset") 171 | eval_dataset = datasets["validation"] 172 | if data_args.max_val_samples is not None: 173 | eval_dataset = eval_dataset.select(range(data_args.max_val_samples)) 174 | eval_dataset = eval_dataset.map( 175 | preprocess_function, 176 | batched=True, 177 | num_proc=data_args.preprocessing_num_workers, 178 | remove_columns=column_names, 179 | load_from_cache_file=not data_args.overwrite_cache, 180 | ) 181 | max_eval_num=30000 182 | if len(eval_dataset)>max_eval_num: 183 | eval_dataset=Dataset.from_dict(eval_dataset[:max_eval_num]) 184 | print(len(eval_dataset)) 185 | 186 | if training_args.do_predict: 187 | max_target_length = data_args.val_max_target_length 188 | if "test" not in datasets: 189 | raise ValueError("--do_predict requires a test dataset") 190 | test_dataset = datasets["test"] 191 | if data_args.max_test_samples is not None: 192 | test_dataset = test_dataset.select(range(data_args.max_test_samples)) 193 | test_dataset = test_dataset.map( 194 | preprocess_function, 195 | batched=True, 196 | batch_size=32, 197 | num_proc=data_args.preprocessing_num_workers, 198 | remove_columns=column_names, 199 | load_from_cache_file=not data_args.overwrite_cache, 200 | ) 201 | 202 | # Data collator 203 | label_pad_token_id = -100 if data_args.ignore_pad_token_for_loss else tokenizer.pad_token_id 204 | data_collator = DataCollatorForSeq2Seq( 205 | tokenizer, 206 | model=model, 207 | label_pad_token_id=label_pad_token_id, 208 | pad_to_multiple_of=8 if training_args.fp16 else None, 209 | ) 210 | 211 | 212 | # Metric 213 | from rouge import Rouge 214 | rouge = Rouge() 215 | 216 | def postprocess_text(preds, labels): 217 | preds = [pred.strip() for pred in preds] 218 | labels = [label.strip() for label in labels] 219 | while '' in preds: 220 | idx=preds.index('') 221 | preds[idx]='。' 222 | 223 | return preds, labels 224 | 225 | def compute_metrics(eval_preds): 226 | preds, labels = eval_preds 227 | if isinstance(preds, tuple): 228 | preds = preds[0] 229 | decoded_preds = tokenizer.batch_decode(preds, skip_special_tokens=True) 230 | if data_args.ignore_pad_token_for_loss: 231 | # Replace -100 in the labels as we can't decode them. 232 | labels = np.where(labels != -100, labels, tokenizer.pad_token_id) 233 | decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True) 234 | 235 | # Some simple post-processing 236 | decoded_preds, decoded_labels = postprocess_text(decoded_preds, decoded_labels) 237 | scores = rouge.get_scores(decoded_preds, decoded_labels,avg=True) 238 | for key in scores: 239 | scores[key]=scores[key]['f']*100 240 | 241 | result=scores 242 | 243 | prediction_lens = [np.count_nonzero(pred != tokenizer.pad_token_id) for pred in preds] 244 | result["gen_len"] = np.mean(prediction_lens) 245 | result = {k: round(v, 4) for k, v in result.items()} 246 | return result 247 | 248 | class TestCallback(TrainerCallback): 249 | def on_evaluate(self, args, state, control, **kwargs): 250 | predictions, labels, metrics = trainer.predict(test_dataset, metric_key_prefix="predict") 251 | metrics['epoch']=state.epoch 252 | state.log_history.append(metrics) 253 | 254 | # Initialize our Trainer 255 | trainer = Seq2SeqTrainer( 256 | model=model, 257 | args=training_args, 258 | train_dataset=train_dataset if training_args.do_train else None, 259 | eval_dataset=eval_dataset if training_args.do_eval else None, 260 | tokenizer=tokenizer, 261 | data_collator=data_collator, 262 | # if training_args.predict_with_generate else None, 263 | ) 264 | 265 | 266 | # Training 267 | if training_args.do_train: 268 | train_result = trainer.train() 269 | trainer.save_model() # Saves the tokenizer too for easy upload 270 | 271 | metrics = train_result.metrics 272 | max_train_samples = ( 273 | data_args.max_train_samples if data_args.max_train_samples is not None else len(train_dataset) 274 | ) 275 | metrics["train_samples"] = min(max_train_samples, len(train_dataset)) 276 | 277 | trainer.log_metrics("train", metrics) 278 | trainer.save_metrics("train", metrics) 279 | trainer.save_state() 280 | 281 | if training_args.do_predict: 282 | if training_args.predict_with_generate: 283 | predictions, labels, metrics = trainer.predict(test_dataset, metric_key_prefix="predict") 284 | test_preds = tokenizer.batch_decode( 285 | predictions, skip_special_tokens=True, 286 | ) 287 | test_preds = ["".join(pred.strip().split()) for pred in test_preds] 288 | output_test_preds_file = args.predict_file 289 | with open(output_test_preds_file, "w",encoding='UTF-8') as writer: 290 | writer.write("\n".join(test_preds)) 291 | 292 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /scorers/ChERRANT/modules/tokenization.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # Copyright 2018 The Google AI Language Team Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | """Tokenization classes.""" 16 | 17 | from __future__ import absolute_import 18 | from __future__ import division 19 | from __future__ import print_function 20 | 21 | import collections 22 | import unicodedata 23 | import six 24 | 25 | 26 | def convert_to_unicode(text): 27 | """Converts `text` to Unicode (if it's not already), assuming utf-8 input.""" 28 | if six.PY3: 29 | if isinstance(text, str): 30 | return text 31 | elif isinstance(text, bytes): 32 | return text.decode("utf-8", "ignore") 33 | else: 34 | raise ValueError("Unsupported string type: %s" % (type(text))) 35 | elif six.PY2: 36 | if isinstance(text, str): 37 | return text.decode("utf-8", "ignore") 38 | elif isinstance(text, unicode): 39 | return text 40 | else: 41 | raise ValueError("Unsupported string type: %s" % (type(text))) 42 | else: 43 | raise ValueError("Not running on Python2 or Python 3?") 44 | 45 | 46 | def printable_text(text): 47 | """Returns text encoded in a way suitable for print or `tf.logging`.""" 48 | 49 | # These functions want `str` for both Python2 and Python3, but in one case 50 | # it's a Unicode string and in the other it's a byte string. 51 | if six.PY3: 52 | if isinstance(text, str): 53 | return text 54 | elif isinstance(text, bytes): 55 | return text.decode("utf-8", "ignore") 56 | else: 57 | raise ValueError("Unsupported string type: %s" % (type(text))) 58 | elif six.PY2: 59 | if isinstance(text, str): 60 | return text 61 | elif isinstance(text, unicode): 62 | return text.encode("utf-8") 63 | else: 64 | raise ValueError("Unsupported string type: %s" % (type(text))) 65 | else: 66 | raise ValueError("Not running on Python2 or Python 3?") 67 | 68 | 69 | def load_vocab(vocab_file): 70 | """Loads a vocabulary file into a dictionary.""" 71 | vocab = collections.OrderedDict() 72 | index = 0 73 | with open(vocab_file, "r") as reader: 74 | while True: 75 | token = convert_to_unicode(reader.readline()) 76 | if not token: 77 | break 78 | token = token.strip() 79 | vocab[token] = index 80 | index += 1 81 | return vocab 82 | 83 | 84 | def convert_by_vocab(vocab, items): 85 | """Converts a sequence of [tokens|ids] using the vocab.""" 86 | output = [] 87 | for item in items: 88 | if item not in vocab: 89 | print("warning: %s not in vocab" % item) 90 | item = "[UNK]" 91 | output.append(vocab[item]) 92 | return output 93 | 94 | 95 | def convert_tokens_to_ids(vocab, tokens): 96 | return convert_by_vocab(vocab, tokens) 97 | 98 | 99 | def convert_ids_to_tokens(inv_vocab, ids): 100 | return convert_by_vocab(inv_vocab, ids) 101 | 102 | 103 | def whitespace_tokenize(text): 104 | """Runs basic whitespace cleaning and splitting on a peice of text.""" 105 | text = text.strip() 106 | if not text: 107 | return [] 108 | tokens = text.split() 109 | return tokens 110 | 111 | 112 | class FullTokenizer(object): 113 | """Runs end-to-end tokenziation.""" 114 | 115 | def __init__(self, vocab_file, do_lower_case=True): 116 | self.vocab = load_vocab(vocab_file) 117 | self.inv_vocab = {v: k for k, v in self.vocab.items()} 118 | self.basic_tokenizer = BasicTokenizer(do_lower_case=do_lower_case) 119 | self.wordpiece_tokenizer = WordpieceTokenizer(vocab=self.vocab) 120 | 121 | def tokenize(self, text): 122 | split_tokens = [] 123 | for token in self.basic_tokenizer.tokenize(text): 124 | for sub_token in self.wordpiece_tokenizer.tokenize(token): 125 | split_tokens.append(sub_token) 126 | 127 | return split_tokens 128 | 129 | def convert_tokens_to_ids(self, tokens): 130 | return convert_by_vocab(self.vocab, tokens) 131 | 132 | def convert_ids_to_tokens(self, ids): 133 | return convert_by_vocab(self.inv_vocab, ids) 134 | 135 | 136 | class BasicTokenizer(object): 137 | """Runs basic tokenization (punctuation splitting, lower casing, etc.).""" 138 | 139 | def __init__(self, do_lower_case=True): 140 | """Constructs a BasicTokenizer. 141 | Args: 142 | do_lower_case: Whether to lower case the input. 143 | """ 144 | self.do_lower_case = do_lower_case 145 | 146 | def tokenize(self, text): 147 | """Tokenizes a piece of text.""" 148 | text = convert_to_unicode(text) 149 | text = self._clean_text(text) 150 | 151 | # This was added on November 1st, 2018 for the multilingual and Chinese 152 | # models. This is also applied to the English models now, but it doesn't 153 | # matter since the English models were not trained on any Chinese data 154 | # and generally don't have any Chinese data in them (there are Chinese 155 | # characters in the vocabulary because Wikipedia does have some Chinese 156 | # words in the English Wikipedia.). 157 | text = self._tokenize_chinese_chars(text) 158 | 159 | orig_tokens = whitespace_tokenize(text) 160 | split_tokens = [] 161 | for token in orig_tokens: 162 | if self.do_lower_case: 163 | token = token.lower() 164 | token = self._run_strip_accents(token) 165 | split_tokens.extend(self._run_split_on_punc(token)) 166 | 167 | output_tokens = whitespace_tokenize(" ".join(split_tokens)) 168 | return output_tokens 169 | 170 | def _run_strip_accents(self, text): 171 | """Strips accents from a piece of text.""" 172 | text = unicodedata.normalize("NFD", text) 173 | output = [] 174 | for char in text: 175 | cat = unicodedata.category(char) 176 | if cat == "Mn": 177 | continue 178 | output.append(char) 179 | return "".join(output) 180 | 181 | def _run_split_on_punc(self, text): 182 | """Splits punctuation on a piece of text.""" 183 | chars = list(text) 184 | i = 0 185 | start_new_word = True 186 | output = [] 187 | while i < len(chars): 188 | char = chars[i] 189 | if _is_punctuation(char): 190 | output.append([char]) 191 | start_new_word = True 192 | else: 193 | if start_new_word: 194 | output.append([]) 195 | start_new_word = False 196 | output[-1].append(char) 197 | i += 1 198 | 199 | return ["".join(x) for x in output] 200 | 201 | def _tokenize_chinese_chars(self, text): 202 | """Adds whitespace around any CJK character.""" 203 | output = [] 204 | for char in text: 205 | cp = ord(char) 206 | if self._is_chinese_char(cp): 207 | output.append(" ") 208 | output.append(char) 209 | output.append(" ") 210 | else: 211 | output.append(char) 212 | return "".join(output) 213 | 214 | def _is_chinese_char(self, cp): 215 | """Checks whether CP is the codepoint of a CJK character.""" 216 | # This defines a "chinese character" as anything in the CJK Unicode block: 217 | # https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block) 218 | # 219 | # Note that the CJK Unicode block is NOT all Japanese and Korean characters, 220 | # despite its name. The modern Korean Hangul alphabet is a different block, 221 | # as is Japanese Hiragana and Katakana. Those alphabets are used to write 222 | # space-separated words, so they are not treated specially and handled 223 | # like the all of the other languages. 224 | if ((cp >= 0x4E00 and cp <= 0x9FFF) or # 225 | (cp >= 0x3400 and cp <= 0x4DBF) or # 226 | (cp >= 0x20000 and cp <= 0x2A6DF) or # 227 | (cp >= 0x2A700 and cp <= 0x2B73F) or # 228 | (cp >= 0x2B740 and cp <= 0x2B81F) or # 229 | (cp >= 0x2B820 and cp <= 0x2CEAF) or 230 | (cp >= 0xF900 and cp <= 0xFAFF) or # 231 | (cp >= 0x2F800 and cp <= 0x2FA1F)): # 232 | return True 233 | 234 | return False 235 | 236 | def _clean_text(self, text): 237 | """Performs invalid character removal and whitespace cleanup on text.""" 238 | output = [] 239 | for char in text: 240 | cp = ord(char) 241 | if cp == 0 or cp == 0xfffd or _is_control(char): 242 | continue 243 | if _is_whitespace(char): 244 | output.append(" ") 245 | else: 246 | output.append(char) 247 | return "".join(output) 248 | 249 | 250 | class WordpieceTokenizer(object): 251 | """Runs WordPiece tokenziation.""" 252 | 253 | def __init__(self, vocab, unk_token="[UNK]", max_input_chars_per_word=100): 254 | self.vocab = vocab 255 | self.unk_token = unk_token 256 | self.max_input_chars_per_word = max_input_chars_per_word 257 | 258 | def tokenize(self, text): 259 | """Tokenizes a piece of text into its word pieces. 260 | This uses a greedy longest-match-first algorithm to perform tokenization 261 | using the given vocabulary. 262 | For example: 263 | input = "unaffable" 264 | output = ["un", "##aff", "##able"] 265 | Args: 266 | text: A single token or whitespace separated tokens. This should have 267 | already been passed through `BasicTokenizer. 268 | Returns: 269 | A list of wordpiece tokens. 270 | """ 271 | 272 | text = convert_to_unicode(text) 273 | 274 | output_tokens = [] 275 | for token in whitespace_tokenize(text): 276 | chars = list(token) 277 | if len(chars) > self.max_input_chars_per_word: 278 | output_tokens.append(self.unk_token) 279 | continue 280 | 281 | is_bad = False 282 | start = 0 283 | sub_tokens = [] 284 | while start < len(chars): 285 | end = len(chars) 286 | cur_substr = None 287 | while start < end: 288 | substr = "".join(chars[start:end]) 289 | if start > 0: 290 | substr = "##" + substr 291 | if substr in self.vocab: 292 | cur_substr = substr 293 | break 294 | end -= 1 295 | if cur_substr is None: 296 | is_bad = True 297 | break 298 | sub_tokens.append(cur_substr) 299 | start = end 300 | 301 | if is_bad: 302 | # output_tokens.append(self.unk_token) 303 | output_tokens.append(token) # keep the UNK token 304 | else: 305 | output_tokens.extend(sub_tokens) 306 | return output_tokens 307 | 308 | 309 | def _is_whitespace(char): 310 | """Checks whether `chars` is a whitespace character.""" 311 | # \t, \n, and \r are technically contorl characters but we treat them 312 | # as whitespace since they are generally considered as such. 313 | if char == " " or char == "\t" or char == "\n" or char == "\r": 314 | return True 315 | cat = unicodedata.category(char) 316 | if cat == "Zs": 317 | return True 318 | return False 319 | 320 | 321 | def _is_control(char): 322 | """Checks whether `chars` is a control character.""" 323 | # These are technically control characters but we count them as whitespace 324 | # characters. 325 | if char == "\t" or char == "\n" or char == "\r": 326 | return False 327 | cat = unicodedata.category(char) 328 | if cat.startswith("C"): 329 | return True 330 | return False 331 | 332 | 333 | def _is_punctuation(char): 334 | """Checks whether `chars` is a punctuation character.""" 335 | cp = ord(char) 336 | # We treat all non-letter/number ASCII as punctuation. 337 | # Characters such as "^", "$", and "`" are not in the Unicode 338 | # Punctuation class but we treat them as punctuation anyways, for 339 | # consistency. 340 | if ((cp >= 33 and cp <= 47) or (cp >= 58 and cp <= 64) or 341 | (cp >= 91 and cp <= 96) or (cp >= 123 and cp <= 126)): 342 | return True 343 | cat = unicodedata.category(char) 344 | if cat.startswith("P"): 345 | return True 346 | return False -------------------------------------------------------------------------------- /tools/segment/tokenization.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # Copyright 2018 The Google AI Language Team Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | """Tokenization classes.""" 16 | 17 | from __future__ import absolute_import 18 | from __future__ import division 19 | from __future__ import print_function 20 | 21 | import collections 22 | import unicodedata 23 | import six 24 | 25 | 26 | def convert_to_unicode(text): 27 | """Converts `text` to Unicode (if it's not already), assuming utf-8 input.""" 28 | if six.PY3: 29 | if isinstance(text, str): 30 | return text 31 | elif isinstance(text, bytes): 32 | return text.decode("utf-8", "ignore") 33 | else: 34 | raise ValueError("Unsupported string type: %s" % (type(text))) 35 | elif six.PY2: 36 | if isinstance(text, str): 37 | return text.decode("utf-8", "ignore") 38 | elif isinstance(text, unicode): 39 | return text 40 | else: 41 | raise ValueError("Unsupported string type: %s" % (type(text))) 42 | else: 43 | raise ValueError("Not running on Python2 or Python 3?") 44 | 45 | 46 | def printable_text(text): 47 | """Returns text encoded in a way suitable for print or `tf.logging`.""" 48 | 49 | # These functions want `str` for both Python2 and Python3, but in one case 50 | # it's a Unicode string and in the other it's a byte string. 51 | if six.PY3: 52 | if isinstance(text, str): 53 | return text 54 | elif isinstance(text, bytes): 55 | return text.decode("utf-8", "ignore") 56 | else: 57 | raise ValueError("Unsupported string type: %s" % (type(text))) 58 | elif six.PY2: 59 | if isinstance(text, str): 60 | return text 61 | elif isinstance(text, unicode): 62 | return text.encode("utf-8") 63 | else: 64 | raise ValueError("Unsupported string type: %s" % (type(text))) 65 | else: 66 | raise ValueError("Not running on Python2 or Python 3?") 67 | 68 | 69 | def load_vocab(vocab_file): 70 | """Loads a vocabulary file into a dictionary.""" 71 | vocab = collections.OrderedDict() 72 | index = 0 73 | with open(vocab_file, "r") as reader: 74 | while True: 75 | token = convert_to_unicode(reader.readline()) 76 | if not token: 77 | break 78 | token = token.strip() 79 | vocab[token] = index 80 | index += 1 81 | return vocab 82 | 83 | 84 | def convert_by_vocab(vocab, items): 85 | """Converts a sequence of [tokens|ids] using the vocab.""" 86 | output = [] 87 | for item in items: 88 | if item not in vocab: 89 | print("warning: %s not in vocab" % item) 90 | item = "[UNK]" 91 | output.append(vocab[item]) 92 | return output 93 | 94 | 95 | def convert_tokens_to_ids(vocab, tokens): 96 | return convert_by_vocab(vocab, tokens) 97 | 98 | 99 | def convert_ids_to_tokens(inv_vocab, ids): 100 | return convert_by_vocab(inv_vocab, ids) 101 | 102 | 103 | def whitespace_tokenize(text): 104 | """Runs basic whitespace cleaning and splitting on a peice of text.""" 105 | text = text.strip() 106 | if not text: 107 | return [] 108 | tokens = text.split() 109 | return tokens 110 | 111 | 112 | class FullTokenizer(object): 113 | """Runs end-to-end tokenziation.""" 114 | 115 | def __init__(self, vocab_file, do_lower_case=True): 116 | self.vocab = load_vocab(vocab_file) 117 | self.inv_vocab = {v: k for k, v in self.vocab.items()} 118 | self.basic_tokenizer = BasicTokenizer(do_lower_case=do_lower_case) 119 | self.wordpiece_tokenizer = WordpieceTokenizer(vocab=self.vocab) 120 | 121 | def tokenize(self, text): 122 | split_tokens = [] 123 | for token in self.basic_tokenizer.tokenize(text): 124 | for sub_token in self.wordpiece_tokenizer.tokenize(token): 125 | split_tokens.append(sub_token) 126 | 127 | return split_tokens 128 | 129 | def convert_tokens_to_ids(self, tokens): 130 | return convert_by_vocab(self.vocab, tokens) 131 | 132 | def convert_ids_to_tokens(self, ids): 133 | return convert_by_vocab(self.inv_vocab, ids) 134 | 135 | 136 | class BasicTokenizer(object): 137 | """Runs basic tokenization (punctuation splitting, lower casing, etc.).""" 138 | 139 | def __init__(self, do_lower_case=True): 140 | """Constructs a BasicTokenizer. 141 | Args: 142 | do_lower_case: Whether to lower case the input. 143 | """ 144 | self.do_lower_case = do_lower_case 145 | 146 | def tokenize(self, text): 147 | """Tokenizes a piece of text.""" 148 | text = convert_to_unicode(text) 149 | text = self._clean_text(text) 150 | 151 | # This was added on November 1st, 2018 for the multilingual and Chinese 152 | # models. This is also applied to the English models now, but it doesn't 153 | # matter since the English models were not trained on any Chinese data 154 | # and generally don't have any Chinese data in them (there are Chinese 155 | # characters in the vocabulary because Wikipedia does have some Chinese 156 | # words in the English Wikipedia.). 157 | text = self._tokenize_chinese_chars(text) 158 | 159 | orig_tokens = whitespace_tokenize(text) 160 | split_tokens = [] 161 | for token in orig_tokens: 162 | if self.do_lower_case: 163 | token = token.lower() 164 | token = self._run_strip_accents(token) 165 | split_tokens.extend(self._run_split_on_punc(token)) 166 | 167 | output_tokens = whitespace_tokenize(" ".join(split_tokens)) 168 | return output_tokens 169 | 170 | def _run_strip_accents(self, text): 171 | """Strips accents from a piece of text.""" 172 | text = unicodedata.normalize("NFD", text) 173 | output = [] 174 | for char in text: 175 | cat = unicodedata.category(char) 176 | if cat == "Mn": 177 | continue 178 | output.append(char) 179 | return "".join(output) 180 | 181 | def _run_split_on_punc(self, text): 182 | """Splits punctuation on a piece of text.""" 183 | chars = list(text) 184 | i = 0 185 | start_new_word = True 186 | output = [] 187 | while i < len(chars): 188 | char = chars[i] 189 | if _is_punctuation(char): 190 | output.append([char]) 191 | start_new_word = True 192 | else: 193 | if start_new_word: 194 | output.append([]) 195 | start_new_word = False 196 | output[-1].append(char) 197 | i += 1 198 | 199 | return ["".join(x) for x in output] 200 | 201 | def _tokenize_chinese_chars(self, text): 202 | """Adds whitespace around any CJK character.""" 203 | output = [] 204 | for char in text: 205 | cp = ord(char) 206 | if self._is_chinese_char(cp): 207 | output.append(" ") 208 | output.append(char) 209 | output.append(" ") 210 | else: 211 | output.append(char) 212 | return "".join(output) 213 | 214 | def _is_chinese_char(self, cp): 215 | """Checks whether CP is the codepoint of a CJK character.""" 216 | # This defines a "chinese character" as anything in the CJK Unicode block: 217 | # https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block) 218 | # 219 | # Note that the CJK Unicode block is NOT all Japanese and Korean characters, 220 | # despite its name. The modern Korean Hangul alphabet is a different block, 221 | # as is Japanese Hiragana and Katakana. Those alphabets are used to write 222 | # space-separated words, so they are not treated specially and handled 223 | # like the all of the other languages. 224 | if ((cp >= 0x4E00 and cp <= 0x9FFF) or # 225 | (cp >= 0x3400 and cp <= 0x4DBF) or # 226 | (cp >= 0x20000 and cp <= 0x2A6DF) or # 227 | (cp >= 0x2A700 and cp <= 0x2B73F) or # 228 | (cp >= 0x2B740 and cp <= 0x2B81F) or # 229 | (cp >= 0x2B820 and cp <= 0x2CEAF) or 230 | (cp >= 0xF900 and cp <= 0xFAFF) or # 231 | (cp >= 0x2F800 and cp <= 0x2FA1F)): # 232 | return True 233 | 234 | return False 235 | 236 | def _clean_text(self, text): 237 | """Performs invalid character removal and whitespace cleanup on text.""" 238 | output = [] 239 | for char in text: 240 | cp = ord(char) 241 | if cp == 0 or cp == 0xfffd or _is_control(char): 242 | continue 243 | if _is_whitespace(char): 244 | output.append(" ") 245 | else: 246 | output.append(char) 247 | return "".join(output) 248 | 249 | 250 | class WordpieceTokenizer(object): 251 | """Runs WordPiece tokenziation.""" 252 | 253 | def __init__(self, vocab, unk_token="[UNK]", max_input_chars_per_word=100): 254 | self.vocab = vocab 255 | self.unk_token = unk_token 256 | self.max_input_chars_per_word = max_input_chars_per_word 257 | 258 | def tokenize(self, text): 259 | """Tokenizes a piece of text into its word pieces. 260 | This uses a greedy longest-match-first algorithm to perform tokenization 261 | using the given vocabulary. 262 | For example: 263 | input = "unaffable" 264 | output = ["un", "##aff", "##able"] 265 | Args: 266 | text: A single token or whitespace separated tokens. This should have 267 | already been passed through `BasicTokenizer. 268 | Returns: 269 | A list of wordpiece tokens. 270 | """ 271 | 272 | text = convert_to_unicode(text) 273 | 274 | output_tokens = [] 275 | for token in whitespace_tokenize(text): 276 | chars = list(token) 277 | if len(chars) > self.max_input_chars_per_word: 278 | output_tokens.append(self.unk_token) 279 | continue 280 | 281 | is_bad = False 282 | start = 0 283 | sub_tokens = [] 284 | while start < len(chars): 285 | end = len(chars) 286 | cur_substr = None 287 | while start < end: 288 | substr = "".join(chars[start:end]) 289 | if start > 0: 290 | substr = "##" + substr 291 | if substr in self.vocab: 292 | cur_substr = substr 293 | break 294 | end -= 1 295 | if cur_substr is None: 296 | is_bad = True 297 | break 298 | sub_tokens.append(cur_substr) 299 | start = end 300 | 301 | if is_bad: 302 | # output_tokens.append(self.unk_token) 303 | output_tokens.append(token) # keep the UNK token 304 | else: 305 | output_tokens.extend(sub_tokens) 306 | return output_tokens 307 | 308 | 309 | def _is_whitespace(char): 310 | """Checks whether `chars` is a whitespace character.""" 311 | # \t, \n, and \r are technically contorl characters but we treat them 312 | # as whitespace since they are generally considered as such. 313 | if char == " " or char == "\t" or char == "\n" or char == "\r": 314 | return True 315 | cat = unicodedata.category(char) 316 | if cat == "Zs": 317 | return True 318 | return False 319 | 320 | 321 | def _is_control(char): 322 | """Checks whether `chars` is a control character.""" 323 | # These are technically control characters but we count them as whitespace 324 | # characters. 325 | if char == "\t" or char == "\n" or char == "\r": 326 | return False 327 | cat = unicodedata.category(char) 328 | if cat.startswith("C"): 329 | return True 330 | return False 331 | 332 | 333 | def _is_punctuation(char): 334 | """Checks whether `chars` is a punctuation character.""" 335 | cp = ord(char) 336 | # We treat all non-letter/number ASCII as punctuation. 337 | # Characters such as "^", "$", and "`" are not in the Unicode 338 | # Punctuation class but we treat them as punctuation anyways, for 339 | # consistency. 340 | if ((cp >= 33 and cp <= 47) or (cp >= 58 and cp <= 64) or 341 | (cp >= 91 and cp <= 96) or (cp >= 123 and cp <= 126)): 342 | return True 343 | cat = unicodedata.category(char) 344 | if cat.startswith("P"): 345 | return True 346 | return False -------------------------------------------------------------------------------- /models/seq2edit-based-CGEC/tokenization.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # Copyright 2018 The Google AI Language Team Authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | """Tokenization classes.""" 16 | 17 | from __future__ import absolute_import 18 | from __future__ import division 19 | from __future__ import print_function 20 | 21 | import collections 22 | import unicodedata 23 | import six 24 | 25 | 26 | def convert_to_unicode(text): 27 | """Converts `text` to Unicode (if it's not already), assuming utf-8 input.""" 28 | if six.PY3: 29 | if isinstance(text, str): 30 | return text 31 | elif isinstance(text, bytes): 32 | return text.decode("utf-8", "ignore") 33 | else: 34 | raise ValueError("Unsupported string type: %s" % (type(text))) 35 | elif six.PY2: 36 | if isinstance(text, str): 37 | return text.decode("utf-8", "ignore") 38 | elif isinstance(text, unicode): 39 | return text 40 | else: 41 | raise ValueError("Unsupported string type: %s" % (type(text))) 42 | else: 43 | raise ValueError("Not running on Python2 or Python 3?") 44 | 45 | 46 | def printable_text(text): 47 | """Returns text encoded in a way suitable for print or `tf.logging`.""" 48 | 49 | # These functions want `str` for both Python2 and Python3, but in one case 50 | # it's a Unicode string and in the other it's a byte string. 51 | if six.PY3: 52 | if isinstance(text, str): 53 | return text 54 | elif isinstance(text, bytes): 55 | return text.decode("utf-8", "ignore") 56 | else: 57 | raise ValueError("Unsupported string type: %s" % (type(text))) 58 | elif six.PY2: 59 | if isinstance(text, str): 60 | return text 61 | elif isinstance(text, unicode): 62 | return text.encode("utf-8") 63 | else: 64 | raise ValueError("Unsupported string type: %s" % (type(text))) 65 | else: 66 | raise ValueError("Not running on Python2 or Python 3?") 67 | 68 | 69 | def load_vocab(vocab_file): 70 | """Loads a vocabulary file into a dictionary.""" 71 | vocab = collections.OrderedDict() 72 | index = 0 73 | with open(vocab_file, "r") as reader: 74 | while True: 75 | token = convert_to_unicode(reader.readline()) 76 | if not token: 77 | break 78 | token = token.strip() 79 | vocab[token] = index 80 | index += 1 81 | return vocab 82 | 83 | 84 | def convert_by_vocab(vocab, items): 85 | """Converts a sequence of [tokens|ids] using the vocab.""" 86 | output = [] 87 | for item in items: 88 | if item not in vocab: 89 | print("warning: %s not in vocab" % item) 90 | item = "[UNK]" 91 | output.append(vocab[item]) 92 | return output 93 | 94 | 95 | def convert_tokens_to_ids(vocab, tokens): 96 | return convert_by_vocab(vocab, tokens) 97 | 98 | 99 | def convert_ids_to_tokens(inv_vocab, ids): 100 | return convert_by_vocab(inv_vocab, ids) 101 | 102 | 103 | def whitespace_tokenize(text): 104 | """Runs basic whitespace cleaning and splitting on a peice of text.""" 105 | text = text.strip() 106 | if not text: 107 | return [] 108 | tokens = text.split() 109 | return tokens 110 | 111 | 112 | class FullTokenizer(object): 113 | """Runs end-to-end tokenziation.""" 114 | 115 | def __init__(self, vocab_file, do_lower_case=True): 116 | self.vocab = load_vocab(vocab_file) 117 | self.inv_vocab = {v: k for k, v in self.vocab.items()} 118 | self.basic_tokenizer = BasicTokenizer(do_lower_case=do_lower_case) 119 | self.wordpiece_tokenizer = WordpieceTokenizer(vocab=self.vocab) 120 | 121 | def tokenize(self, text): 122 | split_tokens = [] 123 | for token in self.basic_tokenizer.tokenize(text): 124 | for sub_token in self.wordpiece_tokenizer.tokenize(token): 125 | split_tokens.append(sub_token) 126 | 127 | return split_tokens 128 | 129 | def convert_tokens_to_ids(self, tokens): 130 | return convert_by_vocab(self.vocab, tokens) 131 | 132 | def convert_ids_to_tokens(self, ids): 133 | return convert_by_vocab(self.inv_vocab, ids) 134 | 135 | 136 | class BasicTokenizer(object): 137 | """Runs basic tokenization (punctuation splitting, lower casing, etc.).""" 138 | 139 | def __init__(self, do_lower_case=True): 140 | """Constructs a BasicTokenizer. 141 | Args: 142 | do_lower_case: Whether to lower case the input. 143 | """ 144 | self.do_lower_case = do_lower_case 145 | 146 | def tokenize(self, text): 147 | """Tokenizes a piece of text.""" 148 | text = convert_to_unicode(text) 149 | text = self._clean_text(text) 150 | 151 | # This was added on November 1st, 2018 for the multilingual and Chinese 152 | # models. This is also applied to the English models now, but it doesn't 153 | # matter since the English models were not trained on any Chinese data 154 | # and generally don't have any Chinese data in them (there are Chinese 155 | # characters in the vocabulary because Wikipedia does have some Chinese 156 | # words in the English Wikipedia.). 157 | text = self._tokenize_chinese_chars(text) 158 | 159 | orig_tokens = whitespace_tokenize(text) 160 | split_tokens = [] 161 | for token in orig_tokens: 162 | if self.do_lower_case: 163 | token = token.lower() 164 | token = self._run_strip_accents(token) 165 | split_tokens.extend(self._run_split_on_punc(token)) 166 | 167 | output_tokens = whitespace_tokenize(" ".join(split_tokens)) 168 | return output_tokens 169 | 170 | def _run_strip_accents(self, text): 171 | """Strips accents from a piece of text.""" 172 | text = unicodedata.normalize("NFD", text) 173 | output = [] 174 | for char in text: 175 | cat = unicodedata.category(char) 176 | if cat == "Mn": 177 | continue 178 | output.append(char) 179 | return "".join(output) 180 | 181 | def _run_split_on_punc(self, text): 182 | """Splits punctuation on a piece of text.""" 183 | chars = list(text) 184 | i = 0 185 | start_new_word = True 186 | output = [] 187 | while i < len(chars): 188 | char = chars[i] 189 | if _is_punctuation(char): 190 | output.append([char]) 191 | start_new_word = True 192 | else: 193 | if start_new_word: 194 | output.append([]) 195 | start_new_word = False 196 | output[-1].append(char) 197 | i += 1 198 | 199 | return ["".join(x) for x in output] 200 | 201 | def _tokenize_chinese_chars(self, text): 202 | """Adds whitespace around any CJK character.""" 203 | output = [] 204 | for char in text: 205 | cp = ord(char) 206 | if self._is_chinese_char(cp): 207 | output.append(" ") 208 | output.append(char) 209 | output.append(" ") 210 | else: 211 | output.append(char) 212 | return "".join(output) 213 | 214 | def _is_chinese_char(self, cp): 215 | """Checks whether CP is the codepoint of a CJK character.""" 216 | # This defines a "chinese character" as anything in the CJK Unicode block: 217 | # https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block) 218 | # 219 | # Note that the CJK Unicode block is NOT all Japanese and Korean characters, 220 | # despite its name. The modern Korean Hangul alphabet is a different block, 221 | # as is Japanese Hiragana and Katakana. Those alphabets are used to write 222 | # space-separated words, so they are not treated specially and handled 223 | # like the all of the other languages. 224 | if ((cp >= 0x4E00 and cp <= 0x9FFF) or # 225 | (cp >= 0x3400 and cp <= 0x4DBF) or # 226 | (cp >= 0x20000 and cp <= 0x2A6DF) or # 227 | (cp >= 0x2A700 and cp <= 0x2B73F) or # 228 | (cp >= 0x2B740 and cp <= 0x2B81F) or # 229 | (cp >= 0x2B820 and cp <= 0x2CEAF) or 230 | (cp >= 0xF900 and cp <= 0xFAFF) or # 231 | (cp >= 0x2F800 and cp <= 0x2FA1F)): # 232 | return True 233 | 234 | return False 235 | 236 | def _clean_text(self, text): 237 | """Performs invalid character removal and whitespace cleanup on text.""" 238 | output = [] 239 | for char in text: 240 | cp = ord(char) 241 | if cp == 0 or cp == 0xfffd or _is_control(char): 242 | continue 243 | if _is_whitespace(char): 244 | output.append(" ") 245 | else: 246 | output.append(char) 247 | return "".join(output) 248 | 249 | 250 | class WordpieceTokenizer(object): 251 | """Runs WordPiece tokenziation.""" 252 | 253 | def __init__(self, vocab, unk_token="[UNK]", max_input_chars_per_word=100): 254 | self.vocab = vocab 255 | self.unk_token = unk_token 256 | self.max_input_chars_per_word = max_input_chars_per_word 257 | 258 | def tokenize(self, text): 259 | """Tokenizes a piece of text into its word pieces. 260 | This uses a greedy longest-match-first algorithm to perform tokenization 261 | using the given vocabulary. 262 | For example: 263 | input = "unaffable" 264 | output = ["un", "##aff", "##able"] 265 | Args: 266 | text: A single token or whitespace separated tokens. This should have 267 | already been passed through `BasicTokenizer. 268 | Returns: 269 | A list of wordpiece tokens. 270 | """ 271 | 272 | text = convert_to_unicode(text) 273 | 274 | output_tokens = [] 275 | for token in whitespace_tokenize(text): 276 | chars = list(token) 277 | if len(chars) > self.max_input_chars_per_word: 278 | output_tokens.append(self.unk_token) 279 | continue 280 | 281 | is_bad = False 282 | start = 0 283 | sub_tokens = [] 284 | while start < len(chars): 285 | end = len(chars) 286 | cur_substr = None 287 | while start < end: 288 | substr = "".join(chars[start:end]) 289 | if start > 0: 290 | substr = "##" + substr 291 | if substr in self.vocab: 292 | cur_substr = substr 293 | break 294 | end -= 1 295 | if cur_substr is None: 296 | is_bad = True 297 | break 298 | sub_tokens.append(cur_substr) 299 | start = end 300 | 301 | if is_bad: 302 | # output_tokens.append(self.unk_token) 303 | output_tokens.append(token) # keep the UNK token 304 | else: 305 | output_tokens.extend(sub_tokens) 306 | return output_tokens 307 | 308 | 309 | def _is_whitespace(char): 310 | """Checks whether `chars` is a whitespace character.""" 311 | # \t, \n, and \r are technically contorl characters but we treat them 312 | # as whitespace since they are generally considered as such. 313 | if char == " " or char == "\t" or char == "\n" or char == "\r": 314 | return True 315 | cat = unicodedata.category(char) 316 | if cat == "Zs": 317 | return True 318 | return False 319 | 320 | 321 | def _is_control(char): 322 | """Checks whether `chars` is a control character.""" 323 | # These are technically control characters but we count them as whitespace 324 | # characters. 325 | if char == "\t" or char == "\n" or char == "\r": 326 | return False 327 | cat = unicodedata.category(char) 328 | if cat.startswith("C"): 329 | return True 330 | return False 331 | 332 | 333 | def _is_punctuation(char): 334 | """Checks whether `chars` is a punctuation character.""" 335 | cp = ord(char) 336 | # We treat all non-letter/number ASCII as punctuation. 337 | # Characters such as "^", "$", and "`" are not in the Unicode 338 | # Punctuation class but we treat them as punctuation anyways, for 339 | # consistency. 340 | if ((cp >= 33 and cp <= 47) or (cp >= 58 and cp <= 64) or 341 | (cp >= 91 and cp <= 96) or (cp >= 123 and cp <= 126)): 342 | return True 343 | cat = unicodedata.category(char) 344 | if cat.startswith("P"): 345 | return True 346 | return False --------------------------------------------------------------------------------