中文命名實體識別(Named Entity Recognition,NER)初探

一、NER技術簡介

命名實體識別(Named Entity Recognition,NER),又稱作“專名識別”,是指識別文本中具有特定意義的實體,主要包括:

  • 人名
  • 地名
  • 機構名
  • 專有名詞等

NER是:

  • 信息提取
  • 問答系統
  • 句法分析
  • 機器翻譯
  • 面向Semantic Web的元數據標註等

應用領域的重要基礎工具,在自然語言處理技術走向實用化的過程中佔有重要的地位。

例如在搜索場景下,NER是深度查詢理解(Deep Query Understanding,簡稱 DQU)的底層基礎信號,主要應用於搜索召回、用戶意圖識別、實體鏈接等環節,NER信號的質量,直接影響到用戶的搜索體驗,是NLP中一項非常基礎的任務。

這裏針對搜索召回稍微展開一些細節。

在O2O搜索中,對商家POI的描述是商家名稱、地址、品類等多個互相之間相關性並不高的文本域。如果對O2O搜索引擎也採用全部文本域命中求交的方式,就可能會產生大量的誤召回。一種解決方法如下圖所示,

即讓特定的查詢只在特定的文本域做倒排檢索,我們稱之爲“結構化召回”,可保證召回商家的強相關性。

舉例來說,對於“海底撈”這樣的請求,有些商家地址會描述爲“海底撈附近幾百米”,若採用全文本域檢索這些商家就會被召回,顯然這並不是用戶想要的。而結構化召回基於NER將“海底撈”識別爲商家,然後只在商家名相關文本域檢索,從而只召回海底撈品牌商家,精準地滿足了用戶需求。

0x1:命名實體識別是什麼? 

要了解NER是一回什麼事,首先要先說清楚,什麼是實體。簡單的理解,實體,可以認爲是某一個概念的實例。

例如,

  • “人名”是一種概念,或者說實體類型,那麼“蔡英文”就是一種“人名”實體了。
  • “時間”是一種實體類型,那麼“中秋節”就是一種“時間”實體了。

所謂實體識別,就是將你想要獲取到的實體類型,從一句話裏面挑出來的過程。

小明 在 北京大學 的 燕園 看了
PER ORG LOC

中國男籃 的一場比賽
ORG

如上面的例子所示,句子“小明在北京大學的燕園看了中國男籃 的一場比賽”,通過NER模型,將“小明 ”以PER,“北京大學”以ORG,“燕園”以LOC,“中國男籃”以ORG爲類別分別挑了出來。

0x2:命名實體識別的數據標註方式 

NER是一種序列標註問題,因此他們的數據標註方式也遵照序列標註問題的方式,主要是以下幾種方法:

  • IOB
  • BIOES
  • Markup

1、BIOES數據標註方式

先列出來BIOES分別代表什麼意思:

  • B,即Begin,表示開始
  • I,即Intermediate,表示中間
  • E,即End,表示結尾
  • S,即Single,表示單個字符
  • O,即Other,表示其他,用於標記無關字符

將“小明在北京大學的燕園看了中國男籃的一場比賽”這句話,進行標註,結果就是:

[B-PER,E-PER,O, B-ORG,I-ORG,I-ORG,E-ORG,O,B-LOC,E-LOC,O,O,B-ORG,I-ORG,I-ORG,E-ORG,O,O,O,O]

換句話說,NER的過程,就是根據輸入的句子,預測出其標註序列的過程。

2、IOB數據標註方式

  • I,表示內部
  • O,表示外部
  • B,表示開始

例子:B/I-XXX,其中:

  • B/I,表示這個詞屬於命名實體的開始或內部
  • XXX,表示命名實體的類型

0x3:命名實體識別的方法介紹

方法 代表技術 核心思想
基於規則方法 字典、規則 關注規則
基於機器學習方法 HMM、HEMM、ME、SVM、CRF 關注概率
基於深度學習方法 BiLSTM-CNN-CRF、BERT-BiLSTM-CRF 關注整體效果
基於大模型方法 注意力模型、遷移學習、GPT-3.5、Llama 關注整體效果、性能

1、實體詞典匹配 - 基於規則方法

基本思想是匹配規則,

在很多搜索場景實體識別的工業實踐中,整體技術選型往往是“實體詞典匹配+模型預測”的框架,如下圖所示,

實體詞典匹配,通俗來說就是”規則匹配“,這種算法主要有如下優點:

  • 1、搜索中用戶查詢的頭部流量通常較短、表達形式簡單,且集中在商戶、品類、地址等三類實體搜索,實體詞典匹配雖簡單但處理這類查詢準確率也可達到90%以上。
  • 2、詞典是NER領域強相關的,通過挖掘業務數據資源獲取業務實體詞典,經過在線詞典匹配後可保證識別結果是領域適配的。
  • 3、新業務接入更加靈活,只需提供業務相關的實體詞表就可完成新業務場景下的實體識別。
  • 4、NER下游使用方中有些對響應時間要求極高,詞典匹配速度快,基本不存在性能問題。
