引子
最近在调研论文,这个过程中我常常能学到一些好东西,比如本文要聊到的水塘抽样(reservoir sampling)。
先大致说明一下论文要解决的问题以及想法。
我们知道,卷积神经网络是深度学习中一种很出名的模型结构,给它一个隐层,再加上一个非线性激活函数,它就能够拟合一切函数。正是因为神经网络这种与生俱来的拟合特性,它常常会过度关注当下,而忘记以前的知识,这被称为灾难性遗忘(Catastrophic forgetting)。论文《Selective Experience Replay for Lifelong Learning》致力于减轻强化学习领域中多任务学习时的遗忘问题。这篇文章从经验池着手,考虑通过保留用于训练先前任务的样本来防止遗忘。作者设置了两个经验池,一个是常见的FIFO经验池,用于记录on-policy的样本;另一个是长期记忆经验池,用于记忆先前任务的训练样本。由于是Lifelong Learning,所以样本池总是相对有限,于是论文引入样本的替换/保留机制,其中效果最好的方法是全局分布匹配(Global Distribution Matching),也即让long-term memory中的样本近似能够代表所有任务的总体分布。换句话说,long-term memory中的样本是近似从全局分布中以同等的概率采样得到的。如果我们能够提前知道全局分布,那么我们直接按照全局分布进行采样就可以了,但在强化学习领域,数据常常是按序到达的,这就引出了一个问题,我们如何在不知道全局分布的前提下,采样得到一个数据集,这个数据集的分布与全局分布近似相同呢?
论文作者借用水塘抽样算法解决了这一问题。接下来对该算法进行简要介绍。
水塘抽样
例1
给出一个数据流,该数据流无限长或者长度未知。数据流中的每个数据只能访问一次。请写出一个随机选择算法,使得数据流中所有的数据被选中的概率相等。
拿到这个问题,我们首先看看题目中有哪些关键点。第一,我们不知道数据流的长度(既然是流,那么可以看作顺序数据);第二,每个数据只能访问一次;第三,目标是让数据流中所有数据被选中的概率相等。
如何解决这一问题?一时半会摸不着头脑的话可以对规律进行归纳:
假设数据流中第
i
i
个数据到达,先分析为1的情形,这种情况下,直接将数据抽出即可,概率为1.
再看看
i
i
为2的情形,此时,第一个数据到达,我们将其抽出,然后在第二个数据到达时,按的概率对第二个数据进行保留,换句话说,产生一个随机数,如果概率小于
12
1
2
则返回直接返回第二个数,否则返回已经保留的第一个数。
当
i
i
为3时,我们希望每个数据被输出的概率均为。首先,第一个数据到达,直接将其留存;然后第二个数据到达,我们按照
12
1
2
的概率对二者进行保留;最后第三个数据到达,我们按照
13
1
3
对这个数据进行保留,或者说,产生一个随机数,如果该随机数小于
13
1
3
,则保留第三个数,否则保留前两个数中留存下来的那个数。分析上面这个过程中各个数据的保留概率:第三个数据被输出的概率为
13
1
3
;第二个数据输出的概率为
(1−13)∗12=23∗12=13
(
1
−
1
3
)
∗
1
2
=
2
3
∗
1
2
=
1
3
;第一个数据被输出的概率为
(1−13)∗(1−12)=23∗12=13
(
1
−
1
3
)
∗
(
1
−
1
2
)
=
2
3
∗
1
2
=
1
3
。此时达到要求。如果我们继续按照这种规律进行采样,则得到算法如下:
import random
data = input("input: ")
i = 1
print data
temp = input("input: ")
print temp
randnum = random.uniform(0, i+1)
while temp != "exit":
if randnum > i:
print "rand num: ", randnum
print "randnum >", i, " change data."
data = temp
else:
print "rand num: ", randnum
print "randnum <", i, " no op."
print "momory: ", data
temp = input("input: ")
i = i + 1
randnum = random.uniform(0, i+1)
print "output: ", data
上面这段代码中,作了稍许处理,即为了不计算分数( 1i 1 i ),直接将 (0,i+1) ( 0 , i + 1 ) 中产生的随机数与 i i 进行对比,效果是一样的。有一点可能会让人感到困惑,在运行上面这段代码的时候,假如我们前面的个数据中,最终保留下来的是num,在第 N+1 N + 1 个数据到来时,有 1−1N+1 1 − 1 N + 1 的概率选择num,当 N N 逐渐增大时,我们可能会发现新到来的数据一直无法取代num,这是正确的。要知道,虽然当前这一步的比较中,num被保留下来的概率很大,但是num可是经过了前面好多轮的激烈竞争的呀,这些概率得乘起来呢,最终其概率还是。
例2
给出一个数据流,该数据流无限长或者长度未知。数据流中的每个数据只能访问一次。请写出一个随机选择算法,在抽取k个数据的情形下,数据流中所有的数据被选中的概率相等。
这道题与例1差别不大,仅仅只是例1中取一个样本,而本题要取
k
k
个样本,其思路是一致的。一句话总结本题,其要求是:取到第个元素时,前
n
n
个元素被留下的几率相等,即。下面对情形进行归纳:
假设我们要抽取的数据量
k
k
是小于数据流中的数据量的。我们将数据放置于memory中,初始情况时,memory中没有数据。
当到达的数据量小于时,这些数据被留下的概率为1。
当第
k+1
k
+
1
个数据到达时,设这个元素去替换掉memory中某个元素的概率为
kk+1
k
k
+
1
,那么不管怎样,这个元素在memory中出现的概率一定是
kk+1
k
k
+
1
。因为这个元素将用于替换memory中已存在的各个元素,其中任意一个元素被替换掉的概率为
kk+1∗1k=1k+1
k
k
+
1
∗
1
k
=
1
k
+
1
,换句话说,memory中任意一个元素最终仍然出现的概率为
kk+1
k
k
+
1
,也即memory中的旧元素最终被输出的概率等于新元素最终被输出的概率,均为
kk+1
k
k
+
1
。如果第
k+1
k
+
1
个数据没有被选择用于替换memory中的数据,那么memory中所有数据被输出的可能性显然相等。
好了,下面我们给出代码:
import random
def reservoir_sample():
memory = []
k = input("input k (the length of memory):")
if k == 0:
print "memory size is zero."
data = input("input: ")
memory.append(data)
if k == 1:
return memory
else:
i = 1
while i < k: # assume k is 2
memory.append(input("input: "))
i = i + 1
print "Memory is full." # i = k
temp = input("input: ")
randnum = random.randint(0, i) #randint(0,2)-->(0,1,2)
while temp != "ok":
if randnum < k:
memory[randnum] = temp
temp = input("input: ")
i = i + 1
randnum = random.randint(0, i)
return memory
def main():
data = reservoir_sample()
print data
if __name__ == "__main__":
main()
上面这段代码中,我们用的是randint,这与例1中的uniform有什么不同呢?randint(0, i)表示从[0, i]的整数中任取一个,而uniform(0, i)则是从连续的随机数中任取一个。以randint(0, 2)与uniform(0, 3)为例,randint(0, 2)表示从0,1,2中任取一个,如果取到0,1,则表示替换掉memory中的数据,而且0,1这两个位置的数据被替换掉的概率相等。如果用uniform(0, 3),则应该考虑当随机数处于(0, 2)这个区间则替换memory中的数据,而(2, 3)则保留memory中的数据不变,或者说,uniform(0, 3)可以很方便地将区间分为3份,与randint(0, 2)是对应的。本题使用randint是为了更加方便的利用randnum来表示数组下标。
我们回到本文最开始提出的问题:“如何在不知道全局分布的前提下,采样得到一个数据集,这个数据集的分布与全局分布近似相同呢?”
在强化学习中,我们采样得到的数据是服从全局分布的,也就是说,如果我们能够让这些数据在memory中留存的概率相等,那么memory中的数据分布可以近似看作与全局分布一致。
好啦,本文就说到这里咯~