昨日は複数のインスタンスを同時に学習するということを試してみた。
これ自体は有効に思われ、あとは複数のインスタンスの出す結果をどうやって統合するのかが課題になった。
その方法の一つとして考えられる、ドロップアウトの実装を行ってみた。
なお、ドロップアウトについては以下を参照。
また、ドロップアウトに対応していない関数近似のためのニューラルネットワークの実装は、以下を参照。
ドロップアウト対応版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
というインスタンス変数は、ドロップアウトを有効にしていても無効にしていても、中間層の出力の合計が同程度になるように調整するためのもの。
もともと本で説明されていたのは、の割合だけユニットを使う場合、ドロップアウトを無効にしたときにはドロップアウトしていた層の出力をすべて倍するということだったけど、ここではドロップアウトをしているときには出力を倍にするようにしている。(本質的には変わらないけど、ユニットの数を減らしてるときに出力を増やすという方が自然な発想)
出力と勾配の計算
次は、出力と勾配の計算。
# 続き 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 # 以下、省略
こちらは、過去に保存されたデータもそのまま使えるように、ちょっとコードを追加している。
過去に保存されたデータの場合、以下のようにしている:
- NNSarsaComのインスタンス変数
@drop_unit_size
がないので、0にする - ValueNNのインスタンス変数
@drop_unit_size
がないので、0にする - NNSarsaComのインスタンス変数
@learn_mode
に値をセットすることで、ValueNNのインスタンスの状態を適切なものにする
これで、ドロップアウトが使えなかったときと同じように、問題なく動作するようになる。
今日はここまで!