既然詞典匹配有這麼多優點,那爲什麼還需要模型預測呢?答案主要有兩方面原因:
  • 1、隨着搜索體量的不斷增大,中長尾搜索流量表述複雜,越來越多OOV(Out Of Vocabulary)問題開始出現,實體詞典已經無法滿足日益多樣化的用戶需求,模型預測具備泛化能力,可作爲詞典匹配的有效補充。
  • 2、實體詞典匹配無法解決歧義問題,比如“黃鶴樓美食”,“黃鶴樓”在實體詞典中同時是武漢的景點、北京的商家、香菸產品,詞典匹配不具備消歧能力,這三種類型都會輸出,而模型預測則可結合上下文,不會輸出“黃鶴樓”是香菸產品。

2、HMM和CRF等機器學習算法 - 基於機器學習方法

HMM的相關細節可以參閱這篇文章。 

CRF的相關細節可以參閱這篇文章。  

3、基於深度學習方法

抽象來說,基於深度學習進行NER任務可以分爲如下兩個步驟:

  • representation extraction(詞向量化):旨在獲取輸入序列中每個標記的高維表示。爲了嵌入每個輸入單詞x,首先將輸入句子X輸入到編碼器模型(例如BERT)。然後,將單詞嵌入模型的最後一層的輸出用作高維表示hi ∈ Rm×1,其中n表示輸入句子的長度,m表示向量的維數的可變參數。
  • classification(多分類):對於分類,每個嵌入的高維向量h被髮送到一個多層感知器,然後使用softmax函數生成命名實體詞彙的分佈: pNER = softmax MLP( h ∈ Rm×1 )

1)LSTM+CRF

採用LSTM作爲特徵抽取器,再接一個CRF層來作爲輸出層。

2)CNN+CRF

CNN雖然在長序列的特徵提取上有弱勢,但是CNN模型可有並行能力,有運算速度快的優勢。膨脹卷積的引入,使得CNN在NER任務中,能夠兼顧運算速度和長序列的特徵提取。 

3)BERT+(LSTM)+CRF

BERT中蘊含了大量的通用知識,利用預訓練好的BERT模型,再用少量的標註數據進行FINETUNE是一種快速的獲得效果不錯的NER的方法。

4、ChatGPT zero-shot prompt技術 - 基於大模型方法

0x4:命名實體識別的未來挑戰

  • 數量無窮:業務不斷髮展,用戶量不斷增加,命名實體的數量不斷增加
  • 構詞靈活:例如”廣州恆大淘寶俱樂部“、”廣州恆大“、”恆大“
  • 類別模糊:例如”廣州未贏夠“、“廣州下雪嘞” 

參考鏈接:

https://zhuanlan.zhihu.com/p/88544122 
https://tech.meituan.com/2020/07/23/ner-in-meituan-nlp.html
https://zhuanlan.zhihu.com/p/156914795

 

二、命名實體識別工具

中文命名實體識別工具有很多,

工具簡介訪問地址
Stanford NER 斯坦福大學開發的基於條件隨機場的命名實體識別系統,該系統參數是基於CoNLL、MUC-6、MUC-7和ACE命名實體語料訓練出來的。 官網 | GitHub 地址
MALLET 麻省大學開發的一個統計自然語言處理的開源包,其序列標註工具的應用中能夠實現命名實體識別。 官網
Hanlp HanLP是一系列模型與算法組成的NLP工具包,由大快搜索主導並完全開源,目標是普及自然語言處理在生產環境中的應用。支持命名實體識別。 官網 | GitHub 地址
NLTK NLTK是一個高效的Python構建的平臺,用來處理人類自然語言數據。 官網 | GitHub 地址
SpaCy 工業級的自然語言處理工具,遺憾的是不支持中文。 官網 | GitHub 地址
Crfsuite 可以載入自己的數據集去訓練CRF實體識別模型。 文檔 | GitHub 地址
CRF++   GitHub地址

0x1:HanLP

1、安裝

pip install pyhanlp

2、中文分詞 

3、依存句法分析

4、API調用

from pyhanlp import *

print(HanLP.segment('你好,歡迎在Python中調用HanLP的API'))
for term in HanLP.segment('下雨天地面積水'):
    print('{}\t{}'.format(term.word, term.nature)) # 獲取單詞與詞性
testCases = [
    "商品和服務",
    "結婚的和尚未結婚的確實在干擾分詞啊",
    "買水果然後來世博園最後去世博會",
    "中國的首都是北京",
    "歡迎新老師生前來就餐",
    "工信處女幹事每月經過下屬科室都要親口交代24口交換機等技術性器件的安裝工作",
    "隨着頁遊興起到現在的頁遊繁盛,依賴於存檔進行邏輯判斷的設計減少了,但這塊也不能完全忽略掉。"]
for sentence in testCases: print(HanLP.segment(sentence))
# 關鍵詞提取
document = "水利部水資源司司長陳明忠9月29日在國務院新聞辦舉行的新聞發佈會上透露," \
           "根據剛剛完成了水資源管理制度的考覈,有部分省接近了紅線的指標," \
           "有部分省超過紅線的指標。對一些超過紅線的地方,陳明忠表示,對一些取用水項目進行區域的限批," \
           "嚴格地進行水資源論證和取水許可的批准。"
print(HanLP.extractKeyword(document, 2))
# 自動摘要
print(HanLP.extractSummary(document, 3))
# 依存句法分析
print(HanLP.parseDependency("徐先生還具體幫助他確定了把畫雄鷹、松鼠和麻雀作爲主攻目標。"))

參考鏈接:

https://hanlp.hankcs.com/
https://github.com/hankcs/pyhanlp
https://easyai.tech/ai-definition/ner/

  

三、BiLSTM+CRF,實現命名實體識別任務

0x1:模型架構簡述

近年來,隨着硬件計算能力的發展以及詞的分佈式表示(word embedding)的提出,神經網絡可以有效處理許多NLP任務。這類方法對於序列標註任務(如CWS、POS、NER)的處理方式是類似的:將token從離散one-hot表示映射到低維空間中成爲稠密的embedding,隨後將句子的embedding序列輸入到RNN中,用神經網絡自動提取特徵,Softmax來預測每個token的標籤。

這種方法使得模型的訓練成爲一個端到端的過程,而非傳統的pipeline,不依賴於特徵工程,是一種數據驅動的方法,但網絡種類繁多、對參數設置依賴大,模型可解釋性差。此外,這種方法的一個缺點是對每個token打標籤的過程是獨立的進行,不能直接利用上文已經預測的標籤(只能靠隱含狀態傳遞上文信息),進而導致預測出的標籤序列可能是無效的,例如標籤I-PER後面是不可能緊跟着B-PER的,但Softmax不會利用到這個信息。

爲了解決這個問題,學界提出了DL-CRF模型做序列標註。在神經網絡的輸出層接入CRF層(重點是利用標籤轉移概率)來做句子級別的標籤預測,使得標註過程不再是對各個token獨立分類。 

應用於NER中的BiLSTM-CRF模型主要由Embedding層(主要有詞向量,字向量以及一些額外特徵),雙向LSTM層,以及最後的CRF層構成。

實驗結果表明BiLSTM-CRF已經達到或者超過了基於豐富特徵的CRF模型,成爲目前基於深度學習的NER方法中的最主流模型。

在特徵方面,該模型繼承了深度學習方法的優勢,無需特徵工程,使用詞向量以及字符向量就可以達到很好的效果,如果有高質量的詞典特徵,能夠進一步提升效果。

接下里的問題是,什麼是CRF層?爲什麼要用CRF?

首先,句子xxx中的每個單詞表達成一個向量,該向量包含了上述的word embedding和character embedding,其中character embedding隨機初始化,word embedding通常採用預訓練模型初始化。所有的embeddings 將在訓練過程中進行微調。

其次,BiLSTM-CRF模型的的輸入是上述的embeddings,輸出是該句子xxx中每個單詞的預測標籤

從上圖可以看出,BiLSTM層的輸出是每個標籤的得分,如單詞w0w_0w0,BiLSTM的輸出爲1.5(B-Person),0.9(I-Person),0.1(B-Organization), 0.08 (I-Organization) and 0.05 (O),這些得分就是CRF層的輸入。

將BiLSTM層預測的得分喂進CRF層,具有最高得分的標籤序列將是模型預測的最好結果。

根據上文,能夠發現,如果沒有CRF層,即我們用下圖所示訓練BiLSTM命名實體識別模型: 

因爲BiLSTM針對每個單詞的輸出是標籤得分,對於每個單詞,我們可以選擇最高得分的標籤作爲預測結果。

例如,對於w0w_0w0,“B-Person"得分最高(1.5),因此我們可以選擇“B-Person”最爲其預測標籤;同樣的,w1w_1w1的標籤爲"I-Person”,w2w_2w2的爲"O", w3w_3w3的標籤爲"B-Organization",w4w_4w4的標籤爲"O"。

按照上述方法,對於xxx雖然我們得到了正確的標籤,但是大多數情況下是不能獲得正確標籤的,例如下圖的例子:

顯然,輸出標籤“I-Organization I-Person” 和 “B-Organization I-Person”是不對的。

CRF層可以對最終的約束標籤添加一些約束條件,從而保證預測標籤的有效性。而這些約束條件是CRF層自動從訓練數據中學到。

約束可能是:

  • 一句話中第一個單詞的標籤應該是“B-“ or “O”,而不能是"I-";
  • “B-label1 I-label2 I-label3 I-…”中,label1, label2, label3 …應該是相同的命名實體標籤。如“B-Person I-Person”是有效的,而“B-Person I-Organization” 是無效的;
  • “O I-label” 是無效的。一個命名實體的第一個標籤應該以 “B-“ 開頭,而不能以“I-“開頭,換句話說, 應該是“O B-label”這種模式;

有了這些約束條件,無效的預測標籤序列將急劇減少。

0x2:代碼示例

1、命名實體識別的數據標註結構

項目使用了conll2003_v2數據集,其中標註的命名實體共計九類:

  • 'O'
  • 'B-LOC'
  • 'B-PER'
  • 'B-ORG'
  • 'I-PER'
  • 'I-ORG'
  • 'B-MISC'
  • 'I-LOC'
  • 'I-MISC'

實現了將輸入識別爲命名實體的模型,如下所示: 

# input
['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.']

# output
['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O', 'O']

2、數據預處理

數據下載並解壓,以供訓練,下載解壓後可以看到三個文件:

  • test.txt
  • train.txt
  • valid.txt

打開後可以看到,數據格式如下:

我們只需要每行開頭和最後一個數據,他們分別是文本信息和命名實體。

我們需要將數據進行處理,使之成爲網絡能接收的形式。

from tqdm import tqdm
from tensorflow.keras.layers import *
from tensorflow.keras.models import *
from tensorflow.keras.optimizers import *
import numpy as np


class NerDatasetReader:
    def read(self, data_path):
        data_parts = ['train', 'valid', 'test']
        extension = '.txt'
        dataset = {}
        for data_part in tqdm(data_parts):
            file_path = data_path + data_part + extension
            dataset[data_part] = self.read_file(str(file_path))
        return dataset
            
    def read_file(self, file_path):
        fileobj = open(file_path, 'r', encoding='utf-8')
        samples = []
        tokens = []
        tags = []

        for content in fileobj:
            content = content.strip('\n')
            if content == '-DOCSTART- -X- -X- O':
                pass
            elif content == '':
                if len(tokens) != 0:
                    # 每一句保存爲了兩個list,一個是單詞list,另一個是標註list
                    samples.append((tokens, tags))
                    tokens = []
                    tags = []
            else:
                contents = content.split(' ')
                tokens.append(contents[0])
                tags.append(contents[-1])
        return samples


def get_dicts(datas):
    w_all_dict, n_all_dict = {}, {}
    for sample in datas:
        for token, tag in zip(*sample):
            if token not in w_all_dict.keys():
                w_all_dict[token] = 1
            else:
                w_all_dict[token] += 1
            
            if tag not in n_all_dict.keys():
                n_all_dict[tag] = 1
            else:
                n_all_dict[tag] += 1

    sort_w_list = sorted(w_all_dict.items(),  key=lambda d: d[1], reverse=True)
    sort_n_list = sorted(n_all_dict.items(),  key=lambda d: d[1], reverse=True)
    # 保留前15999個常用的單詞,新增了一個"UNK"代表未知單詞。
    w_keys = [x for x, _ in sort_w_list[:15999]]
    w_keys.insert(0, "UNK")

    n_keys = [x for x, _ in sort_n_list]
    w_dict = {x:i for i, x in enumerate(w_keys)}
    n_dict = {x:i for i, x in enumerate(n_keys)}
    return(w_dict, n_dict)


def w2num(datas, w_dict, n_dict):
    ret_datas = []
    for sample in datas:
        num_w_list, num_n_list = [], []
        for token, tag in zip(*sample):
            if token not in w_dict.keys():
                token = "UNK"
            if tag not in n_dict:
                tag = "O"
            num_w_list.append(w_dict[token])
            num_n_list.append(n_dict[tag])
        ret_datas.append((num_w_list, num_n_list, len(num_n_list)))
    return(ret_datas)


def len_norm(data_num, lens=80):
    ret_datas = []
    for sample1 in list(data_num):
        sample = list(sample1)
        ls = sample[-1]
        # print(sample)
        while(ls < lens):
            sample[0].append(0)
            ls = len(sample[0])
            sample[1].append(0)
        else:
            sample[0] = sample[0][:lens]
            sample[1] = sample[1][:lens]
        ret_datas.append(sample[:2])
    return(ret_datas)


def build_model(num_classes=9):
    model = Sequential()
    model.add(Embedding(16000, 256, input_length=80))
    model.add(Bidirectional(LSTM(128,return_sequences=True),merge_mode="concat"))
    model.add(Bidirectional(LSTM(128,return_sequences=True),merge_mode="concat"))
    model.add(Dense(128, activation='relu'))
    model.add(Dense(num_classes, activation='softmax'))
    return(model)


Train = True

if __name__ == "__main__":
    ds_rd = NerDatasetReader()
    dataset = ds_rd.read("./conll2003_v2/")
    # print(dataset["test"])
    w_dict, n_dict = get_dicts(dataset["train"])
    # print(w_dict)
    # print(n_dict)
    data_num = {}
    # ([token對應的詞典索引數字...], [標籤對應的索引數字...], [句子tokens長度])
    data_num["train"] = w2num(dataset["train"], w_dict, n_dict)
    # print(data_num["train"][:10])
    # 我們輸出句子長度的統計,發現最大值113,最小值爲1,爲了方便統一訓練,我們歸一化長度爲80
    w_lens = [data[-1] for data in data_num["train"]]
    # print(max(w_lens), min(w_lens))
    # 句子長度歸一化操作,這裏採用padding爲0,就是當做“UNK”與“O”來用,其實也可以使用Mask方法等
    data_norm = {}
    data_norm["train"] = len_norm(data_num["train"])
    print(data_norm["train"][:10])
    '''
    model = build_model()
    print(model.summary())
    opt = Adam(0.001)
    model.compile(loss="sparse_categorical_crossentropy",optimizer=opt)

    train_data = np.array(data_norm["train"])
    train_x = train_data[:,0,:]
    train_y = train_data[:,1,:]


    if(Train):
        print(train_x.shape)

        

        model.fit(x=train_x,y=train_y,epochs=10,batch_size=200,verbose=1,validation_split=0.1)
        model.save("model.h5")
    else:
        model.load_weights("model.h5")
        pre_y = model.predict(train_x[:4])

        print(pre_y.shape)

        pre_y = np.argmax(pre_y,axis=-1)

        for i in range(0,len(train_y[0:4])):
            print("label "+str(i),train_y[i])
            print("pred "+str(i),pre_y[i])
    '''
