2019 CS224N Assignment 5: sub-word modeling and convolutional networks

Character-based convolutional encoder for NMT

使用字符级卷积获得词向量

(a) 在assignment 4 中我们使用的词向量是256维,而在本assignment 中,我们使用的字符的嵌入为50维,解释为什么通常字符级的嵌入比词嵌入更低。

  • 我是这么理解的,嵌入表示了所携带的信息(例如可能含有词性信息,语义信息等),很明显一个单词比起一个字符所携带的信息更多,因此使用更高维表示。还有一个单词由很多字符构成,直观上字符的嵌入维度应该比词嵌入维度要低一些。

(b)

  • character-based embedding model:
    嵌入层: e w o r d = 50 e_{word} = 50 eword=50
    卷积核参数: W ∈ R f × e c h a r × k W \in R^{f× e_{char} ×k} WRf×echar×k,和一个bias b ∈ R f b\in R^{f} bRf
    HighWay层: W p r o j , W g a t e ∈ R e w o r d × e w o r d W_{proj},W_{gate} \in R^{e_{word} × e_{word}} Wproj,WgateReword×eword和两个偏置 b p r o j , b g a t e ∈ R e w o r d b_{proj},b_{gate} \in R^{e_{word}} bproj,bgateReword
    因此参数一共为 V c h a r ∗ e w o r d + f ∗ e c h a r ∗ k + f + 2 ∗ e w o r d ∗ e w o r d + 2 ∗ e w o r d = 96 ∗ 50 + 256 ∗ 50 ∗ 5 + 256 + 2 ∗ 50 ∗ 50 + 2 ∗ 50 = 74156 V_{char} * e_{word} + f * e_{char} * k + f + 2*e_{word}*e_{word} + 2*e_{word}=96*50+256*50*5+256+2*50*50+2*50=74156 Vchareword+fechark+f+2ewordeword+2eword=9650+256505+256+25050+250=74156
  • word-based lookup embedding model:
    嵌入层: V w o r d ∗ e w o r d = 50000 ∗ 256 = 12800000 V_{word} * e_{word} = 50000*256 = 12800000 Vwordeword=50000256=12800000
  • word-based lookup embedding model含有的参数更多,两者的倍数关系为172倍

(c) RNN计算每个位置的上下文表示时,将从左到右或从右到左获得的信息“装进”一个固定长度的向量中,而卷积只是利用window中的几个词或字符来获得一个输出,因此卷积不仅可以并行计算,还可以使用多个filter来捕获不同的特征,拼接在一起可以获得不同长度的表示,可以使用多头注意力的思想,使用不同的attention head 去捕获不同的特征。

(d) 最大池化:

  • 优点:将显著的特征保留下来了
  • 缺点:大部分信息被丢弃

平均池化:

  • 优点:将数据中所有信息都保留下来了
  • 缺点:进行平均后,那些较明显的特征将会被“稀释”

模型架构
在这里插入图片描述
完整代码见我的Github

(e) 实现 words2charindices(),即单词向字符下标的转化,参照下面的 words2indices() 函数即可,这里要注意要在每个单词前后增加 start_of_wordend_of_word 字符

return [[[self.start_of_word]+[self.char2id[c] for c in w]+[self.end_of_word] for w in s] for s in sents]

在这里插入图片描述
(f) 实现 pad_sents_char(),即将每个单词pad成最长单词长度(21),并且对每个句子进行pad,pad成batch中最长句子长度

   sents_padded = []
   max_len = max(len(s) for s in sents)
   for s in sents:
       pad_sent = []
       for w in s:  #for each word
           if len(w)<max_word_length:
               pad_sent.append(w+[char_pad_token]*(max_word_length-len(w))) #pad word
           else:
               pad_sent.append(w[:max_word_length]) #truncate word
       if len(pad_sent) < max_len:  #pad sentence with max_len_word
           if max_len>len(pad_sent):
               pad_sent.extend([char_pad_token]*max_word_length for i in range(max_len-len(pad_sent)))
       sents_padded.append(pad_sent)

在这里插入图片描述
(g) 使用上面实现的两个函数实现 to_input_tensor_char() ,并根据要求对维度进行调整

        sents = self.words2charindices(sents)  #word_ids (list[list[list[int]]])
        tensor = pad_sents_char(sents,self.char2id['<pad>']) #list (batch_size,max_len,max_word_len)
        tensor = torch.tensor(tensor,dtype=torch.long,device = device)
        return tensor.permute([1,0,2])

(h) 实现Highway,其输入为卷积层的输出。构建两个线性层,一个用于对输入线性变换后,再激活得到激活值。另一个对输入进行线性变换后,使用sigmoid,用于控制输出中哪些部分是激活值,哪些部分是原输入。

class Highway(nn.Module):
    def __init__(self,e_word):
        """ initial two Linear to construct highway connection
        @param  (int):the dimention of the word

        """
        super(Highway,self).__init__()
        self.proj = nn.Linear(e_word, e_word)
        self.gate = nn.Linear(e_word, e_word)
        
    
    def forward(self,conv_out:torch.Tensor) -> torch.Tensor:
        """ highway connection
        @param conv_out (torch.Tensor): the convolution layer's output,its shape is (batch_size, e_word)
        @return highway_out (torch.Tensor): tensor of (batch_size, e_word)
        """
        
        x_proj = F.relu(self.proj(conv_out))
        x_gate = torch.sigmoid(self.gate(conv_out))
        highway_out = torch.mul(x_proj,x_gate) + torch.mul(conv_out,1-x_gate)
        
        return highway_out  

可以通过给模型参数赋予特定的值来进行测试模型输出的最终结果是否为期望结果,具体可以参考前几个assignment的sanity_check.py ,下面初始化模型参数的一个例子
在这里插入图片描述
由于模型简单,我们这里只简单验证下输出的形状是否满足要求

if __name__ == '__main__':
    high = Highway(5)
    input = torch.randn(4,5)
    pred = high(input)
    assert(pred.shape==input.shape)

(i) 实现CNN

  • 我们这里使用一维卷积,需要了解 nn.Conv1d(),nn.MaxPool1d().
class CNN(nn.Module):
    def __init__(self,e_char,filter_size,m_word = 21,kernel_size = 5):
        """initial cnn
        @param filter_size (int): the number of filter,also the e_word
        @param e_char (int): the demention of char
        @param kernel_size (int): the filter's length
        """        
        super(CNN,self).__init__()
        self.conv1d = nn.Conv1d(e_char,filter_size,kernel_size)
        self.pool = nn.MaxPool1d(m_word - kernel_size + 1)
        
    def forward(self,reshaped) -> torch.Tensor:
        """
        @param reshaped (torch.Tensor): the char embedding of sentences, which is tensor of (batch_size,e_char,max_word_len)
        @return conv_out (torch.Tensor):the ouput of cnn, which is tensor of (bat_size,e_word)
        """
        conv_out = F.relu(self.conv1d(reshaped))
        conv_out = self.pool(conv_out)
        
        return conv_out.squeeze(-1) #最后一个维度被maxpool了,因此将最后一个维度去掉

这里依旧简单验证下输出维度是否满足

if __name__ == '__main__':
    cnn = CNN(50,4) #(char_embedding, filter_number or word_embedding)
    input = torch.randn(10,50,21) #(batch_size or words, char_embedding, max_word_len)
    assert(cnn(input).shape==(10,4))

(j) 实现 ModelEmbeddings 中的 __init__ 和 forward

  • forward() 需要将 x p a d d e d x_{padded} xpadded 转换为 x w o r d _ e m b x_{word\_emb} xword_emb
  • __init__中需要定义 Embedding,可以自动求导,来训练字符嵌入
__init__:
        self.pad_token_idx = vocab.char2id['<pad>']
        self.dropout_rate = 0.3
        self.e_char = 50
        self.embed_size = embed_size
        self.cnn = CNN(self.e_char,embed_size)
        self.highway = Highway(self.embed_size)
        self.dropout = nn.Dropout(self.dropout_rate)
        self.char_embedding = nn.Embedding(len(vocab.char2id),self.e_char,self.pad_token_idx)
forward():
        word_embedding = []
        for words in input:  #拿出所有句子的第一个单词 (batch_size,max_word_length) 
            char_embedd = self.char_embedding(words) #get all words' embedding (batch_size, max_word_length,e_char)
            reshaped = char_embedd.permute([0,2,1]) #(batch_size,e_char,max_word_length)
            conv_out = self.cnn(reshaped) # 对词进行cnn,maxpool (batch_size,e_word) 
            highway = self.dropout(self.highway(conv_out)) #(sentence_size,e_word)
            word_embedding.append(highway) 
        
        word_emb = torch.stack(word_embedding)  #默认dim = 0 (sentence_length,batch_size,embed_size)
        return word_emb
  • 具体解释请看代码中的注释
    在这里插入图片描述
    (k) 实现NMT中的 forward(),只需要将原来的 to_input_tensor 换成 to_input_tensor_char 即可,这样得到的就是List[List[List[int]]] 得到的tensor。即使用用字符表示的句子,然后在 encode 中使用 ModelEmbeddings 将会获得用字符级嵌入得到的词嵌入。
        source_padded_chars = self.vocab.src.to_input_tensor_char(source,device=self.device) #int(max_sentence_length, batch_size, max_word_length)
        target_padded = self.vocab.tgt.to_input_tensor(target, device=self.device)
        target_padded_chars = self.vocab.tgt.to_input_tensor_char(target,device = self.device)
        enc_hiddens, dec_init_state = self.encode(source_padded_chars, source_lengths)
        enc_masks = self.generate_sent_masks(enc_hiddens, source_lengths)
        combined_outputs = self.decode(enc_hiddens, enc_masks, dec_init_state, target_padded_chars)

(l) 将其他代码补全(从a4中复制过来即可),进行本地训练与测试

  • 起初训练得到了一个很好的结果,满足题目要求了
    在这里插入图片描述
  • 结果测试时分数很差
    在这里插入图片描述
  • 查看输出的翻译也非常不好
    在这里插入图片描述
  • 找了半天的bug,一直以为哪个地方维度调整错误了,或者是别的什么原因,结果到最后发现 原来是历史遗留问题,NMT 中的 step函数 写错了一句代码,
    state,o_t,_ = self.step(Ybar_t, dec_state, enc_hiddens, enc_hiddens_proj, enc_masks)
    这里的state 错了,每个时间步都需要使用上一个时间步输出的隐藏状态,所以应该改为
    dec_state,o_t,_ = self.step(Ybar_t, dec_state, enc_hiddens, enc_hiddens_proj, enc_masks)
    写代码还是得细心。。。
  • 最后重新训练,测试的结果如下
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

Character-based LSTM decoder for NMT

在这里插入图片描述
构建一个基于LSTM的字符级解码器,当遇到<UNK>时,使用此解码器来生成OOV词汇。
几个重要的说明

  • CharDecoderLSTM 起始 h 0 , c 0 h_0,c_0 h0c0使用 combined output vector,即上一时间步产生的隐藏层状态 + 注意力加权,其中 + 表示拼接。然后让每步得到的隐藏层状态 h i h_i hi 通过一个线性层,输出的维度与字符数目相同。最后使用softmax求得每个位置取每个字符的概率
  • 训练时对 target sentence 中所有单词进行训练,不仅仅是用 <UNK> 表示的单词,损失函数使用交叉熵损失函数,一个单词的所有位置(每个字符)求和,再对整个batch单词求和(而不求平均),并且与基于单词模型的loss相加。同时训练基于单词的模型,和基于字符的decoder。
  • 测试时,首先使用基于单词的NMT系统,使用beam search 翻译出 target sentence,若存在 <UNK> 则使用基于字符的decoder产生单词, h 0 , c 0 h_0,c_0 h0,c0 使用combined ouput vector,使用greedy search,即选择每个位置概率最大的字符作为结果。当产生 <END> 时,基于字符的decoder输出产生的单词

