Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initialize BaseOnlineStrategy #880

Merged
merged 4 commits into from
Jan 21, 2022
Merged
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
180 changes: 180 additions & 0 deletions avalanche/training/strategies/base_online_strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
from typing import Optional, Sequence, List, Union

import copy
import torch
from torch.nn import Module, CrossEntropyLoss
from torch.optim import Optimizer
import warnings

from avalanche.training.plugins.evaluation import default_logger
from avalanche.training.plugins import StrategyPlugin, EvaluationPlugin
from avalanche.training.strategies.base_strategy import BaseStrategy
from avalanche.benchmarks.scenarios import Experience
from avalanche.benchmarks.utils.avalanche_dataset import AvalancheSubset
from avalanche.models import DynamicModule


class BaseOnlineStrategy(BaseStrategy):
def __init__(self, model: Module, optimizer: Optimizer,
criterion=CrossEntropyLoss(), num_passes: int = 1,
train_mb_size: int = 1,
eval_mb_size: int = None, device=None,
plugins: Optional[List[StrategyPlugin]] = None,
evaluator: EvaluationPlugin = default_logger, eval_every=-1):
super().__init__(
model, optimizer, criterion,
train_mb_size=train_mb_size, train_epochs=1,
eval_mb_size=eval_mb_size, device=device, plugins=plugins,
evaluator=evaluator, eval_every=eval_every)

AntonioCarta marked this conversation as resolved.
Show resolved Hide resolved
self.num_passes = num_passes

warnings.warn(
"This is an unstable experimental strategy."
"Some plugins may not work properly."
)

def create_sub_experience_list(self, experience):
""" Creates a list of sub-experiences from an experience.
It returns a list of experiences, where each experience is
a subset of the original experience.

:param experience: single Experience.

:return: list of Experience.
"""

# Shuffle the indices
indices = torch.randperm(len(experience.dataset))
num_sub_exps = len(indices) // self.train_mb_size

sub_experience_list = []
for subexp_id in range(num_sub_exps):
subexp_indices = indices[subexp_id * self.train_mb_size:
(subexp_id + 1) * self.train_mb_size]
sub_experience = copy.copy(experience)
subexp_ds = AvalancheSubset(sub_experience.dataset,
indices=subexp_indices)
sub_experience.dataset = subexp_ds
sub_experience_list.append(sub_experience)

return sub_experience_list

def train(self, experiences: Union[Experience, Sequence[Experience]],
eval_streams: Optional[Sequence[Union[Experience,
Sequence[
Experience]]]] = None,
**kwargs):
""" Training loop. if experiences is a single element trains on it.
If it is a sequence, trains the model on each experience in order.
This is different from joint training on the entire stream.
It returns a dictionary with last recorded value for each metric.

:param experiences: single Experience or sequence.
:param eval_streams: list of streams for evaluation.
If None: use training experiences for evaluation.
Use [] if you do not want to evaluate during training.

:return: dictionary containing last recorded value for
each metric name.
"""
self.is_training = True
self._stop_training = False

self.model.train()
self.model.to(self.device)

# Normalize training and eval data.
if not isinstance(experiences, Sequence):
experiences = [experiences]
if eval_streams is None:
eval_streams = [experiences]
self._eval_streams = eval_streams

self.num_sub_exps = len(experiences[0].dataset) // self.train_mb_size
self._before_training(**kwargs)

# Keep the (full) experience in self.full_experience
# for model adaptation
for self.full_experience in experiences:
sub_experience_list = self.create_sub_experience_list(
self.full_experience
)

# Train for each sub-experience
for i, sub_experience in enumerate(sub_experience_list):
self.experience = sub_experience
is_first_sub_exp = i == 0
is_last_sub_exp = i == len(sub_experience_list) - 1
self.train_exp(self.experience, eval_streams,
is_first_sub_exp=is_first_sub_exp,
is_last_sub_exp=is_last_sub_exp,
**kwargs)

self._after_training(**kwargs)

res = self.evaluator.get_last_metrics()
return res

def train_exp(self, experience: Experience, eval_streams=None,
is_first_sub_exp=False, is_last_sub_exp=False,
**kwargs):
""" Training loop over a single Experience object.

:param experience: CL experience information.
:param eval_streams: list of streams for evaluation.
If None: use the training experience for evaluation.
Use [] if you do not want to evaluate during training.
:param is_first_sub_exp: whether the current sub-experience
is the first sub-experience.
:param is_last_sub_exp: whether the current sub-experience
is the last sub-experience.
:param kwargs: custom arguments.
"""
self.experience = experience
self.model.train()

if eval_streams is None:
eval_streams = [experience]
for i, exp in enumerate(eval_streams):
if not isinstance(exp, Sequence):
eval_streams[i] = [exp]

# Data Adaptation (e.g. add new samples/data augmentation)
self._before_train_dataset_adaptation(**kwargs)
self.train_dataset_adaptation(**kwargs)
self._after_train_dataset_adaptation(**kwargs)
self.make_train_dataloader(**kwargs)

# Model Adaptation (e.g. freeze/add new units) in the
# first sub-experience
if is_first_sub_exp:
self.model = self.model_adaptation()
self.make_optimizer()
self._before_training_exp(**kwargs)
self._before_training_epoch(**kwargs)

# if self._stop_training: # Early stopping
# self._stop_training = False
# break

for self.n_pass in range(self.num_passes):
self.training_epoch(**kwargs)

# if is_last_sub_exp:
self._after_training_epoch(**kwargs)
self._after_training_exp(**kwargs)

def model_adaptation(self, model=None):
"""Adapts the model to the data from the current
(full) experience.

Calls the :class:`~avalanche.models.DynamicModule`s adaptation.
"""
if model is None:
model = self.model

for module in model.modules():
if isinstance(module, DynamicModule):
module.adaptation(self.full_experience.dataset)
return model.to(self.device)
70 changes: 70 additions & 0 deletions avalanche/training/strategies/strategy_wrappers_online.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
################################################################################
# Copyright (c) 2021 ContinualAI. #
# Copyrights licensed under the MIT License. #
# See the accompanying LICENSE file for terms. #
# #
# Date: 01-12-2020 #
# Author(s): Antonio Carta, Andrea Cossu, Hamed Hemati #
# E-mail: contact@continualai.org #
# Website: avalanche.continualai.org #
################################################################################
from typing import Optional, Sequence, List, Union

from torch.nn import Module, CrossEntropyLoss
from torch.optim import Optimizer

from avalanche.training.plugins.evaluation import default_logger
from avalanche.training.plugins import StrategyPlugin, EvaluationPlugin
from avalanche.training.strategies.base_online_strategy import \
BaseOnlineStrategy


class OnlineNaive(BaseOnlineStrategy):
""" Naive finetuning.

The simplest (and least effective) Continual Learning strategy. Naive just
incrementally fine tunes a single model without employing any method
to contrast the catastrophic forgetting of previous knowledge.
This strategy does not use task identities.

Naive is easy to set up and its results are commonly used to show the worst
performing baseline.
"""

def __init__(self, model: Module, optimizer: Optimizer,
criterion=CrossEntropyLoss(), num_passes: int = 1,
train_mb_size: int = 1,
eval_mb_size: int = None, device=None,
plugins: Optional[List[StrategyPlugin]] = None,
evaluator: EvaluationPlugin = default_logger,
eval_every=-1):
"""
Creates an instance of the Naive strategy.

:param model: The model.
:param optimizer: The optimizer to use.
:param criterion: The loss criterion to use.
:param num_passes: The number of passes for each sub-experience.
Defaults to 1.
:param train_mb_size: The train minibatch size. Defaults to 1.
:param eval_mb_size: The eval minibatch size. Defaults to 1.
:param device: The device to use. Defaults to None (cpu).
:param plugins: Plugins to be added. Defaults to None.
:param evaluator: (optional) instance of EvaluationPlugin for logging
and metric computations.
:param eval_every: the frequency of the calls to `eval` inside the
training loop. -1 disables the evaluation. 0 means `eval` is called
only at the end of the learning experience. Values >0 mean that
`eval` is called every `eval_every` epochs and at the end of the
learning experience.
"""
super().__init__(
model, optimizer, criterion, num_passes=num_passes,
train_mb_size=train_mb_size,
eval_mb_size=eval_mb_size, device=device, plugins=plugins,
evaluator=evaluator, eval_every=eval_every)


__all__ = [
'OnlineNaive'
]
107 changes: 107 additions & 0 deletions examples/online_naive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
################################################################################
# Copyright (c) 2021 ContinualAI. #
# Copyrights licensed under the MIT License. #
# See the accompanying LICENSE file for terms. #
# #
# Date: 12-10-2020 #
# Author(s): Vincenzo Lomonaco, Hamed Hemati #
# E-mail: contact@continualai.org #
# Website: avalanche.continualai.org #
################################################################################

"""
This is a simple example on how to use the Replay strategy.
"""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

from os.path import expanduser

import argparse
import torch
from torch.nn import CrossEntropyLoss
from torchvision import transforms
from torchvision.datasets import MNIST
from torchvision.transforms import ToTensor, RandomCrop
import torch.optim.lr_scheduler
from avalanche.benchmarks import nc_benchmark
from avalanche.models import SimpleMLP
from avalanche.training.strategies.strategy_wrappers_online import OnlineNaive
from avalanche.training.plugins import ReplayPlugin
from avalanche.training.storage_policy import ReservoirSamplingBuffer
from avalanche.evaluation.metrics import forgetting_metrics, \
accuracy_metrics, loss_metrics
from avalanche.logging import InteractiveLogger
from avalanche.training.plugins import EvaluationPlugin


def main(args):
# --- CONFIG
device = torch.device(f"cuda:{args.cuda}"
if torch.cuda.is_available() and
args.cuda >= 0 else "cpu")
n_batches = 5
# ---------

# --- TRANSFORMATIONS
train_transform = transforms.Compose([
RandomCrop(28, padding=4),
ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
test_transform = transforms.Compose([
ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
# ---------

# --- SCENARIO CREATION
mnist_train = MNIST(root=expanduser("~") + "/.avalanche/data/mnist/",
train=True, download=True, transform=train_transform)
mnist_test = MNIST(root=expanduser("~") + "/.avalanche/data/mnist/",
train=False, download=True, transform=test_transform)
scenario = nc_benchmark(
mnist_train, mnist_test, n_batches, task_labels=False, seed=1234)
# ---------

# MODEL CREATION
model = SimpleMLP(num_classes=scenario.n_classes)

# choose some metrics and evaluation method
interactive_logger = InteractiveLogger()

eval_plugin = EvaluationPlugin(
accuracy_metrics(
minibatch=True, epoch=True, experience=True, stream=True),
loss_metrics(minibatch=True, epoch=True, experience=True, stream=True),
forgetting_metrics(experience=True),
loggers=[interactive_logger])

# CREATE THE STRATEGY INSTANCE (ONLINE-NAIVE)
cl_strategy = OnlineNaive(model, torch.optim.Adam(model.parameters(),
lr=0.001),
CrossEntropyLoss(), num_passes=1,
train_mb_size=1, eval_mb_size=32,
device=device,
evaluator=eval_plugin)

# TRAINING LOOP
print('Starting experiment...')
results = []
for experience in scenario.train_stream:
print("Start of experience ", experience.current_experience)
cl_strategy.train(experience)
print('Training completed')

print('Computing accuracy on the whole test set')
results.append(cl_strategy.eval(scenario.test_stream))


if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--cuda', type=int, default=0,
help='Select zero-indexed cuda device. -1 to use CPU.')
args = parser.parse_args()
main(args)