Jack He's Blog

some idea or work

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);

KNN-OOD

Out-of-Distribution Detection with Deep Nearest Neighbors-腾讯云开发者社区-腾讯云

简单地使用从标准分类模型的特征嵌入导出的最近邻距离是不高效的

为了检测 OOD 样本,我们计算测试输入的嵌入与训练集的嵌入之间的第 k 个最近邻 (KNN) 距离,并使用基于阈值的标准来确定输入是否为 OOD。

KNN 提供了令人信服的优势:
(1)无分布假设
(2)与 OOD 无关(即,仅根据 ID 数据估计距离阈值,并且不依赖于未知数据的信息)
(3)容易使用(即不需要计算协方差矩阵的逆矩阵,因为协方差矩阵在数值上可能不稳定)
(4)与模型无关(即测试过程适用于不同的模型架构和训练损失)

紧凑且标准化的特征空间是 OOD 检测的最近邻方法成功的关键
基于 KNN 的 OOD 检测可以拒绝相当于贝叶斯最优估计器的输入

理论

基于距离的方法利用从模型中提取的特征嵌入,并在测试 OOD 样本距离 ID 数据相对较远的假设下进行操作。
image.png

之前基于距离的 OOD 检测方法采用参数密度估计,并将特征嵌入空间建模为多元高斯分布的混合(Lee et al., 2018)。 然而,这种方法对学习的特征空间做出了很强的分布假设,这可能不一定成立。

基于 KNN 的 OOD 检测器可以拒绝相当于估计的贝叶斯二元决策函数的输入。 较小的 KNN 距离 rk(zi) 直接转化为较高的 ID 概率,反之亦然。

检测OOD

具体来说,我们计算每个测试图像的嵌入与训练集之间的第 k 个最近邻距离,并使用一个简单的基于阈值的标准来确定输入是否 OOD。

我们使用归一化倒数第二个特征 $z = φ(x)/ \lvert \lvert φ(x) \rvert \rvert_{2}$ 进行 OOD 检测,其中 $φ : X → R^m$ 是特征编码器。

将训练数据的嵌入集表示为 $Z_n = (z_1, z_2, …, z_n)$。 在测试过程中,我们推导测试样本 $x^*$ 的归一化特征向量 $z^*$,并计算相对于嵌入向量 $z_i ∈ Z_n$ 的欧几里得距离 $\lvert \lvert z_i -z^* \rvert \rvert_{2}$。

对集合 $Z_{n}$ 按距离 $\parallel z_{i} - z^* \parallel_{2}$ 递增重新排序,得到序列 $Z’_{n} = (z_{(1)}, z_{(2)}, …, z_{(n)})$。OOD 检测决策函数定义为:

$$
G(z^*;k) = \mathbf{1}{-r_{k}(z^*)}
$$

其中 $r_{k}(z^*) = \parallel z^* - z_{(k)} \parallel_{2}$ 是到第 $k$ 近邻的距离,$\mathbf{1}{\cdot}$ 为指示函数。通常选择阈值 $\lambda$ 使 95% ID 数据正确分类,该阈值独立于 OOD 数据。

实验结果

实验指标

(1) ID样本真阳性率为95%时OOD样本的假阳性率(FPR95)
(2) 受试者工作特征曲线下面积(AUROC)
(3) ID 分类精度 (ID ACC)
(4) 每幅图像的推理时间(以毫秒为单位,测试图像的平均值)。

训练损失

在我们的实验中,我们的目的是证明基于 KNN 的 OOD 检测与训练过程无关,并且与在不同损失下训练的模型兼容。 我们考虑两种类型的损失函数,分别有和没有对比学习。
我们采用
(1)交叉熵损失,这是分类中最常用的训练目标
(2)监督对比学习(SupCon)(Khosla 等人,2020)——表示学习的最新发展,它利用了标签 通过在嵌入空间中对齐属于同一类的样本来获取信息

实验细节

我们使用 ResNet-18 作为 CIFAR-10 的骨干网。 遵循 Khosla 等人的原始设置,具有 SupCon 损失的模型训练了 500 个 epoch,批量大小为 1024。温度 τ 为 0.1。
我们执行最近邻搜索的倒数第二个特征的维度是 512。投影头的维度是 128。我们使用从 0.5 开始的余弦退火学习率(Loshchilov & Hutter,2016)。

我们对 CIFAR-10 使用 k = 50,对 CIFAR-100 使用 k = 200,使用验证方法从 k = {1, 10, 20, 50, 100, 200, 500, 1000, 3000, 5000} 中选择 (Hendrycks 等人,2019)。 我们使用动量为 0.9、权重衰减为 10−4 的随机梯度下降来训练模型。 没有对比学习的模型训练了 100 个 epoch。 起始学习率为 0.1,在第 50、75 和 90 时期分别衰减 10 倍。

对比学习的帮助

 (1) 我们使用 UMAP (McInnes et al., 2018) 可视化图 1 中学习到的特征嵌入,其中颜色编码不同的类标签。 一个显着的观察结果是,SupCon 的表示比从 CE 损失获得的表示更具可区分性和紧凑性。 高质量的嵌入空间确实为基于 KNN 的 OOD 检测带来了好处。
 (2) 除了可视化之外,我们还使用 SupCon 与 CE 训练的嵌入来定量比较基于 KNN 的 OOD 检测的性能。 如表 1 所示,与使用经过 CE 损失训练的模型的嵌入相比,具有对比学习表示的 KNN+ 降低了所有测试 OOD 数据集上的 FPR95。

关于k

我们注意到几个有趣的观察结果:(1)最佳 OOD 检测(由 FPR95 测量)在不同的随机采样率 α 下保持相似。 (2) 最优 k 与我们的验证策略选择的一致。 例如,当α=100%时,最优k为1000; 当α = 1%时,最优k变为10。 (3) 当 k 相对较小(例如 k < 1000)时,改变 k 不会显着影响推理速度,如图 3 (a) 所示。

image.png

特征归一化

在本次消融中,我们对比了使用和不使用特征归一化的基于 KNN 的 OOD 检测的性能。 第 $k$ 个 N 距离可以分别通过 $r_{k}( \frac{φ(x)}{\lvert φ(x) \rvert })$ 和 $r_k(φ(x))$ 导出。如图 3 (b) 所示,使用特征归一化改进了与没有归一化的情况相比,FPR95 大幅提高了 61.05% 为了更好地理解这一点,我们研究了两个向量 $u$ 和 $v$ 之间的欧几里得距离 $r = \lvert \lvert u − v \rvert \rvert_{2}$ 。特征向量 $u$ 和 $v$ 的范数可以显着提高。

有趣的是,最近的研究分享了图4(a)中的观察结果,即 ID 数据具有比 OOD 数据更大的 L2 特征范数 (Tack et al., 2020; Huang et al., 2021)。 因此,ID 特征之间的欧几里德距离可能很大(图 4 (b),这与 ID 数据具有比 OOD 数据更小的 k-NN 距离的希望相矛盾。事实上,归一化有效地缓解了这个问题,如图所示。如图4(c)所示,归一化对于最近邻方法在 OOD 检测中的成功起着关键作用,如图3(b)所示。

image.png
image.png

使用倒数第二层的特征比使用投影头更好

在本文中,我们遵循SSD+的惯例,使用倒数第二层的特征来代替投影头。我们还在图3(C)中验证了在所有测试OOD数据集上使用倒数第二层的功能比使用投影头要好。这可能是因为倒数第二层比投影头保留了更多的信息,后者的尺寸要小得多。

image.png

目前存在的蜉蝣生物检测问题

  1. 表示能力不足
    1. 海洋中浮游生物的巨大多样性和丰富性不可避免地导致一些类群之间难以区分的相似性
    2. 不同的光学成像仪器受到自身物理和方法的限制,所获取的图像在分辨率、对比度、噪声、模糊、色彩、动态范围等方面具有不同的质量和限制
    3. 原位 2D 成像还伴随着表示 3D 目标的多种固有缺陷,例如不完整、遮挡和视角模糊等
  2. 数据分布漂移DSS
    1. 测试集与训练集的分布不一致
  3. OOD问题
  4. 数据分布不均匀
    1. 少样本问题
    2. 难以识别少数类群

标准

FPR95

14cd3637ef71ef8f0f1d908910f3544.jpg|300

FPR (False Positive Rate) 和 TPR (True Positive Rate) 是机器学习和统计学中用于评估分类器性能的重要指标。FPR(TPR@95%) 是指当分类器的 TPR(真正例率)达到 95% 时,对应的 FPR(假正例率) 值。

TPR增大的时候,会看到这个边界会往右走,而且我们希望边界还可以往下走(减少假阳性样本)

各术语含义:

  1. TPR (True Positive Rate)
    表示分类器正确识别正例的比例,公式为:
    $$\text{TPR} = \frac{\text{TP}}{\text{TP} + \text{FN}}$$
    其中,TP 是正确预测为正的样本数量,FN 是实际为正但被预测为负的样本数量。
  2. FPR (False Positive Rate)
    表示分类器错误将负例预测为正例的比例,公式为:
    $$\text{FPR} = \frac{\text{FP}}{\text{FP} + \text{TN}}$$
    其中,FP 是实际为负但被预测为正的样本数量,TN 是正确预测为负的样本数量。
  3. FPR@95%
    意味着在模型调整到能够达到 95% TPR 的点时(即能正确识别 95% 的正例),我们观察此时模型的 FPR。

FPR(TPR@95%) 的意义

  • 它衡量在模型保证高 TPR(真正例率)的情况下,对负例的误分类情况(假正例率)有多高。
  • 低 FPR(TPR@95%) 表明分类器在保持高真正例率时,对负例的误分类也很少,性能较好。

AUROC

AUPR

在评估分类模型(尤其是处理不平衡数据或分布外检测任务)时,AUPR(Area Under the Precision-Recall Curve)和FPR@95(False Positive Rate at 95% True Positive Rate)是两个常用的指标,但它们的设计目的和关注点存在显著差异。以下是详细分析:

1. 指标定义与核心目标

(1) AUPR(精确率-召回率曲线下面积)

  • 定义:通过不同分类阈值下的精确率(Precision)和召回率(Recall)绘制曲线,计算其下方的面积。
  • 目标
    • 衡量模型在正类样本(如异常/OOD样本)上的综合性能,尤其是在类别极度不平衡时(正样本远少于负样本)。
    • 反映模型对精确率(避免误报)和召回率(避免漏检)的平衡能力

(2) FPR@95(在召回率95%时的假阳性率)

  • 定义:当模型的召回率(True Positive Rate, TPR)被设定为95%时,对应的假阳性率(False Positive Rate, FPR)。
    • 例如:在OOD检测中,当模型检测到95%的OOD样本时,误将多少ID样本判定为OOD。
  • 目标
    • 衡量模型在确保高召回率(如不漏检关键样本)时,对负类样本的误判程度
    • 直接服务于高召回率优先的场景(如医疗诊断、自动驾驶中的安全关键任务)。

2. 核心区别

维度 AUPR FPR@95
关注点 正类(如OOD)的精确率与召回率的整体平衡 在高召回率(TPR=95%)下对负类的误判控制
数据敏感性 对类别不平衡敏感(适合正样本少的场景) 依赖阈值选择(关注单一操作点)
优化方向 综合提升精确率和召回率 在高召回率下最小化假阳性率
应用场景 数据不平衡、需全面评估正类性能 安全关键领域,要求严格避免漏检

3. 为什么两者的目的不一致?

(1) 问题本质不同

  • AUPR
    • 反映模型在所有可能阈值下对正类的识别能力,强调整体性能
    • 例如:在OOD检测中,若大部分OOD样本得分明显低于ID样本,AUPR会较高。
  • FPR@95
    • 仅关注特定阈值(TPR=95%)下的负类误判率,服务于特定需求(如“宁可误报,不可漏报”)。
    • 例如:在自动驾驶中,必须检测到95%的未知障碍物,同时控制误判已知物体的频率。

(2) 对类别不平衡的响应不同

  • AUPR
    • 在正样本极少时,PR曲线对模型性能变化更敏感,优于AUROC。
    • 例如:若正样本占比1%,AUPR=0.8表明模型在极端不平衡下仍表现良好。
  • FPR@95
    • 不直接受类别分布影响,但实际应用中若负样本(如ID数据)远多于正样本,FPR@95的微小上升可能导致大量误判。
    • 例如:FPR@95=5%时,若测试集中有100万个ID样本,误判数将高达5万。

(3) 优化目标的冲突

  • 提高AUPR:需要同时提升精确率和召回率,可能需要对分类边界进行全面优化。
  • 降低FPR@95:只需在高召回率区域(TPR≥95%)优化假阳性率,可能与全局性能无关。
    • 例如:一个模型可能在TPR=95%时表现极佳(FPR@95低),但在其他阈值下性能波动(导致AUPR一般)。

4. 实际场景中的典型关系

(1) 理想情况

若模型在所有阈值下均表现良好,AUPR和FPR@95可能同步优化。例如:

  • 完美分离ID和OOD样本时,AUPR=1.0,FPR@95=0%。

(2) 常见情况

两者常呈现权衡关系

  • 高AUPR但高FPR@95:模型整体区分能力强,但在高召回率区域(如TPR=95%)误判较多。
    • 例如:OOD检测模型对明显异常的样本敏感,但对边界样本(接近ID分布)误判率高。
  • 低FPR@95但低AUPR:模型在高召回率区域严格控制误判,但整体性能较差。
    • 例如:保守模型仅对极端OOD样本报警,漏检大量边界OOD样本,导致AUPR低。

5. 如何选择指标?

(1) 优先AUPR的场景

  • 数据极度不平衡(如OOD样本占比<1%)。
  • 需综合评估正类性能(如既要减少漏检,又要避免误报)。
  • 示例:金融欺诈检测,需平衡欺诈交易的召回率和正常交易的误拦截率。

(2) 优先FPR@95的场景

  • 安全关键任务(如医疗诊断、自动驾驶),漏检后果严重。
  • 明确要求高召回率(如“必须检测到95%的异常”)。
  • 示例:癌症筛查模型,要求尽可能检测所有疑似病例(TPR=95%),同时控制健康人的误诊率(FPR@95)。

6. 总结

  • AUPR和FPR@95的目的不一致
    • AUPR关注正类识别的全局平衡,FPR@95关注高召回率下的负类误判控制。
  • 实际应用中需根据需求选择
    • 若需全面评估模型对正类的识别能力,优先AUPR。
    • 若需保证高召回率下的可靠性,优先FPR@95。
  • 两者结合使用
    • 例如:在自动驾驶中,可同时要求AUPR>0.9(整体性能强)且FPR@95<5%(高召回率下误判可控)。

OOD

ID与OOD

ID指的是in-distribution数据,也就是我们熟悉的训练数据;OOD指的是out-of-distribution,在不同的领域也可能被叫做outlier或者是anomaly data,说的是与ID分布不一致的数据。

其实ID和OOD的界定比较模糊,通常我们是将语意信息相差较大的两个数据集构成ID和OOD。例如,我们在CIFAR-10上训练好了一个图像分类网络,那么对于这个网络来讲,CIFAR-10数据集的图像就是ID数据,而MNIST,或者是SVHN,以及LSUN等数据集就可以看做是OOD。

[!question] 通常一个比较难以回答的问题就是,在CIFAR-100上训练好的网络,那么CIFAR-10对于网络来说是OOD吗?因为二者相似性很高。
这个实际上由不同的观点,一部分人认为能识别来自与ID不同的类别就叫OOD;但另一部分人认为,应该选择协方差偏移的样本才叫OOD。
我们构造验证试验的时候,还是需要尽量选取语义信息具有差异性的两个数据集构成ID与OOD。

分布外检测在现实世界中部署机器模型时,可靠的分类器不仅应该准确地对已知的分布内 (ID) 样本进行分类,而且还应将任何 OOD 输入识别为“未知”。
这可以通过 OOD 检测器与分类模型 $f$ 配合来实现。 OOD 检测可以表述为二元分类问题。 在测试时,OOD 检测的目标是确定样本 $x ∈ X$ 是否来自 $P_{in} (ID)$ 或不是 (OOD)。 可以通过水平集估计做出决定:
image.png

OOD与常规问题的区别

传统模式识别系统假设:

  • 封闭世界假设
  • 训练测试域数据是独立同分布的(I.I.D)
  • 各类别数据量充足,不会出现不均衡问题

OOD问题与上面的假设均相反

与其他领域的一些区别

  1. Open Set Recognition(OSR)
    1. 不仅要求能够检测未知类别,还要求正确分类已知的类别。
    2. 评价标准:AUROC,AUPR,CCR@FPRx
  2. Out-of-Distribution Detection(OOD)
    1. 保证ID类测试样本的分类性能,拒绝OOD测试样本,ID样本往往具有多个类别,OOD的类别不能与ID的类别重合。
    2. 经常使用CIFAR-10作为ID样本进行训练,其他数据集如SVHN等作为OOD测试样本。
    3. 评价标准:AUROC,AUPR,or F-scores,FPR@TPRx,TNR@TPRx
  3. 和OSR的区别:
    1. OSR常常用一个数据集,一部分类别作为ID一部分作为OOD。而OOD则是一个数据集作为ID样本,再用别的数据集作为OOD样本(仍要保证类别不重合)。
    2. OOD检测方法的范围更广,如多标签分类,解空间更大。

OOD检测的主流是Detecting semantic shift。当然在一些领域中,也十分的关注covariate shift,即要拒绝和原本的ID样本具有不同分布的样本,即使类别可能一样。如医学图像或者隐私敏感的领域,就不能让模型对具有不同分布的样本进行泛化。

1. 定义与核心目标

  • 异常检测(AD)
    旨在识别与“正常”数据显著偏离的样本,即使这些异常可能仍属于训练数据的分布(例如同一分布中的低概率事件)。例如:信用卡欺诈检测、工业设备故障发现等。
  • 分布外检测(OOD)
    专注于检测来自与训练数据完全不同的分布的数据。例如:在猫狗分类模型中,输入汽车的图像会被标记为OOD。

2. 数据假设

  • AD
    • 假设异常是同一分布中的罕见事件(如分布的“尾部”),或由相同生成机制中的异常参数导致(如设备故障)。
    • 训练数据通常仅包含正常样本(无监督学习)。
  • OOD
    • 假设测试数据来自与训练数据完全无关的分布(如不同领域、不同采集条件)。
    • 训练时可能仅使用分布内数据,但需模型在推理时拒绝分布外样本。

3. 应用场景

  • AD的典型场景
    • 网络安全(入侵检测)、医疗诊断(罕见病识别)、质量控制(缺陷产品检测)。
    • 核心需求:在已知环境中捕捉异常模式。
  • OOD的典型场景
    • 自动驾驶(识别未知障碍物)、医疗AI(拒绝非医学图像输入)、开放世界分类(避免对未知类别过度自信)。
    • 核心需求:防止模型对不相关数据做出高置信度预测。

4. 方法与技术

  • AD常用方法
    • 统计方法(如高斯混合模型、马氏距离)。
    • 重建误差(自编码器、生成对抗网络)。
    • 单类分类(如单类SVM、孤立森林)。
  • OOD常用方法
    • 基于置信度(如Softmax阈值、蒙特卡洛Dropout)。
    • 特征空间分析(Mahalanobis距离、能量模型)。
    • 对抗训练或合成OOD样本(如ODIN、Outlier Exposure)。

5. 关键区别总结

维度 异常检测(AD) 分布外检测(OOD)
数据来源 同一分布中的异常(低概率事件) 完全不同的数据分布
问题焦点 应用层面的异常(如故障、欺诈) 数据分布层面的差异(跨域、跨类别)
训练假设 通常仅需正常数据(无监督) 可能需要分布内数据的表征学习
典型方法 重建误差、单类分类 置信度校准、特征空间度量

6. 重叠与联系

  • 交叉场景:某些情况下,OOD样本可能被视作AD中的异常(如模型未见过的类别)。
  • 技术共享:部分方法(如自编码器)可同时用于AD和OOD检测,但优化目标不同。

总结

  • AD 更关注在已知环境(同一分布)中发现异常,强调应用层面的异常模式。
  • OOD 更关注模型泛化性,强调识别与训练数据分布无关的样本,确保模型在开放环境中的安全性。
  • 两者相辅相成,实际应用中可能结合使用(如先检测OOD样本,再在分布内进行AD)。

常规数据集

首先我们介绍OOD样本,OOD样本的偏移包括两类,第一类是semantic shift (语义偏移),即OOD样本是来自于和ID(In-distribution)不同的类别(例如plankton 50 和后面的40个类别)。另一类是covariate shift(协方差偏移),即OOD样本来自于和ID样本不同的domain(如CIFAR-10和Imagenet)。在OOD Survey综述中,所讨论的shift,主要是semantic shift,即OOD样本和ID样本具有不同的类别。

在OOD研究中,常用的数据集主要是一些小规模数据,超大规模数据集目前还没有在研究中大范围的看到,这里介绍一下经常使用的几个并说明实验的构造方法。

MNIST,Fashion- MNIST,Omniglot:这是三个单通道的灰度数据集。MNIST与Fashion-MNIST大家都比较熟悉,一个是手写数字,一个是服装图像。Omniglot是一个在小样本学习中经常使用的数据集,它里面包含了1623个类别,每个类别仅有20个样本。在OOD检测中,通常是使用MNIST作为ID数据,将Fashion-MNIST与Omniglot的混合作为OOD数据;或者是将Fashion- MNIST作为ID数据,将MNIST与Omniglot的混合作为OOD数据。由于Omniglot并不适合训练常规的分类网络,因此通常不作为ID数据。

CIFAR-10,CIFAR-100,TinyImageNet,LSUN,SVHN:这是5个RGB三通道的彩色数据集。CIFAR-10与CIFAR-100大家应该都比较熟悉,在分类网络的评测中经常看到它们。TinyImageNet是类似于ImageNet数据集的一个规模较小的数据集,它里面包含有200个种类的数据。LSUN是一个大规模的场景识别数据集,包含厨房、客厅、卧室等等之类的图像。SVHN是街景数字识别,主要是一些门牌号上的数字。这些数据集的下载链接都比较容易找到,我把它们列在文章的最后。在OOD实验设计中,通常是有以下几种设计方法,当然这些组合方案并不绝对,只是为了方便与大家常用的组合进行比较:

  • CIFAR-10作为ID数据,SVHN、LSUN、TinyImageNet作为OOD数据
  • CIFAR-100作为ID数据,SVHN、LSUN、TinyImageNet作为OOD数据
  • SVHN作为ID数据,LSUN、CIFAR-10、TinyImageNet作为OOD数据

因为需要在ID数据上训练分类网络,因此,在设计实验时,通常会选择不太复杂的数据集作为ID,而将其他数据作为OOD数据。

在OOD检测的训练中,我们通常约定不可以使用任何形式的OOD数据,我们能接触到的只有ID数据,这也是符合OOD研究的目的的。在实际使用中,网络不可能会见到所有的OOD图像,因此,如果在网络接触到的OOD数据上评测它的拒识能力是没有意义的。在一些方法中,作者使用了OOD数据来finetune网络,这样得到的指标结果是偏高的,并且说服力不强。

OOD方法总结

Jingkang50/OODSurvey: The Official Repository for “Generalized OOD Detection: A Survey”

Generalized Out-of-Distribution Detection: A Survey

image.png
image.png

1. Classification-based Methods

简而言之,就是利用maximum softmax probability作为判断ID样本的分数。早期的OOD检测方法侧重于基于神经网络的输出来获得更好的OOD评分。

1.1 Output-based Methods
a. Post-hoc Detection:事后训练方法的优点是易于使用,无需修改训练过程和目标。研究工作包括ODIN score,energy score。
b. Confidence Enhancement Methods:训练神经网络在分布内和分布外的数据之间产生高度可区分的置信度分数。
c. Outlier Exposure:OOD检测方法的另一个分支是在训练过程中使用一组收集的OOD样本,或称“离群值”,以帮助模型学习ID/OOD差异。

1.2 Label Space Redesign
one-hot编码通常用于对分类信息进行编码。然而,one-hot编码忽略了标签之间的内在关系。例如,狗和猫之间的距离与狗和汽车之间有相同的距离是不合理的。为此,一些工作试图使用标签空间中的信息来进行OOD检测。

在测试时,将与来自不同头部的所有embedding vectors距离最小的label作为预测。

1.3 OOD Data Generation
当没有OOD样本可用时,一些方法尝试合成OOD样本以实现ID/OOD可分离性。现在的方法有利用GAN来进行操作的。

1.4 Gradient-based Methods
依赖于梯度值的OOD检测方法也是一个比较有趣的方向。ODIN方法(Enhancing The Reliability of Out-of-distribution Image Detection in Neural Networks)首先使用了梯度信息用于OOD检测,通过对输入增加一些小的扰动,检测梯度的变化。在输入增加的扰动,可以增强模型的预测置信度,最终在ID和OOD样本输入的softmax分数之间产生更大的差距,帮助判断样本是否为OOD样本。类似使用梯度值方法的还有(On the Importance of Gradients for Detecting Distributional Shifts in the Wild),该方法显式的从梯度空间推导评分函数。

1.5 Bayesian Models
最具代表性的Bayesian神经网络方法(Bayesian Learning for Neural Networks)由于其方法预测的不准确性和高额的计算代价被遗弃。进一步的探索采用自然梯度变分推理,实现实用且负担得起的现代深度学习训练,同时保留贝叶斯原则的好处(Practical Deep Learning with Bayesian Principles)。类似的工作还有(Predictive Uncertainty Estimation via Prior Networks;Reverse KL-Divergence Training of Prior Networks: Improved Uncertainty and Adversarial Robustness;Towards Maximizing the Representation Gap between In-Domain & Out-of-Distribution Examples;Locally Most Powerful Bayesian Test for Out-of-Distribution Detection using Deep Generative Models)

1.6 Large-scale OOD Detection
这个方向也是近期比较火的一个方向——大规模的OOD检测。例如,(MOS: Towards Scaling Out-of-distribution Detection for Large Semantic Space)揭示了在 CIFAR 基准上开发的方法可能无法有效地转化为具有大语义空间的 ImageNet 基准,突出了在大规模现实世界设置中评估 OOD 检测的必要性。该方法将大规模语义空间解耦为具有相似概念的小组,有助于简化已知类和未知类之间的边界。(Exploring the Limits of Out-of-Distribution Detection;Pretrained Transformers Improve Out-of-Distribution Robustness;OODformer: Out-Of-Distribution Detection Transformer.)这些方法证明了,大规模的预训练transformer模型可以有效提高方法在OOD任务上的性能。

2. Density-based Methods

OOD检测中的基于密度的方法显式地用一些概率模型对分布内数据进行建模,并将低密度区域的测试数据标记为OOD。一些作品尝试使用似然比来解决问题(Likelihood Ratios for Out-of-Distribution Detection;Input complexity and out-of-distribution detection with likelihood-based generative models)发现似然对输入复杂度表现出强烈的偏差,并提出了一种基于似然比的方法来补偿输入复杂度的影响。相对来说还是Classification-based Methods表现更好。

3. Distance-based Methods

基于距离的方法的基本思想是,OOD样本应该相对远离分布内类的质心或原型。(A Simple Fix to Mahalanobis Distance for Improving Near-OOD Detection)方法通过将图片的前景和背景分离,然后计算Mahalanobis distance。与参数化方法相比,最近的工作(Out-of-distribution Detection with Deep Nearest Neighbors)显示出非参数最近邻距离用于 OOD 检测的强大前景。非参数的方法无需提前对特征空间进行分布的假设,因此更加的简单、灵活和通用。

还有一些方法使用余弦相似度来判断测试样本特征和类内特征的距离,从而判断是否是OOD样本(Hyperparameter-Free Out-of-Distribution Detection Using Cosine Similarity;A Boundary Based Out-of-Distribution Classifier for Generalized Zero-Shot Learning)。此外,其他工作利用输入特征和类质心之间的径向基函数核(Uncertainty Estimation Using a Single Deep Deterministic Neural Network)、欧氏距离(Feature Space Singularity for Out-of-Distribution Detection.)和geodesic distance(Igeood: An Information Geometry Approach to Out-of-Distribution Detection)的距离。

除了计算样本与类质心之间的距离外,在主空间的正交补空间中的特征范数对OOD检测也是有效的(ViM: Out-Of-Distribution with Virtual-logit Matching)。CIDER(CIDER: Exploiting Hyperspherical Embeddings for Out-of-Distribution Detection)引入了一种用于OOD检测的新的表示学习框架,增大了不同类质心的最大角距离,并让类内样本尽可能的靠近类质心。

4. Reconstruction-based Methods

该类方法的核心想法是,encoder-decoder架构对于类内样本和分布外样本往往产生不同的输出。编码器解码器模型的输出表现可以用来判断样本时ID样本还是OOD样本(通常模型生成得到的类内样本效果会更好,以此来判断)。例如,如果一个模型只使用ID data来进行训练,那么输入OOD data,他便无法进行很好的处理,这样就能判断样本是不是OOD样本了。

但很显然,这样的方法是pixel-level的,因此训练成本会比较高。为了解决该问题,(Rethinking Reconstruction Autoencoder-Based Out-of-Distribution Detection)方法尝试reconstruct with hidden features,即只生成隐式特征,不需要恢复原图。

本周的内容是使用CNN进行意图分类任务,关键是掌握怎么处理NLP问题,以及里面可能会遇到的一些小问题

task1&2 CNN进行意图分类

[!question] task1: 根据给定的代码与数据,参考课件内容,填充完整models.py的代码;

[!question] task2: 为关键代码添加注释

主函数

这里task1只需要填充相关的代码,不过如果要理解整个项目的话,最好从main.py开始看起:

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
108
109
# main.py

import torch
import argparse
import yaml
from torch.utils.data import DataLoader

from trainer import train_epoch, evaluate
from models import (
CNN,
CNNSeq
)
from data_utils import SLUDataset # 导入相关模块
import torch.nn as nn

intents_num = {'企业性质查询': 0,
'营业利润查询': 1,
'企业负债查询': 2,
'项目成交状况查询': 3,
'企业营业成本查询': 4,
'小区绿化率查询': 5,
'建筑密度查询': 6,
'小区成交均价查询': 7,
'营业总收入查询': 8,
'地块总价查询': 9,
'地块成交时间查询': 10,
'企业债务违约查询': 11,
'容积率查询': 12,
'企业风险查询': 13,
'地块归属查询': 14,
'项目开发商信息查询': 15
}

def main():
# 解析命令行参数
parser = argparse.ArgumentParser(description="SLU main entry")
parser.add_argument('--config', type=str, default='configs/config.yaml', help='Path to config file')
parser.add_argument('-m', '--model_type', type=str, default='cnn', choices=['cnn', 'cnn_seq'])
args = parser.parse_args()

# 读取args参数中的config路径对应的yaml文件
with open(args.config, 'r', encoding='utf-8') as f:
cfg = yaml.safe_load(f)

# 读取yaml文件中的参数
data_path = cfg['data_path']
model_name = cfg.get('model_name', 'bert-base-chinese') # 取model_name参数,如果没有则取bert-base-chinese
batch_size = cfg.get('batch_size', 16) # 取batch_size参数,如果没有则取16
epochs = cfg.get('epochs', 3) # 取epochs参数,如果没有则取3
lr = cfg.get('lr', 2e-5) # 取lr参数,如果没有则取2e-5
# 取其他参数,如果没有则取默认值
vocab_size = cfg.get('vocab_size', 30522) # 词表大小
embed_dim = cfg.get('embed_dim', 128) # 词向量维度

print("加载预处理后的数据:", data_path)
# 加载预处理后的数据
data = torch.load(data_path)
train_encodings = data['train']
test_encodings = data['test']

# 创建Dataset和DataLoader
train_dataset = SLUDataset(train_encodings)
test_dataset = SLUDataset(test_encodings)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

print(f"训练集: {len(train_dataset)}, 测试集: {len(test_dataset)}")

# 设置模型类型
print("选择模型类型:", args.model_type)

# 设置在cpu还是gpu上运行
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# 选择模型类型
if args.model_type == 'cnn':
model = CNN(
vocab_size=vocab_size,
embed_dim=embed_dim,
num_intent_labels=len(intents_num),
)
elif args.model_type == 'cnn_seq':
model = CNNSeq(
vocab_size=vocab_size,
embed_dim=embed_dim,
num_intent_labels=len(intents_num),
)

# 加载模型到device
model.to(device)

# 定义损失函数
intent_loss_fn = nn.CrossEntropyLoss()
# 优化器选择AdamW
optimizer = torch.optim.AdamW(model.parameters(), lr=float(lr))

# 训练模型
for epoch in range(epochs):
print(f"\nEpoch {epoch+1}/{epochs} - {args.model_type}")
train_epoch(model, train_loader, optimizer, intent_loss_fn, device)
evaluate(model, test_loader, device, intent_loss_fn, args.model_type)

# 保存模型
ckpt_path = f"checkpoints/{args.model_type}_model.pth"
torch.save(model.state_dict(), ckpt_path)
print(f"模型已保存到 {ckpt_path}")

if __name__ == "__main__":
main()

准备内容

  • intent_num 给出了分类,共16类
  • 创建了一个ArgumentParser对象parser,用于解析命令行参数
  • args是一个字典,存储parser解析的内容
  • cfg是按照args.config给出的路径读取的yaml参数(包括embed_dim, batch_size, lr, vocab_size等),存放在cfg字典里(其实更习惯命令行参数把这些参数全都传入)
  • 加载来自yaml的data_path的数据,这里用torch.load进行加载,因为数据已经在预处理过程中转化成pth文件

数据预处理

初始的数据是json,形式是这样的:

1
2
3
4
5
6
7
8
{
"intent": "小区绿化率查询+容积率查询",
"query": "请问在南京市鼓楼区和武汉市硚口区这两个地区,绿化率排名前五的地块中,各自的容积率最低的是多少?",
"tokens": [
'请', '问', '在', '南', '京', '市', '鼓', '楼', '区', '和', '武', '汉', '市', '硚', '口', '区', '这', '两', '个', '地', '区', ',', '绿', '化', '率', '排', '名', '前', '五', '的', '地', '块', '中', ',', '各', '自', '的', '容', '积', '率', '最', '低', '的', '是', '多', '少', '?'
],
"slots": "O O O B-city I-city I-city B-district I-district I-district O B-city I-city I-city O I-district I-district O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O"
}

[!question] 但是为什么main里面直接加载的是pth呢?是怎么从json文件处理成pth的?pth还能读出来’train’,’test’和’val’?

来看看预处理部分:

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
108
109
110
111
112
# data_preprocessing.py

import json
import torch
from transformers import BertTokenizerFast
import os

intents_num = {'企业性质查询': 0,
'营业利润查询': 1,
'企业负债查询': 2,
'项目成交状况查询': 3,
'企业营业成本查询': 4,
'小区绿化率查询': 5,
'建筑密度查询': 6,
'小区成交均价查询': 7,
'营业总收入查询': 8,
'地块总价查询': 9,
'地块成交时间查询': 10,
'企业债务违约查询': 11,
'容积率查询': 12,
'企业风险查询': 13,
'地块归属查询': 14,
'项目开发商信息查询': 15
}

def load_json(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f) # 加载json文件

processed_data = []
for idx, example in enumerate(data):
intent_str = example.get('intent', 'None') # 取出意图
tokens = example.get('tokens', []) # 取出tokens

# 处理意图,多个意图用“+”隔开,取第一个
intents = [intent.strip() for intent in intent_str.split('+')]
intent = intents[0]

# 处理processed_data
processed_data.append({
'tokens': tokens,
'intent': intent,
})
return processed_data

def encode_data(data, tokenizer, max_len=128):
input_ids = []
intent_labels = []

# 遍历json文件数据
for item in data:
tokens = item['tokens']
intent = item['intent']

# 编码tokens,使用BertTokenizerFast进行tokenize
encoding = tokenizer(
tokens,
is_split_into_words=True, # 输入已经分词
padding='max_length', # 填充到最大长度
truncation=True,
max_length=max_len,
return_offsets_mapping=True,
return_tensors='pt'
)
# 将token编码后的input_ids添加到input_ids
input_ids.append(encoding['input_ids'][0])

# 单标签意图编码
intent_id = intents_num.get(intent, -1) # 找到对应的id
intent_labels.append(torch.tensor(intent_id, dtype=torch.long)) # 添加到列表

return {
'input_ids': torch.stack(input_ids),
'intent_labels': torch.stack(intent_labels),
}

def main():
train_file = 'train.json'
val_file = 'val.json'
test_file = 'test.json'
save_path = 'data_preprocessed.pth'

if not os.path.exists(train_file) or not os.path.exists(test_file):
print("Error: train.json or test.json not found.")
return

# 分别加载数据
train_data = load_json(train_file)
val_data = load_json(val_file)
test_data = load_json(test_file)

# 加载tokenizer
tokenizer = BertTokenizerFast.from_pretrained('../../bert-base-chinese') ## 如果是正常下载的huggingface版本,则直接引用“bert-base-chinese”,如果下载到本地,则引用本地地址

# 分别编码数据
print("Encoding training data...")
train_encodings = encode_data(train_data, tokenizer)
print("Encoding validation data...")
val_encodings = encode_data(val_data, tokenizer)
print("Encoding test data...")
test_encodings = encode_data(test_data, tokenizer)

# 保存数据,以pth格式保存
torch.save({
'train': train_encodings,
'val': val_encodings,
'test': test_encodings,
}, save_path)
print(f"Preprocessed data saved to {save_path}")

if __name__ == "__main__":
main()
encode_data函数
  • 该函数的作用是将文本数据和意图标签转换成模型可以理解的格式。具体来说:
  1. tokens的编码
    • 使用BertTokenizerFast对每个数据项的tokens进行编码。BertTokenizerFast是BERT模型的一部分,用来将文本转换为BERT模型输入需要的ID(即input_ids)。
    • is_split_to_word=True,告诉Bert输入已经是分词的列表
    • 对于每个tokens,会进行padding(填充),使所有的输入序列长度一致,最长为max_len,超过的部分会被截断。
    • return_tensors='pt'表示返回PyTorch的张量(tensor),这使得数据可以直接用于PyTorch模型。
    • return_offsets_mapping=True的作用是返回词汇的位置信息(这个在一些任务中会用到,但在当前代码中没有进一步处理)。
    • 返回一个包含多个键值对的字典encoding,其中包括了input_idsattention_masktoken_type_ids
      • input_ids:这是一个包含编码后token的ID列表。对于每个token,tokenizer会找到它在BERT词汇表中的对应ID。
      • 由于我们设置了return_tensors='pt',这个input_ids是一个PyTorch张量(tensor),它的形状通常是 [batch_size, sequence_length],但由于每次调用编码时只处理一个句子,batch_size为1。
  2. 意图标签编码
    • 将意图标签转换为数字ID。字典intents_num定义了每个意图对应的数字ID。
    • 如果在intents_num字典中找不到该意图(比如意图是'None'),则返回默认值-1
  3. 返回结果
    • 返回一个字典,其中包含:
      • input_ids:所有数据项的token ID的stack。
      • intent_labels:每个数据项的意图标签(数字ID)的stack。
    • 这些数据将用于训练、验证和测试模型。
encoding['input_ids'][0]
  • encoding['input_ids'] 是一个形状为 [1, max_len] 的张量,表示单个输入句子的token ID。[0] 表示从这个张量中取出第一个元素,即获取第一个句子的 input_ids
  • 由于在每次编码时我们只处理一个句子,所以encoding['input_ids'][0]就是一个一维的张量,包含了该句子的所有token的ID
    image.png
torch.stack

torch.stack(input_ids) 是一个 PyTorch 操作,它将多个张量沿着新维度进行堆叠(stack)
torch.stack 会将输入的多个张量(input_ids)沿着一个新的维度进行堆叠,生成一个新的张量。

假设 input_ids 是一个包含多个 PyTorch 张量(例如,每个句子的 token IDs 的张量)的列表或其他可迭代对象。
输入:

  • 每个元素(例如每个句子)通常是一个形状为 [sequence_length] 的一维张量。
  • torch.stack(input_ids) 会沿着新维度堆叠这些一维张量,形成一个新的张量。
    输出:
  • torch.stack(input_ids) 将会返回一个形状为 [batch_size, sequence_length] 的二维张量,其中 batch_size 是输入张量的数量(即句子的数量),sequence_length 是每个句子的长度(即每个句子的 token 数量)。
  • 具体来说,如果 input_ids 中有 n 个句子,每个句子的长度为 L,那么堆叠后得到的张量的形状就是 [n, L]

假设我们有以下三个句子的 input_ids

1
2
3
4
5
input_ids = [
torch.tensor([101, 2023, 2003, 1037, 2742]), # 第一个句子
torch.tensor([101, 2023, 2003, 1037, 2742, 102]), # 第二个句子
torch.tensor([101, 2023, 2003, 2742, 102]) # 第三个句子
]

每个张量表示一个句子,[101, 2023, 2003, 1037, 2742] 是一个句子的 token ID 列表。
执行 torch.stack(input_ids) 后,得到一个新的张量:

1
2
stacked_tensor = torch.stack(input_ids)
print(stacked_tensor)

输出结果可能是:

1
2
3
tensor([[  101,  2023,  2003,  1037,  2742],
[ 101, 2023, 2003, 1037, 2742, 102],
[ 101, 2023, 2003, 2742, 102]])

stacked_tensor 的形状是 [3, 6],表示三个句子的 token ID 被堆叠成一个二维张量,其中 batch_size = 3(句子的数量),sequence_length = 6(最长句子的长度)。如果某些句子的长度较短(如第一个句子只有5个token),则 PyTorch 会按最大长度进行对齐,这会影响实际的输入张量的大小。

总结

  • torch.stack(input_ids) 会将多个一维张量(每个句子的 input_ids)堆叠成一个二维张量。
  • 结果的形状是 [batch_size, sequence_length],其中 batch_size 是句子数量,sequence_length 是每个句子的最大长度。
pth文件
  • .pth文件是PyTorch的专用格式,通常用于保存模型或张量数据。torch.save()函数能够将数据保存为PyTorch支持的格式,可以保存模型权重、优化器状态、训练进度等信息。
  • 在代码中,encode_data函数处理过的数据是PyTorch张量。使用torch.save()将数据保存为.pth文件,便于后续训练阶段直接加载。
    • .pth文件的优点是:
      1. 它可以存储大量数据(例如训练集、验证集、测试集的编码数据),这些数据是PyTorch张量,因此可以直接用于模型训练。
      2. .pth文件格式与PyTorch深度学习框架兼容,因此加载速度快,且方便在不同阶段读取、修改数据。

创建数据加载器

  • 为训练数据、测试数据创建数据集
  • 创建DataLoader用于数据加载

进行训练和评测

  • 在每个epoch调用train_epoch进行训练
  • 调用evaluate进行评测,找到最好的模型

dataset

[!question] 在main.py的时候,会看到这句话:print(f"训练集: {len(train_dataset)}, 测试集: {len(test_dataset)}"),但是,按照数据预处理返回的字典,似乎有input_idsintent_labels两个键,那这样不是只会返回2么?

这涉及到Dataset的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import torch
from torch.utils.data import Dataset

class SLUDataset(Dataset):
# 继承自Dataset类,用于加载数据集,对__len__和__getitem__方法进行重写
def __init__(self, encodings):
self.encodings = encodings

def __len__(self):
return len(self.encodings['input_ids']) # 返回 input_ids键对应的值(一个一维tensor)的长度

def __getitem__(self, idx):
# item = {key: self.encodings[key][idx] for key in
# ['input_ids']}
# item['intent_labels'] = self.encodings['intent_labels'][idx]
# return item
# 这样好理解一点
return {
'input_ids': self.encodings['input_ids'][idx],
'intent_labels': self.encodings['intent_labels'][idx]
}

这个 SLUDataset 类是 PyTorch 中用于构建自定义数据集的核心组件,专门为语义理解(SLU)任务设计。它的核心功能是将预处理后的数据封装成 PyTorch 可识别的数据集格式。以下是对这个类的逐层解析:

类结构图示

1
2
3
4
5
6
7
classDiagram
class SLUDataset {
- encodings: dict
+ __init__(encodings)
+ __len__() int
+ __getitem__(idx) dict
}

核心方法详解

1. __init__(self, encodings)

作用:初始化数据集对象
参数说明

  • encodings:字典类型,必须包含以下两个键:
    • 'input_ids':形状为 [num_samples, max_seq_len] 的 tensor,存储 token 索引
    • 'intent_labels':形状为 [num_samples] 的 tensor,存储意图标签的数值编码

示例输入

1
2
3
4
5
encodings = {
'input_ids': torch.tensor([[101, 2345, ...], ...]), # 假设有100个样本
'intent_labels': torch.tensor([0, 3, 8, ...]) # 100个标签
}
dataset = SLUDataset(encodings)

2. __len__(self)

作用:返回数据集样本总数
关键点

  • 假设 input_idsintent_labels 长度严格一致
  • 如果存在其他数据字段(如 attention_mask),也需要保证长度一致

3. __getitem__(self, idx)

作用:根据索引获取单个样本
实现逻辑

1
2
3
4
5
6
7
def __getitem__(self, idx):
# 构建样本字典
item = {
'input_ids': self.encodings['input_ids'][idx], # 取第idx个样本的token序列
'intent_labels': self.encodings['intent_labels'][idx] # 对应标签
}
return item

数据流示例

1
2
3
4
5
# 假设idx=5
item = {
'input_ids': tensor([ 101, 2345, 3456, ..., 0]), # 长度=max_seq_len
'intent_labels': tensor(3) # 数值型标签
}

与DataLoader的配合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from torch.utils.data import DataLoader

# 创建数据集实例
dataset = SLUDataset(encodings)

# 创建DataLoader
loader = DataLoader(
dataset,
batch_size=16,
shuffle=True
)

# 迭代获取批次数据
for batch in loader:
print(batch['input_ids'].shape) # torch.Size([16, 128])
print(batch['intent_labels'].shape # torch.Size([16])

典型应用场景

1
2
3
4
5
6
7
8
9
# 查看单个样本
sample = dataset[10]
print(f"Token IDs: {sample['input_ids']}")
print(f"Intent Label: {sample['intent_labels']}")

# 统计数据集信息
print(f"总样本数: {len(dataset)}")
print(f"输入维度: {dataset[0]['input_ids'].shape}")
print(f"标签示例: {dataset[0]['intent_labels'].item()}")

model

回到model.py

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
# models.py

import torch
from torch import nn

#######################################
# CNN原始模型
#######################################

class CNN(nn.Module):
def __init__(self, vocab_size, embed_dim, num_intent_labels):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim) # 词嵌入层,输入为词索引,输出为词向量
self.pool = nn.AdaptiveMaxPool1d(1) # 池化层,输入为词向量,输出为句子的最大池化结果

self.conv1 = nn.Sequential(
nn.Conv1d(in_channels=embed_dim, out_channels=embed_dim, kernel_size=3, padding=1),
nn.BatchNorm1d(embed_dim),
nn.ReLU(),
nn.Dropout(0.5)
)

self.conv2 = nn.Sequential(
nn.Conv1d(in_channels=embed_dim, out_channels=embed_dim * 2, kernel_size=3, padding=1),
nn.BatchNorm1d(embed_dim * 2),
nn.ReLU(),
)

self.conv3 = nn.Sequential(
nn.Conv1d(in_channels=embed_dim * 2, out_channels=embed_dim, kernel_size=3, padding=1),
nn.BatchNorm1d(embed_dim),
nn.ReLU(),
)

self.intent_classifier = nn.Linear(embed_dim, num_intent_labels) # 分类层,输入为句子的最大池化结果,输出为意图标签的概率分布

def forward(self, input_ids):
'''
input_ids: [batch_size, seq_len],这是在传入时就设置的,即模型的输入
'''

# 词嵌入层
embeds = self.embedding(input_ids) # [batch_size, seq_len, embed_dim]
embeds = embeds.permute(0, 2, 1) # [batch_size, embed_dim, seq_len] 转换为CNN输入的格式,需要卷积的维度放在后面,为seq_len

# 卷积层
conv1_out = self.conv1(embeds) # [batch_size, embed_dim, seq_len]
conv2_out = self.conv2(conv1_out) # [batch_size, embed_dim * 2, seq_len]
conv3_out = self.conv3(conv2_out) # [batch_size, embed_dim, seq_len]

# 池化层
pooled = self.pool(conv3_out) # [batch_size, embed_dim, 1]
result = pooled.squeeze(-1) # [batch_size, embed_dim] 去除最后一个维度

# 分类
it_logits = self.intent_classifier(result) # [batch_size, num_intent_labels]

return it_logits

#######################################
# CNNSeq原始模型
#######################################

class CNNSeq(nn.Module):
def __init__(self, vocab_size, embed_dim, num_intent_labels):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim) # 词嵌入层,输入为词索引,输出为词向量

self.cnn_2 = nn.Conv1d(in_channels=embed_dim, out_channels=embed_dim // 4, kernel_size=2)
self.cnn_3 = nn.Conv1d(in_channels=embed_dim, out_channels=embed_dim // 4, kernel_size=3)
self.cnn_4 = nn.Conv1d(in_channels=embed_dim, out_channels=embed_dim // 4, kernel_size=4)
self.cnn_5 = nn.Conv1d(in_channels=embed_dim, out_channels=embed_dim // 4, kernel_size=5)

self.pool = nn.AdaptiveMaxPool1d(1) # 池化层,输入为词向量,输出为句子的最大池化结果

self.intent_classifier = nn.Linear(embed_dim, num_intent_labels) # 分类层,输入为句子的最大池化结果,输出为意图标签的概率分布

def forward(self, input_ids):
# 词嵌入
embeds = self.embedding(input_ids)
embeds = embeds.permute(0, 2, 1) # [batch_size, embed_dim, seq_len] 转换为CNN输入的格式,需要卷积的维度放在后面,为seq_len

# 卷积
cout_2 = self.cnn_2(embeds) # [batch_size, embed_dim // 4, seq_len - 1]
pool_2 = self.pool(cout_2) # [batch_size, embed_dim // 4, 1]

cout_3 = self.cnn_3(embeds) # [batch_size, embed_dim // 4, seq_len - 2]
pool_3 = self.pool(cout_3) # [batch_size, embed_dim // 4, 1]

cout_4 = self.cnn_4(embeds) # [batch_size, embed_dim // 4, seq_len - 3]
pool_4 = self.pool(cout_4) # [batch_size, embed_dim // 4, 1]

cout_5 = self.cnn_5(embeds) # [batch_size, embed_dim // 4, seq_len - 4]
pool_5 = self.pool(cout_5) # [batch_size, embed_dim // 4, 1]

# 拼接
pooled = torch.cat([pool_2.squeeze(-1), pool_3.squeeze(-1), pool_4.squeeze(-1), pool_5.squeeze(-1)], dim=1) # [batch_size, embed_dim]

# 分类
it_logits = self.intent_classifier(pooled) # [batch_size, num_intent_labels]

return it_logits

这段代码定义了两个模型,CNNCNNSeq,都用于自然语言处理中的意图分类任务。我们将逐步解析这两个模型的结构、nn.EmbeddingAdaptiveMaxPool1d 的作用,并讨论它们的区别。

1. nn.Embedding(vocab_size, embed_dim) 的作用

  • nn.Embedding 是 PyTorch 中的一个层,用于将词索引(即单词的整数标识符)映射到词向量(嵌入向量)空间中。
  • 输入vocab_size 表示词汇表的大小(即总共有多少个不同的单词,通常默认使用30522),embed_dim 表示词向量的维度(即每个单词被嵌入为一个 embed_dim 维的向量)。
  • 输出nn.Embedding 层会返回一个形状为 [batch_size, seq_len, embed_dim] 的张量,其中:
    • batch_size 是输入的句子数量。
    • seq_len 是每个句子的单词数量(即每个句子的最大长度)。
    • embed_dim 是每个单词的嵌入维度。
  • 原本的输入句子将每个token转化为了索引,然后通过embedding嵌入到embed_dim维度的空间中

功能:这层的作用是将每个输入的词(由词索引表示)转换为一个固定维度的词向量。在 NLP 中,词嵌入是为了捕捉单词之间的语义关系和语法信息。

image.png

[!note] permute(0, 2, 1) 的作用
因为PyTorch的Conv1d期望输入维度为[batch, channels, sequence],而Embedding输出是[batch, sequence, channels],因此需要调整维度
image.png

2. AdaptiveMaxPool1d(1) 的作用

AdaptiveMaxPool1d 是一个池化层,主要用于降低输入的维度,保留最重要的特征。

  • 输入:输入是一个形状为 [batch_size, embed_dim, seq_len] 的张量,其中:
    • batch_size 是批量大小,表示输入句子的数量。
    • embed_dim 是词向量的维度。
    • seq_len 是每个句子的长度。
  • 输出AdaptiveMaxPool1d(1) 会对输入数据进行池化,输出形状为 [batch_size, embed_dim, 1],即每个特征维度都被压缩到长度为1。这是通过对 seq_len 长度的特征进行最大池化操作(从中选取最大值)来实现的,池化的目的是保留最重要的特征信息。
    • 因为是最大池化(MaxPooling),它会从输入的每个特征维度中选取该维度的最大值。
    • Adaptive 表示该层会根据输入的尺寸自适应地调整池化的窗口和步长,以确保输出的大小符合要求。

在这段代码中,AdaptiveMaxPool1d(1) 会将每个句子的嵌入表示池化为一个单一的标量(最大池化值),从而将句子表示的维度从 [batch_size, embed_dim, seq_len] 压缩到 [batch_size, embed_dim, 1]

image.png
image.png

3. CNN 模型

CNN 模型是一个卷积神经网络(CNN),用于文本的意图分类任务。主要结构如下:

  • Embedding层:将输入的词索引转化为词向量。
  • 卷积层(conv1, conv2, conv3):使用三个不同的卷积层来提取文本中的特征。
    • conv1:使用 3x3 的卷积核,输出维度为 embed_dim,然后进行批标准化(Batch Normalization),激活(ReLU)和丢弃(Dropout)。
    • conv2:使用 3x3 的卷积核,输出维度为 embed_dim * 2,然后进行批标准化和激活。
    • conv3:使用 3x3 的卷积核,输出维度为 embed_dim,然后进行批标准化和激活。
  • 池化层AdaptiveMaxPool1d(1) 进行最大池化,将每个句子的特征向量压缩成一个标量。
  • 分类层:将池化后的特征传递给全连接层(intent_classifier),输出意图类别的概率分布。

卷积操作

  • 该模型使用了 1D 卷积,通常用于处理序列数据。卷积操作的目的是提取局部特征,例如在文本中提取特定的n-gram模式。

4. CNNSeq 模型

CNNSeq 模型与 CNN 模型类似,但有一些关键的不同点:

  • 多个卷积核CNNSeq 使用了四个不同大小的卷积核(大小为 2, 3, 4, 5)。每个卷积核捕捉不同范围的局部特征,这有助于模型捕捉更丰富的文本特征。
    • cnn_2, cnn_3, cnn_4, cnn_5 分别是不同大小的卷积核,处理文本中的不同特征。
  • 池化:每个卷积层后都有一个池化操作,通过 AdaptiveMaxPool1d(1) 将每个卷积层的输出压缩为一个标量。
  • 特征拼接:最后,模型将来自不同卷积核的池化结果拼接在一起,形成一个完整的特征向量。拼接后的张量维度是 [batch_size, embed_dim],然后通过全连接层分类。

关键区别

  • CNNSeq 在卷积部分使用了多个不同大小的卷积核,旨在捕捉不同的文本特征,而 CNN 只有一个卷积层。
  • CNNSeq 对多个卷积核的输出进行了拼接,而 CNN 只使用一个卷积层的输出。

两个模型区别

感觉是这样的:
如何理解神经网络中通过add的方式融合特征? - 知乎

image.png

总结:

  • **nn.Embedding(vocab_size, embed_dim)**:将词索引转换为词向量,用于表示文本中的每个词。
  • **AdaptiveMaxPool1d(1)**:通过最大池化操作将序列的特征压缩为单个标量,保留最重要的特征。
  • CNNCNNSeq 的区别
    • CNN 使用三个卷积层提取文本特征,池化后通过全连接层分类。
    • CNNSeq 使用多个不同大小的卷积核,并将各个卷积核的池化结果拼接在一起,然后通过全连接层进行分类。

tokens 的全部变化过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 输入文本(已分词)
tokens = ['请', '问', '在', '南',...] # 假设长度=47

# 经过tokenizer编码(max_len=128)
input_ids = [101, 2345, 567, 890,..., 0, 0] # 长度128的向量

# 进入DataLoader后
batch = {
'input_ids': torch.Size([16, 128]), # 假设batch_size=16
'intent_labels': torch.Size([16])
}

# 经过Embedding层(embed_dim=128)
embeds = embedding(input_ids) # [16, 128, 128]

# CNN模型中的维度转换:
embeds.permute(0, 2, 1) # [16, 128, 128] → [16, 128, 128](此处可能设计不合理)
conv1_out = self.conv1(embeds) # 保持[16, 128, 128]
pooled = self.pool(conv3_out) # [16, 128, 1]
result = pooled.squeeze(-1) # [16, 128]

trainer

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
# trainer.py

import torch
import numpy as np
from tqdm import tqdm
from seqeval.metrics import classification_report, precision_score, recall_score, f1_score
from sklearn.metrics import accuracy_score, precision_recall_fscore_support

def train_epoch(model, loader, optimizer, intent_loss_fn, device):
# 训练模式
model.train()
total_loss = 0
for batch in tqdm(loader, desc="Training", leave=False):
optimizer.zero_grad()
# 导入数据
input_ids = batch['input_ids'].to(device) # [batch_size, seq_len]
intent_labels = batch['intent_labels'].to(device) # [batch_size]

it_logits = model(input_ids) # [batch_size, num_intents]

# 计算损失
loss_it = intent_loss_fn(it_logits, intent_labels)
loss_it.backward()
optimizer.step()

total_loss += loss_it.item()

avg_loss = total_loss / len(loader)
print(f"训练损失: {avg_loss:.4f}")
return avg_loss


def evaluate(model, loader, device, intent_loss_fn, model_type="cnn"):
# eval不开梯度回传,关闭dropout等
model.eval()

intent_preds, intent_true = [], []

with torch.no_grad():
for batch in loader:
input_ids = batch['input_ids'].to(device) # [batch_size, seq_len]
intent_labels = batch['intent_labels'].to(device) # [batch_size]

it_logits = model(input_ids) # [batch_size, num_intents]

# print('label size: ', intent_labels.shape)
# print('pred size: ', it_logits.shape)
it_logits_cpu = it_logits.cpu().numpy().argmax(axis=1) # [batch_size],取最大值索引作为预测标签
it_labels_cpu = intent_labels.cpu().numpy() # [batch_size], 真实标签

intent_preds.extend(it_logits_cpu) # 使用extend方法将预测标签添加到列表中
intent_true.extend(it_labels_cpu)
# print('label cpu size: ', intent_labels.shape)
# print('pred cpu size: ', it_logits.shape)

# 重新将列表转为numpy数组
it_true_np = np.array(intent_true)
it_pred_np = np.array(intent_preds)
# print(it_true_np)
# print(it_pred_np)
it_acc = accuracy_score(it_true_np, it_pred_np)
# 调用sklearn的函数计算宏平均精度、精确度、召回率、F1值
it_pre, it_rec, it_f1, _ = precision_recall_fscore_support(
it_true_np, it_pred_np, average='macro', zero_division=0
)

print("\n=== 意图识别 ===")
print(f"Acc: {it_acc:.4f}, MacroP: {it_pre:.4f}, MacroR: {it_rec:.4f}, MacroF1: {it_f1:.4f}")

return {
'intent_accuracy': it_acc,
'intent_precision': it_pre,
'intent_recall': it_rec,
'intent_f1': it_f1,
}
  • 注意训练的时候要开model.train(),测试的时候要开model.eval()
  • it_logits = model(input_ids) 实际上就是在执行forward函数,返回每个intent的置信概率
  • 然后需要与真实标签计算交叉熵损失,进行梯度的反向传播和优化器步进
  • 测试的时候,由于不知道真实标签,所以不能计算损失,采用F1等指标进行衡量准确率

train和eval模式区别

在PyTorch中,model.train()model.eval() 是控制模型行为模式的两个关键方法,它们的主要区别体现在模型内部特定层的计算逻辑梯度计算机制上。以下是它们的核心区别和具体影响:

1. 核心功能对比

方法 model.train() model.eval()
模式 训练模式 评估模式(推理模式)
主要目的 启用训练所需的行为(如随机性) 关闭训练时的随机性,稳定输出
典型场景 模型训练(loss.backward() 模型验证、测试、推理

2. 对具体模块的影响

(1) Dropout 层
  • model.train()
    Dropout 会按照设定的 p 值随机屏蔽神经元,通过引入噪声防止过拟合。
    例如:输入为 [1,2,3,4],可能变为 [1,0,3,0]p=0.5)。
  • model.eval()
    Dropout 完全关闭,所有神经元保留,输出直接传递:
    [1,2,3,4][1,2,3,4]
(2) Batch Normalization 层
  • model.train()
    使用当前批次的均值和方差统计量,并更新全局移动平均(running_meanrunning_var)。
    公式:
    $$
    \text{BN}(x) = \gamma \cdot \frac{x - \mu_{\text{batch}}}{\sqrt{\sigma_{\text{batch}}^2 + \epsilon}} + \beta
    $$
  • model.eval()
    使用预计算的全局移动平均(running_meanrunning_var),停止更新统计量。
    公式:
    $$
    \text{BN}(x) = \gamma \cdot \frac{x - \mu_{\text{running}}}{\sqrt{\sigma_{\text{running}}^2 + \epsilon}} + \beta
    $$

3. 梯度计算机制

方法 梯度计算状态 内存占用
model.train() 默认启用梯度跟踪(requires_grad=True 较高(保存中间变量)
model.eval() 不自动关闭梯度(需手动使用 torch.no_grad() 较低

4. 典型使用场景

训练阶段
1
2
3
4
5
6
7
model.train()  # 切换到训练模式
for data, target in train_loader:
optimizer.zero_grad()
output = model(data)
loss = criterion(output, target)
loss.backward()
optimizer.step() # 参数更新
验证/测试阶段
1
2
3
4
5
model.eval()  # 切换到评估模式
with torch.no_grad(): # 关闭梯度计算
for data, target in val_loader:
output = model(data)
accuracy = calculate_accuracy(output, target)

5. 常见错误及后果

  1. 在验证时忘记调用 model.eval()

    • Dropout 仍会随机屏蔽神经元 → 预测结果不稳定
    • BatchNorm 使用当前小批量统计 → 指标波动大
    • 示例:验证准确率从 85% 波动到 70%
  2. 在训练时误用 model.eval()

    • Dropout 失效 → 模型容易过拟合
    • BatchNorm 停止更新统计量 → 模型无法学习数据分布
    • 示例:训练损失停滞不降,准确率无法提升

总结

维度 model.train() model.eval()
随机性 启用(Dropout、数据增强等) 关闭(稳定输出)
统计量更新 实时更新(BatchNorm) 固定预计算值
梯度计算 启用 需配合 torch.no_grad() 关闭
适用阶段 训练 验证、测试、推理

正确使用这两个模式是保证模型性能的关键步骤,尤其是在需要稳定推理结果的场景(如模型部署)中,model.eval() 必不可少。

实验结果

task1:运行指令

1
2
python main.py --model cnn  
python main.py --model cnn_seq

CNN:
image.png

image.png

CNN_seq:
image.png

image.png

task2相关注释已保存在文件

task3 超参数调优

[!question] 探究不同超参数(学习率lr,batch_size,模型维度embed_dim)对模型的影响(在configs/config.yaml中修改超参数,利用验证集(val.json,对应代码中的data[‘val’])检验性能变化),并画这三种超参数对模型性能的影响的图表(折线图或柱状图均可)

  • 没有特别好说的,注意是在验证集上调优,因为我们并不知道对于测试集来说,什么样子的超参数是最好的(如果知道了就等同于作弊),因此需要在验证集上找到最优的超参数。
  • 代码只需要写个for循环遍历所有超参数即可。通常最理想的方法是做网格搜索,不过这里因为需要画图,所以就选择了固定其他两个超参数,单独调整某个超参数,查看最后指标(我选的是ACC,不过选其他的应该也可以?)变化情况

batch_size.png|475
embedding_dim.png|475
learning_rate.png|475

又是一些跟优化理论相关的内容了。。。

  • 小batch理论上效果会好,因为是对梯度取平均后下降,所以直观上来看小batch下降会更容易些,但是缺点是不稳定和时间更长,实际上batch大小还是一个trade off
  • embedding_dim不能设的太大,不然嵌入的空间太稀疏了,模型处理比较困难,而且冗余的信息不一定会很好地帮助训练,and容易过拟合
  • learning rate动态变化会好,因为epoch小的时候下降幅度太小了,当epoch到训练末期容易陷入山谷来回颠簸,不容易下降到最低点
0%