Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
536 changes: 536 additions & 0 deletions ARCHITECTURE.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions tinyml-modelmaker/tinyml_modelmaker/ai_modules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#################################################################################

import sys
from . import protocols
from . import timeseries
from . import vision
from . import audio
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def get_datasets_list(task_type=None):
elif task_type == 'audio_classification':
return ['SpeechCommands'] # ['oxford_flowers102']
else:
assert False, 'unknown task type for get_datasets_list'
raise ValueError(f'unknown task type for get_datasets_list: {task_type}')


def get_target_module(backend_name):
Expand Down Expand Up @@ -93,7 +93,7 @@ def run(self):
extract_root = os.path.dirname(self.params.dataset.input_data_path)
extract_success = utils.extract_files(self.params.dataset.input_data_path, extract_root)
if not extract_success:
raise "Dataset could not be extracted"
raise RuntimeError("Dataset could not be extracted")
self.params.dataset.input_data_path = os.path.dirname(self.params.dataset.input_data_path)

for split_name in self.params.dataset.split_names:
Expand Down Expand Up @@ -184,14 +184,16 @@ def run(self):
# self.out_files = dataset_utils.create_simple_split(self.file_list, self.params.common.project_run_path + '/dataset', self.params.dataset.split_names, self.params.dataset.split_factor, shuffle_items=True, random_seed=42)
self.logger.info('Splits of the dataset can be found at: {}'.format(self.params.dataset.annotation_path_splits))
else:
assert False, f'invalid dataset provided at {self.params.dataset.input_data_path}'
raise FileNotFoundError(f'invalid dataset provided at {self.params.dataset.input_data_path}')

def get_max_num_files(self):
if isinstance(self.params.dataset.max_num_files, (list, tuple)):
max_num_files = self.params.dataset.max_num_files
elif isinstance(self.params.dataset.max_num_files, int):
assert (0.0 < self.params.dataset.split_factor < 1.0), 'split_factor must be between 0 and 1.0'
assert len(self.params.dataset.split_names) > 1, 'split_names must have at least two entries'
if not (0.0 < self.params.dataset.split_factor < 1.0):
raise ValueError('split_factor must be between 0 and 1.0')
if len(self.params.dataset.split_names) <= 1:
raise ValueError('split_names must have at least two entries')
max_num_files = [None] * len(self.params.dataset.split_names)
for split_id, split_name in enumerate(self.params.dataset.split_names):
if split_id == 0:
Expand All @@ -202,7 +204,8 @@ def get_max_num_files(self):
#
else:
warnings.warn('unrecognized value for max_num_files - must be int, list or tuple')
assert len(self.params.dataset.split_names) > 1, 'split_names must have at least two entries'
if len(self.params.dataset.split_names) <= 1:
raise ValueError('split_names must have at least two entries')
max_num_files = [None] * len(self.params.dataset.split_names)
#
return max_num_files
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import copy
import glob
import json
import logging
import os
import random
import re
Expand All @@ -52,6 +53,8 @@

from .... import utils

logger = logging.getLogger(__name__)


def create_filelist(input_data_path: str, output_dir: str, ignore_str_list=None) -> str:
'''
Expand Down Expand Up @@ -85,26 +88,31 @@ def create_inter_file_split(file_list: str, split_list_files: tuple, split_facto
:param split_factor: can be a float number or a list of splits e.g [0.2, 0.3]
:return: out_files: List containing the paths of files that contain the dataset of the corresponding splits
'''
assert isinstance(split_list_files, (list, tuple)), "split_list_files should be passed as a tuple or list"
if not isinstance(split_list_files, (list, tuple)):
raise TypeError("split_list_files should be passed as a tuple or list")
number_of_splits = len(split_list_files)
split_factors = []
if type(split_factor) == float:
assert split_factor < 1.0, "split_factor should be less than 1"
if not (0.0 < split_factor < 1.0):
raise ValueError("split_factor must be in the range (0.0, 1.0)")
# The default split factor is the fraction for training set.
split_factors.append(split_factor)
# The remainder of the set will be equally split between val or val/test
remainder = 1 - split_factor

elif isinstance(split_factor, (list, tuple)):
assert sum(split_factor) <= 1, "The Sum of split factors should be <=1"
assert len(split_factor) <= len(split_list_files), "The number of elements in split factors should be less than/equal to number of split names"
if sum(split_factor) > 1:
raise ValueError("The Sum of split factors should be <=1")
if len(split_factor) > len(split_list_files):
raise ValueError("The number of elements in split factors should be less than/equal to number of split names")
split_factors.extend(split_factor)
remainder = 1 - sum(split_factor)

if number_of_splits > len(split_factor):
remainder_fraction = remainder / (number_of_splits - len(split_factor))
[split_factors.append(remainder_fraction) for _ in range(number_of_splits - len(split_factor))]
assert len(split_factor) == len(split_list_files), f"Number of split files: {len(split_list_files)} should be same as length of split factors: {len(split_factor)}"
if number_of_splits > len(split_factors):
remainder_fraction = remainder / (number_of_splits - len(split_factors))
[split_factors.append(remainder_fraction) for _ in range(number_of_splits - len(split_factors))]
if len(split_factors) != len(split_list_files):
raise ValueError(f"Number of split files: {len(split_list_files)} should be same as length of split factors: {len(split_factors)}")

with open(file_list) as fp:
list_of_files = [x.strip() for x in fp.readlines()] # Contains the list of files
Expand Down Expand Up @@ -140,26 +148,31 @@ def create_intra_file_split(file_list: str, split_list_files: tuple, split_facto
:param split_list_files: training_list.txt and validation_list.txt and so on...
:param split_factor: can be a float number or a list of splits e.g [0.2, 0.3]
'''
assert isinstance(split_list_files, (list, tuple)), "split_list_files should be passed as a tuple or list"
if not isinstance(split_list_files, (list, tuple)):
raise TypeError("split_list_files should be passed as a tuple or list")
number_of_splits = len(split_list_files)
split_factors = []
if type(split_factor) == float:
assert split_factor < 1.0, "split_factor should be less than 1"
if not (0.0 < split_factor < 1.0):
raise ValueError("split_factor must be in the range (0.0, 1.0)")
# The default split factor is the fraction for training set.
split_factors.append(split_factor)
# The remainder of the set will be equally split between val or val/test
remainder = 1 - split_factor

elif isinstance(split_factor, (list, tuple)):
assert sum(split_factor) <= 1, "The Sum of split factors should be <=1"
assert len(split_factor) <= len(split_list_files), "The number of elements in split factors should be less than/equal to number of split names"
if sum(split_factor) > 1:
raise ValueError("The Sum of split factors should be <=1")
if len(split_factor) > len(split_list_files):
raise ValueError("The number of elements in split factors should be less than/equal to number of split names")
split_factors.extend(split_factor)
remainder = 1 - sum(split_factor)

if number_of_splits > len(split_factor):
remainder_fraction = remainder / (number_of_splits - len(split_factor))
[split_factors.append(remainder_fraction) for _ in range(number_of_splits - len(split_factor))]
assert len(split_factor) == len(split_list_files), f"Number of split files: {len(split_list_files)} should be same as length of split factors: {len(split_factor)}"
if number_of_splits > len(split_factors):
remainder_fraction = remainder / (number_of_splits - len(split_factors))
[split_factors.append(remainder_fraction) for _ in range(number_of_splits - len(split_factors))]
if len(split_factors) != len(split_list_files):
raise ValueError(f"Number of split files: {len(split_list_files)} should be same as length of split factors: {len(split_factors)}")

with open(file_list) as fp:
# list_of_files = [os.path.join(os.path.dirname(os.path.dirname(file_list)), data_dir, x.strip()) for x in fp.readlines()] # Contains the list of files
Expand Down Expand Up @@ -363,7 +376,8 @@ def get_color_palette(num_classes):
if len(colors_list) < 256:
colors_list += [(255,255,255)] * (256-len(colors_list))
#
assert len(colors_list) == 256, f'incorrect length for color palette {len(colors_list)}'
if len(colors_list) != 256:
raise ValueError(f'incorrect length for color palette {len(colors_list)}')
return colors_list


Expand Down Expand Up @@ -492,7 +506,7 @@ def dataset_split(dataset, split_factor, split_names, random_seed=1):
dataset_splits[split_name]['annotations'].extend(annotations)
image_count_split[split_name] += 1
#
print('dataset split sizes', image_count_split)
logger.info('dataset split sizes %s', image_count_split)
return dataset_splits


Expand Down
163 changes: 163 additions & 0 deletions tinyml-modelmaker/tinyml_modelmaker/ai_modules/protocols.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#################################################################################
# Copyright (c) 2023-2024, Texas Instruments
# All Rights Reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#################################################################################

"""Protocol definitions for tinyml-modelmaker component interfaces.

These protocols document the implicit contracts that ModelRunner, ModelTraining,
ModelCompilation, and DatasetHandling implementations must satisfy. They use
structural subtyping (typing.Protocol) so existing classes conform automatically
without inheriting from them.

Usage with static type checkers (mypy / pyright)::

from tinyml_modelmaker.ai_modules.protocols import Trainer

def start_training(trainer: Trainer) -> None:
trainer.clear()
trainer.run()

Runtime checks are also supported via ``@runtime_checkable``::

isinstance(my_training_obj, Trainer) # True if it has the right methods
"""

from __future__ import annotations

from typing import Any, Protocol, runtime_checkable

from ..utils.config_dict import ConfigDict


# ---------------------------------------------------------------------------
# Base protocol shared by all pipeline components
# ---------------------------------------------------------------------------

@runtime_checkable
class LifecycleComponent(Protocol):
"""Base protocol for pipeline components.

Every component in the tinyml-modelmaker pipeline follows the same
lifecycle: ``init_params()`` -> ``__init__()`` -> ``clear()`` ->
``run()`` -> ``get_params()``. This protocol captures the subset
of that lifecycle that is common to *all* component types.

Note: All concrete implementations also store a ``params: ConfigDict``
instance attribute. It is omitted here so that ``@runtime_checkable``
``issubclass()`` checks work (Python disallows non-method members in
runtime-checkable protocol ``issubclass()`` calls). Static type
checkers enforce the attribute via the child protocols' ``__init__``
signatures.
"""

@classmethod
def init_params(cls, *args: Any, **kwargs: Any) -> ConfigDict: ...

def clear(self) -> None: ...

def get_params(self) -> ConfigDict: ...


# ---------------------------------------------------------------------------
# Dataset handling
# ---------------------------------------------------------------------------

@runtime_checkable
class DatasetHandler(LifecycleComponent, Protocol):
"""Protocol for dataset handling components.

Concrete implementation: ``common.datasets.DatasetHandling``
"""

def __init__(self, *args: Any, quit_event: Any = None, **kwargs: Any) -> None: ...

def run(self) -> None: ...


# ---------------------------------------------------------------------------
# Model training
# ---------------------------------------------------------------------------

@runtime_checkable
class Trainer(LifecycleComponent, Protocol):
"""Protocol for model training components.

Concrete implementations:
- ``timeseries.training.tinyml_tinyverse.timeseries_classification.ModelTraining``
- ``timeseries.training.tinyml_tinyverse.timeseries_regression.ModelTraining``
- ``timeseries.training.tinyml_tinyverse.timeseries_anomalydetection.ModelTraining``
- ``timeseries.training.tinyml_tinyverse.timeseries_forecasting.ModelTraining``
- ``vision.training.tinyml_tinyverse.image_classification.ModelTraining``
"""

def __init__(self, *args: Any, quit_event: Any = None, **kwargs: Any) -> None: ...

def run(self, **kwargs: Any) -> None: ...

def stop(self) -> None: ...


# ---------------------------------------------------------------------------
# Model compilation
# ---------------------------------------------------------------------------

@runtime_checkable
class Compiler(LifecycleComponent, Protocol):
"""Protocol for model compilation components.

Concrete implementation: ``common.compilation.tinyml_benchmark.ModelCompilation``
"""

def __init__(self, *args: Any, quit_event: Any = None, **kwargs: Any) -> None: ...

def run(self, **kwargs: Any) -> int: ...


# ---------------------------------------------------------------------------
# Top-level model runner
# ---------------------------------------------------------------------------

@runtime_checkable
class Runner(LifecycleComponent, Protocol):
"""Protocol for the top-level model runner.

Concrete implementations:
- ``timeseries.runner.ModelRunner``
- ``vision.runner.ModelRunner``
"""

def __init__(self, *args: Any, verbose: bool = True, **kwargs: Any) -> None: ...

def prepare(self) -> str: ...

def run(self) -> ConfigDict: ...

def write_status_file(self) -> str: ...

def package_trained_model(self, input_files: list, compressed_file_name: str) -> int: ...
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,11 @@ def get_default_data_dir_for_task(task_category):
str: 'classes' for classification/anomaly tasks, 'files' for regression/forecasting
"""
if task_category in [TASK_CATEGORY_TS_CLASSIFICATION, TASK_CATEGORY_TS_ANOMALYDETECTION]:
return 'classes'
return DATA_DIR_CLASSES
elif task_category in [TASK_CATEGORY_TS_REGRESSION, TASK_CATEGORY_TS_FORECASTING]:
return 'files'
return DATA_DIR_FILES
else:
return 'classes' # Safe fallback
return DATA_DIR_CLASSES # Safe fallback


# target_device
Expand Down Expand Up @@ -242,6 +242,13 @@ def get_default_data_dir_for_task(task_category):
TARGET_DEVICE_TYPE_MCU
]

# training backend
TRAINING_BACKEND_TINYML_TINYVERSE = 'tinyml_tinyverse'

# data directory names
DATA_DIR_CLASSES = 'classes'
DATA_DIR_FILES = 'files'

# training_device
TRAINING_DEVICE_CPU = 'cpu'
TRAINING_DEVICE_CUDA = 'cuda'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,16 +227,20 @@ def get_model_descriptions(params):


def get_model_description(model_name):
assert model_name, 'model_name must be specified for get_model_description().' \
'if model_name is not known, use the method get_model_descriptions() that returns supported models.'
if not model_name:
raise ValueError(
'model_name must be specified for get_model_description(). '
'If model_name is not known, use get_model_descriptions() that returns supported models.')
model_description = training.get_model_description(model_name)
return model_description


def set_model_description(params, model_description):
assert model_description is not None, f'could not find pretrained model for {params.training.model_name}'
assert params.common.task_type == model_description['common']['task_type'], \
f'task_type: {params.common.task_type} does not match the pretrained model'
if model_description is None:
raise ValueError(f'could not find pretrained model for {params.training.model_name}')
if params.common.task_type != model_description['common']['task_type']:
raise ValueError(
f'task_type: {params.common.task_type} does not match the pretrained model')
# get pretrained model checkpoint and other details
params.update(model_description)
return params
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def init_params(*args, **kwargs):
optimizer='sgd',
weight_decay=1e-4,
lr_scheduler='cosineannealinglr',
training_device='cuda', # 'cpu', 'cuda'
training_device=constants.TRAINING_DEVICE_CUDA,
num_gpus=1, # 0,1
distributed=True,
training_master_port=29500,
Expand Down
Loading
Loading