自然语言处理入门练习(五):基于神经网络的语言模型(附代码)

    技术2024-07-25  12

    自然语言处理入门练习(五):基于神经网络的语言模型(附代码)

    目录

    自然语言处理入门练习(五):基于神经网络的语言模型(附代码)1. 前言2. 字符级模型2.1 纯粹字符级模型2.2 字符级输入模型 (Character-aware)2.3 其他字符级模型 3. 总结4.评价方法4.1 困惑度4.2 BLEU算法4.3 ROUGE算法 【实战任务】【核心代码】【完整代码github地址】【参考资料】

       语言建模一直均以分词为最小单位,即词级语言模型。后来研究者尝试在字符级别进行语言建模,提出了多种字符级的语言模型,其中最为成功是Y.Kim and et. al. (2015)提出的模型。字符级语言模型的优势在于能够解决未登录词的问题,并且能够将词语的形态信息引入语言模型,从而提升模型的性能。

    1. 前言

      最初,语言建模的研究主要集中在词级别的语言模型,而后研究者发现词级别的语言模型存在诸多缺点。在词级别的语言建模中,词典通常较大,一般在几万至几十万,导致语言模型的输出层计算量巨大;尽管字典如此巨大,但不管如何扩展字典,词级别的语言模型在应用中还是会遇到未登录词(Words Out of Vocabulary)的问题,一方面由于单词的数量本就巨大,又存在不同的变种,还有不断出现的新词,而未登录词是导致语言模型性能下降到重要因素之一;词级别的语言建模还忽略了词本身所包含的形态信息,比如在英文中的前后缀、词根等信息。针对词级别语言模型存在的诸多问题,研究者开始探索字符级的语言模型,并提出了多种字符级语言模型的方案,部分方案的结果并不理想,但也有些方案取得了很好的效果。

    2. 字符级模型

      字符级语言模型的研究也曾是深度学习语言建模的研究热点之一,也产生了许多研究成果,提出许多字符级的语言模型方案,本文着重分析其中的两种,然后简要地介绍其他的模型方案。

    2.1 纯粹字符级模型

      建立字符级语言模型的直接想法就是将文本作为字符序列来处理,即将词级别的语言模型中的词替换为字符,而模型的输入序列由词序列换为字符序列,这也是字符级模型研究的最初构思,其模型结构如图所示:

      研究结果显示,直接采用上述方案进行字符级语言模型的构建并不成功,训练异常的困难。I. Sutskever, J. Martens, G. Hinton (2011)]首先尝试通过神经网络实现字符级的语言模型,采用的是改进的循环神经网络(Recurrent Neural Network, RNN),并且提出了新训练策略,即Hessian-Free (HF)优化方法,该研究的主要目的也是验证该优化方法 。至于字符级语言模型的效果并不理想,字符级语言模型生成的文本虽然看起来似乎遵循一定的语法,但并不是很有说服力。A. Graves (2013)则是探索了循环神经网络在序列生成中的应用,其中包括利用字符级语言模型生成文本。A. Graves (2013)的研究结果指出字符级语言模型的训练比较困难,并且其性能也无法达到词级别模型的水平。A. Graves (2013)利用宾州树库(Penn Tree Bank, PTB)对比了词级别的语言模型与字符级语言模型的性能,并引入 K. Jim, C. Giles, and B. Horne (1996)提出的权重噪声的泛化方法,实验结果如下表所示:

      理论上,由于字符向量无论在数量上还是大小上都远小于词向量,输出层也存在相同的情况。如果两类模型的隐层采用相同的尺寸,那么字符级模型的容量将远小于词级别的模型。使得两者达到相同的容量,那么字符级模型的隐层应该远大于词级别模型。在A. Graves (2013)的实验中,两类模型采用了相同大小的隐层,此甚至似乎不太合理。但是由于隐层尺寸较大,并且训练数据量较小,该因素的影响可能并不太大。   另外,相对于词序列,当将文本作为字符序列处理的时候,序列的长度将显著的增加,一方面,时序上的计算次数将极大地增加;另一方面,对于十分长的序列,目前已有的神经网络并无法学习长的序列依赖关系,即使采用改进的循环神经网络,如长短期记忆(Long Short Term Memory, LSTM)循环神经网络或者门控单元(Gated Recurrent Unit, GRU)循环神经网络。

    2.2 字符级输入模型 (Character-aware)

      针对纯粹的字符级语言模型的研究结果并不理想,研究者就改变了思路,放弃了字符级语言模型的部分优点,选择了折中的方案。Y. Kim and et. al. (2015)提出了Character-aware的字符级语言模型,即只在模型的输入端采用字符级输入,而输出端仍然沿用词级模型,整个模型的结构如图所示:

      在Y. Kim and et. al. (2015)提出模型中,输入端将每个词当作字符序列进行处理,而后采用卷积网络 (Convolutional Neural Network, CNN) 在字符序列上进行一维卷积,从而得到分词的词向量。值得一提的是在每个分词的字符序列前后都需要添加标志字符序列开始和结束的字符,如分词language转化为字符序列为{,l,a,n,g,u,a,g,e,},其中{分别}表示分词的字符序列的开始和结束,如果去除这样的起始和结束标识,模型的性能将会有大幅的下降,甚至无法训练出有用的语言模型。获取了词向量之后的模型与词级别的模型基本相同,只是因为卷积层的引入(有时卷积层可能有多层),模型的层次变深,会导致模型的训练变得困难。于是,便引入了高速网络层,来改善网络的训练。高速网络由R. Srivastava, K. Greff and J. Schmidhuber提出用于训练深层网络,主要是通过引入了门机制,使得信息在网络层之间传输更为通畅。   Character-aware语言模型的PPL指标相对于词级别的语言模型有了显著的改善,也是最成功的字符级语言模型。Y. Kim and et. al. (2015)在宾州树库(Penn Tree Bank, PTB)对模型进行了测试,实验结果如下表所示:

      从实验结果上不难看出,相对词级别的语言模型,Character-aware语言模型的PPL指标有了显著的改善,尤其是当模型的规模较大时,Character-aware语言模型的参数也有所减少。Character-aware语言模型的PPL之所以能够有所改善,一方面解决了未登录词的问题,另一方面将词的形态信息也引入了模型,是的模型学习到更多的模式。因此,Character-aware语言模型实现了研究者对字符级语言模型的两点期待,但是对于第三点期待,Character-aware语言模型不仅没有实现,还使得语言模型的计算有所增加。相对于词向量,字符向量不仅数量更少,尺寸也小的多,一般为15(词向量的尺寸在100~500)。而模型的参数的数量是相当的,就意味着很多部分参数转移到了卷积网络层和高速网络层,而这两部分是每次训练或者预测都需要参与计算的。但由于卷积网络的高并行的特性,其计算时间并没有过多的增加。Character-aware语言模型之所没有完全取代词级别的语言模型,主要在于其模型比较复杂,而且计算时间有所增加,尤其在预测阶段。

    2.3 其他字符级模型

      相对前面讨论的两种字符级的语言模型,其他类型的字符级语言模型似乎并没有太多值得讨论的价值,此处将对其进行简要的介绍。R. Jozefowicz and et. al. (2016)对神经网络语言模型的局限性进行了研究,但是其研究的深度并不令人满意,此处不做深入的讨论,仅仅介绍一下研究中提出的两类字符级语言模型,如图所示:

      上图中左边所示的模型基本结构与Y. Kim and et. al. (2015)所设计的字符级语言模型类似,只是将输出层的权重矩阵替换为卷积网络输出的对应特征向量,称为CNN Softmax。右侧的模型则是对Y. Kim and et. al. (2015)的模型的输出层进行了修改,将原先的输出层替换为字符级的预测模型,即将原有隐层的输出,作为字符预测模型的输入,依次预测目标词的每个字符。在百万级词语料库(One Billion Word Bench, OBWB)的实验结果如下表所示:

      这两类字符级语言模型不仅结构更为复杂,其PPL指标也远不如词级别的语言模型以及Y. Kim and et. al. (2015)提出的字符级语言模型。

    3. 总结

      理想的字符级语言模型,即模型的输入与输出均为字符级,能够解决词级别语言模型的三大问题,分别是巨大的计算量、未登录词以及词形态的模式,但是由于诸多限制并不能完全实现,主要是目前的神经网络在长依赖的学习上还有许多不足。目前,效果比较好并且最受关注的字符级语言模型就是Y. Kim and et. al. (2015)所提出的模型,如图所示,然而由于计算量以及模型复杂度的问题,并没有完全取代词级别的语言模型,往往是其其他应用中,将字符级别的特征与词向量合并,作为模型的输入以提高模型在该任务中的性能。   以上针对字符级语言模型的讨论还是主要针对英文、德语、西班牙等字母为主的语言,而对于中文、韩文、日文等方块字为主的语言,字符级语言模型的研究比较少。直观上来看,输入和输出层均为字符的字符级语言模型似乎更适合这类语言,并且字符的数量远小于词的数量,并且能够有效地解决未登录词问题。但是词边界在语言模型中是非常重要的特征,能够帮助语言模型更好地学习到目标语言的模式。从Y. Kim and et. al. (2015)处理词的字符序列时,在字符序列两端添加起始与终止标识符就可以看出。

    4.评价方法

      构造一个序列生成模型后, 需要有一个度量来评价其好坏。

    4.1 困惑度

      给定一个测试文本集合,一个好的序列生成模型应该使得测试集合中句子的联合概率尽可能高。   困惑度( Perplexity):是信息论中的一个概念,可以用来衡量一个分布的不确定性。 对于离散随机变量𝑋 ∈ 𝒳, 其概率分布为𝑝(𝑥), 困惑度为

      其中𝑯(𝑝)为分布𝑝的熵。   困惑度也可以用来衡量两个分布之间差异。对于一个未知的数据分布𝑝𝑟(𝑥)和一个模型分布𝑝𝜃(𝑥), 我们从𝑝𝑟(𝑥)中采样出一组测试样本𝑥(1), ⋯ , 𝑥(𝑁), 模型分布𝑝𝜃(𝑥)的困惑度为

      其中 𝑯( ̃𝑝𝑟, 𝑝𝜃) 为样本的经验分布 𝑟̃𝑝 与模型分布 𝑝𝜃 之间的交叉熵, 也是所有样本上的负对数似然函数。   困惑度可以衡量模型分布与样本经验分布之间的契合程度。 困惑度越低则两个分布越接近。因此, 模型分布𝑝𝜃(𝑥)的好坏可以用困惑度来评价。   假设测试集合有 𝑁 个独立同分布的序列 。 我们可以用模型 𝑝𝜃(𝒙)对每个序列计算其概率 , 整个测试集的联合概率为

      模型𝑝𝜃(𝒙)的困惑度定义为

      其中 𝑇 = ∑𝑁 𝑛=1 𝑇𝑛 为测试数据集中序列的总长度。 可以看出, 困惑度为每个词条件概率𝑝𝜃(𝑥𝑡(𝑛)|𝒙(𝑛) 1∶(𝑡-1))的几何平均数的倒数。 测试集中所有序列的概率越大,困惑度越小, 模型越好。   假设一个序列模型赋予每个词出现的概率均等, 即 , 𝑛√∏𝑛 𝑡=1 𝑥𝑡。则该模型的困惑度为|𝒱|。 以英语为例, N元模型的困惑度范围一般为50 ∼ 1000。

    4.2 BLEU算法

      BLEU( BiLingual Evaluation Understudy) 算法是一种衡量模型生成序列和参考序列之间的 N 元词组( N-Gram) 重合度的算法, 最早用来评价机器翻译模型的质量, 目前也广泛应用在各种序列生成任务中。   令 𝒙 为从模型分布 𝑝𝜃 中生成的一个候选( Candidate) 序列, 𝒔(1), ⋯ , 𝒔(𝐾)为从真实数据分布中采集的一组参考( Reference) 序列, 𝒲 为从生成的候选序列中提取所有N元组合的集合, 这些N元组合的精度( Precision)

      其中𝑐𝑤(𝒙)是N元组合𝑤 在生成序列𝒙中出现的次数, 𝑐𝑤(𝒔(𝑘))是N元组合𝑤 在参考序列𝒔(𝑘) 中出现的次数。N元组合的精度𝑃𝑁(𝒙)是计算生成序列中的N元组合有多少比例在参考序列中出现。   由于精度只衡量生成序列中的 N 元组合是否在参考序列中出现, 生成序列越短, 其精度会越高, 因此可以引入长度惩罚因子(Brevity Penalty). 如果生成序列的长度短于参考序列, 就对其进行惩罚。

      其中𝑙𝑥 为生成序列𝒙的长度, 𝑙𝑠 为参考序列的最短长度。   BLEU算法是通过计算不同长度的N元组合( 𝑁 = 1, 2, ⋯) 的精度, 并进行几何加权平均而得到。

      其中𝑁′ 为最长N元组合的长度, 𝛼𝑁 为不同N元组合的权重, 一般设为 1/𝑁′。BLEU算法的值域范围是 [0, 1], 越大表明生成的质量越好。但是 BLEU 算法只计算精度, 而不关心召回率( 即参考序列里的N元组合是否在生成序列中出现)。

    4.3 ROUGE算法

      ROUGE( Recall-Oriented Understudy for Gisting Evaluation)算法最早应用于文本摘要领域。和 BLEU算法类似, 但ROUGE算法计算的是召回率( Recall)。   令𝒙为从模型分布𝑝𝜃 中生成的一个候选序列, 𝒔(1), ⋯ , 𝒔(𝐾) 为从真实数据分布中采样出的一组参考序列, 𝒲 为从参考序列中提取N元组合的集合, ROUGEN算法的定义为

      其中𝑐𝑤(𝒙)是N元组合𝑤 在生成序列𝒙中出现的次数, 𝑐𝑤(𝒔(𝑘))是N元组合𝑤 在参考序列𝒔(𝑘) 中出现的次数。

    【实战任务】

      基于RNN实现古诗词生成模型   RNN(循环神经网络)模型是基于当前的状态和当前的输入来对下一时刻做出预判。而LSTM(长短时记忆网络)模型则可以记忆距离当前位置较远的上下文信息。   在此,我们根据上述预判模型来进行 古诗词的生成模型训练。   首先,我们需要准备好古诗词的数据集:全唐诗共34646首。

    训练数据训练模型

      训练时每次取64首诗进行训练,即每次在列表内取64个数据,然后对其进行输出数据x,输出数据y进行赋值,y为正确的结果,用于训练。(需注意的是,由于模型的作用是对下一个字进行预测,所以y只是x的数据向前移动一个字)定义一个RNN模型,然后把数据代入进行训练,使用RNN进行训练的过程大约分为:   1、定义模型和结构。   2、0初始化当前状态。   3、输入数据进行ID到单词向量的转化。   4、输入数据和初始化状态代入模型进行训练,得到训练结果。   5、对训练结果加入一个全连接层得到最终输出。

    生成古诗使用模型生成诗句

    【核心代码】

    nlp_train.py:

    import collections import numpy as np import tensorflow as tf import os # -------------------------------数据预处理---------------------------# poetry_file = 'poetry.txt' # 诗集 poetrys = [] with open(poetry_file, "r", encoding='utf-8',) as f: for line in f: try: title, content = line.strip().split(':') content = content.replace(' ', '') if '_' in content or '(' in content or '(' in content or '《' in content or '[' in content: continue if len(content) < 5 or len(content) > 79: continue content = '[' + content + ']' poetrys.append(content) except Exception as e: pass # 按诗的字数排序 poetrys = sorted(poetrys, key=lambda line: len(line)) print('唐诗总数: ', len(poetrys)) # 统计每个字出现次数 all_words = [] for poetry in poetrys: all_words += [word for word in poetry] counter = collections.Counter(all_words) count_pairs = sorted(counter.items(), key=lambda x: -x[1]) words, _ = zip(*count_pairs) # 取前多少个常用字 words = words[:len(words)] + (' ',) # 每个字映射为一个数字ID word_num_map = dict(zip(words, range(len(words)))) # 把诗转换为向量形式,参考TensorFlow练习1 to_num = lambda word: word_num_map.get(word, len(words)) poetrys_vector = [list(map(to_num, poetry)) for poetry in poetrys] # [[314, 3199, 367, 1556, 26, 179, 680, 0, 3199, 41, 506, 40, 151, 4, 98, 1], # [339, 3, 133, 31, 302, 653, 512, 0, 37, 148, 294, 25, 54, 833, 3, 1, 965, 1315, 377, 1700, 562, 21, 37, 0, 2, 1253, 21, 36, 264, 877, 809, 1] # ....] # 每次取64首诗进行训练 batch_size = 64 n_chunk = len(poetrys_vector) // batch_size x_batches = [] y_batches = [] for i in range(n_chunk): start_index = i * batch_size end_index = start_index + batch_size batches = poetrys_vector[start_index:end_index] length = max(map(len, batches)) xdata = np.full((batch_size, length), word_num_map[' '], np.int32) for row in range(batch_size): xdata[row, :len(batches[row])] = batches[row] ydata = np.copy(xdata) ydata[:, :-1] = xdata[:, 1:] """ xdata ydata [6,2,4,6,9] [2,4,6,9,9] [1,4,2,8,5] [4,2,8,5,5] """ x_batches.append(xdata) y_batches.append(ydata) # ---------------------------------------RNN--------------------------------------# input_data = tf.placeholder(tf.int32, [batch_size, None]) output_targets = tf.placeholder(tf.int32, [batch_size, None]) # 定义RNN def neural_network(model='lstm', rnn_size=128, num_layers=2): if model == 'rnn': cell_fun = tf.nn.rnn_cell.BasicRNNCell elif model == 'gru': cell_fun = tf.nn.rnn_cell.GRUCell elif model == 'lstm': cell_fun = tf.nn.rnn_cell.BasicLSTMCell cell = cell_fun(rnn_size, state_is_tuple=True) cell = tf.nn.rnn_cell.MultiRNNCell([cell] * num_layers, state_is_tuple=True) initial_state = cell.zero_state(batch_size, tf.float32) with tf.variable_scope('rnnlm'): softmax_w = tf.get_variable("softmax_w", [rnn_size, len(words)+1]) softmax_b = tf.get_variable("softmax_b", [len(words)+1]) with tf.device("/cpu:0"): embedding = tf.get_variable("embedding", [len(words)+1, rnn_size]) inputs = tf.nn.embedding_lookup(embedding, input_data) outputs, last_state = tf.nn.dynamic_rnn(cell, inputs, initial_state=initial_state, scope='rnnlm') output = tf.reshape(outputs, [-1, rnn_size]) logits = tf.matmul(output, softmax_w) + softmax_b probs = tf.nn.softmax(logits) return logits, last_state, probs, cell, initial_state # 训练 def train_neural_network(): logits, last_state, _, _, _ = neural_network() targets = tf.reshape(output_targets, [-1]) loss = tf.contrib.legacy_seq2seq.sequence_loss_by_example([logits], [targets], [tf.ones_like(targets, dtype=tf.float32)], len(words)) cost = tf.reduce_mean(loss) learning_rate = tf.Variable(0.0, trainable=False) tvars = tf.trainable_variables() grads, _ = tf.clip_by_global_norm(tf.gradients(cost, tvars), 5) optimizer = tf.train.AdamOptimizer(learning_rate) train_op = optimizer.apply_gradients(zip(grads, tvars)) with tf.Session() as sess: sess.run(tf.global_variables_initializer()) saver = tf.train.Saver(tf.global_variables()) for epoch in range(10): sess.run(tf.assign(learning_rate, 0.002 * (0.97 ** epoch))) n = 0 for batche in range(n_chunk): train_loss, _ , _ = sess.run([cost, last_state, train_op], feed_dict={input_data: x_batches[n], output_targets: y_batches[n]}) n += 1 print(epoch, batche, train_loss) if epoch % 7 == 0: saver.save(sess, './poetry.module', global_step=epoch) # saver.save(sess, 'poetry.module') train_neural_network()

    结果:

    【完整代码github地址】

    https://github.com/chenlian-zhou/nlp/tree/master/nlp_induction_training/task5

    【参考资料】

    邱锡鹏的《神经网络与深度学习》https://github.com/FudanNLP/nlp-beginner
    Processed: 0.009, SQL: 9