空飛ぶ気まぐれ雑記帳

主に趣味とかプログラミングについて扱います。

【Now Reading】A Disentangling Invertible Interpretation Network for Explaining Latent Representations

はじめに

読んでる最中の論文についてまとめる。

読んでいて気づいたけど、自身の論文を大量に引用していて、前提とする知識が多すぎる。。。
結構時間がかかりそう。


まだまだ読見込んでいる段階なので、ちょっとづつ書き足していきます。
そのうち実装もしたいなと思っているので、誤りや解釈違いがあればご指摘ください

タイトルは「A Disentangling Invertible Interpretation Network for Explaining Latent Representations」
https://arxiv.org/abs/2004.13166

ちなみに著者によるPyTorchの実装も存在する。
github.com


関連する研究にはInfo GANやβ-VAE、Factor VAEなどがある。

端的にどういう論文か

タイトルにある通り、ポイントは潜在変数がInvertible Interpretation(潜在表現が可逆変換可能で変換可能な空間で操作可能)でかつdisentangled representation(意味づけされた潜在表現)なことを特徴にした研究だと思う。

f:id:elda27:20200629214051p:plain
Hyunjik Kim, Andriy Mnih ; Proceedings of the 35th International Conference on Machine Learning, PMLR 80:2649-2658, 2018.

まず、disentangled representationとは潜在変数の各次元が独立で人が解釈可能な特徴量を指す(と思う)。
各次元が独立=各次元で意味が重複しないことを示している。
上の図はFactor VAEの論文から拝借してきたものだ。
各行はLatent traversal(潜在変数をある次元について線形に変化させる操作)することで得られた画像である。
それぞれ異なるcomponent(どの要素が変化しているかは画像の右側に書いてある)が変化していることが分かる。
これがdisentangled representationを持つメリットである。

f:id:elda27:20200629212230p:plain
続いて上の図はInvertible Intepretationを示している。
あるX_1X_2があったときにX_1からX_2への線形な変換とは何かを考える際に単純にX_1X_2それぞれから抽出した潜在変数を補完することは果たして正しい操作なのかということに言及している(ように思う、たぶん)。
この図はおそらく一般的なVAEでは間違いなく当てはまるのだけれども、果たしてFactor VAEのようなネットワークでも同様なことが言えるかは疑問符が付く。

Invertible Transformation of Hidden Representations

f:id:elda27:20200629224408p:plain
以下の文字が頻出するのでそれぞれメモ的に残しておく。
 
f(x) = G \circ E(x) \\
f(x): 何らかのネットワーク \\
E(x): 潜在変数 z を生成するネットワーク \\
G: 潜在変数 z からある出力を得る f のサブネットワーク \\
おおむね、Encoder, Decoder型のネットワークを想像して問題ない。
論文中では、特定のネットワークに依存しない内容で書かれているため、このような表現になっているとみられる。
また\tilde{z}zの違いとして\tilde{z}=T(z)かつ\tilde{z}=T(z)である。したがって、\tilde{z}は人が解釈可能な潜在変数でとzはそれを特に満たさないとみられる。

また、Tは逆変換が可能で、zからz^*へのlatent traversalはz \rightarrow z^* \tilde{z} := T^{-1}(T(z)*)とも書かれている。

ここの中身はSupplementary Materialsに詳細が書かれていて、TはCoupling block, ActNorm, Shuffling layersから構成されているらしい。
色々調べているFLOWベース生成モデルでは一般的に使われている構成のようでGLOWやNICEなどいずれも同様な構成になっていた。
FLOWとか全然知らないので、勉強中。
下記のリンクは勉強用のメモ。

qiita.com
qiita.com
peluigi.hatenablog.com

Disentangling Interpretable Concepts

解釈可能な潜在変数の持ち方に関する話。
ぶっちゃけ式変形がわかってない。Supplemental Materialを読めと書いているが一旦保留。

最終的なロスは下記の通り。
f:id:elda27:20200630205129p:plain
z=E(x)xは入力、Fはsemantic conceptsのindexで、要はK次元の人間が理解可能な概念のindexを指す。
\sigma_{ab}in(0,1)\tilde{z^a}\tilde{z^b}の正の相関を離散化したもの、
T'(z)=|T(z)|で表される。

本当によくわかってない。

Estimating Dimensionality of Factors

\tilde{z}が幾つの次数を持つべきかという話。
スコアs_F自体はF番目のsemantic conceptについてペアとなる画像同士から計算される相関係数である。
f:id:elda27:20200630222453p:plain
端的に言えば、コンセプトを同じくする画像同士はE(\cdot)が抽出するz相関係数の和は高くなるはずということだと思う。

それから、F番目のsemantic comceptの次数N_F=\frac{\exp s_F}{\sum^K_{k=0} \exp s_k}N、ただしNは潜在変数zの次数である。

