授業の課題をPythonで実装してみた

授業の課題で

Q \in \mathbb{R}^{n \times n}が対称正定値行列でq \in \mathbb{R}^{n}の時、


minimize \frac{1}{2}x^{T}Qx + q^{T}x
subject to x \in R^{n}


最急降下法、加速勾配法、共役勾配法のプログラムを実装して解くという課題が出ました。対称正定値行列Qとベクトルqはランダムに作ります。
対称正定値行列(固有値全てが正である行列)を作るのがなかなか面倒で、特殊な方法を使ったりすると一応作れるのですが、実際に固有値を求め、全ての固有値が正であることをチェックするし、条件を満たさなければ行列を作り直すという仕様にしました。

始めは近頃使っているRubyで実装しようと思いましたが、Rubyにはどうもデフォルトで固有値計算のライブラリがないようだったので、Pythonで実装することに。ちなみに研究室の同期の人たちはRやMATLABで実装していました。

まずは環境作り。IDERubyの時にインストールしたaptana studioにPyDevというEclipseプラグインが入っていたのでそれをそのまま使用。僕のMac LeopardPythonはバージョン2.5.1でちょっと古かったので最新版Pythonをインストールしてみましたが、固有値計算をするライブラリであるNumpyがビルド出来なかったので、2.5.1を使うことに。

Python入門の本とネットの力を借りつつ作成。以下がコードです。

experiment = False #True when experiment
sumTime = 0
sumIter = 0
sumF = 0

from numpy import *
import time
#import random

def makeVector(n):
    '''make vector (size n)'''
    v = zeros(n)
    for i in range(n):
        #v[i] = random.random();
        v[i] = random.randint(0,100);
    return v

def makeSPDMatrix(n, eps):
    '''make symmetric positive definite matrix'''
    a = identity(n);
    while True:
        for i in range(n):
            for j in range(i,n):
                a[i,j] = a[j,i] = random.randint(0,100);
                
        a = dot(a,a)
    
        #l : engenvalue, v : eigenvector
        (l,v) = linalg.eig( a );
        #print "eigenvalue = ", l
        bOK = True;
        for i in range(n):
            if((0 - eps < l[i] and l[i] < 0 + eps) or l[i] < 0):
                bOK = False;
        if bOK:
            break
    return a

def doCGM(Q,q,init,eps):
    '''Conjugate gradient method'''
    if(experiment == False): print "----- Conjugate Gradient Method -----"
    x = init
    p = -gradf(Q,q,x)
    k = 0
    sttime = time.clock()
    while True:
        a = dot(-gradf(Q,q,x),p) / dot(dot(p,Q), p) #exact line search
        x1 = x + a*p
        #print "x = ", x, ". x1 = ", x1
        if(diffVector(x,x1) < eps): break
        gf1 = gradf(Q, q, x1)
        gf = gradf(Q, q, x)
        #beta = dot(gf1, gf1) / dot(gf, gf) #Fletcher-Reeves
        beta = dot(gf1, gf1 - gf) / dot(p, gf1 - gf) #Hestenes-Stiefel
        #print beta
        p = -gf1 + beta * p
        x = x1
        k += 1
    entime = time.clock()
    if(experiment):
        global sumTime,sumIter, sumF
        sumTime += entime - sttime
        sumIter += k
        sumF += f(Q, q, x)
    else:
        print "  iteration = ", k, ", time = ", entime - sttime, ", x = ", x, ", f(x) = ", f(Q, q, x)

def doAGM(Q, q, init, eps):
    '''accelerated gradient method'''
    if(experiment == False): print "----- Accelerated Gradient Method -----"
    y = init
    k = 0
    (l,v) = linalg.eig( Q );
    L = max(l)
    #print "l = ", l, ", L = ", L
    ssum = zeros(q.size)
    sttime = time.clock()
    while True:
        gf = gradf(Q,q,y)
        x = y - gf / L
        ssum += gf
        z = init - ssum / L
        y1 = (2.0/(k+3))*z + ((k+1)*1.0/(k+3))*x
        #print "y = ", y, "y1 = ", y1
        if(diffVector(y,y1) < eps): break
        y = y1
        k += 1
    entime = time.clock()
    if(experiment):
        global sumTime,sumIter, sumF
        sumTime += entime - sttime
        sumIter += k
        sumF += f(Q, q, x)
    else:
        print "  iteration = ", k, ", time = ", entime - sttime, ", x = ", x, ", f(x) = ", f(Q, q, x)

