def explain(self, X: np.ndarray, Y: np.ndarray = None, verbose: bool = False) -> Explanation: """ Explain instance and return PP or PN with metadata. Parameters ---------- X Instances to attack Y Labels for X verbose Print intermediate results of optimization if True Returns ------- explanation `Explanation` object containing the PP or PN with additional metadata as attributes. """ if X.shape[0] != 1: logger.warning('Currently only single instance explanations supported (first dim = 1), ' 'but first dim = %s', X.shape[0]) if Y is None: if self.model: Y = self.sess.run(self.predict(tf.convert_to_tensor(X, dtype=tf.float32))) else: Y = self.predict(X) Y_ohe = np.zeros(Y.shape) Y_ohe[np.arange(Y.shape[0]), np.argmax(Y, axis=1)] = 1 Y = Y_ohe.copy() # find best PP or PN self.best_attack = False best_attack, grads = self.attack(X, Y=Y, verbose=verbose) # output explanation dictionary data = copy.deepcopy(DEFAULT_DATA_CEM) data['X'] = X data['X_pred'] = np.argmax(Y, axis=1)[0] if not self.best_attack: logger.warning('No {} found!'.format(self.mode)) # create explanation object explanation = Explanation(meta=copy.deepcopy(self.meta), data=data) return explanation data[self.mode] = best_attack if self.model: Y_pert = self.sess.run(self.predict(tf.convert_to_tensor(best_attack, dtype=tf.float32))) else: Y_pert = self.predict(best_attack) data[self.mode + '_pred'] = np.argmax(Y_pert, axis=1)[0] data['grads_graph'], data['grads_num'] = grads[0], grads[1] # create explanation object explanation = Explanation(meta=copy.deepcopy(self.meta), data=data) return explanation
def build_explanation(self, text: str, result: dict, predicted_label: int, params: dict) -> Explanation: """ Uses the metadata returned by the anchor search algorithm together with the instance to be explained to build an explanation object. Parameters ---------- text Instance to be explained. result Dictionary containing the search result and metadata. predicted_label Label of the instance to be explained. Inferred if not received. params Parameters passed to `explain` """ result['instance'] = text result['prediction'] = predicted_label exp = AnchorExplanation('text', result) # output explanation dictionary data = copy.deepcopy(DEFAULT_DATA_ANCHOR) data.update(anchor=exp.names(), precision=exp.precision(), coverage=exp.coverage(), raw=exp.exp_map) # create explanation object explanation = Explanation(meta=copy.deepcopy(self.meta), data=data) # params passed to explain explanation.meta['params'].update(params) return explanation
def build_explanation(self, X: List[np.ndarray], baselines: List[np.ndarray], target: Optional[List[int]], attributions: List[np.ndarray]) -> Explanation: data = copy.deepcopy(DEFAULT_DATA_INTGRAD) data.update(X=X, baselines=baselines, target=target, attributions=attributions) # calculate predictions predictions = self.model(X).numpy() data.update(predictions=predictions) # calculate convergence deltas deltas = _compute_convergence_delta(self.model, self.input_dtypes, attributions, baselines, X, target, self._has_inputs) data.update(deltas=deltas) return Explanation(meta=copy.deepcopy(self.meta), data=data)
def build_explanation(self, ale_values: List[np.ndarray], ale0: List[np.ndarray], constant_value: float, feature_values: List[np.ndarray], feature_deciles: List[np.ndarray], feature_names: np.ndarray) -> Explanation: """ Helper method to build the Explanation object. """ # TODO decide on the format for these lists of arrays # Currently each list element relates to a feature and each column relates to an output dimension, # this is different from e.g. SHAP but arguably more convenient for ALE. data = copy.deepcopy(DEFAULT_DATA_ALE) data.update( ale_values=ale_values, ale0=ale0, constant_value=constant_value, feature_values=feature_values, feature_names=feature_names, target_names=self.target_names, feature_deciles=feature_deciles ) return Explanation(meta=copy.deepcopy(self.meta), data=data)
def explain(self, X: np.ndarray) -> Explanation: """ Explain an instance and return the counterfactual with metadata. Parameters ---------- X Instance to be explained Returns ------- `Explanation` object containing the counterfactual with additional metadata as attributes. """ # TODO change init parameters on the fly if X.shape[0] != 1: logger.warning( 'Currently only single instance explanations supported (first dim = 1), ' 'but first dim = %s', X.shape[0]) # make a prediction Y = self.predict_fn(X) pred_class = Y.argmax(axis=1).item() pred_prob = Y.max(axis=1).item() self.return_dict['orig_class'] = pred_class self.return_dict['orig_proba'] = pred_prob logger.debug('Initial prediction: %s with p=%s', pred_class, pred_prob) # define the class-specific prediction function self.predict_class_fn, t_class = _define_func(self.predict_fn, pred_class, self.target_class) # initialize with an instance X_init = self._initialize(X) # minimize loss iteratively self._minimize_loss(X, X_init, Y) return_dict = self.return_dict.copy() self.instance_dict = dict.fromkeys( ['X', 'distance', 'lambda', 'index', 'class', 'proba', 'loss']) self.return_dict = { 'cf': None, 'all': {i: [] for i in range(self.max_lam_steps)}, 'orig_class': None, 'orig_proba': None } # create explanation object explanation = Explanation(meta=copy.deepcopy(self.meta), data=return_dict) return explanation
def test_explanation(): exp = Explanation(meta=valid_meta, data=valid_data) assert exp.meta == valid_meta assert exp.data == valid_data assert isinstance(exp, Explanation) # test that a warning is raised if accessing attributes as dict keys with pytest.warns(None) as record: _ = exp['anchor'] assert len(record) == 1
def build_explanation(self, X: Union[np.ndarray, pd.DataFrame, sparse.spmatrix], shap_values: List[np.ndarray], expected_value: List) -> Explanation: """ Create an explanation object. Parameters ---------- X Array of instances to be explained. shap_values Each entry is a n_instances x n_features array, and the length of the list equals the dimensionality of the predictor output. The rows of each array correspond to the shap values for the instances with the corresponding row index in X expected_value A list containing the expected value of the prediction for each class. Returns ------- An explanation containing a meta field with basic classifier metadata # TODO: Plotting default should be same space as the explanation? How do we figure out what space they # explain in? """ # TODO: DEFINE COMPLETE SCHEMA FOR THE METADATA (ONGOING) raw_predictions = self._explainer.linkfv(self.predictor(X)) argmax_pred = np.argmax(np.atleast_2d(raw_predictions), axis=1) importances = self.rank_by_importance(shap_values) if isinstance(X, sparse.spmatrix): X = X.toarray() else: X = np.array(X) # output explanation dictionary data = copy.deepcopy(DEFAULT_DATA_SHAP) data.update(shap_values=shap_values, expected_value=expected_value, link=self.link, categorical_names=self.categorical_names, feature_names=self.feature_names) data['raw'].update(raw_prediction=raw_predictions, prediction=argmax_pred, instances=X, importances=importances) return Explanation(meta=copy.deepcopy(self.meta), data=data)
def build_explanation(self, X: np.ndarray, baselines: np.ndarray, target: list, attributions: np.ndarray) -> Explanation: data = copy.deepcopy(DEFAULT_DATA_INTGRAD) data.update(X=X, baselines=baselines, target=target, attributions=attributions) # calculate predictions predictions = self.model(X).numpy() data.update(predictions=predictions) # calculate convergence deltas deltas = _compute_convergence_delta(self.model, attributions, baselines, X, target) data.update(deltas=deltas) return Explanation(meta=copy.deepcopy(self.meta), data=data)
def build_explanation(self, image: np.ndarray, result: dict, predicted_label: int, params: dict) -> Explanation: """ Uses the metadata returned by the anchor search algorithm together with the instance to be explained to build an explanation object. Parameters ---------- image Instance to be explained. result Dictionary containing the search anchor and metadata. predicted_label Label of the instance to be explained. params Parameters passed to `explain` """ result['instance'] = image result['instances'] = np.expand_dims(image, 0) result['prediction'] = np.array([predicted_label]) # overlay image with anchor mask anchor = self.overlay_mask(image, self.segments, result['feature']) exp = AnchorExplanation('image', result) # output explanation dictionary data = copy.deepcopy(DEFAULT_DATA_ANCHOR_IMG) data.update( anchor=anchor, segments=self.segments, precision=exp.precision(), coverage=exp.coverage(), raw=exp.exp_map ) # create explanation object explanation = Explanation(meta=copy.deepcopy(self.meta), data=data) # params passed to explain explanation.meta['params'].update(params) return explanation
def build_explanation(self, X: np.ndarray, result: dict, predicted_label: int, params: dict) -> Explanation: """ Preprocess search output and return an explanation object containing metdata Parameters ---------- X: Instance to be explained. result: Dictionary with explanation search output and metadata. predicted_label: Label of the instance to be explained (inferred if not given). params Parameters passed to `explain` Return ------ `Explanation` object containing human readable explanation, metadata, and precision/coverage info as attributes. """ self.add_names_to_exp(result) result['prediction'] = np.array([predicted_label]) result['instance'] = X result['instances'] = np.atleast_2d(X) exp = AnchorExplanation('tabular', result) # output explanation dictionary data = copy.deepcopy(DEFAULT_DATA_ANCHOR) data.update(anchor=exp.names(), precision=exp.precision(), coverage=exp.coverage(), raw=exp.exp_map) # create explanation object explanation = Explanation(meta=copy.deepcopy(self.meta), data=data) # params passed to explain explanation.meta['params'].update(params) return explanation
def test_serialize_deserialize_explanation(): exp = Explanation(meta=valid_meta, data=valid_data) jrep = exp.to_json() exp2 = Explanation.from_json(jrep) assert exp == exp2