本文通过自定义多种网络结构模型,通过对外卖评价数据处理,实现对评价数据的正负向情感分类。最终通过网格参数搜索方式,选择一组较好的网络结构参数,达到最佳分类准确率。
main.py
config.py
loader.py
model.py
evaluate.py
chars.txt
外卖点评数据.csv
# -*- 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}秒")
multiprocessing
来加速超参数搜索过程,支持多种模型的训练(如 gated_cnn
、bert
、lstm
),并且保证了每次实验的可重复性。def set_global_determinism(seed):
...
torch.use_deterministic_algorithms(True)
设置为确定性算法,保证每次训练的计算结果一致。torch.manual_seed(seed)
和 np.random.seed(seed)
等设置随机种子,确保训练中涉及的随机操作可重现。def main(config):
...
该函数执行模型的训练过程,传入的 config
参数包含所有配置(如超参数、数据路径、设备类型等)。
步骤:
load_data
函数加载训练数据和测试数据。TorchModel
并将其移至指定设备(如 GPU 或 CPU)。choose_optimizer
函数根据配置选择优化器。Evaluator
类进行模型评估。def worker(config):
...
config
配置的副本,然后调用 main()
函数执行训练,并返回训练结果(准确率)。if __name__ == "__main__":
...
itertools.product()
生成所有可能的超参数组合。worker
函数进行训练,并在训练结束后返回结果。multiprocessing.Pool
并行执行训练任务。每个配置的训练任务都会在独立的进程中执行,从而加速模型的训练。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
函数进行训练。# -*- 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"
valid_data_path
: "外卖点评数据.csv"
vocab_path
: "chars.txt"
model_type
: "bert"
bert
模型,这是一个基于 Transformer 的预训练语言模型,适用于多种自然语言处理任务。class_num
: 2
max_length
: 30
hidden_size
: 256
kernel_size
: 3
num_layers
: 2
epoch
: 12
batch_size
: 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-base-chinese
)。该模型会在训练开始前加载,用于初始化模型的权重。split_ratio
: 0.2
num_workers
: 4
seed
: 987
此 Config
字典包含了训练和评估模型时所需的多个关键配置参数。它定义了模型的结构(如层数、隐藏层大小等)、训练设置(如学习率、优化器等)、数据路径、设备设置等。通过调整这些参数,用户可以灵活地控制模型的训练过程。
# -*- 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
封装数据集。以下是代码的详细解释:
transformers
库导入,用于加载 BERT 模型的分词器。Trainer
类。sklearn.model_selection
导入,用于将数据分割为训练集和验证集。该类用于加载和处理数据,特别是对文本数据进行编码和分词操作。
构造函数,初始化 DataGenerator
实例:
bert
,则加载 BERT 的分词器(BertTokenizer
)。bert
)。seed_everything
: 设置随机种子,以确保实验的可重复性。该方法加载并处理数据:
pd.read_csv()
)。bert
,使用 self.tokenizer.encode()
方法将文本转换为 BERT 所需的输入格式(即 token IDs)。self.encode_sentence()
方法将文本转换为自定义的 token IDs。self.data
中。该方法将句子(text
)转换为自定义模型所需的 token IDs:
self.padding()
对输入进行填充,使其长度一致。该方法用于填充输入的序列,使其符合指定的最大长度 max_length
:
返回数据集的大小,即样本的数量。
返回数据集中的某一项(输入数据和标签)。
该函数加载自定义的词汇表:
该函数用于加载数据并使用 DataLoader
封装训练和验证数据集:
DataGenerator
实例,从文件中加载数据。split_ratio
配置,将数据分割为训练集和验证集。使用 random_split
来分割数据,并确保分割操作的可重复性。train_loader
: 使用训练数据集创建 DataLoader
。val_loader
: 使用验证数据集创建 DataLoader
。DataLoader
。BertTokenizer
),另一种是基于自定义词汇表的编码方式。padding()
方法确保输入序列具有一致的长度,适用于批处理操作。DataLoader
类对训练集和验证集进行批处理。random_split()
来拆分数据集,保证训练和验证数据的独立性。加载的外卖点评数据
# -*- 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
是一个通用的深度学习模型,支持多种不同的网络架构。根据 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
,会选择不同的编码器:
nn.RNN
, nn.LSTM
, nn.GRU
作为编码器。CNN
, GatedCNN
, StackGatedCNN
, RCNN
等。BertModel
或者结合 LSTM/CNN 的 BERT 变体(如 BertLSTM
, BertCNN
, BertMidLayer
)。self.classify
是最后一个全连接层,用于输出类别预测。
cross_entropy
方法:
自定义交叉熵损失函数,避免生成完整的 one-hot 编码矩阵,直接通过 log_softmax
和 gather
计算损失。
forward
方法:
模型的前向传播过程。根据是否使用 BERT(self.use_bert
),处理输入数据。
sequence_output
和 pooler_output
)。self.embedding
转换为嵌入向量,再通过选择的编码器进行处理。然后,通过 pooling_style
决定使用最大池化(MaxPool1d
)还是平均池化(AvgPool1d
)来生成句向量。最后,通过 self.classify
生成预测。
这些类定义了不同类型的神经网络层,用于处理输入数据:
CNN
: 一个简单的卷积层,接受一个输入,进行卷积操作并返回卷积结果。GatedCNN
: 使用两个 CNN 层,其中一个用作主要的卷积层,另一个用作门控机制来控制信息流,最终通过元素级的乘法结合这两者。StackGatedCNN
: 使用多个 GatedCNN
层进行堆叠,同时使用残差连接来防止梯度消失,类似于 Transformer 中的层堆叠结构。RCNN
: 结合了 RNN 和 CNN。先通过 RNN 进行序列建模,然后通过 GatedCNN 层处理 RNN 的输出。这些类结合了 BERT 模型和其他传统模型(如 LSTM 和 CNN):
BertLSTM
: 先通过 BERT 获取序列的表示,然后将其通过 LSTM 进行进一步的序列建模。BertCNN
: 先通过 BERT 获取序列的表示,然后使用卷积层进行特征提取。BertMidLayer
: 从 BERT 的中间层获取输出,而不是仅仅使用池化输出,结合多个中间层的输出。根据 config
配置,选择适当的优化器(Adam
或 SGD
)并返回:
optimizer
: 优化器类型。learning_rate
: 学习率。weight_decay
: 权重衰减,用于正则化。这段代码实现了一个高度灵活的深度学习框架,可以根据不同的需求选择不同类型的网络结构(如传统的 RNN、LSTM、CNN,或是更现代的 BERT 变体),并支持多种优化器配置。模型的前向传播过程允许动态选择编码器和池化方式,可以广泛应用于文本分类、情感分析等任务。
# -*- 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
方法计算并输出准确率。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
)。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
: 返回准确率。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
: 如果预测错误,更新错误预测数。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_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% |