淺談分詞算法(5)基於字的分詞方法(bi-LSTM)

前言

很早便規劃的淺談分詞算法,總共分爲了五個部分,想聊聊本身在各類場景中使用到的分詞方法作個總結,種種事情一直拖到如今,今天抽空趕忙將最後一篇補上。前面幾篇博文中咱們已經闡述了不論分詞、詞性標註亦或NER,均可以抽象成一種序列標註模型,seq2seq,就是將一個序列映射到另外一個序列,這在NLP領域是很是常見的,由於NLP中語序、上下文是很是重要的,那麼判斷當前字或詞是什麼,咱們必須回頭看看以前說了什麼,甚至以後說了什麼,這也符合人類在閱讀理解時的習慣。因爲抽象成了Seq2Seq的模型,那麼咱們即可以套用相關模型來求解,好比HMM、CRF以及深度中的RNN,本文咱們就來聊聊LSTM在分詞中的應用,以及使用中的一些trick,好比如何添加字典等。git

目錄

淺談分詞算法(1)分詞中的基本問題
淺談分詞算法(2)基於詞典的分詞方法
淺談分詞算法(3)基於字的分詞方法(HMM)
淺談分詞算法(4)基於字的分詞方法(CRF)
淺談分詞算法(5)基於字的分詞方法(LSTM)github

循環神經網絡

在以前的博文馬里奧AI實現方式探索 ——神經網絡+加強學習,我闡述了關於神經網絡的歷程,以及最近這波人工智能浪潮的起始CNN,即卷積神經網絡的概念。卷積神經網絡給圖像領域帶來了質的飛越,也將以前由李飛飛教授創建的ImageNet比賽提高到了新的高度,圖像識別領域,計算機第一次超越了人類,從而引爆了最近兩三年來對人工智能、深度學習的持續關注。
當CNN在圖像領域火爆以後,天然做爲人工智能三大領域之一的NLP,也很快拿來使用,即著名的Text-CNN,你們感興趣的能夠去看看這篇論文Convolutional Neural Networks for Sentence Classification,對NLP領域也具備重要的里程碑意義,如今引用量也達到了3436。
可是CNN有個比較嚴重的問題是,其沒有序列的概念在裏面,若是咱們將一個句子作好embedding丟到CNN中作分類模型,那麼CNN更多的是將這個句子看作一個詞袋(bag-of-words bag),這樣在NLP領域重要的語序信息就丟失了,那麼咱們便引出了RNN,即循環神經網絡或說遞歸神經網絡(這裏值得注意的是,若是是對語句作分類模型,那麼用CNN進行不一樣kernel的卷積,而後拼接是能夠提取到一些語序信息,這其中也涉及到各類變種的CNN,你們能夠多查查資料)。
對於循環神經網絡,其實與CRF、HMM有不少共通之處,對於每個輸入\(x_t\),咱們經過網絡變換都會獲得一個狀態\(h_t\),對於一個序列來講,每個token(能夠是字也能夠是詞,在分詞時是字)都會進入網絡迭代,注意網絡中的參數是共享的。這裏不可免俗的放上經典圖像吧:

這裏將循環神經網絡展開,就是後面那樣。你們注意下圖中的\(A\),在RNN中就是一個比較簡單的前饋神經網絡,在RNN中會有一個嚴重的問題,就是當序列很長的時候,BP算法在反饋時,梯度會趨於零,即所謂的梯度消失(vanishing gradient)問題,這便引出了LSTM(Long Short Term Memory)。
LSTM本質上仍是循環神經網絡,只不過呢它把上面咱們提到的\(A\)換了換,加了三個門,其實就是關於向量的幾個變換表達式,來規避這種梯度消失問題,使得LSTM的邏輯單元可以更好的保存序列信息,一樣不可免俗上下面這張經典的圖片:

