Почему np.dot неточен? (n-dim массивы)

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 : более сильный контроль; установите все не вовлеченные цели в ноль : добавьте a[1:] = 0к коду 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, Python 3.7.4, IDE Spyder 3.3.6, 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 процентов, по этой теме .

Ниже приведен gif массивов, полученных в результате подачи к модели, что в основном 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 - Intel i7-8700K - ноутбук с jupyter / scipy - Python 3.7.3
  • Ubuntu 18.04.2 LTS + Docker - AMD FX-8150 - jupyter / scipy-notebook - 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 (пока) никогда не показывал неточность.
  • В этом очень ограниченном примере текущая (Dockerized) среда Jupyter более восприимчива, чем среда IPython.
  • np.show_config()слишком длинный для публикации, но в итоге: envs IPython основаны на BLAS / LAPACK; Colab основан на OpenBLAS. В IPython Linux envs библиотеки BLAS устанавливаются в системе - в Jupyter и Colab они берутся из / opt / conda / lib

ОБНОВЛЕНИЕ : принятый ответ является точным, но широким и неполным. Вопрос остается открытым для всех, кто может объяснить поведение на уровне кода, а именно, точный алгоритм, используемый им np.dot, и как он объясняет «непоследовательные несоответствия», наблюдаемые в приведенных выше результатах (также см. Комментарии). Вот несколько прямых реализаций за пределами моего расшифровки: sdot.c - arraytypes.c.src

OverLordGoldDragon
источник
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Самуэль Лью
Общие алгоритмы 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 (как в вашем CASE 1), различная последовательность операций, которой следует алгоритм в вычислениях (из-за изменения размера матрицы), приводит к различным численным ошибкам. Числовая ошибка объясняет также немного отличающиеся результаты в зависимости от среды и того факта, что в некоторых случаях для некоторых сред численная ошибка может отсутствовать.

MMJ
источник
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) Современные ЦП имеют несколько модулей для параллельного выполнения операций с плавающей запятой, и порядок, в котором они a + b + c + dвычисляются на ЦП, не определен, даже если команда a + bуказана c + dв машинном коде раньше .
9000
1
@OverLordGoldDragon Вы должны знать, что большинство чисел, с которыми вы обращаетесь к вашей программе, не может быть точно представлено плавающей точкой. Попробуй format(0.01, '.30f'). Если даже такое простое число, как, например, 0.01не может быть представлено точно с плавающей точкой NumPy, нет необходимости знать глубокие детали алгоритма умножения матриц NumPy, чтобы понять смысл моего ответа; то есть разные стартовые матрицы приводят к разным последовательностям операций , так что математически равные результаты могут отличаться в незначительной степени из-за числовых ошибок.
MMJ
2
@OverLordGoldDragon re: черная магия. Есть бумага, которую нужно прочитать в паре CS MOOC. Мои воспоминания не так уж хороши
Пол,