読者です 読者をやめる 読者になる 読者になる

静かなる名辞

pythonと読書

【python】sklearnの次元削減クラスSelectPercentileとSelectKBestについて

 ネットの巷には「sklearnで次元削減するのでとりあえずPCA使ってみました」という記事が溢れかえっているのだが、PCAは基本的に線形な世界でしか動かないので、扱うデータによっては「PCAかけてから分類器に入れたら精度がガタっと落ちました」みたいなことが起こり得る。こういう問題に対してはカーネルPCAという手法があり、SVMよろしくカーネル法を使って上手いこと計算してくれるらしい。また、非線形データをなんとかするアルゴリズムは他にもあるようだ。だけど、そんなややこしいもの使いたくないよね。よくわからないし。
 そんな駄目人間でも使える次元削減アルゴリズム(厳密には特徴選択と言うべきなんだけど)が、sklearnの提供するSelectPercentileとSelectKBestである。
 両者は共に、「次元ごとの効き目(分類するときどれくらい有益な情報を持っているか)」を算出し、効き目の高い次元を残すという動作をする。PCAみたいに次元を混ぜ合わせたりは特にしない(要するに分類に効く情報が入った次元とノイズしかない次元があるとき、前者だけを取り出す)。あと、基本的には是が非でも分類に使える情報を取ってくる奴なんで、教師なしでデータをクラスタリングして可視化したいみたいな状況で使うのは考え直した方が良いと思う。この辺の細かいところは公式ドキュメントを読んでください。

 使い方は超簡単。
 まずSelectPercentileの場合。

from sklearn.feature_selection import SelectPercentile

...

sp = SelectPercentile(k=50) #こう書くと次元数が半分になる
vectors = sp.fit_transform(vectors, labels) 

 次にSelectKBest。こっちも大して変わらない。

from sklearn.feature_selection import SelectKBest

...

skb = SelectKBest(k=250) #こう書くと上位250次元を取ってくる
vectors = skb.fit_transform(vectors, labels)

 特に頭を使う要素がなくて助かってます。

 ただ、実際にこのまま使うことはちょっとできない。この手法は正解ラベルを必要とする。要するに、データをテストデータと訓練データに分けてクロスバリデーションやるんだとか言ってるときに、特徴抽出の段階でデータ全体の正解ラベルを持ってきて貼り付けて次元削減に使っちゃうと、だいぶセコいんじゃね? と思うのである。

 なのでこういうときは、こういうもの(↓)を書いて、

class SelectClassifier:
    """
    欲しいメソッドとかあったら自分で追加してね!
    """
    def __init__(self, clf, selector="p", k=10):
        self.clf = clf
        if selector == "p":
            self.selector = SelectPercentile(k=k)
        elif selector == "kb":
            self.selector = SelectKBest(k=k)
        else:
            sys.exit("aho")
        self.k = k

    def fit(self, vectors, labels):
        if vectors.shape[1] > self.k:
            self.selector.fit(vectors, labels)
            vectors = self.selector.transform(vectors)
        self.clf.fit(vectors, labels)

    def predict(self, vectors):
        if vectors.shape[1] > self.k:
            vectors = self.selector.transform(vectors)
        return self.clf.predict(vectors)

 そしてこうやって使ってやる。

clf = SVC() #たとえば
clf = SelectClassifier(clf, "p", k=10)

 訓練データの正解ラベルを使う分には、文句を言われる筋合いはないはず。
 え、なんでわざわざベクトルの次元数がkの値より大きいことを確認する処理なんか書くのって? SelectKBestは指定したkより小さい次元数のベクトルを渡すと例外吐いて止まるんである。そんなの素通りさせてくれれば良いでしょ、と100回思ったけど、どうにもならないので愚直に書いた。この処理だとSelectPercentileで100次元以下のデータを扱うとき、次元数とkの値の兼ね合いで次元削減が実行されない可能性がある。けど、それまで真面目に対応する気力はなかった。

 これの使い所としては、たとえば「ン万次元あるけど、これ大体ノイズだよね」みたいなときに使えば良いでしょう。自然言語処理のBoWなんかを相手にするときは良さそうですね。PCAみたいに軸を混ぜ合わせる訳ではないので、「高次元のデータを低次元で上手く表現する」用途には使えないです。 

参考文献

sklearn.feature_selection.SelectPercentile — scikit-learn 0.18.1 documentation
sklearn.feature_selection.SelectKBest — scikit-learn 0.18.1 documentation
 具体的にどういう処理をして有用な次元/そうでない次元を決めているのかという話も載っています。

【python】 immutableを参照渡ししたい

 pythonの参照渡しスタイル(を積極的に利用するコーディング)にケチ付ける記事を書いたんだけど、しばらく経ってから「逆にpythonでCみたいなバリバリの参照渡しするにはどうしたら良いんだ?」という疑問を持った。
 たとえば、Cで言うところのこういうもの。

