じぶんメモ

プログラミングのメモ、日常のメモとか。

golangテストはじめ

はじめに

golangでのテストはとてもシンプルで、rubyrspecのように新しくDSLを覚える必要もありません。
テストについての記事は沢山あるのですが、自分の中で特にこれは最初に覚えておいた方がいいなと思うことをピックアップしました。

基本的なtestの書き方

  • 例えばcalc.go のテストならば同じディレクトリ内にcalc_test.goという名前で作成する。
  • テストファイル内ではtestingパッケージをインポートする。
  • テストファイル内では、TestXXXという名前でテストメソッドを作成する。
  • DSLは特に無いので普通にテストコードを書く。
  • パラメータと期待値の組み合わせの配列を用意して、ループで検証していく形が推奨されている(Table Driven Test)
package calc

func Add(a,b int) int {
    return a + b
}
package calc

import (
    "testing"
)

func TestAdd(t *testing.T) {
    patterns := []struct {
        a        int
        b        int
        expected int
    }{
        {1, 2, 3},
        {10, -2, 8},
        {-10, -2, -12},
    }

    for idx, pattern := range patterns {
        actual := Add(pattern.a, pattern.b)
        if pattern.expected != actual {
            t.Errorf("pattern %d: want %d, actual %d", idx, pattern.expected, actual)
        }
    }
}

testの実行方法

  • カレントディレクトリ以下すべてを再帰的にテストgo test -v ./...
  • 特定のパッケージをテストgo test -v ./hogehoge(パッケージディレクトリを相対パスで指定する)
  • 特定のメソッドのみテストするgo test -run TestAdd ./...

※ -v オプションを付けると実行結果に詳細が付きますので、基本的にはつけておいたほうが良いです。

テストの実行前後に処理を入れるには

TestMainメソッドを定義します。
code := m.Run()を実行するとテストメソッドが走るので、その前後にDBの初期化処理等を入れることが出来ます。

package calc

import (
    "fmt"
    "os"
    "testing"
)

func TestMain(m *testing.M) {
    fmt.Println("before test")
    code := m.Run()
    fmt.Println("after test")
    os.Exit(code)
}

func TestAdd(t *testing.T) {
    // 以下省略
}

これを実行すると、以下のようになります。
テストの前後にfmt.Printlnが入っているのがわかります。

$ go test -v ./...
before test
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
after test
ok

テストでモックを使うには

インターフェースを使ったモック

インターフェースを使っているオブジェクトの場合、実際のコードとテストコードでインタフェースに定義するオブジェクトを変えることでテスト時の振る舞いを変えることが出来ます。 ここではsomefuncパッケージのClientオブジェクトのRunメソッド内で呼び出されるcallメソッドの振る舞いを、モックを使って切り替える方法を紹介します。

package somefunc

type Caller interface {
    call(val int) int
}

type Client struct {
    FuncCaller Caller
}

type ExampleCaller struct{}

func (c *Client) Run(val int) int {
    return c.FuncCaller.call(val)
}

func (f *ExampleCaller) call(val int) int {
    return val
}

上記のコードを実行するには以下のように呼び出します。

c := somefunc.Client{&somefunc.ExampleCaller{}}
c.Run(1)

ここで、テスト時にExampleCallerのモックを作って、callメソッドの振る舞いを変えるにはテストコードを以下のようにします。

package somefunc

import (
    "testing"
)

func TestRun(t *testing.T) {

    patterns := []struct {
        val      int
        expected int
    }{
        {2, 2},
        {8, 8},
        {-10, -10},
    }

    for idx, pattern := range patterns {
        // Clientのnewの際に、モックオブジェクトを引数にする
        c := Client{&mockCaller{}}
        actual := c.Run(pattern.val)
        if pattern.expected != actual {
            t.Errorf("pattern %d: want %d, actual %d", idx, pattern.expected, actual)
        }
    }
}

// callメソッドのレシーバをmockCallerとして宣言する。
type mockCaller struct{}

// 通常のコードではcallメソッドは引数の値をそのまま返却するが、
// モックでは、引数 + 10した値を返却するようにする。
func (s *mockCaller) call(val int) int {
    return val + 10
}

変数の再代入で行う方法

ここではsomeprocessパッケージのRun関数のテストを行っていますが、Run内でcallという関数を呼び出しています。
このcall関数の挙動をテストの時だけ切り替えるには、call関数を変数に入れ、テスト内で変数にモックを再代入すればOKです。

package someprocess

func Run(val int) int {
    return call(val)
}

var call = func(val int) int {
    return val
}
package someprocess

import (
    "testing"
)

func TestRun(t *testing.T) {
    call = func(val int) int {
        return val + 10
    }

    patterns := []struct {
        val      int
        expected int
    }{
        {2, 12},
        {8, 18},
        {-10, 0},
    }

    for idx, pattern := range patterns {
        actual := Run(pattern.val)
        if pattern.expected != actual {
            t.Errorf("pattern %d: want %d, actual %d", idx, pattern.expected, actual)
        }
    }
}

anyenvを使って~env系をひとまとめにする

ruby, pythonを使用しているとrbenv, pyenv等を使ってバージョン管理をするが、
これらのバージョン管理システムは、言語ごとに導入、PATHを通す必要がある。
anyenvを使用すれば、これらの言語ごとのバージョン管理システムを一元管理できる。

1.以前使用していた~env系を削除

念の為。以前使用していた~env系とanyenvが競合しないように削除しておく。
brewでインストールしている場合は以下のコマンドでOK.

$ brew uninstall rbenv

他のツールが依存していると削除できないこともあるので、
その時はenvに依存しているツールを先に削除することでenvも削除することができる。
ディレクトリは残るのでこれも削除しておく。

$ rm -rf ~/.rbenv

2.anyenvのインストール

githubからcloneしてくる

$ git clone https://github.com/riywo/anyenv ~/.anyenv

PATHを通す。(bash_profileに記載)

if [ -d ${HOME}/.anyenv ] ; then
    export PATH="$HOME/.anyenv/bin:$PATH"
    eval "$(anyenv init -)"
fi

おそらくanyenv導入前は、各env毎に↑のコードを書いていたと思う。

3.各種envをインストール

以下のコマンドで各envを導入する。

$ source ~/.bash_profile
$ anyenv install rbenv
$ anyenv install pyenv

あとは以前と同じように各言語のversionをインストールすればOK。

$ rbenv install 2.4.0

gitで歴史を改ざんするには

直前の履歴を改ざんする

1. コミットをなかったことに

HEADの一つ前に戻る。

$ git reset HEAD~1
$ git push -f origin

2. コミットを修正

対象ファイルを修正し、commit --amendする。

$ git commit --amend
$ git push -f origin

特定の歴史を改ざんする

rebase -iを使い、改ざんしたいコミットまで戻り、修正する。

$ git rebase -i <改ざんしたいコミットのID>
pick c832e Add comment
pick f983e Initial commit

戻りたいコミットを指定し、editにし保存する。

edit c832e Add comment
pick f983e Initial commit

改ざんしたいファイルを修正し、commit --amendする。 その後、git rebase --continueで元に戻す。

$ git rebase --continue

途中、コンフリクトが発生することがあるので、そのときは解消して、
rebase --continueする。
Successfully rebased and updated refs/heads/ブランチ名 と出れば完了。

途中で訳わからなくなってもとに戻したいと思ったら↓のコマンドでOK。

$ git rebase --abort

n-gramによる文章の類似率を求める

N-gramとは、テキストで隣り合ったN文字のことを示す。
以下では2つ文章を指定された文字数で分割し、2つの文章間で分割した文字がどれだけマッチするかの頻度から、2つの文章の類似率を求める。

def ngram(str, num):
    res  = []
    slen = len(str) - num + 1
    for i in range(slen):
        extract_str = str[i:i+num]
        res.append(extract_str)
    return res

# 2つの文章の類似率を調べる
def diff_ngram(str1, str2, num):
    str1_ngram   = ngram(str1, num)
    str2_ngram   = ngram(str2, num)
    match_result = []
    count        = 0
    for str1_unit in str1_ngram:
        for str2_unit in str2_ngram:
            if str1_unit == str2_unit:
                count += 1
                match_result.append(str1_unit)
    return count / len(str1_ngram), match_result

# 2つの文章がどれだけ似ているかテストする
# 文字の分割を2文字ごと3文字ごとにして調べる。
a = '今日、渋谷で美味しいトンカツを食べた。'
b = '渋谷で食べた今日のトンカツは美味しかった。'

result2, word_list2 = diff_ngram(a, b, 2)
result3, word_list3 = diff_ngram(a, b, 3)

print('2-gram:', result2, word_list2)
print('3-gram:', result3, word_list3)

# 2-gram: 0.6111111111111112 ['今日', '渋谷', '谷で', '美味', '味し', 'トン', 'ンカ', 'カツ', '食べ', 'べた', 'た。']
# 3-gram: 0.29411764705882354 ['渋谷で', '美味し', 'トンカ', 'ンカツ', '食べた']

これだと単純な接続詞とかでもヒットしてしまうのであまり精度は高く無い気がする。 もっと本格的に文章解析するならば、RNNやLSTMに手をつかけるのがいいのかな。。。

TensorflowとKerasを用いてmnistのCNNを構築してみる

コードは以下の通り。

Tensorflow・Kerasを使ってmnistの訓練を行う

from keras.models               import Sequential
from keras.datasets             import mnist
from keras.layers.convolutional import Conv2D
from keras.layers.pooling       import MaxPooling2D
from keras.layers.core          import Dense, Dropout, Activation, Flatten
from keras.optimizers           import Adam
from keras.utils                import np_utils

from keras import backend as K

# ------------------------------
# データ準備
# ------------------------------
(X_train, y_train), (X_test, y_test) = mnist.load_data()

# データをfloat型にして正規化する
X_train = X_train.astype('float32') / 255.0
X_test  = X_test.astype('float') / 255.0

img_rows = 28
img_cols = 28

# image_data_formatによって畳み込みに使用する入力データのサイズが違う
if K.image_data_format() == 'channels_first':
    X_train     = X_train.reshape(-1, 1, img_rows, img_cols)
    X_test      = X_test.reshape(-1, 1, img_rows, img_cols)
    input_shape = (1, img_rows, img_cols)
else:
    X_train     = X_train.reshape(-1, img_rows, img_cols, 1)
    X_test      = X_test.reshape(-1, img_rows, img_cols, 1)
    input_shape = (img_rows, img_cols, 1)

# ラベルはone-hot encodingを施す
y_train = y_train.astype('int32')
y_test  = y_test.astype('int32')
y_train = np_utils.to_categorical(y_train, 10)
y_test  = np_utils.to_categorical(y_test, 10)

# ------------------------------
# モデルの定義
# ------------------------------
# 場合分けでインプットデータのテンソルの形を変える
# mnistデータは28×28ピクセルで60000個のデータで、28×28ピクセルで1チャネルのデータに変える
# reshapeの-1は、28×28ピクセルで60000個のデータを28×28ピクセルで1チャネルによしなに変えてくれる
model = Sequential()

# 畳み込み層1
# フィルタは5×5ピクセルで28個→出力データは32チャネル
# 入力データは28×28ピクセルの1チャンネル
# input_shapeを指定するのは1層目だけ
model.add(Conv2D(32, (5, 5), input_shape=input_shape))
model.add(Activation('relu'))
# プーリング層1
model.add(MaxPooling2D(pool_size=(2, 2)))

# 畳み込み層2
# フィルタは5×5ピクセルで64個→出力データは64チャネル
model.add(Conv2D(64, (5, 5)))
model.add(Activation('relu'))
# プーリング層2
model.add(MaxPooling2D(pool_size=(2, 2)))

# Flattenして全結合する
model.add(Flatten())
model.add(Dense(128))
model.add(Activation('relu'))

# 過学習予防のためドロップアウトして最後に出力(出力データ数はone-hotなので10個)
model.add(Dropout(0.5))
model.add(Dense(10, activation='softmax'))

# ------------------------------
# 学習の開始
# ------------------------------
epochs = 20
batch_size = 100

adam = Adam(lr=1e-4)
model.compile(optimizer=adam, loss='categorical_crossentropy', metrics=["accuracy"])
history = model.fit(X_train, y_train, batch_size=batch_size, epochs=epochs, verbose=1, validation_split=0.1)

loss, accuracy = model.evaluate(X_test, y_test, verbose=1)
print('loss=', loss)
print('accuracy=', accuracy)

以下、注意点。

  • mnistは28×28の1チャネルだが、RGBの画像とかだと3チャネルになる。
  • Conv2Dの引数は出力サイズ(フィルタのデータ数), フィルタの縦横サイズ、入力データのサイズ
  • KerasのConv2Dでは、1層目のみ入力データのサイズの指定が必要
  • KerasのバックエンジンによってConv2Dに渡す入力データのサイズの指定形式が異なる。

3つめの入力形式に関しては~/.keras/keras.jsonに記載されているimage_data_formatをみて判断する。

コードでは、1層目でmnistのデータを(データの個数, 28ピクセル, 28ピクセル , 1チャネル)のテンソルに変換し、
(5ピクセル, 5ピクセル, 1チャネル)のデータ32個のフィルタにかけていて、その結果をプーリングして次の層へ渡す。
最終的な出力の前に全結合してドロップアウトした結果をソフトマックス関数にかけてone-hotのラベル形式で出力している。

TensorflowとKerasを使ってmnistの訓練と評価をしてみる

Tensorflow・Kerasとは(ザックリ)

  • Tensorflowはpythonで使える機械学習のためのライブラリ
  • Kerasは更にラッパーライブラリで、Tensorflowだと数百行かかるコードが数十行で済んだりする。

インストール方法

とりあえずpipで入れてみる。

$ pip install -U tensorflow
$ pip install -U keras

Tensorflow・Kerasを使ってmnistの訓練を行う

from keras.datasets    import mnist
from keras.models      import Sequential
from keras.layers.core import Dense, Dropout, Activation
from keras.optimizers  import Adam
from keras.utils       import np_utils

# mnistデータの読み込み
(X_train, y_train), (X_test, y_test) = mnist.load_data()

# データをfloat型にして正規化する
# データ数×縦ピクセル数×横ピクセル数のデータをデータ数×784の方にする
X_train = X_train.reshape(60000, 784).astype('float32') / 255
X_test  = X_test.reshape(10000, 784).astype('float') / 255

# ラベルをone_hot形式に変換
y_train = np_utils.to_categorical(y_train, 10)
y_test  = np_utils.to_categorical(y_test, 10)

# モデルの構造を定義
model = Sequential()
# 1層目
model.add(Dense(512, input_shape=(784,)))
model.add(Activation('relu'))

# 2層目
model.add(Dense(512))
model.add(Activation('relu'))

# 出力
model.add(Dense(10))
model.add(Dropout(0.2))
model.add(Activation('softmax'))

# モデルの構築
model.compile(
    loss='categorical_crossentropy',
    optimizer=Adam(),
    metrics=['accuracy'])

# データで訓練
hist = model.fit(X_train, y_train)

# テストデータで評価する
loss, accuracy = model.evaluate(X_test, y_test, verbose=1)
print('loss=', loss)
print('accuracy=', accuracy)

特に注目したいのがmodelを用いて直感的に学習の手順を記述することができる。
コードでは3層のニューラルネットワークを構築している。
Denseは全結合を行うモジュールで、1層目のみ入力データのサイズを指定する必要がある。

モデルを定義し、コンパイルし、訓練をし、テストデータで精度を評価するという流れ。

【ゼロから作るDeep Learning】要点まとめ 第7章

7章 - 畳み込みニューラルネットワーク

CNN(Convolutional Neural Network)は、畳み込み層、プーリング層からなる学習方法
基本的にはconv→relu→conv→relu→...→pool→conv→relu→conv→relu→...→pool→affine→softmaxの流れ
conv→relu or conv→relu→poolが隠れ層になるイメージ。

このサイトがとてもわかり易い。
http://postd.cc/how-do-convolutional-neural-networks-work/

畳み込み層

  • 畳み込み層は、用意された入力データ(画像データなど)をフィルタ(重み)を使って畳み込む
  • 入力データ(縦×横×チャネル→RGB画像なら3チャネル)に対して、フィルタを少しずつづらしながら行列演算をしていく。
  • 入力データのチャネル数とフィルタのチャネル数は合わせておく必要がある。 例) 入力データxは(32,32,3)、フィルタwは(5,5,3)
  • 入力データxは(32,32,3)、フィルタwは(5,5,3)を1幅ずつスライドさせていくと、出力される値は(28,28,1)のデータになる。
  • フィルタの数が増えれば(4次元配列)、出力されるデータのチャネル数も同じ数だけ増える。
  • 例) 入力データx(32,32,3)、フィルタw(10,5,5,3)(10はフィルタの数)を畳み込むと出力される値は(28,28,10)のデータになる。
  • 入力データxを行列に変換 × フィルタwを行列に変換 + バイアス →reluに突っ込む → 再度4次元データに変換して次の隠れ層へ送る。
  • ストライド == スライド幅
  • パディング == スライドした時にはみ出る箇所のデータを0として扱う

プーリング層

  • 縦横の幅を決めてその範囲内での最大値を取る(maxプーリング)プーリングを行う層。
  • 大きな画像を、重要な情報は残しつつ縮小する利点がある。
  • maxプーリングの他にもaverageプーリングがある。