Numpy配列の非ゼロ要素のインデックスを取得する

とある大き目のマトリックスを対象にゼロでない要素のインデックス(行番号と列番号)を取得する必要があったので、
スマートな方法を探しました。
全部の行と列を2重のfor文で探索すればできるのですがあまりにも無駄が多い気がしましたので。

結果的に、nonzeroという、ずばりそのままの名前の関数があったのでそれを使っています。
ドキュメント: numpy.nonzero

まず、1次元の配列に対してやって見ましょう。


import numpy as np
ary1 = np.array([0, 0, 5, 3, 0, -1, 0])

# 非ゼロ要素のインデックスを取得する
print(np.nonzero(ary1))
# (array([2, 3, 5]),)

# 別の書き方
print(ary1.nonzero())
# (array([2, 3, 5]),)

# 非ゼロ要素の値を取得する
print(ary1[ary1.nonzero()])
# [ 5  3 -1]

てっきり、array([2, 3, 5]) が戻ってくると予想していたのに、
(array([2, 3, 5]),) というタプルの形で結果が戻ってくる点に注意が必要です。
ただ、上の例の最後に挙げたように、スライスとして使いやすいので便利です。

続いて、2次元配列、要するに行列でやってみましょう。


ary2 = np.array(
        [
            [0, 0, -2, 0],
            [1, 0, 2, 0],
            [0, 0, 1, 0],
            [3, 0, 0, 0],
        ]
    )
print(ary2.nonzero())
# (array([0, 1, 1, 2, 3]), array([2, 0, 2, 2, 0]))

これも直感的には[(0, 2), (1, 0), (1, 2), …]みたいなのが戻ってくると思ったら違う形で帰ってきました。
これは次のようにすると、非ゼロ要素に対する操作を行う場合は次のように使います。(一例です。)


xx, yy = ary2.nonzero()
for x, y in zip(xx, yy):
    print(x, y)
"""
0 2
1 0
1 2
2 2
3 0
"""

また、1次元の場合と同じ様に次のようにしてスライスとして使えます。


print(ary2[ary2.nonzero()])
# [-2  1  2  1  3]

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のインストールと最も簡単な例

matplotlibのグラフを高解像度で保存する

普段はmatplotlibで作ったグラフに美しさを求めるようなことは無いのですが、
とある業務で、解像度の高い状態で出力する必要が発生したので、そのメモです。

普段は画像ファイルが必要な場合もplt.show()でjupyter notebook上に表示した物を保存していましたが、
綺麗に出力するために、画像ファイルに直接書き出しました。
使うのはplt.savefig()です。
ドキュメント: matplotlib.pyplot.savefig
そして、保存するときの引数、dpiに大きめの値を与えることで、高解像度に保存することができます。
デフォルトと、dpiを指定した場合の2通りやってみましょう。


import matplotlib.pyplot as plt
import numpy as np
# ダミーデータ生成
x = np.random.randn(50)
y = np.random.randn(50)

fig = plt.figure(facecolor="w")
ax = fig.add_subplot(111)
ax.scatter(x, y)
ax.set_xlabel("x軸")
ax.set_ylabel("y軸")
ax.set_title("タイトル")
# 解像度の指定をせずに保存
plt.savefig("default_dpi_scatter.png", format="png")

# もう一度同じグラフを作る
fig = plt.figure(facecolor="w")
ax = fig.add_subplot(111)
ax.scatter(x, y)
ax.set_xlabel("x軸")
ax.set_ylabel("y軸")
ax.set_title("タイトル")
# 解像度を指定して保存
plt.savefig("300_dpi_scatter.png", format="png", dpi=300)

結果がこちらです。
デフォルトdpi。

dpi=300を設定。

デフォルトの方は明らかにピンボケしていて、dpiに300を指定した方はくっきりしています。
文字を見れば明らかです。

ちなみに、dpiを指定しなかった場合は、
rcParams[“savefig.dpi”] の値が使われます。そしてこれに”figure”が指定されているときは、
plt.rcParams[“figure.dpi”] の値が使用されます。
(matplotlibの設定フィアルでデフォルト値を変えることはできますが、僕の環境では特に変更していません。)

一応初期値を確認しておきましょう。


print(plt.rcParams["savefig.dpi"])
# figure

print(plt.rcParams["figure.dpi"])
# 72.0

何も指定しないと dpi=72 になるようですね。

DataFrameの2列の値からdictを作る

DataFrameの2列の値のうち、一方の列の値をKey、もう一方の列のValueとする辞書を作る方法の紹介です。
自分はよくやるのですが、意外に知られてないらしいことと、なぜこれが動くのか自分も十分に理解していなかったのでこの機会に調べました。

例えば次のようなデータフレームがあったとします。

