TPGR代码详解 Large-Scale Interactive Recommendation with Tree-Structured Policy Gradient

本文主要是介绍TPGR代码详解 Large-Scale Interactive Recommendation with Tree-Structured Policy Gradient,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

论文阅读笔记链接
github代码链接

目录

  • 论文内容
    • MDP过程
    • 训练TPGR过程:
    • 采样过程
  • 结构讲解
  • DEBUG过程
  • 代码详解
    • 整体流程
    • run函数
    • 1. PRE_TRAIN()
      • init()
      • make_graph()
      • train()
      • evaluate
    • 一些问题记录
      • 参数相关
    • 2. Tree()
      • init()
      • construct_tree()
        • build_mapping()
        • hierarchical_code()
        • pca_clustering()
    • 3. TPGR()
      • init()
      • make_graph()
      • evaluate
      • train
  • 补充知识
    • 函数
    • GRU
    • 报错

论文内容

论文阅读笔记

MDP过程

  1. state:用户历史交互记录(itemID编码为embedding,reward编码为one-hot)经过SRU编码后拼接上user_status(给正/负反馈的数量,给连续正/负反馈的数量)
  2. action:选择为用户 i i i推荐的产品 j j j
  3. reward: R ( s , a ) = r i , j + α ( c p − c n ) R(s,a)=r_{i,j}+\alpha(c_p-c_n) R(s,a)=ri,j+α(cpcn),前者表示环境模拟器中标准化[-1~1]后的分数,后者表示连续正反馈数量减去连续负反馈数量。会映射到10维的向量。

训练TPGR过程:

  1. 给定孩子数 c c c和所有物品的 e m b e d d i n g embedding embedding(rating,VAE, MF),建立一颗层次聚类树。
  2. 算出非叶节点数量 L L L,对应策略网络数量,进行随机初始化。
  3. 根据Sampling Episode采样出一条路径 ( s 1 , p 1 , r 1 . . . . s n , p n , r n ) (s_1, p_1, r_1....s_n, p_n, r_n) (s1,p1,r1....sn,pn,rn),通过连乘选路径时做选择的概率( π θ {\pi}_{\theta} πθ输出做某个选择的概率),将路径映射到动作 a t a_t at
  4. 使用策略梯度下降进行训练,已知 ( a t , s t , G ) (a_t,s_t,G) (at,st,G) G G G为累积收益。

采样过程

  1. s t a t e state state输入 d d d个策略网络(物品id映射为embedding,reward经过one-hot编码,输入SRU,再与user_status拼接,得到state),做 d d d次选择,每次从 1 ∼ c 1\sim c 1c中选一个,同时记录下每次选择的 n o d e _ i n d e x node\_index node_index
  2. p t = ( c 1 , c 2 , . . . , c d ) p_t=(c_1,c_2,...,c_d) pt=(c1,c2,...,cd),将 p t p_t pt映射到物品 a t a_t at,获取 r t r_t rt(将物品推荐给用户了,有反馈)。
  3. 根据状态转移函数得到 s t + 1 s_{t+1} st+1,重复上述过程,直到得到 E = ( s 1 , p 1 , r 1 . . . . s n , p n , r n ) E=(s_1, p_1, r_1....s_n, p_n, r_n) E=(s1,p1,r1....sn,pn,rn)
    state表示

代码顺序:

  1. 先预训练rnn(recommender调用tpgr中的PRE_TRAIN)
  2. 再建树(recommender调用tpgr中的Tree)
  3. 最后训练TPGR(recommender调用tpgr中的TPGR)

结构讲解

在github下载代码解压后,可以看见目录如下
在这里插入图片描述

  • data目录中的rating存放的是数据集,形式是:user item rating;可以根据需要替换成自己的数据集。
  • src目录就是所有代码啦

看代码前我们先看看readme文件,如果某个代码中有Readme一定要先看看哦,里面通常包括了整个项目的简单介绍,包括一些参数设置。
其README文件和解释如下:

  1. run_time_tools文件中的mf_with_bias()使用PMF获取item的embedding,将结果item_embeddings_value存在rating_file(config文件定义的)中。
In run_time_tools.py, mf_with_bias() is to gain embeddings of items by utilizing the PMF model; 
  1. clustering_vector_constructor()按照rating/vae/mf,建立平衡层次聚类树,存在result_file_path中。
clustering_vector_constructor() is to construct item representation in order to building the balanced hierarchical clustering tree.
  1. tpgr.py中有三个类,PRE_TRAIN:rnn预训练; TREE:构建树; TPGR:TPGR训练。
In tpgr.py, class PRE_TRAIN, TREE and TPGR correspond to the rnn pretraining step, tree constructing step and model training step of the TPGR model.
  1. config参数如下,具体含义都写出来了,就不解释了:
[META]
ACTION_DIM: the embedding dimension of each action (item).
STATISTIC_DIM: the dimension of the statistic information, in our implementation, there are 9 kinds of statistic information.
REWARD_DIM: the dimension of the one-hot reward representation.
MAX_TRAINING_STEP: the maximum training steps.
DISCOUNT_FACTOR: discount factor for calculating cumulated reward.
EPISODE_LENGTH: the number of recommendation interactions of each episode.
LOG_STEP: the number of interval steps for printing evaluation logs when training the TPGR model.[ENV]
RATING_FILE: the name of the original rating file, the rating files are stored in data/rating as default.
ALPHA: to control the ratio of the sequential reward as described in the paper.
BOUNDARY_RATING: to divide positive and negative rating.
MAX_RATING: maximum rating in the rating file.
MIN_RATING: minimum rating in the rating file.[TPGR]
PRE_TRAINING: bool type (T/F), indicating whether conducting pretraining step.
PRE_TRAINING_STEP: the number of pretraining steps. # 30
PRE_TRAINING_MASK_LENGTH: control the length of the historical rewards to recover when pretraining. # 32
PRE_TRAINING_SEQ_LENGTH: the number of recommendation interactions of each episode when pretraining. # 64
PRE_TRAINING_MAX_ITEM_NUM: the maximum number of items to be considered when pretraining. # 64
PRE_TRAINING_RNN_TRUNCATED_LENGTH: rnn truncated length. # 32
PRE_TRAINING_BATCH_SIZE: the batch size adopted in pre-training step.
PRE_TRAINING_LOG_STEP: the number of interval steps for printing evaluation logs when pre-training.
PRE_TRAINING_LEARNING_RATE: initialized learning rate for AdamOptimizer adopted in pre-training step.
PRE_TRAINING_L2_FACTOR: l2 factor for l2 normalization adopted in pre-training step.CONSTRUCT_TREE: bool type (T/F), indicating whether conducting tree building step.
CHILD_NUM: children number of the non-leaf nodes in the tree.
CLUSTERING_TYPE: clustering type, PCA or KMEANS.
CLUSTERING_VECTOR_TYPE: item representation type, RATING, VAE or MF.
CLUSTERING_VECTOR_VERSION: the version of the item representation.RNN_MODEL_VS: the version of the pretrained rnn.
TREE_VS: the version of the tree.LOAD_MODEL: bool type (T/F), indicating whether loading existing model.
MODEL_LOAD_VS: the version of the model to load from.
MODEL_SAVE_VS: the version of the model to save to.HIDDEN_UNITS: the hidden units list for each policy network, seperated by ','.
SAMPLE_EPISODES_PER_BATCH: the number of sample episodes for each user in a training batch.
SAMPLE_USERS_PER_BATCH: the number of sample users in a training batch.
EVAL_BATCH_SIZE: batch size of episodes for evaluation.LEARNING_RATE: initialized learning rate for AdamOptimizer.
L2_FACTOR: l2 factor for l2 normalization.
ENTROPY_FACTOR: to control the smooth of the possibility distribution of the policy.

