#javascript #node.js #minhash
#javascript #node.js #minhash
Вопрос:
Я ищу node.js / Модуль Javascript, который применяет алгоритм minhash к строке или тексту большего размера и возвращает мне «идентифицирующую» или «характерную» байтовую строку или шестнадцатеричную строку для этого текста. Если я применю алгоритм к другой аналогичной текстовой строке, хэш-строка также должна быть аналогичной. Существует ли подобный модуль уже?
Модули, которые я изучал до сих пор, имели только возможность сравнивать тексты напрямую и вычислять какое-то числовое сходство jaccard непосредственно для сравниваемых текстов, но я хотел бы сохранить какую-то хэш-строку для каждого документа, чтобы позже я мог сравнить строки на предмет сходства, если у меня будут похожие тексты…
По сути, то, что я ищу, это этот код отсюда (Java): в Javascript:https://github.com/codelibs/elasticsearch-minhash
например, для строки типа: "The quick brown fox jumps over the lazy dog"
и "The quick brown fox jumps over the lazy d"
это создало бы хэш для первого предложения типа:
"KV5rsUfZpcZdVojpG8mHLA=="
а для второй строки что-то вроде:
KV5rsSfZpcGdVojpG8mGLA==
обе хэш-строки не очень сильно отличаются… в этом суть алгоритма minhash, однако я не знаю, как создать подобную хэш-строку .. и все библиотеки, которые я нашел на данный момент, сравнивают непосредственно только 2 документа и создают коэффициент подобия, но они не создают хэш-строку, характерную для документа…
Сходство со всеми алгоритмами заключается в том, что они создают хэшированное значение crc32 (или аналогичное) для своего массива токенов word (или shingles). Но я все еще не знаю, как они сравнивают эти хэши друг с другом…
Ответ №1:
Требуется реализация minhash Дугласом Духаймом, но любая другая реализация, вычисляющая массив хэш-значений, может использоваться таким же образом.
const str1 = "The quick brown fox jumps over the lazy dog";
const str2 = "The quick brown fox jumps over the lazy d";
console.log(str1);
console.log(str2);
var s1 = str1.split(' ');
var s2 = str2.split(' ');
// create a hash for each set of words to compare
// default numPerm is 128 but that gives very long hash
// below 8, almost similar string will give exactly the same hash
var m1 = new Minhash({numPerm: 8});
var m2 = new Minhash({numPerm: 8});
// update each hash
s1.map(function(w) { m1.update(w) });
s2.map(function(w) { m2.update(w) });
// estimate the jaccard similarity between two minhashes
console.log('jaccard similarity:', m1.jaccard(m2));
// Now to convert hashvalues to a string we use a kind of base64
// encode but since hasvalues is an array of 32bits integer we
// have to explode it into a array of 8bits integers first
// for a given int32 returns 4 bytes
function int32ToBytes(num) {
// the hexadecimal representation of the largest 32bits unsigned integer is 0xFFFFFFFF
// the hexadecimal representation of the largest unsigned integer (8bits === a byte) is 0xFF
// so it is possible to think a 32bits uint (unsigned integer) as the concatenation of 4 8bits uint.
// the bitwise amp; operator is the bitwise AND
// its table of truth is 0 amp; 0 = 0, 0 amp; 1 = 0, 1 amp; 0 = 0 and 1 amp; 1 = 1
// for instance 8 amp; 1 <=> 0b111 amp; 0b001 <=> 0b001 <=> 1
// the same is possible with hex representation:
// 65535 amp; 255 <=> 0xFFFF amp; 0x00FF <=> 0x0FF <=> 255
// 65535 amp; 65280 <=> 0xFFFF amp; 0xFF00 <=> 0xFF00 <=> 65280
// 255 65535 = 65535
// now about the bitwise >> shift operator
// a >> n shift the number a by n bits to the right
// in hex FF is 8bits so `0xFF00 >> 8 = 0xFF`
// this operation is reversible `0xFF << 8 = 0xFF00`
// 0xFFFF needs 16 bits to be represented, as 0xFF00
// but 0xFF only needs 8 bits
// so its possible to split a 16 bits integer into two 8 bits integer this way:
// int16 = (int16 amp; 0xFF00) >> 8 (int16 amp; 0x00FF) >> 0
// no information was lost because we're able to do the reverse operation
// the same principle is used below to encode a 32 bits integer into 4 bytes (8bits integers)
// max uint32 = 0xFFFFFFFF =
// 0xFF << 24 0xFF << 16 0xFF << 8 0xFF << 0
const arr = [
(num amp; 0xff000000) >> 24,
(num amp; 0x00ff0000) >> 16,
(num amp; 0x0000ff00) >> 8,
(num amp; 0x000000ff)
];
return arr;
}
// tolerant base64 encode of 4 bytes
function Uint8ToString(u8a){
var CHUNK_SZ = 0x8000;
var c = [];
for (var i=0; i < u8a.length; i =CHUNK_SZ) {
c.push(String.fromCharCode.apply(null, u8a.subarray(i, i CHUNK_SZ)));
}
return c.join("");
}
// tolerant base64 encode of int32 array
function base64EncodeInt32Array(intArray) {
let str = '';
intArray.forEach((i) => {
var u8 = new Uint8Array(int32ToBytes(i));
var b64encoded = btoa(Uint8ToString(u8));
str = b64encoded;
});
return str;
}
// replace non significant '==' to shorten hash
console.log(base64EncodeInt32Array(m1.hashvalues).replace(/==/g, ''));
console.log(base64EncodeInt32Array(m2.hashvalues).replace(/==/g, ''));
<script src='https://rawgit.com/duhaime/minhash/master/minhash.min.js'></script>
Комментарии:
1. очень приятно, спасибо за пример. Можете ли вы подробнее объяснить, как вы закодировали числа в шестнадцатеричные символы? Потому что я пытался найти способ сделать это самостоятельно, но безуспешно. Какое это имеет отношение к битовым сдвигам (>>) и как это работает с amp;0x00FF … может быть, где-нибудь есть хороший учебник?
2. Я попытаюсь объяснить в определении int32ToBytes.
Ответ №2:
Если вы собираетесь сравнивать документы только по два за раз (насколько doc A похож на doc B?), то сохранение minhashes каждого документа в виде объединенной строки подойдет. Вы бы сравнили два документа, разделив строки каждого документа обратно на составляющие их мини-хэши и подсчитав, сколько мини-хэшей было общим (идентичным).
Но если вы хотите спросить «какие другие документы похожи на документ A», это плохое решение, поскольку вам пришлось бы сравнивать документ A по отдельности с каждым другим документом, который вы ранее видели. Хуже того, если вы хотите найти все сходства между документами в корпусе, вам придется сравнивать каждый документ с любым другим документом. Для группы из 1000 документов это потребовало бы 499 500 сравнений. С миллионом документов это почти 500 миллиардов сравнений. Это проблема O (n2).
Вместо этого подходящий способ сделать это — сохранить словарь хэшей, сопоставляя мини-хэши с идентификаторами документов. Каждый раз, когда вы сталкиваетесь с новым документом, вы генерируете его мини-хэши, затем ищите в словаре хэшей все другие документы, которые совместно используют один или несколько из этих хэшей. Чем больше хэшей документ разделяет с входящим документом, тем выше его оценочное сходство с jaccard. Наконец, вы добавляете все minhashes для нового документа в словарь хэшей, чтобы он был доступен для будущих поисков.
Вас, вероятно, интересуют сходства только там, где, по крайней мере, скажем, половина minhashes являются общими (по оценкам, 50% сходства с jaccard), но для их поиска все равно может потребоваться много вычислений, поскольку могут существовать миллионы документов, которые используют по крайней мере один minhash с входящим документом, и вам нужно подсчитать количество общих хэшей для каждого. Хеширование с учетом локальности может существенно сократить количество обращений (и требуемое хранилище).