Custom training scripts
Viam’s managed training handles TensorFlow and TFLite classification and detection out of the box. Custom training scripts are available for other use cases like a different framework, custom preprocessing, time-series data from a sequence dataset, or a training pipeline you want to share with your organization. Managed training doesn’t accept sequence datasets, so training on one always requires a custom training script.
Before writing your own, check the registry for existing training scripts and pre-trained models you can deploy directly. If a training script there fits your needs, skip ahead to Submit a training job.
How custom training scripts work
When you submit a custom training job, Viam:
- Pulls your dataset and writes it into the container:
- For a binary dataset, a JSONLines metadata file plus the image files.
- For a sequence dataset, three Parquet files plus the image files.
- Runs your script inside a Viam-hosted Docker container with GPU access and a framework version you select.
- Packages the artifacts your script writes to the output directory and publishes them as a new model version in your organization’s registry.
Because the script runs in a known container and receives standardized inputs, you can retrain on new datasets without changing the script or redeploying code.
Write a training script
A training script is a Python project that the Viam platform runs in the cloud. The platform provides your script with a dataset and an output directory; your script produces a trained model.
File structure
my-training/
├── model/
│ ├── training.py
│ └── __init__.py
└── setup.py
setup.py declares your dependencies:
from setuptools import find_packages, setup
setup(
name="my-training",
version="0.1",
packages=find_packages(),
include_package_data=True,
install_requires=[
# Add your dependencies here, for example:
# "tensorflow>=2.11",
# "numpy",
],
)
training.py
Your script always receives --model_output_directory.
The arguments that point to the dataset depend on the dataset type.
A job on a binary dataset passes these arguments:
| Argument | Description |
|---|---|
--dataset_file | Path to a JSONLines file containing dataset metadata: file paths and annotations for each data point. |
--model_output_directory | Directory where your script must save its model artifacts. |
A job on a sequence dataset passes three Parquet file paths instead of --dataset_file.
See Sequence dataset inputs.
You can add custom arguments (like --num_epochs or --labels) and pass them when you submit the training job.
Here is the overall shape of a training script for a binary dataset:
import argparse
import json
import os
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--dataset_file", dest="data_json",
type=str, required=True)
parser.add_argument("--model_output_directory", dest="model_dir",
type=str, required=True)
# Add custom arguments as needed:
parser.add_argument("--num_epochs", dest="num_epochs",
type=int, default=200)
return parser.parse_args()
def load_dataset(data_json):
"""Parse the JSONLines dataset file."""
entries = []
with open(data_json, "r") as f:
for line in f:
entries.append(json.loads(line))
return entries
if __name__ == "__main__":
args = parse_args()
dataset = load_dataset(args.data_json)
# --- Your training logic goes here ---
# Use the dataset entries to train a model with
# whatever framework fits your use case.
model = ...
labels = ...
# Save model artifacts to the output directory.
# The format must match what your ML model service expects.
# For example, tflite_cpu expects a .tflite file and labels.txt.
with open(os.path.join(args.model_dir, "model.tflite"), "wb") as f:
f.write(model)
with open(os.path.join(args.model_dir, "labels.txt"), "w") as f:
f.write("\n".join(labels))
The critical parts:
- Parse arguments: Accept
--model_output_directoryplus the dataset arguments for your dataset type:--dataset_filefor a binary dataset, or the three sequence file arguments for a sequence dataset. - Read the dataset: For a binary dataset, each line in the JSONLines file is a JSON object with an
image_pathand eitherclassification_annotations,bounding_box_annotations, or both. For a sequence dataset, read the three Parquet files instead. - Save to the output directory: When the job completes, Viam packages everything in this directory and publishes it to the registry as a new model version. Files in a
tmp/subdirectory are excluded: use it for intermediate work.
If the script exits with a non-zero status or produces no files in the output directory, the training job is marked as failed.
Binary dataset file format
Each line of the --dataset_file file is a JSON object like this:
{
"image_path": "/path/to/data/img1.jpeg",
"classification_annotations": [{ "annotation_label": "blue_star" }],
"bounding_box_annotations": [
{
"annotation_label": "blue_star",
"x_min_normalized": 0.382,
"x_max_normalized": 0.51,
"y_min_normalized": 0.356,
"y_max_normalized": 0.527
}
]
}
Bounding box coordinates are normalized to the range 0.0-1.0 relative to image dimensions.
For classification, read classification_annotations.
For object detection, read bounding_box_annotations.
See the example training script for complete parsing functions that handle both annotation types.
Sequence dataset inputs
A job on a sequence dataset doesn’t get a --dataset_file.
Instead, your script receives three Parquet files: --binary_data_file, --tabular_data_file, and --sequences_file.
It still receives --model_output_directory.
The images are files on disk, and the Parquet files hold their metadata and your readings.
Join the three files on sequence_id.
See Sequence dataset format for the columns.
To record and export a sequence dataset to test your script on, follow the sequences tutorial.
Accessing Viam APIs
The platform provides API_KEY and API_KEY_ID environment variables if your script needs to call Viam APIs during training, for example, to query additional data:
import os
from viam.rpc.dial import DialOptions
from viam.app.viam_client import ViamClient
async def connect() -> ViamClient:
dial_options = DialOptions.with_api_key(
os.environ.get("API_KEY"), os.environ.get("API_KEY_ID")
)
return await ViamClient.create_from_dial_options(dial_options)
Match your output to an ML model service
After training, Viam packages your output directory and publishes it as a model version in your organization’s registry. To run the model on a machine, the ML model service you deploy with it must be able to load the files your script wrote.
For TFLite models loaded with tflite_cpu, save a model.tflite file and
a labels.txt listing each label on its own line. This is what the
classification-tflite example
produces.
For other frameworks, consult the module’s README on the registry – for
example, tensorflow-cpu,
onnx-cpu, or
torch-cpu – for the file
format and tensor shape expectations. Write your output in that format so
the deployed model loads correctly.
After uploading, follow Deploy a model to a machine to add the matching ML model service and vision service to your machine.
Complete example
For a full working training script, see the classification-tflite example on GitHub. It trains a TFLite single-label classification model using TensorFlow and Keras.
Test and upload
Before submitting a cloud training job, test your script locally against an exported dataset.
Export a dataset
viam dataset export --destination=<destination> --dataset-id=<dataset-id>
For a binary dataset, this downloads the image files and a dataset.jsonl metadata file.
To download only the JSONL file without the images, add --only-jsonl.
For a sequence dataset, this downloads a zip of three Parquet files plus the images.
To download only the zip without the images, add --only-parquet.
See Export a sequence dataset.
You can get the dataset ID from the DATASETS tab or by running viam dataset list.
Test locally with Docker
The test-local command runs your training script inside the same Docker container that cloud training uses.
This catches problems that plain Python testing misses: missing system dependencies, Python version differences, and package conflicts.
test-local passes your script only --dataset_file, so it works only with scripts for binary datasets.
To test a script for a sequence dataset, run it directly.
viam training-script test-local \
--training-script-directory=my-training/ \
--dataset-file=dataset.jsonl \
--dataset-root=<destination> \
--model-output-directory=<output-dir>
The --dataset-file path is relative to --dataset-root.
The command mounts your script, dataset, and output directories into the container.
To match a specific cloud container version, use --container-version.
Run viam train containers list to list available container versions with their framework versions and end-of-life dates.
You can pass custom arguments with --custom-args:
viam training-script test-local \
--training-script-directory=my-training/ \
--dataset-file=dataset.jsonl \
--dataset-root=<destination> \
--model-output-directory=<output-dir> \
--custom-args=num_epochs=5,labels="label1 label2"
Note
The training containers are built for linux/x86_64 (amd64). On ARM systems like Apple Silicon Macs, Docker uses Rosetta 2 emulation automatically, which may be slower but ensures your script runs in the same environment as cloud training.
Run without Docker
For a quick check without Docker, run your script directly. For a binary dataset:
python3 -m model.training --dataset_file=<path/to/dataset.jsonl> \
--model_output_directory=<output-dir>
For a sequence dataset, unzip the export first, then pass the three Parquet files:
python3 -m model.training \
--binary_data_file=<unzipped-dir>/binary_data.parquet \
--tabular_data_file=<unzipped-dir>/tabular_data.parquet \
--sequences_file=<unzipped-dir>/sequences.parquet \
--model_output_directory=<output-dir>
In an export, the path column in binary_data.parquet is relative to the export directory.
In a cloud training job, path is an absolute path.
Resolve relative paths against the export directory so the same script works in both places.
Package and upload
tar -czvf my-training.tar.gz my-training/
viam training-script upload --path=my-training.tar.gz \
--org-id=<org-id> --script-name=my-training-script
You can also specify --framework, --type, --visibility, and --description when uploading.
Scripts default to private (visible only within your organization). Set --visibility=public to share with other organizations through the registry.
See the CLI reference for the full list of flags.
To find your organization ID, run viam organization list.
After uploading, your script appears in the registry.
Submit a training job
Once a training script is in the registry, whether you uploaded it or are using someone else’s, submit a training job to run it against a dataset.
Every custom training job runs inside a Viam-hosted container on GPU-backed
cloud infrastructure. Containers are based on Python 3.10 with TensorFlow
pre-installed. The currently supported versions are tf:2.16, tf:2.17, and tf:2.21.
To use a different framework (PyTorch, scikit-learn, or anything installable
with pip), add it to the install_requires list in setup.py; Viam
installs the packages in the container before running your script.
Custom training is part of the Viam platform; see pricing for plan details.
To list available containers with their framework versions and end-of-life dates, run:
viam train containers list
- Go to your DATASETS and select the dataset you want to train on.
- Click Train model.
- Select Train on a custom training script and follow the prompts.
- Select a Container version that matches the framework your training script uses. The UI shows a warning if the selected container is approaching its end-of-life date.
- Optionally, add custom arguments as key-value pairs in the Arguments section.
Use viam train submit custom from-registry:
viam train submit custom from-registry --dataset-id=<dataset-id> \
--org-id=<org-id> --model-name=my-model \
--model-version=1 --version=1 \
--script-name=<namespace>:<script-name> \
--container-version=<container-version> \
--args=num_epochs=100,labels="'label1 label2'"
You can get the dataset ID from the DATASETS tab or by running viam dataset list.
Use the ML Training Client API to submit training jobs programmatically.
Monitor and debug
In the Viam app, go to the DATA page, click the MODELS tab, and expand Active Training. Click a job ID to view its logs.
List training jobs:
viam train list --org-id=<org-id> --job-status=unspecified
View logs for a specific job:
viam train logs --job-id=<job-id>
Training logs expire after 7 days. You will receive an email when your training job completes.
If a job fails, check the logs first: the error message usually indicates the problem. Note that training scripts may emit log lines at the error level and still succeed; check the final job status rather than individual log lines.
Was this page helpful?
Glad to hear it! If you have any other feedback please let us know:
We're sorry about that. To help us improve, please tell us what we can do better:
Thank you!