跳转至

8 RNN案例 seq2seq英译法

学习目标

  • 更深一步了解seq2seq模型架构和翻译数据集
  • 掌握使用基于GRU的seq2seq模型架构实现翻译的过程
  • 掌握Attention机制在解码器端的实现过程

1 seq2seq介绍

1.1 seq2seq模型架构

avatar

  • seq2seq模型架构分析:

  • seq2seq模型架构包括三部分,分别是encoder(编码器)、decoder(解码器)、中间语义张量c。其中编码器和解码器的内部实现都使用了GRU模型

  • 图中表示的是一个中文到英文的翻译:欢迎 来 北京 → welcome to BeiJing。编码器首先处理中文输入"欢迎 来 北京",通过GRU模型获得每个时间步的输出张量,最后将它们拼接成一个中间语义张量c;接着解码器将使用这个中间语义张量c以及每一个时间步的隐层张量, 逐个生成对应的翻译语言
  • 我们的案例通过英译法来讲解seq2seq设计与实现。

2 数据集介绍

# 数据集在虚拟机/root/data/下
- data/
        - eng-fra-v2.txt  
i am from brazil .  je viens du bresil .
i am from france .  je viens de france .
i am from russia .  je viens de russie .
i am frying fish .  je fais frire du poisson .
i am not kidding .  je ne blague pas .
i am on duty now .  maintenant je suis en service .
i am on duty now .  je suis actuellement en service .
i am only joking .  je ne fais que blaguer .
i am out of time .  je suis a court de temps .
i am out of work .  je suis au chomage .
i am out of work .  je suis sans travail .
i am paid weekly .  je suis payee a la semaine .
i am pretty sure .  je suis relativement sur .
i am truly sorry .  je suis vraiment desole .
i am truly sorry .  je suis vraiment desolee .

3 案例步骤

基于GRU的seq2seq模型架构实现翻译的过程:

  • 第一步: 导入工具包和工具函数
  • 第二步: 对持久化文件中数据进行处理, 以满足模型训练要求
  • 第三步: 构建基于GRU的编码器和解码器
  • 第四步: 构建模型训练函数, 并进行训练
  • 第五步: 构建模型评估函数, 并进行测试以及Attention效果分析

1 导入工具包和工具函数

# 用于正则表达式
import re
# 用于构建网络结构和函数的torch工具包
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
# torch中预定义的优化方法工具包
import torch.optim as optim
import time
# 用于随机生成数据
import random
import matplotlib.pyplot as plt

# 设备选择, 我们可以选择在cuda或者cpu上运行你的代码
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 起始标志
SOS_token = 0
# 结束标志
EOS_token = 1
# 最大句子长度不能超过10个 (包含标点)
MAX_LENGTH = 10
# 数据文件路径
data_path = './data/eng-fra-v2.txt'

# 文本清洗工具函数
def normalizeString(s):
    """字符串规范化函数, 参数s代表传入的字符串"""
    s = s.lower().strip()
    # 在.!?前加一个空格  这里的\1表示第一个分组   正则中的\num
    s = re.sub(r"([.!?])", r" \1", s)
    # s = re.sub(r"([.!?])", r" ", s)
    # 使用正则表达式将字符串中 不是 大小写字母和正常标点的都替换成空格
    s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)
    return s

2 数据预处理

对持久化文件中数据进行处理, 以满足模型训练要求

1 清洗文本和构建文本字典

  • 清洗文本和构建文本字典思路分析
# my_getdata() 清洗文本构建字典思路分析
# 1 按行读文件 open().read().strip().split(\n) my_lines
# 2 按行清洗文本 构建语言对 my_pairs[] tmppair[]
# 2-1格式 [['英文', '法文'], ['英文', '法文'], ['英文', '法文'], ['英文', '法文']....]
# 2-2调用清洗文本工具函数normalizeString(s)
# 3 遍历语言对 构建英语单词字典 法语单词字典 my_pairs->pair->pair[0].split(' ') pair[1].split(' ')->word
# 3-1 english_word2index english_word_n french_word2index french_word_n
# 其中 english_word2index = {0: "SOS", 1: "EOS"}  english_word_n=2
# 3-2 english_index2word french_index2word
# 4 返回数据的7个结果
# english_word2index, english_index2word, english_word_n,
# french_word2index, french_index2word, french_word_n, my_pairs
  • 代码实现
def my_getdata():

    # 1 按行读文件 open().read().strip().split(\n)
    my_lines = open(data_path, encoding='utf-8').read().strip().split('\n')
    print('my_lines--->', len(my_lines))

    # 2 按行清洗文本 构建语言对 my_pairs
    # 格式 [['英文句子', '法文句子'], ['英文句子', '法文句子'], ['英文句子', '法文句子'], ... ]
    # tmp_pair, my_pairs = [], []
    # for l in my_lines:
    #     for s in l.split('\t'):
    #         tmp_pair.append(normalizeString(s))
    #     my_pairs.append(tmp_pair)
    #     tmp_pair = []
    my_pairs = [[normalizeString(s) for s in l.split('\t')] for l in my_lines]
    print('len(pairs)--->', len(my_pairs))

    # 打印前4条数据
    print(my_pairs[:4])

    # 打印第8000条的英文 法文数据
    print('my_pairs[8000][0]--->', my_pairs[8000][0])
    print('my_pairs[8000][1]--->', my_pairs[8000][1])

    # 3 遍历语言对 构建英语单词字典 法语单词字典
    # 3-1 english_word2index english_word_n french_word2index french_word_n
    english_word2index = {"SOS": 0, "EOS": 1}
    english_word_n = 2

    french_word2index = {"SOS": 0, "EOS": 1}
    french_word_n = 2

    # 遍历语言对 获取英语单词字典 法语单词字典
    for pair in my_pairs:
       for word in pair[0].split(' '):
           if word not in english_word2index:
               english_word2index[word] = english_word_n
               english_word_n += 1

       for word in pair[1].split(' '):
           if word not in french_word2index:
               french_word2index[word] = french_word_n
               french_word_n += 1

    # 3-2 english_index2word french_index2word
    english_index2word = {v:k for k, v in english_word2index.items()}
    french_index2word = {v:k for k, v in french_word2index.items()}

    print('len(english_word2index)-->', len(english_word2index))
    print('len(french_word2index)-->', len(french_word2index))
    print('english_word_n--->', english_word_n, 'french_word_n-->', french_word_n)

    return english_word2index, english_index2word, english_word_n, french_word2index, french_index2word, french_word_n, my_pairs
  • 调用
# 全局函数 获取英语单词字典 法语单词字典 语言对列表my_pairs
english_word2index, english_index2word, english_word_n, \
    french_word2index, french_index2word, french_word_n, \
    my_pairs = my_getdata()
  • 输出效果:
my_lines---> 10599
len(pairs)---> 10599
[['i m .', 'j ai ans .'], ['i m ok .', 'je vais bien .'], ['i m ok .', 'ca va .'], ['i m fat .', 'je suis gras .']]
my_pairs[8000][0]---> they re in the science lab .
my_pairs[8000][1]---> elles sont dans le laboratoire de sciences .
len(english_word2index)--> 2803
len(french_word2index)--> 4345
english_word_n---> 2803 french_word_n--> 4345
x.shape torch.Size([1, 9]) tensor([[ 75,  40, 102, 103, 677,  42,  21,   4,   1]])
y.shape torch.Size([1, 7]) tensor([[ 119,   25,  164,  165, 3222,    5,    1]])
x.shape torch.Size([1, 5]) tensor([[14, 15, 44,  4,  1]])
y.shape torch.Size([1, 5]) tensor([[24, 25, 62,  5,  1]])
x.shape torch.Size([1, 8]) tensor([[   2,    3,  147,   61,  532, 1143,    4,    1]])
y.shape torch.Size([1, 7]) tensor([[  6, 297,   7, 246, 102,   5,   1]])

2 构建数据源对象

# 原始数据 -> 数据源MyPairsDataset --> 数据迭代器DataLoader
# 构造数据源 MyPairsDataset,把语料xy 文本数值化 再转成tensor_x tensor_y
# 1 __init__(self, my_pairs)函数 设置self.my_pairs 条目数self.sample_len
# 2 __len__(self)函数  获取样本条数
# 3 __getitem__(self, index)函数 获取第几条样本数据
#       按索引 获取数据样本 x y
#       样本x 文本数值化   word2id  x.append(EOS_token)
#       样本y 文本数值化   word2id  y.append(EOS_token)
#       返回tensor_x, tensor_y

class MyPairsDataset(Dataset):
    def __init__(self, my_pairs):
        # 样本x
        self.my_pairs = my_pairs

        # 样本条目数
        self.sample_len = len(my_pairs)

    # 获取样本条数
    def __len__(self):
        return self.sample_len

    # 获取第几条 样本数据
    def __getitem__(self, index):

        # 对index异常值进行修正 [0, self.sample_len-1]
        index = min(max(index, 0), self.sample_len-1)

        # 按索引获取 数据样本 x y
        x = self.my_pairs[index][0]
        y = self.my_pairs[index][1]

        # 样本x 文本数值化
        x = [english_word2index[word] for word in x.split(' ')]
        x.append(EOS_token)
        tensor_x = torch.tensor(x, dtype=torch.long, device=device)

        # 样本y 文本数值化
        y = [french_word2index[word] for word in y.split(' ')]
        y.append(EOS_token)
        tensor_y = torch.tensor(y, dtype=torch.long, device=device)
        # 注意 tensor_x tensor_y都是一维数组,通过DataLoader拿出数据是二维数据
        # print('tensor_y.shape===>', tensor_y.shape, tensor_y)

        # 返回结果
        return tensor_x, tensor_y

3 构建数据迭代器

def dm_test_MyPairsDataset():

    # 1 实例化dataset对象
    mypairsdataset = MyPairsDataset(my_pairs)

    # 2 实例化dataloader
    mydataloader = DataLoader(dataset=mypairsdataset, batch_size=1, shuffle=True)
    for  i, (x, y) in enumerate (mydataloader):
        print('x.shape', x.shape, x)
        print('y.shape', y.shape, y)
        if i == 1:
            break
  • 输出效果:
x.shape torch.Size([1, 8]) tensor([[   2,   16,   33,  518,  589, 1460,    4,    1]])
y.shape torch.Size([1, 8]) tensor([[   6,   11,   52,  101, 1358,  964,    5,    1]])
x.shape torch.Size([1, 6]) tensor([[129,  78, 677, 429,   4,   1]])
y.shape torch.Size([1, 7]) tensor([[ 118,  214, 1073,  194,  778,    5,    1]])

3 构建基于GRU的编码器和解码器

1 构建基于GRU的编码器

  • 编码器结构图:

avatar

  • 实现思路分析
# EncoderRNN类 实现思路分析:
# 1 init函数 定义2个层 self.embedding self.gru (batch_first=True)
#    def __init__(self, input_size, hidden_size): # 2803 256

# 2 forward(input, hidden)函数,返回output, hidden
#   数据经过词嵌入层 数据形状 [1,6] --> [1,6,256]
#   数据经过gru层 形状变化 gru([1,6,256],[1,1,256]) --> [1,6,256] [1,1,256]

# 3 初始化隐藏层输入数据 inithidden()
#   形状 torch.zeros(1, 1, self.hidden_size, device=device)
  • 构建基于GRU的编码器
class EncoderRNN(nn.Module):
    def __init__(self, input_size, hidden_size):

        # input_size 编码器 词嵌入层单词数 eg:2803
        # hidden_size 编码器 词嵌入层每个单词的特征数 eg 256
        super(EncoderRNN, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size

        # 实例化nn.Embedding层
        self.embedding = nn.Embedding(input_size, hidden_size)

        # 实例化nn.GRU层 注意参数batch_first=True
        self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)

    def forward(self, input, hidden):

        # 数据经过词嵌入层 数据形状 [1,6] --> [1,6,256]
        output = self.embedding(input)

        # 数据经过gru层 数据形状 gru([1,6,256],[1,1,256]) --> [1,6,256] [1,1,256]
        output, hidden = self.gru(output, hidden)
        return output, hidden

    def inithidden(self):
        # 将隐层张量初始化成为1x1xself.hidden_size大小的张量
        return torch.zeros(1, 1, self.hidden_size, device=device)
  • 调用
