ゼロから作るDeep Learning ❸ ―フレームワーク編
- 作者:斎藤 康毅
- 発売日: 2020/04/20
- メディア: 単行本(ソフトカバー)
ゼロから作るDeep Learning ③を買った。DezeroのPython実装をRubyに移植する形で独自に勉強している。次はステップ17とステップ18。
- ステップ17:メモリの使用量を削減。
循環参照をしている場合にGCで発見して削除するのには時間がかかる場合がある。従ってoutput
の接続関係についてはWearkref
を導入して参照カウントを増やさない工夫をし、循環参照になることを防いでGCが即座に適用されるように工夫する。
RubyでもWeakref
ってあるのかしらと思って調べてみたらあった。
require 'weakref' ... class Function def call(*inputs) xs = inputs.map{|x| x.data} ... @outputs = outputs.map{|output| WeakRef.new(output)} return outputs.size > 1 ? outputs : outputs[0] end
これでメモリ使用量を減らせるかなと思ったらあまり変わらない。現状ではあまり大差がないのだろうか?
WeakRef
を有効にした場合
Process: 15054: RSS = 11.888 MB, VSZ = 26.144000000000002 MB Process: 15054: RSS = 12.232000000000001 MB, VSZ = 26.468 MB Process: 15054: RSS = 12.552 MB, VSZ = 26.788 MB Process: 15054: RSS = 12.864 MB, VSZ = 27.112000000000002 MB Process: 15054: RSS = 13.184000000000001 MB, VSZ = 27.432000000000002 MB Process: 15054: RSS = 13.496 MB, VSZ = 27.756 MB Process: 15054: RSS = 13.816 MB, VSZ = 28.076 MB Process: 15054: RSS = 14.128 MB, VSZ = 28.400000000000002 MB Process: 15054: RSS = 14.448 MB, VSZ = 28.72 MB Process: 15054: RSS = 14.76 MB, VSZ = 29.044 MB
WeakRef
を無効にした場合
Process: 15085: RSS = 11.896 MB, VSZ = 26.144000000000002 MB Process: 15085: RSS = 12.24 MB, VSZ = 26.464000000000002 MB Process: 15085: RSS = 12.552 MB, VSZ = 26.788 MB Process: 15085: RSS = 12.872 MB, VSZ = 27.108 MB Process: 15085: RSS = 13.184000000000001 MB, VSZ = 27.432000000000002 MB Process: 15085: RSS = 13.504 MB, VSZ = 27.752 MB Process: 15085: RSS = 13.816 MB, VSZ = 28.076 MB Process: 15085: RSS = 14.136000000000001 MB, VSZ = 28.396 MB Process: 15085: RSS = 14.448 MB, VSZ = 28.72 MB Process: 15085: RSS = 14.768 MB, VSZ = 29.04 MB
- ステップ18:メモリの使用量を削減するためのモード。
まずは不要な微分を保持しないためにretain_grad
を追加してこの変数がfalse
の時はgradの値を保持しないようにする。
class Variable ... def backward(retain_grad=false) if @grad == nil then @grad = fill_one(@data.clone) ... while not funcs.empty? do f = funcs.pop ... } if not retain_grad then f.outputs.map{|y| y.grad = nil } end end
こうすると中間のx.grad
が保持されなくなる。nilのままだ。
2.0 1.0
続いて逆伝搬の無効モードを付け加える。逆伝搬の無効モードでは、backward()
を実行されない。順方向だけのニューラルネットワークテスト用に使用する。Pythonではconfig::enable_backprop
というクラス内メンバを使用していたが、Rubyで同じことをする方法が分からない。仕方が無いので大域変数$enable_backprop
を定義してそれを使用した。
class Function def call(*inputs) xs = inputs.map{|x| x.data} ys = forward(*xs) if ys.is_a?(Array) then ys = [ys] end outputs = ys.map{|y| Variable.new(y) } if $enable_backprop then @generation = (inputs.map{|x| x.generation}).max outputs.each {|output| output.set_creator(self) } end @inputs = inputs @outputs = outputs.map{|output| WeakRef.new(output)} return outputs.size > 1 ? outputs : outputs[0] end ...
このテスト実行中に間違いを発見した。backward
実行時に、backward
の初期値が存在しない場合は1.0で埋められた行列を作ってそれを入力値としていたのだが、その作り方が間違っている。
def backward() if @grad == nil then @grad = @data.clone.fill(1.0) end funcs = []
これでは@grad
は1次元の配列にしかならない。以下のように変更した。
def fill_one(a) if a.is_a?(Array) a.map{|i| fill_one(i) } else a = 1.0 end end class Variable ... def backward(retain_grad=false) if @grad == nil then @grad = fill_one(@data.clone) end
2つのモードを用いてテストを行う。
begin $enable_backprop = false x = Variable.new(100.times.map{100.times.map{100.times.map{1.0}}}) y = square(square(square(x))) y.backward() end begin $enable_backprop = true x = Variable.new(100.times.map{100.times.map{100.times.map{1.0}}}) y = square(square(square(x))) y.backward() end
最後のwith
ブロックを用いる切替については省略。