Module graphlearning.ssl

Semi-Supervised Learning

This module implements many graph-based semi-supervised learning algorithms in an objected-oriented fashion, similar to sklearn. The usage is similar for all algorithms. Below, we give some high-level examples of how to use this module. There are also examples for some individual functions, given in the documentation below.

Two Moons Example

Semi-supervised learning on the two-moons dataset: ssl_twomoons.py.

import numpy as np
import graphlearning as gl
import matplotlib.pyplot as plt
import sklearn.datasets as datasets

X,labels = datasets.make_moons(n_samples=500,noise=0.1)
W = gl.weightmatrix.knn(X,10)

train_ind = gl.trainsets.generate(labels, rate=5)
train_labels = labels[train_ind]

model = gl.ssl.laplace(W)
pred_labels = model.fit_predict(train_ind, train_labels)

accuracy = gl.ssl.ssl_accuracy(pred_labels, labels, train_ind)   
print("Accuracy: %.2f%%"%accuracy)

plt.scatter(X[:,0],X[:,1], c=pred_labels)
plt.scatter(X[train_ind,0],X[train_ind,1], c='r')
plt.show()

Handwritten Digit Classification

Laplace and Poisson learning on MNIST at 1 label per class: ssl_mnist.py.

import graphlearning as gl

labels = gl.datasets.load('mnist', labels_only=True)
W = gl.weightmatrix.knn('mnist', 10, metric='vae')

num_train_per_class = 1
train_ind = gl.trainsets.generate(labels, rate=num_train_per_class)
train_labels = labels[train_ind]

models = [gl.ssl.laplace(W), gl.ssl.poisson(W)]

for model in models:
    pred_labels = model.fit_predict(train_ind,train_labels)
    accuracy = gl.ssl.ssl_accuracy(pred_labels, labels, train_ind)
    print(model.name + ': %.2f%%'%accuracy)

Comparing Different Methods

The ssl module contains functions for running large scale experiments, with parallel processing, to compare many different semi-supervised learning algorithms at different label rates and with different, randomly chosen, training data. The package includes functions to automatically create LaTeX tables and plots comparing accuracy or test error. This example compares several methods over 100 randomized trials: ssl_trials.py.

import graphlearning as gl

dataset = 'mnist'
metric = 'vae' 
k = 10 

W = gl.weightmatrix.knn(dataset, k, metric=metric)
D = gl.weightmatrix.knn(dataset, k, metric=metric, kernel='distance')

labels = gl.datasets.load(dataset, metric=metric, labels_only=True)
trainsets = gl.trainsets.load(dataset)

model_list = [gl.ssl.graph_nearest_neighbor(D),
              gl.ssl.laplace(W),
              gl.ssl.laplace(W, reweighting='wnll'), 
              gl.ssl.laplace(W, reweighting='poisson'),
              gl.ssl.poisson(W, solver='gradient_descent')]

tag = dataset + '_' + metric + '_k%d'%k
for model in model_list:
    model.ssl_trials(trainsets, labels, num_cores=20, tag=tag)

gl.ssl.accuracy_table(model_list, tag=tag, savefile='SSL_'+dataset+'.tex', title="SSL Comparison: "+dataset)
gl.ssl.accuracy_plot(model_list, tag=tag, title='SSL')

Class Priors

Prior information about relative class sizes can be used to improve performance: ssl_classpriors.py.

import graphlearning as gl

labels = gl.datasets.load('mnist', labels_only=True)
W = gl.weightmatrix.knn('mnist', 10, metric='vae')

num_train_per_class = 1
train_ind = gl.trainsets.generate(labels, rate=num_train_per_class)
train_labels = labels[train_ind]

class_priors = gl.utils.class_priors(labels)
model = gl.ssl.laplace(W, class_priors=class_priors)
model.fit(train_ind,train_labels)

pred_labels = model.predict(ignore_class_priors=True)
accuracy = gl.ssl.ssl_accuracy(labels,pred_labels,train_ind)
print(model.name + ' without class priors: %.2f%%'%accuracy)

pred_labels = model.predict()
accuracy = gl.ssl.ssl_accuracy(labels,pred_labels,train_ind)
print(model.name + ' with class priors: %.2f%%'%accuracy)
Expand source code
"""
Semi-Supervised Learning
========================

This module implements many graph-based semi-supervised learning algorithms in an objected-oriented
fashion, similar to [sklearn](https://scikit-learn.org/stable/). The usage is similar for all algorithms.
Below, we give some high-level examples of how to use this module. There are also examples for some
individual functions, given in the documentation below.

Two Moons Example
----
Semi-supervised learning on the two-moons dataset: [ssl_twomoons.py](https://github.com/jwcalder/GraphLearning/blob/master/examples/ssl_twomoons.py).
```py
import numpy as np
import graphlearning as gl
import matplotlib.pyplot as plt
import sklearn.datasets as datasets

X,labels = datasets.make_moons(n_samples=500,noise=0.1)
W = gl.weightmatrix.knn(X,10)

train_ind = gl.trainsets.generate(labels, rate=5)
train_labels = labels[train_ind]

model = gl.ssl.laplace(W)
pred_labels = model.fit_predict(train_ind, train_labels)

accuracy = gl.ssl.ssl_accuracy(pred_labels, labels, train_ind)   
print("Accuracy: %.2f%%"%accuracy)

plt.scatter(X[:,0],X[:,1], c=pred_labels)
plt.scatter(X[train_ind,0],X[train_ind,1], c='r')
plt.show()
```
Handwritten digit classification
-----
Laplace and Poisson learning on MNIST at 1 label per class: [ssl_mnist.py](https://github.com/jwcalder/GraphLearning/blob/master/examples/ssl_mnist.py).
```py
import graphlearning as gl

labels = gl.datasets.load('mnist', labels_only=True)
W = gl.weightmatrix.knn('mnist', 10, metric='vae')

num_train_per_class = 1
train_ind = gl.trainsets.generate(labels, rate=num_train_per_class)
train_labels = labels[train_ind]

models = [gl.ssl.laplace(W), gl.ssl.poisson(W)]

for model in models:
    pred_labels = model.fit_predict(train_ind,train_labels)
    accuracy = gl.ssl.ssl_accuracy(pred_labels, labels, train_ind)
    print(model.name + ': %.2f%%'%accuracy)
```
Comparing different methods
-----------
The `ssl` module contains functions for running large scale experiments, with parallel processing,
to compare many different semi-supervised learning algorithms at different label rates
and with different, randomly chosen, training data. The package includes functions to automatically
create LaTeX tables and plots comparing accuracy or test error. This example compares several methods over 100 
randomized trials: [ssl_trials.py](https://github.com/jwcalder/GraphLearning/blob/master/examples/ssl_trials.py).
```py
import graphlearning as gl

dataset = 'mnist'
metric = 'vae' 
k = 10 

W = gl.weightmatrix.knn(dataset, k, metric=metric)
D = gl.weightmatrix.knn(dataset, k, metric=metric, kernel='distance')

labels = gl.datasets.load(dataset, metric=metric, labels_only=True)
trainsets = gl.trainsets.load(dataset)

model_list = [gl.ssl.graph_nearest_neighbor(D),
              gl.ssl.laplace(W),
              gl.ssl.laplace(W, reweighting='wnll'), 
              gl.ssl.laplace(W, reweighting='poisson'),
              gl.ssl.poisson(W, solver='gradient_descent')]

tag = dataset + '_' + metric + '_k%d'%k
for model in model_list:
    model.ssl_trials(trainsets, labels, num_cores=20, tag=tag)

gl.ssl.accuracy_table(model_list, tag=tag, savefile='SSL_'+dataset+'.tex', title="SSL Comparison: "+dataset)
gl.ssl.accuracy_plot(model_list, tag=tag, title='SSL')
```
Class Priors
-----------
Prior information about relative class sizes can be used to improve performance: [ssl_classpriors.py](https://github.com/jwcalder/GraphLearning/blob/master/examples/ssl_classpriors.py).
```py
import graphlearning as gl

labels = gl.datasets.load('mnist', labels_only=True)
W = gl.weightmatrix.knn('mnist', 10, metric='vae')

num_train_per_class = 1
train_ind = gl.trainsets.generate(labels, rate=num_train_per_class)
train_labels = labels[train_ind]

class_priors = gl.utils.class_priors(labels)
model = gl.ssl.laplace(W, class_priors=class_priors)
model.fit(train_ind,train_labels)

pred_labels = model.predict(ignore_class_priors=True)
accuracy = gl.ssl.ssl_accuracy(labels,pred_labels,train_ind)
print(model.name + ' without class priors: %.2f%%'%accuracy)

pred_labels = model.predict()
accuracy = gl.ssl.ssl_accuracy(labels,pred_labels,train_ind)
print(model.name + ' with class priors: %.2f%%'%accuracy)
```
"""
import numpy as np
from scipy import sparse
from abc import ABCMeta, abstractmethod
import multiprocessing
from joblib import Parallel, delayed
import sys, os, datetime, matplotlib
import matplotlib.pyplot as plt


from . import utils 
from . import graph



#Directories
results_dir = os.path.join(os.getcwd(),'results')

class ssl:
    __metaclass__ = ABCMeta

    def __init__(self, W, class_priors):
        if W is None:
            self.graph = None
        else:
            self.set_graph(W)
        self.prob = None
        self.fitted = False
        self.name = ''
        self.accuracy_filename = ''
        self.requires_eig = False
        self.onevsrest = False
        self.similarity = True
        self.class_priors = class_priors
        if self.class_priors is not None:
            self.class_priors = self.class_priors / np.sum(self.class_priors)
        self.weights = 1
        self.class_priors_error = 1

    def set_graph(self, W):
        """Set Graph
        ===================

        Sets the graph object for semi-supervised learning.

        Implements 3 different solvers, spectral, gradient_descent, and conjugate_gradient. 
        GPU acceleration is available for gradient descent. See [1] for details.

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        """

        if type(W) == graph.graph:
            self.graph = W
        else:
            self.graph = graph.graph(W)

    def volume_label_projection(self):
        """Volume label projection
        ======
    
        Projects class probabilities to labels while enforcing a constraint on 
        class priors (i.e., class volumes). Does not return anything, just modifies `self.weights`.
        
        Returns
        -------
        labels : numpy array (int)
            Predicted labels after volume correction.
        """

        n = self.graph.num_nodes
        k = self.prob.shape[1]
        if type(self.weights) == int:
            self.weights = np.ones((k,))

        #Time step
        dt = 0.1
        if self.similarity:
            dt *= -1

        #np.set_printoptions(formatter={'float': lambda x: "{0:0.3f}".format(x)})
        i = 0
        err = 1
        while i < 1e4 and err > 1e-3:
            i += 1
            class_size = np.mean(utils.labels_to_onehot(self.predict(),k),axis=0)
            #print(class_size-self.class_priors)
            grad = class_size - self.class_priors
            err = np.max(np.absolute(grad))
            self.weights += dt*grad
            self.weights = self.weights/self.weights[0]

        self.class_priors_error = err

        return self.predict()

    def get_accuracy_filename(self):
        """Get accuracy filename
        ========

        Returns name of the file that will store the accuracy results for `ssl_trials.py`.

        Returns
        -------
        fname : str
            Accuracy filename.
        """

        fname = self.accuracy_filename
        if self.class_priors is not None:
            fname += '_classpriors'
        fname += '_accuracy.csv'
        return fname


    def predict(self, ignore_class_priors=False):
        """Predict
        ========

        Makes label predictions based on the probabilities computed by `fit()`.
        Will use a volume label projection if `class_priors` were given, to ensure 
        the number of nodes predicted in each class is correct.
        
        Parameters
        ----------
        ignore_class_priors : bool (optional), default=False
            Used to disable the volume constrained label decision, when `class_priors` has been provided. 

        Returns
        -------
        pred_labels : (int) numpy array
            Predicted labels as integers for all datapoints in the graph.
        """

        if self.fitted == False:
            sys.exit('Model has not been fitted yet.')

        if ignore_class_priors:
            w = 1
        else:
            w = self.weights

        scores = self.prob - np.min(self.prob)
        scores = scores/np.max(scores)

        #Check if scores are similarity or distance
        if self.similarity:
            pred_labels = np.argmax(scores*w,axis=1)
        else: #Then distances
            pred_labels = np.argmin(scores*w,axis=1)

        return pred_labels 

    def fit_predict(self, train_ind, train_labels, all_labels=None):
        """Fit and predict
        ======

        Calls fit() and predict() sequentially.

        Parameters
        ----------
        train_ind : numpy array, int
            Indicies of training points.
        train_labels : numpy array, int
            Training labels as integers \\(0,1,\\dots,k-1\\) for \\(k\\) classes.
        all_labels : numpy array, int (optional)
            True labels for all datapoints.

        Returns
        -------
        pred_labels : (int) numpy array
            Predicted labels as integers for all datapoints in the graph.
        """

        self.fit(train_ind, train_labels, all_labels=all_labels)
        return self.predict()

    def ssl_trials(self, trainsets, labels, num_cores=1, tag='', save_results=True, overwrite=False, num_trials=-1):
        """Semi-supervised learning trials
        ===================

        Runs a semi-supervised learning algorithm on a list of training sets,
        recording the label rates and saves the results to a csv file in the
        local folder results/. The filename is controlled by the member function
        model.get_accuracy_filename(). The trial will abort if the accuracy result
        file already exists, unless `overwrite=True`.

        Parameters
        ----------
        trainsets : list of numpy arrays
            Collection of training sets to run semi-supervised learning on. This is the output
            of `graphlearning.trainsets.generate` or `graphlearning.trainsets.load`.
        labels : numpy array (int)
            Integer array of labels for entire dataset.
        num_cores : int
            Number of cores to use for parallel processing over trials
        tag : str (optional), default=''
            An extra identifying tag to add to the accuracy filename.
        save_results : bool (optional), default=True
            Whether to save results to csv file or just print to the screen.
        overwrite : bool (optional), default = False
            Whether to overwrite existing results file, if found. If `overwrite=False`, 
            `save_results=True`, and the results file is found, the trial is aborted.
        num_trials: int (optional), defualt = -1
            Number of trials. Any negative number runs all trials.
        """

        if num_trials > 0:
            trainsets = trainsets[:num_trials]

        #Print name
        print('\nModel: '+self.name)

        if save_results:
            if not os.path.exists(results_dir):
                os.makedirs(results_dir)

            outfile = os.path.join(results_dir, tag+self.get_accuracy_filename())
            if (not overwrite) and os.path.exists(outfile):
                print('Aborting: SSL trial ('+self.get_accuracy_filename()+') already completed , and overwrite is False.')
                return
            f = open(outfile,"w")
            #now = datetime.datetime.now()
            #f.write("Date/Time, "+now.strftime("%Y-%m-%d_%H:%M")+"\n")
            if self.class_priors is None:
                f.write('Number of labels,Accuracy\n')
            else:
                f.write('Number of labels,Accuracy,Accuracy with class priors,Class priors error\n')
            f.close()

        if save_results:
            print('Results File: '+outfile)

        if self.class_priors is None:
            print('\nNumber of labels,Accuracy')
        else:
            print('\nNumber of labels,Accuracy,Accuracy with class priors,Class priors error')

        #Trial run to trigger eigensolvers, when needed
        if self.requires_eig:
            pred_labels = self.fit_predict(trainsets[0], labels[trainsets[0]])
     
        def one_trial(train_ind):

            #Number of labels
            num_train = len(train_ind)
            train_labels = labels[train_ind]

            #Run semi-supervised learning
            pred_labels = self.fit_predict(train_ind, train_labels)

            #Compute accuracy
            accuracy = ssl_accuracy(pred_labels,labels,train_ind)

            #If class priors were provided, check accuracy without priors
            if self.class_priors is not None:
                pred_labels = self.predict(ignore_class_priors=True)
                accuracy_without_priors = ssl_accuracy(pred_labels,labels,train_ind)
            
            #Print to terminal
            if self.class_priors is None:
                print("%d" % num_train + ",%.2f" % accuracy)
            else:
                print("%d,%.2f,%.2f,%.5f" % (num_train,accuracy_without_priors,accuracy,self.class_priors_error))

            #Write to file
            if save_results:
                f = open(outfile,"a+")
                if self.class_priors is None:
                    f.write("%d" % num_train + ",%.2f\n" % accuracy)
                else:
                    f.write("%d,%.2f,%.2f,%.5f\n" % (num_train,accuracy_without_priors,accuracy,
                                                                            self.class_priors_error))
                f.close()

        num_cores = min(multiprocessing.cpu_count(),num_cores)

        if num_cores == 1:
            for train_ind in trainsets:
                one_trial(train_ind)
        else:
            Parallel(n_jobs=num_cores)(delayed(one_trial)(train_ind) for train_ind in trainsets)

    def trials_statistics(self, tag=''):
        """Trials statistics
        ===================

        Loads accuracy scores from each trial from csv files created by `ssl_trials`
        and returns summary statistics (mean and standard deviation of accuracy).

        Parameters
        ----------
        tag : str (optional), default=''
            An extra identifying tag to add to the accuracy filename.

        Returns
        -------
        num_train : numpy array
            Number of training examples in each label rate experiment.
        acc_mean : numpy array
            Mean accuracy over all trials in each experiment.
        acc_stddev : numpy array
            Standard deviation of accuracy over all trials in each experiment.
        num_trials : int
            Number of trials for each label rate.
        """

        accuracy_filename = os.path.join(results_dir, tag+self.get_accuracy_filename())
        X = utils.csvread(accuracy_filename)
        num_train = np.unique(X[:,0])

        acc_mean = []
        acc_stddev = []
        for n in num_train:
            Y = X[X[:,0]==n,1:]
            acc_mean += [np.mean(Y,axis=0)]
            acc_stddev += [np.std(Y,axis=0)]

        num_trials = int(len(X[:,0])/len(num_train))
        acc_mean = np.array(acc_mean)
        acc_stddev = np.array(acc_stddev)
        return num_train, acc_mean, acc_stddev, num_trials

   
    def fit(self, train_ind, train_labels, all_labels=None):
        """Fit
        ======

        Solves graph-based learning problem, computing the probability 
        that each node belongs to each class. If `all_labels` is provided, 
        then the solver operates in verbose mode, printing out accuracy
        at each iteration.

        Parameters
        ----------
        train_ind : numpy array, int
            Indicies of training points.
        train_labels : numpy array, int
            Training labels as integers \\(0,1,\\dots,k-1\\) for \\(k\\) classes.
        all_labels : numpy array, int (optional)
            True labels for all datapoints.

        Returns
        -------
        u : (n,k) numpy array, float
            Per-class scores computed by graph-based learning for each node and each class. For some methods these are probabilities (i.e., for Laplace learning), while for others they can take on negative values (e.g., Poisson learning). The class label prediction is either the argmax or argmin over the rows of u (most methods are argmax, except distance-function based methods like nearest neighbor and peikonal).
        """

        if self.graph is None:
            sys.exit('SSL object has no graph. Use graph.set_graph() to provide a graph for SSL.')

        self.fitted = True

        #If a one-vs-rest classifier
        if self.onevsrest:
            unique_labels = np.unique(train_labels)
            num_labels = len(unique_labels)
            self.prob = np.zeros((self.graph.num_nodes,num_labels))
            for i,l in enumerate(unique_labels):
                self.prob[:,i] = self._fit(train_ind, train_labels==l)
        else:
            self.prob = self._fit(train_ind, train_labels, all_labels=all_labels)

        if self.class_priors is not None:
            self.volume_label_projection()

        return self.prob

    @abstractmethod
    def _fit(self, train_ind, train_labels, all_labels=None):
        """Internal Fit Function
        ======

        Internal fit function that any ssl object must override. If `self.onevsrest=True` then
        `train_labels` are binary in the one-vs-rest framework, and `_fit` must return
        a scalar numpy array. Otherwise the labels are integer and `_fit` must return an (n,k)
        numpy array of probabilities, where `k` is the number of classes.

        Parameters
        ----------
        train_ind : numpy array, int
            Indicies of training points.
        train_labels : numpy array, int
            Training labels as integers \\(0,1,\\dots,k-1\\) for \\(k\\) classes, or binary
            if `self.onevsrest=True`.
        all_labels : numpy array, int (optional)
            True labels for all datapoints.

        Returns
        -------
        u : numpy array, float
            (n,k) array of probabilities computed by graph-based learning for each node and each class, unless
            `self.onevsrest=True`, in which case it is a length n numpy array of probablities for the current 
            class.
        """
        raise NotImplementedError("Must override _fit")


class poisson(ssl):
    def __init__(self, W=None, class_priors=None, solver='conjugate_gradient', p=1, use_cuda=False, min_iter=50, max_iter=1000, tol=1e-3, spectral_cutoff=10):
        """Poisson Learning
        ===================

        Semi-supervised learning via the solution of the Poisson equation 
        \\[L^p u = \\sum_{j=1}^m \\delta_j(y_j - \\overline{y})^T,\\]
        where \\(L=D-W\\) is the combinatorial graph Laplacian, 
        \\(y_j\\) are the label vectors, \\(\\overline{y} = \\frac{1}{m}\\sum_{i=1}^m y_j\\) 
        is the average label vector, \\(m\\) is the number of training points, and 
        \\(\\delta_j\\) are standard basis vectors. See the reference for more details.

        Implements 3 different solvers, spectral, gradient_descent, and conjugate_gradient. 
        GPU acceleration is available for gradient descent. See [1] for details.

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        solver : {'spectral', 'conjugate_gradient', 'gradient_descent'} (optional), default='conjugate_gradient'
            Choice of solver for Poisson learning.
        p : int (optional), default=1
            Power for Laplacian, can be any positive real number. Solver will default to 'spectral' if p!=1.
        use_cuda : bool (optional), default=False
            Whether to use GPU acceleration for gradient descent solver.
        min_iter : int (optional), default=50
            Minimum number of iterations of gradient descent before checking stopping condition.
        max_iter : int (optional), default=1000
            Maximum number of iterations of gradient descent.
        tol : float (optional), default=1e-3
            Tolerance for conjugate gradient solver.
        spectral_cutoff : int (optional), default=10
            Number of eigenvectors to use for spectral solver.

        Examples
        --------
        Poisson learning works on directed (i.e., nonsymmetric) graphs with the gradient descent solver: [poisson_directed.py](https://github.com/jwcalder/GraphLearning/blob/master/examples/poisson_directed.py).
        ```py
        import numpy as np
        import graphlearning as gl
        import matplotlib.pyplot as plt
        import sklearn.datasets as datasets

        X,labels = datasets.make_moons(n_samples=500,noise=0.1)
        W = gl.weightmatrix.knn(X,10,symmetrize=False)

        train_ind = gl.trainsets.generate(labels, rate=5)
        train_labels = labels[train_ind]

        model = gl.ssl.poisson(W, solver='gradient_descent')
        pred_labels = model.fit_predict(train_ind, train_labels)

        accuracy = gl.ssl.ssl_accuracy(pred_labels, labels, train_ind)   
        print("Accuracy: %.2f%%"%accuracy)

        plt.scatter(X[:,0],X[:,1], c=pred_labels)
        plt.scatter(X[train_ind,0],X[train_ind,1], c='r')
        plt.show()
        ```

        Reference
        ---------
        [1] J. Calder, B. Cook, M. Thorpe, D. Slepcev. [Poisson Learning: Graph Based Semi-Supervised
        Learning at Very Low Label Rates.](http://proceedings.mlr.press/v119/calder20a.html), 
        Proceedings of the 37th International Conference on Machine Learning, PMLR 119:1306-1316, 2020.
        """
        super().__init__(W, class_priors)
        if solver not in ['conjugate_gradient', 'spectral', 'gradient_descent']:
            sys.exit("Invalid Poisson solver")
        self.solver = solver
        self.p = p
        if p != 1:
            self.solver = 'spectral'
        self.use_cuda = use_cuda
        self.min_iter = min_iter
        self.max_iter = max_iter
        self.tol = tol
        self.spectral_cutoff = spectral_cutoff

        #Setup accuracy filename
        fname = '_poisson' 
        if self.p != 1:
            fname += '_p%.2f'%p
        if self.solver == 'spectral':
            fname += '_N%d'%self.spectral_cutoff
            self.requries_eig = True
        self.accuracy_filename = fname

        #Setup Algorithm name
        self.name = 'Poisson Learning'

    def _fit(self, train_ind, train_labels, all_labels=None):

        n = self.graph.num_nodes
        unique_labels = np.unique(train_labels)
        k = len(unique_labels)
        
        #Zero out diagonal for faster convergence
        W = self.graph.weight_matrix
        W = W - sparse.spdiags(W.diagonal(),0,n,n)
        G = graph.graph(W)

        #Poisson source term
        onehot = utils.labels_to_onehot(train_labels,k)
        source = np.zeros((n, onehot.shape[1]))
        source[train_ind] = onehot - np.mean(onehot, axis=0)
        
        if self.solver == 'conjugate_gradient':  #Conjugate gradient solver

            L = G.laplacian(normalization='normalized')
            D = G.degree_matrix(p=-0.5)
            u = utils.conjgrad(L, D*source, tol=self.tol)
            u = D*u

        elif self.solver == "gradient_descent":

            #Setup matrices
            D = G.degree_matrix(p=-1)
            P = D*W.transpose()
            Db = D*source

            #Invariant distribution
            v = np.zeros(n)
            v[train_ind]=1
            v = v/np.sum(v)
            deg = G.degree_vector()
            vinf = deg/np.sum(deg)
            RW = W.transpose()*D
            u = np.zeros((n,k))

            #Number of iterations
            T = 0
            if self.use_cuda:

                import torch 

                Pt = utils.torch_sparse(P).cuda()
                ut = torch.from_numpy(u).float().cuda()
                Dbt = torch.from_numpy(Db).float().cuda()

                while (T < self.min_iter or np.max(np.absolute(v-vinf)) > 1/n) and (T < self.max_iter):
                    ut = torch.sparse.addmm(Dbt,Pt,ut)
                    v = RW*v
                    T = T + 1

                #Transfer to CPU and convert to numpy
                u = ut.cpu().numpy()

            else: #Use CPU

                while (T < self.min_iter or np.max(np.absolute(v-vinf)) > 1/n) and (T < self.max_iter):
                    u = Db + P*u
                    v = RW*v
                    T = T + 1

                    #Compute accuracy if all labels are provided
                    if all_labels is not None:
                        self.prob = u
                        labels = self.predict()
                        acc = ssl_accuracy(labels,all_labels,train_ind)
                        print('%d,Accuracy = %.2f'%(T,acc))
                
        #Use spectral solver
        elif self.solver == 'spectral':

            vals, vecs = G.eigen_decomp(normalization='randomwalk', k=self.spectral_cutoff+1)
            V = vecs[:,1:]
            vals = vals[1:]
            if self.p != 1:
                vals = vals**self.p
            L = sparse.spdiags(1/vals, 0, self.spectral_cutoff, self.spectral_cutoff)
            u = V@(L@(V.T@source))

        else:
            sys.exit("Invalid Poisson solver " + self.solver)

        return u

class poisson_mbo(ssl):
    def __init__(self, W=None, class_priors=None, solver='conjugate_gradient', use_cuda=False, min_iter=50, max_iter=1000, tol=1e-3, spectral_cutoff=10, Ns=40, mu=1, T=20):
        """PoissonMBO 
        ===================

        Semi-supervised learning via Poisson MBO method [1]. class_priors must be provided.

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). 
        solver : {'spectral', 'conjugate_gradient', 'gradient_descent'} (optional), default='conjugate_gradient'
            Choice of solver for Poisson learning.
        use_cuda : bool (optional), default=False
            Whether to use GPU acceleration for gradient descent solver.
        min_iter : int (optional), default=50
            Minimum number of iterations of gradient descent before checking stopping condition.
        max_iter : int (optional), default=1000
            Maximum number of iterations of gradient descent.
        tol : float (optional), default=1e-3
            Tolerance for conjugate gradient solver.
        spectral_cutoff : int (optional), default=10
            Number of eigenvectors to use for spectral solver.
        Ns : int (optional), default=40
            Number of inner iterations in PoissonMBO.
        mu : float (optional), default=1
            Fidelity parameter.
        T : int (optional), default=20
            Number of MBO iterations.
        
        Example
        -------
        Running PoissonMBO on MNIST at 1 label per class: [poisson_mbo.py](https://github.com/jwcalder/GraphLearning/blob/master/examples/poisson_mbo.py).
        ```py
        import graphlearning as gl

        labels = gl.datasets.load('mnist', labels_only=True)
        W = gl.weightmatrix.knn('mnist', 10, metric='vae')

        num_train_per_class = 1
        train_ind = gl.trainsets.generate(labels, rate=num_train_per_class)
        train_labels = labels[train_ind]

        class_priors = gl.utils.class_priors(labels)
        model = gl.ssl.poisson_mbo(W, class_priors)
        pred_labels = model.fit_predict(train_ind,train_labels,all_labels=labels)

        accuracy = gl.ssl.ssl_accuracy(labels,pred_labels,train_ind)
        print(model.name + ': %.2f%%'%accuracy)
        ```
        Reference
        ---------
        [1] J. Calder, B. Cook, M. Thorpe, D. Slepcev. [Poisson Learning: Graph Based Semi-Supervised
        Learning at Very Low Label Rates.](http://proceedings.mlr.press/v119/calder20a.html), 
        Proceedings of the 37th International Conference on Machine Learning, PMLR 119:1306-1316, 2020.
        """
        super().__init__(W, class_priors)

        self.poisson_model = poisson(W, solver=solver, use_cuda=use_cuda, min_iter=min_iter, 
                                     max_iter=max_iter, tol=tol, spectral_cutoff=spectral_cutoff)

        self.Ns = Ns
        self.mu = mu
        self.T = T
        self.use_cuda = use_cuda

        #Setup accuracy filename
        fname = '_poisson_mbo' 
        if solver == 'spectral':
            fname += '_N%d'%spectral_cutoff
            self.requries_eig = True
        fname += '_Ns_%d_mu_%.2f_T_%d'%(Ns,mu,T)
        self.accuracy_filename = fname

        #Setup Algorithm name
        self.name = 'Poisson MBO' 

    def _fit(self, train_ind, train_labels, all_labels=None):

        #Short forms
        Ns = self.Ns
        mu = self.mu
        T = self.T
        use_cuda = self.use_cuda

        n = self.graph.num_nodes
        unique_labels = np.unique(train_labels)
        k = len(unique_labels)

        #Zero out diagonal for faster convergence
        W = self.graph.weight_matrix
        W = W - sparse.spdiags(W.diagonal(),0,n,n)
        G = graph.graph(W)
        
        #Poisson source term
        onehot = utils.labels_to_onehot(train_labels,k)
        source = np.zeros((n, onehot.shape[1]))
        source[train_ind] = onehot - np.mean(onehot, axis=0)

        #Initialize via Poisson learning
        labels = self.poisson_model.fit_predict(train_ind, train_labels, all_labels=all_labels)
        u = utils.labels_to_onehot(labels,k)

        #Time step for stability
        dt = 1/np.max(G.degree_vector())

        #Precompute some things
        P = sparse.identity(n) - dt*G.laplacian()
        Db = mu*dt*source

        if use_cuda:
            import torch
            Pt = utils.torch_sparse(P).cuda()
            Dbt = torch.from_numpy(Db).float().cuda()

        for i in range(T):

            #Heat equation step
            if use_cuda:

                #Put on GPU and run heat equation
                ut = torch.from_numpy(u).float().cuda()
                for j in range(Ns):
                    ut = torch.sparse.addmm(Dbt,Pt,ut)

                #Put back on CPU
                u = ut.cpu().numpy()
             
            else: #Use CPU 
                for j in range(Ns):
                    u = P*u + Db

            #Projection step
            self.prob = u
            labels = self.volume_label_projection()
            u = utils.labels_to_onehot(labels,k)

            #Compute accuracy if all labels are provided
            if all_labels is not None:
                acc = ssl_accuracy(labels,all_labels,train_ind)
                print('%d, Accuracy = %.2f'%(i,acc))

        return u

class volume_mbo(ssl):
    def __init__(self, W=None, class_priors=None, temperature=0.1, volume_constraint=0.5):
        """Volume MBO
        ===================

        Semi-supervised learning with the VolumeMBO method [1]. class_priors must be provided.

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array
            Class priors (fraction of data belonging to each class). 
        temperature : float (optional), default=0.1
            Temperature for volume constrained MBO.
        volume_constraint : float (optional), default=0.5
            The number of points in each class is constrained to be a mulitple \\(\\lambda\\) of the true
            class size, where 
            \\[ \\text{volume_constraint} \\leq \\lambda \\leq 2-\\text{volume_constraint}.\\]
            Setting `volume_constraint=1` yields the tightest constraint.

        References
        ----------
        [1] M. Jacobs, E. Merkurjev, and S. Esedoḡlu. [Auction dynamics: A volume constrained MBO scheme.](https://www.sciencedirect.com/science/article/pii/S0021999117308033?casa_token=kNahPd2vu50AAAAA:uJQYQVnmMBV_oL0CG1UcOIulY4vhclMGTztm-jjAzy9Lns7rtoOnKs4iyvLOjKXaHU-D6qrQJT4) Journal of Computational Physics 354:288-310, 2018. 
        """
        super().__init__(W, None)

        if class_priors is None:
            sys.exit("Class priors must be provided for Volume MBO.")
        self.class_counts = (self.graph.num_nodes*class_priors).astype(int)
        self.temperature = temperature
        self.volume_constraint = volume_constraint

        #Setup accuracy filename and model name
        self.accuracy_filename = '_volume_mbo_temp_%.2f_vol_%.2f'%(temperature,volume_constraint)
        self.name = 'Volume MBO (T=%.2f, V=%.2f)'%(temperature,volume_constraint)


    def _fit(self, train_ind, train_labels, all_labels=None):

        #Import c extensions
        from . import cextensions

        n = self.graph.num_nodes

        #Set diagonal entries to zero
        W = self.graph.weight_matrix
        W = W - sparse.spdiags(W.diagonal(),0,n,n)
        G = graph.graph(W)

        #Set up graph for C-code
        k = len(np.unique(train_labels))
        u = np.zeros((n,))
        WI,WJ,WV = sparse.find(W)

        #Type casting and memory blocking
        u = np.ascontiguousarray(u,dtype=np.int32)
        WI = np.ascontiguousarray(WI,dtype=np.int32)
        WJ = np.ascontiguousarray(WJ,dtype=np.int32)
        WV = np.ascontiguousarray(WV,dtype=np.float32)
        train_ind = np.ascontiguousarray(train_ind,dtype=np.int32)
        train_labels = np.ascontiguousarray(train_labels,dtype=np.int32)
        ClassCounts = np.ascontiguousarray(self.class_counts,dtype=np.int32)

        cextensions.volume_mbo(u,WJ,WI,WV,train_ind,train_labels,ClassCounts,k,1.0,self.temperature,self.volume_constraint)

        #Set given labels and convert to vector format
        u[train_ind] = train_labels
        u = utils.labels_to_onehot(u,k)
        return u

class multiclass_mbo(ssl):
    def __init__(self, W=None, class_priors=None, Ns=6, T=10, dt=0.15, mu=50, num_eig=50):
        """Multiclass MBO
        ===================

        Semi-supervised learning via the Multiclass MBO method [1].

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        Ns : int (optional), default=6
            Number of inner iterations.
        T : int (optional), default=10
            Number of outer iterations.
        dt : float (optional), default=0.15
            Time step.
        mu : float (optional), default=50
            Fidelity penalty.
        num_eig : int (optional), default=300
            Number of eigenvectors.
        
        References
        ---------
        [1] C. Garcia-Cardona, E. Merkurjev, A.L. Bertozzi, A. Flenner, and A.G. Percus. [Multiclass data segmentation using diffuse interface methods on graphs.](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.743.9516&rep=rep1&type=pdf) IEEE transactions on pattern analysis and machine intelligence, 36(8), 1600-1613, 2014.
        """
        super().__init__(W, class_priors)

        self.Ns = Ns
        self.T = T
        self.dt = dt
        self.mu = mu
        self.num_eig = num_eig
        self.requires_eig = True

        #Setup accuracy filename
        self.accuracy_filename = '_multiclass_mbo_Ns_%d_T_%d_dt_%.3f_mu_%.2f'%(Ns,T,dt,mu)
        self.name = 'Multiclass MBO'


    def _fit(self, train_ind, train_labels, all_labels=None):

        #Shorten names
        Ns = self.Ns
        T = self.T
        dt = self.dt
        mu = self.mu
        num_eig = self.num_eig

        #Basic parameters
        n = self.graph.num_nodes
        k = len(np.unique(train_labels))

        #Spectral decomposition
        eigvals, X = self.graph.eigen_decomp(normalization='normalized', k=self.num_eig)

        #Form matrices
        V = np.diag(1/(1 + (dt/Ns)*eigvals)) 
        Y = X@V
        Xt = np.transpose(X)

        #Random initial labeling
        u = np.random.rand(k,n)
        u = utils.labels_to_onehot(np.argmax(u,axis=0),k).T
        u[:,train_ind] = utils.labels_to_onehot(train_labels,k).T

        #Indicator of train_ind
        J = np.zeros(n,)
        K = np.zeros(n,)
        J[train_ind] = 1
        K[train_ind] = train_labels
        K = utils.labels_to_onehot(K,k).T

        for i in range(T):
            #Diffusion step
            for s in range(Ns):
                Z = (u - (dt/Ns)*mu*J*(u - K))@Y
                u = Z@Xt
                
            #Projection step
            u = utils.labels_to_onehot(np.argmax(u,axis=0),k).T

            #Compute accuracy if all labels are provided
            if all_labels is not None:
                self.prob = u.T
                labels = self.predict()
                acc = ssl_accuracy(labels,all_labels,train_ind)
                print('Accuracy = %.2f'%acc)

        return u.T

class modularity_mbo(ssl):
    def __init__(self, W=None, class_priors=None, gamma=0.5, epsilon=1, lamb=1, T=20, Ns=5):
        """Modularity MBO
        ===================

        Semi-supervised learning via the Modularity MBO method [1].

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        gamma : float (optional), default=0.5
            Parameter in algorithm.
        epsilon : float (optional), default=1
            Parameter in algorithm.
        lamb : float (optional), default=1
            Parameter in algorithm.
        T : int (optional), default=20
            Number of outer iterations.
        Ns : int (optional), default=5
            Number of inner iterations.
        
        References
        ---------
        [1] Z.M. Boyd, E. Bae, X.C. Tai, and A.L. Bertozzi. [Simplified energy landscape for modularity using total variation.](https://publications.ffi.no/nb/item/asset/dspace:4288/1619750.pdf) SIAM Journal on Applied Mathematics, 78(5), 2439-2464, 2018.
        """
        super().__init__(W, class_priors)

        self.gamma = gamma
        self.epsilon = epsilon
        self.lamb = lamb
        self.requires_eig = True
        self.T = T
        self.Ns = Ns

        #Setup accuracy filename
        self.accuracy_filename = '_modularity_mbo_gamma_%.2f_epsilon_%.2f_lamb_%.2f'%(gamma,epsilon,lamb)
        self.name = 'Modularity MBO'

    def _fit(self, train_ind, train_labels, all_labels=None):

        #Short form
        gamma = self.gamma
        eps = self.epsilon
        lamb = self.lamb
        T = self.T
        Ns = self.Ns

        #One-hot initialization
        n = self.graph.num_nodes
        num_classes = len(np.unique(train_labels))
        train_onehot = utils.labels_to_onehot(train_labels,k)
        u = np.zeros((n,num_classes))
        u[train_ind,:] = train_onehot

        #Spectral decomposition
        num_eig = 5*num_classes
        D, V = self.graph.eigen_decomp(normalization='combinatorial', k=num_eig, gamma=gamma)

        #Time step selection
        deg = self.graph.degree_vector()
        dtlow = 0.15/((gamma+1)*np.max(deg))
        dthigh = np.log(np.linalg.norm(u)/eps)/D[0]
        dt = np.sqrt(dtlow*dthigh)

        #Diffusion matrix
        P = sparse.spdiags(np.exp(-D*dt),0,num_eig,num_eig)@V.T

        #Main MBO iterations
        for i in range(T):

            #Diffusion 
            u = V@(P@u)

            #Training labels
            if lamb > 0:
                for _ in range(Ns):
                    u[train_ind,:] -= (dt/Ns)*lamb*(u[train_ind,:] - train_onehot)

            #Threshold to labels
            labels = np.argmax(u,axis=1)
            
            #Convert to 1-hot vectors
            u = utils.labels_to_onehot(labels,num_classes)

            #Compute accuracy if all labels are provided
            if all_labels is not None:
                self.prob = u
                labels = self.predict()
                acc = ssl_accuracy(labels,all_labels,train_ind)
                print('Accuracy = %.2f'%acc)

        return u


