plot_roc_curveを試す

今回もscikit-learn 0.22.0 の新機能を試します。
今回はROC曲線を書いてくれる、plot_roc_curveです。
ドキュメント: sklearn.metrics.plot_roc_curve

元々、 roc_curve という機能はあったのですが、これを可視化するのは少しだけ面倒だったので結構期待していました。

ただ、試してみたところ、plot_roc_curveは2値分類のモデルにしか対応していないようです。
roc_curveは引数のpos_labelに対象のラベルを指定してあげれば多値分類にも対応しているので、今後に期待します。

ということで、2値分類のダミーデータを作成して、試してみます。
綺麗に分類できる問題だとROC曲線の価値がよくわからないので線形分離不可能な問題にしています。
最後にグラフを二つ出力していますが、左のが、生成したテストデータとモデルの決定境界、右側が、ROC曲線です。


from sklearn.metrics import plot_roc_curve
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
import numpy as np
import matplotlib.pyplot as plt

# 綺麗に線形分類することのできないダミーデータの生成。
X0 = np.clip(2 * np.random.randn(500, 2) + np.array([1, 1]), -7, 7)
X1 = np.clip(2 * np.random.randn(500, 2) + np.array([-1, -1]), -7, 7)
X = np.concatenate([X0, X1])
y = np.array([0]*500 + [1]*500)

# 学習データとテストデータに分ける
X_train, X_test, y_train, y_test = train_test_split(
                                    X,
                                    y,
                                    test_size=0.2,
                                    stratify=y,
                                )
# モデルの作成と学習
clf = LogisticRegression()
clf.fit(X_train, y_train)

# ここから可視化
fig = plt.figure(figsize=(12, 6), facecolor="w")
ax = fig.add_subplot(1, 2, 1, aspect='equal', xlim=(-7, 7), ylim=(-7, 7))
ax.set_title("テストデータと決定境界")

# データを散布図で表示する
ax.scatter(X_test[y_test == 0, 0], X_test[y_test == 0, 1], alpha=0.7)
ax.scatter(X_test[y_test == 1, 0], X_test[y_test == 1, 1], alpha=0.7)

# 決定境界を可視化する
X_mesh, Y_mesh = np.meshgrid(np.linspace(-7, 7, 401), np.linspace(-7, 7, 401))
Z_mesh = clf.predict(np.array([X_mesh.ravel(), Y_mesh.ravel()]).T)
Z_mesh = Z_mesh.reshape(X_mesh.shape)
ax.contourf(X_mesh, Y_mesh, Z_mesh, alpha=0.1)

ax = fig.add_subplot(1, 2, 2, aspect='equal', xlim=(0, 1), ylim=(0, 1))
ax.set_title("ROC曲線",)
plot_roc_curve(clf, X_test, y_test, ax=ax)

plt.show()

出力された図がこちら。

コード全体が長いのでわかりにくくて恐縮ですが、 ROC曲線自体は1行で出力できており、非常に手軽です。
また、AUCも同時に出力してくれています。

scikit-learnにスタッキングのクラスが追加されたので使ってみる

前回に引き続き、scikit-learn 0.22.0 の新機能の紹介です。

複数の機械学習モデルを組み合わせて使う方法の一つに、スタッキング(Stacking)があります。
簡単に言えば、いつくかの機械学習のモデルの予測結果を特徴量として、別のモデルが最終的な予測を行うものです。
アイデアは単純で、効果もあるらしいのですが、これをscikit-learnでやろうとすると、少し面倒だったので僕はあまり使ってきませんでした。

それが、 StackingClassifierStackingRegressor という 二つの機能がscikit-learnに実装され、
手軽にできそうな期待が出てきたので、試してみました。
とりあえず、 StackingClassifier (分類)の方をやってみます。

サンプルのコードを参考しながら、 iris を digitsに変えてやってみました。
ランダムフォレストと、線形SVMの出力を特徴量にして、ロジスティック回帰で予測しています。


from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import LinearSVC
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
from sklearn.ensemble import StackingClassifier

