Widrow-Hoff の学習規則でクラス分類

はじめに

shop.ohmsha.co.jp

わかりやすいパターン認識の第 3 章で Widrow-Hoff の学習規則が説明されていたので実装してみた。

コード

コードはこんな感じ。それぞれ 2 次元正規分布に従う 2 クラスのパターンで学習してみた。パターンはわざとクラス間で一部重なるようにしておいて、線形分離できないケースを試してみた。

from matplotlib import pyplot as plt
import numpy as np


class WidrowHoff:
    EPOCH = 100

    def __init__(self, n_dim, n_class, rho=1e-3):
        self.W = np.random.randn(n_class, n_dim + 1)
        self.rho = rho

    def train(self, data, label):
        for i in range(self.__class__.EPOCH):
            # shuffle
            perm = np.random.permutation(len(data))
            data, label = data[perm], label[perm]

            for x, y in zip(list(data), list(label)):
                # update weight
                X = np.repeat(np.array(list(x) + [1])[np.newaxis, :],
                              self.W.shape[0],
                              axis=0)
                E = np.repeat((self._predict(x) - y)[:, np.newaxis],
                              self.W.shape[1],
                              axis=1)
                self.W = self.W - self.rho * E * X

    def _predict(self, x):
        x = np.array(list(x) + [1])
        return np.dot(self.W, x)

    def predict(self, x):
        return np.argmax(self._predict(x), axis=0)


if __name__ == '__main__':
    # prepare dataset
    X_c0 = 5 * np.random.randn(100, 2)
    X_c1 = 5 * np.random.randn(100, 2) + np.array([10, 10])

    data = np.concatenate([X_c0, X_c1])
    label = np.array([[1, 0] for i in range(100)] +
                     [[0, 1] for i in range(100)])

    # train
    widrow_hoff = WidrowHoff(n_dim=2, n_class=2)
    widrow_hoff.train(data, label)

    # show
    plt.scatter(X_c0[:, 0], X_c0[:, 1])
    plt.scatter(X_c1[:, 0], X_c1[:, 1])

    w = widrow_hoff.W[0] - widrow_hoff.W[1]
    y = lambda x: w[0] / -w[1] * x + w[2] / -w[1]
    x = np.arange(-10, 20, 0.1)
    plt.plot(x, y(x))

    plt.show()

結果

こんな感じで決定境界が引けた。なんとなくそれっぽい境界になってそう。

f:id:ntsujio:20180218001710p:plain

そしてエポックごとにその時点での識別精度をグラフにしたものが以下。

f:id:ntsujio:20180218002120p:plain

今回のデータはそれぞれ平均が (0, 0) と (10, 10)、標準僅差が 5、x と y は独立の 2 次元正規分布を使ったことを考えると、(0, 0) と (10, 10) 間の距離 / 2 / 5 = 1.4142... ということで、標準正規分布で 1.414 よりも上側の確率と識別精度は一致するはず。

正規分布表を参照するとだいたい 0.079 くらいなので、グラフの値とおおよそ一致した。

まとめ

  • Widrow-Hoff でクラス分類してみた
  • 線形分離不可能なパターンでもいい感じに境界引けた
  • 識別性能も理論値とだいたい一致した

参考

パーセプトロンで多クラスの分類

はじめに

shop.ohmsha.co.jp

わかりやすいパターン認識の第 2 章で多クラスを分類するパーセプトロンが説明されていたので実装してみた。

学習データは パーセプトロンで Iris データセットの 2 クラス分類 - 研究会 の時のように Iris データセットを使おうと思ったが、どの特徴量を選んでも線形分離できないっぽかったので疑似乱数で生成したパターンを用いた。

コード

コードは以下。重みはクラス数×パターン数の行列として表現している。

from matplotlib import pyplot as plt
import numpy as np


