This is the second post in our Amazon SageMaker series, building on notebooks, training jobs, and endpoints.
For genuinely standard, well-understood problems — binary classification, regression, ranking — writing custom training code from scratch is often more work than the problem actually requires. SageMaker's built-in algorithms are pre-implemented, optimized, well-tested implementations of common algorithms, requiring only your data and a set of hyperparameters — the same "reach for a well-maintained standard solution before writing something custom" instinct covered for Terraform's Registry and Ansible Galaxy earlier in this blog, applied here to ML algorithms specifically.
Preparing data in the format SageMaker expects
import pandas as pd
from sklearn.model_selection import train_test_split
df = pd.read_csv("customer_churn.csv")
# XGBoost's built-in algorithm expects the target column first, no header row
train_df, val_df = train_test_split(df, test_size=0.2, random_state=42)
train_df.to_csv("train.csv", index=False, header=False)
val_df.to_csv("validation.csv", index=False, header=False)session.upload_data("train.csv", bucket=bucket, key_prefix="training-data")
session.upload_data("validation.csv", bucket=bucket, key_prefix="training-data")Each built-in algorithm expects data in a specific format — here, XGBoost's expectation of the target column first with no header row. Getting this wrong doesn't usually produce an obviously-labeled error; it produces a model that trains "successfully" while learning something subtly different from what you intended, because it's reading the wrong column as the target. Checking the specific algorithm's documented data format requirement before training, not after a confusing result, is worth the extra five minutes.
Configuring and running training
from sagemaker.estimator import Estimator
from sagemaker.inputs import TrainingInput
xgb_image = sagemaker.image_uris.retrieve("xgboost", session.boto_region_name, "1.7-1")
estimator = Estimator(
image_uri=xgb_image,
role=role,
instance_count=1,
instance_type="ml.m5.xlarge",
output_path=f"s3://{bucket}/model-output",
)
estimator.set_hyperparameters(
objective="binary:logistic",
num_round=100,
max_depth=5,
eta=0.2,
eval_metric="auc",
)
train_input = TrainingInput(f"s3://{bucket}/training-data/train.csv", content_type="text/csv")
val_input = TrainingInput(f"s3://{bucket}/training-data/validation.csv", content_type="text/csv")
estimator.fit({"train": train_input, "validation": val_input})sagemaker.image_uris.retrieve() resolves the correct pre-built container image for the algorithm and region — you're not writing training code at all here, only configuring hyperparameters and pointing the job at your prepared data. The container image itself contains the actual algorithm implementation.
Hyperparameter tuning: automating the search, not just the training
from sagemaker.tuner import HyperparameterTuner, ContinuousParameter, IntegerParameter
tuner = HyperparameterTuner(
estimator=estimator,
objective_metric_name="validation:auc",
hyperparameter_ranges={
"eta": ContinuousParameter(0.01, 0.3),
"max_depth": IntegerParameter(3, 10),
"num_round": IntegerParameter(50, 200),
},
max_jobs=20,
max_parallel_jobs=4,
)
tuner.fit({"train": train_input, "validation": val_input})Rather than manually guessing hyperparameter values and re-running training repeatedly by hand, a tuning job runs many training jobs automatically — 20 here, 4 running in parallel at a time — searching the defined ranges for the combination that best optimizes the chosen metric (validation AUC, here). This is a real, non-trivial compute cost (20 training jobs, not one), worth weighing against how much a better hyperparameter combination is actually likely to matter for a given problem, rather than defaulting to an exhaustive search for every model regardless of expected payoff.
Choosing a tuning strategy
By default, HyperparameterTuner uses Bayesian optimization — using the results of earlier jobs to intelligently choose the next combination to try, rather than searching blindly. For very large search spaces, or when jobs can genuinely run at high parallelism without diminishing returns, a random search strategy is worth considering as an alternative:
tuner = HyperparameterTuner(
estimator=estimator,
objective_metric_name="validation:auc",
hyperparameter_ranges={...},
max_jobs=40,
max_parallel_jobs=10,
strategy="Random",
)Bayesian optimization tends to find a good combination in fewer total jobs, since each job's result informs the next choice — generally the better default for a limited job budget. Random search parallelizes more naturally and can be a reasonable choice when you can afford to run many jobs simultaneously and want to avoid the sequential dependency Bayesian optimization's informed choices introduce between rounds.
Comparing tuning job results
import boto3
client = boto3.client("sagemaker")
response = client.list_training_jobs_for_hyper_parameter_tuning_job(
HyperParameterTuningJobName=tuner.latest_tuning_job.name,
SortBy="FinalObjectiveMetricValue",
SortOrder="Descending",
)
for job in response["TrainingJobSummaries"][:5]:
print(job["TrainingJobName"], job["FinalObjectiveMetricValue"])Listing every job from a tuning run, sorted by the actual objective metric, is worth doing directly rather than only trusting the tuner to pick the "best" job automatically — occasionally the best-scoring job on the tuning metric alone has a hyperparameter combination (an unusually high max_depth, for instance) that raises a genuine overfitting concern worth a second look before deploying it, the same "confirm on held-out data, don't just trust a leaderboard number" instinct covered for fine-tuning evaluation in our OpenAI series.
Monitoring training progress
import boto3
client = boto3.client("sagemaker")
response = client.describe_training_job(TrainingJobName=estimator.latest_training_job.name)
print(response["TrainingJobStatus"], response.get("FinalMetricDataList"))Training jobs run asynchronously — fit() blocks by default waiting for completion, but the job itself, and its logs (available directly in CloudWatch Logs), can be inspected independently, useful for a long-running job you don't want to keep a notebook connection open and blocking for the entire duration.
Warm-starting a tuning job from a previous one
Re-running hyperparameter tuning from scratch every time a model needs retraining on updated data throws away everything the previous tuning run already learned about which regions of the search space perform well. Warm start fixes this:
from sagemaker.tuner import WarmStartConfig, WarmStartTypes
warm_start_config = WarmStartConfig(
warm_start_type=WarmStartTypes.TRANSFER_LEARNING,
parents={previous_tuning_job_name},
)
tuner = HyperparameterTuner(
estimator=estimator,
objective_metric_name="validation:auc",
hyperparameter_ranges={
"eta": ContinuousParameter(0.01, 0.3),
"max_depth": IntegerParameter(3, 10),
"num_round": IntegerParameter(50, 200),
},
max_jobs=10,
max_parallel_jobs=4,
warm_start_config=warm_start_config,
)TRANSFER_LEARNING reuses the previous tuning job's results as a prior for the new search — meaning a smaller max_jobs budget can still converge on a good combination quickly, since the search isn't starting blind. This is genuinely valuable for the recurring-retraining case covered in the Pipelines post later in this series: a model retrained nightly on fresh data doesn't need a full 20-job search from scratch every single night if last night's search already narrowed down the productive region of the hyperparameter space.
When a built-in algorithm genuinely isn't the right fit
Built-in algorithms cover standard, well-understood problem shapes well — but a genuinely novel architecture, a custom loss function, or a model type SageMaker doesn't ship a built-in container for needs a different approach: Bring Your Own Container (BYOC), where you supply your own Docker image (built with the same multi-stage, minimal-base-image practices covered in our Docker series) containing your own training code, or Bring Your Own Script, using one of SageMaker's framework containers (PyTorch, TensorFlow) with a custom training script rather than a fully custom image. The decision mirrors the "reach for a maintained standard before writing something custom" principle from earlier in this post: built-in algorithms first, a framework container with a custom script second, a fully custom BYOC image only when neither of the first two options can express what the problem actually needs.
Local mode: iterating without provisioning real infrastructure
For fast iteration on a small sample before committing to a real (billed) training job, instance_type="local" runs the exact same estimator against Docker on your own machine:
estimator = Estimator(
image_uri=xgb_image,
role=role,
instance_count=1,
instance_type="local",
)This catches a data-format or configuration mistake in seconds, on a small sample, before paying for a real ml.m5.xlarge training job against the full dataset — the same "fail fast and cheaply, in a tight local loop" instinct as molecule converge for Ansible roles, covered earlier in this blog.
Reusing a trained model artifact without retraining
A model artifact already sitting in S3 from a previous job can be attached to a new Estimator directly, useful for deploying or evaluating a model without re-running training at all:
estimator = Estimator.attach(training_job_name="previous-job-name")What to actually remember from this post
- Built-in algorithms are pre-implemented and well-tested — the right starting point for standard problems, the same "use a maintained standard before writing custom" instinct as reaching for the Terraform Registry or Ansible Galaxy.
- Each algorithm expects a specific data format — verify it before training, not after a confusing, silently-wrong result.
image_uris.retrieve()resolves the correct pre-built container for a given algorithm and region — you configure hyperparameters, you don't write the algorithm's implementation.- Hyperparameter tuning automates the search across many training jobs — a real compute cost, worth weighing against the problem's actual sensitivity to hyperparameter choice; Bayesian search generally suits a limited job budget better than random search.
- Inspect a tuning job's actual leaderboard, not just its automatically-selected best job — the top-scoring combination on a tuning metric alone can still carry a real overfitting risk worth a second look.
Next in the series: Deploying Models with SageMaker Real-Time and Serverless Endpoints, where the model artifact this training job produced actually starts serving predictions.
