【译文】伪标签学习导论 - 一种半监督学习方法

【译文】伪标签学习导论 - 一种半监督学习方法

作者 SHUBHAM JAIN

译者 钱亦欣

引言

在有监督学习领域,我们已经取得了长足的进步,但这也意味着我们需要大量数据来做图像分类和销量预测,这些算法需要把这些数据扫描一遍又一遍来寻找模式。

.然而,这其实不是人类的学习方法,我们的大脑不需要成千上万的数据循环往复地学习来了解一类图片的主题,我们只需要少量的特征点来习得模式,所以现有的机器学习方法是有所缺陷的。

好在现有已经有一些针对这个问题的研究,我们或许可以构建一个系统,它只需要最少量的监督数据输入但能学得每个任务的主要模式。本文将会介绍其中一种名为伪标签学习的方法,我会深入浅出的讲解原理并演示一个案例。

走起!

注:我既定你已经对机器学习又基本了解,如果没有请学习相关知识再看本文。


目录

  1. 什么是半监督学习 (SSL)?
  2. 如何利用无标签数据
  3. 伪标签学习
  4. 半监督学习的应用
  5. 采样率的作用
  6. 半监督学习的应用场景

1. 什么是半监督学习 (SSL) ?

假设我们目前面临一个简单的图像分类问题,我们的数据有两类标签(如下所示)。

我们的目标就是区分图像中有无日食,现在的问题就是如何仅从两幅图片的信息中构建一个分类系统。

一般而言,为了构建一个稳定的分类系统我们需要更多数据,我们从网上下载了更多相关图片来扩充我们的训练集。

但是,如果从监督学习的方法出发,我们还要给这些图片贴上标签,因此我们要借助人工完成这个过程。

基于这些数据运行了监督学习的算法,我们的模型表现显著高于那个仅基于两张图片的算法。

但是这个方法只在任务量不大的时候起效,数据量一大继续人工介入会消耗大量资源。

为了解决这一类问题,我们定义了一种名为半监督学习的方法,能从有标签(监督学习)和无标签数据(无监督学习)中共同习得模式。

来源: 链接

因此,现在就让我们学习下如何利用无标签数据。


2. 如何利用无标签数据?

考虑如下的情形

我们只有分属两个类别的两个数据点,途中的线代表着任意有监督模型的决策边界。

现在,让我们再途中加一些无标签数据,如下所示。

图片来源: 链接

如果我们注意到两幅图的差异,我们就可以发现有了无标签数据后,两个类别的决策边界变地更加精确了。

因此,使用无标签数据的优点如下:

  1. 有标签数据往往意味着高成本和难以获得,但无标签数据量大又便宜。
  2. 通过提高决策边界的精确性,它们能提高模型的稳健性。

现在,我们对于半监督学习已经有了直观的认识,当然这个领域也有许多种方法,本文就介绍其中的伪标签学习法。


3. 伪标签学习

这一技术,让我们不必再手工标注无标签数据,我们只需要基于有标签数据的技术来给出一个近似的标签,我们把这个过程分成多个步骤逐一介绍。

来源: 链接

我假设你已经看懂了上图的流程,第三步训练的模型将会用来给测试集分类。

为了让你有更好地理解,我会结合一个实际问题来讲解原理。


4. 半监督学习的应用

此处我们使用 Big Mart Sales 问题作为例子。我们先下载这个数据集并做一些探索。

先加载一些基础的库

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
from sklearn.preprocessing import LabelEncoder

之后读入下载好的训练集和测试机,并作简单的预处理

train = pd.read_csv('/Users/shubhamjain/Downloads/AV/Big Mart/train.csv')
test = pd.read_csv('/Users/shubhamjain/Downloads/AV/Big Mart/test.csv')
# 预处理

### 均值插值
train['Item_Weight'].fillna((train['Item_Weight'].mean()), inplace=True)
test['Item_Weight'].fillna((test['Item_Weight'].mean()), inplace=True)

### 把脂肪含量这一变量变为二分类
train['Item_Fat_Content'] = train['Item_Fat_Content'].replace(['low fat','LF'], ['Low Fat','Low Fat']) 
train['Item_Fat_Content'] = train['Item_Fat_Content'].replace(['reg'], ['Regular']) 
test['Item_Fat_Content'] = test['Item_Fat_Content'].replace(['low fat','LF'], ['Low Fat','Low Fat']) 
test['Item_Fat_Content'] = test['Item_Fat_Content'].replace(['reg'], ['Regular'])

## 计算创立年份
train['Outlet_Establishment_Year'] = 2013 - train['Outlet_Establishment_Year'] 
test['Outlet_Establishment_Year'] = 2013 - test['Outlet_Establishment_Year']

### 查补 size 变量的缺失值
train['Outlet_Size'].fillna('Small',inplace=True)
test['Outlet_Size'].fillna('Small',inplace=True)

### 给 cate. var. 编码
col = ['Outlet_Size','Outlet_Location_Type','Outlet_Type','Item_Fat_Content']
test['Item_Outlet_Sales'] = 0
combi = train.append(test)
number = LabelEncoder()
for i in col:
    combi[i] = number.fit_transform(combi[i].astype('str'))
    combi[i] = combi[i].astype('int')
train = combi[:train.shape[0]]
test = combi[train.shape[0]:]
test.drop('Item_Outlet_Sales',axis=1,inplace=True)

## 去除 id 这一变量 
training = train.drop(['Outlet_Identifier','Item_Type','Item_Identifier'],axis=1)
testing = test.drop(['Outlet_Identifier','Item_Type','Item_Identifier'],axis=1)
y_train = training['Item_Outlet_Sales']
training.drop('Item_Outlet_Sales',axis=1,inplace=True)

features = training.columns
target = 'Item_Outlet_Sales'

X_train, X_test = training, testing

训练不同的有监督模型,选取效果最好的那一个

from xgboost import XGBRegressor
from sklearn.linear_model import BayesianRidge, Ridge, ElasticNet
from sklearn.neighbors import KNeighborsRegressor
from sklearn.ensemble import RandomForestRegressor, ExtraTreesRegressor, GradientBoostingRegressor
#from sklearn.neural_network import MLPRegressor

from sklearn.metrics import mean_squared_error
from sklearn.model_selection import cross_val_score
model_factory = [
    RandomForestRegressor(),
    XGBRegressor(nthread=1),
    #MLPRegressor(),
    Ridge(),
    BayesianRidge(),
    ExtraTreesRegressor(),
    ElasticNet(),
    KNeighborsRegressor(),
    GradientBoostingRegressor()
]

for model in model_factory:
    model.seed = 42
    num_folds = 3

scores = cross_val_score(model, X_train, y_train, cv=num_folds, scoring='neg_mean_squared_error')
score_description = " {} (+/- {})".format(np.sqrt(scores.mean()*-1), scores.std() * 2)

print('{model:25} CV-5 RMSE: {score}'.format(
    model=model.__class__.__name__,
    score=score_description
 ))

可以发现XGB的表现最好,同时请注意伪了方便我没有调参。

现在让我们使用伪标签学习法,我将把测试集作为无标签数据。

from sklearn.utils import shuffle
from sklearn.base import BaseEstimator, RegressorMixin


class PseudoLabeler(BaseEstimator, RegressorMixin):
    '''
    伪标签学习中的 sci-kit learn 算法封装
    '''

    def __init__(self, model, unlabled_data, features, target, sample_rate=0.2, seed=42):
        '''
        @采样率 - 无标签样本中被用作伪标签样本的比率
        '''
        
        assert sample_rate <= 1.0, 'Sample_rate should be between 0.0 and 1.0.'

        self.sample_rate = sample_rate
        self.seed = seed
        self.model = model
        self.model.seed = seed

        self.unlabled_data = unlabled_data
        self.features = features
        self.target = target

    def get_params(self, deep=True):
        return {
            "sample_rate": self.sample_rate,
            "seed": self.seed,
            "model": self.model,
            "unlabled_data": self.unlabled_data,
            "features": self.features,
            "target": self.target
        }

    def set_params(self, **parameters):
        for parameter, value in parameters.items():
            setattr(self, parameter, value)
        return self

    def fit(self, X, y):
        '''
        用伪标签样本填充数据集
        '''

        augemented_train = self.__create_augmented_train(X, y)
        self.model.fit(
            augemented_train[self.features],
            augemented_train[self.target]
        )

        return self

    def __create_augmented_train(self, X, y):
        '''
        生成并返回包括标签样本和伪变迁样本的 augmented_train 集合
        '''
        num_of_samples = int(len(self.unlabled_data) * self.sample_rate)

        # 训练模型并生成伪标签
        self.model.fit(X, y)
        pseudo_labels = self.model.predict(self.unlabled_data[self.features])

        # 在测试集中加入伪标签
        pseudo_data = self.unlabled_data.copy(deep=True)
        pseudo_data[self.target] = pseudo_labels

        # 将测试集中又伪标签的部分数据合并入训练集
        sampled_pseudo_data = pseudo_data.sample(n=num_of_samples)
        temp_train = pd.concat([X, y], axis=1)
        augemented_train = pd.concat([sampled_pseudo_data, temp_train])

        return shuffle(augemented_train)

    def predict(self, X):
        '''
        返回预测值
        '''
        return self.model.predict(X)

    def get_model_name(self):
        return self.model.__class__.__name__

