KDOG Notebook

どうも、古くからの友人です。

パーセプトロンによる多クラス分類

はじめに

パーセプトロンを用いたもっとも単純な分類器は2クラス分類(2値分類)である。今回は3クラス以上の分類モデルへの拡張をおこなう。また、後半ではロジスティック回帰を用いて分類性能の改善を行う。検証に用いたデータはscikit-learnに用意されているIrisデータセットである。「がく片の長さ」と「花びらの長さ」を特徴量にして、Iris-Setosa、Iris-VersicolorおよびIris-Virginicaの3種類のアヤメを分類した。全部で150個あるデータのうち105個をトレーニングデータ、残り45個をテストデータとして用いた。

理論

a) 3クラス分類への拡張

3次元からなる特徴量ベクトルから3つのクラス(A,B,C)に分類する場合のネットワークは以下の図のようになる。 例えば、クラスAに分類するときはユニット1が1を出力し、ユニット2およびユニット3は0を出力する( {\bf u}=[1, 0, 0 ])。同様に、クラスBまたはクラスCに分類するときはそれぞれ {\bf u}=[0, 1, 0 ]または {\bf u}=[0, 0, 1 ]が出力される。このようなネットワークに対してトレーニングデータを使って学習をおこない、重み {\bf w}およびバイアス {\bf b}を求めれば良い。

f:id:kdog08:20170514032903p:plain:w300

b) ロジスティック回帰

ネットワークの各ユニットで得られた総入力値( u_{i})に対して次のシグモイド関数と呼ばれる活性化関数を用いる。

{\displaystyle
y = \frac{1}{1+\exp(-x)}
}

シグモイド関数 x=0 y=0.5の値をとり、 x=\inftyおよび x=-\inftyでそれぞれ y=1 y=0に収束する関数である。 恒等関数を活性化関数として使う場合、ユニットの値には1か0以外に物理的意味を持たせることができない(例えば、Iris-Setosaか否か)。しかし、シグモイド関数を用いて、0から1までの連続値を取らせることでユニットのとる値に対して確率の概念を導入することができる(例えば、 y=0.8のとき、Iris-Setosaである確率が80%である)。

c) 負の対数尤度

この関数は確率論の導入に伴って、新しい誤差関数の指標として用いる。 統計学において尤度は収集したデータと統計モデルとの当てはまりの良さを表す。例えば、あるパラメータ \lambdaによって決まる確率分布 p(y|\lambda)と収集したデータ[tex: Y = \{ y{1}, y{2}, \cdots \}]との当てはまりの良さ、尤度 L

 \begin{eqnarray}
L(\lambda) = p(y_{1}|\lambda)p(y_{2}|\lambda)\cdots = \prod_{i} p(y_{i}|\lambda)
\end{eqnarray}

となる。ただし、

 \begin{eqnarray}
\sum_{y}^{\infty} p(y|\lambda) = 1
\end{eqnarray}

である。したがって、モデルの最適化は尤度が最大値をとるようなパラメータ \lambdaを求めることである。実際の手続きでは対数尤度を考える。

 \begin{eqnarray}
\log L(\lambda) = \sum_{i} \log p(y_{i}|\lambda)
\end{eqnarray}

機械学習では、負の対数尤度を誤差関数としてその最小値を持つようなパラーメタ {\bf w}を求めることが学習の目的となる。また、モデルの比較となる分布関数は (p_1,p_2,p_3) = (0, 1, 0)のような形を想定している(教師ラベルに相当)。したがって、誤差関数は

 \begin{eqnarray}
E({\bf w}) = - \sum_{i} t_{i}\log y_{i}
\end{eqnarray}

となる。

実装

