Train a model with a custom training script
A custom training script is Python code that Viam runs in a cloud container to train a model on one of your datasets. Use one when built-in training can’t train the model you need: for example, a robot policy, a model trained on a sequence dataset, a PyTorch or ONNX model, or an image model where you control the architecture, preprocessing, or hyperparameters. To compare the two, see Built-in or custom training.
Prerequisites
- A dataset to train on, either binary or sequence. See Create a dataset.
- The Viam CLI, logged in, for testing, uploading and submitting from the command line. Uploading a script with the robot policy task type needs CLI version 1.15.0 or later.
- Docker, to test the script locally in its training container.
1. Find an existing training script
Before writing your own, check whether a training script in the registry already does what you need. The registry also has pre-trained models you can deploy without training.
- Open the registry, filtered to training scripts. You can also filter by owner, to see scripts from Viam, the community, or your own organization.
- Search by keyword, such as the task (
classification,detection,policy) or the framework (pytorch,onnx). - Open a script and read its description to check:
- the task type and framework of the model it produces
- whether it trains on a binary or a sequence dataset, and what labels it expects
- which arguments it accepts, such as the number of epochs
If a script fits, note its name in <namespace>:<script-name> form and skip ahead to step 6.
To list training scripts from code, call list_registry_items with the PACKAGE_TYPE_ML_TRAINING type.
2. Plan your script
Make three decisions before you write code. Each one shapes the script, and you need all three to test and upload it.
Which dataset type it reads
A script reads either a binary dataset or a sequence dataset, and Viam passes it different files for each:
- Binary dataset: a JSONLines file listing each file and its labels or bounding boxes, plus the files themselves. Use it for models that learn from individual images.
- Sequence dataset: three Parquet files holding images and sensor readings grouped into time-windowed sequences, plus the images. Use it for models that learn from what happened over time, such as robot policies. Built-in training can’t read a sequence dataset.
For the file formats and the arguments that point to them, see Training dataset formats.
What model it produces
A script declares a task type and a framework when you upload it:
- The task type says what the model does: single label classification, multi label classification, object detection, robot policy, or Other. See Types of models.
- The framework decides what runs the model on your machine. See Built-in or custom training, and check that something for your framework runs on your machines’ hardware before you write the script.
Note
Viam’s built-in evaluation supports only single label classification, multi label classification, and object detection models in the TFLite, TensorFlow, or ONNX framework. There’s no built-in evaluation for models trained on a sequence dataset.
Your script must write the files that the service or module loads.
For example, the tflite_cpu ML model service loads a model.tflite file and a labels.txt file listing each label on its own line, which is what the classification-tflite example writes.
For other frameworks, the module’s README lists the files and tensor shapes it expects: for example, tensorflow-cpu, onnx-cpu, or torch-cpu.
Use Registry when the model needs a module to run it, such as a robot policy.
The README of the module that runs it lists the files.
If no module exists for your model, write one.
Which container it runs in
Every custom training job runs in a container on GPU-backed cloud infrastructure, and you pick the container when you test and when you submit:
- A Viam-managed container: Python with a machine learning framework, such as TensorFlow, pre-installed.
Viam adds and retires versions over time, so list them with the command below.
Viam installs any other packages your script lists in
setup.py, such as PyTorch or scikit-learn, before it runs the script. - A custom training container your organization registered, with your dependencies built in. Use one when your dependencies take a long time to install, so each job starts training right away, or when you need a different Python version or system libraries.
To list the containers available to your organization, with their IDs, framework versions, and end-of-life dates, run:
viam train containers list --org-id=<org-id>
Note the ID of the container you choose.
3. Write the script
A training script is a Python package.
Viam installs it in the container and runs it as python3 -m model.training, so it needs this layout:
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:
# "torch",
# "pandas",
],
)
training.py parses its arguments, reads the dataset, trains a model, and writes the model to --model_output_directory.
The arguments that point to the dataset depend on its type:
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 your own arguments as needed:
parser.add_argument("--num_epochs", dest="num_epochs",
type=int, default=200)
return parser.parse_args()
def load_dataset(data_json):
"""Read one JSON object per line of the dataset file."""
with open(data_json, "r") as f:
return [json.loads(line) for line in f]
if __name__ == "__main__":
args = parse_args()
entries = load_dataset(args.data_json)
# --- Your training logic goes here ---
# Each entry has an image_path and its classification_annotations
# or bounding_box_annotations.
model = ...
labels = ...
# Write the files that what runs the model loads.
# For example, tflite_cpu loads model.tflite 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))
import argparse
import os
import pandas as pd
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--binary_data_file", type=str, required=True)
parser.add_argument("--tabular_data_file", type=str, required=True)
parser.add_argument("--sequences_file", type=str, required=True)
parser.add_argument("--model_output_directory", dest="model_dir",
type=str, required=True)
# Add your own arguments as needed:
parser.add_argument("--num_epochs", dest="num_epochs",
type=int, default=200)
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
images = pd.read_parquet(args.binary_data_file)
readings = pd.read_parquet(args.tabular_data_file)
sequences = pd.read_parquet(args.sequences_file)
# In an export, image paths are relative to the export directory.
# In a training job, they're absolute. Resolve them so the script
# runs in both places.
export_dir = os.path.dirname(os.path.abspath(args.binary_data_file))
images["path"] = images["path"].apply(
lambda p: p if os.path.isabs(p) else os.path.join(export_dir, p))
# --- Your training logic goes here ---
# Join images and readings to sequences on sequence_id, and use each
# sequence's tags to select the examples to train on.
model = ...
# Write the files that what runs the model loads.
...
Add pandas and pyarrow to install_requires in setup.py.
If you unzip the export somewhere other than the export directory, resolve the paths against the export directory instead.
As you write the script:
- Accept only the arguments you need. Viam passes each argument you set at submit time as
--<key>=<value>, so your parser must accept every one you plan to pass. For the full list of arguments Viam passes, see Training script arguments. - Write only the model to the output directory. Viam publishes everything in it except a
tmp/subdirectory, which you can use for checkpoints and other intermediate files. - Exit with an error when training fails. The job fails if the script exits with a non-zero status or writes nothing to the output directory.
- Fall back to the CPU. Cloud jobs run on a GPU, but local tests don’t, so detect the GPU rather than assuming one.
- Write a
labels.txtfile if you want to evaluate the model. Evaluation reads the labels the model can predict from alabels.txtfile in the output directory, one label per line, and fails without one. The model also needs a task type and framework that evaluation supports; see What model it produces.
If your script needs to call Viam APIs during training, for example to query more data, see Viam API credentials.
4. Test the script locally
Run your script against an exported dataset before you submit a cloud job. Testing locally is free, while a cloud job bills GPU time from the moment it starts.
Export a dataset
viam dataset export --destination=<destination> --dataset-id=<dataset-id>
A binary dataset exports as a dataset.jsonl file plus the files it lists.
A sequence dataset exports as a zip of three Parquet files plus the images; unzip it before you test.
See Export a dataset.
To keep tests fast, export a small dataset, or pass a low epoch count.
Test locally with Docker
Note
test-local passes your script only --dataset_file, so it works only for a script that reads a binary dataset.
To test a script for a sequence dataset, run it directly.
viam training-script test-local runs your script inside the container you chose, the same one the cloud job uses.
This catches problems that running the script directly misses: missing system dependencies, a different Python version, and package conflicts.
Because a local test runs on the CPU instead of a GPU, you are only checking that the script runs, not how well it trains.
Run it from the directory you ran viam dataset export from:
viam training-script test-local \
--training-script-directory=my-training/ \
--dataset-root=. \
--dataset-file=<destination>/dataset.jsonl \
--container-id=<container-id> \
--model-output-directory=<output-dir> \
--custom-args=num_epochs=1
--dataset-file is relative to --dataset-root, and the container’s working directory is the dataset root, so the image paths in dataset.jsonl resolve.
--custom-args takes your own arguments as key=value pairs, separated by commas.
When it finishes, check that <output-dir> holds the files your framework needs.
Note
Training containers are built for linux/x86_64 (amd64). On ARM systems, such as Apple Silicon Macs, Docker runs them through Rosetta 2 emulation, which is slower. If the container fails to start, check that Docker Desktop has Rosetta emulation turned on.
Run without Docker
To check the script quickly, or to test a script for a sequence dataset, run it directly with Python. This doesn’t use the training container, so install your dependencies locally first.
The image paths in dataset.jsonl are relative to the directory you ran viam dataset export from, so run the script from there:
PYTHONPATH=my-training python3 -m model.training \
--dataset_file=<destination>/dataset.jsonl \
--model_output_directory=<output-dir> --num_epochs=1
Run the script from inside my-training/:
python3 -m model.training \
--binary_data_file=<export-dir>/binary_data.parquet \
--tabular_data_file=<export-dir>/tabular_data.parquet \
--sequences_file=<export-dir>/sequences.parquet \
--model_output_directory=<output-dir> --num_epochs=1
5. Package and upload the script
Package the script as a tarball and upload it to your organization’s registry:
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 \
--framework=<framework> --type=<task-type>
The --framework and --type flags declare what model the script produces: for example, --framework=tflite --type=single_label_classification.
With --framework=registry, also pass --module-resource=<namespace:module:model> for each module resource to deploy with the trained model.
--type=unspecified is the task type the Viam app shows as Other.
Scripts are private to your organization by default.
To share one with other organizations through the registry, add --visibility=public and a --description.
To find your organization ID, run viam organization list.
For every flag, see the CLI reference.
After uploading, your script appears in the registry.
6. Submit a training job
Once a training script is in the registry, whether you uploaded it or found it there, submit a job to run it on a dataset. The dataset must be the type the script reads.
Note
Custom training jobs run on premium GPUs and are billed at a higher rate than built-in training jobs. See pricing.
- 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.
- In the Container version dropdown, select the container you chose. The dropdown lists Viam-managed containers, marked (Viam), followed by your organization’s custom containers, marked (Custom: image). The Viam app shows a warning if the selected Viam-managed container is approaching its end-of-life date.
- Optionally, in the Arguments section, add arguments to pass to your script, such as the number of epochs.
Viam passes each key and value as a command-line argument, so
num_epochsand20become--num_epochs=20.
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-id=<container-id> \
--args=num_epochs=100,labels="'label1 label2'"
--version is the version of the training script to run.
You can get the dataset ID from the DATASETS tab or by running viam dataset list.
Tip
After you’ve tested the script locally, viam train submit custom with-upload uploads the tarball and submits a training job in one command.
It takes the upload flags, except that the task type flag is --model-type, plus the job’s flags, such as --dataset-id and --container-id, and it always uploads the script as private.
See Train with a custom training script.
Use submit_custom_training_job to submit custom training jobs programmatically.
Pass the container ID as container_id.
7. Verify the training job succeeded
A job ends with the status Completed, Failed, or Canceled.
When a training job succeeds or fails, Viam emails the user who submitted it.
Jobs submitted with an API key send no email, so check the job status with viam train list or viam train get instead.
Training logs expire after 7 days.
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. The Details panel shows the container the job runs in, under Container version and Container ID.
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>
When the job’s status is Completed, the trained model is in your organization’s registry: it’s listed on the MODELS tab, and its details page lists its versions. A script can log lines at the error level and still succeed, so check the job’s final status rather than individual log lines.
Troubleshooting
If a job fails, read its logs first: the error message usually names the problem.
What’s next
- Evaluate a model: for a classification or object detection model, measure how well it does on labeled images it wasn’t trained on before you deploy it.
- Deploy a model to a machine: add the trained model and the resource that runs it to your machine.
- Custom training containers: build your script’s dependencies into a container so jobs start training right away.
- Training dataset formats: every column and field your script can read.
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!