読者です 読者をやめる 読者になる 読者になる

新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。気づけば社会人3年目に突入。

学習記録:ゼロから作るDeepLearning

学習記録 プログラミング DeepLearning

3章:ニューラルネットワーク(続き)

出力層の設計

ソフトマックス関数

機械学習の問題は,分類問題(データがどのクラスに属するか)と回帰問題(ある入力データから数値の予測を行う)に大別できる。
一般的に,回帰問題では恒等関数を,分類問題ではソフトマックス関数を用いる。

前述したとおり,恒等関数は入力をそのまま出力する。一方,ソフトマックス関数は次の式で表される。
yk = exp(ak) / ∑exp(ai)

ソフトマックス関数の実装は以下のとおり。

import numpy as np

def softmax(a):
    exp_a = np.exp(a)
    sum_exp_a = np.sum(exp_a)
    return exp_a / sum_exp_a

a = np.array([0.3,2.9,4.0])
print(softmax(a))

この実装には欠陥があり,それは指数関数の計算結果が非常に大きな値になってしまう可能性がある点である。
これによる数値のオーバーフローを防ぐため,分子・分母の指数関数expの中で,定数C(一般には入力信号の最大値)をマイナスするのが一般的である。

def softmax(a):
    c = np.max(a)
    exp_a = np.exp(a-c)   # ここでCを引いておく
    sum_exp_a = np.sum(exp_a)
    return exp_a / sum_exp_a     # 結果は変わらない

さて,このソフトマックス関数には,出力が0から1.0の間の実数になり,総和が1になるという特徴がある。

a = np.array([0.3,2.9,4.0])
print(softmax(a))
print(np.sum(softmax(a)))
% python softmax.py
[ 0.01821127  0.24519181  0.73659691]
1.0

この性質から,ソフトマックス関数の出力を確率として解釈することができる。
例えば上記の例なら,y[2]の確率が73%で一番高いので,y[2]のクラスである,といった判断ができることになる。
ただし,ソフトマックス関数の適用前後における値の大小関係は変わらない(上の例だと適用前はa[2]が一番大きく,適用後も2番目の要素であるy[2]が一番大きい)ので,一般的には出力層のソフトマックス関数は省略することが多い(指数関数の計算にはそれなりの計算資源を食うため)。

手書き数字認識

機械学習の手順は学習と推論の2フェーズに分けられる。
学習フェーズでモデルを学習し,推論フェーズでモデルを使って未知のデータに対して推論を行う。
ここでは,学習済のパラメータを使って,ニューラルネットワークの推論処理を実装してみる。

MNISTデータ・セット

MNISTは機会学習の分野で有名なデータセットで,0から9の数字画像(28x28ピクセル)で構成され,訓練画像とテスト画像が用意されている。
MNIST画像を表示する実装は以下になる。

import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist
from PIL import Image

def img_show(img):
    pil_img = Image.fromarray(np.uint8(img))
    pil_img.show()

(x_train, t_train), (x_test, t_test) = \
    load_mnist(flatten=True, normalize=False)

img = x_train[0]
img = img.reshape(28,28)
img_show(img)

本書のサンプルプログラム中のdataset/mnist.pyにあるload_mnist関数により,MNISTデータのダウンロードから画像データのNumPy配列への変換を一気に行うことができる。
画像の表示にはPILモジュールを使っており,訓練画像の1枚目(「5」の画像)を表示している。
なお,load_mnist関数でflatten=Trueを指定していると,画像がNumPy配列に1次元で格納されるため,もとの28x28のサイズにreshapeしてから表示をする必要がある。

推論処理

このMNISTデータセットに対して推論処理を行うニューラルネットワークを実装する。
入力層は784個(画像サイズが28x28のため),出力層は10個(0〜9)のニューロンで構成する。
隠れ層は二つとし,それぞれ50個と100個のニューロンを持つものとする(ここの個数は任意に設定できる)。

# MNISTデータを取得する                                                              
def get_data():
    # flatten: 入力画像を1次元に配列にする(Falseなら1x28x28の3次元配列)              
    # normalize: 入力画像を0〜1の値に正規化する(Falseなら0〜255)                     
    # one_hot_label: 正解のラベルだけ1の配列にする(Falseなら2,7など正解の値が入る)   
    (x_train, t_train), (x_test, t_test) = \
         load_mnist(flatten=True, normalize=True, one_hot_label=False)
    return x_test, t_test

# 学習済の重みパラメータ(sample_weight.pkl)を使ってネットワークを初期化する          
# このファイルには重みとバイアスがディクショナリ型の変数として保存されている         
def init_network():
    with open("sample_weight.pkl", "rb") as f:
        # pickle(実行中のオブジェクトをファイルとして保存する機能)を使う             
        network = pickle.load(f)

    return network

def predict(network, x):
    W1, W2, W3 = network['W1'], network['W2'], network['W3']
    b1, b2, b3 = network['b1'], network['b2'], network['b3']

    a1 = np.dot(x, W1) + b1
    z1 = sigmoid(a1)
    a2 = np.dot(z1, W2) + b2
    z2 = sigmoid(a2)
    a3 = np.dot(z2, W3) + b3
    y = softmax(a3)

    return y

x, t = get_data()
network = init_network()

accuracy_cnt = 0 # 正解数                                                            
for i in range(len(x)):
    # 予測する                                                                       
    y = predict(network, x[i])
    # 配列yの中で最大の(最も確率の高い)インデックスを取得                            
    p = np.argmax(y)
    # pが正解(t[i])と等しければ正解数を増やす                                        
    if p == t[i]:
        accuracy_cnt += 1

