Skip to content

Commit 65a75b5

Browse files
authored
Merge pull request #70 from cdtools-developers/newpoisson
Switch to a more sensible normalization strategy for loss metrics
2 parents 3f028e2 + fcd0b31 commit 65a75b5

15 files changed

Lines changed: 482 additions & 76 deletions

‎docs/source/tutorial.rst‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ Once again, we start with the basic skeleton
196196

197197
.. code-block:: python
198198
199+
from functools import partial
199200
import torch as t
200201
from cdtools.models import CDIModel
201202
from cdtools import tools
@@ -251,7 +252,11 @@ There is no requirement for what the arguments to the initialization function of
251252
self.probe = t.nn.Parameter(probe_guess / self.probe_norm)
252253
self.obj = t.nn.Parameter(obj_guess)
253254
254-
255+
# We register a loss function and an appropriate normalization
256+
self.loss = partial(tools.losses.amplitude_mse, use_sum=True)
257+
self.loss_normalizer = tools.losses.AmplitudeMSENormalizer()
258+
259+
255260
The first thing to notice about this model is that all the fixed, geometric information is stored with the :code:`module.register_buffer()` function. This is what makes it possible to move all the relevant tensors between devices using a single call to :code:`module.to()`, for example. It stores thetensor as an object attribute, but it also registers it so that pytorch is aware that this attribute helps to encode the state of the model.
256261

257262
The supporting information we need is the wavelength of the illumination, the basis of the probe array in real space, and an offset to define the zero point of the translation.
@@ -268,6 +273,8 @@ The Adam optimizer is designed so that the learning rate sets the maximum stepsi
268273

269274
This is important to remember when adding additional error models. Rescaling all the parameters to have a typical amplitude near 1 is the best way to get well-behaved reconstructions.
270275

276+
The final two lines assign a loss function and its associated normalizer. Here we use :code:`amplitude_mse` and its paired :code:`AmplitudeMSENormalizer`. The normalization function :code:`amplitude_mse` computes the the mean squared error between the square roots of the simulated and measured intensities. In this case, we call it with the :code:`use_sum=True` flag, which will actually calculate a sum-square error, which will be normalized afterward to a mean-squared error by the normalizer, :code:`AmplitudeMSENormalizer`. This pattern is used to ensure that the losses from minibatches with different sizes are properly weighted.
277+
271278

272279
Initialization from Dataset
273280
+++++++++++++++++++++++++++
@@ -347,7 +354,7 @@ Here, we take input in the form of an index and a translation. Note that this in
347354

348355
We start by mapping the translation, given in real space, into pixel coordinates. Then, we use an "off-the-shelf" interaction model - :code:`ptycho_2d_round`, which models a standard 2D ptychography interaction, but rounds the translations to the nearest whole pixel (does not attempt subpixel translations).
349356

350-
The next three definitions amount to just choosing an off-the-shelf function to simulate each step in the chain.
357+
The next two definitions amount to just choosing an off-the-shelf function to simulate each step in the chain.
351358

352359
.. code-block:: python
353360
@@ -357,11 +364,8 @@ The next three definitions amount to just choosing an off-the-shelf function to
357364
def measurement(self, wavefields):
358365
return tools.measurements.intensity(wavefields)
359366
360-
def loss(self, sim_data, real_data):
361-
return tools.losses.amplitude_mse(real_data, sim_data)
362-
363367
364-
The forward propagator maps the exit wave to the wave at the surface of the detector, here using a far-field propagator. The measurement maps that exit wave to a measured pixel value, and the loss defines a loss function to attempt to minimize. The loss function we've chosen - the amplitude mean squared error - is the most reliable one, and can also easily be overridden by an end user.
368+
The forward propagator maps the exit wave to the wave at the surface of the detector, here using a far-field propagator. The measurement maps that wavefield to a measured pixel value. The loss function was already assigned in :code:`__init__` as described above.
365369

366370

367371
Plotting

‎examples/near_field_ptycho.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
near_field=True,
2727
propagation_distance=3.65e-3, # 3.65 downstream from focus
2828
units='um', # Set the units for the live plots
29-
obj_view_crop=-35,
29+
obj_view_crop=-35, # Expand the view for the live plots
30+
loss="poisson_nll", # Best option for photon-counting detectors
3031
panel_plot_mode=True, # Set to False to get individual figures
3132
)
3233

‎examples/tutorial_simple_ptycho.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import partial
12
import torch as t
23
from cdtools.models import CDIModel
34
from cdtools import tools
@@ -41,6 +42,10 @@ def __init__(
4142
self.probe = t.nn.Parameter(probe_guess / self.probe_norm)
4243
self.obj = t.nn.Parameter(obj_guess)
4344

45+
# We register a loss function and an appropriate normalization
46+
self.loss = partial(tools.losses.amplitude_mse, use_sum=True)
47+
self.loss_normalizer = tools.losses.AmplitudeMSENormalizer()
48+
4449

4550
@classmethod
4651
def from_dataset(cls, dataset):
@@ -102,9 +107,6 @@ def forward_propagator(self, wavefields):
102107
def measurement(self, wavefields):
103108
return tools.measurements.intensity(wavefields)
104109

105-
def loss(self, real_data, sim_data):
106-
return tools.losses.amplitude_mse(real_data, sim_data)
107-
108110

109111
# This lists all the plots to display on a call to model.inspect()
110112
plot_list = [

‎src/cdtools/models/bragg_2d_ptycho.py‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import partial
12
import torch as t
23
from cdtools.models import CDIModel
34
from cdtools.datasets import Ptycho2DDataset
@@ -76,6 +77,7 @@ def __init__(
7677
propagate_probe=True,
7778
correct_tilt=True,
7879
lens=False,
80+
loss='amplitude mse',
7981
units='um',
8082
dtype=t.float32,
8183
obj_view_crop=0,
@@ -237,7 +239,22 @@ def __init__(
237239
# TODO: probably doesn't support non-float-32 dtypes
238240
self.register_buffer('universal_propagator',
239241
universal_propagator)
240-
242+
243+
# Here we set the appropriate loss function
244+
if (loss.lower().strip() == 'amplitude mse'
245+
or loss.lower().strip() == 'amplitude_mse'):
246+
self.loss = partial(tools.losses.amplitude_mse, use_sum=True)
247+
self.loss_normalizer = tools.losses.AmplitudeMSENormalizer()
248+
elif (loss.lower().strip() == 'poisson nll'
249+
or loss.lower().strip() == 'poisson_nll'):
250+
self.loss = tools.losses.poisson_nll
251+
self.loss_normalizer = tools.losses.SimplePoissonNLLNormalizer()
252+
elif (loss.lower().strip() == 'intensity mse'
253+
or loss.lower().strip() == 'intensity_mse'):
254+
self.loss = partial(tools.losses.intensity_mse, use_sum=True)
255+
self.loss_normalizer = tools.losses.IntensityMSENormalizer()
256+
else:
257+
raise KeyError('Specified loss function not supported')
241258

242259

243260
@classmethod
@@ -257,6 +274,7 @@ def from_dataset(
257274
propagate_probe=True,
258275
correct_tilt=True,
259276
lens=False,
277+
loss='amplitude mse',
260278
obj_padding=200,
261279
obj_view_crop=None,
262280
units='um',
@@ -450,6 +468,7 @@ def from_dataset(
450468
propagate_probe=propagate_probe,
451469
correct_tilt=correct_tilt,
452470
lens=lens,
471+
loss=loss,
453472
obj_view_crop=obj_view_crop,
454473
units=units,
455474
panel_plot_mode=panel_plot_mode,
@@ -534,10 +553,6 @@ def measurement(self, wavefields):
534553
)
535554

536555

537-
def loss(self, sim_data, real_data, mask=None):
538-
return tools.losses.amplitude_mse(real_data, sim_data, mask=mask)
539-
540-
541556
def sim_to_dataset(self, args_list):
542557
# In the future, potentially add more control
543558
# over what metadata is saved (names, etc.)

‎src/cdtools/models/fancy_ptycho.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import partial
12
import torch as t
23
from cdtools.models import CDIModel
34
from cdtools.datasets import Ptycho2DDataset
@@ -219,10 +220,16 @@ def __init__(self,
219220
# Here we set the appropriate loss function
220221
if (loss.lower().strip() == 'amplitude mse'
221222
or loss.lower().strip() == 'amplitude_mse'):
222-
self.loss = tools.losses.amplitude_mse
223+
self.loss = partial(tools.losses.amplitude_mse, use_sum=True)
224+
self.loss_normalizer = tools.losses.AmplitudeMSENormalizer()
223225
elif (loss.lower().strip() == 'poisson nll'
224226
or loss.lower().strip() == 'poisson_nll'):
225227
self.loss = tools.losses.poisson_nll
228+
self.loss_normalizer = tools.losses.SimplePoissonNLLNormalizer()
229+
elif (loss.lower().strip() == 'intensity mse'
230+
or loss.lower().strip() == 'intensity_mse'):
231+
self.loss = partial(tools.losses.intensity_mse, use_sum=True)
232+
self.loss_normalizer = tools.losses.IntensityMSENormalizer()
226233
else:
227234
raise KeyError('Specified loss function not supported')
228235

‎src/cdtools/models/multislice_2d_ptycho.py‎

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import partial
12
import torch as t
23
from cdtools.models import CDIModel
34
from cdtools.datasets import Ptycho2DDataset
@@ -46,6 +47,7 @@ def __init__(self,
4647
fourier_probe=False,
4748
prevent_aliasing=True,
4849
phase_only=False,
50+
loss='amplitude mse',
4951
units='um',
5052
panel_plot_mode=False,
5153
plot_level=1,
@@ -155,9 +157,25 @@ def __init__(self,
155157

156158
self.as_prop = tools.propagators.generate_angular_spectrum_propagator(shape, spacing, self.wavelength, self.dz, self.bandlimit)
157159

160+
# Here we set the appropriate loss function
161+
if (loss.lower().strip() == 'amplitude mse'
162+
or loss.lower().strip() == 'amplitude_mse'):
163+
self.loss = partial(tools.losses.amplitude_mse, use_sum=True)
164+
self.loss_normalizer = tools.losses.AmplitudeMSENormalizer()
165+
elif (loss.lower().strip() == 'poisson nll'
166+
or loss.lower().strip() == 'poisson_nll'):
167+
self.loss = tools.losses.poisson_nll
168+
self.loss_normalizer = tools.losses.SimplePoissonNLLNormalizer()
169+
elif (loss.lower().strip() == 'intensity mse'
170+
or loss.lower().strip() == 'intensity_mse'):
171+
self.loss = partial(tools.losses.intensity_mse, use_sum=True)
172+
self.loss_normalizer = tools.losses.IntensityMSENormalizer()
173+
else:
174+
raise KeyError('Specified loss function not supported')
175+
158176

159177
@classmethod
160-
def from_dataset(cls, dataset, dz, nz, probe_convergence_semiangle, padding=0, n_modes=1, dm_rank=None, translation_scale=1, saturation=None, propagation_distance=None, scattering_mode=None, oversampling=1, auto_center=True, bandlimit=None, replicate_slice=False, subpixel=True, exponentiate_obj=True, units='um', fourier_probe=False, phase_only=False, prevent_aliasing=True, probe_support_radius=None, panel_plot_mode=False, plot_level=1):
178+
def from_dataset(cls, dataset, dz, nz, probe_convergence_semiangle, padding=0, n_modes=1, dm_rank=None, translation_scale=1, saturation=None, propagation_distance=None, scattering_mode=None, oversampling=1, auto_center=True, bandlimit=None, replicate_slice=False, subpixel=True, exponentiate_obj=True, units='um', fourier_probe=False, phase_only=False, prevent_aliasing=True, probe_support_radius=None, panel_plot_mode=False, plot_level=1, loss='amplitude_mse'):
161179

162180
wavelength = dataset.wavelength
163181
det_basis = dataset.detector_geometry['basis']
@@ -301,7 +319,9 @@ def from_dataset(cls, dataset, dz, nz, probe_convergence_semiangle, padding=0, n
301319
phase_only=phase_only,
302320
prevent_aliasing=prevent_aliasing,
303321
panel_plot_mode=panel_plot_mode,
304-
plot_level=plot_level)
322+
plot_level=plot_level,
323+
loss=loss,
324+
)
305325

306326

307327
def interaction(self, index, translations):
@@ -415,11 +435,6 @@ def measurement(self, wavefields):
415435
oversampling=self.oversampling)
416436

417437

418-
def loss(self, sim_data, real_data, mask=None):
419-
return tools.losses.amplitude_mse(real_data, sim_data, mask=mask)
420-
#return tools.losses.poisson_nll(real_data, sim_data, mask=mask,eps=0.5)
421-
422-
423438
def to(self, *args, **kwargs):
424439
super(Multislice2DPtycho, self).to(*args, **kwargs)
425440
self.wavelength = self.wavelength.to(*args,**kwargs)

‎src/cdtools/models/multislice_ptycho.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import partial
12
import torch as t
23
from cdtools.models import CDIModel
34
from cdtools.datasets import Ptycho2DDataset
@@ -164,14 +165,20 @@ def __init__(self,
164165

165166
self.register_buffer('simulate_finite_pixels',
166167
t.as_tensor(simulate_finite_pixels, dtype=bool))
167-
168+
168169
# Here we set the appropriate loss function
169170
if (loss.lower().strip() == 'amplitude mse'
170171
or loss.lower().strip() == 'amplitude_mse'):
171-
self.loss = tools.losses.amplitude_mse
172+
self.loss = partial(tools.losses.amplitude_mse, use_sum=True)
173+
self.loss_normalizer = tools.losses.AmplitudeMSENormalizer()
172174
elif (loss.lower().strip() == 'poisson nll'
173175
or loss.lower().strip() == 'poisson_nll'):
174176
self.loss = tools.losses.poisson_nll
177+
self.loss_normalizer = tools.losses.SimplePoissonNLLNormalizer()
178+
elif (loss.lower().strip() == 'intensity mse'
179+
or loss.lower().strip() == 'intensity_mse'):
180+
self.loss = partial(tools.losses.intensity_mse, use_sum=True)
181+
self.loss_normalizer = tools.losses.IntensityMSENormalizer()
175182
else:
176183
raise KeyError('Specified loss function not supported')
177184

‎src/cdtools/models/rpi.py‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import partial
12
import torch as t
23
from cdtools.models import CDIModel
34
from cdtools import tools
@@ -54,6 +55,7 @@ def __init__(
5455
exponentiate_obj=False,
5556
phase_only=False,
5657
propagation_distance=0,
58+
loss='amplitude mse',
5759
units='um',
5860
dtype=t.float32,
5961
panel_plot_mode=True,
@@ -146,6 +148,22 @@ def __init__(
146148
self.register_buffer('prop_dir',
147149
t.as_tensor([0, 0, 1], dtype=dtype))
148150

151+
# Here we set the appropriate loss function
152+
if (loss.lower().strip() == 'amplitude mse'
153+
or loss.lower().strip() == 'amplitude_mse'):
154+
self.loss = partial(tools.losses.amplitude_mse, use_sum=True)
155+
self.loss_normalizer = tools.losses.AmplitudeMSENormalizer()
156+
elif (loss.lower().strip() == 'poisson nll'
157+
or loss.lower().strip() == 'poisson_nll'):
158+
self.loss = tools.losses.poisson_nll
159+
self.loss_normalizer = tools.losses.SimplePoissonNLLNormalizer()
160+
elif (loss.lower().strip() == 'intensity mse'
161+
or loss.lower().strip() == 'intensity_mse'):
162+
self.loss = partial(tools.losses.intensity_mse, use_sum=True)
163+
self.loss_normalizer = tools.losses.IntensityMSENormalizer()
164+
else:
165+
raise KeyError('Specified loss function not supported')
166+
149167

150168
@classmethod
151169
def from_dataset(
@@ -164,6 +182,7 @@ def from_dataset(
164182
exponentiate_obj=False,
165183
phase_only=False,
166184
probe_threshold=0,
185+
loss='amplitude mse',
167186
dtype=t.float32,
168187
panel_plot_mode=True,
169188
plot_level=1,
@@ -259,7 +278,9 @@ def from_dataset(
259278
phase_only=phase_only,
260279
weight_matrix=weight_matrix,
261280
panel_plot_mode=panel_plot_mode,
262-
plot_level=plot_level)
281+
plot_level=plot_level,
282+
loss=loss,
283+
)
263284

264285
# I don't love this pattern, where I do the "real" obj initialization
265286
# after creating the rpi object. But, I chose this so that I could
@@ -291,6 +312,7 @@ def from_calibration(
291312
dtype=t.float32,
292313
panel_plot_mode=True,
293314
plot_level=1,
315+
loss='amplitude_mse',
294316
):
295317

296318
complex_dtype = (t.ones([1], dtype=dtype) +
@@ -336,6 +358,7 @@ def from_calibration(
336358
phase_only=phase_only,
337359
panel_plot_mode=panel_plot_mode,
338360
plot_level=plot_level,
361+
loss=loss,
339362
)
340363

341364
rpi_object.init_obj(initialization)

‎src/cdtools/models/simple_ptycho.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import partial
12
import torch as t
23
from cdtools.models import CDIModel
34
from cdtools import tools
@@ -41,6 +42,10 @@ def __init__(
4142
self.probe = t.nn.Parameter(probe_guess / self.probe_norm)
4243
self.obj = t.nn.Parameter(obj_guess)
4344

45+
# We register a loss function and an appropriate normalization
46+
self.loss = partial(tools.losses.amplitude_mse, use_sum=True)
47+
self.loss_normalizer = tools.losses.AmplitudeMSENormalizer()
48+
4449

4550
@classmethod
4651
def from_dataset(cls, dataset):
@@ -99,12 +104,10 @@ def interaction(self, index, translations):
99104
def forward_propagator(self, wavefields):
100105
return tools.propagators.far_field(wavefields)
101106

107+
102108
def measurement(self, wavefields):
103109
return tools.measurements.intensity(wavefields)
104110

105-
def loss(self, real_data, sim_data):
106-
return tools.losses.amplitude_mse(real_data, sim_data)
107-
108111

109112
# This lists all the plots to display on a call to model.inspect()
110113
plot_list = [

0 commit comments

Comments
 (0)