静かなる名辞

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


【python】numpy配列を分割する方法まとめ

はじめに

 numpy配列を分割したくなることがたまにありますよね。

 当然というか、それ用の関数が用意されています。でも使い方をよく忘れるので覚書として書いておくことにします。

 目次

スポンサーリンク



np.split

 そのまますぎる名前の関数がある。

numpy.split — NumPy v1.16 Manual

>>> import numpy as np
>>> a = np.arange(10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> np.split(a, 2)
[array([0, 1, 2, 3, 4]), array([5, 6, 7, 8, 9])]

 わかりやすいですね。返り値は「リスト」で、ドキュメントにも明確にそう書いてあります。

 第二引数は「indices_or_sections : int or 1-D array」で、2通りの使い方があることがわかります。配列も渡せるので試してみます。

>>> np.split(a, [2,8])
[array([0, 1]), array([2, 3, 4, 5, 6, 7]), array([8, 9])]

 要するにスライスですね。こうしたのと一緒。

>>> [a[:2], a[2:8], a[8:]]
[array([0, 1]), array([2, 3, 4, 5, 6, 7]), array([8, 9])]

 numpyらしくaxisも指定できる。デフォルトは0。これは2次元配列で試してみましょう。

>>> b = np.arange(16).reshape(4, 4)
>>> b
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15]])
>>> np.split(b, 2)
[array([[0, 1, 2, 3],
       [4, 5, 6, 7]]),
 array([[ 8,  9, 10, 11],
       [12, 13, 14, 15]])]
>>> np.split(b, 2, axis=1)
[array([[ 0,  1],
       [ 4,  5],
       [ 8,  9],
       [12, 13]]),
 array([[ 2,  3],
       [ 6,  7],
       [10, 11],
       [14, 15]])]
>>> np.split(b, [1,3])
[array([[0, 1, 2, 3]]),
 array([[ 4,  5,  6,  7],
       [ 8,  9, 10, 11]]),
 array([[12, 13, 14, 15]])]
>>> np.split(b, [1,3], axis=1)
[array([[ 0],
       [ 4],
       [ 8],
       [12]]),
 array([[ 1,  2],
       [ 5,  6],
       [ 9, 10],
       [13, 14]]),
 array([[ 3],
       [ 7],
       [11],
       [15]])]

 結果が見づらいので、適当に整形してあります。見ての通りの使い方ができます。

np.array_split

 array_splitというちょっと名前が違う関数があります。

numpy.array_split — NumPy v1.16 Manual

 ドキュメントを見ると、存在意義を疑いたくなるようなことが書いてある。

Please refer to the split documentation. The only difference between these functions is that array_split allows indices_or_sections to be an integer that does not equally divide the axis. For an array of length l that should be split into n sections, it returns l % n sub-arrays of size l//n + 1 and the rest of size l//n.

 拙訳:splitのドキュメントを見てね。splitとの違いは、array_splitは分割した結果の大きさが違ってても使えることだよ。長さlの配列を分割するときに、要素数が(l//n) + 1個の配列がl%n個できて、あとはl//nの長さになるような挙動になるよ。

>>> a = np.arange(10)
>>> np.split(a, 3)
Traceback (most recent call last):
  File "***/lib/python3.5/site-packages/numpy/lib/shape_base.py", line 553, in split
    len(indices_or_sections)
TypeError: object of type 'int' has no len()

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "***/lib/python3.5/site-packages/numpy/lib/shape_base.py", line 559, in split
    'array split does not result in an equal division')
ValueError: array split does not result in an equal division
>>> np.array_split(a, 3)
[array([0, 1, 2, 3]), array([4, 5, 6]), array([7, 8, 9])]

 たったこれだけの違い。もう少し細かく見ていくと、

>>> np.array_split(np.arange(11), 5)
[array([0, 1, 2]), array([3, 4]), array([5, 6]), array([7, 8]), array([ 9, 10])]
>>> np.array_split(np.arange(12), 5)
[array([0, 1, 2]), array([3, 4, 5]), array([6, 7]), array([8, 9]), array([10, 11])]
>>> np.array_split(np.arange(13), 5)
[array([0, 1, 2]), array([3, 4, 5]), array([6, 7, 8]), array([ 9, 10]), array([11, 12])]
>>> np.array_split(np.arange(14), 5)
[array([0, 1, 2]), array([3, 4, 5]), array([6, 7, 8]), array([ 9, 10, 11]), array([12, 13])]

 前の方の配列から長さが増えていく感じの挙動になるようです。あまり信頼しない方が良いとはおもいますが……。

 ちなみに、他の使い方はすべて同じで、第二引数に配列を渡したりaxisを変えたりもできます。

 同じ関数にしてオプション引数で挙動を変えれば十分な気がするけど、歴史的経緯などで統一されていないのかなぁ。

vsplit, hsplit, dsplit

 こういうものも用意されています。お察しの通り、splitのaxis固定バージョンです。v, h, dはそれぞれvertically, horizontally, depthですね(統一しろよと思わなくもないが、ドキュメントにこう書いてあるので……)。

 vsplitはaxis=0, hsplitはaxis=1, dsplitはaxis=2に対して分割を行います。

Array manipulation routines — NumPy v1.16 Manual

まとめ

 こんな感じでできます。

 割り切れる場合はnp.split()、割り切れない場合はnp.array_split()を使うのかな? とも思ったが、どちらでもいけるnp.array_split()だけ覚えておけば良いような気もする。お好みで。

 関連記事
 結合する場合
www.haya-programming.com

【python】関数内関数は動的に生成される

 わかっている人には当たり前のことですが、他の言語から来た人だと「んんん?」かもしれません。

こうなる

>>> def f():
...     def g():
...         pass
...     return g
... 
>>> a = f()
>>> b = f()
>>> a is b
False
>>> id(a)
139834257176640
>>> id(b)
139834231735568

解説

 わかりやすいように、disモジュール(標準)のバイトコードディスアセンブラで見てみましょうか。

>>> import dis
>>> dis.dis(f)
  2           0 LOAD_CONST               1 (<code object g at 0x7f2db33a4a50, file "<stdin>", line 2>)
              3 LOAD_CONST               2 ('f.<locals>.g')
              6 MAKE_FUNCTION            0
              9 STORE_FAST               0 (g)

  4          12 LOAD_FAST                0 (g)
             15 RETURN_VALUE

 中身については深く検討していないのですが*1、MAKE_FUNCTIONがあることはわかりますね。

 これが関数呼び出しのたびに実行され、新たな関数オブジェクトが生成されている訳です。

 名前空間だけ間借りしている訳ではありません。

