20190512のDeepLearningに関する記事は5件です。

【特徴部位可視化】Grad-CAMとGuided Grad-CAMで各Layer可視化ムービー!

このシリーズもこれが四本目で、どうにかやっていることもキャッチアップ出来つつあり、今回は深い層が何をみているかを描画可視化することとします。
※実際にはその層から最終層へ一番寄与する勾配の大きさを評価する技術がGrad-CAMでその視覚的な描画がGuided-Grad-CAMということになります
architecture.png
上記の説明図のAにあたる層を上流に変化させたときのそれぞれの寄与をインプット画像に重畳して示しているものになります。
代表的なものとしてVGG16(weights='imagenet')に適用し、以下の動画が得られました。
layer_VGG16_model_36.gif

やったこと

・各モデルに適用するための工夫
・適用にあたってのコードのポイント
・適用・結果

・各モデルに適用するための工夫

これは前回の「【文字列リスト】条件を満たす要素の一括抽出の仕方」のおまけにコードを置きましたが、モデルを変更するのは、Fine-tuningより簡単です。しかも、学習するのでなければメモリーもあまり必要ありません。
ただし、各モデルはそれなりに大きく特徴(使っている層)がかなり異なります。また、学習データは同じものを学習しているものと思っていましたが、そうでもないようです。
※つまり同一のあのCat_Dogの画像に対して予測されたカテゴリが同じものもありますが、基本異なりました。(というか比較的最近のモデルでは犬や猫を学習していないようです)
しかし、ここではそれを無視して適当に出てきたものを解析することとしました。
そして、何よりモデル構成が異なるのでそれに対応するのが最大の工夫ですが、昨夜のコードを利用してうまく対応できましたので、以下のコードのポイントで述べることとします。

・適用にあたってのコードのポイント

適用するにあたって、コードを整理してリンクのような構成にしました。
1.できるだけ関数化する
前回のカテゴリの動画から変更したコードは以下のとおりです。
今回は、モデルを変更したら、それに合うように解析するLayerを自動的に求めるのが肝であるが、それにともなって処々変更した。
①def preprocessed()の引数にlayer_nameを追加、その層のgrad_camを計算する。そのネットワークモデル(グラフ)としてmodelも入力できるようにし、
model = ResNet50(weights='imagenet')の定義をdef main()に移動した。
②一応、カテゴリの入力sを残した
③以下の関数で一番悩ましかったのは、grad_camのモデルとcompile_saliency_functionのモデルを何にするかといういことである。
本来なら、grad_camは元のmodel、compile_saliency_functionはguided_model(keras.activation.reluをtf.nn.reluに変更したモデル)とすべきだが、ResNet50やXceptionなどの長ーいモデルではactivation層とadd層などがlayer.nemeがmodelとguided_modelで同一でないためgrad_camのところでエラーではじかれる。以下の結果で例と理由を示すが、最終的に以下のコードのようにどちらもguided_modelを入力するのが妥当であるという結論となった。

def preprocessed(model,guided_model,preprocessed_input,s,layer_name='block5_conv3'):
    predictions = model.predict(preprocessed_input)
    top_1 = decode_predictions(predictions)[0][s]
    predicted_class = predictions.argsort()[0][::-1][s]
    cam, heatmap = grad_cam(guided_model, preprocessed_input, predicted_class, layer_name=layer_name) #guided_model
    saliency_fn = compile_saliency_function(guided_model,activation_layer=layer_name) 
    saliency = saliency_fn([preprocessed_input, 0])
    gradcam = saliency[0] * heatmap[..., np.newaxis]
    return predictions, top_1, cam, gradcam

最初に利用するmodelとguided_model(Guided Grad-CAM計算用)を定義する。
そして、解析する画像をインプットする。

def main():
    model = ResNet50(weights='imagenet') #ResNet50 VGG16
    model.summary()
    category=0
    ...
    s1=0
    size=(224,224)
    register_gradient()
    guided_model = modify_backprop(model, 'GuidedBackProp')
    frame = cv2.imread("cat_dog.png")
    ...

そして昨夜の記事で解説した以下のコードを利用して、解析したいlayer.nameをリスト化する。ここでは、ResNet50の場合のものを示している。このモデルは、おまけに置いたようにかなり深いがそのうちaddと最終層のactivation_98という層を抽出している。本来activation_49が最終層なのだが、上記のguided_modelを作成するとaddとactivation層はmodelからの通し番号が振られ、guided_modelではこんなに大きな値になる。

    name_list=[l.name for l in guided_model.layers[1:]]  #guided_model
    name_list = [layer for layer in name_list if 'add' in layer or 'activation_98' in layer]
    print(len(name_list),name_list)

以下のような結果が得られます。

17 ['add_17', 'add_18', 'add_19', 'add_20', 'add_21', 'add_22', 'add_23', 'add_24', 'add_25', 'add_26', 'add_27', 'add_28', 'add_29', 'add_30', 'add_31', 'add_32', 'activation_98']

動画は時間で切っています。
層の種類の変更はs =s1%(len(name_list))で繰り返すようにしています。このsを使って上記のリストから指定してactivation_layerに代入してgrad-camなどを計算します。

    timer0=time.time()
    while True:
        s =s1%(len(name_list))
        activation_layer=name_list[s]
        print(s)
        input= cv2.resize(frame, (480,480))
        input = cv2.cvtColor(input, cv2.COLOR_BGR2RGB)
        ax1.imshow(input)
        ax1.set_title("original_"+str(category)+"_ResNet50 "+str(activation_layer))

まず、画像を4次元に変更してpreprocessed()関数に入力します。

        preprocessed_input= cv2.resize(frame, size)
        preprocessed_input= np.expand_dims(preprocessed_input, axis=0)
        predictions, top_1, cam, gradcam = preprocessed( model, guided_model, preprocessed_input, category, layer_name=activation_layer)

動画はcv2版もできますが、ここではplt版にしています。タイトルにいろいろな属性を記載しています。

        input= cv2.resize(cam, (480,480))
        ax2.imshow(input)
        ax2.set_title("gradcam_ResNet50_"+str(top_1[1])+"_"+ str(int(top_1[2]*1000)/10)+" %")
        input= cv2.resize(deprocess_image(gradcam), (480,480))
        ax3.imshow(input)
        ax3.set_title("guided_gradcam_ResNet50"+str(top_1[1])+"_"+ str(int(top_1[2]*1000)/10)+" %")
        plt.pause(1)
        cv2.imshow("guided_gradcam_"+str(top_1[1])+"_"+ str(int(top_1[2]*1000)/10)+" %",input)
        s1 += 1
        plt.savefig("output/image_ResNet50_"+str(s1)+".jpg")
        dst = cv2.imread('output/image_ResNet50_'+str(s1)+'.jpg')
        img_dst = cv2.resize(dst, (int(480), 480))
        out.write(img_dst)

動画にするには、上記のcv2.imshowが大切で、これを描画すると以下のキー入力が安定して使えます。実際にはその下のifの部分で120秒記録して終了します。
ここで、上記の大量なjpg画像ファイルとoutで出力したmp4ファイルが保存されています。
そして、jpg画像ファイルから綺麗なGifアニメーションを生成したのが、最初に掲示した動画です。ほぼ同じような動画がmp4で保存されています。
※動画保存のおまじないなどのコードは今回は示していませんが、コード全体には含まれているので、参考にしてください

        k = cv2.waitKey(1)&0xff
        if k == ord('q'):
            cv2.destroyAllWindows()
            break
        else:
            cv2.destroyAllWindows()
        print(time.time()-timer0)
        if time.time()-timer0>=120:
            out.release()
            break
if __name__ == '__main__':
    main()

コードは以下に置きました

cheating_DL/plot_grad-cam_categoryC_layer.py

適用・結果

VGG16:guided_modelは上記のとおりです。
VGG16 model compile_saliency_function;modelで実施
layer_vgg16_m18.gif
これだと、Guided-Grad-CAMの画像がつぶれています。
そして、VGG16、VGG19モデルでは、どちらもmodelとguided_modelは同じlayer.nameを持っているので、model compile_saliency_function;にguided_modelを使えばどちらを使っても同じような画像が得られました。
しかし、それ以外ではこれらは異なります。以下、ResNet50の場合を比較します。
grad-cam;model compile_saliency_function;modelの場合
layer_ResNet50_model_36.gif
grad-cam,compile_saliency_function;guided_modelの場合
layer_ResNet50_guided_model_21.gif
上記の動画はgrad-camはほぼ同じ動画が得られていますが、Guided-Grad-CAMはmodel-modelではVGG16と同じように、元の下絵が再現できていないのが分かります。
このことは、やはり先日の記事でreluの差し替えの問題で指摘しています。
ということで、直接画像を見ると以下のとおりになっています。
image_ResNet50_17.jpg
image_ResNet50_17.jpg
最後に、VGG19とVGG16を並べて貼っておきます。比較的似ている画像が動いているのが分かります。

VGG16 VGG19
layer_vgg16_g18.gif layer_vgg19_22.gif

まとめ

