模型评估与调优(PyTorch)

发布于:2025-05-19 ⋅ 阅读:(21) ⋅ 点赞:(0)

模型评估方法

混淆矩阵

  • 混淆矩阵是机器学习中统计分类模型预测结果的表,它以矩阵形式将数据集中的记录按照真实的类别与分类模型预测的类别进行汇总,其中矩阵的行表示真实值,矩阵的列表示模型的预测值。
  • 在机器学习中,正样本就是使模型得出正确结论的例子,负样本是使得模型得出错误结论的例子。
  • 建立一个二分类的混淆矩阵,假如宠物店有10只动物,其中6只狗,4只猫,现在有一个分类器将这10只动物进行分类,分类结果为5只狗,5只猫,分类结果的混淆矩阵为:
混淆矩阵 预测值:正(狗) 预测值:负(猫)
真实值:正(狗) 5 1
真实值:负(猫) 0 4

混淆矩阵中的指标

  • TP(TruePositive):被判定为正样本,事实上也是正样本。真的正样本也叫真阳性。
  • FN(FalseNegative):被判定为负样本,但事实上是正样本。假的负样本也叫假阴性。
  • FP(FalsePositive):被判定为正样本,但事实上是负样本。假的正样本也叫假阳性。
  • TN(TrueNegative):被判定为负样本,事实上也是负样本。真的负样本也叫真阴性。
混淆矩阵 预测值:正(狗) 预测值:负(猫)
真实值:正(狗) TP FN
真实值:负(猫) FP TN

ROC曲线(受试者工作特征)

  • ROC曲线的全称是“受试者工作特征”,常用来衡量分类学习器的好坏。如果一个学习器的ROC曲线能将另一个学习器的ROC曲线完全包括,则说明该学习器的性能优于另一个学习器。ROC曲线有个很好的特性:当测试集中的正负样本的分布变化的时候,ROC曲线能够保持不变。
  • ROC曲线的横轴表示FPR,即错误地预测为正例的概率,纵轴表示TPR,即正确地预测为正例的概率,二者的计算公式如下:
    F P R = F P F P + T N T P R = T P T P + F N FPR=\frac{FP}{FP+TN} \qquad TPR=\frac{TP}{TP+FN} FPR=FP+TNFPTPR=TP+FNTP

AUC

  • AUC是一个数值,它是ROC曲线与坐标轴围成的面积。TPR越大、FPR越小,模型效果越好,ROC曲线就越靠近左上角,表明模型效果越好,AUC值越大,极端情况下为1。由于ROC曲线一般都处于 y = x y=x y=x直线的上方,因此 A U C AUC AUC的取值范围一般在 0.5 0.5 0.5 1 1 1之间。
  • 在部分场景下ROC曲线不能直观说明分类器的效果,使用AUC值作为评价标准可以只管说明分类器的效果。
  • AUC值越大,当前的分类算法越有可能将正样本排在负样本前面,即能够更好地分类,可以从AUC判断分类器(预测模型)优劣的标准。
    • UC=1,是完美分类器,采用这个预测模型时,存在至少一个阈值能得出完美预测。绝大多数预测的场合不存在完美分类
    • 0.5<AUC<1,优于随机猜测。这个分类器(模型)妥善设定阈值的话,有预测价值。
    • AUC=0.5,跟随机猜测一样,模型没有预测价值。
    • AUC<0.5,比随机猜测还差

R平方

  • 判定系数R平方(决定系数)是指在线性回归中,回归可解释离平方和与总离差平方和的比值,其数值等于相关系数R的平方。
  • 判定系数是一个解释性系数,在回归分析中,其主要作用是评估回归模型对因变量y产生变化的解释程度(判定系数R平方是评估模型好坏的指标)。
  • R平方的取值范围为 0   1 0~1 0 1,通常以百分数表示,如回归模型的R平方等于 0.7 0.7 0.7,那么表示此回归模型对预测结果的可解释程度为70%。一般认为,R平方大于0.75,表示模型拟合度很好,可解释程度较高;R平方小于0.5,表示模型拟合有问题,不宜进行回归分析。

  • 问题: 在多元回归中,R²(判定系数)有一个明显缺点:只要增加自变量(哪怕是不相关的变量),R²就会变大。这会导致模型看起来拟合得很好,但实际上可能加入了无用的变量,反而降低了模型的可靠性。

  • 解决方法:为了修正这个问题,我们使用调整后的R²。它会根据样本量(n)和自变量个数(k)自动惩罚无意义的变量,避免R²被高估。计算公式如下:
    调整后的 R 2 = 1 − ( 1 − R 2 ) × n − 1 n − k − 1 \text{调整后的} R^2 = 1 - (1 - R^2) \times \frac{n-1}{n-k-1} 调整后的R2=1(1R2)×nk1n1

