病みつきエンジニアブログ

機械学習、Python、Scala、JavaScript、などなど

Pythonでコード書く時にはちゃんと時間を計測するといいかも、という話

大変狭い見識に基づいた話なので恐縮なのですが。。。

今、研究室でテキスト系の機械学習の勉強をしています。その過程で、LDAという生成モデルの実装をしています。

LDAの話は一旦無視して、テキストマイニング(に限らないかもしれませんが)の特徴として、とにかく扱うデータ量が大きいです。数学的に言えば、要素数のウルトラ長いベクトルです。5TH DIMENSION なんて目じゃないです。長いです。そのため、小さな遅延が、10万倍とかされます。

そしてもう一つ、構造化がされてたりされてなかったりします。どういうことかというと、あるウェブサイト(通常は文書と言います)は単語を複数持っています(a document has many words)。そして、複数のウェブサイト(文書)を集めて学習します。つまり、複数の文書があり、各文書は複数の単語列を持ちます。そういった構造化された配列だとわかりやすいような気もするのですが、そうしない方が良いこともあります。文書の持つ単語数が違うために行列にならなかったり(デコボコした2次元配列はnumpyで扱えないor扱うべきでない)、そもそも元の論文でそのように扱っていない場合などです。

そういう場合は、配列を潰して扱います。英語やRuby語で言うとflattenです。

[[1,2,3,4,5],[6,7,8]].flatten # => [1, 2, 3, 4, 5, 6, 7]

さて、ここからが本題です。長いベクトルを扱うという前提のもと、配列を潰さなければならないとします。 元もとreduceという関数を使って

reduce(lambda a, b: a + b, [[1,2,3,4,5],[6,7,8],[9,10,11,12]])

としていました。このコードやreduceの意味するところは、配列の各要素をa, b, c, ...と呼ぶと、

result = a # [1,2,3,4,5]
result += b # [1,2,3,4,5] + [6,7,8] = [1,2,3,4,5,6,7,8]
result += c # [1,2,3,4,5,6,7,8] + [9,10,11,12] = [1,2,3,4,5,6,7,8,9,10,11,12]

と逐次的(というかiterative)にやっていくことだったりします。

糞みてえに遅かったので、冗長に、単純に、

result = []
docs = [[1,2,3,4,5],[6,7,8],[9,10,11,12]]
for doc in docs:
    for words in doc:
        result.append(word)

みたいなコードにしたところ、速度が大幅に改善しました(計測しろや)

冗長さを取り除いて、なるべく言語に備わった機能に頼ってみたのですが、そうしない方がよかったみたいです。 どちらの書き方が美しいのかは存じ上げません。(追記:@kidomah君が教えてくれましたが、reduceはPy3からポアされました)

しかし、LLは低レイヤーの実装が見えてこない分、こういった遅延を発生させてしまいがちで、 機械学習のように大型なベクトルを扱って、繰り返しがボトルネックとなるような場合には、実際の影響として現れてくるのかなーと、考えてました。 間違ってたらごめんなさい。

追記1

タイトルでPythonと書いたのは、Pythonistaクラスタホイホイしたかったことと、自分の知っているLL内での対比です。Pythonは余分なメソッドをクラスに付けていかないsimplicityがあるので、実装の冗長性・リファクタリングしたさとの争いが現実的に発生しやすいのかなあ、と考えました。Rubyであればflattenで1行に収束するのに、という対比ですね。

追記2

流行っているものに対する「○○は糞だ」みたいな言説は、たいていは実装者の理解の乏しさにあるかと思います。言語に対する、「こうあってほしい」感が、その言語の哲学とそぐわなかったりするのだと思います。 実際、@kidomah君が「配列の結合は+演算子だと新たな配列を生成してしまうのでappendの方が効率がいい」「reduceは確かあまり推奨されていな」い、と教えてくれました。自分の「1行で書きたい感」と「Pythonic」は、多分にミスマッチしていることは、否定出来ません。

正直言って、配列の和の繰り返しは間違いなく重くなるのは当たり前で、言い訳をすると普段から繰り返し構文で文字列を足したりなんてしません。しかし現象としてそんなことをしてしまったので、どうしたら自分は(もしくは初心者が)暗黙的に重くなるような処理を実装せずに、リファクタリングしていけたんだろう、ということはとても悩ましいです。アドバイスをいただけたら幸いです。