//  [python]
def _build_seq_graph(self):
        """ SURGE Model: 

            1) Interest graph: Graph construction based on metric learning
            2) Interest fusion and extraction : Graph convolution and graph pooling 
            3) Prediction: Flatten pooled graph to reduced sequence
        """
        X = tf.concat(
            [self.item_history_embedding, self.cate_history_embedding], 2
        )
        self.mask = self.iterator.mask
        self.float_mask = tf.cast(self.mask, tf.float32)
        self.real_sequence_length = tf.reduce_sum(self.mask, 1)
        # compute recent for matching
        self.recent_k = 1
        #  self.recent_k = 5
        self.real_mask = tf.cast(self.mask, tf.float32)
        self.position = tf.math.cumsum(self.real_mask, axis=1, reverse=True)
        self.recent_mask = tf.logical_and(self.position >= 1, self.position <= self.recent_k)
        self.real_recent_mask = tf.where(self.recent_mask, tf.ones_like(self.recent_mask, dtype=tf.float32), tf.zeros_like(self.recent_mask, dtype=tf.float32))
        self.recent_embedding_mean = tf.reduce_sum(X*tf.expand_dims(self.real_recent_mask, -1), 1)/tf.reduce_sum(self.real_recent_mask, 1, keepdims=True)

        # change to his mean
        #  self.real_mask = tf.cast(self.mask, tf.float32)
        #  self.recent_embedding_mean = tf.reduce_sum(X*tf.expand_dims(self.real_mask, -1), 1)/tf.reduce_sum(self.real_mask, 1, keepdims=True)

        with tf.name_scope('interest_graph'):
            ## Node similarity metric learning 
            S = []
            for i in range(self.metric_heads):
                # weighted cosine similarity
                self.weighted_tensor = tf.layers.dense(tf.ones([1, 1]), X.shape.as_list()[-1], use_bias=False)
                X_fts = X * tf.expand_dims(self.weighted_tensor, 0)
                X_fts = tf.nn.l2_normalize(X_fts,dim=2)
                S_one = tf.matmul(X_fts, tf.transpose(X_fts, (0,2,1))) # B*L*L
                # Euclidean distance
                p1 = tf.matmul(
                    tf.expand_dims(tf.reduce_sum(tf.square(A), -1), 1),
                    tf.ones(shape=(1, M2))
                )
                p2 = tf.transpose(tf.matmul(
                    tf.reshape(tf.reduce_sum(tf.square(B), 1), shape=[-1, 1]),
                    tf.ones(shape=(M1, 1)),
                    transpose_b=True
                ))

                res = tf.sqrt(tf.add(p1, p2) - 2 * tf.matmul(A, B, transpose_b=True))
                # min-max normalization for mask
                S_min = tf.reduce_min(S_one, -1, keepdims=True)
                S_max = tf.reduce_max(S_one, -1, keepdims=True)
                S_one = (S_one - S_min) / (S_max - S_min)
                S += [S_one]
            S = tf.reduce_mean(tf.stack(S, 0), 0)
            # mask invalid nodes
            S = S * tf.expand_dims(self.float_mask, -1) * tf.expand_dims(self.float_mask, -2)

            ## Graph sparsification via seted sparseness 
            S_flatten = tf.reshape(S, [tf.shape(S)[0],-1])
            if 'kwai' in socket.gethostname():
                sorted_S_flatten = tf.contrib.framework.sort(S_flatten, direction='DESCENDING', axis=-1) # B*L -> B*L
            else:
                sorted_S_flatten = tf.sort(S_flatten, direction='DESCENDING', axis=-1) # B*L -> B*L
            # relative ranking strategy of the entire graph
            num_edges = tf.cast(tf.count_nonzero(S, [1,2]), tf.float32) # B
            to_keep_edge = tf.cast(tf.math.ceil(num_edges * self.relative_threshold), tf.int32)
            if 'kwai' in socket.gethostname():
                threshold_index = tf.stack([tf.range(tf.shape(X)[0]), tf.cast(to_keep_edge, tf.int32)], 1) # B*2
                threshold_score = tf.gather_nd(sorted_S_flatten, threshold_index) # indices[:-1]=(B) + data[indices[-1]=() --> (B)
            else:
                threshold_score = tf.gather_nd(sorted_S_flatten, tf.expand_dims(tf.cast(to_keep_edge, tf.int32), -1), batch_dims=1) # indices[:-1]=(B) + data[indices[-1]=() --> (B)
            A = tf.cast(tf.greater(S, tf.expand_dims(tf.expand_dims(threshold_score, -1), -1)), tf.float32)


        with tf.name_scope('interest_fusion_extraction'):
            for l in range(self.pool_layers):
                reuse = False if l==0 else True
                X, A, graph_readout, alphas = self._interest_fusion_extraction(X, A, layer=l, reuse=reuse)


        with tf.name_scope('prediction'):
            # flatten pooled graph to reduced sequence 
            output_shape = self.mask.get_shape()
            if 'kwai' in socket.gethostname():
                sorted_mask_index = tf.contrib.framework.argsort(self.mask, direction='DESCENDING', stable=True, axis=-1) # B*L -> B*L
                sorted_mask = tf.contrib.framework.sort(self.mask, direction='DESCENDING', axis=-1) # B*L -> B*L
            else:
                sorted_mask_index = tf.argsort(self.mask, direction='DESCENDING', stable=True, axis=-1) # B*L -> B*L
                sorted_mask = tf.sort(self.mask, direction='DESCENDING', axis=-1) # B*L -> B*L
            sorted_mask.set_shape(output_shape)
            sorted_mask_index.set_shape(output_shape)
            X = tf.batch_gather(X, sorted_mask_index) # B*L*F  < B*L = B*L*F
            self.mask = sorted_mask
            self.reduced_sequence_length = tf.reduce_sum(self.mask, 1) # B

            # cut useless sequence tail per batch 
            self.to_max_length = tf.range(tf.reduce_max(self.reduced_sequence_length)) # l
            X = tf.gather(X, self.to_max_length, axis=1) # B*L*F -> B*l*F
            self.mask = tf.gather(self.mask, self.to_max_length, axis=1) # B*L -> B*l
            self.reduced_sequence_length = tf.reduce_sum(self.mask, 1) # B

            # use cluster score as attention weights in AUGRU 
            if not self.remove_target: 
                if not self.recent_target:
                    _, alphas = self._attention_fcn(self.target_item_embedding, X, 'AGRU', False, return_alpha=True)
                else:
                    _, alphas = self._attention_fcn(self.recent_embedding_mean, X, 'AGRU', False, return_alpha=True)
                _, final_state = dynamic_rnn_dien(
                    VecAttGRUCell(self.hparams.hidden_size),
                    inputs=X,
                    att_scores = tf.expand_dims(alphas, -1),
                    sequence_length=self.reduced_sequence_length,
                    dtype=tf.float32,
                    scope="gru"
                )
            else:
                _, final_state = dynamic_rnn(
                    GRUCell(self.hparams.hidden_size),
                    inputs=X,
                    sequence_length=self.reduced_sequence_length,
                    dtype=tf.float32,
                    scope="gru",
                )

            #  #  self.recent_k = 5
            #  self.recent_k = 1
            #  self.real_mask = tf.cast(self.mask, tf.float32)
            #  self.position = tf.math.cumsum(self.real_mask, axis=1, reverse=True)
            #  self.recent_mask = tf.logical_and(self.position >= 1, self.position <= self.recent_k)
            #  self.real_recent_mask = tf.where(self.recent_mask, tf.ones_like(self.recent_mask, dtype=tf.float32), tf.zeros_like(self.recent_mask, dtype=tf.float32))
            #  self.recent_embedding_mean = tf.reduce_sum(X*tf.expand_dims(self.real_recent_mask, -1), 1)/tf.reduce_sum(self.real_recent_mask, 1, keepdims=True)

            if not self.remove_target: 
                if not self.recent_target:
                    model_output = tf.concat([final_state, graph_readout, self.target_item_embedding, graph_readout*self.target_item_embedding], 1)
                else:
                    #  model_output = tf.concat([final_state, graph_readout, self.target_item_embedding, graph_readout*self.recent_embedding_mean], 1)
                    #  model_output = tf.concat([final_state, graph_readout, self.target_item_embedding, self.recent_embedding_mean ,graph_readout*self.recent_embedding_mean], 1)
                    model_output = tf.concat([final_state, self.target_item_embedding, self.recent_embedding_mean], 1)
                    #  model_output = tf.concat([final_state, self.target_item_embedding], 1)
            else:
                #  model_output = tf.concat([final_state, graph_readout, self.target_item_embedding, graph_readout], 1)
                #  model_output = tf.concat([final_state, graph_readout, self.target_item_embedding], 1)
                model_output = tf.concat([final_state, self.target_item_embedding], 1)


        return model_output