seq2seq 源碼分析(PyTorch版)

torch.__version__

版本爲-1.1.0

1.首先引入包,定義 填充符 PAD_token、開始符 SOS_token 、結束符 EOS_token

# 在開頭加上from __future__ import print_function這句之後,如果你的python版本是python2.X,你也得按照python3.X那樣使用這些函數。
# Python提供了__future__模塊,把下一個新版本的特性導入到當前版本,於是我們就可以在當前版本中測試一些新版本的特性。
# division 精確除法  
# print_function 打印函數
# unicode_literals 這個是對字符串使用unicode字符
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import torch
import torch.nn as nn
import torch.nn.functional as F
import re
import os
import unicodedata
import numpy as np

device = torch.device("cpu")


MAX_LENGTH = 10  # Maximum sentence length

# Default word tokens
PAD_token = 0  # Used for padding short sentences
SOS_token = 1  # Start-of-sentence token
EOS_token = 2  # End-of-sentence token

2. 模型介紹

seq2seq模型用於輸入是可變長度序列,輸出也是可變長度序列的情況。

Encoder:每一個時間步輸出一個輸出向量和一個隱含向量,隱含向量以此傳遞,輸出向量則被記錄。

Decoder:使用上下文向量和Decoder的隱含向量生成下一個詞,直到輸出“EOS_token”爲止。解碼器中使用注意機制(Global attention)來幫助它在生成輸出時“注意”輸入的某些部分。

全局注意力機制論文:https://arxiv.org/abs/1508.04025

 

3.數據處理

將文本的每個token生成一個詞彙表,映射爲數字。

normalizeString 函數將字符串中的所有字符轉換爲小寫並刪除所有非字母字符。

indicesFromSentence函數 將單詞的句子返回相應的單詞索引序列。

class Voc:
    def __init__(self, name):
        self.name = name
        self.trimmed = False
        self.word2index = {}
        self.word2count = {}
        self.index2word = {PAD_token: "PAD", SOS_token: "SOS", EOS_token: "EOS"}
        self.num_words = 3  # Count SOS, EOS, PAD-0,1,2
        
    # 分詞
    def addSentence(self, sentence):
        for word in sentence.split(' '):
            self.addWord(word)
            
    # 三個詞典的填充 word2index、word2count、index2word 
    def addWord(self, word):
        if word not in self.word2index:
            self.word2index[word] = self.num_words
            self.word2count[word] = 1
            self.index2word[self.num_words] = word
            self.num_words += 1
        else:
            self.word2count[word] += 1

    # 刪除低於設定值的單詞  trim修剪
    def trim(self, min_count):
        if self.trimmed:
            return
        self.trimmed = True
        keep_words = []
        for k, v in self.word2count.items():
            if v >= min_count:
                keep_words.append(k)

        print('keep_words {} / {} = {:.4f}'.format(
            len(keep_words), len(self.word2index), len(keep_words) / len(self.word2index)
        ))
        # 重新初始化詞典
        self.word2index = {}
        self.word2count = {}
        self.index2word = {PAD_token: "PAD", SOS_token: "SOS", EOS_token: "EOS"}
        self.num_words = 3 # Count default tokens
        for word in keep_words:
            self.addWord(word)


# 全部改成小寫字符,刪除非字母字符
def normalizeString(s):
    # 小寫字符
    s = s.lower()
    # 刪除非字母字符  
    # re.sub用於替換字符串中的匹配項  re.sub(正則中的模式字符串, 替換的字符串, 原始字符串)
    s = re.sub(r"([.!?])", r" \1", s)
    # [^...]  不在[]中的字符:[^abc] 匹配除了a,b,c之外的字符。
    s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)
    return s


# 將文本每一段的詞轉換成序號 word2index,並且添加結尾符號 EOS_token
def indexesFromSentence(voc, sentence):
    return [voc.word2index[word] for word in sentence.split(' ')] + [EOS_token]

4.定義編碼器

