surolog

AI・機械学習・データ分析 と 本 など

Pythonでパーセプトロンのふるまいを可視化する

普段分析で機械学習は使いますが、実際に学習器がどう動いて回帰や分類をするのか理屈では理解していた(つもり)ですが、体感としてはよくわかってませんでした。
それに機械学習パッケージを使うと、恐ろしいことに仕組みを全く知らなくてもそれらしい結果が出せてしまうので、本質を理解できていなくても使いこなしている気分になってしまいます。
まあ使えて結果が出れば、それでいいじゃんという話もありますが、なんとなくもやもやしてたんですよね。

で、そんなときに以下の記事を見つけて、すごく肚に落ちたというかもやもやがかなりスッキリしました。


ロジスティック回帰 (勾配降下法 / 確率的勾配降下法) を可視化する - StatsFragments

やっぱり可視化されると納得感が違いますねー。
ということで、自分でもやってみたいと思いこの記事を書きました。
(sinhrksさんの許可を得て一部コードは流用させていただきました。ありがとうございます。)

前置きはこのくらいで、本題に入っていきます。

パーセプトロンについて

パーセプトロンが何かについては、検索すればいくらでも記事や書籍がありますのでここで詳細な説明は省きます。
例えば私は以下の記事などを見て勉強させていただきました。


TokyoNLP#5で「パーセプトロンで楽しい仲間がぽぽぽぽ〜ん」を発表しました - 睡眠不足?!
機械学習超入門III 〜機械学習の基礎、パーセプトロンを30分で作って学ぶ〜 - EchizenBlog-Zwei
テキストマイニングのための機械学習超入門 二夜目 パーセプトロン - あんちべ!


機械学習モデルの分類の1つとして、事前分布から仮定する生成モデル・事後分布のみ仮定する識別モデル・分布を仮定しない識別関数という見方*1がありますが、パーセプトロンは識別関数の最も基本的な学習器と言えます。
最近の機械学習モデルの学習ルールと言えば、損失関数を最小化するようにパラメータを更新していきますが、パーセプトロンは間違えた時だけ更新する(誤り訂正法)ところが男らしいといえば男らしい気がしています(?)。

データを準備

まずはデータを作っていきます。
irisデータを使って、デモ分析用に少し加工します。

def load_data():
    # データ準備
    iris            = pandas.read_csv("http://aima.cs.berkeley.edu/data/iris.csv", header=None)
    names           = ['Sepal.Length', 'Sepal.Width', 'Petal.Length', 'Petal.Width', 'Species']
    iris.columns    = names
    # 2説明変数、目的変数(2クラス)に加工
    data    = iris[:100]
    x       = data[['Petal.Width', 'Petal.Length']]
    y       = data['Species']
    # ラベルを-1,1の列に変換
    y[ y=='setosa' ] = 1
    y[ y=='versicolor'] = -1
    # ランダムシャッフル
    indexer     = numpy.arange(x.shape[0])
    numpy.random.shuffle(indexer)
    # x,y並び替え
    y           = y.iloc[indexer, ]
    x           = x.iloc[indexer, ]
    # indexを初期化
    x           = x.set_index([range(100)])
    y.index     = [range(100)]

    return x,y

合わせて、可視化用の関数も作ります。

def plot_xy(x, y, colors, ax=None, figsize=(5,3.5)):
    if ax is None :
        # 描画領域作成
        fig = pyplot.figure(figsize=figsize)
        # 描画領域にaxesを追加、マージン調整
        ax = fig.add_subplot(1, 1, 1)
        fig.subplots_adjust(bottom=0.15)
    x1 = x.columns[0]
    x2 = x.columns[1]
    for (species, group), c in zip(x.groupby(y), colors):
        ax = group.plot(kind='scatter', x=x1, y=x2, color=c, ax=ax, figsize=figsize)
    return ax

デモ分析用のデータをplot_xyで可視化すると、こんな感じです。普通な感じですね。
f:id:sator926:20141217233227p:plain

探索法

決定境界を探索するため、xを元にyの予測値を定義していきます。
まずはxをwx+bという形でyと同じ次元に写像します。その後、さらに[-1,1]という離散値を持つ値に変換します。
なぜこんなシンプルな変換で良いのかというと、パーセプトロンは与えられた問題が線形分離可能であることを前提として構築されているからですね。なので「実際のyと予測値のyが違った時だけ、パラメータを少し更新する」という戦略で更新と探索を繰り返していけば、いつかは必ず決定境界が見つかることになります。よってyの予測値は「当たっているか外れているか」だけわかればよく、どの程度当たったか外したかという度合い(距離)は不要なので、すごくシンプルな写像になります。
これを表現してyの予測値を求めるのが以下のコードです。

def yhat(x, w, b):
    out =  numpy.dot(x, w) + b
    def link(a):
        if a >= 0:
            res = 1
        else :
            res = -1
        return res

    return link(out)

学習ルール

すでにくどくどと書いてしまっていますが、パーセプトロンでは予測が違った時のみ更新する「誤り訂正法」でパラメータを更新していきます。どの程度更新するか(学習率)はケースバイケースですが、あまり大きく更新すると振動して収束しなくなることもあるようです。今回は学習率として定数0.2を設定しました。更新の方法としては勾配降下法を用いています。

def learning_rule(x, y, w, b, eta=0.2):
    #パーセプトロン学習規則
    res = yhat(x, w, b)
    if ( res*y )<0:
        w = w + eta*x*y
        b = b + eta*y
    return w, b

上記学習を全データに対して実行する関数を作ります。

def update(x, y, w, b, num=3):
    for i in range(1, num):
        for index in range(x.shape[0]):
            # 1行ずつ逐次処理
            _x      = numpy.array(x.ix[index, :])
            _y      = numpy.array(y.ix[index, :])
            w,b     = learning_rule(_x, _y, w, b)
            yield i, w, b, _x

さらに、実行結果を逐次グラフとして保存するように上記関数をラップします。

def run_perceptron(x, generator, fig):
    figs = []

    def db_2d(x, w, b):
        x2 = -w[0]/w[1]*x - b/w[1] #決定境界
        return x2

    for i, w, b, _x in generator:
        by      = db_2d(x, w, b)
        _fig    = fig.plot(x, by, color='gray', linestyle='dashed')
        wt      = """Iteration = {0} times
w = [{1[0]:.2f}, {1[1]:.2f}]
b = {2:.2f}""".format(i, w, b)
        # axes上の相対座標(0.1, 0.9)にtextの上部を合わせて描画
        tx = fig.text(0.1, 0.9, wt, va='top', transform=fig.transAxes)
        # プロトタイプを描画
        _p = fig.plot(_x[0], _x[1], 'ks')
        figs.append(tuple(_fig) + tuple(_p) + (tx, ))

    return figs

これで必要な道具は揃いました。
これらを元に実行スクリプトを書くと、以下のようになります。

# 描画準備
###########
figsize =(5, 3.5)
fig     = pyplot.figure(figsize = figsize)
ax      = fig.add_subplot(1, 1, 1)
fig.subplots_adjust(bottom=0.15)

# 学習準備
###########
# データ作成
x, y    = load_data()
# w, bの初期値を作成
w, b    = [0,0], 0.1
gen     = update(x, y, w, b)

# 学習開始
###########
bx      = numpy.arange(x.ix[:, 0].min(), x.ix[:, 0].max(), 0.1)
figs    = run_perceptron(bx, gen, ax)

# 描画開始
###########
ax      = plot_xy(x, y, colors=['red', 'blue'], ax=ax)
ani     = animation.ArtistAnimation(fig, figs, interval=3, repeat=False)
pyplot.show()

上記コードを実行することで、以下のようにパーセプトロンの動きが可視化されました!
元データがきれいなので、比較的簡単に決定境界を見つけてしまいますね。。
一応、その時点で評価されているxの座標を黒い四角で表しています。
探索中に、この評価点の決定境界から得られる推定値が異なっている(誤りがある)時点で、パラメータが更新されて決定境界が動く様子が見て取れます。

f:id:sator926:20141223005228g:plain

今回の記事は以上です!
もしなにか間違いがあれば、ご指摘いただけると助かります。

せば

*1:ただこれも別にMECEな分け方ではなさそう