def dm_test_EncoderRNN():

    # 实例化dataset对象
    mypairsdataset = MyPairsDataset(my_pairs)

    # 实例化dataloader
    mydataloader = DataLoader(dataset=mypairsdataset, batch_size=1, shuffle=True)

    # 实例化模型
    input_size = english_word_n
    hidden_size = 256 #
    my_encoderrnn = EncoderRNN(input_size, hidden_size)
    print('my_encoderrnn模型结构--->', my_encoderrnn)

    # 给encode模型喂数据
    for  i, (x, y) in enumerate (mydataloader):

        print('x.shape', x.shape, x)
        print('y.shape', y.shape, y)

        # 一次性的送数据
        hidden = my_encoderrnn.inithidden()
        encode_output_c, hidden = my_encoderrnn(x, hidden)
        print('encode_output_c.shape--->', encode_output_c.shape, encode_output_c)

        # 一个字符一个字符给为模型喂数据
        hidden = my_encoderrnn.inithidden()
        for i in range(x.shape[1]):
            tmp = x[0][i].view(1,-1)
            output, hidden = my_encoderrnn(tmp, hidden)

        print('观察:最后一个时间步output输出是否相等') # hidden_size = 8 效果比较好
        print('encode_output_c[0][-1]===>', encode_output_c[0][-1])
        print('output===>', output)

        break
  • 输出效果:
# 本输出效果为hidden_size = 8
x.shape torch.Size([1, 6]) tensor([[129, 124, 270, 558,   4,   1]])
y.shape torch.Size([1, 7]) tensor([[ 118,  214,  101, 1253, 1028,    5,    1]])
encode_output_c.shape---> torch.Size([1, 6, 8]) 
tensor([[[-0.0984,  0.4267, -0.2120,  0.0923,  0.1525, -0.0378,  0.2493,-0.2665],
         [-0.1388,  0.5363, -0.4522, -0.2819, -0.2070,  0.0795,  0.6262, -0.2359],
         [-0.4593,  0.2499,  0.1159,  0.3519, -0.0852, -0.3621,  0.1980, -0.1853],
         [-0.4407,  0.1974,  0.6873, -0.0483, -0.2730, -0.2190,  0.0587, 0.2320],
         [-0.6544,  0.1990,  0.7534, -0.2347, -0.0686, -0.5532,  0.0624, 0.4083],
         [-0.2941, -0.0427,  0.1017, -0.1057,  0.1983, -0.1066,  0.0881, -0.3936]]], grad_fn=<TransposeBackward1>)
观察最后一个时间步output输出是否相等
encode_output_c[0][-1]===> tensor([-0.2941, -0.0427,  0.1017, -0.1057,  0.1983, -0.1066,  0.0881, -0.3936],
       grad_fn=<SelectBackward0>)
output===> tensor([[[-0.2941, -0.0427,  0.1017, -0.1057,  0.1983, -0.1066,  0.0881,
          -0.3936]]], grad_fn=<TransposeBackward1>)

2 构建基于GRU的解码器

  • 解码器结构图:

avatar

  • 构建基于GRU的解码器实现思路分析
# DecoderRNN 类 实现思路分析:
# 解码器的作用:提取事物特征 进行分类(所以比 编码器 多了 线性层 和 softmax层)
# 1 init函数 定义四个层 self.embedding self.gru self.out self.softmax=nn.LogSoftmax(dim=-1)
#    def __init__(self, output_size, hidden_size): # 4345 256

# 2 forward(input, hidden)函数,返回output, hidden
#   数据经过词嵌入层 数据形状 [1,1] --> [1,1,256]
#   数据经过relu()层 output = F.relu(output)
#   数据经过gru层 形状变化 gru([1,1,256],[1,1,256]) --> [1,1,256] [1,1,256]
#   数据结果out层 形状变化 [1,1,256]->[1,256]-->[1,4345]
#   返回 解码器分类output[1,4345],最后隐层张量hidden[1,1,256]

# 3 初始化隐藏层输入数据 inithidden()
#   形状 torch.zeros(1, 1, self.hidden_size, device=device)
  • 编码实现
class DecoderRNN(nn.Module):

    def __init__(self, output_size, hidden_size):

        # output_size 编码器 词嵌入层单词数 eg:4345
        # hidden_size 编码器 词嵌入层每个单词的特征数 eg 256
        super(DecoderRNN, self).__init__()
        self.output_size = output_size
        self.hidden_size = hidden_size

        # 实例化词嵌入层
        self.embedding = nn.Embedding(output_size, hidden_size)

        # 实例化gru层,输入尺寸256 输出尺寸256
        # 因解码器一个字符一个字符的解码 batch_first=True 意义不大
        self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)

        # 实例化线性输出层out 输入尺寸256 输出尺寸4345
        self.out = nn.Linear(hidden_size, output_size)

        # 实例化softomax层 数值归一化 以便分类
        self.softmax = nn.LogSoftmax(dim=-1)

    def forward(self, input, hidden):

        # 数据经过词嵌入层
        # 数据形状 [1,1] --> [1,1,256] or [1,6]--->[1,6,256]
        output = self.embedding(input)

        # 数据结果relu层使Embedding矩阵更稀疏,以防止过拟合
        output = F.relu(output)

        # 数据经过gru层
        # 数据形状 gru([1,1,256],[1,1,256]) --> [1,1,256] [1,1,256]
        output, hidden = self.gru(output, hidden)

        # 数据经过softmax层 归一化
        # 数据形状变化 [1,1,256]->[1,256] ---> [1,4345]
        output = self.softmax(self.out(output[0]))
        return output, hidden

    def inithidden(self):

        # 将隐层张量初始化成为1x1xself.hidden_size大小的张量
        return torch.zeros(1, 1, self.hidden_size, device=device)
  • 调用
