RNN以及其改进版(附2个代码案列)

感谢阅读

RNN简介

RNN(Recurrent Neural Network), 中文称作循环神经网络, 它一般以序列数据为输入, 通过网络内部的结构设计有效捕捉序列之间的关系特征, 一般也是以序列形式进行输出

传统RNN

内部结构过程演示

在这里插入图片描述
两个黑点一起到达蓝色区域(并在之前形成整体)

内部计算公式

在这里插入图片描述

RNN輸出

在这里插入图片描述

激活函数tanh

在这里插入图片描述
于帮助调节流经网络的值, tanh函数将值压缩在-1和1之间

Pytorch构建传统RNN

def dm_run_for_hiddennum():
    '''
    第一个参数:input_size(输入张量x的维度)
    第二个参数:hidden_size(隐藏层的维度, 隐藏层的神经元个数)
    第三个参数:num_layer(隐藏层的数量)
    '''
    rnn = nn.RNN(5, 6, 2)  # A 隐藏层个数从1-->2 下面程序需要修改的地方?
    '''
    第一个参数:sequence_length(输入序列的长度)
    第二个参数:batch_size(批次的样本数量)
    第三个参数:input_size(输入张量的维度)
    '''
    input = torch.randn(1, 3, 5)  # B
    '''
    第一个参数:num_layer * num_directions(层数*网络方向)
    第二个参数:batch_size(批次的样本数)
    第三个参数:hidden_size(隐藏层的维度, 隐藏层神经元的个数)
    '''
    h0 = torch.randn(2, 3, 6)  # C

    output, hn = rnn(input, h0)	  #
    print('output-->', output.shape, output)
    print('hn-->', hn.shape, hn)
    print('rnn模型--->', rnn)  # nn模型---> RNN(5, 6, num_layers=11)

    # 结论:若只有一个隐藏次 output输出结果等于hn
    # 结论:如果有2个隐藏层,output的输出结果有2个,hn等于最后一个隐藏层

梯度计算

在这里插入图片描述

LSTM介绍

优点:LSTM的门结构能够有效减缓长序列问题中可能出现的梯度消失或爆炸, 虽然并不能杜绝这种现象, 但在更长的序列问题上表现优于传统RNN.
缺点:由于内部结构相对较复杂, 因此训练效率在同等算力下较传统RNN低很多

遗忘门结构分析:

与传统RNN的内部结构计算非常相似, 首先将当前时间步输入x(t)与上一个时间步隐含状态h(t-1)拼接, 得到[x(t), h(t-1)], 然后通过一个全连接层做变换, 最后通过sigmoid函数进行激活得到f(t), 我们可以将f(t)看作是门值, 好比一扇门开合的大小程度, 门值都将作用在通过该扇门的张量, 遗忘门门值将作用的上一层的细胞状态上, 代表遗忘过去的多少信息, 又因为遗忘门门值是由x(t), h(t-1)计算得来的, 因此整个公式意味着根据当前时间步输入和上一个时间步隐含状态h(t-1)来决定遗忘多少上一层的细胞状态所携带的过往信息.

输入门结构分析:

我们看到输入门的计算公式有两个, 第一个就是产生输入门门值的公式, 它和遗忘门公式几乎相同, 区别只是在于它们之后要作用的目标上. 这个公式意味着输入信息有多少需要进行过滤. 输入门的第二个公式是与传统RNN的内部结构计算相同. 对于LSTM来讲, 它得到的是当前的细胞状态, 而不是像经典RNN一样得到的是隐含状态.

细胞状态更新分析:

细胞更新的结构与计算公式非常容易理解, 这里没有全连接层, 只是将刚刚得到的遗忘门门值与上一个时间步得到的C(t-1)相乘, 再加上输入门门值与当前时间步得到的未更新C(t)相乘的结果. 最终得到更新后的C(t)作为下一个时间步输入的一部分. 整个细胞状态更新过程就是对遗忘门和输入门的应用.

输出门结构分析:

输出门部分的公式也是两个, 第一个即是计算输出门的门值, 它和遗忘门,输入门计算方式相同. 第二个即是使用这个门值产生隐含状态h(t), 他将作用在更新后的细胞状态C(t)上, 并做tanh激活, 最终得到h(t)作为下一时间步输入的一部分. 整个输出门的过程, 就是为了产生隐含状态h(t).

结构图

在这里插入图片描述

C表示某时刻的记忆细胞
h表示某时刻的状态
f遗忘门
i更新门
o输出门
ht流向下一个时刻以及直接输出

梯度公式

在这里插入图片描述

现实生活列子加强理解

我们以考试为例子:我们马上要期末考试了,第一门是高数,第二门线代。这个图的Xt就是考线代,h(t-1)就是考高数的状态,c(t-1)就是考高数的记忆,ht包含了考线代结束的状态以及分数,分数那一部分作为输出从上面输出,状态传给下一门考试(比如英语)。为什么遗忘?因为并不是所有东西都是线代需要关心的,这就是遗忘门的作用。为什么要保留上一门知识,比如高数用到的运算能力是我们需要保留的。
同样的,传统RNN就是要记住所有的东西,效率就会变低。

代码示例

# 定义LSTM的参数含义: (input_size, hidden_size, num_layers)
# 定义输入张量的参数含义: (sequence_length, batch_size, input_size)
# 定义隐藏层初始张量和细胞初始状态张量的参数含义:
# (num_layers * num_directions, batch_size, hidden_size)

>>> import torch.nn as nn
>>> import torch
>>> rnn = nn.LSTM(5, 6, 2)
>>> input = torch.randn(1, 3, 5)
>>> h0 = torch.randn(2, 3, 6)
>>> c0 = torch.randn(2, 3, 6)
>>> output, (hn, cn) = rnn(input, (h0, c0))
>>> output
tensor([[[ 0.0447, -0.0335,  0.1454,  0.0438,  0.0865,  0.0416],
         [ 0.0105,  0.1923,  0.5507, -0.1742,  0.1569, -0.0548],
         [-0.1186,  0.1835, -0.0022, -0.1388, -0.0877, -0.4007]]],
       grad_fn=<StackBackward>)
>>> hn
tensor([[[ 0.4647, -0.2364,  0.0645, -0.3996, -0.0500, -0.0152],
         [ 0.3852,  0.0704,  0.2103, -0.2524,  0.0243,  0.0477],
         [ 0.2571,  0.0608,  0.2322,  0.1815, -0.0513, -0.0291]],

        [[ 0.0447, -0.0335,  0.1454,  0.0438,  0.0865,  0.0416],
         [ 0.0105,  0.1923,  0.5507, -0.1742,  0.1569, -0.0548],
         [-0.1186,  0.1835, -0.0022, -0.1388, -0.0877, -0.4007]]],
       grad_fn=<StackBackward>)
>>> cn
tensor([[[ 0.8083, -0.5500,  0.1009, -0.5806, -0.0668, -0.1161],
         [ 0.7438,  0.0957,  0.5509, -0.7725,  0.0824,  0.0626],
         [ 0.3131,  0.0920,  0.8359,  0.9187, -0.4826, -0.0717]],

        [[ 0.1240, -0.0526,  0.3035,  0.1099,  0.5915,  0.0828],
         [ 0.0203,  0.8367,  0.9832, -0.4454,  0.3917, -0.1983],
         [-0.2976,  0.7764, -0.0074, -0.1965, -0.1343, -0.6683]]],
       grad_fn=<StackBackward>)

GRU介绍

GRU(Gated Recurrent Unit)也称门控循环单元结构, 它也是传统RNN的变体, 同LSTM一样能够有效捕捉长序列之间的语义关联, 缓解梯度消失或爆炸现象.

结构图

在这里插入图片描述

个人对GRU的理解

鄙人不才,如有理解错误之处,还望海涵斧正,先行谢过。
其实,个人感觉GRU是LSTM的改进版,把记忆、h二者功能进行了合并,其核心是上图最后一个式子,这就决定了记忆和遗忘的强度。
我还是以上学为例子。h(t-1)相当于我们在学校期间所学的所有东西,现在我们要搞毕设(以软件工程为例子,因为不才是软件工程专业出身,其他专业不是很了解)。Xt就是我们要搞毕设.h(t)就是我们学到的东西可以提供给做毕设的,比如python语言、软件体系结构、软件工程导论等。rt是什么呢?就是各学科可以提供给做毕设的东西的比列,比如python语言提供70%,软件体系提供10%。当然也有可能有0,比如选修课中的外国历史。h(上面带波浪线)的就相当于需要新学习的东西,比如我们做毕设的时候,想搞个反向代理服务器,学校没有教我们,我们是不是要自学?

LSTM难以比拟的两个地方

1.由于参数的减少,模型训练速度将会提升,同时可解释性也略微提升了一些。
2.由于运算的改良,降低了过拟合的风险。

RNN示例(人名分类问题)

案例介绍

以一个人名为输入, 使用模型帮助我们判断它最有可能是来自哪一个国家的人名, 这在某些国际化公司的业务中具有重要意义, 在用户注册过程中, 会根据用户填写的名字直接给他分配可能的国家或地区选项, 以及该国家或地区的国旗, 限制手机号码位数等等。

数据集下载与解释

在github上的name_decalre
点我下载
数据格式说明 每一行第一个单词为人名,第二个单词为国家名。中间用制表符tab分割

导包

# 导入torch工具
import torch
# 导入nn准备构建模型
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
# 导入torch的数据源 数据迭代器工具包
from  torch.utils.data import Dataset, DataLoader
# 用于获得常见字母及字符规范化
import string
# 导入时间工具包
import time
# 引入制图工具包  
import matplotlib.pyplot as plt
# 从io中导入文件打开方法
from io import open

查看常用字符数量

def data_process():
    # 获取所有常用字符包括字母和常用标点
    all_letters = string.ascii_letters + " .,;'"
    # 获取常用字符数量
    n_letters = len(all_letters)
    return n_letters


def main():
    print(data_process())
    return 0


if __name__ == '__main__':
    main()

构建国家名字,并获取国家数量

def get_country():
    # 国家名 种类数
    categorys = ['Italian', 'English', 'Arabic', 'Spanish', 'Scottish', 'Irish', 'Chinese', 'Vietnamese', 'Japanese',
                 'French', 'Greek', 'Dutch', 'Korean', 'Polish', 'Portuguese', 'Russian', 'Czech', 'German']
    # 国家名 个数
    categorynum = len(categorys)
    return categorys,categorynum


def main():
    # print(data_process())
    print(get_country())
    return 0

读数据到内存

def read_data(filename):
    """
    :param filename:
    :return:
    # 思路分析
    1 打开数据文件 open(filename, mode='r', encoding='utf-8')
    2 按行读文件、提取样本x 样本y line.strip().split('\t')
    3 返回样本x的列表、样本y的列表 my_list_x, my_list_y
    """
    my_list_x, my_list_y= [], []
    # 打开文件
    with  open(filename, mode='r', encoding='utf-8') as f:
        # 按照行读数据
        for line in f.readlines():
            if len(line) <= 5:
                continue
            # 按照行提取样本x 样本y
            (x, y) = line.strip().split('\t')
            my_list_x.append(x)
            my_list_y.append(y)
    # 返回样本x的列表、样本y的列表
    return my_list_x, my_list_y

