Оригинал: AI Without the Hype: Practical Machine Learning in Symfony
Перевод для канала Мы ж программист
Почему разработчикам PHP стоит интересоваться ИИ
PHP лежит в основе 77% веб-сайтов, использующих известный серверный язык. На Symfony работают некоторые из крупнейших платформ в Интернете. Но способен ли PHP справиться с ИИ и машинным обучением? Да. Вы можете создавать интеллектуальные приложения, не переходя на Python.
В этой статье я покажу вам, как интегрировать машинное обучение в приложения на Symfony. Вы научитесь классифицировать текст, прогнозировать результаты и обрабатывать изображения. И всё это с помощью PHP.
Настройка окружения
Начните с Symfony 7. Вам понадобится PHP 8.2 или выше. Установите пакеты:
composer require symfony/http-client
composer require php-ai/php-ml
composer require rubix/mlPHP-ML предоставляет вам встроенные алгоритмы машинного обучения. Rubix ML предлагает более полный набор инструментов. Оба хорошо работают с Symfony.
Создайте новый файл конфигурации сервиса:
# config/services.yaml
services:
App\Service\MachineLearning\:
resource: '../src/Service/MachineLearning/'
tags: ['app.ml_service']Эта конфигурация обеспечивает автоматическую интеграцию ваших сервисов машинного обучения. Вы можете подключить их в любом месте вашего приложения.
Подключение к внешним API ИИ
OpenAI, Claude и Hugging Face предлагают мощные API. Ваше приложение на Symfony может обращаться к ним. Вот сервис, который оборачивает вызовы API:
<?php
namespace App\Service\MachineLearning;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class AIApiClient
{
private const OPENAI_API = 'https://api.openai.com/v1/chat/completions';
public function __construct(
private HttpClientInterface $httpClient,
private string $apiKey
) {}
public function classifyText(string $text, array $categories): array
{
$prompt = $this->buildClassificationPrompt($text, $categories);
$response = $this->httpClient->request('POST', self::OPENAI_API, [
'headers' => [
'Authorization' => 'Bearer ' . $this->apiKey,
'Content-Type' => 'application/json',
],
'json' => [
'model' => 'gpt-4',
'messages' => [
['role' => 'system', 'content' => 'You are a text classifier.'],
['role' => 'user', 'content' => $prompt]
],
'temperature' => 0.3,
],
]);
$data = $response->toArray();
return $this->parseClassification($data);
}
private function buildClassificationPrompt(string $text, array $categories): string
{
$categoriesList = implode(', ', $categories);
return "Classify this text into one of these categories: {$categoriesList}.\n\nText: {$text}\n\nRespond with only the category name.";
}
private function parseClassification(array $response): array
{
$category = trim($response['choices'][0]['message']['content']);
return [
'category' => $category,
'tokens_used' => $response['usage']['total_tokens'],
];
}
}Сохраните API-ключ в .env:
OPENAI_API_KEY=your_key_hereПрокиньте его в ваши сервисы:
# config/services.yaml
services:
App\Service\MachineLearning\AIApiClient:
arguments:
$apiKey: '%env(OPENAI_API_KEY)%'Нативное машинное обучение с помощью PHP-ML
Не всегда требуются внешние API. PHP-ML позволяет обучать модели локально. Вот пример анализатора тональности:
<?php
namespace App\Service\MachineLearning;
use Phpml\Classification\NaiveBayes;
use Phpml\FeatureExtraction\TokenCountVectorizer;
use Phpml\Tokenization\WordTokenizer;
class SentimentAnalyzer
{
private NaiveBayes $classifier;
private TokenCountVectorizer $vectorizer;
public function __construct()
{
$this->vectorizer = new TokenCountVectorizer(new WordTokenizer());
$this->classifier = new NaiveBayes();
}
public function train(array $samples, array $labels): void
{
$this->vectorizer->fit($samples);
$this->vectorizer->transform($samples);
$this->classifier->train($samples, $labels);
}
public function predict(string $text): string
{
$sample = [$text];
$this->vectorizer->transform($sample);
return $this->classifier->predict($sample[0]);
}
public function saveModel(string $path): void
{
file_put_contents(
$path,
serialize([
'classifier' => $this->classifier,
'vectorizer' => $this->vectorizer,
])
);
}
public function loadModel(string $path): void
{
$data = unserialize(file_get_contents($path));
$this->classifier = $data['classifier'];
$this->vectorizer = $data['vectorizer'];
}
}Обучите его консольной командой:
<?php
namespace App\Command;
use App\Service\MachineLearning\SentimentAnalyzer;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'app:train-sentiment')]
class TrainSentimentCommand extends Command
{
public function __construct(
private SentimentAnalyzer $analyzer,
private string $projectDir
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$samples = [
'I love this product',
'This is amazing',
'Best purchase ever',
'I hate this',
'Terrible experience',
'Waste of money',
];
$labels = [
'positive',
'positive',
'positive',
'negative',
'negative',
'negative',
];
$output->writeln('Training model...');
$this->analyzer->train($samples, $labels);
$modelPath = $this->projectDir . '/var/models/sentiment.model';
$this->analyzer->saveModel($modelPath);
$output->writeln('Model saved to: ' . $modelPath);
return Command::SUCCESS;
}
}Запустите:
php bin/console app:train-sentimentИспользуйте в вашем контроллере:
<?php
namespace App\Controller;
use App\Service\MachineLearning\SentimentAnalyzer;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class SentimentController extends AbstractController
{
public function __construct(
private SentimentAnalyzer $analyzer,
private string $projectDir
) {
$modelPath = $this->projectDir . '/var/models/sentiment.model';
$this->analyzer->loadModel($modelPath);
}
#[Route('/api/sentiment', methods: ['POST'])]
public function analyze(Request $request): JsonResponse
{
$text = $request->request->get('text');
if (!$text) {
return $this->json(['error' => 'Text is required'], 400);
}
$sentiment = $this->analyzer->predict($text);
return $this->json([
'text' => $text,
'sentiment' => $sentiment,
]);
}
}Создание системы рекомендаций товаров
Сайтам электронной коммерции необходимы интеллектуальные рекомендации. Представляем систему совместной фильтрации на базе Rubix ML:
<?php
namespace App\Service\MachineLearning;
use Rubix\ML\Datasets\Labeled;
use Rubix\ML\Regressors\KNearestNeighbors;
use Rubix\ML\Kernels\Distance\Euclidean;
class RecommendationEngine
{
private KNearestNeighbors $estimator;
private array $productIds;
public function __construct()
{
$this->estimator = new KNearestNeighbors(5, new Euclidean());
$this->productIds = [];
}
public function train(array $userPreferences): void
{
$samples = [];
$labels = [];
foreach ($userPreferences as $userId => $prefs) {
$samples[] = $prefs['features'];
$labels[] = $prefs['rating'];
$this->productIds[] = $prefs['product_id'];
}
$dataset = new Labeled($samples, $labels);
$this->estimator->train($dataset);
}
public function recommend(array $userFeatures, int $limit = 5): array
{
$predictions = $this->estimator->predict(
new \Rubix\ML\Datasets\Unlabeled([$userFeatures])
);
$scored = array_map(function($id, $score) {
return ['product_id' => $id, 'score' => $score];
}, $this->productIds, $predictions);
usort($scored, fn($a, $b) => $b['score'] <=> $a['score']);
return array_slice($scored, 0, $limit);
}
}Создайте сервис, чтобы собрать пользовательские данные:
<?php
namespace App\Service;
use App\Entity\User;
use App\Entity\Purchase;
use Doctrine\ORM\EntityManagerInterface;
class UserPreferenceCollector
{
public function __construct(
private EntityManagerInterface $entityManager
) {}
public function collectForUser(User $user): array
{
$purchases = $this->entityManager
->getRepository(Purchase::class)
->findBy(['user' => $user], ['createdAt' => 'DESC'], 50);
$categoryFrequency = [];
$priceRange = [];
foreach ($purchases as $purchase) {
$product = $purchase->getProduct();
$category = $product->getCategory();
$categoryFrequency[$category] =
($categoryFrequency[$category] ?? 0) + 1;
$priceRange[] = $product->getPrice();
}
return [
'category_preference' => $this->normalizeCategories($categoryFrequency),
'avg_price' => array_sum($priceRange) / count($priceRange),
'purchase_frequency' => count($purchases),
];
}
private function normalizeCategories(array $frequencies): array
{
$total = array_sum($frequencies);
return array_map(
fn($count) => $count / $total,
$frequencies
);
}
}Классификация изображений с помощью Symfony
Распознавание изображений требует большего объема ресурсов. Можно использовать внешние сервисы или TensorFlow через PHP-биндинги. Вот практический подход с использованием Hugging Face:
<?php
namespace App\Service\MachineLearning;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class ImageClassifier
{
private const HF_API = 'https://api-inference.huggingface.co/models/';
public function __construct(
private HttpClientInterface $httpClient,
private string $hfToken
) {}
public function classify(string $imagePath, string $model = 'google/vit-base-patch16-224'): array
{
$imageData = file_get_contents($imagePath);
$response = $this->httpClient->request('POST', self::HF_API . $model, [
'headers' => [
'Authorization' => 'Bearer ' . $this->hfToken,
],
'body' => $imageData,
]);
return $response->toArray();
}
public function classifyUploadedFile(\Symfony\Component\HttpFoundation\File\UploadedFile $file): array
{
$tempPath = $file->getRealPath();
return $this->classify($tempPath);
}
}Используйте в контроллере:
<?php
namespace App\Controller;
use App\Service\MachineLearning\ImageClassifier;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class ImageAnalysisController extends AbstractController
{
#[Route('/api/classify-image', methods: ['POST'])]
public function classify(Request $request, ImageClassifier $classifier): JsonResponse
{
/** @var UploadedFile $file */
$file = $request->files->get('image');
if (!$file) {
return $this->json(['error' => 'No image uploaded'], 400);
}
$allowedMimes = ['image/jpeg', 'image/png', 'image/webp'];
if (!in_array($file->getMimeType(), $allowedMimes)) {
return $this->json(['error' => 'Invalid image format'], 400);
}
$results = $classifier->classifyUploadedFile($file);
return $this->json([
'filename' => $file->getClientOriginalName(),
'classifications' => $results,
]);
}
}Прогнозирование временных рядов
Прогнозируйте объемы продаж, посещаемость или потребности в запасах. Вот модель линейной регрессии:
<?php
namespace App\Service\MachineLearning;
use Phpml\Regression\LeastSquares;
class TimeSeriesPredictor
{
private LeastSquares $regression;
public function __construct()
{
$this->regression = new LeastSquares();
}
public function train(array $historicalData): void
{
$samples = [];
$targets = [];
foreach ($historicalData as $index => $value) {
$samples[] = [$index];
$targets[] = $value;
}
$this->regression->train($samples, $targets);
}
public function predict(int $daysAhead): float
{
$lastIndex = count($this->regression->getSamples()) - 1;
$futureIndex = $lastIndex + $daysAhead;
return $this->regression->predict([$futureIndex]);
}
public function predictRange(int $start, int $end): array
{
$predictions = [];
for ($i = $start; $i <= $end; $i++) {
$predictions[$i] = $this->predict($i);
}
return $predictions;
}
}Создайте команду для предсказания будущих продаж:
<?php
namespace App\Command;
use App\Service\MachineLearning\TimeSeriesPredictor;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'app:predict-sales')]
class PredictSalesCommand extends Command
{
public function __construct(
private TimeSeriesPredictor $predictor,
private EntityManagerInterface $entityManager
) {
parent::__construct();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$sql = "SELECT DATE(created_at) as date, SUM(amount) as total
FROM orders
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 90 DAY)
GROUP BY DATE(created_at)
ORDER BY date";
$results = $this->entityManager
->getConnection()
->executeQuery($sql)
->fetchAllAssociative();
$salesData = array_column($results, 'total');
$this->predictor->train($salesData);
$predictions = $this->predictor->predictRange(1, 7);
$output->writeln('Sales predictions for next 7 days:');
foreach ($predictions as $day => $amount) {
$output->writeln(sprintf('Day %d: $%.2f', $day, $amount));
}
return Command::SUCCESS;
}
}Обработка естественного языка
Извлечение смысла из текста. Вот пример экстрактора сущностей:
<?php
namespace App\Service\MachineLearning;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class EntityExtractor
{
public function __construct(
private HttpClientInterface $httpClient,
private string $apiKey
) {}
public function extract(string $text): array
{
$response = $this->httpClient->request('POST',
'https://api.openai.com/v1/chat/completions', [
'headers' => [
'Authorization' => 'Bearer ' . $this->apiKey,
'Content-Type' => 'application/json',
],
'json' => [
'model' => 'gpt-4',
'messages' => [
[
'role' => 'system',
'content' => 'Extract people, organizations, and locations. Return JSON only.'
],
[
'role' => 'user',
'content' => $text
]
],
'temperature' => 0.0,
],
]);
$data = $response->toArray();
$content = $data['choices'][0]['message']['content'];
return json_decode($content, true);
}
}Используйте для обогащения своего контента:
<?php
namespace App\Service;
use App\Entity\Article;
use App\Service\MachineLearning\EntityExtractor;
use Doctrine\ORM\EntityManagerInterface;
class ArticleEnricher
{
public function __construct(
private EntityExtractor $extractor,
private EntityManagerInterface $entityManager
) {}
public function enrichArticle(Article $article): void
{
$entities = $this->extractor->extract($article->getContent());
$article->setExtractedPeople($entities['people'] ?? []);
$article->setExtractedOrganizations($entities['organizations'] ?? []);
$article->setExtractedLocations($entities['locations'] ?? []);
$this->entityManager->flush();
}
}Кэширование и производительность
Операции искусственного интеллекта требуют значительных ресурсов. Активно используйте кэширование:
<?php
namespace App\Service\MachineLearning;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
class CachedAIService
{
public function __construct(
private AIApiClient $apiClient,
private CacheInterface $cache
) {}
public function classifyText(string $text, array $categories): array
{
$cacheKey = 'classification_' . md5($text . implode('', $categories));
return $this->cache->get($cacheKey, function (ItemInterface $item) use ($text, $categories) {
$item->expiresAfter(86400); // 24 hours
return $this->apiClient->classifyText($text, $categories);
});
}
}Добавьте ограничение скорости:
<?php
namespace App\Service\MachineLearning;
use Symfony\Component\RateLimiter\RateLimiterFactory;
class RateLimitedAIService
{
public function __construct(
private AIApiClient $apiClient,
private RateLimiterFactory $aiApiLimiter
) {}
public function classifyText(string $text, array $categories, string $userId): array
{
$limiter = $this->aiApiLimiter->create($userId);
if (!$limiter->consume(1)->isAccepted()) {
throw new \RuntimeException('Rate limit exceeded. Try again later.');
}
return $this->apiClient->classifyText($text, $categories);
}
}Настройте ограничитель:
# config/packages/rate_limiter.yaml
framework:
rate_limiter:
ai_api:
policy: 'sliding_window'
limit: 100
interval: '1 hour'Тестирование ML-сервисов
Тестируйте ваш ML-код как любой другой сервис:
<?php
namespace App\Tests\Service\MachineLearning;
use App\Service\MachineLearning\SentimentAnalyzer;
use PHPUnit\Framework\TestCase;
class SentimentAnalyzerTest extends TestCase
{
private SentimentAnalyzer $analyzer;
protected function setUp(): void
{
$this->analyzer = new SentimentAnalyzer();
$samples = [
'I love this',
'This is great',
'I hate this',
'This is terrible',
];
$labels = ['positive', 'positive', 'negative', 'negative'];
$this->analyzer->train($samples, $labels);
}
public function testPositiveSentiment(): void
{
$result = $this->analyzer->predict('I really enjoy this');
$this->assertEquals('positive', $result);
}
public function testNegativeSentiment(): void
{
$result = $this->analyzer->predict('I dislike this product');
$this->assertEquals('negative', $result);
}
}Замените моками внешние вызовы API:
<?php
namespace App\Tests\Service\MachineLearning;
use App\Service\MachineLearning\AIApiClient;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
class AIApiClientTest extends TestCase
{
public function testClassifyText(): void
{
$mockResponse = new MockResponse(json_encode([
'choices' => [
[
'message' => [
'content' => 'technology'
]
]
],
'usage' => [
'total_tokens' => 50
]
]));
$httpClient = new MockHttpClient($mockResponse);
$client = new AIApiClient($httpClient, 'fake-key');
$result = $client->classifyText('AI news', ['technology', 'sports']);
$this->assertEquals('technology', $result['category']);
$this->assertEquals(50, $result['tokens_used']);
}
}Соглашения по безопасности
Защитите ваши эндпоинты:
<?php
namespace App\Security\Voter;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class AIFeatureVoter extends Voter
{
public const USE_AI = 'ai.use';
protected function supports(string $attribute, mixed $subject): bool
{
return $attribute === self::USE_AI;
}
protected function voteOnAttribute(
string $attribute,
mixed $subject,
TokenInterface $token
): bool {
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
return $user->hasSubscription() && $user->getAiQuota() > 0;
}
}Очищайте данные, вводимые пользователем:
<?php
namespace App\Service;
class InputSanitizer
{
public function sanitizeForAI(string $input): string
{
$input = strip_tags($input);
$input = preg_replace('/[^\w\s\.,!?-]/u', '', $input);
return mb_substr($input, 0, 5000);
}
}Мониторинг и логирование
Контролируйте использование ИИ:
<?php
namespace App\Service\MachineLearning;
use Psr\Log\LoggerInterface;
class MonitoredAIService
{
public function __construct(
private AIApiClient $apiClient,
private LoggerInterface $logger
) {}
public function classifyText(string $text, array $categories): array
{
$startTime = microtime(true);
try {
$result = $this->apiClient->classifyText($text, $categories);
$duration = microtime(true) - $startTime;
$this->logger->info('AI classification completed', [
'duration' => $duration,
'tokens' => $result['tokens_used'],
'category' => $result['category'],
]);
return $result;
} catch (\Exception $e) {
$this->logger->error('AI classification failed', [
'error' => $e->getMessage(),
'text_length' => strlen($text),
]);
throw $e;
}
}
}Реальный пример: интеллектуальная служба поддержки
Сложим всё воедино. Вот готовый классификатор запросов в службу поддержки:
<?php
namespace App\Service;
use App\Entity\SupportTicket;
use App\Service\MachineLearning\AIApiClient;
use App\Service\MachineLearning\SentimentAnalyzer;
use Doctrine\ORM\EntityManagerInterface;
class SmartTicketProcessor
{
public function __construct(
private AIApiClient $aiClient,
private SentimentAnalyzer $sentimentAnalyzer,
private EntityManagerInterface $entityManager
) {}
public function process(SupportTicket $ticket): void
{
$categories = ['billing', 'technical', 'account', 'general'];
$classification = $this->aiClient->classifyText(
$ticket->getDescription(),
$categories
);
$sentiment = $this->sentimentAnalyzer->predict($ticket->getDescription());
$ticket->setCategory($classification['category']);
$ticket->setSentiment($sentiment);
$ticket->setPriority($this->calculatePriority($sentiment, $ticket));
$this->entityManager->flush();
$this->routeTicket($ticket);
}
private function calculatePriority(string $sentiment, SupportTicket $ticket): int
{
$priority = 3;
if ($sentiment === 'negative') {
$priority = 2;
}
if ($ticket->getCustomer()->isVip()) {
$priority = min(1, $priority - 1);
}
return $priority;
}
private function routeTicket(SupportTicket $ticket): void
{
$routing = [
'billing' => 'billing@company.com',
'technical' => 'tech@company.com',
'account' => 'accounts@company.com',
'general' => 'support@company.com',
];
$ticket->setAssignedTo($routing[$ticket->getCategory()]);
$this->entityManager->flush();
}
}Создайте подписчика событий для автоматической обработки тикетов:
<?php
namespace App\EventSubscriber;
use App\Entity\SupportTicket;
use App\Service\SmartTicketProcessor;
use Doctrine\ORM\Events;
use Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventSubscriberInterface;
use Doctrine\ORM\Event\PostPersistEventArgs;
class TicketSubscriber implements EventSubscriberInterface
{
public function __construct(
private SmartTicketProcessor $processor
) {}
public function getSubscribedEvents(): array
{
return [
Events::postPersist,
];
}
public function postPersist(PostPersistEventArgs $args): void
{
$entity = $args->getObject();
if (!$entity instanceof SupportTicket) {
return;
}
$this->processor->process($entity);
}
}Лучшие практики
Обучайте модели в автономном режиме. Не проводите обучение во время обработки веб-запросов. Используйте консольные команды. Сохраняйте обученные модели на диск.
Кэшируйте всё. Вызовы API требуют затрат времени и средств. Кэшируйте данные как минимум на час. Используйте Redis или кэш Symfony.
Проверяйте размер входных данных. Ограничьте текст 5000 символами. Ограничьте размер изображений 5 МБ. Сразу отклоняйте неверные форматы.
Правильно обрабатывайте сбои. API могут давать сбои. Сеть может превысить время ожидания. Перехватывайте исключения. Возвращайте резервные ответы.
Контролируйте расходы. Отслеживайте использование API. Устанавливайте оповещения о превышении бюджета. Регистрируйте использование токенов.
Тестируйте на реальных данных. Ваши обучающие данные имеют значение. Используйте реальные сообщения клиентов. Обновляйте модели ежемесячно.
Следующие шаги
Начните с малого. Выберите одну функцию. Добавьте анализ тональности комментариев. Или классифицируйте заявки в службу поддержки.
Оценивайте результаты. Отслеживайте точность. Контролируйте время отклика. Собирайте отзывы пользователей.
Постепенно расширяйте масштабы. Начните с кэширования. Добавьте очереди для ресурсоемких задач. Перенесите обучение в фоновые процессы.
Продолжайте учиться. Машинное обучение быстро развивается. Следите за библиотеками машинного обучения для PHP. Пробуйте новые модели.
