2021-06-02 01:52:41 +00:00
|
|
|
"""Lightning Callbacks."""
|
|
|
|
|
2021-06-04 20:20:32 +00:00
|
|
|
import logging
|
|
|
|
|
2021-06-02 01:52:41 +00:00
|
|
|
import pytorch_lightning as pl
|
|
|
|
import torch
|
2021-06-04 20:20:32 +00:00
|
|
|
|
2021-06-14 18:08:08 +00:00
|
|
|
from ..core.components import Components
|
2021-06-14 18:29:31 +00:00
|
|
|
from ..core.initializers import LiteralCompInitializer
|
2021-06-04 20:20:32 +00:00
|
|
|
from .extras import ConnectionTopology
|
2021-06-02 01:52:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
class PruneLoserPrototypes(pl.Callback):
|
|
|
|
def __init__(self,
|
|
|
|
threshold=0.01,
|
2021-06-02 10:44:34 +00:00
|
|
|
idle_epochs=10,
|
2021-06-02 01:52:41 +00:00
|
|
|
prune_quota_per_epoch=-1,
|
|
|
|
frequency=1,
|
2021-06-04 13:56:46 +00:00
|
|
|
replace=False,
|
2021-06-14 18:19:08 +00:00
|
|
|
prototypes_initializer=None,
|
2021-06-02 01:52:41 +00:00
|
|
|
verbose=False):
|
|
|
|
self.threshold = threshold # minimum win ratio
|
2021-06-02 10:44:34 +00:00
|
|
|
self.idle_epochs = idle_epochs # epochs to wait before pruning
|
2021-06-02 01:52:41 +00:00
|
|
|
self.prune_quota_per_epoch = prune_quota_per_epoch
|
|
|
|
self.frequency = frequency
|
2021-06-04 13:56:46 +00:00
|
|
|
self.replace = replace
|
2021-06-02 01:52:41 +00:00
|
|
|
self.verbose = verbose
|
2021-06-14 18:19:08 +00:00
|
|
|
self.prototypes_initializer = prototypes_initializer
|
2021-06-02 01:52:41 +00:00
|
|
|
|
|
|
|
def on_epoch_end(self, trainer, pl_module):
|
2021-06-02 10:44:34 +00:00
|
|
|
if (trainer.current_epoch + 1) < self.idle_epochs:
|
2021-06-02 01:52:41 +00:00
|
|
|
return None
|
|
|
|
if (trainer.current_epoch + 1) % self.frequency:
|
|
|
|
return None
|
2021-06-04 20:20:32 +00:00
|
|
|
|
2021-06-02 01:52:41 +00:00
|
|
|
ratios = pl_module.prototype_win_ratios.mean(dim=0)
|
|
|
|
to_prune = torch.arange(len(ratios))[ratios < self.threshold]
|
2021-06-04 20:20:32 +00:00
|
|
|
to_prune = to_prune.tolist()
|
|
|
|
prune_labels = pl_module.prototype_labels[to_prune]
|
2021-06-02 01:52:41 +00:00
|
|
|
if self.prune_quota_per_epoch > 0:
|
|
|
|
to_prune = to_prune[:self.prune_quota_per_epoch]
|
2021-06-04 13:56:46 +00:00
|
|
|
prune_labels = prune_labels[:self.prune_quota_per_epoch]
|
2021-06-04 20:20:32 +00:00
|
|
|
|
2021-06-02 01:52:41 +00:00
|
|
|
if len(to_prune) > 0:
|
|
|
|
if self.verbose:
|
|
|
|
print(f"\nPrototype win ratios: {ratios}")
|
2021-06-04 20:20:32 +00:00
|
|
|
print(f"Pruning prototypes at: {to_prune}")
|
2021-06-04 23:23:58 +00:00
|
|
|
print(f"Corresponding labels are: {prune_labels.tolist()}")
|
2021-06-02 01:52:41 +00:00
|
|
|
cur_num_protos = pl_module.num_prototypes
|
|
|
|
pl_module.remove_prototypes(indices=to_prune)
|
2021-06-04 13:56:46 +00:00
|
|
|
if self.replace:
|
|
|
|
labels, counts = torch.unique(prune_labels,
|
|
|
|
sorted=True,
|
|
|
|
return_counts=True)
|
|
|
|
distribution = dict(zip(labels.tolist(), counts.tolist()))
|
2021-06-04 20:20:32 +00:00
|
|
|
if self.verbose:
|
|
|
|
print(f"Re-adding pruned prototypes...")
|
|
|
|
print(f"{distribution=}")
|
2021-06-14 18:19:08 +00:00
|
|
|
pl_module.add_prototypes(
|
|
|
|
distribution=distribution,
|
|
|
|
components_initializer=self.prototypes_initializer)
|
2021-06-02 01:52:41 +00:00
|
|
|
new_num_protos = pl_module.num_prototypes
|
|
|
|
if self.verbose:
|
2021-06-04 13:56:46 +00:00
|
|
|
print(f"`num_prototypes` changed from {cur_num_protos} "
|
2021-06-02 01:52:41 +00:00
|
|
|
f"to {new_num_protos}.")
|
|
|
|
return True
|
2021-06-02 10:44:34 +00:00
|
|
|
|
|
|
|
|
|
|
|
class PrototypeConvergence(pl.Callback):
|
|
|
|
def __init__(self, min_delta=0.01, idle_epochs=10, verbose=False):
|
|
|
|
self.min_delta = min_delta
|
|
|
|
self.idle_epochs = idle_epochs # epochs to wait
|
|
|
|
self.verbose = verbose
|
|
|
|
|
|
|
|
def on_epoch_end(self, trainer, pl_module):
|
|
|
|
if (trainer.current_epoch + 1) < self.idle_epochs:
|
|
|
|
return None
|
|
|
|
if self.verbose:
|
|
|
|
print("Stopping...")
|
|
|
|
# TODO
|
|
|
|
return True
|
2021-06-04 20:20:32 +00:00
|
|
|
|
|
|
|
|
|
|
|
class GNGCallback(pl.Callback):
|
|
|
|
"""GNG Callback.
|
|
|
|
|
|
|
|
Applies growing algorithm based on accumulated error and topology.
|
|
|
|
|
|
|
|
Based on "A Growing Neural Gas Network Learns Topologies" by Bernd Fritzke.
|
|
|
|
|
|
|
|
"""
|
|
|
|
def __init__(self, reduction=0.1, freq=10):
|
|
|
|
self.reduction = reduction
|
|
|
|
self.freq = freq
|
|
|
|
|
|
|
|
def on_epoch_end(self, trainer: pl.Trainer, pl_module):
|
|
|
|
if (trainer.current_epoch + 1) % self.freq == 0:
|
|
|
|
# Get information
|
|
|
|
errors = pl_module.errors
|
|
|
|
topology: ConnectionTopology = pl_module.topology_layer
|
|
|
|
components: Components = pl_module.proto_layer.components
|
|
|
|
|
|
|
|
# Insertion point
|
|
|
|
worst = torch.argmax(errors)
|
|
|
|
|
|
|
|
neighbors = topology.get_neighbors(worst)[0]
|
|
|
|
|
|
|
|
if len(neighbors) == 0:
|
|
|
|
logging.log(level=20, msg="No neighbor-pairs found!")
|
|
|
|
return
|
|
|
|
|
|
|
|
neighbors_errors = errors[neighbors]
|
|
|
|
worst_neighbor = neighbors[torch.argmax(neighbors_errors)]
|
|
|
|
|
|
|
|
# New Prototype
|
|
|
|
new_component = 0.5 * (components[worst] +
|
|
|
|
components[worst_neighbor])
|
|
|
|
|
|
|
|
# Add component
|
|
|
|
pl_module.proto_layer.add_components(
|
2021-06-14 18:29:31 +00:00
|
|
|
initializer=LiteralCompInitializer(new_component.unsqueeze(0)))
|
2021-06-04 20:20:32 +00:00
|
|
|
|
|
|
|
# Adjust Topology
|
|
|
|
topology.add_prototype()
|
|
|
|
topology.add_connection(worst, -1)
|
|
|
|
topology.add_connection(worst_neighbor, -1)
|
|
|
|
topology.remove_connection(worst, worst_neighbor)
|
|
|
|
|
|
|
|
# New errors
|
|
|
|
worst_error = errors[worst].unsqueeze(0)
|
|
|
|
pl_module.errors = torch.cat([pl_module.errors, worst_error])
|
|
|
|
pl_module.errors[worst] = errors[worst] * self.reduction
|
|
|
|
pl_module.errors[
|
|
|
|
worst_neighbor] = errors[worst_neighbor] * self.reduction
|
|
|
|
|
|
|
|
trainer.accelerator_backend.setup_optimizers(trainer)
|