class MultiClassPerceptron:
    def __init__(self, x_dim, n_class, rho=1e-3):
        self.W = np.random.randn(n_class, x_dim + 1)
        self.rho = rho

    def train(self, data, label):
        while True:
            # shuffle
            perm = np.random.permutation(len(data))
            data, label = data[perm], label[perm]

            classified = True

            for x, y in zip(list(data), list(label)):
                pred = self.predict(x)
                if pred != y:
                    classified = False

                    # update weight
                    x = np.array(list(x) + [1])
                    self.W[y] = self.W[y] + self.rho * x
                    self.W[pred] = self.W[pred] - self.rho * x

            if classified:
                break

    def predict(self, x):
        x = np.array(list(x) + [1])
        return np.argmax(np.dot(self.W, x), axis=0)


if __name__ == '__main__':
    x_c0 = np.random.randn(50, 2)
    x_c1 = np.random.randn(50, 2) + np.array([0, 8])
    x_c2 = np.random.randn(50, 2) + np.array([8, 0])

    perceptron = MultiClassPerceptron(2, 3)

    perceptron.train(np.concatenate([x_c0, x_c1, x_c2]),
                     np.array([0] * 50 + [1] * 50 + [2] * 50))

    # display
    plt.scatter(x_c0[:, 0], x_c0[:, 1], label='c0')
    plt.scatter(x_c1[:, 0], x_c1[:, 1], label='c1')
    plt.scatter(x_c2[:, 0], x_c2[:, 1], label='c2')

    W = perceptron.W
    w_0_1 = W[0] - W[1]
    w_1_2 = W[1] - W[2]
    w_2_0 = W[2] - W[0]
    y_0_1 = lambda x: w_0_1[0] / -w_0_1[1] * x + w_0_1[2] / -w_0_1[1]
    y_1_2 = lambda x: w_1_2[0] / -w_1_2[1] * x + w_1_2[2] / -w_1_2[1]
    y_2_0 = lambda x: w_2_0[0] / -w_2_0[1] * x + w_2_0[2] / -w_2_0[1]
    x = np.arange(-2, 10, 0.1)
    plt.plot(x, y_0_1(x), label='c0 - c1')
    plt.plot(x, y_1_2(x), label='c1 - c2')
    plt.plot(x, y_2_0(x), label='c2 - c0')

    plt.xlabel('x')
    plt.ylabel('y')
    plt.legend()
    plt.show()

結果

実行結果はこんな感じ。

f:id:ntsujio:20180215030031p:plain

決定境界を引いてみたけどかえって分かりにくいかもしれない (領域で色分けしようと思ったけど力尽きた・・・)。けどうまく分類できそうな境界になってる気がする。

あとこの実装だと決定境界が常に 1 点で交わるのだけど、たぶんこれはわかりやすいパターン認識の第 4 章 (61 ページ) で説明されている、「(c) 識別関数 {g_i(x)} の大小によってクラスを決定できる場合」に該当するので、リジェクト領域がないということなのだろう。

まとめ

  • 多クラス分類できるパーセプトロンを書いた
  • 領域を可視化するの難しい
  • 出力層の値が最大になるクラスという基準で分類すると決定境界が 1 点で交わった

参考

パーセプトロンで Iris データセットの 2 クラス分類

はじめに

shop.ohmsha.co.jp

わかりやすいパターン認識の第 2 章でパーセプトロンが説明されていたので実装してみた。

学習データは scikit-learn が提供している Iris データセットを使った。

Iris (アヤメ) データセットは Setosa, Versicolour, Virginica の 3 品種 (クラス) のパターンがまとめられていて、特徴量としてがくの長さ、幅、同じく花弁の長さ、幅の 4 次元が選択されている。

試しに花弁の長さと幅を軸にとってデータを散布図に起こしてみた。

f:id:ntsujio:20180214011431p:plain

そしてそれぞれの品種の画像が以下。

Iris setosa - Wikipedia

f:id:ntsujio:20180214011224j:plain

Iris versicolor - Wikipedia

f:id:ntsujio:20180214010903j:plain