なにか問題なのか

 これのせいで微妙に遅くなる可能性はあるので、測ります。

 コード内で使っているFizz Buzzのコードはこちらからお借りしました。

PythonでFizz Buzz書いてみた - Qiita

import timeit

def fizzbuzz(n):
    for i in range(1, n):
        if i % 15 == 0:
            print("Fizz Buzz!")
        elif i % 3 == 0:
            print("Fizz!")
        elif i % 5 == 0:
            print("Buzz!")
        else:
            print(i)

def f():
    def fizzbuzz_infunc(n):
        for i in range(1, n):
            if i % 15 == 0:
                print("Fizz Buzz!")
            elif i % 3 == 0:
                print("Fizz!")
            elif i % 5 == 0:
                print("Buzz!")
            else:
                print(i)
    return fizzbuzz_infunc

def g():
    return fizzbuzz

print(timeit.timeit(f, number=10**6))
print(timeit.timeit(g, number=10**6))
""" 結果=>
0.13979517295956612
0.08669622003799304
"""

 まあ微妙に遅いけど、気にするほどではないかも。

 これを利用するとクロージャが作れるというのは有名な話ですね。

*1:そもそも私はpythonのバイトコードなんか読めない

【python】内包表記をbreakする方法を考える

 リスト内包表記や辞書内包表記、ジェネレータ式などの内包表記は便利ですが、途中で止めたいときがあったとして(あるかどうかは知りませんが)どうしたら良いのでしょう?

カウンタを使う

 こういう方法を真っ先に思いつきます。

>>> [x for x in range(20) if x < 10]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

 enumerateを使うと任意のiterableに対して使えます。

>>> [x for i, x in enumerate("hoge"*100) if i < 10]
['h', 'o', 'g', 'e', 'h', 'o', 'g', 'e', 'h', 'o']

 欠点としては、内包表記のループ対象のiterableの計算コストが高い場合は無意味になることが挙げられると思います。また、コードが冗長な感があります。

StopIteration例外を投げる

 こちらで紹介されている方法です。

Python の内包表記の使い方まとめ - Life with Python

def end_of_loop():
    raise StopIteration

list(x if x < 10 else end_of_loop() for x in range(100))
# 次の書き方でもOK
# list(x if x < 10 else next(iter([])) for x in range(100))

 これ以上良い方法は簡単には思いつきません。だけど、ちょっと冗長でわかりづらい感じがします。

 残念ながらraiseは式ではなく文なので、普通には内包表記の子に入れられません。

 また、これで止めるとリスト内包表記、辞書内包表記ではそのまま例外が戻ってきます。ジェネレータ式だからできる芸当と言えるでしょう。

itertools.takewhile

 素直にitertools.takewhileを使うべきでは? という考え方もあります。

10.1. itertools — 効率的なループ実行のためのイテレータ生成関数 — Python 3.6.5 ドキュメント

>>> t = takewhile(lambda x:x < 10, (x for x in range(20)))
>>> t
<itertools.takewhile object at 0x7f2b68c049c8>
>>> list(t)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

 すべてをジェネレータで作る前提でコードを書けば使えると思います。

その他

 黒魔術を使うとやり方は色々あると思いますが、ここでは触れません。

まとめ

 素直にfor文のループで書けばいいと思いました(小学生並みの感想)。

【python】execを使って変数名を動的に変える方法についての考察

はじめに

 pythonでどうしても変数名を動的に変えたい場合、execを使うことになる。

 実用的には無意味というかやるべきではないのだけど(他の方法でもっと合理的なコードが書ける)、やった場合の挙動でちょっと気になる点があったので、検証して記事にまとめておく。

スポンサーリンク


やり方としてはexecでやれば良い

 まずやり方から。

exec("hoge = 'ほげ'")  # hoge = 'ほげ'と書いたのと同じ
print(hoge) # => ほげ

 stringを引数に渡せるので、なんでもし放題です。でも、こういうことをすると何かと大変なことになるので、やめておいた方が無難。

もっといいやり方

 もっとマシなやり方も、念の為紹介しておく。

d = dict()
d["hoge"] = "ほげ"
print(d["hoge"]) # => ほげ

 識別子がほしいときは辞書などのコレクション型などに入れよう。それで普通は事足りる。

 あるいは、連番の変数を作りたがる人も一定数いる。

# こんな奴
string1 = "ほげ"
string2 = "ふが"
string3 = "ぴよ"

# 動的に生成するとしたら
for i, s in enumerate(["'ほげ'","'ふが'", "'ぴよ'"]):
    exec(f"string{i+1} = {s}")

 リストに入れれば事足りる。

strings = ["ほげ", "ふが", "ぴよ"]

# indexが0からなのは仕方ない。プログラミングをやるなら慣れた方が良い。
print(strings[0]) # => ほげ

素朴な疑問

 ここからが本編で、この記事で考察したいことである。

 考察したいこと:
  スコープどうなってるの?

hoge = 10
def f():
    exec("hoge = 'ほげ'")
    print(hoge)
f() # => 10

 理由がわからない。仕方ないのでドキュメントを見に行く。

exec(object[, globals[, locals]])(原文)
この関数は Python コードの動的な実行をサポートします。 object は文字列かコードオブジェクトでなければなりません。文字列なら、その文字列は一連の Python 文として解析され、そして (構文エラーが生じない限り) 実行されます。 [1] コードオブジェクトなら、それは単純に実行されます。どの場合でも、実行されるコードはファイル入力として有効であることが期待されます (リファレンスマニュアルの節 "file-input" を参照)。なお、 return および yield 文は、 exec() 関数に渡されたコードの文脈中においてさえ、関数定義の外では使えません。返り値は None です。

いずれの場合でも、オプションの部分が省略されると、コードは現在のスコープ内で実行されます。globals だけが与えられたなら、辞書でなくてはならず、グローバル変数とローカル変数の両方に使われます。globals と locals が与えられたなら、それぞれグローバル変数とローカル変数として使われます。locals を指定する場合は何らかのマップ型オブジェクトでなければなりません。モジュールレベルでは、グローバルとローカルは同じ辞書です。exec が globals と locals として別のオブジェクトを取った場合、コードはクラス定義に埋め込まれたかのように実行されます。

globals 辞書がキー __builtins__ に対する値を含まなければ、そのキーに対して、組み込みモジュール builtins の辞書への参照が挿入されます。ですから、実行されるコードを exec() に渡す前に、 globals に自作の __builtins__ 辞書を挿入することで、コードがどの組み込みを利用できるか制御できます。

