Mugichoko's blog

Mugichoko’s blog

プログラミングを中心としたメモ書き.

Caffeを通してCNNを理解する #3

大域的目標

本シリーズの目標はCNNのことを全く知らない人間が少し理解することにある.前回までの取り組みは以下のリンクより.

mugichoko.hatenablog.com

今回の目標

  1. 自分で学習用データのLMDBを作成する
  2. 自作のLMDBを使って学習及び認識までを通しで行う

0. LMDBの作成方法の模索

create_mnist.shを覗く

まず,何をどうすればいいのかを把握するために,MNISTのLMDBを構築するために使ったCAFFE_ROOT/examples/mnist/create_mnist.shを開いてみる.基本的にはパスをセットして,いらないデータを削除した後にconvert_mnist_data.exetrain用データセットtest用データセットのそれぞれを入力として起動しているだけの様子.

create_mnist.sh

#!/usr/bin/env sh
# This script converts the mnist data into lmdb/leveldb format,
# depending on the value assigned to $BACKEND.
set -e

EXAMPLE=examples/mnist
DATA=data/mnist
BUILD=build/examples/mnist

BACKEND="lmdb"

echo "Creating ${BACKEND}..."

rm -rf $EXAMPLE/mnist_train_${BACKEND}
rm -rf $EXAMPLE/mnist_test_${BACKEND}

$BUILD/Release/convert_mnist_data.exe $DATA/train-images.idx3-ubyte \
  $DATA/train-labels.idx1-ubyte $EXAMPLE/mnist_train_${BACKEND} --backend=${BACKEND}
$BUILD/Release/convert_mnist_data.exe $DATA/t10k-images.idx3-ubyte \
  $DATA/t10k-labels.idx1-ubyte $EXAMPLE/mnist_test_${BACKEND} --backend=${BACKEND}

echo "Done."

create_mnist_data.cppを覗く

CAFFE_ROOT/examples/mnist/create_mnist_data.cppを開いてみる.GFLAGSやGLOGをよく知らないので,う~んややこしい,ように見える.でも,データセットを作成する上で重要そうなのは,どうやら79行目~115行目にあるようだ.それより前は,バイナリデータを扱うための処理の様子.以下,その抜粋及び自分の理解をコメントで付記したもの.

// データセットのインスタンス化.
// "db_backend" は "create_mnist.sh" の引数で "lmdb" と与えられている.
scoped_ptr<db::DB> db(db::GetDB(db_backend));
// "db_path" も同様に引数で与えられている.
// 具体的には "$EXAMPLE/mnist_train_${BACKEND}" (もしくは,train→test)の部分が入力されている.
// つまり, "$EXAMPLE/mnist_train_lmdb" (もしくは,train→test)というフォルダ(データベース入り)を作る.
db->Open(db_path, db::NEW);
// 新しくデータセット作るよ,ということらしい.
// scoped_ptrはboostライブラリにある,リソースが使われなくなったら勝手に開放するおインタらしい.
scoped_ptr<db::Transaction> txn(db->NewTransaction());

// Storing to db ← dbにデータを格納していくよ,と書いてある
char label; // ラベル.つまり,MNISTの場合0~9が入るっぽい
char* pixels = new char[rows * cols]; // どっからどう見ても1チャンネル画像データ
int count = 0;
string value;

// データ形式を定義
Datum datum;  // Caffeのデータ形式,そこに...
datum.set_channels(1);  // 1チャンネルの画像を入れる
datum.set_height(rows); // 画像の高さ
datum.set_width(cols);  // 画像の幅を入れる
// 単なるログの記録
LOG(INFO) << "A total of " << num_items << " items.";
LOG(INFO) << "Rows: " << rows << " Cols: " << cols;
// ここから実際の "datum" へのデータ格納が始まる.
// 尚,データの数 "num_items" は取得済み(69行目)
for (int item_id = 0; item_id < num_items; ++item_id) {
  // 画像データとラベル(正解値)をファイルから読み込む
  image_file.read(pixels, rows * cols);
  label_file.read(&label, 1);
  // 画像と対応するラベルを "datum" へ格納
  datum.set_data(pixels, rows*cols);
  datum.set_label(label);
  // 今,何番目のデータを処理しているっけ?
  string key_str = caffe::format_int(item_id, 8);
  // 格納したデータから,1連なりの文字列データを取得
  datum.SerializeToString(&value);
  // それを,データベース全体に格納?
  txn->Put(key_str, value);

  // 1000コ処理したら,実際にディスクに書き込んでいる?
  if (++count % 1000 == 0) {
    txn->Commit();
  }
}
// write the last batch ← やっぱりそうだ!最後に書き込む
if (count % 1000 != 0) {
    txn->Commit();
}
// ログを出力したら,画像用メモリを解放
LOG(INFO) << "Processed " << count << " files.";
delete[] pixels;
// dbを閉じて,書き込み完了!
db->Close();

