#Python&Pytorch 2.如何对CTG特征数据建模

本文介绍了如何使用Python和PyTorch对CTG特征数据进行深度学习建模,包括数据读取、数据集划分、自定义数据集、模型创建、训练代码编写以及模型评估。主要改动在于数据预处理,将特征数据转换为适合模型的格式,并调整模型的输入和输出尺寸以匹配数据。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

系列文章目录

#Python&Pytorch 1.如何入门深度学习模型
#Python&Pytorch 2.如何对CTG特征数据建模


  我之前也写过一篇使用GBDT对UCI-CTG特征数据进行建模的博客,不过那是挺早的时候写的,只是简单贴了代码,方便了解流程而已,现在就顺着上一篇博客的代码,来试试怎么样使用torch对CTG特征数据建模吧~

  建模最关键的两个部分,第一就是数据,第二就是模型。只要搞定了这两部分,那建模基本上就是洒洒水的事情。

  UCI-CTG的数据就放在GitHub里面:链接

  (以下数据集描述摘抄至本人的另一篇博客
  在公共CTG数据集的定义中,LB, AC, FM, UC, DL, DS, DP, ASTV, MSTV, ALTV, MLTV, Width, Min, Max,Nmax, Nzeros, Mode, Mean, Median, Variance, Tendency这21个是CTG变量,而NSP为标签(根据胎儿监护指南评价胎儿状态NSP: Normal, Suspicious, Pathological),即一个样本=21个CTG变量值+对应的NSP标签
在这里插入图片描述

  在训练模型时,我们使用特征作为模型的输入,使用标签来验证算法预测的结果是否准确,这就是最简单的分类问题。

  有了上述的描述,我们都大致了解了CTG特征数据的构成,因此我们可以着手构建本次的代码。为了方便大家学习,我会把上一篇的代码和这一篇的代码放在一起,这样大家就能感受到如果使用自己的数据集需要修改什么地方,格式如下:

# 上一篇的代码
# ------------------------------------------------------------------------------------
# 改造后的代码

1.导入所需的库

  第一步,导入所需用到的库

  因为要使用特征数据建模需要读取csv,所以导入了pandas库,然后还需要导入数据集分割函数,当然如果你已经事先分割好数据集了那就不用这个库了。

import os

import numpy as np
import torch
from sklearn.metrics import confusion_matrix, classification_report, f1_score
from torch import nn
from torch.utils.data import DataLoader, Dataset
from torchinfo import summary
from tqdm import tqdm

# ------------------------------------------------------------------------------------
import os

import numpy as np
import pandas as pd  # 增加了pandas库,用于读取csv文件
import torch
from sklearn.metrics import confusion_matrix, classification_report, f1_score
from sklearn.model_selection import train_test_split  # 增加了用于分割数据集的函数
from torch import nn
from torch.utils.data import DataLoader, Dataset
from torchinfo import summary
from tqdm import tqdm

2.读取数据

  之后是读取数据这部分,我们需要读取数据集文件并把数据分割为训练集和测试集。代码如下:

# 读取数据
df = pd.read_csv('CTG.csv')
# 特征列名,因为数据集中除了最后一列是标签之外,其余列都是特征,因此我们直接取除了最后一列的列名作为我们的特征列
feature = df.columns.values[:-1]
print(feature)

# 用sklearn.cross_validation进行训练数据集划分,这里训练集和交叉验证集比例为8:2,可以自己根据需要设置
# NSP为标签,0表示正常,1表示可疑,3表示病理
X_train, X_test, y_train, y_test = train_test_split(
    df[feature],  # X
    df['NSP'],  # y
    test_size=0.2,  # 测试集比例为20%
    random_state=1,  # 设置随机种子以保证结果可以复现
    stratify=df['NSP']  # 这里保证分割后y的比例分布与原数据一致
)

print(f"训练集的数目为 {X_train.shape[0]} 例;共有 {X_train.shape[1]} 个特征")
print(f"测试集的数目为 {X_test.shape[0]} 例;共有 {X_test.shape[1]} 个特征")

# output:
# ['LB' 'AC' 'FM' 'UC' 'DL' 'DS' 'DP' 'ASTV' 'MSTV' 'ALTV' 'MLTV' 'Width'
#  'Min' 'Max' 'Nmax' 'Nzeros' 'Mode' 'Mean' 'Median' 'Variance' 'Tendency']
# 训练集的数目为 1700 例;共有 21 个特征
# 训练集的数目为 426 例;共有 21 个特征

  从上面的代码运行结果可以看到,我们共有1700例训练集,426例测试集,一共有21个特征。

3.自定义数据集

  数据处理完毕后,我们需要改写上篇博客的数据集函数,将它改造成可以处理我们的数据集,代码如下

# 自定义数据集,继承torch.utils.data.Dataset,一般的数据集只需要重写下面的三个方法即可
class MyDataset(Dataset):
    def __init__(self, data_len, ):
        """ 初始化数据集并进行必要的预处理
        初始化数据集一般需要包括数据和标签:数据可以是直接可以使用的特征或路径;标签一般可以存放在csv中,可以选择读取为列表
        """
        # 随机初始化一个shape为(data_len, 10)的矩阵作为输入数据x,数据类型为float
        self.datas = torch.randn(size=(data_len, 10), dtype=torch.float32)
        # 随机初始化一个shape为(data_len, 1)的矩阵作为标签y,数据类型为int
        self.labels = torch.randint(low=0, high=10, size=(data_len, 1))

    def __len__(self):
        """ 返回数据集的大小,方便后续遍历取数据 """
        return len(self.labels)

    def __getitem__(self, item):
        """ item不需要我们手动传入,后续使用dataloader时会自动预取
        这个函数的作用是根据item从数据集中取出对应的数据和标签
        """
        return self.datas[item], self.labels[item][0]

# ------------------------------------------------------------------------------------

# 自定义数据集,继承torch.utils.data.Dataset,一般的数据集只需要重写下面的三个方法即可
class MyDataset(Dataset):
    def __init__(self, feature_data, labels, ):
        """ 初始化数据集并进行必要的预处理
        初始化数据集一般需要包括数据和标签:数据可以是直接可以使用的特征或路径;标签一般可以存放在csv中,可以选择读取为列表
        """
        # 我们可以直接将数据集的特征作为self.datas,因为我们在__getitem__中可以直接按照index从中取值,因此不用进行其他操作
        self.datas = feature_data.values  # .values表示将DataFrame中的矩阵取出,取出之后格式为numpy.array
        # 同理,标签和特征是一一对应的,在__getitem__中取任意一个index都能找到对应的特征和标签
        self.labels = labels
        # 将标签NSP规范化,原始的1、2、3分别表示正常、可疑、病理;需要将替换为0、1、2,以保证各项指标的正常计算
        # 若要进行二分类,则可以将可疑和无反应的标签替换为0,即变成了二分类
        # 这一步操作可以在读取数据集的时候替换,也可以在这里替换,也可以在__getitem__里面替换,inplace=True表示将替换值直接覆盖原结果
        # ctg['NSP'].replace({1: 0, 2: 1, 3: 1}, inplace=True) # 二分类
        self.labels.replace({1: 0, 2: 1, 3: 2}, inplace=True)  # 三分类
        self.labels = self.labels.values

    def __len__(self):
        """ 返回数据集的大小,方便后续遍历取数据 """
        return len(self.labels)

    def __getitem__(self, item):
        """ item不需要我们手动传入,后续使用dataloader时会自动预取
        这个函数的作用是根据item从数据集中取出对应的数据和标签
        """
        # 由于我们这里是读取的pandas数据转成numpy,而数据集的格式要求是tensor,所以我们需要将numpy转成tensor的格式
        data = torch.tensor(self.datas[item], dtype=torch.float32)  # 因为模型的权重和偏置矩阵都是float32的类型,因此也要把特征转换为float32
        label = torch.tensor(self.labels[item])
        return data, label

  可以看到,由于数据集比较简单,所以我们修改的地方其实不多,但也有以下几点需要注意:

  1. 特征数据一般使用pandas读取,读取后的格式为DataFrame,而DataFrame格式在 _getitem_ 函数中不能通过 self.data[item] 的方式获取索引为item的特征,因此我们需要将DataFrame的格式转换为numpy.array或使用 self.data.iloc[item] 的方式获取。如果这点没有修改的话代码会报KeyError的错误;
  2. 分类数据的标签一般要从0开始,以保证部分指标的正常计算。这一步可以在读取数据集之后处理,也可以放在数据集函数里面处理;
  3. 在 _getitem_ 函数中,如果我们的在 _init_ 函数中我们的定义的数据是array的格式,那么在 _getitem_ 函数中我们就需要把我们取出来的值转换为tensor的格式。注意!有时候单纯转换格式可能还会报错,这里就是因为我们的数据集的数据类型 (float64) 跟模型的数据类型 (float32) 不一致,会报 RuntimeError 的错误,因此需要将数据类型转换为一致。如果不知道数据和模型的数据类型是什么格式的,可以通过以下代码输出:
# 在数据集函数的 __getitem__ 函数中
print(self.datas[item].dtype)

# 在模型的 forward 函数中
print(self.fc1.weight.dtype)
print(self.fc1.bias.dtype)

4.配置模型所需的基本参数

  这样,我们的数据集函数也搞定了,在这里就可以来创建我们的数据集了

# 首先要定义我们需要训练多少轮,batch_size要定义为多少
EPOCHS = 2
BATCH_SIZE = 2
# 其次,torch的训练不会自动把数据和模型加载进GPU中,所以需要我们定义一个训练设备,例如device
device = torch.device('cpu')  # 前期学习就只使用CPU训练
# !!重点,如果是使用GPU进行训练,那么我们需要把模型也加载进GPU中,不然就无法使用GPU训练
model = model.to(device)  # 把模型加载至训练设备中

# 定义一个含有4000条数据的训练集和1000条数据的验证集
train_dataset = MyDataset(4000)
valid_dataset = MyDataset(1000)
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
valid_loader = DataLoader(valid_dataset, batch_size=BATCH_SIZE, shuffle=False)  # 需要注意的是,验证集的dataloader不需要打乱数据

# ------------------------------------------------------------------------------------

# 首先要定义我们需要训练多少轮,batch_size要定义为多少
EPOCHS = 200
BATCH_SIZE = 4
# 其次,torch的训练不会自动把数据和模型加载进GPU中,所以需要我们定义一个训练设备,例如device
device = torch.device('cpu')  # 前期学习就只使用CPU训练
# !!重点,如果是使用GPU进行训练,那么我们需要把模型也加载进GPU中,不然就无法使用GPU训练
model = model.to(device)  # 把模型加载至训练设备中

train_dataset = MyDataset(X_train, y_train)
test_dataset = MyDataset(X_test, y_test)
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
test_loader = DataLoader(test_dataset , batch_size=BATCH_SIZE, shuffle=False)  # 需要注意的是,验证集和测试集的dataloader是不需要打乱数据

  定义数据集的部分改动只在创建数据集部分。另外把模型的训练轮次增加到200,批次大小增加到4,这两个改动都是无关紧要的。

5.创建深度学习模型

  接下来是模型部分。

# 接着我们继续来定义一个简单的多层感知机模型
class MyModel(nn.Module):
    """ 这个模型含有三个线性层,两个激活函数层和一个随机失活层 """
    def __init__(self, in_channel, output_channel, drop_rate=0.0):
        super().__init__()
        # 定义第一个线性层,需要定义线性层的输入维度和输出维度,这里就把输出维度设置为输入维度的四倍
        self.fc1 = nn.Linear(in_features=in_channel, out_features=in_channel * 4)  # 线性层等价于Keras的Dense层,即全连接层
        # 定义第二个线性层,将输入维度和输出维度设为in_channel的4倍
        self.fc2 = nn.Linear(in_features=in_channel * 4, out_features=in_channel * 4)
        # 定义最后一层的线性层(也可以称为分类层),用于分类,将数据维度压缩至output_channel的维度用于输出
        self.head = nn.Linear(in_channel * 4, output_channel)

        # 定义一个dropout层用于随机失活神经元
        self.drop1 = nn.Dropout(drop_rate)

        # 定义第一个线性层后的激活函数,处理dropout后的结果
        self.act1 = nn.LeakyReLU()
        # 定义第二个线性层后的激活函数,处理dropout后的结果
        self.act2 = nn.LeakyReLU()
        # 定义softmax函数用于将最终的输出结果映射到(0, 1)之间,dim=1表示softmax在维度为1的数据上进行处理
        self.softmax = nn.Softmax(dim=1)

    def forward(self, x):
        """ 这个函数的作用是定义模型前向计算,指定了如何根据输入数据 x 计算出模型的输出结果,是定义模型结构的关键
        在__init__函数中,我们只定义了我们所需要的网络层,并没有定义数据是先经过哪个网络层再经过哪个网络层。
        模型前向计算也可以看作数据 x 的流动方向,先从哪里流向哪里
        """
        # 输入时,x.shape=(batch_size, 10)
        x = self.fc1(x)  # 先将 x 输入至第一个线性层中,输出维度扩大为输入维度的4倍,此时 x.shape=(batch_size, 40)
        x = self.drop1(x)  # dropout并不会改变 x 的形状,所以 x.shape=(batch_size, 40)
        x = self.act1(x)  # 同样LeakyReLU也不会改变 x 的形状,所以 x.shape=(batch_size, 40)
        x = self.fc2(x)  # 第二个线性层的输入维度和输出维度一致,所以 x 的形状保持不变 x.shape=(batch_size, 40)
        x = self.act2(x)  # 同理 x.shape=(batch_size, 40)
        output = self.head(x)  # x 输入至最后一层分类层,数据的输出维度为output_channel,因此 output.shape=(batch_size, 10)
        output = self.softmax(output)  # softmax同样不改变数据的形状,因此 output.shape=(batch_size, 10)
        return output  # 最后将模型的计算结果返回

  模型方面没有对比的原因是模型根本就不用改!我们只需要在创建模型的时候把输入的维度改为输入特征数 (21)把最终输出维度改为数据的类别数 (3) 就可以匹配我们的数据格式。这样我们的模型就是一个以21个特征为输入,3种标签为结果的模型。

  我们使用torchinfo的summary函数输出一下模型结构~

model = MyModel(21, 3, drop_rate=0.0)
# summary的时候需要把input_size对应修改为输入的特征数
summary(model, input_size=[(4, 21)], dtypes=[torch.float32], col_names=["input_size", "output_size", "num_params"])

# output:
# ===================================================================================================================
# Layer (type:depth-idx)                   Input Shape               Output Shape              Param #
# ===================================================================================================================
# MyModel                                  [4, 21]                   [4, 3]                    --
# ├─Linear: 1-1                            [4, 21]                   [4, 84]                   1,848
# ├─Dropout: 1-2                           [4, 84]                   [4, 84]                   --
# ├─LeakyReLU: 1-3                         [4, 84]                   [4, 84]                   --
# ├─Linear: 1-4                            [4, 84]                   [4, 84]                   7,140
# ├─LeakyReLU: 1-5                         [4, 84]                   [4, 84]                   --
# ├─Linear: 1-6                            [4, 84]                   [4, 3]                    255
# ├─Softmax: 1-7                           [4, 3]                    [4, 3]                    --
# ===================================================================================================================
# Total params: 9,243
# Trainable params: 9,243
# Non-trainable params: 0
# Total mult-adds (M): 0.04
# ===================================================================================================================
# Input size (MB): 0.00
# Forward/backward pass size (MB): 0.01
# Params size (MB): 0.04
# Estimated Total Size (MB): 0.04
# ===================================================================================================================

  简单吧?

6.编写训练代码

  最后,我们的代码还差最后一个地方,改完就能训练我们的模型了,就是训练代码和测试代码的指标运算部分

print(confusion_matrix(targets, predictions, labels=range(0, 10)))
print(classification_report(targets, predictions, labels=range(0, 10)))
f1 = f1_score(targets, predictions, average='macro', labels=range(0, 10))

# ------------------------------------------------------------------------------------

print(confusion_matrix(targets, predictions, labels=range(0, 3)))
print(classification_report(targets, predictions, labels=range(0, 3)))
f1 = f1_score(targets, predictions, average='macro', labels=range(0, 3))

  就是这样,只需要把类别改成CTG数据的标签就可以了。

7.代码总结

  所以总体的代码就搞定啦~其他地方都不用改,直接就能运行。代码总结如下:

# -*- coding: utf-8 -*-
"""
Created on 2023/4/24 11:11
Location CellsVision

@author: Troye Jcan
"""
import os

import numpy as np
import pandas as pd
import torch
from sklearn.metrics import confusion_matrix, classification_report, f1_score
from sklearn.model_selection import train_test_split
from torch import nn
from torch.utils.data import DataLoader, Dataset
from torchinfo import summary
from tqdm import tqdm

# 读取数据
df = pd.read_csv('CTG.csv')
# 特征列名,因为数据集中除了最后一列是标签之外,其余列都是特征,因此我们直接取除了最后一列的列名作为我们的特征列
feature = df.columns.values[:-1]
print(feature)

# 用sklearn.cross_validation进行训练数据集划分,这里训练集和交叉验证集比例为8:2,可以自己根据需要设置
# NSP为标签,0表示正常,1表示可疑,3表示病理
X_train, X_test, y_train, y_test = train_test_split(
    df[feature],  # X
    df['NSP'],  # y
    test_size=0.2,  # 测试集比例为20%
    random_state=1,  # 设置随机种子以保证结果可以复现
    stratify=df['NSP']  # 这里保证分割后y的比例分布与原数据一致
)

print(f"训练集的数目为 {X_train.shape[0]} 例;共有 {X_train.shape[1]} 个特征")
print(f"训练集的数目为 {X_test.shape[0]} 例;共有 {X_test.shape[1]} 个特征")

# output:
# ['LB' 'AC' 'FM' 'UC' 'DL' 'DS' 'DP' 'ASTV' 'MSTV' 'ALTV' 'MLTV' 'Width'
#  'Min' 'Max' 'Nmax' 'Nzeros' 'Mode' 'Mean' 'Median' 'Variance' 'Tendency']
# 训练集的数目为 1700 例;共有 21 个特征
# 训练集的数目为 426 例;共有 21 个特征


class MyDataset(Dataset):
    def __init__(self, feature_data, labels, ):
        """ 初始化数据集并进行必要的预处理
        初始化数据集一般需要包括数据和标签:数据可以是直接可以使用的特征或路径;标签一般可以存放在csv中,可以选择读取为列表
        """
        # 我们可以直接将数据集的特征作为self.datas,因为我们在__getitem__中可以直接按照index从中取值,因此不用进行其他操作
        self.datas = feature_data.values  # .values表示将DataFrame中的矩阵取出,取出之后格式为numpy.array
        # 同理,标签和特征是一一对应的,在__getitem__中取任意一个index都能找到对应的特征和标签
        self.labels = labels
        # 将标签NSP规范化,原始的1、2、3分别表示正常、可疑、病理;需要将替换为0、1、2,以保证各项指标的正常计算
        # 若要进行二分类,则可以将可疑和无反应的标签替换为0,即变成了二分类
        # 这一步操作可以在读取数据集的时候替换,也可以在这里替换,也可以在__getitem__里面替换,inplace=True表示将替换值直接覆盖原结果
        # ctg['NSP'].replace({1: 0, 2: 1, 3: 1}, inplace=True) # 二分类
        self.labels.replace({1: 0, 2: 1, 3: 2}, inplace=True)  # 三分类
        self.labels = self.labels.values

    def __len__(self):
        """ 返回数据集的大小,方便后续遍历取数据 """
        return len(self.labels)

    def __getitem__(self, item):
        """ item不需要我们手动传入,后续使用dataloader时会自动预取
        这个函数的作用是根据item从数据集中取出对应的数据和标签
        """
        # 由于我们这里是读取的pandas数据转成numpy,而数据集的格式要求是tensor,所以我们需要将numpy转成tensor的格式
        data = torch.tensor(self.datas[item], dtype=torch.float32)  # 因为模型的权重和偏置矩阵都是float32的类型,因此也要把特征转换为float32
        label = torch.tensor(self.labels[item])

        return data, label


# 接着我们继续来定义一个简单的多层感知机模型
class MyModel(nn.Module):
    """ 这个模型含有三个线性层,两个激活函数层和一个随机失活层 """
    def __init__(self, in_channel, output_channel, drop_rate=0.0):
        super().__init__()
        # 定义第一个线性层,需要定义线性层的输入维度和输出维度,这里就把输出维度设置为输入维度的四倍
        self.fc1 = nn.Linear(in_features=in_channel, out_features=in_channel * 4)  # 线性层等价于Keras的Dense层,即全连接层
        # 定义第二个线性层,将输入维度和输出维度设为in_channel的4倍
        self.fc2 = nn.Linear(in_features=in_channel * 4, out_features=in_channel * 4)
        # 定义最后一层的线性层(也可以称为分类层),用于分类,将数据维度压缩至output_channel的维度用于输出
        self.head = nn.Linear(in_channel * 4, output_channel)

        # 定义一个dropout层用于随机失活神经元
        self.drop1 = nn.Dropout(drop_rate)

        # 定义第一个线性层后的激活函数,处理dropout后的结果
        self.act1 = nn.LeakyReLU()
        # 定义第二个线性层后的激活函数,处理dropout后的结果
        self.act2 = nn.LeakyReLU()
        # 定义softmax函数用于将最终的输出结果映射到(0, 1)之间,dim=1表示softmax在维度为1的数据上进行处理
        self.softmax = nn.Softmax(dim=1)

    def forward(self, x):
        """ 这个函数的作用是定义模型前向计算,指定了如何根据输入数据 x 计算出模型的输出结果,是定义模型结构的关键
        在__init__函数中,我们只定义了我们所需要的网络层,并没有定义数据是先经过哪个网络层再经过哪个网络层。
        模型前向计算也可以看作数据 x 的流动方向,先从哪里流向哪里
        """
        # 输入时,x.shape=(batch_size, 10)
        x = self.fc1(x)  # 先将 x 输入至第一个线性层中,输出维度扩大为输入维度的4倍,此时 x.shape=(batch_size, 40)
        x = self.drop1(x)  # dropout并不会改变 x 的形状,所以 x.shape=(batch_size, 40)
        x = self.act1(x)  # 同样LeakyReLU也不会改变 x 的形状,所以 x.shape=(batch_size, 40)
        x = self.fc2(x)  # 第二个线性层的输入维度和输出维度一致,所以 x 的形状保持不变 x.shape=(batch_size, 40)
        x = self.act2(x)  # 同理 x.shape=(batch_size, 40)
        output = self.head(x)  # x 输入至最后一层分类层,数据的输出维度为output_channel,因此 output.shape=(batch_size, 10)
        output = self.softmax(output)  # softmax同样不改变数据的形状,因此 output.shape=(batch_size, 10)
        return output  # 最后将模型的计算结果返回


model = MyModel(21, 3, drop_rate=0.0)
# torchinfo的summary方法可以使得模型的结构按照类似表格的方式输出,非常方便我们查看每个层的输入输出维度,强烈推荐!
summary(model, input_size=[(2, 21)], dtypes=[torch.float32], col_names=["input_size", "output_size", "num_params"])


# 交叉熵是最常用的分类损失函数
loss_fn = nn.CrossEntropyLoss()
# 优化器可以使用Adam,需要输入模型的参数和学习率
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)