注釈 組み込み関数 globals() および locals() は、それぞれ現在のグローバルおよびローカルの辞書を返すので、それらを exec() の第二、第三引数にそのまま渡して使うと便利なことがあります。
注釈 標準では locals は後に述べる関数 locals() のように動作します: 標準の locals 辞書に対する変更を試みてはいけません。 exec() の呼び出しが返る時にコードが locals に与える影響を知りたいなら、明示的に locals 辞書を渡してください。

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


 ダメだ、よくわからん。落ち着いて考え直してみる。

 文が実行されていれば、少なくともglobals辞書かlocals辞書のどちらかには追加されるはずだ。

hoge = 10
def f():
    exec("hoge = 'ほげ'")
    print(globals())
    print(locals())
f()
""" =>
{'__cached__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f5989241b00>, '__doc__': None, 'hoge': 10, '__builtins__': <module 'builtins' (built-in)>, '__name__': '__main__', 'f': <function f at 0x7f5989277f28>, '__spec__': None, '__file__': 'exec_scope_test.py'}
{'hoge': 'ほげ'}
"""

 しっかりローカル変数に追加されている。ということは、文は実行されている。

 そうか、わかった。スコープは関数定義時に決まるんだった。定義時にはexecは実行されないので、関数内から見えるhogeはグローバルに定義したhogeになる。あとで実行時にローカル変数が増えようが、そんなのはお構いなしにグローバルのhogeを見続けるということか。

 じゃあ、こうするとどうなるの?

def f():
    exec("hoge = 'ほげ'")
    print(hoge)
f()
Traceback (most recent call last):
  File "exec_scope_test.py", line 4, in <module>
    f()
  File "exec_scope_test.py", line 3, in f
    print(hoge)
NameError: name 'hoge' is not defined

 こちらも同様の事情である。関数定義時に、関数のスコープの中にはhogeが見つからないので、pythonインタプリタはhogeがグローバル変数だと*1勝手に認識する。あとはずっとそう認識しっぱなしなので、「hogeなんてグローバル変数は見つからなかった」という結果になってエラーを吐く。

 この例について考えてみると、わかりやすい。

>>> def f():
...     print(i)
... 
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
NameError: name 'i' is not defined
>>> i = 0
>>> f()
0

 そしてこれをなんとか意図通り動かすのは、おそらく無理じゃないかな。ちょっと思いつかない。

 やりたいことからしたら無意味なんだろうけど、ローカル変数を先に作っておけばできるのだろうか。

def f():
    hoge = None
    exec("hoge = 'ほげ'")
    print(hoge)
    print(locals())
f()
""" =>
None
{'hoge': None}
"""

 できないのかよ。これは何事かと思ってググったら、出てきた。

python - How does exec work with locals? - Stack Overflow

 ややこしい話だから読みたい人は勝手に読んでいただけば良いと思う。とりあえず、こうすると回避はできる。

def f():
    hoge = None
    ldict = {}
    exec("hoge = 'ほげ'", globals(), ldict)
    print(ldict["hoge"])
f() # => ほげ

 この結果を喜んではいけない。「もっといいやり方」で紹介した辞書を使う方法を、とても回りくどく実行しているだけだからだ。

 あと、上のリンクにも書いてあるけど、python2だと普通(?)にできるんだってさ。

>>> def f():
...     hoge = None
...     exec "hoge = 'ほげ'"
...     print hoge
... 
>>> f()
ほげ

 嫌になっちゃうね。

まとめと結論

 execを使うとスコープの概念がぶっ壊れてほんとうにつらいことになる。

 すべてをグローバルな空間で完結させればこの記事に書いたような問題には引っかからないだろうけど、ある程度の規模のプログラムをまともな規模で書くことはできなくなるので、本当に使いみちがない。

 やはり、やらないでおいた方が良い。

*1:厳密には少なくともローカルスコープではないと認識して実行時に上位スコープをたどる・・・んだっけ?

【python】ランダムフォレストのOOBエラーが役に立つか確認

はじめに

 RandomForestではOOBエラー(Out-of-bag error、OOB estimate、OOB誤り率)を見ることができます。交差検証と同様に汎化性能を見れます。

 原理の説明とかは他に譲るのですが、これはちゃんと交差検証のように使えるのでしょうか? もちろん原理的には使えるのでしょうが、実際どうなるのかはやってみないとわかりません。

 もしかしたらもう他の人がやっているかもしれませんが*1、自分でやった方が納得感があります*2

 ということで、やってみました。

みたいこと

 とりあえずトイデータでやってみて、交差検証の場合とスコアを比べる。交差検証は分割のkを変えて様子を見る必要があるでしょう。

 また、モデルの性能がよくなったり悪くなったりしたとき、交差検証と同様のスコアの変化が見れるかも確認してみる必要がありそうです。

プログラム

 こんなプログラムを書きました。

import time

import numpy as np
from sklearn.datasets import load_iris, load_digits, load_wine
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import precision_recall_fscore_support as prf

def test_func(dataset):
    rfc = RandomForestClassifier(n_estimators=300, oob_score=True, n_jobs=-1)
    t1 = time.time()
    rfc.fit(dataset.data, dataset.target)
    t2 = time.time()
    oob_pred = rfc.oob_decision_function_.argmax(axis=1)
    print("{0:6} p:{2:.4f} r:{3:.4f} f1:{4:.4f}  time:{1:.4f}".format(
        "oob", t2-t1,
        *prf(dataset.target, oob_pred, average="macro")))

    rfc = RandomForestClassifier(n_estimators=300, oob_score=False, n_jobs=-1)
    for k in [2,4,6,8]:
        skf = StratifiedKFold(n_splits=k)
        trues = []
        preds = []
        t1 = time.time()
        for train_idx, test_idx in skf.split(dataset.data, dataset.target):
            rfc.fit(dataset.data[train_idx], dataset.target[train_idx])
            trues.append(dataset.target[test_idx])
            preds.append(rfc.predict(dataset.data[test_idx]))
        t2 = time.time()

        print("{0:6} p:{2:.4f} r:{3:.4f} f1:{4:.4f}  time:{1:.4f}".format(
            "CV k={}".format(k), t2-t1,
            *prf(np.hstack(trues), np.hstack(preds), average="macro")))        

def main():
    iris = load_iris()
    digits = load_digits()
    wine = load_wine()

    print("iris")
    test_func(iris)

    print("\ndigits")
    test_func(digits)

    print("\nwine")
    test_func(wine)

    print("\niris + noise")
    iris.data += np.random.randn(*iris.data.shape)*iris.data.std()
    test_func(iris)

    print("\ndigits + noise")
    digits.data += np.random.randn(*digits.data.shape)*digits.data.std()
    test_func(digits)

    print("\nwine + noise")
    wine.data += np.random.randn(*wine.data.shape)*wine.data.std()
    test_func(wine)

if __name__ == "__main__":
    main()

 注目ポイント。

  • iris, digits, wineでためしました
  • rfc.oob_decision_function_.argmax(axis=1)でOOBで推定されたラベルが得られるので、それを使って精度、再現率、F1値を計算しています(マクロ平均)。交差検証でも同様に計算することで、同じ指標で比較を可能にしています(accuracyだけだと寂しいので・・・)
  • 処理の所要時間も測った
  • 特徴量にノイズを付与して分類させることで、条件が悪いときのスコアも確認。ノイズは特徴量全体の標準偏差くらいの正規分布を付与しました。理論的な根拠は特にないです(だいたい軸によってスケールが違うのを無視しているのだし・・・)

 だいたいこんな感じで、あとは普通にやってます*3

結果

 テキスト出力をそのまんま。

iris
oob    p:0.9534 r:0.9533 f1:0.9533  time:0.5774
CV k=2 p:0.9534 r:0.9533 f1:0.9533  time:1.3196
CV k=4 p:0.9600 r:0.9600 f1:0.9600  time:2.7399
CV k=6 p:0.9600 r:0.9600 f1:0.9600  time:4.1367
CV k=8 p:0.9600 r:0.9600 f1:0.9600  time:5.9125

digits
oob    p:0.9795 r:0.9794 f1:0.9794  time:0.9511
CV k=2 p:0.9282 r:0.9271 f1:0.9272  time:1.9101
CV k=4 p:0.9429 r:0.9422 f1:0.9420  time:4.0144
CV k=6 p:0.9519 r:0.9515 f1:0.9515  time:5.9703
CV k=8 p:0.9500 r:0.9494 f1:0.9494  time:7.8814

wine
oob    p:0.9762 r:0.9803 f1:0.9780  time:0.6950
CV k=2 p:0.9748 r:0.9812 f1:0.9774  time:1.2872
CV k=4 p:0.9714 r:0.9746 f1:0.9728  time:3.2011
CV k=6 p:0.9603 r:0.9643 f1:0.9619  time:4.4640
CV k=8 p:0.9714 r:0.9746 f1:0.9728  time:6.1454

iris + noise
oob    p:0.4723 r:0.4800 f1:0.4759  time:0.6677
CV k=2 p:0.5298 r:0.5267 f1:0.5279  time:1.3729
CV k=4 p:0.5456 r:0.5533 f1:0.5489  time:3.1605
CV k=6 p:0.4776 r:0.4800 f1:0.4788  time:4.1999
CV k=8 p:0.5043 r:0.5000 f1:0.5009  time:6.0875

digits + noise
oob    p:0.7850 r:0.7853 f1:0.7834  time:1.4698
CV k=2 p:0.7365 r:0.7377 f1:0.7342  time:2.8328
CV k=4 p:0.7567 r:0.7568 f1:0.7543  time:5.4918
CV k=6 p:0.7682 r:0.7697 f1:0.7671  time:9.1825
CV k=8 p:0.7717 r:0.7714 f1:0.7692  time:12.1008

wine + noise
oob    p:0.5377 r:0.5483 f1:0.5348  time:0.7344
CV k=2 p:0.4911 r:0.5034 f1:0.4873  time:1.4287
CV k=4 p:0.4828 r:0.5058 f1:0.4855  time:3.1024
CV k=6 p:0.5375 r:0.5439 f1:0.5320  time:4.3943
CV k=8 p:0.4778 r:0.5129 f1:0.4840  time:5.9890

 これからわかることとしては、

  • 全体的にOOBエラーとCVで求めたスコアはそこそこ近いので、OOBエラーはそこそこ信頼できると思います
  • OOBは短い時間で済むので、お得です
  • CVの場合はkを大きくすると性能が上がりますが、これは学習に使うデータ量がk=2なら全体の1/2、k=4なら3/4、k=8なら7/8という風に増加していくからです
  • OOBエラーがCVのスコアを上回る場合、下回る場合ともにあるようです。OOBエラーは、学習しているデータ量はほぼleave one outに近いものの、木の本数が設定値の約1/3くらいになるという性質があります。学習データ量の有効性が高いデータセットではCVの場合より高いスコアに、木の本数の有効性が高いデータセットではCVの場合に対して低いスコアになるということでしょう

 まあ、とりあえず妥当に評価できるんじゃねえの? という感じがします。

 もちろん同じ条件下で計測したスコアではないので、OOBエラーと交差検証の結果を直接比較することはできませんが、OOBエラー同士の優劣で性能を見積もる分にはたぶん問題ないでしょう*4

 注意点としては、OOBエラーは全体の木の約1/3(厳密には36%くらい)を使って予測するので、実際の結果よりは悪めに出る可能性があります。木の本数を多めにおごってやると良いでしょう。

結論

 OOBでもいい。

参考

qiita.com

*1:というか論文は確実にあると思いますが

*2:し、簡単なことなら人の書いた論文を読み解くより自分でやった方が楽だったりします

*3:プログラムを書き上げて回してから記事にしているので説明が雑・・・

*4:逆に言えば、他の分類器との比較には使えないというけっこう致命的な欠点がある訳ですが・・・

MeCab+Pythonでunidicを使う

はじめに

 MeCabの辞書といえばipadicが定番ですが、unidicという辞書もあります。

 ちょっとこれを使いたくなったので、使ってみることにしました。

 なお、MeCabおよびmecab-pythonはすでにipadic等で使える状況になっているものとします。

 目次

unidicのいいところ

  • 語彙や単語の分割(これに関しては良し悪しの世界・・・ipadicにはmecab-ipadic-NEologdがあるし)
  • 話し言葉、古文版の辞書がある
  • 分類語彙表(シソーラス)と組み合わせて使える。

unidicをmecabで使えるようにする

 とりあえず落としてきます。

「UniDic」国語研短単位自動解析用辞書

 最新版ダウンロードのページからダウンロードできます。ファイルサイズがでかいので、気長に落とします(相手サーバの回線は速いらしく、私が落としたときは十分程度で終わりました)。

 ダウンロードが終わったら、解凍してディレクトリを作ってください。場所はどこでも構いませんが、わかりやすい場所にしておくことをおすすめします*1

 できあがったディレクトリの中には、けっこう色々なファイルが入っています。が、その中身がすべて必要かというとそうでもなく、確認していませんが、

  • char.bin
  • matrix.bin
  • sys.dic
  • unk.dic
  • dicrc

 あたりがあればとりあえず動くはずです。この他には、ライセンスファイル等と、自分でビルドするときに必要になるファイルが同包されています。ビルドするのは大変なので、今回はバイナリ版を素直に使うことにします*2。なお、ビルド手順についてはこちらが参考になります。

