使用DGL实现基于闲鱼图进行边分类算法

闲鱼图算法解析和使用DGL进行实现

2019年阿里闲鱼团队在CIKM 2019上发了一篇进行Spam评论的检测算法,用到了GNN的思想。论文显示结果比较好。但是由于首先它们并没有开放数据,且也没开源算法的具体实现方法,同时还没有对其中的Attention部分给出具体的公式,所以没法进行复现。虽然这篇论文拿了CIKM 2019的最佳应用研究论文,但是这种无法复现的论文,是否应该给最佳还是要打个问号。

最近由于项目,需要使用这个算法,便尝试使用目前比较好的GNN实现框架——DGL(Deep Graph Library)进行了复现。经过两次迭代后,很顺利地完成了其中最核心部分公式的实现。这里对这次的工作进行一下总结,并形成一个使用DGL的教程,供大家参考。

闲鱼算法的核心部分是对于“用户”->“评论”->“商品”构建的二部图。其基本的数据结构如下图:
使用DGL实现基于闲鱼图进行边分类算法_第1张图片在这个图里,“用户”- U U U通过“评论”- E E E对“商品”- I I I进行交互,由此构成了一个二部图。其中“用户”和“商品”是点,而“评论”是边。

算法的核心任务是对边进行分类,即发现那些不正常的评论行为。

典型的机器学习方法是对评论进行特征提取,比如text2vec,同时把用户和商品的信息也进行特征提取。然后合并这三组特征,再送入分类器中,比如 XGboost等。
但是这种方法并没有充分利用到边和点之间的关联关系,因此闲鱼团队采用了基于GNN的算法,同时另外还加入了通过KNN构建的评论之间的相似图,进一步加入了一些信息。这样就利用了用户、商品、评论和它们之间的关系,理论上采用了更多的信息,提升了分类的效果。
但是由于论文没有数据和实现的细节,所以也没法确切的评估效果到底好不好。后文也会讨论一下采用这个算法碰到的问题。

一、算法原理

这个部分的算法的基本原理就是:

  • 通过Attention机制把“用户”+“评论”的特征发送给“商品”,并结合“商品”已有的特征,更新“商品”的特征;
  • 通过Attention机制把“商品”+“评论”的特征发送给“用户”,并结合“用户”已有的特征,更新“用户”的特征;
  • “用户”+“评论”+“商品”特征发给“评论”,并更新“评论”的特征;
  • 最后使用“用户” ∣ ∣ || “笔记” ∣ ∣ || “评论”作为特征进行“评论”的分类。其中, ∣ ∣ || c o n c a t e n a t e concatenate concatenate的意思。

这里的Attention的含义是,通过一些可学习的权重,来发现那些对于识别有问题的评论更重要的特征,并且通过GNN来把“用户”和“商品”的特征自动组合给“评论”,从而不用再去做人工的特征工程。

二、具体的公式

根据上面介绍的算法原理,这里把论文里的公式梳理一下。这并不会按照论文里的公式顺序来写,但是会给出原文里的公式编号。首先给出GNN的层上的值的定义:

L − 1 L-1 L1 L L L
h e l − 1 ∈ R ( 1 , D e l − 1 ) h_{e}^{l-1} \isin \Reals^{(1, D_{e}^{l-1})} hel1R(1,Del1) h e l ∈ R ( 1 , D e l ) h_{e}^{l} \isin \Reals^{(1, D_{e}^{l})} helR(1,Del)
h u l − 1 ∈ R ( 1 , D u l − 1 ) h_{u}^{l-1} \isin \Reals^{(1, D_{u}^{l-1})} hul1R(1,Dul1) h u l ∈ R ( 1 , D u l ) h_{u}^{l} \isin \Reals^{(1, D_{u}^{l})} hulR(1,Dul)
h i l − 1 ∈ R ( 1 , D i l − 1 ) h_{i}^{l-1} \isin \Reals^{(1, D_{i}^{l-1})} hil1R(1,Dil1) h i l ∈ R ( 1 , D i l ) h_{i}^{l} \isin \Reals^{(1, D_{i}^{l})} hilR(1,Dil)

这里面, e e e指代“评论”所代表的边; u u u指代“用户”节点; i i i指代“商品”节点。 D D D对应的是每一层的特征维度数。

首先来写原论文里的公式(6),这是对一个 u u u节点计算它所有的相邻 i i i节点和连接的边 e e e的隐藏层的值进行合并。同样的操作也对 i i i节点进行,所以有两个。

公式6:把点和边的特征进行 c o n c a t concat concat
关于“用户”类型的节点: H I E l − 1 = { c o n c a t ( h i l − 1 , h e l − 1 ) , ∀ e = ( u , i ) ∈ E ( u ) } \mathcal{H}_{IE}^{l-1} = \{concat(h_i^{l-1}, h_e^{l-1}), \forall e=(u, i)\isin E(u)\} HIEl1={concat(hil1,hel1),e=(u,i)E(u)}
关于“商品”类型的节点: H U E l − 1 = { c o n c a t ( h u l − 1 , h e l − 1 ) , ∀ e = ( u , i ) ∈ I ( u ) } \mathcal{H}_{UE}^{l-1} = \{concat(h_u^{l-1}, h_e^{l-1}), \forall e=(u, i)\isin I(u)\} HUEl1={concat(hul1,hel1),e=(u,i)I(u)}

这里计算后得到的 H I E l − 1 \mathcal{H}_{IE}^{l-1} HIEl1的维度是不固定的,因为这里是一个节点的所有的边的集合,所以是一个不定行数,但是列数量是固定维度: D e l − 1 + D i l − 1 D_{e}^{l-1} + D_{i}^{l-1} Del1+Dil1。同样的情况对于 H U E l − 1 \mathcal{H}_{UE}^{l-1} HUEl1也是一样的, H U E l − 1 \mathcal{H}_{UE}^{l-1} HUEl1的行数量不定,列的维度是固定的: D e l − 1 + D u l − 1 D_{e}^{l-1} + D_{u}^{l-1} Del1+Dul1

这两个计算出来的矩阵将会被用于在公式(7)里面计算Attension机制。但是这里原文写的很含糊,它虽然引用了文献24,但是并没有给出具体的计算方法,而采用基础的加权点积Attention方法是不行的,所以需要自己想办法。我这里采用了文献24里面的思想,给 k e y key key H H H都进行了维度变换。从而保证它们的维度一致,能进行加权点积注意力计算。

公式 5和7: 完成Attention的计算
对于“用户”类型节点: H N ( u ) l = σ ( W U l ∗ A T T N U ( W A U l − 1 ∗ h u l − 1 , W A I E l − 1 ∗ H I E l − 1 ) ) \mathcal{H}_{N(u)}^l = \sigma(W_U^l * ATTN_U(W_{AU}^{l-1} * h_u^{l-1}, W_{AIE}^{l-1} * \mathcal{H}_{IE}^{l-1})) HN(u)l=σ(WUlATTNU(WAUl1hul1,WAIEl1HIEl1))
对于“商品”类型节点: H N ( i ) l = σ ( W I l ∗ A T T N I ( W A I l − 1 ∗ h i l − 1 , W A U E l − 1 ∗ H U E l − 1 ) ) \mathcal{H}_{N(i)}^l = \sigma(W_I^l * ATTN_I(W_{AI}^{l-1} * h_i^{l-1}, W_{AUE}^{l-1} * \mathcal{H}_{UE}^{l-1})) HN(i)l=σ(WIlATTNI(WAIl1hil1,WAUEl1HUEl1))

这里的两个权重矩阵 W A ∗ l − 1 W_{A*}^{l-1} WAl1 W A ∗ E l − 1 W_{A*E}^{l-1} WAEl1就是用来进行维度变换的工具,不然在计算 A T T N ∗ ATTN_* ATTN的时候会无法完成维度对齐。背后的原因是因为 u u u i i i需要各自和 e e e进行 c o n c a t concat concat,他们是相互关联的。除非 u u u i i i的维度一样,不然不能直接进行后续计算。

Attention计算:
原文里没有对 A T T N ATTN ATTN的计算给出具体的计算公式,所以这里按照一般的注意力方式进行了设计。首先,把公式5里面 A T T N ATTN ATTN括号里面的计算先定义:
h ^ u − 1 l − 1 = W A U l − 1 ∗ h u l − 1 \hat{h}_{u-1}^{l-1} = W_{AU}^{l-1} * h_u^{l-1} h^u1l1=WAUl1hul1 H ^ I E l − 1 = W A I E l − 1 ∗ H I E l − 1 \hat{\mathcal{H}}_{IE}^{l-1}=W_{AIE}^{l-1} * \mathcal{H}_{IE}^{l-1} H^IEl1=WAIEl1HIEl1
h ^ i − 1 l − 1 = W A I l − 1 ∗ h i l − 1 \hat{h}_{i-1}^{l-1} = W_{AI}^{l-1} * h_i^{l-1} h^i1l1=WAIl1hil1 H ^ U E l − 1 = W A U E l − 1 ∗ H U E l − 1 \hat{\mathcal{H}}_{UE}^{l-1}=W_{AUE}^{l-1} * \mathcal{H}_{UE}^{l-1} H^UEl1=WAUEl1HUEl1

下面是 A T T N ATTN ATTN计算的4步:
第一步 A a t t n _ u = h ^ u − 1 l − 1 ⊙ H ^ I E l − 1 A_{attn\_u} = \hat{h}_{u-1}^{l-1} \odot \hat{\mathcal{H}}_{IE}^{l-1} Aattn_u=h^u1l1H^IEl1 A a t t n _ i = h ^ i − 1 l − 1 ⊙ H ^ U E l − 1 A_{attn\_i}=\hat{h}_{i-1}^{l-1} \odot \hat{\mathcal{H}}_{UE}^{l-1} Aattn_i=h^i1l1H^UEl1
其中, ⊙ \odot 是点积计算。这里的点积是广播性的计算,所以计算后的 A a t t n _ u A_{attn\_u} Aattn_u A a t t n _ i A_{attn\_i} Aattn_i就变成了一个一行 x x x列的向量。 x x x是一个不定的值,是每个节点的边的数量,也就是 H ∗ E l − 1 \mathcal{H}_{*E}^{l-1} HEl1的行数。
第二步 A a t t n _ u = s o f t m a x ( A a t t n _ u ) A_{attn\_u} =softmax(A_{attn\_u} ) Aattn_u=softmax(Aattn_u) A a t t n _ i = s o f t m a x ( A a t t n _ i ) A_{attn\_i} =softmax(A_{attn\_i} ) Aattn_i=softmax(Aattn_i)
这里对第一步计算出的每个节点的边的 a t t n attn attn值做 s o f t m a x softmax softmax计算,归一化到 ( 0 , 1 ) (0,1) (0,1)之间。
第三步 H ^ I E l = H I E l − 1 ∗ A a t t n _ u \mathcal{\hat{H}}_{IE}^l = \mathcal{H}_{IE}^{l-1} * A_{attn\_u} H^IEl=HIEl1Aattn_u H ^ U E l = H U E l − 1 ∗ A a t t n _ i \mathcal{\hat{H}}_{UE}^l = \mathcal{H}_{UE}^{l-1} * A_{attn\_i} H^UEl=HUEl1Aattn_i
这一步是把上面的到的经过 s o f t m a x softmax softmax后的 a t t n attn attn值再次和 H ∗ E l − 1 \mathcal{H}_{*E}^{l-1} HEl1广播相乘,这里的乘不是矩阵乘,而是类似Numpy的广播乘。完成对原始 H ∗ E l − 1 \mathcal{H}_{*E}^{l-1} HEl1矩阵加权计算。
第四步 H I E l = s u m ( H ^ I E l , d i m = − 1 ) \mathcal{H}_{IE}^l = sum(\mathcal{\hat{H}}_{IE}^l, dim=-1) HIEl=sum(H^IEl,dim=1) H U E l = s u m ( H ^ U E l , d i m = − 1 ) \mathcal{H}_{UE}^l = sum(\mathcal{\hat{H}}_{UE}^l, dim=-1) HUEl=sum(H^UEl,dim=1)
最后一步就是把第三步计算出的值,按列求和,从而在完成Attension的计算后,不定行的矩阵就变成了一个一行的向量。

经过 A T T N ATTN ATTN后, H I E l \mathcal{H}_{IE}^l HIEl W U l W_U^l WUl运算后得到了一行的向量,其维度也就是 H N ( u ) l H_{N(u)}^l HN(u)l的维度。 H U E l \mathcal{H}_{UE}^l HUEl W I l W_I^l WIl运算后得到了一行的向量,其维度也就是 H N ( I ) l H_{N(I)}^l HN(I)l的维度。

最后完成公式5和7里的 σ \sigma σ的非线性函数,得到 H N ( u ) l \mathcal{H}_{N(u)}^l HN(u)l H N ( i ) l \mathcal{H}_{N(i)}^l HN(i)l。到这里消息传递的工作已经完成,下面进入节点聚合的公式(8)。

公式8
对于“用户”类型节点: h u l = c o n c a t ( V U l ∗ h u l − 1 , H N ( u ) l ) h_u^l = concat(V_U^l * h_u^{l-1}, \mathcal{H}_{N(u)}^l) hul=concat(VUlhul1,HN(u)l)
对于“商品”类型节点: h i l = c o n c a t ( V I l ∗ h i l − 1 , H N ( i ) l ) h_i^l = concat(V_I^l * h_i^{l-1}, \mathcal{H}_{N(i)}^l) hil=concat(VIlhil1,HN(i)l)

公式3
计算每个边的 l l l 层的值 h e l h_e^l hel。本质就是把上一层的边的值和两端的两个节点的值 c o n c a t concat concat到一起,然后进行线性变换,最后进行一下非线性变换。

h e l = σ ( W E l ∗ c o n c a t ( h e l − 1 , h u l − 1 , h i l − 1 ) ) h_e^l = \sigma(W_E^l * concat(h_e^{l-1}, h_u^{l-1}, h_i^{l-1})) hel=σ(WElconcat(hel1,hul1,hil1))

由于是把上一层的3个隐藏层的 h h h值concat在一起,所以权重矩阵 W E l W_E^l WEl的输入维度就是:
D _ W E _ i n p u t = D e l − 1 + D u l − 1 + D i l − 1 D\_{WE}\_input = D_{e}^{l-1} + D_{u}^{l-1} + D_{i}^{l-1} D_WE_input=Del1+Dul1+Dil1,而权重矩阵的输出维度就是: D e l D_e^l Del

三、用DGL完成模型的构建

现在各种GNN模型层出不穷,但是要实现出这些模型,只用TensorFlow或者PyTorch写就很累。秉承着不重复造轮子的思想,要实现GNN就需要别人写的框架。DGL是目前非常好的实现GNN的框架。

要实现上面的咸鱼算法,就可以使用DGL里面的消息传递+节点聚合、更新功能,特别是DGL已经实现了基于边的softmax功能,这样就可以在实现Attention的时候非常方便。

下面的代码实现会用上面的插图里展示的这个非常简单的例子来演示。

3.1 定义Graph

闲鱼的算法针对的是一个单向的二部图。在DGL里,实现这个二部图的方法很多,下面给出一个直接的定义方法。这里需要注意的是,为了计算从“商品”到“用户”的消息传递计算,需要多定义一种边的类型。

co_g = dgl.bipartite([(0,0),(1,0),(2,0),(1,1),(1,2),(3,1),(4,1),(4,2)], 'user', 'comment_on', 'item')
cb_g = dgl.bipartite([(0,0),(0,1),(0,2),(1,1),(2,1),(1,3),(1,4),(2,4)], 'item', 'commented_by', 'user')
graph = dgl.hetero_from_relations([co_g, cb_g])

3.2 定义模型结构

使用DGL构建一个GNN模型的方法比较多,这里由于我们需要定制化实现消息传递和聚合,所以需要单独构建GNN的一层。

