2014-05-05

バンディットアルゴリズムによる最適化手法 4章

職場で開催中の「多腕バンディットアルゴリズム勉強会」の復習として実装してみた。オライリーのバンディットアルゴリズムによる最適化手法を参考にしたが、本のサンプルコードがあまりにも読みづらいので、写経のつもりが大幅な書き直しになった。

In [20]:
%matplotlib inline
import matplotlib.pylab as plt
from numpy import zeros, array, average, random as nprandom
import random

テスト実行と結果プロット用の処理

In [21]:
def test_algorithm(algo, arms, num_sims, horizon):
    chosen_arms = zeros((num_sims, horizon), dtype=int)  # 試行毎に選んだ腕
    rewards = zeros((num_sims, horizon))  # 試行毎に得られた報酬
    cumulative_rewards = zeros((num_sims, horizon))  # 累積報酬

    for sim in range(num_sims):
        algo.initialize(len(arms))
        for t in range(horizon):
            chosen_arm = algo.select_arm()
            chosen_arms[sim, t] = chosen_arm
            
            reward = arms[chosen_arm].draw()
            rewards[sim, t] = reward
            
            algo.update(chosen_arm, reward)
        cumulative_rewards[sim] = rewards[sim].cumsum()
    return chosen_arms, rewards, cumulative_rewards
In [22]:
def plot_results(simulate_num, horizon, best_arm, results):
    fig1, axes1 = plt.subplots(nrows=1, ncols=3, figsize=(11, 5))
    x = range(horizon)
    plot1, plot2, plot3 = axes1[0], axes1[1], axes1[2]
    
    for result in test_results:
        accuracy = zeros(horizon)
        reward_ave = zeros(horizon)
        cumulative_rewards = zeros(horizon)

        epsilon = result[0]
        chosen_arms_mat = result[1]
        rewards_mat = result[2]
        cumulative_rewards_mat = result[3]

        for i in xrange(horizon):
            best_arm_selected_count = len(filter(lambda choice: choice == best_arm, chosen_arms_mat[:,i]))
            accuracy[i] = best_arm_selected_count / float(simulate_num)
            reward_ave[i] = average(rewards_mat[:,i])
            cumulative_rewards[i] = average(cumulative_rewards_mat[:,i])

        plot1.plot(x, accuracy, label='%10.2f' % epsilon)
        plot2.plot(x, reward_ave, label='%10.2f' % epsilon)
        plot3.plot(x, cumulative_rewards, label='%10.2f' % epsilon)

    plot1.legend(loc=4)
    plot1.set_xlabel('Time')
    plot1.set_ylabel('Probability of Selecting Best Arm')
    plot1.set_title('Accuracy of the \nEpsilon Greedy Algorithm')

    plot2.legend(loc=4)
    plot2.set_xlabel('Time')
    plot2.set_ylabel('Average Reward')
    plot2.set_title('Performance of the \nEpsilon Greedy Algorithm')

    plot3.legend(loc=4)
    plot3.set_xlabel('Time')
    plot3.set_ylabel('Cumulative Reward of Chosen Arm')
    plot3.set_title('Cumulative Reward of the \nEpsilon Greedy Algorithm')

Epsilon-Greedyアルゴリズムの実装

In [23]:
class EpsilonGreedy(object):
    def __init__(self, epsilon):
        self.epsilon = epsilon # 探索する度合い
        self.counts = None
        self.values = None
        
    def initialize(self, n_arms):
        # 腕を何回引いたか
        self.counts = zeros(n_arms, dtype=int)
        # 引いた腕の報酬の平均値
        self.values = zeros(n_arms)
    
    def select_arm(self):
        """ 次に選択する腕のインデックスを返す """
        if self.epsilon > random.random():
            # epsilonの確率で探索を行なう
            return random.randrange(len(self.values))
        else:
            # それ以外は活用を行なう
            return self.values.argmax()
    
    def update(self, chosen_arm, reward):
        # 腕を選んだ回数をインクリメント
        self.counts[chosen_arm] += 1
        n = self.counts[chosen_arm]
        
        # 腕の平均報酬額を更新
        value = self.values[chosen_arm]
        new_value = ((n-1)/float(n)) * value + (1/float(n)) * reward
        self.values[chosen_arm] = new_value

腕の実装

In [24]:
class BernoulliArm():
    """ ベルヌーイ分布に基づいて報酬を返す腕 """
    def __init__(self, p):
        self.p = p
        
    def draw(self):
        # 確率pで1.0を返す
        return 1.0 if self.p > random.random() else 0.0

実行

In [25]:
SIMULATE_NUM = 5000
HORIZON = 250

means = [0.1, 0.1, 0.1, 0.1, 0.9]
random.shuffle(means)
arms = map(lambda mu: BernoulliArm(mu), means)
best_arm = array(means).argmax()

