
Поскольку я недавно заинтересовался машинным обучением, то и решил применить технологии машинного обучения к поставленной задаче. В общем-то, это всего-лишь задача классификации — получить на вход слово и определить, к какому классу оно относится. Только вот до этого я изучал машинное обучение на Python, а сайт СНТ писал на php. Быстро погуглив, понял, что перенести модель, обученную на питоне, на php — задача не из лёгких. Проще изначально обучать модель средствами языка php. И здесь на помощь приходит проект php-ml.
В первую очередь, для обучения нужен тренировочный набор данных, или датасет. С датасетом я особо не парился — список членов СНТ у меня уже был, из него я и составил датасет. Это CSV-файл такого вида:
"Попов", "surname"
"Новикова", "surname"
"Нелли", "name"
"Михаил", "name"
"Владимировна", "patronymic"
"Сергеевич", "patronymic"
Осталось выбрать классификатор. Я начал с классификатора 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"); ?>
Модель получилась отличная, её точность 0.996. Давайте посмотрим на пример использования обученной модели:
<?php ini_set('memory_limit', '-1'); require_once __DIR__ . '/vendor/autoload.php'; use Phpml\ModelManager; $modelManager = new ModelManager(); $testData = ['Смирнова', 'Николай', 'Алексеев', 'Орлова', 'Зайцев', 'Вячеславовна', 'Ольга']; $restoredClassifier = $modelManager->restoreFromFile(realpath(dirname(__FILE__)) . DIRECTORY_SEPARATOR . "classifier-0.99606299212598.model"); print_r($restoredClassifier->predict($testData)); ?>
Результат исполнения этого кода:
Array
(
[0] => surname
[1] => name
[2] => surname
[3] => surname
[4] => surname
[5] => patronymic
[6] => name
)
Всё здорово, кроме одного: модель очень тяжёлая, файл модели весит 60 Мб. При её использовании на рядовом shared-хостинге вы будете постоянно натыкаться на memory limit.
Перебрав несколько вариантов, я остановился на классификаторе 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"); ?>
Использование — тем более:
<?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)); ?>
Зато файл модели весит всего 655 Кб. Правда, и точность поменьше: 0.980, но для моих задач хватает с лихвой.
Теперь дело за малым — расставить слова в нужном порядке. Я сделал это как-то так:
<?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); ?>
Теперь вы можете извращаться, как душе угодно, вводя в поисковую строку «Иннокентий Илларионович», «Иннокентий Забодай-Бодайло», «Забодай-Бодайло Иннокентий» и даже «Илларионович»: технологии машинного обучения классифицируют каждое слово, выстроят все слова в строгом порядке и передадут полученный результат старому доброму оператору LIKE, который уж точно что-нибудь найдёт.
Небольшое замечание по поводу обучения модели. На хостинге такими вещами лучше не заниматься — в лучшем случае, ресурсов не хватит. Я обучал модель на своём компьютере:
$ php train.php
Конечно, есть в таком методе узкие места. Думаю, как модель не обучай, какие датасеты ей не подсовывай, она никогда не отличит отчество Серге́ евич от фамилии Сергее́ вич. Но технология сама по себе интересная.
Для написания статьи был использован список самых распространённых русских фамилий, доступный по этому адресу.
Кросспостинг с моего сайта.
ссылка на оригинал статьи https://habr.com/ru/post/464033/
Добавить комментарий