Jack He's Blog

some idea or work

Fast Decision Boundary based Out-of-Distribution Detector(FDBD)

ICML 2024
根据 OOD 样本到决策边界的特征距离来检测它们
采用的也是后处理方法 #post-hoc

[!info] OOD介绍
Out-of-distribution,分布外检测。在实际情况下,由于通常不满足封闭性类别平衡性独立同分布假设,因此需要进行识别,哪些是In-distribution样本,哪些是Out-of-distribution样本。通常考虑为一个二分类问题,采用指标为AUROC、FPR95、AUPR等

文章简要概述:

  1. 研究背景
    • 随着机器学习模型在现实世界中的广泛应用,不可避免地会遇到训练分布之外的样本(OOD),OOD检测对于确保模型的安全部署至关重要。
    • 现有的OOD检测方法分为基于模型输出空间和基于特征空间的方法,基于特征空间的方法虽然有效,但依赖于从训练特征构建的辅助模型,计算开销较大。
  2. 研究方向
    • 从决策边界的新视角设计事后(post - hoc)OOD检测器,在不使用辅助模型的情况下利用特征空间中的丰富信息,优化计算效率。
  3. 研究方法
    • 测量到决策边界的特征距离
      • 首先在多类分类器中定义了样本特征到决策边界的距离($D_f(z_x,c)$),由于决策区域的非凸性,该距离没有闭式解,难以直接计算。
      • 提出一种有效的闭式估计方法$\tilde{D}_f(z_x,c)$,从理论上证明了该估计紧密地下界(lower bound)了距离,并且通过实验验证了该方法的有效性(相对误差小于1.5%)和高效性(在推理过程中可在常数时间内计算,相比于迭代优化方法显著降低了计算开销)。
    • 基于决策边界的快速OOD检测器(fDBD)
      • 通过实验观察发现,同分布(In - Distribution,ID)特征相比OOD特征更远离决策边界,并且在将ID和OOD样本按照与训练特征均值的偏差水平进行分组比较时,两者的分离效果更好。
      • 基于上述观察,设计了一种基于正则化平均特征距离到决策边界的OOD检测分数($regDistDB$),通过对该分数设置阈值构建了fDBD检测器,该检测器无超参数、无辅助模型且计算高效(时间复杂度为$O(|C| + P)$,与训练类别的数量$|C|$和特征维度$P$线性相关)。
  4. 实验结果
    • 在CIFAR-10和ImageNet等不同的数据集、不同的训练目标(交叉熵损失和监督对比损失)以及不同的网络架构(ResNet、ViT和DenseNet)上进行了广泛的实验。
    • 使用FPR@95(95%真阳性率下的假阳性率)和AUROC(接收者操作特征曲线下面积)作为评估指标,结果表明fDBD在大多数情况下达到或超过了现有方法的OOD检测性能,同时保持了与普通softmax confidence检测器相当的推理延迟,几乎没有增加额外的开销。
    • 还进行了消融实验,验证了正则化对提高OOD检测效果的作用,以及在不同的激活塑造算法(ReAct、ASH和Scale)下fDBD的性能表现。
  5. 研究结论
    • 提出的fDBD是一种高效且有效的OOD检测器,基于特征到决策边界的距离这一新颖视角,通过闭式估计方法测量距离,揭示了ID和OOD样本与决策边界的关系,通过正则化设计了有效的OOD检测算法。
  6. 研究不足
    • 论文未明确提及研究的不足之处。
  7. 未来研究方向
    • 希望该算法能够启发未来从决策边界的角度探索模型不确定性的工作,包括OOD检测以及其他研究问题,如对抗鲁棒性和域泛化等。

Intro

后处理方法(post-hoc method):与训练无关(training-agnostic),适合在预训练模型上立即实施

  • eg. KNN,马氏距离等距离度量方法;MSP、O-DIN等置信度处理方法

先前方法的问题:它们对根据训练特征构建的辅助模型的依赖会产生额外的计算成本

ID 和 OOD 样本的特征相对于决策边界位于哪里?

为了回答这个问题,我们首先将特征距离到类决策边界的概念形式化。 我们将距离定义为特征空间中改变分类器对类别的决策的最小扰动,如图 1 左图所示。
我们特别关注倒数第二层,即线性分类头之前的层。 由于非凸性,倒数第二层上的距离无法轻易计算。
为了最大限度地减少测量距离的成本,该文章引入了一种有效的封闭式估计,经过分析证明可以严格限制距离。 直观上,特征到决策边界的距离反映了改变模型决策的难度,并且可以量化特征空间中的模型不确定性。
与输出空间 softmax 置信度(常见的MSP)不同,该文章的特征空间距离使用特征空间中嵌入的丰富信息进行 OOD 检测。

image.png|600

直观上,ID 样本的特征比 OOD 样本更远离决策边界,因为分类器在 ID 样本中可能更具决定性。当与训练特征平均值的偏差水平相等时,ID 和 OOD 可以更好地分开

问题定义

我们考虑一个数据空间$X$,一个类集$C$和一个分类器$f: X→C$,该分类器是在从联合分布$P_{XC}$ 中提取的样本 $i.i.d.$ 上训练的。我们表示$P_{XC}$在$X$上的边际分布为$P^{in}$。我们将从$P^{in}$中抽取的样本称为分布内(ID)样本。在实践中,分类器$f$可能会遇到$x∈X$,且它不是从$P^{in}$中提取的。我们称这样的样品为分布外(OOD)。

由于分类器无法对训练期间未见的类别的OOD样本做出有意义的预测,因此区分此类OOD样本和ID样本对于部署可靠性非常重要。此外,对于时间紧迫的应用,及时检测OOD样品以采取预防措施至关重要。与现有技术中使用ID特征聚类和构建辅助模型不同(Lee et al., 2018;Sun et al., 2022),我们可以从决策边界的角度研究OOD-ness,这本质上捕获了训练ID统计。

决策边界判断OOD

第三章主要讲述了使用决策边界检测离群分布(OOD)的相关内容

  1. 测量到决策边界的特征距离(3.1)

    • 定义距离概念:在多分类器的倒数第二层空间,定义样本$x$的特征嵌入$z_x$到类别$c$($c\neq f(x)$)决策边界的$L2$ - 距离$D_f(z_x,c)$为将模型决策变为类别$c$所需的最小扰动。由于决策区域通常非凸,该距离没有闭式解,难以直接计算。
    • 提出有效估计方法:定理3.2提出一种闭式估计$\tilde{D}_f(z_x,c)$,可有效且紧密地下界$D_f(z_x,c)$。从理论上证明了该方法的有效性,并且通过实证验证其精度高(相对误差小于1.5%)。同时分析了该距离测量方法的效率,其在推理过程中可在常数时间内计算,实证表明在CIFAR - 10分类器上使用该方法计算距离时,与不使用该方法相比,平均推理时间基本无增加,而通过迭代优化估计距离则耗时很长(992.2ms),从而验证了所提估计方法的高效性。
  2. 基于快速决策边界的OOD检测器(3.2)

    • 基于决策边界的假设与验证:从决策边界的角度研究OOD检测,假设由于模型对ID样本更确定,ID特征相比OOD特征更可能远离决策边界。通过大量验证(如附录J中的图展示了ID/OOD到决策边界的特征距离)以及可视化ID/OOD样本到决策边界的平均特征距离(图2左),证实了该假设。

    • 深入探究ID/OOD重叠区域:研究在平均距离到决策边界这一度量下ID/OOD的重叠区域(图3),发现ID和OOD样本的平均特征距离到决策边界都会随着特征偏离训练特征均值而增加,这导致具有较高偏差水平的OOD样本与较低偏差水平的ID样本难以区分,而在相同偏差水平内,OOD与ID样本可更好地分离。

    • 设计OOD检测分数与检测器:基于上述理解,设计OOD检测分数$regDistDB$,它是到决策边界的平均特征距离,并由到训练特征均值的特征距离进行正则化,该分数近似在相同偏差水平比较ID和OOD样本,可增强ID/OOD分离(图2)。通过对$regDistDB$设置阈值,提出基于快速决策边界的OOD检测器($fDBD$),将低于阈值的样本识别为OOD。$fDBD$无超参数且无需辅助模型,计算效率高,计算 $\tilde{D}_f(z_x,c)$ 为常数时间,计算 $regDistDB$ 中的 $|z_x - \mu_{train}|_2$ 时间复杂度为 $O(P)$ ( $P$ 为倒数第二层维度),整体 $fDBD$ 时间复杂度为 $O(|C| + P)$ ,对大规模数据集和模型具有计算可扩展性。

    • regDistDB:一种经过正则化处理的 OOD 检测分数,计算方式为:

    $$
    \begin{aligned}
    \text{regDistDB} := \frac{1}{|C| - 1} \sum_{\substack{c \in C \ c \neq f(x)}} \frac{\tilde{D}_{f}(z_{x}, c)}{\parallel z_{x} - \mu_{\text{train}} \parallel_{2}}
    \end{aligned}
    $$

    其中 $\tilde{D}_{f}(z_{x}, c)$ 是到决策边界的估计距离,$\mu_{\text{train}}$ 表示训练特征的均值。该分数通过正则化特征到训练均值的距离,实现在相同偏差水平下比较 ID/OOD 样本,增强检测效果。

    • avgDistDB:是未经过正则化的平均距离,计算方式为$avgDistDB := \parallel z - \mu_{train}\parallel_{2}regDistDB$,在研究正则化对OOD检测效果影响的实验中,被作为一种检测分数与经过正则化的$regDistDB$进行对比,以体现正则化在OOD检测中的作用。

实验

论文的第四章主要是通过实验展示了fDBD(Fast Decision Boundary based Out-of-Distribution Detector)在不同的OOD(Out-of-Distribution)基准测试中的高效性和有效性。具体内容如下:

CIFAR-10 Benchmark

  1. 实验评估指标
    • 使用了文献中广泛认可的两个指标:在95%真阳性率下的假阳性率(FPR95)和接受者操作特征曲线下面积(AUROC),FPR95分数越低、AUROC值越高表示性能越好。此外还报告了在Tesla T4 GPU上评估的每张图像的推理延迟(以毫秒为单位)。
  2. CIFAR - 10基准测试评估
    • 训练方案:评估了在标准交叉熵损失下训练的模型的OOD检测性能(准确率为94.21%),以及使用监督对比损失(SupCon)训练表示映射的模型(准确率为94.64%)。 (这个地方存疑,因为似乎没有在表格里面提到,怀疑这里的ACC指的是ID分类准确率)
    • 数据集:将CIFAR - 10测试集的10,000张图像作为ID(In - Distribution)测试样本,OOD样本考虑了SVHN、iSUN、Places365和Texture。
    • 基线方法:与六种基于标准交叉熵损失训练的模型的基线方法进行比较,包括MSP、ODIN、Energy、ViM、MDS、KNN,还考虑了在对比损失下特别有竞争力的四种基线方法CSI、SSD+、KNN+。fDBD是训练无关的,无需辅助模型并整合了决策边界视角的类别特定信息,且fDBD、MSP和Energy是无超参数的。
    • OOD检测性能
      • fDBD在FPR95和AUROC分数方面达到了最先进的性能,并且由于3.1节中的高效距离估计方法,计算开销最小。
      • 对比分析:
        • fDBD与MSP/Energy:三者都是无超参数且基于模型不确定性检测OOD,fDBD利用特征空间距离,相比MSP和Energy有显著性能提升,表明特征空间对OOD检测的重要性。
        • fDBD与KNN:在相同超参数设置下对比,fDBD和KNN在CIFAR - 10 OOD基准测试上检测效果都较好,但KNN由于使用辅助模型,平均推理时间较长;在ImageNet OOD基准测试中fDBD显著优于KNN,体现了从类别决策边界角度整合类别特定信息的优势。
        • fDBD与ViM:两者都将类别特定信息集成到特征空间表示中,但ViM在推理时需要昂贵的矩阵乘法,导致延迟增加,且fDBD性能优于ViM,尤其是在ImageNet OOD基准测试中,表明fDBD从特征空间类别决策边界角度整合信息的有效性。

image.png

ImageNet Benchmark

  • 训练方案、数据集和基线:考虑了4.1节中的训练方案,使用交叉熵损失和监督对比损失训练的模型,将ImageNet验证图像中的50,000张作为ID测试样本,OOD样本来自去除与ImageNet重叠类后的Texture、Places365、iNaturalist、SUN数据集。与4.1节中的相同基线进行比较(除CSI外),对于KNN考虑了两组超参数。
  • OOD检测性能:fDBD在ImageNet OOD基准测试的平均FPR95和平均AUROC方面优于所有基线,在大规模数据集上保持了OOD检测的高效性,这与在CIFAR-10基准测试中的观察结果一致,也支持了fDBD与类别数量和维度线性扩展的分析。

image.png

替代架构评估

在基于Transformer的ViT模型和DenseNet上进行实验,评估了fDBD、ViM和KNN在使用交叉熵损失对ImageNet - 1k进行微调的ViT - B/16上的性能(分类器准确率为81.14%),还在DenseNet上进行了扩展实验,结果表明fDBD在不同网络架构上的有效性。

image.png

激活整形下的评估

研究了激活整形算法(ReAct、ASH、Scale)对fDBD性能的影响,将这些算法作为标准ReLU激活的替代操作。fDBD作为无超参数方法,可以与这些算法无缝结合。在ResNet - 50上进行实验,结果表明应用激活整形后性能有所提高,验证了fDBD与这些算法的兼容性,且fDBD与Scale结合在该基准测试中达到了最先进的性能。

image.png

消融研究

  • 正则化的影响:比较正则化平均距离 $\text{regDistDB}$、正则化项 $\parallel z - \mu_{\text{train}} \parallel_{2}$ 和未正则化平均距离 $\text{avgDistDB}$ 的性能。实验表明:
    1. 单独使用 $\parallel z - \mu_{\text{train}} \parallel_{2}$ 区分能力有限
    2. 正则化显著增强 ID/OOD 分离效果
    3. $\text{regDistDB}$ 优于 $\text{avgDistDB}$,支持第 3 节的等偏差比较理论(附录 B 提供理论解释)
  • 单个距离的影响:对fDBD的检测分数(基于到决策边界的特征距离的平均值)进行消融研究,通过对每个样本到决策边界的特征距离排序,使用前k个最小距离值的平均值来检测OOD。在CIFAR - 10和ImageNet基准测试上的实验表明,随着k的增加性能提高,证明了fDBD作为无超参数方法利用所有距离进行OOD检测的设计合理性。

image.png

创新点

  • 提出了一种新的判断OOD的方法,且效果上达到SOTA
  • 训练时间和复杂度明显缩小,采用两种损失效果均优秀
  • 消融实验说明正则化的平均距离效果具有优越性,能显著提高AUROC

FSAC 方法

原位浮游生物图像检索(IsPlanktonIR)框架主要由左侧基于CBIR的识别部分和右侧人机交互部分组成。 前者可以自动识别原位捕获的海洋浮游生物和颗粒的感兴趣区域(ROI)图像,后者支持对识别结果进行手动验证和校正,以在必要时提高或恢复性能。 下面介绍框架中关键组件的功能、结构和原理。

image.png

自动识别部分主要由四个模块组成,包括参考图库、图像特征提取器、图像相似性度量和决策策略。 图库中预先存储了大量手动标记的ROI,它们都被特征提取器转换为特征向量并写入查找表(LUT)。 当任何查询ROI图像输入到框架进行识别处理时,首先由特征提取器将其转换为特征向量,然后与LUT中的所有特征向量进行比较。 最后,利用得分最高的前k张图像的标签来确定查询ROI的识别结果。

特征提取器

采用50层结构的SEResNeXt(Hu et al., 2018),丢弃其网络末端的全连接层,仅保留特征提取部分作为图像检索框架中的特征提取器,如图2所示。SEResNeXt 基于 ResNeXt 网络(Xie et al., 2017),并结合了Squeeze-and-Exitation注意力模块。 它已被证明在许多图像分类任务中表现良好(Hu et al., 2018;Olsvik et al., 2019; Han et al., 2020)。 这种结构使得网络具有很强的抽象能力,其注意力机制也帮助网络关注对图像表示贡献更大的部分。

image.png

提取器训练

由于 SEResNeXt 仅接受 224 x 224 像素 RGB 彩色图像输入,因此首先使用填充和重新缩放操作对每个 ROI 进行预处理,以实现无失真尺寸归一化(Li et al., 2022)。 之后,将训练数据集中所有ROI的像素值转换为[0, 1],然后根据公式(1)进行归一化,以加速训练收敛
$$
I_{norm}=\frac{1-mean}{SD}
$$
其中mean和SD分别表示所有训练ROI的像素值平均值和标准差。 特征提取后,输入图像被转换为​​长度为2048的特征向量

接下来,我们使用 SCL 来训练特征提取器。 训练过程如图3所示。SLC使用Siamese网络结构,一对对称分支共享相同的网络结构和权重。 除了特征提取器之外,每个分支最后还有一个由两层全连接网络组成的投影仪,以进一步降低输出特征向量的维数。 在每次训练迭代期间,从训练集中随机选择一批 N 个图像,然后将每个图像随机增强两次以生成总共 2N 个图像。
image.png

图像增强可以是剪切、旋转、翻转、灰度、亮度调整、对比度调整和饱和度调整。 然后增强的 ROI 形成总共 N × (2N−1) 个图像对,没有任何组合重复。 根据它们的标签,属于同一类别的每对 ROI 构成正对,而属于不同类别的每对 ROI 构成负对。 经过特征提取和投影后,一对 ROI 都被简化为两个 128 维向量,并通过 SupCon 损失(Khosla et al., 2020)进一步进行比较,如公式(2)所示,
image.png
其中 I 描述了所有索引 输入 ROI 批次的,P(i) 是增强 ROI 批次中除 i 之外的所有正例的索引集,A(i) 是增强批次中除 i 之外的所有 ROI 的索引集,zi 描述第 i 个 L2归一化后的特征向量,τ是一个温度超参数,它控制正负值之间的平衡(在我们的例子中τ = 0.07)。 通过这样对不同图像对进行迭代后的SCL,网络最终达到了特征空间中压缩正片、推开负片的效果。

