Вот пользовательская функция, которая позволяет переходить через десятичные приращения:

def my_range(start, stop, step):
    i = start
    while i < stop:
        yield i
        i += step

Это работает так:

out = list(my_range(0, 1, 0.1))
print(out)

[0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6, 0.7, 0.7999999999999999, 0.8999999999999999, 0.9999999999999999]

Теперь нет ничего удивительного в этом. Понятно, что это происходит из-за неточностей с плавающей запятой и что 0.1 не имеет точного представления в памяти. Итак, эти ошибки точности понятны.

Возьмите numpy с другой стороны:

import numpy as np

out = np.arange(0, 1, 0.1)
print(out)
array([ 0. ,  0.1,  0.2,  0.3,  0.4,  0.5,  0.6,  0.7,  0.8,  0.9]) 

Интересно то, что здесь нет видимых неточностей неточности. Я подумал, что это может быть связано с тем, что показывает __repr__, поэтому для подтверждения я попробовал это:

x = list(my_range(0, 1.1, 0.1))[-1]
print(x.is_integer())

False

x = list(np.arange(0, 1.1, 0.1))[-1]
print(x.is_integer())

True

Итак, моя функция возвращает неверное верхнее значение (оно должно быть 1.0, но на самом деле это 1.0999999999999999), но np.arange делает это правильно.

Мне известно о математике с плавающей запятой? но смысл этого вопроса :

Как NumPy делает это?

8
cs95 27 Авг 2017 в 19:34

3 ответа

Лучший ответ

Разница в конечных точках заключается в том, что NumPy вычисляет длину впереди, а не ad hoc, потому что ей нужно предварительно выделить массив. Вы можете увидеть это в numpy.arange(0.0, 2.1, 0.3):

In [46]: numpy.arange(0.0, 2.1, 0.3)
Out[46]: array([ 0. ,  0.3,  0.6,  0.9,  1.2,  1.5,  1.8,  2.1])

Гораздо безопаснее использовать numpy.linspace, где вместо размера шага вы говорите, сколько элементов вы хотите и хотите ли вы включить правильную конечную точку.


Может показаться, что при вычислении элементов NumPy не испытывал ошибок округления, но это только из-за другой логики отображения. NumPy усекает отображаемую точность более агрессивно, чем float.__repr__. Если вы используете tolist для получения обычного списка обычных скаляров Python (и, следовательно, обычной float логики отображения), вы можете увидеть, что NumPy также столкнулся с ошибкой округления:

In [47]: numpy.arange(0, 1, 0.1).tolist()
Out[47]: 
[0.0,
 0.1,
 0.2,
 0.30000000000000004,
 0.4,
 0.5,
 0.6000000000000001,
 0.7000000000000001,
 0.8,
 0.9]

Он испытал немного различную ошибку округления - например, в .6 и .7 вместо .8 и .9 - потому что он также использует другие средства вычисления элементов, реализованные в <} { соответствующий тип.

Реализация функции fill имеет то преимущество, что использует start + i*step вместо многократного добавления шага, что позволяет избежать накопления ошибок при каждом добавлении. Однако у него есть недостаток, заключающийся в том, что (без видимой на то причины) он пересчитывает шаг из первых двух элементов вместо того, чтобы принять шаг в качестве аргумента, поэтому он может потерять большую точность на шаг впереди.

10
user2357112 supports Monica 27 Авг 2017 в 18:07

Хотя arange выполняет переход по диапазону немного по-другому, он все еще имеет проблему с представлением с плавающей точкой:

In [1358]: np.arange(0,1,0.1)
Out[1358]: array([ 0. ,  0.1,  0.2,  0.3,  0.4,  0.5,  0.6,  0.7,  0.8,  0.9])

Печать скрывает это; преобразовать его в список, чтобы увидеть кровавые подробности:

In [1359]: np.arange(0,1,0.1).tolist()
Out[1359]: 
[0.0,
 0.1,
 0.2,
 0.30000000000000004,
 0.4,
 0.5,
 0.6000000000000001,
 0.7000000000000001,
 0.8,
 0.9]

Или с другой итерацией

In [1360]: [i for i in np.arange(0,1,0.1)]  # e.g. list(np.arange(...))
Out[1360]: 
[0.0,
 0.10000000000000001,
 0.20000000000000001,
 0.30000000000000004,
 0.40000000000000002,
 0.5,
 0.60000000000000009,
 0.70000000000000007,
 0.80000000000000004,
 0.90000000000000002]

В этом случае каждый отображаемый элемент представляет собой np.float64, где, как и в первом, каждый представляет собой float.

6
hpaulj 27 Авг 2017 в 16:58

Помимо различного представления списков и массивов NumPys arange работает путем умножения вместо повторного добавления. Это больше похоже на:

def my_range2(start, stop, step):
    i = 0
    while start+(i*step) < stop:
        yield start+(i*step)
        i += 1

Тогда вывод полностью равен:

>>> np.arange(0, 1, 0.1).tolist() == list(my_range2(0, 1, 0.1))
True

При повторном добавлении вы «накапливаете» ошибки округления с плавающей запятой. На умножение все еще влияет округление, но ошибка не накапливается.


Как указано в комментариях, это не совсем то, что происходит. Насколько я вижу, это больше похоже на:

def my_range2(start, stop, step):
    length = math.ceil((stop-start)/step)
    # The next two lines are mostly so the function really behaves like NumPy does
    # Remove them to get better accuracy...
    next = start + step
    step = next - start
    for i in range(length):
        yield start+(i*step)

Но не уверен, что это правильно, потому что в NumPy происходит гораздо больше.

5
MSeifert 27 Авг 2017 в 17:56