-
Notifications
You must be signed in to change notification settings - Fork 554
[MRG+2] Added support for named arguments with objective function (NEW version) #592
Conversation
|
Hello @Hvass-Labs! Thanks for updating the PR.
Comment last updated on January 06, 2018 at 09:57 Hours UTC |
|
I wonder if a better name for |
|
Thanks! This looks good at first sight. Can you add tests though before we start the review process? |
| arg_dict = {dim.name: value for dim, value in zip(dimensions, x)} | ||
|
|
||
| # Call the wrapped objective function with the named arguments. | ||
| objective_value = func(**arg_dict) |
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 am not convinced the comments above (line 591, 592 and 595) bring anything helpful except repeating the semantics of the code.
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.
You are wrong. These comments explain in English what is actually fairly unusual Python programming techniques that certainly won't be understood very well by people who are not Python experts. Once again, this code is not written for you!
skopt/utils.py
Outdated
|
|
||
| def wrap_named_args(dimensions): | ||
| """ | ||
| Wrapper for an objective function that uses named arguments to make it |
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.
Wrapper -> Decorator? That's the standard name for that sort of things 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.
I think you are deliberately nitpicking here and although I'm not a Python expert I doubt that what you're saying is strictly true. We are importing and using something called wraps from functools and they repeatedly talk about wrapping functions in the documentation, and they even call it wrapper in their code examples. Yes, these are also called function decorators in Python when used with the @ symbol. But this code may be used by people who are not Python experts so they may not know what a decorator is, but they most likely know what a function-wrapper is.
I'll tell you what. I'll call it a Wrapper / Decorator so everyone is happy :-) Besides I think use_named_args is a better name as well.
| function-decorator. To get explanations of how this works in Python, | ||
| please do an internet search for e.g.: | ||
| Python function decorator with additional arguments. | ||
| """ |
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 would just remove that, or make that comment shorter. This is standard practice 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.
This is an advanced usage of Python decorators. It is poorly understood even by experienced Python programmers as I could see on StackOverflow - I myself have never used this technique before and I had to look it up. It took me quite a while to find out how to do it, and it is not obvious where to find information on this. There is no good doc-page for this in the Python manuals. So to save people a lot of time, I give a good suggestion for a google search-string if they want to understand what is going on in this code.
Please stop being so goddamn stingy with comments in the code!
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.
Please stop being so goddamn stingy with comments in the code!
I must admit, I laughed reading that.
Please read my comment again: I asked to either remove or shorten your comment. Telling people to google if they dont understand Python does not really help.
So let's be constructive. Instead I suggest to point the readers to the page you read yourself to implement this pattern. This can be shortly expressed in 1 or 2 lines at most, and is more helpful to everyone.
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.
It was a bunch of pages and none of them were really good. I picked up a little info here and a little there. If you google for the 'wrong' keywords you find even worse pages. If there was a single good webpage explaining this I would of course have linked to it. I added these keywords because they really helped me. Again, this is not a common Python feature that is being used and even experienced developers on StackOverflow were confused about it.
Again you are deliberately nitpicking what you want changed in this PR.
You can bulldoze all these comments a month from now and I will never notice, but I suggest you instead have some empathy for who is going to be reading the code in the future.
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 with @glouppe. It would be better to state which pages exactly the user should visit where the helpful information is; Otherwise, every user who wants to understand the code will do the search from scratch, possibly ending on wrong pages.
After some search, these two links seem to explain what is going on:
Explanation to @wraps:
https://docs.python.org/2/library/functools.html#functools.wraps
Explanation to decorator with parameters:
https://stackoverflow.com/questions/5929107/decorators-with-parameters
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 concede and have rewritten these doc-strings. However, I didn't like the links you found as I found them confusing :-) I managed to find a 7 minute youtube video that explains it quite well, so I added that link. I hope everyone is happy with this.
|
Before I start, let me once again say that I think the idea of your project is absolutely awesome! But your code is often really, really bad! I wish you could take this as a big compliment that you are on to something very important with this library, but you need to improve certain aspects, and that starts with accepting that your code "style" is actually really awful! As I wrote in #579 (comment) I expected you would demand that I remove all the comments from my code. That is not going to happen! If I add code to your project, then it will be beautifully commented and explained like this, otherwise I will retract the PRs. I will not have my name associated with crappy code! I get job offers from all over the world - even from places you probably wouldn't believe! A part of the reason is that I am so good at explaining what I do and write code so well. Whether you choose to remove all the comments afterwards is your business, but you would be making a huge mistake. Let me explain why. There is one main thing that both you and @betatim misunderstand: This code is not written for you! It is written for the person who in 5 years has a bug or wants to modify the code. None of us will be around to help, or we will certainly have forgotten what exactly the code is supposed to do if it hasn't been documented really well. My code can be understood very easily by even first-year students, in part because the code itself is very clean and polished, but especially because it is so well documented in English. You don't need to have a deep understanding of Your code on the other hand is extremely hard to understand - both because it is often very poorly programmed (it really is!), but the problem is then amplified greatly by the lack of comments, so the intention of the code isn't even clear. This makes it one gigantic puzzle to fix bugs or extend your code. Your code is confusing even to yourself, as I can see from the discussions here when bugs come up, semantics are unclear, etc. I already showed @betatim some work I did 12 years ago on advanced C++ called ArrayOps. It was beautifully designed, coded and documented. Even though I had not really programmed C++ in 12 years - I was still able to grasp my old work from just browsing the manual and the well-documented code for 10 minutes or so the other day. Same goes for my SwarmOps project, which is also quite old now - and by the way is a much simpler way of doing hyper-parameter optimization than skopt, which is why I am so excited about skopt! Now I am of course becoming well-known for my TensorFlow tutorials. They do not get the most views, but I will bet you that those tutorials have taught more people how to program TensorFlow than any other tutorials. And the reason people really like them, is in part because I am so disciplined and careful in polishing and explaining everything. You don't have to lookup API calls, or guess at semantics or anything like that, because it is all beautifully explained and commented in English. With these examples it should be obvious that at least when I was younger I was a far better software engineer than you are now. And I was then, and am still, far better than you at explaining complicated things and documenting my code so it can be understood by mostly everyone. So I really don't understand why you don't want to learn from my way of doing things, or at least welcome it in the code that I have added to your project, instead of stubbornly insisting that your way is better. http://www.hvass-labs.org/projects/arrayops/files/ArrayOps2.pdf |
|
Instead of keeping repeating how bad we are at writing code and how we dare to publish undocumented lines of Python, can we stick to the review process?
|
|
I am trying to convince you that in 5 years nobody will understand your code if you don't put more effort into it. Not even you will understand it. And yes I find it offensive when you want me to write as crappy code as you do, when I have clearly put effort into making it beautiful and easy to understand for everyone. Now I have added tests. And I am certain that you are unhappy with the comments in the code. But I glanced at some of your other tests and they are completely incomprehensible to anybody except yourself. And in 5 years you won't even know what they're doing. I really wish I knew what to say to make you realize this. But I don't think I can, so let's just finish this without killing each other :-) |
skopt/space/space.py
Outdated
| return "Real(low={}, high={}, prior='{}', transform='{}')".format( | ||
| self.low, self.high, self.prior, self.transform_) | ||
| return "Real(low={}, high={}, prior='{}', transform='{}', name='{}')".format( | ||
| self.low, self.high, self.prior, self.transform_, self.name) |
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.
Can you also do that for dimensions of other types, for consistency? (Integer, etc).
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.
Disclaimer: this is not a nitpick.
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.
Also, since names are None by default, I am not sure we actually want to print name=None in the default case. What do others 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.
Now there's a good review request! :-)
It is already done in PR #579 which also fixes other things with Dimension-names. I just needed a single one for testing in this PR.
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.
Can you backport from #579 please? (That's why we prefer self-contained atomic PRs that target a specific goal, to avoid these situations...)
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'm not an expert at git so I thought this would merge seamlessly as the change is identical for the two PRs. But this code was actually not required in this PR, I only needed it for some print-testing. So I just removed it from this PR.
|
Alright, thanks. These changes are good to go for me. I'll let a second opinion pitch in for merge. |
|
Very good, thank you! |
|
@betatim is familiar with this and I give him partial credit for the solution as he came up with the idea of using a decorator (although not the actual implementation). So perhaps he could test if he is happy with it as well? |
|
I think it would be helpful to add a short comment to the doc-strings of all optimizers so people know they can use this. This is the doc-string from After: I don't think you support multi-objective optimization, right? So Should I add this doc-string to all optimizers? |
| """ | ||
|
|
||
| # Ensure all dimensions are correctly typed. | ||
| if not all(isinstance(dim, Dimension) for dim in dimensions): |
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 good to print the value of dim which is not an instance of Dimension - could help solve bugs faster for future users.
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.
skopt/utils.py
Outdated
|
|
||
| # Ensure all dimensions have names. | ||
| if any(dim.name is None for dim in dimensions): | ||
| raise ValueError("All dimensions must have names.") |
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.
Maybe same as above - print which dimension does not have a name, should help identify a wrong dimension faster
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.
|
|
||
| @wraps(func) | ||
| def wrapper(x): | ||
| # Ensure the number of dimensions match |
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.
Consider adding a docstring explaining the type of x and what it is. It seems that Pycharm for instance can infer expected type of variable from docstring, providing extra variable checks and warnings, which sometimes save me bugs. Possibly could also simplify comments as per discussion below
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.
| # Ensure the objective function can be called with a single | ||
| # argument that is an unnamed numpy array. | ||
| res = func(np.array(default_parameters)) | ||
| assert (isinstance(res, float)) |
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.
Maybe add checks whether with unnamed dimensions / non - Dimension objects an exception rises. I imagine that in future we would want to extend your code to support also implicit dimension definitions with names, which could look like:
(0.1, 1.0, 'log-uniform', 'foo') # equivalent to Real(0.1, 1.0, prior='log-uniform', name='foo')
# currently we can only use
# (0.1, 1.0, 'log-uniform')
# and then the dimension is unnamedwhich might temper with these exceptions, and could lead to subtle bugs. While such definition of dimension seems to be somewhat ugly at first glance, it is appealing in that it leads to a more compact code.
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.
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.
Hmm ... I have tested myself whether the exceptions are thrown and the new detailed error-messages are correct. I'm not sure a unit-test adds much "safety" to this? And I don't quite understand what subtle bugs might arise in the future?
Furthermore, regarding your short-hand notation I think it is actually quite problematic, both in terms of reading your code that uses skopt, such as this:
space = [(1, 5), # max_depth
(10**-5, 10**0, "log-uniform"), # learning_rate
(1, n_features), # max_features
(2, 100), # min_samples_split
(1, 100)] # min_samples_leaf
In my opinion, it would be much more clear if you wrote:
space = [Int(1, 5, name='max_depth'),
Int(10**-5, 10**0, prior="log-uniform", name='learning_rate'),
Int(1, n_features, name='max_features'),
Int(2, 100, name='min_samples_split'),
Int(1, 100, name='min_samples_leaf')]
I can also see in your code that the short-hand notation causes problems other places and it requires numerous weird tests, etc.
You will need even more strange tests if you allow naming in the short-hand notation, because now you don't know whether the 3rd arg is the name of the dimension, or if it defines e.g. the prior-mapping. You can fix that with more weird tests, such as whether the 3rd string arg is one of the allowed prior-mappings, otherwise it is assumed to be the name. But what if the user has made a typo in the prior-mapping? ... Suddenly the API and code is very confusing and bloated. It's not going to be pretty! :-)
In my opinion, it is better to aim at making the code clear even if it means it becomes significantly more verbose. Having compactness of the code as a major goal is only really justified in performance-critical applications - in which case the code can be so hard to read, that extensive commenting is then 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.
Yeah I also expect the short-hand definition of dimension with names to be somewhat complex :)
Short-hand seems to go nicely for quick prototyping with spaces in BayesSearchCV, but there actually one does not need named spaces, so yeah not sure whether we would need it in the end. Lets see what future users say.
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 we should try as much as possible to gently persuade people to not use the shorthand notation (anymore), for example by only allowing you to do simple stuff through it. Along the lines of explicit is better than implicit.
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 don't think gentle persuasion is my thing so I'll leave that to you! :-)
Seriously though, I think it would be a wise decision to aim for crystal clear semantics of the API rather than having a million clever little features. I can see both in your code and your github threads that you are also confused about your code and its semantics, and I'm glad that you have now realized the confusion is 10x for newcomers :-)
|
I guess @betatim is busy, so I will take a look. |
|
I think this is a really cool feature, thanks @Hvass-Labs ! As for multi-objective optimization, I will take a look whether #369 interferes |
|
Ah see starting from here scikit-optimize/skopt/optimizer/forest.py Line 80 in 80345a8
Some functions can return two values. Maybe just state |
|
Overall the PR looks good to me! Some of my suggestions can be added in different PR. |
|
@iaroslav-ai I'm glad you like the feature and thanks for the constructive comments! I'll start working on them now. If you have the time then please review PR #579. I really would like these two PRs to be completed soon so I can finish my tutorial on skopt and TensorFlow. And I think you guys would like to get rid of me as well :-) |
|
I have pushed the fixes @iaroslav-ai requested so I think everything is ready to be merged now. Thanks to everyone for contributing to this! |
|
LGTM! |
|
Thank you @Hvass-Labs for this PR! I think I speak for everyone when I say that we all appreciate your contribution and efforts in this PR. I am also looking forward to your tutorial on tensorflow + skopt, so let us know when that is made! I will take a look at another PR (soon). 🥂 🎉 |
|
@iaroslav-ai Thanks for the nice words and I'm glad that I was able to contribute a little to your fantastic project! |
|
This still does not address my issue (#583), because the Fundamentally, we should standardize the way spaces are defined. Rather than adding this decorator, which now requires the space to be in yet a different form in order to leverage the named-params functionality and adds code to @Hvass-Labs, is there a way to define dictionary-form spaces so all works naturally with TensorFlow? If I recall correctly, your problem from #570 was you couldn't use As I said elsewhere, I have actually already created a version of |
|
@pavelkomarov I tried following your arguments in #583 and here, but it would be easier to understand if you provided a small code snippet of what you want to do. But I do agree that the semantics of defining dimensions and search-spaces could perhaps be a bit more clear. But the problem is that we need this for different things. Personally I would have been happy if dimensions always had to be named so the objective function would be called directly with named arguments, instead of using this decorator. I also do not like the scipy API and think skopt could have been far more elegant if it was not built around that. But I don't think there's much chance of making such a huge change to skopt, that's why I just added this decorator-hack, which works well enough for me. Here's an example of what you would do now to define the search-space dimensions: And you can then define your objective function as: Once again, I agree that it would have been more elegant to omit the decorator and have the framework automatically call Now the framework supports both the old scipy-style of calling Why do you want to use a dict to define the dimensions? Would it look something like this? I prefer the more proper object-oriented style above. See also the discussion in #592 (comment) why I prefer that way. |
|
In the world of scikit-learn and other machine-learning libs having a dictionary based definition is indeed natural and what people expect. Which is why There are many applications for scikit-optmize beyond tuning hyper-parameters for ML models, with their preferred way of defining search spaces. With the new decorator you can do the following, which I think is a good compromise between convenience for scikit-learn style parameter setting and the general purpose nature of @use_named_args(dimensions)
def func(**kwargs):
my_sklearn_clf.set_params(**kwargs)
..do stuff and measure performance..If you name all the dimensions in your search space. |
I don't think that is possible. If you come from scikit-learn you prefer dictionary based definitions, if you come from If you then want to dig deeper, really tweak things, or have other power user/special needs scikit-optimize provides the Think of scikit-optimize as Ikea furniture. You can put it together according to the spec from the shop, but you can also heavily modify it or put it together in new ways. The fact that @pavelkomarov managed to quickly put together a copy of |
|
"quickly" was a couple of days, so I'd still lobby to tweak base_minimize so it can support the dictionary form natively rather than requiring people make a list of dimensions with names = keys from their dictionary if they're coming from an sklearn frame of mind. But it's more useful for this to be concrete in a pull request. |
This is a much improved version of PR #590 which was also discussed in various issues including #570 and #583.
It might be a good idea if I update the doc-strings of all optimizers to briefly describe that
funccan be wrapped using this decorator so as to use named arguments. Do you agree?Thanks to @betatim for suggesting the use of a decorator which made this version very elegant.
I have not worked very much with decorators and this is a fairly advanced usage scenario, so I think it is better to direct the interested user to do an internet search if they want an explanation on how advanced decorators work in Python. See my comments in the code.
However, I do hope you appreciate the otherwise detailed level of commenting I have added, which is what I have been discussing in my other issues and PRs. It really makes it so much easier for others to understand the intention of the code in case there a bugs, or in case they want to modify the code in the future.
It is extremely important to try and polish and document the code when it is first added to the library. If I had not polished and documented this code properly and left it for others to do, then they would have had to guess at the intended semantics, and they would have spent maybe 10x as long as it has now taken me to polish and document this code - and they would have been pissed off that I hadn't done it properly in the first place.
I admit openly that this version is superior to what any of us suggested individually, and I suppose this shows how great the design and implementation can be under collaboration - but it requires for each person to properly polish and document his additions to the code. That is what I understand to be real collaboration, and not cutting corners which creates a much larger burden for future developers.
You can test this addition with this code: