解读(Solving Math Word Problems with Multi-Encoders and Multi-DEcoders)的代码(模型部分)

数据处理

数据处理部分见数据处理部分

我们已经知道,prepare_train_batch函数返回14个变量
每一个变量都是一个列表,有batch个元素

  • id_batches[i]代表的是第i个batch中batch个问题的id
  • input1_batches[i]代表的是第i个batch中的batch个问题,每一个问题文本的id序列,根据这些id就可以查找对应的词向量
  • input2_batches[i]代表的是第i个batch中的batch个问题,每一个问题文本对应的词性标注的id序列,因为要将每一个单词的词性也嵌入到模型中。(实验表明,添加了词性标注的信息后,准确率提升了不到1个百分点,大概0.759这样(而且此时是去掉了word2vec的结果))
  • input_lengths[i]代表的是第i个batch中的batch个问题,每一个问题的长度,也就是这个问题有多少个单词
  • output1_batches[i],代表的是每一个问题对应的前缀表达式的id序列,注意此时已经根据当前batch中最长的前缀表达式的长度pad了(前缀表达式作为基于树解码端的标签)
  • output1_lengths[i]自然是每一个前缀表达式的实际长度
  • output2_batches[i],代表的是每一个后缀表达式的id序列,同样根据当前batch中最长的后缀表达式的长度pad了(后缀表达式作为基于seq解码端的标签)
  • output2_lengths[i]自然是每一个后缀表达式的实际长度(由于seq解码需要SOS和EOS以及UNK三个特殊字符,所以output2_lengths的每一个元素要长于output_lengths1的每一个元素)
  • nums_batches[i]记录的是这个batch中每一个问题中出现了多少个数字
  • num_stack_batches[i]记录的是这batch个问题中每一个问题是否有重复的数字出现,如果没有,这是空列表,如果有,那么记录重复数字出现的位置
  • num_pos_batches[i]记录每一个数字出现的位置
  • num_order_batches[i]记录问题中所有数字的大小关系,比如[1, 4, 2, 3]表明,这个问题有四个数字,第一个数字最小,第二个数字最大,第三个数字比第一个数字大,第四个数字比第一个和第三个数字大
  • num_size_batches[i]==nums_batches[i]
  • parse_graph_batches[i]这个其实就是要生成一个(seq_len,seq_len)的矩阵,根据提取出来的这个问题中的依存句法关系来构建GNN需要的图
batch_size=3
id_batches, input1_batches, input2_batches, input_lengths, output1_batches, output1_lengths, output2_batches, output2_lengths, \
        nums_batches, num_stack_batches, num_pos_batches, num_order_batches, num_size_batches, parse_graph_batches = prepare_train_batch(train_pairs, batch_size)

在这里插入图片描述

input1_batch=input1_batches[i]
input2_batch=input2_batches[i]
input_length=input_lengths[i]
target1_batch=output1_batches[i]
target1_length=output1_lengths[i]
target2_batch=output2_batches[i]
target2_length=output2_lengths[i] 
num_stack_batch=num_stack_batches[i]
num_size_batch=num_size_batches[i]
num_pos_batch=num_pos_batches[i]
num_order_batch=num_order_batches[i]
parse_graph_batch=parse_graph_batches[i]

在这里插入图片描述

这是因为不同的decoder端的词汇空间不同
generate_nums指的是1和3.14,这是常数,可能在问题文本中不会出现,但是表达式中出现

seq_mask=[]#构造sequence mask,因为在做注意力时,要避免关注到pad位置的单词
max_len=max(input_length)#这两个问题中最长的
for seq_len in input_length:
    seq_mask.append([0 for _ in range(seq_len)]+[1 for _ in range(max_len-seq_len)])
    #做注意时,将seq_mask乘以-1e12然后element_wise add到向量上,然后softmax,pad位置的权重就接近于0
seq_mask=torch.ByteTensor(seq_mask)
print(seq_mask)
num_mask = []
max_num_size = max(num_size_batch) + len(generate_num1_ids)
for i in num_size_batch:
    d = i + len(generate_num1_ids)
    num_mask.append([0] * d + [1] * (max_num_size - d))
