• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

qiskit-community / qiskit-machine-learning / 17439516114

03 Sep 2025 04:15PM UTC coverage: 90.603% (+0.006%) from 90.597%
17439516114

push

github

web-flow
Update trainable_model.py to assign callback function to non-scipy optimiser (#976)

* Update trainable_model.py to assign callback function to non-scipy optimizer

A simple fix for #893

* Update trainable_model.py

* Fix copyright

---------

Co-authored-by: Edoardo Altamura <38359901+edoaltamura@users.noreply.github.com>

4628 of 5108 relevant lines covered (90.6%)

0.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.38
/qiskit_machine_learning/algorithms/trainable_model.py
1
# This code is part of a Qiskit project.
2
#
3
# (C) Copyright IBM 2021, 2025.
4
#
5
# This code is licensed under the Apache License, Version 2.0. You may
6
# obtain a copy of this license in the LICENSE.txt file in the root directory
7
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8
#
9
# Any modifications or derivative works of this code must retain this
10
# copyright notice, and modified files need to carry a notice indicating
11
# that they have been altered from the originals.
12
"""A base ML model with a Scikit-Learn like interface."""
13
from __future__ import annotations
1✔
14

15
from abc import abstractmethod
1✔
16
from typing import Callable
1✔
17
import numpy as np
1✔
18

19
from qiskit_machine_learning import QiskitMachineLearningError
1✔
20

21
from .objective_functions import ObjectiveFunction
1✔
22
from .serializable_model import SerializableModelMixin
1✔
23
from ..optimizers import Optimizer, SciPyOptimizer, SLSQP, OptimizerResult, Minimizer
1✔
24
from ..utils import algorithm_globals
1✔
25
from ..neural_networks import NeuralNetwork
1✔
26
from ..utils.loss_functions import (
1✔
27
    Loss,
28
    L1Loss,
29
    L2Loss,
30
    CrossEntropyLoss,
31
)
32

33

34
class TrainableModel(SerializableModelMixin):
1✔
35
    """Base class for ML model that defines a scikit-learn-like interface for `Estimator` instances."""
36

37
    # pylint: disable=too-many-positional-arguments
38
    def __init__(
1✔
39
        self,
40
        neural_network: NeuralNetwork,
41
        loss: str | Loss = "squared_error",
42
        optimizer: Optimizer | Minimizer | None = None,
43
        warm_start: bool = False,
44
        initial_point: np.ndarray = None,
45
        callback: Callable[[np.ndarray, float], None] | None = None,
46
    ):
47
        """
48
        Args:
49
            neural_network: An instance of a quantum neural network. If the neural network has a
50
                one-dimensional output, i.e., `neural_network.output_shape=(1,)`, then it is
51
                expected to return values in [-1, +1] and it can only be used for binary
52
                classification. If the output is multidimensional, it is assumed that the result
53
                is a probability distribution, i.e., that the entries are non-negative and sum up
54
                to one. Then there are two options, either one-hot encoding or not. In case of
55
                one-hot encoding, each probability vector resulting a neural network is considered
56
                as one sample and the loss function is applied to the whole vector. Otherwise, each
57
                entry of the probability vector is considered as an individual sample and the loss
58
                function is applied to the index and weighted with the corresponding probability.
59
            loss: A target loss function to be used in training. Default is `squared_error`,
60
                i.e. L2 loss. Can be given either as a string for 'absolute_error' (i.e. L1 Loss),
61
                'squared_error', 'cross_entropy', or as a loss function
62
                implementing the Loss interface.
63
            optimizer: An instance of an optimizer or a callable to be used in training.
64
                Refer to :class:`~qiskit_machine_learning.optimizers.Minimizer` for more information on
65
                the callable protocol. When `None` defaults to
66
                :class:`~qiskit_machine_learning.optimizers.SLSQP`.
67
            warm_start: Use weights from previous fit to start next fit.
68
            initial_point: Initial point for the optimizer to start from.
69
            callback: A reference to a user's callback function that has two parameters and
70
                returns ``None``. The callback can access intermediate data during training.
71
                On each iteration an optimizer invokes the callback and passes current weights
72
                as an array and a computed value as a float of the objective function being
73
                optimized. This allows to track how well optimization / training process is going on.
74
        Raises:
75
            QiskitMachineLearningError: unknown loss, invalid neural network
76
        """
77
        self._neural_network = neural_network
1✔
78
        if len(neural_network.output_shape) > 1:
1✔
79
            raise QiskitMachineLearningError("Invalid neural network output shape!")
×
80
        if isinstance(loss, Loss):
1✔
81
            self._loss = loss
1✔
82
        else:
83
            loss = loss.lower()
1✔
84
            if loss == "absolute_error":
1✔
85
                self._loss = L1Loss()
1✔
86
            elif loss == "squared_error":
1✔
87
                self._loss = L2Loss()
1✔
88
            elif loss == "cross_entropy":
1✔
89
                self._loss = CrossEntropyLoss()
1✔
90
            else:
91
                raise QiskitMachineLearningError(f"Unknown loss {loss}!")
×
92

93
        # call the setter that has some additional checks
94
        if optimizer is not None and not isinstance(optimizer, SciPyOptimizer):
1✔
95
            if hasattr(optimizer, "callback"):
1✔
96
                optimizer.callback = callback
1✔
97
        self.optimizer = optimizer
1✔
98

99
        self._warm_start = warm_start
1✔
100
        self._fit_result: OptimizerResult | None = None
1✔
101
        self._initial_point = initial_point
1✔
102
        self._callback = callback
1✔
103

104
    @property
1✔
105
    def neural_network(self):
1✔
106
        """Returns the underlying neural network."""
107
        return self._neural_network
×
108

109
    @property
1✔
110
    def loss(self):
1✔
111
        """Returns the underlying neural network."""
112
        return self._loss
×
113

114
    @property
1✔
115
    def optimizer(self) -> Optimizer | Minimizer:
1✔
116
        """Returns an optimizer to be used in training."""
117
        return self._optimizer
1✔
118

119
    @optimizer.setter
1✔
120
    def optimizer(self, optimizer: Optimizer | Minimizer | None = None):
1✔
121
        """Sets the optimizer to use in training process."""
122
        if optimizer is None:
1✔
123
            optimizer = SLSQP()
1✔
124
        self._optimizer = optimizer
1✔
125

126
    @property
1✔
127
    def warm_start(self) -> bool:
1✔
128
        """Returns the warm start flag."""
129
        return self._warm_start
×
130

131
    @warm_start.setter
1✔
132
    def warm_start(self, warm_start: bool) -> None:
1✔
133
        """Sets the warm start flag."""
134
        self._warm_start = warm_start
1✔
135

136
    @property
1✔
137
    def initial_point(self) -> np.ndarray:
1✔
138
        """Returns current initial point"""
139
        return self._initial_point
×
140

141
    @initial_point.setter
1✔
142
    def initial_point(self, initial_point: np.ndarray) -> None:
1✔
143
        """Sets the initial point"""
144
        self._initial_point = initial_point
×
145

146
    @property
1✔
147
    def weights(self) -> np.ndarray:
1✔
148
        """Returns trained weights as a numpy array. The weights can be also queried by calling
149
        `model.fit_result.x`, but in this case their representation depends on the optimizer used.
150

151
        Raises:
152
            QiskitMachineLearningError: If the model has not been fit.
153
        """
154
        self._check_fitted()
1✔
155
        return np.asarray(self._fit_result.x)
1✔
156

157
    @property
1✔
158
    def fit_result(self) -> OptimizerResult:
1✔
159
        """Returns a resulting object from the optimization procedure. Please refer to the
160
        documentation of the `OptimizerResult
161
        <https://qiskit-community.github.io/qiskit-machine-learning/stubs/qiskit_machine_learning.optimizers.OptimizerResult.html>`_
162
        class for more details.
163

164
        Raises:
165
            QiskitMachineLearningError: If the model has not been fit.
166
        """
167
        self._check_fitted()
1✔
168
        return self._fit_result
1✔
169

170
    @property
1✔
171
    def callback(self) -> Callable[[np.ndarray, float], None] | None:
1✔
172
        """Return the callback."""
173
        return self._callback
×
174

175
    @callback.setter
1✔
176
    def callback(self, callback: Callable[[np.ndarray, float], None] | None) -> None:
1✔
177
        """Set the callback."""
178
        self._callback = callback
×
179

180
    def _check_fitted(self) -> None:
1✔
181
        if self._fit_result is None:
1✔
182
            raise QiskitMachineLearningError("The model has not been fitted yet")
1✔
183

184
    # pylint: disable=invalid-name
185
    def fit(self, X: np.ndarray, y: np.ndarray) -> TrainableModel:
1✔
186
        """
187
        Fit the model to data matrix X and target(s) y.
188

189
        Args:
190
            X: The input data.
191
            y: The target values.
192

193
        Returns:
194
            self: returns a trained model.
195

196
        Raises:
197
            QiskitMachineLearningError: In case of invalid data (e.g. incompatible with network)
198
        """
199
        if not self._warm_start:
1✔
200
            self._fit_result = None
1✔
201

202
        self._fit_result = self._fit_internal(X, y)
1✔
203
        return self
1✔
204

205
    @abstractmethod
1✔
206
    # pylint: disable=invalid-name
207
    def _fit_internal(self, X: np.ndarray, y: np.ndarray) -> OptimizerResult:
1✔
208
        raise NotImplementedError
×
209

210
    @abstractmethod
1✔
211
    def predict(self, X: np.ndarray) -> np.ndarray:
1✔
212
        """
213
        Predict using the network specified to the model.
214

215
        Args:
216
            X: The input data.
217
        Raises:
218
            QiskitMachineLearningError: Model needs to be fit to some training data first
219
        Returns:
220
            The predicted classes.
221
        """
222
        raise NotImplementedError
×
223

224
    @abstractmethod
1✔
225
    # pylint: disable=invalid-name
226
    def score(self, X: np.ndarray, y: np.ndarray, sample_weight: np.ndarray | None = None) -> float:
1✔
227
        """
228
        Returns a score of this model given samples and true values for the samples. In case of
229
        classification this should be mean accuracy, in case of regression the coefficient of
230
        determination :math:`R^2` of the prediction.
231

232
        Args:
233
            X: Test samples.
234
            y: True values for ``X``.
235
            sample_weight: Sample weights. Default is ``None``.
236

237
        Returns:
238
            a float score of the model.
239
        """
240
        raise NotImplementedError
×
241

242
    def _choose_initial_point(self) -> np.ndarray:
1✔
243
        """Choose an initial point for the optimizer. If warm start is set and the model is
244
        already trained then use a fit result as an initial point. If initial point is passed,
245
        then use this value, otherwise pick a random location.
246

247
        Returns:
248
            An array as an initial point
249
        """
250
        if self._warm_start and self._fit_result is not None:
1✔
251
            self._initial_point = self._fit_result.x  # type: ignore[assignment]
1✔
252
        elif self._initial_point is None:
1✔
253
            self._initial_point = algorithm_globals.random.random(self._neural_network.num_weights)
1✔
254
        return self._initial_point
1✔
255

256
    def _get_objective(
1✔
257
        self,
258
        function: ObjectiveFunction,
259
    ) -> Callable:
260
        """
261
        Wraps the given `ObjectiveFunction` to add callback calls, if `callback` is not None, along
262
        with evaluating the objective value. Returned objective function is passed to
263
        `Optimizer.minimize()`.
264
        Args:
265
            function: The objective function whose objective is to be evaluated.
266

267
        Returns:
268
            Objective function to evaluate objective value and optionally invoke callback calls.
269
        """
270
        if self._callback is None:
1✔
271
            return function.objective
1✔
272

273
        def objective(objective_weights):
1✔
274
            objective_value = function.objective(objective_weights)
1✔
275
            if isinstance(self._optimizer, SciPyOptimizer):
1✔
276
                self._callback(objective_weights, objective_value)
1✔
277
            return objective_value
1✔
278

279
        return objective
1✔
280

281
    def _minimize(self, function: ObjectiveFunction) -> OptimizerResult:
1✔
282
        """
283
        Minimizes the objective function.
284

285
        Args:
286
            function: a function to minimize.
287

288
        Returns:
289
            An optimization result.
290
        """
291
        objective = self._get_objective(function)
1✔
292

293
        initial_point = self._choose_initial_point()
1✔
294
        if callable(self._optimizer):
1✔
295
            optimizer_result = self._optimizer(  # type: ignore[call-arg]
1✔
296
                fun=objective, x0=initial_point, jac=function.gradient
297
            )
298
        else:
299
            optimizer_result = self._optimizer.minimize(
1✔
300
                fun=objective,
301
                x0=initial_point,
302
                jac=function.gradient,  # type: ignore[arg-type]
303
            )
304
        return optimizer_result
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc