Концепции¶
Фреймворк, а не библиотека¶
Библиотеку вы вызываете. Фреймворк вызывает вас. Эта инверсия — вся суть mlango, и именно она покупает все удобства из этой документации.
Когда вы запускаете:
фреймворк находит ваш класс в реестре, открывает ран, ставит сид всем
генераторам, режет данные, вызывает ваш build(), ведёт цикл, пишет метрики,
фиксирует git-коммит, сохраняет артефакт и регистрирует версию. Вы предоставили
метод build() и несколько объявлений полей.
Будь mlango библиотекой, вы бы всё ещё писали цикл обучения, схему трекинга, админку и CLI. Именно потому, что это фреймворк, их писать не нужно.
Четыре декларации¶
Всё, что вы пишете, относится к одному из четырёх семейств:
| Семейство | Объявляет | Живёт в |
|---|---|---|
Dataset |
Как выглядит запись и откуда записи берутся | <app>/datasets.py |
Model |
Гиперпараметры и как собрать оценщик | <app>/models.py |
Agent |
Модель, системный промпт, инструменты, память | <app>/agents.py |
Eval |
Что значит «хорошо» и как это измерить | <app>/evals.py |
У них общая механика: метакласс собирает ваши поля, строит объект _meta и
регистрирует класс.
_meta — это контракт¶
Каждый объявленный класс получает _meta, описывающий его:
>>> Reviews._meta.label
'reviews.Reviews'
>>> Reviews._meta.field_names
['id', 'text', 'label']
>>> Reviews._meta.target_fields
[<LabelField: label>]
>>> Reviews._meta.fingerprint()
'11b053a7043a'
Это важнее, чем кажется. Всё универсальное во фреймворке — админка, миграции,
CLI, схемы API — написано против _meta, а не против Dataset или Model
конкретно. Именно этот приём позволяет одной админке отображать четыре
разных типа объектов, и поэтому добавление пятого семейства не означало бы
переписывание админки.
Поля делают четыре работы¶
Одна система полей обслуживает схему записи датасета, гиперпараметры модели, конфигурацию агента и контракт входа inference-эндпоинта:
text = fields.TextField(max_length=5000) # колонка датасета
learning_rate = fields.FloatField(default=1e-3) # гиперпараметр
tone = fields.ChoiceField(["formal", "friendly"]) # конфигурация агента
Поскольку это одни и те же объекты, гиперпараметр автоматически валидируется, получает значение по умолчанию, записывается в каждый ран, доступен для интроспекции в админке и участвует в свипах — без единой строки об этом с вашей стороны.
Приложения¶
Приложение — импортируемый пакет, группирующий декларации одной предметной
области: reviews, recsys, support. Приложения — единица переиспользования и
единица миграций.
При старте реестр импортирует у каждого приложения datasets.py, models.py,
agents.py, evals.py, admin.py и signals.py — поэтому объявить класс
достаточно, чтобы он существовал. Регистрировать вручную ничего не нужно.
from mlango.core import apps
apps.get_dataset("reviews.Reviews")
apps.get_registered("model")
apps.summary()
Добавьте apps.py, когда нужно человекочитаемое имя или код настройки при
старте:
from mlango.core import AppConfig
class ReviewsConfig(AppConfig):
name = "reviews"
verbose_name = "Отзывы покупателей"
def ready(self) -> None:
from mlango.core.signals import run_finished
run_finished.connect(notify_slack)
Наследование Meta¶
Поля наследуются от абстрактных базовых классов. Meta-опции — тоже, и это намеренное отличие от Django:
class TextClassifier(Model):
class Meta:
abstract = True
trainer = "transformers"
task = "classification"
class Sentiment(TextClassifier):
class Meta:
dataset = Reviews # trainer и task унаследованы
features = ["text"]
В Python тело class Meta дочернего класса не наследует родительский Meta, так
что без явного слияния переиспользуемый базовый класс написать невозможно —
именно на этом стоят пресеты. abstract намеренно исключён из
наследования, иначе ни один класс никогда не стал бы конкретным.
Настройки¶
Один модуль, разрешаемый из MLANGO_SETTINGS_MODULE. Все настройки и их
значения по умолчанию задокументированы в mlango.conf.global_settings;
переопределяйте только то, что отличается. Опечатка — ошибка, а не молчаливое
игнорирование.
Бэкенды меняются настройкой, а не переписыванием кода:
METASTORE = {"URL": "postgresql://user@host/mlango"}
STORAGE = {"BACKEND": "myproject.storage.S3Storage"}
TRAINERS = {"lightgbm": "myproject.trainers.LightGBMTrainer"}
PROVIDERS = {"vllm": "myproject.providers.VLLMProvider"}
См. Настройки.
Метастор¶
Одна база данных записывает всё, что произошло: раны, метрики, артефакты, версии датасетов, версии моделей, трейсы агентов, спаны и результаты оценок. По умолчанию SQLite, поэтому свежему проекту не нужна инфраструктура; та же схема работает на Postgres, когда проектом пользуется команда.
Именно поэтому на вопрос «что породило эту цифру?» всегда есть ответ. Строка рана несёт свои параметры, отпечаток данных, git-коммит, сид и устройство.
from mlango.training import recent_runs, get_run
run = get_run("7c8f1020")
run.params["_data_fingerprint"]
run.git_commit
Сигналы¶
Хуки для инструментирования, в форме Django — fn(sender, **kwargs):
from mlango.core.signals import epoch_finished, run_finished, tool_called
@receiver(run_finished)
def on_finish(sender, run, status, **kwargs):
...
Получатель, который бросил исключение, логируется и пропускается, а не роняет ран: инструментирование никогда не должно быть причиной падения шестичасового обучения.
Две идентичности данных¶
mlango различает две вещи, которые легко спутать:
- Отпечаток схемы — хеш объявленных полей. Меняется, когда меняется декларация.
- Хеш содержимого — хеш самих строк. Меняется, когда меняются данные.
Материализованная версия датасета записывает оба, и именно это позволяет через полгода отличить «та же схема, другие данные» от «те же данные, другая схема».
Где что лежит¶
myproject/
├── manage.py # командная строка этого проекта
├── myproject/
│ ├── settings.py # один модуль настроек
│ └── routes.py # маршруты inference API
└── reviews/ # приложение
├── apps.py # необязательная конфигурация приложения
├── datasets.py
├── models.py
├── agents.py
├── evals.py
├── admin.py
└── migrations/
└── 0001_initial.py