正样本和无标签学习(PU Learning):使用机器学习恢复数据的标签

作者:AaronWard

编译:ronghuaiyang

首发:AI公园公众号

导读

你有数据,但是标签并不可靠,你该怎么办?

问题

通常情况下,公司希望对给定的任务进行机器学习,比如对数据进行分类,但却面临数据标签不足或不可靠的问题。

在这些情况下,公司可以选择手工标签他们的数据,但手工标签可能是一项苛刻的任务,也可能导致人为偏见或重大错误。如果你为正样本贴上了数据标签,但为你的负样本贴上了不可靠的标签,那该怎么办?你如何解决这个问题?

正样本和无标签学习

数据集不足的示例,下面是一个例子:

  • 总共1000个样本
  • 其中100个样本你可以认为是可靠的正样本
  • 其中900个可能是不可靠的负样本或未标记的样本
  • 这些样本中可能会有一些是正样本

为了避免混淆,我将“未标记样本”和“不可靠的负样本”称为unknown。

方案

PU学习(positive and unlabelled learning)是一种半监督二值分类方法,它可以从数据中的未知情况中恢复标签。它是通过从数据中的正样本中学习,并应用所学到的知识来重新标记未知样本来做到这一点的。

这种方法为任何需要对不可靠数据进行二进制分类的机器学习问题提供了好处,而不考虑领域。

应用PU学习主要有两种方法。包括:

  • PU bagging
  • Two-step approach

PU Bagging 的解释

PU bagging是一种并行化的方法,它抽取unknown情况的随机子样本,并创建一个弱分类器集合来输出每个样本的分数。具体步骤包括:

  • 随机抽取unknown数据的子集和所有正样本,创建一个均衡的训练集,
  • 用这个“bootstrap”数据集构建一个分类器,将正样本视为1,将unknown视为0
  • 预测在训练中没有被采样的unknown样本的概率分数,称为袋外样本(OOB)
  • 重复多次,计算OOB的平均分

Two-Step Approach 的解释


two-step方法是一种更复杂的PU学习方法,它使用机器学习技术在训练时重新标记数据。实施步骤如下:

第一步

  • 训练一个正样本和unknown样本的标准分类器。
  • 得到一个确定的正样本的分数范围来标记确定的负样本

第二步

  • 在新标记的数据集上训练第二个分类器,反复重复这个过程,直到满足一个既定的标准。

PU Learning 实战

=================

为了展示这一点,我将使用Banknote dataset:http://archive.ics.uci.edu/ml…。它是一个有两个类的数据集:unauthenticauthentic,分别用0和1表示。

数据集的背景并不重要,因为我们不会做任何特征工程或在测试集上分类。相反,我们要模拟的情况有一些可靠的正样本和很多不可靠的负样本(可能是正负样本混在一起)。好,我们开始!💪

首先,我导入数据,查看原始标签值计数,看看是否有空值:
`

df_raw = pd.read_csv(‘data_banknote_authentication.txt’,

                 names=['variance', 'skewness', 'kurtosis', 'entropy', 'authentic'])

print(df_raw.authentic.value_counts())
print(‘Has null values:’, df_raw.isnull().values.any())

”’
Output:
0 762
1 610
Name: authentic, dtype: int64
Has null values: False
”’`

数据导入和检查

导入的数据的前几行

让我们模拟一个不可靠数据的场景 —— 首先,我们均匀地对数据集进行平衡,使其具有610个类0和610个类1。然后,我们将把一些正类错误地标记为负类(本质上是隐藏了它们),看看模型是否可以恢复它们。

def random_undersampling(tmp_df, TARGET_LABEL):
    df_majority = tmp_df[tmp_df[TARGET_LABEL] == 0]
    df_minority = tmp_df[tmp_df[TARGET_LABEL] == 1]

    # Downsample majority class
    df_majority_downsampled = resample(df_majority, 
                                       replace=False,              # sample without replacement
                                       n_samples=len(df_minority), # to match minority class
                                       random_state=None)        # reproducible results
    # Combine minority class with downsampled majority class
    df_downsampled = pd.concat([df_majority_downsampled, df_minority])

    print("Undersampling complete!")
    print(df_downsampled[TARGET_LABEL].value_counts())
    return df_downsampled
  
df_downsampled = random_undersampling(df_raw, 'authentic')
df_downsampled = df_downsampled.sample(frac=1) #Shuffle the data
df_downsampled = df_downsampled.reset_index() #Reset the index
df_downsampled = df_downsampled.drop(columns=['index']) # Drop original index col


# Make a new df because we will need that for later
df = df_downsampled.copy()

#Separate cols from label
NON_LBL = [c for c in df.columns if c != 'authentic']
X = df[NON_LBL]
y = df['authentic']

