标签传播算法(Label Propagation)及 Python 实现

2017 年 9 月 18 日 Python开发者

(点击上方蓝字,快速关注我们)


来源:zouxy09

blog.csdn.net/zouxy09/article/details/49105265

如有好文章投稿,请点击 → 这里了解详情


众所周知,机器学习可以大体分为三大类:监督学习、非监督学习和半监督学习。监督学习可以认为是我们有非常多的labeled标注数据来train一个模型,期待这个模型能学习到数据的分布,以期对未来没有见到的样本做预测。那这个性能的源头–训练数据,就显得非常感觉。你必须有足够的训练数据,以覆盖真正现实数据中的样本分布才可以,这样学习到的模型才有意义。那非监督学习就是没有任何的labeled数据,就是平时所说的聚类了,利用他们本身的数据分布,给他们划分类别。而半监督学习,顾名思义就是处于两者之间的,只有少量的labeled数据,我们试图从这少量的labeled数据和大量的unlabeled数据中学习到有用的信息。


一、半监督学习


半监督学习(Semi-supervised learning)发挥作用的场合是:你的数据有一些有label,一些没有。而且一般是绝大部分都没有,只有少许几个有label。半监督学习算法会充分的利用unlabeled数据来捕捉我们整个数据的潜在分布。它基于三大假设:


  1. Smoothness平滑假设:相似的数据具有相同的label。

  2. Cluster聚类假设:处于同一个聚类下的数据具有相同label。

  3. Manifold流形假设:处于同一流形结构下的数据具有相同label。


例如下图,只有两个labeled数据,如果直接用他们来训练一个分类器,例如LR或者SVM,那么学出来的分类面就是左图那样的。如果现实中,这个数据是右图那边分布的话,猪都看得出来,左图训练的这个分类器烂的一塌糊涂、惨不忍睹。因为我们的labeled训练数据太少了,都没办法覆盖我们未来可能遇到的情况。但是,如果右图那样,把大量的unlabeled数据(黑色的)都考虑进来,有个全局观念,牛逼的算法会发现,哎哟,原来是两个圈圈(分别处于两个圆形的流形之上)!那算法就很聪明,把大圈的数据都归类为红色类别,把内圈的数据都归类为蓝色类别。因为,实践中,labeled数据是昂贵,很难获得的,但unlabeled数据就不是了,写个脚本在网上爬就可以了,因此如果能充分利用大量的unlabeled数据来辅助提升我们的模型学习,这个价值就非常大。



半监督学习算法有很多,下面我们介绍最简单的标签传播算法(label propagation),最喜欢简单了,哈哈。


二、标签传播算法


标签传播算法(label propagation)的核心思想非常简单:相似的数据应该具有相同的label。LP算法包括两大步骤:1)构造相似矩阵;2)勇敢的传播吧。


2.1、相似矩阵构建


LP算法是基于Graph的,因此我们需要先构建一个图。我们为所有的数据构建一个图,图的节点就是一个数据点,包含labeled和unlabeled的数据。节点i和节点j的边表示他们的相似度。这个图的构建方法有很多,这里我们假设这个图是全连接的,节点i和节点j的边权重为:



这里,α是超参。


还有个非常常用的图构建方法是knn图,也就是只保留每个节点的k近邻权重,其他的为0,也就是不存在边,因此是稀疏的相似矩阵。


2.2、LP算法


标签传播算法非常简单:通过节点之间的边传播label。边的权重越大,表示两个节点越相似,那么label越容易传播过去。我们定义一个NxN的概率转移矩阵P:



Pij表示从节点i转移到节点j的概率。假设有C个类和L个labeled样本,我们定义一个LxC的label矩阵YL,第i行表示第i个样本的标签指示向量,即如果第i个样本的类别是j,那么该行的第j个元素为1,其他为0。同样,我们也给U个unlabeled样本一个UxC的label矩阵YU。把他们合并,我们得到一个NxC的soft label矩阵F=[YL;YU]。soft label的意思是,我们保留样本i属于每个类别的概率,而不是互斥性的,这个样本以概率1只属于一个类。当然了,最后确定这个样本i的类别的时候,是取max也就是概率最大的那个类作为它的类别的。那F里面有个YU,它一开始是不知道的,那最开始的值是多少?无所谓,随便设置一个值就可以了。


千呼万唤始出来,简单的LP算法如下:


  1. 执行传播:F=PF

  2. 重置F中labeled样本的标签:FL=YL

  3. 重复步骤1)和2)直到F收敛。


步骤1)就是将矩阵P和矩阵F相乘,这一步,每个节点都将自己的label以P确定的概率传播给其他节点。如果两个节点越相似(在欧式空间中距离越近),那么对方的label就越容易被自己的label赋予,就是更容易拉帮结派。步骤2)非常关键,因为labeled数据的label是事先确定的,它不能被带跑,所以每次传播完,它都得回归它本来的label。随着labeled数据不断的将自己的label传播出去,最后的类边界会穿越高密度区域,而停留在低密度的间隔中。相当于每个不同类别的labeled样本划分了势力范围。


2.3、变身的LP算法


我们知道,我们每次迭代都是计算一个soft label矩阵F=[YL;YU],但是YL是已知的,计算它没有什么用,在步骤2)的时候,还得把它弄回来。我们关心的只是YU,那我们能不能只计算YU呢?Yes。我们将矩阵P做以下划分:



这时候,我们的算法就一个运算:



迭代上面这个步骤直到收敛就ok了,是不是很cool。可以看到FU不但取决于labeled数据的标签及其转移概率,还取决了unlabeled数据的当前label和转移概率。因此LP算法能额外运用unlabeled数据的分布特点。


这个算法的收敛性也非常容易证明,具体见参考文献[1]。实际上,它是可以收敛到一个凸解的:



所以我们也可以直接这样求解,以获得最终的YU。但是在实际的应用过程中,由于矩阵求逆需要O(n3)的复杂度,所以如果unlabeled数据非常多,那么I – PUU矩阵的求逆将会非常耗时,因此这时候一般选择迭代算法来实现。


三、LP算法的Python实现


Python环境的搭建就不啰嗦了,可以参考前面的博客。需要额外依赖的库是经典的numpy和matplotlib。代码中包含了两种图的构建方法:RBF和KNN指定。同时,自己生成了两个toy数据库:两条长形形状和两个圈圈的数据。第四部分我们用大点的数据库来做实验,先简单的可视化验证代码的正确性,再前线。


算法代码:


import time  

import numpy as np  

  

# return k neighbors index  

def navie_knn(dataSet, query, k):  

    numSamples = dataSet.shape[0]  

  

    ## step 1: calculate Euclidean distance  

    diff = np.tile(query, (numSamples, 1)) - dataSet  

    squaredDiff = diff ** 2  

    squaredDist = np.sum(squaredDiff, axis = 1) # sum is performed by row  

  

    ## step 2: sort the distance  

    sortedDistIndices = np.argsort(squaredDist)  

    if k > len(sortedDistIndices):  

        k = len(sortedDistIndices)  

  

    return sortedDistIndices[0:k]  

  

  

# build a big graph (normalized weight matrix)  

def buildGraph(MatX, kernel_type, rbf_sigma = None, knn_num_neighbors = None):  

    num_samples = MatX.shape[0]  

    affinity_matrix = np.zeros((num_samples, num_samples), np.float32)  

    if kernel_type == 'rbf':  

        if rbf_sigma == None:  

            raise ValueError('You should input a sigma of rbf kernel!')  

        for i in xrange(num_samples):  

            row_sum = 0.0  

            for j in xrange(num_samples):  

                diff = MatX[i, :] - MatX[j, :]  

                affinity_matrix[i][j] = np.exp(sum(diff**2) / (-2.0 * rbf_sigma**2))  

                row_sum += affinity_matrix[i][j]  

            affinity_matrix[i][:] /= row_sum  

    elif kernel_type == 'knn':  

        if knn_num_neighbors == None:  

            raise ValueError('You should input a k of knn kernel!')  

        for i in xrange(num_samples):  

            k_neighbors = navie_knn(MatX, MatX[i, :], knn_num_neighbors)  

            affinity_matrix[i][k_neighbors] = 1.0 / knn_num_neighbors  

    else:  

        raise NameError('Not support kernel type! You can use knn or rbf!')  

      

    return affinity_matrix  

  

  

# label propagation  