test_results = []
for epsilon in [0.1, 0.2, 0.3, 0.4, 0.5]:
    algo = EpsilonGreedy(epsilon)
    chosen_arms_mat, rewards_mat, cumulative_rewards_mat = test_algorithm(algo, arms, SIMULATE_NUM, HORIZON)
    test_results.append([epsilon, chosen_arms_mat, rewards_mat, cumulative_rewards_mat])
plot_results(SIMULATE_NUM, HORIZON, best_arm, test_results)

練習問題

In [26]:
# 腕を200本にした場合
SIMULATE_NUM = 5000
HORIZON = 250

means = nprandom.rand(200)
random.shuffle(means)
arms = map(lambda mu: BernoulliArm(mu), means)
best_arm = array(means).argmax()

test_results = []

for epsilon in [0.1, 0.2, 0.3, 0.4, 0.5]:
    algo = EpsilonGreedy(epsilon)
    chosen_arms_mat, rewards_mat, cumulative_rewards_mat = test_algorithm(algo, arms, SIMULATE_NUM, HORIZON)
    test_results.append([epsilon, chosen_arms_mat, rewards_mat, cumulative_rewards_mat])
plot_results(SIMULATE_NUM, HORIZON, best_arm, test_results)
現段階ではまだ簡単な問題設定でしかない。これが腕が増減したり、同じ腕でも得られる報酬が変化するようなパターンだとどうなるのかが気になる。

このエントリーをはてなブックマークに追加

2014-04-30

IPython Notebookを2.0にアップデートした

IPython Notebookは1.x系をさくらVPS上で稼動させていたんだが、再起動の方法すら忘れてしまったのでこれを期に稼動場所移設と2.0.0にバージョンアップする事にした。以下気づいた事メモ

Macで動かし易くなった

Virtualbox上で稼動させようと思っていたが、試しにMacOSX上にセットアップしたらすんなり動いた。コマンドはbrew installとpip installだけで済む。ただ、virtualenv環境を使うと、brewで入れたsipやPyQTが使えないので一気に面倒な事に。matplotlibのbackendも変更が必要になったり、ハマり所が増える。pyenvやvirtualenvを使わずにbrewで入れたPython2.7の環境に全部入れてしまうと楽。

--pylab=inlineが非推奨に

以前からやりすぎ感はあったので同意。今後はそれぞれimportして使うようにした。
import matplotlib.pyplot as plt
import numpy as np
matplotlibの描画結果をインラインに展開するには
%matplotlib inline
する

.ipynbファイルを置くだけで認識される様になった

importする手間が無くなった!! 次のディレクトリ認識との合せ技で、人の書いたnotebookの利用が非常に簡単になった。

セキュリティモデルの追加

コード実行による影響を抑えるために他人が作成したセルはnotebookを開いた時に勝手に実行されなくなった。詳しくはドキュメント参照。

ディレクトリを認識する様になった

notebook群を管理しやすくなった。プロジェクト毎にnotebookを管理するgitリポジトリを分けたり。git submoduleでscientific-python-lecturesを取りこんだり。よく参照するリポジトリを取りこんだら、ルートは以下の様になった。

nbconverterがIPython本体に取りこまれた

ブログ貼り付け用のHTMLを作る場合は
ipython nbconvert --to=html --template=basic notebook_root/hoge.ipynb
でいける。

まとめ

環境がいっそう作りやすくなった。自分用のセットアップスクリプトをまとめてgithubに上げたので、使い方を忘れるという事態はこれで避けられるはず。




このエントリーをはてなブックマークに追加

2014-03-15

エンジニアの採用面接におけるコーディングテストとフィボナッチ数列

ソフトウェア開発者採用面接のコーディングテストについて。最低限のコーディング能力があるかどうかを見極めるのに、フィボナッチ数を求める関数が出題される事がある。というか自分も出題した事がある。

ホワイトボードに書く形式だと、どうしても行数の少ない実装になってしまうが。手元の端末で自由に書いて良いとなれば、いろいろな解法が出てきて、想定していないコミュニケーションが生まれるかもしれない。最近は専ら採用する側になってしまったのだが、とりあえず自分ならどう書くかなと列挙してみた。使った環境はIPython Notebookである。

定義

  • f(0) = 0
  • f(1) = 1
  • f(n) = f(n-1) + f(n-2)

1. 再帰を使うもの

ナイーブな実装。見たまんまで理解しやすい。まあ計算量がアレだよねーという奴

In [53]:
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

map(fib, range(15))
Out[53]:
$$\begin{bmatrix}0, & 1, & 1, & 2, & 3, & 5, & 8, & 13, & 21, & 34, & 55, & 89, & 144, & 233, & 377\end{bmatrix}$$

2. forループ

計算オーダーがO(n)になった。自分が書くなら上を書いてから、追記でこちらも書くかな。

In [54]:
def fib(n):
    a, b = 0, 1
    for i in xrange(n):
        a, b = b, a + b
    return a