num_mask = torch.ByteTensor(num_mask)
print(num_mask)
#num_mask的作用是这样的,由于我们是batch个问题一起放进去,假设第一个问题出现了5个数字,第二个问题出现了7个数字
#那么我们需要把第一个问题中出现的数字次数pad到7次,那么当生成第一个问题的表达式的时候,在预测每一步对应的数字时
#需要num_mask告知模型最后两个数字对于第一个问题来说是没有的,告知的方式就是在对应的位置上加上-1e12,那么做softmax后
#这个位置的权重就趋向于0(例外需要考虑到常数)

num_pos_pad = []
max_num_pos_size = max(num_size_batch)
for i in range(len(num_pos_batch)):
    temp = num_pos_batch[i] + [-1] * (max_num_pos_size-len(num_pos_batch[i]))
    num_pos_pad.append(temp)
num_pos_pad = torch.LongTensor(num_pos_pad)
#num_pos记录的是每一个问题中数字的位置,由于是batch个问题,需要按照batch里面出现最多数字次数来pad
print(num_pos_pad)

num_order_pad = []
max_num_order_size = max(num_size_batch)
for i in range(len(num_order_batch)):
    temp = num_order_batch[i] + [0] * (max_num_order_size-len(num_order_batch[i]))
    num_order_pad.append(temp)
num_order_pad = torch.LongTensor(num_order_pad)

print(num_order_pad)

在这里插入图片描述

num_stack1_batch = copy.deepcopy(num_stack_batch)
num_stack2_batch = copy.deepcopy(num_stack_batch)

num_start2 = output2_lang.n_words - copy_nums - 2
#num_start2代表的是在decoder2中,数字是从哪个下标开始的(N0在output2_lang.word2index中的起始位置)
unk1 = output1_lang.word2index["UNK"]
unk2 = output2_lang.word2index["UNK"]

input1_var = torch.LongTensor(input1_batch).transpose(0, 1)
input2_var = torch.LongTensor(input2_batch).transpose(0, 1)
target1 = torch.LongTensor(target1_batch).transpose(0, 1)
target2 = torch.LongTensor(target2_batch).transpose(0, 1)
#将数据的形状改为(max_len,batch_size),注意,第二个维度是batch
parse_graph_pad = torch.LongTensor(parse_graph_batch)

hidden_size=4
padding_hidden = torch.FloatTensor([0.0 for _ in range(hidden_size)]).unsqueeze(0)
batch_size = len(input_length)

Encoder

def clones(module, N):
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])

class Parse_Graph_Module(nn.Module):
    def __init__(self, hidden_size):
        super(Parse_Graph_Module, self).__init__()
        
        self.hidden_size = hidden_size
        self.node_fc1 = nn.Linear(hidden_size, hidden_size)
        self.node_fc2 = nn.Linear(hidden_size, hidden_size)
        self.node_out = nn.Linear(hidden_size * 2, hidden_size)
    
    def normalize(self, graph, symmetric=True):
        d = graph.sum(1)
        if symmetric:
            D = torch.diag(torch.pow(d, -0.5))
            return D.mm(graph).mm(D)
        else :
            D = torch.diag(torch.pow(d,-1))
            return D.mm(graph)
        
    def forward(self, node, graph):
        graph = graph.float()
        batch_size = node.size(0)
        for i in range(batch_size):
            graph[i] = self.normalize(graph[i])
        
        node_info = torch.relu(self.node_fc1(torch.matmul(graph, node)))
        node_info = torch.relu(self.node_fc2(torch.matmul(graph, node_info)))
        
        agg_node_info = torch.cat((node, node_info), dim=2)
        agg_node_info = torch.relu(self.node_out(agg_node_info))
        
        return agg_node_info
    
class EncoderSeq(nn.Module):
    def __init__(self,vocab_size,pos_size,embed_model,word_embed_size,pos_embed_size,hidden_size,
                n_layers=2,hop_size=2,dropout=0.5):
        '''
        这是第一个encoder,这个encoder的流程是:
            1. 将单词嵌入成一个向量,将这个单词的词性也嵌入成一个向量,
            2. 拼接这两个向量表示这个单词,然后送进GRU中
            3. GRU的输出代表的是整个问题文本的语义信息,也就是论文中的H
            4. 将H作为第一个GCN的初始节点的嵌入向量表示,也就是P0
            5. 根据P0以及提取得到的依存句法关系矩阵,根据公式1和2计算P1,由于hop_size是2,所以还要计算P2
            6. 输出P2
        '''
        super(EncoderSeq,self).__init__()
        self.vocab_size=vocab_size#词汇空间大小
        self.pos_size=pos_size#词性标注的词汇空间大小
        self.word_embed_size=word_embed_size#词嵌入的维度
        self.pos_embed_size=pos_embed_size#词性标注对应的嵌入维度
        self.hidden_size=hidden_size
        self.n_layers=n_layers
        self.hop_size=hop_size
        self.dropout_rate=dropout#每进行一次min-batch训练时,以0.5的概率随机舍弃神经元,
        #训练时所有的神经元权重要乘以(1-0.5)
        
        self.pretrained_word_embedding_model=embed_model
        self.pos_embedding_model=nn.Embedding(pos_size,embedding_dim=pos_embed_size,padding_idx=0)
        #其中padding_idx是指,索引位置是0的单词对应的embedding是全0向量
        self.dropout_operation=nn.Dropout(self.dropout_rate)
        self.gru=nn.GRU(input_size=word_embed_size+pos_embed_size,hidden_size=hidden_size,
                              num_layers=self.n_layers,dropout=self.dropout_rate,bidirectional=True)
        #其中的dropout操作会在两层BiGRU之间加一层dropout
        self.parse_gnn=clones(Parse_Graph_Module(hidden_size), hop_size)
    
    def forward(self,word_ids,pos_ids,input_length,parse_graph,hidden=None):
        word_embedding=self.pretrained_word_embedding_model(word_ids)
        pos_embedding=self.pos_embedding_model(pos_ids)
        embeddings=torch.cat([word_embedding,pos_embedding],dim=2)
        #size()==(seq_len,batch_size,word_embed_size+pos_embed_size)
        embeddings=self.dropout_operation(embeddings)
        
        #pack_padded_sequence方法,看名字的意思就是将pad过的句子打包,该方法的原理是首先将pad的句子排序,句子长度在前面
        #我们在之前已经将batch个句子按照长度排序了;然后以seq_len为第一个维度,batch为第二个维度
        #该方法会统计每一个时间步有多少个batch,这个方法的输出是一个张量,而且已经去掉了pad位置
        #例如,输入的张量形状是(10,6,100)其中6代表6个问题文本,10代表这6个问题中最长的有10个单词,100代表每一个单词用100维向量表示
        #假如后两个问题文本的长度分别是7,6,那么pack_padded_sequence返回的张量长度是60-4-3,也就是去掉了所有的pad位置
        packed=torch.nn.utils.rnn.pack_padded_sequence(embeddings,input_length)#根据input_length判断哪些位置是pad的,然后去掉
        init_hidden=hidden
        #packed已经包含有batch的信息
        gru_outputs,gru_hidden=self.gru(packed,init_hidden)
        gru_outputs,outputs_length=torch.nn.utils.rnn.pad_packed_sequence(gru_outputs)
        #pad_packed_sequence显然是根据packedpad回原来的样子,之所以能够pad回原来的样子是因为gru_outputs中含有batch_size的信息
        #gru_outputs.size()==(seq_len,batch_size,hidden_size*2)
        #整个GRU的输出是前向的嵌入+反向的嵌入,而不是拼接的结果
        gru_outputs=gru_outputs[:,:,:self.hidden_size]+gru_outputs[:,:,self.hidden_size:]
        gru_outputs=gru_outputs.transpose(0,1)#(batch_size,seq_len,hidden_size)
        for i in range(self.hop_size):
            gru_outputs = self.parse_gnn[i](gru_outputs, parse_graph[:,2])#聚集邻居节点的信息
        #相当于利用句子的依存句法信息加强了句子的嵌入式表示
        gru_outputs=gru_outputs.transpose(0,1)#(seq_len,batch_size,hidden_size)
        return gru_outputs,gru_hidden
n_layers=2
hop_size=2
embedding_size=5
encoder = EncoderSeq(vocab_size=input1_lang.n_words, pos_size=input2_lang.n_words, 
                     embed_model=embed_model, word_embed_size=embedding_size, pos_embed_size=2, 
                     hidden_size=hidden_size, n_layers=n_layers, hop_size=hop_size)
encoder_outputs, encoder_hidden = encoder(input1_var, input2_var, input_length, parse_graph_pad)

在这里插入图片描述

def get_all_number_encoder_outputs(encoder_outputs, num_pos, batch_size, num_size, hidden_size):
    indices = list()
    sen_len = encoder_outputs.size(0)
    masked_index = []
    temp_1 = [1 for _ in range(hidden_size)]
    temp_0 = [0 for _ in range(hidden_size)]
    for b in range(batch_size):
        for i in num_pos[b]:
            indices.append(i + b * sen_len)
            masked_index.append(temp_0)
        indices += [0 for _ in range(len(num_pos[b]), num_size)]
        masked_index += [temp_1 for _ in range(len(num_pos[b]), num_size)]
    indices = torch.LongTensor(indices)
    masked_index = torch.ByteTensor(masked_index)
    masked_index = masked_index.view(batch_size, num_size, hidden_size)
    all_outputs = encoder_outputs.transpose(0, 1).contiguous()
    all_embedding = all_outputs.view(-1, encoder_outputs.size(2))  # S x B x H -> (B x S) x H
    all_num = all_embedding.index_select(0, indices)
    all_num = all_num.view(batch_size, num_size, hidden_size)
    return all_num.masked_fill_(masked_index.bool(), 0.0), masked_index


print(num_pos_batch)
print('num_pos_batch记录的是当前这batch个问题中,每一个问题中数字的位置')
copy_num_len = [len(_) for _ in num_pos_batch]
num_size = max(copy_num_len)
print(num_size)
print('num_size代表的是这batch个问题中,出现数字次数最多的那个问题数字出现的次数')
num_encoder_outputs, masked_index = get_all_number_encoder_outputs(encoder_outputs, num_pos_batch, 
                                                                   batch_size, num_size, encoder.hidden_size)

print(num_encoder_outputs)
print('num_encoder_outputs代表的是每一个数字对应的embedding')

NumEncoder

Encoder的输出是encoder_outputs

  • 从encoder_outputs中提取出数字的embedding,记为num_encoder_outputs
  • 将encoder_outputs, num_encoder_outputs以及num_pos_pad, num_order_pad送进NumEncoder
  • 其中num_pos_pad记录了每一个数字的位置,num_order_pad记录了各个数字之间的大小关系
  • Numencoder会创建两个图
  • 通过一系列操作再次增强了encoder_outputs的表示信息,同时也增强了数字的embedding,因为此时的embedding包含了数字之间大小的信息
encoder_outputs, num_outputs, problem_output = numencoder(encoder_outputs, num_encoder_outputs, 
                                                          num_pos_pad, num_order_pad)
num_outputs = num_outputs.masked_fill_(masked_index.bool(), 0.0)
decoder_hidden = encoder_hidden[:decoder.n_layers]  

Decoder

接下来就是根据encoder_outputs送到两个decoder端,生成表达式

seq-decoder

class Attn(nn.Module):
    def __init__(self,hidden_size):
        '''
        W_a*a+W_b*b==W_ab*[a;b]
        '''
        super(Attn,self).__init__()
        self.hidden_size=hidden_size
        self.attn=nn.Linear(in_features=hidden_size*2,out_features=hidden_size)
        self.score=nn.Linear(in_features=hidden_size,out_features=1,bias=False)
        #score的目的是将向量映射为一个数值表示这个单词的重要程度
        self.softmax=nn.Softmax(dim=1)
    
    def forward(self,hidden,encoder_outputs,seq_mask=None):
        '''
        hidden.size()==(batch_size,hidden_size)
        encoder_outputs.size()==(batch_size,seq_len,hidden_size)
        做attention的流程是att_weights=softmax(torch.bmm(hidden.unsqueeze(1),encoder_outputs.transpose(1,2))
        以上是最常规的做法,利用向量内积的方式提取出hidden和encoder_outputs之间的相关性
        而在这篇源码中,利用的是神经网络的方式,也就是
        att_weights=softmax(W_s*tanh((W_h*hidden+b_h)+(W_e*encoder_outputs+b_e)))
        '''
        
        seq_len,batch_size,hidden_size=encoder_outputs.size()
        #assert hidden.dim()==3
        repeat_dims=[1]*hidden.dim()
        repeat_dims[0]=seq_len#广播操作
        hidden=hidden.repeat(*repeat_dims)#(seq_len,batch_size,hidden_size)
        
        energy_in=torch.cat((hidden,encoder_outputs),2).view(-1,2*self.hidden_size)
        att_energies=self.score(torch.tanh(self.attn(energy_in)))#(batch_size*seq_len,1)
        att_energies=att_energies.squeeze(1)#(batch_size*seq_len)
        att_energies=att_energies.view(seq_len,batch_size).transpose(0,1)
        if seq_mask is not None:
            att_energies=att_energies.masked_fill_(seq_mask.bool(),-1e12)
        att_energies=self.softmax(att_energies)
        return att_energies.unsqueeze(1)#(batch_size,1,seq_len)
        
        
        
        

