お布団宇宙ねこ

にゃーん

optparse-applicativeを使ってCUIツールを作る

Haskellで簡単でもいいから何かツール作りたいなあと思ってシュッと作れそうなCUIツールを作ったので、作るにあたって利用したライブラリoptparse-applicativeの紹介をします。

はじめに

今回CUIツールを作る上で基本となるオプションパーサの部分にライブラリを使いました(というよりそこにしか使いませんでした)。オプションパーサのライブラリはこちらを見つつ、その後optparse-applicativeoptparse-simpleoptparse-genericに絞って、最終的に一番ドキュメントの量が多くコードの読みやすかったoptparse-applicativeを使っています。optparse-genericもよかったのですがサブコマンドの書き方がよくわからなくて断念してしまいました。

オプションパーサのライブラリを使ってますが、実はオプションは一つも実装していないのでどちらかと言うとサブコマンドを実装したい人向けの記事になります。

実装例

コマンド部分は以下のように簡単に作れます。

module Main where

import Options.Applicative
import Data.Semigroup ((<>))

data Command
  = Hello name
  | Bye name msg
  deriving (Show)

parseHello :: Parser Command
parseHello = Hello <$> argument str (metavar "[NAME]")

parseBye :: Parser Command
parseBye = Bye
  <$> argument str (metavar "[NAME]")
  <*> argument str (metavar "[MESSAGE]")

withInfo :: Parser a -> String -> ParserInfo a
withInfo opts desc = info (helper <*> opts) $ progDesc desc

parseCommand :: Parser Command
parseCommand = subparser $
  command "hello" (parseHello `withInfo` "Say Hello") <>
  command "bye" (parseBye `withInfo` "Say Bye")

parseInfo :: ParserInfo Command
parseInfo = parseCommand `withInfo` "Greeting"

execCommand :: IO ()
execCommand = execParser parseInfo >>= run
  where run cmd = case cmd of
                    Hello n -> sayHello n
                    Bye n m -> sayBye n m

main :: IO ()
main = execCommand

まず、実装したいサブコマンドやオプションの型を定義してそれをParser型で包んだパーサ関数を定義します。ここではComannd型を定義してparseHelloparseByeを実装しています。このパーサ関数にはコマンドが受け取る引数やヘルプで表示したい情報などを設定できます。

次にそれらのパーサ関数をまとめるパーサを用意します。ここではparseCommandがそれにあたります。

あとはそれをexecParser関数に食わせることで入力された文字をいい感じにパースしてコマンドと引数に分解してくれます。

実行

デフォルトでヘルプオプションが付いてくるので、上のような実装だけでもいい感じのコマンドが作れてしまいます。

> stack runghc Main.hs -- --help
Usage: Main.hs COMMAND
  Greeting

Available options:
  -h,--help                Show this help text

Available commands:
  hello                    Say Hello
  bye                      Say Bye

> stack runghc Main.hs -- hello --help
Usage: Main.hs hello [NAME]
  Say Hello

Available options:
  -h,--help                Show this help text

まとめ

以上、optparse-applicativeを使うとお手軽にCUIツールを作れるのでした。
自分が実際に作ったものはこちらに置いてあります。

github.com

参考文献

Applicativeを理解する

前回の記事ではFunctorについて紹介しました。今回はFunctorをさらに強力にしたApplicativeという型クラスについて解説します。

適用する関数も箱に入れる

Functorでは、ある状態という箱に入った値を関数に適用するための振る舞いを定義しているのでした。

ここで適用する関数も箱に入っていた場合を考えてみましょう。当然fmapでは箱に入った関数を取り出すことはできません。

> fmap (Just (+3)) (Just 3)
<interactive>:44:7:
    Couldn't match expected type ‘Integer -> b’
    ...

そこで登場するのがApplicativeです。Appliactiveは箱に入った値を箱に入った関数に適用する方法を知っています。

Applicativeとは

ApplicativeはControl.Applicativeに定義されています。

class Functor f => Applicative f where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

http://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#Applicative

Applicativeには型制約が付いておりApplicativeのインスタンスになるためにはFunctorのインスタンスである必要があります。

今回、箱に入った値を箱に入った関数に適用するには(<*>)を使います。この関数はfmapと型シグネチャが似ていますが、第一引数の関数がアプリカティブ値に入っています。Functorと同じくこの値には箱となる型が入ります。

