生成モデルでデータを生成するとは

2019/12/27

Python 機械学習 数学

t f B! P L
生成モデルは識別モデルと異なり、データを生成する確率分布をモデル化しているため、その確率分布からサンプリングすることで、擬似的にデータを生成することもできる。

とかいう文言をよく見かけます。
何のことでしょうね。
さっぱりわかりません。

そこで、有名な生成モデルであるナイーブベイズモデルでこのことを確認し、記事の最後では学習済みのナイーブベイズモデルを用いて疑似データの作成も行ってみます。

生成モデルの式

ベイズの定理を用いて以下のようにモデル化します。
\(x\)が説明変数のベクトル、\(y\)がクラスです。
(例えば、ニュース記事のテキスト分類だったら、説明変数が単語の頻度ベクトル、クラスがスポーツ、政治、経済などのカテゴリーになります)

\(P(y|x) = \frac{P(y)P(x|y)}{P(x)}\)

\(P(y|x)\)が、説明変数\(x\)がクラス\(y\)である確率を示します。
生成モデルではこれを直接求めるのではなく、右辺のよくわからん式で近似することで求めます。
「逆に複雑になっとるやん」って思いますね。

実は、右辺の3つの\(P\)は、それぞれ個別に計算することで簡単に求まります。
以下でそれを見ていきましょう。

説明変数の確率P(x)

まず\(P(x)\)ですが、これは無視できます
これは何でしょうか。
説明変数\(x\)そのものが生起する確率のように見えます。

ですが、素朴に考えてみると、データXのクラス分類を行う場合は、そのXがクラス1なのか、クラス2なのか、クラス3なのか、の確率を比べるだけです(3クラス分類の場合)。
ごく一般的なクラス分類では、データ同士の推論は独立に行われるためです。

そのため、実際には\(P(y=1|x), P(y=2|x), P(y=3|x)\) の3つの確率を計算し、一番大きいものをXのクラスと見なすだけで十分です。

この式、説明変数はすべて同じ\(x\)であるために、生成モデルの定義式の分母\(P(x)\)もすべて同じ値をしていることがわかります。
比べるのは大小関係のみなので、これをわざわざ計算する必要がないことがわかります。


事前分布P(y)

\(P(y)\)は、クラスの事前分布を表します。
事前分布?事後分布?というと堅苦しさがありましすが、たいした話ではありません。
まずYについての確率があって(これが事前分布)、データXを観測したことで、Yの確率分布が変化したものが事後分布です。
なので、左辺の\(P(y|x)\)が事後分布になります。

超ざっくり説明すると、先ほどと同じテキスト分類(スポーツ or 政治 or 経済)の場合、その記事Xを読む前は、もちろんどのカテゴリーかなんて分からんので、すべて1/3の確率と考えます(これが事前分布)。

しかし、ひとたび記事Xを読めば、どのカテゴリーの記事かはわかりますよね。例えば、野球について書かれていたら、3択の中でスポーツの確率がアップします。
これが記事Xを読んだことによる確率分布の変化、つまり事後分布です。

事前分布は先ほどの3択問題では、何の前情報も無しなら1/3と言いましたが、実際に学習させる場合は、訓練データのラベル比を用います。
例えば、訓練データにスポーツ50件、政治30件、経済20件の場合、事前分布は0.5, 0.3, 0.2になります。
事前分布は、データXに依存しないためどんなデータが来ても定数になります。
めっちゃ単純や!

尤度P(x|y)

残ったこの子は「ゆうど」と読みます。
これは\(x\)を含むため、どんな記事Xか?に依存する部分です。
感覚的にいうと、前節で記事Xを読んだことで、スポーツの確率がアップした部分を反映しています。

ただ\(x\)は文全体の特徴量ベクトル(各単語の頻度ベクトル)なので、このままだと少々扱いにくいです。
ナイーブベイズは、単語出現を独立と考える純粋(=ナイーブ)な仕様なので、条件\(y\)におけるデータXの生起確率を以下のようにモデル化します。

\(P(x|y) = \prod_{i} P(x_i|y) \)

条件\(y\)における単語\(x_i\)の生起確率の積になります。
かみ砕いていうと、スポーツ記事に「野球」という単語が出現する確率、などを表します。
これなら簡単に計算できそうです。

ちなみに、実際は単語の出現はぜんぜん独立ではないのですが(共起)、文全体のカテゴリーを当てるような大味なタスクではこのようなモデル化でも十分うまく行くことが知られています。

ナイーブベイズにはいくつかの種類がありますが、それらはすべて\(P(x_i|y) \)のモデル化式の違いによるものです。

連続値データに対してはガウス分布が適用できます。
テキストの頻度ベクトルに使うのは一般的ではないですが、
先の例でいうと、「野球」という単語(特徴量)について
\(P(x_{野球}|y=スポーツ)\)
\(P(x_{野球}|y=政治)\)
\(P(x_{野球}|y=経済)\)
をガウス分布の確率密度関数で表します。
この確率密度関数の平均と分散には、訓練データ内のそれぞれのカテゴリーごとの記事における「野球」の平均出現回数とその分散を使い"推定"します。

\地味ですがこの部分が機械学習でいう学習です/

上の確率を考えてみると最初のひとつは平均が大きく、下の2つは平均が小さくなるはずです。
もしある記事Xに野球という単語が7回出た場合、確率\(P(x_{野球}=7|y=スポーツ)\)は大きくなりますが、他の2つの確率は平均から離れてしまうため小さくなります。
これが「野球」という単語が多ければスポーツぽい、政治や経済の記事には「野球」はそんなに出ない、という感覚を捉えていることがわかります。

以上より\(P(y|x)\)を求めることができるようになりました。


で、どうやって疑似データつくるの

ここまで来ると「確率分布からサンプリングする」の意味もわかりやすくなったと思います。
各特徴量について確率分布(今回はガウス分布)を求めました。

例えば、ガウス分布\(P(x_{野球}|y=スポーツ)\)からサンプリングすると、スポーツ記事に対する単語「野球」の擬似的な出現回数を得ることができます。

サンプリングするというのは、訓練データで求めたカテゴリー「スポーツ」の単語「野球」出現回数に関する平均と分散をもつガウス分布から、乱数生成を行うことを意味します。

同じように、他の単語についてもスポーツを条件にした分布からサンプリングすると、すべての単語についての擬似的な出現回数が得られます。
これをつなげたものは(擬似的な)単語頻度ベクトルそのものです。
これが、データを生成する、サンプリングする、という処理になります。


irisデータセットで試してみる

上記はテキストデータの話ですが、可視化という点ではテキストデータにはぱっと見の分かりやすさがないです。
そのため、お馴染みのirisで試してみます。

irisには4つの特徴量がありますが、適当にpairplotした結果、petal_lengthとpetal_widthのペアが品種ごとにいい感じに分かれていたので、これを可視化してみます。

import seaborn as sns
import matplotlib.pyplot as plt
iris = sns.load_dataset("iris")

sns.lmplot(data=iris, x="petal_length", y="petal_width", hue="species", fit_reg=False)
plt.show()

いい感じですね。

次にナイーブベイズを学習させ、各特徴量に対する平均と分散を取得します。
平均、分散ともに、品種3クラスに対する各特徴量(4つ)について求まるので、3 x 4の行列になっていると思います。

from sklearn.naive_bayes import GaussianNB

clf = GaussianNB()

col = iris.columns
X = iris[col[:-1]]
y = iris[col[-1]]

clf.fit(X, y)

print(clf.theta_) # 平均
print(clf.sigma_) # 分散
print(clf.class_prior_) # 事前分布

ついでに事前分布も見れます。
irisデータセットは、品種3種類 x 50個ずつなので、事前分布の確率はすべて33.3%になります。

次に、各特徴量に対して、ナイーブベイズの学習したパラメータから、疑似データをサンプリングします。
元データと同じく品種ごとに50点ずつサンプリングします。
サンプリングした疑似データのラベルは"setosa"なら"setosa2"として区別できるようにします。

import pandas as pd
import numpy as np

theta = pd.DataFrame(clf.theta_, columns=col[:-1]) # 平均
sigma = pd.DataFrame(clf.sigma_, columns=col[:-1]) # 分散


data = []
spieces = []

size = 50

for c in range(len(clf.classes_)):
  c_data = []
  for i in range(len(col)-1):
    c_data += [np.random.normal(loc=theta.loc[c, col[i]],
                                scale=sigma.loc[c, col[i]]**0.5,
                                size=size)]
  data += [np.vstack(c_data).T]
  spieces += [np.array([clf.classes_[c]+"2"]*size)]

data = np.vstack(data)
spieces = np.hstack(spieces)

data = pd.DataFrame(data, columns=col[:-1])
spieces = pd.DataFrame(spieces, columns=col[-1:])

data = pd.concat([data, spieces], axis=1)


さて、いよいよ可視化です。
上手くいっていれば、疑似データが同じ品種の元データに重なるように分布しているはずです。

sns.lmplot(x="petal_length", y="petal_width", data=pd.concat([iris, data]), hue="species", fit_reg=False, legend=True)
plt.show()


おおおお!!
いい感じです。
元データに対して、同じ品種の疑似データがいい感じに重なっています。
これが生成モデルなんだ!!!!

今日のベストプラクティス

なんか最後テンション上がってしまいました。
長々と書きましたが、ナイーブベイズ自体はシンプルです。
結局やっていることは、特徴量の平均を求めて、その平均に近いデータを作っているだけですね。

このブログを検索

QooQ