いものやま。

雑多な知識の寄せ集め

強化学習とニューラルネットワークを組合せてみた。(その9)

昨日は複数のインスタンスを同時に学習するということを試してみた。

これ自体は有効に思われ、あとは複数のインスタンスの出す結果をどうやって統合するのかが課題になった。

その方法の一つとして考えられる、ドロップアウトの実装を行ってみた。

なお、ドロップアウトについては以下を参照。

また、ドロップアウトに対応していない関数近似のためのニューラルネットワークの実装は、以下を参照。

ドロップアウト対応版ValueNNクラス

ということで、ドロップアウトを行えるようにしたValueNNクラスが、以下。
diffだけ載せてもいいのだけど、分かりづらいと思うので、基本的には全部載せていく。

#====================
# value_nn.rb
#--------------------
# 価値ベクトルを関数近似するためのニューラルネットワーク
#
# 中間層では、以下の活性化関数を使う:
#   f(u) = u     (u >= 0)
#          0.1*u (otherwise)
# なお、この導関数は
#   f'(u) = 1   (u >= 0)
#           0.1 (otherwise)
#
# 出力層では、以下の活性化関数を使う:
#   f(u) = u   (output_min <= u <= output_max)
#          0.1u + 0.9output_max (output_max < u)
#          0.1u + 0.9output_min (u < output_min)
# なお、この導関数は
#   f'(u) = 1   (output_min <= u <= output_max)
#           0.1 (otherwise)
#
# また、ドロップアウトも使える。
#====================

require_relative "normal_dist_random"

class ValueNN

# 続く

イニシャライザとアクセサメソッド

まずはイニシャライザとアクセサメソッドから。

# 続き

  def initialize(input_size, hidden_unit_size, output_min, output_max, drop_unit_size=0)
    @input_size = input_size
    @hidden_unit_size = hidden_unit_size
    @output_min = output_min
    @output_max = output_max
    @drop_unit_size = drop_unit_size

    hidden_unit_weight_variance = 1.0 / (@input_size + 1.0)
    hidden_unit_weight_generator = NormalDistRandom.new(0.0, hidden_unit_weight_variance)
    @hidden_units_weights = Array.new
    @hidden_units_bias = Array.new
    @hidden_unit_size.times do
      weights = Array.new(@input_size) do
        hidden_unit_weight_generator.get_random
      end
      bias = hidden_unit_weight_generator.get_random
      @hidden_units_weights.push(weights)
      @hidden_units_bias.push(bias)
    end

    output_unit_weight_variance = 1.0 / (@hidden_unit_size + 1.0)
    output_unit_weight_generator = NormalDistRandom.new(0.0, output_unit_weight_variance)
    @output_unit_weights = Array.new(@hidden_unit_size) do
      output_unit_weight_generator.get_random
    end
    @output_unit_bias = output_unit_weight_generator.get_random

    @drop_enabled = false
    @amp_rate = 1.0
  end

  attr_reader :drop_enabled

  def drop_enabled=(enabled)
    if enabled
      @drop_enabled = true
      @amp_rate = @hidden_unit_size.to_f / (@hidden_unit_size - @drop_unit_size)
    else
      @drop_enabled = false
      @amp_rate = 1.0
    end
  end

# 続く

引数で、ドロップさせるユニットの数を指定できるようにしている。

なお、本当はドロップさせるユニットの数は動的に指定できるようにしたかったのだけど、感度確認を行うときに都合が悪かったので、静的に指定するようにしている。
そして、ドロップアウトを有効にするか無効にするかを切り替えられるようにするために、@drop_enabledというインスタンス変数と、それに対するアクセサメソッドを用意している。

あと、@amp_rateというインスタンス変数は、ドロップアウトを有効にしていても無効にしていても、中間層の出力の合計が同程度になるように調整するためのもの。
もともと本で説明されていたのは、 pの割合だけユニットを使う場合、ドロップアウトを無効にしたときにはドロップアウトしていた層の出力をすべて p倍するということだったけど、ここではドロップアウトをしているときには出力を \frac{1}{p}倍にするようにしている。(本質的には変わらないけど、ユニットの数を減らしてるときに出力を増やすという方が自然な発想)

出力と勾配の計算

次は、出力と勾配の計算。

