Fashion-MNIST データセットの紹介

最近受けている講座の中で、kerasに同梱されているデータセットの中に、Fashion-MNISTという画像データがあることを知りました。
画像データを使って機械学習を試す時(と言っても、自分が画像データを扱うことはほぼないのですが)は、
大抵手書き数字のMNISTデータを使っていて、いささか飽きていたので、早速これを確認してみました。

ドキュメントはこちら。
Fashion-MNIST database of fashion articles
手書き数字のMNISTと完全互換らしく、クラス数は10個、画像サイズは28*28、データ件数も訓練データ60,000とテストデータ10,000のデータになっています。

読み込みもほぼ同じです。

今回はデータセットの紹介なので、読み込んで実際に画像を表示してみるところまでやってみます。


from tensorflow.keras.datasets import fashion_mnist
import matplotlib.pyplot as plt

(x_train, y_train), (x_test, y_test) = fashion_mnist.load_data()
print("x_train.shape: ", x_train.shape)
print("y_train.shape: ", y_train.shape)
print("x_test.shape: ", x_test.shape)
print("y_test.shape: ", y_test.shape)
"""
x_train.shape:  (60000, 28, 28)
y_train.shape:  (60000,)
x_test.shape:  (10000, 28, 28)
y_test.shape:  (10000,)
"""
# 0〜9が どんなアイテムに対応しているかの対応はdatasetに含まれないので別途作る
target_name = {
        0: "T-shirt/top",
        1: "Trouser",
        2: "Pullover",
        3: "Dress",
        4: "Coat",
        5: "Sandal",
        6: "Shirt",
        7: "Sneaker",
        8: "Bag",
        9: "Ankle boot",
    }

fig = plt.figure(figsize=(10, 13), facecolor="w")
for i in range(100):
    ax = fig.add_subplot(10, 10, i+1)
    ax.set_xticklabels([])
    ax.set_yticklabels([])
    ax.imshow(x_train[y_train == i // 10][i % 10], cmap="gray_r")
    if i % 10 == 0:
        # アイテムの最初の画像にタイトルつける
        ax.set_title(target_name[i//10])

plt.show()

結果として表示される画像がこちらです。

手書き数字のMNISTよりちょっと楽しそうなデータですね。
そして結構難易度たかそうです。

scipyで階層的クラスタリングの樹形図を書く時に上位のクラスタのみ表示する

以前書いた、 scipyで階層的クラスタリング の記事の続きです。

階層的クラスタリングを行って結果を樹形図(デンドログラム)で表示すると、元のデータが多い場合は非常にみづらいものになります。
このような時は、樹形図の表示を途中で打ち切って必要なクラスタ分だけ表示するとクラスタ間の関係が掴みやすくなります。
scipyのdendrogram関数では、 truncate_mode というオプションが用意されており、これと、 値pを適切に指定することで実現できます。
ドキュメント: scipy.cluster.hierarchy.dendrogram

truncate_mode はNone, lastp, levelの3つの値を取ります。
Noneがデフォルト、lastpがデンドログラムの上から数えてp個のノードを残す、
levelは逆に下から数えて、各ノードがp回マージされるように動きます。

それぞれ動かしてみましょう。
truncate_mode=”lastp”, p=16

truncate_mode=”level”, p=3
の場合に、表示されるノードがどちらも16個になるので、動きの違いも見ておきます。


from sklearn.datasets import load_iris
import matplotlib.pyplot as plt
from scipy.cluster.hierarchy import linkage
from scipy.cluster.hierarchy import dendrogram

# データ取得。
X = load_iris().data

# ユークリッド距離とウォード法を使用してクラスタリング
z = linkage(X, metric='euclidean', method='ward')

# 結果を可視化
fig = plt.figure(figsize=(8, 15), facecolor="w")
ax = fig.add_subplot(3, 1, 1, title="樹形図: 全体")
dendrogram(z)
ax = fig.add_subplot(3, 1, 2, title="樹形図: lastp 16")
dendrogram(z, truncate_mode="lastp", p=16)
ax = fig.add_subplot(3, 1, 3, title="樹形図: level 3")
dendrogram(z, truncate_mode="level", p=3)
plt.show()

結果がこちら。

truncate_mode=”lastp” は、樹形図全体の上部の部分をそのまま切り出したものになっていて、
truncate_mode=”level” の方は、各枝に至るまでの分岐回数が一定になっているのがわかります。
また、どちらも図がすっきりしてみやすくなりました。

Optunaで学習曲線を可視化する

Optunaの機能や挙動についてもっとしっかり理解したいので、うまい可視化の方法を考えていたのですが、
ドキュメント中に、Visualizationのセクションを見つけたので、
まずはこれを試してみることにしました。

現段階(version 0.16.0)では、定義されている関数は
optuna.visualization.plot_intermediate_values(study)
だけのようなのでこれを試します。
(最初に試した時、 Plotly が入ってないという警告が出たのでpipインストールしておきます。)

早速使ってみると、次のようにトライアルごとの学習の進捗が可視化できました。

Optunaで枝刈りも使ってKerasのパラメータチューニングの記事で紹介したコードを動かした後に、jupyterで次のコードを実行します。


optuna.visualization.plot_intermediate_values(study)

jupyterで動かすと、マウスカーソルを当てた時にそれぞれの線が何回めのトライアルなどかなどの情報がポップアップされるので、
ぜひ試していただきたいです。
pngエクスポート機能もあり、それで出力した画像がこちら。

見込みがない試行がさっさと打ち切られているのがわかります。
(正解率が低いのに最後まで走ってるのは序盤の試行です。)

Optunaの枝刈りの基準を変更する

前回の記事: Optunaで枝刈りも使ってKerasのパラメータチューニング
の続きです。

Kerasのパラメーターチューニングの中で、枝刈りを使ってみたのですが
100回の試行の中で最後まで走ったのが1/5の19回だけというのは少し不安です。
もしかしたらこのくらいで適切なのかもしれませんが、まだ慣れないライブラリなのでよく感覚がつかめていません。

ということで、もう少し枝刈りの基準を緩める方法を探しました。
そのためには、別のPrunerを使うと良いようです。
前回の記事で使ったのは、MedianPruner という、それまでの試行の中央値より成績が悪ければ打ち切るPrunerでした。
ただ、探索対象のパラメーターの中に学習率等も入っていましたし、序盤のepockで成績が悪くても最終的に高い成果をあげるものもある気がします。

そこで、PercentilePrunerを試してみます。
ドキュメントはこちら。 

percentile – Percentile which must be between 0 and 100 inclusive (e.g., When given 25.0, top of 25th percentile trials are kept).

とある通りなので、上位60パーセンタイルくらいを許容するようにすれば、中央値(50パーセンタイル)を基準にするより緩くなりそうです。
(ちなみに75パーセンタイルで試したらなかなか枝刈りされなくなりました。)

前回のコードのprunerを指定してる部分を次のように書き換えて試してみます。


study = optuna.create_study(
                direction="maximize",
                # pruner=optuna.pruners.MedianPruner()
                pruner=optuna.pruners.PercentilePruner(60.0)
            )

最終的な結果はこちら。
試行のうち、39回は最後まで走るようになり、
正解率も前回の 0.92734375より少し良くなりました。
(この後も何度か試しましたが、必ずしよくなるわけではないです。)


"""
Study statistics: 
  Number of finished trials:  100
  Number of pruned trials:  61
  Number of complete trials:  39
Best trial:
  Value:  0.928125
  Params: 
    n_layers: 1
    n_units_l0: 126.84330007270901
    dropout_l0: 0.4023051335982798
    lr: 0.007029694763599239
"""

今回は、Prunerを変更することで枝刈りの具合を調整できるということが確認できました。
また、結果的に枝刈りが少ない方が最終的な成績は少しだけ良かったのですが、その差は軽微であることもわかりました。
今回かなり小さなデータで試しているのですが、
本当に学習に長時間かかるモデルを試す場合は、もっと高頻度に枝刈りする方がよい場合もありそうです。

Optunaで枝刈りも使ってKerasのパラメータチューニング

少し間が空きましたが、再びOptunaの記事です。
今度はKerasのモデルのパラメーターをチューニングしてみます。
その際、枝刈りも使いました。

Optunaに、Keras用のコールバックが用意されていて、
これを使うと、エポック毎に判定が走り、学習が十分進んでいないと学習を打ち切ってくれるので、
効率的に探索ができます。
classoptuna.integration.KerasPruningCallback

githubのサンプルコードを(少しだけ修正しましたが)ほぼ写経しながら、
コードを書いてみました。


import keras
from keras.datasets import mnist
from keras.utils import to_categorical
from keras.layers import Dense
from keras.layers import Dropout
from keras.models import Sequential
import optuna
from optuna.integration import KerasPruningCallback

# 訓練データとテストデータの件数を指定
N_TRAIN_EXAMPLES = 3840
N_TEST_EXAMPLES = 1280
BATCHSIZE = 128
CLASSES = 10
EPOCHS = 20

# MNISTのデータを準備
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# 特徴量を0~1に正規化する
x_train = x_train.reshape(60000, 784)[:N_TRAIN_EXAMPLES]/255
x_test = x_test.reshape(10000, 784)[:N_TEST_EXAMPLES]/255

# ラベルを1 hot 表現に変換
y_train = to_categorical(y_train[:N_TRAIN_EXAMPLES], CLASSES)
y_test = to_categorical(y_test[:N_TEST_EXAMPLES], CLASSES)


def create_model(trial):
    """MLPモデルの構築
    """

    # 層の数を選択する
    n_layers = trial.suggest_int("n_layers", 1, 3)
    model = Sequential()
    for i in range(n_layers):
        num_hidden = int(
                            trial.suggest_loguniform(
                                    f"n_units_l{i}",
                                    4,
                                    128
                                )
                            )
        model.add(Dense(num_hidden, activation="relu"))
        dropout = trial.suggest_uniform(f"dropout_l{i}", 0.2, 0.5)
        model.add(Dropout(rate=dropout))
    model.add(Dense(CLASSES, activation="softmax"))

    # 学習率も最適化する。
    lr = trial.suggest_loguniform("lr", 1e-5, 1e-1)
    model.compile(loss="categorical_crossentropy",
                  optimizer=keras.optimizers.RMSprop(lr=lr),
                  metrics=["acc"])

    return model


def objective(trial):
    # 前のセッションをクリアする
    keras.backend.clear_session()
    # モデル作成
    model = create_model(trial)

    # モデルの学習
    # KerasPruningCallbackでepoch毎に枝刈りの判定。
    model.fit(
        x_train,
        y_train,
        batch_size=BATCHSIZE,
        callbacks=[KerasPruningCallback(trial, "val_acc")],
        epochs=EPOCHS,
        validation_data=(x_test, y_test),
        verbose=2
    )

    # モデルの評価
    score = model.evaluate(x_test, y_test, verbose=0)
    return score[1]


study = optuna.create_study(
                direction="maximize",
                pruner=optuna.pruners.MedianPruner()
            )
study.optimize(objective, n_trials=100)
pruned_trials = [t for t in study.trials if t.state == optuna.structs.TrialState.PRUNED]
complete_trials = [t for t in study.trials if t.state == optuna.structs.TrialState.COMPLETE]
print("Study statistics: ")
print("  Number of finished trials: ", len(study.trials))
print("  Number of pruned trials: ", len(pruned_trials))
print("  Number of complete trials: ", len(complete_trials))

# 結果の表示
print("Best trial:")
trial = study.best_trial
print("  Value: ", trial.value)
print("  Params: ")
for key, value in trial.params.items():
    print(f"    {key}: {value}")

"""
探索・学習中に出てくる大量の出力は省略。

Study statistics: 
  Number of finished trials:  100
  Number of pruned trials:  81
  Number of complete trials:  19
Best trial:
  Value:  0.92734375
  Params: 
    n_layers: 1
    n_units_l0: 114.75796459517468
    dropout_l0: 0.3088006045981605
    lr: 0.0035503337631505026

"""

100回のトライアルのうち、最後まで走ったのは19回だけのようです。
もっと色々なパターンを試さないと、これがどれほど効率的なのかと、
最終的な結果がどれだけ優れてるのかってのは判断しかねるのですが、
データが少ない中で90%程度の正解率も出していますし、
これまでMNISTを触ってきた経験からあまり違和感のないパラメータが選ばれている感じはします。

create_model の中で、最初に選ばれるlayerの数によって、
ユニットの数やdropoutの割合のなどのパラメーター数が変わるのですが、
Optunaならそのあたりもかなり柔軟にかけることを実感できました。

参考ですが、n_layersが3だったtraialでは、次のようにパラメータが多くサンプリングされています。


print(study.trials[3].params)
"""
{
    'n_layers': 3,
    'n_units_l0': 8.18713988230328,
    'dropout_l0': 0.3510040004516323,
    'n_units_l1': 99.80407964220358,
    'dropout_l1': 0.22630235294167378,
    'n_units_l2': 6.585776717612663,
    'dropout_l2': 0.3731719751601379,
    'lr': 0.000710144846966038
}
"""

なお、n_layersは1の方が良いようで、かなり早い段階で、1の場合ばかり探索されるようになっています。
時短のためデータを減らしているのも影響していそうですね。


print([study.trials[i].params["n_layers"] for i in range(100)])
"""
[3, 1, 1, 3, 2, 2, 2, 1, 1, 3, 1, 1, 1, 1, 2, 1, 2, 1, 1, 2,
 1, 1, 1, 1, 1, 1, 2, 1, 1, 2, 1, 1, 1, 1, 3, 1, 1, 2, 1, 1,
 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
 2, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
"""

Optunaで枝刈りをやってみる

自分がいつも使っているhpyeroptではなく、新たにOptunaを覚えて使ってみようと思ったメインの目的が、枝刈りの機能です。
これは要するに、学習の途中で見込みのないハイパーパラメーターは処理を打ち切ってしまって、
短時間で効率的に探索を進めようという機能です。

チュートリアルとしてはこちらにコードのサンプルがあります。
Pruning Unpromising Trials

また、打ち切りに使う基準には複数のアルゴリズムが使え、こちらのページにリファレンスがあります。
Pruners

では早速やってみましょう。コードはチュートリアルの例をベースに少し書き直しました。


from sklearn.datasets import load_iris
from sklearn.linear_model import SGDClassifier
from sklearn.model_selection import train_test_split
import optuna

iris = load_iris()
classes = list(set(iris.target))

train_x, test_x, train_y, test_y = \
    train_test_split(iris.data, iris.target, test_size=0.25, random_state=0)


def objective(trial):

    alpha = trial.suggest_loguniform('alpha', 1e-5, 1e-1)
    clf = SGDClassifier(alpha=alpha)

    for step in range(100):
        clf.partial_fit(train_x, train_y, classes=classes)

        # Report intermediate objective value.
        intermediate_value = 1.0 - clf.score(test_x, test_y)
        trial.report(intermediate_value, step)

        # Handle pruning based on the intermediate value.
        if trial.should_prune():
            print("step", step, "で打ち切り")  # 何回めのエポックで打ち切ったか見るために追加
            raise optuna.structs.TrialPruned()
    return 1.0 - clf.score(test_x, test_y)


# Set up the median stopping rule as the pruning condition.
study = optuna.create_study(pruner=optuna.pruners.MedianPruner())
study.optimize(objective, n_trials=20)
print(study.best_params)
print(study.best_value)

出力は次のようになります。


[I 2019-10-09 00:45:38,562] Finished trial#0 resulted in value: 0.368421052631579. Current best value is 0.368421052631579 with parameters: {'alpha': 0.0002196017670543267}.
[I 2019-10-09 00:45:38,757] Finished trial#1 resulted in value: 0.10526315789473684. Current best value is 0.10526315789473684 with parameters: {'alpha': 0.0006773222557376204}.
[I 2019-10-09 00:45:38,967] Finished trial#2 resulted in value: 0.39473684210526316. Current best value is 0.10526315789473684 with parameters: {'alpha': 0.0006773222557376204}.
[I 2019-10-09 00:45:39,201] Finished trial#3 resulted in value: 0.02631578947368418. Current best value is 0.02631578947368418 with parameters: {'alpha': 0.0037602050725428606}.
[I 2019-10-09 00:45:39,462] Finished trial#4 resulted in value: 0.3421052631578947. Current best value is 0.02631578947368418 with parameters: {'alpha': 0.0037602050725428606}.
[I 2019-10-09 00:45:39,758] Finished trial#5 resulted in value: 0.3157894736842105. Current best value is 0.02631578947368418 with parameters: {'alpha': 0.0037602050725428606}.
[I 2019-10-09 00:45:40,094] Finished trial#6 resulted in value: 0.052631578947368474. Current best value is 0.02631578947368418 with parameters: {'alpha': 0.0037602050725428606}.
step 4 で打ち切り
[I 2019-10-09 00:45:40,126] Setting status of trial#7 as TrialState.PRUNED. 
step 1 で打ち切り
[I 2019-10-09 00:45:40,211] Setting status of trial#8 as TrialState.PRUNED. 
[I 2019-10-09 00:45:40,625] Finished trial#9 resulted in value: 0.39473684210526316. Current best value is 0.02631578947368418 with parameters: {'alpha': 0.0037602050725428606}.
[I 2019-10-09 00:45:41,195] Finished trial#10 resulted in value: 0.07894736842105265. Current best value is 0.02631578947368418 with parameters: {'alpha': 0.0037602050725428606}.
[I 2019-10-09 00:45:41,675] Finished trial#11 resulted in value: 0.02631578947368418. Current best value is 0.02631578947368418 with parameters: {'alpha': 0.0037602050725428606}.
[I 2019-10-09 00:45:42,132] Finished trial#12 resulted in value: 0.23684210526315785. Current best value is 0.02631578947368418 with parameters: {'alpha': 0.0037602050725428606}.
[I 2019-10-09 00:45:42,605] Finished trial#13 resulted in value: 0.3157894736842105. Current best value is 0.02631578947368418 with parameters: {'alpha': 0.0037602050725428606}.
step 11 で打ち切り
[I 2019-10-09 00:45:42,691] Setting status of trial#14 as TrialState.PRUNED. 
[I 2019-10-09 00:45:43,242] Finished trial#15 resulted in value: 0.07894736842105265. Current best value is 0.02631578947368418 with parameters: {'alpha': 0.0037602050725428606}.
[I 2019-10-09 00:45:43,894] Finished trial#16 resulted in value: 0.02631578947368418. Current best value is 0.02631578947368418 with parameters: {'alpha': 0.0037602050725428606}.
step 1 で打ち切り
[I 2019-10-09 00:45:43,929] Setting status of trial#17 as TrialState.PRUNED. 
step 1 で打ち切り
[I 2019-10-09 00:45:44,067] Setting status of trial#18 as TrialState.PRUNED. 
[I 2019-10-09 00:45:44,756] Finished trial#19 resulted in value: 0.1842105263157895. Current best value is 0.02631578947368418 with parameters: {'alpha': 0.0037602050725428606}.
{'alpha': 0.0037602050725428606}
0.02631578947368418

〜 as TrialState.PRUNED. とメッセージが出てるのを見るとわかる通り、結構な頻度で早い段階で打ち切られています。
alpha や intermediate_value の値も随時print出力すると、挙動の理解が深まるのでおおすすめです。

さて、期待の時間短縮効果ですが、試してみたところこの例ではほとんどないどころか、余計に時間がかかるようでした。
コード中の以下の部分をコメントアウトして実行すると、もっと早く探索が終わります。


        # Report intermediate objective value.
        intermediate_value = 1.0 - clf.score(test_x, test_y)
        trial.report(intermediate_value, step)

        # Handle pruning based on the intermediate value.
        if trial.should_prune():
            print("step", step, "で打ち切り")  # 何回めのエポックで打ち切ったか見るために追加
            raise optuna.structs.TrialPruned()

時間が余計にかかるようになってしまった原因は、
intermediate_value = 1.0 – clf.score(test_x, test_y)
の部分で、評価を頻繁に行っているせいだと思われます。

partial_fit にもっと長時間かかるサンプルであればきっと時短効果が得られると思うので、
次はそういう例で試そうと思います。

Optunaでscikit-learnのパラーメーター最適化

前回に続いてOptunaの記事です。
今回はscikit-learnのモデルをチューニングしてみましょう。

ドキュメントのうち、最初に読むのはこちら。
Advanced Configurations
少々分かりにくいですが、これでカテゴリカルナ値や、一様分布、対数一様分布からのサンプリング方法が分かります。
ただ、全然具体的で無いので、githubの方にあるexamplesもみましょう。
https://github.com/pfnet/optuna/tree/master/examples

今回のコードはその中にある、 sklearn_simple.py を参考にして書きました。
(自分の理解のため、少々アレンジしていますが、元々のより良いものにはなって無いです)

データは定番のirisを使い、SVMとランダムフォレストの二つのモデルで、それぞれ1種類ずつパラメーターを最適化し、
最も良いものを探索しています。


import optuna
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
iris = load_iris()
X = iris.data
y = iris.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)


def objective(trial):

    classifier_name = trial.suggest_categorical(
                        'classifier',
                        ['SVC', 'RandomForest']
                    )
    if classifier_name == 'SVC':
        svc_c = trial.suggest_loguniform('svc_c', 1e-10, 1e10)
        classifier_obj = SVC(C=svc_c, gamma='auto')
    else:
        rf_max_depth = int(trial.suggest_int('rf_max_depth', 2, 32))
        classifier_obj = RandomForestClassifier(
                max_depth=rf_max_depth,
                n_estimators=10
        )
    classifier_obj.fit(X_train, y_train)
    return classifier_obj.score(X_test, y_test)


study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100)
print(study.best_trial)

# 出力結果
"""
FrozenTrial(number=15, state=,
 value=1.0, datetime_start=datetime.datetime(2019, 10, 8, 0, 43, 30, 243112),
 datetime_complete=datetime.datetime(2019, 10, 8, 0, 43, 30, 310756),
 params={'classifier': 'RandomForest', 'rf_max_depth': 10},
 distributions={'classifier': CategoricalDistribution(choices=('SVC', 'RandomForest')),
 'rf_max_depth': IntUniformDistribution(low=2, high=32)},
 user_attrs={}, system_attrs={'_number': 15},
 intermediate_values={}, trial_id=15)
"""

今回は RandomForest で、 rf_max_depth を 10とするのが最適だったようです。
(実行するたびに結果が変わります。)

これはこれで非常に単純な例なのですが、それでも、最初のカテゴリカル変数によって以降の変数たちを
if文で場合分けしてかけるなど、hyperoptに比べて柔軟な探索ができる、ってのは少しわかったような気がします。
(もっとも、このくらいならhpyeroptでも探索できるので、真価が発揮されるのはもっと複雑な例の時だと思います。)

Optunaを触ってみた

早く試さないといけないといけないと思いながら先延ばしにしていたOptunaをいよいよ触ってみました。

公式ページ: Optuna – A hyperparameter optimization framework
ドキュメント: Welcome to Optuna’s documentation! — Optuna 0.16.0 documentation

まずはインストールと、最も簡単なサンプルから動かしてみましょう。コードはチュートリアルの写経です。

インストールはpipでできました。


$ pip install optuna

そして、チュートリアルの First Optimization をみて、
2次関数の最小値を求めてみましょう。


import optuna
def objective(trial):
    x = trial.suggest_uniform('x', -10, 10)
    return (x - 2) ** 2


study = optuna.create_study()
study.optimize(objective, n_trials=100)

# 結果表示
print(study.best_params)
# {'x': 1.9825559314627845}
print(study.best_value)
# 0.0003042955271310708

正しそうな結果が得られていますね。

study.optimizeを繰り返し実行することで、追加で探索することもできるようです。
これは便利そう。


# この時点での探索回数
print(len(study.trials))
# 100

# 追加で探索する
study.optimize(objective, n_trials=100)

# 結果表示
print(study.best_params)
# {'x': 1.9857135612242507}
print(study.best_value)
# 0.00020410233289323346
print(len(study.trials))
# 200

とりあえず一番シンプルな例は試したということで、今後もっと実用的な例を試していきたいと思います。
感想としては、hyperoptとほとんど同じように使えるという噂を聞いていたのですが、若干使用感は違うかなぁという気もします。
ただ、最近は Optuna のほうが良い評判を聞くことが多いのでこれに慣れていった方がよさそうです。

参考: hyperoptのインストールと最も簡単な例

Kerasの学習履歴(History)をDataFrameに変換する

Kerasのちょっとした小ネタです。
Kerasで作ったモデルをfitすると、戻り値として損失関数や正解率を格納したHistoryオブジェクトが返されます。
それを使って、学習の進みなどを可視化できます。

例えばこちらの記事を参照: CNNで手書き数字文字の分類
こちらでは可視化だけで10行くらいのコードになっています。

で、改めてHistoryの中身をみてみると、DataFrameに変換できる形式であることに気づきました。
長いので、実データは {数値のリスト} に置換しましたが、次のようなイメージです。


print(history.history)
'''
{'val_loss': {数値のリスト},
 'val_acc': {数値のリスト},
 'loss': {数値のリスト},
 'acc': {数値のリスト}
'''

これは容易にDataFrameに変換できます。


print(pd.DataFrame(history.history))
'''
    val_loss  val_acc      loss       acc
0   0.106729   0.9677  0.590888  0.811850
1   0.072338   0.9764  0.227665  0.931233
2   0.059273   0.9800  0.174741  0.948033
3   0.047335   0.9837  0.149136  0.955500
4   0.042737   0.9859  0.132351  0.960167
5   0.039058   0.9868  0.121810  0.964600
6   0.034511   0.9881  0.110556  0.967050
7   0.032818   0.9882  0.105487  0.967867
8   0.032139   0.9893  0.100333  0.970167
9   0.030482   0.9898  0.095932  0.971383
10  0.027904   0.9900  0.089120  0.973267
11  0.028368   0.9898  0.086760  0.973683
'''

DataFrameになると、これ自体を分析しやすいですし、さらに非常に容易に可視化できます。


history_df = pd.DataFrame(history.history)
history_df.plot()
plt.show()

たったこれだけです。以前の記事の可視化に比べると非常に楽ですね。
出力はこのようになります。

scikit-learnでK-分割交差検証

普段、機械学習のパラメーターを最適化するために
K分割交差検証をするときはGridSearchCVで済ましているのですが、
今回別の目的があって、K-分割交差検証のような分割が必要になりました。
参考:scikit-learn でグリッドサーチ

そこで、GridSearchCVの中でも使われているKFoldを使ってみたのでその記録です。
ドキュメントはこちら。
sklearn.model_selection.KFold

注意点は、splitした時に得られる結果が、データそのものではなくデータのインデックスである点くらいです。

実行してみるために、まずダミーデータを準備します。


from sklearn.model_selection import KFold
import numpy as np

# ダミーデータの準備
X = np.random.randint(20, size=(10, 2))
y = np.random.choice([0, 1], size=10)
print(X)
'''
[[ 2 12]
 [ 6  3]
 [ 0  3]
 [15  9]
 [19  0]
 [12 14]
 [ 0  0]
 [12 18]
 [ 1  9]
 [ 3 14]]
'''
print(y)
# [0 0 0 1 1 1 0 1 0 0]

次に、 KFoldを動かしてみましょう。
まず挙動を確認したいので、splitして得られるイテレータの中身を表示してみます。


kf = KFold(5, shuffle=True)
for train_index, test_index in kf.split(X):
    print("train_index:", train_index)
    print("test_index:", test_index)
    print()  # 1行開ける

# 以下出力
'''
train_index: [1 2 3 4 5 6 8 9]
test_index: [0 7]

train_index: [0 1 2 3 4 6 7 9]
test_index: [5 8]

train_index: [0 1 3 4 5 6 7 8]
test_index: [2 9]

train_index: [0 1 2 5 6 7 8 9]
test_index: [3 4]

train_index: [0 2 3 4 5 7 8 9]
test_index: [1 6]

'''

ご覧の通り、0〜9の値を、5つのグループに分けて、順番に一個をテスト用にしながら交差検証用のデータセットのインデックスを返してくれています。
インデックスを元にデータを分割するには、つぎのようにして別の変数に取り出すと以降のコードが読みやすくなります。


X_train, X_test = X[train_index], X[test_index]
y_train, y_test = y[train_index], y[test_index]