kerasのMNISTデータを読み込んでみる

kerasにはscikit-learnと同じように、いつくかのサンプルデータが付属しています。
その中の一つがMNISTという28*28ピクセルの手書き数字文字のデータです。

scikit-learn にも digits という手書き数字のサンプルデータがありますが、こちらは8*8ピクセルの結構データ量の小さいデータです。
ライブラリの使い方を紹介する上ではこれで問題がないのでよく使っていますが、深層学習をつかずとも十分に判別できてしまうのが短所です。
(このブログの過去の記事でも使ってきたのはこちらです。)

せっかくディープラーニングを試すのであればこちらを使った方がいいと思うので、使い方を紹介します。

ドキュメントはここ。
MNIST database of handwritten digits

早速ですが読み込み方法です。


from keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()

インポートしてloadすれば良いというのは scikit-learnのサンプルデータと同じですが、
load_dataの戻り値が少し特徴的です。
長さ2のタプル2個を値に持つタプルを返してきます。
これを受け取るために、上記のような書き方をします。
変数に格納された配列の次元数を見ると次のようになります。


print(x_train.shape) # (60000, 28, 28)
print(y_train.shape) # (60000,)
print(x_test.shape) # (10000, 28, 28)
print(y_test.shape) # (10000,)

x_train などの中身を見れば、ピクセルごとの濃淡として画像データが入っていることがわかるのですが、
せっかくの画像データなので、画像として可視化してみましょう。
matplotlibのimshow という関数を使うと便利です。

matplotlib.pyplot.imshow

最初の16データを可視化したのが次のコードです。
デフォルトの配色はイマイチだったので、文字らしく白背景に黒文字にしました。


import matplotlib.pyplot as plt
fig = plt.figure(figsize=(12, 12))
for i in range(16):
    ax = fig.add_subplot(4, 4, i+1, title=str(y_train[i]))
    ax.imshow(x_train[i], cmap="gray_r")
plt.show()

結果はこちら。
digitsに比べて読みやすい数字画像データが入ってますね。

パイプラインのグリッドサーチ

scilit-learnでグリッドサーチする方法買いた以前の記事の、
次の記事として、パイプラインを使ったモデルのグリッドサーチ方法を書く予定だったのを失念していたので紹介します。
(パイプラインそのもの紹介もまだなのでそのうち記事に起こします。)

公式のドキュメントはこちらです。
sklearn.pipeline.Pipeline
sklearn.model_selection.GridSearchCV

単一のモデルのグリッドサーチとの違いは、サーチ対象のパラメータを指定する時、
“変数名”: [変数のリスト]
で指定したところを、
“ステップ名”__”変数名”: [変数のリスト]
と指定するようにするだけです。

例を見た方がわかりやすいので、irisのデータと、
モデルは PCA + ロジスティック回帰 でやってみます。


# 必要なライブラリのインポート
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report

# データの準備
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
            iris.data,
            iris.target,
            test_size=0.2,
            stratify=iris.target,
)

# モデル(パイプライン)の作成
clf = Pipeline(
    [
        ("pca", PCA()),
        ("lr", LogisticRegression())
    ]
)

# 探索するパラメータの設定
# ここのkeyの指定方法が重要
params = {
    "pca__n_components": [2, 3, 4],
    "lr__penalty": ["l1", "l2"],
    "lr__C": [0.01, 0.1, 1, 10, 100]
}

# グリッドサーチ
gs_clf = GridSearchCV(
    clf,
    params,
    cv=5
)
gs_clf.fit(X_train, y_train)

# 最適なパラメーター
print(gs_clf.best_params_)

# 最適なモデルの評価
best_clf = gs_clf.best_estimator_
print(classification_report(y_test, best_clf.predict(X_test)))

# 以下出力結果

