论文阅读 有关SemGCN的公式理解


图1对提出的语义图卷积的说明。(a) 3 × 3 3\times 3 3×3 的CNNs卷积核(用绿色高亮) 为核中的每个点学习了一个不同的转换矩阵 w i w_i wi。我们通过学习每个位置的加权向量 a i a_i ai 和共享的变换矩阵 W W W来对其进行近似。(b)传统的GCNs只学习所有节点的共享转换矩阵 w 0 w_0 w0。(c)近似公式(a) 可以直接推广到(b):我们为图中的每个节点增加一个额外的可学习权值。(d)我们进一步扩展(c),以了解每个节点的通道加权向量 a i a_i ai。将它们与传统GCNs中的香草变换矩阵 W W W 组合后,我们可以获得图的新内核运算,该运算具有与CNN相当的学习能力。 学习的权重向量显示了图中隐含的相邻节点的局部语义关系。

1 普通semgcn

1.1公式描述

在论文中对于semgcn的介绍作者画了上面这张图,然后使用了公式进一步说明:

  • 对于图(c)在这里插入图片描述

为此,我们提出了语义图卷积(SemGConv),其中我们在传统图卷积中添加了一个可学习的权重矩阵 M ∈ R K × K M∈\R^{K×K} MRK×K。 然后Eq.1转换为:
X l + 1 = σ ( W X ( l ) ρ i ( M ⊙ A ) ) , (2) X^{l+1} = \sigma{(WX^{(l)} \rho_i(M\odot A))},\tag{2} Xl+1=σ(WX(l)ρi(MA)),(2)
其中 ρ i ρ_i ρi 是Softmax非线性,它对节点 i i i 的所有选择上的输入矩阵进行归一化 ; ⊙ \odot 是一个元素运算,如果 a i j = 1 a_{ij} = 1 aij=1 则返回 m i j m_{ij} mij,或是一个大负数在 ρ i ρ_i ρi的指数运算之后趋近于0; A A A用作掩码,强制对图中的节点 i i i进行计算,我们仅计算其相邻节点 j ∈ N ( i ) j∈N(i) jN(i)的权重

