MENU

对抗验证:划分一个跟测试集更接近的验证集

July 4, 2021 • Read: 2980 • Deep Learning阅读设置

不论是打比赛、做实验还是搞工程,我们经常会遇到训练集与测试集分布不一致的情况。一般来说,我们会从训练集中划分出一个验证集,通过这个验证集来调整一些超参数,并保存在验证集上效果最好的模型。然而,如果验证集本身和测试集差别比较大,那么在验证集上表现很好的模型不一定在测试集上表现同样好,因此如何让划分出来的验证集跟测试集的分布差异更小,是一个值得研究的课题

两种情况

首先明确一点,本文所考虑的,是能拿到测试集数据、但不知到测试集标签的场景。如果是那种提交模型封闭评测的场景,我们完全看不到测试集的就没什么办法了。为什么会出现测试集跟训练集分布不一致的现象呢?主要有两种情况

  • 一是标签的分布不一致。也就是说,如果只看输入$x$,那么分布基本上是差不多的,但是对应的$y$分布不一样,典型的例子就是信息抽取任务(例如关系抽取),训练集和验证集的输入$x$都是某一领域的文本,所以他们的分布很接近。但是训练集往往是通过"远程监督+人工粗标"的方式构建的,里面的错漏比较多,而测试集可能是通过"人工反复精标"构建的,错漏很少。这种情况下就无法通过划分数据的方式构建一个很好的验证集了
  • 二是输入的分布不一致。说白了就是$x$的分布不一致,但$y$的标注情况基本上是正确的。比如分类问题中,训练集的类别分布跟测试集的类别分布可能不一样;又或者在阅读理解问题中,训练集的事实类/非事实类题型比例跟测试集不一样,等等。这种情况下我们可以适当调整采样策略,让验证集跟测试集分布更接近,从而使得验证集的结果能够更好的反应测试集的结果

Adversarial Validation

Adversarial Validation网上的翻译是对抗验证,它并不是一种评估模型的方法,而是一种用来验证训练集和测试集分布是否一致、找出影响数据分布不一致的特征、从训练集中找出一部分与测试集分布接近的数据。不过实际上有些时候我们并不需要找出影响数据分布不一致的特征,因为可能这个数据集只有一个特征,例如对于nlp的很多任务来说,就只有一个文本,因此也就只有一个特征。对抗验证的核心思想是:

训练一个判别器来区分训练/测试样本,之后将这个判别器应用到训练集中,在训练集中,选取被预测为测试样本的Top n个数据作为验证集,因为这些数据是最模型认为最像测试集的数据

判别器

我们首先让训练集的标签为0,测试集的标签为1,训练一个二分类判别器$D(x)$:

$$ -\mathbb{E}_{x\sim p(x)}[\log (1-D(x))]-\mathbb{E}_{x\sim q(x)}[\log D(x)]\tag{1} $$

其中$p(x)$代表了训练集的分布,$q(x)$则是测试集的分布。要注意的是,我们应该分别从训练集和测试集采样同样多的样本来组成每一个batch,也就是说需要采样到类别均衡

可能有读者担心过拟合问题,即判别器彻底地将训练集和测试集分开了,这样的话我们要找出训练集中top n个最像测试集的样本,就找不出来了。事实上,在训练判别器的时候,我们应该也要像普通的监督训练一样,划分个验证集出来,通过验证集决定训练的epoch数,这样就不会严重过拟合了;或者像网上有些案例一样,用一些简单的回归模型做判别器,这样就不太容易过拟合了

与GAN的判别器类似,不难推导$D(x)$的理论最优解是

$$ D(x)=\frac{q(x)}{p(x)+q(x)}\tag{2} $$

也就是说,判别器训练完后,可以认为它就等于测试集分布的相对大小

代码

以下代码利用AUC指标判别两个数据集的分布是否接近,越接近0.5表示他们的分布越相似。网上对抗验证的代码,大部分是针对于numerical的数据,很少有针对于nlp文本类型数据的代码,对于nlp文本类型的数据,应该先将文本特征转为向量再进行操作。代码并不全面,例如没有实现从训练集中抽取Top n接近测试集的样本

import sklearn
import numpy as np
import pandas as pd
import lightgbm as lgb
from sklearn.feature_extraction.text import TfidfVectorizer

df = pd.read_csv('data.csv')
df = df.sample(frac=1).reset_index(drop=True)

df_train = df[:int(len(df) * 0.7)]
df_test = df[int(len(df) * 0.7):]

col = 'text'

tfidf = TfidfVectorizer(ngram_range=(1, 2), max_features=50).fit(df_train[col].iloc[:].values)
train_tfidf = tfidf.transform(df_train[col].iloc[:].values)
test_tfidf = tfidf.transform(df_test[col].iloc[:].values)

train_test = np.vstack([train_tfidf.toarray(), test_tfidf.toarray()]) # new training data
lgb_data = lgb.Dataset(train_test, label=np.array([0]*len(df_train)+[1]*len(df_test)))
params = {
    'max_bin': 10,
    'learning_rate': 0.01,
    'boosting_type': 'gbdt',
    'metric': 'auc',
}
result = lgb.cv(params, lgb_data, num_boost_round=100, nfold=3, verbose_eval=20)
print(pd.DataFrame(result))

References

Archives Tip
QR Code for this page
Tipping QR Code