天然语言处置惩罚入门6——RNN天生文本

[复制链接]
发表于 2025-9-30 00:14:11 | 显示全部楼层 |阅读模式
一、文本天生

我们在前面的文章中先容了LSTM,根据输入时序数据可以输出下一个大概性最高的数据,如果应用在笔墨上,就是根据输入的笔墨,可以猜测下一个大概性最高的笔墨。使用这个特点,我们可以用LSTM来天生文本。输入一个单词,做embedding处置惩罚后,再输入到LSTM,会输出备选单词的得分,颠末softmax得到概率,我们选择概率最高的单词作为输出。这种输出叫做确定性输出,如下图所示:

如果我们根据概率随机选择一个单词输出,这叫做概率性输出,类似于大语言模子中的temperature,当temperature高时,天生的厘革越多,确定性越低。下面的代码是根据RNN模子天生笔墨的代码,此中start_id代表天生开始的第一个单词编号,skip_ids代表必要过滤的单词,比如空大概数字等等,sample_size代表采样巨细,这里就是天生的单词的数目。
  1. class RnnlmGen(Rnnlm):
  2.     def generate(self, start_id, skip_ids=None, sample_size=100):
  3.         word_ids = [start_id] # 最终的单词编号列表
  4.         x = start_id # 第一个单词编号
  5.         # 如果生成的单词列表长度还小于sample_size就继续生成
  6.         while len(word_ids) < sample_size:
  7.             # 转变shape便于输入模型
  8.             x = np.array(x).reshape(1,1)
  9.             # 根据模型预测输出单词得分
  10.             score = self.predict(x)
  11.             # 得到概率
  12.             p = softmax(score.flatten())
  13.             # 根据概率选择输出的单词编号
  14.             sampled = np.random.choice(len(p), size=1, p=p)
  15.             if (skip_ids is None) or (sampled not in skip_ids):
  16.                 x = sampled
  17.                 word_ids.append(int(x))
  18.         return word_ids
  19.         
  20. corpus, word_to_id, id_to_word = load_data('train')
  21. vocab_size = len(word_to_id)
  22. corpus_size = len(corpus)
  23. model = RnnlmGen()
  24. # 设定start单词和skip单词
  25. start_word = 'you'
  26. start_id = word_to_id[start_word]
  27. skip_words = ['N','<unk>','$']
  28. skip_ids = [word_to_id[w] for w in skip_words]
  29. # 生成文本
  30. word_ids = model.generate(start_id, skip_ids)
  31. txt = ' '.join(id_to_word[i] for i in word_ids)
  32. txt = txt.replace(' <eos>','.\n')
  33. print(txt)
  34. # 输出:
  35. you freshman retreat teeth instantly enhanced university brands exceptionally affiliates
  36. various unfair leslie our assumes studies begin monitored bart leap reasonably gary poorer
  37. industry southeast cemetery tables epo supportive nervous sooner inc soybeans scientific
  38. expertise applying lufthansa introduction leventhal casting lights carries feared revamping
  39. solar sachs widen training reins moves industrials technologies extent diagnostic narcotics
  40. regularly literally hanover primarily reinsurance pro-life serve specifications fm jumbo
  41. penalty actions l.p. ann keenan princeton despite stuart arise instrumentation classic exposed
  42. violation dishonesty warner-lambert nicaragua infringed fantasy marcus portrait imported jordan
  43. spurring component perestroika undo remic sacrifice veterans arms-control postal relying
  44. homelessness quack voters
复制代码
可以看到输出的笔墨险些没有什么寄义,这是由于我们使用的模子是原始的LSTM,没有颠末练习,如果我们加载了上篇笔墨练习事后的模子BetterRnnlm后,结果会有显着提升。
  1. ... ...
  2. model = RnnlmGen()
  3. model.load_params('Rnnlm.pkl')
  4. ... ...
  5. # 输出:
  6. you place a short part of their nation 's relatively rumored air.
  7. far everyone agreed and yet as out next is a market to insist in  out of wohlstetter
  8.   is violates that on why it took the max for one. a caution on a chiefs of substantial
  9.   chips three-year firstsouth in the ratio for candidates more investors the tax in
  10.   medical lawsuits will be something about three to government simultaneous.
  11. a new market will n't be surprising these specify must very be exactly like economic
  12. houses to manufacturers competitors where he adds some error in not new
复制代码
二、序列到序列模子

