한양과학기술고등학교

에듀테크 프로젝트 대시보드

저수지 샘플링 (Reservoir Sampling)

저수지 샘플링 (Reservoir Sampling)

전체 크기를 모르는 데이터 스트림을 딱 한 번만 지나가면서, 모든 원소가 정확히 같은 확률로 뽑히는 표본 k개를 O(k) 메모리로 유지하는 알고리즘.

어떤 문제를 푸나요? 로그 파일이 100GB인데 그중 무작위 10줄만 뽑고 싶다. 총 몇 줄인지 미리 셀 수도 없고, 파일을 메모리에 다 올릴 수도 없다. 스트림이 끝나기 전에는 길이 n을 모르는데, 어떻게 "균일 무작위"를 보장할까?

1. 핵심 아이디어

표본 1개(k = 1)를 뽑는 경우부터 보자. 규칙은 딱 한 줄이다.

i번째로 도착한 원소를 확률 1/i 로 현재 표본과 교체한다.

표본이 k개인 일반형은 이렇게 확장된다: 처음 k개는 그대로 채우고, i > k인 원소는 확률 k/i 로 받아들여 저수지 안의 무작위 슬롯 하나를 덮어쓴다.

2. 알고리즘 코드

# 스트림 stream 에서 표본 k개를 균일 무작위로 뽑는다
import random

def reservoir_sample(stream, k):
    reservoir = []
    for i, item in enumerate(stream):        # i = 0, 1, 2, ...
        if i < k:
            reservoir.append(item)              # 저수지 채우기
        else:
            j = random.randint(0, i)          # 0 ~ i 중 하나
            if j < k:                          # 확률 k/(i+1)
                reservoir[j] = item            # 무작위 슬롯 교체
    return reservoir

메모리 O(k), 시간 O(n), 스트림 1회 통과. n을 미리 알 필요가 전혀 없다.

3. 한 스텝씩 따라가 보기

아래에서 스트림이 하나씩 도착한다. 각 원소마다 주사위를 굴려 저수지에 넣을지 결정한다. 저수지에 들어간(살아있는) 원소는 파란색이다.

스트림

저수지 (표본)

「다음 원소」를 눌러 시작하세요.

4. 정말 균일할까? — 10,000번 시뮬레이션

같은 스트림에서 표본 뽑기를 수천 번 반복하면, 모든 원소가 뽑힌 횟수가 거의 같아야 한다. 이론값은 각 원소마다 k / n. 아래 버튼으로 직접 확인해 보자.

이론적 선택 확률 = 30.0% (= k/n). 실행하면 각 원소의 실제 비율이 이 선에 얼마나 붙는지 볼 수 있습니다.

5. 왜 확률 k/i 로 교체하면 균일해질까?

k = 1 기준으로, 스트림 길이가 n일 때 첫 번째 원소가 최종적으로 살아남을 확률을 따져 보자.

단계일어나야 하는 일확률
2번째 도착교체 안 됨1 − 1/2 = 1/2
3번째 도착교체 안 됨1 − 1/3 = 2/3
4번째 도착교체 안 됨1 − 1/4 = 3/4
………
n번째 도착교체 안 됨(n−1)/n

이 확률들을 모두 곱하면 예쁘게 약분된다 (망원 곱, telescoping):

P(1번 원소 생존) = 1/2 · 2/3 · 3/4 · … · (n−1)/n = 1/n

중간에 도착한 m번째 원소도 똑같다: 들어올 확률 1/m × 이후 m+1 … n에서 안 밀려날 확률 m/(m+1) · … · (n−1)/n = 1/n. 모든 원소가 대칭적으로 1/n이다. 귀납법의 핵심은 "i개까지 균일하다면 i+1개째를 확률 k/(i+1)로 받아도 여전히 균일하다" 이다.

6. 어디에 쓰이나

빅데이터 · 로그 분석

전체를 저장하지 않고 대표 표본만 뽑아 통계·A/B 분석. Apache Spark의 sample, 로그 수집기의 샘플링 등.

스트리밍 시스템

Kafka·센서처럼 끝없이 들어오는 데이터에서 최근 구간의 균일 표본 유지 (분산 추적의 trace sampling).

게임 · 그래픽

조건을 만족하는 오브젝트가 몇 개인지 모를 때, 한 번 순회하며 무작위 하나 선택 (랜덤 스폰 지점 고르기).

연결 리스트 · 트리

길이를 모르는 연결 리스트에서 임의의 노드 하나를 O(1) 메모리로 뽑는 고전 면접 문제.

가중치 버전도 있다. 각 원소에 가중치가 있으면 A-Res 알고리즘: 원소 i에 키 random()^(1/wᵢ)를 부여하고 키가 큰 k개를 최소 힙으로 유지한다. (→ 힙 & 우선순위 큐 참고)