1.目标
本文通过自定义多种网络结构模型,通过对外卖评价数据处理,实现对评价数据的正负向情感分类。最终通过网格参数搜索方式,选择一组较好的网络结构参数,达到最佳分类准确率。
2.文件目录
main.py
config.py
loader.py
model.py
evaluate.py
chars.txt
外卖点评数据.csv
3.主程序
# -*- coding: utf-8 -*-
import torch
import time
import datetime
import random
import os
import numpy as np
import pandas as pd
import logging
from config import Config
from model import TorchModel, choose_optimizer
from evaluate import Evaluator
from loader import load_data
import multiprocessing
from itertools import product
from copy import deepcopy
#[DEBUG, INFO, WARNING, ERROR, CRITICAL]
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
"""
模型训练主程序
"""
from pytorch_lightning import Trainer, seed_everything
# 在主程序开始处添加
def set_global_determinism(seed):
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
torch.use_deterministic_algorithms(True)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
np.random.seed(seed)
random.seed(seed)
torch.manual_seed(seed)
try:
torch.mps.manual_seed(seed) # MPS 专用种子
except AttributeError:
pass
def main(config):
set_global_determinism(Config["seed"])
#创建保存模型的目录
if not os.path.isdir(config["model_path"]):
os.mkdir(config["model_path"])
#加载训练数据
train_data, test_data = load_data(config["train_data_path"], config)
#加载模型
model = TorchModel(config)
device = config["device_type"]
# logger.info("{0}可以使用,迁移模型至{1}".format(device,device))
model = model.to(device)
#加载优化器
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):
optimizer.zero_grad()
input_ids, labels = batch_data #输入变化时这里需要修改,比如多输入,多输出的情况
# 添加设备转移
input_ids = input_ids.to(device)
labels = labels.squeeze(1).to(device)
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(test_data, epoch)
# model_path = os.path.join(config["model_path"], "epoch_%d.pth" % epoch)
# torch.save(model.state_dict(), model_path) #保存模型权重
return acc
def worker(config):
"""并行执行的函数"""
# 深拷贝配置避免共享内存问题
current_config = deepcopy(config)
# 执行主函数并获取准确率
acc = "{:.2%}".format(main(current_config))
print("最后一轮准确率:", acc, "当前配置:", current_config)
# 返回结果字典
return {
"model_type": current_config["model_type"],
"epoch": current_config["epoch"],
"num_layers": current_config["num_layers"],
"hidden_size": current_config["hidden_size"],
"batch_size": current_config["batch_size"],
"pooling_style": current_config["pooling_style"],
"optimizer": current_config["optimizer"],
"learning_rate": current_config["learning_rate"],
"acc": acc
}
if __name__ == "__main__":
print("启动训练...")
# MPS可用性检查
logger.info(f"MPS available: {torch.backends.mps.is_available()}")
logger.info(f"MPS built: {torch.backends.mps.is_built()}")
# main(Config)
start = time.time()
# for model in ["cnn"]:
# Config["model_type"] = model
# print("最后一轮准确率:", main(Config), "当前配置:", Config["model_type"])
print("start...")
#对比所有模型
#中间日志可以关掉,避免输出过多信息
# 超参数的网格搜索
# 生成所有参数组合
param_grid = {
"model_type": ["gated_cnn", "bert", "lstm"],
"learning_rate": [1e-3, 1e-4],
"hidden_size": [128],
"batch_size": [64, 128],
"pooling_style": ["avg", "max"]
}
# 生成所有配置组合
all_configs = []
for params in product(*param_grid.values()):
config = Config.copy()
config.update(dict(zip(param_grid.keys(), params)))
all_configs.append(config)
# 并行处理
with multiprocessing.Pool(processes=multiprocessing.cpu_count()) as pool:
all_test_results = pool.map(worker, all_configs)
df = pd.DataFrame(all_test_results)
# 获取当前时间戳
timestamp = datetime.datetime.now()
# 将时间戳转换为字符串格式
timestamp_str = timestamp.strftime('%Y-%m-%d_%H-%M-%S')
filename = f"{timestamp_str}_all_test_results.xlsx"
df.to_excel(os.path.join(filename), index=False, header=True)
# all_test_results = []
# for model in ['bert', "gated_cnn", 'lstm']:
# Config["model_type"] = model
# for lr in [1e-3, 1e-4]:
# Config["learning_rate"] = lr
# for hidden_size in [128]:
# Config["hidden_size"] = hidden_size
# for batch_size in [64, 128]:
# Config["batch_size"] = batch_size
# for pooling_style in ["avg", 'max']:
# Config["pooling_style"] = pooling_style
# acc = "{:.2%}".format(main(Config))
# print("最后一轮准确率:", acc, "当前配置:", Config)
# data_dict = {"model_type": Config["model_type"], "epoch": Config["epoch"],
# "num_layers": Config["num_layers"], "hidden_size": Config["hidden_size"],
# "batch_size": Config["batch_size"], "pooling_style": Config["pooling_style"],
# "optimizer": Config["optimizer"], "learning_rate": Config["learning_rate"],
# "acc": acc}
# all_test_results.append(data_dict)
# # 将所有数据合并为DataFrame
# df = pd.DataFrame(all_test_results)
# # 写入Excel文件(如果文件已存在,会覆盖)
# df.to_excel(os.path.join("all_test_results.xlsx"), index=False, header=True)
end = time.time()
print(f"总训练时长:{end - start:.2f}秒")
- 使用了一个并行化的深度学习模型训练框架,通过网格搜索超参数,自动执行不同配置的训练任务,并将训练结果存储在 Excel 文件中。
- 使用了
multiprocessing来加速超参数搜索过程,支持多种模型的训练(如gated_cnn、bert、lstm),并且保证了每次实验的可重复性。 - 通过日志记录训练过程,帮助跟踪训练进度和调试。
导入库
- 导入了大量用于深度学习、数据处理和训练过程管理的库。包括:
- torch:PyTorch深度学习框架
- numpy, pandas:数据处理和分析
- logging:用于记录训练过程中的日志信息
- multiprocessing:用于并行计算,提升模型训练效率
- itertools.product:用于生成超参数的网格组合
- copy.deepcopy:避免修改原始配置的深拷贝
- datetime:用于生成当前时间戳,以便命名保存的结果文件
配置全局随机种子
def set_global_determinism(seed):
...
- 该函数用来设置全局的随机种子,使得训练过程具有可重复性。
torch.use_deterministic_algorithms(True)设置为确定性算法,保证每次训练的计算结果一致。- 设置 CUDA、CUDNN 等库为确定性模式,减少由于硬件优化带来的不确定性。
- 通过
torch.manual_seed(seed)和np.random.seed(seed)等设置随机种子,确保训练中涉及的随机操作可重现。
训练主程序
def main(config):
...
-
该函数执行模型的训练过程,传入的
config参数包含所有配置(如超参数、数据路径、设备类型等)。 -
步骤:
- 设置全局随机种子:确保每次运行结果一致。
- 创建模型保存目录:检查并创建保存模型的目录。
- 加载训练和测试数据:调用
load_data函数加载训练数据和测试数据。 - 加载模型:实例化
TorchModel并将其移至指定设备(如 GPU 或 CPU)。 - 选择优化器:调用
choose_optimizer函数根据配置选择优化器。 - 训练过程:按配置中的 epoch 数量进行训练:
- 在每个 epoch 中,遍历训练数据,计算损失,并执行反向传播和优化器步骤。
- 每半个批次输出一次当前批次的损失。
- 评估模型:每个 epoch 结束后使用
Evaluator类进行模型评估。 - 返回准确率:最终返回模型在测试数据上的准确率。
并行执行训练任务
def worker(config):
...
- 该函数是为了支持并行计算而设计的。
- 它会创建
config配置的副本,然后调用main()函数执行训练,并返回训练结果(准确率)。 - 每个训练任务执行完后,都会输出当前配置和最后一轮训练的准确率。
主程序入口
if __name__ == "__main__":
...
- 日志设置:在主程序开始时,检查和输出 MPS(MacOS GPU)是否可用。
- 设置训练时间:记录训练开始的时间,最后输出总训练时长。
- 超参数网格搜索:
- 使用
itertools.product()生成所有可能的超参数组合。 - 每种超参数配置都会被传入
worker函数进行训练,并在训练结束后返回结果。
- 使用
- 并行化训练:使用
multiprocessing.Pool并行执行训练任务。每个配置的训练任务都会在独立的进程中执行,从而加速模型的训练。 - 结果保存:将所有训练结果(包括模型配置和对应的准确率)保存在 Excel 文件中,文件名包含时间戳,确保每次执行都保存为不同的文件。
- 使用
pd.DataFrame()将所有训练结果转换为 DataFrame,然后通过to_excel()方法保存到 Excel 文件中。
- 使用
超参数网格搜索
param_grid = {
"model_type": ["gated_cnn", "bert", "lstm"],
"learning_rate": [1e-3, 1e-4],
"hidden_size": [128],
"batch_size": [64, 128],
"pooling_style": ["avg", "max"]
}
- 这里定义了一个超参数网格搜索的参数组合。包括:
model_type:模型类型(如gated_cnn、bert、lstm)。learning_rate:学习率(1e-3和1e-4)。hidden_size:隐藏层大小(128)。batch_size:批大小(64 和 128)。pooling_style:池化方式(avg和max)。
并行训练和结果存储
multiprocessing.Pool(processes=multiprocessing.cpu_count())用于开启与 CPU 核心数量相同数量的进程,进行并行训练。- 通过
pool.map(worker, all_configs)将每种配置传递给worker函数进行训练。 - 最终,所有训练结果被保存在 Excel 文件中,文件名包含当前时间戳,便于区分不同的实验。
4.模型参数配置
# -*- coding: utf-8 -*-
"""
配置参数信息
"""
Config = {
"device_type": "cpu",
"model_path": "output",
"train_data_path": "外卖点评数据.csv",
"valid_data_path": "外卖点评数据.csv",
"vocab_path":"chars.txt",
"model_type":"bert",
"class_num": 2,
"max_length": 30,
"hidden_size": 256,
"kernel_size": 3,
"num_layers": 2,
"epoch": 12,
"batch_size": 128,
"pooling_style":"max",
"optimizer": "adam",
"learning_rate": 1e-3,
"pretrain_model_path":r"..//..//..//bert-base-chinese",
"split_ratio": 0.2, # 训练集比例
"num_workers": 4, # 数据加载的线程数
"seed": 987
}
这段代码定义了一个配置字典 Config,它包含了模型训练和评估过程中所需的参数。以下是对每个参数的详细解释:
-
device_type:"cpu"- 定义了训练和评估时使用的设备类型。在这里,设备类型被设置为
cpu,意味着模型将在 CPU 上运行。若设置为cuda,则表示使用 GPU 进行训练。
- 定义了训练和评估时使用的设备类型。在这里,设备类型被设置为
-
model_path:"output"- 该路径用于存储训练后保存的模型文件。模型训练完成后会保存在这个目录中。
-
train_data_path:"外卖点评数据.csv"- 训练数据集的路径,指定了训练时使用的数据文件。在这里,数据集是一个 CSV 文件,包含外卖点评的数据。
-
valid_data_path:"外卖点评数据.csv"- 验证数据集的路径,指定了模型验证时使用的数据文件。在此配置中,验证数据集与训练数据集是同一个文件。
-
vocab_path:"chars.txt"- 字符或词汇表文件的路径,该文件包含了训练模型时所使用的词汇信息,通常是每个词汇的索引映射。
-
model_type:"bert"- 指定所使用的模型类型。在这里,选择的是
bert模型,这是一个基于 Transformer 的预训练语言模型,适用于多种自然语言处理任务。
- 指定所使用的模型类型。在这里,选择的是
-
class_num:2- 该参数指定模型要分类的类别数。在这个例子中,模型是一个二分类问题,分类数为 2。
-
max_length:30- 输入序列的最大长度。文本输入会被截断或填充到这个长度,确保所有输入的长度一致。这里设置最大长度为 30。
-
hidden_size:256- 模型隐藏层的大小,指的是模型内部每个层的特征维度。这里设置为 256,意味着每个隐藏层的输出维度为 256。
-
kernel_size:3- 该参数通常用于卷积神经网络 (CNN),这里假设是与卷积层相关的参数。设定卷积核大小为 3,表示卷积操作会使用大小为 3 的窗口。
-
num_layers:2- 模型中层的数量。这个参数通常是指神经网络的层数。这里设置为 2,意味着模型将有 2 层(可能是指 Transformer 层或者其他类型的网络层)。
-
epoch:12- 训练的轮次数。训练过程中,模型会进行 12 次完整的遍历训练数据集。
-
batch_size:128- 每个批次的数据量。即每次训练时,模型会使用 128 个样本来更新参数。
-
pooling_style:"max"- 池化方式。在这里,选择了
max池化方式,表示在进行池化操作时,会选择最大值作为池化结果。max pooling是一种常见的池化策略,常用于减少特征维度。
- 池化方式。在这里,选择了
-
optimizer:"adam"- 使用的优化器类型。在这里,设置为
adam,这是一种常用的优化算法,适用于大多数深度学习任务。Adam(Adaptive Moment Estimation)结合了动量和自适应学习率的优点。
- 使用的优化器类型。在这里,设置为
-
learning_rate:1e-3- 学习率,控制模型参数更新的步伐大小。这里设置为
1e-3,即 0.001,表示每次参数更新的步长。
- 学习率,控制模型参数更新的步伐大小。这里设置为
-
pretrain_model_path:r"..//..//..//bert-base-chinese"- 预训练模型的路径。在这里,指向了一个中文的 BERT 模型(
bert-base-chinese)。该模型会在训练开始前加载,用于初始化模型的权重。
- 预训练模型的路径。在这里,指向了一个中文的 BERT 模型(
-
split_ratio:0.2- 数据集的拆分比例。这里设置为 0.2,表示将 20% 的数据用于验证(测试),剩余的 80% 用于训练。
-
num_workers:4- 数据加载时使用的线程数。设置为 4,表示将使用 4 个工作线程来加载数据,从而加速数据的加载过程。
-
seed:987- 随机种子,用于保证实验的可重复性。设置了种子值为 987,以确保每次运行时生成的随机数序列相同,这样可以在不同的实验中获得一致的结果。
此 Config 字典包含了训练和评估模型时所需的多个关键配置参数。它定义了模型的结构(如层数、隐藏层大小等)、训练设置(如学习率、优化器等)、数据路径、设备设置等。通过调整这些参数,用户可以灵活地控制模型的训练过程。
5.数据加载处理
# -*- coding: utf-8 -*-
import json
import pandas as pd
import re
import os
import torch
import numpy as np
from torch.utils.data import DataLoader, random_split
from transformers import BertTokenizer
from pytorch_lightning import Trainer, seed_everything
from sklearn.model_selection import train_test_split
"""
数据加载
"""
class DataGenerator:
def __init__(self, data_path, config):
self.config = config
self.path = data_path
# self.index_to_label = {0: '家居', 1: '房产', 2: '股票', 3: '社会', 4: '文化',
# 5: '国际', 6: '教育', 7: '军事', 8: '彩票', 9: '旅游',
# 10: '体育', 11: '科技', 12: '汽车', 13: '健康',
# 14: '娱乐', 15: '财经', 16: '时尚', 17: '游戏'}
# 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":
# str = os.path.abspath(config["pretrain_model_path"])
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()
# 设置随机种子以保证数据打乱的可重复性
seed_everything(config["seed"])
def load(self):
self.data = []
df = pd.read_csv(self.path)
columns = df.columns.tolist() # 获取列名,即第一行标题
label_name, review_name = columns
# 获取每一行的数据(所有行数据)
for index, row in df.iterrows():
label, review = row.tolist()
if self.config["model_type"] == "bert":
input_id = self.tokenizer.encode(review, max_length=self.config["max_length"], pad_to_max_length=True,
truncation=True, padding='max_length')
else:
input_id = self.encode_sentence(review)
input_id = torch.LongTensor(input_id)
label_index = torch.LongTensor([label])
self.data.append([input_id, label_index])
# 打乱数据顺序
# np.random.shuffle(self.data)
# self.data = self.data[:3000]
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)
# 计算分割尺寸
train_size = int(config["split_ratio"] * len(dg))
val_size = len(dg) - train_size
# 分割数据集
train_dataset, val_dataset = random_split(dataset = dg, lengths = [train_size, val_size],
generator=torch.Generator().manual_seed(config["seed"]) # 保证分割可重复
)
# 创建DataLoader
train_loader = DataLoader(train_dataset,batch_size=config["batch_size"],shuffle=shuffle)
val_loader = DataLoader(val_dataset, batch_size=config["batch_size"],shuffle=False)
return train_loader, val_loader
这段代码是一个用于数据加载和预处理的 Python 脚本,特别是为深度学习模型(如 BERT)准备数据。它包括了一个 DataGenerator 类,用于加载和处理数据,以及一个 load_data 函数,使用 DataLoader 封装数据集。以下是代码的详细解释:
导入的库
- json: 处理 JSON 数据格式(虽然在当前代码中没有使用)。
- pandas: 用于数据处理,特别是 CSV 文件的加载。
- re: 正则表达式(虽然在当前代码中没有使用)。
- os: 用于与操作系统交互,例如读取文件路径。
- torch: PyTorch 库,用于深度学习,特别是张量操作和数据加载。
- numpy: 用于数组操作和数学计算。
- BertTokenizer: 从 Hugging Face
transformers库导入,用于加载 BERT 模型的分词器。 - pytorch_lightning: 用于简化 PyTorch 的训练过程,主要提供了
Trainer类。 - train_test_split: 从
sklearn.model_selection导入,用于将数据分割为训练集和验证集。
DataGenerator
该类用于加载和处理数据,特别是对文本数据进行编码和分词操作。
init(self, data_path, config)
构造函数,初始化 DataGenerator 实例:
- data_path: 数据文件的路径,通常是 CSV 文件。
- config: 配置字典,包含模型类型、预训练模型路径、词汇表路径等配置信息。
- self.tokenizer: 如果模型类型是
bert,则加载 BERT 的分词器(BertTokenizer)。 - self.vocab: 加载自定义的词汇表(如果模型不是
bert)。 - self.config[“vocab_size”]: 设置词汇表的大小。
seed_everything: 设置随机种子,以确保实验的可重复性。
load(self)
该方法加载并处理数据:
- 读取 CSV 文件(使用 pandas 的
pd.read_csv())。 - 提取数据中的每一行,并根据模型类型进行处理:
- 如果模型是
bert,使用self.tokenizer.encode()方法将文本转换为 BERT 所需的输入格式(即 token IDs)。 - 如果是其他模型,则使用
self.encode_sentence()方法将文本转换为自定义的 token IDs。
- 如果模型是
- 将输入文本(token IDs)和标签(label)存入
self.data中。
encode_sentence(self, text)
该方法将句子(text)转换为自定义模型所需的 token IDs:
- 遍历文本中的每个字符,将其转换为对应的词汇表索引。
- 使用
self.padding()对输入进行填充,使其长度一致。
padding(self, input_id)
该方法用于填充输入的序列,使其符合指定的最大长度 max_length:
- 如果输入序列较长,则截断;如果较短,则用零填充。
len(self)
返回数据集的大小,即样本的数量。
getitem(self, index)
返回数据集中的某一项(输入数据和标签)。
load_vocab(vocab_path)
该函数加载自定义的词汇表:
- 读取指定路径的词汇表文件(每行一个 token)。
- 将每个 token 与其对应的索引关联(词汇表索引从 1 开始,0 被保留给填充 token)。
load_data(data_path, config, shuffle=True)
该函数用于加载数据并使用 DataLoader 封装训练和验证数据集:
- data_path: 数据文件路径。
- config: 配置信息,包含数据分割比例、批大小等。
- shuffle: 是否对数据进行洗牌。
处理过程
- 数据加载:创建
DataGenerator实例,从文件中加载数据。 - 数据分割:根据
split_ratio配置,将数据分割为训练集和验证集。使用random_split来分割数据,并确保分割操作的可重复性。 - 创建 DataLoader:
train_loader: 使用训练数据集创建DataLoader。val_loader: 使用验证数据集创建DataLoader。
- 返回训练集和验证集的
DataLoader。
功能总结
- 文本预处理:
- 代码支持两种不同的文本编码方式:一种是针对 BERT 的分词器(
BertTokenizer),另一种是基于自定义词汇表的编码方式。 - 通过
padding()方法确保输入序列具有一致的长度,适用于批处理操作。
- 代码支持两种不同的文本编码方式:一种是针对 BERT 的分词器(
- 数据集处理:
- 通过
DataLoader类对训练集和验证集进行批处理。 - 通过
random_split()来拆分数据集,保证训练和验证数据的独立性。
- 通过
- 可重复性:
- 设置了随机种子,确保每次实验的结果是一致的。
加载的外卖点评数据
6.模型结构
# -*- 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.binary_cross_entropy #loss采用交叉熵损失
# 自定义交叉熵损失函数
def cross_entropy(self, pred, target):
# 直接使用 log_softmax + gather 组合,避免生成完整 one-hot 矩阵
log_softmax = nn.functional.log_softmax(pred, dim=1)
# 关键优化:用 gather 代替 one-hot 乘法
batch_loss = -log_softmax.gather(1, target.unsqueeze(1)).squeeze(1)
return batch_loss.mean()
#当输入真实标签,返回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.cross_entropy(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, weight_decay=1e-4) # 1e-4 是 L2 正则化的超参数
elif optimizer == "sgd":
return SGD(model.parameters(), lr=learning_rate, weight_decay=1e-4)
这部分定义了一个深度学习模型架构,包括不同类型的神经网络层以及优化器选择。代码的主要功能是根据配置文件 (config) 动态选择不同的网络结构,并定义了模型的前向传播过程。以下是代码的详细解释:
TorchModel类
TorchModel 是一个通用的深度学习模型,支持多种不同的网络架构。根据 config 字典中的 model_type 参数,模型可以选择不同的编码方式,例如 LSTM、GRU、CNN、BERT 等。
-
__init__方法:hidden_size: 隐藏层大小。vocab_size: 词汇表大小(包含 padding token)。class_num: 输出类别数。model_type: 选择的模型类型,决定了编码器的类型(如 LSTM、GRU、BERT 等)。num_layers: 对于 RNN 类模型,表示网络的层数。self.use_bert: 标记是否使用 BERT。self.embedding: 嵌入层,用于将词汇表中的每个词转换为固定维度的向量。
根据
model_type,会选择不同的编码器:- 对于传统的 RNN、LSTM、GRU,使用对应的
nn.RNN,nn.LSTM,nn.GRU作为编码器。 - 对于 CNN 类型模型,使用自定义的
CNN,GatedCNN,StackGatedCNN,RCNN等。 - 对于 BERT 类型模型,使用
BertModel或者结合 LSTM/CNN 的 BERT 变体(如BertLSTM,BertCNN,BertMidLayer)。
self.classify是最后一个全连接层,用于输出类别预测。 -
cross_entropy方法:
自定义交叉熵损失函数,避免生成完整的 one-hot 编码矩阵,直接通过log_softmax和gather计算损失。 -
forward方法:
模型的前向传播过程。根据是否使用 BERT(self.use_bert),处理输入数据。- 如果使用 BERT,输入通过 BERT 编码器得到输出(BERT 返回的是
sequence_output和pooler_output)。 - 否则,先通过嵌入层
self.embedding转换为嵌入向量,再通过选择的编码器进行处理。
然后,通过
pooling_style决定使用最大池化(MaxPool1d)还是平均池化(AvgPool1d)来生成句向量。最后,通过self.classify生成预测。 - 如果使用 BERT,输入通过 BERT 编码器得到输出(BERT 返回的是
cnn变体类
这些类定义了不同类型的神经网络层,用于处理输入数据:
CNN: 一个简单的卷积层,接受一个输入,进行卷积操作并返回卷积结果。GatedCNN: 使用两个 CNN 层,其中一个用作主要的卷积层,另一个用作门控机制来控制信息流,最终通过元素级的乘法结合这两者。StackGatedCNN: 使用多个GatedCNN层进行堆叠,同时使用残差连接来防止梯度消失,类似于 Transformer 中的层堆叠结构。RCNN: 结合了 RNN 和 CNN。先通过 RNN 进行序列建模,然后通过 GatedCNN 层处理 RNN 的输出。
bert变体类
这些类结合了 BERT 模型和其他传统模型(如 LSTM 和 CNN):
BertLSTM: 先通过 BERT 获取序列的表示,然后将其通过 LSTM 进行进一步的序列建模。BertCNN: 先通过 BERT 获取序列的表示,然后使用卷积层进行特征提取。BertMidLayer: 从 BERT 的中间层获取输出,而不是仅仅使用池化输出,结合多个中间层的输出。
choose_optimizer函数
根据 config 配置,选择适当的优化器(Adam 或 SGD)并返回:
optimizer: 优化器类型。learning_rate: 学习率。weight_decay: 权重衰减,用于正则化。
总结
这段代码实现了一个高度灵活的深度学习框架,可以根据不同的需求选择不同类型的网络结构(如传统的 RNN、LSTM、CNN,或是更现代的 BERT 变体),并支持多种优化器配置。模型的前向传播过程允许动态选择编码器和池化方式,可以广泛应用于文本分类、情感分析等任务。
7.测试及评估
# -*- 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 = None
self.stats_dict = {"correct":0, "wrong":0} #用于存储测试结果
def eval(self, valid_data, epoch):
self.logger.info("开始测试第%d轮模型效果:" % epoch)
self.model.eval()
# self.valid_data = valid_data
self.stats_dict = {"correct": 0, "wrong": 0} # 清空上一轮结果
device = self.config["device_type"]
for index, batch_data in enumerate(valid_data):
input_ids, labels = batch_data #输入变化时这里需要修改,比如多输入,多输出的情况
# 添加设备转移
input_ids = input_ids.to(device)
labels = labels.to(device)
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("预测准确率:%.2f%%" % (correct * 100.0 / (correct + wrong)))
self.logger.info("--------------------")
return correct / (correct + wrong)
这段代码定义了一个名为 Evaluator 的类,主要用于评估模型的预测性能。Evaluator 类的主要目的是用于模型效果的评估。它通过接收验证数据集,并计算模型在这些数据上的预测准确率,最终输出评估结果。核心功能包括:
- 在验证数据集上评估模型的预测性能。
- 使用
write_stats方法统计正确和错误的预测数量。 - 使用
show_stats方法计算并输出准确率。
__init__方法
def __init__(self, config, model, logger):
self.config = config
self.model = model
self.logger = logger
self.valid_data = None
self.stats_dict = {"correct": 0, "wrong": 0}
config: 配置字典,包含模型和评估的一些参数(例如设备类型等)。model: 训练好的模型,用于进行预测。logger: 日志记录器,用于输出评估过程中的信息。valid_data: 验证数据集,初始为空。stats_dict: 存储评估结果的字典,包括正确预测数(correct)和错误预测数(wrong)。
eval方法
def eval(self, valid_data, epoch):
self.logger.info("开始测试第%d轮模型效果:" % epoch)
self.model.eval()
self.stats_dict = {"correct": 0, "wrong": 0}
device = self.config["device_type"]
for index, batch_data in enumerate(valid_data):
input_ids, labels = batch_data
input_ids = input_ids.to(device)
labels = labels.to(device)
with torch.no_grad():
pred_results = self.model(input_ids)
self.write_stats(labels, pred_results)
acc = self.show_stats()
return acc
valid_data: 验证数据集,用于评估模型的性能。epoch: 当前的训练轮次,主要用于日志输出。self.model.eval(): 将模型设置为评估模式。在此模式下,模型会禁用掉像 dropout 等训练时特有的操作,确保推理结果稳定。device = self.config["device_type"]: 获取配置中的设备类型(如 CPU 或 GPU)。for index, batch_data in enumerate(valid_data): 遍历验证数据集。每次获取一个批次的数据。input_ids, labels = batch_data: 假设每个batch_data是一个元组,包含输入数据(input_ids)和对应的标签(labels)。input_ids.to(device)和labels.to(device): 将输入数据和标签移动到指定设备(如 GPU 或 CPU)上。with torch.no_grad(): 在评估过程中,关闭梯度计算,以节省内存并提高性能,因为不需要计算梯度。pred_results = self.model(input_ids): 使用模型对输入数据进行预测。self.write_stats(labels, pred_results): 将标签与预测结果进行对比,并更新统计信息。
acc = self.show_stats(): 调用show_stats方法计算并显示模型的准确率。return acc: 返回准确率。
write_stats 方法
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
labels: 真实标签。pred_results: 模型预测的结果。assert len(labels) == len(pred_results): 确保标签和预测结果的长度一致。for true_label, pred_label in zip(labels, pred_results): 遍历标签和预测结果对。torch.argmax(pred_label):pred_label是模型的输出,通常是一个概率分布。使用torch.argmax找出预测结果中概率最高的类别。if int(true_label) == int(pred_label): 判断预测的类别是否与真实标签相同。如果相同,认为是正确预测。self.stats_dict["correct"] += 1: 如果预测正确,更新正确预测数。self.stats_dict["wrong"] += 1: 如果预测错误,更新错误预测数。
show_stats 方法
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("预测准确率:%.2f%%" % (correct * 100.0 / (correct + wrong)))
self.logger.info("--------------------")
return correct / (correct + wrong)
correct和wrong: 从self.stats_dict获取正确预测数和错误预测数。- 日志输出: 打印预测总数、正确预测数、错误预测数以及准确率。
self.logger.info(...): 通过日志记录器输出评估结果。- 准确率计算:
correct * 100.0 / (correct + wrong),计算并输出预测准确率。
return correct / (correct + wrong): 返回准确率,准确率等于正确预测数除以总预测数。
输出不同参数配置下最后一轮测试结果:
Model Comparison Data
| model_type | epoch | num_layers | hidden_size | batch_size | pooling_style | optimizer | learning_rate | acc |
|---|---|---|---|---|---|---|---|---|
| bert | 10 | 2 | 128 | 128 | max | adam | 0.0001 | 87.75% |
| gated_cnn | 10 | 2 | 128 | 64 | max | adam | 0.001 | 87.67% |
| gated_cnn | 10 | 2 | 128 | 128 | max | adam | 0.001 | 87.40% |
| bert | 10 | 2 | 128 | 64 | max | adam | 0.0001 | 86.77% |
| bert | 10 | 2 | 128 | 128 | max | adam | 0.001 | 86.10% |
| bert | 10 | 2 | 128 | 128 | avg | adam | 0.0001 | 85.79% |
| lstm | 10 | 2 | 128 | 64 | max | adam | 0.001 | 85.52% |
| lstm | 10 | 2 | 128 | 128 | max | adam | 0.001 | 85.48% |
| bert | 10 | 2 | 128 | 64 | max | adam | 0.001 | 85.44% |
| gated_cnn | 10 | 2 | 128 | 64 | avg | adam | 0.001 | 85.41% |
| gated_cnn | 10 | 2 | 128 | 64 | max | adam | 0.0001 | 85.31% |
| lstm | 10 | 2 | 128 | 128 | avg | adam | 0.001 | 85.12% |
| bert | 10 | 2 | 128 | 64 | avg | adam | 0.0001 | 85.03% |
| gated_cnn | 10 | 2 | 128 | 128 | avg | adam | 0.001 | 84.90% |
| lstm | 10 | 2 | 128 | 64 | avg | adam | 0.0001 | 84.48% |
| lstm | 10 | 2 | 128 | 64 | avg | adam | 0.001 | 84.39% |
| bert | 10 | 2 | 128 | 64 | avg | adam | 0.001 | 84.16% |
| lstm | 10 | 2 | 128 | 64 | max | adam | 0.0001 | 84.08% |
| gated_cnn | 10 | 2 | 128 | 128 | max | adam | 0.0001 | 83.80% |
| bert | 10 | 2 | 128 | 128 | avg | adam | 0.001 | 83.65% |
| lstm | 10 | 2 | 128 | 128 | avg | adam | 0.0001 | 82.04% |
| lstm | 10 | 2 | 128 | 128 | max | adam | 0.0001 | 81.87% |
| gated_cnn | 10 | 2 | 128 | 64 | avg | adam | 0.0001 | 80.89% |
| gated_cnn | 10 | 2 | 128 | 128 | avg | adam | 0.0001 | 76.87% |

2250

被折叠的 条评论
为什么被折叠?