特征提取器的训练使用在 ImageNet 上预训练的权重进行初始化。 使用动量为 0.9、小批量为 128 的 SGD 进行优化。学习率在前 10 个 epoch 中从 0 线性增加到 0.2,然后通过余弦衰减函数逐渐降低到 0.0001。 我们使用 PyTorch 以分布式数据并行模式训练四个 NVIDIA RTX 3090 GPU 的网络,训练最终在 150 个 epoch 后收敛。

Gallery建设与优化

所有图库图像都会转换为特征向量,组成存储在计算机硬盘中的 LUT。 识别时,将LUT加载到内存中进行比较,无需重复计算参考特征。 由于每张查询图像的特征都要与所有参考特征进行比较,因此LUT的特征数量将影响检索效率。 因此,对LUT进行更好的细化,在保留实例多样性的前提下去除冗余实例,即只有那些在特征空间中的相似度低于阈值的实例才值得保留。 最后,可以在测试数据集上确定每个类的相似度阈值,以去除冗余示例并最小化LUT大小,从而平衡检索精度和效率。

实验部分

数据集

如图5所示,为了更好地利用有限的数据来模拟天然海水中可能的DSS和OOD情况,我们将构建的数据集进一步分为$D_{gallery}$和$D_{test}$两个子集,并具有$C_1$、$C_2$和$C_{OOD}$三个标签组 训练特征提取器并评估 IsPlanktonIR 框架。

子集 Dgallery 由 DYB-PlanktonNet 中每个类别的 $4/5$ ROI 组成,Dgallery 中的所有 90 个目标标签被随机分为 $|C1|= 60$ 和 $|C2|= 30$ 两组。

IsPlanktonIR 框架中的画廊,也用于为以下实验提供训练数据。 子集$D_{test}$包含DYBPlanktonNet的其余1/5左侧ROI和所有长江数据,用于构建各种测试集以评估IsPlanktonIR框架的识别性能。

除了$C1$和$C2$的目标类别外,$D_{test}$中长江数据的标签被标记为$C_{OOD}$。

实验

image.png

在实验1中,整个Dgallery被用来训练第一个特征提取器,Dtest(C1∪C2)中的例子被用作测试集来评估IsPlanktonIR的性能 框架。 该实验的结果被用作基线模仿理想的独立同分布情况。 此外,还使用相同的数据集来训练和测试(Li et al., 2022)中使用的 ResNet18 分类模型,以进行性能比较。

接下来,仅使用Dgallery(C1)中的示例来训练第二个特征提取器,并使用这个新的特征提取器重建图像检索框架。 通过设置与Dtest不同的测试集组成,设计了以下实验2-5的四个实验,以进一步测试IsPlanktonIR在不同模拟数据条件下的性能。

实验2中,使用Dtest(C1∪C2)样本来测试IsPlanktonIR的识别性能。

实验3中,我们在测试集中添加了Dtest(COOD)数据,即用整个Dtest来测试IsPlanktonIR的识别性能。

在实验4中,我们进一步对Dtest(C1∪C2)进行重新采样,生成50个测试集,并按照(González et al., 2017)中推荐的方式使用它们来测试IsPlanktonIR在处理DSS问题时的识别性能。在数据重采样过程中,我们参考了已知的“非浮游生物颗粒>桡足类>其他”的相对数量关系,使测试集的分布更接近真实天然海水中的分布(Suthers等,2019)。 因此,每个测试集中非浮游生物颗粒的比例为其样本总数的50%~90%,其余样本的50%~90%为桡足类,其余样本构成其余样本。 另一方面,我们使用均匀分布率对每个类进行随机重采样,并在每个测试集中将非浮游生物颗粒、桡足类等的分类样本数大小分别限制在 600、200 和 100,以最大限度地 确保人工生成的测试集和训练集之间存在分布差异

在实验5中,我们将Dtest(COOD)数据添加到实验4中生成的50个测试集,以进一步评估IsPlanktonIR框架。 由于OOD数据独立于Dgallery,我们直接使用全部或随机子采样它们进行测试。此外,实验5中的数据集也用于训练和测试AD分类器,如(Pu et al., 2021)中提出的 进行比较。 由于AD模型无法对测试集中的Dtest(C2)示例进行分类,因此我们仅测试其在Dtest(C1)分类和Dtest(COOD)示例检测上的性能。

image.png

对于测试集中 OOD 数据的实验 3 和 5,我们将相似度值设置为 0.75 作为 IsPlanktonIR 拒绝识别的阈值

最后,为了测试LUT剪枝对图像检索框架的影响,我们在实验2的设置下,进一步评估了不同LUT压缩率下相似度阈值变化的性能。

在所有实验中,准确率(ACC)、精度、召回率和 F1 用作表征 IsPlanktonIR 框架性能的指标。

251f08b2adc0742862e1c4c99293635.png

MSP方法(Maximum-Softmax-Probabilities)

A Baseline For Detecting Misclassified And Out-Of-Distribution Examples In Neural Networks - ICLR 2017

github链接:hendrycks/error-detection: A Baseline for Detecting Misclassified and Out-of-Distribution Examples in Neural Networks

采用的是后处理方法 #post-hoc

思想

作者巧妙的规避了显式的设定置信度阈值的评价方法,而是采用了两个阈值无关的评价准则,与AUPR。值得说明的一点是,虽然这两个评价指标不需要定义阈值,但是在实际应用中,需要在ROC曲线或者PR曲线上选择合适的点,也就是合适的置信度阈值来进行OOD判断

OOD检测本质上是一个二分类问题,对于测试样本,我们是知道它们的真实标签的,也就是0-1标签,表示是ID样本还是OOD样本。同样的,我们的模型利用Softmax输出的最大置信度作为当前样本是ID数据的概率。利用这两组数值,我们就可以计算AUPR与AUROC。在计算AUPR的时候,作者将ID看作正样本计算AUPRin,将OOD样本看作正样本计算AUPRout,这两个指标分别反映了网络检测ID的能力与检测OOD的能力。AUPR以及AUROC这两个指标都是越大越好。

评估指标选择

AUROC

  • 提出检测分类器的指标是AUROC,而不是ACC。原因是ACC无法很好地面对正负样本失衡问题,如果负类的可能性远高于正类,那么模型会总是猜测负类并获得较高的ACC
  • 采用了接收者工作特征曲线下面积(AUROC)指标,这是一种与阈值无关的表现评估(Davis & Goadrich, 2006)
  • 此外,AUROC可以被解释为一个积极的例子比一个消极的例子有更大的检测器得分/值的概率(Fawcett, 2005)。因此,随机正例检测器对应于50% AUROC,而“完美”分类器对应于100%

AUPR

  • AUROC没有解决threshold selection(阈值选择)问题
  • 当正类和负类的基本利率(base rates)差异很大时,AUROC并不理想,AUPR会根据这些不同的正和负基本利率进行调整。

那么如何计算AUROC与AUPR呢?OOD检测本质上是一个二分类问题,对于测试样本,我们是知道它们的真实标签的,也就是0-1标签,表示是ID样本还是OOD样本。同样的,我们的模型利用Softmax输出的最大置信度作为当前样本是ID数据的概率。利用这两组数值,我们就可以计算AUPR与AUROC。在计算AUPR的时候,作者将ID看作正样本计算AUPRin,将OOD样本看作正样本计算AUPRout,这两个指标分别反映了网络检测ID的能力与检测OOD的能力。AUPR以及AUROC这两个指标都是越大越好。

模型

大多数分类器都是对于输出的逻辑 (logit) 进行softmax 函数计算后得到预测概率,然后取概率最大值作为预测的类别。由于softmax 函数是使用指数函数进行计算的,因此对逻辑值微小增加,最后的预测概率会发生显著的增加,类似于“赢者通吃”的现象,softmax函数可以认为是指示函数 (indicator function) 的平滑近似。

当分类器进行ID分类时,选择最大概率的类作为输出的类。当遇到OOD数据时,由于模型只有ID类的概率,因此此时无法进行OOD检测。理想情况下,当遇到OOD数据时,模型因为无法识别该样本,因此应该输出一个均匀分布,对所有ID类的概率都相等,此时熵(entropy)最大。实际上很难发生这种情况,OOD类可能由于某些特征与ID类有些相似,此时模型也会因为 softmax 函数的缘故,并不会出现均匀分布的情况,而是某个类的概率显著比其他高,分类器此时很难进行OOD检测。

但是分类器对于ID数据和OOD数据输出的最大概率也有区别:对于OOD数据的最大概率会比ID数据的最大概率低,因此可以通过这种方法,将该概率认为置信度来进行OOD检测。当分类器遇到ID样本时,输出的类概率高 (置信度高) ,遇到OOD样本时,输出的概率虽然比较高,但是相对于ID样本是低的 (置信度低),因此可以设置一个阈值来作为ID和OOD样本的检测边界。

