text: from tensorflow.keras import backend as K
  
  from spektral.layers import ops
  from spektral.layers.convolutional.message_passing import MessagePassing
  
  
  class GraphSageConv(MessagePassing):
      r"""
      A GraphSAGE layer from the paper
  
      > [Inductive Representation Learning on Large Graphs](https://arxiv.org/abs/1706.02216)<br>
      > William L. Hamilton et al.
  
      **Mode**: single, disjoint.
  
      **This layer expects a sparse adjacency matrix.**
  
      This layer computes:
      $$
          \X' = \big[ \textrm{AGGREGATE}(\X) \| \X \big] \W + \b; \\
          \X' = \frac{\X'}{\|\X'\|}
      $$
      where \( \textrm{AGGREGATE} \) is a function to aggregate a node's
      neighbourhood. The supported aggregation methods are: sum, mean,
      max, min, and product.
  
      **Input**
  
      - Node features of shape `(n_nodes, n_node_features)`;
      - Binary adjacency matrix of shape `(n_nodes, n_nodes)`.
  
      **Output**
  
      - Node features with the same shape as the input, but with the last
      dimension changed to `channels`.
  
      **Arguments**
  
      - `channels`: number of output channels;
      - `aggregate_op`: str, aggregation method to use (`'sum'`, `'mean'`,
      `'max'`, `'min'`, `'prod'`);
      - `activation`: activation function;
      - `use_bias`: bool, add a bias vector to the output;
      - `kernel_initializer`: initializer for the weights;
      - `bias_initializer`: initializer for the bias vector;
      - `kernel_regularizer`: regularization applied to the weights;
      - `bias_regularizer`: regularization applied to the bias vector;
      - `activity_regularizer`: regularization applied to the output;
      - `kernel_constraint`: constraint applied to the weights;
      - `bias_constraint`: constraint applied to the bias vector.
  
      """
  
      def __init__(self,
                   channels,
                   aggregate='mean',
                   activation=None,
                   use_bias=True,
                   kernel_initializer='glorot_uniform',
                   bias_initializer='zeros',
                   kernel_regularizer=None,
                   bias_regularizer=None,
                   activity_regularizer=None,
                   kernel_constraint=None,
                   bias_constraint=None,
                   **kwargs):
          super().__init__(aggregate=aggregate,
                           activation=activation,
                           use_bias=use_bias,
                           kernel_initializer=kernel_initializer,
                           bias_initializer=bias_initializer,
                           kernel_regularizer=kernel_regularizer,
                           bias_regularizer=bias_regularizer,
                           activity_regularizer=activity_regularizer,
                           kernel_constraint=kernel_constraint,
                           bias_constraint=bias_constraint,
                           **kwargs)
          self.channels = channels
  
      def build(self, input_shape):
          assert len(input_shape) >= 2
          input_dim = input_shape[0][-1]
          self.kernel = self.add_weight(shape=(2 * input_dim, self.channels),
                                        initializer=self.kernel_initializer,
                                        name='kernel',
                                        regularizer=self.kernel_regularizer,
                                        constraint=self.kernel_constraint)
          if self.use_bias:
              self.bias = self.add_weight(shape=(self.channels,),
                                          initializer=self.bias_initializer,
                                          name='bias',
                                          regularizer=self.bias_regularizer,
                                          constraint=self.bias_constraint)
          else:
              self.bias = None
          self.built = True
  
      def call(self, inputs):
          x, a, _ = self.get_inputs(inputs)
          a = ops.add_self_loops(a)
  
          aggregated = self.propagate(x, a)
          output = K.concatenate([x, aggregated])
          output = ops.dot(output, self.kernel)
  
          if self.use_bias:
              output = K.bias_add(output, self.bias)
          output = K.l2_normalize(output, axis=-1)
          if self.activation is not None:
              output = self.activation(output)
  
          return output
  
      @property
      def config(self):
          return{
              'channels': self.channels
          }
  