読者です 読者をやめる 読者になる 読者になる

いものやま。

雑多な知識の寄せ集め

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

昨日はゲートネットワークの実装を行った。

今日はそれを使って関数近似のためのHMEの実装を行う。

ValueHMEクラス

関数近似のためのHMEをValueHMEクラスとして実装していく。

#====================
# value_hme.rb
#--------------------
# 価値ベクトルを関数近似するためのHME
#====================

require_relative "gate_nn"

class ValueHME

# 続く

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

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

# 続き

  def initialize(input_size, experts)
    @input_size = input_size
    @experts = experts
    @gate_nn = GateNN.new(input_size, @experts.size)

    # cache
    @experts_weights_size = Array.new
    @experts.each do |expert|
      @experts_weights_size.push(expert.get_weights.size)
    end

    @learn_mode = false
  end

  attr_reader :experts
  attr_accessor :learn_mode

# 続く

エキスパートネットワークは外部から受け取るようにしている。
こうすることで、任意の学習器(HME自身を含む)をエキスパートネットワークにすることが出来る。
もちろん、決まったインタフェースを備えている必要はあるけど。

あと、エキスパートネットワークのパラメータのサイズをキャッシュとして持っているようにしている。
これは、パラメータやパラメータに関する勾配を1本のベクトルとして扱うので、そのベクトルのうち、どこまでがどのエキスパートネットワーク(とゲートネットワーク)のパラメータなのかを知る必要があるから。
もちろん、毎回パラメータのサイズをエキスパートネットワークに問い合わせてもいいんだけど、そのたびにエキスパートネットワーク側では重みのベクトル化を行うのでは、効率が悪いので。

それと、拡張のために@learn_modeというインスタンス変数を用意している。

出力と勾配の計算

次は出力と勾配の計算。

# 続き

  def get_value_and_weights_gradient(input)
    experts_output = Array.new
    experts_weights_gradient = Array.new
    @experts.each do |expert|
      expert_output, expert_weights_gradient = expert.get_value_and_weights_gradient(input)
      experts_output.push expert_output
      experts_weights_gradient.push expert_weights_gradient
    end

    gate_outputs, gate_weights_gradient = @gate_nn.get_value_and_weights_gradient(input, experts_output)

    output = experts_output.zip(gate_outputs).reduce(0.0) do |result, (expert_output, gate_output)|
      result += expert_output * gate_output
    end

    weights_gradient = Array.new
    experts_weights_gradient.zip(gate_outputs).each do |expert_weights_gradient, gate_output|
      weights_gradient.push(expert_weights_gradient.map{|expert_weight_gradient| expert_weight_gradient * gate_output})
    end
    weights_gradient.push(gate_weights_gradient)
    weights_gradient.flatten!

    [output, weights_gradient]
  end

# 続く

強化学習とニューラルネットワークを組合せてみた。(その11) - いものやま。の最後に書いた手順に従って、HMEの出力と、その重みに関する勾配を計算している。

感度確認

続いて、感度確認。

# 続き

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

    experts_output = Array.new
    @experts.each_with_index do |expert, expert_index|
      expert_weights_gradient = weights_gradient_copy.shift(@experts_weights_size[expert_index])
      expert_output = expert.get_value_with_weights_gradient(input, expert_weights_gradient, alpha)
      experts_output.push expert_output
    end

    gate_outputs = @gate_nn.get_value_with_weights_gradient(input, weights_gradient_copy, alpha)

    output = experts_output.zip(gate_outputs).reduce(0.0) do |result, (expert_output, gate_output)|
      result += expert_output * gate_output
    end

    output
  end

# 続く

まぁ、いつものとおり。

重みの取得と更新

最後に重みの取得と更新。

# 続き

  def get_weights
    weights = Array.new
    @experts.each do |expert|
      weights.push(expert.get_weights)
    end
    weights.push(@gate_nn.get_weights)

    weights.flatten
  end

  def add_weights(weights_diff)
    weights_diff_copy = weights_diff.dup

    @experts.each_with_index do |expert, expert_index|
      expert_weights_diff = weights_diff_copy.shift(@experts_weights_size[expert_index])
      expert.add_weights(expert_weights_diff)
    end
    @gate_nn.add_weights(weights_diff_copy)
  end
end

# 続く

これも特に難しいことはなく。

動作確認

関数近似のためのニューラルネットワークと同じように、以下の動作確認のコードで動作を確認。

# 続き

