静かなる名辞

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


【python】numpyの型の違いによる計算速度差を見てみる

はじめに

 前回の記事で「なんとなくnp.float32が速い気がする」とか書いたので、実際に測ってみる。

 予め断っておくと、計算速度なんて環境によって違うし、どの型が速いかもCPUのアーキテクチャに依存する。numpyはバリバリにSIMD命令を使って最適化する(と、思う)ので、演算の種類とかによっても優劣は変わる。あくまでも私の環境での試験である。

つかったCPU

  • CPU名:Core i7-3540M

 4年くらい前のモバイル版i7。モバイル版なのが泣ける。物理コアが2コアしかない時点でお察しである。とはいえ、個々のコアの性能は腐ってもi7

  • クロック周波数:3GHz(たーぼ・ぶーすとなる技術によって負荷をかけると3.5GHzくらいまで引っ張ってくれる) 
  • コア数:物理コア*2(HTがあるので、論理コア*4)
  • L1キャッシュ:128KB
  • L2キャッシュ:512KB
  • L3キャッシュ:4.0MB

ソースコード

# coding: UTF-8
import warnings;warnings.filterwarnings('ignore')

import time
from random import randint

import numpy as np

def make_random(l, n):
    return [randint(1,n) for _ in range(l)]

def f(a,b,c):
    return a * b - c

def g(a,b,c):
    return a/b/c

def main():
    for vec_size in [100, 1000, 10000, 100000, 1000000]:
        af64 = np.array(make_random(vec_size, 10000), dtype=np.float64)
        bf64 = np.array(make_random(vec_size, 10000), dtype=np.float64) 
        cf64 = np.array(make_random(vec_size, 10000), dtype=np.float64)

        ai64 = np.array(make_random(vec_size, 10000), dtype=np.int64)
        bi64 = np.array(make_random(vec_size, 10000), dtype=np.int64) 
        ci64 = np.array(make_random(vec_size, 10000), dtype=np.int64)
    
        af32 = np.array(af64, dtype=np.float32)
        bf32 = np.array(bf64, dtype=np.float32)
        cf32 = np.array(cf64, dtype=np.float32)

        ai32 = np.array(ai64, dtype=np.int32)
        bi32 = np.array(bi64, dtype=np.int32)
        ci32 = np.array(ci64, dtype=np.int32)

        af16 = np.array(af64, dtype=np.float16)
        bf16 = np.array(bf64, dtype=np.float16)
        cf16 = np.array(cf64, dtype=np.float16)

        ai16 = np.array(ai64, dtype=np.int16)
        bi16 = np.array(bi64, dtype=np.int16)
        ci16 = np.array(ci64, dtype=np.int16)


        print("vec size:",vec_size)
        start1 = time.time()
        f(af64,bf64,cf64)
        end1 = time.time()
        start2 = time.time()
        g(af64,bf64,cf64)
        end2 = time.time()
        print("float64: {0:.6f} {1:.6f}".format(end1-start1, end2-start2))
        start1 = time.time()
        f(ai64,bi64,ci64)
        end1 = time.time()
        start2 = time.time()
        g(ai64,bi64,ci64)
        end2 = time.time()
        print("int64:   {0:.6f} {1:.6f}".format(end1-start1,end2-start2))
        start1 = time.time()
        f(af32,bf32,cf32)
        end1 = time.time()
        start2 = time.time()
        g(af32,bf32,cf32)
        end2 = time.time()
        print("float32: {0:.6f} {1:.6f}".format(end1-start1, end2-start2))
        start1 = time.time()
        f(ai32,bi32,ci32)
        end1 = time.time()
        start2 = time.time()
        g(ai32,bi32,ci32)
        end2 = time.time()
        print("int32:   {0:.6f} {1:.6f}".format(end1-start1,end2-start2))
        start1 = time.time()
        f(af16,bf16,cf16)
        end1 = time.time()
        start2 = time.time()
        g(af16,bf16,cf16)
        end2 = time.time()
        print("float16: {0:.6f} {1:.6f}".format(end1-start1, end2-start2))
        start1 = time.time()
        f(ai16,bi16,ci16)
        end1 = time.time()
        start2 = time.time()
        g(ai16,bi16,ci16)
        end2 = time.time()
        print("int16:   {0:.6f} {1:.6f}".format(end1-start1,end2-start2))

if __name__ == '__main__':
    main()

 そのまま回すとオーバーフローしまくるので警告は消している。関数fとgは適当に考えた計算処理。本来はもっと色々なものを試すべきだが、面倒くさいのでこれだけ。

 実行すると乱数生成に時間がかかるので十数秒待たされる。ベクトルの計算自体は一瞬で終わる。

実行結果

vec size: 100
float64: 0.000051 0.000012
int64:   0.001442 0.000989
float32: 0.000040 0.000009
int32:   0.001109 0.000763
float16: 0.000125 0.000019
int16:   0.001328 0.001525
vec size: 1000
float64: 0.000123 0.000015
int64:   0.000014 0.000091
float32: 0.000009 0.000007
int32:   0.000008 0.000017
float16: 0.000129 0.000047
int16:   0.000007 0.000016
vec size: 10000
float64: 0.000682 0.000051
int64:   0.000033 0.000406
float32: 0.000017 0.000015
int32:   0.000015 0.000101
float16: 0.000933 0.000438
int16:   0.000015 0.000103
vec size: 100000
float64: 0.000842 0.000844
int64:   0.000672 0.003524
float32: 0.001567 0.000242
int32:   0.000189 0.003431
float16: 0.009531 0.004141
int16:   0.000073 0.002247
vec size: 1000000
float64: 0.034912 0.004574
int64:   0.004239 0.010060
float32: 0.002116 0.002203
int32:   0.002295 0.010271
float16: 0.089270 0.038791
int16:   0.001104 0.010050

 まず全体を見て気づくのは、ほとんどのケースでfloat64に比べてfloat32が速いこと。ちゃんと調べてないが、恐らくCPUアーキテクチャの問題なのだろう。また、float16にメリットはまったくない(半精度の浮動小数点演算を高速に実行したいというニーズはあまりないのだろう)。int16は掛け算と引き算しかない計算では無双しているが、割り算になると極端に遅いことがわかる。というか、intの割り算は全般に遅い。

 結論としては、やはり32bit浮動小数点数が速い。あくまでも私の環境ではと最初に断ったが、実際にはx86系のアーキテクチャなら似たような結果になる可能性が高い訳で、速度にシビアな(かつ精度を要求されない)状況ではこれを使っておけばよさそう。

【python】pythonでメモリ不足になったときにすること

最終更新:2018/11/26

はじめに

 pythonはLLですが、なぜかメモリを何十GBも消費するような(一般的なPCのリソースからすれば)大規模なデータ分析に広く使われています。このようなデータ分析では、往々にしてメモリ不足が生じ、それなりに配慮してプログラムを書かないとそもそもプログラムが走らない、MemoryErrorが出るといった事態が発生しがちです。

 そういうときにやるべきことをつらつらと書いていきます。なお、下の方に行くほど邪悪度()が増していきます。

 目次


スポンサーリンク


対策

メモリを増設する・システムのswap領域を増やす

 一番安直な解決策です。無理矢理チューニングするより手っ取り早いことも多いのではないでしょうか。また、ガチガチにチューニングしても駄目だったとき、最後に選べる選択肢もこれです。

 swapする場合、HDDだと相当厳しいものがあるので、少なくともSSDが必要です。予算がない場合は、USBメモリーを使う(リムーバブルディスクでやる)という手もあります(起動中に抜けちゃったら一貫の終わりですが)。

multiprocessingを使っているなら使うのをやめる、あるいはプロセス数を減らす

 二番目の選択肢です。プロセス並列は基本的に必要なデータがすべてコピーされるので、メモリ消費の面でかなりしんどいものがあります。プログラムが走っている最中にタスクマネージャ等で子プロセスのメモリ消費を見て、500MB以上消費しているなら検討するべき選択肢です。当然処理速度は低下します。

 どうしてもmultiprocessingを使いたい場合、できるだけ小さい粒度で回して、子プロセスにあまり多くのデータが行かないように気をつけます。また、子には必要なデータだけを渡すようにします。ただし、そうした場合は、相対的に並列化のオーバーヘッドが増え、並列化することによる恩恵が減ります。

 なお、multiprocessingには共有メモリがありますが、pythonのデータ型を直接プロセス間で共有することはできません。また、Managerなる強そうな機能もありますが、内部的にはpickle漬けにしてパイプで送る普通のmultiprocessingでしかないので、使ってもメモリ消費低減機能はありません。

 なお、multiprocessing.Poolを使う場合、Poolのインスタンスを作るタイミングを調整するなどしてある程度の対策が可能です。また、spawnという方法で子プロセスを生成すればかなり余計なメモリ消費は抑えられます。ただし、これには余計なオーバーヘッドもあります。

multiprocessing.Poolがやたらメモリを消費するときの対策 - 静かなる名辞

要らないデータはGCに回収させる

 基本中の基本ですが、よく使うので例を挙げて説明します。

with open("data", "r") as file:
    data1 = pickle.load(file)

data2 = 何らかの処理1(data1)
data3 = 何らかの処理2(data2)

 こういう処理を書くと、data1, data2, data3がそれぞれメモリ領域を消費することになります。普通、これらすべてが最終的に必要ということはないはずなので、使い終わったデータは解放してやった方がメモリを節約できます。

 一つの考え方としては、pythonのGCは基本的に参照カウントなので、ぜんぶ同じ名前に束縛しちゃえば良いという方法があります。

with open("data", "r") as file:
    data = pickle.load(file)

data = 何らかの処理1(data)
data = 何らかの処理2(data)

 この方法だとsome_processingが返ってdataに値が束縛される度、古いdataはメモリ上から消えていきます(逆に言えば、代入が終わってからGCが走るまでの期間では、瞬間的に古いdataと新しいdataが両方メモリ上にあることになります。これについては後述)。ただ、実際問題としては、別の名前がついてた方がプログラムの可読性は上がります。なので、del文を使います。

with open("data", "r") as file:
    data1 = pickle.load(file)

data2 = 何らかの処理1(data1)
del data1
data3 = 何らかの処理2(data2)
del data2

 del文は受け取った名前の参照を消します。注意すべきなのは、del文は名前の指す実体を消す訳ではなく、あくまでも参照を消すに過ぎないということです。del文で実体を消せるのは、del文によってオブジェクトへの参照が0になるときだけです。

リストは積極的にnumpy配列にする

 私は癖で「配列の形をlistで作ってからnumpy配列に変換する」という処理を良く書くのですが、その度にその配列のメモリ消費が半分近く減少するという経験をしています。

 これはある意味当たり前のことで、そもそもリスト構造はポインタの塊なので、データの中身を記録するのと同じくらいのメモリ領域をアドレスデータを持つことに費やしています。numpy配列はリストと比べればだいぶ効率的なデータ構造をしているので、これを使ってメモリ消費が小さくなるのは当然です。

 なお、あまり使われているのを見かけませんが、numpy配列は数値型の他にも多くのデータを格納できます。たとえば、str型を入れられる他、objectなるデータ型も持っています。str型のnumpy配列は大規模データならそれなりに有用ですが、object型は実質的にpythonのデータへのポインタでしかなく、numpyの便利な機能(shapeとかtransposeとか)が使える以外のメリットはないと心得るべきです。

疎行列型配列を使用する

 機械学習で、大規模でかつスパース(ほとんどの部分がゼロ)なデータを扱っている場合限定の方法ですが、疎行列型(scipy.sparse.csr_matrixなど)でデータを持たせておくという手があります。これを使うとデータ自体で消費するメモリ領域を減らすことができ、また場合によっては高速化も期待できるなど、多くのメリットがあります。ただし、スパースでない配列を疎行列型で表現してもメリットは得られません。

【python】scikit-learnで大規模疎行列を扱うときのTips - 静かなる名辞

32bitにする

 データ処理では良くfloatのnumpy配列を使うと思います。巨大なnumpy配列がメモリを食っている場合、これを32bitにするとメモリ消費は単純計算で半分になります。

import numpy as np
some_data = np.array(some_data, dtype=np.float32)

 また、これをやると心なしか(ほとんど気のせいレベルですが)実行速度が速くなるような気がします。64bitのFPUが使えるCPUでもそうなのは、キャッシュに入れられるデータ量が増えるからとか?

 当たり前ですが、32bitで必要な精度が得られるかはまったく別の問題です。とはいえ、pythonで良くやる機械学習では32bitで足りる場面も多いのではないでしょうか。

配列処理は破壊的代入で行う

 たとえば、こういう処理があったとします。

large_data = [何らかの処理(x) for x in large_data]

 一見すると良さそうですが、リスト内包表記が返ってlarge_dataに新しい値が代入されてから、GCが走るまでの間、メモリ上には旧いlarge_dataと新しいlarge_dataが併存することになります。データの大きさ次第では、メモリが溢れてしまいます。

 こういうときは破壊的代入で書くしかありません。

for i, x in enumerate(large_data):
    large_data[i] = 何らかの処理(x)

 これだとメモリは溢れません。

 pythonではfor文を回して配列のインデックスを叩くような処理は、とても重いことで有名です。とはいえ、他に良い手がないことも多いので、実際にはこういう方法は割と良く使うかと思います。これで速度が厳しいときはcythonで書くとだいぶマシになります。

numpyの機能に頼る

 numpy配列などを使う場合は、様々な方法でin-placeな処理や、省メモリな処理を実現できます。たとえば、+=などの累積代入文を使う、viewとcopyを適切に使い分ける、などです。

 数値計算などを使う場合は、このようなnumpyの機能を積極的に使いこなすことで、高速化と省メモリ化が図れます。

ファイルにダンプする

 中間ファイルなどを一旦ストレージに吐き出してしまい、あとで必要になったときにまた読むという方法があります。そうすることで一時的にメモリを解放できますが、オーバーヘッドは相当かさみます。

メモリ上でデータを圧縮する

 「必要なデータなんだけどすごく大きくて、ただしそんなに頻繁にアクセスする訳ではない」というときは、メモリ上でデータを圧縮する方法があります。これについては以前記事にしました。

【python】メモリ上のオブジェクトを是が非でも圧縮したい - 静かなる名辞

 当然これには圧縮・解凍分のオーバーヘッドがありますが、中間ファイルを吐くよりはだいぶマシになるかと思います。どこまで実用性があるかは微妙ですが、他にどうしようもないときに検討すべき選択肢ではあります。

 また、同じことですが、RAMディスク領域を確保して中間ファイルを置くのに使うという手もあります。

終わりに

 pythonは泥臭いリスト・配列処理やメモリ管理を隠蔽してくれる良い言語ですが、データフローまで面倒を見てくれる訳ではないので、人間がちゃんとデータフローを作ってやらないとハマります。

 そして、pythonは泥臭い部分を隠蔽してしまっているので、いざというときチューニングが効かないシチュエーションも相応にあります。極端な話、「C/C++ならこのデータフローで大丈夫だけど、pythonだと無駄が多くて無理」という状況もあり得ます。

 なので、pythonで実用できる程度に効率的なプログラムを書くのは意外と難しい側面があります。やむを得ずバッドノウハウを駆使してどうにか動かす、というシチュエーションも多いのではないでしょうか。

 そういう難しい面はありますが、pythonはやっぱり楽です。また、それなりに考えて書けばそれなりにメモリ消費を減らすこともできます。なので、データフローやメモリ確保のされかたを意識しながら工夫してコーディングすれば、極端にひどい事態にはならないと思います。

【python】pythonでn-gramの特徴量を作る

 ○○ってパッケージでできるよ! という意見もあると思いますが、ちょっと挙動を変えたくなる度にパッケージのhelp読んだり、微妙に柔軟性のないパッケージに苦しむ(たとえば文末の句点と次の文の最初の文字は繋げないで欲しいのにできない、とか)くらいなら、最初から自分で書いた方が速いです。好きなだけ編集できます。

 とりあえず、文字列ないし形態素のリストなどをn-gramに切り分ける関数を作ってみます。

def ngram_split(string, n, splitter="-*-"):
    """
    string:iterableなら何でも n:n-gramのn splitter:処理対象と混ざらなければ何でも良い
    """
    lst = []
    for i in range(len(string[:-n+1])):
        lst.append(splitter.join(string[i:i+n]))
    return lst

 結果はこんな感じ。

>>> for x in ngram_split("吾輩は猫である。", 3):
...     print(x)
... 
吾-*-輩-*-は
輩-*-は-*-猫
は-*-猫-*-で
猫-*-で-*-あ
で-*-あ-*-る
あ-*-る-*-。
>>> for x in ngram_split(["吾輩","は","猫","で","ある","。"], 3):
...     print(x)
... 
吾輩-*-は-*-猫
は-*-猫-*-で
猫-*-で-*-ある
で-*-ある-*-。

 悪くないですが、返り値はdictの方が便利そうです。

from collections import defaultdict
def itr_dict(itr):
    d = defaultdict(int)
    for x in itr:
        d[x] += 1
    return d

 ↑こういうのを作っておいて、

>>> itr_dict(ngram_split(["吾輩","は","猫","で","ある","。"], 3))
defaultdict(<class 'int'>, {'は-*-猫-*-で': 1, '吾輩-*-は-*-猫': 1, '猫-*-で-*-ある': 1, 'で-*-ある-*-。': 1})

 こんな感じで使えば良いのではないでしょうか。


 さて、以下のようなデータを考えます。

data_lst = ["吾輩は猫である",
            "国境の長いトンネルを抜けると雪国であった",
            "恥の多い生涯を送って来ました",
            "一人の下人が、羅生門の下で雨やみを待っていた",
            "幼時から父は、私によく、金閣のことを語った"]

 これを文字2-gramの特徴量にしてみます。すでにn-gramは作れるようになっているので簡単です。

from sklearn.feature_extraction import DictVectorizer

bgram_dict_lst = [itr_dict(ngram_split(x, 3)) for x in data_lst]
dict_vectorizer = DictVectorizer()
a = dict_vectorizer.fit_transform(bgram_dict_lst).toarray()

 DictVectorizerって何? という声が聞こえてきそうなので、sklearnのドキュメントを貼ります。
sklearn.feature_extraction.DictVectorizer — scikit-learn 0.20.1 documentation

 結果を先に見せると、こういうものが生成されています。要するに、dictから特徴量まで一気に作ってくれます。

>>> a
array([[ 0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  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.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,
         0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  1.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  1.,  0.,
         0.,  1.,  0.,  0.,  0.,  1.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,
         1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  1.,  0.,  0.,
         1.,  1.,  1.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,  1.,
         0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  1.],
       [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,
         1.,  0.,  1.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,
         0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         1.,  0.,  0.,  1.,  0.,  0.,  1.,  1.,  0.,  0.,  1.,  0.,  0.,
         0.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  1.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,
         0.,  0.,  1.,  1.,  0.,  0.,  1.,  0.,  0.,  0.,  0.,  2.,  0.,
         0.,  0.,  0.,  0.,  1.,  1.,  0.,  0.,  0.,  1.,  0.,  0.,  0.,
         0.,  0.,  0.,  0.,  1.,  1.,  1.,  1.,  1.,  0.,  0.,  0.,  0.,
         0.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,
         1.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  1.,  0.],
       [ 1.,  0.,  1.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  1.,  0.,  1.,
         0.,  1.,  0.,  0.,  0.,  0.,  0.,  1.,  0.,  1.,  1.,  0.,  0.,
         0.,  1.,  0.,  0.,  0.,  0.,  1.,  1.,  0.,  0.,  0.,  1.,  0.,
         0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
         0.,  1.,  0.,  0.,  0.,  1.,  0.,  0.,  1.,  0.,  0.,  0.,  1.,
         0.,  1.,  0.,  0.,  1.,  0.,  0.,  1.,  0.,  0.]])

 DictVectorizerに似たようなものを自分で作ることも、当然できます。が、基本的にこの手のデータ処理で細かく弄り回したいと思うのは前処理の部分ですから、こういう「dictを行列に変換する」みたいな単純な処理はパッケージに投げてしまった方が世の中の幸せの総量は増える気がします。逆に言えば、前処理が必要ならDictVectorizerに投げる前(あるいは投げた後)に済ませておく必要があります。


 ところで、この特徴量はこれでアリですが、たぶんそれぞれのベクトルをテキスト長か何かで割って相対頻度に変換してやると、機械学習的に良い感じになると思います。

import numpy as np

len_array = np.array([[len(x)]*a.shape[1] for x in data_lst])
#↑もうちょっと綺麗な方法があるなら教えて欲しい

std_a = a/len_array

 ここまで来れば、そのまま機械学習アルゴリズムに突っ込んでもそこそこなんとかなるかと思います。性能を求めるなら、更に特徴選択等を入れるべきでしょう。

【python】pythonのsetは値比較なのか?

 ふと思った。「pythonのsetって値比較してるの?」

>>> a = ("hoge",) #同値かつ同一でないimmutableの作り方がタプル以外思いつかなかった
>>> b = ("hoge",)
>>> a is b
False
>>> id(a)
140412431555272
>>> id(b)
140412430884312
>>> set([a,b])
{('hoge',)}

 してるらしい。このsetの中身はどっちのオブジェクトなんだろう。

>>> id(list(set([a,b]))[0])
140412431555272

 先に書いた方? 数を3つにして試してみる。

>>> a = ("hoge",)
>>> b = ("hoge",)
>>> c = ("hoge",)
>>> id(a)
140412431554880
>>> id(b)
140412431555272
>>> id(c)
140412430884312
>>> id(list(set([a,b,c]))[0])
140412431554880
>>> id(list(set([b,c,a]))[0])
140412431555272
>>> id(list(set([c,a,b]))[0])
140412430884312

 やっぱり「先に出現したオブジェクトのid」という規則性があるような気もするけど、言語仕様で決まってる訳ではないので、これを利用してプログラムを書こうとか考えない方が良いだろう。

 まあ、それ以前に使い道もないけど。

【python】pythonでもprintf/scanfしたい!

最終更新:2018-04-10

※注意! この記事はネタ記事です。この記事に書いてある方法でpythonからprintf/scanfを使うことはできますが、実用性は保証しません。

 実用的な方法を知りたい方は、こちらの記事にアクセスしてください。
【python】pythonでprintf的なことをする - 静かなる名辞

【python】pythonでscanf的なことをする - 静かなる名辞



 C言語でプログラミングの基礎を勉強した人がpythonにやってきて初めに抱く不満は「文字列処理面倒くさ」ではないでしょうか。pythonにはpythonの文字列フォーマットがありますし、正規表現も標準で使えるので慣れれば快適な環境という考え方もありますが、Cの書記指定に郷愁を抱く方もいるでしょう(今時そんな奴いるのか? というツッコミはなしで)。

 幸い、pythonには標準でctypesというモジュールがあり、これを使えばDLLを読み出せます。なので、いつもCで使っていたprintf/scanfをpythonから呼び出してやることにしましょう。

 とりあえず練習としてputsを使ってみます。

>>> from ctypes import *
>>> libc = CDLL("libc.so.6")
>>> libc.puts("hello world".encode()) #よくわからないけどエンコードしないと駄目でした
hello world
12

 死ぬほど簡単ですね。hello worldの後に表示されている12はいつもの関数返り値がREPLに出てるだけです。hello worldはDLLが直接標準出力に書いてます(るはず)。「libc.so.6ってなに?」って人は諦めてpythonの文字列フォーマットを覚えましょう。

 ところで、日本語は大丈夫なんでしょうか。

