目录

NLP-29项目-电商评论分类好评-差评-

【NLP 29、项目 Ⅰ:电商评论分类(好评 / 差评) 】


越怕什么就越要去做,击败痛苦的唯一方式就是去直对它

—— 25.1.27

项目介绍

在某电商平台爬取评论,通过模型训练将电商评价进行分类(好评 / 差评),对比三种以上模型结构的分类效果总结成表格进行输出

一、训练及测试数据

通过网盘分享的文件:分享文件

链接: 提取码: snh5

–来自百度网盘超级会员v3的分享


二、代码实现

1.配置文件 config.py

model_path: 保存的模型文件路径,存储训练好的模型权重和结构,便于后续加载和使用

train_data_path: 训练数据集的路径

valid_data_path: 验证数据集的路径

vocab_path: 词汇表文件的路径。

model_type: 指定模型的类型或架构

max_length: 输入序列的最大长度。用于限制模型处理的文本长度,通常通过截断或填充来统一输入尺寸

hidden_size: 隐藏层的大小,即隐藏层中神经元的数量。隐藏层的大小影响模型的表达能力和计算复杂度

kernel_size: 卷积核的大小。在卷积神经网络中,卷积核决定了特征提取的范围和方式

num_layers: 神经网络的层数。层数越多,模型的表达能力越强,但也可能增加训练难度和过拟合风险

epoch: 训练的轮数。每个epoch表示模型对整个训练数据集进行一次完整的训练

batch_size: 每次训练时输入模型的数据样本数量

pooling_style: 池化操作的类型。池化用于减少特征图的尺寸,常见的池化方式包括最大池化和平均池化

optimizer: 优化器的类型。优化器用于更新模型参数以最小化损失函数,常见的优化器包括SGD、Adam等

learning_rate: 学习率。学习率控制模型参数更新的步长,过大的学习率可能导致模型无法收敛,过小的学习率则会使训练变慢

pretrain_model_path: 预训练模型的路径

seed: 随机数种子。用于控制随机初始化,确保实验的可重复性

# -*- coding: utf-8 -*-

"""
配置参数信息
"""

Config = {
    "model_path": "output",
    "train_data_path": "train_data.txt",
    "valid_data_path": "valid_data.txt",
    "vocab_path":"chars.txt",
    "model_type":"bert",
    "max_length": 30,
    "hidden_size": 256,
    "kernel_size": 3,
    "num_layers": 2,
    "epoch": 10,
    "batch_size": 16,
    "pooling_style":"avg",
    "optimizer": "adam",
    "learning_rate": 1e-5,
    "pretrain_model_path":r"F:\人工智能NLP\\NLP资料\week6 语言模型\bert-base-chinese",
    "seed": 987
}

2.分割训练集和验证集 split_train_valid.py

将一个CSV文件随机分割为训练集和验证集,并将结果分别保存到 train_data.txtvalid_data.txt 文件中。

open(): 用于打开文件并返回文件对象,支持读取、写入等操作。

参数名描述
file文件路径(字符串),可以是相对路径或绝对路径。
mode文件打开模式(字符串),如 'r' (只读)、 'w' (写入)、 'a' (追加)。
buffering缓冲策略(整数),默认值为 -1 (系统默认缓冲)。
encoding文件编码(字符串),如 'utf-8'
errors编码错误处理方式(字符串),如 'strict''ignore'
newline换行符处理方式(字符串),如 None'\n'
closefd是否关闭文件描述符(布尔值),默认值为 True
opener自定义文件打开器(可调用对象),默认值为 None

文件对象.readlines(): 读取文件的所有行并返回一个列表,每行作为列表的一个元素。

参数名描述
hint可选参数(整数),指定读取的字节数,默认值为 -1 (读取所有行)。

random.shuffle(): 随机打乱序列(如列表)中元素的顺序,原地修改序列。

参数名描述
x可变序列(如列表),需要被打乱的序列。
random可选参数(函数),用于生成随机数,默认值为 random.random

len(): 返回对象的长度或元素个数,适用于字符串、列表、元组等。

参数名描述
obj需要计算长度的对象(如字符串、列表、元组等)。

文件对象.writelines(): 将字符串列表(或可迭代对象)写入文件,不会自动添加换行符。

参数名描述
sequence可迭代对象(如列表、元组),每个元素必须是字符串。
import random
'''切割训练集和验证集'''
def split_file(file_path):
    with open(file_path, 'r', encoding='utf8') as f:
        lines = f.readlines()[1:]
    random.shuffle(lines)
    num_lines = len(lines)
    num_train = int(0.8 * num_lines)

    train_lines = lines[:num_train]
    valid_lines = lines[num_train:]

    with open('train_data.txt', 'w', encoding='utf8') as f_train:
        f_train.writelines(train_lines)

    with open('valid_data.txt', 'w', encoding='utf8') as f_valid:
        f_valid.writelines(valid_lines)

split_file(r"F:\人工智能NLP/NLP资料\week7 文本分类问题\文本分类练习.csv")

3.数据加载文件 loader.py

从文件中读取数据,解析标签和标题,将标题编码为模型所需的格式,并将输入和标签存储为 PyTorch 张量。最终,所有处理后的数据存储在 self.data 列表中,供后续模型训练使用。

Ⅰ、 加载和处理数据 DataGenerator

① 初始化

dict(): 创建一个字典对象

参数描述
iterable可迭代对象,用于初始化字典(可选)。
**kwargs关键字参数,用于初始化字典(可选)。

列表推导式: 用简洁的语法生成列表

语法描述
[expression for item in iterable]生成一个新列表, expression 是对 item 的操作。
[expression for item in iterable if condition]根据条件筛选 item 并生成新列表。

items(): 返回字典的键值对视图( (key, value) 元组)

BertModel.from_pretrained(): Hugging Face Transformers库中的方法,用于从预训练的权重加载BERT模型。这使得用户可以快速使用预训练的BERT模型进行微调或直接应用。

参数名称类型默认值描述
pretrained_model_name_or_pathstr“bert-base-uncased”预训练模型的名称或路径
configConfig, 可选None模型的配置对象
cache_dirstr, 可选None缓存预训练模型的目录
force_downloadbool, 可选False是否强制重新下载模型
resume_downloadbool, 可选False是否在下载中断后继续下载
local_files_onlybool, 可选False是否仅使用本地文件
mirror_urlstr, 可选None镜像URL地址
progressbool, 可选True是否显示下载进度
class DataGenerator:
    def __init__(self, data_path, config):
        self.config = config
        self.path = data_path
        self.index_to_label = {0: '差评', 1: '好评'}
        self.label_to_index = dict((y, x) for x, y in self.index_to_label.items())
        self.config["class_num"] = len(self.index_to_label)
        if self.config["model_type"] == "bert":
            self.tokenizer = BertTokenizer.from_pretrained(config["pretrain_model_path"])
        self.vocab = load_vocab(config["vocab_path"])
        self.config["vocab_size"] = len(self.vocab)
        self.load()

② 数据加载
  • 逐行读取数据文件,解析标签和文本。
  • 如果模型类型是 BERT,使用 BERT tokenizer 对文本进行编码;否则,调用 encode_sentence 方法编码。
  • 将输入和标签转换为 PyTorch 张量,并存储在 self.data 中。

open(): 用于打开文件并返回文件对象,支持读取、写入等操作。

参数名描述
file文件路径(字符串),可以是相对路径或绝对路径。
mode文件打开模式(字符串),如 'r' (只读)、 'w' (写入)、 'a' (追加)。
buffering缓冲策略(整数),默认值为 -1 (系统默认缓冲)。
encoding文件编码(字符串),如 'utf-8'
errors编码错误处理方式(字符串),如 'strict''ignore'
newline换行符处理方式(字符串),如 None'\n'
closefd是否关闭文件描述符(布尔值),默认值为 True
opener自定义文件打开器(可调用对象),默认值为 None

str.startswith(): 检查字符串是否以指定前缀开头。

参数描述
prefix要检查的前缀字符串。
start开始检查的位置(可选)。
end结束检查的位置(可选)。

strip(): 移除字符串开头和结尾的指定字符。

参数描述
chars要移除的字符集合(可选,默认为空白符)。

torch.LongTensor(): 创建一个 LongTensor 张量。

参数描述
data输入数据(如列表、数组等)。

列表.append(): 在列表末尾添加一个元素。

参数描述
item要添加到列表末尾的元素。
    def load(self):
        self.data = []
        with open(self.path, encoding="utf8") as f:
            for line in f:
                if line.startswith("0,"):
                    label = 0
                elif line.startswith("1,"):
                    label = 1
                else:
                    continue
                title = line[2:].strip()
                if self.config["model_type"] == "bert":
                    input_id = self.tokenizer.encode(title, max_length=self.config["max_length"], pad_to_max_length=True)
                else:
                    input_id = self.encode_sentence(title)
                input_id = torch.LongTensor(input_id)
                label_index = torch.LongTensor([label])
                self.data.append([input_id, label_index])
        return

③ 文本编码
  • 将文本中的每个字符转换为词汇表中的索引,未知字符用 [UNK] 表示。
  • 调用 padding 方法对输入进行补齐或截断。

字典.get(): 获取字典中指定键的值,如果键不存在则返回默认值

参数描述
key要查找的键。
default如果键不存在时返回的默认值(可选)。
    def encode_sentence(self, text):
        input_id = []
        for char in text:
            input_id.append(self.vocab.get(char, self.vocab["[UNK]"]))
        input_id = self.padding(input_id)
        return input_id