先にも書いたがある次数の相関係数が正=そのsemantic conceptを表現するために必要な変数だと理解している。
下記は実際にColor MNISTに対して計算したN_Fらしい。

(もしかして、これも最適化するのか…?)

f:id:elda27:20200630222800p:plain

分かってないポイント

  • 誤差関数
  • Nは動的に最適化するのか

TensorflowかPyTorchなのか

何のポエムか

Deep Learningをするときは常にChainerを使っていたのですが、Chainerがサポート終了とのことで、フレームワークの乗り換えが必要になった。
その時に、選択肢として浮かんだのがTensorflowとPyTorchだった。
Microsoft Cognitive Toolkit(旧CNTK)とかnnablaとかマイナーなフレームワークはいくつかありますが、Chainer以下の普及率のものを使おうとは思わない。

元々Chainerを使っているときも思ったけど、マイナーなライブラリを使うと何かパクるときに実装の負担が大きいことに気づいた。
その点TensorflowかPyTorchなら問題ない訳で、どちらを使うかを当時は悩んでいた。

ちなみにポエムなので私が何を言いたいかは最後まで読まないと分からなくなっているが悪しからず。

Chainerの何がよかったか

そもそも、Chainerが好きだった理由はソースコードの殆どがPythonで記述されているところだ。
実行速度で他ライブラリに不利であったが、バグを見つけた時にVSCodeでCtrl+クリックでファイルを開いて、何が起きているのか検証できたし、Deep Learningの中身を読むことで勉強にもなった。
昔dlibでDeconvolutionの実装についてPRを送ったときに感じたが、やはりソースコードを読むとこれまで無かった実装に対する知見が得られるだけでなく、論文に書かれた数式に対する解釈が深まるように思う。
それは本質的な話ではなく、ChainerのBackendとしてCuPyが動いていること自体に大きな意味があったと今では思っている。
なぜなら自前でLayerを実装する際に一々高速な実装とするためにC++のコードをビルドする必要がないからだ。
実際問題、CuPyはCUDAの機能を一部しか使えないために自前でLayerを定義するときに困ることは確かにあったが、行列演算をはじめとする簡単な計算をnumpyと同様のインタフェースで操作できるというのは圧倒的な強みだった。
(最近ではどのライブラリも同じようなことができるのでこの部分はもはや強みではないかもしれないが)

結局どっちにしたかと選んだ理由について

とりあえずどちらかを選ぶにあたって、PyTorchとTensorflowで書かれたプログラムを読んでいた。
当時はTensorflow 2.0(alpha版だったか?)とPyTorch 1.0がリリースされたころだったと記憶している。

Tensorflow 1.x台の良くないと思っていたところにインタフェースの多さがあった。
tensorflow 1.xの前半はバージョンが2個上がれば異なる名前空間に同じ機能(若干書き方が違う)が1個増えるような状態だった。
確かにほかのライブラリに比べて高速であったが、既述のばらつきというのはそれだけで、他人のコードを参考にする上で害悪だ。
特に、使う名前空間によって挙動が若干異なるとなるとそれは深刻だった。

一方のPyTorchの良くないと思っていたところにChainerのフォークながら名前の語感が嫌いだった。
当時は何が嫌いなのか分からなかったが最近になって分かるようになってきた。
それは、名前のネーミングがnumpyから遠すぎるからだ。
例えば、unsqueeze。これはsqueezeの逆なので配列の次数を拡張するメソッドであるが、numpyではexpand_dimsだ。
当然、Chainerはexpand_dimsであるが、こいつは違うのだ。
ググるときに困るというのもあるが、それ以上に生理的に受け付けない。
ちなみに、Tensorflow2.0はnumpyに合わせたメンバ名を採用しているので、expand_dimsだ。

何が言いたいか

そういう訳で、現在もTensorflow 2.0を使っている。
ぶっちゃけTensorflow 2.0はあまり普及しておらず、論文の著者実装はだいたいTensorflow 1.xかPyTorchなので、「マイナーなライブラリを使うと何かパクるときに実装の負担が大きいことに気づいた」という点に関して、一切寄与していない。

それでも、名前の語感がNumpyに似ているという点と元々高速な実行速度の強みはTensorflowを使うに十分足ると思う。
もちろん、Tensorflowには結構不便なところもたくさんあって、PyTorchに逃げたくなることも多いが、それでも語感が生理的に受け付けないのは無理だ。
少なくとも、Chainerを使っていたので、読むことはできるから移植に困ることはそうない。

ところで、今回どうしてこのポエムを書いたかと言うと、Pythonのエコシステムに乗ることの重要性について書きたかったからだ。
Pythonはどちらかと言えば一神教な言語だと思っている。パッケージマネージャはpipが主流だし、行列計算はnumpy、グラフはmatplotlib、表データはpandasなどなど。
もちろん、グラフならmayaviとか色々あるが、やっぱりmatplotlibを使うというのが一般的だと思っている。
そして、それらのライブラリがあるが故にほかのライブラリでもやはり語感が揃っている方がPythonらしいと私は思っている。

だから、頼むからunsqueezeとか分けわからん名前を付けるのはやめてくれ。

【Windows】DockerでPythonアプリケーションをビルドする方法

背景

年に一度しか更新されず、前置きが長いポエミーな本ブログで未だによく見られている記事にGUIアプリを作る話があったので、折角ならもうちょっとモダンに何とかできないかと。 思い至ったので、実際に作ってみました。

ところで、私は普段WindowsでAnacondaを使ってPythonを使っているのですが、Anaconda+PyInstallerという組み合わせは非常にまずい組み合わせであります。 何がまずいかと言えば、ファイルサイズです。 Anacondaは依存関係が異常に複雑で、特に依存ライブラリのないプログラムであっても、PyInstallerを使ってExeを作ると200MB近いファイルサイズになってしまいます。 それにPySideや何やら入ってしまうと、どんどんファイルサイズが大きくなってしまいます。

それは何とも残念な話ですので、通常のPythonにPyInstallerをインストールしてビルドすれば、ファイルサイズの増大を回避できますが、いくらvenvをインストールしてやったとしても、それは何かと面倒なことがあります。 まず、新規でアプリケーションを作る度にvenvで環境を作るというのも良いと思いますが、個人的にいつもrequirements.txtの書き忘れが問題になります。 Linuxならpipenvを使えばそれも回避できますが、Windowsでpipenvはまともな動きをしてくれないので、それもダメです。

はてさてどうしたものかとなりますが、いい感じにやる方法にDockerがあります。 Dockerの説明はもう良いでしょと思うので割愛しますが、Dockerを使えば使い捨てのビルド環境が作れるのでいい感じにやれるなと思って今回の記事のようなものを作りました。

Windows container on Windows

Windows環境でWindow Containerを動かすのってどうやんの?って思ったので調べたのですが、タスクトレイのDockerを右クリックして出てくるメニューから切り替えられるそうです。 非常に簡単でありますね。ちなみに、WindowsコンテナとLinuxコンテナは同時に使えない問題があるそうですが、まあそれはそれということでしょうか。 f:id:elda27:20200623210106p:plain

ちなみにですが、Windowsコンテナのライセンスってどうなってんの?ってなことも気になって調べましたが、開発用途なら特に使用に制約は無いようです(ないよね?) docs.microsoft.com

Dockerfile

適当なアプリケーションをビルドする用のDockerfileは下記のとおりです。 pythonソースコードと同じディレクトリにファイルを保存している前提で、以下のようなDockerfileを書きました。

FROM winamd64/python:3.8.3-windowsservercore-1809 

# /tmpにソースコードを展開して必要なファイルをインストール
# 事前にrequirements.txtをコピーして必要なライブラリをインストールして
# キャッシュしておく。
WORKDIR /tmp
COPY requirements.txt /tmp
RUN pip install -r requirements.txt
RUN pip install pyinstaller

# それ以外のソースコードをホストからコピーしてpyinstallerを実行する。
COPY . /tmp
RUN pyinstaller Main.spec
WORKDIR /app
RUN cp -r /tmp/dist/* .
RUN Remove-Item -Path /tmp -Recurse -Force
RUN mkdir /mount
ENTRYPOINT cp -r -Force * /mount

ちなみに使っているpyinstallerのspecファイルは以下の通りです。 依存ライブラリとしてeelを使うアプリケーションを考えているので、ライブラリのインストールディレクトリからdataファイルに追加するようにしています。

# -*- mode: python -*-

import eel
from pathlib import Path

path_to_eel_js = Path(eel.__file__).parent / 'eel.js'

block_cipher = None

a = Analysis(
  ['./app/__main__.py'],
  pathex=[],
  binaries=None,
  datas=[
    (str(path_to_eel_js), './eel/')
  ],
  hiddenimports=[
    'pkg_resources.py2_warn',
    'eel',
    'bottle_websocket'
  ],
  hookspath=None,
  runtime_hooks=None,
  excludes=None,
  cipher=block_cipher
)
pyz = PYZ(
  a.pure, a.zipped_data,
  cipher=block_cipher
)
exe = EXE(
  pyz,
  a.scripts,
  a.binaries,
  a.zipfiles,
  a.datas,
  name='ClipDraw',
  debug=False,
  strip=None,
  upx=True,
  console=True, 
  #icon='mainIcon.ico'
)
app = BUNDLE(
 exe,
 name='app.exe',
 #icon='mainIcon.ico'
)

これだけで勝手にexeファイルができます。

コンテナの使い方

実際にはDockerファイル内でビルドしたパッケージをローカルにダウンロードする必要があるので、以下のコマンドでホストにコピーする必要があります。

docker run --rm -it -v ${pwd}\build:C:\mount python-build-env:latest

ちなみに上記はpowershellで実行するケースでたぶんcmdだと下記のようになります。

docker run --rm -it -v %cd%\build:C:\mount python-build-env:latest

これでWindows環境で動くPythonアプリケーションが出来上がるはずです。 LinuxMacでも基となるOSをそろえてやれば(例えばLinuxならPython:3.8-busterとか)同様の手順でアプリケーションを浮くることができるはずです。

OpenCV 4.2.0のビルドエラー対策

未だにこのブログを見る人がいたのと気が向いたので更新しています。

この記事はカスだ

とりあえず、OPENCV_PYTHON3_VERSIONはBOOLではなく、STRINGで指定するものだ。 しかも普通にバージョンを指定すべき変数だ。 少なくともOpenCV 3.4のブランチを見るとOPENCV_PYTHON3_VERSIONはPython3のバージョンを指定するための変数だった。 世間にあふれる情報が正しいとは限らないということを改めて感じたので、戒めに記事事態は残しておく。

というか、解決策気づいてから同じ変数が二つ並んでいるのに気づかんかったのか。。。

最近の困りごと

なぜかOpenCVをビルドする機会があって、armv7でビルドするとエラーがでないのに、x64でビルドするとcmakeエラーがでるという不具合に見舞われて怒っていた。 より具体的に言えばPython3向けのopencvをビルドしようとするとエラーが出るという話であるが。 再現可能なDockerfileは以下の通り。

FROM alpine:3.11
RUN apk update && apk add boost cmake make clang gcc \
  pkgconfig \
  linux-headers\
  python3\
  curl\
  ninja\
  tar\
  g++\
  python3-dev\
  musl\
  musl-dev

RUN pip3 install numpy

WORKDIR /tmp
RUN curl -sSL https://github.com/opencv/opencv/archive/4.2.0.tar.gz | tar xz
RUN mkdir build-opencv && cd build-opencv \
  cmake -G Ninja \
    -D CMAKE_BUILD_TYPE=Release\
    -D CMAKE_INSTALL_PREFIX=/usr/local\
    -D OPENCV_PYTHON3_VERSION=ON\   <----これを消せば問題なく動いた。。。
    -D BUILD_TESTS=OFF\
    ../opencv-4.2.0

このコードはミニマムなDockerfileでないので実運用上はpython3-devみたいなのはapk add時に--virtual-envを使って最後にpurgeするようにしたほうが良い。

それはさておき、このDockerfileをビルドするとcmakeに失敗してエラーがでる。

-- Found PythonInterp: /usr/bin/python2.7 (found suitable version "2.7.16", minimum required is "2.7")
-- Could NOT find PythonLibs (missing: PYTHON_LIBRARIES PYTHON_INCLUDE_DIRS) (Required is exact version "2.7.16")
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ImportError: No module named numpy.distutils
CMake Error at cmake/OpenCVUtils.cmake:131 (find_package):
  find_package called with invalid argument "ON"
Call Stack (most recent call first):
  cmake/OpenCVDetectPython.cmake:58 (find_host_package)
  cmake/OpenCVDetectPython.cmake:280 (find_python)
  CMakeLists.txt:585 (include)
-- Python is not found: ON EXACT

要約するとPythonインタプリタを探そうとすると2系が引っかかる。 また、Python3を見つけようとしない。 見つけようとしない原因はfind_package called with invalid argument "ON"にあって、謎の引数ONが与えられているところにある。

解決方法

cmakeのオプションに「-DOPENCV_PYTHON3_VERSION=3.8」を追加する

詳細

しかたがないのでソースコードを読む。 これまた面倒だが、エラーが出ているのは毎度おなじみOpenCVが中で書いているcmakeのコードだ。 OpenCV 2.X系をCygwinでビルドする時にFind~が死んでオコになった記憶があるので、あまり触りたくないのだが仕方がない。 ちなみに以下は280行目の呼び出し部分だ。

find_python("${OPENCV_PYTHON3_VERSION}" "${MIN_VER_PYTHON3}" PYTHON3_LIBRARY PYTHON3_INCLUDE_DIR
    PYTHON3INTERP_FOUND PYTHON3_EXECUTABLE PYTHON3_VERSION_STRING
    PYTHON3_VERSION_MAJOR PYTHON3_VERSION_MINOR PYTHON3LIBS_FOUND
    PYTHON3LIBS_VERSION_STRING PYTHON3_LIBRARIES PYTHON3_LIBRARY
    PYTHON3_DEBUG_LIBRARIES PYTHON3_LIBRARY_DEBUG PYTHON3_INCLUDE_PATH
    PYTHON3_INCLUDE_DIR PYTHON3_INCLUDE_DIR2 PYTHON3_PACKAGES_PATH
    PYTHON3_NUMPY_INCLUDE_DIRS PYTHON3_NUMPY_VERSION)

なるほど分からん。 が、ほとんどが変数名を渡している引数なので本質的に重要なのは第2引数までと一目で分かるので、とりあえずその2つをチェックする。

# Find specified Python version
# Arguments:
#   preferred_version (value): Version to check for first
#   min_version (value): Minimum supported version
#   library_env (value): Name of Python library ENV variable to check
#   include_dir_env (value): Name of Python include directory ENV variable to check
#   found (variable): Set if interpreter found
#   executable (variable): Output of executable found
#   version_string (variable): Output of found version
#   version_major (variable): Output of found major version
#   version_minor (variable): Output of found minor version
#   libs_found (variable): Set if libs found
#   libs_version_string (variable): Output of found libs version
#   libraries (variable): Output of found Python libraries
#   library (variable): Output of found Python library
#   debug_libraries (variable): Output of found Python debug libraries
#   debug_library (variable): Output of found Python debug library
#   include_path (variable): Output of found Python include path
#   include_dir (variable): Output of found Python include dir
#   include_dir2 (variable): Output of found Python include dir2
#   packages_path (variable): Output of found Python packages path
#   numpy_include_dirs (variable): Output of found Python Numpy include dirs
#   numpy_version (variable): Output of found Python Numpy version
function(find_python preferred_version min_version library_env include_dir_env
         found executable version_string version_major version_minor
         libs_found libs_version_string libraries library debug_libraries
         debug_library include_path include_dir include_dir2 packages_path
         numpy_include_dirs numpy_version)

つまるところ、第1引数が希望するバージョン、第2引数がミニマムなバージョンらしい。 しかもよく見ると、find_pythonの直上にoption(OPENCV_PYTHON3_VERSION "Python3 version" "") が存在するので、これを設定すれば良いことも自明であった。

悲しいかな。 ということで、cmakeに-D OPENCV_PYTHON3_VERSION=3.8で今回のケースは終了。 ソースコードを読むことが解決の最短ルートということでした。 数年に一度OpenCVのビルドに苦しめられるので、どなたかOpenCVDetectPythonでPythonを見つけられない原因について調べてpull reqしておいて貰えると助かります。

PythonでMarkdownをHTMLに変換する

4月になってから環境が変わってクソ忙しくなったせいか、録にブログを更新できず。
なんだかんだで、もう5月も終わり。プログラムを書いてないわけじゃないけど、githubの履歴を見る限り一ヶ月でたったの6000行しか書いてないので、全然ですね。
それはさておき、流石に月間更新数0はいただけないので、何か書こうと思っていたら、ネタはあったので投下します。

# なぜにPythonで?
ただMarkdownを変換するだけならpandocなりを使えば容易に実現可能なのですが、table要素にはclassを追加して、相対パスの画像をサーバにアップロードしたURLに変換してという作業を自動化するのはそう簡単ではありません。
それでペチペチとGoogle先生にご教示願ったら、それっぽいのがちゃんとあったんですよね。


Python Markdown

# 見付けたは良いが…
ただ、コイツ。ドキュメントを読んでもさっぱり使い方がわからない。
いや、少なくとも、Markdown->HTMLに変換する方法はよく分かるのだけれども、それ以上のことをしようとすると何をする必要があるのかさっぱりわからない。
実際Githubのissueを見ると山のような質問。あっ察しというわけで、再びGoogle先生にご教示願うと無事それっぽいのを見つけることができました。
ana-balica.github.io

一通りの使い方はなんとなく書いてあったので、実装してみました。
以下が実際のコード

#encoding:UTF-8

import sys
import glob
import os
import os.path
import markdown
import codecs
from markdown import Extension
from markdown.util import etree
from markdown.treeprocessors import Treeprocessor
from markdown.extensions import tables, toc

def main(argv):
  if len(argv) < 3:
    print('convert.py <Input directory> <output directory>')
    return

  md = markdown.Markdown(extensions = [ElementTreeExtension(), 'markdown.extensions.tables', 'markdown.extensions.toc'])

  input_dir = argv[1]
  output_dir = argv[2]

  try:
    os.mkdir(output_dir)
  except OSError as e:
    if not os.path.exists(output_dir):
      rethrow()

  for file in glob.iglob(os.path.join(input_dir, "**/*.md"), recursive=True):
    print(file)
    output_filename = get_output_filename(file, input_dir, output_dir)
    with codecs.open(file, 'r', 'UTF-8') as fp, codecs.open(output_filename, 'w+', 'UTF-8') as ofp :    
      text = fp.read()
      markdown_text = md.convert(text)
      ofp.write(markdown_text)

