静かなる名辞

pythonとプログラミングのこと

共有渡しと参照の値渡しと

はじめに

 関数やメソッドに引数を渡す方法は、一般的には

  • 値渡し
  • 参照渡し

 の2通りがあると認知されている。

 ところで、『参照の値渡し』という言葉も(ほぼ日本語Web圏限定で)存在する。これは「いわゆる『参照渡し』は参照自体を書き換えるんじゃなくて、参照する対象を変えるだけだから、そう呼んだ方が適当だよ!」という思想に基づくもの、だと思う。このページを見るとわかりやすい。

キミの言語は参照渡しできる?

 つまり、こういうことができたら『参照渡し』で

a = "hoge"
b = "fuga"
swap(a, b)
print(a, b)  # => fuga, hoge

 できなかったら「キミの言語は『参照渡し』できないよ、キミが『参照渡し』だと思っているのは『参照の値渡し』だよ!」ということか。言いたいことはわかる。

 上のリンクにはC言語で同様のことをやる方法としてこういう例が載っているが、

#include <stdio.h>

void swap(char *a, char *b){
  char tmp;
  tmp = *a;
  *a = *b;
  *b = tmp;
}

int main(void){
  char x = 'A', y = 'B';
  swap(&x, &y);
  printf("%c\n", x);
  printf("%c\n", y);
  return 0;
}

 参照する相手を書き換えて実現するのはなんか違う感じがする(それこそ『参照の値渡し』じゃん)。やるならこっちではないか。

#include <stdio.h>

void swap(char **a, char **b){
  char *tmp;
  tmp = *a;
  *a = *b;
  *b = tmp;
}

int main(void){
  char x = 'A', y = 'B';
  char *xptr, *yptr;
  xptr = &x;
  yptr = &y;
  swap(&xptr, &yptr);
  printf("%c\n", *xptr);
  printf("%c\n", *yptr);
  return 0;
}

 このコードを見ていると「あれ、じゃあ『参照渡し』の本当の名前は『参照の参照の値渡し』なの?」という疑問が生まれてくるが、本題から逸れるので追求はしない*1

共有渡しについて考える

 さて、『参照の値渡し』とよく似た概念として、『共有渡し』がある。

 ちなみに、日本語wikipediaの「引数」ページには『共有渡し』は存在しない代わり参照の値渡しがあり、英語版wikipediaの「Evaluation strategy」ページには『Call by sharing』*2はあって参照の値渡しはない。なんだかなぁ。

引数 - Wikipedia
Evaluation strategy - Wikipedia

 『共有渡し』が何者かというと、これは英語版wikipediaのページを読むのがわかりやすいのだが、一行引用してくると

also referred to as call by object or call by object-sharing

 ということであり、つまりは関数(メソッド)の呼び出し元と呼び出し先で同じオブジェクトを「共有」する方法である。

 僕のようなpython使いにとっては「それ普通じゃね?」なのだが、確かに値渡しとも『参照渡し』とも(何を示す言葉かは不問として)異なる概念と言われればそんな気はする。この言葉は、1974年、CLUという初期のオブジェクト指向言語とともに生み出された言葉だそうな。

 これに関連して、こんな議論もある。comp.lang.python *3の昔の議論らしい。pythonの呼び出しモデルは『Call by sharing』だと結論が着いている感じ。

Call By Object

 なんか、同じものを呼ぶ名前がいっぱいある。

The most accurate description is CLU’s “call by object” or “call by sharing“. Or, if you prefer, “call by object reference“.

 こんなに呼び方が多いのはちょっと酷いんだが、“call by object reference“あたりだと言いたいことはよく伝わってくる。