def doSDM(Q, q, init, eps):
    '''Steepest descent method
    init: initial value, eps: tolerance'''
    if(experiment == False): print "----- Steepest Descent Method -----"
    x = init
    k = 0
    sttime = time.clock()
    while True:
        d = -gradf(Q,q,x)
        a = dot(d,d) / dot(dot(d,Q), d) #exact line search
        x1 = x + a*d
        if(diffVector(x,x1) < eps): break
        #print "doSDM : k = ", k, "x = ",x, "x1 = ", x1, "a = ", a, "d = ", d
        x = x1
        k += 1
    entime = time.clock()
    if(experiment):
        global sumTime,sumIter, sumF
        sumTime += entime - sttime
        sumIter += k
        sumF += f(Q, q, x)
    else:
        print "  iteration = ", k, ", time = ", entime - sttime, ", x = ", x, ", f(x) = ", f(Q, q, x)
        
def diffVector(v1, v2):
    '''calculate Euclid norm'''
    return sqrt(sum([x*x for x in v1 - v2]))

def f(Q,q,x):
    '''objective function'''
    return dot(dot(x, Q), x) / 2 + dot(q, x)

def gradf(Q,q,x):
    '''gradient of f'''
    return dot(Q, x) + q

def exp(n, Q, q, init, eps):
    '''funtion for experiment
        n : loop iteration'''
    global experiment
    experiment = True
    ary = [doSDM, doAGM, doCGM]
    for i in range(len(ary)):
        global sumTime, sumIter, sumF
        sumTime = 0
        sumIter = 0
        sumF = 0
        print ary[i], "start."
        for j in range(n):
            ary[i](Q, q, init, eps)
        print "  average time = ", sumTime / n, ", average iteration = ", sumIter / n
    
    experiment = False

def doIt(n):
    eps = 1.0e-9;
    #n = 10 #size of matrix, vector
    it = 1 #num of iteration when experiment
    bExperiment = True #True if experient
    print "make start."
    Q = makeSPDMatrix(n, eps)
    print "made."
    q = makeVector(n)
    init = makeVector(n)
    print "n = ", n
    print "Q = %s" % Q
    print "q = %s" % q
    print "init = %s" % init
    invQ = linalg.inv(Q)
    trueX = -dot(invQ, q)
    ans = -(dot(dot(q, invQ), q)) / 2
    print "trueX = ", trueX, "true ans = %s" % ans
    if(bExperiment):
        exp(it, Q, q, init, eps)
    else:
        doSDM(Q, q, init, eps)
        doAGM(Q, q, init, eps)
        doCGM(Q, q, init, eps)

def doIt2():
    '''exec doIt func 5 times.'''
    print "doIt2 start."
    num = [10]
    for i in range(len(num)):
        for j in range(1):
            doIt(num[i])
    print "doIt2 end."

if __name__ == '__main__':
    doIt2()

Pythonでの実装の際にいくつか気になった点を書いておきます。

・コメントについて
1行コメントは#、複数行コメントは「'''」で囲みます。


グローバル変数について
普通に変数名を書けばOK。関数などからの参照は普通に変数名で出来るが、グローバル変数への書き込みは出来ない。グローバル変数に値を書き込むときには

global g_Valuable
g_Valuable = 2

のようにグローバル変数の使用を宣言してから書き込む。


・ライブラリのインポートについて
ライブラリをインポートして関数を使う際に、

import (ライブラリ名) #import
(ライブラリ名).(関数名)#use function

で使う方法と、

from (ライブラリ名) import (関数名) #import
(関数名)#use function

で使う方法があります。下の方法では関数名に*を用いるとライブラリの全ての関数をインポートできます。ただ、*を使ったインポートはあまり推奨されていないようで、小さいプログラム以外では避けた方がいいようです。


・関数について
定義は以下のように書きます。

def (関数名)(引数1, 引数2,...):
(関数本体)
return (戻り値)

関数の始めに簡単な関数のコメントを書くのが推奨されているみたいです。複数の値を返せるとか・・・
また、関数型言語のように関数を配列に入れたり、関数の引数や戻り値に出来たりします。これを高階関数というらしいです。便利です。


・ブロックについて
上の関数定義やif文などのブロックの始まりでは:を付ける。if文やfor文でよく付け忘れて怒られましたorz
また、pythonはブロックの範囲をインデントで判断します。インデントをずらすとまずいことに。


・構文糖衣について
複数の代入や、数値を簡単に取り扱えます。

eps = 1.0e-9; #eps = 10の-9乗
(a, b) = [1,2] #a = 1, b = 2


僕の印象ですが、pythonシンタックス、能力とRubyのような使い心地でした。ある人に言わせればPythonRubyは哲学が全然違うらしいですが・・・(RubyTIMTOWTDI(there's more than one way to do it - あることをするのにいくつものやり方がある)、Pythonは対照的に言語自身の機能をなるべく抑えている)。
前にRuby使いの人が「Pythonスクリプト言語の癖に速い」と言っていたので、スクリプト言語の中ではコンテスト向けかもしれません。