ディープラーニングの入門ガイド

ディープラーニングの入門ガイド

1. はじめに

この記事は、私がディープラーニングを始める際に経験したプロセスをまとめたものです。2020年の集創賽で神経ネットワークアルゴリズムの開発を担当した際の経験も含んでいます。

2020年初頭、私はディープラーニングの応用を試みるためのプロジェクトを立ち上げました。その課題はハンドジェスチャーセンシングシステムの実現でした。そのため、畳み込みニューラルネットワーク(CNN)を使用することを考えました。しかし、学校で関連する授業を受けたことがなかったため、基本的な知識が不足していました。それにもかかわらず、いくつかのブログやビデオを見て学び始めました。

結果的に、3ヶ月程度で基本的な理解と簡単なデモの作成、そして競技用のコードの完成に成功しました。この記事では、私の学習経路に沿って理論、実験、実践の3つの部分について説明します。また、その後のプロジェクトや理論学習で得たより広範な視点も紹介します。

2. 理論部分

一般的な入門方法として『西瓜書』や『花書』、または吴恩達のオンラインコースがありますが、私の場合は『白話ディープラーニングとTensorflow』という本を参考にしました。ただし、この本にはTensorflow1が使われており、現在では使用されていません。最新のPyTorchベースの本を選び、理論と実装を両方学ぶのがおすすめです。数学の深掘りは必要ないと思っています。ニューラルネットワークの基本概念を理解するには微積分、線形代数、確率論の基礎があれば十分です。

ディープラーニングの主な分野はコンピュータビジョン(CV)と自然言語処理(NLP)であり、強化学習(RL)も一部影響を与えています。これらの分野では、古典的な手法とディープラーニングの手法を両方学ぶことで、学習効率が向上します。特に、伝統的な特徴抽出と線形分類器の方式がどのように置き換えられたのか、どの状況で有効なのか、どの状況で不向きなのかを理解することが重要です。

要約:

  • ディープラーニングに関する簡潔な本を選ぶ。
  • CVとNLPの授業を受講し、古典的アプローチとディープラーニングの方法を学ぶ。

3. 実験部分

まずPythonを学びます。ディープラーニングフレームワークの主流言語はPythonです。解釈型言語であるため、学習コストは低く、環境構築もパッケージマネージャーによって迅速に行えます。

次に、どのフレームワークを学ぶかですが、PyTorchを優先すべきです。学術界で広く使われており、多くのモデルがGitHubでPyTorchで実装されています。TensorFlow2など他のフレームワークも興味がある場合は学ぶことができます。ただし、APIは異なりますが、基本的な考え方は同じです。

フレームワークをインストールしたら、いくつかのデモを実行して練習しましょう。MNISTはディープラーニングの「Hello World」として最も適しています。自分のフレームワークでMNISTの手書き数字認識を実装するチュートリアルを見つけて、ステップバイステップで進めてください。

要約:

  • Pythonを学び、基本的な構文を習得する。
  • ディープラーニングフレームワークを選択し、基本的なAPIを学ぶ。
  • MNISTの手書き数字認識を実装する。

4. 実戦部分

ここが本格的な実践です。私の経験では、3日坊主のようなペースで1か月かけて理論を学び、最初のディープラーニングプログラム(MNIST)を作成しました。その後、必要なデータセットを作成し、CNNを設計・訓練し、重みをエクスポートしてFPGAに配置する必要があります。

  • データセットの作成:

ハンドジェスチャー認識のタスクを行うために、インターネットから公開されているデータセットを探しましたが、ニーズに合致するものが見つかりませんでした。軽量で6種類のジェスチャーがあり、単一チャネルでMNISTのような形式のデータセットが必要でした。最終的にOpenCVを使って自動収集、リサイズ、二値化のスクリプトを作成し、ニーズに合わせました。

各クラスの画像を約2000枚撮影し、回転や反転などのデータ拡張によりそれぞれ8枚に増やしました。その後、トレーニングセットとテストセットに2:8の比率で分割しました。このプロセスは労働的でしたが、非常に面白い体験でした。

  • ネットワーク設計&訓練

以前のCNN+MNISTプロジェクトをカスタマイズし、ハードウェアチームと協力しながらネットワーク構造のパラメータとハードウェアリソースの使用量を調整しました。最終的に非常に小さなネットワークを生成しました。トレーニング自体は簡単にでき、MNISTを読み込む部分を自分のデータセットに置き換えるだけで完了しました。トレーニングが収束しない問題に遭遇しましたが、バッチサイズを小さくし、エポック数を大きくすることで改善しました。150エポック後には90%の精度に到達し、満足しました。さらに小規模な調整を行った後、ネットワークを確定させました。

以下は古くなったコード例です。今では参考にならないものの、他のコードを参考にして自分に合わせて変更する姿勢は学べます。

