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

CUBE SUGAR CONTAINER

技術系のこと書きます。

Mac OS X で Apache Kafka を触ってみる

Apache Kafka は OSS の分散型メッセージングミドルウェア。 似た性質を持ったソフトウェアとしては ActiveMQRabbitMQ などが挙げられる。 ただし、ActiveMQ や RabbitMQ との大きな違いは、独自のバイナリプロトコルを用いてメッセージをやり取りするところ。 ActiveMQ や RabbitMQ は標準化された AMQPMQTT を扱う場合が多い。 独自プロトコルというと、なんだか未来が無さそうなイメージがあるけど、その逆で Kafka はビッグデータ処理の場面ではほぼデファクトの位置にあるようだ。

今回は、そんな Kafka を手元の Mac でさくっと試してみることにする。 使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.12.4
BuildVersion:   16E195

インストールする

Kafka は Homebrew でインストールできる。 なので、まずは Homebrew をインストールしておく。 その上で Homebrew Cask についても使えるようにしておく。

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
$ brew tap caskroom/cask

Kafka は Java と Scala を使って書かれている。 なので、まずは動作に必要な Java の処理系をインストールする。

$ brew cask install java

あとはお目当ての Kafka をインストールするだけ。

$ brew install kafka

このとき、依存パッケージとして Apache ZooKeeper もインストールされる。

サービスを動かす

続いてはインストールした Kafka を動作させる。 これには Homebrew のサービス機能を使うと楽ができる。

インストールした直後では Kafka と ZooKeeper は動いていない。

$ brew services list
Name      Status  User Plist
kafka     stopped
zookeeper stopped

そこで ZooKeeper と Kafka を順番に立ち上げていく。

$ brew services start zookeeper
$ brew services start kafka

両方のサービスが立ち上がればオッケー。

$ brew services list           
Name      Status  User    Plist
kafka     started amedama /Users/amedama/Library/LaunchAgents/homebrew.mxcl.kafka.plist
zookeeper started amedama /Users/amedama/Library/LaunchAgents/homebrew.mxcl.zookeeper.plist

ちなみに、上記の機能を使わなくても次のようにしてコマンドラインから起動することもできる。

$ zookeeper-server-start /usr/local/etc/kafka/zookeeper.properties
$ kafka-server-start /usr/local/etc/kafka/server.properties

もし、上手く立ち上がらないときは、それぞれのログを確認しよう。 ログはデフォルトで /usr/local/var/log に保存されている。

同梱されているコマンドで Kafka を操作してみる

Kafka には操作するためのコマンドラインツールが同梱されている。 今回は、それを使ってみる。

トピックを作る

まずは、動作確認用のトピックを作成する。 トピックは Pub/Sub 型のメッセージングミドルウェアによく登場する概念だ。 これは、例えば扱うメッセージの種類やコンポーネントごとに用意する。

$ kafka-topics --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test
Created topic "test".

--replication-factor オプションはクラスタ内でのメッセージのレプリケーション数を表している。 今回はシングルホスト構成なので必然的に 1 となる。 --partitions オプションはトピックでメッセージを分散処理するための機能の指定で、これを使った例については後ほど紹介する。

まずは、これで動作確認用の test トピックができた。

$ kafka-topics --list --zookeeper localhost:2181
test

メッセージを読み書きする

次に、作成したトピックに対してコンシューマ、つまりメッセージをトピックから読み出す存在を接続する。

$ kafka-console-consumer --bootstrap-server localhost:9092 --topic test --from-beginning

続いて、作成したトピックに対してプロデューサ、つまりメッセージをトピックに書き込む存在を接続する。 ターミナルから、何か適当にメッセージを書き込んでみよう。

$ kafka-console-producer --broker-list localhost:9092 --topic test
Hello, World!

すると、先ほど接続したコンシューマに、プロデューサが書き込んだメッセージが送られる。

$ kafka-console-consumer --bootstrap-server localhost:9092 --topic test --from-beginning
Hello, World!

ちなみに、Kafka のメッセージはデフォルトで永続化されている。 例えば、Ctrl-C で一旦コンシューマを停止させてから、もう一度立ち上げてみよう。

$ kafka-console-consumer --bootstrap-server localhost:9092 --topic test --from-beginning
Hello, World!

ちゃんと、先ほど受信したのと同じ内容が表示された。 これは、Kafka を再起動しても同じ結果になるので試してみると面白い。

ちなみに、コマンドを --from-beginning オプションを付けずに実行すれば、途中から受信できる。

$ kafka-console-consumer --bootstrap-server localhost:9092 --topic test

複数のコンシューマでメッセージを分散処理する

これまでの例では、トピックを作るときにパーティションを 1 に指定した動作を試してきた。 次は、複数のコンピューマでメッセージを分散処理するために、トピックに複数のパーティーションを作ってみる。

今度はトピック greet をパーティーション数 2 で作成する。

$ kafka-topics --create --zookeeper localhost:2181 --replication-factor 1 --partitions 2 --topic greet
Created topic "greet".

続いては、作成したトピックにコンシューマを接続する。 このとき、自身が受信するパーティションを --partition オプションで指定する。 一つ目のターミナルでは、まずは 0 を指定しておく。

$ kafka-console-consumer --bootstrap-server localhost:9092 --partition 0 --topic greet --from-beginning

続いて別のターミナルを開いて、パーティーションが 1 のコンシューマを接続する。

$ kafka-console-consumer --bootstrap-server localhost:9092 --partition 1 --topic greet --from-beginning

次はトピックにプロデューサを接続して、適当にメッセージを送ってみよう。

$ kafka-console-producer --broker-list localhost:9092 --topic greet
Message1
Message2

すると、送ったメッセージがバラバラにコンシューマに届くことが分かる。 まず、最初のメッセージはパーティーション 0 のコンシューマに送られた。

$ kafka-console-consumer --bootstrap-server localhost:9092 --partition 0 --topic greet --from-beginning
Message1

そして、二番目のメッセージはパーティーション 1 のコンシューマに送られている。

$ kafka-console-consumer --bootstrap-server localhost:9092 --partition 1 --topic greet --from-beginning
Message2

このように、パーティーションの機能を使うことでメッセージを複数のコンシューマで分散処理できるようになっている。

まとめ

今回は Mac OS X を使って Apache Kafka を軽く触ってみた。 実運用に載せるなら色々と考えるところはあるけど、手元の検証用としてなら簡単に環境を用意して触れることが分かった。 Apache Kafka はビッグデータ処理のメッセージングミドルウェアとしてはデファクトの存在なので引き続き知見を貯めていきたい。

Mac OS X に GNU Octave をインストールする

GNU Octave 数値計算や解析に使われるプログラミング言語の一つ。 あの MATLAB とほぼ互換性があるんだとか。 今回 CourseraMachine Learning コースを受ける上で必要だったためインストールすることにした。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.12.4
BuildVersion:   16E195

インストールは Homebrew でするので、あらかじめ入れておく。

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

同時に Homebrew Cask も入れる。

$ brew tap caskroom/cask

まずは Homebrew Cask を使って XQuartz をインストールする。 GNU Octave には GUI が付属していて、それが X Window System を使うため。

$ brew cask install xquartz

次に GNU Octave の入っている science リポジトリをタップする。

$ brew tap homebrew/science

あとは Homebrew でインストールするだけ。 ただ、依存パッケージがやたら多いので気長に待つ。

$ brew install octave

インストールできたら octave コマンドで実行環境を起動する。

$ octave

すると、初めは次のようなウィンドウが表示されるので初期設定的なものを進める。 f:id:momijiame:20170428232310p:plain

そのまま進めていくとメインの画面に到達する。 f:id:momijiame:20170428232317p:plain

あとはメインのペインで開発するだけ。

GNU Octave, version 4.2.1
Copyright (C) 2017 John W. Eaton and others.
This is free software; see the source code for copying conditions.
There is ABSOLUTELY NO WARRANTY; not even for MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE.  For details, type 'warranty'.

Octave was configured for "x86_64-apple-darwin16.4.0".

Additional information about Octave is available at http://www.octave.org.

Please contribute if you find this software useful.
For more information, visit http://www.octave.org/get-involved.html

Read http://www.octave.org/bugs.html to learn how to submit bug reports.
For information about changes from previous versions, type 'news'.

>>

めでたしめでたし。

Python: scikit-learn で決定木 (Decision Tree) を試してみる

今回は機械学習アルゴリズムの一つである決定木を scikit-learn で試してみることにする。 決定木は、その名の通り木構造のモデルとなっていて、分類問題ないし回帰問題を解くのに使える。 また、決定木自体はランダムフォレストのような、より高度なアルゴリズムのベースとなっている。

使うときの API は scikit-learn が抽象化しているので、まずは軽く触ってみるところから始めよう。 決定木がどんな構造を持ったモデルなのかは最後にグラフで示す。 また、決定木自体は回帰問題にも使えるけど、今回は分類問題だけにフォーカスしている。

使った環境は次の通り。

$ sw_vers    
ProductName:    Mac OS X
ProductVersion: 10.12.4
BuildVersion:   16E195
$ python --version
Python 3.5.3

