图机器学习(Graph Machine Learning)- 第四章 监督图学习1 (upervised Graph Learning)

第四章 监督图学习1 (upervised Graph Learning)


4.1 前言

监督学习(Supervised learning,SL) 很可能代表了大多数实际机器学习的任务。由于越来越多的主动和有效的数据收集活动,现在处理带标签的数据集是非常普遍的。

对于图数据也是如此,标签可以分配给节点、社区,甚至整个结构。那么,任务就是学习输入和标签(目标,注释)之间的映射函数。

例如,给定一个表示社交网络的图,我们可能会被要求猜测哪个用户(节点)将关闭他们的帐户。我们可以通过在回顾性数据(retrospective data) 上训练图机器学习来学习这个预测函数,其中每个用户根据他们是否在几个月后关闭了账户被标记为“忠实”或“退出者”。

在本章中,我们将探讨监督学习的概念,以及如何将它应用于图。我们也将提供监督图嵌入方法的概述。本章包含一下内容:

  • 监督图嵌入路线图
  • 基于特征的方法
  • 浅嵌入方法
  • 图正则方法
  • 图卷积神经网络

4.2 环境需求

所有的代码在Python 3.8的Jupyter notebook 下运行,所需Python库及版本如下:

Jupyter==1.0.0
networkx==2.5
matplotlib==3.2.2
node2vec==0.3.3
karateclub==1.0.19
scikit-learn==0.24.0
pandas==1.1.3
numpy==1.19.2
tensorflow==2.4.1
neural-structured-learning==1.3.1
stellargraph==1.2.1

4.3 监督图嵌入路线图

在监督学习中,一个训练集由一系列有序对 ( x , y ) (x, y) (x,y)组成,其中 x x x是一个输入特征的集合(通常是图形上定义的信号), y y y是分配给它的输出标签。机器学习模型的目标是学习将每个 x x x值映射到每个 y y y值的函数。常见的监督任务包括预测大型社交网络中的用户属性,或预测生物分子的属性,其中每个分子都是一个图。

但是,有时并不是所有的实例都可以提供一个标签。在此场景中,典型的数据集由一组带标签的实例和一组较大的未带标签的实例组成。针对这种情况,提出了半监督学习(Semi-Supervised learning, SSL)算法,其目的是利用可用标签信息反映的标签依赖信息,以学习无标签样本的预测函数。

