3.RNN及其变体

第三章 RNN及其变体

1 认识RNN模型

【根据RNN内部结构,可以分为哪几类】

复制代码
  • 定义
properties 复制代码
循环神经网络:一般接受的一序列进行输入,输出也是一个序列
  • 作用和应用场景
properties 复制代码
RNN擅长处理连续语言文本,机器翻译、文本生成、文本分类、摘要生成
  • RNN模型的分类

    • 根据输入与输出结构
    properties 复制代码
    N Vs N : 输入和输出等长,应用场景:对联生成;词性标注;NER
    N Vs 1 : 输入N,输出为单值,应用场景:文本分类
    1 Vs N : 输入是一个,输出为N,应用场景:图片文本生成
    N Vs M : 输入和输出不等长,应用场景:文本翻译、摘要总结
    • 根据RNN内部结构
    properties 复制代码
    传统RNN
    LSTM
    BI-LSTM
    GRU
    BI-GRU

2 传统RNN模型

【传统RNN的工作原理是什么】

复制代码

【激活函数tanh的作用是什么】

复制代码

【nn.RNN初始化主要参数有哪几个】

复制代码

【RNN的输入input、h0每一维度表示什么意义】

复制代码

2.1 内部结构

  • 输入:当前时间步xt和上一时间步输出的ht-1
  • 输出:ht和ot (一个时间步内:ht=ot)


  • 多层RNN的解析

  • RNN模型实现

    python 复制代码
    # 输入数据长度发生变化
    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)

3 LSTM模型

【介绍一下LSTM的结构】

复制代码

【什么是BiLSTM】

复制代码

【LSTM的优缺点是什么】

复制代码

【如何调用双向LSTM】

复制代码

3.1 内部结构

  • 遗忘门
  • 输入门
  • 细胞状态
  • 输出门
  • LSTM模型代码实现

    python 复制代码
    import torch
    import torch.nn as nn
    def dm02_lstm_for_direction():
        '''
        第一个参数:input_size(输入张量x的维度)
        第二个参数:hidden_size(隐藏层的维度, 隐藏层的神经元个数)
        第三个参数:num_layer(隐藏层的数量)
        bidirectional = True
        #
        '''
        lstm = nn.LSTM(5, 6, 1, batch_first = True)
    
        '''
        input
        第一个参数:batch_size(批次的样本数量)
        第二个参数:sequence_length(输入序列的长度)
        第三个参数:input_size(输入张量的维度)
        '''
        input = torch.randn(4, 10, 5)
    
        '''
        hn和cn
        第一个参数:num_layer * num_directions(层数*网络方向)
        第二个参数:batch_size(批次的样本数)
        第三个参数:hidden_size(隐藏层的维度, 隐藏层神经元的个数)
        '''
        h0 = torch.zeros(1, 4, 6)
        c0 = torch.zeros(1, 4, 6)
    
        # 数据送入模型
        output, (hn, cn) = lstm(input, (h0, c0))
        print(f'output-->{output.shape}')
        print(f'hn-->{hn.shape}')
        print(f'cn-->{cn.shape}')
  • BI-LSTM

properties 复制代码
定义: 不改变原始的LSTM模型内部结构,只是将文本从左到右计算一遍,再从右到左计算一遍,把最终的输出结果拼接得到模型的完整输出

4 GRU模型

【介绍一下GRU结构】

复制代码

【GRU有什么优势】

复制代码

【如何调用GRU】

复制代码

4.1 内部结构

  • 更新门
  • 重制门
  • GRU模型代码实现

    python 复制代码
    # coding:utf-8
    import torch
    import torch.nn as nn
    
    # 模型参数发生变化对其他输入参数的影响
    def  dm01_gru_():
        '''
        第一个参数:input_size(输入张量x的维度)
        第二个参数:hidden_size(隐藏层的维度, 隐藏层的神经元个数)
        第三个参数:num_layer(隐藏层的数量)
        # batch_first = True,代表batch_size 放在第一位
        '''
        gru = nn.GRU(5, 6, 1, batch_first=True)
        print(gru.all_weights)
        print(gru.all_weights[0][0].shape)
        print(gru.all_weights[0][1].shape)
    
        '''
        第一个参数:batch_size(批次的样本数量)
        第二个参数:sequence_length(输入序列的长度)
        第三个参数:input_size(输入张量的维度)
        '''
        input = torch.randn(4, 3, 5)
    
        '''
        第一个参数:num_layer * num_directions(层数*网络方向)
        第二个参数:batch_size(批次的样本数)
        第三个参数:hidden_size(隐藏层的维度, 隐藏层神经元的个数)
        '''
    
        h0 = torch.randn(1, 4, 6)
    
        # 将数据送入模型得到结果
        output, hn = gru(input, h0)
        print(f'output--》{output}')
        print(f'hn--》{hn}')
    
    if __name__ == '__main__':
        dm01_gru_()
  • Bi-GRU

properties 复制代码
定义: 不改变原始的GRU模型内部结构,只是将文本从左到右计算一遍,再从右到左计算一遍,把最终的输出结果拼接得到模型的完整输出
  • 优缺点
    • 优点:相比LSTM,结构较为简单,能够和lstm一样缓解梯度消失问题
    • 缺点:RNN系列模型不能实现并行运算,数据量大的话,效率比较低

5 RNN案例 人名分类器

5.1 案例介绍

  • 任务目的
properties 复制代码
目的: 给定一个人名,来判定这个人名属于哪个国家
典型的文本分类任务: 18分类---多分类任务
  • 数据格式

  • 注意:两列数据,第一列是人名,第二列是国家类别,中间用制表符号"\t"隔开

properties 复制代码
Ang	Chinese
AuYong	Chinese
Yuasa	Japanese
Yuhara	Japanese
Yunokawa	Japanese

5.2 案例步骤

properties 复制代码
1. 获取数据:案例中是直接给定的
2. 数据预处理: 脏数据清洗、数据格式转换、数据源Dataset的构造、数据迭代器Dataloader的构造
3. 模型搭建: RNN、LSTM、GRU一系列模型
4. 模型评估(测试)
5. 模型上线--API接口
(1)数据预处理
  • 读取txt文档数据

目的

properties 复制代码
将文档里面的数据读取到内存中,实际上我们做了一个操作: 将人名存放到一个列表中,国家类别存放到一个类别中

代码实现

python 复制代码
def read_data(filename):
    # 1. 初始化两个空列表
    my_list_x, my_list_y = [], []
    # 2. 读取文件内容
    with open(filename,'r', encoding='utf-8') as fr:
        for line in fr.readlines():
            if len(line) <= 5:
                continue
            # strip()方法默认将字符串首尾两端的空白去掉
            x, y = line.strip().split('\t')
            my_list_x.append(x)
            my_list_y.append(y)

    return my_list_x, my_list_y
  • 构建DataSet

目的

properties 复制代码
使用Pytorch框架,一般遵从一个规矩:使用DataSet方法构造数据源,来让模型进行使用
构造数据源的过程中:必须继承torch.utils.data.Dataset类,必须构造两个魔法方法:__len__(), __getitem__()
__len__(): 一般返回的是样本的总个数,我们可以直接len(dataset对象)直接就可以获得结果
__getitem__(): 可以根据某个索引取出样本值,我们可以直接用dataset对象[index]来直接获得结果

代码实现

python 复制代码
class NameClassDataset(Dataset):
    def __init__(self, mylist_x, mylist_y):
        self.mylist_x = mylist_x
        self.mylist_y = mylist_y
        self.sample_len = len(mylist_x)

    # 定义魔法方法len
    def __len__(self):
        return self.sample_len

    # 定义魔法方法getitem
    def __getitem__(self, index):
        # 1.index异常值处理
        index = min(max(index, 0), self.sample_len - 1)
        # 2. 根据index取出人名和国家名
        x = self.mylist_x[index]
        # print(f'x--->{x}')
        y = self.mylist_y[index]
        # print(f'y--->{y}')
        # 3.需要对人名进行one-hot编码表示:这里的思路是:针对每个人名组成的单词进行one-hot,然后再拼接
        tensor_x = torch.zeros(len(x), n_letter)
        # print(f'tensor_x-->{tensor_x}')
        for li, letter in enumerate(x):
            tensor_x[li][all_letters.find(letter)] = 1
       # 4.获取标签
       #  print(f'dataset内部的tensor_x--》{tensor_x.shape}')
        tensor_y = torch.tensor(categorys.index(y), dtype=torch.long)
        # print(f'dataset内部的tensor_y-->{tensor_y}')
        return tensor_x, tensor_y
  • 构建Dataloader

目的

properties 复制代码
为了将Dataset我们上一步构建的数据源,进行再次封装,变成一个迭代器,可以进行for循环,而且,可以自动为我们dataset里面的数据进行增维(bath_size),也可以随机打乱我们的取值顺序

代码实现

python 复制代码
filename = './data/name_classfication.txt'
my_list_x, my_list_y = read_data(filename)
mydataset = NameClassDataset(mylist_x=my_list_x, mylist_y=my_list_y)
my_dataloader = DataLoader(dataset=mydataset, batch_size=1, shuffle=True)
(2)模型搭建
搭建RNN模型
  • 注意事项
properties 复制代码
RNN模型在实例化的时候,默认batch_first=False,因此,需要小心输入数据的形状
因为: dataloader返回的结果x---》shape--〉[batch_size, seq_len, input_size], 所以课堂上代码和讲义稍微有点不同,讲义是默认的batch_first=False,而我们的代码是batch_first=True,这样做的目的,可以直接承接x的输入。
  • 代码实现
python 复制代码
class MyRNN(nn.Module):
    def __init__(self, input_size, hidden_size, ouput_size, num_layers=1):
        super().__init__()
        # input_size 代表词嵌入维度;
        self.input_size = input_size
        # hidden_size代表RNN隐藏层维度
        self.hidden_size = hidden_size
        # output_size代表:国家种类个数
        self.ouput_size = ouput_size
        self.num_layers = num_layers
        # 定义RNN网络层
        # 和讲义不一样,我设定了batch_first=True,意味着rnn接受的input第一个参数是batch_size
        self.rnn = nn.RNN(self.input_size, self.hidden_size,
                          num_layers=self.num_layers, batch_first=True)
        # 定义输出网络层
        self.linear = nn.Linear(self.hidden_size, self.ouput_size)

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

    def forward(self, input, hidden):
        # input的shape---》[batch_size, seq_len, input_size] [1, 9, 57]
        # hidden的shape---》[num_layers, batch_size, hidden_size] [1,1,128]

        # 将input和hidden送入RNN模型得到结果rnn_output【1,9,128】,rnn_hn[1,1,128]
        rnn_output, rnn_hn = self.rnn(input, hidden)
        # print(f'rnn_output--》{rnn_output.shape}')
        # temp:[1, 128]
        tmep = rnn_output[0][-1].unsqueeze(0)
        # print(f'tmep--》{tmep.shape}')
        # 将临时tmep:代表当前样本最后一词的隐藏层输出结果[1, 18]
        output = self.linear(tmep)
        # print(f'output--》{output.shape}')
        # 经过softmax
        return self.softmax(output), rnn_hn

    def inithidden(self):
        return torch.zeros(self.num_layers, 1, self.hidden_size)
  • RNN模型测试
python 复制代码
def test_RNN():
    # 1.得到数据
    my_dataloader = get_dataloader()
    # 2.实例化模型
    input_size = n_letter # 57
    hidden_size = 128 # 自定设定RNN模型输出结果维度
    output_size = len(categorys) # 18
    my_rnn = MyRNN(input_size, hidden_size, output_size)
    h0 = my_rnn.inithidden()
    # 3.将数据送入模型
    for i, (x, y) in enumerate(my_dataloader):
        print(f'x--->{x.shape}')
        output, hn = my_rnn(input=x, hidden=h0)
        print(f'output模型输出结果-->{output.shape}')
        print(f'hn-->{hn.shape}')
        break
搭建LSTM模型
  • 注意事项
properties 复制代码
LSTM模型在实例化的时候,默认batch_first=False,因此,需要小心输入数据的形状
因为: dataloader返回的结果x---》shape--〉[batch_size, seq_len, input_size], 所以课堂上代码和讲义稍微有点不同,讲义是默认的batch_first=False,而我们的代码是batch_first=True,这样做的目的,可以直接承接x的输入。
  • 代码实现
python 复制代码
class MyLSTM(nn.Module):
    def __init__(self, input_size, hidden_size, ouput_size, num_layers=1):
        super().__init__()
        # input_size 代表词嵌入维度;
        self.input_size = input_size
        # hidden_size代表RNN隐藏层维度
        self.hidden_size = hidden_size
        # output_size代表:国家种类个数
        self.ouput_size = ouput_size
        self.num_layers = num_layers
        # 定义LSTM网络层
        # 和讲义不一样,我设定了batch_first=True,意味着rnn接受的input第一个参数是batch_size
        self.lstm = nn.LSTM(self.input_size, self.hidden_size,
                            num_layers=self.num_layers, batch_first=True)
        # 定义输出网络层
        self.linear = nn.Linear(self.hidden_size, self.ouput_size)

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

    def forward(self, input, hidden, c0):
        # input的shape---》[batch_size, seq_len, input_size] [1, 9, 57]
        # hidden的shape---》[num_layers, batch_size, hidden_size] [1,1,128]

        # 将input和hidden送入RNN模型得到结果rnn_output【1,9,128】,rnn_hn[1,1,128]
        lstm_output, (lstm_hn, lstm_cn) = self.lstm(input, (hidden, c0))
        # print(f'rnn_output--》{rnn_output.shape}')
        # temp:[1, 128]
        tmep = lstm_output[0][-1].unsqueeze(0)
        # print(f'tmep--》{tmep.shape}')
        # 将临时tmep:代表当前样本最后一词的隐藏层输出结果[1, 18]
        output = self.linear(tmep)
        # print(f'output--》{output.shape}')
        # 经过softmax
        return self.softmax(output), lstm_hn, lstm_cn

    def 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
  • LSTM测试
python 复制代码
def test_LSTM():
    # 1.得到数据
    my_dataloader = get_dataloader()
    # 2.实例化模型
    input_size = n_letter # 57
    hidden_size = 128 # 自定设定LSTM模型输出结果维度
    output_size = len(categorys) # 18
    my_lstm = MyLSTM(input_size, hidden_size, output_size)
    h0, c0 = my_lstm.inithidden()
    # 3.将数据送入模型
    for i, (x, y) in enumerate(my_dataloader):
        print(f'x--->{x.shape}')
        output, hn, cn = my_lstm(input=x, hidden=h0, c0=c0)
        print(f'output模型输出结果-->{output.shape}')
        print(f'hn-->{hn.shape}')
        print(f'cn-->{cn.shape}')
        break
搭建GRU模型
  • 注意事项
properties 复制代码
GRU模型在实例化的时候,默认batch_first=False,因此,需要小心输入数据的形状
因为: dataloader返回的结果x---》shape--〉[batch_size, seq_len, input_size], 所以课堂上代码和讲义稍微有点不同,讲义是默认的batch_first=False,而我们的代码是batch_first=True,这样做的目的,可以直接承接x的输入。
  • 代码实现
python 复制代码
class MyGRU(nn.Module):
    def __init__(self, input_size, hidden_size, ouput_size, num_layers=1):
        super().__init__()
        # input_size 代表词嵌入维度;
        self.input_size = input_size
        # hidden_size代表RNN隐藏层维度
        self.hidden_size = hidden_size
        # output_size代表:国家种类个数
        self.ouput_size = ouput_size
        self.num_layers = num_layers
        # 定义GRU网络层
        # 和讲义不一样,我设定了batch_first=True,意味着rnn接受的input第一个参数是batch_size
        self.gru = nn.GRU(self.input_size, self.hidden_size,
                          num_layers=self.num_layers, batch_first=True)
        # 定义输出网络层
        self.linear = nn.Linear(self.hidden_size, self.ouput_size)

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

    def forward(self, input, hidden):
        # input的shape---》[batch_size, seq_len, input_size] [1, 9, 57]
        # hidden的shape---》[num_layers, batch_size, hidden_size] [1,1,128]

        # 将input和hidden送入RNN模型得到结果rnn_output【1,9,128】,rnn_hn[1,1,128]
        gru_output, gru_hn = self.gru(input, hidden)
        # print(f'rnn_output--》{rnn_output.shape}')
        # temp:[1, 128]
        tmep = gru_output[0][-1].unsqueeze(0)
        # print(f'tmep--》{tmep.shape}')
        # 将临时tmep:代表当前样本最后一词的隐藏层输出结果[1, 18]
        output = self.linear(tmep)
        # print(f'output--》{output.shape}')
        # 经过softmax
        return self.softmax(output), gru_hn

    def inithidden(self):
        return torch.zeros(self.num_layers, 1, self.hidden_size)
  • GRU测试
python 复制代码
def test_GRU():
    # 1.得到数据
    my_dataloader = get_dataloader()
    # 2.实例化模型
    input_size = n_letter # 57
    hidden_size = 128 # 自定设定RNN模型输出结果维度
    output_size = len(categorys) # 18
    my_gru = MyGRU(input_size, hidden_size, output_size)
    # 2.1 初始化参数
    h0 = my_gru.inithidden()
    # 3.将数据送入模型
    for i, (x, y) in enumerate(my_dataloader):
        print(f'x--->{x.shape}')
        output, hn = my_gru(input=x, hidden=h0)
        print(f'output模型输出结果-->{output.shape}')
        print(f'hn-->{hn.shape}')
        break

5.3 模型训练

  • 基本过程
properties 复制代码
1.获取数据
2.构建数据源Dataset
3.构建数据迭代器Dataloader
4.加载自定义的模型
5.实例化损失函数对象
6.实例化优化器对象
7.定义打印日志参数
8.开始训练
8.1 实现外层大循环epoch
(可以在这构建数据迭代器Dataloader)
8.2 内部遍历数据迭代球dataloader
8.3 将数据送入模型得到输出结果
8.4 计算损失
8.5 梯度清零: optimizer.zero_grad()
8.6 反向传播: loss.backward()
8.7 参数更新(梯度更新): optimizer.step()
8.8 打印训练日志
9. 保存模型: torch.save(model.state_dict(), "model_path")
  • 代码实现
python 复制代码
# 思路分析
# a 从文件获取数据、实例化数据源对象nameclassdataset 数据迭代器对象mydataloader
# b 实例化模型对象my_rnn 损失函数对象mycrossentropyloss=nn.NLLLoss() 优化器对象myadam
# c 定义模型训练的参数
#       starttime total_iter_num total_loss  total_loss_list total_acc_num  total_acc_list
# d 外层for循环 控制轮数 for epoch_idx in range(epochs)
# e 内层for循环 控制迭代次数 for i, (x, y) in enumerate(mydataloader)
# 给模型喂数据   # 计算损失  # 梯度清零 # 反向传播  # 梯度更新
# 计算辅助信息   # 累加总损失和准确数 每100次训练计算一个总体平均损失 总体平均准确率 每2000次训练 打印日志
# 其他          # 预测对错 i_predit_tag = (1 if torch.argmax(output).item() == y.item() else 0)

# f 模型保存
# torch.save(my_rnn.state_dict(), './my_rnn_model_%d.bin' % (epoch_idx + 1))
# g 返回平均损失列表total_loss_list, 时间total_time, 平均准确total_acc_list

# 定义全局变量
epochs = 1
my_lr = 1e-3


# 9.RNN模型训练函数
def train_my_rnn():
    # 1. 读取数据
    my_list_x, my_list_y = read_data(filename='./data/name_classfication.txt')
    # 2. 实例化自己定义的Dataset
    myDataset = NameClassDataset(mylist_x=my_list_x, mylist_y=my_list_y)
    # 3. 实例化自己的Dataloader
    my_dataloader = DataLoader(dataset=myDataset, batch_size=1, shuffle=True)
    # 4.  实例化自己定义的RNN模型
    # 4.1 定义参数
    input_size = 57  # n_letters
    hidden_size = 128
    output_size = 18  # 国家类别
    my_rnn = MyRNN(input_size, hidden_size, output_size)
    # 5. 实例化损失函数对象
    my_crossentropy = nn.NLLLoss()
    # 6. 实例化优化器
    my_optimizer = optim.Adam(my_rnn.parameters(), lr=my_lr)
    # 7. 设置训练的打印日志参数
    start_time = time.time()  # 开始的时间
    total_iter_num = 0  # 已经训练好的样本数
    total_loss = 0  # 已经训练的总损失
    total_loss_list = []  # 每隔100步存储一下平均损失
    total_acc_num = 0  # 已经训练的样本数预测正确的样本
    total_acc_list = []  # 每隔100步存储一下平均准确率
    # 8.开始训练
    # 8.1 外层for训练(epochs迭代多少轮)
    for epoch_idx in range(epochs):
        # 8.2 内部数据开始迭代(一个epoch)
        for i, (x, y) in enumerate(tqdm(my_dataloader)):
            # 8.3 将x送入模型(自己定义的模型)
            # 初始化一个h0
            h0 = my_rnn.inithidden()
            output, hn = my_rnn(input=x, hidden=h0)
            # print(f'output--》模型的输出结果:{output}')
            # print(f'y--》真实的样本结果:{y}')
            # 8.4 将output模型预测结果和真实的y做损失计算
            my_loss = my_crossentropy(output, y)
            # print(f'模型的损失--》{my_loss}')
            # 8.5 梯度清零
            my_optimizer.zero_grad()
            # 8.6 反向传播
            my_loss.backward()
            # 8.7 梯度更新
            my_optimizer.step()

            # 打印日志的参数
            total_iter_num += 1  # 计数
            total_loss += my_loss.item()  # 累计损失值
            item1 = 1 if torch.argmax(output, dim=-1).item() == y.item() else 0
            total_acc_num += item1  # 累计正确样本的个数
            # 每隔100步存储一下平均损失和准确率
            if total_iter_num % 100 == 0:
                # 保存平均损失
                loss_avg = total_loss / total_iter_num
                total_loss_list.append(loss_avg)
                # 保存平均准确率
                acc_avg = total_acc_num / total_iter_num
                total_acc_list.append(acc_avg)

            if total_iter_num % 2000 == 0:
                loss_avg = total_loss / total_iter_num
                acc_avg = total_acc_num / total_iter_num
                end_time = time.time()
                use_time = end_time - start_time
                print("当前训练的批次: %d,平均损失: %.5f, 训练时间: %.3f, 准确率: %.2f" % (
                    epoch_idx + 1,
                    loss_avg,
                    use_time,
                    acc_avg
                ))

        # 9. 保存模型
        torch.save(my_rnn.state_dict(), "model/my_rnn.bin")

    # 结束
    all_time = time.time() - start_time
    return total_loss_list, all_time, total_acc_list


# 10.将模型结果保存,方便下次读取
def save_rnn_results():
    # 1.训练模型得到需要的结果
    total_loss_list, all_time, total_acc_list = train_my_rnn()
    # total_loss_list, all_time, total_acc_list = train_my_lstm()
    # total_loss_list, all_time, total_acc_list = train_my_gru()
    # 2. 定义一个字典
    dict1 = {"loss": total_loss_list,
             "time": all_time,
             "acc": total_acc_list}
    # 3. 保存到一个json格式的文件
    with open("./data/rnn_result.json", "w") as fw:
        fw.write(json.dumps(dict1))


# 11.读取json文件内容
def read_json(json_path):
    with open(json_path, "r") as fr:
        results = json.load(fr)
    return results


# 12.LSTM模型训练函数
def train_my_lstm():
    # 1. 读取数据
    my_list_x, my_list_y = read_data(filename='./data/name_classfication.txt')
    # 2. 实例化自己定义的Dataset
    myDataset = NameClassDataset(mylist_x=my_list_x, mylist_y=my_list_y)
    # 3. 实例化自己的Dataloader
    my_dataloader = DataLoader(dataset=myDataset, batch_size=1, shuffle=True)
    # 4.  实例化自己定义的RNN模型
    # 4.1 定义参数
    input_size = 57  # n_letters
    hidden_size = 128
    ouput_size = 18  # 国家类别
    my_lstm = MyLSTM(input_size, hidden_size, ouput_size)
    # 5. 实例化损失函数对象
    my_crossentropy = nn.NLLLoss()
    # 6. 实例化优化器
    my_optimizer = optim.Adam(my_lstm.parameters(), lr=my_lr)
    # 7. 设置训练的打印日志参数
    start_time = time.time()  # 开始的时间
    total_iter_num = 0  # 已经训练好的样本数
    total_loss = 0  # 已经训练的总损失
    total_loss_list = []  # 每隔100步存储一下平均损失
    total_acc_num = 0  # 已经训练的样本数预测正确的样本
    total_acc_list = []  # 每隔100步存储一下平均准确率
    # 8.开始训练
    # 8.1 外层for训练(epochs迭代多少轮)
    for epoch_idx in range(epochs):
        # 8.2 内部数据开始迭代(一个epoch)
        for i, (x, y) in enumerate(tqdm(my_dataloader)):
            # 8.3 将x送入模型(自己定义的模型)
            # 初始化一个h0, c0
            h0, c0 = my_lstm.inithidden()
            output, hn, cn = my_lstm(input=x, hidden=h0, c0=c0)
            # print(f'output--》模型的输出结果:{output}')
            # print(f'y--》真实的样本结果:{y}')
            # 8.4 将output模型预测结果和真实的y做损失计算
            my_loss = my_crossentropy(output, y)
            # print(f'模型的损失--》{my_loss}')
            # 8.5 梯度清零
            my_optimizer.zero_grad()
            # 8.6 反向传播
            my_loss.backward()
            # 8.7 梯度更新
            my_optimizer.step()

            # 打印日志的参数
            total_iter_num += 1  # 计数
            total_loss += my_loss.item()  # 累计损失值
            item1 = 1 if torch.argmax(output, dim=-1).item() == y.item() else 0
            total_acc_num += item1  # 累计正确样本的个数
            # 每隔100步存储一下平均损失和准确率
            if total_iter_num % 100 == 0:
                # 保存平均损失
                loss_avg = total_loss / total_iter_num
                total_loss_list.append(loss_avg)
                # 保存平均准确率
                acc_avg = total_acc_num / total_iter_num
                total_acc_list.append(acc_avg)

            if total_iter_num % 2000 == 0:
                loss_avg = total_loss / total_iter_num
                acc_avg = total_acc_num / total_iter_num
                end_time = time.time()
                use_time = end_time - start_time
                print("当前训练的批次: %d,平均损失: %.5f, 训练时间: %.3f, 准确率: %.2f" % (
                    epoch_idx + 1,
                    loss_avg,
                    use_time,
                    acc_avg
                ))

        # 9. 保存模型
        torch.save(my_lstm.state_dict(), "model/my_lstm.bin")

    # 结束
    all_time = time.time() - start_time
    return total_loss_list, all_time, total_acc_list


# 13.GRU模型训练函数
def train_my_gru():
    # 1. 读取数据
    my_list_x, my_list_y = read_data(filename='./data/name_classfication.txt')
    # 2. 实例化自己定义的Dataset
    myDataset = NameClassDataset(mylist_x=my_list_x, mylist_y=my_list_y)
    # 3. 实例化自己的Dataloader
    my_dataloader = DataLoader(dataset=myDataset, batch_size=1, shuffle=True)
    # 4.  实例化自己定义的RNN模型
    # 4.1 定义参数
    input_size = 57  # n_letters
    hidden_size = 128
    ouput_size = 18  # 国家类别
    my_gru = MyGRU(input_size, hidden_size, ouput_size)
    # 5. 实例化损失函数对象
    my_crossentropy = nn.NLLLoss()
    # 6. 实例化优化器
    my_optimizer = optim.Adam(my_gru.parameters(), lr=my_lr)
    # 7. 设置训练的打印日志参数
    start_time = time.time()  # 开始的时间
    total_iter_num = 0  # 已经训练好的样本数
    total_loss = 0  # 已经训练的总损失
    total_loss_list = []  # 每隔100步存储一下平均损失
    total_acc_num = 0  # 已经训练的样本数预测正确的样本
    total_acc_list = []  # 每隔100步存储一下平均准确率
    # 8.开始训练
    # 8.1 外层for训练(epochs迭代多少轮)
    for epoch_idx in range(epochs):
        # 8.2 内部数据开始迭代(一个epoch)
        for i, (x, y) in enumerate(tqdm(my_dataloader)):
            # 8.3 将x送入模型(自己定义的模型)
            # 初始化一个h0
            h0 = my_gru.inithidden()
            output, hn = my_gru(input=x, hidden=h0)
            # print(f'output--》模型的输出结果:{output}')
            # print(f'y--》真实的样本结果:{y}')
            # 8.4 将output模型预测结果和真实的y做损失计算
            my_loss = my_crossentropy(output, y)
            # print(f'模型的损失--》{my_loss}')
            # 8.5 梯度清零
            my_optimizer.zero_grad()
            # 8.6 反向传播
            my_loss.backward()
            # 8.7 梯度更新
            my_optimizer.step()

            # 打印日志的参数
            total_iter_num += 1  # 计数
            total_loss += my_loss.item()  # 累计损失值
            item1 = 1 if torch.argmax(output, dim=-1).item() == y.item() else 0
            total_acc_num += item1  # 累计正确样本的个数
            # 每隔100步存储一下平均损失和准确率
            if total_iter_num % 100 == 0:
                # 保存平均损失
                loss_avg = total_loss / total_iter_num
                total_loss_list.append(loss_avg)
                # 保存平均准确率
                acc_avg = total_acc_num / total_iter_num
                total_acc_list.append(acc_avg)

            if total_iter_num % 2000 == 0:
                loss_avg = total_loss / total_iter_num
                acc_avg = total_acc_num / total_iter_num
                end_time = time.time()
                use_time = end_time - start_time
                print("当前训练的批次: %d,平均损失: %.5f, 训练时间: %.3f, 准确率: %.2f" % (
                    epoch_idx + 1,
                    loss_avg,
                    use_time,
                    acc_avg
                ))

        # 9. 保存模型
        torch.save(my_gru.state_dict(), "model/my_gru_%s.bin" % (epoch_idx + 1))

    # 结束
    all_time = time.time() - start_time
    return total_loss_list, all_time, total_acc_list


# 14.画图
def plt_RNN():
    # 获取rnn模型的输出结果
    rnn_results = read_json(json_path="./data/rnn_result-epoch3.json")
    total_loss_list_rnn, all_time_rnn, total_acc_list_rnn = rnn_results["loss"], rnn_results["time"], rnn_results["acc"]
    lstm_results = read_json(json_path="./data/lstm_result-epoch3.json")
    total_loss_list_lstm, all_time_lstm, total_acc_list_lstm = lstm_results["loss"], lstm_results["time"], lstm_results[
        "acc"]
    gru_results = read_json(json_path="./data/gru_result-epoch3.json")
    total_loss_list_gru, all_time_gru, total_acc_list_gru = gru_results["loss"], gru_results["time"], gru_results["acc"]
    # 画出损失对比曲线图
    plt.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('./picture/loss.png')
    plt.show()
    # 画出时间对比图
    plt.figure(1)
    x_data = ["RNN", "LSTM", "GRU"]
    y_data = [all_time_rnn, all_time_lstm, all_time_gru]
    plt.bar(range(len(x_data)), y_data, tick_label=x_data)
    plt.savefig("./picture/use_time.png")
    plt.show()
    # 画出准确率对比曲线图
    plt.figure(0)
    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('./picture/acc.png')
    plt.show()


# 15. 定义sequence2tensor():
def line2tensor(x):
    '''
    x :相当于输入的人名"zhang" 需要对它进行one-hot
    '''
    # 1.初始化一个全零的张量 n_letter=57
    tensor_x = torch.zeros(len(x), n_letter)
    # 2.进行one-hot张量的表示
    for li, letter in enumerate(x):
        tensor_x[li][all_letters.find(letter)] = 1
    return tensor_x

5.4 模型预测

  • 基本过程
properties 复制代码
1.获取数据
2.数据预处理:将数据转化one-hot编码
3.实例化模型
4.加载模型训练好的参数: model.load_state_dict(torch.load("model_path"))
5.with torch.no_grad():
6.将数据送入模型进行预测(注意:张量的形状变换)
  • 代码实现
python 复制代码
# 16.实现rnn模型的预测函数
def rnn_predict(x):
    '''
       x :相当于输入的人名"zhang"
    '''
    # 1.对x进行one-hot编码
    tensor_x = line2tensor(x)
    # 2.实例化模型
    my_rnn = MyRNN(input_size=57, hidden_size=128, ouput_size=18)
    my_rnn.load_state_dict(torch.load("./model/my_rnn.bin"))
    # 3.开始预测
    with torch.no_grad():
        # 将数据送入模型:需要将tensor_x升成3维
        input0 = tensor_x.unsqueeze(0)
        h0 = my_rnn.inithidden()
        # output-->[1, 18]
        output, hn = my_rnn(input0, h0)
        # print(f'output-->{output}')
        # 取出output的最大概率的前三个值
        topv, topi = output.topk(3, 1, True)
        # print(f'topv-->{topv}')
        # print(f'topi-->{topi}')
        print("%s这个人名rnn模型的预测结果" % (x))
        for i in range(3):
            value = topv[0][i]
            index = topi[0][i]
            category = categorys[index]
            print('RNN模型预测的结果:%.2f, 国家类别是%s' % (value, category))


# 17.实现lstm模型的预测函数
def lstm_predict(x):
    '''
    x :相当于输入的人名"zhang"
    '''
    # 1.对x进行one-hot编码
    tensor_x = line2tensor(x)
    # 2. 实例化模型
    my_lstm = MyLSTM(input_size=57, hidden_size=128, ouput_size=18)
    my_lstm.load_state_dict(torch.load("./model/my_lstm.bin"))
    # 3.开始预测
    with torch.no_grad():
        # 将数据送入模型:需要将tensor_x升成3维
        input0 = tensor_x.unsqueeze(0)
        h0, c0 = my_lstm.inithidden()
        # output-->[1, 18]
        output, hn, cn = my_lstm(input0, h0, c0)
        # print(f'output-->{output}')
        # 取出output的最大概率的前三个值
        topv, topi = output.topk(3, 1, True)
        # print(f'topv-->{topv}')
        # print(f'topi-->{topi}')
        print("%s这个人名lstm模型的预测结果" % (x))
        for i in range(3):
            value = topv[0][i]
            index = topi[0][i]
            category = categorys[index]
            print('LSTM模型预测的结果:%.2f, 国家类别是%s' % (value, category))


# 18.实现rnn模型的预测函数
def gru_predict(x):
    '''
    x :相当于输入的人名"zhang"
    '''
    # 1.对x进行one-hot编码
    tensor_x = line2tensor(x)
    # 2. 实例化模型
    my_gru = MyGRU(input_size=57, hidden_size=128, ouput_size=18)
    my_gru.load_state_dict(torch.load("./model/my_gru.bin"))
    # 3.开始预测
    with torch.no_grad():
        # 将数据送入模型:需要将tensor_x升成3维
        input0 = tensor_x.unsqueeze(0)
        h0 = my_gru.inithidden()
        # output-->[1, 18]
        output, hn = my_gru(input0, h0)
        # print(f'output-->{output}')
        # 取出output的最大概率的前三个值
        '''
        output.topk(3, 1, True):这是PyTorch中的一个方法,用于找出给定张量中指定维度上最大的k个元素及其索引。
        第一个参数3:这表示我们想要找到每个时间步(或序列的每个元素)上概率最高的3个值。
        第二个参数1:这指定了我们要在哪个维度上查找最大的值。在这个上下文中,它通常意味着在输出特征的维度上查找
        (即,如果output的形状是(batch_size, num_classes),则1表示在每个batch_size的num_classes中查找最大的3个值)
        第三个参数True:这表示我们想要得到最大值的索引,而不仅仅是值本身。
        topv:这是包含每个时间步上最大概率的3个值的张量。
        topi:这是包含这些最大值对应索引的张量,即每个时间步上概率最高的3个类别的索引。
        '''
        topv, topi = output.topk(3, 1, True)
        print(f'topv-->{topv}')
        print(f'topi-->{topi}')
        print("%s这个人名gru模型的预测结果" % (x))
        for i in range(3):
            value = topv[0][i]
            index = topi[0][i]
            category = categorys[index]
            print('GRU模型预测的结果:%.2f, 国家类别是%s' % (value, category))

6 注意力机制

【注意力机制分为几类,每类有什么特点】

复制代码

6.1 注意力机制的由来

properties 复制代码
引入Attention的原因1: 1、在 Attention 机制引入之前,有一个问题大家一直很苦恼:长距离的信息会被弱化,就好像记忆能力弱的人,记不住过去的事情是一样的。
引入Attention的原因2: 参数少、速度快、效果好
最初场景问题: 文本翻译问题
	问题1: 如果翻译的句子很长很复杂,比如直接一篇文章输进去,模型的计算量很大,并且模型的准确率下降严重
	问题2: 没有考虑词与词之间的相关性,导致翻译效果比较差

6.2 什么是注意力机制

6.3 实例解释Attention

properties 复制代码
 通俗理解注意力机制原理与本质:Q(query)、K(key)、V(value)
  - 第一步【查询】:打开京东网站,我们输入查询(query,Q),比如"笔记本"
  - 第二步【计算相似性】:京东后台拿到这个查询Q,会用这个查询Q去和后台的所有商品的关键字(或者title)(key, K)一一来对比,找到物品和我们查询的相似性(或者说物品对应的相似性的权重),相似性越高,越可能推送给我们
  - 第三步【得到价值】:这个时候我们还需要考虑物品的价值(value, V),这个V不是指物品值几块钱,而是这个物品在算法中的价值。如果商家给了京东广告费,或者商品物美价廉,评论好,点赞高等,那么算法就越有可能把物品排在前面推送给我们
  - 第四步【计算带权重的价值】:我们拿刚刚得到的相似性,乘以物品在算法中的价值V,计算结果就是每件物品的最后带有相似性权重的价值,京东最后的算法就是返回这个带权重的价值,也就是把排序好的商品推送给我们
  - **【总结】**:这是个典型的注意力过程,它推送在最前面给我们展示的商品,肯定是它最希望获得我们注意力的商品。当然,京东内部的算法肯定不是这样的,但是他们本质原理一样,都是基于注意力,并且我们看到的现象也是一样的。 

6.4 Attention概念

properties 复制代码
"注意力机制"实际上就是想将人的感知方式、注意力的行为应用在机器上,让机器学会去感知数据中的重要和不重要的部分。

6.5 注意力机制分类

properties 复制代码
1、软注意力: 注意力权重值分布在0-1之间,关注所有的词汇,但是不同词汇根据权重大小关注的程度不一样。
2、硬注意力: 注意力权重值是0或者1,只关注哪些重要的部分,忽略次要的部分
3、自注意力: 通过输入项内部的"表决"来决定应该关注哪些输入项.

6.6 注意力计算步骤(常见三步走战略)

Attention计算构成元素

properties 复制代码
query(Q)、key(K)、value(V)

(计算规则的普遍性表达)

properties 复制代码
第一步: query和key进行相似度计算,得到attention_score
第二步: 对attention_score进行softmax归一化得到权重值,压缩数值到0-1之间
第三步: 对上一步的权重值和value进行融合,得到具有权重信息的新value

properties 复制代码
基本步骤
第一步: 根据注意力计算规则, 对Q,K,V进行相应的计算.
第二步: 根据第一步采用的计算方法, 如果是拼接方法,则需要将Q与第一步的计算结果再进行拼接, 如果是转置点积, 一般是自注意力, Q与V相同, 则不需要进行与Q的拼接.
第三步: 最后为了使整个attention机制按照指定尺寸输出, 使用线性层作用在第二步的结果上做一个线性变换, 得到最终对Q的注意力表示.

6.7 注意力计算规则

计算规则前提

properties 复制代码
必须有指定的数据: Q、K、V;当输入的Q=K=V时(或者Q\K\V来自于同一个X), 称作自注意力计算规则;当Q、K、V不相等时称为一般注意力计算规则

三种规则方法

properties 复制代码
第一种方法: 将Q和K进行纵轴拼接,然后经过线性变换,再经过Softmax进行处理得到权重,最后和V进行相乘
properties 复制代码
第二种方法: 将Q和K进行纵轴拼接,接着经过一次线性变化,然后进过tanh激活函数处理,再进行sum求和,再经过softmax进行处理得到权重,最后和V进行张量的乘法
properties 复制代码
第三种方法: 将Q和K的转置进行点乘,然后除以一个缩放系数,再经过softmax进行处理得到权重,最后和V进行张量的乘法

6.8 注意力机制的作用

概念

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

6.9 注意力机制实现步骤(深度学习中)

properties 复制代码
第一步: 按照注意力规则,对Q、K、V进行注意力的计算
第二步: 如果第一步是拼接操作,需要将Q和第一步计算的结果进行再次拼接,如果是点乘运算,Q和K、V相等,一般属于自注意力,不需要拼接
第三步: 我们需要将第二步的结果,进行线性变化,按照指定输出维度进行结果的表示

代码实现

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

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

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

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

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

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

        # 4 返回注意力结果表示output:[1,1,32], 注意力权重分布attn_weights:[1,32]
        return output, attn_weights

8 SeqSeq英译法案例

8.1 任务目的

properties 复制代码
目的: 给定一段英文,翻译为法文
典型的文本分类任务: 每个时间步去预测应该属于哪个法文单词

8.2 数据格式

  • 注意:两列数据,第一列是英文文本,第二列是法文文本,中间用制表符号"\t"隔开
properties 复制代码
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 .

8.3 实现流程

properties 复制代码
1. 获取数据:案例中是直接给定的
2. 数据预处理: 脏数据清洗、数据格式转换、数据源Dataset的构造、数据迭代器Dataloader的构造
3. 模型搭建: 编码器和解码器等一系列模型
4. 模型评估(测试)
5. 模型上线---API接口

8.4 数据预处理

4.1 定义样本清洗函数和构建字典
  • 目的
properties 复制代码
样本清洗函数: 将脏数据进行清洗,以免影响模型训练
构建字典:一方面是为了将文本进行数字表示,还有一方面进行解码的时候将预测索引数字映射为真实的文本
  • 样本清洗函数代码实现
python 复制代码
# 文本清洗工具函数
def normalizeString(s):
    """字符串规范化函数, 参数s代表传入的字符串"""
    s = s.lower().strip()
    # 在.!?前加一个空格  这里的\1表示第一个分组   正则中的\num
    s = re.sub(r"([.!?])", r" \1", s)
    # s = re.sub(r"([.!?])", r" ", s)
    # 使用正则表达式将字符串中 不是 大小写字母和正常标点的都替换成空格
    s = re.sub(r"[^a-zA-Z.!?]+", r" ", s)
    return s
  • 构建字典代码实现
python 复制代码
def my_getdata():
    # 1.读取数据
    with open(data_path, 'r' , encoding='utf-8') as fr:
        sentens_str = fr.read()
    sentences = sentens_str.strip().split('\n')

    # 2.构建数据源pair
    my_pairs = [[normalizeString(s) for s in l.split('\t')] for l in sentences]

    # 3.1 初始化两个字典
    english_word2index = {"SOS": 0, "EOS": 1}
    english_word_n = 2
    french_word2index =  {"SOS": 0, "EOS": 1}
    french_word_n = 2
    # 3.2遍历my_pairs
    for pair in my_pairs:
        for word in pair[0].split(' '):
            if word not in english_word2index:
                english_word2index[word] = english_word_n
                english_word_n += 1
                # english_word2index[word] = len(english_word2index)

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

    english_index2word = {v: k for k, v in english_word2index.items()}
    french_index2word = {v: k for k, v in french_word2index.items()}
    return english_word2index, english_index2word, english_word_n, french_word2index, french_index2word, french_word_n, my_pairs
4.2 构建DataSet
  • 目的
properties 复制代码
使用Pytorch框架,一般遵从一个规矩:使用DataSet方法构造数据源,来让模型进行使用
构造数据源的过程中:必须继承torch.utils.data.Dataset类,必须构造两个魔法方法:__len__(), __getitem__()
__len__(): 一般返回的是样本的总个数,我们可以直接len(dataset对象)直接就可以获得结果
__getitem__(): 可以根据某个索引取出样本值,我们可以直接用dataset对象[index]来直接获得结果
  • 代码实现
python 复制代码
# 3.构建数据源Dataset
class Seq2SeqDaset(Dataset):
    def __init__(self, my_pairs):
        self.my_pairs = my_pairs
        self.sample_len = len(my_pairs)

    def __len__(self):
        return self.sample_len

    def __getitem__(self, index):
        # 1.index异常值处理[0, self.sample_len-1]
        index = min(max(index, 0), self.sample_len-1)

        # 2. 根据index取出样本数据
        x = self.my_pairs[index][0]
        y = self.my_pairs[index][1]

        # 3.进行文本数据数字化的转换
        x1 = [english_word2index[word] for word in x.split(' ')]
        tensor_x = torch.tensor(x1, dtype=torch.long, device=device)

        y1 = [french_word2index[word] for word in y.split(' ')]
        y1.append(EOS_token)
        tensor_y = torch.tensor(y1, dtype=torch.long, device=device)

        return tensor_x, tensor_y
4.3 构建Dataloader
  • 目的
properties 复制代码
为了将Dataset我们上一步构建的数据源,进行再次封装,变成一个迭代器,可以进行for循环,而且,可以自动为我们dataset里面的数据进行增维(bath_size),也可以随机打乱我们的取值顺序
  • 代码实现
python 复制代码
# 4.构建数据迭代器dataloader
def get_dataloader():
    # 1.实例化dataset
    my_dataset = Seq2SeqDaset(my_pairs)
    # 2.实例化dataloader
    my_dataloader = DataLoader(dataset=my_dataset, batch_size=1, shuffle=True)

    return my_dataloader

8.5 模型搭建

5.1 搭建编码器GRU模型
  • 注意事项
properties 复制代码
GRU模型在实例化的时候,默认batch_first=False,因此,需要小心输入数据的形状
因为: dataloader返回的结果x---》shape--〉[batch_size, seq_len, input_size], 所以课堂上代码和讲义稍微有点不同,讲义是默认的batch_first=False,而我们的代码是batch_first=True,这样做的目的,可以直接承接x的输入。
  • 代码实现
python 复制代码
# 5. 构建GRU编码器模型
class EncoderGRU(nn.Module):
    def __init__(self, vocab_size, hidden_size):
        super().__init__()
        # 1.vocab_size代表英文单词的总个数(去重)
        self.vocab_size = vocab_size
        # 2. hidden_size词嵌入维度/隐藏层输出维度(我们让他相等)
        self.hidden_size = hidden_size
        # 3.定义Embedding层,目的:将每个词汇进行向量表示
        self.embed = nn.Embedding(self.vocab_size, self.hidden_size)
        # 4.定义GRU层第一个self.hidden_size实际上是embedding的输出结果词嵌入维度
        # 4.定义GRU层第二个self.hidden_size实是我们指定的GRU模型的输出维度,只不过这里GRU输入和输出一样
        self.gru = nn.GRU(self.hidden_size, self.hidden_size, batch_first=True)
    def forward(self, input, hidden):
        # 1.input-->[1, 6]需要经过embedding--》[1,6, 256]
        input_x = self.embed(input)
        # 2.将input_x和hidden送入GRU模型
        output, hidden = self.gru(input_x, hidden)
        return output, hidden

    def inithidden(self):
        return torch.zeros(1, 1, self.hidden_size, device=device)
  • 编码器模型测试
python 复制代码
# 测试编码器模型
def test_EncoderGRU():
    # 获取数据
    my_dataloader = get_dataloader()
    # 实例化模型
    my_encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256)
    my_encoder.to(device)
    # 初始化h0
    h0 = my_encoder.inithidden()

    for i, (x, y) in enumerate(my_dataloader):
        print(f'x---》{x.shape}')
        output, hn = my_encoder(x, h0)
        print(f'output--》{output.shape}')
        print(f'hn--》{hn.shape}')
        break
5.2 搭建解码器无Attention模型
  • 代码实现
python 复制代码
#6. 构建没有attention的GRU解码器
class DecoderGRU(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(self.vocab_size, self.hidden_size)

        # GRU层:这里定义GRU模型的输入和输出形状一样
        self.gru = nn.GRU(self.hidden_size,  self.hidden_size, batch_first=True)

        # 定义输出层:判断法语单词属于self.vocab_size里面的哪一个
        self.out = nn.Linear(self.hidden_size, self.vocab_size)

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

    def forward(self, input, hidden):
        # input输入一般是一个字,解码的时候,是一个字符一个字符解码的
        # 1. input-->[1, 1]-->embeding之后--》[1, 1, hidden_size]-->[1, 1, 256]
        input_x = self.embed(input)
        # 2. relu激活函数使用
        input_x = F.relu(input_x)
        # 3. 将数据送入GRU模型:input_x-->[1,1,256],hidden:[1, 1, 256]
        # output-->[1, 1, 256]
        output, hidden = self.gru(input_x, hidden)

        # 4. 将output结果取最后一个词隐藏层输出送入linear层
        # output-->[1, vocab_size]-->[1, 4345]
        output = self.out(output[0])
        return self.softmax(output), hidden

    def inithidden(self):
        return torch.zeros(1, 1, self.hidden_size, device=device)
  • 代码测试
python 复制代码
# 测试没有attention的解码器
def test_DecoderGRU():
    # 1.实例化dataset
    mydataset = Seq2SeqDaset(my_pairs)
    # 2.实例化dataloader
    my_dataloader = DataLoader(dataset=mydataset, batch_size=1, shuffle=True)

    # 3.实例化编码器模型
    my_encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256)
    # my_encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256).to(device)
    my_encoder.to(device)
    # 4. 实例化解码器模型
    my_decoder = DecoderGRU(vocab_size=french_word_n, hidden_size=256)
    my_decoder.to(device)

    # 5.将数据送入模型
    for x, y in my_dataloader:
        print(x)
        print(f'x--->{x.shape}')
        print(f'y--->{y.shape}')
        print(f'y--->{y}')
        # 5.1将x英文原始输入送入编码器模型得到编码结果;hidden就是C
        output, hidden = my_encoder(input=x, hidden=my_encoder.inithidden())
        # 5.2基于C开始一个字符一个字符的去解码
        for i in range(y.shape[1]):
            # print(f'y[0]-->{y[0]}')
            # print(f'y[0][i]-->{y[0][i]}')
            temp = y[0][i].view(1, -1)
            output, hidden = my_decoder(input=temp, hidden=hidden)
            print(f'output--》{output.shape}')
        break
5.3 搭建解码器带Attention模型
  • 注意事项
properties 复制代码
带Attention:需要有三个参数:Q、K、V,在本次案例中Q上一时间步预测的真实结果;K:上一时间步隐藏层输出的结果;V代表编码器的输出结果
  • 代码实现
python 复制代码
# 7.带attention的解码器
class AttentionDecoderGRU(nn.Module):
    def __init__(self, vocab_size, hidden_size, dropout_p=0.1, max_length=MAX_LENGTH):
        super().__init__()
        # vocab_size:属于解码器端,代表法语的总的单词个数
        self.vocab_size = vocab_size
        # hidden_size:代表词嵌入的维度
        self.hidden_size = hidden_size
        # 随机失活概率
        self.dropout_p = dropout_p
        # 最大句子长度:因为训练语料里面不管英文还是法文最大句子长度都不超过10,我们这里限定最大长度,目的是方便计算注意力
        self.max_length = max_length

        # 定义Embedding层
        self.embed = nn.Embedding(self.vocab_size, self.hidden_size)
        # 计算注意力的第一个全连接层:得到权重分数
        self.attn = nn.Linear(self.hidden_size*2, self.max_length)
        # 随机失活层
        self.droupout = nn.Dropout(p=self.dropout_p)
        # 计算注意力的第二个全连接层:让注意力按照指定维度输出
        self.attn_combin = nn.Linear(self.hidden_size*2, self.hidden_size)
        # 定义GRU层
        self.gru = nn.GRU(self.hidden_size, self.hidden_size, batch_first=True)
        # 定义输出层
        self.out = nn.Linear(self.hidden_size, self.vocab_size)

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

    def forward(self, input, hidden, encoder_output):
        # input-->query--》解码器输入某个词[1, 1]
        # hidden-->key--》上一时间步隐藏层输出[1, 1, 256]
        # encoder_output--->value-->编码器的输出结果[10, 256]--[max_length, 256]
        # 1. 将input送入embedding-->input_x-->[1,1,256]--query(真正)
        input_x = self.embed(input)
        # 1.1对input_x进行dropout
        input_x = self.droupout(input_x)
        # 2. 计算注意力权重分数self.attn_weight-->[1, 10]-->[1, max_length]
        attn_weight = F.softmax(self.attn(torch.cat((input_x[0], hidden[0]), dim=-1)), dim=-1)
        # 3. 将注意力权重和Value相乘:[1, 1,10]*[1,10,256]-->self.attn_applied-->[1, 1,256]
        attn_applied = torch.bmm(attn_weight.unsqueeze(0), encoder_output.unsqueeze(0))
        # 4.将query和self.attn_applied结果拼接之后,再经过线性的变换self.output1-->[1, 1, 256]
        output1 = self.attn_combin(torch.cat((input_x[0], attn_applied[0]), dim=-1)).unsqueeze(0)
        # 5. 经过relu激活函数
        relu_output = F.relu(output1)
        # 6. 将self.relu_output,以及hidden送入GRU模型中-->gru_output-->[1, 1,256]
        gru_output, hidden = self.gru(relu_output , hidden)

        # 7.将gru的结果送入输出层,得到最后的预测结果output-->[1, 4345]
        output = self.out(gru_output[0])
        return self.softmax(output), hidden, attn_weight

    def inithidden(self):
        return torch.zeros(1, 1, self.hidden_size, device=device)
  • 模型测试
python 复制代码
# 测试带attention的解码器
def test_AttenDecoder():
    # 1.实例化dataset
    mydataset = Seq2SeqDaset(my_pairs)
    # 2.实例化dataloader
    my_dataloader = DataLoader(dataset=mydataset, batch_size=1, shuffle=True)

    # 3.实例化编码器模型
    my_encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256)
    # my_encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256).to(device)
    my_encoder.to(device)

   # 4.实例化解码器模型
    my_attenDecoder = AttentionDecoderGRU(vocab_size=french_word_n, hidden_size=256)
    my_attenDecoder.to(device)

    #5.循环数据送入模型
    for x, y in my_dataloader:
        print(f'x--》{x.shape}')
        print(f'y--》{y.shape}')
        # 1.将x送入编码器模型得到结果
        h0 = my_encoder.inithidden()
        encoder_output, hidden = my_encoder(input=x, hidden=h0)
        # print(f'encoder_output--》{encoder_output.shape}')
        # 2.将编码的结果进行处理,统一长度,方便计算注意力
        encoder_output_c = torch.zeros(MAX_LENGTH, my_encoder.hidden_size, device=device)
        # print(f'encoder_output_c--》{encoder_output_c.shape}')

        # 2.1将真实的编码的输出 结果赋值到encoder_output_c中,多余的都是用0来表示
        for i in range(encoder_output.shape[1]):
            # print(f'encoder_output[0][i]-->{encoder_output[0][i].shape}')
            # print(f'encoder_output[0, i]-->{encoder_output[0, i].shape}')
            encoder_output_c[i] = encoder_output[0][i]
        # 3.测试:进行解码应用
        for j in range(y.shape[1]):
            temp = y[0][j].view(1, -1)
            output, hidden, attn_weight = my_attenDecoder(temp, hidden, encoder_output_c)
            print(f'output--》{output.shape}')
            print(f'hidden--》{hidden.shape}')
            print(f'attn_weight--》{attn_weight.shape}')
            print("*"*80)
        break

8.6 模型训练

  • 基本过程
properties 复制代码
1.获取数据
2.构建数据源Dataset
3.构建数据迭代器Dataloader
4.实例化自定义的模型: 编码器模型和解码器模型
5.实例化损失函数对象
6.实例化优化器对象: 编码器优化器和解码器优化器
7.定义打印日志参数
8.开始训练
8.1 实现外层大循环epoch
(可以在这构建数据迭代器Dataloader)
8.2 内部遍历数据迭代球dataloader
8.3 将数据送入模型得到输出结果
8.4 计算损失
8.5 梯度清零: optimizer.zero_grad()
8.6 反向传播: loss.backward()
8.7 参数更新(梯度更新): optimizer.step()
8.8 打印训练日志
9. 保存模型: torch.save(model.state_dict(), "model_path")
  • 代码实现
python 复制代码
# 8.构建模型的训练函数
def train_seq2seq():
    # 1.实例化dataset
    mydataset = Seq2SeqDaset(my_pairs)
    # 2.实例化dataloader
    my_dataloader = DataLoader(dataset=mydataset, batch_size=1, shuffle=True)

    # 3.实例化编码器模型
    my_encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256)
    # my_encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256).to(device)
    my_encoder.to(device)

    # 4.实例化解码器模型
    my_attenDecoder = AttentionDecoderGRU(vocab_size=french_word_n, hidden_size=256)
    my_attenDecoder.to(device)

    # 5.实例化优化器
    encoder_optimizer = optim.Adam(my_encoder.parameters(), lr=mylr)
    decoder_optimizer = optim.Adam(my_attenDecoder.parameters(), lr=mylr)

    # 6.实例化损失对象
    crossentropy = nn.NLLLoss()

    # 7.定义一个空列表list--》存储损失值,画图
    plot_loss_list = []

    # 8. 进入外层循环
    for epoch_idx in range(epochs):
        # 初始化损失值为0
        print_loss_total, plot_loss_total = 0.0, 0.0
        start_time = time.time()
        # 进入内部循环
        for i, (x, y) in enumerate(tqdm(my_dataloader), start=1):
            myloss = Train_Iters(x, y, my_encoder,
                                 my_attenDecoder,encoder_optimizer,
                                 decoder_optimizer,crossentropy)
            # print(f'主训练了函数的myloss--》{myloss}')
            print_loss_total += myloss
            plot_loss_total += myloss

            # 打印日志
            # 每隔1000步打印损失
            if i % 10 == 0:
                print_loss_avg = print_loss_total / 1000

                print_loss_total = 0
                use_time = time.time() - start_time
                print(f'当前的轮次%d,平均损失%.4f,时间%.2f'%(epoch_idx+1, print_loss_avg.item()*100, use_time))

            # 每隔100步保留损失,画图
            if i % 10 == 0:
                plot_loss_avg = plot_loss_total / 100
                # 如果画图报错:放到CPU--》plot_loss_avg.cpu().detach().numpy()
                plot_loss_list.append(plot_loss_avg.cpu().detach().numpy())
                plot_loss_total = 0

        # 保存模型
        torch.save(my_encoder.state_dict(), './ai19_model/my_encoder_%s.pth'%(epoch_idx+1))
        torch.save(my_attenDecoder.state_dict(), './ai19_model/my_decoder_%s.pth'%(epoch_idx+1))


    # 画图
    plt.figure()
    plt.plot(plot_loss_list)
    plt.savefig("./ai19_seq2se1_loss.png")
    plt.show()

    return plot_loss_list
  • 模型训练内部迭代函数代码实现