下準備

まずは、今回のサンプルコードを動かすのに必要な Python のパッケージをインストールしておく。

$ pip install scipy scikit-learn matplotlib

アイリスデータセットを分類してみる

まずは定番のアイリス (あやめ) データセットを決定木で分類してみることにする。 といっても scikit-learn を使う限りは、分類器が違っても API は同じなので使用感は変わらない。

次のサンプルコードではアイリスデータセットに含まれる三種類の花の品種を決定木で分類している。 モデルの汎化性能は LOO 法を使って計算した。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import LeaveOneOut
from sklearn.metrics import accuracy_score


def main():
    # アイリスデータセットを読み込む
    dataset = datasets.load_iris()

    # 教師データとラベルデータを取り出す
    features = dataset.data
    targets = dataset.target

    # 判定したラベルデータを入れるリスト
    predicted_labels = []
    # LOO 法で汎化性能を調べる
    loo = LeaveOneOut()
    for train, test in loo.split(features):
        # 学習に使うデータ
        train_data = features[train]
        target_data = targets[train]

        # モデルを学習させる
        clf = DecisionTreeClassifier()
        clf.fit(train_data, target_data)

        # テストに使うデータを正しく判定できるか
        predicted_label = clf.predict(features[test])
        predicted_labels.append(predicted_label)

    # テストデータでの正解率 (汎化性能) を出力する
    score = accuracy_score(targets, predicted_labels)
    print(score)


if __name__ == '__main__':
    main()

上記を実行すると、次のような結果が得られる。 約 95.3% の汎化性能が得られた。 ただし、決定木はどんな木構造になるかが毎回異なるので汎化性能も微妙に異なってくる。

0.953333333333

ハイパーパラメータを調整する

機械学習アルゴリズムで、人間が調整してやらなきゃいけないパラメータのことをハイパーパラメータという。 決定木では、木構造の深さがモデルの複雑度を調整するためのハイパーパラメータになっている。 深いものはより複雑で、浅いものはより単純なモデルになる。

次のサンプルコードは、決定木の深さを指定した数に制限した状態での汎化性能を示すものになっている。 具体的な深さについては 1 ~ 20 を順番に試行している。 ちなみに、指定できるのは「最大の深さ」なので、できあがる木構造がそれよりも浅いということは十分にありうる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from matplotlib import pyplot as plt

from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import LeaveOneOut
from sklearn.metrics import accuracy_score


def main():
    dataset = datasets.load_iris()

    features = dataset.data
    targets = dataset.target

    # 調べる深さ
    MAX_DEPTH = 20
    depths = range(1, MAX_DEPTH)

    # 決定木の最大深度ごとに正解率を計算する
    accuracy_scores = []
    for depth in depths:

        predicted_labels = []
        loo = LeaveOneOut()
        for train, test in loo.split(features):
            train_data = features[train]
            target_data = targets[train]

            clf = DecisionTreeClassifier(max_depth=depth)
            clf.fit(train_data, target_data)

            predicted_label = clf.predict(features[test])
            predicted_labels.append(predicted_label)

        # 各深度での汎化性能を出力する
        score = accuracy_score(targets, predicted_labels)
        print('max depth={0}: {1}'.format(depth, score))

        accuracy_scores.append(score)

    # 最大深度ごとの正解率を折れ線グラフで可視化する
    X = list(depths)
    plt.plot(X, accuracy_scores)

    plt.xlabel('max depth')
    plt.ylabel('accuracy rate')
    plt.show()


if __name__ == '__main__':
    main()

上記の実行結果は次の通り。 前述した通り決定木がどんな木構造になるかは毎回異なるので、これも毎回微妙に異なるはず。

max depth=1: 0.3333333333333333
max depth=2: 0.9533333333333334
max depth=3: 0.9466666666666667
max depth=4: 0.9466666666666667
max depth=5: 0.9466666666666667
max depth=6: 0.9466666666666667
max depth=7: 0.9466666666666667
max depth=8: 0.94
max depth=9: 0.9533333333333334
max depth=10: 0.94
max depth=11: 0.9533333333333334
max depth=12: 0.9466666666666667
max depth=13: 0.9466666666666667
max depth=14: 0.94
max depth=15: 0.94
max depth=16: 0.9466666666666667
max depth=17: 0.96
max depth=18: 0.9466666666666667
max depth=19: 0.9466666666666667

同時に、次のような折れ線グラフが得られる。 どうやら、今回のケースでは最大の深さが 3 以上であれば、汎化性能はどれもそんなに変わらないようだ。 f:id:momijiame:20170425221114p:plain

どのように分類されているのか可視化してみる

先ほどは深さによって汎化性能がどのように変わってくるかを見てみた。 今回扱うデータセットでは 3 以上あれば汎化性能にはさほど大きな影響がないらしいことが分かった。 次は、木構造の深さ (つまりモデルの複雑度) によって分類のされ方がどのように変わるのかを見てみたい。

次のサンプルコードでは、二次元の散布図を元に分類される様子を見るために教師データを二次元に絞っている。 具体的には、データセットの教師データの中から「Petal length」と「Petal width」だけを取り出して使っている。 その上で、それぞれを x 軸と y 軸にプロットした。 また、同時にどの点がどの品種として分類されているかを背景に色付けしている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np

import matplotlib.pyplot as plt

from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier


def main():
    dataset = datasets.load_iris()

    features = dataset.data
    targets = dataset.target

    # Petal length と Petal width だけを特徴量として使う (二次元で図示したいので)
    petal_features = features[:, 2:]

    # 決定木の最大深度は制限しない
    clf = DecisionTreeClassifier()
    clf.fit(petal_features, targets)

    # 教師データの取りうる範囲 +-1 を計算する
    train_x_min = petal_features[:, 0].min() - 1
    train_y_min = petal_features[:, 1].min() - 1
    train_x_max = petal_features[:, 0].max() + 1
    train_y_max = petal_features[:, 1].max() + 1

    # 教師データの取りうる範囲でメッシュ状の座標を作る
    grid_interval = 0.2
    xx, yy = np.meshgrid(
        np.arange(train_x_min, train_x_max, grid_interval),
        np.arange(train_y_min, train_y_max, grid_interval),
    )

    # メッシュの座標を学習したモデルで判定させる
    Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
    # 各点の判定結果をグラフに描画する
    plt.contourf(xx, yy, Z.reshape(xx.shape), cmap=plt.cm.bone)

    # 教師データもプロットしておく
    for c in np.unique(targets):
        plt.scatter(petal_features[targets == c, 0],
                    petal_features[targets == c, 1])

    feature_names = dataset.feature_names
    plt.xlabel(feature_names[2])
    plt.ylabel(feature_names[3])
    plt.show()


if __name__ == '__main__':
    main()

このモデルについては木構造の深さを制限していない。

上記を実行すると、次のような散布図が得られる。

f:id:momijiame:20170425221614p:plain

次は、上記のサンプルコードに木構造の深さの制限を入れてみよう。 とりあえず最大の深さを 3 までに制限してみる。 前述した通り、こうしても汎化性能自体には大きな影響はないようだった。 分類のされ方には変化が出てくるだろうか?

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np

import matplotlib.pyplot as plt

from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier


def main():
    dataset = datasets.load_iris()

    features = dataset.data
    targets = dataset.target

    petal_features = features[:, 2:]

    # 決定木の深さを 3 までに制限する
    clf = DecisionTreeClassifier(max_depth=3)
    clf.fit(petal_features, targets)

    train_x_min = petal_features[:, 0].min() - 1
    train_y_min = petal_features[:, 1].min() - 1
    train_x_max = petal_features[:, 0].max() + 1
    train_y_max = petal_features[:, 1].max() + 1

    grid_interval = 0.2
    xx, yy = np.meshgrid(
        np.arange(train_x_min, train_x_max, grid_interval),
        np.arange(train_y_min, train_y_max, grid_interval),
    )
    Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
    plt.contourf(xx, yy, Z.reshape(xx.shape), cmap=plt.cm.bone)

    for c in np.unique(targets):
        plt.scatter(petal_features[targets == c, 0],
                    petal_features[targets == c, 1])

    feature_names = dataset.feature_names
    plt.xlabel(feature_names[2])
    plt.ylabel(feature_names[3])
    plt.show()


if __name__ == '__main__':
    main()

上記を実行すると、次のような散布図が得られる。

f:id:momijiame:20170425222036p:plain

先ほどの例と比べてみよう。 オレンジ色の品種が緑色の品種のところに食い込んでいるところが、このケースでは正しく認識されなくなっている。 モデルがより単純になったと考えられるだろう。

木構造を可視化してみる

scikit-learn には決定木の構造を DOT 言語で出力する機能がある。 その機能を使って木構造を可視化してみることにしよう。

まずは DOT 言語を処理するために Graphviz をインストールする。

$ brew install graphviz

そして、次のように学習させたモデルから DecisionTreeClassifier#export_graphviz() メソッドで DOT 言語で書かれたファイルを出力させる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from sklearn import datasets
from sklearn.tree import DecisionTreeClassifier
from sklearn import tree


def main():
    dataset = datasets.load_iris()

    features = dataset.data
    targets = dataset.target

    # Petal length と Petal width だけを特徴量として使う
    petal_features = features[:, 2:]

    # モデルを学習させる
    clf = DecisionTreeClassifier(max_depth=3)
    clf.fit(petal_features, targets)

    # DOT 言語のフォーマットで決定木の形を出力する
    with open('iris-dtree.dot', mode='w') as f:
        tree.export_graphviz(clf, out_file=f)


if __name__ == '__main__':
    main()

これを Graphviz で画像データに変換する。

$ dot -T png iris-dtree.dot -o iris-dtree.png

すると、次のようなグラフが得られる。

f:id:momijiame:20170425223006p:plain

グラフでは、葉ノード以外が分類をするための分岐になっている。 これは、ようするに木構造が深くなるに従ってだんだんと対象を絞り込んでいっていることを意味する。 例えば、最初の分岐では x 軸が 2.45 未満のところで分岐している。 そして、左側の葉ノードは青色の品種が全て集まっていることが分かる。

まとめ

  • 今回は決定木を scikit-learn で試してみた
  • 決定木はランダムフォレストのようなアルゴリズムのベースとなっている
  • 決定木のモデルの複雑さは木構造の深さで制御する
  • 木構造の深さが浅くなるほど分類のされ方も単純になった

はじめてのパターン認識

はじめてのパターン認識

Python: SQLAlchemy の生成する SQL をテストするパッケージを作ってみた

SQLAlchemy は Python でよく使われている O/R マッパーの一つ。 今回は、そんな SQLAlchemy が生成する SQL 文を確認するためのパッケージを作ってみたよ、という話。

具体的には、以下の sqlalchemy-profile というパッケージを作ってみた。 このエントリでは、なんでこんなものを作ったのかみたいな話をしてみる。

github.com

使った環境は次の通り。 ただし sqlalchemy-profile 自体はプラットフォームに依存せず Python 2.7, 3.3 ~ 3.6 に対応している。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.12.4
BuildVersion:   16E195
$ python --version
Python 3.6.1

O/R マッパーについて

O/R マッパーというのは、プログラミング言語からリレーショナルデータベース (RDB) を良い感じに使うための機能ないしライブラリの総称。 プログラミング言語から RDB を操作するための SQL 文を直に扱ってしまうと、両者のパラダイムの違いから色々な問題が起こる。 この問題は、一般にインピーダンスミスマッチと呼ばれている。 そこで登場するのが O/R マッパーで、これを使うとプログラミング言語のオブジェクトを操作する形で RDB を操作できるようになる。

論よりソースということで、まずは SQLAlchemy の基本的な使い方から見てみよう。 その前に SQLAlchemy 自体をインストールしておく。

$ pip install sqlalchemy

そして次に示すのがサンプルコード。 ユーザ情報を模したモデルクラスを用意して、それを SQLite のオンメモリデータベースで永続化している。 この中には SQL 文が全く登場していないところがポイントとなる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from sqlalchemy.ext import declarative
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import Text
from sqlalchemy import create_engine
from sqlalchemy.orm.session import sessionmaker

Base = declarative.declarative_base()


class User(Base):
    """SQLAlchemy のモデルクラス

    このクラスが RDB のテーブルと対応し、インスタンスはテーブルの一レコードに対応する
    ここではユーザの情報を格納するテーブルを模している"""
    __tablename__ = 'users'

    # テーブルの主キー
    id = Column(Integer, primary_key=True)
    # 名前を入れるカラム
    name = Column(Text, nullable=False)


def main():
    # データベースとの接続に使う情報
    # ここでは SQLite のオンメモリデータベースを使う
    # echo=True とすることで生成される SQL 文を確認できる
    engine = create_engine('sqlite:///', echo=True)
    # モデルの情報を元にテーブルを生成する
    Base.metadata.create_all(engine)
    # データベースとのセッションを確立する
    session_maker = sessionmaker(bind=engine)
    session = session_maker()

    # データベースのトランザクションを作る
    with session.begin(subtransactions=True):
        # レコードに対応するモデルのインスタンスを作る
        user = User(name='Alice')
        # そのインスタンスをセッションに追加する
        session.add(user)

    # トランザクションがコミットされてオブジェクトが RDB で永続化される

if __name__ == '__main__':
    main()

上記のサンプルコードでは生成される SQL 文を標準出力に表示するようにしている。 なので、実行するとこんな感じの出力が得られる。

2017-04-20 04:48:30,976 INFO sqlalchemy.engine.base.Engine SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
2017-04-20 04:48:30,976 INFO sqlalchemy.engine.base.Engine ()
2017-04-20 04:48:30,978 INFO sqlalchemy.engine.base.Engine SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
2017-04-20 04:48:30,978 INFO sqlalchemy.engine.base.Engine ()
2017-04-20 04:48:30,980 INFO sqlalchemy.engine.base.Engine PRAGMA table_info("users")
2017-04-20 04:48:30,980 INFO sqlalchemy.engine.base.Engine ()
2017-04-20 04:48:30,982 INFO sqlalchemy.engine.base.Engine 
CREATE TABLE users (
    id INTEGER NOT NULL, 
    name TEXT NOT NULL, 
    PRIMARY KEY (id)
)


2017-04-20 04:48:30,983 INFO sqlalchemy.engine.base.Engine ()
2017-04-20 04:48:30,984 INFO sqlalchemy.engine.base.Engine COMMIT
2017-04-20 04:48:30,987 INFO sqlalchemy.engine.base.Engine BEGIN (implicit)
2017-04-20 04:48:30,989 INFO sqlalchemy.engine.base.Engine INSERT INTO users (name) VALUES (?)
2017-04-20 04:48:30,989 INFO sqlalchemy.engine.base.Engine ('Alice',)

たしかに Python のオブジェクトを使うだけで RDB を操作できた。便利。 ただし、上記で使った生成した SQL 文を出力する機能はデバッグ用途なので普段は無効にされる場合が多い。

SQL が隠蔽されることのメリットとデメリット

先ほど見た通り O/R マッパーを使うと Python のオブジェクトを通して RDB を操作できるようになる。 これにはインピーダンスミスマッチの解消という多大なメリットがある反面、生成される SQL が隠蔽されるというデメリットもある。

例えば、直接 SQL を書くならそんな非効率なクエリは組まないよね・・・というような内容も、気をつけていないと生成されうる。 これは、典型的には N + 1 問題とか。 それを防ぐには、これまでだとコードから生成される SQL 文を推測したり、あるいは先ほどのようにして実際に目で見て確かめていた。 慣れてくるとどんな SQL 文が発行されるか分かってくるのと、実際に目で見て確かめるのは手間なので大体は前者になっている。

ただ、パフォーマンスチューニングの世界では、推測する前に測定せよという格言もある。 実際に生成される SQL 文を、ユニットテストで確認できるようになっているべきなのでは、という考えに至った。 それが、今回作ったパッケージ sqlalchemy-profile のモチベーションになっている。

ただし、どんな SQL 文が生成されるかは SQLAlchemy のアルゴリズム次第なので、気をつけないとテストのメンテナンス性が低下する恐れはあると思う。 これは、SQLAlchemy のバージョン変更とか、些細なモデルの構造変更でテストを修正する手間がかかるかも、ということ。 とはいえ、それはそれで生成される SQL が変更されたことにちゃんと気づけるのは大事じゃないかという感じでいる。

sqlalchemy-profile について

やっと本題に入るんだけど、前述した問題を解消すべく sqlalchemy-profile という Python のパッケージを作ってみた。 これを使うことで、SQLAlchemy が生成する SQL 文を確かめることができる。

Python のパッケージリポジトリである PyPI にも登録しておいた。 pypi.python.org

インストールは Python のパッケージマネージャの PIP からできる。

$ pip install sqlalchemy-profile

使い方

ここからは sqlalchemy-profile の具体的な使い方について見ていく。 シンプルなのでサンプルコードをいくつか見れば、すぐに分かってもらえると思う。 ちなみに、トラッキングしている SQL 文は今のところ INSERT, UPDATE, SELECT, DELETE の四つ。

以下のサンプルコードでは、最も基本的な使い方を示している。 まず、プロファイラとなる StatementProfiler には SQLAlchemy のデータベースとの接続情報を渡す。 そして、プロファイルしている期間中に実行された SQL 文を記録する、というもの。 ユニットテストで利用することを意図しているので、サンプルコードも Python の unittest モジュールを使うものにしてみた。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import unittest

from sqlalchemy import create_engine

from sqlalchemy_profile import StatementProfiler


class Test_RawExecute(unittest.TestCase):

    def test(self):
        # データベースとの接続を確立する
        engine = create_engine('sqlite:///')
        connection = engine.connect()

        # データベースとの接続情報を渡してプロファイラをインスタンス化する
        profiler = StatementProfiler(engine)
        # プロファイルを開始する
        profiler.start()

        # SQLAlchemy を使って RDB を操作する
        # ここでは、サンプルコードをシンプルにする目的で低レイヤーな API を使っている
        connection.execute('SELECT 1')
        connection.execute('SELECT 2')

        # プロファイルを停止する
        profiler.stop()

        # 実行された SQL 文の内容を確認する
        assert profiler.count == 2
        assert profiler.select == 2