#include<stdio.h>

void time_ptr(int *ptr, int n){
  *ptr = n*(*ptr);
}

int main(){
  int a;
  a = 3;

  printf("%d\n", a);
  time_ptr(&a, 3);
  printf("%d\n", a);

  return 0;
}

 有名な話だけど、pythonの世界にはmutable(変更可能)なオブジェクトとimmutable(変更不能)なオブジェクトがあり、前者にはリストや辞書、後者にはタプルや文字列、整数などが含まれる。mutableなオブジェクトに対する操作(list.append()とか)は元のオブジェクトを変化させるが、immutableなオブジェクトに対する操作は常に新しいオブジェクトを生成する。そしてpythonのルールとして、すべてのオブジェクトは参照渡しされる。
 こうして考えると、mutableなオブジェクトを参照渡しにするのは難しくない(むしろ容易だ。意図せず参照先をいじってしまい、ロジックエラーになって困るくらい)。一方、immutableなオブジェクトを参照渡しして、その値を書き換えたい(そんなシチュエーションがあるの? という疑問は黙殺するとして)場合、かなり困ったことになる。当たり前だ。なにせ、変更不能なのだから。
 この問題(というほどじゃないけど)の解決策は、以下の2通り考えられる。

  1. 任意のオブジェクトidを指定して新規オブジェクトを生成する
  2. mutableの中に入れて渡してやる

 ほんとうは1が実現できたら、とてもロックで面白いと思ったけど、残念ながらpythonで実現する方法は調べても出てこなかった(恐らくそんなものは存在しないと思う。あったら危なっかしくてしょうがないし)。なので素直に2を使うことにする。
 上のCのコードは、こう書ける。

def time_ref(ptr, n):
    ptr[0] *= n

def main():
    a = [3]
    print(a[0])
    time_ref(a, 3)
    print(a[0])

if __name__ == '__main__':
    main()

 使い道は特に思いつかないけど、参照渡しにするためだけにリストオブジェクトを生成するのも不条理感があって中々良いかもしれないと思えるようにはなった。

【python】メモリ上のオブジェクトを是が非でも圧縮したい

 でかいデータをなにも考えずメモリ上に置いておくと、あっという間にメモリが埋まる。不要なデータはこまめに消して、必要なときに必要なものだけメモリに置くようにすれば大抵なんとかなるのだけど、そうやって整理していくと、ある水準を超えたところで処理時間とかコードの可読性が問題になってしまうときもある。
 こういう問題の解決策として、データを生成したあとは圧縮してメモリ上に置いておき、使うときに解凍して呼び出すという方法が考えられる。もちろん普通にできることではないので、pickleを使う。

 こういうのは説明するよりソースを見せた方が早い。

import pickle
import bz2
PROTOCOL = pickle.HIGHEST_PROTOCOL
class ZpkObj:
    def __init__(self, obj):
        self.zpk_object = bz2.compress(pickle.dumps(obj, PROTOCOL), 9)        

    def load(self):
        return pickle.loads(bz2.decompress(self.zpk_object))

 こういうファイルを作ってパスの通ったとこに置いておく。pickle化・圧縮のプロトコルは気分で変える。
 使い方も特に難しくはない。

from zpkobj import ZpkObj #zpkobjというファイル名にした場合
...
obj = ZpkObj(obj) #圧縮するとき
...
obj = obj.load() #解凍するとき

 留意点としては、オリジナルのオブジェクトの参照がどこかに残っているとGCが動かないので期待したようなメモリ節約効果が得られないことが挙げられる。そういうときは手動で消す。

...
zobj1 = ZpkObj(obj1)
del obj1 #違う名前に代入するなら必須

 せっかくなので、memory_profilerでベンチマークを取ってみる。

mtest.py(ベンチマーク用コード)

# coding: UTF-8
from zpkobj import ZpkObj

@profile
def main():
    lst = list(range(10000000))
    zlst = ZpkObj(lst)
    del lst
    lst = zlst.load()
    del zlst

if __name__ == '__main__':
    main()

結果

$ python -m memory_profiler mtest.py
Filename: mtest.py

Line #    Mem usage    Increment   Line Contents
================================================
     4   28.066 MiB    0.000 MiB   @profile
     5                             def main():
     6  415.867 MiB  387.801 MiB       lst = list(range(10000000))
     7  422.105 MiB    6.238 MiB       zlst = ZpkObj(lst)
     8   34.645 MiB -387.461 MiB       del lst
     9  428.691 MiB  394.047 MiB       lst = zlst.load()
    10  423.742 MiB   -4.949 MiB       del zlst

 7~8行目を見ればわかるように、意図した通りオブジェクトのサイズを1/50以下に圧縮できている(圧縮の効きやすさはデータ依存なんで、すべてのオブジェクトのかさを1/50以下にできる訳ではないけど)。
 ところで、6行目と10行目で若干メモリ消費量が増えてるのはなんで? memory_profilerの使用メモリが載っちゃってるか、それとも一千万個も生成した整数オブジェクトをぜんぶ回収しないうちに次の処理が進んでるんだろうか。そこに関しては詳しくないので謎。

