中文序列標注任務(二)

簡介

記錄中文序列標注任務:動賓搭配識別,主要學習代碼中的數(shù)據(jù)處理,評價函數(shù),模型搭建部分

1. 數(shù)據(jù)長這樣:


采用的是 BIOES 標注,利用句子中成對出現(xiàn)的動賓搭配,到原句子中去匹配,獲得帶有 動賓 標簽的 原句子序列.

2. 數(shù)據(jù)處理:

下面主要記錄一下,要輸入bert 預訓練模型之前,將數(shù)據(jù)應該處理成什么樣子:

  • 原始代碼是手動處理的,其實可以直接使用AutoTokenizer,通過調(diào)用函數(shù):word_ids() 獲得轉換為token 之后的詞(可能有子詞)在原始句子中的位置id,然后根據(jù)這個id 序列將 label 轉換為對應的 id
  • 這里注意 label 序列是如何分布的,因為經(jīng)過 tokenizer 之后原始句子會 前后加上一個 [CLS] 和 [SEP],然后后面進行padding;對應到 label 上面需要跟 句子 input 長度保持一致,[CLS]&[SEP]是補上的 標簽 'O' 對應的 id,padding 部分是直接填充 數(shù)字 0,作為索引.
  • 同時這里還保存了 原始的句子token:ntokens ,為后續(xù)的預測句子預留了調(diào)用原始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 是調(diào)用的 torchcrf 包,封裝好的包,使用很方便進行預測解碼等操作.

4. 評價函數(shù)

  • 這一部分發(fā)現(xiàn),使用crf預測值 長度是不包含 padding 部分的,也就是說,最后模型給出一個序列可能的標簽id,長度為原始句子長度加上前后的 [CLS] & [SEP] 開始和結束標識.但是原始 label 序列經(jīng)過處理是經(jīng)過padding的,長度往往遠大于預測標簽序列長度,zip()函數(shù)可以對應起來進行預測,下面會說.
  • 使用的是 python 版本的 conll03官方的評測腳本conlleval.py
    原始腳本在:https://github.com/spyysalo/conlleval.py
    python 版本支持 IOBES 標簽,個人感覺代碼稍微有些麻煩,總結下來評價方式是:
    預測的標簽真實標簽原始句子token一一對應之后:zip()函數(shù)實現(xiàn),可以只獲得以 [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 塊的開始,結束,中間,是否都跟真實標簽值對應上了,然后一一計數(shù),裝在一個數(shù)據(jù)結構中:

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進行計數(shù),參與正確率計算
self.t_correct_chunk = defaultdict(int)
self.t_found_correct = defaultdict(int)
self.t_found_guessed = defaultdict(int)

最后計算精確率,召回率,F(xiàn)1值

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 識別正確率,這是通過 建立的字典數(shù)據(jù)結構存儲的:

# counts by type#對chunk的type進行計數(shù),參與正確率計算
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:


下面是簡單是實驗結果,沒有精細的微調(diào)參數(shù),只是上手實踐.

image.png
image.png
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容