构建数据源并进行迭代

def read_data(filename):
    """
    :param filename:
    :return:
    # 思路分析
    1 打开数据文件 open(filename, mode='r', encoding='utf-8')
    2 按行读文件、提取样本x 样本y line.strip().split('\t')
    3 返回样本x的列表、样本y的列表 my_list_x, my_list_y
    """
    my_list_x, my_list_y= [], []
    # 打开文件
    with  open(filename, mode='r', encoding='utf-8') as f:
        # 按照行读数据
        for line in f.readlines():
            if len(line) <= 5:
                continue
            # 按照行提取样本x 样本y
            (x, y) = line.strip().split('\t')
            my_list_x.append(x)
            my_list_y.append(y)
    # 返回样本x的列表、样本y的列表
    return my_list_x, my_list_y
class NameClassDataset(Dataset):

    def __init__(self, my_list_x, my_list_y):
        # 样本x
        self.my_list_x = my_list_x
        # 样本y
        self.my_list_y = my_list_y
        # 样本条目数
        self.sample_len = len(my_list_x)

    # 获取样本条数
    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_list_x[index]
        y = self.my_list_y[index]
        # print(x, y)
        # 样本x one-hot张量化
        tensor_x = torch.zeros(len(x), n_letters)
        # 遍历人名 的 每个字母 做成one-hot编码
        for li, letter in enumerate(x):
            # letter2indx 使用all_letters.find(letter)查找字母在all_letters表中的位置
            # 给one-hot赋值
            tensor_x[li][all_letters.find(letter)] = 1
        # 样本y 张量化
        tensor_y = torch.tensor(categorys.index(y), dtype=torch.long)
        # 返回结果
        return tensor_x, tensor_y
def dm_test_NameClassDataset():

    # 1 获取数据
    myfilename = '../data/name_classfication.txt'
    my_list_x, my_list_y = read_data(myfilename)
    # 2 实例化dataset对象
    nameclassdataset = NameClassDataset(my_list_x, my_list_y)
    # 3 实例化dataloader
    mydataloader = DataLoader(dataset=nameclassdataset, batch_size=1, shuffle=True)
    for  i, (x, y) in enumerate (mydataloader):
        print('x.shape', x.shape, x)
        print('y.shape', y.shape, y)
        break

对异常索引的处理的改良

python语言支持我们的索引,所以我们的索引也应该如此,但是程序也会复杂一些,这个代码想替换的可以把NameClassDataset的__getitem__替换为下面的代码:

    def __getitem__(self, index):

        # 对index异常值进行修正 [0, self.sample_len-1]
        if index < 0:
            index = max(-self.sample_len, index)
        else:
            index = min(self.sample_len-1, index)
        # 按索引获取 数据样本 x y
        x = self.my_list_x[index]
        y = self.my_list_y[index]
        # print(x, y)
        # 样本x one-hot张量化
        tensor_x = torch.zeros(len(x), n_letters)
        # 遍历人名 的 每个字母 做成one-hot编码
        for li, letter in enumerate(x):
            # letter2indx 使用all_letters.find(letter)查找字母在all_letters表中的位置
            # 给one-hot赋值
            tensor_x[li][all_letters.find(letter)] = 1
        # 样本y 张量化
        tensor_y = torch.tensor(categorys.index(y), dtype=torch.long)
        # 返回结果
        return tensor_x, tensor_y

构建三种RNN模型

构建传统RNN

class RNN(nn.Module):

    def __init__(self, input_size, hidden_size, output_size, num_layers=1):
        super(RNN, self).__init__()
        # 1 init函数 准备三个层 self.rnn self.linear self.softmax=nn.LogSoftmax(dim=-1)
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.num_layers = num_layers

        # 定义rnn层
        self.rnn = nn.RNN(self.input_size, self.hidden_size, self.num_layers)
        # 定义linear层(全连接线性层)
        self.linear = nn.Linear(self.hidden_size, self.output_size)
        # 定义softmax层
        self.softmax = nn.LogSoftmax(dim=-1)

    def forward(self, input, hidden):
        # 让数据经过三个层 返回softmax结果和hn
        # 数据形状 [6,57] -> [6,1,57]
        input = input.unsqueeze(1)
        # 把数据送给模型 提取事物特征
        # 数据形状 [seqlen,1,57],[1,1,128]) -> [seqlen,1,18],[1,1,128]
        rr, hn = self.rnn(input, hidden)
        # 数据形状 [seqlen,1,128] - [1, 128]
        tmprr = rr[-1]
        tmprr = self.linear(tmprr)
        return self.softmax(tmprr), hn

    def inithidden(self):
        # 初始化隐藏层输入数据 inithidden()
        return torch.zeros(self.num_layers, 1,self.hidden_size)

构建LSTM