・いろいろなモデルの層毎のGrad-camとGuidel-Crad-camの可視化を実施した
・VGG16とVGG19だと、だいたい最後の二層程度で急激に選んだカテゴリに集中する
・そしてResNet50でさえ最後の層のGrad-camやGuided-Grad-CAMの画像は酷似している。
・compile_saliency_function;はguided_model(tf.nn.relu)でないと綺麗な画像は得られない

・この技術をAdversarialAttackの分析に応用したいと思う
・確率が低いカテゴリも綺麗に覚えていることもあるので、さらに利用の仕方を考えようと思う

おまけ

__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to
==================================================================================================
input_1 (InputLayer)            (None, 224, 224, 3)  0
__________________________________________________________________________________________________
conv1_pad (ZeroPadding2D)       (None, 230, 230, 3)  0           input_1[0][0]
__________________________________________________________________________________________________
conv1 (Conv2D)                  (None, 112, 112, 64) 9472        conv1_pad[0][0]
__________________________________________________________________________________________________
bn_conv1 (BatchNormalization)   (None, 112, 112, 64) 256         conv1[0][0]
__________________________________________________________________________________________________
activation_1 (Activation)       (None, 112, 112, 64) 0           bn_conv1[0][0]
__________________________________________________________________________________________________
pool1_pad (ZeroPadding2D)       (None, 114, 114, 64) 0           activation_1[0][0]
__________________________________________________________________________________________________
max_pooling2d_1 (MaxPooling2D)  (None, 56, 56, 64)   0           pool1_pad[0][0]
__________________________________________________________________________________________________
res2a_branch2a (Conv2D)         (None, 56, 56, 64)   4160        max_pooling2d_1[0][0]
__________________________________________________________________________________________________
bn2a_branch2a (BatchNormalizati (None, 56, 56, 64)   256         res2a_branch2a[0][0]
__________________________________________________________________________________________________
activation_2 (Activation)       (None, 56, 56, 64)   0           bn2a_branch2a[0][0]
__________________________________________________________________________________________________
res2a_branch2b (Conv2D)         (None, 56, 56, 64)   36928       activation_2[0][0]
__________________________________________________________________________________________________
bn2a_branch2b (BatchNormalizati (None, 56, 56, 64)   256         res2a_branch2b[0][0]
__________________________________________________________________________________________________
activation_3 (Activation)       (None, 56, 56, 64)   0           bn2a_branch2b[0][0]
__________________________________________________________________________________________________
res2a_branch2c (Conv2D)         (None, 56, 56, 256)  16640       activation_3[0][0]
__________________________________________________________________________________________________
res2a_branch1 (Conv2D)          (None, 56, 56, 256)  16640       max_pooling2d_1[0][0]
__________________________________________________________________________________________________
bn2a_branch2c (BatchNormalizati (None, 56, 56, 256)  1024        res2a_branch2c[0][0]
__________________________________________________________________________________________________
bn2a_branch1 (BatchNormalizatio (None, 56, 56, 256)  1024        res2a_branch1[0][0]
__________________________________________________________________________________________________
add_1 (Add)                     (None, 56, 56, 256)  0           bn2a_branch2c[0][0]
                                                                 bn2a_branch1[0][0]
__________________________________________________________________________________________________
。。。
__________________________________________________________________________________________________
add_15 (Add)                    (None, 7, 7, 2048)   0           bn5b_branch2c[0][0]
                                                                 activation_43[0][0]
__________________________________________________________________________________________________
activation_46 (Activation)      (None, 7, 7, 2048)   0           add_15[0][0]
__________________________________________________________________________________________________
res5c_branch2a (Conv2D)         (None, 7, 7, 512)    1049088     activation_46[0][0]
__________________________________________________________________________________________________
bn5c_branch2a (BatchNormalizati (None, 7, 7, 512)    2048        res5c_branch2a[0][0]
__________________________________________________________________________________________________
activation_47 (Activation)      (None, 7, 7, 512)    0           bn5c_branch2a[0][0]
__________________________________________________________________________________________________
res5c_branch2b (Conv2D)         (None, 7, 7, 512)    2359808     activation_47[0][0]
__________________________________________________________________________________________________
bn5c_branch2b (BatchNormalizati (None, 7, 7, 512)    2048        res5c_branch2b[0][0]
__________________________________________________________________________________________________
activation_48 (Activation)      (None, 7, 7, 512)    0           bn5c_branch2b[0][0]
__________________________________________________________________________________________________
res5c_branch2c (Conv2D)         (None, 7, 7, 2048)   1050624     activation_48[0][0]
__________________________________________________________________________________________________
bn5c_branch2c (BatchNormalizati (None, 7, 7, 2048)   8192        res5c_branch2c[0][0]
__________________________________________________________________________________________________
add_16 (Add)                    (None, 7, 7, 2048)   0           bn5c_branch2c[0][0]
                                                                 activation_46[0][0]
__________________________________________________________________________________________________
activation_49 (Activation)      (None, 7, 7, 2048)   0           add_16[0][0]
__________________________________________________________________________________________________
avg_pool (GlobalAveragePooling2 (None, 2048)         0           activation_49[0][0]
__________________________________________________________________________________________________
fc1000 (Dense)                  (None, 1000)         2049000     avg_pool[0][0]
==================================================================================================
Total params: 25,636,712
Trainable params: 25,583,592
Non-trainable params: 53,120
__________________________________________________________________________________________________

guided_model ResNet50

Layer (type)                    Output Shape         Param #     Connected to
==================================================================================================
input_2 (InputLayer)            (None, 224, 224, 3)  0
__________________________________________________________________________________________________
conv1_pad (ZeroPadding2D)       (None, 230, 230, 3)  0           input_2[0][0]
__________________________________________________________________________________________________
conv1 (Conv2D)                  (None, 112, 112, 64) 9472        conv1_pad[0][0]
__________________________________________________________________________________________________
bn_conv1 (BatchNormalization)   (None, 112, 112, 64) 256         conv1[0][0]
__________________________________________________________________________________________________
activation_50 (Activation)      (None, 112, 112, 64) 0           bn_conv1[0][0]
__________________________________________________________________________________________________
pool1_pad (ZeroPadding2D)       (None, 114, 114, 64) 0           activation_50[0][0]
__________________________________________________________________________________________________
max_pooling2d_2 (MaxPooling2D)  (None, 56, 56, 64)   0           pool1_pad[0][0]
________________________________________________________________________________

。。。
________________________________________________________________________________
__________________
add_31 (Add)                    (None, 7, 7, 2048)   0           bn5b_branch2c[0][0]
                                                                 activation_92[0][0]
__________________________________________________________________________________________________
activation_95 (Activation)      (None, 7, 7, 2048)   0           add_31[0][0]
__________________________________________________________________________________________________
res5c_branch2a (Conv2D)         (None, 7, 7, 512)    1049088     activation_95[0][0]
__________________________________________________________________________________________________
bn5c_branch2a (BatchNormalizati (None, 7, 7, 512)    2048        res5c_branch2a[0][0]
__________________________________________________________________________________________________
activation_96 (Activation)      (None, 7, 7, 512)    0           bn5c_branch2a[0][0]
__________________________________________________________________________________________________
res5c_branch2b (Conv2D)         (None, 7, 7, 512)    2359808     activation_96[0][0]
__________________________________________________________________________________________________
bn5c_branch2b (BatchNormalizati (None, 7, 7, 512)    2048        res5c_branch2b[0][0]
__________________________________________________________________________________________________
activation_97 (Activation)      (None, 7, 7, 512)    0           bn5c_branch2b[0][0]
__________________________________________________________________________________________________
res5c_branch2c (Conv2D)         (None, 7, 7, 2048)   1050624     activation_97[0][0]
__________________________________________________________________________________________________
bn5c_branch2c (BatchNormalizati (None, 7, 7, 2048)   8192        res5c_branch2c[0][0]
__________________________________________________________________________________________________
add_32 (Add)                    (None, 7, 7, 2048)   0           bn5c_branch2c[0][0]
                                                                 activation_95[0][0]
__________________________________________________________________________________________________
activation_98 (Activation)      (None, 7, 7, 2048)   0           add_32[0][0]
__________________________________________________________________________________________________
avg_pool (GlobalAveragePooling2 (None, 2048)         0           activation_98[0][0]
__________________________________________________________________________________________________
fc1000 (Dense)                  (None, 1000)         2049000     avg_pool[0][0]
==================================================================================================
Total params: 25,636,712
Trainable params: 25,583,592
Non-trainable params: 53,120
__________________________________________________________________________________________________
17 ['add_17', 'add_18', 'add_19', 'add_20', 'add_21', 'add_22', 'add_23', 
'add_24', 'add_25', 'add_26', 'add_27', 'add_28', 'add_29', 'add_30', 'add_31', 
'add_32', 'activation_98']
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TensorFlow.jsでDeepLearning(Making Predictions from 2D Data)

こんにちわ。Electric Blue Industries Ltd.という、ITで美を追求するファンキーでマニアックなIT企業のマッツと申します。TensorFlow.jsでDeepLearningのチュートリアル「Making Predictions from 2D Data」の詳細解説の前半です。

これはTensorFlow.JSの公式サイトにある「TensorFlow.js — Making Predictions from 2D Data」をコードの中に記載したコメントで詳細に解説したものです。解説の利便性によりコードの部分の位置関係は変更してありますが、内容に変化はありません。実際に動作するデモはこちらで見られます。

1. コード

1.1. html

ライブラリを読み込んでDeep LearningのためのJavaScriptを実行するHTMLです。

展開してコードを見る
index.html
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TensorFlow.js Tutorial - Making Predictions from 2D Data</title>

    <!-- Import TensorFlow.js (TensorFlow.jsライブラリ本体を読み込みます) -->
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.js"></script>
    <!-- Import tfjs-vis (TensorFlow.js向けの可視化ライブラリを読み込みます) -->
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis@1.0.2/dist/tfjs-vis.umd.min.js"></script>
    <!-- Import the main script file (下記に解説するJavaScriptを読み込みます)-->
    <script type="text/javascript" src="script.js"></script>
</head>
<body>
</body>
</html>

1.2. JavaScript

上記のHTMLにインクルードされてDeep Learning処理を行うJavaScriptです。

展開してコードを見る
script.js
/******************************************************************
TensorFlow.js — Making Predictions from 2D Data
url: https://codelabs.developers.google.com/codelabs/tfjs-training-regression/index.html#0
filename: script.js
copyrighted to: tensorflow.org
composed by: Mats (Electric Blue Industries Ltd.)
description: 車のスペック情報から「燃費」「馬力」のデータを学習し、それらの相関を学習させる
******************************************************************/

async function run() {

    //******************************************************************
    // 1. getData: 元データをgetDataを使って読み込み対象アイテムのみフィルター(元データプロット用)
    //******************************************************************

    // 非同期で元データを取得(getDataは下で関数として定義)
    // なお、取得するデータは [{0:a, 1:b, 2:c}, {0:d, 1:e, 2:f}]のように、各クルマのスペック情報を持つオブジェクトが要素になった配列データ
    const data = await getData();

    // 読み込んだ元データであるdataから項目抽出して、座標情報オブジェクトを各要素とする配列として新たに格納
    const values = data.map(d => ({
        x: d.horsepower,
        y: d.mpg,
    }));

    // TensorFlowJSの可視化ライブラリで元データの位置をプロットする
    tfvis.render.scatterplot(
        // 表のタイトルの指定
        {name: 'Horsepower vs Miles Per Gallon'},
        // 上記で生成した座標情報オブジェクトを各要素とする配列を指定
        {values}, 
        // 表のx軸y軸のタイトルおよび表の高さを指定
        {
            xLabel: 'Horsepower',
            yLabel: 'Miles Per Gallon',
            height: 300
        }
    );

    // 元データを非同期で読み込む関数
    async function getData() {

        // 非同期でクルマのスペックデータ(内容はオブジェクトを要素に持つ配列のフォーマットをしている)を取得しcarsDaraに格納
        const carsDataReq = await fetch('https://storage.googleapis.com/tfjs-tutorials/carsData.json');
        // 読み込んだデータをJSON形式として読んで各クルマのスペック(オブジェクト)を要素として持つ配列carsDataとして格納
        const carsData = await carsDataReq.json();
        // 格納した配列carsDataデータから元データとして用いる2アイテムのみを取得しcleanedとして格納
        // map関数は配列の各要素に繰り返し指定された処理を行うので、cleanedはmpgとhorsepowerの情報のみを含むオブジェクトを要素とする配列
        // なお、mpgとhorsepowerのどちらかでも空文字の場合はデータから削除しておくfilterをかける
        const cleaned = carsData.map(car => ({
            mpg: car.Miles_per_Gallon,
            horsepower: car.Horsepower,
        }))
        .filter(car => (car.mpg != null && car.horsepower != null));

        return cleaned;

    }

    //******************************************************************
    // 2. createModel: モデルの枠組みの作成
    //******************************************************************

    // モデルを作成(createModelは下で関数として定義)
    const model = createModel();
    // 上記で作成したモデルの要約情報(Layer Name, Output Shape, # Of Params, Trainable)表示
    tfvis.show.modelSummary({name: 'Model Summary'}, model);

    // モデル作成の関数定義
    function createModel() {

        // シーケンシャルモデル(線形回帰モデル)の枠組みの作成
        // これはモデルが全体として線形回帰モデルになるという意味でなく、各ニューロンの入出力の関係が y=Σ(wx)+b と書ける線形回帰モデルであるということ
        const model = tf.sequential();

        // 入力層を追加
        model.add(tf.layers.dense({
            // 入力は 1x1 のテンソル(=スカラー)
            inputShape: [1],
            // ユニット(別名:ノード)は1個だけ
            units: 1,
            // y=Σ(wx)+b となる定数項bであるバイアスを使用する
            useBias: true
        }));

        // ここに中間層を追加した場合にはどうなるのかは別途に言及する

        // 出力層を追加
        model.add(tf.layers.dense({
            // ユニット(別名:ノード)は1個だけ
            units: 1,
            // y=Σ(wx)+b となる定数項bであるバイアスを使用する
            useBias: true
        }));

        return model;

    }

    //******************************************************************
    // 3. convertToTensor: 学習データを上記モデルに流し込めるようテンソルに変換する
    //******************************************************************

    // getDataで取得したclean済み配列データを下記で定義したconvertToTensor関数でテンソルに変換(不要なアイテムは同時にフィルター)
    const tensorData = convertToTensor(data);
    const {inputs, labels} = tensorData;

    // 学習データをテンソルに変換する
    function convertToTensor(data) {

        // tidyを使って計算することで、計算経過で生成される変数をメモリから削除してメモリにゴミを残さない
        return tf.tidy(() => {

            // (ステップ1) データをシャッフルする 

            tf.util.shuffle(data);

            // (ステップ2) データを配列に格納してから展開してテンソルに変換

            // 変数inputに馬力に関するデータ(実際は配列)を格納
            const inputs = data.map(d => d.horsepower)
            // 変数labelsに燃費に関するデータ(実際は配列)を格納
            const labels = data.map(d => d.mpg);

            // 上記で作成したインプット値(馬力)の配列を使って Nx1 の行列を生成
            const inputTensor = tf.tensor2d(inputs, [inputs.length, 1]);
            // 上記で作成される2次元テンソルは下記のような縦長の形
            // [[馬力の値1],
            //  [馬力の値2],
            //    :
            //  [馬力の値N]]

            // 上記で作成したラベル値(燃費)配列を使って Nx1 の行列を生成
            const labelTensor = tf.tensor2d(labels, [labels.length, 1]);
            // 上記で作成される2次元テンソルは下記のような縦長の形
            // [[燃費の値1],
            //  [燃費の値2],
            //    :
            //  [燃費の値N]]

            // (ステップ3) 入力データの値を0から1の間に正規化

            // 入力とラベルを最大値と最小値を調べて取得
            // 入力である馬力の最大値を取得
            const inputMax = inputTensor.max();
            // 入力である馬力の最小値を取得
            const inputMin = inputTensor.min(); 
            // 出力である燃費の最大値を取得 
            const labelMax = labelTensor.max();
            // 出力である燃費の最小値を取得 
            const labelMin = labelTensor.min();

            // 入力とラベルを正規化
            // inputTensor.sub(inputMin)で各入値から最小入力値をひく(=最小値をゼロに落とす) >> (これをAとする)
            // inputMax.sub(inputMin)で最大入力値から最小入力値をひく >> (これをBとする)
            // 上記の各(A)の値を(B)で割ることで、最大値が1で最小値が0になるよう正規化する
            const normalizedInputs = inputTensor.sub(inputMin).div(inputMax.sub(inputMin));
            // labelTensor.sub(labelMin)で各入値から最小入力値をひく(=最小値をゼロに落とす) >> (これをA'とする)
            // labelMax.sub(labelMin)で最大入力値から最小入力値をひく >> (これをB'とする)
            // 上記の各(A')の値を(B')で割ることで、最大値が1で最小値が0になるよう正規化する
            const normalizedLabels = labelTensor.sub(labelMin).div(labelMax.sub(labelMin));

            return {
                // 正規化された値を要素に持つ入力テンソルと出力(ラベル)テンソルを返す
                inputs: normalizedInputs,
                labels: normalizedLabels,
                // 入力と出力(ラベル)の最大値最小値もあとで逆正規化できるよう返す
                inputMax,
                inputMin,
                labelMax,
                labelMin,
            }
        });

    }

    //******************************************************************
    // trainModel: モデルの学習
    //******************************************************************

    // モデルの学習(trainModelは下で関数として定義)
    // awaitとすることでtrainModel関数からreturnがあるまで待機する
    await trainModel(model, inputs, labels);

    // モデル・入力(インプット)テンソル・出力(ラベル)テンソルを指定してモデルの学習を行う関数
    async function trainModel(model, inputs, labels) {

        // 学習実行のため、学習方法を指定してモデルをコンパイル  
        model.compile({
            // 最適化法をアダム(=適応モーメント推定法)に指定
            optimizer: tf.train.adam(),
            // 損失関数をMSE(=平均二乗誤差)に指定
            loss: tf.losses.meanSquaredError,
            // 学習とテストに用いる指標(この場合は平均二乗誤差)を表す表現を決める
            metrics: ['mse'],
        });

        // バッチサイズ(小分けにグループ分けした学習データに含まれるデータの個数)を28個とする
        const batchSize = 28;
        // 学習の1手順の回数を50回とする
        const epochs = 50;

        // epochsで指定した回数の学習手順回数(エポック)になるまで学習を実行する
        return await model.fit(inputs, labels, {
            batchSize,
            epochs,
            shuffle: true,
            // 学習結果の随時の描画用にTFVISに、コンパイル時に指定した指標の値をコールバックする指定
            callbacks: tfvis.show.fitCallbacks(
                // 描画する表のタイトル
                { name: 'Training Performance' },
                // 描画する指標(ここではlossとmseを改めて指定)
                ['loss', 'mse'], 
                { 
                    // 表の高さ
                    height: 200, 
                    // コールバックのタイミング
                    callbacks: ['onEpochEnd'] 
                }
            )
        });

    }

    // trainModel関数に返り値があった時点で規定したエポックが終了したことになるので「学習が終わった」とコンソールに出力
    console.log('Done Training');

    //******************************************************************
    // testModel: 学習ずみモデルに入力を与えて出力を得て、元データのプロットと重ねて違いを視覚的に見せる
    //******************************************************************

    // モデルのテスト(testModelは下で関数として定義)
    testModel(model, data, tensorData);


    function testModel(model, inputData, normalizationData) {

        const {inputMax, inputMin, labelMin, labelMax} = normalizationData;  

        // Generate predictions for a uniform range of numbers between 0 and 1;
        // We un-normalize the data by doing the inverse of the min-max scaling 
        // that we did earlier.
        const [xs, preds] = tf.tidy(() => {

            // tf.linespaceで0から1までの間に等間隔となる100個の値を生成(0, 0.01, 0.02, 0.03,・・, 0.98, 0.99)し格納
            // なお、tf.linspace()によって生成されるのは配列ではなく「Array.from()で配列化できるオブジェクト」である
            const xs = tf.linspace(0, 1, 100);
            // 上記で生成した100個の数値を要素にもつ行列を生成し、学習したモデルに予測値として出力させる
            const preds = model.predict(xs.reshape([100, 1]));
            // モデルの入出力は共に正規化されているので、これを元に戻す計算を行う
            const unNormXs = xs.mul(inputMax.sub(inputMin)).add(inputMin);
            const unNormPreds = preds.mul(labelMax.sub(labelMin)).add(labelMin);
            // 上記で非正規化されたデータは100行1列の行列になっているので、これらを単なる配列の形にする
            return [unNormXs.dataSync(), unNormPreds.dataSync()];

        });

        // 学習データのポイント(座標)を配列として格納
        // mapは配列を受け取って、指定した処理を行う
        // ここでは入力値inputDataとして「座標情報を持ったオブジェクト」を要素にもつdataを代入しているので、各要素から馬力と燃費のデータを読んで
        // ポイント(座標)をオブジェクト形式で表す要素を持つ配列の各要素として持たせている
        const originalPoints = inputData.map(d => (
            {
                x: d.horsepower,
                y: d.mpg,
            }
        ));

        // 学習させたモデルを使って算出した予測値のポイント(座標)をオブジェクト形式で表す要素を持つ配列として格納
        const predictedPoints = Array.from(xs).map((val, i) => {
            return {
                x: val,
                y: preds[i]
            }
        });

        // 上記で得た予測値および学習データ値のポイントをTFVISに渡してプロット表示
        tfvis.render.scatterplot(
            {
                // 表のタイトル
                name: 'Model Predictions vs Original Data'
            },{
                // originalPointsとpredictedPointsはポイントの座標をJSONで表現した文字列を要素に持つ配列
                values: [originalPoints, predictedPoints],
                series: ['original', 'predicted']
            },{
                // 縦軸と横軸の名称、表の高さ
                xLabel: 'Horsepower',
                yLabel: 'Miles Per Gallon',
                height: 300
            }
        );

    }

}

document.addEventListener('DOMContentLoaded', run);

2. 実行結果

TensorFlow.jsの処々の情報は可視化ライブラリであるTF-VISを使って表やグラフで可視化することができます。下記は上記のコードを実行した際に表示された情報です。

2.1. 学習データのプロット

学習データの「馬力(横軸:Horsepower)」と「燃費(縦軸:Miles Per Gallon)」の関係をプロットしたプロットチャートです。これを見ると、学習すべき馬力と燃費の関係は互いに反比例する関係にあることが伺えますが、直線的なの反比例関係ではなく、穏やかなカーブを描くような反比例であることが伺えます。

2.2. 学習の進行状況

下記は損失関数の値です。損失関数をmse(= Mean Squared Error = 平均二乗誤差)に指定し、学習のエポックごとにその値が降下していっていることがわかります。このコードでは学習を50エポックまでと指定したので横軸は50までとなっています。損失関数の値がこれで十分なのかどうかと言う話は別途に言及します。

Training Performance - mse.png

2.3. 学習したモデルによる出力予測

学習させたモデルに入力として「馬力」の値を与え、その出力として「予測(predict)される燃費」を得ました。その関係を先ほどの学習データのプロットチャートに重ねて表示したものがこれです。学習したモデルが出力したオレンジ色の点が直線的に並んでいます。

Model Predictions vs Original Data.png

3. モデルの精度を高める

TensorFlow.orgのチュートリアルは上記で終わりとなっているのですが、上記で記述したコードと作成したモデルについて、「これで良いのでしたっけ?」と言う視点で考察と改造を行います。

と言うのは、学習データの「馬力(横軸:Horsepower)」と「燃費(縦軸:Miles Per Gallon)」の関係は直線的なの反比例関係ではなく、穏やかなカーブを描くような反比例であることが伺えます。しかしながら、学習させたモデルが予測した関係は直線的な正比例の傾向を示しています。この相違によってモデルの予測精度が十分ではなくなっていると推測できます。もっと、正確に入力と出力の関係を学習させるにはどうすればイイのか?

なお、上記で作成したモデルによる予測の損失関数の値(MSE: 平均二乗誤差)は0.7から0.8でした。

3.1. 出力層に非線形活性化関数を適用する

// 出力層を追加
        model.add(tf.layers.dense({
            // ユニット(別名:ノード)は1個だけ
            units: 1,
            // y=Σ(wx)+b となる定数項bであるバイアスを使用する
            useBias: true,
            // 出力を線形にしないために非線形活性化関数を被せる(データに対してsoftplusが最適)
            activation: 'softplus'
        }));

そうすると、MSEの値は0.0189程度まで下がり、回帰の精度が向上しました。

3-1_ActivationFuncToOutput_MSE.png

回帰を表す予測値の描くオレンジの線も曲線になり、非線形回帰ができています。

3-1_ActivationFuncToOutput.png

3.2. 中間層を追加する

しかしながら、回帰曲線の曲がり具合が「硬い」ように見受けられ、この曲線をもっと柔らかくしたいように思います。そこで、モデルに中間層を加えます。中間層を加える行為というのは、イメージで言って「回帰曲線に急激な曲がりを加えられるようにすることを可能にする」行為です。また、ユニット(=ノード)の数は回帰曲線の曲げ具合を制御する「コントロールポイント」の数のイメージで、コントロールポイントが多いと回帰曲線を細かく曲げられます。ベジェ曲線のコントロールポイントと似ています。

// ここに中間層を追加した場合にはどうなるのかは別途に言及する
        model.add(tf.layers.dense({
            // ユニット(別名:ノード)は16個
            units: 16,
            // y=Σ(wx)+b となる定数項bであるバイアスを使用する
            useBias: true
        }));

そうすると、MSEの値は0.0136程度まで下がり、回帰の精度が向上しました。

3-2_ActivationFuncToOutput_MSE.png

回帰曲線の曲がり具合も強くなりました。

3-2_ActivationFuncToOutput.png

3.3. 中間層のノード数を増やす

回帰曲線の曲がり具合が柔軟になったので、更にユニットを増やして32個にします。

// ここに中間層を追加した場合にはどうなるのかは別途に言及する
        model.add(tf.layers.dense({
            // ユニット(別名:ノード)は32個
            units: 32,
            // y=Σ(wx)+b となる定数項bであるバイアスを使用する
            useBias: true
        }));

そうすると、MSEの値は0.0136程度でユニット数16の時とほぼ変わらず、回帰の精度は向上しません。

3-3_ActivationFuncToOutput_MSE.png

描かれる回帰曲線もほぼ変わっていません。

3-3_ActivationFuncToOutput.png

これは、「ユニットを増やすことで回帰曲線を細かく曲げられるようになったが、実際に細かく曲げても『右に曲げれば左から遠くなり、左に曲げれば右から遠くなり』という収束点に既に到達したから」と考えられます。このように、ユニット数を増やすのは予測精度を高めるには有効ですが、無駄に増やすと計算処理量ばかりが増えて精度は向上しないので、学習させるデータの特性を理解しながら学習させることが肝要です。


追伸: Machine Learning Tokyoと言うMachine Learningの日本最大のグループに参加しています。作業系の少人数会合を中心に顔を出しています。基本的に英語でのコミュニケーションとなっていますが、能力的にも人間的にもトップレベルの素晴らしい方々が参加されておられるので、機会がありましたら参加されることをオススメします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TensorFlow.jsでDeepLearning(Making Predictions from 2D Data - 前半)

こんにちわ。Electric Blue Industries Ltd.という、ITで美を追求するファンキーでマニアックなIT企業のマッツと申します。TensorFlow.jsでDeepLearningのチュートリアル「Making Predictions from 2D Data」の詳細解説の前半です。

これはTensorFlow.JSの公式サイトにある「TensorFlow.js — Making Predictions from 2D Data」をコードの中に記載したコメントで詳細に解説したものです。解説の利便性によりコードの部分の位置関係は変更してありますが、内容に変化はありません。実際に動作するデモはこちらで見られます。

1. コード

1.1. html

ライブラリを読み込んでDeep LearningのためのJavaScriptを実行するHTMLです。

index.html
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TensorFlow.js Tutorial - Making Predictions from 2D Data</title>

    <!-- Import TensorFlow.js (TensorFlow.jsライブラリ本体を読み込みます) -->
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.js"></script>
    <!-- Import tfjs-vis (TensorFlow.js向けの可視化ライブラリを読み込みます) -->
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-vis@1.0.2/dist/tfjs-vis.umd.min.js"></script>
    <!-- Import the main script file (下記に解説するJavaScriptを読み込みます)-->
    <script type="text/javascript" src="script.js"></script>
</head>
<body>
</body>
</html>

1.2. JavaScript

上記のHTMLにインクルードされてDeep Learning処理を行うJavaScriptです。

script.js
/******************************************************************
TensorFlow.js — Making Predictions from 2D Data
url: https://codelabs.developers.google.com/codelabs/tfjs-training-regression/index.html#0
filename: script.js
copyrighted to: tensorflow.org
composed by: Mats (Electric Blue Industries Ltd.)
description: 車のスペック情報から「燃費」「馬力」のデータを学習し、それらの相関を学習させる
******************************************************************/

async function run() {

    //******************************************************************
    // 1. getData: 元データをgetDataを使って読み込み対象アイテムのみフィルター(元データプロット用)
    //******************************************************************

    // 非同期で元データを取得(getDataは下で関数として定義)
    // なお、取得するデータは [{0:a, 1:b, 2:c}, {0:d, 1:e, 2:f}]のように、各クルマのスペック情報を持つオブジェクトが要素になった配列データ
    const data = await getData();

    // 読み込んだ元データであるdataから項目抽出して、座標情報オブジェクトを各要素とする配列として新たに格納
    const values = data.map(d => ({
        x: d.horsepower,
        y: d.mpg,
    }));

    // TensorFlowJSの可視化ライブラリで元データの位置をプロットする
    tfvis.render.scatterplot(
        // 表のタイトルの指定
        {name: 'Horsepower vs Miles Per Gallon'},
        // 上記で生成した座標情報オブジェクトを各要素とする配列を指定
        {values}, 
        // 表のx軸y軸のタイトルおよび表の高さを指定
        {
            xLabel: 'Horsepower',
            yLabel: 'Miles Per Gallon',
            height: 300
        }
    );

    // 元データを非同期で読み込む関数
    async function getData() {

        // 非同期でクルマのスペックデータ(内容はオブジェクトを要素に持つ配列のフォーマットをしている)を取得しcarsDaraに格納
        const carsDataReq = await fetch('https://storage.googleapis.com/tfjs-tutorials/carsData.json');
        // 読み込んだデータをJSON形式として読んで各クルマのスペック(オブジェクト)を要素として持つ配列carsDataとして格納
        const carsData = await carsDataReq.json();
        // 格納した配列carsDataデータから元データとして用いる2アイテムのみを取得しcleanedとして格納
        // map関数は配列の各要素に繰り返し指定された処理を行うので、cleanedはmpgとhorsepowerの情報のみを含むオブジェクトを要素とする配列
        // なお、mpgとhorsepowerのどちらかでも空文字の場合はデータから削除しておくfilterをかける
        const cleaned = carsData.map(car => ({
            mpg: car.Miles_per_Gallon,
            horsepower: car.Horsepower,
        }))
        .filter(car => (car.mpg != null && car.horsepower != null));

        return cleaned;

    }

    //******************************************************************
    // 2. createModel: モデルの枠組みの作成
    //******************************************************************

    // モデルを作成(createModelは下で関数として定義)
    const model = createModel();
    // 上記で作成したモデルの要約情報(Layer Name, Output Shape, # Of Params, Trainable)表示
    tfvis.show.modelSummary({name: 'Model Summary'}, model);

    // モデル作成の関数定義
    function createModel() {

        // シーケンシャルモデル(線形回帰モデル)の枠組みの作成
        // これはモデルが全体として線形回帰モデルになるという意味でなく、各ニューロンの入出力の関係が y=Σ(wx)+b と書ける線形回帰モデルであるということ
        const model = tf.sequential();

        // 入力層を追加
        model.add(tf.layers.dense({
            // 入力は 1x1 のテンソル(=スカラー)
            inputShape: [1],
            // ユニット(別名:ノード)は1個だけ
            units: 1,
            // y=Σ(wx)+b となる定数項bであるバイアスを使用する
            useBias: true
        }));

        // ここに中間層を追加した場合にはどうなるのかは別途に言及する

        // 出力層を追加
        model.add(tf.layers.dense({
            // ユニット(別名:ノード)は1個だけ
            units: 1,
            // y=Σ(wx)+b となる定数項bであるバイアスを使用する
            useBias: true
        }));

        return model;

    }

    //******************************************************************
    // 3. convertToTensor: 学習データを上記モデルに流し込めるようテンソルに変換する
    //******************************************************************

    // getDataで取得したclean済み配列データを下記で定義したconvertToTensor関数でテンソルに変換(不要なアイテムは同時にフィルター)
    const tensorData = convertToTensor(data);
    const {inputs, labels} = tensorData;

    // 学習データをテンソルに変換する
    function convertToTensor(data) {

        // tidyを使って計算することで、計算経過で生成される変数をメモリから削除してメモリにゴミを残さない
        return tf.tidy(() => {

            // (ステップ1) データをシャッフルする 

            tf.util.shuffle(data);

            // (ステップ2) データを配列に格納してから展開してテンソルに変換

            // 変数inputに馬力に関するデータ(実際は配列)を格納
            const inputs = data.map(d => d.horsepower)
            // 変数labelsに燃費に関するデータ(実際は配列)を格納
            const labels = data.map(d => d.mpg);

            // 上記で作成したインプット値(馬力)の配列を使って Nx1 の行列を生成
            const inputTensor = tf.tensor2d(inputs, [inputs.length, 1]);
            // 上記で作成される2次元テンソルは下記のような縦長の形
            // [[馬力の値1],
            //  [馬力の値2],
            //    :
            //  [馬力の値N]]

            // 上記で作成したラベル値(燃費)配列を使って Nx1 の行列を生成
            const labelTensor = tf.tensor2d(labels, [labels.length, 1]);
            // 上記で作成される2次元テンソルは下記のような縦長の形
            // [[燃費の値1],
            //  [燃費の値2],
            //    :
            //  [燃費の値N]]

            // (ステップ3) 入力データの値を0から1の間に正規化

            // 入力とラベルを最大値と最小値を調べて取得
            // 入力である馬力の最大値を取得
            const inputMax = inputTensor.max();
            // 入力である馬力の最小値を取得
            const inputMin = inputTensor.min(); 
            // 出力である燃費の最大値を取得 
            const labelMax = labelTensor.max();
            // 出力である燃費の最小値を取得 
            const labelMin = labelTensor.min();

            // 入力とラベルを正規化
            // inputTensor.sub(inputMin)で各入値から最小入力値をひく(=最小値をゼロに落とす) >> (これをAとする)
            // inputMax.sub(inputMin)で最大入力値から最小入力値をひく >> (これをBとする)
            // 上記の各(A)の値を(B)で割ることで、最大値が1で最小値が0になるよう正規化する
            const normalizedInputs = inputTensor.sub(inputMin).div(inputMax.sub(inputMin));
            // labelTensor.sub(labelMin)で各入値から最小入力値をひく(=最小値をゼロに落とす) >> (これをA'とする)
            // labelMax.sub(labelMin)で最大入力値から最小入力値をひく >> (これをB'とする)
            // 上記の各(A')の値を(B')で割ることで、最大値が1で最小値が0になるよう正規化する
            const normalizedLabels = labelTensor.sub(labelMin).div(labelMax.sub(labelMin));

            return {
                // 正規化された値を要素に持つ入力テンソルと出力(ラベル)テンソルを返す
                inputs: normalizedInputs,
                labels: normalizedLabels,
                // 入力と出力(ラベル)の最大値最小値もあとで逆正規化できるよう返す
                inputMax,
                inputMin,
                labelMax,
                labelMin,
            }
        });

    }

    //******************************************************************
    // trainModel: モデルの学習
    //******************************************************************

    // モデルの学習(trainModelは下で関数として定義)
    // awaitとすることでtrainModel関数からreturnがあるまで待機する
    await trainModel(model, inputs, labels);

    // モデル・入力(インプット)テンソル・出力(ラベル)テンソルを指定してモデルの学習を行う関数
    async function trainModel(model, inputs, labels) {

        // 学習実行のため、学習方法を指定してモデルをコンパイル  
        model.compile({
            // 最適化法をアダム(=適応モーメント推定法)に指定
            optimizer: tf.train.adam(),
            // 損失関数をMSE(=平均二乗誤差)に指定
            loss: tf.losses.meanSquaredError,
            // 学習とテストに用いる指標(この場合は平均二乗誤差)を表す表現を決める
            metrics: ['mse'],
        });

        // バッチサイズ(小分けにグループ分けした学習データに含まれるデータの個数)を28個とする
        const batchSize = 28;
        // 学習の1手順の回数を50回とする
        const epochs = 50;

        // epochsで指定した回数の学習手順回数(エポック)になるまで学習を実行する
        return await model.fit(inputs, labels, {
            batchSize,
            epochs,
            shuffle: true,
            // 学習結果の随時の描画用にTFVISに、コンパイル時に指定した指標の値をコールバックする指定
            callbacks: tfvis.show.fitCallbacks(
                // 描画する表のタイトル
                { name: 'Training Performance' },
                // 描画する指標(ここではlossとmseを改めて指定)
                ['loss', 'mse'], 
                { 
                    // 表の高さ
                    height: 200, 
                    // コールバックのタイミング
                    callbacks: ['onEpochEnd'] 
                }
            )
        });

    }

    // trainModel関数に返り値があった時点で規定したエポックが終了したことになるので「学習が終わった」とコンソールに出力
    console.log('Done Training');

    //******************************************************************
    // testModel: 学習ずみモデルに入力を与えて出力を得て、元データのプロットと重ねて違いを視覚的に見せる
    //******************************************************************

    // モデルのテスト(testModelは下で関数として定義)
    testModel(model, data, tensorData);


    function testModel(model, inputData, normalizationData) {

        const {inputMax, inputMin, labelMin, labelMax} = normalizationData;  

        // Generate predictions for a uniform range of numbers between 0 and 1;
        // We un-normalize the data by doing the inverse of the min-max scaling 
        // that we did earlier.
        const [xs, preds] = tf.tidy(() => {

            // tf.linespaceで0から1までの間に等間隔となる100個の値を生成(0, 0.01, 0.02, 0.03,・・, 0.98, 0.99)し格納
            // なお、tf.linspace()によって生成されるのは配列ではなく「Array.from()で配列化できるオブジェクト」である
            const xs = tf.linspace(0, 1, 100);
            // 上記で生成した100個の数値を要素にもつ行列を生成し、学習したモデルに予測値として出力させる
            const preds = model.predict(xs.reshape([100, 1]));
            // モデルの入出力は共に正規化されているので、これを元に戻す計算を行う
            const unNormXs = xs.mul(inputMax.sub(inputMin)).add(inputMin);
            const unNormPreds = preds.mul(labelMax.sub(labelMin)).add(labelMin);
            // 上記で非正規化されたデータは100行1列の行列になっているので、これらを単なる配列の形にする
            return [unNormXs.dataSync(), unNormPreds.dataSync()];

        });

        // 学習データのポイント(座標)を配列として格納
        // mapは配列を受け取って、指定した処理を行う
        // ここでは入力値inputDataとして「座標情報を持ったオブジェクト」を要素にもつdataを代入しているので、各要素から馬力と燃費のデータを読んで
        // ポイント(座標)をオブジェクト形式で表す要素を持つ配列の各要素として持たせている
        const originalPoints = inputData.map(d => (
            {
                x: d.horsepower,
                y: d.mpg,
            }
        ));

        // 学習させたモデルを使って算出した予測値のポイント(座標)をオブジェクト形式で表す要素を持つ配列として格納
        const predictedPoints = Array.from(xs).map((val, i) => {
            return {
                x: val,
                y: preds[i]
            }
        });

        // 上記で得た予測値および学習データ値のポイントをTFVISに渡してプロット表示
        tfvis.render.scatterplot(
            {
                // 表のタイトル
                name: 'Model Predictions vs Original Data'
            },{
                // originalPointsとpredictedPointsはポイントの座標をJSONで表現した文字列を要素に持つ配列
                values: [originalPoints, predictedPoints],
                series: ['original', 'predicted']
            },{
                // 縦軸と横軸の名称、表の高さ
                xLabel: 'Horsepower',
                yLabel: 'Miles Per Gallon',
                height: 300
            }
        );

    }

}

document.addEventListener('DOMContentLoaded', run);

2. 実行結果

TensorFlow.jsの処々の情報は可視化ライブラリであるTF-VISを使って表やグラフで可視化することができます。下記は上記のコードを実行した際に表示された情報です。

2.1. 学習データのプロット

学習データの「馬力(横軸:Horsepower)」と「燃費(縦軸:Miles Per Gallon)」の関係をプロットしたプロットチャートです。これを見ると、学習すべき馬力と燃費の関係は互いに反比例する関係にあることが伺えますが、直線的なの反比例関係ではなく、穏やかなカーブを描くような反比例であることが伺えます。

2.2. 学習の進行状況

下記は損失関数の値です。損失関数をmse(= Mean Squared Error = 平均二乗誤差)に指定し、学習のエポックごとにその値が降下していっていることがわかります。このコードでは学習を50エポックまでと指定したので横軸は50までとなっています。損失関数の値がこれで十分なのかどうかと言う話は別途に言及します。

Training Performance - mse.png

2.3. 学習したモデルによる出力予測

学習させたモデルに入力として「馬力」の値を与え、その出力として「予測(predict)される燃費」を得ました。その関係を先ほどの学習データのプロットチャートに重ねて表示したものがこれです。学習したモデルが出力したオレンジ色の点が直線的に並んでいます。

Model Predictions vs Original Data.png

3. 次回の話

TensorFlow.orgのチュートリアルは上記で終わりとなっているのですが、次回は上記で記述したコードと作成したモデルについて、「これで良いのでしたっけ?」と言う視点で考察と改造を行います。

と言うのは、学習データの「馬力(横軸:Horsepower)」と「燃費(縦軸:Miles Per Gallon)」の関係は直線的なの反比例関係ではなく、穏やかなカーブを描くような反比例であることが伺えます。しかしながら、学習させたモデルが予測した関係は直線的な正比例の傾向を示しています。もっと、正確に入力と出力の関係を学習させるにはどうすればイイのか?次回の投稿で考えていきます。


追伸: Machine Learning Tokyoと言うMachine Learningの日本最大のグループに参加しています。作業系の少人数会合を中心に顔を出しています。基本的に英語でのコミュニケーションとなっていますが、能力的にも人間的にもトップレベルの素晴らしい方々が参加されておられるので、機会がありましたら参加されることをオススメします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CornerNet-LiteのWindows 10での実行(インファレンスだけで力尽きました。)

はじめに

 CornerNet-Lite: Efficient Keypoint Based Object Detectionの作者の実装である、githubをWindows 10で動かしたので、ここに記しておきます。

1.CornerNet-Liteとは

 昨年発表された、CornerNet: Detecting Objects as Paired Keypointsを軽くしたもののようです。
 CornerNetは日本語での解説も一部ありますから、いろいろ探してみてください。私としては、Fig.1の、YOLOv3よりAPが高く、スピードが速いというところに魅力を感じました。まだまだ新しい方法ですので、派生のネットワークが出てくるのではないかと思います。

2.私のPC

  • Windows 10

  • CPU Intel Core i5-4570S 2.9GHz

  • RAM 16GB

  • GPU RTX2070 8GB

なんか微妙にしょぼいですね。。。GPUに見合うCPUとメモリが欲しい。。。すでに、Visual Studio Community 2017やらCUDA10(cuDNN含む)やらは入っているので、そのあたりは参考にならないので注意してください。(もう一台まっさらなPCで再構成できればいいのですが、そうもいきません。。。)

3.事前準備

私は、Anaconda環境を使いました。ものによってPythonのバージョンとかが微妙に違うので、仮想環境を使うのがよいと思います。Anacondaのインストールはググってください。本来ならば、CornerNet-Liteの環境を作ってからクローニングしますが、conda_packagelist.txtがあるはずなので、まずはクローニングします。

(base) F:\Users\sounansu\Anaconda3>git clone https://github.com/princeton-vl/CornerNet-Lite.git CornerNet-Lite

はい、CornerNet-Liteのディレクトリに移って、仮想環境を作りましょう。

(base) F:\Users\sounansu\Anaconda3\CornerNet-Lite>conda create --name CornerNet_Lite --file conda_packagelist.txt --channel pytorch
略
PackagesNotFoundError: The following packages are not available from current channels:
略

Windows用はpackagelist.txtにかかれたバージョン等がないのでしょう。
というわけで、地道に入れていきましょう。

(base) F:\Users\sounansu\Anaconda3\CornerNet-Lite>conda create --name CornerNet_Lite python==3.7.1 anaconda
略
done
#
# To activate this environment, use
#
#     $ conda activate CornerNet_Lite
#
# To deactivate an active environment, use
#
#     $ conda deactivate

仮想環境に移ります。

(base) F:\Users\sounansu\Anaconda3\CornerNet-Lite>conda activate CornerNet_Lite

(CornerNet_Lite) F:\Users\sounansu\Anaconda3\CornerNet-Lite>

PyTorchはPytorchサイトで、私は、Stable,windows,conda,python3.7,cuda10でconda install pytorch torchvision cudatoolkit=10.0 -c pytorchをえらびました。

(CornerNet_Lite) F:\Users\sounansu\Anaconda3\CornerNet-Lite>conda install pytorch torchvision cudatoolkit=10.0 -c pytorch

これで、事前準備は終わったこととして、CornerNet-Liteを入れていきましょう。

4.CornerNet-Liteの実行

すでに、git cloneしているので、Compiling Corner Pooling Layersから順番にやっていきましょう。

(CornerNet_Lite) F:\Users\sounansu\Anaconda3\CornerNet-Lite>cd core/models/py_utils/_cpools/
(CornerNet_Lite) F:\Users\sounansu\Anaconda3\CornerNet-Lite\core\models\py_utils\_cpools>python setup.py install --user
略
error: command 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Tools\\MSVC\\14.16.27023\\bin\\HostX86\\x64\\cl.exe' failed with exit status 2

エラーが出ましたが、issueのここを参考に、

diff --git a/core/models/py_utils/_cpools/src/bottom_pool.cpp b/core/models/py_u
tils/_cpools/src/bottom_pool.cpp
index 8a20a43..9f88c66 100644
--- a/core/models/py_utils/_cpools/src/bottom_pool.cpp
+++ b/core/models/py_utils/_cpools/src/bottom_pool.cpp
@@ -1,4 +1,4 @@
-#include <torch/torch.h>
+#include <torch/extension.h>

 #include <vector>

diff --git a/core/models/py_utils/_cpools/src/left_pool.cpp b/core/models/py_uti
ls/_cpools/src/left_pool.cpp
index c36fc1b..90b1e81 100644
--- a/core/models/py_utils/_cpools/src/left_pool.cpp
+++ b/core/models/py_utils/_cpools/src/left_pool.cpp
@@ -1,4 +1,4 @@
-#include <torch/torch.h>
+#include <torch/extension.h>

 #include <vector>

diff --git a/core/models/py_utils/_cpools/src/right_pool.cpp b/core/models/py_ut
ils/_cpools/src/right_pool.cpp
index 1b2da43..2ff98e1 100644
--- a/core/models/py_utils/_cpools/src/right_pool.cpp
+++ b/core/models/py_utils/_cpools/src/right_pool.cpp
@@ -1,4 +1,4 @@
-#include <torch/torch.h>
+#include <torch/extension.h>

 #include <vector>

diff --git a/core/models/py_utils/_cpools/src/top_pool.cpp b/core/models/py_util
s/_cpools/src/top_pool.cpp
index bc63c49..6719357 100644
--- a/core/models/py_utils/_cpools/src/top_pool.cpp
+++ b/core/models/py_utils/_cpools/src/top_pool.cpp
@@ -1,4 +1,4 @@
-#include <torch/torch.h>
+#include <torch/extension.h>

 #include <vector>

のように修正しました。もう一度コーナープーリングをコンパイルしましょう。

(CornerNet_Lite) F:\Users\sounansu\Anaconda3\CornerNet-Lite\core\models\py_utils\_cpools>python setup.py install --user
略
Finished processing dependencies for cpools==0.0.0

うまくいったようです。次は、Compiling NMSです。

(CornerNet_Lite) F:\Users\sounansu\Anaconda3\CornerNet-Lite\core\models\py_utils\_cpools>cd ../../../external

(CornerNet_Lite) F:\Users\sounansu\Anaconda3\CornerNet-Lite\core\external>make
      1 [main] make 8172 find_fast_cwd: WARNING: Couldn't compute FAST_CWD pointer.  Please report this problem to
the public mailing list cygwin@cygwin.com
python setup.py build_ext --inplace
make: python: Command not found
Makefile:2: recipe for target `all' failed
make: *** [all] Error 127

makefileのどこがわるのかよくわからないので、これまたissueのここを参照して、

(CornerNet_Lite) F:\Users\sounansu\Anaconda3\CornerNet-Lite\core\external>python setup.py build_ext --inplace
略
error: command 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\Community\\VC\\Tools\\MSVC\\14.16.27023\\bin\\HostX86\\x64\\cl.exe' failed with exit status 2

これも、issueにあって

index ca3bd04..c1dfb4c 100644
--- a/core/external/setup.py
+++ b/core/external/setup.py
@@ -7,12 +7,12 @@ extensions = [
     Extension(
         "bbox",
         ["bbox.pyx"],
-        extra_compile_args=["-Wno-cpp", "-Wno-unused-function"]
+        extra_compile_args={'gcc': ['/Qstd=c99']}
     ),
     Extension(
         "nms",
         ["nms.pyx"],
-        extra_compile_args=["-Wno-cpp", "-Wno-unused-function"]
+        extra_compile_args={'gcc': ['/Qstd=c99']}
     )
 ]

のように修正しました。あらためて実行すると、

(CornerNet_Lite) F:\Users\sounansu\Anaconda3\CornerNet-Lite\core\external>python setup.py build_ext --inplace
略
コード生成が終了しました。

次に、まずは、出来合いのウエイトデータでインファレンスが動くかやってみましょう。Downloading Modelsに書かれている、google driveからデータをダウンロードして、<CornerNet-Lite dir>/cache/nnet/CornerNet_Saccade/,<CornerNet-Lite dir>/cache/nnet/CornerNet_Squeeze/,<CornerNet-Lite dir>/cache/nnet/CornerNet/にそれぞれ入れてください。

これで準備がおわったので、Running the Demo Scriptにあるように、python demo.pyを実行してみましょう。

(CornerNet_Lite) F:\Users\sounansu\Anaconda3\CornerNet-Lite>python demo.py
Traceback (most recent call last):
  File "demo.py", line 3, in <module>
    import cv2
ModuleNotFoundError: No module named 'cv2'

opencvを入れましょう。

(CornerNet_Lite) F:\Users\sounansu\Anaconda3\CornerNet-Lite>conda install opencv

さて、またpython demo.pyを実行してみましょう。

(CornerNet_Lite) F:\Users\sounansu\Anaconda3\CornerNet-Lite>python demo.py
total parameters: 116969339
loading from F:\Users\sounansu\Anaconda3\CornerNet-Lite\core\..\cache\nnet\CornerNet_Saccade\CornerNet_Saccade_500000.pkl
F:\Users\sounansu\Anaconda3\envs\CornerNet_Lite\lib\site-packages\torch\nn\functional.py:2539: UserWarning: Default upsampling behavior when mode=bilinear is changed to align_corners=False since 0.4.0. Please specify align_corners=True if the old behavior is desired. See the documentation of nn.Upsample for details.
  "See the documentation of nn.Upsample for details.".format(mode))
(CornerNet_Lite) F:\Users\sounansu\Anaconda3\CornerNet-Lite>dir demo*.jpg
 ドライブ F のボリューム ラベルは 記憶域 です
 ボリューム シリアル番号は ACCF-E884 です

 F:\Users\sounansu\Anaconda3\CornerNet-Lite のディレクトリ

2019/05/10  22:29           323,088 demo.jpg
2019/05/12  14:38           219,824 demo_out.jpg
               2 個のファイル             542,912 バイト

実行結果を置いておきます。
demo_out.jpg
うまくいっているようです。

おわりに

 一応これでインファレンスができるようになりました。途中git make等もinstallしました。すでに、cudaやVSCをインストールしていたので、そのあたりでのつまずきはなかったです。

 もし、うまくいかないとかありましたら、コメントに書いてくださいましたら、わかる範囲でお答えいたします。トレーニングもできるようになっているのですが、いろいろと、トラップが存在します。がんばってトレーニングも投稿しますので、、、期待せずに待っていてください。

では。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TensorFlow.jsでDeepLearning(初めの整理)

1. 初めに

こんにちわ。Electric Blue Industries Ltd.という、ITで美を追求するファンキーでマニアックなIT企業のマッツと申します。このところ投稿が疎かになったので、何かお役に立てることをと思い、TensorFlow.jsの解説を不定期することにしました。

Deep Learningをおこなううえで最もメジャーな方法のひとつが「仮想環境やコンテナを作ってTensorFlow & Python」かと思います。Google Colabと言う素晴らしい環境もありますが、「はじめの一歩」にはまだまだ敷居が高いところがあり、初学者には気軽に取り組める状況ではないように思います。

そんな中で特に初学者にオススメしたいのが「TensorFlow.js」です。TensorFlow.jsはJavaScriptでDeep Learningをおこなうためのライブラリです。機能的にはPython版のTensorFlowと基本的に足並みを揃えています(TFGANのような目新しい関数はTensorFlow.jsにはなかったりしますが)。学習に際しての敷居が低く、教育現場での利用にも有効と認識しています。メリット・デメリットは下記かと思います。

メリット

  • Webブラウザがあれば即に動作させられます
  • Webページを作成する際に慣れ親しんだ人も多いJavaScriptでの記述
  • PythonとJavaScriptは記述に類似性があるので思考的に同時並行が楽
  • GPUの使用もブラウザのWebGL経由で行える
  • ウェブサイトへの組み込みが容易でウェブコンテンツ化がしやすい
  • デバイスのカメラやマイクとの連動が簡単
  • TensorBoardに相当するような可視化ライブラリ「TF-VIS」もあり視覚化できる
  • Pythonとの相互変換(コンバーター)も使用可能
  • モデルのインポートとエクスポートも可能

デメリット

  • 日進月歩のDeepLearning業界の進化ペースに微妙にキャッチアップし遅れている
  • 大規模な学習処理にはやっぱりちょっと重いかも

2. この投稿の目的

今後に複数回の投稿に分けて、TensorFlow.orgから公開されているTensorFlow.jsを用いたチュートリアルを、非常に基本的な事項についてまで細かく詳細に説明し、何をしているのか流れを理解するお手伝いをします。狙いとしては、どのようなデータ(配列・オブジェクト・テンソル等)がどのように処理されて何が行われるのかを理解できるようになることです。

3. Deep Learningで頻出するJavaScriptのおさらい

次回からの各チュートリアルの解説をする前に、ウェブサイトでのJavaScriptではあまり登場しない処理について言及します。これらを理解していることが解説を読む上での前提となるので、不明事項はあらかじめ学習をお願いしたいです。

3.1. 変数と宣言(var, let, const)

JavaScriptでは変数を用いる際には使い始めに変数を宣言してから用います。宣言のタイプにはいくつかあり、ウェブサイト内で記述する場合はほぼvarしか使いませんが、Deep Learningではletやconstが多用されます。

var : 通常の変数宣言(上書き可能)
let : ifやforといったブロックスコープ内でのみ有効(上書き可能)
const : ifやforといったブロックスコープ内でのみ有効(上書き不可能)

3.2. 非同期処理(async, await)

asyncは指定された処理を非同期で行うことを宣言し、awaitはasyncで指定された処理が実行結果を返す(コールバック)するまで待つことを宣言します。これもウェブサイト作成では滅多に出てこないものです。

3.3. データのマッピング(map)

JavaScriptのmapは配列データに使うメソッドであり、配列データの各要素1つずつに対し指定されたコールバック処理を実行し、その結果を新しい配列として返すことが出来ます(見かけからはピンと来ないが繰り返し処理をしている状況)。なお、mapと類似するメソッドにfilterとreduceがあるが、ここでは割愛する。

map_1.js
// 1から5までの5つの整数を格納した、要素が5つの配列をitemsと言う名称で作成。
const items = [1, 2, 3, 4, 5];

// 上記の配列itemsの各要素について、各配列要素をvalueとしてfunctionで規定された処理をおこなう。
const result = items.map(function(value) {

    //配列の各要素を2倍
    return value*2;

});

// ブラウザの「開発者ツール(inspect)」のconsole画面にresultの内容が出力されます。
console.log(result);

// resultは配列となり、下記の要素が格納される
// [2, 4, 6, 8, 10]

さらに、インデックス(配列の先頭要素(0番)から何番目か)の番号も使って下記のようにオブジェクトを要素に持たせることもでき、この形式はTensorFlow.jsでのデータ処理に頻出するものです。

map_2.js
const items = [1, 2, 3, 4, 5];

const result = items.map(function(value, index) {

    //valueとindexを含むオブジェクトを要素とする配列を生成
    return {
        value,
        index
    };

});

console.log(result);

// resultは配列となり、下記のvalueとindexを含むオブジェクトを各要素が格納される
// [{value: 1, index: 0}, {value: 2, index: 1}, {value: 3, index: 2}, {value: 4, index: 3}, {value: 5, index: 4}]

3.4 配列の生成(Array.from)

Array.from(x)は下記のように「配列っぽい(インデックス、要素、要素数が記録された)」オブジェクトから正真正銘の配列を生成するメソッドです。

arrayFrom_1.js
const items = {0: 11, 1: 22, 2: 33, length: 3};

const result = Array.from(items);

console.log(result);

// resultには下記の配列が格納される
// [11, 22, 33]

配列の要素にオブジェクトを格納させるべく、上記で述べたmapと併用して用いる場合は

arrayFrom_2.js
const items = {0: 11, 1: 22, 2: 33, length: 3};

const result = Array.from(items).map((value, index) => {
    return {
        x: value,
        y: index
    }
});

console.log(result);

// resultには下記のオブジェクトを要素とする配列が格納される
// [{x: 11, y: 0}, {x: 22, y: 1}, {x: 33, y: 2}]

のように記述します。この処理はDeep Learningの学習データとテストデータの生成に頻出する処理です。

さらに、オブジェクトの中にオブジェクトが入ったような形式の配列もどきの場合は、配列もどきオブジェクトの各要素に対して処理を行わせるべくmapと合わせると下記のようにハンドルできます。

arrayFrom_3.js
const items = {0: {alpha: 11, bravo:111}, 1: {alpha: 22, bravo: 222} , 2: {alpha: 33, bravo: 333}, length: 3};

const result = Array.from(items).map(d => {
    return {
        x: d.alpha,
        y: d.bravo
    }
});

console.log(result);

// resultは下記のオブジェクトを要素とする配列になる。
// [{x: 11, y: 111}, {x: 22, y: 222}, {x: 33, y: 333}]

Deep Learningの学習で最も難しく感じるのは、このように「配列」「オブジェクト」が入れ子になり、かつ多次元行列の要素となってデータが扱われるところかと思います。データを処理する際には「このデータはどのような構造になっているか」を確認しながら処理を理解することが、その後の汎用的なDeep Learning実施に肝要です。

4. TensorFlow.jsで行列を扱う

4.1. ブラウザでTensorFlow.jsが動くようにする

ブラウザ(Google Chromeの最新版を推奨)でTensorFlow.jsが動くようにするには、下記のHTMLファイルを作成し、それと同じフォルダに置いた script.js というJavaScriptを書いたファイルにJaveScriptでコードを記述することで行います(もちろんファイル名は別の名称でも良いです)。実行結果は「開発者ツール(inspect)」のコンソール画面に表示されます。

index.html
<!DOCTYPE html>
<html>
<head>
  <title>TensorFlow.js Tutorial</title>
    <!-- TensorFlow.jsのインポート -->
    <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.js"></script>
    <!-- JavaScriptを書くscript.jsのインポート -->
    <script type="text/javascript" src="script.js"></script>
</head>
<body></body>
</html>

4.2. 行列を作る

下記の方法1から3は全て同じ

a =
\begin{bmatrix}
1 & 2 \\
3 & 4 \\
5 & 6
\end{bmatrix}

という行列aを作ります。

方法1:各行ごとにまとめて指定することを機械的に繰り返す。

cretaeMatrix_1.js
const a = tf.tensor([[1, 2], [3, 4], [5, 6]]);

方法2:行列の要素を左上から機械的に続けて指定し、行列の形(m行 x n列)を指定する。

cretaeMatrix_2.js
const shape = [3, 2];
const a = tf.tensor([1, 2, 3, 4, 5, 6], shape);

方法3:方法1と方法2の合わせ技(なお、画像や音声の学習ではデータが浮動小数になるのでint32などと明示的に指定することも多いので、これは意外と使います)。

cretaeMatrix_3.js
const a = tf.tensor([[1, 2], [3, 4], [5, 6]], [3, 2], 'int32');

4.3. 行列の変形

  • reshape: 上記で作成した行列aに対して、shapeでTensorの再形成を行い行列bを生成
reshape_1.js
// 2行3列に変形
const b = a.reshape([2, 3]);
b =
\begin{bmatrix}
1 & 2 & 3 \\
4 & 5 & 6
\end{bmatrix}
  • transpose: 上記で作成した行列aに対して、transposeで行列の行と列を転置して転置行列bを生成する
transpose_1.js
// 行と列を対角線を軸としてくるっとひっくり返し(転置)
const b = a.transpose();
b =
\begin{bmatrix}
1 & 3 & 5 \\
2 & 4 & 6
\end{bmatrix}

4.4. 行列の要素を得る

  • arraySyncで行列aの各行を要素とする配列を得る
arraySync_1.js
b = a.arraySync();
b =
\begin{bmatrix}
\begin{bmatrix}
1 & 2 
\end{bmatrix} , 
\begin{bmatrix}
3 & 4 
\end{bmatrix} , 
\begin{bmatrix}
5 & 6 
\end{bmatrix}
\end{bmatrix}
  • dataSyncで行列aの各値を要素とする配列を得る
dataSync_1.js
b = a.dataSync();
b =
\begin{bmatrix}
1, 2, 3, 4, 5, 6 
\end{bmatrix}

4.5. 行列の算術計算

  • 行列xの各要素の二乗を得る
square_1.js
const x = tf.tensor([1, 2, 3, 4]);
const y = x.square();
y =
\begin{bmatrix}
1, 4, 9, 16 
\end{bmatrix}

ということで、今後のTensorFlow.jsの詳細解説を前に、今回はベースとなるJavaScriptのおさらいまでをしました。空いた時間をうまく使って、シュウイチくらいでは続きを投稿していく予定です。どうぞよろしくお願いします。

追伸: Machine Learning Tokyoと言うMachine Learningの日本最大のグループに参加しています。作業系の少人数会合を中心に顔を出しています。基本的に英語でのコミュニケーションとなっていますが、能力的にも人間的にもトップレベルの素晴らしい方々が参加されておられるので、機会がありましたら参加されることをオススメします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む