Skip to content

Issue1101 pdhg examples - #1191

Merged
kohr-h merged 17 commits into
odlgroup:masterfrom
mehrhardt:issue1101_pdhg_examples
Oct 26, 2017
Merged

Issue1101 pdhg examples#1191
kohr-h merged 17 commits into
odlgroup:masterfrom
mehrhardt:issue1101_pdhg_examples

Conversation

@mehrhardt

Copy link
Copy Markdown
Contributor

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.

@pep8speaks

pep8speaks commented Oct 11, 2017

Copy link
Copy Markdown

Hello @mehrhardt! Thanks for updating the PR.

No PEP8 issues.

Comment last updated on October 26, 2017 at 09:26 Hours UTC

@adler-j adler-j left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

there is no "grad" above, but i personally like the wrong version which is ||grad(x)||_1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

True, modified it similar to your suggestion.

# Rescale max to 1
image /= image.max()

# Discretized spaces

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

spaces

# Create space element of ground truth
orig = space.element(image.copy())

# Add noise and convert to space element

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

there is no conversion in the line below

noisy = odl.phantom.salt_pepper_noise(orig)

# Gradient operator
gradient = odl.Gradient(space, method='forward')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why 1 / reg_param

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Adding this partially solves #597 which should be mentioned i guess

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also not 100% sure about the naming, is there no way to create something like Huber and control the L1L2 part via parameters?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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?!?)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

DiscreteLp is a subclass of FnBase so no need to mention


Examples
--------

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Remove extra blankline

>>> H = alpha * odl.solvers.HuberL1L2(X, gamma)
>>> L1 = alpha * odl.solvers.GroupL1Norm(X, 2)
>>> abs(H(x) - L1(x)) < 1e-10
"""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It would be tremendous with a Notes section here where you give the definition of the functional

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, please have a look.

@property
def convex_conj(self):
'''The convex conjugate'''
return FunctionalQuadraticPerturb(GroupL1Norm(self.domain,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

split this on two lines for readability

@mehrhardt

Copy link
Copy Markdown
Contributor Author

Anything left to do here before merging?

@adler-j adler-j left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Some minor comments, overall the only major thing is fixing the HuberL1L2 naming

--------
Compare HuberL1L2 and L1 for vanishing smoothing ``\\gamma=0``

>>> import odl

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

odl is auto-imported in doctests, no need to import it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we usually call it "space"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK, done.

>>> import odl
>>> X = odl.uniform_discr([0, 0], [1, 1], [5, 5])
>>> x = odl.phantom.white_noise(X)
>>> alpha = 2

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

alpha here is not really needed to convey the example

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK, done.

>>> X = odl.uniform_discr([0, 0], [1, 1], [5, 5])
>>> x = odl.phantom.white_noise(X)
>>> alpha = 2
>>> gamma = 0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Might as well write gamma=0 on the line below. Keeping the examples compact is good for readability

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK, done.


def _call(self, x):
'''Return the HuberL1L2-norm of ``x``.'''
if isinstance(self.domain, ProductSpace):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What about product-space of product-space, etc?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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?

@mehrhardt

Copy link
Copy Markdown
Contributor Author

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.

@mehrhardt
mehrhardt force-pushed the issue1101_pdhg_examples branch from 857b736 to 5b65eaf Compare October 19, 2017 09:38

@kohr-h kohr-h left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

solve -> solves

obj_fun = l1_norm + huber * gradient

# Strong convexity of "f*"
strong_convexity = 1 / huber.grad_lipschitz

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. In particular as this example is not very introductory.

self.obj_function_values = []


callback = (odl.solvers.CallbackPrintIteration() & CallbackStore())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would prefer using the names f and g immediately farther up instead of aliasing here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Huh? I only see 1 algorithm being run.

i.ufuncs.logical_not(out=i)
out += i * (n - self.gamma / 2)
else:
out = n

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 / 2

This 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

X and F don't refer to anything. I'd go for "Domain of the functional."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Too much copy-pasting :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As above. Used only once, so just inline the code.

@mehrhardt

Copy link
Copy Markdown
Contributor Author

All implemented. The only thing that is left is the actual computation of the function call, gradient, prox etc where I would like not to involve numpy. In the end @adler-j, @kohr-h you are the experts how to implement this most efficiently.

@mehrhardt
mehrhardt force-pushed the issue1101_pdhg_examples branch from 9b029d1 to 76be4e5 Compare October 20, 2017 14:19
@mehrhardt

Copy link
Copy Markdown
Contributor Author

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
git rebase upstream/master

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?

@kohr-h

kohr-h commented Oct 20, 2017

Copy link
Copy Markdown
Member

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?

@mehrhardt

Copy link
Copy Markdown
Contributor Author

It is, but my github reminds me again that I "added some commits 9 days ago". Is that different for you?

@kohr-h

kohr-h commented Oct 20, 2017

Copy link
Copy Markdown
Member

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 kohr-h left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Remove outer parens

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 :-)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also "is" missing after "grad"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

@@ -0,0 +1,135 @@
"""Total variation denoising using PDHG.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Another possibility is to make a big

if data_term == 'L1':
    # Do the L1 case, including plotting and whatnot
elif data_term == 'L2':
    # Other case

Since 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK, changed.

space : `FnBase`
Domain of the functional.
gamma : float
Smoothing parameter of Huberization. If ``gamma = 0``, then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

the

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

and

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Colons at the end of those intermediate texts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

@mehrhardt
mehrhardt force-pushed the issue1101_pdhg_examples branch from 76be4e5 to d7ba623 Compare October 24, 2017 08:42
@kohr-h

kohr-h commented Oct 25, 2017

Copy link
Copy Markdown
Member

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
@mehrhardt

Copy link
Copy Markdown
Contributor Author

I liked all your changes @kohr-h so I merged it without cherry picking.

@kohr-h
kohr-h merged commit 3934c20 into odlgroup:master Oct 26, 2017
@kohr-h

kohr-h commented Oct 26, 2017

Copy link
Copy Markdown
Member

Thanks for the work @mehrhardt!

@mehrhardt
mehrhardt deleted the issue1101_pdhg_examples branch October 26, 2017 09:47
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.

4 participants