if __name__ == '__main__':
    unittest.main()

上記では、分かりやすくするためにあえて SQLAlchemy の直接 SQL 文を扱う低レイヤーな API を使っている。

上記を実行するとテストがパスする。

$ python profile101.py 
.
----------------------------------------------------------------------
Ran 1 test in 0.020s

OK

このとき assert しているところの数値を変更すると、当然だけどテストは失敗するようになる。 想定していた SQL 文の数と、実際に発行された数が一致しないため。

$ python profile101.py
F
======================================================================
FAIL: test (__main__.Test_RawExecute)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "profile101.py", line 32, in test
    assert profiler.count == 1
AssertionError

----------------------------------------------------------------------
Ran 1 test in 0.017s

FAILED (failures=1)

O/R マッピングと共に使う

先ほどの例では、分かりやすさのためにあえて SQLAlchemy の直接 SQL 文を扱う低レイヤーな API を使っていた。 もちろん sqlalchemy-profile は O/R マッピングをしたコードでも動作するし、使い方については何も変わらない。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import unittest

from sqlalchemy.ext import declarative
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import Text
from sqlalchemy import create_engine
from sqlalchemy.orm.session import sessionmaker

from sqlalchemy_profile import StatementProfiler

Base = declarative.declarative_base()


class _User(Base):
    """ユーザ情報を模したモデルクラス"""
    __tablename__ = 'users'

    # 主キー
    id = Column(Integer, primary_key=True)
    # 名前を格納するカラム
    name = Column(Text, nullable=False)


class Test_ORMapping(unittest.TestCase):

    def setUp(self):
        """テストが実行される前の下準備"""
        self.engine = create_engine('sqlite:///')
        Base.metadata.create_all(self.engine)
        self.session_maker = sessionmaker(bind=self.engine)

    def tearDown(self):
        """テストが実行された後の後始末"""
        Base.metadata.drop_all(self.engine)

    def test(self):
        session = self.session_maker()

        profiler = StatementProfiler(self.engine)
        profiler.start()

        # 以下のユーザを模したインスタンスを一通り CRUD していく
        user = _User(name='Alice')

        # INSERT
        with session.begin(subtransactions=True):
            session.add(user)

        # UPDATE
        with session.begin(subtransactions=True):
            user.name = 'Bob'

        # SELECT
        session.query(_User).all()

        # DELETE
        with session.begin(subtransactions=True):
            session.delete(user)

        profiler.stop()

        # SQL 文は各一回ずつ実行されているはず
        assert profiler.count == 4
        assert profiler.insert == 1
        assert profiler.update == 1
        assert profiler.select == 1
        assert profiler.delete == 1


if __name__ == '__main__':
    unittest.main()

with ステートメントと共に使う

これまでの例ではプロファイリング期間を start() メソッドと stop() メソッドで制御したけど、これは with でも代用できる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import unittest

from sqlalchemy import create_engine

from sqlalchemy_profile import StatementProfiler


class Test_WithStatement(unittest.TestCase):

    def test(self):
        engine = create_engine('sqlite:///')
        connection = engine.connect()

        # with ステートメントのスコープで実行された SQL 文を記録する
        with StatementProfiler(engine) as profiler:
            connection.execute('SELECT 1')
            connection.execute('SELECT 2')

        assert profiler.count == 2
        assert profiler.select == 2


if __name__ == '__main__':
    unittest.main()

デコレータと共に使う

with を使うのもめんどくさいなー、というときはテストメソッド自体をデコレータで修飾しちゃうような使い方もできる。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import unittest

from sqlalchemy import create_engine

from sqlalchemy_profile import sqlprofile

ENGINE = create_engine('sqlite:///')


class Test_Decorator(unittest.TestCase):

    # ユニットテストのメソッドをデコレータで修飾する
    # メソッド内で実行されることが想定される SQL 文の数を指定する
    @sqlprofile(ENGINE, count=2, select=2)
    def test(self):
        connection = ENGINE.connect()

        connection.execute('SELECT 1')
        connection.execute('SELECT 2')


if __name__ == '__main__':
    unittest.main()

SQL 文の種類と順序まで確認したい

いやいや回数だけのアサーションとかアバウトすぎるでしょ、っていうときは StatementProfiler#sequence を使う。 これで INSERT, UPDATE, SELECT, DELETE が、どんな順番で実行されたかを確認できる。 中身は文字列で、それぞれの操作の頭文字が入っている。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import unittest

from sqlalchemy.ext import declarative
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import Text
from sqlalchemy import create_engine
from sqlalchemy.orm.session import sessionmaker

from sqlalchemy_profile import StatementProfiler

Base = declarative.declarative_base()


class _User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(Text, nullable=False)


class Test_ORMapping(unittest.TestCase):

    def setUp(self):
        self.engine = create_engine('sqlite:///')
        Base.metadata.create_all(self.engine)
        self.session_maker = sessionmaker(bind=self.engine)

    def tearDown(self):
        Base.metadata.drop_all(self.engine)

    def test(self):
        session = self.session_maker()

        profiler = StatementProfiler(self.engine)
        profiler.start()

        user = _User(name='Alice')

        # INSERT
        with session.begin(subtransactions=True):
            session.add(user)

        # UPDATE
        with session.begin(subtransactions=True):
            user.name = 'Bob'

        # SELECT
        session.query(_User).all()

        # DELETE
        with session.begin(subtransactions=True):
            session.delete(user)

        profiler.stop()

        # [I]NSERT -> [U]PDATE -> [S]ELECT -> [D]ELETE
        assert profiler.sequence == 'IUSD'


if __name__ == '__main__':
    unittest.main()

もちろんデコレータの API でも使える。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import unittest

from sqlalchemy import create_engine

from sqlalchemy_profile import sqlprofile

ENGINE = create_engine('sqlite:///')


class Test_Decorator(unittest.TestCase):

    # SELECT -> SELECT = SS
    @sqlprofile(ENGINE, seq='SS')
    def test(self):
        connection = ENGINE.connect()

        connection.execute('SELECT 1')
        connection.execute('SELECT 2')


if __name__ == '__main__':
    unittest.main()

もっと厳密にアサーションしたい

いやいや SQL 文の構造までもっと調べたいよ、というときは StatementProfiler#statementsStatementProfiler#statements_with_parameters を使う。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import unittest

from sqlalchemy.ext import declarative
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import Text
from sqlalchemy import create_engine
from sqlalchemy.orm.session import sessionmaker

from sqlalchemy_profile import StatementProfiler

Base = declarative.declarative_base()


class _User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(Text, nullable=False)


class Test_ORMapping(unittest.TestCase):

    def setUp(self):
        self.engine = create_engine('sqlite:///')
        Base.metadata.create_all(self.engine)
        self.session_maker = sessionmaker(bind=self.engine)

    def tearDown(self):
        Base.metadata.drop_all(self.engine)

    def test(self):
        session = self.session_maker()

        profiler = StatementProfiler(self.engine)
        profiler.start()

        user = _User(name='Alice')

        # INSERT
        with session.begin(subtransactions=True):
            session.add(user)

        profiler.stop()

        assert profiler.count == 1
        assert profiler.insert == 1

        # 生の SQL 文を取得する
        print(profiler.statements)
        print(profiler.statements_with_parameters)


if __name__ == '__main__':
    unittest.main()

こんな感じ。

['INSERT INTO users (name) VALUES (?)']
[('INSERT INTO users (name) VALUES (?)', ('Alice',))]
.
----------------------------------------------------------------------
Ran 1 test in 0.019s

OK

こちらは、今のところデコレータの API では使えない。

まとめ

  • SQLAlchemy の生成する SQL 文を確認するための sqlalchemy-profile というパッケージを作ってみた
  • O/R マッピングをすると、生成される SQL 文をプログラマが把握しにくくなる
  • 非効率なクエリをコードや実行結果から目で見て確認するのは手間がかかる
  • sqlalchemy-profile を使うことで実行される SQL 文をユニットテストで確認できるようになる

もしかすると似たようなことができるパッケージが既にあるかも。

Python: 相関行列を計算してヒートマップを描いてみる

以前、このブログで相関係数について解説した記事を書いたことがある。 相関係数というのは、データセットのある次元とある次元の関連性を示すものだった。

blog.amedama.jp

この相関係数を、データセットの各次元ごとに計算したものを相関行列と呼ぶ。 データ分析の世界では、それぞれの次元の関連性を見るときに、この相関行列を計算することがある。 また、それを見やすくするためにヒートマップというグラフを用いて図示することが多い。

今回は Python を使って相関行列を計算すると共にヒートマップを描いてみることにした。

使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.12.4
BuildVersion:   16E195
$ python --version
Python 3.5.3

下準備

今回は、相関行列の計算には NumPy を、グラフの描画には seaborn を使うのでインストールしておく。 最後の scikit-learn については、相関行列の計算に使うデータセットを読み込むためだけに使っている。

