diff --git a/pyproject.toml b/pyproject.toml index b1ef3fd7a835119b9946bcda663f9338216672f6..63190e6f33740abb6cc0ca42c9669bf16aece0e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "MaggotUBA-adapter" -version = "0.7" +version = "0.8" description = "Interface between MaggotUBA and the Nyx tagging UI" authors = ["François Laurent"] license = "MIT" diff --git a/src/maggotuba/models/modules.py b/src/maggotuba/models/modules.py index fbb3b844b6bd4212872ff016343acfded091ed74..9d197a3885a6719b3003ce793b84e5d0e4fa4a2f 100644 --- a/src/maggotuba/models/modules.py +++ b/src/maggotuba/models/modules.py @@ -90,6 +90,9 @@ class MaggotModule(nn.Module): def parameters(self, recurse=True): return self.model.parameters(recurse) + def to(self, device): + self.model.to(device) + """ Initialize a model's weights and bias (if any). @@ -305,6 +308,9 @@ class DeepLinear(nn.Module): torch.save(self.state_dict(), path) check_permissions(path) + def to(self, device): + self.layers.to(device) + class MaggotClassifier(MaggotModule): def __init__(self, path, behavior_labels=[], n_latent_features=None, n_layers=1, cfgfile=None, ptfile="trained_classifier.pt"): @@ -374,6 +380,9 @@ class SupervisedMaggot(nn.Module): def forward(self, x): return self.clf(self.encoder(x)) + def mask_forward(self, x): + return self.clf(self.encoder.mask_forward(x)) + def save(self): enc, clf = self.encoder, self.clf enc.save() @@ -385,6 +394,10 @@ class SupervisedMaggot(nn.Module): self.clf.model # force parameter loading or initialization return super().parameters(self) + def to(self, device): + self.encoder.to(device) + self.clf.to(device) + class MultiscaleSupervisedMaggot(nn.Module): def __init__(self, cfgfilepath, behaviors=[], n_layers=1): super().__init__() @@ -413,3 +426,45 @@ class MultiscaleSupervisedMaggot(nn.Module): self.clf.model # force parameter loading or initialization return super().parameters(self) +""" +Bagging for `SupervisedMaggot`. + +Taggers in the bag are individually trained. +For now the bag itself cannot be trained and is used for prediction only. + +Bags of taggers are stored so that the models directory only contains +subdirectories, each subdirectory specifying an individual tagger. +""" +class MaggotBag(nn.Module): + def __init__(self, paths, behaviors=[], n_layers=1, cls=SupervisedMaggot): + super().__init__() + self.maggots = [cls(path, behaviors, n_layers) for path in paths] + self._lead_maggot = None + + def forward(self, x): + #return torch.cat([encoder.mask_forward(x) for encoder in self.encoders], dim=1) + return self.vote([maggot.mask_forward(x) for maggot in self.maggots]) + + def vote(self, y): + vote, _ = torch.mode(torch.stack(y, dim=len(y[0].shape))) + return vote + + @property + def encoder(self): + return self.maggots[self.lead_maggot].encoder + + @property + def clf(self): + return self.maggots[self.lead_maggot].clf + + @property + def lead_maggot(self): + if self._lead_maggot is None: + len_traj = 0 + for i, maggot in enumerate(self.maggots): + len_traj_ = maggot.encoder.config['len_traj'] + if len_traj < len_traj_: + len_traj = len_traj_ + self._lead_maggot = i + return self._lead_maggot + diff --git a/src/maggotuba/models/predict_model.py b/src/maggotuba/models/predict_model.py index aa320d8546320dc8096d806b8b7bc5aef43a4fc7..c1abd548e048cd4b73d07b6d9c9f2a1f45a15da0 100644 --- a/src/maggotuba/models/predict_model.py +++ b/src/maggotuba/models/predict_model.py @@ -1,6 +1,6 @@ from taggingbackends.data.labels import Labels from taggingbackends.features.skeleton import get_5point_spines -from maggotuba.models.trainers import MaggotTrainer, MultiscaleMaggotTrainer, new_generator +from maggotuba.models.trainers import MaggotTrainer, MultiscaleMaggotTrainer, MaggotBagging, new_generator import numpy as np import logging @@ -27,20 +27,25 @@ def predict_model(backend, **kwargs): assert 0 < len(input_files_and_labels) # load the model model_files = backend.list_model_files() - config_file = [file for file in model_files if file.name.endswith("config.json")] - n_config_files = len(config_file) - if n_config_files == 0: - raise RuntimeError(f"no such tagger found: {backend.model_instance}") - config_file = [file - for file in config_file - if file.name.endswith("clf_config.json") - and file.parent == backend.model_dir()] - assert len(config_file) == 1 - config_file = config_file[-1] - if 2 < n_config_files: - model = MultiscaleMaggotTrainer(config_file) + config_files = [file + for file in model_files + if file.name.endswith('config.json')] + if len(config_files) == 0: + raise RuntimeError(f"no config files found for tagger: {backend.model_instance}") + single_encoder_classifier = len(config_files) == 2 + config_files = [file + for file in config_files + if file.name == 'clf_config.json'] + if len(config_files) == 0: + raise RuntimeError(f"no classifier config files found; is {backend.model_instance} tagger trained?") + elif len(config_files) == 1: + config_file = config_files[0] + if single_encoder_classifier: + model = MaggotTrainer(config_file) + else: + model = MultiscaleMaggotTrainer(config_file) else: - model = MaggotTrainer(config_file) + model = MaggotBagging(config_files) # if len(input_files) == 1: input_files = next(iter(input_files.values())) @@ -133,9 +138,9 @@ def predict_individual_data_files(backend, model, input_files_and_labels): # done = True -def predict_larva_dataset(backend, model, file, subset="validation"): +def predict_larva_dataset(backend, model, file, subset="validation", subsets=(.8, .2, 0)): from taggingbackends.data.dataset import LarvaDataset - dataset = LarvaDataset(file, new_generator()) + dataset = LarvaDataset(file, new_generator(), subsets) return model.predict(dataset, subset) def _zip(xs, ys): diff --git a/src/maggotuba/models/trainers.py b/src/maggotuba/models/trainers.py index 025b95dbbb83af436d92fd9d53f6b450d92c1ab6..aba17748fb6256dcd9c453da168b45d0e0b5a2d7 100644 --- a/src/maggotuba/models/trainers.py +++ b/src/maggotuba/models/trainers.py @@ -3,7 +3,7 @@ import torch import torch.nn as nn from behavior_model.models.neural_nets import device #import behavior_model.data.utils as data_utils -from maggotuba.models.modules import SupervisedMaggot, MultiscaleSupervisedMaggot +from maggotuba.models.modules import SupervisedMaggot, MultiscaleSupervisedMaggot, MaggotBag from taggingbackends.features.skeleton import interpolate """ @@ -231,7 +231,7 @@ class MaggotTrainer: self.model.save() def new_generator(seed=None): - generator = torch.Generator(device) + generator = torch.Generator('cpu') if seed == 'random': return generator if seed is None: seed = 0b11010111001001101001110 return generator.manual_seed(seed) @@ -265,6 +265,14 @@ class MultiscaleMaggotTrainer(MaggotTrainer): return self._default_encoder_config +class MaggotBagging(MaggotTrainer): + def __init__(self, cfgfilepaths, behaviors=[], n_layers=1, + average_body_length=1.0, device=device): + self.model = MaggotBag(cfgfilepaths, behaviors, n_layers) + self.average_body_length = average_body_length # usually set later + self.device = device + + """ Pick the adequate trainer following a rapid inspection of the config file(s). @@ -272,6 +280,8 @@ For now, config files are actually not inspected. However, using this function is highly recommended as more models are introduced with future releases. """ def make_trainer(config_file, *args, **kwargs): + # the type criterion does not fail in the case of unimplemented bagging, + # as config files are listed in a pretrained_models subdirectory. if isinstance(config_file, list): # multiple encoders config_files = config_file model = MultiscaleMaggotTrainer(config_files, *args, **kwargs)