def labelPropagation(Mat_Label, Mat_Unlabel, labels, kernel_type = 'rbf', rbf_sigma = 1.5, \  

                    knn_num_neighbors = 10, max_iter = 500, tol = 1e-3):  

    # initialize  

    num_label_samples = Mat_Label.shape[0]  

    num_unlabel_samples = Mat_Unlabel.shape[0]  

    num_samples = num_label_samples + num_unlabel_samples  

    labels_list = np.unique(labels)  

    num_classes = len(labels_list)  

      

    MatX = np.vstack((Mat_Label, Mat_Unlabel))  

    clamp_data_label = np.zeros((num_label_samples, num_classes), np.float32)  

    for i in xrange(num_label_samples):  

        clamp_data_label[i][labels[i]] = 1.0  

      

    label_function = np.zeros((num_samples, num_classes), np.float32)  

    label_function[0 : num_label_samples] = clamp_data_label  

    label_function[num_label_samples : num_samples] = -1  

      

    # graph construction  

    affinity_matrix = buildGraph(MatX, kernel_type, rbf_sigma, knn_num_neighbors)  

      

    # start to propagation  

    iter = 0; pre_label_function = np.zeros((num_samples, num_classes), np.float32)  

    changed = np.abs(pre_label_function - label_function).sum()  

    while iter < max_iter and changed > tol:  

        if iter % 1 == 0:  

            print "---> Iteration %d/%d, changed: %f" % (iter, max_iter, changed)  

        pre_label_function = label_function  

        iter += 1  

          

        # propagation  

        label_function = np.dot(affinity_matrix, label_function)  

          

        # clamp  

        label_function[0 : num_label_samples] = clamp_data_label  

          

        # check converge  

        changed = np.abs(pre_label_function - label_function).sum()  

      

    # get terminate label of unlabeled data  

    unlabel_data_labels = np.zeros(num_unlabel_samples)  

    for i in xrange(num_unlabel_samples):  

        unlabel_data_labels[i] = np.argmax(label_function[i+num_label_samples])  

      

    return unlabel_data_labels


测试代码:


import time  

import math  

import numpy as np  

from label_propagation import labelPropagation  

  

# show  

def show(Mat_Label, labels, Mat_Unlabel, unlabel_data_labels):  

    import matplotlib.pyplot as plt  

      

    for i in range(Mat_Label.shape[0]):  

        if int(labels[i]) == 0:    

            plt.plot(Mat_Label[i, 0], Mat_Label[i, 1], 'Dr')    

        elif int(labels[i]) == 1:    

            plt.plot(Mat_Label[i, 0], Mat_Label[i, 1], 'Db')  

        else:  

            plt.plot(Mat_Label[i, 0], Mat_Label[i, 1], 'Dy')  

      

    for i in range(Mat_Unlabel.shape[0]):  

        if int(unlabel_data_labels[i]) == 0:    

            plt.plot(Mat_Unlabel[i, 0], Mat_Unlabel[i, 1], 'or')    

        elif int(unlabel_data_labels[i]) == 1:    

            plt.plot(Mat_Unlabel[i, 0], Mat_Unlabel[i, 1], 'ob')  

        else:  

            plt.plot(Mat_Unlabel[i, 0], Mat_Unlabel[i, 1], 'oy')  

      

    plt.xlabel('X1'); plt.ylabel('X2')  

    plt.xlim(0.0, 12.)  

    plt.ylim(0.0, 12.)  

    plt.show()    

  

  

def loadCircleData(num_data):  

    center = np.array([5.0, 5.0])  

    radiu_inner = 2  

    radiu_outer = 4  

    num_inner = num_data / 3  

    num_outer = num_data - num_inner  

      

    data = []  

    theta = 0.0  

    for i in range(num_inner):  

        pho = (theta % 360) * math.pi / 180  

        tmp = np.zeros(2, np.float32)  

        tmp[0] = radiu_inner * math.cos(pho) + np.random.rand(1) + center[0]  

        tmp[1] = radiu_inner * math.sin(pho) + np.random.rand(1) + center[1]  

        data.append(tmp)  

        theta += 2  

      

    theta = 0.0  

    for i in range(num_outer):  

        pho = (theta % 360) * math.pi / 180  

        tmp = np.zeros(2, np.float32)  

        tmp[0] = radiu_outer * math.cos(pho) + np.random.rand(1) + center[0]  

        tmp[1] = radiu_outer * math.sin(pho) + np.random.rand(1) + center[1]  

        data.append(tmp)  

        theta += 1  

      

    Mat_Label = np.zeros((2, 2), np.float32)  

    Mat_Label[0] = center + np.array([-radiu_inner + 0.5, 0])  

    Mat_Label[1] = center + np.array([-radiu_outer + 0.5, 0])  

    labels = [0, 1]  

    Mat_Unlabel = np.vstack(data)  

    return Mat_Label, labels, Mat_Unlabel  

  

  

def loadBandData(num_unlabel_samples):  

    #Mat_Label = np.array([[5.0, 2.], [5.0, 8.0]])  

    #labels = [0, 1]  

    #Mat_Unlabel = np.array([[5.1, 2.], [5.0, 8.1]])  

      

    Mat_Label = np.array([[5.0, 2.], [5.0, 8.0]])  

    labels = [0, 1]  

    num_dim = Mat_Label.shape[1]  

    Mat_Unlabel = np.zeros((num_unlabel_samples, num_dim), np.float32)  

    Mat_Unlabel[:num_unlabel_samples/2, :] = (np.random.rand(num_unlabel_samples/2, num_dim) - 0.5) * np.array([3, 1]) + Mat_Label[0]  

    Mat_Unlabel[num_unlabel_samples/2 : num_unlabel_samples, :] = (np.random.rand(num_unlabel_samples/2, num_dim) - 0.5) * np.array([3, 1]) + Mat_Label[1]  

    return Mat_Label, labels, Mat_Unlabel  

  

  

# main function  

if __name__ == "__main__":  

    num_unlabel_samples = 800  

    #Mat_Label, labels, Mat_Unlabel = loadBandData(num_unlabel_samples)  

    Mat_Label, labels, Mat_Unlabel = loadCircleData(num_unlabel_samples)  

      

    ## Notice: when use 'rbf' as our kernel, the choice of hyper parameter 'sigma' is very import! It should be  

    ## chose according to your dataset, specific the distance of two data points. I think it should ensure that  

    ## each point has about 10 knn or w_i,j is large enough. It also influence the speed of converge. So, may be  

    ## 'knn' kernel is better!  

    #unlabel_data_labels = labelPropagation(Mat_Label, Mat_Unlabel, labels, kernel_type = 'rbf', rbf_sigma = 0.2)  

    unlabel_data_labels = labelPropagation(Mat_Label, Mat_Unlabel, labels, kernel_type = 'knn', knn_num_neighbors = 10, max_iter = 400)  

    show(Mat_Label, labels, Mat_Unlabel, unlabel_data_labels)  


该注释的,代码都注释的,有看不明白的,欢迎交流。不同迭代次数时候的结果如下:



是不是很漂亮的传播过程?!在数值上也是可以看到随着迭代的进行逐渐收敛的,迭代的数值变化过程如下:


四、LP算法MPI并行实现


这里,我们测试的是LP的变身版本。从公式,我们可以看到,第二项PULYL迭代过程并没有发生变化,所以这部分实际上从迭代开始就可以计算好,从而避免重复计算。不过,不管怎样,LP算法都要计算一个UxU的矩阵PUU和一个UxC矩阵FU的乘积。当我们的unlabeled数据非常多,而且类别也很多的时候,计算是很慢的,同时占用的内存量也非常大。另外,构造Graph需要计算两两的相似度,也是O(n2)的复杂度,当我们数据的特征维度很大的时候,这个计算量也是非常客观的。所以我们就得考虑并行处理了。而且最好是能放到集群上并行。那如何并行呢?


对算法的并行化,一般分为两种:数据并行和模型并行。


数据并行很好理解,就是将数据划分,每个节点只处理一部分数据,例如我们构造图的时候,计算每个数据的k近邻。例如我们有1000个样本和20个CPU节点,那么就平均分发,让每个CPU节点计算50个样本的k近邻,然后最后再合并大家的结果。可见这个加速比也是非常可观的。


模型并行一般发生在模型很大,无法放到单机的内存里面的时候。例如庞大的深度神经网络训练的时候,就需要把这个网络切开,然后分别求解梯度,最后有个leader的节点来收集大家的梯度,再反馈给大家去更新。当然了,其中存在更细致和高效的工程处理方法。在我们的LP算法中,也是可以做模型并行的。假如我们的类别数C很大,把类别数切开,让不同的CPU节点处理,实际上就相当于模型并行了。