{'lr__C': 100, 'lr__penalty': 'l2', 'pca__n_components': 3}
             precision    recall  f1-score   support

          0       1.00      1.00      1.00        10
          1       1.00      1.00      1.00        10
          2       1.00      1.00      1.00        10

avg / total       1.00      1.00      1.00        30

最終的にテストデータでの評価が正解率100%なのは運が良かっただけです。
train_test_splitの結果次第で、実行するたびにbest_params_も評価も変わります。

次元削減とその後の分類機は、まとめて最適かしたいので非常に便利です。

kerasで学習途中のモデルを保存する

kerasでモデルを学習している時、学習途中のモデルが欲しいことがよくあります。
デフォルトでepockごとに評価スコア出るので、テストデータに限っては過学習する前のモデルが欲しいですし、
前の記事のEarlyStoppingで、patienceを設定しているのであれば、
Stopした最後のバージョンより、そのいくつか前のものの方が評価が高いからです。
このような時、ModelCheckpointというCallBackを使うことで、Epockごとのモデルを保存しておくことができます。

前回同様データの準備やモデルの構築は省略して、ModelCheckpointに必要な部分のコードだけ紹介します。


from keras.callbacks import ModelCheckpoint

# checkpointの設定
checkpoint = ModelCheckpoint(
                    filepath="model-{epoch:02d}-{val_loss:.2f}.h5",
                    monitor='val_loss',
                    save_best_only=True,
                    period=1,
                )
# 学習
history = model.fit(
                    X_train,
                    y_train,
                    epochs=100,
                    batch_size=16,
                    validation_data=[X_test, y_test],
                    callbacks=[early_stopping, checkpoint]
            )

これで、filepathに指定したファイル名でepockごとのモデルが保存できます。
見ての通り、ファイル名にはepock数などを変数で埋め込むことができます。
また、save_best_only=Trueを設定することで、
前のモデルよりも精度が良い場合のみ保存することができます。
ファイル名に変数を含めないように指定しておけば、ベストのモデルだけが保存されている状態になります。

あとは、保存されているモデルをloadして利用すればOKです。

kerasでモデルの学習が進まなくなったら学習を止める方法

機械学習をやっているとき、過学習の抑止や時間の節約のためにモデルの改善が止まった時点で学習を止めたいことがあります。
kerasでは CallBack に EarlyStopping というオブジェクトを設定するおことでそれを実現できます。

モデル本体やデータについてのコードは省略しますので別記事を参照してください、該当部分だけ紹介します。


# インポート
from keras.callbacks import EarlyStopping

# EaelyStoppingの設定
early_stopping =  EarlyStopping(
                            monitor='val_loss',
                            min_delta=0.0,
                            patience=2,
)

# 学習
history = model.fit(
                    X_train,
                    y_train,
                    epochs=100,
                    batch_size=16,
                    validation_data=[X_test, y_test],
                    callbacks=[early_stopping] # CallBacksに設定
            )

これで、 monitor に設定した値が、 patienceの値の回数続けてmin_delta以上改善しないと、
学習がストップします。
monitor には ‘val_loss’ の他、 ‘val_acc’なども設定可能です。

patience の設定が0の時と1の時は挙動が全く同じに見えますね。

特にデメリットも感じられないので、kerasで機械学習を試す時はほぼ確実に使っています。
あまりにも学習が進まないうちに止まってしまう時は、EarlyStopping無しで試したりするのですが、
経験上、EarlyStoppingが悪いことは少なく、モデルの設計が悪かったり、その他どこかにミスがあることが多いです。

kerasで作成したモデルの保存と読み込み

kerasで作成したモデルを保存したり、次回使うときに読み込んだりする方法のメモです。

とりあえず、modelという変数に学習済みのモデルが格納されているとします。
(前回の記事のモデルです。)

 


model.summary()

# 以下出力
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_1 (Dense)              (None, 16)                48        
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 17        
=================================================================
Total params: 65
Trainable params: 65
Non-trainable params: 0
_________________________________________________________________

pythonでオブジェクトを保存するときは、pickleを使うことが多いですが、
kerasではその方法は推奨しないと明記されています。

How can I save a Keras model?

It is not recommended to use pickle or cPickle to save a Keras model.

その代わりに、saveやload_modelというメソッドが用意されていて、
HDF5形式で保存や読み込みができます。


# 保存
model.save("model.h5")

# 読み込み
from keras.models import load_model
model_0 = load_model("model.h5")

kerasで簡単なモデルを作成してみる

前回の記事でkerasを使えるようにしたので、
動作確認を兼ねて非常に単純なモデルを作ってみました。

線形分離可能なサンプルではつまらないので、半径の異なる2円のToyDataでやってみましょう。
scikit-learnにmake_circlesという関数があるので、これを使います。

早速データを生成して訓練データとテストデータに分け、訓練データの方を可視化してみます。


# 必要なモジュールのインポート
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_circles
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from keras.models import Sequential
from keras.layers.core import Dense

# データの準備
data, target = make_circles(n_samples=2000, noise=0.2, factor=0.3)
X_train, X_test, y_train, y_test = train_test_split(
                                            data,
                                            target,
                                            test_size=0.2,
                                            stratify=target
                                        )
# 訓練データの可視化
fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(1, 1, 1, aspect='equal', xlim=(-2, 2), ylim=(-2, 2))
ax.scatter(X_train[y_train == 0, 0], X_train[y_train == 0, 1], marker="o")
ax.scatter(X_train[y_train == 1, 0], X_train[y_train == 1, 1], marker="x")
plt.show()

これが出力された散布図です。
一部ノイズはありますが概ね綺麗に別れていて、動作確認には手頃な問題になっていると思います。

早速ですが、kerasでモデルを作っていきます。
kerasのドキュメントはここです。日本語版があって便利ですね。
特に何も考えず、中間層が1層あるだけのシンプルなニューラルネットワークで作ります。
とりあえず動けば良いので、今回はDropoutもCallbackもなしで。


# モデルの構築
model = Sequential()
model.add(Dense(16, activation='tanh', input_shape=(2,)))
model.add(Dense(1, activation='sigmoid'))
model.summary()
model.compile(
        optimizer='adam',
        loss='binary_crossentropy',
        metrics=['acc']
)

# 以下、出力
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_1 (Dense)              (None, 16)                48        
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 17        
=================================================================
Total params: 65
Trainable params: 65
Non-trainable params: 0
_________________________________________________________________

モデルができたので、学習させます。
epochsもbatch_sizeも適当です。


# 学習
history = model.fit(
                    X_train,
                    y_train,
                    epochs=100,
                    batch_size=32,
                    validation_data=[X_test, y_test]
            )

正常に学習が進んだことを確認するために、
損失関数と正解率の変動を可視化してみましょう。


# Epoch ごとの正解率と損失関数のプロット
fig = plt.figure(figsize=(12, 12))
ax = fig.add_subplot(2, 1, 1, title="loss")
ax.plot(history.epoch, history.history["loss"], label="train_loss")
ax.plot(history.epoch, history.history["val_loss"], linestyle="-.", label="val_loss")
ax.legend()
ax = fig.add_subplot(2, 1, 2, title="acc")
ax.plot(history.epoch, history.history["acc"], label="train_acc")
ax.plot(history.epoch, history.history["val_acc"], linestyle="-.", label="val_acc")
ax.legend()
plt.show()

できたグラフがこちら。
順調に学習できていますね。

訓練データで評価してみましょう。
もともとやさしい問題なので、なかなかの正解率です。


print(classification_report(y_train, model.predict_classes(X_train)))

# 出力結果
             precision    recall  f1-score   support

          0       0.95      0.96      0.95       800
          1       0.96      0.95      0.95       800

avg / total       0.95      0.95      0.95      1600

