- BrainTools - https://www.braintools.ru -

AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP

Это вторая часть статьи.
Часть 1: Практика без Python и data science [1]

AI в PHP: не теория, а место, с которого можно начать

В своей прошлой статье [1] я описал на довольно общем уровне почему тема AI вроде бы везде, но при этом почти не пересекается с повседневной PHP-разработкой. Не потому что PHP “не подходит”, а потому что сам разговор обычно идёт мимо наших задач и привычного способа мышления [2]. Ну и, конечно, о том, что почти нет материала, который объясняет AI именно для PHP-разработчиков, их задач и их мышления.

После публикации мне несколько раз задали один и тот же вопрос, в разных формулировках:

Окей, допустим. А с чего конкретно начать?

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

Про “точку входа”

Когда вы слышите “использование AI в проекте”, скорее всего в вашей голове сразу возникает слишком много лишнего: инфраструктура, обучение [3] моделей, эксперименты, отдельные сервисы, новые роли в команде и т.д.

Но если честно, в большинстве PHP-проектов нам это просто не нужно (хотя разобраться в этом самому – жутко интересная задача).

Но всё же, нам с вами не нужно:

  • обучать модели с нуля

  • разбираться в градиентном спуске

  • собирать датасеты и т.д.

Нужно понять, что AI ≠ Data Science. Друзья, в большинстве PHP-проектов никто не обучает модели, пожалуйста, запомните это! По крайней мере в прикладных PHP-проектах, где мы говорим об использовании готовых моделей, а не об исследованиях и обучении.

Нам нужно вз��ть готовый инструмент и аккуратно встроить его в уже существующую логику [4]. Так же, как мы это делаем с любой другой библиотекой.

И вот тут начинается интересное.

Что это вообще за зверь такой TransformersPHP и с чем его едят?

Про использование LLM через API знают уже все. Это полезно и удобно, но в таком виде AI остаётся чем-то внешним: сервисом, к которому ты просто отправляешь текст. Внутри – туман.

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

И здесь неожиданно выяснилось, что есть инструменты, которые позволяют делать это напрямую в PHP, без Python-стека. Один из них – TransformersPHP [5].

Важно сразу понять: это не попытка превратить PHP в Python и не универсальное решение. Это библиотека для inference [6] (инференс) – использования уже обученных моделей.

Как по мне, TransformersPHP – один из самых интересных и показательных проектов в современной PHP ML-экосистеме. Отдельное спасибо его автору – Kyrian Obikwelu [7] за то что создал этот проект и продолжает над ним работать. В общем это библиотека, которая позволяет использовать трансформер-модели (BERT, RoBERTa, DistilBERT и др.) напрямую из PHP, без Python и без внешних API.

По сути, это PHP-ориентированная обертка над идеями Hugging Face Transformers [8], адаптированная под PHP-экосистему и реальные прикладные сценарии.

Ключевая особенность библиотеки – локальный инференс. Модели загружаются и выполняются на стороне PHP-приложения (через ONNX Runtime [9]), что открывает важные архитектурные возможности:

  • отсутствие сетевых вызовов к LLM API

  • полный контроль над данными (это может быть важно для privacy (конфиденциальности) в вашей работе)

  • предсказуемое и стабильное время обработки запроса

  • возможность оффлайн-работы (после первого запуска и загрузки модели)

TransformersPHP поддерживает типовые задачи NLP, такие как: получение эмбеддингов, классификацию текста, семантическое сравнение и прочее.

Что мне нравится больше всего

Самое ценное ощущение – отсутствие разрыва контекста.

Я остаюсь в PHP, я пишу тот же код, у меня тот же деплой и у меня те же подходы к архитектуре. Ничего не изменилось. Модель для меня в этом случае – не некий “магический объект”, а просто ещё один источник данных. Да, непривычный, но вполне объяснимый – не хуже и не лучше других.

И это сильно меняет отношение к всей теме. Вы согласны?

Запуск модели за 10 минут

Один из самых важных моментов – это первый запуск.
Если он сложный, на этом всё обычно и заканчивается.

В случае с TransformersPHP ощущение как раз обратное: это больше похоже на работу с обычной зависимостью.

Полное руководство по установке можно найти на сайте документации [10].

Мы же с вами для простоты запустим докер контейнер со всеми необходимыми зависимостями. Да, Docker file выглядит объёмно – но это одноразовая инфраструктура. Сам запуск и первый демо-пример действительно укладываются в несколько минут.

Что нам нужно (требования)

  • PHP 8.1 или выше

  • Composer

  • Расширение PHP FFI

  • JIT-компиляция (опционально, для повышения производительности)

  • Увеличенный лимит памяти [11] (для сложных задач, таких как генерация текста)

Структура проекта

/project/
  ├── app/
  │    ├── demo.php
  │    ├── semantic-search.php
  ├── docker/
  │    ├── Dockerfile
  ├── docker-composer.yaml
  ├── composer.json
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 1 [12]

Установка (Docker)

Файл: docker-compose.yml
networks:
  ai-for-php-developers:
    driver: bridge

services:
  app:
    build:
      context: .
      dockerfile: docker/Dockerfile
    volumes:
      - .:/var/www
    ports:
      - "8088:8088"
    command: php -S 0.0.0.0:8088 -t app
    networks:
      - ai-for-php-developers
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 2 [12]
Файл: docker/Dockerfile
# ------------------------------
# Install system dependencies
# ------------------------------
RUN apt-get update && apt-get install -y 
    libzip-dev 
    zip 
    unzip 
    git 
    libxml2-dev 
    libcurl4-openssl-dev 
    libpng-dev 
    libonig-dev 
    && rm -rf /var/lib/apt/lists/*

# ------------------------------
# Install PHP extensions
# ------------------------------
RUN docker-php-ext-install zip pdo_mysql bcmath xml mbstring curl gd pcntl

# ------------------------------
# Enable FFI
# ------------------------------
RUN apt-get update && apt-get install -y 
    libffi-dev 
    pkg-config 
    && rm -rf /var/lib/apt/lists/*
RUN docker-php-ext-install ffi
RUN echo "ffi.enable=1" > /usr/local/etc/php/conf.d/ffi.ini

# ------------------------------
# Install ONNX Runtime
# ------------------------------
ENV ONNXRUNTIME_VERSION=1.17.1

RUN curl -L https://github.com/microsoft/onnxruntime/releases/download/v${ONNXRUNTIME_VERSION}/onnxruntime-linux-x64-${ONNXRUNTIME_VERSION}.tgz 
    | tar -xz 
    && cp onnxruntime-linux-x64-${ONNXRUNTIME_VERSION}/lib/libonnxruntime.so* /usr/lib/ 
    && ldconfig 
    && rm -rf onnxruntime-linux-x64-${ONNXRUNTIME_VERSION}


# ------------------------------
# Install Composer
# ------------------------------
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Set working directory
WORKDIR /var/www

# Copy existing application directory contents
COPY . /var/www

# Configure PHP
RUN echo "memory_limit = 512M" >> /usr/local/etc/php/conf.d/docker-php-ram-limit.ini
RUN echo "max_execution_time = 300" >> /usr/local/etc/php/conf.d/docker-php-max-execution-time.ini

# Expose port 9000 and start php-fpm server
EXPOSE 9000
CMD ["php-fpm"]
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 3 [12]

И для установки самого TransformersPHP

Файл: composer.json
{
    "type": "project",
    "minimum-stability": "stable",
    "prefer-stable": true,
    "require": {
        "codewithkyrian/transformers": "~0.6.2"
    },
    "config": {
        "allow-plugins": {
            "codewithkyrian/platform-package-installer": true
        }
    }
}
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 4 [12]

Команда для запуска окружения

docker compose build --pull
docker compose up -d
docker compose exec app /bin/bash -c "composer install"
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 5 [12]

Идея простая: чтобы пример можно было поднять локально без ручной настройки окружения.

Базовый демо-пример

Начнём с самого простого: анализа настроений.

Если всё, что описано выше сработало хорошо и установка прошла нормально, можно запустить пример, описанный ниже (примите во внимание [13], что первый запуск может занять несколько секунд):

docker compose exec app php app/demo.php
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 6 [12]

Пример использования выглядит концептуально просто: вы загружаете предобученную модель и применяете ее к тексту так же, как это делали бы в Python – но уже внутри PHP-кода. TransformersPHP предлагает простой pipeline API для задач вроде анализа настроений, классификации текста, семантического сравнения и т.д. В примере ниже модель определяет тональность двух фраз и показывает метку и score.

Файл: app/demo.php

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

use function CodewithkyrianTransformersPipelinespipeline;

// Выделить конвейер для анализа настроений
$classifier = pipeline('sentiment-analysis');

$out = $classifier(['I love transformers!']);
echo 'I love transformers!';
echo print_r($out, true);

$out = $classifier(['I hate transformers!']);
echo 'I hate transformers!';
echo print_r($out, true);
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 7 [12]

Результат, конечно, же вполне ожидаемый:

I love transformers!
Array ( 
  [label] => POSITIVE 
  [score] => 0.99978870153427 
)

I hate transformers!
Array ( 
  [label] => NEGATIVE 
  [score] => 0.99863630533218 
)
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 8 [12]

Цель этого примера – снять у вас психологический барьер: эта модель в PHP — это просто ещё один объект, с которым можно работать.

Этот же пример можно запустить онлайн [14].

Важно понимать архитектурную роль TransformersPHP.

Эта библиотека не конкурирует с большими LLM-сервисами вроде GPT или Claude. Она закрывает другой, очень важный слой:

  • быстрые эмбеддинги

  • локальная классификация

  • семантический поиск

  • lightweight NLP без внешних зависимостей

В связке с PHP это выглядит особенно логично. PHP остается центральным слоем бизнес-логики, а трансформеры становятся встроенным инструментом, а не удаленным сервисом. TransformersPHP – это хороший пример того, как современный ML постепенно перестает быть “чужим” для PHP и становится частью его нативной экосистемы, пусть и через аккуратные инженерные мосты вроде ONNX.

Реальный кейс: семантический поиск по событиям

Учебные примеры хороши, но быстро надоедают. Гораздо интереснее посмотреть на задачу, которая реально встречается в бэкенде. Сейчас мы с вами сделаем кое-что поинтересней.

Для запуска этого примера используйте следующую команду

docker compose exec app php app/semantic-search.php
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 9 [12]

Сценарий

Есть события с коротким текстовым описанием. Пользователь ищет, например:

  • “санкции против IT-компаний”

  • “космическая гонка среди стран региона”

При этом, как вы понимаете, в самих данных таких формулировок может не быть вообще. Классический поиск по словам здесь начинает усложняться: синонимы, морфология, разные языки, костыли поверх костылей и прочее.

Небольшое мысленное упражнение

Допустим, у нас есть лента событий или материалов, где каждое событие описано парой предложений. Что-то вроде:

  • введены новые ограничения в отношении технологических корпораций

  • страны региона наращивают инвестиции в спутниковые программы

  • обострение конфликта [15] на политической почве в нескольких провинциях

Теперь пользователь вводит запрос: “космическая гонка среди стран региона”.

Ни одно из этих слов буквально не обязано встречаться в описании событий. Упс… И это нормально – люди редко формулируют мысли так же, как их описывают системы.

Идея решения

Вместо того чтобы пытаться угадать слова, можно попробовать искать по смыслу. Не в философском смысле, а в инженерном:

  • описание события → вектор,

  • запрос пользователя → вектор,

  • дальше – обычный поиск ближайших значений.

То есть, нам нужно использовать эмбеддинги [16] как универсальный индекс смысла.

Таким образом мы:

  1. Берём массив событий {id, title, description}

  2. Считаем эмбеддинг только по description (заголовок часто слишком короткий, неинформативный и может добавлять шум)

  3. Эмбеддим пользовательский запрос

  4. Ищем ближайшие векторы

  5. Сортируем и возвращаем результат

Без обучения моделей и без сложной инфраструктуры. Эмбеддинги для событий считаются один раз и могут храниться где угодно. Запрос пользователя обрабатывается в момент поиска. Дальше – сортировка и вывод результатов.

И… вуаля!
Никакого обучения моделей.
Никакой магии.
Просто другой способ представить текст.

Логика работы

Поместим логику работы в отдельный класс SemanticEventSearch. Этот класс не претендует на звание лучшего кода на планете, и написан только в в демонстрационных целях – поэтому опустим замечания по его качеству.

Класс SemanticEventSearch
final class SemanticEventSearch
{
    private string $model = 'Xenova/paraphrase-multilingual-MiniLM-L12-v2';
    private string $cachePath;
    private string $defaultQuery = 'санкции против IT-компаний';
    private int $topN;

    private ?string $query = null;

    /** @var list<array{id:int,title:string,description:string}> */
    private array $events;

    /** @var array<int, list<float|int>> */
    private array $eventEmbeddingsById = [];

    private $embedder;

    /**
     * Create a new semantic search instance.
     *
     * @param int $topN Number of results to return.
     */
    public function __construct(int $topN = 3)
    {
        $this->cachePath = __DIR__ . '/../embeddings.events.json';
        $this->events = [];
        $this->embedder = null;
        $this->topN = $topN;
    }

    /**
     * Inject events that will be indexed/searched.
     *
     * @param list<array{id:int,title:string,description:string}> $events
     * @return $this
     */
    public function setEvents(array $events): self
    {
        $this->events = $events;
        $this->eventEmbeddingsById = [];
        return $this;
    }

    /**
     * Set the embeddings model identifier.
     *
     * Switching model invalidates in-memory embeddings.
     *
     * @param string $model
     * @return $this
     */
    public function setModel(string $model): self
    {
        $this->model = $model;
        $this->embedder = null;
        $this->eventEmbeddingsById = [];
        return $this;
    }

    /**
     * Set the query to be searched.
     *
     * @param string $query
     * @return $this
     */
    public function setQuery(string $query): self
    {
        $q = trim($query);
        $this->query = $q === '' ? null : $q;
        return $this;
    }

    /**
     * Run the end-to-end semantic search pipeline (cache -> embed query -> score -> top-N).
     *
     * @return array{query:string,results:list<array{score:float,event:array{id:int,title:string,description:string}}>}
     * @throws RuntimeException If events are not set or embeddings output is unexpected.
     */
    public function run(): array
    {
        if (count($this->events) === 0) {
            throw new RuntimeException('Events list is empty. Call setEvents() before run().');
        }

        if ($this->embedder === null) {
            $this->embedder = pipeline('embeddings', $this->model);
        }

        $this->loadEmbeddingsFromCacheIfCompatible();
        $this->ensureAllEventEmbeddings();

        $query = $this->query ?? $this->defaultQuery;
        $queryVec = $this->embedText($query);

        $results = $this->search($queryVec);
        return [
            'query' => $query,
            'results' => $results,
        ];
    }

    /**
     * Compute an embedding vector for a single text.
     *
     * @param string $text
     * @return list<float|int>
     * @throws RuntimeException
     */
    private function embedText(string $text): array
    {
        $emb = ($this->embedder)($text, normalize: true, pooling: 'mean');
        if (!is_array($emb) || !isset($emb[0]) || !is_array($emb[0])) {
            throw new RuntimeException('Unexpected embeddings output format');
        }

        return $emb[0];
    }

    /**
     * Cosine similarity between two vectors.
     *
     * @param list<float|int> $a
     * @param list<float|int> $b
     * @return float
     */
    private function cosineSimilarity(array $a, array $b): float
    {
        $n = min(count($a), count($b));

        $dot = 0.0;
        $normA = 0.0;
        $normB = 0.0;

        for ($i = 0; $i < $n; $i++) {
            $x = (float) $a[$i];
            $y = (float) $b[$i];

            $dot += $x * $y;
            $normA += $x * $x;
            $normB += $y * $y;
        }

        if ($normA <= 0.0 || $normB <= 0.0) {
            return 0.0;
        }

        return $dot / (sqrt($normA) * sqrt($normB));
    }

    /**
     * Load a JSON file and decode to array.
     *
     * @param string $path
     * @return array|null
     */
    private function loadJsonFile(string $path): ?array
    {
        if (!is_file($path)) {
            return null;
        }

        $raw = file_get_contents($path);
        if ($raw === false) {
            return null;
        }

        $data = json_decode($raw, true);
        return is_array($data) ? $data : null;
    }

    /**
     * Encode and save data to JSON file.
     *
     * @param string $path
     * @param array $data
     * @throws RuntimeException
     */
    private function saveJsonFile(string $path, array $data): void
    {
        $json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
        if ($json === false) {
            throw new RuntimeException('Failed to encode JSON');
        }

        $ok = file_put_contents($path, $json);
        if ($ok === false) {
            throw new RuntimeException('Failed to write cache file: ' . $path);
        }
    }

    /**
     * Load cached event embeddings only if they were produced by the current model.
     *
     * @return void
     */
    private function loadEmbeddingsFromCacheIfCompatible(): void
    {
        $cached = $this->loadJsonFile($this->cachePath);

        if (!is_array($cached) || !isset($cached['model'], $cached['events']) || !is_array($cached['events'])) {
            return;
        }

        if ($cached['model'] !== $this->model) {
            return;
        }

        foreach ($cached['events'] as $row) {
            if (isset($row['id'], $row['embedding']) && is_array($row['embedding'])) {
                $this->eventEmbeddingsById[(int) $row['id']] = $row['embedding'];
            }
        }
    }

    /**
     * Ensure embeddings exist for all events and persist them to cache.
     *
     * @return void
     * @throws RuntimeException
     */
    private function ensureAllEventEmbeddings(): void
    {
        $missing = [];
        foreach ($this->events as $event) {
            $id = (int) $event['id'];
            if (!isset($this->eventEmbeddingsById[$id])) {
                $missing[] = $event;
            }
        }

        if (count($missing) === 0) {
            return;
        }

        foreach ($missing as $event) {
            $id = (int) $event['id'];
            $text = (string) $event['description'];

            $this->eventEmbeddingsById[$id] = $this->embedText($text);
        }

        $toCache = [
            'model' => $this->model,
            'events' => array_values(array_map(
                fn(array $event): array => [
                    'id' => (int) $event['id'],
                    'embedding' => $this->eventEmbeddingsById[(int) $event['id']],
                ],
                $this->events
            )),
        ];

        $this->saveJsonFile($this->cachePath, $toCache);
    }

    /**
     * Score all events against the query embedding and return the top-N results.
     *
     * @param list<float|int> $queryVec
     * @return list<array{score:float,event:array{id:int,title:string,description:string}}>
     */
    private function search(array $queryVec): array
    {
        $scored = [];

        foreach ($this->events as $event) {
            $id = (int) $event['id'];
            $score = $this->cosineSimilarity($queryVec, $this->eventEmbeddingsById[$id]);

            $scored[] = [
                'score' => $score,
                'event' => $event,
            ];
        }

        usort($scored, static fn(array $a, array $b): int => $b['score'] <=> $a['score']);

        return array_slice($scored, 0, $this->topN);
    }

    /**
     * Render results as plain text.
     *
     * @param string $query
     * @param list<array{score:float,event:array{id:int,title:string,description:string}}> $results
     * @return void
     */
    public function render(string $query, array $results): void
    {
        echo "Query: {$query}nn";
        foreach ($results as $row) {
            $event = $row['event'];
            $score = (float) $row['score'];

            echo "[" . number_format($score, 4) . "] #{$event['id']} {$event['title']}n";
            echo "  {$event['description']}nn";
        }
    }
}
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 10 [12]

