GitHubのリポジトリはこちらです。
github.com
なんとか「ゼロから作るDeep Learning」を読み終わり、なんとなく理解したところで、まずは実装してみようと思いました。
画像認識や自然言語処理、音声認識などなどありますが、CNNについて勉強したのと割と定番でもあるので画像認識の実装をしようと思いました。
ライブラリ(フレームワーク?)でよく使われる定番としては
・TensorFlow
・Chainer
・PyTorch
があると思います。
今回は、一番使用率が高いであろうTensorFlowを使いました。
環境
・Windows10
・Python3.6
・TensowFlow1.8.0
・PyCharm2018.1.2
ゆるキャンΔはJKがただキャンプをするだけのアニメです
しまりんが好きです
今回はなでしこ/しまりん/その他キャラクターで分類しましたが、二値分類にしないなら全員分類してもよかったのではと思っています
なおゆるキャンΔを選んだ理由は(たぶん)誰もまだブログでは実装して公開していなかったので・・・
参考記事(パクリ記事)
画像取得と顔の切り出しは
blog.aidemy.net
画像の水増し方法も参考になった記事があったが見つからない・・・(見つけたら追記します)
実装部分は
機械学習でNEW GAME!のキャラを判別してみた | こんにゃくの日記
手順としては
①画像を収集
②顔部分を切り出す
③データを増やす
④学習させる
⑤未知の画像を判定させる
となっています。
最終的なディレクトリ構成はGitHubのリポジトリを見てください
(ブログだと綺麗に表示できなかった)
①画像を収集
これはGoogle Custom Search APIでさくっと集められました。
import urllib.request from urllib.parse import quote import httplib2 import json import os import cv2 import sys import shutil # keywordsの画像のurlを取得後、jpg画像に変換しファイルにどんどん入れてく # 全5人100個ずつ取得 API_KEY = "" CUSTOM_SEARCH_ENGINE = "" keywords = ["各務原なでしこ", "志摩リン", "斉藤恵那", "大垣千明", "犬山あおい"] def get_image_url(search_item, total_num): img_list = [] i = 0 while i < total_num: query_img = "https://www.googleapis.com/customsearch/v1?key=" + API_KEY + "&cx=" + CUSTOM_SEARCH_ENGINE + "&num=" + str( 10 if (total_num - i) > 10 else (total_num - i)) + "&start=" + str(i + 1) + "&q=" + quote( search_item) + "&searchType=image" res = urllib.request.urlopen(query_img) data = json.loads(res.read().decode('utf-8')) for j in range(len(data["items"])): img_list.append(data["items"][j]["link"]) i += 10 return img_list def get_image(search_item, img_list, j): opener = urllib.request.build_opener() http = httplib2.Http(".cache") for i in range(len(img_list)): try: fn, ext = os.path.splitext(img_list[i]) print(img_list[i]) response, content = http.request(img_list[i]) filename = os.path.join("./origin_image", str("{0:02d}".format(j)) + "." + str(i) + ".jpg") with open(filename, 'wb') as f: f.write(content) except: print("failed to download the image.") continue for j in range(len(keywords)): print(keywords[j]) img_list = get_image_url(keywords[j], 100) get_image(keywords[j], img_list, j)
API_KEYとCUSTOM_SEARCH_ENGINEには各々取得したものを設定してください。
ただ無料の範囲内では一日あたり100クエリまでのようで、これだけで機械学習をするには課金しないといけなさそうです
今回は一度使って慣れることが目的なので無料で使いました。
ちなみに集めるとこんな感じになります。
可愛いし機械学習とかもうどうでもいいのでは?
と思ってしまいましたが、正気に戻って作業を続けます。
②顔部分を切り出す
これはOpenCVとAnimeFaceを使いました。
アニメ顔検出にlbpcascade_animeface.xmlはを使いました。ここからダウンロードできます
GitHub - nagadomi/lbpcascade_animeface: A Face detector for anime/manga using OpenCV
import numpy as np import cv2 import matplotlib.pyplot as plt import numpy as np from PIL import Image import glob import os # 元画像を取り出して顔部分を正方形で囲み、64×64pにリサイズ、別のファイルにどんどん入れてく in_dir = "./image/*" out_dir = "./face_image" in_jpg = glob.glob(in_dir) in_fileName = os.listdir("./image/") # print(in_jpg) # print(in_fileName) print(len(in_jpg)) for num in range(len(in_jpg)): image = cv2.imread(str(in_jpg[num])) if image is None: # print("Not open:", line) print("Not open") continue image_gs = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) cascade = cv2.CascadeClassifier("lbpcascade_animeface.xml") # 顔認識の実行 face_list = cascade.detectMultiScale(image_gs, scaleFactor=1.1, minNeighbors=2, minSize=(64, 64)) # 顔が1つ以上検出された時 if len(face_list) > 0: for rect in face_list: x, y, width, height = rect image = image[rect[1]:rect[1] + rect[3], rect[0]:rect[0] + rect[2]] if image.shape[0] < 64: continue image = cv2.resize(image, (64, 64)) # 顔が検出されなかった時 else: print("no face") continue print(image.shape) # 保存 # fileName=os.path.join(out_dir,str(in_fileName[num])+".jpg") fileName = os.path.join(out_dir, str(in_fileName[num])) cv2.imwrite(str(fileName), image) in_dir = "./face_image/*" in_jpg = glob.glob(in_dir) img_file_name_list = os.listdir("./face_image/") # img_file_name_listをシャッフル、そのうち2割をtest_imageディテクトリに入れる np.random.shuffle(in_jpg) import shutil for i in range(len(in_jpg) // 5): shutil.move(str(in_jpg[i]), "./test_image")
これで顔部分が繰り出された画像が生成されます。
ここで顔じゃないものが検出されていたら、予め削除しておきます。
③データを増やす
そしてこれだけではデータ数が少なすぎるため反転させたり回転させて増やします
import os from PIL import Image def readImg(imgName): try: img_src = Image.open("face_image/" + imgName) print("read img!") except: print("{} is not image file!".format(imgName)) img_src = 1 return img_src def spinImg(imgNames): for imgName in imgNames: img_src = readImg(imgName) if img_src == 1:continue else: #上下反転 tmp = img_src.transpose(Image.FLIP_TOP_BOTTOM) tmp.save("flipTB_" + imgName) #90度回転 tmp = img_src.transpose(Image.ROTATE_90) tmp.save("spin90_" + imgName) #270度回転 tmp = img_src.transpose(Image.ROTATE_270) tmp.save("spin270_" + imgName) #左右反転 tmp = img_src.transpose(Image.FLIP_LEFT_RIGHT) tmp.save("flipLR_" + imgName) print("{} is done!".format(imgName)) #read imgs names imgNames = os.listdir("face_image")#画像が保存されてるディレクトリへのpathを書きます print(imgNames) spinImg(imgNames)
かなり増やしましたが、しまりんで300枚いかないぐらいでなでしこは200枚いかないので数は少ないのですが、とりあえずこれで実装してみます
④学習させる
では実際に学習させていきます。
#!/usr/bin/env python import os import cv2 import numpy as np import tensorflow as tf path = os.getcwd() + '/data/' class_count = 0 folder_list = os.listdir(path) for folder in folder_list: class_count = class_count+1 NUM_CLASSES = class_count # 最初のアニメ顔切り出しのサイズに設定 IMAGE_SIZE = 56 IMAGE_PIXELS = IMAGE_SIZE*IMAGE_SIZE*3 flags = tf.app.flags FLAGS = flags.FLAGS flags.DEFINE_string('label', 'label.txt', 'File name of label') flags.DEFINE_string('train_dir', './tmp/data', 'Directory to put the training data.') flags.DEFINE_integer('max_steps', 120, 'Number of steps to run trainer.') flags.DEFINE_integer('batch_size', 20, 'Batch size' 'Must divide evenly into the dataset sizes.') # accuracyが変化しなかったため1e-4から変更しました flags.DEFINE_float('learning_rate', 1e-5, 'Initial learning rate.') # 予測モデルを作成する関数 def inference(images_placeholder, keep_prob): # 重みを標準偏差0.1の正規分布で初期化する def weight_variable(shape): initial = tf.truncated_normal(shape, stddev=0.1) return tf.Variable(initial) # バイアスを標準偏差0.1の正規分布で初期化 def bias_variable(shape): initial = tf.constant(0.1, shape=shape) return tf.Variable(initial) # 畳み込み層の作成 def conv2d(x, W): return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME') # プーリング層の作成 def max_pool_2x2(x): return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME') # 入力を56x56x3に変形 x_image = tf.reshape(images_placeholder, [-1, 56, 56, 3]) # 畳み込み層1の作成 with tf.name_scope('conv1') as scope: W_conv1 = weight_variable([3, 3, 3, 32]) b_conv1 = bias_variable([32]) h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1) # プーリング層1の作成 with tf.name_scope('pool1') as scope: h_pool1 = max_pool_2x2(h_conv1) # 畳み込み層2の作成 with tf.name_scope('conv2') as scope: W_conv2 = weight_variable([3, 3, 32, 64]) b_conv2 = bias_variable([64]) h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2) # プーリング層2の作成 with tf.name_scope('pool2') as scope: h_pool2 = max_pool_2x2(h_conv2) # 畳み込み層3の作成 with tf.name_scope('conv3') as scope: W_conv3 = weight_variable([3, 3, 64, 128]) b_conv3 = bias_variable([128]) h_conv3 = tf.nn.relu(conv2d(h_pool2, W_conv3) + b_conv3) # プーリング層3の作成 with tf.name_scope('pool3') as scope: h_pool3 = max_pool_2x2(h_conv3) # 全結合層1の作成 with tf.name_scope('fc1') as scope: W_fc1 = weight_variable([7*7*128, 1024]) b_fc1 = bias_variable([1024]) h_pool3_flat = tf.reshape(h_pool3, [-1, 7*7*128]) h_fc1 = tf.nn.relu(tf.matmul(h_pool3_flat, W_fc1) + b_fc1) h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob) # 全結合層2の作成 with tf.name_scope('fc2') as scope: W_fc2 = weight_variable([1024, NUM_CLASSES]) b_fc2 = bias_variable([NUM_CLASSES]) # ソフトマックス関数による正規化 with tf.name_scope('softmax') as scope: y_conv = tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2) return y_conv # lossを計算する関数 def loss(logits, labels): cross_entropy = -tf.reduce_sum(labels*tf.log(logits)) tf.summary.scalar("cross_entropy", cross_entropy) return cross_entropy # 訓練のOpを定義する関数 def training(loss, learning_rate): train_step = tf.train.AdamOptimizer(learning_rate).minimize(loss) return train_step # 正解率を計算する関数 def accuracy(logits, labels): correct_prediction = tf.equal(tf.argmax(logits, 1), tf.argmax(labels, 1)) accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float")) tf.summary.scalar("accuracy", accuracy) return accuracy if __name__ == '__main__': count = 0 folder_list = os.listdir(path) train_image = [] train_label = [] test_image = [] test_label = [] f = open(FLAGS.label, 'w') for folder in folder_list: subfolder = os.path.join(path, folder) file_list = os.listdir(subfolder) filemax = 0 for file in file_list: filemax = filemax + 1 # train : test = 9 : 1 file_rate = int(filemax/10*9) i = 0 for file in file_list: img = cv2.imread('./data/' + folder + '/' + file) img = cv2.resize(img, (IMAGE_SIZE, IMAGE_SIZE)) if i <= file_rate: train_image.append(img.flatten().astype(np.float32)/255.0) tmp = np.zeros(NUM_CLASSES) tmp[int(count)] = 1 train_label.append(tmp) else: test_image.append(img.flatten().astype(np.float32)/255.0) tmp = np.zeros(NUM_CLASSES) tmp[int(count)] = 1 test_label.append(tmp) i = i + 1 label_name = folder + '\n' f.write(label_name) count = count+1 f.close() train_image = np.asarray(train_image) train_label = np.asarray(train_label) test_image = np.asarray(test_image) test_label = np.asarray(test_label) with tf.Graph().as_default(): # 画像を入れる仮のTensor images_placeholder = tf.placeholder("float", shape=(None, IMAGE_PIXELS)) # ラベルを入れる仮のTensor labels_placeholder = tf.placeholder("float", shape=(None, NUM_CLASSES)) # dropout率を入れる仮のTensor keep_prob = tf.placeholder("float") # inference()を呼び出してモデルを作成 logits = inference(images_placeholder, keep_prob) # loss()を呼び出して損失を計算 loss_value = loss(logits, labels_placeholder) # training()を呼び出して訓練 train_op = training(loss_value, FLAGS.learning_rate) # 精度の計算 acc = accuracy(logits, labels_placeholder) # 保存の準備 saver = tf.train.Saver() # Sessionの作成 sess = tf.Session() # 変数の初期化 sess.run(tf.global_variables_initializer()) # TensorBoardで表示する値の設定 summary_op = tf.summary.merge_all() summary_writer = tf.summary.FileWriter(FLAGS.train_dir, sess.graph) # 訓練の実行 for step in range(FLAGS.max_steps): for i in range(int(len(train_image)/FLAGS.batch_size)): # batch_size文の画像に対して訓練の実行 batch = FLAGS.batch_size*i # Feed_deicでplaceholderに入れるデータを指定する sess.run(train_op, feed_dict={ images_placeholder: train_image[batch:batch+FLAGS.batch_size], labels_placeholder: train_label[batch:batch+FLAGS.batch_size], keep_prob: 0.5}) # 1step終わるたびに精度を計算する train_accuracy = sess.run(acc, feed_dict={ images_placeholder: train_image, labels_placeholder: train_label, keep_prob: 1.0}) print("step %d, training accuracy %g" % (step, train_accuracy)) # 1step終わるたびにTensorBoardに表示する値を追加する summary_str = sess.run(summary_op, feed_dict={ images_placeholder: train_image, labels_placeholder: train_label, keep_prob: 1.0}) summary_writer.add_summary(summary_str, step) # 訓練が終了したらテストデータに対する精度を表示 print("test accuracy %g" % sess.run(acc, feed_dict={ images_placeholder: test_image, labels_placeholder: test_label, keep_prob: 1.0})) # 最終的なモデルを保存 save_path = saver.save(sess, "./model.ckpt")
本当は、あらかじめコマンドプロンプトorターミナルで
tensorboard --logdir /tmp/data
と入力するとTensorBoardで確認できるようなのですが、まだうまくいっていません。。。
accuracyが低いまま(0.15とか)学習が進まないことがありましたが、learning_rateを1e-5に下げることで解決できました
これで学習させると
step 0, training accuracy 0.576119
step 1, training accuracy 0.571144
step 2, training accuracy 0.571144
step 3, training accuracy 0.570149
...
...
step 117, training accuracy 0.972139
step 118, training accuracy 0.972139
step 119, training accuracy 0.973134
test accuracy 0.845455
となり、正解率が訓練データで97%、テストデータで84%となりました
自分のしょぼいノートPCでは風呂に入って出てもまだ学習が終わっていませんでした。。。
そして過学習気味っぽいように見えますがどうなんでしょうか
⑤未知の画像を判定させる
analysisフォルダに判定させたい画像を入れておいて、judge.pyを実行します
#!/usr/bin/env python import glob import os import sys import numpy as np import tensorflow as tf import cv2 path=os.getcwd()+'/analysis/' file_list=os.listdir(path) i = 0 label_name = [] flags = tf.app.flags FLAGS = flags.FLAGS flags.DEFINE_string('label','label.txt','File name of label') f = open(FLAGS.label,'r') for line in f: line = line.rstrip() l = line.rstrip() label_name.append(l) i = i + 1 NUM_CLASSES = i IMAGE_SIZE = 56 IMAGE_PIXELS = IMAGE_SIZE*IMAGE_SIZE*3 def inference(images_placeholder, keep_prob): def weight_variable(shape): initial = tf.truncated_normal(shape, stddev=0.1) return tf.Variable(initial) def bias_variable(shape): initial = tf.constant(0.1, shape=shape) return tf.Variable(initial) def conv2d(x, W): return tf.nn.conv2d(x, W, strides=[1, 1, 1, 1], padding='SAME') def max_pool_2x2(x): return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME') x_image = tf.reshape(images_placeholder, [-1, 56, 56, 3]) with tf.name_scope('conv1') as scope: W_conv1 = weight_variable([3, 3, 3, 32]) b_conv1 = bias_variable([32]) h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1) with tf.name_scope('pool1') as scope: h_pool1 = max_pool_2x2(h_conv1) with tf.name_scope('conv2') as scope: W_conv2 = weight_variable([3, 3, 32, 64]) b_conv2 = bias_variable([64]) h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2) with tf.name_scope('pool2') as scope: h_pool2 = max_pool_2x2(h_conv2) with tf.name_scope('conv3') as scope: W_conv3 = weight_variable([3, 3, 64, 128]) b_conv3 = bias_variable([128]) h_conv3 = tf.nn.relu(conv2d(h_pool2, W_conv3) + b_conv3) with tf.name_scope('pool3') as scope: h_pool3 = max_pool_2x2(h_conv3) with tf.name_scope('fc1') as scope: W_fc1 = weight_variable([7*7*128, 1024]) b_fc1 = bias_variable([1024]) h_pool3_flat = tf.reshape(h_pool3, [-1, 7*7*128]) h_fc1 = tf.nn.relu(tf.matmul(h_pool3_flat, W_fc1) + b_fc1) h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob) with tf.name_scope('fc2') as scope: W_fc2 = weight_variable([1024, NUM_CLASSES]) b_fc2 = bias_variable([NUM_CLASSES]) with tf.name_scope('softmax') as scope: y_conv=tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2) return y_conv if __name__ == '__main__': test_image = [] test_filenm = [] for file in file_list: test_filenm.append(file) img = cv2.imread('./analysis/' + file ) img = cv2.resize(img, (IMAGE_SIZE, IMAGE_SIZE)) test_image.append(img.flatten().astype(np.float32)/255.0) test_image = np.asarray(test_image) images_placeholder = tf.placeholder("float", shape=(None, IMAGE_PIXELS)) labels_placeholder = tf.placeholder("float", shape=(None, NUM_CLASSES)) keep_prob = tf.placeholder("float") logits = inference(images_placeholder, keep_prob) sess = tf.InteractiveSession() saver = tf.train.Saver() sess.run(tf.global_variables_initializer()) saver.restore(sess, "./model.ckpt") for i in range(len(test_image)): accr = logits.eval(feed_dict={ images_placeholder: [test_image[i]], keep_prob: 1.0 })[0] pred = np.argmax(logits.eval(feed_dict={ images_placeholder: [test_image[i]], keep_prob: 1.0 })[0]) pred_label = label_name[pred] print (test_filenm[i],' , ',pred_label)
結果としてしまりんは一応全部正解していました
ただなでしこをよく間違えます
これはなでしこ
これはother
これもother
しまりんはほぼ正しく判定できているが、なでしこはうまく判定できていない理由としては恐らくデータセットが少ないことから過学習しているのではないかと思います
訓練用データとテスト用データで分ける前にデータを水増しすることが果たして適切なのかも微妙ですが、他の事例を見てもしまりんの倍以上はデータセットが必要そうなので、データの数を増やすことで解決できないかもうちょっと試行錯誤したいです
あとは少ないデータセットの場合はアンサンブルも効果的かもしれない
雑な記事になってしまいました。
また改良して精度を上げたいと思います。