HaskellでTensorFlow、を対話的に試してみる




はじめに
=======

[TensorFlowのHaskell用バインディング](https://github.com/tensorflow/haskell)がリリースされています。
本格的に使用する場合はコンパイル方式を用いると思いますが、本記事では、環境に慣れるために対話的に使用する方法について簡単に紹介します。

以下では、Linux(ubuntu ~~16.04LTS~~ 18.04LTS)を用いた例を紹介します。 なお、Dockerはインストール済みであることを前提としています。
本記事は、~~2017年8月時点~~ 2018年8月時点の内容です。TensorFlowのHaskell用バインディングは開発中のため、今後、使い勝手が変わると思いますので注意してください。

以下は参考資料です。
* [Haskell bindings for TensorFlow](https://github.com/tensorflow/haskell)
* [Starting out with Haskell Tensor Flow](https://mmhaskell.com/blog/2017/8/14/starting-out-with-haskell-tensor-flow)


環境の準備
==========

### 1. stackコマンドのインストール

stackをインストールします。 既にインストールされている場合はこの手順は不要です。
[こちらの公式情報](https://docs.haskellstack.org/en/stable/README/#how-to-install)に従って、インストールしていきます。

以下のコマンドにより、グローバル環境(/usr/bin)にstackコマンドがインストールされます。

```
$ curl -sSL https://get.haskellstack.org/ | sh
```

インストールできたか確認してみましょう。

```
$ stack --version
Version 1.7.1, ...
```

インストールできています。


### 2. TensorFlow/haskellバインディングのインストール

つぎに、TensorFlowのHaskellバインディングをインストールしていきます。
[こちらの公式情報](https://github.com/tensorflow/haskell#build-with-docker-on-linux)に従って、インストールしていきます。

まず以下のように、githubからソースを取得(git clone)してから、dockerイメージを作成（docker build）します。

```
$ git clone --recursive https://github.com/tensorflow/haskell.git tensorflow-haskell
$ cd tensorflow-haskell
$ IMAGE_NAME=tensorflow/haskell:v0
$ docker build -t $IMAGE_NAME docker
```

これで、dockerイメージが作成されました。 確認してみましょう。

```
$ docker images
REPOSITORY              TAG                 IMAGE ID            CREATED             SIZE
tensorflow/haskell      v0                  af52250c4a2b        xx minutes ago       1.46GB
tensorflow/tensorflow   1.9.0               caab7ec02690        xx weeks ago         1.25GB
```

dockerイメージ（tensorflow/haskell）を用意できました。（あわせて、tensorflow/tensorflowのイメージも取得されています。）

次に、dockerイメージから生成するdockerコンテナ内で、stackの環境を構築（stack setup）します。

```
$ stack --docker --docker-image=$IMAGE_NAME setup
```

上記のコマンドで、dockerコンテナ内からも利用できる~/.stack以下に、GHC（Haskellコンパイラ）がインストールされました。


次に、TensorFlowのHaskellバインディングのソースをビルドします。
以下のコマンドで、カレントディレクトリ（tensorflow-haskell）以下のHaskellソース群がビルドされ、ローカルのディレクトリ下（./.stack-work）にインストールされます。 stackのtestコマンドを使ってビルドしています。
（環境により数十分かかりますが、最初に一度行うのみです。）

```
$ stack --docker --docker-image=$IMAGE_NAME test
```

以上で、TensorFlowのHaskellバインディング環境のインストールは完了です。



TensorFlowを対話的に試してみる
================================================

### 1. stack --docker環境で bashを対話的に起動する

ここからが本番です。
通常の使い方であれば、`stack build`コマンドでソースをビルドしてから、`stack exec`コマンドで生成されたバイナリを実行する手順になると思います。
ですが、ここでは、試行錯誤的な作業を行いやすいように、対話環境を起動してみます。

具体的には、以下のコマンドで、まず、dockerコンテナ内でbashを起動します。

```
$ stack --docker --docker-image=$IMAGE_NAME exec -- bash
```

これで、ほどよいstack環境が整備されたdockerコンテナ内で、対話的に作業を行えます。
以下のコマンドなどで、環境の確認をしてみましょう。

```
stack$ pwd

stack$ which ghc

stack$ ghc --version

```

さらに、TensorFlowのHaskellバインディングを、パッケージとして認識できているかを、`ghc-pkg`コマンドで確認してみましょう。

```
stack$ ghc-pkg list
/home/my/.stack/programs/x86_64-linux-dkd1ce2ff9c9560b648268df668d177711/ghc-8.2.2/lib/ghc-8.2.2/package.conf.d
    Cabal-2.0.1.0
    array-0.5.2.0
    base-4.10.1.0
    :
/home/my/.stack/snapshots/x86_64-linux-dkd1ce2ff9c9560b648268df668d177711/lts-11.9/8.2.2/pkgdb
    Cabal-2.0.1.1
    HTTP-4000.3.11
    HUnit-1.6.0.0
    QuickCheck-2.10.1
    ansi-terminal-0.8.0.4
    :
/home/my/tensorflow-haskell/.stack-work/install/x86_64-linux-dkd1ce2ff9c9560b648268df668d177711/lts-11.9/8.2.2/pkgdb
    haskell-src-exts-1.19.1
    proto-lens-protobuf-types-0.2.2.0
    proto-lens-protoc-0.2.2.3
    snappy-0.2.0.2
    snappy-framing-0.1.1
    tensorflow-0.2.0.0
    tensorflow-core-ops-0.2.0.0
    tensorflow-logging-0.2.0.0
    tensorflow-mnist-0.1.0.0
    :
```

このように、ローカルディレクトリ以下の`./.stack-work/install/...`以下に、TensorFlow/Haskellバインディングのパッケージが正しく入っています。



### 2. dockerコンテナ内で、ghci を対話的に起動する

さて、ようやく本題です。

dockerコンテナ内で、GHCの対話環境（REPL）である、ghciを起動しましょう。

```
stack$ ghci
GHCi, version 8.2.2: http://www.haskell.org/ghc/  :? for help
Prelude> 
```

正しく動くか確認しましょう。

```
Prelude> 1+2
3
```

OKですね。
では、TensorFlowを試していきます。

そのまえに、importによってghciのプロンプトが長くなって見づらくなるのを避けるために、プロンプトの設定を行います（必須ではありません。）

```
Prelude> :set prompt  "ghci> "
ghci> :set prompt-cont "ghci| "
```


### 3. ghci 上で、TensorFlowを対話的に試す


いよいよ、TensorFlowのコードを試します。
以下では、 [こちら](https://mmhaskell.com/blog/2017/8/14/starting-out-with-haskell-tensor-flow)のコード例を試してみます。


このコード例では、リスト記法を便利にするGHCの言語拡張（OverloadedLists）を設定します。

```
gchi> :set -XOverloadedLists
```

次に、必要なパッケージをimportします。

```
ghci> import Data.Vector (Vector)
ghci> import TensorFlow.Ops (constant, add)
ghci> import TensorFlow.Session (runSession, run)
```

続いて、TensorFlowでグラフ（ネットワーク）を定義します。ここでは単純な加算グラフを定義してみます。

```
ghci> :{
ghci| runSimple :: IO (Vector Float)
ghci| runSimple = runSession $ do
ghci|   let node1 = constant [1] [3 :: Float]
ghci|   let node2 = constant [1] [4 :: Float]
ghci|   let additionNode = node1 `add` node2
ghci|   run additionNode
ghci| :}
```

最後に、TensorFlowのグラフ（ネットワーク）の計算を実行します。

```
ghci> runSimple
[7.0]
```

素直に実行できました！
このように、TensorFlowを対話的、試行錯誤的に試せます。

ghciを終了するには以下のようにします。

```
ghci> :q
Leaving GHCi.
```

dockerコンテナ内のbashを終了するには、`exit`を実行しましょう。

なお、TensorFlowが整備されたdockerコンテナ内のbashを再び起動するには、以下のコマンドから始めればOKです。

```
$ IMAGE_NAME=tensorflow/haskell:v0
$ stack --docker --docker-image=$IMAGE_NAME exec -- bash
```

では、Haskell & TensorFlowを、気軽にenjoy!



補足
====

### runghcも使えます

dockerコンテナ内でbashを立ち上げられるので、ghciだけでなく、runghcなども使用できます。

例えば、以下のようにファイルを準備して、

```
$ cat test1.hs

{-# LANGUAGE OverloadedLists #-}

import Data.Vector (Vector)
import TensorFlow.Ops (constant, add)
import TensorFlow.Session (runSession, run)

runSimple :: IO (Vector Float)
runSimple = runSession $ do
    let node1 = constant [1] [3 :: Float]
    let node2 = constant [1] [4 :: Float]
    let additionNode = node1 `add` node2
    run additionNode

main :: IO ()
main = do
    result <- runSimple
    print result
```

以下のように、runghcで実行することができます。

```
$ runghc test1.hs
[7.0]
```

このように、runghcやghciを使って、試行錯誤的に手軽にTensorFlow/Haskellを試せます。



### stackとdockerについて少し

うまく作られているstackとdockerの連携について、関係を少しだけ補足します。

* docker buildでビルドしたdockerイメージは、stack --dockerコマンドにより使い回される
* そのdockerイメージをもとに、stack --dockerコマンドにより生成されるdockerコンテナは、毎回使い捨てられる
* 但し、それらのdockerコンテナはローカルディレクトリやホームディレクトリをマウントするので、ファイルの更新内容は以降のdockerコンテナに引き継がれる
* また、ホストOSとdockerコンテナ間で、ファイルは読み書き可能に共有される

よく出来てますね。


参考リンク
=========

参考となるリンクをいくつか挙げます。

* [ghciの使い方](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/ghci.html#using-ghci) / [(日本語版)](https://ghcguide.haskell.jp/users_guide/ghci.html)
* [runghcの使い方](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/runghc.html#using-runghc) / [(日本語版)](https://ghcguide.haskell.jp/users_guide/runghc.html)
* [stackの使い方](https://docs.haskellstack.org/en/stable/GUIDE/)
* [dockerコンテナの使い方](https://docs.docker.com/get-started/part2/)
* [dockerのインストール方法](https://docs.docker.com/engine/installation/linux/docker-ce/ubuntu/)


以上です。