いものやま。

雑多な知識の寄せ集め

強化学習用のニューラルネットワークをSwiftで書いてみた。(その8)

前回は○×ゲームをSwiftで実装した。

今日はSarsaComの実装。

なお、Rubyでの実装は、以下を参照:

SarsaComクラス

ということで、さっそく。

//==============================
// TicTacToe
//------------------------------
// SarsaCom.swift
//==============================

import Foundation

@objc class SarsaCom: NSObject, Player, NSCoding {
  private static let isMaruKey = "isMaru"
  private static let valueNetworkKey = "valueNetwork"
  private static let epsilonKey = "epsilon"
  private static let stepSizeKey = "stepSize"
  private static let tdLambdaKey = "tdLambda"
  private static let isLearningKey = "isLearning"
  
  let mark: Mark
  private let valueNetwork: ValueNetwork
  private let epsilon: Double
  private let stepSize: Double
  private let tdLambda: Double
  
  private var previousState: State!
  private var currentState: State!
  private var accumulatedWeightGradient: Weight!
  
  var isLearning: Bool
  
  init(mark: Mark, valueNetwork: ValueNetwork,
       epsilon: Double = 0.1, stepSize: Double = 0.01, tdLambda: Double = 0.6) {
    self.mark = mark
    self.valueNetwork = valueNetwork
    self.epsilon = epsilon
    self.stepSize = stepSize
    self.tdLambda = tdLambda
    
    self.previousState = nil
    self.currentState = nil
    self.accumulatedWeightGradient = nil
    
    self.isLearning = true
    
    super.init()
  }
  
  required init(coder aDecoder: NSCoder) {
    // NOTE: marks are singleton objects.
    if aDecoder.decodeBoolForKey(SarsaCom.isMaruKey) {
      self.mark = Mark.Maru
    } else {
      self.mark = Mark.Batsu
    }
    self.valueNetwork = aDecoder.decodeObjectForKey(SarsaCom.valueNetworkKey) as! ValueNetwork
    self.epsilon = aDecoder.decodeDoubleForKey(SarsaCom.epsilonKey)
    self.stepSize = aDecoder.decodeDoubleForKey(SarsaCom.stepSizeKey)
    self.tdLambda = aDecoder.decodeDoubleForKey(SarsaCom.tdLambdaKey)
    self.isLearning = aDecoder.decodeBoolForKey(SarsaCom.isLearningKey)
    super.init()
  }
  
  func encodeWithCoder(aCoder: NSCoder) {
    if self.mark == Mark.Maru {
      aCoder.encodeBool(true, forKey: SarsaCom.isMaruKey)
    } else {
      aCoder.encodeBool(false, forKey: SarsaCom.isMaruKey)
    }
    // workaround: protocol 'ValueNetwork' is not for Objective-C
    if let valueNN = self.valueNetwork as? ValueNN {
      aCoder.encodeObject(valueNN, forKey: SarsaCom.valueNetworkKey)
    } else if let valueHME = self.valueNetwork as? ValueHME {
      aCoder.encodeObject(valueHME, forKey: SarsaCom.valueNetworkKey)
    } else {
      fatalError("not supported value network.")
    }
    aCoder.encodeDouble(self.epsilon, forKey: SarsaCom.epsilonKey)
    aCoder.encodeDouble(self.stepSize, forKey: SarsaCom.stepSizeKey)
    aCoder.encodeDouble(self.tdLambda, forKey: SarsaCom.tdLambdaKey)
    aCoder.encodeBool(self.isLearning, forKey: SarsaCom.isLearningKey)
  }
  
  func selectIndex(state: State) -> Int {
    let selectedIndex: Int
    if (!self.isLearning) || (Random.getRandomProbability() > self.epsilon) {
      let actions = state.validActions()
      let actionValues: [Double] = actions.map {
        [unowned self] (index: Int) in
        let newState = state.set(self.mark, atIndex: index)
        return self.valueNetwork.getValue(newState.toVector())
      }
      (selectedIndex, _) = zip(actions, actionValues).maxElement{$0.1 < $1.1}!
    } else {
      selectedIndex = state.validActions().sample()
    }
    self.currentState = state.set(self.mark, atIndex: selectedIndex)
    return selectedIndex
  }
  
  func learn(reward: Double) {
    if self.isLearning && (self.previousState != nil) {
      let previousStateVector = self.previousState.toVector()
      let (previousValue, weightGradient) = self.valueNetwork.getValueAndWeightGradient(previousStateVector)
      
      // normalize sensitivity for weight gradient
      
      var scale = 1.0
      var upperBound: Double! = nil
      var lowerBound: Double! = nil
      var sensitivity = 0.0
      var previousSensitivity = 0.0
      for _ in (0..<10) {
        let newValue = self.valueNetwork.getValue(previousStateVector, withWeightDiff: weightGradient, scale: scale)
        previousSensitivity = sensitivity
        sensitivity = newValue - previousValue
        
        if (sensitivity > 1.1) ||
           (sensitivity < 0.0) ||
           ((upperBound == nil) && (sensitivity < previousSensitivity)) {
          upperBound = scale
          scale = (lowerBound == nil) ? scale / 2.0 : (upperBound + lowerBound) / 2.0
        } else if sensitivity < 0.9 {
          lowerBound = scale
          scale = (upperBound == nil) ? scale * 2.0 : (upperBound + lowerBound) / 2.0
        } else {
          break
        }
      }
      
      // calculate accumulated weight gradient
      
      let scaledWeightGradient = weightGradient * scale
      if self.accumulatedWeightGradient == nil {
        self.accumulatedWeightGradient = scaledWeightGradient
      } else {
        self.accumulatedWeightGradient = (self.accumulatedWeightGradient * self.tdLambda
                                            + scaledWeightGradient)
      }
      
      // update weight by sarsa(lambda)
      
      let valueDiff: Double
      if self.currentState != nil {
        // normal state
        let currentValue = self.valueNetwork.getValue(self.currentState.toVector())
        valueDiff = reward + currentValue - previousValue
      } else {
        // terminal state
        valueDiff = reward - previousValue
      }
      let weightDiff = self.stepSize * valueDiff * self.accumulatedWeightGradient
      self.valueNetwork.addWeight(weightDiff)
      
      // finish episode
      
      if self.currentState == nil {
        self.accumulatedWeightGradient = nil
      }
    }
    
    self.previousState = self.currentState
    self.currentState = nil
  }
}

説明は省略。
(過去の記事を参照)


これで一通り実装できたので、あとは実際に学習を行うだけなんだけど、いろいろ問題が。

具体的には、以下のとおり:

  • Accelerateフレームワークの遅延評価のため、実質的なメモリリークが発生し、使用メモリ量が増え続ける。
  • NSKeyedArchiver/NSKeyedUnarchiverでオブジェクトをエンコード/デコードすると、クラス名が(モジュール名).(クラス名)となるため、ある実行ファイルで保存したファイルを他の実行ファイルでロードすると、例外が発生する。

特に、後者は解決がいろいろと面倒・・・

これらについては、明日以降、修正を行っていきたい。

今日はここまで!


ちょっとお知らせ。
いろいろと生活に変化が生じたので、明日以降、更新の頻度が落ちそう・・・

自転車で都内の桜とか見てきた。

花見の季節。
ということで、久々に自転車に乗って、都内の桜とか、いろいろ見てきた。

ルート

今回走ったルートは、以下:

国道4号を南下して、入谷で鶯谷の方に折れて線路越え。
そして、上野、不忍池を回り、秋葉原を通って靖国通りへ。
そのあと、千鳥ヶ淵を回って、内堀通りをぐるっと回って東側へ。
そこからは東京駅を通って国道6号へ。
途中、浅草に寄って、スカイツリーを眺めつつ、そのまま国道6号を北上。
最後、国道298号にぶつかったら、今度は国道298号を北上、というルート。

上野

まずは上野から。

今年は行ったのが遅かったこともあり、だいぶ散っちゃってた。
桜のソフトクリームも食べたかったんだけど、終わっちゃってたり。

まずは国立科学博物館横の桜。

f:id:yamaimo0625:20160410134614p:plain

ただ、キレイに咲いてたのはホントここぐらいで、あとは全体的に葉桜。

f:id:yamaimo0625:20160410133310p:plain

