深度强化学习实验室
来源:https://zhuanlan.zhihu.com/p/329810387
作者:祖守杰(DeepRL-Lab成员)
编辑:DeepRL
1. 前言
做推荐的同学应该多多少少听到过强化学习和推荐系统的结合可以擦出什么样的火花,二者的结合能给推荐领域带来怎么样的效果提升。从强化学习奖励最大化的思维方式来说,将强化学习用于推荐领域可谓是完美的契合了推荐的场景。但问题是如何将RL与推荐相结合一直是各大公司研究的热点。其中谷歌于2018年发表的论文《Top-K Off-Policy Correction for a REINFORCE Recommender System》在推荐领域引起了不小的反响。谷歌出品,必属精品。google发表的论文不管是在工业界还是学术界引起的反响不可小觑。由于笔者对强化学习和推荐相结合的领域一直有莫大的兴趣。在反复研读了这篇论文后,苦于论文的代码没有开源,于是一直致力于对论文的复现工作,经过长达10个多月的思考和翻阅资料后,终于在最近对论文的复现工作有了自己比较满意的成果,现将代码复现过程及心得与不足整理于此,希望能给广大做推荐的同学提供思路。鉴于笔者能力有限,其中有纰漏之处还请读者谅解并提出宝贵意见。
2. 代码及实现细节
注意:模型训练的数据来自于movie-len公开数据集,笔者尝试了该数据集的3类数据分别为ml-1M,ml-20m,ml-latest。先上几张各自的loss下降曲线:
从loss曲线来看,loss下降的还是不错的。说明模型在朝着正确的方向更新。下面进入正题。
读过该论文的同学应该知道,该论文的思路是使用强化学习著名的reinforce 算法来做推荐,然而,reinforce算法有个短板,她是一个on-policy算法,然后推荐场景下拥有大量的行为日志,是一个off-policy的场景,如何将二者结合起来呢,作者借鉴了PPO算法的思路(PS:不好意思,我不知道是谁借鉴谁),使用重要性采样的思路,采用2个网络来建模,一个PI策略,一个beta,beta策略根据行为日志数据提供important sampling中概率计算的功能。按照论文中的模型架构,主网络PI用来做决策,未来用来提供action,beta策略用来计算行为日志中每个p(a|s)的概率。原论文中采用CFN网络来计算下一个state,这里,笔者使用GRU网络来替代CFN网络的功能。网络结构代码定义如下:
代码语言:javascript复制
def _init_graph(self):
with tf.variable_scope('input'):
self.X = tf.placeholder(tf.int32,[None],name='input')
self.label = tf.placeholder(tf.int32,[None],name='label')
self.discounted_episode_rewards_norm = tf.placeholder(shape=[None],name='discounted_rewards',dtype=tf.float32)
self.state = tf.placeholder(tf.float32,[None,self.rnn_size],name='rnn_state')
cell = rnn.GRUCell(self.rnn_size)
with tf.variable_scope('emb'):
embedding = tf.get_variable('item_emb',[self.item_count,self.embedding_size])
inputs = tf.nn.embedding_lookup(embedding,self.X)
outputs,states_ = cell.__call__(inputs,self.state)# outputs为最后一层每一时刻的输出
print(outputs.shape,states_)
self.final_state = states_
# state = tf.reshape(outputs,[-1,self.rnn_size])#bs*step,rnn_size,state
state = outputs
with tf.variable_scope('main_policy'):
weights=tf.get_variable('item_emb_pi',[self.item_count,self.rnn_size])
bias = tf.get_variable('bias',[self.item_count])
self.pi_hat =tf.add(tf.matmul(state,tf.transpose(weights)),bias)
self.PI = tf.nn.softmax(self.pi_hat)# PI策略
self.alpha = cascade_model(self.PI,self.topK)
with tf.variable_scope('beta_policy'):
weights_beta=tf.get_variable('item_emb_beta',[self.item_count,self.rnn_size])
bias_beta = tf.get_variable('bias_beta',[self.item_count])
self.beta = tf.add(tf.matmul(state,tf.transpose(weights_beta)),bias_beta)
self.beta = tf.nn.softmax(self.beta)# β策略
label = tf.reshape(self.label,[-1,1])
with tf.variable_scope('loss'):
pi_log_prob, beta_log_prob, pi_probs = self.pi_beta_sample()
ce_loss_main =tf.nn.sampled_softmax_loss(
weights,bias,label,state,self.num_sampled,num_classes=self.item_count)
topk_correction =gradient_cascade(tf.exp(pi_log_prob),self.topK)# lambda 比值
off_policy_correction = tf.exp(pi_log_prob)/tf.exp(beta_log_prob)
off_policy_correction = self.weight_capping(off_policy_correction)
self.pi_loss = tf.reduce_mean(off_policy_correction*topk_correction*self.discounted_episode_rewards_norm*ce_loss_main)
# tf.summary.scalar('pi_loss',self.pi_loss)
self.beta_loss = tf.reduce_mean(tf.nn.sampled_softmax_loss(
weights_beta,bias_beta,label,state,self.num_sampled,num_classes=self.item_count))
# tf.summary.scalar('beta_loss',self.beta_loss)
with tf.variable_scope('optimizer'):
# beta_vars = [var for var in tf.trainable_variables() if 'item_emb_beta' in var.name or 'bias_beta' in var.name]
beta_vars = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES,scope='beta_policy')
self.train_op_pi = tf.train.AdamOptimizer(0.01).minimize(self.pi_loss)
self.train_op_beta = tf.train.AdamOptimizer(0.01).minimize(self.beta_loss,var_list=beta_vars)
beta网络是常规的多分类网络,由于在推荐场景下,分类数目就是item的个数,相当于是超大规模多分类问题,因此采用tf.nn.sampled_softmax_loss API来训练超大规模多分类问题,根据论文思路,beta网络只更新自己网络中embedding的参数,不更新主网络参数。而在PI网络的训练中,更新所在网络的全部参数。pi网络的loss是严格按照论文的思路来实现的,详细情况请参考论文。
代码语言:javascript复制# 论文中根据概率选动作,根据动作计算概率的代码,主要是pi loss中用到的代码
def pi_beta_sample(self):
# 1. obtain probabilities
# note: detach is to block gradient
beta_probs =self.beta
pi_probs = self.PI
# 2. probabilities -> categorical distribution.
beta_categorical = tf.distributions.Categorical(beta_probs)
pi_categorical = tf.distributions.Categorical(pi_probs)
# 3. sample the actions
# See this issue: https://github.com/awarebayes/RecNN/issues/7
# usually it works like:
# pi_action = pi_categorical.sample(); beta_action = beta_categorical.sample();
# but changing the action_source to {pi: beta, beta: beta} can be configured to be:
# pi_action = beta_categorical.sample(); beta_action = beta_categorical.sample();
available_actions = {
"pi": pi_categorical.sample(),
"beta": beta_categorical.sample(),
}
pi_action = available_actions[self.action_source["pi"]]
beta_action = available_actions[self.action_source["beta"]]
# 4. calculate stuff we need
pi_log_prob = pi_categorical.log_prob(pi_action)
beta_log_prob = beta_categorical.log_prob(beta_action)
return pi_log_prob, beta_log_prob, pi_probs
3. 美中不足
- 论文中说,训练主网络pi采用reward>0的(s,a)对,而训练beta网络使用所有的(s,a),而由于使用的数据集的局限性,在实现论文的时候并没有使用这种思路来训练网络。
- 由于强化学习框架的要求,使用强化学习时,reward是一个必不可少的条件,如果将此算法运用到推荐领域,在用户的历史序列中的item对应的reward该如何定义,最常见的做法是,点击的item reward定为1,未点击的定为0。有精力的同学可以尝试下。
- 原文中使用CFN来建模state,这里笔者采用的session-based-rnn的思想建模state,不得不感叹一下,session-based-rnn中训练RNN模型的思路确实给力,估计以后训练RNN相关模型都采用这种思路来训练了吧
- 原论文中在训练的时候加入了label的信息。由于能力有限,实在不知道该如何实现这种思路,正常来说,输入数据是不应该含有标签的信息的。
- 注意:tf.nn.sampled_softmax_loss API 在训练的时候,其中参数numsampled似乎和batch_size参数有关,在此代码中,20-100倍之间效果差不了多少。比如batch_size=4096,那么num_sampled可以为120.num_sampled的不同会影响loss曲线的下降。
个人认为:本论文给topK推荐提供了一种思路,如果仅仅是考虑top1推荐的话,同样数据训练的前提下,效果似乎没有session-based-rnn好。比如同样适用20m数据集,与session-based-rnn效果对比如下:
Recall@20 | MRR@20 | train time | |
---|---|---|---|
ession-based-rnn | 0.20232047783713317 | 0.07123638589710173 | 445m |
TOCR | 0.19942149398322467 | 0.052183558844622564 | 970m |
com/awarebayes/RecNN
完