世界を動かす技術を、日本語で。

NumPyが好きではありません

2025年5月16日原文(dynomight.net)

概要

  • NumPy は配列計算に便利だが、複雑な操作になると扱いが難しくなる問題を抱えていることを論じる内容。
  • ブロードキャストインデクシング の不透明さが、コードの可読性や予測性を損なう要因として指摘されている。
  • np.einsum のような例外的に明快な機能もあるが、全体的には直感的な設計になっていないと批判。
  • 関数の挙動 や多次元配列の扱いが一貫していないことが、学習コストやバグの温床になっている。
  • 理想的な配列言語 への要望と、NumPyの設計上の「原罪」について考察されている。

NumPyへの愛憎とその課題

NumPyの魅力と限界

  • NumPy はPythonで配列計算を行うためのソフトウェアであり、 PyTorch などの機械学習ライブラリにも大きな影響を与えていることを確認。
  • 2次元までの配列計算は直感的かつ簡単に記述できるが、 高次元配列 や複雑な操作になると急激に分かりづらくなることを実感。
  • 例えば、 複数の5×5行列 (100個)と 複数の長さ5ベクトル (100個)を同時に解く場合、ループを使えば明快だが、NumPy流の「ベクトル化」ではどの記法が正しいか分からなくなる現象を指摘。
  • np.linalg.solve のような関数の使い方が直感的でなく、ドキュメントを読んでも正確な方法が分かりづらいことを問題視。

配列操作の複雑さ

  • 3次元以上の配列同士の演算では、 次元合わせ(dimension alignment)ブロードキャスト のために、Noneやreshape、transposeを多用する必要があり、可読性が著しく低下することを批判。
    • 例:A(K×L×M)、B(L×N)、C(K×M)の場合にDkn = 1/(LM) × ∑lm Aklm Bln Ckmを計算するには、複雑な次元操作やtensordot、einsumなどを駆使する必要があることを確認。
  • np.einsum は例外的に明快で強力だが、独自のインデックス記法によるドメイン固有言語を導入しているため、NumPyの標準的な設計とは異なることを強調。

ブロードキャストの功罪

  • ブロードキャスト は一見便利だが、次元合わせのための冗長な記述や、演算の意味がコードから直感的に読み取れない問題を引き起こすことを指摘。
  • A*Bのような単純な記述でも、内部でどのブロードキャスト規則が適用されているかを常に意識する必要があり、可読性・予測性が損なわれることを強調。

インデクシングの罠

  • NumPyの高度な インデクシング は挙動が複雑で、配列の形状(shape)が直感的に予測できないことを多くの例で示す。
  • 実際にAIモデルに形状を答えさせても、正答率は低く、学習コストが高いことを証明。
  • シンプルなインデクシングを心掛けても、どこで複雑な挙動が紛れ込むか分からず、コードの安全性・可読性が低下することを指摘。

NumPy関数設計の一貫性欠如

  • np.linalg.solve などの関数設計が一貫しておらず、axesパラメータやバリエーション関数、独自の内部規則(Conventions)などが混在していることを批判。
    • どの関数がどの次元に対して動作するか、ユーザーが都度調べて合わせる必要があることを問題視。
  • 新たな関数を自作した場合、NumPyが持つ「Einstein記法」的な柔軟性を持たせることができず、コードの再利用性や拡張性が低いことを指摘。

理想的な配列言語への提案

明快さと直感性への要求

  • 理想的な配列言語とは、「やりたいことが 明らか に書けて、コードを読んだ時に 何をしているか明白 であること」が重要であると提案。
  • NumPyは「インデックス」を隠し、 ブロードキャスト に頼った設計が根本的な問題(原罪)であると考察。

まとめと提案

  • NumPyは配列計算の標準として普及したが、 高次元配列複雑な操作 になると、設計の一貫性や直感性が失われることを再確認。
  • np.einsum のようなインデックスベースの記法が、より明快で強力な設計思想のヒントになることを示唆。
  • 配列言語の今後の発展には、「明快さ」「直感性」「一貫性」を重視した設計が不可欠であると提案。

Hackerたちの意見

そうそう!この不満のいくつかはnumpysaneで解決されてるよね: https://github.com/dkogan/numpysane/ 。numpysaneとgnuplotlibのおかげで、今はnumpyが使いやすくなって、いろんなことにガンガン使ってるよ。でも、これがなかったら使えないよね。

リンクありがとう!私もこの問題について文句を言ってたけど、numpysaneみたいなワークアラウンドライブラリを探そうとは思わなかったな…

numpysaneのほとんどはPythonでループを回してるみたいだね。本当のベクトル化じゃない。