(a) 完成 CharDecoder 的 __init__

 super(CharDecoder,self).__init__()
 self.padding_idx = target_vocab.char2id['<pad>']
 self.charDecoder = nn.LSTM(char_embedding_size,hidden_size)
 self.char_output_projection = nn.Linear(hidden_size, len(target_vocab.char2id)) #将隐藏层输出维度调整为字符表大小,用于预测
 self.decoderCharEmb = nn.Embedding(len(target_vocab.char2id),char_embedding_size,padding_idx = self.padding_idx) #字符嵌入
 self.target_vocab = target_vocab

在这里插入图片描述

(b) 完成char_decoder.py 中的 forward() 函数

 X = self.decoderCharEmb(input) #(length,batch_size,embedding_size)
 hidden_states,dec_hidden = self.charDecoder(X,dec_hidden) #(length,batch_size,hidden_size) (batch_size,hidden_size)
 scores = self.char_output_projection(hidden_states) #(length,batch_size,vocab_size)
 
 return scores,dec_hidden

在这里插入图片描述

(c) 完成 train_forward(),计算loss

scores,dec_hidden = self.forward(char_sequence[:-1],dec_hidden) #(length,batch_size,vocab_size)
entroy = nn.CrossEntropyLoss(ignore_index = self.padding_idx,reduction = 'sum') #不计算padding位置,将所有loss求和
loss = entroy(scores.permute(1,2,0),char_sequence[1:].permute(1,0)) 
#remove 'START' (batch_size,vocab_size,length) (batch_size,length) 第一维和最后一维要相同

return loss

具体看代码中的注释,尤其要知道nn.CrossEntropyLoss() 的用法
在这里插入图片描述

(d) 完成 decode_greedy()

   decodeWords = []
   words = []
   start_char = self.target_vocab.start_of_word
   end_char = self.target_vocab.end_of_word
   batch_size = initialStates[0].shape[1]   
   current_char = torch.tensor([[start_char] * batch_size],device = device) #(len:1, batch_size)
   dec_state = initialStates
   
   for _ in range(max_length):
       scores,dec_state = self.forward(current_char,dec_state) #(len:1,batch_size,vocab_size)
       current_char = scores.argmax(-1) #(len:1, batch_size)
       words.append(current_char)  
   
   words = torch.cat(words).permute(1,0) #(batch_size, len)
   
   for w in words:
       word = ""
       for ch in w:
           if ch == end_char:
               break
           word += self.target_vocab.id2char[int(ch)]
       decodeWords.append(word)
               
   return decodeWords

这里使用的LSTM,而每个时间步的输入都是上个时间步的输出,因此我们需要一个时间步一个时间步的计算,因此输入维度为 (len:1,batch_size,vocab_size),为了方便batch操作,在解码时,没有遇到<END>就结束。因此我们在拼接单词时,遇到<END>就结束,将得到的单词保存下来。
在这里插入图片描述
(e) 本地训练与测试:

  • 同样,自己从 run.sh 中粘贴命令执行
  • 训练:python run.py train --train-src=./en_es_data/train_tiny.es --train-tgt=./en_es_data/train_tiny.en --dev-src=./en_es_data/dev_tiny.es --dev-tgt=./en_es_data/dev_tiny.en --vocab=vocab_tiny_q2.json --batch-size=2 --max-epoch=201 --valid-niter=100
    在这里插入图片描述
  • 测试:首先新建文件夹 outputs 并且新建
    outputs/test_outputs_local_q2.txt
    python run.py decode model.bin ./en_es_data/test_tiny.es ./en_es_data/test_tiny.en outputs/test_outputs_local_q2.txt
    在这里插入图片描述
  • 训练时出现了一个错误,提示tensor不连续,
    target_chars = target_padded_chars[1:].view(-1, max_word_len)
    这里target_padded_chars的维度是(max_sentence_length, batch_size, max_word_length)。
    在使用view前应保证tensor是连续的,因此使用contiguous()
    target_chars = target_padded_chars[1:].contiguous().view(-1, max_word_len)

