Быстрое вычисление расстояния хэмминга между двоичными массивами numpy

У меня есть два массива одинаковой длины, содержащие двоичные значения

import numpy as np
a=np.array([1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0])
b=np.array([1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1])

Я хочу вычислить расстояние Хэмминга между ними как можно быстрее, так как мне нужно сделать миллионы таких вычислений расстояния.

Вот простой, но медленный вариант (взято из Википедии):

%timeit sum(ch1 != ch2 for ch1, ch2 in zip(a, b))
10000 loops, best of 3: 79 us per loop

Я придумал более быстрые варианты, вдохновленные некоторыми ответами здесь о переполнении стека.

%timeit np.sum(np.bitwise_xor(a,b))
100000 loops, best of 3: 6.94 us per loop

%timeit len(np.bitwise_xor(a,b).nonzero()[0])
100000 loops, best of 3: 2.43 us per loop

Мне интересно, есть ли еще более быстрые способы вычислить это, возможно, используя cython?


person benbo    schedule 23.09.2015    source источник
comment
Совпадает ли длина массивов примеров a и b с длинами ваших реальных данных?   -  person Warren Weckesser    schedule 23.09.2015
comment
Вы вычисляете все попарные расстояния в массиве массивов или между двумя массивами массивов? Возможно, вы сможете использовать scipy.spatial.distance.cdist или scipy.spatial.distance.pdist   -  person user2034412    schedule 23.09.2015
comment
@WarrenWeckesser, они одного порядка, да. Они будут иметь длину от 20 до 100 в зависимости от настроек некоторых параметров.   -  person benbo    schedule 23.09.2015
comment
scipy / space / distance.py Hamming (u, v): ... return (u != v).mean(). См. Также битовый массив.   -  person denis    schedule 26.01.2016


Ответы (5)


Есть готовая функция numpy, которая бьет len((a != b).nonzero()[0]);)

np.count_nonzero(a!=b)
person yevgeniy    schedule 23.09.2015

По сравнению с 1,07 мкс для np.count_nonzero (a! = B) на моей платформе, gmpy2.hamdist снижает его примерно до 143 нс после преобразования каждого массива в mpz (целое число с множественной точностью):

import numpy as np
from gmpy2 import mpz, hamdist, pack

a = np.array([1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0])
b = np.array([1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1])

Основываясь на совете от @casevh, преобразование из одномерного массива единиц и нулей в объект gmpy2 mpz может быть выполнено достаточно эффективно с помощью gmpy2.pack (list (reverse (list (array))), 1).

# gmpy2.pack reverses bit order but that does not affect
# hamdist since both its arguments are reversed
ampz = pack(list(a),1) # takes about 4.29µs
bmpz = pack(list(b),1)

hamdist(ampz,bmpz)
Out[8]: 7

%timeit hamdist(ampz,bmpz)
10000000 loops, best of 3: 143 ns per loop

для относительного сравнения на моей платформе:

%timeit np.count_nonzero(a!=b)
1000000 loops, best of 3: 1.07 µs per loop

%timeit len((a != b).nonzero()[0])
1000000 loops, best of 3: 1.55 µs per loop

%timeit len(np.bitwise_xor(a,b).nonzero()[0])
1000000 loops, best of 3: 1.7 µs per loop

%timeit np.sum(np.bitwise_xor(a,b))
100000 loops, best of 3: 5.8 µs per loop   
person Community    schedule 23.09.2015
comment
Честно говоря, вам, вероятно, следует включить время, необходимое для преобразования входных массивов в формат mpz. - person Warren Weckesser; 23.09.2015
comment
Вы можете использовать gmpy2.pack(list(a),1) для преобразования массива numpy в mpz. Это быстрее, чем convert2mpz(). Если вы включите время преобразования, оно все равно будет медленнее, чем решения numpy. - person casevh; 23.09.2015
comment
@WarrenWeckesser: Я думал об этом и вроде как согласен. Что меня беспокоит, так это то, что данные numpy, очевидно, находятся в оптимальном формате для решения numpy, в то время как большинство алгоритмов расстояния Хэмминга в C, которые принимают какой-либо числовой ввод, работают на битовом уровне. Мне кажется, что серьезное отношение к вычислениям расстояния Хэмминга, которые работают хорошо, подразумевает отказ от использования массива для представления последовательности битов, поскольку это всего лишь одно число. Цель моего ответа - предоставить точку данных для простого снижения производительности на расстоянии с помощью достаточно хорошо написанного на C модуля Python. - person ; 23.09.2015
comment
@casevh: Спасибо за подсказку. Обнаружил необходимым использовать gmpy2.pack (list (reverse (list (a))), 1), который на моей платформе занимает около 5,47 мкс. - person ; 23.09.2015
comment
Вам действительно нужно использовать reversed(), если вы хотите создать тот же mpz, что и ваш исходный код. Однако расстояние Хэмминга не зависит от порядка битов (т. Е. От высокого к низкому или от низкого к высокому). Пока два массива имеют одинаковую длину, так что одинаковые позиции битов сравниваются друг с другом, расстояние Хэмминга будет одинаковым. - person casevh; 24.09.2015
comment
gmpy2 - это правильный путь. Время уменьшилось с 1,5 мкс на хэш до 0,21 мкс на хеш (211 нс). Это 7-кратное ускорение, всего 5,44 с на 27-метровом крэне. Рассчитано на массиве логических массивов 16x16 VS список python gmpy2 mpz, построенных из двоичного кода длиной 256. - person Bart; 07.04.2016
comment
Кто-нибудь знает, изменилось ли что-то с тех пор, как это было опубликовано? Когда я пытаюсь использовать pack после копирования и вставки импортов и определений a & b в этом посте, я получаю сообщение об ошибке: TypeError: pack () требует, чтобы элементы списка были положительными целыми числами ‹2 ^ n бит - person Daniel Crane; 04.04.2017
comment
У меня такая же проблема, что и у @DanielCrane - person Guido Mocha; 31.07.2019
comment
Для меня это сработало, изменив list(a) на a.tolist() - person Carlo Bono; 22.04.2020

Использование pythran здесь может принести дополнительную пользу:

$ cat hamm.py
#pythran export hamm(int[], int[])
from numpy import nonzero
def hamm(a,b):
    return len(nonzero(a != b)[0])

Для справки (без питрана):

$ python -m timeit -s 'import numpy as np; a = np.random.randint(0,2, 100); b = np.random.randint(0,2, 100); from hamm import hamm' 'hamm(a,b)'
100000 loops, best of 3: 4.66 usec per loop

Хотя после компиляции pythran:

$ python -m pythran.run hamm.py
$ python -m timeit -s 'import numpy as np; a = np.random.randint(0,2, 100); b = np.random.randint(0,2, 100); from hamm import hamm' 'hamm(a,b)'
1000000 loops, best of 3: 0.745 usec per loop

Это примерно на 6x ускорение по сравнению с реализацией numpy, поскольку pythran пропускает создание промежуточного массива при оценке поэлементного сравнения.

Я также измерил:

def hamm(a,b):
    return count_nonzero(a != b)

И я получаю 3.11 usec per loop для версии Python и 0.427 usec per loop для версии Pythran.

Отказ от ответственности: я один из разработчиков Pythran.

person serge-sans-paille    schedule 23.09.2015

для струн это работает быстрее

def Hamm(a, b):
    c = 0
    for i in range(a.shape[0]):
        if a[i] != b[i]:
            c += 1
    return c
person Micra    schedule 29.10.2020

Я предлагаю вам преобразовать массив numpy bit в массив numpy uint8, используя np.packbits

Взгляните на scipy space.distance.hamming: https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.hamming.html

в противном случае вот небольшой фрагмент, который требует только numpy, вдохновленный Быстрый способ подсчета ненулевых бит в положительном целом числе:

bit_counts = np.array([int(bin(x).count("1")) for x in range(256)]).astype(np.uint8)
def hamming_dist(a,b,axis=None):
    return np.sum(bit_counts[np.bitwise_xor(a,b)],axis=axis)

с axis = -1, это позволяет взять расстояние хаммига между записью и большим массивом; например:

inp = np.uint8(np.random.random((512,8))*255) #512 entries of 8 byte
hd = hamming_dist(inp, inp[123], axis=-1) #results in 512 hamming distances to entry 123
idx_best = np.argmin(hd)    # should point to identity 123
hd[123] = 255 #mask out identity
idx_nearest= np.argmin(hd)    # should point entry in list with shortest distance to entry 123
dist_hist = np.bincount(np.uint8(hd)) # distribution of hamming distances; for me this started at 18bits to 44bits with a maximum at 31
person Oliver Zendel    schedule 26.11.2020