DEBUG过程

  1. 运行时,如果出现缺少包而报错,则使用conda install packagename安装包。
  2. 安装完需要的包后,运行程序,发现报错FileNotFoundError: [Errno 2] No such file or directory: '../data/run_time/demo_data_env_objects'
    找到对应语句:
# if you replace the rating file without renaming, you should delete the old env object file before you run the code
env_object_path = '../data/run_time/%s_env_objects'%self.config['ENV']['RATING_FILE']####省略了很多代码行###
# dump the env object
utils.pickle_save({'r_matrix': self.r_matrix, 'user_to_rele_num': self.user_to_rele_num}, env_object_path)

找到utils中“pickle_save”函数

def pickle_save(object, file_path):f = open(file_path, 'wb')pickle.dump(object, f)
  • pickle提供了一个简单的持久化功能。可以将对象以文件的形式存放在磁盘上。

发现data目录下不存在run_time这个文件夹,于是手动创建一个。

  1. 创建目录了,还是有报错:Ran out of input。找到报错位置,还是和pickle相关:
def pickle_load(file_path):f = open(file_path, 'rb')return pickle.load(f)  

原因:load的文件为空,就会出现这种错误。发现上一步并未成功保存文件。
保存时报错'module' object is not callable,检查后发现pickle.dump写错了(上面是正确的)

  1. 总之就是遇到bug就一个一个改,就可以跑起来啦:
    在这里插入图片描述

代码详解

这部分讲解整个代码框架。

整体流程

  1. main函数使用configparse函数解析配置文件
  2. 创建Recommender实例,除了初始化函数只有一个run函数
  3. 调用Recommender中的run函数

run函数

首先获取配置文件参数值,之后:

  1. 判断是否需要预训练,调用tpgr.py进行预训练
        if self.config['TPGR']['PRE_TRAINING'] == 'T':log.log('start pre-training', True)pre_train = PRE_TRAIN(self.config, self.sess) # TPGR中的类for i in range(pre_training_step):pre_train.train()log.log('end pre-training', True)
  1. 判断是否需要构建树,tpgr.py进行构建
        if self.config['TPGR']['CONSTRUCT_TREE'] == 'T':log.log('start constructing tree', True)tree = Tree(self.config, self.sess)tree.construct_tree()log.log('end constructing tree', True)
  1. 训练TPGR模型
log.log('start training tpgr', True)tpgr = TPGR(self.config, self.sess)for i in range(0, max_training_step):if i % log_step == 0:tpgr.evaluate()log.log('evaluated\n', True)tpgr.train()log.log('end training tpgr')

可以看到,每一部分都用到了tpgr中的类,下面我们看看tpgr文件的构成。tpgr由三个类构成:
在这里插入图片描述

1. PRE_TRAIN()

先创建类pre_train = PRE_TRAIN(self.config, self.sess),再训练pre_train.train()
目的预训练rnn参数,获得更好的state。输出是:W(SRU(action_embedding,reward,pre_statistic),pre_statistic)+b,groundtruth是reward。
共有以下几个函数:

init()

  1. init(): 主要是从config文件中获取参数,创建batch_size个Env。
    self.env = [Env(self.config, self.user_num, self.item_num, self.r_matrix, self.user_to_rele_num) for i in range(max(self.user_num, self.batch_size))]创建多个env实例。
    然后调用make_graph()

Env():创建对象时,读取config文件内容(action维度,episode长度等);加载数据集,存储user_num(用户数), item_num, r_matrix(存储评分矩阵), user_to_rele_num(用户u给出的高分数)等信息。(其中 item_num, r_matrix 存储在env_object_path中(‘…/data/run_time/demo_data_env_objects’))
在这里插入图片描述
然后通过run_time_tools.mf_with_bias(self.config),获取item_embedding,并存到item_embedding_file_path(‘…/data/run_time/demo_data_item_embedding_dim8’)。(如果之前运行过,就直接loda item_embedding_file_path就好啦)
在这里插入图片描述

run_time_tools.mf_with_bias()主要过程:使用训练数据(80%),定义并训练user_embs和item_embs矩阵,使得二者相乘的结果接近rating真实值,将item_embeddings作为结果(和MF差不多)。
后续tpgr.py会用它查表,获取item embedding,作为rnn的一个输入。建立平衡树的时候,会用到item embedding,如果是MF-based方法,也会用到它。

Env类其他函数:

  1. get_init_datareturn self.user_num, self.item_num, self.r_matrix, self.user_to_rele_num 这四个刚好是env需要的初始化输入
  2. resetself.user_id = user_id,其他属性设为0
  3. get_relevant_item_numreturn self.user_to_rele_num[self.user_id] 返回该用户评分高于3.5的item数量
  4. get_reward :初始化reward = [0.0, False];传入item_id,根据get_rating得到item评分,进行归一化处理得到reward[0],并按照reward[0]大小统计各种变量(con_neg_count等)。最后reward[0] += self.alpha * sr # sr是高分数量减去低分数量;item全部遍历后或者达到episode_length时,reward[1] = True
  5. get_statistic :获取参数,如all_neg_count(获取reward时就更新过了),这部分和rnn输出拼接起来作为state
  6. get_rating: 从评分矩阵返回评分 self.r_matrix[self.user_id, item_id]
  7. 属性:在这里插入图片描述

make_graph()

