前言

目前 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 = ['', '', ''] + 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_sizedef 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) + blogits = 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)

相关文章