(f)

Analyzing NMT Systems

(a) 下面的表是西班牙单词 traducir 的各种形式以及与英语的对应,从 vocab.json 中找出哪些形式存在,解释为什么这对基于单词的NMT系统不友好,为什么对基于字符的NMT系统有好处?
在这里插入图片描述

  • 只有 traducir:5152 和traduce:8764 存在,当源语言句子进行嵌入时,只有traduce才有对应的词向量,而其他形式将会被当作 <UNK>进行嵌入,因此训练会有偏差。而基于字符没有OOV问题,能够将源语言句子通过字符嵌入的方式得到每个词的嵌入。而这些形式的前六个字符大多都是相同的(除了traduzco),因此我们的CNN的某些 filter 会学习到这样的字符组合是翻译的意思,而末尾的字符表示了不同的形式(比如主语的不同,时态的不同等)

(b) 将Word2Vec 和我们NMT使用CharCNN训练出词向量进行比较,下面这个网站可以可视化。
在这里插入图片描述

(i) 下面是word2vec中一些词的最接近的词
在这里插入图片描述
在这里插入图片描述

  • 同时在图中还会显示单词位置
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

(ii) 下载我们CharCNN训练好的词向量,上传查看
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(iii) 比较两种词向量,简要形容word2vec和charCNN分别建模了什么相似性,解释两者所用方法的差异是如何导致这两种结果的差异的。

  • word2vec:对词义相似性进行建模
  • charCNN:对词形相似性进行建模
  • 原因:因为 word2vec 方法认为词义相似的单词具有相似的上下文,而charCNN是通过基于window的特征提取来建模,因此结构上相似的单词在特征空间里距离更近

(c) 找出一个使用char-based decoder 产生可接受的翻译,而基于单词的模型产生<UNK>的例子,和一个基于字符的解码器也没有产生可接受翻译的例子
这里需要使用训练模型,无机器且训练时间较长,以后再补。

总结

  • 本次实验比起之前的实验要麻烦一些,之前的实验代码注释里都有详细的Hint,告诉你怎样完成某个函数。还给了需要学会的重要函数的doc链接,而本次实验只告诉你需要完成什么功能,甚至需要自己完成两个类。
  • 完成了两个类 CNN 和 Highway,学会了这两个神经网络中重要组件的实现
  • 本次实验是在 assignment 4 的基础上进行了修改,增加了字符级的编码和解码部分。增加了两个我们自己实现的两个类,对重要函数进行了修改,仅此而已。我们搭建神经网络时,可以学习学习,将各个部分分开,如果要对架构进行修改的话,不至于修改很多代码。有点像软件构造中的内容。
  • 大的层面上解决不了,从小的层面进行尝试。在编码阶段,有些OOV单词无法被编码成特定词向量,只能用<UNK>来替代。既然单词是由字母组成的,那我们是不是可以考虑,用字母组合的一些规律来编码单词,即使用CNN(当然也可以使用RNN)来获得单词嵌入。在解码阶段也是一样,只能产生<UNK>,不能产生单词,那我们是否可以产生字母来组合成单词。换一个层面来考虑,有时候也能解决问题。
  • 对矩阵的维度的含义更加熟悉了,对矩阵维度的变化也更加熟练了。
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值