# 続き

  def get_value_and_weights_gradient(input)
    # select drop units

    drop_units_index =
      if @drop_enabled
        (0..(@hidden_unit_size-1)).to_a.sample(@drop_unit_size).sort
      else
        []
      end

    # calculate output by forward propagation
    
    hidden_units_output = Array.new
    hidden_units_activation_gradient = Array.new
    @hidden_unit_size.times do |hidden_unit_index|
      if drop_units_index[0] != hidden_unit_index
        input_sum = @hidden_units_bias[hidden_unit_index]
        @input_size.times do |input_index|
          input_sum += @hidden_units_weights[hidden_unit_index][input_index] * input[input_index]
        end
        output, activation_gradient = hidden_unit_activation_and_gradient(input_sum)
        hidden_units_output.push output * @amp_rate
        hidden_units_activation_gradient.push activation_gradient
      else
        drop_units_index.shift
        hidden_units_output.push 0.0
        hidden_units_activation_gradient.push 0.0
      end
    end

    hidden_units_output_sum = @output_unit_bias
    @hidden_unit_size.times do |hidden_unit_index|
      hidden_units_output_sum += @output_unit_weights[hidden_unit_index] * hidden_units_output[hidden_unit_index]
    end
    output_unit_output, output_unit_activation_gradient = output_unit_activation_and_gradient(hidden_units_output_sum)

    # calculate delta by back propagation

    output_unit_delta = output_unit_activation_gradient

    hidden_units_delta = Array.new
    @hidden_unit_size.times do |hidden_unit_index|
      delta = output_unit_delta * @output_unit_weights[hidden_unit_index] * hidden_units_activation_gradient[hidden_unit_index]
      hidden_units_delta.push delta
    end

    # calculate weights gradient

    weights_gradient = Array.new
    @hidden_unit_size.times do |hidden_unit_index|
      hidden_unit_delta = hidden_units_delta[hidden_unit_index]
      weights_gradient.push hidden_unit_delta # hidden unit bias gradient
      @input_size.times do |input_index|
        hidden_unit_weight_gradient = hidden_unit_delta * input[input_index]
        weights_gradient.push hidden_unit_weight_gradient
      end
    end
    weights_gradient.push output_unit_delta # output unit bias gradient
    @hidden_unit_size.times do |hidden_unit_index|
      output_unit_weight_gradient = output_unit_delta * hidden_units_output[hidden_unit_index]
      weights_gradient.push output_unit_weight_gradient
    end

    [output_unit_output, weights_gradient]
  end

# 続く

ドロップアウトが有効になっている場合、最初に使わないユニットをランダムに選ぶようにしている。
そして、今計算を行おうとしているユニットが使わないユニットの場合、その出力と活性化関数の勾配を0として計算するようにしている。

感度確認

続いて、感度確認。

# 続き

  def get_value_with_weights_gradient(input, weights_gradient, alpha=1.0)
    weights_gradient_copy = weights_gradient.dup

    # calculate output by forward propagation, with weights_gradient
    
    hidden_units_output = Array.new
    @hidden_unit_size.times do |hidden_unit_index|
      bias_gradient = weights_gradient_copy.shift
      if bias_gradient != 0.0
        input_sum = @hidden_units_bias[hidden_unit_index] + alpha * bias_gradient
        @input_size.times do |input_index|
          input_sum += (@hidden_units_weights[hidden_unit_index][input_index] + alpha * weights_gradient_copy.shift) * input[input_index]
        end
        output, _ = hidden_unit_activation_and_gradient(input_sum)
        hidden_units_output.push output * @amp_rate
      else
        weights_gradient_copy.shift(@input_size)
        hidden_units_output.push 0.0
      end
    end

    hidden_units_output_sum = @output_unit_bias + alpha * weights_gradient_copy.shift
    @hidden_unit_size.times do |hidden_unit_index|
      hidden_units_output_sum += (@output_unit_weights[hidden_unit_index] + alpha * weights_gradient_copy.shift) * hidden_units_output[hidden_unit_index]
    end
    output_unit_output, _ = output_unit_activation_and_gradient(hidden_units_output_sum)

    output_unit_output
  end

# 続く

このとき、出力と勾配の計算を行ったときに使わなかったユニットについては、同様に使わないようにしないといけない。
その判断に使っているのが、バイアスの勾配の値。
バイアスの勾配の値はデルタの値と等しくなっていて、デルタを求めるときには活性化関数の勾配を掛けるので、使われなかったユニットのデルタは0になっている。
逆に、使われたユニットは、出力層との結合の重みが0でない限り、0にはならない。(ただし、重みが0なら、そもそも使っても使わなくても変わらない)
なので、バイアスの勾配を確認し、その値が0であった場合には、そのユニットを使わないようにしている。

重みの取得と更新