这里定义模块的核心是确定中间计算过程的维度值。这是整个模型最有奇技淫巧的地方。所以要深入地讨论一下。

  1. 注意上面的公式5的第一步中,需要进行内积运算,因此需要内积计算的两边的维度一致。
  2. 同时 H N ( u ) l \mathcal{H}_{N(u)}^l HN(u)l H N ( i ) l ) \mathcal{H}_{N(i)}^l) HN(i)l)在公式8里面的维度没有确定,这就需要自己定,代码里采用输出维度的一半下取整来作为它们的维度。
class layer(nn.Module):
    """
    This layer is designed specifically for user Xianyu Graph algorithm.
    """
    def __init__(self, d_u_in, d_u_out, d_e_in, d_e_out, d_i_in, d_i_out):
        super(layer, self).__init__()
        self.act = F.relu

        # The Weight in formula (3). Use a different way to implement concatinate and linear transform
        self.W_Ee = nn.Linear(d_e_in, d_e_out, bias=False)
        self.W_Eu = nn.Linear(d_u_in, d_e_out, bias=False)
        self.W_Ei = nn.Linear(d_i_in, d_e_out, bias=False)

        # weight parameter in formula (7), will do it in two ways, u->i and i->u
        # 1. i -> u
        self.d_attn_ie_in = d_e_in + d_i_in
        self.d_attn_u_out = self.d_attn_ie_in
        self.W_ATTN_ie = nn.Linear(self.d_attn_ie_in, self.d_attn_u_out, bias=False)
        self.W_ATTN_u = nn.Linear(d_u_in, self.d_attn_u_out, bias=False)

        # 2. u -> i
        self.d_attn_ue_in = d_e_in + d_u_in
        self.d_attn_i_out = self.d_attn_u_out
        self.W_ATTN_ue = nn.Linear(self.d_attn_ue_in, self.d_attn_i_out, bias=False)
        self.W_ATTN_i = nn.Linear(d_i_in, self.d_attn_i_out, bias=False)

        # weight parameter in formula (5)
        self.d_wu_in = self.d_attn_u_out
        self.d_wu_out = int(d_u_out / 2)
        self.W_nu = nn.Linear(self.d_wu_in, self.d_wu_out, bias=False)

        self.d_wi_in = self.d_attn_i_out
        self.d_wi_out = int(d_i_out / 2)
        self.W_ni = nn.Linear(self.d_wi_in, self.d_wi_out, bias=False)

        # weight parameters in formula (8)
        self.d_vu_out = d_u_out - self.d_wu_out
        self.W_u = nn.Linear(d_u_in, self.d_vu_out, bias=False)

        self.d_vi_out = d_i_out - self.d_wi_out
        self.W_i = nn.Linear(d_i_in, self.d_vi_out, bias=False)

3.3 实现前向计算

