研修の2日目
はじめは前日のやり残しから
初日にcelebAを使ったマルチラベルの属性分類までを予定しましたが、できなかったので、2日目はそこから始めました。これについては前回のブログに書きましたので省略します。
さて、1日目を振り返ると...
1日目と2日目のはじめに、MNISTの手書き数字とcelebAの顔画像を扱いました。これらは画像をニューラルネットワークに入力して、その結果としてクラス分類やマルチラベルの属性分類を行いました。
MNISTは?
このうちの小さいほうのMNISTは、縦横28×28で784画素で、各画素は浮動小数点数です。これが入力となるいっぽう出力は0~9の10本です。
簡単に言うならば、784 から 10 へと情報が少なくなっているのです。
具体的に考えると、手書き数字の筆跡やぶれといった情報をすべて捨てて、0~9のどれなのかだけを表すようにしているのです。
celebAは?
ではcelebAの場合はどうでしょう。こちらは先に述べたように縦横218x178のカラー画像を縦横64x48に縮小したものを入力としました。縮小しても64×48=3072が赤・青・緑の3原色で3072×3=9216が画素数になります。そして出力としてほしいのはマルチラベルの40項目ですが、こちらは各項目につき該当か非該当かを表す1/0の数値です。
具体的に例を挙げると、例えば項目の一つに、Bald「禿」という項目があります。そして、禿に分類できる人はBald=1、そうでない人はBald=-1としています。禿といってもはげ方や毛の生え方には色々ありますが、そんな違いは全部捨ててBald=1ないし-1とするのです。Male「男性」もまたしかり、Young「若い」もまたしかり、...で、どの項目も人によりさまざまであることを「じっぱひとからげ」にして、該当・非該当の、まさに1/-1に分けてしまっているのです。
そのように決めつけたようになっているのですが、これは入力にあった多様な情報を捨てていることに他なりません。
余談にはなりますが、
この40項目の該当・非該当の情報は、いったいどれだけの情報なのでしょう?単純化して考えると、2の40乗=1,099,511,627,776です。つまり40項目の組み合わせで表せるのは、およそ1兆通りなのです。もちろんその組み合わせには、その項目に関連性があったり、組み合わせに制約があったり、などして、とりうる値は小さくはなります。しかし大きな数であることは間違いないのです。しかしそれでも先に述べたように情報を大幅に捨てているのですよ。人の顔って無茶苦茶多様なんですよね?
つまり1日目は、ニューラルネットワークで
「情報を捨てる」
ただし、
「必要な情報を残し、それを目的に応じた形に変えて出力」
することだったのです。
情報を「捨てるのは簡単」なのです。 それでこれらを1日目と2日目のはじめの課題としたのです。
簡単かどうかはさておき...
これらの課題では、ニューラルネットワークに画像を入力して、分類に関する情報を出力しました。その中で情報を捨てている=簡単ということも先に述べました。
しかしその捨てているから簡単、その逆は簡単でない、というのは、ひとまずおいておくことにします。
そしてニューラルネットワークで入力から出力へと変換しているという見方をするならば、入力と出力を逆さにすれば逆の変換が出来るのではないかと思いませんか?
文字の生成?
ここからが本来の2日目の内容です。「ともかくやってみよう!」なのです。
入力は0~9のどれであるかで、出力は数字の画像です。
ここで入力をどうやってニューラルネットワークに入れるかを少しだけ考えなければなりません。
今回とは逆だった1日目の分類問題では10本の出力を用意して、そこに0~9をそれぞれ各1本に割り当てて、それが割り当てられたその数である確率を表すように出力を活性化関数で調整しました。
こんどはその逆に、0~9のどれであるかをニューラルネットワークで扱える数値にするわけですが、いろいろなやり方がある中で簡単なのは、one hotと呼ばれる形式にするか、あるいはまた、ニューラルネットワークにそのはたらきをさせるか、のいずれかです。
one hot
one hotとは、カテゴリ間の順序や大小関係を持たせずに表現する方法のひとつで、「複数のカテゴリのうち1つだけを1、その他を0で表す表現」です。
たとえば、0~9の10通りに対して、0ならば、1000000000、1ならば0100000000、...といった具合です。
数値計算ライブラリのnumpyには便利な関数があってカテゴリを表すtに対して、
np.eye(10)[t]
とやるとone hotに変換してくれます。
ともかくもone hotに変換すれば、それをそのままニューラルネットワークに10本の入力として入れることができます。
ニューラルネットワークの出力はといえば、画像の大きさそのものなので先に述べたようにMNISTでは、1*28*28=784 です。
画像として出力を取り出すためには、白黒画像で1チャネルとして、縦28、横28に成型します。
そして学習させるために損失関数をCrossEntropyError2を使いこととします。
細かいことですが、このCrossEntropyError2は0/1両方向の隔たりを;
- t log(y) - (1 - t) log(1 - y)
で評価するもので、画像の正解からの隔たりを見るのに適しています。
以上をそのままニューラルネットワークにすれば次のようになります。
モデルをつくる
model = nn.SequentialWithLoss(
nn.NeuronLayer(1*28*28, activate='Sigmoid', optimize='AdamT'),
F.Reshape(-1,1,28,28),
lf.CrossEntropyError2(),
)
モデルの学習
モデルもできたので、あとは学習させるだけです。
error = []
for i in range(epoch):
for x, t in loader_train:
y, l = model(t, x)
l.backtrace()
model.update(eta=0.0003)
error.append(float(l))
print(f'{i+1:3d}/{epoch:3d} | {l:7.3f}')
これで学習させると次のような学習曲線が得られます。

