Pensive复现日志

Pensive复现日志

这里简单记录一下复现的过程,虽然没有成功,但我把整个历程都进行了记录,算是一个小记录

框架解析

一共分为了四个部分:

  • 模型训练
  • 数值仿真
  • mahimahi网络仿真
  • 实际平台测试

前两部分基本完成了,后两部分遇到了一些bug,现将查找过的途径放在此处,以供参考,遇到的最大的问题是results为空的情况

遇到的bug&调试方法(虽然没有成功 😢 )

不过还是可以记录一下调试bug的思路:

  1. 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信息:

image-20220813013950650

  1. 在网上查阅后猜测为 mahimahi 版本不对劲,于是按照作者的说法绕过 mahimahi 直接运行
image-20220813014709847

可惜我在chrome上直接运行都不行,我在网上也查找了一些对chrome网页进行设置,依旧没有成功,下面是对网页修改以及对HTML代码的一些小修改(添加 autoplay 据网上说可能有效果,但不太熟悉,不知真伪,总之没能成功)

image-20220813015853973 image-20220813015808655

可恶啊!!!我后面尝试在windows的chrome上运行也不行,我猜测可能作者提供的html需要linux环境,然后我那个linux的chrome的版本不对,时间不够就没有继续验证了

Codes analysis

especially for sim and test

get_video_sizes.py

提供了六个视频,video6是最差的,video1是最好的,这个对应了bitrate从最小到最大,画质逐渐变好

multi_agent . py /main(.)

  1. 先get_vedio_sizes,得到每个m4s格式的视频的大小,存储到vedio_size_n的文本中

  2. 创建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))
    
  3. 将队列存储到net_params_queues和exp_queues中

  4. 创建并开启coordinator即central_agent进程,传入queue的信息,以后就依靠这个net_params_queues和exp_queues进行信息传播

  5. 从cooked_traces中得到all_cooked_time, all_cooked_bw

image-20220812160708837

数据类似于这种形式,分为两列,分别代表all_cooked_time, all_cooked_bw,时间戳和带宽

  1. 创建并开启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区分

  2. 阻塞住主进程再等待子进程结束,然后再往下执行,(里面会用wait())

    coordinator.join()
    

Analysis of each process

central_agent(.):
  1. 交代log的地址以及格式,用于打印运行信息

  2. Session提供了 Operation 执行和 Tensor 值的环境,使用with是为了不手动关闭会话

     with tf.Session() as sess, open(LOG_FILE + '_test', 'wb') as test_log_file:
    
  3. 创建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就是强化学习的两个很重要的要素

  4. 使用tensorboard,a3c.build_summaries() 的定义就是在tensorboard上打印 td_loss eps_total_reward avg_entropy

summary_ops, summary_vars = a3c.build_summaries()
  1. sess.run(tf.global_variables_initializer())代表run了 所有global Variableassign op,相当于对所有的都初始化

  2. 接下来是创建writer和saver

    writer = tf.summary.FileWriter(SUMMARY_DIR, sess.graph)  # 保存图关系
    saver = tf.train.Saver()  							   # save neural net parameters
    
  3. 加载NN_MODEL

  4. 初始化epoch,加下来进入循环

  5. 这是一个同步的过程,每个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])
    
  6. 这是获取每个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'])
    
  7. 将得到的gradient更新每个agent

    for i in xrange(len(actor_gradient_batch)):
                    actor.apply_gradients(actor_gradient_batch[i])
                    critic.apply_gradients(critic_gradient_batch[i])
    
  8. 打印并更新tensorboard上的信息,

SyntaxDescription
avg_rewardreward参数
avg_td_loss用于衡量predict的值是否精确
avg_entropyentropy用于衡量无序的程度,一开始训练的时候entropy要比较大,这样可以便于explore,但是到了训练的后期,模型的结果要相对比较稳定
  1. 每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极其类似,现挑出一些不一样的地方进行讲解

  1. 创建agent的训练模型
    net_env = env.Environment(all_cooked_time=all_cooked_time,
                                  all_cooked_bw=all_cooked_bw,
                                  random_seed=agent_id)
    
  2. 模拟真实的信息,相当于 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

    ​ ②然后比较delaybuffer_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
    
  3. 计算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
    

    对应公式

    image-20220812210144405

  4. 更新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()

