pytorch官方教程3 translation with seq2seq and attention 拓展(3)

本文深入探讨了深度学习中Attention机制的原理与实现,包括Bahdanau和Luong两种Attention模型的详细解析。通过实例展示了如何将Attention机制应用于Seq2Seq模型中,提高模型的性能。

前言

该代码来源:《深度学习理论与实战:基础篇》
的ch4章节,编著:李理。
github,第一篇探索attention原理时发现的大神。
代码形式与官网大致相似,也有优化的地方。

Variable

可以训练的PyTorch模块要求输入是Variable而不是Tensor。Variable除了包含Tensor的内容之外,它还会跟踪计算图的状态,从而可以进行自动梯度的求值。

def variable_from_sentence(lang, sentence):
    indexes = indexes_from_sentence(lang, sentence)
    indexes.append(EOS_token)
    var = Variable(torch.LongTensor(indexes).view(-1, 1))

    if USE_CUDA: var = var.cuda()
    return var

官方

def tensorFromSentence(lang, sentence):
    indexes = indexesFromSentence(lang,sentence)
    indexes.append(EOS_token)
    return torch.tensor(indexes, dtype = torch.long, device=device).view(-1,1)

看过莫烦python就知道,这是旧版本的pytorch,不过通过对比可以学习不同的表达方式,而且加深印象。

EncoderRNN

RNN对于输入的每一个词都输出一个向量和一个隐状态,这个隐状态会作为下一个时刻的输入。
这里只显示增加的地方:多层RNN

class EncoderRNN(nn.Module):
    def __init__(self, input_size, hidden_size, n_layers=1):
        super(EncoderRNN, self).__init__()
        ...
        self.n_layers = n_layers
		...
        self.gru = nn.GRU(hidden_size, hidden_size, n_layers)   
    def forward(self, word_inputs, hidden):
        # 注意:和名字分类不同,我们这里一次处理完一个输入的所有词,而不是for循环每次处理一个词。
        # 两者的效果是一样的,但是一次处理效率更高。
        seq_len = len(word_inputs)
        embedded = self.embedding(word_inputs).view(seq_len, 1, -1)
        output, hidden = self.gru(embedded, hidden)
        return output, hidden

    def init_hidden(self):
        hidden = Variable(torch.zeros(self.n_layers, 1, self.hidden_size))
        if USE_CUDA: hidden = hidden.cuda()
        return hidden

官方

class EncoderRNN(nn.Module):
	def __init__(self,input_size,hidden_size):
        super(EncoderRNN, self).__init__()
        ...
         self.gru = nn.GRU(hidden_size, hidden_size)
    def forward(self, input, hidden):
        embedded = self.embedding(input).view(1,1,-1)
        output, hidden = self.gru(embedded,hidden)
        return output,hidden
    def initHidden(self):
        return torch.zeros(1,1,self.hidden_size, device=device)

Luong attention

Bahdanau attention模型是通过 s i − 1 s_{i-1} si1 h j h_j hj计算 a i j a_{ij} aij,也就是当前的注意力权重依赖与前一个状态,而这里的注意力依赖与decoder当前的隐状态和encoder所有隐状态。
a t ( s ) = a l i g n ( h t , h ˉ s ) = e x p ( s c o r e ( h t , h ˉ s ) ) ∑ s ′ e x p ( s c o r e ( h t , h ˉ s ′ ) ) a_t(s) = align(h_t, \bar h_s) = \dfrac{exp(score(h_t, \bar h_s))}{\sum_{s'} exp(score(h_t, \bar h_{s'}))} at(s)=align(ht,hˉs)=sexp(score(ht,hˉs))exp(score(ht,hˉs))

"score"函数会比较两个hidden state的”相似度“,可以是两个向量的内积,也可以是 h s ′ h_{s'} hs做一个线性变换之后和 h t h_t ht的内积,也可以是把两个向量拼接起来然后做一个线性变换,然后和一个参数 v a v_a va(这个参数是学习出来的)的内积: h t h_t ht是decoder的, h s ′ h_{s'} hs是encoder的

