-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathlayers.py
140 lines (115 loc) · 5.7 KB
/
layers.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import tensorflow as tf
# noise_shape is [num_nonzero_elements], namely it is a list containing the number of elements of the sparse tensor
# keep_prob is 1-dropout_rate
# inputs is the sparse tensor which we are applying dropout to
def dropout_sparse(inputs, keep_prob, noise_shape):
keep_tensor = keep_prob + tf.random.uniform(noise_shape)
to_retain = tf.cast(tf.floor(keep_tensor), dtype=tf.bool)
out = tf.sparse.retain(inputs, to_retain=to_retain)
# the elements of the tensor are rescaled after dropout
return out * (1/keep_prob)
class GraphSparseConvolution(tf.keras.layers.Layer):
# the graph takes as input the adjacency matrix in the form of a sparse tensor
# it is important that the type of the elements of the tensor are explicitly converted to floats
def __init__(self, adj_norm, output_size=32, dropout_rate=0.0, act=tf.nn.relu):
super(GraphSparseConvolution, self).__init__()
self.adj_norm = adj_norm
self.output_size = output_size
self.dropout_rate = dropout_rate
self.act = act
# input_shape here will be automatically set as the shape of the input tensor, that will be the feature matrix
def build(self, input_shape):
init = tf.keras.initializers.GlorotNormal(seed=None)
self.kernel = self.add_weight('kernel', initializer=init, shape=[int(input_shape[-1]),self.output_size])
# the input is a sparse tensor whose elements have been explicitly converted to floats
def call(self, inputs):
x = inputs
x = dropout_sparse(inputs, 1-self.dropout_rate, [len(inputs.values)])
x = tf.sparse.sparse_dense_matmul(x, self.kernel)
x = tf.sparse.sparse_dense_matmul(self.adj_norm, x)
outputs = self.act(x)
return outputs
# the only difference between these two classes is that the first will treat also the features as a sparse matrix (the adjacency matrix will always be sparse)
class GraphConvolution(tf.keras.layers.Layer):
def __init__(self, adj_norm, output_size=16, dropout_rate=0.0, act=tf.nn.softmax):
super(GraphConvolution, self).__init__()
self.adj_norm = adj_norm
self.output_size = output_size
self.dropout_rate = dropout_rate
self.act= act
def build(self, input_shape):
init = tf.keras.initializers.GlorotNormal(seed=None)
self.kernel = self.add_weight('kernel', initializer=init, shape=[int(input_shape[-1]),self.output_size])
# the input to the call function is a dense tensor whose elements have been explicitly converted to floats
def call(self, inputs):
x = inputs
x = tf.keras.layers.Dropout(rate=self.dropout_rate)(inputs)
x = tf.matmul(x, self.kernel)
x = tf.sparse.sparse_dense_matmul(self.adj_norm, x)
outputs = self.act(x)
return outputs
class MRFLayer(tf.keras.layers.Layer):
# beta is the tradeoff parameter that balances topology and attributes
# Y \in R^{n,n}:
# Y_{i,j} = beta \frac{d_i d_j}{2e} + (1-beta)\frac{cos_sim(att(v_i), att(v_j))}{\sum_k cos_sim(att(v_i), att(v_k))}
# K is the number of clusters
def __init__(self, Y, K, dropout_rate=0, act=tf.nn.softmax):
super(MRFLayer, self).__init__()
self.Y = Y
self.K = K
self.dropout_rate = dropout_rate
self.act = act
def build(self, input_shape):
init = tf.keras.initializers.GlorotNormal(seed=None)
self.MRFLayer_weights = self.add_weight('MRFLayer_weights', initializer=init, shape=[self.K, self.K])
# returns act(inputs - Y*inputs*MRFLayer_weights)
def call(self, inputs):
inputs = tf.keras.layers.Dropout(rate=self.dropout_rate)(inputs)
x = inputs
x = tf.matmul(x, self.MRFLayer_weights)
x = tf.matmul(self.Y, x)
x = tf.subtract(inputs, x)
return self.act(x)
class TopologyDecoder(tf.keras.layers.Layer):
# K number of clusters
# D diagonal matrix with values deg1,..., degn
def __init__(self, D, K, act=tf.math.sigmoid):
super(TopologyDecoder, self).__init__()
self.K = K
self.D = D
self.act = act
def build(self, input_shape):
init = tf.keras.initializers.GlorotNormal(seed=None)
self.TopologyDecoder_weights = self.add_weight('TopologyDecoder_weights', initializer=init, shape=[self.K, self.K])
# returns (D*Z*TopologyDecoder_weights*Z^T*D^T)
# the transpose of D makes no sense since it is diagonal, but the paper does this
def call(self, inputs):
x = inputs
x = tf.matmul(self.D, x)
x = tf.matmul(x, self.TopologyDecoder_weights)
x = tf.matmul(x, inputs, transpose_b = True)
x = tf.matmul(x, self.D, transpose_b = True)
return self.act(x)
# constraint used in the attribute decoder
# sum of the cols of the weight matrix = 1
class SumToOne(tf.keras.constraints.Constraint):
def __call__(self, w):
col_sums = tf.reduce_sum(w, axis=0)
return tf.transpose(tf.divide(tf.transpose(w), tf.reshape(col_sums, (-1, 1))))
def get_config(self):
return {}
class AttributesDecoder(tf.keras.layers.Layer):
# K number of clusters
# m is the number of features
def __init__(self, m, K, act=tf.nn.sigmoid):
super(AttributesDecoder, self).__init__()
self.m = m
self.K = K
self.act = act
def build(self, input_shape):
init = tf.keras.initializers.GlorotNormal(seed=None)
self.AttributesDecoder_weights = self.add_weight('AttributesDecoder_weights', constraint=SumToOne(), initializer=init, shape=[self.K, self.m])
# returns (Z*AttributesDecoder_weights)
def call(self, inputs):
x = tf.matmul(inputs, self.AttributesDecoder_weights)
return self.act(x)