中文序列标注任务(二)

简介

记录中文序列标注任务:动宾搭配识别,主要学习代码中的数据处理,评价函数,模型搭建部分

1. 数据长这样:


采用的是 BIOES 标注,利用句子中成对出现的动宾搭配,到原句子中去匹配,获得带有 动宾 标签的 原句子序列.

2. 数据处理:

下面主要记录一下,要输入bert 预训练模型之前,将数据应该处理成什么样子:

  • 原始代码是手动处理的,其实可以直接使用AutoTokenizer,通过调用函数:word_ids() 获得转换为token 之后的词(可能有子词)在原始句子中的位置id,然后根据这个id 序列将 label 转换为对应的 id
  • 这里注意 label 序列是如何分布的,因为经过 tokenizer 之后原始句子会 前后加上一个 [CLS] 和 [SEP],然后后面进行padding;对应到 label 上面需要跟 句子 input 长度保持一致,[CLS]&[SEP]是补上的 标签 'O' 对应的 id,padding 部分是直接填充 数字 0,作为索引.
  • 同时这里还保存了 原始的句子token:ntokens ,为后续的预测句子预留了调用原始token 空间
textlist=['实', '际', '上', ',', '不', '仅', '法', '国', '殖', '民', '主', '义', '者', ',', '而', '且', '美', '、', '英', '殖', '民', '主', '义', '者', '都', '对', '这', '个', '“', '根', '本', '法', '”', '草', '案', '寄', '予', '很', '大', '希', '望', '。']
labellist=['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'B-V', 'E-V', 'O', 'O', 'B-N', 'E-N', 'O']
label_map={'B-V': 0, 'E-N': 1, 'E-V': 2, 'B-N': 3, 'O': 4}
print("label_map: ",label_map)

from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-chinese")
tokenized_input = tokenizer(textlist, is_split_into_words=True)

print("tokenized_input: ",tokenized_input)
ntokens = tokenizer.convert_ids_to_tokens(tokenized_input["input_ids"])
print("ntokens: ",ntokens)
word_ids=tokenized_input.word_ids()
print("word_ids: ",word_ids)
label_id=[]
for item in word_ids:#词的id 索引
    if item is None:
        label_id.append(label_map["O"])
    else:
        label_id.append(label_map[labellist[item]])
print(label_id)
while len(tokenized_input["input_ids"]) < 50:
    tokenized_input["input_ids"].append(0)
    tokenized_input["attention_mask"].append(0)
    tokenized_input["token_type_ids"].append(0)
    # we don't concerned about it!
    label_id.append(0)#padding直接补零
    ntokens.append("**NULL**")
print("input_ids: ",tokenized_input["input_ids"])
print("input_mask: ",tokenized_input["attention_mask"])
print("segment_ids: ",tokenized_input["token_type_ids"])
print("label_ids: ",label_id)
print("ntokens: ",ntokens)
#这一部分因为transformer 版本的原因没有使用成功,还是使用的手动处理各项,input,attention,labe 等

3. 模型部分

模型采用的是 bert+BiLSTM+crf,其中BiLSTM部分是可选项,可以在传参的过程中选择是否保留,CRF 是调用的 torchcrf 包,封装好的包,使用很方便进行预测解码等操作.

4. 评价函数

  • 这一部分发现,使用crf预测值 长度是不包含 padding 部分的,也就是说,最后模型给出一个序列可能的标签id,长度为原始句子长度加上前后的 [CLS] & [SEP] 开始和结束标识.但是原始 label 序列经过处理是经过padding的,长度往往远大于预测标签序列长度,zip()函数可以对应起来进行预测,下面会说.
  • 使用的是 python 版本的 conll03官方的评测脚本conlleval.py
    原始脚本在:https://github.com/spyysalo/conlleval.py
    python 版本支持 IOBES 标签,个人感觉代码稍微有些麻烦,总结下来评价方式是:
    预测的标签真实标签原始句子token一一对应之后:zip()函数实现,可以只获得以 [SEP] 结尾的长度,全部句子对应成三元组形式,放入一个list,句子之间以 '\n' 分割.
    下面这只是一个句子,最后是'\n'
['实 O E-N\n', '际 O B-V\n', '上 O B-V\n', ', O B-V\n', '不 O B-V\n', '仅 O B-V\n', '法 O B-V\n', '国 O B-V\n', '殖 O B-V\n', '民 O B-V\n', '主 O B-V\n', '义 O B-V\n', '者 O B-V\n', ', O B-V\n', '而 O B-V\n', '且 O B-V\n', '美 O B-V\n', '、 O B-V\n', '英 O B-V\n', '殖 O B-V\n', '民 O B-V\n', '主 O B-V\n', '义 O B-V\n', '者 O B-V\n', '都 O B-V\n', '对 O B-V\n', '这 O B-V\n', '个 O B-V\n', '“ O B-N\n', '根 O B-V\n', '本 O B-V\n', '法 O B-N\n', '” O B-N\n', '草 O B-N\n', '案 O B-N\n', '寄 B-V B-V\n', '予 E-V B-V\n', '很 O B-V\n', '大 O B-V\n', '希 B-N B-V\n', '望 E-N B-N\n', '。 O B-N\n', '\n']

然后整个代码就是遍历这些对应的 三元组,判断模型预测值跟真实的标签值是否一致,同时还要判断两者并行的 序列是否是正确预测的,比如模型预测的是否是 一个 chunk 块的开始,结束,中间,是否都跟真实标签值对应上了,然后一一计数,装在一个数据结构中:

self.correct_chunk = 0    # number of correctly identified chunks
self.correct_tags = 0     # number of correct chunk tags
self.found_correct = 0    # number of chunks in corpus
self.found_guessed = 0    # number of identified chunks
self.token_counter = 0    # token counter (ignores sentence breaks)

# counts by type#对chunk的type进行计数,参与正确率计算
self.t_correct_chunk = defaultdict(int)
self.t_found_correct = defaultdict(int)
self.t_found_guessed = defaultdict(int)

最后计算精确率,召回率,F1值

def calculate_metrics(correct, guessed, total):#计算正确率,召回率和F1
    tp, fp, fn = correct, guessed-correct, total-correct#正确预测的/错误预测的/全部语料块-你正确预测的=还有多少是正确的
    p = 0 if tp + fp == 0 else 1.*tp / (tp + fp)#正确识别的/正确识别的+错误预测的##真正正确的占所有预测为正的比例。
    r = 0 if tp + fn == 0 else 1.*tp / (tp + fn)#正确识别的/正确识别的+还有多少是正确的##真正正确的占所有实际为正的比例。
    f = 0 if p + r == 0 else 2 * p * r / (p + r)
    return Metrics(tp, fp, fn, p, r, f)

同时会计算 每个标签的 type 识别正确率,这是通过 建立的字典数据结构存储的:

# counts by type#对chunk的type进行计数,参与正确率计算
self.t_correct_chunk = defaultdict(int)
self.t_found_correct = defaultdict(int)
self.t_found_guessed = defaultdict(int)

在 test 上面预测新的标签的时候有一个要注意的地方,原始label 没有加了 [CLS]&[SEP]标志,而预测的结果是加了的,写入文件的时候是zip并行对应写入,遇到开始结束标志会 跳过,所以真实序列跟预测的序列位置为相差一个值,但是不影响测评结果,也就是这样的:第二列是真实label,第三列是预测label:


下面是简单是实验结果,没有精细的微调参数,只是上手实践.

image.png
image.png

你可能感兴趣的:(中文序列标注任务(二))