程式碼詳解:Sequence2Sequence模型讓NER so easy
全文共4463字,預計學習時長9分鐘
當下有一種非常流行的自然語言處理任務,叫做命名實體識別(Named Entity Recognition NER)。簡而言之,NER是從一系列單詞(句子)中提取名稱實體的任務。
例如,給出句子:
“Jim bought 300 shares of Acme Corp. in 2006.”
“吉姆”是一個人,“Acme”是一個組織,“2006”是時間。
為此,本文將使用公開的Kaggle資料集,跳過所有資料處理程式碼,重點關注實際問題和解決方案。
在此資料集中有許多實體型別,如Person(PER),Organization(ORG)等。對於每種實體型別,有兩種型別的標籤:“B-SOMETAG”和“I-SOMETAG”。 B表示實體名稱的開頭,I表示該實體的延續。因此,如果有像“Word Health Organization”這樣的實體,相應的標籤將是[B-ORG,I-ORG,I-ORG]。
以下是資料集中的例子:
import pandas as pd
ner_df = pd.read_csv('ner_dataset.csv')
ner_df.head(30)
所以我們得到一些序列(句子),想要預測每個單詞的“類”。這不像分類或迴歸這樣的機器學習任務一樣瑣碎。得到一個序列,輸出應該是一個大小相同的序列。
有很多方法可以解決這個問題。在這裡,需要做以下事情:
- 構建一個非常簡單的模型,將此任務視為句子中每個單詞的分類,並將其用作基準。
- 使用Keras構建Seq2Seq模型。
- 談談衡量和比較結果的正確方法。
- 在Seq2Seq模型中使用預先訓練好的Glove嵌入。
隨意跳轉到任何部分。
詞袋(Bag of Words)和多級分類
正如之前提到的,輸出應該是一系列的分類。但是,本文想探索一些簡單的方法——一個簡單的多級分類模型。將句子中的每個單詞視為一個單獨的例項,並且對於每個例項(單詞)都能預測其分類,即O,B-ORG,I-ORG,B-PER等中的一個。
這當然不是模擬這個問題的最佳方法,但這樣做有兩個原因:建立一個儘可能保持簡單的基準;當我們使用序列模型時,sequence2sequence會執行得更好。
很多時候,當我們試圖模擬現實生活中的問題時,並不總是清楚正在處理什麼型別的問題。有時需要嘗試將這些問題建模為簡單的分類任務,而實際上,序列模型可能會更好。
正如先前所說,將這種方法視為基準,並儘可能簡化事物。因此對於每個單詞(例項)來說,這個方法會將這些單詞向量(Bag of words)簡化,同一句子中的其他單詞也是這樣處理的。
def sentence_to_instances(words, tags, bow, count_vectorizer):
X = []
y = []
for w, t in zip(words, tags):
v = count_vectorizer.transform([w])[0]
v = scipy.sparse.hstack([v, bow])
X.append(v)
y.append(t)
return scipy.sparse.vstack(X), y
可以看一下以下句子:
“The World Health Organization says 227 people have died from bird flu”
我們為每個單詞準備12個例項。
the O
world B-org
health I-org
organization I-org
says O
227 O
people O
have O
died O
from O
bird O
flu O
現在我們的任務是在每個句子中用一個單詞來預測它的類別。
資料集中有47958個句子,將其分為“訓練”集和“測試”集:
train_size = int(len(sentences_words) * 0.8)
train_sentences_words = sentences_words[:train_size]
train_sentences_tags = sentences_tags[:train_size]
test_sentences_words = sentences_words[train_size:]
test_sentences_tags = sentences_tags[train_size:]
# ============== Output
==============================
Train: 38366
Test: 9592
使用上述方法將所有句子轉換為多個單詞例項。在訓練資料集中,有839,214個單詞例項。
train_X, train_y = sentences_to_instances(train_sentences_words,
train_sentences_tags,
count_vectorizer)
print 'Train X shape:', train_X.shape
print 'Train Y shape:', train_y.shape
# ============== Output
==============================
Train X shape: (839214, 50892)
Train Y shape: (839214,)
在X中,有50892個維度:當前單詞有一個熱向量,同一個句子中所有其他單詞有一個詞袋向量。
使用Gradient Boosting Classifier作為預測器:
clf = GradientBoostingClassifier().fit(train_X, train_y)
predicted = clf.predict(test_X)
print classification_report(test_y, predicted)
然後得到以下資訊:
precision recall f1-score support
B-art 0.57 0.05 0.09 82
B-eve 0.68 0.28 0.40 46
B-geo 0.91 0.40 0.56 7553
B-gpe 0.96 0.84 0.90 3242
B-nat 0.52 0.27 0.36 48
B-org 0.93 0.31 0.46 4082
B-per 0.80 0.52 0.63 3321
B-tim 0.91 0.66 0.76 4107
I-art 0.09 0.02 0.04 43
I-eve 0.33 0.02 0.04 44
I-geo 0.82 0.55 0.66 1408
I-gpe 0.86 0.62 0.72 40
I-nat 0.20 0.08 0.12 12
I-org 0.88 0.24 0.38 3470
I-per 0.93 0.25 0.40 3332
I-tim 0.67 0.15 0.25 1308
O 0.91 1.00 0.95 177215
avg/ 0.91 0.91 0.89 209353
total
這樣的效果好嗎?很難知道。我們可能會考慮幾種方法來改進模型,但這不是這篇文章的目標,正如先前所說的——我們想讓它成為一個非常簡單的基準。
這不是衡量模型的正確方法。我們得到了每個單詞的精確度/召回率,但它並沒有告訴我們任何關於真實的實體資訊。以相同的句子為例,下面是一個簡單的例子:
“The World Health Organization says 227 people have died from bird flu”
我們有3個ORG分類,如果只正確預測其中兩個,將得到66%的準確率,但我們沒有正確提取“World Health Organization”實體,所以實體的準確性將是0!
本文將在後面討論更好的方法來測量命名實體識別模型,但首先需要構建 Sequence to Sequence模型。
Sequence to Sequence模型
前一個方法的主要缺點是丟失了依賴性資訊。給出句子中的單詞,知道左側(或右側)的單詞是實體可能是有用的。為每個單詞構建例項時,不僅很難實現,而且也沒有在預測時間內獲得這些資訊。這是將整個序列用作例項的原因。
我們可以使用許多不同的模型來實現這一點。像隱馬爾可夫模型(HMM)或條件隨機場(CRF)這樣的演算法可能效果很好,但在這裡將用Keras實現遞迴神經網路。
要使用Keras,需要將句子轉換為數字序列,每個數字代表一個單詞。我們需要使所有序列的長度相同。對此,可以使用Keras util方法來完成它。
首先,使用標記器來將單詞轉換為數字。將它只安裝在訓練集是非常重要。
words_tokenizer = Tokenizer(num_words=VOCAB_SIZE,
filters=[],
oov_token='__UNKNOWN__')
words_tokenizer.fit_on_texts(map(lambda s: ' '.join(s),
train_sentences_words))
word_index = words_tokenizer.word_index
word_index['__PADDING__'] = 0
index_word = {i:w for w, i in word_index.iteritems()}
# ============== Output
==============================
print 'Unique tokens:', len(word_index)
接下來,使用標記器建立序列並對其進行填充以獲得相同長度的序列:
train_sequences = words_tokenizer.texts_to_sequences(map(lambda s: ' '.join(s), train_sentences_words))
test_sequences = words_tokenizer.texts_to_sequences(map(lambda s: ' '.join(s), test_sentences_words))
train_sequences_padded = pad_sequences(train_sequences,
maxlen=MAX_LEN)
test_sequences_padded = pad_sequences(test_sequences,
maxlen=MAX_LEN)
print train_sequences_padded.shape, test_sequences_padded.shape
# ============== Output
==============================
(38366, 75) (9592, 75)
可以看到,在訓練集中有38,366個序列,在測試集中有9,592個序列,每個序列中有75個標記。
print train_tags_padded.shape, test_tags_padded.shape
# ============== Output
==============================
(38366, 75, 1) (9592, 75, 1)
在訓練集中有38,366個序列,在測試集中有9,592個序列,每個序列中有17個標籤。
現在準備建立模型。使用理解長短期記憶模型(LSTM)層,事實證明對這些任務非常有效:
input = Input(shape=(75,), dtype='int32')
emb = Embedding(V_SIZE, 300, max_len=75)(input)
x = Bidirectional(LSTM(64, return_sequences=True))(emb)
preds = Dense(len(tag_index), activation='softmax')(x)
model = Model(sequence_input, preds)
model.compile(loss='sparse_categorical_crossentropy',
optimizer='adam',
metrics=['sparse_categorical_accuracy'])
讓我們看看這裡有什麼:
第一層是Input它接受形狀向量(75),並且匹配X變數(在訓練和測試中的每個序列中都有75個標記)。
接下來是嵌入層。這一層會抓取每個標記/文字,並把它變成一個容量大小為300的密集向量。將它看作一個巨大的查詢表(或字典),其中標記(單詞id)作為鍵,實際向量作為值。該查詢表是可訓練的,即在模型訓練期間的每個階段,更新這些向量以匹配輸入。
在嵌入層之後,輸入從長度為75的向量變為大小為75,300的矩陣。 這75個標記,每個現在都有一個大小為300的向量。
一旦有了這個,就可以使用雙向LSTM層,每個標記都會在句子中檢視兩種方式並返回一個狀態,以幫助我們稍後對該單詞進行分類。預設情況下,LSTM層將返回單個向量(最後一個),但在示例中,我們需要每個標記的向量,因此我們使用return_sequences = True。
操作如下:
該層的輸出是一個大小為75,128的矩陣——其中包括75個標記,一個方向為64個,另一個也為64個。
最後,有一個時間分佈密集層(當我們使用return_sequences = True時,它變為時間分佈)。
它採用LSTM層輸出的75,128個矩陣並返回所需的75,18個矩——包括75個標記,每個標記有17個標籤概率和一個__PADDING__。
使用model.summary()方法很容易看到發生了什麼:
___________________________
Layer (type) Output Shape Param # ===========================
input_1 (InputLayer) (None, 75) 0
___________________________
embedding_1 (Embedding) (None, 75, 300) 8646600 ___________________________
bidirectional_1 (Bidirection (None, 75, 128) 186880 ___________________________
dense_2 (Dense) (None, 75, 18) 627 ==========================
Total params: 8,838,235
Trainable params: 8,838,235
Non-trainable params: 0
___________________________
可以使用輸入和輸出形狀檢視所有圖層。此外,我們可以看到模型中的引數數量。你可能已經注意到嵌入層的引數最多。這是因為我們有很多單詞,需要為每個單詞學習300個數字。之後,我們會使用預先訓練的嵌入來改進模型。
開始訓練我們的模型:
model.fit(train_sequences_padded, train_tags_padded,
batch_size=32,
epochs=10,
validation_data=(test_sequences_padded, test_tags_padded))
# ============== Output
==============================
Train on 38366 samples, validate on 9592 samples
Epoch 1/10
38366/38366 [==============================] - 274s
7ms/step - loss: 0.1307 - sparse_categorical_accuracy: 0.9701 - val_loss:
0.0465 - val_sparse_categorical_accuracy: 0.9869
Epoch 2/10
38366/38366 [==============================] - 276s
7ms/step - loss: 0.0365 - sparse_categorical_accuracy: 0.9892 - val_loss:
0.0438 - val_sparse_categorical_accuracy: 0.9879
Epoch 3/10
38366/38366 [==============================] - 264s
7ms/step - loss: 0.0280 - sparse_categorical_accuracy: 0.9914 - val_loss:
0.0470 - val_sparse_categorical_accuracy: 0.9880
Epoch 4/10
38366/38366 [==============================] - 261s
7ms/step - loss: 0.0229 - sparse_categorical_accuracy: 0.9928 - val_loss:
0.0480 - val_sparse_categorical_accuracy: 0.9878
Epoch 5/10
38366/38366 [==============================] - 263s
7ms/step - loss: 0.0189 - sparse_categorical_accuracy: 0.9939 - val_loss:
0.0531 - val_sparse_categorical_accuracy: 0.9878
Epoch 6/10
38366/38366 [==============================] - 294s
8ms/step - loss: 0.0156 - sparse_categorical_accuracy: 0.9949 - val_loss:
0.0625 - val_sparse_categorical_accuracy: 0.9874
Epoch 7/10
38366/38366 [==============================] - 318s
8ms/step - loss: 0.0129 - sparse_categorical_accuracy: 0.9958 - val_loss:
0.0668 - val_sparse_categorical_accuracy: 0.9872
Epoch 8/10
38366/38366 [==============================] - 275s
7ms/step - loss: 0.0107 - sparse_categorical_accuracy: 0.9965 - val_loss:
0.0685 - val_sparse_categorical_accuracy: 0.9869
Epoch 9/10
38366/38366 [==============================] - 270s
7ms/step - loss: 0.0089 - sparse_categorical_accuracy: 0.9971 - val_loss:
0.0757 - val_sparse_categorical_accuracy: 0.9870
Epoch 10/10
38366/38366 [==============================] - 266s
7ms/step - loss: 0.0076 - sparse_categorical_accuracy: 0.9975 - val_loss:
0.0801 - val_sparse_categorical_accuracy: 0.9867
測試集的準確率達到98.6%。這種準確性並沒有告訴我們多少實際價值,因為大多數標籤都是“0”(其他)。我們希望像以前一樣看到每個類的精確度/召回率,但這也不是評估模型的最佳方法。這種方法可以看到有多少不同型別的實體能夠被正確預測。
Sequence to Sequence模型的評估
使用序列時,標籤/實體也可能是序列。如果將“World Health Organisation”作為真正的實體,從單詞角度來講,預測“World Organisation”或“World Health”可能會給出66%的準確度,但兩者都是錯誤的預測。這裡我們包裝每個句子中的所有實體,並將它們與預測的實體進行比較。
我們可以使用非常好的seqeval庫。對於每個句子,seqeval庫可以查詢所有不同的標籤並構造實體。通過對真實標籤和預測標籤進行處理,可以比較真實實體值而不僅僅是單詞的價值。在這種情況下,沒有“B-”或“I-”標籤,我們比較實體的實際型別而不是單詞分類。
使用預測值,這是一個概率矩陣,我們想要為每個句子構建一個具有原始長度的標籤序列(而不是我們所做的那樣),因此可以將它們與真實值進行比較。為LSTM模型和詞袋模型執行以下操作:
lstm_predicted = model.predict(test_sequences_padded)
lstm_predicted_tags = []
bow_predicted_tags = []
for s, s_pred in zip(test_sentences_words, lstm_predicted):
tags = np.argmax(s_pred, axis=1)
tags = map(index_tag_wo_padding.get,tags)[-len(s):]
lstm_predicted_tags.append(tags)
bow_vector, _ = sentences_to_instances([s],
[['x']*len(s)],
count_vectorizer)
bow_predicted = clf.predict(bow_vector)[0]
bow_predicted_tags.append(bow_predicted)
現在我們準備使用seqeval庫評估模型:
from seqeval.metrics import classification_report, f1_score
print 'LSTM'
print '='*15
print classification_report(test_sentences_tags,
lstm_predicted_tags)
print 'BOW'
print '='*15
print classification_report(test_sentences_tags,bow_predicted_tags)
可以得到:
LSTM
===============
precision recall f1-score support
art 0.11 0.10 0.10 82
gpe 0.94 0.96 0.95 3242
eve 0.21 0.33 0.26 46
per 0.66 0.58 0.62 3321
tim 0.84 0.83 0.84 4107
nat 0.00 0.00 0.00 48
org 0.58 0.55 0.57 4082
geo 0.83 0.83 0.83 7553
avg / 0.77 0.75 0.76 22481
total
BOW
===============
precision recall f1-score support
art 0.00 0.00 0.00 82
gpe 0.01 0.00 0.00 3242
eve 0.00 0.00 0.00 46
per 0.00 0.00 0.00 3321
tim 0.00 0.00 0.00 4107
nat 0.00 0.00 0.00 48
org 0.01 0.00 0.00 4082
geo 0.03 0.00 0.00 7553
avg / 0.01 0.00 0.00 22481
total
這呈現出了很大的不同。可以看到詞袋模型幾乎無法正確預測任何事情,而LSTM模型卻表現得很好。
當然,我們可以在詞袋模型上多次操作以獲得更好的結果,但是結果顯而易見,在這種情況下,Sequence to Sequence模型更合適。
如前所述,大多數模型引數都是嵌入層。訓練這一層非常困難,因為單詞量很大但訓練資料有限。用預先訓練的嵌入層非常常見。大多數當前的嵌入模型使用所謂的“分佈假設”,其表明同一上下文中的單詞具有相似的含義。通過構建一個預測給定上下文(或其他方式)的單詞模型,可以生成具有良好的單詞含義表示的單詞向量。雖然這與我們的任務沒有直接關係,但使用這些嵌入可能有助於模型更好地為其目標鎖定單詞。
還有其他方法可以構建單詞嵌入,從簡單的共生矩陣到更復雜的語言模型。在此之前,我嘗試使用了影象構建單詞嵌入。
在這裡,會使用流行的Glove嵌入。Word2Vec或任何其他模型的效果也很好。
將Glove模型下載下來,載入單詞向量並建立嵌入矩陣。我們將使用此矩陣作為嵌入層的不可訓練權重:
embeddings = {}
with open(os.path.join(GLOVE_DIR, 'glove.6B.300d.txt')) as f:
for line in f:
values = line.split()
word = values[0]
coefs = np.asarray(values[1:], dtype='float32')
embeddings[word] = coefs
num_words = min(VOCAB_SIZE, len(word_index) + 1)
embedding_matrix = np.zeros((num_words, 300))
for word, i in word_index.items():
if i >= VOCAB_SIZE:
continue
embedding_vector = embeddings.get(word)
if embedding_vector is not None:
embedding_matrix[i] = embedding_vector
下面是我們的模型:
input = Input(shape=(75,), dtype='int32')
emb = Embedding(VOCAB_SIZE, 300,
embeddings_initializer=Constant(embedding_matrix),
input_length=MAX_LEN,
trainable=False)(input)
x = Bidirectional(LSTM(64, return_sequences=True))(emb)
preds = Dense(len(tag_index), activation='softmax')(x)
model = Model(sequence_input, preds)
model.compile(loss='sparse_categorical_crossentropy',
optimizer='adam',
metrics=['sparse_categorical_accuracy'])
model.summary()
# ============== Output
==============================
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_2 (InputLayer) (None, 75) 0
_________________________________________________________________
embedding_2 (Embedding) (None, 75, 300) 8646600
_________________________________________________________________
bidirectional_2 (Bidirection (None, 75, 128) 186880
_________________________________________________________________
dropout_2 (Dropout) (None, 75, 128) 0
_________________________________________________________________
dense_4 (Dense) (None, 75, 18) 627
=================================================================
Total params: 8,838,235
Trainable params: 191,635
Non-trainable params: 8,646,600
_________________________________________________________________
所有的內容都和以前一樣。唯一的區別是,現在的嵌入層具有恆定的非訓練權重。可以看到總引數的數量沒有改變,而可訓練引數的數量要低得多。
讓我們來擬合模型:
Train on 38366 samples, validate on 9592 samples
Epoch 1/10
38366/38366 [==============================] - 143s
4ms/step - loss: 0.1401 - sparse_categorical_accuracy: 0.9676 - val_loss:
0.0514 - val_sparse_categorical_accuracy: 0.9853
Epoch 2/10
38366/38366 [==============================] - 143s
4ms/step - loss: 0.0488 - sparse_categorical_accuracy: 0.9859 - val_loss:
0.0429 - val_sparse_categorical_accuracy: 0.9875
Epoch 3/10
38366/38366 [==============================] - 138s
4ms/step - loss: 0.0417 - sparse_categorical_accuracy: 0.9876 - val_loss:
0.0401 - val_sparse_categorical_accuracy: 0.9881
Epoch 4/10
38366/38366 [==============================] - 132s
3ms/step - loss: 0.0381 - sparse_categorical_accuracy: 0.9885 - val_loss:
0.0391 - val_sparse_categorical_accuracy: 0.9887
Epoch 5/10
38366/38366 [==============================] - 146s
4ms/step - loss: 0.0355 - sparse_categorical_accuracy: 0.9891 - val_loss:
0.0367 - val_sparse_categorical_accuracy: 0.9891
Epoch 6/10
38366/38366 [==============================] - 143s
4ms/step - loss: 0.0333 - sparse_categorical_accuracy: 0.9896 - val_loss:
0.0373 - val_sparse_categorical_accuracy: 0.9891
Epoch 7/10
38366/38366 [==============================] - 145s
4ms/step - loss: 0.0318 - sparse_categorical_accuracy: 0.9900 - val_loss:
0.0355 - val_sparse_categorical_accuracy: 0.9894
Epoch 8/10
38366/38366 [==============================] - 142s
4ms/step - loss: 0.0303 - sparse_categorical_accuracy: 0.9904 - val_loss:
0.0352 - val_sparse_categorical_accuracy: 0.9895
Epoch 9/10
38366/38366 [==============================] - 138s
4ms/step - loss: 0.0289 - sparse_categorical_accuracy: 0.9907 - val_loss:
0.0362 - val_sparse_categorical_accuracy: 0.9894
Epoch 10/10
38366/38366 [==============================] - 137s
4ms/step - loss: 0.0278 - sparse_categorical_accuracy: 0.9910 - val_loss:
0.0358 - val_sparse_categorical_accuracy: 0.9895
準確性沒有太大變化,但正如之前所見,準確性並不是合適的指標。讓我們換一種方式對其進行評估,並與之前的模型進行比較:
lstm_predicted_tags = []
for s, s_pred in zip(test_sentences_words, lstm_predicted):
tags = np.argmax(s_pred, axis=1)
tags = map(index_tag_wo_padding.get,tags)[-len(s):]
lstm_predicted_tags.append(tags)
print 'LSTM + Pretrained Embbeddings'
print '='*15
print classification_report(test_sentences_tags, lstm_predicted_tags)
# ============== Output
==============================
LSTM + Pretrained Embbeddings
===============
precision recall f1-score support
art 0.45 0.06 0.11 82
gpe 0.97 0.95 0.96 3242
eve 0.56 0.33 0.41 46
per 0.72 0.71 0.72 3321
tim 0.87 0.84 0.85 4107
nat 0.00 0.00 0.00 48
org 0.62 0.56 0.59 4082
geo 0.83 0.88 0.86 7553
avg / 0.80 0.80 0.80 22481
total
效果特別好, F1得分從76增加到80!
結論
Sequence to Sequence模型非常強大,適用於許多工,如命名實體識別(NER),詞性(POS)標記,解析等。有很多技術和方式來訓練它們,但最重要的是知道何時使用何種方式以及如何正確地模擬問題。
留言 點贊 發個朋友圈
我們一起分享AI學習與發展的乾貨
編譯組:草田
相關連結:
https://towardsdatascience.com/solving-nlp-task-using-sequence2sequence-model-from-zero-to-hero-c193c1bd03d1