基於PyTorch的Seq2Seq翻譯模型詳細註釋介紹(一)

Seq2Seq是目前主流的深度學習翻譯模型,在自然語言翻譯,甚至跨模態知識映射方面都有不錯的效果。在軟件工程方面,近年來也得到了廣泛的應用,例如:

Jiang, Siyuan, Ameer Armaly, and Collin McMillan. "Automatically generating commit messages from diffs using neural machine translation." In Proceedings of the 32nd IEEE/ACM International Conference on Automated Software Engineering, pp. 135-146. IEEE Press, 2017.

Hu, Xing, Ge Li, Xin Xia, David Lo, and Zhi Jin. "Deep code comment generation." In Proceedings of the 26th Conference on Program Comprehension, pp. 200-210. ACM, 2018.

這裏我結合PyTorch給出的Seq2Seq的示例代碼來簡單總結一下這個模型實現時的細節以及PyTorch對應的API。PyTorch在其官網上有Tutorial:https://pytorch.org/tutorials/intermediate/seq2seq_translation_tutorial.html,其對應的GitHub鏈接是:https://github.com/pytorch/tutorials/blob/master/intermediate_source/seq2seq_translation_tutorial.py。這裏就以這段代碼爲例來進行總結:

在上面那個官網的鏈接中給出了對應數據的下載鏈接:https://download.pytorch.org/tutorial/data.zip,另外,其實網上很多教程也都是翻譯上面這個官方教程的,我也參考了一些,主要包括:

https://www.cnblogs.com/HolyShine/p/9850822.html

https://www.cnblogs.com/www-caiyin-com/p/10123346.html

http://www.pianshen.com/article/5376154542/

所以大家可以以這些教程爲基礎,我也只是在它們的基礎上進行一些補充和解釋,所以並不會像上面教程一樣給出完整的解釋,只是總結一些我覺得重要的內容。首先,初始化編碼這些就不總結了,大家看看現有的教程就理解。從Encoder開始總結:

class EncoderRNN(nn.Module):
    def __init__(self, input_size, hidden_size):
        super(EncoderRNN, self).__init__()#對繼承自父類的屬性進行初始化。
        self.hidden_size = hidden_size

        self.embedding = nn.Embedding(input_size, hidden_size)#對輸入做初始化Embedding。
        self.gru = nn.GRU(hidden_size, hidden_size)#Applies a multilayer gated recurrent unit (GRU) RNN to an input sequence.

    def forward(self, input, hidden):
        embedded = self.embedding(input).view(1, 1, -1)#view實際上是對現有tensor改造的方法。
        output = embedded
        output, hidden = self.gru(output, hidden)
        return output, hidden

    def initHidden(self):
        return torch.zeros(1, 1, self.hidden_size, device=device)#初始化,生成(1,1,256)維的全零Tensor。

