RNN及其变体的概念和案例
传统RNN模型
传统RNN结构分析
- 结构解释图:
- 内部结构分析: 我们把目光集中在中间的方块部分, 它的输入有两部分, 分别是h(t-1)以及x(t), 代表上一时间步的隐层输出, 以及此时间步的输入, 它们进入RNN结构体后, 会"融合"到一起, 这种融合我们根据结构解释可知, 是将二者进行拼接, 形成新的张量[x(t), h(t-1)], 之后这个新的张量将通过一个全连接层(线性层), 该层使用tanh作为激活函数, 最终得到该时间步的输出h(t), 它将作为下一个时间步的输入和x(t+1)一起进入结构体。以此类推。
- 内部结构过程演示:
根据结构分析得出内部计算公式:
激活函数tanh的作用:用于帮助调节流经网络的值, tanh函数将值压缩在-1和1之间。
使用Pytorch构建RNN模型
- 位置: 在torch.nn工具包之中, 通过torch.nn.RNN可调用
- nn.RNN使用示例1:
import torch
import torch.nn as nndef dm_rnn_for_base():'''第一个参数:input_size(输入张量x的维度)第二个参数:hidden_size(隐藏层的维度, 隐藏层的神经元个数)第三个参数:num_layer(隐藏层的数量)'''rnn = nn.RNN(5, 6, 1) #A'''第一个参数:sequence_length(输入序列的长度)第二个参数:batch_size(批次的样本数量)第三个参数:input_size(输入张量的维度)'''input = torch.randn(1, 3, 5) #B'''第一个参数:num_layer * num_directions(层数*网络方向)第二个参数:batch_size(批次的样本数)第三个参数:hidden_size(隐藏层的维度, 隐藏层神经元的个数)'''h0 = torch.randn(1, 3, 6) #C# [1,3,5],[1,3,6] ---> [1,3,6],[1,3,6]output, hn = rnn(input, h0)print('output--->',output.shape, output)print('hn--->',hn.shape, hn)print('rnn模型--->', rnn)# 程序运行效果如下:
output---> torch.Size([1, 3, 6]) tensor([[[ 0.8947, -0.6040, 0.9878, -0.1070, -0.7071, -0.1434],[ 0.0955, -0.8216, 0.9475, -0.7593, -0.8068, -0.5549],[-0.1524, 0.7519, -0.1985, 0.0937, 0.2009, -0.0244]]],grad_fn=<StackBackward0>)hn---> torch.Size([1, 3, 6]) tensor([[[ 0.8947, -0.6040, 0.9878, -0.1070, -0.7071, -0.1434],[ 0.0955, -0.8216, 0.9475, -0.7593, -0.8068, -0.5549],[-0.1524, 0.7519, -0.1985, 0.0937, 0.2009, -0.0244]]],grad_fn=<StackBackward0>)rnn模型---> RNN(5, 6)
- nn.RNN使用示例2:
# 输入数据长度发生变化
def dm_rnn_for_sequencelen():'''第一个参数:input_size(输入张量x的维度)第二个参数:hidden_size(隐藏层的维度, 隐藏层的神经元个数)第三个参数:num_layer(隐藏层的数量)'''rnn = nn.RNN(5, 6, 1) #A'''第一个参数:sequence_length(输入序列的长度)第二个参数:batch_size(批次的样本数量)第三个参数:input_size(输入张量的维度)'''input = torch.randn(20, 3, 5) #B'''第一个参数:num_layer * num_directions(层数*网络方向)第二个参数:batch_size(批次的样本数)第三个参数:hidden_size(隐藏层的维度, 隐藏层神经元的个数)'''h0 = torch.randn(1, 3, 6) #C# [20,3,5],[1,3,6] --->[20,3,6],[1,3,6]output, hn = rnn(input, h0) #print('output--->', output.shape)print('hn--->', hn.shape)print('rnn模型--->', rnn)# 程序运行效果如下:
output---> torch.Size([20, 3, 6])
hn---> torch.Size([1, 3, 6])
rnn模型---> RNN(5, 6)
- nn.RNN使用示例3:
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) # Coutput, 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等于最后一个隐藏层# 程序运行效果如下:
output--> torch.Size([1, 3, 6]) tensor([[[ 0.4987, -0.5756, 0.1934, 0.7284, 0.4478, -0.1244],[ 0.6753, 0.5011, -0.7141, 0.4480, 0.7186, 0.5437],[ 0.6260, 0.7600, -0.7384, -0.5080, 0.9054, 0.6011]]],grad_fn=<StackBackward0>)
hn--> torch.Size([2, 3, 6]) tensor([[[ 0.4862, 0.6872, -0.0437, -0.7826, -0.7136, -0.5715],[ 0.8942, 0.4524, -0.1695, -0.5536, -0.4367, -0.3353],[ 0.5592, 0.0444, -0.8384, -0.5193, 0.7049, -0.0453]],[[ 0.4987, -0.5756, 0.1934, 0.7284, 0.4478, -0.1244],[ 0.6753, 0.5011, -0.7141, 0.4480, 0.7186, 0.5437],[ 0.6260, 0.7600, -0.7384, -0.5080, 0.9054, 0.6011]]],grad_fn=<StackBackward0>)
rnn模型---> RNN(5, 6, num_layers=2)
传统RNN优缺点
传统RNN的优势
- 由于内部结构简单, 对计算资源要求低, 相比之后我们要学习的RNN变体:LSTM和GRU模型参数总量少了很多, 在短序列任务上性能和效果都表现优异。
传统RNN的缺点
- 传统RNN在解决长序列之间的关联时, 通过实践,证明经典RNN表现很差, 原因是在进行反向传播的时候, 过长的序列导致梯度的计算异常, 发生梯度消失或爆炸。
梯度消失或爆炸介绍
根据反向传播算法和链式法则, 梯度的计算可以简化为以下公式:
- 其中sigmoid的导数值域是固定的, 在[0, 0.25]之间, 而一旦公式中的w也小于1, 那么通过这样的公式连乘后, 最终的梯度就会变得非常非常小, 这种现象称作梯度消失. 反之, 如果我们人为的增大w的值, 使其大于1, 那么连乘够就可能造成梯度过大, 称作梯度爆炸.
- 梯度消失或爆炸的危害:
如果在训练过程中发生了梯度消失,权重无法被更新,最终导致训练失败; 梯度爆炸所带来的梯度过大,大幅度更新网络参数,在极端情况下,结果会溢出(NaN值)。
LSTM模型
LSTM介绍
LSTM(Long Short-Term Memory)也称长短时记忆结构, 它是传统RNN的变体, 与经典RNN相比能够有效捕捉长序列之间的语义关联, 缓解梯度消失或爆炸现象。 同时LSTM的结构更复杂, 它的核心结构可以分为四个部分去解析:
- 遗忘门
- 输入门
- 细胞状态
- 输出门
LSTM的内部结构
LSTM结构分析
- 结构解释图:
- 遗忘门部分结构图与计算公式:
- 遗忘门结构分析: 与传统RNN的内部结构计算非常相似, 首先将当前时间步输入x(t)与上一个时间步隐含状态h(t-1)拼接, 得到[x(t), h(t-1)], 然后通过一个全连接层做变换, 最后通过sigmoid函数进行激活得到f(t), 我们可以将f(t)看作是门值, 好比一扇门开合的大小程度, 门值都将作用在通过该扇门的张量, 遗忘门门值将作用的上一层的细胞状态上, 代表遗忘过去的多少信息, 又因为遗忘门门值是由x(t), h(t-1)计算得来的, 因此整个公式意味着根据当前时间步输入和上一个时间步隐含状态h(t-1)来决定遗忘多少上一层的细胞状态所携带的过往信息。
- 遗忘门内部结构过程演示:
- 激活函数sigmiod的作用: * 用于帮助调节流经网络的值, sigmoid函数将值压缩在0和1之间。
- 输入门部分结构图与计算公式:
- 输入门结构分析: 我们看到输入门的计算公式有两个, 第一个就是产生输入门门值的公式, 它和遗忘门公式几乎相同, 区别只是在于它们之后要作用的目标上. 这个公式意味着输入信息有多少需要进行过滤. 输入门的第二个公式是与传统RNN的内部结构计算相同. 对于LSTM来讲, 它得到的是当前的细胞状态, 而不是像经典RNN一样得到的是隐含状态。
- 输入门内部结构过程演示:
- 细胞状态更新图与计算公式:
- 细胞状态更新分析: 细胞更新的结构与计算公式非常容易理解, 这里没有全连接层, 只是将刚刚得到的遗忘门门值与上一个时间步得到的C(t-1)相乘, 再加上输入门门值与当前时间步得到的未更新C(t)相乘的结果. 最终得到更新后的C(t)作为下一个时间步输入的一部分. 整个细胞状态更新过程就是对遗忘门和输入门的应用。
- 细胞状态更新过程演示:
- 输出门部分结构图与计算公式:
- 输出门结构分析: 输出门部分的公式也是两个, 第一个即是计算输出门的门值, 它和遗忘门,输入门计算方式相同. 第二个即是使用这个门值产生隐含状态h(t), 他将作用在更新后的细胞状态C(t)上, 并做tanh激活, 最终得到h(t)作为下一时间步输入的一部分. 整个输出门的过程, 就是为了产生隐含状态h(t)。
- 输出门内部结构过程演示:
Bi-LSTM介绍
Bi-LSTM即双向LSTM, 它没有改变LSTM本身任何的内部结构, 只是将LSTM应用两次且方向不同, 再将两次得到的LSTM结果进行拼接作为最终输出。
- Bi-LSTM结构分析: 我们看到图中对"我爱中国"这句话或者叫这个输入序列, 进行了从左到右和从右到左两次LSTM处理, 将得到的结果张量进行了拼接作为最终输出. 这种结构能够捕捉语言语法中一些特定的前置或后置特征, 增强语义关联,但是模型参数和计算复杂度也随之增加了一倍, 一般需要对语料和计算资源进行评估后决定是否使用该结构。
使用Pytorch构建LSTM模型
- 位置: 在torch.nn工具包之中, 通过torch.nn.LSTM可调用.
- nn.LSTM类初始化主要参数解释: input_size: 输入张量x中特征维度的大小. 。hidden_size: 隐层张量h中特征维度的大小。 num_layers: 隐含层的数量。bidirectional: 是否选择使用双向LSTM, 如果为True, 则使用; 默认不使用。
- nn.LSTM类实例化对象主要参数解释: input: 输入张量x。h0: 初始化的隐层张量h。c0: 初始化的细胞状态张量c。
- nn.LSTM使用示例:
# 定义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>)
LSTM优缺点
- LSTM优势:
LSTM的门结构能够有效减缓长序列问题中可能出现的梯度消失或爆炸, 虽然并不能杜绝这种现象, 但在更长的序列问题上表现优于传统RNN。
- LSTM缺点:
由于内部结构相对较复杂, 因此训练效率在同等算力下较传统RNN低很多。
GRU模型
GRU介绍
GRU(Gated Recurrent Unit)也称门控循环单元结构, 它也是传统RNN的变体, 同LSTM一样能够有效捕捉长序列之间的语义关联, 缓解梯度消失或爆炸现象。同时它的结构和计算要比LSTM更简单, 它的核心结构可以分为两个部分去解析:
- 更新门
- 重置门
GRU的内部结构图
GRU结构分析
- 结构解释图:
- GRU的更新门和重置门结构图:
- 内部结构分析:
和之前分析过的LSTM中的门控一样, 首先计算更新门和重置门的门值, 分别是z(t)和r(t), 计算方法就是使用X(t)与h(t-1)拼接进行线性变换, 再经过sigmoid激活。 之后重置门门值作用在了h(t-1)上, 代表控制上一时间步传来的信息有多少可以被利用.。接着就是使用这个重置后的h(t-1)进行基本的RNN计算, 即与x(t)拼接进行线性变化, 经过tanh激活, 得到新的h(t)。最后更新门的门值会作用在新的h(t),而1-门值会作用在h(t-1)上, 随后将两者的结果相加, 得到最终的隐含状态输出h(t), 这个过程意味着更新门有能力保留之前的结果, 当门值趋于1时, 输出就是新的h(t), 而当门值趋于0时, 输出就是上一时间步的h(t-1)。
Bi-GRU介绍
Bi-GRU与Bi-LSTM的逻辑相同, 都是不改变其内部结构, 而是将模型应用两次且方向不同, 再将两次得到的LSTM结果进行拼接作为最终输出。具体参见上小节中的Bi-LSTM。
使用Pytorch构建GRU模型
- 位置: 在torch.nn工具包之中, 通过torch.nn.GRU可调用。
- nn.GRU类初始化主要参数解释: input_size: 输入张量x中特征维度的大小。hidden_size: 隐层张量h中特征维度的大小。num_layers: 隐含层的数量。bidirectional: 是否选择使用双向LSTM, 如果为True, 则使用; 默认不使用。
- nn.GRU类实例化对象主要参数解释: input: 输入张量x。h0: 初始化的隐层张量h。
- nn.GRU使用示例:
import torch
import torch.nn as nn
rnn = nn.GRU(5, 6, 2)
input = torch.randn(1, 3, 5)
h0 = torch.randn(2, 3, 6)
output, hn = rnn(input, h0)输出结果:
output
tensor([[[-0.2097, -2.2225, 0.6204, -0.1745, -0.1749, -0.0460],[-0.3820, 0.0465, -0.4798, 0.6837, -0.7894, 0.5173],[-0.0184, -0.2758, 1.2482, 0.5514, -0.9165, -0.6667]]],grad_fn=<StackBackward>)
>>> hn
tensor([[[ 0.6578, -0.4226, -0.2129, -0.3785, 0.5070, 0.4338],[-0.5072, 0.5948, 0.8083, 0.4618, 0.1629, -0.1591],[ 0.2430, -0.4981, 0.3846, -0.4252, 0.7191, 0.5420]],[[-0.2097, -2.2225, 0.6204, -0.1745, -0.1749, -0.0460],[-0.3820, 0.0465, -0.4798, 0.6837, -0.7894, 0.5173],[-0.0184, -0.2758, 1.2482, 0.5514, -0.9165, -0.6667]]],grad_fn=<StackBackward>)
GRU优缺点
- GRU的优势: GRU和LSTM作用相同, 在捕捉长序列语义关联时, 能有效抑制梯度消失或爆炸, 效果都优于传统RNN且计算复杂度相比LSTM要小。
- GRU的缺点: GRU仍然不能完全解决梯度消失问题, 同时其作用RNN的变体, 有着RNN结构本身的一大弊端, 即不可并行计算, 这在数据量和模型体量逐步增大的未来, 是RNN发展的关键瓶颈。
RNN案例 人名分类器
案例介绍
- 关于人名分类问题:
以一个人名为输入, 使用模型帮助我们判断它最有可能是来自哪一个国家的人名, 这在某些国际化公司的业务中具有重要意义, 在用户注册过程中, 会根据用户填写的名字直接给他分配可能的国家或地区选项, 以及该国家或地区的国旗, 限制手机号码位数等等.
人名分类数据预览
- 数据存放路径:$(home)/data/name_classfication.txt
- 数据格式说明 每一行第一个单词为人名,第二个单词为国家名。中间用制表符tab分割。
- 数据集下载:
百度网盘链接: https://pan.baidu.com/s/13nJukj4A2AMX0qEe0hOYOg?pwd=k88s 提取码: k88s
Huffmann German
Hummel German
Hummel German
Hutmacher German
Ingersleben German
Jaeger German
Jager German
Deng Chinese
Ding Chinese
Dong Chinese
Dou Chinese
Duan Chinese
Eng Chinese
Fan Chinese
Fei Chinese
Abaimov Russian
Abakeliya Russian
Abakovsky Russian
Abakshin Russian
Abakumoff Russian
Abakumov Russian
Abakumtsev Russian
Abakushin Russian
Abalakin Russian
案例步骤
整个案例的实现可分为以下五个步骤:
- 第一步导入必备的工具包
- 第二步对data文件中的数据进行处理,满足训练要求
- 第三步构建RNN模型(包括传统RNN, LSTM以及GRU)
- 第四步构建训练函数并进行训练
- 第五步构建预测函数并进行预测
导入必备的工具包
# 导入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
数据预处理
这里需要对data文件中的数据进行处理,满足训练要求。
获取常用的字符数量
# 获取所有常用字符包括字母和常用标点
all_letters = string.ascii_letters + " .,;'"# 获取常用字符数量
n_letters = len(all_letters)print("n_letter:", n_letters)
- 输出效果:
国家名种类数和个数
# 国家名 种类数
categorys = ['Italian', 'English', 'Arabic', 'Spanish', 'Scottish', 'Irish', 'Chinese', 'Vietnamese', 'Japanese','French', 'Greek', 'Dutch', 'Korean', 'Polish', 'Portuguese', 'Russian', 'Czech', 'German']
# 国家名 个数
categorynum = len(categorys)
print('categorys--->', categorys)
- 输出效果:
categorys---> ['Italian', 'English', 'Arabic', 'Spanish', 'Scottish', 'Irish', 'Chinese', 'Vietnamese', 'Japanese', 'French', 'Greek', 'Dutch', 'Korean', 'Polish', 'Portuguese', 'Russian', 'Czech', 'German']
categorynum---> 18
读数据到内存
# 思路分析
# 1 打开数据文件 open(filename, mode='r', encoding='utf-8')
# 2 按行读文件、提取样本x 样本y line.strip().split('\t')
# 3 返回样本x的列表、样本y的列表 my_list_x, my_list_y
def read_data(filename):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)# 打印样本的数量print('my_list_x->', len(my_list_x))print('my_list_y->', len(my_list_y))# 返回样本x的列表、样本y的列表return my_list_x, my_list_y
构建数据源NameClassDataset
# 原始数据 -> 数据源NameClassDataset --> 数据迭代器DataLoader
# 构造数据源 NameClassDataset,把语料转换成x y
# 1 init函数 设置样本x和y self.my_list_x self.my_list_y 条目数self.sample_len
# 2 __len__(self)函数 获取样本条数
# 3 __getitem__(self, index)函数 获取第几条样本数据
# 按索引 获取数据样本 x y
# 样本x one-hot张量化 tensor_x[li][all_letters.find(letter)] = 1
# 样本y 张量化 torch.tensor(categorys.index(y), dtype=torch.long)
# 返回tensor_x, tensor_y
class NameClassDataset(Dataset):def __init__(self, my_list_x, my_list_y):# 样本xself.my_list_x = my_list_x# 样本yself.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 yx = self.my_list_x[index]y = self.my_list_y[index]# 样本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
- 分析
- 文本张量化,这里也就是人名张量化是通过one-hot编码来完成。
# 将字符串(单词粒度)转化为张量表示,如:"ab" --->
# tensor([[[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
# 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
# 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
# 0., 0., 0., 0., 0., 0.]],# [[0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
# 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
# 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
# 0., 0., 0., 0., 0., 0.]]])
构建迭代器遍历数据
def dm_test_NameClassDataset():# 1 获取数据myfilename = './data/name_classfication.txt'my_list_x, my_list_y = read_data(myfilename)print('my_list_x length', len(my_list_x))print('my_list_y length', len(my_list_y))# 2 实例化dataset对象nameclassdataset = NameClassDataset(my_list_x, my_list_y)# 3 实例化dataloadermydataloader = 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
- 输出效果:
my_list_x length 20074
my_list_y length 20074
x.shape torch.Size([1, 5, 57]) tensor([[[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0.],[0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0.],[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0.],[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0.],[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,0., 0., 0., 0., 0., 0.]]])
y.shape torch.Size([1]) tensor([15])
构建RNN模型
构建RNN模型
# RNN类 实现思路分析:
# 1 init函数 准备三个层 self.rnn self.linear self.softmax=nn.LogSoftmax(dim=-1)
# def __init__(self, input_size, hidden_size, output_size, num_layers=1)# 2 forward(input, hidden)函数
# 让数据经过三个层 返回softmax结果和hn
# 形状变化 [seqlen,1,57],[1,1,128]) -> [seqlen,1,128],[1,1,128]# 3 初始化隐藏层输入数据 inithidden()
# 形状[self.num_layers, 1, self.hidden_size]
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_sizeself.hidden_size = hidden_sizeself.output_size = output_sizeself.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):'''input [6,57]-2维矩阵 hidden[1,1,57] - 3维矩阵'''# 数据形状 [6,57] -> [6,1,57]input = input.unsqueeze(1)# 1 数据经过模型 提取事物特征# 数据形状 [seqlen,1,57],[1,1,128]) -> [seqlen,1,18],[1,1,128]rr, hn = self.rnn(input, hidden)# 数据形状 [seqlen,1,128] - [1, 128] eg:[6,1,128] --> [1,128]tmprr = rr[-1]# 2 数据经过全连接层 [1,128] -->[1,18]tmprr = self.linear(tmprr)# 3 数据经过softmax层返回return self.softmax(tmprr), hndef inithidden(self):# 初始化隐藏层输入数据 inithidden()return torch.zeros(self.num_layers, 1,self.hidden_size)
- torch.unsqueeze演示:
>>> x = torch.tensor([1, 2, 3, 4])
>>> torch.unsqueeze(x, 0)
tensor([[ 1, 2, 3, 4]])
>>> torch.unsqueeze(x, 1)
tensor([[ 1],[ 2],[ 3],[ 4]])
- 调用:
def dm01_test_myrnn():# 1 实例化rnn对象myrnn = RNN(57, 128, 18)print('myrnn--->', myrnn)# 2 准备数据input = torch.randn(6, 57)print(input.shape)hidden = myrnn.inithidden()# 3 给模型1次性的送数据# [seqlen, 57], [1, 1, 128]) -> [1,18], [1,1,128]output, hidden = myrnn(input, hidden)print('一次性的送数据:output->', output.shape, output)print('hidden->', hidden.shape)# 4 给模型1个字符1个字符的喂数据hidden = myrnn.inithidden()for i in range(input.shape[0]):tmpinput = input[i].unsqueeze(0)output, hidden = myrnn(tmpinput, hidden)# 最后一次ouputprint('一个字符一个字符的送数据output->', output.shape, output)
- 调用结果:
myrnn---> RNN((rnn): RNN(57, 128)(linear): Linear(in_features=128, out_features=18, bias=True)(softmax): LogSoftmax(dim=-1)
)
torch.Size([6, 57])
一次性的送数据:output-> torch.Size([1, 18]) tensor([[-2.8194, -3.1730, -3.3112, -2.9715, -3.0997, -2.8097, -2.8016, -2.8738,-2.7229, -2.8181, -2.7881, -3.0218, -2.9169, -2.6193, -2.8507, -2.9684,-2.8589, -2.8273]], grad_fn=<LogSoftmaxBackward0>)
hidden-> torch.Size([1, 1, 128])
一个字符一个字符的送数据output-> torch.Size([1, 18]) tensor([[-2.8194, -3.1730, -3.3112, -2.9715, -3.0997, -2.8097, -2.8016, -2.8738,-2.7229, -2.8181, -2.7881, -3.0218, -2.9169, -2.6193, -2.8507, -2.9684,-2.8589, -2.8273]], grad_fn=<LogSoftmaxBackward0>)
构建LSTM模型
# LSTM类 实现思路分析:
# 1 init函数 准备三个层 self.rnn self.linear self.softmax=nn.LogSoftmax(dim=-1)
# def __init__(self, input_size, hidden_size, output_size, num_layers=1)# 2 forward(input, hidden)函数
# 让数据经过三个层 返回softmax结果和hn
# 形状变化 [seqlen,1,57],[1,1,128]) -> [seqlen,1,128],[1,1,128]# 3 初始化隐藏层输入数据 inithidden()
# 形状[self.num_layers, 1, self.hidden_size]
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_sizeself.hidden_size = hidden_sizeself.output_size = output_sizeself.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, cn) = self.rnn(input, (hidden, c))# 数据形状 [seqlen,1,128] - [1, 128]tmprr = rr[-1]tmprr = self.linear(tmprr)return self.softmax(tmprr), hn, cndef inithidden(self):# 初始化隐藏层输入数据 inithidden()hidden = c = torch.zeros(self.num_layers, 1, self.hidden_size)return hidden, c
构建GRU模型
# GRU类 实现思路分析:
# 1 init函数 准备三个层 self.rnn self.linear self.softmax=nn.LogSoftmax(dim=-1)
# def __init__(self, input_size, hidden_size, output_size, num_layers=1)# 2 forward(input, hidden)函数
# 让数据经过三个层 返回softmax结果和hn
# 形状变化 [seqlen,1,57],[1,1,128]) -> [seqlen,1,128],[1,1,128]# 3 初始化隐藏层输入数据 inithidden()
# 形状[self.num_layers, 1, self.hidden_size]
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_sizeself.hidden_size = hidden_sizeself.output_size = output_sizeself.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)return self.softmax(tmprr), hndef inithidden(self):# 初始化隐藏层输入数据 inithidden()return torch.zeros(self.num_layers, 1,self.hidden_size)
模型RNN_LSTM_GRU测试
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)print('categorys--->', categorys)# 2 实例化dataset对象nameclassdataset = NameClassDataset(my_list_x, my_list_y)# 3 实例化dataloadermydataloader = 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)print('rnn 模型', my_rnn)print('lstm 模型', my_lstm)print('gru 模型', my_gru)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):breakfor 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):breakfor 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 模型 RNN((rnn): RNN(57, 128)(linear): Linear(in_features=128, out_features=18, bias=True)(softmax): LogSoftmax(dim=-1)
)
lstm 模型 LSTM((rnn): LSTM(57, 128)(linear): Linear(in_features=128, out_features=18, bias=True)(softmax): LogSoftmax(dim=-1)
)
gru 模型 GRU((rnn): GRU(57, 128)(linear): Linear(in_features=128, out_features=18, bias=True)(softmax): LogSoftmax(dim=-1)
)
rnn output.shape--->: torch.Size([1, 18]) tensor([[-2.9552, -2.9024, -2.8828, -2.7737, -2.8387, -3.0154, -2.8587, -2.9567,-2.8406, -3.0098, -2.8152, -2.8472, -2.9561, -2.8780, -2.8332, -2.8117,-2.9560, -2.9384]], grad_fn=<LogSoftmaxBackward0>)
lstm output.shape--->: torch.Size([1, 18]) tensor([[-2.9283, -3.0017, -2.8902, -2.8179, -2.8484, -2.8152, -2.9654, -2.8846,-2.8642, -2.8602, -2.8860, -2.9505, -2.8806, -2.9436, -2.8388, -2.9312,-2.9241, -2.8211]], grad_fn=<LogSoftmaxBackward0>)
gru output.shape--->: torch.Size([1, 18]) tensor([[-2.8898, -3.0236, -2.7403, -2.8986, -2.8163, -2.9486, -2.8674, -2.9294,-2.8889, -3.0082, -2.8785, -2.8741, -2.8736, -2.7923, -2.9261, -2.8990,-2.9456, -2.8668]], grad_fn=<LogSoftmaxBackward0>)
构建训练函数并进行训练
构建RNN训练函数
# 思路分析
# 从文件获取数据、实例化数据源对象nameclassdataset 数据迭代器对象mydataloader
# 实例化模型对象my_rnn 损失函数对象mycrossentropyloss=nn.NLLLoss() 优化器对象myadam
# 定义模型训练的参数
# starttime total_iter_num total_loss total_loss_list total_acc_num total_acc_list
# 外层for循环 控制轮数 for epoch_idx in range(epochs)
# 内层for循环 控制迭代次数 for i, (x, y) in enumerate(mydataloader)# 给模型喂数据 # 计算损失 # 梯度清零 # 反向传播 # 梯度更新# 计算辅助信息 # 累加总损失和准确数 每100次训练计算一个总体平均损失 总体平均准确率 每2000次训练 打印日志# 其他 # 预测对错 i_predit_tag = (1 if torch.argmax(output).item() == y.item() else 0)# 模型保存# torch.save(my_rnn.state_dict(), './my_rnn_model_%d.bin' % (epoch_idx + 1))
# 返回 平均损失列表total_loss_list, 时间total_time, 平均准确total_acc_list
# 模型训练参数
mylr = 1e-3
epochs = 1def 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 = 57n_hidden = 128output_size = 18my_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):# 实例化dataloadermydataloader = 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 + 1total_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_numtotal_loss_list.append(tmploss)tmpacc = total_acc_num/total_iter_numtotal_acc_list.append(tmpacc)# 每2000次训练 打印日志if (total_iter_num % 2000 == 0):tmploss = total_loss / total_iter_numprint('轮次:%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训练函数
# 思路分析
# 同RNN实现分析
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 = 57n_hidden = 128output_size = 18my_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):# 实例化dataloadermydataloader = 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 + 1total_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_numtotal_loss_list.append(tmploss)tmpacc = total_acc_num/total_iter_numtotal_acc_list.append(tmpacc)# 每2000次训练 打印日志if (total_iter_num % 2000 == 0):tmploss = total_loss / total_iter_numprint('轮次:%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训练函数
# 思路分析
# 同RNN实现分析
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 = 57n_hidden = 128output_size = 18my_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):# 实例化dataloadermydataloader = 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 + 1total_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_numtotal_loss_list.append(tmploss)tmpacc = total_acc_num/total_iter_numtotal_acc_list.append(tmpacc)# 每2000次训练 打印日志if (total_iter_num % 2000 == 0):tmploss = total_loss / total_iter_numprint('轮次:%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
模型训练并制图
def dm_test_train_rnn_lstm_gru():total_loss_list_rnn, total_time_rnn, total_acc_list_rnn = my_train_rnn()total_loss_list_lstm, total_time_lstm, total_acc_list_lstm = my_train_lstm()total_loss_list_gru, total_time_gru, total_acc_list_gru = my_train_gru()# 绘制损失对比曲线# 创建画布0plt.figure(0)# # 绘制损失对比曲线plt.plot(total_loss_list_rnn, label="RNN")plt.plot(total_loss_list_lstm, color="red", label="LSTM")plt.plot(total_loss_list_gru, color="orange", label="GRU")plt.legend(loc='upper left')plt.savefig('./img/RNN_LSTM_GRU_loss2.png')plt.show()# 绘制柱状图# 创建画布1plt.figure(1)x_data = ["RNN", "LSTM", "GRU"]y_data = [total_time_rnn, total_time_lstm, total_time_gru]# 绘制训练耗时对比柱状图plt.bar(range(len(x_data)), y_data, tick_label=x_data)plt.savefig('./img/RNN_LSTM_GRU_period2.png')plt.show()# 绘制准确率对比曲线plt.figure(2)plt.plot(total_acc_list_rnn, label="RNN")plt.plot(total_acc_list_lstm, color="red", label="LSTM")plt.plot(total_acc_list_gru, color="orange", label="GRU")plt.legend(loc='upper left')plt.savefig('./img/RNN_LSTM_GRU_acc2.png')plt.show()
- RNN模型训练日志输出:
轮次:3, 损失:1.002102, 时间:54,准确率:0.700
轮次:3, 损失:0.993880, 时间:56,准确率:0.703
轮次:3, 损失:0.986200, 时间:58,准确率:0.705
轮次:3, 损失:0.981136, 时间:61,准确率:0.706
轮次:3, 损失:0.976931, 时间:63,准确率:0.707
轮次:3, 损失:0.972190, 时间:65,准确率:0.708
轮次:3, 损失:0.967081, 时间:68,准确率:0.710
轮次:3, 损失:0.964384, 时间:70,准确率:0.711
轮次:4, 损失:0.958782, 时间:72,准确率:0.713
轮次:4, 损失:0.955343, 时间:75,准确率:0.713
轮次:4, 损失:0.950741, 时间:77,准确率:0.715
轮次:4, 损失:0.945756, 时间:80,准确率:0.716
轮次:4, 损失:0.942663, 时间:82,准确率:0.717
轮次:4, 损失:0.939319, 时间:84,准确率:0.718
轮次:4, 损失:0.936169, 时间:87,准确率:0.719
轮次:4, 损失:0.933440, 时间:89,准确率:0.720
轮次:4, 损失:0.930918, 时间:91,准确率:0.720
轮次:4, 损失:0.927330, 时间:94,准确率:0.721
- LSTM模型训练日志输出:
轮次:3, 损失:0.805885, 时间:118,准确率:0.759
轮次:3, 损失:0.794148, 时间:123,准确率:0.762
轮次:3, 损失:0.783356, 时间:128,准确率:0.765
轮次:3, 损失:0.774931, 时间:133,准确率:0.767
轮次:3, 损失:0.765427, 时间:137,准确率:0.769
轮次:3, 损失:0.757254, 时间:142,准确率:0.771
轮次:3, 损失:0.750375, 时间:147,准确率:0.773
轮次:3, 损失:0.743092, 时间:152,准确率:0.775
轮次:4, 损失:0.732983, 时间:157,准确率:0.778
轮次:4, 损失:0.723816, 时间:162,准确率:0.780
轮次:4, 损失:0.716507, 时间:167,准确率:0.782
轮次:4, 损失:0.708377, 时间:172,准确率:0.785
轮次:4, 损失:0.700820, 时间:177,准确率:0.787
轮次:4, 损失:0.694714, 时间:182,准确率:0.788
轮次:4, 损失:0.688386, 时间:187,准确率:0.790
轮次:4, 损失:0.683056, 时间:191,准确率:0.791
轮次:4, 损失:0.677051, 时间:196,准确率:0.793
轮次:4, 损失:0.671668, 时间:201,准确率:0.794
- GRU模型训练日志输出:
轮次:3, 损失:0.743891, 时间:106,准确率:0.772
轮次:3, 损失:0.733144, 时间:111,准确率:0.775
轮次:3, 损失:0.723484, 时间:116,准确率:0.777
轮次:3, 损失:0.714760, 时间:120,准确率:0.780
轮次:3, 损失:0.706929, 时间:125,准确率:0.782
轮次:3, 损失:0.698657, 时间:130,准确率:0.784
轮次:3, 损失:0.690443, 时间:134,准确率:0.787
轮次:3, 损失:0.683878, 时间:139,准确率:0.789
轮次:4, 损失:0.674766, 时间:144,准确率:0.791
轮次:4, 损失:0.665543, 时间:148,准确率:0.794
轮次:4, 损失:0.657179, 时间:153,准确率:0.796
轮次:4, 损失:0.650314, 时间:157,准确率:0.798
轮次:4, 损失:0.643698, 时间:162,准确率:0.800
轮次:4, 损失:0.637341, 时间:167,准确率:0.802
轮次:4, 损失:0.632063, 时间:171,准确率:0.803
轮次:4, 损失:0.626060, 时间:176,准确率:0.805
轮次:4, 损失:0.621460, 时间:180,准确率:0.806
轮次:4, 损失:0.616704, 时间:185,准确率:0.808
模型训练结果分析
损失对比曲线分析
- 左图:1个轮次损失对比曲线,右图4个轮次损失对比曲线
- 模型训练的损失降低快慢代表模型收敛程度。由图可知, 传统RNN的模型第一个轮次开始收敛情况最好,然后是GRU, 最后是LSTM, 这是因为RNN模型简单参数少,见效快。随着训练数据的增加,GRU效果最好、LSTM效果次之、RNN效果排最后。
- 所以在以后的模型选用时, 要通过对任务的分析以及实验对比, 选择最适合的模型。
训练耗时分析
训练耗时对比图:
- 模型训练的耗时长短代表模型的计算复杂度,由图可知, 也正如我们之前的理论分析,传统RNN复杂度最低, 耗时几乎只是后两者的一半, 然后是GRU,最后是复杂度最高的LSTM。
训练准确率分析
训练准确率对比图:
- 由图可知, GRU效果最好、LSTM效果次之、RNN效果排最后。
结论
模型选用一般应通过实验对比,并非越复杂或越先进的模型表现越好,而是需要结合自己的特定任务,从对数据的分析和实验结果中获得最佳答案。
构建预测函数并进行预测
构建RNN预测函数
# 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):# 文本张量化xtensor_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)] = 1return tensor_x
# 思路分析
# 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 = 57n_hidden = 128n_categories = 18# 输入文本, 张量化one-hotx_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 = 57n_hidden = 128n_categories = 18# 输入文本, 张量化one-hotx_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 = 57n_hidden = 128n_categories = 18# 输入文本, 张量化one-hotx_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))
构建RNN_LSTM_GRU预测调用函数
def dm_test_predic_rnn_lstm_gru():# 把三个函数的入口地址 组成列表,统一输入数据进行测试for func in [my_predict_rnn, my_predict_lstm, my_predict_gru]:func('zhang')
- 输出效果
rnn => zhangvalue:0 category:Russianvalue:0 category:Chinesevalue:-4 category:German
rnn => zhangvalue:0 category:Chinesevalue:-1 category:Russianvalue:-1 category:German
rnn => zhangvalue:0 category:Russianvalue:0 category:Chinesevalue:-2 category:Korean
完整代码
# 导包import json
import torch
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
from tqdm import tqdm# 1.todo: 获取常用的字符数量
# 此次将人名变成向量的过程:将人名中的每个字母(字符)进行one-hot张量表示,然后拼接代表整个人名的向量表示
# 因为人名的组成大部分都是由大小写英文字母以及某些特殊的字符组成,这里一个展示是57个,其实就是one-hot编码的维度
all_letters = string.ascii_letters + " .,;'"
n_letters = len(all_letters)# 2 todo: 获取国家的类别个数
# 国家名 种类数
categorys = ['Italian', 'English', 'Arabic', 'Spanish', 'Scottish', 'Irish', 'Chinese', 'Vietnamese', 'Japanese','French', 'Greek', 'Dutch', 'Korean', 'Polish', 'Portuguese', 'Russian', 'Czech', 'German']
# 国家名 个数
categorynum = len(categorys)# 3.todo:读取数据到内存里
def read_data(file_name):# 定义两个空列表,分别存储人名和国家名my_list_x, my_list_y = [], []# 读取数据with open(file_name, mode='r', encoding='utf-8') as fr:for line in fr.readlines():# 数据清洗if len(line) <= 5:continuex, y = line.strip().split("\t")my_list_x.append(x)my_list_y.append(y)return my_list_x, my_list_y# 4.todo 构建dataset类
class NameClassDataset(Dataset):def __init__(self, my_list_x, my_list_y):# 获取样本self.my_list_x = my_list_x# 获取样本对应的标签self.my_list_y = my_list_y# 获取样本长度self.sample_len = len(my_list_x)def __len__(self):return self.sample_lendef __getitem__(self, item):# item为索引index = min(max(item, 0), self.sample_len - 1)# 工具索引来取出对应的x和yx = self.my_list_x[index]y = self.my_list_y[index]# 初始化全零的一个张量tensor_x = torch.zeros(len(x), n_letters)# 遍历人名的每个字母变成one-hot编码# enumerate(x) 是 Python 的内置函数,用于在遍历可迭代对象(如字符串、列表等)时,同时获取元素的索引和值for idx, letter in enumerate(x):tensor_x[idx][all_letters.find(letter)] = 1tensor_y = torch.tensor(categorys.index(y), dtype=torch.long)return tensor_x, tensor_y# 5.todo 实例化Dataloader
def get_dataloader():my_list_x, my_list_y = read_data(file_name="./data/name_classfication.txt")# 这里传入两个列表:由于NameClassDataset的类中init方法定义,实例化类的时候需要传入两个列表形参# 仅需在实例化时传入 init 的参数,其他方法的参数在调用时传入nameclass_dataset = NameClassDataset(my_list_x, my_list_y)# 实例化Dataloadertrain_dataloader = DataLoader(nameclass_dataset, batch_size=1, shuffle=True)# for tensor_x, tensor_y in train_dataloader:# print(f"tensor_x:{tensor_x}")# print(f"tensor_y:{tensor_y}")# breakreturn train_dataloader# 6.todo 定义RNN模型
class NameRNN(nn.Module):def __init__(self, input_size, hidden_size, output_size, num_layers=1):super().__init__()# input_size:输入x单词的词嵌入维度self.input_size = input_size# hidden_size:RNN输出的隐藏层维度self.hidden_size = hidden_size# output_size:输出的国家类别个数self.output_size = output_size# num_layers:几层隐藏层self.num_layers = num_layers# 实例化RNN对象# batch_first = False,默认self.rnn = nn.RNN(input_size, hidden_size, num_layers)# 定义输出层self.out = nn.Linear(hidden_size, output_size)# 定义logsoftmax层self.softmax = nn.LogSoftmax(dim=-1)def forward(self, input_x, hidden):# input_x按照讲义,输入的时候,是一个二维的[seq_len, input_size]-->[6,57]# hidden:初始化隐藏层的值[1,1,128]# 升维度,在dim=1的维度进行升维:input_x-->[6,1,57]input_x = input_x.unsqueeze(dim=1)# 将input_x和hidden送入rnn:rnn_output的形状为[6,1,57];hn的形状为[1,1,128]rnn_output, hn = self.rnn(input_x, hidden)# 将上述rnn的结果经过输出层;rnn_output[-1]获取最后一个单词的词向量代表整个句子的语义# temp_vec-->[1,128]temp_vec = rnn_output[-1]# 将temp_vec送入输出层:result-->[1,18]即(batch_size=1,categorynum=18)# temp_vec的形状是[1, 128],而全连接层的权重矩阵是[128, 18]。矩阵相乘的结果是[1, 18],这正是result的形状。result = self.out(temp_vec)return self.softmax(result), hndef inithidden(self):return torch.zeros(self.num_layers, 1, self.hidden_size)# 7.todo 定义LSTM模型
class NameLSTM(nn.Module):def __init__(self, input_size, hidden_size, output_size, num_layers=1):super().__init__()# input_size-->输入x单词的词嵌入维度self.input_size = input_size# hidden_size-->RNN输出的隐藏层维度self.hidden_size = hidden_size# output_size-->国家类别的总个数self.output_size = output_size# num_layers:几层隐藏层self.num_layers = num_layers# 实例化对象# batch_first = False,默认self.lstm = nn.LSTM(input_size, hidden_size, num_layers)# 定义输出层self.out = nn.Linear(hidden_size, output_size)# 定义logsoftmax层self.softmax = nn.LogSoftmax(dim=-1)def forward(self, input_x, h0, c0):input_x = input_x.unsqueeze(dim=1)lstm_output, (hn, cn) = self.lstm(input_x, (h0, c0))temp_vec = lstm_output[-1]result = self.out(temp_vec)return self.softmax(result), hn, cndef inithidden(self):h0 = torch.zeros(self.num_layers, 1, self.hidden_size)c0 = torch.zeros(self.num_layers, 1, self.hidden_size)return h0, c0# 8.todo gru模型定义
class NameGRU(nn.Module):def __init__(self, input_size, hidden_size, output_size, num_layers=1):super().__init__()# input_size:输入x单词的词嵌入维度self.input_size = input_size# hidden_size:RNN输出的隐藏层维度self.hidden_size = hidden_size# output_size:输出的国家类别个数self.output_size = output_size# num_layers:几层隐藏层self.num_layers = num_layers# 实例化RNN对象# batch_first = False,默认self.gru = nn.GRU(input_size, hidden_size, num_layers)# 定义输出层self.out = nn.Linear(hidden_size, output_size)# 定义logsoftmax层self.softmax = nn.LogSoftmax(dim=-1)def forward(self, input_x, hidden):# input_x按照讲义,输入的时候,是一个二维的[seq_len, input_size]-->[6,57]# hidden:初始化隐藏层的值[1,1,128]# 升维度,在dim=1的维度进行升维:input_x-->[6,1,57]input_x = input_x.unsqueeze(dim=1)# 将input_x和hidden送入rnn:rnn_output的形状为[6,1,57];hn的形状为[1,1,128]gru_output, hn = self.gru(input_x, hidden)# 将上述rnn的结果经过输出层;rnn_output[-1]获取最后一个单词的词向量代表整个句子的语义# temp_vec-->[1,128]temp_vec = gru_output[-1]# 将temp_vec送入输出层:result-->[1,18]即(batch_size=1,categorynum=18)# temp_vec的形状是[1, 128],而全连接层的权重矩阵是[128, 18]。矩阵相乘的结果是[1, 18],这正是result的形状。result = self.out(temp_vec)return self.softmax(result), hndef inithidden(self):return torch.zeros(self.num_layers, 1, self.hidden_size)my_lr = 0.001
epochs = 1# 9. todo: 定义RNN模型的训练函数def train_rnn():# 1.读取数据my_list_x, my_list_y = read_data(file_name="./data/name_classfication.txt")# 2.实例化Dataset对象name_dataset = NameClassDataset(my_list_x, my_list_y)# 3.实例化自定义的RNN模型对象input_size = n_lettershidden_size = 128output_size = categorynumname_rnn = NameRNN(input_size, hidden_size, output_size)# 4.实例化损失函数对象以及优化器对象rnn_nllloss = nn.NLLLoss()rnn_adam = optim.Adam(name_rnn.parameters(), lr=my_lr)# 5.定义训练日志的参数start_time = time.time()total_iter_num = 0 # 已经训练的样本的总数total_loss = 0.0 # 已经训练的样本的总损失total_loss_list = [] # 每隔100个样本我们计算一下平均损失,并保存total_acc_num = 0 # 已经训练的样本中预测正确的个数total_acc_list = [] # 每隔100个样本我们计算一下平均准确率,并保存# 6.开始外部迭代for epoch_idx in range(epochs):# 6.1实例化Dataloader的对象train_dataloader = DataLoader(dataset=name_dataset, batch_size=1, shuffle=True)# 6.2遍历迭代器,内部迭代for idx, (tensor_x, tensor_y) in enumerate(tqdm(train_dataloader)):# 6.3准备模型需要的数据tensor_x0 = tensor_x[0]h0 = name_rnn.inithidden()# print(f'tensor_y--》{tensor_y}')# 6.4 将数据送入模型得到预测结果output-->shape-->[1, 18]output, hn = name_rnn(tensor_x0, h0)# print(f'output--》{output}')# 6.5 计算损失my_loss = rnn_nllloss(output, tensor_y)# print(f'my_loss--》{my_loss}')# 6.6 梯度清零rnn_adam.zero_grad()# 6.7 反向传播my_loss.backward()# 6.8 梯度更新rnn_adam.step()# 6.9 统计已经训练好的样本总个数total_iter_num = total_iter_num + 1# 6.10 统计已经训练的样本总损失total_loss = total_loss + my_loss.item()# 6.11 统计已经训练的样本中预测正确的样本总个数# torch.argmax(output)取出预测结果中最大概率值对应的索引predict_id = 1 if torch.argmax(output).item() == tensor_y.item() else 0total_acc_num = total_acc_num + predict_id# 6.12 每间隔100步,保存平均损失已经平均准确率if (total_iter_num % 100) == 0:# 计算平均损失并保存avg_loss = total_loss / total_iter_numtotal_loss_list.append(avg_loss)# 计算平均准确率并保存avg_acc = total_acc_num / total_iter_numtotal_acc_list.append(avg_acc)# 6.13 每间隔2000步,打印日志if (total_iter_num % 2000) == 0:temp_avg_loss = total_loss / total_iter_numtemp_avg_acc = total_acc_num / total_iter_numprint('轮次:%d, 损失:%.6f, 时间:%d,准确率:%.3f' % (epoch_idx + 1, temp_avg_loss, time.time() - start_time, temp_avg_acc))# 7保存模型torch.save(name_rnn.state_dict(), './model/szAI_%d.bin' % (epoch_idx + 1))# 8.计算训练总时间total_time = time.time() - start_time# 9. 将损失列表和准确率列表以及时间保存到字典并存储到文件里rnn_dict = {"total_loss_list": total_loss_list,"total_time": total_time,"total_acc_list": total_acc_list}with open("name_classify_rnn.json", "w", encoding="utf-8") as fw:fw.write(json.dumps(rnn_dict))# 10 todo:定义LSTM模型的训练函数
def train_lstm():# 1.读取txt文档数据my_list_x, my_list_y = read_data(file_name="./data/name_classfication.txt")# 2.实例化Dataset对象name_dataset = NameClassDataset(my_list_x, my_list_y)# 3.实例化自定义的RNN模型对象input_size = n_letters # 57hidden_size = 128output_size = categorynum # 18name_lstm = NameLSTM(input_size, hidden_size, output_size)# 4.实例化损失函数对象以及优化器对象rnn_nllloss = nn.NLLLoss()rnn_adam = optim.Adam(name_lstm.parameters(), lr=my_lr)# 5.定义训练日志的参数start_time = time.time()total_iter_num = 0 # 已经训练的样本的总数total_loss = 0.0 # 已经训练的样本的总损失total_loss_list = [] # 每隔100个样本我们计算一下平均损失,并保存total_acc_num = 0 # 已经训练的样本中预测正确的个数total_acc_list = [] # 每隔100个样本我们计算一下平均准确率,并保存# 6. 开始外部迭代for epoch_idx in range(epochs):# 6.1 实例化Dataloader的对象train_dataloader = DataLoader(dataset=name_dataset, batch_size=1, shuffle=True)# 6.2 开始遍历迭代器,内部迭代for idx, (tensor_x, tensor_y) in enumerate(tqdm(train_dataloader)):# 6.3 准备模型需要的数据tensor_x0 = tensor_x[0] # [seq_length, input_size]h0, c0 = name_lstm.inithidden()# print(f'tensor_y--》{tensor_y}')# 6.4 将数据送入模型得到预测结果output-->shape-->[1, 18]output, hn, cn = name_lstm(tensor_x0, h0, c0)# print(f'output--》{output}')# 6.5 计算损失my_loss = rnn_nllloss(output, tensor_y)# print(f'my_loss--》{my_loss}')# 6.6 梯度清零rnn_adam.zero_grad()# 6.7 反向传播my_loss.backward()# 6.8 梯度更新rnn_adam.step()# 6.9 统计已经训练的样本的总个数total_iter_num = total_iter_num + 1# 6.10 统计已经训练的样本总损失total_loss = total_loss + my_loss.item()# 6.11 统计已经训练的样本中预测正确的样本总个数# torch.argmax(output)取出预测结果中最大概率值对应的索引predict_id = 1 if torch.argmax(output).item() == tensor_y.item() else 0total_acc_num = total_acc_num + predict_id# 6.12 每间隔100步,保存平均损失已经平均准确率if (total_iter_num % 100) == 0:# 计算平均损失并保存avg_loss = total_loss / total_iter_numtotal_loss_list.append(avg_loss)# 计算平均准确率并保存avg_acc = total_acc_num / total_iter_numtotal_acc_list.append(avg_acc)# 6.13 每间隔2000步,打印日志if (total_iter_num % 2000) == 0:temp_avg_loss = total_loss / total_iter_numtemp_avg_acc = total_acc_num / total_iter_numprint('轮次:%d, 损失:%.6f, 时间:%d,准确率:%.3f' % (epoch_idx + 1, temp_avg_loss, time.time() - start_time, temp_avg_acc))# 7保存模型torch.save(name_lstm.state_dict(), './model/szAI_lstm_%d.bin' % (epoch_idx + 1))# 8. 计算训练的总时间total_time = time.time() - start_time# 9. 将损失列表和准确率列表以及时间保存到字典并存储到文件里lstm_dict = {"total_loss_list": total_loss_list,"total_time": total_time,"total_acc_list": total_acc_list}with open('name_classify_lstm.json', 'w', encoding='utf-8') as fw:fw.write(json.dumps(lstm_dict))# 9. todo: 定义RNN模型的训练函数def train_rnn():# 1.读取数据my_list_x, my_list_y = read_data(file_name="./data/name_classfication.txt")# 2.实例化Dataset对象name_dataset = NameClassDataset(my_list_x, my_list_y)# 3.实例化自定义的RNN模型对象input_size = n_lettershidden_size = 128output_size = categorynumname_rnn = NameRNN(input_size, hidden_size, output_size)# 4.实例化损失函数对象以及优化器对象rnn_nllloss = nn.NLLLoss()rnn_adam = optim.Adam(name_rnn.parameters(), lr=my_lr)# 5.定义训练日志的参数start_time = time.time()total_iter_num = 0 # 已经训练的样本的总数total_loss = 0.0 # 已经训练的样本的总损失total_loss_list = [] # 每隔100个样本我们计算一下平均损失,并保存total_acc_num = 0 # 已经训练的样本中预测正确的个数total_acc_list = [] # 每隔100个样本我们计算一下平均准确率,并保存# 6.开始外部迭代for epoch_idx in range(epochs):# 6.1实例化Dataloader的对象train_dataloader = DataLoader(dataset=name_dataset, batch_size=1, shuffle=True)# 6.2遍历迭代器,内部迭代for idx, (tensor_x, tensor_y) in enumerate(tqdm(train_dataloader)):# 6.3准备模型需要的数据tensor_x0 = tensor_x[0]h0 = name_rnn.inithidden()# print(f'tensor_y--》{tensor_y}')# 6.4 将数据送入模型得到预测结果output-->shape-->[1, 18]output, hn = name_rnn(tensor_x0, h0)# print(f'output--》{output}')# 6.5 计算损失my_loss = rnn_nllloss(output, tensor_y)# print(f'my_loss--》{my_loss}')# 6.6 梯度清零rnn_adam.zero_grad()# 6.7 反向传播my_loss.backward()# 6.8 梯度更新rnn_adam.step()# 6.9 统计已经训练好的样本总个数total_iter_num = total_iter_num + 1# 6.10 统计已经训练的样本总损失total_loss = total_loss + my_loss.item()# 6.11 统计已经训练的样本中预测正确的样本总个数# torch.argmax(output)取出预测结果中最大概率值对应的索引predict_id = 1 if torch.argmax(output).item() == tensor_y.item() else 0total_acc_num = total_acc_num + predict_id# 6.12 每间隔100步,保存平均损失已经平均准确率if (total_iter_num % 100) == 0:# 计算平均损失并保存avg_loss = total_loss / total_iter_numtotal_loss_list.append(avg_loss)# 计算平均准确率并保存avg_acc = total_acc_num / total_iter_numtotal_acc_list.append(avg_acc)# 6.13 每间隔2000步,打印日志if (total_iter_num % 2000) == 0:temp_avg_loss = total_loss / total_iter_numtemp_avg_acc = total_acc_num / total_iter_numprint('轮次:%d, 损失:%.6f, 时间:%d,准确率:%.3f' % (epoch_idx + 1, temp_avg_loss, time.time() - start_time, temp_avg_acc))# 7保存模型torch.save(name_rnn.state_dict(), './model/szAI_%d.bin' % (epoch_idx + 1))# 8.计算训练总时间total_time = time.time() - start_time# 9. 将损失列表和准确率列表以及时间保存到字典并存储到文件里rnn_dict = {"total_loss_list": total_loss_list,"total_time": total_time,"total_acc_list": total_acc_list}with open("name_classify_rnn.json", "w", encoding="utf-8") as fw:fw.write(json.dumps(rnn_dict))# 11. todo: 定义GRU模型的训练函数def train_gru():# 1.读取数据my_list_x, my_list_y = read_data(file_name="./data/name_classfication.txt")# 2.实例化Dataset对象name_dataset = NameClassDataset(my_list_x, my_list_y)# 3.实例化自定义的RNN模型对象input_size = n_lettershidden_size = 128output_size = categorynumname_rnn = NameGRU(input_size, hidden_size, output_size)# 4.实例化损失函数对象以及优化器对象rnn_nllloss = nn.NLLLoss()rnn_adam = optim.Adam(name_rnn.parameters(), lr=my_lr)# 5.定义训练日志的参数start_time = time.time()total_iter_num = 0 # 已经训练的样本的总数total_loss = 0.0 # 已经训练的样本的总损失total_loss_list = [] # 每隔100个样本我们计算一下平均损失,并保存total_acc_num = 0 # 已经训练的样本中预测正确的个数total_acc_list = [] # 每隔100个样本我们计算一下平均准确率,并保存# 6.开始外部迭代for epoch_idx in range(epochs):# 6.1实例化Dataloader的对象train_dataloader = DataLoader(dataset=name_dataset, batch_size=1, shuffle=True)# 6.2遍历迭代器,内部迭代for idx, (tensor_x, tensor_y) in enumerate(tqdm(train_dataloader)):# 6.3准备模型需要的数据tensor_x0 = tensor_x[0]h0 = name_rnn.inithidden()# print(f'tensor_y--》{tensor_y}')# 6.4 将数据送入模型得到预测结果output-->shape-->[1, 18]output, hn = name_rnn(tensor_x0, h0)# print(f'output--》{output}')# 6.5 计算损失my_loss = rnn_nllloss(output, tensor_y)# print(f'my_loss--》{my_loss}')# 6.6 梯度清零rnn_adam.zero_grad()# 6.7 反向传播my_loss.backward()# 6.8 梯度更新rnn_adam.step()# 6.9 统计已经训练好的样本总个数total_iter_num = total_iter_num + 1# 6.10 统计已经训练的样本总损失total_loss = total_loss + my_loss.item()# 6.11 统计已经训练的样本中预测正确的样本总个数# torch.argmax(output)取出预测结果中最大概率值对应的索引predict_id = 1 if torch.argmax(output).item() == tensor_y.item() else 0total_acc_num = total_acc_num + predict_id# 6.12 每间隔100步,保存平均损失已经平均准确率if (total_iter_num % 100) == 0:# 计算平均损失并保存avg_loss = total_loss / total_iter_numtotal_loss_list.append(avg_loss)# 计算平均准确率并保存avg_acc = total_acc_num / total_iter_numtotal_acc_list.append(avg_acc)# 6.13 每间隔2000步,打印日志if (total_iter_num % 2000) == 0:temp_avg_loss = total_loss / total_iter_numtemp_avg_acc = total_acc_num / total_iter_numprint('轮次:%d, 损失:%.6f, 时间:%d,准确率:%.3f' % (epoch_idx + 1, temp_avg_loss, time.time() - start_time, temp_avg_acc))# 7保存模型torch.save(name_rnn.state_dict(), './model/szAI_gru%d.bin' % (epoch_idx + 1))# 8.计算训练总时间total_time = time.time() - start_time# 9. 将损失列表和准确率列表以及时间保存到字典并存储到文件里rnn_dict = {"total_loss_list": total_loss_list,"total_time": total_time,"total_acc_list": total_acc_list}with open("name_classify_gru.json", "w", encoding="utf-8") as fw:fw.write(json.dumps(rnn_dict))# 12 todo: 对比不同模型训练的结果
def compare_results():# 1.读取rnn模型训练的结果with open('name_classify_rnn.json', 'r') as fr:rnn_result = fr.read()rnn_dict = json.loads(rnn_result)# print(f'rnn_dict["total_loss_list"]-->{rnn_dict["total_loss_list"]}')# print(f'rnn_dict["total_time"]-->{rnn_dict["total_time"]}')# print(f'rnn_dict["total_acc_list"]-->{rnn_dict["total_acc_list"]}')# 2.读取lstm模型训练的结果with open('name_classify_lstm.json', 'r') as fr:lstm_result = fr.read()lstm_dict = json.loads(lstm_result)# 3.读取gru模型训练的结果with open('name_classify_gru.json', 'r') as fr:gru_result = fr.read()gru_dict = json.loads(gru_result)# 4. 对比不同模型损失结果plt.figure(0)plt.plot(rnn_dict["total_loss_list"], label="RNN")plt.plot(lstm_dict["total_loss_list"], color='red', label="LSTM")plt.plot(gru_dict["total_loss_list"], color='orange', label="GRU")plt.legend(loc='upper left')plt.savefig('name_classify_loss.png')plt.show()# 5.对比不同模型时间结果plt.figure(1)x_data = ["RNN", "LSTM", "GRU"]y_data = [rnn_dict["total_time"], lstm_dict["total_time"], gru_dict["total_time"]]plt.bar(range(len(x_data)), y_data, tick_label=x_data)plt.savefig('name_classify_time.png')plt.show()# 6. 对比不同模型准确率结果plt.figure(0)plt.plot(rnn_dict["total_acc_list"], label="RNN")plt.plot(lstm_dict["total_acc_list"], color='red', label="LSTM")plt.plot(gru_dict["total_acc_list"], color='orange', label="GRU")plt.legend(loc='upper left')plt.savefig('name_classify_acc.png')plt.show()# 13 todo:实现模型的预测
rnn_model_path = './model/szAI_1.bin'
lstm_model_path = './model/szAI_lstm_1.bin'
gru_model_path = './model/szAI_gru1.bin'# 13.1 定义数据的预处理:将人名转为向量
def line2tensor(x):# x--》代表人名--》"zhang"tensor_x = torch.zeros(len(x), n_letters)# 将上述的tensor_x变成one-hotfor idx, letter in enumerate(x):tensor_x[idx][all_letters.find(letter)] = 1return tensor_x# 13.2 定义rnn模型的预测函数
def rnn2predict(x):tensor_x = line2tensor(x)input_size = n_lettershidden_size = 128output_size = categorynummy_rnn = NameRNN(input_size, hidden_size, output_size)my_rnn.load_state_dict(torch.load(rnn_model_path))with torch.no_grad():output, hn = my_rnn(tensor_x, my_rnn.inithidden())print(f'output--》{output}')values, indexes = torch.topk(output, k=3, dim=1)print(f'values--》{values}')print(f'indexes--》{indexes}')print('rnn预测的结果=========》')for i in range(3):value = values[0][i]idx = indexes[0][i]predict_label = categorys[idx]print(f"当前的人名是--》{x}, 预测的国家为:{predict_label}")def lstm2predict(x):# 将x人名进行向量的转换tensor_x = line2tensor(x)# 实例化模型input_size = n_letters # 57hidden_size = 128output_size = categorynum # 18my_rnn = NameLSTM(input_size, hidden_size, output_size)# 加载训练好的模型的参数my_rnn.load_state_dict(torch.load(lstm_model_path))# 将数据送入模型得到预测结果with torch.no_grad():h0, c0 = my_rnn.inithidden()output, hn, cn = my_rnn(tensor_x, h0, c0)# print(f'output--》{output}')# 取出预测结果的前三个最大的值以及对应的索引values, indexes = torch.topk(output, k=3, dim=1)# print(f'values--》{values}')# print(f'indexes--》{indexes}')print('lstm预测的结果=========》')for i in range(3):value = values[0][i]idx = indexes[0][i]predict_label = categorys[idx]print(f'当前的人名是--》{x}, 预测的国家为:{predict_label}')# 13.4 定义gru模型的预测函数def gru2predict(x):# 将x人名进行向量的转换tensor_x = line2tensor(x)# 实例化模型input_size = n_letters # 57hidden_size = 128output_size = categorynum # 18my_rnn = NameGRU(input_size, hidden_size, output_size)# 加载训练好的模型的参数my_rnn.load_state_dict(torch.load(gru_model_path))# 将数据送入模型得到预测结果with torch.no_grad():output, hn = my_rnn(tensor_x, my_rnn.inithidden())# print(f'output--》{output}')# 取出预测结果的前三个最大的值以及对应的索引values, indexes = torch.topk(output, k=3, dim=1)# print(f'values--》{values}')# print(f'indexes--》{indexes}')print('gru预测的结果=========》')for i in range(3):value = values[0][i]idx = indexes[0][i]predict_label = categorys[idx]print(f'当前的人名是--》{x}, 预测的国家为:{predict_label}')if __name__ == '__main__':# train_rnn()# train_lstm()# train_gru()# compare_results()# print(line2tensor(x="zhang"))rnn2predict(x="zhang")print("--------------------------------------------------------------")lstm2predict(x="zhang")print("--------------------------------------------------------------")gru2predict(x="zhang")
注意力机制
注意力机制的由来,解决了什么问题?
- 在认识注意力之前,我们先简单了解下机器翻译任务:
例子:seq2seq(Sequence to Sequence))架构翻译任务
- seq2seq模型架构包括三部分,分别是encoder(编码器)、decoder(解码器)、中间语义张量c。
- 图中表示的是一个中文到英文的翻译:欢迎 来 北京 → welcome to BeiJing。编码器首先处理中文输入"欢迎 来 北京",通过GRU模型获得每个时间步的输出张量,最后将它们拼接成一个中间语义张量c;接着解码器将使用这个中间语义张量c以及每一个时间步的隐层张量, 逐个生成对应的翻译语言
- 早期在解决机器翻译这一类seq2seq问题时,通常采用的做法是利用一个编码器(Encoder)和一个解码器(Decoder)构建端到端的神经网络模型,但是基于编码解码的神经网络存在两个问题:
- 问题1:如果翻译的句子很长很复杂,比如直接一篇文章输进去,模型的计算量很大,并且模型的准确率下降严重。
- 问题2:在翻译时,可能在不同的语境下,同一个词具有不同的含义,但是网络对这些词向量并没有区分度,没有考虑词与词之间的相关性,导致翻译效果比较差。
- 针对这样的问题,注意力机制被提出。
什么是注意力机制
- 注意力机制早在上世纪九十年代就有研究,最早注意力机制应用在视觉领域,后来伴随着2017年Transformer模型结构的提出,注意力机制在NLP,CV相关问题的模型网络设计上被广泛应用。“注意力机制”实际上就是想将人的感知方式、注意力的行为应用在机器上,让机器学会去感知数据中的重要和不重要的部分。
- 举例说明:当我们看到下面这张图时,短时间内大脑可能只对图片中的“锦江饭店”有印象,即注意力集中在了“锦江饭店”处。短时间内,大脑可能并没有注意到锦江饭店上面有一串电话号码,下面有几个行人,后面还有“喜运来大酒家”等信息。
- 所以,大脑在短时间内处理信息时,主要将图片中最吸引人注意力的部分读出来了,大脑注意力只关注吸引人的部分, 类似下图所示。
- 同样的如果我们在机器翻译中,我们要让机器注意到每个词向量之间的相关性,有侧重地进行翻译,模拟人类理解的过程。
注意力机制分类以及如何实现
- 通俗来讲就是对于模型的每一个输入项,可能是图片中的不同部分,或者是语句中的某个单词分配一个权重,这个权重的大小就代表了我们希望模型对该部分一个关注程度。这样一来,通过权重大小来模拟人在处理信息的注意力的侧重,有效的提高了模型的性能,并且一定程度上降低了计算量。
- 深度学习中的注意力机制通常可分为三类: 软注意(全局注意)、硬注意(局部注意)和自注意(内注意)
- 软注意机制(Soft/Global Attention: 对每个输入项的分配的权重为0-1之间,也就是某些部分关注的多一点,某些部分关注的少一点,因为对大部分信息都有考虑,但考虑程度不一样,所以相对来说计算量比较大。
- 硬注意机制(Hard/Local Attention,[了解即可]): 对每个输入项分配的权重非0即1,和软注意不同,硬注意机制只考虑那部分需要关注,哪部分不关注,也就是直接舍弃掉一些不相关项。优势在于可以减少一定的时间和计算成本,但有可能丢失掉一些本应该注意的信息。
- 自注意力机制( Self/Intra Attention): 对每个输入项分配的权重取决于输入项之间的相互作用,即通过输入项内部的"表决"来决定应该关注哪些输入项。和前两种相比,在处理很长的输入时,具有并行计算的优势。
Soft Attention (最常见)
- 需要注意:注意力机制是一种通用的思想和技术,不依赖于任何模型,换句话说,注意力机制可以用于任何模型。我们这里只是以文本处理领域的Encoder-Decoder框架为例进行理解。这里我们分别以普通Encoder-Decoder框架以及加Attention的Encoder-Decoder框架分别做对比。
普通Encoder-Decoder框架
下图1是Encoder-Decoder框架的一种抽象表示方式:
上图图例可以把它看作由一个句子(或篇章)生成另外一个句子(或篇章)的通用处理模型。对于句子对,我们的目标是给定输入句子Source,期待通过Encoder-Decoder框架来生成目标句子Target。Source和Target可以是同一种语言,也可以是两种不同的语言。而Source和Target分别由各自的单词序列构成:
encoder顾名思义就是对输入句子Source进行编码,将输入句子通过非线性变换转化为中间语义表示C:
对于解码器Decoder来说,其任务是根据句子Source的中间语义表示C和之前已经生成的历史信息,y_1, y_2…y_i-1来生成i时刻要生成的单词y_i 。
上述图中展示的Encoder-Decoder框架是没有体现出“注意力模型”的,所以可以把它看作是注意力不集中的分心模型。为什么说它注意力不集中呢?请观察下目标句子Target中每个单词的生成过程如下:
其中f是Decoder的非线性变换函数。从这里可以看出,在生成目标句子的单词时,不论生成哪个单词,它们使用的输入句子Source的语义编码C都是一样的,没有任何区别。而语义编码C又是通过对source经过Encoder编码产生的,因此对于target中的任何一个单词,source中任意单词对某个目标单词y_i来说影响力都是相同的,这就是为什么说图1中的模型没有体现注意力的原因。
加Attention的Encoder-Decoder框架
- 举例说明,为何添加Attention:
- 比如机器翻译任务,输入source为:Tom chase Jerry,输出target为:“汤姆”,“追逐”,“杰瑞”。在翻译“Jerry”这个中文单词的时候,普通Encoder-Decoder框架中,source里的每个单词对翻译目标单词“杰瑞”贡献是相同的,很明显这里不太合理,显然“Jerry”对于翻译成“杰瑞”更重要。
- 如果引入Attention模型,在生成“杰瑞”的时候,应该体现出英文单词对于翻译当前中文单词不同的影响程度,比如给出类似下面一个概率分布值:(Tom,0.3)(Chase,0.2) (Jerry,0.5).每个英文单词的概率代表了翻译当前单词“杰瑞”时,注意力分配模型分配给不同英文单词的注意力大小。
- 因此,基于上述例子所示, 对于target中任意一个单词都应该有对应的source中的单词的注意力分配概率.而且,由于注意力模型的加入,原来在生成target单词时候的中间语义C就不再是固定的,而是会根据注意力概率变化的C,加入了注意力模型的Encoder-Decoder框架就变成了下图2所示:
即生成目标句子单词的过程成了下面的形式:
而每个Ci可能对应着不同的源语句子单词的注意力分配概率分布,比如对于上面的英汉翻译来说,其对应的信息可能如下:
f2函数代表Encoder对输入英文单词的某种变换函数,比如如果Encoder是用的RNN模型的话,这个f2函数的结果往往是某个时刻输入后隐层节点的状态值;g代表Encoder根据单词的中间表示合成整个句子中间语义表示的变换函数,一般的做法中,g函数就是对构成元素加权求和,即下列公式:
Lx代表输入句子source的长度, a_ij代表在Target输出第i个单词时source输入句子中的第j个单词的注意力分配系数, 而hj则是source输入句子中第j个单词的语义编码, 假设Ci下标i就是上面例子所说的'汤姆', 那么Lx就是3, h1=f('Tom'), h2=f('Chase'),h3=f('jerry')分别输入句子每个单词的语义编码, 对应的注意力模型权值则分别是0.6, 0.2, 0.2, 所以g函数本质上就是加权求和函数, 如果形象表示的话, 翻译中文单词'汤姆'的时候, 数学公式对应的中间语义表示Ci的形成过程类似下图3:
如何得到注意力概率分布
- 为了便于说明,我们假设Encoder-Decoder框架中,Encoder和Decoder都采用RNN模型,如下图4所示:
- 那么注意力分配概率分布值的通用计算过程如下:
- 上图中h_i表示Source中单词j对应的隐层节点状态h_j,H_i表示Target中单词i的隐层节点状态,注意力计算的是Target中单词i对Source中每个单词对齐可能性,即F(h_j,H_i-1),而函数F可以用不同的方法,然后函数F的输出经过softmax进行归一化就得到了注意力分配概率分布。
- 上面就是经典的Soft Attention模型的基本思想,区别只是函数F会有所不同。
Attention机制的本质思想
- 其实Attention机制可以看作,Target中每个单词是对Source每个单词的加权求和,而权重是Source中每个单词对Target中每个单词的重要程度。因此,Attention的本质思想会表示成下图:
- 将Source中的构成元素看作是一系列的数据对,给定Target中的某个元素Query,通过计算Query和各个Key的相似性或者相关性,即权重系数;然后对Value进行加权求和,并得到最终的Attention数值。将本质思想表示成公式如下:
- 深度学习中的注意力机制中提到:Source 中的 Key 和 Value 合二为一,指向的是同一个东西,也即输入句子中每个单词对应的语义编码,所以可能不容易看出这种能够体现本质思想的结构。因此,Attention计算转换为下面3个阶段。
- 输入由三部分构成:Query、Key和Value。其中,(Key, Value)是具有相互关联的KV对,Query是输入的“问题”,Attention可以将Query转化为与Query最相关的向量表示。
- Attention的计算主要分3步,如下图所示。
- Attention 3步计算过程Attention3步计算过程
- 第一步:Query和Key进行相似度计算,得到Attention Score;
- 第二步:对Attention Score进行Softmax归一化,得到权值矩阵;
- 第三步:权重矩阵与Value进行加权求和计算。
- Query、Key和Value的含义是什么呢?我们以刚才大脑读图为例。Value可以理解为人眼视网膜对整张图片信息的原始捕捉,不受“注意力”所影响。我们可以将Value理解为像素级别的信息,那么假设只要一张图片呈现在人眼面前,图片中的像素都会被视网膜捕捉到。Key与Value相关联,Key是图片原始信息所对应的关键性提示信息,比如“锦江饭店”部分是将图片中的原始像素信息抽象为中文文字和牌匾的提示信息。一个中文读者看到这张图片时,读者大脑有意识地向图片获取信息,即发起了一次Query,Query中包含了读者的意图等信息。在一次读图过程中,Query与Key之间计算出Attention Score,得到最具有吸引力的部分,并只对具有吸引力的Value信息进行提取,反馈到大脑中。就像上面的例子中,经过大脑的注意力机制的筛选,一次Query后,大脑只关注“锦江饭店”的牌匾部分。
- 再以一个搜索引擎的检索为例。使用某个Query去搜索引擎里搜索,搜索引擎里面有好多文章,每个文章的全文可以被理解成Value;文章的关键性信息是标题,可以将标题认为是Key。搜索引擎用Query和那些文章们的标题(Key)进行匹配,看看相似度(计算Attention Score)。我们想得到跟Query相关的知识,于是用这些相似度将检索的文章Value做一个加权和,那么就得到了一个新的信息,新的信息融合了相关性强的文章们,而相关性弱的文章可能被过滤掉。
Hard Attention
- 在3.1章节我们使用了一种软性注意力的方式进行Attention机制,它通过注意力分布来加权求和融合各个输入向量。而硬性注意力(Hard Attention)机制则不是采用这种方式,它是根据注意力分布选择输入向量中的一个作为输出。这里有两种选择方式:
- 选择注意力分布中,分数最大的那一项对应的输入向量作为Attention机制的输出。
- 根据注意力分布进行随机采样,采样结果作为Attention机制的输出。
- 硬性注意力通过以上两种方式选择Attention的输出,这会使得最终的损失函数与注意力分布之间的函数关系不可导,导致无法使用反向传播算法训练模型,硬性注意力通常需要使用强化学习来进行训练。因此,一般深度学习算法会使用软性注意力的方式进行计算,
Self Attention
- Self Attention是Google在transformer模型中提出的,上面介绍的都是一般情况下Attention发生在Target元素Query和Source中所有元素之间。而Self Attention,指的是Source内部元素之间或者Target内部元素之间发生的Attention机制,也可以理解为Target=Source这种特殊情况下的注意力机制。当然,具体的计算过程仍然是一样的,只是计算对象发生了变化而已。
- 上面内容也有说到,一般情况下Attention本质上是Target和Source之间的一种单词对齐机制。那么如果是Self Attention机制,到底学的是哪些规律或者抽取了哪些特征呢?或者说引入Self Attention有什么增益或者好处呢?仍然以机器翻译为例来说明, 如下图所示:
- Attention的发展主要经历了两个阶段:
- 从上图中可以看到, self Attention可以远距离的捕捉到语义层面的特征(its的指代对象是Law).
- 应用传统的RNN, LSTM, 在获取长距离语义特征和结构特征的时候, 需要按照序列顺序依次计算, 距离越远的联系信息的损耗越大, 有效提取和捕获的可能性越小.
- 但是应用self-attention时, 计算过程中会直接将句子中任意两个token的联系通过一个计算步骤直接联系起来
注意力机制规则
- 它需要三个指定的输入Q(query), K(key), V(value), 然后通过计算公式得到注意力的结果, 这个结果代表query在key和value作用下的注意力表示. 当输入的Q=K=V时, 称作自注意力计算规则;当Q、K、V不相等时称为一般注意力计算规则,例子:seq2seq架构翻译应用中的Q、K、V解释:
- seq2seq模型架构包括三部分,分别是encoder(编码器)、decoder(解码器)、中间语义张量c。
- 图中表示的是一个中文到英文的翻译:欢迎 来 北京 → welcome to BeiJing。编码器首先处理中文输入"欢迎 来 北京",通过GRU模型获得每个时间步的输出张量,最后将它们拼接成一个中间语义张量c;接着解码器将使用这个中间语义张量c以及每一个时间步的隐层张量, 逐个生成对应的翻译语言。
- 在上述机器翻译架构中加入Attention的方式有两种:
- 第一种tensorflow版本(传统方式),如下图所示:
上图翻译应用中的Q、K、V解释:
查询张量Q: 解码器上一时间步的隐藏层输出结果
键张量K: 编码部分每个时间步的结果组合而成
值张量V:编码部分每个时间步的结果组合而成
- 第二种Pytorch版本(改进版),如下图所示:
上图翻译应用中的Q、K、V解释
查询张量Q: 解码器每一步的输出(预测结果)或者是当前输入的x
键张量K: 解码器上一步的隐藏层输出
值张量V:编码部分每个时间步输出结果组合而成
- 两个版本对比:
- pytorch版本的是乘型attention,tensorflow版本的是加型attention。pytorch这里直接将与上一个unit隐状态prev_hidden拼接起来✖W得到score,之后将score过softmax得到attenion_weights。
- 解码过程如下:
- (1)采用自回归机制,比如:输入“go”来预测“welcome”,输入“welcome”来预测"to",输入“to”来预测“Beijing”。在输入“welcome”来预测"to"解码中,可使用注意力机制
- (2)查询张量Q:一般可以是“welcome”词嵌入层以后的结果,查询张量Q为生成谁就是谁的查询张量(比如这里为了生成“to”,则查询张量就是“to”的查询张量,请仔细体会这一点)
- (3) 键向量K:一般可以是上一个时间步的隐藏层输出
- (4)值向量V:一般可以是编码部分每个时间步的结果组合而成
- (5)查询张量Q来生成“to”,去检索“to”单词和“欢迎”、“来”、“北京”三个单词的权重分布,注意力结果表示(用权重分布 乘以内容V)
常见的注意力计算规则
将Q,K进行纵轴拼接, 做一次线性变化, 再使用softmax处理获得结果最后与V做张量乘法
将Q,K进行纵轴拼接, 做一次线性变化后再使用tanh函数激活, 然后再进行内部求和, 最后使用softmax处理获得结果再与V做张量乘法。
将Q与K的转置做点积运算, 然后除以一个缩放系数, 再使用softmax处理获得结果最后与V做张量乘法.。
说明:当注意力权重矩阵和V都是三维张量且第一维代表为batch条数时, 则做bmm运算.bmm是一种特殊的张量乘法运算。
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])
什么是深度神经网络注意力机制
- 注意力机制是注意力计算规则能够应用的深度学习网络的载体, 同时包括一些必要的全连接层以及相关张量处理, 使其与应用网络融为一体. 使用自注意力计算规则的注意力机制称为自注意力机制。
- 说明: NLP领域中, 当前的注意力机制大多数应用于seq2seq架构, 即编码器和解码器模型。
- 请思考:为什么要在深度神经网络中引入注意力机制?
* 1、rnn等循环神经网络,随着时间步的增长,前面单词的特征会遗忘,造成对句子特征提取不充分 * 2、rnn等循环神经网络是一个时间步一个时间步的提取序列特征,效率低下 * 3、研究者开始思考,能不能对32个单词(序列)同时提取事物特征,而且还是并行的,所以引入注意力机制!
注意力机制的作用
- 在解码器端的注意力机制: 能够根据模型目标有效的聚焦编码器的输出结果, 当其作为解码器的输入时提升效果. 改善以往编码器输出是单一定长张量, 无法存储过多信息的情况。
- 在编码器端的注意力机制: 主要解决表征问题, 相当于特征提取过程, 得到输入的注意力表示. 一般使用自注意力(self-attention)。
注意力机制在网络中实现的图形表示:
注意力机制实现步骤
步骤
- 第一步: 根据注意力计算规则, 对Q,K,V进行相应的计算。
- 第二步: 根据第一步采用的计算方法, 如果是拼接方法,则需要将Q与第一步的计算结果再进行拼接, 如果是转置点积, 一般是自注意力, Q与V相同, 则不需要进行与Q的拼接。
- 第三步: 最后为了使整个attention机制按照指定尺寸输出, 使用线性层作用在第二步的结果上做一个线性变换, 得到最终对Q的注意力表示。
代码实现
- 常见注意力机制的代码分析:
# 任务描述:
# 有QKV:v是内容比如32个单词,每个单词64个特征,k是32个单词的索引,q是查询张量
# 我们的任务:输入查询张量q,通过注意力机制来计算如下信息:
# 1、查询张量q的注意力权重分布:查询张量q和其他32个单词相关性(相识度)
# 2、查询张量q的结果表示:有一个普通的q升级成一个更强大q;用q和v做bmm运算
# 3 注意:查询张量q查询的目标是谁,就是谁的查询张量。
# eg:比如查询张量q是来查询单词"我",则q就是我的查询张量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 32def __init__(self, query_size, key_size, value_size1, value_size2, output_size):super(MyAtt, self).__init__()self.query_size = query_sizeself.key_size = key_sizeself.value_size1 = value_size1self.value_size2 = value_size2self.output_size = output_size# 线性层1 注意力权重分布self.attn = nn.Linear(self.query_size + self.key_size, self.value_size1)# 线性层2 注意力结果表示按照指定维度输出层 self.attn_combineself.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__':query_size = 32key_size = 32value_size1 = 32 # 32个单词value_size2 = 64 # 64个特征output_size = 32Q = 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)
- 输出效果:
查询张量q的注意力结果表示output---> torch.Size([1, 1, 32]) tensor([[[ 0.3135, -0.0539, 0.0597, -0.0046, -0.3389, -0.1238, 1.0385,0.8896, -0.0268, -0.0705, -0.8409, 0.6547, 0.5909, -0.6048,0.6303, -0.2233, 0.7678, -0.3140, 0.3635, -0.3234, -0.1053,0.5845, 0.1163, -0.2203, -0.0812, -0.0868, 0.0218, -0.0597,0.6923, -0.1848, -0.8266, -0.0614]]], grad_fn=<UnsqueezeBackward0>)
查询张量q的注意力权重分布attn_weights---> torch.Size([1, 32]) tensor([[0.0843, 0.0174, 0.0138, 0.0431, 0.0110, 0.0308, 0.0608, 0.0216, 0.0101,0.0406, 0.0462, 0.0111, 0.0349, 0.0065, 0.0383, 0.0526, 0.0151, 0.0193,0.0294, 0.0632, 0.0322, 0.0072, 0.0294, 0.0388, 0.0135, 0.0443, 0.0594,0.0332, 0.0117, 0.0168, 0.0293, 0.0344]], grad_fn=<SoftmaxBackward0>)
- 更多有关注意力机制的应用我们将在案例中进行详尽的理解分析。
RNN案例 seq2seq英译法
seq2seq介绍
seq2seq模型架构
seq2seq模型架构分析:
- seq2seq模型架构包括三部分,分别是encoder(编码器)、decoder(解码器)、中间语义张量c。其中编码器和解码器的内部实现都使用了GRU模型。
- 图中表示的是一个中文到英文的翻译:欢迎 来 北京 → welcome to BeiJing。编码器首先处理中文输入"欢迎 来 北京",通过GRU模型获得每个时间步的输出张量,最后将它们拼接成一个中间语义张量c;接着解码器将使用这个中间语义张量c以及每一个时间步的隐层张量, 逐个生成对应的翻译语言。
- 我们的案例通过英译法来讲解seq2seq设计与实现。
数据集介绍
数据集下载: https://pan.baidu.com/s/1KlZkGVjKnP7o9udhtfzGdA?pwd=bg4c 提取码: bg4c
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 .
案例步骤
基于GRU的seq2seq模型架构实现翻译的过程:
- 第一步: 导入工具包和工具函数
- 第二步: 对持久化文件中数据进行处理, 以满足模型训练要求
- 第三步: 构建基于GRU的编码器和解码器
- 第四步: 构建模型训练函数, 并进行训练
- 第五步: 构建模型评估函数, 并进行测试以及Attention效果分析
导入工具包和工具函数
# 用于正则表达式
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表示第一个分组 正则中的\nums = 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[] 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_nenglish_word2index = {"SOS": 0, "EOS": 1}english_word_n = 2french_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_nenglish_word_n += 1for word in pair[1].split(' '):if word not in french_word2index:french_word2index[word] = french_word_nfrench_word_n += 1# 3-2 english_index2word french_index2wordenglish_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]])
构建数据源对象
# 原始数据 -> 数据源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_yclass MyPairsDataset(Dataset):def __init__(self, my_pairs):# 样本xself.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 yx = 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
构建数据迭代器
def dm_test_MyPairsDataset():# 1 实例化dataset对象mypairsdataset = MyPairsDataset(my_pairs)# 2 实例化dataloadermydataloader = 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]])
构建基于GRU的编码器和解码器
构建基于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)
- 构建基于GRU的编码器:
class EncoderRNN(nn.Module):def __init__(self, input_size, hidden_size):# input_size 编码器 词嵌入层单词数 eg:2803# hidden_size 编码器 词嵌入层每个单词的特征数 eg 256super(EncoderRNN, self).__init__()self.input_size = input_sizeself.hidden_size = hidden_size# 实例化nn.Embedding层self.embedding = nn.Embedding(input_size, hidden_size)# 实例化nn.GRU层 注意参数batch_first=Trueself.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, hiddendef inithidden(self):# 将隐层张量初始化成为1x1xself.hidden_size大小的张量return torch.zeros(1, 1, self.hidden_size, device=device)
- 调用:
def dm_test_EncoderRNN():# 实例化dataset对象mypairsdataset = MyPairsDataset(my_pairs)# 实例化dataloadermydataloader = DataLoader(dataset=mypairsdataset, batch_size=1, shuffle=True)# 实例化模型input_size = english_word_nhidden_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>)
构建基于GRU的解码器
- 解码器结构图:
- 构建基于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 256super(DecoderRNN, self).__init__()self.output_size = output_sizeself.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 输出尺寸4345self.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, hiddendef inithidden(self):# 将隐层张量初始化成为1x1xself.hidden_size大小的张量return torch.zeros(1, 1, self.hidden_size, device=device)
- 调用:
def dm03_test_DecoderRNN():# 实例化dataset对象mypairsdataset = MyPairsDataset(my_pairs)# 实例化dataloadermydataloader = DataLoader(dataset=mypairsdataset, batch_size=1, shuffle=True)# 实例化模型input_size = english_word_nhidden_size = 256 # 观察结果数据 可使用8my_encoderrnn = EncoderRNN(input_size, hidden_size)print('my_encoderrnn模型结构--->', my_encoderrnn)# 实例化模型input_size = french_word_nhidden_size = 256 # 观察结果数据 可使用8my_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])
构建基于GRU和Attention的解码器
- 解码器结构图:
- 实现思路分析:
# 构建基于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 最大长度10super(AttnDecoderRNN, self).__init__()self.output_size = output_sizeself.hidden_size = hidden_sizeself.dropout_p = dropout_pself.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_weightsdef 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_encoderrnnmy_encoderrnn = EncoderRNN(english_word_n, 256)# 实例化 解码器DecoderRNNmy_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])# 中间语义张量Cencode_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])
构建模型训练函数, 并进行训练
teacher_forcing介绍
它是一种用于序列生成任务的训练技巧, 在seq2seq架构中, 根据循环神经网络理论,解码器每次应该使用上一步的结果作为输入的一部分, 但是训练过程中,一旦上一步的结果是错误的,就会导致这种错误被累积,无法达到训练效果, 因此,我们需要一种机制改变上一步出错的情况,因为训练时我们是已知正确的输出应该是什么,因此可以强制将上一步结果设置成正确的输出, 这种方式就叫做teacher_forcing。
teacher_forcing的作用
- 能够在训练的时候矫正模型的预测,避免在序列生成的过程中误差进一步放大。
- teacher_forcing能够极大的加快模型的收敛速度,令模型训练过程更快更平稳。
构建内部迭代训练函数
- 模型训练参数
# 模型训练参数
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]# 解码参数2decode_hidden = encode_hidden# 解码参数3input_y = torch.tensor([[SOS_token]], device=device)myloss = 0.0y_len = y.shape[1]use_teacher_forcing = True if random.random() < teacher_forcing_ratio else Falseif 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:breakinput_y = topi.detach()# 梯度清零myadam_encode.zero_grad()myadam_decode.zero_grad()# 反向传播myloss.backward()# 梯度更新myadam_encode.step()myadam_decode.step()# 返回 损失列表myloss.item()/y_lenreturn myloss.item() / y_len
构建模型训练函数
- 实现思路分析:
# 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对象 实例化 mydataloadermypairsdataset = MyPairsDataset(my_pairs)mydataloader = DataLoader(dataset=mypairsdataset, batch_size=1, shuffle=True)# 实例化编码器 my_encoderrnn 实例化解码器 my_attndecoderrnnmy_encoderrnn = EncoderRNN(2803, 256)my_attndecoderrnn = AttnDecoderRNN(output_size=4345, hidden_size=256, dropout_p=0.1, max_length=10)# 实例化编码器优化器 myadam_encode 实例化解码器优化器 myadam_decodemyadam_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.0starttime = 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 += mylossplot_loss_total += myloss# 计算打印屏幕间隔损失-每隔1000次if item % print_interval_num ==0 :print_loss_avg = print_loss_total / print_interval_num# 将总损失归0print_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)# 总损失归0plot_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
损失曲线分析
损失下降曲线:
一直下降的损失曲线, 说明模型正在收敛, 能够从数据中找到一些规律应用于数据。
构建模型评估函数并测试
构建模型评估函数
# 模型评估代码与模型预测代码类似,需要注意使用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 固定长度中间语义张量cencoder_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=10output_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>')breakelse: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)# 实例化dataloadermydataloader = DataLoader(dataset=mypairsdataset, batch_size=1, shuffle=True)# 实例化模型input_size = english_word_nhidden_size = 256 # 观察结果数据 可使用8my_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_nhidden_size = 256 # 观察结果数据 可使用8my_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>
Attention张量制图
def dm_test_Attention():# 实例化dataset对象mypairsdataset = MyPairsDataset(my_pairs)# 实例化dataloadermydataloader = DataLoader(dataset=mypairsdataset, batch_size=1, shuffle=True)# 实例化模型input_size = english_word_nhidden_size = 256 # 观察结果数据 可使用8my_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_nhidden_size = 256 # 观察结果数据 可使用8my_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可视化:
- Attention图像的纵坐标代表输入的源语言各个词汇对应的索引, 0-6分别对应["we", "re", "both", "teachers", ".", ""], 纵坐标代表生成的目标语言各个词汇对应的索引, 0-7代表['nous', 'sommes', 'toutes', 'deux', 'enseignantes', '.', ''], 图中浅色小方块(颜色越浅说明影响越大)代表词汇之间的影响关系, 比如源语言的第1个词汇对生成目标语言的第1个词汇影响最大, 源语言的第4,5个词对生成目标语言的第5个词会影响最大, 通过这样的可视化图像, 我们可以知道Attention的效果好坏, 与我们人为去判定到底还有多大的差距. 进而衡量我们训练模型的可用性。
完整代码
# -*-coding:utf-8-*-
# 用于正则表达式
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
from tqdm import tqdm# 选择设备,如果有GPU而且已经安装了GPU版本的pytorch,可以正常使用:Windows或者linux
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# print(f"当前运行的设备device:{device}")
# device = 'mps'
# 定义开始字符的标志
SOS_TOKEN = 0
# 定义结束字符的标志
EOS_TOKEN = 1
# 最大句子长度
MAX_LENGTH = 10
# 定义原始文件的路径
eng_fra_path = './data/eng-fra-v2.txt'# todo:1.定义数据清洗工具
def normal2string(s):# 将字符串变成小写,并且去除两边的空白s1 = s.lower().strip()# 将标点符号和字母分割开s2 = re.sub(r"([.?!])", r" \1", s1)# 将非大小写英文字母以及.?!标点符号的字符都用空格替代s3 = re.sub(r"[^a-zA-Z.?!]+", r" ", s2)return s3# todo:2. 定义数据预处理函数
def get_data():# 1.读取原始的txt文档with open(eng_fra_path, "r", encoding="utf-8") as fr:lines = fr.readlines()# print(f"lines-->{lines[:3]}")# 2.对原始句子进行规范化处理# my_pairs===[[english, french],[english, french],...]my_pairs = [[normal2string(s) for s in line.strip().split("\t")] for line in lines]# print(f"my_paris-->{my_paris[:3]}")# 3.构建英文和法文字典english_word2index = {"SOS": 0, "EOS": 1}english_word_n = 2french_word2index = {"SOS": 0, "EOS": 1}french_word_n = 2for pair in my_pairs:# 填充英文词典for word in pair[0].split(" "):if word not in english_word2index:english_word2index[word] = english_word_nenglish_word_n += 1# 填充法文词典for word in pair[1].split(" "):if word not in french_word2index:french_word2index[word] = french_word_nfrench_word_n += 1# 获取index2wordenglish_index2word = {value: key for key, value in english_word2index.items()}french_index2word = {value: key for key, value in french_word2index.items()}return english_word2index, english_index2word, english_word_n, french_word2index, french_index2word, french_word_n, my_pairsenglish_word2index, english_index2word, english_word_n, \french_word2index, french_index2word, french_word_n, my_pairs = get_data()# todo:3. 定义Dataset类
class SeqDataset(Dataset):def __init__(self, my_pairs):super().__init__()# 样本对:[[英文句子,法文句子],。。。]self.my_paris = my_pairs# 样本的总个数self.sample_len = len(my_pairs)def __len__(self):return self.sample_lendef __getitem__(self, item):# 对索引进行修正index = min(max(0, item), self.sample_len - 1)# 根据索引去除样本,拿到对应的原始英文句子以及法文句子x = self.my_paris[index][0]# print(f'x------>{x}')y = self.my_paris[index][1]# print(f'y------->{y}')# 将原始的英文句子x转换成张量的表示形式:每个词表示成对应的idx = [english_word2index[word] for word in x.split(' ')]# 在序列生成任务中,向目标序列末尾添加结束标记(EOS)# 该操作表示模型输出的终止位置x.append(EOS_TOKEN)tensor_x = torch.tensor(x, dtype=torch.long, device=device)# print(f'tensor_x--》{tensor_x}')# 将原始的法文句子y转换成张量的表示形式:每个词表示成对应的idy = [french_word2index[word] for word in y.split(' ')]# 在序列生成任务中,向目标序列末尾添加结束标记(EOS)# 该操作表示模型输出的终止位置y.append(EOS_TOKEN)tensor_y = torch.tensor(y, dtype=torch.long, device=device)# print(f'tensor_y--》{tensor_y}')return tensor_x, tensor_ydef use_dataset():my_datset = SeqDataset(my_pairs)print(len(my_datset))tensor_x, tensor_y = my_datset[0]print(f'tensor_x--》{tensor_x}')print(f'tensor_y--》{tensor_y}')# tensor_x(英文句子) 形状为 [seq_len_en],其中 seq_len_en 是英文句子的单词数量 + 1(包含结尾的 EOS_TOKEN)。
# 示例:若原句为 "He is running"(3个单词),则 tensor_x.shape = [4]。
# tensor_y(法文句子) 形状为 [seq_len_fr],其中 seq_len_fr 是法文句子的单词数量 + 1(包含结尾的 EOS_TOKEN)。
# 示例:若原句为 "Il court"(2个单词),则 tensor_y.shape = [3]。# todo:5. 定义编码器模型
class EncoderGRU(nn.Module):def __init__(self, vocab_size, hidden_size):super().__init__()# vocab_size:英文单词的总个数self.vocab_size = vocab_size# hidden_size:单词的词嵌入维度self.hidden_size = hidden_size# 定义Embedding层self.embed = nn.Embedding(vocab_size, hidden_size)# 定义GRU层self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)def forward(self, input_x, h0):# input_x--》编码器的输入:【batch_size, seq_len】-->【1,6】# h0--》初始化的隐藏层:[1, 1, 256]# 将input_x经过embedding层进行词嵌入表示:embed_x-->shape-->[1,6, 256]embed_x = self.embed(input_x)# 将embed_x和h0送入GRU模型output, hn = self.gru(embed_x, h0)return output, hndef inithidden(self):return torch.zeros(1, 1, self.hidden_size, device=device)# todo:6定义解码器模型(没有注意力机制)
class DecoderGRU(nn.Module):def __init__(self, french_vocab_size, hidden_size):super().__init__()# french_vocab_size:法文单词的总个数self.french_vocab_size = french_vocab_size# hidden_size: 词嵌入维度self.hidden_size = hidden_size# 定义EMbedding层self.embed = nn.Embedding(french_vocab_size, hidden_size)# 定义GRU层self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)# 定义输出层self.out = nn.Linear(hidden_size, french_vocab_size)# 因为讲义中用的是Nllloss,所以这里只需要加一个logSoftmaxself.softmax = nn.LogSoftmax(dim=-1)def forward(self, input_y0, h0):# input_y0-->解码器端的输入,形状--》[batch_size, 1],-->[1, 1]# h0-->[1, 1, 256]# 1.将input_y进行Embedding的处理-->[1, 1, 256]embed_y = self.embed(input_y0)# 2. 为了防止过拟合relu_y = F.relu(embed_y)# 3. 需要将relu_y和h0送入gru模型:gru_output--》shape-->[1, 1, 256]gru_output, hn = self.gru(relu_y, h0)# 4. 需要将gru_output送入输出层:output-->[1, 4345]output = self.out(gru_output[0])return self.softmax(output), hndef inithidden(self):return torch.zeros(1, 1, self.hidden_size, device=device)# 测试解码器(没有注意力机制)
def dm_test_decoder():# 1.实例化Dataset对象my_dataset = SeqDataset(my_pairs)# 2.实例化dataloader对象train_dataloader = DataLoader(dataset=my_dataset, batch_size=1, shuffle=True)# 3. 实例化编码器对象encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256)# 4. 实例化解码器对象decoder = DecoderGRU(french_vocab_size=french_word_n, hidden_size=256)# 5.将数据送入编码器和解码器得到结果for x, y in train_dataloader:print(y)# x--》shape-->[batch_size, seq_len]-->[1, 6]# y--》shape-->[batch_size, seq_len]-->[1, 7]# 5.1 将x送入编码器得到预测结果encoder_output, encoder_hidden = encoder.forward(input_x=x, h0=encoder.inithidden())print(f'encoder_output--》{encoder_output.shape}')print(f'encoder_hidden--》{encoder_hidden.shape}')# 5.2根据编码的结果进行解码:解码是一个token一个token来进行解码hidden = encoder_hiddenfor i in range(y.shape[1]):temp_vec = y[0][i].view(1, -1)output, hidden = decoder(temp_vec, hidden)print(f'output--》{output.shape}')break# todo:7. 定义解码器模型(带Attention)
class AttentionDecoder(nn.Module):def __init__(self, french_vocab_size, hidden_size, dropout_p=0.1, max_len=MAX_LENGTH):super().__init__()# french_vocab_size:解码器端输入的法文单词的总个数self.french_vocab_size = french_vocab_size# hidden_size:代表词嵌入的维度self.hidden_size = hidden_size# dropout_p:随机失活的系数,防止过拟合self.dropout_p = dropout_p# max_len:规范化最大句子长度:方便计算self.max_len = max_len# 定义Embedding层self.embed = nn.Embedding(french_vocab_size, hidden_size)# 定义dropout层self.dropout = nn.Dropout(p=dropout_p)# 根据注意力计算规则第一个规则,需要定义第一个全连接层对Q和K的拼接向量处理# 因为self.attn输出的结果要和Value进行相乘,所以输出维度要和value的中间维度保持一郅# value--》来自于编码器--》[batch_size, seq_len, embed_dim]-->[1, max_len, embed_dim]self.attn = nn.Linear(hidden_size * 2, max_len)# 定义第二个全连接层:按照注意力计算步骤的第三步,需要对第二步:Q和第一步拼接的结果处理,而且按照指定的尺寸输出结果self.attn_combine = nn.Linear(hidden_size * 2, hidden_size)# 定义GRU层self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)# 定义输出层self.out = nn.Linear(hidden_size, french_vocab_size)# 定义log-softmax层self.softmax = nn.LogSoftmax(dim=-1)def forward(self, input_y, hidden, encoder_out_c):# input_y(query)-->解码器的上一时间步预测结果,[1, 1]# hidden(key)-->上一时间步隐藏层输出结果:[1, 1,256]# encoder_output_c(value)--->编码器的输出结果--》[max_len, hidden_size]-->[10, 256]# 1.将input_y送入Embedding层进行升维embed_y0 = self.embed(input_y)# 2.将上述的embed_y0经过dropout,防止过拟合:embed_y1-_>[1, 1, 256]embed_y1 = self.dropout(embed_y0)# 3. 将embed_y1和hidden进行拼接,并且经过线性层输出,还需要经过softmax# embed_y1-_>[1, 1, 256]; hidden--->[1, 1, 256];atten_weights-->[1, 10]atten_weights = F.softmax(self.attn(torch.cat((embed_y1[0], hidden[0]), dim=-1)), dim=-1)# 4.需要将权重和Value进行相乘:atten1-->[1, 1, 256]atten1 = torch.bmm(atten_weights.unsqueeze(dim=0), encoder_out_c.unsqueeze(dim=0))# 5.需要将embed_y1和atten1需要再次拼接:temp_vec-->[1, 512]temp_vec = torch.cat((embed_y1[0], atten1[0]), dim=-1)# 6. 将拼接后的结果进行线性变换,按照指定尺寸输出:combin_output-->[1,1, 256]combin_output = self.attn_combine(temp_vec).unsqueeze(dim=0)# 7. 将上述结果经过relu:relu_output-->[1,1,256]relu_output = F.relu(combin_output)# 8. 需要将relu_output和hidden送入GRU模型gru_output, hn = self.gru(relu_output, hidden)# 9. 需要将gru_output送入输出层result = self.out(gru_output[0])return self.softmax(result), hn, atten_weights# 测试解码器(带Attention)
def dm02_test_attenDecoder():# 1.实例化Dataset对象my_dataset = SeqDataset(my_pairs)# 2.实例化dataloader对象train_dataloader = DataLoader(dataset=my_dataset, batch_size=1, shuffle=True)# 3. 实例化编码器对象encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256)encoder = encoder.to(device=device)# 4. 实例化带Attention解码器对象atten_decoder = AttentionDecoder(french_vocab_size=french_word_n, hidden_size=256)atten_decoder = atten_decoder.to(device=device)# 5.将数据送入编码器和解码器得到结果for x, y in train_dataloader:print(y)# x--》shape-->[batch_size, seq_len]-->[1, 6]# y--》shape-->[batch_size, seq_len]-->[1, 7]# 5.1 将x送入编码器得到预测结果encoder_output, encoder_hidden = encoder(input_x=x, h0=encoder.inithidden())print(f'encoder_output--》{encoder_output.shape}')print(f'encoder_hidden--》{encoder_hidden.shape}')# 5.2根据编码的结果进行解码:解码是一个token一个token来进行解码encoder_output_c = torch.zeros(MAX_LENGTH, encoder.hidden_size, device=device)print(f'encoder_output_c--》{encoder_output_c.shape}')for idx in range(encoder_output.shape[1]):encoder_output_c[idx] = encoder_output[0, idx]print(f'encoder_output_c--》{encoder_output_c}')# 开始解码hidden = encoder_hiddenfor j in range(y.shape[1]):temp_vec = y[0][j].view(1, -1)output, hidden, atten_weights = atten_decoder(temp_vec, hidden, encoder_output_c)print(f'output--》{output.shape}')print(f'hidden--》{hidden.shape}')print(f'atten_weights--》{atten_weights.shape}')breakmy_lr = 0.0001
epochs = 1
teacher_forcing_ratio = 0.5# todo:8. 定义模型内部训练函数
def train_iter(x, y, encoder_model, decoder_model, encoder_adam, decoder_adam, cross_entropy):# x-->源文本输入(英文)--》[batch_size, seq_len]-->[1, 6]# y-->目标文本输入(法文)--》[batch_size, seq_len]-->[1, 8]# encoder_model-->编码器模型的对象;decoder_model-->解码器模型(带Attention)的对象# encoder_adam-->编码器优化器的对象;decoder_adam-->解码器优化器的对象# cross_entropy-->损失函数的对象# 1. 将源文本输入(英文)送入编码器中,得到编码之后的结果encoder_output, encoder_hidden = encoder_model(x, h0=encoder_model.inithidden())# print(f'encoder_output--》{encoder_output.shape}')# print(f'encoder_hidden--》{encoder_hidden.shape}')# 2.准备解码器的输入参数# 2.1 准备第一个参数:encoder_output_c-->value--》[max_len, hidden_size]encoder_output_c = torch.zeros(MAX_LENGTH, encoder_model.hidden_size, device=device)for idx in range(x.shape[1]):encoder_output_c[idx] = encoder_output[0, idx]# 2.2 准备第二个参数:decoder_hidden--->keydecoder_hidden = encoder_hidden# 2.3 准备第三个参数:queryinput_y = torch.tensor([[SOS_TOKEN]], device=device)# 3.定义变量my_loss = 0.0# 真实翻译的法文句子长度y_len = y.shape[0]# 是否使用teacher_forcing策略use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False# 4.计算损失(解码过程)if use_teacher_forcing:for idx in range(y_len):decoder_output, decoder_hidden, atten_weights = decoder_model(input_y, decoder_hidden, encoder_output_c)# print(f'decoder_output--》{decoder_output.shape}')# print(f'decoder_hidden--》{decoder_hidden.shape}')# 获得当前时间步真实的targettarget_y = y[0][idx].view(1)my_loss = my_loss + cross_entropy(decoder_output, target_y)# print(f"my_loss-->{my_loss}")input_y = y[0][idx].view(1, -1)breakelse:for idx in range(y_len):decoder_output, decoder_hidden, atten_weights = decoder_model(input_y, decoder_hidden, encoder_output_c)# print(f'不用teacher_forcing策略的decoder_output--》{decoder_output.shape}')# 获得当前时间步真实的targettarget_y = y[0][idx].view(1)my_loss = my_loss + cross_entropy(decoder_output, target_y)# 获取当前时间步预测的最大概率值及对应的索引# decoder_output--》[1, 4345];idx-->[1, 1]-->具体值:eg:[[3889]]value, idx = torch.topk(decoder_output, k=1)if idx.item() == EOS_TOKEN:breakinput_y = idx# 5.梯度清零encoder_adam.zero_grad()decoder_adam.zero_grad()# 6.反向传播my_loss.backward()# 梯度更新encoder_adam.step()decoder_adam.step()return my_loss.item() / y_len# todo:9. 定义模型训练函数
def train_model():# 1.实例化Dataset对象my_dataset = SeqDataset(my_pairs)# 2.实例化Dataloader对象train_dataloader = DataLoader(dataset=my_dataset, batch_size=1, shuffle=True)# 3.实例化编码器模型对象encoder_model = EncoderGRU(vocab_size=english_word_n, hidden_size=256)encoder_model = encoder_model.to(device=device)# 4.实例化带Attention解码器模型对象atten_decoder_model = AttentionDecoder(french_vocab_size=french_word_n, hidden_size=256)atten_decoder_model = atten_decoder_model.to(device=device)# 5.实例化优化器对象encoder_adam = optim.Adam(encoder_model.parameters(), lr=my_lr)decoder_adam = optim.Adam(atten_decoder_model.parameters(), lr=my_lr)# 6.实例化损失函数对象cross_entropy = nn.NLLLoss()# 7.定义训练日志的参数plot_loss_list = []# 8. 开始外部循环for epoch_idx in range(1, 1 + epochs):# 定义一些变量print_loss_total, plot_loss_total = 0.0, 0.0# 开始计时start_time = time.time()# 9. 开始内部迭代for item, (x, y) in enumerate(tqdm(train_dataloader), start=1):# 调用内部训练函数得到损失my_loss = train_iter(x, y, encoder_model, atten_decoder_model, encoder_adam, decoder_adam, cross_entropy)# print(f'my_loss--》{my_loss}')# 打印训练日志的参数print_loss_total += my_lossplot_loss_total += my_loss# 每隔1000步计算一下平均损失if item % 1000 == 0:avg_loss = print_loss_total / 1000print_loss_total = 0.0print('当前训练的轮次为:%s, 平均损失为:%s, 训练耗时:%s' % (epoch_idx, avg_loss, time.time() - start_time))# 每隔100步计算一下平均损失,并且存储到列表中,画图展示if item % 100 == 0:plot_avg_loss = plot_loss_total / 100plot_loss_list.append(plot_avg_loss)plot_loss_total = 0# 10.保存模型torch.save(encoder_model.state_dict(), './save_model/encoder_%d.pth' % (epoch_idx))torch.save(atten_decoder_model.state_dict(), './save_model/decoder_%d.pth' % (epoch_idx))# 画图plt.figure()plt.plot(plot_loss_list)plt.savefig('seq2seq_eng2french.png')plt.show()# todo:10. 定义模型内部评估函数
def seq2seq_evalute(tensor_x, encoder_model, atten_decoder_model):with torch.no_grad():# 1.得到编码器的输出结果:encoder_output-_>[batch_size, seq_len, hidden_size]-->[1, 6, 256]encoder_output, encoder_hidden = encoder_model(tensor_x, encoder_model.inithidden())# 2. 准备解码器模型输入参数# 2.1 准备value--》来自编码器的输出结果:encoder_output_c:[10, 256]encoder_output_c = torch.zeros(MAX_LENGTH, encoder_model.hidden_size, device=device)for idx in range(tensor_x.shape[1]):encoder_output_c[idx] = encoder_output[0, idx]# 2.2 准备key--》来自解码器的上一层的隐藏层输出结果,初始时,来自与编码器最后一个单词的隐藏层输出结果decoder_hidden = encoder_hidden# 2.3 准备query--》来自于解码器上一层的真实预测结果,初始时时特殊的token--》SOSinput_y = torch.tensor([[SOS_TOKEN]], device=device)# 3. 准备变量decoded_words = []decoded_attention_weights = torch.zeros(MAX_LENGTH, MAX_LENGTH)# 4.开始整个解码的过程:一个token一个token去解码for idx in range(MAX_LENGTH):output_y, decoder_hidden, atten_weight = atten_decoder_model(input_y, decoder_hidden, encoder_output_c)# print(f'output_y--》{output_y.shape}')# print(f'decoder_hidden--》{decoder_hidden.shape}')# print(f'atten_weight--》{atten_weight.shape}')# 得到预测最大概率值及其对应的索引topv, topi = torch.topk(output_y, k=1)# print(f'topi---》{topi}')# 将真实的每个时间步得到的注意力权重进行赋值decoded_attention_weights[idx] = atten_weight# decoded_attention_weights[idx] = atten_weight[0]# 如果预测的结果等于结束符EOS,直接终止if topi.item() == EOS_TOKEN:decoded_words.append('<EOS>')breakelse:decoded_words.append(french_index2word[topi.item()])# 将预测的结果当作下一个时间步的输入input_y = topireturn decoded_words, decoded_attention_weights[:idx+1]
# todo:11. 定义模型预测函数def model2predict():# 1.实例化编码器模型对象encoder_model = EncoderGRU(vocab_size=english_word_n, hidden_size=256)encoder_model.load_state_dict(torch.load('./save_model/encoder_1.pth'))encoder_model = encoder_model.to(device=device)# encoder_model.load_state_dict(torch.load('./save_model/zzz_encoder_1.pth', map_location='cpu'))print(encoder_model)# 2.实例化带Attention解码器模型对象atten_decoder_model = AttentionDecoder(french_vocab_size=french_word_n, hidden_size=256)atten_decoder_model.load_state_dict(torch.load('./save_model/decoder_1.pth'))print(atten_decoder_model)atten_decoder_model = atten_decoder_model.to(device=device)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 idx, pair in enumerate(my_samplepairs):# print(f'pair--》{pair}')x = pair[0] # 英文y = pair[1] #法文# 需要将英文句子变成张量的形式temp_x = [english_word2index[word] for word in x.split(' ')]temp_x.append(EOS_TOKEN)tensor_x = torch.tensor(temp_x, dtype=torch.long, device=device).view(1, -1)# print(f'tensor_x--》{tensor_x}')# 将数据送入模型得到预测的结果decoder_words, atten_weights = seq2seq_evalute(tensor_x, encoder_model, atten_decoder_model)predict_y = ' '.join(decoder_words)print('*'*80)print(f'x-----》{x}')print(f'y-----》{y}')print(f'predict_y-----》{predict_y}')# todo:12. 绘图def model2attention():# 1.实例化编码器模型对象encoder_model = EncoderGRU(vocab_size=english_word_n, hidden_size=256)encoder_model.load_state_dict(torch.load('./save_model/encoder_1.pth'))encoder_model = encoder_model.to(device=device)# encoder_model.load_state_dict(torch.load('./save_model/zzz_encoder_1.pth', map_location='cpu'))print(encoder_model)# 2.实例化带Attention解码器模型对象atten_decoder_model = AttentionDecoder(french_vocab_size=french_word_n, hidden_size=256)atten_decoder_model.load_state_dict(torch.load('./save_model/decoder_1.pth'))print(atten_decoder_model)atten_decoder_model = atten_decoder_model.to(device=device)sentence = "we re both teachers ."# 需要将英文句子变成张量的形式temp_x = [english_word2index[word] for word in sentence.split(' ')]temp_x.append(EOS_TOKEN)tensor_x = torch.tensor(temp_x, dtype=torch.long, device=device).view(1, -1)# print(f'tensor_x--》{tensor_x}')# 将数据送入模型得到预测的结果decoder_words, atten_weights = seq2seq_evalute(tensor_x, encoder_model, atten_decoder_model)predict_y = ' '.join(decoder_words)print('*'*80)print(f'x-----》{sentence}')print(f'predict_y-----》{predict_y}')print(f'atten_weights-----》{atten_weights.shape}')# 绘图plt.matshow(atten_weights)plt.savefig('./seq2seq_attention.png')plt.show()if __name__ == '__main__':# dm_test_decoder()# dm02_test_attenDecoder()# train_model()# model2predict()model2attention()
尾声
这篇文字长达10w+字,大部分是可以直接用来执行的代码,友友们可以放心使用。
rnn及其变体非常重要,学习透彻之后,对于后面的transformer机制的学习有巨大帮助,AI学习之路艰苦漫长,大家一起加油鸭!
专栏下一篇将介绍transformer机制的概念和案例,感兴趣的友友们可以关注一下<( ̄︶ ̄)↗[GO!]。