关键点:

  1. R²的缺陷:单纯增加变量就会提高R²,即使变量没用。
  2. 调整原理:公式中的 n − 1 n − k − 1 \frac{n-1}{n-k-1} nk1n1 会惩罚变量过多的模型(k越大,分母越小,整体值越大,从而降低R²)。
  3. 效果:只有真正有用的变量才会提升调整后的R²,垃圾变量会被过滤掉。

  • 在回归分析(尤其是多元回归)中,调整后的R²比普通R²更准确,因为它会惩罚无用的自变量,避免模型“虚高”的拟合效果。因此,我们通常用调整后的R²来评估模型的真实拟合度。

  • 调整后的R²是否合格?临界值:0.5

    • 如果调整后的R² ≥ 0.5,说明模型拟合效果尚可。
    • 如果调整后的R² < 0.5,说明模型解释力较弱,需要检查: 已使用的自变量是否真的影响因变量?是否遗漏了重要的自变量

残差

  • 残差在数理统计中指实际观测值估计值之间的差,它蕴涵了有关模型基本假设的重要信息。如果回归模型正确的话,可以将残差看作误差的观测值。
  • 回归算法的残差评价指标有均方误差(Mean Squared Error,MSE)、均方根误差(Root Mean Square Error,RMSE),平均绝对误差(Mean Absolute Error,MAE)。

均方误差(MSE)

  • 均方误差(MSE)表示预测值和观测值之间差异(残差平方)的平均值。(线性回归的损失函数,线性回归的目的就是让这个损失函数的数值最小。)
    M S E = 1 m ∑ i = 1 m ( y ^ i − y i ) 2 MSE=\frac{1}{m}\sum_{i=1}^{m}(\hat y_i-y_i)^2 MSE=m1i=1m(y^iyi)2

均方根误差(RMSE)

  • 均方根误差(RMSE)表示预测值和观测值之间差异(残差)的样本标准差。
    R M S E = M S E = 1 m ∑ i = 1 m ( y ^ i − y i ) 2 RMSE=\sqrt{MSE}=\sqrt{\frac{1}{m}\sum_{i=1}^{m}(\hat y_i-y_i)^2} RMSE=MSE =m1i=1m(y^iyi)2

平均绝对误差(MAE)

  • 平均绝对误差(MAE)表示预测值和观测值之间绝对误差的平均值。MAE是一种线性分数,所有个体差异在平均值上的权重都相等,而RMSE相比MAE,会对高的差异惩罚更多。
    M A E = 1 m ∑ i = 1 m ∣ y ^ i − y i ∣ MAE=\frac{1}{m}\sum_{i=1}^{m}|\hat y_i - y_i| MAE=m1i=1my^iyi

模型调优方法

交叉验证(CV)

  • 交叉验证(Cross Validation,CV)或者循估计,是一种统计学上将数据样本切割成较小子集的实用方法,主要用于数据建模。
  • 交叉验证的基本思想:将原始数据进行分组,一部分作为训练集,另外一部分作为验证集。首先用训练集对分类器进行训练,再利用验证集来测试训练得到的模型,以此评价分类器的性能指标,使用交叉验证的目的得到可到稳定的模型。

交叉验证方法

Holdout验证

  • Holdout验证将原始数据随机分为两组,一组作为训练集,另一组作为验证集,利用训练集训练分类器,然后利用验证集验证模型,记录最后的分类准确率,以此作为分类器的性能指标。

K折交叉验证

  • K折交叉验证将初始采样分割成K个子样本,一个单独的子样本被保留作为验证模型的数据,其他K-1个样本用来训练。交叉验证重复K次,每个子样本验证一次,平均K次的结果,最终得到一个单一估测。优势在于,同时重复运用随机产生的子样本进行训练和验证,每次的结果验证一次。

留一验证

  • 留一验证指只使用原本样本中的一项当作验证数据,而剩余的则留下当作训练数据。这个步骤一直持续到每个样本都被当作一次验证数据。事实上,这等同于K折交叉验证,其中K为原样本个数。

