Pensive复现日志
这里简单记录一下复现的过程,虽然没有成功,但我把整个历程都进行了记录,算是一个小记录
框架解析
一共分为了四个部分:
- 模型训练
- 数值仿真
mahimahi
网络仿真- 实际平台测试
前两部分基本完成了,后两部分遇到了一些bug,现将查找过的途径放在此处,以供参考,遇到的最大的问题是results为空的情况
遇到的bug&调试方法(虽然没有成功 😢 )
不过还是可以记录一下调试bug的思路:
run_exp\run_all_traces.py
运行之后在rusults中没有数据生成,单步调试后决定分步调整,于是单独运行run_exp\run_traces.py
具体步骤为打印 run_exp\run_all_traces.py
中的命令,直接对 run_exp\run_traces.py
运行,但是也没有得到结果
python run_traces.py ../cooked_traces/ BB 0 117.173.139.66
但是得到了在 pensieve-master/run_exp/chrome_retry_log
log信息:
- 在网上查阅后猜测为
mahimahi
版本不对劲,于是按照作者的说法绕过mahimahi
直接运行
可惜我在chrome上直接运行都不行,我在网上也查找了一些对chrome网页进行设置,依旧没有成功,下面是对网页修改以及对HTML代码的一些小修改(添加 autoplay
据网上说可能有效果,但不太熟悉,不知真伪,总之没能成功)
可恶啊!!!我后面尝试在windows的chrome上运行也不行,我猜测可能作者提供的html需要linux环境,然后我那个linux的chrome的版本不对,时间不够就没有继续验证了
Codes analysis
especially for sim
and test
get_video_sizes.py
提供了六个视频,video6是最差的,video1是最好的,这个对应了bitrate从最小到最大,画质逐渐变好
multi_agent . py /main(.)
-
先get_vedio_sizes,得到每个m4s格式的视频的大小,存储到vedio_size_n的文本中
-
创建16个queue队列,用于进程间的通信
for i in xrange(NUM_AGENTS): net_params_queues.append(mp.Queue(1)) #create queue,which is used for communication exp_queues.append(mp.Queue(1))
-
将队列存储到net_params_queues和exp_queues中
-
创建并开启coordinator即central_agent进程,传入queue的信息,以后就依靠这个net_params_queues和exp_queues进行信息传播
-
从cooked_traces中得到all_cooked_time, all_cooked_bw
数据类似于这种形式,分为两列,分别代表all_cooked_time, all_cooked_bw,时间戳和带宽
-
创建并开启16个子process,这些线程用于异步计算(虽然这里的代码是同步):
for i in xrange(NUM_AGENTS): agents.append(mp.Process(target=agent, args=(i, all_cooked_time, all_cooked_bw, net_params_queues[i], exp_queues[i])))
传入了all_cooked_time, all_cooked_bw以及用于和central agent进行通信的queue,注意都有编号,是为了让centrol区分
-
阻塞住主进程再等待子进程结束,然后再往下执行,(里面会用wait())
coordinator.join()
Analysis of each process
central_agent(.):
-
交代log的地址以及格式,用于打印运行信息
-
Session提供了 Operation 执行和 Tensor 值的环境,使用with是为了不手动关闭会话
with tf.Session() as sess, open(LOG_FILE + '_test', 'wb') as test_log_file:
-
创建actor和critic
actor = a3c.ActorNetwork(sess, state_dim=[S_INFO, S_LEN], action_dim=A_DIM, learning_rate=ACTOR_LR_RATE) critic = a3c.CriticNetwork(sess, state_dim=[S_INFO, S_LEN], learning_rate=CRITIC_LR_RATE)
传入的信息有state的信息,action的维度以及学习率,这很合理,本身就state(observation),action就是强化学习的两个很重要的要素
-
使用tensorboard,
a3c.build_summaries()
的定义就是在tensorboard上打印td_loss
eps_total_reward
avg_entropy
summary_ops, summary_vars = a3c.build_summaries()
-
sess.run(tf.global_variables_initializer())
代表run
了 所有global Variable
的assign op
,相当于对所有的都初始化 -
接下来是创建writer和saver
writer = tf.summary.FileWriter(SUMMARY_DIR, sess.graph) # 保存图关系 saver = tf.train.Saver() # save neural net parameters
-
加载NN_MODEL
-
初始化epoch,加下来进入循环
-
这是一个同步的过程,每个epoch一次,将coordinator的数据传入到每个agent中
'''本来A3C是异步的,但是这里的代码是同步''' #获取actor和critic的参数 actor_net_params = actor.get_network_params() critic_net_params = critic.get_network_params() #将获取到的参数,通过queue传入到每个agent的process中,注意这里的put是一个阻塞函数,和后面的get类似 for i in xrange(NUM_AGENTS): net_params_queues[i].put([actor_net_params, critic_net_params])
-
这是获取每个agent的数据,具体参数的计算后面会讲
for i in xrange(NUM_AGENTS): #注意这里的get是一个阻塞函数,意思是如果没有get到就会一直挂起等待,所以实现了一个同步的过程 #换句话说,就是要等到每一个agent把参数传给coordinator,才会继续往下 s_batch, a_batch, r_batch, terminal, info = exp_queues[i].get() #通过获取到的刚刚的参数进行计算,获取到对应的gradient值 actor_gradient, critic_gradient, td_batch = \ a3c.compute_gradients( s_batch=np.stack(s_batch, axis=0), a_batch=np.vstack(a_batch), r_batch=np.vstack(r_batch), terminal=terminal, actor=actor, critic=critic) actor_gradient_batch.append(actor_gradient) critic_gradient_batch.append(critic_gradient) #计算总的reward以及td_loss total_reward += np.sum(r_batch) total_td_loss += np.sum(td_batch) total_batch_len += len(r_batch) total_agents += 1.0 total_entropy += np.sum(info['entropy'])
-
将得到的gradient更新每个agent
for i in xrange(len(actor_gradient_batch)): actor.apply_gradients(actor_gradient_batch[i]) critic.apply_gradients(critic_gradient_batch[i])
-
打印并更新tensorboard上的信息,
Syntax | Description |
---|---|
avg_reward | reward参数 |
avg_td_loss | 用于衡量predict的值是否精确 |
avg_entropy | entropy用于衡量无序的程度,一开始训练的时候entropy要比较大,这样可以便于explore,但是到了训练的后期,模型的结果要相对比较稳定 |
-
每100保存模型参数,并进行test,得到此轮训练得到的指标
if epoch % MODEL_SAVE_INTERVAL == 0: # Save the neural net parameters to disk. save_path = saver.save(sess, SUMMARY_DIR + "/nn_model_ep_" + str(epoch) + ".ckpt") logging.info("Model saved in file: " + save_path) testing(epoch, SUMMARY_DIR + "/nn_model_ep_" + str(epoch) + ".ckpt", test_log_file)
agent(.):
和central极其类似,现挑出一些不一样的地方进行讲解
-
创建agent的训练模型
net_env = env.Environment(all_cooked_time=all_cooked_time, all_cooked_bw=all_cooked_bw, random_seed=agent_id)
-
模拟真实的信息,相当于 download video chunk over mahimahi
delay, sleep_time, buffer_size, rebuf, \ video_chunk_size, next_video_chunk_sizes, \ end_of_video, video_chunk_remain = \ net_env.get_video_chunk(bit_rate)
这里需要详细的讲一讲
get_video_chunk
是如何实现的(env.py)
:首先是一些参数的定义
#单位的转换 MILLISECONDS_IN_SECOND = 1000.0 B_IN_MB = 1000000.0 BITS_IN_BYTE = 8.0 #一个video chunk的长度是4s VIDEO_CHUNCK_LEN = 4000.0 # millisec, every time add this amount to buffer BITRATE_LEVELS = 6 # 一共有6个bitrate的level供选择 TOTAL_VIDEO_CHUNCK = 48 #1个video一共48个chunk BUFFER_THRESH = 60.0 * MILLISECONDS_IN_SECOND # buffer的上限是60s,超过就要sleep DRAIN_BUFFER_SLEEP_TIME = 500.0 # sleep 500 ms PACKET_PAYLOAD_PORTION = 0.95 # payload占整个包的比例 LINK_RTT = 80 # RTT 即来回的时间 PACKET_SIZE = 1500 #一个packet有1500 bytes #乘性噪声的范围,这个影响delay NOISE_LOW = 0.9 NOISE_HIGH = 1.1
下面来说下这个函数比较精彩的部分:
大概的逻辑就是:
①计算按照这个bitrate下载的chunk总共需要花费的时间,这个时间包括了RTT,noise等等,记这个时间为delay
②然后比较
delay
和buffer_size
的大小,buffer_size
指的是现在buffer里面存储的量 ③因为是必须要满了一个chunk才能播放,所以当buffer_size里面的量小于delay的长度,即播放完了都没有下载完成的话就要进入 rebuffer time,同时会重新计算剩余的buffer_size
④里面有一个参数video_chunk_counter,是相当于遍历这个视频,比如bitrate_1的第1个chunk过了等会如果选到了第bitrate_3, 就是第bitrate_3的第2个chunk了,相当于这个视频有6种不同的bitrate,我们每次只是模拟显示的网络情况,但还是下载这个视 频,这个视频下载完了就换一个,或者重复下载
这里是模拟真实的throughput和duration
#每次进入这个函数会随机生成一个mahimahi_ptr,然后从trace里面可以读取一个随机的throughput #以及这个throughput持续的时间,可以根据这个duration和throughput计算吞吐量packet_payload throughput = self.cooked_bw[self.mahimahi_ptr] \ * B_IN_MB / BITS_IN_BYTE duration = self.cooked_time[self.mahimahi_ptr] \ - self.last_mahimahi_time 吞吐量(速度)*duration(时间)*payload所占比例 packet_payload = throughput * duration * PACKET_PAYLOAD_PORTION
这里是判断跳出条件:如果我们每一次都相当于是把这段时间微分成很多段,每段的throughput我们看作不变,这里就是说在还没有变化的时候我就已经达到一个chunk了,那就是说不需要等到下一次throughput变化了,直接跳出循环(每次循环都是一个throughput)
if video_chunk_counter_sent + packet_payload > video_chunk_size: fractional_time = (video_chunk_size - video_chunk_counter_sent) / \ throughput / PACKET_PAYLOAD_PORTION delay += fractional_time self.last_mahimahi_time += fractional_time assert(self.last_mahimahi_time <= self.cooked_time[self.mahimahi_ptr]) break
这里开始计算整个下载和delay的总时间(这里有一个乘性噪声),记作delay
delay *= MILLISECONDS_IN_SECOND delay += LINK_RTT # add a multiplicative noise to the delay delay *= np.random.uniform(NOISE_LOW, NOISE_HIGH)
开始计算是否需要rebuff,然后剩余了多少buffer
# rebuffer time rebuf = np.maximum(delay - self.buffer_size, 0.0) # update the buffer self.buffer_size = np.maximum(self.buffer_size - delay, 0.0) # add in the new chunk self.buffer_size += VIDEO_CHUNCK_LEN
如果buffer_size超过了上限就需要sleep,这段时间就不用下载了
# sleep if buffer gets too large sleep_time = 0 if self.buffer_size > BUFFER_THRESH: # exceed the buffer limit # we need to skip some network bandwidth here # but do not add up the delay drain_buffer_time = self.buffer_size - BUFFER_THRESH sleep_time = np.ceil(drain_buffer_time / DRAIN_BUFFER_SLEEP_TIME) * \ DRAIN_BUFFER_SLEEP_TIME self.buffer_size -= sleep_time
这里表示下载了完这个chunk过后,这个视频有没有下载完,下载完了的话就换一个视频
if self.video_chunk_counter >= TOTAL_VIDEO_CHUNCK: end_of_video = True self.buffer_size = 0 self.video_chunk_counter = 0 # pick a random trace file self.trace_idx = np.random.randint(len(self.all_cooked_time)) self.cooked_time = self.all_cooked_time[self.trace_idx] self.cooked_bw = self.all_cooked_bw[self.trace_idx] # randomize the start point of the video # note: trace file starts with time 0 self.mahimahi_ptr = np.random.randint(1, len(self.cooked_bw)) self.last_mahimahi_time = self.cooked_time[self.mahimahi_ptr - 1]
计算一些参数最后返回便于
state
计算的参数sleep_time, \ return_buffer_size / MILLISECONDS_IN_SECOND, \ rebuf / MILLISECONDS_IN_SECOND, \ video_chunk_size, \ next_video_chunk_sizes, \ end_of_video, \ video_chunk_remain
-
计算reward
reward = VIDEO_BIT_RATE[bit_rate] / M_IN_K \ - REBUF_PENALTY * rebuf \ - SMOOTH_PENALTY * np.abs(VIDEO_BIT_RATE[bit_rate] - VIDEO_BIT_RATE[last_bit_rate]) / M_IN_K
对应公式
-
更新state
# this should be S_INFO number of terms
state[0, -1] = VIDEO_BIT_RATE[bit_rate] / float(np.max(VIDEO_BIT_RATE)) # last quality
state[1, -1] = buffer_size / BUFFER_NORM_FACTOR # 10 sec
state[2, -1] = float(video_chunk_size) / float(delay) / M_IN_K # kilo byte / ms
state[3, -1] = float(delay) / M_IN_K / BUFFER_NORM_FACTOR # 10 sec
state[4, :A_DIM] = np.array(next_video_chunk_sizes) / M_IN_K / M_IN_K # mega byte
state[5, -1] = np.minimum(video_chunk_remain, CHUNK_TIL_VIDEO_END_CAP) / float(CHUNK_TIL_VIDEO_END_CAP)
# compute action probability vector
action_prob = actor.predict(np.reshape(state, (1, S_INFO, S_LEN)))
action_cumsum = np.cumsum(action_prob) #这是计算cdf
#这个是action的概率求cdf,然后随机生成一个值(0-1),返回第一个最大的值(返回第一个True的下标),具体见下
bit_rate = (action_cumsum > np.random.randint(1, RAND_RANGE) / float(RAND_RANGE)).argmax()
相当于按照概率大小进行一个随机的决策,这个决策将用于下一轮
-
计算entropy等参数,并且当达到
TRAIN_SEQ_LEN
时就向coordinator传输一次s_batch.append(state) action_vec = np.zeros(A_DIM) action_vec[bit_rate] = 1 a_batch.append(action_vec)
把这一轮计算得到的 action,state都传入下一轮
Essential codes
actor.get_gradients(.)
# Compute the objective (log action_vector and entropy)
self.obj = tf.reduce_sum(tf.multiply(
tf.log(tf.reduce_sum(tf.multiply(self.out, self.acts),\
reduction_indices=1, keep_dims=True)),-self.act_grad_weights)) \
+ ENTROPY_WEIGHT * tf.reduce_sum(tf.multiply(self.out,tf.log(self.out + ENTROPY_EPS)))
对应的公式为:
其实就是和李宏毅老师的这个是相似的的:区别在于这个地方用的是A,其实不是单独代表说一个简简单单的reward,它是优势函数,嗲表的是比一般好多少,是这个表示式子 李宏毅老师的截图2
李宏毅老师的截图1
非常的直觉,先看后面一项 p θ ( a t n ∣ s t n ) p_\theta(a_t^n|s_t^n) pθ(atn∣stn) ,让agent和环境互动一下,在某一个state,采取了某一个action的概率,然后这个概率会有一个reward,这个reward是说从现在开始到结束的总的reward(越远的话会有一个指数性质的factor约束它),减去一个b偏置
整个的目的就是让reward越大越好
李宏毅老师的截图2
critic.get_gradients(.)
# Mean square error
self.loss = tflearn.mean_square(self.td_target, self.out)
# Compute critic gradient
self.critic_gradients = tf.gradients(self.loss, self.network_params)
# Optimization Op
self.optimize = tf.train.RMSPropOptimizer(self.lr_rate).\
apply_gradients(zip(self.critic_gradients, self.network_params))
这里是让两个分布越相似越好,注意看输入的参数(compute_gradients
中)
R_batch[t, 0] = r_batch[t] + GAMMA * R_batch[t + 1, 0]
critic_gradients = critic.get_gradients(s_batch, R_batch)
r_batch里面存放的值就是前面计算的Qoe,R_batch这其实是一个等比数列的差值问题,它表示从这个时刻开始到结束总的reward是多少,因为我们现在唯一确定的就是当前的reward(通过Qoe的计算式子得到的)
Qoe的计算式:
然后这整个差分的过程实际上就是:
为了达到这个目的,gradient的方法就是让下面的图中的红线部分和蓝线部分越接近越好
create_actor_network(.)
def create_actor_network(self):
with tf.variable_scope('actor'):
inputs = tflearn.input_data(shape=[None, self.s_dim[0], self.s_dim[1]])
split_0 = tflearn.fully_connected(inputs[:, 0:1, -1], 128, activation='relu')
split_1 = tflearn.fully_connected(inputs[:, 1:2, -1], 128, activation='relu')
split_2 = tflearn.conv_1d(inputs[:, 2:3, :], 128, 4, activation='relu')
split_3 = tflearn.conv_1d(inputs[:, 3:4, :], 128, 4, activation='relu')
split_4 = tflearn.conv_1d(inputs[:, 4:5, :A_DIM], 128, 4, activation='relu')
split_5 = tflearn.fully_connected(inputs[:, 4:5, -1], 128, activation='relu')
split_2_flat = tflearn.flatten(split_2)
split_3_flat = tflearn.flatten(split_3)
split_4_flat = tflearn.flatten(split_4)
merge_net = tflearn.merge([split_0, split_1, split_2_flat, split_3_flat, split_4_flat, split_5], 'concat')
dense_net_0 = tflearn.fully_connected(merge_net, 128, activation='relu')
out = tflearn.fully_connected(dense_net_0, self.a_dim, activation='softmax')
return inputs, out
整个模型用可视化(一小部分)
勉强看的出来
此时我感到很疑惑,为什么差别能这么大???,为什么会看着这么复杂,原来它画的太仔细了,比如conv_1d
它都画的很仔细(细分了),于是我对比了一下正常2.x版本的tensorflow
的写法,和这个地方的写法
正常的
此文中较为古老的版本
哦哦哦,怪不得了那
接下来和文中的图进行一个对应
可以看到critic和actor的区别就在最后那个全连接层,输出的维度不一样
最后放一个tensorboard
里面生成的graph,稍微好看一点,能够看出是一个强化学习的网络:
Results
1.tensorflowboard
放一下我训练量了8000轮的结果,这是CPU跑的,所以就没有继续训练了,这里贴一个链接,是有个老哥用 python3
和 tensorflow2.x
写的https://github.com/ahmad-hl/pensieve-py38
这里TD_Loss
应该是表征critic的训练的怎么样,效果不太好,其他的基本都收敛了,只是reward
不知道为什么是负数,虽然在向0靠近,entropy
一开始高说明是好事情,explore的比较好,后面也趋于收敛
2. test
然后放一下我跑的test的结果,只跑了当时最强的RoustMPC
和sim
的rl
,这里也有一个bug,不能同时显示,我单独跑的
MPC
sim_rl
使用的是作者给的pretrain_linear_reward.ckpt
然后是我自己训练出来的结果:nn_model_ep_8200.ckpt
这个图稍微问题大一点:
total reward
是0这个是好事情吗?感觉训练轮数不够? train之后的模型 效果不如启发式算法,正常- 不过带宽的表现看着还是比
MPC
好的
这个是论文里面的结果,可以看到和论文还是比较相符的,注意纵坐标,对带宽的利用比较保守
Reflection
1. 复现过程中对buffer存储的理解更加深入了,当代码和论文有所对照的时候有丝丝成就感
2. 不过也有了新的问题,就是最后的结果图,效果并不怎么好
3. 调bug的能力得到了提升 :) 写代码小能手× bug制造机√
4. 和师兄交流的过程中,明白了对经典文章的熟练掌握,对以后的工作大有帮助
Way to go !!!