Iris virginica - Wikipedia

f:id:ntsujio:20180214010952j:plain

確かに Setosa, Versicolour, Virginica の順に花弁が大きい・・・気がする。

コード

パーセプトロンは線形分離可能なクラスしか分類できないらしいので、花弁の大きさで線形分離できそうな Setosa と Versicolour を分類してみた。

import os
from matplotlib import pyplot as plt
from matplotlib.font_manager import FontProperties
import numpy as np
from sklearn import datasets, model_selection


class Perceptron:
    def __init__(self, x_dim, rho=1e-3):
        self.w = np.random.randn(x_dim + 1)
        self.rho = rho

    def train(self, data, label):
        while True:
            # shuffle
            perm = np.random.permutation(len(data))
            data, label = data[perm], label[perm]

            classified = True

            for x, y in zip(list(data), list(label)):
                pred = self.predict(x)
                if pred != y:
                    classified = False

                    # update weight
                    x = np.array(list(x) + [1])
                    self.w = self.w - pred * self.rho * x

            if classified:
                break

    def predict(self, x):
        x = np.array(list(x) + [1])
        return 1 if np.dot(self.w, x) > 0 else -1


if __name__ == '__main__':
    dataset = datasets.load_iris()

    x_train, y_train = dataset.data, dataset.target

    perceptron = Perceptron(x_dim=2)

    # preprocess train data
    mask = np.bitwise_or(y_train == 0, y_train == 1)
    x_train = x_train[mask][:, 2:]
    y_train = y_train[mask]
    y_train = np.array([-1 if y == 0 else 1 for y in y_train])

    # train
    perceptron.train(x_train, y_train)

    # display
    fp = FontProperties(fname=r'C:\Windows\Fonts\meiryo.ttc', size=12)

    x_c0 = x_train[y_train == -1]
    x_c1 = x_train[y_train == 1]
    plt.scatter(x_c0[:, 0], x_c0[:, 1], label='Setosa')
    plt.scatter(x_c1[:, 0], x_c1[:, 1], label='Versicolour')

    w = perceptron.w
    y = lambda x: w[0] / -w[1] * x + w[2] / -w[1]
    x = np.arange(1, 5, 0.1)
    plt.plot(x, y(x))

    plt.xlabel('花弁の長さ', fontproperties=fp)
    plt.ylabel('花弁の幅', fontproperties=fp)
    plt.title('パーセプトロンによるアヤメ科の花の分類', fontproperties=fp)

    plt.legend()
    plt.show()

実行結果

こんな感じな図が描けた。ちゃんと分類できそうな境界が引けている。

f:id:ntsujio:20180214012353p:plain

ちなみに、Versicolour と Virginica だと線形分離できないので学習時に無限ループしてしまった。

まとめ

  • パーセプトロンで Iris データセットを分類してみた
  • Setosa と Versicolour は花弁の大きさで決定境界を引けた
  • Versicolour と Virginica は花弁の大きさでは決定境界を引けなかった

参考

Nearest Neighbor 法で MNIST データセットの分類

はじめに

shop.ohmsha.co.jp

わかりやすいパターン認識の第 1 章で k-NN 法が説明されていたので、MNIST データセットを分類してみる。

コード

コードは以下の通り。特徴ベクトルとしてはピクセルごとの濃淡値をそのまま使い、距離尺度としてはユークリッド距離を用いた。

import os
from matplotlib import pyplot as plt
import numpy as np
from sklearn import datasets, model_selection

K = int(os.environ.get('NN_K', 5))
CLUSTER_SIZE = int(os.environ.get('NN_CLUSTER_SIZE', 10))


def distance(x, y):
    return np.sqrt(np.sum((x - y)**2))


def predict(x, prototypes):
    # calculate distances from x for each prototype
    distances = []
    for cls, data in prototypes.items():
        distances.extend([(int(cls), distance(x, y)) for y in data])

    distances.sort(key=lambda d: d[1])

    # count classes of top K
    class_count = [0] * 10
    for cls, _ in distances[:K]:
        class_count[cls] += 1

    # return the class having maximum class count
    return max(enumerate(class_count), key=lambda i: i[1])[0]


