赞
踩
语言模型根据已经出现的单词输出下一个出现的单词的概率分布。
一种可能的方法是选择概率最高的单词。在这种情况下,因为选择的是概率最高的单词,所以结果能唯一确定。也就是说,这是一种“确定性的”方法。
另一种方法是“概率性地”进行选择。根据概率分布进行选择,这样概率高的单词容易被选到,概率低的单词难以被选到。在这种情况下,被选到的单词(被采样到的单词)每次都不一样。
seq2seq 模型也称为 Encoder-Decoder 模型。顾名思义,这个模型有两个模块——Encoder(编码器)和 Decoder(解码器)。编码器对输入数据进行编码,解码器对被编码的数据进行解码。
编码器和解码器协作,将一个时序数据转换为另一个时序数据。另外,在这些编码器和解码器内部可以使用 RNN。
上图,编码器利用 RNN(LSTM)将时序数据转换为隐藏状态 h,是 LSTM 层的最后一个隐藏状态,其中编码了翻译输入文本所需的信息。说到底,编码就是将任意长度的文本转换为一个固定长度的向量。
上图,解码器的结构和 LSTM 完全相同。不过存在一点差异,就是 LSTM 层会接收向量 h。在之前的语言模型中,LSTM 层不接收任何信息(硬要说的话,也可以说LSTM 的隐藏状态接收“0 向量”)。这个唯一的、微小的改变使得普通的语言模型进化为可以驾驭翻译的解码器。
上图,seq2seq 由两个 LSTM 层构成,即编码器的 LSTM 和解码器的LSTM。此时,LSTM层的隐藏状态是编码器和解码器的“桥梁”。在正向传播时,编码器的编码信息通过 LSTM 层的隐藏状态传递给解码器;在反向传播时,解码器的梯度通过这个“桥梁”传递给编码器。
Encoder 类接收字符串,将其转化为向量 h。
上图,Encoder 类由 Embedding 层和 LSTM 层组成。Embedding 层将字符(字符 ID)转化为字符向量,然后将字符向量输入 LSTM 层。LSTM 层向右(时间方向)输出隐藏状态和记忆单元,向上输出隐藏状态。这里,因为上方不存在层,所以丢弃 LSTM 层向上的输出。在编码器处理完最后一个字符后,输出 LSTM 层的隐藏状态 h。然后,这个隐藏状态 h 被传递给解码器。
class Encoder: def __init__(self, vocab_size, wordvec_size, hidden_size): """词汇量、字符向量维数、LSTM隐藏状态维数""" V, D, H = vocab_size, wordvec_size, hidden_size rn = np.random.randn embed_W = (rn(V, D) / 100).astype('f') lstm_Wx = (rn(D, 4 * H) / np.sqrt(D)).astype('f') lstm_Wh = (rn(H, 4 * H) / np.sqrt(H)).astype('f') lstm_b = np.zeros(4 * H).astype('f') self.embed = TimeEmbedding(embed_W) self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=False) self.params = self.embed.params + self.lstm.params self.grads = self.embed.grads + self.lstm.grads self.hs = None def forward(self, xs): xs = self.embed.forward(xs) hs = self.lstm.forward(xs) self.hs = hs return hs[:, -1, :] def backward(self, dh): dhs = np.zeros_like(self.hs) dhs[:, -1, :] = dh dout = self.lstm.backward(dhs) dout = self.embed.backward(dout) return dout
Decoder 类接收 Encoder 类输出的 h,输出目标字符串。
上图,解码器在学习时的层结构。这里使用了监督数据 _62 进行学习,此时输入数据是 [‘_’, ‘6’, ‘2’, ’ '],对应的输出是 [‘6’, ‘2’, ’ ', ’ ']。
上图,解码器生成字符串的过程。
class Decoder: def __init__(self, vocab_size, wordvec_size, hidden_size): """词汇量、字符向量维数、LSTM隐藏状态维数""" V, D, H = vocab_size, wordvec_size, hidden_size rn = np.random.randn embed_W = (rn(V, D) / 100).astype('f') lstm_Wx = (rn(D, 4 * H) / np.sqrt(D)).astype('f') lstm_Wh = (rn(H, 4 * H) / np.sqrt(H)).astype('f') lstm_b = np.zeros(4 * H).astype('f') affine_W = (rn(H, V) / np.sqrt(H)).astype('f') affine_b = np.zeros(V).astype('f') self.embed = TimeEmbedding(embed_W) self.lstm = TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=True) self.affine = TimeAffine(affine_W, affine_b) self.params, self.grads = [], [] for layer in (self.embed, self.lstm, self.affine): self.params += layer.params self.grads += layer.grads def forward(self, xs, h): """学习时使用""" self.lstm.set_state(h) out = self.embed.forward(xs) out = self.lstm.forward(out) score = self.affine.forward(out) return score def backward(self, dscore): dout = self.affine.backward(dscore) dout = self.lstm.backward(dout) dout = self.embed.backward(dout) dh = self.lstm.dh return dh def generate(self, h, start_id, sample_size): """生成时使用 从编码器接收的隐藏状态 h、最开始输入的字符ID start_id 、生成的字符数量 sample_size """ sampled = [] sample_id = start_id self.lstm.set_state(h) for _ in range(sample_size): x = np.array(sample_id).reshape((1, 1)) out = self.embed.forward(x) out = self.lstm.forward(out) score = self.affine.forward(out) sample_id = np.argmax(score.flatten()) sampled.append(int(sample_id)) return sampled
class Seq2seq(BaseModel): def __init__(self, vocab_size, wordvec_size, hidden_size): V, D, H = vocab_size, wordvec_size, hidden_size self.encoder = Encoder(V, D, H) self.decoder = Decoder(V, D, H) self.softmax = TimeSoftmaxWithLoss() self.params = self.encoder.params + self.decoder.params self.grads = self.encoder.grads + self.decoder.grads def forward(self, xs, ts): decoder_xs, decoder_ts = ts[:, :-1], ts[:, 1:] h = self.encoder.forward(xs) score = self.decoder.forward(decoder_xs, h) loss = self.softmax.forward(score, decoder_ts) return loss def backward(self, dout=1): dout = self.softmax.backward(dout) dh = self.decoder.backward(dout) dout = self.encoder.backward(dh) return dout def generate(self, xs, start_id, sample_size): h = self.encoder.forward(xs) sampled = self.decoder.generate(h, start_id, sample_size) return sampled
这里需要做的只是将 Encoder 类和 Decoder 类连接在一起,然后使用 Time Softmax with Loss 层计算损失。
在许多情况下,使用这个技巧后,学习进展得更快,最终的精度也有提高。
# 读入数据集
(x_train, t_train), (x_test, t_test) = sequence.load_data('addition.txt')
...
x_train, x_test = x_train[:, ::-1], x_test[:, ::-1]
...
为什么反转数据后,学习进展变快,精度提高了呢?
虽然理论上不是很清楚,但是直观上可以认为,反转数据后梯度的传播可以更平滑。比如,考虑将“我是一只猫”翻译成“I am a cat”这一问题,单词“我”和单词“I”之间有转换关系。此时,从“我”到“I”的路程必须经过“是”“一”“只”“猫”这 3 个单词的 LSTM 层。因此,在反向传播时,梯度从“I”抵达“我”,也要受到这个距离的影响。如果反转输入语句,也就是变为“猫只一是我”,结果会怎样呢?此时,“我”和“I”彼此相邻,梯度可以直接传递。如此,因为通过反转,输入语句的开始部分和对应的转换后的单词之间的距离变近(这样的情况变多),所以梯度的传播变得更容易,学习效率也更高。不过,在反转输入数据后,单词之间的“平均”距离并不会发生改变。
编码器将输入语句转换为固定长度的向量 h,这个 h 集中了解码器所需的全部信息。也就是说,它是解码器唯一的信息源。但是,当前的 seq2seq 只有最开始时刻的 LSTM 层利用了 h。我们能更加充分地利用这个 h 吗?
上图,将编码器的输出 h 分配给所有时刻的 Affine 层和 LSTM 层。之前 LSTM 层专用的重要信息 h 现在在多个层(在这个例子中有 8 个层)中共享了。重要的信息不是一个人专有,而是多人共享,这样我们或许可以做出更加正确的判断。有两个向量同时被输入到了 LSTM 层和 Affine 层,这实际上表示两个向量的拼接(concatenate)。
继续阅读:
《深度学习进阶:自然语言处理(第1章)》-读书笔记
《深度学习进阶:自然语言处理(第2章)》-读书笔记
《深度学习进阶:自然语言处理(第3章)》-读书笔记
《深度学习进阶:自然语言处理(第4章)》-读书笔记
《深度学习进阶:自然语言处理(第5章)》-读书笔记
《深度学习进阶:自然语言处理(第6章)》-读书笔记
《深度学习进阶:自然语言处理(第7章)》-读书笔记
《深度学习进阶:自然语言处理(第8章)》-读书笔记
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。