diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bff731 --- /dev/null +++ b/.gitignore @@ -0,0 +1,105 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4dda4a1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2019 anonymousneuripssubmission + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e7d16c --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# ACE +Automatic Concept-based Explanations +Required libraries: + Scikit-image: https://scikit-image.org/ + Tensorflow: https://www.tensorflow.org/ + TCAV: https://github.com/tensorflow/tcav + diff --git a/ace.py b/ace.py new file mode 100644 index 0000000..ea40796 --- /dev/null +++ b/ace.py @@ -0,0 +1,776 @@ +"""ACE library. + +Library for discovering and testing concept activation vectors. It contains +ConceptDiscovery class that is able to discover the concepts belonging to one +of the possible classification labels of the classification task of a network +and calculate each concept's TCAV score.. +""" +from multiprocessing import dummy as multiprocessing +import sys +import os +import numpy as np +from PIL import Image +import scipy.stats as stats +import skimage.segmentation as segmentation +import sklearn.cluster as cluster +import sklearn.metrics.pairwise as metrics +import tensorflow as tf +from tcav import cav, tcav_helpers +class ConceptDiscovery(object): + """Discovering and testing concepts of a class. + + For a trained network, it first discovers the concepts as areas of the iamges + in the class and then calculates the TCAV score of each concept. It is also + able to transform images from pixel space into concept space. + """ + + def __init__(self, + model, + target_class, + random_concept, + bottlenecks, + sess, + source_dir, + activation_dir, + cav_dir, + num_random_exp=2, + channel_mean=True, + max_imgs=40, + min_imgs=20, + num_discovery_imgs=40, + num_workers=20, + average_image_value=117): + """Runs concept discovery for a given class in a trained model. + + For a trained classification model, the ConceptDiscovery class first + performs unsupervised concept discovery using examples of one of the classes + in the network. + + Args: + model: A trained classification model on which we run the concept + discovery algorithm + target_class: Name of the one of the classes of the network + random_concept: A concept made of random images (used for statistical + test) e.g. "random500_199" + bottlenecks: a list of bottleneck layers of the model for which the cocept + discovery stage is performed + sess: Model's tensorflow session + source_dir: This directory that contains folders with images of network's + classes. + activation_dir: directory to save computed activations + cav_dir: directory to save CAVs of discovered and random concepts + num_random_exp: Number of random counterparts used for calculating several + CAVs and TCAVs for each concept (to make statistical + testing possible.) + channel_mean: If true, for the unsupervised concept discovery the + bottleneck activations are averaged over channels instead + of using the whole acivation vector (reducing + dimensionality) + max_imgs: maximum number of images in a discovered concept + min_imgs : minimum number of images in a discovered concept for the + concept to be accepted + num_discovery_imgs: Number of images used for concept discovery. If None, + will use max_imgs instead. + num_workers: if greater than zero, runs methods in parallel with + num_workers parallel threads. If 0, no method is run in parallel + threads. + average_image_value: The average value used for mean subtraction in the + nework's preprocessing stage. + """ + self.model = model + self.sess = sess + self.target_class = target_class + self.num_random_exp = num_random_exp + if isinstance(bottlenecks, str): + bottlenecks = [bottlenecks] + self.bottlenecks = bottlenecks + self.source_dir = source_dir + self.activation_dir = activation_dir + self.cav_dir = cav_dir + self.channel_mean = channel_mean + self.random_concept = random_concept + self.image_shape = model.get_image_shape()[:2] + self.max_imgs = max_imgs + self.min_imgs = min_imgs + if num_discovery_imgs is None: + num_discovery_imgs = max_imgs + self.num_discovery_imgs = num_discovery_imgs + self.num_workers = num_workers + self.average_image_value = average_image_value + + def load_concept_imgs(self, concept, max_imgs=1000): + """Loads all colored images of a concept. + + Args: + concept: The name of the concept to be loaded + max_imgs: maximum number of images to be loaded + + Returns: + Images of the desired concept or class. + """ + concept_dir = os.path.join(self.source_dir, concept) + img_paths = [ + os.path.join(concept_dir, d) + for d in tf.gfile.ListDirectory(concept_dir) + ] + return tcav_helpers.load_images_from_files( + img_paths, + max_imgs=max_imgs, + return_filenames=False, + do_shuffle=False, + run_parallel=(self.num_workers > 0), + shape=(self.image_shape), + num_workers=self.num_workers) + + def create_patches(self, method='slic', discovery_images=None, + param_dict=None): + """Creates a set of image patches using superpixel methods. + + This method takes in the concept discovery images and transforms it to a + dataset made of the patches of those images. + + Args: + method: The superpixel method used for creating image patches. One of + 'slic', 'watershed', 'quickshift', 'felzenszwalb'. + discovery_images: Images used for creating patches. If None, the images in + the target class folder are used. + + param_dict: Contains parameters of the superpixel method used in the form + of {'param1':[a,b,...], 'param2':[z,y,x,...], ...}. For instance + {'n_segments':[15,50,80], 'compactness':[10,10,10]} for slic + method. + """ + if param_dict is None: + param_dict = {} + dataset, image_numbers, patches = [], [], [] + if discovery_images is None: + raw_imgs = self.load_concept_imgs( + self.target_class, self.num_discovery_imgs) + self.discovery_images = raw_imgs + else: + self.discovery_images = discovery_images + if self.num_workers: + pool = multiprocessing.Pool(self.num_workers) + outputs = pool.map( + lambda img: self._return_superpixels(img, method, param_dict), + self.discovery_images) + for fn, sp_outputs in enumerate(outputs): + image_superpixels, image_patches = sp_outputs + for superpixel, patch in zip(image_superpixels, image_patches): + dataset.append(superpixel) + patches.append(patch) + image_numbers.append(fn) + else: + for fn, img in enumerate(self.discovery_images): + image_superpixels, image_patches = self._return_superpixels( + img, method, param_dict) + for superpixel, patch in zip(image_superpixels, image_patches): + dataset.append(superpixel) + patches.append(patch) + image_numbers.append(fn) + self.dataset, self.image_numbers, self.patches =\ + np.array(dataset), np.array(image_numbers), np.array(patches) + + def _return_superpixels(self, img, method='slic', + param_dict=None): + """Returns all patches for one image. + + Given an image, calculates superpixels for each of the parameter lists in + param_dict and returns a set of unique superpixels by + removing duplicates. If two patches have Jaccard similarity more than 0.5, + they are concidered duplicates. + + Args: + img: The input image + method: superpixel method, one of slic, watershed, quichsift, or + felzenszwalb + param_dict: Contains parameters of the superpixel method used in the form + of {'param1':[a,b,...], 'param2':[z,y,x,...], ...}. For instance + {'n_segments':[15,50,80], 'compactness':[10,10,10]} for slic + method. + Raises: + ValueError: if the segementation method is invaled. + """ + if param_dict is None: + param_dict = {} + if method == 'slic': + n_segmentss = param_dict.pop('n_segments', [15, 50, 80]) + n_params = len(n_segmentss) + compactnesses = param_dict.pop('compactness', [20] * n_params) + sigmas = param_dict.pop('sigma', [1.] * n_params) + elif method == 'watershed': + markerss = param_dict.pop('marker', [15, 50, 80]) + n_params = len(markerss) + compactnesses = param_dict.pop('compactness', [0.] * n_params) + elif method == 'quickshift': + max_dists = param_dict.pop('max_dist', [20, 15, 10]) + n_params = len(max_dists) + ratios = param_dict.pop('ratio', [1.0] * n_params) + kernel_sizes = param_dict.pop('kernel_size', [10] * n_params) + elif method == 'felzenszwalb': + scales = param_dict.pop('scale', [1200, 500, 250]) + n_params = len(scales) + sigmas = param_dict.pop('sigma', [0.8] * n_params) + min_sizes = param_dict.pop('min_size', [20] * n_params) + else: + raise ValueError('Invalid superpixel method!') + unique_masks = [] + for i in range(n_params): + param_masks = [] + if method == 'slic': + segments = segmentation.slic( + img, n_segments=n_segmentss[i], compactness=compactnesses[i], + sigma=sigmas[i]) + elif method == 'watershed': + segments = segmentation.watershed( + img, markers=markerss[i], compactness=compactnesses[i]) + elif method == 'quickshift': + segments = segmentation.quickshift( + img, kernel_size=kernel_sizes[i], max_dist=max_dists[i], + ratio=ratios[i]) + elif method == 'felzenszwalb': + segments = segmentation.felzenszwalb( + img, scale=scales[i], sigma=sigmas[i], min_size=min_sizes[i]) + for s in range(segments.max()): + mask = (segments == s).astype(float) + if np.mean(mask) > 0.001: + unique = True + for seen_mask in unique_masks: + jaccard = np.sum(seen_mask * mask) / np.sum((seen_mask + mask) > 0) + if jaccard > 0.5: + unique = False + break + if unique: + param_masks.append(mask) + unique_masks.extend(param_masks) + superpixels, patches = [], [] + while unique_masks: + superpixel, patch = self._extract_patch(img, unique_masks.pop()) + superpixels.append(superpixel) + patches.append(patch) + return superpixels, patches + + def _extract_patch(self, image, mask): + """Extracts a patch out of an image. + + Args: + image: The original image + mask: The binary mask of the patch area + + Returns: + image_resized: The resized patch such that its boundaries touches the + image boundaries + patch: The original patch. Rest of the image is padded with average value + """ + mask_expanded = np.expand_dims(mask, -1) + patch = (mask_expanded * image + ( + 1 - mask_expanded) * float(self.average_image_value) / 255) + ones = np.where(mask == 1) + h1, h2, w1, w2 = ones[0].min(), ones[0].max(), ones[1].min(), ones[1].max() + image = Image.fromarray((patch[h1:h2, w1:w2] * 255).astype(np.uint8)) + image_resized = np.array(image.resize(self.image_shape, + Image.BICUBIC)).astype(float) / 255 + return image_resized, patch + + def _patch_activations(self, imgs, bottleneck, bs=100, channel_mean=None): + """Returns activations of a list of imgs. + + Args: + imgs: List/array of images to calculate the activations of + bottleneck: Name of the bottleneck layer of the model where activations + are calculated + bs: The batch size for calculating activations. (To control computational + cost) + channel_mean: If true, the activations are averaged across channel. + + Returns: + The array of activations + """ + if channel_mean is None: + channel_mean = self.channel_mean + if self.num_workers: + pool = multiprocessing.Pool(self.num_workers) + output = pool.map( + lambda i: self.model.run_imgs(imgs[i * bs:(i + 1) * bs], bottleneck), + np.arange(int(imgs.shape[0] / bs) + 1)) + else: + output = [] + for i in range(int(imgs.shape[0] / bs) + 1): + output.append( + self.model.run_imgs(imgs[i * bs:(i + 1) * bs], bottleneck)) + output = np.concatenate(output, 0) + if channel_mean and len(output.shape) > 3: + output = np.mean(output, (1, 2)) + else: + output = np.reshape(output, [output.shape[0], -1]) + return output + + def _cluster(self, acts, method='KM', param_dict=None): + """Runs unsupervised clustering algorithm on concept actiavtations. + + Args: + acts: activation vectors of datapoints points in the bottleneck layer. + E.g. (number of clusters,) for Kmeans + method: clustering method. We have: + 'KM': Kmeans Clustering + 'AP': Affinity Propagation + 'SC': Spectral Clustering + 'MS': Mean Shift clustering + 'DB': DBSCAN clustering method + param_dict: Contains superpixl method's parameters. If an empty dict is + given, default parameters are used. + + Returns: + asg: The cluster assignment label of each data points + cost: The clustering cost of each data point + centers: The cluster centers. For methods like Affinity Propagetion + where they do not return a cluster center or a clustering cost, it + calculates the medoid as the center and returns distance to center as + each data points clustering cost. + + Raises: + ValueError: if the clustering method is invalid. + """ + if param_dict is None: + param_dict = {} + centers = None + if method == 'KM': + n_clusters = param_dict.pop('n_clusters', 25) + km = cluster.KMeans(n_clusters) + d = km.fit(acts) + centers = km.cluster_centers_ + d = np.linalg.norm( + np.expand_dims(acts, 1) - np.expand_dims(centers, 0), ord=2, axis=-1) + asg, cost = np.argmin(d, -1), np.min(d, -1) + elif method == 'AP': + damping = param_dict.pop('damping', 0.5) + ca = cluster.AffinityPropagation(damping) + ca.fit(acts) + centers = ca.cluster_centers_ + d = np.linalg.norm( + np.expand_dims(acts, 1) - np.expand_dims(centers, 0), ord=2, axis=-1) + asg, cost = np.argmin(d, -1), np.min(d, -1) + elif method == 'MS': + ms = cluster.MeanShift(n_jobs=self.num_workers) + asg = ms.fit_predict(acts) + elif method == 'SC': + n_clusters = param_dict.pop('n_clusters', 25) + sc = cluster.SpectralClustering( + n_clusters=n_clusters, n_jobs=self.num_workers) + asg = sc.fit_predict(acts) + elif method == 'DB': + eps = param_dict.pop('eps', 0.5) + min_samples = param_dict.pop('min_samples', 20) + sc = cluster.DBSCAN(eps, min_samples, n_jobs=self.num_workers) + asg = sc.fit_predict(acts) + else: + raise ValueError('Invalid Clustering Method!') + if centers is None: ## If clustering returned cluster centers, use medoids + centers = np.zeros((asg.max() + 1, acts.shape[1])) + cost = np.zeros(len(acts)) + for cluster_label in range(asg.max() + 1): + cluster_idxs = np.where(asg == cluster_label)[0] + cluster_points = acts[cluster_idxs] + pw_distances = metrics.euclidean_distances(cluster_points) + centers[cluster_label] = cluster_points[np.argmin( + np.sum(pw_distances, -1))] + cost[cluster_idxs] = np.linalg.norm( + acts[cluster_idxs] - np.expand_dims(centers[cluster_label], 0), + ord=2, + axis=-1) + return asg, cost, centers + + def discover_concepts(self, + method='KM', + activations=None, + param_dicts=None): + """Discovers the frequent occurring concepts in the target class. + + Calculates self.dic, a dicationary containing all the informations of the + discovered concepts in the form of {'bottleneck layer name: bn_dic} where + bn_dic itself is in the form of {'concepts:list of concepts, + 'concept name': concept_dic} where the concept_dic is in the form of + {'images': resized patches of concept, 'patches': original patches of the + concepts, 'image_numbers': image id of each patch} + + Args: + method: Clustering method. + activations: If activations are already calculated. If not calculates + them. Must be a dictionary in the form of {'bn':array, ...} + param_dicts: A dictionary in the format of {'bottleneck':param_dict,...} + where param_dict contains the clustering method's parametrs + in the form of {'param1':value, ...}. For instance for Kmeans + {'n_clusters':25}. param_dicts can also be in the format + of param_dict where same parameters are used for all + bottlenecks. + """ + if param_dicts is None: + param_dicts = {} + if set(param_dicts.keys()) != set(self.bottlenecks): + param_dicts = {bn: param_dicts for bn in self.bottlenecks} + self.dic = {} ## The main dictionary of the ConceptDiscovery class. + for bn in self.bottlenecks: + bn_dic = {} + if activations is None or bn not in activations.keys(): + bn_activations = self._patch_activations(self.dataset, bn) + else: + bn_activations = activations[bn] + bn_dic['label'], bn_dic['cost'], centers = self._cluster( + bn_activations, method, param_dicts[bn]) + concept_number, bn_dic['concepts'] = 0, [] + for i in range(bn_dic['label'].max() + 1): + label_idxs = np.where(bn_dic['label'] == i)[0] + if len(label_idxs) > self.min_imgs: + concept_costs = bn_dic['cost'][label_idxs] + concept_idxs = label_idxs[np.argsort(concept_costs)[:self.max_imgs]] + concept_image_numbers = set(self.image_numbers[label_idxs]) + discovery_size = len(self.discovery_images) + highly_common_concept = len( + concept_image_numbers) > 0.5 * len(label_idxs) + mildly_common_concept = len( + concept_image_numbers) > 0.25 * len(label_idxs) + mildly_populated_concept = len( + concept_image_numbers) > 0.25 * discovery_size + cond2 = mildly_populated_concept and mildly_common_concept + non_common_concept = len( + concept_image_numbers) > 0.1 * len(label_idxs) + highly_populated_concept = len( + concept_image_numbers) > 0.5 * discovery_size + cond3 = non_common_concept and highly_populated_concept + if highly_common_concept or cond2 or cond3: + concept_number += 1 + concept = '{}_concept{}'.format(self.target_class, concept_number) + bn_dic['concepts'].append(concept) + bn_dic[concept] = { + 'images': self.dataset[concept_idxs], + 'patches': self.patches[concept_idxs], + 'image_numbers': self.image_numbers[concept_idxs] + } + bn_dic[concept + '_center'] = centers[i] + bn_dic.pop('label', None) + bn_dic.pop('cost', None) + self.dic[bn] = bn_dic + + def _random_concept_activations(self, bottleneck, random_concept): + """Wrapper for computing or loading activations of random concepts. + + Takes care of making, caching (if desired) and loading activations. + + Args: + bottleneck: The bottleneck layer name + random_concept: Name of the random concept e.g. "random500_0" + + Returns: + A nested dict in the form of {concept:{bottleneck:activation}} + """ + rnd_acts_path = os.path.join(self.activation_dir, 'acts_{}_{}'.format( + random_concept, bottleneck)) + if not tf.gfile.Exists(rnd_acts_path): + rnd_imgs = self.load_concept_imgs(random_concept, self.max_imgs) + acts = tcav_helpers.get_acts_from_images(rnd_imgs, self.model, bottleneck) + with tf.gfile.Open(rnd_acts_path, 'w') as f: + np.save(f, acts, allow_pickle=False) + del acts + del rnd_imgs + with tf.gfile.Open(rnd_acts_path, 'r') as f: + return np.load(f).squeeze() + + def _calculate_cav(self, c, r, bn, act_c, ow, directory=None): + """Calculates a sinle cav for a concept and a one random counterpart. + + Args: + c: conept name + r: random concept name + bn: the layer name + act_c: activation matrix of the concept in the 'bn' layer + ow: overwrite if CAV already exists + directory: to save the generated CAV + + Returns: + The accuracy of the CAV + """ + if directory is None: + directory = self.cav_dir + act_r = self._random_concept_activations(bn, r) + cav_instance = cav.get_or_train_cav([c, r], + bn, { + c: { + bn: act_c + }, + r: { + bn: act_r + } + }, + cav_dir=directory, + overwrite=ow) + return cav_instance.accuracies['overall'] + + def _concept_cavs(self, bn, concept, activations, randoms=None, ow=True): + """Calculates CAVs of a concept versus all the random counterparts. + + Args: + bn: bottleneck layer name + concept: the concept name + activations: activations of the concept in the bottleneck layer + randoms: None if the class random concepts are going to be used + ow: If true, overwrites the existing CAVs + + Returns: + A dict of cav accuracies in the form of {'bottleneck layer': + {'concept name':[list of accuracies], ...}, ...} + """ + if randoms is None: + randoms = [ + 'random500_{}'.format(i) for i in np.arange(self.num_random_exp) + ] + if self.num_workers: + pool = multiprocessing.Pool(20) + accs = pool.map( + lambda rnd: self._calculate_cav(concept, rnd, bn, activations, ow), + randoms) + else: + accs = [] + for rnd in randoms: + accs.append(self._calculate_cav(concept, rnd, bn, activations, ow)) + return accs + + def cavs(self, min_acc=0., ow=True): + """Calculates cavs for all discovered concepts. + + This method calculates and saves CAVs for all the discovered concepts + versus all random concepts in all the bottleneck layers + + Args: + min_acc: Delete discovered concept if the average classification accuracy + of the CAV is less than min_acc + ow: If True, overwrites an already calcualted cav. + + Returns: + A dicationary of classification accuracy of linear boundaries orthogonal + to cav vectors + """ + acc = {bn: {} for bn in self.bottlenecks} + concepts_to_delete = [] + for bn in self.bottlenecks: + for concept in self.dic[bn]['concepts']: + concept_imgs = self.dic[bn][concept]['images'] + concept_acts = tcav_helpers.get_acts_from_images( + concept_imgs, self.model, bn) + acc[bn][concept] = self._concept_cavs(bn, concept, concept_acts, ow=ow) + if np.mean(acc[bn][concept]) < min_acc: + concepts_to_delete.append((bn, concept)) + target_class_acts = tcav_helpers.get_acts_from_images( + self.discovery_images, self.model, bn) + acc[bn][self.target_class] = self._concept_cavs( + bn, self.target_class, target_class_acts, ow=ow) + rnd_acts = self._random_concept_activations(bn, self.random_concept) + acc[bn][self.random_concept] = self._concept_cavs( + bn, self.random_concept, rnd_acts, ow=ow) + for bn, concept in concepts_to_delete: + self.delete_concept(bn, concept) + return acc + + def load_cav_direction(self, c, r, bn, directory=None): + """Loads an already computed cav. + + Args: + c: concept name + r: random concept name + bn: bottleneck layer + directory: where CAV is saved + + Returns: + The cav instance + """ + if directory is None: + directory = self.cav_dir + params = tf.contrib.training.HParams(model_type='linear', alpha=.01) + cav_key = cav.CAV.cav_key([c, r], bn, params.model_type, params.alpha) + cav_path = os.path.join(self.cav_dir, cav_key.replace('/', '.') + '.pkl') + vector = cav.CAV.load_cav(cav_path).cavs[0] + return np.expand_dims(vector, 0) / np.linalg.norm(vector, ord=2) + + def _sort_concepts(self, scores): + for bn in self.bottlenecks: + tcavs = [] + for concept in self.dic[bn]['concepts']: + tcavs.append(np.mean(scores[bn][concept])) + concepts = [] + for idx in np.argsort(tcavs)[::-1]: + concepts.append(self.dic[bn]['concepts'][idx]) + self.dic[bn]['concepts'] = concepts + + def _return_gradients(self, images): + """For the given images calculates the gradient tensors. + + Args: + images: Images for which we want to calculate gradients. + + Returns: + A dictionary of images gradients in all bottleneck layers. + """ + + gradients = {} + class_id = self.model.label_to_id(self.target_class.replace('_', ' ')) + for bn in self.bottlenecks: + acts = tcav_helpers.get_acts_from_images(images, self.model, bn) + bn_grads = np.zeros((acts.shape[0], np.prod(acts.shape[1:]))) + for i in range(len(acts)): + bn_grads[i] = self.model.get_gradient( + acts[i:i+1], [class_id], bn).reshape(-1) + gradients[bn] = bn_grads + return gradients + + def _tcav_score(self, bn, concept, rnd, gradients): + """Calculates and returns the TCAV score of a concept. + + Args: + bn: bottleneck layer + concept: concept name + rnd: random counterpart + gradients: Dict of gradients of tcav_score_images + + Returns: + TCAV score of the concept with respect to the given random counterpart + """ + vector = self.load_cav_direction(concept, rnd, bn) + prod = np.sum(gradients[bn] * vector, -1) + return np.mean(prod < 0) + + def tcavs(self, test=False, sort=True, tcav_score_images=None): + """Calculates TCAV scores for all discovered concepts and sorts concepts. + + This method calculates TCAV scores of all the discovered concepts for + the target class using all the calculated cavs. It later sorts concepts + based on their TCAV scores. + + Args: + test: If true, perform statistical testing and removes concepts that don't + pass + sort: If true, it will sort concepts in each bottleneck layers based on + average TCAV score of the concept. + tcav_score_images: Target class images used for calculating tcav scores. + If None, the target class source directory images are used. + + Returns: + A dictionary of the form {'bottleneck layer':{'concept name': + [list of tcav scores], ...}, ...} containing TCAV scores. + """ + + tcav_scores = {bn: {} for bn in self.bottlenecks} + randoms = ['random500_{}'.format(i) for i in np.arange(self.num_random_exp)] + if tcav_score_images is None: # Load target class images if not given + raw_imgs = self.load_concept_imgs(self.target_class, 2 * self.max_imgs) + tcav_score_images = raw_imgs[-self.max_imgs:] + gradients = self._return_gradients(tcav_score_images) + for bn in self.bottlenecks: + for concept in self.dic[bn]['concepts'] + [self.random_concept]: + def t_func(rnd): + return self._tcav_score(bn, concept, rnd, gradients) + if self.num_workers: + pool = multiprocessing.Pool(self.num_workers) + tcav_scores[bn][concept] = pool.map(lambda rnd: t_func(rnd), randoms) + else: + tcav_scores[bn][concept] = [t_func(rnd) for rnd in randoms] + if test: + self.test_and_remove_concepts(tcav_scores) + if sort: + self._sort_concepts(tcav_scores) + return tcav_scores + + def do_statistical_testings(self, i_ups_concept, i_ups_random): + """Conducts ttest to compare two set of samples. + + In particular, if the means of the two samples are staistically different. + + Args: + i_ups_concept: samples of TCAV scores for concept vs. randoms + i_ups_random: samples of TCAV scores for random vs. randoms + + Returns: + p value + """ + min_len = min(len(i_ups_concept), len(i_ups_random)) + _, p = stats.ttest_rel(i_ups_concept[:min_len], i_ups_random[:min_len]) + return p + + def test_and_remove_concepts(self, tcav_scores): + """Performs statistical testing for all discovered concepts. + + Using TCAV socres of the discovered concepts versurs the random_counterpart + concept, performs statistical testing and removes concepts that do not pass + + Args: + tcav_scores: Calculated dicationary of tcav scores of all concepts + """ + concepts_to_delete = [] + for bn in self.bottlenecks: + for concept in self.dic[bn]['concepts']: + pvalue = self.do_statistical_testings\ + (tcav_scores[bn][concept], tcav_scores[bn][self.random_concept]) + if pvalue > 0.01: + concepts_to_delete.append((bn, concept)) + for bn, concept in concepts_to_delete: + self.delete_concept(bn, concept) + + def delete_concept(self, bn, concept): + """Removes a discovered concepts if it's not already removed. + + Args: + bn: Bottleneck layer where the concepts is discovered. + concept: concept name + """ + self.dic[bn].pop(concept, None) + if concept in self.dic[bn]['concepts']: + self.dic[bn]['concepts'].pop(self.dic[bn]['concepts'].index(concept)) + + def _concept_profile(self, bn, activations, concept, randoms): + """Transforms data points from activations space to concept space. + + Calculates concept profile of data points in the desired bottleneck + layer's activation space for one of the concepts + + Args: + bn: Bottleneck layer + activations: activations of the data points in the bottleneck layer + concept: concept name + randoms: random concepts + + Returns: + The projection of activations of all images on all CAV directions of + the given concept + """ + def t_func(rnd): + products = self.load_cav_direction(concept, rnd, bn) * activations + return np.sum(products, -1) + if self.num_workers: + pool = multiprocessing.Pool(self.num_workers) + profiles = pool.map(lambda rnd: t_func(rnd), randoms) + else: + profiles = [t_func(rnd) for rnd in randoms] + return np.stack(profiles, axis=-1) + + def find_profile(self, bn, images, mean=True): + """Transforms images from pixel space to concept space. + + Args: + bn: Bottleneck layer + images: Data points to be transformed + mean: If true, the profile of each concept would be the average inner + product of all that concepts' CAV vectors rather than the stacked up + version. + + Returns: + The concept profile of input images in the bn layer. + """ + profile = np.zeros((len(images), len(self.dic[bn]['concepts']), + self.num_random_exp)) + class_acts = tcav_helpers.get_acts_from_images( + images, self.model, bn).reshape([len(images), -1]) + randoms = ['random500_{}'.format(i) for i in range(self.num_random_exp)] + for i, concept in enumerate(self.dic[bn]['concepts']): + profile[:, i, :] = self._concept_profile(bn, class_acts, concept, randoms) + if mean: + profile = np.mean(profile, -1) + return profile + diff --git a/ace_helpers.py b/ace_helpers.py new file mode 100644 index 0000000..ede1759 --- /dev/null +++ b/ace_helpers.py @@ -0,0 +1,399 @@ +""" collection of various helper functions for running ACE""" + +from multiprocessing import dummy as multiprocessing +import sys +import os +from matplotlib import pyplot as plt +import matplotlib.gridspec as gridspec +import model +import numpy as np +from PIL import Image +from skimage.segmentation import mark_boundaries +from sklearn import linear_model +from sklearn.model_selection import cross_val_score +import tensorflow as tf + +def make_model(model_to_run, sess, randomize=False, model_path=None, + labels_path=None): + """Make an instance of a model. + + Args: + model_to_run: a string that describes which model to make. + sess: tf session instance. + randomize: Start with random weights + model_path: Path to models saved graph. If None uses default paths + labels_path: Path to models line separated labels text file. If None uses + default labels. + + Returns: + a model instance. + + Raises: + ValueError: If model name is not valid. + """ + models_root = MODEL_LOCATION + if model_to_run == 'InceptionV3': + basepath = models_root + 'inception_v3/' + if model_path is None: + model_path = basepath + 'tensorflow_inception_graph.pb' + if labels_path is None: + labels_path = basepath + 'imagenet_comp_graph_label_strings.txt' + mymodel = model.InceptionV3Wrapper_public( + sess, model_saved_path=model_path, labels_path=labels_path) + elif model_to_run == 'GoogleNet': + basepath = models_root + 'tensorflow_inception/' + if model_path is None: + model_path = basepath + 'tensorflow_inception_graph.pb' + if labels_path is None: + labels_path = basepath + 'imagenet_comp_graph_label_strings.txt' + # common_typos_disable + mymodel = model.GoolgeNetWrapper_public( + sess, model_saved_path=model_path, labels_path=labels_path) + else: + raise ValueError('Invalid model name') + if randomize: # randomize the network! + sess.run(tf.global_variables_initializer()) + return mymodel + + +def flat_profile(cd, images, bottlenecks=None): + """Returns concept profile of given images. + + Given a ConceptDiscovery class instance and a set of images, and desired + bottleneck layers, calculates the profile of each image with all concepts and + returns a profile vector + + Args: + cd: The concept discovery class instance + images: The images for which the concept profile is calculated + bottlenecks: Bottleck layers where the profile is calculated. If None, cd + bottlenecks will be used. + + Returns: + The concepts profile of input images using discovered concepts in + all bottleneck layers. + + Raises: + ValueError: If bottlenecks is not in right format. + """ + profiles = [] + if bottlenecks is None: + bottlenecks = list(cd.dic.keys()) + if isinstance(bottlenecks, str): + bottlenecks = [bottlenecks] + elif not isinstance(bottlenecks, list) and not isinstance(bottlenecks, tuple): + raise ValueError('Invalid bottlenecks parameter!') + for bn in bottlenecks: + profiles.append(cd.find_profile(str(bn), images).reshape((len(images), -1))) + profile = np.concatenate(profiles, -1) + return profile + + +def cross_val(a, b, methods): + """Performs cross validation for a binary classification task. + + Args: + a: First class data points as rows + b: Second class data points as rows + methods: The sklearn classification models to perform cross-validation on + + Returns: + The best performing trained binary classification odel + """ + x, y = binary_dataset(a, b) + best_acc = 0. + if isinstance(methods, str): + methods = [methods] + best_acc = 0. + for method in methods: + temp_acc = 0. + params = [10**e for e in [-4, -3, -2, -1, 0, 1, 2, 3]] + for param in params: + clf = give_classifier(method, param) + acc = cross_val_score(clf, x, y, cv=min(100, max(2, int(len(y) / 10)))) + if np.mean(acc) > temp_acc: + temp_acc = np.mean(acc) + best_param = param + if temp_acc > best_acc: + best_acc = temp_acc + final_clf = give_classifier(method, best_param) + final_clf.fit(x, y) + return final_clf, best_acc + + +def give_classifier(method, param): + """Returns an sklearn classification model. + + Args: + method: Name of the sklearn classification model + param: Hyperparameters of the sklearn model + + Returns: + An untrained sklearn classification model + + Raises: + ValueError: if the model name is invalid. + """ + if method == 'logistic': + return linear_model.LogisticRegression(C=param) + elif method == 'sgd': + return linear_model.SGDClassifier(alpha=param) + else: + raise ValueError('Invalid model!') + + +def binary_dataset(pos, neg, balanced=True): + """Creates a binary dataset given instances of two classes. + + Args: + pos: Data points of the first class as rows + neg: Data points of the second class as rows + balanced: If true, it creates a balanced binary dataset. + + Returns: + The data points of the created data set as rows and the corresponding labels + """ + if balanced: + min_len = min(neg.shape[0], pos.shape[0]) + ridxs = np.random.permutation(np.arange(2 * min_len)) + x = np.concatenate([neg[:min_len], pos[:min_len]], 0)[ridxs] + y = np.concatenate([np.zeros(min_len), np.ones(min_len)], 0)[ridxs] + else: + ridxs = np.random.permutation(np.arange(len(neg) + len(pos))) + x = np.concatenate([neg, pos], 0)[ridxs] + y = np.concatenate( + [np.zeros(neg.shape[0]), np.ones(pos.shape[0])], 0)[ridxs] + return x, y + + +def plot_concepts(cd, bn, num=10, address=None, mode='diverse', concepts=None): + """Plots examples of discovered concepts. + + Args: + cd: The concept discovery instance + bn: Bottleneck layer name + num: Number of images to print out of each concept + address: If not None, saves the output to the address as a .PNG image + mode: If 'diverse', it prints one example of each of the target class images + is coming from. If 'radnom', randomly samples exmples of the concept. If + 'max', prints out the most activating examples of that concept. + concepts: If None, prints out examples of all discovered concepts. + Otherwise, it should be either a list of concepts to print out examples of + or just one concept's name + + Raises: + ValueError: If the mode is invalid. + """ + if concepts is None: + concepts = cd.dic[bn]['concepts'] + elif not isinstance(concepts, list) and not isinstance(concepts, tuple): + concepts = [concepts] + num_concepts = len(concepts) + plt.rcParams['figure.figsize'] = num * 2.1, 4.3 * num_concepts + fig = plt.figure(figsize=(num * 2, 4 * num_concepts)) + outer = gridspec.GridSpec(num_concepts, 1, wspace=0., hspace=0.3) + for n, concept in enumerate(concepts): + inner = gridspec.GridSpecFromSubplotSpec( + 2, num, subplot_spec=outer[n], wspace=0, hspace=0.1) + concept_images = cd.dic[bn][concept]['images'] + concept_patches = cd.dic[bn][concept]['patches'] + concept_image_numbers = cd.dic[bn][concept]['image_numbers'] + if mode == 'max': + idxs = np.arange(len(concept_images)) + elif mode == 'random': + idxs = np.random.permutation(np.arange(len(concept_images))) + elif mode == 'diverse': + idxs = [] + while True: + seen = set() + for idx in range(len(concept_images)): + if concept_image_numbers[idx] not in seen and idx not in idxs: + seen.add(concept_image_numbers[idx]) + idxs.append(idx) + if len(idxs) == len(concept_images): + break + else: + raise ValueError('Invalid mode!') + idxs = idxs[:num] + for i, idx in enumerate(idxs): + ax = plt.Subplot(fig, inner[i]) + ax.imshow(concept_images[idx]) + ax.set_xticks([]) + ax.set_yticks([]) + if i == int(num / 2): + ax.set_title(concept) + ax.grid(False) + fig.add_subplot(ax) + ax = plt.Subplot(fig, inner[i + num]) + mask = 1 - (np.mean(concept_patches[idx] == float( + cd.average_image_value) / 255, -1) == 1) + image = cd.discovery_images[concept_image_numbers[idx]] + ax.imshow(mark_boundaries(image, mask, color=(1, 1, 0), mode='thick')) + ax.set_xticks([]) + ax.set_yticks([]) + ax.set_title(str(concept_image_numbers[idx])) + ax.grid(False) + fig.add_subplot(ax) + plt.suptitle(bn) + if address is not None: + with tf.gfile.Open(address + bn + '.png', 'w') as f: + fig.savefig(f) + plt.clf() + plt.close(fig) + + +def cosine_similarity(a, b): + """Cosine similarity of two vectors.""" + assert a.shape == b.shape, 'Two vectors must have the same dimensionality' + a_norm, b_norm = np.linalg.norm(a), np.linalg.norm(b) + if a_norm * b_norm == 0: + return 0. + cos_sim = np.sum(a * b) / (a_norm * b_norm) + return cos_sim + + +def similarity(cd, num_random_exp=None, num_workers=25): + """Returns cosine similarity of all discovered concepts. + + Args: + cd: The ConceptDiscovery module for discovered conceps. + num_random_exp: If None, calculates average similarity using all the class's + random concepts. If a number, uses that many random counterparts. + num_workers: If greater than 0, runs the function in parallel. + + Returns: + A similarity dict in the form of {(concept1, concept2):[list of cosine + similarities]} + """ + + def concepts_similarity(cd, concepts, rnd, bn): + """Calcualtes the cosine similarity of concept cavs. + + This function calculates the pairwise cosine similarity of all concept cavs + versus an specific random concept + + Args: + cd: The ConceptDiscovery instance + concepts: List of concepts to calculate similarity for + rnd: a random counterpart + bn: bottleneck layer the concepts belong to + + Returns: + A dictionary of cosine similarities in the form of + {(concept1, concept2): [list of cosine similarities], ...} + """ + similarity_dic = {} + for c1 in concepts: + cav1 = cd.load_cav_direction(c1, rnd, bn) + for c2 in concepts: + if (c1, c2) in similarity_dic.keys(): + continue + cav2 = cd.load_cav_direction(c2, rnd, bn) + similarity_dic[(c1, c2)] = cosine_similarity(cav1, cav2) + similarity_dic[(c2, c1)] = similarity_dic[(c1, c2)] + return similarity_dic + + similarity_dic = {bn: {} for bn in cd.bottlenecks} + if num_random_exp is None: + num_random_exp = cd.num_random_exp + randoms = ['random500_{}'.format(i) for i in np.arange(num_random_exp)] + concepts = {} + for bn in cd.bottlenecks: + concepts[bn] = [cd.target_class, cd.random_concept] + cd.dic[bn]['concepts'] + for bn in cd.bottlenecks: + concept_pairs = [(c1, c2) for c1 in concepts[bn] for c2 in concepts[bn]] + similarity_dic[bn] = {pair: [] for pair in concept_pairs} + def t_func(rnd): + return concepts_similarity(cd, concepts[bn], rnd, bn) + if num_workers: + pool = multiprocessing.Pool(num_workers) + sims = pool.map(lambda rnd: t_func(rnd), randoms) + else: + sims = [t_func(rnd) for rnd in randoms] + while sims: + sim = sims.pop() + for pair in concept_pairs: + similarity_dic[bn][pair].append(sim[pair]) + return similarity_dic + + +def save_ace_report(cd, accs, scores, address): + """Saves TCAV scores. + + Saves the average CAV accuracies and average TCAV scores of the concepts + discovered in ConceptDiscovery instance. + + Args: + cd: The ConceptDiscovery instance. + accs: The cav accuracy dictionary returned by cavs method of the + ConceptDiscovery instance + scores: The tcav score dictionary returned by tcavs method of the + ConceptDiscovery instance + address: The address to save the text file in. + """ + report = '\n\n\t\t\t ---CAV accuracies---' + for bn in cd.bottlenecks: + report += '\n' + for concept in cd.dic[bn]['concepts']: + report += '\n' + bn + ':' + concept + ':' + str( + np.mean(accs[bn][concept])) + save_report(address, report) + report = '\n\n\t\t\t ---TCAV scores---' + for bn in cd.bottlenecks: + report += '\n' + for concept in cd.dic[bn]['concepts']: + pvalue = cd.do_statistical_testings( + scores[bn][concept], scores[bn][cd.random_concept]) + report += '\n{}:{}:{},{}'.format(bn, concept, + np.mean(scores[bn][concept]), pvalue) + with tf.gfile.open(address, 'w') as f: + f.write(report) + + +def save_concepts(cd, concepts_dir): + """Saves discovered concept's images or patches. + + Args: + cd: The ConceptDiscovery instance the concepts of which we want to save + concepts_dir: The directory to save the concept images + """ + for bn in cd.bottlenecks: + for concept in cd.dic[bn]['concepts']: + patches_dir = os.path.join(concepts_dir, bn + '_' + concept + '_patches') + images_dir = os.path.join(concepts_dir, bn + '_' + concept) + patches = (np.clip(cd.dic[bn][concept]['patches'], 0, 1) * 256).astype( + np.uint8) + images = (np.clip(cd.dic[bn][concept]['images'], 0, 1) * 256).astype( + np.uint8) + tf.gfile.MakeDirs(patches_dir) + tf.gfile.MakeDirs(images_dir) + image_numbers = cd.dic[bn][concept]['image_numbers'] + image_addresses, patch_addresses = [], [] + for i in range(len(images)): + image_name = '0' * int(np.ceil(2 - np.log10(i + 1))) + '{}_{}'.format( + i + 1, image_numbers[i]) + patch_addresses.append(os.path.join(patches_dir, image_name + '.png')) + image_addresses.append(os.path.join(images_dir, image_name + '.png')) + save_images(patch_addresses, patches) + save_images(image_addresses, images) + + +def save_images(addresses, images): + """Save images in the addresses. + + Args: + addresses: The list of addresses to save the images as or the address of the + directory to save all images in. (list or str) + images: The list of all images in numpy uint8 format. + """ + if not isinstance(addresses, list): + image_addresses = [] + for i, image in enumerate(images): + image_name = '0' * (3 - int(np.log10(i + 1))) + str(i + 1) + '.png' + image_addresses.append(os.path.join(addresses, image_name)) + addresses = image_addresses + assert len(addresses) == len(images), 'Invalid number of addresses' + for address, image in zip(addresses, images): + with tf.gfile.Open(address, 'w') as f: + Image.fromarray(image).save(f, format='PNG') + diff --git a/ace_run.py b/ace_run.py new file mode 100644 index 0000000..c553bef --- /dev/null +++ b/ace_run.py @@ -0,0 +1,164 @@ +"""This script runs the whole ACE method.""" + + +import sys +import os +import numpy as np +import sklearn.metrics as metrics +from tcav import utils +import tensorflow as tf + +import ace_helpers +from ace import ConceptDiscovery +import argparse + + +def main(args): + + source_dir = args.source_dir + test_dir = args.test_dir + working_dir = args.working_dir + model_to_run = args.model_to_run + target_class = args.target_class + bottlenecks = args.bottlenecks.split(',') + num_test = args.num_test + num_random_exp = args.num_random_exp + max_imgs = args.max_imgs + min_imgs = args.min_imgs + ###### related DIRs on CNS to store results ####### + discovered_concepts_dir = os.path.join(working_dir, 'concepts/') + results_dir = os.path.join(working_dir, 'results/') + cavs_dir = os.path.join(working_dir, 'cavs/') + activations_dir = os.path.join(working_dir, 'acts/') + results_summaries_dir = os.path.join(working_dir, 'results_summaries/') + if tf.gfile.Exists(working_dir): + tf.gfile.DeleteRecursively(working_dir) + tf.gfile.MakeDirs(working_dir) + tf.gfile.MakeDirs(discovered_concepts_dir) + tf.gfile.MakeDirs(results_dir) + tf.gfile.MakeDirs(cavs_dir) + tf.gfile.MakeDirs(activations_dir) + tf.gfile.MakeDirs(results_summaries_dir) + random_concept = 'random500_100' # Random concept for statistical testing + sess = utils.create_session() + mymodel = ace_helpers.make_model(model_to_run, sess) + # Creating the ConceptDiscovery class instance + cd = ConceptDiscovery( + mymodel, + target_class, + random_concept, + bottlenecks, + sess, + source_dir, + activations_dir, + cavs_dir, + num_random_exp=num_random_exp, + channel_mean=True, + max_imgs=max_imgs, + min_imgs=min_imgs, + num_discovery_imgs=max_imgs, + num_workers=25) + # Creating the dataset of image patches + cd.create_patches(param_dict={'n_segments': [15, 50, 80]}) + # Saving the concept discovery target class images + image_dir = os.path.join(discovered_concepts_dir, 'images') + tf.gfile.MakeDirs(image_dir) + ace_helpers.save_images(image_dir, + (cd.discovery_images * 256).astype(np.uint8)) + # Discovering Concepts + cd.discover_concepts(method='KM', param_dicts={'n_clusters': 25}) + del cd.dataset # Free memory + del cd.image_numbers + del cd.patches + # Save discovered concept images (resized and original sized) + ace_helpers.save_concepts(cd, discovered_concepts_dir) + # Calculating CAVs and TCAV scores + cav_accuraciess = cd.cavs(min_acc=0.0) + scores = cd.tcavs(test=False) + ace_helpers.save_ace_report(cd, cav_accuraciess, scores, + results_summaries_dir + 'ace_results.txt') + # Plot examples of discovered concepts + for bn in cd.bottlenecks: + ace_helpers.plot_concepts(cd, bn, 10, address=results_dir) + # Delete concepts that don't pass statistical testing + cd.test_and_remove_concepts(scores) + # Train a binary classifier on concept profiles + report = '\n\n\t\t\t ---Concept space---' + report += '\n\t ---Classifier Weights---\n\n' + pos_imgs = cd.load_concept_imgs(cd.target_class, + 2 * cd.max_imgs + num_test)[-num_test:] + neg_imgs = cd.load_concept_imgs('random500_180', num_test) + a = ace_helpers.flat_profile(cd, pos_imgs) + b = ace_helpers.flat_profile(cd, neg_imgs) + lm, _ = ace_helpers.cross_val(a, b, methods=['logistic']) + for bn in cd.bottlenecks: + report += bn + ':\n' + for i, concept in enumerate(cd.dic[bn]['concepts']): + report += concept + ':' + str(lm.coef_[-1][i]) + '\n' + # Test profile classifier on test images + cd.source_dir = test_dir + pos_imgs = cd.load_concept_imgs(cd.target_class, num_test) + neg_imgs = cd.load_concept_imgs('random500_180', num_test) + a = ace_helpers.flat_profile(cd, pos_imgs) + b = ace_helpers.flat_profile(cd, neg_imgs) + x, y = ace_helpers.binary_dataset(a, b, balanced=True) + probs = lm.predict_proba(x)[:, 1] + report += '\nProfile Classifier accuracy= {}'.format( + np.mean((probs > 0.5) == y)) + report += '\nProfile Classifier AUC= {}'.format( + metrics.roc_auc_score(y, probs)) + report += '\nProfile Classifier PR Area= {}'.format( + metrics.average_precision_score(y, probs)) + # Compare original network to profile classifier + target_id = cd.model.label_to_id(cd.target_class.replace('_', ' ')) + predictions = [] + for img in pos_imgs: + predictions.append(mymodel.get_predictions([img])) + predictions = np.concatenate(predictions, 0) + true_predictions = (np.argmax(predictions, -1) == target_id).astype(int) + truly_predicted = np.where(true_predictions)[0] + report += '\nNetwork Recall = ' + str(np.mean(true_predictions)) + report += ', ' + str(np.mean(np.max(predictions, -1)[truly_predicted])) + agreeableness = np.sum(lm.predict(a) * true_predictions)*1./\ + np.sum(true_predictions + 1e-10) + report += '\nProfile classifier agrees with network in {}%'.format( + 100 * agreeableness) + with tf.gfile.Open(results_summaries_dir + 'profile_classifier.txt', 'w') as f: + f.write(report) + +def parse_arguments(argv): + """Parses the arguments passed to the run.py script.""" + parser = argparse.ArgumentParser() + parser.add_argument('--source_dir', type=str, + help='''Directory where the network's classes image folders and random + concept folders are saved.''', default='./Imagenet_train') + parser.add_argument('--test_dir', type=str, + help='''Directory where the network's classes test image folders and random + concept folders are saved.''', default='./Imagenet_test') + parser.add_argument('--working_dir', type=str, + help='Directory to save the results.', default='./ACE') + parser.add_argument('--model_to_run', type=str, + help='The name of the model.', default='InceptionV3') + parser.add_argument('--target_class', type=str, + help='The name of the target class to be interpreted', default='Zebra') + parser.add_argument('--bottlenecks', type=str, + help='Names of the target layers of the network (comma separated)', + default='mixed_8') + parser.add_argument('--num_test', type=int, + help="Number of test images used for binary profile classifier", + default=20) + parser.add_argument('--num_random_exp', type=int, + help="Number of random experiments used for statistical testing, etc", + default=20) + parser.add_argument('--max_imgs', type=int, + help="Maximum number of images in a discovered concept", + default=40) + parser.add_argument('--min_imgs', type=int, + help="Minimum number of images in a discovered concept", + default=40) + return parser.parse_args(argv) + + +if __name__ == '__main__': + main(parse_arguments(sys.argv[1:])) +