class LSTM(nn.Module):

    def __init__(self, input_size, hidden_size, output_size, num_layers=1):
        super(LSTM, self).__init__()
        # 1 init函数 准备三个层 self.rnn self.linear self.softmax=nn.LogSoftmax(dim=-1)
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.num_layers = num_layers

        # 定义rnn层
        self.rnn = nn.LSTM(self.input_size, self.hidden_size, self.num_layers)

        # 定义linear层
        self.linear = nn.Linear(self.hidden_size, self.output_size)

        # 定义softmax层
        self.softmax = nn.LogSoftmax(dim=-1)

    def forward(self, input, hidden, c):
        # 让数据经过三个层 返回softmax结果和 hn c
        # 数据形状 [6,57] -> [6,1,52]
        input = input.unsqueeze(1)
        # 把数据送给模型 提取事物特征
        # 数据形状 [seqlen,1,57],[1,1,128], [1,1,128]) -> [seqlen,1,18],[1,1,128],[1,1,128]
        rr, (hn, c) = self.rnn(input, (hidden, c))
        # 数据形状 [seqlen,1,128] - [1, 128]
        tmprr = rr[-1]

        tmprr = self.linear(tmprr)

        return self.softmax(tmprr), hn, c

    def inithidden(self):
        # 初始化隐藏层输入数据 inithidden()
        hidden = c = torch.zeros(self.num_layers, 1, self.hidden_size)
        return hidden, c

构建GRU

class GRU(nn.Module):

    def __init__(self, input_size, hidden_size, output_size, num_layers=1):
        super(GRU, self).__init__()
        # 1 init函数 准备三个层 self.rnn self.linear self.softmax=nn.LogSoftmax(dim=-1)
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.num_layers = num_layers

        # 定义rnn层
        self.rnn = nn.GRU(self.input_size, self.hidden_size, self.num_layers)
        # 定义linear层
        self.linear = nn.Linear(self.hidden_size, self.output_size)
        # 定义softmax层
        self.softmax = nn.LogSoftmax(dim=-1)

    def forward(self, input, hidden):
        # 让数据经过三个层 返回softmax结果和hn
        # 数据形状 [6,57] -> [6,1,52]
        input = input.unsqueeze(1)
        # 把数据送给模型 提取事物特征
        # 数据形状 [seqlen,1,57],[1,1,128]) -> [seqlen,1,18],[1,1,128]
        rr, hn = self.rnn(input, hidden)
        # 数据形状 [seqlen,1,128] - [1, 128]
        tmprr = rr[-1]
        tmprr = self.linear(tmprr)
        # 多分类softmax 二分类sigmoid
        return self.softmax(tmprr), hn

    def inithidden(self):
        # 初始化隐藏层输入数据 inithidden()
        return torch.zeros(self.num_layers, 1, self.hidden_size)

对三个模型进行测试与训练

测试

def dm_test_rnn_lstm_gru():
    # one-hot编码特征57(n_letters),也是RNN的输入尺寸
    input_size = 57

    # 定义隐层的最后一维尺寸大小
    n_hidden = 128
    # 输出尺寸为语言类别总数n_categories # 1个字符预测成18个类别
    output_size = 18
    # 1 获取数据
    myfilename = '../data/name_classfication.txt'
    my_list_x, my_list_y = read_data(myfilename)
    # 2 实例化dataset对象
    nameclassdataset = NameClassDataset(my_list_x, my_list_y)
    # 3 实例化dataloader
    mydataloader = DataLoader(dataset=nameclassdataset, batch_size=1, shuffle=True)
    my_rnn = RNN(n_letters, n_hidden, categorynum)
    my_lstm = LSTM(n_letters, n_hidden, categorynum)
    my_gru = GRU(n_letters, n_hidden, categorynum)


    for i, (x, y) in enumerate(mydataloader):
        # print('x.shape', x.shape, x)
        # print('y.shape', y.shape, y)
        # 初始化一个三维的隐层0张量, 也是初始的细胞状态张量
        output, hidden = my_rnn(x[0], my_rnn.inithidden())
        print("rnn output.shape--->:", output.shape, output)
        if (i == 0):
            break

    for i, (x, y) in enumerate(mydataloader):
        # print('x.shape', x.shape, x)
        # print('y.shape', y.shape, y)
        hidden, c = my_lstm.inithidden()
        output, hidden, c = my_lstm(x[0], hidden, c)
        print("lstm output.shape--->:", output.shape, output)
        if (i == 0):
            break

    for i, (x, y) in enumerate(mydataloader):
        # print('x.shape', x.shape, x)
        # print('y.shape', y.shape, y)
        output, hidden = my_gru(x[0], my_gru.inithidden())
        print("gru output.shape--->:", output.shape, output)
        if (i == 0):
            break

训练

传统RNN
# 模型训练参数
mylr = 1e-3
epochs = 1

def my_train_rnn():

    # 获取数据
    myfilename = './data/name_classfication.txt'
    my_list_x, my_list_y = read_data(myfilename)

    # 实例化dataset对象
    nameclassdataset = NameClassDataset(my_list_x, my_list_y)

    # 实例化 模型
    input_size = 57
    n_hidden = 128
    output_size = 18
    my_rnn = RNN(input_size, n_hidden, output_size)
    print('my_rnn模型--->', my_rnn)

    # 实例化 损失函数 adam优化器
    mycrossentropyloss = nn.NLLLoss()
    myadam = optim.Adam(my_rnn.parameters(), lr=mylr)

    # 定义模型训练参数
    starttime = time.time()
    total_iter_num = 0  # 已训练的样本数
    total_loss = 0.0  # 已训练的损失和
    total_loss_list = []  # 每100个样本求一次平均损失 形成损失列表
    total_acc_num = 0  # 已训练样本预测准确总数
    total_acc_list = []  # 每100个样本求一次平均准确率 形成平均准确率列表

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

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

        # 内层for循环 控制迭代次数
        for i, (x, y) in enumerate(mydataloader):
            # 给模型喂数据
            output, hidden = my_rnn(x[0], my_rnn.inithidden())

            # 计算损失
            myloss = mycrossentropyloss(output, y)

            # 梯度清零
            myadam.zero_grad()

            # 反向传播
            myloss.backward()

            # 梯度更新
            myadam.step()

            # 计算总损失
            total_iter_num = total_iter_num + 1
            total_loss = total_loss + myloss.item()

            # 计算总准确率
            i_predit_tag = (1 if torch.argmax(output).item() == y.item() else 0)
            total_acc_num = total_acc_num + i_predit_tag

            # 每100次训练 求一次平均损失 平均准确率
            if (total_iter_num % 100 == 0):
                tmploss = total_loss/total_iter_num
                total_loss_list.append(tmploss)

                tmpacc = total_acc_num/total_iter_num
                total_acc_list.append(tmpacc)

            # 每2000次训练 打印日志
            if (total_iter_num % 2000 == 0):
                tmploss = total_loss / total_iter_num
                print('轮次:%d, 损失:%.6f, 时间:%d,准确率:%.3f' %(epoch_idx+1, tmploss, time.time() - starttime, tmpacc))

        # 每个轮次保存模型
        torch.save(my_rnn.state_dict(), './my_rnn_model_%d.bin' % (epoch_idx + 1))

    # 计算总时间
    total_time = int(time.time() - starttime)

    return total_loss_list, total_time, total_acc_list
LSTM
def my_train_lstm():

    # 获取数据
    myfilename = './data/name_classfication.txt'
    my_list_x, my_list_y = read_data(myfilename)

    # 实例化dataset对象
    nameclassdataset = NameClassDataset(my_list_x, my_list_y)

    # 实例化 模型
    input_size = 57
    n_hidden = 128
    output_size = 18
    my_lstm = LSTM(input_size, n_hidden, output_size)
    print('my_lstm模型--->', my_lstm)

    # 实例化 损失函数 adam优化器
    mycrossentropyloss = nn.NLLLoss()
    myadam = optim.Adam(my_lstm.parameters(), lr=mylr)

    # 定义模型训练参数
    starttime = time.time()
    total_iter_num = 0  # 已训练的样本数
    total_loss = 0.0  # 已训练的损失和
    total_loss_list = []  # 每100个样本求一次平均损失 形成损失列表
    total_acc_num = 0  # 已训练样本预测准确总数
    total_acc_list = []  # 每100个样本求一次平均准确率 形成平均准确率列表

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

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

        # 内层for循环 控制迭代次数
        for i, (x, y) in enumerate(mydataloader):
            # 给模型喂数据
            hidden, c = my_lstm.inithidden()
            output, hidden, c = my_lstm(x[0], hidden, c)

            # 计算损失
            myloss = mycrossentropyloss(output, y)

            # 梯度清零
            myadam.zero_grad()

            # 反向传播
            myloss.backward()

            # 梯度更新
            myadam.step()

            # 计算总损失
            total_iter_num = total_iter_num + 1
            total_loss = total_loss + myloss.item()

            # 计算总准确率
            i_predit_tag = (1 if torch.argmax(output).item() == y.item() else 0)
            total_acc_num = total_acc_num + i_predit_tag

            # 每100次训练 求一次平均损失 平均准确率
            if (total_iter_num % 100 == 0):
                tmploss = total_loss/total_iter_num
                total_loss_list.append(tmploss)

                tmpacc = total_acc_num/total_iter_num
                total_acc_list.append(tmpacc)

            # 每2000次训练 打印日志
            if (total_iter_num % 2000 == 0):
                tmploss = total_loss / total_iter_num
                print('轮次:%d, 损失:%.6f, 时间:%d,准确率:%.3f' %(epoch_idx+1, tmploss, time.time() - starttime, tmpacc))

        # 每个轮次保存模型
        torch.save(my_lstm.state_dict(), './my_lstm_model_%d.bin' % (epoch_idx + 1))

    # 计算总时间
    total_time = int(time.time() - starttime)

    return total_loss_list, total_time, total_acc_list
GRU
def my_train_gru():

    # 获取数据
    myfilename = './data/name_classfication.txt'
    my_list_x, my_list_y = read_data(myfilename)

    # 实例化dataset对象
    nameclassdataset = NameClassDataset(my_list_x, my_list_y)

    # 实例化 模型
    input_size = 57
    n_hidden = 128
    output_size = 18
    my_gru = GRU(input_size, n_hidden, output_size)
    print('my_gru模型--->', my_gru)

    # 实例化 损失函数 adam优化器
    mycrossentropyloss = nn.NLLLoss()
    myadam = optim.Adam(my_gru.parameters(), lr=mylr)

    # 定义模型训练参数
    starttime = time.time()
    total_iter_num = 0  # 已训练的样本数
    total_loss = 0.0  # 已训练的损失和
    total_loss_list = []  # 每100个样本求一次平均损失 形成损失列表
    total_acc_num = 0  # 已训练样本预测准确总数
    total_acc_list = []  # 每100个样本求一次平均准确率 形成平均准确率列表

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

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

        # 内层for循环 控制迭代次数
        for i, (x, y) in enumerate(mydataloader):
            # 给模型喂数据
            output, hidden = my_gru(x[0], my_gru.inithidden())

            # 计算损失
            myloss = mycrossentropyloss(output, y)

            # 梯度清零
            myadam.zero_grad()

            # 反向传播
            myloss.backward()

            # 梯度更新
            myadam.step()

            # 计算总损失
            total_iter_num = total_iter_num + 1
            total_loss = total_loss + myloss.item()

            # 计算总准确率
            i_predit_tag = (1 if torch.argmax(output).item() == y.item() else 0)
            total_acc_num = total_acc_num + i_predit_tag

            # 每100次训练 求一次平均损失 平均准确率
            if (total_iter_num % 100 == 0):
                tmploss = total_loss/total_iter_num
                total_loss_list.append(tmploss)

                tmpacc = total_acc_num/total_iter_num
                total_acc_list.append(tmpacc)

            # 每2000次训练 打印日志
            if (total_iter_num % 2000 == 0):
                tmploss = total_loss / total_iter_num
                print('轮次:%d, 损失:%.6f, 时间:%d,准确率:%.3f' %(epoch_idx+1, tmploss, time.time() - starttime, tmpacc))

        # 每个轮次保存模型
        torch.save(my_gru.state_dict(), './my_gru_model_%d.bin' % (epoch_idx + 1))

    # 计算总时间
    total_time = int(time.time() - starttime)

    return total_loss_list, total_time, total_acc_list

