np.dotが不正確なのはなぜですか?(n次元配列)


15

np.dot2つの'float32'2D配列を取得するとします。

res = np.dot(a, b)   # see CASE 1
print(list(res[0]))  # list shows more digits
[-0.90448684, -1.1708503, 0.907136, 3.5594249, 1.1374011, -1.3826287]

数字。例外として、次のものを変更できます。


ケース1:スライスa

np.random.seed(1)
a = np.random.randn(9, 6).astype('float32')
b = np.random.randn(6, 6).astype('float32')

for i in range(1, len(a)):
    print(list(np.dot(a[:i], b)[0])) # full shape: (i, 6)
[-0.9044868,  -1.1708502, 0.90713596, 3.5594249, 1.1374012, -1.3826287]
[-0.90448684, -1.1708503, 0.9071359,  3.5594249, 1.1374011, -1.3826288]
[-0.90448684, -1.1708503, 0.9071359,  3.5594249, 1.1374011, -1.3826288]
[-0.90448684, -1.1708503, 0.907136,   3.5594249, 1.1374011, -1.3826287]
[-0.90448684, -1.1708503, 0.907136,   3.5594249, 1.1374011, -1.3826287]
[-0.90448684, -1.1708503, 0.907136,   3.5594249, 1.1374011, -1.3826287]
[-0.90448684, -1.1708503, 0.907136,   3.5594249, 1.1374011, -1.3826287]
[-0.90448684, -1.1708503, 0.907136,   3.5594249, 1.1374011, -1.3826287]

印刷されたスライスは、まったく同じ数を掛けたものから派生したものですが、結果は異なります。


ケース2aの1Dバージョンを平坦化しb次にスライスしますa

np.random.seed(1)
a = np.random.randn(9, 6).astype('float32')
b = np.random.randn(1, 6).astype('float32')

for i in range(1, len(a)):
    a_flat = np.expand_dims(a[:i].flatten(), -1) # keep 2D
    print(list(np.dot(a_flat, b)[0])) # full shape: (i*6, 6)
[-0.3393164, 0.9528787, 1.3627989, 1.5124314, 0.46389243, 1.437775]
[-0.3393164, 0.9528787, 1.3627989, 1.5124314, 0.46389243, 1.437775]
[-0.3393164, 0.9528787, 1.3627989, 1.5124314, 0.46389243, 1.437775]
[-0.3393164, 0.9528787, 1.3627989, 1.5124314, 0.46389243, 1.437775]
[-0.3393164, 0.9528787, 1.3627989, 1.5124314, 0.46389243, 1.437775]
[-0.3393164, 0.9528787, 1.3627989, 1.5124314, 0.46389243, 1.437775]
[-0.3393164, 0.9528787, 1.3627989, 1.5124314, 0.46389243, 1.437775]
[-0.3393164, 0.9528787, 1.3627989, 1.5124314, 0.46389243, 1.437775]

ケース3:より強力な制御。関与しないすべてのゼロ0に設定します。CASE1 a[1:] = 0コードに追加します。結果:矛盾が持続します。


ケース4:以外のインデックスをチェックします[0]。の場合と同様に[0]、結果は作成時点から一定数の配列拡大を安定させ始めます。出力

np.random.seed(1)
a = np.random.randn(9, 6).astype('float32')
b = np.random.randn(6, 6).astype('float32')

for j in range(len(a) - 2):
    for i in range(1, len(a)):
        res = np.dot(a[:i], b)
        try:    print(list(res[j]))
        except: pass
    print()

したがって、2D * 2Dの場合、結果は異なりますが、1D * 1Dでは一貫しています。私のいくつかのリーディングから、これは単純な加算を使用した1D-1Dから生じているように見えますが、2D-2Dは「より洗練された」パフォーマンスを高める加算を使用します。それにもかかわらず、1がaセットの「しきい値」を超えてスライスされると、なぜ差異が消えるのか理解できません。とが大きいほどabこのしきい値は後であるように見えますが、常に存在します。

すべてが言った:np.dotND-NDアレイに対してなぜ不正確(そして矛盾)なのか?関連Git


追加情報

  • 環境:Win-10 OS、Python 3.7.4、Spyder 3.3.6 IDE、Anaconda 3.0 2019/10
  • CPUi7-7700HQ 2.8 GHz
  • Numpy v1.16.5

考えられる原因ライブラリ:Numpy MKL -BLASSライブラリ。注目してくれたBi Ricoに感謝


ストレステストコード:前述のように、不一致は、より大きな配列での頻度で悪化します。上記が再現可能でない場合は、以下を再現する必要があります(そうでない場合は、より大きな寸法を試してください)。私の出力

np.random.seed(1)
a = (0.01*np.random.randn(9, 9999)).astype('float32') # first multiply then type-cast
b = (0.01*np.random.randn(9999, 6)).astype('float32') # *0.01 to bound mults to < 1

for i in range(1, len(a)):
    print(list(np.dot(a[:i], b)[0]))

問題の重大度:表示された差異は「小さい」ものですが、数十億の数が数秒で乗算され、ランタイム全体で何兆もあるニューラルネットワークで動作している場合は、もはやそうではありません。報告されたモデルの精度は、このスレッドごとに、全体の数十パーセントの違いがあります

以下は基本的に何モデルへの供給に起因する配列のGIFですa[0]/ wは、len(a)==1対はlen(a)==32


Paulのテストによると他のプラットフォームの結果:

ケース1の複製(一部)

  • Google Colab VM-インテルXeon 2.3 G-Hz-Jupyter-Python 3.6.8
  • Win-10 Pro Dockerデスクトップ-Intel i7-8700K-jupyter / scipy-notebook-Python 3.7.3
  • Ubuntu 18.04.2 LTS + Docker-AMD FX-8150-jupyter / scipy-notebook-Python 3.7.3

:これらのエラーは上記のエラーよりもはるかに低くなります。最初の行の2つのエントリは、他の行の対応するエントリから最下位桁が1ずれています。

再現されていないケース1

  • Ubuntu 18.04.3 LTS-Intel i7-8700K-IPython 5.5.0-Python 2.7.15+および3.6.8(2テスト)
  • Ubuntu 18.04.3 LTS-Intel i5-3320M-IPython 5.5.0-Python 2.7.15+
  • Ubuntu 18.04.2 LTS-AMD FX-8150-IPython 5.5.0-Python 2.7.15rc1

  • リンクコラボノートブックとjupyter環境は私のシステムで観察されるよりも(そして唯一の最初の2行分の)はるかに小さい不一致を示しています。また、ケース2では(まだ)不正確さが示されていません。
  • この非常に限られたサンプルの中で、現在の(Docker化された)Jupyter環境は、IPython環境よりも影響を受けやすくなっています。
  • np.show_config()投稿するには長すぎますが、要約すると、IPython envはBLAS / LAPACKベースです。ColabはOpenBLASベースです。IPython Linux環境では、BLASライブラリはシステムにインストールされます-JupyterおよびColabでは、/ opt / conda / libから取得されます

更新:受け入れられた答えは正確ですが、広く不完全です。コードレベルで動作を説明できる人、つまり、が使用する正確なアルゴリズムnp.dot、および上記の結果で観察された「一貫性のない矛盾」をどのように説明するかについては、疑問が残ります(コメントも参照)。ここに私の解読を超えたいくつかの直接的な実装があります:sdot.c - arraytypes.c.src


コメントは詳細な議論のためのものではありません。この会話はチャットに移動さました
Samuel Liew

ndarrays通常、数値精度の低下を無視するための一般的なアルゴリズム。単純にreduce-sumするために、各軸に沿ってそれらを実行するため、操作の順序は最適な順序ではない可能性があります...精度エラーを気にする場合は、同様に使用できることに注意してくださいfloat64
Vitor SRG

明日レビューする時間がないので、賞金を授与します。
ポール

@Paulそれはとにかく最高の投票された答えに自動的に授与されます-しかし、
申し訳ありませんが

回答:


7

これは避けられない数値の不正確さのように見えます。ここで説明するようにNumPyは高度に最適化され、注意深く調整されたBLASメソッドを使用して行列を乗算します。これは、2つの行列を乗算するための一連の操作(合計と積)が、行列のサイズが変化すると変化することを意味します。

より明確にするために、数学的には、結果の行列の各要素は、2つのベクトル(同じ長さの数列)のドット積として計算できることがわかります。しかし、これはNumPyが結果の行列の要素を計算する方法ではありません。実際、Strassenアルゴリズムのように、行と列のドット積を直接計算せずに同じ結果を得る、より効率的で複雑なアルゴリズムがあります。

でも素子場合、このようなアルゴリズムを使用する場合のC ijは、得られたマトリックスのC = ABは、数学のドット積として定義されるi番目の列Aj番目の列B君乗算行列場合、A2が有しますBと同じj番目の列を持つ行列B2を持つAと同じi番目の行、要素C2 ijは実際には異なる演算シーケンスに従って計算されます(これはA2B2全体に依存します) 行列)、異なる数値エラーにつながる可能性があります。

そのため、数学的にC ij = C2 ij(CASE 1のように)であっても、計算でアルゴリズムが続くさまざまな操作シーケンス(行列サイズの変更による)により、異なる数値エラーが発生します。数値誤差は、環境によって多少異なる結果が生じること、および場合によっては、一部の環境では数値誤差がない場合があることも説明しています。


2
リンクのおかげで、関連情報が含まれているようです。ただし、これまでのところ、質問の下のコメントの言い換えであるので、あなたの回答はより詳細になる可能性があります。たとえば、リンクされたSOは直接的なCコードを示し、アルゴリズムレベルの説明を提供するため、正しい方向に向かっています。
OverLordGoldDragon

また、質問の最後に示されているように、「避けられない」わけではありません。不正確さの程度は環境によって異なりますが、説明はされていません
OverLordGoldDragon

1
@OverLordGoldDragon:(1)追加の簡単な例:数値nを取る、数値をの最後の仮数桁のk精度を下回るようにしますk。Pythonのネイティブfloatの場合n = 1.0k = 1e-16機能します。今すぐましょうks = [k] * 100。それを参照してくださいsum([n] + ks) == nながら、sum(ks + [n]) > n、つまり、加算順序が大事。(2)最近のCPUには、浮動小数点(FP)演算を並列で実行するいくつかのユニットa + b + c + dがあり、CPUで計算される順序は、コマンドa + bc + dマシンコードで前にあっても、定義されていません。
9000年

1
@OverLordGoldDragonプログラムで処理するように要求するほとんどの数値は、浮動小数点で正確に表すことができないことに注意してください。お試しくださいformat(0.01, '.30f')。のような単純な数値でも0.01NumPy浮動小数点で正確に表すことができない場合、私の回答の要点を理解するためにNumPy行列乗算アルゴリズムの詳細を知る必要はありません。つまり、開始行列異なると、演算のシーケンスも異なります。そのため、数学的に等しい結果は、数値誤差によりわずかに異なる場合があります。
mmj

2
@OverLordGoldDragon re:黒魔術。いくつかのCS MOOCで読む必要のある論文があります。私の思い出
Paul
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.