204 lines
6.2 KiB
Python
204 lines
6.2 KiB
Python
|
# Copyright (C) 2017-2019 by
|
||
|
# Luca Baldesi
|
||
|
# BSD license.
|
||
|
#
|
||
|
# Author: Luca Baldesi (luca.baldesi@unitn.it)
|
||
|
"""Generates graphs with a given eigenvector structure"""
|
||
|
|
||
|
|
||
|
import networkx as nx
|
||
|
from networkx.utils import np_random_state
|
||
|
|
||
|
__all__ = ['spectral_graph_forge']
|
||
|
|
||
|
|
||
|
def _truncate(x):
|
||
|
""" Returns the truncated value of x in the interval [0,1]
|
||
|
"""
|
||
|
|
||
|
if x < 0:
|
||
|
return 0
|
||
|
if x > 1:
|
||
|
return 1
|
||
|
return x
|
||
|
|
||
|
|
||
|
def _mat_spect_approx(A, level, sorteigs=True, reverse=False, absolute=True):
|
||
|
""" Returns the low-rank approximation of the given matrix A
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
A : numpy matrix
|
||
|
level : integer
|
||
|
It represents the fixed rank for the output approximation matrix
|
||
|
sorteigs : boolean
|
||
|
Whether eigenvectors should be sorted according to their associated
|
||
|
eigenvalues before removing the firsts of them
|
||
|
reverse : boolean
|
||
|
Whether eigenvectors list should be reversed before removing the firsts
|
||
|
of them
|
||
|
absolute : boolean
|
||
|
Whether eigenvectors should be sorted considering the absolute values
|
||
|
of the corresponding eigenvalues
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
B : numpy matrix
|
||
|
low-rank approximation of A
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
Low-rank matrix approximation is about finding a fixed rank matrix close
|
||
|
enough to the input one with respect to a given norm (distance).
|
||
|
In the case of real symmetric input matrix and euclidean distance, the best
|
||
|
low-rank approximation is given by the sum of first eigenvector matrices.
|
||
|
|
||
|
References
|
||
|
----------
|
||
|
.. [1] G. Eckart and G. Young, The approximation of one matrix by another
|
||
|
of lower rank
|
||
|
.. [2] L. Mirsky, Symmetric gauge functions and unitarily invariant norms
|
||
|
|
||
|
"""
|
||
|
|
||
|
import numpy as np
|
||
|
|
||
|
d, V = np.linalg.eigh(A)
|
||
|
d = np.ravel(d)
|
||
|
n = len(d)
|
||
|
if sorteigs:
|
||
|
if absolute:
|
||
|
k = np.argsort(np.abs(d))
|
||
|
else:
|
||
|
k = np.argsort(d)
|
||
|
# ordered from the lowest to the highest
|
||
|
else:
|
||
|
k = range(n)
|
||
|
if not reverse:
|
||
|
k = np.flipud(k)
|
||
|
|
||
|
z = np.zeros((n, 1))
|
||
|
for i in range(level, n):
|
||
|
V[:, k[i]] = z
|
||
|
|
||
|
B = V*np.diag(d)*np.transpose(V)
|
||
|
return B
|
||
|
|
||
|
|
||
|
@np_random_state(3)
|
||
|
def spectral_graph_forge(G, alpha, transformation='identity', seed=None):
|
||
|
"""Returns a random simple graph with spectrum resembling that of `G`
|
||
|
|
||
|
This algorithm, called Spectral Graph Forge (SGF), computes the
|
||
|
eigenvectors of a given graph adjacency matrix, filters them and
|
||
|
builds a random graph with a similar eigenstructure.
|
||
|
SGF has been proved to be particularly useful for synthesizing
|
||
|
realistic social networks and it can also be used to anonymize
|
||
|
graph sensitive data.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
G : Graph
|
||
|
alpha : float
|
||
|
Ratio representing the percentage of eigenvectors of G to consider,
|
||
|
values in [0,1].
|
||
|
transformation : string, optional
|
||
|
Represents the intended matrix linear transformation, possible values
|
||
|
are 'identity' and 'modularity'
|
||
|
seed : integer, random_state, or None (default)
|
||
|
Indicator of numpy random number generation state.
|
||
|
See :ref:`Randomness<randomness>`.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
H : Graph
|
||
|
A graph with a similar eigenvector structure of the input one.
|
||
|
|
||
|
Raises
|
||
|
------
|
||
|
NetworkXError
|
||
|
If transformation has a value different from 'identity' or 'modularity'
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
Spectral Graph Forge (SGF) generates a random simple graph resembling the
|
||
|
global properties of the given one.
|
||
|
It leverages the low-rank approximation of the associated adjacency matrix
|
||
|
driven by the *alpha* precision parameter.
|
||
|
SGF preserves the number of nodes of the input graph and their ordering.
|
||
|
This way, nodes of output graphs resemble the properties of the input one
|
||
|
and attributes can be directly mapped.
|
||
|
|
||
|
It considers the graph adjacency matrices which can optionally be
|
||
|
transformed to other symmetric real matrices (currently transformation
|
||
|
options include *identity* and *modularity*).
|
||
|
The *modularity* transformation, in the sense of Newman's modularity matrix
|
||
|
allows the focusing on community structure related properties of the graph.
|
||
|
|
||
|
SGF applies a low-rank approximation whose fixed rank is computed from the
|
||
|
ratio *alpha* of the input graph adjacency matrix dimension.
|
||
|
This step performs a filtering on the input eigenvectors similar to the low
|
||
|
pass filtering common in telecommunications.
|
||
|
|
||
|
The filtered values (after truncation) are used as input to a Bernoulli
|
||
|
sampling for constructing a random adjacency matrix.
|
||
|
|
||
|
References
|
||
|
----------
|
||
|
.. [1] L. Baldesi, C. T. Butts, A. Markopoulou, "Spectral Graph Forge:
|
||
|
Graph Generation Targeting Modularity", IEEE Infocom, '18.
|
||
|
https://arxiv.org/abs/1801.01715
|
||
|
.. [2] M. Newman, "Networks: an introduction", Oxford university press,
|
||
|
2010
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> import networkx as nx
|
||
|
>>> G = nx.karate_club_graph()
|
||
|
>>> H = nx.spectral_graph_forge(G, 0.3)
|
||
|
>>>
|
||
|
"""
|
||
|
|
||
|
import numpy as np
|
||
|
import scipy.stats as stats
|
||
|
|
||
|
available_transformations = ['identity', 'modularity']
|
||
|
alpha = _truncate(alpha)
|
||
|
A = nx.to_numpy_matrix(G)
|
||
|
n = A.shape[1]
|
||
|
level = int(round(n*alpha))
|
||
|
|
||
|
if transformation not in available_transformations:
|
||
|
msg = '\'{0}\' is not a valid transformation. '.format(transformation)
|
||
|
msg += 'Transformations: {0}'.format(available_transformations)
|
||
|
raise nx.NetworkXError(msg)
|
||
|
|
||
|
K = np.ones((1, n)) * A
|
||
|
|
||
|
B = A
|
||
|
if (transformation == 'modularity'):
|
||
|
B -= np.transpose(K) * K / float(sum(np.ravel(K)))
|
||
|
|
||
|
B = _mat_spect_approx(B, level, sorteigs=True, absolute=True)
|
||
|
|
||
|
if (transformation == 'modularity'):
|
||
|
B += np.transpose(K) * K / float(sum(np.ravel(K)))
|
||
|
|
||
|
B = np.vectorize(_truncate, otypes=[np.float])(B)
|
||
|
np.fill_diagonal(B, np.zeros((1, n)))
|
||
|
|
||
|
for i in range(n-1):
|
||
|
B[i, i+1:] = stats.bernoulli.rvs(B[i, i+1:], random_state=seed)
|
||
|
B[i+1:, i] = np.transpose(B[i, i+1:])
|
||
|
|
||
|
H = nx.from_numpy_matrix(B)
|
||
|
|
||
|
return H
|
||
|
|
||
|
|
||
|
# fixture for pytest
|
||
|
def setup_module(module):
|
||
|
import pytest
|
||
|
numpy = pytest.importorskip('numpy')
|
||
|
scipy = pytest.importorskip('scipy')
|