著者が言ってることは確かに一理あるね。MatlabからNumpyに移行する時、いろいろ小さな不満があった気がする。データのスライスはNumpyの方がMatlabやJuliaよりもやりにくい感じがするけど、それがMatlabの統計や信号処理ツールボックスのライセンス料を正当化するわけじゃないよね。この記事で取り上げられている問題は、主にランクが2より大きいテンソルに関するものだね。Numpyは元々行列だから、この領域で問題があるのは驚くことじゃないよね。Torchみたいな専用ライブラリの方が確実に良いけど、Torchにも独自の難しさがあるし。うーん、著者の結論「numpyは他の配列言語を除けば最悪の配列言語」ってのは正しい気がする。もしかしたら、私の想像力不足かも。

Numpyは最初からN次元配列に対応してたんだ。これはnumarrayの続きで、Ndだったんだよね。

Matlab(とある程度Julia)と比べると、私のNumpyに対する不満はこの2つの段落にまとめられるかな:> いくつかの関数には軸の引数がある。いくつかは異なる名前の異なるバージョンがある。いくつかには慣例がある。いくつかには慣例と軸の引数がある。そして、いくつかは全くベクトル化されたバージョンを提供しない。> でも、NumPyの最大の欠点はこれだ:ある形の配列で問題を解決する関数を作ったとする。じゃあ、その関数を大きな配列の特定の次元にどう適用するの?答えは:もっと複雑な方法で関数を一から書き直すこと。プログラミングの基本原則は抽象化—単純な問題を解決して、その解決策を使ってより複雑な問題を解決することなんだけど、NumPyはそれを許してくれない。通常、Matlabのコードを書くと、ベクトル化されたバージョンはそのまま動くし、必要な変更もかなり小さくて直感的なんだけど、numpysaneだと、どの関数でもドキュメントを調べなきゃいけない気がするし、配列をその関数が期待する形に転置したりリシェイプしたりしなきゃいけない。あまり一貫性がないよね。

2つ目の問題について、私が正しく理解していれば、jaxのvmapを試してみるといいかもしれないよ。

Matlabの配列における2次元以上のサポートはひどくて、TFAで嘆かれている状況に出くわすことはめったにないよ。

reshape

さて、どうやって大きな配列の特定の次元に適用するの?ここでの不満は具体的に何?2x2の配列に関数を書いたら、3x2x2の配列のタイルに適用する方法がないってこと?でも、スライスして絞ればできるよ。何が問題なの?編集: もし問題が「なぜ任意のインデックスのセットに関数を適用できないのか」っていう変なバージョンなら(2x2x2の配列の0,1みたいな)、言葉が出てこないよ(だってそれは全く違う形だから - 実際には全体の配列だし)。

Numpyに対する私の主な問題は、どの操作がベクトル化されるのか、またはどのようにベクトル化されるのかがしばしば不明確で、Juliaのドット構文のように明示的にできないことだね。さまざまな関数や操作から返される型に関する落とし穴もたくさんあるし。特にひどい例として、長い間、単変数多項式オブジェクトのクラスはnp.poly1dだった。多項式に対する通常の操作を行うための便利な機能がたくさんあったんだ。もしpoly1dオブジェクトPを右から複素数スカラーz0で掛けると、予想通りの結果が得られる:z0でスケーリングされた係数のpoly1dが得られる。でも、左からz0で掛けると、スケーリングされた係数の配列が返ってくる—ここで静かな型変換が起こっているんだ。だから、Pz0 # はpoly1dを返し、z0P # は配列を返す。これはPythonの結合則とデータ型に対する放任主義のせいだと分かってるけど、こういうのをデバッグするのは本当に厄介だよ!poly1dのもう一つの面白い落とし穴:二次の先頭係数にアクセスしたい場合、P.coef[0]でもP[2]でもできる。誰もこれを混同しないよね?これらの具体的な例はあまり公平じゃないけど、numpysaneのドキュメントはpoly1dを「古い」多項式APIの一部として説明していて、新しいコードはPolynomial APIで書くように勧めてるんだ—ただし、公式に非推奨になっているわけではなく、poly1dを使っても警告は出ないみたい。とにかく、ライブラリ全体に似たような欠点があるよ。静かな型変換や、同じメソッドから返される不一致なデータ型に関する落とし穴がたくさんあって、デバッグするのが本当に悪夢みたい。

これは、ダンダーオペレーターのメソッドに関するディスパッチセマンティクスのせいなんだよね。Pythonのドキュメントはそれをうまく伝えてないし。君が説明した通りの型が返ってくるのは、まさに俺が期待してた通りで、Pythonらしいよ。これはnumpyの問題じゃないね。

Hacker Newsで議論の続きを見る