昨日はHMEを強化学習の関数近似に使うときの勾配計算について説明した。
今日からは実際にRubyで実装を進めていく。
まずはゲートネットワークの実装から。
GateNNクラス
ゲートネットワークをGateNNクラスとして実装していく。
#==================== # gate_nn.rb #-------------------- # ゲートネットワークのためのニューラルネットワーク # # 活性化関数は、ソフトマックス関数: # g_i(s_1, ... , s_n) = exp(s_i) / sum_j exp(s_j) # ただし、 # s_i = <v_i, x> (<,>は内積) # そして、偏微分d g_i / d w_jは、 # d g_i / d w_j = g_i * (1 - g_i) * x (i = j) # - (g_i)^2 * x (otherwise) # # 出力の重みに関する偏微分は、 # 各エキスパートネットワークの出力y_iを用いて、 # d y / d w_i = \sum_j y_j * (d g_j / d w_i) # なので、勾配を計算するときには、 # 各エキスパートネットワークの出力が必要になる。 #==================== class GateNN # 続く
イニシャライザ
まずは初期化から。
# 続き def initialize(input_size, output_size) @input_size = input_size @output_size = output_size @weights = Array.new @output_size.times do weight = Array.new(@input_size, 0.0) @weights.push(weight) end end # 続く
特に難しいことはなく。
HMEのゲートネットワークの重みは、エキスパートネットワークの出力によって変化していくので、最初は均等で構わない。
このあたりは普通のニューラルネットワークとちょっと違う。
出力と勾配の計算
次に、出力と勾配の計算。
# 続き def get_value_and_weights_gradient(input, experts_output) inner_prods = @weights.map do |weight| input.zip(weight).reduce(0.0) do |result, (i, w)| result += i * w end end max_inner_prod = inner_prods.max middle_outputs = Array.new middle_output_sum = 0.0 inner_prods.each do |inner_prod| middle_output = Math.exp(inner_prod - max_inner_prod) middle_outputs.push middle_output middle_output_sum += middle_output end outputs = Array.new middle_outputs.each do |middle_output| outputs.push(middle_output / middle_output_sum) end weights_gradient = Array.new @weights.size.times do |weight_index| delta = 0.0 experts_output.each_with_index do |expert_output, expert_index| if weight_index == expert_index delta += expert_output * outputs[expert_index] * (1 - outputs[expert_index]) else delta -= expert_output * (outputs[expert_index] ** 2) end end weights_gradient.push(input.map{|i| delta * i}) end weights_gradient.flatten! [outputs, weights_gradient] end # 続く
まず、昨日書いたゲートネットワークの出力計算のとおりに計算していく。
そのあと、その出力と、エキスパートネットワークの出力を使って、各重みに対する勾配を求めている。
(2016-03-07追記)
重みと入力の内積から中間出力を求めるときに、内積の値が大きくなりすぎて、Math.exp()
がNaN
を返すということが起こった。
そこで、内積で最も大きい値を内積から一律に引くことで、NaN
にならないように修正を行った。
感度確認
続いて感度確認の計算。
# 続き def get_value_with_weights_gradient(input, weights_gradient, alpha=1.0) weights_gradient_copy = weights_gradient.dup middle_outputs = Array.new middle_output_sum = 0.0 @weights.each do |weight| weight_gradient = weights_gradient_copy.shift(@input_size) inner_prod = input.zip(weight, weight_gradient).reduce(0.0) do |result, (i, w, d)| result += i * (w + d * alpha) end middle_output = Math.exp(inner_prod) middle_outputs.push middle_output middle_output_sum += middle_output end outputs = Array.new middle_outputs.each do |middle_output| outputs.push(middle_output / middle_output_sum) end outputs end # 続く
引数として指定された出力に関する重みの勾配を、オプションの引数として指定されたステップサイズ(デフォルトは1.0)の分だけ足し込んで、出力の計算を行っているだけ。
重みの取得と更新
最後に重みの取得と更新。
# 続き def get_weights @weights.flatten end def add_weights(weights_diff) weights_diff.each_slice(@input_size).each_with_index do |hidden_unit_weights_diff, hidden_unit_index| hidden_unit_weights_diff.each_with_index do |hidden_unit_weight_diff, weight_index| @weights[hidden_unit_index][weight_index] += hidden_unit_weight_diff end end end end
これも特に難しいことはなく。
これでゲートネットワークの実装は出来たので、明日はHMEの実装。
今日はここまで!