三、深度学习处理文本

11 阅读10分钟

1、梯度计算过程以及Adam优化器细节

梯度计算

概述:梯度计算本质上就是对损失函数进行求导的结果,该结果就是本次机器学习的梯度。 在这里插入图片描述

Adam优化器实现细节

概述:优化器的作用是帮助权重进行更新,根据上面求解的梯度加上学习率进行权重的更新;最简单的优化器有SGD:权重=权重-学习 率*梯度,Adam优化器的更加复杂,更新效果更好的优化器,下面介绍其基本原理。
在这里插入图片描述 针对上面的原理解析用代码进行实现:

import torch
import torch.nn as nn
import numpy as np

# 通过numpy实现整个求导、计算梯度、更新权重的整个过程,并比较torch的结果


# 通过pytorch定义一个模型框架,
class ord_model(nn.Module):
    def __init__(self, input_size, output_size):
        super(ord_model, self).__init__()
        self.linear = nn.Linear(input_size, output_size, bias=False)
        self.activate = nn.Sigmoid()
        self.loss = nn.MSELoss()

    def forward(self, x, y=None):
        x = self.linear(x)
        pred = self.activate(x)
        return self.loss(pred, y)


# 自定义模型,并手动实现其方法
class diy_model:
    def __init__(self, weight, input_x, input_y):
        self.weight = weight
        self.input_x = input_x
        self.input_y = input_y

    # 线性层
    def diy_linear(self):
        weight = self.weight.detach().numpy()
        return np.dot(self.input_x, np.transpose(weight))

    #激活函数
    def diy_sigmoid(self, x):
        return 1/(1+np.exp(-x))

    # 损失函数
    def mes_loss(self, y_pred, y_true):
        return (y_pred-y_true)**2

    # 梯度计算
    def diy_grad(self, y_pred, y, x):
        grad = np.array([[0.0, 0.0], [0.0, 0.0]])
        grad[0][0] = (y_pred[0]-y[0]) * y_pred[0]*(1-y_pred[0]) * x[0]
        grad[0][1] = (y_pred[1]-y[1]) * y_pred[1]*(1-y_pred[1]) * x[0]
        grad[1][0] = (y_pred[0]-y[0]) * y_pred[0]*(1-y_pred[0]) * x[1]
        grad[1][1] = (y_pred[1]-y[1]) * y_pred[1]*(1-y_pred[1]) * x[1]
        return np.transpose(grad)

    # 更新权重
    def update_grad(self, grad):
        self.weight[0][0] = self.weight[0][0] - 1e-3 * grad[0][0]
        self.weight[0][1] = self.weight[0][1] - 1e-3 * grad[0][1]
        self.weight[1][0] = self.weight[1][0] - 1e-3 * grad[1][0]
        self.weight[1][1] = self.weight[1][1] - 1e-3 * grad[1][1]

    # 构建Adam优化器
    def diy_adam(self, grad):
        lr = 1e-3
        beta1 = 0.9
        beta2 = 0.999
        eps = 1e-8
        m = 0
        v = 0
        t = 0
        t += 1
        m = beta1*m + (1-beta1)*grad
        v = beta2*v + (1-beta2)*grad**2
        m_hat = m/(1-beta1**t)
        v_hat = v/(1-beta2**t)
        self.weight = torch.FloatTensor(self.weight)-lr*m_hat/(torch.sqrt(v_hat)+eps)
        return self.weight

    def forward(self):
        self.x = self.diy_linear()
        y_pred = self.diy_sigmoid(self.x)
        loss = np.mean(self.mes_loss(y_pred, self.input_y))
        return loss, self.diy_grad(y_pred, self.input_y, self.input_x)


if __name__ == "__main__":
    x = np.array([1, 2])
    y = np.array([4, 1])
    model = ord_model(2, 2)         #定义模型
    optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)   #定义SGD优化器
    optimizer_adam = torch.optim.Adam(model.parameters(), lr=1e-3)  # 定义Adam优化器
    weight = model.state_dict()['linear.weight']
    print("======================框架初始化权重======================")
    for name, param in model.named_parameters():
        print(f"name:{name}, weight:{param}")

    # 创建自定义模型
    diy_model = diy_model(weight.tolist(), x, y)

    optimizer_adam.zero_grad()   # 更新梯度
    loss = model(torch.FloatTensor(x), torch.FloatTensor(y))
    loss.backward()         # 计算梯度
    grads = {}              # 存放模型产生的梯度
    for name, param in model.named_parameters():
        if param.grad is not None:
            grads[name] = param.grad
    print("======================Adam更新权重======================")
    print(diy_model.diy_adam(grads["linear.weight"]))

    optimizer_adam.step()        # 权重更新

    print("======================框架更新后权重======================")
    for name, param in model.named_parameters():
        print(f"name:{name}, weight:{param}")

    diy_loss, diy_grad = diy_model.forward()
    print("======================比较计算的loss值======================")
    print(f"pytorch框计算的损失结果:{loss}")
    print(f"手动实现结果的损失结果  :{diy_loss}")
    print("======================MSE计算梯度======================")
    print(diy_grad)
    print(model.linear.weight.grad)
    print("======================MSE更新权重======================")
    diy_model.update_grad(diy_grad)
    print(diy_model.weight)

比对其结果:
在这里插入图片描述 学习建议:对于学习到的大模型训练相关组件的知识可以通过python代码自己实现一遍,然后再用框架调取处理一样的数据,检查比对最后的结果看是否和框架的结果一致,若相同则证明自己的学习掌握的没有问题。

2、Embedding层实现细节

说明:Embedding层的主要作用就是将文本字符转换为向量,也就是将离散的数据转换为向量

import torch
import torch.nn as nn


if __name__ == "__main__":
    embed = nn.Embedding(10, 4, padding_idx=0)
    input = torch.tensor([[0, 2, 3, 4], [5, 6, 7, 8]])
    print("=====================Embedding初始化参数=====================")
    print(embed.weight)
    print("=====================Embedding层转换结果=====================")
    output = embed(input)
    print(output)

3、Pooling层实现细节

说明:Pooling层的主要作用是将减小输入数据的大小,减轻模型训练的成本(在不影响模型训练效果的前提下)

import torch
import torch.nn as nn

if __name__ == "__main__":
    embedding = nn.Embedding(10, 4, padding_idx=0)
    pooling = nn.MaxPool2d((4, 1), stride=(1, 1))
    pooling2 = nn.MaxPool1d(2, stride=1)
    pooling1 = nn.AvgPool2d((4, 1), stride=(1, 1))
    pooling3 = nn.AvgPool1d(2, stride=2)
    input = torch.tensor([0, 1, 2, 3])
    output = embedding(input)

    print("=====================池化前结果=====================")
    print(output)
    print("=====================二维最大池化后结果=====================")
    output = output.unsqueeze(0).unsqueeze(0)
    output_pooling = pooling(output)
    print(output_pooling.squeeze())

    print("=====================一维最大池化后结果=====================")
    output_1d = pooling2(output.squeeze(0))
    print(output_1d)

    print("=====================二维平均池化后结果=====================")
    output_pooling1 = pooling1(output)
    print(output_pooling1.squeeze())

    print("=====================一维平均池化后结果=====================")
    output = output.squeeze(0)
    output_pooling3 = pooling3(output)
    print(output_pooling3)

4、RNN

说明:将整个序列划分为多个时间步,每个时间步依次传入模型中,同时将输出结果传入下一个时间步。多用于文本自然语言处理当 中。 在这里插入图片描述

import torch
import torch.nn as nn

if __name__ == "__main__":
    rnn = nn.RNN(5, 6, num_layers=2,batch_first=True)
    # tarh(x*w1+b1+h*w2+b2)
    print(rnn.weight_ih_l0.shape) # 6*5
    print(rnn.bias_ih_l0.shape)   # 1*6
    print(rnn.weight_hh_l0.shape) # 6*6
    print(rnn.bias_hh_l0.shape)   # 1*6
    input = torch.randn(1, 4, 5)
    print(input)
    output, h0 = rnn(input)
    print(output.shape)
    print(h0.shape)

其中需要中点注意的参数是batch_first,该参数的作用是控制数据排列的顺序,若为True则会将批次大小放在第一个维度,若为False则会 将时间步数(即序列中元素的数量)放在第一个维度,总体来讲就是输出更加符合一些数据的加载方式(例如批处理模式)。(大部分任务一般会置为True) 在框架中RNN最终会输出两个结果,其中output是含有每个时间步对应的计算结果;h0含有的是每一层RNN最终输出的结果,其中h0[-1]可以形象的代表为所输入一句话在当前模型结构的浓缩向量表示(类似于映射)。

5、CNN

说明:卷积神经网络的工作原理就是通过初始化的卷积核(一个小型矩阵),然后对输入的目标矩阵中每个卷积核大小的区域与卷积 核做加权求和,同时卷积核就按照指定步长进行滑动,最终得到输入目标的特征矩阵,对这些特征矩阵会将其放入到后面的多个全连 接层进行训练,进而得到可以对输入进行识别的模型。

对一维的卷积神经网络Conv1d()进行重要参数的相关解析:

  • in_channels:输入的通道数,主要作用确定表示输入数据的维度
  • out_channels:输出的通道数,代表有多少个卷积核,其中这所有的卷积核会对每一个通道中的进行卷积,进而每个通道都会产生卷 积核数量的特征图
  • kernel_size:卷积核的大小,一般一维的就是一个数,二维的就是一个(m,n)【处理黑白图片】,三维的卷积神经网络是(x,y,z)【处理 彩图】
  • stride:卷积核滑动的步长 卷积神经网络输出数据的形状为: (batch_size,out_channels,(input_size-kernel_size+2*padding)/stride+1)

6、Normalization(数据归一化)

说明:归一化层的作用主要是对上层输出的数据做标准化,使得模型的训练速度加快、减轻梯度爆炸/梯度消失、防止过拟合;其中优又 会细分为 Batch NormalizationLayear NormalizationInstance Normalization,其中第一个主要用于图像领域,第二个 主要用于文本处理领域,最后一个主要用于图像处理中每个通道中元素的归一化。

Normalization实现细节: 在这里插入图片描述

import torch
import torch.nn as nn

if __name__ == "__main__":
    bn = nn.BatchNorm1d(3)
    input = torch.randn(5, 3)
    print(input)
    output = bn(input)
    print(output)

相较于上面的 Batch Noramlization同一批次元素公用一个均值和方差, Layer Normalization则是对每个单独的元素序列都有一 个均值方差,形象的理解为 Batch Normalization是对整个班级的成绩做平均标准化,而 Layer Normalization则是对每个同学的 成绩做平均标准化。

7、DropOut层

说明:Dropout层的作用就是减少过拟合的情况,对输入元素进行一定概率p的丢弃然后对未丢弃的元素进行等比例的放大为元素乘以 1/(1-p)。

import torch
import torch.nn as nn

if __name__ == "__main__":
    m = nn.Dropout(p=0.6)
    # m.eval()
    input = torch.FloatTensor([1, 2, 3, 4, 5, 6])
    print(input)
    output = m(input)
    print(output)

8、实例模型训练代码实现

用上述pytorch相关组件完成下面的任务, 实现任务:输入一个5-6位的英文字符串,其中含有a的被分为第一类(0),含有b的被分类第二类(1),含有c的被分为第三类(2),上面是一个顺序优先级,第一类优先级最高,第二类次之,第三类最低。

import random

import torch
import torch.nn as nn
import string
import json
import numpy as np
import matplotlib.pyplot as plt


# 实现任务:输入一个5-6位的英文字符串,其中含有a的被分为第一类,含有b的被分类第二类,含有c的被分为第三类
# 构建训练数据
def structure_data(train_number):
    train_input = []
    train_output = []
    for i in range(train_number):
        length = random.choice([5, 6])
        need_char = random.choice('abc')
        surplus_string_list = random.choices(string.ascii_letters, k=length-1)
        random_string_list = surplus_string_list+[need_char]
        random.shuffle(random_string_list)
        random_string = ''.join(random_string_list)
        if 'a' in random_string:
            train_input.append(random_string)
            train_output.append(0)
        elif 'b' in random_string:
            train_input.append(random_string)
            train_output.append(1)
        elif 'c' in random_string:
            train_input.append(random_string)
            train_output.append(2)
    return train_input, torch.LongTensor(train_output)


# 将训练数据转换为对应词表坐标
def convert_str(train_input, vocab):
    result = []
    for str in train_input:
        str_array = list(str)
        for i in range(len(str_array)):
            str_array[i] = vocab.index(str_array[i])+1
        if len(str_array) != 6:
            str_array.append(0)
        result.append(str_array)
    return torch.LongTensor(result)


# 定义网络结构
class RNN_Model(nn.Module):
    def __init__(self, vocab_size, embedding_size):
        super(RNN_Model, self).__init__()
        self.embedding = nn.Embedding(vocab_size+1, embedding_size, padding_idx=0)
        self.rnn = nn.RNN(10, 20, num_layers=2, batch_first=True)
        self.linear = nn.Linear(6*20, 3)
        self.loss = nn.CrossEntropyLoss()

    def forward(self, x, y=None):
        x = self.embedding(x)
        x, _ = self.rnn(x)
        x = x.reshape(x.shape[0], -1)
        x = self.linear(x)
        if y is not None:
            return self.loss(x, y)
        else:
            return x


# 校验模型的准确率
def verify_model(model, vocab):
    verify_input_data, verify_output_data = structure_data(100)
    verify_input_data = convert_str(verify_input_data, vocab)
    correct, worry = 0, 0
    model.eval()
    with torch.no_grad():
        output = model(verify_input_data)
        for y_true, y_pred in zip(verify_output_data, output):
            if np.argmax(y_pred) == y_true:
                correct += 1
            else:
                worry += 1
    return correct/(correct+worry)


# 训练模型
def train_model(model, vocab):
    # 定义外部参数
    epochs = 1000               # 模型训练轮次
    batch_size = 64             # 批次大小
    train_data_number = 6400    # 训练数据量大小
    learning_rate = 1e-3        # 学习率

    # 定义Adam优化器
    optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
    # 准备训练数据
    train_input_data, train_output_data = structure_data(train_data_number)
    train_input_data = convert_str(train_input_data, vocab)
    # 存放训练数据日志
    log = []
    # 训练模型
    for epoch_number in range(epochs):
        model.train()
        loss_value = []
        for i in range(len(train_input_data)//batch_size):
            x = train_input_data[i*batch_size:(i+1)*batch_size]
            y = train_output_data[i*batch_size:(i+1)*batch_size]
            loss = model(x, y)      # 计算损失值
            loss.backward()         # 计算梯度
            optimizer.step()        # 梯度更新
            optimizer.zero_grad()   # 梯度置零
            loss_value.append(loss.item())
        # 计算每轮损失均值以及当前模型的准确率放入日志中
        loss_avg = np.mean(loss_value)
        acc = verify_model(model, vocab)
        log.append([loss_avg, acc])
        print(f'第{epoch_number}轮训练完成!')
        # 判断损失值是否达到阈值
        if loss_avg <= 1e-5:
            print('模型符合要求,结束训练过程!')
            break
    # 保存模型
    torch.save(model.state_dict(), './model/rnn_class_model.bin')

    # 可视化展示损失值以及模型准确率的变化趋势
    plt.plot(list(range(len(log))), [loss[0] for loss in log], label='loss', color='red')
    plt.plot(list(range(len(log))), [acc[1] for acc in log], label='acc', color='blue')
    plt.legend()
    plt.title('Change Trend')
    plt.show()

# 调用模型进行预测
def prediction(model_path, vocab, predict_data):
    model = RNN_Model(len(vocab), 10)
    model.load_state_dict(torch.load(model_path, weights_only=True))
    data = convert_str(predict_data, vocab)
    model.eval()
    with torch.no_grad():
        output = model(data)
        for i, j in zip(predict_data, output):
            print(f'字符串{i},对应类别{np.argmax(j)}')

if __name__ == "__main__":
    x, y = structure_data(5)
    with open('./vocab/vocab.json', 'r') as f:
        vocab = json.load(f)
    model = RNN_Model(len(vocab), 10)
    # 训练模型
    # train_model(model, vocab)
    # 运用模型做预测
    prediction('./model/rnn_class_model.bin', vocab, x)

模型训练效果: 在这里插入图片描述 实际使用效果: 在这里插入图片描述