if __name__ == '__main__':
    mnist = datasets.fetch_mldata('MNIST original', data_home='.')

    x_train, x_test, y_train, y_test = model_selection.train_test_split(
        mnist.data, mnist.target, test_size=0.5, shuffle=True
    )

    # choose prototypes for each class
    prototypes = {}
    for i in range(10):
        data = x_train[y_train == i]
        prototypes[str(i)] = data[:CLUSTER_SIZE]

    # predict
    predicts = np.array([predict(x, prototypes) for x in x_test])
    accuracy = len(x_test[predicts == y_test]) / len(x_test)

    print(f"accuracy = {accuracy}")

結果

CLUSTER_SIZE を変化させて分類精度と処理時間をグラフにしてみた。

K は CLUSTER_SIZE = 1 のときは 1、それ以外の時は 10 に設定した。

f:id:ntsujio:20180213001728p:plain

CLUSTER_SIZE が 1000 のときは分類精度が 0.95 と、単純方法の割に予想以上に高い結果となった。

また処理時間は CLUSTER_SIZE に線形になった。最近傍を求めるためにクラスター内の全パターンとの距離を計算しているためであろう。

まとめ

  • k-NN で MNIST を分類してみた
  • 単純な方法でも以外と精度出た
  • データ量大事

参考

MNIST データセットの読み込み方

機械学習を学習する上で学習データを準備するのに苦労する場面がよくある。

今回は MNIST データセットと呼ばれる、機械学習ベンチマークでよく使われるデータセットの使い方をまとめる。

MNIST データセット

MNIST データセット は手書き数字文字データをまとめたデータセットであり、訓練用に 60,000 枚、テスト用に 10,000 枚の画像データ、そしてそれぞれの正解データ (画像がどの数字を表しているか) が用意されている。

データの形式は独自のバイナリで、ページの下のほうにフォーマットが解説されている。プログラムで扱う際はこのバイナリをパースして使う。

コード

ページの解説に従ってバイナリをパースするコードが以下。Python でバイナリをパースするのは struct モジュールを使った。

gist7b55be06fba08e00a51edf9e4f2eb207

読み込んだ画像の表示

画像は matplotlib モジュールで表示できる。表示コードと結果は以下の通り。

gistf9ec5c6229fa65402b8bbe012d23c887

f:id:ntsujio:20180211205121p:plain

画像は「5」のデータ。ちゃんと読み込めてるっぽい。

scikit-learn で読み込む

実は MNIST データセットは自分でコードを書かなくてもバイナリを読み込める。

機械学習ライブラリである scikit-learn を使うとこんな感じに書ける。

from sklearn import datasets

mnist = datasets.fetch_mldata('MNIST original', data_home='.')

img = mnist.data[0].reshape(28, 28)

plt.imshow(img, cmap='gray')
plt.show()

fetch_mldata メソッドのドキュメントを見ると、データは mldata.org から取得すると書いてある。

mldata.org は MNIST データセットだけでなく、様々なデータセットを公開している。また有用なデータセットをアップロードして貢献することもできる。

MNIST (Original) のページを見ると CC0 ライセンスと記載されており、学習にとても有用である。感謝。

まとめ

  • 機械学習の学習データには MNIST データセットが便利
  • MNIST データセットの読み込みには scikit-learn が便利
  • 便利なものを用意してくれている先人に感謝

参考

人工知能を初めから学びなおそうと思った

最近、二十代も終盤に差し掛かり、技術者として何か一本これというスキルを得たいと思い始めている。

世間では AI、コンテナ、ブロックチェーン、ドローン…等々、華やかな話題で賑わっているが、さて自分はどのような分野に興味があるだろうか。

