Reservoir sampling
Reservoir sampling is a family of randomized algorithms for choosing a simple random sample, without replacement, of items from a population of unknown size in a single pass over the items. The size of the population is not known to the algorithm and is typically too large for all 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 over the part of the population seen so far.
Motivation
Suppose we see a sequence of items, one at a time. We want to keep 10 items in memory, and we want them to be selected at random from the sequence. If we know the total number of items and can access the items arbitrarily, then the solution is easy: select 10 distinct indices between 1 and with equal probability, and keep the -th elements. The problem is that we do not always know the exact in advance.Simple: Algorithm R
A simple and popular but slow algorithm, Algorithm R, was created by Jeffrey Vitter.Initialize an array indexed from to, containing the first items of the input. This is the reservoir.
For each new input, generate a random number uniformly in. If, then set Otherwise, discard.
Return after all inputs are processed.
This algorithm works by induction on.
While conceptually simple and easy to understand, this algorithm needs to generate a random number for each item of the input, including the items that are discarded. The algorithm's asymptotic running time is thus. Generating this amount of randomness and the linear run time causes the algorithm to be unnecessarily slow if the input population is large.
This is Algorithm R, implemented as follows:
ReservoirSample
// fill the reservoir array
for i := 1 to k
R := S
end
// replace elements with gradually decreasing probability
for i := k+1 to n
j := randomInteger
if j <= k
R := S
end
end
end
Optimal: Algorithm L
If we generate random numbers independently, then the indices of the smallest of them is a uniform sample of the k-subsets of.The process can be done without knowing :
Keep the smallest of that has been seen so far, as well as, the index of the largest among them.Now couple this with the stream of inputs. Every time some is accepted, store the corresponding. Every time some is discarded, discard the corresponding.
For each new, compare it with. If, then discard, store, and set to be the index of the largest among them. Otherwise, discard, and set.
This algorithm still needs random numbers, thus taking time. But it can be simplified.
First simplification: it is unnecessary to test new one by one, since the probability that the next acceptance happens at is, that is, the interval of acceptance follows a geometric distribution.
Second simplification: it's unnecessary to remember the entire array of the smallest of that has been seen so far, but merely, the largest among them. This is based on three observations:
- Every time some new is selected to be entered into storage, a uniformly random entry in storage is discarded.
- has the same distribution as, where all independently. This can be sampled by first sampling, then taking.
ReservoirSample
// fill the reservoir array
for i = 1 to k
R := S
end
generates a uniform
W := exp)/k)
while i <= n
i := i + floor)/log) + 1
if i <= n
R := S // random index between 1 and k, inclusive
W := W * exp)/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 thus, which is optimal. At the same time, it is simple to implement efficiently and does not depend on random deviates from exotic or hard-to-compute distributions.
With random sort
If we associate with each item of the input a uniformly generated random number, the items with the largest associated values form a simple random sample. A simple reservoir-sampling thus maintains the items with the currently largest associated values in a priority queue.-> Remove the item with minimum key
Insert -> Adds item with specified key
*)
ReservoirSample
H := new min-priority-queue
while S has data
r := random // uniformly random between 0 and 1, exclusive
if H.Count < k
H.Insert
else
// keep k items with largest associated keys
if r > H.Minimum
H.Extract-Min
H.Insert
end
S.Next
end
return items in H
end
The expected running time of this algorithm is 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 be, and the sum of all weights be. There are two ways to interpret weights assigned to each item in the set:
- 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 is the current sample, then the probability of an item to be selected in the current round is.
- The probability of each item to be included in the random sample is proportional to its relative weight, i.e.,. It is clear that it is not achievable in the case where.,.
Algorithm [|A-Res]
The following algorithm was given by Efraimidis and Spirakis that uses interpretation 1:-> returns minimum key value of all items
Extract-Min -> Remove the item with minimum key
Insert -> Adds item with specified key
*)
ReservoirSample
H := new min-priority-queue
while S has data
r := random ^ // random produces a uniformly random number in
if H.Count < k
H.Insert
else
// keep k items with largest associated keys
if r > H.Minimum
H.Extract-Min
H.Insert
end
end
S.Next
end
return items in H
end
Algorithm A-ExpJ
The following algorithm is a more efficient version of A-Res, also given by Efraimidis and Spirakis:-> Remove the item with minimum key
Insert -> Adds item with specified key
*)
ReservoirSampleWithJumps
H := new min-priority-queue
while S has data and H.Count < k
r := random ^ // random produces a uniformly random number in
H.Insert
S.Next
end
X := log) / log // 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 ^ // random produces a uniformly random number in
H.Extract-Min
H.Insert
X := log) / log
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 to in expectation, where is the reservoir size, and is the number of items in the stream.
Algorithm A-Chao
Warning: the following description is wrong, see Chao's original paper and the discussion .Following algorithm was given by M. T. Chao uses interpretation 2: and Tillé.
WeightedReservoir-Chao
WSum := 0
// fill the reservoir array
for i := 1 to k
R := S
WSum := WSum + S.Weight
end
for i := k+1 to n
WSum := WSum + S.Weight
p := S.Weight / WSum // probability for this item
j := random; // uniformly random between 0 and 1
if j <= p // select item according to probability
R := S //uniform selection in reservoir for replacement
end
end
end
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 π".
Algorithm A-Chao with Jumps
Similar to the other algorithms, it is possible to compute a random weightj and subtract items' probability mass values, skipping them while j > 0, reducing the number of random numbers that have to be generated.WeightedReservoir-Chao
WSum := 0
// fill the reservoir array
for i := 1 to k
R := S
WSum := WSum + S.Weight
end
j := random // uniformly random between 0 and 1
pNone := 1 // probability that no item has been selected so far
for i := k+1 to n
WSum := WSum + S.Weight
p := S.Weight / WSum // probability for this item
j -= p * pNone
pNone := pNone *
if j <= 0
R := S //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 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 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
Input:
* Stream
* BufferSize M
* TargetDistribution
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_.
- Compute target distribution p_.
- For each class k in the buffer:
- Simulate removing one instance of class k and adding.
- Compute KL divergence v_k between p_ and the modified buffer distribution.
- Select the class k* with the minimum v_k.
- If k* = y_t:
- Generate random number r in .
- If r ≤ m_,
- replace a random instance of class y_t in the buffer with.
- Else, replace a random instance of the selected class k* in the buffer with.
- Update buffer counters m_
Relation to Fisher–Yates shuffle
Suppose one wanted to draw random cards from a deck of cards.A natural approach would be to shuffle the deck and then take the top cards.
In the general case, the shuffle also needs to work even if the number of cards in the deck is not known in advance, a condition which is satisfied by the inside-out version of the Fisher–Yates shuffle:
Shuffle
R := S
for i from 2 to n do
j := randomInteger // inclusive range
R := R
R := S
end
end
Note that although the rest of the cards are shuffled, only the first are important in the present context.
Therefore, the array need only track the cards in the first positions while performing the shuffle, reducing the amount of memory needed.
Truncating to length, the algorithm is modified accordingly:
ReservoirSample
R := S
for i from 2 to k do
j := randomInteger // inclusive range
R := R
R := S
end
for i from k + 1 to n do
j := randomInteger // inclusive range
if
R := S
end
end
end
Since the order of the first cards is immaterial, the first loop can be removed and can be initialized to be the first items of the input.
This yields Algorithm R.