Почему np.dot неточен? (n-dim массивы) - PullRequest
13 голосов
/ 07 ноября 2019

Предположим, мы берем 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]

Числа. Кроме того, они могут измениться:


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

Результаты отличаются, даже если напечатанный срез получен източно такие же числа умножены.


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

CASE 3 : более сильный контроль;установите все не вовлеченные цели в ноль : добавьте a[1:] = 0 к коду 1. Результат: расхождения сохраняются.


CASE 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, IDE Spyder 3.3.6, Anaconda 3.0 2019/10
  • CPU : i7-7700HQ 2,8 ГГц
  • 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]))

Серьезность проблемы : показанные расхождения являются «небольшими», но больше не так при работе в нейронной сети с миллиардами чиселумножается на несколько секунд и триллионы за все время выполнения;Сообщаемая точность модели отличается на целые 10 процентов, по этой теме .

Ниже приведен gif массивов, полученных в результате подачи к модели, которая в основном a[0], w / len(a)==1 против len(a)==32:

image


ДРУГИЕ ПЛАТФОРМЫ результаты, в соответствии с результатами Пола :

Воспроизведен случай 1 (частично) :

  • Google Colab VM - Intel 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

Примечание : они дают намного меньшую ошибку, чем показано выше;две записи в первой строке отключены на 1 в младшей значащей цифре из соответствующих записей в других строках.

Случай 1 не воспроизводится :

  • Ubuntu18.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 (пока) никогда не было неточности.
  • В этом очень ограниченном примере текущая (докеризованная) среда Jupyter более восприимчива, чем среда IPython.
  • np.show_config() слишком долго, чтобы публиковать здесь - но очень скучно в средах ipython (на основе blas / lapack) и на основе openblas на Google Colab;В средах ipython Linux библиотеки blas устанавливаются системой, тогда как в средах jupyter и colab они берутся из / opt / conda / lib

1 Ответ

4 голосов
/ 12 ноября 2019

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

...