通过阅读代码,我们可以知道每个符号的维度数来进一步理解公式:

  • X ( l ) X^{(l)} X(l)是第 l l l层网络的输入数据input,根据论文中画网络层时候的图标或者根据代码可以知道它是一个 B ∗ 16 ∗ 2 B*16*2 B162的矩阵, B B B表示批处理大小batchsize。(不过经过第一个semgcn层后,输出变成了128,所以再往后以此作为输入的semgcn的输入就变成了 B ∗ 16 ∗ 128 B*16*128 B16128
  • W W W是一个转换矩阵,可以先把 W W W看成一个 2 ∗ 128 2*128 2128的矩阵
  • M ∈ R K × K M∈\R^{K×K} MRK×K M M M 是一个 16 × 16 16\times 16 16×16的矩阵
  • A A A 是图的经过处理后的邻接矩阵(拉普拉斯矩阵),节点 i i i j j j 有边则 a i j = 1 a_{ij}=1 aij=1。且对角线为1,也就是 a i i = 1 a_{ii} =1 aii=1,因为自己节点的数据也是一个可以利用的训练数据。
  • ⊙ \odot 是一个元素运算:也就是如果 A A A矩阵中元素为1的位置被 m i j m_{ij} mij替换,不为1的位置被一个大负数替换(代码中的大负数为-9e15)
  • σ \sigma σ ρ i ρ_i ρi,作者已经解释了为Relu激活函数和softmax非线性变换归一化

所以对于第一个Semgcn层(不同的层维数有差异,但原理相同),公式中的维度变化(不考虑batchsize维度),在代码中的实现顺序和公式有所不同,进行调整后可以写成下面这样:
X l + 1 ⏟ 16*128 = σ ( ρ i ( M ⏟ 16*16 ⊙ A ⏟ 16*16 ) X l ⏟ 16*2 W ⏟ 2*128 ) \underbrace{X^{l+1}}_{\text{16*128}} = \sigma{(\rho_i(\underbrace{M}_{\text{16*16}}\odot \underbrace{A}_{\text{16*16}})\underbrace{X^{l}}_{\text{16*{2}}}\underbrace{W}_{\text{2*128}})} 16*128 Xl+1=σ(ρi(16*16 M16*16 A)16*2 Xl2*128 W)
花括号下的数字表示矩阵的大小。首先是输入的16个关节点的2维坐标 X X X,然后和转换矩阵 W W W相乘变成 16 ∗ 128 16*128 16128的矩阵假设这个矩阵是 H H H。然后是权重矩阵 M M M和处理后的邻接矩阵 A A A进行元素运算,运算后仍然是一个 16 ∗ 16 16*16 1616的矩阵,这个矩阵再和 16 ∗ 128 16*128 16128的矩阵 H H H进行运算,得到输出 16 ∗ 128 16*128 16128的矩阵 X X X

在实际实现中,W并不是一个整体,它被拆成两部分:一部分W[0]用于对节点本身进行变换,另一部分用于对邻接节点进行变换。在论文的附录中是这样描述的:

​ 一些先前的方法[64,67]提出利用图卷积中的两个不同的转换矩阵。 具体来说,当将图卷积滤波器应用于图中的结点时,使用一个矩阵 W 0 W_0 W0来变换结点的表示,而为其所有邻居学习另一个矩阵 W 1 W_1 W1。 我们重写Eq.1为:
X ( l + 1 ) = σ ( I ⊗ W 0 X ( l ) A ~ + ( 1 − I ) ⊗ W 1 X ( l ) A ~ ) , (9) X^{(l+1)} = \sigma{(I\otimes W_0X^{(l)} \tilde{A} + (1 - I) \otimes W_1 X ^{(l)} \tilde{A})}, \tag{9} X(l+1)=σ(IW0X(l)A~+(1I)W1X(l)A~),(9)
⊗ \otimes 表示逐元素相乘, I I I是单位矩阵。 我们还实现了由Eq.2和3定义的SemGConv以类似的方式。

  • I I I是对角线全为1的单位矩阵, ( 1 − I ) (1-I) (1I)就得到了除了对角线其余全为1的矩阵
  • W[0]和W[1]都是一个 D ( l ) ∗ 128 D^{(l)}*128 D(l)128的矩阵,和上面那个简单公式的 W W W含义一样,根据网络层的不同可以是 16 ∗ 128 16*128 16128或者 128 ∗ 128 128*128 128128的矩阵

1.2代码

# 语义图卷积层
class SemGraphConv(nn.Module):
    """
    Semantic graph convolution layer
    """
    # 构造函数,输入特征数=2,输出特征数=128,邻接矩阵adj=16*16
    def __init__(self, in_features, out_features, adj, bias=True):
        super(SemGraphConv, self).__init__()
        self.in_features = in_features      # 输入特征数=2
        self.out_features = out_features    # 输出特征数=128
        # 转换矩阵W = 2 * in * out = 2 * 2 * 128    /   2*128*128
        self.W = nn.Parameter(torch.zeros(size=(2, in_features, out_features), dtype=torch.float))
        nn.init.xavier_uniform_(self.W.data, gain=1.414)    # W 矩阵初始化,使用均匀分布

        self.adj = adj
        self.m = (self.adj > 0)     # 权重矩阵M = 16 * 16,邻边设置为true
        # e 矩阵,另一种形式的mij
        # m.nonzero(): 返回m矩阵中非零元素的索引 = 非零元素的个数z*维度(2)
        # e = 1 * z
        self.e = nn.Parameter(torch.zeros(1, len(self.m.nonzero()), dtype=torch.float))
        nn.init.constant_(self.e.data, 1)   # e矩阵全部填充为1

        if bias:
            self.bias = nn.Parameter(torch.zeros(out_features, dtype=torch.float))
            stdv = 1. / math.sqrt(self.W.size(2))
            self.bias.data.uniform_(-stdv, stdv)
        else:
            self.register_parameter('bias', None)

    def forward(self, input):
        # input = 16*2  / 16*128
        h0 = torch.matmul(input, self.W[0])     # W[0]表示节点的变换 = (16 *2) * (2 * 128) = 16*128
        h1 = torch.matmul(input, self.W[1])     # W[1]表示节点所有邻居的变换 = (16*2) * (2 * 128) = 16*128
        # 生成一个与adj形状相同的全1张量,然后与-9e15相乘
        adj = -9e15 * torch.ones_like(self.adj).to(input.device)
        # 邻接矩阵中与节点邻接的点 = eij, 那么不与节点相邻的点就=-9e15
        adj[self.m] = self.e    # 元素运算 ⊙
        adj = F.softmax(adj, dim=1)     # 按行使用softmax进行归一化
        # 生成一个有邻接矩阵行数(adj.size(0))的方阵,矩阵对角线全1,其余部分全0的二维矩阵
        # 即公式中的I矩阵
        M = torch.eye(adj.size(0), dtype=torch.float).to(input.device)
        # 邻接矩阵adj*单位矩阵M * W[0] + A*(1-M)*W[1]
        output = torch.matmul(adj * M, h0) + torch.matmul(adj * (1 - M), h1)

        if self.bias is not None:
            return output + self.bias.view(1, 1, -1)
        else:
            return output

    def __repr__(self):
        return self.__class__.__name__ + ' (' + str(self.in_features) + ' -> ' + str(self.out_features) + ')'

带有通道级联的semgcn

对于图(d):在这里插入图片描述

如图1(d)所示,我们可以进一步扩展Eq. 2,通过学习一组 M d ∈ R K × K M_d\in \R^{K\times K} MdRK×K,从而对输出节点特征的每个通道d应用不同的加权矩阵
X ( l + 1 ) = ∣ ∣ d = 1 D l + 1 σ ( w ⃗ d X ( l ) ρ i ( M d ⊙ A ) ) , (3) X^{(l+1)} = \mathop{||}\limits_{d=1}^{D_{l+1}}\sigma{(\vec{w}_dX^{(l)} \rho_i(M_d\odot A))},\tag{3} X(l+1)=d=1∣∣Dl+1σ(w dX(l)ρi(MdA)),(3)
其中 ∣ ∣ || 表示通道级联, w ⃗ d \vec{w}_d w d是变换矩阵 W W W的第d行。

通过阅读代码,对每个符号进行解释。
在代码中其实对矩阵相乘的顺序进行了变换,以及为了提高效率,有些操作合并到一个矩阵进行运算。

  • 论文中所说的通道 d d d,其实是作者所说的输出通道也就是输出的128维数通道中的一条数据
  • 变换矩阵 W W W 经过变换后可以看作是一个 128 ∗ 2 128*2 1282 的矩阵,它的一行可以看作一个长度为2的向量 w ⃗ d \vec{w}_d w d,也就是一个 1 ∗ 2 1*2 12 的矩阵
  • X ( l ) X^{(l)} X(l) 经过变换后其实是一个 2 ∗ 16 2*16 216的矩阵
  • M d ∈ R K × K M_d\in \R^{K\times K} MdRK×K 可以看作一个 16 ∗ 16 16*16 1616 的矩阵,然后对于每一个通道都有一个 M d M_d Md,所以 M M M是一个 128 ∗ 16 ∗ 16 128*16*16 1281616的矩阵在这里插入图片描述

所以公式的含义是:
一个 1 ∗ 2 1*2 12的矩阵 w ⃗ d \vec{w}_d w d 2 ∗ 16 2*16 216的矩阵 X X X相乘得到一个 1 ∗ 16 1*16 116的矩阵,一个通道的 M d M_d Md A A A进行元素运算后得到一个 16 ∗ 16 16*16 1616的矩阵,这个矩阵和之前的 1 ∗ 16 1*16 116的矩阵相乘得到一个 1 ∗ 16 1*16 116的矩阵。然后上述过程重复 D ( l ) D^{(l)} D(l)次,也就是128次后将它们连接起来就是一个 128 ∗ 16 128*16 12816的矩阵,代码中进行了一些转换后输出一个 16 ∗ 128 16*128 16128的矩阵。

在实际的代码中,并没有重复 D ( l ) D^{(l)} D(l)次这个操作,通过扩充维数和通道维数的变换在一次计算中进行了通道的级联。

代码

class SemCHGraphConv(nn.Module):
    """
    Semantic channel-wise graph convolution layer
    """

    def __init__(self, in_features, out_features, adj, bias=True):
        super(SemCHGraphConv, self).__init__()
        self.in_features = in_features
        self.out_features = out_features

        self.W = nn.Parameter(torch.zeros(size=(2, in_features, out_features), dtype=torch.float))
        nn.init.xavier_uniform_(self.W.data, gain=1.414)
        # adj在第0维增加一个维度 16*16 --> 1*16*16,然后在0维重复out_features次,2维1次,3维1次-->out_features*16*16
        self.adj = adj.unsqueeze(0).repeat(out_features, 1, 1)
        self.m = (self.adj > 0)
        self.e = nn.Parameter(torch.zeros(out_features, len(self.m[0].nonzero()), dtype=torch.float))
        nn.init.constant_(self.e.data, 1)

        if bias:
            self.bias = nn.Parameter(torch.zeros(out_features, dtype=torch.float))
            stdv = 1. / math.sqrt(self.W.size(1))
            self.bias.data.uniform_(-stdv, stdv)
        else:
            self.register_parameter('bias', None)

    def forward(self, input):
        # (B*16*2)*(2*128) -->(B*16*128) -->B*1*16*128-->B*128*16*1
        h0 = torch.matmul(input, self.W[0]).unsqueeze(1).transpose(1, 3)  # B * C * J * 1
        h1 = torch.matmul(input, self.W[1]).unsqueeze(1).transpose(1, 3)  # B * C * J * 1

        adj = -9e15 * torch.ones_like(self.adj).to(input.device)  # C * J * J = 128*16*16
        adj[self.m] = self.e.view(-1)
        adj = F.softmax(adj, dim=2)

        E = torch.eye(adj.size(1), dtype=torch.float).to(input.device)
        E = E.unsqueeze(0).repeat(self.out_features, 1, 1)  # C * J * J
        output = torch.matmul(adj * E, h0) + torch.matmul(adj * (1 - E), h1)
        output = output.transpose(1, 3).squeeze(1)

        if self.bias is not None:
            return output + self.bias.view(1, 1, -1)
        else:
            return output

    def __repr__(self):
        return self.__class__.__name__ + ' (' + str(self.in_features) + ' -> ' + str(self.out_features) + ')'

注意unsqueeze和transpose操作

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值