hyperoptで探索したパラメーターの履歴を確認する

前回の記事に続いて、hyperoptの記事です。
前回のサンプルコードでは、100回の探索を経て見つけた最良の値を取得しましたが、
そのほかの結果もみたいということはあると思います。
機械学習のモデルであれば、どのパラメーターが精度に寄与していたとか、
どれはあまり重要でないとか確認したいですからね。

そのような時、hyperopt では Trials というオブジェクトを使います。
ドキュメントのこちらを参考にやってみましょう。
1.3 The Trials Object


from hyperopt import fmin, tpe, hp, Trials
trials = Trials()
best = fmin(
            fn=lambda x: x ** 2,
            space=hp.uniform('x', -10, 10),
            algo=tpe.suggest,
            max_evals=100,
            trials=trials,
        )
print(best)

# 出力
100%|██████████| 100/100 [00:00<00:00, 554.94it/s, best loss: 8.318492974446729e-09]
{'x': 9.120577270352315e-05}

Trials() ってのが途中で出てきた以外は前回の記事と同じコードですね。
この時、trials変数に100回分の履歴が残っています。
試しに一つ見てみましょう。


# trials.trialsに配列型で格納されている結果の一つ目
trials.trials[0]

# 内容
{'state': 2,
 'tid': 0,
 'spec': None,
 'result': {'loss': 96.69903496469523, 'status': 'ok'},
 'misc': {'tid': 0,
  'cmd': ('domain_attachment', 'FMinIter_Domain'),
  'workdir': None,
  'idxs': {'x': [0]},
  'vals': {'x': [-9.833566746847007]}},
 'exp_key': None,
 'owner': None,
 'version': 0,
 'book_time': datetime.datetime(2019, 3, 10, 15, 4, 45, 779000),
 'refresh_time': datetime.datetime(2019, 3, 10, 15, 4, 45, 779000)}

$x=-9.833566746847007$ を試したら 損失関数$x^2$の値は$96.69903496469523$ だったということがわかります。

このほか、次のような変数にそれぞれの値の辞書も格納されています。
trials.results
trials.vals

せっかくなので、本当に正解の$x=0$付近を探索していたのか可視化してみましょう。


import matplotlib.pyplot as plt
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(1, 1, 1)
ax.scatter(trials.vals["x"], trials.losses())
plt.show()

結果がこちら。

きちんと正解付近に点が密集していますね。

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

昨年参加したpycon2018
実践・競馬データサイエンス というセッションで知った、hyperopt というライブラリのメモです。
とりあえずこの記事では最もシンプルな例で動かすところまで行きます。
ちなみに、pyconの時の資料はこちら。
実践・競馬データサイエンス

なんでも、ハイパーパラメーターをグリッドサーチなどより効率的に探索してくれるとのこと。

公式サイトの通り、 pipでインストールできます。


pip install hyperopt

早速動かしてみましょう。
Basic tutorial

$x^2$ が最小になる$x$(正解は$x=0$です)を探索するコードです。


from hyperopt import fmin, tpe, hp
best = fmin(
            fn=lambda x: x ** 2,
            space=hp.uniform('x', -10, 10),
            algo=tpe.suggest,
            max_evals=100
        )
print(best)

# 以下出力
100%|██████████| 100/100 [00:00<00:00, 570.89it/s, best loss: 0.0009715421527694116]
{'x': 0.031169570942979175}

これで、 $-10<=x<=10$ の範囲を100回探索し、$x=0.031$あたりで最小と見つけてきたようです。 単純すぎていまいちありがたみが感じられませんね。 ただグリッドサーチと比較すると、仮に-10から10を単純に100分割したら区間の幅は0.2になるわけで、 それよりはずっと小さい誤差で最小値付近の値を見つけています。 (とはいえ、グリッドサーチなら $x=0$をピタリと見つけるとは思いますが。) 機械学習のハイパーパラメーターの探索等で役に立つと聞いて調べたものであり、 僕もそのように使うことが多いので、今後の記事でもう少し細かく紹介しようと思います。 一点気になるのは公式ドキュメントのこの記述。

Hyperopt has been designed to accommodate Bayesian optimization algorithms based on Gaussian processes and regression trees, but these are not currently implemented.

[翻訳]
Hyperoptは、ガウス過程と回帰木に基づくベイズ最適化アルゴリズムに対応するように設計されていますが、これらは現在実装されていません。

Qiitaなどで、hpyeroptをベイズ最適化のライブラリだって紹介している人を見かけたこともあるのですが、そうではなさそうです。

DataFrameを特定の列の値によって分割する

python(pandas)で巨大なデータフレームを扱っている時、
ある列の値によって、分割したいことが良くあります。

pandasのgroupbyのユーザーガイドを見ていて、良い方法を見つけたのでそれを紹介します。
Iterating through groups
のところに書かれていますが、groupbyした後そのままsumなどの関数を適用するだけでなく、イテレーターとして使うことで、
各グループに対して順番に処理を行えます。

実際にやってみましょう。
とりあえずサンプルのデータフレームを作ります。


