Source code for secml.adv.attacks.poisoning.c_attack_poisoning_svm

"""
.. module:: CAttackPoisoningSVM
   :synopsis: Poisoning attacks against Support Vector Machine

.. moduleauthor:: Battista Biggio <battista.biggio@unica.it>
.. moduleauthor:: Ambra Demontis <ambra.demontis@unica.it>

"""
from secml.adv.attacks.poisoning import CAttackPoisoning
from secml.array import CArray


[docs]class CAttackPoisoningSVM(CAttackPoisoning): """Poisoning attacks against Support Vector Machines (SVMs). This is an implementation of the attack in https://arxiv.org/pdf/1206.6389: - B. Biggio, B. Nelson, and P. Laskov. Poisoning attacks against support vector machines. In J. Langford and J. Pineau, editors, 29th Int'l Conf. on Machine Learning, pages 1807-1814. Omnipress, 2012. where the gradient is computed as described in Eq. (10) in https://www.usenix.org/conference/usenixsecurity19/presentation/demontis: - A. Demontis, M. Melis, M. Pintor, M. Jagielski, B. Biggio, A. Oprea, C. Nita-Rotaru, and F. Roli. Why do adversarial attacks transfer? Explaining transferability of evasion and poisoning attacks. In 28th USENIX Security Symposium. USENIX Association, 2019. For more details on poisoning attacks, see also: - https://arxiv.org/abs/1804.00308, IEEE Symp. SP 2018 - https://arxiv.org/abs/1712.03141, Patt. Rec. 2018 - https://arxiv.org/abs/1708.08689, AISec 2017 - https://arxiv.org/abs/1804.07933, ICML 2015 Parameters ---------- classifier : CClassifierSVM Target SVM, trained in the dual (i.e., with kernel not set to None). training_data : CDataset Dataset on which the the classifier has been trained on. val : CDataset Validation set. distance : {'l1' or 'l2'}, optional Norm to use for computing the distance of the adversarial example from the original sample. Default 'l2'. dmax : scalar, optional Maximum value of the perturbation. Default 1. lb, ub : int or CArray, optional Lower/Upper bounds. If int, the same bound will be applied to all the features. If CArray, a different bound can be specified for each feature. Default `lb = 0`, `ub = 1`. y_target : int or None, optional If None an error-generic attack will be performed, else a error-specific attack to have the samples misclassified as belonging to the `y_target` class. solver_type : str or None, optional Identifier of the solver to be used. Default 'pgd-ls'. solver_params : dict or None, optional Parameters for the solver. Default None, meaning that default parameters will be used. init_type : {'random', 'loss_based'}, optional Strategy used to chose the initial random samples. Default 'random'. random_seed : int or None, optional If int, random_state is the seed used by the random number generator. If None, no fixed seed will be set. """ __class_type = 'p-svm' def __init__(self, classifier, training_data, val, distance='l1', dmax=0, lb=0, ub=1, y_target=None, solver_type='pgd-ls', solver_params=None, init_type='random', random_seed=None): CAttackPoisoning.__init__(self, classifier=classifier, training_data=training_data, val=val, distance=distance, dmax=dmax, lb=lb, ub=ub, y_target=y_target, solver_type=solver_type, solver_params=solver_params, init_type=init_type, random_seed=random_seed) # check if SVM has been trained in the dual if self.classifier.kernel is None: raise ValueError( "Please retrain the SVM in the dual (kernel != None).") # indices of support vectors (at previous iteration) # used to check if warm_start can be used in the iterative solver self._sv_idx = None ########################################################################### # PRIVATE METHODS ########################################################################### def _init_solver(self): """Overrides _init_solver to additionally reset the SV indices.""" super(CAttackPoisoningSVM, self)._init_solver() # reset stored indices of SVs self._sv_idx = None ########################################################################### # OBJECTIVE FUNCTION & GRAD COMPUTATION ########################################################################### def _alpha_c(self, clf): """ Returns alpha value of xc, assuming xc to be appended as the last point in tr """ # index of poisoning point within xc. # This will be replaced by the input parameter xc if self._idx is None: idx = 0 else: idx = self._idx # index of the current poisoning point in the set self._xc # as this set is appended to the training set, idx is shifted idx += self.training_data.num_samples # k is the index of sv_idx corresponding to the training idx of xc k = clf.sv_idx.find(clf.sv_idx == idx) if len(k) == 1: # if not empty alpha_c = clf.alpha[k].todense().ravel() return alpha_c return 0
[docs] def alpha_xc(self, xc): """ Parameters ---------- xc: poisoning point Returns ------- f_obj: values of objective function (average hinge loss) at x """ # index of poisoning point within xc. # This will be replaced by the input parameter xc if self._idx is None: idx = 0 else: idx = self._idx xc = CArray(xc).atleast_2d() n_samples = xc.shape[0] if n_samples > 1: raise TypeError("xc is not a single sample!") self._xc[idx, :] = xc self._update_poisoned_clf() # PARAMETER CLF UNFILLED return self._alpha_c()
########################################################################### # GRAD COMPUTATION ########################################################################### def _Kd_xc(self, clf, alpha_c, xc, xk): """ Derivative of the kernel w.r.t. a training sample xc Parameters ---------- xk : CArray features of a validation set xc: CArray features of the training point w.r.t. the derivative has to be computed alpha_c: integer alpha value of the of the training point w.r.t. the derivative has to be computed """ # handle normalizer, if present p = clf.kernel.preprocess # xc = xc if p is None else p.forward(xc, caching=False) xk = xk if p is None else p.forward(xk, caching=False) rv = clf.kernel.rv clf.kernel.rv = xk dKkc = alpha_c * clf.kernel.gradient(xc) clf.kernel.rv = rv return dKkc.T # d * k def _gradient_fk_xc(self, xc, yc, clf, loss_grad, tr, k=None): """ Derivative of the classifier's discriminant function f(xk) computed on a set of points xk w.r.t. a single poisoning point xc """ xc0 = xc.deepcopy() d = xc.size grad = CArray.zeros(shape=(d,)) # gradient in input space alpha_c = self._alpha_c(clf) if abs(alpha_c) == 0: # < svm.C: # this include alpha_c == 0 # self.logger.debug("Warning: xc is not an error vector.") return grad # take only validation points with non-null loss xk = self._val.X[abs(loss_grad) > 0, :].atleast_2d() grad_loss_fk = CArray(loss_grad[abs(loss_grad) > 0]).T # gt is the gradient in feature space # this gradient component is the only one if margin SV set is empty # gt is the derivative of the loss computed on a validation # set w.r.t. xc Kd_xc = self._Kd_xc(clf, alpha_c, xc, xk) assert (clf.kernel.rv.shape[0] == clf.alpha.shape[1]) gt = Kd_xc.dot(grad_loss_fk).ravel() # gradient of the loss w.r.t. xc xs, sv_idx = clf._sv_margin() # these points are already normalized if xs is None: self.logger.debug("Warning: xs is empty " "(all points are error vectors).") return gt if clf.kernel.preprocess is None else \ clf.kernel.preprocess.gradient(xc0, w=gt) s = xs.shape[0] # derivative of the loss computed on a validation set w.r.t. the # classifier params fd_params = clf.grad_f_params(xk) grad_loss_params = fd_params.dot(grad_loss_fk) H = clf.hessian_tr_params() H += 1e-9 * CArray.eye(s + 1) # handle normalizer, if present # xc = xc if clf.preprocess is None else clf.kernel.transform(xc) G = CArray.zeros(shape=(gt.size, s + 1)) rv = clf.kernel.rv clf.kernel.rv = xs G[:, :s] = clf.kernel.gradient(xc).T clf.kernel.rv = rv G *= alpha_c # warm start is disabled if the set of SVs changes! # if self._sv_idx is None or self._sv_idx.size != sv_idx.size or \ # (self._sv_idx != sv_idx).any(): # self._warm_start = None # self._sv_idx = sv_idx # store SV indices for the next iteration # # # iterative solver # v = - self._compute_grad_solve_iterative( # G, H, grad_loss_params, tol=1e-3) # solve with standard linear solver # v = - self._compute_grad_solve(G, H, grad_loss_params, sym_pos=False) # solve using inverse/pseudo-inverse of H # v = - self._compute_grad_inv(G, H, grad_loss_params) v = self._compute_grad_inv(G, H, grad_loss_params) gt += v # propagating gradient back to input space if clf.kernel.preprocess is not None: return clf.kernel.preprocess.gradient(xc0, w=gt) return gt