class EncoderRNN(nn.Module):
    def __init__(self, hidden_size, embedding, n_layers=1, dropout=0):
        super(EncoderRNN, self).__init__()
        self.n_layers = n_layers
        self.hidden_size = hidden_size
        self.embedding = embedding

        # 初始化 GRU
        # input_size、hidden_size都設置爲 hidden_size
        self.gru = nn.GRU(hidden_size, hidden_size, n_layers,
                          dropout=(0 if n_layers == 1 else dropout), bidirectional=True)

    def forward(self, input_seq, input_lengths, hidden=None):
        # 將單詞索引 轉換爲 embeddings
        embedded = self.embedding(input_seq)
        # pack sequence -> recurrent network -> unpack sequence
        # 按列壓緊,B x T x *,壓縮的理解如下圖
        packed = torch.nn.utils.rnn.pack_padded_sequence(embedded, input_lengths)
        # Forward GRU
        outputs, hidden = self.gru(packed, hidden)
        # 填充回原始排列,pack_padded_sequence()的逆操作,返回T x B x *(T最長序列的長度,B-batch size)
        outputs, _ = torch.nn.utils.rnn.pad_packed_sequence(outputs)
        # 雙向GRU輸出的和 Sum bidirectional GRU outputs
        outputs = outputs[:, :, :self.hidden_size] + outputs[:, : ,self.hidden_size:]
        # 返回輸出,最終隱藏狀態
        return outputs, hidden

理解 torch.nn.utils.rnn.pack_padded_sequence:

5.定義解碼器的注意模塊

注意力通過評分函數來獲取每個編碼器隱藏狀態的分數(標量)。評分函數在本程序中有三種:

將得分放到softmax函數層,使softmax得分(標量)之和爲1

Soft Attention 是對所有的信息進行加權求和。Hard Attention是選擇最大信息的那一個。

本程序使用Soft Attention:將每個編碼器的隱藏狀態與其softmaxed得分(標量)相乘,就能獲得對齊向量。這就是對齊機制

上下文向量:將對齊向量聚合起來。

# Luong 注意力層
class Attn(nn.Module):
    def __init__(self, method, hidden_size):
        # 如果子類重寫了父類的構造方法,那麼子類的構造方法必須調用父類的構造方法
        super(Attn, self).__init__()
        self.method = method
        if self.method not in ['dot', 'general', 'concat']:
            # raise 引發一個異常
            raise ValueError(self.method, "is not an appropriate attention method.")
        self.hidden_size = hidden_size
        # self.attn 爲不同的注意力計算方法,選擇不同的線性層
        # torch.nn.Linear(in_features, out_features, bias=True)
        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)
            # torch.FloatTensor 	torch.cuda.FloatTensor
            # nn.Parameter理解爲類型轉換函數
            # 將一個不可訓練的類型Tensor轉換成可以訓練的類型parameter並將這個parameter綁定到這個module裏面,所以在參數優化的時候可以進行優化的
            self.v = nn.Parameter(torch.FloatTensor(hidden_size))
    # 乘法-求和
    def dot_score(self, hidden, encoder_output):
        return torch.sum(hidden * encoder_output, dim=2)
    # 線性層-乘法-求和
    def general_score(self, hidden, encoder_output):
        energy = self.attn(encoder_output)
        return torch.sum(hidden * energy, dim=2)
    
    # 擴大維度-拼接-線性層-tanh-乘法-求和
    def concat_score(self, hidden, encoder_output):
        # expand單個維度擴大爲更大的尺寸,擴展的是encoder_output.size(0)
        # cat連接操作,cat(inputs, dimension=0),在給定維度上對輸入的張量序列進行連接操作
        energy = self.attn(torch.cat((hidden.expand(encoder_output.size(0), -1, -1), encoder_output), 2)).tanh()
        return torch.sum(self.v * energy, dim=2)

    def forward(self, hidden, encoder_outputs):
        # 根據給定的方法,計算注意力分數
        if self.method == 'general':
            attn_energies = self.general_score(hidden, encoder_outputs)
        elif self.method == 'concat':
            attn_energies = self.concat_score(hidden, encoder_outputs)
        elif self.method == 'dot':
            attn_energies = self.dot_score(hidden, encoder_outputs)

        #  轉置max_length和batch_size維度
        attn_energies = attn_energies.t()

        # 返回softmax歸一化概率分數(增加維度)
        return F.softmax(attn_energies, dim=1).unsqueeze(1)

unsqueeze和tanh的用法如下所示:

         

6.定義解碼器

 

7.

8.

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