id col1 col2
1 key1 value1
2 key2 value2
3 key3 value3
4 key4 value4
5 key5 value5

そして、このcol1の値をkey, col2の値をvalueとして、
{‘key1’: ‘value1’, ‘key2’: ‘value2’, ‘key3’: ‘value3’, ‘key4’: ‘value4’, ‘key5’: ‘value5’}
のようなdictを作りたいとします。

このような場合、僕は次のコードのようにデータフレームの該当の2列を抽出して、そのvaluesプロパティをdict関数に渡します。


import pandas as pd
# サンプルとなるデータフレームを作る
data = [[i, "key"+str(i), "value"+str(i)] for i in range(1, 6)]
df = pd.DataFrame(data, columns=["id", "col1", "col2"])

result_dict = dict(df[["col1", "col2"]].values)
print(result_dict)
# {'key1': 'value1', 'key2': 'value2', 'key3': 'value3', 'key4': 'value4', 'key5': 'value5'}

注意ですが .values を忘れると次のように 列名:Seriesの辞書になってしまいます。


print(dict(df[["col1", "col2"]]))
"""
{'col1': 0    key1
1    key2
2    key3
3    key4
4    key5
Name: col1, dtype: object, 'col2': 0    value1
1    value2
2    value3
3    value4
4    value5
Name: col2, dtype: object}
"""

これも参考ですがよく見かけるのは次ような書き方。


result_dict = dict()
for i in range(len(df)):
    result_dict[df.iloc[i]["col1"]] = df.iloc[i]["col2"]

print(result_dict)
# {'key1': 'value1', 'key2': 'value2', 'key3': 'value3', 'key4': 'value4', 'key5': 'value5'}

さて、話を戻してdict(df[["col1", "col2"]].values)がなぜうまく動くのかです。

改めてドキュメントを読んでみるとdictはiterableを引数に取ることができます。
class dict(iterable, **kwarg)
そして、
iterable のそれぞれの要素自身は、ちょうど 2 個のオブジェクトを持つイテラブルでなければなりません。
とのことです。

実際見てみると、df[[“col1”, “col2”]].valuesはその条件を満たすデータになっています。


print(df[["col1", "col2"]].values)
"""
[['key1' 'value1']
 ['key2' 'value2']
 ['key3' 'value3']
 ['key4' 'value4']
 ['key5' 'value5']]
"""

DataFrame側にdictに渡したら空気を読んでいい感じに変換される機能が実装されている、と勘違いしていたこともあるのですが、
dictの通常の挙動にマッチした動きだったようです。
for分で回すのに比べてかなりスマートに書けるので、最初に紹介した書き方は結構おすすめです。

2019年第3四半期によく読まれた記事

前回:2019年第2四半期によく読まれた記事

3ヶ月ごとの恒例?(まだ3回目ですが)の、この四半期に読まれた記事のランキングです.

7月から9月のベスト5はこちら。

  1. macにgraphvizをインストールする
  2. DataFrameを特定の列の値によって分割する
  3. pythonでARモデルの推定
  4. graphvizで決定木を可視化
  5. pandasでgroupbyした時に複数の集計関数を同時に適用する

前回から引き続きランクインしているものが目立ち、最近書いた記事が無いです。
そもそもここ最近書いていた数式が多めの記事はほぼ読まれていないように見えます。
もともと自分のメモとしてテキスト等を写したような記事も多く、あまり独自の内容がないからでしょうかね。
もっと良い情報を提供できるようにまだまだ精進していく必要があります。

7月から記事の更新を毎日から平日のみに絞り、ネタ切れ感はあるもののだいぶ無理のない運用になってきました。
$TeX$もよりスムーズにかけるようになってきたのも更新が楽になってきたポイントです。

一週間の訪問者も最近は600人を超えるようになってきました。(2Qの1.5倍)
わざわざ訪問してくださった方々の期待に応えられる記事になってるかと言うと、全く自信のないところなのですが、非常にありがたいです。

さっと読み返してみると、時間をかけて調査した内容をまとめた記事はまだ十分な量になっておらず、
ちょっとしたメモ書きのようなのが多くなってしまっていて、まだまだ理想とは遠いと感じています。
この辺りは最近の残業の増加とも関係していると思うので、仕事や生活を総合的に見直して改善していきたいです。

ユークリッド空間内の複数の点の中から、最も近い点を探す短いコード

実務上次の問題を解く必要が発生し、短くて効率のいい書き方を探したのでそのメモです。

n次元ユークリッド空間内に、m個の点が存在し、その座標がm*n行列で与えられる。
新たに1つの点の座標がn次元ベクトルで与えられた時、m個の点のうち最も近い点のインデックスを求める。

