Reservoir sampling

From HandWiki
Short description: Randomized algorithm

Reservoir sampling is a family of randomized online algorithms for choosing a simple random sample, without replacement, of k items from a population of unknown size n in a single pass over the items. The size of the population n is not known to the algorithm and is typically too large for all n items to fit into main memory. The population is revealed to the algorithm over time, and the algorithm cannot look back at previous items. At any point, the current state of the algorithm must permit extraction of a simple random sample without replacement of size k over the part of the population seen so far.

Example

Consider the case when we want to maintain a sample of one item. A simple algorithm is to store the first item with probability 1 and the k-th item with probability 1k. We want to show that the sample is uniformly distributed over all of the items that we have seen at each step since.[1]

The proof is by induction on the number of items n. If there is 1 item we always choose it and we are done. If there are already n items, then the probability of one of the items already in the store remaining the store becomes nn+1×1n=1n+1 so the probability of any of the n+1 items remaining in the sample is now 1n+1. The proof of correctness for the general algorithm where we have an arbitrary number of slots is an extension of this argument.

History and Algorithm R

Reservoir sampling was introduced as early as 1962 by Fan, Muller, and Rezucha, who described a sampling algorithm which did not need to know the population size n in advance.[2] A simpler algorithm, known as Algorithm R, was discovered independently by Alan G. Waterman[3][4] and McLeod & Bellhouse;[5] it can also be viewed as a variant of the Fisher–Yates shuffle.[6] Algorithm R works as follows.

  • Initialize an array R indexed from 1 to k, containing the first k items of the input x1,...,xk. This is the reservoir.
  • For each new input xi, generate a random number j uniformly in {1,...,i}. If j∈{1,...,k}, then set R[j]:=xi. Otherwise, discard xi.
  • Return R after all inputs are processed.

Optimizing running time by skipping elements

