Issue1101 pdhg examples - #1191
Conversation
|
Hello @mehrhardt! Thanks for updating the PR. No PEP8 issues. Comment last updated on October 26, 2017 at 09:26 Hours UTC |
adler-j
left a comment
There was a problem hiding this comment.
Very good addition.
Some minor comments on style etc, and I also need a discussion on how we should handle these rather long examples, we need to somehow indicate that they are "advanced" in some sense.
|
|
||
| min_{x >= 0} ||x - d||_1 + lam TV_gamma(x) | ||
|
|
||
| where ``grad`` the spatial gradient and ``d`` is given noisy data. |
There was a problem hiding this comment.
there is no "grad" above, but i personally like the wrong version which is ||grad(x)||_1
There was a problem hiding this comment.
True, modified it similar to your suggestion.
| # Rescale max to 1 | ||
| image /= image.max() | ||
|
|
||
| # Discretized spaces |
| # Create space element of ground truth | ||
| orig = space.element(image.copy()) | ||
|
|
||
| # Add noise and convert to space element |
There was a problem hiding this comment.
there is no conversion in the line below
| noisy = odl.phantom.salt_pepper_noise(orig) | ||
|
|
||
| # Gradient operator | ||
| gradient = odl.Gradient(space, method='forward') |
There was a problem hiding this comment.
method='forward' is the default, so no need to add it
| reg_param = 1 | ||
|
|
||
| # l1 data matching | ||
| l1_norm = 1 / reg_param * odl.solvers.L1Norm(space).translated(noisy) |
There was a problem hiding this comment.
The regularization parameter changes the strong convexity of f*. Thus, it is sometimes better to have it as part of g. Here it is 1, so it doesn't matter.
| (1 / self.sigma) * self.functional.proximal(self.sigma)) | ||
|
|
||
|
|
||
| class HuberL1L2(Functional): |
There was a problem hiding this comment.
Adding this partially solves #597 which should be mentioned i guess
There was a problem hiding this comment.
Also not 100% sure about the naming, is there no way to create something like Huber and control the L1L2 part via parameters?
There was a problem hiding this comment.
Good point, maybe this is possible. So far, I only came across the L1L2 version. Maybe we can add a TODO note and implement this when there is need for this (which may be never?!?)?
There was a problem hiding this comment.
I still feel that we need to improve the naming here before we go ahead. Is there any "reasonable" way to make this simply Huber?
|
|
||
| Parameters | ||
| ---------- | ||
| space : `DiscreteLp` or `FnBase` |
There was a problem hiding this comment.
DiscreteLp is a subclass of FnBase so no need to mention
|
|
||
| Examples | ||
| -------- | ||
|
|
| >>> H = alpha * odl.solvers.HuberL1L2(X, gamma) | ||
| >>> L1 = alpha * odl.solvers.GroupL1Norm(X, 2) | ||
| >>> abs(H(x) - L1(x)) < 1e-10 | ||
| """ |
There was a problem hiding this comment.
It would be tremendous with a Notes section here where you give the definition of the functional
There was a problem hiding this comment.
Done, please have a look.
| @property | ||
| def convex_conj(self): | ||
| '''The convex conjugate''' | ||
| return FunctionalQuadraticPerturb(GroupL1Norm(self.domain, |
There was a problem hiding this comment.
split this on two lines for readability
|
Anything left to do here before merging? |
adler-j
left a comment
There was a problem hiding this comment.
Some minor comments, overall the only major thing is fixing the HuberL1L2 naming
| -------- | ||
| Compare HuberL1L2 and L1 for vanishing smoothing ``\\gamma=0`` | ||
|
|
||
| >>> import odl |
There was a problem hiding this comment.
odl is auto-imported in doctests, no need to import it
There was a problem hiding this comment.
I know. I thought we discussed this a few months ago and you preferred this so that one can copy-paste the examples into an ipython shell?
| Compare HuberL1L2 and L1 for vanishing smoothing ``\\gamma=0`` | ||
|
|
||
| >>> import odl | ||
| >>> X = odl.uniform_discr([0, 0], [1, 1], [5, 5]) |
| >>> import odl | ||
| >>> X = odl.uniform_discr([0, 0], [1, 1], [5, 5]) | ||
| >>> x = odl.phantom.white_noise(X) | ||
| >>> alpha = 2 |
There was a problem hiding this comment.
alpha here is not really needed to convey the example
| >>> X = odl.uniform_discr([0, 0], [1, 1], [5, 5]) | ||
| >>> x = odl.phantom.white_noise(X) | ||
| >>> alpha = 2 | ||
| >>> gamma = 0 |
There was a problem hiding this comment.
Might as well write gamma=0 on the line below. Keeping the examples compact is good for readability
|
|
||
| def _call(self, x): | ||
| '''Return the HuberL1L2-norm of ``x``.''' | ||
| if isinstance(self.domain, ProductSpace): |
There was a problem hiding this comment.
What about product-space of product-space, etc?
There was a problem hiding this comment.
Good point. I don't know how to handle this. You are the expert here I guess.
| (1 / self.sigma) * self.functional.proximal(self.sigma)) | ||
|
|
||
|
|
||
| class HuberL1L2(Functional): |
There was a problem hiding this comment.
I still feel that we need to improve the naming here before we go ahead. Is there any "reasonable" way to make this simply Huber?
|
I made the changes you requested. The product space issue is hard to handle I suppose. It needs to be clear what "norm" is locally defined and not for all norms does this construction make sense. Probably it is best to check for these things and to throw an error if one tries something strange? Regarding the naming, I changed it to "Huber" as this is the only HuberNorm I know of that is being used. One could then generalize this later if need be without breaking backward compatibility by ensuring that the default parameter will refer to this version. It would also be good to merge this version with the one of Axel #1195 to get the best of two worlds. |
857b736 to
5b65eaf
Compare
kohr-h
left a comment
There was a problem hiding this comment.
A somewhat larger PR, therefore more comments. All in all a very nice addition, this is for the final polish.
| @@ -0,0 +1,135 @@ | |||
| """Total variation denoising using PDHG. | |||
|
|
|||
| This exhaustive example solve the L1-HuberTV problem | |||
| obj_fun = l1_norm + huber * gradient | ||
|
|
||
| # Strong convexity of "f*" | ||
| strong_convexity = 1 / huber.grad_lipschitz |
There was a problem hiding this comment.
Maybe our initial examples encouraged this, but I feel that this style is a bit too much "comment-code-staccato". I would prefer if there were some logical blocks like "space and data creation", "setup of the functionals and operators", etc.
Also, some of the comments only repeat what is already obvious from the code, like "Gradient operator", they can just be left out.
There was a problem hiding this comment.
Agreed. In particular as this example is not very introductory.
| self.obj_function_values = [] | ||
|
|
||
|
|
||
| callback = (odl.solvers.CallbackPrintIteration() & CallbackStore()) |
There was a problem hiding this comment.
Using a step of 5 or 10 makes these kinds of examples run much faster. In 2D, plotting usually takes most of the runtime with step=1.
| # Assign operator and functionals | ||
| op = gradient | ||
| f = huber | ||
| g = l1_norm |
There was a problem hiding this comment.
I would prefer using the names f and g immediately farther up instead of aliasing here.
| tau = 1.0 / gradient.norm # Step size for primal variable | ||
| sigma = 1.0 / gradient.norm # Step size for dual variable | ||
|
|
||
| # Run algorithms 2 and 3 |
There was a problem hiding this comment.
Huh? I only see 1 algorithm being run.
| i.ufuncs.logical_not(out=i) | ||
| out += i * (n - self.gamma / 2) | ||
| else: | ||
| out = n |
There was a problem hiding this comment.
This is a matter of taste, but a good principle for local variable names is "the larger the scope, the more verbose the name".
In practice that means that names like i and n are fine for short loops or list comprehensions. As soon as the scope spans more than, say, 5 lines of code, though, one-letter names make code less readable simply because they are harder to distinguish from numbers, e.g. i or l from 1.
Here, n could easily be norm without making lines annoyingly long. Also, the i multiplication can be replaced by boolean array indexing by going through Numpy:
norm = self.local_norm(x).asarray()
with writable_array(out) as out_arr:
if self.gamma > 0:
# Quadratic part
i = (norm < gamma)
out_arr[i] = norm[i] ** 2 / (2 * self.gamma)
# Absolute value part
np.logical_not(i, out=i)
out_arr[i] = norm[i] - self.gamma / 2This is slightly slower due to boolean indexing, but more memory-friendly. You can scrape off even more copies by changing norm in-place before assigning to out, at the expense of an additional (slow) boolean indexing each time.
There was a problem hiding this comment.
I agree with the variable names. When you write this code for yourself I guess one is just too lazy :)
About the computations, I am not so sure. I would like to keep the code numpy independent so that you can also use it on the gpu which I believe should be the case at the moment.
There was a problem hiding this comment.
Fair enough. Just be aware that most of these ufuncs are not implemented in the CUDA backend and fall back to "copy to CPU, then use Numpy, then copy back to GPU". So you will probably end up being slower AND use more memory.
That said, Jonas is doing the (I hope) final round of review of #1088 which will make all this work much nicer.
| f = FunctionalQuadraticPerturb(n.convex_conj, | ||
| quadratic_coeff=self.gamma / 2) | ||
|
|
||
| f.strong_convexity = 1 / self.grad_lipschitz |
There was a problem hiding this comment.
See above. Assigning attributes to foreign objects is not pretty (although I'm guilty of this, see #1177, but that's an exception 😇 ). We should aim for a solution that takes this as an optional initalization argument.
| Parameters | ||
| ---------- | ||
| space : `FnBase` | ||
| Space X which is the domain of the functional F |
There was a problem hiding this comment.
X and F don't refer to anything. I'd go for "Domain of the functional."
There was a problem hiding this comment.
Too much copy-pasting :)
There was a problem hiding this comment.
Yeah we sure have some cleanup to do in the older parts.
| ----- | ||
| The proximal operator is given by given by the proximal operator of | ||
| 1/(2*gamma) * L2 norm in points that are <= gamma, and by the | ||
| proximal operator of the l1 norm in points that are > gamma. |
There was a problem hiding this comment.
Can you use backticks for things like gamma and formula-like stuff?
| if isinstance(self.domain, ProductSpace): | ||
| return PointwiseNorm(self.domain, 2)(x) | ||
| else: | ||
| return x.ufuncs.absolute() |
There was a problem hiding this comment.
As above. Used only once, so just inline the code.
9b029d1 to
76be4e5
Compare
|
OK, this looks weird. In the log of the files, these are not "new" commits. What I did was to rebase the current branch onto upstream/master as git fetch upstream It then complained about the branches having diverged. I force-pushed my local version into the repo which "looks" good but github can't handle it (see fake commits above). What is the proper way to handle this situation? |
Which ones do you mean? Isn't 23fe237 the first commit in this PR? |
|
It is, but my github reminds me again that I "added some commits 9 days ago". Is that different for you? |
Ah, I think I know what you mean. When you rebase, GitHub adds the commits as new ones below the last comment. If you reload the page, the old ones go away. |
kohr-h
left a comment
There was a problem hiding this comment.
I just have a few further comments and one hiccup with the norm thing, and I would like to settle this discussion about the two examples is a good way. Otherwise (without looking at all details) it looks good to me.
| self.obj_function_values = [] | ||
|
|
||
|
|
||
| callback = (odl.solvers.CallbackPrintIteration(step=10) & CallbackStore()) |
There was a problem hiding this comment.
I am OK with removing these but the two of you should settle on this. A while ago @adler-j told me to put parentheses for readability somewhere.
There was a problem hiding this comment.
For stuff on one line? I'd be surprised. It makes sense when you have stuff that spans 2 or more lines.
| min_{x >= 0} 1/2 ||x - d||_2^2 | ||
| + lam * sum_i eta_gamma(||grad(x)_i||_2) | ||
|
|
||
| where grad the spatial gradient and d is given noisy data. Here eta_gamma |
There was a problem hiding this comment.
Can you put the math stuff in double backticks, like ``grad``? Not that it's rendered anywhere, but it sticks a bit more out in the plain text. And who knows, maybe one day we can actually also render this text somewhere :-)
| @@ -0,0 +1,135 @@ | |||
| """Total variation denoising using PDHG. | |||
There was a problem hiding this comment.
Another possibility is to make a big
if data_term == 'L1':
# Do the L1 case, including plotting and whatnot
elif data_term == 'L2':
# Other caseSince it's a rather elaborate example anyway, making one large example from two slightly-less-than-large would still be good IMO.
What's your opinion @adler-j?
|
|
||
| # Define objective functional | ||
| op = odl.Gradient(space) # operator | ||
| op.norm = np.sqrt(8) + 1e-4 # norm with forward differences is well-known |
There was a problem hiding this comment.
We shouldn't be setting attributes on instances like this. In fact, this overrides the method Operator.norm with a constant.
Until Gradient.norm is implemented please make this a free variable instead. Do we have an open issue on that somewhere? I can't find one.
| space : `FnBase` | ||
| Domain of the functional. | ||
| gamma : float | ||
| Smoothing parameter of Huberization. If ``gamma = 0``, then |
| Domain of the functional. | ||
| gamma : float | ||
| Smoothing parameter of Huberization. If ``gamma = 0``, then | ||
| functional is non-smooth corresponds to the usual L1 norm. For |
| Smoothing parameter of Huberization. If ``gamma = 0``, then | ||
| functional is non-smooth corresponds to the usual L1 norm. For | ||
| ``gamma > 0``, it has a ``1/gamma``-Lipschitz gradient so that | ||
| its convex conjugate is ``gamma``-strongly convex. |
There was a problem hiding this comment.
You surely prepare for this addition 😛
| >>> abs(huber_norm(x) - l1_norm(x)) < tol | ||
| True | ||
|
|
||
| Redo previous example for a product space in two dimensions. |
There was a problem hiding this comment.
Colons at the end of those intermediate texts.
76be4e5 to
d7ba623
Compare
|
I made a small PR due to some things I noticed when doing the final skim. When that's in (you can also pick only the stuff that you like) I'll hit the merge button. |
MAINT: small fixes to Huber doc and examples
|
I liked all your changes @kohr-h so I merged it without cherry picking. |
|
Thanks for the work @mehrhardt! |
Here are the two examples I promised a few weeks ago. I suppose the documentation can be improved but first I would like to have some more general feedback. For these, I also needed the Huber L1 norm for which I can compute the prox of the convex conjugate and its function evaluation.
Please let me know what you think about these.