先验与半监督学习

作者: Jacob Schreiber 联系方式: jmschreiber91@gmail.com

大多数经典机器学习算法要么假设整个数据集已标注(监督学习),要么假设完全没有标签(无监督学习)。然而实际情况往往是:存在部分标注数据,同时还有大量未标注数据。计算机视觉领域就是典型例子——互联网上充斥着大量图片(主要是猫图),这些数据可能很有价值,但您既没有时间也没有资金根据具体任务为所有图片打标签。最终通常会出现两种情况:要么直接丢弃未标注数据,仅用标注数据训练模型;要么先用标注数据初始化无监督模型,再将其应用于未标注数据。这两种方法都未能在优化过程中同时利用两类数据。

半监督学习是一种将标记数据和未标记数据同时纳入训练任务的方法,通常能比仅使用标记数据训练出性能更优的估计器。半监督学习可采用多种方法实现,scikit-learn文档对这些技术有很好的阐述。

pomegranate 原生支持半监督学习,通过为大多数类接受一个先验概率矩阵来实现。具体来说,先验概率给出了一个样本映射到模型中每个组件的概率,例如混合模型中的某个分布。当这个概率为1.0时,它实际上相当于一个硬标签,表示某个样本必须来自该分布。如果所有样本对某个组件的先验概率都是1.0,这就是完全的监督学习。如果部分样本的先验概率为1.0而其他样本是均匀分布的,这就对应于半监督学习。当存在软证据时,这些先验概率可以介于均匀分布和1.0之间,以表示一定程度的置信度。

需要注意的是,先验概率与软目标不同,因为训练目的不是按比例将每个点分类到各个分布中。例如,如果给定模型的示例先验概率为[0.4, 0.6],学习目标并不是让基础分布对该示例产生[0.4, 0.6]的结果,而是通过贝叶斯规则将这些值乘以各分布的似然概率。

让我们来看看!

[1]:
%pylab inline
import seaborn; seaborn.set_style('whitegrid')

import torch

numpy.random.seed(0)
numpy.set_printoptions(suppress=True)

%load_ext watermark
%watermark -m -n -p numpy,scipy,torch,pomegranate
Populating the interactive namespace from numpy and matplotlib
numpy      : 1.23.4
scipy      : 1.9.3
torch      : 1.13.0
pomegranate: 1.0.0

Compiler    : GCC 11.2.0
OS          : Linux
Release     : 4.15.0-208-generic
Machine     : x86_64
Processor   : x86_64
CPU cores   : 8
Architecture: 64bit

简单设置

首先,我们生成一些紧密聚集的团状数据。通常情况下,未标记数据往往远多于已标记数据,假设一个人只有50个已标记训练样本和4950个未标记样本。在pomegranate中,可以通过将标签指定为整数-1来表示样本缺少标签,这与scikit-learn中的做法一致。同时假设标记样本中存在一些偏差,为问题引入噪声,否则即使只有少量样本,高斯团也能被轻易建模。

[2]:
from sklearn.datasets import make_blobs
numpy.random.seed(1)

X, y = make_blobs(10000, n_features=2, centers=3, cluster_std=2)
X_train = X[:5000].astype('float32')
y_train = y[:5000]

# Set the majority of samples to unlabeled.
y_train[numpy.random.choice(5000, size=4950, replace=False)] = -1

# Inject noise into the problem
X_train[y_train != -1] += 2.5

X_test = X[5000:].astype('float32')
y_test = y[5000:]


plt.figure(figsize=(6, 6))
plt.scatter(X_train[y_train == -1, 0], X_train[y_train == -1, 1], color='0.6', s=5)
plt.scatter(X_train[y_train == 0, 0], X_train[y_train == 0, 1], color='c', s=15)
plt.scatter(X_train[y_train == 1, 0], X_train[y_train == 1, 1], color='m', s=15)
plt.scatter(X_train[y_train == 2, 0], X_train[y_train == 2, 1], color='r', s=15)
plt.axis('off')
plt.show()
../_images/tutorials_C_Feature_Tutorial_4_Priors_and_Semi-supervised_Learning_4_0.png

对于我们来说,未标记数据的聚类分布看起来很明显,而已标记数据似乎并不完全符合这些聚类模式。这种形式的分布偏移在半监督学习场景中也经常出现,因为被标记的数据有时会存在偏差——可能是由于选择容易标注的数据进行标记,或者标记过程本身存在倾向性。不过,由于我们拥有大量未标记数据,通常能够克服这类偏移问题。

现在让我们尝试将一个简单的贝叶斯分类器拟合到标记数据上,并将其准确率和决策边界与半监督方式拟合高斯混合模型时的情况进行比较。如前所述,我们可以通过传入一个先验概率矩阵来进行半监督学习,当标签未知时该矩阵为均匀分布,而当标签已知时,对应类别的概率值为1.0。