这看起来很复杂,不过不用担心其实就和前面介绍的流程一毛一样,所以每次复制上面的代码就可以投入到别的用例了。

现在让我们来检测下伪标签学习的效果。

model_factory = [
    XGBRegressor(nthread=1),
    PseudoLabeler(
        XGBRegressor(nthread=1),
        test,
        features,
        target,
        sample_rate=0.3
    ),
]

for model in model_factory:
    model.seed = 42
    num_folds = 8
    scores = cross_val_score(model, X_train, y_train, cv=num_folds, scoring='neg_mean_squared_error', n_jobs=8)
    score_description = "MSE: {} (+/- {})".format(np.sqrt(scores.mean()*-1), scores.std() * 2)

print('{model:25} CV-{num_folds} {score_cv}'.format(
    model=model.__class__.__name__,
    num_folds=num_folds,
    score_cv=score_description
))

本例中,我们得到了一个比任何有监督学习算法都小的rmse值。

你或许已经注意到 采样比例(sample_rate)这个参数,它代表无标签数据中本用作伪标签样本的比率。

下面我就就来测试下这个参数对伪标签学习法预测表现的影响。


5. 采样比例的作用

为了说明这个参数的作用,我们来画个图。时间有限,我就只花了两个算法的图,你自己可以试试别的。

sample_rates = np.linspace(0, 1, 10)

def pseudo_label_wrapper(model):
    return PseudoLabeler(model, test, features, target)

# 受试模型列表
model_factory = [
    RandomForestRegressor(n_jobs=1),
    XGBRegressor(),
]

# 对每个模型使用伪标签法
model_factory = map(pseudo_label_wrapper, model_factory)

# 用不同的采样率训练模型
results = {}
num_folds = 5

for model in model_factory:
    model_name = model.get_model_name()
    print('{}'.format(model_name))

    results[model_name] = list()
    for sample_rate in sample_rates:
        model.sample_rate = sample_rate

    # 计算 CV-3 R2 得分
        scores = cross_val_score(
        model, X_train, y_train, cv=num_folds, scoring='neg_mean_squared_error', n_jobs=8)
        results[model_name].append(np.sqrt(scores.mean()*-1))
plt.figure(figsize=(16, 18))

i = 1
for model_name, performance in results.items():
    plt.subplot(3, 3, i)
    i += 1

    plt.plot(sample_rates, performance)
    plt.title(model_name)
    plt.xlabel('sample_rate')
    plt.ylabel('RMSE')

plt.show()


如此一来,我们发现对于不同的模型我们都要选取特定的采样率来达到最佳的效果,因而调节这个参数就显得很有必要了。

6. 半监督学习的应用场景

过去,半监督学习的应用场景着实有限,但如今在不少领域已经出现了它的身影。我找到的场景如下:

1. 图片分类中的多模式半监督学习

通常来讲,图片分类的目标是判断一幅图是否属于一个特定类别。但在这篇文章里,不光是图片本身,与之相关联的关键词夜奔由来提升半监督学习的分类质量。

来源: 链接

2. 在人口贩卖中的作用

人口贩卖一直都是最恶劣的犯罪行为之一,需要全球一起关注。使用半监督学习可以比普通的方法提供更好的结果。

来源:链接


结语

我希望你现在对班监督学习有了初步认识,并知道如何再具体问题中应用它了。不妨试着学习其他的半监督算法并和大家分享吧。

本文的所有的代码可以再我的 github 上找到。

原文链接

文章被以下专栏收录

    这是一个时不时就发点翻译的文章的专栏,更新什么的完全看心情。前期会搬运以前发在图灵社区的译文,后期随缘。 可能会发点我个人的随笔啊,感悟什么的。不过我文笔不好,要求高的你们就别点开看了。 当然也欢迎原创投稿,稿费就别想了,毕竟我比你们大多数人都穷。。。 此致敬礼,么么哒!