DatasetGenerate.py

import numpy as np
import os
import tensorflow as tf
from PIL import Image

# small dataset ----> train/test
# cwd = 'D:/PycharmProjects/CNNHandGestureRecognization/Dataset_Final/train'
# cwd = 'D:/PycharmProjects/CNNHandGestureRecognization/Dataset_Final/test'

# enhanced dataset ------> train enhanced/test enhanced
# cwd = 'D:/PycharmProjects/CNNHandGestureRecognization/Dataset_Final_Enhanced/train'
cwd = 'D:/PycharmProjects/CNNHandGestureRecognization/Dataset_Final_Enhanced/test'

classes = ['1', '2', '3', '4', '5', '6']

# TFRecordsデータを作成
def create_record():
    # writer = tf.python_io.TFRecordWriter("train.tfrecords")
    # writer = tf.python_io.TFRecordWriter("test.tfrecords")

    # writer = tf.python_io.TFRecordWriter("train_enhanced.tfrecords")
    writer = tf.python_io.TFRecordWriter("test_enhanced.tfrecords")
    for index, name in enumerate(classes):
        class_path = cwd + "/" + name + "/"
        for img_name in os.listdir(class_path):
            img_path = class_path + img_name
            img = Image.open(img_path)
            img = img.resize((64, 64))
            img_raw = img.tobytes()
            print(img_path, index)
            example = tf.train.Example(
                features=tf.train.Features(feature={
                    "label": tf.train.Feature(int64_list=tf.train.Int64List(value=[index])),
                    'img_raw': tf.train.Feature(bytes_list=tf.train.BytesList(value=[img_raw]))
                }))
            writer.write(example.SerializeToString())
    writer.close()


# -------------------------------------------------------------------------

# 二進データを読み込む

def read_and_decode(filename):
    # ファイルキューを作成
    filename_queue = tf.train.string_input_producer([filename])
    # リーダーを作成
    reader = tf.TFRecordReader()
    # ファイルキューからシリアライズされたサンプルを読み込む
    _, serialized_example = reader.read(filename_queue)
    # シリアライズされたサンプルからフィーチャーを解析
    features = tf.parse_single_example(
        serialized_example,
        features={
            'label': tf.FixedLenFeature([], tf.int64),
            'img_raw': tf.FixedLenFeature([], tf.string)
        })
    label = features['label']
    img = features['img_raw']
    img = tf.decode_raw(img, tf.uint8)
    img = tf.reshape(img, [64, 64, 1])
    # 画像を表示する場合、下記の行をコメントアウト
    img = tf.cast(img, tf.float32) * (1. / 255)
    label = tf.cast(label, tf.int32)
    return img, label


# --------------------------------------------------------------------------
# ---------メインプログラム----------------------------------------------------------
if __name__ == '__main__':
    create_record()
    # batch = read_and_decode('train.tfrecords')
    # batch = read_and_decode('test.tfrecords')
    # batch = read_and_decode('train_enhanced.tfrecords')
    batch = read_and_decode('test_enhanced.tfrecords')

    init_op = tf.group(tf.global_variables_initializer(), tf.local_variables_initializer())

    with tf.Session() as sess:  # セッションを開始
        sess.run(init_op)
        coord = tf.train.Coordinator()
        threads = tf.train.start_queue_runners(coord=coord)
        for i in range(339):
            example, lab = sess.run(batch)  # セッションで画像とラベルを取得
            # 画像を生成する場合、read_and_decode内でreshape([64,64])に変更
            # img = Image.fromarray(example, 'L')  # これは前で説明したImage
            # img.save(cwd + '/test/' + str(i) + '_Label_' + str(lab) + '.png')  # 画像を保存; 注意:cwdの後に'/'を追加
            print(example, lab)
        coord.request_stop()
        coord.join(threads)
        sess.close()

Train.py

import tensorflow as tf
import numpy as np
import DatasetGenerate

epoch = 150
batch_size = 1000
dataset_size = 72000

def one_hot(labels, Label_class):
    one_hot_label = np.array([[int(i == int(labels[j])) for i in range(Label_class)] for j in range(len(labels))])
    return one_hot_label

# 畳み込み層
def conv2d(x, W):
    return tf.nn.conv2d(x, W, strides=[1,1,1,1], padding='SAME')

# マックスプール層
def max_pool_4x4(x):
    return tf.nn.max_pool(x, ksize=[1,4,4,1], strides=[1,4,4,1], padding='SAME')

# 入力
x = tf.placeholder(tf.float32, [batch_size,64,64,1])
y_ = tf.placeholder(tf.float32, [batch_size,6])

# 最初の畳み込みとプールレイヤー
W_conv1 = tf.Variable(tf.truncated_normal([3,3,1,4], stddev = 0.02), name="w1")
b_conv1 = tf.Variable(tf.constant(0.0, shape=[4]), name="b1")
h_conv1 = tf.nn.relu(conv2d(x, W_conv1) + b_conv1)
h_pool1 = max_pool_4x4(h_conv1)


# 次の畳み込みとプールレイヤー
W_conv2 = tf.Variable(tf.truncated_normal([3,3,4,2], stddev = 0.02), name="w2")
b_conv2 = tf.Variable(tf.constant(0.0, shape=[2]), name="b2")
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_4x4(h_conv2)

# 全結合層に変換
reshape = tf.reshape(h_pool2,[batch_size, -1])
dim = reshape.get_shape()[1].value
W_fc1 = tf.Variable(tf.truncated_normal([dim, 6], stddev = 0.02), name="wfc1")
b_fc1 = tf.Variable(tf.constant(0.0, shape=[6]), name="bfc1")
y_conv = tf.nn.softmax(tf.matmul(reshape, W_fc1) + b_fc1)

# 損失関数と最適化アルゴリズム
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y_ * tf.log(y_conv), reduction_indices=[1]))
train_step = tf.train.AdamOptimizer(0.0001).minimize(cross_entropy)

correct_prediction = tf.equal(tf.argmax(y_conv,1),tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction,tf.float32))

# セーバー
saver = tf.train.Saver()

img, label = DatasetGenerate.read_and_decode("train_enhanced.tfrecords")
img_test, label_test = DatasetGenerate.read_and_decode("test_enhanced.tfrecords")

# 使用shuffle_batchで入力をランダムにシャッフル
img_batch, label_batch = tf.train.shuffle_batch([img, label],
                                                batch_size=batch_size, capacity=2000,
                                                min_after_dequeue=1000)
img_test, label_test = tf.train.shuffle_batch([img_test, label_test],
                                                batch_size=batch_size, capacity=2000,
                                                min_after_dequeue=1000)
init = tf.initialize_all_variables()
t_vars = tf.trainable_variables()
print(t_vars)
with tf.Session() as sess:
    sess.run(init)
    coord = tf.train.Coordinator()
    threads=tf.train.start_queue_runners(sess=sess,coord=coord)
    batch_idxs = int(dataset_size/batch_size)
    for i in range(epoch):
        for j in range(batch_idxs):
            val, l = sess.run([img_batch, label_batch])
            l = one_hot(l,6)
            _, acc = sess.run([train_step, accuracy], feed_dict={x: val, y_: l})
            print("Epoch:[%4d] [%4d/%4d], accuracy:[%.8f]" % (i, j, batch_idxs, acc) )
        saver.save(sess, "./Model2/model.ckpt")
    val, l = sess.run([img_test, label_test])
    l = one_hot(l,6)
    print(l)
    y, acc = sess.run([y_conv, accuracy], feed_dict={x: val, y_: l})
    print(y)
    print("test accuracy: [%.8f]" % (acc))

    coord.request_stop()
    coord.join(threads)

Predict.py

import tensorflow as tf
import numpy as np
from PIL import Image
import os
import glob

path = './Dataset_Final_Enhanced/1/2.png'

precision = 1

np.set_printoptions(threshold=np.inf)

def get_image_paths(folder):
    return glob.glob(os.path.join(folder, '*.png'))

# 畳み込み層
def conv2d(x,W):
    return tf.nn.conv2d(x, W, strides=[1,1,1,1], padding='SAME')

# マックスプール層
def max_pool_4x4(x):
    return tf.nn.max_pool(x, ksize=[1,4,4,1], strides=[1,4,4,1], padding='SAME')

sess = tf.Session()
# まずメタグラフをロードし、重みを復元
saver = tf.train.import_meta_graph('./Model/model.ckpt.meta')
saver.restore(sess, './Model/model.ckpt')
# グラフをアクセス
graph = tf.get_default_graph()
conv1_w = sess.run(graph.get_tensor_by_name('w1:0'))
conv1_b = sess.run(graph.get_tensor_by_name('b1:0'))
conv2_w = sess.run(graph.get_tensor_by_name('w2:0'))
conv2_b = sess.run(graph.get_tensor_by_name('b2:0'))
fc1_w = sess.run(graph.get_tensor_by_name('wfc1:0'))
fc1_b = sess.run(graph.get_tensor_by_name('bfc1:0'))
conv1_w_1 = np.around(conv1_w, decimals=precision)
conv1_b_1 = np.around(conv1_b, decimals=precision)
conv2_w_1 = np.around(conv2_w, decimals=precision)
conv2_b_1 = np.around(conv2_b, decimals=precision)
fc1_w_1 = np.around(fc1_w, decimals=precision)
fc1_b_1 = np.around(fc1_b, decimals=precision)

x = tf.placeholder(tf.float32, [1,64,64,1])

# 最初の畳み込みとプールレイヤー
W_conv1 = tf.Variable(tf.truncated_normal([3,3,1,4], stddev = 0.02), name="w1")
b_conv1 = tf.Variable(tf.constant(0.0, shape=[4]), name="b1")
h_conv1 = tf.nn.relu(conv2d(x, W_conv1) + b_conv1)
h_pool1 = max_pool_4x4(h_conv1)


# 次の畳み込みとプールレイヤー
W_conv2 = tf.Variable(tf.truncated_normal([3,3,4,8], stddev = 0.02), name="w2")
b_conv2 = tf.Variable(tf.constant(0.0, shape=[8]), name="b2")
h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_4x4(h_conv2)

# 全結合層に変換
reshape = tf.reshape(h_pool2,[1, -1])
W_fc1 = tf.Variable(tf.truncated_normal([128, 6], stddev = 0.02), name="wfc1")
b_fc1 = tf.Variable(tf.constant(0.0, shape=[6]), name="bfc1")
h_out = tf.matmul(reshape, W_fc1) + b_fc1
y_conv = tf.nn.softmax(tf.matmul(reshape, W_fc1) + b_fc1)

def predict(image_path):
    image = Image.open(image_path).convert('L')
    image_array = np.array(image)
    image_array = image_array.astype(float)
    x_input = np.reshape(image_array,[1,64,64,1]) * (1./255)

    # h1 = sess.run(h_pool1, feed_dict={x:x_input,W_conv1:conv1_w_1, b_conv1:conv1_b_1})
    # print(h1)
    # h2 = sess.run(h_pool2, feed_dict={x:x_input,W_conv1:conv1_w_1, b_conv1:conv1_b_1, W_conv2:conv2_w_1, b_conv2:conv2_b_1})
    # print(h2)


    # print(image_path)
    # h3 = sess.run(h_out,
    #               feed_dict={x: x_input, W_conv1: conv1_w_1, b_conv1: conv1_b_1, W_conv2: conv2_w_1, b_conv2: conv2_b_1,
     #                         W_fc1: fc1_w_1, b_fc1: fc1_b_1})
    # print(h3)

    y = sess.run(y_conv, feed_dict={x:x_input,W_conv1:conv1_w_1, b_conv1:conv1_b_1, W_conv2:conv2_w_1, b_conv2:conv2_b_1, W_fc1:fc1_w_1, b_fc1:fc1_b_1})
    # print(image_path)
    # print(y)
    y_out = np.array(y)
    result = np.argmax(y_out)+1
    return result
    # print('predict result is',np.argmax(y_out)+1)

img_path = './Dataset_Final_Enhanced/test/1/'
imgs = get_image_paths(img_path)
sum = 0
correct = 0
for i in imgs:
    sum = sum + 1
    if(predict(i) == 1):
        correct = correct + 1
    # predict('./cnn_input.png')
    # predict('./Dataset_Final_Enhanced/test/1/'+str(i)+'.png')
    # predict('./Dataset_Final_Enhanced/test/4/' + str(i) + '.png')

print('accuracy is',correct/sum)

  • 重みのエクスポートと配置

この部分については詳細は省略します。ディープラーニングとは関係ありません。浮動小数点の重みをエクスポートし、C++でバイナリファイルに変換し、FPGAが読み込めるようにしました。この過程では、重みファイル内の各値がどの行列のどのチャンネルのどの行と列に対応しているかを整理する必要があります。C++で実装されたベンチマークを使用して順序を確認し、Pythonで計算した結果と一致していることを検証しました。その後、Verilogで対応する実装を行いました。

5. 結論

以上が私のディープラーニングの入門プロセスです。以降のプロジェクトは基本的にこのフローを繰り返しました。

要約:ディープラーニングを用いてタスクを解決するには、

  • 関連するオープンソースデータセットはあるか?
    • ある → 取り込み、使い方を理解する
    • ない → 自分のニーズに合わせて作成する
  • 現在のネットワークはあるか?
    • ある → 取り込み、使い方を理解する
    • ない → 自分のニーズに合わせて構築/カットする
  • ネットワークが収束しない場合どうするか?
    • コードをチェックする
    • 超パラメータを調整する
    • TensorBoardなどのツールを利用して問題を特定する
  • 重みをエクスポートして配置するには?
    • 使用するハードウェアプラットフォームに応じて重み/モデルの形式をエクスポートする
    • ツールや自分で実装したコードで配置する

以上が私のディープラーニングの入門経路です。参考になりますよう願っています。ご意見をお待ちしています。

タグ: TensorFlow PyTorch CNN データセット作成 モデル部署

6月21日 20:30 投稿