python 复制代码
# 定义内部迭代函数
def Train_Iters(x, y, my_encoder, my_attenDecoder, encoder_optimizer, decoder_optimizer, crossentropy):
    # 1.将x送入编码器得到编码的结果
    # print(f'x-->{x.shape}')
    # print(f'y-->{y.shape}')
    h0 = my_encoder.inithidden()
    encoder_output, encoder_hidden = my_encoder(x, h0)
    # print(f'encoder_output--》{encoder_output.shape}')
    # print(f'encoder_hidden--》{encoder_hidden.shape}')
    # 2. 定义解码器的参数
    # 2.1 中间语意张量C:value
    encoder_output_c = torch.zeros(MAX_LENGTH, my_encoder.hidden_size, device=device)
    for i in range(x.shape[1]):
        encoder_output_c[i] = encoder_output[0][i]
    # 2.2 解码器的初始化的hidden, key
    decoder_hidden = encoder_hidden
    # 2.3 解码器的初始化输出:query
    input_y = torch.tensor([[SOS_token]], dtype=torch.long, device=device)

    # 3.定义一个初始化的损失
    my_loss = 0.0
    # 4.选择性的使用teacher_forcing策略
    teacher_forcing = True if random.random() < teacher_forcing_ratio else False
    # 5.开始计算损失
    if teacher_forcing:
        for i in range(y.shape[1]):
            # output_y--》[1, 4345]
            output_y, decoder_hidden, attn_weight =my_attenDecoder(input_y, decoder_hidden, encoder_output_c)
            # 根据预测结果计算损失
            target_y = y[0][i].view(1)
            # print(f'target_y--》{target_y}')

            my_loss = my_loss + crossentropy(output_y, target_y)
            # print(f'my_loss--》{my_loss}')
            # 将真实的下一个单词当作input_y
            input_y = y[0][i].view(1, -1)
            # print(f'input_y--》{input_y}')
    else:
        for i in range(y.shape[1]):
            # output_y--》[1, 4345]
            output_y, decoder_hidden, attn_weight = my_attenDecoder(input_y, decoder_hidden, encoder_output_c)
            # 根据预测结果计算损失
            target_y = y[0][i].view(1)
            my_loss = my_loss + crossentropy(output_y, target_y)
            topv, topi = output_y.topk(1)
            # 如果output_y预测的最大值对应的索引刚好等EOS,直接终止
            if topi.squeeze().item() == EOS_token:
                break
            # 将预测结果的当作下一个input_y
            input_y = topi.detach()

    # 6. 梯度清零
    encoder_optimizer.zero_grad()
    decoder_optimizer.zero_grad()
    # 7. 反向传播
    my_loss.backward()
    # 8.梯度更新
    encoder_optimizer.step()
    decoder_optimizer.step()

    return my_loss / y.shape[1]

8.7 模型预测

  • 基本过程
properties 复制代码
1.获取数据
2.数据预处理
3.实例化模型: 编码器和解码器
4.加载模型训练好的参数: model.load_state_dict(torch.load("model_path"))
5.with torch.no_grad():
6.将数据送入模型进行预测(注意:张量的形状变换)
  • 代码实现
python 复制代码
# 9.定义模型评估/预测函数
def test_Seq2Seq_Evaluate():
    # 1.加载训练好的编码器模型
    my_encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256).to(device)
    my_encoder.load_state_dict(torch.load("./ai19_model/my_encoder_1.pth"))
    # my_encoder.to(device)
    print(my_encoder)
    # 2.加载训练好的解码器模型
    my_decoder = AttentionDecoderGRU(vocab_size=french_word_n, hidden_size=256).to(device)
    my_decoder.load_state_dict(torch.load("./ai19_model/my_decoder_1.pth"))
    # my_decoder.load_state_dict(torch.load("./ai19_model/my_decoder_1.pth", map_location="cpu"), strict=False)
    print(my_decoder)
    print('*'*80)
    # 3.准备样本

    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))

    # 4. 将样本输入模型得到结果
    for index, pair in enumerate(my_samplepairs[2:]):
        x = pair[0]
        y = pair[1]
        # 需要对x英文文本进行处理
        x_word2id = [english_word2index[word] for word in x.split(' ')]
        # print(f'x_word2id--》{x_word2id}')
        tensor_x = torch.tensor([x_word2id], dtype=torch.long, device=device)
        # print(f'tensor_x-->{tensor_x}')
        decoder_words, attention_weights = evaluate_seq2seq(tensor_x, my_encoder, my_decoder)
        decoder_french = ' '.join(decoder_words)
        print("*"*80)
        print(f'原始的英文输入文本是---》{x}')
        print(f'原始的法文标签文本是---》{y}')
        print(f'模型预测的法文---》{decoder_french}')
  • 内部评估代码实现
python 复制代码
def evaluate_seq2seq(x, encoder, decoder):
    # x代表当前需要翻译的英文文本
    # encoder代表编码器模型
    # decoder代表解码器模型
    # 1. 将x送入编码器得到编码结果
    # print(f'x---》{x.shape}')
    h0 = encoder.inithidden()
    encoder_outputs, encoder_hidden = encoder(x, h0)
    # 2. 准备解码的参数
    # 2.1 编码器结果--Value
    encoder_outputs_c = torch.zeros(MAX_LENGTH, encoder.hidden_size, device=device)
    for index in range(x.shape[1]):
        encoder_outputs_c[index] = encoder_outputs[0][index]
    # 2.2 解码器的key
    decode_hidden = encoder_hidden

    # 2.3 解码器的原始输入
    input_y = torch.tensor([[SOS_token]], dtype=torch.long, device=device)

    # 3.准备变量
    # 3.1 存储模型的预测结果
    decoder_words = []
    # 3.2 初始化一个全0的权重矩阵:存储每一步解码出来的注意力权重
    attention_weights = torch.zeros(MAX_LENGTH, MAX_LENGTH, device=device)
    # 4.进行模型的预测
    index = 0

    for i in range(MAX_LENGTH):
        output_y, decode_hidden, atten = decoder(input_y, decode_hidden, encoder_outputs_c)
        # 获取output_y预测的最大概率值对应索引
        topv, topi  = output_y.topk(1)
        # print(f'topi--》{topi}')
        # print(f'atten-->{atten}')
        # 将真实的注意力权重赋值给attention_weights
        # print(f' attention_weights[i]-->{ attention_weights[i]}')
        attention_weights[i] = atten
        if topi.item() == EOS_token:
            decoder_words.append("<EOS>")
            break
        else:
            decoder_words.append(french_index2word[topi.item()])
        input_y = topi.detach()
        index = i

    print(f'attention_weights--》{attention_weights}')
    return decoder_words, attention_weights[:index+1]
 
  • 注意力图
python 复制代码
# 10.注意力图展示
def test_attention_plot():
    # 1.加载训练好的编码器模型
    my_encoder = EncoderGRU(vocab_size=english_word_n, hidden_size=256).to(device)
    my_encoder.load_state_dict(torch.load("./ai19_model/my_encoder_1.pth"))
    # my_encoder.to(device)
    print(my_encoder)
    # 2.加载训练好的解码器模型
    my_decoder = AttentionDecoderGRU(vocab_size=french_word_n, hidden_size=256).to(device)
    my_decoder.load_state_dict(torch.load("./ai19_model/my_decoder_1.pth"))
    # my_decoder.load_state_dict(torch.load("./ai19_model/my_decoder_1.pth", map_location="cpu"), strict=False)
    print(my_decoder)
    print('*' * 80)
    sentence = "we re both teachers ."
    # 需要对x英文文本进行处理
    x_word2id = [english_word2index[word] for word in sentence.split(' ')]
    # print(f'x_word2id--》{x_word2id}')
    tensor_x = torch.tensor([x_word2id], dtype=torch.long, device=device)
    # print(f'tensor_x-->{tensor_x}')
    decoder_words, attention_weights = evaluate_seq2seq(tensor_x, my_encoder, my_decoder)

    plt.matshow(attention_weights.cpu().detach().numpy())
    plt.savefig('./ai19_attention.png')
    plt.show()
相关推荐
longze_72 小时前
Uigenius:开启 UI 设计革命的中国 AI 智能体
人工智能·ui·ai·ai tools
流云细水2 小时前
Spec实战:需求编辑与需求评审
java·人工智能
大力财经2 小时前
热餐可口 归途无忧!七鲜小厨开进北京南站,首次开辟大交通场景
人工智能
J-TS2 小时前
线性自抗扰控制LADRC
c语言·人工智能·stm32·单片机·算法
Hhang2 小时前
Pageindex -- 新一代的文档智能检索
前端·人工智能
前端付豪2 小时前
LangChain 模型I/O:输入提示、调用模型、解析输出
人工智能·程序员·langchain
瑞华丽PLM2 小时前
守住数字化的胜算:PLM项目实施风险控制全景方案
大数据·人工智能·plm·国产plm·瑞华丽plm·瑞华丽
恋猫de小郭2 小时前
Claude Code 已经 100% 自己写代码,为什么 Anthropic 还有上百个工程职位空缺?
前端·人工智能·ai编程
董厂长2 小时前
用 LangGraph 实现 Small-to-Big 分块检索策略
人工智能·算法·rag