模型的定义和forward的实现都在这个layer里面实现。这里forward的输入是图的结构graph,以及点和边的特征。

    def forward(self, graph, u_feats, e_feats, i_feats):
        """
        Specificlly For this algorithm, feat_dict has 3 types of features:
        'User': l-1 layer's user features, in dict {'u': features}
        'Edge': l-1 layer's edge features, in dict {'e': features}
        'Item': l-1 layer's note features, in dict {'i': features}

        This version, we have one edge but two types:
            - 'comment_on'
            - 'commented_by'

        :param graph: bi-partitie
        :return:
        """
        # assign values
        graph.nodes['user'].data['u'] = u_feats
        graph.nodes['item'].data['i'] = i_feats
        graph.edges['comment_on'].data['e'] = e_feats
        graph.edges['commented_by'].data['e'] = e_feats

        # formula 6
        graph.apply_edges(lambda edges: {'h_ie': th.cat([edges.src['i'], edges.data['e']], dim=-1)}, etype='commented_by')
        graph.apply_edges(lambda edges: {'h_ue': th.cat([edges.src['u'], edges.data['e']], dim=-1)}, etype='comment_on')

        # formula 7, self_attension
        graph.nodes['user'].data['h_attnu'] = self.W_ATTN_u(u_feats)
        graph.nodes['item'].data['h_attni'] = self.W_ATTN_i(i_feats)
        graph.edges['commented_by'].data['h_attne'] = self.W_ATTN_ie(graph.edges['commented_by'].data['h_ie'])
        graph.edges['comment_on'].data['h_attne'] = self.W_ATTN_ue(graph.edges['comment_on'].data['h_ue'])

        # Step 1: dot product
        graph.apply_edges(fn.e_dot_v('h_attne', 'h_attnu', 'edotv'), etype='commented_by')
        graph.apply_edges(fn.e_dot_v('h_attne', 'h_attni', 'edotv'), etype='comment_on')

        # Step 2. softmax
        graph.edges['commented_by'].data['sfm'] = edge_softmax(graph['commented_by'], graph.edges['commented_by'].data['edotv'])
        graph.edges['comment_on'].data['sfm'] = edge_softmax(graph['comment_on'], graph.edges['comment_on'].data['edotv'])

        # Step 3. Broadcast softmax value to each edge, and then attention is done
        graph.apply_edges(lambda edges: {'attn': edges.data['h_attne'] * edges.data['sfm'].unsqueeze(dim=0).T},
                          etype='commented_by')
        graph.apply_edges(lambda edges: {'attn': edges.data['h_attne'] * edges.data['sfm'].unsqueeze(dim=0).T},
                          etype='comment_on')

        # Step 4. Aggregate attention to dst,user nodes, so formula 7 is done
        graph.update_all(fn.copy_e('attn', 'm'), fn.sum('m', 'agg_u'), etype='commented_by')
        graph.update_all(fn.copy_e('attn', 'm'), fn.sum('m', 'agg_i'), etype='comment_on')

        # formula 5
        graph.nodes['user'].data['h_nu'] = self.act(self.W_nu(graph.nodes['user'].data['agg_u']))
        graph.nodes['item'].data['h_ni'] = self.act(self.W_ni(graph.nodes['item'].data['agg_i']))

        # formula 8
        graph.nodes['user'].data['u'] = th.cat([self.W_u(u_feats), graph.nodes['user'].data['h_nu']], dim=-1)
        graph.nodes['item'].data['i'] = th.cat([self.W_i(i_feats), graph.nodes['item'].data['h_ni']], dim=-1)

        # formula 3 and 4
        # first compute 3 matrix multiply
        graph.edges['comment_on'].data['h_e'] = self.W_Ee(e_feats)
        graph.edges['commented_by'].data['h_e'] = self.W_Ee(e_feats)
        graph.nodes['user'].data['h_u4e'] = self.W_Eu(u_feats)
        graph.nodes['item'].data['h_i4e'] = self.W_Ei(i_feats)

        # formula 3, add them up
        graph.apply_edges(fn.u_add_e('h_u4e', 'h_e', 'h_ue'), etype='comment_on')
        graph.apply_edges(fn.e_add_v('h_ue', 'h_i4e', 'e'), etype='comment_on')
        graph.edges['comment_on'].data['e'] = self.act(graph.edges['comment_on'].data['e'])

        graph.edges['commented_by'].data['e'] = graph.edges['comment_on'].data['e']

        u_feats = graph.nodes['user'].data['u']
        e_feats = graph.edges['comment_on'].data['e']
        i_feats = graph.nodes['item'].data['i']

        return u_feats, e_feats, i_feats

上面代码里面出现的edge_softmax函数是DGL的模块,处于dgl.nn.pytorch.softmax包内。

3.4 构建整个GNN网络

完成了模型的一层后,下面就可以定义整个GNN的网络。这里使用了2层的架构。