ちょっと残ってたとしても、こんな感じ。

f:id:yamaimo0625:20160410133323p:plain

不忍池

そこから不忍池の方へ。
池の周りの方が涼しいのか、こっちの方がまだ残ってた感じ。
種類もいろいろ。

f:id:yamaimo0625:20160410133406p:plain

f:id:yamaimo0625:20160410133414p:plain

f:id:yamaimo0625:20160410133424p:plain

f:id:yamaimo0625:20160410133435p:plain

千鳥ヶ淵

上野のあとは靖国通りを通って千鳥ヶ淵へ。

ただ、こちらもやはりだいぶ散ってた・・・

f:id:yamaimo0625:20160410133533p:plain

通路もこんな感じ。

f:id:yamaimo0625:20160410133544p:plain

むしろ、足元の花の方がキレイだったり。
(シャガという花らしい)

f:id:yamaimo0625:20160410133605p:plain

東京駅

千鳥ヶ淵からは内堀通りをぐるっと回って東京駅へ。

f:id:yamaimo0625:20160410133615p:plain

実は、東京駅は東側からしか見たことがなくて、西側から見るのはおそらく初めて。
レンガ造りの駅舎、いいねぇ。

浅草

東京駅を過ぎたら国道6号に乗って北上。
そして、浅草へ。

f:id:yamaimo0625:20160410133624p:plain

浅草、めっちゃ混んでたw
こんなに人いるんだねぇ。

今回、浅草に寄った理由は、久々に舟和の芋ようかんが食べたくなったから。

小さい頃はお土産でもらったのをよく食べてたんだけど、最近は全然食べてなかった。
けど、ネットで久々に見て食べたくなったw

本店で詰め合わせを買って帰ったんだけど、これは美味しいね!
ほどよい甘みで筋もほとんどなく、すごく美味しかった。
また買いに行こう・・・

東京スカイツリー

浅草からは隅田川を渡って東京スカイツリーの方へ。

f:id:yamaimo0625:20160410133634p:plain

今回は横目で見るだけ。
もうちょいライトアップされてると、キレイなんだろうけどなぁ。

f:id:yamaimo0625:20160410133648p:plain

そこからはひたすら国道6号を北上。

6号の噂はいろいろ聞いてたけど、確かにちょっと危ないかも。
路肩に微妙な段差が多くて、ちょっと怖かった。
(まぁ、国道4号も道がガタガタになってるところが多くて、そんなに走りやすくはないので、微妙なところ)

今日はここまで!

越谷市の花田苑に行ってきた。

埼玉県越谷市にある日本庭園、花田苑

すごく地元なんだけど、自分も最近まで存在を知らなくて、この間、初めて行った。

桜がすごくキレイだったので、ちょっと紹介。

f:id:yamaimo0625:20160409023856p:plain

入り口

入り口からもう見事な桜。

f:id:yamaimo0625:20160409025043p:plain

この桜を眺めながら進むと門があるので、そこで入園料を100円払えば、中に入れる。

園内の様子

入るとまず目に入るのが、大きな池。

f:id:yamaimo0625:20160409025536p:plain

ちなみに、奥に見えるのは能楽堂

日本庭園と銘打っているけど、広くて見晴らしがいいので、風情のある公園という方があってるかもしれない。
侘び寂びという感じはあまりなく、人工的に整えられている感も否めない・・・のだけど、のどかな空気はなかなかいい。

ちょうど桜が見ごろで、キレイに咲いていた。

f:id:yamaimo0625:20160409025948p:plain

f:id:yamaimo0625:20160409030014p:plain

f:id:yamaimo0625:20160409030101p:plain

f:id:yamaimo0625:20160409030127p:plain

f:id:yamaimo0625:20160409030138p:plain

桜の足元には、キレイな水仙も。

f:id:yamaimo0625:20160409030243p:plain

池の端の方には、舟もあったり。
まぁ、飾りのようなものだと思うけどw

f:id:yamaimo0625:20160409030548p:plain

この船着き場の近くには茶室もあって、定期的にお茶会も開かれてるみたい。

他にも、藤棚があって、4月下旬〜5月上旬は藤も楽しめそうだった。

それ以外にも様々な草花が。

f:id:yamaimo0625:20160409031122p:plain

f:id:yamaimo0625:20160409031131p:plain

f:id:yamaimo0625:20160409031147p:plain

行ったときには終わっちゃってたけど、梅の樹もあったので、梅の頃もキレイだったかもしれない。

あと、カエデもあったり。

f:id:yamaimo0625:20160409031242p:plain

まだ若い緑だけど、秋にはキレイに紅葉しそう。

一角は小高い丘になっていて、ちょっとした趣向が凝らされてた。

f:id:yamaimo0625:20160409031905p:plain

f:id:yamaimo0625:20160409031919p:plain

岩が並んでいる風景は、去年の冬に行った松尾大社の上古の庭を少し思い出したり。
規模は違うけどw

丘の上では水が湧き出していて、流れを作っているのもまた面白い。

f:id:yamaimo0625:20160409032422p:plain

まぁ、わざわざ観光しに来るようなところかというと、そんな感じではないのだけど、四季折々でいろんな草花が楽しめるように管理されているようなので、天気のいい日にふらっと立ち寄って散歩をするにはいい場所だと思う。

アクセス

さて、アクセスだけど、駅からけっこう離れているので、遠方からだと少しアクセスしづらいかもしれない。
一応、駐車場があるので、車で行くことは可能。
公共交通機関を使うなら、武蔵野線南越谷駅(もしくは東武伊勢崎線新越谷駅)から朝日バスで市立病院方面に向かい、花田苑前で降りることになる。

あと、もうちょっと足をのばすと、キャンベルタウン野鳥の森という施設もある。
いろんな鳥を見ることが出来るみたいなので、ついでに行ってみるのもありかも。

今日はここまで!

強化学習用のニューラルネットワークをSwiftで書いてみた。(その7)

昨日はValueNetworkの保存とロードの実装をした。
(ただ、いろいろ問題があったので、後で修正する予定)

これで実際に学習をするために、今日は○×ゲームをSwiftで実装する。

Markクラス

まずはマークを表すMarkクラスから。
enumで実装するのも一つの手だけど(というか、その方がSwiftっぽくはある)、今回はJavaenumに実装した。
(Swiftのenumだと、switchを書くのが面倒)

//==============================
// TicTacToe
//------------------------------
// Mark.swift
//==============================

import Foundation

class Mark: CustomStringConvertible, Hashable {
  static let Empty = Mark(mark: ".", value: 0.0, hash: 0)
  static let Maru = Mark(mark: "o", value: 1.0, hash: 1)
  static let Batsu = Mark(mark: "x", value: -1.0, hash: 2)
  
  let mark: String
  let value: Double
  let hash: Int
  
  private init(mark: String, value: Double, hash: Int) {
    self.mark = mark
    self.value = value
    self.hash = hash
  }
  
  var description: String {
    return self.mark
  }
  
  var hashValue: Int {
    return self.hash
  }
  
  var isEmpty: Bool {
    return self === Mark.Empty
  }
  
  var opponent: Mark {
    if self === Mark.Maru {
      return Mark.Batsu
    } else if self === Mark.Batsu {
      return Mark.Maru
    } else {
      return Mark.Empty
    }
  }
}

func ==(left: Mark, right: Mark) -> Bool {
  return left === right
}

Stateクラス

次は状態を表すStateクラス。

//==============================
// TicTacToe
//------------------------------
// State.swift
//==============================

import Foundation

class State: CustomStringConvertible {
  private let state: [Mark]
  
  convenience init() {
    let state = Array(count: 9, repeatedValue: Mark.Empty)
    self.init(state: state)
  }
  
  private init(state: [Mark]) {
    self.state = state
  }
  
  var description: String {
    return (0..<3).map {
      [unowned self] (row: Int) in
      let from = 3 * row
      let to = 3 * (row + 1)
      return self.state[from..<to].map{$0.mark}.joinWithSeparator("")
    }.joinWithSeparator("\n")
  }
  
  subscript(index: Int) -> Mark {
    return self.state[index]
  }
  
  func set(mark: Mark, atIndex index: Int) -> State {
    var newState = Array(self.state)
    newState[index] = mark
    return State(state: newState)
  }
  
  func toVector() -> Vector {
    return Vector.fromArray(self.state.map{$0.value})
  }
  
  func validActions() -> [Int] {
    return (0..<9).filter {
      [unowned self] (index: Int) in
      self.state[index].isEmpty
    }
  }
  
  func win(mark: Mark) -> Bool {
    let lines = [
      (0, 1, 2), (3, 4, 5), (6, 7, 8),
      (0, 3, 6), (1, 4, 7), (2, 5, 8),
      (0, 4, 8), (2, 4, 6)]
    for (i, j, k) in lines {
      if (self.state[i] == mark) &&
         (self.state[j] == mark) &&
         (self.state[k] == mark) {
        return true
      }
    }
    return false
  }
  
  var isDraw: Bool {
    return self.validActions().isEmpty && (!self.win(Mark.Maru)) && (!self.win(Mark.Batsu))
  }
  
  var isEnd: Bool {
    return self.win(Mark.Maru) || self.win(Mark.Batsu) || self.validActions().isEmpty
  }
}

Playerプロトコル

そして、プレイヤーを表すPlayerプトロコル。

//==============================
// TicTacToe
//------------------------------
// Player.swift
//==============================

import Foundation

protocol Player {
  var mark: Mark { get }
  var isLearning: Bool { get set }
  func selectIndex(state: State) -> Int
  func learn(reward: Double)
}

HumanPlayerクラス

人間プレイヤーを表すHumanPlayerクラス。

//==============================
// TicTacToe
//------------------------------
// HumanPlayer.swift
//==============================

import Foundation

class HumanPlayer: Player {
  let mark: Mark
  
  var isLearning: Bool {
    get { return false }
    set { /* ignore */ }
  }
  
  init(mark: Mark) {
    self.mark = mark
  }
  
  func selectIndex(state: State) -> Int {
    let stdin = NSFileHandle.fileHandleWithStandardInput()
    
    print("<player: \(self.mark)>")
    let actions = state.validActions()
    while true {
      print("select index [\(actions.map{$0.description}.joinWithSeparator(","))]")
      let input = stdin.readString()
      if let selectedIndex = Int(input) {
        if actions.indexOf(selectedIndex) != nil {
          return selectedIndex
        } else {
          print("invalid number.")
        }
      } else {
        print("invalid input.")
      }
    }
  }
  
  func learn(reward: Double) {
    // do nothing
  }
}

Gameクラス

最後に、ゲームを行うためのGameクラス。

//==============================
// TicTacToe
//------------------------------
// Game.swift
//==============================

import Foundation

class Game {
  private class func verboseOutput(description: String) {
    print(description)
  }
  
  private class func emptyOutput(description: String) {
    // do nothing
  }
  
  let players: [Mark: Player]
  
  init(maruPlayer: Player, batsuPlayer: Player) {
    self.players = [
      Mark.Maru: maruPlayer,
      Mark.Batsu: batsuPlayer,
    ]
  }
  
  func start(verbose: Bool = false) -> Mark {
    let output = verbose ? Game.verboseOutput : Game.emptyOutput
    
    var state = State()
    var currentPlayerMark = Mark.Maru
    var winner = Mark.Empty
    
    while true {
      let currentPlayer = self.players[currentPlayerMark]!
      
      output(state.description)
      let index = currentPlayer.selectIndex(state)
      output("player \(currentPlayerMark) selected \(index).")
      
      state = state.set(currentPlayerMark, atIndex: index)
      currentPlayer.learn(0.0)
      
      if state.win(currentPlayerMark) {
        winner = currentPlayerMark
        currentPlayer.learn(1.0)
        self.players[currentPlayerMark.opponent]!.learn(-1.0)
        output(state.description)
        output("player \(currentPlayerMark) win.")
        break
      } else if state.isDraw {
        for (_, player) in self.players {
          player.learn(0.0)
        }
        output(state.description)
        output("draw.")
        break
      } else {
        currentPlayerMark = currentPlayerMark.opponent
      }
    }
    
    return winner
  }
}

ちょっと説明が必要かもしれないところは、冗長出力の切り替え。
冗長出力を行うクラスメソッドと行わないクラスメソッドを用意しておいて、関数ポインタの指し先を切り替えることで、冗長出力を行うかどうかを切り替えている。

動作確認

とりあえずこれで人同士でプレイできるようにしたコードが以下:

//==============================
// TicTacToe
//------------------------------
// main.swift
//==============================

import Foundation

// Human v.s. Human

var maruPlayer: Player = HumanPlayer(mark: Mark.Maru)
var batsuPlayer: Player = HumanPlayer(mark: Mark.Batsu)
var game = Game(maruPlayer: maruPlayer, batsuPlayer: batsuPlayer)
game.start(true)

この実行例は、以下:

...
...
...
<player: o>
select index [0,1,2,3,4,5,6,7,8]
4
player o selected 4.
...
.o.
...
<player: x>
select index [0,1,2,3,5,6,7,8]
0
player x selected 0.
x..
.o.
...
<player: o>
select index [1,2,3,5,6,7,8]
8
player o selected 8.
x..
.o.
..o
<player: x>
select index [1,2,3,5,6,7]
2
player x selected 2.
x.x
.o.
..o
<player: o>
select index [1,3,5,6,7]
1
player o selected 1.
xox
.o.
..o
<player: x>
select index [3,5,6,7]
7
player x selected 7.
xox
.o.
.xo
<player: o>
select index [3,5,6]
3
player o selected 3.
xox
oo.
.xo
<player: x>
select index [5,6]
5
player x selected 5.
xox
oox
.xo
<player: o>
select index [6]
6
player o selected 6.
xox
oox
oxo
draw.

今日はここまで!

強化学習用のニューラルネットワークをSwiftで書いてみた。(その6)

昨日は強化学習用のHMEの実装を行った。

今日はそれらのデータの保存とロードが出来るようにする。

(2016-04-08追記:いろいろ問題があったので、大幅に修正する予定)

NSKeyedArchiver、NSKeyedUnarchiver

データをファイルに保存し、ロードする一つの方法は、NSDictionary#writeToFile()NSDictionary(contentsOfFile:)を利用する方法。

ただ、ValueHMEのような再帰的な構造を持っていると、保存するデータをDictionaryの形に変換するのは、ちょっと面倒。
(ただ、今から考えると、そうした方がよかったかも・・・)

そこで、NSKeyedArchiverとNSKeyedUnarchiverを使うことにした。
これらを使うと、NSCodingプロトコルに準拠したオブジェクトを、オブジェクト間の構造を保ったまま比較的簡単に保存することが出来る。

例えば、データをファイルに保存するには、次のようにする:

NSKeyedArchiver.archiveRootObject(保存したいオブジェクト, toFile: ファイル名)

逆に、データをファイルからロードするには、次のようにする:

let ロードされたオブジェクト = NSKeyedUnarchiver.unarchiveObjectWithFile(ファイル名) as! オブジェクトのクラス

NSCodingプロトコル

NSKeyedArchiverとNSKeyedUnarchiverでデータの保存とロードを出来るようにするためには、NSCodingプロトコルに準拠する必要がある。

NSCodingプロトコルでは、次の2つを実装する必要がある:

  • init(coder aDecoder: NSCoder)
    オブジェクトのデコード方法を定義する
  • func encodeWithCoder(aCoder: NSCoder)
    オブジェクトのエンコード方法を定義する

NSCoderプロトコルというのはNSKeyedArchiverとNSKeyedUnarchiverのいずれも準拠しているプロトコルで、基本的なデータのエンコード、デコードを行うためのメソッドが用意されている。
なので、これを使ってプロパティの保存をしたり、あるいは、プロパティの復元をしたりする。

ValueNetworkプロトコル、Weightプロトコルの修正

まず、保存とロードの対象となるValueNetworkプロトコルとWeightプロトコルを、NSCodingプロトコルを継承するように修正:

//==============================
// ValueNetwork
//------------------------------
// ValueNetwork.swift
//==============================

import Foundation

protocol ValueNetwork: NSCoding {
  // 省略
}
//==============================
// ValueNetwork
//------------------------------
// Weight.swift
//==============================

import Foundation

protocol Weight: NSCoding {
  // 省略
}

// 省略

ValueNNクラス、ValueNN.NNWeightクラスの修正

ValueNNクラス、ValueNN.NNWeightクラスを、NSCodingに準拠させるために、次のように修正:

//==============================
// ValueNetwork
//------------------------------
// ValueNN.swift
//==============================

import Foundation

@objc class ValueNN: NSObject, ValueNetwork {
  @objc class NNWeight: NSObject, Weight {
    private static let hiddenLayerWeightKey = "hiddenLayerWeight"
    private static let hiddenLayerBiasKey = "hiddenLayerBias"
    private static let outputLayerWeightKey = "outputLayerWeight"
    private static let outputLayerBiasKey = "outputLayerBias"
    
    // 省略
    
    private init(hiddenLayerWeight: Matrix,
                 hiddenLayerBias: Vector,
                 outputLayerWeight: Vector,
                 outputLayerBias: Double) {
      // 省略
      super.init()
    }
    
    convenience required init(coder aDecoder: NSCoder) {
      let hiddenLayerWeightBuffer = aDecoder.decodeObjectForKey(NNWeight.hiddenLayerWeightKey) as! [[Double]]
      let hiddenLayerBiasBuffer = aDecoder.decodeObjectForKey(NNWeight.hiddenLayerBiasKey) as! [Double]
      let outputLayerWeightBuffer = aDecoder.decodeObjectForKey(NNWeight.outputLayerWeightKey) as! [Double]
      let outputLayerBias = aDecoder.decodeDoubleForKey(NNWeight.outputLayerBiasKey)
      
      let hiddenLayerWeight = Matrix.fromArray(hiddenLayerWeightBuffer)
      let hiddenLayerBias = Vector.fromArray(hiddenLayerBiasBuffer)
      let outputLayerWeight = Vector.fromArray(outputLayerWeightBuffer)
      
      self.init(hiddenLayerWeight: hiddenLayerWeight,
                hiddenLayerBias: hiddenLayerBias,
                outputLayerWeight: outputLayerWeight,
                outputLayerBias: outputLayerBias)
    }
    
    func encodeWithCoder(aCoder: NSCoder) {
      let hiddenLayerWeightBuffer: [[Double]] = self.hiddenLayerWeight.toArray()
      let hiddenLayerBiasBuffer: [Double] = self.hiddenLayerBias.toArray()
      let outputLayerWeightBuffer: [Double] = self.outputLayerWeight.toArray()
      
      aCoder.encodeObject(hiddenLayerWeightBuffer, forKey: NNWeight.hiddenLayerWeightKey)
      aCoder.encodeObject(hiddenLayerBiasBuffer, forKey: NNWeight.hiddenLayerBiasKey)
      aCoder.encodeObject(outputLayerWeightBuffer, forKey: NNWeight.outputLayerWeightKey)
      aCoder.encodeDouble(self.outputLayerBias, forKey: NNWeight.outputLayerBiasKey)
    }
    
    // 省略
  }
  
  private static let outputMinKey = "outputMin"
  private static let outputMaxKey = "outputMax"
  private static let weightKey = "weight"
  
  // 省略
  
  init(inputSize: Int, hiddenUnitSize: Int, outputMin: Double, outputMax: Double) {
    // 省略
    super.init()
  }
  
  required init(coder aDecoder: NSCoder) {
    self.outputMin = aDecoder.decodeDoubleForKey(ValueNN.outputMinKey)
    self.outputMax = aDecoder.decodeDoubleForKey(ValueNN.outputMaxKey)
    self.weight = aDecoder.decodeObjectForKey(ValueNN.weightKey) as! NNWeight
    super.init()
  }
  
  func encodeWithCoder(aCoder: NSCoder) {
    aCoder.encodeDouble(self.outputMin, forKey: ValueNN.outputMinKey)
    aCoder.encodeDouble(self.outputMax, forKey: ValueNN.outputMaxKey)
    aCoder.encodeObject(self.weight, forKey: ValueNN.weightKey)
  }
  
  // 省略
}

NSKeyedArchiverとNSKeyedUnarchiverで保存とロードが出来るようにするためには、Objective-Cのオブジェクトでないといけないので、@objcをつけ、NSObjectを継承するようにしてある。
そして、NSCodingプロトコルに準拠するようにメソッドを追加している。

GateNetworkクラス、GateNetwork.GateWeightクラスの修正

同様に、GateNetworkクラス、GateNetwork.GateWeightクラスの修正。

//==============================
// ValueNetwork
//------------------------------
// GateNetwork.swift
//==============================

import Foundation
import Accelerate

@objc class GateNetwork: NSObject, NSCoding {
  @objc class GateWeight: NSObject, Weight {
    private static let weightKey = "weight"
    
    // 省略
    
    private init(weight: Matrix) {
      // 省略
      super.init()
    }
    
    convenience required init(coder aDecoder: NSCoder) {
      let weightBuffer = aDecoder.decodeObjectForKey(GateWeight.weightKey) as! [[Double]]
      let weight = Matrix.fromArray(weightBuffer)
      self.init(weight: weight)
    }
    
    func encodeWithCoder(aCoder: NSCoder) {
      let weightBuffer: [[Double]] = self.weight.toArray()
      aCoder.encodeObject(weightBuffer, forKey: GateWeight.weightKey)
    }
    
    // 省略
  }
  
  private static let weightKey = "weight"
  
  // 省略
  
  init(inputSize: Int, outputSize: Int) {
    // 省略
    super.init()
  }
  
  required init(coder aDecoder: NSCoder) {
    self.weight = aDecoder.decodeObjectForKey(GateNetwork.weightKey) as! GateWeight
    super.init()
  }
  
  func encodeWithCoder(aCoder: NSCoder) {
    aCoder.encodeObject(self.weight, forKey: GateNetwork.weightKey)
  }
  
  // 省略
}

ValueHMEクラス、ValueHME.HMEWeightクラスの修正

最後にValueHMEクラス、ValueHME.HMEWeightクラスの修正・・・なんだけど、ここでちょっと誤算が。
とりあえず、修正後のコードは以下:

//==============================
// ValueNetwork
//------------------------------
// ValueHME.swift
//==============================

import Foundation

@objc class ValueHME: NSObject, ValueNetwork {
  @objc class HMEWeight: NSObject, Weight {
    private static let expertWeightKey = "expertWeight"
    private static let gateWeightKey = "gateWeight"
    
    // 省略
    
    private init(expertWeight: [Weight], gateWeight: Weight) {
      // 省略
      super.init()
    }
    
    required init(coder aDecoder: NSCoder) {
      // workaround: protocol 'Weight' is not for Objective-C
      let expertWeightData = aDecoder.decodeObjectForKey(HMEWeight.expertWeightKey) as! [NSData]
      self.expertWeight = expertWeightData.map {
        (weightData: NSData) in
        return NSKeyedUnarchiver.unarchiveObjectWithData(weightData) as! Weight
      }
      self.gateWeight = aDecoder.decodeObjectForKey(HMEWeight.gateWeightKey) as! Weight
      super.init()
    }
    
    func encodeWithCoder(aCoder: NSCoder) {
      // workaround: protocol 'Weight' is not for Objective-C
      let expertWeightData: [NSData] = self.expertWeight.map {
        (weight: Weight) in
        if let nnWeight = weight as? ValueNN.NNWeight {
          return NSKeyedArchiver.archivedDataWithRootObject(nnWeight)
        } else if let hmeWeight = weight as? ValueHME.HMEWeight {
          return NSKeyedArchiver.archivedDataWithRootObject(hmeWeight)
        } else {
          fatalError("not supperted.")
        }
      }
      aCoder.encodeObject(expertWeightData, forKey: HMEWeight.expertWeightKey)
      aCoder.encodeObject(self.gateWeight, forKey: HMEWeight.gateWeightKey)
    }
    
    // 省略
  }
  
  private static let expertsKey = "experts"
  private static let gateNetworkKey = "gateNetwork"
  
  // 省略
  
  init(inputSize: Int, experts: [ValueNetwork]) {
    // 省略
    super.init()
  }
  
  required init(coder aDecoder: NSCoder) {
    // workaround: protocol 'ValueNetwork' is not for Objective-C
    let expertsData = aDecoder.decodeObjectForKey(ValueHME.expertsKey) as! [NSData]
    self.experts = expertsData.map {
      (expertData: NSData) in
      return NSKeyedUnarchiver.unarchiveObjectWithData(expertData) as! ValueNetwork
    }
    self.gateNetwork = aDecoder.decodeObjectForKey(ValueHME.gateNetworkKey) as! GateNetwork
    super.init()
  }
  
  func encodeWithCoder(aCoder: NSCoder) {
    // workaround: protocol 'ValueNetwork' is not for Objective-C
    let expertsData: [NSData] = self.experts.map {
      (expert: ValueNetwork) in
      if let valueNN = expert as? ValueNN {
        return NSKeyedArchiver.archivedDataWithRootObject(valueNN)
      } else if let valueHME = expert as? ValueHME {
        return NSKeyedArchiver.archivedDataWithRootObject(valueHME)
      } else {
        fatalError("not supperted.")
      }
    }
    aCoder.encodeObject(expertsData, forKey: ValueHME.expertsKey)
    aCoder.encodeObject(self.gateNetwork, forKey: ValueHME.gateNetworkKey)
  }
  
  // 省略
}

最初は、次のように保存が出来ると思ってた:

@objc class ValueHME: NSObject, ValueNetwork {

  // 省略

  func encodeWithCoder(aCoder: NSCoder) {
    aCoder.encodeObject(self.experts, forKey: ValueHME.expertsKey)
    aCoder.encodeObject(self.gateNetwork, forKey: ValueHME.gateNetworkKey)
  }

  // 省略
}

でも、これだとダメ。
というのも、ValueNetworkプロトコルObjective-Cプロトコルになっていないので、これだとNSKeyedArchiverでは保存できないから。

かといって、ValueNetworkプロトコルObjective-Cプロトコルになるように、次のように修正したとする:

@objc protocol ValueNetwork: class, NSCoding {
  // 省略
}

すると今度は、インタフェースにVectorクラスが出てくるので、VectorクラスがObjective-Cのクラスであることが要求されてくる。
けど、Vectorクラスは引数の型によるメソッドのオーバーロードを行っているので、Objective-Cのクラスには簡単には出来ない・・・

妥協策として、Objective-Cのクラスにした具体的なクラスにダウンキャストして、そのオブジェクトをNSKeyedArchiverを使ってNSDataに変換し、保存するとしている。
ただ、ValueHMEはエキスパートネットワークに依存しないように実装していたのに、これで依存関係が生じるようになってしまった・・・
SwiftがObjective-Cの資産に頼らない独立したクラスライブラリを持っていれば、こんな問題は起きないんだけど、まぁ仕方ない。

動作確認

動作確認として、ValueNN、GateNetwork、ValueHMEの動作確認のコードを以下のように修正した:

//==============================
// ValueNetwork
//------------------------------
// main.swift
//
// Test code for ValueNetwork
//==============================

import Foundation

// 省略

// ValueNN

// 省略

NSKeyedArchiver.archiveRootObject(valueNN, toFile: "ValueNN.dat")

let restoredValueNN = NSKeyedUnarchiver.unarchiveObjectWithFile("ValueNN.dat") as! ValueNetwork
for col in (0..<inputMatrix.col) {
  let input = inputMatrix.colVector(col)
  let original = valueNN.getValue(input)
  let restored = restoredValueNN.getValue(input)
  print ("input: \(input.transpose()), original: \(original), restored: \(restored)")
}

// GateNetwork

// 省略

NSKeyedArchiver.archiveRootObject(gateNetwork, toFile: "GateNetwork.dat")

let restoredGateNetwork = NSKeyedUnarchiver.unarchiveObjectWithFile("GateNetwork.dat") as! GateNetwork
for col in (0..<inputMatrix.col) {
  let input = inputMatrix.colVector(col)
  let originalGateOutput = gateNetwork.getValue(input)
  let restoredGateOutput = restoredGateNetwork.getValue(input)
  let original = originalGateOutput +* expertOutput
  let restored = restoredGateOutput +* expertOutput
  print ("input: \(input.transpose()), original: \(original), restored: \(restored)")
}

// ValueHME

// 省略

NSKeyedArchiver.archiveRootObject(valueHME, toFile: "ValueHME.dat")

let restoredValueHME = NSKeyedUnarchiver.unarchiveObjectWithFile("ValueHME.dat") as! ValueNetwork
for col in (0..<inputMatrix.col) {
  let input = inputMatrix.colVector(col)
  let original = valueHME.getValue(input)
  let restored = restoredValueHME.getValue(input)
  print ("input: \(input.transpose()), original: \(original), restored: \(restored)")
}

いずれも、一度オブジェクトをファイルに保存した後、それをロードし、保存前のオブジェクトとロード後のオブジェクトで同じ動作をすることを確認している。

実行例は以下:

----------
ValueNN
----------
// 省略
input: [1.0, 1.0, 1.0], original: 1.20646309393211, restored: 1.20646309393211
input: [1.0, 1.0, 0.0], original: 1.11205199619916, restored: 1.11205199619916
input: [1.0, 0.0, 1.0], original: 1.1206058447007, restored: 1.1206058447007
input: [0.0, 1.0, 1.0], original: 1.16939746775197, restored: 1.16939746775197
input: [1.0, 0.0, 0.0], original: 1.02277258252449, restored: 1.02277258252449
input: [0.0, 1.0, 0.0], original: 1.0788044921628, restored: 1.0788044921628
input: [0.0, 0.0, 1.0], original: 1.08393743462573, restored: 1.08393743462573
input: [0.0, 0.0, 0.0], original: 0.811094155139819, restored: 0.811094155139819
----------
GateNetwork
----------
// 省略
input: [1.0, 1.0, 1.0], original: 0.736714777148314, restored: 0.736714777148314
input: [1.0, 1.0, 0.0], original: 0.685940425590263, restored: 0.685940425590263
input: [1.0, 0.0, 1.0], original: 0.683478674694305, restored: 0.683478674694305
input: [0.0, 1.0, 1.0], original: 0.681627244276781, restored: 0.681627244276781
input: [1.0, 0.0, 0.0], original: 0.605472969776552, restored: 0.605472969776552
input: [0.0, 1.0, 0.0], original: 0.602887409981527, restored: 0.602887409981527
input: [0.0, 0.0, 1.0], original: 0.59937319511023, restored: 0.59937319511023
input: [0.0, 0.0, 0.0], original: 0.5, restored: 0.5
----------
ValueHME
----------
// 省略
input: [1.0, 1.0, 1.0], original: 2.13632052620362, restored: 2.13632052620362
input: [1.0, 1.0, 0.0], original: 1.89965029982235, restored: 1.89965029982235
input: [1.0, 0.0, 1.0], original: 1.93688731245699, restored: 1.93688731245699
input: [0.0, 1.0, 1.0], original: 1.77723520125908, restored: 1.77723520125908
input: [1.0, 0.0, 0.0], original: 1.68732099031037, restored: 1.68732099031037
input: [0.0, 1.0, 0.0], original: 1.57164125986244, restored: 1.57164125986244
input: [0.0, 0.0, 1.0], original: 1.57578328859702, restored: 1.57578328859702
input: [0.0, 0.0, 0.0], original: 1.35896039021037, restored: 1.35896039021037

今日はここまで!

強化学習用のニューラルネットワークをSwiftで書いてみた。(その5)

昨日は強化学習用のHMEの計算を行列で表現した。

今日はそれを使って実際に実装していく。

なお、Rubyでの実装は、以下を参照:

GateNetworkクラス

まずはゲートネットワークの実装。

//==============================
// ValueNetwork
//------------------------------
// GateNetwork.swift
//==============================

import Foundation
import Accelerate

class GateNetwork {
  class GateWeight: Weight {
    private let weight: Matrix
    
    private init(weight: Matrix) {
      self.weight = weight
    }
    
    func scale(scalar: Double) -> Weight {
      let weight = self.weight * scalar
      return GateWeight(weight: weight)
    }
    
    func add(other: Weight) -> Weight {
      let otherWeight = other as! GateWeight
      let weight = self.weight + otherWeight.weight
      return GateWeight(weight: weight)
    }
    
    func subtract(other: Weight) -> Weight {
      let otherWeight = other as! GateWeight
      let weight = self.weight - otherWeight.weight
      return GateWeight(weight: weight)
    }
  }
  
  private var weight: GateWeight
  