$ pip install seaborn numpy scikit-learn

相関行列を計算してみる

まずはヒートマップの描く以前に相関行列の計算から。 データセットにはみんな大好きアイリス (あやめ) データセットを用いる。 これは 150 行 4 次元の構造になっている。

>>> from sklearn import datasets
>>> dataset = datasets.load_iris()
>>> features = dataset.data
>>> features.shape
(150, 4)

相関行列の計算には NumPycorrcoef() 関数が使える。 この関数には、相関行列を計算したい次元をリストの形で渡す。 すごくベタに書くとしたら、こんな感じ。

>>> import numpy as np
>>> np.corrcoef([features[:, 0], features[:, 1], features[:, 2], features[:, 3]])
array([[ 1.        , -0.10936925,  0.87175416,  0.81795363],
       [-0.10936925,  1.        , -0.4205161 , -0.35654409],
       [ 0.87175416, -0.4205161 ,  1.        ,  0.9627571 ],
       [ 0.81795363, -0.35654409,  0.9627571 ,  1.        ]])

上記には、それぞれの次元ごとの相関係数が格納されている。 対角要素が全て 1 になっているのは、全く同じデータ同士の相関係数は 1 になるため。

ただ、実際には上記のようなベタ書きをする必要はない。 次元ごとにリストで、というのはようするに行と列を入れ替えれば良いということ。 つまり M 行 N 列のデータなら N 行 M 列に直して渡すことになる。

これは NumPy 行列なら transpose() メソッドで実現できる。

>>> features.transpose().shape
(4, 150)

ようするに、こうなる。

>>> np.corrcoef(features.transpose())
array([[ 1.        , -0.10936925,  0.87175416,  0.81795363],
       [-0.10936925,  1.        , -0.4205161 , -0.35654409],
       [ 0.87175416, -0.4205161 ,  1.        ,  0.9627571 ],
       [ 0.81795363, -0.35654409,  0.9627571 ,  1.        ]])

相関行列はこれで計算できた。

ヒートマップを描いてみる

続いては、先ほどの相関行列をヒートマップにしてみよう。 Python のグラフ描画ライブラリの seaborn には、あらかじめヒートマップを描くための API が用意されている。

次のサンプルコードではアイリスデータセットの相関行列をヒートマップで図示している。 それぞれの行の説明についてはコメントで補足している。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
from matplotlib import pyplot as plt
import seaborn as sns
from sklearn import datasets


def main():
    # アイリスデータセットを読み込む
    dataset = datasets.load_iris()

    features = dataset.data
    feature_names = dataset.feature_names

    # N 行 M 列を M 行 N 列に変換して相関行列を計算する
    correlation_matrix = np.corrcoef(features.transpose())

    # 相関行列のヒートマップを描く
    sns.heatmap(correlation_matrix, annot=True,
                xticklabels=feature_names,
                yticklabels=feature_names)

    # グラフを表示する
    plt.show()


if __name__ == '__main__':
    main()

上記を実行すると、次のようなグラフが得られる。

f:id:momijiame:20170418225256p:plain

このヒートマップでは、正の相関が強いものほど赤く、負の相関が強いものほど青く図示されている。 相関がないものについては色が薄いことから白に近づくことになる。 上記を見ると、アイリスデータセットには相関の強い次元が多いことが分かる。

主成分分析した結果の相関行列でヒートマップを描いてみる

ここで一つ気になったことを試してみることにした。 主成分分析した結果を相関行列にしてヒートマップで描いてみる、というものだ。 主成分分析とは何ぞや、ということは以下のブログエントリで書いた。

blog.amedama.jp

理屈の上では、主成分分析した結果は互いに直交した次元になるため相関しないはず。 これを相関行列とヒートマップで確かめてみよう、ということ。

次のサンプルコードでは、アイリスデータセットを主成分分析している。 そして、分析した内容に対して相関行列を計算してヒートマップを描いた。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import numpy as np
from matplotlib import pyplot as plt
import seaborn as sns
from sklearn import datasets
from sklearn.decomposition import PCA


def main():
    # アイリスデータセットを読み込む
    dataset = datasets.load_iris()

    features = dataset.data

    # 特徴量を主成分分析する
    pca = PCA()
    pca.fit(features)

    # 分析にもとづいて特徴量を主成分に変換する
    transformed_features = pca.fit_transform(features)

    # 主成分の相関行列を計算する
    correlation_matrix = np.corrcoef(transformed_features.transpose())

    # 主成分の相関行列をヒートマップで描く
    feature_names = ['PCA{0}'.format(i)
                     for i in range(features.shape[1])]
    sns.heatmap(correlation_matrix, annot=True,
                xticklabels=feature_names,
                yticklabels=feature_names)

    # グラフを表示する
    plt.show()


if __name__ == '__main__':
    main()

上記を実行すると、次のようなグラフが得られる。

f:id:momijiame:20170418225930p:plain

見事に真っ白で、互いに相関が全然ないことが分かる。 理屈通りの結果になった。

まとめ

  • 二つの次元の関連性を調べるには相関係数を用いる
  • データセットに含まれる全ての次元で相関係数を計算したものを相関行列と呼ぶ
  • 相関行列はヒートマップというグラフで図示することが多い
  • 主成分分析した結果の相関行列は対角要素を覗いてゼロになる

めでたしめでたし。

フレッツ回線が遅すぎる問題を IPv6/IPoE と DS-Lite で解決した

最近というほど最近でもないんだけど、近頃はとにかくフレッツ回線のスループットが出ない。 下手をすると、モバイルネットワークの方が速いので時間帯によってはテザリングをし始めるような始末だった。 今回は、そんなスループットの出ないフレッツ回線を何とか使い物になるようにするまでの流れを書いてみる。

先に断っておくと、今回はいつものような特定の技術に関する解説という側面は強くない。 思考の過程なども含んでいるので、いつもより読み物的な感じになっていると思う。 調べ物をして、それらについて理解した内容のまとめになっている。

結論から書いてしまうと、今回のケースでは IPv6/IPoE 接続と DS-Lite を使って何とかなった。 DS-Lite というのはゲーム端末ではなくて IPv4/IPv6 共存技術の一つである RFC6333 (Dual-Stack Lite Broadband Deployments Following IPv4 Exhaustion) の通称を指す。

そして、背景とか技術的な内容が不要なら下の方にある「利用までの流れ」から読み始めてもらうと手っ取り早いと思う。

フレッツ回線が遅い原因

まず、一般的に「フレッツが遅い」と言われる主な原因は、フレッツ網内にある PPPoE の終端装置がボトルネックになっているため。 技術に明るい個人ユーザレベルでは周知の事実みたいだけど、ISP なんかが公式でこの問題について言及しているものにも以下があった。 この記事を読むと、どのような設備を通ってパケットがインターネットまで届くのかが理解できる。

techlog.iij.ad.jp

ようするに、フレッツ網と ISP の接続境界 (POI: Point of Interface) が詰まっている。 この場合、フレッツ網内も ISP のバックボーン内も転送能力には余裕があるからタチが悪い。 フレッツ網内だけでスループットを測定しても、バックボーン内だけで測定しても、どちらも何ら問題は出ない。 また、終端装置の設備は都道府県ごと・ISP ごとに用意されているので、状況はまちまちらしい。 とはいえ、都内では特に多くの ISP で上記の輻輳が起きているようだ。

そんな背景もあって、これまで遅いなと感じたときには PPPoE のセッションを張り直してみることもあった。 たまに、比較的空いている終端装置につながることもあって、そうするとスループットが改善したりする。 PPPoE ガチャ。

じゃあ、ボトルネックになっている終端装置を増強すれば良いじゃないか、という話になるんだけど、それがどうやら難しいらしい。 何故なら、ISP が終端装置を増強したくても、その権限は NTT 側にあるため。 NTT 的には PPPoE セッション数に応じて設備増強をするポリシーでいるようで、ISP から要望があっても拒んでいる。 これは、総務省が収集している NGN (フレッツ光ネクストのこと) に対するパブリックコメントを見ると分かる。

www.soumu.go.jp

ここには ISP の NTT に対する恨みつらみが書かれていて、収集された意見に対して「終端装置」で検索をかけると、それが分かる。

インターネット上のコンテンツはリッチ化を続けており、一人あたりのトラフィック量は急速に増え続けている。

www.soumu.go.jp

そうした状況の中で、フレッツ網の設備増強基準が時代にそぐわないものになっているんだろう。

解決策について

話がだいぶ大きくなったけど、ここからは個人の話になる。 この問題の解決策としては、当初いくつかの選択肢が考えられた。

まず一つ目は、そもそも別の回線キャリアに乗り換えてしまうこと。 これは、例えば NURO のようなサービスを指している。 ただ、調べていくとこのやり方だとかなりお金がかかることが分かった。 具体的には、初期工事費用が万単位でかかるのと、月額料金も現状よりだいぶ上がりそうな感じ。 初期工事費用については乗り換えキャンペーンやキャッシュバックで補填するとしても、月額料金は如何ともしがたい。

二つ目は、そもそもフレッツ回線を解約してしまうこと。 モバイルネットワークはそれなりにスループットが出ているので、これに一本化してしまう。 必要な経費は、固定回線っぽく使うために必要な SIM フリーのモバイルルータを購入する資金のみ。 具体的には、以下のようなやつ。

指す SIM カードについては MVNO で複数枚出せるプランに加入しているので問題なし。 ただ、これだと転送量上限がある点に不安を覚えた。 自宅で技術的な検証をしていると数 GB のファイルとかダウンロードする機会もあるし、これはまずい。

三つ目は、今あるフレッツ回線をなんとかする方法。 上記の記事でも挙げられているけど、ようするにフレッツ網内にある PPPoE の終端装置さえ迂回できればボトルネックは解消できる。 この迂回する方法としては IPoE 接続を使うやり方が挙げられる。

IPoE について

従来、フレッツ網で IPv4 の通信をするときは PPPoE というトンネリングプロトコルが使われている。 これは、主にユーザの認証のために用いられる。 このプロトコルの終端装置がフレッツ網内でボトルネックになっていることは前述した通り。 この通信方式を、以後は IPv4/PPPoE と記述する。

それに対して IPv6 の通信では、上記の PPPoE を使うやり方と IPoE という二つの方法がある。 これらの通信方式を、以後はそれぞれ IPv6/PPPoE と IPv6/IPoE と記述する。 その中でも IPv6/IPoE については現時点で輻輳しておらず快適らしい。

IPoE というのは Internet Protocol over Ethernet の略で、トンネリングプロトコルを使わず直にイーサネットを扱うやり方を指す。 IPoE については以下のサイトが分かりやすい。

IPv6 IPoEの仕組み:Geekなぺーじ

また、これは副次的なものだけど IPv6/IPoE を使うと下りの速度が IPv4/PPPoE よりもサービススペックとして上がる場合もある。 例えば、フレッツ光ネクストのマンション・ハイスピードタイプを使う場合を考えてみよう。 以下の公式サイトを見ると、但し書きで IPv6/IPoE は受信速度が 1Gbps と書かれている。

flets.com

ちなみに、上りと下りの両方が 1Gbps のプランを契約すると、だいたい月額料金が ¥500 は上がる。 それに対し、月額料金はそのままで下りに関しては 1Gbps で使えるのはでかい。

ただし、全ての ISP が IPv6/IPoE に対応しているわけではない。 例えば、東京で IPv6/IPoE が使える ISP は次の通り。 それぞれの回線プランごとにも使える・使えないなどが細かく分かれている。

flets.com

IPv6/IPoE を使えば全て解決?

また、IPv6/IPoE さえ使うことができれば全て解決かというと、そうもいかない。 なぜかというと、ちまたのサービスによって IPv6 への対応状況がまちまちだから。 IPv6 に対応していないホストへの通信は IPv4/PPPoE を経由するので今まで通りのスループットしか出ない。

それに、IPv4 と IPv6 のデュアルスタックな環境で、どちらが使われるかは設定や OS の実装にも影響を受ける。 低速な IPv4/PPPoE と高速な IPv6/IPoE のどちらも使えるようになっているときに IPv4/PPPoE が優先して使われてしまうと、遅いままになる。

IPv4 のトラフィックも IPoE に流したい

そこで考えられるのが IPv4 の通信も IPv6/IPoE 経由で流してしまう、という発想。 これには、元々は IPv4 の縮小期に使うために検討されていた技術を応用する。 色々な種類があるんだけど、日本では主に MAP-EDS-Lite が使われている。 これは、一般に IPv4 over IPv6 トンネルと呼ばれるもので IPv4 のパケットを IPv6 のパケットで包んで送ってしまう手法になっている。

MAP-E と DS-Lite それぞれの手法の違いは、次のサイトが詳しい。 ようするに NAPT (Network Address Port Translation) が CPE (Customer Private Edge) にあるか CGN (Carrier Grade NAT) になるかの違い。

techlog.iij.ad.jp

IPv6/IPoE を提供しているのは誰?

ちょっと話が巻き戻るんだけど、そもそも IPv6/IPoE を使ったインターネットへの疎通性を提供しているのは ISP ではない。 これは、いわゆるネイティブ接続事業者 (VNE: Virtual Network Enabler) が担っている。 VNE は最近まで三社だけに限定されていて、それぞれの固定回線キャリアが出資している IX (Internet eXchange) 事業者が同時に VNE も担っていた。 ISP は、そのいずれかと契約することで IPv6/IPoE をユーザが使えるようにしている。

そんな VNE なんだけど、事業者によって IPv4 を IPv6 を通して使うために採用している技術はそれぞれ異なる。 例えば JPNE (KDDI 系) は MAP-E を、マルチフィード (NTT 系) は DS-Lite をそれぞれ採用している。 BBIX (ソフトバンク系) については公開されているドキュメントを調べても、具体的に何を使っているかは分からなかった。

JPNE は MAP-E を「v6 プラス」というサービス名で提供している。

v6プラス | サービス紹介 | JPNE | 日本ネットワークイネイブラー株式会社

マルチフィードは DS-Lite を「IPv4インターネット接続オプションサービス」というサービス名で提供している。

サービスのご案内 - transixサービス|インターネットマルチフィード株式会社

BBIX は「IPv6 IPoE + IPv4 ハイブリッドサービス」というサービスを提供している。 IPv4 over IPv6 トンネルを使っている、ということは分かるんだけど具体的な方式は分からない。

IPv6 IPoE + IPv4 ハイブリッドサービス サービス概要|ソリューション|BBIX株式会社

つまり、まとめるとこう。

VNE サービス名 方式
JPNE v6 プラス MAP-E
マルチフィード IPv4インターネット接続オプションサービス DS-Lite
BBIX IPv6 IPoE + IPv4 ハイブリッドサービス IPv4 over IPv6 トンネル (具体的な方式は不明)

なので、続いては IPv6/IPoE を使うにしても ISP が契約している VNE が何処かを確認したい。 これは、それぞれの方式によって用意すべきルータなどが異なるため。

契約している VNE については、公開している ISP もあれば非公開にしている ISP もある。 もし、非公開な ISP だと上記のサービスが使えたとしても公式なサポートではなくなる。 つまり、いつ通信できなくなっても文句は言えない自己責任ということ。

ちなみに、今回自分が使った ISP も契約している VNE は非公開だった。 ただし、ウェブサイトでの下調べや、使ってみた結果としてはマルチフィードであることは明らかだった。 つまり、DS-Lite に対応したルータを用意すれば良い、ということになる。

とはいえ、公式には非公開とされているので、ここでは具体的にどの ISP を使っているか書くことは控えることにする。 参考までにということであれば、個別に聞いてもらえたらお伝えします。 IPoE を申し込んでも初期費用や追加の月額料金がかからない点は良かったと思う。

ちなみに、元々は 3 社に限られていた VNE 事業者の枠は、最近になって 16 社にまで拡大された。 そのため、ISP 自身が直接 VNE 事業に乗り出す例も出てきている。 具体的には、以下。

「IPv6接続機能」提供開始のご案内|プロバイダ ASAHIネット |料金、接続品質、満足度で比較して選ばれるISP

ただし、そのような新規参入した事業者が今後どういった IPv4/IPv6 共存技術を採用するのかは分からない。 同種の技術としては MAP-E や DS-Lite 以外にも 464XLATMAP-T4rd/SAM といった選択肢もある。 また、いつ頃から提供し始めるのかも分からない。 そもそも、そのようなサービスを VNE が提供しなければいけない、といったような決まりは存在しないので。

ここらへんは、どうも以前に「VNE はみんなで 4rd/SAM を使っていこう」みたいな流れがあったっぽいんだけど。 今はどうしてこうなったのかな。

NTT NGNネイティブ方式上でIPv4 over IPv6提供へ:Geekなぺーじ

利用までの流れ

続いては IPv6/IPoE と DS-Lite を使えるようにするまでに実施した流れなどについて書く。 ちなみに IPoE についての知見は、どうやら何処のサポートセンターにもあまりたまっていないらしい。 そのため、問い合わせて返ってきた答えが間違っていたということも平気で起こる、というか起こった。

また、不安に思う人もいるかもしれないのであらかじめ書いておくと、IPv6/IPoE + DS-Lite を一旦使い始めたからといって従来の IPv4/PPPoE が使えなくなるわけではない。 両者は二者択一というわけではなく ISP によって IPv4/PPPoE の機能は提供され続ける。 これは、あくまで VNE が IPv6/IPoE を通して IPv4 の通信も通せるようにする機能を「オプションとして」提供しているに過ぎないため。 仮に何らかの問題があったとしても (遅いとはいえ) IPv4/PPPoE に戻すことはできる。 さらに言えば、例えば複数のルータを使ったり業務用ルータを使うなどの工夫をすれば、両者を同時に併用することも可能になっている。

フレッツの契約内容を確認する

まずは、自分が使っているフレッツの契約内容を確認する。 これは、前述した通りフレッツの契約内容によって ISP の対応状況が異なるため。

