機械学習入門(多値分類モデル)

schedule 2021/11/26  refresh 2023/11/08

 

前回はAかBかというような2値分類モデルの説明をしましたが2つの分類じゃたりない!なんてことは山ほどあるわけで今回は多値分類モデルです。


コンセプトは単純に前回の分類モデルを分類する数だけつくり一番確率の高いものを選ぶというだけです。といっても分類モデルを分類数毎に教育・判定をするのではなく2値分類モデルを拡張します。

 

まず分類を判定する境界関数を複数に拡張する必要があります。インプット数m、分類数をnとすると単純に重みWを(mx1)の行列(ベクトル)だったものを(mxn)の行列に拡張します。

 

次に境界関数の結果の確率化にシグモイド関数を使いましたが一つの変数が0から1の間で変化するもので多値になったとき表現ができずに困ります。なのでsoftmax関数という複数の変数値の合計が1になるという都合のいい関数を結果表現に使うようにします。あとは前回と全く同じです。

 

 

多値分類モデル作成のステップ

  • 分類の境界関数を仮定する。
  • 確率関数化する。
  • 関数を学習データに最適化し関数の係数を決定する。

 

 

1. 分類の境界関数を仮定する。

 

データ

前回同様データは機械学習の学習用に公開されているデータiris datasetというがくの長さ、幅、花びらの長さ、幅とあやめの種類のデータを利用します。
前回はあやめの種類を2つに制限して取得してましたが3種類全部使います。

 

境界関数はmn行列とm次列ベクトルの積となります。(テキストで書きにくいので行ベクトルっぽく書いてます。) 今まではAかBかが答えだったので評価指標は一つ(Aである確率、Bの確率はAでない確率と表現できるので)だったがインプット数m、分類数nとすると mn行列とm次列ベクトルの積なのでn次ベクトルが答えとなります。

 

境界関数

u = (w11 * x1 + w12 * x2 + w13 * x3 + w14 * x4 + x5,
    w21 * x1 + w22 * x2 + w23 * x3 + w24 * x4 + x5,
     w31 * x1 + w32 * x2 + w33 * x3 + w34 * x4 + x5)
   = W・X

 

 

結果は上記のようにn次ベクトルになり判断しにくいので一番確率が大きかった分類と判定し1,0表記に変換して比較しやすくします。

 

(0.2, 0.1, 0.7) => (0, 0, 1)

 

 

2. 確率関数化する。

 

softmax関数

結果ベクトルの値の自然対数の大きさを確率として表現してます。対象の自然対数を自然対数の総和で割っただけです。


なぜ自然対数の指数表現にするかというと例によって計算できなくなるのを防ぎ微分などの計算に非常に都合がよく絶対値に意味がなく置き換えても矛盾がないというだけですので深く考えないでください。
例えば境界関数の結果ベクトルuの要素2の確率は以下のようになります

 

f(u2) = e ** u2 / e ** u1 + e ** u2 + e ** u3
         = exp(u2) / Σexp(u)

 



3. 関数を学習データに最適化し関数の係数を決定する。

ここも前回のやり方をそのまま適用します。
正解値yt,予測値ypとすると以下のようになります。単にそれぞれ値が1つの値からベクトルに変わっただけです。

 

yt = (yt0, yt1, yt2)
yp = (yp0, yp1, yp2)

 

さらに分類数nとしiを0からn-1までの変数としデータ数Mとしjを0からM-1までの変数とすると対数尤度関数z0として全体をzとすると以下のようになります。

 

z0 = Σyti*log(ypi)
z = Σz0j

 

さらに前回同様損失関数L、データ数M、対数尤度関数zとすると以下のようになります。

 

L = -1 / M * z

 

そして重み行列のa列b行のwabでこの式を偏微分するどうなるかが問題です。

 

θL/θwab = θL/θua * θua/θwa

 

ここで損失関数Lは境界関数とsoftmax関数z0・対数尤度関数f(=yp)の総和の合成関数なので

 

θL/θwab = Σ(θz0/θyp * θyp/θua) * θua/θwab

 

境界関数部分の偏微分θua/θwabは

ua = wa1 * x1 + wa2 * x2 + ・・・ + wab * xb + ・・・

なので

 

θua/θwab = xb

 

次にθyp/θuaは分類iについて偏微分を考えるとsoftmax関数の偏微分です。
exp(x) = f(x)、Σexp(u)をg(x)と定義すると商の微分公式を利用して

 

(f(x)/g(x))' = (f'(x)g(x) - f(x)g'(x)) / g(x) ** 2

 

θyp(ui)/θua = θexp(ui)/θua

# i = aのとき

             = θexp(ui)/θui
             = exp(ui)
             = yp(ui)

# i ≠ aのとき

             = θexp(ui)/θua
             = 0


θg(ui)/θua = θ(exp(u0) + exp(u1) + ・・・ + exp(ua) + ・・・)/θua
             = θexp(ua)/θua
             = exp(ua)
             = yp(ua)

 

つまりz0の要素z0(ui)をuaで偏微分すると

 

θyp(ui)/θua = (θf(ui)/θua * g(ui) - f(ui) * θg(ui)/θua) / g(ui) ** 2

# i = aのときはf(ui) = θf(ui)/θua = θg(ui)/θuaとなり 