class ElementTreeExtension(Extension):
  def extendMarkdown(self, md, md_globals):
    md.treeprocessors.add('elementtreeprocessor', ElementTreeProcessor(), '_end')
    md.registerExtension(self)
class ElementTreeProcessor(Treeprocessor):
  def run(self, root):
    h1s = root.getiterator("h1")
    for h1 in h1s:
      h1.set("class", "h")
    
def get_output_filename(input_filename, input_root_dir, output_dir):
  input_dir, filename = os.path.split(input_filename)
  base, ext = os.path.splitext(filename)
  relative_root_to_file = os.path.relpath(input_dir, input_root_dir)
  output_full_path = os.path.join(output_dir, relative_root_to_file)
  if not os.path.isdir(output_full_path) and not os.path.exists(output_full_path):
    os.mkdir(output_full_path)

  return os.path.join(output_full_path, base + ".html")

if __name__ == '__main__':
  main(sys.argv)

第1引数にMarkdown形式のファイルが保存されたディレクトリ。
第2引数に出力ファイルが保存されるディレクトリを設定すれば良い。
これで変換したhtmlのh1要素にhというclassが自動で追加されるようになっている。

スクリプトとDLLの間に生きる時のデバッグ

はじめに

4月になって環境が変わって色々と忙しい今日このごろ。
気づけば4月も末日、そろそろGW前だというのに未だに慣れず。

そう、4月末なのに今月はまだ1記事も書いてないのです。
マズイ…

というわけで、今回はちょっとしたデバッグのテクニックをご紹介。
他の人がCでDLL作ったけど、Pythonから関数呼び出して使うとなぜか動かない…でも、他人のソースを全部読むのはダルい…という場合にオススメです。

DLLを作る

とりあえず、今回は最小限の構成でDLLを作るのでヘッダーファイルなしのcppファイルのみです。

CMakeLists.txt

cmake_minimum_required(VERSION 3.3)

project(DLL)

add_library(${PROJECT_NAME} SHARED DLLFunc.cpp)

DLLFunc.cpp

#include <iostream>
#include <random>

#define DLL_EXPORT __declspec(dllexport)

extern "C" DLL_EXPORT float add(int x, int y)
{
  float value = std::random_device()();
  return x + y + value;
}

extern "C" DLL_EXPORT void print(const char* s)
{
  std::cout << "DLL:" << s << std::endl;
}


これをcmakeしてからビルドすればとりあえず、DLLが出来上がります。
この時、DLLのデバッグをしたいので、DLLのビルド時のConfigはReleaseではなく、DebugかRelWithDebInfoを選択してください。
このRelWithDebInfoというのは最適化は掛けるけど、デバッグシンボルを埋め込むため、ブレークポイントを挿入したときにちゃんと止まってくれるようになります。(実際は一部最適化の関係でブレークポイントを挿入できない場所もあります)

DLLのテスト

最初はとりあえず、C++からテスト。というわけで、下記のコードを使います。コマンドライン引数にDLLのパス(Debugビルドのもの推奨)を与えてください
なお、もちろんですが下記のファイルは別のディレクトリに保存してくださいね。

CMakeLists.txt

cmake_minimum_required(VERSION 3.3)

project(Main)

add_executable(${PROJECT_NAME} main.cpp)

main.cpp

#include <iostream>
#include <string>
#include <Windows.h>

int main(int argc, char** argv)
{
  if (argc < 2)
  {
    std::cerr << "Wrong arguments!\nMain.exe <Input dll path>" << std::endl;
    return -1;
  }

  auto dll = LoadLibraryA(argv[1]);

  auto print_fn = GetProcAddress(dll, "print");
  auto add_fn = GetProcAddress(dll, "add");

  auto value = reinterpret_cast<float(*)(int x, int y)>(add_fn)(10, 20);
  reinterpret_cast<void(*)(const char*)>(print_fn)(std::to_string(value).c_str());

  FreeLibrary(dll);

  return 0;
}


これを実行すると何か標準出力に出るはずです。
このとき、Mainソリューションを開いた状態でDLLFunc.cppをVisualStudioで開き、ブレークポイントを設定すればちゃんとブレークしてくれるはずです。

Pythonで使う

ここまでだとぶっちゃけあまり意味がないのでPythonスクリプトから呼び出します。
実際に使ったのは下記コード。

import ctypes
import sys

print('Type input dll')
input_dll = input()
print('--------------------------------------')
dll = ctypes.cdll.LoadLibrary(input_dll)