# 正解率を精度として出力                                                             
print("Accuracy: " + str(float(accuracy_cnt) / len(x)))
% python neuralnet_mnist.py
Accuracy: 0.9352

この結果は,93.52%正しく分類できた,ということを表す。
今後ニューラルネットワークの構造や学習方法を工夫することで,99%以上の精度を目指す。

学習記録:ゼロから作るDeepLearning

学習記録 Python DeepLearning

3章:ニューラルネットワーク(続き)

多次元配列の計算

まずはNumPyによる多次元配列の計算について学ぶ。
2次元配列=行列の内積を計算してみる。内積の計算にはdot関数を使う。

>>> A = np.array([[1,2],[3,4]])
>>> B = np.array([[5,6],[7,8]])
>>> np.dot(A,B)
array([[19, 22],
       [43, 50]])

なお,当然内積の計算ができるように,行列Aの1次元目と行列Bの0次元目の次元数は一致している必要がある。
では,続いてNumPy行列を使ってニューラルネットワークの実装を行う。
今回はバイアスと活性化関数は省略し,重みだけあるものとする。

>>> X = np.array([1,2])
>>> W = np.array([[1,3,5],[2,4,6]])
>>> Y = np.dot(X,W)
>>> print(Y)
[ 5 11 17]

この例では,入力x1とx2があり,出力はy1,y2,y3がある。
x1からy1〜3への重みがw1で,x2からy1〜3への重みがw2で表されている。

3層ニューラルネットワークへの実装

それでは実践的なニューラルネットワークの実装を行う。
ここで実装するのは3層のニューラルネットワークで,入力層は2つ,続いて3つ -> 2つの隠れ層があり,出力層は2つのニューロンから構成される。

入力層を1 x 2の行列X,重みを2 x 3の行列W,バイアスを1 x 3の行列Bとして表すと,
第1層(1つ目の隠れ層)の1 x 3行列Aは次のように表される。

import numpy as np

X = np.array([1.0, 0.5])
W1 = np.array([[0.1,0.3,0.5],[0.2,0.4,0.6]])
B1 = np.array([0.1,0.2,0.3])

A1 = np.dot(X, W1) + B1
print(A1)
% python 3layer_nn.py
[ 0.3  0.7  1.1]

続いては第1層の活性化関数を見ていく。
隠れ層での重み付き和aに対し,h()で表される活性化関数により,出力zが得られる。
ここでは活性化関数としてシグモイド関数を使うことにし,出力zはそのまま次の第2層への入力になるので,
第1層目から2層目への実装は次のようになる。

import matplotlib.pylab as plt

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

Z1 = sigmoid(A1)
W2 = np.array([[0.1,0.4],[0.2,0.5],[0.3,0.6]])
B2 = np.array([0.1,0.2])

A2 = np.dot(Z1, W2) + B2
print(A2)
% python 3layer_nn.py
[ 0.3  0.7  1.1]
[ 0.51615984  1.21402696]

ここまでは先程の実装とほぼ同じで,このA2が第2層(2つ目の隠れ層)への入力になる。
ここでの第2層での活性化関数も同様にシグモイド関数を使うと,Z2はsigmoid(A2)となり,これが最後の出力層への入力となるところも同じである。

出力層の実装もここまでとほぼおなじだが,最後の活性化関数だけが異なる。

def identity_function(x):
    return x

Z2 = sigmoid(A2)
W3 = np.array([[0.1,0.3],[0.2,0.4]])
B3 = np.array([0.1,0.2])

A3 = np.dot(Z2, W3) + B3
Y = identity_function(A3)

print("Y: ")
print(Y)
% python 3layer_nn.py
[ 0.3  0.7  1.1]
[ 0.51615984  1.21402696]
Y: 
[ 0.31682708  0.69627909]

出力層の活性化関数として,identity_function()を使用している。
上の例では,入力をそのまま出力する恒等関数を使っている。出力層の活性化関数はσ()で表し,中間層のh()とは異なることを示す。
なお,σ()は解く問題の性質に応じて決める。回帰問題では恒等関数,2クラス分類問題ではシグモイド関数…といった決め方になる。

学習記録:ゼロから作るDeepLearning

学習記録 Python

3章:ニューラルネットワーク

パーセプトロンからニューラルネットワーク

2層のニューラルネットワークでは,0層が入力層,1層が中間層,2層が出力層と呼ばれる。
パーセプトロンでは,バイアスbとxiwiの和が1を超えたら出力が1になった。
これをニューラルネットワークで表すには,bも入力として追加する必要がある。
x1とx2の2入力がある場合,x1は重みw1,x2は重みw2,bは入力が1で重みbの入力として表現され,
式として表すと,y = h(b + w1x1 + w2x2) となる。(h(x)はxが0より大きい時1になる関数)

活性化関数

前述の式は,閾値を境にして出力が切り替わることから,ステップ関数や階段関数と呼ばれる。
では,Pythonを用いてステップ関数をグラフで表してみる。

def step_function(x):
    y = x > 0
    return y.astype(np.int)

上記は[1.0,2.0]のようなNymPy配列に対応した実装になっている。
Numpy配列に対して不等号による演算を行うと,Booleanの配列が作成される。
さらに,astype(np.int)によってBooleanを1か0のint型入れtに変換することができる。

>>> import numpy as np
>>> x = np.array([-1.0,1.0,2.0])
>>> x
array([-1.,  1.,  2.])
>>> y = x > 0
>>> y
array([False,  True,  True], dtype=bool)
>>> y.astype(np.int)
array([0, 1, 1])

それでは,このステップ関数をグラフで表示してみる。

import numpy as np
import matplotlib.pylab as plt

def step_function(x):
    y = x > 0
    return y.astype(np.int)