θyp(ui)/θux = ((f(ui)*g(ui)) - f(ui) ** 2) / g(ui) **2
              = (f(ui)/g(ui))(1 - f(ui)/g(ui))
              = yp(ui) * (1 - yp(ui))

# i ≠ aのときはθf(ui)/θua = 0, θg(ui)/θua = f(ua)

θyp(ui)/θua = ((0*g(ui)) - f(ui) * f(ua)) / g(ui) **2
              = -1 * (f(ui)/g(ui))*(f(ua)/f(ua))
              = -1 * yp(ui)*yp(ua)

 

最後にθz0/θypの部分は

 

θz0/θyp = Σyti*log(ypi)
          = θ(yt0*log(yp0) + yt1*log(yp1) + ・・・ yta*log(ypa) + ・・・)/θypa
          = θ(yta*log(ypa))/θypa
          = yta/ypa

 

Σ(θz0/θyp * θyp/θua)をまとめると

 

θz0/θua = θz0/θyp0*θyp0/θua + θz0/θyp1*θyp1/θua + ・・・ + θz0/θypa*θypa/θua + ・・・
          = -yt0/yp0*(-1*yp0*ypa) + -yt1/yp1*(-1*yp1*ypa) + ・・・ + -yta/ypa*(ypa*(1 - ypa)) + ・・・
          = yt0*ypa + yt1*ypa + ・・・ + yta(ypa - yta) +  ・・・
          = ypa(Σyt) - yta
          = ypa - yta

 

 

すべてまとめると

 

θL/θwab = θL/θua * θua/θwab
          = xb * (ypa - yta)

 

機械学習入門(分類モデル)の結果と比較してみましょう。想像通り変数の添え字が増えただけですね。
今回も長々説明しましたが重みがベクトルから行列に、結果が一つの数値からベクトルに変わっただけでした。
なのでプログラム的にはほぼ一緒です。

 

θL/θwb = xb * (yp - yt)

 

  

Pythonでの実装

データ取得

 

from sklearn import datasets
from sklearn.model_selection import train_test_split



def onehot(y):
    return np.eye(3)[y]

iris = datasets.load_iris()
x = iris.data
y = iris.target
y2 = onehot(y)

 

今回は3種類全てのアヤメのデータを使います。onehost関数は0 => (1,0,0)のように分類を3次元ベクトルに変換してます。np.eye(3)は3*3の単位ベクトルです。単位ベクトルが行ベクトルは行数の分類ベクトルと同じなのでかけてます。

 

softmax関数

 

def softmax(u):
    e = np.exp(u)
    return (e / e.sum(axis=0))

 

説明のとおりだと上のようなコードを書いてしまいそうですがuが行列だと処理の方向が違うので一回転置して処理してから転置して返します。また値が大きくなりすぎてオーバーフローしてしまうので各行の最大値を引いて値を小さくします。これは計算の安定性を保つためにこうやるものと覚えてもらったほうがいいです。よって以下の通りです。

 

def softmax(u):
    u = u.T
    u -= u.max(axis=0)
    e = np.exp(u)
    return (e / e.sum(axis=0)).T

 

予測関数

 

def pred(x, w):
    return softmax(x @ w)

 

前回同様sigmoidがsoftmaxに変わっただけです。

 

正解率

 

def accuracy_rate(w, x, y):
    yp = pred(x, w)
    yp = np.eye(3)[argmax(yp, axis=1)]
    diff = (y == yp)
    correct = diff[np.where(diff == True)]
    return (correct.size / diff.size) * 100

 

前回とはyp = np.where(yp > 0.5, 1, 0)yp = np.eye(3)[argmax(yp, axis=1)]に変わっただけです。onehot関数と同じことをやっているだけです。

今回も前回のコードと全く同じなので説明を割愛。

 

全体

 

import numpy as np
from numpy.core.fromnumeric import argmax
from sklearn import datasets
from sklearn.model_selection import train_test_split

def onehot(y):
    return np.eye(3)[y]

def softmax(u):
    u = u.T
    u -= u.max(axis=0)
    e = np.exp(u)
    return (e / e.sum(axis=0)).T

def pred(x, w):
    return softmax(x @ w)

def accuracy_rate(w, x, y):
    yp = pred(x, w)
    yp = np.eye(3)[argmax(yp, axis=1)]
    diff = (y == yp)
    correct = diff[np.where(diff == True)]
    return (correct.size / diff.size) * 100

iris = datasets.load_iris()
x = iris.data
y = iris.target
y2 = onehot(y)
train_x, test_x, train_y, test_y = train_test_split(x, y2, test_size=0.3)

# データ数
M = train_x.shape[0]
# データ次元数
D = train_x.shape[1]
w = np.ones((D, 3))

l_rate = 0.01
max_run = 4000

rate = accuracy_rate(w, test_x, test_y)
print(rate)

x = train_x
yt = train_y

for _ in range(max_run):
    yp = pred(x, w)
    yd = yp - yt
    w -= l_rate * x.T @ yd / M

rate = accuracy_rate(w, test_x, test_y)
print(rate)

 

実行してみると

 

60.0
98.51851851851852

 

毎回誤差はありますが学習の成果がでてますね。次回はいよいよニューラルネットワークです。