浅谈分词算法(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这一套在文本分类中的应用哈,欢迎你们交流。