十折交叉验证

  • 十折交叉验证用来测试算法的准确性。将数据集分成10份,轮流将其中9份作为训练数据,1份作为测试数据。每次试验都会得出相应的正确率。10次结果的正确率的平均值作为算法精度的估计,一般还需要进行多次10折交叉验证(例如10次10折交叉验证),再求其均值,作为算法的最终准确性估计。

网格搜索交叉验证

  • 网格搜索交叉验证(GridSearchC):部分:网格搜索(GridSearch)和交叉验证(CV)。网格搜索搜索的是参数,即在指定的参数范围内,按步长依次调整参数,利用调整的参数训练模型,从所有的参数中找到在验证集上精度最高的,这其实是一个训练和比较的过程。
  • 网格搜索可以保证在指定的参数范围内找到精度最高的参数,它要求遍历所有可能的参数的组合,在面对大数据集和多参数的情况下,非常耗时。所以网格搜索适用于三四个(或者更少)超参数,用户列出一个较小的超参数值域,这些超参数值域的笛卡儿积为一组超参数。

随机搜索

  • 在搜索超参数时,如果超参数个数较少可以采用网格搜索。但当超参数个数比较多时,如果仍然采用网格搜索,搜索时间将呈指数上升。随机搜索的方法,随机在超参数空间中搜索几十甚至几百个点,其中就有可能有比较小的值。
  • 随机搜索不是尝试所有可能的组合,而是通过选择每一个超参数的一个随机值的特定数量的随机组合,方便通过设定搜索次数控制超参数搜索的计算量等。对于有连续变量的参数,随机搜索会将其当成一个分布进行采样。

PyTorch实现交叉验证

源代码地址

代码内容

import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
import torch.nn.functional as f

# 构造训练集
train_x = torch.rand(100, 28, 28)
train_y = torch.randn(100, 28, 28)
train_x = torch.cat((train_x, train_y), dim=0)
#  构造标签 前100个元素为1 后100个元素为0
labels = [1] * 100 + [0] * 100
# 将标签列转为张量
labels = torch.tensor(labels, dtype=torch.long)

# 设置网络结构
"""
__init__方法中定义三个全连接层:fc1、fc2和fc3。
forward方法实现前向传播过程,对输入数据进行展平,通过全连接层和激活函数进行处理。
num_flat_features方法用于计算输入数据的展平特征数量。
"""
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(28 * 28, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 2)

    def forward(self, x):
        # 将输入张量x展平为一维张量,并计算展平后的特征数量
        x = x.view(-1, self.num_flat_features(x))
        x = f.relu(self.fc1(x))
        x = f.relu(self.fc2(x))
        x = self.fc3(x)
        return x

    # 计算输入x的展平特征数量的函数
    def num_flat_features(self, x):
        size = x.size()[1:]  # 获取输入x除了第一个维度外的尺寸
        num_features = 1  # 初始化特征数量为1
        for s in size:  # 遍历尺寸列表
            num_features *= s  # 计算特征数量
        return num_features  # 返回特征数量


# 训练集数据处理
class train_data_set(TensorDataset):
    def __init__(self, train_features, train_labels):
        self.x_data = train_features  # 保存训练特征
        self.y_data = train_labels  # 保存训练标签
        self.len = len(train_labels)  # 保存训练样本数量

    # 获取训练数据
    def __getitem__(self, index):
        return self.x_data[index], self.y_data[index]

    # 获取训练数据数量
    def __len__(self):
        return self.len


# 设置损失函数
loss_func = nn.CrossEntropyLoss()

"""
get_k_fold_data函数用于将数据集分为K折,并返回当前折的训练集数据、训练集标签、验证集数据和验证集标签。
"""


# 设置k划分
def get_k_fold_data(k, i, x, y):
    """
    获取k折交叉验证数据
    :param k: 折数
    :param i: 当前折的索引
    :param x: 输入的数据
    :param y: 标签
    :return:
        x_train(torch.Tensor):训练集数据
        y_train(torch.Tensor):训练集标签
        x_valid(torch.Tensor):验证集数据
        y_valid(torch.Tensor):验证集标签
    """
    assert k > 1
    fold_size = x.shape[0] // k  # 每折的大小
    x_train, y_train = None, None
    for j in range(k):
        idx = slice(j * fold_size, (j + 1) * fold_size)
        x_part, y_part = x[idx, :], y[idx]
        if j == i:
            x_valid, y_valid = x_part, y_part
        elif x_train is None:
            x_train, y_train = x_part, y_part
        else:
            x_train = torch.cat((x_train, x_part), dim=0)
            y_train = torch.cat((y_train, y_part), dim=0)
    return x_train, y_train, x_valid, y_valid


