Skip to content

Commit 876140c

Browse files
committed
fix DynamicOptimizer initialization
1 parent 447f168 commit 876140c

File tree

3 files changed

+127
-13
lines changed

3 files changed

+127
-13
lines changed

avalanche/models/dynamic_optimizers.py

+26-8
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,19 @@ class DynamicOptimizer(Adaptable):
6767
agent.pre_adapt(experience)
6868
"""
6969

70-
def __init__(self, optim):
70+
def __init__(self, optim, model, reset_state=False, verbose=False):
7171
self.optim = optim
72+
self.reset_state = reset_state
73+
self.verbose = verbose
74+
75+
# initialize param-id
76+
self._optimized_param_id = update_optimizer(
77+
self.optim,
78+
dict(model.named_parameters()),
79+
None,
80+
reset_state=True,
81+
verbose=self.verbose,
82+
)
7283

7384
def zero_grad(self):
7485
self.optim.zero_grad()
@@ -78,10 +89,12 @@ def step(self):
7889

7990
def pre_adapt(self, agent: Agent, exp: CLExperience):
8091
"""Adapt the optimizer before training on the current experience."""
81-
update_optimizer(
92+
self._optimized_param_id = update_optimizer(
8293
self.optim,
83-
new_params=dict(agent.model.named_parameters()),
84-
optimized_params=dict(agent.model.named_parameters()),
94+
dict(agent.model.named_parameters()),
95+
self._optimized_param_id,
96+
reset_state=self.reset_state,
97+
verbose=self.verbose,
8598
)
8699

87100

@@ -329,16 +342,22 @@ def update_optimizer(
329342
330343
Newly added parameters are added by default to parameter group 0
331344
332-
:param new_params: Dict (name, param) of new parameters
345+
WARNING: the first call to `update_optimizer` must be done before
346+
calling the model's adaptation.
347+
348+
:param optimizer: the Optimizer object.
349+
:param new_params: Dict (name, param) of new parameters.
333350
:param optimized_params: Dict (name, param) of
334-
currently optimized parameters
351+
currently optimized parameters. In most use cases, it will be `None in
352+
the first call and the return value of the last `update_optimizer` call
353+
for the subsequent calls.
335354
:param reset_state: Whether to reset the optimizer's state (i.e momentum).
336355
Defaults to False.
337356
:param remove_params: Whether to remove parameters that were in the optimizer
338357
but are not found in new parameters. For safety reasons,
339358
defaults to False.
340359
:param verbose: If True, prints information about inferred
341-
parameter groups for new params
360+
parameter groups for new params.
342361
343362
:return: Dict (name, param) of optimized parameters
344363
"""
@@ -381,7 +400,6 @@ def update_optimizer(
381400
new_p = new_params[key]
382401
group = param_structure[key].single_group
383402
optimizer.param_groups[group]["params"].append(new_p)
384-
optimized_params[key] = new_p
385403
optimizer.state[new_p] = {}
386404

387405
if reset_state:

tests/models/test_dynamic_optimizers.py

+98-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import unittest
22

3+
from torch.nn import CrossEntropyLoss
34
from torch.optim import SGD
45
from torch.utils.data import DataLoader
56

67
from avalanche.core import Agent
7-
from avalanche.models import SimpleMLP, as_multitask
8+
from avalanche.models import SimpleMLP, as_multitask, IncrementalClassifier, MTSimpleMLP
9+
from avalanche.models.dynamic_modules import avalanche_model_adaptation
810
from avalanche.models.dynamic_optimizers import DynamicOptimizer
911
from avalanche.training import MaskedCrossEntropy
1012
from tests.unit_tests_utils import get_fast_benchmark
@@ -17,7 +19,7 @@ def test_dynamic_optimizer(self):
1719
agent.loss = MaskedCrossEntropy()
1820
agent.model = as_multitask(SimpleMLP(input_size=6), "classifier")
1921
opt = SGD(agent.model.parameters(), lr=0.001)
20-
agent.opt = DynamicOptimizer(opt)
22+
agent.opt = DynamicOptimizer(opt, agent.model, verbose=False)
2123

2224
for exp in bm.train_stream:
2325
agent.model.train()
@@ -32,3 +34,97 @@ def test_dynamic_optimizer(self):
3234
l.backward()
3335
agent.opt.step()
3436
agent.post_adapt(exp)
37+
38+
def init_scenario(self, multi_task=False):
39+
if multi_task:
40+
model = MTSimpleMLP(input_size=6, hidden_size=10)
41+
else:
42+
model = SimpleMLP(input_size=6, hidden_size=10)
43+
model.classifier = IncrementalClassifier(10, 1)
44+
criterion = CrossEntropyLoss()
45+
benchmark = get_fast_benchmark(use_task_labels=multi_task)
46+
return model, criterion, benchmark
47+
48+
def _is_param_in_optimizer(self, param, optimizer):
49+
for group in optimizer.param_groups:
50+
for curr_p in group["params"]:
51+
if hash(curr_p) == hash(param):
52+
return True
53+
return False
54+
55+
def _is_param_in_optimizer_group(self, param, optimizer):
56+
for group_idx, group in enumerate(optimizer.param_groups):
57+
for curr_p in group["params"]:
58+
if hash(curr_p) == hash(param):
59+
return group_idx
60+
return None
61+
62+
def test_optimizer_groups_clf_til(self):
63+
"""
64+
Tests the automatic assignation of new
65+
MultiHead parameters to the optimizer
66+
"""
67+
model, criterion, benchmark = self.init_scenario(multi_task=True)
68+
69+
g1 = []
70+
g2 = []
71+
for n, p in model.named_parameters():
72+
if "classifier" in n:
73+
g1.append(p)
74+
else:
75+
g2.append(p)
76+
77+
agent = Agent()
78+
agent.model = model
79+
optimizer = SGD([{"params": g1, "lr": 0.1}, {"params": g2, "lr": 0.05}])
80+
agent.optimizer = DynamicOptimizer(optimizer, model=model, verbose=False)
81+
82+
for experience in benchmark.train_stream:
83+
avalanche_model_adaptation(model, experience)
84+
agent.optimizer.pre_adapt(agent, experience)
85+
86+
for n, p in model.named_parameters():
87+
assert self._is_param_in_optimizer(p, agent.optimizer.optim)
88+
if "classifier" in n:
89+
self.assertEqual(
90+
self._is_param_in_optimizer_group(p, agent.optimizer.optim), 0
91+
)
92+
else:
93+
self.assertEqual(
94+
self._is_param_in_optimizer_group(p, agent.optimizer.optim), 1
95+
)
96+
97+
def test_optimizer_groups_clf_cil(self):
98+
"""
99+
Tests the automatic assignation of new
100+
IncrementalClassifier parameters to the optimizer
101+
"""
102+
model, criterion, benchmark = self.init_scenario(multi_task=False)
103+
104+
g1 = []
105+
g2 = []
106+
for n, p in model.named_parameters():
107+
if "classifier" in n:
108+
g1.append(p)
109+
else:
110+
g2.append(p)
111+
112+
agent = Agent()
113+
agent.model = model
114+
optimizer = SGD([{"params": g1, "lr": 0.1}, {"params": g2, "lr": 0.05}])
115+
agent.optimizer = DynamicOptimizer(optimizer, model)
116+
117+
for experience in benchmark.train_stream:
118+
avalanche_model_adaptation(model, experience)
119+
agent.optimizer.pre_adapt(agent, experience)
120+
121+
for n, p in model.named_parameters():
122+
assert self._is_param_in_optimizer(p, agent.optimizer.optim)
123+
if "classifier" in n:
124+
self.assertEqual(
125+
self._is_param_in_optimizer_group(p, agent.optimizer.optim), 0
126+
)
127+
else:
128+
self.assertEqual(
129+
self._is_param_in_optimizer_group(p, agent.optimizer.optim), 1
130+
)

tests/models/test_models.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -369,10 +369,10 @@ def test_recursive_adaptation(self):
369369
sizes = {}
370370
for t, exp in enumerate(benchmark.train_stream):
371371
sizes[t] = np.max(exp.classes_in_this_experience) + 1
372-
model1.pre_adapt(exp)
372+
model1.pre_adapt(None, exp)
373373
# Second adaptation should not change anything
374374
for t, exp in enumerate(benchmark.train_stream):
375-
model1.pre_adapt(exp)
375+
model1.pre_adapt(None, exp)
376376
for t, s in sizes.items():
377377
self.assertEqual(s, model1.classifiers[str(t)].classifier.out_features)
378378

@@ -385,7 +385,7 @@ def test_recursive_loop(self):
385385
model2.layer2 = model1
386386

387387
benchmark = get_fast_benchmark(use_task_labels=True, shuffle=True)
388-
model1.pre_adapt(benchmark.train_stream[0])
388+
model1.pre_adapt(None, benchmark.train_stream[0])
389389

390390
def test_multi_head_classifier_masking(self):
391391
benchmark = get_fast_benchmark(use_task_labels=True, shuffle=True)

0 commit comments

Comments
 (0)