Подобие косинуса против расстояния Хэмминга

Чтобы вычислить сходство между двумя документами, я создаю вектор признаков, содержащий термин частоты. Но затем, для следующего шага, я не могу выбрать между "подобие косинуса" и "Расстояние Хэмминга".

Мой вопрос: есть ли у вас опыт работы с этими алгоритмами? Какой из них дает вам лучшие результаты?

В дополнение к этому: не могли бы вы сказать мне, как закодировать подобие косинуса в PHP? Для расстояния Хэмминга у меня уже есть код:

function check ($terms1, $terms2) {
    $counts1 = array_count_values($terms1);
    $totalScore = 0;
    foreach ($terms2 as $term) {
        if (isset($counts1[$term])) $totalScore += $counts1[$term];
    }
    return $totalScore * 500 / (count($terms1) * count($terms2));
}

Я не хочу использовать какой-либо другой алгоритм. Я только хотел бы иметь помощь, чтобы выбрать между обоими.

И, может быть, кто-то может сказать что-то о том, как улучшить алгоритмы. Вы получите лучшие результаты, если отфильтруете стоп-слова или общие слова?

Я надеюсь, что вы можете мне помочь. Заранее спасибо!


person caw    schedule 03.06.2009    source источник


Ответы (4)


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

Поскольку ваши документы, безусловно, имеют разную длину, и если места слов не учитываются, косинусное сходство лучше (обратите внимание, что в зависимости от ваших потребностей существуют лучшие решения). :)

Вот косинусная функция сходства двух массивов слов:

function cosineSimilarity($tokensA, $tokensB)
{
    $a = $b = $c = 0;
    $uniqueTokensA = $uniqueTokensB = array();

    $uniqueMergedTokens = array_unique(array_merge($tokensA, $tokensB));

    foreach ($tokensA as $token) $uniqueTokensA[$token] = 0;
    foreach ($tokensB as $token) $uniqueTokensB[$token] = 0;

    foreach ($uniqueMergedTokens as $token) {
        $x = isset($uniqueTokensA[$token]) ? 1 : 0;
        $y = isset($uniqueTokensB[$token]) ? 1 : 0;
        $a += $x * $y;
        $b += $x;
        $c += $y;
    }
    return $b * $c != 0 ? $a / sqrt($b * $c) : 0;
}

Это быстро (isset() вместо in_array() - убийца на больших массивах).

Как видите, результаты не учитывают «величину» каждого слова.

Я использую его для обнаружения многопостовых сообщений «почти» скопированных текстов. Это работает хорошо. :)

Лучшая ссылка о метриках сходства строк: http://www.dcs.shef.ac.uk/~sam/stringmetrics.html

Для дальнейших интересных чтений:

http://www.miislita.com/information-retrieval-tutorial/cosine-similarity-tutorial.html http://bioinformatics.oxfordjournals.org/cgi/content/full/22/18/2298

person Toto    schedule 17.08.2009
comment
Большое Вам спасибо. :) Но разве решение Майка (выбранный ответ) не лучше? Код короче и кажется таким же быстрым, как ваш. Каковы различия? - person caw; 18.08.2009
comment
Функция Майка не очень точна. Попробуйте echo check(array('a', 'b', 'c'), array('a', 'b', 'c')); Он должен вернуть 1 (cos(0)), но его функция возвращает 0,33. :( - person Toto; 18.08.2009
comment
Ваша функция действительно правильная? Это дает 0,71 для [1, 1, 1] и [1, 1, 0]. Но miislita.com/searchito/binary-similarity-calculator.html дает 0,82?! Нужно ли делить значение подобия на длину документа? - person caw; 20.09.2009
comment
Этот инструмент предназначен для сравнения бинарных строк. Моя функция для документов слов. Результат будет другим. :) - person Toto; 21.09.2009
comment
Хорошо спасибо. Я искал какой-нибудь инструмент для сравнения, так как хочу быть уверенным, что на этот раз у меня правильная функция;) И мне не нужно делить значение на длину документа, так как длина не играет роли в косинусе. сходство, да? - person caw; 21.09.2009

Если я не ошибаюсь, я думаю, что у вас есть алгоритм на полпути между двумя алгоритмами. Для расстояния Хэмминга используйте:

