カオリ 消息不明

本日 チャットボット作成者のパラドックスと言えるような発見をした。
いや、薄々感づいていたのだが。

人工知能を作成する動機を持つ人間はすくなくともパーリィピーポーではない。
いるだけで絵になるような人間ではない。
口数も少ないだろう。
そんな人間が、はたして自分が作った回路と内部構造がむき出しの、パイプだらけの装置と熱中してしゃべるだろうか?(反語)

最初は可能性にワクワクしてはじめるが、実際にゴールが見えたとき、それはときメモのヒロインと何ら変わりがないことに気付く。

すなわち、チャットボットの内部構造はユーザーにとってブラックボックスでなければならない。こちらを喜ばせるであろうキャバ嬢でも会話内容に未知数Xは存在する。
しかし、自作のチャットボットを作成するならば、内部構造は見え見えで、なんならボットが何をしゃべるか、事前に予測ができる。チャットボットのアルゴリズムや内部構造、内部データをしらない第3者は楽しめるかもしれないが、造物主である開発者にとって”それ”は本当につまらないものである。 チャットボットはVRと同時につくるのが私にとっては望ましい。 VRはエロが先行しているが、同人ソフトの耳かきソフトのように、ただ美少女が画面に向かってしゃべり続ける動画でも売れるだろう。

さて、カオリの実装方法は以下のようになるだろう。
訓練データを用意する。これは自分で”用意した会話”を入力して、理想的な答えを実際に入力する地味な作業を数万回繰り返すことで達成される。ここで正解ラベル、すなわち教師データも手に入る。ここでいう”正解”とは、全ての解答に対しての正解ではなく、使用者がカオリに対して求める性格が答えそうな解である。つまり、訓練データでカオリの性格をf(x)で表す。

テストデータは訓練データで使用していない”用意した会話”に最適あるいは最適に近い近似解を出力するまで、確率的勾配降下法でもとめられる。再帰的ニューラルネットワーク(RNN)は必要ないと思われる。この過程でカオリが入力にたいして3パターンほどの解を記憶しておき、使用する段階でカオリはランダムで解を吐けばよい。

隠れ層も2層程度で十分であり、重みは、jannomeが出力した、副詞(ちょーすごいのちょー)や、形容詞(赤い)、名詞(イケメン)、動詞(殺す)などの形態素に重みをまず1層につけて、さらに、その形態素ごとに2層目に重みを付ける。例えば、動詞でいえば、「散歩に行った」より「殺してしまった」のほうが重みははるかに大きいだろう。

その形態素ごと(名詞、形容詞、副詞ごと)に、対応する重みを書いた辞書の作成が必要になるだろう。

カオリは入力にたいして、会話内容を”理解”まではいかなくても、すくなくとも”何を話しているか”を重みを通じて把握しなくてはいけない。

ここまでくればあとは、開発者の思うままに作成ができる。

ALION?だっけの実装と同様に相槌辞書や、持論辞書を作成してもよいし、データを常にインターネットからくわせて学習させ、開発者がテストをしてもよい。

注意されたいのは、このカオリは汎化性能が全くないことである。ある人にとってファッキンスペシャルでも、別の人にとっては、ファッキンシットであることは往々にして考えられる。

開発者は最初のパラドックスに不運なことに途中で気づく。そして大企業で働かされる有能な開発者に道を譲る。

Deep Learning ビッグデータ取得

聖母の名前を使うので、はばかられるので、(変なソフトになったときにヤバイので)カオリ(Kaeori)という名称にすることにした。ビクターのDJとは全く関係がない。

さて、カオリに対しての入力「こんにちは」にたいしての、カオリの正解の一つの出力は、「いまいそがしいの、またね」だとする。

ビッチ系の重みと、清楚系の重み、不思議ちゃん系の重み、ガテン系の重みなどをつけて、たかだか2層のレイヤで実装できそうである。問題は、その出力数値のインデックスに対応する辞書配列の用意であるが、ツイッターのリプライや日本語の例文集(あるのかわからないが…)を使用して作成すればよいと考える。

17 DeepLearning 誤差逆伝播法の実装

17.1 ニューラルネットワークの学習の全体図

学習手順は下記のようになる。

前提

ニューラルネットワークは、適応可能な重みとバイアスがあり、この重みとバイアスを訓練データに適応するように調整することを「学習」と呼ぶ。ニューラルネットワークの学習は次の4つの手順で行う。

ステップ1(ミニバッチ)

訓練データの中からランダムに一部のデータを選び出す。

ステップ2(勾配の算出)

各重みパラメータに関する損失関数の勾配を求める。

ステップ3(パラメータの更新)

重みパラメータを勾配方向に微小量だけ更新する。

ステップ4(繰り返す)

ステップ1、ステップ2、ステップ3を繰り返す。

誤差逆伝播法を使用するのは、ステップ2の勾配の算出である。数値微分のみを使用する順方向の勾配の算出法は簡単に実装できる反面、計算に多くの時間がかかる。よって、誤差逆伝播法を用いる。