では、Maybeを例に(<*>)がどのような実装になっているのか見てみましょう。

instance Applicative Maybe where
  pure = Just
  Just f <*> m = fmap f m
  Nothing <*> _m = Nothing

http://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#line-656

実際に使ってみます。

> Just (+3) <*> Just 3
Just 6
> Nothing <*> Just 3
Nothing

Justに入った関数がきたときには、箱から関数を取り出してfmapを使って値に適用しています。値を関数に適用した後の箱に値を入れ直す処理はfmapを使えばFunctorが全てやってくれます。

さらに<*>は連続して組み合わせることもできるので、はじめから箱に関数が入っていなくても計算することができます。pure関数を使って関数をアプリカティブ値で包むことで実現できます。

> :t pure (+) <*> Just 100
pure (+) <*> Just 100 :: Num a => Maybe (a -> a)
> pure (+) <*> Just 100 <*> Just 3
Just 103

けれど<$>を使うことでもっと簡単に書くことができます。

> :t (+) <$> Just 100
(+) <$> Just 100 :: Num a => Maybe (a -> a)
> (+) <$> Just 100 <*> Just 3
Just 103

それと同じ処理をする便利な関数liftA2もあります。

> :m Control.Applicative
> liftA2 (+) (Just 100) (Just 3)
Just 103

Applicativeのインスタンス

Functorのインスタンスである型ならどれでもApplicativeになれます。
その中でもリストは変わった結果を返します。

> [(^2),(+100)] <*> [1..5]
[1,4,9,16,25,101,102,103,104,105]

リストアプリカティブファンクターは適用する関数が複数である可能性があるため、その全ての関数に対してそれぞれの値を適用した新しいリストを返します。この例では2*5=10通りの組み合わせのリストが返ってきます。このようにリストは複数の結果を取り得る非決定性計算を表現するのに使われます。

インスタンス定義は次のようになっています。<*>はリスト内包表記で実装されています。

instance Applicative [] where
  pure x = [x]
  fs <*> xs = [f x | f <- fs, x <- xs]

http://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#line-739

Maybeのときと同じく<*>は連続して使うことができます。

> [(^),(+)] <*> [2,10] <*> [1..5]
[2,4,8,16,32,10,100,1000,10000,100000,3,4,5,6,7,11,12,13,14,15]

おっと、さっきの組み合わせよりも大きなリストができてしまいました。
左から順を追って見てみましょう。まずは2*2=4通りの組み合わせリストが返ります。

> let a = [(^),(+)] <*> [2,10]
> :t a
a :: Integral b => [b -> b]
-- [(2^),(10^),(2+),(10+)]

そしてこの関数リストに[1..5]を適用します。組み合わせは4*5=20通りです。

> a <*> [1..5]
> :t a <*> [1..5]
a <*> [1..5] :: Integral b => [b]
-- [2,4,8,16,32,10,100,1000,10000,100000,3,4,5,6,7,11,12,13,14,15]

参考文献

Functorを理解する

この記事ではFunctorというHaskellには欠かせない型クラスについて、概念や捉え方、どういった型がFunctorという振る舞いをできるのかを解説します。

型クラスとは

型クラスとは、値の等値性を判定したり、ある値を文字列として表現したりと特定の振る舞いを定義するインターフェイスです。その特定の振る舞いを定義した関数のことをメソッド、そのメソッドを実装した型をその型クラスのインスタンスと呼びます。

状態という箱

値を関数に適用したいときは次のように書けます。

> (+100) 5
105

Haskellではある状態に値が入っていることがよくあります。例えばMaybe型は値がJust 5に入っていたりするように。このある状態は値を入れる「箱」と考えることができます。

ここでMaybe型に入った値を関数に適用したいとします。しかし、次のように書いてもうまくいきません。

> (+100) (Just 5)
<interactive>:5:1:
    Non type-variable argument in the constraint: Num (Maybe a)
    ...

これはMaybeという箱に値が入ったままで、適用したい関数がその値の取り出し方を知らないからです。そこで活躍するのがFunctorという型クラスとfmapという関数です。

Functorとは

Functorは型クラスです。

早速ですがFunctorの型クラスの定義を見てみましょう。Data.Functorに定義されています。

class Functor f where
  fmap :: (a -> b) -> f a -> f b

https://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#Functor