class laplace(ssl):
    def __init__(self, W=None, class_priors=None, X=None, reweighting='none', normalization='combinatorial', 
                 tau=0, order=1, mean_shift=False, tol=1e-5, alpha=2, zeta=1e7, r=0.1):
        """Laplace Learning
        ===================

        Semi-supervised learning via the solution of the Laplace equation
        \\[\\tau u_j + L^m u_j = 0, \\ \\ j \\geq m+1,\\]
        subject to \\(u_j = y_j\\) for \\(j=1,\\dots,m\\), where \\(L=D-W\\) is the 
        combinatorial graph Laplacian and \\(y_j\\) for \\(j=1,\\dots,m\\) are the 
        label vectors. Default order is m=1, and m > 1 corresponds to higher order Laplace Learning.

        The original method was introduced in [1]. This class also implements reweighting 
        schemes `poisson` proposed in [2], `wnll` proposed in [3], and `properly`, proposed in [4].
        If `properly` is selected, the user must additionally provide the data features `X`.

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        X : numpy array (optional)
            Data features, used to construct the graph. This is required for the `properly` weighted 
            graph Laplacian method.
        normalization : {'combinatorial','randomwalk','normalized'} (optional), defualt='combinatorial'
            Normalization for the graph Laplacian.
        reweighting : {'none', 'wnll', 'poisson', 'properly'} (optional), default='none'
            Reweighting scheme for low label rate problems. If 'properly' is selected, the user
            must supply the data features `X`.
        tau : float or numpy array (optional), default=0
            Zeroth order term in Laplace equation. Can be a scalar or vector.
        order : integer (optional), default=1
            Power m for higher order Laplace learning. Currently only integers are allowed. 
        mean_shift : bool (optional), default=False
            Whether to shift output to mean zero.
        tol : float (optional), default=1e-5
            Tolerance for conjugate gradient solver.
        alpha : float (optional), default=2
            Parameter for `properly` reweighting.
        zeta : float (optional), default=1e7
            Parameter for `properly` reweighting.
        r : float (optional), default=0.1
            Radius for `properly` reweighting.

        References
        ---------
        [1] X. Zhu, Z. Ghahramani, and J. D. Lafferty. [Semi-supervised learning using gaussian fields 
        and harmonic functions.](https://www.aaai.org/Papers/ICML/2003/ICML03-118.pdf) Proceedings 
        of the 20th International Conference on Machine Learning (ICML-03), 2003.

        [2] J. Calder, B. Cook, M. Thorpe, D. Slepcev. [Poisson Learning: Graph Based Semi-Supervised
        Learning at Very Low Label Rates.](http://proceedings.mlr.press/v119/calder20a.html), 
        Proceedings of the 37th International Conference on Machine Learning, PMLR 119:1306-1316, 2020.

        [3] Z. Shi, S. Osher, and W. Zhu. [Weighted nonlocal laplacian on interpolation from sparse data.](https://idp.springer.com/authorize/casa?redirect_uri=https://link.springer.com/article/10.1007/s10915-017-0421-z&casa_token=33Z7gqJy3mMAAAAA:iMO0pGmpn_qf5PioVIGocSRq_p4CDm-KNOQhgIC1uvqG9pWlZ6t7I-IZtSJfocFDEHCdMpK8j7Fx1XbzDQ)
        Journal of Scientific Computing 73.2 (2017): 1164-1177.

        [4] J. Calder, D. Slepčev. [Properly-weighted graph Laplacian for semi-supervised learning.](https://link.springer.com/article/10.1007/s00245-019-09637-3) Applied mathematics & optimization (2019): 1-49.
        """
        super().__init__(W, class_priors)

        self.reweighting = reweighting
        self.normalization = normalization
        self.mean_shift = mean_shift
        self.tol = tol
        self.order = order
        self.X = X

        #Set up tau
        if type(tau) in [float,int]:
            self.tau = np.ones(self.graph.num_nodes)*tau
        elif type(tau) is np.ndarray:
            self.tau = tau

        #Setup accuracy filename
        fname = '_laplace' 
        self.name = 'Laplace Learning'
        if self.reweighting != 'none':
            fname += '_' + self.reweighting
            self.name += ': ' + self.reweighting + ' reweighted'
        if self.normalization != 'combinatorial':
            fname += '_' + self.normalization
            self.name += ' ' + self.normalization
        if self.mean_shift:
            fname += '_meanshift'
            self.name += ' with meanshift'
        if self.order > 1:
            fname += '_order%d'%int(self.order)
            self.name += ' order %d'%int(self.order)
        if np.max(self.tau) > 0:
            fname += '_tau_%.3f'%np.max(self.tau)
            self.name += ' tau=%.3f'%np.max(self.tau)

        self.accuracy_filename = fname



    def _fit(self, train_ind, train_labels, all_labels=None):

        #Reweighting
        if self.reweighting == 'none':
            G = self.graph
        else:
            W = self.graph.reweight(train_ind, method=self.reweighting, normalization=self.normalization, X=self.X)
            #W = self.graph.reweight(train_ind, method=self.reweighting, X=self.X)
            G = graph.graph(W)

        #Get some attributes
        n = G.num_nodes
        unique_labels = np.unique(train_labels)
        k = len(unique_labels)
        
        #tau + Graph Laplacian and one-hot labels
        L = sparse.spdiags(self.tau, 0, G.num_nodes, G.num_nodes) + G.laplacian(normalization=self.normalization)
        if self.order > 1:
            Lpow = L*L
            if self.order > 2:
                for i in range(2,self.order):
                    Lpow = L*Lpow
            L = Lpow
        F = utils.labels_to_onehot(train_labels,k)

        #Locations of unlabeled points
        idx = np.full((n,), True, dtype=bool)
        idx[train_ind] = False

        #Right hand side
        b = -L[:,train_ind]*F
        b = b[idx,:]

        #Left hand side matrix
        A = L[idx,:]
        A = A[:,idx]

        #Preconditioner
        m = A.shape[0]
        M = A.diagonal()
        M = sparse.spdiags(1/np.sqrt(M+1e-10),0,m,m).tocsr()
       
        #Conjugate gradient solver
        v = utils.conjgrad(M*A*M, M*b, tol=self.tol)
        v = M*v

        #Add labels back into array
        u = np.zeros((n,k))
        u[idx,:] = v
        u[train_ind,:] = F

        #Mean shift
        if self.mean_shift:
            u -= np.mean(u,axis=0)

        return u

class dynamic_label_propagation(ssl):
    def __init__(self, W=None, class_priors=None, alpha=0.05, lam=0.1, T=2):
        """Dynamic Label Propagation
        ===================

        Semi-supervised learning via dynamic label propagation [1].

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        alpha : float (optional), default=0.05
            Value of parameter \\(\\alpha\\).
        lam : float (optional), default=0.1
            Value of parameter \\(\\lambda\\).
        T : int (optional), default=2
            Number of iterations.

        References
        ---------
        [1] B. Wang, Z. Tu, and J.K. Tsotsos. [Dynamic label propagation for semi-supervised multi-class multi-label classification.](https://openaccess.thecvf.com/content_iccv_2013/html/Wang_Dynamic_Label_Propagation_2013_ICCV_paper.html) Proceedings of the IEEE international conference on computer vision. 2013.
        """
        super().__init__(W, class_priors)

        self.alpha = alpha
        self.lam = lam
        self.T = T

        self.accuracy_filename = '_dynamic_label_propagation'
        self.name = 'Dynamic Label Propagation'
 

    def _fit(self, train_ind, train_labels, all_labels=None):

        #Short forms
        alpha = self.alpha
        lam = self.lam
        T = self.T
        n = self.graph.num_nodes
        k = len(np.unique(train_labels))

        #Zero out diagonal 
        W = self.graph.weight_matrix
        W = W - sparse.spdiags(W.diagonal(),0,n,n)
        G = graph.graph(W)
        
        #Labels to vector and correct position
        K = utils.labels_to_onehot(train_labels,k)
        u = np.zeros((n,k))
        u[train_ind,:] = K 
      
        if n > 5000:
            print("Cannot use Dynamic Label Propagation on large datasets.")
        else:
            #Setup matrices
            Id = sparse.identity(n) 
            D = G.degree_matrix(p=-1)
            P = D*W
            P = np.array(P.todense())
            Pt = np.copy(P)

            for i in range(T):
                v = P@u
                u = Pt@u
                u[train_ind,:] = K
                Pt = P@Pt@np.transpose(P) + alpha*v@np.transpose(v) + lam*Id

                #Compute accuracy if all labels are provided
                if all_labels is not None:
                    self.prob = np.array(u)
                    labels = self.predict()
                    acc = ssl_accuracy(labels,all_labels,train_ind)
                    print('Accuracy = %.2f'%acc)

            u = np.array(u)

        return u


class centered_kernel(ssl):
    def __init__(self, W=None, class_priors=None, tol=1e-10, power_it=100, alpha=1.05):
        """Centered Kernel Method
        ===================

        Semi-supervised learning via the centered kernel method of [1].

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        tol : float (optional), default=1e-10
            Tolerance to solve equation.
        power_it : int (optional), default=100
            Number of power iterations to find largest eigenvalue.
        alpha : float (optional), default = 1.05
            Value of \\(\\alpha\\) as a fraction of largest eigenvalue.

        References
        ---------
        [1] X. Mai and R. Couillet. [Random matrix-inspired improved semi-supervised learning on graphs.](https://romaincouillet.hebfree.org/docs/conf/SSL_ICML18.pdf) International Conference on Machine Learning. 2018.
        """
        super().__init__(W, class_priors)

        self.tol = tol
        self.power_it = power_it
        self.alpha = alpha

        self.accuracy_filename = '_centered_kernel'
        self.name = 'Centered Kernel'
        
    def _fit(self, train_ind, train_labels, all_labels=None):

        n = self.graph.num_nodes
        k = len(np.unique(train_labels))

        #Zero diagonal
        W = self.graph.weight_matrix
        W = W - sparse.spdiags(W.diagonal(),0,n,n)

        #Indicator of train_ind
        K = np.zeros((n,k))
        K[train_ind] = utils.labels_to_onehot(train_labels,k)
        
        #Center labels
        K[train_ind,:] -= np.sum(K,axis=0)/len(train_ind)

        #Initialization
        u = np.copy(K)
        v = np.ones((n,1))
        vt = np.ones((1,n))

        e = np.random.rand(n,1)
        for i in range(self.power_it):
            y = W*(e -  (1/n)*v@(vt@e))
            w = y - (1/n)*v@(vt@y) #=Ae
            l = abs(np.transpose(e)@w/(np.transpose(e)@e))
            e = w/np.linalg.norm(w)

        #Number of iterations
        alpha = self.alpha * l
        err = 1
        while err > self.tol:
            y = W*(u -  (1/n)*v@(vt@u))
            w = (1/alpha)*(y - (1/n)*v@(vt@y)) - u #Laplacian
            w[train_ind,:] = 0
            err = np.max(np.absolute(w))
            u = u + w

            #Compute accuracy if all labels are provided
            if all_labels is not None:
                self.prob = u
                labels = self.predict()
                acc = ssl_accuracy(labels,all_labels,train_ind)
                print('Accuracy = %.2f'%acc)
            
        return u


class sparse_label_propagation(ssl):
    def __init__(self, W=None, class_priors=None, T=100):
        """Sparse Label Propagation
        ===================

        Semi-supervised learning via the sparse label propagation method of [1].

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        T : int (optional), default=100
            Number of iterations

        References
        ---------
        [1] A. Jung, A.O. Hero III, A. Mara, and S. Jahromi. [Semi-supervised learning via sparse label propagation.](https://arxiv.org/abs/1612.01414) arXiv:1612.01414, 2016.
        """
        super().__init__(W, class_priors)

        self.T = T
        self.accuracy_filename = '_sparse_label_propagation'
        self.name = 'Sparse LP'
        
    def _fit(self, train_ind, train_labels, all_labels=None):

        n = self.graph.num_nodes
        k = len(np.unique(train_labels))

        #Sparse matrix with ones in all entries
        B = self.graph.adjacency()

        #Construct matrix 1/2W and 1/deg
        lam = 2*self.graph.weight_matrix - (1-1e-10)*B
        lam = -lam.log1p()
        lam = lam.expm1() + B
        Id = sparse.identity(n) 
        gamma = self.graph.degree_matrix(p=-1)

        #Random initial labeling
        u = np.zeros((k,n))

        #Indicator of train_ind
        one_hot_labels = utils.labels_to_onehot(train_labels,k).T

        #Initialization
        Y = list()
        for j in range(k):
            Gu = self.graph.gradient(u[j,:], weighted=True)
            Y.append(Gu)

        #Main loop for sparse label propagation
        for i in range(self.T):

            u_prev = np.copy(u)
            #Compute div
            for j in range(k):
                div = 2*self.graph.divergence(Y[j])
                u[j,:] = u_prev[j,:] - gamma*div
                u[j,train_ind] = one_hot_labels[j,:]  #Set labels
                u_tilde = 2*u[j,:] - u_prev[j,:]

                Gu = -self.graph.gradient(u_tilde, weighted=True)
                Y[j] = Y[j] + Gu.multiply(lam)
                ind1 = B.multiply(abs(Y[j])>1)
                ind2 = B - ind1
                Y[j] = ind1.multiply(Y[j].sign()) + ind2.multiply(Y[j])

            #Compute accuracy if all labels are provided
            if all_labels is not None:
                self.prob = u.T
                labels = self.predict()
                acc = ssl_accuracy(labels,all_labels,train_ind)
                print('%d,Accuracy = %.2f'%(i,acc))
 
        return u.T


class graph_nearest_neighbor(ssl):
    def __init__(self, W=None, class_priors=None, D=None, alpha=1):
        """Graph nearest neighbor classifier
        ===================

        Semi-supervised learning by graph (geodesic) nearest neighbor classifier. The 
        graph geodesic distance is defined by
        \\[ d_{ij} = \\min_p \\sum_{k=1}^M w_{p_k,p_{k+1}},\\]
        where the minimum is over paths \\(p\\) connecting nodes \\(i\\) and \\(j\\). The label
        returned for each testing point is the label of the closest labeled point.

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        D : numpy array or scipy sparse matrix (optional)
            (n,n) Distance matrix, giving distances between neighbors. If provided,
            this is used to construct a knn density estimator for reweighting the eikonal equation.
        alpha : float (optional), default=1
            Reweighting exponent.
        """
        super().__init__(W, class_priors)


        self.alpha = alpha

        if class_priors is not None:
            self.onevsrest = True
            self.similarity = False

        #Cannot reweight if distance matrix not provided
        if D is None:
            self.f = 1
        else:
            d = D.max(axis=1).toarray().flatten() #distance to furtherest neighbor
            self.f = (d/np.max(d))**alpha

        #Setup accuracy filename and model name
        self.accuracy_filename = '_graph_nearest_neighbor_alpha%.2f'%(self.alpha) 
        self.name = 'Graph NN (alpha=%.2f)'%(self.alpha)

    def _fit(self, train_ind, train_labels, all_labels=None):

        if self.onevsrest:
            u = self.graph.dijkstra(train_ind[train_labels], bdy_val=0, f=self.f)
        else: 
            _,l = self.graph.dijkstra(train_ind, bdy_val=np.zeros_like(train_ind), f=self.f, return_cp=True)
            u = np.zeros(l.shape)
            u[train_ind] = train_labels
            k = len(np.unique(train_labels))
            u = utils.labels_to_onehot(u[l],k)

        return u

class amle(ssl):
    def __init__(self, W=None, class_priors=None, tol=1e-3, max_num_it=1e5, weighted=False, prog=False):
        """AMLE learning
        ===================

        Semi-supervised learning by the Absolutely Minimal Lipschitz Extension (AMLE). This is the same as
        p-Laplace with \\(p=\\infty\\), except that AMLE has an option `weighted=False` that significantly 
        accelerates the solver.

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            (n,n) Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        tol : float (optional), default=1e-3
            Tolerance with which to solve the equation.
        max_num_it : int (optional), default=1e5
            Maximum number of iterations
        weighted : bool (optional), default=False
            When False, the graph is converted to a 0/1 adjacency matrix, which affords a much faster solver.
        prog : bool (optional), default=False
            Whether to print progress information.
        """
        super().__init__(W, class_priors)

        self.tol = tol
        self.max_num_it = max_num_it
        self.weighted = weighted
        self.prog = prog
        self.onevsrest = True

        #Setup accuracy filename and model name
        self.accuracy_filename = '_amle'
        if not self.weighted:
            self.accuracy_filename += '_unweighted'
        self.name = 'AMLE'


    def _fit(self, train_ind, train_labels, all_labels=None):

        u = self.graph.amle(train_ind, train_labels, tol=self.tol, max_num_it=self.max_num_it, 
                                                     weighted=self.weighted, prog=self.prog)
        return u

class peikonal(ssl):
    def __init__(self, W=None, class_priors=None, D=None, p=1, alpha=1, max_num_it=1e5, tol=1e-3, num_bisection_it=30, eps_ball_graph=False):
        """Graph p-eikonal classifier
        ===================

        Semi-supervised learning by via the solution of the graph `graph.peikonal` equation.

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            (n,n) Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        D : numpy array or scipy sparse matrix (optional)
            (n,n) Distance matrix, giving distances between neighbors. If provided,
            this is used to construct a knn density estimator for reweighting the p-eikonal equation.
        p : float (optional), default=1
            Value of exponent p in the p-eikonal equation
        alpha : float (optional), default=1
            Reweighting exponent.
        max_num_it : int (optional), default=1e5
            Maximum number of iterations.
        tol : float (optional), default=1e-3
            Tolerance with which to solve the equation.
        num_bisection_it : int (optional), default=30
            Number of bisection iterations for solver.
        eps_ball_graph : bool (optional), default=False
            Whether the graph is an epsilon-ball graph or not. If it is, then the density reweighting
            will be done with the degree vector, which is a kernel density estimator in this case
        """
        super().__init__(W, class_priors)

        self.p = p
        self.alpha = alpha
        self.max_num_it = max_num_it
        self.tol = tol
        self.num_bisection_it = num_bisection_it
        self.onevsrest = True
        self.similarity = False

        #Cannot reweight if distance matrix not provided
        if D is None:
            if eps_ball_graph:
                d = self.graph.degree_vector()
                self.f = (d/np.max(d))**(-alpha)
            else:
                self.f = 1
        else:
            d = D.max(axis=1).toarray().flatten() #distance to furtherest neighbor
            self.f = (d/np.max(d))**alpha

        #Setup accuracy filename and model name
        self.accuracy_filename = '_peikonal_p%.2f_alpha%.2f'%(self.p,self.alpha) 
        self.name = 'p-eikonal (p=%.2f, alpha=%.2f)'%(self.p,self.alpha)


    def _fit(self, train_ind, train_labels, all_labels=None):

        u = self.graph.peikonal(train_ind[train_labels], bdy_val=0, f=self.f, p=self.p, max_num_it=self.max_num_it, 
                                                tol=self.tol, num_bisection_it=self.num_bisection_it, prog=False)
        return u


class plaplace(ssl):
    def __init__(self, W=None, class_priors=None, p=10, max_num_it=1e6, tol=1e-1, fast=True):
        """Graph p-laplace classifier
        ===================

        Semi-supervised learning by via the solution of the game-theoretic p-Laplace equation [1].

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            (n,n) Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        p : float (optional), default=10
            Value of p in the p-Laplace equation (\\( 2 \\leq p \\leq \\infty\\).
        max_num_it : int (optional), default=1e5
            Maximum number of iterations
        tol : float (optional)
            Tolerance with which to solve the equation. Default tol=0.1 for fast=False and tol=1e-5 otherwise.
        fast : bool (optional), default=True
            Whether to use constant \\(w_{ij}=1\\) weights for the infinity-Laplacian
            which allows a faster algorithm to be used.

        References
        ----------
        [1] M. Flores Rios, J. Calder, and G. Lerman. [Algorithms for \\( \\ell_p\\)-based semi-supervised learning on graphs.](https://arxiv.org/abs/1901.05031) arXiv:1901.05031, 2019.
        """
        super().__init__(W, class_priors)

        self.p = p
        self.max_num_it = max_num_it
        self.tol = tol
        self.onevsrest = True
        self.fast = fast

        if fast:
            self.tol = 1e-5
        #Setup accuracy filename and model name
        self.accuracy_filename = '_plaplace_p%.2f'%self.p 
        self.name = 'p-Laplace (p=%.2f)'%self.p


    def _fit(self, train_ind, train_labels, all_labels=None):
        u = self.graph.plaplace(train_ind, train_labels, self.p, max_num_it=self.max_num_it, tol=self.tol,fast=self.fast)
        return u



class randomwalk(ssl):
    def __init__(self, W=None, class_priors=None, alpha=0.95):
        """Lazy random walk classification
        ===================

        Add description.

        The original method was introduced in [1], and can be interpreted as a lazy random walk. 

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        alpha : float (optional), default=0.95
            Parameter in model.

        References
        ---------
        [1] D. Zhou and B. Schölkopf. [Learning from labeled and unlabeled data using random walks.](https://link.springer.com/chapter/10.1007/978-3-540-28649-3_29) Joint Pattern Recognition Symposium. 
        Springer, Berlin, Heidelberg, 2004.
        """
        super().__init__(W, class_priors)

        self.alpha = alpha

        #Setup accuracy filename and model name
        self.accuracy_filename = '_randomwalk' 
        self.name = 'Lazy Random Walks'


    def _fit(self, train_ind, train_labels, all_labels=None):
        
        alpha = self.alpha

        #Zero diagonals
        n = self.graph.num_nodes
        W = self.graph.weight_matrix
        W = W - sparse.spdiags(W.diagonal(),0,n,n)
        G = graph.graph(W)

        #Construct Laplacian matrix
        L = (1-alpha)*sparse.identity(n) + alpha*G.laplacian(normalization='normalized')

        #Preconditioner
        m = L.shape[0]
        M = L.diagonal()
        M = sparse.spdiags(1/np.sqrt(M+1e-10),0,m,m).tocsr()
       
        #Right hand side
        k = len(np.unique(train_labels))
        onehot = utils.labels_to_onehot(train_labels,k)
        Y = np.zeros((n, onehot.shape[1]))
        Y[train_ind,:] = onehot

        #Conjugate gradient solver
        u = utils.conjgrad(M*L*M, M*Y, tol=1e-6)
        u = M*u

        return u

def ssl_accuracy(pred_labels, true_labels, train_ind):   
    """SSL Accuacy
    ======

    Accuracy for semi-supervised graph learning, taking care to remove training set.
    NOTE: Any true labels with negative values will be removed from 
    accuracy computation.

    Parameters
    ----------
    pred_labels : numpy array, int
        Predicted labels
    true_labels : numpy array, int
        True labels
    train_ind : numpy array, int 
        Indices of training points, which will be removed from the accuracy computation.

    Returns
    -------
    accuracy : float
        Accuracy as a number in [0,100].
    """

        #Remove labeled data
    mask = np.ones(len(pred_labels),dtype=bool)
    if type(train_ind) != np.ndarray:
        print("Warning: ssl_accuracy has been updated and now requires the user to provide the indices of the labeled points, and not just the number of labels. Accuracy computation will be incorrect unless you update your code accordingly. See https://jwcalder.github.io/GraphLearning/ssl.html#two-moons-example")
    else:
        mask[train_ind]=False

    pred_labels = pred_labels[mask]
    true_labels = true_labels[mask]

    #Remove unlabeled nodes
    I = true_labels >=0
    pred_labels = pred_labels[I]
    true_labels = true_labels[I]

    #Compute accuracy
    return 100*np.mean(pred_labels==true_labels)


def accuracy_plot(model_list, tag='', testerror=False, savefile=None, title=None, errorbars=False, 
                  loglog=False, ylim=None, fontsize=16, legend_fontsize=16, label_fontsize=16):
    """Accuracy Plot
    ======

    Creates a plot of accuracy scores for experiments run with `ssl_trials`.

    Parameters
    ----------
    model_list : list of ssl objects
        Models to include in table.
    tag : str (optional), default=''
        An extra identifying tag to add to the accuracy filename.
    testerror : bool (optional), default=False
        Show test error (instead of accuracy) in the table.
    savefile : str (optional), default=None
        If a savefile name is provided, then the plot is saved instead of displayed to the screen
    title : str (optional), default=None
        If title is provided, then it will be added to the plot.
    errorbars : bool (optional), default=False
        Whether to add error bars to plot.
    loglog : bool (optional), default=False
        Make the plot on a loglog scale.
    ylim : tuple (optional), default=None
        If provided, then y-limits are set with `plt.ylim(ylim)`
    fontsize : int (optional), default=16
        Font size text, other than legend and labels
    legend_fontsize : int (optional), default=16
        Font size for legend.
    label_fontsize : int (optional), default=16
        Font size for x and y labels.
    """

    fig = plt.figure()
    if errorbars:
        matplotlib.rcParams.update({'errorbar.capsize': 5})
    matplotlib.rcParams.update({'font.size': fontsize})
    styles = ['^b-','or-','dg-','sk-','pm-','xc-','*y-']
    i = -1
    for model in model_list:
        i = (i+1)%7
        num_train,acc_mean,acc_stddev,num_trials = model.trials_statistics(tag=tag)
        if testerror:
            acc_mean = 100-acc_mean

        if errorbars:
            plt.errorbar(num_train, acc_mean[:,0], fmt=styles[i], yerr=acc_stddev[:,0], label=model.name)
        else:
            if loglog:
                plt.loglog(num_train, acc_mean[:,0], styles[i], label=model.name)
            else:
                plt.plot(num_train, acc_mean[:,0], styles[i], label=model.name) #3rd argumnet=styles[i]

        #If class priors were run as well
        if acc_mean.shape[1] > 1:
            i = (i+1)%7
            if errorbars:
                plt.errorbar(num_train, acc_mean[:,1], fmt=styles[i], yerr=acc_stddev[:,1], label=model.name+'+CP')
            else:
                if loglog:
                    plt.loglog(num_train, acc_mean[:,1], styles[i], label=model.name+'+CP')
                else:
                    plt.plot(num_train, acc_mean[:,1], styles[i], label=model.name+'+CP') #3rd argumnet=styles[i]

     
    plt.xlabel('Number of labels',fontsize=label_fontsize)
    if testerror:
        plt.ylabel('Test error (%)',fontsize=label_fontsize)
        plt.legend(loc='upper right',fontsize=legend_fontsize)
    else:
        plt.ylabel('Accuracy (%)',fontsize=label_fontsize)
        plt.legend(loc='lower right',fontsize=legend_fontsize)
    if title is not None:
        plt.title(title)

    plt.tight_layout()
    plt.grid(True)

    if ylim is not None:
        plt.ylim(ylim)

    #ax.xaxis.set_major_locator(MaxNLocator(integer=True))

    if savefile is not None:
        plt.savefig(savefile)
    else:
        plt.show()

def accuracy_table(model_list, tag='', testerror=False, savefile='table.tex', title='', 
                               append=False, fontsize='small', small_caps=True, two_column=True):
    """Accuracy table
    ======

    Creates a table of accuracy scores for experiments run with `ssl_trials` to be
    included in a LaTeX document.

    Parameters
    ----------
    model_list : list of ssl objects
        Models to include in table.
    tag : str (optional), default=''
        An extra identifying tag to add to the accuracy filename.
    testerror : bool (optional), default=False
        Show test error (instead of accuracy) in the table.
    savefile : str (optional), default='table.tex'
        Filename to save tex code for table.
    title : str (optional), default=''
        Title for table.
    append : bool (optional), default=False
        Whether to create a new tex file or append table to existing file.
    fontsize : str (optional), default='small'
        LaTeX font size.
    small_caps : bool (optional), default=True
        Whether to use small caps in LaTeX for table.
    two_column : bool (optional), default=True
        Whether the table will be in a two-column LaTeX document.
    """


    num_train,acc_mean,acc_stddev,num_trials = model_list[0].trials_statistics(tag=tag)
    m = len(num_train)

    #Determine best algorithm at each label rate
    best = [None]*m
    class_priors_best = [False]*m
    best_score = [0]*m
    for i, model in enumerate(model_list):
        num_train,acc_mean,acc_stddev,num_trials = model.trials_statistics(tag=tag)
        for j in range(m):
            if acc_mean[j,0] > best_score[j]:
                best_score[j] = acc_mean[j,0]
                best[j] = i

        #Check if class priors accuracy was included as well
        if acc_mean.shape[1] > 1:
            for j in range(m):
                if acc_mean[j,1] > best_score[j]:
                    best_score[j] = acc_mean[j,1]
                    class_priors_best[j] = True
                    best[j] = i

    
    if append:
        f = open(savefile,"r")
        lines = f.readlines()
        f.close()
        f = open(savefile,"w")
        f.writelines([item for item in lines[:-1]])
    else:
        f = open(savefile,"w")
        f.write("\\documentclass{article}\n")
        f.write("\\usepackage[T1]{fontenc}\n")
        f.write("\\usepackage{booktabs}\n")
        f.write("\\usepackage[margin=1in]{geometry}\n")
        f.write("\\begin{document}\n")

    f.write("\n\n\n")
    if two_column:
        f.write("\\begin{table*}[t!]\n")
    else:
        f.write("\\begin{table}[t!]\n")
    f.write("\\vspace{-3mm}\n")
    f.write("\\caption{"+title+": Average (standard deviation) classification accuracy over %d trials.}\n"%num_trials)
    f.write("\\vspace{-3mm}\n")
    f.write("\\label{tab:"+title+"}\n")
    f.write("\\vskip 0.15in\n")
    f.write("\\begin{center}\n")
    f.write("\\begin{"+fontsize+"}\n")
    if small_caps:
        f.write("\\begin{sc}\n")
    f.write("\\begin{tabular}{l")
    for i in range(m):
        f.write("l")
    f.write("}\n")
    f.write("\\toprule\n")
    f.write("\\# Labels")
    for i in range(m):
        f.write("&\\textbf{%d}"%int(num_train[i]))
    f.write("\\\\\n")
    f.write("\\midrule\n")
    i = 0

    for i, model in enumerate(model_list):
        num_train,acc_mean,acc_stddev,num_trials = model.trials_statistics(tag=tag)

        f.write(model.name.ljust(15))
        for j in range(m):
            if best[j] == i and not class_priors_best[j]: 
                f.write("&{\\bf %.1f"%acc_mean[j,0]+" (%.1f)}"%acc_stddev[j,0])
            else:
                f.write("&%.1f"%acc_mean[j,0]+" (%.1f)      "%acc_stddev[j,0])
        f.write("\\\\\n")
        
        #Check if class priors accuracy was included as well
        if acc_mean.shape[1] > 1:
            f.write((model.name+'+CP').ljust(15))
            for j in range(m):
                if best[j] == i and class_priors_best[j]: 
                    f.write("&{\\bf %.1f"%acc_mean[j,1]+" (%.1f)}"%acc_stddev[j,1])
                else:
                    f.write("&%.1f"%acc_mean[j,1]+" (%.1f)      "%acc_stddev[j,1])
            f.write("\\\\\n")

    f.write("\\bottomrule\n")
    f.write("\\end{tabular}\n")
    if small_caps:
        f.write("\\end{sc}\n")
    f.write("\\end{"+fontsize+"}\n")
    f.write("\\end{center}\n")
    f.write("\\vskip -0.1in\n")
    if two_column:
        f.write("\\end{table*}")
    else:
        f.write("\\end{table}")
    f.write("\n\n\n")
    f.write("\\end{document}\n")
    f.close()

Functions

def accuracy_plot(model_list, tag='', testerror=False, savefile=None, title=None, errorbars=False, loglog=False, ylim=None, fontsize=16, legend_fontsize=16, label_fontsize=16)

Accuracy Plot

Creates a plot of accuracy scores for experiments run with ssl_trials.

Parameters

model_list : list of ssl objects
Models to include in table.
tag : str (optional), default=''
An extra identifying tag to add to the accuracy filename.
testerror : bool (optional), default=False
Show test error (instead of accuracy) in the table.
savefile : str (optional), default=None
If a savefile name is provided, then the plot is saved instead of displayed to the screen
title : str (optional), default=None
If title is provided, then it will be added to the plot.
errorbars : bool (optional), default=False
Whether to add error bars to plot.
loglog : bool (optional), default=False
Make the plot on a loglog scale.
ylim : tuple (optional), default=None
If provided, then y-limits are set with plt.ylim(ylim)
fontsize : int (optional), default=16
Font size text, other than legend and labels
legend_fontsize : int (optional), default=16
Font size for legend.
label_fontsize : int (optional), default=16
Font size for x and y labels.
Expand source code
def accuracy_plot(model_list, tag='', testerror=False, savefile=None, title=None, errorbars=False, 
                  loglog=False, ylim=None, fontsize=16, legend_fontsize=16, label_fontsize=16):
    """Accuracy Plot
    ======

    Creates a plot of accuracy scores for experiments run with `ssl_trials`.

    Parameters
    ----------
    model_list : list of ssl objects
        Models to include in table.
    tag : str (optional), default=''
        An extra identifying tag to add to the accuracy filename.
    testerror : bool (optional), default=False
        Show test error (instead of accuracy) in the table.
    savefile : str (optional), default=None
        If a savefile name is provided, then the plot is saved instead of displayed to the screen
    title : str (optional), default=None
        If title is provided, then it will be added to the plot.
    errorbars : bool (optional), default=False
        Whether to add error bars to plot.
    loglog : bool (optional), default=False
        Make the plot on a loglog scale.
    ylim : tuple (optional), default=None
        If provided, then y-limits are set with `plt.ylim(ylim)`
    fontsize : int (optional), default=16
        Font size text, other than legend and labels
    legend_fontsize : int (optional), default=16
        Font size for legend.
    label_fontsize : int (optional), default=16
        Font size for x and y labels.
    """

    fig = plt.figure()
    if errorbars:
        matplotlib.rcParams.update({'errorbar.capsize': 5})
    matplotlib.rcParams.update({'font.size': fontsize})
    styles = ['^b-','or-','dg-','sk-','pm-','xc-','*y-']
    i = -1
    for model in model_list:
        i = (i+1)%7
        num_train,acc_mean,acc_stddev,num_trials = model.trials_statistics(tag=tag)
        if testerror:
            acc_mean = 100-acc_mean

        if errorbars:
            plt.errorbar(num_train, acc_mean[:,0], fmt=styles[i], yerr=acc_stddev[:,0], label=model.name)
        else:
            if loglog:
                plt.loglog(num_train, acc_mean[:,0], styles[i], label=model.name)
            else:
                plt.plot(num_train, acc_mean[:,0], styles[i], label=model.name) #3rd argumnet=styles[i]

        #If class priors were run as well
        if acc_mean.shape[1] > 1:
            i = (i+1)%7
            if errorbars:
                plt.errorbar(num_train, acc_mean[:,1], fmt=styles[i], yerr=acc_stddev[:,1], label=model.name+'+CP')
            else:
                if loglog:
                    plt.loglog(num_train, acc_mean[:,1], styles[i], label=model.name+'+CP')
                else:
                    plt.plot(num_train, acc_mean[:,1], styles[i], label=model.name+'+CP') #3rd argumnet=styles[i]

     
    plt.xlabel('Number of labels',fontsize=label_fontsize)
    if testerror:
        plt.ylabel('Test error (%)',fontsize=label_fontsize)
        plt.legend(loc='upper right',fontsize=legend_fontsize)
    else:
        plt.ylabel('Accuracy (%)',fontsize=label_fontsize)
        plt.legend(loc='lower right',fontsize=legend_fontsize)
    if title is not None:
        plt.title(title)

    plt.tight_layout()
    plt.grid(True)

    if ylim is not None:
        plt.ylim(ylim)

    #ax.xaxis.set_major_locator(MaxNLocator(integer=True))

    if savefile is not None:
        plt.savefig(savefile)
    else:
        plt.show()
def accuracy_table(model_list, tag='', testerror=False, savefile='table.tex', title='', append=False, fontsize='small', small_caps=True, two_column=True)

Accuracy table

Creates a table of accuracy scores for experiments run with ssl_trials to be included in a LaTeX document.

Parameters

model_list : list of ssl objects
Models to include in table.
tag : str (optional), default=''
An extra identifying tag to add to the accuracy filename.
testerror : bool (optional), default=False
Show test error (instead of accuracy) in the table.
savefile : str (optional), default='table.tex'
Filename to save tex code for table.
title : str (optional), default=''
Title for table.
append : bool (optional), default=False
Whether to create a new tex file or append table to existing file.
fontsize : str (optional), default='small'
LaTeX font size.
small_caps : bool (optional), default=True
Whether to use small caps in LaTeX for table.
two_column : bool (optional), default=True
Whether the table will be in a two-column LaTeX document.
Expand source code
def accuracy_table(model_list, tag='', testerror=False, savefile='table.tex', title='', 
                               append=False, fontsize='small', small_caps=True, two_column=True):
    """Accuracy table
    ======

    Creates a table of accuracy scores for experiments run with `ssl_trials` to be
    included in a LaTeX document.

    Parameters
    ----------
    model_list : list of ssl objects
        Models to include in table.
    tag : str (optional), default=''
        An extra identifying tag to add to the accuracy filename.
    testerror : bool (optional), default=False
        Show test error (instead of accuracy) in the table.
    savefile : str (optional), default='table.tex'
        Filename to save tex code for table.
    title : str (optional), default=''
        Title for table.
    append : bool (optional), default=False
        Whether to create a new tex file or append table to existing file.
    fontsize : str (optional), default='small'
        LaTeX font size.
    small_caps : bool (optional), default=True
        Whether to use small caps in LaTeX for table.
    two_column : bool (optional), default=True
        Whether the table will be in a two-column LaTeX document.
    """


    num_train,acc_mean,acc_stddev,num_trials = model_list[0].trials_statistics(tag=tag)
    m = len(num_train)

    #Determine best algorithm at each label rate
    best = [None]*m
    class_priors_best = [False]*m
    best_score = [0]*m
    for i, model in enumerate(model_list):
        num_train,acc_mean,acc_stddev,num_trials = model.trials_statistics(tag=tag)
        for j in range(m):
            if acc_mean[j,0] > best_score[j]:
                best_score[j] = acc_mean[j,0]
                best[j] = i

        #Check if class priors accuracy was included as well
        if acc_mean.shape[1] > 1:
            for j in range(m):
                if acc_mean[j,1] > best_score[j]:
                    best_score[j] = acc_mean[j,1]
                    class_priors_best[j] = True
                    best[j] = i

    
    if append:
        f = open(savefile,"r")
        lines = f.readlines()
        f.close()
        f = open(savefile,"w")
        f.writelines([item for item in lines[:-1]])
    else:
        f = open(savefile,"w")
        f.write("\\documentclass{article}\n")
        f.write("\\usepackage[T1]{fontenc}\n")
        f.write("\\usepackage{booktabs}\n")
        f.write("\\usepackage[margin=1in]{geometry}\n")
        f.write("\\begin{document}\n")

    f.write("\n\n\n")
    if two_column:
        f.write("\\begin{table*}[t!]\n")
    else:
        f.write("\\begin{table}[t!]\n")
    f.write("\\vspace{-3mm}\n")
    f.write("\\caption{"+title+": Average (standard deviation) classification accuracy over %d trials.}\n"%num_trials)
    f.write("\\vspace{-3mm}\n")
    f.write("\\label{tab:"+title+"}\n")
    f.write("\\vskip 0.15in\n")
    f.write("\\begin{center}\n")
    f.write("\\begin{"+fontsize+"}\n")
    if small_caps:
        f.write("\\begin{sc}\n")
    f.write("\\begin{tabular}{l")
    for i in range(m):
        f.write("l")
    f.write("}\n")
    f.write("\\toprule\n")
    f.write("\\# Labels")
    for i in range(m):
        f.write("&\\textbf{%d}"%int(num_train[i]))
    f.write("\\\\\n")
    f.write("\\midrule\n")
    i = 0

    for i, model in enumerate(model_list):
        num_train,acc_mean,acc_stddev,num_trials = model.trials_statistics(tag=tag)

        f.write(model.name.ljust(15))
        for j in range(m):
            if best[j] == i and not class_priors_best[j]: 
                f.write("&{\\bf %.1f"%acc_mean[j,0]+" (%.1f)}"%acc_stddev[j,0])
            else:
                f.write("&%.1f"%acc_mean[j,0]+" (%.1f)      "%acc_stddev[j,0])
        f.write("\\\\\n")
        
        #Check if class priors accuracy was included as well
        if acc_mean.shape[1] > 1:
            f.write((model.name+'+CP').ljust(15))
            for j in range(m):
                if best[j] == i and class_priors_best[j]: 
                    f.write("&{\\bf %.1f"%acc_mean[j,1]+" (%.1f)}"%acc_stddev[j,1])
                else:
                    f.write("&%.1f"%acc_mean[j,1]+" (%.1f)      "%acc_stddev[j,1])
            f.write("\\\\\n")

    f.write("\\bottomrule\n")
    f.write("\\end{tabular}\n")
    if small_caps:
        f.write("\\end{sc}\n")
    f.write("\\end{"+fontsize+"}\n")
    f.write("\\end{center}\n")
    f.write("\\vskip -0.1in\n")
    if two_column:
        f.write("\\end{table*}")
    else:
        f.write("\\end{table}")
    f.write("\n\n\n")
    f.write("\\end{document}\n")
    f.close()
def ssl_accuracy(pred_labels, true_labels, train_ind)

SSL Accuacy

Accuracy for semi-supervised graph learning, taking care to remove training set. NOTE: Any true labels with negative values will be removed from accuracy computation.

Parameters

pred_labels : numpy array, int
Predicted labels
true_labels : numpy array, int
True labels
train_ind : numpy array, int
Indices of training points, which will be removed from the accuracy computation.

Returns

accuracy : float
Accuracy as a number in [0,100].
Expand source code
def ssl_accuracy(pred_labels, true_labels, train_ind):   
    """SSL Accuacy
    ======

    Accuracy for semi-supervised graph learning, taking care to remove training set.
    NOTE: Any true labels with negative values will be removed from 
    accuracy computation.

    Parameters
    ----------
    pred_labels : numpy array, int
        Predicted labels
    true_labels : numpy array, int
        True labels
    train_ind : numpy array, int 
        Indices of training points, which will be removed from the accuracy computation.

    Returns
    -------
    accuracy : float
        Accuracy as a number in [0,100].
    """

        #Remove labeled data
    mask = np.ones(len(pred_labels),dtype=bool)
    if type(train_ind) != np.ndarray:
        print("Warning: ssl_accuracy has been updated and now requires the user to provide the indices of the labeled points, and not just the number of labels. Accuracy computation will be incorrect unless you update your code accordingly. See https://jwcalder.github.io/GraphLearning/ssl.html#two-moons-example")
    else:
        mask[train_ind]=False

    pred_labels = pred_labels[mask]
    true_labels = true_labels[mask]

    #Remove unlabeled nodes
    I = true_labels >=0
    pred_labels = pred_labels[I]
    true_labels = true_labels[I]

    #Compute accuracy
    return 100*np.mean(pred_labels==true_labels)

Classes

class amle (W=None, class_priors=None, tol=0.001, max_num_it=100000.0, weighted=False, prog=False)

AMLE learning

Semi-supervised learning by the Absolutely Minimal Lipschitz Extension (AMLE). This is the same as p-Laplace with p=\infty, except that AMLE has an option weighted=False that significantly accelerates the solver.

Parameters