このように一見きれいな学習曲線が得られました。
文字の生成
あとは次のコードで生成するだけです。
T0 = np.array([2,0,2,6,0,5,2,8]) Y0 = model(T0) cf.show_multi_samples(Y0)
きれいに数字が生成されました。

あれれ、何で?
さてここで、あれれ、何で?と思った人はすごいです。
なぜなら、「情報を捨てる」のの反対=「難しい」はずのことが、簡単に出来てしまったからです。
これはいったいどうしたことなのでしょうか?
よく見ると...
この生成された数字、なにか妙に整っていませんか?
そう感じた人は鋭いです。前回のブログにも掲載しましたが、MNSITの元の数字はさまざまな癖のある筆跡で、こんなにきれいではなかったのです。むしろ読みにくいくらいでした。このたった8文字ですが、数字の2は3つ出てきます。その3つが同じ形をしていませんか?2つ出てくる0も同じ形です。そうです、個性がないのです。個性がないことが文字を読みやすくし、整っていると感じさせ、場合によっては美しいと感じさせているのです。
それはそうです。数字は数そのものを伝える記号であって、その記号にとって、筆跡=個性は邪魔なのです。それが取り除かれた結果として、読みやすく整って見えるのです。
つまりこの場合、文字の生成にあたって再現出来ていないのは、うまい具合に邪魔な情報なのです。
何が起きているのでしょう?
そうなんです。実はこの場合、ニューラルネットワークは「何も生み出し」てはいないのです。ここで言う「生み出す」とは元に無かった新たな情報をつくることです。確かにニューラルネットワークに0~9のどれかを指定するだけで数字の画像になっています。そして画像の情報量は大きいです。しかし何度を描かせても同じ画像しか、出さない、出せないのです。つまりこれは、0~9のカテゴリ分類を画像にマップしただけなのです。そしてその画像の空間にはカテゴリの数と同じ10通りしか存在しないのです。
改めてニューラルネットワークを見ると...
この問題を扱ったニューラルネットワークは、先に示しましたが、そこにあるように、このニューラルネットワークは、
nn.NeuronLayer(1*28*28, activate='Sigmoid', optimize='AdamT'),
の部分がニューラルネットワークとしての働きの元となる演算を担っていますが、
F.Reshape(-1,1,28,28)
は単に出力の形を画像表示に適したようにけているだけです。そして、
lf.CrossEntropyError2(),
は正解と出力の隔たりを評価する損失関数です。
つまりこのニューラルネットワークは単層の変換器に過ぎないのです。
損失が小さくなるようにすることは、平均をとることになる!?
損失関数では出力画像と正解として与えられるMNISTの画像の隔たりを評価します。そしてそれを小さくしようとすることは、何を意味するでしょう?
画像は「たくさん」あります。(近年のAIの学習データのように膨大ではありません、それと比べれば「少し」です。)それらは筆跡の違う複数の文字画像です。
ともかくもMNISTの画像を正解値として出力画像と比較して、その隔たりが「全体として小さく」なるように先に示した学習コードで学習していくのです。
ここで「全体として小さく」が、どうなれば実現されるでしょうか?
もし、どれか特定の画像と完全に一致するようなことになると、どうなるでしょう?
その場合一致する一つに対しては隔たりが無い―つまり、極限まで小さくなりますが、いろいろな筆跡の画像があるのだから、一致するもの以外については、どれもこれも違う―つまり隔たりが大きくなります。
では同じ数字を表す複数の画像の平均に出力を調整したら、どうなるでしょう?
この場合は、平均はあくまで平均であって、どの画像とも一致はしなくなります。しかしどれとも似ていて、隔たりは小さくなります。
しかし、小さくなるといっても一致はしません。あくまでも平均は平均であって、どのサンプルとも一致しません。先に示した学習曲線が、0.2よりも少し大きいところで、それ以下にはならないのは、まさにそれを表しています。
ちゃんと確認しておきます。
ここで平均という話をしましたので、それならば本当に生成画像が平均なのかを確認しておきます。
以下の上の画像はMNISTの画像を数字毎に全平均した画像です。
いっぽう、下の画像は、この学習済みのニューラルネットワークに[0,1,2,3,4,5,6,7,8,9]を入力して得られた画像です。
どうですか?確かに平均になっていますよね?
平均画像