有监督图机器学习技术已经发展了许多算法。然而,正如论文(https://arxiv.org/abs/2005.03675) 所报道的那样,这些方法宏观上可分为基于特征的方法、浅嵌入方法、正则化方法和图神经网络(GNNs) ,如下图所示:
在这里插入图片描述

在下面的章节中,您将学习每组算法背后的主要原理。我们也将尝试提供该领域中最著名的算法,并用这些算法解决现实世界的问题。

4.4 基于特征的方法

在图上应用机器学习的一个非常简单(但功能强大)的方法是将编码函数视为简单的嵌入查找。在处理监督任务时,一种简单的方法是利用图属性。在第一章中我们学习了如何用结构属性来描述图(或图中的节点),每个结构属性“编码”了图本身的重要信息。

让我们暂时忘记图机器学习:经典的监督机器学习任务是找到一个函数,将一个数据的一组(描述性的)特征映射到一个特定的输出。这些特征应该经过精心设计,以便它们具有足够的代表性来学习这一概念。因此,由于花瓣数和萼片长度可能是描述一朵花的很好的描述符,所以在描述一个图时,我们可以依赖于它的平均度、全局效率和特征路径长度。

这种浅层方法分为两个步骤,概述如下:

  1. 选择一组好的图属性描述。
  2. 使用这些属性作为传统机器学习算法的输入

不幸的是,良好的属性描述没有通用的定义,它们的选择严格取决于要解决的具体问题。然而,您仍然可以计算各种各样的图形属性,然后执行特征选择以选择信息量最大的那些。特征选择是机器学习中一个被广泛研究的主题,但是提供关于各种方法的详细信息超出了本书的范围。

现在让我们看一个如何应用这种基本方法的实际例子。我们将使用一个蛋白质(PROTEINS)数据集来执行一个有监督的图分类任务。蛋白质数据集包含几个图形表示蛋白质结构。每个图都有标记,以确定该蛋白质是否是一种酶。我们将遵循以下步骤:

  1. 首先,通过stellargraphPython库加载 PROTEINS 数据集
from stellargraph import datasets
from IPython.display import display, HTML

dataset = datasets.PROTEINS()
display(HTML(dataset.description))
graphs, graph_labels = dataset.load()
Each graph represents a protein and graph labels represent whether they are are enzymes or non-enzymes. The dataset includes 1113 graphs with 39 nodes and 73 edges on average for each graph. Graph nodes have 4 attributes (including a one-hot encoding of their label), and each graph is labelled as belonging to 1 of 2 classes.
  1. 如第一章一样,我们使用networkx计算图形属性,为此,我们需要将图表从stellargraph格式转换为networkx格式。这可以通过两个步骤完成:首先,将图从stellargraph表示转换为numpy邻接矩阵。然后,使用邻接矩阵得到networkx表示。此外,我们还将标签(存储为panda序列)转换为numpy数组,这样可以更好地利用评估函数,我们将在接下来的步骤中看到这一点。
# convert graphs from StellarGraph format to numpy adj matrices
adjs = [graph.to_adjacency_matrix().A for graph in graphs]
# convert labes fom Pandas.Series to numpy array
labels = graph_labels.to_numpy(dtype=int)
  1. 然后,对于每个图,我们计算其全局度量来描述它。在这个例子中,我们选择了边数、平均聚类系数和全局效率。我们也建议您计算其他几个属性,您会发现这是值得探索的。我们可以使用networkx提取图形度量,如下所示:
import numpy as np
import networkx as nx

metrics = []
for adj in adjs:
  G = nx.from_numpy_matrix(adj)
  # basic properties
  num_edges = G.number_of_edges()
  # clustering measures
  cc = nx.average_clustering(G)
  # measure of efficiency
  eff = nx.global_efficiency(G)

  metrics.append([num_edges, cc, eff])
  1. 利用scikit-learn来创建训练集和测试集。在我们的实验中,我们将使用70%的数据集作为训练集,其余的作为测试集。我们可以通过使用scikit-learn提供的train_test_split函数来实现。与许多机器学习通用工作流程中一样,我们也对特征进行预处理,使其均值和单位标准差为零。
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(metrics, labels, test_size=0.3, random_state=42)
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
scaler.fit(X_train)

X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
  1. 训练合适的机器学习算法。我们选择了支持向量机(support vector machine, SVM) 来完成这个任务。更准确地说,SVM被训练成最小化预测标签和实际标签(ground truth)之间的差异。我们可以通过使用scikit-learnSVC模块来实现,如下所示:
from sklearn import svm
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score

clf = svm.SVC()
clf.fit(X_train_scaled, y_train)

y_pred = clf.predict(X_test_scaled)

print('Accuracy', accuracy_score(y_test,y_pred))
print('Precision', precision_score(y_test,y_pred))
print('Recall', recall_score(y_test,y_pred))
print('F1-score', f1_score(y_test,y_pred))
Accuracy 0.7455089820359282
Precision 0.7709251101321586
Recall 0.8413461538461539
F1-score 0.8045977011494253

我们使用Accuracy、Precision、Recall和F1-score来评估算法在测试集上的表现。我们获得了大约80%的F1成绩,这对于这样一个naïve任务来说已经相当不错了。

4.5 浅嵌入方法

正如第三章“无监督图学习”中所描述的,浅嵌入方法是图嵌入方法的一个子集,这些方法只针对有限的输入数据集学习节点、边或图表示。它们不能应用于与用于训练模型的实例不同的其他实例。

在开始讨论之前,重要的是定义监督和无监督浅嵌入算法的不同之处。无监督和监督嵌入方法之间的主要区别本质上在于它们试图解决的任务。事实上,无监督浅嵌入算法为了构建定义良好的集群而尝试学习良好的图、节点或边缘表示,监督算法则尝试为预测任务(如节点、标签或图分类)找到最佳解决方案。

在本节中,我们将详细解释一些监督浅嵌入算法。此外,我们将通过提供几个如何在Python中使用这些算法的示例来丰富我们的描述。对于本节中描述的所有算法,我们将使用scikitlearn库中提供的基本类来实现。

import matplotlib.pyplot as plt

def draw_graph(G, node_names={}, nodes_label=[], node_size=900):
    pos_nodes = nx.spring_layout(G)
    
    col = {0:"steelblue",1:"red",2:"green"}
    
    colors = [col[x] for x in nodes_label]
    
    nx.draw(G, pos_nodes, with_labels=True, node_color=colors, node_size=node_size, edge_color='gray', 
            arrowsize=30)
    
    
    
    pos_attrs = {}
    for node, coords in pos_nodes.items():
        pos_attrs[node] = (coords[0], coords[1] + 0.08)
        
    
    plt.axis('off')
    axis = plt.gca()
    axis.set_xlim([1.2*x for x in axis.get_xlim()])
    axis.set_ylim([1.2*y for y in axis.get_ylim()])
    plt.show()

4.5.1 标签传播算法 Label propagation

标签传播算法是数据科学中广泛应用的半监督算法,用于解决节点分类任务。更准确地说,该算法将给定节点的标签 传播 到它的邻居或从该节点到达的概率较高的节点。

这种方法背后的基本思想非常简单:给定一个图,其中包含一组标记节点和未标记节点,标记节点将它们的标记传播到有最大可能被到达的节点。在下面的图中,我们可以看到一个有标签节点和无标签节点的图的例子:

import networkx as nx

G = nx.barbell_graph(m1=3, m2=2)
nodes_label = [0 for x in range(len(G.nodes()))]
nodes_label[0] = 1
nodes_label[6] = 2
draw_graph(G, nodes_label=nodes_label, node_size=1200)

在这里插入图片描述

nodes_label

[1, 0, 0, 0, 0, 0, 2, 0]

由上图可知,算法利用已标记节点(节点0和节点6)的信息,计算移动到另一个未标记节点的概率。从一个已标记的节点获得标签的概率最高的节点将获得该节点的标签。

形式上,令 G = ( V , E ) G=(V,E) G=(V,E)是一个图, Y = { y 1 , … , y p } Y=\{y_1, \ldots,y_p\} Y={y1,,yp}是一组标签。由于算法是半监督的,只有一个子集节点会有一个分配的标签。 A ∈ R ∣ V ∣ × ∣ V ∣ A \in \mathbb{R}^{|V|\times|V|} ARV×V为输入图 G G G的邻接矩阵, D ∈ R ∣ V ∣ × ∣ V ∣ D \in \mathbb{R}^{|V|\times|V|} DRV×V为对角度矩阵,其中对角度矩阵的每个元 d i j d_{ij} dij定义如下:

d i j = { 0 , if i ≠ j d e g ( v i ) , if i = j d_{ij} = \begin{cases} 0, & \text{if} i \neq j\\ deg(v_i), & \text{if} i = j \end{cases} dij={0,deg(vi),ifi=jifi=j

换句话说,度矩阵中唯一的非零元素是对角元素,它们的值由行所表示的节点的度给出。图4.2所示图的对角度矩阵如下:

import numpy as np
from numpy.linalg import inv
#对角度矩阵
D = [G.degree(n) for n in G.nodes()]
D = np.diag(D)
D
array([[2, 0, 0, 0, 0, 0, 0, 0],
       [0, 2, 0, 0, 0, 0, 0, 0],
       [0, 0, 3, 0, 0, 0, 0, 0],
       [0, 0, 0, 2, 0, 0, 0, 0],
       [0, 0, 0, 0, 2, 0, 0, 0],
       [0, 0, 0, 0, 0, 3, 0, 0],
       [0, 0, 0, 0, 0, 0, 2, 0],
       [0, 0, 0, 0, 0, 0, 0, 2]])

从对角度矩阵可以看到只有矩阵的对角元素包含非零值,这些值表示特定节点的度。我们另外引入转移矩阵 L = D − 1 A L = D^{-1}A L=D1A,这个矩阵定义了一个节点从另一个节点到达的概率。更准确地说, l i j ∈ L l_{ij} \in L lijL是从节点 v i v_i vi到达节点 v j v_j vj的概率。图4.2所示图的转移矩阵 L L L如下:

#转移矩阵
A = inv(D)*nx.to_numpy_matrix(G)
A
matrix([[0.        , 0.5       , 0.5       , 0.        , 0.        ,
         0.        , 0.        , 0.        ],
        [0.5       , 0.        , 0.5       , 0.        , 0.        ,
         0.        , 0.        , 0.        ],
        [0.33333333, 0.33333333, 0.        , 0.33333333, 0.        ,
         0.        , 0.        , 0.        ],
        [0.        , 0.        , 0.5       , 0.        , 0.5       ,
         0.        , 0.        , 0.        ],
        [0.        , 0.        , 0.        , 0.5       , 0.        ,
         0.5       , 0.        , 0.        ],
        [0.        , 0.        , 0.        , 0.        , 0.33333333,
         0.        , 0.33333333, 0.33333333],
        [0.        , 0.        , 0.        , 0.        , 0.        ,
         0.5       , 0.        , 0.5       ],
        [0.        , 0.        , 0.        , 0.        , 0.        ,
         0.5       , 0.5       , 0.        ]])

转移矩阵显示了从一个起始节点出发到达终节点的概率。例如,从矩阵的第一行我们可以看到如何从节点0到达节点1和节点2(概率为0.5)。如果定义 Y 0 Y^0 Y0为初始标签赋值,利用矩阵 L L L得到的每个节点的标签赋值概率可以计算为 Y 1 = L Y 0 Y^1 = L Y^0 Y1=LY0。根据图4.2的图计算出的 Y 1 Y^1 Y1矩阵如下:

#[1,0]标签表示属于第1类,[0,1]表示属于第2类
Y_0 = np.matrix([[1,  0],
       [0,  0],
       [0,  0],
       [0,  0],
       [0,  0],
       [0,  0],
       [0,  1],
       [0,  0],])
Y_1 = A*Y_0
Y_1
matrix([[0.        , 0.        ],
        [0.5       , 0.        ],
        [0.33333333, 0.        ],
        [0.        , 0.        ],
        [0.        , 0.        ],
        [0.        , 0.33333333],
        [0.        , 0.        ],
        [0.        , 0.5       ]])

从上面的计算中我们可以看出,利用转移矩阵,节点1和节点2分别有概率0.5和0.33被分配到的[1 0]标签,而节点5和节点6分别有概率0.33和0.5被分配到[0 1]标签的。

此外,我们从上式可以看到两个主要问题如下:

  • 在该方案中,可以只给节点[1 2]和[5 7]分配一个与标签相关的概率。
  • 节点0和节点6的初始标签与 Y 0 Y^0 Y0中定义的不同。

为了解决第一个问题,算法将进行 n n n次不同的迭代;在第 t t t次迭代中,算法将利用如下公式计算该迭代的解:
Y t = L Y t − 1 Y^t = L Y^{t-1} Yt=LYt1

当满足一定条件时,算法停止迭代。

解决第二个问题的办法是在标签传播算法中,在第t次迭代中强制将标记节点赋予初始类值。例如,在计算出结果后,算法强制将结果矩阵的第一行赋为[1 0],第七行赋为[0 1]。

在这里,我们提出了scikit-learn库中的LabelPropagation类的改进版本。该改进主要基于如下事实:LabelPropagation类接受数据集的表示矩阵作为输入。矩阵的每一行代表一个样本,每一列代表一个特征。

在执行fit操作之前,LabelPropagation类在内部执行_build_graph函数。这个函数将使用KNN参数核径向基函数(包含在_get_kernel函数内)来构建输入数据集的图表示。因此,原始数据集被转换为图(在其邻接矩阵表示中),其中每个节点是一个样本(输入数据集的一行),每个边是样本之间的联系。

在我们的具体例子中,输入数据集已经是一个图,因此我们需要定义一个新的类,能够处理networkx图并在原始图上执行计算操作。这个目标是通过扩展ClassifierMixin,BaseEstimatorABCMeta基类来创建一个新类,即GraphLabelPropagation来实现.

import numpy as np
import networkx as nx
from numpy.linalg import inv
from abc import ABCMeta, abstractmethod
from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.utils.multiclass import check_classification_targets
from sklearn.utils.validation import check_is_fitted, _deprecate_positional_args

class GraphLabelPropagation(ClassifierMixin, BaseEstimator, metaclass=ABCMeta):
    """Graph label propagation module.
    Parameters
    ----------
    max_iter : int, default=30
        Change maximum number of iterations allowed.
    tol : float, default=1e-3
        Convergence tolerance: threshold to consider the system at steady
        state.
    """

    @_deprecate_positional_args
    def __init__(self, max_iter=30, tol=1e-3):

        self.max_iter = max_iter
        self.tol = tol

    def predict(self, X):
        """Performs inductive inference across the model.
        Parameters
        ----------
        X : A networkx array.
            The data matrix.
        Returns
        -------
        y : ndarray of shape (n_samples,)
            Predictions for input data.
        """
        probas = self.predict_proba(X)
        return self.classes_[np.argmax(probas, axis=1)].ravel()

    def predict_proba(self, X):
        """Predict probability for each possible outcome.
        Compute the probability estimates for each single node in X
        and each possible outcome seen during training (categorical
        distribution).
        Parameters
        ----------
        X : A networkx array.
        Returns
        -------
        probabilities : ndarray of shape (n_samples, n_classes)
            Normalized probability distributions across
            class labels.
        """
        check_is_fitted(self)
        
        return self.label_distributions_
    
    def _validate_data(self, X, y):
        if not isinstance(X, nx.Graph):
            raise ValueError("Input should be a networkX graph")
        if not len(y) == len(X.nodes()):
            raise ValueError("Label data input shape should be equal to the number of nodes in the graph")
        return X, y
    
    @staticmethod
    def build_label(x,classes):
        tmp = np.zeros((classes))
        tmp[x] = 1
        return tmp
    
    def fit(self, X, y):
        """Fit a semi-supervised label propagation model based
        on the input graph G and corresponding label matrix y with a dedicated marker value for
        unlabeled samples.
        Parameters
        ----------
        X : A networkX array.
        y : array-like of shape (n_samples,)
            `n_labeled_samples` (unlabeled points are marked as -1)
            All unlabeled samples will be transductively assigned labels.
        Returns
        -------
        self : object
        """
        X, y = self._validate_data(X, y)
        self.X_ = X
        check_classification_targets(y)

        D = [X.degree(n) for n in X.nodes()]
        D = np.diag(D)
        
        # label construction
        # construct a categorical distribution for classification only
        unlabeled_index = np.where(y==-1)[0]
        labeled_index = np.where(y!=-1)[0]
        unique_classes = np.unique(y[labeled_index])
        
        self.classes_ = unique_classes
        
        Y0 = np.array([self.build_label(y[x], len(unique_classes)) 
                                 if x in labeled_index else np.zeros(len(unique_classes)) for x in range(len(y))])
        
        A = inv(D)*nx.to_numpy_matrix(G)
        Y_prev = Y0
        it = 0
        c_tool = 10
        
        while it < self.max_iter & c_tool > self.tol:
            Y = A*Y_prev
            #force labeled nodes
            Y[labeled_index] = Y0[labeled_index]
            
            it +=1
            c_tol = np.sum(np.abs(Y-Y_prev))
            
            Y_prev = Y
            
        self.label_distributions_ = Y
        return self

该算法可以使用以下代码应用于图4.2所示的示例图:

glp = GraphLabelPropagation()
y = np.array([-1 for x in range(len(G.nodes()))])
y[0] = 1
y[6] = 0
glp.fit(G,y)
tmp = glp.predict(G)
print(glp.predict_proba(G))

draw_graph(G, nodes_label=tmp+1, node_size=1200)
[[0.         1.        ]
 [0.05338542 0.90006109]
 [0.11845743 0.8081115 ]
 [0.31951678 0.553297  ]
 [0.553297   0.31951678]
 [0.8081115  0.11845743]
 [1.         0.        ]
 [0.90006109 0.05338542]]

在这里插入图片描述
在图4.6中,我们可以看到应用到图4.2例中的算法的结果。从最终的概率分配矩阵中,可以看到由于算法的约束,初始标记节点的概率是1,以及“接近”标记节点的节点是如何得到它们的标签的。

4.5.2标签扩散算法 Label Spreading Algorithm

标签扩散算法是另一种半监督浅层嵌入算法。它的建立是为了克服标签传播方法的一个主要缺陷:初始标签 (initial labeling)。事实上,根据标签传播算法,初始标签在训练过程中是不能修改的,在每次迭代中,它们都被强制等于它们的原始值。当初始标记受到误差或噪声的影响时,这种约束可能会产生不正确的结果。这种错误将传播到输入图的所有节点。

为了解决这一缺陷,标签扩散算法尝试放松原始标记数据的约束,允许标记输入节点在训练过程中改变自己的标记。

形式上,令 G = ( V , E ) G=(V,E) G=(V,E)是一个图, Y = { y 1 , … , y p } Y=\{y_1, \ldots,y_p\} Y={y1,,yp}是一组标签。由于算法是半监督的,只有一个子集节点会有一个分配的标签。 A ∈ R ∣ V ∣ × ∣ V ∣ A \in \mathbb{R}^{|V|\times|V|} ARV×V为输入图 G G G的邻接矩阵, D ∈ R ∣ V ∣ × ∣ V ∣ D \in \mathbb{R}^{|V|\times|V|} DRV×V为对角度矩阵。标签传播算法不计算概率转移矩阵,而是使用归一化图拉普拉斯矩阵(normalized graph Laplacian matrix) ,定义如下:

L = D − 1 / 2 A D − 1 / 2 L = D^{-1/2}AD^{-1/2} L=D1/2AD1/2

与标签传播一样,这个矩阵可以看作是定义在整个图上的连接的一种紧凑低维表示。这个矩阵可以通过networkx计算如下:

import networkx as nx

G = nx.barbell_graph(m1=3, m2=2)
nodes_label = [0 for x in range(len(G.nodes()))]
nodes_label[0] = 1
nodes_label[6] = 2
draw_graph(G, nodes_label=nodes_label, node_size=1200)

在这里插入图片描述

import numpy as np
from numpy.linalg import inv
# Degree matrix
D = [G.degree(n) for n in G.nodes()]
D = np.diag(D)
D
array([[2, 0, 0, 0, 0, 0, 0, 0],
       [0, 2, 0, 0, 0, 0, 0, 0],
       [0, 0, 3, 0, 0, 0, 0, 0],
       [0, 0, 0, 2, 0, 0, 0, 0],
       [0, 0, 0, 0, 2, 0, 0, 0],
       [0, 0, 0, 0, 0, 3, 0, 0],
       [0, 0, 0, 0, 0, 0, 2, 0],
       [0, 0, 0, 0, 0, 0, 0, 2]])
# Normalized graph Laplacian matrix
from scipy.linalg import fractional_matrix_power
D_inv = fractional_matrix_power(D, -0.5)
L = D_inv*nx.to_numpy_matrix(G)*D_inv
L
matrix([[0.        , 0.5       , 0.40824829, 0.        , 0.        ,
         0.        , 0.        , 0.        ],
        [0.5       , 0.        , 0.40824829, 0.        , 0.        ,
         0.        , 0.        , 0.        ],
        [0.40824829, 0.40824829, 0.        , 0.40824829, 0.        ,
         0.        , 0.        , 0.        ],
        [0.        , 0.        , 0.40824829, 0.        , 0.5       ,
         0.        , 0.        , 0.        ],
        [0.        , 0.        , 0.        , 0.5       , 0.        ,
         0.40824829, 0.        , 0.        ],
        [0.        , 0.        , 0.        , 0.        , 0.40824829,
         0.        , 0.40824829, 0.40824829],
        [0.        , 0.        , 0.        , 0.        , 0.        ,
         0.40824829, 0.        , 0.5       ],
        [0.        , 0.        , 0.        , 0.        , 0.        ,
         0.40824829, 0.5       , 0.        ]])

标签扩散和标签传播算法之间最重要的区别在于用于提取标签的函数。如果我们定义 Y 0 Y^0 Y0为初始标签分配,通过如下计算可以使用 L L L矩阵得到的每个节点的标签分配概率
Y 1 = α L Y 0 + ( 1 − α ) Y 0 Y^1 = \alpha L Y^0 + (1-\alpha)Y^0 Y1=αLY0+(1α)Y0

与标签传播一样,标签扩散有一个迭代过程来计算最终的解。该算法将执行 n n n次不同的迭代;在第 t t t次迭代中,算法通过如下步骤计算迭代解:
Y t = α L Y t − 1 + ( 1 − α ) Y 0 Y^t = \alpha L Y^{t-1} + (1-\alpha)Y^0 Yt=αLYt1+(1α)Y0

当满足一定条件时,算法停止迭代。上式中$(1-\alpha)Y^0 非 常 重 要 。 事 实 上 , 正 如 我 们 所 说 , 标 签 扩 散 并 不 强 制 迭 代 解 的 标 签 等 于 它 的 初 始 值 。 相 反 , 该 算 法 使 用 正 则 化 参 数 非常重要。事实上,正如我们所说,标签扩散并不强制迭代解的标签等于它的初始值。相反,该算法使用正则化参数 使\alpha \in [0,1)$来加权每次迭代时初始解的影响。这有助于量化初始解及其对最终解的影响。

与标签传播算法一样,在下面的代码片段中,我们提出了scikit-learn库中可用的LabelSpreading类的改进版本。通过改进GraphLabelPropagation类来得到graphlabelspread类,唯一的区别是类中fit()函数。

import numpy as np
import networkx as nx
from sklearn.preprocessing import normalize
from scipy.linalg import fractional_matrix_power
from sklearn.utils.multiclass import check_classification_targets

class GraphLabelSpreading(GraphLabelPropagation):
    """Graph label propagation module.
    Parameters
    ----------
    max_iter : int, default=30
        Change maximum number of iterations allowed.
    tol : float, default=1e-3
        Convergence tolerance: threshold to consider the system at steady
        state.
    """

    @_deprecate_positional_args
    def __init__(self, max_iter=30, tol=1e-3, alpha=0.6):

        self.alpha = alpha
        super().__init__(max_iter, tol)
    
    def fit(self, X, y):
        """Fit a semi-supervised label propagation model based
        on the input graph G and corresponding label matrix y with a dedicated marker value for
        unlabeled samples.
        Parameters
        ----------
        X : A networkX array.
        y : array-like of shape (n_samples,)
            `n_labeled_samples` (unlabeled points are marked as -1)
            All unlabeled samples will be transductively assigned labels.
        Returns
        -------
        self : object
        """
        X, y = self._validate_data(X, y)
        self.X_ = X
        check_classification_targets(y)

        D = [X.degree(n) for n in X.nodes()]
        D = np.diag(D)
        D_inv = np.matrix(fractional_matrix_power(D,-0.5))
        L = D_inv*nx.to_numpy_matrix(G)*D_inv
        
        # label construction
        # construct a categorical distribution for classification only
        unlabeled_index = np.where(y==-1)[0]
        labeled_index = np.where(y!=-1)[0]
        unique_classes = np.unique(y[labeled_index])
        
        self.classes_ = unique_classes
        
        Y0 = np.array([self.build_label(y[x], len(unique_classes)) 
                                 if x in labeled_index else np.zeros(len(unique_classes)) for x in range(len(y))])
        
        Y_prev = Y0
        it = 0
        c_tool = 10
        
        while it < self.max_iter & c_tool > self.tol:
            Y = self.alpha*(L*Y_prev)+((1-self.alpha)*Y0)

            it +=1
            c_tol = np.sum(np.abs(Y-Y_prev))
            Y_prev = Y
        self.label_distributions_ = Y
        return self

在这个类中,fit()函数是焦点。该函数接受一个networkx X X X和一个表示分配给每个节点的标签的数组 y y y作为输入。不带标签的节点赋予标签值-1。while循环在每次迭代时计算 Y t Y^t Yt,通过参数 α \alpha α加权初始标记的影响。 此外,该算法使用迭代次数和连续两次解的差值作为停止准则。

该算法可以使用以下代码来求解图4.2所示的示例:

gls = GraphLabelSpreading(max_iter=1000)
y = np.array([-1 for x in range(len(G.nodes()))])
y[0] = 1
y[6] = 0
gls.fit(G,y)
tmp = gls.predict(G)
print(gls.predict_proba(G))
draw_graph(G, nodes_label=tmp+1, node_size=1200)
[[0.00148824 0.50403871]
 [0.00148824 0.19630098]
 [0.00471728 0.18369265]
 [0.01591722 0.05001252]
 [0.05001252 0.01591722]
 [0.18369265 0.00471728]
 [0.50403871 0.00148824]
 [0.19630098 0.00148824]]

在这里插入图片描述
上图所示图中的结果与使用标签传播算法得到的结果相似。主要的区别与标签分配的概率有关。实际上,在这种情况下,可以看到节点0和6(有初始标记的节点)的概率是0.5,这比使用标签传播算法获得的概率1要低得多。这种结果是符合预期的,因为初始标签分配的影响是由正则化参数 α \alpha α加权。

总结

在下一节中,我们将继续介绍监督图嵌入方法。我们将介绍基于网络的信息如何用于正则化训练和创建更稳健的模型。

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值