# 首先要定义我们需要训练多少轮,batch_size要定义为多少
EPOCHS = 100
BATCH_SIZE = 4
# 其次,torch的训练不会自动把数据和模型加载进GPU中,所以需要我们定义一个训练设备,例如device
device = torch.device('cpu')  # 前期学习就只使用CPU训练
# !!重点,如果是使用GPU进行训练,那么我们需要把模型也加载进GPU中,不然就无法使用GPU训练
model = model.to(device)  # 把模型加载至训练设备中

# 定义一个含有4000条数据的训练集和1000条数据的验证集
train_dataset = MyDataset(X_train, y_train)
valid_dataset = MyDataset(X_test, y_test)
train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)
valid_loader = DataLoader(valid_dataset, batch_size=BATCH_SIZE, shuffle=False)  # 需要注意的是,验证集的dataloader不需要打乱数据


def simple_train(dataloader, model, loss_fn, optimizer):
    model.train()  # 将模型调整为训练模式
    average_loss = 0.  # 定义平均损失为0.
    train_bar = tqdm(enumerate(dataloader), total=len(dataloader), desc='Train')
    for batch, (X, y) in train_bar:
        X, y = X.to(device), y.to(device)  # 将数据和标签都加载至训练设备中
        pred = model(X)  # 将 X 输入至模型中计算结果
        loss = loss_fn(pred, y)  # 用损失函数对模型的预测结果和标签进行计算
        average_loss += loss.item()  # 把损失加到平均损失中,可以用在进度条上实时显示该batch的损失

        optimizer.zero_grad()  # 首先需要将优化器的梯度初始化为0,如果没有初始化,之前每个batch计算的梯度就会累积起来
        loss.backward()  # 之后损失函数计算输出的梯度(误差),同时将梯度从输出层向输入层反向传播,并通过链式法则计算每个神经元的梯度
        optimizer.step()  # 最后根据梯度下降法计算损失函数关于每个参数的梯度,并更新模型的参数

        # set_postfix可以给进度条的后面加上后缀,参数名称写什么进度条的后面就会显示什么
        train_bar.set_postfix(loss=average_loss / (batch + 1))
        train_bar.update()  # 立即更新进度条,方便看到训练进度


