Theano Tutorial 2 - Derivatives, Conditions and Loop

Feb 19, 2017   #Python  #Machine Learning 

はじめに

 前回のTheano Tutorial 1 - Variables and Functionでは、Theano変数と関数のチュートリアルについてまとめた。今回はその続きとして、自動微分、条件分岐、繰り返し処理のチュートリアルについてまとめた。
 特に繰り返し処理scanは、振る舞いが複雑なので、詳細はドキュメントを参照されたい1

自動微分(Derivatives)

 theanoは、theano関数を自動微分することができる。以下にシンプルな一次関数とロジスティック関数の自動微分の例を示す。theano関数の微分はgradにより計算可能で、第一引数に指定したtheano関数の第二引数に指定したtheanoのシンボル変数に関する微分を計算できる。

import numpy as np
import theano
import theano.tensor as T

x = T.dscalar('x')
y = x ** 2 + 3 * x
gy = T.grad(y, x)
f = theano.function([x], gy)
assert f(4) == 11 # gy = 2x + 3

x = T.dmatrix('x')
s = T.sum(1 / (1 + T.exp(-x)))
gs = T.grad(s, x)
dlogistic = theano.function([x], gs)
assert np.allclose(dlogistic([[0, 1], [-1, -2]]),
                   [[ 0.25      ,  0.19661193],
                    [ 0.19661193,  0.10499359]])

 このように、定義したtheano関数の微分は平易に計算できる。なので、条件分岐や繰り返し処理などを含む処理をどのようにtheano関数として定義するかが問題である。

条件分岐(Conditions)

 theanoにおける条件分岐には、以下の二つがある。

  • theano.switch
  • theano.ifelse

 APIの違いは多少あるが、いずれも第一引数に条件、第二引数に条件が真出会った場合のシンボル変数、第三引数に条件が偽であった場合のシンボル変数を指定する。
 これらの違いは、ifelseでは引数を遅延評価することができ、条件を満たす側のみを評価することができる。これによりswitchよりも高いパフォーマンスを得られる場合がある。ifelseswitchの特殊な形式である。
 また、theanoには基本的なシンボル変数の比較関数が準備されている2
 以下の例では、switchと遅延評価でないifelse、遅延評価したifelseの処理時間を比較している。遅延評価するためにはtheano.Modelinker名前付き引数にvmまたはcvmを指定する必要がある。
 尚、theano.Modeには様々なものがあるため、詳細はドキュメントを参照されたい3

import numpy as np
import theano
import theano.tensor as T
from theano.ifelse import ifelse
import timeit

a,b = T.scalars('a', 'b')
x,y = T.matrices('x', 'y')

z_switch = T.switch(T.lt(a, b), T.mean(x), T.mean(y))
z_lazy = ifelse(T.lt(a, b), T.mean(x), T.mean(y))

f_switch = theano.function([a, b, x, y], z_switch,
                               mode=theano.Mode(linker='vm'))
f_ifelse = theano.function([a, b, x, y], z_lazy,
                               mode=theano.Mode(linker='c|py'))
f_lazyifelse = theano.function([a, b, x, y], z_lazy,
                               mode=theano.Mode(linker='vm'))
iter_n = 100
mat1 = np.ones((10000, 1000))
mat2 = np.ones((10000, 1000))

print(timeit.timeit(
        stmt='f_switch(0, 1, mat1, mat2)',
        setup='from __main__ import f_switch, mat1, mat2',
        number=iter_n) / iter_n)
# 0.021718298419727944

print(timeit.timeit(
        stmt='f_ifelse(0, 1, mat1, mat2)',
        setup='from __main__ import f_ifelse, mat1, mat2',
        number=iter_n) / iter_n)
# 0.021513296669581905

print(timeit.timeit(
        stmt='f_lazyifelse(0, 1, mat1, mat2)',
        setup='from __main__ import f_lazyifelse, mat1, mat2',
        number=iter_n) / iter_n)
