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.

发布了93 篇原创文章 · 获赞 119 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_18310041/article/details/95523771