Урок курса
Сохранение обученной модели через joblib
Машинное обучение с нуля на Python: первая модельВ прошлом уроке вы обучили LogisticRegression на данных о задержках доставки и сравнили её метрики с DummyClassifier. Объект lr с подобранными весами существовал в памяти — но только до конца той сессии. В новом сеансе переменные не сохраняются, поэтому здесь мы сначала воспроизведём обучение, а затем зафиксируем результат на диске через joblib, чтобы не повторять этот шаг каждый раз заново.
joblib.dump: сериализация обученной модели в бинарный файл
joblib превращает любой Python-объект в последовательность байт и записывает её в файл. Для scikit-learn это означает, что все внутренние атрибуты модели — веса coef_, смещение intercept_, параметры вроде C и max_iter — упакованы в один бинарный файл. Файл можно открыть в следующей сессии или скопировать на другую машину, однако scikit-learn не поддерживает загрузку между разными версиями как официальный контракт: файл, сохранённый в одном окружении, может не загрузиться в другом. Даже при совпадающих версиях Python, scikit-learn и joblib корректный результат — это воспроизводимое окружение, а не безусловная гарантия.
Именно поэтому позже мы запишем метаданные с версией библиотеки — чтобы перед загрузкой сверить окружения и не получить молчаливо неверный результат.
Переменные не сохраняются между сессиями, поэтому начнём с полного воспроизводимого блока обучения — он запускается здесь целиком и ни на какие переменные из предыдущего урока не рассчитывает:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
import joblib
df = pd.read_csv('delivery_risk.csv')
features = ['distance_km', 'items_count', 'warehouse_load', 'forecast_rain_mm', 'is_weekend']
X = df[features]
y = df['delayed']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
lr = LogisticRegression(max_iter=1000, random_state=42)
lr.fit(X_train, y_train)
Теперь lr — обученный объект в памяти. Стоит перезапустить ядро, и все веса исчезнут.
Критический момент: порядок вызовов. joblib.dump не знает, обучена ли модель. Если вызвать его до lr.fit(X_train, y_train), файл всё равно создастся — без ошибки и без предупреждения. Внутри окажется объект с параметрами, заданными при создании (max_iter=1000, random_state=42), но без обученных коэффициентов — атрибут coef_ будет отсутствовать. При последующей попытке предсказания такой объект выбросит NotFittedError: scikit-learn проверяет состояние fit перед predict и не допускает работы необученной модели.
Чтобы зафиксировать результат на диске, сначала убеждаемся в fitted-состоянии, а затем делаем единственный вызов joblib.dump:
assert hasattr(lr, 'coef_'), 'модель не обучена — fit не вызван'
saved = joblib.dump(lr, 'model.joblib')
print(saved) # ['model.joblib']
coef_ появляется у LogisticRegression только после успешного fit, поэтому его наличие — надёжный индикатор fitted-состояния. Это особенно важно в длинных ноутбуках, где ячейки легко запустить не в том порядке. joblib.dump возвращает список путей записанных файлов — обычно из одного элемента. Сам файл бинарный: открывать его текстовым редактором бессмысленно.
После вызова joblib.dump файл model.joblib содержит полный снимок обученной модели.
Схема признаков и метаданные версии: сохранение сопутствующих объектов
Когда LogisticRegression обучается на DataFrame со строковыми именами столбцов, sklearn сохраняет их в атрибуте feature_names_in_. Начиная с sklearn 1.2 несовпадение имён при predict поднимает ValueError; в версиях 1.0–1.1 то же самое могло завершиться лишь FutureWarning без остановки. Встроенная защита есть, но её поведение зависит от версии окружения.
Поэтому надёжнее иметь собственный явный контракт — отдельный файл с зафиксированным списком признаков. Схема — обычный словарь:
feature_schema = {'features': list(X_train.columns)}
joblib.dump(feature_schema, 'feature_schema.joblib')
list(X_train.columns) фиксирует порядок именно той версии X_train, на которой вызван fit. Для нашего проекта это ['distance_km', 'items_count', 'warehouse_load', 'forecast_rain_mm', 'is_weekend'] — ровно пять признаков, с которыми обучалась модель.
В следующем уроке, при загрузке артефактов, схему признаков можно будет использовать для явной проверки входных данных перед вызовом predict:
# пример проверки перед predict (тема следующего урока)
# assert list(X_new.columns) == feature_schema['features'], 'несовпадение схемы признаков'
Важно понимать: feature_schema.joblib десериализуется тем же механизмом joblib.load, что и сама модель — а значит, несёт те же риски при загрузке подменённого файла. Ожидаемый тип dict и небольшой размер не защищают от исполнения произвольного кода при десериализации. Загружать feature_schema.joblib безопасно только в том случае, если файл был создан вами же и его происхождение не вызывает сомнений. Сравнение списков признаков проверяет входные данные, но не безопасность самого файла — это разные задачи.
Второй сопутствующий файл — метаданные версии. Спустя месяц может быть неочевидно, какой версией sklearn обучена модель: sklearn иногда меняет численные алгоритмы между минорными версиями, и coef_ может незначительно отличаться. Зафиксированная версия позволяет сравнить окружения:
import sklearn
metadata = {
'model_version': '1.0',
'trained_on': 'deliveries_v1',
'sklearn_version': sklearn.__version__
}
joblib.dump(metadata, 'metadata.joblib')
sklearn.__version__ — строка вида '1.4.2', она читается прямо в рантайме и гарантированно соответствует той версии, которая только что выполнила fit. Поле trained_on — произвольная метка датасета, чтобы было ясно, на каких данных обучалась модель.
Каждый из трёх объектов сохраняется отдельным вызовом joblib.dump в отдельный файл. На диске появляются model.joblib, feature_schema.joblib и metadata.joblib — три независимых артефакта, которые вместе описывают один обученный результат.
Проверка существования файлов через os.path.isfile без загрузки содержимого
После трёх вызовов joblib.dump из предыдущих секций убедимся, что файлы действительно появились на диске. os.path.isfile возвращает True, если по указанному пути лежит обычный файл (не директория, не симлинк на несуществующий путь), — и делает это без открытия содержимого:
import os
assert os.path.isfile('model.joblib'), 'model.joblib не найден'
assert os.path.isfile('feature_schema.joblib'), 'feature_schema.joblib не найден'
assert os.path.isfile('metadata.joblib'), 'metadata.joblib не найден'
Если все три assert прошли без исключения — файлы на месте. Это лёгкая санитарная проверка — «файл существует по этому пути», а не «файл корректен». os.path.isfile не открывает файл, не десериализует его и ничего не говорит о целостности содержимого.
Что проверка не обнаруживает. Если при dump допустить опечатку 'modell.joblib' и ту же опечатку повторить в isfile, оба вызова пройдут без ошибки, а ожидаемый model.joblib так и не появится на диске. Проверка существования обнаруживает только отсутствие файла по проверяемому пути — желаемое имя нужно сверять отдельно.
Что касается ошибок записи — нехватка прав или места на диске — joblib.dump поднимает OSError или PermissionError и прерывает выполнение ещё до вызова isfile. Поэтому assert os.path.isfile(...) не дублирует обработку ошибок записи; он отвечает на другой вопрос: «файл с таким именем существует по этому пути».
Что внутри файлов корректно и пригодно к использованию, покажет следующий урок, где речь пойдёт о загрузке артефактов через joblib.load.
Попробуйте решить
Сохранить обученную модель с описанием признаков
Команда передаёт модель риска задержки доставки следующему разработчику. Получателю нужны сама обученная модель, точный порядок её входных признаков и сведения о версии. В этом задании вы создаёте собственные локальные файлы; чужие файлы загружать не нужно.
В df уже лежат пять числовых признаков и цель delayed. Это отдельный запуск песочницы: переменные и файлы прошлых задач сюда не переходят. Для воспроизводимости используйте LogisticRegression(max_iter=1000) и стратифицированное разделение 80/20 с random_state=42; обучайте только на train.
Создайте в текущем каталоге три файла:
model.joblib— обученная на этих данных модель;feature_schema.joblib— словарь{'features': list(X_train.columns)}с порядком признаков, увиденных моделью;metadata.joblib— словарь{'model_version': '1.0', 'trained_on': 'deliveries_v1', 'sklearn_version': sklearn.__version__}.
Перед завершением убедитесь, что модель действительно обучена и все три файла созданы. stdin и вывод в stdout здесь не нужны. Проверка откроет только созданные вами в этой песочнице файлы и сверит их содержимое с результатом обучения.
Продолжить с проверкой и прогрессом
Откройте интерактивный раннер с заданиями урока.
