[Pytorch系列-61]:循环神经网络 - 中文新闻文本分类详解-3-CNN网络训练与评估代码详解

作者主页(文火冰糖的硅基工坊):文火冰糖(王文兵)的博客_文火冰糖的硅基工坊_CSDN博客

本文网址:https://blog.csdn.net/HiWangWenBing/article/details/121802852


目录

第1章 预备知识

1.1 业务概述

1.2 CNN网络

1.3 CNN网络与LSTM网络的区别

1.4 LSTM网络的代码

第2章 代码准备 (Jupter)

2.1 代码与数据集下载

2.2 导入库

2.3 系统配置

第3章 构建数据集

3.1 构建单词表API

3.2 定义构建数据集API

3.3 构建三大数据集

3.4 构建迭代器

第4章 构建模型:CNN网络

4.1 定义模型类

4.2 实例化模型并显示模型结构

第5章 模型训练、评估

5.1 模型评估方法

5.2 模型训练方法

5.3 边训练、边评估模型

第6章 在测试集上对模型进行评估

6.1 测试方法的定义

6.2 开始测试




第1章 预备知识

1.1 业务概述

[Pytorch系列-59]:循环神经网络 - 中文新闻文本分类详解-1-业务目标分析与总体架构_文火冰糖(王文兵)的博客-CSDN博客https://blog.csdn.net/HiWangWenBing/article/details/121756744

1.2 CNN网络

(1)概述

本文使用CNN网络来实现具有时序记忆功能的文本分类应用。

我们会发现,通过合理的模型构建,

  • CNN网络具备与LSTM相似的准确率基本相当。
  • CNN网络具备与LSTM相当的复杂度。

(2) 如何为文本分类构建CNN网络

文本分类的CNN网络与图像分类的CNN网络,基本单元都是卷积核,然后,在具体实现时,与图像分类的CNN却不是完全相同的,需要一些特殊的处理。

这些特殊的处理包括:

  • 输入数据:把文本向量集与图像像素进行映射
  • CNN网络:按照文本向量数据的特点设计卷积核的形状

(3)输入数据

图片数据的数据格式为:通道数 * 长 * 宽 = 3 * 244 * 244

文本数据的格式为:通道数 * 长 * 宽 =  1 * 32 * 300

其中:

32: 表示单词的个数

300:表示词向量的长度

经过上述方法构建的输入数据,单词就不是独立的,单词与单词之间也就有了相邻关系和时序关系。一个新闻标题就等效为一张图片。

这是这种“图片”,不是正方形的图片,且只有一个通道,相当于灰色图片。

(4)如何通过卷积获取图片特征

Conv2d (in_channels, out_channels, kernel_size, stride=1,padding=0, dilation=1, groups=1,bias=True, padding_mode=‘zeros’)

  • in_channels:输入的通道数目 【必选】
  • out_channels: 输出的通道数目 【必选】
  • kernel_size:卷积核的大小,类型为int 或者元组,当卷积是方形的时候,只需要一个整数边长即可,卷积不是方形,要输入一个元组表示 高和宽。【必选】
  • stride: 卷积每次滑动的步长为多少,默认是 1 【可选】
  • padding: 设置在所有边界增加 值为 0 的边距的大小(也就是在feature map 外围增加几圈 0 ),例如当 padding =1 的时候,如果原来大小为 3 × 3 ,那么之后的大小为 5 × 5 。即在外围加了一圈 0 。【可选】