fmapという関数が定義されており、この関数は「aを取りbを返す関数」とf aを取り、f bを返します。ここでいうfはファンクター値と呼ばれFunctorのインスタンスとなる型が入ります。

そして、MaybeはFunctorのインスタンスなので当然fmapを実装しています。このときのfmapの型シグネチャは次のようになります。

fmap :: (a -> b) -> Maybe a -> Maybe b

ちょうどファンクター値fがインスタンスであるMaybeに置き換わった感じですね。 ついでにインスタンス定義も見てみましょう。

instance Functor Maybe where
  fmap f Nothing = Nothing
  fmap f (Just a) = Just (f a)

https://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#line-652

fmapにはパターンマッチが使われていて箱に入った値がJustかNothingで挙動が違います。Justの場合は箱から値を取り出して関数fに適用してから再度Justに入れ直して返します。一方でNothingの場合は箱には何もせずにそのままNothingのみを返します。

それでは実際に使ってみましょう。

> fmap (+100) (Just 5)
Just 105
> fmap (+100) Nothing
Nothing

うまくいきました。Nothingの場合は箱に値も何も入っていないことと同じなので、何もないものに関数を適用しても何も変化がないのは当然と言えるでしょう。


fmapは中置演算子( <$> )としても提供されています。

> (+100) <$> (Just 5)
Just 105

Functorのインスタンスたち

Maybe以外にもFunctorのインスタンスになれる型があります。

リスト

リストもFunctorです。リストという箱には複数の値が入りますが、リストのfmapは箱から値を1つずつ取り出して関数に適用します。

> fmap (+100) [1..5]
[101,102,103,104,105]

この動作はmap関数と同じものなので、インスタンス定義も次のようになっています。

instance Functor [] where
  fmap = map

https://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#line-734

Either

EitherはMaybeと似ていますが、MaybeのNothingと違い失敗を表したいときにも値を付けられます。

data Either a b = Left a | Right b

Maybeファンクターと同じく箱に入った値には何もせずそのまま返します。

> fmap (+100) (Right 5)
Right 105
> fmap (+100) (Left "error...")
Left "error..."

Eitherは型変数を2つ取るのでインスタンス定義が他のものと少し違います。Functorは型クラス定義から分かるように1つの型変数しか取らないので、Eitherのような型変数を複数持つ型をインスタンスにしたい場合はFunctor (Either a)のように部分適用により型変数の数を調整する必要があります。

instance Functor (Either a) where
  fmap _ (Left x) = Left x
  fmap f (Right y) = Right (f y)

https://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#line-127

この定義が正しいことはfmapの型シグネチャにファンクター値を実際に代入してみることで確かめることができます。

元のfmapの型シグネチャはこうでした。

fmap :: (a -> b) -> f a -> f b

そしてEitherファンクターのファンクター値はEither aなので、fmapの型シグネチャは次のようになります。

fmap :: (b -> c) -> Either a b -> Either a c

仮にファンクター値がEither a bだった場合は次のようになりますが、Either a b cは最早Either型でないことが分かります。

fmap :: (c -> d) -> Either a b c -> Either a b d

関数

今まで我々が何気なく使っていた関数もまたFunctorです。実際に試してみましょう。

> let a = fmap (*2) (+100)
> :t a
a :: Num b => b -> b
> a 5
210

値として渡した関数に関数を適用しようとすると、どうやら新しい関数を返すようです。これは「箱から値を取り出して関数に適用し、その値をまた箱に入れ直す」というFunctorの性質と合っているように見えます。

新しく返ってきた関数は引数を1つ取り、(+100),(*2)の順で適用して値を返します。この処理は合成関数(.)でやっていることと同じです。

instance Functor ((->) r) where
  fmap = (.)

https://hackage.haskell.org/package/base-4.9.1.0/docs/src/GHC.Base.html#line-638


この他にもいろんな型がありますが、ファンクターとは別の概念の説明が必要になるのでまたの機会に紹介します。

参考文献

Stateモナドを理解する

ということがあったので主に備忘録としてまとめました。

この記事では 『すごいHaskellたのしく学ぼう!』 (通称「すごいH本」)からコイントスをシミュレーションする関数を書く問題を参考に、Stateモナドについて学んでいこうと思います。

すごいHaskellたのしく学ぼう!

すごいHaskellたのしく学ぼう!

はじめに

下記のような3回コインを投げてその裏表を返す関数があったとします。