import pandas as pd
import numpy as np
df = pd.DataFrame(
            np.random.randn(10000, 10),
            columns=['x'+str(i) for i in range(10)],
        )
df['y'] = np.random.randint(20, size=10000)

これで、長さが1万行、列が11個あるデータフレーム(df)ができました。
これをy列の値(0〜19)によって、20個のデータフレームに分割します。


df_dict = {}
for name, group in df.groupby('y'):
    df_dict[name] = group

これで、20個のデータフレームに分割できました。

ちなみに、この方法を知る前は次のように書いていました。


df_dict = {}
for name in set(df.y):
    df_dict[name] = df[df.y == name]

コードの量はほとんど同じなのですが、この方法は無駄な比較を何度も行うことになります。

実際、実行時刻を測ってみるとgroupbyを使う方法は、
CPU times: user 7.47 ms, sys: 3.28 ms, total: 10.8 ms
Wall time: 7.71 ms

なのに対して、使わない方法は
CPU times: user 16.4 ms, sys: 1.81 ms, total: 18.2 ms
Wall time: 16.5 ms
となり、倍以上の時間を要しています。

matplotlibで折れ線グラフ(plot)を書く時に線の書式を指定する

matplotlibで折れ線グラフを書く時に線の書式を指定する方法のメモです。
大抵のケースでは色で見分ければ十分なのですが、たまに点線や破線を使いたいことがあります。
(色が使えない場合もマーカーで見分けてもいいのですけどね。)

ドキュメントはこちらです。
set_linestyle(ls)
matplotlib.pyplot.plot の linestyle のリンクをクリックすると上のページに飛びます。

方法は簡単で、 plot する時に linestyle変数に ‘dashed’ などの文字列や ‘:’ などの記号を指定するだけです。
Noneで、線を消すこともできます。(その場合はマーカーを使いましょう)

試しに4種類の線を引いてみました。
(線の種類の区別を目立たさせるため、色は揃えました。)


import matplotlib.pyplot as plt
import numpy as np

# データ生成
x = np.arange(10)
y0 = np.random.normal(0, 0.5, 10)
y1 = np.random.normal(1, 0.5, 10)
y2 = np.random.normal(2, 0.5, 10)
y3 = np.random.normal(3, 0.5, 10)

plt.rcParams["font.size"] = 14
fig = plt.figure(figsize=(10, 6))
ax = fig.add_subplot(1, 1, 1, title="linestyleの例")
ax.plot(x, y0, linestyle="-", c="b", label="linestyle : solid, '-'")
ax.plot(x, y1, linestyle="--", c="b", label="linestyle : dashed, '--'")
ax.plot(x, y2, linestyle="-.", c="b", label="linestyle : dashdot, '-.''")
ax.plot(x, y3, linestyle=":", c="b", label="linestyle : dotted, ':''")
ax.legend(bbox_to_anchor=(1.05, 1))
plt.show()

出力はこちら。

pythonのdictから値を取得する時にデフォルト値を設定する

今は当たり前のように使っているテクニックですが、初めて知った時は感動したので紹介。

ドキュメントはここ

pythonの辞書オブジェクトから値を取得する時、[ ]を使う方法と、
get()を使う方法があります。


sample_dict = {
    'apple': 'リンゴ',
    'orange': 'オレンジ'
}

print(sample_dict['apple'])  # 'リンゴ'
print(sample_dict.get('orange'))  # 'オレンジ'

タイプ数が少ないので[ ]の方を好むかたが多いと思うのですが、
getの方には、引数がkeyの中に存在しなかった時の初期値を指定できるメリットがあります。

例えば、
sample_dict["grape"] は keyErrorが発生してしまいます。
しかし、
sample_dict.get("grape")はNoneを返しますし、
sample_dict.get("grape", "未定義")とすると、
2個目の引数である、”未定義”を返してくれます。

エラー処理もいらないし、事前に辞書に取得しようとしているkeyが含まれているか確認する必要もなくなり、
非常に便利です。

requestsにタイムアウトを設定する

requestsを使ってurlのリストをチェックしていた時にredirectに加えて困ったのが、応答に時間がかかるサーバーへアクセスした時です。
数秒待って結果が戻って来ればまだいいのですが、そのままスクリプトが進まなくなってしまうことがありました。
これを防ぐには、timeoutを設定すると良いようです。
(デフォルトでは設定されていない)

ドキュメントはこちら。
クイックスタート – timeouts
日本語が少しおかしい気がします。
timeout パラメーターに秒数を指定すると、指定した秒数の間、Requestsのレスポンスの待機を止めることができます。
おそらく意図は「timeout パラメーターに秒数を指定すると、指定した秒数でRequestsのレスポンスの待機を止めることができます。」ではないかと。

早速ですがやってみましょう。
timeout になると例外が発生するので、キャッチできるようにします。
(本来はrequestsライブラリで定義されている専用の例外を使うべきなのですが、とりあえずException使います)


import requests

url = "https://httpstat.us/200?sleep=10000"  # 時間のかかるURL

try:
    response = requests.get(url, timeout=3)
    print(response.status_code)  # 実行されない
    print(response.url)  # 実行されない

except Exception as e:
    print(e.args)

# 出力
(ReadTimeoutError("HTTPSConnectionPool(host='httpstat.us', port=443): Read timed out. (read timeout=3)",),)

想定通りに動きました。

requestsを使って、GETでアクセスすると自動的にリダイレクトされる

日常的に使っていて、このブログでも紹介したことのあるrequestsの話です。
参考:requestsを使って、Webサイトのソースコードを取得する

これまで意識せずに使っていたのですが、requestsでgetすると、リダイレクトがあるページの場合、
自動的にリダイレクトされます。
ドキュメントにもはっきり書いてありますね。
リダイレクトと履歴

例えば、このブログはhttpでアクセスすると、httpsのurlにリダイレクトする設定になっています。
そのため、以下のコードは、”https://analytics-note.xyz/” ではなく、
そこからリダイレクトされて、”https://analytics-note.xyz/”にアクセスします。


import requests
url = "https://analytics-note.xyz/"
response = requests.get(url)
print(response.status_code)
print(response.url)

# 以下出力
200
https://analytics-note.xyz/

status_codeがリダイレクトの302ではなく、200になることや、
urlがhttpsの方に書き換えられていることがわかります。

ちなみにリダイレクトされたページへのアクセス結果は、Responseオブジェクトの、historyというプロパティに、
Responseオブジェクトの配列として格納されます。
今回リダイレクトは1回でしたが、複数回に及ぶ可能性もあるので配列で格納されているようです。


print(response.history)
# 出力
[<Response [302]>]

この自動的にリダイレクトしてくれる仕組みはデータ収集等では非常に便利なのですが、
作業の目的によっては逆に不便です。

リダイレクトして欲しくない時は、allow_redirectsという引数にFalseを渡すことでリダイレクトを禁止できます。


response = requests.get(url, allow_redirects=False)
print(response.status_code)
print(response.url)

# 以下出力
302
https://analytics-note.xyz/

pythonで編集距離(レーベンシュタイン距離)を求める

ごく稀にですが、文字列同士の編集距離を求める必要が発生するのでその時のメモです。

編集距離(レーベンシュタイン距離)とは、二つの文字列がどの程度異なっているかを表す距離の一種です。
Wikipediaにも解説があります。

一方の文字列に対して、1文字の挿入、削除、置換を最低何回施せばもう一方の文字列に等しくなるかで定まります。

pythonでこれを求めるときは、python-Levenshtein というライブラリが使えます。

インストール


pip install python-Levenshtein

使い方


>>> import Levenshtein
>>> text1 = 'Levenshtein'
>>> text2 = 'Lenvinsten'
>>> Levenshtein.distance(text1, text2)
4

pickleを使ってpythonのオブジェクトをファイルに保存する

(注)この記事はscikit-learnのモデルをファイルに保存することを念頭に書いていますが、
pickle自体はscikit-learnのモデル以外のものも直列化してファイルに書き出すことができるモジュールです。

以前の記事で、kerasで作成したmodelを保存したり読み込んだりする方法を書きました。
今回はscikit-learnで作ったモデルを保存してみます。
kerasには専用の関数が用意されていたのですが、scikit-learnにはありません。
そのため、他の方法が必要です。
そこでpython標準ライブラリの pickleが使えます。
ドキュメント

利用方法は、ドキュメントのpickle.dumppickle.loadの説明と、一番下の使用例が参考になります。

clfという変数に、学習済みのモデルが格納されているという想定で、保存と読み込みのコード例を紹介します。
また、保存するファイル名は何でも良いのですが、サンプルコードではclf.pickleとします。

まずは保存。


import pickle
with open("clf.pickle", "wb") as f:
    pickle.dump(clf, f)

次に読み込み。


import pickle
with open("clf.pickle", "rb") as f:
    clf = pickle.load(f)

これで、一度学習したモデルを読み込んで、予測に活用することができます。
scikit-learnで学習したモデルを本番運用するならばほぼ必須の技術です。
(pickle以外の方法を使うという手もありますが、何らかの形での保存と読み込みの手段が必要です)

pandasでgroupbyした時に複数の集計関数を同時に適用する

前の記事の続きです。
pandasでデータフレームをgroupbyした時に使える集計関数
ドキュメントのこの記事で参照した部分のすぐ下に、
Applying multiple functions at once
という段落があります。
実はこれ初めて知りました。
今までグルプごとに個数と、平均と、標準偏差を計算したい、みたいな時は、
groupbyして集計を個別に実施して、その結果をmergeするという非常に面倒なことをずっとやっていました。

それが、aggというのを使うと一発でできるようです。


import pandas as pd
from sklearn.datasets import load_iris

# データフレームの準備
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df["target"] = iris.target
df["target_name"] = df.target.apply(lambda x:iris.target_names[x])
del df["target"]

df.groupby("target_name").agg(["count", "mean", "std"])

出力されるのが次です。(ブログのレイアウトの都合上画像で貼り付けます。)

これは便利です。
また、DataFrameのカラム名が2段になっています。
これをみて、indexだけではなく実はcolumnsでも、MultiIndexが使えることを知りました。