圖中對應了四個表達式以下:
遺忘門:
\[f_t=\sigma (W_f\cdot [h_{t-1},x_t]+b_f\]
輸入門:
\[i_t=\sigma (W_i\cdot [h_{t-1},x_t]+b_i\]
\[\widetilde{C}=tanh(W_C\cdot [h_{t-1},x_t]+b_C\]
狀態更新:
\[C_t=f_t*C_{t-1}+i_t*\widetilde{C}_t\]
輸出門:
\[O_t=\sigma (W_o[h_{t-1},x_t]+b_o)\]
\[h_t=O_t*tanh(C_t)\]
通常呢LSTM都是一個方向將序列循環輸入到網絡之中,然而有時候咱們須要兩頭關注序列的信息,這樣便引出了Bi-LSTM,即雙向LSTM,很簡單,就是對於一個序列,咱們有兩個LSTM網絡,一個正向輸入序列,一個反向輸入序列,而後將輸出的state拼接在一塊兒,供後續使用。
到這裏咱們簡單的說了下關於循環神經網絡的事情,下面咱們看下在分詞中應用LSTM算法

基於LSTM的分詞

前文以及以前的系列博文,咱們已經熟悉分詞轉換爲Seq2Seq的思路,那麼對於LSTM,咱們須要作的是將一串句子映射成爲Embedding,而後逐個輸出到網絡中,獲得狀態輸出,進行序列標註。咱們採用TensorFlow來開發。網絡

Embedding

關於Embedding,咱們能夠直接下載網上公開的Wiki數據集訓練好的Embedding,通常維度是100,也能夠本身根據場景,利用Word2Vec、Fasttext等訓練本身的Embedding。app

數據預處理

其實深度的好多模型已經很成熟,最麻煩的是數據的預處理,在數據預處理階段核心要作的是將序列映射到Embedding文件對應的id序列,而且按照Batch來切分,通常根據數據集的大小會設置6四、12八、256等不一樣的batch大小,在向網絡輸入數據,進行epoch迭代時,注意進行必要的shuffle操做,對於結果提升頗有用,shuffle相似以下:dom

def shuffle(char_data, tag_data, dict_data, len_data):
    char_data = np.asarray(char_data)
    tag_data = np.asarray(tag_data)
    dict_data = np.asarray(dict_data)
    len_data = np.asarray(len_data)
    idx = np.arange(len(len_data))
    np.random.shuffle(idx)

    return (char_data[idx], tag_data[idx], dict_data[idx], len_data[idx])

數據預處理我這裏很少講了,讀者能夠直接看github上開源的代碼,有問題隨時留言,我有空會來解答~ide

模型

咱們的核心模型結構也很簡單,將輸入的id序列,經過Tensorflow 的查表操做,映射成對應的Embedding,而後輸入到網絡中,獲得最終結果,進行Decode操做,獲得每一個字符的標記(BEMS),核心代碼以下:函數

def __init__(self, config, init_embedding = None):
        self.batch_size = batch_size = config.batch_size
        self.embedding_size = config.embedding_size # column
        self.hidden_size = config.hidden_size
        self.vocab_size = config.vocab_size # row

        # Define input and target tensors
        self._input_data = tf.placeholder(tf.int32, [batch_size, None], name="input_data")
        self._targets = tf.placeholder(tf.int32, [batch_size, None], name="targets_data")
        self._dicts = tf.placeholder(tf.float32, [batch_size, None], name="dict_data")
        self._seq_len = tf.placeholder(tf.int32, [batch_size], name="seq_len_data")

        with tf.device("/cpu:0"):
            if init_embedding is None:
                self.embedding = tf.get_variable("embedding", [self.vocab_size, self.embedding_size], dtype=data_type())
            else:
                self.embedding = tf.Variable(init_embedding, name="embedding", dtype=data_type())
        inputs = tf.nn.embedding_lookup(self.embedding, self._input_data)
        inputs = tf.nn.dropout(inputs, config.keep_prob)
        inputs = tf.reshape(inputs, [batch_size, -1, 9 * self.embedding_size])
        d = tf.reshape(self._dicts, [batch_size, -1, 16])
        self._loss, self._logits, self._trans = _bilstm_model(inputs, self._targets, d, self._seq_len, config)
        # CRF decode
        self._viterbi_sequence, _ = crf_model.crf_decode(self._logits, self._trans, self._seq_len)
        with tf.variable_scope("train_ops") as scope:
            # Gradients and SGD update operation for training the model.
            self._lr = tf.Variable(0.0, trainable=False)
            tvars = tf.trainable_variables()  # all variables need to train
            # use clip to avoid gradient explosion or gradients vanishing
            grads, _ = tf.clip_by_global_norm(tf.gradients(self._loss, tvars), config.max_grad_norm)
            self.optimizer = tf.train.AdamOptimizer(self._lr)
            self._train_op = self.optimizer.apply_gradients(
                zip(grads, tvars),
                global_step=tf.contrib.framework.get_or_create_global_step())

            self._new_lr = tf.placeholder(data_type(), shape=[], name="new_learning_rate")
            self._lr_update = tf.assign(self._lr, self._new_lr)
        self.saver = tf.train.Saver(tf.global_variables())

代碼邏輯很清晰,將各類輸入獲得後,embedding查表結束後,放入Bi-LSTM模型,獲得的結果進行Decode,這裏注意咱們用了一個CRF進行尾部Decode,通過試驗效果更好,其實直接上一層Softmax也ok。對於bilstm以下:學習

def _bilstm_model(inputs, targets, dicts, seq_len, config):
    '''
    @Use BasicLSTMCell, MultiRNNCell method to build LSTM model
    @return logits, cost and others
    '''
    batch_size = config.batch_size
    hidden_size = config.hidden_size
    vocab_size = config.vocab_size
    target_num = config.target_num  # target output number
    seq_len = tf.cast(seq_len, tf.int32)

    fw_cell = lstm_cell(hidden_size)
    bw_cell = lstm_cell(hidden_size)

    with tf.variable_scope("seg_bilstm"): # like namespace
        # we use only one layer
        (forward_output, backward_output), _ = tf.nn.bidirectional_dynamic_rnn(
            fw_cell,
            bw_cell,
            inputs,
            dtype=tf.float32,
            sequence_length=seq_len,
            scope='layer_1'
        )
        # [batch_size, max_time, cell_fw.output_size]/[batch_size, max_time, cell_bw.output_size]
        output = tf.concat(axis=2, values=[forward_output, backward_output])  # fw/bw dimension is 3
        if config.stack: # False
            (forward_output, backward_output), _ = tf.nn.bidirectional_dynamic_rnn(
                fw_cell,
                bw_cell,
                output,
                dtype=tf.float32,
                sequence_length=seq_len,
                scope='layer_2'
            )
            output = tf.concat(axis=2, values=[forward_output, backward_output])

        output = tf.concat(values=[output, dicts], axis=2)  # add dicts to the end
        # outputs is a length T list of output vectors, which is [batch_size*maxlen, 2 * hidden_size]
        output = tf.reshape(output, [-1, 2 * hidden_size + 16])
        softmax_w = tf.get_variable("softmax_w", [hidden_size * 2 + 16, target_num], dtype=data_type())
        softmax_b = tf.get_variable("softmax_b", [target_num], dtype=data_type())

        logits = tf.matmul(output, softmax_w) + softmax_b
        logits = tf.reshape(logits, [batch_size, -1, target_num])

    with tf.variable_scope("loss") as scope:
        # CRF log likelihood
        log_likelihood, transition_params = tf.contrib.crf.crf_log_likelihood(
            logits, targets, seq_len)
        loss = tf.reduce_mean(-log_likelihood)
    return loss, logits, transition_params

注意這裏作了兩次LSTM,並將結果拼接在一塊兒,而咱們的損失函數是關於crf_log_likelihood。

如何添加用戶詞典

咱們能夠看到在整個模型訓練好後,inference的過程是直接根據網絡權重進行的,那麼如何添加用戶詞典呢,這裏咱們採用的方式是將用戶詞典做爲額外的特徵拼接在Bi-LSTM結果的後面,就是在上面代碼的output = tf.concat(values=[output, dicts], axis=2) # add dicts to the end這裏,這個詞典會分紅四個部分,head、mid、single、tail,詞頭、詞中、詞尾以及單字詞,這樣對於用戶詞典是否出現用one-hot形式表達,不過實際使用過程當中也仍是存在切不出來的問題,讀者能夠考慮增強這部分特徵。

整個代碼我放在github上了,感興趣的讀者直接看源代碼,有問題歡迎留言~
https://github.com/xlturing/machine-learning-journey/tree/master/seg_bilstm

終於寫好這個系列了,以後謝謝最近在弄的Attention、Transformer以及BERT這一套在文本分類中的應用哈,歡迎你們交流。

相關文章
相關標籤/搜索