def simple_valid(dataloader, model, best_score):
    model.eval()  # 将模型调整为验证模式,加快推理速度
    pred_list, target_list = [], []
    with torch.no_grad():  # 在验证时使用,使得模型在推理过程中不计算梯度,大大加快推理速度
        for X, y in dataloader:  # 因为在验证过程中并不需要观察验证中间过程的损失值等,所以不需要使用枚举,直接循环
            X, y = X.to(device), y.to(device)  # 同样也需要把数据和标签都加载至训练设备中
            pred = model(X)  # 将 X 输入至模型中进行推理,计算预测结果

            # 将每个样本的预测结果概率中最大值的索引当成预测结果取出
            # 如果是用GPU训练的话还要加上.cpu()表示把数据传输到cpu上,因为在GPU上没法处理;之后再用.numpy()表示只取预测结果的数值
            pred_list.append(pred.argmax(1).cpu().numpy())  # 预测结果列表
            target_list.append(y.cpu().numpy())  # 标签列表
        # 预测结果列表和标签列表都是按batch拼接的,因此我们需要使用numpy转换成按样本拼接,这样才能计算混淆矩阵和分类报告
        predictions = np.concatenate(pred_list, axis=0)
        targets = np.concatenate(target_list, axis=0)
        # 接下来就可以把标签和预测结果输入函数中啦,记得不要搞反了~ 还有labels建议也要输入,否则混淆矩阵和分类报告的行列都会随机,不方便统计结果
        print(confusion_matrix(targets, predictions, labels=range(0, 3)))
        print(classification_report(targets, predictions, labels=range(0, 3)))
        f1 = f1_score(targets, predictions, average='macro', labels=range(0, 3))

        if f1 > best_score:
            print(f'------ The best result improved from {best_score} to {f1} -----')
            best_score = f1
            torch.save(model.state_dict(), 'best.pth')
        else:
            if os.path.exists(f'best.pth'):
                model.load_state_dict(torch.load('best.pth'))
                print(f'model restore the best.pth')
            print(f'best m_score till now: {best_score}')
    return best_score