View Code

3、模型搭建

模型結構:

Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 embedding (Embedding)       (None, 80, 256)           4096000   
                                                                 
 bidirectional (Bidirectiona  (None, 80, 256)          394240    
 l)                                                              
                                                                 
 bidirectional_1 (Bidirectio  (None, 80, 256)          394240    
 nal)                                                            
                                                                 
 dense (Dense)               (None, 80, 128)           32896     
                                                                 
 dense_1 (Dense)             (None, 80, 9)             1161      
                                                                 
=================================================================
Total params: 4,918,537
Trainable params: 4,918,537
Non-trainable params: 0
_________________________________________________________________

4、模型訓練

5、模型應用 - 文本實體識別

from tqdm import tqdm
from tensorflow.keras.layers import *
from tensorflow.keras.models import *
from tensorflow.keras.optimizers import *
import numpy as np


class NerDatasetReader:
    def read(self, data_path):
        data_parts = ['train', 'valid', 'test']
        extension = '.txt'
        dataset = {}
        for data_part in tqdm(data_parts):
            file_path = data_path + data_part + extension
            dataset[data_part] = self.read_file(str(file_path))
        return dataset
            
    def read_file(self, file_path):
        fileobj = open(file_path, 'r', encoding='utf-8')
        samples = []
        tokens = []
        tags = []

        for content in fileobj:
            content = content.strip('\n')
            if content == '-DOCSTART- -X- -X- O':
                pass
            elif content == '':
                if len(tokens) != 0:
                    # 每一句保存爲了兩個list,一個是單詞list,另一個是標註list
                    samples.append((tokens, tags))
                    tokens = []
                    tags = []
            else:
                contents = content.split(' ')
                tokens.append(contents[0])
                tags.append(contents[-1])
        return samples


def get_dicts(datas):
    w_all_dict, n_all_dict = {}, {}
    for sample in datas:
        for token, tag in zip(*sample):
            if token not in w_all_dict.keys():
                w_all_dict[token] = 1
            else:
                w_all_dict[token] += 1
            
            if tag not in n_all_dict.keys():
                n_all_dict[tag] = 1
            else:
                n_all_dict[tag] += 1

    sort_w_list = sorted(w_all_dict.items(),  key=lambda d: d[1], reverse=True)
    sort_n_list = sorted(n_all_dict.items(),  key=lambda d: d[1], reverse=True)
    # 保留前15999個常用的單詞,新增了一個"UNK"代表未知單詞。
    w_keys = [x for x, _ in sort_w_list[:15999]]
    w_keys.insert(0, "UNK")

    n_keys = [x for x, _ in sort_n_list]
    w_dict = {x:i for i, x in enumerate(w_keys)}
    n_dict = {x:i for i, x in enumerate(n_keys)}
    return(w_dict, n_dict)


def w2num(datas, w_dict, n_dict):
    ret_datas = []
    for sample in datas:
        num_w_list, num_n_list = [], []
        for token, tag in zip(*sample):
            if token not in w_dict.keys():
                token = "UNK"
            if tag not in n_dict:
                tag = "O"
            num_w_list.append(w_dict[token])
            num_n_list.append(n_dict[tag])
        ret_datas.append((num_w_list, num_n_list, len(num_n_list)))
    return(ret_datas)


def len_norm(data_num, lens=80):
    ret_datas = []
    for sample1 in list(data_num):
        sample = list(sample1)
        ls = sample[-1]
        # print(sample)
        while(ls < lens):
            sample[0].append(0)
            ls = len(sample[0])
            sample[1].append(0)
        else:
            sample[0] = sample[0][:lens]
            sample[1] = sample[1][:lens]
        ret_datas.append(sample[:2])
    return(ret_datas)


def build_model(num_classes=9):
    model = Sequential()
    model.add(Embedding(16000, 256, input_length=80))
    model.add(Bidirectional(LSTM(128, return_sequences=True), merge_mode="concat"))
    model.add(Bidirectional(LSTM(128, return_sequences=True), merge_mode="concat"))
    model.add(Dense(128, activation='relu'))
    model.add(Dense(num_classes, activation='softmax'))
    return(model)


