Perché np.dot non è impreciso? (array n-dim)


15

Supponiamo di prendere np.dotdue 'float32'array 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]

Numeri. Tranne che possono cambiare:


CASO 1 : fettaa

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]

I risultati differiscono, anche se la sezione stampata deriva dagli stessi stessi numeri moltiplicati.


CASO 2 : appiattire a, prendere una versione 1D di b, quindi tagliare 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]

CASO 3 : controllo più forte; imposta a zero tutti gli enti non coinvolti : aggiungi a[1:] = 0al codice CASE 1. Risultato: persistono discrepanze.


CASO 4 : controllare indici diversi da [0]; come per [0], i risultati iniziano a stabilizzare un numero fisso di ingrandimenti di array dal loro punto di creazione. Produzione

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

Quindi, per il caso 2D * 2D, i risultati differiscono, ma sono coerenti per 1D * 1D. Da alcune delle mie letture, questo sembra derivare dalla 1D-1D usando una semplice aggiunta, mentre il 2D-2D usa un'aggiunta più "fantasiosa", che può essere meno precisa (ad esempio, l'aggiunta a coppie fa il contrario). Tuttavia, non sono in grado di capire perché le discrepanze scompaiano nel caso in cui una volta avenga superata una "soglia" fissa; più grande ae b, più tardi questa soglia sembra mentire, ma esiste sempre.

Tutto detto: perché è np.dotimpreciso (e incoerente) per gli array ND-ND? Git pertinente


Informazioni aggiuntive :

  • Ambiente : sistema operativo Win-10, Python 3.7.4, Spyder 3.3.6 IDE, Anaconda 3.0 2019/10
  • CPU : i7-7700HQ 2,8 GHz
  • Numpy v1.16.5

Possibile libreria di colpevoli : Numpy MKL - anche librerie BLASS; grazie a Bi Rico per aver notato


Codice del test da sforzo : come notato, le discrepanze si aggravano in frequenza con array più grandi; se sopra non è riproducibile, sotto dovrebbe essere (in caso contrario, provare dim più grandi). La mia uscita

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]))

Gravità del problema : le discrepanze mostrate sono "piccole", ma non lo sono più quando si opera su una rete neurale con miliardi di numeri moltiplicati per alcuni secondi e trilioni durante l'intero tempo di esecuzione; l'accuratezza del modello riportata differisce per interi 10 di percentuali, per questa discussione .

Di seguito è riportato un gif di array risultante dall'alimentazione a un modello di cosa è sostanzialmente a[0], w / len(a)==1vs len(a)==32.:


ALTRI PIATTAFORME risultati, secondo e con grazie ai test di Paul :

Caso 1 riprodotto (in parte) :

  • Google Colab VM - Intel Xeon 2.3 G-Hz - Jupyter - Python 3.6.8
  • Desktop Docker Win-10 Pro - 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

Nota : questi producono errori molto più bassi di quelli mostrati sopra; due voci nella prima riga sono disattivate di 1 nella cifra meno significativa dalle voci corrispondenti nelle altre righe.

Caso 1 non riprodotto :

  • Ubuntu 18.04.3 LTS - Intel i7-8700K - IPython 5.5.0 - Python 2.7.15+ e 3.6.8 (2 test)
  • 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

Note :

  • Il notebook Colab collegato e gli ambienti jupyter mostrano una discrepanza molto minore (e solo per le prime due righe) rispetto a quella osservata sul mio sistema. Inoltre, il caso 2 non ha mai (ancora) mostrato imprecisione.
  • All'interno di questo campione molto limitato, l'attuale ambiente Jupyter (Dockerized) è più sensibile dell'ambiente IPython.
  • np.show_config()troppo lungo per essere pubblicato, ma in sintesi: gli envs di IPython sono basati su BLAS / LAPACK; Colab è basato su OpenBLAS. In IPython Linux envs, le librerie BLAS sono installate dal sistema - in Jupyter e Colab provengono da / opt / conda / lib

AGGIORNAMENTO : la risposta accettata è accurata, ma ampia e incompleta. La domanda rimane aperta a chiunque sia in grado di spiegare il comportamento a livello di codice - vale a dire, un algoritmo esatto utilizzato da np.dote come spiega "incoerenze coerenti" osservate nei risultati precedenti (vedere anche i commenti). Ecco alcune implementazioni dirette oltre la mia decifrazione: sdot.c - arraytypes.c.src


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Samuel Liew

