Node.js / модуль minhash javascript, который выводит аналогичную хэш-строку для аналогичного текста

#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 с входящим документом, и вам нужно подсчитать количество общих хэшей для каждого. Хеширование с учетом локальности может существенно сократить количество обращений (и требуемое хранилище).