a) 3クラス分類への拡張
class Perceptron_Multi_Classifier():
    def __init__(self, eta=0.01, n_iter=10, shuffle=True, random_state=None):
        self.eta = eta
        self.n_iter = n_iter
        self.shuffle = shuffle
        if random_state:
            seed(random_state)
    
    def train(self, X, y):
        self.w_ = np.random.uniform(low=-0.08, high=0.08, size=(X.shape[1], 3)).astype('float32')
        self.b_ = np.zeros(3).astype('float32')
        self.cost_ = np.zeros(self.n_iter)
        
        for j in range(self.n_iter):
            if self.shuffle:
                X, y = self._shuffle(X,y)
            cost = np.zeros(len(y))
            i = 0
            for xi, yi in zip(X, y):
                ti = self.one_hot_vec(yi)
                error = ti - self.predict(xi)
                update = self.eta * error
                self.w_ += np.dot(xi[:,np.newaxis],update[np.newaxis,:])
                self.b_ += update
                icost = 0.5 * np.sum(error**2)
                cost[i] = icost
                i += 1
            ave_cost = np.average(cost)
            self.cost_[j] = ave_cost
                        
        return self
    
    def _shuffle(self,X,y):
        r = np.random.permutation(len(y))
        return X[r], y[r]
    def one_hot_vec(self,y):
        t = np.zeros(3).astype('float32')
        t[y] = 1.0
        return t
    def predict(self,X):
        return np.dot(X,self.w_) + self.b_
    def activate(self,X):
        return self.predict(X).argmax(axis=1)

b) ロジスティック回帰の実装

class Perceptron_sigmoid():
    def __init__(self, eta=0.01, n_iter=10, shuffle=True, random_state=None):
        self.eta = eta
        self.n_iter = n_iter
        self.shuffle = shuffle
        if random_state:
            seed(random_state)
    
    def train(self, X, t):
        self.w_ = np.random.uniform(low=-0.08, high=0.08, size=(X.shape[1], 3)).astype('float32')
        self.b_ = np.zeros(3).astype('float32')
        self.cost_ = np.zeros(self.n_iter)
        
        for j in range(self.n_iter):
            if self.shuffle:
                X, t = self._shuffle(X,t)
            cost = np.zeros(len(t))
            i = 0
            for xi, ti in zip(X, t):
                t_label = ti
                ti = self.one_hot_vec(t_label)
                ui = self.predict(xi)
                yi = self.sigmoid(ui)
                icost = -np.sum(np.log(yi[t_label]+1.e-5))
                delta = yi - ti
                dW = np.dot(xi[:,np.newaxis],delta[np.newaxis,:])
                db = delta
                self.w_ -= self.eta*dW
                self.b_ -= self.eta*db
                cost[i] = icost
                i += 1

            ave_cost = np.average(cost)
            self.cost_[j] = ave_cost
                        
        return self
    
    def _shuffle(self,X,y):
        r = np.random.permutation(len(y))
        return X[r], y[r]
    def one_hot_vec(self,y):
        t = np.zeros(3).astype('float32')
        t[y] = 1.0
        return t
    def sigmoid(self,x):
        return 1 / (1 + np.exp(-x))
    def predict(self,X):
        return np.dot(X,self.w_) + self.b_
    def activate(self,X):
        return self.predict(X).argmax(axis=1)

結果

a) 3クラス分類

学習ごとの誤差関数の変化を下図に示す。エポック数が50に達したあたりで誤差関数の値が0.16に収束していることがわかった。また、45個のテストデータを用いて精度を調べたところ、13個を誤分類していることがわかった。

f:id:kdog08:20170514045353p:plain:w300

b) ロジスティック回帰を用いた分類

学習ごとの誤差関数の変化を下図に示す。値はおよそ0.37に収束することがわかった。また、45個のテストデータのうち2個を誤分類していることがわかった。

f:id:kdog08:20170514232842p:plain:w300

ロジスティック回帰を用いていない場合よりも誤差関数が大きな値を示したが、用いている誤差関数が異なるため値の大きさを比べることはここでは重要でないと考えられる。一方、シグモイド関数を用いたことで分類の精度が上がったと言える。

メモ

活性化関数にはシグモイド関数以外に、tanhやReLU関数などがある。単純に確率の概念を取り入れるための関数であり、ここでの回帰という言葉は統計で用いられる回帰と同じ意味合いを持って使われている用語ではないようだ。

参考文献

Sebastian Raschka 「Python機械学習プログラミング」第3章, インプレス, 2016.