x = np.arange(-5.0, 5.0, 0.1)
y = step_function(x)
plt.plot(x, y)
plt.ylim(-0.1, 1.1)
plt.show()
  • 5.0から5.0まで0.1刻みのNumPy配列を生成し,それをstep_functionに渡して結果を1か0からの配列として受け取ってプロットしている。

これを実行すると,以下のようなグラフが表示される。

f:id:uriku:20170205191659p:plain

シグモイド関数

ニューラルネットワークでよく用いられる活性化関数の一つが,シグモイド関数である。
h(x) = 1 / (1 + exp(-x))

これはPythonで次のように書くことができる。

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

この関数は,NumPyのブロードキャストと呼ばれる機能により,引数xとしてNumPy配列を受け取ることもできる点に注意。
さて,シグモイド関数を先程と同じようにグラフにプロットすると,次のようになる。

f:id:uriku:20170205192437p:plain

さて,ステップ関数と比べて線が滑らかであることがわかる。
ステップ関数が0か1かしか返さないのに対し,シグモイド関数は実数を返すことができる。
つまり,パーセプトロンニューロン間を0か1の二値の信号しか流れなかったのに対し,ニューラルネットワークでは連続的な実数の値が信号として流れることになる。

ReLU関数

シグモイド関数ニューラルネットワークの歴史の古くから使用されてきたが,最近ではReLU(Rectified Linear Unit)という関数が主に用いられる。
ReLUは,入力が0を超えていればその入力をそのまま出力し,0以下なら0を出力する。

def relu(x):
    return np.maximum(0, x)

f:id:uriku:20170205193117p:plain

学習記録:ゼロから作るDeepLearning

学習記録 Python

参考文献

2章:パーセプトロン

パーセプトロンとは

複数の信号を入力として受取,ひとつの信号を出力するアルゴリズム
パーセプトロンの信号は流すか流さないか(1/0)の二値の値をとる。
入力信号(xi)には重み(wi)が乗算され,信号の総和が閾値(θ)を超えると出力(y)が1になる(=ニューロンが発火する)。

単純な論理回路

ANDゲートは,二つの入力値x1とx2を持ち,両方が1のときだけ出力も1となる。
このANDゲートをパーセプトロンで表現しようとすると,これを満たすw1, w2, θの値を決める必要がある。
この値の組み合わせは無限にあり,例えば(0.5, 0.5, 0.7)などがANDゲートの条件を満たす。

同様に,NANDゲートは(-0.5,-0.5,-0.7),ORゲートは(0.5,0.5,0.2)などで表現できる。

パーセプトロンの実装

先の論理回路Pythonで実装すると以下のようになる。

def AND(x1, x2):
    w1, w2, theta = 0.5, 0.5, 0.7
    tmp = x1*w1 + x2*w2
    if tmp <= theta:
        return 0
    else:
        return 1

print(AND(0,0))
print(AND(1,0))
print(AND(0,1))
print(AND(1,1))

このプログラムの出力は以下になり,想定どおりの結果になっていることがわかる。

% python AND.py
0
0
0
1

この実装は素直でわかりやすいが,以降のことを考えて,重みとバイアスによる実装方法に変更する。
閾値θを-bとして表すと,b + wi*xiの和が0以下なら出力は1に,0より大きければ出力は1になる。
このbをバイアスと呼び,これを使うと実装は以下のようになる。

import numpy as np

def AND(x1, x2):
    x = np.array([x1, x2])
    w = np.array([0.5, 0.5])
    b = -0.7
    tmp = np.sum(x*w) + b
    if tmp <= 0:
        return 0
    else:
        return 1

def NAND(x1, x2):
    x = np.array([x1, x2])
    w = np.array([-0.5, -0.5])
    b = 0.7
    tmp = np.sum(x*w) + b
    if tmp <= 0:
        return 0
    else:
        return 1

def OR(x1, x2):
    x = np.array([x1, x2])
    w = np.array([0.5, 0.5])
    b = -0.2
    tmp = np.sum(x*w) + b
    if tmp <= 0:
        return 0
    else:
        return 1

ここで,同時にORゲートも実装したが,ANDとの違いは重みとバイアス部分だけであることがわかる。

では,XORゲートはどのように実装すればよいか?
XORゲートではx1かx2のどちらかだけが1のときだけ出力が1になるが,考えてみるとどのような重み・バイアスを設定してもこれを実装できないことがわかる。
一見するとパーセプトロンではXORゲートを表現できないように思えるが,層を重ねることでこれを実現できる。

多層パーセプトロン

XORゲートを作る方法の一つに,これまで作ってきたゲートを組み合わせる方法がある。
NANDとORを前段におき,それぞれの出力をANDゲートに繋ぐことでXORゲートが実現できる。

def XOR(x1, x2):
    s1 = NAND(x1, x2)
    s2 = OR(x1, x2)
    y = AND(s1, s2)
    return y

print(XOR(0, 0))
print(XOR(1, 0))
print(XOR(0, 1))
print(XOR(1, 1))

このように複数の層を重ねたパーセプトロンと多層パーセプトロンと呼び,今回のXORゲートは2層のパーセプトロンとなる。

学習記録:ドワンゴ 新人向けScala研修テキスト

学習記録 関数型 プログラミング Scala

19. Javaとの相互運用

ScalaJava

ScalaJVM上で動作するため,JavaのライブラリのほとんどをそのままScalaから呼び出すことができる。
また,現状ではJavaの機能を利用せざるを得ないことがある。
Javaの機能といっても,Scalaの機能とほとんど同じように利用することができる。

import

Javaのライブラリをimportするためには,Scalaでほとんど同じことを記述すればよい。

import java.util._
import java.util.ArrayList

