基于深度學(xué)習(xí)的命名實體識別方法
命名實體識別介紹
和分詞和詞性標(biāo)注一樣然眼,命名實體識別 也是自然語言處理里面一項非彻径校基礎(chǔ)但又非常重要的技術(shù)。命名實體識別(Named Entity Recongition)蔼紧,簡稱 NER嚷掠。命名實體識別指的是指從文本中識別出命名性質(zhì)的稱項,為關(guān)系抽取等下游任務(wù)做鋪墊。
狹義上來說尚蝌,命名實體識別指的是識別出人名、地名和組織機構(gòu)名這三類命名實體充尉。因為像時間飘言、貨幣名稱等會構(gòu)成規(guī)律明顯的實體類型,因此可以直接用正則表達式等方式來進行識別驼侠。一個命名實體識別的例子如下:
當(dāng)然姿鸿,在特定的領(lǐng)域中,會相應(yīng)地定義領(lǐng)域內(nèi)的各種實體類型泪电。例如化學(xué)領(lǐng)域的各種化學(xué)物質(zhì)名稱【氯化鈉】【二氧化鉀】【聚酸銨】等般妙。
命名實體識別方法
同中文分詞和詞性標(biāo)注一樣纪铺,命名實體識別也是一種典型的序列標(biāo)注任務(wù)相速。命名實體識別可以基于詞來進行識別,也可以基于字來進行識別鲜锚。而在本次實驗中突诬,我們主要講解基于字的識別方法,其類似于中文分詞芜繁,也是對一個句子中的每個字進行一個標(biāo)記旺隙。
假設(shè)現(xiàn)在的任務(wù)是識別出文本中的人名、地名骏令、機構(gòu)名蔬捷。則可以定義以下規(guī)則來對句子進行標(biāo)記。
B-PER:人名的第一個字榔袋。
I-PER:人名的后幾個字周拐。
B-ORG:機構(gòu)名的第一個字。
I-ORG:機構(gòu)名的后幾個字凰兑。
B-LOC:地名的第一個字妥粟。
I-LOC:地名的后幾個字。
O:非實體名稱的其他字吏够。
通過定義上面所述的規(guī)則勾给,就可以把一個句子轉(zhuǎn)化成為標(biāo)注的形式。例如下圖所示:
而現(xiàn)在的命名實體識別的任務(wù)就是輸入一個句子锅知,然后輸出該句子每個字相應(yīng)的標(biāo)注播急,然后再通過規(guī)則轉(zhuǎn)換得到實體名稱。
目前售睹,命名實體識別的方法主要以有監(jiān)督學(xué)習(xí)方法為主旅择,在工業(yè)應(yīng)用上亦是如此。例如美國斯坦福大學(xué)開發(fā)的命名實體識別工具 Stanford NER 的基本模型為條件隨機場侣姆。當(dāng)然生真,條件隨機場也是目前最有效的命名實體識別方法之一沉噩。
條件隨機場在上一個實驗中,我們已經(jīng)進行詳細的介紹柱蟀。因此本節(jié)將重點介紹近年來在學(xué)術(shù)界比較受歡迎的方法: 長短時記憶網(wǎng)絡(luò)+條件隨機場 的方法川蒙。
循環(huán)神經(jīng)網(wǎng)絡(luò)簡介
雙向長短時記憶網(wǎng)絡(luò)+條件隨機場的方法一般簡稱 BiLSTM+CRF。其主要由雙向長短時記憶網(wǎng)絡(luò) (BiLSTM) 和條件隨機場 (CRF) 構(gòu)成长已。要想了解長短時記憶網(wǎng)絡(luò)畜眨,先從簡單的循環(huán)神經(jīng)網(wǎng)絡(luò)開始講解。
單向循環(huán)神經(jīng)網(wǎng)絡(luò)
循環(huán)神經(jīng)網(wǎng)絡(luò)术瓮,簡稱 RNN (Recurrent Neural Network)康聂,主要是用來解決含有上下文關(guān)系的輸入數(shù)據(jù)。也就是說胞四,循環(huán)神經(jīng)網(wǎng)絡(luò)能夠提取時間序列每個時刻之間相關(guān)的特征恬汁。
你可能會問,為什么要提取上下文的特征呢辜伟?這其實很簡單氓侧,假設(shè)現(xiàn)在有一個任務(wù):輸入一個句子的前大部分的字,預(yù)測出一個句子的最后兩個字导狡。例如這句話【白宮是美國總統(tǒng)居住的地方约巷,而特朗普是美國總統(tǒng),所以特朗普居住在__ 】旱捧。
對于人來說独郎,很簡單答案肯定是【白宮】。現(xiàn)在想一想你是怎么得出這個答案的枚赡。按照人類的思考邏輯氓癌,答案肯定是和前面的句子相關(guān)的。但計算機并沒有人類的思考邏輯标锄,因此要定義一套算法來模仿這種邏輯顽铸,即答案要依賴于前文的信息。而循環(huán)神經(jīng)網(wǎng)絡(luò)正是這樣的一套算法料皇。
循環(huán)神經(jīng)網(wǎng)絡(luò)的網(wǎng)絡(luò)結(jié)構(gòu)圖如下圖所示:
在上式中谓松,f(?) 表示激活函數(shù),其作用是過濾掉無用的信息践剂,保留有用的信息鬼譬。可以選擇 Relu 函數(shù)逊脯、Tanh 函數(shù)优质、Sigmoid 函數(shù)等。一般選擇 Tanh 函數(shù)作為激活函數(shù),其表達式如下:
為了更好的理解巩螃,現(xiàn)在將 Tanh 函數(shù)的圖像繪制出來演怎。
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline
x = np.linspace(-10, 10, 100)
y = (np.exp(x)-np.exp(-x))/(np.exp(x)+np.exp(-x))
# 這里主要設(shè)置坐標(biāo)軸
ax = plt.gca() # 得到圖像的Axes對象
ax.spines['right'].set_color('none') # 將圖像右邊的軸設(shè)為透明
ax.spines['top'].set_color('none') # 將圖像上面的軸設(shè)為透明
ax.xaxis.set_ticks_position('bottom') # 將x軸刻度設(shè)在下面的坐標(biāo)軸上
ax.yaxis.set_ticks_position('left') # 將y軸刻度設(shè)在左邊的坐標(biāo)軸上
ax.spines['bottom'].set_position(('data', 0)) # 將兩個坐標(biāo)軸的位置設(shè)在數(shù)據(jù)點原點
ax.spines['left'].set_position(('data', 0))
# 畫出圖形
plt.plot(x, y)
從上圖可以看到 Tanh 函數(shù)的將輸入置于 -1 到 1 區(qū)間。這可以增加輸入與輸出的非線性關(guān)系避乏,使模型能夠擁有更強的非線性擬合能力爷耀。
同理,g(?) 表示激活函數(shù)拍皮〈醵#可以選擇 Softmax 函數(shù)等。
雙向循環(huán)神經(jīng)網(wǎng)絡(luò)
前面中所講的網(wǎng)絡(luò)都是單向的铆帽,也就是后面時刻的輸出值可以依賴于前面時刻的輸入值的歷史信息咆耿,反過來則不行。也就是前面時刻的輸出值不能依賴于后面時刻的輸入值爹橱。如下圖所示:
長短時記憶網(wǎng)絡(luò)簡介
長短時記憶網(wǎng)絡(luò)萨螺,也簡稱為 LSTM( Long Short Term Memory)。其是簡單循環(huán)神經(jīng)網(wǎng)絡(luò)的一種重要變體宅荤。上文講解的循環(huán)神經(jīng)網(wǎng)絡(luò)主要存在一個缺點屑迂,那就是不能記憶長期的特征浸策。例如當(dāng)輸入的句子太長時冯键,標(biāo)準(zhǔn)的循環(huán)神經(jīng)網(wǎng)絡(luò)并不能很好的把一些重要的歷史信息一直傳遞下去。例如下圖:
為了解決這一問題庸汗,Hochreiter 等人于上個世紀(jì) 90 年代提出了長短時記憶網(wǎng)絡(luò)惫确。后來經(jīng)過其他學(xué)者的改良并得到廣泛的應(yīng)用。LSTM 每個記憶單元(也稱為細胞)的結(jié)構(gòu)如下圖所示蚯舱。
LSTM 的整體架構(gòu)與標(biāo)準(zhǔn)的循環(huán)神經(jīng)網(wǎng)絡(luò)一樣改化。與之不同的是,LSTM 每個單元的內(nèi)部結(jié)構(gòu)更復(fù)雜枉昏,其主要是通過定義三個門來實現(xiàn)對長期歷史信息的記憶陈肛。
現(xiàn)在來逐步解析其內(nèi)部結(jié)構(gòu)原理。
遺忘門
遺忘門主要是針對上一個節(jié)點傳來的輸入信息進行選擇性的忘記兄裂。簡單來說就是忘記不重要的信息句旱,記住重要的信息。
為了便于理解晰奖,這里將 Sigmoid 函數(shù)圖繪制出來谈撒。
x = np.linspace(-10, 10, 100)
y = 1/(1+np.exp(-x))
ax = plt.gca() # 得到圖像的Axes對象
ax.spines['right'].set_color('none') # 將圖像右邊的軸設(shè)為透明
ax.spines['top'].set_color('none') # 將圖像上面的軸設(shè)為透明
ax.xaxis.set_ticks_position('bottom') # 將x軸刻度設(shè)在下面的坐標(biāo)軸上
ax.yaxis.set_ticks_position('left') # 將y軸刻度設(shè)在左邊的坐標(biāo)軸上
ax.spines['bottom'].set_position(('data', 0)) # 將兩個坐標(biāo)軸的位置設(shè)在數(shù)據(jù)點原點
ax.spines['left'].set_position(('data', 0))
plt.plot(x, y)
輸入門
輸出門
以上就是 LSTM 一個單元的內(nèi)部計算過程。 LSTM 是目前最經(jīng)典的循環(huán)神經(jīng)網(wǎng)絡(luò)之一匾南。因為其通過對每個單元內(nèi)部進行巧妙設(shè)計啃匿,從而有效避免了信息斷流的問題。
雙向長短時記憶網(wǎng)絡(luò)
前面主要講解了循環(huán)神經(jīng)網(wǎng)絡(luò)的理論。現(xiàn)在就動手來實現(xiàn)一個 BiLSTM 來進行命名實體識別溯乒。本次實驗所構(gòu)建的網(wǎng)絡(luò)結(jié)構(gòu)如下圖所示:
上圖看起來可能稍顯復(fù)雜夹厌,但仔細看其實很簡單。在上圖中裆悄,我們假設(shè)輸入的句子為【朝鮮最高領(lǐng)導(dǎo)人金正恩】尊流,其對應(yīng)的標(biāo)簽為【B-ORG I-ORG O O O O O B-PER I-PER I-PER】。
因為神經(jīng)網(wǎng)絡(luò)要求輸入的是數(shù)值向量灯帮,所以將每個字都轉(zhuǎn)成對應(yīng)的向量形式崖技。在這里,我們將每個字用一個長度為1×20 的向量進行表示钟哥。
轉(zhuǎn)換成為向量之后就直接送入到 BiLSTM 網(wǎng)絡(luò)迎献。在 BiLSTM 網(wǎng)絡(luò)中,設(shè)置正向網(wǎng)絡(luò)和逆向網(wǎng)絡(luò)的輸出為24 腻贰。合并兩者得到1×48 的向量吁恍。然后再乘以一個矩陣形狀為48×7 的矩陣得到1×7 的向量。該向量的每個位置負責(zé)預(yù)測一個標(biāo)簽的值播演。
你可能會有疑問冀瓦,為什么最終的輸出要設(shè)置成為1×7 形式的向量呢?其實這主要是根據(jù)我們的數(shù)據(jù)集標(biāo)簽種類的數(shù)量來決定的写烤。在本次實驗中翼闽,使用的是人民日報上的標(biāo)注數(shù)據(jù)集。讓我們先下載數(shù)據(jù)。
!wget -nc "https://labfile.oss.aliyuncs.com/courses/1329/ner_data.txt"
下載完成之后,我們現(xiàn)在就來讀取數(shù)據(jù)集庶喜。
f = open("ner_data.txt", "r", encoding='utf8')
data = f.readlines()
data[:10]
可以看到技羔,數(shù)據(jù)中的每一行都是由一個字和對應(yīng)的標(biāo)簽,【\n】表示換行符。當(dāng)一行沒有字或?qū)?yīng)的標(biāo)注時,則表示前面的文字構(gòu)成一句話,類似于句號的意思撑毛。我們現(xiàn)在先對其進行預(yù)處理。
sentences = []
labels = []
seq_data = []
seq_label = []
for char in data:
char = char.strip()
lst = char.split(" ")
if len(lst) == 2:
seq_data.append(lst[0])
seq_label.append(lst[1])
else: # 語料中是空行分隔句子
sent = " ".join(seq_data)
seq_data.clear()
sentences.append(sent)
label = " ".join(seq_label)
seq_label.clear()
labels.append(label)
len(sentences), len(labels)
通過處理之后唧领,得到 50658 個句子藻雌,現(xiàn)在打印出一個句子和對應(yīng)的標(biāo)簽。
print(sentences[3])
print(labels[3])
通過上面的預(yù)處理疹吃,我們得到的每個句子還是字符串的形式蹦疑,現(xiàn)在將每個字拆分開。
data_X = []
data_y = []
sentence_length = []
for i in range(len(sentences)):
sentence_length.append(len(sentences[i].split()))
data_X.append(sentences[i].split())
data_y.append(labels[i].split())
data_X[0][:10]
為了便于處理萨驶,將每個字轉(zhuǎn)換成為一個對應(yīng)的數(shù)字歉摧。因此這里先構(gòu)建出轉(zhuǎn)換的字典。
tag_to_ix = {} # 漢字轉(zhuǎn)換成為數(shù)字的字典
word_to_ix = {"<UNK>": 0} # 標(biāo)簽轉(zhuǎn)換成為數(shù)字的字典,并且生字給id=0
for sentence in data_X:
for word in sentence:
if word not in word_to_ix:
word_to_ix[word] = len(word_to_ix)
for tags in data_y:
for tag in tags:
if tag not in tag_to_ix:
tag_to_ix[tag] = len(tag_to_ix)
讓我們打印出標(biāo)簽轉(zhuǎn)化字典叁温。
tag_to_ix
此時再悼,標(biāo)簽的總類別數(shù)為 7 個。因此對應(yīng)于模型最終輸出的長度為 7 的向量膝但。
前面我們只是構(gòu)建出了每個字對應(yīng)于數(shù)字的一個字典〕寰牛現(xiàn)在定義將字轉(zhuǎn)換成為數(shù)字的函數(shù)。需要注意的是跟束,因為每個句子的長度不一樣莺奸,所以統(tǒng)一設(shè)置句子的長度為 100,如果一個句子的長度未達到 100冀宴,則在其后面添加 0灭贷。
# 這里增加未登錄字處理
def word2indx(seq, to_ix):
idxs = [to_ix[w] if w in to_ix else to_ix["<UNK>"] for w in seq]
for i in range(100-len(idxs)): # 這里做 padding
idxs.append(0)
return idxs
def tag2indx(tags, to_ix):
idxs = [to_ix[t] for t in tags]
for i in range(100-len(idxs)): # 這里做 padding
idxs.append(0)
return idxs
劃分訓(xùn)練集和測試集。
spl = int(len(data_X)*0.8)
train_X_char = data_X[:spl]
train_y_char = data_y[:spl]
train_sentence_length = sentence_length[:spl]
test_X_char = data_X[spl:]
test_y_char = data_y[spl:]
test_sentence_length = sentence_length[spl:]
轉(zhuǎn)換所有數(shù)據(jù)略贮,并將數(shù)值轉(zhuǎn)化成為 NumPy 格式甚疟,以便后續(xù)的處理。
train_X = [word2indx(sent, word_to_ix) for sent in train_X_char]
train_y = [tag2indx(tag, tag_to_ix) for tag in train_y_char]
test_X = [word2indx(sent, word_to_ix) for sent in test_X_char]
test_y = [tag2indx(tag, tag_to_ix) for tag in test_y_char]
train_X = np.array(train_X)
train_y = np.array(train_y)
test_X = np.array(test_X)
test_y = np.array(test_y)
test_sentence_length = np.array(test_sentence_length)
train_sentence_length = np.array(train_sentence_length)
# 打印一份數(shù)據(jù)逃延,查看轉(zhuǎn)化結(jié)果
train_X[0]
定義模型的一些超參數(shù)览妖,這里定義為全局變量。需要注意的是揽祥,這里為了便于模型訓(xùn)練讽膏,詞向量、循環(huán)單元等超參數(shù)都設(shè)置得比較小盔然。如果自己線下跑桅打,可以適當(dāng)?shù)脑龃筮@些值是嗜。
import tensorflow as tf
batch_size = 8 # 每次訓(xùn)練使用的樣本數(shù)
max_sent_length = 100 # 每個句子 padding 后的長度愈案,
word_vec_length = 20 # 詞向量的長度
num_tags = 7 # 標(biāo)簽總數(shù)
cell_unit = 24 # LSTM 單元數(shù)
learning_rate = 0.001 # 學(xué)習(xí)率
word_length = len(word_to_ix) # 總詞數(shù)
按照上面所展示的 BiLSTM 模型圖,構(gòu)建出 BiLSTM 模型鹅搪。這里我們主要使用 Google 開源的 TensorFlow 深度學(xué)習(xí)框架來完成站绪。
這里需要注意的是,前文我們講到 LSTM 每個時刻的輸入xt 都是一個向量丽柿。而前面我們完成的數(shù)據(jù)預(yù)處理得到的是每個字對應(yīng)于一個數(shù)字恢准。因此要將數(shù)字轉(zhuǎn)換成為向量。
首先自定義我們需要的 BiLSTM 層:
class BiLSTM(tf.keras.layers.Layer): # 通過繼承 tf.keras.layers.Layer 類來自定義層
def __init__(self, cell_unit):
super().__init__()
# 正向的 LSTM 層
fwd_lstm = tf.keras.layers.LSTM(cell_unit, return_sequences = True, go_backwards= False)
# 反向的 LSTM 層
bwd_lstm = tf.keras.layers.LSTM(cell_unit, return_sequences = True, go_backwards = True)
# 使用 tf.keras.layers.Bidirectional 將兩個 LSTM 層合并為雙向長短時記憶網(wǎng)絡(luò)
self.bilstm = tf.keras.layers.Bidirectional(merge_mode = "concat", layer = fwd_lstm, backward_layer = bwd_lstm)
def call(self, inputs, training):
outputs = self.bilstm(inputs, training = training)
return outputs
下面就可以定義我們的模型了甫题,包含一個用于向量化的 Embedding 層馁筐,一個 BiLSTM 層,以及一個全連接層坠非。
class MyModel(tf.keras.Model):
def __init__(self, cell_unit, word_length, word_vec_length):
super().__init__()
self.embedding_layer = tf.keras.layers.Embedding(word_length, word_vec_length, embeddings_initializer = "uniform")
self.bilstm_layer = BiLSTM(cell_unit)
self.dense_layer = tf.keras.layers.Dense(num_tags, activation = tf.nn.softmax)
def call(self, input_x, training):
# Embedding 層敏沉,輸入形狀為【batch_size, max_sent_length】,輸出形狀為【batch_size, max_sent_length, word_vec_length】
x = self.embedding_layer(input_x)
# BiLSTM 層,輸出形狀為【batch_size, max_sent_length, 2 * cell_unit】
x = self.bilstm_layer(x, training = training)
# 全連接層盟迟,輸出形狀為【batch_size, max_sent_length, num_tags】
output = self.dense_layer(x)
return output
下面我們定義一個用于選擇數(shù)據(jù)的函數(shù)秋泳,它隨機選擇 batch_size 個數(shù)據(jù),用于訓(xùn)練或測試:
def data_choice(X, y, sentence_length, batch_size):
"""
X, y:分別為句子和對應(yīng)的標(biāo)簽
sentence_length:句子的實際長度
"""
index = np.random.choice(len(X), size=batch_size, replace=True)
X_rd = X[index]
y_rd = y[index]
sentence_length_rd = sentence_length[index]
return X_rd, y_rd, sentence_length_rd
def bilstm_loss(y_true, y_pred, sent_len):
"""
y_true:真實的標(biāo)簽攒菠,形狀為【batch_size迫皱,max_sent_length】
y_pred:模型的輸出,形狀為【batch_size辖众,max_sent_length卓起,num_tags】
sent_len:每個句子的真實長度,形狀為【batch_size】
"""
# 交叉熵損失函數(shù)
loss = tf.keras.losses.sparse_categorical_crossentropy(y_true=y_true, y_pred=y_pred)
# 掩碼操作凹炸,只保留句子真實長度的部分的損失
mask = tf.sequence_mask(sent_len, max_sent_length)
loss = tf.boolean_mask(loss, mask)
loss = tf.reduce_mean(loss)
return loss
下面定義一個計算準(zhǔn)確率的函數(shù)既绩,用于評價我們得到的模型。
def calculate_accuracy(y_true, y_pred, sent_len):
# 掩碼操作还惠,只保留句子真實長度的部分的準(zhǔn)確率
mask = tf.sequence_mask(sent_len, max_sent_length)
y_true = tf.boolean_mask(y_true, mask)
# 取 y_pred 最大的索引作為標(biāo)簽
y_pred = tf.argmax(y_pred, -1)
y_pred = tf.boolean_mask(y_pred, mask)
# 計算預(yù)測值與真實值相等的個數(shù)
correct_labels = tf.equal(y_pred, y_true).numpy().sum()
# 計算準(zhǔn)確率
accuracy = 100.0 * correct_labels / y_true.shape[0]
return accuracy
現(xiàn)在開始訓(xùn)練模型饲握。
from tqdm.notebook import tqdm
# 實例化模型
model = MyModel(cell_unit=cell_unit, word_length=word_length, word_vec_length=word_vec_length)
# 定義優(yōu)化器
optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)
for i in tqdm(range(30)):
# 每次隨機取一個 batch 的數(shù)據(jù)進行訓(xùn)練
train_X_rd, train_y_rd, train_sentence_length_rd = data_choice(train_X, train_y, train_sentence_length, batch_size)
with tf.GradientTape() as tape:
y_pred = model(train_X_rd, training=True)
loss = bilstm_loss(y_true=train_y_rd, y_pred=y_pred, sent_len=train_sentence_length_rd)
grads = tape.gradient(loss, model.variables)
optimizer.apply_gradients(grads_and_vars=zip(grads, model.variables))
accuracy1 = calculate_accuracy(y_true=train_y_rd, y_pred=y_pred, sent_len=train_sentence_length_rd)
# 每次隨機取一個 batch 的數(shù)據(jù)進行測試
test_X_rd, test_y_rd, test_sentence_length_rd = data_choice(test_X, test_y, test_sentence_length, batch_size)
y_pred_test = model(test_X_rd, training=False)
accuracy2 = calculate_accuracy(y_true=test_y_rd, y_pred=y_pred_test, sent_len=test_sentence_length_rd)
print("訓(xùn)練集準(zhǔn)確率:{},測試集準(zhǔn)確率:{} ".format(accuracy1, accuracy2))
從上面的實驗結(jié)果可以看到蚕键,基于 BiLSTM 的方法有著很高的識別準(zhǔn)確率救欧。
BiLSTM+CRF 方法
上面我們主要完成了 BiLSTM 模型的構(gòu)建并進行訓(xùn)練。現(xiàn)在開始講解 BiLSTM+CRF 的方法锣光。為什么要加上 CRF 呢笆怠?
我們都知道 CRF 表示條件隨機場,在上一個實驗中講解到誊爹,條件隨機場的的預(yù)測是一個尋找最大路徑的問題蹬刷。也許是給定一個句子,得出的是許多滿足要求的標(biāo)注序列频丘,然后尋找出概率最大的那一條序列办成。換句話說,就是條件隨機場在預(yù)測時搂漠,會考慮整個句子之間的上下文關(guān)系迂卢。
現(xiàn)在再來看 BiLSTM。在上面講解的 BiLSTM 中桐汤,最終的輸出是每個時刻都對應(yīng)一個長度為 7 的向量而克。而向量的每個位置負責(zé)預(yù)測一個類別標(biāo)簽。每個時刻之間的預(yù)測都是獨立的怔毛。因此员萍,BiLSTM 在預(yù)測時沒有依賴上下文關(guān)系。
所以拣度,為了使模型在預(yù)測時也能考慮上下文之間的關(guān)系碎绎,所以在 BiLSTM 的輸出加上一層 CRF蜂莉。對于這個問題,如果你想了解更多混卵,可以參考這篇 論文
下面我們就來實現(xiàn)一下 BiLSTM+CRF 模型映穗。BiLSTM+CRF 模型與 BiLSTM 差不多,只是在 BiLSTM 的輸出加上一層 CRF 而已幕随。而 CRF 在 TensorFlow 中已經(jīng)實現(xiàn)蚁滋,只需調(diào)用函數(shù)即可。
現(xiàn)在先來定義添加 CRF 層后的損失函數(shù)赘淮。
import tensorflow_addons as tfa
def bilstm_crf_loss(y_true, y_pred, sent_len):
# 計算 CRF 層的損失函數(shù)辕录,直接調(diào)用 tensorflow_addons 提供的接口
log_likelihood, transition_params = tfa.text.crf_log_likelihood(y_pred, train_y_rd, sent_len)
# 使用維特比算法進行預(yù)測。得到預(yù)測序列
viterbi_sequence, viterbi_score = tfa.text.crf_decode(y_pred, transition_params, sent_len)
loss = tf.reduce_mean(-log_likelihood)
return loss
和前面訓(xùn)練 BiLSTM 模型一樣梢卸,現(xiàn)在進行訓(xùn)練模型走诞。
model = MyModel(cell_unit=cell_unit, word_length=word_length, word_vec_length=word_vec_length)
optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)
for i in tqdm(range(30)):
# 每次隨機取一個 batch 的數(shù)據(jù)進行訓(xùn)練
train_X_rd, train_y_rd, train_sentence_length_rd = data_choice(train_X, train_y, train_sentence_length, batch_size)
with tf.GradientTape() as tape:
y_pred = model(train_X_rd, training=True)
loss = bilstm_crf_loss(y_true=train_y_rd, y_pred=y_pred, sent_len=train_sentence_length_rd)
grads = tape.gradient(loss, model.variables)
optimizer.apply_gradients(grads_and_vars=zip(grads, model.variables))
accuracy1 = calculate_accuracy(y_true=train_y_rd, y_pred=y_pred, sent_len=train_sentence_length_rd)
# 每次隨機取一個 batch 的數(shù)據(jù)進行測試
test_X_rd, test_y_rd, test_sentence_length_rd = data_choice(test_X, test_y, test_sentence_length, batch_size)
y_pred_test = model(test_X_rd, training=False)
accuracy2 = calculate_accuracy(y_true=test_y_rd, y_pred=y_pred_test, sent_len=test_sentence_length_rd)
print("訓(xùn)練集準(zhǔn)確率:{},測試集準(zhǔn)確率:{} ".format(accuracy1, accuracy2))
從實驗結(jié)果可以看出蛤高,BiLSTM+CRF 模型的整體識別準(zhǔn)確率與 BiLSTM 模型差不多蚣旱。差距不是很明顯,主要的原因是這里我們只迭代了 30 次戴陡。這對于許多深度學(xué)習(xí)模型來說是遠遠不夠的塞绿。如果你感興趣,可以自己線下跑一遍恤批。
下面打印出异吻,模型的預(yù)測結(jié)果。
print('模型預(yù)測標(biāo)簽:', tf.argmax(y_pred_test, -1)[0])
print('-'*100)
print('模型真實標(biāo)簽:', test_y_rd[0])
從上面的結(jié)果可以看到喜庞,模型幾乎都預(yù)測成為了 O诀浪。這主要是我們?yōu)榱斯?jié)約時間,模型置訓(xùn)練了 30 次延都。這導(dǎo)致模型還沒學(xué)習(xí)到許多的東西雷猪。
那假設(shè)我們預(yù)測得到了標(biāo)簽,那該怎么轉(zhuǎn)化為提取的實體呢窄潭?下面就來解釋這一問題春宣。
我們通過模型得到的是數(shù)字形式的標(biāo)簽,而前面我們已經(jīng)定義了將標(biāo)簽轉(zhuǎn)換為數(shù)字的字典嫉你。而現(xiàn)在要求的是將數(shù)字轉(zhuǎn)化為標(biāo)簽,所以定義一個將數(shù)字轉(zhuǎn)化為標(biāo)簽的字典躏惋。只需將前面所構(gòu)建的字典反過來即可幽污。
ix_to_tag = dict(zip(tag_to_ix.values(), tag_to_ix.keys()))
ix_to_tag
剛剛只是定義了轉(zhuǎn)換字典,現(xiàn)在定義轉(zhuǎn)化函數(shù)簿姨。
def indx2tag(sent, to_tags, sent_len):
tags = [to_tags[ix] for ix in sent]
# 只需要真實標(biāo)簽部分即可
tags = tags[:sent_len]
return tags
定義完字典和轉(zhuǎn)化函數(shù)之后距误,就可以將預(yù)測的結(jié)果為數(shù)字的標(biāo)注值轉(zhuǎn)換成為標(biāo)簽值簸搞。
由于我們前面訓(xùn)練的模型,因參數(shù)簡單准潭,而且迭代次數(shù)過小的原因趁俊,不能很準(zhǔn)確的預(yù)測。這里為了講解如何根據(jù)預(yù)測的標(biāo)注結(jié)果來提取命名實體刑然。直接使用真實的標(biāo)簽值來講解寺擂。當(dāng)然,你也可以在線下對模型進行充分的訓(xùn)練泼掠,然后再進行預(yù)測怔软。
test_pred = []
for i in range(50):
test_pred.append(indx2tag(test_y[i], ix_to_tag, test_sentence_length[i]))
test_pred[0]
通過轉(zhuǎn)化之后,得到如上的標(biāo)簽形式≡裾颍現(xiàn)在使用標(biāo)簽來對句子進行切分挡逼。切出命名實體部分,下面定義一個切分函數(shù)腻豌。
def ner(sentence_tags, sentence_char):
"""
sentence_tags:預(yù)測標(biāo)注序列
sentence_char:原中文句子序列
"""
sentence_tags.append('O')
per = []
loc = []
org = []
for j in range(len(sentence_tags)-1):
if sentence_tags[j] == 'B-PER':
per_s = j
I_PER = 0
if sentence_tags[j] == 'I-PER':
I_PER += 1
if sentence_tags[j+1] != 'I-PER':
word = ''.join(sentence_char[per_s:per_s+I_PER+1])
I_PER = 0
per.append(word)
if sentence_tags[j] == 'B-LOC':
loc_s = j
I_LOC = 0
if sentence_tags[j] == 'I-LOC':
I_LOC += 1
if sentence_tags[j+1] != 'I-LOC':
word = ''.join(sentence_char[loc_s:loc_s+I_LOC+1])
I_LOC = 0
loc.append(word)
if sentence_tags[j] == 'B-ORG':
org_s = j
I_ORG = 0
start = 1
if sentence_tags[j] == 'I-ORG':
I_ORG += 1
if sentence_tags[j+1] != 'I-ORG':
word = ''.join(sentence_char[org_s:org_s+I_ORG+1])
I_ORG = 0
org.append(word)
return per, loc, org
測試上面所定義的實體切分函數(shù)家坎。
for i in range(50):
print(''.join(test_X_char[i]))
print('-'*100)
per, loc, org = ner(test_pred[i], test_X_char[i])
print('人名:{};地名:{}吝梅;機構(gòu)名:{}'.format(per, loc, org))
print('='*100)
從上面的結(jié)果可以看到乘盖,已經(jīng)成功提取出命名實體。
實現(xiàn)基于規(guī)則的命名實體識別方法
挑戰(zhàn)介紹
在上一個實驗中憔涉,我們講解了命名實體識別的基本概念订框,并使用 TensorFlow 深度學(xué)習(xí)框架實現(xiàn)了基于 BiLSTM+CRF 的方法。深度學(xué)習(xí)方法雖然識別準(zhǔn)確率較高兜叨,但是其需要大量標(biāo)注的數(shù)據(jù)穿扳,而運行時間較長。其實国旷,在實際應(yīng)用中還是以基于規(guī)則的方法為主矛物。而本次挑戰(zhàn)的主角就基于規(guī)則的命名實體識別方法。
我們先來簡單介紹一下跪但,基于規(guī)則的命名實體識別方法履羞。假設(shè)有下面一段文本。
決定書指出屡久,北京國旺和美科技有限公司應(yīng)主動司糾正相關(guān)違法北行為忆首,并于 2024 年 05 北月 17 日后申請中移出嚴(yán)重違法失信企業(yè)名單。如對本決定有異議被环,可以自公示之日起三十日內(nèi)向北京市市場監(jiān)督管理局提出書面申請并提交相關(guān)證明材料糙及,要求核實。也可在接到本決定書之日起六十日內(nèi)向中國國家市場監(jiān)督管理總局或者北京市人民政府申請行政復(fù)議筛欢;或者六個月內(nèi)向北京市海淀區(qū)人民法院提起行政訴訟浸锨。
我們現(xiàn)在的任務(wù)是識別出上面文本的組織機構(gòu)名唇聘,即黑體字。仔細觀察可以發(fā)現(xiàn)柱搜,這些機構(gòu)名中迟郎,開頭的字大多都以地名開頭,例如:“北京”聪蘸,“中國”等宪肖。結(jié)尾大多以一些比較規(guī)則的詞結(jié)尾,例如:“公司”宇姚,“局”匈庭,“院” 等。
為了便于理解這里只看第一個字和最后一個字浑劳。我們現(xiàn)在定義兩個字典阱持,分別是地名字典和特征字典,地名字典用于存放常用的第一個字魔熏,例如在這個例子中衷咽,該字典為【“北”,“中”】蒜绽,特征字典存放為最后一個字镶骗,例如在這個例子中為【“司”,“局”躲雅,“府”鼎姊,“院”】,我們現(xiàn)在定義個規(guī)則相赁,如下:
要是一個字在地名字典中則標(biāo)記為 S相寇。
要是一個字在特征字典中則標(biāo)記為 E。
其他情況標(biāo)記為 O钮科。
使用上面規(guī)則唤衫,現(xiàn)在就可以將一份文本轉(zhuǎn)化為相應(yīng)的標(biāo)記,例如【決定書指出绵脯,北京國旺和美科技有限公司應(yīng)主動】被標(biāo)記為【O O O O O O S O O O O O O O O O O E O O O】佳励。
轉(zhuǎn)化成為標(biāo)記之后現(xiàn)在就可以,使用正則表達式對標(biāo)記序列進行提取了蛆挫。正則表達式是一個很有效的工具赃承。如果你對正則表達式不熟悉,可以去看 《正則表達式必知必會》或是查閱相關(guān)的資料璃吧。
那怎么提取呢楣导?在正則表達式中,我們可以定義一個模式畜挨,例如:'SO*E'
表示匹配兩端為 S 和 E 筒繁,中間為 O 的任意長度的子字符串,可以為 SOE巴元、SOOE毡咏、SOOOOE 等。這有什么用呢逮刨,來看下面例子呕缭。
import re
# 句子為
sentence= '決定書指出,北京國旺和美科技有限公司應(yīng)主動'
# 對應(yīng)的字符串 label 為
label= 'OOOOOOSOOOOOOOOOOEOOO'
p='SO*E' #定義的匹配模式
pattern=re.compile(p) #使用正則表達式
ne_label=re.finditer(pattern,label)# 尋找字符串 label 中所有可能匹配到的子字符串修己。
# 這里得到的子串應(yīng)為 ' SOOOOOOOOOOE ' 的一個正則表達式的對象恢总。
for ne in ne_label:
print(ne.group())# 返回子字符串 ' SOOOOOOOOOOE '
print(ne.start())# 返回子串第一個字符(S)在原字符串 label 中的位置,這里為 6
print(ne.end()) # 返回子串最后一個字符(E)在原字符串 label 中的位置睬愤,這里為 18
那么通過正則表達式就可以得到一個實體在一個句子中開始出現(xiàn)的第一個字以及最后一個字的位置片仿,然后通過切片的方法就可以將實體從句子中抽出。
我們現(xiàn)在再來梳理一下上面所述的基于規(guī)則的方法的運行流程:
輸入句子尤辱、地名字典砂豌、特征字典。
轉(zhuǎn)換句子變成對應(yīng)的標(biāo)簽形式光督。
使用正則表達式求出實體的位置阳距。
使用切片的方法,將實體從原輸入句子中抽出结借。
以上就是一種簡單的基于規(guī)則的命名實體識別方法筐摘。而現(xiàn)在的挑戰(zhàn)就是讓你實現(xiàn)一個基于規(guī)則的識別方法。
挑戰(zhàn)內(nèi)容
在本次挑戰(zhàn)中船老,你需要在 ~/Code/regu_ner.py 文件中編寫一個函數(shù) re_ner咖熟,re_ner 函數(shù)接受三個參數(shù),分別是要提取的句子努隙、地名字典以及特征字典球恤。然后返回抽取結(jié)果,抽取結(jié)果用 list 的形式存放荸镊。
挑戰(zhàn)要求
代碼必須寫入 ~/Code/regu_ner.py 文件中咽斧。
函數(shù)名必須是 re_ner 。
測試時請使用 /home/shiyanlou/anaconda3/bin/python 運行 regu_ner.py 躬存,避免出現(xiàn)無相應(yīng)模塊的情況张惹。