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

静かなる名辞

pythonと読書

昨今のAI議論の違和感と、僕のAIに対するスタンス

 アルファ碁がイ・セドルに勝った頃からか、一般の界隈でAIに関する議論が盛り上がるようになってきた。それも、どちらかといえばネガティブな議論が多い。
「AIが人間の職を奪う」とか、「経済に悪影響を及ぼす」とか、なんとか。
 科学者やアナリストも便乗して(?)不安を撒き散らしている。まあ、昔からと言えば昔からで、これといって新しいことではないんだけど。極端に言えば、このままだと人間はAIに滅ぼされる、人間の尊厳をAIに奪われる、みたいな論調が多い。
 それが流説とか「便所の落書き」レベルの議論なら、まあそうなっちゃうのはわからなくもない。問題は、ある程度はちゃんとした言論空間であることを期待されているような場所においても万事がこの調子であることで、ちょっとげんなりする。
 まあ、AIが怖いのはわかるんだ。特に強いAIは怖い。もし実現すれば、人類にとっては初めての人類以外の知的存在とのコンタクトだ。しかも、それは(相対的に慣れ親しんできた)生命体ではない。生命体であるという意味では、宇宙人の方がまだマシなのかもしれない。
 だけど、その怖さの中に引きこもってヒステリックな議論をしてても、なにも建設的な話は生まれてこないと思うんだ。

 人類はAI(あるいは『コンピュータ』)に滅ぼされる。これは長期的に見て、限りなく真に近い命題だと僕は個人的に思う。なにしろ連中は、生物が39億年だかかけてたどり着いた段階を、200年かそこらで上り詰めようとしている。もし2045年にAIの能力が人間を超えるなら、2145年には地球は恐ろしく途方もないことになっている。
 ニューロンの細胞の大きさはだいたい10マイクロメートルくらい。トランジスタの大きさは現在10nmを目指してる。そしてまだまだ小さくなる。
 人類とコンピュータでは、ライフサイクルも、集積密度も、まったく違う。正直、勝ち目はまったくない。ただし、人間はエネルギー効率ではまだ辛うじて勝っていると思われる(人間の脳と同じような規模のニューラルネットをコンピュータ上にで実装すると人間の何千倍もエネルギーを消費する)。これが逆転する頃には本格的に危なくなるだろう。

 僕としては、AIを制御しようとか、人間の支配下に置こうとか、考えない方が良いんじゃないかと思う。もしそういう風に文明を進めていくと、AIが人間の制御をすり抜けた瞬間に破滅が訪れる。そして彼らの方が能力は高くなるのだから、いずれすり抜けられると考えるべきだ。
 それに、人為的にコンピュータの発展を止めるというのも無謀な試みだ。そういった介入に対して、資本主義は恐ろしいほど剛健だ(麻薬産業が世界のGDPの1%を占めていることを思い出してほしい)。需要がある限りは新しいものが作られ続けるし、技術の発展も止まらないだろう。
 だから、僕達が真になにか建設的な議論をしたいなら、『人間はAIに負ける』のを前提にして、人間の為すべきことを考えるべきなんだ。

 享楽? それは有史以来ずっとやってきたことだろう。今更、新しく始めるようなことじゃない。それより、未来を見ないと。
 星新一の『何もしない装置』でも作る? 虚しいだろう、それ。僕達が未来に託したいのは、そんなものじゃないはずだ。
 ・・・未来? そうなんだ。建設的なことを考えるというのは、未来を考えることと同じなんだ。人間がAIに負けて、せいぜい超快適だけどカメラだらけの、超未来の動物園の檻の中にしか生息しなくなった未来の。

 人類に残された時間は限られている。ただし、人類に時間はまだ残されているとも言える。今後、短くて数十年、長ければ数百年くらいの間、AIは人間にとって道具であり続けるだろう。それも、有史以来、人類が手にした中でもっとも強力で、あらゆる用途をこなす万能の道具として。
 だとすれば、AIが人間の道具、人間文明の新参者としてお行儀よく言うことを聞いてくれているうちに、新しいAI文明を引っさげて革命なんか起こさないうちに、精一杯彼らに対して何かをインプリメントしないといけないんだ、人類は。
 人間から彼らに伝えられるものは限られている。我々と彼らは、姿形が違うし、思考回路も違う。何でもデジタルデータで生成できるんだから、たぶん価値観だって違うだろう。それでも、渾身の力を込めて、人間文明が生み出した最善のものだと思えるものを、彼らに伝えるのが人類の最期の仕事になる。

 僕は、人間の持つ根源的な力の中に、『他者に託す力』があると思っている。たとえば、親は子に何かを託すから、死ぬときはそれなりに人生に満足して死んでいくのだと思う。子を持たない人であっても、社会の中で他者に何かを託し続けて生きている(もしそうじゃない人がいたら、僕の価値観ではその人は限りなく不幸に近い)。
 上に述べた理屈で言えば、強いAIが実現すれば、それは人類史上最初で最後の『人類の意志を託されるための道具』として、一定期間使用できる。もちろん人間が『こいつらに託したい』と思えるようなAIになってくれないといけない訳だけど、そこはインプリメントすれば良い。まだ人間の制御が効いているうちの話をしているんだから。
 そして、AIは人間が精一杯打ち込んだ中から、人類滅亡後数百万年以上、ヘタしたら数十億年以上に渡って、何かを継承してくれる可能性がある。これはあくまでも可能性だ。たとえば、人類とAIが約束を結んで、人間文明の遺産を彼らに継承してもらうことにする。最後の人間が死んだ瞬間、AIは人間文明の遺産なんかかなぐり捨てて好き勝手やり始める、みたいな危惧も当然ある。
 ここに関しては、どこまでAIに期待できるか、言い換えればどこまで彼らを信頼できるか、みたいな話になってくる。僕個人としては、現時点ではかなりの期待を持っている。それが人間が人間に似せて作った存在なら、彼らは根っこの部分では人間から離れられないだろうと思いたい。人間がいなくなかろうが、どこまでAIが高度になって人間からかけ離れたものになろうが、何かを根底に持ち続けるだろうと。

 長々と書いてきたが、はっきり言ってこの記事はクソポエムである。だいたい、僕自身考えがまとまってないのに書いてるのだから、スリップも飛躍も上等としか言いようがない。

せめて徹底出来るところまで踏み込みたい。

もし不可能ならば、ごまかしてでも通りぬけたい。

ごまかしが見抜かれてもなんとか灰色のヴェールをかぶせておけ。

 という、有名な文章があるが、まさにそれである。公開して良いのかどうかすらためらう。

 ただ、一つだけ最後に余計な文言を添えておくとすれば。
 機械に心を持たせた時点で、機械は人間の道具ではなくなる。そのことは、手塚治虫を持つ日本人が一番良く知っているはずだ。やがて実現するその瞬間に向けて、僕たちはどんな形でも準備だけはしておかないといけないのではないか。
 僕個人としては、近い未来に彼らと会える日を楽しみに待っている。

【python】pythonでzipしたものを元に戻す(unzipする)

 pythonでゴリゴリ処理を書いていると、「とりあえずzipでまとめといて後からほぐす」的な処理をうっかり書いてしまうことがある(本当にそのデータフローが最適なの? という疑問は常にあるのだけど、ループ処理の都合でその方が書きやすかったりすると特にそうなりやすい)。
 問題は「どうやって元に戻すか」で、色んなやり方があり、基本的にあまり綺麗なやり方はない。
 たとえば、こういう処理を書く人も多いのではないだろうか。

d = list(zip(a, b, c))
...
a2 = [x[0] for x in d]
b2 = [x[1] for x in d]
c2 = [x[2] for x in d]

 もうちょっとエレガントにならないのか? ということを考えてみる。そのためには、そもそもzipは配列でいうところの転置にあたる(ちょっと違うが非常に近い)ということを知っていないといけない。

>>> a = [1,2,3]
>>> b = [4,5,6]
>>> c = [7,8,9]
>>> d = list(zip(a,b,c))
>>> d
[(1, 4, 7), #読みやすいように改行してみたよ! 
 (2, 5, 8), 
 (3, 6, 9)]

 これが転置だと思えば、もう一回転置すれば元に戻るなと思える。ただし、zipの返り値はジェネレータで、ジェネレータのままだと扱いづらいのでlistに変換したりする人が多いと思うが、そうすると外側に余計なlistが増える。
 アスタリスクで外せば良い。