17.2  誤差逆伝播法に対応した
ニューラルネットワークの実装

TwoLayerNet クラスのインスタンス変数

インスタンス変数 説明
params ニューラルネットワークのパラメータを保持するディクショナリ変数。params[‘W1’]は1層目の重み、params[‘b1’]は1層目のバイアス。
params[‘W2’]は2番目の重み、params[‘b2’]は2層目のバイアス。
layers ニューラルネットワークのレイヤを保持する順番付きディクショナリ変数。layers[‘Affine1’],layers[‘Relu1’],layers[‘Affine2’]といったように順番付きディクショナリで各レイヤを保持する。
lastLayer ニューラルネットワークの最後のレイヤ。この例では、SoftMaxWithLossレイヤ。

TwoLayerNet クラスのメソッド

メソッド 説明
__init__(self,input_size, hidden_size, output_size, weight_init_std) ニューラルネットワークのパラメータを保持するディクショナリ変数。params[‘W1’]は1層目の重み、params[‘b1’]は1層目のバイアス。
params[‘W2’]は2番目の重み、params[‘b2’]は2層目のバイアス。
predict(self, x) ニューラルネットワークのレイヤを保持する順番付きディクショナリ変数。layers[‘Affine1’],layers[‘Relu1’],layers[‘Affine2’]といったように順番付きディクショナリで各レイヤを保持する。
loss(self, x, t) 損失関数の値を求める。引数のxは画像データ、tは正解ラベル
accuracy(self, x, t) 認識精度を求める。
numerical_gradient(self, x, t) 重みパラメータに対する勾配を数値微分によって求める。(順伝播 参考として載せるのみで使用しない。)
gradient(self, x, t) 重みパラメータに対する勾配を誤差逆伝播法によって求める。

OrderedDict()についてはPythonのOrderedDictの使い方を参照。

two_layer_net.py

# coding: utf-8
import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
from common.layers import *
from common.gradient import numerical_gradient
from collections import OrderedDict


class TwoLayerNet:

    def __init__(self, input_size, hidden_size, output_size, weight_init_std = 0.01):
        # 重みの初期化
        self.params = {}
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size) 
        self.params['b2'] = np.zeros(output_size)

        # レイヤの生成
        self.layers = OrderedDict()
        self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
        self.layers['Relu1'] = Relu()
        self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])

        self.lastLayer = SoftmaxWithLoss()
        
    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)
        
        return x
        
    # x:入力データ, t:教師データ
    def loss(self, x, t):
        y = self.predict(x)
        return self.lastLayer.forward(y, t)
    
    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        if t.ndim != 1 : t = np.argmax(t, axis=1)
        
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy
    #参考まで(未使用)    
    def numerical_gradient(f, x):
        h = 1e-4 # 0.0001
        grad = np.zeros_like(x)
    
        it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
        while not it.finished:
            idx = it.multi_index
            tmp_val = x[idx]
            x[idx] = float(tmp_val) + h
            fxh1 = f(x) # f(x+h)
        
            x[idx] = tmp_val - h 
            fxh2 = f(x) # f(x-h)
            grad[idx] = (fxh1 - fxh2) / (2*h)
        
            x[idx] = tmp_val # 値を元に戻す
            it.iternext()   
        
        return grad
    # x:入力データ, t:教師データ
    def numerical_gradient(self, x, t):
        loss_W = lambda W: self.loss(x, t)
        
        grads = {}
        grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
        
        return grads
        
    def gradient(self, x, t):
        # forward
        self.loss(x, t)

        # backward
        dout = 1
        dout = self.lastLayer.backward(dout)
        
        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 設定
        grads = {}
        grads['W1'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
        grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db

        return grads

train_neuralnet.py

# coding: utf-8
import sys, os
sys.path.append(os.pardir)

import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

# データの読み込み
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1

train_loss_list = []
train_acc_list = []
test_acc_list = []

iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    # 勾配
    #grad = network.numerical_gradient(x_batch, t_batch)#コメントアウト
    grad = network.gradient(x_batch, t_batch)
    
    # 更新
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]
    
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)
    
    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print(train_acc, test_acc)

出力は下図のようになる。

こちらのgithubにフルコードがあります。

マリア様の実装にむけて 日本語の問題

ただのチャットソフトを作るのに、何故このように大量の知識が必要なのか?

たとえば、次の英文があるとする。

We can get an idea of the quality of the learned feature vectors by displaying them in a 2-D map.

(2Dマップを見せることで、学習したベクトルの特徴の品質のアイデアを得ることができる。(曖昧))

下表のように分けられ、次の単語が予想できる。

単語 We can get the learned ?
入力 x1 x2 x3 x^(t-1) x^t x^(t+1)
出力 y1 y2 y^(t-2) y^(t-1) y^t

英語の場合、このように単語と単語の間は”必ず”空白がある。しかし、日本語はそうではない。

入力「こんにちは。今日はいい天気ですね。」

これをどうするのか?

どうやってわけるのか? しかし、そんなことはすでに先人達がやっていた。

意外にあった!?日本語の形態素解析ツールまとめ