map(fib, range(15))
Out[54]:
$$\begin{bmatrix}0, & 1, & 1, & 2, & 3, & 5, & 8, & 13, & 21, & 34, & 55, & 89, & 144, & 233, & 377\end{bmatrix}$$

3. 行列と内積を使う

numpy使いはこう解くかもしれない。

In [55]:
from numpy import array

def fib(n):
    A = array([[0,1],[1,1]])
    tmp = array([0,1])
    for i in xrange(n):
        tmp = tmp.dot(A)
    return tmp[0]

map(fib, range(15))
Out[55]:
$$\begin{bmatrix}0, & 1, & 1, & 2, & 3, & 5, & 8, & 13, & 21, & 34, & 55, & 89, & 144, & 233, & 377\end{bmatrix}$$

4. 逐次平方して計算量O(log2n)で解く

SICPでやった奴だ!! だが、面接の場でこのコードを捻り出せる自信は無い。
速く動作する実装を求められたら10分ぐらい時間をもらって導き出すかも。

In [56]:
def fib(n):
    a, b = 1, 0
    p, q = 0, 1

    rest = n
    while rest > 0:
        if rest % 2 == 0:
            p, q = p**2 + q**2, q**2 + 2*p*q
            rest /= 2
        else:
            a, b = b*q + a*q + a*p, a*q + b*p
            rest -= 1
    return b

map(fib, range(15))
Out[56]:
$$\begin{bmatrix}0, & 1, & 1, & 2, & 3, & 5, & 8, & 13, & 21, & 34, & 55, & 89, & 144, & 233, & 377\end{bmatrix}$$

5. 一般項を使って解く

一般項を記憶している場合のみ可能とも言える。ただし、計算機に優しくない見た目の通り、fib(2000)とやるとOverflowErrorになってしまう。
プロダクション環境に突っ込むとしたらこのコード使いますか?? みたいな話になりそう。

In [57]:
def fib(n):
    return int(1/5**0.5*(((1+5**0.5)/2)**n - ((1-5**0.5)/2)**n))

map(fib, range(15))
Out[57]:
$$\begin{bmatrix}0, & 1, & 1, & 2, & 3, & 5, & 8, & 13, & 21, & 34, & 55, & 89, & 144, & 233, & 377\end{bmatrix}$$

6. 方程式 f(n) - f(n-1) - f(n-2) = 0 をf(n)について解く

面接で緊張して頭が真っ白になってしまった場合は、数学的な宣言をそのまま利用すれば良い

In [102]:
%load_ext sympy.interactive.ipythonprinting
import sympy as sym
from sympy.abc import n
In [127]:
# 関数fibを定義
fib = sym.Function('fib(n)')

# fib(n) = fib(n-1) + fib(n-2) を変形して
# fib(n) - fib(n-1) - fib(n-2) = 0 の方程式とする
f = fib(n) - fib(n-1) - fib(n-2)

# fib(0) -> 0, fib(1) -> 1 である事を利用して fib(n) について解く
fib_term = sym.rsolve(f, fib(n), {fib(0):0, fib(1):1})
sym.Eq(fib, fib_term)
Out[127]:
$$fib(n) = \frac{1}{5} \sqrt{5} \left(\frac{1}{2} + \frac{1}{2} \sqrt{5}\right)^{n} - \frac{1}{5} \sqrt{5} \left(- \frac{1}{2} \sqrt{5} + \frac{1}{2}\right)^{n}$$

解けた。5.と同じfib(n)の一般項が導出できたので、あとは実行するだけ。

In [125]:
map((lambda i: int(fib_term.evalf(subs={n:i}))), range(15))
Out[125]:
$$\begin{bmatrix}0, & 1, & 1, & 2, & 3, & 5, & 8, & 13, & 21, & 34, & 55, & 89, & 144, & 233, & 377\end{bmatrix}$$

最後のは極端だが、方程式を解くという行為を数値計算ライブラリに丸投げすると、プログラミングの特徴である平叙文的な記述を命令文的な記述に変換するという行為がどこにも無いという事態になる。メモ化すると計算量がうんたらとか、末尾最適化がほげほげというコミュニケーションが取りたい場合は、そういった誘導が必要だろう。

最近は面接の前にその人のコードが読める事も増えてきて、最低限の力を見極めるのならそれが一番楽なんだけど、せっかくコーディングテストやるなら解法がいろいろあってコミュニケーションの選択肢が多くなるようにしたい。エラー処理の有無、読み易さ、メモリ使用量、入力値の想定 etc... 。手書きよりも解答の幅が広がりそうな端末持ち込みライブコーディング面接、一度やってみたい。

計算機プログラムの構造と解釈計算機プログラムの構造と解釈
ジェラルド・ジェイ サスマン,ジュリー サスマン,ハロルド エイブルソン,Gerald Jay Sussman,Julie Sussman,Harold Abelson,和田 英一

ピアソンエデュケーション
売り上げランキング : 100220

Amazonで詳しく見る by AZlink

このエントリーをはてなブックマークに追加