# 0.011418448360054755

 コメントに記載したように、遅延評価した場合は、約半分の時間で計算できている。

この数値は、OS X El Capitan, 2.6GHz Intel Core i5上で実行したものである。

繰り返し処理(Loop)

 theanoでは、繰り返し処理をtheano.scan関数により表現できる。これにより繰り返し処理を使ったtheano関数の自動微分が可能となる。scanには様々な引数を指定することができ、それにより振る舞いが大きく変化する。scanの詳細と幾つかの例は公式ドキュメントにある45

例1

 まず、初めの例では、A**kscanを用いて計算する。
 引数fnには関数やlambdaを指定する。指定された関数には、scanに指定した引数に応じて様々な引数が渡される。fnの返り値はoutputsupdatesのtupleで、前者は各ステップのfnの結果のlistで、後者は共有変数の更新式を表すdictのサブクラスの値である(この例ではNoneが返る)。
 outputs_info引数にはシンボル変数かシンボル変数のlist、辞書を指定でき、この例ではシンボル変数を指定している。outputs_infoにシンボル変数を指定した場合、これは初期値を意味すると同時に、繰り返し処理の各ステップで直前のfnの結果を、現在のfnの引数として渡すことを意味する。
 scanには、sequencesnon_sequencesという名前の引数があり、前者は各ステップで順次渡されるシンボル変数を(後述)、後者は各ステップで共通して渡されるシンボル変数を指定する。
 n_stepsは繰り返し回数を指定する。

import theano
import theano.tensor as T
import numpy as np

k = T.iscalar("k")
A = T.vector("A")

# Aと同じshapeのベクトルを1で初期化し初期値とする。
# このベクトルはprior_resultとして、各ステップのfnに渡される。
# resultは、各ステップでA倍されたベクトルのlistである。
result, _ = theano.scan(fn=lambda prior_result, A: prior_result * A,
                        outputs_info=T.ones_like(A),
                        non_sequences=A,
                        n_steps=k)

# 最終ステップのoutputだけを取得する。
final_result = result[-1]
power = theano.function(inputs=[A,k], outputs=final_result)

assert (power(range(10), 2) == [n ** 2 for n in range(10)]).all()
assert (power(range(10), 4) == [n ** 4 for n in range(10)]).all()

例2

 この例では、スカラ変数と係数のlistから多項式の解を計算する。
 scanには先ほどとは異なりsequencesを指定していしている。sequencesでは繰り返し可能なシンボル変数かシンボル変数のlistまたは辞書を指定できる。シンボル変数のlistを指定した場合、組み込みのzip関数と同様に最小長の長さに合わせられる。
 outputs_infoNoneを指定した場合、初期値を持たず、fnには直前の結果が渡されない。

import theano
import theano.tensor as T
import numpy as np

coefficients = theano.tensor.vector("coefficients")
x = T.scalar("x")

def term_in_polynominal(coefficient, power, free_variable):
    return coefficient * (free_variable ** power)

# 乗数のlistを作成
max_coefficients_supported = 10000
powers = theano.tensor.arange(max_coefficients_supported)

# 係数のlistと乗数のlistのlistがsequencesに指定されている。
# また、non_sequencesには各ステップに共通の値としてシンボル変数xが指定されている。
# よって各ステップでは、i番目の係数と乗数、固定値xがfnに渡される。
components, _ = theano.scan(fn=term_in_polynominal,
                            outputs_info=None,
                            sequences=[coefficients, powers],
                            non_sequences=x)

polynomial = components.sum()
calculate_polynomial = theano.function(inputs=[coefficients, x], outputs=polynomial)

test_coefficients = np.asarray([1, 0, 2], dtype=np.float32)
assert calculate_polynomial(test_coefficients, 3) ==\
       (1.0 * (3 ** 0) + 0.0 * (3 ** 1) + 2.0 * (3 ** 2))

例3

 この例では、共有変数を利用してカウンタ関数を定義している。
 fnには、更新式を表す辞書を返す関数を指定できる。この場合、updatesにはdictのサブクラス型として更新値が返され、tupleの第一引数outputsNoneである。

import theano

a = theano.shared(1)
_, updates = theano.scan(fn=lambda: {a: a+1}, n_steps=10)
b = updates[a] + 1
f = theano.function([], [a, b], updates=updates)

assert a.get_value() == 1
assert f() == [[1], [12]]
assert a.get_value() == 11

例4

 この例では、条件付き終了を含む繰り返し処理を定義する。  条件付き終了は、theano.scan_module.untilで表現できる。

import theano
import theano.tensor as T

# fn引数に、返り値とtheano.scan_module.untilを返すことで、条件付き終了を指定できる。
def power_of_2(previous_power, max_value):
    return previous_power*2, theano.scan_module.until(previous_power*2 > max_value)

max_value = T.scalar()
values, _ = theano.scan(fn=power_of_2,
                        outputs_info = T.constant(1.),
                        non_sequences = max_value,
                        n_steps = 1024)
f = theano.function([max_value], values)
assert (f(45) == [2, 4, 8, 16, 32, 64]).all()

例5

 最後の例は、公式ドキュメントからの抜粋で、outputs_infoに辞書を指定して、i個手前の結果を使用する関数を定義する例を示す。
 これまでの例では、outputs_infoにシンボル変数を指定することで初期値を指定し、直前のfnの結果をfnの引数として渡せることを示した。outputs_infoに辞書を指定した場合、これを明示的に指定できる。辞書には、initialtapsキーを設定し、初期値とどのオフセットのfnの結果をfnの引数として渡すかを指定できる

import theano
import theano.tensor as T
import numpy as np

X = T.matrix("X")
W = T.matrix("W")
b_sym = T.vector("b_sym")
U = T.matrix("U")
V = T.matrix("V")
n_sym = T.iscalar("n_sym")

# 初期値としてXを指定し、各ステップのfnの引数として、直前(-1)の結果と二つ前(-2)の結果が渡される。
results, updates = theano.scan(
    fn=lambda x_tm2, x_tm1: T.dot(x_tm2, U) + T.dot(x_tm1, V) + T.tanh(T.dot(x_tm1, W) + b_sym),
    n_steps=n_sym,
    outputs_info=[dict(initial=X, taps=[-2, -1])])

compute_seq2 = theano.function(inputs=[X, U, V, W, b_sym, n_sym], outputs=results)

# -2, -1を指定したため、長さ2の初期値が必要
x = np.zeros((2, 2), dtype=theano.config.floatX)
x[1, 1] = 1
w = 0.5 * np.ones((2, 2), dtype=theano.config.floatX)
u = 0.5 * (np.ones((2, 2), dtype=theano.config.floatX) - np.eye(2, dtype=theano.config.floatX))
v = 0.5 * np.ones((2, 2), dtype=theano.config.floatX)
n = 10
b = np.ones((2), dtype=theano.config.floatX)
result_seq2 = compute_seq2(x, u, v, w, b, n)

# numpyで記述した例
x_res = np.zeros((10, 2))
x_res[0] = x[0].dot(u) + x[1].dot(v) + np.tanh(x[1].dot(w) + b)
x_res[1] = x[1].dot(u) + x_res[0].dot(v) + np.tanh(x_res[0].dot(w) + b)
x_res[2] = x_res[0].dot(u) + x_res[1].dot(v) + np.tanh(x_res[1].dot(w) + b)
for i in range(2, 10):
    x_res[i] = (x_res[i - 2].dot(u) + x_res[i - 1].dot(v) +
                np.tanh(x_res[i - 1].dot(w) + b))

assert (result_seq2 == x_res).all()

おわりに

  前回のTheano Tutorial 1 - Variables and Functionと今回のTheano Tutorial 2 - Derivatives, Conditions, Loopを通して、theanoの基本的な使い方を解説した。Advancedなチュートリアルには、疎行列や(マルチ)GPUにおける使い方があるので、気が向いたらまとめてみる。

参考

関連記事