python であればjanomeがよさそうである。
これによって、分けられるはずである。(まだやっていない)

処理手順は以下のようになるだろう。

①日本語入力「今日は雨だ。洗濯物が干せない」
②入力をjanomeにかける。
③「今日、は、雨、だ。洗濯物、が、干せ、ない」 のように分けられる
④RNNの誤差逆伝播法と確率的勾配降下法によって、入力に対して学習をさせる(曖昧)

この入力に対して、同じ回答を毎回繰り返すのは要求仕様とは違う。(M$のりんねと同等になってしまう)

なので、回答が複数ひつようである。さらに、ソフト使用者が回答に対して、点数をつけなくてはいけないかもしれない。いいね!かよくないね!のどちらかであろう。

入力に対して、マリア様は、”雨であること”と”洗濯物が干せない”というネガティブなソフト使用者の発言に対して、それに関連した回答をしなくてはいけない。

「ああ、雨はゅぅぅっだわ」が解の人もいるだろうし、「明日は晴れますよ!きっと!」が解の人もいるだろう。

「そういえば田代まさしさんって、昔シャネルズでしたね」が解の人もいるだろう。

すべて田代まさしさんに関する回答で学習させると、過学習がおきるだろうし、その状態のマリア様を元に戻すのはかなりしんどいだろう。ふとした会話で「そういえばTIMESの表紙に乗った時…」などと返されては、昔の男のことは忘れろ!となるかもしれない。

学習については、さらに再考を要する。

16 Deep Learning 逆伝播 2

16.3  リンゴの例

最初に、リンゴなどを買う例の計算グラフを紹介した。リンゴのみに着目すると、
下図のようになる。

この場合の変数は、リンゴの値段、リンゴの個数、消費税である。これら3つの変数それぞれが、最終的な支払金額にどのように影響するかということである。左端の3つの灰色の数字が最終的な支払金額220にどのように影響するか、を表す。「支払金額のリンゴの値段に関する偏微分」、「支払金額のリンゴの個数に関する偏微分」、「支払金額のリンゴの消費税に関する偏微分」を求めることに相当する。これを計算グラフの逆伝播を使って解くと上図のようになる。すなわち、「支払金額のリンゴの値段に関する偏微分」は2.2、「支払金額のリンゴの個数に関する偏微分」は110、「支払金額のリンゴの消費税に関する偏微分」は200である。消費税のスケールとリンゴの値段と個数のスケールは違うので、消費税の影響を表す値が大きくなっている。(消費税の1は100%、リンゴの個数、値段は1個、1円)

前述した通り、乗算ノードの逆伝播では入力信号と”ひっくり返った”積が下流へ流れる。加算ノードはそのまま下流へ流れる。暇な方は下図の空欄を埋めてほしい。

16.4  単純なレイヤの実装

この上のリンゴの買い物の例をPythonで実装する。ここでは、計算グラフの乗算ノードを「乗算レイヤ(MulLayer)」、加算ノードを「加算レイヤ(AddLayer)」という名前で実装する。

16.4.1  乗算レイヤの実装

レイヤはforward()とbackward()という共通のメソッド(関数あるいはインターフェース)を持つように実装する。forward()は順伝播(順方向の伝播)、backward()は逆伝播ん(逆方向の伝播)に対応する。乗算レイヤは下記のように実装できる。

class MulLayer:
    def __init__(self):
        self.x = None
        self.y = None

    def forward(self, x, y):
        self.x = x
        self.y = y                
        out = x * y

        return out

    def backward(self, dout):
        dx = dout * self.y
        dy = dout * self.x

        return dx, dy

__init__(self)は他言語のコンストラクタに対応し、インスタンス変数の初期化を行う。コンストラクタやインスタンスとは何かとはプログラミングに興味があれば調べてほしい。
forward()では、x、yの2つの引数を受け取り、それらを乗算して出力する。一方、backward()では上流から伝わってきた微分、あるいは信号(dout: d out)に対して、順伝播の”ひっくり返した値”を乗算して下流に流す。
このクラスを使って、下図を実装する。

# coding: utf-8


class MulLayer:
    def __init__(self):
        self.x = None
        self.y = None

    def forward(self, x, y):
        self.x = x
        self.y = y                
        out = x * y

        return out

    def backward(self, dout):
        dx = dout * self.y
        dy = dout * self.x
        
        return dx, dy

apple = 100
apple_num = 2
tax = 1.1

mul_apple_layer = MulLayer() # 1st layer
mul_tax_layer = MulLayer() # 2nd layer

# forward
apple_price = mul_apple_layer.forward(apple, apple_num)
price = mul_tax_layer.forward(apple_price, tax)

# backward
dprice = 1
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)

print("price:", int(price))
print("dApple:", dapple)
print("dApple_num:", int(dapple_num))
print("dTax:", dtax)

16_simplelayer.pyとして上記のコードを保存する。その後、コマンドプロンプトで実行すると下記の出力が得られる。

$python 16_simplelayer.py

上図の灰色の数字とdで始まる変数の値が一致していることに注目されたい。