>>> libc.puts("ほげほげ".encode())
ほげほげ
13

 とりあえず動いてるんだと思いますが、この辺は環境によってシビアな場合もあるので、なんとも言えません。とりあえず私のlinuxは大丈夫みたいです(たぶん。特定文字だけ化けるみたいな嫌過ぎるアレがなければ)。

 いけそうなので、printfしてみましょう。

>>> libc.printf("%s%d\n".encode(),"ほげほげ".encode(), 1234)
ほげほげ1234
17

 死ぬほど簡単ですね。これなら特に悩むこともなく使えそうです。ところで、小数を与えたら?

>>> libc.printf("%s%f\n".encode(),"ほげほげ".encode(), 12.34)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ctypes.ArgumentError: argument 3: <class 'TypeError'>: Don't know how to convert parameter 3

 こうなりました。実はctypesでは基本的に、pythonの型を「Cの型をラップしたオブジェクト」に変換してからCの関数に渡す必要があります。ただし「文字列と整数(とNone)は頑張って自動的キャストする」という仕様になっているので、これまでエラーが出てこなかっただけです。

 こういう場合は、大人しく型を書きましょう。簡単です。

>>> libc.printf("%s%f\n".encode(),"ほげほげ".encode(), c_double(12.34))
ほげほげ12.340000
22

 使える型の一覧とかは公式ドキュメント見てください。
16.16. ctypes — Pythonのための外部関数ライブラリ — Python 3.5.4 ドキュメント

 printfはこれで何とか使えそうですが、次はscanfを使わないといけませんね。scanfは周知の通り、標準入力から読み込んだ文字列を書式に従って解釈し、結果をポインタに格納します。なんかポインタとかやばそうです。そもそも使えるんでしょうか。

 結論から言うと、使えます。つーか、ドキュメントに使えるって書いてあります

 どれどれ

>>> i = c_int()
>>> libc.scanf("%d".encode(), byref(i))
0
>>> i
c_int(0)

 これを実行すると、入力待ちにならないで速攻終わってくれます。そして値が入っていてほしかったiの中身は、初期化された状態のままの0です。駄目じゃん。駄目な理由はわからないようななんとなくわかるような雰囲気ですが、とりあえずちゃんと調べて対処するのは面倒くさいのでパス。動いてないのは標準入力だと思うので、sscanfを使ってみます(ドキュメントにもそっちが書いてあるし)。

>>> i = c_int()
>>> libc.sscanf("1234".encode(), "%d".encode(), byref(i))
1
>>> i
c_int(1234)

 これは期待通りですね。ちなみに、c_intからpythonの型の値を取り出すには、

>>> i.value
1234

 で良いようです。

 これができるなら後は簡単で、

>>> s = input()
1234
>>> libc.sscanf(s.encode(), "%d".encode(), byref(i))
1
>>> i.value
1234

 と動かすことができ、scanfと同じことができます。やったねたえちゃん、書式指定が使えるよ!

まとめ

 こんなの使うより正規表現とか覚えるべき。あとはsscanf風(というかpythonのformatの逆みたいな感じ)に文字列を切り出せるparseってパッケージもあるらしいです。pipで入ります。
parse · PyPI

【python】multiprocessingはアホみたいにメモリ食うよって話

 それなりに大きい(それでも数GBとかそんなもん)データをmultiprocessingで処理しようとしたら、メモリが溢れて大変だった。その原因と対処法について書いておく。

 multiprocessingはプロセス間でメモリを共有しない。ということは、処理するデータは一々プロセスにコピーされる。3GBのデータを4つのプロセスで並列処理しようとしたら、あっという間に12GB+親プロセスの3GBで15GB埋まる。

 いや、それどころか、プロセス間のデータ転送アルゴリズムがpickleなせいで、瞬間的に転送されたデータ+そのpickleが存在することになり、そしてpickleはお世辞にも省容量とは言い難いデータ構造をしている。つまりpickleの分もメモリは消費されると見ておいた方が良い。

 こういうことは理解して正しく使えば問題にならないんだけど(つまりメモリ消費が大きくなりそうなときはmultiprocessingはやめとけば良い。大して時間がかからないなら諦めて1コアで実行するか、可能ならcython等に逃げる)、「ヒャッハー! お手軽並列処理だぜ!」な気分で使ってた自分は割と酷い目に合った。反省。