[3]:
from pomegranate.gmm import GeneralMixtureModel
from pomegranate.bayes_classifier import BayesClassifier
from pomegranate.distributions import Normal

idx = y_train != -1

model_a = BayesClassifier([Normal(), Normal(), Normal()])
model_a.fit(X_train[idx], y_train[idx])

y_hat_a = model_a.predict(X_test).numpy()
print("Supervised Learning Accuracy: {}".format((y_hat_a == y_test).mean()))

priors = torch.zeros(len(X_train), 3)
for i, y in enumerate(y_train):
    if y != -1:
        priors[i, y] = 1.0
    else:
        priors[i] = 1./3

dists = []
for i in range(3):
    dists.append(Normal().fit(X_train[y_train == i]))

model_b = GeneralMixtureModel(dists)
model_b.fit(X_train, priors=priors)

y_hat_b = model_b.predict(X_test).numpy()
print("Semisupervised Learning Accuracy: {}".format((y_hat_b == y_test).mean()))
Supervised Learning Accuracy: 0.8842
Semisupervised Learning Accuracy: 0.9206

看起来,当我们使用半监督学习时,测试集准确率有了显著提升。需要注意的是,由于先验概率的平滑特性,分布最初并不会根据硬标签进行初始化。不过,您可以像上面那样手动操作,即首先将分布拟合到标记数据,然后根据给定的先验概率在整个数据集上进行微调。让我们可视化数据,以便更好地理解这里发生的情况。

[4]:
def plot_contour(X, y, Z):
    plt.scatter(X[y == -1, 0], X[y == -1, 1], color='0.6', s=5)
    plt.scatter(X[y == 0, 0], X[y == 0, 1], color='c', s=15)
    plt.scatter(X[y == 1, 0], X[y == 1, 1], color='m', s=15)
    plt.scatter(X[y == 2, 0], X[y == 2, 1], color='r', s=15)
    plt.contour(xx, yy, Z)
    plt.xlim(x_min, x_max)
    plt.ylim(y_min, y_max)
    plt.xticks(fontsize=14)
    plt.yticks(fontsize=14)