  init(inputSize: Int, outputSize: Int) {
    let weight = Matrix.filledWith(0.0, row: outputSize, col: inputSize)
    self.weight = GateWeight(weight: weight)
  }
  
  func getValue(input: Vector) -> Vector {
    let weightedInput = (self.weight.weight * input) as! Vector
    let maxValue: Double = weightedInput.toArray().maxElement()!
    let limitedWeightedInput = weightedInput - Vector.filledWith(maxValue, size: weightedInput.size)
    
    let plainOutput = limitedWeightedInput.map(exp)
    let output = plainOutput.normalizedVector(Int(LA_L1_NORM))
    
    return output
  }
  
  func getValueAndWeightGradient(input: Vector, expertOutput: Vector) -> (Vector, Weight) {
    let output = self.getValue(input)
    
    let outputMatrix = output *+ Vector.filledWith(1.0, size: output.size)
    let gradientMatrix = outputMatrix <*> (Matrix.identityMatrix(output.size) - outputMatrix)
    let weightGradientMatrix = ((gradientMatrix * expertOutput) as! Vector) *+ input
    let weightGradient = GateWeight(weight: weightGradientMatrix)
    
    return (output, weightGradient)
  }
  
  func getValue(input: Vector, withWeightDiff weightDiff: Weight, scale: Double) -> Vector {
    let newWeight = (self.weight + weightDiff * scale) as! GateWeight
    
    let weightedInput = (newWeight.weight * input) as! Vector
    let maxValue: Double = weightedInput.toArray().maxElement()!
    let limitedWeightedInput = weightedInput - Vector.filledWith(maxValue, size: weightedInput.size)
    
    let plainOutput = limitedWeightedInput.map(exp)
    let output = plainOutput.normalizedVector(Int(LA_L1_NORM))
    
    return output
  }
  
  func addWeight(weightDiff: Weight) {
    self.weight = (self.weight + weightDiff) as! GateWeight
  }
}

細かい説明はしないけど、やってる計算は強化学習用のニューラルネットワークをSwiftで書いてみた。(その4) - いものやま。に書いたとおり。

ValueHMEクラス

そして、強化学習用のHMEの実装。

//==============================
// ValueNetwork
//------------------------------
// ValueHME.swift
//==============================

import Foundation

class ValueHME: ValueNetwork {
  class HMEWeight: Weight {
    private let expertWeight: [Weight]
    private let gateWeight: Weight
    
    private init(expertWeight: [Weight], gateWeight: Weight) {
      self.expertWeight = expertWeight
      self.gateWeight = gateWeight
    }
    
    func scale(scalar: Double) -> Weight {
      let expertWeight = self.expertWeight.map {
        (weight: Weight) in
        return weight * scalar
      }
      let gateWeight = self.gateWeight * scalar
      return HMEWeight(expertWeight: expertWeight, gateWeight: gateWeight)
    }
    
    func add(other: Weight) -> Weight {
      let otherWeight = other as! HMEWeight
      let expertWeight = zip(self.expertWeight, otherWeight.expertWeight).map {
        (selfExpertWeight: Weight, otherExpertWeight: Weight) in
        return selfExpertWeight + otherExpertWeight
      }
      let gateWeight = self.gateWeight + otherWeight.gateWeight
      return HMEWeight(expertWeight: expertWeight, gateWeight: gateWeight)
    }
    
    func subtract(other: Weight) -> Weight {
      let otherWeight = other as! HMEWeight
      let expertWeight = zip(self.expertWeight, otherWeight.expertWeight).map {
        (selfExpertWeight: Weight, otherExpertWeight: Weight) in
        return selfExpertWeight - otherExpertWeight
      }
      let gateWeight = self.gateWeight - otherWeight.gateWeight
      return HMEWeight(expertWeight: expertWeight, gateWeight: gateWeight)
    }
  }
  
  class func create(inputSize: Int, outputMin: Double, outputMax: Double, structure: [AnyObject]) -> ValueHME {
    var experts: [ValueNetwork] = []
    for item in structure {
      if let hiddenUnitSize = item as? Int {
        let valueNN = ValueNN(inputSize: inputSize,
                              hiddenUnitSize: hiddenUnitSize,
                              outputMin: outputMin,
                              outputMax: outputMax)
        experts.append(valueNN)
      } else if let lowerStructure = item as? [AnyObject] {
        let lowerHME = ValueHME.create(inputSize,
                                       outputMin: outputMin,
                                       outputMax: outputMax,
                                       structure: lowerStructure)
        experts.append(lowerHME)
      }
    }
    return ValueHME(inputSize: inputSize, experts: experts)
  }
  
  private let experts: [ValueNetwork]
  private let gateNetwork: GateNetwork
  
  init(inputSize: Int, experts: [ValueNetwork]) {
    self.experts = experts
    self.gateNetwork = GateNetwork(inputSize: inputSize, outputSize: experts.count)
  }
  
  func getValue(input: Vector) -> Double {
    let expertOutputArray = self.experts.map {
      (expert: ValueNetwork) in
      return expert.getValue(input)
    }
    let expertOutput = Vector.fromArray(expertOutputArray)
    let gateOutput = self.gateNetwork.getValue(input)
    
    return gateOutput +* expertOutput
  }
  
  func getValueAndWeightGradient(input: Vector) -> (Double, Weight) {
    var expertOutputArray: [Double] = []
    var expertWeightGradientArray: [Weight] = []
    for expert in self.experts {
      let (output, weightGradient) = expert.getValueAndWeightGradient(input)
      expertOutputArray.append(output)
      expertWeightGradientArray.append(weightGradient)
    }
    let expertOutput = Vector.fromArray(expertOutputArray)
    
    let (gateOutput, gateWeightGradient) = self.gateNetwork.getValueAndWeightGradient(input, expertOutput: expertOutput)
    
    let expertWeightGradient = zip(gateOutput.toArray(), expertWeightGradientArray).map {
      (gateOutput: Double, expertWeightGradient: Weight) in
      return gateOutput * expertWeightGradient
    }
    
    let output = gateOutput +* expertOutput
    let weightGradient = HMEWeight(expertWeight: expertWeightGradient, gateWeight: gateWeightGradient)
    
    return (output, weightGradient)
  }
  
  func getValue(input: Vector, withWeightDiff weightDiff: Weight, scale: Double) -> Double {
    let hmeWeightDiff = weightDiff as! HMEWeight
    
    let expertOutputArray = zip(self.experts, hmeWeightDiff.expertWeight).map {
      (expert: ValueNetwork, expertWeightDiff: Weight) in
      return expert.getValue(input, withWeightDiff: expertWeightDiff, scale: scale)
    }
    let expertOutput = Vector.fromArray(expertOutputArray)
    let gateOutput = self.gateNetwork.getValue(input, withWeightDiff: hmeWeightDiff.gateWeight, scale: scale)
    
    return gateOutput +* expertOutput
  }
  
  func addWeight(weightDiff: Weight) {
    let hmeWeightDiff = weightDiff as! HMEWeight
    
    for (expert, expertWeightDiff) in zip(self.experts, hmeWeightDiff.expertWeight) {
      expert.addWeight(expertWeightDiff)
    }
    self.gateNetwork.addWeight(hmeWeightDiff.gateWeight)
  }
}

こちらも強化学習用のニューラルネットワークをSwiftで書いてみた。(その4) - いものやま。に書いたとおり。

動作確認

動作確認として、次のようなコードを書いた。
(ValueNNの動作確認のコードを修正し、GateNetwork、ValueHMEの動作確認を追加している)

//==============================
// ValueNetwork
//------------------------------
// main.swift
//
// Test code for ValueNetwork
//==============================

import Foundation

let inputMatrix = Matrix.fromArray([[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]]).transpose()

// ValueNN

print("----------")
print("ValueNN")
print("----------")

let valueNN = ValueNN(inputSize: 3, hiddenUnitSize: 10, outputMin: -1.0, outputMax: 1.0)

