Skip to content

Commit 3c5ae22

Browse files
committed
Add utility to compute sparsity in each network type.
1 parent 346f724 commit 3c5ae22

File tree

12 files changed

+133
-6
lines changed

12 files changed

+133
-6
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ There are 3 different types of Forward-Forward-based Neural Networks implemented
5555
- [**FF+C**](#ffc) - [example usage](./examples/ff_c_mnist.py)
5656
- [**FFRNN**](#ffrnn) - [example usage](./examples/ff_rnn_mnist.py)
5757

58+
> [!NOTE]
59+
> There's also a [backpropagation example](./examples/bp_mnist.py) implemented as a base case.
60+
5861
### FFNet
5962

6063
The basic example of a Neural Network based on the Forward-Forward Algorithm.
@@ -92,6 +95,22 @@ from the previous layer, but also backward weights from the next layer.
9295
These networks have to be trained with multiple frames per batch, thus
9396
requiring even more time for both, training and inference.
9497

98+
## Utilities
99+
100+
We aim to implement many small utilities for training, validating, testing, developing, debugging and analysing FF networks.
101+
For each type of network, we have build small network-specifc train-test suites that allow you to quickly train and test
102+
a FF network with some specific [DataProcessor](./src/fflib/utils/data/dataprocessor.py).
103+
For each new dataset, you just need to define a data processor.
104+
The suite will take in the network and the data processor and train the network in the way it is supposed to.
105+
There are currently implemented 4 different testing suites:
106+
- [FF Suite](./src/fflib/utils/ff_suite.py)
107+
- [FF+C Suite](./src/fflib/utils/ffc_suite.py)
108+
- [FFRNN Suite](./src/fflib/utils/ffrnn_suite.py)
109+
- [BP Suite](./src/fflib/utils/bp_suite.py)
110+
111+
Furthermore, each network implements some analysis functions, currently only a `sparsity` function is implemented.
112+
Sparsity can be computed in two ways `HOYER` [3] and `ENTROPY_BASED`.
113+
95114
## Contributions
96115

97116
We really appreciate contributions from the community!
@@ -151,3 +170,4 @@ Here are a few guidelines to following while contributing on the library:
151170

152171
- [**[1]**](https://arxiv.org/abs/2212.13345) - Hinton, G. (2022). The Forward-Forward Algorithm: Some preliminary investigations.
153172
- [**[2]**](https://pytorch.org/) - PyTorch.
173+
- [**[3]**](https://dl.acm.org/doi/10.5555/1005332.1044709) - Hoyer, P. (2004). Non-negative Matrix Factorization with Sparseness Constraints.

src/fflib/enums.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from enum import Enum
2+
3+
4+
class SparsityType(Enum):
5+
HOYER = 1
6+
ENTROPY_BASED = 2

src/fflib/interfaces/iff_recurrent_layer.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import torch
22

33
from torch.nn import Module
4+
from fflib.enums import SparsityType
45
from abc import ABC, abstractmethod
5-
from typing import Callable, Tuple, List
6+
from typing import Callable, Tuple, List, Dict
67

78

89
class IFFRecurrentLayer(ABC, Module):
@@ -52,3 +53,7 @@ def run_train(
5253
@abstractmethod
5354
def strip_down(self) -> None:
5455
pass
56+
57+
@abstractmethod
58+
def sparsity(self, type: SparsityType) -> Dict[str, float]:
59+
pass

src/fflib/nn/ff_linear.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
from torch.nn import Linear, Module, ReLU
33
from torch.optim import Adam, Optimizer
44

5+
from fflib.enums import SparsityType
6+
from fflib.utils.maths import ComputeSparsity
7+
58
from typing import Callable, Tuple, Any, cast
69

710

@@ -131,3 +134,7 @@ def run_train(self, x_pos: torch.Tensor, x_neg: torch.Tensor) -> None:
131134

132135
def strip_down(self) -> None:
133136
self.opt = None
137+
138+
def sparsity(self, type: SparsityType) -> torch.Tensor:
139+
"""Computes the sparsity of the weight's matrix."""
140+
return ComputeSparsity(torch.flatten(self.weight), type)

src/fflib/nn/ff_net.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from torch.nn import Module
44
from fflib.interfaces.iff import IFF
55
from fflib.nn.ff_linear import FFLinear
6+
from fflib.enums import SparsityType
67
from typing import List, Any, Dict, Callable
78

89

@@ -68,3 +69,8 @@ def strip_down(self) -> None:
6869
for layer in self.layers:
6970
layer.strip_down()
7071
delattr(self, "hooks")
72+
73+
def sparsity(self, type: SparsityType) -> Dict[str, float]:
74+
return {
75+
f"layer_{i}": float(layer.sparsity(type).item()) for i, layer in enumerate(self.layers)
76+
}

src/fflib/nn/ff_recurrent_layer.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
from torch.nn import Module, ReLU
55
from torch.optim import Adam, Optimizer
66
from fflib.interfaces.iff_recurrent_layer import IFFRecurrentLayer
7-
from typing import Callable, List, Tuple, cast, Any
7+
from fflib.enums import SparsityType
8+
from fflib.utils.maths import ComputeSparsity
9+
from typing import Callable, List, Tuple, Dict, cast, Any
810

911

1012
class FFRecurrentLayer(IFFRecurrentLayer):
@@ -144,6 +146,17 @@ def run_train(
144146
def strip_down(self) -> None:
145147
self.opt = None
146148

149+
def sparsity(self, type: SparsityType) -> Dict[str, float]:
150+
return {
151+
"fw": float(ComputeSparsity(torch.flatten(self.fw), type).item()),
152+
"bw": float(ComputeSparsity(torch.flatten(self.bw), type).item()),
153+
"fw+bw": float(
154+
ComputeSparsity(
155+
torch.cat((torch.flatten(self.fw), torch.flatten(self.fb))), type
156+
).item()
157+
),
158+
}
159+
147160

148161
class FFRecurrentLayerDummy(IFFRecurrentLayer):
149162
def __init__(self, dimensions: int):
@@ -175,3 +188,6 @@ def run_train(
175188

176189
def strip_down(self) -> None:
177190
pass
191+
192+
def sparsity(self, type: SparsityType) -> Dict[str, float]:
193+
return {}

src/fflib/nn/ff_rnn.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from fflib.nn.ff_recurrent_layer import FFRecurrentLayer, FFRecurrentLayerDummy
66
from fflib.interfaces.iff import IFF
77
from fflib.interfaces.iff_recurrent_layer import IFFRecurrentLayer
8-
from typing import List, Tuple, cast, Any
8+
from fflib.enums import SparsityType
9+
from typing import List, Tuple, Dict, cast, Any
910
from typing_extensions import Self
1011

1112

@@ -231,3 +232,7 @@ def strip_down(self) -> None:
231232
for layer in self.layers:
232233
layer.strip_down()
233234
delattr(self, "hooks")
235+
236+
def sparsity(self, type: SparsityType) -> Dict[str, Dict[str, float]]:
237+
"""Returns a dictionary of dictionaries describing the sparsity levels at each layer."""
238+
return {f"layer_{i}": layer.sparsity(type) for i, layer in enumerate(self.layers)}

src/fflib/nn/ffc.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
from torch.optim import Adam, Optimizer
55
from fflib.interfaces.iff import IFF
66
from fflib.nn.ff_linear import FFLinear
7+
from fflib.enums import SparsityType
8+
from fflib.utils.maths import ComputeSparsity
79

8-
from typing import List, Callable, Any
10+
from typing import List, Dict, Callable, Any
911

1012

1113
class FFC(IFF, Module):
@@ -90,3 +92,12 @@ def strip_down(self) -> None:
9092
layer.strip_down()
9193
self.optimizer = None
9294
delattr(self, "hooks")
95+
96+
def sparsity(self, type: SparsityType) -> Dict[str, float]:
97+
result = {
98+
f"layer_{i}": float(layer.sparsity(type).item()) for i, layer in enumerate(self.layers)
99+
}
100+
result["classifier"] = float(
101+
ComputeSparsity(torch.flatten(self.classifier.weight), type).item()
102+
)
103+
return result

src/fflib/utils/maths.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import torch
2+
3+
from fflib.enums import SparsityType
4+
5+
from math import sqrt, log2
6+
7+
8+
def ComputeSparsity(x: torch.Tensor, type: SparsityType) -> torch.Tensor:
9+
n = x.shape[0]
10+
if type == SparsityType.HOYER:
11+
r = torch.sum(torch.abs(x)) / torch.sqrt(torch.sum(torch.square(x)))
12+
sqn = sqrt(n)
13+
return torch.Tensor((sqn - r) / (sqn - 1))
14+
elif type == SparsityType.ENTROPY_BASED:
15+
t = torch.sum(torch.abs(x))
16+
p = torch.abs(x).div(t) + 1e-8
17+
v = p * torch.log2(p)
18+
h = -torch.sum(v)
19+
return torch.Tensor(1 - (h / log2(n)))

tests/nn/test_ff_linear.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import pytest
21
import torch
32

43
from fflib.nn.ff_linear import FFLinear
4+
from fflib.enums import SparsityType
55

66

77
def test_setup_linear() -> None:
@@ -57,3 +57,16 @@ def test_train_linear_basic() -> None:
5757
# Expect the minimum goodness of the positive data to be bigger than the max goodness of neg.
5858
assert g_pos.min().item() > g_neg.max().item()
5959
assert g_pos.mean().item() > g_neg.mean().item()
60+
61+
62+
def test_sparsity_linear() -> None:
63+
torch.manual_seed(42)
64+
linear = FFLinear(in_features=10, out_features=2, loss_threshold=1, lr=0.02)
65+
hoyer = linear.sparsity(SparsityType.HOYER).item()
66+
entropy = linear.sparsity(SparsityType.ENTROPY_BASED).item()
67+
68+
print(f"Hoyer: {hoyer}")
69+
print(f"Entropy-based: {entropy}")
70+
71+
assert hoyer >= 0 and hoyer <= 1
72+
assert entropy >= 0 and entropy <= 1

0 commit comments

Comments
 (0)