接下来我花一天时间精读了论文《Learning Fair Representations for Recommendation: A Graph-based Perspective》[1],将论文的结构和核心思想进行了详细地梳理,之后准备使用Pytorch框架对该论文进行复现。
论文创新点
该论文有两个要点,其一个是使用生成对抗网络(GAN)训练的滤波器对原始的用户-物品embeddings向量进行转换,以除去用户的敏感信息(该论文假定原始嵌入算法不可修改,只能在已经生成的embeddings向量上做转换);其二是在GAN的优化目标函数(被称为价值函数)中加入用户-物品二分图的信息,以充分利用用户和物品的关系。除此之外,该论文通篇都散发着表示学习(representation learning)[2]思想的光辉。
背景知识储备
因为该论文使用了GAN网络,我先查阅了Goodfellow的论文《Generative Adversarial Nets》cite{gan}对GAN网络进行了复习。
GAN的主要思想如下:给定样例数据( extbf{x}),我们想学习得到数据的生成分布(p_{data}( extbf{x}))。我们定义一个先验噪声变量(p_{ extbf{z}}( extbf{z})),再定义生成器(G( extbf{z}; heta_{g}))根据噪声数据(z)产生“伪造”样本,这里(G)可以是一个由多层感知机(MLP)表示的可微函数,参数为( heta_{g})。我们训练判别器(D(x; heta_{d}))指示样本(x)是真实训练样本而不是“伪造”样本的概率。
我们训练判别器(D)极大化(log D(x))和(log(1-D(G( extbf{z})))),以期求“存真去伪”;同时训练生成器(G)来极小化(log(1-D(G( extbf{z})))),以期求“以假乱真”。也就是说,(D)和(G)进行双人的极小极大(minimax)博弈,价值函数为:
接下来是训练部分,我们设置(k)轮迭代。设生成器产生的样本分布为(p_{g}),我们假定模型会逐渐收敛到全局最优点,即(p_{g}=p_{data})。每轮迭代我们从噪声先验(p_{ extbf{z}}( extbf{z}))采(m)个噪声样本{ ( extbf{z}^{(1)},ldots extbf{z}^{(m)}) },从数据生成分布(p_{data}( extbf{x}))中采(m)个小批量(minibatch)样例{( extbf{x}_{(1)},ldots extbf{x}_{(m)})}。
然后我们按照随机梯度上升的方式更新判别器参数( heta_{d}):
然后再次从噪声先验(p_{ extbf{z}}( extbf{z}))采m个小批量噪声样本{ ( extbf{z}^{(1)},ldots extbf{z}^{(m)}) },并按照随机梯度下降的方式更新生成器的参数( heta_{f}):
具体的基于梯度的参数更新法则不限,可以使用任何标准的基于梯度的学习法则,原始论文中用的momentum优化算法(接下来我复现的时候用的Adam优化算法)。
论文理解与复现
接下来我们开始编写代码并进行实验。
我们采用的是Lastfm-360K数据集,该数据集包含用户-物品图结构的邻接矩阵。因为我们的算法主要用户和物品的嵌入向量上进行转换,故我们从网上直接下载经过图卷积网络得到的用户和物品的embeddings向量。用户和物品的embeddings向量都以.npy矩阵格式存储,大小分别为1.5MB和1MB。
接下来我们定义属性滤波器(做为生成器)和判别器的细节。属性过滤器我们直接采用多层感知机(MLP)进行实现。该MLP的输入维度和输出维度都是用户的embeddings向量的维度。其输出可以理解为属性滤波器力求“伪造”的不含敏感属性的用户embeddings,以“骗过”判别器。我们定义了两个隐藏层,每个隐藏层的激活函数采用LeakyReLU激活函数。LeakyReLU实在ReLU激活函数之上的改进,它在负输入值段的函数梯度(lambda)是一个取自连续性均匀分布(U(l, u))概率模型的随机变量,即
其中(lambdasim U(l, u)),(l<u),且(l,u in [0,1))。这样可以保证激活函数在负输入段也有梯度,可以有效避免梯度消失问题。属性滤波器的核心网络架构如下:
self.net = nn.Sequential(
nn.Linear(self.embed_dim, int(self.embed_dim*2), bias=True),
nn.LeakyReLU(0.2,inplace=True),
nn.Linear(int(self.embed_dim*2), self.embed_dim, bias=True),
nn.LeakyReLU(0.2,inplace=True),
nn.Linear(self.embed_dim, self.embed_dim , bias=True),
)
在判别器方面,因为我们需要训练出三个不同的属性滤波器,分别对用户的性别(gender)、年龄(age)、职业(occupation)这三种敏感属性进行滤波,所以我们分别定义性别判别器、年龄判别器、职业判别器三种网络,以训练三种不同的属性滤波器。判别器也采用MLP实现,其输入维度维度为用户embeddings向量的维度,输出维度是一个softmax概率分布,指示样本属于属性空间中各类别的概率,据此来判定属性滤波器所生成滤波后样本的质量。
我们设输入的用户或物品的embeddins向量为( extbf{e}_{i}),生成器为(mathcal{F}),判别器为(mathcal{D}),这样我们可以定义地(k)个滤波器在第(i)个用户向量的输出是(mathcal{F}( extbf{e}_{i}))。(mathcal{F}( extbf{e}_{i}))由调用滤波器类的forward方法返回。判别器的核心网络架构如下,以用户性别属性判别器为例:(注:softmax函数在forward方法中调用)
self.net = nn.Sequential(
nn.Linear(self.embed_dim, int(self.embed_dim/4), bias=True),
nn.LeakyReLU(0.2,inplace=True),
nn.Linear(int(self.embed_dim/4), int(self.embed_dim/8), bias=True),
nn.LeakyReLU(0.2,inplace=True),
nn.Linear(int(self.embed_dim /8), self.out_dim , bias=True),
nn.LeakyReLU(0.2,inplace=True),
nn.Linear(self.out_dim, self.out_dim , bias=True)
)
我们定义InforMax类做为我们的GAN模型的总体架构,该类的主要目的是计算属性滤波器和判别器的价值函数。InforMax类中包含我们做为属性滤波器的三个Filter(对于用户的性别、年龄、性格)和对应的三个做为判别器的Discriminator。最终对于嵌入向量(e_{i})我们计算其滤波后的向量为(f_{i}=sum_{k=1}^{K}mathcal{F}^{k}(e_{i})/K)。然后我们再考虑到用户-物品图结构的信息,可以定义价值函数(V_{R})表示为训练样本数据中rating(用户的评分等级)分布的对数似然,价值函数(V_{G})表示成我们预测的属性分布的对数似然,如下所示:
这个类的forward方法中同时返回GAN中的价值函数(V_{G})和价值函数(V_{R})。核心部分代码如下:
w_f=[1,2,1]
d_loss = (
d_mask[0]*d_loss1*w_f[0]+ d_mask[1]*d_loss2*w_f[1]
+ d_mask[2]*d_loss3*w_f[2])
d_loss_local = (
d_mask[0]*d_loss1_local*w_f[0]+ d_mask[1]*d_loss2_local*w_f[1]
+ d_mask[2]*d_loss3_local*w_f[2])
#L_R preference prediction loss.
user_person_f = user_f2_tmp
item_person_f = item_f2_tmp
user_b = F.embedding(user_batch,user_person_f)
item_b = F.embedding(item_batch,item_person_f)
prediction = (user_b * item_b).sum(dim=-1)
loss_part = self.mse_loss(prediction,rating_batch)
l2_regulization = 0.01*(user_b**2+item_b**2).sum(dim=-1)
loss_p_square=loss_part+l2_regulization.mean()
d_loss_all= 1*(d_loss+1*d_loss_local)
g_loss_all= 10*loss_p_square
g_d_loss_all = - 1*d_loss_all
#the loss needs to be returned.
d_g_loss = [d_loss_all,g_loss_all,g_d_loss_all]
最后我们定义模型训练模块。我们定义两个Adam优化器f_optimizer和d_optimizer分别对最后我们定义模型训练(优化)模块。在原始论文中,作者定义了生成器(mathcal{F})和判别器(mathcal{D})对价值函数(V(mathcal{F}, mathcal{D}))的极大极小化:
这里(lambda)是一个平衡参数来平衡(V_{R})和(V_{G})这两个价值函数。如果(lambda)等于0,那么我们公平性的需求就消失了。
这里我们采用两次遍历训练集的方式来实现:
第一次遍历的每一轮迭代,调用InforMax模型的forward方法取得属性滤波器需要优化的价值函数部分,并调用backward方法方向传播计算梯度后,用Adam优化器分别对属性滤波器进行优化。
第二次遍历的每一轮迭代,调用Informax模型的forward方法取得判别器需要优化的价值函数部分,并调用backward方法反向传播计算梯度后,调用Adam优化器对判别器进行优化。
就这样,我们采取了两个优化器分别往正梯度和负梯度方向对判别器和属性滤波器进行优化。核心部分代码如下:
loss_current = [[],[],[],[]]
for user_batch, rating_batch, item_batch in train_loader:
user_batch = user_batch.cuda()
rating_batch = rating_batch.cuda()
item_batch = item_batch.cuda()
d_g_l_get = model(copy.deepcopy(pos_adj),user_batch,rating_batch,item_batch)
d_l,f_l,_ = d_g_l_get
loss_current[2].append(d_l.item())
d_optimizer.zero_grad()
d_l.backward()
d_optimizer.step()
for user_batch, rating_batch, item_batch in train_loader:
user_batch = user_batch.cuda()
rating_batch = rating_batch.cuda()
item_batch = item_batch.cuda()
d_g_l_get = model(copy.deepcopy(pos_adj),user_batch,rating_batch,item_batch)
_,f_l,d_l = d_g_l_get
loss_current[0].append(f_l.item())
f_optimizer.zero_grad()
f_l.backward()
f_optimizer.step()
总结
因为该篇论文的模型较为复杂,实现的工程量比较大。目前我只是初步完成了论文核心部分的代码编写,有一些细节还没有完成,代码也还没有经过调试。我准备在未来两天完成代码的编写与调试。
目前遇到的最大的难点主要在于两点:
一是理清论文的逻辑思路和各模型组分之间的从属、先后关系,这要求我们充分把握整个模型的架构,然后再进行模块化编程;
二是搞清楚论文中每一个变量的含义。模型的关键部分,也就是价值函数部分,原始论文写得十分含糊,我也是反复来回咀嚼了几次原始论文,并在网上查询了其他人对论文的解读(这里又得感谢一下人大赵鑫老师AIBox实验室的博客文章),然后才充分理解了论文想表达的价值函数的定义。不过我相信我在这个地方花费的功夫是值得的,因为如果一个深度学习模型的优化函数都不对,那么显然它就无法完成我们的任务需求了,甚至南辕北辙。
参考文献
- [1] Wu L, Chen L, Shao P, et al. Learning Fair Representations for Recommendation: A Graph-based Perspective[C]//Proceedings of the Web Conference 2021. 2021: 2198-2208.
- [2] Goodfellow I, Bengio Y, Courville A. Deep learning[M]. MIT press, 2016.
- [3] Goodfellow I, Pouget-Abadie J, Mirza M, et al. Generative adversarial nets[J]. Advances in neural information processing systems, 2014, 27.