雖然只有短短几行,可還是有些需要討論的內容:nn.Embedding是進行初始embedding,當然,這種embedding是完全隨機的,並不通過訓練或具有實際意義,我覺得網上有些文章連這一點都沒搞清楚(例如這裏的解釋就是錯誤的:https://my.oschina.net/earnp/blog/1113896),具體可以參看這裏的討論:https://blog.csdn.net/qq_36097393/article/details/88567942。其參數含義可以參考這個解釋:nn.Embedding(2, 5),這裏的2表示有2個詞,5表示維度爲5,其實也就是一個2x5的矩陣,所以如果你有1000個詞,每個詞希望是100維,你就可以這樣建立一個word embeddingnn.Embedding(1000, 100)。也可以運行下面我總結示例代碼:

import torch
import torch.nn as nn

word_to_ix={'hello':0, 'world':1}
embeds=nn.Embedding(2,5)
hello_idx=torch.LongTensor([word_to_ix['hello']])
world_idx=torch.LongTensor([word_to_ix['world']])
hello_embed=embeds(hello_idx)
print(hello_embed)
world_embed=embeds(world_idx)
print(world_embed)

具體含義相信大家一看便知,可以試着跑一下(每次print的結果不相同,並且也沒啥實際含義)。

另外就是.view(1, 1, -1)的含義,說實話我也沒搞清楚過,其實在stackoverflow上已經有人討論了這個問題:

https://stackoverflow.com/questions/42479902/how-does-the-view-method-work-in-pytorch

大家看看就知,我這裏也把上面別人給出的例子提供一下:

import torch
a = torch.range(1, 16)
print(a)
a = a.view(4, 4)
print(a)

Encoder就簡單總結這些。下面直接進入到帶注意力機制的解碼器的總結(爲了幫助理解,下面增加了一些註釋,說明每一步Tensor的緯度,我個人覺得還是能夠便於理解的):

class AttnDecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):#MAX_LENGTH在翻譯任務中定義爲10
        super(AttnDecoderRNN, self).__init__()
        self.hidden_size = hidden_size
        self.output_size = output_size#這裏的output_size是output_lang.n_words
        self.dropout_p = dropout_p#dropout的比例。
        self.max_length = max_length

        self.embedding = nn.Embedding(self.output_size, self.hidden_size)
        self.attn = nn.Linear(self.hidden_size * 2, self.max_length)
        self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)#按照維度要求,進行線性變換。
        self.dropout = nn.Dropout(self.dropout_p)
        self.gru = nn.GRU(self.hidden_size, self.hidden_size)
        self.out = nn.Linear(self.hidden_size, self.output_size)

    def forward(self, input, hidden, encoder_outputs):
        
        print(input)
        print('size of input: '+str(input.size()))
        print('size of self.embedding(input): '+str(self.embedding(input).size()))
        
        embedded = self.embedding(input).view(1, 1, -1)
        print('size of embedded: '+str(embedded.size()))
        
        embedded = self.dropout(embedded)
        print('size of embedded[0]: '+str(embedded[0].size()))
        print('size of torch.cat((embedded[0], hidden[0]), 1): '+str(torch.cat((embedded[0], hidden[0]), 1).size()))
        print('size of self.attn(torch.cat((embedded[0], hidden[0]), 1)): '+str(self.attn(torch.cat((embedded[0], hidden[0]), 1)).size()))
        
        #Size of embedded: [1,1,256]
        #Size of embedded[0]: [1,256]
        #Size of size of torch.cat((embedded[0], hidden[0]), 1): [1,512]
        
        # 此處相當於學出來了attention的權重
        # 需要注意的是torch的concatenate函數是torch.cat,是在已有的維度上拼接,按照代碼中的寫法,就是在第二個緯度上拼接。
        # 而stack是建立一個新的維度,然後再在該緯度上進行拼接。
        attn_weights = F.softmax(
            self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)#這裏的F.softmax表示的是torch.nn.functional.softmax
        
        #Size of attn_weights: [1,10]
        #Size of attn_weights.unsqueeze(0): [1,1,10]
        #Size of encoder_outputs: [10,256]
        #Size of encoder_outputs.unsqueeze(0): [1,10,256]
        
        #unsqueeze的解釋是Returns a new tensor with a dimension of size one inserted at the specified position.
        attn_applied = torch.bmm(attn_weights.unsqueeze(0),
                                 encoder_outputs.unsqueeze(0))#bmm本質上來講是個批量的矩陣乘操作。
        
        #Size of attn_applied: [1,1,256]
        output = torch.cat((embedded[0], attn_applied[0]), 1)
        #Size of output here is: [1,512]
        print('size of output (at this location): '+str(output.size()))
        output = self.attn_combine(output).unsqueeze(0)
        #Size of output here is: [1,1,256]
        #print(output)
        output = F.relu(output)#rectified linear unit function element-wise:
        #print(output)
        output, hidden = self.gru(output, hidden)
        output = F.log_softmax(self.out(output[0]), dim=1)
        print('')
        print('------------')
        return output, hidden, attn_weights

    def initHidden(self):
        return torch.zeros(1, 1, self.hidden_size, device=device)

首先是dropout,關於dropout可以首先參考一下PyTorch的官方解釋:

https://pytorch.org/docs/stable/nn.html?highlight=nn%20dropout#torch.nn.Dropout

簡單來說,就是During training, randomly zeroes some of the elements of the input tensor with probability p using samples from a Bernoulli distribution,有朋友給出了很詳細的討論和解釋:

https://blog.csdn.net/stdcoutzyx/article/details/49022443

其次應該注意一下nn.Linear的含義和作用,還是給出官網的解釋:Applies a linear transformation to the incoming data,類似地,可以參考一下我下面給出的示例代碼:

import torch
import torch.nn as nn
m = nn.Linear(2, 3)
input = torch.randn(2, 2)
print(input)
output = m(input)
print(output)

接下來解釋一下torch.bmm。按照PyTorch官網的解釋,https://pytorch.org/docs/stable/torch.html?highlight=torch%20bmm#torch.bmm

torch.bmm起的作用是:Performs a batch matrix-matrix product of matrices stored in batch1 and batch2,這樣的解釋還是太抽象,其實通過一個例子就很好懂了,實際就是一個批量矩陣乘法:

import torch
batch1=torch.randn(2,3,4)
print(batch1)
batch2=torch.randn(2,4,5)
print(batch2)
res=torch.bmm(batch1,batch2)
print(res)

具體的乘法規則是:If batch1 is a (b×n×m) tensor, batch2 is a (b×m×p) tensor, out will be a (b×n×p) tensor.

關於torch.cat,還是以PyTorch官網給出的例子做一個簡單說明:

Concatenates the given sequence of seq tensors in the given dimension. 例子如下:

import torch
x=torch.randn(2,3)
print(x)
print(torch.cat((x, x, x), 0))
print(torch.cat((x, x, x), 1))

這裏就先總結到這裏,會在下一篇博客中繼續總結。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章