# データの準備
X, y = load_digits(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
        X,
        y,
        stratify=y,
        random_state=42,
    )


# 1段目として、二つのモデルを構築。
# 初期値の max_iter では収束しなかったので大きめの値を設定
estimators = [
        ('rf', RandomForestClassifier(n_estimators=10, random_state=42)),
        ('svr', make_pipeline(
                StandardScaler(),
                LinearSVC(max_iter=4000, random_state=42)
                )
            )
        ]
clf = StackingClassifier(
    estimators=estimators,
    final_estimator=LogisticRegression(max_iter=600),
)
clf.fit(X_train, y_train)
print("正解率:", clf.score(X_test, y_test))
# 正解率: 0.96

そこそこの正解率が出ましたね。
ちなみに、同じパラメータのランダムフォレストと線形SVM、の単体の正解率は次のようになります。


make_pipeline(
            StandardScaler(),
            LinearSVC(max_iter=4000, random_state=42)
        ).fit(X_train, y_train).score(X_test, y_test)

# 0.9511111111111111

RandomForestClassifier(
    n_estimators=10,
    random_state=42
).fit(X_train, y_train).score(X_test, y_test)

# 0.9333333333333333

確かにスタッキングしたモデルの方が正解率が高くなっていました。

scikit-learnのversion 0.22.0 がリリースされたので、混同行列の可視化機能を試す

先日、scikit-learnの新しいバージョンがリリースされていたことに気づきました。
Version 0.22.0 December 3 2019

色々、機能が追加されていたり、改善が施されていたりしますが、何かパッと試せるものを試してみようと眺めてみたのですが、
新機能の中に metrics.plot_confusion_matrix というのが目についたのでこれをやってみることにしました。

元々、 confusion_matrix を計算する関数はあるのですが、
出力がそっけないarray で、自分でlabelを設定したりしていたのでこれは便利そうです。

まず、元々存在する confusion_matrix で混同行列を出力してみます。


from sklearn.svm import SVC
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix

# データの読み込み
iris = load_iris()
X = iris.data
y = iris.target
class_names = iris.target_names

X_train, X_test, y_train, y_test = train_test_split(
                                        X,
                                        y,
                                        test_size=0.2,
                                        random_state=0,
                                        stratify=y,
                                    )

# モデルの作成と学習
classifier = SVC(kernel='linear', C=0.01).fit(X_train, y_train)
y_pred = classifier.predict(X_test)
print(confusion_matrix(y_test, y_pred))

"""
[[10  0  0]
 [ 0 10  0]
 [ 0  3  7]]
"""

最後の行列が出力された混同行列です。各行が正解のラベル、各列が予測したラベルに対応し、
例えば一番下の行の中央の3は、正解ラベルが2なのに、1と予測してしまったデータが3件あることを意味します。
(とても便利なのですが、行と列のどちらがどっちだったのかすぐ忘れるのが嫌でした。)

さて、次に sklearn.metrics.plot_confusion_matrix を使ってみます。
どうやら、confusion_matrixのように、正解ラベルと予測ラベルを渡すのではなく、
モデルと、データと、正解ラベルを引数に渡すようです。
こちらにサンプルコードもあるので、参考にしながらやってみます。
normalizeに4種類の設定を渡せるのでそれぞれ試しました。

データとモデルは上のコードのものをそのまま使います。


import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import plot_confusion_matrix

# 表示桁数の設定
np.set_printoptions(precision=2)

# 可視化時のタイトルと、正規化の指定
titles_options = [
        ("Confusion matrix, without normalization", None),
        ("Normalized confusion matrix: true", 'true'),
        ("Normalized confusion matrix: pred", 'pred'),
        ("Normalized confusion matrix: all", 'all'),
    ]

fig = plt.figure(figsize=(10, 10), facecolor="w")
fig.subplots_adjust(hspace=0.2, wspace=0.4)
i = 0
for title, normalize in titles_options:
    i += 1
    ax = fig.add_subplot(2, 2, i)
    disp = plot_confusion_matrix(
                        classifier,
                        X_test,
                        y_test,
                        display_labels=class_names,
                        cmap=plt.cm.Blues,
                        normalize=normalize,
                        ax=ax,
                    )

    # 画像にタイトルを表示する。
    disp.ax_.set_title(title)

    print(title)
    print(disp.confusion_matrix)
plt.show()

"""
Confusion matrix, without normalization
[[10  0  0]
 [ 0 10  0]
 [ 0  3  7]]
Normalized confusion matrix: true
[[1.  0.  0. ]
 [0.  1.  0. ]
 [0.  0.3 0.7]]
Normalized confusion matrix: pred
[[1.   0.   0.  ]
 [0.   0.77 0.  ]
 [0.   0.23 1.  ]]
Normalized confusion matrix: all
[[0.33 0.   0.  ]
 [0.   0.33 0.  ]
 [0.   0.1  0.23]]
"""

最後に表示された画像がこちら。

今回例なので4つ並べましたが、一つだけ表示する方が カラーバーの割合がいい感じにフィットします。
軸に True label、 Predicated label の表記を自動的につけてくれるのありがたいです。

pandasのデータフレームの行をランダムにシャッフルする

てっきり専用のメソッドがあると思っていたら、無さそうだったのでやり方のメモです。
pandas のデータフレームのデータをランダムにシャッフルする方法を紹介します。

これを使う場面の一例ですが、たとえばkerasで作ったモデルを学習する時にvalidation_split を使う場合、
データをシャッフルせずに後ろの方から指定した割合をvalidationデータとして使うため、
事前にデータをシャッフルしておかないとtrainとvalidationでデータの傾向が違うということが発生し得ます。
参考:validation splitはどのように実行されますか?

pandasのデータフレームをランダムに混ぜ合わせる方法は何通りも考えらるのですが、
sample()メソッドに、frac=1を渡して実行するのが一番簡単そうです。
参考:DataFrameのsampleメソッドのドキュメントを読む
そうすることによって、データフレームの全ての行がサンプリングされ、さらに、sample()の仕様により、データはシャッフルされます。

ついでにインデックスの振り直しまでやっておくと便利なので、次の例のように使うと良さそうです。


import pandas as pd
# サンプルデータ生成
df = pd.DataFrame(
    {f"col{i}": [f"value{i}{j}" for j in range(10)] for i in range(3)}
)
print(df)
"""
      col0     col1     col2
0  value00  value10  value20
1  value01  value11  value21
2  value02  value12  value22
3  value03  value13  value23
4  value04  value14  value24
5  value05  value15  value25
6  value06  value16  value26
7  value07  value17  value27
8  value08  value18  value28
9  value09  value19  value29
"""

# シャッフルしてインデックスを振り直す
df = df.sample(frac=1).reset_index(drop=True)
print(df)
"""
      col0     col1     col2
0  value08  value18  value28
1  value06  value16  value26
2  value00  value10  value20
3  value03  value13  value23
4  value05  value15  value25
5  value02  value12  value22
6  value07  value17  value27
7  value04  value14  value24
8  value09  value19  value29
9  value01  value11  value21
"""

jupyter notebook で HTMLを表示する

諸事情あって、notebookで出力するテキストに色を塗りたかったので、方法を調べました。
結論として、spanタグでbackground-colorを設定したHTMLを組み立てて、それを表示する方法をとりました。

notebookで、変数に格納されたHTMLテキストを表示するには、次の関数を使います。

class IPython.display.HTML

使い方のイメージ


from IPython.display import HTML
# 変数 html_text に表示したいhtmlが文字列で入ってるものとする。
HTML(html_text)

ただ、変数に格納されたhtmlを表示するのではなく、普通にhtmlを書くのであれば、
セルをマークダウンモードにしておけば自動的にhtmlとして解釈して表示してくれます。

この他、 %%html というマジックコマンドを使うことも可能です。
参考:cellmagic-html

kerasで使える活性化関数を可視化する

kerasではあらかじめ様々な活性化関数が用意されています。
正直普段使ってないものもあるのですが、どのようなもがあるのか一通り把握しておきたかったので可視化を試みました。
原論文か何か読んで数式を順番に実装しようかと思ったのですが、
Activationレイヤーだけから構成される超単純なモデルを作るのが手軽だと気付いたのでやってみました。

今回はこちらのページにある、 利用可能な活性化関数 を対象にしました。
(この他にもより高度な活性化関数はいくつか用意されています。)

コードは次のようになります。


from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Activation
import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(-5, 5, 101).reshape(-1, 1)

# 絶対値の範囲が 有限なもの
activations_0 = [
    "sigmoid",
    "tanh",
    "hard_sigmoid",
    "softsign",
    ]

# 絶対値の範囲に上限がないもの
activations_1 = [
    "elu",
    "selu",
    "softplus",
    "relu",
    "linear",
]

fig = plt.figure(figsize=(10, 8))
for i, activations in enumerate([activations_0, activations_1]):
    ax = fig.add_subplot(2, 1,  i+1)
    for activation_str in activations:
        model = Sequential()
        model.add(
            Activation(
                activation_str,
                input_shape=(1, ),
            )
        )
        y = model.predict(x).ravel()
        ax.plot(x, y, label=activation_str)

    ax.legend()

plt.show()

出力結果はこちら。

bashコマンドで標準出力に出力する内容をファイルにも書き出す

久々に使おうとしたら忘れてしまっていたのでメモです。

長時間かかるコマンドの実行を待つ時、標準出力にずらずらと出てくるメッセージを見ながら、
それをファイルにも残しておきたいことがあります。
だいたい、リダイレクション(>や>>)を使って、ファイルに書き出しながら、
ファイルを tail -f コマンドで監視することが多いのですが、
tee という専用のコマンドも用意されています。

次のように、パイプラインで出力をtee コマンドに渡してあげて、 teeコマンドの引数に出力したいファイルを指定しておくと、
標準出力にはそのまま出力され、ファイルにも同じ内容が残ります。


$ echo 'tee Test' | tee file.txt
tee Test
$ cat file.txt
tee Test

-aをつけると追記です。(つけないと上書き)


$ echo 'tee Test2' | tee -a file.txt
tee Test2
$ cat file.txt
tee Test
tee Test2

printでお手軽プログレスバー

前回のprintのオプションの記事の応用です。
参考:Python3のprintの引数

大きめのデータの前処理など待ち時間が長い処理をする時に、進捗を表示したくなる時があります。
専用の良いライブラリもたくさんあるので、実用上はそれで十分なのですが、
前回のprintのオプションを使うと、簡易的なプログレスバーを作成できます。
(何も考えずに printしても進捗はわかるのですが、notebookが見にくくなったりします。)

ポイントは end を使って改行を止めることと、 “\r”(キャリッジリターン)を出力して、
次の文字の出力先を行の先頭にし、前回の出力を上書きすることです。

一例としてやってみます。
本当は重い処理のループなどの中でやるのですが、今回例として time の sleepで無理やり待ち時間を入れています。