x_min, x_max = X[:,0].min()-2, X[:,0].max()+2
y_min, y_max = X[:,1].min()-2, X[:,1].max()+2
xx, yy = numpy.meshgrid(numpy.arange(x_min, x_max, 0.1), numpy.arange(y_min, y_max, 0.1))
Z1 = model_a.predict(numpy.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
Z2 = model_b.predict(numpy.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)

plt.figure(figsize=(16, 16))
plt.subplot(221)
plt.title("Training Data, Supervised Boundaries", fontsize=16)
plot_contour(X_train, y_train, Z1)

plt.subplot(223)
plt.title("Training Data, Semi-supervised Boundaries", fontsize=16)
plot_contour(X_train, y_train, Z2)

plt.subplot(222)
plt.title("Test Data, Supervised Boundaries", fontsize=16)
plot_contour(X_test, y_test, Z1)

plt.subplot(224)
plt.title("Test Data, Semi-supervised Boundaries", fontsize=16)
plot_contour(X_test, y_test, Z2)
plt.show()
../_images/tutorials_C_Feature_Tutorial_4_Priors_and_Semi-supervised_Learning_8_0.png

等高线图展示了不同类别之间的决策边界,左侧图形对应部分标记的训练集,右侧图形对应测试集。可以看到,仅使用标记数据学习得到的边界在考虑未标记数据时显得不太合理,尤其是未能清晰地将青色簇与其他两个簇区分开。此外,洋红色与红色簇之间的边界呈现不自然的弯曲形态——我们不会认为落在(-18, -7)附近的点实际上属于红色类别。通过半监督方式训练的模型解决了这两个问题,学习到了更优、更平缓且更具泛化能力的边界。

接下来我们比较训练时间,看看半监督学习比简单的监督学习慢多少。

[5]:
from sklearn.semi_supervised import LabelPropagation

print("Supervised Learning")
%timeit BayesClassifier([Normal(), Normal(), Normal()]).fit(X_train[idx], y_train[idx])
print()

print("Semi-supervised Learning")
%timeit GeneralMixtureModel([Normal(), Normal(), Normal()]).fit(X_train, priors=priors)
print()

print("Label Propagation (sklearn): ")
%timeit -n 1 -r 1 LabelPropagation().fit(X_train, y_train) # Setting to 1 loop because it takes a long time
Supervised Learning
3.13 ms ± 304 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Semi-supervised Learning
444 ms ± 59.3 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Label Propagation (sklearn):
1min ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
/home/jmschr/anaconda3/lib/python3.9/site-packages/sklearn/semi_supervised/_label_propagation.py:316: ConvergenceWarning: max_iter=1000 was reached without convergence.
  warnings.warn(

在这个示例中,半监督学习比简单的监督学习要慢得多。这是预料之中的,因为贝叶斯分类器的简单监督更新只是对每个分布进行简单的最大似然估计(MLE),而半监督情况则需要使用迭代算法EM来收敛。不过,使用EM进行半监督学习仍然比拟合sklearn中的标签传播估计器快得多。

更复杂的设置

虽然之前的设置已经说明了我们的观点,但它仍然相当简单。我们可以构建一个更复杂的情境,其中包含复杂的高斯分布,并且每个分量都是分布的混合体而非单一分布。这将突显半监督学习的强大能力,以及pomegranate堆叠模型的优势——在本例中,就是在混合模型中嵌套另一个混合模型。

首先让我们生成一些更复杂、噪声更多的数据。

[6]:
X = numpy.empty(shape=(0, 2))
X = numpy.concatenate((X, numpy.random.normal(4, 1, size=(3000, 2)).dot([[-2, 0.5], [2, 0.5]])))
X = numpy.concatenate((X, numpy.random.normal(3, 1, size=(6500, 2)).dot([[-1, 2], [1, 0.8]])))
X = numpy.concatenate((X, numpy.random.normal(7, 1, size=(8000, 2)).dot([[-0.75, 0.8], [0.9, 1.5]])))
X = numpy.concatenate((X, numpy.random.normal(6, 1, size=(2200, 2)).dot([[-1.5, 1.2], [0.6, 1.2]])))
X = numpy.concatenate((X, numpy.random.normal(8, 1, size=(3500, 2)).dot([[-0.2, 0.8], [0.7, 0.8]])))
X = numpy.concatenate((X, numpy.random.normal(9, 1, size=(6500, 2)).dot([[-0.0, 0.8], [0.5, 1.2]])))
X = X.astype('float32')

x_min, x_max = X[:,0].min()-2, X[:,0].max()+2
y_min, y_max = X[:,1].min()-2, X[:,1].max()+2

y = numpy.concatenate((numpy.zeros(9500), numpy.ones(10200), numpy.ones(10000)*2)).astype('int32')
idxs = numpy.arange(29700)
numpy.random.shuffle(idxs)

X = X[idxs]
y = y[idxs]

X_train, X_test = X[:25000], X[25000:]
y_train, y_test = y[:25000], y[25000:]
y_train[numpy.random.choice(25000, size=24920, replace=False)] = -1

plt.figure(figsize=(6, 6))
plt.scatter(X_train[y_train == -1, 0], X_train[y_train == -1, 1], color='0.6', s=1)
plt.scatter(X_train[y_train == 0, 0], X_train[y_train == 0, 1], color='c', s=10)
plt.scatter(X_train[y_train == 1, 0], X_train[y_train == 1, 1], color='m', s=10)
plt.scatter(X_train[y_train == 2, 0], X_train[y_train == 2, 1], color='r', s=10)
plt.axis('off')
plt.show()
../_images/tutorials_C_Feature_Tutorial_4_Priors_and_Semi-supervised_Learning_12_0.png

现在让我们来看看仅使用标记样本训练模型与以半监督方式使用所有样本训练模型所获得的准确率。

[7]:
d1 = GeneralMixtureModel([Normal(), Normal()])
d2 = GeneralMixtureModel([Normal(), Normal()])
d3 = GeneralMixtureModel([Normal(), Normal()])

model_a = BayesClassifier([d1, d2, d3])
model_a.fit(X_train[y_train != -1], y_train[y_train != -1])

y_hat_a = model_a.predict(X_test).numpy()
print("Supervised Learning Accuracy: {}".format((y_hat_a == y_test).mean()))

priors = torch.zeros(len(X_train), 3)
for i, y in enumerate(y_train):
    if y != -1:
        priors[i, y] = 1.0
    else:
        priors[i] = 1./3

d1 = GeneralMixtureModel([Normal(), Normal()]).fit(X_train[y_train == 0])
d2 = GeneralMixtureModel([Normal(), Normal()]).fit(X_train[y_train == 1])
d3 = GeneralMixtureModel([Normal(), Normal()]).fit(X_train[y_train == 2])

model_b = GeneralMixtureModel([d1, d2, d3])
model_b.fit(X_train, priors=priors)

y_hat_b = model_b.predict(X_test).numpy()
print("Semisupervised Learning Accuracy: {}".format((y_hat_b == y_test).mean()))
Supervised Learning Accuracy: 0.935531914893617
Semisupervised Learning Accuracy: 0.9846808510638297

正如预期的那样,半监督方法表现更优。让我们沿用之前的方式可视化分析,以探究其原因。

[8]:
xx, yy = numpy.meshgrid(numpy.arange(x_min, x_max, 0.1), numpy.arange(y_min, y_max, 0.1))
Z1 = model_a.predict(numpy.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
Z2 = model_b.predict(numpy.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)

plt.figure(figsize=(16, 16))
plt.subplot(221)
plt.title("Training Data, Supervised Boundaries", fontsize=16)
plot_contour(X_train, y_train, Z1)

plt.subplot(223)
plt.title("Training Data, Semi-supervised Boundaries", fontsize=16)
plot_contour(X_train, y_train, Z2)

plt.subplot(222)
plt.title("Test Data, Supervised Boundaries", fontsize=16)
plot_contour(X_test, y_test, Z1)

plt.subplot(224)
plt.title("Test Data, Semi-supervised Boundaries", fontsize=16)
plot_contour(X_test, y_test, Z2)
plt.show()
../_images/tutorials_C_Feature_Tutorial_4_Priors_and_Semi-supervised_Learning_16_0.png

可以立即注意到,使用半监督学习时的决策边界比仅使用少量样本时更加平滑。这主要是因为拥有更多数据通常能带来更平滑的决策边界,因为模型不会过度拟合数据集中的虚假样本。看起来大多数正确分类的样本来自于对左侧聚类中洋红色样本更准确的决策边界。当仅使用标记样本时,该区域许多洋红色样本被错误分类为青色样本。相比之下,使用全部数据时这些点都能被正确分类。

隐马尔可夫模型

隐马尔可夫模型可以像混合模型一样轻松地接收先验信息。正如数据必须是三维的,先验信息也必须是三维的。

[9]:
from pomegranate.hmm import DenseHMM

d1 = Normal([1.0], [1.0], covariance_type='diag')
d2 = Normal([3.0], [1.0], covariance_type='diag')

model = DenseHMM([d1, d2], [[0.7, 0.3], [0.3, 0.7]], starts=[0.4, 0.6])

我们可以先看看在没有先验的情况下,前向传播会是什么样子。

[10]:
X = torch.randn(1, 10, 1)

model.predict_proba(X)
[10]:
tensor([[[0.9398, 0.0602],
         [0.9271, 0.0729],
         [0.6459, 0.3541],
         [0.9699, 0.0301],
         [0.9812, 0.0188],
         [0.9953, 0.0047],
         [0.9987, 0.0013],
         [0.9738, 0.0262],
         [0.9782, 0.0218],
         [0.9924, 0.0076]]])

现在让我们添加一个规则:每个观测值必须映射到一个特定的状态。

[11]:
priors = torch.ones(1, 10, 2) / 2
priors[0, 5, 0], priors[0, 5, 1] = 0, 1
priors
[11]:
tensor([[[0.5000, 0.5000],
         [0.5000, 0.5000],
         [0.5000, 0.5000],
         [0.5000, 0.5000],
         [0.5000, 0.5000],
         [0.0000, 1.0000],
         [0.5000, 0.5000],
         [0.5000, 0.5000],
         [0.5000, 0.5000],
         [0.5000, 0.5000]]])
[12]:
model.predict_proba(X, priors=priors)
[12]:
tensor([[[0.9398, 0.0602],
         [0.9267, 0.0733],
         [0.6427, 0.3573],
         [0.9620, 0.0380],
         [0.9070, 0.0930],
         [0.0000, 1.0000],
         [0.9930, 0.0070],
         [0.9732, 0.0268],
         [0.9782, 0.0218],
         [0.9924, 0.0076]]])

我们可以看到,尽管模型试图将所有观测值分配到分布0,但它被迫将第6个观测值分配到分布1。

使用这样的先验概率为我们提供了一种非常灵活的方式在HMMs上进行半监督学习。如果您有完整的标记序列,可以为序列中的每个观察传递1.0的先验概率,即使其他序列完全没有标签。如果您有部分标记的序列,可以轻松地使用某些观察的先验概率训练模型,而不需要所有观察都有标签,并利用这些部分观察来指导其他所有内容。

概述

在现实世界中,经常会出现只有一小部分可用数据带有有用标签的情况。半监督学习提供了一个框架,可以同时利用数据集的标记和未标记部分来学习一个复杂的估计器。在这种情况下,半监督学习与概率模型配合良好,因为可以在标记数据上进行常规的最大似然估计,并使用相同的分布在未标记数据上运行期望最大化算法。

本笔记本介绍了如何在pomegranate中使用混合模型和隐马尔可夫模型实现半监督学习。用户只需将观测值的先验与标签保持一致,pomegranate就会自动处理其余部分。这在处理现实世界中复杂、嘈杂且非标准高斯分布的数据时特别有用。