④ 补齐 / 截断

将输入序列补齐或截断到指定长度 max_length

    #补齐或截断输入的序列,使其可以在一个batch内运算
    def padding(self, input_id):
        input_id = input_id[:self.config["max_length"]]
        input_id += [0] * (self.config["max_length"] - len(input_id))
        return input_id

⑤ 获取数据集长度和指定索引的数据
    def __len__(self):
        return len(self.data)

    def __getitem__(self, index):
        return self.data[index]

Ⅱ、加载词汇表

open(): 用于打开文件并返回文件对象,支持读取、写入等操作。

参数名描述
file文件路径(字符串),可以是相对路径或绝对路径。
mode文件打开模式(字符串),如 'r' (只读)、 'w' (写入)、 'a' (追加)。
buffering缓冲策略(整数),默认值为 -1 (系统默认缓冲)。
encoding文件编码(字符串),如 'utf-8'
errors编码错误处理方式(字符串),如 'strict''ignore'
newline换行符处理方式(字符串),如 None'\n'
closefd是否关闭文件描述符(布尔值),默认值为 True
opener自定义文件打开器(可调用对象),默认值为 None

enumerate(): 返回一个枚举对象,包含索引和值。

参数描述
iterable可迭代对象。
start索引的起始值(可选,默认为 0)。

strip(): 移除字符串开头和结尾的指定字符。

参数描述
chars要移除的字符集合(可选,默认为空白符)。
def load_vocab(vocab_path):
    token_dict = {}
    with open(vocab_path, encoding="utf8") as f:
        for index, line in enumerate(f):
            token = line.strip()
            token_dict[token] = index + 1  #0留给padding位置,所以从1开始
    return token_dict

Ⅲ、封装数据

  • 使用 DataGenerator 加载数据,并用 DataLoader 封装,支持批量加载和打乱数据。

DataLoader(): 将数据集封装为可迭代对象,支持批量加载。

参数描述
dataset数据集对象。
batch_size每个批次的样本数量(可选)。
shuffle是否打乱数据(可选)。
num_workers数据加载的线程数(可选)。
#用torch自带的DataLoader类封装数据
def load_data(data_path, config, shuffle=True):
    dg = DataGenerator(data_path, config)
    dl = DataLoader(dg, batch_size=config["batch_size"], shuffle=shuffle)
    return dl

Ⅳ、loader.py 代码全貌

# -*- coding: utf-8 -*-

import json
import re
import os
import torch
import numpy as np
from torch.utils.data import Dataset, DataLoader
from transformers import BertTokenizer
"""
数据加载
"""


class DataGenerator:
    def __init__(self, data_path, config):
        self.config = config
        self.path = data_path
        self.index_to_label = {0: '差评', 1: '好评'}
        self.label_to_index = dict((y, x) for x, y in self.index_to_label.items())
        self.config["class_num"] = len(self.index_to_label)
        if self.config["model_type"] == "bert":
            self.tokenizer = BertTokenizer.from_pretrained(config["pretrain_model_path"])
        self.vocab = load_vocab(config["vocab_path"])
        self.config["vocab_size"] = len(self.vocab)
        self.load()


    def load(self):
        self.data = []
        with open(self.path, encoding="utf8") as f:
            for line in f:
                if line.startswith("0,"):
                    label = 0
                elif line.startswith("1,"):
                    label = 1
                else:
                    continue
                title = line[2:].strip()
                if self.config["model_type"] == "bert":
                    input_id = self.tokenizer.encode(title, max_length=self.config["max_length"], pad_to_max_length=True)
                else:
                    input_id = self.encode_sentence(title)
                input_id = torch.LongTensor(input_id)
                label_index = torch.LongTensor([label])
                self.data.append([input_id, label_index])
        return

    def encode_sentence(self, text):
        input_id = []
        for char in text:
            input_id.append(self.vocab.get(char, self.vocab["[UNK]"]))
        input_id = self.padding(input_id)
        return input_id

    #补齐或截断输入的序列,使其可以在一个batch内运算
    def padding(self, input_id):
        input_id = input_id[:self.config["max_length"]]
        input_id += [0] * (self.config["max_length"] - len(input_id))
        return input_id

    def __len__(self):
        return len(self.data)

    def __getitem__(self, index):
        return self.data[index]

def load_vocab(vocab_path):
    token_dict = {}
    with open(vocab_path, encoding="utf8") as f:
        for index, line in enumerate(f):
            token = line.strip()
            token_dict[token] = index + 1  #0留给padding位置,所以从1开始
    return token_dict


#用torch自带的DataLoader类封装数据
def load_data(data_path, config, shuffle=True):
    dg = DataGenerator(data_path, config)
    dl = DataLoader(dg, batch_size=config["batch_size"], shuffle=shuffle)
    return dl

if __name__ == "__main__":
    from config import Config
    dg = DataGenerator("F:\人工智能NLP/NLP\HomeWork\demo6.1_训练分类模型对比效果\data/valid_tag_news.json", Config)
    print(dg[1])

4.模型文件 model.py

Ⅰ、模型初始化

hidden_size: 对于神经网络模型(如RNN、LSTM、Transformer等),这是 隐藏层 的大小。常见的值根据任务和模型复杂度有所不同,比如在简单的文本分类任务中可以是128或者256。

vocab_size: 指定 词表的存储路径 。例如在自然语言处理中,词汇表可能是一个文本文件,路径可以是 ./data/vocab.txt

class_num: 指定 分类数目

model_type: 根据任务选择合适的 模型类型 ,如 Transformer (常用于机器翻译等序列到序列任务)、 LSTM (长短时记忆网络,适合处理序列数据)、 CNN (卷积神经网络,可用于图像或者文本分类等任务)等。

num_layers: 对于多层神经网络(如多层LSTM、Transformer的多层结构等),指定 网络的层数 。比如在构建一个较深的LSTM网络时, num_layers = 3

use_bert: 是否使用bert模型

nn.Embedding(): 是一个 嵌入层 ,用于将离散的输入(如单词索引)映射到固定大小的向量空间中。这在自然语言处理(NLP)中非常常见,用于将词汇表中的每个词转换为一个向量表示(词嵌入)。

参数名称类型默认值描述
num_embeddingsint-词汇表中词的总数
embedding_dimint-每个词的嵌入维度
padding_idxint, 可选None如果提供,将此索引对应的嵌入向量初始化为零
max_normfloat, 可选None嵌入向量的最大范数,超过则会被归一化
norm_typefloat, 可选2.0计算范数的类型(如L2范数)
scale_grad_by_freqbool, 可选False是否根据词频缩放梯度
sparsebool, 可选False是否使用稀疏梯度
_weightTensor, 可选None预定义的权重张量

lambda: Python中的一个关键字,用于 创建匿名函数 (即没有名称的函数)。在深度学习中,常用于定义简单的函数,如损失函数、激活函数或自定义层。

nn.LSTM(): 实现了 长短期记忆网络(Long Short-Term Memory) ,一种特殊的循环神经网络(RNN),能够有效捕捉长距离依赖关系。LSTM通过引入门控机制(输入门、遗忘门、输出门)来解决传统RNN中的梯度消失问题。

参数名称类型默认值描述
input_sizeint-输入特征的维度
hidden_sizeint-隐藏层的大小
num_layersint, 可选1LSTM层的堆叠层数
biasbool, 可选True是否使用偏置项
batch_firstbool, 可选False如果为True,输入和输出张量的形状为 (batch, seq, feature)
dropoutfloat, 可选0如果 num_layers > 1,层之间的dropout概率
bidirectionalbool, 可选False是否使用双向LSTM
proj_sizeint, 可选0输出投影的大小(仅适用于某些实现)

nn.GRU(): 实现了 门控循环单元(Gated Recurrent Unit) ,一种简化版的LSTM。GRU通过引入更新门和重置门来控制信息的流动,通常比LSTM具有更少的参数和更高的计算效率。

参数名称类型默认值描述
input_sizeint-输入特征的维度
hidden_sizeint-隐藏层的大小
num_layersint, 可选1GRU层的堆叠层数
biasbool, 可选True是否使用偏置项
batch_firstbool, 可选False如果为True,输入和输出张量的形状为 (batch, seq, feature)
dropoutfloat, 可选0如果 num_layers > 1,层之间的dropout概率
bidirectionalbool, 可选False是否使用双向GRU

nn.RNN(): 实现了基本的 循环神经网络(Recurrent Neural Network )。RNN能够处理序列数据,但存在梯度消失问题,通常不如LSTM和GRU适用于长序列。

参数名称类型默认值描述
input_sizeint-输入特征的维度
hidden_sizeint-隐藏层的大小
num_layersint, 可选1RNN层的堆叠层数
nonlinearitystr, 可选’tanh'激活函数类型,可选 ’tanh’ 或 ‘relu’
biasbool, 可选True是否使用偏置项
batch_firstbool, 可选False如果为True,输入和输出张量的形状为 (batch, seq, feature)
dropoutfloat, 可选0如果 num_layers > 1,层之间的dropout概率
bidirectionalbool, 可选False是否使用双向RNN

CNN(): 指 卷积神经网络(Convolutional Neural Network) ,用于处理具有网格结构的数据,如图像。CNN通过卷积层、池化层和全连接层提取特征,广泛应用于图像分类、目标检测等任务。

注意 :PyTorch中没有直接的 CNN 类,通常需要自定义或使用组合层(如 nn.Conv2dnn.MaxPool2d 等)构建CNN模型。