进行预测

构建预测函数

# 1 构建传统RNN预测函数
my_path_rnn = './model/my_rnn_model_1.bin'
my_path_lstm = './model/my_lstm_model_1.bin'
my_path_gru = './model/my_gru_model_1.bin'

# 将人名转化为onehot张量
# eg 'bai' --> [3,57]
def lineToTensor(x):
    # 文本张量化x
    tensor_x = torch.zeros(len(x), n_letters)
    # 遍历这个人名中的每个字符索引和字符
    for li, letter in enumerate(x):
        # letter在字符串all_letters中的位置 就是onehot张量1索引的位置
        # letter在字符串all_letters中的位置 使用字符串find()方法获取
        tensor_x[li][all_letters.find(letter)] = 1
    return tensor_x
传统RNN
# 思路分析
# 1 输入文本数据 张量化one-hot
# 2 实例化模型 加载已训练模型参数 m.load_state_dict(torch.load(my_path_rnn))
# 3 模型预测 with torch.no_grad()
# 4 从预测结果中取出前3名,显示打印结果 output.topk(3, 1, True)
#   category_idx = topi[0][i] category = categorys[category_idx]

# 构建rnn预测函数
def my_predict_rnn(x):

    n_letters = 57
    n_hidden = 128
    n_categories = 18


    # 输入文本, 张量化one-hot
    x_tensor = lineToTensor(x)

    # 实例化模型 加载已训练模型参数
    my_rnn = RNN(n_letters, n_hidden, n_categories)
    my_rnn.load_state_dict(torch.load(my_path_rnn))

    with torch.no_grad():
        # 模型预测
        output, hidden = my_rnn(x_tensor, my_rnn.inithidden())

        # 从预测结果中取出前3名
        # 3表示取前3名, 1表示要排序的维度, True表示是否返回最大或是最下的元素
        topv, topi = output.topk(3, 1, True)

        print('rnn =>', x)
        for i in range(3):
            value = topv[0][i]
            category_idx = topi[0][i]
            category = categorys[category_idx]
            print('\t value:%d  category:%s' %(value, category))
LSTM
# 构建LSTM 预测函数
def my_predict_lstm(x):

    n_letters = 57
    n_hidden = 128
    n_categories = 18

    # 输入文本, 张量化one-hot
    x_tensor = lineToTensor(x)

    # 实例化模型 加载已训练模型参数
    my_lstm = LSTM(n_letters, n_hidden, n_categories)
    my_lstm.load_state_dict(torch.load(my_path_lstm))

    with torch.no_grad():
        # 模型预测
        hidden, c = my_lstm.inithidden()
        output, hidden, c = my_lstm(x_tensor, hidden, c)

        # 从预测结果中取出前3名
        # 3表示取前3名, 1表示要排序的维度, True表示是否返回最大或是最下的元素
        topv, topi = output.topk(3, 1, True)

        print('rnn =>', x)
        for i in range(3):
            value = topv[0][i]
            category_idx = topi[0][i]
            category = categorys[category_idx]
            print('\t value:%d  category:%s' % (value, category))
            print('\t value:%d  category:%s' % (value, category))
GRU
# 构建GRU 预测函数
def my_predict_gru(x):

    n_letters = 57
    n_hidden = 128
    n_categories = 18

    # 输入文本, 张量化one-hot
    x_tensor = lineToTensor(x)

    # 实例化模型 加载已训练模型参数
    my_gru = GRU(n_letters, n_hidden, n_categories)
    my_gru.load_state_dict(torch.load(my_path_gru))

    with torch.no_grad():
        # 模型预测
        output, hidden = my_gru(x_tensor, my_gru.inithidden())

        # 从预测结果中取出前3名
        # 3表示取前3名, 1表示要排序的维度, True表示是否返回最大或是最下的元素
        topv, topi = output.topk(3, 1, True)

        print('rnn =>', x)
        for i in range(3):
            value = topv[0][i]
            category_idx = topi[0][i]
            category = categorys[category_idx]
            print('\t value:%d  category:%s' % (value, category))
调用
def dm_test_predic_rnn_lstm_gru():
        # 把三个函数的入口地址 组成列表,统一输入数据进行测试
    for func in [my_predict_rnn, my_predict_lstm, my_predict_gru]:
        func('zhang')

注意力机制

注意力机制简介

注意力概念

我们观察事物时,之所以能够快速判断一种事物(当然允许判断是错误的), 是因为我们大脑能够很快把注意力放在事物最具有辨识度的部分从而作出判断,而并非是从头到尾的观察一遍事物后,才能有判断结果. 正是基于这样的理论,就产生了注意力机制.

注意力计算规则

它需要三个指定的输入Q(query), K(key), V(value), 然后通过计算公式得到注意力的结果, 这个结果代表query在key和value作用下的注意力表示. 当输入的Q=K=V时, 称作自注意力计算规则.

作用

在解码器端的注意力机制: 能够根据模型目标有效的聚焦编码器的输出结果, 当其作为解码器的输入时提升效果. 改善以往编码器输出是单一定长张量, 无法存储过多信息的情况.
在编码器端的注意力机制: 主要解决表征问题, 相当于特征提取过程, 得到输入的注意力表示. 一般使用自注意力(self-attention).

生活场景帮助理解

我们做英语的阅读理解时,4个选项只有1个是对的,其余都是干扰项。注意力机制就是把焦点集中在正确选项的获取上。

bmm运算简介

# 如果参数1形状是(b × n × m), 参数2形状是(b × m × p), 则输出为(b × n × p)
>>> input = torch.randn(10, 3, 4)
>>> mat2 = torch.randn(10, 4, 5)
>>> res = torch.bmm(input, mat2)
>>> res.size()
torch.Size([10, 3, 5])

代码实现

import torch
import torch.nn as nn
import torch.nn.functional as F

# MyAtt类实现思路分析
# 1 init函数 (self, query_size, key_size, value_size1, value_size2, output_size)
# 准备2个线性层 注意力权重分布self.attn 注意力结果表示按照指定维度进行输出层 self.attn_combine
# 2 forward(self, Q, K, V):
# 求查询张量q的注意力权重分布, attn_weights[1,32]
# 求查询张量q的注意力结果表示 bmm运算, attn_applied[1,1,64]
# q 与 attn_applied 融合,再按照指定维度输出 output[1,1,32]
# 返回注意力结果表示output:[1,1,32], 注意力权重分布attn_weights:[1,32]

class MyAtt(nn.Module):
    #                   32          32          32              64      32
    def __init__(self, query_size, key_size, value_size1, value_size2, output_size):
        super(MyAtt, self).__init__()
        self.query_size = query_size
        self.key_size = key_size
        self.value_size1 = value_size1
        self.value_size2 = value_size2
        self.output_size = output_size

        # 线性层1 注意力权重分布
        self.attn = nn.Linear(self.query_size + self.key_size, self.value_size1)

        # 线性层2 注意力结果表示按照指定维度输出层 self.attn_combine
        self.attn_combine = nn.Linear(self.query_size+self.value_size2, output_size)

    def forward(self, Q, K, V):
        # 1 求查询张量q的注意力权重分布, attn_weights[1,32]
        # [1,1,32],[1,1,32]--> [1,32],[1,32]->[1,64]
        # [1,64] --> [1,32]
        # tmp1 = torch.cat( (Q[0], K[0]), dim=1)
        # tmp2 = self.attn(tmp1)
        # tmp3 = F.softmax(tmp2, dim=1)
        attn_weights = F.softmax( self.attn(torch.cat( (Q[0], K[0]), dim=1)), dim=1)

        # 2 求查询张量q的结果表示 bmm运算, attn_applied[1,1,64]
        # [1,1,32] * [1,32,64] ---> [1,1,64]
        attn_applied =  torch.bmm(attn_weights.unsqueeze(0), V)

        # 3 q 与 attn_applied 融合,再按照指定维度输出 output[1,1,64]
        # 3-1 q与结果表示拼接 [1,32],[1,64] ---> [1,96]
        output = torch.cat((Q[0], attn_applied[0]), dim=1)
        # 3-2 shape [1,96] ---> [1,32]
        output = self.attn_combine(output).unsqueeze(0)

        # 4 返回注意力结果表示output:[1,1,32], 注意力权重分布attn_weights:[1,32]
        return output, attn_weights
if __name__ == '__main__':
    # 为什么引入注意力机制:
    # rnn系列循环神经网络,随着时间步的增长,会对前面单词的特征遗忘,造成对句子特征提取不充分
    # rnn系列循环神经网络是一个时间步一个时间步的提取句子特征,效率低下
    # 能不能对32个单词同时提取事物特征,而且还是并行的,这就是注意力机制!

    # 任务描述
    # v是内容比如32个单词每个单词64个特征,k是32个单词的索引,q是查询张量
    # 我们的任务:输入查询张量q,通过注意力机制来计算如下信息:
    # 1、查询张量q的注意力权重分布:查询张量q和其他32个单词相关性(相识度)
    # 2、查询张量q的结果表示:有一个普通的q升级成一个更强大q;用q和v做bmm运算
    # 3 注意:查询张量q查询的目标是谁,就是谁的查询张量。
    #   eg:比如查询张量q未来就是来查询单词"我",则q就是"我"的查询张量

    query_size = 32
    key_size = 32
    value_size1 = 32 # 32个单词
    value_size2 = 64 # 64个特征
    output_size = 32

    Q = torch.randn(1, 1, 32)
    K = torch.randn(1, 1, 32)
    V = torch.randn(1, 32, 64)
    # V = torch.randn(1, value_size1, value_size2)

    # 1 实例化注意力类 对象
    myattobj = MyAtt(query_size, key_size, value_size1, value_size2, output_size)

    # 2 把QKV数据扔给注意机制,求查询张量q的注意力结果表示、注意力权重分布
    (output, attn_weights) = myattobj(Q, K, V)
    print('查询张量q的注意力结果表示output--->', output.shape, output)
    print('查询张量q的注意力权重分布attn_weights--->', attn_weights.shape, attn_weights)

RNN案例 seq2seq英译法