最後に、決定境界を可視化してみます。
以前紹介した、等高線のプロットを使います。


# 決定境界の可視化
X_mesh, Y_mesh = np.meshgrid(np.linspace(-2, 2, 401), np.linspace(-2, 2, 401))
Z_mesh = model.predict_classes(np.array([X_mesh.ravel(), Y_mesh.ravel()]).T)
Z_mesh = Z_mesh.reshape(X_mesh.shape)
fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(1, 1, 1, aspect='equal', xlim=(-2, 2), ylim=(-2, 2))
ax.contourf(X_mesh, Y_mesh, Z_mesh, alpha=0.2)
plt.scatter(X_test[y_test == 0, 0], X_test[y_test == 0, 1], marker="o")
plt.scatter(X_test[y_test == 1, 0], X_test[y_test == 1, 1], marker="x")
plt.show()

出力がこちら。
真円にはなっていませんが、きちんと円の内側と外側にデータを分離できました。

Tensorflowをインストールしようとしたら出たエラー

MacのOSをMojaveにあげてから、Homebrewや、pyenvなどを全部入れ直しになってしまったので、
TensorflowやKeras も消えてしまっていましたと。

ということで、サクッと pip インストールを試みたのですが、
下記のエラーが出て失敗しました。

~$ pip install tensorflow
Collecting tensorflow
  Could not find a version that satisfies the requirement tensorflow (from versions: )
No matching distribution found for tensorflow

原因はpythonのバージョンが 3.7.0だったことです。

こちらの記載を見ると、3.4〜3.6じゃないとダメだと書いてあります。
Install TensorFlow with pip
Requires Python 3.4, 3.5, or 3.6

みなさんいろんな方法で回避をここみられていますが、
まぁ、せっかくpyenvで仮想環境として構築しているので、もっと古いバージョンの仮想環境を作りましょう。

ちなみに今入っているバージョンは
anaconda3-5.3.1
です。

anacondaのバージョンとpythonのバージョンの対応表があれば便利なのですが、なかなか見つかりません。
とりあえず、 anaconda3-5.2.0 を試してみましょう。


$ pyenv install anaconda3-5.2.0
$ pyenv global anaconda3-5.2.0
$ python --version
Python 3.6.5 :: Anaconda, Inc.

運良く? 3.6が入りました。

あとは、改めて tensorflowをpipインストールします。
kerasも入れておきましょう。
改めて見るとkerasの方もpythonは3.6までしか対応してないって書いてありますね。
(kerasのドキュメント)


pip install tensorflow
pip install keras

これで再びkerasとtensorflowを使える環境が整いました。

20ニュースグループのテキストデータを読み込んでみる

職場にはテキストデータは大量にあるのですが、自宅での学習で自然言語処理にについて学ぼうとすると途端にデータ不足に悩まされます。
そんなわけで青空文庫からデータを持ってくるような記事をこのブログに書いているのですが、
実はscikit-learnの付属のデータセットにもテキストデータはあります。(ただし英語)

Tfidfや、word2vecを動かしてみるには十分なので、それの取得方法を紹介します。
ドキュメントはこちら。
sklearn.datasets.fetch_20newsgroups

インポートして、引数でsubsetを指定することで訓練データとテストデータを入手できます。未指定だと訓練データのみです。両方一度に入手するためにはsubset="all"を指定する必要があります。
僕は始めはそれを知らず引数なしで実行して、訓練データを2つに分けて使ってました。


from sklearn.datasets import fetch_20newsgroups
twenty_train = fetch_20newsgroups(subset="train") # 引数省略可能。
twenty_test = fetch_20newsgroups(subset="test")
# trainとtestを同時に入手したい時は  subset="all" を指定。

# 件数の確認
print(len(twenty_train.data)) # 11314
print(len(twenty_test.data)) # 7532

初回実行時にダウンロードされ、data_home で指定したパスか、デフォルトでは~/scikit_learn_dataにデータが保存されます。
そのため、1回目だけは時間がかかりますが、2回目からは高速です。

twenty_train と、 twenty_test には辞書型で各情報が入ります。
twenty_train.keys()を実行すると、
dict_keys(['data', 'filenames', 'target_names', 'target', 'DESCR'])
と出ます。

DESCR にデータの説明、 target_namesに各ニュースグループの名前が入っているので、それぞれ一度は見ておくことをお勧めします。


# 出力
['alt.atheism',
 'comp.graphics',
 'comp.os.ms-windows.misc',
 'comp.sys.ibm.pc.hardware',
 'comp.sys.mac.hardware',
 'comp.windows.x',
 'misc.forsale',
 'rec.autos',
 'rec.motorcycles',
 'rec.sport.baseball',
 'rec.sport.hockey',
 'sci.crypt',
 'sci.electronics',
 'sci.med',
 'sci.space',
 'soc.religion.christian',
 'talk.politics.guns',
 'talk.politics.mideast',
 'talk.politics.misc',
 'talk.religion.misc']

普段は、 data に入ってるテキストデータの配列と、正解ラベルであるtargetをつかえばOKだと思います。

このほか、BoWに変換済みの、 fetch_20newsgroups_vectorized というのもありますが、
あまり使う機会はないのではないかなぁと思います。
学習目的であれば、前処理なども自分で経験したほうがいいと思うので。

ちなみに、scikit-learnのチュートリアルには、
このデータセットを用いて、tfidfとナイーブベイズでモデルを作るものがあります。
Working With Text Data
(このページは見ていませんでしたが、)僕も自然言語処理を始めたばかりの頃、全く同じような内容で勉強をスタートしたので、非常に懐かしく思いました。
その時の試したファイルはあるので、そのうちこのブログにもまとめ直します。

t-SNEでDigitsを次元圧縮して可視化してみた

特に意図はないのですが、これまで高次元のデータを次元削減して可視化する時はPCAをよく使っていました。
基本的には線形変換なので、非線形な構造を持ってるデータはうまく特徴を捉えられません。
(それはそれで確認する意味があると思いますが。)

最近は、t-SNEという手法を使っている人が多いようなので、やってみたメモです。
irisだとPCAで十分うまく次元削減できてしまうので、今回はdigitsを使います(8*8の手書き数字画像データ)

t-SNEの論文

t-SNE自体の実装は、scikit-leearnを使います。
ドキュメントはここ


# ライブラリインポート
from sklearn.datasets import load_digits
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt

# データ準備
digits = load_digits()
X = digits.data
y = digits.target

# t-SNEの実行
tsne = TSNE(n_components=2)
X_tsne = tsne.fit_transform(X)

# 可視化
x_max, x_min = X_tsne[:, 0].max() * 1.05, X_tsne[:, 0].min() * 1.05
y_max, y_min = X_tsne[:, 1].max() * 1.05, X_tsne[:, 1].min() * 1.05
fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(1, 1, 1, xlim=(x_min, x_max), ylim=(y_min, y_max))
ax.set_title("t-SNE")
for i, target in enumerate(y):
    ax.text(X_tsne[i, 0], X_tsne[i, 1], target)
plt.show()

これを実行して表示される画像がこちらです。

一部、変なところに分類されている数字があったり、1が複数のグループに分かれていたりするところはありますが、
非常に見事に分類できています。
これを好んで使う人がいるのも納得です。
高次元のデータの可視化のツールとして提唱されているだけはあります。

ちなみに、PCAで2次元に圧縮したのがこれ。

t-SNEと全く違う結果になっていますね。
(だからといって、PCAという手法自体が劣るというわけではないので注意です。)

scikit-learnでテキストをBoWやtfidfに変換する時に一文字の単語も学習対象に含める

本当に初めて自然言語処理をやった頃のメモから記事化。

テキストを分かち書きしたあと、BoW(Bag of Words) や tfidfに変換するとき、
scikit-learnを使うと便利です。
sklearn.feature_extraction.text に次の二つのクラスが定義されていて、
それぞれ語彙の学習と BoW /tfidfへの変換を行ってくれます。

CountVectorizer
TfidfVectorizer

ただ、これらのクラスはデフォルトパラメーターに少し癖があり注意していないと一文字の単語を拾ってくれません。
TfidfVectorizer の方を例にやってみましょう。


from sklearn.feature_extraction.text import TfidfVectorizer

model = TfidfVectorizer()
text_list = [
    "すもも も もも も もも の うち",
    "隣 の 客 は よく 柿 食う 客 だ",
]
model.fit(text_list)
print(model.vocabulary_)

これの出力結果が、下記になります。
これらが学習した単語です。
{‘すもも’: 1, ‘もも’: 2, ‘うち’: 0, ‘よく’: 3, ‘食う’: 4}

“も” や “の” が入ってないのはまだ許容範囲としても、
“隣”や”客”がvocabulary_に含まれないのは困ります。

これらは TfidfVectorizer のインスタンスをデフォルトのパラメーターで作ったことに起因します。

modelの内容を表示してみましょう。(jupyterで model とだけ入力して実行した結果)


>>> model
TfidfVectorizer(analyzer='word', binary=False, decode_error='strict',
        dtype=, encoding='utf-8', input='content',
        lowercase=True, max_df=1.0, max_features=None, min_df=1,
        ngram_range=(1, 1), norm='l2', preprocessor=None, smooth_idf=True,
        stop_words=None, strip_accents=None, sublinear_tf=False,
        token_pattern='(?u)\\b\\w\\w+\\b', tokenizer=None, use_idf=True,
        vocabulary=None)

注目するべきはここです。
token_pattern='(?u)\\b\\w\\w+\\b’
\\ は \をエスケープしたものなので、以下の説明中では\\ は \と書きます。
正規表現で、\b は単語の境界 、 \wは、単語構成文字、\w+ は1文字以上の単語構成文字の連続 を意味します。
そのため、 \w\w+ は2文字以上の単語構成文字の連続を意味し、
\b\w\w+\b は “単語の境界、2文字以上の単語構成文字、単語の境界” と続く文字列を単語のパターンとして採用するという指定になります。
そのため、 一文字の単語は学習対象から抜けています。

英語なら a や I が抜けるだけですが日本語では多くの漢字が抜けてしまうので、これは困りますね。

ということで、最初に TfidfVectorizer のオブジェクトを作る時には token_pattern を指定しましょう。
1文字以上を含めたいだけなので、 ‘(?u)\\b\\w+\\b’ にすれば大丈夫です。


from sklearn.feature_extraction.text import TfidfVectorizer

model = TfidfVectorizer(token_pattern='(?u)\\b\\w+\\b')
text_list = [
    "すもも も もも も もも の うち",
    "隣 の 客 は よく 柿 食う 客 だ",
]
model.fit(text_list)
print(model.vocabulary_)

これで出力は下記のようりなり、”隣”も”客”も含まれます。
{‘すもも’: 1, ‘も’: 5, ‘もも’: 6, ‘の’: 3, ‘うち’: 0, ‘隣’: 10, ‘客’: 8, ‘は’: 4, ‘よく’: 7, ‘柿’: 9, ‘食う’: 11, ‘だ’: 2}

これ以外にも “-” (ハイフン) などが単語の境界として設定されていて想定外のところで切られたり、
デフォルトでアルファベットを小文字に統一する設定になっていたり(lowercase=True)と、注意する時に気をつけないといけないことが、結構あります。

norm,smooth_idf,sublinear_tf などの影響でよくある自然言語処理の教科書に載っている数式と実装が違うのも注意ですね。
この辺はまた別の機会にまとめようと思います。