GatedCNN(): GatedCNN 是一种 带有门控机制的卷积神经网络 。它结合了卷积操作和门控机制(类似于GRU或LSTM中的门控),用于更有效地捕捉和利用序列数据中的特征。

注意 :PyTorch中没有内置的 GatedCNN 类,通常需要自定义实现。

StackGatedCNN(): StackGatedCNN 是 堆叠带有门控机制的卷积神经网络 。通过堆叠多个带有门控的卷积层,可以增强模型的特征提取能力,适用于复杂的序列数据处理任务。

注意 :PyTorch中没有内置的 StackGatedCNN 类,通常需要自定义实现。

RCNN(): 指 区域卷积神经网络(Region-based Convolutional Neural Network) ,常用于目标检测任务。RCNN通过提取候选区域并对其进行卷积操作,实现对图像中多个目标的检测和分类。

注意 :PyTorch中没有内置的 RCNN 类,通常使用如 torchvision.models.detection 中的模型(如Faster R-CNN)。

BertModel.from_pretrained(): Hugging Face Transformers库中的方法,用于从预训练的权重加载BERT模型。这使得用户可以快速使用预训练的BERT模型进行微调或直接应用。

参数名称类型默认值描述
pretrained_model_name_or_pathstr“bert-base-uncased”预训练模型的名称或路径
configConfig, 可选None模型的配置对象
cache_dirstr, 可选None缓存预训练模型的目录
force_downloadbool, 可选False是否强制重新下载模型
resume_downloadbool, 可选False是否在下载中断后继续下载
local_files_onlybool, 可选False是否仅使用本地文件
mirror_urlstr, 可选None镜像URL地址
progressbool, 可选True是否显示下载进度

BertLSTM(): 是 结合BERT和LSTM的模型架构 。通常, BERT用于提取上下文特征,然后将这些特征输入到LSTM层中,以捕捉序列中的时间依赖关系。 这种组合常用于文本分类、序列标注等任务。

注意 :PyTorch中没有内置的 BertLSTM 类,通常需要自定义实现。

BertCNN(): 是 结合BERT和卷积神经网络(CNN) 的模型架构。 BERT用于提取上下文特征,然后通过CNN层提取局部特征 ,适用于文本分类、情感分析等任务。

注意 :PyTorch中没有内置的 BertCNN 类,通常需要自定义实现。

BertMidLayer(): 用于 提取BERT模型中间层的输出 。这允许用户获取BERT在特定层次的特征表示,以便进行更细粒度的分析或应用。

注意 :PyTorch中没有内置的 BertMidLayer 类,通常需要自定义实现或使用Hugging Face Transformers库中的相关功能。

nn.Linear(): 实现了一个 全连接层(线性层) ,用于 将输入特征映射到指定维度的输出 。它通过矩阵乘法和可选的偏置项进行线性变换。

参数名称类型默认值描述
in_featuresint-输入特征的数量
out_featuresint-输出特征的数量
biasbool, 可选True是否使用偏置项
devicedevice, 可选None指定张量所在的设备(如CPU或GPU)
dtypedtype, 可选None张量的数据类型

nn.functional.cross_entropy(): 计算 交叉熵损失 ,常用于 分类任务。它 结合了 log_softmaxNLLLoss (负对数似然损失) ,简化了多分类损失函数的计算。

参数名称类型默认值描述
inputTensor-模型的未归一化得分(通常经过 log_softmax
targetTensor-真实标签,类别索引
weightTensor, 可选None每个类别的权重,用于处理类别不平衡
reductionstr, 可选‘mean’损失的缩减方式,可选 ’none’, ‘mean’, ‘sum’
ignore_indexint, 可选-1忽略指定索引的损失
label_smoothingfloat, 可选0标签平滑系数
    def __init__(self, config):
        super(TorchModel, self).__init__()
        hidden_size = config["hidden_size"]
        vocab_size = config["vocab_size"] + 1
        class_num = config["class_num"]
        model_type = config["model_type"]
        num_layers = config["num_layers"]
        self.use_bert = False
        self.embedding = nn.Embedding(vocab_size, hidden_size, padding_idx=0)
        if model_type == "fast_text":
            self.encoder = lambda x: x
        elif model_type == "lstm":
            self.encoder = nn.LSTM(hidden_size, hidden_size, num_layers=num_layers, batch_first=True)
        elif model_type == "gru":
            self.encoder = nn.GRU(hidden_size, hidden_size, num_layers=num_layers, batch_first=True)
        elif model_type == "rnn":
            self.encoder = nn.RNN(hidden_size, hidden_size, num_layers=num_layers, batch_first=True)
        elif model_type == "cnn":
            self.encoder = CNN(config)
        elif model_type == "gated_cnn":
            self.encoder = GatedCNN(config)
        elif model_type == "stack_gated_cnn":
            self.encoder = StackGatedCNN(config)
        elif model_type == "rcnn":
            self.encoder = RCNN(config)
        elif model_type == "bert":
            self.use_bert = True
            self.encoder = BertModel.from_pretrained(config["pretrain_model_path"], return_dict=False)
            hidden_size = self.encoder.config.hidden_size
        elif model_type == "bert_lstm":
            self.use_bert = True
            self.encoder = BertLSTM(config)
            hidden_size = self.encoder.bert.config.hidden_size
        elif model_type == "bert_cnn":
            self.use_bert = True
            self.encoder = BertCNN(config)
            hidden_size = self.encoder.bert.config.hidden_size
        elif model_type == "bert_mid_layer":
            self.use_bert = True
            self.encoder = BertMidLayer(config)
            hidden_size = self.encoder.bert.config.hidden_size

        self.classify = nn.Linear(hidden_size, class_num)
        self.pooling_style = config["pooling_style"]
        self.loss = nn.functional.cross_entropy  #loss采用交叉熵损失

Ⅱ、前向计算

range(): Python内置函数,用于生成一个整数序列。它常用于 for 循环中,以迭代指定范围内的数值。

参数名称类型默认值描述
startint0序列的起始值(包含)。默认为0。
stopint-序列的结束值(不包含)。必须指定。
stepint1序列中相邻两个数之间的差值。默认为1。

isinstance(): Python内置函数,用于检查一个对象是否是指定类型或其子类的实例。它常用于类型检查和条件判断。

参数名称类型描述
object任意要检查的对象。
classinfotype 或 tuple of types要比较的类型或类型的元组。可以是单个类型或多个类型的组合。

tuple: Python的内置数据类型,用于存储有序且不可变的元素集合。与列表( list )类似,元组可以包含任意类型的元素,但其内容在创建后无法修改。元组通常用于存储不需要更改的数据,如函数返回多个值、字典的键等。

常用方法名称描述
count(x)返回元素 x 在元组中出现的次数。
index(x[, start[, end]])返回元素 x 在元组中首次出现的索引。可选参数 startend 用于指定搜索范围。

nn.MaxPool1d(): 实现了 一维最大池化操作 ,常用于 卷积神经网络(CNN)中对序列数据进行下采样,减少参数数量并提取主要特征。

参数名称类型默认值描述
kernel_sizeint 或 tuple-池化窗口的大小。可以是单个整数或一个包含单个整数的元组。
strideint 或 tuplekernel_size池化操作的步幅。可以是单个整数或一个包含单个整数的元组。
paddingint 或 tuple0输入的每一侧填充的大小。可以是单个整数或一个包含单个整数的元组。
dilationint 或 tuple1控制窗口中元素步幅的参数。可以是单个整数或一个包含单个整数的元组。
ceil_modebool, 可选False如果为True,使用ceil而不是floor计算输出大小。
return_indicesbool, 可选False如果为True,返回最大值的索引。

nn.AvgPool1d(): 实现了 一维平均池化操作 , 用于卷积神经网络(CNN)中对序列数据进行下采样,通过 计算窗口内元素的平均值来提取特征。

参数名称类型默认值描述
kernel_sizeint 或 tuple-池化窗口的大小。可以是单个整数或一个包含单个整数的元组。
strideint 或 tuplekernel_size池化操作的步幅。可以是单个整数或一个包含单个整数的元组。
paddingint 或 tuple0输入的每一侧填充的大小。可以是单个整数或一个包含单个整数的元组。
ceil_modebool, 可选False如果为True,使用ceil而不是floor计算输出大小。
count_include_padbool, 可选True是否将填充的元素包含在平均值计算中。

.transpose(): PyTorch张量的方法,用于交换张量的两个维度。它返回一个新的张量,其指定维度的顺序被交换。

参数名称类型描述
dim0int第一个要交换的维度索引。
dim1int第二个要交换的维度索引。

.squeeze(): PyTorch张量的方法,用于移除张量中大小为1的维度。如果未指定维度,则移除所有大小为1的维度。

参数名称类型描述
dimint, 可选要移除的维度索引。如果指定,仅移除该维度如果其大小为1。默认为None,移除所有大小为1的维度。
    #当输入真实标签,返回loss值;无真实标签,返回预测值
    def forward(self, x, target=None):
        if self.use_bert:  # bert返回的结果是 (sequence_output, pooler_output)
            #sequence_output:batch_size, max_len, hidden_size
            #pooler_output:batch_size, hidden_size
            x = self.encoder(x)
        else:
            x = self.embedding(x)  # input shape:(batch_size, sen_len)
            x = self.encoder(x)  # input shape:(batch_size, sen_len, input_dim)

        if isinstance(x, tuple):  #RNN类的模型会同时返回隐单元向量,我们只取序列结果
            x = x[0]
        #可以采用pooling的方式得到句向量
        if self.pooling_style == "max":
            self.pooling_layer = nn.MaxPool1d(x.shape[1])
        else:
            self.pooling_layer = nn.AvgPool1d(x.shape[1])
        x = self.pooling_layer(x.transpose(1, 2)).squeeze() #input shape:(batch_size, sen_len, input_dim)

        #也可以直接使用序列最后一个位置的向量
        # x = x[:, -1, :]
        predict = self.classify(x)   #input shape:(batch_size, input_dim)
        if target is not None:
            return self.loss(predict, target.squeeze())
        else:
            return predict

Ⅲ、手动实现卷积神经网络(CNN)

int(): Python的内置函数,用于将一个数值或字符串转换为整数类型。如果传入的参数无法转换为整数,会引发 ValueError

参数名称类型描述
xint, float, str要转换的值。可以是整数、浮点数或表示整数的字符串。
baseint, 可选用于解析字符串的进制(2到36之间)。默认为10。

nn.Conv1d(): 是PyTorch中的一个模块,用于实现 一维卷积操作 。它通常用于处理序列数据,如时间序列或单通道的图像数据(例如,灰度图像沿宽度方向的卷积)。

参数名称类型默认值描述
in_channelsint-输入张量的通道数。
out_channelsint-输出张量的通道数。
kernel_sizeint 或 tuple-卷积核的大小。可以是单个整数或一个包含单个整数的元组。
strideint 或 tuple1卷积操作的步幅。可以是单个整数或一个包含单个整数的元组。
paddingint 或 tuple0输入的每一侧填充的大小。可以是单个整数或一个包含单个整数的元组。
dilationint 或 tuple1卷积核元素之间的间距。可以是单个整数或一个包含单个整数的元组。
groupsint1输入通道与输出通道之间的连接方式。默认为1,表示标准卷积。
biasbool, 可选True是否使用偏置项。如果为False,则不使用偏置。
padding_modestr, 可选‘zeros’填充模式,如 ‘zeros’, ‘reflect’, ‘replicate’, ‘circular’ 等。

.transpose(): 是PyTorch张量的方法,用于 交换张量的两个维度 。它返回一个新的张量,其指定维度的顺序被交换。

参数名称类型描述
dim0int第一个要交换的维度索引。
dim1int第二个要交换的维度索引。
class CNN(nn.Module):
    def __init__(self, config):
        super(CNN, self).__init__()
        hidden_size = config["hidden_size"]
        kernel_size = config["kernel_size"]
        pad = int((kernel_size - 1)/2)
        self.cnn = nn.Conv1d(hidden_size, hidden_size, kernel_size, bias=False, padding=pad)

    def forward(self, x): #x : (batch_size, max_len, embeding_size)
        return self.cnn(x.transpose(1, 2)).transpose(1, 2)

Ⅳ、手动实现带有门控机制的卷积神经网络(GatedCNN)

torch.sigmoid(): PyTorch 中的一个函数,用于 计算输入张量中每个元素的 Sigmoid 激活函数值 。Sigmoid 函数 将输入值压缩到 0 和 1 之间,常用于二分类问题的输出层,将模型的输出转换为概率。

公式: https://i-blog.csdnimg.cn/direct/5015e026caa246a48006bc61f72d84cd.png

参数名称类型描述
inputTensor输入的张量。可以是任意形状的张量。
outTensor, 可选输出张量,用于存储结果。如果未提供,将创建一个新的张量。

torch.mul(): PyTorch 中的一个函数,用于 对输入张量进行逐元素相乘(element-wise multiplication) 。它支持广播机制,可以 用于不同形状的张量之间的乘法操作。

参数名称类型描述
inputTensor第一个输入张量。
otherTensor 或 float第二个输入张量或标量。可以是与 input 形状相同的张量,或者是一个标量。
outTensor, 可选输出张量,用于存储结果。如果未提供,将创建一个新的张量。
class GatedCNN(nn.Module):
    def __init__(self, config):
        super(GatedCNN, self).__init__()
        self.cnn = CNN(config)
        self.gate = CNN(config)

    def forward(self, x):
        a = self.cnn(x)
        b = self.gate(x)
        b = torch.sigmoid(b)
        return torch.mul(a, b)

Ⅴ、手动实现堆叠带有门控机制的卷积神经网络( StackGatedCNN

range(): Python 的内置函数,用于生成一个整数序列。它常用于 for 循环中,以迭代指定范围内的数值。

参数名称类型默认值描述
startint0序列的起始值(包含)。默认为0。
stopint-序列的结束值(不包含)。必须指定。
stepint1序列中相邻两个数之间的差值。默认为1。

nn.ModuleList(): PyTorch 中的一个 容器类 ,用于 将多个子模块(通常是神经网络层或其他模块)组织在一起 。与普通的 Python 列表不同, nn.ModuleList 能够正确地注册和管理其中的子模块,使其在模型的参数列表中可见,并且能够在保存和加载模型时被正确处理。

参数名称类型描述
modulesIterable一个可迭代的子模块(如 nn.Linearnn.ReLU 等)。

nn.Linear(): 实现了一个 全连接层(线性层) ,用于 将输入特征映射到指定维度的输出 。它通过矩阵乘法和可选的偏置项进行线性变换。

参数名称类型默认值描述
in_featuresint-输入特征的数量
out_featuresint-输出特征的数量
biasbool, 可选True是否使用偏置项
devicedevice, 可选None指定张量所在的设备(如CPU或GPU)
dtypedtype, 可选None张量的数据类型

nn.LayerNorm(): PyTorch 中的一个模块,用于实现 层归一化(Layer Normalization) 。层归一化是一种在深度神经网络中常用的正则化技术,旨在加速训练过程并提高模型的泛化能力。与批归一化(Batch Normalization)不同, 层归一化在每个样本的所有特征维度上进行归一化,而不是在批次维度上。 这使得层归一化在处理变长序列数据(如自然语言处理中的句子)时尤为有效。

参数名称类型默认值描述
normalized_shapeint 或 tuple-需要归一化的特征维度的大小。可以是单个整数或一个包含多个整数的元组。
epsfloat, 可选1e-5用于数值稳定性的小常数,防止除以零。
elementwise_affinebool, 可选True是否对每个归一化后的特征维度应用可学习的仿射变换(即缩放和偏移)。默认为True。
devicedevice, 可选None指定张量所在的设备(如CPU或GPU)。默认为None,使用当前设备。
dtypedtype, 可选None指定张量的数据类型。默认为None,使用当前默认数据类型。

torch.relu(): PyTorch 中的一个函数,用于 计算输入张量的修正线性单元(Rectified Linear Unit, ReLU)激活值 。ReLU 是一种常用的非线性激活函数,广泛应用于各种神经网络架构中,特别是 在深度学习中用于引入非线性特性。

公式: https://i-blog.csdnimg.cn/direct/6d9de7a4471d4f5cb07bf3209e65e79f.png

参数名称类型描述
inputTensor输入的张量。可以是任意形状的张量。
inplacebool, 可选是否在输入张量上进行原地操作。默认为 False 。如果设置为 True ,则结果会覆盖输入张量,节省内存但会改变原始数据。
outTensor, 可选输出张量,用于存储结果。如果未提供,将创建一个新的张量。
class StackGatedCNN(nn.Module):
    def __init__(self, config):
        super(StackGatedCNN, self).__init__()
        self.num_layers = config["num_layers"]
        self.hidden_size = config["hidden_size"]
        #ModuleList类内可以放置多个模型,取用时类似于一个列表
        self.gcnn_layers = nn.ModuleList(
            GatedCNN(config) for i in range(self.num_layers)
        )
        self.ff_liner_layers1 = nn.ModuleList(
            nn.Linear(self.hidden_size, self.hidden_size) for i in range(self.num_layers)
        )
        self.ff_liner_layers2 = nn.ModuleList(
            nn.Linear(self.hidden_size, self.hidden_size) for i in range(self.num_layers)
        )
        self.bn_after_gcnn = nn.ModuleList(
            nn.LayerNorm(self.hidden_size) for i in range(self.num_layers)
        )
        self.bn_after_ff = nn.ModuleList(
            nn.LayerNorm(self.hidden_size) for i in range(self.num_layers)
        )

    def forward(self, x):
        #仿照bert的transformer模型结构,将self-attention替换为gcnn
        for i in range(self.num_layers):
            gcnn_x = self.gcnn_layers[i](x)
            x = gcnn_x + x  #通过gcnn+残差
            x = self.bn_after_gcnn[i](x)  #之后bn
            # # 仿照feed-forward层,使用两个线性层
            l1 = self.ff_liner_layers1[i](x)  #一层线性
            l1 = torch.relu(l1)               #在bert中这里是gelu
            l2 = self.ff_liner_layers2[i](l1) #二层线性
            x = self.bn_after_ff[i](x + l2)        #残差后过bn
        return x

Ⅵ、手动实现循环区域卷积神经网络(RCNN)

nn.RNN(): PyTorch 中的一个模块,用于实现基本的 循环神经网络(Recurrent Neural Network) 。RNN 特别适用于处理序列数据,如时间序列、自然语言文本等。 它通过在时间步之间传递隐藏状态来捕捉序列中的依赖关系。

返回值: ① output: 描述 :包含每个时间步的隐藏状态。 ② hn: 描述 :最后一个时间步的隐藏状态。

参数名称类型默认值描述
input_sizeint-输入特征的维度。每个时间步的输入张量的最后一个维度应为 input_size
hidden_sizeint-隐藏层的大小。即每个时间步隐藏状态的维度。
num_layersint, 可选1RNN 层的堆叠层数。默认为1。
biasbool, 可选True是否使用偏置项。如果为 False ,则不使用偏置。
batch_firstbool, 可选False如果为 True ,输入和输出张量的形状为 (batch, seq, feature) 。默认为 False (形状为 (seq, batch, feature) )。
dropoutfloat, 可选0如果 num_layers > 1,层之间的 dropout 概率。默认为0,表示不使用 dropout。
bidirectionalbool, 可选False是否使用双向 RNN。如果为 True ,则每个时间步会合并前向和后向的信息。默认为 False
proj_sizeint, 可选0输出投影的大小。如果大于0,输出会被投影到 proj_size 维度。默认为0,表示不进行投影。

GatedCNN(): 手动实现 带有门控机制的卷积神经网络

class RCNN(nn.Module):
    def __init__(self, config):
        super(RCNN, self).__init__()
        hidden_size = config["hidden_size"]
        self.rnn = nn.RNN(hidden_size, hidden_size)
        self.cnn = GatedCNN(config)

    def forward(self, x):
        x, _ = self.rnn(x)
        x = self.cnn(x)
        return x

Ⅶ、手动实现结合BERT和LSTM的模型(BertLSTM)

BertModel.from_pretrained(): 是 Hugging Face Transformers 库中的一个方法,用于 从预训练的权重加载 BERT 模型 。这使得用户可以快速使用预训练的 BERT 模型进行微调或直接应用,而无需从头开始训练。

参数名称类型默认值描述
pretrained_model_name_or_pathstr“bert-base-uncased”预训练模型的名称(如 "bert-base-uncased" )或本地路径。
configConfigNone模型的配置对象,可以自定义配置参数。
cache_dirstrNone缓存预训练模型的目录。如果模型已经缓存,将从缓存中加载。
force_downloadboolFalse是否强制重新下载模型,即使已经存在缓存。
resume_downloadboolFalse下载中断后是否继续下载。
local_files_onlyboolFalse是否仅使用本地文件,不尝试从远程服务器下载。
mirror_urlstrNone镜像 URL 地址,用于从镜像源下载模型。
progressboolTrue是否显示下载进度条。

nn.LSTM(): PyTorch 中实现 长短期记忆网络(Long Short-Term Memory) 的模块。LSTM 是一种特殊的循环神经网络(RNN),能够 有效捕捉长距离依赖关系,广泛应用于序列建模任务 ,如自然语言处理、时间序列预测等。

参数名称类型默认值描述
input_sizeint-输入特征的维度。
hidden_sizeint-隐藏层的大小。
num_layersint1LSTM 层的堆叠层数。
biasboolTrue是否使用偏置项。如果为 False ,则不使用偏置。
batch_firstboolFalse如果为 True ,输入和输出张量的形状为 (batch, seq, feature) 。默认为 False (形状为 (seq, batch, feature) )。
dropoutfloat0如果 num_layers > 1,层之间的 dropout 概率。
bidirectionalboolFalse是否使用双向 LSTM。
proj_sizeint0输出投影的大小。如果大于0,输出会被投影到 proj_size 维度。默认为0,表示不进行投影。
class BertLSTM(nn.Module):
    def __init__(self, config):
        super(BertLSTM, self).__init__()
        self.bert = BertModel.from_pretrained(config["pretrain_model_path"], return_dict=False)
        self.rnn = nn.LSTM(self.bert.config.hidden_size, self.bert.config.hidden_size, batch_first=True)

    def forward(self, x):
        x = self.bert(x)[0]
        x, _ = self.rnn(x)
        return x

Ⅷ、 手动实现集合Bert和CNN的模型(BertCNN)

BertModel.from_pretrained(): 是 Hugging Face Transformers 库中的一个方法,用于 从预训练的权重加载 BERT 模型 。这使得用户可以 快速使用预训练的 BERT 模型进行微调或直接应用,而无需从头开始训练。

参数名称类型默认值描述
pretrained_model_name_or_pathstr“bert-base-uncased”预训练模型的名称(如 "bert-base-uncased" )或本地路径。
configConfigNone模型的配置对象,可以自定义配置参数。
cache_dirstrNone缓存预训练模型的目录。如果模型已经缓存,将从缓存中加载。
force_downloadboolFalse是否强制重新下载模型,即使已经存在缓存。
resume_downloadboolFalse下载中断后是否继续下载。
local_files_onlyboolFalse是否仅使用本地文件,不尝试从远程服务器下载。
mirror_urlstrNone镜像 URL 地址,用于从镜像源下载模型。
progressboolTrue是否显示下载进度条。
class BertCNN(nn.Module):
    def __init__(self, config):
        super(BertCNN, self).__init__()
        self.bert = BertModel.from_pretrained(config["pretrain_model_path"], return_dict=False)
        config["hidden_size"] = self.bert.config.hidden_size
        self.cnn = CNN(config)

    def forward(self, x):
        x = self.bert(x)[0]
        x = self.cnn(x)
        return x

Ⅸ、手动实现提取BERT模型中间层的输出(BertMidLayer)

BertModel.from_pretrained(): 是 Hugging Face Transformers 库中的一个方法,用于 从预训练的权重加载 BERT 模型 。这使得用户可以 快速使用预训练的 BERT 模型进行微调或直接应用,而无需从头开始训练。

参数名称类型默认值描述
pretrained_model_name_or_pathstr“bert-base-uncased”预训练模型的名称(如 "bert-base-uncased" )或本地路径。
configConfigNone模型的配置对象,可以自定义配置参数。
cache_dirstrNone缓存预训练模型的目录。如果模型已经缓存,将从缓存中加载。
force_downloadboolFalse是否强制重新下载模型,即使已经存在缓存。
resume_downloadboolFalse下载中断后是否继续下载。
local_files_onlyboolFalse是否仅使用本地文件,不尝试从远程服务器下载。
mirror_urlstrNone镜像 URL 地址,用于从镜像源下载模型。
progressboolTrue是否显示下载进度条。

self.bert.config.output_hidden_states: 是否取出Bert中间层的输出

torch.add(): PyTorch 中的一个函数,用于对输入张量进行逐元素相加操作。它支持多种操作模式,包括将一个张量加到另一个张量上,或者将一个标量加到张量的每个元素上。

参数名称类型描述
inputTensor第一个输入张量。
otherTensor 或 float第二个输入张量或标量。可以是与 input 形状相同的张量,或者是一个标量。
outTensor, 可选输出张量,用于存储结果。如果未提供,将创建一个新的张量。
alphafloat, 可选标量乘数,用于缩放 other 张量后再进行相加。默认为1。
class BertMidLayer(nn.Module):
    def __init__(self, config):
        super(BertMidLayer, self).__init__()
        self.bert = BertModel.from_pretrained(config["pretrain_model_path"], return_dict=False)
        self.bert.config.output_hidden_states = True

    def forward(self, x):
        layer_states = self.bert(x)[2]#(13, batch, len, hidden)
        layer_states = torch.add(layer_states[-2], layer_states[-1])
        return layer_states

Ⅹ、选择优化器

Adam(): 是一种自适应学习率优化算法,结合了动量(Momentum)和均方根传播(RMSprop)的优点。它通过计算梯度的一阶矩估计和二阶矩估计来调整每个参数的学习率,从而在训练过程中自适应地调整学习步长。

参数名称类型默认值描述
paramsIterable-需要优化的模型参数(通常是 model.parameters() )。
lrfloat1e-3学习率。
betasTuple[float, float](0.9, 0.999)用于计算梯度及其平方的运行平均值的系数。
epsfloat1e-8数值稳定性的小常数,防止除以零。
weight_decayfloat0权重衰减(L2正则化)。
amsgradboolFalse是否使用 AMSGrad 变种。

SGD(): 随机梯度下降)是一种基本的优化算法,通过计算损失函数对模型参数的梯度,并按照一定的学习率更新参数。虽然简单,但在许多情况下仍然有效,尤其是在结合动量(momentum)时。

参数名称类型默认值描述
paramsIterable-需要优化的模型参数(通常是 model.parameters() )。
lrfloat0.01学习率。
momentumfloat0动量系数,用于加速SGD在相关方向上的收敛,并抑制震荡。
dampeningfloat0动量的阻尼系数。
weight_decayfloat0权重衰减(L2正则化)。
nesterovboolFalse是否使用Nesterov动量。

.parameters(): 是 PyTorch 中 nn.Module 类的一个方法,用于返回模型中所有需要优化的参数的迭代器。这些参数通常包括模型的权重和偏置项。通过调用此方法,可以将模型的参数传递给优化器进行更新。

参数名称类型描述
recursebool如果为 True ,则递归地遍历所有子模块,收集它们的参数。默认为 True
def choose_optimizer(config, model):
    optimizer = config["optimizer"]
    learning_rate = config["learning_rate"]
    if optimizer == "adam":
        return Adam(model.parameters(), lr=learning_rate)
    elif optimizer == "sgd":
        return SGD(model.parameters(), lr=learning_rate)

ⅩⅠ、建立网络模型结构

BertModel.from_pretrained(): 是 Hugging Face Transformers 库中的一个方法,用于从预训练的权重加载 BERT 模型。这使得用户可以快速使用预训练的 BERT 模型进行微调或直接应用,而无需从头开始训练。

参数名称类型默认值描述
pretrained_model_name_or_pathstr“bert-base-uncased”预训练模型的名称(如 "bert-base-uncased" )或本地路径。
configConfigNone模型的配置对象,可以自定义配置参数。
cache_dirstrNone缓存预训练模型的目录。如果模型已经缓存,将从缓存中加载。
force_downloadboolFalse是否强制重新下载模型,即使已经存在缓存。
resume_downloadboolFalse下载中断后是否继续下载。
local_files_onlyboolFalse是否仅使用本地文件,不尝试从远程服务器下载。
mirror_urlstrNone镜像 URL 地址,用于从镜像源下载模型。
progressboolTrue是否显示下载进度条。

torch.LongTensor(): PyTorch 中用于创建一个长整型(64位整数)张量的函数。长整型张量在需要存储整数索引、类别标签或其他需要较大整数范围的场景中非常有用。例如,在处理分类任务时,类别标签通常使用长整型表示。

参数名称类型描述
data可选用于初始化张量的数据,可以是列表、元组、NumPy 数组等。
dtype可选指定张量的数据类型,默认为 torch.int64 (即 LongTensor )。
device可选指定张量所在的设备(如 'cpu''cuda:0' )。
requires_grad可选是否需要计算梯度,默认为 False
size可选指定张量的形状,可以是一个整数或整数元组。
其他参数可选根据 data 的类型,可能需要提供其他参数,如 dtype 等。
# -*- coding: utf-8 -*-

import torch
import torch.nn as nn
from torch.optim import Adam, SGD
from transformers import BertModel
"""
建立网络模型结构
"""

class TorchModel(nn.Module):
    def __init__(self, config):
        super(TorchModel, self).__init__()
        hidden_size = config["hidden_size"]
        vocab_size = config["vocab_size"] + 1
        class_num = config["class_num"]
        model_type = config["model_type"]
        num_layers = config["num_layers"]
        self.use_bert = False
        self.embedding = nn.Embedding(vocab_size, hidden_size, padding_idx=0)
        if model_type == "fast_text":
            self.encoder = lambda x: x
        elif model_type == "lstm":
            self.encoder = nn.LSTM(hidden_size, hidden_size, num_layers=num_layers, batch_first=True)
        elif model_type == "gru":
            self.encoder = nn.GRU(hidden_size, hidden_size, num_layers=num_layers, batch_first=True)
        elif model_type == "rnn":
            self.encoder = nn.RNN(hidden_size, hidden_size, num_layers=num_layers, batch_first=True)
        elif model_type == "cnn":
            self.encoder = CNN(config)
        elif model_type == "gated_cnn":
            self.encoder = GatedCNN(config)
        elif model_type == "stack_gated_cnn":
            self.encoder = StackGatedCNN(config)
        elif model_type == "rcnn":
            self.encoder = RCNN(config)
        elif model_type == "bert":
            self.use_bert = True
            self.encoder = BertModel.from_pretrained(config["pretrain_model_path"], return_dict=False)
            hidden_size = self.encoder.config.hidden_size
        elif model_type == "bert_lstm":
            self.use_bert = True
            self.encoder = BertLSTM(config)
            hidden_size = self.encoder.bert.config.hidden_size
        elif model_type == "bert_cnn":
            self.use_bert = True
            self.encoder = BertCNN(config)
            hidden_size = self.encoder.bert.config.hidden_size
        elif model_type == "bert_mid_layer":
            self.use_bert = True
            self.encoder = BertMidLayer(config)
            hidden_size = self.encoder.bert.config.hidden_size

        self.classify = nn.Linear(hidden_size, class_num)
        self.pooling_style = config["pooling_style"]
        self.loss = nn.functional.cross_entropy  #loss采用交叉熵损失

    #当输入真实标签,返回loss值;无真实标签,返回预测值
    def forward(self, x, target=None):
        if self.use_bert:  # bert返回的结果是 (sequence_output, pooler_output)
            #sequence_output:batch_size, max_len, hidden_size
            #pooler_output:batch_size, hidden_size
            x = self.encoder(x)
        else:
            x = self.embedding(x)  # input shape:(batch_size, sen_len)
            x = self.encoder(x)  # input shape:(batch_size, sen_len, input_dim)

        if isinstance(x, tuple):  #RNN类的模型会同时返回隐单元向量,我们只取序列结果
            x = x[0]
        #可以采用pooling的方式得到句向量
        if self.pooling_style == "max":
            self.pooling_layer = nn.MaxPool1d(x.shape[1])
        else:
            self.pooling_layer = nn.AvgPool1d(x.shape[1])
        x = self.pooling_layer(x.transpose(1, 2)).squeeze() #input shape:(batch_size, sen_len, input_dim)

        #也可以直接使用序列最后一个位置的向量
        # x = x[:, -1, :]
        predict = self.classify(x)   #input shape:(batch_size, input_dim)
        if target is not None:
            return self.loss(predict, target.squeeze())
        else:
            return predict


class CNN(nn.Module):
    def __init__(self, config):
        super(CNN, self).__init__()
        hidden_size = config["hidden_size"]
        kernel_size = config["kernel_size"]
        pad = int((kernel_size - 1)/2)
        self.cnn = nn.Conv1d(hidden_size, hidden_size, kernel_size, bias=False, padding=pad)

    def forward(self, x): #x : (batch_size, max_len, embeding_size)
        return self.cnn(x.transpose(1, 2)).transpose(1, 2)

class GatedCNN(nn.Module):
    def __init__(self, config):
        super(GatedCNN, self).__init__()
        self.cnn = CNN(config)
        self.gate = CNN(config)

    def forward(self, x):
        a = self.cnn(x)
        b = self.gate(x)
        b = torch.sigmoid(b)
        return torch.mul(a, b)


class StackGatedCNN(nn.Module):
    def __init__(self, config):
        super(StackGatedCNN, self).__init__()
        self.num_layers = config["num_layers"]
        self.hidden_size = config["hidden_size"]
        #ModuleList类内可以放置多个模型,取用时类似于一个列表
        self.gcnn_layers = nn.ModuleList(
            GatedCNN(config) for i in range(self.num_layers)
        )
        self.ff_liner_layers1 = nn.ModuleList(
            nn.Linear(self.hidden_size, self.hidden_size) for i in range(self.num_layers)
        )
        self.ff_liner_layers2 = nn.ModuleList(
            nn.Linear(self.hidden_size, self.hidden_size) for i in range(self.num_layers)
        )
        self.bn_after_gcnn = nn.ModuleList(
            nn.LayerNorm(self.hidden_size) for i in range(self.num_layers)
        )
        self.bn_after_ff = nn.ModuleList(
            nn.LayerNorm(self.hidden_size) for i in range(self.num_layers)
        )

    def forward(self, x):
        #仿照bert的transformer模型结构,将self-attention替换为gcnn
        for i in range(self.num_layers):
            gcnn_x = self.gcnn_layers[i](x)
            x = gcnn_x + x  #通过gcnn+残差
            x = self.bn_after_gcnn[i](x)  #之后bn
            # # 仿照feed-forward层,使用两个线性层
            l1 = self.ff_liner_layers1[i](x)  #一层线性
            l1 = torch.relu(l1)               #在bert中这里是gelu
            l2 = self.ff_liner_layers2[i](l1) #二层线性
            x = self.bn_after_ff[i](x + l2)        #残差后过bn
        return x


class RCNN(nn.Module):
    def __init__(self, config):
        super(RCNN, self).__init__()
        hidden_size = config["hidden_size"]
        self.rnn = nn.RNN(hidden_size, hidden_size)
        self.cnn = GatedCNN(config)

    def forward(self, x):
        x, _ = self.rnn(x)
        x = self.cnn(x)
        return x

class BertLSTM(nn.Module):
    def __init__(self, config):
        super(BertLSTM, self).__init__()
        self.bert = BertModel.from_pretrained(config["pretrain_model_path"], return_dict=False)
        self.rnn = nn.LSTM(self.bert.config.hidden_size, self.bert.config.hidden_size, batch_first=True)

    def forward(self, x):
        x = self.bert(x)[0]
        x, _ = self.rnn(x)
        return x

class BertCNN(nn.Module):
    def __init__(self, config):
        super(BertCNN, self).__init__()
        self.bert = BertModel.from_pretrained(config["pretrain_model_path"], return_dict=False)
        config["hidden_size"] = self.bert.config.hidden_size
        self.cnn = CNN(config)

    def forward(self, x):
        x = self.bert(x)[0]
        x = self.cnn(x)
        return x

class BertMidLayer(nn.Module):
    def __init__(self, config):
        super(BertMidLayer, self).__init__()
        self.bert = BertModel.from_pretrained(config["pretrain_model_path"], return_dict=False)
        self.bert.config.output_hidden_states = True

    def forward(self, x):
        layer_states = self.bert(x)[2]#(13, batch, len, hidden)
        layer_states = torch.add(layer_states[-2], layer_states[-1])
        return layer_states


#优化器的选择
def choose_optimizer(config, model):
    optimizer = config["optimizer"]
    learning_rate = config["learning_rate"]
    if optimizer == "adam":
        return Adam(model.parameters(), lr=learning_rate)
    elif optimizer == "sgd":
        return SGD(model.parameters(), lr=learning_rate)


if __name__ == "__main__":
    from config import Config
    # Config["class_num"] = 3
    # Config["vocab_size"] = 20
    # Config["max_length"] = 5
    Config["model_type"] = "bert"
    model = BertModel.from_pretrained(Config["pretrain_model_path"], return_dict=False)
    x = torch.LongTensor([[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]])
    sequence_output, pooler_output = model(x)
    print(x[2], type(x[2]), len(x[2]))


    # model = TorchModel(Config)
    # label = torch.LongTensor([1,2])
    # print(model(x, label))

5.模型效果评估 evaluate.py

Ⅰ、初始化

  • config :配置字典,包含验证数据路径等参数。
  • model :待评估的模型。
  • logger :日志记录器,用于输出评估结果。
  • 加载验证数据 self.valid_data
  • 初始化 self.stats_dict ,用于存储预测结果的统计信息(正确和错误的预测数量)。
    def __init__(self, config, model, logger):
        self.config = config
        self.model = model
        self.logger = logger
        self.valid_data = load_data(config["valid_data_path"], config, shuffle=False)
        self.stats_dict = {"correct":0, "wrong":0}  #用于存储测试结果

Ⅱ、模型效果测试

评估模型在验证集上的表现。

  • 将模型设置为评估模式( self.model.eval() )。
  • 清空上一轮的统计结果。
  • 遍历验证数据 self.valid_data ,对每个批次的数据进行预测:
    • 如果 GPU 可用,将数据移动到 GPU。
    • 使用模型对输入 input_ids 进行预测,得到预测结果 pred_results
    • 调用 write_stats 方法更新统计信息。
  • 调用 show_stats 方法输出评估结果,并返回准确率 acc

logger.info(): 记录信息级别的日志消息,通常用于输出程序运行中的一般信息

参数描述
msg要记录的日志消息(字符串)。
*args用于格式化日志消息的可变参数(可选)。
**kwargs关键字参数,如 exc_infostack_info 等(可选)。

model.eval(): 将模型设置为评估模式,关闭 Dropout 和 BatchNorm 层的训练模式,确保模型在推理时行为一致

enumerate(): 返回一个枚举对象,生成索引和对应元素的元组,便于遍历时获取索引和值

参数描述
iterable可迭代对象(如列表、元组等)。
start索引的起始值(可选,默认为 0)。

torch.cuda.is_available(): 检查当前系统是否支持 CUDA(即是否有可用的 GPU),返回布尔值( TrueFalse

.cuda(): 将张量或模型移动到 GPU 上,以便利用 GPU 进行计算加速

参数描述
device目标设备(如 torch.device('cuda:0') ,可选)。

torch_no_grad(): 禁用梯度计算,通常在模型推理或评估时使用,以减少内存消耗并提高效率

    def eval(self, epoch):
        self.logger.info("开始测试第%d轮模型效果:" % epoch)
        self.model.eval()
        self.stats_dict = {"correct": 0, "wrong": 0}  # 清空上一轮结果
        for index, batch_data in enumerate(self.valid_data):
            if torch.cuda.is_available():
                batch_data = [d.cuda() for d in batch_data]
            input_ids, labels = batch_data   #输入变化时这里需要修改,比如多输入,多输出的情况
            with torch.no_grad():
                pred_results = self.model(input_ids) #不输入labels,使用模型当前参数进行预测
            self.write_stats(labels, pred_results)
        acc = self.show_stats()
        return acc

Ⅲ、更新统计信息

更新预测结果的统计信息

  • 确保 labelspred_results 的长度一致。
  • 对每个样本,计算预测标签(通过 torch.argmax 获取最大概率的类别)。
  • 如果预测标签与真实标签一致,增加 correct 计数;否则增加 wrong 计数。

zip(): 将多个可迭代对象的元素打包成元组,返回一个迭代器,便于同时遍历多个序列

参数描述
*iterables多个可迭代对象(如列表、元组等)。

torch.argmax(): 返回张量中最大值所在的索引,常用于分类任务中获取预测类别

参数描述
input输入张量。
dim指定沿哪个维度查找最大值索引(可选)。
keepdim是否保持输出张量的维度(可选,默认为 False )。

    def write_stats(self, labels, pred_results):
        assert len(labels) == len(pred_results)
        for true_label, pred_label in zip(labels, pred_results):
            pred_label = torch.argmax(pred_label)
            if int(true_label) == int(pred_label):
                self.stats_dict["correct"] += 1
            else:
                self.stats_dict["wrong"] += 1
        return

Ⅳ、展示统计信息

logger.info(): 记录信息级别的日志消息,通常用于输出程序运行中的一般信息

参数描述
msg要记录的日志消息(字符串)。
*args用于格式化日志消息的可变参数(可选)。
**kwargs关键字参数,如 exc_infostack_info 等(可选)。
    def show_stats(self):
        correct = self.stats_dict["correct"]
        wrong = self.stats_dict["wrong"]
        self.logger.info("预测集合条目总量:%d" % (correct +wrong))
        self.logger.info("预测正确条目:%d,预测错误条目:%d" % (correct, wrong))
        self.logger.info("预测准确率:%f" % (correct / (correct + wrong)))
        self.logger.info("--------------------")
        return correct / (correct + wrong)

Ⅴ、模型效果测试

# -*- coding: utf-8 -*-
import torch
from loader import load_data

"""
模型效果测试
"""

class Evaluator:
    def __init__(self, config, model, logger):
        self.config = config
        self.model = model
        self.logger = logger
        self.valid_data = load_data(config["valid_data_path"], config, shuffle=False)
        self.stats_dict = {"correct":0, "wrong":0}  #用于存储测试结果

    def eval(self, epoch):
        self.logger.info("开始测试第%d轮模型效果:" % epoch)
        self.model.eval()
        self.stats_dict = {"correct": 0, "wrong": 0}  # 清空上一轮结果
        for index, batch_data in enumerate(self.valid_data):
            if torch.cuda.is_available():
                batch_data = [d.cuda() for d in batch_data]
            input_ids, labels = batch_data   #输入变化时这里需要修改,比如多输入,多输出的情况
            with torch.no_grad():
                pred_results = self.model(input_ids) #不输入labels,使用模型当前参数进行预测
            self.write_stats(labels, pred_results)
        acc = self.show_stats()
        return acc

    def write_stats(self, labels, pred_results):
        assert len(labels) == len(pred_results)
        for true_label, pred_label in zip(labels, pred_results):
            pred_label = torch.argmax(pred_label)
            if int(true_label) == int(pred_label):
                self.stats_dict["correct"] += 1
            else:
                self.stats_dict["wrong"] += 1
        return

    def show_stats(self):
        correct = self.stats_dict["correct"]
        wrong = self.stats_dict["wrong"]
        self.logger.info("预测集合条目总量:%d" % (correct +wrong))
        self.logger.info("预测正确条目:%d,预测错误条目:%d" % (correct, wrong))
        self.logger.info("预测准确率:%f" % (correct / (correct + wrong)))
        self.logger.info("--------------------")
        return correct / (correct + wrong)

6.模型训练文件 main.py

Ⅰ、日志配置

配置日志输出格式和级别,方便在训练过程中记录信息。

logging.basicConfig(): 配置日志系统的基本设置,如日志级别、输出格式和输出目标

参数描述
level设置日志级别(如 logging.INFO )。
format日志输出格式(如 '%(asctime)s - %(levelname)s - %(message)s' )。
filename日志输出到文件(可选)。
filemode文件打开模式(如 'w''a' ,可选)。
datefmt日期时间格式(可选)。

logging.getLogger(): 获取或创建一个日志记录器对象,用于记录日志

参数描述
name日志记录器的名称(可选,默认为根记录器)。
# [DEBUG, INFO, WARNING, ERROR, CRITICAL]
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

Ⅱ、设置随机数种子

设置随机种子,确保实验的可重复性

random.seed(): 初始化随机数生成器,确保生成的随机数序列可复现

参数描述
seed随机数生成器的种子值(整数)。

np.random.seed(): 设置 NumPy 随机数生成器的种子,确保生成的随机数序列可复现

参数描述
seed随机数生成器的种子值(整数)。

torch.manual_seed(): 设置 PyTorch 随机数生成器的种子,确保生成的随机数序列可复现

参数描述
seed随机数生成器的种子值(整数)

torch.cuda.manual_seed(): 设置 GPU 随机数生成器的种子,确保生成的随机数序列可复现

参数描述
seed随机数生成器的种子值(整数)。
seed = Config["seed"]
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)

Ⅲ、模型训练核心步骤

① 创建模型保存目录

如果模型保存目录不存在,则创建该目录

os.path.isdir(): 检查指定路径是否为目录,返回布尔值

参数描述
path要检查的路径(字符串)

os.mkdir(): 创建指定路径的目录

参数描述
path要创建的目录路径(字符串)。
mode目录权限模式(可选,默认为 0o777 )。
if not os.path.isdir(config["model_path"]):
    os.mkdir(config["model_path"])
② 加载训练数据

从指定路径加载训练数据

train_data = load_data(config["train_data_path"], config)
③ 加载模型

根据配置文件初始化模型

model = TorchModel(config)
④ 检查GPU可用性

检查 GPU 是否可用,如果可用则将模型迁移到 GPU。

torch.cuda.is_available(): 检查当前系统是否支持 CUDA(即是否有可用的 GPU),返回布尔值

model.cuda(): 将模型迁移到 GPU 上,以便利用 GPU 进行计算

cuda_flag = torch.cuda.is_available()
if cuda_flag:
    logger.info("gpu可以使用,迁移模型至gpu")
    model = model.cuda()
⑤ 加载优化器

根据配置文件选择优化器

    optimizer = choose_optimizer(config, model)
⑥ 加载评估器

初始化评估器,用于在训练过程中评估模型性能

evaluator = Evaluator(config, model, logger)
⑦ 训练主流程
  • 遍历每个 epoch,将模型设置为训练模式。
  • 对每个批次的数据进行前向传播、计算损失、反向传播和参数更新。
  • 记录并输出每个 epoch 的平均损失。
  • 调用评估器评估模型性能。

model.train(): 将模型设置为训练模式,启用 Dropout 和 BatchNorm 层的训练行为

logger.info(): 记录信息级别的日志消息

参数描述
msg要记录的日志消息(字符串)。
*args格式化日志消息的可变参数(可选)。
**kwargs关键字参数(如 exc_info ,可选)。

enumerate(): 返回一个枚举对象,生成索引和对应元素的元组

参数描述
iterable可迭代对象(如列表、元组等)。
start索引的起始值(可选,默认为 0)。

cuda(): 将张量或模型移动到 GPU 上

参数描述
device目标设备(如 torch.device('cuda:0') ,可选)。

optimizer.zero_grad(): 清空优化器中所有参数的梯度,避免梯度累积

loss.backward(): 计算损失函数对模型参数的梯度

optimizer.step(): 根据梯度更新模型参数

列表.append(): 在列表末尾添加一个元素

参数描述
item要添加到列表末尾的元素。

np.mean(): 计算数组或列表的均值

参数描述
a输入数组或列表。
axis计算均值的轴(可选)。
dtype输出数据类型(可选)。
for epoch in range(config["epoch"]):
    epoch += 1
    model.train()
    logger.info("epoch %d begin" % epoch)
    train_loss = []
    for index, batch_data in enumerate(train_data):
        if cuda_flag:
            batch_data = [d.cuda() for d in batch_data]

        optimizer.zero_grad()
        input_ids, labels = batch_data
        loss = model(input_ids, labels)
        loss.backward()
        optimizer.step()

        train_loss.append(loss.item())
        if index % int(len(train_data) / 2) == 0:
            logger.info("batch loss %f" % loss)
    logger.info("epoch average loss: %f" % np.mean(train_loss))
    acc = evaluator.eval(epoch)
⑧ 保存模型

将训练好的模型权重保存到指定路径

os.path.join(): 将多个路径组件连接成一个完整的路径

参数描述
*paths多个路径组件(字符串)。

torch.save(): 保存模型或张量到指定文件

参数描述
obj要保存的对象(如模型或张量)。
f文件路径或文件对象。
model_path = os.path.join(config["model_path"], "epoch_%d.pth" % epoch)
torch.save(model.state_dict(), model_path)

Ⅳ、模型训练文件 main.py

① 网格搜索

遍历不同的模型类型、学习率、隐藏层大小、批次大小和池化方式,调用 main 函数训练模型,并记录准确率。

pd.DataFrame(): Pandas 库中的一个构造函数,用于创建一个二维的、带有标签的表格型数据结构(即 DataFrame)

参数描述
data用于创建 DataFrame 的数据源,可以是字典、列表、NumPy 数组、另一个 DataFrame 等。
index行索引标签,可以是索引对象或数组。如果未指定,默认从 0 开始的整数索引。
columns列名标签,可以是列名列表。如果未指定且 data 是字典,则使用字典的键作为列名。
dtype数据类型,用于强制 DataFrame 中的数据类型。如果未指定,Pandas 将自动推断数据类型。
copy布尔值,指示是否在创建 DataFrame 时复制数据。默认为 False ,即不复制数据。

DataFrame的常用属性和方法

  • 属性
    • df.columns :返回列名。
    • df.index :返回行索引。
    • df.shape :返回 DataFrame 的行数和列数。
    • df.dtypes :返回每列的数据类型。
    • df.values :返回 DataFrame 的值,作为一个 NumPy 数组。
  • 方法
    • df.head(n) :返回前 n 行。
    • df.tail(n) :返回后 n 行。
    • df.describe() :返回数值列的统计描述。
    • df.sort_values(by) :根据某列的值排序。
    • df.drop(labels) :删除指定的行或列。
    • df.fillna(value) :用指定值填充缺失值。
import pandas as pd
df = pd.DataFrame(columns=["model_type", "learning_rate", "hidden_size", "batch_size", "pooling_style", "acc"])
for model in ['fast_text', 'lstm', 'bert', 'cnn', 'rcnn', 'stack_gated_cnn']:
    Config["model_type"] = model
    for lr in [1e-3, 1e-4]:
        Config["learning_rate"] = lr
        for hidden_size in [128, 256]:
            Config["hidden_size"] = hidden_size
            for batch_size in [64, 256]:
                Config["batch_size"] = batch_size
                for pooling_style in ["avg", 'max']:
                    Config["pooling_style"] = pooling_style
                    acc = main(Config)
                    df = df._append({"model_type": model, "learning_rate": lr, "hidden_size": hidden_size, "batch_size": batch_size, "pooling_style": pooling_style, "acc": acc}, ignore_index=True)
② 保存结果

将网格搜索的结果保存到 Excel 文件中

to_excel(): 将 DataFrame 数据写入 Excel 文件

参数描述
path文件路径(字符串)。
sheet_name工作表名称(可选)。
index是否写入索引(可选,默认为 True )。
df.to_excel("result.xlsx", index=False)

df.append(): Pandas 中用于将一个 DataFrame 或 Series 添加到另一个 DataFrame 末尾的方法。它不会修改原始 DataFrame,而是返回一个新的 DataFrame。

参数描述
other要添加的 DataFrame 或 Series 对象。
ignore_index如果为 True ,则忽略原始索引并生成新的整数索引。默认为 False
verify_integrity如果为 True ,则检查新索引是否唯一。如果存在重复索引,抛出 ValueError 。默认为 False
sort如果为 True ,则按列名的字母顺序对结果进行排序。默认为 False

# -*- coding: utf-8 -*-

import torch
import os
import random
import os
import numpy as np
import logging
from config import Config
from model import TorchModel, choose_optimizer
from evaluate import Evaluator
from loader import load_data

# [DEBUG, INFO, WARNING, ERROR, CRITICAL]
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

"""
模型训练主程序
"""

seed = Config["seed"]
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)


def main(config):
    # 创建保存模型的目录
    if not os.path.isdir(config["model_path"]):
        os.mkdir(config["model_path"])
    # 加载训练数据
    train_data = load_data(config["train_data_path"], config)
    # 加载模型
    model = TorchModel(config)
    # 标识是否使用gpu
    cuda_flag = torch.cuda.is_available()
    if cuda_flag:
        logger.info("gpu可以使用,迁移模型至gpu")
        model = model.cuda()
    # 加载优化器
    optimizer = choose_optimizer(config, model)
    # 加载效果测试类
    evaluator = Evaluator(config, model, logger)
    # 训练
    for epoch in range(config["epoch"]):
        epoch += 1
        model.train()
        logger.info("epoch %d begin" % epoch)
        train_loss = []
        for index, batch_data in enumerate(train_data):
            if cuda_flag:
                batch_data = [d.cuda() for d in batch_data]

            optimizer.zero_grad()
            input_ids, labels = batch_data  # 输入变化时这里需要修改,比如多输入,多输出的情况
            loss = model(input_ids, labels)
            loss.backward()
            optimizer.step()

            train_loss.append(loss.item())
            if index % int(len(train_data) / 2) == 0:
                logger.info("batch loss %f" % loss)
        logger.info("epoch average loss: %f" % np.mean(train_loss))
        acc = evaluator.eval(epoch)

    model_path = os.path.join(config["model_path"], "epoch_%d.pth" % epoch)
    torch.save(model.state_dict(), model_path)  #保存模型权重
    return acc


if __name__ == "__main__":
    # main(Config)

    # for model in ["cnn"]:
    #     Config["model_type"] = model
    #     print("最后一轮准确率:", main(Config), "当前配置:", Config["model_type"])

    # 对比所有模型
    # 中间日志可以关掉,避免输出过多信息
    # 超参数的网格搜索,结果写入excel
    import pandas as pd
    df = pd.DataFrame(columns=["model_type", "learning_rate", "hidden_size", "batch_size", "pooling_style", "acc"])
    for model in ['fast_text', 'lstm', 'bert', 'cnn', 'rcnn', 'stack_gated_cnn']:
        Config["model_type"] = model
        for lr in [1e-3, 1e-4]:
            Config["learning_rate"] = lr
            for hidden_size in [128, 256]:
                Config["hidden_size"] = hidden_size
                for batch_size in [64, 256]:
                    Config["batch_size"] = batch_size
                    for pooling_style in ["avg", 'max']:
                        Config["pooling_style"] = pooling_style
                        acc = main(Config)
                        df = df._append({"model_type": model, "learning_rate": lr, "hidden_size": hidden_size, "batch_size": batch_size, "pooling_style": pooling_style, "acc": acc}, ignore_index=True)
    df.to_excel("result.xlsx", index=False)

7.模型分类效果表格

通过网盘分享的文件:电商评论分类_Lcl.xls

链接: 提取码: cndv

–来自百度网盘超级会员v3的分享

https://i-blog.csdnimg.cn/direct/70719431ab7f4900995b5ed5d27df579.png

https://i-blog.csdnimg.cn/direct/1c62793551a74b18897eb8739855a3d6.png

https://i-blog.csdnimg.cn/direct/6263aea94b424321aa316bea19160889.png