if __FILE__ == $PROGRAM_NAME
  require_relative "value_nn"

  value_nn_1_1 = ValueNN.new(3, 10, -1.0, 1.0)
  value_nn_1_2 = ValueNN.new(3, 10, -1.0, 1.0)
  value_nn_1_3 = ValueNN.new(3, 10, -1.0, 1.0)
  value_nn_2_1 = ValueNN.new(3, 10, -1.0, 1.0)
  value_nn_2_2 = ValueNN.new(3, 10, -1.0, 1.0)
  value_hme_1 = ValueHME.new(3, [value_nn_1_1, value_nn_1_2, value_nn_1_3])
  value_hme_2 = ValueHME.new(3, [value_nn_2_1, value_nn_2_2])
  value_hme = ValueHME.new(3, [value_hme_1, value_hme_2])

  [
    [1.0, 1.0, 1.0],
    [1.0, 1.0, 0.0],
    [1.0, 0.0, 1.0],
    [0.0, 1.0, 1.0],
    [1.0, 0.0, 0.0],
    [0.0, 1.0, 0.0],
    [0.0, 0.0, 1.0],
    [0.0, 0.0, 0.0],
  ].each do |input|
    output, weights_gradient = value_hme.get_value_and_weights_gradient(input)
    output_with_weights_gradient = value_hme.get_value_with_weights_gradient(input, weights_gradient)
    diff = output_with_weights_gradient - output

    alpha = 1.0
    upper_bound = nil
    lower_bound = nil
    last = 100
    100.times do |t|
      if diff < 0.0
        upper_bound = alpha
        if lower_bound.nil?
          alpha /= 2.0
        else
          alpha = (upper_bound + lower_bound) / 2.0
        end
      elsif diff < 0.9
        lower_bound = alpha
        if upper_bound.nil?
          alpha /= diff
        else
          alpha = (upper_bound + lower_bound) / 2.0
        end
      elsif diff > 1.1
        upper_bound = alpha
        if lower_bound.nil?
          alpha /= diff
        else
          alpha = (upper_bound + lower_bound) / 2.0
        end
      else
        last = t
        break
      end

      output_with_weights_gradient_and_alpha = value_hme.get_value_with_weights_gradient(input, weights_gradient, alpha)
      diff = output_with_weights_gradient_and_alpha - output
    end

    output_with_01alpha = value_hme.get_value_with_weights_gradient(input, weights_gradient, 0.1 * alpha)
    diff_01alpha = output_with_01alpha - output

    puts "input: #{input}, output: #{output}"
    puts "  alpha: #{alpha}, iterations: #{last}"
    puts "  diff (alpha): #{diff}, diff (0.1*alpha): #{diff_01alpha}"

    weights_diff = weights_gradient.map{|i| i * 0.1 * alpha}
    value_hme.add_weights(weights_diff)
    new_output, _ = value_hme.get_value_and_weights_gradient(input)
    new_diff = new_output - output
    puts "  new output: #{new_output}, diff: #{new_diff}"
  end
end

ここでは、関数近似のためのニューラルネットワークを5つ用意して、value_nn_1_1〜value_nn_1_3をvalue_hme_1というHMEに、value_2_1〜value_2_2をvalue_hme2というもう1つのHMEにして、さらにvalue_hme_1とvalue_hme_2をエキスパートネットワークとしたHMEを作って動作確認している。
(同様にして、何階層にも出来る)

これを実行してみると、次のような出力になる:

input: [1.0, 1.0, 1.0], output: 0.05986416012358227
  alpha: 1.0, iterations: 0
  diff (alpha): 0.9289888817805412, diff (0.1*alpha): 0.2585116061468381
  new output: 0.31837576627042036, diff: 0.2585116061468381
input: [1.0, 1.0, 0.0], output: -0.0088557389615605
  alpha: 1.6901813093565354, iterations: 3
  diff (alpha): 0.9083100963636939, diff (0.1*alpha): 0.2150878300932007
  new output: 0.2062320911316402, diff: 0.2150878300932007
input: [1.0, 0.0, 1.0], output: 0.4098816295435096
  alpha: 2.45126158569981, iterations: 3
  diff (alpha): 0.9349657780321493, diff (0.1*alpha): 0.24806198244540345
  new output: 0.657943611988913, diff: 0.24806198244540345
input: [0.0, 1.0, 1.0], output: 0.606900855467693
  alpha: 22.403913017504227, iterations: 6
  diff (alpha): 0.9483159536041491, diff (0.1*alpha): 0.4518258839565471
  new output: 1.0587267394242401, diff: 0.4518258839565471
input: [1.0, 0.0, 0.0], output: 0.47960205270690637
  alpha: 7.034555778405797, iterations: 4
  diff (alpha): 0.9510846974619821, diff (0.1*alpha): 0.2590382423336586
  new output: 0.7386402950405649, diff: 0.2590382423336586
input: [0.0, 1.0, 0.0], output: 0.82224932422473
  alpha: 6.27189544455273, iterations: 2
  diff (alpha): 0.9216118044509434, diff (0.1*alpha): 0.17414874787148726
  new output: 0.9963980720962172, diff: 0.17414874787148726
input: [0.0, 0.0, 1.0], output: 1.0708394764756104
  alpha: 16.73667461474436, iterations: 2
  diff (alpha): 0.9788545561868958, diff (0.1*alpha): 0.16173027182584243
  new output: 1.2325697483014528, diff: 0.16173027182584243
input: [0.0, 0.0, 0.0], output: 0.8046987548914151
  alpha: 24.164416551615396, iterations: 3
  diff (alpha): 0.9837389148124958, diff (0.1*alpha): 0.2792031061429212
  new output: 1.0839018610343363, diff: 0.2792031061429212

ちゃんと期待通りに動いていることが分かる。

今日はここまで!