W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
(n,n) Weight matrix representing the graph.
class_priors : numpy array (optional), default=None
Class priors (fraction of data belonging to each class). If provided, the predict function will attempt to automatic balance the label predictions to predict the correct number of nodes in each class.
tol : float (optional), default=1e-3
Tolerance with which to solve the equation.
max_num_it : int (optional), default=1e5
Maximum number of iterations
weighted : bool (optional), default=False
When False, the graph is converted to a 0/1 adjacency matrix, which affords a much faster solver.
prog : bool (optional), default=False
Whether to print progress information.
Expand source code
class amle(ssl):
    def __init__(self, W=None, class_priors=None, tol=1e-3, max_num_it=1e5, weighted=False, prog=False):
        """AMLE learning
        ===================

        Semi-supervised learning by the Absolutely Minimal Lipschitz Extension (AMLE). This is the same as
        p-Laplace with \\(p=\\infty\\), except that AMLE has an option `weighted=False` that significantly 
        accelerates the solver.

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            (n,n) Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        tol : float (optional), default=1e-3
            Tolerance with which to solve the equation.
        max_num_it : int (optional), default=1e5
            Maximum number of iterations
        weighted : bool (optional), default=False
            When False, the graph is converted to a 0/1 adjacency matrix, which affords a much faster solver.
        prog : bool (optional), default=False
            Whether to print progress information.
        """
        super().__init__(W, class_priors)

        self.tol = tol
        self.max_num_it = max_num_it
        self.weighted = weighted
        self.prog = prog
        self.onevsrest = True

        #Setup accuracy filename and model name
        self.accuracy_filename = '_amle'
        if not self.weighted:
            self.accuracy_filename += '_unweighted'
        self.name = 'AMLE'


    def _fit(self, train_ind, train_labels, all_labels=None):

        u = self.graph.amle(train_ind, train_labels, tol=self.tol, max_num_it=self.max_num_it, 
                                                     weighted=self.weighted, prog=self.prog)
        return u

Ancestors

Inherited members

class centered_kernel (W=None, class_priors=None, tol=1e-10, power_it=100, alpha=1.05)

Centered Kernel Method

Semi-supervised learning via the centered kernel method of [1].

Parameters

W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
Weight matrix representing the graph.
class_priors : numpy array (optional), default=None
Class priors (fraction of data belonging to each class). If provided, the predict function will attempt to automatic balance the label predictions to predict the correct number of nodes in each class.
tol : float (optional), default=1e-10
Tolerance to solve equation.
power_it : int (optional), default=100
Number of power iterations to find largest eigenvalue.
alpha : float (optional), default = 1.05
Value of \alpha as a fraction of largest eigenvalue.

References

[1] X. Mai and R. Couillet. Random matrix-inspired improved semi-supervised learning on graphs. International Conference on Machine Learning. 2018.

Expand source code
class centered_kernel(ssl):
    def __init__(self, W=None, class_priors=None, tol=1e-10, power_it=100, alpha=1.05):
        """Centered Kernel Method
        ===================

        Semi-supervised learning via the centered kernel method of [1].

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        tol : float (optional), default=1e-10
            Tolerance to solve equation.
        power_it : int (optional), default=100
            Number of power iterations to find largest eigenvalue.
        alpha : float (optional), default = 1.05
            Value of \\(\\alpha\\) as a fraction of largest eigenvalue.

        References
        ---------
        [1] X. Mai and R. Couillet. [Random matrix-inspired improved semi-supervised learning on graphs.](https://romaincouillet.hebfree.org/docs/conf/SSL_ICML18.pdf) International Conference on Machine Learning. 2018.
        """
        super().__init__(W, class_priors)

        self.tol = tol
        self.power_it = power_it
        self.alpha = alpha

        self.accuracy_filename = '_centered_kernel'
        self.name = 'Centered Kernel'
        
    def _fit(self, train_ind, train_labels, all_labels=None):

        n = self.graph.num_nodes
        k = len(np.unique(train_labels))

        #Zero diagonal
        W = self.graph.weight_matrix
        W = W - sparse.spdiags(W.diagonal(),0,n,n)

        #Indicator of train_ind
        K = np.zeros((n,k))
        K[train_ind] = utils.labels_to_onehot(train_labels,k)
        
        #Center labels
        K[train_ind,:] -= np.sum(K,axis=0)/len(train_ind)

        #Initialization
        u = np.copy(K)
        v = np.ones((n,1))
        vt = np.ones((1,n))

        e = np.random.rand(n,1)
        for i in range(self.power_it):
            y = W*(e -  (1/n)*v@(vt@e))
            w = y - (1/n)*v@(vt@y) #=Ae
            l = abs(np.transpose(e)@w/(np.transpose(e)@e))
            e = w/np.linalg.norm(w)

        #Number of iterations
        alpha = self.alpha * l
        err = 1
        while err > self.tol:
            y = W*(u -  (1/n)*v@(vt@u))
            w = (1/alpha)*(y - (1/n)*v@(vt@y)) - u #Laplacian
            w[train_ind,:] = 0
            err = np.max(np.absolute(w))
            u = u + w

            #Compute accuracy if all labels are provided
            if all_labels is not None:
                self.prob = u
                labels = self.predict()
                acc = ssl_accuracy(labels,all_labels,train_ind)
                print('Accuracy = %.2f'%acc)
            
        return u

Ancestors

Inherited members

class dynamic_label_propagation (W=None, class_priors=None, alpha=0.05, lam=0.1, T=2)

Dynamic Label Propagation

Semi-supervised learning via dynamic label propagation [1].

Parameters

W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
Weight matrix representing the graph.
class_priors : numpy array (optional), default=None
Class priors (fraction of data belonging to each class). If provided, the predict function will attempt to automatic balance the label predictions to predict the correct number of nodes in each class.
alpha : float (optional), default=0.05
Value of parameter \alpha.
lam : float (optional), default=0.1
Value of parameter \lambda.
T : int (optional), default=2
Number of iterations.

References

[1] B. Wang, Z. Tu, and J.K. Tsotsos. Dynamic label propagation for semi-supervised multi-class multi-label classification. Proceedings of the IEEE international conference on computer vision. 2013.

Expand source code
class dynamic_label_propagation(ssl):
    def __init__(self, W=None, class_priors=None, alpha=0.05, lam=0.1, T=2):
        """Dynamic Label Propagation
        ===================

        Semi-supervised learning via dynamic label propagation [1].

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        alpha : float (optional), default=0.05
            Value of parameter \\(\\alpha\\).
        lam : float (optional), default=0.1
            Value of parameter \\(\\lambda\\).
        T : int (optional), default=2
            Number of iterations.

        References
        ---------
        [1] B. Wang, Z. Tu, and J.K. Tsotsos. [Dynamic label propagation for semi-supervised multi-class multi-label classification.](https://openaccess.thecvf.com/content_iccv_2013/html/Wang_Dynamic_Label_Propagation_2013_ICCV_paper.html) Proceedings of the IEEE international conference on computer vision. 2013.
        """
        super().__init__(W, class_priors)

        self.alpha = alpha
        self.lam = lam
        self.T = T

        self.accuracy_filename = '_dynamic_label_propagation'
        self.name = 'Dynamic Label Propagation'
 

    def _fit(self, train_ind, train_labels, all_labels=None):

        #Short forms
        alpha = self.alpha
        lam = self.lam
        T = self.T
        n = self.graph.num_nodes
        k = len(np.unique(train_labels))

        #Zero out diagonal 
        W = self.graph.weight_matrix
        W = W - sparse.spdiags(W.diagonal(),0,n,n)
        G = graph.graph(W)
        
        #Labels to vector and correct position
        K = utils.labels_to_onehot(train_labels,k)
        u = np.zeros((n,k))
        u[train_ind,:] = K 
      
        if n > 5000:
            print("Cannot use Dynamic Label Propagation on large datasets.")
        else:
            #Setup matrices
            Id = sparse.identity(n) 
            D = G.degree_matrix(p=-1)
            P = D*W
            P = np.array(P.todense())
            Pt = np.copy(P)

            for i in range(T):
                v = P@u
                u = Pt@u
                u[train_ind,:] = K
                Pt = P@Pt@np.transpose(P) + alpha*v@np.transpose(v) + lam*Id

                #Compute accuracy if all labels are provided
                if all_labels is not None:
                    self.prob = np.array(u)
                    labels = self.predict()
                    acc = ssl_accuracy(labels,all_labels,train_ind)
                    print('Accuracy = %.2f'%acc)

            u = np.array(u)

        return u

Ancestors

Inherited members

class graph_nearest_neighbor (W=None, class_priors=None, D=None, alpha=1)

Graph nearest neighbor classifier

Semi-supervised learning by graph (geodesic) nearest neighbor classifier. The graph geodesic distance is defined by d_{ij} = \min_p \sum_{k=1}^M w_{p_k,p_{k+1}}, where the minimum is over paths p connecting nodes i and j. The label returned for each testing point is the label of the closest labeled point.

Parameters

W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
Weight matrix representing the graph.
class_priors : numpy array (optional), default=None
Class priors (fraction of data belonging to each class). If provided, the predict function will attempt to automatic balance the label predictions to predict the correct number of nodes in each class.
D : numpy array or scipy sparse matrix (optional)
(n,n) Distance matrix, giving distances between neighbors. If provided, this is used to construct a knn density estimator for reweighting the eikonal equation.
alpha : float (optional), default=1
Reweighting exponent.
Expand source code
class graph_nearest_neighbor(ssl):
    def __init__(self, W=None, class_priors=None, D=None, alpha=1):
        """Graph nearest neighbor classifier
        ===================

        Semi-supervised learning by graph (geodesic) nearest neighbor classifier. The 
        graph geodesic distance is defined by
        \\[ d_{ij} = \\min_p \\sum_{k=1}^M w_{p_k,p_{k+1}},\\]
        where the minimum is over paths \\(p\\) connecting nodes \\(i\\) and \\(j\\). The label
        returned for each testing point is the label of the closest labeled point.

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        D : numpy array or scipy sparse matrix (optional)
            (n,n) Distance matrix, giving distances between neighbors. If provided,
            this is used to construct a knn density estimator for reweighting the eikonal equation.
        alpha : float (optional), default=1
            Reweighting exponent.
        """
        super().__init__(W, class_priors)


        self.alpha = alpha

        if class_priors is not None:
            self.onevsrest = True
            self.similarity = False

        #Cannot reweight if distance matrix not provided
        if D is None:
            self.f = 1
        else:
            d = D.max(axis=1).toarray().flatten() #distance to furtherest neighbor
            self.f = (d/np.max(d))**alpha

        #Setup accuracy filename and model name
        self.accuracy_filename = '_graph_nearest_neighbor_alpha%.2f'%(self.alpha) 
        self.name = 'Graph NN (alpha=%.2f)'%(self.alpha)

    def _fit(self, train_ind, train_labels, all_labels=None):

        if self.onevsrest:
            u = self.graph.dijkstra(train_ind[train_labels], bdy_val=0, f=self.f)
        else: 
            _,l = self.graph.dijkstra(train_ind, bdy_val=np.zeros_like(train_ind), f=self.f, return_cp=True)
            u = np.zeros(l.shape)
            u[train_ind] = train_labels
            k = len(np.unique(train_labels))
            u = utils.labels_to_onehot(u[l],k)

        return u

Ancestors

Inherited members

class laplace (W=None, class_priors=None, X=None, reweighting='none', normalization='combinatorial', tau=0, order=1, mean_shift=False, tol=1e-05, alpha=2, zeta=10000000.0, r=0.1)

Laplace Learning

Semi-supervised learning via the solution of the Laplace equation \tau u_j + L^m u_j = 0, \ \ j \geq m+1, subject to u_j = y_j for j=1,\dots,m, where L=D-W is the combinatorial graph Laplacian and y_j for j=1,\dots,m are the label vectors. Default order is m=1, and m > 1 corresponds to higher order Laplace Learning.

The original method was introduced in [1]. This class also implements reweighting schemes poisson proposed in [2], wnll proposed in [3], and properly, proposed in [4]. If properly is selected, the user must additionally provide the data features X.

Parameters

W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
Weight matrix representing the graph.
class_priors : numpy array (optional), default=None
Class priors (fraction of data belonging to each class). If provided, the predict function will attempt to automatic balance the label predictions to predict the correct number of nodes in each class.
X : numpy array (optional)
Data features, used to construct the graph. This is required for the properly weighted graph Laplacian method.
normalization : {'combinatorial','randomwalk','normalized'} (optional), defualt='combinatorial'
Normalization for the graph Laplacian.
reweighting : {'none', 'wnll', 'poisson', 'properly'} (optional), default='none'
Reweighting scheme for low label rate problems. If 'properly' is selected, the user must supply the data features X.
tau : float or numpy array (optional), default=0
Zeroth order term in Laplace equation. Can be a scalar or vector.
order : integer (optional), default=1
Power m for higher order Laplace learning. Currently only integers are allowed.
mean_shift : bool (optional), default=False
Whether to shift output to mean zero.
tol : float (optional), default=1e-5
Tolerance for conjugate gradient solver.
alpha : float (optional), default=2
Parameter for properly reweighting.
zeta : float (optional), default=1e7
Parameter for properly reweighting.
r : float (optional), default=0.1
Radius for properly reweighting.

References

[1] X. Zhu, Z. Ghahramani, and J. D. Lafferty. Semi-supervised learning using gaussian fields and harmonic functions. Proceedings of the 20th International Conference on Machine Learning (ICML-03), 2003.

[2] J. Calder, B. Cook, M. Thorpe, D. Slepcev. Poisson Learning: Graph Based Semi-Supervised Learning at Very Low Label Rates., Proceedings of the 37th International Conference on Machine Learning, PMLR 119:1306-1316, 2020.

[3] Z. Shi, S. Osher, and W. Zhu. Weighted nonlocal laplacian on interpolation from sparse data. Journal of Scientific Computing 73.2 (2017): 1164-1177.

[4] J. Calder, D. Slepčev. Properly-weighted graph Laplacian for semi-supervised learning. Applied mathematics & optimization (2019): 1-49.

Expand source code
class laplace(ssl):
    def __init__(self, W=None, class_priors=None, X=None, reweighting='none', normalization='combinatorial', 
                 tau=0, order=1, mean_shift=False, tol=1e-5, alpha=2, zeta=1e7, r=0.1):
        """Laplace Learning
        ===================

        Semi-supervised learning via the solution of the Laplace equation
        \\[\\tau u_j + L^m u_j = 0, \\ \\ j \\geq m+1,\\]
        subject to \\(u_j = y_j\\) for \\(j=1,\\dots,m\\), where \\(L=D-W\\) is the 
        combinatorial graph Laplacian and \\(y_j\\) for \\(j=1,\\dots,m\\) are the 
        label vectors. Default order is m=1, and m > 1 corresponds to higher order Laplace Learning.

        The original method was introduced in [1]. This class also implements reweighting 
        schemes `poisson` proposed in [2], `wnll` proposed in [3], and `properly`, proposed in [4].
        If `properly` is selected, the user must additionally provide the data features `X`.

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        X : numpy array (optional)
            Data features, used to construct the graph. This is required for the `properly` weighted 
            graph Laplacian method.
        normalization : {'combinatorial','randomwalk','normalized'} (optional), defualt='combinatorial'
            Normalization for the graph Laplacian.
        reweighting : {'none', 'wnll', 'poisson', 'properly'} (optional), default='none'
            Reweighting scheme for low label rate problems. If 'properly' is selected, the user
            must supply the data features `X`.
        tau : float or numpy array (optional), default=0
            Zeroth order term in Laplace equation. Can be a scalar or vector.
        order : integer (optional), default=1
            Power m for higher order Laplace learning. Currently only integers are allowed. 
        mean_shift : bool (optional), default=False
            Whether to shift output to mean zero.
        tol : float (optional), default=1e-5
            Tolerance for conjugate gradient solver.
        alpha : float (optional), default=2
            Parameter for `properly` reweighting.
        zeta : float (optional), default=1e7
            Parameter for `properly` reweighting.
        r : float (optional), default=0.1
            Radius for `properly` reweighting.

        References
        ---------
        [1] X. Zhu, Z. Ghahramani, and J. D. Lafferty. [Semi-supervised learning using gaussian fields 
        and harmonic functions.](https://www.aaai.org/Papers/ICML/2003/ICML03-118.pdf) Proceedings 
        of the 20th International Conference on Machine Learning (ICML-03), 2003.

        [2] J. Calder, B. Cook, M. Thorpe, D. Slepcev. [Poisson Learning: Graph Based Semi-Supervised
        Learning at Very Low Label Rates.](http://proceedings.mlr.press/v119/calder20a.html), 
        Proceedings of the 37th International Conference on Machine Learning, PMLR 119:1306-1316, 2020.

        [3] Z. Shi, S. Osher, and W. Zhu. [Weighted nonlocal laplacian on interpolation from sparse data.](https://idp.springer.com/authorize/casa?redirect_uri=https://link.springer.com/article/10.1007/s10915-017-0421-z&casa_token=33Z7gqJy3mMAAAAA:iMO0pGmpn_qf5PioVIGocSRq_p4CDm-KNOQhgIC1uvqG9pWlZ6t7I-IZtSJfocFDEHCdMpK8j7Fx1XbzDQ)
        Journal of Scientific Computing 73.2 (2017): 1164-1177.

        [4] J. Calder, D. Slepčev. [Properly-weighted graph Laplacian for semi-supervised learning.](https://link.springer.com/article/10.1007/s00245-019-09637-3) Applied mathematics & optimization (2019): 1-49.
        """
        super().__init__(W, class_priors)

        self.reweighting = reweighting
        self.normalization = normalization
        self.mean_shift = mean_shift
        self.tol = tol
        self.order = order
        self.X = X

        #Set up tau
        if type(tau) in [float,int]:
            self.tau = np.ones(self.graph.num_nodes)*tau
        elif type(tau) is np.ndarray:
            self.tau = tau

        #Setup accuracy filename
        fname = '_laplace' 
        self.name = 'Laplace Learning'
        if self.reweighting != 'none':
            fname += '_' + self.reweighting
            self.name += ': ' + self.reweighting + ' reweighted'
        if self.normalization != 'combinatorial':
            fname += '_' + self.normalization
            self.name += ' ' + self.normalization
        if self.mean_shift:
            fname += '_meanshift'
            self.name += ' with meanshift'
        if self.order > 1:
            fname += '_order%d'%int(self.order)
            self.name += ' order %d'%int(self.order)
        if np.max(self.tau) > 0:
            fname += '_tau_%.3f'%np.max(self.tau)
            self.name += ' tau=%.3f'%np.max(self.tau)

        self.accuracy_filename = fname



    def _fit(self, train_ind, train_labels, all_labels=None):

        #Reweighting
        if self.reweighting == 'none':
            G = self.graph
        else:
            W = self.graph.reweight(train_ind, method=self.reweighting, normalization=self.normalization, X=self.X)
            #W = self.graph.reweight(train_ind, method=self.reweighting, X=self.X)
            G = graph.graph(W)

        #Get some attributes
        n = G.num_nodes
        unique_labels = np.unique(train_labels)
        k = len(unique_labels)
        
        #tau + Graph Laplacian and one-hot labels
        L = sparse.spdiags(self.tau, 0, G.num_nodes, G.num_nodes) + G.laplacian(normalization=self.normalization)
        if self.order > 1:
            Lpow = L*L
            if self.order > 2:
                for i in range(2,self.order):
                    Lpow = L*Lpow
            L = Lpow
        F = utils.labels_to_onehot(train_labels,k)

        #Locations of unlabeled points
        idx = np.full((n,), True, dtype=bool)
        idx[train_ind] = False

        #Right hand side
        b = -L[:,train_ind]*F
        b = b[idx,:]

        #Left hand side matrix
        A = L[idx,:]
        A = A[:,idx]

        #Preconditioner
        m = A.shape[0]
        M = A.diagonal()
        M = sparse.spdiags(1/np.sqrt(M+1e-10),0,m,m).tocsr()
       
        #Conjugate gradient solver
        v = utils.conjgrad(M*A*M, M*b, tol=self.tol)
        v = M*v

        #Add labels back into array
        u = np.zeros((n,k))
        u[idx,:] = v
        u[train_ind,:] = F

        #Mean shift
        if self.mean_shift:
            u -= np.mean(u,axis=0)

        return u

Ancestors

Inherited members

class modularity_mbo (W=None, class_priors=None, gamma=0.5, epsilon=1, lamb=1, T=20, Ns=5)

Modularity MBO

Semi-supervised learning via the Modularity MBO method [1].

Parameters

W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
Weight matrix representing the graph.
class_priors : numpy array (optional), default=None
Class priors (fraction of data belonging to each class). If provided, the predict function will attempt to automatic balance the label predictions to predict the correct number of nodes in each class.
gamma : float (optional), default=0.5
Parameter in algorithm.
epsilon : float (optional), default=1
Parameter in algorithm.
lamb : float (optional), default=1
Parameter in algorithm.
T : int (optional), default=20
Number of outer iterations.
Ns : int (optional), default=5
Number of inner iterations.

References

[1] Z.M. Boyd, E. Bae, X.C. Tai, and A.L. Bertozzi. Simplified energy landscape for modularity using total variation. SIAM Journal on Applied Mathematics, 78(5), 2439-2464, 2018.

Expand source code
class modularity_mbo(ssl):
    def __init__(self, W=None, class_priors=None, gamma=0.5, epsilon=1, lamb=1, T=20, Ns=5):
        """Modularity MBO
        ===================

        Semi-supervised learning via the Modularity MBO method [1].

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        gamma : float (optional), default=0.5
            Parameter in algorithm.
        epsilon : float (optional), default=1
            Parameter in algorithm.
        lamb : float (optional), default=1
            Parameter in algorithm.
        T : int (optional), default=20
            Number of outer iterations.
        Ns : int (optional), default=5
            Number of inner iterations.
        
        References
        ---------
        [1] Z.M. Boyd, E. Bae, X.C. Tai, and A.L. Bertozzi. [Simplified energy landscape for modularity using total variation.](https://publications.ffi.no/nb/item/asset/dspace:4288/1619750.pdf) SIAM Journal on Applied Mathematics, 78(5), 2439-2464, 2018.
        """
        super().__init__(W, class_priors)

        self.gamma = gamma
        self.epsilon = epsilon
        self.lamb = lamb
        self.requires_eig = True
        self.T = T
        self.Ns = Ns

        #Setup accuracy filename
        self.accuracy_filename = '_modularity_mbo_gamma_%.2f_epsilon_%.2f_lamb_%.2f'%(gamma,epsilon,lamb)
        self.name = 'Modularity MBO'

    def _fit(self, train_ind, train_labels, all_labels=None):

        #Short form
        gamma = self.gamma
        eps = self.epsilon
        lamb = self.lamb
        T = self.T
        Ns = self.Ns

        #One-hot initialization
        n = self.graph.num_nodes
        num_classes = len(np.unique(train_labels))
        train_onehot = utils.labels_to_onehot(train_labels,k)
        u = np.zeros((n,num_classes))
        u[train_ind,:] = train_onehot

        #Spectral decomposition
        num_eig = 5*num_classes
        D, V = self.graph.eigen_decomp(normalization='combinatorial', k=num_eig, gamma=gamma)

        #Time step selection
        deg = self.graph.degree_vector()
        dtlow = 0.15/((gamma+1)*np.max(deg))
        dthigh = np.log(np.linalg.norm(u)/eps)/D[0]
        dt = np.sqrt(dtlow*dthigh)

        #Diffusion matrix
        P = sparse.spdiags(np.exp(-D*dt),0,num_eig,num_eig)@V.T

        #Main MBO iterations
        for i in range(T):

            #Diffusion 
            u = V@(P@u)

            #Training labels
            if lamb > 0:
                for _ in range(Ns):
                    u[train_ind,:] -= (dt/Ns)*lamb*(u[train_ind,:] - train_onehot)

            #Threshold to labels
            labels = np.argmax(u,axis=1)
            
            #Convert to 1-hot vectors
            u = utils.labels_to_onehot(labels,num_classes)

            #Compute accuracy if all labels are provided
            if all_labels is not None:
                self.prob = u
                labels = self.predict()
                acc = ssl_accuracy(labels,all_labels,train_ind)
                print('Accuracy = %.2f'%acc)

        return u

Ancestors

Inherited members

class multiclass_mbo (W=None, class_priors=None, Ns=6, T=10, dt=0.15, mu=50, num_eig=50)

Multiclass MBO

Semi-supervised learning via the Multiclass MBO method [1].

Parameters

W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
Weight matrix representing the graph.
class_priors : numpy array (optional), default=None
Class priors (fraction of data belonging to each class). If provided, the predict function will attempt to automatic balance the label predictions to predict the correct number of nodes in each class.
Ns : int (optional), default=6
Number of inner iterations.
T : int (optional), default=10
Number of outer iterations.
dt : float (optional), default=0.15
Time step.
mu : float (optional), default=50
Fidelity penalty.
num_eig : int (optional), default=300
Number of eigenvectors.

References

[1] C. Garcia-Cardona, E. Merkurjev, A.L. Bertozzi, A. Flenner, and A.G. Percus. Multiclass data segmentation using diffuse interface methods on graphs. IEEE transactions on pattern analysis and machine intelligence, 36(8), 1600-1613, 2014.

Expand source code
class multiclass_mbo(ssl):
    def __init__(self, W=None, class_priors=None, Ns=6, T=10, dt=0.15, mu=50, num_eig=50):
        """Multiclass MBO
        ===================

        Semi-supervised learning via the Multiclass MBO method [1].

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        Ns : int (optional), default=6
            Number of inner iterations.
        T : int (optional), default=10
            Number of outer iterations.
        dt : float (optional), default=0.15
            Time step.
        mu : float (optional), default=50
            Fidelity penalty.
        num_eig : int (optional), default=300
            Number of eigenvectors.
        
        References
        ---------
        [1] C. Garcia-Cardona, E. Merkurjev, A.L. Bertozzi, A. Flenner, and A.G. Percus. [Multiclass data segmentation using diffuse interface methods on graphs.](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.743.9516&rep=rep1&type=pdf) IEEE transactions on pattern analysis and machine intelligence, 36(8), 1600-1613, 2014.
        """
        super().__init__(W, class_priors)

        self.Ns = Ns
        self.T = T
        self.dt = dt
        self.mu = mu
        self.num_eig = num_eig
        self.requires_eig = True

        #Setup accuracy filename
        self.accuracy_filename = '_multiclass_mbo_Ns_%d_T_%d_dt_%.3f_mu_%.2f'%(Ns,T,dt,mu)
        self.name = 'Multiclass MBO'


    def _fit(self, train_ind, train_labels, all_labels=None):

        #Shorten names
        Ns = self.Ns
        T = self.T
        dt = self.dt
        mu = self.mu
        num_eig = self.num_eig

        #Basic parameters
        n = self.graph.num_nodes
        k = len(np.unique(train_labels))

        #Spectral decomposition
        eigvals, X = self.graph.eigen_decomp(normalization='normalized', k=self.num_eig)

        #Form matrices
        V = np.diag(1/(1 + (dt/Ns)*eigvals)) 
        Y = X@V
        Xt = np.transpose(X)

        #Random initial labeling
        u = np.random.rand(k,n)
        u = utils.labels_to_onehot(np.argmax(u,axis=0),k).T
        u[:,train_ind] = utils.labels_to_onehot(train_labels,k).T

        #Indicator of train_ind
        J = np.zeros(n,)
        K = np.zeros(n,)
        J[train_ind] = 1
        K[train_ind] = train_labels
        K = utils.labels_to_onehot(K,k).T

        for i in range(T):
            #Diffusion step
            for s in range(Ns):
                Z = (u - (dt/Ns)*mu*J*(u - K))@Y
                u = Z@Xt
                
            #Projection step
            u = utils.labels_to_onehot(np.argmax(u,axis=0),k).T

            #Compute accuracy if all labels are provided
            if all_labels is not None:
                self.prob = u.T
                labels = self.predict()
                acc = ssl_accuracy(labels,all_labels,train_ind)
                print('Accuracy = %.2f'%acc)

        return u.T

Ancestors

Inherited members

class peikonal (W=None, class_priors=None, D=None, p=1, alpha=1, max_num_it=100000.0, tol=0.001, num_bisection_it=30, eps_ball_graph=False)

Graph p-eikonal classifier

Semi-supervised learning by via the solution of the graph graph.peikonal equation.

Parameters

W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
(n,n) Weight matrix representing the graph.
class_priors : numpy array (optional), default=None
Class priors (fraction of data belonging to each class). If provided, the predict function will attempt to automatic balance the label predictions to predict the correct number of nodes in each class.
D : numpy array or scipy sparse matrix (optional)
(n,n) Distance matrix, giving distances between neighbors. If provided, this is used to construct a knn density estimator for reweighting the p-eikonal equation.
p : float (optional), default=1
Value of exponent p in the p-eikonal equation
alpha : float (optional), default=1
Reweighting exponent.
max_num_it : int (optional), default=1e5
Maximum number of iterations.
tol : float (optional), default=1e-3
Tolerance with which to solve the equation.
num_bisection_it : int (optional), default=30
Number of bisection iterations for solver.
eps_ball_graph : bool (optional), default=False
Whether the graph is an epsilon-ball graph or not. If it is, then the density reweighting will be done with the degree vector, which is a kernel density estimator in this case
Expand source code
class peikonal(ssl):
    def __init__(self, W=None, class_priors=None, D=None, p=1, alpha=1, max_num_it=1e5, tol=1e-3, num_bisection_it=30, eps_ball_graph=False):
        """Graph p-eikonal classifier
        ===================

        Semi-supervised learning by via the solution of the graph `graph.peikonal` equation.

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            (n,n) Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        D : numpy array or scipy sparse matrix (optional)
            (n,n) Distance matrix, giving distances between neighbors. If provided,
            this is used to construct a knn density estimator for reweighting the p-eikonal equation.
        p : float (optional), default=1
            Value of exponent p in the p-eikonal equation
        alpha : float (optional), default=1
            Reweighting exponent.
        max_num_it : int (optional), default=1e5
            Maximum number of iterations.
        tol : float (optional), default=1e-3
            Tolerance with which to solve the equation.
        num_bisection_it : int (optional), default=30
            Number of bisection iterations for solver.
        eps_ball_graph : bool (optional), default=False
            Whether the graph is an epsilon-ball graph or not. If it is, then the density reweighting
            will be done with the degree vector, which is a kernel density estimator in this case
        """
        super().__init__(W, class_priors)

        self.p = p
        self.alpha = alpha
        self.max_num_it = max_num_it
        self.tol = tol
        self.num_bisection_it = num_bisection_it
        self.onevsrest = True
        self.similarity = False

        #Cannot reweight if distance matrix not provided
        if D is None:
            if eps_ball_graph:
                d = self.graph.degree_vector()
                self.f = (d/np.max(d))**(-alpha)
            else:
                self.f = 1
        else:
            d = D.max(axis=1).toarray().flatten() #distance to furtherest neighbor
            self.f = (d/np.max(d))**alpha

        #Setup accuracy filename and model name
        self.accuracy_filename = '_peikonal_p%.2f_alpha%.2f'%(self.p,self.alpha) 
        self.name = 'p-eikonal (p=%.2f, alpha=%.2f)'%(self.p,self.alpha)


    def _fit(self, train_ind, train_labels, all_labels=None):

        u = self.graph.peikonal(train_ind[train_labels], bdy_val=0, f=self.f, p=self.p, max_num_it=self.max_num_it, 
                                                tol=self.tol, num_bisection_it=self.num_bisection_it, prog=False)
        return u

Ancestors

Inherited members

class plaplace (W=None, class_priors=None, p=10, max_num_it=1000000.0, tol=0.1, fast=True)

Graph p-laplace classifier

Semi-supervised learning by via the solution of the game-theoretic p-Laplace equation [1].

Parameters