The Algorithm R described in the previous section has linear runtime, since it generates a random number for every element in the input stream to determine whether it should be included in the reservoir. It was observed by Jeffrey Vitter that it is possible to obtain faster algorithms with sublinear runtime when the algorithm is allowed to skip past sections of the input stream without examining them. Vitter gave a reservoir sampling algorithm which runs in time O(k(1+log⁡(n/k)), and proved that this is optimal.[7][4] Kim-Hung Li described a simple algorithm, Algorithm L, achieving the same asymptotic runtime, and which is described in this section.[8]

The starting point for Algorithm L is the observation that if n random numbers u1,...,un∼U[0,1] are generated uniformly and independently, then the indices of the smallest k of them form a uniform sample of the k-subsets of {1,...,n}.

The process can be done without knowing

n

:

Keep the smallest

k

of

u1,...,ui

that has been seen so far, as well as

wi

, the index of the largest among them. For each new

ui+1

, compare it with

uwi

. If

ui+1<uwi

, then discard

uwi

, store

ui+1

, and set

wi+1

to be the index of the largest among them. Otherwise, discard

ui+1

, and set

wi+1=wi

.

Now couple this with the stream of inputs

x1,...,xn

. Every time some

ui

is accepted, store the corresponding

xi

. Every time some

ui

is discarded, discard the corresponding

xi

.

This algorithm still needs O(n) random numbers, thus taking O(n) time. However, it can be improved by observing that after each step, the number of following items which will be skipped by the algorithm follows a geometric distribution with parameter uwi. Thus, the sampling algorithm can simply generate a sample from this distribution to determine how many items to skip over, rather than examining them one at a time. A further simplification is to store only the maximum value uwi at each point, and avoid storing u1,…,ui. This results in Algorithm L, which is implemented as follows:

// S has items to sample, R will contain the result
ReservoirSample(S[1..n], R[1..k])
  // fill the reservoir array with the first k items
  for i = 1 to k
      R[i] := S[i]
  end

  // random() generates a uniform (0,1) random number
  W := exp(log(random())/k)

  while i <= n
      // skip items according to a geometric distribution
      i := i + floor(log(random())/log(1-W)) + 1
      if i <= n
          // replace a random item of the reservoir with item i
          R[randomInteger(1,k)] := S[i]
          W := W * exp(log(random())/k)
      end
  end
end

This algorithm computes three random numbers for each item that becomes part of the reservoir, and does not spend any time on items that do not. Its expected running time is O(k(1+log⁡(n/k))), which is optimal.

Sampling with replacement and the single-item case

Reservoir sampling can also be performed with replacement, meaning that the same input item may appear more than once in the final sample. In sampling without replacement, each input item can appear at most once. For a sample of size one, however, the resulting sample distribution is the same.

Park, Ostrouchov, Samatova, and Geist (2004) derived a skip-based algorithm for reservoir sampling with replacement.[9]

For a reservoir of size one, suppose the currently selected item is at 1-based position n in the stream. The position of the next replacement can be sampled directly from a single uniform random value q:

n′=⌊nq⌋+1.

This gives a particularly simple algorithm:

if stream is empty
    return no sample

sample := first item
n := 1

loop
    q := random()   // uniform in (0,1)
    n := floor(n / q) + 1

    advance stream to 1-based item n
    if stream is exhausted
        break

    sample := item n

return sample

Here random() denotes a uniform random value strictly between 0 and 1. An implementation whose random-number generator can return an endpoint must exclude or resample such values. The variable n is the absolute 1-based position in the stream, rather than a count of items to skip. The algorithm therefore generates replacement positions directly instead of making a random replacement decision for every input item.

For a reservoir of size one, each new replacement position requires only one random variate. By comparison, Algorithm L normally uses three. When Algorithm L is specialized to k = 1, choosing the reservoir slot requires no randomness, leaving two nontrivial random variates per replacement.[8]

A weighted generalization was later given by Meligrana and Fazzone.[10]

For a single-item weighted reservoir with current cumulative weight W, the cumulative-weight threshold for the next replacement is

Wq,

where q is uniform on (0,1). The stream is scanned until its cumulative weight reaches that threshold.

With random sort

If we associate with each item of the input a uniformly generated random number, the k items with the largest (or, equivalently, smallest) associated values form a simple random sample.[11] A simple reservoir-sampling thus maintains the k items with the currently largest associated values in a priority queue.

(*
  S is a stream of items to sample
  S.Current returns current item in stream
  S.Next advances stream to next position
  min-priority-queue supports:
    Count -> number of items in priority queue
    Minimum -> returns minimum key value of all items
    Extract-Min() -> Remove the item with minimum key
    Insert(key, Item) -> Adds item with specified key
 *)
ReservoirSample(S[1..?])
  H := new min-priority-queue
  while S has data
    r := random()   // uniformly random between 0 and 1, exclusive
    if H.Count < k
      H.Insert(r, S.Current)
    else
      // keep k items with largest associated keys
      if r > H.Minimum
        H.Extract-Min()
        H.Insert(r, S.Current)
    end
    S.Next
  end
  return items in H
end

The expected running time of this algorithm is O(n+klog⁡klog⁡(n/k)) and it is relevant mainly because it can easily be extended to items with weights.

Weighted random sampling

The methods presented in the previous sections do not allow to obtain a priori fixed inclusion probabilities. Some applications require items' sampling probabilities to be according to weights associated with each item. For example, it might be required to sample queries in a search engine with weight as number of times they were performed so that the sample can be analyzed for overall impact on user experience. Let the weight of item i be wi, and the sum of all weights be W. There are two ways to interpret weights assigned to each item in the set:[12]

  1. In each round, the probability of every unselected item to be selected in that round is proportional to its weight relative to the weights of all unselected items. If X is the current sample, then the probability of an item i∉X to be selected in the current round is wi/(W−∑j∈Xwj).
  2. The probability of each item to be included in the random sample is proportional to its relative weight, i.e., wi/W. It is not achievable when k=n, since each item is then included with probability 1.

Algorithm A-Res

The following algorithm was given by Efraimidis and Spirakis that uses interpretation 1:[13]

(*
  S is a stream of items to sample
  S.Current returns current item in stream
  S.Weight  returns weight of current item in stream
  S.Next advances stream to next position
  The power operator is represented by ^
  min-priority-queue supports:
    Count -> number of items in priority queue
    Minimum() -> returns minimum key value of all items
    Extract-Min() -> Remove the item with minimum key
    Insert(key, Item) -> Adds item with specified key
 *)
ReservoirSample(S[1..?])
  H := new min-priority-queue
  while S has data
    r := random() ^ (1/S.Weight)   // random() produces a uniformly random number in (0,1)
    if H.Count < k
      H.Insert(r, S.Current)
    else
      // keep k items with largest associated keys
      if r > H.Minimum
        H.Extract-Min()
        H.Insert(r, S.Current)
      end
    end
    S.Next
  end
  return items in H
end

This algorithm is identical to the algorithm given in Reservoir Sampling with Random Sort except for the generation of the items' keys. The algorithm is equivalent to assigning each item a key

r1/wi

where r is the random number and then selecting the k items with the largest keys. Equivalently, a more numerically stable formulation of this algorithm computes the keys as

−ln⁡(r)/wi

and select the k items with the smallest keys.[14][failed verification]

Algorithm A-ExpJ

The following algorithm is a more efficient version of A-Res, also given by Efraimidis and Spirakis:[13]

(*
  S is a stream of items to sample
  S.Current returns current item in stream
  S.Weight  returns weight of current item in stream
  S.Next advances stream to next position
  The power operator is represented by ^
  min-priority-queue supports:
    Count -> number of items in the priority queue
    Minimum -> minimum key of any item in the priority queue
    Extract-Min() -> Remove the item with minimum key
    Insert(Key, Item) -> Adds item with specified key
 *)
ReservoirSampleWithJumps(S[1..?])
  H := new min-priority-queue
  while S has data and H.Count < k
    r := random() ^ (1/S.Weight)   // random() produces a uniformly random number in (0,1)
    H.Insert(r, S.Current)
    S.Next
  end
  X := log(random()) / log(H.Minimum) // this is the amount of weight that needs to be jumped over
  while S has data
    X := X - S.Weight
    if X <= 0
      t := H.Minimum ^ S.Weight
      r := random(t, 1) ^ (1/S.Weight) // random(x, y) produces a uniformly random number in (x, y)
    
      H.Extract-Min()
      H.Insert(r, S.Current)

      X := log(random()) / log(H.Minimum)
    end
    S.Next
  end
  return items in H
end

This algorithm follows the same mathematical properties that are used in A-Res, but instead of calculating the key for each item and checking whether that item should be inserted or not, it calculates an exponential jump to the next item which will be inserted. This avoids having to create random variates for each item, which may be expensive. The number of random variates required is reduced from O(n) to O(klog⁡(n/k)) in expectation, where k is the reservoir size, and n is the number of items in the stream.[13]

Algorithm A-Chao

Warning: the following description is wrong, see Chao's original paper and the discussion here.

Following algorithm was given by M. T. Chao uses interpretation 2:[15] and Tillé (2006).[16]

(*
  S has items to sample, R will contain the result
  S[i].Weight contains weight for each item
 *)
WeightedReservoir-Chao(S[1..n], R[1..k])
  WSum := 0
  // fill the reservoir array
  for i := 1 to k
      R[i] := S[i]
      WSum := WSum + S[i].Weight
  end
  for i := k+1 to n
    WSum := WSum + S[i].Weight
    p := S[i].Weight / WSum // probability for this item
    j := random();          // uniformly random between 0 and 1
    if j <= p               // select item according to probability
        R[randomInteger(1,k)] := S[i]  //uniform selection in reservoir for replacement
    end
  end
end

For each item, its relative weight is calculated and used to randomly decide if the item will be added into the reservoir. If the item is selected, then one of the existing items of the reservoir is uniformly selected and replaced with the new item. The trick here is that, if the probabilities of all items in the reservoir are already proportional to their weights, then by selecting uniformly which item to replace, the probabilities of all items remain proportional to their weight after the replacement.

Note that Chao doesn't specify how to sample the first k elements. He simple assumes we have some other way of picking them in proportion to their weight. Chao: "Assume that we have a sampling plan of fixed size with respect to S_k at time A; such that its first-order inclusion probability of X_t is π(k; i)".

Algorithm A-Chao with Jumps

Similar to the other algorithms, it is possible to compute a random weight j and subtract items' probability mass values, skipping them while j > 0, reducing the number of random numbers that have to be generated.[12]

(*
  S has items to sample, R will contain the result
  S[i].Weight contains weight for each item
 *)
WeightedReservoir-Chao(S[1..n], R[1..k])
  WSum := 0
  // fill the reservoir array
  for i := 1 to k
      R[i] := S[i]
      WSum := WSum + S[i].Weight
  end
  j := random() // uniformly random between 0 and 1
  pNone := 1 // probability that no item has been selected so far (in this jump)
  for i := k+1 to n
    WSum := WSum + S[i].Weight
    p := S[i].Weight / WSum // probability for this item
    j -= p * pNone
    pNone := pNone * (1 - p)
    if j <= 0
        R[randomInteger(1,k)] := S[i]  //uniform selection in reservoir for replacement
        j = random()
        pNone := 1
    end
  end
end

Applications for Multi-Class Fairness

In machine learning applications, fairness is a critical consideration, especially in scenarios where data streams exhibit class imbalance. To address this, Nikoloutsopoulos, Titsias, and Koutsopoulos proposed the Kullback-Leibler Reservoir Sampling (KLRS) algorithm as a solution to the challenges of Continual Learning, where models must learn incrementally from a continuous data stream.

KLRS Algorithm

The KLRS algorithm was designed to create a flexible policy that matches class percentages in the buffer to a target distribution while employing Reservoir Sampling techniques. This is achieved by minimizing the Kullback-Leibler (KL) divergence between the current buffer distribution and the desired target distribution.

KLRS generalizes earlier methods like Reservoir Sampling and Class-Balancing Reservoir Sampling, as verified by experiments using confidence intervals, demonstrating its broader applicability and improved performance.

Algorithm Description

The KLRS algorithm operates by maintaining a buffer of size and updating its contents as new data points arrive in a stream. Below is the pseudocode for the KLRS algorithm:

Algorithm: KLRS

KLRS(Stream, BufferSize M, TargetDistribution)
  Input:
    * Stream (data points (x, y) arriving sequentially)
    * BufferSize M
    * TargetDistribution (desired class distribution in the buffer)

  Output: 
    Updated buffer maintaining the target distribution

  Initialize buffer M with the first M data points from the stream.
  
  For t = M+1 to ∞:
    - Update stream counters to compute n_{t+1}.
    - Compute target distribution p_{t+1}.
    - For each class k in the buffer:
      - Simulate removing one instance of class k and adding (x_t, y_t).
      - Compute KL divergence v_k between p_{t+1} and the modified buffer distribution.
    - Select the class k* with the minimum v_k.
    - If k* = y_t:
      - Generate random number r in [1, n_{t+1}, y_t].
      - If r ≤ m_{t, y_t},
        - replace a random instance of class y_t in the buffer with (x_t, y_t).
    - Else, replace a random instance of the selected class k* in the buffer with (x_t, y_t).
    - Update buffer counters m_{t+1].
    - Update class set C_{t+1} := unique(C_t ∪ {y_t}).

Limitations

Reservoir sampling makes the assumption that the desired sample fits into main memory, often implying that k is a constant independent of n. In applications where we would like to select a large subset of the input list (say a third, i.e. k=n/3), other methods need to be adopted. Distributed implementations for this problem have been proposed.[17]

References

  1. ↑ Mitzenmacher, Michael; Upfal, Eli (2005). Probability and computing: randomized algorithms and probabilistic analysis. Cambridge: Cambridge university press. ISBN 978-0-521-83540-4. 
  2. ↑ Fan, C.T.; Muller, Mervin E.; Rezucha, Ivan (June 1962). "Development of Sampling Plans by Using Sequential (Item by Item) Selection Techniques and Digital Computers". Journal of the American Statistical Association 57 (298): 387–402. doi:10.1080/01621459.1962.10480667. 
  3. ↑ Knuth, Donald Ervin (1981). The Art of Computer Programming (2 ed.). Reading, MA: Addison-Wesley. pp. 138–139. ISBN 0-201-03822-6. 
  4. ↑ 4.0 4.1 Vitter, Jeffrey S. (1 March 1985). "Random sampling with a reservoir". ACM Transactions on Mathematical Software 11 (1): 37–57. doi:10.1145/3147.3165. http://www.cs.umd.edu/~samir/498/vitter.pdf. 
  5. ↑ McLeod, Angus Ian; Bellhouse, David Richard (1983). "A Convenient Algorithm for Drawing a Simple Random Sample". Journal of the Royal Statistical Society. Series C (Applied Statistics) 32 (2): 182–184. doi:10.2307/2347297. 
  6. ↑ National Research Council (2013) (in English). Frontiers in Massive Data Analysis. The National Academies Press. p. 121. ISBN 978-0-309-28781-4. https://nap.nationalacademies.org/read/18374. 
  7. ↑ Vitter, Jeffrey Scott (July 1984). "Faster methods for random sampling". Communications of the ACM 27 (7): 703–718. doi:10.1145/358105.893. 
  8. ↑ 8.0 8.1 Li, Kim-Hung (4 December 1994). "Reservoir-Sampling Algorithms of Time Complexity O(n(1+log(N/n)))". ACM Transactions on Mathematical Software 20 (4): 481–493. doi:10.1145/198429.198435. 
  9. ↑ Park, Byung-Hoon; Ostrouchov, George; Samatova, Nagiza F.; Geist, Al (2004). "Reservoir-based Random Sampling with Replacement from Data Stream". pp. 492–496. doi:10.1137/1.9781611972740.53. 
  10. ↑ Meligrana, Adriano; Fazzone, Adriano (2026). "Weighted Reservoir Sampling with Replacement from Data Streams". pp. 8789–8792. doi:10.1145/3774904.3792966. 
  11. ↑ Fan, C.; Muller, M.E.; Rezucha, I. (1962). "Development of sampling plans by using sequential (item by item) selection techniques and digital computers". Journal of the American Statistical Association 57 (298): 387–402. doi:10.1080/01621459.1962.10480667. 
  12. ↑ 12.0 12.1 Efraimidis, Pavlos S. (2015). "Weighted Random Sampling over Data Streams". Algorithms, Probability, Networks, and Games. Lecture Notes in Computer Science. 9295. pp. 183–195. doi:10.1007/978-3-319-24024-4_12. ISBN 978-3-319-24023-7. 
  13. ↑ 13.0 13.1 13.2 Efraimidis, Pavlos S.; Spirakis, Paul G. (2006-03-16). "Weighted random sampling with a reservoir". Information Processing Letters 97 (5): 181–185. doi:10.1016/j.ipl.2005.11.003. 
  14. ↑ Arratia, Richard (2002). Bela Bollobas. ed. "On the amount of dependence in the prime factorization of a uniform random integer". Contemporary Combinatorics 10: 29–91. ISBN 978-3-642-07660-2. 
  15. ↑ Chao, M. T. (1982). "A general purpose unequal probability sampling plan". Biometrika 69 (3): 653–656. doi:10.1093/biomet/69.3.653. 
  16. ↑ Tillé, Yves (2006). Sampling Algorithms. Springer. ISBN 978-0-387-30814-2. 
  17. ↑ Reservoir Sampling in MapReduce