[algorithm] 큰 집합에서 해밍 거리가 낮은 이진 문자열을 효율적으로 찾습니다.
문제:
부호없는 32 비트 정수의 큰 (~ 1 억) 목록, 부호없는 32 비트 정수 입력 값 및 최대 Hamming Distance가 주어지면 입력 값 의 지정된 Hamming Distance 내에있는 모든 목록 멤버를 반환합니다.
목록을 보관할 실제 데이터 구조는 공개되어 있고 성능 요구 사항은 인 메모리 솔루션을 요구하며 데이터 구조를 구축하는 데 드는 비용은 부차적이며 데이터 구조를 쿼리하는 데 드는 비용이 매우 중요합니다.
예:
For a maximum Hamming Distance of 1 (values typically will be quite small)
And input:
00001000100000000000000001111101
The values:
01001000100000000000000001111101
00001000100000000010000001111101
should match because there is only 1 position in which the bits are different.
11001000100000000010000001111101
should not match because 3 bit positions are different.
지금까지 내 생각 :
해밍 거리가 0 인 퇴화 사례의 경우 정렬 된 목록을 사용하고 특정 입력 값에 대해 이진 검색을 수행합니다.
해밍 거리가 1 일 경우 원래 입력에서 각 비트를 뒤집고 위의 32 번 반복 할 수 있습니다.
어떻게하면 전체 목록을 스캔하지 않고 Hamming Distance> 1 인 목록 구성원을 효율적으로 찾을 수 있습니다.
답변
질문 : 해밍 거리 d (x, y)에 대해 무엇을 알고 있습니까?
대답:
- 음수가 아님 : d (x, y) ≥ 0
- 동일한 입력에 대해서만 0입니다 : d (x, y) = 0 ⇔ x = y
- 대칭입니다 : d (x, y) = d (y, x)
- 그것은 따르는 삼각 부등식 , D를 (X, Z) ≤ D (X, Y) + (D) (Y, z)
질문 : 우리는 왜 관심이 있습니까?
답변 : 이 해밍 거리가 있다는 것을 의미하기 때문에 메트릭 A에 대한 메트릭 공간 . 메트릭 공간을 인덱싱하는 알고리즘이 있습니다.
당신의 공간이 유클리드 아니지만 그 또한 지식으로 무장 일반적으로 “공간 인덱싱”에 대한 알고리즘을 찾아 볼 수 있다 메트릭 공간. 이 주제에 관한 많은 책은 해밍 거리와 같은 메트릭을 사용한 문자열 인덱싱을 다룹니다.
각주 : 고정 너비 문자열의 해밍 거리를 비교하는 경우 어셈블리 또는 프로세서 내장 함수를 사용하여 상당한 성능 향상을 얻을 수 있습니다. 예를 들어, GCC ( 수동 )를 사용 하면 다음을 수행합니다.
static inline int distance(unsigned x, unsigned y)
{
return __builtin_popcount(x^y);
}
그런 다음 GCC에 SSE4a로 컴퓨터를 컴파일한다고 알리면 몇 개의 opcode로 줄여야한다고 생각합니다.
편집 : 여러 소스에 따르면 이것은 때때로 / 종종 일반적인 마스크 / 시프트 / 추가 코드보다 느립니다. 벤치마킹에 따르면 내 시스템에서 C 버전이 GCC보다 __builtin_popcount
약 160 % 성능이 뛰어납니다 .
부록 : 직접 문제에 대해 궁금해서 선형 검색, BK 트리 및 VP 트리의 세 가지 구현을 프로파일 링했습니다. VP와 BK 트리는 매우 유사합니다. BK 트리에서 노드의 자식은 트리의 중심에서 각각 고정 된 거리에있는 점을 포함하는 트리의 “쉘”입니다. VP 트리의 노드에는 두 개의 하위가 있습니다. 하나는 노드의 중심에있는 구 내의 모든 점을 포함하고 다른 하나는 외부의 모든 점을 포함합니다. 따라서 VP 노드를 여러 개의 미세한 “쉘”대신 두 개의 매우 두꺼운 “쉘”이있는 BK 노드로 생각할 수 있습니다.
결과는 내 3.2GHz PC에서 캡처되었으며 알고리즘은 다중 코어를 사용하려고 시도하지 않습니다 (쉬울 것임). 나는 100M 의사 난수 정수의 데이터베이스 크기를 선택했습니다. 결과는 거리 1..5에 대한 1000 개의 쿼리, 6..10 및 선형 검색에 대한 100 개의 쿼리의 평균입니다.
- 데이터베이스 : 100M 의사 난수 정수
- 테스트 수 : 거리 1..5의 경우 1000, 거리 6..10 및 선형의 경우 100
- 결과 : 평균 쿼리 히트 수 (매우 근사치)
- 속도 : 초당 쿼리 수
- 적용 범위 : 쿼리 당 검사 된 데이터베이스의 평균 비율
-BK 트리--VP 트리--선형- 거리 결과 속도 Cov 속도 Cov 속도 Cov 1 0.90 3800 0.048 % 4200 0.048 % 2 11300 0.68 % 330 0.65 % 3130 56 3.8 % 63 3.4 % 4970 18 12 % 22 10 % 5 5700 8.5 26 % 10 22 % 6 2.6e4 5.2 42 % 6.0 37 % 7 1.1e5 3.7 60 % 4.1 54 % 8 3.5e5 3.0 74 % 3.2 70 % 9 1.0e6 2.6 85 % 2.7 82 % 10 2.5e6 2.3 91 % 2.4 90 % 2.2 100 %
귀하의 의견에서 다음과 같이 언급했습니다.
다른 루트 노드를 가진 BK- 트리를 여러 개 생성하고이를 확산시킴으로써 BK- 트리를 개선 할 수 있다고 생각합니다.
이것이 바로 VP 트리가 BK 트리보다 (약간) 더 잘 수행되는 이유라고 생각합니다. “얕은”보다는 “더 깊은”것으로, 더 적은 수의 포인트에 대해 더 세밀한 비교를 사용하는 대신 더 많은 포인트와 비교합니다. 나는 그 차이가 더 높은 차원의 공간에서 더 극단적이라고 생각합니다.
마지막 팁 : 트리의 리프 노드는 선형 스캔을위한 정수의 평평한 배열이어야합니다. 작은 세트 (아마도 1000 포인트 이하)의 경우 더 빠르고 메모리 효율성이 높습니다.
답변
나는 2 32 비트의 비트 세트로 입력 숫자를 나타내는 솔루션을 작성하여 특정 숫자가 입력에 있는지 O (1)에서 확인할 수 있습니다. 그런 다음 쿼리 된 숫자와 최대 거리에 대해 해당 거리 내의 모든 숫자를 재귀 적으로 생성하고 비트 세트와 비교하여 확인합니다.
예를 들어 최대 거리 5의 경우 242825 숫자입니다 ( sum d = 0 ~ 5 {32 choose d} ). 비교를 위해 Dietrich Epp의 VP-tree 솔루션은 예를 들어 1 억 개의 숫자 중 22 %, 즉 2 천 2 백만 개의 숫자를 통과합니다.
저는 Dietrich의 코드 / 솔루션을 기반으로 내 솔루션을 추가하고 그의 솔루션과 비교했습니다. 최대 10 개의 거리에 대한 초당 쿼리 속도는 다음과 같습니다.
Dist BK Tree VP Tree Bitset Linear
1 10,133.83 15,773.69 1,905,202.76 4.73
2 677.78 1,006.95 218,624.08 4.70
3 113.14 173.15 27,022.32 4.76
4 34.06 54.13 4,239.28 4.75
5 15.21 23.81 932.18 4.79
6 8.96 13.23 236.09 4.78
7 6.52 8.37 69.18 4.77
8 5.11 6.15 23.76 4.68
9 4.39 4.83 9.01 4.47
10 3.69 3.94 2.82 4.13
Prepare 4.1s 21.0s 1.52s 0.13s
times (for building the data structure before the queries)
작은 거리의 경우 bitset 솔루션이 4 개 중 가장 빠릅니다. 질문 작성자 Eric은 아래에서 가장 큰 관심 거리는 아마도 4-5 일 것이라고 말했습니다. 당연히 내 bitset 솔루션은 더 먼 거리에서 느리고 선형 검색보다 느립니다 (거리 32의 경우 2 32 개의 숫자를 통과 합니다). 그러나 거리 9의 경우 여전히 쉽게 이어집니다.
나는 또한 Dietrich의 테스트를 수정했습니다. 위의 각 결과는 알고리즘이 최소 3 개의 쿼리와 가능한 한 많은 쿼리를 약 15 초 내에 해결하도록하기위한 것입니다 (최소 10 초가 될 때까지 1, 2, 4, 8, 16 등의 쿼리로 라운드를 수행합니다. 총 통과). 상당히 안정적이며 1 초 동안 비슷한 수치를 얻습니다.
내 CPU는 i7-6700입니다. 내 코드 (Dierich ‘s를 기반으로 함)가 여기에 있습니다 (적어도 지금은 문서를 무시하고 이에 대해 무엇을해야할지 확실하지 않지만 tree.c
에는 모든 코드 가 포함되어 있으며 test.bat
내가 컴파일하고 실행 한 방법을 보여줍니다 (Dierich ‘s의 플래그를 사용했습니다 Makefile
)). . 내 솔루션에 대한 바로 가기 .
한 가지주의 사항 : 내 쿼리 결과에는 숫자가 한 번만 포함되므로 입력 목록에 중복 된 숫자가 포함되어 있으면 바람직 할 수도 있고 원하지 않을 수도 있습니다. 문제의 저자 Eric의 경우에는 중복이 없었습니다 (아래 주석 참조). 어쨌든이 솔루션은 입력에 중복이 없거나 쿼리 결과에 중복이 필요하지 않거나 필요하지 않은 사람들에게 유용 할 수 있습니다 (순수한 쿼리 결과는 목적을위한 수단 일 뿐이며 일부 다른 코드는 숫자를 다른 것으로 바꿉니다. 예를 들어 해시가 해당 숫자 인 파일 목록에 숫자를 매핑하는 맵).
답변
일반적인 접근 방식 (적어도 나에게 공통적)은 비트 문자열을 여러 청크로 나누고 이러한 청크에 대해 사전 필터 단계와 같은 정확한 일치를 쿼리하는 것입니다. 파일로 작업하는 경우 각 청크가 앞에있는 청크 (예 : 여기에서는 4 개)가있는만큼의 파일을 만든 다음 파일을 정렬합니다. 바이너리 검색을 사용할 수 있으며 보너스를 위해 일치하는 청크 위와 아래로 검색을 확장 할 수도 있습니다.
그런 다음 반환 된 결과에 대해 비트 해밍 거리 계산을 수행 할 수 있습니다. 이는 데이터 파일 또는 SQL 테이블을 사용하여 수행 할 수 있습니다.
요약하자면, DB 또는 파일에 32 비트 문자열이 있고 “쿼리”비트 문자열의 3 비트 해밍 거리 내에있는 모든 해시를 찾으려고한다고 가정 해 보겠습니다.
-
4 개의 열이있는 테이블을 만듭니다. 각 열에는 32 비트 해시의 8 비트 (문자열 또는 정수) 조각이 포함됩니다. islice 1 ~ 4입니다. 또는 파일을 사용하는 경우 4 개의 파일을 만듭니다. 각 파일은 다음을 갖는 조각의 순열입니다. 각 “행”의 앞쪽에 하나의 “섬”
-
qslice 1에서 4로 동일한 방식으로 쿼리 비트 문자열을 슬라이스합니다.
-
이 테이블을 쿼리하여
qslice1=islice1 or qslice2=islice2 or qslice3=islice3 or qslice4=islice4
. 이렇게하면8 - 1
쿼리 문자열의 7 비트 ( ) 내에있는 모든 문자열이 제공됩니다 . 파일을 사용하는 경우 동일한 결과에 대해 4 개의 순열 된 파일 각각에서 이진 검색을 수행하십시오. -
반환 된 각 비트 문자열에 대해 쿼리 비트 문자열과 쌍으로 정확한 해밍 거리를 계산합니다 (DB 또는 치환 된 파일에서 4 개의 슬라이스에서 인덱스 측 비트 문자열 재구성).
4 단계의 연산 수는 전체 테이블의 전체 쌍별 해밍 계산보다 훨씬 적어야하며 실제로 매우 효율적입니다. 또한 병렬 처리를 사용하여 더 빠른 속도가 필요하므로 더 작은 파일로 파일을 쉽게 분할 할 수 있습니다.
이제 물론 귀하의 경우에는 일종의 자체 조인, 즉 서로 어느 정도 거리 내에있는 모든 값을 찾고 있습니다. 동일한 접근 방식이 여전히 IMHO로 작동하지만, 시작 청크를 공유하는 순열 (파일 또는 목록 사용)의 시작점에서 위아래로 확장하고 결과 클러스터에 대한 해밍 거리를 계산해야합니다.
파일 대신 메모리에서 실행하는 경우 100M 32 비트 문자열 데이터 세트는 4GB 범위에 있습니다. 따라서 4 개의 순열 된 목록에는 약 16GB 이상의 RAM이 필요할 수 있습니다. 대신 메모리 매핑 파일로 우수한 결과를 얻고 비슷한 크기의 데이터 세트에 대해서는 RAM을 줄여야합니다.
사용 가능한 오픈 소스 구현이 있습니다. 공간에서 가장 좋은 것은 IMHO입니다. Moz , C ++ 에서 Simhash를 위해 수행 했지만 32 비트가 아닌 64 비트 문자열 용으로 설계되었습니다.
이 경계 happing 거리 방법은 첫째로 AFAIK를 설명한 모 시스 샤리 카르 는 “simhash”정액에 종이 와 해당 Google의 특허 :
- 해밍 공간에서 가장 가까운 이웃 검색
[…]
각각 d 비트로 구성된 비트 벡터가 주어지면 비트의 N = O (n 1 / (1+)) 임의 순열을 선택합니다. 각 임의 순열 σ에 대해 σ에 의해 순열 된 비트의 사전 식 순서로 비트 벡터의 정렬 된 순서 O σ를 유지합니다. 쿼리 비트 벡터 q가 주어지면 다음을 수행하여 근사 근사치를 찾습니다.
각 순열 σ에 대해 O σ에 대해 이진 검색을 수행하여 q에 가장 가까운 두 비트 벡터를 찾습니다 (σ에 의해 순열 된 비트로 얻은 사전 순서대로). 이제 우리는 q와 일치하는 가장 긴 접두사의 길이 순서로 이진 검색에서 반환 된 위치의 위와 아래 요소를 검사하는 정렬 된 순서 O σ 각각에서 검색합니다.
Monika Henziger 는 “거의 중복 된 웹 페이지 찾기 : 알고리즘에 대한 대규모 평가”라는 논문에서이를 확장했습니다 .
3.3 알고리즘 C의 결과
우리는 각 페이지의 비트 열을 12 개의 겹치지 않는 4 바이트 조각으로 분할하여 20B 조각을 만들고, 적어도 하나의 공통 조각이있는 모든 페이지의 C 유사성을 계산했습니다. 이 접근 방식은 최대 11 개의 차이 (예 : C 유사성 373)가있는 모든 페이지 쌍을 찾을 수 있도록 보장하지만 더 큰 차이에서는 일부를 놓칠 수 있습니다.
이는 Gurmeet Singh Manku, Arvind Jain 및 Anish Das Sarma의 웹 크롤링 을 위한 거의 중복 된 항목 감지 문서에서도 설명합니다 .
- 해밍 거리 문제
정의 : f 비트 지문 모음과 쿼리 지문 F가 주어지면 기존 지문이 최대 k 비트에서 F와 다른지 여부를 식별합니다. (위 문제의 배치 모드 버전에서는 단일 쿼리 지문 대신 일련의 쿼리 지문이 있습니다.)
[…]
직관 : 2 개의 df-bit 진정한 무작위 지문으로 구성된 정렬 된 표를 고려하십시오. 테이블에서 가장 중요한 d 비트에만 집중하십시오. 이러한 d- 비트 번호의 목록은 (a) 2d 비트 조합이 상당히 많고 (b) d- 비트 조합이 거의 복제되지 않는다는 점에서 “거의 카운터”에 해당합니다. 반면에 최하위 f-d 비트는 “거의 무작위”입니다.
이제 | d − d | 작은 정수입니다. 테이블이 정렬 되었기 때문에 단일 프로브는 d 가장 중요한 비트 위치에서 F와 일치하는 모든 지문을 식별하는 데 충분합니다. | d − d | 이후 적기 때문에 그러한 경기의 수도 적을 것으로 예상됩니다. 일치하는 각 지문에 대해 최대 k 비트 위치에서 F와 다른지 여부를 쉽게 파악할 수 있습니다 (이러한 차이는 자연스럽게 f-d 최하위 비트 위치로 제한됨).
위에서 설명한 절차는 k 비트 위치에서 F와 다른 기존 지문을 찾는 데 도움이되며, 모두 F의 최하위 f-d 비트 중 하나로 제한됩니다. 이것은 상당한 수의 경우를 처리합니다. 모든 경우를 다루기 위해 다음 섹션에 공식적으로 설명 된대로 몇 개의 추가 정렬 된 테이블을 작성하는 것으로 충분합니다.
참고 : 관련 DB 전용 질문에 유사한 답변을 게시했습니다.
답변
지정된 해밍 거리 내에서 원본 목록의 가능한 모든 변형을 미리 계산하여 블룸 필터에 저장할 수 있습니다. 이것은 빠른 “아니오”를 제공하지만 반드시 “예”에 대한 명확한 대답은 아닙니다.
예의 경우, 블룸 필터의 각 위치와 관련된 모든 원래 값 목록을 저장하고 한 번에 하나씩 살펴 봅니다. 속도 / 메모리 절충을 위해 블룸 필터의 크기를 최적화하십시오.
모든 것이 정확히 작동하는지 확실하지 않지만, 구울 런타임 RAM이 있고 사전 계산에 매우 오랜 시간을 소비하려는 경우 좋은 접근 방식으로 보입니다.
답변
목록을 정렬 한 다음 Hamming Distance 내에서 가능한 다른 값에 대해 정렬 된 목록에서 이진 검색을 수행하는 것은 어떻습니까?
답변
이 문제를 해결하기위한 한 가지 가능한 접근 방식은 Disjoint-set 데이터 구조를 사용하는 것 입니다. 아이디어는 동일한 세트에서 Hamming distance <= k 인 목록 멤버를 병합하는 것입니다. 알고리즘의 개요는 다음과 같습니다.
-
각 목록 구성원에 대해 해밍 거리 <= k로 가능한 모든 값 을 계산합니다 . k = 1의 경우 32 개의 값이 있습니다 (32 비트 값의 경우). k = 2, 32 + 32 * 31 / 2 값의 경우.
-
계산 된 각 값 에 대해 원래 입력에 있는지 테스트합니다. 크기가 2 ^ 32 인 배열 또는 해시 맵을 사용하여이 검사를 수행 할 수 있습니다.
-
값 이 원래 입력에 있으면 다음 과 함께 “union”연산을 수행하십시오. 목록 구성원 .
- 변수에서 실행 된 통합 연산의 수를 유지합니다.
-
N 개의 분리 세트 (여기서 N은 입력의 요소 수)로 알고리즘을 시작합니다. 통합 작업을 실행할 때마다 분리 된 집합의 수가 1 씩 감소합니다. 알고리즘이 종료되면 분리 세트 데이터 구조는 분리 세트로 그룹화 된 해밍 거리 <= k 인 모든 값을 갖게됩니다. 이 분리 집합 데이터 구조는 거의 선형 시간 으로 계산할 수 있습니다 .
답변
여기에 간단한 아이디어가 있습니다. 100m 입력 정수의 바이트 단위 기수 정렬을 수행하고, 최상위 바이트를 먼저 수행하고 일부 외부 구조의 처음 세 수준에서 버킷 경계를 추적합니다.
쿼리하려면 거리 예산 d
과 입력 단어로 시작하십시오.w
. 바이트 값과 상위 레벨의 각 버킷 b
, 해밍 거리 계산 d_0
사이 b
그리고 높은 바이트 w
. 재귀 예산과 그 버킷 검색 d - d_0
각 바이트 값이며 b'
,하자 d_1
간의 해밍 거리를 b'
그리고 두번째 바이트 w
. 예산 등으로 세 번째 레이어를 재귀 적으로 검색 d - d_0 - d_1
합니다.
버킷은 나무를 형성합니다. 예산이 마이너스가 될 때마다 해당 하위 트리 검색을 중지하십시오. 거리 예산을 낭비하지 않고 반복적으로 리프로 내려 가면 해당 리프 값이 출력의 일부가되어야합니다.
다음은 외부 버킷 경계 구조를 나타내는 한 가지 방법입니다. 길이가 16_777_216 ( = (2**8)**3 = 2**24
) 인 배열을 사용합니다 . 여기서 index의 요소는 i
[256 * i, 256 * i + 255] 범위의 값을 포함하는 버킷의 시작 색인입니다. 해당 버킷의 끝을 벗어난 인덱스 1을 찾으려면 인덱스 i + 1을 찾거나 i + 1 = 2 ** 24에 대해 배열 끝을 사용합니다.
메모리 예산은 100m * 단어 당 4 바이트 = 입력의 경우 400MB, 주소 당 2 ** 24 * 4 바이트 = 인덱싱 구조의 경우 64MiB이거나 총 절반 기가 이하입니다. 인덱싱 구조는 원시 데이터에 대한 6.25 % 오버 헤드입니다. 물론 인덱싱 구조를 구성한 후에는 각 입력 단어의 최하위 바이트 만 저장하면됩니다. 나머지 세 개는 인덱스 구조의 인덱스에 내재되어 있으므로 총 ~ (64 + 50) MB입니다.
입력이 균등하게 분포되어 있지 않으면 모든 엔트로피를 트리 상단에 배치하는 (단일, 보편적으로 공유되는) 순열을 사용하여 입력 단어의 비트를 순열 할 수 있습니다. 이렇게하면 첫 번째 수준의 가지 치기가 검색 공간의 더 큰 청크를 제거합니다.
나는 몇 가지 실험을 시도했는데 이것은 선형 검색만큼이나 더 나쁘게 수행됩니다. 이 멋진 아이디어에 너무 많이. 아 글쎄, 적어도 메모리 효율적입니다.