これまでの人生を振り返ってみると、自分は絵だったりプログラムだったり、何か「作品」を作ることが好きだったように思える。

それでいくと、AI の分野ではゴッホに絵を描かせたり (ゴッホの絵をコンピュータに描かせるとどうなるか? - GIGAZINE)、白黒画に自動着色したり (「あなたの絵をAIが自動着色します!」 コミケで体験 - ITmedia NEWS) といった作品が発表されている。また機械学習で動く木 (How to Walk Branches, 前川和純 他, 第19回東京大学制作展 “WYSIWYG?”) みたいな、原始の知能を感じさせる作品も感動する。これを自分もやってみたい。

しかしこれまで人工知能については熱心に勉強してこず基礎が分かっていない感があるので、いきなりディープラーニングとか強化学習に手を出しても何もできないまま終わる可能性がある。

とりあえず学部生レベルから復習するために、確率統計の基礎から学びなおすことにした。

ということでこれから復習系の投稿が増える予定。続くといいな。

Freenet のプラグインを作ろう: HelloWorld プラグインの作成

はじめに

最近 Freenetプラグインを書く必要に迫られ情報収集をしている。しかし開発に当たって必要な情報が英語でも日本語でも纏まっていない印象なのでここに記しておく。

Freenet はインターネット検閲に対抗するため匿名でのコミュニケーションを実現する P2P 型システムである。ユーザーからは分散型のストレージのような感じで使える。そしてそのストレージ機能を利用して匿名メールや匿名掲示板のようなものを実現するプラグインが開発されている。

今回は公式 Wikiチュートリアル に則って簡単な HelloWorld プラグインを作成する。

ソースコード

HelloWorld.java

/**
 * 参考: https://wiki.freenetproject.org/Plugin_API
 */

import freenet.pluginmanager.*;
import java.util.Date;

public class HelloWorld implements FredPlugin {
    private volatile boolean goon = true;

    /* Freenet の API にアクセスするためのオブジェクト */
    PluginRespirator pr;

    /* プラグインがアンロードされた時に呼ばれる */
    public void terminate() {
        goon = false;
    }

    /* プラグインがロードされた時に呼ばれる */
    public void runPlugin(PluginRespirator pr) {
        this.pr = pr;

        while (goon) {
            /* 標準エラーへの出力は wrapper.log ファイルに吐かれる */
            System.err.println("Heartbeat from HelloWorld-plugin: " + (new Date()));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // Who cares ?
            }
        }
    }
}

manifest.mf

※ jar を作るときに生成されるマニフェストファイルへ追記する設定。Freenetプラグインのエントリーポイントを知らせる。

Plugin-Main-Class: HelloWorld

ビルド

Freenet のインストールフォルダー (Windows でのデフォルトは AppData\Local\Freenet) に freenet.jar があるはずなので、クラスパスとして指定しつつビルドする。また jar 作成時にマニフェストファイルも指定する。

javac -cp path\to\freenet.jar HelloWorld.java
jar -cvmf manifest.mf HelloWorld.jar HelloWorld.class

実行

Freenet の Web インターフェースにアクセス (localhost:8888 あたり) し、上部の「設定」タブから「プラグイン」に移動、下部の「非公式プラグインを追加」で作成した HelloWorld.jar を指定してロードする。

AppData\Local\Freenet\wrapper 付近にある wrapper.log というファイルを見ると以下のような出力が確認されるはず。

INFO   | jvm 1    | 2014/12/14 02:31:56 | Downloading plugin path\to\HelloWorld.jar
INFO   | jvm 1    | 2014/12/14 02:31:56 | Heartbeat from HelloWorld-plugin: Sun Dec 14 02:31:56 JST 2014
INFO   | jvm 1    | 2014/12/14 02:31:57 | Heartbeat from HelloWorld-plugin: Sun Dec 14 02:31:57 JST 2014

プラグインのアンロードは「Plugins currently loaded」から HelloWorld プラグインを見つけて「アンロード」ボタンを押す。

まとめ