【python】random.shuffleについて

問題のコード

 こういう奴。

import random
lst = list(range(10))
shuffled = random.shuffle(lst)
for x in shuffled:
    print(x)

 いかにも人畜無害な雰囲気のコードですが、実行すると「TypeError: 'NoneType' object is not iterable」を吐いてくれます。

正しいやり方
import random
lst = list(range(10))
random.shuffle(lst)
for x in lst:
    print(x)
"""
=>
1
9
5
3
7
0
4
2
8
6
"""

 random.shuffleはいわゆる「破壊的操作」で、リストの参照を受け取って(pythonのリストは参照渡しだからね)、その参照先をシャッフルします。Cかよ。
 ともかく、こいつは関数じゃないのです。pythonがうたう『マルチパラダイム言語』の中にはオールドファッションな手続き型も入ってると思うんで、こういうものが標準ライブラリの中にいても怒ってはいけません。僕は思わず画面に湯呑みを投げつけそうになりました。

でも元のリスト破壊されたら困るときもあるじゃん

 random.shuffleは参照先をそのままいじるので、元々どんな順番で要素が並んでいたのかという情報は残してくれません。元のリストもとっておきたいときは自前でやる必要があります。
 色々なやり方があると思いますが、とりあえずlist()しとけば別オブジェクトになるのでそれで良いんじゃないかな。

import random
lst = list(range(10))
shuffled = list(lst); random.shuffle(shuffled)
for x, y in zip(lst, shuffled):
    print(x, y)
"""
=>
0 0
1 3
2 8
3 6
4 4
5 9
6 1
7 7
8 5
9 2

"""

 ひとつづきの処理であることを強調するために、セミコロン使って同じ行に入れてみました。ちなみに、random.shuffle(list(lst))とかすると、random.shuffleさんはちゃんと健気に動いてくれるものの、結果を取り出す手段がなくなって死にます(オブジェクトが)。

【python】混同行列(Confusion matrix)をヒートマップにして描画

 pythonでラクして混同行列を描画したい(sklearnとかpandasとかseabornとか使って)という話。

 そもそもscikit-learnにはsklearn.metrics.confusion_matrixなるメソッドがあって、混同行列がほしいときはこれ使えば解決じゃん、と思う訳だが、このconfusion_matrixは2次元のnumpy配列を返すだけで「あとはユーザーが自分で描画してね♪」というメソッド。なので、とりあえずコンソールに結果を吐かせて、混同行列(の値が入った2次元配列)を確認したあと、ちょっとどう料理してやるか悩む羽目になる。

 表形式で出すのはダサいし見づらいので、ヒートマップにしようというところまではそんなに迷わないと思う。で、pythonのヒートマップの作り方はぶっちゃけよくわからない(日本語資料があまりない)。とりあえずseabornというライブラリを使えば良いらしいんだけど……。

 
 日本語で「python 混同行列 ヒートマップ」みたいな検索をすると、ぶっちゃけ楽そうな(5,6行くらいで書ける)方法がほとんど出てこない(皆無?)のだけど、Stack Overflowにはあった。

python - How can I plot a confusion matrix? - Stack Overflow


 とりあえずやり方はわかったので、使いやすいように正解ラベルと予測ラベルを受け取って描画する関数をメモしておく(骨子だけ)。

import pandas as pd
import seaborn as sn
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt

def print_cmx(y_true, y_pred):
    labels = sorted(list(set(y_true)))
    cmx_data = confusion_matrix(y_true, y_pred, labels=labels)
    
    df_cmx = pd.DataFrame(cmx_data, index=labels, columns=labels)

    plt.figure(figsize = (10,7))
    sn.heatmap(df_cmx, annot=True)
    plt.show()

 これをたとえば、

print_cmx(["a","b","c","d","e"],["a","b","c","d","e"])

 こうやって呼び出すと、
f:id:hayataka2049:20161215224000p:plain
 こんな結果が表示される。あとはmatplotlibなんで、好きにいじれば(文字大きくするとかタイトル/軸ラベル付けるとか)良いと思う。

confusion_matrixを呼ぶとき、明示的にラベルを渡して(sklearnのドキュメントにもなんか正解ラベル+予測ラベルから重複取り除いてソートして使うみたいに書いてあるから、この場合は不要かもしれないけど。自分好みの順番で出力したいときは必要)、そのラベルをpandasデータフレームに渡すindex,columnsに使いまわすのがミソ。