def load_dataset(dataset_type="test"):
    ds_rd = NerDatasetReader()
    dataset = ds_rd.read("./conll2003_v2/")
    # print(dataset["test"])
    w_dict, n_dict = get_dicts(dataset[dataset_type])
    # print(w_dict)
    # print(n_dict)
    data_num = {}
    # ([token對應的詞典索引數字...], [標籤對應的索引數字...], [句子tokens長度])
    data_num[dataset_type] = w2num(dataset[dataset_type], w_dict, n_dict)
    # print(data_num[dataset_type][:10])
    # 我們輸出句子長度的統計,爲了方便統一訓練,我們歸一化長度爲80
    w_lens = [data[-1] for data in data_num[dataset_type]]
    # print(max(w_lens), min(w_lens))
    # 句子長度歸一化操作,這裏採用padding爲0,就是當做“UNK”與“O”來用,其實也可以使用Mask方法等
    data_norm = {}
    data_norm[dataset_type] = len_norm(data_num[dataset_type])
    # print(data_norm[dataset_type][:10])
    return data_norm[dataset_type]


def do_train():
    data_norm = load_dataset(dataset_type="train")
    # 模型搭建
    model = build_model()
    print(model.summary())
    opt = Adam(0.001)
    model.compile(loss="sparse_categorical_crossentropy", optimizer=opt)

    train_data = np.array(data_norm)
    train_x = train_data[:, 0, :]
    train_y = train_data[:, 1, :]
    print(train_x.shape)
    model.fit(
        x=train_x,
        y=train_y,
        epochs=10,
        batch_size=200,
        verbose=1,
        validation_split=0.1
    )
    model.save("model.h5")


def do_test():
    data_norm = load_dataset(dataset_type="test")
    # 模型搭建
    model = build_model()
    model.load_weights("model.h5")
    # 準備測試數據
    test_data = np.array(data_norm)
    test_x = test_data[:, 0, :]
    test_y = test_data[:, 1, :]
    print(test_x.shape)
    # 生成預測序列
    pre_y = model.predict(test_x[:100])
    print(pre_y.shape)
    pre_y = np.argmax(pre_y, axis=-1)
    overlap_rate_avg = 0
    for i in range(0, len(test_x[0:100])):
        print("label " + str(i), test_y[i])
        print("pred " + str(i), pre_y[i])
        overlap_rate = np.sum(test_y[i] == pre_y[i]) / len(test_y[i])
        overlap_rate_avg += overlap_rate
        print("準確率:", overlap_rate)
    print("平均準確率:", overlap_rate_avg / i)


if __name__ == "__main__":
    # do_train()
    do_test()
View Code

需要提醒讀者朋友注意的是,這裏僅僅演示了基於分詞後的語料進行命名實體識別的過程,從原始連續文本分詞語料這一步同樣是需要額外處理的。分詞是分詞,命名實體識別是命名實體識別。

參考鏈接:

https://github.com/xiaosongshine/NLP_NER_RNN_Keras/tree/master 
https://mp.weixin.qq.com/s/kMxdjdAZhgkAbbsExkdOOQ 

 

四、Bert embedding+BiLSTM_CRF,實現命名實體識別任務

這裏使用開源框架“Kashgari”完成該任務。

0x1:任務分析

序列標註任務是中文自然語言處理(NLP)領域在句子層面中的主要任務,在給定的文本序列上預測序列中需要作出標註的標籤。常見的子任務有:

  • 命名實體識別(NER)
  • Chunk 提取
  • 詞性標註(POS)等
在我們的NER任務重,需要預測的標籤序列就是輸入文本的實體標籤序列。 

0x2:搭建環境和數據準備

安裝相關依賴:
pip install kashgari==2.0.2
pip install tensorflow==2.2.0
pip install pandas==2.0.3
pip install keras==2.13.1
pip install tensorflow_addons==0.11.2
pip install torch==2.0.0
pip install rich==13.5.2

打印測試數據:

from kashgari.corpus import SMP2018ECDTCorpus
import kashgari
from kashgari.tasks.classification import BiLSTM_Model
import logging
logging.basicConfig(level='DEBUG')


if __name__ == "__main__":
    # Kashgari provides the basic intent-classification corpus for experiments. You could also use your corpus in any language for training.
    # Load build-in corpus.
    train_x, train_y = SMP2018ECDTCorpus.load_data('train')
    valid_x, valid_y = SMP2018ECDTCorpus.load_data('valid')
    test_x, test_y = SMP2018ECDTCorpus.load_data('test')
    print(train_x[0])
    print(train_y[0])

    # Or use your own corpus
    train_x = [['Hello', 'world'], ['Hello', 'Kashgari'], ['I', 'love', 'Beijing']]
    train_y = [['O', 'O'], ['O', 'B-PER'], ['O', 'B-LOC']]
    valid_x, valid_y = train_x, train_y
    test_x, test_x = train_x, train_y

    print(train_x[0])
    print(train_y[0])

0x3:其他可用於訓練的相關數據集

數據集簡要說明訪問地址
位置、組織、人… 這是來自GMB語料庫的摘錄,用於訓練分類器以預測命名實體,例如姓名,位置等。 kaggle
口語 NLPCC2018開放的任務型對話系統中的口語理解評測 NLPCC

參考鏈接:

https://www.kaggle.com/datasets/abhinavwalia95/entity-annotated-corpus?resource=download 
https://eliyar.biz/nlp_chinese_bert_ner/
https://github.com/BrikerMan/Kashgari 

 

