【Python】モンテカルロ法でネイピア数eを推算してみた
以前、モンテカルロ法により円周率を推算する方法を調べて、実装した。
このとき、よく知られた無理数の定数のうちの一つであるネイピア数
調べてみると結構面白かったので、簡単な証明を自分でしつつ、メモする。
モンテカルロ法によるネイピア数の推算
モンテカルロ法とは
Wikipediaによると以下。
シミュレーションや数値計算を乱数を用いて行う手法の総称。
簡単に言えば、ランダムに発生させた数値を使ってその値を求めることをいう。
よくこれを直感的にもわかりやすく説明するために、円周率
推算のやり方
以下の方法で推算できるとのこと。
区間の一様乱数(0と1の間に一様に分布した実数値確率変数)を独立にいくつも発生させて加えていくとき、その和が1を越えるまでにかかる乱数の個数の平均は、自然対数の底 である。
この著者も書いているが、定義と全く関係のない設定から
簡単な証明
基本的にはこちらの文献を参考にした。
区間
一様分布の確率密度関数
一様分布の確率密度関数
参考: 連続一様分布1 | 統計学の時間
合計が 以下の確率の愚直計算
まず、
続いて、
同様に
同様に
合計が 以下の確率の一般項
係数の分母に着目すると、
ここで、以下の仮定を置いてみる。
先ほどまで行っていた愚直な計算のやり方を踏まえると帰納的に次の値を求められることがわかるので、帰納法による証明を試みる。
(i)
先ほど示した通り。
(ii)
ここで、
(i), (ii)より、すべての自然数に対して成り立つ。
この後の便宜のため、
0回目のサンプリングを行った後とは、まだサンプリングしていない状態(=合計は0)であるということになる。
したがって、
一般項に
このように
証明する命題に立ち返る
以上を踏まえて、最初の問題に戻る。今回の問題では
はじめて
つまり、
これを代入した上で整理すると、
よって、はじめて合計が1を超える回数の期待値
わかりやすさのため、
一方、ネイピア数
特に、
つまり、
Pythonで実装
matplotlibを除いて標準ライブラリのみで実装した。
import random
import sys
import math
import itertools
from time import time
import matplotlib as mpl
import matplotlib.pyplot as plt
バージョン
print(f'Python: {sys.version}')
print(f'Matplotlib: {mpl.__version__}')
Python: 3.9.13 | packaged by conda-forge | (main, May 27 2022, 17:01:00)
[Clang 13.0.1 ]
Matplotlib: 3.5.2
再現性のためにシード値を固定する。
random.seed(334)
1,000,000回 (100万回)やってみる。
n_iteration = 1_000_000
# 時間測定
t1 = time()
lst_k = []
for iter in range(n_iteration):
x = 0
k = 0
while x <= 1:
x+=random.random()
k+=1
lst_k.append(k)
t2 = time()
print(f'Time: {t2-t1:.3f} sec')
Time: 0.474 sec
すぐ終わる。
# 累積和を利用してeを求める
lst_e_estimated:list[float] = [k_accumulated/(n_iter+1) for n_iter, k_accumulated in enumerate(itertools.accumulate(lst_k))]
# style
mpl.style.use('seaborn-darkgrid')
# figure, axisを生成
fig, axes = plt.subplots(2, 1, figsize=(6.4, 9.6), facecolor='w', dpi=144)
for j, _range in enumerate([(0, n_iteration), (0, int(n_iteration*0.00015))]):
ax:plt.Axes = axes[j]
# x軸の描画範囲を手動で設定するために計算
alpha = 0.05
offset = (_range[1] - _range[0]) * alpha
xlim = (_range[0] - offset, _range[1] + offset)
# 実際のeの値
ax.plot(xlim, [math.e]*len(xlim), c='gray', linestyle='--', label='$e$')
# 推算したeの値
ax.plot(range(*_range), lst_e_estimated[slice(*_range)], label='estimated $e$')
# layout
if j == 0:
ax.set_title(f'Random simulation (n = {n_iteration})')
else:
ax.set_title(f'{min(_range)+1} to {max(_range)}')
ax.set_xlim(xlim)
ax.set_xlabel('iteration')
ax.set_ylabel('$e$')
ax.legend()
fig.tight_layout()
print('実際の値: {:.5f}'.format(math.e))
print('推算値: {:.5f}'.format(sum(lst_k)/n_iteration))
実際の値: 2.71828
推算値: 2.71798
コメント
おそらく何かに役立つものではないと思うが、とても面白く、久しぶりに数学をやりたい気分だったので遊んでみた。
自分で上記のコードを動かせる方は収束の仕方が変わるのでシード値を変えて試してみると面白いと思う。