关于深度学习:恒源云CAN-借助数据分布提升分类性能

8次阅读

共计 5890 个字符,预计需要花费 15 分钟才能阅读完成。

文章起源 | 恒源云社区(专一人工智能 / 深度学习 GPU 收费减速平台,官网体验网址:https://gpushare.com)

原文作者 | Mathor

原文地址 | https://gpushare.com/forum/topic/683/can-%E5%80%9F%E5%8A%A9%E6%95%B0%E6%8D%AE%E5%88%86%E5%B8%83%E6%8F%90%E5%8D%87%E5%88%86%E7%B1%BB%E6%80%A7%E8%83%BD?_=1635993190297

本文将介绍一种用于分类问题的后处理技巧(Trick),出自 EMNLP 2021 Findings 的一篇论文《When in Doubt: Improving Classification Performance with Alternating Normalization》。通过实测,CAN(Classification with Alternating Normalization)的确少数状况下能晋升多分类问题的成果(CV、NLP 通用),而且简直没有减少预测老本,因为它仅仅只是对预测后果的从新归一化操作

CAN 的思维

乏味的是,其实 CAN 的思维十分奢侈,奢侈到咱们每个人简直都用过。具体来说,假如考试中有 10 道选择题,前 9 道你都比拟有信念,第 10 题齐全不会只能瞎蒙,然而你发现后面 9 题选 A、B、C、D 的比例是 3:3:2:2,那么第 10 题在蒙的时候,你会不会更偏向于 C 和 D?如果是更极其的状况,你发现后面 9 题选 A、B、C 的都有,但就是没有选 D 的,那你蒙第 10 题的时候,会不会更偏向于 D?

回到分类工作上,假如当初有一个二分类问题,模型对于输出 aaa 给出的预测后果是 \(p^{(a)}=[0.05,0.95]\),那么咱们就能够给出预测类别为 1;接下来,对于输出 bbb,模型给出的预测后果是 \(p^{(b)}=[0.5,0.5]\),这种后果是最不确定的,咱们也不晓得应该输入哪个类别

然而,如果我通知你:

  1. 类别必然是 0 或 1 其中之一
  2. 两个类别呈现的概率各为 0.5

在已知这两点「先验」信息的状况下,因为前一个样本的预测后果为 1,那么基于奢侈的平均思维,咱们是否会更偏向于将后一个样本预测为 0,以失去一个满足第二点「先验」的预测后果?

这些简略的例子背地,有着跟 CAN 同样的思维,其实就是用「先验散布」来校对「低置信度」的预测后果,使得新的预测后果的散布更靠近先验散布

TOP- K 熵

精确地说,CAN 是针对低置信度预测后果的后处理伎俩,所以咱们首先要有一个掂量预测后果不确定性的指标。常见的度量是「熵」,对于 \(p=[p_1,p_2,…,p_m]\),定义为

尽管熵是一个常见的抉择,但其实它得出的后果并不总是合乎咱们的直观了解。例如对于 \(p^{(a)}=[0.5, 0.25,0.25]和 p^{(b)}=[0.5,0.5,0]\),间接套用公式失去 \(H(p^{(b)})H(p ^{(b)} )\),但如果让人主观去评估这两个概率分布,显然咱们会认为 \(p^{(b)}比 p^{(a)}\)更不确定,所以间接用熵还不够正当

主观地讲,熵值越大,示意这个零碎外部越不稳固。如果要与置信度分割起来,熵值越大,置信度越低

一个简略的修改是只用前 top- k 个概率值来算熵,假如 \(p_1,p_2,…,p_k\)是概率最高的 \(k\)个值,那么

其中,\(\mathcal{T}\)是一个取向量最大的前 k 个值的操作 \({R}^{m}\)→\({R}^{k}\)。咱们能够将式 (2) 带入式 (1) 开展得

其中,\(p~i= p_i / \sum\limits_{i=1}^k p_i\)

交替归一化(ALTERNATING NORMALIZATION)

这一部分我先给出论文中的算法步骤形容,下一节我会手动模仿一遍计算过程

Step1

设列向量 \(\mathbf{b}_0\in \mathbb{R}^m\)为输出样本 \(x\)对应各类别的概率分布,\(m\)示意类别数。咱们生成一个 \(n\)×\(m\)的概率矩阵 \(A_0,A_0\)其实是 \(n\)个置信度十分高的样本对各个类别的预测概率向量拼接而得,通过将 \(A_0 和 \mathbf{b}_0\)进行拼接失去一个 \((n+1)×m\)的矩阵 \(L_0\)

Step2

第二步是一个迭代的过程,具体来说,首先对矩阵 \(L_0\)进行列归一化(使得每列求和为 1),而后进行行归一化(使得每行求和为 1)。进行算法步骤前,先定义一个向量对角化操作:

D(v)会将列向量 \(\mathbf{v}\in \mathbb{R}^n\)转换为 \(n×n\)的对角矩阵,对角线元素即本来的向量元素

列归一化

其中,参数 \(\alpha \in \mathbb{N}^+\)管制着 \(\mathbf{b}_0\)收敛到高置信度的速度(越大速度越快,默认取 1);\(\mathbf{e}\in \mathbb{R}^{n+1}\)是全 1 的列向量。通过式 (4) 的变换后,矩阵 \(S_d\in \mathbb{R}^{(n+1)\times m}\)是 \(L_{d-1}\)矩阵的列归一化模式;\(\Lambda_S^{-1}\)是 \(\Lambda_S\)的逆矩阵

行归一化

其中,\(\mathbf{e}\in \mathbb{R}^{m}\)依然是全 1 的列向量,只不过此时它的维度是 mmm 维的;矩阵 \(L_d\in \mathbb{R}^{(n+1)\times m}\)是行归一化的(但 \(L_d\)并不是具体某个矩阵的行归一化模式);\(\Lambda_q \in \mathbb{R}^{m\times m}\)是一个对角矩阵,对角线上的元素是各类别的散布占比

例如

示意这是一个三分类问题,并且各个类别的比例为 1:2:2

Step3

Step2 循环迭代 ddd 次后失去的矩阵 \(L_d\):

其中,\(\mathbf{b}_d\)就是依据「先验散布」调整后的新的概率分布

留神,这个过程须要咱们遍历每个低置信度的预测后果,也就是说一一样本进行修改,而不是一次性修改的。并且尽管迭代过程中 \(A_0\)里对应的各个样本的预测概率也都随之更新了,但那只是长期后果,最初都是弃之不必的,每次修改都是用原始的 \(A_0\)

模拟计算 AN(ALTERNATING NORMALIZATION)

首先咱们设置一些矩阵和参数

略微解释一下,\(A_0\)依据原算法形容是 nnn 个置信度比拟高的样本的预测概率分布进行拼接,能够看出只有 3 个样本置信度比拟高,并且他们的预测类别别离为 2,0,2;\(b_0\)是某样本 \(x\)的预测概率,因为是概率分布,所以必须满足求和为 1;\(\Lambda_q\)是三个类别的样本比例,能够看出第一个类别的数据十分多

首先是列归一化

仔细观察矩阵 \(S_d\),它每列求和都是 1,也就是列归一化,如果咱们追根溯源的话,实际上 \(S_d\)就是 \(L_0\)对每列求和,而后将 \(L_0\)每列元素除以该和

接着是行归一化

咱们只须要 \(L_1\)的最初一行,即 \(\mathbf{b}_1=\begin{bmatrix}23/25,0,2/25\end{bmatrix}^T\),能够看,本来 \(\mathbf{b}_0\)的概率分布是 \(\begin{bmatrix}0.5 ,0,0.5\end{bmatrix}^T\),通过「先验」调整后的类别显著偏差数据占比比拟多的第一类,并且 \(\mathbf{b}_1\)向量求和为 1,合乎概率的定义

实际上这个过程用 Python 实现也非常简单,上面就是我本人写的一个代码,变量命名与公式中的变量名完全一致

import numpy as np

n, m, d, alpha = 3, 3, 5, 1
# n: 样本数量
# m: 类别数
# d: 迭代次数
# alpha: 次方

def softmax(arr):
    return np.exp(arr) / np.sum(np.exp(arr))

A_0 = np.array([[0.2, 0, 0.8], [0.9, 0.1, 0], [0, 0, 1]])
# A_0 = softmax(np.random.randn(n, m))

b_0 = np.array([0.5, 0, 0.5])
# b_0 = softmax(np.random.randn(m))

L_0 = np.vstack((A_0, b_0)) # (n+1) * m

Lambda_q = np.diag(np.array([0.8, 0.1, 0.1]) )
# Lambda_q = np.diag(softmax(np.random.randn(m)) )

print("预测概率:", b_0)
print("各类别样本数量散布:", np.diag(Lambda_q, 0))

L_d_1 = L_0
for _ in range(d):
    Lambda_S = np.diag(np.dot((L_d_1 ** alpha).T, np.ones((n + 1))) )
    S_d = np.dot((L_d_1 ** alpha), np.linalg.inv(Lambda_S))
    Lambda_L = np.diag(np.dot(np.dot(S_d, Lambda_q), np.ones((m))) )
    L_d_1 = np.dot(np.dot(np.linalg.inv(Lambda_L), S_d), Lambda_q )
    print("依据先验调整后的概率:", L_d_1[-1:])

参考实现

上面给出苏剑林大佬的实现,他的代码中将 Top- k 熵做了个归一化,保障 \(H_{\text{top-k}} (p) \in [0,1]\),这样比拟好确定阈值(即代码中的threshold

import numpy as np

# 预测后果,计算修改前准确率
y_pred = np.array([[0.2, 0.5, 0.2, 0.1],
          [0.3, 0.1, 0.5, 0.1],
          [0.4, 0.1, 0.1, 0.4],
          [0.1, 0.1, 0.1, 0.8],
          [0.3, 0.2, 0.2, 0.3],
          [0.2, 0.2, 0.2, 0.4]])
num_classes = y_pred.shape[1]
y_true = np.array([0, 1, 2, 3, 1, 2])
acc_original = np.mean([y_pred.argmax(1) == y_true])
print('original acc: %s' % acc_original)

# 从训练集统计先验散布
# prior = np.zeros(num_classes)
# for d in train_data:
#     prior[d[1]] += 1.
# prior /= prior.sum()
prior = np.array([0.2, 0.2, 0.25, 0.35])


# 评估每个预测后果的不确定性
k = 3
y_pred_topk = np.sort(y_pred, axis=1)[:, -k:]
y_pred_topk /= y_pred_topk.sum(axis=1, keepdims=True) # 归一化
y_pred_entropy = -(y_pred_topk * np.log(y_pred_topk)).sum(1) / np.log(k) # top- k 熵
print(y_pred_entropy)

# 抉择阈值,划分高、低置信度两局部
threshold = 0.9
y_pred_confident = y_pred[y_pred_entropy < threshold] # top- k 熵低于阈值的是高置信度样本
y_pred_unconfident = y_pred[y_pred_entropy >= threshold] # top- k 熵高于阈值的是低置信度样本
y_true_confident = y_true[y_pred_entropy < threshold]
y_true_unconfident = y_true[y_pred_entropy >= threshold]

# 显示两局部各自的准确率
# 一般而言,高置信度集准确率会远高于低置信度的
acc_confident = (y_pred_confident.argmax(1) == y_true_confident).mean()
acc_unconfident = (y_pred_unconfident.argmax(1) == y_true_unconfident).mean()
print('confident acc: %s' % acc_confident)
print('unconfident acc: %s' % acc_unconfident)

# 一一批改低置信度样本,并从新评估准确率
right, alpha, iters = 0, 1, 1 # 正确的个数,alpha 次方,iters 迭代次数
for i, y in enumerate(y_pred_unconfident):
    Y = np.concatenate([y_pred_confident, y[None]], axis=0) # Y is L_0
    for _ in range(iters):
        Y = Y ** alpha
        Y /= Y.sum(axis=0, keepdims=True)
        Y *= prior[None]
        Y /= Y.sum(axis=1, keepdims=True)
    y = Y[-1]
    if y.argmax() == y_true_unconfident[i]:
        right += 1

# 输入修改后的准确率
acc_final = (acc_confident * len(y_pred_confident) + right) / len(y_pred)
print('new unconfident acc: %s' % (right / (i + 1.)))
print('final acc: %s' % acc_final)

试验后果

那么,这样简略的后处理,到底能带来多大的晋升呢?原论文给出的试验后果是相当可观的:

大体来说,类别数越多,成果晋升越显著,如果类别数比拟少,那么晋升可能比拟强劲甚至会降落

ONE MORE THING

一个很天然的疑难是为什么不间接将所有低置信度的后果跟高置信度的后果拼在一起进行修改,而是要一一修改?其实很好了解,CAN 本意是要借助「先验散布」,联合高置信度后果来修改低置信度,在这个过程中如果掺入的低置信度后果越多,最终的偏差可能就越大,因而实践上一一修改会比批量修改更为牢靠

REFERENCES
When in Doubt: Improving Classification Performance with Alternating Normalization
CAN:借助先验散布晋升分类性能的简略后处理技巧

正文完
 0