命名實體識別在越來越多的場景下被應用,如自動問答、知識圖譜等。非結構化的文本內容有很多豐富的信息,但找到相關的知識始終是一個具有挑戰性的任務,命名實體識別也不例外。
前面我們用隱馬爾可夫模型(HMM)自己嘗試訓練過一個分詞器,其實 HMM 也可以用來訓練命名實體識別器,但在本文,我們講另外一個算法——條件隨機場(CRF),來訓練一個命名實體識別器。
淺析條件隨機場(CRF)條件隨機場(Conditional Random Fields,簡稱 CRF)是給定一組輸入序列條件下另一組輸出序列的條件概率分布模型,在自然語言處理中得到了廣泛應用。
首先,我們來看看什麼是隨機場。「隨機場」的名字取的很玄乎,其實理解起來不難。隨機場是由若干個位置組成的整體,當按照某種分布給每一個位置隨機賦予一個值之後,其全體就叫做隨機場。
還是舉詞性標註的例子。假如我們有一個十個詞形成的句子需要做詞性標註。這十個詞每個詞的詞性可以在我們已知的詞性集合(名詞,動詞……)中去選擇。當我們為每個詞選擇完詞性後,這就形成了一個隨機場。
了解了隨機場,我們再來看看馬爾科夫隨機場。馬爾科夫隨機場是隨機場的特例,它假設隨機場中某一個位置的賦值僅僅與和它相鄰的位置的賦值有關,和與其不相鄰的位置的賦值無關。
繼續舉十個詞的句子詞性標註的例子。如果我們假設所有詞的詞性只和它相鄰的詞的詞性有關時,這個隨機場就特化成一個馬爾科夫隨機場。比如第三個詞的詞性除了與自己本身的位置有關外,還只與第二個詞和第四個詞的詞性有關。
理解了馬爾科夫隨機場,再理解 CRF 就容易了。CRF 是馬爾科夫隨機場的特例,它假設馬爾科夫隨機場中只有 X 和 Y 兩種變量,X 一般是給定的,而 Y 一般是在給定 X 的條件下我們的輸出。這樣馬爾科夫隨機場就特化成了條件隨機場。
在我們十個詞的句子詞性標註的例子中,X 是詞,Y 是詞性。因此,如果我們假設它是一個馬爾科夫隨機場,那麼它也就是一個 CRF。
對於 CRF,我們給出準確的數學語言描述:設 X 與 Y 是隨機變量,P(Y|X) 是給定 X 時 Y 的條件概率分布,若隨機變量 Y 構成的是一個馬爾科夫隨機場,則稱條件概率分布 P(Y|X) 是條件隨機場。
基於 CRF 的中文命名實體識別模型實現在常規的命名實體識別中,通用場景下最常提取的是時間、人物、地點及組織機構名,因此本模型也將提取以上四種實體。
1.開發環境。
本次開發所選用的環境為:
Sklearn_crfsuite
Python 3.6
Jupyter Notebook
2.數據預處理。
本模型使用人民日報1998年標註數據,進行預處理。語料庫詞性標記中,對應的實體詞依次為 t、nr、ns、nt。對語料需要做以下處理:
將語料全形字符統一轉為半角;
合併語料庫分開標註的姓和名,例如:溫/nr 家寶/nr;
合併語料庫中括號中的大粒度詞,例如:[國家/n 環保局/n]nt;
合併語料庫分開標註的時間,例如:(/w 一九九七年/t 十二月/t 三十一日/t )/w。
首先引入需要用到的庫:
import re import sklearn_crfsuite from sklearn_crfsuite import metrics from sklearn.externals import joblib
數據預處理,定義 CorpusProcess 類,我們還是先給出類實現框架:
class CorpusProcess(object): def __init__(self): """初始化""" pass def read_corpus_from_file(self, file_path): """讀取語料""" pass def write_corpus_to_file(self, data, file_path): """寫語料""" pass def q_to_b(self,q_str): """全形轉半角""" pass def b_to_q(self,b_str): """半角轉全形""" pass def pre_process(self): """語料預處理 """ pass def process_k(self, words): """處理大粒度分詞,合併語料庫中括號中的大粒度分詞,類似:[國家/n 環保局/n]nt """ pass def process_nr(self, words): """ 處理姓名,合併語料庫分開標註的姓和名,類似:溫/nr 家寶/nr""" pass def process_t(self, words): """處理時間,合併語料庫分開標註的時間詞,類似: (/w 一九九七年/t 十二月/t 三十一日/t )/w """ pass def pos_to_tag(self, p): """由詞性提取標籤""" pass def tag_perform(self, tag, index): """標籤使用BIO模式""" pass def pos_perform(self, pos): """去除詞性攜帶的標籤先驗知識""" pass def initialize(self): """初始化 """ pass def init_sequence(self, words_list): """初始化字序列、詞性序列、標記序列 """ pass def extract_feature(self, word_grams): """特徵選取""" pass def segment_by_window(self, words_list=None, window=3): """窗口切分""" pass def generator(self): """訓練數據""" pass
由於整個代碼實現過程較長,我這裡給出重點步驟,最後會在 Github 上連同語料代碼一同給出,下面是關鍵過程實現。
對語料中的句子、詞性,實體分類標記進行區分。標籤採用「BIO」體系,即實體的第一個字為 B_*,其餘字為 I_*,非實體字統一標記為 O。大部分情況下,標籤體系越複雜,準確度也越高,但這裡採用簡單的 BIO 體系也能達到相當不錯的效果。這裡模型採用 tri-gram 形式,所以在字符列中,要在句子前後加上佔位符。
def init_sequence(self, words_list): """初始化字序列、詞性序列、標記序列 """ words_seq = [[word.split(u'/')[0] for word in words] for words in words_list] pos_seq = [[word.split(u'/')[1] for word in words] for words in words_list] tag_seq = [[self.pos_to_tag(p) for p in pos] for pos in pos_seq] self.pos_seq = [[[pos_seq[index][i] for _ in range(len(words_seq[index][i]))] for i in range(len(pos_seq[index]))] for index in range(len(pos_seq))] self.tag_seq = [[[self.tag_perform(tag_seq[index][i], w) for w in range(len(words_seq[index][i]))] for i in range(len(tag_seq[index]))] for index in range(len(tag_seq))] self.pos_seq = [[u'un']+[self.pos_perform(p) for pos in pos_seq for p in pos]+[u'un'] for pos_seq in self.pos_seq] self.tag_seq = [[t for tag in tag_seq for t in tag] for tag_seq in self.tag_seq] self.word_seq = [[u'<BOS>']+[w for word in word_seq for w in word]+[u'<EOS>'] for word_seq in words_seq]
處理好語料之後,緊接著進行模型定義和訓練,定義 CRF_NER 類,我們還是採用先給出類實現框架,再具體講解其實現:
class CRF_NER(object): def __init__(self): """初始化參數""" pass def initialize_model(self): """初始化""" pass def train(self): """訓練""" pass def predict(self, sentence): """預測""" pass def load_model(self): """加載模型 """ pass def save_model(self): """保存模型""" pass
在 CRF_NER 類中,分別完成了語料預處理和模型訓練、保存、預測功能,具體實現如下。
第一步,init 函數實現了模型參數定義和 CorpusProcess 的實例化和語料預處理:
def __init__(self): """初始化參數""" self.algorithm = "lbfgs" self.c1 ="0.1" self.c2 = "0.1" self.max_iterations = 100 self.model_path = dir + "model.pkl" self.corpus = CorpusProcess() self.corpus.pre_process() self.corpus.initialize() self.model = None
第二步,給出模型定義,了解 sklearn_crfsuite.CRF 詳情可查該文檔。
def initialize_model(self): """初始化""" algorithm = self.algorithm c1 = float(self.c1) c2 = float(self.c2) max_iterations = int(self.max_iterations) self.model = sklearn_crfsuite.CRF(algorithm=algorithm, c1=c1, c2=c2, max_iterations=max_iterations, all_possible_transitions=True)
第三步,模型訓練和保存,分為訓練集和測試集:
def train(self): """訓練""" self.initialize_model() x, y = self.corpus.generator() x_train, y_train = x[500:], y[500:] x_test, y_test = x[:500], y[:500] self.model.fit(x_train, y_train) labels = list(self.model.classes_) labels.remove('O') y_predict = self.model.predict(x_test) metrics.flat_f1_score(y_test, y_predict, average='weighted', labels=labels) sorted_labels = sorted(labels, key=lambda name: (name[1:], name[0])) print(metrics.flat_classification_report(y_test, y_predict, labels=sorted_labels, digits=3)) self.save_model()
第四至第六步中 predict、load_model、save_model 方法的實現,大家可以在文末給出的地址中查看源碼,這裡就不堆代碼了。
最後,我們來看看模型訓練和預測的過程和結果:
ner = CRF_NER() model = ner.train()
經過模型訓練,得到的準確率和召回率如下:
進行模型預測,其結果還不錯,如下:
基於 CRF 的中文命名實體識別模型實現先講到這兒,項目源碼和涉及到的語料,大家可以到:Github 上查看。
總結本文淺析了條件隨機場,並使用 sklearn_crfsuite.CRF 模型,對人民日報1998年標註數據進行了模型訓練和預測,以幫助大家加強對條件隨機場的理解。
參考資料及推薦閱讀
條件隨機場(CRF)
條件隨機場CRF(一)從隨機場到線性鏈條件隨機場
命名實體:基於 CRF 的中文命名實體識別模型
條件隨機場(CRF)理論及應用