从 2 年前的 LightGCN 代码学习到的一些知识点。
LightGCN 是 何向南 团队在 SIGIR 2020 上发表的一篇论文,目前看还是有一定影响力的。 之前简单看了 paddlepaddle pgl 的实现,看起来很简单; 最近想看看从头写一个 GCN 是怎么写的, 于是看了下官方开源的代码: LightGCN-PyTorch,从中发现了一些之前不太关注的点。 这里记录下来,防止遗忘。
1. dropout 里的随机mask是如何实现的 ?
作为一个调包侠,平时我们都是直接使用底层库的 x.nn.dropout
类的函数,
最多知道 dropout 的做法:
- 训练时开启,随机 mask 掉
drop_prob
的连接,并把剩余节点的值放大1 / (1 - drop_prob)
倍来 保证结果经过 dropout 后期望不变(或者说量纲不变?)。 - 测试时,dropout 不激活
但因为 LightGCN 使用的是稀疏张量,大概是不能直接用针对稠密张量的 dropout, 因此需要手工实现一版。 实现如下:
def __dropout_x(self, x, keep_prob):
size = x.size()
# indices 默认是 2 x N 的,第一行是横坐标列表,第二行是纵坐标列表;
# 转置一下,就成了 Nx2, 每个元素就是一对坐标
index = x.indices().t()
values = x.values()
# 秀啊!
# rand是均一分布;
# v + keep_prob , 然后转 int,再转bool;
# - 在 1 - keep_prob 下的元素,int后就是0;
# - 而 1-keep_prob 就是1;
# 而 >= 1-keep_prob 的概率就是 keep_prob
# 简直黑人问号??
# 不过,是不是比choice / shuffle 等高效呢? 可能没那么明显?
random_index = torch.rand(len(values)) + keep_prob
random_index = random_index.int().bool()
#
index = index[random_index]
values = values[random_index]/keep_prob
g = torch.sparse.FloatTensor(index.t(), values, size)
return g
这个函数特别的点,在注释中已经说明。我们重点关注其中对节点的mask:先 rand 一个 [0, 1]
的概率向量,然后通过 +keep_prob 和 int 截断,达成了按指定概率生成 mask 向量的目标。
如注释所言,这个做法其实理解有点绕。我又看了下几个库的实现:
-
TF
nn.dropout
见 nn_ops.dropout, 关键逻辑:
# Sample a uniform distribution on [0.0, 1.0) and select values larger # than or equal to `rate`. random_tensor = uniform_sampler(shape=noise_shape, dtype=x_dtype) keep_mask = random_tensor >= rate
可以看到,这个逻辑其实和 LightGCN 是一致的,但显然这个实现直接啊……
-
PyTorch dropout
torch 找最底层实现还有点难,在仓库里找了下,不确定这个 caff2/op/dropout.cc 是不是CPU上的实现:
for (int i = 0; i < X.numel(); ++i) { mask_data[i] = dist(gen) > 0.5; // NOLINTNEXTLINE(cppcoreguidelines-narrowing-conversions,bugprone-narrowing-conversions) Ydata[i] = Xdata[i] * scale * mask_data[i]; }
这才是最基本的实现啊,符合直觉…… 看还有 CUDA 的op,但是直接调的是 CuDNN的实现,这里就不先不深究了……
由上,看出来 LightGCN 的 dropout 里的随机mask,其实不是那么直观的。
附:按我自己之前的想法,我会怎么实现这个随机 mask 呢?
如前面注释所言,我想的其实是直接用 choice 取1 - drop_prob
比例个元素; 或者 shuffle 全部全素后再取Top1 - drop_prob
个。 这个更符合『随机选择』的直觉,但显然不符合NN里面一贯的 mask 的思想。
最后,还额外想到了如何来实现shffle? 之前在word2vec
里看到过,有点忘了。网上search一下,就是它,如此巧妙: 名为Knuth-Shuffle
或者 Fisher-Yates shuffle.
2. BPR loss 怎么用 softplus 实现?
LightGCN 使用的 loss 是 BPR loss
. BPR loss 的公式为
而在 LightGCN 中, BPR loss 的计算为
\[\operatorname{bpr}(pos, neg) = \operatorname{softplus}(neg - pos)\]这是为何?
原理其实很简单——二者是等价的:
\[\begin{align} \operatorname{softplus}(x) &= \ln(1 + \exp(x)) \\ \operatorname{sigmoid}(x) &= \frac{1} {1 + \exp(-x)} \\ \operatorname{softplus}(x) &= - \ln \frac{1}{1 + \exp(x)} \\ &= -\ln \operatorname{sigmoid}(-x) \end{align}\]这个可以作为结论记住:用 softplus
来实现 BRP loss.