Самый быстрый способ получить расстояние Хэмминга для целочисленного массива

Пусть a и b будут векторами одинакового размера с 8-битными целыми числами (0-255). Я хочу вычислить количество битов, в которых эти векторы различаются, то есть расстояние Хэмминга между векторами, образованными конкатенацией двоичных представлений этих чисел. Например:

a = [127,255]
b= [127,240]

Использование библиотеки numpy

np.bitwise_xor(a,b)
# Output: array([ 0, 15])

Теперь мне нужно двоично представить каждый элемент вышеуказанного массива и подсчитать количество единиц во всех элементах массива. В приведенном выше примере расстояние Хэмминга равно 0 + 4 = 4. Есть ли какое-нибудь быстрое и элегантное решение для этого в Python?


person Debasish Mitra    schedule 29.11.2016    source источник
comment
Не будет ли это 0 + 1 вместо этого, потому что 254 — это все единицы, кроме одного бита, тогда как 255 — это все единицы?   -  person Divakar    schedule 29.11.2016
comment
Возможно, просто взять стандартный рецепт popcount, передать его по массиву и просуммировать результат. Возможно, вы сможете получить ускорение, рассматривая буфер массива как больший dtype.   -  person user2357112 supports Monica    schedule 29.11.2016
comment
@Divakar Это была опечатка с моей стороны. Хороший улов. Обновлено число до 240 в образце данных.   -  person Debasish Mitra    schedule 30.11.2016
comment
Какова типичная длина векторов a и b?   -  person Warren Weckesser    schedule 30.11.2016
comment
@WarrenWeckesser Пример фактических данных приведен ниже: a = [34, 200, 96, 158, 75, 208, 158, 230, 151, 85, 192, 131, 40, 142, 54, 64, 75, 251, 147, 195 , 78, 11, 62, 245, 49, 32, 154, 59, 21, 28, 52, 222] b = [128, 129, 2, 129, 196, 2, 168, 101, 60, 35, 83, 18, 12, 10, 104, 73, 122, 13, 2, 176, 114, 188, 1, 198, 12, 0, 154, 68, 5, 8, 177, 128]   -  person Debasish Mitra    schedule 30.11.2016
comment
Сколько раз за один запуск вашей программы вы вычисляете расстояние Хэмминга? Только раз? Несколько раз? Тысячи раз?   -  person Warren Weckesser    schedule 30.11.2016


Ответы (3)


Подход № 1. Мы могли бы транслировать их в двоичные биты и подсчитывать количество разных битов, например:

def hamming_distance(a, b):
    r = (1 << np.arange(8))[:,None]
    return np.count_nonzero( (a & r) != (b & r) )

Пробный запуск -

In [144]: a = [127,255]
     ...: b = [127,240]
     ...: 

In [145]: hamming_distance(a, b)
Out[145]: 4

Подход 2. Использование bitwise-xor мы можем узнать количество различных двоичных битов между a и b -

def hamming_distance_v2(a, b):
    r = (1 << np.arange(8))[:,None]
    return np.count_nonzero((np.bitwise_xor(a,b) & r) != 0)
person Divakar    schedule 29.11.2016
comment
Подход 2 генерирует исключение: TypeError: неподдерживаемый тип (ы) операнда для -: «список» и «список» - person Debasish Mitra; 30.11.2016
comment
@DebasishMitra Добавил туда лучший вариант с xor. - person Divakar; 30.11.2016
comment
hamming_distance_v2() в два раза быстрее, чем hamming_distance(). - person Nirmal; 01.11.2019
comment
В обеих версиях несоответствие входных размеров и/или значений за пределами [0,255] может привести к нежелательным результатам. Лучше использовать эти проверки работоспособности: assert np.all(a >= 0) assert np.all(a <= 255) assert np.all(b >= 0) assert np.all(b <= 255) assert a.shape == b.shape - person Nirmal; 02.11.2019

