Keras による学習モデルを変換して Raspberry Pi 3 Model B+ 上で .NET Core アプリから共有ライブラリ経由で TensorFlow Lite の学習モデルによる推論を実行する (2)
ここでは、タイトルの通り Keras の学習モデルを Raspberry Pi 上で動作させるまでのプロセスをまとめています。第 2 回の今回は TensorFlow Lite の静的ライブラリをビルドしたいと思います。
目次
- 予告
- Ubuntu with VMWare Player で環境を準備する
- libtensorflow-lite.a 静的ライブラリをビルドする <-- 今ココ!
- Keras in Python でサンプル用の学習モデルを構築する
- Keras で構築した学習モデルを TensorFlow Lite のモデルに変換する
- 静的ライブラリで TensorFlow Lite のモデルを使用する C アプリケーションを Ubuntu 上で動かす
- TensorFlow Lite のモデルで推論する機能を提供する ARM 向け共有ライブラリをクロスコンパイルする
- 作成した共有ライブラリを使用する C アプリケーションを Raspberry Pi 上で動かす
- .NET Core コンソールアプリケーションを Raspberry Pi 上で動かす
- .NET Core コンソールアプリケーションから共有ライブラリを参照して TensorFlow Lite のモデルを使用した推論をおこなう
TensorFlow の公式ページはこちらになりますが、そのソースコードは GitHub で公開されています。このソースコードを自分でコンパイルすることで静的ライブラリを生成しようという試みです。「自分でコンパイル」とは言っても、既に用意されているスクリプトを実行するだけの簡単なお仕事です。
TensorFlow のリポジトリをクローンする
それでは早速 TensorFlow のリポジトリをクローンします。
cd ~/Documents
git clone https://github.com/tensorflow/tensorflow.git
cd tensrflow
git checkout v2.0.0
git checkout -b cross_build
v2.0.0 というタグで TensorFlow 2.0.0 リリース時の状態になるので、いったんそのコミットをチェックアウトします。さらにそこから独自の cross_build ブランチを切っておくことで、元の状態にいつでも戻ってこれるように準備しておきます。何事も準備は大切です。
ビルドのための準備
依存関係のあるパッケージをいろいろ用意する必要がありますが、これを自動的におこなってくれる便利なスクリプトが既に用意されているのでこれを実行します。
./tensorflow/lite/tools/make/download_dependencies.sh
タグ v2.0.0 をチェックアウトしておけば特にエラーが起こることもないと思いますが、中途半端なコミットで実行するとたまにこのスクリプトでエラーが発生してしまいます。半端なコミットは公式でも動作保証できないものですので、できるだけリリースビルドされているコミットで実行しましょう。
さらに、Makefile の一部を編集します。
gedit tensorflow/lite/tools/make/Makefile
# original)
# BUILD_WITH_NNAPI ?= true
# change)
# BUILD_WITH_NNAPI = false
「BUILD_WITH_NNAPI」で検索すると該当の行へすぐにジャンプできます。Android の NNAPI を使用するかどうかを設定する部分のようですが、false にしておいたほうが無難なようです。
最後にビルドに必要な Bazel をインストールします。まずは cat .bazelversion で必要な Bazel のバージョンを確認しておきましょう。v2.0.0 のコミットをチェックアウトしたのであれば「1.2.1」と表示されるはずです。
cat .bazelversion
# 1.2.1
というわけで Bazel 1.2.1 をインストールしましょう。Ubuntu へのインストール方法は公式ページにも掲載されており、ここでもその通りの手順でインストールします。
公式ページにも記載がありますが、ここでは Ubuntu 16.04.3 を使用しているので openjdk-8-jdk をインストールしています。もし Ubuntu 18.04.3 を使用している場合は openjdk-11-jdk をインストールしてください。
sudo apt-get install g++ unzip zip
sudo apt-get install openjdk-8-jdk(ちょっと時間かかる)
cd ~/Downloads
curl -LO https://github.com/bazelbuild/bazel/releases/download/1.2.1/bazel-1.2.1-installer-linux-x86_64.sh
chmod +x bazel*
./bazel-1.2.1-installer-linux-x86_64.sh --user
bazel --version
# bazel 1.2.1
最後に「bazel --version」でインストールされた Bazel のバージョンを確認しています。もし間違ったバージョンをインストールしてしまった場合は、関連ファイルを削除することでアンインストールしてから再度インストール作業をおこなってください。関連ファイルの削除は以下の通りです。
# Bazel をアンインストールする場合に実行
rm -fr ~/.bazel
rm -fr ~/.cache/bazel
TensorFlow Lite のビルド
それではいよいよビルドです。とは言ってもビルド用のシェルスクリプトを実行するだけです。私の環境では、このスクリプト実行完了におよそ 5 分ほどかかりました。
cd ~/Documents/tensorflow
./tensorflow/lite/tools/make/build_rpi_lib.sh(5 分かかった)
cp tensorflow/lite/tools/make/gen/rpi_armv7l/lib/libtensorflow-lite.a .
完了後に生成される libtensorflow-lite.a というファイルが TensorFlow Lite の静的ライブラリになります。これを使用すると、Linux 上で C/C++ によるプログラムに TensorFlow Lite の機能を組み込むことができるようになります。今後のためにカレントディレクトリにコピーしています。
もしビルドが途中で失敗するなどしてもう一度ビルドする場合は、下記ディレクトリを削除してからおこなうと良いでしょう。
# ビルドをやり直すときに実行
rm -fr tensorflow/lite/tools/make/gen
まとめ
今回は TensorFlow Lite の静的ライブラリである libtensorflow-lite.a ファイルを生成するところまでを掲載しました。このビルド方法を調べているときは中々正常にビルドできず、非常に苦労していましたが、こうやってまとめると迷うようなところがないように見えて不思議です。また、調査している段階では TensorFlow のリポジトリはハッシュ 2cbbe2ae0d4ab61d8f08f1eb31417e4a163395c7 の中途半端なコミットでビルドしていました。一応ビルドはできましたが、できあがったライブラリの信頼性を考えたとき、リリースビルドされているコミットにチェックアウトすべき、ということに気が付きました。
せっかく git を使っているので、生成した libtensorflow-lite.a を含めてコミットしておいても良いかもしれません。バイナリファイルなので git としてはあまりよろしくありませんが、後でもう一度 Bazel でビルドし直すのも面倒ですし。私の場合は余計なディレクトリなどは .gitignore に登録して無視するように設定し、編集した Makefile や生成した libtensorflow-lite.a ファイルを含めていったんコミットしました。
さて、作成したライブラリはしばらく寝かせておくとして、次回は Python でサンプル用の学習モデルを作成します。
Tweet
Keras による学習モデルを変換して Raspberry Pi 3 Model B+ 上で .NET Core アプリから共有ライブラリ経由で TensorFlow Lite の学習モデルによる推論を実行する (1)
ここでは、タイトルの通り Keras の学習モデルを Raspberry Pi 上で動作させるまでのプロセスをまとめています。今回は第 1 回として VMWare Player 上で Ubuntu の環境を整えるまでをまとめていきます。
目次
- 予告
- Ubuntu with VMWare Player で環境を準備する <-- 今ココ!
- libtensorflow-lite.a 静的ライブラリをビルドする
- Keras in Python でサンプル用の学習モデルを構築する
- Keras で構築した学習モデルを TensorFlow Lite のモデルに変換する
- 静的ライブラリで TensorFlow Lite のモデルを使用する C アプリケーションを Ubuntu 上で動かす
- TensorFlow Lite のモデルで推論する機能を提供する ARM 向け共有ライブラリをクロスコンパイルする
- 作成した共有ライブラリを使用する C アプリケーションを Raspberry Pi 上で動かす
- .NET Core コンソールアプリケーションを Raspberry Pi 上で動かす
- .NET Core コンソールアプリケーションから共有ライブラリを参照して TensorFlow Lite のモデルを使用した推論をおこなう
VMWare Workstation Player のインストール
VMWare Workstation Player はこちらから入手してください。インストーラのウィザードにしたがってインストールするだけの簡単なお仕事です。
Ubuntu の入手
Ubuntu はこちらから入手してください。バージョンは 18.04.3 でも問題ないと思いますが、ここでは動作確認の際に使用していた ubuntu-16.04.3-desktop-amd64.iso を紹介しておきます。
Ubuntu の導入
VMWare Workstation Player を起動し、仮想環境の新規作成をおこないます。ダウンロードした Ubuntu の .iso ファイルを指定すると、簡易インストーラが自動的に選択され、ものの数分で Ubuntu デスクトップ環境が使用できる状態になります。OS のインストールなんて下手したら 1 日かかっていたような作業だったのに、時代の変化って怖いですね。何はともあれ、これにて終了、ってわけにもいかんのです。
割り当てる RAM 領域は 8GB 以上がオススメです。本連載記事には関係ありませんが、TensorFlow の C++ API を Bazel でビルドする場合、メモリが 8GB 以上ないとコンパイル途中で "matrix_square_root_op (exit 4)" などのエラーが発生してにっちもさっちもいかなくなります。
Ubuntu の各種設定
1. 自動サスペンド無効
パソコンで作業していたら、いつのまにか仮想環境の画面が真っ黒…。さらに自分の場合、そこから復帰できなくて VMWare を強制終了した経験もあります。そうならないためにも、自動サスペンド機能は無効にしておいてほうが無難です。
SystemSettings -> Brightness & Lock のメニューで、「Turn screen off when inactive for:」の項目を Never、「Lock」の項目を OFF にする。
2. キーボード配列を日本語に変更
Ubuntu Japanese から .iso ファイルを入手してインストールした方や英語配列を使う方には無縁ですが、念のため掲載。
sudo dpkg-reconfigure keyboard-configuration
で表示されるメニューから Generic 105-key (intl.) PC を選択。さらに、
sudo dpkg-reconfigure keyboard-configuration
で同じメニューを表示して、 Generic 105-key (intl.) PC -> Japanese -> Japanese -> The default for the keyboard layout -> No compose key -> No を選択。
3. 自動アップデート停止
個人的に自動アップデートは嫌いなのでその辺を手動設定に切り替えます。
SystemSettings -> Software & Updates のメニューで、Updates タブの「Automatically check for updates」を Never にする。
もし作業中に「Upgrade Available」のダイアログが表示された場合は「Don't Upgrade」ボタンを押し、次のダイアログで「OK」を押す。
4. タイムゾーン設定
時刻がずれている場合があるので、タイムゾーンに注意しながら設定しましょう。
SystemSettings -> Time & Date のメニューで、 地図上で日本周辺をクリックして Location を Tokyo にする。時刻を入力した後、「Automatically from the Internet」を ON にする。
5. 既存パッケージの更新
言わずもがな、有名なコマンドですね。自分の場合はダウロードに 18 分、適用に 4 分かかりました。
sudo apt-get update
sudo apt-get upgrade
6. git と curl のインストール
これからの作業で必要になってくるパッケージをインストールしておきます。
sudo apt-get install git curl
7. パッケージを掃除する
インストール作業などで汚れた環境を掃除しておきます。df コマンドなどで before/after を比較すると面白いです。自分の場合およそ 400MB ほど削除されました。
sudo apt-get clean
sudo apt-get autoremove
まとめ
まだ本題にはまったく入っていませんが、これでとりあえず Ubuntu の環境が整いました。次回は TensorFlow Lite の静的ライブラリをビルドしたいと思います。
Tweet
Keras による学習モデルを変換して Raspberry Pi 3 Model B+ 上で .NET Core アプリから共有ライブラリ経由で TensorFlow Lite の学習モデルによる推論を実行する (0)
約 2 年ぶりの突然な投稿で、しかもモリモリなタイトルではありますが、 最近自分が開発している内容が流行の最先端にほど近い内容ということで、 せっかくなら自分のブログに載せておこうと思った次第です。
とりあえず今回は予告だけで、詳細説明については次回以降にしたいと思います。
ざっくりとした内容をまとめると以下のようになります。
目次
- 予告 <-- 今ココ!
- Ubuntu with VMWare Player で環境を準備する
- libtensorflow-lite.a 静的ライブラリをビルドする
- Keras in Python でサンプル用の学習モデルを構築する
- Keras で構築した学習モデルを TensorFlow Lite のモデルに変換する
- 静的ライブラリで TensorFlow Lite のモデルを使用する C アプリケーションを Ubuntu 上で動かす
- TensorFlow Lite のモデルで推論する機能を提供する ARM 向け共有ライブラリをクロスコンパイルする
- 作成した共有ライブラリを使用する C アプリケーションを Raspberry Pi 上で動かす
- .NET Core コンソールアプリケーションを Raspberry Pi 上で動かす
- .NET Core コンソールアプリケーションから共有ライブラリを参照して TensorFlow Lite のモデルを使用した推論をおこなう
内容が薄くなりそうなものもありそうですが、一応以上の全 9 回でまとめていきたいと思います。ただし、例えば TensorFlow の git リポジトリは 1 時間に 3 回以上コミットされることもあるほど更新が頻繁におこなわれているため、あくまでも「こういう環境で動作した」という程度の情報であることを認識した上で読んでいただければと思います。
Tweet
円周率の小数点以下に現れる数値
ふと円周率に思いをはせることがあり(かなり頭おかC)、
小数点以下に現れる 0 から 9 の数の出現率は
どのような分布になっているのだろう、と気になってしまった。
気になってしまったものはしょうがない。
というわけで C# コードで簡単に調べてみたよ。
namespace PiTest
{
using System;
using System.Linq;
using System.Text;
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("最大値 = 9、最小値 = 0 のとき、");
Console.WriteLine("おおよそ、一様分布の");
Console.WriteLine("平均値は {(最大値) - (最小値)} / 2 = (9 - 0) / 2 = 4.50");
Console.WriteLine("標準偏差は {(最大値) - (最小値)} / 2√3 = (9 - 0) / 2√3 = " + (9 / 2 / Math.Sqrt(3.0)).ToString("#0.000"));
Console.WriteLine("であることが知られている。");
Console.WriteLine();
var nums = ASCIIEncoding.ASCII.GetBytes(Constants.PiString).Select(x => x - 0x30).ToArray();
Console.WriteLine("円周率 " + nums.Length.ToString("N0") +" 桁に出現する数の");
var ave = nums.Average();
var stdev = Math.Sqrt(((double)nums.Select(x => x * x).Sum() / nums.Length - ave * ave));
Console.WriteLine("平均値は " + ave.ToString("#0.00"));
Console.WriteLine("標準偏差は = " + stdev.ToString("#0.000"));
Console.WriteLine();
Console.WriteLine("一様分布より若干分布が広い");
Console.ReadKey();
}
}
}
Constants.PiString は static な string 型で、
円周率の小数点以下の数値を文字列として 1,000,000 桁分保持しています。
というのも、web 上で円周率を調べたらこの桁であれば
テキストデータで掲載されていたので、
それをそのまま流用させていただいたというだけです。
標準偏差が一様分布のものよりもやや大きめ、という結果でした。
円周率は調べ出すと人生終わるらしいので程々にしておきます。
Tweet
シーケンスを n 個毎に分割する
実務で必要になったのでやってみた内容です。 要求仕様はいたってシンプルで、平坦に並んでいるシーケンスを n 個毎に区切ってグルーピングすることが目的です。
例えば { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } を 3 個毎に分割して、{ { 0, 1, 2 }, { 3, 4, 5 }, { 6, 7, 8 }, { 9 } } という 4 つの要素に分割したシーケンスにします。最後の { 9 } については { 9, 0, 0 } というように既定値で詰めたりとかいろいろあるかもしれませんが、ここではある分だけの要素をそのままにしておくようにします。
で、結局 stackoverflow にお世話になりました。
ところが、方法がいくつも書いてあって、しかも ToArray() したら不具合のあるものもあって結局どれを使えばいいの?ってなったので、自分で処理時間を評価してみました。
まず時間計測用のクラスを用意します。ここでは tocsworld | C# での処理時間計測いろいろ を参考にして、Win32 API を使用する方法を採用しました。
namespace ConsoleApplication2
{
using System.Runtime.InteropServices;
internal class TimeCount
{
[DllImport("kernel32.dll")]
private static extern bool QueryPerformanceCounter(ref long lpPerformanceCount);
[DllImport("kernel32.dll")]
private static extern bool QueryPerformanceFrequency(ref long lpFrequency);
private long _startCounter;
public void Start()
{
QueryPerformanceCounter(ref this._startCounter);
}
public void Reset()
{
QueryPerformanceCounter(ref this._startCounter);
}
public double ElapsedMilliseconds
{
get
{
long stopCounter = 0;
QueryPerformanceCounter(ref stopCounter);
long frequency = 0;
QueryPerformanceFrequency(ref frequency);
return frequency != 0 ? (double)(stopCounter - this._startCounter) * 1000.0 / frequency : 0;
}
}
}
}
そして、今回比較するメソッドは次の 3 つです。
namespace ConsoleApplication2
{
using System;
using System.Collections.Generic;
using System.Linq;
public static class Extensions
{
public static IEnumerable<IEnumerable<T>> Chunk1<T>(this IEnumerable<T> source, int size)
{
if (source == null) throw new ArgumentException("source");
if (size < 1) throw new ArgumentException("size");
return source.Select((x, i) => new { Index = i, Value = x })
.GroupBy(x => x.Index / size, (key, result) => result.Select(r => r.Value));
}
public static IEnumerable<IEnumerable<T>> Chunk2<T>(this IEnumerable<T> source, int size)
{
if (source == null) throw new ArgumentException("self");
if (size < 1) throw new ArgumentException("size");
while (source.Any())
{
yield return source.Take(size);
source = source.Skip(size);
}
}
public static IEnumerable<IEnumerable<T>> Chunk3<T>(this IEnumerable<T> source, int size)
{
if (source == null) throw new ArgumentException("self");
if (size < 1) throw new ArgumentException("size");
using (var enumerator = source.GetEnumerator())
{
var list = new List<T>(size);
while (enumerator.MoveNext())
{
list.Add(enumerator.Current);
if (list.Count >= size)
{
yield return list;
list = new List<T>();
}
}
// 残りの部分
if (list.Any())
{
yield return list;
}
}
}
}
}
Chunk1<T>() 拡張メソッドは GroupBy() 拡張メソッドを利用したスマートな方法です。Select() 拡張メソッドで 2 回射影をおこなってはいますが、処理時間のオーダーとしては O(n) になっています。GroupBy() 拡張メソッドもハッシュ値による処理をおこなっているため、それほど遅くはならないという予測です。
Chunk2<T>() 拡張メソッドは Take() 拡張メソッドと Skip() 拡張メソッドの組み合わせで、非常に直感的なコードになっています。しかし、一度のループの中で 2 回全要素を走査しているため、処理時間のオーダーは O(n^2) となります。
Chunk3<T>() 拡張メソッドは Linq をあきらめて泥臭くした方法です。IEnumerator<T> を使って先頭要素から順番に指定個数分を List<T> に入れていく方法です。こちらの処理時間のオーダーは O(n) となります。
というわけで、もう結果は見えているような気がしますが、念のため比較をしてみます。
namespace ConsoleApplication2
{
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main(string[] args)
{
var func = new Action<IEnumerable<int>>(sequence =>
{
Console.WriteLine("length = {0} ------------------", sequence.Count());
var size = 13;
double elapsed;
var counter = new TimeCount();
counter.Start();
var chunk1 = sequence.Chunk1(size).ToArray();
elapsed = counter.ElapsedMilliseconds;
Console.WriteLine("Chunk1 : {0, 10:f3}[ms] : {1} : {2}", elapsed, string.Join(" ", chunk1.First().Select(x => x.ToString())), string.Join(" ", chunk1.Last().Select(x => x.ToString())));
counter.Reset();
var chunk2 = sequence.Chunk2(size).ToArray();
elapsed = counter.ElapsedMilliseconds;
Console.WriteLine("Chunk2 : {0, 10:f3}[ms] : {1} : {2}", elapsed, string.Join(" ", chunk2.First().Select(x => x.ToString())), string.Join(" ", chunk2.Last().Select(x => x.ToString())));
counter.Reset();
var chunk3 = sequence.Chunk3(size).ToArray();
elapsed = counter.ElapsedMilliseconds;
Console.WriteLine("Chunk3 : {0, 10:f3}[ms] : {1} : {2}", elapsed, string.Join(" ", chunk3.First().Select(x => x.ToString())), string.Join(" ", chunk3.Last().Select(x => x.ToString())));
Console.WriteLine("");
});
var rnd = new Random();
do
{
Console.Clear();
var source = Enumerable.Range(0, 10000).Select(x => rnd.Next(0, 101)).ToArray();
func(source.Take(1000).ToArray());
func(source.Take(2000).ToArray());
func(source.Take(3000).ToArray());
func(source.Take(4000).ToArray());
func(source.Take(5000).ToArray());
func(source);
} while (Console.ReadKey().Key != ConsoleKey.Escape);
}
}
}
func 変数で 3 つの拡張メソッドを実行するメソッドを準備して、 色々な長さのシーケンスを処理させます。実行結果はこちら。
予想通り Chunk3<T>() 拡張メソッドが一番速い結果となりました。Chunk2<T>() 拡張メソッドは遅いとは思っていましたが、予想を超えてはるかに遅い…。控えめに言って使い物になりませんね。
以上、シーケンスを n 個毎に分割する方法でした。Linq は油断すると処理が遅くなるので要注意。
Tweet
<< 古い記事へ |