Refactoring machine learning code - comments as code
This article covers:
I find that in the field of data science and machine learning some coding principles that are standard in traditional software engineering sometimes are lacking. One such principle is to strive to rather specify everything that is possible in code rather than as comments.
Why does it make sense to do that?
Comments often don’t age well. You write them in the context of the current code, but then over time as the code gets changed and readapted to other use cases, the context changes. At some point the context often has changed so much that the original comment is out of context and doesn’t make sense anymore. Of course, you could say that when you change the context, you also have to change the comments. In reality, though, often this doesn’t happen, because the code is (hopefully) covered by some tests, but the comments are not, so it’s easy to forget about them.
The second big argument for writing comments as code is that it makes your code often much more readable. It often happens that someone writes hard to read code and then adds a comment to make it more understandable. Of course, it’s better to do it that way than just writing hard to read code ;) Even better is to write clean, easily readable code from the beginning.
Let’s take a look at some examples I’ve encountered in the context of machine learning:
def forward(x): b, c, h, w = x.shape # batch, channel, height, width # ... Do sth. with b, c, h, w
So this is the case of writing a comment to explain the naming of your short variable names. You saved a couple of characters when reusing then over and over later on, but to what price? The reader has to read the comment potentially again and again to make sure what are the variables referring to. Of course, in this case, batch, channel, height and width are so common and often used that most likely anyone who has been in the field for a while knows it right away, but it’s a habit which you will also use in other contexts then where people might not be as familiar as you are with the concepts.
In my opinion, it’s better to write:
def forward(x): batch, channel, height, width = x.shape # ... Do sth. with batch, channel, height or width
No need for the comment and your variables are named exactly for what they are, so when they are used later down the line, it’s immediately obvious what is happening.
Another good example is metrics and losses where you often find code like this:
def loss(predictions, targets, background_index): # filter out background targets mask = targets != background_index predictions = predictions[mask] targets = targets[mask] loss = F.l1_loss(predictions, targets) return loss
So in this case, it reads much better if you make this a method instead of having a comment:
def _filter_background_targets(predictions, targets, background_index): mask = targets != background_index return predictions[mask], targets[mask] def loss(predictions, targets, background_index): predictions, targets = _filter_background_targets(predictions, targets, background_index) loss = F.l1_loss(predictions, targets) return loss
Also, you can reuse that filtering potentially in other losses.
And a final example where I often see this pattern is for some complicated if statements like:
# ... # Check if image dimensions are appropriate if img.shape > max_size or img.shape > max_size or img.shape < min_size or img.shape < min_size: raise Error("Image dimensions are out of bounds")
Here the code also becomes much more readable transforming the comment and the code implementing it to a small method:
def _img_is_out_of_bounds(img, min_size, max_size): height, width = img.shape img_is_in_bounds = min_size < height < max_size and min_size < width < max_size return not img_is_in_bounds # ... if _img_is_out_of_bounds(img, min_size, max_size): raise Error("Image dimensions are out of bounds")
comments powered by Disqus