//  [python]
def build_graph(self, inputs):
        """Add operations to classify a batch of input images.
        Args:
          inputs: A Tensor representing a batch of input images.
        Returns:
          A logits Tensor with shape [<batch_size>, self.num_classes].
        """
        original_global_variables = tf.compat.v1.global_variables()

        shape_ids = tf.cast(inputs['shape_ids'][:, 0], tf.int32)
        batch_size = shape_ids.get_shape()[0]
        shape_id_to_embeddings_map = tf.tile(
          tf.expand_dims(inputs['shape_id_to_embeddings_map'], 0), [batch_size, 1, 1])
        shape_embeddings = tf.squeeze(tf.batch_gather(shape_id_to_embeddings_map, shape_ids))
        self.shape_ids = shape_ids
        self.shape_embeddings = shape_embeddings

        inputs = inputs['images'][:, 0]

        batch_size = inputs.get_shape()[0]
        training = True if self.mode == 'train' else False
        with self._model_variable_scope():
            if self.data_format == 'channels_first':
                # Convert the inputs from channels_last (NHWC) to channels_first (NCHW).
                # This provides a large performance boost on GPU. See
                # https://www.tensorflow.org/performance/performance_guide#data_formats
                inputs = tf.transpose(a=inputs, perm=[0, 3, 1, 2])

        inputs = conv2d_fixed_padding(
          inputs=inputs, filters=self.num_filters, kernel_size=self.kernel_size,
          strides=self.conv_stride, data_format=self.data_format)
        inputs = tf.identity(inputs, 'initial_conv')

        # We do not include batch normalization or activation functions in V2
        # for the initial conv1 because the first ResNet unit will perform these
        # for both the shortcut and non-shortcut paths as part of the first
        # block's projection. Cf. Appendix of [2].
        if self.resnet_version == 1:
            inputs = batch_norm(inputs, training, self.data_format)
            inputs = tf.nn.relu(inputs)

        if self.first_pool_size:
            inputs = tf.compat.v1.layers.max_pooling2d(
                inputs=inputs, pool_size=self.first_pool_size,
                strides=self.first_pool_stride, padding='SAME',
                data_format=self.data_format)
            inputs = tf.identity(inputs, 'initial_max_pool')

        for i, num_blocks in enumerate(self.block_sizes):
            num_filters = self.num_filters * (2**i)
            inputs = block_layer(
                inputs=inputs, filters=num_filters, bottleneck=self.bottleneck,
                block_fn=self.block_fn, blocks=num_blocks,
                strides=self.block_strides[i], training=training,
                name='block_layer{}'.format(i + 1), data_format=self.data_format)

        # Only apply the BN and ReLU for model that does pre_activation in each
        # building/bottleneck block, eg resnet V2.
        if self.pre_activation:
            inputs = batch_norm(inputs, training, self.data_format)
            inputs = tf.nn.relu(inputs)

        # The current top layer has shape
        # `batch_size x pool_size x pool_size x final_size`.
        # ResNet does an Average Pooling layer over pool_size,
        # but that is the same as doing a reduce_mean. We do a reduce_mean
        # here because it performs better than AveragePooling2D.
        axes = [2, 3] if self.data_format == 'channels_first' else [1, 2]
        inputs = tf.reduce_mean(input_tensor=inputs, axis=axes, keepdims=True)
        inputs = tf.identity(inputs, 'final_reduce_mean')

        inputs = tf.squeeze(inputs, axes)
        inputs = tf.compat.v1.layers.dense(inputs=inputs, units=self.num_classes)
        inputs = tf.identity(inputs, 'final_dense')
        self.logits = inputs

        self.loss = tf.losses.mean_squared_error(shape_embeddings, self.logits)

        optimizer = tf.compat.v1.train.AdamOptimizer(self.lr, self.beta1, self.beta2)
        with tf.name_scope('compute_gradients'):
            gradvars = optimizer.compute_gradients(self.loss, var_list=tf.compat.v1.trainable_variables())
        with tf.name_scope('apply_gradients'):
            self.train_op = optimizer.apply_gradients(gradvars)

        self.saveable_variables = [var for var in tf.compat.v1.global_variables() if var not in original_global_variables]
        return inputs