どっちが良いのか

 べつに『参照の値渡し』≒『共有渡し』とみなしても良いのだが、言葉のニュアンスは違うし、他にも考えるべきことがあって「どっちを使うか、あるいはどっちでも良いのか」という問題には答えを出しづらいと思うのだ・・・。

 C言語に対して『共有渡し』を使うのには躊躇する。まあ、メモリ領域を「共有」するのは同じという考え方もあるだろうけど。

  • あくまでも値渡し+参照渡し的な世界観で説明しようとする『参照の値渡し』と、オブジェクトが呼び出し元・先で「共有」されるという現象を重視する『共有渡し』

 同じ現象でも見方が違うのだと思う。
 この違いは意外と効いてきて、前者の立場を取るとJavaは「基本的に値渡し。プリミティブ型はそのまま値で渡るが、それ以外はアドレスの値が渡る」と値渡し的な世界観で説明できるが、後者の立場を取ると「プリミティブ型の値渡し、配列の『参照の値渡し』、オブジェクトの『共有渡し』の折衷」という苦しい説明にせざるを得ないと思う(配列に対して『共有渡し』の言葉を使うことを許すなら、真ん中は削れるけど)。Java使いの人たちが「Javaは値渡し!」と主張したがるのは、つまるところそういうことだろう*4
 逆にpythonrubyのような「すべてがオブジェクト」な言語では、プリミティブ型やら何やらのことは考える必要はなく、また言語仕様の表面で参照の値(要するにアドレス)が見えてくる訳でもないので、『共有渡し』の方がすっきりすると思う。

  • 認知度とか言葉としての良し悪し

 なんか、どっちもどっちという気がする。
 単純な好みの問題だと思うけど強いて選ぶとすれば、国際的に(一応は)通用するであろうこと、言葉の良し悪しについて議論の余地が少ない*5ことから、共有渡しの方が筋は良さそう

 まあ、個人的には『共有渡し』の方がスッキリするし、好きです。でも、『参照の値渡し』が絶対に駄目かというと別にそんなことはないと思うので、TPOをわきまえて使い分けるということで、どうですか。

まとめ

 ややこしくない(動作としてはよくわかる)けど、ややこしい(名前がうまく決められない)話だよね。

*1:けど、こういう動作を無条件に参照渡しと呼んでしまうのは、必ずしも良い考え方ではないと思う。だいたいこれの実装方法なんて、いくらでもあるんだしさ

*2:念のため書いておくと、「渡し」に対応するのは「Pass by」、「Call by」は「呼び出し」に対応するのだが、どちらにせよ意味は大して変わらないのでどちらで訳しても問題はない。この記事では日本語圏で一般的な「渡し」で統一している

*3:筆者はこれが何なのかはよくわからないが

*4:個人的には「Cみたいに明示的にアドレス渡す訳でもないのに値渡しって呼ぶのは逆説的でわかりづらいよ」と思うのだが、そのコストに勝るメリットがあるとする考えなのだろう

*5:というか議論になっていない程度に流行っていないだけかも・・・

【python】immutableなオブジェクトは1つしか存在しないという迷信

 たまに誤解している人がいるので、書いておく。

 pythonのオブジェクトにはimmutableという概念がある。これはオブジェクトが変更不可能であるということを示す。intやstr, tupleなどが代表的なimmutableなオブジェクトである。

 オブジェクトがimmutableであるかどうかは、オブジェクトがいわゆるシングルトンであるかどうかとはまったく関係ない

 確かに、そんな風に振る舞っているように見える状況もある。

>>> a = 1
>>> b = 1
>>> a is b
True
>>> a = "a"
>>> b = "a"
>>> a is b
True

 しかし、tupleを作ると、これが見せかけでしかないことがすぐわかる。

>>> a = (1,2,3)
>>> b = (1,2,3)
>>> a is b
False

 もう少しいろいろ見てみる。

>>> all([a is b for a,b in zip(range(100), range(100))])
True
>>> all([a is b for a,b in zip(range(1000), range(1000))])
False

 不可解だって? まあ、不可解なんだけど、これはpythonの実装の問題である。

 pythonGCを持つオブジェクト指向言語である。つまり、どんなオブジェクトであろうと生成にはインスタンス生成分のコストがかかるし、参照されなくなったオブジェクトはGCが連れて行く。それはそれで良いのだが、「小さめの数字」とか「短めの文字列」みたいなしょっちゅう使われるオブジェクトを必要になるたびに生成し、不要になるたびにGCを呼ぶのは無駄である。よって、pythonは内部にキャッシュを持ち、オブジェクトのインスタンスを残しておく。いわゆるインターン*1だと思えば良い。

 ここのページでその辺のことには触れられている。
Cool Python Tips: is演算子はimmutable変数に使わないこと

 結論を言うと、immutableだからといってシングルトンだと思いこんではいけないし、ましてやis比較などしてはいけない。