best_score = 0.
for epoch in range(EPOCHS):
    print(f"Epoch {epoch+1} / {EPOCHS}\n-------------------------------")
    simple_train(train_loader, model, loss_fn, optimizer)
    best_score = simple_valid(valid_loader, model, best_score)

  最终模型训练了100轮,最佳f1的结果在第88epoch,f1=0.7515

# output:
# Epoch 88 / 100
# -------------------------------
# Train: 100%|██████████| 425/425 [00:00<00:00, 692.15it/s, loss=0.69]
# [[302  22   8]
#  [ 27  31   1]
#  [  0   5  30]]
#               precision    recall  f1-score   support
# 
#            0       0.92      0.91      0.91       332
#            1       0.53      0.53      0.53        59
#            2       0.77      0.86      0.81        35
# 
#     accuracy                           0.85       426
#    macro avg       0.74      0.76      0.75       426
# weighted avg       0.85      0.85      0.85       426
# 
# ------ The best result improved from 0.7473452473452472 to 0.7514974534641704 -----

  从上面的流程大家也可以看到,其实写代码训练模型的过程是不难的。首先只要你有了一个框架,那你只需解决两个问题:

  1. 数据集读取问题:像这个例子中我们将csv格式的数据作为数据集划分为训练集和测试集,然后构造一个可以按索引从数据集中读取样本的数据集函数,之后就可以用dataloader把它包装起来使用。在这一步一定要检查数据的格式和类型跟模型匹不匹配
  2. 模型输入输出问题:我们需要理清楚模型的输入的shape和输出的shape。网络层会对输入产生什么影响是可以直接在forward函数里面print出来的,例如我们可以直接通过print(self.fc1(x).shape)来查看 x 在经过 fc1 层之后形状发生了什么变化,以此来判断输入下一个网络层之前需要进行什么操作。

  理清楚上面两点后,剩下的就是一些小修小补的事情了,类似于计算指标时 labels 要及时更改呀、epoch和batch_size设置为多少啊这种。建议动手试试!



!如果copy了上面的总结代码但是运行不成功的朋友,一定要跟我说!我会及时修改,谢谢~



  下一篇就写怎么使用torch对CTU-CHB信号数据建模吧~


  有疑惑的朋友一定欢迎在评论区留言~都会回复的


End 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值