目录
一 引言
由于现实中,自带可用类标签的数据集少之又少,数据集中的标签不足,就无法构造出能精准预测验证集和测试集的标签的分类器
我们通常会想到手动给数据贴上标签,但这不仅耗时,还会产生一些特定的人工误差
于是半监督学习诞生了,它能够捕获潜在的数据分布状况来给无标签的数据添加标签,节省大量标注时间
二 What is 半监督学习
半监督学习同时使用无标签数据和标签数据
半监督学习会预留一部分测试数据,不用于训练,而用于后期测试,转导推理技术能为无标签数据生成标签
三 半监督算法实战
1 自训练
自训练是最简单、最快的半监督学习方法,旨在将无标签实例的信息与标签实例的信息相结合,以迭代确定无标签实例的标签值。标签训练集会在每次迭代中增大,直到整个数据集都添加了标签,自训练算法通常作为封装器应用于基础模型
2 自训练步骤
- 用一系列标签数据预测无标签数据(可以是所有无标签数据或部分无标签数据)的标签值
- 计算所有新标签实例的置信度
- 从新增标签实例中挑选几个实例用于下次迭代
- 用所有标签实例(包括上次迭代挑选出的实例)训练模型
- 重复1-4步,直到模型成功收敛
结束训练前,自训练模型应经过测试和验证,可以使用交叉验证或者使用其他特意保留的标签数据进行验证
3 实现自训练 -准备阶段
自训练每次迭代的第一步就是为无标签实例生成标签,首先构造一个sleflearningmodel类,并将基于监督算法的基本模型(basemodel)和迭代限制作为参数,prob_threshold参数提供接受标签的可信度的最低限制,低于该水平的预估标签都会被拒绝,除了硬编码值域
定义 SelfLearningModel 类的外层
class BaseEstimator:
def _init_(self, basemodel, max_iter = 200, prob_threshold = 0.8):
self.model = basemodel
self.max_iter = max_iter
self.prob_threshold = prob_threshold
定义用于半监督模型拟合处理的函数
def fit(self,x,y):
unlabeledx = x[y==-1,:]
labeledx = x[y!=-1,:]
labeledy = y[y!=-1,:]
self.model.fit(labeledx,labeledy)
unlabeledy = self.predict(unlabeledx)
unlabeledprob = self.predict_proba(unlabeledx)
unlabeledy_old = []
i = 0
参数x是输入数据矩阵,其形状等于 [n_samples,n_features] ,用于构建 [n_samples,n_features] 的矩阵;参数y是标签数组,无标签数据点在y中标记为-1;通过对x进行运算,即在x中选择y标签为 -1 的元素,可以构建 unlabeledx 和 labeledx 参数。labeledy 参数在 y 上进行类似的选择(通常我们对 y 中的无标签样本不感兴趣,但需要它们的标签来进行分类)
标签预测的过程首先要用到 sklearn 封装的预测操作 ,unlabeledy 参数是用 sklearn 的 predict 方法生成的,,predict_proba 方法则用于计算每个预测标签的概率;这些概率值存储在 unlabeledprob 中
接下来需要一个循环来进行迭代,以下代码使用了一个 while 循环,该循环执行直到 unlabeledy_old (unlabeledy 的副本)内五实例或者达到最大循环次数。每次迭代时,尝试对每个没有标签、概率超过概率阈值(prob_threshold)的实例添加标签
while(len(unlabeledy_old) == 0 or numpy.any(unlabeledy!=unlabeledy_old))
and i < self.max_iter:
unlabeledY_old = numpy.copy(unlabeledY)
uidx = numpy.where((unlabeledprob[:,0] > self.prob_threshold)|
(unlabeledprob[:,1] > self.prob_threshold))[0]
然后 self.model.fit 方法用模型拟合无标签数据,后者将以矩阵形式存储,矩阵大小为[n_samples,n_features] 该矩阵会(通过vstack 和 hstack 函数)不断补充无标签数据
self.model.fit(numpy.vstack((labeiedx,unlabeiedx,unlabeledx[uidx,:])),
numpy.hstack((labeledy,unlabeledy_old[uidx])))
最后预测标签,以及标签的概率
unlabeledy = self.predict(unlabeledx)
unlabeledprob = self.predict_proba(unlabeledx)
i += 1
下次迭代时,模型将执行相同的处理,将概率预测超过阈值的新增标签数据作为 model.fit 步骤中的数据集的一部分
如果模型还未包含能产生标签预测的分类方法,可以引入一个,以下是代码查找 predict_proba 方法,并在找不到该方法时引用基于 Platt scaling 的标签生成函数
if not getattr(self.model,"predict_proba",None):
self.plattlr = LR()
preds = self.model.predict(labeledx)
else.plattlr.fit(preds.reshape(-1,1),labeledy)
return self
def predict_proba(self,x):
if getattr(self.model,"predict_proba",None):
return self.model.predict_proba(x)
else:
preds = self.model.predict(x)
return self.plattlr.predict_proba(preds.reshape( -1 , 1))
总代码
class BaseEstimator:
def _init_(self, basemodel, max_iter = 200, prob_threshold = 0.8):
self.model = basemodel
self.max_iter = max_iter
self.prob_threshold = prob_threshold
def fit(self,x,y):
unlabeledx = x[y==-1,:]
labeledx = x[y!=-1,:]
labeledy = y[y!=-1,:]
self.model.fit(labeledx,labeledy)
unlabeledy = self.predict(unlabeledx)
unlabeledprob = self.predict_proba(unlabeledx)
unlabeledy_old = []
i = 0
while(len(unlabeledy_old) == 0 or numpy.any(unlabeledy!=unlabeledy_old))
and i < self.max_iter:
unlabeledY_old = numpy.copy(unlabeledY)
uidx = numpy.where((unlabeledprob[:,0] > self.prob_threshold)|
(unlabeledprob[:,1] > self.prob_threshold))[0]
self.model.fit(numpy.vstack((labeiedx,unlabeiedx,unlabeledx[uidx,:])),
numpy.hstack((labeledy,unlabeledy_old[uidx])))
unlabeledy = self.predict(unlabeledx)
unlabeledprob = self.predict_proba(unlabeledx)
i += 1
if not getattr(self.model,"predict_proba",None):
self.plattlr = LR()
preds = self.model.predict(labeledx)
else.plattlr.fit(preds.reshape(-1,1),labeledy)
return self
def predict_proba(self,x):
if getattr(self.model,"predict_proba",None):
return self.model.predict_proba(x)
else:
preds = self.model.predict(x)
return self.plattlr.predict_proba(preds.reshape( -1 , 1))
4 改善自训练的实现
自训练是一个很脆弱的过程,如果算法的某个元素配置不当,或者输入的某个元素含杂质,迭代的过程就很可能一而再、再而三的出错,不断地将标签的错误数据引入下一个添加标签的步骤中,因为自训练算法会迭代的自反馈
判断自训练中的错误只需要在标签预测循环中加入员工代表当前分类精度的变量,就可以得知早期精度随迭代的变化
自训练模型很容易出现过拟合,为了应对这种风险,必须保留一些数据用于后续的验证;另一种做法是用数据集的多个子集来训练多个自训练模型实例;这样经过多次尝试后,就能更加了解输入数据对自调练模型性能的影响
还有一些集成方法,以使用多个自训练模型共同生成预测;使用集成方法时,可以考虑同时应用多种采样技术
如果不想从数量上下手,可以考虑改进质量 选择构建多样性适当的标签数据子集。开始自训练的标签实例没有最小数量等硬性限制。如果想从一个类中只有一个标签实例开始,很快就会发现,更多的标签数据有利于训练更多样化、重叠性更强的类
自训练模型容易犯的另一类错误是偏差选择( biased selection),本来设想每次代的数据,选择最差只会出现一点偏差 (即稍微偏向其中一类),但是许多因素都会影响偏差选择的可能性,其原因是对某一类的不合比例抽样
如果整个数据集或使用的标签子集偏向某一类,自训练分类器过度拟合的风险就会增加,当实例传到下一次代,但其多样性不足以解决问题时,会使得问题更加复杂,自训练算法也会随之建立错误的决策边界,从而导致对数据子集过度拟合。各类的实例数量不都是问题关键,诊断与选择偏差相关的问题也是识别过度拟合的常用方法
自训练更严重的潜在风险是 无标签数据往往存在噪声,如果需要处理的数据集中的部分甚至全部无标签数据都存在噪声,那么分类的精度就会降低,可以用线性分类器或者包含非线性成分的分类器实现有效的度量
5 改良选择过程
自训练算法正常运行的关键指标是能精确计算每个预测标签的置信度
- 将所有预测标签加入标签数据集
- 根据置信度值选出数据集中置信度最高的几个标签
- 将所有预测标签加入标签数据集,并将置信度作为每个标签的权重
自训练的实现有很多问题,容易出现一系列训练失误,也容易过度拟合;随着无标签数据的增加,自训练分类器的精度也会越来越成问题
四 对比悲观似然估计(CPLE)
CPLE用最大对数似然函数来优化参数
为了构建更好的半监督学习器(基于监督学习器进行改良),CPLE明确考虑了监督估计,并将半监督模型和监督模型间的损失作为训练性能测度
构建CPLE类
class CPLELearningModel (BaseEstimator):
def _init_(self, basemodel, pessimistic = True, predict_from_probabilities =
False, use_sample_weighting = True, max_iter = 3000,verbose = 1):
self.model = basemodel
self.pessimistic = pessimistic
self.predict_from_probabilities = predict_from_probabilities
self.use_sample_weighting = use_sample_weighting
self.max_iter = max_iter
self.verbose = verbose
pessimistic参数应用非悲观(乐观)模型,pessimistic方法会保守考虑无标签数据和标签数据的似然估计的差值,即把其最小化,而乐观模型会将其最大化,这样做结果更好(主要指训练集上的表现),但风险也更大
这里选择悲观模型predict_from probabilities参数通过考虑多个数据的预测概率来完成优化。设置该参数为true时,如果用于预测的概率超过均值,CPLE会将该预测赋值为1,反之为0.还可以使用基础模型的预测概率作为预测结果,该指标性能更好,因此通常更受青睐。当然,对于大量实例,往往调用predict来实现批量预测
还可以选用use_sample_weighting,也称软标签(或“后验概率”),软标签比硬标签灵活,多作为首选(除非模型只支持硬标签)
self.it = 0
self.noimprovementsince = 0
self.maxnoimprovementsince = 3
self.buffersize = 200
self.lastdls = [0]*self.buffersize
selff.bestdl = numpy.infty
self.bestlbls = []
self.id = str(unichr(numpy.random.randint(26)+97))+str(unichr(numpy.random.
randint(26)+97))
discriminative_likelihood 函数计算给定输入下的似然估计(对于判别模型来说,即
在给定输入X的情况下,寻找使y=1的概率最大的模型)
每次迭代时计算预测标签的概率
def discriminative_likelihood(self, model, labeledData, labeledy =
None, unlabeledData = None, unlabeledWeights = None, unlabeledlambda =
1, gradient = [], alpha = 0.01):
unlabeledy = (unlabeledWeights[:,0] < 0.5)*1
uweights = numpy.copy(unlabeledweights[:,0])
uweights[unlabeledy == 1] = 1-uweights [unlabeledy == 1 ]
weights = numpy.hstack((numpy.ones(len(labeledy)),uweights))
Labels = numpy.hstack((labeledy, unlabeledy))
定义好CPMLE的这些部分后,还需要定义监督模型的积合过程,这要用到model,fit 和 model.predict_proba 组件来预测概率
if self.use_sample_weighting:
model.fit(numpy.vstack((labeledData)),
labels,sampel_weight = weights)
else:
model.fit(numpy.vstack((labeledData,unlabeledData)),labels)
p = model.predict_proba(labeledData)
为了执行悲观CPLE,需要先根据监督数据和无监督数据分别计算判别对数似然值。下面依次在标签数据和无标签数据上执行 predict_proba
try:
labeledDL = -sklearn.metrics.log_loss(labeledy, P)
except Exception, e:
print e
p = model.predict_proba(labeledData)
unlabeledP = model.predict_proba(unlabeledData)
try:
eps = le - 15
unlabeledP = numpy.clip(unlabeledP, eps, 1 - eps)
unlabeledDL = numpy.average((
unlabeledWeights*numpy.vstack((1-unlabeledy, unlabeledy)).T*numpy.log
(unlabeledP)).sum(axis = 1))
except Exception, e:
print e
unlabeledP = model.predict_proba(unlabeledData)
能够计算标签数据和无标签数据的判别对数似然值时,就可以通过discriminative_likelihood_objective函数设置目标了。这里的目标是用悲观方法(或乐观方法)计算每次迭代的d1,直到模型收敛或达到最大迭代次数,每次迭代时,用t检验判断概率是否发生变化。收敛前,概率应随每次迭代发生变化。连续3次未变化的t检验意味着迭代应终止(可以通过 maxnoimprovementsince 参数配置次数)
if self.pessimistic:
dl = unlabeledlambda * unlabeledDL - labeledDL
else:
dl = - unlabeledlambda * unlabeledDL - labeledDL
return dl
def discriminative_likelihood_objective(self, model, labeledData,
labeledy = None, unlabeledData = None, unlabeledweights = None,
unlabeledlambda = 1, gradient=[], alpha = 0.01):
if self.it==0:
self.lastdls =[0]*self.buffersize
dl=self.discriminative_likelihood(
model, labeledData, labeledy, unlabeledData, unlabeledweights,
unlabeledlambda, gradient, alpha)
self.it+=1
self.lastdls [numpy.mod(self.it, len(self.lastdls))]= dl
if numpy.mod(self.it, self.buffersize) ==0: # or True:
improvement = numpy.mean ( (self.lastdls[(1en(self.lastdls)/2):])) -
numpy.mean((self.lastdls[:(len(self.
lastdls)/2)]))
_, prob = scipy.stats.ttest_ind(self.lastdls[(1en(self.lastdls)/2):],
self.lastdls[: (len(self.lastdls)/2)])
noimprovement = prob > 0.1 and numpy.mean(
self.lastdls[(1en(self.lastdls)/2):]) < numpy.mean (self.lastdls
[:(1en(self.lastdls)/2)])
if noimprovement:
self.noimprovementsince + = 1
if self.noimprovementsince >= self.
maxnoimprovementsince:
self.noimprovementsince = 0
raise Exception(" converged.")
else:
self.noimprovementsince =0
每次迭代时,算法会存储最佳的判别似然值和权重集合,用于下次迭代
if dl < self.bestdl:
self.bestdl=d1
self.bestlbls = numpy.copy(unlabeledWeights[:, 0])
return d1
如何构造软标签
f = 1ambda softlabels, grad=[]: self.discriminative_likelihood_objective(
self.model, labeledx, 1abeledy=labeledy,
unlabeledData=unlabeledX,
unlabeledweights=numpy.vstack((softlabels, 1-softlabels)).T, gradient=grad)
1blinit = numpy. random. random (1en (unlabeledy))
软标签用optimize方法计算
try:
self.it = 0
opt = nlopt.opt(nlopt.GN_DIRECT_L_RAND, M)
opt.set_lower_bounds(numpy.zeros(M))
opt.set_upper_bounds (numpy.ones (M) )
opt.set_min_objective(f)
opt.set_maxeval(self.max_iter)
self.bestsoft1bl = opt.optimize(1blinit)
print" max_iter exceeded."
except Exception, e:
print e
self.bestsoft1b1=self.bestlb1s
if numpy.any(self.bestsoft1bl != self.bestlbls):
self.bestsoftlbl = self.bestlbls
11 = f(self.bestsoft1b1)
unlabeledy = (self.bestsoft1b1<0.5)*1
uweights = numpy.copy(self.bestsoft1b1)
uweights[unlabeledy == 1] = 1 - uweights[unlabeledy == 1]
weights = numpy.hstack( (numpy.ones(1en(labeledy)), uweights))
labels = numpy.hstack((labeledy, unlabeledy))
计算比较最佳监督标签和软标签,并将bestsoftlabe1参数设为最佳标签集,然后用最佳标签集计算判别似然值,并计算fit函数
if self.use_sample_weighting:
self.model. fit (numpy.vstac((labeledx, unlabeledx)),
1abels, sample_weight=weights)
else:
self.model.fit (numpy.vstack((labeledx, unlabeledX)),labels)
if self.verbose>1:
print "number of non-one soft labels: ", numpy.sum(
self.bestsoft1bl != 1), ", balance:", numpy.sum(self.bestsoft1b1<0.5),
"/",len(self.bestsoft1b1)
print "current likelihood: ", 11