function check ($terms1, $terms2) {
    $counts1 = array_count_values($terms1);
    $totalScore = 0;
    foreach ($terms2 as $term) {
        if (isset($counts1[$term])) $totalScore += 1;
    }
    return $totalScore * 500 / (count($terms1) * count($terms2));
}

(Обратите внимание, что вы добавляете только 1 для каждого совпадающего элемента в векторах токенов.)

И для подобия косинуса используйте:

function check ($terms1, $terms2) {
    $counts1 = array_count_values($terms1);
    $counts2 = array_count_values($terms2);
    $totalScore = 0;
    foreach ($terms2 as $term) {
        if (isset($counts1[$term])) $totalScore += $counts1[$term] * $counts2[$term];
    }
    return $totalScore / (count($terms1) * count($terms2));
}

(Обратите внимание, что вы добавляете произведение количества токенов между двумя документами.)

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

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

Куда бы вы ни пошли, удачи!

person Mike    schedule 03.06.2009
comment
Большое тебе спасибо! Итак, если я использую комбинацию обоих алгоритмов, сочетает ли она их преимущества? Чем было бы лучше этих алгоритмов, правда? :) Или мне лучше использовать один из ваших примеров кода? Ваша последняя фраза довольно интересна. Так что косинусное сходство было бы лучше для моей цели, верно? Поскольку оно выражает более сильную связь между двумя текстами, если одно слово появляется часто, не так ли? - person caw; 03.06.2009
comment
@ marco92w: я думаю, что в этом случае лучше всего подходит косинусное сходство - также см. Мое недавнее редактирование о функциональных словах. твоя интуиция была мертва там. - person Mike; 03.06.2009
comment
Спасибо, редактирование тоже информативно. Последний вопрос: :) В чем разница между косинусным сходством и моим алгоритмом (кодом, о котором идет речь)? Какой лучше? - person caw; 03.06.2009
comment
Ваш код в его нынешнем виде отдает предпочтение первому документу - вы игнорируете количество токенов из второго документа. В идеале алгоритм подобия должен быть симметричным. Если вы примените алгоритм от A к B, вы должны получить тот же результат, что и от B к A. Если вы попробуете это со своим кодом, вы получите другие результаты. Теперь, если у вас есть предпочтительный документ, ваш код может быть именно тем, что вы ищете! - person Mike; 03.06.2009
comment
Мой алгоритм не поддерживает и не предпочитает какой-либо текст. См. этот пример: paste.bradleygill.com/index.php?paste_id=9911 Значит должна быть еще какая разница!? - person caw; 03.06.2009
comment
Попробуйте это с text1 = the и text2 = the. Вы получите разницу в 2 раза. - person Mike; 03.06.2009
comment
Извините, я не знаю, что вы имеете в виду. Ваш пример всегда возвращает 500. Так в чем проблема? - person caw; 04.06.2009
comment
Может быть, я неправильно понимаю ваш код - является ли $counts1[$term] хэшем, который возвращает количество токенов для данного термина? - person Mike; 04.06.2009
comment
$counts1 содержит все слова текста в качестве ключей и количество их вхождений в качестве значений. Это станет ясно, если вы посмотрите, что делает array_count_values(). Так может быть, вы действительно неправильно понимаете мой код!? Так что лучше? Мой код (на полпути между двумя алгоритмами) или косинусное сходство? - person caw; 04.06.2009
comment
Нет, я именно так и думал. Но посмотрите на свой код — что произойдет, если в тексте 1 встречается один раз, а в тексте 2 — дважды? Переменная $totalScore будет другой. Уникален ли ваш список $terms2? Это единственное место, где я мог запутаться. Я предполагал, что в списке $terms2 есть только уникальные записи. - person Mike; 04.06.2009
comment
Нет, ни $terms1, ни $terms2 не уникальны. Они содержат повторяющиеся значения, так что новый массив (после array_count_values) содержит разные числа в качестве значений. Например: массив(=›1) и массив(=›2) - person caw; 05.06.2009
comment
ой! тогда все готово - просто уникальны массивы $terms(1|2) после вызова array_count_values, и код, который у меня есть, как написано, сделает свое дело. для ваших целей я все еще думаю, что косинусное сходство - лучшая мера, если вы просто выполняете сравнение текста. Вы не возражаете, если я спрошу, какой домен вы решаете? - person Mike; 05.06.2009
comment
Да, тогда ваш код косинусного подобия работает. Спасибо! :) Но лучше ли это моего промежуточного кода между двумя алгоритмами? Каковы различия? - person caw; 06.06.2009
comment
Есть что-то странное в этой функции подобия косинусов. Разве результат не должен быть равен 1 в этом случае: echo check(array('a', 'b', 'c'), array('a', 'b', 'c')); вместо этого я получаю 0,333, что, кстати, является тем же результатом, что и: echo check(array('a', 'b', 'c'), array('a', 'b')); - person Toto; 17.08.2009
comment
Тото прав. Вычисления нормы вектора неверны для обеих функций расстояния. - person outis; 18.08.2009
comment
Я написал небольшую функцию для вычисления сходства косинусов (нижняя часть этой страницы). Надеюсь это поможет. :) - person Toto; 18.08.2009

Прошу прощения за игнорирование того факта, что вы сказали, что не хотите использовать никакие другие алгоритмы, но серьезно, Расстояние Левенштейна и расстояние Дамерау-Левенштейна намного больше чертовски полезно, чем расстояние Хэмминга. Вот реализация удаленного DL в PHP, и если вы этого не сделаете, как родная функция PHP levenshtein(), которую, я думаю, вы не сделаете, потому что у нее есть ограничение по длине, вот версия без ограничения длины:

function levenshtein_distance($text1, $text2) {
    $len1 = strlen($text1);
    $len2 = strlen($text2);
    for($i = 0; $i <= $len1; $i++)
        $distance[$i][0] = $i;
    for($j = 0; $j <= $len2; $j++)
        $distance[0][$j] = $j;
    for($i = 1; $i <= $len1; $i++)
        for($j = 1; $j <= $len2; $j++)
            $distance[$i][$j] = min($distance[$i - 1][$j] + 1, $distance[$i][$j - 1] + 1, $distance[$i - 1][$j - 1] + ($text1[$i - 1] != $text2[$j - 1]));
    return $distance[$len1][$len2];
}
person chaos    schedule 03.06.2009
comment
Спасибо. Я думаю, вы что-то неправильно поняли. Я НЕ ХОЧУ использовать только расстояние Хэмминга. Я хотел бы применить его к вектору признаков текста, а не к самому тексту. Так что я бы сказал, что это полезнее Левенштейна, не так ли? ;) Но спасибо за код, я уверен, что он пригодится многим пользователям для других целей. - person caw; 03.06.2009
comment
Упс. Мне не удалось усвоить часть вектора признаков. Не бери в голову. :) Поскольку вам нравится код, я оставлю ответ не удаленным. Я надеюсь, что downvoters будут милосердны. :) - person chaos; 03.06.2009
comment
Да у них есть. Проголосовавших больше, чем проголосовавших. ;) - person caw; 03.06.2009
comment
levenshtein следует использовать для расчета расстояния редактирования. так что это зависит от потребности. АННА ФРЕД и ФРЕД АННА. Ленвенштейн даст большое число, но по косинусному сходству (по словам) оно будет 100%-ным. Похожие или нет? Это зависит от ваших потребностей. - person Toto; 18.08.2009

Вот мой исправленный код для функции косинусного расстояния, опубликованный Тото

function cosineSimilarity($tokensA, $tokensB)
{
    $a = $b = $c = 0;
    $uniqueTokensA = $uniqueTokensB = array();

    $uniqueMergedTokens = array_unique(array_merge($tokensA, $tokensB));

    foreach ($tokensA as $token) $uniqueTokensA[$token] = 0;
    foreach ($tokensB as $token) $uniqueTokensB[$token] = 0;

    foreach ($uniqueMergedTokens as $token) {
        $x = isset($uniqueTokensA[$token]) ? 1 : 0;
        $y = isset($uniqueTokensB[$token]) ? 1 : 0;
        $a += $x * $y;
        $b += pow($x,2);
        $c += pow($y,2);
    }
    return $b * $c != 0 ? $a / sqrt($b * $c) : 0;
}
person LorenzoMarchi    schedule 07.08.2010
comment
$x (и $y) всегда будет равно 1 (токен существует) или 0 (токен не существует). В этом случае POW($x, 2) всегда будет возвращать $x. Поэтому я удалил его, чтобы сохранить процессор. :) - person Toto; 13.08.2010
comment
Значит, обе ваши версии верны, Лоренцо и Тото? Они оба работают? - person caw; 26.08.2010