Skip to main content

Module 5 — Custom containers and training scripts

Built-in XGBoost is convenient, but the moment you want to add a preprocessing step, a stratified split or a metric that AWS does not export, you leave that comfort. This module ports the churn model to a scikit-learn script, then shows the two escape hatches for cases the script mode does not cover.

Script mode, the default path​

The script mode is the middle ground: AWS provides a container with a framework already installed, you provide the training script. Nine cases out of ten this is what you want.

from sagemaker.sklearn.estimator import SKLearn

sk = SKLearn(
entry_point="train.py",
source_dir="src/",
role=role,
instance_type="ml.m5.xlarge",
framework_version="1.2-1",
py_version="py3",
output_path=f"s3://{bucket}/churn/models/",
hyperparameters={
"n-estimators": 200,
"max-depth": 8,
},
)
sk.fit({"train": train_input, "validation": val_input})

The source_dir is a folder that gets bundled and uploaded to S3, then extracted inside the container. Add a requirements.txt in that folder and SageMaker installs those dependencies before running the script. That is the polite way to add imbalanced-learn or shap without touching an image.

What the script sees​

Inside the container, SageMaker exposes the same interface for every framework, through environment variables and fixed paths. Learning them is a fifteen-minute investment that pays off every project.

VariablePoints toContents
SM_CHANNEL_TRAIN/opt/ml/input/data/trainFiles from the train channel
SM_CHANNEL_VALIDATION/opt/ml/input/data/validationFiles from the validation channel
SM_MODEL_DIR/opt/ml/modelWhere to save the model; auto-uploaded to S3
SM_OUTPUT_DATA_DIR/opt/ml/output/dataExtra outputs, tarballed to S3
SM_NUM_GPUS, SM_NUM_CPUS—Instance capabilities
SM_HPSJSONAll hyperparameters as a single object

The corresponding train.py, dead-simple:

import argparse, os, json
import pandas as pd
import joblib
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import roc_auc_score

def parse():
p = argparse.ArgumentParser()
p.add_argument("--n-estimators", type=int, default=100)
p.add_argument("--max-depth", type=int, default=6)
p.add_argument("--train", type=str, default=os.environ["SM_CHANNEL_TRAIN"])
p.add_argument("--validation", type=str, default=os.environ["SM_CHANNEL_VALIDATION"])
p.add_argument("--model-dir", type=str, default=os.environ["SM_MODEL_DIR"])
return p.parse_args()

def load(path):
frames = [pd.read_parquet(os.path.join(path, f))
for f in os.listdir(path) if f.endswith(".parquet")]
df = pd.concat(frames, ignore_index=True)
return df.drop(columns=["churn"]), df["churn"]

if __name__ == "__main__":
args = parse()
X_tr, y_tr = load(args.train)
X_val, y_val = load(args.validation)

model = GradientBoostingClassifier(
n_estimators=args.n_estimators, max_depth=args.max_depth
).fit(X_tr, y_tr)

auc = roc_auc_score(y_val, model.predict_proba(X_val)[:, 1])
print(f"validation-auc: {auc:.4f}") # picked up by the log metric parser

joblib.dump(model, os.path.join(args.model_dir, "model.joblib"))

Two points to notice. Hyperparameters passed to the estimator arrive as command-line arguments, with dashes converted from underscores — n-estimators from the SDK becomes --n-estimators for argparse. And the model must be written to SM_MODEL_DIR; anything left elsewhere is discarded when the container stops. Files written to SM_MODEL_DIR are tarred into model.tar.gz and uploaded to the estimator's output_path.

Exposing metrics for the console and tuning​

The training console shows exactly the metrics you tell it about. Declare them once with a regex on the standard output:

sk = SKLearn(
...,
metric_definitions=[
{"Name": "validation:auc", "Regex": r"validation-auc:\s*([0-9\.]+)"},
],
)

The Regex group ([0-9\.]+) captures the number that follows validation-auc: in the script's print. That value is what module 6 uses as the objective for tuning; without a metric definition, the tuning job has nothing to optimize.

When script mode is not enough: the fully custom image​

There is a small set of cases the framework containers do not cover: a system library needed at runtime (libgeos for geopandas), a compiled binary (OR-Tools), an unusual framework version, or a corporate wrapper that must be installed the same way in dev and in prod. The answer is a fully custom image.

FROM public.ecr.aws/deep-learning-containers/sklearn:1.2-1-cpu-py3

USER root
RUN apt-get update && apt-get install -y --no-install-recommends libgomp1 \
&& rm -rf /var/lib/apt/lists/*

COPY train.py /opt/ml/code/train.py
ENV SAGEMAKER_PROGRAM=train.py

Push the image to ECR, pass its URI as image_uri= to a generic Estimator, and SageMaker treats it like a built-in. The interface is the same — the /opt/ml/... paths and the SM_* variables — because it is what SageMaker inspects; the base image only has to respect it.

Local mode: debugging without paying​

Building an image and pushing it to ECR each iteration is a slow feedback loop. SageMaker's local mode runs the same container on your laptop's Docker, with the exact same environment variables, before you launch anything on AWS.

sk = SKLearn(
entry_point="train.py",
source_dir="src/",
role=role,
instance_type="local", # the magic value
framework_version="1.2-1",
)
sk.fit({"train": "file://./data/train", "validation": "file://./data/validation"})

file:// paths and instance_type="local" are the only two changes. Loop on local mode until the script exits with the metric printed, then switch back to ml.m5.xlarge for the real run. A pushed-to-cloud iteration that would take five minutes takes fifteen seconds locally; that is not a minor difference on a script you are debugging.

Every hyperparameter has to arrive as a string

SageMaker sends hyperparameters to the container as strings; argparse with type=int casts them back. A script that writes if args.max_depth == 6 when it was passed "6" will fail silently. This surfaces most often on booleans: type=bool accepts any non-empty string as True. Use type=lambda s: s.lower() == "true" explicitly.

Summary​

  • Script mode is the default: your script inside AWS's framework container, with requirements.txt for extra dependencies.
  • The script talks to SageMaker through a fixed set of environment variables and paths — SM_CHANNEL_*, SM_MODEL_DIR.
  • metric_definitions regexes turn print() lines into structured metrics for the console and for tuning.
  • A fully custom image covers system dependencies; local mode shortens the debug loop from minutes to seconds.

Next module: let SageMaker choose the hyperparameters for us.