Сравните множество текстов (кластеризация) с матрицей - PullRequest
2 голосов
/ 23 мая 2009

У меня есть следующая PHP-функция для вычисления отношения между текстами:

function check($terms_in_article1, $terms_in_article2) {
    $length1 = count($terms_in_article1); // number of words
    $length2 = count($terms_in_article2); // number of words
    $all_terms = array_merge($terms_in_article1, $terms_in_article2);
    $all_terms = array_unique($all_terms);
    foreach ($all_terms as $all_termsa) {
        $term_vector1[$all_termsa] = 0;
        $term_vector2[$all_termsa] = 0;
    }
    foreach ($terms_in_article1 as $terms_in_article1a) {
        $term_vector1[$terms_in_article1a]++;
    }
    foreach ($terms_in_article2 as $terms_in_article2a) {
        $term_vector2[$terms_in_article2a]++;
    }
    $score = 0;
    foreach ($all_terms as $all_termsa) {
        $score += $term_vector1[$all_termsa]*$term_vector2[$all_termsa];
    }
    $score = $score/($length1*$length2);
    $score *= 500; // for better readability
    return $score;
}

Переменная $terms_in_articleX должна быть массивом, содержащим все отдельные слова, которые появляются в тексте.

Предполагая, что у меня есть база данных из 20 000 текстов, этой функции потребуется очень много времени, чтобы пройти через все соединения.

Как я могу ускорить этот процесс? Должен ли я добавлять все тексты в огромную матрицу вместо того, чтобы всегда сравнивать только два текста? Было бы замечательно, если бы у вас было несколько подходов с кодом, желательно на PHP.

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

Ответы [ 5 ]

4 голосов
/ 24 мая 2009

Вы можете разделить текст на добавление его. Простой пример: preg_match_all(/\w+/, $text, $matches); Конечно, реальное разбиение не так просто ... но возможно, просто исправьте шаблон:)

Создайте идентификатор таблицы (первичный автоинкремент int), значение (уникальный varchar) и таблицу ссылок, например: word_id (int), text_id (int), word_count (int) Затем заполните таблицы новыми значениями после разделения текста.

Наконец, вы можете делать с этими данными все, что захотите, быстро работая с индексированными целыми числами (ID) в БД.

UPDATE: Вот таблицы и запросы:

CREATE TABLE terms (
    id int(11) NOT NULL auto_increment, value char(255) NOT NULL,
    PRIMARY KEY  (`id`), UNIQUE KEY `value` (`value`)
);

CREATE TABLE `terms_in_articles` (
    term int(11) NOT NULL, 
    article int(11) NOT NULL, 
    cnt int(11) NOT NULL default '1',
    UNIQUE KEY `term` (`term`,`article`)
);


/* Returns all unique terms in both articles (your $all_terms) */
SELECT t.id, t.value 
FROM terms t, terms_in_articles a 
WHERE a.term = t.id AND a.article IN (1, 2);

/* Returns your $term_vector1, $term_vector2 */
SELECT article, term, cnt 
FROM terms_in_articles 
WHERE article IN (1, 2) ORDER BY article;

/* Returns article and total count of term entries in it ($length1, $length2) */
SELECT article, SUM(cnt) AS total 
FROM terms_in_articles 
WHERE article IN (1, 2) GROUP BY article;

/* Returns your $score wich you may divide by ($length1 / $length2) from previous query */
SELECT SUM(tmp.term_score) * 500 AS total_score FROM 
(
    SELECT (a1.cnt * a2.cnt) AS term_score 
    FROM terms_in_articles a1, terms_in_articles a2 
    WHERE a1.article = 1 AND a2.article = 2 AND a1.term = a2.term
    GROUP BY a2.term, a1.term
) AS tmp;

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

1 голос
/ 27 мая 2009