# Save the original labels and indices
y_orig = y.copy()
original_idx = np.where(df_downsampled.authentic == 1)

# Here we are imputing 300 positives as negative
hidden_size = 300
y.loc[
    np.random.choice(
        y[y == 1].index, 
        replace = False, 
        size = hidden_size
    )
] = 0

平衡数据并把标签标错

如上图所示,610个正样本中有300个被误标记为负。这样做的原因是我们想看看我们的PU学习者能否恢复它们。总结一下我所做的:

  • 1220样本及4个特征
  • 隐藏标签前是610个正样本
  • 隐藏标签后是310个正样本

伪类别不平衡

首先,让我们设定一个基准。我先训练一个标准的随机森林分类器,然后将结果与原始标签进行比较,看看恢复了多少。

#First random forest
rf = RandomForestClassifier(
    n_estimators = 50,  
    n_jobs = -1           
)
rf.fit(X, y)

print('---- {} ----'.format('Standard Random Forest'))
print(print_cm(sklearn.metrics.confusion_matrix(y_orig, rf.predict(X)), labels=['negative', 'positive']))
print('')
print('Precision: ', precision_score(y_orig, rf.predict(X)))
print('Recall: ', recall_score(y_orig, rf.predict(X)))
print('Accuracy: ', accuracy_score(y_orig, rf.predict(X)))
---- Standard Random Forest ----
                        pred_negative        pred_positive 
           true_negative        610.0          0.0 
           true_positive        300.0        310.0 
None

Precision:  1.0
Recall:  0.5081967213114754
Accuracy:  0.7540983606557377

正如你所看到的,标准随机森林在预测隐藏的正样本方面做得不是很好。只有50%的召回,这意味着它没有恢复任何隐藏的正样本。让我们通过进入PU bagging来进一步扩展。

PU Bagging

如果你回想一下上面的解释,PU bagging是一种并行训练多个集成分类器的方法。简而言之:它基本上就是一个集成方法。在每个集合中,类别数与正样本类的大小保持平衡。这个脚本:https://github.com/aaronward/… bagging实现,所以我们将使用它作为包装器:

bc = BaggingClassifierPU(RandomForestClassifier(n_estimators=20, random_state=2019), 
                         n_estimators = 50, 
                         n_jobs = -1, 
                         max_samples = sum(y)  # Each training sample will be balanced 
                        )
bc.fit(X, y)

print('---- {} ----'.format('Bagging PU'))
print(print_cm(sklearn.metrics.confusion_matrix(y_orig, bc.predict(X)), labels=['negative', 'positive']))
print('')
print('Precision: ', precision_score(y_orig, bc.predict(X)))
print('Recall: ', recall_score(y_orig, bc.predict(X)))
print('Accuracy: ', accuracy_score(y_orig, bc.predict(X)))
---- PU Bagging ----
                        pred_negative        pred_positive 
           true_negative        610.0          0.0 
           true_positive         32.0        578.0 
None

Precision:  1.0
Recall:  0.9475409836065574
Accuracy:  0.9737704918032787

该方法在610个样本(95%召回)中召回了了578个正样本。这个结果是很好的,看一下可视化:

PU Bagging预测正样本的数量

想象一下,在一个具有数百万行不可靠标记的巨大数据集上实现这一点。能够有效地召回它们是一项非常有用的技术,是不是很酷?👌🏼

Two-Step方法

对于两步方法,它的实现要比打包稍微复杂一些。最简单地说,我们需要做的是:

  • 确定可以确定标记为负样本的数据子集(可靠的负样本)。
  • 使用可靠的负样本和可靠的正样本训练分类器,并使用这个分类器来标记未知样本。

但在数据集中并不总是存在可靠的负样本的情况。只有正样本和未知样本。为了减轻这个问题,你需要:

  • 训练一个正样本和未知样本的分类器。
  • 使用predict_proba()函数创建一个概率范围,对于正样本最低到最高的得分就是这个范围。
  • 将所有落入该分数范围的样本重新标记为样本和负样本。
  • 在新标记的数据上训练第二个分类器

首先,我们将创建一个新的目标向量,其中1表示正样本,-1表示未知样,0表示可靠的负样本,开始时没有可靠的负样本。

print('Converting unlabaled to -1 and positive to 1...')
ys = 2 * y - 1

# Get the scores from before
pred = rf.predict_proba(X)[:,1]

# Find the range of scores given to positive data points
range_pos = [min(pred * (ys > 0)), max(pred * (ys > 0))]

print('Relabelling unknowns in score range as positive...')
# STEP 1
# If any unlabeled point has a score above all known positives, 
# or below all known positives, label it accordingly
iP_new = ys[(ys < 0) & (pred >= range_pos[1])].index
iN_new = ys[(ys < 0) & (pred <= range_pos[0])].index
ys.loc[iP_new] = 1
ys.loc[iN_new] = 0

TSA步骤1

现在我们已经完成了第一步,我们可以继续进行第二步。为此,我们需要初始化一个分类器(在本例中是一个标准随机森林),然后用正样本的新概率范围迭代地重新训练模型。

tsa = RandomForestClassifier(n_estimators = 50, n_jobs = -1)

for i in range(15):
    print('Iteration: ', i)
    # If step 1 didn't find new labels, we're done
    if len(iP_new) + len(iN_new) == 0 and i > 0:
        break
    
    print('Step 1 labeled %d new positives and %d new negatives.' % (len(iP_new), len(iN_new)))
    print('Doing step 2... ', end = '')
    
    # STEP 2
    # Retrain on new labels and get new scores
    tsa.fit(X, ys)
    pred = tsa.predict_proba(X)[:,-1]
    
    # Find the range of scores given to positive data points
    range_P = [min(pred * (ys > 0)), max(pred * (ys > 0))]
    
    # Repeat step 1
    iP_new = ys[(ys < 0) & (pred >= range_P[1])].index
    iN_new = ys[(ys < 0) & (pred <= range_P[0])].index
    ys.loc[iP_new] = 1
    ys.loc[iN_new] = 0

当分类器达到以下两个条件之一时,这就完成了:找不到到任何新的标签,或者当循环完成。但是,我应该补充一点,当在具有大参数的大数据集(_n\_estimators_)上使用此方法时,这种训练可能会花费很长时间。我们来看看效果如何。

print('---- {} ----'.format('TSA'))
y_hat_val = tsa.predict(X)
y_hat_val = [x if ((x==0) or (x==1)) else 0 for x in y_hat_val] # Convert leftover unknowns as negative

print(print_cm(sklearn.metrics.confusion_matrix(y_orig, y_hat_val), 
labels=['negative', 'positive']))
print('')
print('Precision: ', precision_score(y_orig, y_hat_val))
print('Recall: ',    recall_score(y_orig, y_hat_val))
print('Accuracy: ',  accuracy_score(y_orig, y_hat_val))

因为这个RF是在3个类上训练的,我们将-1转换为0。

---- TSA ----
                        pred_negative        pred_positive 
           true_negative        610.0          0.0 
           true_positive        300.0        310.0 
None

Precision:  1.0
Recall:  0.5081967213114754
Accuracy:  0.7540983606557377

TSA的输出

在这种情况下,TSA模型的表现不如PU bagging模型。然而,通过一些特征工程和更多的数据,可以证明这种方法并不像乍看之下那么无用。

PU Learning 可以用在哪里?

  • 这些方法可以应用于许多领域,其中一个例子是检测垃圾邮件 — 你想要在未知样本中发现垃圾邮件的正样本(即:刚开始认为不是垃圾邮件的垃圾邮件)。
  • 它也可以用于推荐系统 — 假设一个人使用你的网站卖衣服。有一种功能可以让用户喜欢他们感兴趣的某些物品。我们可以使用客户喜欢的产品(真正的正样本)数据来发现他们可能喜欢的新产品(潜在的正样本)。
  • 这只是举几个例子,还有许多其他领域可以使用此方法。

总结

并不是所有的数据集都被正确的标记。更糟糕的是,一些数据集可能根本没有标签,但通过一些领域知识和弱监管,依然可以实现一个有用的机器学习模型。

  • PU bagging比普通的集成方法(基于树的算法)更快,因为它利用了并行化。
  • TSA需要更长的时间来训练。
  • 请记住:这是一个预处理步骤,与提供更好结果的深度学习方法相比,最好不要依赖于此来处理分类问题。

那么你现在能做什么呢?如果你的数据包含不可靠的类别,你可以使用其中一个模型来预测正样本类的概率,然后根据阈值重新分配一个新标签(例如:If probability > 90%就标记这一行为正样本)。

—END—

英文原文:https://heartbeat.fritz.ai/po…

推荐阅读

  • 知识图谱嵌入的Translate模型汇总(TransE,TransH,TransR,TransD)
  • 现代NLP中的零样本学习
  • 12中主要的Dropout方法:如何应用于DNNs,CNNs,RNNs中的数学和可视化解释
  • MLOps介绍:机器学习技术债
  • 机器学习中的七宗罪

关注图像处理,自然语言处理,机器学习等人工智能领域,请点击关注AI公园专栏
欢迎关注微信公众号

发表评论

邮箱地址不会被公开。 必填项已用*标注