前言
目前 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)
————-推荐阅读————
我的2017文章汇总——机器学习篇
我的2017文章汇总——Java及中间件
我的2017文章汇总——深度学习篇
我的2017文章汇总——JDK源码篇
我的2017文章汇总——自然语言处理篇
我的2017文章汇总——Java并发篇
跟我交流,向我提问:
公众号的菜单已分为“读书总结”、“分布式”、“机器学习”、“深度学习”、“NLP”、“Java深度”、“Java并发核心”、“JDK源码”、“Tomcat内核”等,可能有一款适合你的胃口。
为什么写《Tomcat内核设计剖析》
欢迎关注: