论文阅读笔记-LogME: Practical Assessment of Pre-trained Models for Transfer Learning
前言
在NLP领域中(准确地说应为在自然语言处理领域的),预训练语言模型似乎已经成为各类任务不可或缺的关键组件。人们常常看到文章将新时代称为"BERT时代"或"XXX时代"(指代某种特定的版本或框架),这些文章通常会对许多主流模型进行系统地分析和比较,并详细阐述其优势与不足;然而,在这些研究中相对而言属于理论层面的研究。
一种简洁高效的方法就是对每个候选模型分别针对具体任务进行微调训练。由于微调过程需要投入大量的训练时间,在这种情况下最少需要几个小时的时间。对于某些预训练语言模型而言,在进行微调时还需要优化超参数设置。选择并评估一个预训练模型的迁移性能大约需要50个小时!对于计算资源有限的研究者而言,寻找一种能够高效选择适合的预训练语言模型的方法尤为关键。偶然间我发现了一篇介绍这一方法的论文,在这篇论文中提到了一种名为LogME的方法值得一试(图1)。

请多提一下——您可能已经知道的是关于预训练语言模型的知识吗?实际上,在自然语言处理(NLP)领域的模型选择评分中使用了这一方法。然而,在计算机视觉(CV)领域中,则有其他的评分标准和方法可供参考——例如LEEP等指标。对这些感兴趣的朋友不妨深入研究相关文献!
本文围绕LogME方法的相关内容,在学院官方公众号上发布了一篇文章,并提供原文链接:直击原文阅读。开源代码采用PyTorch实现GPU加速效果显著,在文章末尾我已经将其转换为TensorFlow2版本,并附上了方便直接应用于相关模型的学习资料。
前情提要
将上面提到的问题,描述成图模型,就是论文中所画出如下的这样:

在这个任务中,我们假设有一个由M个预训练模型组成的集合\{\phi_m\}_{m=1}^{M}以及包含n个标签的数据集\{(x_i, y_i)\}_{i=1}^{n}。通常情况下,我们是通过微调结合各种评估指标来衡量模型\phi的性能T_m的;而现在我们试图通过一种方法获得S_m的结果集合\{S_m\}_{m=1}^{M}与原始的性能指标集合\{T_m\}_{m=1}^{M}之间具有良好的相关性。
主要是围绕用户的语言需求展开讨论,并重点分析了其特定的关注点和需求特点。
LogME方法
LogME的优越性能来自于以下三个方面:
无须梯度计算
为加快预训练模型的选择速度, 我们仅将预训练模型 φ 视为特征提取器, 并避免对它进行更新. 这样做的结果就是, 只需完成一次前馈传播过程, 就能获得特征 {f_i = φ(x_i)}_{i=1}^{n} 和对应的标注 {y_1, y_2, ..., y_n}. 从而进一步转化为评估这些特征与标注之间关联的可能性.
基于此,在研究过程中我们采用了普遍适用的概率统计方法,并使用概率密度函数p(y|F)来量化特征与标签之间的关系。值得注意的是,在迁移学习的过程中,默认是在预训练模型的基础上增加了一层新的线性映射关系,并且这种关系能够有效捕捉到原始数据中的关键特性。
到这里的时候, 很多人自然会想到, 一种直接的方法是利用Logistic Regression或者Linear Regression来获得最优权重, 然后以似然函数 p(y|F,w^*) 作为评估标准. 然而这实际上等同于训练一个模型来建模问题, 这样容易导致过拟合的问题, 而且这些方法通常涉及许多需要选择的超参数, 因此计算成本会很高而且效果也不太理想.
无须超参数调优
为了避免超参数进行调优,论文中的方法选用的是统计学中的证据(evidence,也叫marginalized likelihood,即边缘似然)来衡量特征与标注的关系。它不使用某个特定的 w^* 的值,而是使用 w 的分布 p(w) 来得到边缘化似然的值 p(y|F)=\int p(w)p(y|F,w)dw。它相当于取遍了所有可能的 w 值,能够更加准确地反映特征与标注的关系,不会有过拟合的问题。其中,p(w) 与 p(y|F,w) 分别由超参数 \alpha 和 \beta 决定,但是它们不需要 grid search,可以通过最大化evidence来直接求解。于是,我们就得到了对数最大证据(Log Maximum Evidence, 缩写LogME)标准来作为预训练模型选择的依据,如下图:

数学推导的内容在此省略了,请您移步原文阅读全文;具体细节已在图中标注出来需要注意的是,在计算过程中将预训练模型视为特征提取器这一假设下进行分析;但值得注意的是LogME也可评估在迁移学习(微调)过程中的性能

算法实现优化
值得注意的是,在LogME算法中涉及了大量矩阵分解、求逆以及相乘操作等关键步骤,在不谨慎执行的情况下可能会导致算法复杂度急剧攀升(例如参考上图第9行所述的粗放实现)。经过深入分析后我们发现通过对矩阵运算开销进行巧妙优化能够显著提升算法效率(具体改进措施体现在上图第10行所示的具体实施流程)。这一改进不仅使整体计算复杂度下降了一个数量级即从四次方减少到三次方(如表所示)而且实现了能在几秒钟内高效处理常见场景的目的:

实验结果
在实验部分中,我们采用了合成数据、真实数据等多种途径对LogME进行了评估,在共17个数据集和14个预训练模型上进行了测试。该方法在这些大量数据集和预训练模型上均表现出色。
值得我们深入探讨的是 LogME 的评估标准是否与人类主观感受相吻合。为了验证这一假设, 我们针对分类问题和回归问题分别构建了简单的试验框架。通过生成数据集计算得到, 在分类任务中无论特征质量如何变化, 在回归任务中同样如此: 随着特征质量逐渐降低时, 在两种任务中的 LogME 值都会持续下降。这表明 LogME 能够有效地评估特征与其标注之间的关系, 并因此成为一个合适的预训练模型选择指标

随后
将10种常用预训练模型迁移至9种典型分类数据集上,研究发现,在迁移过程中,Large margin entropy(LogME)与微调精度的相关性显著较高(如图所示),其性能明显优于Leap和Noise contrastive estimation(NCE)等现有方法.在这些数据集中,LogME的相关系数\tau_w至少达到0.5,而在大多数情况下则可达到0.7或更高数值,这表明,采用LogME进行预训练模型选择可以获得高达85%至90%的高准确率

在回归任务的实验设置中

从下图可以看出,在五个任务中,LogME出色地预测了四家预训练模型在各项指标中的相对水平,在其他两个领域中也有不错的效果。

LogME方法不仅效果显著, 更加珍贵的是它所需时间极为短暂, 可迅速完成预训练模型的评估工作. 以直接微调时间为基准, LogME仅需0.31‰的时间(注意不是百分号而是千分号), 即使加速了3000倍!相比之下, 前代方法如LEEP与NCE虽耗时较少, 但效果欠佳且适用范围受限, 完全不及LogME的优势所在:

TensorFlow2代码
import tensorflow as tf
from numba import njit
import numpy as np
@njit
def each_evidence(y_, f, fh, v, s, vh, N, D):
"""
compute the maximum evidence for each class
"""
alpha = 1.0
beta = 1.0
lam = alpha / beta
tmp = (vh @ (f @ y_))
for _ in range(11):
gamma = (s / (s + lam)).sum()
m = v @ (tmp * beta / (alpha + beta * s))
alpha_de = (m * m).sum()
alpha = gamma / alpha_de
beta_de = ((y_ - fh @ m) ** 2).sum()
beta = (N - gamma) / beta_de
new_lam = alpha / beta
if np.abs(new_lam - lam) / lam < 0.01:
break
lam = new_lam
evidence = D / 2.0 * np.log(alpha) \
+ N / 2.0 * np.log(beta) \
- 0.5 * np.sum(np.log(alpha + beta * s)) \
- beta / 2.0 * beta_de \
- alpha / 2.0 * alpha_de \
- N / 2.0 * np.log(2 * np.pi)
return evidence / N
# D = 20, N = 50
f_tmp = np.random.randn(20, 50).astype(np.float64)
each_evidence(np.random.randint(0, 2, 50).astype(np.float64), f_tmp, f_tmp.transpose(),
np.eye(20, dtype=np.float64), np.ones(20, dtype=np.float64), np.eye(20, dtype=np.float64), 50,
20)
def LogME(f: tf.Tensor, y: tf.Tensor, regression=False):
f = f.numpy().astype(np.float64)
y = y.numpy()
if regression:
y = y.numpy().astype(np.float64)
fh = f
f = f.transpose()
D, N = f.shape
v, s, vh = np.linalg.svd(f @ fh, full_matrices=True)
evidences = []
if regression:
K = y.shape[1]
for i in range(K):
y_ = y[:, i]
evidence = each_evidence(y_, f, fh, v, s, vh, N, D)
evidences.append(evidence)
else:
K = int(y.max() + 1)
for i in range(K):
y_ = (y == i).astype(np.float64)
evidence = each_evidence(y_, f, fh, v, s, vh, N, D)
evidences.append(evidence)
return np.mean(evidences)

