Skip to content
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

Extend docstrings of loss functions #64

Merged
Merged
Changes from 2 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
3976d62
loss function docstring extension
beichensinn Apr 19, 2021
05cb4ab
Merge branch 'master' of https://github.com/Qiskit/qiskit-machine-lea…
beichensinn Apr 20, 2021
543974a
Merge branch 'main' into extend_loss_docstring_#49
manoelmarques Apr 21, 2021
f931c6f
Merge branch 'main' into extend_loss_docstring_#49
manoelmarques Apr 27, 2021
1f1d4b6
Merge branch 'main' into extend_loss_docstring_#49
adekusar-drl Apr 27, 2021
f9d586d
Merge branch 'main' into extend_loss_docstring_#49
adekusar-drl Apr 30, 2021
39e5b81
loss function docstring revision
beichensinn May 6, 2021
b1594bd
Merge branch 'main' of https://github.com/Qiskit/qiskit-machine-learn…
beichensinn May 7, 2021
ef6640f
loss function docstring revision
beichensinn May 7, 2021
acba123
black loss function
beichensinn May 7, 2021
e49aa16
fix check 3.9
beichensinn May 7, 2021
e93b9bc
1) move raises 2) fix float
beichensinn May 7, 2021
6bd7abc
try to add raises multiple places
beichensinn May 7, 2021
c42dc1f
Merge branch 'Qiskit:main' into extend_loss_docstring_#49
beichensinn May 7, 2021
891ad9a
fix lint, mypy, formatting
adekusar-drl May 7, 2021
7473f31
Merge branch 'main' into extend_loss_docstring_#49
adekusar-drl May 12, 2021
b37fd66
Merge branch 'main' into extend_loss_docstring_#49
adekusar-drl May 12, 2021
063cb7b
Merge branch 'main' into extend_loss_docstring_#49
beichensinn May 20, 2021
79324d7
Merge branch 'main' into extend_loss_docstring_#49
beichensinn May 24, 2021
b93787f
Merge branch 'main' into extend_loss_docstring_#49
beichensinn May 26, 2021
10acb36
change predict and target type to Union[int, np.ndarray]
beichensinn May 26, 2021
cfde2eb
add typing library
beichensinn May 26, 2021
f51660c
swap numpy and typing library
beichensinn May 26, 2021
1dc3354
covert int to np.array
beichensinn May 26, 2021
2312963
covert int to np.array
beichensinn May 26, 2021
b223742
black
beichensinn May 26, 2021
9e40911
fixing int
beichensinn May 27, 2021
f1a086f
Merge branch 'main' into extend_loss_docstring_#49
adekusar-drl May 27, 2021
c4da0d3
parameter and return type added as per Steve's comments
beichensinn May 27, 2021
c1fc4d3
Merge remote-tracking branch 'origin/extend_loss_docstring_#49' into …
beichensinn May 27, 2021
da7d902
black
beichensinn May 27, 2021
d0d1362
Merge branch 'main' into extend_loss_docstring_#49
adekusar-drl Jun 1, 2021
07e2fae
Merge branch 'main' into extend_loss_docstring_#49
adekusar-drl Jun 2, 2021
3d98473
Merge branch 'main' into extend_loss_docstring_#49
beichensinn Jun 3, 2021
a695cfe
Merge branch 'main' into extend_loss_docstring_#49
adekusar-drl Jun 3, 2021
6f2ea8e
Merge branch 'main' of https://github.com/Qiskit/qiskit-machine-learn…
adekusar-drl Jun 15, 2021
1253dcd
update docstrings
adekusar-drl Jun 16, 2021
a3cc6d6
Merge branch 'main' of https://github.com/Qiskit/qiskit-machine-learn…
adekusar-drl Jun 16, 2021
93bf18a
fix style/lint/mypy
adekusar-drl Jun 16, 2021
668ca6d
add blank lines
adekusar-drl Jun 16, 2021
666d370
fix spell
adekusar-drl Jun 16, 2021
b36af50
remove unsupported tests
adekusar-drl Jun 16, 2021
a8c26dc
Merge branch 'main' into extend_loss_docstring_#49
adekusar-drl Jun 16, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 148 additions & 20 deletions qiskit_machine_learning/utils/loss_functions/loss_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,57 @@

class Loss(ABC):
"""
Abstract base class for Loss.
Abstract base class for computing Loss.
"""

def __call__(self, predict, target):
def __call__(self, predict: np.ndarray, target: np.ndarray):
"""
Args:
predict: a numpy array of predicted values using the model
target: a numpy array of the true values

Returns:
a float value of the loss function
"""
return self.evaluate(predict, target)

@abstractmethod
def evaluate(self, predict, target):
""" evaluate """
def evaluate(self, predict: np.ndarray, target: np.ndarray):
Copy link
Member

Choose a reason for hiding this comment

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

I see in the derived classes they document this returning a float and the meaning of the float. In which case I think the signature here should be updated to return a float ie def evaluate(self, predict: np.ndarray, target: np.ndarray) -> float: and document here the meaning of that return.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated

Copy link
Member

Choose a reason for hiding this comment

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

For both evaluate and gradient we expect them to return a float right so the caller of a concrete sub-class knows what to expect. Hence in the abstract methods here the signature should show the float return typehint and the Returns in the docstring document the meaning of the float. The docstrings here would then be complete meaning no docstring should be needed on the derived classes - which at present all have a copy/paste of the same Returns text when they should all just simply have no docstring i.e inherit the one from the base class.

Copy link
Member

@woodsp-ibm woodsp-ibm May 13, 2021

Choose a reason for hiding this comment

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

Suggested change
def evaluate(self, predict: np.ndarray, target: np.ndarray):
def evaluate(self, predict: np.ndarray, target: np.ndarray) -> float:

The typehint should show it returns a float

Also there should be a Returns: entry in the docstring that states the meaning of the return value

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated

"""
Args:
predict: a numpy array of predicted values using the model
target: a numpy array of the true values

Copy link
Member

Choose a reason for hiding this comment

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

It would be good to have a Returns here to state what the float return is

Copy link
Collaborator

Choose a reason for hiding this comment

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

It would be good to have a Returns here to state what the float return is

Added a Returns section.

An abstract method for evaluating the loss function
beichensinn marked this conversation as resolved.
Show resolved Hide resolved
"""
raise NotImplementedError

@abstractmethod
def gradient(self, predict, target):
""" gradient """
def gradient(self, predict: np.ndarray, target: np.ndarray):
Copy link
Member

Choose a reason for hiding this comment

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

Similar comment on return value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
def gradient(self, predict: np.ndarray, target: np.ndarray):
def gradient(self, predict: np.ndarray, target: np.ndarray) -> float:

The typehint should show the return type

And likewise there should be a Returns: entry in the docstring that states the meaning of the return value

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated

"""
Args:
predict: a numpy array of predicted values using the model
target: a numpy array of the true values

Copy link
Member

Choose a reason for hiding this comment

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

Again it would be good to have a Returns statement here for the float return

Copy link
Collaborator

Choose a reason for hiding this comment

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

Again it would be good to have a Returns statement here for the float return

Same here, added Returns.

An abstract method for computing the gradient
"""
raise NotImplementedError

@staticmethod
def _validate(predict, target):
def _validate(predict: np.ndarray, target: np.ndarray):
woodsp-ibm marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

@woodsp-ibm woodsp-ibm May 13, 2021

Choose a reason for hiding this comment

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

Suggested change
def _validate(predict: np.ndarray, target: np.ndarray):
def _validate(predict: np.ndarray, target: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:

The typehint should have the return type. The docstring has a Returns entry so thats good. Tuple will need importing from typing though for this.

I must admit this method is kinda confusing. If it just needs to validate the shapes why does it need to return them since the caller already has them? Now I see it makes a numpy array of them, but then the typehint says it takes a numpy array and not something like a list where I could see it adjusting the types as needed as well as doing some validation, with the (possibly) adjusted values returned,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated

"""
Args:
predict: a numpy array of predicted values using the model
target: a numpy array of the true values

Raise:
QiskitMachineLearningError: shapes of predict and target do not match

Returns:
predict: a numpy array of predicted values using the model
target: a numpy array of the true values
"""

predict = np.array(predict)
target = np.array(target)
if predict.shape != target.shape:
Expand All @@ -47,9 +80,24 @@ def _validate(predict, target):


class L1Loss(Loss):
""" L1Loss """
"""
L1Loss:
Copy link
Member

Choose a reason for hiding this comment

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

The name of the class will be in the docs from the class itself, it does not need to be put into the docstring as it will not really add any value. All it needs is the description of what this class is about ie. the L1Loss: bit can simply be removed. Same in the other classes in this module.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The name of the class will be in the docs from the class itself, it does not need to be put into the docstring as it will not really add any value. All it needs is the description of what this class is about ie. the L1Loss: bit can simply be removed. Same in the other classes in this module.

Agree, docstrings are updated.

This class computes the L1 loss: sum |target - predict|
Copy link
Member

Choose a reason for hiding this comment

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

Duplicating the name of the class into the docstring is not necessary; It should be a simple sentence summary, as you have there already, followed by further paragraph(s) of text describing things for more complicated classes that need it = which I do not think is the case here.

Suggested change
L1Loss:
This class computes the L1 loss: sum |target - predict|
This class computes the L1 loss: sum |target - predict|

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated

"""

def evaluate(self, predict: np.ndarray, target: np.ndarray):
Copy link
Member

Choose a reason for hiding this comment

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

When overridding a base class, where we have (and should have) documentation on base class method, there is no need to duplicate/repeat it on derived classes. The base class documentation is inherited via Sphinx which means it can be updated/managed in one place rather than having to deal with copies of it all over the place in derived classes. I think we can say in the base class that it Raises: QiskitMachineLearingError if the shape is incorrect or if used for a classification it does not support, e.g it can only handle binary but is given multiclass

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated

"""
Args:
predict: a numpy array of predicted values using the model
target: a numpy array of the true values

Returns:
a float value of the loss function

def evaluate(self, predict, target):
Raise:
QiskitMachineLearningError: the shape of predict is incorrect

"""
predict, target = self._validate(predict, target)

if len(predict.shape) == 0:
Expand All @@ -61,16 +109,40 @@ def evaluate(self, predict, target):
else:
raise QiskitMachineLearningError(f'Invalid shape {predict.shape}!')

def gradient(self, predict, target):
def gradient(self, predict: np.ndarray, target: np.ndarray):
"""
Args:
predict: a numpy array of predicted values using the model
target: a numpy array of the true values

Returns:
a float value of the gradient
"""

predict, target = self._validate(predict, target)

return np.sign(predict - target)


class L2Loss(Loss):
""" L2Loss """
"""
L2Loss:
This class computes the L2 loss: sum (target - predict)^2
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
L2Loss:
This class computes the L2 loss: sum (target - predict)^2
This class computes the L2 loss: sum (target - predict)^2

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated

"""

def evaluate(self, predict: np.ndarray, target: np.ndarray):
"""
Args:
predict: a numpy array of predicted values using the model
target: a numpy array of the true values

Returns:
a float value of the loss function

Raise:
QiskitMachineLearningError: the shape of predict is incorrect

def evaluate(self, predict, target):
"""
predict, target = self._validate(predict, target)

if len(predict.shape) <= 1:
Expand All @@ -80,31 +152,78 @@ def evaluate(self, predict, target):
else:
raise QiskitMachineLearningError(f'Invalid shape {predict.shape}!')

def gradient(self, predict, target):
def gradient(self, predict: np.ndarray, target: np.ndarray):
"""
Args:
predict: a numpy array of predicted values using the model
target: a numpy array of the true values

Returns:
a float value of the gradient
"""
predict, target = self._validate(predict, target)

return 2 * (predict - target)


class CrossEntropyLoss(Loss):
""" CrossEntropyLoss """
"""
CrossEntropyLoss:
This class computes the cross entropy loss: -sum target * log(predict)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
CrossEntropyLoss:
This class computes the cross entropy loss: -sum target * log(predict)
This class computes the cross entropy loss: -sum target * log(predict)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated

"""

def evaluate(self, predict: np.ndarray, target: np.ndarray):
"""
Args:
predict: a numpy array of predicted values using the model
target: a numpy array of the true values

def evaluate(self, predict, target):
Returns:
a float value of the loss function

"""
predict, target = self._validate(predict, target)

return -np.sum([target[i] * np.log2(predict[i]) for i in range(len(predict))])

def gradient(self, predict, target):
""" Assume softmax is used, and target vector may or may not be one-hot encoding"""
def gradient(self, predict: np.ndarray, target: np.ndarray):
"""
Assume softmax is used, and target vector may or may not be one-hot encoding

Args:
predict: a numpy array of predicted values using the model
target: a numpy array of the true values

Returns:
a float value of the gradient

"""
predict, target = self._validate(predict, target)

return predict * np.sum(target) - target


class CrossEntropySigmoidLoss(Loss):
"""This is used for binary classification"""
"""
CrossEntropySigmoidLoss:
This class computes the cross entropy sigmoid loss.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
CrossEntropySigmoidLoss:
This class computes the cross entropy sigmoid loss.
This class computes the cross entropy sigmoid loss.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated


This is used for binary classification.
"""

def evaluate(self, predict: np.ndarray, target: np.ndarray):
"""
Args:
predict: a numpy array of predicted values using the model
target: a numpy array of the true values

def evaluate(self, predict, target):
Returns:
a float value of the loss function

Raise:
QiskitMachineLearningError: Sigmoid Cross Entropy is used for binary classification!

"""
predict, target = self._validate(predict, target)

if len(set(target)) != 2:
Expand All @@ -114,7 +233,16 @@ def evaluate(self, predict, target):
x = CrossEntropyLoss()
return 1. / (1. + np.exp(-x.evaluate(predict, target)))

def gradient(self, predict, target):
def gradient(self, predict: np.ndarray, target: np.ndarray):
"""
Args:
predict: a numpy array of predicted values using the model
target: a numpy array of the true values

Returns:
a float value of the gradient

"""
predict, target = self._validate(predict, target)

return target * (1. / (1. + np.exp(-predict)) - 1) + (1 - target) * (
Expand Down