Gli algoritmi generali di ndarrayssolito ignorano la perdita di precisione numerica. Perché per semplicità loro reduce-sumlungo ciascun asse, l'ordine delle operazioni potrebbe non essere quello ottimale ... Nota che se ti dispiace errore di precisione potresti anche usarefloat64
Vitor SRG

Potrei non avere il tempo di rivedere domani, quindi assegnando la taglia ora.
Paul,

@Paul Verrà assegnato automaticamente alla risposta più votata comunque - ma va bene, grazie per
averlo

Risposte:


7

Sembra inevitabile imprecisione numerica. Come spiegato qui , NumPy utilizza un metodo BLAS altamente ottimizzato e accuratamente calibrato per la moltiplicazione delle matrici . Ciò significa che probabilmente la sequenza di operazioni (somma e prodotti) seguita per moltiplicare 2 matrici, cambia quando cambia la dimensione della matrice.

Cercando di essere più chiari, sappiamo che, matematicamente , ogni elemento della matrice risultante può essere calcolato come il prodotto punto di due vettori (sequenze di numeri di uguale lunghezza). Ma non è così che NumPy calcola un elemento della matrice risultante. Esistono infatti algoritmi più efficienti ma complessi, come l' algoritmo Strassen , che ottengono lo stesso risultato senza calcolare direttamente il prodotto punto riga-colonna.

Quando si utilizzano tali algoritmi, anche se l'elemento C ij di una matrice risultante C = AB è matematicamente definito come il prodotto punto dell'i-esima riga di A con il j-esima colonna di B , se si moltiplica una matrice A2 avente il stessa i-esima riga di A con una matrice B2 avente la stessa j-esima colonna di B , l'elemento C2 ij verrà effettivamente calcolato seguendo una diversa sequenza di operazioni (che dipende dall'intero A2 e B2 matrici), con possibili errori numerici diversi.

Ecco perché, anche se matematicamente C ij = C2 ij (come nel CASO 1), la diversa sequenza di operazioni seguita dall'algoritmo nei calcoli (a causa della modifica delle dimensioni della matrice) porta a diversi errori numerici. L'errore numerico spiega anche i risultati leggermente diversi a seconda dell'ambiente e del fatto che, in alcuni casi, per alcuni ambienti, l'errore numerico potrebbe essere assente.


2
Grazie per il link, sembra che contenga informazioni pertinenti - la tua risposta, tuttavia, potrebbe essere più dettagliata, poiché finora è una parafrasi di commenti sotto la domanda. Ad esempio, il SO collegato mostra il Ccodice diretto e fornisce spiegazioni a livello di algoritmo, quindi sta andando nella giusta direzione.
OverLordGoldDragon

Inoltre non è "inevitabile", come mostrato in fondo alla domanda - e l'entità dell'imprecisione varia a seconda degli ambienti, il che rimane inspiegabile
OverLordGoldDragon

1
@OverLordGoldDragon: (1) Un esempio banale con aggiunta: prendi il numero n, prendi il numero in kmodo che sia inferiore alla precisione kdell'ultima cifra della mantissa. Per i float nativi di Python n = 1.0e k = 1e-16funziona. Ora lascia ks = [k] * 100. Vedi che sum([n] + ks) == n, mentre sum(ks + [n]) > n, cioè, l'ordine di somma contava. (2) Le CPU moderne dispongono di più unità per eseguire operazioni in virgola mobile (FP) in parallelo e l'ordine in cui a + b + c + dviene calcolato su una CPU non è definito, anche se il comando a + bprecede c + dnel codice macchina.
9000,

1
@OverLordGoldDragon Dovresti essere consapevole che la maggior parte dei numeri che chiedi al tuo programma di trattare non può essere rappresentata esattamente da un punto mobile. Prova format(0.01, '.30f'). Se anche un semplice numero come 0.01non può essere rappresentato esattamente da un punto mobile NumPy, non è necessario conoscere i dettagli profondi dell'algoritmo di moltiplicazione della matrice NumPy per comprendere il punto della mia risposta; ovvero matrici di partenza diverse portano a diverse sequenze di operazioni , quindi i risultati matematicamente uguali possono differire di una piccola quantità a causa di errori numerici.
mmj,

2
@OverLordGoldDragon re: magia nera. C'è un articolo che è necessario leggere in un paio di MOOC CS. Il mio ricordo non è eccezionale, ma penso che sia così: itu.dk/~sestoft/bachelor/IEEE754_article.pdf
Paul
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.