项目文件夹

文件
2026-07-13 13:22:34 +08:00

223 行
7.1 KiB
Python

import json
import math
import keras
import numpy as np
import pytest
import mlflow
from mlflow.models import Model
from mlflow.tracking.fluent import flush_async_logging
from mlflow.types import Schema, TensorSpec
from mlflow.utils.autologging_utils import AUTOLOGGING_INTEGRATIONS
@pytest.fixture(autouse=True)
def clear_autologging_config():
yield
AUTOLOGGING_INTEGRATIONS.pop("keras", None)
def _create_keras_model():
model = keras.Sequential([
keras.Input([28, 28, 3]),
keras.layers.Flatten(),
keras.layers.Dense(2),
])
model.compile(
loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
optimizer=keras.optimizers.Adam(0.001),
metrics=[keras.metrics.SparseCategoricalAccuracy()],
)
return model
def test_default_autolog_behavior():
mlflow.keras.autolog()
# Prepare data for a 2-class classification.
data = np.random.uniform(size=(20, 28, 28, 3))
label = np.random.randint(2, size=20)
model = _create_keras_model()
num_epochs = 2
batch_size = 4
with mlflow.start_run() as run:
model.fit(
data,
label,
validation_data=(data, label),
batch_size=batch_size,
epochs=num_epochs,
)
flush_async_logging()
client = mlflow.MlflowClient()
mlflow_run = client.get_run(run.info.run_id)
run_metrics = mlflow_run.data.metrics
model_info = mlflow_run.data.params
# Assert training configs are logged correctly.
assert int(model_info["batch_size"]) == batch_size
assert model_info["optimizer_name"] == "adam"
assert math.isclose(float(model_info["optimizer_learning_rate"]), 0.001, rel_tol=1e-6)
assert "loss" in run_metrics
assert "sparse_categorical_accuracy" in run_metrics
assert "validation_loss" in run_metrics
# Assert metrics are logged in the correct number of times.
loss_history = client.get_metric_history(run_id=run.info.run_id, key="loss")
assert len(loss_history) == num_epochs
validation_loss_history = client.get_metric_history(
run_id=run.info.run_id,
key="validation_loss",
)
assert len(validation_loss_history) == num_epochs
# Test the loaded pyfunc model produces the same output for the same input as the model.
test_input = np.random.uniform(size=[2, 28, 28, 3]).astype(np.float32)
model_uri = f"runs:/{run.info.run_id}/model"
loaded_pyfunc_model = mlflow.pyfunc.load_model(model_uri)
np.testing.assert_allclose(
keras.ops.convert_to_numpy(model(test_input)),
loaded_pyfunc_model.predict(test_input),
)
# Test the signature is logged.
input_schema = Schema([TensorSpec(np.dtype(np.float32), (-1, 28, 28, 3))])
output_schema = Schema([TensorSpec(np.dtype(np.float32), (-1, 2))])
mlflow_model = Model.load(model_uri)
assert mlflow_model.signature.inputs == input_schema
assert mlflow_model.signature.outputs == output_schema
@pytest.mark.parametrize(
(
"log_every_epoch",
"log_every_n_steps",
"log_models",
"log_model_signatures",
"save_exported_model",
),
[
(False, 1, False, False, False),
(False, 2, True, True, True),
(True, None, False, False, False),
],
)
def test_custom_autolog_behavior(
log_every_epoch,
log_every_n_steps,
log_models,
log_model_signatures,
save_exported_model,
):
if keras.backend.backend() != "tensorflow" and save_exported_model:
pytest.skip("Only TensorFlow backend supports saving exported models.")
mlflow.keras.autolog(
log_every_epoch=log_every_epoch,
log_every_n_steps=log_every_n_steps,
log_models=log_models,
log_model_signatures=log_model_signatures,
save_exported_model=save_exported_model,
)
# Prepare data for a 2-class classification.
data = np.random.uniform(size=(20, 28, 28, 3))
label = np.random.randint(2, size=20)
model = _create_keras_model()
num_epochs = 1
batch_size = 4
with mlflow.start_run() as run:
model.fit(
data,
label,
validation_data=(data, label),
batch_size=batch_size,
epochs=num_epochs,
)
flush_async_logging()
client = mlflow.MlflowClient()
mlflow_run = client.get_run(run.info.run_id)
run_metrics = mlflow_run.data.metrics
model_info = mlflow_run.data.params
# Assert training configs are logged correctly.
assert int(model_info["batch_size"]) == batch_size
assert model_info["optimizer_name"] == "adam"
assert math.isclose(float(model_info["optimizer_learning_rate"]), 0.001, rel_tol=1e-6)
assert "loss" in run_metrics
assert "sparse_categorical_accuracy" in run_metrics
assert "validation_loss" in run_metrics
# Assert metrics are logged in the correct number of times.
loss_history = client.get_metric_history(run_id=run.info.run_id, key="loss")
if log_every_n_steps:
metric_length = model.optimizer.iterations.numpy() // log_every_n_steps
else:
metric_length = num_epochs
assert len(loss_history) == metric_length
validation_loss_history = client.get_metric_history(
run_id=run.info.run_id,
key="validation_loss",
)
assert len(validation_loss_history) == num_epochs
logged_model = mlflow.last_logged_model()
if log_models:
assert logged_model is not None
assert run_metrics.items() <= {m.key: m.value for m in logged_model.metrics}.items()
else:
assert logged_model is None
assert "mlflow.log-model.history" not in mlflow_run.data.tags
@pytest.mark.parametrize("log_models", [True, False])
@pytest.mark.parametrize("log_datasets", [True, False])
def test_keras_autolog_log_datasets(log_datasets, log_models):
mlflow.keras.autolog(log_datasets=log_datasets, log_models=log_models)
# Prepare data for a 2-class classification.
data = np.random.uniform(size=(20, 28, 28, 3)).astype(np.float32)
label = np.random.randint(2, size=20)
model = _create_keras_model()
model.fit(data, label, epochs=2)
flush_async_logging()
client = mlflow.MlflowClient()
run_inputs = client.get_run(mlflow.last_active_run().info.run_id).inputs
dataset_inputs = run_inputs.dataset_inputs
if log_datasets:
assert len(dataset_inputs) == 1
feature_schema = Schema([
TensorSpec(np.dtype(np.float32), (-1, 28, 28, 3)),
])
target_schema = Schema([
TensorSpec(np.dtype(np.int64), (-1,)),
])
expected = json.dumps({
"mlflow_tensorspec": {
"features": feature_schema.to_json(),
"targets": target_schema.to_json(),
}
})
assert dataset_inputs[0].dataset.schema == expected
else:
assert len(dataset_inputs) == 0
if log_models:
if log_datasets:
assert len(run_inputs.model_inputs) == 1
assert run_inputs.model_inputs[0].model_id == mlflow.last_logged_model().model_id
else:
assert mlflow.last_logged_model() is not None
else:
assert len(run_inputs.model_inputs) == 0