W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
(n,n) Weight matrix representing the graph.
class_priors : numpy array (optional), default=None
Class priors (fraction of data belonging to each class). If provided, the predict function will attempt to automatic balance the label predictions to predict the correct number of nodes in each class.
p : float (optional), default=10
Value of p in the p-Laplace equation (( 2 \leq p \leq \infty).
max_num_it : int (optional), default=1e5
Maximum number of iterations
tol : float (optional)
Tolerance with which to solve the equation. Default tol=0.1 for fast=False and tol=1e-5 otherwise.
fast : bool (optional), default=True
Whether to use constant w_{ij}=1 weights for the infinity-Laplacian which allows a faster algorithm to be used.

References

[1] M. Flores Rios, J. Calder, and G. Lerman. Algorithms for \ell_p-based semi-supervised learning on graphs. arXiv:1901.05031, 2019.

Expand source code
class plaplace(ssl):
    def __init__(self, W=None, class_priors=None, p=10, max_num_it=1e6, tol=1e-1, fast=True):
        """Graph p-laplace classifier
        ===================

        Semi-supervised learning by via the solution of the game-theoretic p-Laplace equation [1].

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            (n,n) Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        p : float (optional), default=10
            Value of p in the p-Laplace equation (\\( 2 \\leq p \\leq \\infty\\).
        max_num_it : int (optional), default=1e5
            Maximum number of iterations
        tol : float (optional)
            Tolerance with which to solve the equation. Default tol=0.1 for fast=False and tol=1e-5 otherwise.
        fast : bool (optional), default=True
            Whether to use constant \\(w_{ij}=1\\) weights for the infinity-Laplacian
            which allows a faster algorithm to be used.

        References
        ----------
        [1] M. Flores Rios, J. Calder, and G. Lerman. [Algorithms for \\( \\ell_p\\)-based semi-supervised learning on graphs.](https://arxiv.org/abs/1901.05031) arXiv:1901.05031, 2019.
        """
        super().__init__(W, class_priors)

        self.p = p
        self.max_num_it = max_num_it
        self.tol = tol
        self.onevsrest = True
        self.fast = fast

        if fast:
            self.tol = 1e-5
        #Setup accuracy filename and model name
        self.accuracy_filename = '_plaplace_p%.2f'%self.p 
        self.name = 'p-Laplace (p=%.2f)'%self.p


    def _fit(self, train_ind, train_labels, all_labels=None):
        u = self.graph.plaplace(train_ind, train_labels, self.p, max_num_it=self.max_num_it, tol=self.tol,fast=self.fast)
        return u

Ancestors

Inherited members

class poisson (W=None, class_priors=None, solver='conjugate_gradient', p=1, use_cuda=False, min_iter=50, max_iter=1000, tol=0.001, spectral_cutoff=10)

Poisson Learning

Semi-supervised learning via the solution of the Poisson equation L^p u = \sum_{j=1}^m \delta_j(y_j - \overline{y})^T, where L=D-W is the combinatorial graph Laplacian, y_j are the label vectors, \overline{y} = \frac{1}{m}\sum_{i=1}^m y_j is the average label vector, m is the number of training points, and \delta_j are standard basis vectors. See the reference for more details.

Implements 3 different solvers, spectral, gradient_descent, and conjugate_gradient. GPU acceleration is available for gradient descent. See [1] for details.

Parameters

W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
Weight matrix representing the graph.
class_priors : numpy array (optional), default=None
Class priors (fraction of data belonging to each class). If provided, the predict function will attempt to automatic balance the label predictions to predict the correct number of nodes in each class.
solver : {'spectral', 'conjugate_gradient', 'gradient_descent'} (optional), default='conjugate_gradient'
Choice of solver for Poisson learning.
p : int (optional), default=1
Power for Laplacian, can be any positive real number. Solver will default to 'spectral' if p!=1.
use_cuda : bool (optional), default=False
Whether to use GPU acceleration for gradient descent solver.
min_iter : int (optional), default=50
Minimum number of iterations of gradient descent before checking stopping condition.
max_iter : int (optional), default=1000
Maximum number of iterations of gradient descent.
tol : float (optional), default=1e-3
Tolerance for conjugate gradient solver.
spectral_cutoff : int (optional), default=10
Number of eigenvectors to use for spectral solver.

Examples

Poisson learning works on directed (i.e., nonsymmetric) graphs with the gradient descent solver: poisson_directed.py.

import numpy as np
import graphlearning as gl
import matplotlib.pyplot as plt
import sklearn.datasets as datasets

X,labels = datasets.make_moons(n_samples=500,noise=0.1)
W = gl.weightmatrix.knn(X,10,symmetrize=False)

train_ind = gl.trainsets.generate(labels, rate=5)
train_labels = labels[train_ind]

model = gl.ssl.poisson(W, solver='gradient_descent')
pred_labels = model.fit_predict(train_ind, train_labels)

accuracy = gl.ssl.ssl_accuracy(pred_labels, labels, train_ind)   
print("Accuracy: %.2f%%"%accuracy)

plt.scatter(X[:,0],X[:,1], c=pred_labels)
plt.scatter(X[train_ind,0],X[train_ind,1], c='r')
plt.show()

Reference

[1] J. Calder, B. Cook, M. Thorpe, D. Slepcev. Poisson Learning: Graph Based Semi-Supervised Learning at Very Low Label Rates., Proceedings of the 37th International Conference on Machine Learning, PMLR 119:1306-1316, 2020.

Expand source code
class poisson(ssl):
    def __init__(self, W=None, class_priors=None, solver='conjugate_gradient', p=1, use_cuda=False, min_iter=50, max_iter=1000, tol=1e-3, spectral_cutoff=10):
        """Poisson Learning
        ===================

        Semi-supervised learning via the solution of the Poisson equation 
        \\[L^p u = \\sum_{j=1}^m \\delta_j(y_j - \\overline{y})^T,\\]
        where \\(L=D-W\\) is the combinatorial graph Laplacian, 
        \\(y_j\\) are the label vectors, \\(\\overline{y} = \\frac{1}{m}\\sum_{i=1}^m y_j\\) 
        is the average label vector, \\(m\\) is the number of training points, and 
        \\(\\delta_j\\) are standard basis vectors. See the reference for more details.

        Implements 3 different solvers, spectral, gradient_descent, and conjugate_gradient. 
        GPU acceleration is available for gradient descent. See [1] for details.

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        solver : {'spectral', 'conjugate_gradient', 'gradient_descent'} (optional), default='conjugate_gradient'
            Choice of solver for Poisson learning.
        p : int (optional), default=1
            Power for Laplacian, can be any positive real number. Solver will default to 'spectral' if p!=1.
        use_cuda : bool (optional), default=False
            Whether to use GPU acceleration for gradient descent solver.
        min_iter : int (optional), default=50
            Minimum number of iterations of gradient descent before checking stopping condition.
        max_iter : int (optional), default=1000
            Maximum number of iterations of gradient descent.
        tol : float (optional), default=1e-3
            Tolerance for conjugate gradient solver.
        spectral_cutoff : int (optional), default=10
            Number of eigenvectors to use for spectral solver.

        Examples
        --------
        Poisson learning works on directed (i.e., nonsymmetric) graphs with the gradient descent solver: [poisson_directed.py](https://github.com/jwcalder/GraphLearning/blob/master/examples/poisson_directed.py).
        ```py
        import numpy as np
        import graphlearning as gl
        import matplotlib.pyplot as plt
        import sklearn.datasets as datasets

        X,labels = datasets.make_moons(n_samples=500,noise=0.1)
        W = gl.weightmatrix.knn(X,10,symmetrize=False)

        train_ind = gl.trainsets.generate(labels, rate=5)
        train_labels = labels[train_ind]

        model = gl.ssl.poisson(W, solver='gradient_descent')
        pred_labels = model.fit_predict(train_ind, train_labels)

        accuracy = gl.ssl.ssl_accuracy(pred_labels, labels, train_ind)   
        print("Accuracy: %.2f%%"%accuracy)

        plt.scatter(X[:,0],X[:,1], c=pred_labels)
        plt.scatter(X[train_ind,0],X[train_ind,1], c='r')
        plt.show()
        ```

        Reference
        ---------
        [1] J. Calder, B. Cook, M. Thorpe, D. Slepcev. [Poisson Learning: Graph Based Semi-Supervised
        Learning at Very Low Label Rates.](http://proceedings.mlr.press/v119/calder20a.html), 
        Proceedings of the 37th International Conference on Machine Learning, PMLR 119:1306-1316, 2020.
        """
        super().__init__(W, class_priors)
        if solver not in ['conjugate_gradient', 'spectral', 'gradient_descent']:
            sys.exit("Invalid Poisson solver")
        self.solver = solver
        self.p = p
        if p != 1:
            self.solver = 'spectral'
        self.use_cuda = use_cuda
        self.min_iter = min_iter
        self.max_iter = max_iter
        self.tol = tol
        self.spectral_cutoff = spectral_cutoff

        #Setup accuracy filename
        fname = '_poisson' 
        if self.p != 1:
            fname += '_p%.2f'%p
        if self.solver == 'spectral':
            fname += '_N%d'%self.spectral_cutoff
            self.requries_eig = True
        self.accuracy_filename = fname

        #Setup Algorithm name
        self.name = 'Poisson Learning'

    def _fit(self, train_ind, train_labels, all_labels=None):

        n = self.graph.num_nodes
        unique_labels = np.unique(train_labels)
        k = len(unique_labels)
        
        #Zero out diagonal for faster convergence
        W = self.graph.weight_matrix
        W = W - sparse.spdiags(W.diagonal(),0,n,n)
        G = graph.graph(W)

        #Poisson source term
        onehot = utils.labels_to_onehot(train_labels,k)
        source = np.zeros((n, onehot.shape[1]))
        source[train_ind] = onehot - np.mean(onehot, axis=0)
        
        if self.solver == 'conjugate_gradient':  #Conjugate gradient solver

            L = G.laplacian(normalization='normalized')
            D = G.degree_matrix(p=-0.5)
            u = utils.conjgrad(L, D*source, tol=self.tol)
            u = D*u

        elif self.solver == "gradient_descent":

            #Setup matrices
            D = G.degree_matrix(p=-1)
            P = D*W.transpose()
            Db = D*source

            #Invariant distribution
            v = np.zeros(n)
            v[train_ind]=1
            v = v/np.sum(v)
            deg = G.degree_vector()
            vinf = deg/np.sum(deg)
            RW = W.transpose()*D
            u = np.zeros((n,k))

            #Number of iterations
            T = 0
            if self.use_cuda:

                import torch 

                Pt = utils.torch_sparse(P).cuda()
                ut = torch.from_numpy(u).float().cuda()
                Dbt = torch.from_numpy(Db).float().cuda()

                while (T < self.min_iter or np.max(np.absolute(v-vinf)) > 1/n) and (T < self.max_iter):
                    ut = torch.sparse.addmm(Dbt,Pt,ut)
                    v = RW*v
                    T = T + 1

                #Transfer to CPU and convert to numpy
                u = ut.cpu().numpy()

            else: #Use CPU

                while (T < self.min_iter or np.max(np.absolute(v-vinf)) > 1/n) and (T < self.max_iter):
                    u = Db + P*u
                    v = RW*v
                    T = T + 1

                    #Compute accuracy if all labels are provided
                    if all_labels is not None:
                        self.prob = u
                        labels = self.predict()
                        acc = ssl_accuracy(labels,all_labels,train_ind)
                        print('%d,Accuracy = %.2f'%(T,acc))
                
        #Use spectral solver
        elif self.solver == 'spectral':

            vals, vecs = G.eigen_decomp(normalization='randomwalk', k=self.spectral_cutoff+1)
            V = vecs[:,1:]
            vals = vals[1:]
            if self.p != 1:
                vals = vals**self.p
            L = sparse.spdiags(1/vals, 0, self.spectral_cutoff, self.spectral_cutoff)
            u = V@(L@(V.T@source))

        else:
            sys.exit("Invalid Poisson solver " + self.solver)

        return u

Ancestors

Inherited members

class poisson_mbo (W=None, class_priors=None, solver='conjugate_gradient', use_cuda=False, min_iter=50, max_iter=1000, tol=0.001, spectral_cutoff=10, Ns=40, mu=1, T=20)

PoissonMBO

Semi-supervised learning via Poisson MBO method [1]. class_priors must be provided.

Parameters

W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
Weight matrix representing the graph.
class_priors : numpy array (optional), default=None
Class priors (fraction of data belonging to each class).
solver : {'spectral', 'conjugate_gradient', 'gradient_descent'} (optional), default='conjugate_gradient'
Choice of solver for Poisson learning.
use_cuda : bool (optional), default=False
Whether to use GPU acceleration for gradient descent solver.
min_iter : int (optional), default=50
Minimum number of iterations of gradient descent before checking stopping condition.
max_iter : int (optional), default=1000
Maximum number of iterations of gradient descent.
tol : float (optional), default=1e-3
Tolerance for conjugate gradient solver.
spectral_cutoff : int (optional), default=10
Number of eigenvectors to use for spectral solver.
Ns : int (optional), default=40
Number of inner iterations in PoissonMBO.
mu : float (optional), default=1
Fidelity parameter.
T : int (optional), default=20
Number of MBO iterations.

Example

Running PoissonMBO on MNIST at 1 label per class: poisson_mbo.py.

import graphlearning as gl

labels = gl.datasets.load('mnist', labels_only=True)
W = gl.weightmatrix.knn('mnist', 10, metric='vae')

num_train_per_class = 1
train_ind = gl.trainsets.generate(labels, rate=num_train_per_class)
train_labels = labels[train_ind]

class_priors = gl.utils.class_priors(labels)
model = gl.ssl.poisson_mbo(W, class_priors)
pred_labels = model.fit_predict(train_ind,train_labels,all_labels=labels)

accuracy = gl.ssl.ssl_accuracy(labels,pred_labels,train_ind)
print(model.name + ': %.2f%%'%accuracy)

Reference

[1] J. Calder, B. Cook, M. Thorpe, D. Slepcev. Poisson Learning: Graph Based Semi-Supervised Learning at Very Low Label Rates., Proceedings of the 37th International Conference on Machine Learning, PMLR 119:1306-1316, 2020.

Expand source code
class poisson_mbo(ssl):
    def __init__(self, W=None, class_priors=None, solver='conjugate_gradient', use_cuda=False, min_iter=50, max_iter=1000, tol=1e-3, spectral_cutoff=10, Ns=40, mu=1, T=20):
        """PoissonMBO 
        ===================

        Semi-supervised learning via Poisson MBO method [1]. class_priors must be provided.

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). 
        solver : {'spectral', 'conjugate_gradient', 'gradient_descent'} (optional), default='conjugate_gradient'
            Choice of solver for Poisson learning.
        use_cuda : bool (optional), default=False
            Whether to use GPU acceleration for gradient descent solver.
        min_iter : int (optional), default=50
            Minimum number of iterations of gradient descent before checking stopping condition.
        max_iter : int (optional), default=1000
            Maximum number of iterations of gradient descent.
        tol : float (optional), default=1e-3
            Tolerance for conjugate gradient solver.
        spectral_cutoff : int (optional), default=10
            Number of eigenvectors to use for spectral solver.
        Ns : int (optional), default=40
            Number of inner iterations in PoissonMBO.
        mu : float (optional), default=1
            Fidelity parameter.
        T : int (optional), default=20
            Number of MBO iterations.
        
        Example
        -------
        Running PoissonMBO on MNIST at 1 label per class: [poisson_mbo.py](https://github.com/jwcalder/GraphLearning/blob/master/examples/poisson_mbo.py).
        ```py
        import graphlearning as gl

        labels = gl.datasets.load('mnist', labels_only=True)
        W = gl.weightmatrix.knn('mnist', 10, metric='vae')

        num_train_per_class = 1
        train_ind = gl.trainsets.generate(labels, rate=num_train_per_class)
        train_labels = labels[train_ind]

        class_priors = gl.utils.class_priors(labels)
        model = gl.ssl.poisson_mbo(W, class_priors)
        pred_labels = model.fit_predict(train_ind,train_labels,all_labels=labels)

        accuracy = gl.ssl.ssl_accuracy(labels,pred_labels,train_ind)
        print(model.name + ': %.2f%%'%accuracy)
        ```
        Reference
        ---------
        [1] J. Calder, B. Cook, M. Thorpe, D. Slepcev. [Poisson Learning: Graph Based Semi-Supervised
        Learning at Very Low Label Rates.](http://proceedings.mlr.press/v119/calder20a.html), 
        Proceedings of the 37th International Conference on Machine Learning, PMLR 119:1306-1316, 2020.
        """
        super().__init__(W, class_priors)

        self.poisson_model = poisson(W, solver=solver, use_cuda=use_cuda, min_iter=min_iter, 
                                     max_iter=max_iter, tol=tol, spectral_cutoff=spectral_cutoff)

        self.Ns = Ns
        self.mu = mu
        self.T = T
        self.use_cuda = use_cuda

        #Setup accuracy filename
        fname = '_poisson_mbo' 
        if solver == 'spectral':
            fname += '_N%d'%spectral_cutoff
            self.requries_eig = True
        fname += '_Ns_%d_mu_%.2f_T_%d'%(Ns,mu,T)
        self.accuracy_filename = fname

        #Setup Algorithm name
        self.name = 'Poisson MBO' 

    def _fit(self, train_ind, train_labels, all_labels=None):

        #Short forms
        Ns = self.Ns
        mu = self.mu
        T = self.T
        use_cuda = self.use_cuda

        n = self.graph.num_nodes
        unique_labels = np.unique(train_labels)
        k = len(unique_labels)

        #Zero out diagonal for faster convergence
        W = self.graph.weight_matrix
        W = W - sparse.spdiags(W.diagonal(),0,n,n)
        G = graph.graph(W)
        
        #Poisson source term
        onehot = utils.labels_to_onehot(train_labels,k)
        source = np.zeros((n, onehot.shape[1]))
        source[train_ind] = onehot - np.mean(onehot, axis=0)

        #Initialize via Poisson learning
        labels = self.poisson_model.fit_predict(train_ind, train_labels, all_labels=all_labels)
        u = utils.labels_to_onehot(labels,k)

        #Time step for stability
        dt = 1/np.max(G.degree_vector())

        #Precompute some things
        P = sparse.identity(n) - dt*G.laplacian()
        Db = mu*dt*source

        if use_cuda:
            import torch
            Pt = utils.torch_sparse(P).cuda()
            Dbt = torch.from_numpy(Db).float().cuda()

        for i in range(T):

            #Heat equation step
            if use_cuda:

                #Put on GPU and run heat equation
                ut = torch.from_numpy(u).float().cuda()
                for j in range(Ns):
                    ut = torch.sparse.addmm(Dbt,Pt,ut)

                #Put back on CPU
                u = ut.cpu().numpy()
             
            else: #Use CPU 
                for j in range(Ns):
                    u = P*u + Db

            #Projection step
            self.prob = u
            labels = self.volume_label_projection()
            u = utils.labels_to_onehot(labels,k)

            #Compute accuracy if all labels are provided
            if all_labels is not None:
                acc = ssl_accuracy(labels,all_labels,train_ind)
                print('%d, Accuracy = %.2f'%(i,acc))

        return u

Ancestors

Inherited members

class randomwalk (W=None, class_priors=None, alpha=0.95)

Lazy random walk classification

Add description.

The original method was introduced in [1], and can be interpreted as a lazy random walk.

Parameters

W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
Weight matrix representing the graph.
class_priors : numpy array (optional), default=None
Class priors (fraction of data belonging to each class). If provided, the predict function will attempt to automatic balance the label predictions to predict the correct number of nodes in each class.
alpha : float (optional), default=0.95
Parameter in model.

References

[1] D. Zhou and B. Schölkopf. Learning from labeled and unlabeled data using random walks. Joint Pattern Recognition Symposium. Springer, Berlin, Heidelberg, 2004.

Expand source code
class randomwalk(ssl):
    def __init__(self, W=None, class_priors=None, alpha=0.95):
        """Lazy random walk classification
        ===================

        Add description.

        The original method was introduced in [1], and can be interpreted as a lazy random walk. 

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        alpha : float (optional), default=0.95
            Parameter in model.

        References
        ---------
        [1] D. Zhou and B. Schölkopf. [Learning from labeled and unlabeled data using random walks.](https://link.springer.com/chapter/10.1007/978-3-540-28649-3_29) Joint Pattern Recognition Symposium. 
        Springer, Berlin, Heidelberg, 2004.
        """
        super().__init__(W, class_priors)

        self.alpha = alpha

        #Setup accuracy filename and model name
        self.accuracy_filename = '_randomwalk' 
        self.name = 'Lazy Random Walks'


    def _fit(self, train_ind, train_labels, all_labels=None):
        
        alpha = self.alpha

        #Zero diagonals
        n = self.graph.num_nodes
        W = self.graph.weight_matrix
        W = W - sparse.spdiags(W.diagonal(),0,n,n)
        G = graph.graph(W)

        #Construct Laplacian matrix
        L = (1-alpha)*sparse.identity(n) + alpha*G.laplacian(normalization='normalized')

        #Preconditioner
        m = L.shape[0]
        M = L.diagonal()
        M = sparse.spdiags(1/np.sqrt(M+1e-10),0,m,m).tocsr()
       
        #Right hand side
        k = len(np.unique(train_labels))
        onehot = utils.labels_to_onehot(train_labels,k)
        Y = np.zeros((n, onehot.shape[1]))
        Y[train_ind,:] = onehot

        #Conjugate gradient solver
        u = utils.conjgrad(M*L*M, M*Y, tol=1e-6)
        u = M*u

        return u

Ancestors

Inherited members

class sparse_label_propagation (W=None, class_priors=None, T=100)

Sparse Label Propagation

Semi-supervised learning via the sparse label propagation method of [1].

Parameters

W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
Weight matrix representing the graph.
class_priors : numpy array (optional), default=None
Class priors (fraction of data belonging to each class). If provided, the predict function will attempt to automatic balance the label predictions to predict the correct number of nodes in each class.
T : int (optional), default=100
Number of iterations

References

[1] A. Jung, A.O. Hero III, A. Mara, and S. Jahromi. Semi-supervised learning via sparse label propagation. arXiv:1612.01414, 2016.

Expand source code
class sparse_label_propagation(ssl):
    def __init__(self, W=None, class_priors=None, T=100):
        """Sparse Label Propagation
        ===================

        Semi-supervised learning via the sparse label propagation method of [1].

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array (optional), default=None
            Class priors (fraction of data belonging to each class). If provided, the predict function
            will attempt to automatic balance the label predictions to predict the correct number of
            nodes in each class.
        T : int (optional), default=100
            Number of iterations

        References
        ---------
        [1] A. Jung, A.O. Hero III, A. Mara, and S. Jahromi. [Semi-supervised learning via sparse label propagation.](https://arxiv.org/abs/1612.01414) arXiv:1612.01414, 2016.
        """
        super().__init__(W, class_priors)

        self.T = T
        self.accuracy_filename = '_sparse_label_propagation'
        self.name = 'Sparse LP'
        
    def _fit(self, train_ind, train_labels, all_labels=None):

        n = self.graph.num_nodes
        k = len(np.unique(train_labels))

        #Sparse matrix with ones in all entries
        B = self.graph.adjacency()

        #Construct matrix 1/2W and 1/deg
        lam = 2*self.graph.weight_matrix - (1-1e-10)*B
        lam = -lam.log1p()
        lam = lam.expm1() + B
        Id = sparse.identity(n) 
        gamma = self.graph.degree_matrix(p=-1)

        #Random initial labeling
        u = np.zeros((k,n))

        #Indicator of train_ind
        one_hot_labels = utils.labels_to_onehot(train_labels,k).T

        #Initialization
        Y = list()
        for j in range(k):
            Gu = self.graph.gradient(u[j,:], weighted=True)
            Y.append(Gu)

        #Main loop for sparse label propagation
        for i in range(self.T):

            u_prev = np.copy(u)
            #Compute div
            for j in range(k):
                div = 2*self.graph.divergence(Y[j])
                u[j,:] = u_prev[j,:] - gamma*div
                u[j,train_ind] = one_hot_labels[j,:]  #Set labels
                u_tilde = 2*u[j,:] - u_prev[j,:]

                Gu = -self.graph.gradient(u_tilde, weighted=True)
                Y[j] = Y[j] + Gu.multiply(lam)
                ind1 = B.multiply(abs(Y[j])>1)
                ind2 = B - ind1
                Y[j] = ind1.multiply(Y[j].sign()) + ind2.multiply(Y[j])

            #Compute accuracy if all labels are provided
            if all_labels is not None:
                self.prob = u.T
                labels = self.predict()
                acc = ssl_accuracy(labels,all_labels,train_ind)
                print('%d,Accuracy = %.2f'%(i,acc))
 
        return u.T

Ancestors

Inherited members

class ssl (W, class_priors)
Expand source code
class ssl:
    __metaclass__ = ABCMeta

    def __init__(self, W, class_priors):
        if W is None:
            self.graph = None
        else:
            self.set_graph(W)
        self.prob = None
        self.fitted = False
        self.name = ''
        self.accuracy_filename = ''
        self.requires_eig = False
        self.onevsrest = False
        self.similarity = True
        self.class_priors = class_priors
        if self.class_priors is not None:
            self.class_priors = self.class_priors / np.sum(self.class_priors)
        self.weights = 1
        self.class_priors_error = 1

    def set_graph(self, W):
        """Set Graph
        ===================

        Sets the graph object for semi-supervised learning.

        Implements 3 different solvers, spectral, gradient_descent, and conjugate_gradient. 
        GPU acceleration is available for gradient descent. See [1] for details.

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        """

        if type(W) == graph.graph:
            self.graph = W
        else:
            self.graph = graph.graph(W)

    def volume_label_projection(self):
        """Volume label projection
        ======
    
        Projects class probabilities to labels while enforcing a constraint on 
        class priors (i.e., class volumes). Does not return anything, just modifies `self.weights`.
        
        Returns
        -------
        labels : numpy array (int)
            Predicted labels after volume correction.
        """

        n = self.graph.num_nodes
        k = self.prob.shape[1]
        if type(self.weights) == int:
            self.weights = np.ones((k,))

        #Time step
        dt = 0.1
        if self.similarity:
            dt *= -1

        #np.set_printoptions(formatter={'float': lambda x: "{0:0.3f}".format(x)})
        i = 0
        err = 1
        while i < 1e4 and err > 1e-3:
            i += 1
            class_size = np.mean(utils.labels_to_onehot(self.predict(),k),axis=0)
            #print(class_size-self.class_priors)
            grad = class_size - self.class_priors
            err = np.max(np.absolute(grad))
            self.weights += dt*grad
            self.weights = self.weights/self.weights[0]

        self.class_priors_error = err

        return self.predict()

    def get_accuracy_filename(self):
        """Get accuracy filename
        ========

        Returns name of the file that will store the accuracy results for `ssl_trials.py`.

        Returns
        -------
        fname : str
            Accuracy filename.
        """

        fname = self.accuracy_filename
        if self.class_priors is not None:
            fname += '_classpriors'
        fname += '_accuracy.csv'
        return fname


    def predict(self, ignore_class_priors=False):
        """Predict
        ========

        Makes label predictions based on the probabilities computed by `fit()`.
        Will use a volume label projection if `class_priors` were given, to ensure 
        the number of nodes predicted in each class is correct.
        
        Parameters
        ----------
        ignore_class_priors : bool (optional), default=False
            Used to disable the volume constrained label decision, when `class_priors` has been provided. 

        Returns
        -------
        pred_labels : (int) numpy array
            Predicted labels as integers for all datapoints in the graph.
        """

        if self.fitted == False:
            sys.exit('Model has not been fitted yet.')

        if ignore_class_priors:
            w = 1
        else:
            w = self.weights

        scores = self.prob - np.min(self.prob)
        scores = scores/np.max(scores)

        #Check if scores are similarity or distance
        if self.similarity:
            pred_labels = np.argmax(scores*w,axis=1)
        else: #Then distances
            pred_labels = np.argmin(scores*w,axis=1)

        return pred_labels 

    def fit_predict(self, train_ind, train_labels, all_labels=None):
        """Fit and predict
        ======

        Calls fit() and predict() sequentially.

        Parameters
        ----------
        train_ind : numpy array, int
            Indicies of training points.
        train_labels : numpy array, int
            Training labels as integers \\(0,1,\\dots,k-1\\) for \\(k\\) classes.
        all_labels : numpy array, int (optional)
            True labels for all datapoints.

        Returns
        -------
        pred_labels : (int) numpy array
            Predicted labels as integers for all datapoints in the graph.
        """

        self.fit(train_ind, train_labels, all_labels=all_labels)
        return self.predict()

    def ssl_trials(self, trainsets, labels, num_cores=1, tag='', save_results=True, overwrite=False, num_trials=-1):
        """Semi-supervised learning trials
        ===================

        Runs a semi-supervised learning algorithm on a list of training sets,
        recording the label rates and saves the results to a csv file in the
        local folder results/. The filename is controlled by the member function
        model.get_accuracy_filename(). The trial will abort if the accuracy result
        file already exists, unless `overwrite=True`.

        Parameters
        ----------
        trainsets : list of numpy arrays
            Collection of training sets to run semi-supervised learning on. This is the output
            of `graphlearning.trainsets.generate` or `graphlearning.trainsets.load`.
        labels : numpy array (int)
            Integer array of labels for entire dataset.
        num_cores : int
            Number of cores to use for parallel processing over trials
        tag : str (optional), default=''
            An extra identifying tag to add to the accuracy filename.
        save_results : bool (optional), default=True
            Whether to save results to csv file or just print to the screen.
        overwrite : bool (optional), default = False
            Whether to overwrite existing results file, if found. If `overwrite=False`, 
            `save_results=True`, and the results file is found, the trial is aborted.
        num_trials: int (optional), defualt = -1
            Number of trials. Any negative number runs all trials.
        """

        if num_trials > 0:
            trainsets = trainsets[:num_trials]

        #Print name
        print('\nModel: '+self.name)

        if save_results:
            if not os.path.exists(results_dir):
                os.makedirs(results_dir)

            outfile = os.path.join(results_dir, tag+self.get_accuracy_filename())
            if (not overwrite) and os.path.exists(outfile):
                print('Aborting: SSL trial ('+self.get_accuracy_filename()+') already completed , and overwrite is False.')
                return
            f = open(outfile,"w")
            #now = datetime.datetime.now()
            #f.write("Date/Time, "+now.strftime("%Y-%m-%d_%H:%M")+"\n")
            if self.class_priors is None:
                f.write('Number of labels,Accuracy\n')
            else:
                f.write('Number of labels,Accuracy,Accuracy with class priors,Class priors error\n')
            f.close()

        if save_results:
            print('Results File: '+outfile)

        if self.class_priors is None:
            print('\nNumber of labels,Accuracy')
        else:
            print('\nNumber of labels,Accuracy,Accuracy with class priors,Class priors error')

        #Trial run to trigger eigensolvers, when needed
        if self.requires_eig:
            pred_labels = self.fit_predict(trainsets[0], labels[trainsets[0]])
     
        def one_trial(train_ind):

            #Number of labels
            num_train = len(train_ind)
            train_labels = labels[train_ind]

            #Run semi-supervised learning
            pred_labels = self.fit_predict(train_ind, train_labels)

            #Compute accuracy
            accuracy = ssl_accuracy(pred_labels,labels,train_ind)

            #If class priors were provided, check accuracy without priors
            if self.class_priors is not None:
                pred_labels = self.predict(ignore_class_priors=True)
                accuracy_without_priors = ssl_accuracy(pred_labels,labels,train_ind)
            
            #Print to terminal
            if self.class_priors is None:
                print("%d" % num_train + ",%.2f" % accuracy)
            else:
                print("%d,%.2f,%.2f,%.5f" % (num_train,accuracy_without_priors,accuracy,self.class_priors_error))

            #Write to file
            if save_results:
                f = open(outfile,"a+")
                if self.class_priors is None:
                    f.write("%d" % num_train + ",%.2f\n" % accuracy)
                else:
                    f.write("%d,%.2f,%.2f,%.5f\n" % (num_train,accuracy_without_priors,accuracy,
                                                                            self.class_priors_error))
                f.close()

        num_cores = min(multiprocessing.cpu_count(),num_cores)

        if num_cores == 1:
            for train_ind in trainsets:
                one_trial(train_ind)
        else:
            Parallel(n_jobs=num_cores)(delayed(one_trial)(train_ind) for train_ind in trainsets)

    def trials_statistics(self, tag=''):
        """Trials statistics
        ===================

        Loads accuracy scores from each trial from csv files created by `ssl_trials`
        and returns summary statistics (mean and standard deviation of accuracy).

        Parameters
        ----------
        tag : str (optional), default=''
            An extra identifying tag to add to the accuracy filename.

        Returns
        -------
        num_train : numpy array
            Number of training examples in each label rate experiment.
        acc_mean : numpy array
            Mean accuracy over all trials in each experiment.
        acc_stddev : numpy array
            Standard deviation of accuracy over all trials in each experiment.
        num_trials : int
            Number of trials for each label rate.
        """

        accuracy_filename = os.path.join(results_dir, tag+self.get_accuracy_filename())
        X = utils.csvread(accuracy_filename)
        num_train = np.unique(X[:,0])

        acc_mean = []
        acc_stddev = []
        for n in num_train:
            Y = X[X[:,0]==n,1:]
            acc_mean += [np.mean(Y,axis=0)]
            acc_stddev += [np.std(Y,axis=0)]

        num_trials = int(len(X[:,0])/len(num_train))
        acc_mean = np.array(acc_mean)
        acc_stddev = np.array(acc_stddev)
        return num_train, acc_mean, acc_stddev, num_trials

   
    def fit(self, train_ind, train_labels, all_labels=None):
        """Fit
        ======

        Solves graph-based learning problem, computing the probability 
        that each node belongs to each class. If `all_labels` is provided, 
        then the solver operates in verbose mode, printing out accuracy
        at each iteration.

        Parameters
        ----------
        train_ind : numpy array, int
            Indicies of training points.
        train_labels : numpy array, int
            Training labels as integers \\(0,1,\\dots,k-1\\) for \\(k\\) classes.
        all_labels : numpy array, int (optional)
            True labels for all datapoints.

        Returns
        -------
        u : (n,k) numpy array, float
            Per-class scores computed by graph-based learning for each node and each class. For some methods these are probabilities (i.e., for Laplace learning), while for others they can take on negative values (e.g., Poisson learning). The class label prediction is either the argmax or argmin over the rows of u (most methods are argmax, except distance-function based methods like nearest neighbor and peikonal).
        """

        if self.graph is None:
            sys.exit('SSL object has no graph. Use graph.set_graph() to provide a graph for SSL.')

        self.fitted = True

        #If a one-vs-rest classifier
        if self.onevsrest:
            unique_labels = np.unique(train_labels)
            num_labels = len(unique_labels)
            self.prob = np.zeros((self.graph.num_nodes,num_labels))
            for i,l in enumerate(unique_labels):
                self.prob[:,i] = self._fit(train_ind, train_labels==l)
        else:
            self.prob = self._fit(train_ind, train_labels, all_labels=all_labels)

        if self.class_priors is not None:
            self.volume_label_projection()

        return self.prob

    @abstractmethod
    def _fit(self, train_ind, train_labels, all_labels=None):
        """Internal Fit Function
        ======

        Internal fit function that any ssl object must override. If `self.onevsrest=True` then
        `train_labels` are binary in the one-vs-rest framework, and `_fit` must return
        a scalar numpy array. Otherwise the labels are integer and `_fit` must return an (n,k)
        numpy array of probabilities, where `k` is the number of classes.

        Parameters
        ----------
        train_ind : numpy array, int
            Indicies of training points.
        train_labels : numpy array, int
            Training labels as integers \\(0,1,\\dots,k-1\\) for \\(k\\) classes, or binary
            if `self.onevsrest=True`.
        all_labels : numpy array, int (optional)
            True labels for all datapoints.

        Returns
        -------
        u : numpy array, float
            (n,k) array of probabilities computed by graph-based learning for each node and each class, unless
            `self.onevsrest=True`, in which case it is a length n numpy array of probablities for the current 
            class.
        """
        raise NotImplementedError("Must override _fit")

Subclasses

Methods

def fit(self, train_ind, train_labels, all_labels=None)

Fit

Solves graph-based learning problem, computing the probability that each node belongs to each class. If all_labels is provided, then the solver operates in verbose mode, printing out accuracy at each iteration.

Parameters

train_ind : numpy array, int
Indicies of training points.
train_labels : numpy array, int
Training labels as integers 0,1,\dots,k-1 for k classes.
all_labels : numpy array, int (optional)
True labels for all datapoints.

Returns

u : (n,k) numpy array, float
Per-class scores computed by graph-based learning for each node and each class. For some methods these are probabilities (i.e., for Laplace learning), while for others they can take on negative values (e.g., Poisson learning). The class label prediction is either the argmax or argmin over the rows of u (most methods are argmax, except distance-function based methods like nearest neighbor and peikonal).
Expand source code
def fit(self, train_ind, train_labels, all_labels=None):
    """Fit
    ======

    Solves graph-based learning problem, computing the probability 
    that each node belongs to each class. If `all_labels` is provided, 
    then the solver operates in verbose mode, printing out accuracy
    at each iteration.

    Parameters
    ----------
    train_ind : numpy array, int
        Indicies of training points.
    train_labels : numpy array, int
        Training labels as integers \\(0,1,\\dots,k-1\\) for \\(k\\) classes.
    all_labels : numpy array, int (optional)
        True labels for all datapoints.

    Returns
    -------
    u : (n,k) numpy array, float
        Per-class scores computed by graph-based learning for each node and each class. For some methods these are probabilities (i.e., for Laplace learning), while for others they can take on negative values (e.g., Poisson learning). The class label prediction is either the argmax or argmin over the rows of u (most methods are argmax, except distance-function based methods like nearest neighbor and peikonal).
    """

    if self.graph is None:
        sys.exit('SSL object has no graph. Use graph.set_graph() to provide a graph for SSL.')

    self.fitted = True

    #If a one-vs-rest classifier
    if self.onevsrest:
        unique_labels = np.unique(train_labels)
        num_labels = len(unique_labels)
        self.prob = np.zeros((self.graph.num_nodes,num_labels))
        for i,l in enumerate(unique_labels):
            self.prob[:,i] = self._fit(train_ind, train_labels==l)
    else:
        self.prob = self._fit(train_ind, train_labels, all_labels=all_labels)

    if self.class_priors is not None:
        self.volume_label_projection()

    return self.prob
def fit_predict(self, train_ind, train_labels, all_labels=None)

Fit and predict

Calls fit() and predict() sequentially.

Parameters

train_ind : numpy array, int
Indicies of training points.
train_labels : numpy array, int
Training labels as integers 0,1,\dots,k-1 for k classes.
all_labels : numpy array, int (optional)
True labels for all datapoints.

Returns

pred_labels : (int) numpy array
Predicted labels as integers for all datapoints in the graph.
Expand source code
def fit_predict(self, train_ind, train_labels, all_labels=None):
    """Fit and predict
    ======

    Calls fit() and predict() sequentially.

    Parameters
    ----------
    train_ind : numpy array, int
        Indicies of training points.
    train_labels : numpy array, int
        Training labels as integers \\(0,1,\\dots,k-1\\) for \\(k\\) classes.
    all_labels : numpy array, int (optional)
        True labels for all datapoints.

    Returns
    -------
    pred_labels : (int) numpy array
        Predicted labels as integers for all datapoints in the graph.
    """

    self.fit(train_ind, train_labels, all_labels=all_labels)
    return self.predict()
def get_accuracy_filename(self)

Get accuracy filename

Returns name of the file that will store the accuracy results for ssl_trials.py.

Returns

fname : str
Accuracy filename.
Expand source code
def get_accuracy_filename(self):
    """Get accuracy filename
    ========

    Returns name of the file that will store the accuracy results for `ssl_trials.py`.

    Returns
    -------
    fname : str
        Accuracy filename.
    """

    fname = self.accuracy_filename
    if self.class_priors is not None:
        fname += '_classpriors'
    fname += '_accuracy.csv'
    return fname
def predict(self, ignore_class_priors=False)

Predict

Makes label predictions based on the probabilities computed by fit(). Will use a volume label projection if class_priors were given, to ensure the number of nodes predicted in each class is correct.

Parameters

ignore_class_priors : bool (optional), default=False
Used to disable the volume constrained label decision, when class_priors has been provided.

Returns

pred_labels : (int) numpy array
Predicted labels as integers for all datapoints in the graph.
Expand source code
def predict(self, ignore_class_priors=False):
    """Predict
    ========

    Makes label predictions based on the probabilities computed by `fit()`.
    Will use a volume label projection if `class_priors` were given, to ensure 
    the number of nodes predicted in each class is correct.
    
    Parameters
    ----------
    ignore_class_priors : bool (optional), default=False
        Used to disable the volume constrained label decision, when `class_priors` has been provided. 

    Returns
    -------
    pred_labels : (int) numpy array
        Predicted labels as integers for all datapoints in the graph.
    """

    if self.fitted == False:
        sys.exit('Model has not been fitted yet.')

    if ignore_class_priors:
        w = 1
    else:
        w = self.weights

    scores = self.prob - np.min(self.prob)
    scores = scores/np.max(scores)

    #Check if scores are similarity or distance
    if self.similarity:
        pred_labels = np.argmax(scores*w,axis=1)
    else: #Then distances
        pred_labels = np.argmin(scores*w,axis=1)

    return pred_labels 
def set_graph(self, W)

Set Graph

Sets the graph object for semi-supervised learning.

Implements 3 different solvers, spectral, gradient_descent, and conjugate_gradient. GPU acceleration is available for gradient descent. See [1] for details.

Parameters

W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
Weight matrix representing the graph.
Expand source code
def set_graph(self, W):
    """Set Graph
    ===================

    Sets the graph object for semi-supervised learning.

    Implements 3 different solvers, spectral, gradient_descent, and conjugate_gradient. 
    GPU acceleration is available for gradient descent. See [1] for details.

    Parameters
    ----------
    W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
        Weight matrix representing the graph.
    """

    if type(W) == graph.graph:
        self.graph = W
    else:
        self.graph = graph.graph(W)
def ssl_trials(self, trainsets, labels, num_cores=1, tag='', save_results=True, overwrite=False, num_trials=-1)

Semi-supervised learning trials

Runs a semi-supervised learning algorithm on a list of training sets, recording the label rates and saves the results to a csv file in the local folder results/. The filename is controlled by the member function model.get_accuracy_filename(). The trial will abort if the accuracy result file already exists, unless overwrite=True.

Parameters

trainsets : list of numpy arrays
Collection of training sets to run semi-supervised learning on. This is the output of generate() or load().
labels : numpy array (int)
Integer array of labels for entire dataset.
num_cores : int
Number of cores to use for parallel processing over trials
tag : str (optional), default=''
An extra identifying tag to add to the accuracy filename.
save_results : bool (optional), default=True
Whether to save results to csv file or just print to the screen.
overwrite : bool (optional), default = False
Whether to overwrite existing results file, if found. If overwrite=False, save_results=True, and the results file is found, the trial is aborted.
num_trials : int (optional), defualt = -1
Number of trials. Any negative number runs all trials.
Expand source code
def ssl_trials(self, trainsets, labels, num_cores=1, tag='', save_results=True, overwrite=False, num_trials=-1):
    """Semi-supervised learning trials
    ===================

    Runs a semi-supervised learning algorithm on a list of training sets,
    recording the label rates and saves the results to a csv file in the
    local folder results/. The filename is controlled by the member function
    model.get_accuracy_filename(). The trial will abort if the accuracy result
    file already exists, unless `overwrite=True`.

    Parameters
    ----------
    trainsets : list of numpy arrays
        Collection of training sets to run semi-supervised learning on. This is the output
        of `graphlearning.trainsets.generate` or `graphlearning.trainsets.load`.
    labels : numpy array (int)
        Integer array of labels for entire dataset.
    num_cores : int
        Number of cores to use for parallel processing over trials
    tag : str (optional), default=''
        An extra identifying tag to add to the accuracy filename.
    save_results : bool (optional), default=True
        Whether to save results to csv file or just print to the screen.
    overwrite : bool (optional), default = False
        Whether to overwrite existing results file, if found. If `overwrite=False`, 
        `save_results=True`, and the results file is found, the trial is aborted.
    num_trials: int (optional), defualt = -1
        Number of trials. Any negative number runs all trials.
    """

    if num_trials > 0:
        trainsets = trainsets[:num_trials]

    #Print name
    print('\nModel: '+self.name)

    if save_results:
        if not os.path.exists(results_dir):
            os.makedirs(results_dir)

        outfile = os.path.join(results_dir, tag+self.get_accuracy_filename())
        if (not overwrite) and os.path.exists(outfile):
            print('Aborting: SSL trial ('+self.get_accuracy_filename()+') already completed , and overwrite is False.')
            return
        f = open(outfile,"w")
        #now = datetime.datetime.now()
        #f.write("Date/Time, "+now.strftime("%Y-%m-%d_%H:%M")+"\n")
        if self.class_priors is None:
            f.write('Number of labels,Accuracy\n')
        else:
            f.write('Number of labels,Accuracy,Accuracy with class priors,Class priors error\n')
        f.close()

    if save_results:
        print('Results File: '+outfile)

    if self.class_priors is None:
        print('\nNumber of labels,Accuracy')
    else:
        print('\nNumber of labels,Accuracy,Accuracy with class priors,Class priors error')

    #Trial run to trigger eigensolvers, when needed
    if self.requires_eig:
        pred_labels = self.fit_predict(trainsets[0], labels[trainsets[0]])
 
    def one_trial(train_ind):

        #Number of labels
        num_train = len(train_ind)
        train_labels = labels[train_ind]

        #Run semi-supervised learning
        pred_labels = self.fit_predict(train_ind, train_labels)

        #Compute accuracy
        accuracy = ssl_accuracy(pred_labels,labels,train_ind)

        #If class priors were provided, check accuracy without priors
        if self.class_priors is not None:
            pred_labels = self.predict(ignore_class_priors=True)
            accuracy_without_priors = ssl_accuracy(pred_labels,labels,train_ind)
        
        #Print to terminal
        if self.class_priors is None:
            print("%d" % num_train + ",%.2f" % accuracy)
        else:
            print("%d,%.2f,%.2f,%.5f" % (num_train,accuracy_without_priors,accuracy,self.class_priors_error))

        #Write to file
        if save_results:
            f = open(outfile,"a+")
            if self.class_priors is None:
                f.write("%d" % num_train + ",%.2f\n" % accuracy)
            else:
                f.write("%d,%.2f,%.2f,%.5f\n" % (num_train,accuracy_without_priors,accuracy,
                                                                        self.class_priors_error))
            f.close()

    num_cores = min(multiprocessing.cpu_count(),num_cores)

    if num_cores == 1:
        for train_ind in trainsets:
            one_trial(train_ind)
    else:
        Parallel(n_jobs=num_cores)(delayed(one_trial)(train_ind) for train_ind in trainsets)
def trials_statistics(self, tag='')

Trials statistics

Loads accuracy scores from each trial from csv files created by ssl_trials and returns summary statistics (mean and standard deviation of accuracy).

Parameters

tag : str (optional), default=''
An extra identifying tag to add to the accuracy filename.

Returns

num_train : numpy array
Number of training examples in each label rate experiment.
acc_mean : numpy array
Mean accuracy over all trials in each experiment.
acc_stddev : numpy array
Standard deviation of accuracy over all trials in each experiment.
num_trials : int
Number of trials for each label rate.
Expand source code
def trials_statistics(self, tag=''):
    """Trials statistics
    ===================

    Loads accuracy scores from each trial from csv files created by `ssl_trials`
    and returns summary statistics (mean and standard deviation of accuracy).

    Parameters
    ----------
    tag : str (optional), default=''
        An extra identifying tag to add to the accuracy filename.

    Returns
    -------
    num_train : numpy array
        Number of training examples in each label rate experiment.
    acc_mean : numpy array
        Mean accuracy over all trials in each experiment.
    acc_stddev : numpy array
        Standard deviation of accuracy over all trials in each experiment.
    num_trials : int
        Number of trials for each label rate.
    """

    accuracy_filename = os.path.join(results_dir, tag+self.get_accuracy_filename())
    X = utils.csvread(accuracy_filename)
    num_train = np.unique(X[:,0])

    acc_mean = []
    acc_stddev = []
    for n in num_train:
        Y = X[X[:,0]==n,1:]
        acc_mean += [np.mean(Y,axis=0)]
        acc_stddev += [np.std(Y,axis=0)]

    num_trials = int(len(X[:,0])/len(num_train))
    acc_mean = np.array(acc_mean)
    acc_stddev = np.array(acc_stddev)
    return num_train, acc_mean, acc_stddev, num_trials
def volume_label_projection(self)

Volume label projection

Projects class probabilities to labels while enforcing a constraint on class priors (i.e., class volumes). Does not return anything, just modifies self.weights.

Returns

labels : numpy array (int)
Predicted labels after volume correction.
Expand source code
def volume_label_projection(self):
    """Volume label projection
    ======

    Projects class probabilities to labels while enforcing a constraint on 
    class priors (i.e., class volumes). Does not return anything, just modifies `self.weights`.
    
    Returns
    -------
    labels : numpy array (int)
        Predicted labels after volume correction.
    """

    n = self.graph.num_nodes
    k = self.prob.shape[1]
    if type(self.weights) == int:
        self.weights = np.ones((k,))

    #Time step
    dt = 0.1
    if self.similarity:
        dt *= -1

    #np.set_printoptions(formatter={'float': lambda x: "{0:0.3f}".format(x)})
    i = 0
    err = 1
    while i < 1e4 and err > 1e-3:
        i += 1
        class_size = np.mean(utils.labels_to_onehot(self.predict(),k),axis=0)
        #print(class_size-self.class_priors)
        grad = class_size - self.class_priors
        err = np.max(np.absolute(grad))
        self.weights += dt*grad
        self.weights = self.weights/self.weights[0]

    self.class_priors_error = err

    return self.predict()
class volume_mbo (W=None, class_priors=None, temperature=0.1, volume_constraint=0.5)

Volume MBO

Semi-supervised learning with the VolumeMBO method [1]. class_priors must be provided.

Parameters

W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
Weight matrix representing the graph.
class_priors : numpy array
Class priors (fraction of data belonging to each class).
temperature : float (optional), default=0.1
Temperature for volume constrained MBO.
volume_constraint : float (optional), default=0.5
The number of points in each class is constrained to be a mulitple \lambda of the true class size, where \text{volume_constraint} \leq \lambda \leq 2-\text{volume_constraint}. Setting volume_constraint=1 yields the tightest constraint.

References

[1] M. Jacobs, E. Merkurjev, and S. Esedoḡlu. Auction dynamics: A volume constrained MBO scheme. Journal of Computational Physics 354:288-310, 2018.

Expand source code
class volume_mbo(ssl):
    def __init__(self, W=None, class_priors=None, temperature=0.1, volume_constraint=0.5):
        """Volume MBO
        ===================

        Semi-supervised learning with the VolumeMBO method [1]. class_priors must be provided.

        Parameters
        ----------
        W : numpy array, scipy sparse matrix, or graphlearning graph object (optional), default=None
            Weight matrix representing the graph.
        class_priors : numpy array
            Class priors (fraction of data belonging to each class). 
        temperature : float (optional), default=0.1
            Temperature for volume constrained MBO.
        volume_constraint : float (optional), default=0.5
            The number of points in each class is constrained to be a mulitple \\(\\lambda\\) of the true
            class size, where 
            \\[ \\text{volume_constraint} \\leq \\lambda \\leq 2-\\text{volume_constraint}.\\]
            Setting `volume_constraint=1` yields the tightest constraint.

        References
        ----------
        [1] M. Jacobs, E. Merkurjev, and S. Esedoḡlu. [Auction dynamics: A volume constrained MBO scheme.](https://www.sciencedirect.com/science/article/pii/S0021999117308033?casa_token=kNahPd2vu50AAAAA:uJQYQVnmMBV_oL0CG1UcOIulY4vhclMGTztm-jjAzy9Lns7rtoOnKs4iyvLOjKXaHU-D6qrQJT4) Journal of Computational Physics 354:288-310, 2018. 
        """
        super().__init__(W, None)

        if class_priors is None:
            sys.exit("Class priors must be provided for Volume MBO.")
        self.class_counts = (self.graph.num_nodes*class_priors).astype(int)
        self.temperature = temperature
        self.volume_constraint = volume_constraint

        #Setup accuracy filename and model name
        self.accuracy_filename = '_volume_mbo_temp_%.2f_vol_%.2f'%(temperature,volume_constraint)
        self.name = 'Volume MBO (T=%.2f, V=%.2f)'%(temperature,volume_constraint)


    def _fit(self, train_ind, train_labels, all_labels=None):

        #Import c extensions
        from . import cextensions

        n = self.graph.num_nodes

        #Set diagonal entries to zero
        W = self.graph.weight_matrix
        W = W - sparse.spdiags(W.diagonal(),0,n,n)
        G = graph.graph(W)

        #Set up graph for C-code
        k = len(np.unique(train_labels))
        u = np.zeros((n,))
        WI,WJ,WV = sparse.find(W)

        #Type casting and memory blocking
        u = np.ascontiguousarray(u,dtype=np.int32)
        WI = np.ascontiguousarray(WI,dtype=np.int32)
        WJ = np.ascontiguousarray(WJ,dtype=np.int32)
        WV = np.ascontiguousarray(WV,dtype=np.float32)
        train_ind = np.ascontiguousarray(train_ind,dtype=np.int32)
        train_labels = np.ascontiguousarray(train_labels,dtype=np.int32)
        ClassCounts = np.ascontiguousarray(self.class_counts,dtype=np.int32)

        cextensions.volume_mbo(u,WJ,WI,WV,train_ind,train_labels,ClassCounts,k,1.0,self.temperature,self.volume_constraint)

        #Set given labels and convert to vector format
        u[train_ind] = train_labels
        u = utils.labels_to_onehot(u,k)
        return u

Ancestors

Inherited members