PyLearn PR 02: 正規分布(1変数)

一つ前で学習した一様分布は、指定した範囲の数値が、すべて同じ確率で発生していました。今回学ぶ正規分布は、2つのパラメータ(平均および標準偏差)により定義される確率分布となります。この確率分布は、平均値に近いデータほど発生しやすいく、平均値から遠いデータほど発生しにくいという仮定が置かれています。ここでいう近い、遠いを標準偏差により定義づけます。つまり、すべての範囲で数値がまったく同じ確率では発生しません。

あるデータが平均値を$\mu$、標準偏差を$s$とした正規分布に従って発生したと仮定すると、そのデータは以下の範囲・確率で発生します。

  • $\mu - s$から$\mu + s$の範囲に、68.27%の確率でデータが発生
  • $\mu - 2s$から$\mu + 2s$の範囲に、95.45%の確率でデータが発生
  • $\mu - 3s$から$\mu + 3s$の範囲に、99.73%の確率でデータが発生
これは大変重要な性質です。なぜなら、データの平均値と標準偏差が明らかであれば、どのくらいの範囲にどれくらいの確率で何が起こるのか、実際に観測せずとも明らかになるためです。連続型の確率密度関数は、積分により確率を取得します。ですので、もしこの具体的な数字に興味がある方は、正規分布の式の形を自分で調べて、$\mu - s$から$\mu + s$の範囲で定積分してみてください。0.6827という数字が出てくるはずです。

正規分布を描画するコード

以下は、平均0、標準偏差1の正規分布を描画するコードです。np.arrangeにより、-10から10まで、0.01刻みのデータ列を生成しています。それを、norm.pdfに代入して、正規分布の出力を得ています。なお、norm.pdfの第二引数に平均値を、第三引数に標準偏差を入れます。グラフの描画には、plot関数を使用します。plot関数の第一引数に横軸の値、第二引数に縦軸の値を入れます。

In [2]:
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm

# 正規分布の構成
x = np.arange(-10, 10, 0.01)
y = norm.pdf(x, 0, 1)
 
# グラフ描画
plt.plot(x, y, color='r', label='mu=0, std=1')
plt.xlim(-10, 10)
plt.xlabel("x")
plt.ylabel("y")
plt.legend() # 凡例表示
plt.show()

次に、平均値と標準偏差を変えた色々なパターンで正規分布を描画してみます。以下のコードを実装し、平均と標準偏差を変えると正規分布の形がどのようになるか、自分なりにまとめてみてください。

In [3]:
# 正規分布の構成
x = np.arange(-10, 10, 0.01)
y1 = norm.pdf(x, 0, 1)
y2 = norm.pdf(x, 0, 1.5)
y3 = norm.pdf(x, 0, 0.5)
y4 = norm.pdf(x, 5, 1)
y5 = norm.pdf(x, -5, 1)
 
# グラフ描画
plt.plot(x, y1, color='r', label='mu=0, std=1')
plt.plot(x, y2, color='g', label='mu=0, std=1.5')
plt.plot(x, y3, color='b', label='mu=0, std=0.5')
plt.plot(x, y4, color='y', linestyle='dashdot', label='mu=5, std=1')
plt.plot(x, y5, color='m', linestyle='dashed', label='mu=-5, std=1')
   # linestyle を変えると、破線などに変化させることができます。

plt.xlim(-10, 10)
plt.xlabel("x")
plt.ylabel("y")
plt.legend() # 凡例表示

# 保存したい場合
plt.savefig('f1.png')
plt.savefig('f1.pdf')

# グラフ可視化
plt.show()

正規分布の見方について簡単に説明します。横軸xは、分析者が考えたい知りたい事象の数値を意味します。例えば、目の前にお菓子が一つあって、その重さがどれくらいか、などです。縦軸yはその事象がどれくらいの度合いで起こりそうかを示しています。もし、お菓子の重さが黄色の正規分布に従って発生しているのであれば、5.0gが最もあり得る結果、ということになります。黄色のグラフは、x=10、つまり10gのとき、yの値はほぼ0です。すなわち、目の前のお菓子が10gである可能性はほぼ0%であることを意味しています。

さて、きちんとプログラムのコードと描画された図を見比べた人は、標準偏差を小さくすると正規分布がとがり、平均値から遠ざかったデータが発生する確率が0になる領域が広まることに気がつくと思います。標準偏差はそもそも観測したデータのばらつきを表す指標です。ばらつきがないということは、平均値付近のデータしか発生しなくなっていくことを意味しています。これが、標準偏差が小さい正規分布は平均値付近で強く尖ることと対応づいています。

正規分布の活用方法

さて、人工知能などを作るとき、データをたくさん集める必要があります。しかし、データを十分に集められないという状況なんて、腐るほどあります。そんなとき、確率分布を活用してデータを増やすことができます。ここではその方法について述べます。具体的な手続きは以下の通りです。

  • 集めたデータの平均値と標準偏差を算出する。
  • それを用いて、正規分布を構成する。
  • 正規分布に従い、データを複製する。
実際にこれを行うコードを以下に示します。

In [4]:
# 集めた少しのデータ(6個しかデータがない!)
dataset = [55, 53, 60, 69, 43, 59]

# 平均値と標準偏差の算出
import numpy as np
mu = np.mean(dataset) # 平均
s = np.std(dataset) # 標準偏差
print("平均:", mu, "/標準偏差:", s)

# 正規分布に従う乱数生成
size = 1000 # 複製するデータの数
dataset_new = np.random.normal(mu, s, size)

# 生成データの可視化
plt.plot(dataset_new)
平均: 56.5 /標準偏差: 7.868714422741918
Out[4]:
[<matplotlib.lines.Line2D at 0x12474b080>]

集めたわずか6個のデータから、1000個のデータを生成してみました。横軸が1000点分のデータ、縦軸がその値です。平均値が56.5ですから、56.5を中心にデータが発生していることがわかりますね。また、標準偏差は7.9くらいです。

  • $\mu - s$から$\mu + s$の範囲に、68.27%の確率でデータが発生
冒頭に示したこれを思い出してみます。この法則が本当に正しいならば、56.5-7.9から56.5+7.9の間にだいたい7割くらいデータが集まるはずです。これを確認してみます。

In [5]:
counter = 0
for i in range(0, len(dataset_new)):
    if (dataset_new[i] > mu - s) & (dataset_new[i] < mu + s):
        counter = counter + 1
        
print("平均-標準偏差〜平均+標準偏差のデータ数は、", counter, "でした。")
平均-標準偏差〜平均+標準偏差のデータ数は、 688 でした。

上のコードは、正規分布により生成したデータのうち、平均-標準偏差〜平均+標準偏差の間に位置するものをカウントするコードです。全データ数は1000なので、だいたい68.27%に近い数字が得られていることを確認できます。

実際に生成された数値を確認するには、printを使用してください。1000個も表示させるとまずいので、10個だけにしてみます。

In [6]:
print(dataset_new[0:10])
[70.9721958  58.13774565 66.86975867 36.6243623  55.96967463 55.35306559
 51.32254218 72.59944251 53.84211051 69.03687921]

正規分布により生成されたデータが表示されました。これで、わずか6個しか集められなかったデータから、似たようなデータを莫大に生成できたことを確認できました。このように、集めたデータが少なすぎて困っている場合には、確率分布を活用することでデータを増やすことができます。

生成したデータを保存する場合には、以下のように書けばokです。

In [7]:
np.savetxt('dataset_new.csv', dataset_new)

ディレクトリの中に、dataset_new.csvがあることを確認してみてください。

再現可能なデータの生成

正規分布はある種の乱数によってデータを複製します。乱数は生成させるたびに値が変わるので、自分が行った研究成果を再現できなくなります。科学という観点からすると、再現できないというのはとてもまずいので、通常は再現可能な乱数を使用します。このためには、np.random.seed(1)をプログラムの一番上に書きます。以下の3つのコードを見てください。

In [31]:
# 毎回、実行のたびに結果が変わる
print(np.random.normal(mu, s, 1))
print(np.random.normal(mu, s, 1))
print(np.random.normal(mu, s, 1))
print(np.random.normal(mu, s, 1))
[59.01042754]
[54.53777573]
[68.00490981]
[40.28934109]
In [36]:
# 何回やっても同じ乱数
np.random.seed(1)
print(np.random.normal(mu, s, 1))
print(np.random.normal(mu, s, 1))
print(np.random.normal(mu, s, 1))
print(np.random.normal(mu, s, 1))
[69.28150979]
[51.68626348]
[52.34396732]
[48.05711633]
In [38]:
# 何回やっても同じ乱数
np.random.seed(1)
print(np.random.normal(mu, s, 1))
print(np.random.normal(mu, s, 1))
print(np.random.normal(mu, s, 1))
print(np.random.normal(mu, s, 1))
[69.28150979]
[51.68626348]
[52.34396732]
[48.05711633]

np.random.seed(1)を書いていない場合は、毎回異なる結果が出ることがわかると思います。一方、np.random.seed(1)をかくと、毎回同じ乱数が発生します。np.random.seed(1)以降、1回目は必ず69.28...が出ることが確定するわけです。乱数を発生するコードは、必ずnp.random.seed(1)をかく癖をつけるようにしましょう。然もなくば、分析をしているのに、日によって結果が違うという訳がわからない事態に陥ってしまいます。