Подготовим данные

Предположим, что это наши данные, собранные из разных источников. Для простоты поместим их в массив.

Массив $events
$events = [
    [
        'id' => 1,
        'title' => 'Ограничения против технологических корпораций',
        'description' => 'Введены новые экономические меры в отношении крупных технологических компаний.',
    ],
    [
        'id' => 2,
        'title' => 'Развитие космических программ',
        'description' => 'Несколько стран региона увеличили финансирование национальных спутниковых проектов.',
    ],
    [
        'id' => 3,
        'title' => 'Эскалация политического конфликта',
        'description' => 'Обострение конфликта на политической почве в нескольких провинциях.',
    ],
    [
        'id' => 4,
        'title' => 'Ограничения против ИТ-сектора',
        'description' => 'Правительство объявило о новых ограничениях для компаний, работающих в сфере информационных технологий.',
    ],
    [
        'id' => 5,
        'title' => 'Рост инфляции и пересмотр ключевой ставки',
        'description' => 'Центральный банк повысил ключевую ставку на фоне ускорения инфляции и роста цен на импортные товары.',
    ],
    [
        'id' => 6,
        'title' => 'Запуск программы поддержки малого бизнеса',
        'description' => 'Власти объявили о льготных кредитах и налоговых послаблениях для малого и среднего бизнеса в регионах.',
    ],
    [
        'id' => 8,
        'title' => 'Утечка данных в сфере онлайн-ритейла',
        'description' => 'Интернет-магазин расследует утечку персональных данных клиентов после компрометации учётных записей сотрудников.',
    ],
    [
        'id' => 9,
        'title' => 'Прорыв в медицине: новый метод диагностики',
        'description' => 'Исследователи представили метод ранней диагностики заболеваний по биомаркерам, сокращающий время анализа.',
    ],
    [
        'id' => 10,
        'title' => 'Сезонный рост заболеваемости',
        'description' => 'В нескольких городах отмечен рост заболеваемости респираторными инфекциями, клиники усилили приём пациентов.',
    ],
    [
        'id' => 12,
        'title' => 'Засуха и риски для сельского хозяйства',
        'description' => 'Из-за продолжительной засухи фермеры прогнозируют снижение урожайности, обсуждаются меры поддержки аграриев.',
    ],
    [
        'id' => 13,
        'title' => 'Финал крупного спортивного турнира',
        'description' => 'В решающем матче сезона команда одержала победу в дополнительное время, установив новый рекорд по посещаемости.',
    ],
    [
        'id' => 14,
        'title' => 'Трансфер игрока и усиление состава',
        'description' => 'Клуб подписал контракт с новым нападающим, рассчитывая усилить атакующую линию перед серией дерби.',
    ],
    [
        'id' => 15,
        'title' => 'Новые правила для маркетплейсов',
        'description' => 'Регулятор предложил требования к маркировке товаров и прозрачности комиссий на торговых онлайн-платформах.',
    ],
    [
        'id' => 17,
        'title' => 'Сбои в поставках полупроводников',
        'description' => 'Производители электроники предупредили о задержках поставок чипов из-за ограничений экспорта и перегрузки заводов.',
    ],
    [
        'id' => 18,
        'title' => 'Открытие фестиваля современного искусства',
        'description' => 'В столице стартовал фестиваль современного искусства с выставками, перформансами и лекциями художников.',
    ],
    [
        'id' => 19,
        'title' => 'Крупная сделка на рынке недвижимости',
        'description' => 'Инвестфонд приобрёл портфель коммерческой недвижимости, планируя реконструкцию и повышение энергоэффективности.',
    ],
    [
        'id' => 20,
        'title' => 'Исследование океана и новые данные',
        'description' => 'Научная экспедиция собрала данные о течениях и температуре воды, уточнив прогнозы по изменению климата.',
    ],
];
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 11 [12]

Использование примера

Здесь всё просто – запускаем наш код и ждём результата.

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

use function CodewithkyrianTransformersPipelinespipeline;

final class SemanticEventSearch {...}

$events = [...];

$query = 'санкции против IT-компаний';

$search = new SemanticEventSearch(topN: 3);
$search->setModel('Xenova/paraphrase-multilingual-MiniLM-L12-v2');
$search->setEvents($events);
$search->setQuery($query);
$out = $search->run();
$search->render($out['query'], $out['results']);
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 12 [12]

Логика работы (по шагам)

Если вам не хочется разбираться в деталях реализации SemanticEventSearch, ниже – упрощённое пошаговое описание. Более подробный разбор кода выходит за рамки этой статьи.

Логика работы кода
  • Снаружи задаём:

    • список событий (setEvents)

    • модель (setModel)

    • запрос (setQuery)

    • topN через конструктор

  • Дальше — обычная логика в run():

    • Поднимаем embedder (pipeline(’embeddings’, model)) если ещё не поднят.

    • .transformers-cache:

      • при первом использовании модель и файлы токенизации/весов скачиваются и кладутся в .transformers-cache

      • дальше они берутся оттуда, чтобы не качать заново и работать быстрее

    • embeddings.events.json:

      • это наш локальный кэш эмбеддингов событий

      • пытаемся его прочитать и использовать только если model в кэше совпадает с текущей моделью

    • Если для каких-то событий эмбеддингов нет:

      • считаем эмбеддинги для description

      • сохраняем обратно в embeddings.events.json

    • Эмбеддим запрос (эмбеддинги нормализуются, чтобы косинусная близость была стабильной и сравнимой)

    • Считаем близость запроса к каждому событию (cosine similarity)

    • Сортируем, берём top‑N, возвращаем результаты

    • Рендер (вывод) делается снаружи через render(query, results)

Результат

На выходе мы получаем не совпадение слов, а совпадение по смыслу. Первый результат – наиболее подходящий по схожести с нашим запросом про “санкции против IT-компаний”. И всё это – без сложной математики [17] и без танцев с бубнами.

Query: санкции против IT-компаний

[0.4288] #4 Ограничения против ИТ-сектора
  Правительство объявило о новых ограничениях для компаний, работающих в сфере информационных технологий.

[0.3356] #15 Новые правила для маркетплейсов
  Регулятор предложил требования к маркировке товаров и прозрачности комиссий на торговых онлайн-платформах.

[0.2598] #8 Утечка данных в сфере онлайн-ритейла
  Интернет-магазин расследует утечку персональных данных клиентов после компрометации учётных записей сотрудников.
AI для PHP-разработчиков. Часть 2: практическое использование TransformersPHP - 13 [12]

Где это можно применять в проде

В целом это уже похоже на продуктовый подход, а не на эксперимент. К тому же вы можете легко заметить, что такой подход:

  • слабо привязан к конкретной формулировке запроса

  • хорошо работает на коротких описаниях

  • легко комбинируется с обычными фильтрами (дата, регион, тип события)

То есть это не “AI ради AI”, а вполне конкретная прикладная логика. Та самая, которую можно объяснить, отладить и поддерживать.

