PyLearnPR 03: 正規分布(二変数以上)

前回、正規分布のパラメータが平均値と標準偏差であること、正規分布の構成方法、データの複製方法を学びました。ただしあれだけだと、ちょっと不都合が生じる場合があります。これを理解するため、以下の例を考えて見ます。

今、身長と体重のデータをたくさん欲しいと考えています。10人くらいから、身長と体重のデータを集めました。そして、身長と体重の平均値と標準偏差を求め、身長の正規分布、体重の正規分布、これを求めました。最後に、2つの正規分布でデータを複製させ、大量のデータを得ました。

これで問題になることは、実際には少数しかいない存在が多くの比率で発生してしまう点にあります。身長が高い人は、骨や内臓なども大きいので、身長と体重は一般に強い相関性があります(身長が高い人で痩せている人がまったくいないとは言っていないことに注意してください)。これをまとめると、なんとなくですが以下のようになるでしょう。

  • 身長が高く、体重が重い人: -> たくさんいそう
  • 身長が高く、体重が軽い人: -> あまりいなさそう
  • 身長が低く、体重が重い人: -> あまりいなさそう
  • 身長が低く、体重が軽い人: -> たくさんいなさそう

しかし、先ほどの事例では、身長と体重を完全に独立させ、正規分布に従い乱数を生成してしまっているので、以下のようになってしまいます。

  • 身長が高く、体重が重い人: -> そこそこいる
  • 身長が高く、体重が軽い人: -> そこそこいる
  • 身長が低く、体重が重い人: -> そこそこいる
  • 身長が低く、体重が軽い人: -> そこそこいる

これは、身長と体重の正規分布を独立に生成している、すなわち、その強い相関性を無視しているがために起こる現象です。これを解消するため、今回は、変数間の相関性を維持したままの乱数を生成する方法として、多変量正規分布を扱います。

2変数の正規分布

上の例では、身長と体重の関連性を維持したまま乱数を発生する事象を説明しました。変数が2個あるので、これを成す正規分布を、2変数正規分布、2変量正規分布と言います。ここではそれについて説明します。

集めた少数のデータ

普通はCSVファイルなどに保存しておき、ロードしますが、めんどくさいので直に代入します。

In [1]:
import numpy as np
dataset = np.zeros([10, 2]) # 10人分の身長と体重を格納するnumpy配列

# 1人目
dataset[0, 0] = 150
dataset[0, 1] = 59

# 2人目
dataset[1, 0] = 153
dataset[1, 1] = 61

# 3人目
dataset[2, 0] = 149
dataset[2, 1] = 75

# 4人目
dataset[3, 0] = 165
dataset[3, 1] = 67

# 5人目
dataset[4, 0] = 164
dataset[4, 1] = 68

# 6人目
dataset[5, 0] = 163
dataset[5, 1] = 49

# 7人目
dataset[6, 0] = 178
dataset[6, 1] = 69

# 8人目
dataset[7, 0] = 180
dataset[7, 1] = 70

# 9人目
dataset[8, 0] = 177
dataset[8, 1] = 95

# 10人目
dataset[9, 0] = 176
dataset[9, 1] = 65

print(dataset)
[[150.  59.]
 [153.  61.]
 [149.  75.]
 [165.  67.]
 [164.  68.]
 [163.  49.]
 [178.  69.]
 [180.  70.]
 [177.  95.]
 [176.  65.]]

平均値と分散共分散行列の算出

1変数の場合は平均値と標準偏差を計算しました。今回は、2変数間の関連性も算出する必要があります。これを保有する情報に、分散共分散行列というものがあります。詳しく知りたい人は自分で調べてください。

In [2]:
#平均の算出
m = np.mean(dataset, 0) # 0指定で行の平均, 1指定で列の平均

# 分散共分散行列の算出
cov = np.cov(dataset, rowvar=False)
# 行が個別データを表すなら rowvar = False
# 列が個別データを表すなら rowvar = True

print("平均値:")
print(m)