MeCab で UniDic 辞書を使ってみる / 桃缶食べたい。

 なお、実行する前に、こちらを参考にしてunidicのdicrcを編集し、出力フォーマットを整えておきます。

UniDic - rmecab

 デフォルトの出力フォーマットは、何かうまくいっていないような感じだからです。

;以下の2行をコメントアウトする(連続しているとは限らない)
;bos-feature = BOS/EOS,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*,*
;node-format-unidic22 = %m\t%f[0],%f[1],%f[2],%f[3],%f[4],%f[5],%f[6],%f[7],%f[8],%f[9],%f[10],%f[11],%f[12],%f[13],%f[14],%f[15],%f[16],%f[17],%f[18],%f[19],%f[20],%f[21],%f[22],%f[23],%f[24],%f[25],%f[26],%f[27],%f[28]\n

;以下の2行を追加
bos-feature = BOS/EOS,*,*,*,*,*,*,*,*
node-format-unidic22 = %m\t%f[0],%f[1],%f[2],%f[3],%f[4],%f[5],%f[10],%f[9],%f[11]\n

 2018年7月現在では上記リンクとは微妙に設定ファイルの書式が変わっているようですが、そんなに困惑するほどの違いはないのでそのまま書き換えます。

 書き換えたら、コマンドラインで以下のように打ってみます。

$ mecab -d 解凍してできたディレクトリのパス
適当な日本語
適当	名詞,普通名詞,サ変形状詞可能,,,,適当,テキトー,テキトー
な	助動詞,,,,助動詞-ダ,連体形-一般,だ,ナ,ダ
日本	名詞,固有名詞,地名,国,,,日本,ニホン,ニホン
語	名詞,普通名詞,一般,,,,語,ゴ,ゴ
EOS

 上のように出力されれば成功です。

 unidicをデフォルトにしたければ、mecabrcなどを書き換えるという作業がこの後に続きますが、私はデフォルトはipadicでいいのでそのままにします。

mecab-pythonから呼ぶ

 コマンドライン引数をMeCab.Taggerの引数に渡すだけ。

>>> import MeCab
>>> tagger = MeCab.Tagger("-d 解凍してできたディレクトリのパス")
>>> print(tagger.parse("何らかの日本語"))
何	代名詞,,,,,,何,ナン,ナン
ら	接尾辞,名詞的,一般,,,,ら,ラ,ラ
か	助詞,副助詞,,,,,か,カ,カ
の	助詞,格助詞,,,,,の,ノ,ノ
日本	名詞,固有名詞,地名,国,,,日本,ニホン,ニホン
語	名詞,普通名詞,一般,,,,語,ゴ,ゴ
EOS

 簡単でいいですね。

*1:ホーム以下にディレクトリを掘って置いても良いし、正式にはmecabの辞書を置くディレクトリに入れるべきでしょうか

*2:linux環境、UTF-8で使う分には問題ないと思います。それ以外だと文字コードを変換して自分でビルドする作業が必要になるはずです

【python】多重リストを昇降混ぜてソート

 pythonでは多重リストのソートは次のように書ける。

import random
from pprint import pprint

data = [[random.randint(0, 20), 
         random.randint(0, 20)]
        for _ in range(10)]

print("data")
pprint(data)

print("\nsorted data")
pprint(sorted(data, key=lambda x:(x[0], x[1])))

 結果

data
[[6, 10],
 [11, 4],
 [3, 1],
 [17, 10],
 [8, 5],
 [3, 15],
 [13, 9],
 [8, 12],
 [11, 12],
 [20, 5]]

sorted data
[[3, 1],
 [3, 15],
 [6, 10],
 [8, 5],
 [8, 12],
 [11, 4],
 [11, 12],
 [13, 9],
 [17, 10],
 [20, 5]]

 参考:
【Python】ソート - Qiita

 要するにkeyをこのような形式にして、各子リストの要素を桁のようにみなしてソートできる訳だ(いまいちうまく表現できないけど)。

 降順にしたければ、reversed=Trueを指定するだけ。

import random
from pprint import pprint

data = [[random.randint(0, 20), 
         random.randint(0, 20)]
        for _ in range(10)]

print("data")
pprint(data)

print("\nsorted data")
pprint(sorted(data, key=lambda x:(x[0], x[1]), reverse=True))

 結果

data
[[4, 19],
 [3, 8],
 [15, 9],
 [13, 9],
 [7, 10],
 [8, 12],
 [20, 2],
 [19, 17],
 [7, 11],
 [5, 11]]

sorted data
[[20, 2],
 [19, 17],
 [15, 9],
 [13, 9],
 [8, 12],
 [7, 11],
 [7, 10],
 [5, 11],
 [4, 19],
 [3, 8]]


 では、最初の要素は昇順、次の要素は降順にしたいときは、どうしたら良いのでしょう。

 reverse引数はbooleanのみ受け付ける。tupleが渡せたら良かったのだけど、渡せないので仕方ない。

 しかし、そんなに心配する必要はない。

ソート HOW TO — Python 3.6.5 ドキュメント

 によると、pythonのソートは安定ソートである。このことを利用して、こう書ける。

import random
from pprint import pprint

data = [[random.randint(0, 20), 
         random.randint(0, 20)]
        for _ in range(10)]

print("data")
pprint(data)

print("\nsorted data")
tmp = sorted(data, key=lambda x:x[1], reverse=True)
pprint(sorted(tmp, key=lambda x:x[0]))

 結果

data
[[0, 12],
 [13, 1],
 [15, 6],
 [14, 4],
 [11, 1],
 [7, 2],
 [17, 3],
 [13, 8],
 [0, 19],
 [19, 6]]

sorted data
[[0, 19],
 [0, 12],
 [7, 2],
 [11, 1],
 [13, 8],
 [13, 1],
 [14, 4],
 [15, 6],
 [17, 3],
 [19, 6]]
[0, 19],
 [0, 12],

 とか
>||
[13, 8],
[13, 1],
|

【python】TF-IDFで重要語を抽出してみる

概要

 すでに語り尽くされた感のあるネタですが、TF-IDFで文書の重要な単語(重要語、あるいは特徴語)を抽出してみます。

 numpyとsklearnを使うと、10行程度のコードで実現できるので簡単です。

