Model fitting using IRLS and variants
Project description
GNC Smoothie package
- M-estimation
- The Welsch influence function
- Iteratively reweighted least squares
- Supervised Gauss-Newton algorithm
- GNC Smoothie software
- Designing your own GNC-Smoothie model classes
Python library supporting M-estimation using two algorithms: the well-known Iteratively Reweighted Least Squares (IRLS)
and our custom Supervised Gauss-Newton algorithm. Author: Philip McLauchlan philipmclauchlan6@gmail.com.
First some introductory theory.
M-estimation
M-estimation is a generalisation of maximum-likelihood estimation. We assume a known population probability density function (PDF) $ f(.) $, parametrised by a vector of parameters $ {\bf x} $, and a set of independent and identically distributed data $ {\bf z}_i $, $ i=1,...,n $ sampled from the population. The general model for the observations is $$ {\bf z}_i = {\bf h}_i({\bf x}) + \text{noise} $$ for some observation model function $ {\bf h}_i({\bf x}) $. The distribution of the noise determined by the population PDF, which is defined as some function $$ f({\bf h}_i({\bf x}) - {\bf z}_i) = f({\bf r}_i) $$ defining the $ i $'th data error or "residual" vector $ {\bf r}_i $ as $$ {\bf r}_i({\bf x}) = {\bf h}_i({\bf x}) - {\bf z}_i $$
For instance for Normal distributed observation errors with standard deviation $ \sigma $ we would have $$ f({\bf r}_i) = ,\mathrm{e}^{-\frac{|| {\bf r}_i ||^2}{2\sigma^2}} $$
The maximum likelihood estimator of $ {\bf x} $ can be computed as
$$ \widehat{\bf x} = \underset{{\bf x}}{\text{arg},\text{max}} \left( \prod_{i=1}^n f({\bf r}_i({\bf x})) \right) $$
or equivalently, $$ \widehat{\bf x} = \underset{{\bf x}}{\text{arg},\text{min}} \left( \sum_{i=1}^n - \log f({\bf r}_i({\bf x})) \right) $$
M-estimation generalises this method by substituting a different function into the above sum, so we instead compute $$ \widehat{\bf x} = \underset{{\bf x}}{\text{arg},\text{min}} \left( \sum_{i=1}^n \rho(|| {\bf r}_i({\bf x}) ||) \right) $$ for some function $ \rho(r_i) $ where $$ r_i = || {\bf r}_i({\bf x}) || $$
We write the objective function above as
$$ F({\bf x}) = \sum_{i=1}^n \rho(|| {\bf r}_i({\bf x}) ||) $$
or $$ F({\bf x}) = \sum_{i=1}^n \rho(r_i({\bf x})) $$
In the special case of normally distributed observation errors, this give rise to standard least-squares, $ \rho(r) \sim r^2 $, the squared error in the observations. The development and popularisation of M-estimation was driven by the need to fit models to data with outliers, i.e. data not sampled from the population pdf but from a distinct distribution or distributions. When outliers are present the least-squares method breaks down because single outliers can have a huge influence, leading to a wildly incorrect value for $ \widehat{\bf x} $. To allow for outliers $ \rho(r) $ is shaped by reducing the value of $ \rho(r) $ for large $ r $ error values. The choice of influence function $ \psi(r) = d\rho(r)/dr $ is driven by a trade-off between the desire to provide a good accuracy in the resulting estimate for $ \widehat{\bf x} $ while providing robustness to noise in the data. For instance, instead of the quadratic function required for least-squares, the pseudo-Huber influence function [1] is asymptotically linear in order to provide some level of robustness.
The Welsch influence function
Redescending influence functions have the property that their gradient tends to zero at either ends of the range. This allows them to be robust to outliers with large errors. On the negative side, redescending influence functions have the problem that the objective function above minimised by M-estimation may have multiple local minima. It is difficult to ensure that the global minimum is reached. When the standard method "iteratively reweighted least-squares" (IRLS) is used, the result will depend on the quality of the initial value of $ {\bf x} $ used for the iteration.
We start with the Welsch influence function [2]. This uses a negative Gaussian: $$ \rho(r) = \frac{\sigma^2}{2} \left( 1 - ,\mathrm{e}^{-\frac{r^2}{2\sigma^2}} \right) $$ $$ \psi(r) = \frac{d\rho(r)}{dr} = \frac{r}{2} ,\mathrm{e}^{-\frac{r^2}{2\sigma^2}} $$ where the width $ \sigma $ of the Gaussian is known as the "wavelength" of the Welsch influence function. Using a Gaussian influence function, whose gradient tends to zero for large errors, ensures robustness to large errors, because their influence on the solution will be very small. However in general it is presumed in the literature that solving M-estimation using redescending influence functions requires a good initial estimate of the solution and comes with no guarantees of convergence. Recent work [3] in IRLS using Graduated-Non-Convextity [4] has clarified that this is not always the case, and for many practical problems we can achieve the global optimum solution without any initial model estimate being provided.
Iteratively reweighted least squares
IRLS is the standard technique used to solve the non-linear optimisation problems that arise in M-estimation using robust influence functions. IRLS assumes that the non-robust least-squares solution for $ {\bf x} $ is soluble in closed form, given some "weights" assigned to the data points. In other words there is a (simple) algorithm that can be used to solve the optimisation problem
$$ \widehat{\bf x} = \underset{{\bf x}}{\text{arg},\text{min}} \left( \sum_{i=1}^n w_i || {\bf h}_i({\bf x}) - {\bf z}_i ||^2)\right) $$
or $$ \widehat{\bf x} = \underset{{\bf x}}{\text{arg},\text{min}} \left( F_{\text{LS}}({\bf x}) \right) $$
where $$ F_{\text{LS}}({\bf x}) = \sum_{i=1}^n w_i r_i({\bf x})^2 = \sum_{i=1}^n w_i ||{\bf h}_i({\bf x}) - {\bf z}_i||^2 $$
for weights $ w_i $. IRLS is based on the observation that the solution $ \widehat{\bf x} $ must be a stationary point of the objective function $ F_{\text{LS}} $ in the above equation, so we must have
$$ \frac{dF_{\text{LS}}}{d{\bf x}} = \sum_{i=1}^n w_i r_i \frac{dr_i}{d{\bf x}} = {\bf 0} $$
The stationary point condition for solving the original optimisation problem for $ \widehat{\bf x} $ is the similar equation $$ \frac{dF}{d{\bf x}} = \sum_{i=1}^n \frac{d\rho(r_i)}{d{\bf x}} = \sum_{i=1}^n \frac{d\rho(r_i)}{dr_i}\frac{dr_i}{d{\bf x}} = \sum_{i=1}^n \psi(r_i)\frac{dr_i}{d{\bf x}} = {\bf 0} $$
Comparing the equations involving $ \frac{dF_{\text{LS}}}{d{\bf x}} $ and $ \frac{dF}{d{\bf x}} $ above, we see that the appropriate weight to use is $$ w_i = \frac{1}{r_i} \psi(r_i) $$
This choice will ensure that solving for $ \frac{dF_{\text{LS}}}{d{\bf x}} $ will also solve for $ \frac{dF}{d{\bf x}} $ to first order, and hopefully improve the solution. IRLS repeats the following two steps to convergence, given an initial state estimate $ \widehat{\bf x} $:
- Estimate weights using the above equation for $ w_i $ for each data item, to be used when calculating the next value for $ \widehat{\bf x} $. $ r_i $ and its derivative are evaluated at the current solution $ \widehat{\bf x} $.
- Calculate the next estimate for $ \widehat{\bf x} $, using the updated weights $ w_i $ from the previous step.
IRLS normally requires a good initial estimate $ \widehat{\bf x} $ of $ {\bf x} $ to avoid local minimal in the objective function.
Supervised Gauss-Newton algorithm
Now we describe an alternative to IRLS, which we term Supervised Gauss-Newton or Sup-GN
Beginning with the objective function $ F({\bf x}) $, let us assume that we have an existing estimate $ \widehat{\bf x}^{} $ of $ {\bf x} $. We can then try to improve this estimate by solving $$ \frac{dF({\bf x})}{d{\bf x}} = {\bf 0} $$ We then build the first-order approximation to the weighting function $ \psi(r) $ that solves for an improved $ \widehat{\bf x} $: $$ \frac{dF({\bf x})}{d{\bf x}} + \frac{d^2 F}{d{\bf x}^2} (\widehat{\bf x} - \widehat{\bf x}^{}) = {\bf 0} $$
where the derivatives are evaluated at $ {\bf x}=\widehat{\bf x}^{} $, or $$ \sum_{i=1}^n \psi(r_i) \frac{dr_i}{d{\bf x}} + \sum_{i=1}^n \left( \frac{d^2\rho(r_i)}{dr_i^2} \frac{d r_i}{d{\bf x}}^\intercal \frac{d r_i}{d{\bf x}} + \psi(r_i) \frac{d^2 r_i}{d{\bf x}^2} \right) (\widehat{\bf x} - \widehat{\bf x}^{}) = {\bf 0} $$ where $ r_i $ and the derivatives are again evaluated at $ {\bf x}=\widehat{\bf x}^{*} $. Noting the equation for $ r_i $ in the section above, we can write
$$ \frac{dr_i}{d{\bf x}} = \frac{dr_i}{d{\bf r}_i} \frac{d{\bf r}_i}{d{\bf x}} = \frac{1}{r_i} {\bf r}_i^\intercal \frac{d{\bf r}_i}{d{\bf x}} $$
and from this derive
$$ \frac{d^2 r_i}{d{\bf x}^2} = \frac{1}{r_i^3} \left( \frac{d{\bf r}_i}{d{\bf x}} \right)^{\intercal} \left( r_i^2 I - {\bf r}_i {\bf r}_i^\intercal \right) \frac{d{\bf r}_i}{d{\bf x}} + \frac{1}{r_i} {\bf r}_i^{\intercal} \frac{d^2{\bf r}_i}{d{\bf x}^2} $$
We assume that the data error $ {\bf r}_{i} $ is a smooth function of $ {\bf x} $ and so ignore the second derivative term involving $ d^2{\bf r}_i/d{\bf x}^2 $. Substituting the above (without the second term) into the equation for $ \widehat{\bf x} - \widehat{\bf x}^{*} $ above and combining with the equation for $ \frac{dr_i}{d{\bf x}} $ provides the result
$$ \sum_{i=1}^n {\bf a} + (A + B) (\widehat{\bf x} - \widehat{\bf x}^{*}) = {\bf 0} $$
where
$$ {\bf a} = \sum_{i=1}^n \frac{1}{r_i} \psi(r_i) {\bf r}_i^\intercal \frac{d{\bf r}_i}{d{\bf x}} $$
$$ A = \sum_{i=1}^n \frac{1}{r_i} \psi(r_i) \left(\frac{d{\bf r}_i}{d{\bf x}}\right)^\intercal \frac{d{\bf r}_i}{d{\bf x}} $$
$$ B = \sum_{i=1}^n \frac{1}{r_i^3} \left(r_i\frac{d^2\rho}{dr_i^2} - \psi(r_i) \right) \left(\frac{d{\bf r}_i}{d{\bf x}}\right)^\intercal {\bf r}_i {\bf r}_i^\intercal \frac{d{\bf r}_i}{d{\bf x}} $$
For the Welsch influence function we obtain
$$ \frac{1}{r_i} \psi(r_i) = \frac{1}{2} ,\mathrm{e}^{-\frac{r^2}{2\sigma^2}} $$
$$ \frac{1}{r_i^3} \left(r_i\frac{d^2\rho}{dr_i^2} - \psi(r_i) \right) = -\frac{1}{2\sigma^2} ,\mathrm{e}^{-\frac{r^2}{2\sigma^2}} $$
We can solve the Gauss-Newton update equations to provide updated parameters $ \widehat{\bf x} $ given residuals, derivatives and hence matrices $ A $, $ B $ evaluated at the previous parameters $ \widehat{\bf x}^{} $. However a direct Gauss-Newton iteration gives no guarantee of convergence. We propose the following "damped" Gauss-Newton updates, in the manner of Levenberg-Marquardt [5] damping: $$ \sum_{i=1}^n \psi(r_i) \frac{dr_i}{d{\bf x}} + (A + \lambda B) (\widehat{\bf x} - \widehat{\bf x}^{}) = {\bf 0} $$
where $ \lambda $ in the range $ [0,1] $ is a damping factor. When $ \lambda=1 $ (no damping) we have a pure Gauss-Newton update. When $ \lambda=0 $ (maximum damping), we apply an update that is exactly equivalent to IRLS for linear data models (proof omitted). As a result we can treat the extreme value $ \lambda=0 $ as a "safe" iteration that will guarantee, at least for linear models, a convergent update. The Sup-GN algorithm then proceeeds as follows:
First initialize $ \widehat{\bf x}^{*} $ in the same way as IRLS (least-squares solution with weights $ w_i $ set to one), and set $ \lambda=1 $. Then given a damping adjustment factor $ k<1 $:
- Solve the damped Gauss-Newton update equation above to produce an updated estimate $ \widehat{\bf x} $.
- Check the objective function $ F() $ evaluated at $ \widehat{\bf x}^{*} $ and $ \widehat{\bf x} $.
If we managed to improve the objective function, we can reduce the damping, otherwise we need to reject the new
estimate and increase the damping:
- If $ F(\widehat{\bf x}) < F(\widehat{\bf x}^{}) $, set $ \lambda \leftarrow k\lambda $ and $ \widehat{\bf x}^{} \leftarrow \widehat{\bf x} $.
- Else set $ \lambda \leftarrow \min(1,\frac{\lambda}{k}) $.
- Iterate to convergence.
The advantage of this algorithm over IRLS is that it provides much faster convergence when we are near the solution. It is well known that Gauss-Newton iterations can provide quadratic convergence [6], and we are taking advantage of this, whilst still maintaining the option of pure IRLS iterations to guarantee convergence.
GNC Smoothie software
- IRLS class
- Supervised Gauss-Newton class
- Example code for the IRLS and Sup-GN classes
- Base class for IRLS and Sup-GN algorithms
- Accelerated Robust Linear regression
- Alternative API for Robust Linear regression
The Python library is based on numpy and contains the following top-level modules:
IRLS class
Implementation in irls.py
Top-level 'IRLS' class. Once you have constructed an instance of this class, call the run()
method to run it. This returns True on successful convergence, False on failure.
The final model and model reference (see below) are stored in final_model and final_model_ref,
whether the run() method succeeds or not.
Here are the parameters that need to be passed to the IRLS class constructor. Optional parameters follow.
-
param_instanceDefines the GNC schedule to be followed by IRLS. If GNC is not being used then this can be aGNC_NullParamsinstance imported from gnc_null_params.py. Should have an internalinfluence_func_instancethat specifies the IRLS influence function to be used. The influence_func_instance should provide the following method:summary(self) -> strReturns a string containing the values of the internal parameters.
param_instanceitself should provide the following methods:reset(self, init: bool = True) -> NoneResets the internal influence_func_instance according to the stage of the GNC schedule indicated by the init parameter. If init isTrue, reset to the starting value to prepare for the GNC process to start. If init isFalse, reset to the final stage of GNC.n_steps(self) -> int:Returns the number of steps in the GNC schedule.alpha(self) -> floatReturns the stage reached in the GNC schedule, as a value between zero (start) and one (end)increment(self) -> NoneUpdates the influence_func_instance to the next step in the GNC schedule.
-
dataAn array of data items. Each data item should itself be an array.
Now the optional parameters for the IRLS class constructor:
-
model_instanceA Python-based model being fitted to the data, an instance of a class you design that provides at minimum following methods:cache_model(self, model, model_ref=None) -> NoneUse this method to cache the model, prior to theresidual()method being called on each data item. If the model contains reference parameters e.g. for estimating rotation, these are passed asmodel_ref.residual(self, data_item) -> np.arrayCalculates the residual (error) of thedata_itemgiven the model.
These methods may also be required depending on the model design
residual2(self, data_item) -> np.arrayCalculates the residual (error) of thedata_itemof the type of data passed in thedata2array.residual3(self, data_item) -> np.arrayCalculates the residual (error) of thedata_itemof the type of data passed in thedata3array.linear_model_size(self) -> intReturns the number of parameters in the model if the model is linear. TheBaseIRLSclass uses an internal weighted_fit() method to fit a linear model to the data with specified weights, so that the programmer does not have to implement it. If the model is non-linear, omit this method.weighted_fit(self, data, weight, scale) -> (np.array, np.array)Iflinear_model_size()is not provided, the model is not linear. If a closed-form solution for the best model given the data with weights nevertheless exists, implement it in yout class using this method. Thescalearray indicates that certain data items are less accurate and so have a scale value > 1, indicating that the influence function for that data item should be stretched by the given scale factor. For non-linear problems with no closed-form solution, pass a suitable starting point as themodel_start(and optionally model_ref_start) parameters, see below. In that case theweighted_fit()should be omitted. If second and possibly third types of data item are being used, add argumentsdata2weight2,scale2anddata3,weight3,scale3as appropriate.
-
evaluator_instanceA vectorised evaluator that provides a fast implementation of the model specific to the influence function. It should be a class instance that provides at least the following methods.set_residual_size(self, residual_size: npt.ArrayLike) -> NoneSets the residual size for each data type.update_weights(self, model: npt.ArrayLike, model_ref, influence_func_instance, data: npt.ArrayLike, weight: npt.ArrayLike, scale: npt.ArrayLike, new_weight: npt.ArrayLike) -> NoneUpdate IRLS weights.weighted_fit(self, data: npt.ArrayLike, weight: npt.ArrayLike, scale: npt.ArrayLike) -> np.arrayReturn the model fitted to the data, taking the weights into account. We expect that the implementation of the above vectorised functions will use Cython.
-
weightAn array of float weight values for each data item. If not provided, weights are initialised to one. -
scaleAn array of scale values, indicating that one or more data items are known to have reduced accuracy, i.e. a wider influence function. The scale indicates the stretching to apply to the influence function for that data item. -
data2A second array of data items. Each data item should itself be an array. Use this if you have a second type of data item. -
weight2An array of float weight values for each of the second data itemsdata2. If not provided withdata2, weights are initialised to one -
scale2An array of scale values, indicating that one or more data items indata2are known to have reduced accuracy, i.e. a wider influence function. The scale indicates the stretching to apply to the influence function for that data item. -
data3A third array of data items. Each data item should itself be an array. Use this if you have a third type of data item. -
weight3An array of float weight values for each of the third data itemsdata3. If not provided withdata3, weights are initialised to one -
scale3An array of scale values, indicating that one or more data items indata3are known to have reduced accuracy, i.e. a wider influence function. The scale indicates the stretching to apply to the influence function for that data item. -
numeric_derivs_influence: boolWhether to calculate derivatives of the influence function numerically from a providedrho()method or directly using a providedrhop()method. -
max_niterations: intMaximum number of IRLS iterations to apply before aborting. -
diff_thres: floatTerminate when successful update changes the model model parameters by less than this value. -
messages_file: TextIOFile to print debugging information. -
model_startStarting value for model model parameters. -
model_ref_startStarting reference parameters for model, e.g. if optimising rotation -
debug: boolWhether to add extra debugging data to theIRLSclass instance on exit:debug_n_iterationsThe number of iterations actually applied.debug_model_listA list of the model parameters at each iteration.debug_diffsA list of norm of model parameter changes applied at each iteration, as a list of difference values.debug_diff_alphaA list of alpha values corresponding to the difference valuesdebug_diffs, indicating the GNC stage reached when each model change was applied. Alpha equal to zero indicates the start of the GNC schedule, alpha equal to one indicates the final stage of the GNC schedule.debug_update_weights_timeThe total time spent updating weights in seconds.debug_weighted_fit_timeThe total time spent fitting the model to data in secondsdebug_total_timeThe total time spent in the algorithm in seconds.
Supervised Gauss-Newton class
Implementation at sup_gauss_newton.py
Top-level SupGaussNewton class, an implementation of Supervised Gauss-Newton (Sup-GN).
Sup-GN is an alternative to IRLS most suitable for the two cases:
- Linear model where the data relates to the model via a linear function.
- Non-linear model where there is no closed-form solution to calculating the model parameters from the weighted data.
Use the basic IRLS in the remaining case where a non-trivial closed-form solution for the model
is available, such as 3D point cloud registration (SVD solution). IRLS is not suitable for
non-linear problems where a closed-form solution is not available, but Sup-GN can be used in
such problems so long as a reasonable starting point for the model can be supplied (see the
model_start and model_ref_start parameters below). For linear models Sup-GN provides a simpler
model implementation than IRLS, since the closed-form solution for the model is calculated
internally. Also Sup-GN converges quadratically for linear models when close to the solution.
Once you have constructed an instance of the SupGaussNewton class, call the run()
method to run it. This returns True on successful convergence, False on failure.
The final model and model reference (see below) are stored in final_model and final_model_ref,
whether the run() method succeeds or not.
The parameters to the SupGaussNewton constructor are very similar to the IRLS class,
but there are some twists due to Sup-GN requiring differentiation of the model residual.
Here are the parameters you need to pass to the SupGaussNewton class:
-
param_instanceDefines the GNC schedule to be followed by IRLS. If GNC is not being used then this can be aGNC_NullParamsinstance imported from gnc_null_params.py. Should have an internalinfluence_func_instancethat specifies the IRLS influence function to be used. Thisinfluence_func_instanceshould provide these methods:objective_func_sign(self) -> floatReturns either one or minus one depending on whether the objective function increases for large residuals (one) or decreases (minus one). Typical IRLS objective functions such as Huber and Geman-McClure increase for large residuals, so most functions will return one. The version of Welsch we have implemented in gnc_welsch_params.py uses a nagative sense, which slightly simplifies the implementation, because otherwise we would have to add one to the objective function in order to keep it positive.rho(self, rsqr: float, s: float) -> floatThe objective function givenrsqrThe square of the L2 norm of the residual vectorsThe scale of the data item indicating its known inaccuracy, so a value >= 1. Returns the value of the objective function.
rhop(self, rsqr: float, s: float) -> floatThe influence function, which is equal to the derivative with respect to $ r $ ofrho(rsqr,s)divided by $ r $, where $ r $ is the L2 norm of the residual vector. Ifnumeric_derivs_influenceis set toTrue(see below) then the derivatives are calculated numerically fromrho()andrhop()should be omitted.Bterm(self, rsqr: float, s: float) -> floatImplements $ (r*\rho''(r) - \rho'(r))/(r^3) $ where ' indicates derivative. Ifnumeric_derivs_influenceis set toTrue(see below) then the derivatives are calculated numerically fromrho()andBterm()should be omitted.summary(self) -> strReturns a string containing the values of the internal parameters.
param_instanceitself should provide the following methods:reset(self, init: bool = True) -> NoneResets the internal influence_func_instance according to the stage of the GNC schedule indicated by the init parameter. If init isTrue, reset to the starting value to prepare for the GNC process to start. If init isFalse, reset to the final stage of GNC.n_steps(self) -> int:Returns the number of steps in the GNC schedule.alpha(self) -> floatReturns the stage reached in the GNC schedule, as a value between zero (start) and one (end)increment(self) -> NoneUpdates the influence_func_instance to the next step in the GNC schedule.
-
dataAn array of data items. Each data item should itself be an array.
Now the optional parameters for the SupGaussNewton class constructor:
-
model_instanceA Python-based model being fitted to the data, an instance of a class you design that provides at minimum the following methods:cache_model(self, model, model_ref=None)Use this method to cache the model, prior toresidual()andresidual_gradient()methods being called on each data item. If the model contains reference parameters e.g. for estimating rotation, these are passed asmodel_ref.residual(self, data_item) -> np.arrayCalculates the residual (error) of the data_item given the model.
These methods may also be required depending on the model design
model_is_valid(self, model, model_ref=None)If provided, this function can be used to reject model parameters by returningFalsefor the provided model. For instance, if a model parameter is restricted to be positive, but is updated to have a negative value, you can flag this problem in this method.residual_gradient(self, data_item) -> np.arrayThe Jacobian or derivative matrix of the residual vector with respect to the model parameters. If thenumeric_derivs_modelparameter is set toTrue(see below) then the derivatives are calculated numerically using theresidual()method. In that case omit this method.residual2(self, data_item) -> np.arrayCalculates the residual (error) of thedata_itemof the type of data passed in thedata2array.residual3(self, data_item) -> np.arrayCalculates the residual (error) of thedata_itemof the type of data passed in thedata3array.residual_gradient2(self, data_item) -> np.arrayCalculates the Jacobian of the residual vector for adata_itemof the type of data passed in thedata2array.residual_gradient3(self, data_item) -> np.arrayCalculates the Jacobian of the residual vector for adata_itemof the type of data passed in thedata3array.linear_model_size(self) -> intReturns the number of parameters in the model if the model is linear. TheBaseIRLSclass uses an internalweighted_fit()method to fit a linear model to the data with specified weights, so that the programmer does not have to implement it. If the model is non-linear, omit this method.weighted_fit(self, data, weight, scale) -> (np.array, np.array)Iflinear_model_size()is not provided, the model is not linear. If a closed-form solution for the best model given the data with weights nevertheless exists, implement it in yout class. Thescalearray indicates that certain data items are less accurate and so have a scale value > 1, indicating that the influence function for that data item should be stretched by the given scale factor. For non-linear problems with no closed-form solution, pass a suitable starting point as themodel_start(and optionallymodel_ref_start) parameters, see below. In that case theweighted_fit()method should be omitted. If second and possibly third types of data item are being used, add argumentsdata2weight2,scale2anddata3,weight3,scale3as appropriate.
-
evaluator_instanceA vectorised evaluator that provides a fast implementation of the model specific to the influence function. It should be a class instance that provides at least the following methods.set_residual_size(self, residual_size: npt.ArrayLike) -> NoneSets the residual size for each data type.objective_func(self, model: npt.ArrayLike, model_ref, influence_func_instance, data: npt.ArrayLike, weight: npt.ArrayLike, scale: npt.ArrayLike) -> floatReturns the total Sup-GN objective function evaluated over all data.weighted_derivs(self, model: npt.ArrayLike, model_ref, influence_func_instance, lambda_val: float, data: npt.ArrayLike, weight: npt.ArrayLike, scale: npt.ArrayLike) -> (np.array, np.array)Returns the sums of weighted derivatives used in the Sup-GN algorithm.weighted_fit(self, data: npt.ArrayLike, weight: npt.ArrayLike, scale: npt.ArrayLike) -> np.arrayReturn the model fitted to the data, taking the weights into account. We expect that the implementation of the above vectorised functions will use Cython.
-
weightAn array of float weight values for each data item. If not provided, weights are initialised to one -
scaleAn array of scale values, indicating that one or more data items are known to have reduced accuracy, i.e. a wider influence function. The scale indicates the stretching to apply to the influence function for that data item. -
data2A second array of data items. Each data item should itself be an array. Use this if you have a second type of data item. -
weight2An array of float weight values for each of the second data itemsdata2. If not provided withdata2, weights are initialised to one -
scale2An array of scale values, indicating that one or more data items indata2are known to have reduced accuracy, i.e. a wider influence function. The scale indicates the stretching to apply to the influence function for that data item. -
data3A third array of data items. Each data item should itself be an array. Use this if you have a third type of data item. -
weight3An array of float weight values for each of the third data itemsdata3. If not provided withdata3, weights are initialised to one -
scale3An array of scale values, indicating that one or more data items indata3are known to have reduced accuracy, i.e. a wider influence function. The scale indicates the stretching to apply to the influence function for that data item. -
numeric_derivs_model: boolWhether to calculate derivatives of the data residual vector with respect to the model parameters numerically using a providedresidual()method or directly using a providedresidual_gradient()method. -
numeric_derivs_influence: boolWhether to calculate derivatives of the influence function numerically from a providedrho()method or directly using a providedrhop()method. -
max_niterations: intMaximum number of Sup-GN iterations to apply before aborting -
residual_tolerance: floatAn parameter that is used to terminate Sup-GN when the improvement to the objective function value is smaller than the provided threshold -
lambda_start: floatStarting value for the Sup-GN damping factor $\lambda$, similar to Levenberg-Marquart damping. In Sup-GN the level of damping is high when $\lambda$ is small, so normally it is best to start with an optimistic small value. -
lambda_max: floatMaximum value for $\lambda$ in Sup-GN damping. This should be in the range [0,1]. -
lambda_scale: floatScale factor to multiply $\lambda$ by when an iteration successfully reduces/increases the objective function (depending on the +/- sign specified byparam_instance.influence_func_instance.influence_func_sign(), see above). When the iteration is not successful, the model change is reverted and $ \lambda $ is divided by this factor to increase the damping at the next iteration. -
lambda_thres: floatThreshold for $\lambda$ below which the Sup-GN iteration switches to pure gradient-based updates. -
diff_thres: floatTerminate when successful update changes the model parameters by less than this value. -
messages_file: TextIOFile to print debugging information. -
model_startStarting value for model parameters. -
model_ref_startStarting reference parameters for model, e.g. if optimising rotation. -
debug: boolWhether to add extra debugging data to theSupGaussNewtonclass instance on exit:debug_n_iterationsThe number of iterations actually applied.debug_model_listA list of the model parameters at each iteration.debug_diffsA list of norm of model parameter changes applied at each iteration, as a list of difference values.debug_diff_alphaA list of alpha values corresponding to the difference valuesdebug_diffs, indicating the GNC stage reached when each model change was applied. Alpha equal to zero indicates the start of the GNC schedule, alpha equal to one indicates the final stage of the GNC schedule.debug_weighted_derivs_timeThe total time spend calculating derivatives in seconds.debug_solve_timeThe total time spent solving for the model in seconds.debug_total_timeThe total time spent in the algorithm in seconds.
Example code for the IRLS and Sup-GN classes
The simplest non-trivial example of an IRLS/Sup-GN model class is to support fitting a straight line through 2D data, with the model $ y=ax+b $, where $ a $ is the gradient of the line and $ b $ is the intercept. The model class for line fitting might look like this:
import numpy as np
# Line model is y = a*x + b
class LineFit:
def __init__(self):
pass
# copy model parameters and apply any internal calculations
def cache_model(self, model, model_ref=None) -> None:
self.__a = model[0]
self.__b = model[1]
# r = a*xi + b - yi
def residual(self, data_item) -> np.array:
x = data_item[0]
y = data_item[1]
return np.array([self.__a*x + self.__b - y])
# dr/d(a b) = (x 1)
def residual_gradient(self, data_item) -> np.array:
x = data_item[0]
return np.array([[x, 1.0]])
# return number of parameters in model if the model is linear,
# otherwise omit this method
def linear_model_size(self) -> int:
return 2 # a,b
In this case the data items will have two values each, for $ x,y $. So an example data array could be
data = np.array([[0.0, 0.90], [0.1, 0.95], [0.2, 1.0], [0.3, 1.05], [0.4, 1.1]])
Then the code to build and run IRLS could look like ths.
from gnc_smoothie.sup_gauss_newton import SupGaussNewton
from gnc_smoothie.gnc_null_params import GNC_NullParams
from gnc_smoothie.welsch_influence_func import WelschInfluenceFunc
sigma = 0.2
param_instance = GNC_NullParams(WelschInfluenceFunc(sigma))
model_instance = LineFit()
optimiser_instance = SupGaussNewton(param_instance, data, model_instance=model_instance)
if optimiser_instance.run():
model = optimiser_instance.final_model
print("line a b:",model)
The correct line parameters should be printed:
line a b: [0.5 0.9]
To use Supervised Gauss-Newton instead simply substitute SupGaussNewton for IRLS in the above code.
Base class for IRLS and Sup-GN algorithms
Implementation in base_irls.py
Implements the many features in common between IRLS and Sup-GN. Should not be used directly in your code.
Accelerated Robust Linear regression
Implementation in linear_regressor_welsch.py.
We have implemented a vectorised Cython-based version of robust linear regression. There is also a pure Python-based reference implementation at linear_regressor.py, used mainly for regression testing the Cython implementation. The residual model is $$ {\bf r}i({\bf x}) = \left( \begin{array}{c} {\bf x}1.{\bf z}{i1} + x_1 - z{i1} \ {\bf x}2.{\bf z}{i2} + x_2 - z_{i2} \ ... \ {\bf x}m.{\bf z}{im} + x_m - z_{im} \end{array} \right) $$ where we organise the state estimate as a matrix $$ X = \left( \begin{array}{cc} {\bf x}1^\intercal & x_1 \ {\bf x}2^\intercal & x_2 \ ... & ... \ {\bf x}m^\intercal & x_m \end{array} \right) $$ and $m$ is the dimension of the residual vector. We can write the $i$'th observation in matrix form as $$ Z_i = \left( \begin{array}{cc} {\bf z}{i1}^\intercal & z{i1} \ {\bf z}{i2}^\intercal & z_{i2} \ ... & ... \ {\bf z}{im}^\intercal & z{im} \end{array} \right) $$
To use the vectorised linear regression, incorporating our recommended Welsch influence function [2], you will need the following line:
from gnc_smoothie.linear_model.linear_regressor_welsch import LinearRegressorWelsch
Then you will need to create an linear regression instance with a line like
linear_regressor = LinearRegressorWelsch(1.0, sigma_limit=100.0, num_sigma_steps=20)
The only obligatory parameter is
sigma_base: floatThe final small value of $\sigma$ in the GNC Welsch influence function schedule
Optional parameters are:
sigma_limit: floatThe initial high value of $\sigma$ in the GNC Welsch influence function schedulenum_sigma_steps: intThe number of steps of $\sigma$ in the GNC Welsch influence function schedulemax_niterations: intMaximum number of Sup-GN iterations to apply before aborting.lambda_start: floatStarting value for the Sup-GN damping factor $\lambda$, similar to Levenberg-Marquart damping. In Sup-GN the level of damping is high when $\lambda$ is small, so normally it is best to start with an optimistic small value.lambda_max: floatMaximum value for $\lambda$ in Sup-GN damping. This should be in the range [0,1].lambda_scale: floatScale factor to multiply $\lambda$ by when an iteration successfully reduces/increases the objective function (depending on the +/- sign specified byparam_instance.influence_func_instance.influence_func_sign(), see above). When the iteration is not successful, the model change is reverted and $ \lambda $ is divided by this factor to increase the damping at the next iteration.lambda_thres: floatThreshold for $\lambda$ below which the Sup-GN iteration switches to pure gradient-based updates.diff_thres: floatTerminate when successful update changes the model model parameters by less than this value.use_slow_version: boolWhether to use the slower pure Python implementation.messages_file: TextIOFile to print debugging information.debug: boolWhether to add extra debugging data to theLinearRegressorWelschclass instance on exit:debug_n_iterationsThe number of iterations actually applied.debug_model_listA list of the model parameters at each iteration.debug_diffsA list of norm of model parameter changes applied at each iteration, as a list of difference values.debug_diff_alphaA list of alpha values corresponding to the difference valuesdebug_diffs, indicating the GNC stage reached when each model change was applied. Alpha equal to zero indicates the start of the GNC schedule, alpha equal to one indicates the final stage of the GNC schedule.debug_update_weights_timeThe total time spent updating weights in seconds.debug_solve_timeThe total time spent solving for the model in seconds.debug_total_timeThe total time spent in the algorithm in seconds.
To run the GNC Smoothie linear regressor, create a data array in the format described below and call
if linear_regressor.run(data):
model = linear_regressor.final_model
Mapping the mathematical representation above to the software, the model vector ${\bf x}$ is the rows of $X$ concatenated:
$$
{\bf x} = \left( {\bf x}_1^\intercal ~ x_1 | {\bf x}_2^\intercal ~ x_2 | ... | {\bf x}_m^\intercal ~ x_m \right)^\intercal
$$
This is the vector returned as the final_model by the linear regression software. The data array should
be organised as an array of the $Z_i$ matrices defined above. The data array should therefore
be three-dimensional. If the dimension $m$ of the residual vector is one, we also support
a two-dimensional format for data. In summary, the
data array should have either two or three dimensions. For a two-dimensional data array, the data format is
- First dimension - The number of data items.
- Second dimension - The size of the row of the $Z$ matrix.
A three-dimensional data array has the following dimensions:
- First dimension - The number of data items.
- Second dimension - The size $m$ of the residual vector.
- Third dimension - The size of the row of the $Z$ matrix.
Alternative API for Robust Linear regression
scikit-learn packages
such as RANSACRegressor
separate data and output parameters into "training/target" and "coefficient/intercept" values.
The gnc_smoothie API combines these so only one input data input array is provided and one final_model
output array is generated. To allow for interchange between gnc_smoothie and scikit-learn,
an alternative API for linear_regressor is available whereby the data is provided as a tuple.
You prepare two arrays, such as data_x and data_y, as the "training" and
as you would for RANSACRegressor.
Then you can call linear_regressor in a similar way:
if linear_regressor.run((data_x, data_y)):
coeff = line_fitter.final_coeff
intercept = line_fitter.final_intercept
Essentially data_y is equivalent to the last column of the original data array,
and intercept is the last column of the output final_model in the default API.
Designing your own GNC Smoothie model classes
- Checking derivatives
- Welsch influence function
- GNC Welsch schedule class
- Pseudo-Huber influence function
- Geman-McClure influence function
- GNC IRLS-p influence function
- GNC IRLS-p schedule class
- Quadratic influence function
- Null GNC parameter class
- Using a model reference
Checking derivatives
Implementation in check_derivs.py
When you design a model class to be used for Sup-GN optimisation, and have written your residual()
method defining how to calculate the model/data errors, you have a design choice:
- Implement the
residual_gradient()method yourself, first working out the Jacobian matrix of the residual vector. - Calculating the derivatives numerically. This is handled internally by the
SupGaussNewtonclass. You just need to passnumeric_derivs_model=Truein the arguments to theSupGaussNewtonconstructor.
In the former case, you will want to check that your derivative calculation is correct.
Use the check_derivs() method to do this. It calculates derivatives both ways, first calling
your residual_gradient() method and then calculating them numerically, and compares
them using thresholds. If the error in any derivative is greater than the provided threshold
the method returns False. The required arguments to the method are:
optimiser_instanceAn instance ofSupGaussNewtonbuilt with an instance of your model class. TheSupGaussNewtoninstance also needs aparam_instanceand some data to construct it - see below for an example of how to do this.modelAn example vector of model parameters.
Optional argmuments that may be required:
model_refReference model parameters, for instance if the model contains rotation parameters.diff_threshold_a: floatThreshold used for the terms of the $ {\bf a} $ vector in the Sup-GN iteration.diff_threshold_AlB: floatThreshold used for the terms of the $ A $ and $ B $ matrices in the Sup-GN iteration.print_diffs: boolWhether to print the differences between the analytic and numerical derivative estimates.print_derivs: boolWhether to print all the derivatives being compared.
For example, to test the derivatives of the LineFit class above you could use this code:
from gnc_smoothie.sup_gauss_newton import SupGaussNewton
from gnc_smoothie.gnc_null_params import GNC_NullParams
from gnc_smoothie.quadratic_influence_func import QuadraticInfluenceFunc
from gnc_smoothie.check_derivs import check_derivs
data = [[2.0, -1.0]] # single data point
derivs_good = check_derivs(SupGaussNewton(GNC_NullParams(QuadraticInfluenceFunc()), data, model_instance=LineFit()), [1.0, 2.0], # model a,b
diff_threshold_AlB=1.e-4)
Note that we use the simplest parameter class GNC_NullParams and influence function class QuadraticInfluenceFunc to build
the SupGaussNewton instance, although the derivative testing will work for other classes.
Welsch influence function
Implementation in welsch_influence_func.py
This provides the class WelschInfluenceFunc, that implements the Welsch influence function, defined as
$$
\rho(r) = \frac{\sigma^2}{2} \left( 1 - ,\mathrm{e}^{-\frac{r^2}{2\sigma^2}} \right)
$$
with a single parameter $ sigma $. This is the simplest robust influence function.
We recommend using the Welsch influence function over others because of its simplicity and superior convergence properties
(details to come). The Welsch influence function is redescending, meaning that the gradient tends to zero at either ends of the range.
This provides it with remarkable robustness, even in the presence of very bad outliers.
Wrap it with a GNC_WelschParams class to provide it with a GNC schedule and outstanding convergence.
GNC Welsch schedule class
Implementation in gnc_welsch_params.py
Build a parameter class instance by building a GNC_WelschInfluenceFunc instance and then
wrapping it into this GNC_WelschParams class to provide the GNC schedule. The sigma value
starts at a high value sigma_limit and descends geometrically through num_sigma_steps
values to a low value sigma_base that approximates the population error standard deviation.
Pseudo-Huber influence function
Implementation in pseudo_huber_influence_func.py
The class PseudoHuberInfluenceFunc implements the fully differentiable
version [1] of the original Huber influence function [7].
$$
\rho(r) = \sigma^2 \left( \sqrt{1 + (r/\sigma)^2} - 1 \right)
$$
with a single parameter $ \sigma $. You can use this influence function if you know that your outliers
are relatively small. The Pseudo-Huber objective function $ \rho(r) $ is convex, so you don't
need to use a GNC schedule. Instead use it with GNC_NullParams. On the other hand, if the outliers are
very bad then it will fail to converge to the correct solution.
Geman-McClure influence function
Implementation in geman_mcclure_influence_func.py
This GemanMcClureInfluenceFunc class implements the Geman-McClure influence function [8], defined by
$$
\rho(r) = \frac{r^2}{\sigma^2 + r^2}
$$
with a single parameter $ \sigma $. Geman-McClure is another redescending influence function,
so it is suitable for being embedded in a GNC schedule and handling large outliers. Wrap it in the
same GNC_WelschParams GNC schedule class used for the Welsch influence function if you want
to try this, but we still recommend using the Welsch influence function over Geman-McClure.
GNC IRLS-p influence function
Implementation in gnc_irls_p_influence_func.py
This GNC_IRLSpInfluenceFunc class implements the GNC schedule recommended by Peng et al. in their
excellent paper [3]. Peng et al. prove that for many IRLS problems, GNC IRLS-p provides guaranteed
fast convergence. The influence function is more complex than the above alternatives - see the paper
and the code for details. Combining this class with the GNC schedule class GNC_IRLSpParams (see below)
provides an alternative to our recommended GNC_WelschParams + GNC_WelschInfluenceFunc combination.
GNC IRLS-p schedule class
Implementation in gnc_irls_p_params.py
Build a parameter class instance by building a GNC_IRLSpInfluenceFunc instance and then
wrapping it into this GNC_IRLSpParams class to provide the GNC schedule.
Quadratic influence function
Implementation in quadratic_influence_func.py
This file provides the QuadraticInfluenceFunc class that implements the quadratic objective function that
is used in non-robust least squares. The main use of this class is to provide a simple mechanism for
building an instance of SupGaussNewton for the purpose of checking derivative calculations in the model
residual_gradient() method.
Null GNC parameter class
Implementation in gnc_null_params.py
When you want to apply standard IRLS, or use Sup-GN optimisation without GNC, wrap your influence function
in a GNC_NullParams instance.
Using a model reference
It is usually desirable to model your system with the minimum number of parameters, to avoid redundancy. However this principle is sometimes in conflict with symmetry and gauge invariance issues. The canonical example of this is estimating a model containing a rotation. Consider the 3D point registration problem of calculating the rotation and translation between two point clouds, where the correspondence is known in advance. There is a closed-form solution for this problem [9], but in the case of outliers we should look to implement this in the IRLS and Sup-GN frameworks. To represent rotation minimally, we need to use three parameters. These might be one of the various three-angle representations, or Rodrigues parameters. However any minimal representation will have singularities. Worse than this, the choice of initial coordinate frame will affect the result because of the non-linearities present in any minimal representation of rotation [10].
The solution to this problem is to use a non-minimal rotation representation as a "reference" for estimating small changes. For instance you can use a rotation matrix, and combine it with a small rotation representation such as Rodrigues parameters as implemented in scipy.spatial.transform.Rotation.from_mrp. The idea is that before each IRLS/Sup-GN iteration, the reference rotation matrix is updated to the latest rotation, including the change made at the previous iteration. The residual is calculated by combining the reference rotation with the small rotation change at the current iteration. The residual derivatives are calculated assuming that the small rotation parameters are zero.
The implementation of model reference in gnc_smoothie allows you to control it completely within
your model class. Here is how an implementation of a model class for 3D point cloud registration might look.
import numpy as np
from scipy.spatial.transform import Rotation as Rot
from ls_registration import LS_PointCloudRegistration
class PointRegistration:
def __init__(self):
pass
# copy model parameters and apply any internal calculations
def cache_model(self, model, model_ref=None):
rotd = Rot.from_mrp(-0.25*model[0:3])
self.__R = np.matmul(Rot.as_matrix(rotd), model_ref)
self.__t = model[3:6]
# r = y - R*x - t
# = Rs*R0*x + t, Rs = ( 1 -az ay), R0*x = (R0_xx*x_x + R0_xy*x_y + R0_xz*x_z) = (R0x_x)
# ( az 1 -ax) (R0_yx*x_x + R0_yy*x_y + R0_yz*x_z) (R0x_y)
# (-ay ax 1 ) (R0_zx*x_x + R0_zy*x_y + R0_zz*x_z) (R0x_z)
# where R0x = R0*x
def residual(self, data_item) -> np.array:
x = data_item[0]
y = data_item[1]
return np.array(y - np.matmul(self.__R,x) - self.__t)
# dr ( 0 R0x_z -R0x_y) dr
# -- = (-R0x_z 0 R0x_x) = Rx_x, -- = -I_3x3
# da ( R0x_y -R0x_x 0 ) dt
def residual_gradient(self, data_item) -> np.array:
x = data_item[0]
Rx = np.matmul(self.__R,x)
return np.array([[ 0.0, Rx[2], -Rx[1], -1.0, 0.0, 0.0],
[-Rx[2], 0.0, Rx[0], 0.0, -1.0, 0.0],
[ Rx[1], -Rx[0], 0.0, 0.0, 0.0, -1.0]])
def update_model_ref(self, model, prev_model_ref=None):
rotd = Rot.from_mrp(-0.25*model[0:3])
if prev_model_ref is None:
R = Rot.as_matrix(rotd)
else:
R = np.matmul(Rot.as_matrix(rotd), prev_model_ref)
# reset model parameters because they are subsumed by reference
model[0:3] = 0.0
# convert to quaternion and back to matrix to ensure orthogonality
q = Rot.as_quat(Rot.from_matrix(R))
return Rot.as_matrix(Rot.from_quat(q))
# fits the model to the data
def weighted_fit(self, data, weight, scale) -> (np.array, np.array):
R,t = LS_PointCloudRegistration(data, weight)
model = np.zeros(6)
model[3:6] = t
return model,R
The reference rotation is first created in the weighted_fit method that implements the algorithm of [9]
to initiate the model. It returns the model and the rotation matrix R that is return as the initial model_ref.
In the cache_model() method, the reference rotation matrix $ R_0 $ and the small rotation $ R_s $
are combined as a cached rotation $ R = R_s R_0 $. This combined $ R $ is then used to calculate
the residual. In the residual_gradient method, the derivatives are calculated with respect to the small
rotation parameters, and a small angle approximation is used. The update_model_ref method updates the
reference rotation matrix $ R_0 $ based on the model change (small rotation) and the previous state
of the rotation reference prev_model_ref. The small rotation parameters in the model are reset to zero,
ready for the next iteration. A necessary refinement is that we should ensure that the rotation reference
maintains its orthogonality. Because it is computed incrementally, floating point errors could push it
away from being an actual rotation matrix. We solve this problem by converting into a quaternion and
back to a matrix.
References
[1] P. Charbonnier et al. in "Deterministic edge-preserving regularization in computed imaging", PAMI 6(2), 1997.
[2] P.W. Holland & R.E. Welsch "Robust regression using iteratively reweighted least-squares", Communications in Statistics-theory and Methods, 6(9), 1977.
[3] L. Peng et al. "On the Convergence of IRLS and Its Variants in Outlier-Robust Estimation", CVPR 2023.
[4] A. Blake & A. Zisserman "Visual reconstruction", MIT Press, 1987.
[5] K. Levenberg "A method for the solution of certain non – linear problems in least squares", Quarterly of Applied Mathematics, 2, 1944.
[6] A. Björck, "Numerical Methods for Least Squares Problems", (1996)
[7] P.J. Huber, "Robust Estimation of a Location Parameter", The Annals of Mathematical Statistics 35(1), 1964.
[8] D. Geman and S. Geman, "Bayesian image analysis", in "Disordered systems and biological organization", Springer, 1986.
[9] B.K.P. Horn, H.M. Hilden and S. Negahdaripour, "Closed-form solution of absolute orientation using orthonormal matrices", Journal of the Optical Society of America, 5(7), 1988.
[10] P.F. McLauchlan, "Gauge invariance in projective 3D reconstruction", Proceedings IEEE Workshop on Multi-View Modeling and Analysis of Visual Scenes (MVIEW'99), 1999.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gnc_smoothie-0.0.2.tar.gz.
File metadata
- Download URL: gnc_smoothie-0.0.2.tar.gz
- Upload date:
- Size: 672.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
888b9ab8d4f46ea960bbc858c16ddfcf83dda447b795827ea9789d2a0dfc3199
|
|
| MD5 |
0c648ea3dc6fef0eee4a80c730c10afd
|
|
| BLAKE2b-256 |
e134e5b9f7c57a84c54682110b116fd5d2fc2a5ca7da33425d66779eb8668290
|
File details
Details for the file gnc_smoothie-0.0.2-cp312-cp312-win_amd64.whl.
File metadata
- Download URL: gnc_smoothie-0.0.2-cp312-cp312-win_amd64.whl
- Upload date:
- Size: 951.3 kB
- Tags: CPython 3.12, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e32d32cc6730490f1c98a802f5d686617d56c53309a85ccf64949e5e320a8b46
|
|
| MD5 |
a4ead57e4f878c74452d6750b8955e27
|
|
| BLAKE2b-256 |
6c3f7f924df3e80291131fc5637de8331313a6c0e74a4e143a0b83b8db4a84f9
|
File details
Details for the file gnc_smoothie-0.0.2-cp312-cp312-manylinux1_x86_64.whl.
File metadata
- Download URL: gnc_smoothie-0.0.2-cp312-cp312-manylinux1_x86_64.whl
- Upload date:
- Size: 8.5 MB
- Tags: CPython 3.12
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f305e532b8016f41b5dff98e8791724b3dfb27af818c993b2e78ae334ab6970e
|
|
| MD5 |
b2b58e3ed526d68ca54ad4e3dbfb165b
|
|
| BLAKE2b-256 |
1ea7070e419f06cecd85df77c401805e1d9616f761802315d56e817573e486f9
|
File details
Details for the file gnc_smoothie-0.0.2-cp310-cp310-macosx_15_0_arm64.whl.
File metadata
- Download URL: gnc_smoothie-0.0.2-cp310-cp310-macosx_15_0_arm64.whl
- Upload date:
- Size: 980.3 kB
- Tags: CPython 3.10, macOS 15.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
75913b63912737e7d33c898d41c517ac420f4b066e849145af50bac7faa4833e
|
|
| MD5 |
5579f5f406950aa554c8144077e041ee
|
|
| BLAKE2b-256 |
ba96710a69e93bb3fe719e7f8c97d3b3c00cb8cfe7f966233ecd5ccc6272e048
|