初始化时就调用了,主要是定义了整个前向传播过程,loss函数定义。

  1. 首先建立了多个placeholder(action, rnn_state, reward, statistic),长度为rnn预训练长度32。从env中获取action_embeddings(也就是PMF分解得到的物品embedding);
  2. 调用create_sru,输入self.rnn_input_dim, self.rnn_output_dim:对W,b等参数进行初始化,运算过程函数。返回self.rnn(unit函数,定义遗忘门,重置门,记忆单元;返回隐藏层和记忆单元拼接), self.rnn_variables(5个参数)
	# 创建SRU#init_matrix表示从“服从指定正态分布的序列”中随机取出指定个数的值Wf = tf.Variable(self.init_matrix([rnn_input_dim, rnn_output_dim])) bf = tf.Variable(self.init_matrix([rnn_output_dim]))Wr = tf.Variable(self.init_matrix([rnn_input_dim, rnn_output_dim]))br = tf.Variable(self.init_matrix([rnn_output_dim]))U = tf.Variable(self.init_matrix([rnn_input_dim, rnn_output_dim]))sru_variables = [Wf, Wr, U, bf, br]def unit(x, h_c):pre_h, pre_c = tf.unstack(h_c)# forget gatef = tf.sigmoid(tf.matmul(x, Wf) + bf)# reset gater = tf.sigmoid(tf.matmul(x, Wr) + br)# memory cellc = f * pre_c + (1 - f) * tf.matmul(x, U)# hidden stateh = r * tf.nn.tanh(c) + (1 - r) * xreturn tf.stack([h, c]) # 拼接起来,输出隐藏层和记忆层  
  1. 进行查表操作,找到每一个action对应的嵌入(8维,共32个);将reward映射为one-hot形式,维度为reward_dim。构造rnn输入pre_ars:拼接action embedding + reward + pre_statistic,27维,32个;初始隐藏层,初始化为0了(2,batch_size,27)。
  2. SRU输出tmp_state = self.rnn(self.pre_ars[i], tmp_state)循环32次,结果保存在cur_rnn_states_list,32个(2,?,27) 其中2代表[h,c],27是维度。
  3. 整体训练过程了! W ( S R U ( ) , s t a t i c ) + b W(SRU(),static)+b W(SRU(),static)+b定义variable变量,W(36,64) b(64,)其中64是max_item_num,36是 rnn_output_dim+statistic_dim。定义了l2损失。
  4. 预测值self.pn_outputs = [tf.matmul(tf.concat([self.cur_rnn_states_list[i][0], self.pre_statistic[i]], axis=1), self.W) + self.b for i in range(self.pre_train_truncated_length)] rnn输出和pre_statistic拼接起来,和w相乘再加上b,共32个值
  5. 真实值:placeholder (?,64); mask也是placeholder(有的地方被设置成0了)
  6. loss函数:设置了两种loss函数 all_zero_loss(将预测结果视为0矩阵)和loss((1.0 / tf.reduce_sum(self.mask)*求和{根号下(mask *(预测值-真实值))} 开根号)。一共也是32个loss。
self.all_zero_loss = tf.pow((1.0 / tf.reduce_sum(self.mask)) * tf.reduce_sum(tf.square(self.mask * (tf.cast(tf.constant(np.zeros(shape=[self.batch_size, elf.max_item_num])), dtype=tf.float32)self.expected_pn_outputs))), 0.5) # mask掉的是看不见的
self.loss = [tf.pow((1.0 / tf.reduce_sum(self.mask)) * tf.reduce_sum(tf.square(self.mask * (self.pn_outputs[i] - self.expected_pn_outputs))), 0.5)  for i in range(self.pre_train_truncated_length)]

train()

  1. 首先构造了batch_size个用户,每个用户产生了max_item_num个action,然后一个用户一个物品地训练
    ① 首先随机选取batch_size个用户id,每个用户都运行 self.env[i].reset(user_id):记下idself.user_id = user_id把各个属性重置为零(step_count,con_neg_count,con_zero_count,all_neg_count,history_items)。
    ② 定义action_value_list:batch_size个预训练action列表[1,2,3…64];然后打乱顺序;再取出前pre_train_seq_length列
    ③ 对每一个user env类(共batch_size个),调用env中函数获取reward:首先从评分矩阵返回对该物品的评分,并进行归一化,reward[0] += self.alpha * sr
  2. 循环pre_train_seq_length次:sampled_action = action_value_list[:, i],先取出一列action的值(相当于每个env对应的item);ars存储action(最终有64个list,长度为batch_size),reward,statistic(9个固定值除以32,all_neg_count,all_pos_count, con_neg_count等)。 第j个env中用户对应的item为sampled_action[j],ars存储反馈等值。 对batch_size用户采样64个item,并存储他们的reward,statistic
  3. 运行evaluate函数
  4. 训练模型,初始化ground_truth和mask_value,都是(batch_size,64)的矩阵,表示用户对第i个采样的反馈;运行pre_rnn_state_list,初始化为0
  5. 开始预训练了,循环64次。取出ars中的actions,rewards(长度为batchsize);存入ground_truth;初始化mask。
  6. 喂数据:pre_rnn_state_list[0],ground_truth,mask_value 以及ars数据。注意action是0~i+1,也就是第i次及其前面的action(一共传入i个action),reward都传进去。然后run pre_train_op最小化损失, cur_rnn_states_list[i]rnn前向传播过程。都是一轮一轮训练的哦!pre_rnn_state_list.append(rnn_state)
  7. 训练5轮就存储rnn_variables,即rnn的参数。这也是预训练的最终目的
    在这里插入图片描述
    注意!!训练长度64,但是是分了两部分(可能是因为episode=32?),前32和后32if i < self.pre_train_truncated_length。前32次,pre_actions长度逐渐递增,rnn输入之一pre_rnn_state一直是0,输出存起来了,训练pre_train_op[i]。后32次要一个一个取出来pre_rnn_state作为输入,ground_truth和mask_value计算方法没有变,变的是pre_actions(长度一直为32了):1-32;2-33…32-63作为pre,训练pre_train_op[31](都只训练最后一次,预测值也是只训练最后一个矩阵,因为输入都给完了)。整个64步都走完,才有pre_training_steps+1。这一整个步骤又会重复pre_training_step次。 最后保存参数

所以预训练rnn的目的是,输出结果拼接上固定值后经过全连接的结果与reward尽量相似。(有点不太明白,为什么这样就可以衡量state好坏呢)似乎是通过前i个序列(item embedding,reward,固定参数)预测第i个reward
真实值:一个矩阵,行数代表batch,列数代表采样该action获取的reward。预测值也是这样的一个矩阵,不过训练32次就有32个矩阵
有两个loss,all_zero_loss是干嘛的?应该是为了做对比

  1. init_matrix():return tf.random_normal(shape, stddev=0.1)
  2. _get_initial_ars()

evaluate

这里只是评估,并没有训练

  1. 首先和train一样,创建batch_size个用户,reset;创建action_value_list [0,1,2…,63],共batch_size个。

  2. 获取batch_size中用户对action_value_list的reward、statistic,用ars存储
    在这里插入图片描述

  3. 初始化ground_truth,mask_value,运行rnn_state(全为0)

  4. 循环64次,取出ars中的数据:① 取出第一轮采样的action(一共64轮,长度为batch_size) ② 为ground_truth赋值,存储对应action的对应reward。就是一个矩阵,batchsize行,64列(对应action)。 ③ mask_value,把ground_truth对应reward位置全改成0 ④ 当循环<32时,直接喂数据(第i列的action),并run cur_rnn_states_list,rnn前向传播过程,只运行一轮,传入27位的拼接向量以及隐藏层(0初始化),输出(2,?,27)

  5. 循环次数>=32时:pre_actions = ars[0][i - self.pre_train_mask_length] 然后把pre_actions内容对应的mask值改成0,再喂数据,run。这个地方是保证,能看见的数据长度只有32?

            feed_dict = {self.pre_rnn_state: rnn_state,self.expected_pn_outputs: ground_truth,self.mask: mask_value,self.pre_actions[0]: ars[0][i], # batchsize长度self.pre_rewards[0]: ars[1][i], self.pre_statistic[0]: ars[2][i]}rnn_state = self.sess.run(self.cur_rnn_states_list[0], feed_dict=feed_dict)
  1. run 损失函数,并输出
    之后进行训练,最后再调用evaluate,训练多次后,保存rnn参数utils.pickle_save(self.sess.run(self.rnn_variables), self.rnn_file_path + 's%d' % self.pre_training_steps)
    对rnn进行预训练后,之后就不用预训练啦~
    后面我就把运行预训练的部分注释掉啦~
    在这里插入图片描述

一些问题记录

参数相关

  1. pre_train_truncated_length(32,rnn预训练长度):action 的placeholder数量,也就是训练时要输入多少个action
    在这里插入图片描述

  2. max_item_num(64):决定了W和b的参数大小,有点像最后一层神经元个数。
    在这里插入图片描述
    action_value_list是由[1,2,3…64]组成的
    在这里插入图片描述

  3. pre_train_seq_length(64,预训练时每一轮的交互长度)代码中和max_item_num是一样的,用于控制action_value_list长度;控制采样多少次,ars长度

  4. pre_train_mask_length(32,在预训练时控制要恢复的历史奖励的长度)mask的都看不见了
    在这里插入图片描述

2. Tree()

先初始化tree = Tree(self.config, self.sess) ,再调用建树函数tree.construct_tree()
将item_embedding(VAE,rating,MF)聚类(k-means-based, PCA-based),结果文件包括id_to_code, code_to_id,dataset,child_num,clustering_type。
id_to_code :[父节点位置,叶节点位置], code_to_id:叶节点在最后一层的位置对应的一开始在数据集中的位置。

init()

init():定义各种参数,如非叶节点子节点数量child_num(伪代码中是给出总item数和深度d,计算c;实际代码直接给出了c),聚类方式PCA,clustering_vector_file_path,tree_file_path。创建env对象。
根据总item数和c,计算深度:self.bc_dim = int(math.ceil(math.log(self.env.item_num, self.child_num)))

construct_tree()

主要是调用 build_mapping()函数:id_to_code, code_to_id = self.build_mapping(),并将输出id_to_code,code_to_id,dataset,child_num,clustering_type转为obj并存储utils.pickle_save(obj, self.tree_file_path)
在这里插入图片描述

build_mapping()

主要是两个映射。

  1. 初始化全为0的数组 id_to_code,[item_num, bc_dim] 这个相当于辈分列表
  2. 获取聚类需要的物品embedding(rating,VAE,MF):run_time_tools.clustering_vector_constructor(self.config, self.sess),将结果id_to_vector(user num * item num)存入clustering_vector_file_path。(如果之前保存过结果,就直接打开文件就好了)
    在这里插入图片描述
  3. 调用hierarchical_code(self, item_list, code_range, id_to_code, id_to_vector函数,首先传入:从0 ∼ \sim 物品数组成的list,(0 ∼ c h i l d _ n u m b c _ d i m \sim {child\_num}^{bc\_dim} child_numbc_dim)组成的tuple,id_to_code(会被修改), id_to_vector。经过内部的递归调用,id_to_code会变化,是一个
  4. code_to_id :code_to_id = self.get_code_to_id(id_to_code) 返回一个一维数组,总长度代表叶节点数量,值表示一开始在数据集中的位置。也就是item在叶节点编号对应的数据集中的id
    def get_code_to_id(self, id_to_code):result = -np.ones(shape=[int(int(math.pow(self.child_num, float(self.bc_dim))))], dtype=int)for i in range(len(id_to_code)):code = id_to_code[i]result[self.get_index(code)] = iprint('leaf num count: %d\nitem num count: %d'%(len(result), int(np.sum([int(item>=0) for item in result]))))return result
  1. 返回(id_to_code, code_to_id)
hierarchical_code()

先是100个物品传入,通过聚类分成十个一组,共10个列表 --> 再分别将十个列表传入,通过聚类分成一个一组,共10个列表 --> 再将一个一组的列表传入,这时就会修改id_to_code。
在这里插入图片描述

id_to_code:是一个二维数组,第一列代表父节点在整棵树的位置,第二维代表当前节点在当前子树的位置(有点类似于排辈分)。比如[5,0]就代表当前节点的爸爸是他那一辈排第五的,而当前节点在他这一辈是老大,排第一。

  1. 先判断item_list长度,如果为1,就要改变id_to_code:id_to_code[item_list[0]] = self.get_code(code_range[0]),并返回“辈分排名”。
  2. 传入item_list和id_to_vector,进行聚类pca_clustering(),返回结果item_list_assign,是10个列表(每个列表是n个一组的item list(n=总长度除以孩子数10))。如传入100个物品,就会返回10个list,每个list有10个元素;最后一次是传入10个物品,返回10个长度为1的list。
  3. 开始递归调用,重复十次(因为分成了10类)
 for i in range(self.child_num): # 10个组里,每个组都继续递归调用;直到len(item_list)=1self.hierarchical_code(item_list_assign[i], (code_range[0]+i*range_len, code_range[0]+(i+1)*range_len), id_to_code, id_to_vector)

① 先是把分好的十个组一个一个放进去调用hierarchical_code,传入的第二个参数代表传入数据的起始位置((0,10),(10,20)…(90,100))
② 长度为10的列表传入后,还是先进行聚类,这次返回的是[[83], [95], [20], [92], [65], [58], [98], [42], [94], [90]]。(返回10个list);然后继续传入hierarchical_code,这次item_list长度为1。
③ 当传入递归的列表长度为1时,要修改id_to_code了!运行id_to_code[item_list[0]] = self.get_code(code_range[0]),传入当前在原始数据集中的位置。id_to_code[item]等于返回的二元数组

    def get_code(self, id):code = np.zeros(dtype=int, shape=[self.bc_dim])for i in range(self.bc_dim):c = id % self.child_numcode[self.bc_dim-i-1] = cid = int(id / self.child_num)if id == 0:breakreturn code
pca_clustering()
  1. 输入为(0~item_num)的列表以及每个item对应的向量;首先找到每个item对应的向量,然后调用sklearn中的pca.fit(data)
  2. 然后建立投影,w是pca中的参数(代表所保留的特征个数):item_to_projection = [(item, np.dot(id_to_vector[item], w)) for item in item_list](这里像是按聚类相似度排序?),然后按照从小到大排序存入result,然后10个一组(10也是算出来的,传入的物品数除以孩子数,int(math.ceil(len(result) * 1.0 / self.child_num)) item_list_assign.append([result[j][0] for j in range(start, end)])

三种聚类方式: kmeans_clustering(),random_clustering(),pca_clustering()

小小总结一下,这里就是建立平衡层次聚类树的过程,首先根据item embedding(三种获取方式)进行聚类,然后不停递归调用,建立聚类层次树。还有两个映射操作id_to_code :[父节点位置,叶节点位置], code_to_id:item在叶节点中的编号对应在数据集中的id

3. TPGR()

先创建对象,再运行evaluate函数。tpgr = TPGR(self.config, self.sess), tpgr.evaluate(),再train
在创建变量时可以看出,分成了训练集和评估集

init()

定义了损失函数哦

  1. 首先是从配置文件中读取参数,读取前面保存的的obj文件(rnn参数,建树的结果)
  2. 建立了env对象,forward_env = Env(self.config),有很多数据集中的属性,以及使用PMF获取embedding的函数、获取reward的函数。
  3. 使用刚刚初始化env对象时的值,再次创建64000个env对象。
  4. layer_units = [self.statistic_dim + self.rnn_output_dim] + self.hidden_units + [self.child_num] 这里不知道是什么意思,(state + 隐藏层 + 孩子数 目前是[36,7])
  5. self.aval_val = self.get_aval() ,调用时首先获取node_num_before_depth_i,创建一个aval_list(child_num, node_num_before_depth_i(self.bc_dim)),然后又开始递归调用函数rec_get_aval。
    最后的aval_list如下,表示 每个非叶节点的每个子树中可用的item
  • 行数代表孩子节点数列表示非叶节点数量(policy数量)

  • 第i列表示标号为i的policy各个孩子节点可用的nodes;第一列代表节点0,也就是根节点。他一共有10个孩子节点0~9,每个孩子节点都还有10个孩子节点可用。行数=孩子数

  • 后面十列表示第二层那十个节点,他们也有10个孩子节点,但是都是叶节点了,每个子节点只有一个节点可用
    在这里插入图片描述

make_graph()

  1. 创建很多个placeholder:forward_action(None),forward_reward(None),forward_statistic(None,statistic_dim),forward_rnn_state(2, None, rnn_output_dim),cur_q(None)(Q值),cur_action(None)
  2. 创建常量:action_embeddings(value=self.forward_env.item_embedding)PMF生成的物品embedding,bc_embeddings(value=self.id_to_code)[爸爸辈分,儿子辈分]
  3. 创建RNN输入需要的值。forward_a_emb(使用forward_action查表action_embeddings),one_hot_reward(使用tf.one命令将经过转换的forward_reward映射到reward_dim维),forward_ars(拼接[forward_a_emb, one_hot_reward, forward_statistic],这就是rnn输入哦)
  4. 初始化 rnn initial_states:拼接两个0值填充的tensor:tf.stack([tf.zeros([self.train_batch_size, self.rnn_output_dim]), tf.zeros([self.train_batch_size, self.rnn_output_dim])]) shape:(2,64000,27)
  5. 加载前面预训练rnn得到的obj文件,并用参数创建create_sru,返回函数rnn和参数rnn_variables
  6. 定义rnn_state:使用rnn函数rnn(forward_ars, forward_rnn_state),给rnn两个输入(item embedding+reward+statistic)+ forward_rnn_state(placeholder)
  7. 定义user_state:拼接rnn隐藏层和固定参数[rnn_state[0], forward_statistic]。(rnn函数会返回两个值[h, c],隐藏层的和记忆细胞)
  8. 定义需要更新的参数W_list和b_list,初始化时正太分布, shape=[node_num_before_depth_i(self.bc_dim), layer_units[i], layer_units[i + 1]]。(11,36,10):11表示非叶节点数,36是statistic_dim + rnn_output_dim,10是孩子数;layer_units = [self.statistic_dim + self.rnn_output_dim] + self.hidden_units + [self.child_num]
  9. 将hidden state映射到action。定义常量code2id(value=code_to_id),树中的位置对应在数据集中的索引。
  10. Variable变量:aval_list和aval_eval_list。tf.Variable(np.tile(np.expand_dims(self.aval_val, 1), [1, self.train_batch_size, 1]), dtype=tf.float32),tile() 函数将原矩阵按照维度复制;np.expand_dims:用于扩展数组的形状; 首先expand_dims把aval_val扩展成(10,1,11),再经过tile变为(10, 64000, 11)。相当于复制了train_batch_size份和eval_batch_size份aval_val。aval_val本来是(10,11),表示 每个非叶节点的每个子树中可用的item数。 eval_probs=[]
  11. 又开始创建常量了,全部为0值:pre_shift(大小为train_batch_size),pre_mul_choice(大小为train_batch_size),action_index(大小为train_batch_size),pre_shift_eval(大小为eval_batch_size),pre_max_choice_eval(大小为eval_batch_size),action_index_eval(大小为eval_batch_size)。
  12. 赋值aval_list_t = aval_list,aval_eval_list_t = aval_eval_list;就是刚刚定义为Variable的变量。

后面开始进行采样过程,总采样次数应该是episode次(也就是需要为用户推荐多少item),每一次采样需要做d次选择,输入state,通过策略网络从1~child_num中做选择,并记录下节点索引。做了d次选择后, p t p_t pt存储每次做的选择,然后需要把路径映射到action。获取reward,更新state(h)。
前面算出的11,是非叶节点数,也是需要初始化的策略网络数

  1. 采样过程,做d次选择:循环深度次数 for i in range(self.bc_dim)2次:① 计算forward_index self.forward_index = self.node_num_before_depth_i(i) + self.child_num * self.pre_shift + tf.cast(self.pre_mul_choice, tf.int32) (第i层前面的节点索引+孩子数*pre_shift + pre_mul_choice,是一个shape为64000的tensor)② 给h(h就是RL的输入state)赋值,if i == 0一开始赋user_state(拼接rnn隐藏层和固定参数(?,36)),之后将user_state增加一个维度(tf.expand_dims(self.user_state, axis=1)):tf.expand_dims(self.user_state, axis=1) ,(?, 1, 36) ③ for k in range(len(self.W_list)):循环len(self.W_list)次(1次),W_list是前面定义的Variable变量,它的长度是len(self.layer_units) - 1,layer_units的长度又是state + 隐藏层 (为空)+ 孩子数[36,7],长度减一是1(如果有隐藏层,就要先经过一层隐藏层)。 ④ if k == (len(self.W_list) - 1)(k是循环的最后一轮)定义移动概率forward_prob(否则给h赋值), if i == 0self.forward_prob = tf.nn.softmax(tf.matmul(h, self.W_list[k][0]) + self.b_list[k][0], axis=1),就是经过一层全连接 外面包一层softmax,最终维度是(?,10),不就是输入state,返回选择10个节点的概率嘛(但是不是最终概率,后面还会处理的)。W的维度是(11,36,10):11表示非叶节点数,36是statistic_dim + rnn_output_dim,10是孩子数;self.W_list[k][0]维度是(36, 10)。b的维度是(11,10),h维度是(?,36)(后来会修改成 (?, 1, 36))

W维度(11,36,10)的含义:有11个一模一样的二维数组(36,10),36行,10列。10应该代表可选子树数量,11代表策略网络个数
下面写了一个例子,方便理解三维向量的意思。随机生成了(2,3,5):生成了两个一样的二维数组(3,5)

x = np.random.random([2,3,5])
x = array([[[0.90213642, 0.86585019, 0.63359569, 0.54320493, 0.61305485],[0.92393074, 0.54836019, 0.06053656, 0.57022926, 0.78738432],[0.24339059, 0.68440075, 0.42425335, 0.34418762, 0.42389787]],[[0.99615986, 0.16249693, 0.79286802, 0.45866597, 0.9477994 ],[0.80744563, 0.38976343, 0.56733192, 0.62285278, 0.038492  ],[0.11921885, 0.00678705, 0.67478142, 0.12596062, 0.58212996]]])

在这里插入图片描述

  1. 还是刚刚for i in range(self.bc_dim)循环里面:修改pre_shift (一开始全赋值为0)self.pre_shift = self.child_num * self.pre_shift + tf.cast(self.pre_mul_choice, tf.int32)
  2. 修改gather_index(64000, 10, 3)

evaluate

  1. 先计算eval_step_num,也就是每次采样多少;再将初始化eval_batch_size * eval_step_num个用户env( 共创建了5000个env),并传入用户id进行reset(从0~499,10个轮回)。这样每一个env都有对应的userID了

  2. 初始化ars:_get_initial_ars,调用env中获取reward的函数返回item_id,reward,statistic。① 循环5000次 ② 先随机选itemid,调用env[i]中函数获取选择item_id得到的reward,同时得到get_statistic。因此最终的result包括了5000组结果:item_id,reward,statistic。对应的是用户0~499。
    ![在这里插入图片描述](https://img-blog.csdnimg.cn/4d40047335ac48009bd2ef48e6284afb.png 400x)

  3. 初始化entropy_list,用于存储指标rmse

  4. 为每位用户采样episode:① 算出首尾:0-5000。② 零值初始化rnn_state,step_count=0;self.update_avalable_items(ars[0][0][start:end]) ③ 开始赋值,5000为一个batchsize:self.forward_action: ars[0][step_count][start:end],self.forward_reward: ars[1][step_count][start:end],self.forward_statistic: ars[2][step_count][start:end],self.forward_rnn_state: rnn_state (ars[0]代表action,也就是itemID,ars[1]代表reward,ars[2]代表固定值;这些placeholder都是在make_graph中定义的)。

  5. 开始run了:run_list = [self.forward_sampled_action_eval, self.rnn_state, self.eval_probs, self.update_aval_eval_list],其中① forward_sampled_action_eval是查表操作,目的是将action索引转成数据集中的;② rnn_state是运行rnn函数,传入forward_ars和forward_rnn_state,输出h和c的stack;③ eval_probs存储了每次选择时算出的概率;④ update_aval_eval_list是删除了选过的item,并进行赋值。
    在这里插入图片描述

  6. sampled_action, rnn_state, probs = result_list[0:3]进行赋值:数据集中对应的action(5000,),运行rnn的结果(2,5000,27),每次选择item的概率list(5000,20)(长度为20,因为需要选择2次,每次有10个选项)

  7. 计算rmse: rmse = np.power(np.mean(np.square(probs - 1.0 / self.child_num)), 0.5) 这里不太明白。append到entropy_list中。step_count+1

  8. 为ars添加[]

  9. 循环5000次,衡量action好坏了!查找sampled_action的reward,像之前一样append到ars中,如果reward[1]=True(item全部遍历后或者达到episode_length),stop_flag = True。 好奇怪 step_count应该每次都+1啊,不对!传入的user不一样,是不同的env

  10. 结束上面的循环后, 取出前500个用户的reward,并计算训练集的平均奖励和测试集的平均奖励,以及平均rmse

  11. tp_list存储每个用户推荐列表中的高分数量;rele_list存储用户历史评分高于3.5的数量

  12. 循环user_num次,要衡量rating了:self.forward_env.reset(j),这是最开始传入config文件初始化的哦,返回ratings(list 32) ratings = [self.forward_env.get_rating(ars[0][k][j]) for k in range(0, len(ars[0]))],ars[0]是(32,5000),k是0~31,代表获取给用户生成的32长度的episode的rating;tp = len(list(filter(lambda x: x>=self.boundary_rating, ratings)))返回分数大于等于3.5的个数,也相当于这个用户的高分数量。

  13. 计算precision(高分数量除以32)和recall(高分数量除以用户历史高分数量)以及f1值;计算训练集各个指标的平均值train_ave_precision,train_ave_recall,train_ave_f1;测试集的指标:train_ave_f1,test_ave_precision,test_ave_recall,test_ave_f1

  14. 保存training_steps的模型,W_list,b_list (‘…/data/result/result_log/20210725160757_0.0_demo_data’)。保存结果: self.storage.append([train_ave_reward, train_ave_precision, train_ave_recall, train_ave_f1, test_ave_reward, test_ave_precision, test_ave_recall, test_ave_f1, ave_rmse])

  15. 输出结果:这里的0是训练次数,与training_steps相关,它只在train中更改;
    在这里插入图片描述
    然后就返回recommender中,开始调用train了!

train

  1. 每一个batch选的用户数=4000, 训练的时候每个用户采样的episode=16
  2. 循环4000次,从1~400(boundry_user_id)中随机选userID;嵌套循环16次,self.env[i*self.sample_episodes_per_batch+j].reset(user_id),传入userID进行重置。(self.env()在初始化的时候,初始化了64000个)这个地方就是先选出4000个用户id,再重置该用户的前16个env,刚刚好64000.
  3. 初始化ars:64000个 item_id,reward,statistic;给用户随机选item,然后获取reward,train_batch_size=64000
  4. self.update_avalable_items(ars[0][0]),传入64000个itemid,目的是删除树中选过的item(爸爸和儿子都要删除?),① sampled_codes = self.id_to_code[sampled_items],从爸爸找儿子(64000,2) ② aval_val_tmp = np.tile(np.expand_dims(self.aval_val, axis=1), [1, self.train_batch_size, 1])(10,64000,11); ③ 开始循环,修改aval_val_tmp,并且run assign_aval_list,也就是赋值操作。
        for i in range(len(sampled_codes)):code = sampled_codes[i]index = 0for c in code: # 爸爸和儿子辈分c = int(c)aval_val_tmp[c][i][index] -= 1index = self.child_num * index + 1 + c #
  1. 用0初始化rnn_state;step_count = 0;stop_flag = False;action_list = []
  2. 根据策略采样action,先赋值
feed_dict = {self.forward_action: ars[0][step_count],#长度为64000self.forward_statistic:ars[2][step_count],self.forward_reward: ars[1][step_count],self.forward_rnn_state: rnn_state}
  1. run_list =[self.forward_sampled_action, self.rnn_state, self.update_aval_list],其中
    ① forward_sampled_action是查表操作,目的是将action索引转成数据集中的id;
    ② rnn_state是运行rnn函数,传入forward_ars和forward_rnn_state,输出[h,c];
    ③ update_aval_eval_list是删除了选过的item。赋值给sampled_action, rnn_state
  2. action_list.append(sampled_action)step_count += 1(最后加到了31),ars加[]
  3. 又开始循环64000次,reward = self.env[j].get_reward(sampled_action[j]),这里是返回选取的item的reward,共32个,每个长度为64000。也就是要衡量采样结果好不好了。
  4. 接下来标准化Q值, 就是在算累积收益
        for i in reversed(range(len(qs))): # 折扣值c_reward = self.discount_factor * c_reward + qs[i]qs[i] = c_reward
  1. qs_mean_list 取平均;又对qs进行标准化。这里传入的matrix是(31,16)
    def standardization(self, q_matrix):q_matrix -= np.mean(q_matrix)std = np.std(q_matrix)if std == 0.0:return q_matrixq_matrix /= stdreturn q_matrix
  1. 初始化rnn(2,64000,27)
  2. 强化学习更新!
        for i in range(step_count):feed_dict = {self.forward_action: ars[0][i],self.forward_reward: ars[1][i],self.forward_statistic: ars[2][i],self.forward_rnn_state: rnn_state,self.cur_action: ars[0][i + 1],self.cur_q: qs[i]} # Q值 _, rnn_state = self.sess.run([self.train_op, self.rnn_state], feed_dict=feed_dict)

总结:传入采样生成的action,reward等记录(是为用户随机选item,得到reward;大小为batchsize),返回推荐结果(长度为64000),然后衡量好坏。

补充知识

这部分是遇到的一些没见过的或者是需要巩固的知识点:

函数

  1. Batch Size定义:一次训练所选取的样本数。Batch Size的大小影响模型的优化程度和速度。同时其直接影响到GPU内存的使用情况,假如你GPU内存不大,该数值最好设置小一点
  2. configparser:用来读取配置文件的包配置文件的格式如下:中括号“[ ]”内包含的为section。section 下面为类似于key-value 的配置内容。
  3. 读文件时,r,rb,w,wb等的含义:

‘r’:只读。该文件必须已存在。

‘r+’:可读可写。该文件必须已存在,写为追加在文件内容末尾。

‘rb’:表示以二进制方式读取文件。该文件必须已存在。

‘w’:只写。打开即默认创建一个新文件,如果文件已存在,则覆盖写(即文件内原始数据会被新写入的数据清空覆盖)。

‘w+’:写读。打开创建新文件并写入数据,如果文件已存在,则覆盖写。

‘wb’:表示以二进制写方式打开,只能写文件, 如果文件不存在,创建该文件;如果文件已存在,则覆盖写。

‘a’:追加写。若打开的是已有文件则直接对已有文件操作,若打开文件不存在则创建新文件,只能执行写(追加在后面),不能读。

‘a+’:追加读写。打开文件方式与写入方式和’a’一样,但是可以读。需注意的是你若刚用‘a+’打开一个文件,一般不能直接读取,因为此时光标已经是文件末尾,除非你把光标移动到初始位置或任意非末尾的位置。(可使用seek() 方法解决这个问题,详细请见下文Model 8 示例)

  1. 创建一个不含有重复值的列表,不要再用list.append()方法了,因为要提前判断是否在列表内。可以采用 set add() 方法,用于给集合添加元素,如果添加的元素在集合中已存在,则不执行任何操作。
  2. coo_matrix构建稀疏矩阵coo_matrix((data, (i, j)), [shape=(M, N)]),data[:] 表示原始矩阵中的数据;i[:] 是行的指示符号,如row的第0个元素是0,就代表data中第一个数据在第0行;j[:] 是列的指示符号;如col的第0个元素是0,就代表data中第一个数据在第0列;总结一下,根据i和j的位置确定data的每个元素位于矩阵哪个位置。但是主要用来创建矩阵,因为 coo_matrix 无法对矩阵的元素进行增删改等操作
  3. 之前将评分数据转换为矩阵都是先用numpy初始化空矩阵,再一个一个读取数据集填充。现在可以使用coo_matrix方法啦!self.r_matrix = coo_matrix((rating[:, 2], (rating[:, 0].astype(int), rating[:, 1].astype(int)))).todok()
  4. todok():采用字典来记录矩阵中不为 0 的元素。字典的 key 存的是记录元素的位置信息的元组, value 是记录元素的具体值。 这篇文章总结的很好
  5. tqdm 是一个快速,可扩展的Python进度条,可以在 Python 长循环中添加一个进度提示信息,用户只需要封装任意的迭代器 tqdm(iterator)。
  6. filter() 函数用于过滤序列,过滤掉不符合条件的元素,返回一个迭代器对象,如果要转换为列表,可以使用 list() 来转换。filter(function, iterable)该接收两个参数,第一个为函数,第二个为序列,序列的每个元素作为参数传递给函数进行判断,然后返回 True 或 False,最后将返回 True 的元素放到新列表中。
  7. tf.nn.embedding_lookup函数:选取一个张量里面索引对应的元素。tf.nn.embedding_lookup(params, ids):params可以是张量也可以是数组等,id就是对应的索引。
  8. tf.stack用于拼接矩阵。tf.concat是沿某一维度拼接shape相同的张量,拼接生成的新张量维度不会增加。而tf.stack是在新的维度上拼接,拼接后维度加1。
  9. math.ceil() “向上取整”, 即小数部分直接舍去,并向正数部分进1。
  10. math.log(10,2)以2为底10的对数。
  11. tf.nn.embedding_lookup(params,id):根据id,寻找embeddings中的第id行。
  12. tf.cast()函数的作用是执行 tensorflow 中张量数据类型转换: cast(x, dtype, name=None),第一个参数 x: 待转换的数据(张量)第二个参数 dtype: 目标数据类型;第三个参数 name: 可选参数,定义操作的名称.
  13. ·tf.random_normal()·函数用于从“服从指定正态分布的序列”输出指定维度的值。
  14. tf.unstack()是一个矩阵分解的函数
  15. PCA方法:PCA(n_components=None, copy=True, whiten=False)
    (1)n_components: int, float, None 或 string,PCA算法中所要保留的主成分个数,也即保留下来的特征个数,如果 n_components = 1,将把原始数据降到一维;
    (2)copy:True 或False,默认为True,即是否需要将原始训练数据复制;
    (3)whiten:True 或False,默认为False,即是否白化,使得每个特征具有相同的方差。
    (4)方法 :fit(X,y=None)表示用数据X来训练PCA模型。函数返回值:调用fit方法的对象本身。比如pca.fit(X),表示用X对pca这个对象进行训练。n_components_返回所保留的特征个数
    拓展:fit()可以说是scikit-learn中通用的方法,每个需要训练的算法都会有fit()方法,它其实就是算法中的“训练”这一步骤。 主成分分析(PCA)的概念很简单——减少数据集的变量数量,同时保留尽可能多的信息。
    (5)fit_transform(X):用X来训练PCA模型,同时返回降维后的数据。
  16. 创建常量:创建一个常量tensor,按照给出value来赋值,可以用shape来指定其形状。value可以是一个数,也可以是一个list。
tf.constant(value,dtype=None,shape=None,name='Const',verify_shape=False
)
  1. filter(function, iterable),过滤掉不符合条件的元素,返回一个迭代器对象,如果要转换为列表,可以使用 list() 来转换
  2. gc.collect()可以清内存
  3. ConfigParser模块在python中用来读取.conf配置文件,每个节可以有多个参数(键=值)
  4. tf.equal(x, y, name=None)逐个元素进行判断,如果相等就是True,不相等,就是False。

GRU

在这里插入图片描述
GRU是LSTM网络的一种效果很好的变体,它较LSTM网络的结构更加简单,而且效果也很好,因此也是当前非常流形的一种网络。GRU既然是LSTM的变体,因此也是可以解决RNN网络中的长依赖问题。
图中的 z t z_t zt r t r_t rt分别表示更新门和重置门。更新门用于控制前一时刻的状态信息被带入到当前状态中的程度,更新门的值越大说明前一时刻的状态信息带入越多。重置门控制前一状态有多少信息被写入到当前的候选集 h t ~ \tilde{h_t} ht~,重置门越小,前一状态的信息被写入的越少。

  • 截断反向传播(truncated backpropagation)
    在配置文件中看见“rnn truncated length”这个描述,不是很懂“截断”的意思,所以百度了一下。大概是指训练时的长度,应该和batch size差不多吧。
  • 假设我们训练含有1000000个数据的序列,如果全部训练的话,整个的序列都feed进RNN中,容易造成梯度消失或爆炸的问题,所以解决的方法就是truncated backpropagation,我们将序列截断来进行训练(num_steps)。

报错

1.dictionary changed size during iteration:找了一个多小时,发现是传入字典的时候就出问题了!key值当中,int和str没有统一!!

这篇关于TPGR代码详解 Large-Scale Interactive Recommendation with Tree-Structured Policy Gradient的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/685765

相关文章

uniapp接入微信小程序原生代码配置方案(优化版)

uniapp项目需要把微信小程序原生语法的功能代码嵌套过来,无需把原生代码转换为uniapp,可以配置拷贝的方式集成过来 1、拷贝代码包到src目录 2、vue.config.js中配置原生代码包直接拷贝到编译目录中 3、pages.json中配置分包目录,原生入口组件的路径 4、manifest.json中配置分包,使用原生组件 5、需要把原生代码包里的页面修改成组件的方

公共筛选组件(二次封装antd)支持代码提示

如果项目是基于antd组件库为基础搭建,可使用此公共筛选组件 使用到的库 npm i antdnpm i lodash-esnpm i @types/lodash-es -D /components/CommonSearch index.tsx import React from 'react';import { Button, Card, Form } from 'antd'

17.用300行代码手写初体验Spring V1.0版本

1.1.课程目标 1、了解看源码最有效的方式,先猜测后验证,不要一开始就去调试代码。 2、浓缩就是精华,用 300行最简洁的代码 提炼Spring的基本设计思想。 3、掌握Spring框架的基本脉络。 1.2.内容定位 1、 具有1年以上的SpringMVC使用经验。 2、 希望深入了解Spring源码的人群,对 Spring有一个整体的宏观感受。 3、 全程手写实现SpringM

十四、观察者模式与访问者模式详解

21.观察者模式 21.1.课程目标 1、 掌握观察者模式和访问者模式的应用场景。 2、 掌握观察者模式在具体业务场景中的应用。 3、 了解访问者模式的双分派。 4、 观察者模式和访问者模式的优、缺点。 21.2.内容定位 1、 有 Swing开发经验的人群更容易理解观察者模式。 2、 访问者模式被称为最复杂的设计模式。 21.3.观察者模式 观 察 者 模 式 ( Obser

【操作系统】信号Signal超详解|捕捉函数

🔥博客主页: 我要成为C++领域大神🎥系列专栏:【C++核心编程】 【计算机网络】 【Linux编程】 【操作系统】 ❤️感谢大家点赞👍收藏⭐评论✍️ 本博客致力于知识分享,与更多的人进行学习交流 ​ 如何触发信号 信号是Linux下的经典技术,一般操作系统利用信号杀死违规进程,典型进程干预手段,信号除了杀死进程外也可以挂起进程 kill -l 查看系统支持的信号

Jitter Injection详解

一、定义与作用 Jitter Injection,即抖动注入,是一种在通信系统中人为地添加抖动的技术。该技术通过在发送端对数据包进行延迟和抖动调整,以实现对整个通信系统的时延和抖动的控制。其主要作用包括: 改善传输质量:通过调整数据包的时延和抖动,可以有效地降低误码率,提高数据传输的可靠性。均衡网络负载:通过对不同的数据流进行不同程度的抖动注入,可以实现网络资源的合理分配,提高整体传输效率。增

代码随想录算法训练营:12/60

非科班学习算法day12 | LeetCode150:逆波兰表达式 ,Leetcode239: 滑动窗口最大值  目录 介绍 一、基础概念补充: 1.c++字符串转为数字 1. std::stoi, std::stol, std::stoll, std::stoul, std::stoull(最常用) 2. std::stringstream 3. std::atoi, std

记录AS混淆代码模板

开启混淆得先在build.gradle文件中把 minifyEnabled false改成true,以及shrinkResources true//去除无用的resource文件 这些是写在proguard-rules.pro文件内的 指定代码的压缩级别 -optimizationpasses 5 包明不混合大小写 -dontusemixedcaseclassnames 不去忽略非公共

Steam邮件推送内容有哪些?配置教程详解!

Steam邮件推送功能是否安全?如何个性化邮件推送内容? Steam作为全球最大的数字游戏分发平台之一,不仅提供了海量的游戏资源,还通过邮件推送为用户提供最新的游戏信息、促销活动和个性化推荐。AokSend将详细介绍Steam邮件推送的主要内容。 Steam邮件推送:促销优惠 每当平台举办大型促销活动,如夏季促销、冬季促销、黑色星期五等,用户都会收到邮件通知。这些邮件详细列出了打折游戏、

麻了!一觉醒来,代码全挂了。。

作为⼀名程序员,相信大家平时都有代码托管的需求。 相信有不少同学或者团队都习惯把自己的代码托管到GitHub平台上。 但是GitHub大家知道,经常在访问速度这方面并不是很快,有时候因为网络问题甚至根本连网站都打不开了,所以导致使用体验并不友好。 经常一觉醒来,居然发现我竟然看不到我自己上传的代码了。。 那在国内,除了GitHub,另外还有一个比较常用的Gitee平台也可以用于