s c o r e ( h t , h ˉ s ) = { h t ⊤ h ˉ s d o t h t ⊤ W a h ˉ s g e n e r a l v a ⊤ W a [ h t ; h ˉ s ] c o n c a t score(h_t, \bar h_s) = \begin{cases} h_t ^\top \bar h_s & dot \\ h_t ^\top \textbf{W}_a \bar h_s & general \\ v_a ^\top \textbf{W}_a [ h_t ; \bar h_s ] & concat \end{cases} score(ht,hˉs)=hthˉshtWahˉsvaWa[ht;hˉs]dotgeneralconcat

这个模块的输入总是decoder的隐状态和encoder的所有输出。
这有一篇,之前提过,batch不为1。本篇也有使用batch的版本,后续再比较。

class Attn(nn.Module):
    def __init__(self, method, hidden_size, max_length=MAX_LENGTH):
        super(Attn, self).__init__()
        self.method = method
        self.hidden_size = hidden_size
        if self.method == 'general':
            self.attn = nn.Linear(self.hidden_size, hidden_size)
        elif self.method == 'concat':
            self.attn = nn.Linear(self.hidden_size * 2, hidden_size)
            self.other = nn.Parameter(torch.FloatTensor(1, hidden_size))

    def forward(self, hidden, encoder_outputs):
        seq_len = len(encoder_outputs)
        # 创建变量来存储注意力能量
        attn_energies = Variable(torch.zeros(seq_len))
        if USE_CUDA: attn_energies = attn_energies.cuda()

        # 计算
        for i in range(seq_len):
            attn_energies[i] = self.score(hidden, encoder_outputs[i])
		# (1,1,seq_len)
        return F.softmax(attn_energies, 0).unsqueeze(0).unsqueeze(0)
    
    def score(self, hidden, encoder_output):
        if self.method == 'dot':
            energy = hidden.view(-1).dot(encoder_output.view(-1))
            return energy
        elif self.method == 'general':
            energy = self.attn(encoder_output)
            energy = hidden.view(-1).dot(energy.view(-1))
            return energy
        elif self.method == 'concat':
            energy = self.attn(torch.cat((hidden, encoder_output), 1))
            energy = self.other.view(-1).dot(energy.view(-1))
            return energy

nn.Parameter解释
这一篇的attention计算:

		if self.method == 'general':
        	self.attn = torch.nn.Linear(self.hidden_size, hidden_size)
    def forward(self, decoder_output, encoder_output):     	
	# encoder_output的shape=[max(length), batch, hidden_size]
    # decoder_hidden的shape=[1, batch, hidden_size]
    	#energy [max(length), batch]
    	energy = self.general_score(decoder_output, encoder_output)
		# energy.shape = [batch, max(length)]
        energy = energy.t()    	
	def general_score(self, decoder_output, encoder_output):
        attn_general = self.attn(encoder_output) #nn.Linear只全连接最后一维
        #[1, batch, hidden_size] * [max(length), batch, hidden_size]应该是扩展到其他维度,专业名词broadcast
        #sum之后[max(length), batch]
        score = torch.sum(decoder_output * attn_general, dim=2)#
        return score

AttnDecoderRNN

它会把Attn模块放到RNN(decoder)之后用来计算注意力的权重,并且用它来计算context向量。

GRU的n_layers,层数不影响,因为
(1)我们只需要encoder的output,encoder多少层也只有一个output
(2)使用decoder的output去和encoder的output进行attention,decoder多少层也只有一个output。
(3)不需要padding到max_length,attn_weights长度就为encoder的seq_len,每次只输入一句话。

class AttnDecoderRNN(nn.Module):
    def __init__(self, attn_model, hidden_size, output_size, n_layers=1, dropout_p=0.1):
        super(AttnDecoderRNN, self).__init__()
        # 保存到self里
        self.attn_model = attn_model
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.n_layers = n_layers
        self.dropout_p = dropout_p
        
        # 定义网络中的层
        self.embedding = nn.Embedding(output_size, hidden_size)
        self.gru = nn.GRU(hidden_size * 2, hidden_size, n_layers, dropout=dropout_p)
        self.out = nn.Linear(hidden_size * 2, output_size)
        
        # 选择注意力模型
        if attn_model != 'none':
            self.attn = Attn(attn_model, hidden_size)
    
    def forward(self, word_input, last_context, last_hidden, encoder_outputs):
        # 注意:每次我们处理一个time step
        # 得到当前输入(上一个输出)的embedding
        word_embedded = self.embedding(word_input).view(1, 1, -1) # S=1 x B x N
        # 把当前的embedding和上一个context拼接起来输入到RNN里
        rnn_input = torch.cat((word_embedded, last_context.unsqueeze(0)), 2)
        rnn_output, hidden = self.gru(rnn_input, last_hidden)

        # 使用RNN的输出和所有encoder的输出来计算注意力权重,然后计算context向量
        attn_weights = self.attn(rnn_output.squeeze(0), encoder_outputs)
        # (1,1,seq_len)*(1,seq_len,N)
        context = attn_weights.bmm(encoder_outputs.transpose(0, 1)) # B x 1 x N
        # 使用RNN的输出和context向量预测下一个词
        rnn_output = rnn_output.squeeze(0) # S=1 x B x N -> B x N
        # 1*1*N -> 1*N
        context = context.squeeze(1)       # B x (S=1) x N -> B x N
        output = F.log_softmax(self.out(torch.cat((rnn_output, context), 1)), 1)
        
        return output, context, hidden, attn_weights

官方,则需要用到decoder的hidden layer计算attention

# 计算注意力权重
        attn_weights = F.softmax(
            self.attn(torch.cat((embedded[0],hidden[0]),1)),dim=1)

这有一篇的decoder与本篇相似,小不同在于:

embedd = self.embedding(input_seq)
embedd_dropout = self.dropout_embedd(embedd)
de_gru_output, de_gru_hidden = self.gru(embedd_dropout, de_gru_hidden)

训练

teacher_forcing_ratio = 0.5
clip = 5.0

def train(input_variable, target_variable, encoder, decoder, encoder_optimizer, decoder_optimizer, criterion, max_length=MAX_LENGTH):

    # 梯度清零
    encoder_optimizer.zero_grad()
    decoder_optimizer.zero_grad()
    loss = 0 

    # 得到输入和输出句子的长度
    input_length = input_variable.size()[0]
    target_length = target_variable.size()[0]

    # encoding
    encoder_hidden = encoder.init_hidden()
    encoder_outputs, encoder_hidden = encoder(input_variable, encoder_hidden)
    
    # 准备输入和输出变量
    decoder_input = Variable(torch.LongTensor([[SOS_token]]))
    decoder_context = Variable(torch.zeros(1, decoder.hidden_size))
    decoder_hidden = encoder_hidden # Use last hidden state from encoder to start decoder
    if USE_CUDA:
        decoder_input = decoder_input.cuda()
        decoder_context = decoder_context.cuda()

    # 随机选择是否Teacher Forcing
    use_teacher_forcing = random.random() < teacher_forcing_ratio
    if use_teacher_forcing:
        
        # Teacher forcing:使用真实的输出作为下一个时刻的输入
        for di in range(target_length):
            decoder_output, decoder_context, decoder_hidden, decoder_attention = decoder(decoder_input, decoder_context, decoder_hidden, encoder_outputs)
            loss += criterion(decoder_output, target_variable[di])
            decoder_input = target_variable[di] # 下一个时刻的输入来自target

    else:
        # 不使用 teacher forcing:使用decoder的预测作为下一个时刻的输入
        for di in range(target_length):
            decoder_output, decoder_context, decoder_hidden, decoder_attention = decoder(decoder_input, decoder_context, decoder_hidden, encoder_outputs)
            loss += criterion(decoder_output, target_variable[di])
            
            # 选择最可能的词
            topv, topi = decoder_output.data.topk(1)
            ni = topi.item()
            
            decoder_input = Variable(torch.LongTensor([[ni]])) # 下个时刻的输入
            if USE_CUDA: decoder_input = decoder_input.cuda()

            # 如果decoder输出EOS_token,那么提前结束
            if ni == EOS_token: break

    # 反向计算梯度
    loss.backward()
    torch.nn.utils.clip_grad_norm_(encoder.parameters(), clip)
    torch.nn.utils.clip_grad_norm_(decoder.parameters(), clip)
    encoder_optimizer.step()
    decoder_optimizer.step()
    
    return loss.item() / target_length

比较

  • decoder_hidden = encoder_hidden,这里要求两个模型的hidden state大小一致
  • 官方:encoder逐个单词输出
for ei in range(input_length):
	...
	encoder_outputs[ei] = encoder_output[0,0]
  • 官方:decoder不需要传入decoder_context
decoder(decoder_input, decoder_hidden, encoder_outputs)
  • 官方:选择decoder的上一次输出单词作为本次输入
topv,topi = decoder_output.topk(1)
decoder_input = topi.squeeze().detach()
  • 截断反向传播超过阈值的参数
torch.nn.utils.clip_grad_norm_(encoder.parameters(), clip)
torch.nn.utils.clip_grad_norm_(decoder.parameters(), clip)

结果

2m 37s (- 36m 51s) (5000 6%) 2.6901
5m 22s (- 34m 58s) (10000 13%) 2.0667
8m 7s (- 32m 31s) (15000 20%) 1.7538
10m 54s (- 29m 58s) (20000 26%) 1.4743
13m 42s (- 27m 24s) (25000 33%) 1.3136
16m 30s (- 24m 45s) (30000 40%) 1.0805
19m 19s (- 22m 5s) (35000 46%) 0.9613
22m 8s (- 19m 22s) (40000 53%) 0.8335
24m 56s (- 16m 37s) (45000 60%) 0.7474
27m 45s (- 13m 52s) (50000 66%) 0.6443
30m 34s (- 11m 7s) (55000 73%) 0.5689
33m 22s (- 8m 20s) (60000 80%) 0.5394
36m 12s (- 5m 34s) (65000 86%) 0.4676
39m 1s (- 2m 47s) (70000 93%) 0.4181
41m 48s (- 0m 0s) (75000 100%) 0.3958

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值