В публикации приведено пошаговое применение латентно-статисчитеского анализа и предпринята попытка оценить его эффективность для решения практических задач.
Данные для анализа - описание заявок на заключение государственных контрактов с официального сайта Госзакупок. На практике большие объемы данных, как правило, хранятся в базе данных. В описываемом примере используется бд postgresql.
Цель
- Выявить группы (кластеры) ключевых слов, позволяющие ...
- Разбить документы по кластерам ...
Определения
Документ - строка (набор слов, одно или несколько предложений), относящаяся к одной единице информации (описание одного государственного контракта)
Этап 0. Подготовка данных
На данном этапе, чтобы снизить объем обработываемой информации, и повысить скорость работы алгоритма, проводится предварительная обработка документов. Обработка проводится в соответствии с целями и заранее сформулированными экспертными гипотезами. Гипотезы формулируются с учетом спицифики и предметной области обрабатываемого массива документов.
Для описываемой задачи были выдвинуты следующие гипотезы:
- стоп слова (союзы, предлоги и т.д.) и часто употребимые слова (встречающиеся в большом количестве документов) не несут полезной информации, на этапе подготовки их необходимо исключить,
- слова, встречающиеся в массиве документов только один раз, также не являются информативными и тоже исключаются,
- документ состоит только из русских слов и имен собственных (торговая марка, город и т.д.), которые прописаны в соотвествующих словарях. Слова не включенные в эти словари исключаются. Это некоторое упрощение практической задачи, но на понимание алгоритма латентно-статисчитеского анализа оно не влияет
- документ описывает объект закупок и его характеристики. Объект закупки - имя существительное, его характеристика - имя прилагательное. Все остальные типы слов из обработки исключаются.
Результатом выполнения данного этапа является частотный словарь, с расчетными характеристиками частоты вхождения каждого слова, значимости слова для каждого документа и др.
Реализовать данный этап можно средствами полнотекстового поиска postgresql
Шаг 0.0. Подготовка словарей
Для задачи нам необходим словарь русскоязычных слов:
- хранящий слова в формате ispell
- содержащий только имена существительные, прилагательные и имена собственные
Чтобы не формировать данный словарь вручную, воспользуемся словарем OpenCorpora. Как подготовить словарь для нашей задачи, описано в этой публикации. Будем надеяться, что слова, которыми описаны предмет и характеристики государственных контрактов по большей части присутствуют в нашем словаре :)
Проблема на данном шаге. Выбранный нами словарь в основном содержит общеупотребимые слова. Если предметная область к которой относятся документы специфична, она может содержать много важных слов не присуствующих в словаре. В этом случае нужно искать соотсвествующий словарь или формировать его вручную. Иначе эффективность алгоритма снижается
Шаг 0.1. Создание конфигурации полнотекстового поиска
Задача конфигурации в нашем случае - обработать каждый документ таким образом, чтобы на выходе получить только слова, имеющиеся в наших словарях. Стоп-слова и нераспознанные должны быть исключены. Кроме того, конфигурация должна приводить привести каждое слово к нормальной форме. С учетом перечисленных требований, конфигурация создается примерно так:
...
Подробнее о конфигурации полнотекстового поиска можно прочитать здесь
Проблема на данном шаге. Конфигурация не учитывает грамматических ошибок. Слово написанное с ошибкой не распознается и будет исключено. Если ошибок много - эффективность всего алгоритма снизится.
Шаг 0.3. Генерация частотного списка слов
Специфика массива документов нашего примера такова, что документы могут повторяться. Поэтому, прежде всего необходимо провести очистку, оставив только уникальные записи. Каждый документ имеет идентификатор (номер карточки), отбор уникальных записей будем проводить по этому номеру.
Кстати, при проверке оказалось, что документы с одинаковым номером могут совпадать не полностью. Поэтому, sql-команда ниже предполагает, что будет отобрана уникальная (по номеру) запись с максимальной длиной документа
-- создаем копию (временную таблицу), удаляем из нее дубликаты
SELECT
distinct on (number) number, description, fz, link,
currency, district, customer, price, create_date
INTO TEMP uniq_table
FROM
(SELECT {{schema}}.{{table}}.* FROM {{schema}}.{{table}}
ORDER BY length(description) DESC
) AS foo;
-- генерируем временную таблицу с частотным словарем
SELECT * INTO TEMP friq_word
FROM ts_stat(
'SELECT to_tsvector(''{{schema}}.{{fts_conf_name}}'', description) FROM uniq_table'
)
ORDER BY ndoc DESC;
Получили временную таблицу friq_word с уникальным списком слов (word), количеством документов в которых слово упоминается (ndoc) и общим числом вхождений слова в массив текстов (nentry).
Для дальнейшего анализа необходима дополнительная информация о распределении слов в документах. Запускаем sql-команды
SELECT number, replace((a->'lexeme')::text, '"', '') AS word, json_array_length(a->'positions') AS nindoc
INTO TEMP word_in_doc
FROM (
SELECT number, row_to_json(unnest(to_tsvector('{{schema}}.{{fts_conf_name}}', description))) AS a FROM uniq_table
) AS foo
ORDER BY number;
SELECT number, sum(nindoc) AS c INTO TEMP all_words_in_doc FROM word_in_doc GROUP BY number
В итоге получаем две временные таблицы:
- word_in_doc - сколько раз (nindoc) слово (word) повторяется в документе (number),
- all_words_in_doc - количество слов (c) в документе (number)
Теперь мы готовы оценить вес каждого слова в каждом документе. Для оценки используем метрику TF-IDF. Подробно о ней можно прочитать тут. Запускаем sql-команду
COPY(
SELECT
word_in_doc.number, word_in_doc.word
, (word_in_doc.nindoc::real/all_words_in_doc.c::real*log((SELECT count(number) FROM uniq_table)/friq_word.ndoc)) AS tf_idf
, uniq_table.description
FROM word_in_doc
JOIN all_words_in_doc ON (word_in_doc.number=all_words_in_doc.number)
JOIN friq_word ON (friq_word.word=word_in_doc.word)
JOIN uniq_table ON (uniq_table.number=word_in_doc.number)
WHERE friq_word.nentry>1
ORDER BY number
) TO '/tmp/tf-idf.csv' With CSV DELIMITER ';';
В итоге получаем таблицу, сохраненную в виде csv-файла в которой:
- number - идентификатор документа,
- word - слово
- tf_idf - вес слова для каждого документа
Приведенной командой мы дополнительно исключаем слова встречающиеся в массиве документов только один раз (см. выдвинутые гипотезы).
Проблема на данном шаге. Метрика выбрана экспертным способом. Если окажется, что она непригодна для данного массива документов - эффективность алгоритма снизится
Этап 1. Анализ данных
Данный этап выполняем на python. Для этого запускаем следующий скрипт: