Files
mlflow--mlflow/tests/transformers/test_transformers_autolog.py
2026-07-13 13:22:34 +08:00

578 lines
18 KiB
Python

import random
import numpy as np
import optuna
import pytest
import sklearn.cluster
import sklearn.datasets
import torch
import transformers
from datasets import load_dataset
from packaging.version import Version
from transformers import (
DistilBertForSequenceClassification,
DistilBertTokenizerFast,
Trainer,
TrainingArguments,
pipeline,
)
import mlflow
@pytest.fixture
def iris_data():
iris = sklearn.datasets.load_iris()
return iris.data[:, :2], iris.target
@pytest.fixture
def setfit_trainer():
import setfit
from sentence_transformers.losses import CosineSimilarityLoss
from setfit import SetFitModel, sample_dataset
from setfit import Trainer as SetFitTrainer
from setfit import TrainingArguments as SetFitTrainingArguments
dataset = load_dataset("stanfordnlp/sst2")
train_dataset = sample_dataset(dataset["train"], label_column="label", num_samples=8)
eval_dataset = dataset["validation"]
model = SetFitModel.from_pretrained("sentence-transformers/all-MiniLM-L6-v2")
training_args = SetFitTrainingArguments(
loss=CosineSimilarityLoss,
batch_size=16,
num_iterations=5,
num_epochs=1,
report_to="none",
)
# TODO: Remove this once https://github.com/huggingface/setfit/issues/512
# is resolved. This is a workaround during the deprecation of the
# evaluation_strategy argument is being addressed in the SetFit library.
training_args.eval_strategy = training_args.evaluation_strategy
trainer = SetFitTrainer(
model=model,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
metric="accuracy",
column_mapping={"sentence": "text", "label": "label"},
args=training_args,
)
# setfit >= 1.1.0 defines an internal BCSentenceTransformersTrainer
# which directly uses transformers.Trainer, and the default callbacks
# include MLflowCallback, so it produces extra runs no matter autologging
# is on or off
# ref: https://github.com/huggingface/transformers/blob/11c27dd331151e7d2ac20016cce11d9d7c4b1756/src/transformers/integrations/integration_utils.py#L2138
if Version(setfit.__version__) >= Version("1.1.0"):
from transformers.integrations.integration_utils import MLflowCallback
trainer.remove_callback(MLflowCallback)
return trainer
@pytest.fixture
def transformers_trainer(tmp_path):
random.seed(8675309)
np.random.seed(8675309)
torch.manual_seed(8675309)
tokenizer = DistilBertTokenizerFast.from_pretrained("distilbert-base-uncased")
model = DistilBertForSequenceClassification.from_pretrained(
"distilbert-base-uncased", num_labels=2
)
train_texts = ["I love this product!", "This is terrible."]
train_labels = [1, 0]
train_encodings = tokenizer(train_texts, truncation=True, padding=True)
class CustomDataset(torch.utils.data.Dataset):
def __init__(self, encodings, labels):
self.encodings = encodings
self.labels = labels
def __getitem__(self, idx):
item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}
item["labels"] = torch.tensor(self.labels[idx])
return item
def __len__(self):
return len(self.labels)
train_dataset = CustomDataset(train_encodings, train_labels)
training_args = TrainingArguments(
output_dir=str(tmp_path.joinpath("results")),
num_train_epochs=1,
per_device_train_batch_size=4,
logging_dir=str(tmp_path.joinpath("logs")),
)
return Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
)
@pytest.fixture
def transformers_hyperparameter_trainer(tmp_path):
random.seed(555)
np.random.seed(555)
torch.manual_seed(555)
tokenizer = DistilBertTokenizerFast.from_pretrained("distilbert-base-uncased")
model = DistilBertForSequenceClassification.from_pretrained(
"distilbert-base-uncased", num_labels=2
)
train_texts = ["I love this product!", "This is terrible."]
train_labels = [1, 0]
train_encodings = tokenizer(train_texts, truncation=True, padding=True)
class CustomDataset(torch.utils.data.Dataset):
def __init__(self, encodings, labels):
self.encodings = encodings
self.labels = labels
def __getitem__(self, idx):
item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}
item["labels"] = torch.tensor(self.labels[idx])
return item
def __len__(self):
return len(self.labels)
train_dataset = CustomDataset(train_encodings, train_labels)
def model_init():
return DistilBertForSequenceClassification.from_pretrained(
"distilbert-base-uncased", num_labels=2
)
def objective(trial):
learning_rate = trial.suggest_float("learning_rate", 1e-7, 1e-1, log=True)
training_args = TrainingArguments(
output_dir=str(tmp_path.joinpath("results")),
num_train_epochs=1,
per_device_train_batch_size=4,
learning_rate=learning_rate,
logging_dir=str(tmp_path.joinpath("logs")),
report_to="none",
)
trainer = Trainer(
model_init=model_init,
args=training_args,
train_dataset=train_dataset,
)
train_result = trainer.train()
return train_result.training_loss
study = optuna.create_study(direction="minimize")
study.optimize(objective, n_trials=2)
best_params = study.best_params
best_training_args = TrainingArguments(
output_dir=str(tmp_path.joinpath("results")),
num_train_epochs=3,
per_device_train_batch_size=4,
learning_rate=best_params["learning_rate"],
logging_dir=str(tmp_path.joinpath("logs")),
)
return Trainer(
model=model,
args=best_training_args,
train_dataset=train_dataset,
)
@pytest.fixture
def transformers_hyperparameter_functional(tmp_path):
random.seed(555)
np.random.seed(555)
torch.manual_seed(555)
tokenizer = DistilBertTokenizerFast.from_pretrained("distilbert-base-uncased")
train_texts = [
"I simply adore artisinal baked goods!",
"I thoroughly dislike artisinal bathroom cleaning.",
]
train_labels = [1, 0]
eval_texts = [
"It was an excellent experience.",
"I'd rather pick my teeth with a rusty pitchfork.",
]
eval_labels = [1, 0]
train_encodings = tokenizer(train_texts, truncation=True, padding=True)
eval_encodings = tokenizer(eval_texts, truncation=True, padding=True)
class CustomDataset(torch.utils.data.Dataset):
def __init__(self, encodings, labels):
self.encodings = encodings
self.labels = labels
def __getitem__(self, idx):
item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}
item["labels"] = torch.tensor(self.labels[idx])
return item
def __len__(self):
return len(self.labels)
train_dataset = CustomDataset(train_encodings, train_labels)
eval_dataset = CustomDataset(eval_encodings, eval_labels)
training_args = TrainingArguments(
output_dir=str(tmp_path.joinpath("results")),
num_train_epochs=1,
per_device_train_batch_size=4,
logging_dir=str(tmp_path.joinpath("logs")),
report_to="none",
)
def model_init():
return DistilBertForSequenceClassification.from_pretrained(
"distilbert-base-uncased", num_labels=2
)
trainer = Trainer(
model_init=model_init,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
)
def my_hp_space_optuna(trial):
return {
"learning_rate": trial.suggest_float("learning_rate", 1e-5, 1e-1, log=True),
}
best_run = trainer.hyperparameter_search(
hp_space=my_hp_space_optuna,
backend="optuna",
n_trials=2,
direction="minimize",
)
best_training_args = TrainingArguments(
output_dir=str(tmp_path.joinpath("best_results")),
num_train_epochs=1,
per_device_train_batch_size=4,
learning_rate=best_run.hyperparameters["learning_rate"],
logging_dir=str(tmp_path.joinpath("best_logs")),
)
return Trainer(
model=model_init(),
args=best_training_args,
train_dataset=train_dataset,
)
skip_setfit = pytest.mark.skipif(
Version(transformers.__version__) >= Version("4.46.0"),
reason="fails due to issue: https://github.com/huggingface/setfit/issues/564",
)
@skip_setfit
def test_setfit_does_not_autolog(setfit_trainer):
mlflow.autolog()
setfit_trainer.train()
last_run = mlflow.last_active_run()
assert not last_run
preds = setfit_trainer.model([
"Always carry a towel!",
"The hobbits are going to Isengard",
"What's tatoes, precious?",
])
assert len(preds) == 3
@skip_setfit
def test_transformers_trainer_does_not_autolog_sklearn(transformers_trainer):
mlflow.sklearn.autolog()
exp = mlflow.set_experiment(experiment_name="trainer_autolog_test")
transformers_trainer.train()
last_run = mlflow.last_active_run()
assert last_run.data.metrics["epoch"] == 1.0
assert last_run.data.params["_name_or_path"] == "distilbert-base-uncased"
pipe = pipeline(
task="text-classification",
model=transformers_trainer.model,
tokenizer=DistilBertTokenizerFast.from_pretrained("distilbert-base-uncased"),
)
assert len(pipe("This is wonderful!")[0]["label"]) > 5 # Checking for 'LABEL_0' or 'LABEL_1'
client = mlflow.MlflowClient()
runs = client.search_runs([exp.experiment_id])
assert len(runs) == 1
@skip_setfit
def test_transformers_autolog_adheres_to_global_behavior_using_setfit(setfit_trainer):
mlflow.transformers.autolog(disable=False)
setfit_trainer.train()
assert len(mlflow.search_runs()) == 0
preds = setfit_trainer.model(["Jim, I'm a doctor, not an archaeologist!"])
assert len(preds) == 1
def test_transformers_autolog_adheres_to_global_behavior_using_trainer(transformers_trainer):
mlflow.transformers.autolog()
exp = mlflow.set_experiment(experiment_name="autolog_with_trainer")
transformers_trainer.train()
last_run = mlflow.last_active_run()
assert last_run.data.metrics["epoch"] == 1.0
assert last_run.data.params["model_type"] == "distilbert"
pipe = pipeline(
task="text-classification",
model=transformers_trainer.model,
tokenizer=DistilBertTokenizerFast.from_pretrained("distilbert-base-uncased"),
)
preds = pipe(["This is pretty ok, I guess", "I came here to chew bubblegum"])
assert len(preds) == 2
assert all(x["score"] > 0 for x in preds)
client = mlflow.MlflowClient()
runs = client.search_runs([exp.experiment_id])
assert len(runs) == 1
@skip_setfit
def test_active_autolog_no_setfit_logging_followed_by_successful_sklearn_autolog(
iris_data, setfit_trainer
):
mlflow.autolog()
exp = mlflow.set_experiment(experiment_name="setfit_with_sklearn")
# Train and evaluate
setfit_trainer.train()
metrics = setfit_trainer.evaluate()
assert metrics["accuracy"] > 0
# Run inference
preds = setfit_trainer.model([
"i loved the new Star Trek show!",
"That burger was gross; it tasted like it was made from cat food!",
])
assert len(preds) == 2
# Test that autologging works for a simple sklearn model (local disabling functions)
with mlflow.start_run(experiment_id=exp.experiment_id) as run:
model = sklearn.cluster.KMeans()
X, y = iris_data
model.fit(X, y)
logged_sklearn_data = mlflow.get_run(run.info.run_id)
assert logged_sklearn_data.data.tags["estimator_name"] == "KMeans"
# Assert only the sklearn KMeans model was logged to the experiment
client = mlflow.MlflowClient()
runs = client.search_runs([exp.experiment_id])
assert len(runs) == 1
assert runs[0].info == logged_sklearn_data.info
def test_active_autolog_allows_subsequent_sklearn_autolog(iris_data, transformers_trainer):
mlflow.autolog()
exp = mlflow.set_experiment(experiment_name="trainer_with_sklearn")
transformers_trainer.train()
last_run = mlflow.last_active_run()
assert last_run.data.metrics["epoch"] == 1.0
assert last_run.data.params["model_type"] == "distilbert"
pipe = pipeline(
task="text-classification",
model=transformers_trainer.model,
tokenizer=DistilBertTokenizerFast.from_pretrained("distilbert-base-uncased"),
)
preds = pipe(["This is pretty ok, I guess", "I came here to chew bubblegum"])
assert len(preds) == 2
assert all(x["score"] > 0 for x in preds)
with mlflow.start_run(experiment_id=exp.experiment_id) as run:
model = sklearn.cluster.KMeans()
X, y = iris_data
model.fit(X, y)
logged_sklearn_data = mlflow.get_run(run.info.run_id)
assert logged_sklearn_data.data.tags["estimator_name"] == "KMeans"
# Assert only the sklearn KMeans model was logged to the experiment
client = mlflow.MlflowClient()
runs = client.search_runs([exp.experiment_id])
assert len(runs) == 2
sklearn_run = [x for x in runs if x.info.run_id == run.info.run_id]
assert sklearn_run[0].info == logged_sklearn_data.info
@skip_setfit
def test_disabled_sklearn_autologging_does_not_revert_to_enabled_with_setfit(
iris_data, setfit_trainer
):
mlflow.autolog()
mlflow.sklearn.autolog(disable=True)
exp = mlflow.set_experiment(experiment_name="setfit_with_sklearn_no_autologging")
# Train and evaluate
setfit_trainer.train()
metrics = setfit_trainer.evaluate()
assert metrics["accuracy"] > 0
# Run inference
preds = setfit_trainer.model([
"i loved the new Star Trek show!",
"That burger was gross; it tasted like it was made from cat food!",
])
assert len(preds) == 2
# Test that autologging does not log since it is manually disabled above.
with mlflow.start_run(experiment_id=exp.experiment_id) as run:
model = sklearn.cluster.KMeans()
X, y = iris_data
model.fit(X, y)
# Assert that only the run info is logged
logged_sklearn_data = mlflow.get_run(run.info.run_id)
assert logged_sklearn_data.data.params == {}
assert logged_sklearn_data.data.metrics == {}
client = mlflow.MlflowClient()
runs = client.search_runs([exp.experiment_id])
assert len(runs) == 1
assert runs[0].info == logged_sklearn_data.info
def test_disable_sklearn_autologging_does_not_revert_with_trainer(iris_data, transformers_trainer):
mlflow.autolog()
mlflow.sklearn.autolog(disable=True)
exp = mlflow.set_experiment(experiment_name="trainer_with_sklearn")
transformers_trainer.train()
mlflow.flush_async_logging()
last_run = mlflow.last_active_run()
assert last_run.data.metrics["epoch"] == 1.0
assert last_run.data.params["model_type"] == "distilbert"
pipe = pipeline(
task="text-classification",
model=transformers_trainer.model,
tokenizer=DistilBertTokenizerFast.from_pretrained("distilbert-base-uncased"),
)
preds = pipe([
"Did you hear that guitar solo? Brilliant!",
"That band should avoid playing live.",
])
assert len(preds) == 2
assert all(x["score"] > 0 for x in preds)
# Test that autologging does not log since it is manually disabled above.
with mlflow.start_run(experiment_id=exp.experiment_id) as run:
model = sklearn.cluster.KMeans()
X, y = iris_data
model.fit(X, y)
# Assert that only the run info is logged
logged_sklearn_data = mlflow.get_run(run.info.run_id)
assert logged_sklearn_data.data.params == {}
assert logged_sklearn_data.data.metrics == {}
client = mlflow.MlflowClient()
runs = client.search_runs([exp.experiment_id])
assert len(runs) == 2
sklearn_run = [x for x in runs if x.info.run_id == run.info.run_id]
assert sklearn_run[0].info == logged_sklearn_data.info
def test_trainer_hyperparameter_tuning_does_not_log_sklearn_model(
transformers_hyperparameter_trainer,
):
mlflow.autolog()
exp = mlflow.set_experiment(experiment_name="hyperparam_trainer")
transformers_hyperparameter_trainer.train()
mlflow.flush_async_logging()
last_run = mlflow.last_active_run()
assert last_run.data.metrics["epoch"] == 3.0
assert last_run.data.params["model_type"] == "distilbert"
pipe = pipeline(
task="text-classification",
model=transformers_hyperparameter_trainer.model,
tokenizer=DistilBertTokenizerFast.from_pretrained("distilbert-base-uncased"),
)
assert len(pipe("This is wonderful!")[0]["label"]) > 5 # checking for 'LABEL_0' or 'LABEL_1'
client = mlflow.MlflowClient()
runs = client.search_runs([exp.experiment_id])
assert len(runs) == 1
def test_trainer_hyperparameter_tuning_functional_does_not_log_sklearn_model(
transformers_hyperparameter_functional,
):
mlflow.autolog()
exp = mlflow.set_experiment(experiment_name="hyperparam_trainer_functional")
transformers_hyperparameter_functional.train()
mlflow.flush_async_logging()
last_run = mlflow.last_active_run()
assert last_run.data.metrics["epoch"] == 1.0
assert last_run.data.params["model_type"] == "distilbert"
pipe = pipeline(
task="text-classification",
model=transformers_hyperparameter_functional.model,
tokenizer=DistilBertTokenizerFast.from_pretrained("distilbert-base-uncased"),
)
assert len(pipe("This is wonderful!")[0]["label"]) > 5 # checking for 'LABEL_0' or 'LABEL_1'
client = mlflow.MlflowClient()
runs = client.search_runs([exp.experiment_id])
assert len(runs) == 1