image-20220812212806979

相当于按照概率大小进行一个随机的决策,这个决策将用于下一轮

  1. 计算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)))

对应的公式为:

image-20220812231743490

其实就是和李宏毅老师的这个是相似的的:区别在于这个地方用的是A,其实不是单独代表说一个简简单单的reward,它是优势函数,嗲表的是比一般好多少,是这个表示式子 李宏毅老师的截图2

李宏毅老师的截图1

非常的直觉,先看后面一项 p θ ( a t n ∣ s t n ) p_\theta(a_t^n|s_t^n) pθ(atnstn) ,让agent和环境互动一下,在某一个state,采取了某一个action的概率,然后这个概率会有一个reward,这个reward是说从现在开始到结束的总的reward(越远的话会有一个指数性质的factor约束它),减去一个b偏置

image-20220812232248120

整个的目的就是让reward越大越好

李宏毅老师的截图2

image-20220812234121659
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的计算式:

image-20220812233711850

然后这整个差分的过程实际上就是:

image-20220812233844032

为了达到这个目的,gradient的方法就是让下面的图中的红线部分和蓝线部分越接近越好

image-20220812234316878

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

整个模型用可视化(一小部分)

image-20220812235139342

勉强看的出来

image-20220813000604603

此时我感到很疑惑,为什么差别能这么大???,为什么会看着这么复杂,原来它画的太仔细了,比如conv_1d它都画的很仔细(细分了),于是我对比了一下正常2.x版本的tensorflow的写法,和这个地方的写法

正常的

image-20220813000223424

此文中较为古老的版本

image-20220813000401359

哦哦哦,怪不得了那

接下来和文中的图进行一个对应

image-20220813000922745

可以看到critic和actor的区别就在最后那个全连接层,输出的维度不一样

最后放一个tensorboard里面生成的graph,稍微好看一点,能够看出是一个强化学习的网络:

image-20220813111907610

Results

1.tensorflowboard

放一下我训练量了8000轮的结果,这是CPU跑的,所以就没有继续训练了,这里贴一个链接,是有个老哥用 python3tensorflow2.x 写的https://github.com/ahmad-hl/pensieve-py38

image-20220813004251942 image-20220813004309746 image-20220813004327313

这里TD_Loss应该是表征critic的训练的怎么样,效果不太好,其他的基本都收敛了,只是reward不知道为什么是负数,虽然在向0靠近,entropy一开始高说明是好事情,explore的比较好,后面也趋于收敛

2. test

然后放一下我跑的test的结果,只跑了当时最强的RoustMPCsimrl ,这里也有一个bug,不能同时显示,我单独跑的

MPC

image-20220813002207338 image-20220813002612011
image-20220813002631773 image-20220813002650690

sim_rl 使用的是作者给的pretrain_linear_reward.ckpt

image-20220813005059018 image-20220813005116307 image-20220813005137020 image-20220813005213176

然后是我自己训练出来的结果:nn_model_ep_8200.ckpt

image-20220813113017374 image-20220813113049943 image-20220813113131781 image-20220813113150603

这个图稍微问题大一点:

  1. total reward是0这个是好事情吗?感觉训练轮数不够? train之后的模型 效果不如启发式算法,正常
  2. 不过带宽的表现看着还是比MPC好的

这个是论文里面的结果,可以看到和论文还是比较相符的,注意纵坐标,对带宽的利用比较保守

image-20220813003913378

Reflection

1. 复现过程中对buffer存储的理解更加深入了,当代码和论文有所对照的时候有丝丝成就感 
2. 不过也有了新的问题,就是最后的结果图,效果并不怎么好
3. 调bug的能力得到了提升 :) 写代码小能手× bug制造机√
4. 和师兄交流的过程中,明白了对经典文章的熟练掌握,对以后的工作大有帮助
Way to go !!!
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值