ARD Reference

The RegressionARD class implements Automatic Relevance Determination (ARD) via the Sparse Bayesian Learning (SBL) algorithm [^1][^2]. When used with cv=True, the model internally runs K-fold cross-validation at each ARD iteration and selects the best-performing model state via retrospective selection.

Cross-Validation Methods

Three scoring methods are available through the cv_method parameter:

Method Description
'bayesian' Predictive log-likelihood CV with per-fold centering — the training data within each fold is centered independently using only that fold's statistics, and the validation data is transformed with the training-fold statistics. This eliminates the data-leakage that would result from centering on the full dataset before splitting. The score is \(\log p(y_{\text{val}} \mid X_{\text{val}}, \mathcal{D}_{\text{train}})\), i.e. the log predictive density of the held-out data under the ARD posterior fitted on the training fold. This is the recommended method.
'predictive' Alias for 'bayesian'.
'ridge' Legacy Ridge-regression CV (sklearn.linear_model.Ridge). Evaluates the current active feature set using a non-sparse L2-penalised model, which is statistically inconsistent with the ARD framework. Retained for backward compatibility only.

All methods use 10-fold CV with a fixed random seed (42) for reproducibility. The best model iteration is chosen retrospectively after all ARD iterations complete, avoiding the instability of early-stopping heuristics.

Per-Fold Centering (v0.5.2+)

Prior to v0.5.2, CV was performed on data that had been centered using the full dataset mean before splitting. This leaked information from the validation fold into the centering transformation, producing systematically optimistic CV scores. The current implementation centres each fold independently:

\[ \begin{aligned} \bar{\mathbf{x}}_{\text{train}} &= \frac{1}{n_{\text{train}}} \sum_{i \in \text{train}} \mathbf{x}_i, \qquad \bar{y}_{\text{train}} = \frac{1}{n_{\text{train}}} \sum_{i \in \text{train}} y_i \\[4pt] \mathbf{X}_{\text{train}}^{(c)} &= \mathbf{X}_{\text{train}} - \bar{\mathbf{x}}_{\text{train}}, \qquad \mathbf{y}_{\text{train}}^{(c)} = \mathbf{y}_{\text{train}} - \bar{y}_{\text{train}} \\[4pt] \mathbf{X}_{\text{val}}^{(c)} &= \mathbf{X}_{\text{val}} - \bar{\mathbf{x}}_{\text{train}} \end{aligned} \]

The posterior \(\mathbf{m}, \mathbf{S}\) is computed from \(\mathbf{X}_{\text{train}}^{(c)}\) and predictions are converted back to original scale via \(\hat{\mathbf{y}}_{\text{val}} = \mathbf{X}_{\text{val}}^{(c)} \mathbf{m} + \bar{y}_{\text{train}}\).

[^1]: Tipping, M. E., & Faul, A. C. (2003). Fast marginal likelihood maximisation for sparse Bayesian models. AISTATS. [^2]: Tipping, M. E., & Faul, A. C. (2001). Analysis of sparse Bayesian learning. NeurIPS.

shapleyx.utilities.ARD

Backward-compatibility shim — delegates to shapleyx.ard._sbl.

The canonical implementation of RegressionARD and update_precisions now lives at shapleyx.ard._sbl. This module is kept so that existing code with from shapleyx.utilities.ARD import RegressionARD continues to work.

RegressionARD(n_iter=300, tol=0.001, fit_intercept=True, copy_X=True, verbose=False, cv_tol=0.1, cv=False, cv_method='bayesian', cv_folds=10, retrospective_selection=True, store_history=False, threshold_lambda=10000.0, algorithm='sequential', alpha_1=1e-06, alpha_2=1e-06, lambda_1=1e-06, lambda_2=1e-06, return_std=False)

Bases: RegressorMixin, LinearModel

Regression with Automatic Relevance Determination (ARD) using Sparse Bayesian Learning.

This class implements a fast version of ARD regression, which is a Bayesian approach to regression that automatically determines the relevance of each feature. It is based on the Sparse Bayesian Learning (SBL) algorithm, which promotes sparsity in the model by estimating the precision of the coefficients.

Parameters:
  • n_iter (int, default: 300 ) –

    Maximum number of iterations for the optimization algorithm. Defaults to 300.

  • tol (float, default: 0.001 ) –

    Convergence threshold. If the absolute change in the precision parameter for the weights is below this threshold, the algorithm terminates. Defaults to 1e-3.

  • fit_intercept (bool, default: True ) –

    Whether to calculate the intercept for this model. If set to False, no intercept will be used in calculations (e.g., data is expected to be already centered). Defaults to True.

  • copy_X (bool, default: True ) –

    If True, X will be copied; else, it may be overwritten. Defaults to True.

  • verbose (bool, default: False ) –

    If True, the algorithm will print progress messages during fitting. Defaults to False.

  • cv_tol (float, default: 0.1 ) –

    DEPRECATED - Tolerance for cross-validation early stopping. If the percentage change in cross-validation score is below this threshold, the algorithm terminates. Defaults to 0.1. Note: Early stopping based on CV is deprecated; use retrospective_selection=True instead.

  • cv (bool, default: False ) –

    If True, cross-validation will be used for model selection. Defaults to False.

  • cv_method (str, default: 'bayesian' ) –

    Method for cross-validation scoring. Options: 'ridge' - Uses ridge regression (legacy, not recommended), 'bayesian' - Uses predictive log-likelihood CV (recommended), 'predictive' - Alias for 'bayesian'. Defaults to 'bayesian'.

  • cv_folds (int, default: 10 ) –

    Number of folds for cross-validation. Defaults to 10.

  • retrospective_selection (bool, default: True ) –

    If True, runs all iterations and retrospectively selects the best model based on CV score. If False, uses early stopping (deprecated). Defaults to True.

  • store_history (bool, default: False ) –

    If True, stores model states at each iteration for debugging/analysis. Increases memory usage. Defaults to False.

  • threshold_lambda (float, default: 10000.0 ) –

    Post-fit pruning threshold. After ARD converges (and after retrospective CV selection, if enabled), any feature whose estimated precision lambda_[i] exceeds this threshold has its coefficient set to exactly zero. This mirrors the pruning step in sklearn's ARDRegression and produces sparser models for problems where the SBL algorithm retains marginal features with large but finite precisions. Defaults to 10\,000. Set to np.inf to disable.

  • algorithm (str, default: 'sequential' ) –

    Which ARD variant to use. 'sequential' (default) — Tipping & Faul (2003) fast sequential SBL: picks one feature per iteration (add/recompute/delete). 'em' — batch evidence maximization (MacKay 1992_): updates all weights simultaneously with Gamma hyper-priors (alpha_1, alpha_2, lambda_1, lambda_2) and per-iteration pruning via threshold_lambda. Typically produces sparser models matching sklearn's ARDRegression behaviour.

  • alpha_1, alpha_2

    float, optional Gamma hyper-prior shape/inverse-scale for the noise precision. Only used when algorithm='em'. Default 1e-6.

  • lambda_1, lambda_2

    float, optional Gamma hyper-prior shape/inverse-scale for the weight precisions. Only used when algorithm='em'. Default 1e-6.

Attributes:
  • coef_ (array) –

    Coefficients of the regression model (mean of the posterior distribution). Shape (n_features,).

  • alpha_ (float) –

    Estimated precision of the noise.

  • active_ (array) –

    Boolean array indicating which features are active (non-zero coefficients). Shape (n_features,), dtype=bool.

  • lambda_ (array) –

    Estimated precisions of the coefficients. Shape (n_features,).

  • sigma_ (array) –

    Estimated covariance matrix of the weights, computed only for non-zero coefficients. Shape (n_features, n_features).

  • scores_ (list) –

    List of cross-validation scores if cv is True.

  • history_ (dict) –

    Dictionary containing iteration history if store_history=True. Includes 'iterations', 'cv_scores', 'n_features', and optionally 'states'.

  • best_iteration_ (int) –

    Index of the best iteration selected via retrospective selection.

  • best_cv_score_ (float) –

    Best cross-validation score across all iterations.

References

[1] Tipping, M. E., & Faul, A. C. (2003). Fast marginal likelihood maximisation for sparse Bayesian models. In Proceedings of the Ninth International Workshop on Artificial Intelligence and Statistics (pp. 276-283).

[2] Tipping, M. E., & Faul, A. C. (2001). Analysis of sparse Bayesian learning. In Advances in Neural Information Processing Systems (pp. 383-389).

Note

The RegressionARD class code has been adapted from the original implementation by Amazasp Shaumyan https://github.com/AmazaspShumik/sklearn-bayes