用RNN天生文本,尚有一种更通用的用法,称为序列到序列的模子,也就是sequence to sequence。最范例的seq2seq应用就是呆板翻译,输入一串用某种语言体现的笔墨,输出用另一种笔墨体现的笔墨。别的范例的应用包罗:
主动择要,它是输入一个长文本,输出一个表达核心寄义的短文本;
问答体系:输入一个标题文本,输出一个答案文本;
谈天呆板人:输入人类的文本,输出呆板的文本;
算法学习:输入一串算法形貌,输出盘算答案;
图像笔墨天生:输入图像,这里图像也可以通过CNN等网络体现成一串向量,输出形貌图像的笔墨;
可以看出,seq2seq可以有两个模块构成,一个模块处置惩罚输入文本,一个模块天生输出文本,处置惩罚输入文本的模块我们称为Encoder,天生文本的模块我们称为Decoder。一样寻常来说,一个seq2seq就是由一个Encoder和一个Decoder归并在一起得到的。以书中的例子,日语翻译成英语为例的话,流程图如下:

下面我们以模拟一个加法的学习来实现这个seq2seq模子。这个模子的输入是一个三位数字以内的加法表达式,如“32+100”,输出是运算的结果,如“132”,编码器对“32+100”这个表达式拆分成“3”,“2”,“+”,“1”,“0”,“0”等几个字符,作为文本输入到编码器,得到隐蔽信息,解码器输入隐蔽信息以及“1”,“3”,“2”作为标签值,得到输出值,将输出值与“1”,“3”,“2”比力,得到丧失,举行反向流传,实现整个练习过程。
不外这里有几个必要注意的地方:由于输入到编码器中的加法表达式长度大概差异,以是必要办理这个标题,最方便的方法是padding,也就是在表达式的前后插入添补字符。如:

总体流程如下图所示。练习的时间接纳编码器息争码器练习,实际使用中,把编码器得到的隐蔽信息输出到天生器,天生结果。

代码实现是基于之前的LSTM代码底子之上的,着实和LSTM的代码构建有许多类似的地方,编码器根本就是一个平凡的LSTM,编码器代码:
  1. class Encoder:
  2.     def __init__(self, vocab_size, wordvec_size, hidden_size):
  3.         V,D,H = vocab_size, wordvec_size, hidden_size
  4.         rn = np.random.randn
  5.         embed_W = (rn(V,D)/100).astype('f')
  6.         lstm_Wx = (rn(D,4*H)/np.sqrt(D)).astype('f')
  7.         lstm_Wh = (rn(H,4*H)/np.sqrt(H)).astype('f')
  8.         lstm_b = np.zeros(4*H).astype('f')
  9.         self.embed = TimeEmbedding(embed_W)
  10.         self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful = False)
  11.         self.params = self.embed.params + self.lstm.params
  12.         self.grads = self.embed.grads + self.lstm.grads
  13.         self.hs = None
  14.         
  15.     def forward(self,xs):
  16.         xs = self.embed.forward(xs)
  17.         hs = self.lstm.forward(xs)
  18.         self.hs = hs
  19.         return self.hs[:,-1,:]
  20.    
  21.     def backward(self,dh):
  22.         dhs = np.zeros_like(self.hs)
  23.         dhs[:,-1,:] = dh
  24.         dout = self.lstm.backward(dhs)
  25.         dout = self.embed.backward(dout)
  26.         return dout
复制代码
解码器和编码器的区别就在于,解码器还要多输入一个隐蔽信息,而且正向流传输出多一个打分步调。generate是实际天生文本结果的函数,和前面所述天生文本的区别在于,这里是天生加法结果的,以是不消概率性输出,而接纳确定性输出,就是用argmax选择得分最高的输出,解码器代码:
  1. class Decoder:
  2.     def __init__(self, vocab_size, wordvec_size, hidden_size):
  3.         V,D,H = vocab_size, wordvec_size, hidden_size
  4.         rn = np.random.randn
  5.         embed_W = (rn(V,D)/100).astype('f')
  6.         lstm_Wx = (rn(D,4*H)/np.sqrt(D)).astype('f')
  7.         lstm_Wh = (rn(H,4*H)/np.sqrt(H)).astype('f')
  8.         lstm_b = np.zeros(4*H).astype('f')
  9.         affine_W = (rn(H,V)/np.sqrt(H)).astype('f')
  10.         affine_b = np.zeros(V).astype('f')
  11.         self.embed = TimeEmbedding(embed_W)
  12.         self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful = True)
  13.         self.affine = TimeAffine(affine_W, affine_b)
  14.         self.params, self.grads = [], []
  15.         for layer in (self.embed, self.lstm, self.affine):
  16.             self.params += layer.params
  17.             self.grads += layer.grads
  18.         
  19.     def forward(self, xs, h):
  20.         self.lstm.set_state(h)
  21.         out = self.embed.forward(xs)
  22.         out = self.lstm.forward(out)
  23.         score = self.affine.forward(out)
  24.         return score
  25.    
  26.     def backward(self, dscore):
  27.         dout = self.affine.backward(dscore)
  28.         dout = self.lstm.backward(dout)
  29.         dout = self.embed.backward(dout)
  30.         dh = self.lstm.dh
  31.         return dh
  32.    
  33.     def generate(self, h, start_id, sample_size):
  34.         sampled = []
  35.         sample_id = start_id
  36.         self.lstm.set_state(h)
  37.         for _ in range(sample_size):
  38.             x = np.array(sample_id).reshape((1,1))
  39.             out = self.embed.forward(x)
  40.             out = self.lstm.forward(out)
  41.             score = self.affine.forward(out)
  42.             sample_id = np.argmax(score.flatten())
  43.             sampled.append(int(sample_id))
  44.         return sampled