"""
k_fold函数则在K折交叉验证中循环训练模型,并计算和累积每折的训练集和验证集的损失与准确度。
"""
def k_fold(k, x_train, y_train, num_epochs=3, learning_rate=0.001, weight_decay=0.1, batch_size=5):
    """
    进行k折验证
    :param k: 折数
    :param x_train: 训练数据
    :param y_train: 训练标签
    :param num_epochs: 训练轮数 默认值为3
    :param learning_rate:  学习率 默认值为0.001
    :param weight_decay:  权重衰减 默认值为0.1
    :param batch_size: 批次大小 默认为5
    :return:
        train_loss_sum(float):训练集损失总和
        valid_loss_sum(float):验证集损失总和
        train_acc_sum(float):训练集准确度总和
        valid_acc_sum(float):验证集准确度总和
    """
    train_loss_sum, valid_loss_sum = 0.0, 0.0
    train_acc_sum, valid_acc_sum = 0.0, 0.0

    for i in range(k):
        data = get_k_fold_data(k, i, x_train, y_train)
        net = Net()  # 创建网络实例
        train_ls, valid_ls = train(net, *data, num_epochs, learning_rate, weight_decay, batch_size)
        print('*' * 10, '第', i + 1, '折', '*' * 10)
        print('训练集损失:%.6f' % train_ls[-1][0], '训练集准确度:%.4f' % valid_ls[-1][1],
              '测试集损失:%.6f' % valid_ls[-1][0], '测试集准确度:%.4f' % valid_ls[-1][1])
        train_loss_sum += train_ls[-1][0]
        valid_loss_sum += valid_ls[-1][0]
        train_acc_sum += train_ls[-1][1]
        valid_acc_sum += valid_ls[-1][1]
    print('#' * 5, '最终k折交叉验证结果', '#' * 5)
    print('训练集累积损失:%.4f' % (train_loss_sum / k), '训练集累积准确度:%.4f' % (train_acc_sum / k),
          '测试集累积损失:%.4f' % (valid_loss_sum / k), '测试集累积准确度:%.4f' % (valid_acc_sum / k))

    return train_loss_sum, valid_loss_sum, train_acc_sum, valid_acc_sum


# 设置训练函数

def train(net, train_features, train_labels, test_features, test_labels, num_epochs, learning_rate, weight_decay,
          batch_size):
    """
    设置训练函数
    :param net: 神经网络模型
    :param train_features: 训练数据特征
    :param train_labels:   训练数据标签
    :param test_features:  测试数据特征
    :param test_labels:    测试数据标签
    :param num_epochs:     训练轮数
    :param learning_rate:  学习率
    :param weight_decay:   权重衰减
    :param batch_size:    批次大小
    :return:
        train_ls: 训练集的损失和准确度损失
        test_ls: 测试集的损失和准确度列表
    """
    # 初始化训练集和测试集的损失和准确度列表
    train_ls, test_ls = [], []
    # 创建训练数据集和数据加载器
    dataset = train_data_set(train_features, train_labels)
    train_iter = DataLoader(dataset, batch_size, shuffle=True)
    # 创建优化器
    optimizer = torch.optim.Adam(params=net.parameters(), lr=learning_rate, weight_decay=weight_decay)
    # 遍历每个训练轮次
    for epoch in range(num_epochs):
        # 遍历每个批次
        for X, y in train_iter:
            output = net(X)
            loss = loss_func(output, y)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        # 将当前轮次的训练集损失和准确度添加到列表
        train_ls.append(log_rmse(0, net, train_features, train_labels))
        # 测试集损失和准确度添加到列表中
        if test_labels is not None:
            test_ls.append(log_rmse(1, net, test_features, test_labels))
    #  返回训练集和测试集的损失和准确度列表
    return train_ls, test_ls


# 设置准确度计算函数
def log_rmse(flag, net, x, y):
    """
    计算对数均方根误差和准确度的函数
    :param flag: 0表示训练集,1表示测试集
    :param net: 评估的模型
    :param x: 数据特征
    :param y: 数据标签
    :return:
        loss.data.item(): 损失值
        accuracy: 准确度
    """
    # 如果是测试集,设置网络为评估模式
    if flag == 1:
        net.eval()
    # 前向传播 获取预测结果
    output = net(x)
    result = torch.max(output, 1)[1].view(y.size())
    # 计算正确预测的数量
    corrects = (result.data == y.data).sum().item()
    # 计算准确度
    accuracy = corrects * 100.0 / len(y)
    # 计算损失
    loss = loss_func(output, y)
    # 如果是测试集,设置网络为训练模式
    net.train()

    return loss.data.item(), accuracy