最後に重みの取得と更新だけど、これはドロップアウトなしの場合と何も変わらず。

# 続き

  def get_weights
    weights = Array.new
    @hidden_unit_size.times do |hidden_unit_index|
      weights.push @hidden_units_bias[hidden_unit_index]
      @input_size.times do |input_index|
        weights.push @hidden_units_weights[hidden_unit_index][input_index]
      end
    end
    weights.push @output_unit_bias
    @hidden_unit_size.times do |hidden_unit_index|
      weights.push @output_unit_weights[hidden_unit_index]
    end
    weights
  end

  def add_weights(weights_diff)
    weights_diff_copy = weights_diff.dup
    @hidden_unit_size.times do |hidden_unit_index|
      @hidden_units_bias[hidden_unit_index] += weights_diff_copy.shift
      @input_size.times do |input_index|
        @hidden_units_weights[hidden_unit_index][input_index] += weights_diff_copy.shift
      end
    end
    @output_unit_bias += weights_diff_copy.shift
    @hidden_unit_size.times do |hidden_unit_index|
      @output_unit_weights[hidden_unit_index] += weights_diff_copy.shift
    end
  end

  private

  def hidden_unit_activation_and_gradient(u)
    if u >= 0.0
      [u, 1.0]
    else
      [0.1 * u, 0.1]
    end
  end

  def output_unit_activation_and_gradient(u)
    if u < @output_min
      [0.1 * u + 0.9 * @output_min, 0.1]
    elsif u <= @output_max
      [u, 1.0]
    else
      [0.1 * u + 0.9 * @output_max, 0.1]
    end
  end
end

〜以下、動作確認のコードなので省略〜

差分

一応、diffも載せておく:

diff --git a/RLandNN/TicTacToe/value_nn.rb b/RLandNN/TicTacToe/value_nn.rb
index aa11f69..984e263 100644
--- a/RLandNN/TicTacToe/value_nn.rb
+++ b/RLandNN/TicTacToe/value_nn.rb
@@ -15,16 +15,17 @@
 #   f'(u) = 1   (output_min <= u <= output_max)
 #           0.1 (otherwise)
 #
-# また、ドロップアウトも使える。(未実装)
+# また、ドロップアウトも使える。
 
 require_relative "normal_dist_random"
 
 class ValueNN
-  def initialize(input_size, hidden_unit_size, output_min, output_max)
+  def initialize(input_size, hidden_unit_size, output_min, output_max, drop_unit_size=0)
     @input_size = input_size
     @hidden_unit_size = hidden_unit_size
     @output_min = output_min
     @output_max = output_max
+    @drop_unit_size = drop_unit_size
 
     hidden_unit_weight_variance = 1.0 / (@input_size + 1.0)
     hidden_unit_weight_generator = NormalDistRandom.new(0.0, hidden_unit_weight_variance)
@@ -45,21 +46,51 @@ class ValueNN
       output_unit_weight_generator.get_random
     end
     @output_unit_bias = output_unit_weight_generator.get_random
+
+    @drop_enabled = false
+    @amp_rate = 1.0
   end
 
-  def get_value_and_weights_gradient(input, drop_rate=0.0)
+  attr_reader :drop_enabled
+
+  def drop_enabled=(enabled)
+    if enabled
+      @drop_enabled = true
+      @amp_rate = @hidden_unit_size.to_f / (@hidden_unit_size - @drop_unit_size)
+    else
+      @drop_enabled = false
+      @amp_rate = 1.0
+    end
+  end
+
+  def get_value_and_weights_gradient(input)
+    # select drop units
+
+    drop_units_index =
+      if @drop_enabled
+        (0..(@hidden_unit_size-1)).to_a.sample(@drop_unit_size).sort
+      else
+        []
+      end
+
     # calculate output by forward propagation
     
     hidden_units_output = Array.new
     hidden_units_activation_gradient = Array.new
     @hidden_unit_size.times do |hidden_unit_index|
-      input_sum = @hidden_units_bias[hidden_unit_index]
-      @input_size.times do |input_index|
-        input_sum += @hidden_units_weights[hidden_unit_index][input_index] * input[input_index]
+      if drop_units_index[0] != hidden_unit_index
+        input_sum = @hidden_units_bias[hidden_unit_index]
+        @input_size.times do |input_index|
+          input_sum += @hidden_units_weights[hidden_unit_index][input_index] * input[input_index]
+        end
+        output, activation_gradient = hidden_unit_activation_and_gradient(input_sum)
+        hidden_units_output.push output * @amp_rate
+        hidden_units_activation_gradient.push activation_gradient
+      else
+        drop_units_index.shift
+        hidden_units_output.push 0.0
+        hidden_units_activation_gradient.push 0.0
       end
-      output, activation_gradient = hidden_unit_activation_and_gradient(input_sum)
-      hidden_units_output.push output
-      hidden_units_activation_gradient.push activation_gradient
     end
 
     hidden_units_output_sum = @output_unit_bias
@@ -105,12 +136,18 @@ class ValueNN
     
     hidden_units_output = Array.new
     @hidden_unit_size.times do |hidden_unit_index|
-      input_sum = @hidden_units_bias[hidden_unit_index] + alpha * weights_gradient_copy.shift
-      @input_size.times do |input_index|
-        input_sum += (@hidden_units_weights[hidden_unit_index][input_index] + alpha * weights_gradient_copy.shift) * input[input_index]
+      bias_gradient = weights_gradient_copy.shift
+      if bias_gradient != 0.0
+        input_sum = @hidden_units_bias[hidden_unit_index] + alpha * bias_gradient
+        @input_size.times do |input_index|
+          input_sum += (@hidden_units_weights[hidden_unit_index][input_index] + alpha * weights_gradient_copy.shift) * input[input_index]
+        end
+        output, _ = hidden_unit_activation_and_gradient(input_sum)
+        hidden_units_output.push output * @amp_rate
+      else
+        weights_gradient_copy.shift(@input_size)
+        hidden_units_output.push 0.0
       end
-      output, _ = hidden_unit_activation_and_gradient(input_sum)
-      hidden_units_output.push output
     end
 
     hidden_units_output_sum = @output_unit_bias + alpha * weights_gradient_copy.shift

ドロップアウト対応版NNSarsaComクラス

ValueNNクラスを変更したので、それに合わせてNNSarsaComクラスも修正を行う。
こちらは本質的な変更はないので、差分のあるところだけ。

イニシャライザとアクセサメソッド

class NNSarsaCom
  def initialize(mark, hidden_unit_size=4, drop_unit_size=0, epsilon=0.1, step_size=0.01, td_lambda=0.6)
    @mark = mark
    @hidden_unit_size = hidden_unit_size
    @drop_unit_size = drop_unit_size
    @epsilon = epsilon
    @step_size = step_size
    @td_lambda = td_lambda

    @value_nn = ValueNN.new(9, @hidden_unit_size, -1.0, 1.0, @drop_unit_size)

    @previous_state = nil
    @current_state = nil
    @accumulated_weights_gradient = nil

    @learn_mode = true
    @debug_mode = false
  end

  attr_reader :mark, :hidden_unit_size, :epsilon, :step_size, :td_lambda
  attr_reader :learn_mode
  attr_accessor :debug_mode
  alias_method :learn_mode?, :learn_mode
  alias_method :debug_mode?, :debug_mode

  def learn_mode=(value)
    if value
      @learn_mode = true
      @value_nn.drop_enabled = true
    else
      @learn_mode = false
      @value_nn.drop_enabled = false
    end
  end

# 続く

ドロップするユニットの数を指定できるようにしている。

あと、学習を行うときにはドロップアウトを有効にし、そうでないときにはドロップアウトを無効にする必要があるので、@learn_modeのセッタの中で、その切り替えを行うようにしている。

復元

続いて、復元。

# 続き

  def self.load(filename)
    nn_sarsa_com = nil
    File.open(filename) do |file|
      nn_sarsa_com = Marshal.load(file)
    end
    if nn_sarsa_com.mark == 1
      nn_sarsa_com.instance_variable_set :@mark, Mark::Maru
    elsif nn_sarsa_com.mark == -1
      nn_sarsa_com.instance_variable_set :@mark, Mark::Batsu
    end

    if nn_sarsa_com.is_a?(NNSarsaCom)
      if nn_sarsa_com.instance_variable_get(:@drop_unit_size) == nil
        nn_sarsa_com.instance_variable_set :@drop_unit_size, 0
        value_nn = nn_sarsa_com.instance_variable_get :@value_nn
        value_nn.instance_variable_set :@drop_unit_size, 0
        nn_sarsa_com.learn_mode = nn_sarsa_com.learn_mode # set value_nn state legal.
      end
    end

    nn_sarsa_com
  end

# 以下、省略

こちらは、過去に保存されたデータもそのまま使えるように、ちょっとコードを追加している。

過去に保存されたデータの場合、以下のようにしている:

これで、ドロップアウトが使えなかったときと同じように、問題なく動作するようになる。

今日はここまで!