注意すべきは,Javaでのワイルドカードが*ではなく_になっている点である。

インスタンスの生成

インスタンスの生成もJavaと同様にできる。

scala> import java.util._
import java.util._

scala> val list = new ArrayList[String]()
list: java.util.ArrayList[String] = []

上記は,ArrayList list = new ArrayList<>();に対応する。

インスタンスメソッドの呼び出し

こちらも同様。

scala> list.add("hello")
res0: Boolean = true

scala> list.add("world")
res1: Boolean = true

scala> list
res2: java.util.ArrayList[String] = [hello, world]
staticメソッドの呼び出し

staticメソッドの呼び出しも同様に可能だが,Scalaではstaticメソッドは継承されない(というかstaticメソッドという概念がない)という点に注意が必要である。
つまり,クラスAがstaticメソッドfooを持っていても,Aを継承したクラスBについてB.foo()とすることはできず,A.foo()としなければならない。

scala> System.currentTimeMillis
res3: Long = 1470559909192
staticフィールドの参照

staticフィールドもstaticメソッドと同様継承されない。
Javaであれば,JFrameを継承したクラスでフィールドEXIT_ON_CLOSEを使用できるが,Scalaの場合はJFrame.EXIT_ON_CLOSEと書く必要がある。

Scalaの型とJavaの型のマッピング

Javaの型は適切にScalaマッピングされる。

Javaの型 Scalaの型
void scala.Unit
boolean scala.Boolean
byte, short, int, long, char, float, double scala.Byte, ..., scala.Double
java.lang.Object scala.AnyRef
java.lang.String java.lang.String
nullとOption

Scalaの世界ではnullを使うことはなく,代わりにOption型を使う。
一方,Javaのメソッドを呼び出したりすると,返り値としてnullが返ってくることがある。
幸いにも,ScalaではOption(value)とすることでvalueがnullのときにNoneを,nullでないときにSome(value)を返すことができる。

scala> val map = new java.util.HashMap[String, Int]()
map: java.util.HashMap[String,Int] = {}

scala> map.put("A", 1)
res0: Int = 0

scala> Option(map.get("A"))
res1: Option[Int] = Some(1)

scala> Option(map.get("B"))
res2: Option[Int] = None

このように,Scalaの世界からJavaのメソッドを呼び出すときは,返り値をできるだけOption()でくるむよう意識するべきである。

JavaConverters

JavaのコレクションとScalaのコレクションはインタフェースに互換性がないので,JvaConverterを使う必要がある。

import scala.collection.JavaConverters._

これで,JavaScalaのコレクションのそれぞれにasJava()やasScala()といったメソッドが追加されるので,以下のように呼び出せばよい。

scala> import scala.collection.JavaConverters._
import scala.collection.JavaConverters._

scala> val list = new java.util.ArrayList[String]()
list: java.util.ArrayList[String] = []

scala> list.add("A")
res3: Boolean = true

scala> list.add("B")
res4: Boolean = true

scala> val scalaList = list.asScala
scalaList: scala.collection.mutable.Buffer[String] = Buffer(A, B)

学習記録:ドワンゴ 新人向けScala研修テキスト

学習記録 関数型 プログラミング Scala

18. テスト

ユニットテスト

ここでは,ユニットテストを小さな単位で自動実行できるテストと定義して解説を行う。
ユニットテストを行う理由は大きく3つあげられる。

  1. 実装前に満たすべき仕様を定義し,要件漏れをなくす
  2. 満たすべき仕様がテストされた状態なら,安心してリファクタリングできる
  3. 全ての機能を実装する前に単体でテストできる

スティングフレームワーク

Scalaで広く利用されているテスティングフレームワークは以下の2つ。

  • Specs2
  • ScalaTest

今回は振舞駆動開発(BDD)をサポートしているフレームワークであるScalaTestを使う。
BDDでは,テスト内にそのプログラムに与えられた機能的な外部仕様を記述させることで,テストが本質的に何をテストしようとしているのかをわかりやすくする手法になる。

テストができるsbtプロジェクトの作成

適当な作業ディレクトリにて,src/main/scalaとsrc/test/scalaの2つのディレクトリを作り,以下のbuild.sbtを用意する。

name := "scalatest_study"

version := "1.0"

scalaVersion := "2.11.8"

libraryDependencies += "org.scalatest" %% "scalatest" % "2.2.6" % "test"

その後,sbt compileを実行して準備完了になる。

Calcクラスとテストを作る

下記の仕様を満たすCalcクラスを作成し,それらをテストしていく。

  • 整数の配列を取得し,それらを足し合わせた整数を返すsum関数を持つ
  • 整数を2つ受け取り,分子を分母で割った浮動小数点を返すdiv関数を持つ
  • 整数を1つ受け取り,素数であるかを返すisPrime関数を持つ

この実装は,以下のようになる。

class Calc {
  def sum(seq: Seq[Int]): Int = seq.foldLeft(0)(_ + _)
  def div(numerator: Int, denominator: Int): Double = {
    if (denominator == 0) throw new ArithmeticException("divide by zero")
    numerator.toDouble / denominator.toDouble
  }
  def isPrime(n: Int): Boolean = {
    if (n < 2) false else !((2 to Math.sqrt(n).toInt) exists (n % _ == 0))
  }
}

続いてsum関数のテストを書いてみる。テストクラスにDiagrammedAssertionsをミックスインし,assertメソッドの引数に期待する条件を記述していく。

import org.scalatest._

class CalcSpec extends FlatSpec with DiagrammedAssertions {
  val calc = new Calc

  "sum関数" should "整数の配列を取得し,それらを足し合わせた整数を返すことができる" in {
    assert(calc.sum(Seq(1, 2, 3)) === 6)
    assert(calc.sum(Seq(0)) === 0)
    assert(calc.sum(Seq(-1, 1)) === 0)
    assert(calc.sum(Seq()) === 0)
  }

  it should "Intの最大を上回った際はオーバフローする" in {
    assert(calc.sum(Seq(Integer.MAX_VALUE, 1)) === Integer.MIN_VALUE)
  }
}

sbt testでテストが実行される。わざと失敗させると以下のような表示になる。

[info] CalcSpec:
[info] sum関数
[info] - should 整数の配列を取得し,それらを足し合わせた整数を返すことができる *** FAILED ***
[info]   assert(calc.sum(Seq(-1, 1)) === 6)
[info]          |    |  ||    |  |   |   |
[info]          |    0  ||    -1 1   |   6
[info]          |       |List(-1, 1) false
[info]          |       0
[info]          Calc@d79df17 (CalcSpec.scala:9)
[info] - should Intの最大を上回った際はオーバフローする
[info] Run completed in 260 milliseconds.
[info] Total number of tests run: 2
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 1, failed 1, canceled 0, ignored 0, pending 0
[info] *** 1 TEST FAILED ***
[error] Failed tests:
[error] 	CalcSpec
[error] (test:test) sbt.TestsFailedException: Tests unsuccessful
[error] Total time: 5 s, completed 2016/08/06 18:07:16

成功時には以下のようになる。

[info] CalcSpec:
[info] sum関数
[info] - should 整数の配列を取得し,それらを足し合わせた整数を返すことができる
[info] - should Intの最大を上回った際はオーバフローする
[info] Run completed in 211 milliseconds.
[info] Total number of tests run: 2
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 4 s, completed 2016/08/06 18:12:04

次に,例外が発生するテストを記述してみる。

  "div関数" should "0で割ろうとした際には実行時例外が投げられる" in {
    intercept[ArithmeticException] {
      calc.div(1, 0)
    }
  }

上記ではゼロ除算の際に投げられる例外をテストしている。
intercept[Exception]という構文で作ったスコープ内で投げられる例外がある場合に成功となり,例外がない場合にはテストが失敗する。

最後にパフォーマンスを保証するテストを書く。

import org.scalatest.concurrent.Timeouts
import org.scalatest.time.SpanSugar._

...

class CalcSpec extends FlatSpec with DiagrammedAssertions wi\
th Timeouts {
  ...

  "isPrime関数" should "100万以下の値の素数判定を1秒以内で処\
理できる" in {
    failAfter(1000 millis) {
      assert(calc.isPrime(9999991))
    }
  }
}

Timeoutというトレイトを利用することで,failAfterという処理時間をテストする機能を利用できるようになる。

モック

モックとは,テストする際に必要となるオブジェクトを偽装して用意出来る機能で,以下のようなモックライブラリが存在する。

  • ScalaMock
  • EasyMock
  • JMock
  • Mockito

ここでは,Mockitoを利用してみる。build.sbtに以下を追記することで利用可能になる。

libraryDependencies += "org.mockito" % "mockito-core" % "1.10.19" % "test"

先ほど作成したCalcクラスのモックを用意して,モックにsumの振る舞いを仕込んで見る。

import org.scalatest._
import org.scalatest.concurrent.Timeouts
import org.scalatest.time.SpanSugar._
import org.scalatest.mock.MockitoSugar
import org.mockito.Mockito._

class CalcSpec extends FlatSpec with DiagrammedAssertions wi\
th Timeouts with MockitoSugar {
  ...

  "Calcのモックオブジェクト" should "振舞を偽装できる" in {
    val mockCalc = mock[Calc]
    when(mockCalc.sum(Seq(3, 4, 5))).thenReturn(12)
    assert(mockCalc.sum(Seq(3, 4, 5)) === 12)
  }
}

MockitoSugarトレイトをミックスインすることで,ScalaTest独自の省略記法を用いてMockitoを利用できるようになる。
val = mockCalc = mock[Calc]でモックオブジェクトを作成し,when(...)で振る舞いを作成している。

上記のようなモックの機能は,実際には時間がかかってしまう通信などの部分を高速に動かすために利用されている。
モックを含め,テスト対象が依存しているオブジェクトを置き換える代用品を,総称してテストダブルと呼ぶ。

コードカバレッジの測定

テストが機能のどれくらいを網羅できているのかを知る方法としてコードカバレッジを計測する方法がある。ここでは,scoverageを利用する。

project/plugins.sbtというファイルを作り,以下のコードを記述する。

resolvers += Classpaths.sbtPluginReleases

addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.3.3")

その後,sbt clean coverage testを実行することで,target/scala-2.11/scoverage-report/index.htmlにレポートが出力される。

コードスタイルチェック

ここまで紹介したテストは,実際にはJenkinsなどのCIツールで実施され,リグレッションを検出するために使われる。
その際に一緒に行われることが多いのがコードスタイルチェックである。

ここでは,ScalaStyleを利用する。
project/plugins.sbtに以下のコードを記述する。

addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.6.0")

その後,sbt scalastyleGenerateConfigを一度だけ実行後,sbt scalastyleを実行することで,ルールに即したエラーや警告が表示される。
ルールの変更はscalastyle-config.xmlで行うことができ,デフォルトではApacheライセンスの記述を入れないと警告が出る設定になっている。

学習記録:ドワンゴ 新人向けScala研修テキスト

学習記録 関数型 プログラミング Scala

17. Future/Promiseについて

非同期プログラミングにおいて,終了しているかどうかわからない処理結果を抽象化した型で,Futureは未来の結果を表し,Promiseは一度だけ,成功あるいは失敗を表す処理または値を設定することでFutureに変換できる型になる。

Futureとは

非同期に処理される結果が入ったOption型のようなもので,その処理が終わっているか(isCompleted)や正しく終わった時/例外が起こった時の処理を適用する(onSuccess/onFailure)などが可能。

scala> import scala.concurrent.Future
import scala.concurrent.Future

scala> import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.ExecutionContext.Implicits.global

scala> object FutureSample extends App {
     |   val s = "Hello"
     |   val f: Future[String] = Future {
     |     Thread.sleep(1000)
     |     s + " future!"
     |   }
     |   f.onSuccess { case s: String => println(s) }
     |   println(f.isCompleted)
     |   Thread.sleep(5000)
     |   println(f.isCompleted)
     | }
defined object FutureSample

scala> FutureSample.main(null)
false
Hello future!
true

Futureシングルトンは,関数を与えるとその関数を非同期に与えるFuture[T]を返す。
上記の実装例では,まず1000ミリ秒待機して"Hello" と "future!"を文字列結合という処理を非同期に処理している。
そして成功時の処理を定義した後,Futureの処理が終わっているか確認し,5000ミリ秒待った後,再度結果を確認している。

このように5000ミリ秒待たなくても,そのFuture自体の処理を待つこともできる。Await.ready(f, 5000 millisecond)とすることで,Futureが終わるまで最大5000ミリ秒待つという書き方になる。
ただし,以下のインポート文が必要。

import scala.concurrent.Await
import scala.concurrent.duration._
import scala.language.postfixOps

では,これらを使ってコードを書いてみる。

scala> object FutureSample extends App {
     | 
     |   val s = "Hello"
     |   val f: Future[String] = Future {
     |     Thread.sleep(1000)
     |     println(s"[ThreadName] In Future: ${Thread.currentThread.getName}")
     |     s + " future!"
     | 
     |   }
     |   f.onSuccess { case s: String =>
     |     println(s"[ThreadName] In onSuccess: ${Thread.currentThread.getName}")
     |     println(s)
     |   }
     |   println(f.isCompleted) // false
     |   Await.ready(f, 5000 millisecond) // Hello future!
     |   println(s"[ThreadName] In App: ${Thread.currentThread.getName}")
     |   println(f.isCompleted) // true
     | }
defined object FutureSample

scala> FutureSample.main(null)
false
[ThreadName] In Future: ForkJoinPool-1-worker-5
[ThreadName] In App: run-main-0
true
[ThreadName] In onSuccess: ForkJoinPool-1-worker-5
Hello future!

上記のコードではスレッド名を各所で出力してみている。FutureとonSuccessに渡した関数については,mainスレッドとは異なるスレッドで実行されている。つまり,Futureを使うことで知らないうちにマルチスレッドのプログラミングが実行されていたということになる
また,Await.readyを使うことで,isCompleteの処理(true)の方が"Hello future!"よりも早く出力されているが,これは文字列結合の方が値参照よりコストが高いためである。

ForkJoinPoolに関しては,Javaの並行プログラミングをサポートするExecutorServiceというインタフェースを被ったクラスになる。内部的にスレッドプールを持っており,スレッドを使い回すことでスレッド生成のコストを低減している。

では続いて,FutureがOptionのように扱えることを説明する。

scala> import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.ExecutionContext.Implicits.global

scala> import scala.concurrent.Future
import scala.concurrent.Future

scala> import scala.util.{Failure, Random, Success}
import scala.util.{Failure, Random, Success}

scala> object FutureOptionUsageSample extends App {
     |   val random = new Random()
     |   val waitMaxMilliSec = 3000
     | 
     |   val futureMilliSec: Future[Int] = Future {
     |     val waitMilliSec = random.nextInt(waitMaxMilliSec);
     |     if(waitMilliSec < 1000) throw new RuntimeException(s"waitMilliSec is ${waitMilliSec}" )
     |     Thread.sleep(waitMilliSec)
     |     waitMilliSec
     |   }
     |   val futureSec: Future[Double] = futureMilliSec.map(i => i.toDouble / 1000)
     |   futureSec onComplete {
     |     case Success(waitSec) => println(s"Success! ${waitSec} sec")
     |     case Failure(t) => println(s"Failure: ${t.getMessage}")
     |   }
     |   Thread.sleep(3000)
     |  }
defined object FutureOptionUsageSample

scala> FutureOptionUsageSample.main(null)
Success! 1.667 sec

scala> FutureOptionUsageSample.main(null)
Success! 1.225 sec

scala> FutureOptionUsageSample.main(null)
Failure: waitMilliSec is 952

この処理では,3000以下のランダムな値を生成し,生成した値が1000未満であれば失敗とみなして例外を投げ,そうでなければ生成した値のミリ秒だけ待ってその値を返すFutureを定義している。
この最初に得られるFutureをfutureMilliSecとしてFuture[Int]型にしているが,その後,map型を利用してIntのミリ秒をDoubleの秒に変換している(futureSec)。
なお,先ほどと異なり,onSuccessではなくonCompleteを利用して成功と失敗の両方の処理を記述している。

Futureを使って非同期に取れてくる複数の結果を利用して結果を作る

さて,FutureにはflatMapやfor式が利用できる。これらはよく複数のFutureを組み合わせて新しいFutureを作るのに用いられる。

scala> import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.ExecutionContext.Implicits.global

scala> import scala.concurrent.Future
import scala.concurrent.Future

scala> import scala.language.postfixOps
import scala.language.postfixOps

scala> import scala.util.{Failure, Success, Random}
import scala.util.{Failure, Success, Random}

scala> object CompositeFutureSample extends App {
     |   val random = new Random()
     |   val waitMaxMilliSec = 3000
     |   def waitRandom(futureName: String): Int = {
     |     val waitMilliSec = random.nextInt(waitMaxMilliSec);
     |     if(waitMilliSec < 500) throw new RuntimeException(s"${futureName} waitMilliSec is ${waitMilliSec}" )
     |     Thread.sleep(waitMilliSec)
     |     waitMilliSec
     |   }
     |   val futureFirst: Future[Int] = Future { waitRandom("first") }
     |   val futureSecond: Future[Int] = Future { waitRandom("second") }
     | 
     |   val compositeFuture: Future[(Int, Int)] = for { 
     |     first: Int <- futureFirst
     |     second: Int <- futureSecond
     |   } yield (first, second)
     | 
     |   compositeFuture onComplete  {
     |     case Success((first, second)) => println(s"Success! first:${first} second:${second}")
     |     case Failure(t) => println(s"Failure: ${t.getMessage}")
     |   }
     | 
     |   Thread.sleep(5000)
     |  }
defined object CompositeFutureSample

scala> CompositeFutureSample.main(null)
Success! first:2085 second:1574

scala> CompositeFutureSample.main(null)
Failure: first waitMilliSec is 349

scala> CompositeFutureSample.main(null)
Failure: second waitMilliSec is 390

ランダムで最大3秒待つ関数を用意し,500ミリ秒未満の場合は失敗とみなしている。その関数を実行する関数をFutureとして2つ用意し,それらをfor式で畳み込んで新しいFutureを作っている。そして最終的に,新しいFutureに対して成功した場合と失敗した場合を出力する。

Promiseとは

成功あるいは失敗を表す処理または値を設定することにより,Futureに変換することのできるクラスである。

scala> import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.ExecutionContext.Implicits.global

scala> import scala.concurrent.{Promise, Future}
import scala.concurrent.{Promise, Future}

scala> import scala.util.{Success, Failure, Random}
import scala.util.{Success, Failure, Random}

scala> object PromiseSample extends App {
     |   val random = new Random()
     |   val promiseGetInt: Promise[Int] = Promise[Int]
     |   val futureGetInt: Future[Int] = promiseGetInt.success(random.nextInt(1000)).future
     |   futureGetInt.onComplete {
     |     case Success(i) => println(s"Success! i: ${i}")
     |     case Failure(t) => println(s"Failure! t: ${t.getMessage}")
     |   }
     | 
     |   Thread.sleep(1000)
     | }
defined object PromiseSample

scala> PromiseSample.main(null)
Success! i: 785

scala> PromiseSample.main(null)
Success! i: 269

Promiseに成功を表すsuccess(random.nextInt())を与えることによって,この処理は必ずSuccess! i: <ランダムな値> という値を返す。

この1度だけしか結果ができようされないという特性を活かして,以下のような実装が可能である。なお,複数successが定義される場合,trySuccessというPromiseのメソッドを利用する。successを利用して複数回成功した値を定義した場合,IllegalStateExceptionが投げられる。

scala> object PromiseFutureCompositionSample extends App {
     |   val promiseGetInt: Promise[Int] = Promise[Int]
     | 
     |   val firstFuture: Future[Int] = Future {
     |     Thread.sleep(100)
     |     1
     |   }
     |   firstFuture.onSuccess{ case i => promiseGetInt.trySuccess(i)}
     | 
     |   val secondFuture: Future[Int] = Future {
     |     Thread.sleep(200)
     |     2
     |   }
     |   secondFuture.onSuccess{ case i => promiseGetInt.trySuccess(i)}
     | 
     |   val futureGetInt: Future[Int] = promiseGetInt.future
     | 
     |   futureGetInt.onComplete {
     |     case Success(i) => println(s"Success! i: ${i}")
     |     case Failure(t) => println(s"Failure! t: ${t.getMessage}")
     |   }
     | 
     |   Thread.sleep(1000)
     | }
defined object PromiseFutureCompositionSample

scala> PromiseFutureCompositionSample.main(null)
Success! i: 1

scala> PromiseFutureCompositionSample.main(null)
Success! i: 1

結果は必ず1になる。100ミリ秒待って1を返すfirstFutureと,200ミリ秒待って2を返すsecondFutureが定義されているが,時系列的にfirstFutureがpromiseのfutureを完成させる役割をするため,必ず1が変える仕組みになっている。

学習記録:ドワンゴ 新人向けScala研修テキスト

学習記録 関数型 プログラミング Scala

15. Implicit

Scalaにはimplicit Conversion(暗黙の型変換)とimplicit parameter(暗黙のパラメータ)という機能がある。
この2つを使いこなすことで,Scalaでのプログラミングの生産性は劇的に向上する。

Implicit Conversion

暗黙の型変換機能をユーザが定義できるようにする機能。
implicitというキーワードと引数が1つだけのことを除けば通常のメソッド定義と同じになる。

implicit def メソッド名(引数名: 引数の型): 返り値の型 = 本体

上記の定義により,引数の型の式が現れたとき,返り値の型を暗黙の型変換候補として登録することになる。
定義したimplicit conversionは大きく分けて2通りの使われ方をする。
1つは,新しく定義したユーザ定義の型などを既存の型に当てはめたい場合になる。

scala> implicit def intToBoolean(arg: Int): Boolean = arg != 0
<console>:12: warning: implicit conversion method intToBoolean should be enabled
...
intToBoolean: (arg: Int)Boolean

scala> if (1) {
     |   println("1 == true")
     | }
1 == true

こうすることで,本来Booleanしか渡せないはずのif式にIntを渡すことができる。しかし,Boolean型の式しか渡せないようにコンパイラがチェックしている部分を通りぬけてしまうことができることから,この使い方はあまり良いものではない。

pimp my library