class Algorithm_Model(nn.Module):

    def __init__(self, u_in_dim, u_hidden_dim, u_out_dim,
                 e_in_dim, e_hidden_dim, e_out_dim,
                 i_in_dim, i_hidden_dim, i_out_dim):

        super(Algorithm_Model, self).__init__()
        self.layer_1 = layer(u_in_dim, u_hidden_dim, e_in_dim, e_hidden_dim, i_in_dim, i_hidden_dim)
        self.layer_2 = layer(u_hidden_dim, u_out_dim, e_hidden_dim, e_out_dim, i_hidden_dim, i_out_dim)

    def forward(self, graph, u_features, e_features, i_features):
        h_u, h_e, h_i = self.layer_1(graph, u_features, e_features, i_features)
        h_u = F.relu(h_u)
        h_e = F.relu(h_e)
        h_i = F.relu(h_i)
        h_u, h_e, h_i = self.layer_2(graph, h_u, h_e, h_i)

        # use graph to concat user nodes logits and item nodes logits to the edges
        # assign values
        graph.nodes['user'].data['u'] = h_u
        graph.nodes['item'].data['i'] = h_i
        graph.edges['comment_on'].data['e'] = h_e

        graph.apply_edges(lambda edges:
                          {'output': th.cat([edges.src['u'], edges.data['e'], edges.dst['i']], dim=-1)}, etype='comment_on')

        return graph.edges['comment_on'].data['output']

3.5 模型的整体框架

有了这个GNN的网络,就需要把GNN的最终输出送入一个分类器,来完成整体的端到端的框架。这里不再给出具体的代码,实现起来并不难。

四、实验结果的讨论

采用闲鱼的这个算法,对项目的数据进行了实验,同时也直接采用MLP对“用户”||“商品”||“评论”直接进行建模作为基线。
比对的结果发现,闲鱼的网络的分类结果并不比MLP好多少。特别是在我们的场景里,数据有比较多的噪音。

对于数据里面有噪音的场景,实验结果显示Attention的机制可能过于敏感,学到了噪音的信号,反而会降低模型的性能
所以会使用一个类似但是更简单的模型来替换Attention机制,并通过实验验证在有噪音的场景下,简单的模型会提升性能,就如同MLP那样。
从类似的角度看,那么基本算法就是:

  • 对于边, h e l = σ ( W E l ∗ c o n c a t ( h e l − 1 , h u l − 1 , h i l − 1 ) ) h_e^l = \sigma(W_E^l * concat(h_e^{l-1}, h_u^{l-1}, h_i^{l-1})) hel=σ(WElconcat(hel1,hul1,hil1))
  • 对于用户点, h u l = σ ( c o n c a t ( a g g ( W I ∗ h i l − 1 + W E ∗ h e l − 1 ) , V U ∗ h u l − 1 ) ) h_u^l=\sigma(concat(agg(W_I*h_i^{l-1}+W_E*h_e^{l-1}), V_U*h_u^{l-1})) hul=σ(concat(agg(WIhil1+WEhel1),VUhul1))
  • 对于物品点, h i l = σ ( c o n c a t ( a g g ( W U ∗ h u l − 1 + W E ∗ h e l − 1 ) , V I ∗ h i l − 1 ) ) h_i^l=\sigma(concat(agg(W_U*h_u^{l-1}+W_E*h_e^{l-1}), V_I*h_i^{l-1})) hil=σ(concat(agg(WUhul1+WEhel1),VIhil1))

其中,端点到边的计算是消息传播的,不是单纯地矩阵相加。即 U U U类型的节点更新是 { c o n c a t ( h i l − 1 , h e l − 1 ) , ∀ e = ( u , i ) ∈ E ( u ) } \{concat(h_i^{l-1}, h_e^{l-1}), \forall e=(u, i)\isin E(u)\} {concat(hil1,hel1),e=(u,i)E(u)},然后再和一个权重矩阵相乘。上面的公式里面先相乘再相加,和这个公式里面的先 c o n c a t concat concat再乘权重是等价的。

这个方式就仅仅只是完成源节点+边特征传播给目标节点,然后本地聚合,并更新目标节点的 h l h^l hl

在DGL里实现这个简单的模型就更简单了。参照上面的代码就可以了,这里不具体实现。如有问题欢迎留言交流。

你可能感兴趣的:(图学习,图神经网络,机器学习,人工智能,GNN,图神经网络,DGL,算法)