몬테카를로 시뮬레이션 - 랜덤으로 복잡한 문제 풀기
원의 넓이를 구할 때, π(파이)를 어떻게 계산하는지 궁금해 본 적 있나요?
저는 처음에 “수학 공식으로 정확히 계산하는 것 아니야?”라고 생각했습니다. 하지만 랜덤한 점을 던져서 π를 근사할 수 있다는 것을 알고 깜짝 놀랐습니다!
// ❌ 복잡한 수식으로 계산
const pi = calculatePiWithComplexMath();
// ✅ 랜덤 샘플링으로 근사
const pi = estimatePiWithMonteCarloSimulation(); // 약 3.14159...
몬테카를로 시뮬레이션은 1940년대 맨해튼 프로젝트에서 처음 사용되었습니다. 물리학자들이 원자폭탄 개발 중 중성자 확산 문제를 풀 때, 너무 복잡해서 직접 계산할 수 없었습니다. 그래서 수학자 스타니스와프 울람(Stanisław Ulam)과 존 폰 노이만(John von Neumann)은 “무작위 샘플링”이라는 아이디어를 떠올렸고, 이를 당시 유명했던 모나코의 몬테카를로 카지노의 이름을 따서 “Monte Carlo Method”라고 명명했습니다.
핵심 아이디어는 간단합니다: 랜덤하게 많은 샘플을 생성하고, 그 결과를 관찰해서 확률이나 기댓값을 추정하는 것입니다. 수학적으로 정확한 답을 구하기 어려울 때, “충분히 많은 시도”를 통해 실용적인 근사값을 얻는 방법이죠.
왜 몬테카를로 시뮬레이션을 배워야 할까요?
1. 복잡한 문제를 단순하게 접근
// 복잡한 적분 문제를 수식 없이 해결
// 예: 불규칙한 도형의 넓이 구하기
function estimateArea(shape) {
let insideCount = 0;
const totalSamples = 1000000;
for (let i = 0; i < totalSamples; i++) {
const x = Math.random();
const y = Math.random();
if (shape.contains(x, y)) {
insideCount++;
}
}
return insideCount / totalSamples;
}
// 샘플이 많을수록 정확한 결과!
2. 실생활의 불확실성 모델링
// 주식 가격 시뮬레이션
function simulateStockPrice(initialPrice, days, volatility) {
let price = initialPrice;
const prices = [price];
for (let i = 0; i < days; i++) {
// 랜덤한 변화율 (-volatility ~ +volatility)
const change = (Math.random() - 0.5) * 2 * volatility;
price = price * (1 + change);
prices.push(price);
}
return prices;
}
// 100번 시뮬레이션 -> 가능한 미래 시나리오 분석
3. 다양한 분야에서 활용
실제로 몬테카를로 시뮬레이션은 이런 곳에서 사용될 수 있습니다.
- 금융: 옵션 가격 결정, 포트폴리오 리스크 분석 (블랙-숄즈 모델)
- 물리학: 입자 운동, 분자 시뮬레이션
- 게임: 게임 AI의 의사결정 (예: 알파고의 MCTS)
- 머신러닝: 베이지안 추론, 강화학습
- 공학: 품질 관리, 신뢰성 분석
- 보험: 위험 평가, 보험료 책정
먼저, 기초부터 이해하기
몬테카를로 시뮬레이션을 이해하려면, 먼저 “확률”과 “기댓값”이라는 개념을 알아야 합니다.
확률의 기본
확률은 “어떤 사건이 일어날 가능성”을 0과 1 사이의 숫자로 표현한 것입니다.
확률 = 원하는 결과가 나온 횟수 / 전체 시도 횟수
예를 들어 동전을 던져서 앞면이 나올 확률은 1/2 (50%)입니다.
// 동전 던지기 시뮬레이션
function flipCoin() {
return Math.random() < 0.5 ? '앞면' : '뒷면';
}
// 10,000번 던지면?
let heads = 0;
for (let i = 0; i < 10000; i++) {
if (flipCoin() === '앞면') heads++;
}
console.log(heads / 10000); // 약 0.5 (50%)
핵심: 시도 횟수가 많을수록 이론적 확률에 가까워집니다. 이것을 “큰 수의 법칙(Law of Large Numbers)”이라고 합니다.
랜덤 샘플링이란?
샘플링은 “전체 중에서 일부를 뽑는 것”을 의미합니다. 몬테카를로에서는 무작위로 샘플을 뽑습니다.
// 0과 1 사이의 랜덤한 숫자
const randomSample = Math.random(); // 예: 0.742...
// 두 개의 랜덤한 좌표 (x, y)
const point = {
x: Math.random(),
y: Math.random()
};
이런 랜덤 샘플을 수천, 수만, 수백만 개 생성하면, 전체 공간의 성질을 파악할 수 있습니다.
원주율(π) 추정하기 - 가장 유명한 예제
가장 직관적이고 유명한 몬테카를로 시뮬레이션 예제는 π를 추정하는 것입니다.
아이디어
정사각형 안에 사분원(quarter circle)이 있다고 생각해보세요:
1 ┌─────────────┐
│ ⚫⚫⚫ │
│ ⚫⚫⚫⚫⚫ │
│ ⚫⚫⚫⚫⚫⚫│ ⚫ = 원 안에 있는 점
y │ ⚫⚫⚫⚫⚫⚫ │ ⚪ = 원 밖에 있는 점
│ ⚫⚫⚫⚫⚫⚪⚪│
│ ⚫⚫⚫⚪⚪⚪ │
│ ⚫⚪⚪⚪⚪ │
0 └─────────────┘
0 1 x
사분원의 넓이 = π * r² / 4 = π / 4 (r = 1)
정사각형의 넓이 = 1 * 1 = 1
비율 = (원 안의 점 개수) / (전체 점 개수) ≈ π / 4
따라서, π ≈ 4 * (원 안의 점 개수) / (전체 점 개수)
구현
function estimatePi(numSamples) {
let insideCircle = 0;
for (let i = 0; i < numSamples; i++) {
// 0과 1 사이의 랜덤 좌표
const x = Math.random();
const y = Math.random();
// 원점으로부터의 거리
const distance = Math.sqrt(x * x + y * y);
// 반지름 1인 사분원 안에 있는지 확인
if (distance <= 1) {
insideCircle++;
}
}
// π 추정
const piEstimate = 4 * insideCircle / numSamples;
return piEstimate;
}
// 실행
console.log(estimatePi(1000)); // 약 3.1~3.2
console.log(estimatePi(10000)); // 약 3.13~3.15
console.log(estimatePi(100000)); // 약 3.141~3.142
console.log(estimatePi(1000000)); // 약 3.1415~3.1416
console.log(Math.PI); // 정확한 값: 3.141592653589793
관찰 포인트:
- 샘플이 1,000개일 때: 오차 약 ±0.05
- 샘플이 10,000개일 때: 오차 약 ±0.01
- 샘플이 1,000,000개일 때: 오차 약 ±0.001
샘플 개수가 10배 증가하면, 오차는 약 √10 ≈ 3.16배 감소합니다.
시각화로 이해하기
코드를 실행하면서 점들이 어떻게 분포하는지 생각해보세요:
function visualizePiEstimation(numSamples) {
const points = [];
let insideCount = 0;
for (let i = 0; i < numSamples; i++) {
const x = Math.random();
const y = Math.random();
const distance = Math.sqrt(x * x + y * y);
const inside = distance <= 1;
if (inside) insideCount++;
points.push({
x,
y,
inside,
// 현재까지의 π 추정값
currentEstimate: 4 * insideCount / (i + 1)
});
}
return points;
}
const simulation = visualizePiEstimation(1000);
// 처음 10개 샘플의 π 추정값
simulation.slice(0, 10).forEach((p, i) => {
console.log(`${i + 1}번째 샘플 후 π ≈ ${p.currentEstimate.toFixed(4)}`);
});
// 마지막 π 추정값
console.log(`최종 추정: π ≈ ${simulation[simulation.length - 1].currentEstimate.toFixed(4)}`);
출력 예시:
1번째 샘플 후 π ≈ 4.0000 (편차가 큼!)
2번째 샘플 후 π ≈ 4.0000
3번째 샘플 후 π ≈ 2.6667
10번째 샘플 후 π ≈ 3.2000
100번째 샘플 후 π ≈ 3.1600
1000번째 샘플 후 π ≈ 3.1440 (점점 수렴!)
최종 추정: π ≈ 3.1440
초기에는 값이 크게 흔들리다가, 샘플이 늘어날수록 안정적으로 π에 가까워지는 것을 볼 수 있습니다.
실전 예제 1: 주사위 게임 시뮬레이션
두 개의 주사위를 던져서 합이 7이 나올 확률을 몬테카를로로 추정해봅시다.
이론적 확률
두 주사위의 합이 7이 되는 경우:
(1,6), (2,5), (3,4), (4,3), (5,2), (6,1) = 6가지
전체 경우의 수: 6 × 6 = 36가지
확률 = 6/36 = 1/6 ≈ 0.1667 (약 16.67%)
시뮬레이션으로 확인
function rollDice() {
return Math.floor(Math.random() * 6) + 1; // 1~6
}
function simulateDiceGame(numTrials) {
let sumIsSeven = 0;
for (let i = 0; i < numTrials; i++) {
const dice1 = rollDice();
const dice2 = rollDice();
if (dice1 + dice2 === 7) {
sumIsSeven++;
}
}
const probability = sumIsSeven / numTrials;
return probability;
}
console.log(simulateDiceGame(1000)); // 약 0.16~0.17
console.log(simulateDiceGame(10000)); // 약 0.166~0.167
console.log(simulateDiceGame(100000)); // 약 0.1666~0.1668
console.log(1/6); // 정확한 값: 0.16666...
배울 점: 간단한 확률 문제라도 시뮬레이션으로 검증할 수 있습니다. 특히 복잡한 규칙이 있는 게임에서는 이론적 계산보다 시뮬레이션이 더 쉬울 수 있습니다.
실전 예제 2: 불규칙한 도형의 넓이
수학 공식으로 구하기 어려운 복잡한 도형의 넓이를 추정해봅시다.
// 복잡한 도형: x² + y² < 0.5이고 y > x인 영역
function isInsideComplexShape(x, y) {
return (x * x + y * y < 0.5) && (y > x);
}
function estimateAreaOfComplexShape(numSamples) {
let insideCount = 0;
// 0~1 범위의 정사각형(넓이 = 1) 안에서 샘플링
for (let i = 0; i < numSamples; i++) {
const x = Math.random();
const y = Math.random();
if (isInsideComplexShape(x, y)) {
insideCount++;
}
}
// 정사각형의 넓이(1) × 비율
const area = insideCount / numSamples;
return area;
}
console.log(estimateAreaOfComplexShape(100000)); // 약 0.196
시각화:
1 ┌─────────────┐
│ ▓▓▓ │
│ ▓▓▓▓▓ │ ▓ = 도형 안
y │ ▓▓▓▓▓▓ │
│ ▓▓▓▓▓▓ │
│ ◆◆◆ │ ◆ = y ≤ x 영역
0 └─────────────┘
0 1 x
이런 방법으로 아무리 복잡한 도형이라도 넓이를 추정할 수 있습니다!
실전 예제 3: 기댓값 계산
카지노 게임에서 평균적으로 얼마를 딸 수 있는지(또는 잃는지) 계산해봅시다.
룰렛 게임
- 빨간색에 $10 베팅
- 빨간색이 나오면 $10 수익 (확률 18/38 ≈ 47.4%)
- 검은색이나 초록색이 나오면 $10 손실
function playRoulette() {
const random = Math.random();
// 빨간색: 18/38 확률
if (random < 18/38) {
return 10; // 수익
} else {
return -10; // 손실
}
}
function simulateRouletteExpectedValue(numGames) {
let totalProfit = 0;
for (let i = 0; i < numGames; i++) {
totalProfit += playRoulette();
}
// 게임당 평균 수익
const expectedValue = totalProfit / numGames;
return expectedValue;
}
console.log(simulateRouletteExpectedValue(10000)); // 약 -0.5
console.log(simulateRouletteExpectedValue(100000)); // 약 -0.53
console.log(simulateRouletteExpectedValue(1000000)); // 약 -0.526
// 이론적 기댓값: (18/38) × 10 + (20/38) × (-10) ≈ -0.526
결론: 평균적으로 게임당 약 $0.53를 잃습니다. 카지노가 돈을 버는 이유죠!
⚠️ 흔한 실수와 함정
몬테카를로 시뮬레이션을 사용할 때 주의해야 할 점들이 있습니다.
함정 1: 샘플 개수가 너무 적음
// ❌ 샘플이 너무 적어서 부정확
const pi1 = estimatePi(10); // 2.8 또는 3.6 같은 엉뚱한 값
const pi2 = estimatePi(100); // 여전히 오차가 큼
// ✅ 충분한 샘플로 정확도 향상
const pi3 = estimatePi(100000); // 3.14... (신뢰할 만함)
가이드라인:
- 빠른 테스트: 1,000 ~ 10,000 샘플
- 실용적 정확도: 100,000 ~ 1,000,000 샘플
- 높은 정확도: 10,000,000+ 샘플
함정 2: 랜덤이 진짜 랜덤이 아님
JavaScript의 Math.random()은 의사 난수(pseudo-random)입니다. 진짜 무작위가 아니라 알고리즘으로 생성된 것이죠.
// ❌ 같은 패턴이 반복될 수 있음 (매우 드물지만)
// Math.random()은 내부적으로 seed 기반
// ✅ 대부분의 경우 Math.random()으로 충분
// ✅ 보안이 중요하면 crypto.getRandomValues() 사용
const randomBytes = new Uint32Array(1);
crypto.getRandomValues(randomBytes);
const secureRandom = randomBytes[0] / (0xffffffff + 1);
함정 3: 수렴 속도를 과대평가
몬테카를로의 수렴 속도는 O(1/√n)입니다. 즉, 정확도를 10배 높이려면 샘플을 100배 늘려야 합니다!
// 오차를 1/10로 줄이려면?
// 샘플을 100배 늘려야 함
estimatePi(1000); // 오차 ± 0.05
estimatePi(100000); // 오차 ± 0.005 (100배 샘플, 10배 정확)
estimatePi(10000000); // 오차 ± 0.0005 (10,000배 샘플, 100배 정확)
교훈: 몬테카를로는 강력하지만, 매우 높은 정확도가 필요하면 비효율적일 수 있습니다.
함정 4: 모든 문제에 적용 가능하다고 착각
// ❌ 이렇게 간단한 문제는 직접 계산이 나음
function addTwoNumbers(a, b) {
// 몬테카를로로 덧셈을 할 필요가 없습니다!
return a + b;
}
// ✅ 복잡하거나 해석적 해가 없을 때만 사용
// - 고차원 적분
// - 복잡한 확률 분포
// - 시스템 시뮬레이션
좋은 예 vs 나쁜 예
✅ 좋은 사용 사례
// 1. 복잡한 확률 문제
function simulatePokerWinRate(hand, opponents, numTrials) {
let wins = 0;
for (let i = 0; i < numTrials; i++) {
// 랜덤하게 카드 배분하고 승패 판정
if (playPokerRound(hand, opponents)) {
wins++;
}
}
return wins / numTrials;
}
// 2. 다변수 최적화
function findOptimalPortfolio(stocks, budget, riskTolerance) {
let bestPortfolio = null;
let bestReturn = -Infinity;
for (let i = 0; i < 10000; i++) {
// 랜덤하게 포트폴리오 구성
const portfolio = generateRandomPortfolio(stocks, budget);
const expectedReturn = simulatePortfolioReturn(portfolio);
if (expectedReturn > bestReturn &&
portfolioRisk(portfolio) <= riskTolerance) {
bestReturn = expectedReturn;
bestPortfolio = portfolio;
}
}
return bestPortfolio;
}
❌ 나쁜 사용 사례
// ❌ 간단한 평균 계산에 몬테카를로 불필요
function averageOfArray(arr) {
let sum = 0;
for (let i = 0; i < 1000; i++) {
const randomIndex = Math.floor(Math.random() * arr.length);
sum += arr[randomIndex];
}
return sum / 1000;
}
// ✅ 직접 계산이 정확하고 빠름
function averageOfArray(arr) {
return arr.reduce((sum, val) => sum + val, 0) / arr.length;
}
실무에서 사용할 때 팁
1. 진행 상황 표시
시뮬레이션이 오래 걸릴 때는 진행 상황을 보여주세요:
function estimatePiWithProgress(numSamples) {
let insideCircle = 0;
const milestones = [0.1, 0.25, 0.5, 0.75, 1.0];
let nextMilestone = 0;
for (let i = 0; i < numSamples; i++) {
const x = Math.random();
const y = Math.random();
if (Math.sqrt(x * x + y * y) <= 1) {
insideCircle++;
}
// 진행 상황 출력
const progress = (i + 1) / numSamples;
if (progress >= milestones[nextMilestone]) {
const currentEstimate = 4 * insideCircle / (i + 1);
console.log(`${(progress * 100).toFixed(0)}% 완료 - 현재 π ≈ ${currentEstimate.toFixed(5)}`);
nextMilestone++;
}
}
return 4 * insideCircle / numSamples;
}
estimatePiWithProgress(1000000);
// 출력:
// 10% 완료 - 현재 π ≈ 3.14080
// 25% 완료 - 현재 π ≈ 3.14272
// 50% 완료 - 현재 π ≈ 3.14156
// 75% 완료 - 현재 π ≈ 3.14165
// 100% 완료 - 현재 π ≈ 3.14159
2. 결과에 신뢰구간 제공
function estimatePiWithConfidence(numSamples, numRuns) {
const estimates = [];
for (let run = 0; run < numRuns; run++) {
estimates.push(estimatePi(numSamples));
}
// 평균과 표준편차
const mean = estimates.reduce((sum, val) => sum + val, 0) / numRuns;
const variance = estimates.reduce((sum, val) => sum + (val - mean) ** 2, 0) / numRuns;
const stdDev = Math.sqrt(variance);
return {
estimate: mean,
stdDev: stdDev,
// 95% 신뢰구간 (정규분포 가정)
confidenceInterval: [mean - 1.96 * stdDev, mean + 1.96 * stdDev]
};
}
const result = estimatePiWithConfidence(10000, 100);
console.log(`π ≈ ${result.estimate.toFixed(5)} ± ${result.stdDev.toFixed(5)}`);
console.log(`95% 신뢰구간: [${result.confidenceInterval[0].toFixed(5)}, ${result.confidenceInterval[1].toFixed(5)}]`);
// 출력 예:
// π ≈ 3.14152 ± 0.01623
// 95% 신뢰구간: [3.10971, 3.17333]
3. 재현 가능한 시뮬레이션
디버깅이나 테스트를 위해 시드를 고정할 수 있습니다:
// 간단한 시드 가능 난수 생성기
class SeededRandom {
constructor(seed) {
this.seed = seed;
}
random() {
// Mulberry32 알고리즘
let t = this.seed += 0x6D2B79F5;
t = Math.imul(t ^ t >>> 15, t | 1);
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
}
}
// 같은 시드 = 같은 결과
const rng1 = new SeededRandom(12345);
console.log(rng1.random()); // 항상 0.2785...
const rng2 = new SeededRandom(12345);
console.log(rng2.random()); // 항상 0.2785... (재현 가능!)
더 나아가기
몬테카를로 시뮬레이션의 고급 주제들:
1. 분산 감소 기법 (Variance Reduction)
같은 샘플 개수로 더 정확한 결과를 얻는 기법들:
- 중요도 샘플링 (Importance Sampling): 중요한 영역을 더 많이 샘플링
- 층화 샘플링 (Stratified Sampling): 공간을 균등하게 나눠서 샘플링
- 대조 변수법 (Control Variates): 알려진 값과의 차이를 이용
2. 마르코프 체인 몬테카를로 (MCMC)
복잡한 확률 분포에서 샘플링하는 기법:
- Metropolis-Hastings 알고리즘
- Gibbs Sampling
머신러닝의 베이지안 추론에서 핵심적으로 사용됩니다.
3. 몬테카를로 트리 탐색 (MCTS)
게임 AI에서 최적의 수를 찾는 알고리즘. 알파고가 사용한 핵심 기법입니다!
정리하면
몬테카를로 시뮬레이션은:
✅ 장점:
- 복잡한 문제를 단순하게 접근
- 구현이 직관적이고 쉬움
- 다차원 문제에 효과적
- 실제 불확실성을 모델링
❌ 단점:
- 정확한 답이 아닌 근사값
- 높은 정확도에는 많은 샘플 필요
- 수렴이 느림 (O(1/√n))
- 진짜 난수가 아닐 수 있음
언제 사용?
- 수학적으로 풀기 어려운 문제
- 확률이 개입된 복잡한 시스템
- 여러 시나리오를 비교하고 싶을 때
언제 피해야?
- 간단하게 직접 계산 가능할 때
- 매우 높은 정확도가 필요할 때
- 실시간 성능이 중요할 때
연습 문제
직접 구현해보면서 이해를 깊게 만들어보세요!
문제 1: 버핏 바늘 (Buffon’s Needle)
바닥에 평행선이 그어져 있고, 바늘을 떨어뜨렸을 때 선을 가로지를 확률을 구하세요. (힌트: 이것도 π와 관련이 있습니다!)
문제 2: 생일 문제
몇 명이 모이면 같은 생일을 가진 사람이 있을 확률이 50%를 넘을까요? 시뮬레이션으로 확인해보세요.
문제 3: 주차 시뮬레이션
길이 L인 주차 공간에 길이 d인 차들이 랜덤하게 주차합니다. 평균 몇 대나 주차할 수 있을까요?
문제 4: 주식 포트폴리오
3개 주식이 있고, 각각 다른 기대수익률과 변동성을 가집니다. 몬테카를로로 1년 후 포트폴리오 가치 분포를 시뮬레이션하세요.
참고 자료
- 위키백과 - Monte Carlo method
- Seeing Theory - Probability Visualizations
- 3Blue1Brown - Pi in the Mandelbrot set (시각적 설명)
- Introduction to Monte Carlo Methods - MIT OCW
몬테카를로 시뮬레이션은 “불확실성”을 다루는 강력한 도구입니다. 완벽한 답을 구할 수 없을 때, “충분히 좋은 답”을 빠르게 찾을 수 있게 해주죠. 실제 프로젝트에서 확률적 문제를 만나면, 몬테카를로를 떠올려보세요!
댓글