-
Notifications
You must be signed in to change notification settings - Fork 229
[MRG] FIX Fix LMNN rollback #101
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
[MRG] FIX Fix LMNN rollback #101
Conversation
Stores L and G in addition to what was stored before
I'll also add some non regression tests that fail in master and pass in this PR |
- test that LMNN converges on a simple example where it should converge - test that the objective function never has twice the same value
test/metric_learn_test.py
Outdated
X, y = make_classification(random_state=0) | ||
old_stdout = sys.stdout | ||
sys.stdout = StringIO() | ||
lmnn = LMNN(verbose=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use python_LMNN
here and the other test, to not fail when the shogun version is available.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
test/metric_learn_test.py
Outdated
out = sys.stdout.getvalue() | ||
sys.stdout.close() | ||
sys.stdout = old_stdout | ||
assert ("LMNN converged with objective" in out) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style nit: the parens here aren't needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
test/metric_learn_test.py
Outdated
finally: | ||
out = sys.stdout.getvalue() | ||
sys.stdout.close() | ||
sys.stdout = old_stdout |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be nice to have this logic in a context manager. See https://eli.thegreenplace.net/2015/redirecting-all-kinds-of-stdout-in-python/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Also I just found there exist something in pytest that seem to do the job quite nicely: https://docs.pytest.org/en/3.2.1/capture.html
But it breaks a bit the structure of unittest classes... If it is important to keep the previous structure I'll use the context manager
Tell me what you think
test/metric_learn_test.py
Outdated
sys.stdout.close() | ||
sys.stdout = old_stdout | ||
lines = re.split("\n+", out) | ||
objectives = [re.search("\d* (?:(\d*.\d*))[ | -]\d*.\d*", s) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a comment explaining this regular expression, with an example of what it should be matching.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
metric_learn/lmnn.py
Outdated
# we update L and will see in the next iteration if it does indeed | ||
# better | ||
L -= learn_rate * 2 * L.dot(G) | ||
learn_rate *= 1.01 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems wrong to me, though it was also wrong before this PR.
Reading through the Shogun implementation here and here, they don't do any rolling back.
They do the L
update unconditionally in gradient_step
, then compute the objective value for the current iteration, then do the learning rate update based on the change in objective.
In one of the reference Matlab implementations here they do the L
update first, then optionally roll back to a saved state when updating the step size.
--
So I think the correct fix would be to move the L
update to the # do the gradient update
section, after computing the new G
, using the existing learning rate. Then, if the objective didn't improve we can halve the learning rate and roll back to the last good state (including L
and G
). Otherwise, we just grow the learning rate by 1% and carry on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inverting the order of things as suggested would also improve the readability of the code I think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just submitted a new commit inverting the order of things. I have commented the code to make it clearer: basically it starts from a reference point, and tries the next possible updated point, retrying with a smaller learning rate if needed, until it finds a new reference point which has a better objective
Try forward updates rather than doing rollback after wrong updates
# Conflicts: # test/metric_learn_test.py
@@ -72,6 +74,37 @@ def test_iris(self): | |||
self.assertLess(csep, 0.25) | |||
|
|||
|
|||
def test_convergence_simple_example(capsys): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where does capsys get passed in from?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be automatically found by pytest (as one of the integrated fixtures), and runned when running test_convergence_simple_example
. I verified doing -v (verbose) when doing pytest and these tests are correctly passing when they should (and failing when modifying the error message)
assert "LMNN converged with objective" in out | ||
|
||
|
||
def test_no_twice_same_objective(capsys): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these be methods of TestLMNN
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I read here (pytest-dev/pytest#2504 (comment)) that pytest fixtures cannot be integrated with unittest classes, so I extracted the tests from the class hierarchy. But I agree that it is not ideal. They propose a workaround in the link, so maybe it would be better ? (adding these lines to TestLMNN
, include the test in TestLMNN
, and replace capsys by self.capsys in the test)
@pytest.fixture(autouse=True)
def capsys(self, capsys):
self.capsys = capsys
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. The current solution is fine, then.
metric_learn/lmnn.py
Outdated
# previous L, following the gradient: | ||
|
||
# we want to enter the loop for the first try, with the original | ||
# learning rate (hence * 2 since it will be / 2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit confusing to read. How about a while True:
loop that breaks on the delta_obj > 0
condition at the end?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree it would improve readability
Done
@perimosocordiae I adressed your comments, except for the ones about pytest functions being outside of the unittest class architecture |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like there's a keyring.deb
file added in this PR, which probably shouldn't be.
Otherwise, I'm +1 to merge after the conflicts are resolved.
# Conflicts: # test/metric_learn_test.py
Oups, you're right for the .deb file, I don't know how it got there :p |
# objective than the previous L, following the gradient: | ||
while True: | ||
# the next point next_L to try out is found by a gradient step | ||
L_next = L - 2 * learn_rate * G |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here it should be 2*learn_rate*L.dot(G)
, not 2*learn_rate*G
... (see #201)
objective = objective_old | ||
else: | ||
# update L | ||
L -= learn_rate * 2 * L.dot(G) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixes #88
Stores
L
andG
in addition to what was already stored (df
,a1
anda2
), at the last "good" point (meaning the last time a point had a better objective than all the previous points), and gets back to this point if the update worsens the objective.Here is a little code to show the difference between this PR and master:
This PR: