Clojureのデータ構造をシリアライズしてファイルに書き出す

More than 3 years have passed since last update.

ニューラルネットのモデルをディスクに保存したいという、モチベーションでした。

ただ、少し状況が特殊なのは、ニューラルネットのモデルパラメータをプリミティブのfloat配列で表現していることです。

最初に一般的なデータ構造の保存の方法を見なおした後に、float配列等のプリミティブに対応したデータ構造を保存する方法を見ていきましょう。


データ構造を保存

一般的な状況であれば、次のような方法で読み書きができます。


save

(defn save [obj path] (spit path (pr-str obj)))

(save {:a 123 :b "qwerty" :c [1 2 3] :d '("x" "y" "z")} "tmp.txt")


load

(defn load [path] (clojure.edn/read-string (slurp path)))

(load "tmp.txt") ;=> {:a 123, :b "qwerty", :c [1 2 3], :d ("x" "y" "z")}

シンプルでいいですね。

しかし、次のようにfloat-arrayなどのプリミティブには対応できません。


float-array

(save (float-array (range 10)) "tmp2.txt")

(load "tmp2.txt") ;=> ERROR

ここで、tmp2.txtの中身を見てみると、#object["[F" 0x2457b2d4 "[F@2457b2d4"]といった内容が記載されていました、見えてはいけなさそうな箇所が剥き出しですね。

これではread-stringが使えないわけです。


シリアライズしてデータ構造を保存

さて、ここから本題ですが、先に簡潔に流れを説明すると、1.シリアライズして、2.ファイルに書き出す、だけです。

この投稿が良い方法を示しています、ptaoussanis/nippyを使うと簡単そうですね。

ちなみに、この方法を使う似たような動機だと、ゲームのセーブデータを作成したい場合や、通信越しにオブジェクトのやり取りをしたい場合などがあるようです。

また、シリアライズ/デシリアライズ自体については解説されてる資料が多々ありますので、この記事では省きます。

最初に、依存関係を示します。

[com.taoensso/nippy "2.11.1"]project.cljに追加してください。

そして、requireします。


dependencies

(ns my-ns (:require [taoensso.nippy :as nippy]))


次のようにすることでシリアライズして保存できます。


シリアライズして保存

(defn save [obj target-path]

(with-open [w (clojure.java.io/output-stream target-path)]
(freeze-to-out! (java.io.DataOutputStream. w) obj)))

もしシリアライズのみを行いたい場合であれば、freezeが使えます。

また、次のようにすることで読み込みます。


読み込み

(defn load [target-path]

(with-open [w (clojure.java.io/input-stream target-path)]
(thaw-from-in! (java.io.DataInputStream. w))))

こちらも単にデシリアライズしたい場合にはthawが使えます。

では、定義した関数を試してみましょう。


シリアライズの後に保存して読み込み

(save (float-array (range 10)) "tmp.txt")

(load "tmp.txt") ;=> #object["[F" 0x428cf9b8 "[F@428cf9b8"]
(seq (load "tmp.txt")) ;=> (0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0)

バッチリですね!