class AttnDecoderRNN(nn.Module):
    def __init__(self,hidden_size,embedding_size,input_size,output_size,n_layers=2,dropout_rate=0.5):
        '''
        hidden_size就是decoder端的RNN的hidden_size
        embedding_size是decoder端嵌入层的维度,也是RNN输入的维度
        input_size和output_size就是output2_lang中有少个字符,一共有25个,分别是+-/*以及Ni,还有1和3.14以及SOS,EOS等
        '''
        super(AttnDecoderRNN,self).__init__()
        self.hidden_size=hidden_size
        self.embedding_size=embedding_size
        self.n_layers=n_layers
        self.input_size=input_size
        self.output_size=output_size
        self.dropout_rate=dropout_rate
        
        self.dropout_operation=nn.Dropout(self.dropout_rate)#dropout操作可以缓解过拟合问题,因为每一次网络结构会变小
        #最后在测试阶段,所有的权重都要乘以(1-dropout_rate),有集成学习的思想
        self.embedding_model=nn.Embedding(input_size,embedding_size,padding_idx=0)
        self.gru=nn.GRU(hidden_size+embedding_size,hidden_size,n_layers,dropout=self.dropout_rate)
        #GRU与LSTM的区别在于,GRU的更新门同时作为遗忘门和输入门的功能
        #GRU的输入是context向量与input token的embedding的拼接,会在两层之间做一次dropout操作,如果n_layers=1,那么dropout操作是不会做的
        self.concat_affine_layer=nn.Linear(in_features=hidden_size*2,out_features=hidden_size)
        #注意,预测当前时间步的token时,不仅仅考虑RNN的输出,还要考虑context这个上下文向量,因此需要将当前时间步的
        #GRU的hidden_state和context拼接
        self.output_layer=nn.Linear(in_features=hidden_size,out_features=self.output_size)#将向量映射成输出词汇空间上的概率分布
        self.att_layer=Attn(hidden_size)#上一时间步的hidden_state会和encoder_outputs做注意力,得到context
        
    
    def forward(self,decoder_input,last_hidden,encoder_outputs,seq_mask):
        '''
        decoder就是根据当前时间步的decoder_input(size()==(batch_size,)),嵌入成embeddings
        last_hidden就是上一时刻的隐藏状态h_{t-1}
        典型的seq2seq结构在解码时,要拿上一时刻的隐藏状态,也就是h_{t-1}和encoder_outputs做注意力运算
        目的是提取出encoder端在当前时间步有用的信息,提取出来的张量叫context,然后将context和当前时间步的嵌入embeddings
        拼接作为当前时间步的输入x_{t},再加上last_hidden,然后得到当前时间步的隐藏状态current_hidden
        以上就是典型的seq2seq结构在解码端的运作机制
        '''
        batch_size=decoder_input.size(0)#decoder_input.size()==(batch_size,)
        current_embeddings=self.dropout_operation(self.embedding_model(decoder_input))#(batch_size,embed_dim)
        current_embeddings=current_embeddings.unsqueeze(0)#(1,batch_size,embed_dim)
        #last_hidden.size()==(n_layers,batch_size,hidden_size),n_layers表示有几层RNN
        att_weights=self.att_layer(last_hidden[-1].unsqueeze(0),encoder_outputs,seq_mask)#此时每一个数字代表对应的token的重要程度
        context=torch.bmm(att_weights,encoder_outputs.transpose(0,1))#context是encoder-aware representation
        #context.size()==(batch_size,1,hidden_size)
        #接下来把context连同decoder_input_embedding作为当前时间步GRU的输入
        rnn_output,hidden=self.gru(torch.cat((current_embeddings,context.transpose(0,1)),2),last_hidden)
        #rnn_output.size()==(1,batch_size,hidden_size)
        #接下来就是利用rnn_output作为预测的依据来生成表达式的单词
        concat_vector=torch.cat((rnn_output.squeeze(0),context.squeeze(1)),1)#(batch_size,hidden_size*2)
        output=self.output_layer(torch.tanh(self.concat_affine_layer(concat_vector)))#(batch_size,output_size)
        return output,hidden
    
decoder=AttnDecoderRNN(hidden_size=hidden_size,embedding_size=10,input_size=output2_lang.n_words,
                       output_size=output2_lang.n_words)
print("decoder端的词汇空间 : ",output2_lang.word2index,"\n","一共有%d个词汇"%output2_lang.n_words)
print("初始时刻的decoder_input是batch个SOS token : ",decoder_input)
print("decoder_hidden代表的是上一个时间步的隐藏状态,初始时刻利用encoder端GRU的最后一个时间步的hidden_state作为decoder端的地一个时间步的hidden_state")
print(decoder_hidden.size(),"其中2表示2层GRU,我们实际只用最后一层的hidden state;3代表batch个数,4表示hidden_size")
print(encoder_outputs.size(),"其中第一个维度代表的是这batch个句子的最长的句子的序列长度,如果某个句子不够长,那就pad 0")
print(seq_mask.size(),"seq_mask用来指明那些位置的单词是pad的,做注意力的时候需要seq_mask")

接下来就是将上述变量输入到第一个时间步(每一个时间步都需要四个元素:)

  • decoder_input,也就是上一个时刻模型预测出来的token
  • last_hidden,也就是上一个时刻隐藏单元的hidden_state
  • encoder_outputs,用来计算上下文向量context
  • seq_mask,在做注意力运算时需要指明那些位置的单词是pad的(这个不是必须的)
decoder_output,decoder_hidden=decoder(decoder_input,last_hidden=decoder_hidden,encoder_outputs=encoder_outputs,seq_mask=seq_mask)

在这里插入图片描述

target如下图。每一列代表一个样本对应的表达式,每一行代表一个时间步的所有样本在这个时间步的token。6代表的是EOS结束标记
在这里插入图片描述

decoder_output的每一行代表每一个样本在当前时间步下在词汇空间上预测出来的分数,接下来的decoder_output有两个用途,第一是与当前时间步的target计算交叉熵loss,第二个用途是选取分数最大的那个预测token作为下一个时间步的输入。不过如果在训练阶段,我们需要利用到teacher-forcing机制,不然模型训练的不会太好。教师指导就是不管decoder_output预测的是那一个token,始终用target作为下一个时间步的输入。

不过MWP问题与机器翻译或者其它的文本生成任务不一样,主要在于数字的处理。尤其是重复数字的处理。

例如:

问题文本:小芳家5月份用水量是16.5吨,每吨水的价格是2.1元,小芳家一共有5口人,平均每人应交多少水费?

对应的求解表达式是:16.5*2.1/5
我们知道数字的具体数值对于模型来说是无关紧要的,为了降低decoder端的词汇空间,我们把数字替换成Ni,
其中i代表这个数字在问题文本中出现的顺序。
例如: 16.5是N1,2.1是N2,那么现在问题来了
这个5怎么表示???,我们说表达式现在应该变成N1*N2/,除以谁???,
所以我们可以看到如果出现了重复数字,那么对于原始表达式中的5,我们没办法替换成Ni,所以此时的做法就是不替换了。
转换后的表达式就是N1*N2/5
但是这样就行了吗,我们说decoder端的标签是+,-,/,*,1,3.14,以及N0,N1,N2...Nn。这里面的n是指所有问题中出现的数字次数的那个问题出现了n个数字。
也就是说decoder端不会涉及到具体的数字数值。所以此时没办法把5这个数字送给模型。
我们送给模型的是[10,0,11,1,25]其中10代表N1,0代表*,11代表N2,1代表/,25代表UNK。也就是说凡是有具体数字的表达式,具体数字就换成UNK
然后在解码的时候,如果当前的标签包含UNK对应的id,那么说明当前的样本是有重复数字的,那么我们选择哪个作为标签呢,此时就让模型来抉择吧,我们选出来模型预测出来分数最大的那个位置作为重复数字的标签。
def generate_decoder_input(target,decoder_output,num_stack_batch,num_start,unk):
    '''
    如果你的数据集中不含有重复数字,那么不需要这个函数
    target的长度是batch_size,每一个值代表一个样本在当前时间步的标签
    decoder_output的长度是batch_size,每一个值是长度为26的向量,26是num_classes
    num_start是指在decoder的词汇空间中,数字是从哪个下标开始的,比如
    {'-': 0, '*': 1, '+': 2, '/': 3, '^': 4, 'PAD': 5, 'EOS': 6, '1': 7, '3.14': 8, 'N0': 9, 'N1': 10, 'N2': 11, 'N3': 12, 'N4': 13, 'N5': 14, 'N6': 15, 'N7': 16, 'N8': 17, 'N9': 18, 'N10': 19, 'N11': 20, 'N12': 21, 'N13': 22, 'N14': 23, 'SOS': 24, 'UNK': 25}
    那么num_start是9
    
    例:
    问题文本:小芳家5月份用水量是16.5吨,每吨水的价格是2.1元,小芳家一共有5口人,平均每人应交多少水费?
    转换后的输入文本:['小', '芳', '家', 'NUM', '月份', '用水量', '是', 'NUM', '吨', ',', '每吨', '水', '的', '价格', '是', 'NUM', '元', ',', '小', '芳', '家', '一共', '有', 'NUM', '口', '人', ',', '平均', '每人', '应交', '多少', '水费', '?']
    前缀表达式:  ['/', '*', 'N1', 'N2', '5']
    '''
    
    for i in range(target.size(0)):
        if target[i]==unk:
            #说明这个位置是重复数字
            assert num_stack_batch!=[]
            num_stack=num_stack_batch[i].pop()
            assert num_stack!=[]#以上面的例子,那么num_stack就是[[0,3]]代表有一个重复数字出现,位置分别是出现在
            #第一个和第四个数字
            max_score=-float('100000')#选取decoder_outputs在这两个位置中分数大的那个位置
            #因为不管怎么样,这两个位置的数字的数值是一样的
            for num_pos in num_stack:
                #num_pos要么是0,要么是3
                if decoder_output[i,num_start+num]>max_score:
                    #num_start+num正好对应着N0和N3,这也正是上面那个例子中问题文本中重复数字出现的位置顺序
                    max_score=decoder_output[i,num_start+num]
                    target[i]=num_start+num
    return target
all_decoder_outputs=[]
num_start=9
unk=25
decoder_input=generate_decoder_input(target2[0],decoder_output,num_stack2_batch,num_start,unk)
decoder_output,decoder_hidden=decoder(decoder_input,
                                      last_hidden=decoder_hidden,
                                      encoder_outputs=encoder_outputs,seq_mask=seq_mask)
all_decoder_outputs.append(decoder_output)

decoder_input=generate_decoder_input(target2[1],decoder_output,num_stack2_batch,num_start,unk)
decoder_output,decoder_hidden=decoder(decoder_input,
                                      last_hidden=decoder_hidden,
                                      encoder_outputs=encoder_outputs,seq_mask=seq_mask)
all_decoder_outputs.append(decoder_output)

decoder_input=generate_decoder_input(target2[2],decoder_output,num_stack2_batch,num_start,unk)
decoder_output,decoder_hidden=decoder(decoder_input,
                                      last_hidden=decoder_hidden,
                                      encoder_outputs=encoder_outputs,seq_mask=seq_mask)
all_decoder_outputs.append(decoder_output)

上述过程要放在一个for循环中,迭代次数要与数据集中最长的表达式为准。

最后的all_encoder_outputs就会是形如(batch_size,max_target_len,num_classes)的张量

再强调一遍,用num_stack的原因是target没法办,因为对于重复来说,他们在word2index中得到的下标都是一样的,所以target只能用UNK对应的id来表示重复数字,那么在解码的过程中,怎么区分重复数字呢,num_stack记录的是重复数字在nums中的位置,而表达式中的Ni正是根据nums中数字的顺序决定的,所以对于decoder_outputs来说,只需要找到它预测出来的重复数字位置分别对应的分数,找到分数最大的那个,就认为是该数字。

eg:
['小', '芳', '家', 'NUM', '月份', '用水量', '是', 'NUM', '吨', ',', '每吨', '水', '的', '价格', '是', 'NUM', '元', ',', '小', '芳', '家', '一共', '有', 'NUM', '口', '人', ',', '平均', '每人', '应交', '多少', '水费', '?']
其中prefix expression :  ['/', '*', 'N1', 'N2', '5']
我们发现没有N0和N3这个位置的数字在表达式中,而且表达式中有5这个数字
这个expression对应的target形如是[0,1,10,11,25]
其中25表示UNK,num_stack==[[0, 3]]
此时在第五个时间步时,由于target是UNK,而真正的target应该是N0还是N3呢,那么此时就看decoder_outputs在N0和N3两个位置哪个位置的分数大,就认为当前的标签是那一个

在这里插入图片描述

计算loss

在这里插入图片描述

没有pad的情况
l o s s = − ∑ i = 1 N ∑ j = 1 T y j ( i ) log ⁡ y ^ j ( i ) loss=-\sum_{i=1}^{N}\sum_{j=1}^{T}y_j^{(i)}\log \hat{y}_{j}^{(i)} loss=i=1Nj=1Tyj(i)logy^j(i)
其中N是指batch,T是指序列长度, y j ( i ) y_j^{(i)} yj(i)代表第 i i i个样本在第 j j j个时间步的标签,
y ^ j ( i ) \hat{y}_{j}^{(i)} y^j(i)代表第 i i i个样本在第 j j j个时间步预测的token分数

all_decoder_outputs=all_decoder_outputs.transpose(0,1).contiguous()#(batch_size,max_target_len,num_classes)
target=target2.transpose(0,1)#(batch_size,max_target_len)

logits_flat=all_decoder_outputs.view(-1,all_decoder_outputs.size(-1))#(batch_size*max_target_len,num_classes)
log_probs_flat=torch.nn.functional.log_softmax(logits_flat)#(batch_size*max_target_len,num_classes)
target_flat=target.view(-1,1)#(batch_size*max_target_len,1)
loss_flat=-torch.gather(log_probs_flat,dim=1,index=target_flat)#(batch_size*max_target_len)

在这里插入图片描述
所以说,loss的计算流程就是按照上述公式,将decoder_outputs经过log_softmax运算后,与target相成,gather有点类似与one_hot形式的相乘,也就是查找的运算。如果没有pad的情况,那么可以直接调用库函数,NLLLoss和CrossEntropyLoss,两者唯一的区别是CrossEntropyLoss就是log_softmax+NLLLoss

如果有pad的情况,那么此时需要根据实际的target_length构造一个(batch_size,max_target_len)的矩阵,其中,pad位置是0,不是pad位置是1,然后与loss相乘

target_length=[9,7,10]
max_target_len=max(target_length)
seq_range=torch.arange(0,max_target_len).long().unsqueeze(0).expand(len(target_length),max_target_len)
print(seq_range)
target_length=torch.LongTensor(target_length)
target_length=target_length.unsqueeze(1).expand_as(seq_range)
print(target_length)
target_mask=(seq_range<target_length).float()
print(target_mask)

在这里插入图片描述

然后利用loss*target_mask,最后求和(此时pad位置的loss是0)再除以target_length.sum(),这样就不包含pad位置的loss了

BeamSearch

原理

假设beam size=3,词汇表大小是26,

  • 那么第一步传进去SOS这个特殊的token,此时有26个可能的序列,从中选出来最高的3个,假设是a,r,z。
  • 然后每一个单词都会作为下一个时间步的输入,也就是说每一个单词再生成26种可能的序列,一共是3*26种可能的序列。
  • 从这3*26种可能的序列中选取出来最高的3个(注意,我们是从3*26种可能的序列中共同选取出来最大的三个,而不是a对应的26个序列选取最大的,r对应的26个序列选取最大的,z对应的26个序列选取最大的)。
  • 假设此时是ab,ae,za。(也就是说r那条路径就不要了),然后同样的,ab作为下一时刻的输入,得到26种可能的路径,ae,za同理。
  • 也就是说除了第一步,每一步都会生成3*26种可能的路径,然后选取出来最高的3个。
  • 如果某一条路径到了EOS,那么这条路径就保存下来,接下来生成的路径可能数目是3-1乘以26,因为到了EOS的路径不会再继续生成下去
  • 所以此后每一步要生成的路径数目是2*26。假如又有一条路径到了EOS,那么此后每一步其实就是从一条路径开始搜索,变成了贪心策略。直到这条路径到达了EOS结束。

代码

class Beam:
	def __init__(self,score,input_var,hidden,output):
		self.score=score#记录的是分数
		self.input_var=input_var
		self.hidden=hidden
		self.output=output

流程是:

  1. 第一层循环是从0到max_decode_len,也就是最多可以有多长的时间步
  2. 第二层是循环对于每一个时间步,遍历beam_size条路径,每一条路径都会生成词汇空间大小个可能的分数。(具体做法是把每一条路径当前的input和hidden作为decoder的输入)
  3. 第二层循环结束之后,从这beam_size*output_size个分数中选取出前beam_size个预测的token,也就是beam_size个路径。
  4. 整个过程需要注意处理那些已经到达了EOS的路径,这种路径不再继续生成token
  5. 最终返回beam_size个分数中最大的那个分数的对应的路径
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值