*1:python インターンで検索すると「インターンシップ」ばっかり出てきて、有益な情報が出てこない・・・

【python】GridSearchCV『の』パラメータ・チューニング

はじめに

 機械学習でパラメータ・チューニングをしたい場合、グリッドサーチを行うのが定石とされています。sklearnではグリッドサーチはGridSearchCVで行うことができます。

sklearn.model_selection.GridSearchCV — scikit-learn 0.19.1 documentation

 それで何の問題もないかというと、さにあらず。グリッドサーチは計算コストの高い処理ですから*1、素直に書くとデータとアルゴリズム次第ではとんでもない処理時間がかかります。

 もちろん「寝ている間、出かけている間に回すから良い」と割り切るという方法もありますが、可能なら速くしたいですよね。

 そうすると、パラメータ・チューニングのために使うGridSearchCV『の』パラメータを弄り回すという本末転倒気味な目に遭います。そういうとき、どうしたら良いのかを、この記事で書きます。

 先に結論を言ってしまうと、本質的に計算コストの高い処理なので、劇的に速くすることは不可能です。それでも、ちょっとの工夫で2倍程度なら速くすることができます。その2倍で救われる人も結構いると思うので*2、単純なことですがまとめておきます。

 目次

下準備とベースライン

 とりあえず、何も考えずに「GridSearchCVをデフォルトパラメタで使ってみた場合」の時間を測ります。

 そのためには適当なタスクを回してやる必要がありますが、今回はPCA+SVMでdigitsの分類でもやってみることにします。

 コードはこんな感じです。

import timeit
import pandas as pd
from sklearn.datasets import load_digits
from sklearn.decomposition import PCA
from sklearn.svm import SVC
from sklearn.pipeline import Pipeline
from sklearn.model_selection import GridSearchCV

digits = load_digits()
svm = SVC()
pca = PCA(svd_solver="randomized")
pl = Pipeline([("pca", pca), ("svm", svm)])

params = {"pca__n_components":[10, 15, 30, 45],
          "svm__C":[1, 5, 10, 20], 
          "svm__gamma":[0.0001, 0.0005, 0.001, 0.01]}

def print_df(df):
    print(df[["param_pca__n_components",
              "param_svm__C", "param_svm__gamma", 
              "mean_score_time", 
              "mean_test_score"]])

def main1():
    clf = GridSearchCV(pl, params, n_jobs=-1)
    clf.fit(digits.data, digits.target)
    df = pd.DataFrame(clf.cv_results_)
    print_df(df)
    
if __name__ == "__main__":
    print(timeit.timeit(main1, number=1))

 色々なテクニックを使っているコードなので多少解説すると、とりあえずPipelineを使っています。また、GridSearchCV.cv_results_はそのままpandas.DataFrameとして扱えることも利用しています。

hayataka2049.hatenablog.jp

 digits, svm, pca, pl, paramsの変数はmain関数の外でグローバル変数として作っていますが、これはあとでmain2とかmain3を作って使い回すための処置です。

 あと、速くするために必要と思われる常識的なこと(PCAのsvd_solver="randomized"とか、GridSearchCVのn_jobs=-1とか)はすでに実施しています。

 そんなことより本当にやりたいことは、この処理にどれだけ時間がかかるかを知ることです。そのために、timeitを使って時間を計測しています。

27.5. timeit — 小さなコード断片の実行時間計測 — Python 3.6.5 ドキュメント

 さて、私の環境(しょぼいノートパソコン)ではこのプログラムの実行には42.2秒かかりました。

 これをベースラインにします。ここからどれだけ高速化できるかが今回のテーマです。

cvを指定する(効果:大)

 さて、GridSearchCVにはcvというパラメータがあります。default=3であり、この設定だと3分割交差検証になります。交差検証について理解していれば、特に不自然なところはないと思います。

 これを2にしてみます。交差検証できる最低の数字です。こうすると、

  • 交差検証のループ回数が3回→2回になり、それだけで1.5倍速くなる
  • チューニング対象のモデルの計算量が学習データサイズnに対してO(n)以上なら、それ(nが小さくなること)によるご利益もある。なお予測データサイズmに対する予測時間は普通O(m)なので、影響はない

 この相乗効果で、高速化が期待できます。

 この方法のデメリットは学習データを減らしたことで性能が低めになることですが、チューニングのときはパラメータの良し悪し(スコアの大小関係)がわかれば良いので、あまり問題になりません。とにかくやってみます。

