Классификация русских имён с использованием технологий машинного обучения

Дмитрий Филатов
Так сложилось, что основным языком, применяемым в проектировании систем машинного обучения, стал Python. На Python написаны многие библиотеки машинного обучения, например, scikit-learn. Однако, для некоторых приложений уместнее использовать другие языки, и для них, конечно, есть свои библиотеки. Так, было бы интересно применить технологии машинного обучения на веб-сайте, а веб-сайты часто пишут на PHP. Существует замечательный проект PHP-ML, который реализует все основные алгоритмы машинного обучения на PHP. Примеров работы с этой библиотекой в Сети немного, поэтому я предлагаю разобрать алгоритм обучения и использования модели на php-ml на какой-нибудь конкретной задаче.

Одна из интереснейших задач машинного обучения - задача классификации. Для примера, попробуем классифицировать составные части полных русских имён. Предположим, у нас есть строка "Забодай-Бодайло Иннокентий Илларионович". Требуется определить, какое из этих слов - имя, какое - фамилия, а какое - отчество.

В первую очередь, нам понадобится тренировочный набор данных, или датасет. Я составил его сам, использовав списки самых распространённых русских фамилий и имён, взятые из Интернета. Датасет представляет собой обычный CSV-файл такого вида:

"Попов", "surname"
"Новикова", "surname"
"Нелли", "name"
"Михаил", "name"
"Владимировна", "patronymic"
"Сергеевич", "patronymic"

Конечно, чем больше данных будет в датасете, тем точнее получится обученная модель. Приемлемые результаты получаются при размере датасета от 400 значений. В моём списке была тысяча имён, отчеств и фамилий.

Затем следует выбрать подходящий классификатор, то есть, алгоритм классификаци. Список доступных можно посмотреть на страничке проекта на GitHub. Перебрав несколько вариантов, я остановился на классификаторе LogisticRegression. Процесс обучения нашей модели выглядит так:

<?php
declare(strict_types=1);

ini_set('memory_limit', '-1');

require_once __DIR__ . '/vendor/autoload.php';

use Phpml\Dataset\CsvDataset;
use Phpml\Dataset\ArrayDataset;
use Phpml\FeatureExtraction\TokenCountVectorizer;
use Phpml\Tokenization\NGramTokenizer;
use Phpml\CrossValidation\StratifiedRandomSplit;
use Phpml\FeatureExtraction\TfIdfTransformer;
use Phpml\Metric\Accuracy;
use Phpml\Classification\Linear\LogisticRegression;
use Phpml\SupportVectorMachine\Kernel;
use Phpml\ModelManager;
use Phpml\Pipeline;

$dataset = new CsvDataset('parts_of_name.csv', 1);

$samples = [];
foreach ($dataset->getSamples() as $sample) {
$samples[] = $sample[0];
}

$dataset = new ArrayDataset($samples, $dataset->getTargets());
$randomSplit = new StratifiedRandomSplit($dataset, 0.1);

$pipeline = new Pipeline([
new TokenCountVectorizer(new NGramTokenizer(1, 3)),
new TfIdfTransformer()
], new LogisticRegression());
$pipeline->train($randomSplit->getTrainSamples(), $randomSplit->getTrainLabels());

$predictedLabels = $pipeline->predict($randomSplit->getTestSamples());
echo 'Accuracy: '.Accuracy::score($randomSplit->getTestLabels(), $predictedLabels);

$modelManager = new ModelManager();
$modelManager->saveToFile($pipeline, realpath(dirname(__FILE__)) . DIRECTORY_SEPARATOR . "classifier-".Accuracy::score($randomSplit->getTestLabels(), $predictedLabels).".model");
?>

Обученную модель мы сохраняем в файл для дальнейшего использования. А ещё, как вы могли заметить по коду, мы оцениваем точность полученной модели. В случае классификатора SVC точность получается очень высокой - 0.996, но и размер модели почти 60 Мб. Классификатор LogisticRegression даёт точность 0.980, зато файл модели весит всего 655 Кб. Этот параметр критичен, потому что при использовании большой модели на рядовом хостинге вы всё время будете упираться в memory limit - нехватку оперативной памяти, выделенной сервером для ваших php-скриптов.

По этой же причине, обучить модель на сервере вашего хостера, скорее всего, не получится - процесс обучения очень требователен к памяти. В лучшем случае - вам просто ресурсов не хватит, в худшем - хостер вас выгонит. Я обучал модель на своём компьютере:

$ php train.php

Давайте попробуем использовать обученную модель для классификации неизвестных ей слов. Применяется обученная модель так:

<?php
require_once __DIR__ . '/vendor/autoload.php';

use Phpml\ModelManager;

$modelManager = new ModelManager();

$testData = ['Смирнова', 'Николай', 'Алексеев', 'Орлова', 'Зайцев', 'Вячеславовна', 'Яна'];

$restoredClassifier = $modelManager->restoreFromFile(realpath(dirname(__FILE__)) . DIRECTORY_SEPARATOR . "classifier-0.98031496062992.model");
print_r($restoredClassifier->predict($testData));
?>

Этот скрипт уже можно выполнять на сервере хостинг-провайдера. Результат его выполнения будет примерно таким:

Array
(
[0] => surname
[1] => name
[2] => surname
[3] => surname
[4] => surname
[5] => patronymic
[6] => name
)

Каждому слову была присвоена метка, классифицирующая это слово.

Конечно, есть в таком методе узкие места. Думаю, как модель не обучай, какие датасеты ей не подсовывай, она никогда не отличит отчество Серге́евич от фамилии Сергее́вич. Но технология сама по себе интересная.

Дополнительно

Если хотите попробовать другой классификатор, скажем, упомянутый в статье SVC, то вот пример кода:

<?php
declare(strict_types=1);

ini_set('memory_limit', '-1');

require_once __DIR__ . '/vendor/autoload.php';

use Phpml\Dataset\CsvDataset;
use Phpml\Dataset\ArrayDataset;
use Phpml\FeatureExtraction\TokenCountVectorizer;
use Phpml\Tokenization\NGramTokenizer;
use Phpml\CrossValidation\StratifiedRandomSplit;
use Phpml\FeatureExtraction\TfIdfTransformer;
use Phpml\Metric\Accuracy;
use Phpml\Classification\SVC;
use Phpml\SupportVectorMachine\Kernel;
use Phpml\ModelManager;
use Phpml\Pipeline;

$dataset = new CsvDataset('parts_of_name.csv', 1);

$samples = [];
foreach ($dataset->getSamples() as $sample) {
$samples[] = $sample[0];
}

$dataset = new ArrayDataset($samples, $dataset->getTargets());
$randomSplit = new StratifiedRandomSplit($dataset, 0.1);

$pipeline = new Pipeline([
new TokenCountVectorizer(new NGramTokenizer(1, 3)),
new TfIdfTransformer()
], new SVC(Kernel::RBF, 10000));
$pipeline->train($randomSplit->getTrainSamples(), $randomSplit->getTrainLabels());

$predictedLabels = $pipeline->predict($randomSplit->getTestSamples());
echo 'Accuracy: '.Accuracy::score($randomSplit->getTestLabels(), $predictedLabels);

$modelManager = new ModelManager();
$modelManager->saveToFile($pipeline, realpath(dirname(__FILE__)) . DIRECTORY_SEPARATOR . "classifier-".Accuracy::score($randomSplit->getTestLabels(), $predictedLabels).".model");
?>

Использование обученной модели отличается только названием файла модели, вот в этом месте:

	$restoredClassifier = $modelManager->restoreFromFile(realpath(dirname(__FILE__)) . DIRECTORY_SEPARATOR . "classifier-0.98031496062992.model");

В начале статьи я упоминал, что наша цель - сопоставить каждому слову из строки свою метку. Сделать это с обученной моделью можно, например, так:

<?php
$words = explode(' ', $query);
$model = new \Phpml\ModelManager();
$classifier = $model->restoreFromFile(__DIR__ . '/classifier-0.98.model');
$build = Array();
foreach($words as $name_part) {
$order = -1;
$type = $classifier->predict([$name_part]);
switch($type[0]) {
case 'surname':
$order = 0;
break;
case 'name':
$order = 1;
break;
case 'patronymic':
$order = 2;
break;
default:
$order = -1;
}
$build[$order] = $name_part;
}
//Убираем повторяющиеся значения
$build = array_unique($build);
//Удаляем ненужное
unset($build[-1]);
//Сортируем массив
ksort($build);
//Склеиваем в строку
$full_name = implode(' ', $build);
?>

И ещё, обратите внимание на этот фрагмент кода:

		new TokenCountVectorizer(new NGramTokenizer(1, 3)),
new TfIdfTransformer()

Любая система машинного обучения работает только с числами, и наша главная задача - грамотно превратить входной объект в набор чисел. Если мы классифицируем изображения - там будут свои методики. Для слов же лучший способ превращения в набор чисел - разбивка на N-граммы, но при этом важно также учесть частоту встречаемости каждой N-граммы, иначе любая наша модель будет ориентирована только на предлоги и союзы, наиболее часто встречающиеся в тексте, но не несущие никакой информации о смысле этого текста. Для определения значимости каждой N-граммы используется такая мера, как TF-IDF.
2019-08-18