ランダムサンプリング
概要
ランダムサンプリング (RS; random sampling) は統計学やデータ分析において大規模な集合 (母集団) から無作為にデータを抽出する手法である。母集団の各要素が等しい確率で選ばれるサンプリングでは、得られたサンプルは母集団全体の特性を統計的に正しく反映していることが期待される。社会科学分野での実験的統計のために利用されることも多いが、このページではコンピュータ分野におけるランダムサンプリングのアルゴリズムに焦点を当てている。
Table of Contents
- 概要
- サンプリングの特徴
- 確率的サンプリング
- 選択サンプリング
- 累積和法
- Alias 法
- min-wise 独立なハッシュを使う方法
- Naive サンプリング
- Reservoir サンプリング法
- 重み付き非復元サンプリングの不公平性
- ウィンドウサンプリング
- 参考文献
サンプリングの特徴
一様/重み付きサンプリング
集合の各要素が「選ばれやすさ」に関連するパラメータを持っている場合は重み付きランダムサンプリング (WRS; weighted random sampling) あるいは非等価ランダムサンプリング (unequal random sampling) と呼ばれる。WRS に対して重みを持たないランダムサンプリングを一様ランダムサンプリング (uniformed random sampling) と呼ぶ。
分散システムにおいてマシンの相対的な性能でタスク分配の優先度を重み付けしたり、Web 広告での関連度や出稿料金でランダムに広告を選択する場合などでは WRS が利用される。
復元/非復元サンプリング
母集団から続けてサンプリングを行うとき、次の要素を選択する前に抽出した要素を元の母集団に戻す方法を復元サンプリング (sampling with replacement) と呼ぶ。復元サンプリングではそれぞれの抽選は事象として独立しており、
次の要素を選択する前に抽出された要素を母集団には戻さずまだ選択されていない要素のみで抽選を続ける方法を非復元サンプリング (sampling without replacement) と呼ぶ (これらは壺問題
における復元と同じ意味)。これは数学の組み合わせ問題から一つの組み合わせをランダムに選択するのと本質的に同じである。非復元サンプリングではサンプリングの対象となる母集団は
効率化
母集団が巨大で全ての要素を検討するには非効率であったり、母集団の全ての要素を検討する必要がない場合は、サンプリングの前に部分母集団を作成する多段サンプリング (multi-stage sampling) や層化サンプリング
(stratified sampling) といった体系的サンプリング (systematic sampling) の手法が使われる。それらに対し、母集団に対して単純にサンプリングを行う手法を単純サンプリング (simple sampling) と呼ぶ。
ランダムサンプリングは乱数を使用した非決定論的な標本抽出手法である。もし結果に非決定性が不要であれば (重み付き) ラウンドロビンも検討することができる。
確率的サンプリング
確率的サンプリング (probabilistic sampling) またはベルヌーイサンプリング (Bernoulli sampling) は母集団に含まれるすべての要素が等しい確率
各要素が独立してサンプリングされるため選択される標本数は固定されておらず、
データストリーム化された母集団からベルヌーイサンプリングを行う場合はギャップサンプリングを導入すると計算効率が良い。ギャップサンプリングは、データストリーム上のそれぞれの要素で選択確率を評価する代わりに、選択間隔の分布に従う乱数を使用してサンプルを選択する手法である。
ベルヌーイサンプリングをギャップサンプリングで表現する場合、各要素の間に発生するギャップ (選択されない要素の数) は幾何分布に従うため、幾何分布に従う乱数を構築すれば良い (幾何分布は確率
use rand::Rng;
fn bernoulli_gap_sampling<T>(data: &[T], p: f64) -> Vec<&T> {
let mut rng = rand::thread_rng();
let mut sampled = Vec::new();
let mut i = gap(rng.gen(), p);
while i < data.len() {
sampled.push(&data[i]);
let k = gap(rng.gen(), p);
i += k + 1; // move to the next element after the gap
}
sampled
}
fn gap(u: f64, p: f64) -> usize {
(u.ln() / (1.0 - p).ln()).floor() as usize
}
ポアソンサンプリング (Poisson sampling) は選択確率
選択サンプリング
選択サンプリング (selection sampling) または Algorithm S は
すでに集合内の
累積和法
累積和法 (cumulative sum) は Algorithm D [1] とも呼ばれるシンプルな WRS アルゴリズムである。母集団に含まれている要素を直列に並べ、一様乱数が重み累積のどの要素の区間に含まれるかで判定する (これは累積離散確率分布から逆変換サンプリングで乱数を生成する方法と同じである)。個人的に重みの円グラフを高速回転させ矢を放つ Fig 1 で示すような回転ダーツを用いて説明することが多い。
以下は累積和法による重み付き非復元サンプリングで選択された要素のインデックスを参照するサンプルコードである。
use rand;
use crate::wrs::Weighted;
pub fn sampling<T: Weighted>(population: &[&T]) -> usize {
let total_weight = population.iter().map(|e| e.weight()).sum::<f64>();
let threshold = total_weight * rand::random::<f64>();
let mut cumsum = 0.0;
for (i, e) in population.iter().enumerate() {
if cumsum <= threshold && threshold < cumsum + e.weight() {
return i;
}
cumsum += e.weight();
}
panic!()
}
単純な累積和法は線形探索となることから
Alias 法
Alias 法 (Walker's alias method) は離散確率分布からランダムサンプリングを行うための効率的なアルゴリズム。正方ヒストグラム (square histogram, エイリアス) [2] を作成し、横方向と縦方向のたかだか 2 回の一様ランダムサンプリングで WRS を行うことができる。正方ヒストグラムの作成に
ロビンフッド法
正方ヒストグラムはロビンフッド法 (Robin Hood method) で作成することができる。これは平均より高いものから低いものへ、低いほうが平均と一致する量の富を移動する操作を、貧富の差がなくなるまで (全てが平均と等しい富を持つまで) 繰り返す方法である (Fig 2 参照)。
各要素
正方ヒストグラムの生成
Alias 法ではロビンフッド法に基づいて以下の手順で
-
で初期化する。 -
となるような が存在するかぎり以下の 3 ステップを繰り返す。-
となるような を適当に選択する ( が存在するなら も必ず存在する)。以下の 2 ステップは から へ の量を移動する意味の操作である。 -
を設定する。 -
へ再設定する。
-
- すべての区分
に対してしきい値 を計算する。
この操作によって 1 つまたは 2 つの領域を持つ等しい高さ
操作 2.1. の判断があるため正方ヒストグラムの作成は浮動小数点の誤差に非常に敏感である。
ラフな対処としては、
サンプリングの実行
サンプリング操作は、ランダムに選んだ区分
-
となるような一様乱数 を生成する。 -
, とする (つまり )。、 - 区分
において であれば下の確率域の 、そうでなければ上の確率域の をサンプルとする。
Alias 法サンプリングの実装
以下は Alias 法に基づく WRS の実装例である。ここでは overfull, underfull となるように分類している。
min-wise 独立なハッシュを使う方法
入力に対して出力値のランダム性さが保証されているハッシュ関数を
Naive サンプリング
ある母集団から複数の標本を選択する最も単純な方法は、1 つの標本を選択するランダムサンプリング (抽選) を既定の個数が選択されるまで繰り返すことである。
この方法での復元サンプリングは各要素ごとに何回選択されたかの回数が生成される。これは選択回数を確率変数とした離散確率分布 (多項分布) と等価である。
一方で、少数の母集団に対する復元サンプリングは重複選択の発生する頻度が高く、それによって規定数からの差異が無視できないなくなることがある。シミュレーションなどで WRS を扱う場合は重複選択なく正確に
Reservoir サンプリング法
Reservoir (リザーバー) サンプリングは母集団のサイズが未知のデータストリームから非復元サンプリングで特定の個数のサンプルを抽出するときに利用できるアルゴリズムである。
Algorithm R
Algorithm R と呼ばれるサンプリング手法は未知の大きさのデータセットから等しい確率で
データセットの先頭の要素を
データセットの先頭に現われる
の要素を無条件で候補 (リザーバー) に入れる。-
の評価では、要素 に対して の乱数を生成し、 であれば候補内の位置 の要素と置き換える。データセットの最後に到達するまでこの操作を繰り返す。
🎲サンプルコード
use rand::Rng;
struct AlgorithmR<T> {
n: usize,
reservoir: Vec<T>,
i: u64, // 最初の要素を i=0 とする
}
impl<T: Clone> AlgorithmR<T> {
fn new(n: usize) -> Self {
AlgorithmR { n, reservoir: Vec::with_capacity(n), i: 0 }
}
fn add(&mut self, element: T) {
if self.i < self.n as u64 {
// 最初の n 個の要素はそのままリザーバに入れる
self.reservoir.push(element.clone());
} else {
// n+1 個目以降の要素はランダムにリザーバ内の要素と入れ替える
let mut rng = rand::thread_rng();
let j: u64 = rng.gen_range(0..=self.i);
if j < self.n as u64 {
self.reservoir[j as usize] = element.clone();
}
}
self.i += 1;
}
fn get_samples(&self) -> &Vec<T> {
&self.reservoir
}
} 証明
帰納法を使う。アルゴリズムの目標は
要素
バイアス付き Reservoir サンプリング
Algorithm R の Reservoir サンプリングはデータストリームから偏りのないサンプルを抽出するが、サンプルは時間の経過により関連や関心が薄くなることがある。特に長期間にわたる計測でストリームの長さが大きくなるとサンプル間の期間は広がり、新しい要素がサンプルへ挿入される機会が著しく減少する。このような理由でサンプリングされる確率が逆指数的に減少するアルゴリズムが提案されている [6]。
新しく到着した要素は確率
🎲サンプルコード [7] と実行例
到着した最新の要素が必ず一度リザーバーに入る
use rand::Rng;
struct BiasedReservoir<T> {
n: usize,
reservoir: Vec<Option<T>>,
occupied: usize,
}
impl<T: Clone> BiasedReservoir<T> {
fn new(lambda: f64) -> Self {
let n = (1.0 / lambda).floor() as usize;
let reservoir = vec![None; n];
BiasedReservoir { n, reservoir, occupied: 0 }
}
fn add(&mut self, element: T) {
let mut rng = rand::thread_rng();
let u = rng.gen_range(0..self.n);
if u < self.occupied {
let j = rng.gen_range(0..self.occupied);
self.reservoir[j] = Some(element);
} else {
let j = self.occupied;
self.reservoir[j] = Some(element);
self.occupied += 1;
}
}
fn get_samples(&self) -> &Vec<Option<T>> {
&self.reservoir
}
}
Algorithm A
Algorithm A と呼ばれている非復元 WRS アルゴリズム [1] は母集団から式 (
val samples = population
.sortBy { e => - math.pow(math.random, 1 / e.weight()) }
.take(m)
A アルゴリズムは非常に単純で見通しが良く簡易実装に向いているが、母集団のサイズと同じ回数
しかし
Reservoir サンプリングでは「Reservoir 内の最小の
Algorithm A-Res
Algorithm A-Res (A-Reservoir) はサイズ
- 先頭の
個の要素に対して を計算し Reservoir に挿入する。 -
個目以降の要素に対して を計算し、Reservoir に含まれる要素で最も小さい より大きければ、その最も小さい を持つ要素と置き換える。 - 最終的に Reservoir に残った要素が非復元 WRS によって得られた標本である。
use rand;
use std::collections::BinaryHeap;
use crate::wrs::a::Sample;
use crate::wrs::Weighted;
pub fn sampling<'s, T: Weighted>(population: &[&'s T], m: usize) -> Vec<&'s T> {
assert!(m <= population.len());
let mut reservoir = BinaryHeap::new(); // in general, priority queue
for i in 0..m {
let k = rand::random::<f64>().powf(1.0 / population[i].weight());
reservoir.push(Sample { value: population[i], key: k });
}
for i in m..population.len() {
let k = rand::random::<f64>().powf(1.0 / population[i].weight());
if k > reservoir.peek().unwrap().key {
reservoir.pop(); // remove a sample with the smallest key
reservoir.push(Sample { value: population[i], key: k });
}
}
assert!(reservoir.len() == m);
reservoir.iter().map(|s| s.value).collect()
}
A-Res は A と同様に
Algorithm A-ExpJ
Algorithm A-ExpJ (A-Exponential Jump) は A-Res の計算量を大幅に削減した改良バージョンである。A-Res において新しい要素が Reservoir に入るまでスキップされる要素の重みの合計を
use rand;
use std::collections::BinaryHeap;
use crate::wrs::a::Sample;
use crate::wrs::Weighted;
pub fn sampling<'s, T: Weighted>(population: &[&'s T], m: usize) -> Vec<&'s T> {
assert!(m <= population.len());
let mut reservoir = BinaryHeap::new(); // in general, priority queue
for i in 0..m {
let k = rand::random::<f64>().powf(1.0 / population[i].weight());
reservoir.push(Sample { value: population[i], key: k });
}
let mut x = rand::random::<f64>().ln() / reservoir.peek().unwrap().key.ln();
for i in 0..m {
x -= population[i].weight();
if x <= 0.0 {
let t = reservoir.peek().unwrap().key.powf(population[i].weight());
let r = (t + (1.0 - t) * rand::random::<f64>()).powf(1.0 / population[i].weight());
reservoir.pop();
reservoir.push(Sample { value: population[i], key: r });
x += rand::random::<f64>().ln() / reservoir.peek().unwrap().key.ln();
}
}
assert!(reservoir.len() == m);
reservoir.iter().map(|s| s.value).collect()
}
A-ExpJ も A-Res と同様に母集団全体をメモリ上に保持する必要がなくデータストリームとして扱うことができる。
アルゴリズム比較
以下はそれぞれの非復元 WRS アルゴリズムのコスト理論値と、上記のサンプル Rust コードを実際に Core i7 2.7GHz macOS 10.14 で実行した結果である。
| 累積和法 | A-Res | A-ExpJ | |
|---|---|---|---|
| 最悪計算量 | |
|
|
| 乱数生成 | |
|
|
| べき乗演算 | |
|
|
| |
524 [nsec] | 4,277 [nsec] | 723 [nsec] |
| |
1,769 [nsec] | 4,991 [nsec] | 2,331 [nsec] |
| |
9,090 [nsec] | 40,357 [nsec] | 2,138 [nsec] |
| |
96,336 [nsec] | 374,878 [nsec] | 2,146 [nsec] |
| |
76,089 [nsec] | 43,913 [nsec] | 16,592 [nsec] |
累積和法は標本数
使用したベンチマーク
extern crate test;
use test::Bencher;
use super::*;
const N: usize = 100;
const M: usize = 25;
struct Element(f64);
impl Weighted for Element {
fn weight(&self) -> f64 { self.0 }
}
#[bench]
fn bench_cumulative_sum(b: &mut Bencher) {
let set = generate_population(N, dist_inverse);
let population = set.iter().map(|e| e).collect::<Vec<&Element>>();
b.iter(|| {
cumsum::without_replacement::sampling(&population[..], M);
});
}
#[bench]
fn bench_algorithm_a_res(b: &mut Bencher) {
let set = generate_population(N, dist_inverse);
let population = set.iter().map(|e| e).collect::<Vec<&Element>>();
b.iter(|| {
a::res::sampling(&population[..], M);
});
}
#[bench]
fn bench_algorithm_a_expj(b: &mut Bencher) {
let set = generate_population(N, dist_inverse);
let population = set.iter().map(|e| e).collect::<Vec<&Element>>();
b.iter(|| {
a::expj::sampling(&population[..], M);
});
}
fn dist_inverse(_: usize, x: f64) -> f64 { 1.0 / (x + 0.2).powf(2.0) }
fn generate_population(n: usize, dist: fn(usize, f64) -> f64) -> Vec<Element> {
let mut population = Vec::<Element>::with_capacity(n);
for i in 0..n {
population.push(Element(dist(i, i as f64 / n as f64)));
}
population
}
重み付き非復元サンプリングの不公平性
重み付き非復元ランダムサンプリングには要素の重みと実際の選択頻度が一致しないという特徴的な挙動がある。以下のシミュレーションは非復元ランダムサンプリングを行って各要素の選択された回数と勝率を示しているが、重みが均一でない場合に重みと勝率が一致しない傾向が見て取れる。
ウィンドウサンプリング
大規模データセットに対して最新のデータのみに興味があるアプリケーションでは、直近の
チェーンサンプリング
データストリーム上に 1 から
Fig 5 はチェーンサンプリングのサンプラーを表している。現在サンプルとして選択されている 1 つの要素
🎲 サンプルコード
以下は [7] に基づく。オリジナルの [8] から、
use rand::Rng;
use std::collections::VecDeque;
use std::cmp::min;
struct ChainSampling<T> {
n: usize, // ウィンドウサイズ
i: usize, // 次に到着するデータのインデックス (最初の要素が 1)
k: usize, // 選択されているがまだ到達していないデータのインデックス
chain: VecDeque<(T, usize)>,
}
impl<T: Clone> ChainSampling<T> {
fn new(n: usize) -> Self {
Self { n, i: 1, k: 0, chain: VecDeque::new() }
}
// 新しい要素の到着を逐次的に通知する
fn add(&mut self, element: T) {
if let Some((_, j)) = self.chain.front() {
if self.i == j + self.n {
self.chain.pop_front();
}
}
let mut rng = rand::thread_rng();
let u = rng.gen_range(0.0..1.0);
if u < 1.0 / min(self.i, self.n) as f64 {
self.chain.clear(); // サンプルをクリアして新たに追加
self.chain.push_back((element.clone(), self.i));
let u = rng.gen_range(0.0..1.0);
self.k = self.i + (self.n as f64 * u).floor() as usize + 1;
} else if self.i == self.k {
self.chain.push_back((element.clone(), self.i));
let u = rng.gen_range(0.0..1.0);
self.k = self.i + (self.n as f64 * u).floor() as usize + 1;
}
self.i += 1;
}
// 現在のウィンドウ内でランダムに選択されたサンプルを取得
fn get_sample(&self) -> Option<&T> {
self.chain.front().map(|(value,_)| value)
}
}
優先度サンプリング
優先度サンプリング (priority sampling) [7, 8] はある固定の時間ウィンドウの中でランダムサンプリングを行う。この基本的な考え方は、データストリームから到着した要素ごとに一様乱数の優先度
時間ウィンドウの幅
- 最初に
の優先度 を一様にランダムに割り当てる。 - 次に
の先頭から順に時刻が より前のエントリ (時間ウィンドウから外れたエントリ) を削除する。 - 次に
の末尾から順に優先度が より小さいエントリ (時間ウィンドウ内でもう選択されることがないエントリ) を削除する。 - 最後に
の末尾にタプル を格納する。
この動作により
参考文献
- Efraimidis P, Spirakis P. Weighted Random Sampling. In: Kao MY. (eds) Encyclopedia of Algorithms. Springer, New York, NY (日本語訳)
- G. Marsaglia, W. W. Tsang, and J. Wang. Fast Generation of Discrete Random Variables, Journal of Statistical Software, 11(3):1-11, 2004. (日本語訳)
- 汪 金芳, 標本調査法入門, 平成 17 年5 月 16 日
- BRODER, Andrei Z., et al. Min-wise independent permutations. In: Proceedings of the thirtieth annual ACM symposium on Theory of computing. 1998. p. 327-336.
- KNUTH, Donald E. The Art of Computer Programming: Seminumerical Algorithms, Volume 2. Addison-Wesley Professional, 2014.
- Charu C. Aggarwal. 2006. On biased reservoir sampling in the presence of stream evolution. In Proceedings of the 32nd international conference on Very large data bases (VLDB '06). VLDB Endowment, 607–618.
- Dzejla Medjedovic, Emin Tahirovic, Ines Dedovic. 大規模データセットのためのアルゴリズムとデータ構造. マイナビ出版 (2024)
- B. Babcock, M. Datar, and M. Rajeev, Sampling from a Moving Window Over Streaming Data. Annual ACM-SIAM Symposium on Discrete Algorithms, pp. 633-634, 2002


