Caffe2: Tutorial: Caffe2 基本概念 – 演算子 & ネット

Caffe2: Tutorial: Caffe2 基本概念 – 演算子 & ネット
翻訳 : (株)クラスキャット セールスインフォメーション
日時 : 04/21/2017

* 本ページは、Caffe2 Tutorials の Caffe2 Basic Concepts – Operators & Nets を動作確認・翻訳した上で
適宜、補足説明したものです:
https://github.com/caffe2/caffe2/blob/master/caffe2/python/tutorials/Basics.ipynb

 

このチュートリアルでは Caffe2 の基本のセットを通り抜けます : 基本概念はどのように演算子とネットが書かれるかを含みます。

最初に、caffe2. core と workspace を import しましょう、通常は最も必要とする2つです。caffe2 により生成された protocol buffer を操作することを望むのであれば、caffe2.proto から caffe2_pb2 を import することも多分望むでしょう。

# We'll also import a few standard python libraries
from matplotlib import pyplot
import numpy as np
import time

# These are the droids you are looking for.
from caffe2.python import core, workspace
from caffe2.proto import caffe2_pb2

(訳注: 通常は %matplotlib inline の記述も必要です。)

caffe2 が GPU サポートを持たないという警告を見るかもしれません。これは CPU-only ビルドを実行していることを意味します。心配は無用です – どのような CPU でも依然として問題なく動作可能です。

 

Workspaces

最初に workspace をカバーしましょう、そこでは全てのデータが存在します (reside)。もし Matlab に精通しているのであれば、workspace はメモリに作成してストアした blob から成ります。当面は、blob は numpy の ndarray に類似した N-次元テンソルと考えてください、しかし連続しています。近々、blob は実際には C++ オブジェクトの任意の型をストアできる型付きポインタであることを示します、しかしテンソルが blob にストアされるもっとも一般的な型です。インターフェイスがどのように見えるかを示しましょう。

Blobs() は workspace に存在する全ての blob を出力します。HasBlob() は blob が workspace に存在するかを問い合わせます。当面は、まだ何も保持していません。

print("Current blobs in the workspace: {}".format(workspace.Blobs()))
print("Workspace has blob 'X'? {}".format(workspace.HasBlob("X")))
Current blobs in the workspace: []
Workspace has blob 'X'? False

FeedBlob() を使用して blob を workspace に供給できます。

X = np.random.randn(2, 3).astype(np.float32)
print("Generated X from numpy:\n{}".format(X))
workspace.FeedBlob("X", X)
Generated X from numpy:
[[ 0.95215136  0.04019461 -1.22403371]
 [-0.18039554 -1.94116426  0.02027692]]
True

さて、workspace にどのような blob があるか見てみましょう。

print("Current blobs in the workspace: {}".format(workspace.Blobs()))
print("Workspace has blob 'X'? {}".format(workspace.HasBlob("X")))
print("Fetched X:\n{}".format(workspace.FetchBlob("X")))
Current blobs in the workspace: [u'X']
Workspace has blob 'X'? True
Fetched X:
[[ 0.95215136  0.04019461 -1.22403371]
 [-0.18039554 -1.94116426  0.02027692]]

配列が同じものであるか確認しましょう。

np.testing.assert_array_equal(X, workspace.FetchBlob("X"))

また、存在しない blob にアクセスしようとする、エラーが投げられます :

try:
    workspace.FetchBlob("invincible_pink_unicorn")
except RuntimeError as err:
    print(err)
[enforce fail at pybind_state.cc:150] ws->HasBlob(name). Can't find blob: invincible_pink_unicorn 

すぐには使わないであろうことを一つ : 異なる名前を使用して Python では複数の workspace を持つことが可能で、それらの間で切り替えられます。異なる workspace の blob は互いに分離されています。CurrentWorkSpace を使用して現在の workspace を問い合わせることができます。workspace を名前 (gutentag) で切り替えて新しい一つを存在しないならば作成してみましょう。

print("Current workspace: {}".format(workspace.CurrentWorkspace()))
print("Current blobs in the workspace: {}".format(workspace.Blobs()))

# Switch the workspace. The second argument "True" means creating 
# the workspace if it is missing.
workspace.SwitchWorkspace("gutentag", True)

# Let's print the current workspace. Note that there is nothing in the
# workspace yet.
print("Current workspace: {}".format(workspace.CurrentWorkspace()))
print("Current blobs in the workspace: {}".format(workspace.Blobs()))
Current workspace: default
Current blobs in the workspace: [u'X']
Current workspace: gutentag
Current blobs in the workspace: []

default workspace にスイッチバックしましょう。

workspace.SwitchWorkspace("default")
print("Current workspace: {}".format(workspace.CurrentWorkspace()))
print("Current blobs in the workspace: {}".format(workspace.Blobs()))
Current workspace: default
Current blobs in the workspace: [u'X']

最後に、ResetWorkspace() は現在の workspace にある任意のものをクリアします。

workspace.ResetWorkspace()
True

演算子 (Operators)

Caffe2 の演算子は関数のようなものです。C++ 側からは、それらは全てコモン・インターフェイスに由来し、型で登録され、実行時に異なる演算子を呼び出すことができます。演算子のインターフェイスは caffe2/proto/caffe2.proto で定義されます。基本的には、それは多くの入力を取り、多くの出力を生成します。

覚えておいてください、Caffe2 Python で “演算子を作成する” と言う時は、まだ何も実行されていません。それが行なうことの全ては、演算子が何であるかを指定する protocol buffer を作成することだけです。後で実行のために C++ バックエンドに送られます。もし protobuf に慣れていないのであれば、構造化データのための json ライクなシリアライゼーション・ツールです。protocol buffer についての詳細は ここ で見つかります。

実際の例を見ましょう。

# Create an operator.
op = core.CreateOperator(
    "Relu", # The type of operator that we want to run
    ["X"], # A list of input blobs by their names
    ["Y"], # A list of output blobs by their names
)
# and we are done!

言及したように、作成された op は実際には protobuf オブジェクトです。内容を表示しましょう。

print("Type of the created op is: {}".format(type(op)))
print("Content:\n")
print(str(op))
Type of the created op is: <class 'caffe2.proto.caffe2_pb2.OperatorDef'>
Content:

input: "X"
output: "Y"
name: ""
type: "Relu"

良いでしょう、演算子を実行しましょう。最初に workspace に入力 X において供給します。それから演算子を実行する最も単純な方法は workspace.RunOperatorOnce(operator) を行なうことです。

workspace.FeedBlob("X", np.random.randn(2, 3).astype(np.float32))
workspace.RunOperatorOnce(op)
True

実行後、演算子が正しいことを行なったか見てみましょう、それはこの場合ニューラルネットワークの活性化関数 (ReLU) です。

print("Current blobs in the workspace: {}\n".format(workspace.Blobs()))
print("X:\n{}\n".format(workspace.FetchBlob("X")))
print("Y:\n{}\n".format(workspace.FetchBlob("Y")))
print("Expected:\n{}\n".format(np.maximum(workspace.FetchBlob("X"), 0)))
Current blobs in the workspace: [u'X', u'Y']
X:
[[-0.93145239  1.33625531 -0.44922987]
 [ 0.154258    0.17276877  1.04186237]]
Y:
[[ 0.          1.33625531  0.        ]
 [ 0.154258    0.17276877  1.04186237]]
Expected:
[[ 0.          1.33625531  0.        ]
 [ 0.154258    0.17276877  1.04186237]]

このサンプルでは Expected 出力が Y 出力とマッチすればこれは動作しています。

演算子はまた必要ならばオプションの引数を取ります。それらは key-value ペアで指定されます。一つの単純な例を見てみましょう、それはテンソルを取りそれを Gaussian ランダム変数で満たします。

op = core.CreateOperator(
    "GaussianFill",
    [], # GaussianFill does not need any parameters.
    ["Z"],
    shape=[100, 100], # shape argument as a list of ints.
    mean=1.0,  # mean as a single float
    std=1.0, # std as a single float
)
print("Content of op:\n")
print(str(op))
Content of op:

output: "Z"
name: ""
type: "GaussianFill"
arg {
  name: "std"
  f: 1.0
}
arg {
  name: "shape"
  ints: 100
  ints: 100
}
arg {
  name: "mean"
  f: 1.0
}

実行して物事が意図したものであるか見てみましょう。

workspace.RunOperatorOnce(op)
temp = workspace.FetchBlob("Z")
pyplot.hist(temp.flatten(), bins=50)
pyplot.title("Distribution of Z")
<matplotlib.text.Text at 0x7fdffdb1b050>

ベル形状のカーブが見れたらそれは動作しています!

 

Nets

Net は本質的には計算グラフです。後方一貫性 (backward consistency) のため Net という名前を保持します (そしてまたニューラルネットに敬意を表しています)。プログラムがコマンドのシークエンスとして書かれるように Net は複数の演算子から成ります。見てみましょう。

net について語る時、BlobReference についてもまた話しをするでしょう、これは文字列まわりをラップするオブジェクトですので演算子の簡単な連鎖を行なうことができます。

ネットワークを作成しましょう、これは本質的には次の Python math と同値です :