print("分散共分散行列:")
print(cov)
平均値:
[165.5  67.8]
分散共分散行列:
[[142.94444444  53.33333333]
 [ 53.33333333 142.62222222]]

2変量正規分布の構築

In [3]:
from scipy.stats import multivariate_normal

# 描画範囲の設定 (最小値, 最大値, 分割数の順に)
x, y = np.meshgrid(np.linspace(100, 200, 100), np.linspace(25, 125, 100))
z = np.dstack((x, y))

# 二変量正規分布の構築
pdf2 = multivariate_normal.pdf(z, m, cov) #範囲、平均、分散共分散の順

# 出力
print(pdf2)
[[9.24468097e-11 1.38233908e-10 2.04990965e-10 ... 1.38240940e-10
  9.24516108e-11 6.13182513e-11]
 [1.07083161e-10 1.60616810e-10 2.38923181e-10 ... 2.16341697e-10
  1.45132672e-10 9.65578592e-11]
 [1.23009817e-10 1.85078830e-10 2.76166626e-10 ... 3.35763220e-10
  2.25946303e-10 1.50790796e-10]
 ...
 [2.21860989e-21 4.48206243e-21 8.97991722e-21 ... 1.57120443e-08
  1.41966148e-08 1.27213844e-08]
 [1.14729699e-21 2.32498399e-21 4.67263176e-21 ... 1.09774610e-08
  9.94949787e-09 8.94330204e-09]
 [5.88382975e-22 1.19605560e-21 2.41123821e-21 ... 7.60607227e-09
  6.91523375e-09 6.23520530e-09]]

これで、multivariate...が二変量正規分布を構成する部分です。これには、範囲、平均、分散共分散行列を入れる必要があります。printでその中身を表示していますが、わかりにくいので、以下で空間を可視化してみます。pcolor関数を用いることで、これを実現することができます。

In [5]:
from matplotlib import pyplot as plt
plt.pcolor(x, y, pdf2, cmap="hot")
plt.colorbar() # カラーバー
plt.title('visualization', size=20)
plt.xlabel('height [cm]', size=20)
plt.ylabel('weight [kg]', size=20)
plt.show()

色が強いほど、そのデータの発生確率が高いことを示しています。楕円になっているので、身長が高く体重が高い人ほど発生確率が高いこと、身長が低く体重も低い人ほど発生確率が高いことが見て取れます。一方、身長が低く体重が重い、といった人も一応は発生するようになっています。このように、変数間の相関関係を維持した正規分布を構成することができました。

ところで、pythonのmatplotlibでは、いろいろな色が用意されています。

詳しくはこちら: https://matplotlib.org/examples/color/colormaps_reference.html もしお気に入りの色があれば、cmapにその文字列を代入してみましょう。以下、適当に6つピックアップした結果です。奇抜なものもありますね。お硬い場所では変な色は使わないように注意してください。

In [6]:
plt.figure(figsize=(10, 5)) # 描画領域の横幅, 縦幅
plt.subplot(2,3,1)
plt.pcolor(x, y, pdf2, cmap="YlGn")
plt.subplot(2,3,2)
plt.pcolor(x, y, pdf2, cmap="bone")
plt.subplot(2,3,3)
plt.pcolor(x, y, pdf2, cmap="cool")
plt.subplot(2,3,4)
plt.pcolor(x, y, pdf2, cmap="seismic")
plt.subplot(2,3,5)
plt.pcolor(x, y, pdf2, cmap="tab10") # きちんとした場所では使ったらダメ
plt.subplot(2,3,6)
plt.pcolor(x, y, pdf2, cmap="Pastel1") # きちんとした場所では使ったらダメ
plt.show()

データの生成

それでは、2変量正規分布からデータを生成してみます。高い相関性を有するデータを集めたが、数が少ない...というときに使います。平均mと分散共分散行列は上で求めたものを使います。

