Julia でデータのセーブとロード

JuliaLang Advent Calendar 2015 の 18 日目の記事です。

概要

他のプログラムとファイルを介して情報をやりとりするお話です。 他のプログラムというのは必ずしもJulia で書かれているとは限らないし、はたまた自分自身のこともあります。 他のプログラムの計算結果をデータ処理したり、逆にこれらに与える入力ファイルを作る場合や、使える計算時間が限られている環境でデータをセーブ・ロードする場合などです。

テキストファイ

open 関数でファイルを開いた後、print, println 関数でファイルに書き込みをしたり、readlinereadlines, eachline 関数などを使ってファイル内容から文字列、文字列の配列、文字列の配列のイテレータなどを取得できます。 テキストファイルは非常に汎用性が高い形式です。人間が直接読むこともできるし、GrepAWK, Gnuplot などのUNIX ツールと組み合わせることもできます。 一方で読み込みには文字列操作が必要で、ファイルサイズも大きくなるため、バイナリ形式と比べると性能が落ちます。

xs, ys = rand(100)
open("text.dat", "w") do io
  println(io, "# x y")
  for (x,y) in zip(xs, ys)
    println(io, x, " ", y)
  end
end

xs2, ys2 = zeros(0), zeros(0)
open("text.dat", "w") do io
  for line in eachline(io)
    if ismatch(r"^\s*($|#)", line)
      continue
    end
    words = split(line)
    push!(xs2, float(words[1]))
    push!(ys2, float(words[2]))
  end
end
@assert xs == xs2
@assert ys == ys2

バイナリファイル

テキストファイルと同様、open 関数でファイルを開いた後、 write 関数および read 関数で書き出し、読み込みを行います。 文字列を介さずに直接データを扱えるので、テキストファイルよりもファイルサイズ・処理速度両面で有利です。一方で、Julia 以外で扱えないことや、Julia でも書き込んだ順番や型を覚えておく必要があるという欠点があります。

xs, ys = rand(100), rand(100)
open("hoge.dat", "w") do io
  write(io, xs, ys)
end

xs2 = zeros(0)
ys2 = zeros(100)
io = open("hoge.dat")
xs2 = read(io, Float64, 100)
read!(io, ys2)

@assert xs == xs2
@assert ys == ys2

Base.serialize()Base.deserialize()

Julia の標準ライブラリとして、serialize(stream,x)deserialize(stream)という2つの関数が用意されています。 基本的には write, read と同じですが、型タグによるメタ情報を付与して書き込むため、任意の値の読み書きができるという利点があります。 メタ情報がある分だけオーバーヘッドがありますが、大抵気にするほどではないでしょう。 読み書きは先頭から順番に行うしかありませんが、辞書をそのまま読み書きできるので、必要なデータを辞書に入れてから読み書きすると楽です。

なお、無名関数以外の関数や型などは名前しか保持しないので、読み出し前に定義が必要ですし、その定義が変わると結果も変わります。 また、メタ情報や内部表現が変わることがあるなど、バージョン間の互換性が保証されていません。長時間保存する場合は後述のJLD.jl をつかうと良いでしょう。

type A
  n :: Int64
  x :: Float64
end
a = A(42, 3.14)
open(io -> serialize(io, a), "hoge.dat", "w")

workspace()

type A
  x :: Float64
  n :: Int64
end
a = open(deserialize, "hoge.dat")
@assert a.x == reinterprete(Float64, 42)
@assert a.n == reinterprete(Int64, 3.14)

HDF5.jl

HDF5 (Hierarchical Data Format version 5) は主に科学技術コミュニティで使われるバイナリフォーマットです。 複数のデータを、UNIX のディレクトリ・ファイル構造のような階層構造でひとまとめにしたフォーマットです。 おおざっぱには文字列をキーにした辞書が入れ子になっていると考えるのが良いでしょう。 辞書オブジェクトをそのまま (de)serialize するのと比べると、Julia 以外の言語でも使えるという利点があります。実際に、C/C++Fortran はもちろん、Python やR, Go などの言語からも利用できます。 整数((U)Int(8|16|32|64))と浮動小数点数(Float(32|64))、文字列((ASCII|UTF8)String) およびこれらの配列が格納できます。 それ以外の値を格納するためにはファイルに情報(attribution)を追加する必要がありますが、次に示すJLD.jl はそれを自動で行ってくれます。

using HDF5
h = h5open("hoge.h5", "w")
h["n"] = 42
h["x"] = 3.14
close(h)

h = h5open("hoge.h5", "r+") # read / write
names(h) # => ["n", "x"]
dump(h) # show contents

# read
@assert read(h, "n") == 42
@assert read(h, "x") = 3.14

# append 
h["arr"] = [32, 64]
h["str"] = "julia"

# delete
o_delete(h, "n")

names(h) # => ["arr", "str", "x"]

close(h)

Tips

Pkg.add("HDF5") をすると、自動でHDF5 をインストールしますが、スパコンなどパッケージ管理ツールが使えない場合にはインストールに失敗します。 そういった場合には、あらかじめHDF5 を(自分でソースからビルドするか、管理者に頼むなどして)インストールして、ライブラリのあるディレクトリをLibdl.DL_LOAD_PATH 配列に追加してからPkg.add("HDF5") をしましょう。 ~/.juliarc.jl

for path in split(ENV["LD_LIBRARY_PATH"], ":")
  push!(Libdl.DL_LOAD_PATH, path)
end

とかやっておくと便利です。

JLD.jl

JLD はHDF5 フォーマットのJulia 方言です。HDF5.jl で、Julia の任意のオブジェクトを取り扱えるようにしたものです。 基本的にHDF5.jl と使い方は同じです。 外部パッケージで定義された型の値を保存する場合、addrequire(jldfile, package) とすることで、読み出し時に自動でimport するようになります。 なお、ユーザ定義型を保存するとき、型定義も同時に保存してくれるため、読み出し時に型を定義していない場合、自動で型定義を復元してくれるという謎機能もついています。 一方で総称関数(名前付き関数)は保存できません*1。無名関数はできます。 最後に、(de)serialize よりフォーマットが安定しているという利点があります。

その他

tsv やcsv などはDataFrames.jl を使って読み書きをすると便利です。 SQLite.jl を使うとSQLite データベースに接続したり、クエリ文字列を作ってデータベース操作をすることができます。 ただDataFrame に変換してそちらを使ったほうが楽でしょう。(参考:14日目の記事)。

*1:する必要はあまりないと思いますが