五、GPT大語言模型,命名實體識別

0x1:LLM在NER任務上的研究進展 

命名實體識別(NER)是一種典型的序列標註任務,它將給定句子X = {x1, ..., xn} 中的每個單詞x分配一個實體類型y∈Y,其中Y表示實體標籤集合,n表示給定句子的長度。

大規模語言模型(LLMs)表現出令人印象深刻的在上下文學習方面的能力:只需少量特定任務的示例作爲演示,LLMs就能爲新的測試輸入生成結果。在上下文學習框架下,LLMs在各種自然語言處理任務中取得了有希望的結果,包括機器翻譯(MT),問答(QA)和命名實體抽取(NEE)。

儘管取得了進展,LLMs在NER任務上的性能仍遠低於監督基準,這主要是因爲如下幾個原因:

  • 1、第一個問題是NER和LLMs之間固有的差距:NER本質上是一個序列標註任務,模型需要爲句子中的每個標記分配一個實體類型標籤,而LLMs是在文本生成任務下形式化的。語義標註任務與文本生成模型之間的差距導致了將LLMs應用於解決NER任務時性能較差。
  • 2、LLMs在NER任務中的另一個大問題是幻覺問題,即LLMs傾向於過於自信地將NULL輸入標記爲實體。

爲了解決第一個問題,GPT-NER被提出,主要的解決方案思路如下:

GPT-NER將NER任務轉化爲一項可以被LLMs輕鬆適應的文本生成任務。具體而言,將在輸入文本“Columbus is a city”中找到位置實體的任務轉化爲生成文本序列“@@Columbus## is a city”,其中特殊標記@@##表示實體。我們發現,與其他形式化方法相比,所提出的策略可以顯著降低生成完全編碼輸入序列標籤信息的文本的難度,因爲模型只需標記實體的位置併爲其餘標記複製。實驗證明,所提出的策略顯著提高了性能。

爲了解決第二個問題,提出了一種自我驗證策略,該策略位於實體提取階段之後,促使LLMs自問提取的實體是否屬於已標記的實體標籤。自我驗證策略作爲一種調節功能來抵消LLMs過度自信的效果,在解決幻覺問題方面非常有效,導致性能顯著提升。

特別值得注意的是,GPT-NER在低資源和少樣本NER設置中表現出令人印象深刻的熟練程度:當訓練數據極爲稀缺時,GPT-NER比監督模型表現顯著更好。這說明了GPT-NER在實際的NER應用中的潛力,即使標記樣本的數量很少。

0x2:GPT-NER原理

GPT-NER使用大型語言模型來解決NER任務。GPT-NER遵循基於上下文學習的一般範例,可以分解爲三個步驟:

  • (1)提示構建:對於給定的輸入句子X,我們爲X構建一個提示(表示爲Prompt(X)),few-shot prompt如下圖所示。
  • (2)將構建的提示輸入到大型語言模型(LLM)中,以獲得生成的文本序列W = {w1, ..., wn}。對於這個文件序列W的格式有兩點約束要求:(i) 很容易被轉換成實體標籤序列;(ii) 很容易被LLM以高準確率的方式生成,即要考慮到LLM文本序列生成任務的難度。
  • (3)將文本序列W轉換爲實體標籤序列,從而獲得最終結果。

The example of the prompt of GPT-NER. Suppose that we need to recognize location entities for the given sentence: China says Taiwan spoils atmosphere for talks. The prompt consists of three parts:
  • (1) Task Description: It’s surrounded by a red rectangle, and instructs the GPT-3 model that the current task is to recognize Location entities using linguistic knowledge.
  • (2) Few-shot Demonstrations: It’s surrounded by a yellow rectangle giving the GPT-3 model few-shot examples for reference.
  • (3) Input Sentence: It’s surrounded by a blue rectangle indicating the input sentence, and the output of the GPT-3 model is colored green.

儘管基於上下文學習範例是直觀的,但NER任務並不容易適應它,因爲NER任務本質上是一個序列標註任務,而不是生成任務,上述方法存在如下幾點問題:

  • 採用few-shot prompt的方法,每次次只能提取一種實體,因爲要提取出所有的實體,必須遍歷實體列表,但是LLM對提示的長度有一個硬性的限制(例如GPT-3的4096個tokens)。 

1、怎麼在few-shot prompt中提供實例樣本?

few-shot prompt提示構建這一步的目的主要有兩個:

  • 規範LLM輸出的格式
  • 給LLM提升生成輸出的輔助證據

格式問題容易解決,這裏主要需要解決的是實例樣本質量的問題,我們需要按照一定的策略給LLM提供合適的實例樣本,以提高LLM生成輸出的質量。

1)Random Retrieval

最直接的策略是從訓練集中隨機選擇k個示例。明顯的缺點是:無法保證檢索到的示例在語義上與輸入接近。

2)kNN-based Retrieval 

我們可以從訓練集中檢索輸入序列的k個最近鄰(kNN):我們首先計算所有訓練示例的向量化表示,然後根據這些向量化表示獲取輸入測試序列的k個最近鄰,並用這k個最近鄰作爲few-shot prompt中的實例樣本。

向量化表示有兩種可選的方法,

  • kNN based on Sentence-level Representations

要在訓練集中找到kNN示例,一種直觀的方法是使用文本相似性模型,例如SimCSE:我們首先爲訓練示例和輸入序列獲取句子級表示,然後使用餘弦相似度找到kNN。

基於句子級表示的kNN的缺點是顯而易見的:NER是一個基於標記級別的任務,更關注局部證據,而不是句子級任務。很容易遇到一個問題,即檢索到的句子(例如,他是一名士兵)與輸入(例如,John是一名士兵)在句子級別上語義上相似,但對於標記輸入不提供任何參考幫助。在上面的例子中,檢索到的句子不包含NER,因此無法對標記輸入提供證據。

  • Entity-level Embedding

爲了解決上述問題,我們需要基於標記級別的表示而不是句子級別的表示來檢索kNN示例。

我們首先使用經過微調的NER標記模型從所有訓練示例的所有標記中提取實體級表示作爲數據存儲。對於長度爲N的給定輸入序列,我們首先迭代遍歷序列中的所有標記,爲每個標記找到kNN,得到K×N個檢索到的標記。接下來,我們從K×N個檢索到的標記中選擇前k個標記,並使用它們關聯的句子作爲示範。

整個過程如下圖所示,

假設我們需要爲輸入句子“Obama lives in Washington”檢索少樣本演示,其中定義了LOC實體。

  • 步驟1:數據存儲構建:我們首先使用經過微調的NER模型提取訓練集中每個句子的實體,並將它們形成(鍵,值)對,其中鍵是提取的實體,值是對應的句子。然後,我們將所有形成的(鍵,值)對連接起來構建數據存儲。
  • 步驟2:表示提取:首先,利用經過微調的NER模型將輸入句子嵌入爲一系列高維向量。然後,根據softmax層將嵌入的高維向量分類爲標籤,其中“Obama”和“Washington”是兩個識別的實體。
  • 步驟3:kNN搜索:使用提取的LOC實體“Washington”的嵌入作爲查詢,在數據存儲中找到k個最近鄰居,並將檢索到的句子視爲k個少樣本演示。
Prompt:
I am an excellent linguist. The task is to labellocation entities in the given sentence.
Below are some examples.
Input:Columbus is a city
Output:@@Columbus## is a city
Input:Rare Hendrix song sells for $17
Output:

GPT-3 Output:
Rare @@Hendrix## song sells for $17

在GPT-3中,“Hendrix”被識別爲一個位置實體,這顯然是不正確的。

爲了解決這個問題,我們提出了自我驗證策略。給定LLMs提取的實體,我們要求LLM進一步驗證提取的實體是否正確,回答是或否。

我們構建了自我驗證的提示,如下圖所示。

以提取位置實體爲例,提示從任務描述開始:“任務是驗證給定句子中的單詞是否是從位置實體中提取出來的”。

按照上述生成過程同樣的樣例選擇策略,我們需要少量示範來提高自我驗證器的準確性。如上圖中的黃色矩形所示,每個示範包含三行:

  • (1) “輸入句子:Only France and Britain backed Fischler's proposal”。
  • (2) “在輸入句子中,單詞"France"是一個位置實體嗎?請用是或否回答”。
  • (3) 是。

在少量示範設置中,我們將多個示範放入提示中。示範後面是測試示例,然後將其輸入LLM以獲取輸出。

我們進行實驗來估計示例數量的影響。實驗是在100個樣本的CoNLL 2003數據集上進行的。結果如下圖所示。

Comparisons by varying k-shot demonstrations.

我們可以觀察到,隨着k的增加,所有三種基於LLM的結果都在不斷上升。當我們接近4096個示例的令牌限制時,結果仍未達到平穩狀態。這意味着如果允許更多的示例,性能仍將提高。

有一個有趣的現象觀察到,當示例數量較少時,即k = 2、4時,基於kNN的策略表現不如隨機檢索策略。一種可能的解釋如下:基於kNN的檢索傾向於選擇與輸入句子非常相似的演示。因此,如果輸入句子不包含任何實體,檢索到的示例很可能也不包含實體。在這種情況下,示例不包含我們希望強制執行的輸出格式信息,導致LLM輸出任意格式。

下面是一個例子: 當示例數量較少且GPT需要識別某種特定類型的實體(例如位置),但少量的示例句子都沒有命名實體識別時,GPT會感到困惑,並以自己的格式輸出,如下例所示:

Prompt:
I am an excellent linguist. The task is to label
organization entities in the given sentence. Below
are some examples.
Input:Korean pro-soccer games
Output:Korean pro-soccer games
Input:Australia defend the Ashes
Output:Australia defend the Ashes
Input:Japan get lucky win
Output:

GPT-3 Output:
Japan [Organization Entity] get lucky win 

參考鏈接:

https://arxiv.org/pdf/2304.10428v1.pdf
https://zhuanlan.zhihu.com/p/623739638
https://github.com/ShuheWang1998/GPT-NER 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章