Source code in shapleyx/ard/_sbl.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def __init__( self, n_iter = 300, tol = 1e-3, fit_intercept = True,
              copy_X = True, verbose = False, cv_tol = 0.1, cv=False,
              cv_method='bayesian', cv_folds=10, retrospective_selection=True,
              store_history=False, threshold_lambda=1e4,
              algorithm='sequential',
              alpha_1=1e-6, alpha_2=1e-6,
              lambda_1=1e-6, lambda_2=1e-6,
              return_std=False):
    self.n_iter          = n_iter
    self.tol             = tol
    self.fit_intercept   = fit_intercept
    self.copy_X          = copy_X
    self.verbose         = verbose
    self.cv              = cv
    self.cv_tol          = cv_tol
    self.cv_method       = cv_method
    self.cv_folds        = cv_folds
    self.retrospective_selection = retrospective_selection
    self.store_history   = store_history
    self.threshold_lambda = float(threshold_lambda)
    self.algorithm        = algorithm
    self.alpha_1          = float(alpha_1)
    self.alpha_2          = float(alpha_2)
    self.lambda_1         = float(lambda_1)
    self.lambda_2         = float(lambda_2)
    self.return_std       = return_std

    if self.algorithm not in ('sequential', 'em'):
        raise ValueError(
            f"Unknown algorithm '{algorithm}'. "
            f"Expected 'sequential' or 'em'."
        )

    # Warn about deprecated cv_tol if retrospective_selection is True
    if retrospective_selection and cv_tol != 0.1:
        warnings.warn(
            "cv_tol parameter is deprecated when retrospective_selection=True. "
            "Early stopping based on CV tolerance is disabled. "
            "Set retrospective_selection=False to use cv_tol for early stopping.",
            DeprecationWarning
        )
fit(X, y)

Fit the ARD regression model to the data.

Parameters

X : {array-like, sparse matrix}, shape (n_samples, n_features) Training data, matrix of explanatory variables.

array-like, shape (n_samples,)

Target values.

Returns

self : object Returns the instance itself.

