Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Computing Hessian products with image data objects and related stuff #1253

Merged
merged 21 commits into from
May 16, 2024

Conversation

evgueni-ovtchinnikov
Copy link
Contributor

@evgueni-ovtchinnikov evgueni-ovtchinnikov commented May 10, 2024

Changes in this pull request

Testing performed

Related issues

Fixes #1244, #1249

Checklist before requesting a review

  • I have performed a self-review of my code
  • I have added docstrings/doxygen in line with the guidance in the developer guide
  • I have implemented unit tests that cover any new or modified functionality
  • The code builds and runs on my machine
  • CHANGES.md has been updated with any functionality change

Contribution Notes

Please read and adhere to the contribution guidelines.

Please tick the following:

  • The content of this Pull Request (the Contribution) is intentionally submitted for inclusion in SIRF (the Work) under the terms and conditions of the Apache-2.0 License.

typedef xSTIR_PoissonLLhLinModMeanListDataProjMatBin3DF LMObjFun;

extern "C"
void* cSTIR_objFunListModeSetInterval(void* ptr_f, size_t ptr_data)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really for a question for this PR, but why size_t ptr_data and not void * ptr_data? On some systems, they are not the same size, and this could therefore create trouble.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vaguely remember having some SWIG trouble with void* but no longer remember what it was. Perhaps with the latest SWIG void* is now ok.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW SWIG actually still does not like passing the numpy array data pointer as void* - tried with cSTIR_getImageData, got this:

Traceback (most recent call last):
  File "/home/sirfuser/devel/buildVM/sources/SIRF/examples/Python/PET/acquisition_data.py", line 172, in <module>
    main()
  File "/home/sirfuser/devel/buildVM/sources/SIRF/examples/Python/PET/acquisition_data.py", line 141, in main
    image_array = image.as_array()
  File "/home/sirfuser/devel/install/python/sirf/STIR.py", line 611, in as_array
    try_calling(pystir.cSTIR_getImageData(self.handle, array.ctypes.data))
  File "/home/sirfuser/devel/install/python/sirf/pystir.py", line 306, in cSTIR_getImageData
    return _pystir.cSTIR_getImageData(ptr, ptr_data)
TypeError: in method 'cSTIR_getImageData', argument 2 of type 'void *'
sirfuser@vagrant:~/devel/buildVM/sources/SIRF/examples/Python/PET$ 

same story with float*:

Traceback (most recent call last):
  File "/home/sirfuser/devel/buildVM/sources/SIRF/examples/Python/PET/acquisition_data.py", line 172, in <module>
    main()
  File "/home/sirfuser/devel/buildVM/sources/SIRF/examples/Python/PET/acquisition_data.py", line 141, in main
    image_array = image.as_array()
  File "/home/sirfuser/devel/install/python/sirf/STIR.py", line 611, in as_array
    try_calling(pystir.cSTIR_getImageData(self.handle, array.ctypes.data))
  File "/home/sirfuser/devel/install/python/sirf/pystir.py", line 306, in cSTIR_getImageData
    return _pystir.cSTIR_getImageData(ptr, ptr_data)
TypeError: in method 'cSTIR_getImageData', argument 2 of type 'float *'

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm. weird. Here's an example that does that https://stackoverflow.com/a/37308401. Anyway, let's leave that for later!