import time
for i in range(1, 101):
    print("■" * (i//10), "□"*(10-i//10), sep="", end=" : ")
    print(str(i).zfill(3), "/", 100, "\r", sep="", end="")
    time.sleep(1)

手軽に、という割にコードが汚くなりました。
■と□は余計かもしれませんね。

これを実行すると、こんな感じの表示が出て数字が増えながら、徐々に黒く塗りつぶされていきます。
■■■■□□□□□□ : 040/100

Python3のprintの引数

printは表示したい文字列以外にも色々引数を指定して出力をカスタマイズできるよという話。

Python3系では、Python2と違って、printは関数です。(2では文)
そのため、色々と引数をとることができます。(文ならカスタマイズできないわけでもないし、関数が絶対引数とるわけでもないのですが、その辺は置いといて。)

あまりにも使い慣れすぎてドキュメントを読んでなかったのですが、printの説明には次のようにあります。
print(*objects, sep=’ ‘, end=’\n’, file=sys.stdout, flush=False)

objects を sep で区切りながらテキストストリーム file に表示し、最後に end を表示します。sep 、 end 、 file 、 flush を与える場合、キーワード引数として与える必要があります。

キーワードなしの引数はすべて、 str() がするように文字列に変換され、 sep で区切られながらストリームに書き出され、最後に end が続きます。 sep と end の両方とも、文字列でなければなりません。これらを None にすると、デフォルトの値が使われます。 objects が与えられなければ、 print() は end だけを書き出します。

file 引数は、 write(string) メソッドを持つオブジェクトでなければなりません。指定されないか、 None である場合、 sys.stdout が使われます。表示される引数は全てテキスト文字列に変換されますから、 print() はバイナリモードファイルオブジェクトには使用できません。代わりに file.write(…) を使ってください。

出力がバッファ化されるかどうかは通常 file で決まりますが、flush キーワード引数が真ならストリームは強制的にフラッシュされます。

バージョン 3.3 で変更: キーワード引数 flush が追加されました。

printにカンマ区切りで複数文字列を渡すと、スペースで結合して表示してくれるのは知っていたのですが、
それがsepって引数で調整できることは初めて知りました。


# デフォルトではsep=" "で区切って出力
print("abc", "def", "ghi")
# abc def ghi

# sepを指定すると区切り文字を変えられる
print("abc", "def", "ghi", sep="==>")
# abc==>def==>ghi

# 末尾につける文字もend(デフォルトは改行) で変更可能
print("abc", "def", "ghi", end="jkl")
# abc def ghijkl

これまで、いくつかの文字列を出力したいけど、途中にスペースや改行を入れたくないときは、
“+”で文字列を連結して表示文字列を作ったりしていましたが、
これでそんな面倒なことをしなくても大丈夫になりました。

numpyの高次元配列に対するdot積の挙動について

numpyの比較的よく使う関数に、dot積があります。
スカラーとベクトルを渡せばベクトルをスカラー倍してくれて、ベクトル同士なら内積を取ってくれ、
行列を二つ渡せばそれらの行列積を戻してくれるとても便利な関数です。
(本当は行列積は、 np.matmul を使ったほうがいいらしい。)

さて、そのnp.dot ですが、行列よりもより高次元の配列についても定義されていることを最近知りました。
ドキュメント : numpy.dot

二つの多次元配列$a$と$b$に対して、$a$の一番最後の次元の長さと、$b$の最後から2番目の次元の長さが等しい時に、
np.dot(a, b)を計算することができます。
i*j行列と、j*k行列に積が定義されて、その積がi*k行列になるのと似ています。

具体的な挙動について、コード動かしてみていきましょう。
まず、サンプルとなるデータを作ります。
aの最後の次元の要素数と、bの最後から2番目の次元の要素数は 5で揃えましたが、
それ以外の次元の要素数はバラバラにして、結果と比較しやすいようにしました。


import numpy as np
a = np.random.randn(2, 3, 4, 5).round(2)
b = np.random.randn(6, 7, 5, 8).round(2)

print(a.shape, b.shape)
# (2, 3, 4, 5) (6, 7, 5, 8)

このdot積を取って、shapeをみてみます。


c = np.dot(a, b)
print(c.shape)
# (2, 3, 4, 6, 7, 8)

5は消えましたが残りの数はそのまま残りましたね。

さて、結果の$c$の各要素の値ですが、次のように計算されたものが入っています。
$$
c[i, j, k, m, n, o] = \sum_{l} a[i, j, k, l] \cdot b[m, n, l, o].
$$
別の書き方をすればこうです。
$$
c[i, j, k, m, n, o] = np.dot(a[i, j, k, :], b[m, n, :, o]).
$$

一応確認しておきましょう。


c[1, 2, 3, 4, 5, 6] ==  np.dot(a[1, 2, 3, :], b[4, 5, :, 6])
# True