def dm03_test_DecoderRNN():

    # 实例化dataset对象
    mypairsdataset = MyPairsDataset(my_pairs)

    # 实例化dataloader
    mydataloader = DataLoader(dataset=mypairsdataset, batch_size=1, shuffle=True)

    # 实例化模型
    input_size = english_word_n
    hidden_size = 256 # 观察结果数据 可使用8
    my_encoderrnn = EncoderRNN(input_size, hidden_size)
    print('my_encoderrnn模型结构--->', my_encoderrnn)

    # 实例化模型
    input_size = french_word_n
    hidden_size = 256  # 观察结果数据 可使用8
    my_decoderrnn = DecoderRNN(input_size, hidden_size)
    print('my_decoderrnn模型结构--->', my_decoderrnn)

    # 给模型喂数据 完整演示编码 解码流程
    for i, (x, y) in enumerate (mydataloader):

        print('x.shape', x.shape, x)
        print('y.shape', y.shape, y)

        # 1 编码:一次性的送数据
        hidden = my_encoderrnn.inithidden()
        encode_output_c, hidden = my_encoderrnn(x, hidden)
        print('encode_output_c.shape--->', encode_output_c.shape, encode_output_c)


        print('观察:最后一个时间步output输出') # hidden_size = 8 效果比较好
        print('encode_output_c[0][-1]===>', encode_output_c[0][-1])

        # 2 解码: 一个字符一个字符的解码
        # 最后1个隐藏层的输出 作为 解码器的第1个时间步隐藏层输入
        for i in range(y.shape[1]):
            tmp = y[0][i].view(1, -1)
            output, hidden = my_decoderrnn(tmp, hidden)
            print('每个时间步解码出来4345种可能 output===>', output.shape)

        break
  • 输出效果
my_encoderrnn模型结构---> EncoderRNN(
  (embedding): Embedding(2803, 256)
  (gru): GRU(256, 256, batch_first=True)
)
my_decoderrnn模型结构---> DecoderRNN(
  (embedding): Embedding(4345, 256)
  (gru): GRU(256, 256, batch_first=True)
  (out): Linear(in_features=256, out_features=4345, bias=True)
  (softmax): LogSoftmax(dim=-1)
)
x.shape torch.Size([1, 8]) tensor([[ 14,  40, 883, 677, 589, 609,   4,   1]])
y.shape torch.Size([1, 6]) tensor([[1358, 1125,  247, 2863,    5,    1]])
每个时间步解码出来4345种可能 output===> torch.Size([1, 4345])
每个时间步解码出来4345种可能 output===> torch.Size([1, 4345])
每个时间步解码出来4345种可能 output===> torch.Size([1, 4345])
每个时间步解码出来4345种可能 output===> torch.Size([1, 4345])
每个时间步解码出来4345种可能 output===> torch.Size([1, 4345])
每个时间步解码出来4345种可能 output===> torch.Size([1, 4345])

3 构建基于GRU和Attention的解码器

  • 解码器结构图:

avatar

  • 实现思路分析
# 构建基于GRU和Attention的解码器
# AttnDecoderRNN 类 实现思路分析:
# 1 init函数 定义六个层
#   self.embedding self.attn  self.attn_combine
#   self.gru self.out self.softmax=nn.LogSoftmax(dim=-1)
#   def __init__(self, output_size, hidden_size, dropout_p=0.1, max_length=MAX_LENGTH):: # 4345 256

# 2 forward(input, hidden, encoder_outputs)函数,返回output, hidden
#   数据经过词嵌入层 数据形状 [1,1] --> [1,1,256]
#   1 求查询张量q的注意力权重分布, attn_weights[1,10]
#   2 求查询张量q的注意力结果表示 bmm运算, attn_applied[1,1,256]
#   3 q 与 attn_applied 融合,经过层attn_combine 按照指定维度输出 output[1,1,256]
#   数据经过relu()层 output = F.relu(output)
#   数据经过gru层 形状变化 gru([1,1,256],[1,1,256]) --> [1,1,256] [1,1,256]
#   返回 # 返回解码器分类output[1,4345],最后隐层张量hidden[1,1,256] 注意力权重张量attn_weights[1,10]

# 3 初始化隐藏层输入数据 inithidden()
#   形状 torch.zeros(1, 1, self.hidden_size, device=device)

# 相对传统RNN解码 AttnDecoderRNN类多了注意力机制,需要构建QKV
# 1 在init函数中 (self, output_size, hidden_size, dropout_p=0.1, max_length=MAX_LENGTH)
    # 增加层 self.attn  self.attn_combine  self.dropout
# 2 增加函数 attentionQKV(self, Q, K, V)
# 3 函数forward(self, input, hidden, encoder_outputs)
    # encoder_outputs 每个时间步解码准备qkv 调用attentionQKV
    # 函数返回值 output, hidden, attn_weights
# 4 调用需要准备中间语义张量C encode_output_c
  • 编码实现
class AttnDecoderRNN(nn.Module):
    def __init__(self, output_size, hidden_size, dropout_p=0.1, max_length=MAX_LENGTH):

        # output_size   编码器 词嵌入层单词数 eg:4345
        # hidden_size   编码器 词嵌入层每个单词的特征数 eg 256
        # dropout_p     置零比率,默认0.1,
        # max_length    最大长度10
        super(AttnDecoderRNN, self).__init__()
        self.output_size = output_size
        self.hidden_size = hidden_size
        self.dropout_p = dropout_p
        self.max_length = max_length

        # 定义nn.Embedding层 nn.Embedding(4345,256)
        self.embedding = nn.Embedding(self.output_size, self.hidden_size)

        # 定义线性层1:求q的注意力权重分布
        self.attn = nn.Linear(self.hidden_size * 2, self.max_length)

        # 定义线性层2:q+注意力结果表示融合后,在按照指定维度输出
        self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)

        # 定义dropout层
        self.dropout = nn.Dropout(self.dropout_p)

        # 定义gru层
        self.gru = nn.GRU(self.hidden_size, self.hidden_size, batch_first=True)

        # 定义out层 解码器按照类别进行输出(256,4345)
        self.out = nn.Linear(self.hidden_size, self.output_size)

        # 实例化softomax层 数值归一化 以便分类
        self.softmax = nn.LogSoftmax(dim=-1)

    def forward(self, input, hidden, encoder_outputs):
        # input代表q [1,1] 二维数据 hidden代表k [1,1,256] encoder_outputs代表v [10,256]

        # 数据经过词嵌入层
        # 数据形状 [1,1] --> [1,1,256]
        embedded = self.embedding(input)

        # 使用dropout进行随机丢弃,防止过拟合
        embedded = self.dropout(embedded)

        # 1 求查询张量q的注意力权重分布, attn_weights[1,10]
        attn_weights = F.softmax(
            self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)

        # 2 求查询张量q的注意力结果表示 bmm运算, attn_applied[1,1,256]
        # [1,1,10],[1,10,256] ---> [1,1,256]
        attn_applied = torch.bmm(attn_weights.unsqueeze(0), encoder_outputs.unsqueeze(0))

        # 3 q 与 attn_applied 融合,再按照指定维度输出 output[1,1,256]
        output = torch.cat((embedded[0], attn_applied[0]), 1)
        output = self.attn_combine(output).unsqueeze(0)

        # 查询张量q的注意力结果表示 使用relu激活
        output = F.relu(output)

        # 查询张量经过gru、softmax进行分类结果输出
        # 数据形状[1,1,256],[1,1,256] --> [1,1,256], [1,1,256]
        output, hidden = self.gru(output, hidden)
        # 数据形状[1,1,256]->[1,256]->[1,4345]
        output = self.softmax(self.out(output[0]))

        # 返回解码器分类output[1,4345],最后隐层张量hidden[1,1,256] 注意力权重张量attn_weights[1,10]
        return output, hidden, attn_weights

    def inithidden(self):
        # 将隐层张量初始化成为1x1xself.hidden_size大小的张量
        return torch.zeros(1, 1, self.hidden_size, device=device)
  • 调用
def dm_test_AttnDecoderRNN():
    # 1 实例化 数据集对象
    mypairsdataset = MyPairsDataset(my_pairs)

    # 2 实例化 数据加载器对象
    mydataloader = DataLoader(dataset=mypairsdataset, batch_size=1, shuffle=True)

    #  实例化 编码器my_encoderrnn
    my_encoderrnn = EncoderRNN(english_word_n, 256)

    # 实例化 解码器DecoderRNN
    my_attndecoderrnn = AttnDecoderRNN(french_word_n, 256)

    # 3 遍历数据迭代器
    for i, (x, y) in enumerate(mydataloader):

        # 编码-方法1 一次性给模型送数据
        hidden = my_encoderrnn.inithidden()
        print('x--->', x.shape, x)
        print('y--->', y.shape, y)

        # [1, 6, 256], [1, 1, 256]) --> [1, 6, 256][1, 1, 256]
        output, hidden = my_encoderrnn(x, hidden)
        # print('output-->', output.shape, output)
        # print('最后一个时间步取出output[0,-1]-->', output[0, -1].shape, output[0, -1])

        # 中间语义张量C
        encode_output_c = torch.zeros(MAX_LENGTH, my_encoderrnn.hidden_size,device=device)
        for idx in range(output.shape[1]):
            encode_output_c[idx] = output[0, idx]

        # # 编码-方法2 一个字符一个字符给模型送数据
        # hidden = my_encoderrnn.inithidden()
        # for i in range(x.shape[1]):
        #     tmp = x[0][i].view(1, -1)
        #     # [1, 1, 256], [1, 1, 256]) --> [1, 1, 256][1, 1, 256]
        #     output, hidden = my_encoderrnn(tmp, hidden)
        # print('一个字符一个字符output', output.shape, output)

        # 解码-必须一个字符一个字符的解码 
        for i in range(y.shape[1]):
            tmp = y[0][i].view(1, -1)
            output, hidden, attn_weights = my_attndecoderrnn(tmp, hidden, encode_output_c)
            print('解码output.shape', output.shape )
            print('解码hidden.shape', hidden.shape)
            print('解码attn_weights.shape', attn_weights.shape)

        break
  • 输出效果:
x---> torch.Size([1, 7]) tensor([[ 129,   78, 1873,  294, 1215,    4,    1]])
y---> torch.Size([1, 6]) tensor([[ 210, 3097,  248, 3095,    5,    1]])
解码output.shape torch.Size([1, 4345])
解码hidden.shape torch.Size([1, 1, 256])
解码attn_weights.shape torch.Size([1, 10])
解码output.shape torch.Size([1, 4345])
解码hidden.shape torch.Size([1, 1, 256])
解码attn_weights.shape torch.Size([1, 10])
解码output.shape torch.Size([1, 4345])
解码hidden.shape torch.Size([1, 1, 256])
解码attn_weights.shape torch.Size([1, 10])
解码output.shape torch.Size([1, 4345])
解码hidden.shape torch.Size([1, 1, 256])
解码attn_weights.shape torch.Size([1, 10])
解码output.shape torch.Size([1, 4345])
解码hidden.shape torch.Size([1, 1, 256])
解码attn_weights.shape torch.Size([1, 10])
解码output.shape torch.Size([1, 4345])
解码hidden.shape torch.Size([1, 1, 256])
解码attn_weights.shape torch.Size([1, 10])

4 构建模型训练函数, 并进行训练

1 teacher_forcing介绍

它是一种用于序列生成任务的训练技巧, 在seq2seq架构中, 根据循环神经网络理论,解码器每次应该使用上一步的结果作为输入的一部分, 但是训练过程中,一旦上一步的结果是错误的,就会导致这种错误被累积,无法达到训练效果, 因此,我们需要一种机制改变上一步出错的情况,因为训练时我们是已知正确的输出应该是什么,因此可以强制将上一步结果设置成正确的输出, 这种方式就叫做teacher_forcing.

2 teacher_forcing的作用

  • 能够在训练的时候矫正模型的预测,避免在序列生成的过程中误差进一步放大.

  • teacher_forcing能够极大的加快模型的收敛速度,令模型训练过程更快更平稳.

3 构建内部迭代训练函数

  • 模型训练参数
# 模型训练参数
mylr = 1e-4
epochs = 2
# 设置teacher_forcing比率为0.5
teacher_forcing_ratio = 0.5
print_interval_num = 1000
plot_interval_num = 100
  • 实现思路分析
# 内部迭代训练函数Train_Iters
# 1 编码 encode_output, encode_hidden = my_encoderrnn(x, encode_hidden)
# 数据形状 eg [1,6],[1,1,256] --> [1,6,256],[1,1,256]

# 2 解码参数准备和解码
# 解码参数1 固定长度C encoder_outputs_c = torch.zeros(MAX_LENGTH, my_encoderrnn.hidden_size, device=device)
# 解码参数2 decode_hidden # 解码参数3 input_y = torch.tensor([[SOS_token]], device=device)
# 数据形状数据形状 [1,1],[1,1,256],[10,256] ---> [1,4345],[1,1,256],[1,10]
# output_y, decode_hidden, attn_weight = my_attndecoderrnn(input_y, decode_hidden, encode_output_c)
# 计算损失 target_y = y[0][idx].view(1)
# 每个时间步处理 for idx in range(y_len): 处理三者之间关系input_y output_y target_y

# 3 训练策略 use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
# teacher_forcing  把样本真实值y作为下一次输入 input_y = y[0][idx].view(1, -1)
# not teacher_forcing 把预测值y作为下一次输入
# topv,topi = output_y.topk(1) # if topi.squeeze().item() == EOS_token: break input_y = topi.detach()

# 4 其他 # 计算损失  # 梯度清零 # 反向传播  # 梯度更新 # 返回 损失列表myloss.item()/y_len
  • 编码实现
def Train_Iters(x, y, my_encoderrnn, my_attndecoderrnn, myadam_encode, myadam_decode, mycrossentropyloss):

    # 1 编码 encode_output, encode_hidden = my_encoderrnn(x, encode_hidden)
    encode_hidden = my_encoderrnn.inithidden()
    encode_output, encode_hidden = my_encoderrnn(x, encode_hidden) # 一次性送数据
    # [1,6],[1,1,256] --> [1,6,256],[1,1,256]

    # 2 解码参数准备和解码
    # 解码参数1 encode_output_c [10,256]
    encode_output_c = torch.zeros(MAX_LENGTH, my_encoderrnn.hidden_size, device=device)
    for idx in range(x.shape[1]):
        encode_output_c[idx] = encode_output[0, idx]

    # 解码参数2
    decode_hidden = encode_hidden

    # 解码参数3
    input_y = torch.tensor([[SOS_token]], device=device)

    myloss = 0.0
    y_len = y.shape[1]

    use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False
    if use_teacher_forcing:
        for idx in range(y_len):
            # 数据形状数据形状 [1,1],[1,1,256],[10,256] ---> [1,4345],[1,1,256],[1,10]
            output_y, decode_hidden, attn_weight = my_attndecoderrnn(input_y, decode_hidden, encode_output_c)
            target_y = y[0][idx].view(1)
            myloss = myloss + mycrossentropyloss(output_y, target_y)
            input_y = y[0][idx].view(1, -1)
    else:
        for idx in range(y_len):
            # 数据形状数据形状 [1,1],[1,1,256],[10,256] ---> [1,4345],[1,1,256],[1,10]
            output_y, decode_hidden, attn_weight = my_attndecoderrnn(input_y, decode_hidden, encode_output_c)
            target_y = y[0][idx].view(1)
            myloss = myloss + mycrossentropyloss(output_y, target_y)

            topv, topi = output_y.topk(1)
            if topi.squeeze().item() == EOS_token:
                break
            input_y = topi.detach()

    # 梯度清零
    myadam_encode.zero_grad()
    myadam_decode.zero_grad()

    # 反向传播
    myloss.backward()

    # 梯度更新
    myadam_encode.step()
    myadam_decode.step()

    # 返回 损失列表myloss.item()/y_len
    return myloss.item() / y_len

4 构建模型训练函数

  • 实现思路分析
# Train_seq2seq() 思路分析
# 实例化 mypairsdataset对象  实例化 mydataloader
# 实例化编码器 my_encoderrnn 实例化解码器 my_attndecoderrnn
# 实例化编码器优化器 myadam_encode 实例化解码器优化器 myadam_decode
# 实例化损失函数 mycrossentropyloss = nn.NLLLoss()
# 定义模型训练的参数
# epoches mylr=1e4 teacher_forcing_ratio print_interval_num  plot_interval_num (全局)
# plot_loss_list = [] (返回) print_loss_total plot_loss_total starttime (每轮内部)

# 外层for循环 控制轮数 for epoch_idx in range(1, 1+epochs):
# 内层for循环 控制迭代次数 # for item, (x, y) in enumerate(mydataloader, start=1):
#   调用内部训练函数 Train_Iters(x, y, my_encoderrnn, my_attndecoderrnn, myadam_encode, myadam_decode, mycrossentropyloss)
# 计算辅助信息
#   计算打印屏幕间隔损失-每隔1000次 # 计算画图间隔损失-每隔100次
#   每个轮次保存模型 torch.save(my_encoderrnn.state_dict(), PATH1)
#   所有轮次训练完毕 画损失图 plt.figure() .plot(plot_loss_list) .save('x.png') .show()
  • 编码实现
def Train_seq2seq():

    # 实例化 mypairsdataset对象  实例化 mydataloader
    mypairsdataset = MyPairsDataset(my_pairs)
    mydataloader = DataLoader(dataset=mypairsdataset, batch_size=1, shuffle=True)

    # 实例化编码器 my_encoderrnn 实例化解码器 my_attndecoderrnn
    my_encoderrnn = EncoderRNN(2803, 256)
    my_attndecoderrnn = AttnDecoderRNN(output_size=4345, hidden_size=256, dropout_p=0.1, max_length=10)

    # 实例化编码器优化器 myadam_encode 实例化解码器优化器 myadam_decode
    myadam_encode = optim.Adam(my_encoderrnn.parameters(), lr=mylr)
    myadam_decode = optim.Adam(my_attndecoderrnn.parameters(), lr=mylr)

    # 实例化损失函数 mycrossentropyloss = nn.NLLLoss()
    mycrossentropyloss = nn.NLLLoss()

    # 定义模型训练的参数
    plot_loss_list = []

    # 外层for循环 控制轮数 for epoch_idx in range(1, 1+epochs):
    for epoch_idx in range(1, 1+epochs):

        print_loss_total, plot_loss_total = 0.0, 0.0
        starttime = time.time()

        # 内层for循环 控制迭代次数
        for item, (x, y) in enumerate(mydataloader, start=1):
            # 调用内部训练函数
            myloss = Train_Iters(x, y, my_encoderrnn, my_attndecoderrnn, myadam_encode, myadam_decode, mycrossentropyloss)
            print_loss_total += myloss
            plot_loss_total += myloss

            # 计算打印屏幕间隔损失-每隔1000次
            if item % print_interval_num ==0 :
                print_loss_avg = print_loss_total / print_interval_num
                # 将总损失归0
                print_loss_total = 0
                # 打印日志,日志内容分别是:训练耗时,当前迭代步,当前进度百分比,当前平均损失
                print('轮次%d  损失%.6f 时间:%d' % (epoch_idx, print_loss_avg, time.time() - starttime))

            # 计算画图间隔损失-每隔100次
            if item % plot_interval_num == 0:
                # 通过总损失除以间隔得到平均损失
                plot_loss_avg = plot_loss_total / plot_interval_num
                # 将平均损失添加plot_loss_list列表中
                plot_loss_list.append(plot_loss_avg)
                # 总损失归0
                plot_loss_total = 0

        # 每个轮次保存模型
        torch.save(my_encoderrnn.state_dict(), './my_encoderrnn_%d.pth' % epoch_idx)
        torch.save(my_attndecoderrnn.state_dict(), './my_attndecoderrnn_%d.pth' % epoch_idx)

    # 所有轮次训练完毕 画损失图
    plt.figure()
    plt.plot(plot_loss_list)
    plt.savefig('./s2sq_loss.png')
    plt.show()

    return plot_loss_list
  • 输出效果