In [7]:
# データ生成 平均、分散共分散行列、生成したデータ数の順に入れる
Dataset_new = np.random.multivariate_normal(m, cov, 100)
print(Dataset_new[0:10])
[[168.92540878  61.91494444]
 [147.15436497  70.1120094 ]
 [162.65375296  80.21655588]
 [167.11371713  83.91238532]
 [157.23194942  68.80151603]
 [178.33310603  54.58806532]
 [161.06815048  63.85973776]
 [142.61959189  67.71540314]
 [154.24752068  60.40693096]
 [167.63844141  83.02435266]]

このように、新たなデータを生成できました(10個だけ表示しています)。例外もありますが、身長と体重になんとなく関連性がありそうですね。それでは、散布図で取得したデータを確認してみます。

In [8]:
plt.scatter(Dataset_new[:, 0], Dataset_new[:, 1])
plt.show()

「身長が低いほど体重が低く、身長が高いほど体重が高い」という傾向があり、例外もちょっとはあるよというデータを生成できました。このように、2変量正規分布を用いることで、2つのデータの間にある関連性を保存したまま、データを発生させることができます。人工知能を作りたいが、ちょっとしかデータを集められなかったといったときに使うと良いでしょう。

3変数以上の場合

これまで、2変数の関連性を保存させたままデータを発生させる方法を学びました。しかし、現実的には3変数、4変数など、関連性のある変数の数は、いろいろな場合があります(例えば、算数、物理、化学は理系科目なので、点数の関連性が強い、などです)。この場合は、以下のコードを書けばokです。参考までに、4人から算数、物理、数学のテスト結果を教えてもらったことを想定したデータを用意します。

In [9]:
dataset3 = np.zeros([4, 3]) # 4人分、3科目

# 1人目
dataset3[0, 0] = 90
dataset3[0, 1] = 80
dataset3[0, 2] = 79

# 2人目
dataset3[1, 0] = 53
dataset3[1, 1] = 60
dataset3[1, 2] = 61

# 3人目
dataset3[2, 0] = 54
dataset3[2, 1] = 46
dataset3[2, 2] = 59

# 4人目
dataset3[3, 0] = 76
dataset3[3, 1] = 41
dataset3[3, 2] = 30

続いて、先ほどと同様に平均と分散共分散行列を求めてみます。

In [10]:
# 平均と分散共分散行列を求める
m3 = np.mean(dataset3, 0)
cov3 = np.cov(dataset3, rowvar=False)

最後に、以下のように書きます。これで、100セットのデータが複製できます。

In [11]:
Dataset_new3 = np.random.multivariate_normal(m3, cov3, 100)
print(Dataset_new3[0:10])
[[63.39250533 69.38614166 73.05757093]
 [48.39093967 53.90495363 67.71885665]
 [68.31510583 43.5878215  42.95493248]
 [56.02651252 59.47057819 61.15229492]
 [70.5980413  69.36445466 68.2796262 ]
 [61.27052084 52.31548072 63.08936227]
 [61.05726649 57.3942415  74.80384546]
 [44.50988179 47.94430685 51.05986108]
 [96.9549987  78.26705441 83.98573543]
 [49.15554685 30.85573074 31.41268763]]

散布図で確認してみます。今回は3科目なので、それらの組み合わせ分散布図を描いてみます。

In [12]:
plt.figure(figsize=(13, 3)) # 描画領域の横幅, 縦幅
plt.subplot(1,3,1)
plt.scatter(Dataset_new3[:, 0], Dataset_new3[:, 1])
plt.xlabel("x0")
plt.ylabel("x1")

plt.subplot(1,3,2)
plt.scatter(Dataset_new3[:, 0], Dataset_new3[:, 2])
plt.xlabel("x0")
plt.ylabel("x2")

plt.subplot(1,3,3)
plt.scatter(Dataset_new3[:, 1], Dataset_new3[:, 2])
plt.xlabel("x1")
plt.ylabel("x2")

plt.show()

このように、3変数でもそれらのデータ間の関連性を保存したまま、データを生成できることが確認できました。毎回同じデータを発生させたい場合は、プログラムの一番上にnp.random.seed(1)を記載することを忘れないようにしてください。