Чому np.dot неточний? (n-затемнені масиви)


15

Припустимо, ми беремо np.dotдва '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]

Результати відрізняються, навіть якщо надрукований фрагмент походить від точно збіжених чисел.


СЛУЧАЙ 2 : сплюстіть a, візьміть 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 : посилений контроль; встановіть усі нульові entires до нуля : додайте a[1:] = 0до CASE 1 коду. Результат: розбіжності зберігаються.


СЛУЧАЙ 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прорізається встановлений "поріг"; чим більший aі b, тим пізніше цей поріг, схоже, лежить, але він завжди існує.

Всі сказали: чому np.dotнеточні (і непослідовні) для масивів ND-ND? Відповідний Git


Додаткова інформація :

  • Навколишнє середовище : Win-10 OS, Python 3.7.4, Spyder 3.3.6 IDE, Anaconda 3.0 2019/10
  • Процесор : i7-7700HQ 2,8 ГГц
  • Numpy v1.16.5

Можлива бібліотека винуватця : Numpy MKL - також бібліотеки BLASS; дякую Бі-Ріко за те, що помітили


Код стрес-тесту : як зазначалося, розбіжності посилюються в частоті з / більшими масивами; якщо вище не відтворюється, нижче має бути (якщо ні, спробуйте більші дими). Мій вихід

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

Гострота проблеми : показані невідповідності "невеликі", але вже не так при роботі в нейронній мережі з мільйонами чисел, помножених на кілька секунд, і трильйонами протягом усього часу виконання; Точність звітної моделі відрізняється на ці 10 ниток на 10 відсотків .

Нижче наведено графік масивів, що виникає в результаті подачі на модель, що в основному a[0], з / len(a)==1в len(a)==32:


ІНШІ ПЛАТФОРМИ, результати та завдяки подякам Павла :

Справа 1 відтворена (частково) :

  • Google Colab VM - Intel Xeon 2.3 G-Hz - Jupyter - Python 3.6.8
  • Win-10 Pro Docker Desktop - Intel i7-8700K - jupyter / scipy-notebook - Python 3.7.3
  • Ubuntu 18.04.2 LTS + Docker - AMD FX-8150 - юпітер / scipy-ноутбук - Python 3.7.3

Примітка : вони дають набагато меншу помилку, ніж показано вище; два записи в першому рядку відключаються на 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

Примітки :

  • В пов'язаних Colab ноутбуків і jupyter середовищ показують набагато менше розбіжність (і тільки для перших двох рядків) , ніж спостерігається в моїй системі. Також Справа 2 ніколи (ще) не виявляла точності.
  • У цьому дуже обмеженому зразку поточне (докерізоване) середовище Юпітера є більш сприйнятливим, ніж середовище IPython.
  • np.show_config()занадто довго для публікації, але підсумовуючи: IPython envs засновані на BLAS / LAPACK; Colab заснований на OpenBLAS. У IPython Linux envs бібліотеки BLAS встановлені системою - у Jupyter та Colab вони надходять з / opt / conda / lib

ОНОВЛЕННЯ : прийнята відповідь точна, але широка і неповна. Питання залишається відкритим для всіх, хто може пояснити поведінку на рівні коду, а саме - точний алгоритм, який використовується np.dot, і як він пояснює "послідовні невідповідності", що спостерігаються у вищезазначених результатах (див. Також коментарі). Ось декілька прямих реалізацій поза моїм розшифровкою: sdot.c - arraytypes.c.src


Коментарі не для розширеного обговорення; ця розмова була переміщена до чату .
Самуель Liew

Загальні алгоритми ndarraysзазвичай ігнорують числові втрати точності. Оскільки для простоти вони reduce-sumвздовж кожної осі, порядок операцій може бути не оптимальним ... Зауважте, що якщо ви float64
Vitor SRG

Можливо, я не встигну завтра переглядати, тому нагороджуйте ще раз.
Павло

@Paul Буде присвоєно автоматично за відповідь з найбільшою оцінкою - але добре, дякую за повідомлення
OverLordGoldDragon

Відповіді:


7

Це виглядає як неминуча чисельна неточність. Як пояснено тут , NumPy використовує високооптимізований, ретельно налаштований метод BLAS для множення матриці . Це означає, що, ймовірно, послідовність операцій (сума і добутки), що слідують за множенням 2 матриць, змінюється, коли розмір матриці змінюється.

Намагаючись бути зрозумілішими, ми знаємо, що математично кожен елемент отриманої матриці можна обчислити як крапковий добуток двох векторів (однакові довжини послідовностей чисел). Але це не так, як NumPy обчислює елемент отриманої матриці. Фактично існують більш ефективні, але складні алгоритми, такі як алгоритм Страссена , які отримують той самий результат, не обчислюючи безпосередньо точковий добуток рядка-стовпця.

При використанні таких алгоритмів, навіть якщо елемент C ij результуючої матриці C = AB математично визначається як крапковий добуток i-го рядка A з j-м стовпцем B , якщо помножити матрицю A2, що має той самий i-й рядок, що і A з матрицею B2, що має той самий j-й стовпець, що і B , елемент C2 ij буде фактично обчислений після різної послідовності операцій (це залежить від цілого A2 і B2 матриці), можливо, що призводить до різних числових помилок.

Ось чому, навіть якщо математично C ij = C2 ij (як у вашому випадку 1), різна послідовність операцій, що послідує за алгоритмом в обчисленнях (через зміну розміру матриці), призводить до різних числових помилок. Числова помилка пояснює також дещо різні результати залежно від середовища та той факт, що в деяких випадках для деяких середовищ числова помилка може бути відсутнім.


2
Дякуємо за посилання, схоже, він містить відповідну інформацію - однак ваша відповідь може бути більш детальною, оскільки поки це перефразовування коментарів під питанням. Наприклад, пов'язаний SO показує прямий Cкод і надає пояснення на рівні алгоритму, тому він спрямовується в правильному напрямку.
OverLordGoldDragon

Це також не є "неминучим", як показано в нижній частині питання - і ступінь неточності змінюється в різних середовищах, що залишається нез'ясованим
OverLordGoldDragon

1
@OverLordGoldDragon: (1) Тривіальний приклад з додаванням: взяти число n, взяти число kтаким, щоб воно було нижче точності kостанньої цифри мантіси. Для Python рідний плаває, n = 1.0і k = 1e-16працює. Тепер нехай ks = [k] * 100. Дивіться, що в sum([n] + ks) == nтой час sum(ks + [n]) > n, тобто порядок підсумовування має значення. (2) Сучасні процесори мають кілька одиниць для виконання операцій з плаваючою комою (FP) паралельно, і порядок, в якому a + b + c + dобчислюється CPU, не визначається, навіть якщо команда a + bнадходить до c + dмашинного коду раніше.
9000

1
@OverLordGoldDragon Ви повинні пам’ятати, що більшість номерів, з якими ви запитуєте свою програму, не може бути точно представлена ​​плаваючою точкою. Спробуйте format(0.01, '.30f'). Якщо навіть просте число на зразок 0.01не може бути точно представлене плаваючою точкою NumPy, не потрібно знати глибокі деталі алгоритму множення матриці NumPy, щоб зрозуміти точку моєї відповіді; тобто різні стартові матриці призводять до різних послідовностей операцій , так що математично рівні результати можуть відрізнятися на невелику кількість через чисельні помилки.
mmj

2
@OverLordGoldDragon re: чорна магія. Там є документ, який потрібно прочитати в кількох CS MOOC. Моє відкликання не таке велике, але я думаю, це все: itu.dk/~sestoft/bachelor/IEEE754_article.pdf
Павло
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.