那为啥不切大矩阵PUU,而是切小点的矩阵FU,因为大矩阵PUU没法独立分块,并行的一个原则是处理必须是独立的。 矩阵FU依赖的是所有的U,而把PUU切开分发到其他节点的时候,每次FU的更新都需要和其他的节点通信,这个通信的代价是很大的(实际上,很多并行系统没法达到线性的加速度的瓶颈是通信!线性加速比是,我增加了n台机器,速度就提升了n倍)。但是对类别C也就是矩阵FU切分,就不会有这个问题,因为他们的计算是独立的。只是决定样本的最终类别的时候,将所有的FU收集回来求max就可以了。


所以,在下面的代码中,是同时包含了数据并行和模型并行的雏形的。另外,还值得一提的是,我们是迭代算法,那决定什么时候迭代算法停止?除了判断收敛外,我们还可以让每迭代几步,就用测试label测试一次结果,看模型的整体训练性能如何。特别是判断训练是否过拟合的时候非常有效。因此,代码中包含了这部分内容。


好了,代码终于来了。大家可以搞点大数据库来测试,如果有MPI集群条件的话就更好了。


下面的代码依赖numpy、scipy(用其稀疏矩阵加速计算)和mpi4py。其中mpi4py需要依赖openmpi和Cpython,可以参考我之前的博客进行安装。


由于微信字数限制,此部分的代码通过点击阅读原文查看


五、参考资料


[1]Semi-SupervisedLearning with Graphs.pdf


看完本文有收获?请转发分享给更多人

关注「大数据与机器学习文摘」,成为Top 1%

登录查看更多
5

相关内容

【Google】无监督机器翻译,Unsupervised Machine Translation
专知会员服务
35+阅读 · 2020年3月3日
【新书】Pro 机器学习算法Python实现,379页pdf
专知会员服务
195+阅读 · 2020年2月11日
Transformer文本分类代码
专知会员服务
116+阅读 · 2020年2月3日
【书籍推荐】简洁的Python编程(Clean Python),附274页pdf
专知会员服务
173+阅读 · 2020年1月1日
【GitHub实战】Pytorch实现的小样本逼真的视频到视频转换
专知会员服务
35+阅读 · 2019年12月15日
实战 | 手把手教你用PyTorch实现图像描述(附完整代码)
Python | 50行代码实现人脸检测
计算机与网络安全
3+阅读 · 2018年1月23日
tensorflow LSTM + CTC实现端到端OCR
数据挖掘入门与实战
8+阅读 · 2017年11月15日
支持向量机分类实战
全球人工智能
4+阅读 · 2017年10月18日
朴素贝叶斯和贝叶斯网络算法及其R语言实现
R语言中文社区
10+阅读 · 2017年10月2日
入坑机器学习,这10个知识点你要了解!
THU数据派
5+阅读 · 2017年9月15日
入坑机器学习,十个知识点你不得不知
人工智能头条
7+阅读 · 2017年9月15日
人工神经网络算法及其简易R实现
R语言中文社区
18+阅读 · 2017年8月5日
Arxiv
14+阅读 · 2019年9月11日
Arxiv
26+阅读 · 2018年2月27日
VIP会员
相关资讯
实战 | 手把手教你用PyTorch实现图像描述(附完整代码)
Python | 50行代码实现人脸检测
计算机与网络安全
3+阅读 · 2018年1月23日
tensorflow LSTM + CTC实现端到端OCR
数据挖掘入门与实战
8+阅读 · 2017年11月15日
支持向量机分类实战
全球人工智能
4+阅读 · 2017年10月18日
朴素贝叶斯和贝叶斯网络算法及其R语言实现
R语言中文社区
10+阅读 · 2017年10月2日
入坑机器学习,这10个知识点你要了解!
THU数据派
5+阅读 · 2017年9月15日
入坑机器学习,十个知识点你不得不知
人工智能头条
7+阅读 · 2017年9月15日
人工神经网络算法及其简易R实现
R语言中文社区
18+阅读 · 2017年8月5日
Top
微信扫码咨询专知VIP会员