スポンサーリンク



コードの書き方

 とりあえず、対象データとしては20newsgroupsを使います。関数一つで読み込めて便利だからです。

 sklearn.datasets.fetch_20newsgroups — scikit-learn 0.20.1 documentation

 自然言語処理の技術紹介などの記事で、Webスクレイピングなどをしてデータを作っているケースをよく見かけますが、こちらの方が手間がかからなくて、再現性も高いです*1。使えるデータは使いましょう。

 関連記事:【python】sklearnのfetch_20newsgroupsで文書分類を試す(1) - 静かなる名辞

 あとはTfidfVectorizerに入れて、いきなりTF-IDFのベクトルに変換します。

 sklearn.feature_extraction.text.TfidfVectorizer — scikit-learn 0.20.1 documentation

 詳しい使い方は、ドキュメントやCountVectorizerの記事を読んでいただければ良いです(CountVectorizerと使い方はほぼ同じ)。

 使い方のコツとして

  • min_dfオプションを適当に指定してゴミ単語を削った方が良いこと
  • 基本的にtransformした返り値がsparse matrix型なのでtoarray()メソッドで密行列に変換して取り扱ってやる必要があること

 が挙げられます。それ以外は、とりあえず使うだけならそれほど気は配らなくても良いはず。

 ここまでの記述をコードにすると、こんな感じです。

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.datasets import fetch_20newsgroups

news20 = fetch_20newsgroups()
vectorizer = TfidfVectorizer(min_df=0.03)
tfidf_X = vectorizer.fit_transform(news20.data[:1000]).toarray()  # ぜんぶで1万データくらいあるけど、そんなに要らないので1000件取っている

 ここからどうするんじゃい、ということですが、スマートに書くためには、ちょっとしたnumpy芸が要求されます。

index = tfidf_X.argsort(axis=1)[:,::-1]

 tfidf_X.argsort(axis=1)でソートした結果のindexを返します。[:,::-1]はreverseです。これによって、各文書のTF-IDF値にもとづいて降順ソートされたindexが得られます。

 次に、このindexに基づいて単語を復元することを考えます。TfidfVectorizer.get_feature_names()で、特徴抽出時に使ったindexの順に並んだ単語のリストが得られるのですが*2、リストだとnumpy芸が使えないのでnumpy配列にしておきます。あとは、一気に変換します。

feature_names = np.array(vectorizer.get_feature_names())
feature_words = feature_names[index]

 numpyのこの機能を使っているコードはあまり見かけないのですが、実は

>>> import numpy as np
>>> a = np.array(["hoge","fuga","piyo"])
>>> b = np.array([[0,0,0],[2,1,0],[0,2,0]])
>>> a[b]
array([['hoge', 'hoge', 'hoge'],
       ['piyo', 'fuga', 'hoge'],
       ['hoge', 'piyo', 'hoge']], dtype='<U4')

 こういう仕様になっておりまして、意図した通りの変換が一発でできています。知らないと戸惑いますね。

 あとは配列から適当に取り出せばオッケーです。各文書ベクトル(というか単語の順列)の先頭n次元を取ると、それがそのままn個目までの重要語になっています。

やってみた

 コード全文を以下に示します。

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.datasets import fetch_20newsgroups

news20 = fetch_20newsgroups()
vectorizer = TfidfVectorizer(min_df=0.03)
tfidf_X = vectorizer.fit_transform(news20.data[:1000]).toarray()

index = tfidf_X.argsort(axis=1)[:,::-1]
feature_names = np.array(vectorizer.get_feature_names())
feature_words = feature_names[index]

n = 5  # top何単語取るか
m = 15  # 何記事サンプルとして抽出するか
for fwords, target in zip(feature_words[:m,:n], news20.target):
    # 各文書ごとにtarget(ラベル)とtop nの重要語を表示
    print(news20.target_names[target])
    print(fwords)

 結果は、

rec.autos
['car' 'was' 'this' 'the' 'where']
comp.sys.mac.hardware
['washington' 'add' 'guy' 'speed' 'call']
comp.sys.mac.hardware
['the' 'display' 'anybody' 'heard' 'disk']
comp.graphics
['division' 'chip' 'systems' 'computer' 'four']
sci.space
['error' 'known' 'tom' 'memory' 'the']
talk.politics.guns
['of' 'the' 'com' 'to' 'says']
sci.med
['thanks' 'couldn' 'instead' 'file' 'everyone']
comp.sys.ibm.pc.hardware
['chip' 'is' 'fast' 'ibm' 'bit']
comp.os.ms-windows.misc
['win' 'help' 'please' 'appreciated' 'figure']
comp.sys.mac.hardware
['the' 'file' 'lost' 've' 'it']
rec.motorcycles
['00' 'org' 'the' 'out' 'and']
talk.religion.misc
['the' 'that' 'may' 'to' 'is']
comp.sys.mac.hardware
['hp' 'co' 'com' 'tin' 'newsreader']
sci.space
['the' 'power' 'and' 'space' 'nasa']
misc.forsale
['10' 'very' 'and' 'reasonable' 'sale']

 まあ、それなりにうまくいってるんじゃね? という結果が得られました*3

 車のカテゴリやコンピュータのカテゴリ、宇宙のカテゴリなんかは割とわかりやすいですが、talk.religion.misc(宗教に関する話題?)だと['the' 'that' 'may' 'to' 'is']になっていたりするのは面白いです。この文書だけがたまたまとても抽象的だったのか、このカテゴリ自体こんな感じなのかはよくわかりません。

 ということで、文書ごとにやってうまく結果が出るのはわかったので、次は各カテゴリ(ラベル)ごとに特徴的な単語を出してみようと思ったのですが、これはちょっとめんどいのでとりあえずパス。そのうち気が向いたら追記します。

まとめ

 特徴抽出とTF-IDFの計算を自分で書いて、重要語への変換も自分で書いてという感じでやるとかなり手間がかかるのですが、sklearnとnumpyのちからに頼ると簡潔に書けて嬉しいですね。

 TF-IDFの上位数件くらいは、それなりに文書の特徴を反映するような単語と言って良いと思うので、ざっくり内容を把握したいとか、ざっくり特徴抽出したいというときはこういう方法も良いと思います。

*1:sklearnが仕様変更しない限り再現できる

*2:つまりindexと特徴ベクトルの次元が対応

*3:それなりに「まとも」な結果になっているのはTfidfVectorizerのオプションでmin_df=0.03を指定しているからで、これをやらないと見事にdfが低すぎるゴミ単語ばっかり引っかかる結果になる。注意しましょう

【python】io.StringIOは便利なので使いこなそう

はじめに

 io.StringIOというものがあります。標準モジュールのioに属します。

io --- ストリームを扱うコアツール — Python 3.7.1 ドキュメント

 これがどう便利かというと、「ファイルオブジェクトのように見えるオブジェクト」を作れます。

スポンサーリンク



読み込みに使ってみる

 こんな感じですかね。

import io

txt = """hoge
fuga
piyo
"""

f = io.StringIO(txt)
for line in f:
    print(line, end="")

""" =>
hoge
fuga
piyo
"""

 何が嬉しいかって? たとえば、これを見てください。

import io
import pandas as pd

txt = """
number,name,score
1,hoge,100
2,fuga,200
3,piyo,300
"""

df = pd.read_csv(io.StringIO(txt), index_col="number")
print(df)
""" =>
        name  score
number             
1       hoge    100
2       fuga    200
3       piyo    300
"""

 CSVの文字列を読み込むのに使っています。

 pandas.read_csvの第一引数はファイルパスかストリームしか受け付けませんが、io.StringIOをかましてやることでstrを渡せている訳です。

pandas.read_csv — pandas 0.23.4 documentation

 「ファイルを作るまでもない、作るのが面倒くさい」ような軽い確認やデバッグ作業に重宝します。

書き込んでみる

 io.StringIOが読み込みモードのファイルオブジェクトの代用品として使えることはわかりました。では、書き込みはどうでしょう?

import io
f = io.StringIO()
f.write("hoge\n")
f.close()

 このコードはエラーなく終了しますが、特に副作用は得られません。

 中身を見たければ、こうします。

import io
f = io.StringIO()
f.write("hoge\n")
print(f.getvalue())  # => hoge
f.close()

 StringIO.getvalue()はバッファに書き込まれた内容をすべて吐き出します。closeすると呼べなくなるので注意が必要です。

 こちらもpandasと組み合わせて使ってみます。

import io
import pandas as pd

txt = """
number,name,score
1,hoge,100
2,fuga,200
3,piyo,300
"""

df = pd.read_csv(io.StringIO(txt), index_col="number")

with io.StringIO() as f:
    f.write(df.to_csv())
    print(f.getvalue())

""" =>
number,name,score
1,hoge,100
2,fuga,200
3,piyo,300
"""

 使えているようです。

仲間たち

 ドキュメントを見るとbytes版のBytesIOもあることがわかる。他にも色々あるようだが、あまり目を通していないのでコメントしない。

io --- ストリームを扱うコアツール — Python 3.7.1 ドキュメント

まとめ

 テキストファイルを相手にするとき、データが小さければソースコードに埋め込めます。そうするとソースコードだけで再現性を確保できて、けっこう良いです。

オブジェクト指向の教育にPythonが向いていると思うこれだけの理由

はじめに

 オブジェクト指向は今となっては常識である。

 常識であるがゆえに、いかに初心者にわかりやすく教えるかが課題になる。

 世の中でオブジェクト指向の「教材」として使われている言語は、

  • Java
  • Ruby

 の二択くらいだと思う。が、あえて僕はPythonを推してみるよ、という記事。ぶっちゃけポエム。

 内容は、Javaでオブジェクト指向を理解するのはしんどいし、RubyとPythonだと僅差でPythonが勝つんじゃないかなぁ、という主張。以下で理由を書いていくよ。

 目次

スポンサーリンク



Pythonが向いていると思う理由

理由1:すべてがオブジェクト

 すべてがオブジェクト。これは重要なことである。

 オブジェクトとオブジェクト以外で異なる扱いをしなければならない、プリミティブ型のある言語でオブジェクト指向の教育をするなんて、正気の沙汰ではない。初心者はintと配列ばっかり使う訳だしさ。

 なので、Javaはぶっちゃけ論外だと思う。Ruby使いの人は「ならPythonとRubyは互角だ」と言いたくなるかもしれないけど、Rubyには関数が第一級オブジェクトではないという弱点があり、「すべてがオブジェクト」ははっきり言って誇大広告である。

 関数オブジェクトが自然に使えないと困るのかって? まあ、そんなに困らないかもしれない。でも「関数オブジェクト」は理解しておいた方が、オブジェクト指向がよくわかるようになると思わない?(上級者向けすぎるか)

理由2:仕様がスリムで綺麗。書きやすい

 Javaにはまず、publicやらprivateやらある。interfaceというわかりづらい概念もある。静的型付けなのも相まって、メソッドの宣言なんかカオス。public static void mainはどう考えても初心者向けではない*1

 Rubyは、なんかブロックとかいうよくわからないものがあるね。あと、メソッド呼び出しのカッコを省略できるとか。教育上あんまりよくないと思います。

 Pythonにはそういう問題はない。

理由3:self

 Pythonのselfはよく批判されるけど、なんだかんだでわかりやすい。

a.hoge("fuga")

 は

type(a).hoge(a, "fuga")

 と実質的に等価というルールがあることを理解すれば、後は不自然な点はなにもない。インスタンスの外側ではaとして見えているものは、内側ではselfとして見えていると考えれば良いということで、自然な発想でコードを書いていくことができる。

 selfを省略する言語はこれがないので、クラス変数とインスタンス変数の区別を付けるだけでも一苦労だし、ローカル変数まで混ざってくると本格的に訳がわからなくなる(から、EclipseでJavaを書くとぜんぶ違う色で見せてくれる)。

理由4:普通にプログラミングしているだけでオブジェクト指向への理解が深まる

 Python初心者はlistをよく使う。そうするとappendやextendが出てくる。これはもうメソッドだ。

 関数の引数に渡したリストにappendするとリストの中身が変わるけど、intだと足し算しても変わらない。mutableなオブジェクトとimmutableなオブジェクトの違いを理解するだろうし、オブジェクトは変数に束縛されているだけというオブジェクト指向の基本的なモデル*2への理解も深まる。

 list.sort()とsorted(list)の違いを理解すれば、破壊的なメソッドには注意しないといけないこともわかる。

 だから、「オブジェクト指向の勉強のためにJavaを半年学んだ人」と「ただ単にプログラミングの勉強として半年Pythonを学んだ人」だと、オブジェクト指向に対する理解度は同程度か、ヘタしたら後者のほうが勝るくらいになっているかもしれない。

 まあ、これに関してはたぶんRubyも互角。

理由5:書いてて楽しい

 タイプ数も少ないし、なんかPythonはパズルみたいな技巧的な面があるし、書いてて楽しい。