轮次1  损失8.123402 时间:4
轮次1  损失6.658305 时间:8
轮次1  损失5.252497 时间:12
轮次1  损失4.906939 时间:16
轮次1  损失4.813769 时间:19
轮次1  损失4.780460 时间:23
轮次1  损失4.621599 时间:27
轮次1  损失4.487508 时间:31
轮次1  损失4.478538 时间:35
轮次1  损失4.245148 时间:39
轮次1  损失4.602579 时间:44
轮次1  损失4.256789 时间:48
轮次1  损失4.218111 时间:52
轮次1  损失4.393134 时间:56
轮次1  损失4.134959 时间:60
轮次1  损失4.164878 时间:63

5 损失曲线分析

损失下降曲线

avatar

一直下降的损失曲线, 说明模型正在收敛, 能够从数据中找到一些规律应用于数据.

5 构建模型评估函数并测试

1 构建模型评估函数

# 模型评估代码与模型预测代码类似,需要注意使用with torch.no_grad()
# 模型预测时,第一个时间步使用SOS_token作为输入 后续时间步采用预测值作为输入,也就是自回归机制
def Seq2Seq_Evaluate(x, my_encoderrnn, my_attndecoderrnn):
    with torch.no_grad():
        # 1 编码:一次性的送数据
        encode_hidden = my_encoderrnn.inithidden()
        encode_output, encode_hidden = my_encoderrnn(x, encode_hidden)

        # 2 解码参数准备
        # 解码参数1 固定长度中间语义张量c
        encoder_outputs_c = torch.zeros(MAX_LENGTH, my_encoderrnn.hidden_size, device=device)
        x_len = x.shape[1]
        for idx in range(x_len):
            encoder_outputs_c[idx] = encode_output[0, idx]

        # 解码参数2 最后1个隐藏层的输出 作为 解码器的第1个时间步隐藏层输入
        decode_hidden = encode_hidden

        # 解码参数3 解码器第一个时间步起始符
        input_y = torch.tensor([[SOS_token]], device=device)

        # 3 自回归方式解码
        # 初始化预测的词汇列表
        decoded_words = []
        # 初始化attention张量
        decoder_attentions = torch.zeros(MAX_LENGTH, MAX_LENGTH)
        for idx in range(MAX_LENGTH): # note:MAX_LENGTH=10
            output_y, decode_hidden, attn_weights = my_attndecoderrnn(input_y, decode_hidden, encoder_outputs_c)
            # 预测值作为为下一次时间步的输入值
            topv, topi = output_y.topk(1)
            decoder_attentions[idx] = attn_weights

            # 如果输出值是终止符,则循环停止
            if topi.squeeze().item() == EOS_token:
                decoded_words.append('<EOS>')
                break
            else:
                decoded_words.append(french_index2word[topi.item()])

            # 将本次预测的索引赋值给 input_y,进行下一个时间步预测
            input_y = topi.detach()

        # 返回结果decoded_words, 注意力张量权重分布表(把没有用到的部分切掉)
        return decoded_words, decoder_attentions[:idx + 1]

2 模型评估函数调用

# 加载模型
PATH1 = './gpumodel/my_encoderrnn.pth'
PATH2 = './gpumodel/my_attndecoderrnn.pth'
def dm_test_Seq2Seq_Evaluate():
    # 实例化dataset对象
    mypairsdataset = MyPairsDataset(my_pairs)
    # 实例化dataloader
    mydataloader = DataLoader(dataset=mypairsdataset, batch_size=1, shuffle=True)

    # 实例化模型
    input_size = english_word_n
    hidden_size = 256  # 观察结果数据 可使用8
    my_encoderrnn = EncoderRNN(input_size, hidden_size)
    # my_encoderrnn.load_state_dict(torch.load(PATH1))
    my_encoderrnn.load_state_dict(torch.load(PATH1, map_location=lambda storage, loc: storage), False)
    print('my_encoderrnn模型结构--->', my_encoderrnn)

    # 实例化模型
    input_size = french_word_n
    hidden_size = 256  # 观察结果数据 可使用8
    my_attndecoderrnn = AttnDecoderRNN(input_size, hidden_size)
    # my_attndecoderrnn.load_state_dict(torch.load(PATH2))
    my_attndecoderrnn.load_state_dict(torch.load(PATH2, map_location=lambda storage, loc: storage), False)
    print('my_decoderrnn模型结构--->', my_attndecoderrnn)

    my_samplepairs = 
    [
      ['i m impressed with your french .', 'je suis impressionne par votre francais .'],
      ['i m more than a friend .', 'je suis plus qu une amie .'],
      ['she is beautiful like her mother .', 'elle est belle comme sa mere .']
    ]
    print('my_samplepairs--->', len(my_samplepairs))

    for index, pair in enumerate(my_samplepairs):
        x = pair[0]
        y = pair[1]

        # 样本x 文本数值化
        tmpx = [english_word2index[word] for word in x.split(' ')]
        tmpx.append(EOS_token)
        tensor_x = torch.tensor(tmpx, dtype=torch.long, device=device).view(1, -1)

        # 模型预测
        decoded_words, attentions = Seq2Seq_Evaluate(tensor_x, my_encoderrnn, my_attndecoderrnn)
        # print('decoded_words->', decoded_words)
        output_sentence = ' '.join(decoded_words)

        print('\n')
        print('>', x)
        print('=', y)
        print('<', output_sentence)
  • 输出效果:
> i m impressed with your french .
= je suis impressionne par votre francais .
< je suis impressionnee par votre francais . <EOS>

> i m more than a friend .
= je suis plus qu une amie .
< je suis plus qu une amie . <EOS>

> she is beautiful like her mother .
= elle est belle comme sa mere .
< elle est sa sa mere . <EOS>

> you re winning aren t you ?
= vous gagnez n est ce pas ?
< tu restez n est ce pas ? <EOS>

> he is angry with you .
= il est en colere apres toi .
< il est en colere apres toi . <EOS>

> you re very timid .
= vous etes tres craintifs .
< tu es tres craintive . <EOS>

3 Attention张量制图

def dm_test_Attention():

    # 实例化dataset对象
    mypairsdataset = MyPairsDataset(my_pairs)
    # 实例化dataloader
    mydataloader = DataLoader(dataset=mypairsdataset, batch_size=1, shuffle=True)

    # 实例化模型
    input_size = english_word_n
    hidden_size = 256  # 观察结果数据 可使用8
    my_encoderrnn = EncoderRNN(input_size, hidden_size)
    # my_encoderrnn.load_state_dict(torch.load(PATH1))
    my_encoderrnn.load_state_dict(torch.load(PATH1, map_location=lambda storage, loc: storage), False)

    # 实例化模型
    input_size = french_word_n
    hidden_size = 256  # 观察结果数据 可使用8
    my_attndecoderrnn = AttnDecoderRNN(input_size, hidden_size)
    # my_attndecoderrnn.load_state_dict(torch.load(PATH2))
    my_attndecoderrnn.load_state_dict(torch.load(PATH2, map_location=lambda storage, loc: storage), False)

    sentence = "we re both teachers ."
    # 样本x 文本数值化
    tmpx = [english_word2index[word] for word in sentence.split(' ')]
    tmpx.append(EOS_token)
    tensor_x = torch.tensor(tmpx, dtype=torch.long, device=device).view(1, -1)

    # 模型预测
    decoded_words, attentions = Seq2Seq_Evaluate(tensor_x, my_encoderrnn, my_attndecoderrnn)
    print('decoded_words->', decoded_words)

    # print('\n')
    # print('英文', sentence)
    # print('法文', output_sentence)

    plt.matshow(attentions.numpy()) # 以矩阵列表的形式 显示
    # 保存图像
    plt.savefig("./s2s_attn.png")
    plt.show()

    print('attentions.numpy()--->\n', attentions.numpy())
    print('attentions.size--->', attentions.size())
  • 输出效果:
decoded_words-> ['nous', 'sommes', 'toutes', 'deux', 'enseignantes', '.', '<EOS>']
  • Attention可视化:

avatar

  • Attention图像的纵坐标代表输入的源语言各个词汇对应的索引, 0-6分别对应["we", "re", "both", "teachers", ".", ""], 纵坐标代表生成的目标语言各个词汇对应的索引, 0-7代表['nous', 'sommes', 'toutes', 'deux', 'enseignantes', '.', ''], 图中浅色小方块(颜色越浅说明影响越大)代表词汇之间的影响关系, 比如源语言的第1个词汇对生成目标语言的第1个词汇影响最大, 源语言的第4,5个词对生成目标语言的第5个词会影响最大, 通过这样的可视化图像, 我们可以知道Attention的效果好坏, 与我们人为去判定到底还有多大的差距. 进而衡量我们训练模型的可用性.

4 小结

  • seq2seq模型架构分析
    • seq2seq模型架构包括三部分,分别是encoder(编码器)、decoder(解码器)、中间语义张量c。其中编码器和解码器的内部实现都使用了GRU模型
  • 基于GRU的seq2seq模型架构实现翻译的过程
    • 第一步: 导入必备的工具包和工具函数
    • 第二步: 对持久化文件中数据进行处理, 以满足模型训练要求
    • 第三步: 构建基于GRU的编码器和解码器
    • 第四步: 构建模型训练函数, 并进行训练
    • 第五步: 构建模型评估函数, 并进行测试以及Attention效果分析
  • 第一步: 导入必备的工具包
    • python版本使用3.6.x, pytorch版本使用1.3.1
  • 第二步: 对持久化文件中数据进行处理, 以满足模型训练要求
    • 清洗文本和构建文本字典、构建数据源、构建数据迭代器。文本处理的本质就是根据任务构建标签x、标签y
  • 第三步: 构建基于GRU的编码器和解码器
    • 构建基于GRU的编码器
    • 构建基于GRU的解码器
    • 构建基于GRU和Attention的解码器
  • 第四步: 构建模型训练函数, 并进行训练
    • 什么是teacher_forcing: 它是一种用于序列生成任务的训练技巧, 在seq2seq架构中, 根据循环神经网络理论,解码器每次应该使用上一步的结果作为输入的一部分, 但是训练过程中,一旦上一步的结果是错误的,就会导致这种错误被累积,无法达到训练效果, 因此,我们需要一种机制改变上一步出错的情况,因为训练时我们是已知正确的输出应该是什么,因此可以强制将上一步结果设置成正确的输出, 这种方式就叫做teacher_forcing
    • teacher_forcing的作用: 能够在训练的时候矫正模型的预测,避免在序列生成的过程中误差进一步放大. 另外, teacher_forcing能够极大的加快模型的收敛速度,令模型训练过程更快更平稳
    • 构建训练函数train
    • 调用训练函数并打印日志和制图
    • 损失曲线分析: 一直下降的损失曲线, 说明模型正在收敛, 能够从数据中找到一些规律应用于数据
  • 第五步: 构建模型评估函数, 并进行测试以及Attention效果分析
    • 构建模型评估函数evaluate
    • 随机选择指定数量的数据进行评估
    • 进行了Attention可视化分析