X = np.random.randn(2, 3)
W = np.random.randn(5, 3)
b = np.ones(5)
Y = X * W^T + b

進捗を一歩ずつ示します。Caffe2 の core.Net は NetDef protocol buffer 回りのラッパー・クラスです。

ネットワークを作成する時、その基礎となる protocol buffer はネットワーク名以外は本質的には空です。net を作成して proto の内容を表示します。

net = core.Net("my_first_net")
print("Current network proto:\n\n{}".format(net.Proto()))
Current network proto:

name: "my_first_net"

X と呼ぶ blob を作成して、あるランダム・データで満たすために GaussianFill を使用します。

X = net.GaussianFill([], ["X"], mean=0.0, std=1.0, shape=[2, 3], run_once=0)
print("New network proto:\n\n{}".format(net.Proto()))

New network proto:

name: "my_first_net"
op {
  output: "X"
  name: ""
  type: "GaussianFill"
  arg {
    name: "std"
    f: 1.0
  }
  arg {
    name: "run_once"
    i: 0
  }
  arg {
    name: "shape"
    ints: 2
    ints: 3
  }
  arg {
    name: "mean"
    f: 0.0
  }
}

先の core.CreateOperator 呼び出しとの 2,3 の違いに気がついたかもしれません。基本的には、net を持つ時、演算子は直接作成可能で Python トリックを使って同時にそれを net に追加することができます : 本質的には、SomeOp を演算子の登録された型文字列 (type string) として net.SomeOp を呼び出した場合、これは本質的には次のように翻訳されます :

op = core.CreateOperator(“SomeOp”, …)
net.Proto().op.append(op)

 
また、X が何か疑問に思うかもしれません。X は基本的には次の2つのことを記録する BlobReference です :

  • その名前が何であるか。str(X) により名前にアクセスできます。
  • それがどのネットから作成されたか。内部変数 _from_net で記録されますが、それは必要ない可能性が高いでしょう。

それを確かめましょう。また、思い出してください、実際にはまだ何も実行していません、従って X はシンボル以外は何も含みません。現時点ではそれから数値を得ることは期待しないでください 🙂

print("Type of X is: {}".format(type(X)))
print("The blob name is: {}".format(str(X)))
Type of X is: <class 'caffe2.python.core.BlobReference'>
The blob name is: X

続けて W と b を作成しましょう。

W = net.GaussianFill([], ["W"], mean=0.0, std=1.0, shape=[5, 3], run_once=0)
b = net.ConstantFill([], ["b"], shape=[5,], value=1.0, run_once=0)

さて、一つの単純なコード・シュガーとして : BlobReference オブジェクトはそれがどの net から生成されたかを知っていますので、net から演算子を作成することに加えて、BlobReference から演算子も作成できます。FC 演算子をこのように作成しましょう。

Y = X.FC([W, b], ["Y"])

その下では、X.FC(…) は、X を相当する演算子の最初の入力として挿入することにより単に net.FC に委任していますので、上は以下と同値です :

Y = net.FC([X, W, b], [“Y”])

 
現在のネットワークを見てみましょう。

print("Current network proto:\n\n{}".format(net.Proto()))
Current network proto:

name: "my_first_net"
op {
  output: "X"
  name: ""
  type: "GaussianFill"
  arg {
    name: "std"
    f: 1.0
  }
  arg {
    name: "run_once"
    i: 0
  }
  arg {
    name: "shape"
    ints: 2
    ints: 3
  }
  arg {
    name: "mean"
    f: 0.0
  }
}
op {
  output: "W"
  name: ""
  type: "GaussianFill"
  arg {
    name: "std"
    f: 1.0
  }
  arg {
    name: "run_once"
    i: 0
  }
  arg {
    name: "shape"
    ints: 5
    ints: 3
  }
  arg {
    name: "mean"
    f: 0.0
  }
}
op {
  output: "b"
  name: ""
  type: "ConstantFill"
  arg {
    name: "run_once"
    i: 0
  }
  arg {
    name: "shape"
    ints: 5
  }
  arg {
    name: "value"
    f: 1.0
  }
}
op {
  input: "X"
  input: "W"
  input: "b"
  output: "Y"
  name: ""
  type: "FC"
}

 
Too verbose huh? グラフとして可視化してみましょう。Caffe2 はこの目的のために最小限のグラフ可視化ツールとともに提供されます。ipython でそれを見ましょう。

from caffe2.python import net_drawer
from IPython import display
graph = net_drawer.GetPydotGraph(net, rankdir="LR")
display.Image(graph.create_png(), width=800)

 
さて Net を定義しました、しかしまだ何も実行されていません。上の net は本質的にはネットワークの定義を持つ protobuf であることを思い出してください。実際にネットワークを実行することを望む時、その下で起きることは :

  • protobuf から C++ net オブジェクトをインスタンス化する ;
  • インスタンス化された net の Run() 関数を呼び出す。

何かを行なう前には、ResetWorkspace() で以前の workspace 変数をクリアするべきです。

そして Python から net を実行するには2つの方法があります。後のサンプルでは最初のオプションを行ないます。

  1. workspace.RunNetOnce() を使用します、これはネットワークをインスタンス化し、実行して直ちにデストラクトします。
  2. もう少し複雑で2つのステップを含みます : (a) workspace による所有される C++ net オブジェクトを作成するために workspace.CreateNet() を呼び出します、そして (b) ネットワークの名前を渡すことにより workspace.RunNet() を呼び出します。
workspace.ResetWorkspace()
print("Current blobs in the workspace: {}".format(workspace.Blobs()))
workspace.RunNetOnce(net)
print("Blobs in the workspace after execution: {}".format(workspace.Blobs()))
# Let's dump the contents of the blobs
for name in workspace.Blobs():
    print("{}:\n{}".format(name, workspace.FetchBlob(name)))
Current blobs in the workspace: []
Blobs in the workspace after execution: [u'W', u'X', u'Y', u'b']
W:
[[-0.3918522   0.97351104  1.47800612]
 [-0.10992592 -0.82484931 -1.22697937]
 [ 0.76055688 -0.0940667  -0.69477838]
 [-1.06389129 -1.15665662  0.48218504]
 [-0.12535553  2.87811232 -1.03707039]]
X:
[[-0.51353592 -1.47136855  0.544806  ]
 [-0.20765217  0.42352653 -0.03023981]]
Y:
[[ 0.57406324  1.60164261  0.36931407  3.5109117  -3.73539162]
 [ 1.448982    0.7105844   0.82323897  0.71646345  2.27634811]]
b:
[ 1.  1.  1.  1.  1.]

さて2つ目の方法を試して net を作成して、そしてそれを実行しましょう。最初に ResetWorkspace() で変数をクリアし、先に作成した workspace の net オブジェクトにより CreateNet(net_object) で net を作成し 、そして名前により RunNet(net_name) で net を実行します。

workspace.ResetWorkspace()
print("Current blobs in the workspace: {}".format(workspace.Blobs()))
workspace.CreateNet(net)
workspace.RunNet(net.Proto().name)
print("Blobs in the workspace after execution: {}".format(workspace.Blobs()))
for name in workspace.Blobs():
    print("{}:\n{}".format(name, workspace.FetchBlob(name)))
Current blobs in the workspace: []
Blobs in the workspace after execution: [u'W', u'X', u'Y', u'b']
W:
[[-0.69486839 -0.44680837 -1.0081656 ]
 [ 0.50875938  0.00922652  0.29405427]
 [ 1.19760656 -1.65450573 -0.27045119]
 [ 0.40573624  0.77259463 -0.04206046]
 [-0.56559163 -0.73117679 -0.37961239]]
X:
[[ 2.8032155   2.48511195 -0.31391221]
 [ 0.85310286  0.34384146  0.75974047]]
Y:
[[-1.7417593   2.35678387  0.33041543  4.07055378 -2.28336644]
 [-0.51236963  1.6606015   1.24732113  1.57982981 -0.02232361]]
b:
[ 1.  1.  1.  1.  1.]

RunNetOnce と RunNet の間には 2,3 の違いしかありませんが、多分主な違いは計算時間のオーバーヘッドでしょう。RunNetOnce は Python と C の間で渡す protobuf のシリアライズとネットワークのインスタンス化を含みますので、実行するためにはより長くかかるかもしれません。このケースでオーバヘッドが何か見ましょう。

# It seems that %timeit magic does not work well with
# C++ extensions so we'll basically do for loops
start = time.time()
for i in range(1000):
    workspace.RunNetOnce(net)
end = time.time()
print('Run time per RunNetOnce: {}'.format((end - start) / 1000))

start = time.time()
for i in range(1000):
    workspace.RunNet(net.Proto().name)
end = time.time()
print('Run time per RunNet: {}'.format((end - start) / 1000))
Run time per RunNetOnce: 3.61049175262e-05
Run time per RunNet: 2.70199775696e-06

OK、上は 2, 3の重要なコンポーネントです、もし Caffe2 を python 側から使用したいのであれば。必要性があれば更にチュートリアルに追加していきます。取り敢えずは、チュートリアルの残りをチェックしてください!

 

以上