for col in (0..<inputMatrix.col) {
  let input = inputMatrix.colVector(col)
  let (output, weightGradient) = valueNN.getValueAndWeightGradient(input)
  let outputWithWeightGradient = valueNN.getValue(input, withWeightDiff: weightGradient, scale: 1.0)
  var diff = outputWithWeightGradient - output
  
  var scale = 1.0
  var upperBound: Double! = nil
  var lowerBound: Double! = nil
  var last = 100
  for i in (0..<100) {
    if (diff < 0.0) || (1.1 < diff) {
      upperBound =  scale
      scale = (lowerBound == nil) ? scale / 2.0 : (upperBound + lowerBound) / 2.0
    } else if diff < 0.9 {
      lowerBound = scale
      scale = (upperBound == nil) ? scale * 2.0 : (upperBound + lowerBound) / 2.0
    } else {
      last = i
      break
    }
    
    let outputWithScaledWeightGradient = valueNN.getValue(input, withWeightDiff: weightGradient, scale: scale)
    diff = outputWithScaledWeightGradient - output
  }
  
  let outputWith01Scaled = valueNN.getValue(input, withWeightDiff: weightGradient, scale: 0.1 * scale)
  let diffWith01Scaled = outputWith01Scaled - output
  
  print("input: \(input.transpose()), output: \(output)")
  print("  scale: \(scale), iterations: \(last)")
  print("  diff (scaled): \(diff), diff (0.1*scaled): \(diffWith01Scaled)")
  
  let weightDiff = weightGradient * 0.1 * scale
  valueNN.addWeight(weightDiff)
  let newOutput = valueNN.getValue(input)
  let newDiff = newOutput - output
  print("  new output: \(newOutput), diff: \(newDiff)")
}

// GateNetwork

print("----------")
print("GateNetwork")
print("----------")

let gateNetwork = GateNetwork(inputSize: 3, outputSize: 2)
let expertOutput = Vector.fromArray([0.2, 0.8])

for col in (0..<inputMatrix.col) {
  let input = inputMatrix.colVector(col)
  let (gateOutput, weightGradient) = gateNetwork.getValueAndWeightGradient(input, expertOutput: expertOutput)
  let output = gateOutput +* expertOutput
  
  let gateOutputWithWeightGradient = gateNetwork.getValue(input, withWeightDiff: weightGradient, scale: 1.0)
  let outputWithWeightGradient = gateOutputWithWeightGradient +* expertOutput
  
  let diff = outputWithWeightGradient - output
  
  print("input: \(input.transpose()), output: \(output)")
  print("  with gradient: \(outputWithWeightGradient), diff: \(diff)")
  
  gateNetwork.addWeight(weightGradient)
  let newGateOutput = gateNetwork.getValue(input)
  let newOutput = newGateOutput +* expertOutput
  let newDiff = newOutput - output
  print("  new output: \(newOutput), diff: \(newDiff)")
}

// ValueHME

print("----------")
print("ValueHME")
print("----------")

let valueHME = ValueHME.create(3, outputMin: -1.0, outputMax: 1.0, structure: [10, 10])

for col in (0..<inputMatrix.col) {
  let input = inputMatrix.colVector(col)
  let (output, weightGradient) = valueHME.getValueAndWeightGradient(input)
  let outputWithWeightGradient = valueHME.getValue(input, withWeightDiff: weightGradient, scale: 1.0)
  var diff = outputWithWeightGradient - output
  
  var scale = 1.0
  var upperBound: Double! = nil
  var lowerBound: Double! = nil
  var last = 100
  for i in (0..<100) {
    if (diff < 0.0) || (1.1 < diff) {
      upperBound =  scale
      scale = (lowerBound == nil) ? scale / 2.0 : (upperBound + lowerBound) / 2.0
    } else if diff < 0.9 {
      lowerBound = scale
      scale = (upperBound == nil) ? scale * 2.0 : (upperBound + lowerBound) / 2.0
    } else {
      last = i
      break
    }
    
    let outputWithScaledWeightGradient = valueHME.getValue(input, withWeightDiff: weightGradient, scale: scale)
    diff = outputWithScaledWeightGradient - output
  }
  
  let outputWith01Scaled = valueHME.getValue(input, withWeightDiff: weightGradient, scale: 0.1 * scale)
  let diffWith01Scaled = outputWith01Scaled - output
  
  print("input: \(input.transpose()), output: \(output)")
  print("  scale: \(scale), iterations: \(last)")
  print("  diff (scaled): \(diff), diff (0.1*scaled): \(diffWith01Scaled)")
  
  let weightDiff = weightGradient * 0.1 * scale
  valueHME.addWeight(weightDiff)
  let newOutput = valueHME.getValue(input)
  let newDiff = newOutput - output
  print("  new output: \(newOutput), diff: \(newDiff)")
}

実行例は、以下:

----------
ValueNN
----------
input: [1.0, 1.0, 1.0], output: -0.183942077296883
  scale: 0.1875, iterations: 4
  diff (scaled): 1.01724963088052, diff (0.1*scaled): 0.105130155911477
  new output: -0.0788119213854061, diff: 0.105130155911477
input: [1.0, 1.0, 0.0], output: -0.0727086141640791
  scale: 0.25, iterations: 2
  diff (scaled): 1.09166909286381, diff (0.1*scaled): 0.129576278870883
  new output: 0.0568676647068041, diff: 0.129576278870883
input: [1.0, 0.0, 1.0], output: -0.383684591887409
  scale: 0.25, iterations: 2
  diff (scaled): 1.07005618759768, diff (0.1*scaled): 0.115689382429312
  new output: -0.267995209458097, diff: 0.115689382429312
input: [0.0, 1.0, 1.0], output: 0.501642422844154
  scale: 1.25, iterations: 3
  diff (scaled): 1.03458420400201, diff (0.1*scaled): 0.463696492593989
  new output: 0.965338915438143, diff: 0.463696492593989
input: [1.0, 0.0, 0.0], output: 0.0147913651058598
  scale: 0.5, iterations: 1
  diff (scaled): 1.09849345190749, diff (0.1*scaled): 0.224957607158544
  new output: 0.239748972264404, diff: 0.224957607158544
input: [0.0, 1.0, 0.0], output: 1.00349356627074
  scale: 18.0, iterations: 8
  diff (scaled): 1.04838464458158, diff (0.1*scaled): 0.0657863751854824
  new output: 1.06927994145622, diff: 0.0657863751854824
input: [0.0, 0.0, 1.0], output: 1.00445024397176
  scale: 18.0, iterations: 8
  diff (scaled): 1.02882994407428, diff (0.1*scaled): 0.070282826622567
  new output: 1.07473307059433, diff: 0.070282826622567
input: [0.0, 0.0, 0.0], output: 1.04718514605304
  scale: 20.0, iterations: 7
  diff (scaled): 0.965905785136957, diff (0.1*scaled): 0.0690312508752593
  new output: 1.1162163969283, diff: 0.0690312508752593
----------
GateNetwork
----------
input: [1.0, 1.0, 1.0], output: 0.5
  with gradient: 0.626569701575002, diff: 0.126569701575002
  new output: 0.626569701575002, diff: 0.126569701575002
input: [1.0, 1.0, 0.0], output: 0.587393783735477
  with gradient: 0.627638891939002, diff: 0.0402451082035251
  new output: 0.627638891939002, diff: 0.0402451082035251
input: [1.0, 0.0, 1.0], output: 0.60807465845349
  with gradient: 0.637937581551879, diff: 0.0298629230983883
  new output: 0.637937581551879, diff: 0.0298629230983883
input: [0.0, 1.0, 1.0], output: 0.623373727874239
  with gradient: 0.645955025290896, diff: 0.022581297416657
  new output: 0.645955025290896, diff: 0.022581297416657
input: [1.0, 0.0, 0.0], output: 0.583841706173601
  with gradient: 0.605472969776552, diff: 0.0216312636029506
  new output: 0.605472969776552, diff: 0.0216312636029506
input: [0.0, 1.0, 0.0], output: 0.580303255512027
  with gradient: 0.602887409981527, diff: 0.0225841544694998
  new output: 0.602887409981527, diff: 0.0225841544694998
input: [0.0, 0.0, 1.0], output: 0.575481724278274
  with gradient: 0.59937319511023, diff: 0.0238914708319556
  new output: 0.59937319511023, diff: 0.0238914708319556
input: [0.0, 0.0, 0.0], output: 0.5
  with gradient: 0.5, diff: 0.0
  new output: 0.5, diff: 0.0
----------
ValueHME
----------
input: [1.0, 1.0, 1.0], output: 0.583369090800088
  scale: 12.0, iterations: 5
  diff (scaled): 0.955143642897197, diff (0.1*scaled): 0.55711728368868
  new output: 1.14048637448877, diff: 0.55711728368868
input: [1.0, 1.0, 0.0], output: 1.17912976992144
  scale: 10.0, iterations: 6
  diff (scaled): 0.958244431157407, diff (0.1*scaled): 0.218163118461483
  new output: 1.39729288838292, diff: 0.218163118461483
input: [1.0, 0.0, 1.0], output: 1.26053893688435
  scale: 7.0, iterations: 5
  diff (scaled): 1.00825100197074, diff (0.1*scaled): 0.100641423159278
  new output: 1.36118036004363, diff: 0.100641423159278
input: [0.0, 1.0, 1.0], output: 1.25521721395406
  scale: 7.0, iterations: 5
  diff (scaled): 0.98711439450816, diff (0.1*scaled): 0.0774174523384112
  new output: 1.33263466629247, diff: 0.0774174523384112
input: [1.0, 0.0, 0.0], output: 1.37228160440216
  scale: 24.0, iterations: 6
  diff (scaled): 0.926736515167874, diff (0.1*scaled): 0.151453564687681
  new output: 1.52373516908985, diff: 0.151453564687681
input: [0.0, 1.0, 0.0], output: 1.42285096816873
  scale: 10.0, iterations: 6
  diff (scaled): 0.995109500116182, diff (0.1*scaled): 0.0862919554937307
  new output: 1.50914292366246, diff: 0.0862919554937307
input: [0.0, 0.0, 1.0], output: 1.33805920315436
  scale: 7.0, iterations: 5
  diff (scaled): 1.0092130994799, diff (0.1*scaled): 0.0818008367414018
  new output: 1.41986003989576, diff: 0.0818008367414018
input: [0.0, 0.0, 0.0], output: 1.33449511265373
  scale: 16.0, iterations: 4
  diff (scaled): 0.947467726275787, diff (0.1*scaled): 0.0741106840380314
  new output: 1.40860579669177, diff: 0.0741106840380314

これでValueNetworkの実装はOK、と言いたいところなんだけど、このままだとデータの保存や復帰が出来ないので、ちょっと困る。
なので、データの保存・復帰が出来るように修正していきたい。

今日はここまで!

強化学習用のニューラルネットワークをSwiftで書いてみた。(その4)

昨日は強化学習用のニューラルネットワークの実装を行った。

今日はHMEの実装を行うために、同様にHMEの計算を行列で表現していく。

強化学習用のHMEの計算

ここでは、HMEへの入力を  \boldsymbol{x} \in \mathbb{R}^{n}、出力を  y \in \mathbb{R} とする。
また、エキスパートネットワーク  i の出力を  y_i \in \mathbb{R}、パラメータを  \boldsymbol{\theta}_i とし、それに対応するゲートネットワークの出力を  g_i \in [0, 1]、重みを  \boldsymbol{w}_i \in \mathbb{R}^{n} とする。

行列で表現しない計算

ニューラルネットワークのときと同様に、まずは行列で表現しない計算から。

  1. エキスパートネットワークの出力  y_i とその出力のパラメータに関する勾配  \frac{\partial y_i}{\partial \boldsymbol{\theta}_i} を求める。
  2. ゲートネットワークの出力  g_i とHMEの出力の重みに関する勾配  \frac{\partial y}{\partial \boldsymbol{w}_i} を求める:
    1.  s_i = \boldsymbol{w}_i^{\mathrm{T}} \boldsymbol{x}
    2.  g_i = \frac{\mathrm{exp}(s_i)}{\sum_j \mathrm{exp}(s_j)}
    3.  \frac{\partial y}{\partial \boldsymbol{w}_i} = \sum_j y_j \cdot \frac{\partial g_j}{\partial s_i} \cdot \boldsymbol{x}
      ただし、 i = j ならば  \frac{\partial g_j}{\partial s_i} = g_j (1 - g_j) i \neq j ならば  \frac{\partial g_j}{\partial s_i} = - g_j^{2}
  3. HMEの出力  y を求める:
    1.  y = \sum_i g_i y_i
  4. HMEの出力のエキスパートネットワークのパラメータに関する勾配  \frac{\partial y}{\partial \boldsymbol{\theta}_i} を求める:
    1.  \frac{\partial y}{\partial \boldsymbol{\theta}_i} = g_i \frac{\partial y_i}{\partial \boldsymbol{\theta}_i}

行列で表現した計算

まず、エキスパートネットワークの各出力をまとめて、ベクトル  \boldsymbol{y} \in \mathbb{R}^{m}(ただし、 m はエキスパートネットワークの数)で表現することにする。

そして、ゲートネットワークの重みを  W \in \mathbb{R}^{m \times n} で次のように表すことにする:

 {
W = \left( \begin{array}{c}
\boldsymbol{w}_1 {}^{\mathrm{T}} \\
\vdots \\
\boldsymbol{w}_m {}^{\mathrm{T}}
\end{array} \right)
}

さらに、次のような行列  G \in \mathbb{R}^{m \times m} を考える:

 {
\begin{align}
G &= \left( \begin{array}{ccc}
\frac{\partial g_1}{\partial s_1} & \cdots & \frac{\partial g_m}{\partial s_1} \\
\vdots & \ddots & \vdots \\
\frac{\partial g_1}{\partial s_m} & \cdots & \frac{\partial g_m}{\partial s_m}
\end{array} \right) \\
&= \left( \begin{array}{ccc}
g_1 (1 - g_1) & \cdots & - g_m^2 \\
\vdots & \ddots & \vdots \\
- g_1^2 & \cdots & g_m (1 - g_m)
\end{array} \right) \\
&= (\boldsymbol{g} \otimes \boldsymbol{1}) \odot (I - \boldsymbol{g} \otimes \boldsymbol{1})
\end{align}
}

ただし、 I \in \mathbb{R}^{m \times m}単位行列 \boldsymbol{1} \in \mathbb{R}^{m} はすべての要素が1のベクトル、 \otimes はベクトルの直積(外積)、 \odotアダマール積(要素ごとの積)。

すると、上の計算は次のように書き直すことが出来る:

  1. エキスパートネットワークの出力  \boldsymbol{y} とその出力のパラメータに関する勾配  \frac{\partial y_i}{\partial \boldsymbol{\theta}_i} を求める。
  2. ゲートネットワークの出力  \boldsymbol{g} とHMEの出力の重みに関する勾配  \frac{\partial y}{\partial W} を求める:
    1.  \boldsymbol{s} = W \boldsymbol{x}
    2.  \boldsymbol{t} = \mathrm{exp}(\boldsymbol{s})
    3.  \boldsymbol{g} = \frac{1}{\|\boldsymbol{t}\|_1} \boldsymbol{t} (※L1ノルムで正規化)
    4.  \frac{\partial y}{\partial W} = (G \boldsymbol{y}) \otimes \boldsymbol{x}
  3. HMEの出力  y を求める:
    1.  y = \boldsymbol{g}^{\mathrm{T}} \boldsymbol{y}
  4. HMEの出力のエキスパートネットワークのパラメータに関する勾配  \frac{\partial y}{\partial \boldsymbol{\theta}_i} を求める:
    1.  \frac{\partial y}{\partial \boldsymbol{\theta}_i} = g_i \frac{\partial y_i}{\partial \boldsymbol{\theta}_i}

かなりトリッキーな感じもするけど、地道に追えば、元の計算と同等になっていることが分かると思う。

なお、途中、 \boldsymbol{t} をL1ノルムで正規化しているけど、これは指数関数の値域が  (0, \infty) であることから、 \| \boldsymbol{t} \|_1 = \sum_i \left|t_i\right| = \sum_i t_i = \sum_i \mathrm{exp}(s_i) となることを利用している。

あと、実際に実装する場合には、 \boldsymbol{s} の一番大きい値を見つけて、その値を  \boldsymbol{s} のすべての要素から一律に引く必要がある。
というのも、指数関数は急激に大きくなるので、下手するとNaNになってしまって計算が破綻することがあるから。
ソフトマックス関数は各要素から定数を引いても値は変わらないという性質があるので、一番大きい値を一律に引いておくことで、指数関数の値が  (0, 1] に収まるようになる。
強化学習とニューラルネットワークを組合せてみた。(その12) - いものやま。も参照)

今日はここまで!