向いていない理由も一応書く

 ダメな理由もなんか色々あるといえばあるような気もする。

罠が多い

 Pythonはシンプルなクセに罠が多い言語だと思う(「文字コード」とか「test.py作っただけでまともに動かない」とか「IndentationError」とか)。

 エラーメッセージも、わからないときはとことんわからないのが出るし。

 初心者のうちは、よくわからないところで詰む。そして初心者は自己解決できない。

 罠は回避するように教育していくとかで軽減は可能。ただ、本質的じゃない問題で初心者を悩ませるのもなんだかなぁという気がする。

そういうコンセプトの解説記事とかが少ない。あっても古い

 困るよね。

しょせんスクリプト言語

 カプセル化はないし、ポリモーフィズムの実現方法もすごく簡単(メソッド作るだけ!)。ので、Pythonやってから他の言語のオブジェクト指向を理解しようとすると、追加の学習コストがかかる。

独自の風習

 避けては通れないけど避けなかったところで得るものの少ないデコレータとか、

 初心者を惑わす内包表記とかジェネレータとか、

 やればやるほど実感するtuple周りのキモさとか*3

 これどうなんだ、と思う側面はたくさんあります。

windowsとそんなに相性がよくない

 最近はマシにはなりましたが、まだまだハマると思います。linux環境でやれとなるといきなり敷居が高くなります。

でもまあ、

 なんだかんだで学習コストの低さ、とっつきやすさでは、総合的には初心者向けのわかりやすい言語と言っても別に構わないくらいだとは思うよ(震え声)。

まとめ

 Pythonのオブジェクト指向はわかりやすいよね、最初からこれで教えてもらえたらなぁ。Python良いよね! って気持ちの記事です。

 特に内容に責任は持たないが、意見等はご自由にどうぞ。

*1:「そんなのIDEが補完してくれるから良いんだよ!」という意見が当然あると思うが、それをやると馬鹿の一つ覚えのようにIDEの補完と修正任せでコードを書こうとする奴が続出するのでダメだ

*2:異論はあるだろうけど

*3:tupleは丸括弧によって作られるのではない、カンマによって作られるのである

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

はじめに

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

  • 値渡し
  • 参照渡し

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

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

 このページを見るとわかりやすい。

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

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

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;
}

 ちなみに、こっちの「参照渡し」は歴史が古く、少なくともFORTRANからあるっぽい。というか、FORTRANはデフォルトですべて参照渡し。

subroutine f(x)
  integer x
  x = 42
end subroutine

program main
  integer a
  a = 3
  print *, a
  call f(a)
  print *, a
end program

!           3
!          42

 C言語とかに慣れた目には奇異に映るけど、よくよく考えてみるとメモリ番地だけ渡せばいいので効率的だし(まあ実際にどういう実装なのかまでは確認していないけど)、多少注意していればプログラムも書きやすいので、これはこれで合理的だと思う。

 話が逸れた。参照の値渡しでは、こういう「参照渡し」チックな動作はできない。pythonの例。

def swap(a, b):
    a, b = b, a

a = 10
b = 20
swap(a, b)

 pythonの変数はすべてJavaなどでいうところの参照型ではあるのだけど、swapの中のa,bは単なるswap関数のローカル変数であって、呼び出し元のa, bの参照するものを書き換えたりはしない。受け取っているのは「参照の値」であって、「参照の値を格納している変数への参照」ではない。

 参照型の「真の」参照渡しについては、以下の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';
  char *xptr, *yptr;
  xptr = &x;
  yptr = &y;
  swap(&xptr, &yptr);
  printf("%c\n", *xptr);
  printf("%c\n", *yptr);
  return 0;
}

 xptr, yptrのアドレスを渡す訳ですね。pythonではこれはできない(というか、そもそも変数の概念そのものが違うけど)。で、こういうものを称して参照の値渡し、とする。

共有渡しについて考える

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

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

引数 - Wikipedia
Evaluation strategy - Wikipedia

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

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

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

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

 これに関連して、こんな議論もある。comp.lang.python *2の昔の議論らしい。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は値渡し!」と主張したがるのは、つまるところそういうことだろう*3

 逆にpythonやrubyのような「すべてがオブジェクト」な言語では、プリミティブ型やら何やらのことは考える必要はなく、また言語仕様の表面で参照の値(要するにアドレス)が見えてくる訳でもないので、『共有渡し』の方がすっきりすると思う。

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

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

 まあ、個人的には『共有渡し』の方がスッキリするし、好きです。でも、『参照の値渡し』が絶対に駄目かというと・・・難しいです。呼び方の問題は厄介。

まとめ

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

続き

 某所でこの問題が再燃していたので、続きを書いた。

www.haya-programming.com

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

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

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

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

【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の実装の問題である。

 pythonは参照カウントGCを持つオブジェクト指向言語である。どんなオブジェクトであろうと生成にはインスタンス生成分のコストがかかるし、参照されなくなったオブジェクトはGCが連れて行く。

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

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

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

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

GridSearchCV『の』パラメータ・チューニング 高速化中心に

はじめに

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

sklearn.model_selection.GridSearchCV — scikit-learn 0.21.2 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に変換できる辞書として扱えることも利用しています。

www.haya-programming.com

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

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

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

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

 さて、私の環境(しょぼいノートパソコン)ではこのプログラムの実行には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分以内には処理できていますが、ちょっと重いモデルにちょっと多量のデータを突っ込んだりすると、もうダメです。何十分とか何時間もかかってしまいます。

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

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

 粗いグリッドである程度チューニングしてから、RandomizedSearchCVを使うというのもいい手だと思います。

www.haya-programming.com

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

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

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

はじめに

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

sklearn.feature_selection.VarianceThreshold — scikit-learn 0.20.2 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 0.20から変更された結果、この記事の内容はもはやあまり役に立たなくなっています。

 この記事は記録として残しますが、最新の仕様については下リンクの記事を御覧ください。

www.haya-programming.com

 以下のこの記事の記述は古い仕様に基づいており、また内容的にも若干不完全な部分があります。お読みになる方は、その点についてご承知くださいますようお願いします。

 2018年12月2日


スポンサーリンク



はじめに

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

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

 こんなとき使えます。

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

LabelEncoderの使い方

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

sklearn.preprocessing.LabelEncoder — scikit-learn 0.20.2 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.20.2 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月現在)。

http://scikit-learn.org/dev/modules/generated/sklearn.preprocessing.CategoricalEncoder.html

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

 2018年11月15日追記:sklearn 0.20はリリースされましたが、CategoricalEncoderはなくなっちゃったみたいです・・・。残念。