if __name__ == '__main__':
    """
    执行k折交叉验证
    """
    # 调用交叉验证函数
    k_fold(10, train_x, labels)
********** 第 1 折 **********
训练集损失:0.039320 训练集准确度:100.0000 测试集损失:0.024422 测试集准确度:100.0000
********** 第 2 折 **********
训练集损失:0.042435 训练集准确度:100.0000 测试集损失:0.024841 测试集准确度:100.0000
********** 第 3 折 **********
训练集损失:0.043264 训练集准确度:100.0000 测试集损失:0.024778 测试集准确度:100.0000
********** 第 4 折 **********
训练集损失:0.033361 训练集准确度:100.0000 测试集损失:0.018814 测试集准确度:100.0000
********** 第 5 折 **********
训练集损失:0.039068 训练集准确度:100.0000 测试集损失:0.024000 测试集准确度:100.0000
********** 第 6 折 **********
训练集损失:0.039641 训练集准确度:95.0000 测试集损失:0.370682 测试集准确度:95.0000
********** 第 7 折 **********
训练集损失:0.040322 训练集准确度:95.0000 测试集损失:0.447232 测试集准确度:95.0000
********** 第 8 折 **********
训练集损失:0.038236 训练集准确度:95.0000 测试集损失:0.425054 测试集准确度:95.0000
********** 第 9 折 **********
训练集损失:0.036090 训练集准确度:95.0000 测试集损失:0.414253 测试集准确度:95.0000
********** 第 10 折 **********
训练集损失:0.037329 训练集准确度:100.0000 测试集损失:0.353675 测试集准确度:100.0000
##### 最终k折交叉验证结果 #####
训练集累积损失:0.0389 训练集累积准确度:100.0000 测试集累积损失:0.2128 测试集累积准确度:98.0000

准确度为0的问题分析和解决

 ********** 第 1 折 ********** 
 训练集损失:0.690276 训练集准确度:0.0000 测试集损失:0.714697 训练集准确度:0.0000 
 ********** 第 2 折 ********** 
 训练集损失:0.686439 训练集准确度:0.0000 测试集损失:0.777147 测试集准确度:0.0000

解决方法:

train_y = torch.rand(100, 28, 28)

将其修改为:

train_y = torch.randn(100, 28, 28)

❌ 原始错误原因分析

1. 数据分布不合理
  • torch.rand:生成的是 [0, 1) 区间内的均匀分布随机数。
  • torch.randn:生成的是标准正态分布(均值为0,方差为1)的随机数,取值范围更广。

  • 在模型中:模型期望学习区分两类样本(标签为 0 和 1)。如果输入数据(如 train_xtrain_y)都是从相同分布(如 rand)生成的,那么这两类样本之间没有可区分的特征模式。更严重的是,rand 数据集中在 [0,1] 区间,导致模型输出难以区分两个类别,从而准确率始终为 0。
2. 训练集和验证集无区分性
  • 原始 train_xtrain_y 都使用了 torch.rand,它们本质上是同一类数据,只是被人为地赋予了不同的标签。这种构造方式使得模型无法学到任何有意义的分类边界。
3. 模型无法收敛到有效解
  • 因为输入数据缺乏类别间的差异性,损失函数无法下降到合理值。准确率一直为 0 是模型无法识别任何样本类别的直接体现。

✅ 修改后为何能解决问题?

train_y = torch.rand(100, 28, 28) 改为:

train_y = torch.randn(100, 28, 28)

带来的变化包括:

对比项 torch.rand torch.randn
分布类型 均匀分布 正态分布
数值范围 [0, 1) 大致 [-3, 3]
样本差异性
是否适合用于分类任务
  • 通过使用 torch.randntrain_xtrain_y 的数据分布出现明显差异,这为模型提供了可学习的特征差异。因此,模型可以逐渐学习如何区分这两个类别,准确率也随之提升。

📌 总结

错误点 原因 影响 解决方案
使用 torch.rand 构造数据 数据分布单一、无类别差异 模型无法学习分类边界 改用 torch.randn 提供更大差异性
缺乏真实数据 输入无语义信息 模型无法收敛 使用真实图像数据(如 MNIST)效果更佳