Вот немного оптимизированная версия вашей оригинальной функции. Это дает точно такие же результаты. (Я запускаю его на двух статьях из Википедии с более чем 10000 терминами и примерно 20 прогонами в каждой:

check():
test A score: 4.55712524522
test B score: 5.08138042619
--Time: 1.0707

check2():
test A score: 4.55712524522
test B score: 5.08138042619
--Time: 0.2624

Вот код:

function check2($terms_in_article1, $terms_in_article2) {
    $length1 = count($terms_in_article1); // number of words
    $length2 = count($terms_in_article2); // number of words

    $score_table = array();
    foreach($terms_in_article1 as $term){
        if(!isset($score_table[$term])) $score_table[$term] = 0;
        $score_table[$term] += 1;
    }
    $score_table2 = array();
    foreach($terms_in_article2 as $term){
        if(isset($score_table[$term])){
            if(!isset($score_table2[$term])) $score_table2[$term] = 0;
            $score_table2[$term] += 1;
        }
    }
    $score =0;
    foreach($score_table2 as $key => $entry){
        $score += $score_table[$key] * $entry;
    }
    $score = $score / ($length1*$length2);
    $score *= 500;
    return $score;
}

(Кстати. Время, необходимое для разбиения всех слов на массивы, не было включено.)

1 голос
/ 23 мая 2009

РЕДАКТИРОВАТЬ: Попытка быть более явным:

  1. Сначала закодируйте каждый член в целое число. Вы можете использовать словарь ассоциативный массив, например:

       $count = 0;
        foreach ($doc as $term) {
          $val = $dict[$term];
          if (!defined($val)) {
            $dict[$term] = $count++;
          }
          $doc_as_int[$val] ++;
        }
    

    Таким образом, вы заменяете строку вычисления с целым числом расчеты. Например, вы можете представлять слово «облако» как номер 5, а затем используйте индекс 5 массивов для хранения подсчета слово "облако". Обратите внимание, что мы только используйте поиск по ассоциативному массиву здесь, нет необходимости в CRC и т. д.

  2. Сохраняйте все тексты в виде матрицы, предпочтительно разреженный .
  3. Использовать выбор функции (PDF) .
  4. Возможно, использовать нативную реализацию на более быстром языке.
  5. Я предлагаю вам сначала использовать K-средства примерно с 20 кластерами, чтобы получить черновой вариант документа, который находится рядом с другим, а затем сравнить только пары внутри каждого кластера. Предполагая кластер одинакового размера, это увеличивает количество сравнений до 20*200 + 20*10*9 - около 6000 сравнений вместо 19900.
0 голосов
/ 01 июня 2009

Другим подходом, который следует использовать, является скрытый семантический анализ, который использует большой объем данных для поиска сходства между документами.

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

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

(Извините, это мой первый пост, поэтому не могу публиковать ссылки, просто посмотрите)

0 голосов
/ 26 мая 2009

Если вы можете использовать простой текст вместо массивов для сравнения, и если я правильно понял, где ваша цель, вы можете использовать levenshtein php функцию (которая обычно используется для создания google-like ' Вы имели ввиду функцию ...? В поисковых системах php).

Он работает противоположным образом, который вы используете: вернуть разницу между двумя строками.

Пример:

<?php
function check($a, $b) {
    return levenshtein($a, $b);
}

$a = 'this is just a test';
$b = 'this is not test';
$c = 'this is just a test';

echo check($a, $b) . '<br />';
//return 5
echo check($a, $c) . '<br />';
//return 0, the strings are identical
?>

Но я не знаю точно, улучшит ли это скорость выполнения ... но, возможно, да, вы убрали много циклов foreach и функцию array_merge.

EDIT:

Простой тест на скорость (это 30-секундный скрипт, его не на 100%, а):

function check($terms_in_article1, $terms_in_article2) {
    $length1 = count($terms_in_article1); // number of words
    $length2 = count($terms_in_article2); // number of words
    $all_terms = array_merge($terms_in_article1, $terms_in_article2);
    $all_terms = array_unique($all_terms);
    foreach ($all_terms as $all_termsa) {
        $term_vector1[$all_termsa] = 0;
        $term_vector2[$all_termsa] = 0;
    }
    foreach ($terms_in_article1 as $terms_in_article1a) {
        $term_vector1[$terms_in_article1a]++;
    }
    foreach ($terms_in_article2 as $terms_in_article2a) {
        $term_vector2[$terms_in_article2a]++;
    }
    $score = 0;
    foreach ($all_terms as $all_termsa) {
        $score += $term_vector1[$all_termsa]*$term_vector2[$all_termsa];
    }
    $score = $score/($length1*$length2);
    $score *= 500; // for better readability
    return $score;
}


$a = array('this', 'is', 'just', 'a', 'test');
$b = array('this', 'is', 'not', 'test');

$timenow = microtime();
list($m_i, $t_i) = explode(' ', $timenow);

for($i = 0; $i != 10000; $i++){
    check($a, $b);
}
$last = microtime();
list($m_f, $t_f) = explode(' ', $last);
$fine = $m_f+$t_f;
$inizio = $m_i+$t_i;
$quindi = $fine - $inizio;
$quindi = substr($quindi, 0, 7);
echo 'end in ' . $quindi . ' seconds';

печать: конец 0,36765 секунд

Второй тест:

<?php
function check($a, $b) {
    return levenshtein($a, $b);
}

$a = 'this is just a test';
$b = 'this is not test';

$timenow = microtime();
list($m_i, $t_i) = explode(' ', $timenow);
for($i = 0; $i != 10000; $i++){
    check($a, $b);
}
$last = microtime();
list($m_f, $t_f) = explode(' ', $last);
$fine = $m_f+$t_f;
$inizio = $m_i+$t_i;
$quindi = $fine - $inizio;
$quindi = substr($quindi, 0, 7);
echo 'end in ' . $quindi . ' seconds';
?>

печать: конец 0,05023 секунд

Так что, да, похоже, быстрее. Было бы неплохо попробовать много элементов массива (и много слов для левенштейна)

2 ° EDIT

При одинаковом тексте скорость кажется равной методу Левенштейна:

<?php
function check($a, $b) {
    return similar_text($a, $b);
}

$a = 'this is just a test ';
$b = 'this is not test';

$timenow = microtime();
list($m_i, $t_i) = explode(' ', $timenow);
for($i = 0; $i != 10000; $i++){
    check($a, $b);
}
$last = microtime();
list($m_f, $t_f) = explode(' ', $last);
$fine = $m_f+$t_f;
$inizio = $m_i+$t_i;
$quindi = $fine - $inizio;
$quindi = substr($quindi, 0, 7);
echo 'end in ' . $quindi . ' seconds';
?>

печать: конец 0,05988 секунд

Но это может занять более 255 символов:

Обратите внимание, что сложность этого Алгоритм O (N ** 3), где N является длина самой длинной строки.

и даже может возвращать аналогичное значение в процентах:

function check($a, $b) {
    similar_text($a, $b, $p);
    return $p;
}

Еще одно редактирование

А как насчет создания функции базы данных, чтобы выполнять сравнение непосредственно в запросе sql, вместо того, чтобы извлекать все данные и зацикливать их?

Если вы используете Mysql, взгляните на этот (функция Левенштейна, сделанная вручную, по-прежнему 255 символов) Иначе, если вы находитесь на Postgresql, этот другой (много функций, которые должны быть оценены)

...