ゼロから作るDeep Learning ❸ をC++で実装する。
「ゼロから作るDeep Learning ❸」を参考にしながら、ディープラーニング用フレームワーク DeZero を C++ で実装しながら学習する。
目的は、「ゼロから作るDeep Learning ❸」を再読しながら理解を深めることと、C++ の復習。
DeZero で使用している Python 用のライブラリや外部ツールの全てを C++ で用意することは難しいかもしれないが、取り合えず以下の構成で始めてみる。
Windows 上で Visual Studio 2017 以降を使用する。
言語は C++17 以降。
DeZero | DeZeroCpp |
---|---|
Python 3 | C++ (MSVC) |
NumPy | NumCpp (v2.00) + boost (v1.73) |
matplotlib | matplotlib-cpp ※対応保留中 |
CuPy | 未定。多分 GUI 対応は不可能。 |
Pillow | 未定 |
Graphviz | Graphviz |
- NumCpp には boost も必要。ただし、libファイルは不要なのでダウンロードしてらビルドせずに配置するだけで良い。
- プロジェクトのプロパティで、「追加のインクルードディレクトリ」に 2 つのライブラリのパスを追加しておく。
- Graphviz はテキストインタフェースのツールなのでそのまま使える。
- NumCpp や boost のように、ヘッダオンリーのライブラリを目指す。
- step07 で破綻した。最終的に、h/cpp にクラスの宣言と定義を分けることにする。
- step27 で再挑戦して成功。
- 復習が目的なので、気づいた点はなるべく細かくメモする。
- 最低限ステップごとにコミットする。ビルドは通る状態とする。
- Variable クラス
__init__
は C++ ではコンストラクタになる。初期化代入子を使ってメンバを初期化。- 今後、継承することになるため、仮想デストラクタを用意しておく。
- NumCpp
- NumPy は n 次元のテンソルを扱えるが、NumCpp は 2 次元の行列固定。3 次元以上のテンソルが CNN のあたりで出てくるが、まだまだ先のことだし、NumPy ライクに使える他のライブラリが見つからないので、このまま進める。
- スカラーやベクトルも 2 次元で保持されてしまうようなので、適宜変換が必要かもしれない。
- NumCpp の warning C4819 (文字コード関係) が 3 つ出るが、とりあえず保留にして進む。
- その他
- 命名規則は C++ の標準的なスタイルに従う。メンバ変数のスタイルは色々あるようだけど、特に接頭辞や接尾辞は付けないスタイルとする。
- 名前空間は
dz
としてまとめる。 - よく使う型は using でエイリアスを作っておく。
- Function クラス
- オーバーライド必須の関数は、Python では
NoImplementedError
例外を投げているが、C++ では純粋仮想関数とした。
- オーバーライド必須の関数は、Python では
- step02
- Python の
type(y)
は、C++ ではtypeid(y).name()
に置き換えた。
- Python の
- その他
- 書籍のステップに合わせて、stepXX という関数を作りながら進めることにする。その関数を main から呼び出すことにする。
- Python だと実行するスクリプトを step01.py, step02.py, ... のように変えながら進めるのが簡単だが、Visual Studio 上の C++ で開発していると、main 関数をひとつに固定しておかないと面倒なので。
- あと、同じ名前のクラスがプロジェクト内に複数存在するとリンクエラーになってしまうので、とりあえず step.hpp というヘッダに押し込めて共有することにした。
- この先、別のヘッダにクラスを移すときには削除しなければならないかも。コンパイルエラーになるようなケースでは、古いコードは消していくしかない。
- 書籍のステップに合わせて、stepXX という関数を作りながら進めることにする。その関数を main から呼び出すことにする。
- NdArrayPrinter クラス
- 書籍には無いが、NumCpp の NdArray クラスを標準出力するとき、スカラーであっても配列形式で表示されてしまうので、スカラーか否かで出力形式を切り替えるためのヘルパークラスを作成。
- その他
- 書籍と実行結果をなるべく合わせるため、NdArray 内部で使用するデータ型は当面 double 型とする。
- ディープラーニングにはそこまでの精度は不要なので、最終的には float にする。
- 同様の理由で、NdArrayPrinter クラスを作成した。
- dz 名前空間を using しておくことにした。これに加えて
auto
を使用することで、main 関数内は書籍の Python コードにかなり近づいた。
- 書籍と実行結果をなるべく合わせるため、NdArray 内部で使用するデータ型は当面 double 型とする。
- numrical_diff 関数
- この先 Variant クラスや Function クラスは内部でデータを保持しながら使うことになり const 参照渡しはマッチしないので、通常の参照渡しに変えることになりそう。
- 取り急ぎ、Function 型の引数についてのみ const を外したが、今後も必要に応じて外していくことにする。
- …と思ったら、合成関数の微分のところでに、通常の関数
f
をこの関数に渡すコードが出てきてしまい、そもそも Function 型にしてはダメだと気付く。std::function
を使用して、Callable なオブジェクトを受け取れるように変更した。C++ は型に厳密なので、引数で渡す CallableオブジェクトのシグネチャをVarialbe(Variable)
に限定せざるを得なかったが、この先、Python のダックタイピングをどこまで受け入れられるか不安。
- step04
f()
というシンプルな関数が出てきた。この先も、各ステップの中でこのような関数が出てくると名前が衝突するので、苦肉の策で、各ステップの処理はそれぞれ別の名前空間にすることにした。過去のステップも修正。
- このステップでの実装は無し。
- その他
- フォルダ構成等見直し。stepXX.cpp を作成し、なるべく書籍のサンプルに合わせた構成とした。
- ソースファイルごとに NumCpp.hpp をビルドしているとこれから先どんどんビルド時間が伸びてしまうので、プリコンパイル済みヘッダを使用することにした。最終的に再度外すかも。
- DeZeroCpp のメインソース部分は、必要なヘッダファイルを適宜インクルードするようにする。dezero.hpp に DeZeroCpp で用意したヘッダフィあるのインクルード分を並べ、DeZeroCpp を使用する側(step など)からは、dezero.hpp をインクルードさせる。
- Variant, Function クラス
- 初期状態が None のメンバが出てきた。C++ だと、生ポインタなら nullptr 、スマートポインタなら Empty の状態にすべきか。
- 生ポインタではなくスマートポインタを使うのは大前提だが、最初 unique_ptr にしようとしてエラーになった。コピー代入を行っているので当たり前だった。shared_ptr を使うこととする。
- スマートポインタを使うと、その変数に初めて値を代入するときにいちいち
std::make_shared
を使わせることになってしまうのが気になる。それが DeZero ライブラリ内であればよいが、step06 のような使用する側のコードであれば意識させたくない。ただ、おそらくこの辺りも最終的にはライブラリに内包される部分と仮定し、サンプルコードになるべく準拠する状態で先へ進む。
- Function クラス
- 抽象クラスにしていたが、
Variable::set_creator
でインスタンスを Function 型の shared_ptr に保持しようとすると、抽象クラスのインスタンス化に失敗というエラーが出てしまいうまくいかなかった。ポリモーフィズムのために親クラス型のポインタ/参照でインスタンスを保持するケースで、スマートポインタをどう使うのが正解か不明。- とりあえず、抽象クラスをやめることにした。
- 抽象クラスにしていたが、
- Variant, Function クラス
- Python のようなメモリ構造(参照カウンタでメモリ管理されており、誰も参照しなくなったオブジェクトは自動的に削除される仕組み)をそのまま実現するために、shared_ptr を利用していたが、中途半端にスマートポインタを利用していてはダメで、ライブラリを利用する側が Variable や Function を作ったタイミングでスマートポインタになっていないといけない。これは利用側の負担が大きいため、しばらく生ポインタでアドレスを保持することにして先に進む。
- 既に存在するオブジェクトを指すポインタは生ポインタとするが、新たに
new
はしたくないので、そこだけは unique_ptr を使う。 - 言い方を変えると、Function と Variable がお互いを指す場合のポインタは生ポインタを使うことになる。それぞれのオブジェクトのスコープが不一致だと破綻するが、暫定処置とする。
- 今後、もし shared_ptr を使う場合、
Variable::set_creator
関数に this を渡す際、this は生ポインタなのでそのまま渡せない。その場合は、enable_shared_from_this
を利用する必要がある。 Variable::backward
の内部でFunction
クラスのメンバを参照するので、宣言と定義を分離する必要が出てきた。もともとヘッダオンリーのライブラリに使用としていたが、ここで破綻してきた。最終的に h/cpp を分けることとする。
- step07
- サンプルコードでは assert でインスタンスの同値チェックをしているが、C++ だとアドレスの一致チェックとした。スマートポインタを使う場合は再考が必要かもしれないが、とりあえずは生ポインタとしたので問題なく使える。
- 結局全部生ポインタにしてしまった。スマートポインタを使うことで、make関数や
*
演算子を使用しなければならなくなって、数式チックにコードを書けるメリットが損なわれていってしまったので。
- 生ポインタを使うと何が問題か
- Function や Variantのインスタンス内部で生ポインタでお互いを参照する状態になる。インスタンスのコピーが行われると、メモリ管理が不可能で誰が delete すべきか決められない。
- ×:生ポインタを使用してはダメ。
- Function や Variantのインスタンス内部で生ポインタでお互いを参照する状態になる。インスタンスのコピーが行われると、メモリ管理が不可能で誰が delete すべきか決められない。
- スマートポインタを使うと何が問題か
- ライブラリ使用する側のコードも含め、全ての Function や Variant のインスタンスをスマートポインタで管理する必要がある。FunctionやVariantのインスタンスを作るとき、いちいちヘルパー関数 std::make_xxx を使わないといけなくなって使用感が損なわれる。
- △:ステップが進むと、Function のラッパー関数を作ることになるなど、このあたりはライブラリ内に隠蔽されるから大丈夫かもしれない。Variable を作るときはヘルパー関数を使うしかないが。
- Function や Variant のインスタンスを利用して計算式を連結するのに自然なコードでなくなる。いちいち * や & 演算子で参照しないといけないなど。これが一番の課題。
- ○:計算式は NdArray に対してしか作らないのでは?もしそうなら、NdArray は常にコピー渡しとし、Function と Variable はスマートポインタを使用すれば対応可能かもしれない。Function 呼ぶときだけ * や & が必要になってしまう問題もあるが、前述の通り Function のラッパー関数によって隠蔽できる。
- Function は this を Variant に渡すことになるが、通常、スマートポインタ内部で this は使えない。
- ○:
std::enable_shared_from_this
が使える。
- ○:
- ライブラリ使用する側のコードも含め、全ての Function や Variant のインスタンスをスマートポインタで管理する必要がある。FunctionやVariantのインスタンスを作るとき、いちいちヘルパー関数 std::make_xxx を使わないといけなくなって使用感が損なわれる。
- ラッパークラスを使い、スマートポインタを内部に隠蔽する方法もあるか
- Function の派生クラスを自然に作れなくなるなど、ポリモーフィズムがうまくいかなくなる。
- ×:インナークラスを継承させれば解決できそうだが、Functionを継承して新しいクラスが簡単に作れるようにしたいので複雑にしたくない。
- Function の派生クラスを自然に作れなくなるなど、ポリモーフィズムがうまくいかなくなる。
- NdArray は基本的にコピー渡し。
Variable::grad
のように空の状態が必要な場合はstd::shared_ptr
で管理。 - Function と Variable は
std::shared_ptr
で管理。- 通常のコンストラクタは
protected
で外から使えなくして、それぞれインスタンス生成用の静的メンバ関数を用意しておき、その中でstd::shared_ptr
に入れて返すようにすることも、CRTP を使えばできそうだが、そこまではやらない。
- 通常のコンストラクタは
- Function は
std::enable_shared_from_this
を継承。 - Function の 派生クラスを
std::shared_ptr<Function>
に入れる場合、std::make_shared<Function>
ヘルパー関数は使えないので、new でインスタンス生成する。- これを守れば、Function クラスがインスタンス化されることはなくなるので、以前のように抽象クラスに戻す。
- square, exp 関数
- ラッパー関数ができたことで、
std::shared_ptr
を使うことによるコードの煩雑さを隠蔽できた。
- ラッパー関数ができたことで、
- as_array, as_variant 関数
- スカラーを NdArray に変換するためのヘルパー関数であるが、
make::shared_ptr
を内部に隠蔽するためにも使うことにしたので、オーバーロードで様々なバリエーションを用意した。 - as_variant は書籍にはないが、同様の理由で作成した。
- スカラーを NdArray に変換するためのヘルパー関数であるが、
- その他
- 書籍の後半に出てくる「ndarray だけを扱う」に関しては、C++はそもそも型に厳密なので不要であった。
- NdArray は基本的にコピー渡しのつもりだったが、結局 Empty (nullptr) の状態を作れなければならず、スマートポインタに統一することになった。
- このステップは省略する。
- Variable::backward 関数
- 書籍のサンプルコードは、この時点ではリスト化に対応していない(ステップ 13 で対応。)Python だと呼び出ささなければ問題無いが、C++ ではコンパイルが通らないので一時的にコメントアウトした。
- Function::operator() 関数
- 1要素の場合とリストの場合で関数をオーバーロードして対応。
- その他
- リストは、ランダムアクセスが必要なので
std::vector
を使用する。一部、ソートが必要になる場合はstd::list
で対応する。
- リストは、ランダムアクセスが必要なので
- Function::operator() 関数
- 前のステップで、
std::list
を引数に取る関数を準備したが、これだけで{}
によるstd::initializer_list
の引数渡しに対応できているため、そのままで良い。 - 戻り値を、1要素とリストの両方に対応することは C++ ではできないので、リストを返すままとする。
- 前のステップで、
- Function::forward 関数
- リストのアンパッキングに相当する機能は C++ には無いため変更しない。
- 結局、行ったのは add 関数の実装だけ。
- add, square 関数
- Add, Square クラスの operator() 関数はリストを受け取りリストを返せるように実装した。実際に使う引数や戻り値がリストではなく1要素である場合、これらラッパー関数のシグネチャで対応する。
- Variable::backward 関数
- 付録Aに書かれているように、
x->grad
のインスタンスを新しく生成する必要がある。スマートポインタを使っているため、書籍に書かれているように+=
ではなく+
を使うだけでは解決にならないため、as_array
を使ってインスタンス生成するようにした。
- 付録Aに書かれているように、
- Variable::cleargrad, as_array 関数
- スマートポインタで管理している変数 grad を空にするために、引数なしの
as_array
関数を用意していたが、これは不要で、単にnullptr
を代入すればよかった。こちらの方が書籍のサンプルコードにも近いし簡潔に書けるので、修正した。
- スマートポインタで管理している変数 grad を空にするために、引数なしの
- このステップでの実装は無し。
- Function::operator() 関数
- コンテナから最大値を探すために、
std::max_element
を使用。
- コンテナから最大値を探すために、
- Variable::backward 関数
- 内部関数(クロージャ)は、ラムダ式で作成。
- Variable::set_creator 関数
- 内部で Function クラスのメンバを参照するようになったので、宣言と定義を分離した。
- step17
- 弱参照を使用する前は循環参照によってメモリが増え続ける状態となり、弱参照を使用した後はループしてもメモリが増えることが無くなった。
- プロセスが使用するワーキングセットプライベートの容量で確認。
- 弱参照を使用する前は循環参照によってメモリが増え続ける状態となり、弱参照を使用した後はループしてもメモリが増えることが無くなった。
- その他
- Python の
weakref
の代わりにstd::weak_ptr
を使用する。
- Python の
- Config クラス
- サンプルコードに倣い文字列でパラメータ名を指定させるために、パラメータの実体を
std::map<std::string, bool>
で保持。- bool 以外のパラメータが出てきたらテンプレート化するなど検討する。
- パラメータの初期化タイミングを保証したかったので、静的クラスではなくシングルトンとした。
- シングルトンならではのコードはライブラリ内に隠蔽されることを期待。このステップでは問題なし。
- サンプルコードに倣い文字列でパラメータ名を指定させるために、パラメータの実体を
- UsingConfig, no_grad クラス
- いずれも、サンプルコードでは with 句で使用できる関数だったが、C++ ではクラスで作成した。
- C++ には with 句は無いため、専用のスコープを作って先頭でこれらのクラスをインスタンス化する。そうすることで、前処理はコンストラクタ、後処理はデストラクタに行わせることができ、擬似的に with 句と同じ効果を得ることができる。
- no_grad の方はあえて class ではなく struct とし、サンプルコードと同様のスネークケースで命名した。
- 単なるこだわりでしかない。
- Variable クラス
- shape と size は実装。内部の NdArray に委譲すればよく最小限のコードとする。戻り値も型推論で書く。
- ndim は NumCpp だと 2 固定なので実装不要。
- dtype は NumCpp だと
NdArray<T>::value_type
で取得可能なので実装不要。 - len に相当するメンバが NumCpp に無いので未実装。
- print に相当する << 演算子に対応。
- VariablePtr クラス
- add, mul 関数は
VariablePtr
を引数に取るように作っているため、VariablePtr
の演算子オーバーロードを行うことにした。- つまり、
std::shared_ptr<Variable>
の演算子オーバーロードをすることになるが、スマートポインタの算術演算子はオーバーロードされていないため問題無いと考える。
- つまり、
- 演算子オーバーロードは、定石に従って += 演算子を定義して + 演算子で利用しようとしたが、スマートポインタ、つまり自作していないクラスの演算子オーバーロードをするため、メンバ関数として定義する必要がある += 演算子は使えない。よって、単独で + 演算子をオーバーロードすることとした。* も同じ。
- そもそも、add, mul 関数を利用して定義する以上、+= や *= はサンプルコードでも使わない前提と思われる。
- add, mul 関数は
-
as_variant 関数
- 以前のステップで既に必要が生じて作成済みなので、ここでは何もする必要が無い。
-
VariablePtr クラス
- 整数, 浮動小数, NdArrayPtr との演算は、C++ だと演算子オーバーロードを複数種類定義することで対応することになる。
- 内部データ型
data_t
つまり浮動小数用の演算子オーバーロードを行うことで、暗黙変換によって整数値も使えるようになる。 - NdArrayPtr 用は個別で定義。
- 内部データ型
- 整数, 浮動小数, NdArrayPtr との演算は、C++ だと演算子オーバーロードを複数種類定義することで対応することになる。
- VariablePtr クラス
- 負数(-)演算子は、本来はメンバ関数としてオーバーロードすべきだが、スマートポインタを使う関係上不可能なので、グローバル関数のスタイルで定義。
- 書籍には記載が無いが、合わせて正数(+)演算子も定義。値に何も変化をもたらさないので、順伝播は入力値をそのまま返し、逆伝播は 1 を返すものとする。実際に処理する関数名は Pos (Positive) とする。
- 負数(-)演算子は、本来はメンバ関数としてオーバーロードすべきだが、スマートポインタを使う関係上不可能なので、グローバル関数のスタイルで定義。
- その他
- Function クラスの派生クラスである、Add, Sub, Mul, Div などの順伝播/逆伝播の計算は、スマートポインタから中身の
NdArray
まで取り出してしまった方が計算式が簡潔になることに気付いたので修正した。 - C++ には ** 演算子は無いため、累乗を行う場合は pow 関数を直接使うこととする。
- Function クラスの派生クラスである、Add, Sub, Mul, Div などの順伝播/逆伝播の計算は、スマートポインタから中身の
- 現時点のライブラリ部分のソースは、core_simple.hpp, core_simple.cpp としてまとめることにした。
- できればヘッダーオンリーにしたいが、テンプレートではない関数定義がある以上無理みたいなので、ソースファイルも作成。
- 各ステップのソースに個別の名前空間を切っていたおかげで、同名のクラスや関数を dz 名前空間に定義しても競合することはなかった。
- ライブラリ使用側からは、dezero.hpp をインクルードするだけ(加えて、
using namespace dz
してもよい)で使用できるようになった。(当初の予定通り)- dezero.hpp で、core.hpp と core_simple.hpp の切り替えを定義した。
- 計算結果は書籍と一致したので問題無いはず。
- やはり累乗が pow なのが惜しい。^ を使うことも考えたが、演算子の優先順が異なるので問題あり。
- このステップでの実装は無し。
- dot_var, dot_func 関数
- 文字列の書式化に
std::format
を使いたかったが、C++20 以降しか使えないので断念。
- 文字列の書式化に
- plot_dot_graph 関数
- dot ファイルの内容も確認したいので、画像ファイルと同じ場所に出力することとした。
std::filesystem::path
を使いたかったので、コンパイル設定を C++17 に設定。
- その他
- 出力した計算グラフは、書籍とはノードの位置が若干異なるが(Graphviz のバージョンの差異と思われる)、内容は一致。
- power 関数
- pow とすると、cmath の pow と名前が重なるため変更する。NumCpp にも power が定義されているが、シグネチャが異なるため pow よりは紛らわしくないと判断した。
- my_sin 関数
- 階乗計算はオーバーフローしてしまうので、前の項の値を利用して計算するようにした。
- その際、VariablePtr 変数とそれ以外の定数の計算を別に行っておき後でまとめるように注意。
- 不要な計算グラフを作らないため。
- Python3 では数値の上限が無いため問題無い模様。
- その際、VariablePtr 変数とそれ以外の定数の計算を別に行っておき後でまとめるように注意。
- ↑を行うと勾配の計算が合わなかったため再検討。計算グラフがサンプルと異なるため?
- ある程度までオーバーフローしないように double 型を使い、結局は書籍のサンプルと同じコードとした。
- 階乗計算はオーバーフローしてしまうので、前の項の値を利用して計算するようにした。
- 特になし。
inline
とstatic
を適切に指定することで、ヘッダーオンリーにできた。std::tuple
やstd::apply
や、C++17 で導入された機能を使うことでタプルと変数の行き来を自然に行うことができ、よりサンプルコードに近づくことができるかもしれないが、実装までは行っていない。
- 特になし。
- このステップでの実装は無し。
- このステップでの実装は無し。
- その他
- core.hpp と core_simple.hpp の切り替えは C++ では難しそう。
Variable::grad
の型が変わることで、各種関数の引数や戻り値の型が変わるため実装し直しになり、それらの関数シグネチャが変わるため過去のステップのコードがコンパイルエラーになるという流れ。型厳密である以上、これらのヘッダを同一視することは難しい。- コンパイルエラーになるステップのコード全てに対して、2種類のコードを用意した。
- core.hpp と core_simple.hpp の切り替えは C++ では難しそう。
- Variable::backward 関数
- 設定済みの勾配に新しい勾配を加算するところを、NdArray 同士の計算にしてしまっていたため高階微分が計算されていなかった。VariablePtr 同士の計算にして計算グラフを構築することが重要であった。
- その他
- ライブラリのヘッダファイルのインクルード順が重要になるので、全て dezero.hpp に集約して、どのファイルも dezero.hpp をインクルードするようにした。
- matplotlib でのグラフ描画は、matplotlib-cpp を使わせてもらうかと思ったが、うまく動作させることができず一旦保留。
- その他
- 8階微分で graphviz が途中で終了してしまい、png ファイルを作成できなかった。dot ファイル出力は問題無さそうに見える。最新バージョンを入れて試してみると変わるかもしれない。
- 特になし。
- NdArrayPrinter の << 演算子
- NdArray がテンソルの場合(1x1 のスカラでは無い場合)にバグがあったので修正。これまではスカラしか動かしていなかったため気付いていなかった。
- ステップ
- sum 関数が未実装のため、保留にして先に進む。
- Variable::reshape 関数
- この関数の実装のために、Variable クラスの this を使用する必要が出てきたため、Function クラスと同様、
std::enable_shared_from_this
を継承する必要が出てきた。 - サンプルコードとは違い、C++ 引数がスカラ、タプル、リストのようなケースに対応する必要は無く
nc::Shape
のみで問題無い。
- この関数の実装のために、Variable クラスの this を使用する必要が出てきたため、Function クラスと同様、
- Variable::transpose 関数
- C++ ではプロパティ T への対応は行えない。
- その他
NdArray::shape
関数を変数と誤解してしまうことが多いので注意する。
- Sum クラス
- axis は nc::Axis をそのまま利用。
- NdArray は行列(2階テンソル)の形状で固定的に使用するため、keepdims は実装不可。
- broadcast_to 関数はステップ 40 で実装。
- broadcast_to, sum_to 関数(NdArray 用)
- NdArray にはこれらの関数が用意されていないので、utils.hpp 内に独自実装する。NdArray が行列固定であることを前提にした簡易的な実装とした。
- broadcast_mutual 関数
- NdArray は四則演算の際などに自動的にブロードキャストが行われないので、専用の関数を用意して対応する。
- Add, Sub, Mul, Div クラス
- forward 関数で行う四則演算の直前で、broadcast_mutual 関数を実行するようにした。
- backward 関数はサンプルコードの通り。
- 特になし。
- 特になし。
- その他
- 動作的には問題が無いが、10000 回繰り返し実行している間メモリが増え続けてしまうことが気になる。何か問題があるかもしれない。
- やはりおかしかった。 "enable_backprop" という設定値のON/OFF切り替えで、逆伝播時に、更なる逆伝播(高階微分)のための計算グラフ保持をしないように設定しているはずが、設定値のスペルミスにより計算グラフを保持してしまっていた。修正後はメモリが増え続けることは無くなった。
- 各関数クラスの backward 関数で、弱参照ポインタで保持している outputs を使用することがあり、それを参照して計算グラフに組み込んでいたため弱参照の意味が無くなってしまい、メモリが増え続けてしまう原因かと思ったが、こちらは問題無かった。計算グラフに組み込むべきときには組み込んで良いし、組み込むべきでないときに組み込まなければ余計な参照は発生しない。結局は "enable_backprop" の設定値が正しく機能していれば問題無いということであった。
- 動作的には問題が無いが、10000 回繰り返し実行している間メモリが増え続けてしまうことが気になる。何か問題があるかもしれない。
- functions.hpp
- VariablePtrList を受け取って VariablePtrList を返すオーバーロード関数を各関数に対して作成。
- Layer を利用するときは List で渡す方が簡潔に書ける。
- VariablePtrList を受け取って VariablePtrList を返すオーバーロード関数を各関数に対して作成。
- Layer クラス
- Python のような
__dict__
の仕組みは使えないので、内部で unordered_map を持って自前でプロパティ管理することにした。 - 外部とのインタフェースは、[] 演算子で行うようにしたが、this から利用するときにコードが煩雑になるため、同等の prop 関数を用意して対応。
- [] 演算子はプロパティの実体である VariablePtr を直接返すのではなく、PropProxy クラス(プロキシ)を中継するようにした。こうすることで、set 時に params への登録も行えるようにした。
- Python のような
- その他
- Liner クラスが2種類出てきたkとで、名前空間を見直した。
- std::map ではなく std::unordered_map を利用するように修正した。
std::make_shared<T>
に渡す引数リストに対応するコンストラクタが T に存在しない場合、memory や xmemory といったヘッダファイルの中でコンパイルエラー(C2664)が出てしまう。原因を特定しにくいので注意する。
- layers.hpp
- パラメータとしてレイヤも保持できるようにする設計に、
std::variant
を使用。 - 実際に保持されているのが
Parameter
かを判断する際、Parameter
型として typeid 演算子を使用。 - 実際に保持されているのが
Layer
またはその派生クラス化を判断するため、dynamic_cast を利用。typeid では判断不可能なため。スマートでは無いが妥協。
- パラメータとしてレイヤも保持できるようにする設計に、
- model.hpp
- 関数を引数として受け取りメンバで保持するケースは、引数で受け取るときは関数ポインタ型、メンバ変数での保持は
std::function
を使用することでうまく実現できた。- 受け取る関数がオーバーロードされている関数のため、引数の方も
std::function
にしてしまうと推論失敗してしまう。
- 受け取る関数がオーバーロードされている関数のため、引数の方も
- 関数を引数として受け取りメンバで保持するケースは、引数で受け取るときは関数ポインタ型、メンバ変数での保持は
- その他
- FunctionPtr 生成時に new を使っていた箇所を
std::make_shared
に修正。
- FunctionPtr 生成時に new を使っていた箇所を
- Optimizer::setup 関数
- 引数でスマートポインタを受け取るようにしたかったが、使用側としてはスマートポインタを意識したく無いはずなので、オーバーロード関数でポインタ渡しもできるようにした。
- get_item 関数
- GetItem と GetItemGrad は NumCpp では使わない(スライス操作に対応していない)ため用意しない。