結論から言うと次のコードで求まります。
最後の行が今回作成したコードです。(それ以外はサンプルの点を作成しています。)
ここでは、 n=10, m=5です。


import numpy as np
# m個の点の座標を生成
X = (np.random.randn(5, 10) * 20).round(2)
print(X)
"""
[[-25.3   -3.49 -21.3   -1.98  10.99   2.77  -8.37  -9.01  20.05   7.91]
 [ 16.33  -9.    16.55 -10.27   6.56   7.81  16.03 -14.13  10.58 -32.39]
 [ 12.01 -11.79  34.79  -2.94 -12.24   0.71 -35.01 -17.92  20.6   33.73]
 [-11.47  30.95   4.92 -19.94  34.81   2.54 -11.73  21.91  24.16   4.87]
 [ 38.32  22.13  11.5   18.82  -8.29   4.02 -17.15  -5.26 -35.62 -19.43]]
"""
# 新しい点の座標
y = (np.random.randn(10) * 20).round(2)
print(y)
"""
[-23.09 -57.02  28.07  39.45 -22.52  -5.91   9.58  30.66 -11.82  21.5 ]
"""
# 最も近い点のインデックス
print(((X-y)**2).sum(axis=1).argmin())
"""
2
"""

今回は最も近い点を探せればよく、具体的な距離は必要としないので、
ユークリッド距離の定義にある平方根の計算は省略しました。

各点との距離のリストが必要な場合は次のコードで求めることができます。
((X-y)**2).sum(axis=1)**0.5
もしくは線形代数モジュールを使い次のように計算することもできます。
np.linalg.norm(X-y, axis=1)

掲題の問いについても、次のようにかけるのですが、
不要な平方根の計算が入るせいか少しだけ遅かったので、最初の書き方を採用しています。
np.linalg.norm(X-y, axis=1).argmin()

データフレームの背景に棒グラフを表示する

データフレームの書式設定シリーズの最後の記事です。
今回は背景にセルの値の大きさを表す棒グラフを表示します。
これ、地味に便利です。
ドキュメントはこちら

使い方は簡単で、df.style.bar()を呼び出すだけです。
オプションも色々指定子できますので、いくつか設定してやってみます。


# 適当なデータフレームを作成
df = pd.DataFrame(
        np.random.randint(-100, 100, size=(10, 4)),
        columns=["col0", "col1", "col2", "col3"]
    )

df.style.bar(
    align="mid",
    width=90,
    axis=None,
    color=['#d65f5f', '#5fba7d']
)

出力結果がこちら。

引数をいくつか説明しておきます。
まず align。 以下の3種類の値のどれかを取ります。

left
値が最小のセルの値が左端。デフォルト。
zero
ゼロがセルの中心
mid
最大値と最小値の平均か、もし正の数と負の数を両方含む場合は0が中心。

次に width は0〜100の値を取り、棒グラフの最大長がセルの何%を締めるかを表します。
colerは棒グラフの色です。文字列を一つ渡せば全てその色、配列にして2つ渡せばそれぞれ負の値の時と正の値の時の色です。
axis は最大最小値の基準が列方向(0)、行方向(1)、テーブル全体(None)のどれかを表します。
例では使っていませんが、vmin, vmax で最小値、最大値を指定することもできます。
外れ値があるような時は便利です。

データフレームの書式設定に組込書式を使う

データフレームを表示するときにCSSで書式設定する方法(apply,applymapを使う)を紹介してきましたが、
最初の方で少し書いた通り、これらの方法を使わなくてもよく使う書式はあらかじめ関数が用意されています。

ドキュメント: Builtin styles

非常によく使う、最大値最小値やNullの強調には、それぞれ次の3つの関数が使えます。

  • df.style.highlight_null(null_color=’red’)
  • df.style.highlight_max(subset=None, color=’yellow’, axis=0)
  • df.style.highlight_min(subset=None, color=’yellow’, axis=0)

subsetは対象の列を指定でき、colorは背景色、axisは最大値や最小値を評価する軸方向を指定します。

また、値の大小を色の濃淡(もしくは色相など)で、表現するには、df.style.background_gradientが使えます。
引数は次の通り。だいたいイメージ通り動きます。
df.style.background_gradient(cmap=’PuBu’, low=0, high=0, axis=0, subset=None, text_color_threshold=0.408)

cmapに渡すのはMatplotlibのcolormapです。
具体的な名前と色はこちらを参照して選びます。
Choosing Colormaps in Matplotlib

そして最後に、値によらず、データフレーム全体の書式を一括変更する時は、
set_propertiesを使います。
ドキュメントに例が載っていますが、これだけ少し引数の渡し方が独特なので注意です。
サンプル:


df.style.set_properties(**{'background-color': 'black',
                           'color': 'lawngreen',
                           'border-color': 'white'})