もう1つの使い方はpimp my libraryパターンと呼ばれ,既存のクラスにメソッドを追加して拡張する(ようにみせる)使い方で,こちらが本来の使い方といってもよい。
例えば,(1 to 5)という式のtoメソッドはこのパターンの最たる例で,本来Int型が持たないtoメソッドを使えるようにしている。

試しに,Stringの末尾に"^^"という文字列を追加して返すimplicit conversionを定義してみる。

scala> class RichString(val src: String) {
     |   def smile: String = src + "^^"
     | }
defined class RichString

scala> implicit def enrichString(arg: String): RichString = new RichString(arg)
<console>:14: warning: implicit conversion method enrichString should be enabled
enrichString: (arg: String)RichString

scala> "Hi, ".smile
res2: String = Hi, ^^

ちゃんと文字列の末尾に"^^"を追加するsmileメソッドが定義できている。
コンパイラは,ある型に対するメソッド呼び出しを見つけたとき,そのメソッドを定義した型がimplicit conversionの返り値の型にないか探索し,型が合ったらimplicit Conversionの呼び出しを挿入する。

Implicit Class

上の定義は,Scala2.10以降ではclassにimplicitというキーワードをつけて書くことができる。

scala> implicit class RichString(val src: String) {
     |   def smile: String = src + "^^"
     | }
defined class RichString

scala> "uwaa".smile
res0: String = uwaa^^

implicit classはpimp my libraryパターン専用の機能で,implicit defで既存型への変換をした場合などによる混乱がないため,pimp my libraryパターンを使うときにはこちらの形式にした方がよい。

Implicit Parameter

implicit parameterの使い方も2つあり,1つ目はあちこちのメソッドに共通で引き渡されるオブジェクト(ソケットやコネクションなど)を明示的に引き渡すのを省略するために使うものになる。
例えば,データベースとのコネクションを表すConnection型があり,データベースと接続するメソッドは全てこのConnection型を引き渡す必要があるとする。

def useDatabase1(..., conn: Connection)
def useDatabase2(..., conn: Connection)
...

これらのメソッドは呼び出す度に明示的にConnectionオブジェクトを渡さなければならないのが面倒である。そこで,

def useDatabase1(...)(implicit conn: Connection)
def useDatabase2(...)(implicit conn: Connection)
...

とする。implicit修飾子は引数の先頭要素に付けなければならず,またカリー化されたメソッド定義が必要になる。
最後の引数リストが(implicit conn: Connection)とあるのがポイントで,Scalaコンパイラはこう定義されたメソッドが呼び出されると,現在のスコープから辿って直近のimplicitとマークされた値を暗黙にメソッドを引き渡す。
「値をimplicitとしてマークする」とは次のようにして行う。

implicit val connection: Connection = connectDatabase(...)

こうすることで,最後の引数リストに暗黙にConnectionオブジェクトを渡してくれる。このような使い方は,各種O/Rマッパーで頻出する。

scala> case class Connection(connStr: String)
defined class Connection

scala> def useDatabase(implicit conn: Connection)
     | = { 
     |   println("use: " + conn.connStr)
     | }
useDatabase: (implicit conn: Connection)Unit

scala> implicit val conn: Connection = new Connection("hogeDb")
conn: Connection = Connection(hogeDb)

scala> useDatabase
use: hogeDb

もう一つの使い方は少々変わっている。例えばListの全ての要素を加算するsumメソッドを定義したいとする。このメソッドのポイントは,何のリストか全くわかっていないため,整数の+メソッドをそのまま使ったりすることはできないということだ。このような場合,二つの手順を踏む。

まず,2つの同じ型を足す方法を知っている型,Additiveを定義する。

scala> trait Additive[A] {
     |   def plus(a: A, b: A): A
     |   def zero: A
     | }
defined trait Additive

ここで,型パラメータAは加算されるListの要素の型を表す。また,zero: Aの0に相当する値を返す,plus: Aを持つ2つの値を加算して返す,というメソッドを持つ。

次に,このAdditiveを使ってListの全ての要素を合計するメソッドを定義する。

scala> def sum[A](lst: List[A])(m: Additive[A]) = lst.foldLeft(m.zero)((x, y) => m.plus(x, y))
sum: [A](lst: List[A])(m: Additive[A])A

最後に,それぞれの型に応じた加算と0の定義を持ったobjectを定義する。

scala> object StringAdditive extends Additive[String] {
     |   def plus(a: String, b: String): String = a + b
     |   def zero: String = ""
     | }
defined object StringAdditive

scala> object IntAdditive extends Additive[Int] {
     |   def plus(a: Int, b: Int): Int = a + b
     |   def zero: Int = 0
     | }
defined object IntAdditive

さて,このsumメソッドを呼び出したいときには,以下のようにすればよい。

scala> sum(List(1,3,5))(IntAdditive)
res8: Int = 9

scala> sum(List("a","bb","ccc"))(StringAdditive)
res10: String = abbccc

しかし,Listの要素の型は型チェックする時点でわかっているのだから,わざわざIntAdditiveなどと明示的に渡さなくても推論してほしいものだ。そこで,implicit parameterを使う。
方法は簡単で,String/IntAdditiveの定義の前にimplicitと付け,sumの最後の引数のリストのmにimplicitを付けるだけである。

implicit object IntAdditive extends Additive[Int] { ... }
implicit object StringAdditive extends Additive[String] { ... }
def sum[A](lst: List[A])(implicit m: Additive[A]) = lst.foldLeft(m.zero)((x, y) => m.plus(x, y))

scala> sum(List(1,5,9))
res11: Int = 15

scala> sum(List("Alice","Bob","Curl"))
res12: String = AliceBobCurl