threeCoins :: StdGen -> (Bool, Bool, Bool)
threeCoins gen = do
  let (c1, g1) = random gen
      (c2, g2) = random g1
      (c3, g3) = random g2
  in (c1, c2, c3)

ところがStateモナドを使うことで乱数の状態をより簡単に扱うことができるようになります。

randomSt :: (RandomGen g, Random a) => State g a
randomSt = state random

threeCoins' :: State StdGen (Bool, Bool, Bool)
threeCoins' = do
  c1 <- randomSt
  c2 <- randomSt
  c3 <- randomSt
  return (c1, c2, c3)

Stateモナドとは一体どのようなものなのでしょうか。またそれは通常の関数による値の受け渡しとどう違い、どのように便利なのか。それを知るためにはまず状態を伴う関数について考える必要があります。

状態を伴う関数

System.Randomのrandom関数は、乱数ジェネレータを引数に取り、乱数と新しいジェネレータを返します。random関数が乱数ジェネレータを受け取って新しいジェネレータを返しているのは、ジェネレータの状態を上書きすることができないからです。これは純粋関数型言語の参照透過性によるものです。

random :: (RandomGen g, Random a) => g -> (a, g)

そんなrandom関数の型から、 状態付き計算 という汎用的な型を得ることができます。 状態付き計算は、状態をsとし、計算結果aと更新された状態を返す関数として下記のように表現できます。

s -> (a, s)

Stateモナド

状態付き計算をモナドとして扱えるのがStateモナドです。 Stateモナドは次のような定義となっています。

type State s a = s -> (a, s)

Stateモナドは値コンストラクタ( State )を提供していないので、Stateモナドで状態付き計算を包みたい場合はstate関数を使う必要があります。

Stateモナドの関数たち

提供されている関数は以下のようなものがあります。

  • state
  • runState
  • evalState
  • execState
  • get
  • put

state関数は状態付き計算を受け取ってStateモナドで包んで返します。

> :t state
state :: (s -> (a, s)) -> State s a
> :t state $ \x -> (25, x)
state $ \x -> (25, x) :: (Num a) => State s a

runState関数はStateモナドと状態を受け取って、Stateモナドを解いて状態付き計算を返します。state関数の逆ですね。

> :t runState
runState :: State s a -> s -> (a, s)
> runState (state $ \x -> (25, x)) "neko"
(25,"neko")

evalState・execState関数は計算結果か状態のどちらかだけを返したいときに使います。

> :t evalState
evalState :: State s a -> s -> a
> :t execState
execState :: State s a -> s -> s

get・put関数は、コイントス問題とは直接関係ないので後で説明します。

returnと>>=

returnと>>=の実装があるStateモナドインスタンス定義は次のようになっています。

instance Monad (State s) where
  return x = State $ \s -> (x, s)
  (State h) >>= f = State $ \s -> let (a, newState) = h s
                                      (State g) = f a
                                  in g newState

returnは値を受け取ってそれが結果になるような状態付き計算を返すだけです。

> :t (return "neko") :: State s String
(return "neko") :: State s String :: State s String

Stateモナドを>>=で関数を適用した場合はその結果もまたStateモナドである必要があるため、右辺の始まりは State $ \s -> ... となっています。 まずは 状態付き計算 である左辺のhと、右辺のラムダ式で引数として渡されている 状態 sを使って計算結果aと新しい状態newStateのペアを返しています。 次に左辺の関数fを計算結果aに適用すると新しい 状態付き計算 gを返します。 あとは 状態付き計算 gにnewStateを適用すれば最終的な計算結果と状態のペアを得ることができます。

これでコイントス問題をStateモナドで解くのに必要なことは説明したのでもう一度考えてみましょう。

Stateモナドを使ってコイントス問題を解く

コイントス問題に使われるrandom関数は乱数ジェネレータを受け取って乱数と新しいジェネレータを返すのでした。これをStateモナドとして扱うためにはstate関数を使って包めばよさそうです。

randomSt :: (RandomGen g, Random a) => State g a
randomSt = state random

あとはrandomSt関数を呼んでいって結果を受け取り最後にreturnで返すだけです。

threeCoins' :: State StdGen (Bool, Bool, Bool)
threeCoins' = do
  c1 <- randomSt
  c2 <- randomSt
  c3 <- randomSt
  return (c1, c2, c3)

なぜrandomSt関数に状態付き計算を明示的に渡す必要がないのかは、ラムダ式を使って書いてみると理解できると思います。

threeCoins' = randomSt >>= (\c1 -> randomSt c1 >>= (\c2 -> randomSt c2 >>= return (c1, c2, c3)))

runState関数などに適用して実際に使ってみましょう。

> runState threeCoins' $ mkStdGen 7
((True,False,True),33684305 2103410263)

本編はこれで終わりです。あとは上で説明しなかったStateモナドの関数の説明だけします。

get・put関数

コイントス問題では使いませんでしたが、get・put関数も重要な関数です。

get関数は現在の状態をそのまま計算結果としたStateモナドを返します。

> runState get [1..5]
([1,2,3,4,5],[1,2,3,4,5])

put関数は状態を受け取って、その値で状態を更新したStateモナドを返します。

> runState (do x <- get; put $ (*2) <$> x) [1..5]
((),[2,4,6,8,10])

まとめ

StateモナドはMaybeやリストモナドと比べると特殊なモナドに見えるので敬遠していましたが、Stateモナド無しで書いた関数をStateモナドを使って書き直してみるとその良さが分かるかと思います。自分は特にdo記法によるStateモナドの受け渡しをちゃんと理解していなくて、今回>>=で置き換えることをやったおかげでようやく理解ができた感じでした。

あとはStateモナドの定義が実は type State s a = s -> (a, s) じゃなくて StateT s Identity だとか、state関数はMonadState型クラスの制約があるだとか色々と省いている部分がありますがそれはまた別の機会にまとめます。今回は分かりやすさを重視しました。ソースコードなど詳しくは Control.Monad.State.Strict あたりに書いてあります。

HaskellでMNISTを使えるようにする

Qiita に 雑な記事 を書いただけになっていたのでちょっとした解説記事を書きました。

今回やったこと

『ゼロから作るDeep Learning – Pythonで学ぶディープラーニングの理論と実装』 のMNISTを扱うための下記サンプルコードを、PythonからHaskellに実装し直しました。

deep-learning-from-scratch/mnist.py at master · oreilly-japan/deep-learning-from-scratch

MNISTについて

MNISTとは手書き数字画像のデータセットのことです。

MNISTには画像データ本体とそれに対応した数字のラベルがあり、それぞれに訓練用とテスト用のものが用意されています。

データの中身は例えば訓練画像は以下のような構成です。 16バイト以降が画像データで、28*28バイトのピクセルデータが60000枚入っています。

TRAINING SET IMAGE FILE (train-images-idx3-ubyte):

[offset] [type]          [value]          [description]
0000     32 bit integer  0x00000803(2051) magic number
0004     32 bit integer  60000            number of images
0008     32 bit integer  28               number of rows
0012     32 bit integer  28               number of columns
0016     unsigned byte   ??               pixel
0017     unsigned byte   ??               pixel
........
xxxx     unsigned byte   ??               pixel

Pixels are organized row-wise. Pixel values are 0 to 255. 0 means background (white), 255 means foreground (black).

また、訓練ラベルは以下のような構成です。 8バイト以降がラベルデータで、1つ1バイトで10000枚入っています。

TRAINING SET LABEL FILE (train-labels-idx1-ubyte):

[offset] [type]          [value]          [description]
0000     32 bit integer  0x00000801(2049) magic number (MSB first)
0004     32 bit integer  60000            number of items
0008     unsigned byte   ??               label
0009     unsigned byte   ??               label
........
xxxx     unsigned byte   ??               label

The labels values are 0 to 9.

ref: MNIST handwritten digit database, Yann LeCun, Corinna Cortes and Chris Burges

サンプルコードの処理の流れ

元のサンプルコードでは以下のような処理をやっています。

  • MNISTを準備する
    • MNISTをダウンロード
    • MNISTをNumPyで変換する
    • 変換したものをPickle化して保存する
  • Pickle化されたファイルを読み込んで復元する
  • オプションパラメータに応じて処理を行う
  • 画像とラベルのデータセットを返す

どう実装したのか

上記の処理フローの中でも重要な部分について解説します。

MNISTをNumPyで変換する→hmatrixで変換する

サンプルコードでは NumPy というライブラリを利用することでベクトルや行列を扱っています。

Haskellで実装する際には hmatrix というライブラリを利用しました。

例えば行列はこんな感じで定義できます。

// Python
import numpy as np
A = np.array([1,2],[3,4])

// Haskell
import Numeric.LinearAlgebra
let a = (2><2) [1,2,3,4] :: Matrix R

Haskellでは型を指定する必要があるので、画像データを Matrix R 、ラベルデータを Vector R とし、それをまとめたデータセットをタプルで定義しました。

type Image = Matrix R
type Label = Vector R
type DataSet = (Image, Label)

ダウンロードしたデータセットをhmatrixで変換するコードは下記のようになります。 画像データとラベルデータそれぞれの変換関数を作りました。

loadImg :: String -> IO Image
loadImg fn = do
    c <- fmap GZ.decompress (BL.readFile $ generatePath fn)
    return . matrix imgSize . toDoubleList $ BL.drop 16 c

loadLabel :: String -> IO Label
loadLabel fn = do
    c <- fmap GZ.decompress (BL.readFile $ generatePath fn)
    return . vector . toDoubleList $ BL.drop 8 c

BLData.ByteString.LazyGZCodec.Compression.GZip のことです。

fmap GZ.decompress (BL.readFile $ generatePath fn) の部分では、あるデータセットファイルを ByteString で読み込み decompress という関数を使って解凍しています。(ダウンロードするファイルはgz形式であるため)

はじめの方の項目で書いたようにMNISTのデータセットは、画像データが16バイト以降、ラベルデータが8バイト以降が必要となるデータなので、それより前は drop で切り捨てています。

hmatrixへの変換は、画像データの場合は matrix imgSize . toDoubleList 、ラベルデータの場合は vector . toDoubleList でやっています。共通する関数 toDoubleListVectorMatrix で扱う数値の型を Double にするための変換関数です。

toDoubleList :: BL.ByteString -> [Double]
toDoubleList = fmap (read . show . fromEnum) . BL.unpack

matrix には列数を引数として渡すことで行列を作ることができます。1行に画像データ1つが入ればよいので列数には画像サイズの 28*28=784 を指定します。一方でラベルデータは、1行にすべてのデータを入れるので vector を使っています。

最後に、loadImg , loadLabel を使ってhmatrixで変換した画像データとラベルデータをデータセット DataSet 型としてまとめます。

convertDataset :: IO [DataSet]
convertDataset = do
    tri <- loadImg . snd . head $ keyFiles
    trl <- loadLabel . snd . (!!1) $ keyFiles
    ti <- loadImg . snd . (!!2) $ keyFiles
    tl <- loadLabel . snd . (!!3) $ keyFiles

    return [(tri, trl), (ti, tl)]

変換したものをPickle化して保存する→バイナリで保存する

Pythonにはオブジェクトをバイナリファイルで保存しそれを復元するための機能が pickleモジュール で提供されています。

Haskellにはpickleのような機能は見当たらなかったのでpickleでやっていることを愚直にやることにしました。幸いなことにhmatrixの VectorMatrix という型は Data.Binaryインスタンスとなっているため、 Data.Binary の関数がそのまま使えます。

https://github.com/albertoruiz/hmatrix/blob/0.18.0.0/packages/base/src/Internal/Vector.hs#L417 https://github.com/albertoruiz/hmatrix/blob/0.18.0.0/packages/base/src/Internal/Element.hs#L40

実際にバイナリで保存するコードは下記のようになります。

createPickle :: String -> [DataSet] -> IO ()
createPickle p ds = BL.writeFile p $ (GZ.compress . encode) ds

データセットをまずバイナリに変換して圧縮します。 Data.Binaryインスタンスとなっているので encode という関数を適用するだけでバイナリ ( ByteString )に変換することができます。あとはこのBytestringを圧縮してバイナリファイルとして保存するだけです。圧縮には compress 、保存には writeFile を使います。

Pickle化されたファイルを読み込んで復元する→バイナリを読み込む

バイナリで保存したときと逆のことをすれば元のデータセットを復元できます。

loadPickle :: String -> IO [DataSet]
loadPickle p = do
    eds <- BL.readFile p
    return $ (decode . GZ.decompress) eds

利用例

MNISTを扱えるようになっただけでは少し物足りないので、これらのコードを使ってニューラルネットワークの推論処理をやって締めようと思います。

下記コードは こちらのサンプルコードHaskellで実装し直したものです。

入力層を784個、出力層を10個のニューロンで構成しています。

import Numeric.LinearAlgebra
import ActivationFunction
import Mnist
import SampleWeight

batchSize = 100