Если вы собираетесь вызывать функцию расстояния много раз во время одного выполнения вашей программы, вы можете немного увеличить скорость, используя предварительно вычисленную таблицу битовых счетчиков. Вот (еще одна) версия функции расстояния Хэмминга:

# _nbits[k] is the number of 1s in the binary representation of k for 0 <= k < 256.
_nbits = np.array(
      [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3,
       4, 2, 3, 3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4,
       4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2,
       3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5,
       4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4,
       5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3,
       3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2,
       3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6,
       4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5,
       6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 3, 4, 4, 5, 4, 5,
       5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6,
       7, 7, 8], dtype=np.uint8)


def hamming_distance1(a, b):
    c = np.bitwise_xor(a, b)
    n = _nbits[c].sum()
    return n

Далее a и b — это списки Python длиной 32, указанные в комментарии к вопросу. divakar_hamming_distance() и divakar_hamming_distance_v2() взяты из ответа @Divakar.

Вот время работы @Divakar:

In [116]: %timeit divakar_hamming_distance(a, b)
The slowest run took 5.57 times longer than the fastest. This could mean that an intermediate result is being cached.
100000 loops, best of 3: 11.3 µs per loop

In [117]: %timeit divakar_hamming_distance_v2(a, b)
The slowest run took 5.35 times longer than the fastest. This could mean that an intermediate result is being cached.
100000 loops, best of 3: 10.3 µs per loop

hamming_distance1(a, b) немного быстрее:

In [118]: %timeit hamming_distance1(a, b)
The slowest run took 6.04 times longer than the fastest. This could mean that an intermediate result is being cached.
100000 loops, best of 3: 7.42 µs per loop

На моем компьютере инициализация _nbits занимает около 11 мкс, поэтому нет никаких преимуществ в использовании hamming_distance1, если вы вызываете функцию только один раз. Если вы вызовете его три или более раз, вы получите чистый выигрыш в производительности.

Если входные данные уже представляют собой массивы numpy, все функции выполняются значительно быстрее:

In [119]: aa = np.array(a)

In [120]: bb = np.array(b)

In [121]: %timeit divakar_hamming_distance_v2(aa, bb)
The slowest run took 8.22 times longer than the fastest. This could mean that an intermediate result is being cached.
100000 loops, best of 3: 5.72 µs per loop

In [122]: %timeit hamming_distance1(aa, bb)
The slowest run took 12.67 times longer than the fastest. This could mean that an intermediate result is being cached.
100000 loops, best of 3: 2.77 µs per loop

Конечно, если вы всегда делаете это непосредственно перед вычислением расстояния Хэмминга, время, необходимое для преобразования, должно быть включено в общее время. Однако, если вы напишете код, который генерирует a и b, чтобы воспользоваться преимуществом numpy ранее, вы можете уже иметь их в виде массивов numpy к тому времени, когда вы вычислите расстояние Хэмминга.


(Я также немного поэкспериментировал с двумерным массивом предварительно вычисленных расстояний Хэмминга между 8-битными значениями — массивом формы (256, 256) — но стоимость инициализации выше, а прирост производительности невелик.)

person Warren Weckesser    schedule 29.11.2016
comment
Без Numba: divakar_hamming_distance(aa, bb) 5,8 мкс на цикл hamming_distance1(aa, bb) 15,4 мкс на цикл С Numba: divakar_hamming_distance(aa, bb) 1,2 мкс на цикл hamming_distance1(aa, bb) 896 нс на цикл - person Nirmal; 02.11.2019

может быть, не самый эффективный способ, но самый простой imo - преобразовать ваш выходной массив в строки в двоичной форме, а затем взять сумму всех символов, преобразованных обратно в целые числа...

import numpy as np

output = np.random.randint(0,63,10)
hamming = ['{:b}'.format(x).count('1') for x in output]
person Aaron    schedule 29.11.2016