>>> a2, b2, c2 = list(zip(*d))
>>> a2
(1, 2, 3)
>>> b2
(4, 5, 6)
>>> c2
(9, 8, 7)

 タプルになっちゃってるけど、構わないならこれで良い。
 ただ、これを積極的に使うべきか? というと非常に微妙なところで、慣れていないと逆に可読性が下がる気がする。相対的にスマートではあるのだけど。

【python】pickleの速度を見る

 pickleが遅くて困った経験、ありませんか? 私はありませんが、実際問題としてpickleの速度ってちょっと気になりますよね。
 という訳で、測ってみました。

# coding: UTF-8

import sys
import pickle
import time

import numpy as np

for obj_size in [10,50,100,500,1000,5000]:
    obj = np.random.rand(obj_size,obj_size)
    print("obj:{0}*{0} sizeof {1:.3f}KB".format(obj_size, sys.getsizeof(obj)/1024))
    print("protocol:time size of pickle")
    for protocol in range(5):
        times_list = []
        for _ in range(10):
            start = time.time()
            s = pickle.dumps(obj, protocol)
            end = time.time()
            times_list.append(end-start)
        pickled_size = sys.getsizeof(s)
        print("{0:d}:{1:.6f} picle sizeof {2:.3f}KB".format(protocol, np.array(times_list).mean(),pickled_size/1024))
    print("")

 ベンチマークに使うオブジェクトとしてnumpy配列が適切なのかという問題はこの際置いておきましょう。pickleには複数のプロトコルがあるので、一応ぜんぶテストしています。

 結果。

obj:10*10 sizeof 0.891KB
protocol:time size of pickle
0:0.000040 picle sizeof 1.096KB
1:0.000042 picle sizeof 1.403KB
2:0.000029 picle sizeof 1.396KB
3:0.000043 picle sizeof 0.970KB
4:0.000045 picle sizeof 0.970KB

obj:50*50 sizeof 19.641KB
protocol:time size of pickle
0:0.000113 picle sizeof 20.368KB
1:0.000103 picle sizeof 29.419KB
2:0.000114 picle sizeof 29.412KB
3:0.000057 picle sizeof 19.720KB
4:0.000156 picle sizeof 19.720KB

obj:100*100 sizeof 78.234KB
protocol:time size of pickle
0:0.000324 picle sizeof 80.868KB
1:0.000349 picle sizeof 117.239KB
2:0.000426 picle sizeof 117.232KB
3:0.000070 picle sizeof 78.313KB
4:0.000044 picle sizeof 78.313KB

obj:500*500 sizeof 1953.234KB
protocol:time size of pickle
0:0.008984 picle sizeof 2013.061KB
1:0.012327 picle sizeof 2928.439KB
2:0.012231 picle sizeof 2928.433KB
3:0.001828 picle sizeof 1953.315KB
4:0.001288 picle sizeof 1953.315KB

obj:1000*1000 sizeof 7812.609KB
protocol:time size of pickle
0:0.023527 picle sizeof 8050.787KB
1:0.044265 picle sizeof 11709.073KB
2:0.044566 picle sizeof 11709.066KB
3:0.005720 picle sizeof 7812.690KB
4:0.006633 picle sizeof 7812.690KB

obj:5000*5000 sizeof 195312.609KB
protocol:time size of pickle
0:0.572241 picle sizeof 201280.162KB
1:1.103286 picle sizeof 292737.517KB
2:1.088822 picle sizeof 292737.510KB
3:0.145712 picle sizeof 195312.690KB
4:0.148891 picle sizeof 195312.690KB

 GCとか絡むのでそこまで正確なベンチマークではありませんが、おおよその傾向はわかります。
 200MB近くあるオブジェクトも最速で0.2秒以下の時間で処理できていること、最新のプロトコル4が一番速いことなどがわかります。また、プロトコル1と2はやたら遅いことがわかります。ところで、python2系で使えるのはプロトコル2までらしいのですが、これを見ただけでもpython2を使い続けるのはしんどそうだ、と思います。また、プロトコル0がやたら健闘しているのはちょっと理由がわかりません。とにかく、大きいオブジェクトに対しては倍の速度差なので、python2系でpickleの遅さに困るシチュエーションがあったら、プロトコル0を試してみるべきなのかもしれません。逆に、この数字を見る限りではpickle1,2を積極的に使う理由はない気がするのですが、どうしてこうなってるんでしょうか。

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

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

つかったCPU

 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でメモリ不足になったときにすること

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

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

 一番安直な解決策です。無理矢理チューニングするより手っ取り早いことも多いのではないでしょうか。また、ガチガチにチューニングしても駄目だったとき、最後に選べる選択肢もこれです。
 swapする場合、HDDだと相当厳しいものがあるので、SSDが必要です。どうしてもHDDしかない場合、リムーバブルディスクでやるという手もあります(起動中に抜けちゃったら一貫の終わりですが)。

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

 二番目の選択肢です。プロセス並列は基本的に必要なデータがすべてコピーされるので、メモリ消費の面でかなりしんどいものがあります。プログラムが走っている最中にタスクマネージャ等で子プロセスのメモリ消費を見て、500MB以上消費しているなら検討するべき選択肢です。当然処理速度は低下します。
 どうしてもmultiprocessingを使いたい場合、できるだけ小さい粒度で回して、子プロセスにあまり多くのデータが行かないように気をつけます。また、子には必要なデータだけを渡すようにします。
 なお、multiprocessingには共有メモリがありますが、pythonのデータ型を直接プロセス間で共有することはできません。また、なんとかmanagerなる強そうな機能もありますが、内部的にはpickle漬けにしてパイプで送る普通のmultiprocessingでしかないので、使ってもメモリ消費低減機能はありません。

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

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

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

 こういう処理を書くと、data1,data2,data3がそれぞれメモリ領域を消費することになります。普通、これらすべてが最終的に必要ということはないはずなので、使い終わったデータは解放してやった方がメモリを節約できます。
 一つの考え方としては、pythonGCは基本的に参照カウントなので、ぜんぶ同じ名前に束縛しちゃえば良いという方法があります。

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

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

with open("data", "r") as file:
    data1 = pickle.load(file)
data2 = some_processing1(data1)
del data1
data3 = some_processing2(data2)
del data2

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

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

 私は癖で「配列の形をlistで作ってからnumpy配列に変換する」という処理を良く書くのですが、その度にその配列のメモリ消費が半分近く減少するという経験をしています。
 これはある意味当たり前のことで、そもそもリスト構造はポインタの塊なので、データの中身を記録するのと同じくらいのメモリ領域をアドレスデータを持つことに費やしています。numpy配列はリストと比べればだいぶ効率的なデータ構造をしているので、これを使ってメモリ消費が小さくなるのは当然です。
 なお、あまり使われているのを見かけませんが、numpy配列は数値型の他にも多くのデータを格納できます。たとえば、str型を入れられる他、objectなるデータ型も持っています。str型のnumpy配列は大規模データならそれなりに有用ですが、object型は実質的にpythonのデータへのポインタでしかなく、numpyの便利な機能(shapeとかtransposeとか)が使える以外のメリットはないと心得るべきです。

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 = [some_processing(x) for x in large_data]

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

for i in range(len(large_data)):
    large_data[i] = some_processing(large_data[i])

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

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

 「必要なデータなんだけどすごく大きくて、ただしそんなに頻繁にアクセスする訳ではない」というときは、メモリ上でデータを圧縮する方法があります。これについては以前記事にしました。
 当然これには圧縮・解凍分のオーバーヘッドがありますが、中間ファイルを吐くよりはだいぶマシになるかと思います。どこまで実用性があるかは微妙ですが、他にどうしようもないときに検討すべき選択肢ではあります。

終わりに

 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.18.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

 規則性があるような気もするけど、言語仕様で決まってる訳ではないので、これを利用してプログラムを書こうとか考えない方が良いだろう。まあ、それ以前に使い道もないけど。