实验

实验的目标是检测一个样本是否被错误分类,或者是否属于模型未见过的分布(即分布外样本)。为此,实验使用了神经网络模型的softmax probability来进行检测

在每个样本上,模型通过软最大函数计算出各个类别的概率,并返回概率最大值作为该样本的预测类别。这就是文中提到的maximum/predicted class probability

image.png

AUROC 指标相对基础版本高,说明模型能识别大部分正例样本,且好于基础base版本
AUPRSUCC高于 BASE 说明模型好于基础版本,AUPRSUCC和AUPRERR有很大的差距,说明可以通过设定预测出的得分阈值,检测是否是错误样本;wrong mean值高,说明如果只单独计算softmax无法检测出是否是错误样本,这点在论文中已有描述。这里AUROC没有设定AUROC_ERR和AUROC_SUCC 是因为这俩个结果是一致的 $AUROC = P(S > E) = P(-E > -S)$.

image.png

实验的核心目的是**检测样本是否属于分布内(In-distribution)**,而不是对样本进行具体类别的预测。也就是说,这些实验关注的是模型是否能够判断一个样本是来自训练数据的分布(即“分布内”样本),还是来自不同的分布(即“分布外”样本)。

可以看出,相对于真实数据集,高斯噪声以及均匀噪声是很容易被检测出来的,它们被模型赋予了很低的置信度,对应的,它们的AUROC以及AUPR指标都很高。

总而言之,AUROC是综合反应模型检测效果的,AUPRin反应的是模型检测ID数据的能力,AUPRout反应的是模型检测OOD数据的能力,较高的指标寓意着性能更好的检测模型。

补充

AUPR Succ(精确度-召回率曲线下的面积,针对正确分类的样本)和AUPR Err(精确度-召回率曲线下的面积,针对错误分类的样本)之间的区别主要在于它们分别评估模型在正确分类样本错误分类样本上的表现。

AUPR Succ(针对正确分类的AUPR)

  • 定义:AUPR Succ衡量的是模型在正确分类样本上的表现。具体来说,这个指标评估的是模型对正确分类的正类样本(即模型正确识别的正类样本)的精确度(Precision)和召回率(Recall)之间的平衡。
  • 计算方法
    • 在计算AUPR Succ时,所有被正确分类的样本(即模型预测类别与真实类别一致的样本)都被视为“正类”。
    • 然后,基于这些“正类”样本,我们计算每个阈值下的精确度和召回率,绘制精确度-召回率(PR)曲线,并计算该曲线下的面积(即AUPR Succ)。
    • 结果越高,说明模型在正确分类的样本上的表现越好,精确度和召回率的平衡较好。
  • 适用情况:AUPR Succ用于评估模型在“成功”分类样本上的精确度和召回率的平衡,通常适用于模型已经表现良好的情况,特别是在正类样本和负类样本不平衡的情况下,AUPR Succ是一个非常有意义的指标。

AUPR Err(针对错误分类的AUPR)

  • 定义:AUPR Err衡量的是模型在错误分类样本上的表现。这个指标关注的是模型错误地将正类样本分类为负类的情况,计算错误分类(误判的正类样本)作为正类的精确度和召回率。
  • 计算方法
    • 在计算AUPR Err时,所有被错误分类的样本(即模型预测类别与真实类别不一致的样本)被视为“正类”。
    • 然后,基于这些“错误分类”样本,我们计算每个阈值下的精确度和召回率,绘制精确度-召回率(PR)曲线,并计算该曲线下的面积(即AUPR Err)。
    • AUPR Err反映了模型在误分类样本上的表现。AUPR Err值越高,说明模型越能识别和区分错误分类的样本。
  • 适用情况:AUPR Err用于评估模型在“错误分类”样本上的精确度和召回率的平衡,通常适用于分析模型的误差以及模型在识别错误时的能力。

AUPR Succ和AUPR Err的区别

  1. 关注的样本不同
    • AUPR Succ关注的是模型正确分类的样本,它评估的是模型对正确分类的样本(正类)的精确度和召回率。
    • AUPR Err关注的是模型错误分类的样本,评估的是模型对错误分类的样本(正类被错误地分类为负类)的精确度和召回率。
  2. 精确度和召回率的目标不同
    • AUPR Succ的目标是最大化模型在正确分类样本上的精确度和召回率,即我们希望模型能够正确识别更多的正类,同时尽量避免将负类错误地分类为正类。
    • AUPR Err则是分析模型如何处理错误分类的样本,特别是模型是否能够识别出那些被错误分类为负类的正类样本。在这个情况下,模型的表现并不依赖于它识别出多少正类,而是它识别出多少原本应该是正类的样本并避免将其误分类。
  3. 在实际应用中的意义
    • AUPR Succ适用于评估模型在正类样本上的表现,特别是在样本不平衡时,能够说明模型对正类样本的识别能力。
    • AUPR Err则用于分析模型如何识别错误分类的样本,帮助我们了解模型在处理“难例”时的表现。

举个例子

假设我们有一个二分类任务,任务是区分“猫”和“狗”。在测试集上,我们得到以下样本:

  • 正确分类的样本:例如,模型将猫图像预测为猫,狗图像预测为狗。
  • 错误分类的样本:例如,模型将猫图像预测为狗,狗图像预测为猫。
AUPR Succ:
  • 我们将正确分类的猫图像和狗图像视为正类,计算精确度和召回率,并绘制PR曲线。我们希望模型能够正确识别这些猫和狗,避免错误分类。
AUPR Err:
  • 对于被错误分类的猫图像和狗图像,我们将它们视为正类,计算它们在误分类时的精确度和召回率。我们希望模型能够检测到这些错误分类的样本,并提高其识别能力。

总结

  • AUPR Succ衡量的是模型在正确分类样本上的精确度和召回率的表现,适用于评估正确分类的能力。
  • AUPR Err衡量的是模型在错误分类样本上的精确度和召回率的表现,适用于评估模型在识别错误时的能力。 这两个指标从不同的角度衡量模型的性能,结合使用可以帮助我们全面了解模型在正类和负类样本上的表现。

ODIN(Out-of-DIstribution detector for Neural networks)

Enhancing the reliability of out-of-distribution image detection in neural networks ICLR 2018

github链接:facebookresearch/odin: A simple and effective method for detecting out-of-distribution images in neural networks.

  • 对softmax函数进行温度缩放(temperature scaling)和输入扰动(small controlled perturbations)
  • 不需要重新训练新的网络

However, when deploying neural networks in real-world applications, there is often very little control over the testing data distribution. Recent works have shown that neural networks tend to make high confidence predictions even for completely unrecognizable or irrelevant inputs(对很能识别的或者完全无关的类输入也会产生高置信度。 这里给予的还是最基础的OOD方法,根据置信度判断)

使用的是后处理方法 #post-hoc

使用的trick

温度缩放

具体参考:Calibration of Modern Neural Networks
首先学习一下temperature scaling的作用,在这之前需要了解一下knowledge distillation and calibrated(知识蒸馏与校准)。论文提出temperature scaling可以区分ID和OOD的最大softmax分数。那么什么是模型校准呢?

通常模型的输出是一个对应最大softmax的索引,也就是输出预测类,但是如果我们希望模型可以输出预测的置信度(confidence)是多少,那么这个confidence就是calibrated的。譬如:进行一个分类任务,将模型预测判断为某一类A且confidence score为90%的所有样本统计在一起,总数为N;并对着N个样本进行真实类别统计,如果有90%的样本都为A类,则说明该模型是calibrated的

image.png

如上图所示,横纵坐标分别是confidence和acc,蓝色图代表的模型输出,灰色线代表calibration。那么该模型就是较自信的(输出的confidence大于实际acc)譬如在confidence为0.8的这些输出中,期望的是有80%的应该分类正确,但实际只有60%分类正确,那么也就是“网络过于自信了”,输出的置信度具体式子如下:
image.png
如果没有参数T,那么置信度就是softmax对应最大值

image.png

$T$ 的作用可以这样理解:

  • 当 $T \to \infty$ 时,上式会趋于$\dfrac{1}{K}$,其中$K$表示类别数,也就是confidence score为1/K,那就是说模型完全的不确定是否判断正确(模型说”我是瞎猜的”)
  • 当 $T \to 0$ 时,上式会趋于 $1$,代表置信度为1(模型说”我预测的就是对的”)
    image.png|575

那么根据之前讲的 直接用argmax softmax作为置信度分数有些过于自信了,我们就可以设置参数T,调整他的置信度。注意的是,调整需要用valid set进行调整,训练后利用NLL调整参数T的值。论文中是直接给定T,经过实验和推导证明T越大,检测效果越好

好处

Temperature Scaling不会改变各个维度之间的相对顺序关系,这丛上面的两个示例也可以看出。这保证了Max-Softmax方法结合Temperature Scaling使用,性能只可能上升,而不可能下降,这是非常巧妙的一点。作者在文中对温度做了一系列消融实验,对比了不同温度的作用效果:

image.png

从上图左侧的四幅图中可以看出,在不同的温度设置下,OOD检测的性能均高于Max-Softmax(对英语温度为1的时候)。

输入扰动

除了对Softmax的输出进行处理之外,作者还提出了另外一种针对输入图像的处理方法,文中称作Input Preprocessing。这个方法是基于一个先验的:由于我们的分类网络是在ID数据上训练的,那么我们可以直观的认为,ID数据分布在得分峰值或者是峰值附近,而OOD数据则往往分布在远离得分峰值的区域。

image.png|425

在上图中,蓝色字体表示OOD数据,红色字体表示ID数据。竖直方向的坐标轴表示的是经过Softmax后输出的得分,也就是上文中提到的。因为卷积神经网络是在ID数据上训练的,它对于ID数据更加敏感,并且,ID数据在训练过程中会尽量聚集在得分峰值或者是峰值附近。与此不同的是,OOD数据往往分布在距离峰值较远的区域

论文里提到对输入增加一个小扰动,其motivation是利用对抗样本,强迫模型输出错误的结果以及对应真实类别的低softmax score。但是论文里是相反的,希望利用一种扰动,使得输出的softmax score可以更高,the perturbation can have stronger effect on the in distribution images than that on out-of-distribution images, making them more separable(扰动对分布图像的影响比对分布外图像的影响更大,使它们更容易分离)。

image.png

对抗样本的进一步想法可以看Out-of-distribution Detection系列专栏(一)_decoupling maxlogit for out-of-distribution detect-CSDN博客

实验

指标

  • FPR@95
  • Detection Error 即$P_e$衡量TPR为95%时的误分类概率。$P_e$的定义为$P_e = 0.5(1 - TPR) + 0.5 FPR$,其中我们假设正反例在测试集中出现的概率相等。
  • AUROC
  • AUPR

image.png

image.png

分布距离衡量

带高斯RBF核的最大平均离散度MMD
image.png

我们的方法与分布内和分布外数据集之间的MMD的表现。神经网络在CIFAR-100上进行训练。分布外数据集为1:LSUN (cop), 2: TinyImageNet (crop), 3: LSUN (resize), 4: is iSUN (resize), 5: TinyImageNet (resize)。
image.png

  • 裁剪后的数据集与CIFAR-100之间的MMD距离往往更大。这可能是因为裁剪的图像只包含局部图像上下文,因此与CIFAR-100图像更不同,而调整大小的图像包含全局模式,因此与CIFAR-100中的图像相似。
  • MMD距离与检测表现呈负相关。这表明检测任务变得更加困难,因为分布内和分布外的图像彼此更加相似

G-ODIN

CVPR2020:Generalized ODIN: Detecting Out-of-distribution Image without Learning from Out-of-distribution Data

image.png

ODIN有一个要求,即它需要OOD数据来调整其两个策略的超参数,这导致了一种担忧,即用一个分布外数据集调整的超参数可能无法推广到其他数据集

使用的是训练方法 #training

image.png

在概念上将差异类型分为非语义转移和语义转移

  • 具有非语义偏移的数据从分布 $p_{out}(x, y)$ 中提取。具有这种偏移的示例来自相同的对象类,但以不同的形式呈现,例如卡通或素描图像。
  • 在语义转移的情况下,数据是从${\bar{y}}∩{y} =∅$的分布$p_{out}(x,\bar{y})$中提取的

引入一个显式的二元域变量$d∈{d_{in}, d_{out}}$来表示这个决策,其中$d_{in}$表示输入是$x \sim p_{in}$,而$d_{out}$表示$x≁p_{in}$(或等价于$x \sim p_{out}$)

作者在classifier中使用变量 $d_{in}$ 联合class-domain概率和domain probability重写条件概率如公式2所示。
image.png

公式2也从某一侧面反映出为什么classifiers会趋向于overconfident。即,当有一个样本 $x \sim p_{out}$,我们希望分子小(e.g. 0.09),分母也小(e.g. 0.1),这造成预测概率还是很大(0.9);

[!note] 使用FNN处理图像时遇到的问题

  • 参数过多:$100 \times 100 \times 3$ 的图像,,在FNN中,第一个隐藏层的每个神经元到输入层都有$100 \times 100 \times 3=30000$ 个连接,且每个连接都有1个独立的参数,那么随着隐藏层神经元增多,参数规模也会急剧增大,使得训练效率非常低
  • 局部不变性特征:尺度缩放、平移、旋转等操作不影响语义信息,而FNN很难提取

卷积神经网络,也就是convolutional neural networks(简称CNN),现在已经被用来应用于各个领域,如物体分割,风格转换,自动上色等。但是CNN真正能做的,只是起到一个特征提取器的作用,所有这些应用,都是建立在CNN对图像进行特征提取的基础上进行的。

在用神经网络训练提取图像特征时,我们主要考虑到图像的两个性质:

  1. _平移不变性_(translation invariance):不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为“平移不变性”。
  2. _局部性_(locality):神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,这就是“局部性”原则。最终,可以聚合这些局部特征,以在整个图像级别进行预测。

在李宏毅的网课上,他提到了三个特性,是针对神经网络的:

  • 局部连接
  • 权重共享
  • 汇聚

怎么把图像当做模型的输入呢?对于机器,图像可以描述为三维张量(张量可以想成维度大于 2 的矩阵)。一张图像是一个三维的张量,其中一维代表图像的宽,另外一维代表图像的高,还有一维代表图像的通道(channel)的数目(如果为rgb,那么就有3个色彩通道)。

再传入网络之前,需要先进行“拉直”,即将其按照通道、排列进行衔接,形成一个巨大的向量
image.png

如果把原本的向量作为全连接网络输入,那么特征向量会非常长,对应需要的权重参数也会非常多。因此需要专门针对图像任务对网络进行修改

修改后的网络输出一个向量$y’$,通过softmax后输出一个$\hat{y}$,我们希望$y’$和$\hat{y}$的交叉熵越小越好

参考:
卷积神经网络CNN完全指南终极版(一) - 沉迷学习的糕糕的文章 - 知乎

卷积基本概念

观察1:检测模式不需要整张图像

比如,检测一只鸟,我们往往是通过看到了显著的特征,从而进行分类
那么,对于神经元来说,只要它们看到的一小部分检测出了一些关键模式,即可帮助进行物种判断
image.png

感受野

根据观察 1 可以做第 1 个简化,卷积神经网络会设定一个区域,即感受野(receptivefield),每个神经元都只关心自己的感受野里面发生的事情,感受野是由我们自己决定的。

蓝色的神经元看左上角这个范围,这是它的感受野。黄色的神经元看右下角 3 × 3 × 3 的范围。图 4.7 中的一个正方形代表 3 × 3 × 3 的范围,右下角的正方形是黄色神经元的感受野

神经元会把 3 × 3 × 3 的数值“拉直”变成一个长度是 3 × 3 × 3=27 维的向量,再把这 27 维的向量作为神经元的输入,这个神经元会给 27 维的向量的每个维度一个权重,所以这个神经元有 3 × 3 × 3 = 27 个权重,再加上偏置(bias)得到输出。这个输出再送给下一层的神经元当作输入。

感受野可以互相重叠
image.png

在卷积神经网络中,感受野的定义是 卷积神经网络每一层输出的特征图(feature map)上的像素点在原始图像上映射的区域大小,更深的网络可以使得感受野更广阔

image.png

假设我们有一个简单的卷积神经网络,其中包含两个卷积层。我们将关注第二个卷积层的一个特定神经元,想要计算它的感受野大小。

  1. 第一个卷积层的每个神经元在输入图像上应用了3x3大小的卷积核
  2. 第一个卷积层的步幅(stride)为1,没有填充(padding为0)。
  3. 第二个卷积层的卷积核大小也为3x3,步幅为1,没有填充。

现在,让我们计算第二个卷积层的一个神经元的感受野大小:

  1. 第一个卷积层的神经元的感受野是3x3,因为它受到了3x3大小的卷积核的影响。
  2. 第二层的卷积核大小为3x3。但是,由于它是在第一层的输出上操作,通过上图的映射可以看到第二层神经元的感受野是5x5。

可以看到,随着网络的深度增加,神经元的感受野也逐渐扩大,可以感受到更大范围的输入信息。这种分层结构允许网络在不同层次上捕获输入数据的不同特征和抽象信息,从而有助于更好地理解图像或其他类型的数据

感受野计算公式:
$$
RF_{i}=(RF_{i+1}-1)\times stride_{i}+K_{size_{i}}
$$
卷积计算公式:N=(W-F+2P)/S+1

卷积计算

概念|卷积的三种模式:valid、same、full_valid卷积-CSDN博客

计算即为互相关运算

在二维互相关运算中,卷积窗口从输入张量的左上角开始,从左到右、从上到下滑动。 当卷积窗口滑动到新一个位置时,包含在该窗口中的部分张量与卷积核张量进行按元素相乘,得到的张量再求和得到一个单一的标量值,由此我们得出了这一位置的输出张量值。

注意,输出大小略小于输入大小。这是因为卷积核的宽度和高度大于1, 而卷积核只与图像中每个大小完全适合的位置进行互相关运算。 所以,输出大小等于输入大小$n_h×n_w$减去卷积核大小$k_h×k_w$,即:

$$
(n_{h}-k_{h}+1)\times (n_{w}-k_{w}+1)
$$

填充

[!question] 感受野超出了图像的范围,怎么办呢?
如果不在超过图像的范围“摆”感受野,就没有神经元去检测出现在边界的模式,这样就会漏掉图像边界的地方,所以一般边界的地方也会考虑的。如图 所示,超出范围就做填充(padding),填充就是补值,一般使用零填充(zero padding),超出范围就补 0
image.png

image.png

通常,如果我们添加$p_h$行填充(大约一半在顶部,一半在底部)和$p_w$列填充(左侧大约一半,右侧一半),则输出形状将为
$$
(n_{k}-k_{h}+p_{h}+1) \times (n_{w}-k_{w}+p_{w}+1)
$$

卷积核的高度和宽度通常为奇数,例如1、3、5或7。 选择奇数的好处是,保持空间维度的同时,我们可以在顶部和底部填充相同数量的行,在左侧和右侧填充相同数量的列

更详细的计算公式:
image.png

步幅

在计算互相关时,卷积窗口从输入张量的左上角开始,向下、向右滑动。 在前面的例子中,我们默认每次滑动一个元素。 但是,有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。

滑动的幅度,是超参数,因为希望感受野跟感受野之间是有重叠的,所以步幅往往不会设太大,一般设为 1 或 2。

当设置垂直步幅为3,水平步幅为2的二维互相关运算时,可以看到,为了计算输出中第一列的第二个元素和第一行的第二个元素,卷积窗口分别向下滑动三行和向右滑动两列。但是,当卷积窗口继续向右滑动两列时,没有输出,因为输入元素无法填充窗口(除非我们添加另一列填充)。

image.png

通常,当垂直步幅为$s_h$、水平步幅为$s_w$时,输出形状为
$$⌊(n_h−k_h+p_h+s_h)/s_h⌋×⌊(n_w−k_w+p_w+s_w)/s_w⌋.$$

如果我们设置了$p_h=k_h−1和p_w=k_w−1$,则输出形状将简化为$⌊(n_h+s_h−1)/s_h⌋×⌊(n_w+s_w−1)/s_w⌋$。 更进一步,如果输入的高度和宽度可以被垂直和水平步幅整除,则输出形状将为$(n_h/s_h)×(n_w/s_w)$。

观察2:共享参数(通道)

同样的模式可能会出现在图像的不同区域

假设其中有一个神经元可以检测鸟嘴,鸟嘴出现在图像的中间也会被检测出来。

这些检测鸟嘴的神经元做的事情是一样的,只是它们守备的范围不一样。既然如此,没必要每个守备范围都去放一个检测鸟嘴的神经元。而是让不同感受野的神经元共享参数

image.png

图 4.16 中使用一样的颜色代表这两个神经元共享一样的参数,所以每个感受野都只有一组参数,就是上面感受野的第 1 个神经元会跟下面感受野的第 1 个神经元共用参数,上面感受野的第 2 个神经元跟下面感受野的第 2 个神经元共用参数……所以每个感受野都只有一组参数而已,这些参数称为滤波器(filter)

image.png

由此引入多输入多输出通道的具体内容

多输入通道

  • 当输入通道数$c_{i}=1$时,卷积核即为简单的$k_{h} \times k_{w}$的二维张量
  • 但当$c_i>1$时,我们卷积核的每个输入通道将包含形状为$k_h×k_w$的张量
  • 将这些张量$c_i$连结在一起可以得到形状为$c_i×k_h×k_w$的卷积核。由于输入和卷积核都有$c_i$个通道,我们可以对每个通道输入的二维张量和卷积核的二维张量进行互相关运算,再对通道求和(将$c_i$的结果相加)得到二维张量。
  • 这是多通道输入和多输入通道卷积核之间进行二维互相关运算的结果。

image.png

现实情况在相加后可能还有偏置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch
from d2l import torch as d2l

def corr2d_multi_in(X, K):
# 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在一起
return sum(d2l.corr2d(x, k) for x, k in zip(X, K))

X = torch.tensor([[[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]],
[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]])
K = torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[1.0, 2.0], [3.0, 4.0]]])

corr2d_multi_in(X, K)
'''
tensor([[ 56., 72.],
[104., 120.]])
'''

多输出通道

增加多个卷积核即可

直观地说,我们可以将每个通道看作对不同特征的响应。而现实可能更为复杂一些,因为每个通道不是独立学习的,而是为了共同使用而优化的。因此,多输出通道并不仅是学习多个单通道的检测器。
用$c_i$和$c_o$分别表示输入和输出通道的数目,并让$k_h$和$k_w$为卷积核的高度和宽度。为了获得多个通道的输出,我们可以为每个输出通道创建一个形状为$c_i×k_h×k_w$的卷积核张量,这样卷积核的形状是$c_o×c_i×k_h×k_w$(我的理解是有$c_{o}$个卷积核)。在互相关运算中,每个输出通道先获取所有输入通道,再以对应该输出通道的卷积核计算出结果。

1
2
3
4
5
6
7
8
9
def corr2d_multi_in_out(X, K):
# 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
# 最后将所有结果都叠加在一起
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)

K = torch.stack((K, K + 1, K + 2), 0)
K.shape
# 执行互相关运算
corr2d_multi_in_out(X, K)

学习卷积核

可以通过仅查看“输入-输出”对来学习由X生成Y的卷积核。 我们先构造一个卷积层,并将其卷积核初始化为随机张量。接下来,在每次迭代中,我们比较Y与卷积层输出的平方误差,然后计算梯度来更新卷积核。为了简单起见,我们在此使用内置的二维卷积层,并忽略偏置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
X = torch.ones((6, 8))
X[:, 2:6] = 0

# 构造一个二维卷积层,它具有1个输出通道和形状为(1,2)的卷积核
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)

# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
# 其中批量大小和通道数都为1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
lr = 3e-2 # 学习率

for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
conv2d.zero_grad()
l.sum().backward()
# 迭代卷积核
conv2d.weight.data[:] -= lr * conv2d.weight.grad
if (i + 1) % 2 == 0:
print(f'epoch {i+1}, loss {l.sum():.3f}')

conv2d.weight.data.reshape((1, 2)) # tensor([[ 1.0010, -0.9739]])

学习到的权重接近想要的权重

1 x 1 卷积

通常用于调整网络层的通道数量和控制模型复杂性

因为使用了最小窗口,1×1卷积失去了卷积层的特有能力——在高度和宽度维度上,识别相邻元素间相互作用的能力。 其实1×1卷积的唯一计算发生在通道上。

下图展示了使用1×1卷积核与3个输入通道和2个输出通道的互相关计算。
image.png

这里输入和输出具有相同的高度和宽度,输出中的每个元素都是从输入图像中同一位置的元素的线性组合。 我们可以将1×1卷积层看作在每个像素位置应用的全连接层,以ci个输入值转换为co个输出值。 因为这仍然是一个卷积层,所以跨像素的权重是一致的。 同时,1×1卷积层需要的权重维度为co×ci,再额外加上一个偏置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def corr2d_multi_in_out_1x1(X, K):
c_i, h, w = X.shape
c_o = K.shape[0]
X = X.reshape((c_i, h * w))
K = K.reshape((c_o, c_i))
# 全连接层中的矩阵乘法
Y = torch.matmul(K, X)
return Y.reshape((c_o, h, w))

X = torch.normal(0, 1, (3, 3, 3))
K = torch.normal(0, 1, (2, 3, 1, 1))

Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)
assert float(torch.abs(Y1 - Y2).sum()) < 1e-6

观察3:池化(汇聚)层

假设三:下采样不影响图像的识别

通常当我们处理图像时,我们希望逐渐降低隐藏表示的空间分辨率、聚集信息,这样随着我们在神经网络中层叠的上升,每个神经元对其敏感的感受野(输入)就越大。

而我们的机器学习任务通常会跟全局图像的问题有关(例如,“图像是否包含一只猫呢?”),所以我们最后一层的神经元应该对整个输入的全局敏感。通过逐渐聚合信息,生成越来越粗糙的映射,最终实现学习全局表示的目标,同时将卷积图层的所有优势保留在中间层。

此外,当检测较底层的特征时,我们通常希望这些特征保持某种程度上的平移不变性。例如,如果我们拍摄黑白之间轮廓清晰的图像X,并将整个图像向右移动一个像素,即Z[i, j] = X[i, j + 1],则新图像Z的输出可能大不相同。而在现实中,随着拍摄角度的移动,任何物体几乎不可能发生在同一像素上。即使用三脚架拍摄一个静止的物体,由于快门的移动而引起的相机振动,可能会使所有物体左右移动一个像素(除了高端相机配备了特殊功能来解决这个问题)。

汇聚层具有双重目的:

  • 降低卷积层对位置的敏感性
  • 降低对空间降采样表示的敏感性

最大汇聚和平均汇聚

汇聚层运算符由一个固定形状的窗口组成,该窗口根据其步幅大小在输入的所有区域上滑动,为固定形状窗口(有时称为 汇聚窗口 )遍历的每个位置计算一个输出。然而,不同于卷积层中的输入与卷积核之间的互相关计算,汇聚层不包含参数
相反,池运算是确定性的,我们通常计算汇聚窗口中所有元素的最大值或平均值。
这些操作分别称为 _最大汇聚层_(maximum pooling)和 _平均汇聚层_(average pooling)。

image.png

回到开头提到的对象边缘检测示例,现在我们将使用卷积层的输出作为2×2最大汇聚的输入。 设置卷积层输入为X,汇聚层输出为Y。 无论X[i, j]X[i, j + 1]的值相同与否,或X[i, j + 1]X[i, j + 2]的值相同与否,汇聚层始终输出Y[i, j] = 1。 也就是说,使用2×2最大汇聚层,即使在高度或宽度上移动一个元素,卷积层仍然可以识别到模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import torch
from torch import nn
from d2l import torch as d2l

def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y[i, j] = X[i: i + p_h, j: j + p_w].max()
elif mode == 'avg':
Y[i, j] = X[i: i + p_h, j: j + p_w].mean()
return Y

X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
pool2d(X, (2, 2))

多通道

在处理多通道输入数据时,汇聚层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总。 这意味着汇聚层的输出通道数与输入通道数相同。

在实践中,通常将卷积和汇聚交替使用,可以先做几次卷积,再做一次汇聚

CNN 代码精读

1
git clone https://www.modelscope.cn/datasets/Datawhale/LeeDL-HW3-CNN.git

先看到Datawhale的介绍:
卷积神经网络是深度学习中的一个非常重要的分支,本作业提供了进行图像分类任务的基本范式。

  • 准备数据
  • 训练模型
  • 应用模型

要完成一个深度神经网络训练模型的代码,大概需要完成下面的内容:

  1. 导入所需要的库/工具包
  2. 数据准备与预处理
  3. 定义模型
  4. 定义损失函数和优化器等其他配置
  5. 训练模型
  6. 评估模型
  7. 进行预测
    此范式不仅适用于图像分类任务,对于广泛的深度学习任务也是适用的。

导入必要的库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 导入必要的库
import numpy as np
import pandas as pd
import torch
import os
import torch.nn as nn
import torchvision.transforms as transforms
from PIL import Image
# “ConcatDataset” 和 “Subset” 在进行半监督学习时可能是有用的。
from torch.utils.data import ConcatDataset, DataLoader, Subset, Dataset
from torchvision.datasets import DatasetFolder, VisionDataset
# 这个是用来显示进度条的。
from tqdm.auto import tqdm
import random

设置随机种子,配置CUDA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 设置随机种子以确保实验结果的可重复性
myseed = 6666

# 确保在使用CUDA时,卷积运算具有确定性,以增强实验结果的可重复性
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False

# 为numpy和pytorch设置随机种子
np.random.seed(myseed)
torch.manual_seed(myseed)

# 如果使用CUDA,为所有GPU设置随机种子
if torch.cuda.is_available():
torch.cuda.manual_seed_all(myseed)

torch.backends.cudnn.benchmark 是 PyTorch 深度学习框架中的一个参数,它与 CUDA 神经网络库(cuDNN)相关。cuDNN 是 NVIDIA 提供的一个 GPU 加速的深度神经网络库,它允许 PyTorch 利用 NVIDIA 的 GPU 进行快速的神经网络运算。

torch.backends.cudnn.benchmark 参数的作用是:

  • 当设置为 True 时,cuDNN 会尝试寻找最优的算法来执行每个操作,这可能会增加模型的初始化时间,但可以提高模型运行时的效率。
  • 当设置为 False 时,cuDNN 会使用一个默认的算法来执行操作,这可能会减少初始化时间,但可能不会达到最优的运行效率。

通常,在训练大型模型或进行多次迭代时,使用 torch.backends.cudnn.benchmark=True 可能会带来性能上的提升。然而,如果模型较小或者只运行一次,开启这个参数可能不会带来明显的好处,反而可能会因为初始化时间的增加而导致总体运行时间变长。

数据准备与预处理

数据准备包括从指定路径加载图像数据,并对其进行预处理。作业中对图像的预处理操作包括调整大小和将图像转换为Tensor格式。

Torchvision为图像预处理、数据增强和数据加载提供了一系列的API,这些API可以方便的实现图像预处理、数据增强和数据加载。  

具体不同的详细操作可以通过Pytorch的官方文档查看。

为了增强模型的鲁棒性,可以对训练集进行数据增强。相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 在测试和验证阶段,通常不需要图像增强。
# 我们所需要的只是调整PIL图像的大小并将其转换为Tensor。
test_tfm = transforms.Compose([
transforms.Resize((128, 128)),
transforms.ToTensor(),
])

# 不过,在测试阶段使用图像增强也是有可能的。
# 你可以使用train_tfm生成多种图像,然后使用集成方法进行测试。
train_tfm = transforms.Compose([
# 将图像调整为固定大小(高度和宽度均为128)
transforms.Resize((128, 128)),
# TODO:你可以在这里添加一些图像增强的操作。

# ToTensor()应该是所有变换中的最后一个。
transforms.ToTensor(),
])

数据集

数据通过名称进行标记,因此在调用’getitem’时我们同时加载图像和标签。  

定义了一个名为 FoodDataset 的类,继承自 Dataset,用于加载并预处理食品图像数据集,支持图像变换及从文件名中提取标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class FoodDataset(Dataset):
"""
用于加载食品图像数据集的类。

该类继承自Dataset,提供了对食品图像数据集的加载和预处理功能。
它可以自动从指定路径加载所有的jpg图像,并对这些图像应用给定的变换。
"""

def __init__(self, path, tfm=test_tfm, files=None):
"""
初始化FoodDataset实例。

参数:
- path: 图像数据所在的目录路径。
- tfm: 应用于图像的变换方法(默认为测试变换)。
- files: 可选参数,用于直接指定图像文件的路径列表(默认为None)。
"""
super(FoodDataset).__init__()
self.path = path
# 列出目录下所有jpg文件,并按顺序排序
self.files = sorted([os.path.join(path, x) for x in os.listdir(path) if x.endswith(".jpg")])
if files is not None:
self.files = files # 如果提供了文件列表,则使用该列表
self.transform = tfm # 图像变换方法

def __len__(self):
"""
返回数据集中图像的数量。

返回:
- 数据集中的图像数量。
"""
return len(self.files)

def __getitem__(self, idx):
"""
获取给定索引的图像及其标签。

参数:
- idx: 图像在数据集中的索引。

返回:
- im: 应用了变换后的图像。
- label: 图像对应的标签(如果可用)。
"""
fname = self.files[idx]
im = Image.open(fname)
im = self.transform(im) # 应用图像变换

# 尝试从文件名中提取标签
try:
label = int(fname.split("/")[-1].split("_")[0])
except:
label = -1 # 如果无法提取标签,则设置为-1(测试数据无标签)

return im, label

模型定义

这段代码定义了一个图像分类器类(Classifier),继承自PyTorch的nn.Module。该分类器通过一系列卷积层、批归一化层、激活函数和池化层构建卷积神经网络(CNN),用于提取图像特征。随后,这些特征被输入到全连接层进行分类,最终输出11个类别的概率,用于图像分类任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class Classifier(nn.Module):
"""
定义一个图像分类器类,继承自PyTorch的nn.Module。
该分类器包含卷积层和全连接层,用于对图像进行分类。
"""
def __init__(self):
"""
初始化函数,构建卷积神经网络的结构。
包含一系列的卷积层、批归一化层、激活函数和池化层。
"""
super(Classifier, self).__init__()
# 定义卷积神经网络的序列结构
self.cnn = nn.Sequential(
nn.Conv2d(3, 64, 3, 1, 1), # 输入通道3,输出通道64,卷积核大小3,步长1,填充1
nn.BatchNorm2d(64), # 批归一化,作用于64个通道
nn.ReLU(), # ReLU激活函数
nn.MaxPool2d(2, 2, 0), # 最大池化,池化窗口大小2,步长2,填充0

nn.Conv2d(64, 128, 3, 1, 1), # 输入通道64,输出通道128,卷积核大小3,步长1,填充1
nn.BatchNorm2d(128), # 批归一化,作用于128个通道
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # 最大池化,池化窗口大小2,步长2,填充0

nn.Conv2d(128, 256, 3, 1, 1), # 输入通道128,输出通道256,卷积核大小3,步长1,填充1
nn.BatchNorm2d(256), # 批归一化,作用于256个通道
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # 最大池化,池化窗口大小2,步长2,填充0

nn.Conv2d(256, 512, 3, 1, 1), # 输入通道256,输出通道512,卷积核大小3,步长1,填充1
nn.BatchNorm2d(512), # 批归一化,作用于512个通道
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # 最大池化,池化窗口大小2,步长2,填充0

nn.Conv2d(512, 512, 3, 1, 1), # 输入通道512,输出通道512,卷积核大小3,步长1,填充1
nn.BatchNorm2d(512), # 批归一化,作用于512个通道
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # 最大池化,池化窗口大小2,步长2,填充0
)
# 定义全连接神经网络的序列结构
self.fc = nn.Sequential(
nn.Linear(512*4*4, 1024), # 输入大小512*4*4,输出大小1024
nn.ReLU(),
nn.Linear(1024, 512), # 输入大小1024,输出大小512
nn.ReLU(),
nn.Linear(512, 11) # 输入大小512,输出大小11,最终输出11个类别的概率
)

def forward(self, x):
"""
前向传播函数,对输入进行处理。

参数:
x -- 输入的图像数据,形状为(batch_size, 3, 128, 128)

返回:
输出的分类结果,形状为(batch_size, 11)
"""
out = self.cnn(x) # 通过卷积神经网络处理输入
out = out.view(out.size()[0], -1) # 展平输出,以适配全连接层的输入要求
return self.fc(out) # 通过全连接神经网络得到最终输出

定义损失函数和优化器等其他配置

这段代码实现了图像分类模型的初始化和训练配置,目的是准备好训练环境和参数。它选择合适的设备(GPU或CPU),设置模型、批量大小、训练轮数、提前停止策略,定义了损失函数和优化器,为后续的模型训练奠定了基础。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 根据GPU是否可用选择设备类型
device = "cuda" if torch.cuda.is_available() else "cpu"

# 初始化模型,并将其放置在指定的设备上
model = Classifier().to(device)

# 定义批量大小
batch_size = 64

# 定义训练轮数
n_epochs = 8

# 如果在'patience'轮中没有改进,则提前停止
patience = 5

# 对于分类任务,我们使用交叉熵作为性能衡量标准
criterion = nn.CrossEntropyLoss()

# 初始化优化器,您可以自行调整一些超参数,如学习率
optimizer = torch.optim.Adam(model.parameters(), lr=0.0003, weight_decay=1e-5)

加载数据

1
2
3
4
5
6
7
8
9
10
# 构建训练和验证数据集
# "loader" 参数定义了torchvision如何读取数据
train_set = FoodDataset("./hw3_data/train", tfm=train_tfm)
# 创建训练数据加载器,设置批量大小、是否打乱数据顺序、是否使用多线程加载以及是否固定内存地址
train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True, num_workers=0, pin_memory=True)
# 构建验证数据集
# "loader" 参数定义了torchvision如何读取数据
valid_set = FoodDataset("./hw3_data/valid", tfm=test_tfm)
# 创建验证数据加载器,设置批量大小、是否打乱数据顺序、是否使用多线程加载以及是否固定内存地址
valid_loader = DataLoader(valid_set, batch_size=batch_size, shuffle=True, num_workers=0, pin_memory=True)

训练与验证模型

这段代码实现了一个图像分类模型的训练和验证循环,目的是通过多轮训练(epochs)逐步优化模型的参数,以提高其在验证集上的性能,并保存效果最好的模型。训练阶段通过前向传播、计算损失、反向传播和参数更新来优化模型,验证阶段评估模型在未见过的数据上的表现。如果验证集的准确率超过了之前的最好成绩,保存当前模型,并在连续多轮验证性能未提升时提前停止训练。

训练完成后,需要在测试集上评估模型的性能。通过计算准确率来衡量模型在测试集上的表现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# 初始化追踪器,这些不是参数,不应该被更改
stale = 0
best_acc = 0

for epoch in range(n_epochs):
# ---------- 训练阶段 ----------
# 确保模型处于训练模式
model.train()

# 这些用于记录训练过程中的信息
train_loss = []
train_accs = []

for batch in tqdm(train_loader):
# 每个批次包含图像数据及其对应的标签
imgs, labels = batch
# imgs = imgs.half()
# print(imgs.shape,labels.shape)

# 前向传播数据。(确保数据和模型位于同一设备上)
logits = model(imgs.to(device))

# 计算交叉熵损失。
# 在计算交叉熵之前不需要应用softmax,因为它会自动完成。
loss = criterion(logits, labels.to(device))

# 清除上一步中参数中存储的梯度
optimizer.zero_grad()

# 计算参数的梯度
loss.backward()

# 为了稳定训练,限制梯度范数
grad_norm = nn.utils.clip_grad_norm_(model.parameters(), max_norm=10)

# 使用计算出的梯度更新参数
optimizer.step()

# 计算当前批次的准确率
acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()

# 记录损失和准确率
train_loss.append(loss.item())
train_accs.append(acc)

train_loss = sum(train_loss) / len(train_loss)
train_acc = sum(train_accs) / len(train_accs)

# 打印信息
print(f"[ 训练 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {train_loss:.5f}, acc = {train_acc:.5f}")

# ---------- 验证阶段 ----------
# 确保模型处于评估模式,以便某些模块如dropout能够正常工作
model.eval()

# 这些用于记录验证过程中的信息
valid_loss = []
valid_accs = []

# 按批次迭代验证集
for batch in tqdm(valid_loader):
# 每个批次包含图像数据及其对应的标签
imgs, labels = batch
# imgs = imgs.half()

# 我们在验证阶段不需要梯度。
# 使用 torch.no_grad() 加速前向传播过程。
with torch.no_grad():
logits = model(imgs.to(device))

# 我们仍然可以计算损失(但不计算梯度)。
loss = criterion(logits, labels.to(device))

# 计算当前批次的准确率
acc = (logits.argmax(dim=-1) == labels.to(device)).float().mean()

# 记录损失和准确率
valid_loss.append(loss.item())
valid_accs.append(acc)
# break

# 整个验证集的平均损失和准确率是所记录值的平均
valid_loss = sum(valid_loss) / len(valid_loss)
valid_acc = sum(valid_accs) / len(valid_accs)

# 打印信息
print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")

# 更新日志
if valid_acc > best_acc:
with open(f"./{_exp_name}_log.txt", "a"):
print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f} -> 最佳")
else:
with open(f"./{_exp_name}_log.txt", "a"):
print(f"[ 验证 | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")

# 保存模型
if valid_acc > best_acc:
print(f"在第 {epoch} 轮找到最佳模型,正在保存模型")
torch.save(model.state_dict(), f"{_exp_name}_best.ckpt") # 只保存最佳模型以防止输出内存超出错误
best_acc = valid_acc
stale = 0
else:
stale += 1
if stale > patience:
print(f"连续 {patience} 轮没有改进,提前停止")
break

进行预测

最后的代码构建一个测试数据集和数据加载器,以便高效地读取数据。实例化并加载预训练的分类器模型,并将其设置为评估模式。在不计算梯度的情况下,遍历测试数据,使用模型进行预测,并将预测标签存储在列表中。将预测结果与测试集的ID生成一个DataFrame,并将其保存为submission.csv文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 构建测试数据集
# "loader"参数指定了torchvision如何读取数据
test_set = FoodDataset("./hw3_data/test", tfm=test_tfm)
# 创建测试数据加载器,批量大小为batch_size,不打乱数据顺序,不使用多线程,启用pin_memory以提高数据加载效率
test_loader = DataLoader(test_set, batch_size=batch_size, shuffle=False, num_workers=0, pin_memory=True)

# 实例化分类器模型,并将其转移到指定的设备上
model_best = Classifier().to(device)

# 加载模型的最优状态字典
model_best.load_state_dict(torch.load(f"{_exp_name}_best.ckpt"))

# 将模型设置为评估模式
model_best.eval()

# 初始化一个空列表,用于存储所有预测标签
prediction = []

# 使用torch.no_grad()上下文管理器,禁用梯度计算
with torch.no_grad():
# 遍历测试数据加载器
for data, _ in tqdm(test_loader):
# 将数据转移到指定设备上,并获得模型的预测结果
test_pred = model_best(data.to(device))
# 选择具有最高分数的类别作为预测标签
test_label = np.argmax(test_pred.cpu().data.numpy(), axis=1)
# 将预测标签添加到结果列表中
prediction += test_label.squeeze().tolist()

# 创建测试csv文件
def pad4(i):
"""
将输入数字i转换为长度为4的字符串,如果长度不足4,则在前面补0。
:param i: 需要转换的数字
:return: 补0后的字符串
"""
return "0" * (4 - len(str(i))) + str(i)

# 创建一个空的DataFrame对象
df = pd.DataFrame()
# 使用列表推导式生成Id列,列表长度等于测试集的长度
df["Id"] = [pad4(i) for i in range(len(test_set))]
# 将预测结果赋值给Category列
df["Category"] = prediction
# 将DataFrame对象保存为submission.csv文件,不保存索引
df.to_csv("submission.csv", index=False)
0%