def main2():
    clf = GridSearchCV(pl, params, cv=2, n_jobs=-1)
    clf.fit(digits.data, digits.target)
    df = pd.DataFrame(clf.cv_results_)
    print_df(df)

if __name__ == "__main__":
    # print(timeit.timeit(main1, number=1))
    print(timeit.timeit(main2, number=1))

 上のコードと重複する部分は削ってあります。見比べると、ほとんど変わっていないことが、おわかりいただけるかと思います。

 この処置で、処理時間は28.0秒に改善しました。ちょっといじっただけで、2/3くらいに改善してしまった訳です。そして「mean_test_score」はやはり全体的に低くなりましたが、傾向は同じでした。よってパラメータチューニングには使えます。

return_train_score=Falseする(効果:それなり)

 さて、GridSearchCVはデフォルトの設定ではreturn_train_score='warn'になっています。「'warn'って何さ?」というと、こんな警告を出します。

FutureWarning: You are accessing a training score ('std_train_score'), which will not be available by default any more in 0.21. If you need training scores, please set return_train_score=True

 return_train_scoreは要するに学習データに対するスコアを計算するかどうかを指定できる引数です。この警告は割とくだらないことが書いてあるのですが、将来的にはこれがdefault=Falseにされるという警告です。

 基本的に、パラメータチューニングで見たいのはテストデータに対するスコアであるはずです。なのに、現在のデフォルト設定では学習データに対する評価指標も計算されてしまいます。

 これは無駄なので、return_train_score=Falseすると学習データに対する評価指標の計算分の計算コストをケチれます。予測時間なんてたかが知れていますが、それでも一応やってみましょう。

def main3():
    clf = GridSearchCV(pl, params, cv=2,
                       return_train_score=False,
                       n_jobs=-1)
    clf.fit(digits.data, digits.target)
    df = pd.DataFrame(clf.cv_results_)
    print_df(df)
    
if __name__ == "__main__":
    # print(timeit.timeit(main1, number=1))
    # print(timeit.timeit(main2, number=1))
    print(timeit.timeit(main3, number=1))

 この措置によって、処理時間は22.1秒まで短縮されました。ベースラインと比較すると1/2強の時間で済んでいる訳です。

まとめ

 この記事では

  • cv=2にする
  • return_train_score=Falseにする

 という方法で、パラメータチューニングの機能を損なわないまま2倍弱の速度の改善を実現しました。

工夫なし cv=2 cv=2&return_train_score=False
42.2秒 28.0秒 22.1秒

 このテクニックはきっと皆さんの役に立つと思います。

それでも時間がかかりすぎるときは

 そもそもグリッドサーチしようとしているパラメータ候補が多すぎる可能性があります。

 たとえば、3つのパラメータでそれぞれ10個の候補を調べるとなると、10*10*10=1000回の交差検証が行われます。いつまで経っても終わらない訳です。

 今回の記事では4*4*4=64回としましたが、これでもけっこう多い方だと思います。それでも解こうとしている問題が単純なので、デフォルトパラメータでも1分以内には処理できていますが、ちょっと重いモデルにちょっと多量のデータを突っ込んだりすると、もうダメです。何十分とか何時間もかかってしまいます。

 そういう場合、まずは粗いステップ(少ないパラメータ候補数)でざっくりパラメータチューニングしてしまい、どの辺りのパラメータが良いのかわかったら、その周辺に絞ってもう一回パラメータチューニングを行います。こういうのを二段グリッドサーチと言ったりします。

 あるいはベイズ最適化とか、他のアルゴリズムに走るのも一つの手かもしれません。

*1:なにせすべての組み合わせを計算する

*2:たとえば「今から回して、朝までにデータを出さないと教授への報告が間に合わないんだ!」みたいな状況を想定しています

【python】sklearnのVarianceThresholdを試してみる

はじめに

 VarianceThresholdは名前の通り、分散がしきい値以下の特徴量を捨てます。

sklearn.feature_selection.VarianceThreshold — scikit-learn 0.19.1 documentation

 これといってすごいところはありませんが、気楽に使えそうなので試してみました。

 目次

とりあえず試す

 しきい値の設定でどれだけ特徴量のshapeが減るか見てみました。データは20newsgroupsです。
Pipelineにしてあるのは、あとでこれを使って分類のチューニングをしてみるためです。

from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_selection import VarianceThreshold
from sklearn.pipeline import Pipeline

def test_shape():
    news20 = fetch_20newsgroups()
    cv = CountVectorizer(min_df=0.005,
                         max_df=0.5,
                         stop_words="english")
    vth = VarianceThreshold()
    pl = Pipeline([("cv", cv), ("vth", vth)])
    for v in [0.0,0.05,0.1,0.15]:
        pl.set_params(vth__threshold=v)
        print(pl.fit_transform(news20.data).shape)

if __name__ == "__main__":
    test_shape()

 関連記事:


 結果は、

(11314, 3705)
(11314, 1476)
(11314, 859)
(11314, 573)

 なるほど。
 (実際にはいろいろ試してちょうど良いshapeの減り具合になる値を探しています。これを使うならそういう作業が必要になると思います)

分類を試してみる

 これをうまく設定すると、分類精度が上がったりするのでしょうか?

import pandas as pd
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.preprocessing import FunctionTransformer
from sklearn.feature_selection import VarianceThreshold
from sklearn.naive_bayes import GaussianNB
from sklearn.pipeline import Pipeline
from sklearn.model_selection import GridSearchCV

def convert(x):
    return x.toarray()
def test_best_v():
    news20 = fetch_20newsgroups()
    cv = CountVectorizer(min_df=0.005,
                         max_df=0.5,
                         stop_words="english")
    vth = VarianceThreshold()
    sparse_to_dense = FunctionTransformer(func=convert,
                                          accept_sparse=True)
    gnb = GaussianNB()
    pl = Pipeline([("cv", cv),
                   ("vth", vth),
                   ("s2d", sparse_to_dense),
                   ("gnb", gnb)])
    
    params = {"vth__threshold":[0.0,0.05,0.1,0.15]}
    
    clf = GridSearchCV(pl, params, 
                       return_train_score=False,
                       n_jobs=-1)
    clf.fit(news20.data, news20.target)
    cv_result_df = pd.DataFrame(clf.cv_results_)
    df = cv_result_df[["param_vth__threshold", 
                       "mean_score_time", 
                       "mean_test_score"]]
    print(df)

if __name__ == "__main__":
    test_best_v()

 ただ単にナイーブベイズに入れて性能を見ているだけですが、かなり色々なテクニックを使っているコードなので、初見だと読みづらいと思います。

  • FunctionTransformer:

 ナイーブベイズが疎行列を受け付けてくれないので変換している。こんな関数ラムダ式で良いじゃんと思う向きもあるかもしれませんが、GridSearchCVでn_jobs=-1を指定するためにはトップレベル関数として定義してあげる必要があります(中でpickleを使うので)

  • GridSearchCV:

 return_train_score=Falseにすると速くなります。

  • pd.DataFrame

 GridSearchCV.cv_results_はそのままpandas.DataFrameに変換できるとドキュメントに書いてあるので、それを使ってpandasで取り扱っています。

 走らせた結果は、

  param_vth__threshold  mean_score_time  mean_test_score
0                    0         9.646958         0.656178
1                 0.05         4.504278         0.587149
2                  0.1         2.804257         0.512551
3                 0.15         1.909767         0.453244

 改善する訳ではない。CountVectorizerのmin_dfで予めゴミ変数を削っていること、スパースな空間なので分散が低くてもそれはそれで構わず、ナイーブベイズが意外とスパースに強いのも相まって優秀に働いていることが原因でしょう。

 それより注目すべきはmean_score_timeで、今回のデータで変数を削っていくと、しきい値を0.05上げるたびに、0.07ポイントくらいの性能低下と引き換えに半減するような傾向です。性能と時間のトレードオフになったときは、これをいじって調整する手はあるのかも。

まとめ

 微妙といえば微妙だし、データによっては効くのかもしれない気もします。とりあえず確実に速くはなります。性能はあまり重視していないとき、気楽に変数を捨てて速くするのに使えそうです。

sklearnのLabelEncoderとOneHotEncoderの使い方

はじめに

 sklearnのLabelEncoderとOneHotEncoderは、カテゴリデータを取り扱うときに大活躍します。シチュエーションとしては、

  • なんかぐちゃぐちゃとカテゴリデータがある特徴量をとにかくなんとかしてしまいたい
  • 教師ラベルがカテゴリデータなので数値ラベルにしたい

 こんなとき使えます。

 使い方は簡単なのですが、備忘録としてまとめておきます。

LabelEncoderの使い方

 厳密にはsklearn.preprocessing.LabelEncoderですね。

sklearn.preprocessing.LabelEncoder — scikit-learn 0.19.1 documentation

 必要なことは公式サンプルにぜんぶ書いてあるのですが、自分でも使ってみましょう。

>>> from sklearn.preprocessing import LabelEncoder
>>> week_breakfast = ["パン","ご飯","なし","パン","シリアル","なし","なし"]
>>> le = LabelEncoder()
>>> labels = le.fit_transform(week_breakfast)
>>> labels
array([3, 0, 1, 3, 2, 1, 1])

 このように変換できます。

 ラベルから元のカテゴリに変換するには?

>>> le.classes_  # indexとカテゴリが対応したnumpy配列になっていることを確認
array(['ご飯', 'なし', 'シリアル', 'パン'], dtype='<U4')
>>> [le.classes_[x] for x in labels]    # リスト内包表記を活用する(最速かどうかはよくわからない)
['パン', 'ご飯', 'なし', 'パン', 'シリアル', 'なし', 'なし']
>>> week_breakfast  # 確認のため再掲(私の食生活とかではありません)
['パン', 'ご飯', 'なし', 'パン', 'シリアル', 'なし', 'なし']

 普通にできますね。

 実際にはこういうことはしないと思います。その代わり、le.classes_をいろいろなものに渡して使うことができます。

 たとえば、分類をしてsklearn.metrics.classification_reportで見てみたいと思ったときに、

>>> from sklearn.metrics import classification_report
>>> print(classification_report(labels, labels))  # とりあえず両方同じlabelsを渡している
             precision    recall  f1-score   support

          0       1.00      1.00      1.00         1
          1       1.00      1.00      1.00         3
          2       1.00      1.00      1.00         1
          3       1.00      1.00      1.00         2

avg / total       1.00      1.00      1.00         7

 0,1,2,3だとわかりづらいですね。でもclassification_reportにはtarget_namesという引数があり、

>>> print(classification_report(labels, labels, target_names=le.classes_))
             precision    recall  f1-score   support

         ご飯       1.00      1.00      1.00         1
         なし       1.00      1.00      1.00         3
       シリアル       1.00      1.00      1.00         1
         パン       1.00      1.00      1.00         2

avg / total       1.00      1.00      1.00         7

 こうしてやることができる訳です。なので、LabelEncoderのインスタンスは、大切に(プログラムのスコープ上とかに)取っておきましょう。

 参考:
sklearn.metrics.classification_report — scikit-learn 0.19.1 documentation
sklearnのclassification_reportで多クラス分類の結果を簡単に見る - 静かなる名辞
 (二番目は自分の記事)

OneHotEncoderの使い方

 これはいわゆるOne-hot表現を得るものです。いろいろな機械学習フレームワークに類似の機能があると思いますが、sklearnではsklearn.preprocessing.OneHotEncoderが対応します。

 使い方は以下の通りです。

>>> import numpy as np
>>> data = np.arange(9).reshape(9,1)
>>> data
array([[0],
       [1],
       [2],
       [3],
       [4],
       [5],
       [6],
       [7],
       [8]])
>>> from sklearn.preprocessing import OneHotEncoder
>>> ohe = OneHotEncoder()
>>> ohe.fit_transform(data).A  # sparse matrixを返しやがるのでdenseにして見ている
array([[1., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 1., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 1., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 1., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 1., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 1., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 1., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 1.]])

 注意点として、ndim=1の配列を渡すとエラーになるので、ベクトル風の表現に直して渡してやる必要があります。

>>> ohe.fit_transform(np.arange(10))
# 中略
ValueError: Expected 2D array, got 1D array instead:
array=[0. 1. 2. 3. 4. 5. 6. 7. 8. 9.].
Reshape your data either using array.reshape(-1, 1) if your data has a single feature or array.reshape(1, -1) if it contains a single sample.
>>> ohe.fit_transform(np.arange(10).reshape(-1, 1)).A
array([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 1., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 1., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 1., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 1., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 1., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 1.]])

 ところで、ベクトル風にして渡すということは、こういうことになります。

>>> np.arange(10).reshape(5,2)
array([[0, 1],
       [2, 3],
       [4, 5],
       [6, 7],
       [8, 9]])
>>> ohe.fit_transform(np.arange(10).reshape(5,2)).A
array([[1., 0., 0., 0., 0., 1., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0., 0., 1., 0., 0., 0.],
       [0., 0., 1., 0., 0., 0., 0., 1., 0., 0.],
       [0., 0., 0., 1., 0., 0., 0., 0., 1., 0.],
       [0., 0., 0., 0., 1., 0., 0., 0., 0., 1.]])

 なるほど、こうなるのか。

 文字列でもいけるかな?

>>> week_breakfast = ["パン","ご飯","なし","パン","シリアル","なし","なし"]
>>> ohe.fit_transform(np.array(week_breakfast).reshape(-1, 1))
# 中略
ValueError: could not convert string to float: 'パン'

 ダメでした。LabelEncoderと併用して数値ラベルにしておく必要があるということだと思います(ドキュメントにもそんな感じのことが書いてある)。

おまけ:CategoricalEncoder

 これは開発中のscikit-learn 0.20の機能です。なので、まだ使えません。リリース待ちです(2018年6月現在)。

sklearn.preprocessing.CategoricalEncoder — scikit-learn 0.20.dev0 documentation

 いろいろと柔軟に使えるような機能が追加されているようです。リリースされたら、こっちも使ってみましょう(つっても、まだまだ時間かかりそうよねぇ)。

【python】反転させて先頭n個取るスライス

 タイトルの通りのものが必要になりました。一体どう書くのでしょう?

とりあえず反転させる

>>> lst = list(range(20))
>>> lst[::-1]
[19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

 ま、これは常識(python廃人の皆さんには)。

n個取ってみる

>>> lst[::-1][:10]
[19, 18, 17, 16, 15, 14, 13, 12, 11, 10]

 こうするとリストオブジェクトの生成が二回繰り返されるので遅いはずです。できればスライス一発で済ませたい。

やってみる

>>> lst[:9:-1]
[19, 18, 17, 16, 15, 14, 13, 12, 11, 10]

 なんとなくできたような気になりますが、

>>> lst[:3:-1]
[19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4]

 逆向きなのでした。スライスも左から順に評価される(切り出してから反転)のでしょうか。

正しい(?)やりかた

>>> lst[:-4:-1]
[19, 18, 17]

 微妙な違和感が・・・

先頭n個取って反転

 これもやってみよう。考え方は同じです。

>>> lst[2::-1]
[2, 1, 0]

測ってみた

>>> import timeit
>>> timeit.timeit(lambda : lst[::-1][:3])
0.35431641300010597
>>> timeit.timeit(lambda : lst[:-4:-1])
0.20682314900022902

 計測誤差ではないようです。かくして約43%の高速化が実現されました。

追記:書き上げて投稿してから思いついた別解

>>> lst[:-4:-1]
[19, 18, 17]
>>> lst[-3:][::-1]
[19, 18, 17]

 結果は同じ。これだと反転するリストが小さいので速いのでは? と思いました。

 効果を見やすくするために、長さ1000のリストで試してみます。

>>> timeit.timeit(lambda : lst[:-4:-1])
0.21827364299997498
>>> timeit.timeit(lambda : lst[::-1][:3])
2.8037329549997594
>>> timeit.timeit(lambda : lst[-3:][::-1])
0.33725221500026237

 早い順に、

  1. 1つのスライスで反転と切り出しをやる
  2. 後ろ3つを取ってから反転
  3. ぜんぶ反転させてから先頭3つを取る

 となりました。

 「ぜんぶ反転させてから~」が遅いのはまあ、予想通りですが、「後ろ3つを取ってから~」は1つのスライスでやる方法に勝てておらず、リストが小さいときの「ぜんぶ反転させてから~」と同程度。つまりオーバーヘッド分が同程度あるということです。また、built-inのスライスはそうとう気が効いてるみたいです(恐らく、lenを見てどの順番で切っていくか決めているのでは? 確認していませんが・・・)。

 結論としては、とにかくできるだけ1つのスライスで書いちゃおう、ということになると思います。ただし、実際には大した時間じゃないので、可読性重視で分けるのも不可ではない、という程度です。

【python】べき乗とべき根の計算

 べき乗はx^n、べき根は\sqrt[n]{x}です。2乗とかsqrtくらいはわかっても、n乗根あたりになるとすぐ出てこないという人も多いのでは?

 目次

組み込み関数powを使う方法

 powという組み込み関数があります。まあ、要は達します。

>>> pow(2, 10)
1024
>>> pow(0.5, 10)
0.0009765625
>>> pow(1.5, 3.5)
4.133513940946613

 見たところ浮動小数点でも大丈夫。

 ドキュメントによると、第三引数を指定することで「x の y 乗に対する z の剰余」を計算できるそうですが、何に使えるのでしょうかね・・・。

2. 組み込み関数 — Python 3.6.5 ドキュメント

べき乗演算子を使う方法

 まったく同じことが演算子でもできます。

>>> 2**10
1024
>>> 0.5**10
0.0009765625
>>> 1.5**3.5
4.133513940946613

 powは必要なのでしょうか。powを多用すると式がごちゃごちゃするので、こちらを使いたいです。

 ただ、罠があるようです。

Python のべき乗演算子に潜む罠 | CUBE SUGAR STORAGE

 思い通りの結果を得たければカッコを多用すべきであり、そうするとなんかpowでも良いような気もしてきます。

numpyに頼る方法

 np.powerがあります。

>>> np.power(2, 10)
1024
>>> np.power(0.5, 10)
0.0009765625
>>> np.power(1.5, 3.5)
4.133513940946613

 見たところ、型の取扱も含めてほぼ同じ。まあ、これを単体で使うメリットは特に感じません。numpy配列を相手にするなら、ありかも(ただしnumpy配列にべき乗演算子を使うという選択肢もある)。なお、片方が配列で片方がスカラー、とか配列同士、というケースでは、

>>> import numpy as np
>>> np.power([1,2,3], 2)
array([1, 4, 9])
>>> np.power(2, [1,2,3])
array([2, 4, 8])
>>> np.power([1,2,3], [4,5,6])
array([  1,  32, 729])

 こんな扱いになります。特に難しいことはないです。

n乗根について

 n乗根(べき根)がわからなかった人は、\sqrt[2]{x} = x^\frac{1}{2}を思い出しましょう。

>>> 2**10  # べき乗(10乗)
1024
>>> 1024**(1/10)  # べき根(10乗根)
2.0

 小数のべき乗が計算できれば、べき根も計算できるということです。

どれが速いの?

 timeitで簡単に比較してみます。私はIPython使いではないので、モジュールimportで使います。

>>> import numpy as np
>>> import timeit
>>> timeit.timeit(lambda : [[x**y for y in range(30)] for x in range(30)], number=1000)
0.37399847699998645
>>> timeit.timeit(lambda : [[pow(x,y) for y in range(30)] for x in range(30)], number=1000)
0.4485901359998934
>>> timeit.timeit(lambda : [[np.power(x,y) for y in range(30)] for x in range(30)], number=1000)
1.3186961119999978

 スカラーに対してはべき乗演算子一択。

 numpy配列だと、べき乗演算子とnp.powerはどちらが上でしょうか。

>>> timeit.timeit(lambda : np.arange(1000)**np.arange(1000), number=100000)
1.4120757860000595
>>> timeit.timeit(lambda : np.power(np.arange(1000), np.arange(1000)), number=100000)
1.402805570000055

 そもそも冗談みたいに速いんですが(numberに注目)、速度差自体は計測誤差レベル。一番コアな部分は同じ処理なのでしょう。

まとめ

 ま、べき乗演算子ですべてまかなえるので、罠に気をつけて使えば良いと思いました。

 あと、numpyすごいなぁ・・・