Ограничения и подводные камни

Важно не обманываться: это не серебряная пуля.

Модели весят немало. Производительность нужно учитывать. Некоторые задачи проще и надёжнее решаются без AI обычным SQL.

Но это уже нормальный инженерный разговор – про trade-off’ы, а не про магию или чёрный ящик.

Куда двигаться дальш��

Как для меня, то TransformersPHP – это хороший пример того, что AI можно использовать напрямую в PHP-проектах без смены стека и без Python.

В своей книге “AI для PHP-разработчиков” (открытой и бесплатной) я как раз и разбираю подобные кейсы: где это имеет смысл, как выбирать модели и как не превратить проект в набор экспериментальных фич.

Ссылка на книгу “AI для PHP-разработчиков” [18].

Кстати, все примеры можно скачать и запустить через готовую среду Docker [19].

Или же вы также можете запускать все примеры из книги напрямую [20].

Если тема откликается – буду рад обсуждению и фидбэку.
Особенно интересно, какие задачи вы уже решаете или хотели бы решать с помощью AI в PHP-проектах.

Автор: samako

Источник [21]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/25350

URLs in this post:

[1] Практика без Python и data science: https://habr.com/ru/articles/984042/

[2] мышления: http://www.braintools.ru/thinking

[3] обучение: http://www.braintools.ru/article/5125

[4] логику: http://www.braintools.ru/article/7640

[5] TransformersPHP: https://transformers.codewithkyrian.com/

[6] inference: https://bigdataschool.ru/wiki/model-inference/

[7] Kyrian Obikwelu: https://github.com/CodeWithKyrian

[8] Hugging Face Transformers: https://huggingface.co/docs/transformers/en/index

[9] ONNX Runtime: https://github.com/microsoft/onnxruntime

[10] сайте документации: https://transformers.codewithkyrian.com/getting-started#installation

[11] памяти: http://www.braintools.ru/article/4140

[12] Image: https://sourcecraft.dev/

[13] внимание: http://www.braintools.ru/article/7595

[14] онлайн: https://aiwithphp.org/books/ai-for-php-developers/examples/ml-ecosystem-in-php

[15] конфликта: http://www.braintools.ru/article/7708

[16] эмбеддинги: https://ru.wikipedia.org/wiki/%D0%92%D0%B5%D0%BA%D1%82%D0%BE%D1%80%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%B5%D0%B4%D1%81%D1%82%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5_%D1%81%D0%BB%D0%BE%D0%B2

[17] математики: http://www.braintools.ru/article/7620

[18] “AI для PHP-разработчиков”: https://apphp.gitbook.io/ai-for-php-developers/

[19] примеры можно скачать и запустить через готовую среду Docker: https://github.com/apphp/ai-for-php-developers-examples

[20] запускать все примеры из книги напрямую: https://aiwithphp.org/books/ai-for-php-developers/examples/

[21] Источник: https://habr.com/ru/articles/993966/?utm_source=habrahabr&utm_medium=rss&utm_campaign=993966

www.BrainTools.ru

Rambler's Top100