ちなみに、サービス名などが途中で変わっていたというケースもあるみたい。 今回のケースは正にそれで、いつの間にか B フレッツマンションタイプからフレッツ光ネクストマンションタイプになっていた。 念を入れるならサポートセンターに問い合わせると今の契約内容を書いた紙が送ってもらえる。

契約内容が分かったら、フレッツの公式サイトで自分の使っている ISP がそれに対応しているかを確認しよう。

flets.com

また、上記の表に載っていなくても実は対応していた、ということもあるみたい。 なので、あきらめずに ISP のサポートセンターに問い合わせてみるのも手だと思う。

ISP に IPv6/IPoE の利用を申し込む

続いて、対応していることが分かったら ISP に IPoE を使いたい旨を伝えて申し込む。 これはおそらくサポートセンターに電話することになると思う。

同時に、ISP が契約している VNE についても確認しておこう。 前述した通り VNE によって用意するルータが異なってくる。 JPNE なら MAP-E 対応のものになるし、マルチフィードなら DS-Lite 対応のものになる。 また、前述した通り契約している VNE を公式には非公開にしている ISP もある。 なので、ウェブサイトであらかじめ下調べするのが手っ取り早いだろう。

ちなみに、自分の場合はここで一度「使っている回線がフレッツ光ネクストではないので無理」と断られた。 ただ、結論としては ISP に登録されている内容がおかしくなっていただけだった。 前述したフレッツの契約内容を調べた上で再度問い合わせたところ受け付けてもらえた。

フレッツに v6 オプションを申し込む

続いて、フレッツ側でも v6 オプションというサービスを有効にする。 これにはサポートセンターに電話する以外にも、サービス情報サイトという Web サービス経由を使って自分で設定することもできる。 また、ISP によってはユーザの同意を得た上で代理で申し込んでくれる場合もあるようだ。

flets.com

ちなみに、NTT のサポートセンターからは一度「VDSL 配線方式なので IPoE は使えない」と言われた。 しかし、結論としては VDSL 配線方式だろうと光配線方式だろうと関係なく IPoE は使える。 ここは、調べても情報が出てこないので、あやうく工事をするところだった。

IPv6/IPoE + DS-Lite 対応ルータを用意する

ここまでで回線のレベルでは IPv6/IPoE が使えるようになる。 続いては、それを実際に使ってみるフェーズ。 今回のケースでは ISP が契約している VNE がマルチフィードだった。

そのため、DS-Lite に対応したルータを用意する必要がある。 これについては、マルチフィードで接続性を検証した機種を公式サイトで公開している。

transix DS-Lite 接続確認機種情報

ここから選ぶなら、入手性や設定の敷居という面では Buffalo のルータを選ぶのが良さそう。 もし、腕に覚えがあるようなら YAMAHA も業務用ルータとしては意外と安く買えるから良いのかも。 上記の検証で使われたそのものズバリな機種は古くて既に入手がしづらくなっているみたいだけど、後継機種はこれかな。

また、Buffalo のウェブサイトを調べたところ、次のようなページを見つけた。 どうやら、ここに載っている機種であれば MAP-E と DS-Lite の両方に対応しているらしい。

buffalo.jp

とはいえ DS-Lite の機構は極めてシンプルで、CPE 側に関しては IPv6 の SLAAC と IPv4 over IPv6 トンネルの機能さえあれば動作する。 なので、ちょっとがんばれば Linux なんかを使って自作することもできる。 例えば次のサイトでは Raspberry Pi を使って DS-Lite のルータを作る方法を紹介してる。

techlog.iij.ad.jp

つまり、自分で作ればタダみたいなもんやぞ! いや、筐体の費用がかかるので、タダではないです。

それと、買うにしても作るにしてもファイアーウォールはしっかりかけよう。 IPv4/PPPoE を使っていた頃とは、使うべきルールが異なっているはずなので。 IPv6 をブリッジしてデュアルスタックにするなら、特に気をつけた方が良い。

使う

あとは DS-Lite を設定したルータでインターネットへの疎通があることを確認して使うだけ。

例えば、以下は Google に traceroute してみたところ。

$ traceroute www.google.co.jp
traceroute to www.google.co.jp (216.58.197.195), 64 hops max, 52 byte packets
 1  172.16.0.1 (172.16.0.1)  1.437 ms  1.103 ms  1.015 ms
 2  ike-gw00.transix.jp (14.0.9.66)  6.579 ms  6.584 ms  6.410 ms
 3  ike-bbrt10.transix.jp (14.0.9.65)  6.456 ms  6.336 ms  6.506 ms
 4  bek-bbrt10.transix.jp (14.0.8.66)  7.005 ms
    bek-bbrt10.transix.jp (14.0.8.94)  6.952 ms
    bek-bbrt10.transix.jp (14.0.8.78)  6.999 ms
 5  210.173.176.243 (210.173.176.243)  7.474 ms  7.551 ms  8.019 ms
 6  108.170.242.193 (108.170.242.193)  7.615 ms  9.450 ms  8.119 ms
 7  72.14.233.221 (72.14.233.221)  7.687 ms  7.825 ms  7.665 ms
 8  nrt13s48-in-f195.1e100.net (216.58.197.195)  7.958 ms  7.940 ms  7.865 ms

ちゃんと Transix を経由していることが経路の逆引き結果から分かる。

DS-Lite を使うことで、以前は混雑しているときの RTT が 35ms 以上あったところが 15ms 未満まで短縮された。 それに伴って TCP のスループットも、ひどいときは 1Mbps 前後だったのが常時 50Mbps 以上出るようになった。

(2017/4/9 追記)

一つ注意点としては DS-Lite は NAPT が CGN になっているのでポート開放がユーザレベルでは制御できない。 家でサーバを立てている場合なんかには、それらを IPv4/PPPoE の方にルーティングしてやる必要がある。 これが v6 プラスの MAP-E なら NAPT が CPE にあるので原理上は開けられるはずなんだけど。 とはいえ、その場合も一つの IPv4 グローバルアドレスを複数の CPE で共有する関係で使えるポートレンジは限られるはず。

もう一つ懸念しているのは CGN の NAPT セッション数が足りなくなったりしないかという点。 同じく DS-Lite を試している他のブログでそれらしき事象が起こったらしい言及があった。 まだ遭遇したことはないんだけど、使うアプリケーションや時間帯によっては影響を受けるかもしれない。

まとめ

  • フレッツが遅い原因は PPPoE の終端装置がボトルネックになっているせい
  • IPv6/IPoE を使うとボトルネックになっている PPPoE の終端装置を迂回できる
  • ただし、使っているフレッツの契約と ISP の組み合わせで使えるかは異なる
  • また、そのままデュアルスタックで使うと IPv4 の通信が遅いままで残ってしまう
  • その解決策として IPv6/IPoE を使って IPv4 をトランスポートする技術がある
  • 採用している技術は IPv6/IPoE を提供している VNE ごとに異なる
  • JPNE なら MAP-E を、MF なら DS-Lite を使っている
  • 使うには ISP への申込みとフレッツで v6 オプションの有効化がいる
  • ISP によって契約している VNE が異なる
  • ISP が契約している VNE を非公開としてるなら、上記も非公式サポートとなる
  • DS-Lite 対応ルータは MF が公開している
  • とはいえ、ぶっちゃけ IP-IP トンネルさえあれば何とかなる
  • IPoE 関連でサポートセンターの返答は間違っていることが多い

Ω<つまり、これは PPPoE の終端装置をあえて輻輳させることで IPv6 への移行を促す陰謀だったんだよ!

ΩΩΩ<な、なんだってー?!

おわり。

Ubuntu 16.04 LTS で NVIDIA Docker を使ってみる

以前、このブログで Keras/TensorFlow の学習を GPU (CUDA) で高速化する記事を書いた。 このときは、それぞれの環境の分離には Python の virtualenv を使っていた。

blog.amedama.jp

今回は、別の選択肢として NVIDIA Docker を使う方法を試してみる。 NVIDIA Docker というのは NVIDIA が公式で出している Docker から CUDA を使えるようにするユーティリティ群と Docker イメージ。 このやり方だと Docker ホストには NVIDIA Driver さえ入っていれば動作する。 そして、CUDA Toolkit と cuDNN は Docker コンテナの中に配備される。 ここら辺の概念図は GitHub の公式ページが分かりやすい。

github.com

この方法なら、それぞれのコンテナ毎に異なるバージョンの CUDA Toolkit と cuDNN を組み合わせて使うことができる。 例えば、あるコンテナでは CUDA Toolkit 7.5 + cuDNN 4.0 を、別のコンテナでは CUDA Toolkit 8.0 + cuDNN 5.1 を、といった具合に。

今回使った環境は次の通り。

$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"
$ uname -r
4.4.0-71-generic

GPU については GTX 1050 Ti を使っている。

$ lspci | grep -i nvidia
01:00.0 VGA compatible controller: NVIDIA Corporation Device 1c82 (rev a1)
01:00.1 Audio device: NVIDIA Corporation Device 0fb9 (rev a1)

