前ステップまでに実装したコード
[1]:
import numpy as np
class Variable:
def __init__(self, data):
self.data = data
前ステップで、Variable
クラスは「箱」として使えるようになりました。しかし、今のままでは「ただの箱」です。その「ただの箱」を「魔法の箱」に変える仕掛けが必要になります。そのためのキーとなる存在が「関数」です。本ステップでは関数について考えます。
関数とは何でしょうか。関数とは、少し硬い言い回しをすれば、「ある変数から別の変数への対応関係を定めたもの」と言えます。具体例として、2乗の計算を行う\(f(x) = x^2\)という関数を考えます。その場合、\(y = f(x)\)とすれば、変数\(y\)と\(x\)の関係が関数\(f\)によって決まります。つまり、「\(y\)は\(x\)の2乗である」という関係が、関数\(f\)によって決定します。
このように、変数間の対応関係を定める役割が関数にはあります。このとき、変数と関数の関係を視覚的に表すと図2-1のようになります。
図2-1 変数と関数の関係を表したグラフ
図2-1によって、変数\(x\)と\(y\)、そして関数\(f\)の関係性が可視化されました。このように、○と□で表したノードと矢印によって計算を表した図を「計算グラフ」と言います。なお本書では、変数をオレンジの○で、関数を水色の□で図示することにします。
WARNING
「グラフ」と言うと棒グラフや円グラフなどの図を思い浮かべるかもしれません。しかし、コンピュータサイエンスの分野では、「グラフ」とはノードとエッジで構成されるデータ構造(および、その構造を表した図)を指します。
それでは、図1-3で表される関数をプログラミングの視点で考えてみましょう。具体的に言うと、変数\(x\)と\(y\)を先ほど実装したVariable
インスタンスだと仮定し、それらを処理できる関数\(f\)をFunction
クラスとして実装します。ここでの注意点は、次の2つです。
Function
クラスで実装するメソッドは、Variable
インスタンスを入力とし、Variable
インスタンスを出力とすること
Variable
インスタンスの実際のデータは、インスタンス変数のdata
に存在すること
この2点に注意すると、Function
クラスは次のように実装できます。
[2]:
class Function:
def __call__(self, input):
x = input.data # データを取り出す
y = x ** 2 # 実際の計算
output = Variable(y) # Variableとして返す
return output
上のように、ここでは__call__
メソッドを実装します。__call__
メソッドは、引数としてinput
を取りますが、これはVariable
インスタンスが与えられることを想定します。そのため実際のデータは、input.data
に存在します。データを取り出した後は、目的とする計算――ここでは、2乗の計算――を行い、その結果をVariable
の「箱」に入れて返します。
NOTE
__call__
メソッドは、Pythonの特殊なメソッドです。このメソッドを定義すれば、f = Function()
としたとき、f(…)
と書くことで__call__
メソッドを呼び出せます。
それでは、Function
クラスを実際に使ってみましょう。ここでは、Variable
インスタンスのx
をFunction
インスタンスのf
に入力してみます。
[3]:
x = Variable(np.array(10))
f = Function()
y = f(x)
print(type(y)) # type()を使って、オブジェクトの型を取得
print(y.data)
<class '__main__.Variable'>
100
上記のとおり、Variable
とFunction
を連携して使うことができました。実行結果を見ると、y
の型はVariable
で、そのデータはy.data
に格納されていることが分かります。
さて、ここで実装したFunction
クラスですが、これは「入力された値を2乗する」という具体的な関数です。そのため、Square
というような具体的な名前の方が適切です。また今後、様々な関数(Sin
関数やExp
関数など)を追加することになります。その点を考慮すると、Function
クラスは基底クラスとして実装し、DeZeroのすべての関数に共通する機能を持たせた方が良いでしょう。そこで、次の2点を満たすようにDeZeroの関数を作り変えます。
Function
クラスは基底クラスとして、すべての関数に共通する機能を実装する
具体的な関数は、Function
クラスを継承したクラスで実装する
以上の点を考慮すると、Function
クラスは次のように実装できます。
[4]:
class Function:
def __call__(self, input):
x = input.data
y = self.forward(x) # 具体的な計算はforwardメソッドで行う
output = Variable(y)
return output
def forward(self, x):
raise NotImplementedError()
ここでは、__call__
とforward
という2つのメソッドを実装します。__call__
メソッドでは、「Variable
からデータを取り出す」ことと「計算結果をVariable
に詰める」という2つの作業を行います。そして、その間の具体的な計算は、forward
メソッドを呼ぶことで行います。そのforward
メソッドの実装は、継承先のクラスで行うことにします。
NOTE
Functionクラス
のforward
メソッドでは、例外を発生させます。そうすることで、Function
クラスのforward
メソッドを使った人(使ってしまった人)に対して、そのメソッドは継承して実装すべきであることをアピールします。
それでは、このFunction
クラスを継承して、入力された値を2乗するクラスを実装してみます。ここでは、Square
というクラス名で次のように実装します。続いて、上のコードのx
に新しいデータを代入してみます。それには、次のように書くことができます。
[5]:
class Square(Function):
def forward(self, x):
return x ** 2
Square
クラスはFunction
クラスを継承しているので、__call__
メソッドはそのまま受け継がれます。そのため、具体的な計算をforward
メソッドに書くだけで、Square
クラスの実装は完成です。このSquare
クラスを使えば、次のようにVariable
を処理できます。
[6]:
x = Variable(np.array(10))
f = Square()
y = f(x)
print(type(y))
print(y.data)
<class '__main__.Variable'>
100
見てのとおり、前と同じ結果が得られました。以上で、ステップ2の作業は終了です。これで、Variable
とFunction
クラスの基礎は完成です!
NOTE
これからしばらくの間、Function
の入力と出力は「1つの変数」に限定します。「ステップ11」から、複数の変数に対応できるようにDeZeroを拡張していきます。