Day08【使用不同的网络模型实现外卖评价情感分类】

使用不同的网络模型实现外卖评价情感分类

      • 1.目标
      • 2.文件目录
      • 3.主程序
        • 导入库
        • 配置全局随机种子
        • 训练主程序
        • 并行执行训练任务
        • 主程序入口
        • 超参数网格搜索
        • 并行训练和结果存储
      • 4.模型参数配置
      • 5.数据加载处理
        • 导入的库
        • DataGenerator
          • __init__(self, data_path, config)
          • load(self)
          • encode_sentence(self, text)
          • padding(self, input_id)
          • __len__(self)
          • __getitem__(self, index)
        • load_vocab(vocab_path)
        • load_data(data_path, config, shuffle=True)
        • 处理过程
        • 功能总结
      • 6.模型结构
        • TorchModel类
        • cnn变体类
        • bert变体类
        • choose_optimizer函数
        • 总结
      • 7.测试及评估
        • __init__方法
        • eval方法
        • write_stats 方法
        • show_stats 方法
        • Model Comparison Data

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_cnnbertlstm),并且保证了每次实验的可重复性。
  • 通过日志记录训练过程,帮助跟踪训练进度和调试。
导入库
  • 导入了大量用于深度学习、数据处理和训练过程管理的库。包括:
    • 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 参数包含所有配置(如超参数、数据路径、设备类型等)。

  • 步骤:

    1. 设置全局随机种子:确保每次运行结果一致。
    2. 创建模型保存目录:检查并创建保存模型的目录。
    3. 加载训练和测试数据:调用 load_data 函数加载训练数据和测试数据。
    4. 加载模型:实例化 TorchModel 并将其移至指定设备(如 GPU 或 CPU)。
    5. 选择优化器:调用 choose_optimizer 函数根据配置选择优化器。
    6. 训练过程:按配置中的 epoch 数量进行训练:
      • 在每个 epoch 中,遍历训练数据,计算损失,并执行反向传播和优化器步骤。
      • 每半个批次输出一次当前批次的损失。
    7. 评估模型:每个 epoch 结束后使用 Evaluator 类进行模型评估。
    8. 返回准确率:最终返回模型在测试数据上的准确率。
并行执行训练任务
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_cnnbertlstm)。
    • learning_rate:学习率(1e-31e-4)。
    • hidden_size:隐藏层大小(128)。
    • batch_size:批大小(64 和 128)。
    • pooling_style:池化方式(avgmax)。
并行训练和结果存储
  • 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,它包含了模型训练和评估过程中所需的参数。以下是对每个参数的详细解释:

  1. device_type: "cpu"

    • 定义了训练和评估时使用的设备类型。在这里,设备类型被设置为 cpu,意味着模型将在 CPU 上运行。若设置为 cuda,则表示使用 GPU 进行训练。
  2. model_path: "output"

    • 该路径用于存储训练后保存的模型文件。模型训练完成后会保存在这个目录中。
  3. train_data_path: "外卖点评数据.csv"

    • 训练数据集的路径,指定了训练时使用的数据文件。在这里,数据集是一个 CSV 文件,包含外卖点评的数据。
  4. valid_data_path: "外卖点评数据.csv"

    • 验证数据集的路径,指定了模型验证时使用的数据文件。在此配置中,验证数据集与训练数据集是同一个文件。
  5. vocab_path: "chars.txt"

    • 字符或词汇表文件的路径,该文件包含了训练模型时所使用的词汇信息,通常是每个词汇的索引映射。
  6. model_type: "bert"

    • 指定所使用的模型类型。在这里,选择的是 bert 模型,这是一个基于 Transformer 的预训练语言模型,适用于多种自然语言处理任务。
  7. class_num: 2

    • 该参数指定模型要分类的类别数。在这个例子中,模型是一个二分类问题,分类数为 2。
  8. max_length: 30

    • 输入序列的最大长度。文本输入会被截断或填充到这个长度,确保所有输入的长度一致。这里设置最大长度为 30。
  9. hidden_size: 256

    • 模型隐藏层的大小,指的是模型内部每个层的特征维度。这里设置为 256,意味着每个隐藏层的输出维度为 256。
  10. kernel_size: 3

    • 该参数通常用于卷积神经网络 (CNN),这里假设是与卷积层相关的参数。设定卷积核大小为 3,表示卷积操作会使用大小为 3 的窗口。
  11. num_layers: 2

    • 模型中层的数量。这个参数通常是指神经网络的层数。这里设置为 2,意味着模型将有 2 层(可能是指 Transformer 层或者其他类型的网络层)。
  12. epoch: 12

    • 训练的轮次数。训练过程中,模型会进行 12 次完整的遍历训练数据集。
  13. batch_size: 128

    • 每个批次的数据量。即每次训练时,模型会使用 128 个样本来更新参数。
  14. pooling_style: "max"

    • 池化方式。在这里,选择了 max 池化方式,表示在进行池化操作时,会选择最大值作为池化结果。max pooling 是一种常见的池化策略,常用于减少特征维度。
  15. optimizer: "adam"

    • 使用的优化器类型。在这里,设置为 adam,这是一种常用的优化算法,适用于大多数深度学习任务。Adam(Adaptive Moment Estimation)结合了动量和自适应学习率的优点。
  16. learning_rate: 1e-3

    • 学习率,控制模型参数更新的步伐大小。这里设置为 1e-3,即 0.001,表示每次参数更新的步长。
  17. pretrain_model_path: r"..//..//..//bert-base-chinese"

    • 预训练模型的路径。在这里,指向了一个中文的 BERT 模型(bert-base-chinese)。该模型会在训练开始前加载,用于初始化模型的权重。
  18. split_ratio: 0.2

    • 数据集的拆分比例。这里设置为 0.2,表示将 20% 的数据用于验证(测试),剩余的 80% 用于训练。
  19. num_workers: 4

    • 数据加载时使用的线程数。设置为 4,表示将使用 4 个工作线程来加载数据,从而加速数据的加载过程。
  20. 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: 是否对数据进行洗牌。
处理过程
  1. 数据加载:创建 DataGenerator 实例,从文件中加载数据。
  2. 数据分割:根据 split_ratio 配置,将数据分割为训练集和验证集。使用 random_split 来分割数据,并确保分割操作的可重复性。
  3. 创建 DataLoader
    • train_loader: 使用训练数据集创建 DataLoader
    • val_loader: 使用验证数据集创建 DataLoader
  4. 返回训练集和验证集的 DataLoader
功能总结
  • 文本预处理
    • 代码支持两种不同的文本编码方式:一种是针对 BERT 的分词器(BertTokenizer),另一种是基于自定义词汇表的编码方式。
    • 通过 padding() 方法确保输入序列具有一致的长度,适用于批处理操作。
  • 数据集处理
    • 通过 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_softmaxgather 计算损失。

  • forward 方法:
    模型的前向传播过程。根据是否使用 BERT(self.use_bert),处理输入数据。

    • 如果使用 BERT,输入通过 BERT 编码器得到输出(BERT 返回的是 sequence_outputpooler_output)。
    • 否则,先通过嵌入层 self.embedding 转换为嵌入向量,再通过选择的编码器进行处理。

    然后,通过 pooling_style 决定使用最大池化(MaxPool1d)还是平均池化(AvgPool1d)来生成句向量。最后,通过 self.classify 生成预测。

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 配置,选择适当的优化器(AdamSGD)并返回:

  • 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)
  • correctwrong: 从 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%

你可能感兴趣的:(自然语言处理,文本分类,自然语言处理,分类)