ニューラルネットワークの出力画像

なーんだ、そうだったのか。
この課題は、平均を求めれば良いことを、わざわざ、もっともらしくニューラルネットワークにやらせただけだったのです。
そのことが表すのは、ニューラルネットワークが単に算数を置き換えているだけだということです。言い方を変えると、ニューラルネットワークは数値計算しているだけなのです。
このMNISTの課題で作ったモデルは非常に単純なモデルでした。そして平均と置き換えができます。
実は敢えて、活性化関数に'Sigmoid関数'をつかったり、損失関数に'CrossEntropyError2'をつかったりしましたが、活性化関数は無くしても動くし、損失関数ももっと単純な'MeanSquaredError'に置き換えても動きます。そうすれば本当に入力の単純な重み付け和をとるだけになります。特にこの場合、ニューラルネットワークの入力はone hotで、10本の入力の1つが1、残りは0なので、個々の数字ごとに何らかの係数がけしただけになります。(学習からやり直しとはなりますが...)
しかしながら、この単純なものに活性化関数を加えて、それで変換する、それを層として扱って、そういう層を何層も積み上げていくと、それを数式で表すのは、とんでもなく大変になるし、自ずと結果も簡単な算数では置き換えられなくなります。
その簡単には置き換えできないような複雑な計算をさせる、しかも、目的に合った結果を出力させるように調整する、ニューラルネットワークはまさにそういう仕組みなのです。
そういうニューラルネットワークの仕組みの基本を学ぶのが、この一見「しょうもない」ように見える課題の狙いでした。
おまけ(蛇足)
先に述べたことを逆に言うならば、平均の場合はさておき、ニューラルネットワークは、結果を直接求める式が書かれているわけではないにも関わらず、目的とする結果が得られるように動作するということなのです。そのように動作するように繰り返し少しずつパラメータを調整して学習する機械なのです。
だからニューラルネットワークは、合目的的に、あるいは3段論法的にというべきか、そういう作り方はされていないのです。そして平均のような簡単に結果が算出できる場合ではなく、入力と出力の関係がとんでもなくややこしいような場合に真価を発揮します。もちろんだからと言って、なんでもできるわけではありません。
ブログのご感想やAIフレームワーク「ufiesia」「pyaino」に関するお問い合わせは、
問い合わせフォームからお送りください。
- お問い合わせ内容によりましては、ご期待に添えない場合やご回答が出来ない事が有ります。