Подключаем ONNX-модель скоринга email-адресов к HTTP API на TypeScript

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Часть 1. Учимся отличать реальные email-адреса от сгенерированных с CatBoost

В прошлой статье я обучил CatBoost отличать локальные части email-адресов, придуманные человеком, от сгенерированных. На ivan.ivanov@gmail.com модель выдаёт около 0.99, на xnjjeka.2.e.2.2@gmail.com - ноль. Теперь её надо встроить в CleanContact. Загвоздка в том, что обучение живёт в Python, а бэкенд у меня на Node.js.

Модель я выгрузил в ONNX и подключил через onnxruntime-node. Весит она пару мегабайт, так что файл просто лежит в репозитории. Деревья CatBoost внутри графа - это оператор TreeEnsembleClassifier.

Самая основная сложность в том, что модель ждёт не строку с адресом, а набор векторов с признаками. Все признаки пришлось переписать на TypeScript, и это оказалось основной работой. Причем, ошибка в признаке ничего не ломает - инференс запускается, но скоринг получается уже другой. HTTP-часть по сравнению с этим напротив, заняла совсем немного кода. Тут ничего удивительного, поскольку любой современный HTTP фреймворк берет на себя всю рутину.

TypeScript без сборки

Стек: Node.js 24, Hono с @hono/node-server и onnxruntime-node. Тесты на встроенных node:test и node:assert. TypeScript нужен только для проверки типов через tsc --noEmit, ничего не компилируется.

Node.js умеет запускать напрямую .ts сам без компиляции - он просто вырезает аннотации типов. Но только их. enum, namespace с исполняемым кодом и parameter properties так не работают, их надо транслировать в JS, поэтому в проекте их нет.

{
  "compilerOptions": {
    "target": "es2024",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "noEmit": true,
    "allowImportingTsExtensions": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true
  }
}

erasableSyntaxOnly ругается, если в коде появится что-то из списка выше. verbatimModuleSyntax заставляет писать import type для типов, чтобы они не утекали в рантайм.

{
  "type": "module",
  "scripts": {
    "start": "node src/server.ts",
    "dev": "node --watch src/server.ts",
    "test": "node --test 'test/**/*.test.ts'",
    "typecheck": "tsc --noEmit"
  }
}

Что переносим из Python-проекта

Кроме ONNX-файла нужны метаданные, частоты n-грамм и словари имён. Экстрактор признаков я разбил на несколько модулей:

email-human-ml-ts/
- models/
-- email-pattern-v1.onnx
-- email-pattern-v1.meta.json
-- email-pattern-v1.ngrams.json
-- first_names.txt
-- last_names.txt
- scripts/
-- sync-model.sh
- src/
-- normalize.ts
-- features/
-- model.ts
-- app.ts
-- server.ts
- test/
-- fixtures/parity.json
-- features.test.ts
-- model.test.ts
-- app.test.ts

Чтобы вручную не носить файлы, артефакты копирует scripts/sync-model.sh. Порядок признаков, а заодно имена входа и выхода модели лежат в meta.json (список признаков тут обрезан).

SRC="${ML_REPO:-../email-validator-ml}"
VERSION="${MODEL_VERSION:-email-pattern-v1}"

cp "$SRC/models/$VERSION.onnx" "$SRC/models/$VERSION.meta.json" "$SRC/models/$VERSION.ngrams.json" models/
cp "$SRC/data/dictionaries/first_names.txt" "$SRC/data/dictionaries/last_names.txt" models/
cp "$SRC/tests/fixtures/parity.json" test/fixtures/parity.json
{
  "version": "email-pattern-v1",
  "feature_order": ["length", "letter_count", "digit_count", "...", "longest_keyboard_run"],
  "input": { "name": "features", "dtype": "float32", "shape": ["N", 38] },
  "output": { "name": "probabilities", "shape": ["N", 2], "score_column": 1 },
  "files": { "ngrams": "email-pattern-v1.ngrams.json" }
}

TypeScript берёт feature_order из этого файла. Если перепутать два столбца, модель ничего не заметит и молча начнёт выдавать ерунду. В parity.json Python-проект кладёт пробные и синтетические адреса, их признаки и скоры модели. По этому набору я и сверял TypeScript-версию.

Где разошлись Python и JavaScript

Сами формулы простые: длина, число букв и цифр, энтропия, частоты n-грамм, поиск имён по словарям. Проблемы начались в мелочах, где языки ведут себя по-разному.

Длина строки

В Python len() считает кодовые точки Unicode, а .length в JavaScript - единицы UTF-16. Для "

Источник: https://habr.com/ru/articles/1098050/


Интересные статьи

Интересные статьи

У Битрикса есть официальная обёртка для REST API — класс CRest, который лежит в каждом примере приложения и который все копируют к себе в проект. Он умещается в один файл и работает — ровно до того мо...
Дисклеймер: статья выражает личное мнение автора, не является научным исследованием. Подсвечивает современный тренд почтовых рассылок и рабочих коммуникаций и призывает поделиться читателя своим мнени...
Привет! Меня зовут Анастасия Соколенко, я отвечаю за безопасную разработку в Битрикс24. Вместе с коллегами мы не только проверяем код, который пишут наши разработчики, но и учим их делать его максимал...
Система отслеживания звонков, она же кол-трекинг – верный друг маркетологов и предпринимателей. Это она помогает понять, какие каналы работают лучше всего, как оптимизировать рекламные бюджеты, какие ...
В данной статье я расскажу о моём аддоне к блендеру, о причинах, побудивших меня к его созданию, процессе разработки и об «успехе» на YouTube.