いものやま。

雑多な知識の寄せ集め

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

昨日は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の実装。

今日はここまで!