Разделение текста на предложения с помощью Томита-парсера

в 5:26, , рубрики: natural language processing, nlp, php, segmentator, Алгоритмы, машинное обучение, Программирование, томита-парсер, метки:

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

Если вы начнете это делать, то довольно быстро столкнетесь с тем, что точка — это не всегда разделитель предложений (“т.к.”, “т.д.”, “т.п.”, “пр.”, “S.T.A.L.K.E.R.”). Причем эти токены не всегда будут исключениями при разбивке текста на предложения. Например, “т.п.” может быть в середине предложения, а может и в конце.

Вопросительный и восклицательный знак тоже не всегда разделяют текст на предложения. Например, “Yachoo!”. Предложения могут разделять и другие знаки, например, двоеточие (когда следует список из отдельных утверждений).

Поэтому я долго не думая поискал готовый инструмент и остановился на Томита-парсере от Яндекса. О нем и расскажу.

Вообще, Томита-парсер — это мощный инструмент для извлечения фактов из текста. Сегментатор (разбивка текста на предложения) в нем — лишь часть проекта. Томита-парсер можно скачать сразу в виде бинарника и запускать из командной строки. Мне эта система понравилась тем, что она работает на основе правил, не прихотлива к ресурсам и дает возможность настраивать процесс сегментации. А также по моим наблюдениям в большинстве случаев отлично справляется с задачей.

Еще мне понравилось, что при возникновении вопросов можно задать их на github и иногда даже получить ответ.

Запуск

Запускается Томита-парсер таким образом

$ echo "Парсеp, Разбей эти... буквы, знаки и т.п. на предложения. И покажи пож. как со словом S.T.A.L.K.E.R. получится." | ./tomita-linux64 config.proto

То есть чтение происходит из stdin, вывод — в stdout.
Результат получаем примерно такой:

[10:01:17 17:06:37] - Start.  (Processing files.)
Парсер , Разбей эти . . . буквы , знаки и т.п . на предложения . 
И покажи пож . как со словом S. T. A. L. K. E. R. получится . 
[10:01:17 17:06:37] - End.  (Processing files.)

Одна строка — одно предложение. На этом примере видно, что разбивка прошла корректно.

Особенности

На что обращаем внимание.

  • В результат добавляются пробелы перед знаками пунктуации.
  • Лишние пробелы удаляются.
  • Происходит автоматическая коррекция некоторых опечаток (например, в исходном тексте последняя буква в слове “Парсеp” — это английская “пи”, а в обработанном тексте — это уже русская “эр”).

Эти особенности могут быть как плюсами так и минусами в зависимости от того, что вы дальше будете делать с полученным текстом. Я, например, дальше по полученному тексту строю синтаксические деревья с помощью SyntaxNet, а там как раз знаки препинания должны быть отделены пробелами, так что для меня это плюс.

Настройки

Я столкнулся с тем, что при анализе предложений, содержащих адреса, система разбивает их некорректно. Пример:

$ echo "Я живу на ул. Ленина и меня зарубает время от времени." | ./tomita-linux64 config.proto
[10:01:17 18:00:38] - Start.  (Processing files.)
Я живу на ул . 
Ленина и меня зарубает время от времени . 
[10:01:17 18:00:38] - End.  (Processing files.)

Как видим, разбивка прошла некорректно. К счастью, такие вещи можно настраивать. Для этого в gzt файле прописываем

TAbbreviation "ул." {
  key = { "abbreviation_г." type = CUSTOM }
  text = "ул."
  type = NewerEOS
}

То есть просим считать, что после “ул.” предложение всегда продолжается. Пробуем:

$ echo "Я живу на ул. Ленина и меня зарубает время от времени." | ./tomita-linux64 config.proto
[10:01:17 18:20:59] - Start.  (Processing files.)
Я живу на ул. Ленина и меня зарубает время от времени . 
[10:01:17 18:20:59] - End.  (Processing files.)

Теперь все хорошо. Пример настроек я выложил на github.

Какие минусы

О некоторых особенностях я упомянул выше. Пару слов о минусах инструмента на данный момент.

Первое — это документация. Она есть, но в ней описано не все. Попробовал сейчас поискать настройку, которую описал выше — не нашел.

Второе — это отсутствие легкой возможности работы с парсером в режиме демона. Обработка одного текста за 0.3-0.4 секунды с учетом загрузки всей системы в память для меня не критична, так как вся обработка идет в фоновых процессах и среди них есть гораздо более жирные задачи. Для кого-то это может стать узким местом.

Пример вызова из PHP

Как и говорил выше, подаем входные данные в stdin, читаем из stdout. Пример ниже сделан на основе github.com/makhov/php-tomita:

<?php

class TomitaParser
{
    /**
     * @var string Path to Yandex`s Tomita-parser binary
     */
    protected $execPath;

    /**
     * @var string Path to Yandex`s Tomita-parser configuration file
     */
    protected $configPath;

    /**
     * @param string $execPath Path to Yandex`s Tomita-parser binary
     * @param string $configPath Path to Yandex`s Tomita-parser configuration file
     */
    public function __construct($execPath, $configPath)
    {
        $this->execPath = $execPath;
        $this->configPath = $configPath;
    }

    public function run($text)
    {
        $descriptors = array(
            0 => array('pipe', 'r'), // stdin
            1 => array('pipe', 'w'), // stdout
            2 => array('pipe', 'w')  // stderr
        );

        $cmd = sprintf('%s %s', $this->execPath, $this->configPath);
        $process = proc_open($cmd, $descriptors, $pipes, dirname($this->configPath));

        if (is_resource($process))
        {

            fwrite($pipes[0], $text);
            fclose($pipes[0]);

            $output = stream_get_contents($pipes[1]);

            fclose($pipes[1]);
            fclose($pipes[2]);

            return $this->processTextResult($output);
        }

        throw new Exception('proc_open fails');
    }

    /**
     * Обработка текстового результата
     * @param string $text
     * @return string[]
     */
    public function processTextResult($text)
    {
        return array_filter(explode("n", $text));
    }

}

$parser = new TomitaParser('/home/mnv/tmp/tomita/tomita-linux64', '/home/mnv/tmp/tomita/config.proto');
var_dump($parser->run('Предложение раз. Предложение два.'));

Проверяем:

$ php example.php 
/home/mnv/tmp/tomita/example.php:66:
array(2) {
  [0] =>
  string(32) "Предложение раз . "
  [1] =>
  string(32) "Предложение два . "
}

В завершение

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

Буду рад узнать из комментариев, каким инструментом для разбивки текста на предложения пользуетесь вы?

Автор: mnv

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js