用深度神經網路處理NER命名實體識別問題
本文結構:
- 什麼是命名實體識別(NER)
- 怎麼識別?
cs224d Day 7: 專案2-用DNN處理NER問題
課程專案描述地址
什麼是NER?
命名實體識別(NER)是指識別文字中具有特定意義的實體,主要包括人名、地名、機構名、專有名詞等。命名實體識別是資訊提取、問答系統、句法分析、機器翻譯等應用領域的重要基礎工具,作為結構化資訊提取的重要步驟。摘自BosonNLP
怎麼識別?
先把解決問題的邏輯說一下,然後解釋主要的程式碼,有興趣的話,完整程式碼請去這裡看。
程式碼是在 Tensorflow 下建立只有一個隱藏層的 DNN 來處理 NER 問題。
1.問題識別:
NER 是個分類問題。
給一個單詞,我們需要根據上下文判斷,它屬於下面四類的哪一個,如果都不屬於,則類別為0,即不是實體,所以這是一個需要分成 5 類的問題:
• Person (PER)
• Organization (ORG)
• Location (LOC)
• Miscellaneous (MISC)
我們的訓練資料有兩列,第一列是單詞,第二列是標籤。
EU ORG
rejects O
German MISC
Peter PER
BRUSSELS LOC
2.模型:
接下來我們用深度神經網路對其進行訓練。
模型如下:
輸入層的 x^(t) 為以 x_t 為中心的視窗大小為3的上下文語境,x_t 是 one-hot 向量,x_t 與 L 作用後就是相應的詞向量,詞向量的長度為 d = 50 :
我們建立一個只有一個隱藏層的神經網路,隱藏層維度是 100,y^ 就是得到的預測值,維度是 5:
用交叉熵來計算誤差:
J 對各個引數進行求導:
得到如下求導公式:
在 TensorFlow 中求導是自動實現的,這裡用Adam優化演算法更新梯度,不斷地迭代,使得loss越來越小直至收斂。
3.具體實現
在 def test_NER()
中,我們進行 max_epochs
次迭代,每次,用 training data 訓練模型 得到一對 train_loss, train_acc
,再用這個模型去預測 validation data,得到一對 val_loss, predictions
val_loss
,並把相應的引數 weights 儲存起來,最後我們是要用這些引數去預測 test data 的類別標籤:
def test_NER():
config = Config()
with tf.Graph().as_default():
model = NERModel(config) # 最主要的類
init = tf.initialize_all_variables()
saver = tf.train.Saver()
with tf.Session() as session:
best_val_loss = float('inf') # 最好的值時,它的 loss 它的 迭代次數 epoch
best_val_epoch = 0
session.run(init)
for epoch in xrange(config.max_epochs):
print 'Epoch {}'.format(epoch)
start = time.time()
###
train_loss, train_acc = model.run_epoch(session, model.X_train,
model.y_train) # 1.把 train 資料放進迭代裡跑,得到 loss 和 accuracy
val_loss, predictions = model.predict(session, model.X_dev, model.y_dev) # 2.用這個model去預測 dev 資料,得到loss 和 prediction
print 'Training loss: {}'.format(train_loss)
print 'Training acc: {}'.format(train_acc)
print 'Validation loss: {}'.format(val_loss)
if val_loss < best_val_loss: # 用 val 資料的loss去找最小的loss
best_val_loss = val_loss
best_val_epoch = epoch
if not os.path.exists("./weights"):
os.makedirs("./weights")
saver.save(session, './weights/ner.weights') # 把最小的 loss 對應的 weights 儲存起來
if epoch - best_val_epoch > config.early_stopping:
break
###
confusion = calculate_confusion(config, predictions, model.y_dev) # 3.把 dev 的lable資料放進去,計算prediction的confusion
print_confusion(confusion, model.num_to_tag)
print 'Total time: {}'.format(time.time() - start)
saver.restore(session, './weights/ner.weights') # 再次載入儲存過的 weights,用 test 資料做預測,得到預測結果
print 'Test'
print '=-=-='
print 'Writing predictions to q2_test.predicted'
_, predictions = model.predict(session, model.X_test, model.y_test)
save_predictions(predictions, "q2_test.predicted") # 把預測結果儲存起來
if __name__ == "__main__":
test_NER()
4.模型是怎麼訓練的呢?
- 首先匯入資料 training,validation,test:
# Load the training set
docs = du.load_dataset('data/ner/train')
# Load the dev set (for tuning hyperparameters)
docs = du.load_dataset('data/ner/dev')
# Load the test set (dummy labels only)
docs = du.load_dataset('data/ner/test.masked')
- 把單詞轉化成 one-hot 向量後,再轉化成詞向量:
def add_embedding(self):
# The embedding lookup is currently only implemented for the CPU
with tf.device('/cpu:0'):
embedding = tf.get_variable('Embedding', [len(self.wv), self.config.embed_size]) # assignment 中的 L
window = tf.nn.embedding_lookup(embedding, self.input_placeholder) # 在 L 中直接把window大小的context的word vector搞定
window = tf.reshape(
window, [-1, self.config.window_size * self.config.embed_size])
return window
- 建立神經層,包括用 xavier 去初始化第一層, L2 正則化和用 dropout 來減小過擬合的處理:
def add_model(self, window):
with tf.variable_scope('Layer1', initializer=xavier_weight_init()) as scope: # 用initializer=xavier去初始化第一層
W = tf.get_variable( # 第一層有 W,b1,h
'W', [self.config.window_size * self.config.embed_size,
self.config.hidden_size])
b1 = tf.get_variable('b1', [self.config.hidden_size])
h = tf.nn.tanh(tf.matmul(window, W) + b1)
if self.config.l2: # L2 regularization for W
tf.add_to_collection('total_loss', 0.5 * self.config.l2 * tf.nn.l2_loss(W)) # 0.5 * self.config.l2 * tf.nn.l2_loss(W)
with tf.variable_scope('Layer2', initializer=xavier_weight_init()) as scope:
U = tf.get_variable('U', [self.config.hidden_size, self.config.label_size])
b2 = tf.get_variable('b2', [self.config.label_size])
y = tf.matmul(h, U) + b2
if self.config.l2:
tf.add_to_collection('total_loss', 0.5 * self.config.l2 * tf.nn.l2_loss(U))
output = tf.nn.dropout(y, self.dropout_placeholder) # 返回 output,兩個variable_scope都帶dropout
return output
關於 L2正則化 和 dropout 是什麼, 如何減小過擬合問題的,可以看這篇部落格,總結的簡單明瞭。
- 用 cross entropy 來計算 loss:
def add_loss_op(self, y):
cross_entropy = tf.reduce_mean( # 1.關鍵步驟:loss是用cross entropy定義的
tf.nn.softmax_cross_entropy_with_logits(y, self.labels_placeholder)) # y是模型預測值,計算cross entropy
tf.add_to_collection('total_loss', cross_entropy) # Stores value in the collection with the given name.
# collections are not sets, it is possible to add a value to a collection several times.
loss = tf.add_n(tf.get_collection('total_loss')) # Adds all input tensors element-wise. inputs: A list of Tensor with same shape and type
return loss
- 接著用 Adam Optimizer 把loss最小化:
def add_training_op(self, loss):
optimizer = tf.train.AdamOptimizer(self.config.lr)
global_step = tf.Variable(0, name='global_step', trainable=False)
train_op = optimizer.minimize(loss, global_step=global_step) # 2.關鍵步驟:用 AdamOptimizer 使 loss 達到最小,所以更關鍵的是 loss
return train_op
每一次訓練後,得到了最小化 loss 相應的 weights。
這樣,NER 這個分類問題就搞定了,當然為了提高精度等其他問題,還是需要查閱文獻來學習的。下一次先實現個 RNN。