src/xSTIR/cSTIR/cstir.cpp Outdated Show resolved Hide resolved
src/xSTIR/cSTIR/cstir.cpp Outdated Show resolved Hide resolved
src/xSTIR/cSTIR/cstir.cpp Outdated Show resolved Hide resolved
fun.accumulate_sub_Hessian_times_input(output, curr_est, input, subset);
else {
int nsub = fun.get_num_subsets();
output.fill(0.0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we're accumulating in SIRF (which I don't think we should), then this fill is wrong. On the other hand, if we're not accumulating, then you'll need it also in the case subset>=0

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved the fill above if for now - will get rid of it when we switch to multiply_with_Hessian

@KrisThielemans
Copy link
Member

Thanks, this already looks usable (with a caveat for the "accumulation"). Is this in a state that @gschramm can already try?

@evgueni-ovtchinnikov
Copy link
Contributor Author

We still have #1251, which is due to the fact that PoissonLogLikelihoodWithLinearModelForMeanAndListModeDataWithProjMatrixByBin does not override empty ObjectiveFunction.get_subset_sensitivity(). I am afraid I cannot be of much help here.

@gschramm
Copy link

gschramm commented May 12, 2024

@KrisThielemans @evgueni-ovtchinnikov Just implemented a short test script for the PoissonLogL .accumulate_Hessian_times_input() here.

current_estimate = ref_recon.copy()
input_img = acq_data.create_uniform_image(value=1, xy=nxny)
np.random.seed(0)
input_img.fill(np.random.rand(*input_img.shape) * (obj_fun.get_subset_sensitivity(0).as_array() > 0) * current_estimate.max())

hess_out_img = acq_data.create_uniform_image(value=1.0, xy=nxny)

obj_fun.accumulate_Hessian_times_input(current_estimate, input_img, subset=0, out=hess_out_img)
hess_out_img2 = obj_fun.accumulate_Hessian_times_input(current_estimate, input_img, subset=0)

fig, ax = plt.subplots(1, 4, figsize=(16, 4), tight_layout=True)
ax[0].imshow(current_estimate.as_array()[71, :, :], cmap = 'Greys')
ax[1].imshow(input_img.as_array()[71, :, :], cmap = 'Greys')
ax[2].imshow(hess_out_img.as_array()[71, :, :], cmap = 'Greys')
ax[3].imshow(hess_out_img2.as_array()[71, :, :], cmap = 'Greys')
ax[0].set_title('current estimate', fontsize = 'medium')
ax[1].set_title('input', fontsize = 'medium')
ax[2].set_title('hess_out_img', fontsize = 'medium')
ax[3].set_title('hess_out_img2', fontsize = 'medium')
fig.show()

Unfortunately, the output of both calls (without and with using the out kwarg), don't seem to work.
hess_out_img is still an image full of ones and hess_out_img2 seems to be the same as input_img.

Figure_1

These results used:
STIR
commit feb6d85eadb392f5b8278d3b97ae2ee67ca439d9 (HEAD, origin/master, origin/HEAD, master)
Author: Kris Thielemans k.thielemans@ucl.ac.uk
Date: Thu May 9 23:39:17 2024 +0100
SIRF
commit 66c35c7 (HEAD, origin/acc-hess)
Author: Evgueni Ovtchinnikov evgueni.ovtchinnikov@stfc.ac.uk
Date: Sat May 11 07:35:35 2024 +0000

@KrisThielemans
Copy link
Member

This is a test with protection data, correct?

Also use auto more
@gschramm
Copy link

Indeed. The obj_fun is

obj_fun = sirf.STIR.make_Poisson_loglikelihood(acq_data)

@KrisThielemans
Copy link
Member

@gschramm could you try again?

@gschramm
Copy link

Looks much better now. Tmr morning I will test whether we images are what we expect from the formulas.

Screenshot 2024-05-12 at 22 51 27

@KrisThielemans
Copy link
Member

Could work for listmode as well

@gschramm
Copy link

@KrisThielemans I just compared against manually computing the "Hessian multiply" calling the fwd / back projections in SIRF. The result is very close in the foreground (where the current estimate is >> 0), but not super close in the background (where the current estimate is close to 0). Any ideas why that is?

hess_out_img = obj_fun.accumulate_Hessian_times_input(current_estimate, input_img, subset=0)

# %%
# calcuate the the Hessian multiply "manually"

acq_model.set_up(acq_data, initial_image)
acq_model.num_subsets = num_subsets
acq_model.subset_num = 0

# get the linear (Ax) part of the Ax + b affine acq. model
lin_acq_model = acq_model.get_linear_acquisition_model()
lin_acq_model.num_subsets = num_subsets
lin_acq_model.subset_num = 0

# for the Hessian "multiply" we need the linear part of the acquisition model applied to the input image
input_img_fwd = lin_acq_model.forward(input_img)
current_estimate_fwd = acq_model.forward(current_estimate)
h = -acq_model.backward(acq_data*input_img_fwd / (current_estimate_fwd*current_estimate_fwd))

The Hessian outputs are shown using a "narrow" color window (top) and a wide color window (bottom) to show the differences in foreground and background.

Screenshot 2024-05-13 at 09 22 15

@KrisThielemans
Copy link
Member

hmmm. We try to cancel singularities in the division
https://github.com/UCL/STIR/blob/feb6d85eadb392f5b8278d3b97ae2ee67ca439d9/src/recon_buildblock/PoissonLogLikelihoodWithLinearModelForMeanAndProjData.cxx#L1128 with STIR's divide_and_truncate function. However, possibly that goes a bit wrong, as it was designed for (measured / estimated), while here it is used for (fwd * measured / estimated^2), so maybe thresholds are a bit off.

Of course, ideally there wouldn't be any singularities. Is your background strictly positive?

@gschramm
Copy link

  1. The background term has indeed a few 0s (Siemens mMR data which I guess has virtual 0-sens. LORs).
  2. In the meantime I also tested the listmode objective hessian. (i) currently, it returns the negative of what we want (output is positive, but should be negative). (ii) The "listmode hessian" is very close to my "manual sinogram" hessian (see below - I plotted the "minus the LM hessian output").
Screenshot 2024-05-13 at 11 47 33

@KrisThielemans
Copy link
Member

that's encouraging.

can you try your manual with some handling of 0/0 or so? A bit surprising it didn't generate NaNs then I guess. Easiest is to add an eps to the denominator.

I'll flip the sign of the LM Hessian. 😊

@gschramm
Copy link

adding an eps = 1e-8 in the denominator of the ratio to be back projected in the Hessian does not change much - still much closer to the LM Hessian applied.

Screenshot 2024-05-13 at 12 44 26

@KrisThielemans
Copy link
Member

ah well, this looks like a STIR issue then for the Hessian with projection data. Best to open an issue there.

@KrisThielemans
Copy link
Member

@evgueni-ovtchinnikov can you then add multiply_with_Hessian? (Can just as well leave the accumulate version in I guess)

@KrisThielemans
Copy link
Member

@gschramm what computation times are you getting?

@evgueni-ovtchinnikov
Copy link
Contributor Author

multiply_with_Hessian: @KrisThielemans which version/branch of STIR should I use? I have rel_6.0.0 on my latest VMs.

@gschramm
Copy link

gschramm commented May 13, 2024

@gschramm what computation times are you getting?

Using 21 subsets, mMR LM file with 4,332,816 counts and acq_model = sirf.STIR.AcquisitionModelUsingRayTracingMatrix() and 1 tangential LOR:

In [5]: %timeit hess_out_img = obj_fun.accumulate_Hessian_times_input(current_estimate, input_img, subset=0)
2.7 s ± 45.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [6]: %timeit hess_out_img_lm = lm_obj_fun.accumulate_Hessian_times_input(current_estimate, input_img, subset=0)
9.43 s ± 249 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

@KrisThielemans
Copy link
Member

multiply_with_Hessian: @KrisThielemans which version/branch of STIR should I use? I have rel_6.0.0 on my latest VMs.

you can use 6.0.0. Just add an extra member (that calls accumulate) and remove the fill in `accumulate.

@KrisThielemans
Copy link
Member

Thanks @gschramm would you mind adding the timings in the STIR PR? Ideally also add gradient timings. I'd be interested as well in seeing what it does if num_subsets decreases (i.e. is the listmode calculations slowing us down, or the reading). Of course, you could also enable the lm_obj_fun caching, but this is going beyond what you care about. Thanks a lot for all your help.

@gschramm
Copy link

@KrisThielemans @evgueni-ovtchinnikov is accumulate_Hessian_times_input the final name? (I remember discussions on the naming) If possible, I'd like to finish my notebooks today.

@KrisThielemans
Copy link
Member

@evgueni-ovtchinnikov is working on implementing #1244 (comment). Hopefully this will be done soon. @evgueni-ovtchinnikov if you have trouble, please ask for help.

@KrisThielemans
Copy link
Member

see osem_reconstruction.py for a simple regression test for
the Hessian multiplication
@evgueni-ovtchinnikov evgueni-ovtchinnikov marked this pull request as ready for review May 15, 2024 16:37
@KrisThielemans
Copy link
Member

@evgueni-ovtchinnikov you seem to be pretty close. Can you summarise current situation?

@evgueni-ovtchinnikov
Copy link
Contributor Author

@KrisThielemans @gschramm I believe this PR can be merged - any objections?

@evgueni-ovtchinnikov
Copy link
Contributor Author

builds failed at Coveralls step - any idea anyone why all of a sudden?

Copy link
Member

@KrisThielemans KrisThielemans left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are problems with the fill(0). It should NOT be done for the accumulate function, but has to be done for the multiply version.

Please batch commits of suggestions (I think via the Files tab), or do them manually of course.

Also, can you add the test_Hessian to one of the existing test scripts such that it will always run?

src/xSTIR/cSTIR/cstir.cpp Outdated Show resolved Hide resolved
src/xSTIR/cSTIR/cstir.cpp Outdated Show resolved Hide resolved
src/xSTIR/cSTIR/cstir.cpp Outdated Show resolved Hide resolved
src/xSTIR/pSTIR/STIR.py Outdated Show resolved Hide resolved
src/xSTIR/pSTIR/STIR.py Show resolved Hide resolved
examples/Python/PET/osem_reconstruction.py Outdated Show resolved Hide resolved
@KrisThielemans
Copy link
Member

Also, please comment out the Coveralls lines
https://github.com/SyneRBI/SIRF/blob/d54c4550d1533132c6a281b6cf2159c8fd22f45c/.github/workflows/build-test.yml#L133C4-L147C32
You probably have to preserve indentation. Or leave that for a separate PR

@KrisThielemans
Copy link
Member

@evgueni-ovtchinnikov test_Hessian isn't a great test, as it just compares the norms, while it should compare the difference.

I'm modifying it and putting it in test_ObjectiveFunction.py

  def test_Hessian(self, subset=-1, eps=1e-3):
        """Checks that grad(x + dx) - grad(x) is close to H(x)*dx
        """
        x = self.image
        dx = x.clone()
        dx *= eps/dx.norm()
        dx += eps/2
        y = x + dx
        gx = self.obj_fun.gradient(x, subset)
        gy = self.obj_fun.gradient(y, subset)
        dg = gy - gx
        Hdx = self.obj_fun.multiply_with_Hessian(x, dx, subset)
        norm = dg.norm()
        q = (dg - Hdx).norm()/dg.norm()
        print('norm of grad(x + dx) - grad(x): %f' % dg.norm())
        print('norm of H(x)*dx: %f' % Hdx.norm())
        print('relative difference: %f' % q)
        numpy.testing.assert_array_equal(q,0)

However, I had to add a constant term to the acq_model as otherwise the test fails (essentially because the Hessian is ill-defined otherwise). I'll commit this soon.

@KrisThielemans
Copy link
Member

@evgueni-ovtchinnikov I'm now adding a similar test to the priors. (Had to delete a subset in the call). However, I'm confused by results. Does get_gradient(x) modify the result? Looks like it for priors.

@evgueni-ovtchinnikov
Copy link
Contributor Author

evgueni-ovtchinnikov commented May 16, 2024

@KrisThielemans

Does get_gradient(x) modify the result? Looks like it for priors.

what result?

@KrisThielemans
Copy link
Member

KrisThielemans commented May 16, 2024

got distracted... Sorry,

I did push the test for the LogLikelihood. Works for me.

@KrisThielemans
Copy link
Member

@KrisThielemans

Does get_gradient(x) modify the result? Looks like it for priors.

what result?

ignore that. The loop in the test is a bit hard to understand... Update coming soon.

also remove test_Hessian from the objective function
@KrisThielemans
Copy link
Member

ok. Should be all done now. Can you add a line to CHANGES.md and merge?

@evgueni-ovtchinnikov evgueni-ovtchinnikov merged commit 12949b4 into master May 16, 2024
6 checks passed
@evgueni-ovtchinnikov evgueni-ovtchinnikov deleted the acc-hess branch May 16, 2024 15:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

add accumulate_Hessian_times_input to STIR objective function and prior
3 participants