dll.print.argtypes = [ctypes.c_char_p]
dll.add.restype = ctypes.c_float
dll.add.argtypes = [ctypes.c_int, ctypes.c_int]

value = dll.add(10, 20)
dll.print(ctypes.c_char_p(str(value).encode('utf-8')))


基本的には先程のテストと同様ですが今回は実行時にちょっとした処理を必要とするため、標準入力からdllファイルのパスを受け取るようにしています。
それから実際に実行して、DLLのパスを入力する前にブレークポイントを入れるためにちょっとした処理を行います。

まず、Visual Studioを起動します。次に「デバッグ>プロセスにアタッチ」もしくはctrl+alt+pで表示されるダイアログの選択可能なプロセスから「python.exe」を探します。pとか押せばpのとこまで飛べるんでそれを活用しつつ選んでから「アタッチ」ボタンを押せばアタッチ完了です。
この状態でDLLFunc.cppを開いてブレークポイントを入れれば、pythonから該当する関数を呼び出すとちゃんとブレークポイントで止まります。

まとめ

DLLデバッグしたいと思ったらとりあえずctrl+alt+pでプロセスにアタッチと覚えとけばいいと思います。
今回は簡単な例でしたが、DLL内で例外が発生して落ちてる場合もVisual Studioがキャッチしてくれて、その時のコールスタックが見れるので、それを活用してデバッグしましょう。
それとPython以外でもmatlabやRなんかでも使えるので(matlabの場合javaで動いている影響かガンガン例外が飛び交うので少々知識が必要ですが)是非お試しあれ

OpenCVのDeformable Part Modelを試してみた

はじめに

今回は、故あってOpenCVのDPM(Deformable Part Model)を試してみたので、それについて紹介したいと思います。

DPMって?

DPMとは物体を検出するアルゴリズムの1つで、入力画像からHOG特徴を抽出し、事前に学習しておいた物体全体のモデルと物体をパーツ毎に分けた変形可能なモデルの2種類を併用することで、ロバストに検出することができるアルゴリズムです。

結構古いアルゴリズムで、原著論文が2007年位なのですが、R-CNN(Region-based Convolutional Neural Network)と比較すると高速で、実時間で利用可能なためかロボティクス系の分野では未だにチラチラ見かけます。

まあ、R-CNNも段々と改良されて、高速になりつつあって、実時間で利用可能なアルゴリズムもあった気がするので、近いうちに撲滅されかねませんが。

なお、アルゴリズムは下記が詳しいです。

www.slideshare.net

OpeCVのDPM

OpenCV3.0以降ではOpenCV Contribに入っているのでそれを使います。
公式ドキュメントとサンプルコードを参考に試してました。

http://docs.opencv.org/trunk/df/dba/classcv_1_1dpm_1_1DPMDetector.htmldocs.opencv.org
github.com

サンプルコードでは、動画を前提としてフレームレートの計算とかしていますが、私は動画を用意する余裕が無かったので、該当部分の削除等行ったソースコードを用意しました。
なお、Windows環境の場合、OpenCV公式のサンプルコードだと改行コードでポシャるので、std::ifstreamの引数に注意が必要です。(下記サンプルでは修正済みです)

#include <opencv2/dpm.hpp>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>

#include <filesystem>

#include <iostream>
#include <fstream>

#if defined(DEBUG) || defined(_DEBUG)
# pragma comment(lib, "opencv_core310d.lib")
# pragma comment(lib, "opencv_dpm310d.lib")
# pragma comment(lib, "opencv_highgui310d.lib")
# pragma comment(lib, "opencv_imgproc310d.lib")
# pragma comment(lib, "opencv_imgcodecs310d.lib")
#else
# pragma comment(lib, "opencv_core310.lib")
# pragma comment(lib, "opencv_dpm310.lib")
# pragma comment(lib, "opencv_highgui310.lib")
# pragma comment(lib, "opencv_imgproc310.lib")
# pragma comment(lib, "opencv_imgcodecs310.lib")
#endif

void help();
bool readImageLists(const std::string &file, std::vector<std::string> &imgFileList);
void drawBoxes(cv::Mat &frame,
  std::vector<cv::dpm::DPMDetector::ObjectDetection> ds,
  cv::Scalar color);

int main(int argc, char** argv) try
{
  const char* keys =
  {
    "{@model_path    | | Path of the DPM cascade model}"
    "{@image_dir     | | Directory of the images      }"
  };

  cv::CommandLineParser parser(argc, argv, keys);
  std::string model_path(parser.get<std::string>(0));
  std::string image_dir(parser.get<std::string>(1));
  std::string image_list = image_dir + "/files.txt";

  if (model_path.empty() || image_dir.empty())
  {
    help();
    return -1;
  }

  std::vector<std::string> imgFileList;
  if (!readImageLists(image_list, imgFileList))
    return -1;

#ifdef HAVE_TBB
  std::cout << "Running with TBB" << std::endl;
#else
#ifdef _OPENMP
  std::cout << "Running with OpenMP" << std::endl;
#else
  std::cout << "Running without OpenMP and without TBB" << std::endl;
#endif
#endif

  cv::Ptr<cv::dpm::DPMDetector> detector = cv::dpm::DPMDetector::create(std::vector<std::string>(1, model_path));

  cv::namedWindow("DPM Cascade Detection", 1);
  // the color of the rectangle
  cv::Scalar color(0, 255, 255); // yellow
  cv::Mat frame;

  const int MAX_LONGSIDE = 600;
  for (size_t i = 0; i < imgFileList.size(); i++)
  {
    std::vector<cv::dpm::DPMDetector::ObjectDetection> ds;

    cv::Mat image = cv::imread(image_dir + "/" + imgFileList[i]);
    if (image.cols > MAX_LONGSIDE)
    {
      cv::resize(image, image, cv::Size(MAX_LONGSIDE, static_cast<float>(MAX_LONGSIDE) * image.rows / image.cols));
    }
    else if (image.rows > MAX_LONGSIDE)
    {
      cv::resize(image, image, cv::Size(static_cast<float>(MAX_LONGSIDE) * image.cols / image.rows, MAX_LONGSIDE));
    }

    frame = image.clone();

    if (image.empty()) {
      std::cerr << "\nInvalid image:\n" << imgFileList[i] << std::endl;
      return -1;
    }

    // detection
    detector->detect(image, ds);
    
    // draw boxes
    drawBoxes(frame, ds, color);
    std::cout << "Frame:" << i << std::endl;

    // show detections
    imshow("DPM Cascade Detection", frame);

    cv::waitKey(0);

    cv::imwrite(image_dir + "/s" + std::to_string(i + 1) + ".jpg", frame);
  }

  return 0;
}
catch (std::exception& e)
{
  std::cerr << e.what() << std::endl;
  throw;
}

void help()
{
  std::cout << "\nThis example shows object detection on image sequences using \"Deformable Part-based Model (DPM) cascade detection API\n"
    "Call:\n"
    "./example_dpm_cascade_detect_sequence <model_path> <image_dir>\n"
    "The image names has to be provided in \"files.txt\" under <image_dir>.\n"
    << std::endl;
}


bool readImageLists(const std::string &file, std::vector<std::string> &imgFileList)
{
  std::ifstream in(file.c_str(), std::ios::in);

  if (in.is_open())
  {
    while (in)
    {
      std::string line;
      std::getline(in, line);
      if (line.empty())
      {
        continue;
      }

      imgFileList.push_back(line);
    }
    return true;
  }
  else
  {
    std::cerr << "Invalid image index file: " << file << std::endl;
    return false;
  }
}


void drawBoxes(cv::Mat &frame, std::vector<cv::dpm::DPMDetector::ObjectDetection> ds, cv::Scalar color)
{
  if (ds.empty())
  {
    return;
  }

  cv::rectangle(frame, ds[0].rect, cv::Scalar(0, 0, 255), 2);
  for (unsigned int i = 1; i < ds.size(); i++)
  {
    cv::rectangle(frame, ds[i].rect, color, 2);
  }
}

また、今回は手間を省くために学習済のモデルを使うので、下記リンクのmotorbike.xmlを用います。

github.com

実践

今回紹介しているサンプルでは、下記のように引数を与える必要があります。

TestDPM motorbike.xml <画像が保存されているディレクトリ> 

また、画像が保存されているディレクトリに、files.txtというファイル名の画像ファイルを改行区切りで列挙する必要があります。
なお、画像ファイルのパスはfiles.txtからの相対パスでOKです。

実際にバイクに対して適応した結果は下記の通りです。赤色が最も評価値が高い矩形で、黄色がその他の候補
4枚目は、さすがに斜めから撮影した画像を学習していないせいか、明らかに失敗していますが、他は結構色が似ていて難しいものもあるのですが、検出精度は良好。
ただ、メモリの使用量が尋常ではなく、600✕450の画像ファイルを処理するのに1GBほどのメモリを必要とします。

f:id:elda27:20170329005714j:plainf:id:elda27:20170329005719j:plainf:id:elda27:20170329005723j:plainf:id:elda27:20170329005727j:plainf:id:elda27:20170329005732j:plainf:id:elda27:20170329005736j:plainf:id:elda27:20170329005743j:plain

おわりに

結構いい感じに動いたので、万々歳ですね。
その割に、日本語でDPMについて検索しても使ってみたみたいな話が非常に少ない(論文はちらほら見るのですが、ブログで取り上げてる例が少ない)のが不思議なところです。
次回は自力でモデルを作りたいと思います。