追記

 対策記事を書いた。使い方を工夫すればメモリ消費は抑えられるので、うまく使うことが肝心だと思った。
multiprocessing.Poolがやたらメモリを消費するときの対策 - 静かなる名辞

【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()

 使い道は特に思いつかないけど、参照渡しにするためだけにリストオブジェクトを生成するのもちょっと不条理な感じでクール? いや、そんなことはないですね。

2018/11/26 追記

 その後勉強し、そもそも参照渡しという発想で捉えるのが間違っていたという結論に達しました。

 こちらの記事を御覧ください。

共有渡しと参照の値渡しと - 静かなる名辞

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

 でかいデータをなにも考えずメモリ上に置いておくと、あっという間にメモリが埋まる。

 不要なデータはこまめに消して、必要なときに必要なものだけメモリに置くようにすれば大抵なんとかなるのだけど、そうやって整理していくと、ある水準を超えたところで処理時間とかコードの可読性が問題になってしまう。

 こういう問題の解決策として、データを生成したあとは圧縮してメモリ上に置いておき、使うときに解凍して呼び出すという方法が考えられる。もちろん普通にできることではない。ないのだが、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の使用メモリが載っちゃってるか、それとも一千万個も生成した整数オブジェクトをぜんぶ回収しないうちに次の処理が進んでるんだろうか。そこに関しては詳しくないので謎。

2018/11/26 追記

 この記事を書いたときからほぼ2年の歳月が過ぎ、古い記事を見直していた私はこう思った。

「joblibでできそう。わざわざ自分で書く意味なくね」

joblib.dump — joblib 0.13.0 documentation

 近日中に検証して記事にする予定。

【python】random.shuffleについて

はじめに

 標準モジュールのrandom.shuffleは直感と違う挙動をするので、メモしておきます。

 参考:
9.6. random — 擬似乱数を生成する — Python 3.6.5 ドキュメント

問題のコード

 このようなコードです。

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

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

 どうやらrandom.shuffle()はNoneを返すらしい。どういうことだろう? ああ、なんかなんとなく想像はできるけど・・・。

正しいやり方

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はいわゆる「破壊的操作」で、listの参照を受け取って(pythonのオブジェクトはいわゆる参照で渡されるから)、その参照先をシャッフルします。Cかよ。

 ともかく、こいつは副作用のない、お行儀の良い関数じゃないのです。pythonは『マルチパラダイム言語』なので、そしてその『マルチパラダイム』の中にはオールドファッションな手続き型も入ってると思うので、こういうものが標準ライブラリの中にあっても怒ってはいけません。

この挙動が嫌なとき

 random.shuffleは引数のオブジェクトを直接変更するので、シャッフル前の情報は残してくれません。

 元のリストも取っておきたいときは自前でやる必要があります。

 これにはいろいろな方法があります。

  • list(lst)でオブジェクトを新しく生成する
  • lst[:]のように空のスライスを使う
  • lst.copy()のようにlist.copy()メソッドを使う

 どれが優れているとかは特にないと思いますが、lst.copy()で書いてみることにします。

import random

lst = list(range(10))
shuffled = lst.copy()
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

"""

 shuffled = lst.copy()の時点ではシャッフルされていないという気持ち悪さがある。変数名には改善の余地があります。

 ちなみに、random.shuffle(lst.copy())としてもちゃんと動きますが、動き終わった後に結果のlistがゴミ集めに回収されていくだけなので無意味です。

まとめ

 知っていればハマりませんが、知らないと戸惑います。

【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 sns
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))
    sns.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に使いまわすのがミソ。

2019/12/15 追記

 sklearn v0.22でもっといいのができた。

www.haya-programming.com