Source code in shapleyx/ard/_sbl.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
def fit(self,X,y):
    '''
    Fit the ARD regression model to the data.

    Parameters
    ----------
    X : {array-like, sparse matrix}, shape (n_samples, n_features)
        Training data, matrix of explanatory variables.

    y : array-like, shape (n_samples,)
        Target values.

    Returns
    -------
    self : object
        Returns the instance itself.
    '''
    X, y = check_X_y(X, y, dtype=np.float64, y_numeric=True)
    # ── Route to the appropriate algorithm ─────────────────
    if self.algorithm == 'em':
        return self._fit_em(X, y)
    # ── Sequential SBL (Tipping & Faul 2003) ──────────────
    # Save original-scale data for cross-validation (prevents leakage
    # from full-dataset centering into per-fold evaluation).
    X_orig, y_orig = X.copy(), y.copy()
    X, y, X_mean, y_mean, X_std = self._center_data(X, y)
    n_samples, n_features = X.shape

    # Initialize history storage
    self.history_ = {
        'iterations': [],
        'cv_scores': [],
        'n_features': [],
        'states': [] if self.store_history else None
    }
    self.best_iteration_ = None
    self.best_cv_score_ = None

    # For backward compatibility
    cv_list = []
    current_r = 0

    #  precompute X'*Y , X'*X for faster iterations & allocate memory for
    #  sparsity & quality vectors
    XY     = np.dot(X.T,y)
    XX     = np.dot(X.T,X)
    XXd    = np.diag(XX)

    #  initialise precision of noise & and coefficients
    var_y  = np.var(y)

    # check that variance is non zero !!!
    if var_y == 0 :
        beta = 1e-2
    else:
        beta = 1. / np.var(y)

    A      = np.inf * np.ones(n_features)
    active = np.zeros(n_features , dtype = bool)

    # in case of almost perfect multicollinearity between some features
    # start from feature 0
    if np.sum( XXd - X_mean**2 < np.finfo(np.float32).eps ) > 0:
        A[0]       = np.finfo(np.float16).eps
        active[0]  = True
    else:
        # start from a single basis vector with largest projection on targets
        proj  = XY**2 / XXd
        start = np.argmax(proj)
        active[start] = True
        A[start]      = XXd[start]/( proj[start] - var_y)

    warning_flag = 0

    # Store best model state for retrospective selection
    best_state = None
    best_cv_score = -np.inf
    best_iteration = -1

    for i in range(self.n_iter):
        XXa     = XX[active,:][:,active]
        XYa     = XY[active]
        Aa      =  A[active]

        # mean & covariance of posterior distribution
        Mn,Ri,cholesky  = self._posterior_dist(Aa,beta,XXa,XYa)
        if cholesky:
            Sdiag  = np.sum(Ri**2,0)
        else:
            Sdiag  = np.copy(np.diag(Ri))
            warning_flag += 1

        # raise warning in case cholesky failes
        if warning_flag == 1:
            warnings.warn(("Cholesky decomposition failed ! Algorithm uses pinvh, "
                           "which is significantly slower, if you use RVR it "
                           "is advised to change parameters of kernel"))

        # compute quality & sparsity parameters
        s,q,S,Q = self._sparsity_quality(XX,XXd,XY,XYa,Aa,Ri,active,beta,cholesky)

        # update precision parameter for noise distribution
        rss     = np.sum( ( y - np.dot(X[:,active] , Mn) )**2 )
        beta    = n_samples - np.sum(active) + np.sum(Aa * Sdiag )
        beta   /= ( rss + np.finfo(np.float32).eps )

        # update precision parameters of coefficients
        A,converged  = update_precisions(Q,S,q,s,A,active,self.tol,
                                         n_samples,False)

        # --- Cross-validation scoring (if enabled) ---
        cv_score = None
        if self.cv:
            cv_score = self._compute_cv_score(X_orig, y_orig, active, beta, A, XX, XY,
                                             X_mean, y_mean, X_std)

            # Store in history
            self.history_['iterations'].append(i)
            self.history_['cv_scores'].append(cv_score)
            self.history_['n_features'].append(np.sum(active))

            if self.store_history:
                # Store model state
                state = {
                    'active': active.copy(),
                    'coef': np.zeros(n_features),
                    'coef_active': Mn.copy(),
                    'lambda': A.copy(),
                    'alpha': beta,
                    'sigma': Ri.copy() if not cholesky else None,
                    'cholesky': cholesky
                }
                state['coef'][active] = Mn
                self.history_['states'].append(state)

            # Update best model for retrospective selection
            if cv_score is not None and cv_score > best_cv_score:
                best_cv_score = cv_score
                best_iteration = i
                # Store best state
                best_state = {
                    'active': active.copy(),
                    'A': A.copy(),
                    'beta': beta,
                    'XXa': XXa.copy(),
                    'XYa': XYa.copy(),
                    'Aa': Aa.copy(),
                    'X_mean': X_mean.copy(),
                    'y_mean': y_mean,
                    'X_std': X_std.copy()
                }

            # For backward compatibility
            cv_list.append(cv_score)
            if i == 0:
                current_r = cv_score if cv_score is not None else 0

            # Print CV status
            if self.verbose:
                print(f'Iteration: {i:<4}  CV Score: {cv_score:.6f}  '
                      f'Active features: {np.sum(active)}')

        # --- Verbose output for main iteration progress ---
        if self.verbose and not self.cv:
            # Use f-string for consistency and clarity
            print(f'Iteration: {i:<5}, number of features remaining: {np.sum(active)}')

        # --- Check for convergence (ARD criteria only, no CV early stopping) ---
        # Note: CV-based early stopping is disabled when retrospective_selection=True
        if converged or i == self.n_iter - 1:
            if self.verbose:
                print(f'Finished ARD iterations at iteration {i+1}.')
                if converged:
                    print('Algorithm converged (ARD criteria).')
                elif i == self.n_iter - 1:
                    print('Reached maximum number of iterations without full convergence.')

            # If using retrospective selection and we have a best state, restore it
            if self.cv and self.retrospective_selection and best_state is not None:
                if self.verbose:
                    print(f'Restoring best model from iteration {best_iteration} '
                          f'with CV score: {best_cv_score:.6f}')

                # Restore best state
                active = best_state['active']
                A = best_state['A']
                beta = best_state['beta']
                XXa = best_state['XXa']
                XYa = best_state['XYa']
                Aa = best_state['Aa']
                X_mean = best_state['X_mean']
                y_mean = best_state['y_mean']
                X_std = best_state['X_std']

                # Update best iteration attributes
                self.best_iteration_ = best_iteration
                self.best_cv_score_ = best_cv_score

            # Break only if not using retrospective selection, if we're at the last iteration, or if converged
            if not self.retrospective_selection or i == self.n_iter - 1 or converged:
                break


    # after last update of alpha & beta update parameters
    # of posterior distribution
    XXa,XYa,Aa         = XX[active,:][:,active],XY[active],A[active]
    Mn, Sn, cholesky   = self._posterior_dist(Aa,beta,XXa,XYa,True)
    self.coef_         = np.zeros(n_features)
    self.coef_[active] = Mn
    self.sigma_        = Sn
    self.active_       = active
    self.lambda_       = A
    self.alpha_        = beta

    # Post-fit pruning: zero out coefficients whose estimated
    # precision exceeds threshold_lambda (mirrors sklearn ARDRegression).
    if np.isfinite(self.threshold_lambda):
        prune_mask = (self.lambda_ > self.threshold_lambda) & self.active_
        self.coef_[prune_mask] = 0.0
        self.active_[prune_mask] = False
        if self.verbose and np.any(prune_mask):
            print(f"  Pruned {np.sum(prune_mask)} features with "
                  f"lambda > {self.threshold_lambda:.0f}")

    self._set_intercept(X_mean,y_mean,X_std)

    # Store scores for backward compatibility
    self.scores_ = cv_list if self.cv else []

    if self.cv and self.verbose:
        print(('Number of features in the model: {0}').format(np.sum(active)))
    return self
