Home > 4月, 2009

2009.04.02

10行強で書けるロジスティック回帰モデル学習

ロジスティック回帰(logistic regression)の学習が,確率的勾配降下法(SGD: stochastic gradient descent)を使って,非常に簡単に書けることを示すPythonコード.コメントや空行を除けば十数行です.

"""
    Logistic Regression with Stochastic Gradient Descent.
    Copyright (c) 2009, Naoaki Okazaki
"""
import collections
import math

N = 17997       # Change this to present the number of training instances.
eta0 = 0.1      # Initial learning rate; change this if desired.

def update(W, X, l, eta):
    # Compute the inner product of features and their weights.
    a = sum([W[x] for x in X])

    # Compute the gradient of the error function (avoiding +Inf overflow).
    g = ((1. / (1. + math.exp(-a))) - l) if -100. < a else (0. - l)

    # Update the feature weights by Stochastic Gradient Descent.
    for x in X:
        W[x] -= eta * g

def train(fi):
    t = 1
    W = collections.defaultdict(float)
    # Loop for instances.
    for line in fi:
        fields = line.strip('\n').split('\t')
        update(W, fields[1:], float(fields[0]), eta0 / (1 + t / float(N)))
        t += 1
    return W

リストの内包表記,条件演算子(Cで言う三項演算子),自動的に初期化してくれる辞書型(collections.defaultdict)は,Python以外ではあまり見ないかも知れません.

リストの内包表記は,Haskell, OCaml, C#にもあるようなので,結構メジャーかも知れません.

[W[x] for x in X]

と書くと,「Xに含まれるすべてのxに対し,それぞれW[x]を計算した結果をリストにしたもの」という意味になります.sum関数はリストの値の和を返すので,変数aにはXとWの内積が計算されます.

Pythonでは,三項演算子を条件演算子と呼びます.書き方もちょっと変わっていて「真の時の値 if 条件 else 偽の時の値」という形式で書きます.C言語では,「条件 ? 真の時の値 : 偽の時の値」なので,最初は違和感があるかも知れません.「x = 通常の値  if 通常の条件 else 異常な場合の値」と書けば,xと「通常の値」がソースコード上で隣接するので,慣れてくると分かりやすい気もします.

g = ((1. / (1. + math.exp(-a))) - l) if -100. < a else (0. - l)

では,内積の値aが-100よりも大きければ事例Xが正例になる確率をシグモイド関数で計算し,-100以下ならexp(-a)がオーバーフローする恐れがあるので,0と近似します.この確率値から実際のラベル (0または1)を減算することで,誤差gが計算されます.

collections.defaultdictは,辞書(マップ型)オブジェクトにキーが登録されていないとき,値をデフォルト値として自動的に登録してくれる便利なオブジェクトです.

W = collections.defaultdict(float)

に対して,W['hoge'] += 1.0とすると,'hoge'というキーがWに登録されていれば,その値を1.0増やし,'hoge'というキーがWに登録されていなければ,自動的にW['hoge'] = 0.と初期化し,さらにその値を1.0増やします.通常の辞書型では登録されていないキーにアクセスすると例外が発生するので,setdefaultというメソッドを使う必要があって,コードが読みづらくなります.defaultdictを使うと,スッキリしますね.

ロジスティック回帰について説明しませんでしたが,ざっくり言うと事例Xが正例もしくは負例に分類される確率が計算できることがウリの,二値分類器です.確率的勾配降下法は,名前こそ難しそうですが,素性の勾配を少量(このサンプルコードでは1個)の事例で近似して,降下させる方法です.こんな簡単な方法でも,news20データセットにおいて95.50%の精度(1/10のholdout評価)が出せます.libsvm(またエイプリルフールリリースしてますね)で線形SVMを回した場合の精度は,97%弱なので,こんなシンプルな方法でも善戦していることが分かります.

上のコードは,PRML本の第4章の輪読をするときに,数式を追うだけでは眠くなるので,書いてみました(そのときの輪読の資料全体のサンプルコード).分かりやすさ最優先なので,バイアス項,正則化,実数値素性,多クラスロジスティック回帰,複数回の反復,収束判定,学習率の自動調節など,いろいろなトピックをカバーしていませんが,機械学習の入門用,またPythonの学習の教材として,いろいろ遊べると思います.