seq2seq介绍

seq2seq模型架构

在这里插入图片描述

模型解释

seq2seq模型架构包括三部分,分别是encoder(编码器)、decoder(解码器)、中间语义张量c。其中编码器和解码器的内部实现都使用了GRU模型
图中表示的是一个中文到英文的翻译:欢迎 来 北京 → welcome to BeiJing。编码器首先处理中文输入"欢迎 来 北京",通过GRU模型获得每个时间步的输出张量,最后将它们拼接成一个中间语义张量c;接着解码器将使用这个中间语义张量c以及每一个时间步的隐层张量, 逐个生成对应的翻译语言

数据集下载

eng_transfor_fra下载

导包并进行文件清洗

# 用于正则表达式
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

思路分析

my_getdata() 清洗文本构建字典思路分析
1 按行读文件 open().read().strip().split(\n) my_lines
2 按行清洗文本 构建语言对 my_pairs
2-1格式 [['英文', '法文'], ['英文', '法文'], ['英文', '法文'], ['英文', '法文']....]
2-2调用清洗文本工具函数normalizeString(s)
3 遍历语言对 构建英语单词字典 法语单词字典
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')
    # 2 按行清洗文本 构建语言对 my_pairs
    my_pairs = [[normalizeString(s) for s in l.split('\t')] for l in my_lines]

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

    french_word2index = {0: "SOS", 1: "EOS"}
    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
                # print(english_word2index)
                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()}

    return english_word2index, english_index2word, english_word_n,\
           french_word2index, french_index2word, french_word_n, my_pairs


def main():
    # 全局函数 获取英语单词字典 法语单词字典 语言对列表my_pairs
    english_word2index, english_index2word, english_word_n, \
    french_word2index, french_index2word, french_word_n, \
    my_pairs = my_getdata()
    
    return 0

构建数据源对象并测试

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]
        if index < 0:
            index = max(-self.sample_len, index)
        else:
            index = min(self.sample_len - 1, index)
        # 按索引获取 数据样本 x y
        x = self.my_pairs[index][0]
        y = self.my_pairs[index][1]
        # 样本x 文本数值化
        x = [english_word2index[word] for word in x.split(' ')]
        # print(x)
        x.append(EOS_token)
        # print("x2: ", x)
        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


def dm_test_MyPairsDataset(my_pairs):

    # 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


# 全局函数 获取英语单词字典 法语单词字典 语言对列表my_pairs
english_word2index, english_index2word, english_word_n, \
french_word2index, french_index2word, french_word_n, \
my_pairs = my_getdata()


def main():
    dm_test_MyPairsDataset(my_pairs)
    return 0

编码器和解码器

构建基于GRU的编码器

思路分析
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)
代码实现
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)

基于GRU和Attention的解码器

结构图

在这里插入图片描述

代码实现
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)

训练模型

teacher_forcing

teacher_forcing是一种用于序列生成任务的训练技巧, 在seq2seq架构中, 根据循环神经网络理论,解码器每次应该使用上一步的结果作为输入的一部分, 但是训练过程中,一旦上一步的结果是错误的,就会导致这种错误被累积,无法达到训练效果, 因此,我们需要一种机制改变上一步出错的情况,由此诞生teacher_forcing

内部迭代训练函数

设置参数
# 模型训练参数
mylr = 1e-4
epochs = 2
# 设置teacher_forcing比率为0.5
teacher_forcing_ratio = 0.5
print_interval_num = 1000
plot_interval_num = 100
代码实现
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_c[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

训练

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

模型评估与测试

评估函数书写

def Seq2Seq_Evaluate(x, my_encoderrnn, my_attndecoderrnn):
    """
    模型评估代码与模型预测代码类似,需要注意使用with torch.no_grad()
    模型预测时,第一个时间步使用SOS_token作为输入 后续时间步采用预测值作为输入,也就是自回归机制
    """"""
    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]

评估

# 加载模型
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 .', 'vous gagnez n est ce pas ?']]
    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)

关于服务器调试python的说明

以刚才的代码为例子(py文件为"翻译.py"),勿喷,我考虑到有人可能会用中文命名出问题,特此尝试,顺便解决问题。大家千万不要编程用中文!大家千万不要编程用中文!大家千万不要编程用中文!

操作前需要知道的知识点

nohup

nohup 命令运行由 Command参数和任何相关的 Arg参数指定的命令,忽略所有挂断(SIGHUP)信号。在注销后使用 nohup 命令运行后台中的程序。要运行后台中的 nohup 命令,添加 & ( 表示“and”的符号)到命令的尾部。
参数说明:
Command:要执行的命令。
Arg:一些参数,可以指定输出文件。
&:让命令在后台执行,终端退出后命令仍旧执行。

tail

tail 命令可用于查看文件的内容,有一个常用的参数 -f 常用于查阅正在改变的日志文件
参数:
-f 循环读取
-q 不显示处理信息
-v 显示详细的处理信息
-c<数目> 显示的字节数
-n<行数> 显示文件的尾部 n 行内容
–pid=PID 与-f合用,表示在进程ID,PID死掉之后结束
-q, --quiet, --silent 从不输出给出文件名的首部
-s, --sleep-interval=S 与-f合用,表示在每次反复的间隔休眠S秒

首先进入python文件所在目录并进行编译

在这里插入图片描述

转后台进程

nohup python -u 翻译.py > run.log 2>&1 &

启动一个SSH连接看训练输出

声明:过大的模型生成需要时间,如果没有请先耐心等待

tail -f run.log

执行结果

在这里插入图片描述
成了

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值