├── GICOPix-SGCN ├── lib │ ├── __init__.py │ ├── spherical_vertex.py │ ├── graph.py │ ├── coarsening.py │ ├── utils.py │ └── models.py ├── requirements.txt ├── main.py └── data_generate.py ├── README.md └── .gitignore /GICOPix-SGCN/lib/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /GICOPix-SGCN/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | scikit-learn 4 | matplotlib 5 | 6 | gensim 7 | tensorflow-gpu 8 | #tensorflow 9 | 10 | jupyter 11 | ipython 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SGCN 2 | Code for the paper "Rotation Equivariant Graph Convolutional Network for Spherical Image Classification", CVPR 2020 3 | 4 | ## Requirements 5 | * Python3 == 3.6.5 6 | * Tensorflow == 1.10.0 7 | 8 | ## Usages 9 | 1.Install the dependencies 10 | ~~~ 11 | pip install -r requirements.txt 12 | ~~~ 13 | 2.Generate the dataset 14 | ~~~ 15 | python3.6 data_generate.py 16 | ~~~ 17 | 18 | 3.Train and test the model 19 | ~~~ 20 | python3.6 main.py 21 | ~~~ 22 | 23 | ## Citation 24 | If you find this code is useful for your research, please cite our paper "Rotation Equivariant Graph Convolutional Network for Spherical Image Classification" 25 | 26 | ~~~ 27 | @inproceedings{yang2020rotation, 28 | title={Rotation Equivariant Graph Convolutional Network for Spherical Image Classification}, 29 | author={Yang, Qin and Li, Chenglin and Dai, Wenrui and Zou, Junni and Qi, Guo-Jun and Xiong, Hongkai}, 30 | booktitle={Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition}, 31 | pages={4303--4312}, 32 | year={2020} 33 | } 34 | ~~~ 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /GICOPix-SGCN/main.py: -------------------------------------------------------------------------------- 1 | import sys, os,pdb 2 | sys.path.insert(0, '..') 3 | from lib import models, graph, utils, spherical_vertex 4 | 5 | from tensorflow.examples.tutorials.mnist import input_data 6 | import tensorflow as tf 7 | import numpy as np 8 | import time 9 | import pdb 10 | import scipy 11 | import pickle as pkl 12 | from PIL import Image 13 | 14 | os.environ['CUDA_VISIBLE_DEVICES']='1' 15 | 16 | flags = tf.app.flags 17 | FLAGS = flags.FLAGS 18 | 19 | # Graphs. 20 | flags.DEFINE_integer('number_edges', 6, 'Graph: minimum number of edges per vertex.') 21 | flags.DEFINE_string('metric', 'euclidean', 'Graph: similarity measure (between features).') 22 | # TODO: change cgcnn for combinatorial Laplacians. 23 | flags.DEFINE_bool('normalized_laplacian', True, 'Graph Laplacian: normalized.') 24 | flags.DEFINE_integer('coarsening_levels', 2, 'Number of coarsened graphs.') 25 | flags.DEFINE_integer('vertex_number',2562,'Number of vertex of the sphere') 26 | 27 | # Directories. 28 | flags.DEFINE_string('dir_data', os.path.join('data', 'mnist'), 'Directory to store data.') 29 | 30 | 31 | #r is the radius of the sphere 32 | def grid_graph(m,r,corners=False): 33 | z,z_theta = graph.grid_sphere(m,r) #z is rectangular coordinate system while z_theta is polar 34 | print('z shape is'+str(np.array(z).shape)+'z_theta shape is'+str(np.array(z_theta).shape)) 35 | dist, idx = graph.distance_sklearn_metrics(np.array(z), k=FLAGS.number_edges, metric=FLAGS.metric) 36 | A = graph.adjacency(dist, idx) 37 | 38 | # Connections are only vertical or horizontal on the grid. 39 | # Corner vertices are connected to 2 neightbors only. 40 | if corners: 41 | import scipy.sparse 42 | A = A.toarray() 43 | A[A < A.max()/1.5] = 0 44 | A = scipy.sparse.csr_matrix(A) 45 | print('{} edges'.format(A.nnz)) 46 | 47 | print("{} > {} edges".format(A.nnz//2, FLAGS.number_edges*m**2//2)) 48 | return z,z_theta,A 49 | 50 | t_start = time.process_time() 51 | tile_number = int((np.log2((FLAGS.vertex_number-2)/10))/2) #2**(2*tile_num)*10+2=vertex_number# 52 | graphs_adjacency=[] 53 | z_xyz = [] 54 | vertex_theta = [] 55 | for m in range(tile_number,tile_number-FLAGS.coarsening_levels-1,-1): 56 | z,z_theta,A = grid_graph(m,1,corners=False) #m is the tile_number,r is the radius of the sphere 57 | z_xyz.append(z) 58 | vertex_theta.append(z_theta) 59 | graphs_adjacency.append(A) 60 | L = [graph.laplacian(A, normalized=True) for A in graphs_adjacency] 61 | print(len(L)) 62 | 63 | 64 | index = [] 65 | for m in range(FLAGS.coarsening_levels): 66 | temp = [] 67 | for j in z_xyz[m+1]: 68 | temp.append(z_xyz[m].index(j)) 69 | index.append(temp) 70 | print('the shape of index',len(index),len(index[0]),len(index[1])) 71 | 72 | 73 | project_data_path = 'data/mnist_2562_TT' # for mnist 74 | #project_data_path = 'data/cifar10_2562_FFT' # for cifar10 75 | #project_data_path = '/mnt/data/modelnet40_40962_TTT' # for modelnet40 76 | 77 | if not os.path.exists(project_data_path): 78 | print("Please generate the dataset!") 79 | 80 | 81 | train_data = scipy.io.loadmat('{}/train_data.mat'.format(project_data_path))['train_value'] 82 | train_data /= train_data.max() 83 | val_data = scipy.io.loadmat('{}/val_data.mat'.format(project_data_path))['val_value'] 84 | val_data /= val_data.max() 85 | test_data = scipy.io.loadmat('{}/test_data.mat'.format(project_data_path))['test_value'] 86 | test_data /= test_data.max() 87 | 88 | train_labels = scipy.io.loadmat('{}/train_labels.mat'.format(project_data_path))['train_labels'][0] 89 | val_labels = scipy.io.loadmat('{}/val_labels.mat'.format(project_data_path))['val_labels'] 90 | test_labels = scipy.io.loadmat('{}/test_labels.mat'.format(project_data_path))['test_labels'][0] 91 | print('load data from {} done.'.format(project_data_path)) 92 | 93 | 94 | 95 | common = {} 96 | common['dir_name'] = 'MNIST/' 97 | common['num_epochs'] = 50 98 | common['batch_size'] = 10 99 | step_num = common['num_epochs']*train_data.shape[0]/common['batch_size'] 100 | common['eval_frequency'] = train_data.shape[0]/common['batch_size'] 101 | common['brelu'] = 'b1relu' 102 | common['pool'] = 'multipool' 103 | C = max(train_labels) + 1 # number of classes 104 | 105 | model_perf = utils.model_perf() 106 | 107 | 108 | common['regularization'] = 5e-4 109 | common['dropout'] = 0.9 110 | common['is_training'] = True#batch normalization 111 | 112 | common['learning_rate'] = [0.02,0.002] 113 | common['boundaries'] = [int(2/3*step_num)] 114 | print('boundaries is {}'.format(common['boundaries'])) 115 | common['momentum'] = 0.9 116 | 117 | common['F'] = [32, 64] 118 | common['K'] = [25, 25] 119 | common['p'] = [4, 4] 120 | common['M'] = [512, C] 121 | 122 | out_path = 'lr{}{}_bou{}_ep{}_bat{}_reg{}_drp{}_BN_mnist_TTT_logk10_K25'.format(common['learning_rate'][0], common['learning_rate'][1], common['boundaries'][0], common['num_epochs'], common['batch_size'], common['regularization'], common['dropout']) 123 | #file_out = open(out_path, 'a') 124 | file_out = None 125 | 126 | if True: 127 | name = out_path 128 | params = common.copy() 129 | params['dir_name'] += name 130 | params['filter'] = 'chebyshev5' 131 | model_perf.test(models.cgcnn(L, graphs_adjacency, index, z_xyz[-1], file_out, **params), name, params, 132 | train_data, train_labels, val_data, val_labels, test_data, test_labels, file_out) 133 | file_out.close() 134 | -------------------------------------------------------------------------------- /GICOPix-SGCN/lib/spherical_vertex.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import argparse 3 | from tensorflow.examples.tutorials.mnist import input_data 4 | import tensorflow as tf 5 | import matplotlib.pyplot as plt 6 | from math import isnan 7 | import pdb 8 | 9 | def cos(x): 10 | return np.cos(x*np.pi) 11 | def sin(x): 12 | return np.sin(x*np.pi) 13 | 14 | def normalize(v): 15 | d = np.sqrt(v[0]**2+v[1]**2+v[2]**2) 16 | for i in range(3): 17 | v[i]=v[i]/d 18 | return v 19 | 20 | def subdivide(v1,v2,v3,tile_number,points,depth): 21 | depth = depth+1 22 | if tile_number == 0: 23 | return 24 | 25 | v12=[0,0,0] 26 | v23=[0,0,0] 27 | v13=[0,0,0] 28 | for i in range(3): 29 | v12[i] = (v1[i]+v2[i])/2 30 | v23[i] = (v2[i]+v3[i])/2 31 | v13[i] = (v1[i]+v3[i])/2 32 | v12 = normalize(v12) 33 | v23 = normalize(v23) 34 | v13 = normalize(v13) 35 | if v12 not in points: 36 | points.append(v12) 37 | if v23 not in points: 38 | points.append(v23) 39 | if v13 not in points: 40 | points.append(v13) 41 | subdivide(v1,v12,v13,tile_number-1,points,depth) 42 | subdivide(v2,v23,v12,tile_number-1,points,depth) 43 | subdivide(v3,v13,v23,tile_number-1,points,depth) 44 | subdivide(v12,v23,v13,tile_number-1,points,depth) 45 | #return depth 46 | 47 | def uniform_sampling(m,r): 48 | #geodesic grid# 49 | #m is the number of vertex# 50 | #2**(2*tile_num)*10+2=m# 51 | #r is the radius# 52 | 53 | 54 | points_theta = [] 55 | depth = 0 56 | tile_number = m 57 | '''initial define''' 58 | m = np.sqrt(50-10*np.sqrt(5))/10 59 | n = np.sqrt(50+10*np.sqrt(5))/10 60 | v_base=[[-m,0,n],[m,0,n],[-m,0,-n],[m,0,-n],[0,n,m],[0,n,-m],[0,-n,m],[0,-n,-m],[n,m,0],[-n,m,0],[n,-m,0],[-n,-m,0]] 61 | points = v_base 62 | index = [[1,4,0],[4,9,0],[4,5,9],[8,5,4],[1,8,4], 63 | [1,10,8],[10,3,8],[8,3,5],[3,2,5],[3,7,2], 64 | [3,10,7],[10,6,7],[6,11,7],[6,0,11],[6,1,0], 65 | [10,1,6],[11,0,9],[2,11,9],[5,2,9],[11,2,7]] 66 | for i in range(20): 67 | subdivide(v_base[index[i][0]],v_base[index[i][1]],v_base[index[i][2]],tile_number,points,depth) 68 | print('the len of point'+str(len(points))+'the second'+str(points[0])) 69 | 70 | for i in range(len(points)): 71 | x = points[i][0] 72 | y = points[i][1] 73 | z = points[i][2] 74 | r = np.sqrt(x**2+y**2+z**2) 75 | theta = np.arccos(z/r)/np.pi 76 | phi = np.arctan(y/x)/np.pi 77 | if x < 0 : 78 | phi = phi+1 79 | if x >= 0 and y < 0 : 80 | phi = phi +2 #theta:0~1, phi:0~2 81 | theta = round(0.5-theta,4) 82 | phi = round(phi-1,4) #theta:0.5~-0.5, phi:-1~1 83 | if x == 0 and y == 0 : 84 | phi = 0.0 85 | points_theta.append([theta,phi]) 86 | return points, points_theta 87 | 88 | def transform(v): 89 | 90 | v = np.array(v) 91 | x, y, z = v[:,0], v[:, 1], v[:, 2] 92 | long = np.arctan2(y, x) 93 | xy2 = x**2 + y**2 94 | lat = np.arctan2(z, np.sqrt(xy2)) 95 | points_theta = np.transpose(np.array([lat/np.pi, long/np.pi]), [1,0]) 96 | 97 | return points_theta 98 | 99 | 100 | def uniform_sampling_spiral(m,r): 101 | 102 | #m is the number of vertex 103 | #spiral grid# 104 | points_xyz = [] 105 | points_theta = [] 106 | for i in range(m): 107 | h = -1+2.0*i/(m-1) 108 | theta=np.arccos(h)/np.pi 109 | if i==0 or i==m-1: 110 | phi=0 111 | else: 112 | phi=(phi*np.pi+3.6/np.sqrt(m*(1-h**2)))%(2*np.pi)/np.pi #the phi of the right side is the i-1 value 113 | phi=round(phi,4) 114 | theta = round(theta-0.5, 4) 115 | if phi>1: 116 | phi=phi-2 117 | x = r*sin(theta)*cos(phi) 118 | y = r*sin(theta)*sin(phi) 119 | z = r*cos(theta) 120 | points_xyz.append([x,y,z]) 121 | points_theta.append([theta,phi]) 122 | return points_xyz,points_theta 123 | 124 | 125 | def project_point(imgs,radius,points,rotation=False,selfRotation=False, centers=None, points_raw=None): 126 | vertex = [] 127 | if len(imgs.shape)>3: 128 | img_num, img_w, img_h, img_channel = imgs.shape 129 | elif len(imgs.shape)==3: 130 | img_num, img_w, img_h = imgs.shape 131 | elif len(imgs.shape)<3: 132 | print('imgs for dataset are unknown shape') 133 | pdb.set_trace() 134 | for i in range(imgs.shape[0]): 135 | beta = np.random.rand()*2 136 | if rotation: 137 | theta_tangent = round(np.random.randint(-90,90)/180,4) #discrete integer in degree, -0.5~0.5 138 | phi_tangent = round(np.random.randint(-180,180)/180,4) #-1~1 139 | else: 140 | theta_tangent = round(np.random.randint(-45,45)/180,4) #-0.5~0.5 141 | phi_tangent = round(np.random.randint(-90,90)/180,4) #-1~1 142 | if centers is not None: 143 | theta_tangent, phi_tangent = centers 144 | pixels = [] 145 | theta_erp = [] 146 | phi_erp = [] 147 | for j in range(len(points)): 148 | theta = points[j][0] 149 | phi = points[j][1] 150 | distance = np.arccos(cos(theta)*cos(theta_tangent)*cos(phi-phi_tangent)+sin(theta)*sin(theta_tangent)) 151 | c = sin(theta_tangent)*sin(theta)+cos(theta_tangent)*cos(theta)*cos(phi-phi_tangent) 152 | x = radius*cos(theta)*sin(phi-phi_tangent)/c 153 | y = radius*(cos(theta_tangent)*sin(theta)-sin(theta_tangent)*cos(theta)*cos(phi-phi_tangent))/c 154 | 155 | if selfRotation: 156 | x1 = x*cos(beta)-y*sin(beta) 157 | y1 = y*cos(beta)+x*sin(beta) 158 | x = x1 159 | y = y1 160 | if abs(x)>=img_w/2 or abs(y)>=img_h/2 or distance>0.5*np.pi or isnan(x): 161 | pixel = 0 * imgs[i,0,0] 162 | else: 163 | x = x+img_w/2 164 | y = img_h/2-y 165 | ''' 166 | pixel = imgs[i, int(y),int(x)] 167 | ''' 168 | # bilinear 169 | x_int = int(x) 170 | y_int = int(y) 171 | dx = x - x_int 172 | dy = y - y_int 173 | pixel_bo_left = imgs[i, y_int, x_int] 174 | pixel_bo_right = imgs[i, y_int, min(x_int+1, img_w-1)] 175 | pixel_up_left = imgs[i, min(y_int+1, img_h-1), x_int] 176 | pixel_up_right = imgs[i, min(y_int+1, img_h-1), min(x_int+1, img_w-1)] 177 | pixel_bo = (1-dx) * pixel_bo_left + dx * pixel_bo_right 178 | pixel_up = (1-dx) * pixel_up_left + dx * pixel_up_right 179 | pixel = (1-dy) * pixel_up + dy * pixel_bo 180 | pixels.append(pixel) 181 | if (i+1)%100==0: 182 | print('project_data for {}/{} done.'.format(i+1,img_num)) 183 | vertex.append(pixels) 184 | return vertex 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /GICOPix-SGCN/lib/graph.py: -------------------------------------------------------------------------------- 1 | import sklearn.metrics 2 | import sklearn.neighbors 3 | import matplotlib.pyplot as plt 4 | import scipy.sparse 5 | import scipy.sparse.linalg 6 | import scipy.spatial.distance 7 | import numpy as np 8 | from lib import spherical_vertex 9 | 10 | 11 | def grid(m, dtype=np.float32): 12 | """Return the embedding of a grid graph.""" 13 | M = m**2 14 | x = np.linspace(0, 1, m, dtype=dtype) 15 | y = np.linspace(0, 1, m, dtype=dtype) 16 | xx, yy = np.meshgrid(x, y) 17 | z = np.empty((M, 2), dtype) 18 | z[:, 0] = xx.reshape(M) 19 | z[:, 1] = yy.reshape(M) 20 | return z 21 | 22 | 23 | def grid_sphere(vertex_num,r,dtype=np.float32): 24 | z,z_theta=spherical_vertex.uniform_sampling(vertex_num,r) 25 | return z,z_theta 26 | 27 | def distance_scipy_spatial(z, k=4, metric='euclidean'): 28 | """Compute exact pairwise distances.""" 29 | d = scipy.spatial.distance.pdist(z, metric) 30 | d = scipy.spatial.distance.squareform(d) 31 | # k-NN graph. 32 | idx = np.argsort(d)[:, 1:k+1] 33 | d.sort() 34 | d = d[:, 1:k+1] 35 | return d, idx 36 | 37 | 38 | def distance_sklearn_metrics(z, k=4, metric='euclidean'): 39 | """Compute exact pairwise distances.""" 40 | d = sklearn.metrics.pairwise.pairwise_distances( 41 | z, metric=metric, n_jobs=-2) 42 | # k-NN graph. 43 | idx = np.argsort(d)[:, 1:k+1] 44 | d.sort() 45 | d = d[:, 1:k+1] 46 | return d, idx 47 | 48 | 49 | def distance_lshforest(z, k=4, metric='cosine'): 50 | """Return an approximation of the k-nearest cosine distances.""" 51 | assert metric is 'cosine' 52 | lshf = sklearn.neighbors.LSHForest() 53 | lshf.fit(z) 54 | dist, idx = lshf.kneighbors(z, n_neighbors=k+1) 55 | assert dist.min() < 1e-10 56 | dist[dist < 0] = 0 57 | return dist, idx 58 | 59 | # TODO: other ANNs s.a. NMSLIB, EFANNA, FLANN, Annoy, sklearn neighbors, PANN 60 | 61 | 62 | def adjacency(dist, idx): 63 | """Return the adjacency matrix of a kNN graph.""" 64 | M, k = dist.shape 65 | assert M, k == idx.shape 66 | assert dist.min() >= 0 67 | 68 | # Weights. 69 | sigma2 = np.mean(dist[:, -1])**2 70 | dist = np.exp(- dist**2 / sigma2) 71 | 72 | # Weight matrix. 73 | I = np.arange(0, M).repeat(k) 74 | J = idx.reshape(M*k) 75 | V = dist.reshape(M*k) 76 | W = scipy.sparse.coo_matrix((V, (I, J)), shape=(M, M)) 77 | 78 | # No self-connections. 79 | W.setdiag(0) 80 | 81 | # Non-directed graph. 82 | bigger = W.T > W 83 | W = W - W.multiply(bigger) + W.T.multiply(bigger) 84 | 85 | assert W.nnz % 2 == 0 86 | assert np.abs(W - W.T).mean() < 1e-10 87 | assert type(W) is scipy.sparse.csr.csr_matrix 88 | return W 89 | 90 | 91 | def replace_random_edges(A, noise_level): 92 | """Replace randomly chosen edges by random edges.""" 93 | M, M = A.shape 94 | n = int(noise_level * A.nnz // 2) 95 | 96 | indices = np.random.permutation(A.nnz//2)[:n] 97 | rows = np.random.randint(0, M, n) 98 | cols = np.random.randint(0, M, n) 99 | vals = np.random.uniform(0, 1, n) 100 | assert len(indices) == len(rows) == len(cols) == len(vals) 101 | 102 | A_coo = scipy.sparse.triu(A, format='coo') 103 | assert A_coo.nnz == A.nnz // 2 104 | assert A_coo.nnz >= n 105 | A = A.tolil() 106 | 107 | for idx, row, col, val in zip(indices, rows, cols, vals): 108 | old_row = A_coo.row[idx] 109 | old_col = A_coo.col[idx] 110 | 111 | A[old_row, old_col] = 0 112 | A[old_col, old_row] = 0 113 | A[row, col] = 1 114 | A[col, row] = 1 115 | 116 | A.setdiag(0) 117 | A = A.tocsr() 118 | A.eliminate_zeros() 119 | return A 120 | 121 | 122 | def laplacian(W, normalized=True): 123 | """Return the Laplacian of the weigth matrix.""" 124 | 125 | # Degree matrix. 126 | d = W.sum(axis=0) 127 | 128 | # Laplacian matrix. 129 | if not normalized: 130 | D = scipy.sparse.diags(d.A.squeeze(), 0) 131 | L = D - W 132 | else: 133 | d += np.spacing(np.array(0, W.dtype)) 134 | d = 1 / np.sqrt(d) 135 | D = scipy.sparse.diags(d.A.squeeze(), 0) 136 | I = scipy.sparse.identity(d.size, dtype=W.dtype) 137 | L = I - D * W * D 138 | 139 | # assert np.abs(L - L.T).mean() < 1e-9 140 | assert type(L) is scipy.sparse.csr.csr_matrix 141 | return L 142 | 143 | 144 | def lmax(L, normalized=True): 145 | """Upper-bound on the spectrum.""" 146 | if normalized: 147 | return 2 148 | else: 149 | return scipy.sparse.linalg.eigsh( 150 | L, k=1, which='LM', return_eigenvectors=False)[0] 151 | 152 | 153 | def fourier(L, algo='eigh', k=1): 154 | """Return the Fourier basis, i.e. the EVD of the Laplacian.""" 155 | 156 | def sort(lamb, U): 157 | idx = lamb.argsort() 158 | return lamb[idx], U[:, idx] 159 | 160 | if algo is 'eig': 161 | lamb, U = np.linalg.eig(L.toarray()) 162 | lamb, U = sort(lamb, U) 163 | elif algo is 'eigh': 164 | lamb, U = np.linalg.eigh(L.toarray()) 165 | elif algo is 'eigs': 166 | lamb, U = scipy.sparse.linalg.eigs(L, k=k, which='SM') 167 | lamb, U = sort(lamb, U) 168 | elif algo is 'eigsh': 169 | lamb, U = scipy.sparse.linalg.eigsh(L, k=k, which='SM') 170 | 171 | return lamb, U 172 | 173 | 174 | def plot_spectrum(L, algo='eig'): 175 | """Plot the spectrum of a list of multi-scale Laplacians L.""" 176 | # Algo is eig to be sure to get all eigenvalues. 177 | plt.figure(figsize=(17, 5)) 178 | for i, lap in enumerate(L): 179 | lamb, U = fourier(lap, algo) 180 | step = 2**i 181 | x = range(step//2, L[0].shape[0], step) 182 | lb = 'L_{} spectrum in [{:1.2e}, {:1.2e}]'.format(i, lamb[0], lamb[-1]) 183 | plt.plot(x, lamb, '.', label=lb) 184 | plt.legend(loc='best') 185 | plt.xlim(0, L[0].shape[0]) 186 | plt.ylim(ymin=0) 187 | 188 | 189 | def lanczos(L, X, K): 190 | """ 191 | Given the graph Laplacian and a data matrix, return a data matrix which can 192 | be multiplied by the filter coefficients to filter X using the Lanczos 193 | polynomial approximation. 194 | """ 195 | M, N = X.shape 196 | assert L.dtype == X.dtype 197 | 198 | def basis(L, X, K): 199 | """ 200 | Lanczos algorithm which computes the orthogonal matrix V and the 201 | tri-diagonal matrix H. 202 | """ 203 | a = np.empty((K, N), L.dtype) 204 | b = np.zeros((K, N), L.dtype) 205 | V = np.empty((K, M, N), L.dtype) 206 | V[0, ...] = X / np.linalg.norm(X, axis=0) 207 | for k in range(K-1): 208 | W = L.dot(V[k, ...]) 209 | a[k, :] = np.sum(W * V[k, ...], axis=0) 210 | W = W - a[k, :] * V[k, ...] - ( 211 | b[k, :] * V[k-1, ...] if k > 0 else 0) 212 | b[k+1, :] = np.linalg.norm(W, axis=0) 213 | V[k+1, ...] = W / b[k+1, :] 214 | a[K-1, :] = np.sum(L.dot(V[K-1, ...]) * V[K-1, ...], axis=0) 215 | return V, a, b 216 | 217 | def diag_H(a, b, K): 218 | """Diagonalize the tri-diagonal H matrix.""" 219 | H = np.zeros((K*K, N), a.dtype) 220 | H[:K**2:K+1, :] = a 221 | H[1:(K-1)*K:K+1, :] = b[1:, :] 222 | H.shape = (K, K, N) 223 | Q = np.linalg.eigh(H.T, UPLO='L')[1] 224 | Q = np.swapaxes(Q, 1, 2).T 225 | return Q 226 | 227 | V, a, b = basis(L, X, K) 228 | Q = diag_H(a, b, K) 229 | Xt = np.empty((K, M, N), L.dtype) 230 | for n in range(N): 231 | Xt[..., n] = Q[..., n].T.dot(V[..., n]) 232 | Xt *= Q[0, :, np.newaxis, :] 233 | Xt *= np.linalg.norm(X, axis=0) 234 | return Xt # Q[0, ...] 235 | 236 | 237 | def rescale_L(L, lmax=2): 238 | """Rescale the Laplacian eigenvalues in [-1,1].""" 239 | M, M = L.shape 240 | I = scipy.sparse.identity(M, format='csr', dtype=L.dtype) 241 | L /= lmax / 2 242 | L -= I 243 | return L 244 | 245 | 246 | def chebyshev(L, X, K): 247 | """Return T_k X where T_k are the Chebyshev polynomials of order up to K. 248 | Complexity is O(KMN).""" 249 | M, N = X.shape 250 | assert L.dtype == X.dtype 251 | 252 | # L = rescale_L(L, lmax) 253 | # Xt = T @ X: MxM @ MxN. 254 | Xt = np.empty((K, M, N), L.dtype) 255 | # Xt_0 = T_0 X = I X = X. 256 | Xt[0, ...] = X 257 | # Xt_1 = T_1 X = L X. 258 | if K > 1: 259 | Xt[1, ...] = L.dot(X) 260 | # Xt_k = 2 L Xt_k-1 - Xt_k-2. 261 | for k in range(2, K): 262 | Xt[k, ...] = 2 * L.dot(Xt[k-1, ...]) - Xt[k-2, ...] 263 | return Xt 264 | -------------------------------------------------------------------------------- /GICOPix-SGCN/data_generate.py: -------------------------------------------------------------------------------- 1 | import sys, os,pdb 2 | sys.path.insert(0, '..') 3 | from lib import models, graph, utils, spherical_vertex 4 | 5 | from tensorflow.examples.tutorials.mnist import input_data 6 | import tensorflow as tf 7 | import numpy as np 8 | import time 9 | import pdb 10 | import scipy 11 | import pickle as pkl 12 | from PIL import Image 13 | 14 | flags = tf.app.flags 15 | FLAGS = flags.FLAGS 16 | 17 | # Graphs. 18 | flags.DEFINE_integer('number_edges', 6, 'Graph: minimum number of edges per vertex.') 19 | flags.DEFINE_string('metric', 'euclidean', 'Graph: similarity measure (between features).') 20 | flags.DEFINE_bool('normalized_laplacian', True, 'Graph Laplacian: normalized.') 21 | flags.DEFINE_integer('coarsening_levels', 2, 'Number of coarsened graphs.') 22 | flags.DEFINE_integer('vertex_number',2562,'Number of vertex of the sphere') 23 | 24 | # Directories. 25 | flags.DEFINE_string('dir_data', os.path.join('data', 'mnist'), 'Directory to store data.') 26 | flags.DEFINE_string('f','','kernel') 27 | 28 | #r is the radius of the sphere 29 | def grid_graph(m,r,corners=False): 30 | z,z_theta = graph.grid_sphere(m,r) #z is rectangular coordinate system while z_theta is polar 31 | print('z shape is'+str(np.array(z).shape)+'z_theta shape is'+str(np.array(z_theta).shape)) 32 | dist, idx = graph.distance_sklearn_metrics(np.array(z), k=FLAGS.number_edges, metric=FLAGS.metric) 33 | A = graph.adjacency(dist, idx) 34 | 35 | # Connections are only vertical or horizontal on the grid. 36 | # Corner vertices are connected to 2 neightbors only. 37 | if corners: 38 | import scipy.sparse 39 | A = A.toarray() 40 | A[A < A.max()/1.5] = 0 41 | A = scipy.sparse.csr_matrix(A) 42 | print('{} edges'.format(A.nnz)) 43 | 44 | print("{} > {} edges".format(A.nnz//2, FLAGS.number_edges*m**2//2)) 45 | return z,z_theta,A 46 | 47 | t_start = time.process_time() 48 | tile_number = int((np.log2((FLAGS.vertex_number-2)/10))/2) #2**(2*tile_num)*10+2=vertex_number# 49 | graphs_adjacency=[] 50 | z_xyz = [] 51 | vertex_theta = [] 52 | for m in range(tile_number,tile_number-FLAGS.coarsening_levels-1,-1): 53 | z,z_theta,A = grid_graph(m,1,corners=False) #m is the tile_number,r is the radius of the sphere 54 | #A = graph.replace_random_edges(A, 0) retain the hexagonal shape 55 | z_xyz.append(z) 56 | vertex_theta.append(z_theta) 57 | graphs_adjacency.append(A) 58 | L = [graph.laplacian(A, normalized=True) for A in graphs_adjacency] 59 | print(len(L)) 60 | 61 | 62 | index = [] 63 | for m in range(FLAGS.coarsening_levels): 64 | temp = [] 65 | for j in z_xyz[m+1]: 66 | temp.append(z_xyz[m].index(j)) 67 | index.append(temp) 68 | print('the shape of index',len(index),len(index[0]),len(index[1])) 69 | 70 | 71 | def load_mnist(FLAGS): 72 | mnist = input_data.read_data_sets(FLAGS.dir_data, one_hot=False) 73 | 74 | train_data = mnist.train.images.astype(np.float32).reshape(-1, 28,28) 75 | val_data = mnist.validation.images.astype(np.float32).reshape(-1,28,28) 76 | test_data = mnist.test.images.astype(np.float32).reshape(-1, 28,28) 77 | train_labels = mnist.train.labels 78 | val_labels = mnist.validation.labels 79 | test_labels = mnist.test.labels 80 | 81 | print('Load mnist dataset done.') 82 | return train_data, train_labels, val_data, val_labels, test_data, test_labels 83 | 84 | 85 | def _load_cifar10_batch(file_name): 86 | ''' 87 | Read cifar10 per file by pickle 88 | Args: 89 | file_name 90 | Return: 91 | X: data with shape [10000, 32, 32, 3] 92 | Y: label with shape [10000] 93 | ''' 94 | with open(file_name, 'rb') as f: 95 | data_dict = pkl.load(f, encoding='latin1') 96 | X = data_dict['data'] 97 | X = X.reshape(10000, 3, 32, 32).transpose(0,2,3,1).astype(np.float32) 98 | Y = data_dict['labels'] 99 | Y = np.array(Y) 100 | return X,Y 101 | 102 | 103 | def load_cifar10(root_path='./data/cifar10'): 104 | ''' 105 | Load cifar10 dataset 106 | Args: 107 | root_path: path of cifar10 dataset, not contain 'cifar-10-batches-py' 108 | Return: 109 | train_data: shape [50000, 32, 32, 3] 110 | train_labels: shape [50000] 111 | test_data: shape [10000, 32, 32, 3] 112 | test_labels: shape [10000] 113 | ''' 114 | train_batches = ['{}/cifar-10-batches-py/data_batch_{}'.format(root_path, i+1) for i in range(5)] 115 | test_batch = '{}/cifar-10-batches-py/test_batch'.format(root_path) 116 | train_data = [] 117 | train_labels = [] 118 | for train_batch in train_batches: 119 | tmp_data, tmp_label = _load_cifar10_batch(train_batch) 120 | train_data.append(tmp_data) 121 | train_labels.append(tmp_label) 122 | train_data = np.vstack(train_data) 123 | train_labels = np.hstack(train_labels) 124 | val_data = train_data[45000:] 125 | val_labels = train_labels[45000:] 126 | train_data = train_data[:45000] 127 | train_labels = train_labels[:45000] 128 | test_data, test_labels = _load_cifar10_batch(test_batch) 129 | print('Load cifar10 dataset done.') 130 | return train_data, train_labels, val_data, val_labels, test_data, test_labels 131 | 132 | def load_modelnet40(root_path='/home/yq/models/PDOs-master'): 133 | root_path = '{}/experiments/exp2_modelnet40/data'.format(root_path) 134 | classes = ['airplane', 'bowl', 'desk', 'keyboard', 'person', 'sofa', 'tv_stand', 'bathtub', 'car', 'door', 135 | 'lamp', 'piano', 'stairs', 'vase', 'bed', 'chair', 'dresser', 'laptop', 'plant', 'stool', 136 | 'wardrobe', 'bench', 'cone', 'flower_pot', 'mantel', 'radio', 'table', 'xbox', 'bookshelf', 'cup', 137 | 'glass_box', 'monitor', 'range_hood', 'tent', 'bottle', 'curtain', 'guitar', 'night_stand', 'sink', 'toilet'] 138 | class2index = dict(zip(classes, range(len(classes)))) 139 | files_train = ['{}/modelnet40_train/{}'.format(root_path, f) for f in os.listdir('{}/modelnet40_train'.format(root_path)) if 'npy' in f] 140 | files_test = ['{}/modelnet40_test/{}'.format(root_path, f) for f in os.listdir('{}/modelnet40_test'.format(root_path)) if 'npy' in f] 141 | train_data = [np.load(f) for f in files_train] 142 | train_data = np.stack(train_data).transpose([0,2,1]) 143 | test_data = [np.load(f) for f in files_test] 144 | test_data = np.stack(test_data).transpose([0,2,1]) 145 | train_labels = [] 146 | for f in files_train: 147 | train_labels.append(class2index[f.split('/')[-1].strip('.npy')[4:-5]]) 148 | train_labels = np.array(train_labels) 149 | test_labels = [] 150 | for f in files_test: 151 | test_labels.append(class2index[f.split('/')[-1].strip('.npy')[4:-5]]) 152 | test_labels = np.array(test_labels) 153 | print('train_data shape ', train_data.shape, 'train_labels shape', train_labels.shape) 154 | print('test_data shape ', test_data.shape, 'test_labels shape', test_labels.shape) 155 | val_data = test_data[:1000] 156 | val_labels = test_labels[:1000] 157 | 158 | return train_data, train_labels, val_data, val_labels, test_data, test_labels#, points_raw 159 | 160 | points_raw = None 161 | train_data, train_labels, val_data, val_labels, test_data, test_labels = load_mnist(FLAGS) 162 | #train_data, train_labels, val_data, val_labels, test_data, test_labels = load_cifar10() 163 | #train_data, train_labels, val_data, val_labels, test_data, test_labels = load_modelnet40() 164 | 165 | 166 | train_data = train_data / train_data.max(0,keepdims=True).max(1,keepdims=True) 167 | val_data = val_data / val_data.max(0,keepdims=True).max(1,keepdims=True) 168 | test_data = test_data / test_data.max(0,keepdims=True).max(1,keepdims=True) 169 | print('load dataset done.') 170 | 171 | 172 | 173 | #transform to sphere_data 174 | 175 | 176 | img_w = train_data.shape[1] #img_w is the size of img 177 | if img_w>1000: 178 | img_w = 2 179 | #radius = img_w/0.83 #80*80/180*360 180 | radius = img_w/1.73 #120*120/180*360 181 | #radius = img_w/5.67 #160*160/180*360 182 | 183 | 184 | project_data_path = 'data/mnist_2562_FFT' # for mnist 185 | #project_data_path = 'data/cifar10_2562_FFT' # for cifar10 inter 186 | #project_data_path = '/mnt/data/modelnet40_40962_TTT' # for modelnet40 187 | 188 | 189 | print('start to project dataset') 190 | if not os.path.exists(project_data_path): 191 | os.mkdir(project_data_path) 192 | 193 | train_data=spherical_vertex.project_point(train_data,radius,vertex_theta[0],rotation=False, selfRotation=False, points_raw=points_raw) 194 | scipy.io.savemat('{}/train_data.mat'.format(project_data_path), {'train_value':train_data}) 195 | 196 | val_data=spherical_vertex.project_point(val_data,radius,vertex_theta[0],rotation=False, selfRotation=False, points_raw=points_raw) 197 | scipy.io.savemat('{}/val_data.mat'.format(project_data_path), {'val_value':val_data}) 198 | 199 | test_data=spherical_vertex.project_point(test_data,radius,vertex_theta[0],rotation=True, selfRotation=False, points_raw=points_raw) 200 | scipy.io.savemat('{}/test_data.mat'.format(project_data_path), {'test_value':test_data}) 201 | 202 | scipy.io.savemat('{}/train_labels.mat'.format(project_data_path), {'train_labels':train_labels}) 203 | scipy.io.savemat('{}/val_labels.mat'.format(project_data_path), {'val_labels':val_labels}) 204 | scipy.io.savemat('{}/test_labels.mat'.format(project_data_path), {'test_labels':test_labels}) 205 | print('project dataset done') 206 | 207 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /GICOPix-SGCN/lib/coarsening.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.sparse 3 | import pdb 4 | CHANNELS = None # [None, 3, 6] 5 | 6 | 7 | def coarsen(A, levels, self_connections=False): 8 | """ 9 | Coarsen a graph, represented by its adjacency matrix A, at multiple 10 | levels. 11 | """ 12 | graphs, parents = metis(A, levels) 13 | perms = compute_perm(parents) 14 | 15 | for i, A in enumerate(graphs): 16 | M, M = A.shape 17 | 18 | if not self_connections: 19 | A = A.tocoo() 20 | A.setdiag(0) 21 | 22 | if i < levels: 23 | A = perm_adjacency(A, perms[i]) 24 | 25 | A = A.tocsr() 26 | A.eliminate_zeros() 27 | graphs[i] = A 28 | 29 | Mnew, Mnew = A.shape 30 | print('Layer {0}: M_{0} = |V| = {1} nodes ({2} added),' 31 | '|E| = {3} edges'.format(i, Mnew, Mnew-M, A.nnz//2)) 32 | 33 | return graphs, perms[0] if levels > 0 else None 34 | 35 | 36 | def metis(W, levels, rid=None): 37 | """ 38 | Coarsen a graph multiple times using the METIS algorithm. 39 | 40 | INPUT 41 | W: symmetric sparse weight (adjacency) matrix 42 | levels: the number of coarsened graphs 43 | 44 | OUTPUT 45 | graph[0]: original graph of size N_1 46 | graph[2]: coarser graph of size N_2 < N_1 47 | graph[levels]: coarsest graph of Size N_levels < ... < N_2 < N_1 48 | parents[i] is a vector of size N_i with entries ranging from 1 to N_{i+1} 49 | which indicate the parents in the coarser graph[i+1] 50 | nd_sz{i} is a vector of size N_i that contains the size of the supernode in the graph{i} 51 | 52 | NOTE 53 | if "graph" is a list of length k, then "parents" will be a list of length k-1 54 | """ 55 | 56 | N, N = W.shape 57 | if rid is None: 58 | rid = np.random.permutation(range(N)) 59 | parents = [] 60 | degree = W.sum(axis=0) - W.diagonal() 61 | graphs = [] 62 | graphs.append(W) 63 | #supernode_size = np.ones(N) 64 | #nd_sz = [supernode_size] 65 | #count = 0 66 | 67 | #while N > maxsize: 68 | for _ in range(levels): 69 | 70 | #count += 1 71 | 72 | # CHOOSE THE WEIGHTS FOR THE PAIRING 73 | # weights = ones(N,1) # metis weights 74 | weights = degree # graclus weights 75 | # weights = supernode_size # other possibility 76 | weights = np.array(weights).squeeze() 77 | 78 | # PAIR THE VERTICES AND CONSTRUCT THE ROOT VECTOR 79 | idx_row, idx_col, val = scipy.sparse.find(W) 80 | perm = np.argsort(idx_row) 81 | rr = idx_row[perm] 82 | cc = idx_col[perm] 83 | vv = val[perm] 84 | cluster_id = metis_one_level(rr,cc,vv,rid,weights) # rr is ordered 85 | parents.append(cluster_id) 86 | 87 | # TO DO 88 | # COMPUTE THE SIZE OF THE SUPERNODES AND THEIR DEGREE 89 | #supernode_size = full( sparse(cluster_id, ones(N,1) , supernode_size ) ) 90 | #print(cluster_id) 91 | #print(supernode_size) 92 | #nd_sz{count+1}=supernode_size; 93 | 94 | # COMPUTE THE EDGES WEIGHTS FOR THE NEW GRAPH 95 | nrr = cluster_id[rr] 96 | ncc = cluster_id[cc] 97 | nvv = vv 98 | Nnew = cluster_id.max() + 1 99 | # CSR is more appropriate: row,val pairs appear multiple times 100 | W = scipy.sparse.csr_matrix((nvv,(nrr,ncc)), shape=(Nnew,Nnew)) 101 | W.eliminate_zeros() 102 | # Add new graph to the list of all coarsened graphs 103 | graphs.append(W) 104 | N, N = W.shape 105 | 106 | # COMPUTE THE DEGREE (OMIT OR NOT SELF LOOPS) 107 | degree = W.sum(axis=0) 108 | #degree = W.sum(axis=0) - W.diagonal() 109 | 110 | # CHOOSE THE ORDER IN WHICH VERTICES WILL BE VISTED AT THE NEXT PASS 111 | #[~, rid]=sort(ss); # arthur strategy 112 | #[~, rid]=sort(supernode_size); # thomas strategy 113 | #rid=randperm(N); # metis/graclus strategy 114 | ss = np.array(W.sum(axis=0)).squeeze() 115 | rid = np.argsort(ss) 116 | 117 | return graphs, parents 118 | 119 | 120 | # Coarsen a graph given by rr,cc,vv. rr is assumed to be ordered 121 | def metis_one_level(rr,cc,vv,rid,weights): 122 | 123 | nnz = rr.shape[0] 124 | N = rr[nnz-1] + 1 125 | 126 | marked = np.zeros(N, np.bool) 127 | rowstart = np.zeros(N, np.int32) 128 | rowlength = np.zeros(N, np.int32) 129 | cluster_id = np.zeros(N, np.int32) 130 | 131 | oldval = rr[0] 132 | count = 0 133 | clustercount = 0 134 | 135 | for ii in range(nnz): 136 | rowlength[count] = rowlength[count] + 1 137 | if rr[ii] > oldval: 138 | oldval = rr[ii] 139 | rowstart[count+1] = ii 140 | count = count + 1 141 | 142 | for ii in range(N): 143 | tid = rid[ii] 144 | if not marked[tid]: 145 | wmax = 0.0 146 | rs = rowstart[tid] 147 | marked[tid] = True 148 | bestneighbor = -1 149 | for jj in range(rowlength[tid]): 150 | nid = cc[rs+jj] 151 | if marked[nid]: 152 | tval = 0.0 153 | else: 154 | tval = vv[rs+jj] * (1.0/weights[tid] + 1.0/weights[nid]) 155 | if tval > wmax: 156 | wmax = tval 157 | bestneighbor = nid 158 | 159 | cluster_id[tid] = clustercount 160 | 161 | if bestneighbor > -1: 162 | cluster_id[bestneighbor] = clustercount 163 | marked[bestneighbor] = True 164 | 165 | clustercount += 1 166 | 167 | return cluster_id 168 | 169 | def compute_perm(parents): 170 | """ 171 | Return a list of indices to reorder the adjacency and data matrices so 172 | that the union of two neighbors from layer to layer forms a binary tree. 173 | """ 174 | 175 | # Order of last layer is random (chosen by the clustering algorithm). 176 | indices = [] 177 | if len(parents) > 0: 178 | M_last = max(parents[-1]) + 1 179 | indices.append(list(range(M_last))) 180 | 181 | for parent in parents[::-1]: 182 | #print('parent: {}'.format(parent)) 183 | 184 | # Fake nodes go after real ones. 185 | pool_singeltons = len(parent) 186 | 187 | indices_layer = [] 188 | for i in indices[-1]: 189 | indices_node = list(np.where(parent == i)[0]) 190 | assert 0 <= len(indices_node) <= 2 191 | #print('indices_node: {}'.format(indices_node)) 192 | 193 | # Add a node to go with a singelton. 194 | if len(indices_node) is 1: 195 | indices_node.append(pool_singeltons) 196 | pool_singeltons += 1 197 | #print('new singelton: {}'.format(indices_node)) 198 | # Add two nodes as children of a singelton in the parent. 199 | elif len(indices_node) is 0: 200 | indices_node.append(pool_singeltons+0) 201 | indices_node.append(pool_singeltons+1) 202 | pool_singeltons += 2 203 | #print('singelton childrens: {}'.format(indices_node)) 204 | 205 | indices_layer.extend(indices_node) 206 | indices.append(indices_layer) 207 | 208 | # Sanity checks. 209 | for i,indices_layer in enumerate(indices): 210 | M = M_last*2**i 211 | # Reduction by 2 at each layer (binary tree). 212 | assert len(indices[0] == M) 213 | # The new ordering does not omit an indice. 214 | assert sorted(indices_layer) == list(range(M)) 215 | 216 | return indices[::-1] 217 | 218 | assert (compute_perm([np.array([4,1,1,2,2,3,0,0,3]),np.array([2,1,0,1,0])]) 219 | == [[3,4,0,9,1,2,5,8,6,7,10,11],[2,4,1,3,0,5],[0,1,2]]) 220 | 221 | def perm_data(x, indices): 222 | """ 223 | Permute data matrix, i.e. exchange node ids, 224 | so that binary unions form the clustering tree. 225 | """ 226 | if indices is None: 227 | return x 228 | 229 | #pdb.set_trace() 230 | 231 | if CHANNELS == None: 232 | N, M = x.shape 233 | else: 234 | N, M, F = x.shape 235 | Mnew = len(indices) 236 | assert Mnew >= M 237 | if CHANNELS == None: 238 | xnew = np.empty((N, Mnew)) 239 | else: 240 | xnew = np.empty((N, Mnew, F)) 241 | for i,j in enumerate(indices): 242 | # Existing vertex, i.e. real data. 243 | if j < M: 244 | if CHANNELS == None: 245 | xnew[:,i] = x[:,j] 246 | else: 247 | xnew[:,i,:] = x[:,j,:] 248 | # Fake vertex because of singeltons. 249 | # They will stay 0 so that max pooling chooses the singelton. 250 | # Or -infty ? 251 | else: 252 | if CHANNELS == None: 253 | xnew[:,i] = np.zeros(N) 254 | else: 255 | xnew[:,i,:] = np.zeros((N,F)) 256 | return xnew 257 | 258 | def perm_adjacency(A, indices): 259 | """ 260 | Permute adjacency matrix, i.e. exchange node ids, 261 | so that binary unions form the clustering tree. 262 | """ 263 | if indices is None: 264 | return A 265 | 266 | M, M = A.shape 267 | Mnew = len(indices) 268 | assert Mnew >= M 269 | A = A.tocoo() 270 | 271 | # Add Mnew - M isolated vertices. 272 | if Mnew > M: 273 | rows = scipy.sparse.coo_matrix((Mnew-M, M), dtype=np.float32) 274 | cols = scipy.sparse.coo_matrix((Mnew, Mnew-M), dtype=np.float32) 275 | A = scipy.sparse.vstack([A, rows]) 276 | A = scipy.sparse.hstack([A, cols]) 277 | 278 | # Permute the rows and the columns. 279 | perm = np.argsort(indices) 280 | A.row = np.array(perm)[A.row] 281 | A.col = np.array(perm)[A.col] 282 | 283 | # assert np.abs(A - A.T).mean() < 1e-9 284 | assert type(A) is scipy.sparse.coo.coo_matrix 285 | return A 286 | -------------------------------------------------------------------------------- /GICOPix-SGCN/lib/utils.py: -------------------------------------------------------------------------------- 1 | import gensim 2 | import sklearn, sklearn.datasets 3 | import sklearn.naive_bayes, sklearn.linear_model, sklearn.svm, sklearn.neighbors, sklearn.ensemble 4 | import matplotlib.pyplot as plt 5 | import scipy.sparse 6 | import numpy as np 7 | import time, re 8 | 9 | 10 | # Helpers to process text documents. 11 | 12 | 13 | class TextDataset(object): 14 | def clean_text(self, num='substitute'): 15 | # TODO: stemming, lemmatisation 16 | for i,doc in enumerate(self.documents): 17 | # Digits. 18 | if num is 'spell': 19 | doc = doc.replace('0', ' zero ') 20 | doc = doc.replace('1', ' one ') 21 | doc = doc.replace('2', ' two ') 22 | doc = doc.replace('3', ' three ') 23 | doc = doc.replace('4', ' four ') 24 | doc = doc.replace('5', ' five ') 25 | doc = doc.replace('6', ' six ') 26 | doc = doc.replace('7', ' seven ') 27 | doc = doc.replace('8', ' eight ') 28 | doc = doc.replace('9', ' nine ') 29 | elif num is 'substitute': 30 | # All numbers are equal. Useful for embedding (countable words) ? 31 | doc = re.sub('(\\d+)', ' NUM ', doc) 32 | elif num is 'remove': 33 | # Numbers are uninformative (they are all over the place). Useful for bag-of-words ? 34 | # But maybe some kind of documents contain more numbers, e.g. finance. 35 | # Some documents are indeed full of numbers. At least in 20NEWS. 36 | doc = re.sub('[0-9]', ' ', doc) 37 | # Remove everything except a-z characters and single space. 38 | doc = doc.replace('$', ' dollar ') 39 | doc = doc.lower() 40 | doc = re.sub('[^a-z]', ' ', doc) 41 | doc = ' '.join(doc.split()) # same as doc = re.sub('\s{2,}', ' ', doc) 42 | self.documents[i] = doc 43 | 44 | def vectorize(self, **params): 45 | # TODO: count or tf-idf. Or in normalize ? 46 | vectorizer = sklearn.feature_extraction.text.CountVectorizer(**params) 47 | self.data = vectorizer.fit_transform(self.documents) 48 | self.vocab = vectorizer.get_feature_names() 49 | assert len(self.vocab) == self.data.shape[1] 50 | 51 | def data_info(self, show_classes=False): 52 | N, M = self.data.shape 53 | sparsity = self.data.nnz / N / M * 100 54 | print('N = {} documents, M = {} words, sparsity={:.4f}%'.format(N, M, sparsity)) 55 | if show_classes: 56 | for i in range(len(self.class_names)): 57 | num = sum(self.labels == i) 58 | print(' {:5d} documents in class {:2d} ({})'.format(num, i, self.class_names[i])) 59 | 60 | def show_document(self, i): 61 | label = self.labels[i] 62 | name = self.class_names[label] 63 | try: 64 | text = self.documents[i] 65 | wc = len(text.split()) 66 | except AttributeError: 67 | text = None 68 | wc = 'N/A' 69 | print('document {}: label {} --> {}, {} words'.format(i, label, name, wc)) 70 | try: 71 | vector = self.data[i,:] 72 | for j in range(vector.shape[1]): 73 | if vector[0,j] != 0: 74 | print(' {:.2f} "{}" ({})'.format(vector[0,j], self.vocab[j], j)) 75 | except AttributeError: 76 | pass 77 | return text 78 | 79 | def keep_documents(self, idx): 80 | """Keep the documents given by the index, discard the others.""" 81 | self.documents = [self.documents[i] for i in idx] 82 | self.labels = self.labels[idx] 83 | self.data = self.data[idx,:] 84 | 85 | def keep_words(self, idx): 86 | """Keep the documents given by the index, discard the others.""" 87 | self.data = self.data[:,idx] 88 | self.vocab = [self.vocab[i] for i in idx] 89 | try: 90 | self.embeddings = self.embeddings[idx,:] 91 | except AttributeError: 92 | pass 93 | 94 | def remove_short_documents(self, nwords, vocab='selected'): 95 | """Remove a document if it contains less than nwords.""" 96 | if vocab is 'selected': 97 | # Word count with selected vocabulary. 98 | wc = self.data.sum(axis=1) 99 | wc = np.squeeze(np.asarray(wc)) 100 | elif vocab is 'full': 101 | # Word count with full vocabulary. 102 | wc = np.empty(len(self.documents), dtype=np.int) 103 | for i,doc in enumerate(self.documents): 104 | wc[i] = len(doc.split()) 105 | idx = np.argwhere(wc >= nwords).squeeze() 106 | self.keep_documents(idx) 107 | return wc 108 | 109 | def keep_top_words(self, M, Mprint=20): 110 | """Keep in the vocaluary the M words who appear most often.""" 111 | freq = self.data.sum(axis=0) 112 | freq = np.squeeze(np.asarray(freq)) 113 | idx = np.argsort(freq)[::-1] 114 | idx = idx[:M] 115 | self.keep_words(idx) 116 | print('most frequent words') 117 | for i in range(Mprint): 118 | print(' {:3d}: {:10s} {:6d} counts'.format(i, self.vocab[i], freq[idx][i])) 119 | return freq[idx] 120 | 121 | def normalize(self, norm='l1'): 122 | """Normalize data to unit length.""" 123 | # TODO: TF-IDF. 124 | data = self.data.astype(np.float64) 125 | self.data = sklearn.preprocessing.normalize(data, axis=1, norm=norm) 126 | 127 | def embed(self, filename=None, size=100): 128 | """Embed the vocabulary using pre-trained vectors.""" 129 | if filename: 130 | model = gensim.models.Word2Vec.load_word2vec_format(filename, binary=True) 131 | size = model.vector_size 132 | else: 133 | class Sentences(object): 134 | def __init__(self, documents): 135 | self.documents = documents 136 | def __iter__(self): 137 | for document in self.documents: 138 | yield document.split() 139 | model = gensim.models.Word2Vec(Sentences(self.documents), size) 140 | self.embeddings = np.empty((len(self.vocab), size)) 141 | keep = [] 142 | not_found = 0 143 | for i,word in enumerate(self.vocab): 144 | try: 145 | self.embeddings[i,:] = model[word] 146 | keep.append(i) 147 | except KeyError: 148 | not_found += 1 149 | print('{} words not found in corpus'.format(not_found, i)) 150 | self.keep_words(keep) 151 | 152 | class Text20News(TextDataset): 153 | def __init__(self, **params): 154 | dataset = sklearn.datasets.fetch_20newsgroups(**params) 155 | self.documents = dataset.data 156 | self.labels = dataset.target 157 | self.class_names = dataset.target_names 158 | assert max(self.labels) + 1 == len(self.class_names) 159 | N, C = len(self.documents), len(self.class_names) 160 | print('N = {} documents, C = {} classes'.format(N, C)) 161 | 162 | class TextRCV1(TextDataset): 163 | def __init__(self, **params): 164 | dataset = sklearn.datasets.fetch_rcv1(**params) 165 | self.data = dataset.data 166 | self.target = dataset.target 167 | self.class_names = dataset.target_names 168 | assert len(self.class_names) == 103 # 103 categories according to LYRL2004 169 | N, C = self.target.shape 170 | assert C == len(self.class_names) 171 | print('N = {} documents, C = {} classes'.format(N, C)) 172 | 173 | def remove_classes(self, keep): 174 | ## Construct a lookup table for labels. 175 | labels_row = [] 176 | labels_col = [] 177 | class_lookup = {} 178 | for i,name in enumerate(self.class_names): 179 | class_lookup[name] = i 180 | self.class_names = keep 181 | 182 | # Index of classes to keep. 183 | idx_keep = np.empty(len(keep)) 184 | for i,cat in enumerate(keep): 185 | idx_keep[i] = class_lookup[cat] 186 | self.target = self.target[:,idx_keep] 187 | assert self.target.shape[1] == len(keep) 188 | 189 | def show_doc_per_class(self, print_=False): 190 | """Number of documents per class.""" 191 | docs_per_class = np.array(self.target.astype(np.uint64).sum(axis=0)).squeeze() 192 | print('categories ({} assignments in total)'.format(docs_per_class.sum())) 193 | if print_: 194 | for i,cat in enumerate(self.class_names): 195 | print(' {:5s}: {:6d} documents'.format(cat, docs_per_class[i])) 196 | plt.figure(figsize=(17,5)) 197 | plt.plot(sorted(docs_per_class[::-1]),'.') 198 | 199 | def show_classes_per_doc(self): 200 | """Number of classes per document.""" 201 | classes_per_doc = np.array(self.target.sum(axis=1)).squeeze() 202 | plt.figure(figsize=(17,5)) 203 | plt.plot(sorted(classes_per_doc[::-1]),'.') 204 | 205 | def select_documents(self): 206 | classes_per_doc = np.array(self.target.sum(axis=1)).squeeze() 207 | self.target = self.target[classes_per_doc==1] 208 | self.data = self.data[classes_per_doc==1, :] 209 | 210 | # Convert labels from indicator form to single value. 211 | N, C = self.target.shape 212 | target = self.target.tocoo() 213 | self.labels = target.col 214 | assert self.labels.min() == 0 215 | assert self.labels.max() == C - 1 216 | 217 | # Bruna and Dropout used 2 * 201369 = 402738 documents. Probably the difference btw v1 and v2. 218 | #return classes_per_doc 219 | 220 | ### Helpers to quantify classifier's quality. 221 | 222 | 223 | def baseline(train_data, train_labels, test_data, test_labels, omit=[]): 224 | """Train various classifiers to get a baseline.""" 225 | clf, train_accuracy, test_accuracy, train_f1, test_f1, exec_time = [], [], [], [], [], [] 226 | clf.append(sklearn.neighbors.KNeighborsClassifier(n_neighbors=10)) 227 | clf.append(sklearn.linear_model.LogisticRegression()) 228 | clf.append(sklearn.naive_bayes.BernoulliNB(alpha=.01)) 229 | clf.append(sklearn.ensemble.RandomForestClassifier()) 230 | clf.append(sklearn.naive_bayes.MultinomialNB(alpha=.01)) 231 | clf.append(sklearn.linear_model.RidgeClassifier()) 232 | clf.append(sklearn.svm.LinearSVC()) 233 | for i,c in enumerate(clf): 234 | if i not in omit: 235 | t_start = time.process_time() 236 | c.fit(train_data, train_labels) 237 | train_pred = c.predict(train_data) 238 | test_pred = c.predict(test_data) 239 | train_accuracy.append('{:5.2f}'.format(100*sklearn.metrics.accuracy_score(train_labels, train_pred))) 240 | test_accuracy.append('{:5.2f}'.format(100*sklearn.metrics.accuracy_score(test_labels, test_pred))) 241 | train_f1.append('{:5.2f}'.format(100*sklearn.metrics.f1_score(train_labels, train_pred, average='weighted'))) 242 | test_f1.append('{:5.2f}'.format(100*sklearn.metrics.f1_score(test_labels, test_pred, average='weighted'))) 243 | exec_time.append('{:5.2f}'.format(time.process_time() - t_start)) 244 | print('Train accuracy: {}'.format(' '.join(train_accuracy))) 245 | print('Test accuracy: {}'.format(' '.join(test_accuracy))) 246 | print('Train F1 (weighted): {}'.format(' '.join(train_f1))) 247 | print('Test F1 (weighted): {}'.format(' '.join(test_f1))) 248 | print('Execution time: {}'.format(' '.join(exec_time))) 249 | 250 | def grid_search(params, grid_params, train_data, train_labels, val_data, 251 | val_labels, test_data, test_labels, model): 252 | """Explore the hyper-parameter space with an exhaustive grid search.""" 253 | params = params.copy() 254 | train_accuracy, test_accuracy, train_f1, test_f1 = [], [], [], [] 255 | grid = sklearn.grid_search.ParameterGrid(grid_params) 256 | print('grid search: {} combinations to evaluate'.format(len(grid))) 257 | for grid_params in grid: 258 | params.update(grid_params) 259 | name = '{}'.format(grid) 260 | print('\n\n {} \n\n'.format(grid_params)) 261 | m = model(params) 262 | m.fit(train_data, train_labels, val_data, val_labels) 263 | string, accuracy, f1, loss = m.evaluate(train_data, train_labels) 264 | train_accuracy.append('{:5.2f}'.format(accuracy)); train_f1.append('{:5.2f}'.format(f1)) 265 | print('train {}'.format(string)) 266 | string, accuracy, f1, loss = m.evaluate(test_data, test_labels) 267 | test_accuracy.append('{:5.2f}'.format(accuracy)); test_f1.append('{:5.2f}'.format(f1)) 268 | print('test {}'.format(string)) 269 | print('\n\n') 270 | print('Train accuracy: {}'.format(' '.join(train_accuracy))) 271 | print('Test accuracy: {}'.format(' '.join(test_accuracy))) 272 | print('Train F1 (weighted): {}'.format(' '.join(train_f1))) 273 | print('Test F1 (weighted): {}'.format(' '.join(test_f1))) 274 | for i,grid_params in enumerate(grid): 275 | print('{} --> {} {} {} {}'.format(grid_params, train_accuracy[i], test_accuracy[i], train_f1[i], test_f1[i])) 276 | 277 | 278 | class model_perf(object): 279 | 280 | def __init__(s): 281 | s.names, s.params = set(), {} 282 | s.fit_accuracies, s.fit_losses, s.fit_time = {}, {}, {} 283 | s.train_accuracy, s.train_f1, s.train_loss = {}, {}, {} 284 | s.test_accuracy, s.test_f1, s.test_loss = {}, {}, {} 285 | 286 | def test(s, model, name, params, train_data, train_labels, val_data, val_labels, test_data, test_labels, file_out): 287 | s.params[name] = params 288 | s.fit_accuracies[name], s.fit_losses[name], s.fit_time[name] = \ 289 | model.fit(train_data, train_labels, val_data, val_labels) 290 | string, s.train_accuracy[name], s.train_f1[name], s.train_loss[name] = \ 291 | model.evaluate(train_data, train_labels) 292 | print('train {}'.format(string), file=file_out) 293 | string, s.test_accuracy[name], s.test_f1[name], s.test_loss[name] = \ 294 | model.evaluate(test_data, test_labels) 295 | print('test {}'.format(string), file=file_out) 296 | s.names.add(name) 297 | 298 | def show(s, fontsize=None): 299 | if fontsize: 300 | plt.rc('pdf', fonttype=42) 301 | plt.rc('ps', fonttype=42) 302 | plt.rc('font', size=fontsize) # controls default text sizes 303 | plt.rc('axes', titlesize=fontsize) # fontsize of the axes title 304 | plt.rc('axes', labelsize=fontsize) # fontsize of the x any y labels 305 | plt.rc('xtick', labelsize=fontsize) # fontsize of the tick labels 306 | plt.rc('ytick', labelsize=fontsize) # fontsize of the tick labels 307 | plt.rc('legend', fontsize=fontsize) # legend fontsize 308 | plt.rc('figure', titlesize=fontsize) # size of the figure title 309 | print(' accuracy F1 loss time [ms] name') 310 | print('test train test train test train') 311 | for name in sorted(s.names): 312 | print('{:5.2f} {:5.2f} {:5.2f} {:5.2f} {:.2e} {:.2e} {:3.0f} {}'.format( 313 | s.test_accuracy[name], s.train_accuracy[name], 314 | s.test_f1[name], s.train_f1[name], 315 | s.test_loss[name], s.train_loss[name], s.fit_time[name]*1000, name)) 316 | 317 | fig, ax = plt.subplots(1, 2, figsize=(15, 5)) 318 | for name in sorted(s.names): 319 | steps = np.arange(len(s.fit_accuracies[name])) + 1 320 | steps *= s.params[name]['eval_frequency'] 321 | ax[0].plot(steps, s.fit_accuracies[name], '.-', label=name) 322 | ax[1].plot(steps, s.fit_losses[name], '.-', label=name) 323 | ax[0].set_xlim(min(steps), max(steps)) 324 | ax[1].set_xlim(min(steps), max(steps)) 325 | ax[0].set_xlabel('step') 326 | ax[1].set_xlabel('step') 327 | ax[0].set_ylabel('validation accuracy') 328 | ax[1].set_ylabel('training loss') 329 | ax[0].legend(loc='lower right') 330 | ax[1].legend(loc='upper right') 331 | #fig.savefig('training.pdf') 332 | -------------------------------------------------------------------------------- /GICOPix-SGCN/lib/models.py: -------------------------------------------------------------------------------- 1 | from . import graph 2 | 3 | import tensorflow as tf 4 | import sklearn 5 | import scipy.sparse 6 | import numpy as np 7 | import os, time, collections, shutil 8 | import pdb 9 | from lib import spherical_vertex 10 | 11 | #NFEATURES = 28**2 12 | #NCLASSES = 10 13 | CHANNELS = None # [None, 3, 6] 14 | 15 | # Common methods for all models 16 | 17 | 18 | class base_model(object): 19 | 20 | def __init__(self): 21 | self.regularizers = [] 22 | 23 | # High-level interface which runs the constructed computational graph. 24 | 25 | def predict(self, data, labels=None, sess=None): 26 | loss = 0 27 | size = data.shape[0] 28 | predictions = np.empty(size) 29 | #pdb.set_trace() 30 | sess = self._get_session(sess) 31 | for begin in range(0, size, self.batch_size): 32 | end = begin + self.batch_size 33 | end = min([end, size]) 34 | 35 | if CHANNELS is None: 36 | batch_data = np.zeros((self.batch_size, data.shape[1])) 37 | else: 38 | batch_data = np.zeros((self.batch_size, data.shape[1], CHANNELS)) 39 | tmp_data = data[begin:end,:] 40 | if type(tmp_data) is not np.ndarray: 41 | tmp_data = tmp_data.toarray() # convert sparse matrices 42 | batch_data[:end-begin] = tmp_data 43 | feed_dict = {self.ph_data: batch_data, self.ph_dropout: 1, self.ph_is_training:False} 44 | 45 | # Compute loss if labels are given. 46 | if labels is not None: 47 | batch_labels = np.zeros(self.batch_size) 48 | batch_labels[:end-begin] = labels[begin:end] 49 | feed_dict[self.ph_labels] = batch_labels 50 | batch_pred, batch_loss = sess.run([self.op_prediction, self.op_loss], feed_dict) 51 | loss += batch_loss 52 | else: 53 | batch_pred = sess.run(self.op_prediction, feed_dict) 54 | 55 | predictions[begin:end] = batch_pred[:end-begin] 56 | 57 | if labels is not None: 58 | return predictions, loss * self.batch_size / size 59 | else: 60 | return predictions 61 | 62 | def evaluate(self, data, labels, sess=None): 63 | """ 64 | Runs one evaluation against the full epoch of data. 65 | Return the precision and the number of correct predictions. 66 | Batch evaluation saves memory and enables this to run on smaller GPUs. 67 | 68 | sess: the session in which the model has been trained. 69 | op: the Tensor that returns the number of correct predictions. 70 | data: size N x M 71 | N: number of signals (samples) 72 | M: number of vertices (features) 73 | labels: size N 74 | N: number of signals (samples) 75 | """ 76 | t_process, t_wall = time.process_time(), time.time() 77 | predictions, loss = self.predict(data, labels, sess) 78 | #print(predictions) 79 | ncorrects = sum(predictions == labels) 80 | accuracy = 100 * sklearn.metrics.accuracy_score(labels, predictions) 81 | f1 = 100 * sklearn.metrics.f1_score(labels, predictions, average='weighted') 82 | string = 'accuracy: {:.2f} ({:d} / {:d}), f1 (weighted): {:.2f}, loss: {:.2e}'.format( 83 | accuracy, ncorrects, len(labels), f1, loss) 84 | if sess is None: 85 | string += '\ntime: {:.0f}s (wall {:.0f}s)'.format(time.process_time()-t_process, time.time()-t_wall) 86 | return string, accuracy, f1, loss 87 | 88 | def fit(self, train_data, train_labels, val_data, val_labels): 89 | t_process, t_wall = time.process_time(), time.time() 90 | tf_config = tf.ConfigProto() 91 | tf_config.gpu_options.allow_growth = True 92 | sess = tf.Session(config=tf_config,graph=self.graph) 93 | 94 | 95 | shutil.rmtree(self._get_path('summaries'), ignore_errors=True) 96 | writer = tf.summary.FileWriter(self._get_path('summaries'), self.graph) 97 | shutil.rmtree(self._get_path('checkpoints_5'), ignore_errors=True) 98 | os.makedirs(self._get_path('checkpoints_5')) 99 | path = os.path.join(self._get_path('checkpoints_5'), 'model') 100 | sess.run(self.op_init) 101 | #self.op_saver.restore(sess, "/home/yq/models/mine_of_cnn_graph_master/checkpoints_5/cifar10/cgconv_cgconv_softmax/model-9600") 102 | #print('load mat done') 103 | # Training. 104 | accuracies = [] 105 | losses = [] 106 | indices = collections.deque() 107 | num_steps = int(self.num_epochs * train_data.shape[0] / self.batch_size) 108 | print('model.fit(train_data, train_labels, val_data, val_labels)', file=self.file_out) 109 | for step in range(1,num_steps+1): 110 | # Be sure to have used all the samples before using one a second time. 111 | if len(indices) < self.batch_size: 112 | indices.extend(np.random.permutation(train_data.shape[0])) 113 | idx = [indices.popleft() for i in range(self.batch_size)] 114 | 115 | #pdb.set_trace() 116 | batch_data, batch_labels = train_data[idx,:], train_labels[idx] 117 | if type(batch_data) is not np.ndarray: 118 | batch_data = batch_data.toarray() # convert sparse matrices 119 | feed_dict = {self.ph_data: batch_data, self.ph_labels: batch_labels, self.ph_dropout: self.dropout, self.ph_is_training: True} 120 | learning_rate, loss_average = sess.run([self.op_train, self.op_loss_average], feed_dict) 121 | #pdb.set_trace() 122 | #print('step-', step, ':', loss_average) 123 | if step%100==0: 124 | print('step {} / {}: loss:{:0.3f}'.format(step, num_steps, loss_average), file=self.file_out) 125 | print('step {} / {}: loss:{:0.3f}'.format(step, num_steps, loss_average)) 126 | 127 | # Periodical evaluation of the model. 128 | epoch = step * self.batch_size / train_data.shape[0] 129 | if step%1000 == 0 or step == num_steps: 130 | 131 | print('step {} / {} (epoch {:.2f} / {}):'.format(step, num_steps, epoch, self.num_epochs), file=self.file_out) 132 | print(' learning_rate = {:.2e}, loss_average = {:.2e}'.format(learning_rate, loss_average), file=self.file_out) 133 | print('step {} / {} (epoch {:.2f} / {}):'.format(step, num_steps, epoch, self.num_epochs)) 134 | print(' learning_rate = {:.2e}, loss_average = {:.2e}'.format(learning_rate, loss_average)) 135 | string, accuracy, f1, loss = self.evaluate(val_data, val_labels, sess) 136 | accuracies.append(accuracy) 137 | losses.append(loss) 138 | print(' validation {}'.format(string), file=self.file_out) 139 | print(' validation {}'.format(string)) 140 | print(' time: {:.0f}s (wall {:.0f}s)'.format(time.process_time()-t_process, time.time()-t_wall), file=self.file_out) 141 | 142 | # Summaries for TensorBoard. 143 | summary = tf.Summary() 144 | summary.ParseFromString(sess.run(self.op_summary, feed_dict)) 145 | summary.value.add(tag='validation/accuracy', simple_value=accuracy) 146 | summary.value.add(tag='validation/f1', simple_value=f1) 147 | summary.value.add(tag='validation/loss', simple_value=loss) 148 | writer.add_summary(summary, step) 149 | 150 | # Save model parameters (for evaluation). 151 | self.op_saver.save(sess, path, global_step=step) 152 | 153 | print('validation accuracy: peak = {:.2f}, mean = {:.2f}'.format(max(accuracies), np.mean(accuracies[-10:])), file=self.file_out) 154 | print('validation accuracy: peak = {:.2f}, mean = {:.2f}'.format(max(accuracies), np.mean(accuracies[-10:]))) 155 | writer.close() 156 | sess.close() 157 | 158 | t_step = (time.time() - t_wall) / num_steps 159 | return accuracies, losses, t_step 160 | 161 | def get_var(self, name): 162 | sess = self._get_session() 163 | var = self.graph.get_tensor_by_name(name + ':0') 164 | val = sess.run(var) 165 | sess.close() 166 | return val 167 | 168 | # Methods to construct the computational graph. 169 | 170 | def build_graph(self, M_0): 171 | """Build the computational graph of the model.""" 172 | self.graph = tf.Graph() 173 | with self.graph.as_default(): 174 | 175 | # Inputs. 176 | with tf.name_scope('inputs'): 177 | 178 | if CHANNELS is None: 179 | self.ph_data = tf.placeholder(tf.float32, (self.batch_size, M_0), 'data') 180 | else: 181 | self.ph_data = tf.placeholder(tf.float32, (self.batch_size, M_0, CHANNELS), 'data') 182 | ''' 183 | ''' 184 | self.ph_labels = tf.placeholder(tf.int32, (self.batch_size), 'labels') 185 | self.ph_dropout = tf.placeholder(tf.float32, (), 'dropout') 186 | self.ph_is_training = tf.placeholder(tf.bool,(),'is_training' ) 187 | 188 | # Model. 189 | op_logits = self.inference(self.ph_data, self.ph_dropout, self.ph_is_training) 190 | self.op_loss, self.op_loss_average = self.loss(op_logits, self.ph_labels, self.regularization) 191 | 192 | self.op_train = self.training(self.op_loss, self.learning_rate, 193 | self.boundaries, self.momentum) 194 | self.op_prediction = self.prediction(op_logits) 195 | 196 | # Initialize variables, i.e. weights and biases. 197 | self.op_init = tf.global_variables_initializer() 198 | 199 | # Summaries for TensorBoard and Save for model parameters. 200 | self.op_summary = tf.summary.merge_all() 201 | self.op_saver = tf.train.Saver(max_to_keep=5) 202 | 203 | self.graph.finalize() 204 | 205 | def inference(self, data, dropout, is_training): 206 | """ 207 | It builds the model, i.e. the computational graph, as far as 208 | is required for running the network forward to make predictions, 209 | i.e. return logits given raw data. 210 | 211 | data: size N x M 212 | N: number of signals (samples) 213 | M: number of vertices (features) 214 | training: we may want to discriminate the two, e.g. for dropout. 215 | True: the model is built for training. 216 | False: the model is built for evaluation. 217 | """ 218 | # TODO: optimizations for sparse data 219 | logits = self._inference(data, dropout, is_training) 220 | return logits 221 | 222 | def probabilities(self, logits): 223 | """Return the probability of a sample to belong to each class.""" 224 | with tf.name_scope('probabilities'): 225 | probabilities = tf.nn.softmax(logits) 226 | return probabilities 227 | 228 | def prediction(self, logits): 229 | """Return the predicted classes.""" 230 | with tf.name_scope('prediction'): 231 | prediction = tf.argmax(logits, axis=1) 232 | return prediction 233 | 234 | def loss(self, logits, labels, regularization): 235 | """Adds to the inference model the layers required to generate loss.""" 236 | with tf.name_scope('loss'): 237 | with tf.name_scope('cross_entropy'): 238 | labels = tf.to_int64(labels) 239 | cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=labels) 240 | #pdb.set_trace() 241 | cross_entropy = tf.reduce_mean(cross_entropy) 242 | with tf.name_scope('regularization'): 243 | regularization *= tf.add_n(self.regularizers) 244 | loss = cross_entropy + regularization 245 | 246 | # Summaries for TensorBoard. 247 | tf.summary.scalar('loss/cross_entropy', cross_entropy) 248 | tf.summary.scalar('loss/regularization', regularization) 249 | tf.summary.scalar('loss/total', loss) 250 | with tf.name_scope('averages'): 251 | averages = tf.train.ExponentialMovingAverage(0.9) 252 | op_averages = averages.apply([cross_entropy, regularization, loss]) 253 | tf.summary.scalar('loss/avg/cross_entropy', averages.average(cross_entropy)) 254 | tf.summary.scalar('loss/avg/regularization', averages.average(regularization)) 255 | tf.summary.scalar('loss/avg/total', averages.average(loss)) 256 | with tf.control_dependencies([op_averages]): 257 | loss_average = tf.identity(averages.average(loss), name='control') 258 | return loss, loss_average 259 | 260 | def training(self, loss, learning_rate, boundaries, momentum=0.9): 261 | """Adds to the loss model the Ops required to generate and apply gradients.""" 262 | with tf.name_scope('training'): 263 | # Learning rate. 264 | global_step = tf.Variable(0, name='global_step', trainable=False) 265 | ''' 266 | if decay_rate != 1: 267 | learning_rate = tf.train.exponential_decay( 268 | learning_rate, global_step, decay_steps, decay_rate, staircase=True) 269 | ''' 270 | learning_rate = tf.train.piecewise_constant(global_step, boundaries=boundaries, values=learning_rate) 271 | tf.summary.scalar('learning_rate', learning_rate) 272 | # Optimizer. 273 | if momentum == 0: 274 | optimizer = tf.train.GradientDescentOptimizer(learning_rate) 275 | #optimizer = tf.train.AdamOptimizer(learning_rate=0.001) 276 | else: 277 | optimizer = tf.train.MomentumOptimizer(learning_rate, momentum) 278 | #yq 279 | extra_update_ops = tf.get_collection(tf.GraphKeys.UPDATE_OPS) 280 | with tf.control_dependencies(extra_update_ops): 281 | grads = optimizer.compute_gradients(loss) 282 | op_gradients = optimizer.apply_gradients(grads, global_step=global_step) 283 | # Histograms. 284 | for grad, var in grads: 285 | if grad is None: 286 | print('warning: {} has no gradient'.format(var.op.name)) 287 | else: 288 | tf.summary.histogram(var.op.name + '/gradients', grad) 289 | # The op return the learning rate. 290 | with tf.control_dependencies([op_gradients]): 291 | op_train = tf.identity(learning_rate, name='control') 292 | ''' 293 | grads = optimizer.compute_gradients(loss) 294 | op_gradients = optimizer.apply_gradients(grads, global_step=global_step) 295 | # Histograms. 296 | for grad, var in grads: 297 | if grad is None: 298 | print('warning: {} has no gradient'.format(var.op.name)) 299 | else: 300 | tf.summary.histogram(var.op.name + '/gradients', grad) 301 | # The op return the learning rate. 302 | with tf.control_dependencies([op_gradients]): 303 | op_train = tf.identity(learning_rate, name='control') 304 | ''' 305 | return op_train 306 | 307 | # Helper methods. 308 | 309 | def _get_path(self, folder): 310 | path = os.path.dirname(os.path.realpath(__file__)) 311 | return os.path.join(path, '..', folder, self.dir_name) 312 | 313 | def _get_session(self, sess=None): 314 | """Restore parameters if no session given.""" 315 | if sess is None: 316 | tf_config = tf.ConfigProto() 317 | tf_config.gpu_options.allow_growth = True 318 | sess = tf.Session(config = tf_config, graph=self.graph) 319 | filename = tf.train.latest_checkpoint(self._get_path('checkpoints_5')) 320 | print('restore from %s'%filename, file=self.file_out) 321 | self.op_saver.restore(sess, filename) 322 | return sess 323 | 324 | def _weight_variable(self, shape, regularization=True): 325 | initial = tf.truncated_normal_initializer(0, 0.1) 326 | var = tf.get_variable('weights', shape, tf.float32, initializer=initial) 327 | if regularization: 328 | self.regularizers.append(tf.nn.l2_loss(var)) 329 | tf.summary.histogram(var.op.name, var) 330 | return var 331 | 332 | def _bias_variable(self, shape, regularization=True): 333 | initial = tf.constant_initializer(0.1) 334 | var = tf.get_variable('bias', shape, tf.float32, initializer=initial) 335 | if regularization: 336 | self.regularizers.append(tf.nn.l2_loss(var)) 337 | tf.summary.histogram(var.op.name, var) 338 | return var 339 | 340 | 341 | # Graph convolutional 342 | 343 | class cgcnn(base_model): 344 | """ 345 | Graph CNN which uses the Chebyshev approximation. 346 | 347 | The following are hyper-parameters of graph convolutional layers. 348 | They are lists, which length is equal to the number of gconv layers. 349 | F: Number of features. 350 | K: List of polynomial orders, i.e. filter sizes or number of hopes. 351 | p: Pooling size. 352 | Should be 1 (no pooling) or a power of 2 (reduction by 2 at each coarser level). 353 | Beware to have coarsened enough. 354 | 355 | L: List of Graph Laplacians. Size M x M. One per coarsening level. 356 | 357 | The following are hyper-parameters of fully connected layers. 358 | They are lists, which length is equal to the number of fc layers. 359 | M: Number of features per sample, i.e. number of hidden neurons. 360 | The last layer is the softmax, i.e. M[-1] is the number of classes. 361 | 362 | The following are choices of implementation for various blocks. 363 | filter: filtering operation, e.g. chebyshev5, lanczos2 etc. 364 | brelu: bias and relu, e.g. b1relu or b2relu. 365 | pool: pooling, e.g. mpool1. 366 | 367 | Training parameters: 368 | num_epochs: Number of training epochs. 369 | learning_rate: Initial learning rate. 370 | decay_rate: Base of exponential decay. No decay with 1. 371 | decay_steps: Number of steps after which the learning rate decays. 372 | momentum: Momentum. 0 indicates no momentum. 373 | 374 | Regularization parameters: 375 | regularization: L2 regularizations of weights and biases. 376 | dropout: Dropout (fc layers): probability to keep hidden neurons. No dropout with 1. 377 | batch_size: Batch size. Must divide evenly into the dataset sizes. 378 | eval_frequency: Number of steps between evaluations. 379 | 380 | Directories: 381 | dir_name: Name for directories (summaries and model parameters). 382 | """ 383 | def __init__(self, L, graphs, index, points,file_out, F, K, p, M, filter='chebyshev5', brelu='b1relu', pool='mpool1', 384 | num_epochs=20, learning_rate=[0.1,0.1], boundaries=[40], momentum=0.9, 385 | regularization=0, dropout=0,is_training=True, batch_size=100, eval_frequency=200, 386 | dir_name=''): 387 | super().__init__() 388 | self.file_out = file_out 389 | self.dir_name = dir_name 390 | self.num_epochs, self.learning_rate = num_epochs, learning_rate 391 | self.boundaries, self.momentum = boundaries, momentum 392 | self.regularization, self.dropout, self.is_training = regularization, dropout, is_training 393 | self.batch_size, self.eval_frequency = batch_size, eval_frequency 394 | 395 | # Verify the consistency w.r.t. the number of layers. 396 | assert len(L) >= len(F) == len(K) == len(p) 397 | assert np.all(np.array(p) >= 1) 398 | p_log2 = np.where(np.array(p) > 1, np.log2(p), 0) 399 | assert np.all(np.mod(p_log2, 1) == 0) # Powers of 2. 400 | print('len(L)', len(L)) 401 | #assert len(L) >= 1 + np.sum(p_log2) # Enough coarsening levels for pool sizes. 402 | 403 | # Keep the useful Laplacians only. May be zero. 404 | M_0 = L[0].shape[0] 405 | 406 | 407 | # Store attributes and bind operations. 408 | self.L, self.F, self.K, self.p, self.M = L, F, K, p, M 409 | self.graphs = graphs 410 | self.index = index 411 | self.points = points 412 | self.filter = getattr(self, filter) 413 | self.brelu = getattr(self, brelu) 414 | self.pool = getattr(self, pool) 415 | 416 | # Build the computational graph. 417 | self.build_graph(M_0) 418 | 419 | def chebyshev5(self, x, L, Fout, K): 420 | x = tf.cast(x, tf.float64) 421 | N, M, Fin = x.get_shape() 422 | N, M, Fin = int(N), int(M), int(Fin) 423 | # Rescale Laplacian and store as a TF sparse tensor. Copy to not modify the shared L. 424 | L = scipy.sparse.csr_matrix(L) 425 | L = graph.rescale_L(L, lmax=2) 426 | L = L.tocoo() 427 | indices = np.column_stack((L.row, L.col)) 428 | L = tf.SparseTensor(indices, L.data, L.shape) 429 | L = tf.sparse_reorder(L) 430 | # Transform to Chebyshev basis 431 | x0 = tf.transpose(x, perm=[1, 2, 0]) # M x Fin x N 432 | x0 = tf.reshape(x0, [M, Fin*N]) # M x Fin*N 433 | x = tf.expand_dims(x0, 0) # 1 x M x Fin*N 434 | def concat(x, x_): 435 | x_ = tf.expand_dims(x_, 0) # 1 x M x Fin*N 436 | return tf.concat([x, x_], axis=0) # K x M x Fin*N 437 | if K > 1: 438 | x1 = tf.sparse_tensor_dense_matmul(L, x0) 439 | x = concat(x, x1) 440 | for k in range(2, K): 441 | x2 = 2 * tf.sparse_tensor_dense_matmul(L, x1) - x0 # M x Fin*N 442 | x = concat(x, x2) 443 | x0, x1 = x1, x2 444 | x = tf.reshape(x, [K, M, Fin, N]) # K x M x Fin x N 445 | x = tf.transpose(x, perm=[3,1,2,0]) # N x M x Fin x K 446 | x = tf.reshape(x, [N*M, Fin*K]) # N*M x Fin*K 447 | # Filter: Fin*Fout filters of order K, i.e. one filterbank per feature pair. 448 | 449 | x = tf.cast(x, tf.float32) 450 | W = self._weight_variable([Fin*K, Fout], regularization=False) 451 | x = tf.matmul(x, W) # N*M x Fout 452 | return tf.reshape(x, [N, M, Fout]) # N x M x Fout 453 | 454 | # yq logist layer 455 | def logist_layer_1(self, x, L, K): 456 | x = tf.cast(x, tf.float64) 457 | N, M, Fin = x.get_shape() 458 | N, M, Fin = int(N), int(M), int(Fin) 459 | # Rescale Laplacian and store as a TF sparse tensor. Copy to not modify the shared L. 460 | L = scipy.sparse.csr_matrix(L) 461 | L = graph.rescale_L(L, lmax=2) 462 | L = L.tocoo() 463 | indices = np.column_stack((L.row, L.col)) 464 | L = tf.SparseTensor(indices, L.data, L.shape) 465 | L = tf.sparse_reorder(L) 466 | # Transform to Chebyshev basis 467 | x0 = tf.transpose(x, perm=[1, 2, 0]) # M x Fin x N 468 | x0 = tf.reshape(x0, [M, Fin*N]) # M x Fin*N 469 | x = tf.expand_dims(x0, 0) # 1 x M x Fin*N 470 | def concat(x, x_): 471 | x_ = tf.expand_dims(x_, 0) # 1 x M x Fin*N 472 | return tf.concat([x, x_], axis=0) # K x M x Fin*N 473 | if K > 1: 474 | x1 = tf.sparse_tensor_dense_matmul(L, x0) 475 | x = concat(x, x1) 476 | for k in range(2, K): 477 | x2 = 2 * tf.sparse_tensor_dense_matmul(L, x1) - x0 # M x Fin*N 478 | x = concat(x, x2) 479 | x0, x1 = x1, x2 480 | x = tf.reshape(x, [K, M, Fin, N]) # K x M x Fin x N 481 | x = tf.transpose(x, perm=[3,2,0,1]) # N x Fin x K x M 482 | 483 | # Filter: Fin*Fout filters of order K, i.e. one filterbank per feature pair. 484 | x_mean,x_variance = tf.nn.moments(x,[3]) 485 | x = tf.concat([x_mean,x_variance],axis=2) 486 | x = tf.cast(x, tf.float32) 487 | #W = self._weight_variable([Fin*K, Fout], regularization=False) 488 | #x = tf.matmul(x, W) # N*M x Fout 489 | return tf.reshape(x, [N, K*2, Fin]) # N x M x Fout 490 | 491 | def b1relu(self, x): 492 | """Bias and ReLU. One bias per filter.""" 493 | N, M, F = x.get_shape() 494 | b = self._bias_variable([1, 1, int(F)], regularization=False) 495 | return tf.nn.relu(x + b) 496 | 497 | def multipool(self,x,i): 498 | """retain the vertex directly through downsample""" 499 | x = tf.cast(x, tf.float64) 500 | N, M, Fin = x.get_shape() 501 | index = tf.convert_to_tensor(self.index[i]) 502 | xnew = tf.gather(x,index,axis=1) 503 | print('averagepool{0}:the shape of xnew is{1}'.format(i,xnew.shape)) 504 | 505 | return xnew # N x M x Fin 506 | 507 | def fc(self, x, Mout, relu=True): 508 | """Fully connected layer with Mout features.""" 509 | x = tf.cast(x, tf.float32) 510 | N, Min = x.get_shape() 511 | W = self._weight_variable([int(Min), Mout], regularization=True) 512 | b = self._bias_variable([Mout], regularization=True) 513 | x = tf.matmul(x, W) + b 514 | return tf.nn.relu(x) if relu else x 515 | 516 | 517 | def _inference(self, x, dropout, is_training): 518 | # Graph convolutional layers. 519 | self.features_visual = {} 520 | if len(x.shape)==2: 521 | x = tf.expand_dims(x, 2) # N x M x (F=1) 522 | for i in range(len(self.p)): 523 | with tf.variable_scope('conv{}'.format(i+1)): 524 | with tf.name_scope('filter'): 525 | x = self.filter(x, self.L[i], self.F[i], self.K[i]) 526 | if i==0: 527 | self.features_visual['gc1']=x #for equivariance error 528 | x = tf.layers.batch_normalization(x,training=is_training) 529 | with tf.name_scope('bias_relu'): 530 | x = self.brelu(x) 531 | x = tf.nn.dropout(x, dropout) 532 | with tf.name_scope('pooling'): 533 | x = self.pool(x, i) 534 | #logist_layer 535 | x = self.logist_layer_1(x,self.L[-1],5) 536 | self.features_visual['logist']=x #for invariance error 537 | 538 | # Fully connected hidden layers. 539 | N, M, F = x.get_shape() 540 | x = tf.reshape(x, [int(N), int(M*F)]) # N x M 541 | 542 | ''' 543 | for i,M in enumerate(self.M[:-1]): 544 | with tf.variable_scope('fc{}'.format(i+1)): 545 | x = self.fc(x, M) 546 | x = tf.nn.dropout(x, dropout) 547 | ''' 548 | # Logits linear layer, i.e. softmax without normalization. 549 | with tf.variable_scope('logits'): 550 | x = self.fc(x, self.M[-1], relu=False) 551 | return x 552 | --------------------------------------------------------------------------------