1. 自分で学習用データのLMDBを作成する

データの準備

MNISTを模して,train及びtest用の画像とラベルの対を用意することにした.これはC++で書いた.

Main.cpp

ここでは,単に,train及びtest用に関数createDataset()を呼び出しているだけ.

#include <iostream>

#include "DatasetGenerator.h"

void main()
{
    const int imgSize = 28;
    const int numItems = 50000;
    const float numTrainTestRatio = 0.7f;
    const int numTrainItems = int(numItems * numTrainTestRatio);
    const int numTestItems = int(numItems * (1.0f - numTrainTestRatio));

    if (!createDataset("train", imgSize, numTrainItems))
    {
        std::cerr << "ERR: Failed to create training database" << std::endl;
        exit(EXIT_FAILURE);
    }

    if (!createDataset("test", imgSize, numTestItems))
    {
        std::cerr << "ERR: Failed to create testing database" << std::endl;
        exit(EXIT_FAILURE);
    }
}

DatasetGenerator.h

以下の組み合わせで生成される0~9の画像を自動生成する.

  1. ランダムに明るさの底上げ(66行目)
  2. ランダムにフォントを選択(67行目)
  3. ランダムに数画素分位置をずらして画像に書き込み(69行目)
  4. ランダムに各画素にノイズを追加(71~74行目)
  5. ランダムな確率で画像にぼかしを付与(76~79行目)

後は,ラベル情報(0~9の数値)と共に,画像の名前をテキストに書き込んで保存する.

#pragma once

#include <opencv2/opencv.hpp>
#include <fstream>
#include <direct.h>
#include <iomanip>

double getFontScale(cv::HersheyFonts fontType)
{
    double scale = 1.0;

    switch (fontType)
    {
    case cv::FONT_HERSHEY_PLAIN:
        scale = 2.0;
        break;
    case cv::FONT_HERSHEY_COMPLEX_SMALL:
        scale = 1.8;
        break;
    default:
        break;
    }

    return scale;
}

bool createDataset(
    const std::string &dbPath = "my_lmdb",
    int imageSize = 28,
    int numItems = 50000
)
{
    std::cout << "Creating " << dbPath << "..." << std::endl;

    std::string listName(dbPath + "/list.txt");
    std::ofstream dbItemList(listName.c_str());

    if (!dbItemList.is_open())
    {
        if (_mkdir(dbPath.c_str()) == 0)
        {
            std::cout << "Created " << dbPath << " folder" << std::endl;
        }
        else
        {
            std::cerr << "ERR: Failed to create " << dbPath << std::endl;
            return false;
        }

        dbItemList.open(listName.c_str());
        if (!dbItemList.is_open())
        {
            std::cerr << "ERR: Failed to create " << listName << std::endl;
            return false;
        }
    }
    
    dbItemList << numItems << std::endl;
    dbItemList << imageSize << " " << imageSize << " " << 1 << std::endl;
    for (int idx = 0; idx < numItems; ++idx)
    {
        std::string digit = std::to_string(idx % 10);
        cv::Mat img(imageSize, imageSize, CV_8U);

        // add bias
        img = rand() % 100;
        int fontType = rand() % 8;
        double fontScale = getFontScale(cv::HersheyFonts(fontType));
        cv::putText(img, digit, cv::Point(2 + rand() % 4, 22 + rand() % 4), fontType, fontScale, cv::Scalar(255));
        // add noise
        img.forEach<uchar>([](uchar &p, const int *pos) -> void
        {
            p = std::min((p + (rand() % 150)), 255);
        });
        // add blur
        if (rand() % 2)
        {
            cv::blur(img, img, cv::Size(3, 3));
        }

        std::stringstream imgPath("");
        imgPath << dbPath << "/img_" << std::setw(8) << std::setfill('0') << idx << ".bmp";
        cv::imwrite(imgPath.str(), img);

        dbItemList << digit << " " << imgPath.str() << std::endl;
    }

    std::cout << "Created " << dbPath << "" << std::endl;

    return true;
}

この処理が終わると以下のような構成の,2つのフォルダが生成される.

  • train
    • list.txt
    • 画像群 (.bmp)
  • test
    • list.txt
    • 画像群 (.bmp)

具体的な中身は以下の様になっている.

f:id:Mugichoko:20171130222128p:plain

15000
28 28 1
0 test/img_00000000.bmp
1 test/img_00000001.bmp
2 test/img_00000002.bmp

(以下省略)

LMDBの作成

create_mnist_data.cppをビルドしようと思ったのだが,どうも依存関係が多すぎて面倒だ.ということで,PythonでLMDB変換用プログラムを書くことにした.

LMDBのインストール

まずは,LMDBをインストールする必要があるが,Anacondaを使えば conda install -c conda-forge python-lmdbと,コマンドプロンプトで打てばよいだけなので,とても簡単だ.(参考:Python Lmdb :: Anaconda Cloud

LMDB作成用Pythonスクリプト

ここのWebサイトを参考に作成した.create_mnist_data.cppとよく似ている.

ConvertDataset.py

途中の容量確保のところで10というマジックナンバーが出てくるが,謎である.ここに書いてあることを読めば理解できるのかな?とも思うが,面倒なので,とりあえずほおっておく.

import caffe
import lmdb
import numpy as np
from PIL import Image
import sys

fileName = "test/list.txt"
dbName = "my_test_lmdb"

with open(fileName, "r") as file:
    numItems = file.readline()
    width, height, channels = file.readline().split(" ")

    numItems = int(numItems)
    width = int(width)
    height = int(height)
    channels = int(channels)

    maxSize = np.zeros((numItems, channels, width, height), dtype=np.uint8).nbytes * 10
    env = lmdb.open(dbName, map_size = maxSize)

    with env.begin(write = True) as txn:
        for idx, line in enumerate(file):
            label, path = line.split(" ")
            
            img = np.array(Image.open(path.rstrip()))
            
            datum = caffe.proto.caffe_pb2.Datum()
            datum.width = width
            datum.height = height
            datum.channels = channels
            datum.data = img.tobytes()
            datum.label = int(label)

            sys.stdout.write("\ridx: {}".format(idx))

            strID = '{:08}'.format(idx)
            txn.put(strID.encode('ascii'), datum.SerializeToString())

print("\n")

env.close()

これを以下の通り実行する.

  1. ConvertDataset.pyと同一フォルダ内にtrainフォルダとtestフォルダを置く
  2. コマンドプロンプトからpython ConvertDataset.pyで実行
  3. my_train_lmdb及びmy_test_lmdbフォルダが生成されていることを確認

2. 自作のLMDBを使って学習及び認識までを通しで行う

基本的な流れは以下の通り.

  1. 必要なファイルを%CAFFE_ROOT%\mycnnに置く
    • my_train_lmdbフォルダとmy_test_lmdbフォルダ
    • solver.prototxttrain.prototxtdeploy.prototxt
      • 参考:前回記事
      • ただし,train.prototxt内で指定している入力ファイル名は今回のものに変更する!
    • classify_digits.y
    • 認識させたい画像データ
      • 今回は,前回と同じものを用いる
      • 参考:前回記事
  2. 学習開始
    • ..\build\tools\Release\caffe.exe train --solver=solver.prototxt
  3. 以下のファイルが生成されたか確認
    • _iter_5000.caffemodel
    • _iter_5000.solverstate
  4. 学習結果を用いて認識処理を実行
    • python classify_digits.py 7.pngの様に実行する
  5. 結果が得られる
    • 以下は,前回と同じく6,pngを認識させた結果
C:\Libraries\caffe\mycnn> python classify_digits.py 6.png
(中略)
I1130 14:45:17.019353  6984 net.cpp:744] Ignoring source layer ip2_ip2_0_split
I1130 14:45:17.019353  6984 net.cpp:744] Ignoring source layer accuracy
C:\Users\Shohei\Anaconda3\lib\site-packages\skimage\transform\_warps.py:84: UserWarning: The default mode, 'constant', will be changed to 'reflect' in skimage 0.15.
  warn("The default mode, 'constant', will be changed to 'reflect' in "
prediction shape: (500,)
prediction shape: 6

10枚の画像の結果を見ただけなので短絡的な見解だが,6や9を8と間違わないので,MNISTで学習させた結果よりも良さそうだ(前回記事参照).というよりは,自身の手書き文字が,MNISTの手書き文字よりもフォントに近いのだと思われる.

まとめ

自作のデータセットを作成し,Caffeを使って学習させ,自作の入力画像を用いてCaffeから認識結果を得ることができた!この作業を通して,CNNの基本を理解したつもりだ.

次の目標を設定するとすれば,論文に書いてあるネットワークを実際に自分で作ってみることかな?と思う.

Fast Digital Image Inpainting

目標

画像上の欠損領域を同一画像内の他の画素を用いて埋めるImage Inpaintingを実装する.

Fast Digital Image Inpainting

概要

とても簡単に実装できるImage Inpaintingの一つ.単に論文に書かれた以下のカーネルを用いて,欠損領域にて畳み込み処理を行うだけ.100回~数百回この処理を繰り返すことで,欠損領域を埋めることができる.

f:id:Mugichoko:20171125073003p:plain

※ 図は論文より抜粋.尚,{a = 0.073235, b = 0.176765}とする.

M. M. Oliveira, B. Bowen, R. McKenna, Y.-S. Chang: Fast Digital Image Inpainting, Proc. of Int. Conf. on Visualization, Imaging and Image Processing (VIIP), pp. 261-266, 2001.

結果

左から,入力画像,マスク画像,結果の画像の順で5つの例を挙げる.いずれもループ回数は500回.

example_1 example_2 example_3 example_4 example_5

ソースコード

今更ながら,初めてGitHubを使ってみた.これまでBitBucketで自身のコードを管理していたが,公開する場合はGitHubの方が便利そうだ.

github.com

所感

単に畳み込み処理を実装するだけなので,テストなども含めて数時間で終わる程度の実装だった.非常に簡単である.尚,欠損領域が大きい場合はループ回数を増やす必要がある.

Caffeを通してCNNを理解する #2

  • 大域的目標
  • 今回の目標
  • 自らprototxtを作成し,MNISTの学習を行う
    • 1. prototxtの作成
      • train.prototxt
      • solver.prototxt
    • 2. 学習の実行
      • 2.1 ファイルを構成
      • 2.2 trainコマンドの実行
  • Deployment(学習した結果を用いて認識)を行う
    • 1. deploy.prototxtの作成
    • 2. classify_digits.pyの作成
    • 3. 入力画像の用意
    • 4. Pythonスクリプトの実行
  • 所感

大域的目標

本シリーズの目標はCNNのことを全く知らない人間が少し理解することにある.前回までの取り組みは以下のリンクより.

mugichoko.hatenablog.com

今回の目標

  1. 自らprototxtを作成し,MNISTの学習を行う
  2. Deployment(学習した結果を用いて認識)を行う
続きを読む

Caffeを通してCNNを理解する #1

目標と現状

大局的目標

機械学習に関しては全くの初心者なので分からないことだらけだ.それでもConvolutional Neural Network (CNN) を学ぶ時が来た!実装もしないといけないので,Caffeを通して学んでいきたい.本シリーズの目標はCNNのことを全く知らない人間が少し理解することにある.

今回の目標

さて,最初の目標は良い教科書を揃え,自分が何が理解できていないのかを理解するところからだ!

現状

Caffeのサンプルを動かした(過去記事参照)程度.尚,画像処理や線形代数の基礎知識は備えているつもりです.

mugichoko.hatenablog.com

良質な教材

Caffeを使ったCNNによる手書き文字分類

このサイトに4つの記事があるが,これを読んで,私の基本的な疑問は全て解消された.構造的に,かつ適切な日本語で説明されており,これを読まないと損すると思ってしまうくらいだ.

li.nu

4つの記事の各項目を勝手ながら抜粋させて頂くと,ここから何が学べるか把握できると思う.いくつかのサイトを眺めたが,これがベストであり,これ以外は読まなくてもよいのではないかと思う.半日もあれば全て読めます.

  1. CNNの概要、データセットの準備
  2. CNNの構造と学習の仕組み
    • CNNの構造
    • 特徴抽出部
    • 畳み込みフィルタ
    • 畳み込み層
    • プーリング層
    • 識別部
    • 全結合層
    • 活性化関数
    • ここまでのまとめ
    • 誤差関数
    • 勾配降下法 (GD)
    • 確率的勾配降下法 (SGD)
    • 今回のまとめ
  3. prototxtの作成
    • prototxtとは
    • prototxtの基本文法
    • 4つのprototext
    • train.prototext / test.prototxt の作成
    • 各層の基本的な記述方法
    • 入力層 (Data)
    • 畳み込み層 (Convolution)
    • プーリング層 (Pooling)
    • 全結合層 (InnerProduct)
    • 活性化関数 (ReLU)
    • 誤差関数 (SoftmaxWithLoss)
    • solver.prototxtの書き方
    • 今回のまとめ
  4. ネットワークの訓練方法
    • フォルダ構成の確認
    • 訓練の方法
    • 訓練誤差とテスト誤差
    • 正答率の算出
    • 今回のまとめ

最終回のまとめに,

次は、訓練により出来上がった caffemodel を使って、実際の一般データを識別させる方法、つまり、deploy.prototxtの書き方とPyCaffeでの使い方について見ていきます。

とあるのですが,残念ながら5つ目の記事を見つけられませんでした... 残念.

更なる学習のために

英語教材

書籍

便利ツール

さて,概要を把握したところで,Caffeは,基本的にprototxtを編集して動かせることが分かった.ということで,これを編集するための便利ツールを探した.

Sublime Text 3

これ自体はただのテキストエディタですが,パッケージをインストールすることで,様々な拡張子のファイルを見やすいようにカラーリングしてくれるところが嬉しいのです.早速,proto buffやprototxtが見やすいようにパッケージをインストールした.

パッケージのインストール方法は以下の通り.

  1. Tools/Command Palette... Ctrl+Shift+Pを開いてinstallと入力(下図)
  2. Package Control: Install Packageを選択
  3. Proto等と入力してパッケージをインストール

f:id:Mugichoko:20171122001002p:plain

尚,私は以下の2つをインストールした.

  • Protobuf Syntax Highliting
  • Caffe Prototxt Syntax

Netscope

prototxtの内容を可視化してくれるオンラインサービス.手順は以下の通り.

  1. "Launch Editor"からエディタを開く
  2. 左画面のテキストメニューに自身のprototxtファイルの内容をコピペ
  3. Shift + Enterで右画面に可視化

CaffeのMNISTのサンプルにあるlenet_train_test.prototxtを可視化すると以下の様になる.ブロックにマウスオーバーすると,その詳細が見られる.

f:id:Mugichoko:20171121234217p:plain

まとめ

  • CNNに関わる処理の流れと専門用語をWebサイトを読んで把握した
  • Caffeで必要な処理を把握した(ただし,学習データを使って認識させる処理は不明)
  • 現状,講義を受けた学生状態(理解したつもりだが実践できるかどうか分からない状態)なので,次回は,prototxtを実際に編集してCaffeを実行する予定

はじめてのASUS Xtion2

  • 目標
  • 導入方法
  • 遭遇したトラブル
  • 結果

目標

ASUS Xtion2を動作させる.

続きを読む

Caffe 1.0 (CPU) on Windows 10

  • 目標
  • 経緯
  • 開発環境
  • インストール方法
  • MNISTの例
    • 実行方法
    • 実行結果

目標

Deep Learningがやりたい!そこでCaffe(カフェ)をインストールして,サンプルプログラムを実行することで動作確認を行い,経験談をまとめた.

経緯

今回,将来的にモバイル上で動作させることも考えてCPU版(CUDAを使わない)Caffeを使うことにした.尚,Caffe2も既にリリースされているが,諸事情によりバージョン1.0を用いる.

インストール方法はネット上で多々紹介されているが,バージョンの違いか環境の違いか,なかなかその通りにやっても上手くいかないものばかりだった.ファイル内のパスを書き換えることで動作するものがほとんどだったので,今回はその辺りを経験談としてまとめる.

続きを読む

オーストリアでの在留許可申請(オーストリア実践編)

  • 状況説明
  • 必要書類
  • 申請場所
    • 地図
    • 施設内での大体の手順
  • 申請手続き記録
    • ファースト・トライアル(2017年9月26日)
    • セカンド・トライアル前の事前準備
    • セカンド・トライアル(2017年10月22日)

状況説明

ビザDを取得していても,オーストリアでの6ヶ月以上の滞在には在留許可申請が必要だ.前回の記事の通り,私は学振特別研究員としてここオーストリアに留学している.その状況下での在留許可申請についてまとめた.

続きを読む