(5)如何通过卷积获取文本特征

  (convs): ModuleList(
    (0): Conv2d(1, 256, kernel_size=(2, 300), stride=(1, 1))
    (1): Conv2d(1, 256, kernel_size=(3, 300), stride=(1, 1))
    (2): Conv2d(1, 256, kernel_size=(4, 300), stride=(1, 1))

卷积核的尺寸:

  • 宽度:与单词向量相同的size=300,stride的步长为1,因此,一次卷积核的移动,移动一个单词的词向量的长度。
  • 长度=2, 3, 4:这里有三种并行的卷积核,长度分别为2, 3, 4。2表示,一次卷积,覆盖相邻的2个单词。3表示,一次卷积,覆盖相邻的3个单词。4表示,一次卷积,覆盖相邻的4个单词。每种卷积核的个数是out_channels。

Cond2d输入输出:

  • in_channels=1:输入文本的通道数是1,而不是图片通道数3,因此文本数据相当于灰色图片。
  • out_channels=256:反应的相同卷积尺寸的卷积核的个数。

(6)池化层

池化层核的尺寸与卷积核的输出特征是相同尺寸

因此,池化核的输出是1*1

一共有 256 * 3 = 768个池化核的输出 

(7)dropout

(dropout): Dropout(p=0.5, inplace=False)

(8)全连接层

(fc): Linear(in_features=768, out_features=10, bias=True)
  • in_features:256 * 3 = 768,来自池化层的输出。
  • out_features:10分类

1.3 CNN网络与LSTM网络的区别

本文的代码与LSTM网络的代码基本相同,除了如下的区别:

(1)系统配置:class Config(object):

        # 模型参数
        self.filter_sizes = (2, 3, 4)                                   # 三层卷积核,卷积核尺寸
        self.num_filters = 256                                          # 卷积核数量(channels数)
 

(2)神经网络模型结构:class Model(nn.Module)

(3)神经网络的名称:model_name = "TextCNN"

(4)训练的输出结果:训练结果

1.4 LSTM网络的代码

[Pytorch系列-60]:循环神经网络 - 中文新闻文本分类详解-2-LSTM网络训练与评估代码详解_文火冰糖(王文兵)的博客-CSDN博客作者主页(文火冰糖的硅基工坊):文火冰糖(王文兵)的博客_文火冰糖的硅基工坊_CSDN博客本文网址:目录第1章 预备知识1.1 业务概述1.2 LSTM网络第2章 代码准备 (Jupter)2.1 代码与数据集下载2.2 导入库2.3系统配置第3章 构建数据集3.1 构建单词表API3.2 定义构建数据集API3.3 构建三大数据集3.4 构建迭代器第4章 构建模型:LSTM4.1 定义模型类4.2 实例化模型并显示模型结构4.3 初.https://blog.csdn.net/HiWangWenBing/article/details/121800521

第2章 代码准备 (Jupter)

2.1 代码与数据集下载

https://download.csdn.net/download/HiWangWenBing/60358291icon-default.png?t=LA92https://download.csdn.net/download/HiWangWenBing/60358291

2.2 导入库

import time
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from tensorboardX import SummaryWriter
from sklearn import metrics
import os
import torch
import numpy as np
import pickle as pkl
from tqdm import tqdm
import time
from datetime import timedelta

2.3 系统配置

(1)系统配置数据结构

class Config(object):

    """配置参数"""
    def __init__(self, dataset, embedding):
        self.model_name = 'TextRNN'
        #数据集路径
        self.train_path = dataset + '/data/train.txt'                                # 训练集
        self.dev_path = dataset + '/data/dev.txt'                                    # 验证集
        self.test_path = dataset + '/data/test.txt'                                  # 测试集
        
        #类别文件
        self.class_list = [x.strip() for x in open(
            dataset + '/data/class.txt').readlines()]                                # 类别名单
        
        #单词表:是单词与其索引的对应表
        self.vocab_path = dataset + '/data/vocab.pkl'                                # 词表
        
        # 词向量表: 是索引与向量编码的对应表
        self.embedding_pretrained = torch.tensor(
            np.load(dataset + '/data/' + embedding)["embeddings"].astype('float32'))\
            if embedding != 'random' else None                                       # 预训练词向量

        self.embed = self.embedding_pretrained.size(1)\
            if self.embedding_pretrained is not None else 300           # 字向量维度, 若使用了预训练词向量,则维度统一
    
        # 训练数据保存
        self.save_path = dataset + '/saved_dict/' + self.model_name + '.ckpt'        # 模型训练结果
        self.log_path = dataset + '/log/' + self.model_name
        
        # GPU or CPU
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')   # 设备
        
        # 模型参数
        self.filter_sizes = (2, 3, 4)                                   # 三层卷积核,卷积核尺寸
        self.num_filters = 256                                          # 卷积核数量(channels数)

        # 训练时的参数
        self.dropout = 0.5                                              # 随机失活
        self.require_improvement = 1000                                 # 若超过1000batch效果还没提升,则提前结束训练
        self.num_classes = len(self.class_list)                         # 类别数
        self.n_vocab = 0                                                # 词表大小,在运行时赋值
        self.num_epochs = 20                                            # epoch数
        self.batch_size = 64                                           # mini-batch大小
        self.pad_size = 32                                              # 每句话处理成的长度(短填长切)
        self.learning_rate = 1e-3                                       # 学习率

(2)实例化配置对象

# 数据集目录
dataset = 'THUCNews'  

# 搜狗新闻:embedding_SougouNews.npz, 腾讯:embedding_Tencent.npz, 随机初始化:random
embedding = 'embedding_SougouNews.npz'

#通过空格分隔的英文单词还是中文的字符
word = False

#初始化配置实例
config = Config(dataset, embedding)

#显示配置信息
print(config.device)
print(config.embed)
print(config.embedding_pretrained)

第3章 构建数据集

3.1 构建单词表API

MAX_VOCAB_SIZE = 10000

#新闻标题的填充,固定输入长度为32
UNK, PAD = '<UNK>', '<PAD>'

# 单词表不是词向量表,而是单词与其索引对应关系的字典表。
# 从指定单词表中读取词向量表:
# file_path:单词表的路径
# tokenizer:分词器,与英文不同,中文的单词是仅仅相邻的,中间没有空格,因此需要分词器进行分词。
# max_size:单词的最大数量
# min_freq:单词表排序时的参考词频
def build_vocab(file_path, tokenizer, max_size, min_freq):
    # 单词表是一个字典
    vocab_dic = {}
    with open(file_path, 'r', encoding='UTF-8') as f:
        # 通过tqdm从单词表中读取一行单词,tqdm能够显示进度条
        for line in tqdm(f):
            # 移除字符串头尾指定的字符(默认为空格或换行符)或字符序列
            lin = line.strip()
            if not lin:
                #空行
                continue
            #按照空格或table键,把字符转换成短语列表
            content = lin.split('\t')[0]
            
            # 从列表中提取一个个独立的中文单词(即中文字)
            for word in tokenizer(content):
                # 构建单词字典表
                vocab_dic[word] = vocab_dic.get(word, 0) + 1
        
        #对单词表进行排序
        vocab_list = sorted([_ for _ in vocab_dic.items() if _[1] >= min_freq], key=lambda x: x[1], reverse=True)[:max_size]
        
        #还原成字典
        vocab_dic = {word_count[0]: idx for idx, word_count in enumerate(vocab_list)}
        
        #使用UNK填充单词表的尾部
        #  ,'<UNK>': 4760, '<PAD>': 4761}
        vocab_dic.update({UNK: len(vocab_dic), PAD: len(vocab_dic) + 1})
    return vocab_dic

3.2 定义构建数据集API

def build_dataset(config, ues_word):
    print("构建单词表")
    # 指定分词器
    print("ues_word=",ues_word)
    if ues_word:
        tokenizer = lambda x: x.split(' ')  # 以空格隔开,word-level
    else:
        tokenizer = lambda x: [y for y in x]  # char-level =》适合中文
    
    # load单词表
    if os.path.exists(config.vocab_path):
        # 如果有现成的单词表,则使用已有的单词表(单词与索引的字典)
        print("使用已有的单词表:", config.vocab_path)
        vocab = pkl.load(open(config.vocab_path, 'rb'))
    else:
        # 如果没有现成的单词表,则基于训练集,构建一个新的词表
        print("基于训练集,新构建单词表:", config.train_path)
        vocab = build_vocab(config.train_path, tokenizer=tokenizer, max_size=MAX_VOCAB_SIZE, min_freq=1)
        pkl.dump(vocab, open(config.vocab_path, 'wb'))
    
    print(f"Vocab size: {len(vocab)}")

    print("构建数据集")
    # 定义load和转换数据集的函数
    # 固定长度为32。
    def load_dataset(path, pad_size=32):
        contents = []
        print("数据集:", path)
        with open(path, 'r', encoding='UTF-8') as f:
            # 读取一行文件,并显示进度条
            for line in tqdm(f):
                #去掉头尾标识符
                lin = line.strip()
                if not lin:
                    # 跳过空行
                    continue
                
                #通过空格分离单词和标签
                content, label = lin.split('\t')
                
                words_line = []
                token = tokenizer(content)
                seq_len = len(token)
                
                # 根据填充单词,确定有效字符长度:seq_len
                if pad_size:
                    if len(token) < pad_size:
                        token.extend([vocab.get(PAD)] * (pad_size - len(token)))
                    else:
                        token = token[:pad_size]
                        seq_len = pad_size
                
                #构建一个个样本数据
                for word in token:
                    # 从单词表中获取每个单词对应的索引index,并添加到文字样本对应的列表中
                    # words_line:存放当个样本数据(单词的index列表)
                    words_line.append(vocab.get(word, vocab.get(UNK)))
                
                #contents:存放所有样本数据(单词的index列表)
                contents.append((words_line, int(label), seq_len))
        return contents  # [([...], 0), ([...], 1), ...]
    
    # load训练数据集
    train = load_dataset(config.train_path, config.pad_size)
    
    # load 验证数据集
    dev = load_dataset(config.dev_path, config.pad_size)
    
    # load 测试数据集
    test = load_dataset(config.test_path, config.pad_size)
    
    return vocab, train, dev, test

3.3 构建三大数据集

(1)构建数据集

def get_time_dif(start_time):
    """获取已使用时间"""
    end_time = time.time()
    time_dif = end_time - start_time
    return timedelta(seconds=int(round(time_dif)))

start_time = time.time()

#构建三大数据集
print("Loading data...")
vocab, train_data, dev_data, test_data = build_dataset(config, word)

# 更新词向量的长度
config.n_vocab = len(vocab)

time_dif = get_time_dif(start_time)
print("Time usage:", time_dif)

(2)显示单词表

print(vocab)
{' ': 0, '0': 1, '1': 2, '2': 3, ':': 4, '大': 5, '国': 6, '图': 7, '(': 8, ')': 9, '3': 10, '人': 11, '年': 12, '5': 13, '中': 14, '新': 15, '9': 16, '生': 17, '金': 18, '高': 19, '《': 20, '》': 21, '4': 22, '上': 23, '8': 24, '不': 25, '考': 26, '一': 27, '6': 28, '日': 29, '元': 30, '开': 31, '美': 32, '价': 33, '发': 34, '学': 35, '公': 36, '成': 37, '月': 38, '将': 39, '万': 40, '7': 41, '基': 42, '市': 43, '出': 44, '子': 45, '行': 46, '机': 47, '业': 48, '被': 49, '家': 50, '股': 51, '的': 52, '在': 53, '网': 54, '女': 55, '期': 56, '平': 57, '房': 58, '名': 59, '三': 60, '-': 61, '会': 62, '地': 63, '场': 64, '全': 65, '小': 66, '现': 67, '有': 68, '分': 69, '后': 70, '称': 71, '组': 72, '为': 73, '下': 74, '盘': 75, '最': 76, '“': 7

........

737, '恫': 4738, '诣': 4739, '叁': 4740, '氮': 4741, '曳': 4742, '膑': 4743, '峦': 4744, '攫': 4745, '鹄': 4746, '啄': 4747, '憩': 4748, '鞑': 4749, '垠': 4750, '鹕': 4751, '鄞': 4752, '呸': 4753, 'V': 4754, '玷': 4755, '瘁': 4756, '蚱': 4757, '§': 4758, '霎': 4759, '<UNK>': 4760, '<PAD>': 4761}

(3)显示训练数据集

# 训练集索引是单词的索引
# 样本:
# 第一组数: 输入:32个单词序列的索引,文本新闻标题样本,转换成其索引,固定长度为32个单词,不足填充=》4760:PAD
# 第二个数:分类的类别
# 第三个数:有效字符的长度(不包括填充字符)

# 训练集输入数据的长度(包括填充字符)
print(len(train_data[0][0]))

#中华女子学院:本科层次仅1专业招男生	3
print(train_data[0])

#两天价网站背后重重迷雾:做个网站究竟要多少钱	4
print(train_data[1])
32
([14, 125, 55, 45, 35, 307, 4, 81, 161, 941, 258, 494, 2, 175, 48, 145, 97, 17, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760], 3, 18)
([135, 80, 33, 54, 505, 1032, 70, 95, 95, 681, 2288, 4, 486, 179, 54, 505, 626, 1156, 180, 115, 421, 561, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760], 4, 22)

(3)显示验证数据集

print(dev_data[0])
print(dev_data[1])
([173, 714, 3, 186, 1844, 889, 0, 2641, 80, 2061, 416, 478, 382, 5, 308, 15, 1264, 1344, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760], 8, 18)
([28, 1, 12, 567, 1371, 31, 365, 899, 846, 1300, 1095, 256, 1311, 8, 72, 7, 9, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760], 5, 17)

(4)显示测试数据集

print(test_data[0])
print(test_data[1])
([1393, 686, 1350, 656, 110, 232, 1138, 0, 1, 24, 12, 26, 216, 1533, 56, 123, 434, 270, 742, 65, 112, 236, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760], 3, 22)
([14, 6, 11, 156, 36, 211, 5, 35, 3, 1, 2, 3, 12, 830, 324, 216, 626, 17, 334, 291, 461, 659, 334, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760, 4760], 3, 23)

3.4 构建迭代器

(1)定义类或函数

# 迭代器类
class DatasetIterater(object):
    def __init__(self, batches, batch_size, device):
        self.batch_size = batch_size
        self.batches = batches
        self.n_batches = len(batches) // batch_size
        self.residue = False  # 记录batch数量是否为整数
        if len(batches) % self.n_batches != 0:
            self.residue = True
        self.index = 0
        self.device = device

    def _to_tensor(self, datas):
        x = torch.LongTensor([_[0] for _ in datas]).to(self.device)
        y = torch.LongTensor([_[1] for _ in datas]).to(self.device)

        # pad前的长度(超过pad_size的设为pad_size)
        seq_len = torch.LongTensor([_[2] for _ in datas]).to(self.device)
        return (x, seq_len), y

    # 迭代函数
    def __next__(self):
        if self.residue and self.index == self.n_batches:
            batches = self.batches[self.index * self.batch_size: len(self.batches)]
            self.index += 1
            batches = self._to_tensor(batches)
            return batches

        elif self.index > self.n_batches:
            self.index = 0
            raise StopIteration
        else:
            batches = self.batches[self.index * self.batch_size: (self.index + 1) * self.batch_size]
            self.index += 1
            batches = self._to_tensor(batches)
            return batches

    def __iter__(self):
        return self

    def __len__(self):
        if self.residue:
            return self.n_batches + 1
        else:
            return self.n_batches

# 构建迭代器的API
def build_iterator(dataset, config):
    iter = DatasetIterater(dataset, config.batch_size, config.device)
    return iter

(2)实例化

# 训练集loader
train_iter = build_iterator(train_data, config)

# 验证集loader
dev_iter = build_iterator(dev_data, config)

# 测试集loader
test_iter = build_iterator(test_data, config)

print(train_iter)
<__main__.DatasetIterater object at 0x0000022804DAFD30>

第4章 构建模型:CNN网络

4.1 定义模型类

class Model(nn.Module):
    def __init__(self, config):
        super(Model, self).__init__()
        # 词向量网络
        if config.embedding_pretrained is not None:
            # 使用不需要重新训练的、预训练好的词向量,加快训练速度、提升性能
            self.embedding = nn.Embedding.from_pretrained(config.embedding_pretrained, freeze=False)
        else:
            # 使用新定义的可训练的词词向量
            self.embedding = nn.Embedding(config.n_vocab, config.embed, padding_idx=config.n_vocab - 1)
        
        # 卷积神经网络CNN
        # num_filters:卷积核的通道数
        # config.filter_sizes:卷积核尺寸:(2, 3, 4)
        self.convs = nn.ModuleList(
            [nn.Conv2d(1, config.num_filters, (k, config.embed)) for k in config.filter_sizes])
        
        # dropout层
        self.dropout = nn.Dropout(config.dropout)
        
        # 全连接分类网络
        # 输入 = num_filters * len(config.filter_sizes) = 768 = 256 * 3
        # config.num_classes:输出分类数,10类
        self.fc = nn.Linear(config.num_filters * len(config.filter_sizes), config.num_classes)

    # 定义池化层
    def conv_and_pool(self, x, conv):
        x = F.relu(conv(x)).squeeze(3)
        x = F.max_pool1d(x, x.size(2)).squeeze(2)
        return x

    def forward(self, x):
        #print (x[0].shape)
        out = self.embedding(x[0])
        out = out.unsqueeze(1)
        out = torch.cat([self.conv_and_pool(out, conv) for conv in self.convs], 1)
        out = self.dropout(out)
        out = self.fc(out)
        return out

4.2 实例化模型并显示模型结构

# 构建模型
#设定随机种子,确保每次随机初始化的结果是一样的
np.random.seed(1)
torch.manual_seed(1)
torch.cuda.manual_seed_all(1)
torch.backends.cudnn.deterministic = True  # 保证每次结果一样

model_name = "TextRNN"

# 创建模型实例
model = Model(config).to(config.device)

#显示网络参数
for name, w in model.named_parameters():
    print(name)
    
print(model.parameters)
embedding.weight

convs.0.weight
convs.0.bias
convs.1.weight
convs.1.bias
convs.2.weight
convs.2.bias

fc.weight
fc.bias

<bound method Module.parameters of Model(
  (embedding): Embedding(4762, 300)
  (convs): ModuleList(
    (0): Conv2d(1, 256, kernel_size=(2, 300), stride=(1, 1))
    (1): Conv2d(1, 256, kernel_size=(3, 300), stride=(1, 1))
    (2): Conv2d(1, 256, kernel_size=(4, 300), stride=(1, 1))
  )
  (dropout): Dropout(p=0.5, inplace=False)
  (fc): Linear(in_features=768, out_features=10, bias=True)
)>
# 权重初始化:不同的初始化方法,导致精确性和收敛时间不同
# 默认xavier
# xavier:“Xavier”初始化方法是一种很有效的神经网络初始化方法
# kaiming:何凯明初始化
# normal_: 正态分布初始化
def init_network(model, method='xavier', exclude='embedding', seed=123):
    for name, w in model.named_parameters():
        if exclude not in name:
            if 'weight' in name:
                if method == 'xavier':
                    nn.init.xavier_normal_(w)
                elif method == 'kaiming':
                    nn.init.kaiming_normal_(w)
                else:
                    nn.init.normal_(w)
            elif 'bias' in name:
                nn.init.constant_(w, 0)
            else:
                pass

#初始化网络
init_network(model)

第5章 模型训练、评估

5.1 模型评估方法

# 模型评估方法
def evaluate(config, model, data_iter, test=False):
    # 设置在评估模式
    model.eval()
    loss_total = 0
    predict_all = np.array([], dtype=int)
    labels_all = np.array([], dtype=int)
    
    # 不进行梯度更新
    with torch.no_grad():
        # 数据集迭代
        for texts, labels in data_iter:
            
            # 模型预测输出
            outputs = model(texts)
            
            # 计算当前的loss
            loss = F.cross_entropy(outputs, labels)
            loss_total += loss
            
            # 计算当前的精度
            labels = labels.data.cpu().numpy()
            predic = torch.max(outputs.data, 1)[1].cpu().numpy()
            
            # 记录当前的label的数目
            labels_all = np.append(labels_all, labels)
            
            # 记录当前正确预测的数目
            predict_all = np.append(predict_all, predic)
    
    # 计算整个数据集上的平均精度
    acc = metrics.accuracy_score(labels_all, predict_all)
    
    if test:
        report = metrics.classification_report(labels_all, predict_all, target_names=config.class_list, digits=4)
        confusion = metrics.confusion_matrix(labels_all, predict_all)
        return acc, loss_total / len(data_iter), report, confusion
    
    # 返回整个数据集上的平均精度与平均loss
    return acc, loss_total / len(data_iter)

5.2 模型训练方法

# 训练方法
writer = SummaryWriter(log_dir=config.log_path + '/' + time.strftime('%m-%d_%H.%M', time.localtime()))

def train(config, model, train_iter, dev_iter, writer):
    start_time = time.time()
    
    # 设定在模式下
    model.train()
    
    #设定优化器
    optimizer = torch.optim.Adam(model.parameters(), lr=config.learning_rate)

    # 学习率指数衰减,每次epoch:学习率 = gamma * 学习率
    # scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.9)
    total_batch = 0  # 记录进行到多少batch,一个训练集包含多个batch
    
    #记录当前最好的loss值
    dev_best_loss = float('inf')
    last_improve = 0  # 记录上次验证集loss下降的batch数
    flag = False  # 记录是否很久没有效果提升
    
    # 启动一个SummaryWriter对象,用于 记录训练过程
    writer = SummaryWriter(log_dir=config.log_path + '/' + time.strftime('%m-%d_%H.%M', time.localtime()))
    
    #开始训练
    for epoch in range(config.num_epochs):
        print('Epoch [{}/{}]'.format(epoch + 1, config.num_epochs))
        
        #自动调整学习率
        #scheduler.step() # 学习率衰减
        
        # 迭代数据集
        for i, (trains, labels) in enumerate(train_iter):
            #print (trains[0].shape)
            
            #获取模型输出
            outputs = model(trains)
            
            #复位模型梯度
            model.zero_grad()
            
            # 计算模型loss
            loss = F.cross_entropy(outputs, labels)
            
            # 根据loss计算梯度
            loss.backward()
            
            # 反向迭代,更新W参数
            optimizer.step()
            
            # 对迭代进行测试与评估
            # 每100次迭代输出,在训练集和验证集上的评估一次效果
            if total_batch % 100 == 0:
                # 获取训练集上的精度
                true = labels.data.cpu()
                predic = torch.max(outputs.data, 1)[1].cpu()
                train_acc = metrics.accuracy_score(true, predic)
                
                # 获取验证集上的精度
                dev_acc, dev_loss = evaluate(config, model, dev_iter, test=False)
                if dev_loss < dev_best_loss:
                    dev_best_loss = dev_loss
                    # 保存当前精度更高时候的模型
                    torch.save(model.state_dict(), config.save_path)
                    improve = '*'
                    
                    # 记录模型更新时的batch数
                    last_improve = total_batch
                else:
                    improve = ''
                
                # 打印log信息
                time_dif = get_time_dif(start_time)
                msg = 'Iter: {0:>6},  Train Loss: {1:>5.2},  Train Acc: {2:>6.2%},  Val Loss: {3:>5.2},  Val Acc: {4:>6.2%},  Time: {5} {6}'
                print(msg.format(total_batch, loss.item(), train_acc, dev_loss, dev_acc, time_dif, improve))
                writer.add_scalar("loss/train", loss.item(), total_batch)
                writer.add_scalar("loss/dev", dev_loss, total_batch)
                writer.add_scalar("acc/train", train_acc, total_batch)
                writer.add_scalar("acc/dev", dev_acc, total_batch)
                
                #重新进入训练模式
                model.train()
            
            # batch数++
            total_batch += 1
            
            # 如果连续迭代后,精度没有得到进一步的提升,当次数得到一定的设定值后,自动停止迭代。
            # total_batch:连续进行了多少次batch
            # last_improve:记录模型更新时的batch数
            # config.require_improvement
            if total_batch - last_improve > config.require_improvement:
                # 验证集loss超过1000batch 没下降,结束训练
                print("No optimization for a long time, auto-stopping...")
                print("total_batch=", total_batch)
                print("last_improve=", last_improve)
                print("require_improvement=", config.require_improvement)
                flag = True
                break
        if flag:
            break
    writer.close()

5.3 边训练、边评估模型

在训练集上训练,在验证集上评估

# 一边训练,一边评估
train(config, model, train_iter, dev_iter, writer)
Epoch [1/20]
Iter:      0,  Train Loss:   2.3,  Train Acc:  4.69%,  Val Loss:   2.6,  Val Acc: 13.59%,  Time: 0:00:00 *
Iter:    100,  Train Loss:   1.1,  Train Acc: 64.06%,  Val Loss:  0.79,  Val Acc: 75.50%,  Time: 0:00:02 *
Iter:    200,  Train Loss:  0.66,  Train Acc: 81.25%,  Val Loss:  0.65,  Val Acc: 79.40%,  Time: 0:00:04 *
Iter:    300,  Train Loss:  0.65,  Train Acc: 84.38%,  Val Loss:  0.56,  Val Acc: 82.18%,  Time: 0:00:06 *
Iter:    400,  Train Loss:  0.52,  Train Acc: 78.12%,  Val Loss:  0.51,  Val Acc: 84.03%,  Time: 0:00:08 *
Iter:    500,  Train Loss:  0.49,  Train Acc: 79.69%,  Val Loss:  0.48,  Val Acc: 85.09%,  Time: 0:00:10 *
.............................................................................
Iter:   7200,  Train Loss:  0.31,  Train Acc: 92.19%,  Val Loss:  0.33,  Val Acc: 90.02%,  Time: 0:02:17 
Iter:   7300,  Train Loss:  0.25,  Train Acc: 93.75%,  Val Loss:  0.34,  Val Acc: 89.66%,  Time: 0:02:19 
Iter:   7400,  Train Loss:  0.18,  Train Acc: 92.19%,  Val Loss:  0.33,  Val Acc: 89.92%,  Time: 0:02:21 
Iter:   7500,  Train Loss:   0.3,  Train Acc: 89.06%,  Val Loss:  0.33,  Val Acc: 90.24%,  Time: 0:02:23 
Iter:   7600,  Train Loss:  0.31,  Train Acc: 93.75%,  Val Loss:  0.33,  Val Acc: 89.81%,  Time: 0:02:25 
No optimization for a long time, auto-stopping...
total_batch= 7601
last_improve= 6600
require_improvement= 1000

第6章 在测试集上对模型进行评估

6.1 测试方法的定义

# 在测试集上对模型进行评估
def test(config, model, test_iter):
    # test
    # 获取保存的最佳精度的模型
    model.load_state_dict(torch.load(config.save_path))
    
    # 进入评估模式
    model.eval()
    
    start_time = time.time()
    
    # 测试测试集进行评估
    test_acc, test_loss, test_report, test_confusion = evaluate(config, model, test_iter, test=True)
    
    # 打印测试集的评估结果
    # 测试集的loss和精度
    msg = 'Test Loss: {0:>5.2},  Test Acc: {1:>6.2%}'
    print(msg.format(test_loss, test_acc))
    
    # 打印准确率、召回率、F1-Score的分数
    print("Precision, Recall and F1-Score...")
    print(test_report)
    
    # 打印混淆矩阵
    print("Confusion Matrix...")
    print(test_confusion)
    
    
    time_dif = get_time_dif(start_time)
    print("Time usage:", time_dif)

备注:

至于模型的评分指标:Loss、accuracy、Precision, Recall and F1-Score,请参看相关文章。

6.2 开始测试

# 对训练好的模型进行测试
test(config, model, test_iter)
Test Loss:   0.3,  Test Acc: 90.50%
Precision, Recall and F1-Score...
               precision    recall  f1-score   support

      finance     0.9180    0.8840    0.9007      1000
       realty     0.9331    0.9200    0.9265      1000
       stocks     0.8651    0.8530    0.8590      1000
    education     0.9556    0.9480    0.9518      1000
      science     0.8402    0.8830    0.8610      1000
      society     0.8695    0.9260    0.8969      1000
     politics     0.9025    0.8610    0.8813      1000
       sports     0.9222    0.9600    0.9407      1000
         game     0.9290    0.9030    0.9158      1000
entertainment     0.9212    0.9120    0.9166      1000

     accuracy                         0.9050     10000
    macro avg     0.9056    0.9050    0.9050     10000
 weighted avg     0.9056    0.9050    0.9050     10000

Confusion Matrix...
[[884  13  51   4  17   9   7  10   1   4]
 [ 13 920  14   1   9  20   7   4   3   9]
 [ 46  19 853   2  35   3  31   5   4   2]
 [  2   2   4 948   7  17   7   5   0   8]
 [  2   3  22   4 883  16  16   6  34  14]
 [  4  16   1  15   8 926  15   3   2  10]
 [  9   5  32   8  20  46 861  12   1   6]
 [  1   2   1   1   3   9   4 960   4  15]
 [  0   0   7   3  55   6   2  14 903  10]
 [  2   6   1   6  14  13   4  22  20 912]]
Time usage: 0:00:00

作者主页(文火冰糖的硅基工坊):文火冰糖(王文兵)的博客_文火冰糖的硅基工坊_CSDN博客

本文网址:https://blog.csdn.net/HiWangWenBing/article/details/121802852

猜你喜欢

转载自blog.csdn.net/HiWangWenBing/article/details/121802852