NVIDIA Driver をインストールする

まずは NVIDIA Driver をインストールする。 ドライバを単体で入れても良いんだけど、ついでだから CUDA Toolkit ごと入れてしまう。 次の公式ページからインストール用のファイルを取得する。

developer.nvidia.com

今回はネットワークインストール用の deb ファイルを使った。 ボタンを Linux > x86_64 > Ubuntu > 16.04 > deb (network) のように操作する。

得られたリンクから deb ファイルを取得する。

$ wget http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1604/x86_64/cuda-repo-ubuntu1604_8.0.61-1_amd64.deb

インストールしてからリポジトリの内容を更新する。

$ sudo dpkg -i cuda-repo-ubuntu1604_8.0.61-1_amd64.deb
$ sudo apt-get update

これで CUDA Toolkit がインストールできる。 依存パッケージとして、ついでに NVIDIA Driver も入る。

$ sudo apt-get install cuda

インストールが終わったら、一旦再起動しておこう。

$ sudo shutdown -r now

Docker エンジンをインストールする

続いて NVIDIA Docker の動作に必要な Docker エンジンをインストールする。

まずは、必要な依存パッケージをインストールする。

$ sudo apt-get install apt-transport-https ca-certificates curl software-properties-common

続いて APT の認証鍵をインストールする。

$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

Docker エンジンをインストールするための公式リポジトリを登録する。

$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

リポジトリの内容を更新する。

$ sudo apt-get update

Docker エンジン (Community Edition) をインストールする。

$ sudo apt-get -y install docker-ce

これで、自動的に Docker のサービスが起動する。 docker version コマンドを実行してエラーにならなければ上手くいっている。

$ sudo docker version
Client:
 Version:      17.03.1-ce
 API version:  1.27
 Go version:   go1.7.5
 Git commit:   c6d412e
 Built:        Mon Mar 27 17:14:09 2017
 OS/Arch:      linux/amd64

Server:
 Version:      17.03.1-ce
 API version:  1.27 (minimum version 1.12)
 Go version:   go1.7.5
 Git commit:   c6d412e
 Built:        Mon Mar 27 17:14:09 2017
 OS/Arch:      linux/amd64
 Experimental: false

NVIDIA Docker をインストールする

続いて、肝心の NVIDIA Docker をインストールする。 今のところ、配布パッケージはローカルインストールの deb ファイルしかないようだ。 もしパッケージが更新されても自動更新はされないはずなので、また入れ直す必要があるね。

github.com

ローカルインストール用の deb ファイルを取得する。

$ wget https://github.com/NVIDIA/nvidia-docker/releases/download/v1.0.1/nvidia-docker_1.0.1-1_amd64.deb

取得したファイルをインストールする。

$ sudo dpkg -i nvidia-docker_1.0.1-1_amd64.deb

これで、自動的に nvidia-docker サービスが起動する。

$ systemctl list-units --type=service | grep -i nvidia-docker
nvidia-docker.service                                 loaded active running NVIDIA Docker plugin

まずは、CUDA Toolkit 8.0 + cuDNN 5 がインストールされたコンテナをダウンロードしてこよう。

$ sudo docker pull nvidia/cuda:8.0-cudnn5-runtime

終わったら、動作確認として先ほどのコンテナで nvidia-smi コマンドを実行してみよう。 次のように、エラーにならず結果が出力されれば上手くいっている。

$ sudo nvidia-docker run --rm nvidia/cuda nvidia-smi
Mon Apr  3 12:49:17 2017
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 375.39                 Driver Version: 375.39                    |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  GeForce GTX 105...  Off  | 0000:01:00.0      On |                  N/A |
| 35%   27C    P8    35W /  75W |     61MiB /  4038MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID  Type  Process name                               Usage      |
|=============================================================================|
+-----------------------------------------------------------------------------+

これで Docker コンテナから GPU を使えるようになった。

カスタムイメージをビルドして使ってみる

先ほどは公式が配布しているバニラなイメージを使って動作確認をしてみた。 続いてはカスタムイメージをビルドして実行してみることにしよう。 サンプルとしては Keras と GPU 版 TensorFlow をインストールしてみることにした。

まずは適当に Dockerfile を書く。

$ cat << 'EOF' > Dockerfile
FROM nvidia/cuda:8.0-cudnn5-runtime

LABEL maintainer "example@example.jp"

RUN apt-get update
RUN apt-get -y install python3-pip curl
RUN pip3 install keras tensorflow-gpu
EOF

適当な名前でイメージをビルドする。

$ sudo docker build -t test/myimage .

ビルドしたイメージをインタラクティブモードで起動する。

$ sudo nvidia-docker run --rm -i -t test/myimage /bin/bash

Keras の CNN サンプルをダウンロードしてくる。

# curl -O https://raw.githubusercontent.com/fchollet/keras/master/examples/mnist_cnn.py
# echo 'K.clear_session()' >> mnist_cnn.py

そして実行する。 MNIST データセットのダウンロードとか GPU デバイスのスピンアップに時間がかかるので、これもイメージのビルドに含めた方が良かったかも。

# python3 mnist_cnn.py
Using TensorFlow backend.
I tensorflow/stream_executor/dso_loader.cc:135] successfully opened CUDA library libcublas.so.8.0 locally
I tensorflow/stream_executor/dso_loader.cc:135] successfully opened CUDA library libcudnn.so.5 locally
I tensorflow/stream_executor/dso_loader.cc:135] successfully opened CUDA library libcufft.so.8.0 locally
I tensorflow/stream_executor/dso_loader.cc:135] successfully opened CUDA library libcuda.so.1 locally
I tensorflow/stream_executor/dso_loader.cc:135] successfully opened CUDA library libcurand.so.8.0 locally
Downloading data from https://s3.amazonaws.com/img-datasets/mnist.npz
x_train shape: (60000, 28, 28, 1)
60000 train samples
10000 test samples
Train on 60000 samples, validate on 10000 samples
Epoch 1/12
W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use SSE3 instructions, but these are available on your machine and could speed up CPU computations.
W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use SSE4.1 instructions, but these are available on your machine and could speed up CPU computations.
W tensorflow/core/platform/cpu_feature_guard.cc:45] The TensorFlow library wasn't compiled to use SSE4.2 instructions, but these are available on your machine and could speed up CPU computations.
I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:910] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
I tensorflow/core/common_runtime/gpu/gpu_device.cc:885] Found device 0 with properties:
name: GeForce GTX 1050 Ti
major: 6 minor: 1 memoryClockRate (GHz) 1.455
pciBusID 0000:01:00.0
Total memory: 3.94GiB
Free memory: 3.84GiB
I tensorflow/core/common_runtime/gpu/gpu_device.cc:906] DMA: 0
I tensorflow/core/common_runtime/gpu/gpu_device.cc:916] 0:   Y
I tensorflow/core/common_runtime/gpu/gpu_device.cc:975] Creating TensorFlow device (/gpu:0) -> (device: 0, name: GeForce GTX 1050 Ti, pci bus id: 0000:01:00.0)
60000/60000 [==============================] - 106s - loss: 0.3181 - acc: 0.9041 - val_loss: 0.0792 - val_acc: 0.9739
Epoch 2/12
60000/60000 [==============================] - 9s - loss: 0.1105 - acc: 0.9674 - val_loss: 0.0572 - val_acc: 0.9811
Epoch 3/12
60000/60000 [==============================] - 9s - loss: 0.0851 - acc: 0.9743 - val_loss: 0.0440 - val_acc: 0.9858
Epoch 4/12
60000/60000 [==============================] - 9s - loss: 0.0700 - acc: 0.9793 - val_loss: 0.0387 - val_acc: 0.9869
Epoch 5/12
60000/60000 [==============================] - 9s - loss: 0.0610 - acc: 0.9818 - val_loss: 0.0380 - val_acc: 0.9873
Epoch 6/12
60000/60000 [==============================] - 9s - loss: 0.0554 - acc: 0.9833 - val_loss: 0.0324 - val_acc: 0.9901
Epoch 7/12
60000/60000 [==============================] - 9s - loss: 0.0512 - acc: 0.9849 - val_loss: 0.0311 - val_acc: 0.9892
Epoch 8/12
60000/60000 [==============================] - 9s - loss: 0.0475 - acc: 0.9861 - val_loss: 0.0331 - val_acc: 0.9894
Epoch 9/12
60000/60000 [==============================] - 9s - loss: 0.0432 - acc: 0.9871 - val_loss: 0.0304 - val_acc: 0.9902
Epoch 10/12
60000/60000 [==============================] - 9s - loss: 0.0421 - acc: 0.9874 - val_loss: 0.0313 - val_acc: 0.9892
Epoch 11/12
60000/60000 [==============================] - 9s - loss: 0.0400 - acc: 0.9880 - val_loss: 0.0305 - val_acc: 0.9902
Epoch 12/12
60000/60000 [==============================] - 9s - loss: 0.0377 - acc: 0.9887 - val_loss: 0.0290 - val_acc: 0.9910
Test loss: 0.0289570764096
Test accuracy: 0.991

良い感じ。