Предположим, мы берем 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
ndarrays
обычно игнорируют потерю точности чисел. Поскольку для простоты они расположеныreduce-sum
вдоль каждой оси, порядок операций может быть не оптимальным ... Обратите внимание, что если вы не возражаете против погрешности точности, вы также можете использоватьfloat64
Ответы:
Это выглядит как неизбежная числовая неточность. Как объясняется здесь , 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), различная последовательность операций, которой следует алгоритм в вычислениях (из-за изменения размера матрицы), приводит к различным численным ошибкам. Числовая ошибка объясняет также немного отличающиеся результаты в зависимости от среды и того факта, что в некоторых случаях для некоторых сред численная ошибка может отсутствовать.
источник
C
код и предоставляет объяснения на уровне алгоритма, поэтому он движется в правильном направлении.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
в машинном коде раньше .format(0.01, '.30f')
. Если даже такое простое число, как, например,0.01
не может быть представлено точно с плавающей точкой NumPy, нет необходимости знать глубокие детали алгоритма умножения матриц NumPy, чтобы понять смысл моего ответа; то есть разные стартовые матрицы приводят к разным последовательностям операций , так что математически равные результаты могут отличаться в незначительной степени из-за числовых ошибок.