FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

ゼロから作るDeep Learning ③ のPython実装をRubyで作り直してみる(ステップ17/ステップ18)

ゼロから作るDeep Learning ❸ ―フレームワーク編

ゼロから作る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ブロックを用いる切替については省略。