いものやま。

雑多な知識の寄せ集め

強化学習用のニューラルネットワークを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はエキスパートネットワークに依存しないように実装していたのに、これで依存関係が生じるようになってしまった・・・
SwiftObjective-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

今日はここまで!