前言
目前 NLP 領域的很多工基本都會朝深度學習、注意力模型、半監督等方向發展,而且確實也取得了更好的效果,而有些也會把深度學習和傳統機器學習結合起來,都能有不錯的效能提升。這裡講一個用深度學習和機器學習結合來做分詞。
關於分詞
分詞就是將一句話按照最合理的單詞分開,英語一般就沒有這個麻煩,因為英語詞語都是空格隔開的,而中文就需要做額外處理。分詞任務一般是nlp其他任務的基礎,分詞分得好不好將直接對後面的其他任務產生很大的影響。
傳統做法
在此之前,先了解分詞的一般做法:
- 基於詞典正向最大匹配法,很簡單的從左往右的規則匹配,類似的還有逆向最大匹配法。
- 基於詞典最小切分法,通用是規則匹配,它使句子儘可能少單詞數量。
- 基於n元文法的分詞法,主要是通過大量語料統計單詞或字的轉換概率,並通過動態歸劃的演算法算出最後最優的分詞序列。
- 隱馬爾科夫模型分詞法,主要通過大量語料的觀測序列和狀態學習到引數,然後對觀測序列進行隱含狀態推測,也是需要解碼的過程,解碼完成及分詞完成。
- 條件隨機場分詞法,通過大量語料學習到引數,這裡需要設計很多特徵函式和轉移函式,條件隨機場分詞準確率很高,它比隱馬爾可夫的精度高很多,因為條件隨機場考慮了上下文。
關於LSTM
LSTM 是迴圈神經網路的一種變種,是處理序列的小能手,具體可以看前面的文章《LSTM神經網路》,而雙向 LSTM 可以看前面文章《雙向迴圈神經網路及TensorFlow實現》。
關於CRF
CRF是一種概率無向圖模型,也是處理序列的小能手,具體可以看前面的文章《機器學習之條件隨機場(CRF)》。
LSTM+CRF
LSTM 和 CRF 我們都瞭解了,單獨使用它們都挺好理解,但如何將它們結合起來是我們更關注的。
其實如果沒有 CRF 參與其實也是可以完成任務的,我們說單向 LSTM 網路因為沒考慮上下文,所以引入了雙向 LSTM 網路,此時每個詞經過詞嵌入層再進入前向和後向迴圈神經網路,這時輸出能得到標籤的概率。如下圖,
在沒有 CRF 參與的時候可能會存在一個小缺陷,它沒辦法約束標籤的特徵,比如某標籤到另外一標籤的轉換概率。如果有標籤的特徵就能進一步提高學習能力。
所以最終的網路結構圖如下,第一層為詞嵌入層,第二層為雙向迴圈神經網路層,正向網路的輸出和反向網路的輸出分別作為輸入輸到一個隱含層,最後再輸入到 CRF 層。
分詞標籤
我們可以設定狀態值集合S為(B, M, E,S),分別代表每個狀態代表的是該字在詞語中的位置,B代表該字是詞語中的起始字,M代表是詞語中的中間字,E代表是詞語中的結束字,S則代表是單字成詞。
核心程式碼
https://github.com/sea-boat/nlp_lab/tree/master/bilstm_crf_seg
建立詞彙
def create_vocab(text):
unique_chars = [`<NUM>`, `<UNK>`, `<ENG>`] + list(set(text))
print(unique_chars)
vocab_size = len(unique_chars)
vocab_index_dict = {}
index_vocab_dict = {}
for i, char in enumerate(unique_chars):
vocab_index_dict[char] = i
index_vocab_dict[i] = char
return vocab_index_dict, index_vocab_dict, vocab_size
複製程式碼
處理字元首先就是需要建立包含語料中所有的詞的詞彙,需要一個從字元到詞彙位置索引的詞典,也需要一個從位置索引到字元的詞典。
詞彙儲存及讀取
def load_vocab(vocab_file):
with codecs.open(vocab_file, `r`, encoding=`utf-8`) as f:
vocab_index_dict = json.load(f)
index_vocab_dict = {}
vocab_size = 0
for char, index in iteritems(vocab_index_dict):
index_vocab_dict[index] = char
vocab_size += 1
return vocab_index_dict, index_vocab_dict, vocab_size
def save_vocab(vocab_index_dict, vocab_file):
with codecs.open(vocab_file, `w`, encoding=`utf-8`) as f:
json.dump(vocab_index_dict, f, indent=2, sort_keys=True)
複製程式碼
第一次建立詞彙後我們需要將它儲存下來,後面在使用模型預測時需要讀取該詞彙,如果不儲存而每次都建立的話則可能導致詞彙順序不同。
批量遍歷器
def batch_yield(data, batch_size, vocab, tag2label, shuffle=False):
if shuffle:
random.shuffle(data)
seqs, labels = [], []
for (sent_, tag_) in data:
sent_ = sentence2id(sent_, vocab)
label_ = [tag2label[tag] for tag in tag_]
if len(seqs) == batch_size:
yield seqs, labels
seqs, labels = [], []
seqs.append(sent_)
labels.append(label_)
if len(seqs) != 0:
yield seqs, labels
複製程式碼
構建圖
建立需要的佔位符,分別為輸入佔位符、標籤佔位符、序列長度佔位符、dropout佔位符和學習率佔位符。
word_ids = tf.placeholder(tf.int32, shape=[None, None], name="word_ids")
labels = tf.placeholder(tf.int32, shape=[None, None], name="labels")
sequence_lengths = tf.placeholder(tf.int32, shape=[None], name="sequence_lengths")
dropout_pl = tf.placeholder(dtype=tf.float32, shape=[], name="dropout")
lr_pl = tf.placeholder(dtype=tf.float32, shape=[], name="lr")
複製程式碼
建立嵌入層,
with tf.variable_scope("words"):
_word_embeddings = tf.Variable(embeddings, dtype=tf.float32, trainable=True, name="_word_embeddings")
word_embeddings = tf.nn.embedding_lookup(params=_word_embeddings, ids=word_ids, name="word_embeddings")
word_embeddings = tf.nn.dropout(word_embeddings, dropout_pl)
複製程式碼
建立向前 LSTM 網路和向後 LSTM 網路,
cell_fw = LSTMCell(hidden_dim)
cell_bw = LSTMCell(hidden_dim)
(output_fw_seq, output_bw_seq), _ = tf.nn.bidirectional_dynamic_rnn(cell_fw=cell_fw, cell_bw=cell_bw,
inputs=word_embeddings,
sequence_length=sequence_lengths,
dtype=tf.float32)
複製程式碼
將兩個方向的網路輸出連線起來並輸入到一個隱含層,得到預測結果,
output = tf.concat([output_fw_seq, output_bw_seq], axis=-1)
output = tf.nn.dropout(output, dropout_pl)
W = tf.get_variable(name="W", shape=[2 * hidden_dim, label_num],
initializer=tf.contrib.layers.xavier_initializer(),
dtype=tf.float32)
b = tf.get_variable(name="b", shape=[label_num], initializer=tf.zeros_initializer(), dtype=tf.float32)
s = tf.shape(output)
output = tf.reshape(output, [-1, 2 * hidden_dim])
pred = tf.matmul(output, W) + b
logits = tf.reshape(pred, [-1, s[1], label_num])
labels_softmax_ = tf.argmax(logits, axis=-1)
labels_softmax_ = tf.cast(labels_softmax_, tf.int32)
複製程式碼
最後再新增一個 crf 層,
log_likelihood, transition_params = crf_log_likelihood(inputs=logits, tag_indices=labels,
sequence_lengths=sequence_lengths)
複製程式碼
定義損失函式,
loss = -tf.reduce_mean(log_likelihood)
複製程式碼
使用 adam 優化器來優化。
with tf.variable_scope("train_step"):
global_step = tf.Variable(0, name="global_step", trainable=False)
optim = tf.train.AdamOptimizer(learning_rate=lr_pl)
grads_and_vars = optim.compute_gradients(loss)
grads_and_vars_clip = [[tf.clip_by_value(g, -clip_grad, clip_grad), v] for g, v in grads_and_vars]
train_op = optim.apply_gradients(grads_and_vars_clip, global_step=global_step)
複製程式碼
————-推薦閱讀————
跟我交流,向我提問:
公眾號的選單已分為“讀書總結”、“分散式”、“機器學習”、“深度學習”、“NLP”、“Java深度”、“Java併發核心”、“JDK原始碼”、“Tomcat核心”等,可能有一款適合你的胃口。
歡迎關注: