水塘抽樣(Reservoir Sampling)問題


水塘抽樣是一系列的隨機算法,其目的在於從包含n個項目的集合S中選取k個樣本,其中n為一很大或未知的數量,尤其適用於不能把所有n個項目都存放到主內存的情況。

在高德納的計算機程序設計藝術中,有如下問題:可否在一未知大小的集合中,隨機取出一元素?。或者是Google面試題: I have a linked list of numbers of length N. N is very large and I don’t know in advance the exact value of N. How can I most efficiently write a function that will return k completely random numbers from the list(中文簡化的意思就是:在不知道文件總行數的情況下,如何從文件中隨機的抽取一行?)。兩題的核心意思都是在總數不知道的情況下如何等概率地從中抽取一行?即是說如果最后發現文字檔共有N行,則每一行被抽取的概率均為1/N?

我們可以:定義取出的行號為choice,第一次直接以第一行作為取出行 choice ,而后第二次以二分之一概率決定是否用第二行替換 choice ,第三次以三分之一的概率決定是否以第三行替換 choice ……,以此類推。由上面的分析我們可以得出結論,在取第n個數據的時候,我們生成一個0到1的隨機數p,如果p小於1/n,保留第n個數。大於1/n,繼續保留前面的數。直到數據流結束,返回此數,算法結束。

 

問題一

首先考慮k為1的情況,即:給定一個長度很大或者長度未知數據流,限定對每個元素只能訪問一次,寫出一個隨機選擇算法,使得所有元素被選中的概率相等。

設當前讀取的是第n個元素,采用歸納法分析如下:

  1. n = 1 時,只有一個元素,直接返回即可,概率為1。
  2. n = 2 時,需要等概率返回前兩個元素,顯然概率為1/2。可以生成一個0~1之間的隨機數p,p < 0.5 時返回第一個,否則返回第二個。
  3. n = 3 時,要求每個元素返回的概率為1/3。注意此時前兩個元素留下來的概率均為1/2。做法是:生成一個0~1之間的隨機數,若<1/3,則返回第三個,否則返回上一步留下的那個。元素1和2留下的概率均為:1/2 * (1 - 1/3) = 1/3,即上一步留下的概率乘以這一步留下(即元素3不留下)的概率。
  4. 假設 n = m 時,前n個元素留下的概率均為:1/n = 1/m;
  5. 那么 n = m+1 時,生成0~1之間的隨機數並判斷是否<1/(m+1),若是則留下元素m+1,否則留下上一步留下的元素。這樣一來,元素m+1留下的概率為1/(m+1),前m個元素留下來的概率均為:1/m * (1 - 1/(m+1)) = 1/(m+1),也就是1/n。
  6. 綜上可知,算法成立。

 

問題二

將問題一中的條件變為,k為任意整數的情況,即要求最終返回的元素有k個,這就是水塘抽樣(Reservoir Sampling)問題。要求是:取到第n個元素時,前n個元素被留下的幾率相等,即k/n。

算法同上面思路類似,將1/n換乘k/n即可。在取第n個數據的時候,我們生成一個0到1的隨機數p,如果p小於k/n,替換池中任意一個為第n個數。大於k/n,繼續保留前面的數。直到數據流結束,返回此k個數。但是為了保證計算機計算分數額准確性,一般是生成一個0到n的隨機數,跟k相比,道理是一樣的

同樣采用歸納法來分析:

  1. 初始情況 n <= k:此時每個元素留下的概率均為1。
  2. 當 n = k+1 時,第k+1個元素留下的概率為k/(k+1),前k個元素留下的概率均為:k/k * (1 - k/(k+1) * 1/k) = k/(k+1),即上一步留下的概率乘以這一步留下的概率。
  3. 假設 n = m 時,每個元素留下的概率均為 k/n = k/m。
  4. 那么,當 n = m+1 時,第m+1個元素留下的概率為1/(m+1),前m個元素留下的概率均為:k/m * (1 - k/(m+1) * 1/k) = k/(m+1),其中:k/m為上一步留下來的概率,k/(m+1) * 1/k 為這一步不能留下來的概率(第m+1個留下來,同時池中一個元素被踢出的概率)。
  5. 綜上可知,算法成立。

 

偽代碼如下:

//stream代表數據流
//reservoir代表返回長度為k的池塘

//從stream中取前k個放入reservoir;
for ( int i = 1; i < k; i++)
    reservoir[i] = stream[i];
for (i = k; stream != null; i++) {
    p = random(0, i);
    if (p < k) reservoir[p] = stream[i];
return reservoir;

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM