sparsepixels 0.3.1__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,199 @@
1
+ Metadata-Version: 2.4
2
+ Name: sparsepixels
3
+ Version: 0.3.3
4
+ Summary: Efficient convolution for sparse data on FPGAs
5
+ Home-page: https://github.com/hftsoi/sparse-pixels
6
+ Author: Ho Fung Tsoi
7
+ Author-email: ho.fung.tsoi@cern.ch
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: tensorflow
15
+ Requires-Dist: keras>=3.0
16
+ Requires-Dist: HGQ2>=0.1.8
17
+ Requires-Dist: quantizers<2
18
+ Requires-Dist: matplotlib
19
+ Dynamic: license-file
20
+
21
+ <p align="center">
22
+ <img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/logo.png" width="300" />
23
+ </p>
24
+
25
+ <p align="center">
26
+ <img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/sparsepixels.png" width="900"/>
27
+ </p>
28
+
29
+ <p align="center">
30
+ <img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/cnn_standard.gif" width="400" />
31
+ <img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/cnn_sparse.gif" width="400" />
32
+ </p>
33
+
34
+ # SparsePixels: Efficient convolution for sparse data on FPGAs
35
+
36
+ [![Docs](https://img.shields.io/badge/docs-site-informational?color=blue&style=flat-square)](https://hftsoi.github.io/sparse-pixels)
37
+ [![arXiv](https://img.shields.io/badge/arXiv-2512.06208-b31b1b.svg?style=flat-square)](https://arxiv.org/abs/2512.06208)
38
+ [![PyPI - Version](https://img.shields.io/pypi/v/sparsepixels?color=orange&style=flat-square)](https://pypi.org/project/sparsepixels)
39
+
40
+ SparsePixels is a Keras 3 library to build, train, and deploy sparse convolutional neural networks on FPGAs. In many detectors, especially in high-energy physics experiments, the images are almost empty: only a handful of pixels carry a signal (the hits), yet a standard CNN still spends compute on every pixel. A sparse CNN convolves only over the active pixels, so its cost scales with the number of hits rather than the image size, which is what makes low-latency, real-time inference (e.g. in a trigger) feasible on an FPGA. This library builds quantization-aware (with [HGQ2](https://github.com/calad0i/HGQ2) as a backend) sparse CNNs in which the pixel budget and the activity threshold can be learned from data, with a hardware-aware penalty that drives the budget toward the fewest pixels the task tolerates. Trained models convert to FPGA firmware through the [hls4ml](https://github.com/fastmachinelearning/hls4ml) integration, with control over the parallelization of the sparse layers so user can trade latency against resource usage. This training library is designed to mirror our HLS design with bit-exact accuracy.
41
+
42
+ ## Installation
43
+
44
+ With Python >= 3.10:
45
+
46
+ ```
47
+ pip install sparsepixels
48
+ ```
49
+
50
+ This includes all necessary backends (tensorflow, keras, HGQ2).
51
+
52
+ ## Getting Started
53
+
54
+ First, study the data to pick a threshold and an initial pixel budget `n`: how many pixels stay
55
+ active as the threshold rises, and what a candidate `(n, threshold)` maskes out pixels on a few example images.
56
+
57
+ ```python
58
+ from sparsepixels.utils import active_pixels_vs_threshold, plot_reduced_examples
59
+
60
+ active_pixels_vs_threshold(x_train)
61
+ plot_reduced_examples(x_train, n=20, threshold=0.1, n_examples=3)
62
+ ```
63
+
64
+ Build an example sparse CNN within HGQ2 quantization scopes. Set generous initial bit-widths at the
65
+ scope level: sparse signals are small (and get diluted further by pooling), so the low HGQ2 defaults
66
+ (4-bit weights, 2 fractional bits on activations) can quantize them, or entire low-magnitude
67
+ kernels, to exact zeros at initialization and block training. Starting wide (e.g. `b0=8` for
68
+ weights, `f0=8` for activations) keeps everything alive, and the EBOPS regularizer trims the
69
+ bit-widths back down during training. `InputReduce` keeps the first `n` active pixels (based on the
70
+ first channel above `threshold`) in a row-major order.
71
+ By default `n` and `threshold` are trainable hyperparameters: `beta_n` controls how aggressively `n` is driven smaller for a lower hardware footprint, and `beta_maskedE` penalizes over-masking pixel intensity.
72
+
73
+ ```python
74
+ import keras
75
+ from hgq.layers import QDense
76
+ from hgq.config import QuantizerConfigScope, LayerConfigScope
77
+ from sparsepixels.layers import InputReduce, QConv2DSparse, AveragePooling2DSparse, MaxPooling2DSparse
78
+
79
+ with (
80
+ QuantizerConfigScope(place='all', default_q_type='kbi', overflow_mode='SAT_SYM', b0=8, i0=0),
81
+ QuantizerConfigScope(place='datalane', default_q_type='kif', overflow_mode='WRAP', i0=4, f0=8),
82
+ LayerConfigScope(enable_ebops=True, enable_iq=True, beta0=1e-5),
83
+ ):
84
+ x_in = keras.Input(shape=(28, 28, 1), name='x_in')
85
+
86
+ x, keep_mask = InputReduce(
87
+ n=30, # initial pixel budget
88
+ threshold=0.1, # initial activity threshold
89
+ beta_n=5e-3, # higher -> drives the pixel budget n smaller
90
+ beta_maskedE=1.0, # higher -> prevents over-masking
91
+ learn_n=True, # trainable pixel budget
92
+ learn_threshold=True, # trainable threshold
93
+ name='input_reduce',
94
+ )(x_in)
95
+ x = QConv2DSparse(filters=3, kernel_size=3, name='conv1', padding='same', strides=1,
96
+ activation='relu')([x, keep_mask])
97
+ x, keep_mask = AveragePooling2DSparse(2, name='pool1')([x, keep_mask])
98
+
99
+ x = keras.layers.Flatten(name='flatten')(x)
100
+ x = QDense(10, name='dense1', activation='relu')(x)
101
+ x = keras.layers.Activation('softmax', name='softmax')(x)
102
+
103
+ model = keras.Model(x_in, x)
104
+ model.compile(optimizer=keras.optimizers.Adam(1e-3),
105
+ loss='categorical_crossentropy', metrics=['accuracy'])
106
+ ```
107
+
108
+ Train the model with `SparseTrainingMonitor`. It records the loss breakdown, the learned
109
+ budget/threshold, the EBOPS and the masked-intensity penalty each epoch, and it corrects the EBOPS
110
+ (a proxy for the quantized hardware cost) to the sparse compute automatically.
111
+
112
+ ```python
113
+ from sparsepixels.utils import SparseTrainingMonitor
114
+
115
+ early_stop = keras.callbacks.EarlyStopping(monitor='val_accuracy', mode='max',
116
+ patience=20, restore_best_weights=True)
117
+ history = model.fit(x_train, y_train, validation_data=(x_val, y_val),
118
+ epochs=100, batch_size=128,
119
+ callbacks=[early_stop, SparseTrainingMonitor()])
120
+ ```
121
+
122
+ After training, plot the diagnostics from the monitoring tool. The final pixel budget and threshold values are stored in `InputReduce`: `layer.n_max_pixels` and `layer.threshold` (hls4ml converter will auto-parse these from the model).
123
+
124
+ ```python
125
+ from sparsepixels.utils import plot_history, print_quantization, plot_quantization
126
+
127
+ plot_history(history, early_stopping=early_stop) # loss breakdown, budget, threshold, EBOPS
128
+ print_quantization(model) # per-layer bit-width distribution and EBOPS
129
+ plot_quantization(model)
130
+
131
+ ir = model.get_layer('input_reduce')
132
+ print(f"n_max_pixels={ir.n_max_pixels}, threshold={ir.threshold:.3f}")
133
+ ```
134
+
135
+ ## Converting a trained model to HLS with hls4ml
136
+
137
+ > **Note:** `sparsepixels` support is part of the official [hls4ml](https://github.com/fastmachinelearning/hls4ml) repo ([PR #1468](https://github.com/fastmachinelearning/hls4ml/pull/1468), merged). It is not in a tagged release yet, so install hls4ml from the main branch:
138
+ >
139
+ > ```bash
140
+ > pip install "git+https://github.com/fastmachinelearning/hls4ml.git"
141
+ > ```
142
+
143
+ Once installed, pull a config from the trained model, optionally set the per-layer parallelization
144
+ knobs, and convert:
145
+
146
+ ```python
147
+ import hls4ml
148
+
149
+ hls_config = hls4ml.utils.config_from_keras_model(model, granularity='name')
150
+ hls_config.setdefault('Model', {})['PipelineStyle'] = 'dataflow' # "#pragma HLS DATAFLOW"
151
+
152
+ n_max_pixels = model.get_layer('input_reduce').n_max_pixels
153
+
154
+ # input reduce: 'tree' (default, lowest latency) or 'stream' (sequential, fewer resources)
155
+ hls_config['LayerName']['input_reduce']['Variant'] = 'tree'
156
+ # conv: active pixels in parallel (<= n_max_pixels), and filters in parallel (<= that conv's filters)
157
+ hls_config['LayerName']['conv1']['PixelParallelFactor'] = n_max_pixels
158
+ hls_config['LayerName']['conv1']['FiltParallelFactor'] = 3
159
+ # pool: active pixels in parallel, and channels in parallel
160
+ hls_config['LayerName']['pool1']['PixelParallelFactor'] = n_max_pixels
161
+ hls_config['LayerName']['pool1']['ChanParallelFactor'] = 3
162
+ # flatten: scatter positions in parallel (<= out_height * out_width; here 28*28 pools to 14*14)
163
+ hls_config['LayerName']['flatten']['ParallelFactor'] = 14 * 14
164
+
165
+ hls_model = hls4ml.converters.convert_from_keras_model(
166
+ model,
167
+ hls_config=hls_config,
168
+ output_dir='hls_proj/my_sparse_cnn',
169
+ backend='Vitis',
170
+ io_type='io_parallel',
171
+ )
172
+ hls_model.write()
173
+ hls_model.compile()
174
+ y_hls = hls_model.predict(x_test)
175
+ ```
176
+
177
+ ## Documentation
178
+
179
+ See [here](https://hftsoi.github.io/sparse-pixels/) for documentation with walk-through guidance.
180
+
181
+ ## Citation
182
+
183
+ If you find this useful in your research, please consider citing:
184
+
185
+ ```
186
+ @article{Tsoi:2025nvg,
187
+ author = "Tsoi, Ho Fung and Rankin, Dylan and Loncar, Vladimir and Harris, Philip",
188
+ title = "{SparsePixels: efficient convolution for sparse data on FPGAs}",
189
+ eprint = "2512.06208",
190
+ archivePrefix = "arXiv",
191
+ primaryClass = "cs.AR",
192
+ doi = "10.1088/2632-2153/ae9fb5",
193
+ journal = "Mach. Learn. Sci. Tech.",
194
+ volume = "7",
195
+ number = "5",
196
+ pages = "055025",
197
+ year = "2026"
198
+ }
199
+ ```
@@ -0,0 +1,179 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/logo.png" width="300" />
3
+ </p>
4
+
5
+ <p align="center">
6
+ <img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/sparsepixels.png" width="900"/>
7
+ </p>
8
+
9
+ <p align="center">
10
+ <img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/cnn_standard.gif" width="400" />
11
+ <img src="https://raw.githubusercontent.com/hftsoi/sparse-pixels/main/docs/figs/cnn_sparse.gif" width="400" />
12
+ </p>
13
+
14
+ # SparsePixels: Efficient convolution for sparse data on FPGAs
15
+
16
+ [![Docs](https://img.shields.io/badge/docs-site-informational?color=blue&style=flat-square)](https://hftsoi.github.io/sparse-pixels)
17
+ [![arXiv](https://img.shields.io/badge/arXiv-2512.06208-b31b1b.svg?style=flat-square)](https://arxiv.org/abs/2512.06208)
18
+ [![PyPI - Version](https://img.shields.io/pypi/v/sparsepixels?color=orange&style=flat-square)](https://pypi.org/project/sparsepixels)
19
+
20
+ SparsePixels is a Keras 3 library to build, train, and deploy sparse convolutional neural networks on FPGAs. In many detectors, especially in high-energy physics experiments, the images are almost empty: only a handful of pixels carry a signal (the hits), yet a standard CNN still spends compute on every pixel. A sparse CNN convolves only over the active pixels, so its cost scales with the number of hits rather than the image size, which is what makes low-latency, real-time inference (e.g. in a trigger) feasible on an FPGA. This library builds quantization-aware (with [HGQ2](https://github.com/calad0i/HGQ2) as a backend) sparse CNNs in which the pixel budget and the activity threshold can be learned from data, with a hardware-aware penalty that drives the budget toward the fewest pixels the task tolerates. Trained models convert to FPGA firmware through the [hls4ml](https://github.com/fastmachinelearning/hls4ml) integration, with control over the parallelization of the sparse layers so user can trade latency against resource usage. This training library is designed to mirror our HLS design with bit-exact accuracy.
21
+
22
+ ## Installation
23
+
24
+ With Python >= 3.10:
25
+
26
+ ```
27
+ pip install sparsepixels
28
+ ```
29
+
30
+ This includes all necessary backends (tensorflow, keras, HGQ2).
31
+
32
+ ## Getting Started
33
+
34
+ First, study the data to pick a threshold and an initial pixel budget `n`: how many pixels stay
35
+ active as the threshold rises, and what a candidate `(n, threshold)` maskes out pixels on a few example images.
36
+
37
+ ```python
38
+ from sparsepixels.utils import active_pixels_vs_threshold, plot_reduced_examples
39
+
40
+ active_pixels_vs_threshold(x_train)
41
+ plot_reduced_examples(x_train, n=20, threshold=0.1, n_examples=3)
42
+ ```
43
+
44
+ Build an example sparse CNN within HGQ2 quantization scopes. Set generous initial bit-widths at the
45
+ scope level: sparse signals are small (and get diluted further by pooling), so the low HGQ2 defaults
46
+ (4-bit weights, 2 fractional bits on activations) can quantize them, or entire low-magnitude
47
+ kernels, to exact zeros at initialization and block training. Starting wide (e.g. `b0=8` for
48
+ weights, `f0=8` for activations) keeps everything alive, and the EBOPS regularizer trims the
49
+ bit-widths back down during training. `InputReduce` keeps the first `n` active pixels (based on the
50
+ first channel above `threshold`) in a row-major order.
51
+ By default `n` and `threshold` are trainable hyperparameters: `beta_n` controls how aggressively `n` is driven smaller for a lower hardware footprint, and `beta_maskedE` penalizes over-masking pixel intensity.
52
+
53
+ ```python
54
+ import keras
55
+ from hgq.layers import QDense
56
+ from hgq.config import QuantizerConfigScope, LayerConfigScope
57
+ from sparsepixels.layers import InputReduce, QConv2DSparse, AveragePooling2DSparse, MaxPooling2DSparse
58
+
59
+ with (
60
+ QuantizerConfigScope(place='all', default_q_type='kbi', overflow_mode='SAT_SYM', b0=8, i0=0),
61
+ QuantizerConfigScope(place='datalane', default_q_type='kif', overflow_mode='WRAP', i0=4, f0=8),
62
+ LayerConfigScope(enable_ebops=True, enable_iq=True, beta0=1e-5),
63
+ ):
64
+ x_in = keras.Input(shape=(28, 28, 1), name='x_in')
65
+
66
+ x, keep_mask = InputReduce(
67
+ n=30, # initial pixel budget
68
+ threshold=0.1, # initial activity threshold
69
+ beta_n=5e-3, # higher -> drives the pixel budget n smaller
70
+ beta_maskedE=1.0, # higher -> prevents over-masking
71
+ learn_n=True, # trainable pixel budget
72
+ learn_threshold=True, # trainable threshold
73
+ name='input_reduce',
74
+ )(x_in)
75
+ x = QConv2DSparse(filters=3, kernel_size=3, name='conv1', padding='same', strides=1,
76
+ activation='relu')([x, keep_mask])
77
+ x, keep_mask = AveragePooling2DSparse(2, name='pool1')([x, keep_mask])
78
+
79
+ x = keras.layers.Flatten(name='flatten')(x)
80
+ x = QDense(10, name='dense1', activation='relu')(x)
81
+ x = keras.layers.Activation('softmax', name='softmax')(x)
82
+
83
+ model = keras.Model(x_in, x)
84
+ model.compile(optimizer=keras.optimizers.Adam(1e-3),
85
+ loss='categorical_crossentropy', metrics=['accuracy'])
86
+ ```
87
+
88
+ Train the model with `SparseTrainingMonitor`. It records the loss breakdown, the learned
89
+ budget/threshold, the EBOPS and the masked-intensity penalty each epoch, and it corrects the EBOPS
90
+ (a proxy for the quantized hardware cost) to the sparse compute automatically.
91
+
92
+ ```python
93
+ from sparsepixels.utils import SparseTrainingMonitor
94
+
95
+ early_stop = keras.callbacks.EarlyStopping(monitor='val_accuracy', mode='max',
96
+ patience=20, restore_best_weights=True)
97
+ history = model.fit(x_train, y_train, validation_data=(x_val, y_val),
98
+ epochs=100, batch_size=128,
99
+ callbacks=[early_stop, SparseTrainingMonitor()])
100
+ ```
101
+
102
+ After training, plot the diagnostics from the monitoring tool. The final pixel budget and threshold values are stored in `InputReduce`: `layer.n_max_pixels` and `layer.threshold` (hls4ml converter will auto-parse these from the model).
103
+
104
+ ```python
105
+ from sparsepixels.utils import plot_history, print_quantization, plot_quantization
106
+
107
+ plot_history(history, early_stopping=early_stop) # loss breakdown, budget, threshold, EBOPS
108
+ print_quantization(model) # per-layer bit-width distribution and EBOPS
109
+ plot_quantization(model)
110
+
111
+ ir = model.get_layer('input_reduce')
112
+ print(f"n_max_pixels={ir.n_max_pixels}, threshold={ir.threshold:.3f}")
113
+ ```
114
+
115
+ ## Converting a trained model to HLS with hls4ml
116
+
117
+ > **Note:** `sparsepixels` support is part of the official [hls4ml](https://github.com/fastmachinelearning/hls4ml) repo ([PR #1468](https://github.com/fastmachinelearning/hls4ml/pull/1468), merged). It is not in a tagged release yet, so install hls4ml from the main branch:
118
+ >
119
+ > ```bash
120
+ > pip install "git+https://github.com/fastmachinelearning/hls4ml.git"
121
+ > ```
122
+
123
+ Once installed, pull a config from the trained model, optionally set the per-layer parallelization
124
+ knobs, and convert:
125
+
126
+ ```python
127
+ import hls4ml
128
+
129
+ hls_config = hls4ml.utils.config_from_keras_model(model, granularity='name')
130
+ hls_config.setdefault('Model', {})['PipelineStyle'] = 'dataflow' # "#pragma HLS DATAFLOW"
131
+
132
+ n_max_pixels = model.get_layer('input_reduce').n_max_pixels
133
+
134
+ # input reduce: 'tree' (default, lowest latency) or 'stream' (sequential, fewer resources)
135
+ hls_config['LayerName']['input_reduce']['Variant'] = 'tree'
136
+ # conv: active pixels in parallel (<= n_max_pixels), and filters in parallel (<= that conv's filters)
137
+ hls_config['LayerName']['conv1']['PixelParallelFactor'] = n_max_pixels
138
+ hls_config['LayerName']['conv1']['FiltParallelFactor'] = 3
139
+ # pool: active pixels in parallel, and channels in parallel
140
+ hls_config['LayerName']['pool1']['PixelParallelFactor'] = n_max_pixels
141
+ hls_config['LayerName']['pool1']['ChanParallelFactor'] = 3
142
+ # flatten: scatter positions in parallel (<= out_height * out_width; here 28*28 pools to 14*14)
143
+ hls_config['LayerName']['flatten']['ParallelFactor'] = 14 * 14
144
+
145
+ hls_model = hls4ml.converters.convert_from_keras_model(
146
+ model,
147
+ hls_config=hls_config,
148
+ output_dir='hls_proj/my_sparse_cnn',
149
+ backend='Vitis',
150
+ io_type='io_parallel',
151
+ )
152
+ hls_model.write()
153
+ hls_model.compile()
154
+ y_hls = hls_model.predict(x_test)
155
+ ```
156
+
157
+ ## Documentation
158
+
159
+ See [here](https://hftsoi.github.io/sparse-pixels/) for documentation with walk-through guidance.
160
+
161
+ ## Citation
162
+
163
+ If you find this useful in your research, please consider citing:
164
+
165
+ ```
166
+ @article{Tsoi:2025nvg,
167
+ author = "Tsoi, Ho Fung and Rankin, Dylan and Loncar, Vladimir and Harris, Philip",
168
+ title = "{SparsePixels: efficient convolution for sparse data on FPGAs}",
169
+ eprint = "2512.06208",
170
+ archivePrefix = "arXiv",
171
+ primaryClass = "cs.AR",
172
+ doi = "10.1088/2632-2153/ae9fb5",
173
+ journal = "Mach. Learn. Sci. Tech.",
174
+ volume = "7",
175
+ number = "5",
176
+ pages = "055025",
177
+ year = "2026"
178
+ }
179
+ ```
@@ -1149,6 +1149,103 @@ def preview_patches_with_pooling(
1149
1149
  plt.show()
1150
1150
 
1151
1151
 
1152
+ def plot_layer_outputs(model, x_input, sample_idx=0, layer_names=None,
1153
+ cmap='viridis', figsize_per_ch=3):
1154
+ """Plot channel-wise feature maps for intermediate layers of a sparse or dense CNN.
1155
+
1156
+ Parameters
1157
+ ----------
1158
+ model : keras.Model
1159
+ The model (sparse or dense). Sparse models may have layers with
1160
+ tuple outputs (features, mask).
1161
+ x_input : np.ndarray
1162
+ Full input dataset (batch, H, W, C).
1163
+ sample_idx : int
1164
+ Index of the sample in *x_input* to visualise.
1165
+ layer_names : list[str] or None
1166
+ Layer names to plot. If *None*, auto-discovers layers whose names
1167
+ start with common prefixes (x_in, input_reduce, conv, pool, dense).
1168
+ cmap : str
1169
+ Matplotlib colormap name.
1170
+ figsize_per_ch : float
1171
+ Width of each channel subplot in inches.
1172
+ """
1173
+ import keras
1174
+
1175
+ if layer_names is None:
1176
+ prefixes = ('x_in', 'input_reduce', 'conv', 'pool', 'dense')
1177
+ layer_names = [l.name for l in model.layers
1178
+ if any(l.name.startswith(p) for p in prefixes)]
1179
+
1180
+ plot_tensors = []
1181
+ plot_names = []
1182
+ for name in layer_names:
1183
+ layer = model.get_layer(name)
1184
+ output = layer.output
1185
+ if isinstance(output, (list, tuple)):
1186
+ plot_tensors.append(output[0])
1187
+ plot_names.append(f'{name} (features)')
1188
+ plot_tensors.append(output[1])
1189
+ plot_names.append(f'{name} (mask)')
1190
+ else:
1191
+ plot_tensors.append(output)
1192
+ plot_names.append(name)
1193
+
1194
+ probe = keras.models.Model(inputs=model.input, outputs=plot_tensors)
1195
+ preds = probe.predict(x_input[sample_idx:sample_idx + 1], verbose=0)
1196
+
1197
+ if not isinstance(preds, list):
1198
+ preds = [preds]
1199
+
1200
+ i = 0
1201
+ while i < len(plot_names):
1202
+ name = plot_names[i]
1203
+
1204
+ if '(features)' in name and i + 1 < len(plot_names) and '(mask)' in plot_names[i + 1]:
1205
+ arr = preds[i][0]
1206
+ if arr.ndim == 3:
1207
+ h, w, c = arr.shape
1208
+ fig, axes = plt.subplots(1, c, figsize=(c * figsize_per_ch, figsize_per_ch))
1209
+ fig.suptitle(name.replace(' (features)', ''), fontsize=13)
1210
+ if c == 1:
1211
+ axes = [axes]
1212
+ for ch in range(c):
1213
+ axes[ch].imshow(arr[..., ch], cmap=cmap)
1214
+ axes[ch].set_title(f'ch{ch}')
1215
+ plt.tight_layout()
1216
+ plt.show()
1217
+ i += 2
1218
+ continue
1219
+
1220
+ arr = preds[i][0]
1221
+ if arr.ndim == 1:
1222
+ fig, ax = plt.subplots(1, 1, figsize=(max(len(arr) * 0.3, 4), figsize_per_ch))
1223
+ fig.suptitle(name, fontsize=13)
1224
+ ax.bar(range(len(arr)), arr)
1225
+ ax.set_xlabel('unit')
1226
+ plt.tight_layout()
1227
+ plt.show()
1228
+ elif arr.ndim == 2:
1229
+ fig, ax = plt.subplots(1, 1, figsize=(figsize_per_ch, figsize_per_ch))
1230
+ fig.suptitle(name, fontsize=13)
1231
+ ax.imshow(arr, cmap=cmap)
1232
+ ax.set_title('ch0')
1233
+ plt.tight_layout()
1234
+ plt.show()
1235
+ elif arr.ndim == 3:
1236
+ h, w, c = arr.shape
1237
+ fig, axes = plt.subplots(1, c, figsize=(c * figsize_per_ch, figsize_per_ch))
1238
+ fig.suptitle(name, fontsize=13)
1239
+ if c == 1:
1240
+ axes = [axes]
1241
+ for ch in range(c):
1242
+ axes[ch].imshow(arr[..., ch], cmap=cmap)
1243
+ axes[ch].set_title(f'ch{ch}')
1244
+ plt.tight_layout()
1245
+ plt.show()
1246
+ i += 1
1247
+
1248
+
1152
1249
  def _fmt_bits(bits_tensor):
1153
1250
  b = np.array(bits_tensor).flatten()
1154
1251
  if b.size == 1:
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = sparsepixels
3
- version = 0.3.1
3
+ version = 0.3.3
4
4
  description = Efficient convolution for sparse data on FPGAs
5
5
  author = Ho Fung Tsoi
6
6
  author_email = ho.fung.tsoi@cern.ch
@@ -20,6 +20,7 @@ install_requires =
20
20
  tensorflow
21
21
  keras>=3.0
22
22
  HGQ2>=0.1.8
23
+ quantizers<2
23
24
  matplotlib
24
25
  include_package_data = True
25
26
 
@@ -32,13 +32,18 @@ class InputReduce(keras.layers.Layer):
32
32
  plain, non-learnable selection). The selection is always exact -- the learnable versions only
33
33
  shape the gradient, so the layer behaves identically at inference and stays deployable. When n is
34
34
  learned, a penalty of weight beta_n nudges it smaller, trading a little accuracy for lower FPGA
35
- latency and resources; it starts at n and stays within [1, 4*n]. After training, read the values
36
- to deploy from the n_max_pixels and threshold properties.
35
+ latency and resources; it starts at n and can range over [1, H*W]. An optional penalty of weight
36
+ beta_maskedE discourages masking pixel intensity, giving the threshold (and n) a restoring force so a
37
+ trainable threshold settles near the noise floor instead of over-masking signal. After training,
38
+ read the values to deploy from the n_max_pixels and threshold properties.
37
39
 
38
40
  Args:
39
41
  n: initial pixel budget, and the fixed budget when learn_n is False.
40
42
  threshold: initial activity threshold on the first channel, fixed when learn_threshold is False.
41
43
  beta_n: weight of the budget penalty added to the loss (0 disables it).
44
+ beta_maskedE: weight of the masked-intensity penalty (0 disables it). Penalizes the fraction of pixel
45
+ intensity that gets masked, so masking bright (signal) pixels is costly while masking dim
46
+ (noise) pixels is cheap -- an adaptive, non-vanishing restoring force against over-masking.
42
47
  learn_n: make the pixel budget trainable.
43
48
  learn_threshold: make the threshold trainable.
44
49
  tau_threshold: softness of the threshold surrogate used to obtain gradients.
@@ -49,7 +54,8 @@ class InputReduce(keras.layers.Layer):
49
54
  self,
50
55
  n=30,
51
56
  threshold=0.0,
52
- beta_n=1e-5,
57
+ beta_n=5e-3,
58
+ beta_maskedE=1.0,
53
59
  learn_n=True,
54
60
  learn_threshold=True,
55
61
  tau_threshold=0.05,
@@ -60,6 +66,7 @@ class InputReduce(keras.layers.Layer):
60
66
  self.n_init = int(n)
61
67
  self.threshold_init = float(threshold)
62
68
  self.beta_n = float(beta_n)
69
+ self.beta_maskedE = float(beta_maskedE)
63
70
  self.learn_n = learn_n
64
71
  self.learn_threshold = learn_threshold
65
72
  self.tau_threshold = float(tau_threshold)
@@ -76,13 +83,24 @@ class InputReduce(keras.layers.Layer):
76
83
  )
77
84
  if self.learn_n:
78
85
  # Parametrize the budget as a fraction of its initial value (n = n_init * n_frac) so it
79
- # moves at a useful rate under Adam; clip to [1, 4x] so it can shrink or modestly grow.
86
+ # moves at a useful rate under Adam; clip to [1, H*W] so it can shrink to a single pixel
87
+ # or grow up to keeping every pixel.
88
+ if input_shape[1] and input_shape[2]:
89
+ hi = (input_shape[1] * input_shape[2]) / self.n_init
90
+ else:
91
+ hi = 4.0
80
92
  self.n_frac = self.add_weight(
81
93
  name="n_frac",
82
94
  shape=(),
83
95
  initializer=keras.initializers.Constant(1.0),
84
96
  trainable=True,
85
- constraint=_ClipToRange(1.0 / self.n_init, 4.0),
97
+ constraint=_ClipToRange(1.0 / self.n_init, max(1.0, hi)),
98
+ )
99
+ if self.beta_maskedE > 0:
100
+ # Running masked-intensity fraction, tracked (not trained) so SparseTrainingMonitor can
101
+ # report the penalty; refreshed each call.
102
+ self._masked_intensity = self.add_weight(
103
+ name="masked_intensity", shape=(), initializer="zeros", trainable=False
86
104
  )
87
105
  super().build(input_shape)
88
106
 
@@ -108,6 +126,15 @@ class InputReduce(keras.layers.Layer):
108
126
  rank_soft = ops.cumsum(active_soft, axis=1)
109
127
  keep_soft = active_soft * ops.sigmoid((n - rank_soft) / self.tau_n)
110
128
  keep_flat = keep_soft + ops.stop_gradient(keep_hard - keep_soft)
129
+
130
+ if self.beta_maskedE > 0:
131
+ # Fraction of total pixel intensity that gets masked, added straight to the loss (not
132
+ # through the classifier) so its gradient does not vanish for the masked pixels.
133
+ # Masking bright pixels costs more than masking dim ones, so this pushes the threshold
134
+ # back down (and n up) before it eats into signal -- an adaptive anti-over-masking term.
135
+ masked_frac = ops.mean(ops.sum(score * (1.0 - keep_soft), axis=1) / (ops.sum(score, axis=1) + 1e-6))
136
+ self.add_loss(self.beta_maskedE * masked_frac)
137
+ self._masked_intensity.assign(masked_frac)
111
138
  else:
112
139
  keep_flat = keep_hard
113
140
 
@@ -140,6 +167,7 @@ class InputReduce(keras.layers.Layer):
140
167
  "n": self.n_init,
141
168
  "threshold": self.threshold_init,
142
169
  "beta_n": self.beta_n,
170
+ "beta_maskedE": self.beta_maskedE,
143
171
  "learn_n": self.learn_n,
144
172
  "learn_threshold": self.learn_threshold,
145
173
  "tau_threshold": self.tau_threshold,