def test_edge_convolution_template_exception_raised_shapes(self): """Check that invalid input shapes trigger the right exceptions.""" with self.assertRaisesRegexp(ValueError, "must have a rank of 2"): data, neighbors = _dummy_data(1, 5, 2) data = data[0, :] _ = gc.edge_convolution_template( data=data, neighbors=neighbors, sizes=None, edge_function=self._zeros, reduction="weighted", edge_function_kwargs=dict()) with self.assertRaisesRegexp(ValueError, "must have a rank greater than 1"): data = np.ones(shape=(5), dtype=np.float32) neighbors = _dense_to_sparse(np.ones(shape=(5), dtype=np.float32)) _ = gc.edge_convolution_template( data=data, neighbors=neighbors, sizes=None, edge_function=self._zeros, reduction="weighted", edge_function_kwargs=dict()) with self.assertRaisesRegexp(ValueError, "must have a rank of 1"): data, neighbors = _dummy_data(1, 5, 2) _ = gc.edge_convolution_template( data=data, neighbors=neighbors, sizes=((1, 1), (1, 1)), edge_function=self._zeros, reduction="weighted", edge_function_kwargs=dict())
def test_edge_convolution_template_preset_max(self): data = np.array(((1, 2), (3, 4), (5, 6), (7, 8)), np.float32) neighbors = np.array( ((0, 1, 0, 1), (0, 0, 1, 0), (1, 1, 1, 0), (0, 0, 1, 1)), np.float32) neighbors = _dense_to_sparse(neighbors) true = np.array(((8, 10), (8, 10), (10, 12), (14, 16)), np.float32) with self.subTest("max_sum"): max_sum = gc.edge_convolution_template( data=data, neighbors=neighbors, sizes=None, edge_function=lambda x, y: x + y, reduction="max", edge_function_kwargs=dict()) self.assertAllEqual(max_sum, true) with self.subTest("max_sum_scaled"): # Max reduction ignores the weights, so scaling the neighbors weights # should not change the result. max_sum_scaled = gc.edge_convolution_template( data=data, neighbors=neighbors * 10.0, sizes=None, edge_function=lambda x, y: x + y, reduction="max", edge_function_kwargs=dict()) self.assertAllEqual(max_sum_scaled, true)
def test_edge_convolution_template_exception_raised_reduction( self, reduction): """Check that an invalid reduction method triggers the exception.""" with self.assertRaisesRegexp(ValueError, "reduction method"): data, neighbors = _dummy_data(1, 5, 2) gc.edge_convolution_template(data=data, neighbors=neighbors, sizes=None, edge_function=self._zeros, reduction=reduction, edge_function_kwargs=dict())
def test_edge_convolution_template_exception_raised_types( self, err_msg, data_type, neighbors_type, sizes_type): """Check the type errors for invalid input types.""" data, neighbors, sizes = _random_data(1, 5, 3, True, False, data_type, neighbors_type, sizes_type) with self.assertRaisesRegexp(TypeError, err_msg): gc.edge_convolution_template(data=data, neighbors=neighbors, sizes=sizes, edge_function=self._zeros, edge_function_kwargs=dict())
def test_edge_convolution_template_exception_not_raised_types( self, data_type, neighbors_type, sizes_type): """Check there are no exceptions for valid input types.""" data, neighbors, sizes = _random_data(1, 5, 3, True, False, data_type, neighbors_type, sizes_type) try: gc.edge_convolution_template(data=data, neighbors=neighbors, sizes=sizes, edge_function=self._zeros, edge_function_kwargs=dict()) except Exception as e: # pylint: disable=broad-except self.fail("Exception raised: %s" % str(e))
def test_edge_convolution_template_curvature(self): r"""Test the expected result with curvature. (Approximate) curvature for meshes is defined as $$\kappa_{v_i} = \frac{1}{|\mathcal{N}(v_i)|} \sum_{v_j \in \mathcal{N}(v_i)} \frac{(\vec{v_i} - \vec{v_j})^T (\vec{n_{v_i}} - \vec{n_{v_j}})} {\left|\vec{v_i}-\vec{v_j}\right|^2} $$ This can be computed using `edge_convolution_template` with $$f(x, y) = (n_x - n_y)^T (x - y) / ||x - y||^2.$$ where $$n_x$$ and $$n_y$$ are the normals at points $$x$$ and $$y$$ respectively. """ # We can reuse `self._edge_curvature_2d` as the curvature functional. num_vertices = 500 data, neighbors = self._circular_2d_data(num_vertices, include_normals=True) data_curvature = gc.edge_convolution_template( data=data, neighbors=neighbors, sizes=None, edge_function=self._edge_curvature_2d, reduction="weighted", edge_function_kwargs=dict()) # The curvature at each point on a circle of radius 1 should be 1. self.assertAllClose(data_curvature, np.ones(shape=(num_vertices, 1)))
def test_edge_convolution_template_jacobian_random(self, batch_size, num_vertices, in_channels, padding, reduction): """Test the jacobian for random input data.""" random_data = _random_data( batch_size, num_vertices, in_channels, padding, only_self_edges=False, data_type=np.float64, neighbors_type=np.float64) data_init = random_data[0] neighbors = random_data[1] sizes = None if not padding else random_data[2] data = tf.convert_to_tensor(value=data_init) y = gc.edge_convolution_template( data=data, neighbors=neighbors, sizes=sizes, edge_function=self._pass_through, reduction=reduction, edge_function_kwargs=dict()) self.assert_jacobian_is_correct(data, data_init, y)
def edge_convolution_template(data): return gc.edge_convolution_template( data=data, neighbors=neighbors, sizes=None, edge_function=self._pass_through, reduction=reduction, edge_function_kwargs=dict())
def test_edge_convolution_template_laplacian_smoothing(self): r"""Test the expected result with laplacian smoothing. Laplacian smoothing for meshes is defined as $$y_i = \frac{1}{|\mathcal{N(i)}|} \sum_{j \in \mathcal{N(i)}} x_j$$ This can be computed using `edge_convolution_template` with `f(x, y)->y`. """ # We can reuse `self._pass_through(x, y)->y` as the smoothing functional. with self.subTest(name="only_self_edges_random"): num_vertices = 500 data = np.random.uniform(size=(num_vertices, 5)) neighbors = tf.sparse.eye(num_vertices, dtype=tf.as_dtype(data.dtype)) data_smoothed = gc.edge_convolution_template( data=data, neighbors=neighbors, sizes=None, edge_function=self._pass_through, reduction="weighted", edge_function_kwargs=dict()) self.assertAllEqual(data, data_smoothed) with self.subTest(name="circular_2d"): num_vertices = 500 data, neighbors = self._circular_2d_data(num_vertices) data_smoothed = gc.edge_convolution_template( data=data, neighbors=neighbors, sizes=None, edge_function=self._pass_through, reduction="weighted", edge_function_kwargs=dict()) # The smoothed points should have the same direction as the originals. data_smoothed_normalized = tf.nn.l2_normalize(data_smoothed, axis=-1) self.assertAllClose(data, data_smoothed_normalized)
def test_edge_convolution_template_zero_neighbors(self): """Check that vertices with no neighbors map to zeros in the output.""" # We can reuse `self._edge_curvature_2d` as the curvature functional. num_vertices = 500 data, neighbors = self._circular_2d_data(num_vertices, include_normals=True) # Interleave the data with rows filled with random data, these rows will # have no neighbors in the adjacency matrix so should map to all zeros in # the output. rows_odd = tf.expand_dims( tf.range(start=1, limit=(2 * num_vertices), delta=2), -1) rows_even = tf.expand_dims( tf.range(start=0, limit=(2 * num_vertices + 1), delta=2), -1) data_interleaved = tf.scatter_nd(indices=rows_odd, updates=data, shape=(2 * num_vertices + 1, tf.shape(input=data)[-1])) random_data = tf.random.uniform(shape=(data.shape[0] + 1, data.shape[-1]), dtype=data.dtype) random_interleaved = tf.scatter_nd(indices=rows_even, updates=random_data, shape=(2 * num_vertices + 1, tf.shape(input=data)[-1])) data_interleaved = data_interleaved + random_interleaved neighbors_interleaved_indices = neighbors.indices * 2 + 1 neighbors_interleaved = tf.SparseTensor( indices=neighbors_interleaved_indices, values=neighbors.values, dense_shape=(2 * num_vertices + 1, 2 * num_vertices + 1)) # Convolve the interleaved data. data_curvature = gc.edge_convolution_template( data=data_interleaved, neighbors=neighbors_interleaved, sizes=None, edge_function=self._edge_curvature_2d, reduction="weighted", edge_function_kwargs=dict()) self.assertEqual(data_curvature.shape, (2 * num_vertices + 1, 1)) # The rows corresponding to the original input data measure the curvature. # The curvature at any point on a circle of radius 1 should be 1. # The interleaved rows of random data should map to zeros in the output. self.assertAllClose(data_curvature[1::2, :], np.ones(shape=(num_vertices, 1))) self.assertAllClose(data_curvature[::2, :], np.zeros(shape=(num_vertices + 1, 1)))
def test_edge_convolution_template_jacobian_preset(self, num_vertices, num_channels, data_multiplier): """Test the jacobian is correct for preset inputs.""" # Corner cases include one vertex, one channel, and all-zero features. data_init = data_multiplier * np.random.uniform( size=(num_vertices, num_channels)).astype(np.float64) neighbors = tf.sparse.eye(num_vertices, dtype=tf.float64) data = tf.convert_to_tensor(value=data_init) y = gc.edge_convolution_template(data=data, neighbors=neighbors, sizes=None, edge_function=self._pass_through, edge_function_kwargs=dict()) self.assert_jacobian_is_correct(data, data_init, y)
def test_edge_convolution_template_output_shape(self, batch_size, num_vertices, in_channels, out_channels): """Check that the output of convolution has the correct shape.""" data, neighbors = _dummy_data(batch_size, num_vertices, in_channels) y = gc.edge_convolution_template( data, neighbors, None, self._zeros, edge_function_kwargs={"out_dimensions": out_channels}) y_shape = y.shape.as_list() with self.subTest(name="out_channels"): self.assertEqual(y_shape[-1], out_channels) with self.subTest(name="shape"): self.assertAllEqual(y_shape[:-1], data.shape[:-1])
def call(self, inputs, sizes=None): # pyformat: disable """Executes the convolution. The shorthands used below are `V`: The number of vertices. `C`: The number of channels in the input data. Note: In the following, A1 to An are optional batch dimensions. Args: inputs: A list of two tensors `[data, neighbors]`. `data` is a `float` tensor with shape `[A1, ..., An, V, C]`. `neighbors` is a `SparseTensor` with the same type as `data` and with shape `[A1, ..., An, V, V]` representing vertex neighborhoods. The neighborhood of a vertex defines the support region for convolution. For a mesh, a common choice for the neighborhood of vertex `i` would be the vertices in the K-ring of `i` (including `i` itself). Each vertex must have at least one neighbor. For `reduction='weighted'`, `neighbors` should be a row-normalized matrix: `sum(neighbors, axis=-1)[A1, ..., An, i] == 1.0` for all `i`, although this is not enforced in the implementation in case different neighbor weighting schemes are desired. sizes: An `int` tensor of shape `[A1, ..., An]` indicating the true input sizes in case of padding (`sizes=None` indicates no padding). `sizes[A1, ..., An] <= V`. If `data` and `neighbors` are 2-D, `sizes` will be ignored. As an example usage of `sizes`, consider an input consisting of three graphs `G0`, `G1`, and `G2` with `V0`, `V1`, and `V2` vertices respectively. The padded input would have the shapes `data.shape = [3, V, C]`, and `neighbors.shape = [3, V, V]`, where `V = max([V0, V1, V2])`. The true sizes of each graph will be specified by `sizes=[V0, V1, V2]`. `data[i, :Vi, :]` and `neighbors[i, :Vi, :Vi]` will be the vertex and neighborhood data of graph `Gi`. The `SparseTensor` `neighbors` should have no nonzero entries in the padded regions. Returns: Tensor with shape `[A1, ..., An, V, num_output_channels]`. """ # pyformat: enable def _edge_convolution(vertices, neighbors, conv1d_layer): r"""The edge filtering op passed to `edge_convolution_template`. This instance implements the edge function $$h_{\theta}(x, y) = MLP_{\theta}([x, y - x])$$ Args: vertices: A 2-D Tensor with shape `[D1, D2]`. neighbors: A 2-D Tensor with the same shape and type as `vertices`. conv1d_layer: A callable 1d convolution layer. Returns: A 2-D Tensor with shape `[D1, D3]`. """ concat_features = tf.concat( values=[vertices, neighbors - vertices], axis=-1) concat_features = tf.expand_dims(concat_features, 0) convolved_features = conv1d_layer(concat_features) convolved_features = tf.squeeze(input=convolved_features, axis=(0,)) return convolved_features kwargs = { 'conv1d_layer': self._conv1d_layer } return gc.edge_convolution_template( data=inputs[0], neighbors=inputs[1], sizes=sizes, edge_function=_edge_convolution, reduction=self._reduction, edge_function_kwargs=kwargs)