复制代码
基于上述编码器息争码器,构建seq2seq模子:
  1. class Seq2seq(BaseModel):
  2.     def __init__(self, vocab_size, wordvec_size, hidden_size):
  3.         V,D,H = vocab_size, wordvec_size, hidden_size
  4.         self.encoder = Encoder(V,D,H)
  5.         self.decoder = Decoder(V,D,H)
  6.         self.softmax = TimeSoftmaxWithLoss()
  7.         self.params = self.encoder.params + self.decoder.params
  8.         self.grads = self.encoder.grads + self.decoder.grads
  9.         
  10.     def forward(self, xs, ts):
  11.         # 样本从开始到倒数第二个,标签从第1个开始到最后一个
  12.         decoder_xs, decoder_ts = ts[:,:-1], ts[:,1:]
  13.         h = self.encoder.forward(xs)
  14.         score = self.decoder.forward(decoder_xs, h)
  15.         loss = self.softmax.forward(score, decoder_ts)
  16.         return loss
  17.    
  18.     def backward(self, dout=1):
  19.         dout = self.softmax.backward(dout)
  20.         dh = self.decoder.backward(dout)
  21.         dout = self.encoder.backward(dh)
  22.         return dout
  23.    
  24.     def generate(self, xs, start_id, sample_size):
  25.         h = self.encoder.forward(xs)
  26.         sampled = self.decoder.generate(h, start_id, sample_size)
  27.         return sampled
复制代码
接纳该模子练习25个epoch后,猜测准确度约在11%左右。一个改进办法是,将输入的表达式反转:

另一个改进方法是Peeky,它的特点就是把编码器传过来的隐蔽信息,都输入到解码器的每个节点中,而之前只有解码器的第一个节点吸取编码器传过来的隐蔽信息。

练习代码如下(完备代码可以参考书的附带源代码):
  1. # 读入数据集
  2. (x_train,t_train),(x_test,t_test) = load_data('addition.txt', seed=1984)
  3. char_to_id, id_to_char = get_vocab()
  4. x_train, x_test = x_train[:,::-1], x_test[:,::-1] # 反转
  5. # 设定超参数
  6. vocab_size = len(char_to_id)
  7. wordvec_size = 16
  8. hidden_size = 128
  9. batch_size = 128
  10. max_epoch = 25
  11. max_grad = 5.0
  12. # 生成模型/优化器/训练器
  13. model = PeekySeq2seq(vocab_size, wordvec_size, hidden_size)
  14. optimizer = Adam()
  15. trainer = Trainer(model, optimizer)
  16. acc_list = []
  17. for epoch in range(max_epoch):
  18.     trainer.fit(x_train, t_train, max_epoch=1, batch_size=batch_size, max_grad=max_grad)
  19.     correct_num = 0
  20.     for i in range(len(x_test)):
  21.         question, correct = x_test[[i]], t_test[[i]]
  22.         verbose = i < 10
  23.         correct_num += eval_seq2seq(model, question, correct, id_to_char, verbose)
  24.     acc = float(correct_num)/len(x_test)
  25.     acc_list.append(acc)
  26.     print('val acc %.3f%%' % (acc*100))
  27. acc_list3 = acc_list
  28. plt.plot([i for i in range(max_epoch)], [acc*100 for acc in acc_list3], label='peeky+reverse', c='r',linestyle='--',marker='o')
  29. plt.plot([i for i in range(max_epoch)], [acc*100 for acc in acc_list2], label='reverse', c='y',linestyle='-.',marker='>')
  30. plt.plot([i for i in range(max_epoch)], [acc*100 for acc in acc_list1], label='original', c='g',linestyle=':',marker='*')
  31. plt.xlabel('iterations')
  32. plt.ylabel('accuracy')
  33. plt.legend()
  34. plt.show()
复制代码
这里,我轻微修改了一下书中代码,我把三个精度图放在一起比力了。acc_list1代表原始seq2seq模子的练习精度,acc_list2代表输入反转后的模子练习精度,acc_list3代码输入反转而且到场Peeky后的模子练习精度。

可以看到,原始的seq2seq模子在练习25个epoch后,精度约莫11%,反转输入后,练习精度大概55%,再到场Peeky后,精度已经非常靠近100%了,一样寻常可以到达96%~98%之间。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表