製品・サービスAIフレームワーク
“ufiesia”“pyaino”
学び、応用できる
2つのAI環境を構築
AIは日々進化し、私たちの生活や仕事に欠かせない存在になりつつあります。
でも、「どう動いているのか分からないまま」使っていませんか?
私は、その仕組みを理解しながら使えるAI環境を作りたいと考えました。
基礎の仕組みをシンプルに学べるufiesiaと、実践的な開発に使えるpyaino。
学びから応用までーAIを「ブラックボックス」から「透明なツール」へ変える2つのフレームワークです。(井上愛一郎)
ufiesia(ウフィーシア)とは
Ufiesiaはニューラルネットワークやディープラーニングの基本構造を理解するために用意したフレームワークです。
ソースコードを見て理解の助けとなるように、内部の記述も出来るだけ平易にしています。
同時に公開した「京」の扉に沿って学習していけば基本的な仕組みの理解が出来るものと期待します。
GitHubより、ufiesia(ウフィーシア)を
ダウンロードいただけます。
pyaino(ピアイノ)とは
pyainoは、NumPyをそのまま活用しながら実践的なAI開発を可能にするディープラーニング・フレームワークです。
微分計算はdefine-by-run方式で提供され、単純な関数の微分から、ディープラーニングの複雑な勾配計算まで正確に実現します。たとえば、1次関数の二階微分が正しくゼロになるように、高階微分も数学的に正確に計算できます。また、行列積の扱いも、数学と同じ規約で行われ、数値計算や数学の素養を持つ方にとって混乱がありません。
画像処理や言語処理に必要なモジュールも備えており、わずか数行の記述で機械学習やディープラーニングを体験できます。加えて、各モジュールは順伝播と逆伝播を明示的に記述しているため、不要な計算グラフを生成せず、計算資源を効率的に利用でき、検証時の動作確認も容易です。
GitHubより、pyaino(ピアイノ)を
ダウンロードいただけます。
AIフレームワーク 開発ブログ 過去の日誌を見る
「何をやってるんか、わからへんかった。」
畳込み
そら、わからへんわなあ...
先日、畳込みと逆畳み込みに関連してImg2colとCol2imが逆の関係という話をブログに書きました。その話を某T君としていたら、畳込みのソースコードを見ても「何をやってるんか、わからへんかった。」と言っていました。「じゃあ、今はわかるやろ。」とは言わなかったのですが、確かに畳込みと逆畳み込みは、Img2colとCol2imの変換を伴うために、やりたいことと、やってることとの照合がとりずらく、ソースコードが読みやすいようにということを考えて作ったufiesiaでも、そしてましてpyainoでは、わかりにくい作りになっています。ということで、説明をちゃんと書かなきゃならないかなあ、とも思ったりしたのですが、いや待てよ、僕自身ちゃんと分かっているんだっけと疑わしく思ったので、性能は度外視してでも、そのやりたいことと、やってることが、照合しやすいように改めてコードを作ってみました。そして、ufiesia0のNeuronにPremitiveConvLayerクラスとして加え、現役のufiesia0およびpyainoの畳込みと同じ入力で同じ結果が得られること、さらに逆伝播も行って現役版と同様に学習することも検証しました。それで一安心なのですが、せっかくそこまでやったので、この「やりたいことと、やってることが、照合しやすいコード」を眺めてみることにします。ここではこのクラス全体は長いので、このクラスのforwardメソッドだけを添付します。
### 畳み込み層 #####################################################
class PremitiveConvLayer(BaseLayer):
""" 畳み込み層(動作原理に忠実な基本版) """
# B:バッチサイズ, C:入力チャンネル数, Ih:入力画像高, Iw:入力画像幅
# M:フィルタ数, Fh:フィルタ高, Fw:フィルタ幅
# Sh:ストライド高,Sw:ストライド幅, pad:パディング幅
# 出力チャンネル数=フィルタ数M, Oh:出力高, Ow:出力幅
def forward(self, x):
if None in self.config:
self.fix_configuration(x.shape)
C, Ih, Iw, M, Fh, Fw, Sh, Sw, pad, Oh, Ow = self.config
if self.w is None:
self.init_parameter(C*Fh*Fw, M)
B = x.size // (C*Ih*Iw) # B = x.shape[0] = len(x)
# 画像調整 (C,Ih*Iw)にも対応 B C Ih上Ih下 Iw左Iw右 ゼロパディング
self.x = np.pad(x.reshape(B,C,Ih,Iw), [(0,0),(0,0),(pad,pad),(pad,pad)], 'constant')
u = np.zeros((B,Oh,Ow,M), dtype=Config.dtype)
for ih in range(0, Ih-Fh+2*pad+1, Sh): # ih+FhがIhからはみ出さないように
for iw in range(0, Iw-Fw+2*pad+1, Sw): # iw+FwがIwからはみ出さないように
xij = self.x[:,:,ih:ih+Fh, iw:iw+Fw] # xのFh*Fwの領域を取出す
xij = xij.reshape(B, -1)
uij = np.dot(xij, self.w) + self.b # 取出した領域を共通のwとbでaffine変換
u[:,ih//Sh,iw//Sw,:] = uij # uの該当箇所に値を設定
u = u.transpose(0,3,1,2)
y = self.activator.forward(u)
return y
鍵となるのは、二重のforループの部分です。
for ih in range(0, Ih-Fh+2*pad+1, Sh): と、for iw in range(0, Iw-Fw+2*pad+1, Sw):
で二重ループを構成しますが、これは2次元の画像の縦と横に対応します。
ちなみに、forwardメソッドの入力となる画像は、縦Ih横Iwの2次元のほかに、データをまとめて処理するためのバッチB、チャネルC―例えばチャネルはカラー画像の光の3原色で3チャネル―で、(B,C,Ih,Iw)の形状の4次元のデータとなっています。
ともかくも、その画像の縦横をループの、0からはじめて順繰りに増分が加えられる縦横のインデクスih,iwを用いて、
xij = self.x[:,:,ih:ih+Fh, iw:iw+Fw]
でもって縦Fh横Fwの四角い領域を切り出します。この領域はB,C軸も含めると、(B,C,Fh,Fw)という形状になります。
そしてそれを、
xij = xij.reshape(B, -1)
によって(B, C*Fh*Fw)の2次元の行列にしてから、
uij = np.dot(xij, self.w) + self.b
でaffine変換します。これはつまり、画像から切り出した四角い領域と、重みself.wの縦に並ぶベクトルとの内積をとって、バイアスを足していることになります。そしてその内積をとるというのは、両者の類似性を見ていることと関連します。そして、それを重みself.wの横方向の数Mの分だけ、つまり、M通りの値を求めています。そしてソースコードをちょっと戻ると、
self.init_parameter(C*Fh*Fw, M)
というのがありますが、まさにこの操作に対応して、重みself.wは、縦C*Fh*Fw、横Mで初期化しているのです。ここでCについてはチャネルだと述べましたが、xijを切り出す際に、Cの軸はBの軸と併せて:,:,で元のまま全部を切り出しています。だから、reshape(B, -1)とすると、Fh,Fwと一緒にされています。「画像から切り出した四角い領域」と先に述べたのは、正確には「チャネルごとに画像から切り出した四角い領域をチャネル分並べたもの」なのです。
さて、いささか本題からそれてしまったので戻します。
画像から切り出した四角い領域というのは、ループでih,iwにより位置を変えながら、内側のループで横方向に、外側のループで縦方向に、動かしていることになり、そうやって、画像全体の特徴を捉えていくことになるのです。

たとえば、ここに表示した手書き数字0の縦横8×8の白黒画像ですが、この場合の画像の形状は(1, 1, 8, 8)となります。
これに対し、Fh=Fw=2とすると、図に田の字で示す2×2の枠を左上から右方向にずらしていき、端まで行ったら、下にずらして、また左端から右端まで動かしていくのです。
左上端に田の字の枠があるときは、枠の中の4つの升はすべて白(=0)ですが、

右に一つずれると、

順に白、灰色、白、濃い灰色となって、さっそく全体の数字0の中の一部の特徴を捉えることが分かります。そうやって田の字の枠をずらしていくことで、この画像の数字0の特徴を部分部分で捉えていくのです。
その一度にずらす量は Sh:ストライド高,Sw:ストライド幅 で指定します。
Affine変換の結果を活性化関数を通したものが特徴量というのは通常のニューラルネットワークと全く同じですが、Affine変換は部分部分で行うので、その結果を入れるための器は、ループに入る直前で、
u = np.zeros((B,Oh,Ow,M), dtype=Config.dtype)
によって用意しています。
またもう1行遡って、
self.x = np.pad(x.reshape(B,C,Ih,Iw), [(0,0),(0,0),(pad,pad),(pad,pad)], 'constant')
では、画像の上下左右に空白を加える操作を行っています。
また、u = u.transpose(0,3,1,2)の後に y = self.activator.forward(u) としています。活性化関数を通すのは通常のニューロン層と同じで、これによってニューラルネットワークとしての表現力を劇的に向上しています。
ところでここまであまり説明しませんでしたが、この特徴を捉えるのをFh×Fwの枠を動かしながら行うことによって、ある特徴、例えば、

順に黒、灰色、灰色、黒というのが、上からどれだけ、左からどれだけ、行ったところにあるのか、ということが違っていたとしても、動かした先で同じことに出会えば、結果の行列のどこかに同じ結果が出てくることになるから、この例でいうならば、数字0の位置が違っていても、同じ特徴としてとらえやすいということも、優れている点になります。
これでforwardメソッドの内容についての説明は終わりですが、肝心なことは、Fh×Fwの枠で画像の部分部分の特徴を捉えることで、画像を横×縦の大きなベクトルにしてニューラルネットワークに入れたのでは捉えられない、画像の中の個々の部分の特徴を縦方向も含めて、その位置の違いは抑えつつ捉えることができるということなのです。
ブログのご感想やAIフレームワーク「ufiesia」「pyaino」に関するお問い合わせは、
問い合わせフォームからお送りください。
- お問い合わせ内容によりましては、ご期待に添えない場合やご回答が出来ない事が有ります。