predict_dist(X)

Computes predictive distribution for test set. Predictive distribution for each data point is one dimensional Gaussian and therefore is characterised by mean and variance.

Parameters

X : {array-like, sparse matrix}, shape (n_samples_test, n_features) Test data, matrix of explanatory variables.

Returns

y_hat : array, shape (n_samples_test,) Estimated values of targets on the test set (mean of the predictive distribution).

array, shape (n_samples_test,)

Variance of the predictive distribution.

Source code in shapleyx/ard/_sbl.py
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
def predict_dist(self,X):
    '''
    Computes predictive distribution for test set.
    Predictive distribution for each data point is one dimensional
    Gaussian and therefore is characterised by mean and variance.

    Parameters
    ----------
    X : {array-like, sparse matrix}, shape (n_samples_test, n_features)
        Test data, matrix of explanatory variables.

    Returns
    -------
    y_hat : array, shape (n_samples_test,)
        Estimated values of targets on the test set (mean of the predictive distribution).

    var_hat : array, shape (n_samples_test,)
        Variance of the predictive distribution.
    '''
    y_hat     = self._decision_function(X)
    var_hat   = 1./self.alpha_
    var_hat  += np.sum( np.dot(X[:,self.active_],self.sigma_) * X[:,self.active_], axis = 1)
    return y_hat, var_hat

update_precisions(Q, S, q, s, A, active, tol, n_samples, clf_bias)

Updates the precision parameters (alpha) for features in a sparse Bayesian learning model by selecting a feature to add, recompute, or delete based on its impact on the log marginal likelihood. The function also checks for convergence.

Parameters:

Q : numpy.ndarray Quality parameters for all features. S : numpy.ndarray Sparsity parameters for all features. q : numpy.ndarray Quality parameters for features currently in the model. s : numpy.ndarray Sparsity parameters for features currently in the model. A : numpy.ndarray Precision parameters (alpha) for all features. active : numpy.ndarray (bool) Boolean array indicating whether each feature is currently in the model. tol : float Tolerance threshold for determining convergence based on changes in precision. n_samples : int Number of samples in the dataset, used to normalize the change in log marginal likelihood. clf_bias : bool Flag indicating whether the model includes a bias term (used in classification tasks).

Returns:

list A list containing two elements: - Updated precision parameters (A) for all features. - A boolean flag indicating whether the model has converged.

Notes:

The function performs the following steps: 1. Computes the change in log marginal likelihood for adding, recomputing, or deleting features. 2. Identifies the feature that causes the largest change in likelihood. 3. Updates the precision parameter (alpha) for the selected feature. 4. Checks for convergence based on whether no features are added/deleted and changes in precision are below the specified tolerance. 5. Returns the updated precision parameters and convergence status.

Convergence is determined by two conditions: - No features are added or deleted. - The change in precision for features already in the model is below the tolerance threshold.

The function ensures that the bias term is not removed in classification tasks.

Source code in shapleyx/ard/_sbl.py
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
def update_precisions(Q,S,q,s,A,active,tol,n_samples,clf_bias):
    '''
    Updates the precision parameters (alpha) for features in a sparse Bayesian learning model
    by selecting a feature to add, recompute, or delete based on its impact on the log marginal
    likelihood. The function also checks for convergence.

    Parameters:
    -----------
    Q : numpy.ndarray
        Quality parameters for all features.
    S : numpy.ndarray
        Sparsity parameters for all features.
    q : numpy.ndarray
        Quality parameters for features currently in the model.
    s : numpy.ndarray
        Sparsity parameters for features currently in the model.
    A : numpy.ndarray
        Precision parameters (alpha) for all features.
    active : numpy.ndarray (bool)
        Boolean array indicating whether each feature is currently in the model.
    tol : float
        Tolerance threshold for determining convergence based on changes in precision.
    n_samples : int
        Number of samples in the dataset, used to normalize the change in log marginal likelihood.
    clf_bias : bool
        Flag indicating whether the model includes a bias term (used in classification tasks).

    Returns:
    --------
    list
        A list containing two elements:
        - Updated precision parameters (A) for all features.
        - A boolean flag indicating whether the model has converged.

    Notes:
    ------
    The function performs the following steps:
    1. Computes the change in log marginal likelihood for adding, recomputing, or deleting features.
    2. Identifies the feature that causes the largest change in likelihood.
    3. Updates the precision parameter (alpha) for the selected feature.
    4. Checks for convergence based on whether no features are added/deleted and changes in precision
       are below the specified tolerance.
    5. Returns the updated precision parameters and convergence status.

    Convergence is determined by two conditions:
    - No features are added or deleted.
    - The change in precision for features already in the model is below the tolerance threshold.

    The function ensures that the bias term is not removed in classification tasks.
    '''
    # initialise vector holding changes in log marginal likelihood
    deltaL = np.zeros(Q.shape[0])

    # identify features that can be added , recomputed and deleted in model
    theta        =  q**2 - s 
    add          =  (theta > 0) * (active == False)
    recompute    =  (theta > 0) * (active == True)
    delete       = ~(add + recompute)

    # compute sparsity & quality parameters corresponding to features in 
    # three groups identified above
    Qadd,Sadd      = Q[add], S[add]
    Qrec,Srec,Arec = Q[recompute], S[recompute], A[recompute]
    Qdel,Sdel,Adel = Q[delete], S[delete], A[delete]

    # compute new alpha's (precision parameters) for features that are 
    # currently in model and will be recomputed
    Anew           = s[recompute]**2/ ( theta[recompute] + np.finfo(np.float32).eps)
    delta_alpha    = (1./Anew - 1./Arec)

    # compute change in log marginal likelihood 
    deltaL[add]       = ( Qadd**2 - Sadd ) / Sadd + np.log(Sadd/Qadd**2 )
    deltaL[recompute] = Qrec**2 / (Srec + 1. / delta_alpha) - np.log(1 + Srec*delta_alpha)
    deltaL[delete]    = Qdel**2 / (Sdel - Adel) - np.log(1 - Sdel / Adel)
    deltaL            = deltaL  / n_samples

    # find feature which caused largest change in likelihood
    feature_index = np.argmax(deltaL)

    # no deletions or additions
    same_features  = np.sum( theta[~recompute] > 0) == 0

    # changes in precision for features already in model is below threshold
    no_delta       = np.sum( abs( Anew - Arec ) > tol ) == 0

    # check convergence: if no features to add or delete and small change in 
    #                    precision for current features then terminate
    converged = False
    if same_features and no_delta:
        converged = True
        return [A,converged]

    # if not converged update precision parameter of weights and return
    if theta[feature_index] > 0:
        A[feature_index] = s[feature_index]**2 / theta[feature_index]
        if active[feature_index] == False:
            active[feature_index] = True
    else:
        # at least two active features
        if active[feature_index] == True and np.sum(active) >= 2:
            # do not remove bias term in classification 
            # (in regression it is factored in through centering)
            if not (feature_index == 0 and clf_bias):
               active[feature_index] = False
               A[feature_index]      = np.inf

    return [A,converged]