predict :: SampleWeight -> Vector R -> Vector R
predict ([w1,w2,w3],[b1,b2,b3]) x =
    softMax' . (\x'' -> sumInput x'' w3 b3) . sigmoid . (\x' -> sumInput x' w2 b2) . sigmoid $ sumInput x w1 b1

sumInput :: Vector R -> Weight -> Bias -> Vector R
sumInput x w b = (x <# w) + b

maxIndexPredict :: SampleWeight -> Vector R -> Double
maxIndexPredict sw x = fromIntegral . maxIndex $ predict sw x

take' :: Indexable c t => Int -> Int -> c -> [t]
take' n1 n2 x
    | n1 >= n2  = []
    | otherwise = (x ! n1) : take' (n1+1) n2 x

increment :: [Double] -> [Double] -> Double
increment ps l = fromIntegral . length . filter id $ zipWith (==) ps l

countAccuracy' :: Double -> Int -> SampleWeight -> DataSet -> Double
countAccuracy' a n sw ds@(i,l)
    | n <= 0    = a
    | otherwise = countAccuracy' (a+cnt) (n-batchSize) sw ds
        where ps = maxIndexPredict sw <$> take' (n-batchSize) n i
              ls = take' (n-batchSize) n l
              cnt = increment ps ls

main = do
    [_, ds] <- loadMnist True
    sw <- loadSW
    let r = rows $ fst ds
        cnt = countAccuracy' 0 r sw ds

    putStrLn $ "Accuracy: " ++ show (cnt / fromIntegral r)
$ stack runghc src/NeuralnetMnist.hs
Accuracy: 0.9352

サンプルコードと同じ値が出力されたので正しく実装できていそうです。

まとめ

MNISTを扱うコードは100行ほどで実装できましたが、テストを書かなくてもそれなりに動くものが作れるのはやはり型を定義しているおかげなのでしょう(コンパイルが通れば大体意図した通りに動く)。コードを読むときも型が書いてあることで一目で関数の入出力がわかるため全体の処理の流れが追いやすいです。しかし、今回のようにファイル操作など IO モナドを多用しているために若干読みづらいコードになっている気がします…。

今回紹介したコードは一部なので全貌が気になる方は こちらのリポジトリソースコードを置いてあります。

参考文献

MNIST 手書き数字データを画像ファイルに変換する - y_uti のブログ
HaskellでParsecを使ってCSVをパースする - Qiita
Haskellから簡単にWeb APIを叩く方法 - Qiita

Haskell で『ゼロから作るDeep Learning』(3)

『ゼロから作るDeep Learning – Pythonで学ぶディープラーニングの理論と実装』 の読書メモです。

今回は 「4.5.1 2層ニューラルネットワークのクラス」の手前まで。

4章 ニューラルネットワークの学習

学習

  • この章でいう学習とは、訓練データから最適なパラメータの値を自動で獲得すること
  • 機械学習ニューラルネットワーク(ディープラーニング)は、人の介入を極力避けてデータからパターンを見つけ出すことができる
    • 例えば、ゼロから数値を認識するアルゴリズムを考える代わりに、画像から特徴のパターンを機械学習で学習させる
    • この特徴のことを 特徴量 といい、有名なものとしては SIFT 、 SURF 、 HOG などが挙げられる
      • ただし、解く問題に応じた特徴量の設計が必要になることも
    • 機械学習の識別器として有名なのは SVM や KNN など
  • 一方で、ニューラルネットワークによる学習では、機械学習に必要な特徴量の設計は不要

訓練データとテストデータ

  • 訓練データ: 学習を行い、最適なパラメータを探索するのに使う
    • 教師データとも呼ばれる
  • テストデータ: 訓練データにより学習を行ったモデルの汎化能力を評価するのに使う
    • 訓練データには含まれていないデータで評価する
  • あるデータセットにだけ過度に対応した状態を 過学習 という

損失関数

  • ニューラルネットワークの学習では、指標(基準)を設け、それを手がかりに最適な重みパラメータを探す。この指標のことを 損失関数 と呼ぶ
  • 損失関数は、モデルがどれだけ訓練データに適合していないかというモデルの性能の悪さを示す指標である
  • 損失関数として有名なのは、 2乗和誤差交差エントロピー誤差
  • 損失関数はすべての訓練データを対象として求めるのが理想だが、データ量が膨大であるときは一部データを全体の近似として対象とする。このような学習手法を ミニバッチ学習 という
  • 認識精度を指標としないのは、パラメータの微分がほとんどの場所で 0 になるため
    • ある瞬間だけ変化するのではなく連続的に変化する指標が必要
    • 活性化関数を例に挙げると、シグモイド関数微分がそれにあたる

勾配

  • 勾配: すべての変数の偏微分をベクトルとしてまとめたもの
  • 勾配の結果にマイナスを付けて描画したとき、勾配の示す方向は各場所において関数の値を最も減らす方向となる
  • 勾配は損失関数が最小値を取る場所を探索するための手がかりとなる
    • 勾配はあくまで関数の値を最も減らす方向を示すだけであり、その方向が最小値を取るとは限らない(極小値や鞍点の可能性もある)
  • 勾配方向へ一定距離進むことを繰り返して関数の値を減らしていく手法を 勾配法 と呼ぶ
    • 厳密には最小値を探す場合を 勾配降下法 、最大値を探す場合を 勾配上昇法 という

ニューラルネットワークの学習

  • ニューラルネットワークの学習は 1.ミニバッチ学習 -> 2.勾配の算出 -> 3.パラメータの更新 -> 4. 1,2,3 の繰り返し という手順で行う
  • 上記手順は、無作為に選定したデータを用いていることから 確率的勾配降下法 (SGD) と呼ばれる

実践編

今回は無し

『デザインパターンとともに学ぶオブジェクト指向のこころ』を読んだ

デザインパターンとともに学ぶオブジェクト指向のこころ (Software patterns series)

デザインパターンとともに学ぶオブジェクト指向のこころ (Software patterns series)

なぜ読んだのか

最近、仕事でコードレビューを受けたときに「このコードはオブジェクト指向でないのでは? なんか関数型っぽい」と言われたことで Static おじさん?化の兆候に気がつき、なんとかせねばと思ったのがきっかけでした。

そもそも、何気なく触れていたであろうオブジェクト指向って一体何なんだっけという疑問を改めて解消するために読み始めました。

本書を選んだのは、チームメンバーがおすすめしていた本から選びました。 (教えていただきありがとうございました :pray: )

感想

オブジェクト指向について、本書では次のように説明しています。

オブジェクト指向は、オブジェクトという概念を中心に捉えたものの考え方です。このため、オブジェクトという観点からすべてのものごとを見ることになります。つまり、問題領域を機能に分解していくのではなく、オブジェクトに分解していくわけです。

– 1.6 オブジェクト指向パラダイム

また、オブジェクトについては

概念上の観点に基づいた場合、オブジェクトは責務を備えた実体であると定義できます。こういった責務によって、オブジェクトの振る舞いが定義されるのです。また、場合によっては、オブジェクトは特定の振る舞いを保持した実体であるとも考えられます。

– 8.2 オブジェクト : 従来からの考え方と新たな考え方

私は問題領域に存在するものを核に、その問題領域に存在する要求をメソッドとして肉付けしたものをオブジェクトとし、それらを扱うことをオブジェクト指向だと教わってきました。しかし、そのようなテクニックではすぐに設計が複雑化してしまい、変化に対応することが困難になることを指摘しています。オブジェクトをただの再利用可能な汎用モジュールとして定義するだけでは足りないというわけです。

本書は、変化に対応できるような柔軟な設計にするための道具として「共通性/可変性分析」や「デザインパターン」を用いた設計アプローチについての説明が主ですが、その説明を通してどのようにオブジェクト指向という考え方が使われているのかを知ることができました。また、デザインパターンの使い所についても、デザインパターンを適用するだけですべての問題が解決できるわけではないので、解決しようとしている問題に集中することが重要というのはなるほどなあという感じでした。デザインパターンはある問題に対する解決策の一例として見るべきだったのですね。

まとめ

一番の収穫は、今までの自分のオブジェクト指向の理解では不十分だったことを知れたことでした。旧来の考え方が間違っていたというよりも、問題に取り組むための視点を変えることでより柔軟な設計をすることができるという本書の説明の仕方にしている点も良かったです。

また、今までデザインパターンの本を読んできて腑に落ちていなかった点も、解決しようとしている問題についての比重が少なくデザインパターンを使う上での When , Why が曖昧だったのが原因なのかなと思ったのでした。このような視点を意識しつつ他の本ももう一度読み直してみたいです。