└── HybridDocuments ├── ThirdParty └── LRP_and_DeepLIFT │ ├── code │ ├── LRP_linear_layer.pyc │ ├── LICENSE │ ├── LRP_linear_layer.py │ └── layers.py │ ├── LICENSE │ └── README ├── prep_models.sh ├── prep_manual.sh ├── SRC ├── config.py ├── util.py ├── manual_eval.py ├── main.py ├── pointing_game.py ├── task_methods.py ├── corpora.py └── explanation_methods.py └── README.md /HybridDocuments/ThirdParty/LRP_and_DeepLIFT/code/LRP_linear_layer.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NPoe/neural-nlp-explanation-experiment/HEAD/HybridDocuments/ThirdParty/LRP_and_DeepLIFT/code/LRP_linear_layer.pyc -------------------------------------------------------------------------------- /HybridDocuments/prep_models.sh: -------------------------------------------------------------------------------- 1 | cd Models 2 | 3 | for model in GRU QGRU LSTM QLSTM CNN; do 4 | for corpus in yelp newsgroup; do 5 | wget "www.cis.uni-muenchen.de/~poerner/blobs/neural_nlp_explanation_experiment/${model}_${corpus}.hdf5"; 6 | done; 7 | done; 8 | -------------------------------------------------------------------------------- /HybridDocuments/ThirdParty/LRP_and_DeepLIFT/LICENSE: -------------------------------------------------------------------------------- 1 | COPYRIGHT 2 | 3 | All contributions by Fraunhofer Heinrich Hertz Institute and TU Berlin: 4 | Copyright (c) 2017, Leila Arras, Gregoire Montavon, Klaus-Robert Mueller, Wojciech Samek 5 | All rights reserved. Free for academic use only. Patent pending. 6 | 7 | See subfolder code for individual license statement 8 | 9 | -------------------------------------------------------------------------------- /HybridDocuments/prep_manual.sh: -------------------------------------------------------------------------------- 1 | # download the manual evaluation benchmark [1] 2 | git clone https://github.com/SinaMohseni/ML-Interpretability-Evaluation-Benchmark ML-Interpretability-Evaluation-Benchmark 3 | 4 | cd ML-Interpretability-Evaluation-Benchmark/Text/org_documents/20news-bydate/20news-bydate-test/sci.electronics 5 | 6 | # convert files to utf-8 if necessary 7 | for F in *; do 8 | if file $F | grep 'ISO-8859'; then 9 | iconv -f ISO-8859-1 -t UTF-8 $F > tmp.txt 10 | mv tmp.txt $F 11 | fi 12 | done 13 | 14 | # [1] Mohseni, S., Ragan, E.D. (2018). A Human-Grounded Evaluation Benchmark for Local Explanations of Machine Learning. arXiv preprint arXiv:1801.05075. 15 | -------------------------------------------------------------------------------- /HybridDocuments/SRC/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Paths of necessary inputs, change if necessary! 3 | """ 4 | 5 | # directory where models, results etc. can be stored 6 | WORKDIR = ".." 7 | 8 | # text format pre-trained embedding 9 | # format per line: 10 | # word dim1 dim2 ... dim300 11 | # any 300 dimensional embedding is fine, we use GloVe 12 | GLOVEPATH = "../Glove/glove.840B.300d.txt" 13 | 14 | # json files containing sentiment analysis review data 15 | # format per line: 16 | # {'text': '...', 'stars': [1-5], ...} 17 | TRAINJSON = "../Data/reviews.PA.train" 18 | DEVJSON = "../Data/reviews.PA.dev" 19 | TESTJSON = "../Data/reviews.PA.test" 20 | 21 | # locations of third party repositories 22 | LRP_RNN_REPO = "../ThirdParty/LRP_and_DeepLIFT/code" 23 | 24 | # location of manual interpretability benchmark 25 | MANUAL_BENCHMARK = "../ML-Interpretability-Evaluation-Benchmark" 26 | 27 | ##################################################################### 28 | -------------------------------------------------------------------------------- /HybridDocuments/ThirdParty/LRP_and_DeepLIFT/code/LICENSE: -------------------------------------------------------------------------------- 1 | COPYRIGHT 2 | 3 | All contributions by Fraunhofer Heinrich Hertz Institute and TU Berlin: 4 | Copyright (c) 2017, Leila Arras, Gregoire Montavon, Klaus-Robert Mueller, Wojciech Samek 5 | All rights reserved. Free for academic use only. Patent pending. 6 | 7 | 8 | LICENSE 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions are met: 12 | 13 | 1. Redistributions of source code must retain the above copyright notice, this 14 | list of conditions and the following disclaimer. 15 | 2. Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | 31 | -------------------------------------------------------------------------------- /HybridDocuments/ThirdParty/LRP_and_DeepLIFT/README: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | This code release contains an implementation of Layer-wise Relevance Propagation (LRP) and DeepLIFT for (Q)LSTM, (Q)GRU and CNN as used for the paper [Evaluating Neural Network Explanation Methods using Hybrid Documents and Morphosyntactic Agreement by N. Poerner, B. Roth and H. Schütze, 2018](https://arxiv.org/abs/1801.06422). It is based on the LRP\_for\_LSTM package by Leila Arras released for the paper [Explaining Recurrent Neural Network Predictions in Sentiment Analysis by L. Arras, G. Montavon, K.-R. Müller and W. Samek, 2017](http://aclweb.org/anthology/W/W17/W17-5221.pdf). 4 | 5 | ## Dependencies 6 | 7 | Python>=3.5 + Numpy 8 | 9 | ## Citation 10 | 11 | @INPROCEEDINGS{arras2017, 12 | title = {Explaining Recurrent Neural Network Predictions in Sentiment Analysis}, 13 | author = {Leila Arras and Gr{\'e}goire Montavon and Klaus-Robert M{\"u}ller and Wojciech Samek}, 14 | booktitle = {Proceedings of the EMNLP 2017 Workshop on Computational Approaches to Subjectivity, Sentiment and Social Media Analysis}, 15 | year = {2017}, 16 | pages = {159-168}, 17 | publisher = {Association for Computational Linguistics}, 18 | url = {http://aclweb.org/anthology/W/W17/W17-5221.pdf} 19 | } 20 | 21 | @INPROCEEDINGS{poerner2018, 22 | title={Evaluating Neural Network Explanation Methods using Hybrid Documents and Morphological Prediction}, 23 | author={Poerner, Nina and Sch{\"u}tze, Hinrich and Roth, Benjamin}, 24 | booktitle={Proceedings of the 56th Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers)}, 25 | volume={1}, 26 | year={2018} 27 | } 28 | 29 | -------------------------------------------------------------------------------- /HybridDocuments/ThirdParty/LRP_and_DeepLIFT/code/LRP_linear_layer.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @author: Leila Arras 3 | @maintainer: Leila Arras 4 | @date: 21.06.2017 5 | @version: 1.0 6 | @copyright: Copyright (c) 2017, Leila Arras, Gregoire Montavon, Klaus-Robert Mueller, Wojciech Samek 7 | @license: BSD-2-Clause 8 | ''' 9 | 10 | import numpy as np 11 | from numpy import newaxis as na 12 | 13 | 14 | def lrp_linear(hin, w, b, hout, Rout, bias_nb_units, eps, bias_factor, debug=False): 15 | """ 16 | LRP for a linear layer with input dim D and output dim M. 17 | Args: 18 | - hin: forward pass input, of shape (D,) 19 | - w: connection weights, of shape (D, M) 20 | - b: biases, of shape (M,) 21 | - hout: forward pass output, of shape (M,) (unequal to np.dot(w.T,hin)+b if more than one incoming layer!) 22 | - Rout: relevance at layer output, of shape (M,) 23 | - bias_nb_units: number of lower-layer units onto which the bias/stabilizer contribution is redistributed 24 | - eps: stabilizer (small positive number) 25 | - bias_factor: for global relevance conservation set to 1.0, otherwise 0.0 to ignore bias redistribution 26 | Returns: 27 | - Rin: relevance at layer input, of shape (D,) 28 | """ 29 | sign_out = np.where(hout[na,:]>=0, 1., -1.) # shape (1, M) 30 | 31 | #numer = (w * hin[:,na]) + ( (bias_factor*b[na,:]*1. + eps*sign_out*1.) * 1./bias_nb_units ) # shape (D, M) 32 | numer = (w * hin[:,na]) + ( (bias_factor*b[na,:]*1.) * 1./bias_nb_units ) # shape (D, M) 33 | 34 | denom = hout[na,:] + (eps*sign_out*1.) # shape (1, M) 35 | 36 | message = (numer/denom) * Rout[na,:] # shape (D, M) 37 | 38 | Rin = message.sum(axis=1) # shape (D,) 39 | 40 | # Note: local layer relevance conservation if bias_factor==1.0 and bias_nb_units==D 41 | # global network relevance conservation if bias_factor==1.0 (can be used for sanity check) 42 | if debug: 43 | print("local diff: ", Rout.sum() - Rin.sum()) 44 | 45 | return Rin 46 | -------------------------------------------------------------------------------- /HybridDocuments/SRC/util.py: -------------------------------------------------------------------------------- 1 | from config import * 2 | import os 3 | import sys 4 | import re 5 | # add third party repos to pythonpath 6 | 7 | L2SCORES = ["grad_raw_l2", "grad_prob_l2"] 8 | L2SCORES += [s + "_integrated" for s in L2SCORES] 9 | DOTSCORES = [re.sub("l2", "dot", s) for s in L2SCORES] 10 | OMITSCORES = ["omit-1", "omit-3", "omit-7"] 11 | LIMESCORES = ["limsse_" + s for s in ("class", "raw", "prob")] 12 | 13 | OCCSCORES = [re.sub("omit", "occ", s) for s in OMITSCORES] 14 | 15 | SCORES = L2SCORES + DOTSCORES + OMITSCORES + OCCSCORES + ["decomp", "lrp", "deeplift"] + LIMESCORES 16 | 17 | CORPORA = ("newsgroup", "yelp") 18 | 19 | ARCHITECTURES = ("GRU", "LSTM", "CNN", "QLSTM", "QGRU") 20 | 21 | DIRECTORIES = {name: os.path.join(WORKDIR, name) for name in ("Models", "CSV", "Inputs", "Scores", "Predictions")} 22 | 23 | JSONS = [TRAINJSON, DEVJSON, TESTJSON] 24 | 25 | def prep_directories(): 26 | """ 27 | Create all necessary directories 28 | """ 29 | for directory in DIRECTORIES.values(): 30 | if not os.path.exists(directory): 31 | os.mkdir(directory) 32 | 33 | for corpus in CORPORA: 34 | tmp = os.path.join(DIRECTORIES["Inputs"], corpus) 35 | if not os.path.exists(tmp): 36 | os.mkdir(tmp) 37 | 38 | 39 | def command_line_overlap(primary_list): 40 | """ 41 | Returns the list of all candidates from primary_list that are in the argv; 42 | important for scoring/training/evaluating more than one corpus/architecture/relevance score 43 | """ 44 | if "all" in sys.argv and "gamma" in primary_list: 45 | return primary_list 46 | return [x for x in primary_list if x in sys.argv] 47 | 48 | 49 | 50 | 51 | ## Utity functions that return paths of interest ## 52 | ################################################### 53 | 54 | def make_modelpath(architecture, corpus): 55 | return os.path.join(DIRECTORIES["Models"], "_".join((architecture, corpus)) + ".hdf5") 56 | 57 | def make_scorepath(dataset, architecture, corpus, score, pred_only): 58 | suffix = "" 59 | if pred_only: suffix = "_k" 60 | return os.path.join(DIRECTORIES["Scores"], "_".join((dataset, architecture, corpus, score)) + ".score" + suffix) 61 | 62 | def make_csvpath(architecture, corpus): 63 | return os.path.join(DIRECTORIES["CSV"], "_".join((architecture, corpus)) + ".csv") 64 | 65 | def make_predpath(dataset, architecture, corpus): 66 | return os.path.join(DIRECTORIES["Predictions"], "_".join((dataset, architecture, corpus)) + ".pred") 67 | 68 | def make_storagedir(corpus): 69 | return os.path.join(DIRECTORIES["Inputs"], corpus) 70 | 71 | 72 | prep_directories() 73 | -------------------------------------------------------------------------------- /HybridDocuments/README.md: -------------------------------------------------------------------------------- 1 | # Code for hybrid document experiment in [1]. 2 | 3 | ## CORPORA 4 | 5 | The 20 newsgroups corpus is available from sklearn and will be downloaded when running 6 | 7 | ```main.py prepare newsgroup``` 8 | 9 | The manual evaluation benchmark [2] can be downloaded by running 10 | 11 | ```./prep_manual``` 12 | 13 | The 10th yelp dataset challenge is no longer available on-line, and we do not have the license to publish it. 14 | However, the majority of relevant reviews (203756 out of 206338) are present in the [11'th 15 | dataset challenge](https://www.yelp.com/dataset/download), and hopefully also in future editions. Note that 16 | we only used reviews from the Pennsylvania area (state = 'PA'). The file [reviews.json](reviews.json) contains the IDs of all reviews that we used, along with our train/dev/test split. 17 | 18 | ## MODELS 19 | 20 | Prior to training, you will need to download pre-trained embeddings [here](http://nlp.stanford.edu/data/glove.840B.300d.zip). 21 | Then set this variable in the [config file](SRC/config.py): 22 | 23 | ```GLOVEPATH = # path to embedding txt file``` 24 | 25 | Then run 26 | 27 | ``` 28 | cd SRC 29 | main.py prepare corpus 30 | main.py train corpus architecture 31 | ``` 32 | 33 | where 34 | 35 | ``` 36 | # corpus is one of yelp|newsgroup 37 | # architecture is one of CNN|GRU|LSTM|QGRU|QLSTM 38 | ``` 39 | 40 | For reproducibility, you may want to skip the second step and instead download the models from our original experiment: 41 | 42 | ```./prep_models.sh``` 43 | 44 | ## RUNNING THE EXPERIMENT 45 | 46 | ``` 47 | main.py eval architecture corpus # evaluate primary model performance on test set 48 | main.py score architecture corpus method # pre-calculate relevance scores 49 | main.py pointinggame architecture corpus method # evaluate relevance maximum 50 | main.py manual architecture> method # evaluate relevance maximum on manual benchmark 51 | ``` 52 | 53 | where 54 | 55 | ``` 56 | # method is one of limsse_raw|limsse_class|omit-1|occ-1|grad_raw_dot| ... 57 | ``` 58 | 59 | See [SRC/util.py](SRC/util.py) for full list of explanation methods. 60 | 61 | ## KNOWN ISSUES 62 | 63 | There used to be a groundtruth-prediction mismatch on trimmed documents (i.e., documents with a length > 1000 words). 64 | This means that results reported in [1] underestimate pointing game accuracy on very long documents. This bug has 65 | been fixed in the present codebase. 66 | 67 | ## REFERENCES 68 | 69 | [1] Poerner, N., Roth, B., Schütze, H. (2018). Evaluating neural network explanation methods using hybrid 70 | documents and morphosyntactic agreement. ACL. 71 | 72 | [2] Mohseni, S., Ragan, E.D. (2018) A Human-Grounded Evaluation Benchmark for Local Explanations of Machine Learning. 73 | arXiv preprint arXiv:1801.05075 74 | -------------------------------------------------------------------------------- /HybridDocuments/SRC/manual_eval.py: -------------------------------------------------------------------------------- 1 | from util import * 2 | import numpy as np 3 | np.random.seed(12345) 4 | import _pickle 5 | import json 6 | import sys 7 | from util import * 8 | from corpora import * 9 | from task_methods import * 10 | from explanation_methods import * 11 | from nltk.tokenize import word_tokenize 12 | 13 | from progressbar import ProgressBar 14 | 15 | class ManualEval: 16 | def __init__(self, architecture): 17 | self.architecture = architecture 18 | 19 | def build(self): 20 | self.corpus = get_corpus_object("newsgroup") 21 | self.corpus.load_select_if_necessary(["classdict", "worddict"]) 22 | self.load_gt() 23 | self.model = TaskMethod(self.architecture, self.corpus) 24 | self.model.load(make_modelpath(self.architecture, "newsgroup")) 25 | 26 | def build_score_model(self, score): 27 | if score != "random": 28 | self.score_model = self.model.make_explanation_method(score) 29 | self.score_model.build() 30 | 31 | def eval(self, score): 32 | self.build_score_model(score) 33 | hits = 0 34 | total = 0 35 | 36 | if score == "random": 37 | np.random.seed(12345) 38 | 39 | for cl in self.CLASSES: 40 | cl = self.corpus.classdict[cl] 41 | if "grad" in score: 42 | self.score_model.build_n(cl) 43 | 44 | bar = ProgressBar() 45 | for x,g,raw in bar(list(zip(self.X[cl], self.GT[cl],self.raw[cl]))): 46 | if self.model.model.predict(np.expand_dims(x,0))[0].argmax() == cl: 47 | peak = self.point(x, score, cl,g,raw,total) 48 | hits += int(peak in g) 49 | total += 1 50 | 51 | 52 | print(self.architecture, score) 53 | print(hits, "/", total, "=", hits/total) 54 | print(flush=True) 55 | 56 | def point(self, x, score, cl, g, raw, i): 57 | if score == "random": 58 | return np.random.randint(len(x)) 59 | else: 60 | s = self.score_model.score(np.expand_dims(x, 0), pred = np.array([cl]))[0].squeeze() 61 | assert(s.shape == x.shape) 62 | return s.argmax() 63 | 64 | 65 | def load_gt(self): 66 | ORIGPATH = os.path.join(MANUAL_BENCHMARK, "Text/org_documents/20news-bydate/20news-bydate-test/") 67 | EVALPATH = os.path.join(MANUAL_BENCHMARK, "Text/user_evaluation/") 68 | 69 | self.CLASSES = os.listdir(ORIGPATH) 70 | 71 | self.X = {} 72 | self.GT = {} 73 | self.raw = {} 74 | 75 | lengths = [] 76 | for cl in self.CLASSES: 77 | clname = self.corpus.classdict[cl] 78 | self.X[clname] = [] 79 | self.GT[clname] = [] 80 | self.raw[clname] = [] 81 | for doc in os.listdir(os.path.join(ORIGPATH, cl)): 82 | with open(os.path.join(ORIGPATH, cl, doc)) as handle: 83 | tmp = handle.read().strip() 84 | if len(tmp): 85 | tokenized = word_tokenize(tmp)[:1000] 86 | tokenized_lk = [w.lower() for w in word_tokenize(tmp)] 87 | jpath = os.path.join(EVALPATH, cl, doc + ".json") 88 | if os.path.exists(jpath): 89 | self.X[clname].append(np.array([self.corpus.worddict.get(w, 1) for w in tokenized])) 90 | self.raw[clname].append(tokenized) 91 | with open(os.path.join(EVALPATH, cl, doc + ".json")) as jhandle: 92 | jobj = json.load(jhandle) 93 | words = [w[0] for w in jobj["words"]] 94 | lengths.append(len(word_tokenize(tmp))) 95 | for word in words: 96 | if not word in tokenized_lk: 97 | a = [w for w in tokenized_lk if word in w] 98 | indices = [i for i, x in enumerate(tokenized_lk) if any([x.startswith(_) or x.endswith(_) for _ in words])] 99 | self.GT[clname].append(np.array(indices)) 100 | 101 | -------------------------------------------------------------------------------- /HybridDocuments/SRC/main.py: -------------------------------------------------------------------------------- 1 | from util import * 2 | from corpora import * 3 | from task_methods import * 4 | from explanation_methods import * 5 | from manual_eval import * 6 | from pointing_game import * 7 | import _pickle 8 | 9 | def evaluate(architecture, corpus): 10 | """ 11 | Print test and dev set accuracy and loss of on 12 | """ 13 | corpus_object = get_corpus_object(corpus) 14 | model = TaskMethod(architecture, corpus_object) 15 | model.load(make_modelpath(architecture, corpus)) 16 | model.evaluate() 17 | 18 | def train(architecture, corpus): 19 | """ 20 | Train on 21 | """ 22 | corpus_object = get_corpus_object(corpus) 23 | 24 | model = TaskMethod(architecture, corpus_object) 25 | model.build() 26 | 27 | 28 | model.train(make_modelpath(architecture, corpus), make_csvpath(architecture, corpus)) 29 | 30 | 31 | def predict(architecture, corpus): 32 | """ 33 | Let predict classes for all test set and hybrid documents in calculate relevance scores for for all test set and hybrid documents in 52 | 53 | pred_only: if true, calculate only relevance scores for the classes predicted by 54 | else, calculate relevance scores for all possible target classes 55 | """ 56 | 57 | corpus_object = get_corpus_object(corpus) 58 | 59 | model = TaskMethod(architecture, corpus_object) 60 | modelpath = make_modelpath(architecture, corpus) 61 | 62 | model.load(make_modelpath(architecture, corpus)) 63 | 64 | score_model = model.make_explanation_method(score) 65 | score_model.build() 66 | 67 | for dataset in ("hybrid",):# "test"): 68 | tmp = score_model.score_dataset(dataset, pred_only = pred_only) 69 | 70 | with open(make_scorepath(dataset, architecture, corpus, score, pred_only), "wb") as handle: 71 | _pickle.dump(tmp, handle) 72 | 73 | 74 | def prepare(corpus): 75 | """ 76 | Prepare all input files for 77 | """ 78 | corpus_object = get_corpus_object(corpus) 79 | corpus_object.prepare() 80 | 81 | def datatest(corpus): 82 | """ 83 | Do some sanity checks on input files for 84 | """ 85 | corpus_object = get_corpus_object(corpus) 86 | corpus_object.sanity_check() 87 | 88 | 89 | 90 | if __name__ == "__main__": 91 | 92 | if "prepare" in sys.argv: 93 | for corpus in command_line_overlap(CORPORA): 94 | prepare(corpus) 95 | 96 | if "datatest" in sys.argv: 97 | for corpus in command_line_overlap(CORPORA): 98 | datatest(corpus) 99 | 100 | if "train" in sys.argv: 101 | for corpus in command_line_overlap(CORPORA): 102 | for architecture in command_line_overlap(ARCHITECTURES): 103 | train(architecture, corpus) 104 | 105 | if "eval" in sys.argv: 106 | for corpus in command_line_overlap(CORPORA): 107 | for architecture in command_line_overlap(ARCHITECTURES): 108 | evaluate(architecture, corpus) 109 | 110 | if "predict" in sys.argv: 111 | for corpus in command_line_overlap(CORPORA): 112 | for architecture in command_line_overlap(ARCHITECTURES): 113 | predict(architecture, corpus) 114 | 115 | if "score" in sys.argv: 116 | for corpus in command_line_overlap(CORPORA): 117 | for architecture in command_line_overlap(ARCHITECTURES): 118 | for score in command_line_overlap(SCORES): 119 | calculate_score(architecture, corpus, score, pred_only = True) 120 | 121 | if "pointinggame" in sys.argv: 122 | for corpus in command_line_overlap(CORPORA): 123 | for architecture in command_line_overlap(ARCHITECTURES): 124 | for score in command_line_overlap(SCORES) + ["random"]: 125 | game = get_pointing_game(score)(architecture, corpus, score, pred_only = True) 126 | game.prepare() 127 | game.play() 128 | if "manual" in sys.argv: 129 | for architecture in command_line_overlap(ARCHITECTURES): 130 | game = ManualEval(architecture) 131 | game.build() 132 | for score in command_line_overlap(SCORES) + ["random"]: 133 | if architecture == "CNN" and score == "decomp": continue 134 | game.eval(score) 135 | -------------------------------------------------------------------------------- /HybridDocuments/SRC/pointing_game.py: -------------------------------------------------------------------------------- 1 | from util import * 2 | import numpy as np 3 | np.random.seed(12345) 4 | import _pickle 5 | 6 | def get_pointing_game(score): 7 | """ 8 | Returns the correct pointing game contestant for a given score 9 | """ 10 | if score == "random": return NaiveRandomPointingGame 11 | if score == "biased": return BiasedRandomPointingGame 12 | return ScorePointingGame 13 | 14 | class PointingGame: 15 | """ 16 | Pointing game contestant. 17 | """ 18 | def __init__(self, architecture, corpus, score, pred_only): 19 | self.architecture = architecture 20 | self.corpus = corpus 21 | self.score = score 22 | # if pred_only, expect relevance scores of the shape (num_words, 1); else (num_words, num_classes) 23 | self.pred_only = pred_only 24 | self.points = {} 25 | 26 | def load(self, path): 27 | with open(path, "rb") as handle: 28 | tmp = _pickle.load(handle) 29 | return tmp 30 | 31 | def load_groundtruths(self): 32 | groundtruthpath = os.path.join(make_storagedir(self.corpus), "GT") 33 | self.groundtruths = self.load(groundtruthpath) 34 | 35 | def load_predictions(self): 36 | predpath = make_predpath("hybrid", self.architecture, self.corpus) 37 | self.predictions = self.load(predpath) 38 | 39 | def valid(self, i): 40 | """ 41 | Returns true if the task method predicted a class that is the gold label of at least one word 42 | """ 43 | return self.predictions[i] in self.groundtruths[i] 44 | 45 | def play(self): 46 | print("Pointing game") 47 | print("Architecture", self.architecture), 48 | print("Corpus", self.corpus) 49 | print("Score", self.score) 50 | 51 | hits, discarded = 0, 0 52 | total = len(self.predictions) 53 | 54 | for i in range(total): 55 | peak = self.point(i) 56 | 57 | if self.valid(i): 58 | hits += int(self.groundtruths[i][peak] == self.predictions[i]) 59 | else: 60 | discarded += 1 61 | 62 | print("Discarded", discarded, "/", total, "=", discarded / total) 63 | print("Pointing game accuracy", hits, "/ (", total, "-", discarded, ") =", hits / (total-discarded)) 64 | print() 65 | 66 | return hits / (total-discarded) 67 | 68 | class ScorePointingGame(PointingGame): 69 | """ 70 | Pointing game contestant that places relevance peak according to some relevance scoring method. 71 | """ 72 | 73 | def point(self, i): 74 | if self.pred_only: 75 | return np.squeeze(self.scores[i]).argmax(0) 76 | 77 | prediction = self.predictions[i] 78 | return self.scores[i].argmax(0)[prediction] 79 | 80 | def load_scores(self): 81 | scorepath = make_scorepath("hybrid", self.architecture, self.corpus, self.score, self.pred_only) 82 | self.scores = self.load(scorepath) 83 | 84 | def prepare(self): 85 | self.load_groundtruths() 86 | self.load_predictions() 87 | self.load_scores() 88 | assert len(self.groundtruths) == len(self.predictions) == len(self.scores) 89 | 90 | 91 | class RandomPointingGame(PointingGame): 92 | """ 93 | Parent class for random pointing game contestant. 94 | """ 95 | def prepare(self): 96 | self.randstate = np.random.RandomState(123) 97 | self.load_groundtruths() 98 | self.load_predictions() 99 | assert len(self.groundtruths) == len(self.predictions) 100 | 101 | 102 | class NaiveRandomPointingGame(RandomPointingGame): 103 | """ 104 | Pointing game contestant that randomly picks a relevance peak in the document. 105 | """ 106 | def point(self, i): 107 | length = self.groundtruths[i].shape[0] 108 | return self.randstate.randint(0, length) 109 | 110 | class BiasedRandomPointingGame(RandomPointingGame): 111 | """ 112 | Random pointing game contestant that has information about probable locations of truly relevant words. 113 | Doesn't really outperform the naive baseline. 114 | """ 115 | def prepare(self): 116 | super(BiasedRandomPointingGame, self).prepare() 117 | positions_dict = {} 118 | total = 0 119 | 120 | for prediction, groundtruth in zip(self.predictions, self.groundtruths): 121 | for i in range(groundtruth.shape[0]): 122 | if groundtruth[i] == prediction: 123 | position = float(i) / groundtruth.shape[0] 124 | positions_dict[position] = positions_dict.get(position, 0) + 1 125 | total += 1 126 | 127 | self.positions = list(positions_dict.keys()) 128 | self.probabilities = [positions_dict[position] / float(total) for position in self.positions] 129 | 130 | def point(self, i): 131 | length = self.groundtruths[i].shape[0] 132 | r = self.randstate.choice(self.positions, p = self.probabilities) 133 | return int(r * length) 134 | 135 | 136 | -------------------------------------------------------------------------------- /HybridDocuments/SRC/task_methods.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module responsible for task methods (i.e., the neural networks that will be explained). 3 | """ 4 | 5 | import numpy as np 6 | np.random.seed(123) 7 | import keras 8 | import _pickle 9 | 10 | from keras.layers import * 11 | from keras.models import * 12 | from keras.callbacks import * 13 | from keras.metrics import * 14 | from corpora import * 15 | from util import * 16 | 17 | from explanation_methods import * 18 | 19 | class TaskMethod: 20 | BATCH_SIZE = 8 21 | 22 | def __init__(self, architecture, corpus): 23 | self.architecture = architecture 24 | self.corpus = corpus 25 | 26 | if not self.architecture in ("LSTM", "GRU", "CNN", "QLSTM", "QGRU"): 27 | raise Exception("Unknown architecture", self.architecture) 28 | 29 | def predict_dataset(self, dataset): 30 | """ 31 | Return a list of class predictions for dataset 32 | """ 33 | 34 | generator = self.corpus.get_generator(dataset, self.BATCH_SIZE, shuffle = False) 35 | spe = self.corpus.get_steps_per_epoch(dataset, self.BATCH_SIZE) 36 | 37 | return self.predict_from_generator(generator, spe) 38 | 39 | def predict_from_generator(self, generator, spe): 40 | bar = ProgressBar() 41 | 42 | predictions = [] 43 | 44 | for _ in bar(list(range(spe))): 45 | x, y = next(generator) 46 | tmp = self.model.predict_on_batch(x).argmax(-1) # (batch_size,) 47 | predictions.extend(tmp) # (n_samples,) 48 | 49 | return predictions 50 | 51 | def build(self): 52 | """ 53 | Build and compile the keras model 54 | """ 55 | self.input = Input((None,)) 56 | self.corpus.load_select_if_necessary(("embeddings", "worddict", "classdict")) 57 | 58 | self.embedding = Embedding(\ 59 | input_dim = self.corpus.FREQCAP, 60 | output_dim = self.corpus.EMB_SIZE, 61 | mask_zero = self.architecture != "CNN", # Conv1D does not support masking 62 | name = "embedding", 63 | weights = self.corpus.embeddings) 64 | 65 | self.dropout1 = Dropout(self.corpus.DROPOUT) 66 | 67 | if self.architecture == "CNN": 68 | self.main = Sequential(name = "main") 69 | I = {"input_shape": (None, self.corpus.EMB_SIZE)} 70 | self.main.add(\ 71 | Conv1D(\ 72 | filters = self.corpus.HIDDEN_SIZE, 73 | kernel_size = 5, 74 | activation = "relu", 75 | padding = "same", 76 | input_shape = (None, self.corpus.EMB_SIZE))) 77 | self.main.add(GlobalMaxPooling1D()) 78 | 79 | else: 80 | mainclass = {"LSTM": LSTM, "GRU": GRU, "QLSTM": QLSTM, "QGRU": QGRU}[self.architecture] # like eval() 81 | 82 | config = {} 83 | if self.architecture in ("GRU", "LSTM"): 84 | config["recurrent_dropout"] = self.corpus.DROPOUT 85 | else: 86 | config["kernel_size"] = 5 87 | config["padding"] = "causal" 88 | self.main = Bidirectional(\ 89 | mainclass(\ 90 | units = self.corpus.HIDDEN_SIZE // 2, 91 | **config), 92 | merge_mode = "concat", name = "main") 93 | 94 | activation = "softmax" 95 | loss = "sparse_categorical_crossentropy" 96 | outputs = len(self.corpus.classdict) 97 | 98 | self.dropout2 = Dropout(self.corpus.DROPOUT) 99 | self.dense = Dense(units = outputs, activation = "linear") 100 | 101 | self.softmax = Activation(activation) 102 | self.output = self.softmax(self.dense(self.dropout2(self.main(self.dropout1(self.embedding(self.input)))))) 103 | 104 | self.model = Model([self.input], [self.output]) 105 | self.model.compile(\ 106 | optimizer = "adam", 107 | loss = loss, 108 | metrics = ["accuracy"]) 109 | 110 | def train(self, modelpath, csvpath): 111 | """ 112 | Train the task method 113 | 114 | modelpath: path where model should be stored 115 | csvpath: path where logging information can be stored 116 | """ 117 | generators = {dset: self.corpus.get_generator(dset, 8, shuffle = dset == "train") \ 118 | for dset in self.corpus.DATASETS} 119 | spe = {dset: self.corpus.get_steps_per_epoch(dset, 8) \ 120 | for dset in self.corpus.DATASETS} 121 | self.model.summary() 122 | print("num weights:", len(self.model.get_weights())) 123 | 124 | # Early Stopping: stops training after a certain number of epochs without improvement of dev set accuracy 125 | # CSV logger: write some stats to CSV file 126 | # Model Checkpoint: Store the model if it is the best so far 127 | # Reduce LR On Pleateau: halve learning rate if dev set accuracy does not improve 128 | callbacks = [\ 129 | EarlyStopping(patience = self.corpus.PATIENCE, monitor = "val_acc", verbose = 1), 130 | CSVLogger(csvpath), 131 | ModelCheckpoint(modelpath, save_best_only = True, monitor = "val_acc", verbose = 1), 132 | ReduceLROnPlateau(monitor='val_acc', factor=0.5, patience=2, min_lr=0, verbose = 1)] 133 | 134 | # train for 1000 epochs (read: unlimited epochs) until Early Stopping kicks in 135 | self.model.fit_generator(\ 136 | generators["train"], 137 | epochs = 1000, 138 | steps_per_epoch = spe["train"], 139 | validation_data = generators["dev"], 140 | validation_steps = spe["dev"], 141 | callbacks = callbacks, 142 | verbose = 1) 143 | 144 | def load(self, modelpath): 145 | self.model = load_model(modelpath) 146 | 147 | def make_explanation_method(self, score): 148 | """ 149 | Returns the desired explanation method, which will be applied to 'self' 150 | """ 151 | 152 | if score == "decomp": 153 | return ScoreModelGamma(self) 154 | if score.startswith("omit"): 155 | return ScoreModelErasure(self, n_gram=int(score.split("-")[-1]), mode = "omission") 156 | if score.startswith("occ"): 157 | return ScoreModelErasure(self, n_gram=int(score.split("-")[-1]), mode = "occlusion") 158 | if score == "grad_raw_l2": 159 | return ScoreModelGradientRaw(self, "l2") 160 | if score == "grad_raw_dot": 161 | return ScoreModelGradientRaw(self, "dot") 162 | if score == "grad_prob_l2": 163 | return ScoreModelGradientProb(self, "l2") 164 | if score == "grad_prob_dot": 165 | return ScoreModelGradientProb(self, "dot") 166 | if score == "limsse_class": 167 | return ScoreLimeClass(self) 168 | if score == "limsse_prob": 169 | return ScoreLimeProb(self) 170 | if score == "limsse_raw": 171 | return ScoreLimeRaw(self) 172 | if score == "lrp": 173 | return ScoreLRP(self) 174 | if score == "deeplift": 175 | return ScoreDL(self) 176 | if score == "grad_raw_dot_integrated": 177 | return ScoreModelGradientRaw(self, "dot", integrated=True) 178 | if score == "grad_prob_dot_integrated": 179 | return ScoreModelGradientProb(self, "dot", integrated=True) 180 | if score == "grad_raw_l2_integrated": 181 | return ScoreModelGradientRaw(self, "l2", integrated=True) 182 | if score == "grad_prob_l2_integrated": 183 | return ScoreModelGradientProb(self, "l2", integrated=True) 184 | 185 | raise Exception("Unknown score", score) 186 | 187 | 188 | def evaluate(self): 189 | print("Evaluating on test data") 190 | print("Architecture:", self.architecture) 191 | 192 | generator = self.corpus.get_generator("test", self.BATCH_SIZE, shuffle = False) 193 | spe = self.corpus.get_steps_per_epoch("test", self.BATCH_SIZE) 194 | results = self.model.evaluate_generator(generator, spe) 195 | 196 | for name, value in zip(self.model.metrics_names, results): 197 | print(name, value) 198 | -------------------------------------------------------------------------------- /HybridDocuments/SRC/corpora.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module responsible for preparing, storing and handling data. 3 | """ 4 | 5 | import numpy as np 6 | import os 7 | import _pickle 8 | import json 9 | 10 | from nltk.tokenize import word_tokenize, sent_tokenize 11 | from sklearn.datasets import fetch_20newsgroups 12 | 13 | from util import * 14 | 15 | def get_corpus_object(corpus_name): 16 | """ 17 | Return the correct corpus object based on its name 18 | """ 19 | if corpus_name.startswith("yelp"): 20 | return CorpusYelp(make_storagedir(corpus_name), GLOVEPATH, *JSONS) 21 | 22 | elif corpus_name == "newsgroup": 23 | return CorpusNewsgroup(make_storagedir(corpus_name), GLOVEPATH) 24 | 25 | raise Exception("Unknown corpus", corpus_name) 26 | 27 | class Corpus: 28 | """ 29 | Corpus parent class 30 | """ 31 | 32 | DATASETS = ("test", "train", "dev", "hybrid") 33 | FREQCAP = 50000 # words with a frequency rank above this number are mapped to __oov__ 34 | MAXLENGTH = 1000 # number of words above which documents are trimmed 35 | FILENAMES = ("embeddings", "classdict", "worddict", "X", "Y", "GT", "tokenized_documents", "raw_documents") 36 | EMB_SIZE = 300 # embedding size (same for both corpora) 37 | HIDDEN_SIZE = 150 # hidden size (same for both corpora) 38 | DROPOUT = 0.5 # dropout (same for both corpora) 39 | HYBRID_LENGTH = 10 # length of hybrid documents, in sentences 40 | 41 | def __init__(self, storagedir, embeddingpath): 42 | self.storagedir = storagedir 43 | self.embeddingpath = embeddingpath 44 | self.pred = {} 45 | 46 | def prepare(self): 47 | """ 48 | Prepare the corpus by storing all necessary files in the storage directory. 49 | """ 50 | if len(os.listdir(self.storagedir)): 51 | raise Exception("There are already files in", self.storagedir + ".", "Delete manually!") 52 | 53 | self.worddict = {"__pad__": 0, "__oov__": 1} 54 | self.classdict = {} 55 | self.raw_documents, self.tokenized_documents = {}, {} 56 | self.X, self.Y = {}, {} 57 | 58 | for dataset in self.DATASETS_TMP: 59 | self.get_raw_data(dataset) 60 | self.delete_empty_documents(dataset) 61 | self.tokenize_documents(dataset) 62 | 63 | self.make_classdict() 64 | self.make_worddict() 65 | self.make_embeddings() 66 | self.reverse_dicts() 67 | 68 | for dataset in self.DATASETS_TMP: 69 | self.make_X(dataset) 70 | self.shuffle_dataset(dataset) 71 | 72 | if not "dev" in self.X: 73 | self.split_dev() 74 | self.make_hybrid() 75 | self.store() 76 | 77 | def make_embeddings(self): 78 | """ 79 | Preset embedding weights with GloVe pre-trained embeddings (where possible). 80 | """ 81 | print("Presetting embedding weights") 82 | 83 | np.random.seed(0) 84 | weights = np.random.uniform(low = -0.05, high = 0.05, size = (self.FREQCAP, self.EMB_SIZE)) 85 | 86 | counter = 0 87 | 88 | words = [] 89 | weights_tmp = [] 90 | 91 | with open(self.embeddingpath) as handle: 92 | for i, line in enumerate(handle): 93 | tmp = line.strip() 94 | if len(tmp) > 0: 95 | split = tmp.split(" ") 96 | if split[0] in self.worddict and len(split[1:]) == 300: 97 | words.append(split[0]) 98 | weights_tmp.append([float(a) for a in split[1:]]) 99 | 100 | weights_tmp = np.array(weights_tmp) 101 | 102 | for word, column in zip(words, weights_tmp): 103 | if self.worddict[word] < self.FREQCAP: 104 | counter += 1 105 | weights[self.worddict[word],:] = column 106 | 107 | print("Set", counter, "of", weights.shape[0], "columns") 108 | 109 | if self.EMB_SIZE < weights.shape[-1]: 110 | print("Reducing dimensionality to", self.EMB_SIZE) 111 | pca = PCA(self.EMB_SIZE) 112 | weights = pca.fit_transform(weights) 113 | 114 | self.embeddings = [weights] 115 | 116 | 117 | def reverse_dicts(self): 118 | """ 119 | Reverse class and word dicts; important for printing + sanity checks 120 | """ 121 | self.rev_worddict = {self.worddict[word]: word for word in self.worddict} 122 | self.rev_classdict = {self.classdict[cl]: cl for cl in self.classdict} 123 | 124 | def store(self): 125 | """ 126 | Store corpus to its storage directory 127 | """ 128 | print("Storing to", self.storagedir) 129 | 130 | for filename in self.FILENAMES: 131 | with open(os.path.join(self.storagedir, filename), "wb") as handle: 132 | _pickle.dump(getattr(self, filename), handle) 133 | 134 | def load(self, which): 135 | """ 136 | Load a corpus component from its storage directory 137 | """ 138 | path = os.path.join(self.storagedir, which) 139 | print("Loading from", path) 140 | with open(path, "rb") as handle: 141 | setattr(self, which, _pickle.load(handle)) 142 | 143 | def load_full(self): 144 | """ 145 | Load the entire corpus from its storage directory 146 | """ 147 | for filename in self.FILENAMES: 148 | self.load(filename) 149 | self.reverse_dicts() 150 | 151 | def load_select(self, selected): 152 | """ 153 | Load selected components (from list) from corpus storage directory 154 | """ 155 | for filename in selected: 156 | self.load(filename) 157 | 158 | if "worddict" in selected and "classdict" in selected: 159 | self.reverse_dicts() 160 | 161 | def get_steps_per_epoch(self, dataset, batchsize): 162 | """ 163 | Returns the number of steps that are necessary to generate all samples exactly once. 164 | 165 | dataset: one of 'train', 'dev', 'test', 'hybrid' 166 | batchsize: batch size that the generator will be working on 167 | """ 168 | self.load_if_necessary("X") 169 | 170 | num_samples = len(self.X[dataset]) 171 | if num_samples % batchsize == 0: 172 | return num_samples // batchsize 173 | 174 | return num_samples // batchsize + 1 # account for the smaller last batch if necessary 175 | 176 | def trim_and_pad_batch(self, batch): 177 | """ 178 | Trim all samples in a batch to MAXLENGTH and pad them to identical lengths. 179 | """ 180 | maxlength = min(self.MAXLENGTH, max([len(x) for x in batch])) 181 | 182 | batch = [x[:maxlength] for x in batch] 183 | batch = [np.concatenate([x, np.zeros(maxlength - x.shape[0])]) for x in batch] 184 | 185 | return batch 186 | 187 | def load_if_necessary(self, which): 188 | """ 189 | Load corpus component only if it has not yet been loaded 190 | """ 191 | if not hasattr(self, which): 192 | self.load(which) 193 | 194 | def load_select_if_necessary(self, selected): 195 | """ 196 | Load selected corpus components only if they have not yet been loaded 197 | """ 198 | for which in selected: 199 | self.load_if_necessary(which) 200 | 201 | if "worddict" in selected and "classdict" in selected: 202 | self.reverse_dicts() 203 | 204 | 205 | def get_generator(self, dataset, batchsize, shuffle = False): 206 | """ 207 | Returns a generator that will generate (X,Y) pairs for the given dataset. 208 | 209 | dataset: one of 'train', 'dev', 'test', 'hybrid' 210 | batchsize: batch size that the generator will be working on 211 | shuffle: if true, the dataset is shuffled at the beginning of every epoch 212 | """ 213 | self.load_select_if_necessary(("X", "Y")) 214 | random_state = np.random.RandomState(0) 215 | 216 | while True: 217 | indices = list(range(len(self.X[dataset]))) 218 | if shuffle: 219 | random_state.shuffle(indices) 220 | 221 | X = [self.X[dataset][idx] for idx in indices] 222 | Y = [self.Y[dataset][idx] for idx in indices] 223 | 224 | for idx in range(0, len(X), batchsize): 225 | batch_X = X[idx:min(idx + batchsize, len(X))] 226 | batch_Y = Y[idx:min(idx + batchsize, len(X))] 227 | batch_X = np.array(self.trim_and_pad_batch(batch_X)) 228 | 229 | yield(batch_X, np.array(batch_Y)) 230 | 231 | def sanity_check(self): 232 | """ 233 | A number of checks to make sure that data is generated correctly 234 | """ 235 | self.load_full() 236 | generators_not_shuffling = {dataset: self.get_generator(dataset, 16, False) for dataset in self.DATASETS} 237 | generators_shuffling = {dataset: self.get_generator(dataset, 16, True) for dataset in self.DATASETS} 238 | steps_per_epoch = {dataset: self.get_steps_per_epoch(dataset, 16) for dataset in self.DATASETS} 239 | 240 | # make sure that non-shuffling generators return data in the same order every epoch 241 | # and that shuffling generators don't 242 | for dataset in self.DATASETS: 243 | print(dataset) 244 | 245 | assert len(self.X[dataset]) == len(self.Y[dataset]) 246 | 247 | for _ in range(50): 248 | x1, y1 = next(generators_not_shuffling[dataset]) 249 | for _ in range(steps_per_epoch[dataset]): 250 | x2, y2 = next(generators_not_shuffling[dataset]) 251 | 252 | assert np.allclose(x1, x2) 253 | assert np.allclose(y1, y2) 254 | 255 | for _ in range(50): 256 | x1, y1 = next(generators_shuffling[dataset]) 257 | for _ in range(steps_per_epoch[dataset]): 258 | x2, y2 = next(generators_shuffling[dataset]) 259 | 260 | assert x1.shape != x2.shape or not np.allclose(x1, x2) 261 | 262 | if dataset != "hybrid": 263 | assert not np.allclose(y1, y2) 264 | 265 | # display some data 266 | for k in (6, 77, 99): 267 | for _ in range(k): 268 | x, y = next(generators_shuffling[dataset]) 269 | words = [self.rev_worddict[word] for word in x[0] if word > 0] 270 | label = self.rev_classdict[y[0]] 271 | text = " ".join(words) 272 | print(label) 273 | print(text) 274 | print() 275 | 276 | print("Hybrid documents") 277 | 278 | generator_hybrid = self.get_generator("hybrid", 1) 279 | counter = -1 280 | for k in (55, 66, 999): 281 | for _ in range(k): 282 | x, y = next(generator_hybrid) 283 | counter += 1 284 | words = [self.rev_worddict[word] for word in x[0] if word > 0] 285 | labels = ["(" + self.rev_classdict[label] + ")" for label in self.GT[counter]] 286 | text = " ".join(word + " " + label for word, label in zip(words, labels)) 287 | print(text) 288 | print() 289 | 290 | 291 | 292 | 293 | def delete_empty_documents(self, dataset): 294 | """ 295 | Delete any documents that do not contain any words (i.e., that were blank-only). 296 | 297 | dataset: one of 'train', 'dev', 'test', 'hybrid' 298 | """ 299 | print("Deleting empty documents in", dataset) 300 | number_documents = len(self.raw_documents[dataset]) 301 | indices = list(filter(lambda x:len(self.raw_documents[dataset][x].strip()), range(number_documents))) 302 | 303 | self.raw_documents[dataset] = [self.raw_documents[dataset][idx] for idx in indices] 304 | self.Y[dataset] = [self.Y[dataset][idx] for idx in indices] 305 | 306 | def tokenize_documents(self, dataset): 307 | print("Word-tokenizing documents in", dataset) 308 | self.tokenized_documents[dataset] = [word_tokenize(document) for document in self.raw_documents[dataset]] 309 | 310 | def shuffle_dataset(self, dataset): 311 | print("Shuffling dataset", dataset) 312 | 313 | indices = list(range(len(self.X[dataset]))) 314 | np.random.seed(0) 315 | np.random.shuffle(indices) 316 | 317 | self.X[dataset] = [self.X[dataset][idx] for idx in indices] 318 | self.Y[dataset] = [self.Y[dataset][idx] for idx in indices] 319 | self.tokenized_documents[dataset] = [self.tokenized_documents[dataset][idx] for idx in indices] 320 | self.raw_documents[dataset] = [self.raw_documents[dataset][idx] for idx in indices] 321 | 322 | 323 | def make_X(self, dataset): 324 | """ 325 | Create word index arrays from the tokenized documents. 326 | 327 | The word index arrays serve as input to training/evaluation/relevance scoring. 328 | """ 329 | print("Making X", dataset) 330 | self.X[dataset] = [] 331 | for document in self.tokenized_documents[dataset]: 332 | array = np.array([self.worddict.get(word, self.worddict["__oov__"]) for word in document]) 333 | self.X[dataset].append(array) 334 | 335 | def make_hybrid(self): 336 | """ 337 | Create hybrid documents by: 338 | 339 | 1) sentence-tokenizing the raw documents in the test set 340 | 2) shuffling all sentences 341 | 3) re-concatenating the sentences 342 | """ 343 | print("Making hybrid documents") 344 | self.X["hybrid"] = [] 345 | self.tokenized_documents["hybrid"] = [] 346 | self.GT = [] 347 | 348 | all_sentences = [] 349 | for document, label in zip(self.raw_documents["test"], self.Y["test"]): 350 | sentences = sent_tokenize(document) 351 | for sentence in sentences: 352 | all_sentences.append((sentence, label)) 353 | 354 | np.random.seed(0) 355 | np.random.shuffle(all_sentences) 356 | 357 | for i in range(0, len(all_sentences), self.HYBRID_LENGTH): 358 | batch = all_sentences[i:min(i+self.HYBRID_LENGTH, len(all_sentences))] 359 | 360 | hybrid_tokenized_document = [] 361 | hybrid_X = [] 362 | hybrid_labels = [] 363 | 364 | for sentence, label in batch: 365 | for word in word_tokenize(sentence): 366 | hybrid_tokenized_document.append(word) 367 | hybrid_X.append(self.worddict.get(word, self.worddict["__oov__"])) 368 | hybrid_labels.append(label) 369 | 370 | self.X["hybrid"].append(np.array(hybrid_X)) 371 | self.tokenized_documents["hybrid"].append(hybrid_tokenized_document) 372 | self.GT.append(np.array(hybrid_labels)) 373 | 374 | self.Y["hybrid"] = np.zeros(len(self.X["hybrid"])) # pseudo-labels, we won't do anything with these 375 | 376 | print("Created", len(self.X["hybrid"]), "hybrid documents from", len(self.X["test"]), "test documents") 377 | 378 | 379 | def make_word_to_freq(self): 380 | """ 381 | Map all words in the corpus to their absolute frequency 382 | """ 383 | word_to_freq = {} 384 | documents = self.tokenized_documents["train"] 385 | for document in documents: 386 | for word in document: 387 | if not word in self.worddict: # make sure we have not found one of the pre-defined words 388 | word_to_freq[word] = word_to_freq.get(word, 0) + 1 389 | 390 | return word_to_freq 391 | 392 | def make_worddict(self): 393 | """ 394 | Create a dictionary that maps word types to their frequency rank (e.g., 'and' -> 6) 395 | """ 396 | print("Making word dictionary") 397 | word_to_freq = self.make_word_to_freq() 398 | words = list(word_to_freq.keys()) 399 | words.sort() # sort alphabetically first to avoid non-deterministic ordering of words with the same frequency 400 | words.sort(key = lambda x:word_to_freq[x], reverse = True) 401 | 402 | for word in words[:self.FREQCAP-len(self.worddict)]: 403 | self.worddict[word] = len(self.worddict) 404 | 405 | print("Word dictionary size:", len(self.worddict)) 406 | 407 | 408 | class CorpusNewsgroup(Corpus): 409 | DATASETS_TMP = ("test", "train") # names of the datasets that are initially downloaded 410 | PATIENCE = 25 # number of epochs to wait for early stopping 411 | NAME = "newsgroup" 412 | 413 | def __init__(self, storagedir, embeddingpath = None, *args): 414 | super(CorpusNewsgroup, self).__init__(storagedir, embeddingpath) 415 | self.fetched = {} 416 | 417 | def make_classdict(self): 418 | """ 419 | Make a dictionary that maps class names to class indices (e.g., 'sci.med' -> 16) 420 | """ 421 | target_names = self.fetched["train"].target_names 422 | self.classdict = {target_names[idx]: idx for idx in range(len(target_names))} 423 | 424 | def get_raw_data(self, dataset): 425 | """ 426 | Download raw data for one of 'train', 'test' 427 | """ 428 | print("Getting raw data for", dataset) 429 | self.fetched[dataset] = fetch_20newsgroups(remove = ('headers', 'footers', 'quotes'), subset = dataset) 430 | self.raw_documents[dataset] = self.fetched[dataset].data 431 | self.Y[dataset] = self.fetched[dataset].target 432 | 433 | def split_dev(self): 434 | """ 435 | Randomly split test set into a development set and test set. 436 | """ 437 | print("Splitting test set into dev and test set") 438 | 439 | old_length = len(self.X["test"]) 440 | indices = list(range(old_length)) 441 | 442 | np.random.seed(0) 443 | np.random.shuffle(indices) 444 | 445 | split = int(len(indices) * 0.5) 446 | 447 | split_indices = {"test": indices[:split], "dev": indices[split:]} 448 | 449 | for dataset in ("dev", "test"): 450 | self.X[dataset] = [self.X["test"][idx] for idx in split_indices[dataset]] 451 | self.Y[dataset] = [self.Y["test"][idx] for idx in split_indices[dataset]] 452 | self.raw_documents[dataset] = [self.raw_documents["test"][idx] for idx in split_indices[dataset]] 453 | self.tokenized_documents[dataset] = [self.tokenized_documents["test"][idx] for idx in split_indices[dataset]] 454 | 455 | print("Split test set with", old_length, "samples into", len(self.X["test"]), "/", len(self.X["dev"]), "samples") 456 | 457 | 458 | class CorpusYelp(Corpus): 459 | DATASETS_TMP = ("test", "dev", "train") 460 | PATIENCE = 5 # since the yelp corpus takes longer to train, we only wait for five epochs before early stopping 461 | NAME = "yelp" 462 | 463 | def __init__(self, storagedir, embeddingpath = None, trainjson = None, devjson = None, testjson = None): 464 | super(CorpusYelp, self).__init__(storagedir, embeddingpath) 465 | self.jsons = {"train": trainjson, "test": testjson, "dev": devjson} 466 | 467 | def make_classdict(self): 468 | self.classdict = {"negative": 0, "positive": 1} 469 | 470 | def get_raw_data(self, dataset): 471 | """ 472 | Read raw data from the json file associated with 473 | 474 | (path to be set in config.py) 475 | """ 476 | print("Getting raw data for", dataset) 477 | self.raw_documents[dataset] = [] 478 | self.Y[dataset] = [] 479 | 480 | with open(self.jsons[dataset]) as handle: 481 | for line in handle: 482 | json_obj = json.loads(line) 483 | stars = json_obj["stars"] 484 | if stars != 3: 485 | self.raw_documents[dataset].append(json_obj["text"]) 486 | self.Y[dataset].append(int(stars > 3)) 487 | -------------------------------------------------------------------------------- /HybridDocuments/SRC/explanation_methods.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module responsible for explanation methods. 3 | """ 4 | 5 | import numpy as np 6 | np.random.seed(123) 7 | import keras 8 | import _pickle 9 | from keras.models import * 10 | from keras.layers import * 11 | from keras.wrappers import * 12 | from corpora import * 13 | from util import * 14 | from progressbar import ProgressBar 15 | 16 | def get_pred_generator(dataset, batchsize, architecture, corpus): 17 | """ 18 | Generator that will return arrays of size (batch_size,) with classes predicted by for 19 | data set of corpus . 20 | """ 21 | with open(make_predpath(dataset, architecture, corpus.NAME), "rb") as handle: 22 | pred = _pickle.load(handle) 23 | while True: 24 | for idx in range(0, len(pred), batchsize): 25 | yield np.array(pred[idx:min(idx+batchsize, len(pred))]) 26 | 27 | 28 | class Score: 29 | """ 30 | Parent class for all explanation methods. 31 | """ 32 | BATCH_SIZE = 1 33 | 34 | def __init__(self, orig): 35 | self.orig = orig # pointer to the tas kmethod 36 | 37 | def score_dataset(self, dataset, pred_only = False): 38 | """ 39 | Produce relevance scores for all samples of the dataset. 40 | 41 | dataset: one of 'test', 'hybrid' 42 | pred_only: if true, relevance scores are only calculated for the class predicted by the task method 43 | """ 44 | generator = self.orig.corpus.get_generator(dataset, self.BATCH_SIZE, shuffle = False) 45 | spe = self.orig.corpus.get_steps_per_epoch(dataset, self.BATCH_SIZE) 46 | if pred_only: 47 | pred_generator = get_pred_generator(dataset, self.BATCH_SIZE, self.orig.architecture, self.orig.corpus) 48 | else: 49 | pred_generator = None 50 | return self.score_from_generator(generator, spe, pred_generator = pred_generator, dataset = dataset) 51 | 52 | def score_from_generator(self, generator, spe, pred_generator, dataset): 53 | """ 54 | Produce relevance scores from a generator 55 | 56 | generator: generator that returns (X,Y) tuples where X has the shape (batch_size, num_words) 57 | spe: steps per epoch 58 | pred_generator: generator returning arrays of target classes of the shape (batch_size,); can be None 59 | """ 60 | bar = ProgressBar() 61 | 62 | scores = [] 63 | 64 | for _ in bar(list(range(spe))): 65 | x, y = next(generator) 66 | 67 | if pred_generator is None: 68 | pred = None 69 | else: 70 | pred = next(pred_generator) 71 | 72 | scores.extend(self.score(x, pred)) 73 | return scores 74 | 75 | def get_layer(self, layertype): 76 | """ 77 | Utility function that returns keras layer of a certain type. 78 | We can do this because there is only ever one layer of a particular type in our models, 79 | otherwise, this is a bad idea! 80 | 81 | layertype: layer class (e.g., Embedding, Bidirectional, Dense) 82 | """ 83 | tmp = list(filter(lambda x:type(x) == layertype, self.orig.model.layers)) 84 | assert len(tmp) == 1 85 | return tmp[0] 86 | 87 | class ScoreModel(Score): 88 | def score(self, x, pred): 89 | """ 90 | Return relevance scores for x. 91 | 92 | pred: if none, return scores for all possible classes 93 | else, assume that pred is a list of target classes 94 | """ 95 | 96 | # n.b. with all children of this class, calling score_k does not result in a speed-up, 97 | # since all target classes are calculated anyway 98 | tmp = self.score_model.predict(x) 99 | if not pred is None: 100 | # if we only look at predicted classes, get the correct indices 101 | tmp = np.array([t[:,p:p+1] for t,p in zip(tmp, pred)]) 102 | # cut off any zero padding 103 | tmp = [np.array([tmp[i][j] for j in range(tmp.shape[1]) if x[i][j] != 0]) for i in range(tmp.shape[0])] 104 | return tmp 105 | 106 | 107 | def check(self): 108 | """ 109 | Basic sanity check (e.g., output shapes ...) 110 | """ 111 | assert self.score_model.output_shape == (None,) + self.orig.model.output_shape 112 | 113 | this_weights = self.score_model.get_weights() 114 | orig_weights = self.get_orig_weights() 115 | 116 | assert len(this_weights) == len(orig_weights) 117 | assert all([np.allclose(x,y) for x,y in zip(this_weights, orig_weights)]) 118 | 119 | _ = self.score_model.predict(np.array([[1,2,3,4,5,0,0], [4,5,6,7,8,9,0]])) 120 | 121 | #print("Score model passed all checks") 122 | 123 | #self.score_model.summary() 124 | 125 | class ScoreModelLinear(ScoreModel): 126 | """ 127 | Score Model that is linear, meaning that the fully connected layer can be applied after the relevance scoring layer. 128 | """ 129 | def build(self): 130 | 131 | embedding = self.get_layer(Embedding) 132 | 133 | dense = self.get_layer(Dense) 134 | dense_config = dense.get_config() 135 | dense_config["use_bias"] = False # bias is cancelled out in beta, gamma & omission scores 136 | 137 | self.score_model = Sequential() 138 | self.score_model.add(embedding) 139 | self.build_inner() # build the relevance scoring layer 140 | 141 | self.score_model.add(TimeDistributed(Dense(**dense_config, weights = dense.get_weights()[:1]))) # add dense layer on top 142 | 143 | self.check() 144 | 145 | def get_orig_weights(self): 146 | return self.orig.model.get_weights()[:-1] 147 | 148 | class ScoreModelBetaGamma(ScoreModelLinear): 149 | """ 150 | Decomposition scores (Murdoch & Szlam 2017), section 3.4 151 | """ 152 | BATCH_SIZE = 8 153 | 154 | def build_inner(self): 155 | if self.orig.architecture == "CNN": 156 | raise Exception("Cannot use beta or gamma decomposition on a CNN") 157 | 158 | bidir = self.get_layer(Bidirectional) 159 | rnn = bidir.forward_layer 160 | rnn_config = rnn.get_config() 161 | dense = self.get_layer(Dense) 162 | 163 | # a bug (?) in keras means that we cannot keep the dropout 164 | # since dropout is only used in training, this does not make a difference to predictions; 165 | # but it keeps the theano backend from crashing 166 | for tmp in ("recurrent_dropout", "dropout"): 167 | if tmp in rnn_config: 168 | del rnn_config[tmp] 169 | 170 | self.score_model.add(Bidirectional(self._WRAPPER(rnn.__class__(**rnn_config)), 171 | merge_mode = "concat", weights = bidir.get_weights())) 172 | 173 | class ScoreModelErasure(ScoreModelLinear): 174 | """ 175 | Omission or occlusion, section 3.5 176 | """ 177 | 178 | def __init__(self, orig, mode, n_gram = 1, **kwargs): 179 | super(ScoreModelErasure, self).__init__(orig, **kwargs) 180 | self.n_gram = n_gram 181 | self.mode = mode 182 | 183 | def build_inner(self): 184 | if self.orig.architecture == "CNN": 185 | mainmodel = self.get_layer(Sequential) 186 | cnns = [l for l in mainmodel.layers if isinstance(l, Conv1D)] 187 | main = Sequential([Conv1D(**cnn.get_config(), weights = cnn.get_weights()) for cnn in cnns]) 188 | main.add(GlobalMaxPooling1D()) 189 | else: 190 | bidir = self.get_layer(Bidirectional) 191 | rnn = bidir.forward_layer 192 | rnn_config = rnn.get_config() 193 | for tmp in ("recurrent_dropout", "dropout"): 194 | if tmp in rnn_config: del rnn_config[tmp] 195 | main = Bidirectional(rnn.__class__(**rnn_config), 196 | merge_mode = "concat", weights = bidir.get_weights()) 197 | self.score_model.add(ErasureWrapper(main, ngram = self.n_gram, mode = self.mode)) 198 | 199 | def score(self, x, pred): 200 | x = np.concatenate([x, np.zeros((x.shape[0], max(0, self.n_gram-x.shape[1])))], axis=1) 201 | orig = self.score_model.predict(x) 202 | if not pred is None: 203 | orig = np.array([t[:,p:p+1] for t,p in zip(orig, pred)]) 204 | 205 | tmp = [] 206 | for o in orig: 207 | stack = [] 208 | for n in range(self.n_gram): 209 | left = np.zeros((self.n_gram - 1 - n, o.shape[1])) 210 | right = np.zeros((n, o.shape[1])) 211 | stack.append(np.concatenate([left, o, right], axis = 0)) 212 | stack = np.stack(stack, axis = 0) 213 | mean = np.sum(stack, axis = 0) / self.n_gram 214 | assert mean.shape == (o.shape[0] + self.n_gram - 1, o.shape[1]) 215 | tmp.append(mean) 216 | tmp = np.array(tmp) 217 | tmp = [np.array([tmp[i][j] for j in range(tmp.shape[1]) if x[i][j] != 0]) for i in range(tmp.shape[0])] 218 | return tmp 219 | 220 | 221 | class ScoreModelGradient(ScoreModel): 222 | """ 223 | Gradient scores, section 3.1 224 | """ 225 | def __init__(self, orig, mode, integrated = False, **kwargs): 226 | ScoreModel.__init__(self, orig, **kwargs) 227 | self._SCORE = self._SCORE + mode 228 | self.mode = mode 229 | # simple gradient means integrated gradient with M = 1 230 | self.num_alpha = 1 + 49 * int(integrated) 231 | 232 | def build(self): 233 | old_embedding = self.get_layer(Embedding) 234 | self.embmodel = Sequential([Embedding(**old_embedding.get_config(), weights = old_embedding.get_weights())]) 235 | 236 | def build_n(self, n): 237 | """ 238 | Since the Gradient Wrapper gets very slow when we ask it to calculate 20 classes at once, we build one model per target class. 239 | 240 | n: the index of the class that we are interested in 241 | """ 242 | old_dense = self.get_layer(Dense) 243 | old_embedding = self.get_layer(Embedding) 244 | 245 | inp = Input((None,)) 246 | self.score_model = Sequential()#[old_embedding]) 247 | 248 | main = Sequential() 249 | input_shape = (None, old_embedding.output_dim) 250 | 251 | if self.orig.architecture == "CNN": 252 | mainmodel = self.get_layer(Sequential) 253 | cnns = [l for l in mainmodel.layers if isinstance(l, Conv1D)] 254 | cnns = [Conv1D(**cnn.get_config(), weights = cnn.get_weights()) for cnn in cnns] 255 | for cnn in cnns: 256 | main.add(cnn) 257 | main.add(GlobalMaxPooling1D()) 258 | 259 | else: 260 | old_bidir = self.get_layer(Bidirectional) 261 | rnn = old_bidir.forward_layer 262 | rnn_config = rnn.get_config() 263 | for tmp in ("recurrent_dropout", "dropout"): 264 | if tmp in rnn_config: 265 | del rnn_config[tmp] 266 | bidir = Bidirectional(rnn.__class__(**rnn_config), input_shape=input_shape, 267 | merge_mode = "concat", weights = old_bidir.get_weights()) 268 | main.add(bidir) 269 | 270 | main.add(Dense(**old_dense.get_config(), weights = old_dense.get_weights())) 271 | main.add(Activation(self._ACTIVATION)) 272 | 273 | self.score_model.add(GradientWrapper(main, mode = None, out = n, input_shape = input_shape, num_alpha = 1)) 274 | 275 | def score_dataset(self, dataset, pred_only = False): 276 | self.orig.corpus.load_if_necessary("classdict") 277 | self.orig.corpus.load_if_necessary("X") 278 | self.score_model = 0 279 | if pred_only: 280 | return self.score_dataset_pred(dataset) 281 | return self.score_dataset_all(dataset) 282 | 283 | def score_dataset_pred(self, dataset): 284 | """ 285 | Calculate relevance scores only for the predicted classes 286 | """ 287 | X = self.orig.corpus.X[dataset] 288 | 289 | pred_generator = get_pred_generator(dataset, self.BATCH_SIZE, self.orig.architecture, self.orig.corpus) 290 | spe = self.orig.corpus.get_steps_per_epoch(dataset, self.BATCH_SIZE) 291 | 292 | pred = [] 293 | for _ in range(spe): 294 | pred.extend(next(pred_generator)) 295 | 296 | scores = [None for _ in range(len(X))] 297 | 298 | by_pred = {p:[] for p in range(len(self.orig.corpus.classdict))} 299 | for x, p, i in zip(X, pred, range(len(X))): 300 | # since we are going by target classes, we have to remember where samples originated 301 | by_pred[p].append((x, i)) 302 | 303 | for p in by_pred: 304 | del self.score_model 305 | self.build_n(p) 306 | 307 | bar = ProgressBar() 308 | 309 | for x,i in bar(by_pred[p]): 310 | E = self.embmodel.predict(np.array([x[:self.orig.corpus.MAXLENGTH]])) 311 | 312 | tmp = 0 313 | for j in range(self.num_alpha): 314 | tmp += self.score_model.predict(E * (j+1) / self.num_alpha)[0] 315 | tmp /= self.num_alpha 316 | 317 | if self.mode == "dot": 318 | scores[i] = np.sum(np.expand_dims(E[0], -1) * tmp, axis = 1) 319 | elif self.mode == "l2": 320 | scores[i] = np.sqrt(np.sum(tmp * tmp, axis = 1)) 321 | 322 | assert len(scores[i].shape) == 2 and scores[i].shape[0] == x[:self.orig.corpus.MAXLENGTH].shape[0] 323 | 324 | assert all([not s is None for s in scores]) # make sure we have not left out anything 325 | return scores 326 | 327 | def score(self, x, pred=None): 328 | E = self.embmodel.predict(x[:,:self.orig.corpus.MAXLENGTH]) 329 | 330 | tmp = 0 331 | for j in range(self.num_alpha): 332 | tmp += self.score_model.predict(E * (j+1) / self.num_alpha) 333 | tmp /= self.num_alpha 334 | 335 | if self.mode == "dot": 336 | return np.sum(np.expand_dims(E, -1) * tmp, axis = 2) 337 | elif self.mode == "l2": 338 | return np.sqrt(np.sum(tmp * tmp, axis = 2)) 339 | 340 | 341 | def score_dataset_all(self, dataset): 342 | """ 343 | Calculate relevance scores for all possible target classes 344 | """ 345 | stack = [] 346 | for n in range(len(self.orig.corpus.classdict)): 347 | generator = self.orig.corpus.get_generator(dataset, self.BATCH_SIZE, shuffle = False) 348 | spe = self.orig.corpus.get_steps_per_epoch(dataset, self.BATCH_SIZE) 349 | 350 | del self.score_model 351 | self.build_n(n) 352 | 353 | bar = ProgressBar() 354 | 355 | scores = [] 356 | 357 | for _ in bar(list(range(spe))): 358 | x, y = next(generator) 359 | score = self.score(x, None) 360 | for sample_x, sample_score in zip(x, score): 361 | tmp = np.array([sample_score[i] for i in range(sample_x.shape[0]) if sample_x[i] != 0]) 362 | scores.append(tmp) 363 | 364 | 365 | stack.append(scores) 366 | 367 | return [np.concatenate(x, axis = -1) for x in zip(*stack)] 368 | 369 | 370 | 371 | class ScoreModelGradientRaw(ScoreModelGradient): 372 | _SCORE = "grad_raw" 373 | _ACTIVATION = "linear" 374 | 375 | 376 | class ScoreModelGradientProb(ScoreModelGradient): 377 | _SCORE = "grad_prob" 378 | def __init__(self, orig, mode, integrated = False, **kwargs): 379 | super(ScoreModelGradientProb, self).__init__(orig = orig, mode = mode, integrated = integrated, **kwargs) 380 | self._ACTIVATION = "softmax" 381 | 382 | class ScoreModelBeta(ScoreModelBetaGamma): 383 | _WRAPPER = BetaDecomposition 384 | _SCORE = "beta" 385 | 386 | class ScoreModelGamma(ScoreModelBetaGamma): 387 | _WRAPPER = GammaDecomposition 388 | _SCORE = "gamma" 389 | 390 | 391 | class ScoreLime(Score): 392 | """ 393 | LIMSSE method, section 3.6 394 | """ 395 | def score(self, x, pred): 396 | if not pred is None: 397 | tmp = self.lime.call(x, verbose = 0, out = pred) 398 | else: 399 | tmp = self.lime.call(x, verbose = 0) 400 | tmp = [np.array([tmp[i][j] for j in range(tmp.shape[1]) if x[i][j] != 0]) for i in range(tmp.shape[0])] 401 | return tmp 402 | 403 | def build(self): 404 | self.lime = self._LIMECLASS(self.orig.model, nb_samples = 3000, **self._LIMEPARAMS) 405 | 406 | class ScoreLimeClass(ScoreLime): 407 | _SCORE = "lime_class" 408 | _LIMECLASS = TextLime 409 | _LIMEPARAMS = {"loss": "binary_crossentropy", "activation": "sigmoid", "minlength": 1, "maxlength": 7, "mode": "random"} 410 | 411 | class ScoreLimeProb(ScoreLime): 412 | _SCORE = "lime_prob" 413 | _LIMECLASS = TextLime 414 | _LIMEPARAMS = {"loss": "mse", "activation": "linear", "minlength": 1, "maxlength": 7, "mode": "random"} 415 | 416 | class ScoreLimeRaw(ScoreLime): 417 | _SCORE = "lime_raw" 418 | _LIMEPARAMS = {"loss": "mse", "activation": "linear", "minlength": 1, "maxlength": 7, "mode": "random"} 419 | 420 | def build(self): 421 | model_config = self.orig.model.get_config() 422 | model_config["layers"][-1]["config"]["activation"] = "linear" 423 | # copy the original architecture but replace the softmax activation with a linear activation 424 | copy = Model.from_config(model_config) 425 | copy.set_weights(self.orig.model.get_weights()) 426 | self.lime = TextLime(copy, **self._LIMEPARAMS) 427 | 428 | 429 | class ScoreThirdParty(Score): 430 | def build(self): 431 | """ 432 | Build relevance scoring models using third party code 433 | """ 434 | sys.path.append(LRP_RNN_REPO) 435 | if self.orig.architecture == "LSTM": # LRP_for_LSTM 436 | from layers import keras_to_weights_birnn, BILSTM_np 437 | weights = keras_to_weights_birnn(self.orig.model, [0,2,1,3]) # get and refromat weights from original model 438 | self.score_model = BILSTM_np(weights, mode = self.MODE) 439 | elif self.orig.architecture == "GRU": # LRP_for_LSTM 440 | from layers import keras_to_weights_birnn, BIGRU_np 441 | weights = keras_to_weights_birnn(self.orig.model, [0,1,2]) 442 | self.score_model = BIGRU_np(weights, mode = self.MODE) 443 | elif self.orig.architecture == "QLSTM": # LRP_for_LSTM 444 | from layers import keras_to_weights_biqrnn, BIQLSTM_np 445 | weights = keras_to_weights_biqrnn(self.orig.model, [0,2,1,3]) # get and refromat weights from original model 446 | self.score_model = BIQLSTM_np(weights, mode = self.MODE) 447 | elif self.orig.architecture == "QGRU": # LRP_for_LSTM 448 | from layers import keras_to_weights_biqrnn, BIQGRU_np 449 | weights = keras_to_weights_biqrnn(self.orig.model, [0,1]) 450 | self.score_model = BIQGRU_np(weights, mode = self.MODE) 451 | elif self.orig.architecture == "CNN": 452 | from layers import keras_to_weights_cnn, CNN_np 453 | weights = keras_to_weights_cnn(self.orig.model) 454 | self.score_model = CNN_np(weights, mode = self.MODE) 455 | sys.path.pop() 456 | 457 | def score(self, x, pred): 458 | all_scores = [] 459 | 460 | if pred is None: # if pred is None, assume that we want relevance scores for all possible classes 461 | pred = [list(range(len(self.orig.corpus.classdict))) for _ in range(x.shape[0])] 462 | else: 463 | pred = [[p] for p in pred] 464 | 465 | for x_sample, pred_sample in zip(x, pred): 466 | scores = [] 467 | for cl in pred_sample: 468 | forward, backward, _ = self.score_model.lrp(x_sample.astype(int), cl, eps = 0.001, bias_factor = 0.0) 469 | # sum relevance scores from the forward and backward RNNs, then sum over the embedding dimension 470 | scores.append(np.sum(forward + backward, axis = -1)) 471 | 472 | scores = np.stack(scores, -1) 473 | assert scores.shape == (x_sample.shape[0], len(pred_sample)) 474 | all_scores.append(scores) 475 | return all_scores 476 | 477 | class ScoreDL(ScoreThirdParty): 478 | """DeepLIFT method, section 3.3""" 479 | MODE = "dl" 480 | 481 | class ScoreLRP(ScoreThirdParty): 482 | """LRP method, section 3.2""" 483 | MODE = "lrp" 484 | -------------------------------------------------------------------------------- /HybridDocuments/ThirdParty/LRP_and_DeepLIFT/code/layers.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @author: Leila Arras 3 | @maintainer: Leila Arras 4 | @date: 21.06.2017 5 | @version: 1.0 6 | @copyright: Copyright (c) 2017, Leila Arras, Gregoire Montavon, Klaus-Robert Mueller, Wojciech Samek 7 | @license: BSD-2-Clause 8 | ''' 9 | 10 | import numpy as np 11 | import pickle 12 | from numpy import newaxis as na 13 | from LRP_linear_layer import * 14 | 15 | def keras_to_weights_biqrnn(orig_model, reordering): 16 | weights = {} 17 | 18 | embedding = orig_model.get_layer("embedding") 19 | bidir = orig_model.get_layer("main") 20 | dense = orig_model.layers[-2] 21 | 22 | weights["E"] = np.array(embedding.embeddings.container.storage[0]) 23 | 24 | # left encoder 25 | SIDE_MAPPING = {"Left": "forward_layer", "Right": "backward_layer"} 26 | 27 | for side in SIDE_MAPPING: 28 | layer = getattr(bidir, SIDE_MAPPING[side]) 29 | tmp = getattr(layer, "kernel") 30 | rnn_weights = np.array(tmp.container.storage[0]) 31 | 32 | d = rnn_weights.shape[-1] // len(reordering) 33 | slices = [slice(s*d, (s+1)*d) for s in reordering] 34 | 35 | rnn_weights = np.concatenate([rnn_weights[:,:,s] for s in slices], axis = 2) 36 | 37 | bias = np.array(layer.bias.container.storage[0]) 38 | bias = np.concatenate([bias[s] for s in slices], axis = 0) 39 | 40 | weights["bxh_" + side] = bias 41 | weights["Wxh_" + side] = rnn_weights 42 | 43 | dense_weights = np.array(dense.kernel.container.storage[0]) 44 | dense_bias = np.array(dense.bias.container.storage[0]) 45 | split = dense_weights.shape[0] // 2 46 | 47 | weights["Why_Left"] = dense_weights[:split].transpose() 48 | weights["Why_Right"] = dense_weights[split:].transpose() 49 | weights["bhy_Left"] = dense_bias / 2 50 | weights["bhy_Right"] = dense_bias / 2 51 | 52 | return weights 53 | 54 | def keras_to_weights_cnn(orig_model): 55 | embedding = orig_model.get_layer("embedding") 56 | dense = orig_model.get_layer("dense_1") 57 | 58 | weights = {} 59 | weights["E"] = orig_model.get_weights()[0] 60 | weights["Why"] = orig_model.get_weights()[-2].transpose() 61 | weights["bhy"] = orig_model.get_weights()[-1] 62 | 63 | weights["W"] = orig_model.get_weights()[1] 64 | weights["B"] = orig_model.get_weights()[2] 65 | 66 | return weights 67 | 68 | def keras_to_weights_birnn(orig_model, reordering): 69 | weights = {} 70 | 71 | embedding = orig_model.get_layer("embedding") 72 | bidir = orig_model.get_layer("main") 73 | dense = orig_model.get_layer("dense_1") 74 | 75 | weights["E"] = np.array(embedding.embeddings.container.storage[0]) 76 | 77 | # left encoder 78 | SIDE_MAPPING = {"Left": "forward_layer", "Right": "backward_layer"} 79 | 80 | CONNECTION_MAPPING = {"x": "kernel", "h": "recurrent_kernel"} 81 | 82 | 83 | for side in SIDE_MAPPING: 84 | for connection in CONNECTION_MAPPING: 85 | layer = getattr(bidir, SIDE_MAPPING[side]) 86 | tmp = getattr(layer, CONNECTION_MAPPING[connection]) 87 | rnn_weights = np.array(tmp.container.storage[0]) 88 | 89 | d = rnn_weights.shape[1] // len(reordering) 90 | slices = [slice(s*d, (s+1)*d) for s in reordering] 91 | 92 | rnn_weights = np.concatenate([rnn_weights[:,s] for s in slices], axis = 1) 93 | if connection == "h": 94 | rnn_weights = np.concatenate([rnn_weights[s] for s in slices], axis = 0) 95 | 96 | weights["W" + connection + "h_" + side] = np.transpose(rnn_weights) 97 | 98 | bias = np.array(layer.bias.container.storage[0]) 99 | bias = np.concatenate([bias[s] for s in slices], axis = 0) 100 | 101 | weights["bxh_" + side] = bias / 2 102 | weights["bhh_" + side] = bias / 2 103 | 104 | dense_weights = np.array(dense.kernel.container.storage[0]) 105 | dense_bias = np.array(dense.bias.container.storage[0]) 106 | split = dense_weights.shape[0] // 2 107 | 108 | weights["Why_Left"] = dense_weights[:split].transpose() 109 | weights["Why_Right"] = dense_weights[split:].transpose() 110 | weights["bhy_Left"] = dense_bias / 2 111 | weights["bhy_Right"] = dense_bias / 2 112 | 113 | return weights 114 | 115 | 116 | class CNN_np: 117 | def __init__(self, weights, mode): 118 | for key in weights: 119 | setattr(self, key, weights[key]) 120 | 121 | assert mode in ("dl", "lrp", "dlrec") 122 | 123 | self.mode = mode 124 | if mode == "dl": 125 | self.ref = CNN_np(weights, "dlrec") 126 | 127 | def set_input(self, w, delete_pos=None): 128 | """ 129 | Build the numerical input x/x_rev from word sequence indices w (+ initialize hidden layers h, c) 130 | Optionally delete words at positions delete_pos. 131 | """ 132 | T = w.shape[-1] # input word sequence length 133 | d = int(self.W.shape[-1]) # hidden layer dimension 134 | f = int(self.W.shape[0]) 135 | e = self.E.shape[1] # word embedding dimension 136 | x = np.zeros((T, e)) 137 | x[:,:] = self.E[w,:] 138 | if delete_pos is not None: 139 | x[delete_pos, :] = np.zeros((len(delete_pos), e)) 140 | 141 | self.w = w 142 | self.x = np.concatenate([np.zeros(((f-1)//2, e)), x, np.zeros(((f-1)//2, e))], axis = 0) 143 | self.g = np.zeros((T, d)) 144 | self.g_pre = np.zeros((T, d)) 145 | 146 | 147 | def forward(self): 148 | """ 149 | Update the hidden layer values (using model weights and numerical input x/x_rev previously built from word sequence w) 150 | """ 151 | T = self.w.shape[0] 152 | d = int(self.W.shape[-1]) 153 | f = int(self.W.shape[0]) 154 | 155 | for t in range(T): 156 | for c in range(f): 157 | self.g_pre[t] += np.dot(self.x[t+f-1-c], self.W[c]) 158 | self.g_pre[t] += self.B 159 | self.g[t] = np.maximum(self.g_pre[t], np.zeros(d)) 160 | 161 | self.h = self.g.max(axis = 0) 162 | 163 | self.s = np.dot(self.Why, self.h) + self.bhy 164 | self.pred = np.exp(self.s) / np.sum(np.exp(self.s)) 165 | return self.s.copy() # prediction scores 166 | 167 | def predict(self, w): 168 | self.set_input(w) 169 | self.forward() 170 | return self.pred 171 | 172 | def lrp(self, w, LRP_class, eps=0.001, bias_factor=1.0): 173 | """ 174 | Update the hidden layer relevances by performing LRP for the target class LRP_class 175 | """ 176 | # forward pass 177 | self.set_input(w) 178 | self.forward() 179 | 180 | if self.mode == "dl": 181 | self.ref.set_input(w) 182 | self.ref.x = np.zeros_like(self.x) 183 | self.ref.x_rev = np.zeros_like(self.x) 184 | 185 | self.ref.forward() 186 | 187 | T = self.w.shape[0] 188 | d = int(self.W.shape[-1]) 189 | e = self.E.shape[1] 190 | C = self.Why.shape[0] # number of classes 191 | f = int(self.W.shape[0]) 192 | 193 | # initialize 194 | Rx = np.zeros(self.x.shape) 195 | 196 | Rh = np.zeros((d,)) 197 | Rg = np.zeros((T, d)) # gate g only 198 | 199 | Rout_mask = np.zeros((C)) 200 | Rout_mask[LRP_class] = 1.0 201 | 202 | # format reminder: lrp_linear(hin, w, b, hout, Rout, bias_nb_units, eps, bias_factor) 203 | 204 | s = self.s.copy() 205 | h = self.h.copy() 206 | g = self.g.copy() 207 | g_pre = self.g_pre.copy() 208 | 209 | if self.mode == "dl": 210 | s -= self.ref.s 211 | h -= self.ref.h 212 | g -= self.ref.g 213 | g_pre -= self.ref.g_pre 214 | 215 | Rh = lrp_linear(h, self.Why.T , np.zeros((C)), s, s*Rout_mask, d, eps, bias_factor, debug=False) 216 | for feature in range(d): 217 | Rg[g[:,feature].argmax()][feature] = Rh[feature] 218 | 219 | for t in range(T): 220 | for c in range(f): 221 | Rx[t+f-1-c] += lrp_linear(self.x[t+f-1-c], self.W[c], self.B, g_pre[t], Rg[t], f*e, eps, bias_factor, debug=False) 222 | a, b, c = Rx[(f-1)//2:-(f-1)//2], 0, Rx[:(f-1)//2].sum() + Rx[-(f-1)//2:].sum() 223 | 224 | return a, b, c 225 | 226 | 227 | class BIQGRU_np: 228 | def __init__(self, weights, mode): 229 | for key in weights: 230 | setattr(self, key, weights[key]) 231 | 232 | assert mode in ("dl", "lrp", "dlrec") 233 | 234 | self.mode = mode 235 | if mode == "dl": 236 | self.ref = BIQGRU_np(weights, "dlrec") 237 | 238 | def set_input(self, w, delete_pos=None): 239 | """ 240 | Build the numerical input x/x_rev from word sequence indices w (+ initialize hidden layers h, c) 241 | Optionally delete words at positions delete_pos. 242 | """ 243 | T = w.shape[-1] # input word sequence length 244 | d = int(self.Wxh_Left.shape[-1]/2) # hidden layer dimension 245 | f = int(self.Wxh_Left.shape[0]) 246 | e = self.E.shape[1] # word embedding dimension 247 | x = np.zeros((T, e)) 248 | x[:,:] = self.E[w,:] 249 | if delete_pos is not None: 250 | x[delete_pos, :] = np.zeros((len(delete_pos), e)) 251 | 252 | self.w = w 253 | self.x = np.concatenate([np.zeros((f-1, e)), x], axis = 0) 254 | self.x_rev = np.concatenate([np.zeros((f-1, e)), x[::-1].copy()], axis = 0) 255 | 256 | self.h_Left = np.zeros((T, d)) 257 | self.h_Right = np.zeros((T, d)) 258 | 259 | 260 | def forward(self): 261 | """ 262 | Update the hidden layer values (using model weights and numerical input x/x_rev previously built from word sequence w) 263 | """ 264 | T = self.w.shape[0] 265 | d = int(self.Wxh_Left.shape[-1]/2) 266 | f = int(self.Wxh_Left.shape[0]) 267 | 268 | # initialize 269 | self.gates_pre_Left = np.zeros((T, 2*d)) # gates i, g, f, o pre-activation 270 | self.gates_Left = np.zeros((T, 2*d)) # gates i, g, f, o activation 271 | 272 | self.gates_pre_Right= np.zeros((T, 2*d)) 273 | self.gates_Right = np.zeros((T, 2*d)) 274 | 275 | for t in range(T): 276 | for c in range(f): 277 | self.gates_pre_Left[t] += np.dot(self.x[t+f-1-c], self.Wxh_Left[c]) 278 | self.gates_pre_Left[t] += + self.bxh_Left 279 | zeros = np.zeros_like(self.gates_pre_Left[t,:d]) 280 | ones = zeros + 1 281 | self.gates_Left[t,:d] = np.maximum(zeros, np.minimum(ones, 0.2*self.gates_pre_Left[t,:d] + 0.5)) 282 | self.gates_Left[t,d:2*d] = np.tanh(self.gates_pre_Left[t,d:2*d]) 283 | self.h_Left[t] = self.gates_Left[t,:d]*self.h_Left[t-1] + (1-self.gates_Left[t,:d]) * self.gates_Left[t,d:] 284 | 285 | for c in range(f): 286 | self.gates_pre_Right[t] += np.dot(self.x_rev[t+f-1-c], self.Wxh_Right[c]) 287 | self.gates_pre_Right[t] += + self.bxh_Right 288 | zeros = np.zeros_like(self.gates_pre_Right[t,:d]) 289 | ones = zeros + 1 290 | self.gates_Right[t,:d] = np.maximum(zeros, np.minimum(ones, 0.2*self.gates_pre_Right[t,:d] + 0.5)) 291 | self.gates_Right[t,d:2*d] = np.tanh(self.gates_pre_Right[t,d:2*d]) 292 | self.h_Right[t] = self.gates_Right[t,:d]*self.h_Right[t-1] + (1-self.gates_Right[t,:d]) * self.gates_Right[t,d:] 293 | 294 | self.y_Left = np.dot(self.Why_Left, self.h_Left[T-1]) + self.bhy_Left 295 | self.y_Right = np.dot(self.Why_Right, self.h_Right[T-1]) + self.bhy_Right 296 | self.s = self.y_Left + self.y_Right 297 | 298 | self.pred = np.exp(self.s) / np.sum(np.exp(self.s)) 299 | return self.s.copy() # prediction scores 300 | 301 | def predict(self, w): 302 | self.set_input(w) 303 | self.forward() 304 | return self.pred 305 | 306 | def lrp(self, w, LRP_class, eps=0.001, bias_factor=1.0): 307 | """ 308 | Update the hidden layer relevances by performing LRP for the target class LRP_class 309 | """ 310 | # forward pass 311 | self.set_input(w) 312 | self.forward() 313 | 314 | if self.mode == "dl": 315 | self.ref.set_input(w) 316 | self.ref.x = np.zeros_like(self.x) 317 | self.ref.x_rev = np.zeros_like(self.x) 318 | 319 | self.ref.forward() 320 | 321 | T = self.w.shape[0] 322 | d = int(self.Wxh_Left.shape[-1]/2) 323 | e = self.E.shape[1] 324 | C = self.Why_Left.shape[0] # number of classes 325 | f = int(self.Wxh_Left.shape[0]) 326 | 327 | # initialize 328 | Rx = np.zeros(self.x.shape) 329 | Rx_rev = np.zeros(self.x.shape) 330 | 331 | Rh_Left = np.zeros((T+1, d)) 332 | Rg_Left = np.zeros((T, d)) # gate g only 333 | Rh_Right = np.zeros((T+1, d)) 334 | Rg_Right = np.zeros((T, d)) # gate g only 335 | 336 | Rout_mask = np.zeros((C)) 337 | Rout_mask[LRP_class] = 1.0 338 | 339 | # format reminder: lrp_linear(hin, w, b, hout, Rout, bias_nb_units, eps, bias_factor) 340 | 341 | s = self.s.copy() 342 | 343 | gates_Left = self.gates_Left.copy() 344 | gates_pre_Left = self.gates_pre_Left.copy() 345 | h_Left = self.h_Left.copy() 346 | 347 | gates_Right = self.gates_Right.copy() 348 | gates_pre_Right = self.gates_pre_Right.copy() 349 | h_Right = self.h_Right.copy() 350 | 351 | if self.mode == "dl": 352 | s -= self.ref.s 353 | 354 | gates_Left[:,d:] -= self.ref.gates_Left[:,d:] 355 | gates_pre_Left[:,d:] -= self.ref.gates_pre_Left[:,d:] 356 | h_Left -= self.ref.h_Left 357 | 358 | gates_Right[:,d:] -= self.ref.gates_Right[:,d:] 359 | gates_pre_Right[:,d:] -= self.ref.gates_pre_Right[:,d:] 360 | h_Right -= self.ref.h_Right 361 | 362 | 363 | Rh_Left[T-1] = lrp_linear(h_Left[T-1], self.Why_Left.T , np.zeros((C)), s, s*Rout_mask, 2*d, eps, bias_factor, debug=False) 364 | Rh_Right[T-1] = lrp_linear(h_Right[T-1], self.Why_Right.T, np.zeros((C)), s, s*Rout_mask, 2*d, eps, bias_factor, debug=False) 365 | for t in reversed(range(T)): 366 | Rh_Left[t-1] = lrp_linear(gates_Left[t,:d]*h_Left[t-1], np.identity(d), np.zeros((d)), h_Left[t], Rh_Left[t], 2*d, eps, bias_factor, debug=False) 367 | Rg_Left[t] = lrp_linear((1-gates_Left[t,:d])*gates_Left[t,d:], np.identity(d), np.zeros((d)), h_Left[t], Rh_Left[t], 2*d, eps, bias_factor, debug=False) 368 | 369 | Rh_Right[t-1] = lrp_linear(gates_Right[t,:d]*h_Right[t-1], np.identity(d), np.zeros((d)), h_Right[t], Rh_Right[t], 2*d, eps, bias_factor, debug=False) 370 | Rg_Right[t] = lrp_linear((1-gates_Right[t,:d])*gates_Right[t,d:], np.identity(d), np.zeros((d)), h_Right[t], Rh_Right[t], 2*d, eps, bias_factor, debug=False) 371 | 372 | for t in range(T): 373 | for c in range(f): 374 | Rx[t+f-1-c] += lrp_linear(self.x[t+f-1-c], self.Wxh_Left[c,:,d:], self.bxh_Left[d:], gates_pre_Left[t,d:], Rg_Left[t], f*e, eps, bias_factor, debug=False) 375 | Rx_rev[t+f-1-c] += lrp_linear(self.x_rev[t+f-1-c], self.Wxh_Right[c, :, d:], self.bxh_Right[d:], gates_pre_Right[t,d:], Rg_Right[t], f*e, eps, bias_factor, debug=False) 376 | 377 | a, b, c = Rx[f-1:], Rx_rev[f-1:][::-1,:], Rh_Left[-1].sum()+Rh_Right[-1].sum()+ + Rx[:f-1].sum() + Rx_rev[:f-1][::-1].sum() 378 | 379 | summed = np.sum(a+b, axis = -1) 380 | relpeak = summed.argmax(-1) / len(summed) 381 | return a, b, c 382 | 383 | class BIQLSTM_np: 384 | def __init__(self, weights, mode): 385 | for key in weights: 386 | setattr(self, key, weights[key]) 387 | 388 | assert mode in ("dl", "lrp", "dlrec") 389 | 390 | self.mode = mode 391 | if mode == "dl": 392 | self.ref = BIQLSTM_np(weights, "dlrec") 393 | 394 | def set_input(self, w, delete_pos=None): 395 | """ 396 | Build the numerical input x/x_rev from word sequence indices w (+ initialize hidden layers h, c) 397 | Optionally delete words at positions delete_pos. 398 | """ 399 | T = w.shape[-1] # input word sequence length 400 | d = int(self.Wxh_Left.shape[-1]/4) # hidden layer dimension 401 | f = int(self.Wxh_Left.shape[0]) 402 | e = self.E.shape[1] # word embedding dimension 403 | x = np.zeros((T, e)) 404 | x[:,:] = self.E[w,:] 405 | if delete_pos is not None: 406 | x[delete_pos, :] = np.zeros((len(delete_pos), e)) 407 | 408 | self.w = w 409 | self.x = np.concatenate([np.zeros((f-1, e)), x], axis = 0) 410 | self.x_rev = np.concatenate([np.zeros((f-1, e)), x[::-1].copy()], axis = 0) 411 | 412 | self.h_Left = np.zeros((T, d)) 413 | self.c_Left = np.zeros((T, d)) 414 | self.h_Right = np.zeros((T, d)) 415 | self.c_Right = np.zeros((T, d)) 416 | 417 | 418 | def forward(self): 419 | """ 420 | Update the hidden layer values (using model weights and numerical input x/x_rev previously built from word sequence w) 421 | """ 422 | T = self.w.shape[0] 423 | d = int(self.Wxh_Left.shape[-1]/4) 424 | idx = np.hstack((np.arange(0,d), np.arange(2*d,4*d))).astype(int) # indices of the gates i,f,o 425 | f = int(self.Wxh_Left.shape[0]) 426 | 427 | # initialize 428 | self.gates_pre_Left = np.zeros((T, 4*d)) # gates i, g, f, o pre-activation 429 | self.gates_Left = np.zeros((T, 4*d)) # gates i, g, f, o activation 430 | 431 | self.gates_pre_Right= np.zeros((T, 4*d)) 432 | self.gates_Right = np.zeros((T, 4*d)) 433 | 434 | for t in range(T): 435 | for c in range(f): 436 | self.gates_pre_Left[t] += np.dot(self.x[t+f-1-c], self.Wxh_Left[c]) 437 | self.gates_pre_Left[t] += + self.bxh_Left 438 | zeros = np.zeros_like(self.gates_pre_Left[t,idx]) 439 | ones = zeros + 1 440 | self.gates_Left[t,idx] = np.maximum(zeros, np.minimum(ones, 0.2*self.gates_pre_Left[t,idx] + 0.5)) 441 | self.gates_Left[t,d:2*d] = np.tanh(self.gates_pre_Left[t,d:2*d]) 442 | self.c_Left[t] = self.gates_Left[t,2*d:3*d]*self.c_Left[t-1] + self.gates_Left[t,0:d]*self.gates_Left[t,d:2*d] 443 | self.h_Left[t] = self.gates_Left[t,3*d:4*d]*np.tanh(self.c_Left[t]) 444 | 445 | for c in range(f): 446 | self.gates_pre_Right[t] += np.dot(self.x_rev[t+f-1-c], self.Wxh_Right[c]) 447 | self.gates_pre_Right[t] += + self.bxh_Right 448 | zeros = np.zeros_like(self.gates_pre_Right[t,idx]) 449 | ones = zeros + 1 450 | self.gates_Right[t,idx] = np.maximum(zeros, np.minimum(ones, 0.2*self.gates_pre_Right[t,idx] + 0.5)) 451 | self.gates_Right[t,d:2*d] = np.tanh(self.gates_pre_Right[t,d:2*d]) 452 | self.c_Right[t] = self.gates_Right[t,2*d:3*d]*self.c_Right[t-1] + self.gates_Right[t,0:d]*self.gates_Right[t,d:2*d] 453 | self.h_Right[t] = self.gates_Right[t,3*d:4*d]*np.tanh(self.c_Right[t]) 454 | 455 | self.y_Left = np.dot(self.Why_Left, self.h_Left[T-1]) + self.bhy_Left 456 | self.y_Right = np.dot(self.Why_Right, self.h_Right[T-1]) + self.bhy_Right 457 | self.s = self.y_Left + self.y_Right 458 | 459 | self.pred = np.exp(self.s) / np.sum(np.exp(self.s)) 460 | return self.s.copy() # prediction scores 461 | 462 | def predict(self, w): 463 | self.set_input(w) 464 | self.forward() 465 | return self.pred 466 | 467 | def lrp(self, w, LRP_class, eps=0.001, bias_factor=1.0): 468 | """ 469 | Update the hidden layer relevances by performing LRP for the target class LRP_class 470 | """ 471 | # forward pass 472 | self.set_input(w) 473 | self.forward() 474 | 475 | if self.mode == "dl": 476 | self.ref.set_input(w) 477 | self.ref.x = np.zeros_like(self.x) 478 | self.ref.x_rev = np.zeros_like(self.x) 479 | 480 | self.ref.forward() 481 | 482 | T = self.w.shape[0] 483 | d = int(self.Wxh_Left.shape[-1]/4) 484 | e = self.E.shape[1] 485 | C = self.Why_Left.shape[0] # number of classes 486 | idx = np.hstack((np.arange(0,d), np.arange(2*d,4*d))).astype(int) 487 | f = int(self.Wxh_Left.shape[0]) 488 | 489 | # initialize 490 | Rx = np.zeros(self.x.shape) 491 | Rx_rev = np.zeros(self.x.shape) 492 | 493 | Rh_Left = np.zeros((T+1, d)) 494 | Rc_Left = np.zeros((T+1, d)) 495 | Rg_Left = np.zeros((T, d)) # gate g only 496 | Rh_Right = np.zeros((T+1, d)) 497 | Rc_Right = np.zeros((T+1, d)) 498 | Rg_Right = np.zeros((T, d)) # gate g only 499 | 500 | Rout_mask = np.zeros((C)) 501 | Rout_mask[LRP_class] = 1.0 502 | 503 | # format reminder: lrp_linear(hin, w, b, hout, Rout, bias_nb_units, eps, bias_factor) 504 | 505 | s = self.s.copy() 506 | 507 | gates_Left = self.gates_Left.copy() 508 | gates_pre_Left = self.gates_pre_Left.copy() 509 | c_Left = self.c_Left.copy() 510 | h_Left = self.h_Left.copy() 511 | 512 | gates_Right = self.gates_Right.copy() 513 | gates_pre_Right = self.gates_pre_Right.copy() 514 | c_Right = self.c_Right.copy() 515 | h_Right = self.h_Right.copy() 516 | 517 | if self.mode == "dl": 518 | s -= self.ref.s 519 | 520 | gates_Left[:,d:2*d] -= self.ref.gates_Left[:,d:2*d] 521 | gates_pre_Left[:,d:2*d] -= self.ref.gates_pre_Left[:,d:2*d] 522 | c_Left -= self.ref.c_Left 523 | h_Left -= self.ref.h_Left 524 | 525 | gates_Right[:,d:2*d] -= self.ref.gates_Right[:,d:2*d] 526 | gates_pre_Right[:,d:2*d] -= self.ref.gates_pre_Right[:,d:2*d] 527 | c_Right -= self.ref.c_Right 528 | h_Right -= self.ref.h_Right 529 | 530 | 531 | Rh_Left[T-1] = lrp_linear(h_Left[T-1], self.Why_Left.T , np.zeros((C)), s, s*Rout_mask, 2*d, eps, bias_factor, debug=False) 532 | Rh_Right[T-1] = lrp_linear(h_Right[T-1], self.Why_Right.T, np.zeros((C)), s, s*Rout_mask, 2*d, eps, bias_factor, debug=False) 533 | for t in reversed(range(T)): 534 | #Rc_Left[t] += Rh_Left[t] 535 | Rc_Left[t] += lrp_linear(gates_Left[t,3*d:]*np.tanh(c_Left[t]), np.identity(d), np.zeros((d)), h_Left[t], Rh_Left[t], d, eps, bias_factor, debug=False) 536 | 537 | Rc_Left[t-1] = lrp_linear(gates_Left[t,2*d:3*d]*c_Left[t-1], np.identity(d), np.zeros((d)), c_Left[t], Rc_Left[t], 2*d, eps, bias_factor, debug=False) 538 | Rg_Left[t] = lrp_linear(gates_Left[t,0:d]*gates_Left[t,d:2*d], np.identity(d), np.zeros((d)), c_Left[t], Rc_Left[t], 2*d, eps, bias_factor, debug=False) 539 | 540 | #Rc_Right[t] += Rh_Right[t] 541 | Rc_Right[t] += lrp_linear(gates_Right[t,3*d:]*np.tanh(c_Right[t]), np.identity(d), np.zeros((d)), h_Right[t], Rh_Right[t], d, eps, bias_factor, debug=False) 542 | Rc_Right[t-1] = lrp_linear(gates_Right[t,2*d:3*d]*c_Right[t-1], np.identity(d), np.zeros((d)), c_Right[t], Rc_Right[t], 2*d, eps, bias_factor, debug=False) 543 | Rg_Right[t] = lrp_linear(gates_Right[t,0:d]*gates_Right[t,d:2*d], np.identity(d), np.zeros((d)), c_Right[t], Rc_Right[t], 2*d, eps, bias_factor, debug=False) 544 | 545 | for t in range(T): 546 | for c in range(f): 547 | Rx[t+f-1-c] += lrp_linear(self.x[t+f-1-c], self.Wxh_Left[c,:,d:2*d], self.bxh_Left[d:2*d], gates_pre_Left[t,d:2*d], Rg_Left[t], f*e, eps, bias_factor, debug=False) 548 | Rx_rev[t+f-1-c] += lrp_linear(self.x_rev[t+f-1-c], self.Wxh_Right[c,:,d:2*d], self.bxh_Right[d:2*d], gates_pre_Right[t,d:2*d], Rg_Right[t], f*e, eps, bias_factor, debug=False) 549 | 550 | a, b, c = Rx[f-1:], Rx_rev[f-1:][::-1,:], Rh_Left[-1].sum()+Rc_Left[-1].sum()+Rh_Right[-1].sum()+Rc_Right[-1].sum() + Rx[:f-1].sum() + Rx_rev[:f-1][::-1].sum() 551 | 552 | summed = np.sum(a+b, axis = -1) 553 | relpeak = summed.argmax(-1) / len(summed) 554 | return a, b, c 555 | 556 | 557 | class BILSTM_np: 558 | def __init__(self, weights, mode): 559 | for key in weights: 560 | setattr(self, key, weights[key]) 561 | 562 | assert mode in ("dl", "lrp", "dlrec") 563 | 564 | self.mode = mode 565 | if mode == "dl": 566 | self.ref = BILSTM_np(weights, "dlrec") 567 | 568 | def set_input(self, w, delete_pos=None): 569 | """ 570 | Build the numerical input x/x_rev from word sequence indices w (+ initialize hidden layers h, c) 571 | Optionally delete words at positions delete_pos. 572 | """ 573 | T = w.shape[-1] # input word sequence length 574 | d = int(self.Wxh_Left.shape[0]/4) # hidden layer dimension 575 | e = self.E.shape[1] # word embedding dimension 576 | x = np.zeros((T, e)) 577 | x[:,:] = self.E[w,:] 578 | if delete_pos is not None: 579 | x[delete_pos, :] = np.zeros((len(delete_pos), e)) 580 | 581 | self.w = w 582 | self.x = x 583 | self.x_rev = x[::-1,:].copy() 584 | 585 | self.h_Left = np.zeros((T+1, d)) 586 | self.c_Left = np.zeros((T+1, d)) 587 | self.h_Right = np.zeros((T+1, d)) 588 | self.c_Right = np.zeros((T+1, d)) 589 | 590 | 591 | def forward(self): 592 | """ 593 | Update the hidden layer values (using model weights and numerical input x/x_rev previously built from word sequence w) 594 | """ 595 | T = self.x.shape[0] 596 | d = int(self.Wxh_Left.shape[0]/4) 597 | idx = np.hstack((np.arange(0,d), np.arange(2*d,4*d))).astype(int) # indices of the gates i,f,o 598 | 599 | # initialize 600 | self.gates_xh_Left = np.zeros((T, 4*d)) 601 | self.gates_hh_Left = np.zeros((T, 4*d)) 602 | self.gates_pre_Left = np.zeros((T, 4*d)) # gates i, g, f, o pre-activation 603 | self.gates_Left = np.zeros((T, 4*d)) # gates i, g, f, o activation 604 | 605 | self.gates_xh_Right = np.zeros((T, 4*d)) 606 | self.gates_hh_Right = np.zeros((T, 4*d)) 607 | self.gates_pre_Right= np.zeros((T, 4*d)) 608 | self.gates_Right = np.zeros((T, 4*d)) 609 | 610 | for t in range(T): 611 | self.gates_xh_Left[t] = np.dot(self.Wxh_Left, self.x[t]) + self.bxh_Left 612 | self.gates_hh_Left[t] = np.dot(self.Whh_Left, self.h_Left[t-1]) + self.bhh_Left 613 | self.gates_pre_Left[t] = self.gates_xh_Left[t] + self.gates_hh_Left[t] 614 | #self.gates_Left[t,idx] = 1.0/(1.0 + np.exp(- self.gates_pre_Left[t,idx])) 615 | zeros = np.zeros_like(self.gates_pre_Left[t,idx]) 616 | ones = zeros + 1 617 | self.gates_Left[t,idx] = np.maximum(zeros, np.minimum(ones, 0.2*self.gates_pre_Left[t,idx] + 0.5)) 618 | self.gates_Left[t,d:2*d] = np.tanh(self.gates_pre_Left[t,d:2*d]) 619 | self.c_Left[t] = self.gates_Left[t,2*d:3*d]*self.c_Left[t-1] + self.gates_Left[t,0:d]*self.gates_Left[t,d:2*d] 620 | self.h_Left[t] = self.gates_Left[t,3*d:4*d]*np.tanh(self.c_Left[t]) 621 | 622 | self.gates_xh_Right[t] = np.dot(self.Wxh_Right, self.x_rev[t]) + self.bxh_Right 623 | self.gates_hh_Right[t] = np.dot(self.Whh_Right, self.h_Right[t-1]) + self.bhh_Right 624 | self.gates_pre_Right[t] = self.gates_xh_Right[t] + self.gates_hh_Right[t] 625 | #self.gates_Right[t,idx] = 1.0/(1.0 + np.exp(- self.gates_pre_Right[t,idx])) 626 | zeros = np.zeros_like(self.gates_pre_Right[t,idx]) 627 | ones = zeros + 1 628 | self.gates_Right[t,idx] = np.maximum(zeros, np.minimum(ones, 0.2*self.gates_pre_Right[t,idx] + 0.5)) 629 | self.gates_Right[t,d:2*d] = np.tanh(self.gates_pre_Right[t,d:2*d]) 630 | self.c_Right[t] = self.gates_Right[t,2*d:3*d]*self.c_Right[t-1] + self.gates_Right[t,0:d]*self.gates_Right[t,d:2*d] 631 | self.h_Right[t] = self.gates_Right[t,3*d:4*d]*np.tanh(self.c_Right[t]) 632 | 633 | self.y_Left = np.dot(self.Why_Left, self.h_Left[T-1]) + self.bhy_Left 634 | self.y_Right = np.dot(self.Why_Right, self.h_Right[T-1]) + self.bhy_Right 635 | self.s = self.y_Left + self.y_Right 636 | 637 | self.pred = np.exp(self.s) / np.sum(np.exp(self.s)) 638 | return self.s.copy() # prediction scores 639 | 640 | def predict(self, w): 641 | self.set_input(w) 642 | self.forward() 643 | return self.pred 644 | 645 | def lrp(self, w, LRP_class, eps=0.001, bias_factor=1.0): 646 | """ 647 | Update the hidden layer relevances by performing LRP for the target class LRP_class 648 | """ 649 | # forward pass 650 | self.set_input(w) 651 | self.forward() 652 | 653 | if self.mode == "dl": 654 | self.ref.set_input(w) 655 | self.ref.x = np.zeros_like(self.x) 656 | self.ref.x_rev = np.zeros_like(self.x) 657 | 658 | self.ref.forward() 659 | 660 | T = self.w.shape[-1] 661 | d = int(self.Wxh_Left.shape[0]/4) 662 | e = self.E.shape[1] 663 | C = self.Why_Left.shape[0] # number of classes 664 | idx = np.hstack((np.arange(0,d), np.arange(2*d,4*d))).astype(int) 665 | 666 | # initialize 667 | Rx = np.zeros(self.x.shape) 668 | Rx_rev = np.zeros(self.x.shape) 669 | 670 | Rh_Left = np.zeros((T+1, d)) 671 | Rc_Left = np.zeros((T+1, d)) 672 | Rg_Left = np.zeros((T, d)) # gate g only 673 | Rh_Right = np.zeros((T+1, d)) 674 | Rc_Right = np.zeros((T+1, d)) 675 | Rg_Right = np.zeros((T, d)) # gate g only 676 | 677 | Rout_mask = np.zeros((C)) 678 | Rout_mask[LRP_class] = 1.0 679 | 680 | # format reminder: lrp_linear(hin, w, b, hout, Rout, bias_nb_units, eps, bias_factor) 681 | 682 | s = self.s.copy() 683 | 684 | gates_Left = self.gates_Left.copy() 685 | gates_pre_Left = self.gates_pre_Left.copy() 686 | c_Left = self.c_Left.copy() 687 | h_Left = self.h_Left.copy() 688 | 689 | gates_Right = self.gates_Right.copy() 690 | gates_pre_Right = self.gates_pre_Right.copy() 691 | c_Right = self.c_Right.copy() 692 | h_Right = self.h_Right.copy() 693 | 694 | if self.mode == "dl": 695 | s -= self.ref.s 696 | 697 | gates_Left[:,d:2*d] -= self.ref.gates_Left[:,d:2*d] 698 | gates_pre_Left[:,d:2*d] -= self.ref.gates_pre_Left[:,d:2*d] 699 | c_Left -= self.ref.c_Left 700 | h_Left -= self.ref.h_Left 701 | 702 | gates_Right[:,d:2*d] -= self.ref.gates_Right[:,d:2*d] 703 | gates_pre_Right[:,d:2*d] -= self.ref.gates_pre_Right[:,d:2*d] 704 | c_Right -= self.ref.c_Right 705 | h_Right -= self.ref.h_Right 706 | 707 | 708 | Rh_Left[T-1] = lrp_linear(h_Left[T-1], self.Why_Left.T , np.zeros((C)), s, s*Rout_mask, 2*d, eps, bias_factor, debug=False) 709 | Rh_Right[T-1] = lrp_linear(h_Right[T-1], self.Why_Right.T, np.zeros((C)), s, s*Rout_mask, 2*d, eps, bias_factor, debug=False) 710 | for t in reversed(range(T)): 711 | #Rc_Left[t] += Rh_Left[t] 712 | Rc_Left[t] += lrp_linear(gates_Left[t,3*d:]*np.tanh(c_Left[t]), np.identity(d), np.zeros((d)), h_Left[t], Rh_Left[t], d, eps, bias_factor, debug=False) 713 | 714 | Rc_Left[t-1] = lrp_linear(gates_Left[t,2*d:3*d]*c_Left[t-1], np.identity(d), np.zeros((d)), c_Left[t], Rc_Left[t], 2*d, eps, bias_factor, debug=False) 715 | Rg_Left[t] = lrp_linear(gates_Left[t,0:d]*gates_Left[t,d:2*d], np.identity(d), np.zeros((d)), c_Left[t], Rc_Left[t], 2*d, eps, bias_factor, debug=False) 716 | Rx[t] = lrp_linear(self.x[t], self.Wxh_Left[d:2*d].T, self.bxh_Left[d:2*d]+self.bhh_Left[d:2*d], gates_pre_Left[t,d:2*d], Rg_Left[t], d+e, eps, bias_factor, debug=False) 717 | Rh_Left[t-1] = lrp_linear(h_Left[t-1], self.Whh_Left[d:2*d].T, self.bxh_Left[d:2*d]+self.bhh_Left[d:2*d], gates_pre_Left[t,d:2*d], Rg_Left[t], d+e, eps, bias_factor, debug=False) 718 | 719 | #Rc_Right[t] += Rh_Right[t] 720 | Rc_Right[t] += lrp_linear(gates_Right[t,3*d:]*np.tanh(c_Right[t]), np.identity(d), np.zeros((d)), h_Right[t], Rh_Right[t], d, eps, bias_factor, debug=False) 721 | Rc_Right[t-1] = lrp_linear(gates_Right[t,2*d:3*d]*c_Right[t-1], np.identity(d), np.zeros((d)), c_Right[t], Rc_Right[t], 2*d, eps, bias_factor, debug=False) 722 | Rg_Right[t] = lrp_linear(gates_Right[t,0:d]*gates_Right[t,d:2*d], np.identity(d), np.zeros((d)), c_Right[t], Rc_Right[t], 2*d, eps, bias_factor, debug=False) 723 | Rx_rev[t] = lrp_linear(self.x_rev[t], self.Wxh_Right[d:2*d].T, self.bxh_Right[d:2*d]+self.bhh_Right[d:2*d], gates_pre_Right[t,d:2*d], Rg_Right[t], d+e, eps, bias_factor, debug=False) 724 | Rh_Right[t-1] = lrp_linear(h_Right[t-1], self.Whh_Right[d:2*d].T, self.bxh_Right[d:2*d]+self.bhh_Right[d:2*d], gates_pre_Right[t,d:2*d], Rg_Right[t], d+e, eps, bias_factor, debug=False) 725 | 726 | a, b, c = Rx, Rx_rev[::-1,:], Rh_Left[-1].sum()+Rc_Left[-1].sum()+Rh_Right[-1].sum()+Rc_Right[-1].sum() 727 | 728 | return a, b, c 729 | 730 | 731 | class BIGRU_np: 732 | def __init__(self, weights, mode): 733 | for key in weights: 734 | setattr(self, key, weights[key]) 735 | 736 | assert mode in ("dl", "lrp", "dlrec") 737 | 738 | self.mode = mode 739 | if mode == "dl": 740 | self.ref = BIGRU_np(weights, "dlrec") 741 | 742 | def set_input(self, w, delete_pos=None): 743 | """ 744 | Build the numerical input x/x_rev from word sequence indices w (+ initialize hidden layers h, c) 745 | Optionally delete words at positions delete_pos. 746 | """ 747 | T = w.shape[-1] # input word sequence length 748 | d = int(self.Wxh_Left.shape[0]/3) # hidden layer dimension 749 | e = self.E.shape[1] # word embedding dimension 750 | x = np.zeros((T, e)) 751 | x[:,:] = self.E[w,:] 752 | if delete_pos is not None: 753 | x[delete_pos, :] = np.zeros((len(delete_pos), e)) 754 | 755 | self.w = w 756 | self.x = x 757 | self.x_rev = x[::-1,:].copy() 758 | 759 | self.h_Left = np.zeros((T+1, d)) 760 | self.c_Left = np.zeros((T+1, d)) 761 | self.h_Right = np.zeros((T+1, d)) 762 | self.c_Right = np.zeros((T+1, d)) 763 | 764 | 765 | def forward(self): 766 | """ 767 | Update the hidden layer values (using model weights and numerical input x/x_rev previously built from word sequence w) 768 | """ 769 | T = self.x.shape[0] 770 | d = int(self.Wxh_Left.shape[0]/3) 771 | idx = np.hstack((np.arange(0,d), np.arange(d,2*d))).astype(int) # indices of the gates i,f,o 772 | 773 | # initialize 774 | self.gates_xh_Left = np.zeros((T, 3*d)) 775 | self.gates_pre_Left = np.zeros((T, 3*d)) # gates i, g, f, o pre-activation 776 | self.gates_Left = np.zeros((T, 3*d)) # gates i, g, f, o activation 777 | 778 | self.gates_xh_Right = np.zeros((T, 3*d)) 779 | self.gates_pre_Right= np.zeros((T, 3*d)) 780 | self.gates_Right = np.zeros((T, 3*d)) 781 | 782 | for t in range(T): 783 | 784 | def hard_sigmoid(vec): 785 | zeros = np.zeros_like(vec) 786 | ones = zeros + 1 787 | return np.maximum(zeros, np.minimum(ones, 0.2 * vec + 0.5)) 788 | 789 | self.gates_xh_Left[t] = np.dot(self.Wxh_Left, self.x[t]) + self.bxh_Left 790 | 791 | z_Left = self.gates_xh_Left[t][:d] + np.dot(self.Whh_Left[:d], self.h_Left[t-1]) + self.bhh_Left[:d] 792 | r_Left = self.gates_xh_Left[t][d:2*d] + np.dot(self.Whh_Left[d:2*d], self.h_Left[t-1]) + self.bhh_Left[d:2*d] 793 | 794 | hh_Left = self.gates_xh_Left[t][2*d:] + np.dot(self.Whh_Left[2*d:], hard_sigmoid(r_Left) * self.h_Left[t-1]) + self.bhh_Left[2*d:] 795 | 796 | self.gates_pre_Left[t] = np.concatenate([z_Left, r_Left, hh_Left], axis = -1) 797 | self.gates_Left[t, idx] = hard_sigmoid(self.gates_pre_Left[t, idx]) 798 | self.gates_Left[t, 2*d:] = np.tanh(self.gates_pre_Left[t, 2*d:]) 799 | self.h_Left[t] = self.gates_Left[t,:d]*self.h_Left[t-1]+(1-self.gates_Left[t,:d])*self.gates_Left[t,2*d:] 800 | 801 | 802 | self.gates_xh_Right[t] = np.dot(self.Wxh_Right, self.x_rev[t]) + self.bxh_Right 803 | 804 | z_Right = self.gates_xh_Right[t][:d] + np.dot(self.Whh_Right[:d], self.h_Right[t-1]) + self.bhh_Right[:d] 805 | r_Right = self.gates_xh_Right[t][d:2*d] + np.dot(self.Whh_Right[d:2*d], self.h_Right[t-1]) + self.bhh_Right[d:2*d] 806 | 807 | hh_Right = self.gates_xh_Right[t][2*d:] + np.dot(self.Whh_Right[2*d:], hard_sigmoid(r_Right) * self.h_Right[t-1]) + self.bhh_Right[2*d:] 808 | 809 | self.gates_pre_Right[t] = np.concatenate([z_Right, r_Right, hh_Right], axis = -1) 810 | self.gates_Right[t, idx] = hard_sigmoid(self.gates_pre_Right[t, idx]) 811 | self.gates_Right[t, 2*d:] = np.tanh(self.gates_pre_Right[t, 2*d:]) 812 | self.h_Right[t] = self.gates_Right[t,:d]*self.h_Right[t-1]+(1-self.gates_Right[t,:d])*self.gates_Right[t,2*d:] 813 | 814 | self.y_Left = np.dot(self.Why_Left, self.h_Left[T-1]) + self.bhy_Left 815 | self.y_Right = np.dot(self.Why_Right, self.h_Right[T-1]) + self.bhy_Right 816 | self.s = self.y_Left + self.y_Right 817 | 818 | self.pred = np.exp(self.s) / np.sum(np.exp(self.s)) 819 | return self.s.copy() # prediction scores 820 | 821 | def predict(self, w): 822 | self.set_input(w) 823 | self.forward() 824 | return self.pred 825 | 826 | def lrp(self, w, LRP_class, eps=0.001, bias_factor=1.0): 827 | """ 828 | Update the hidden layer relevances by performing LRP for the target class LRP_class 829 | """ 830 | # forward pass 831 | self.set_input(w) 832 | self.forward() 833 | 834 | if self.mode == "dl": 835 | self.ref.set_input(w) 836 | self.ref.x = np.zeros_like(self.x) 837 | self.ref.x_rev = np.zeros_like(self.x) 838 | 839 | self.ref.forward() 840 | 841 | T = self.w.shape[-1] 842 | d = int(self.Wxh_Left.shape[0]/3) 843 | e = self.E.shape[1] 844 | C = self.Why_Left.shape[0] # number of classes 845 | idx = np.hstack((np.arange(0,d), np.arange(d,2*d))).astype(int) 846 | 847 | # initialize 848 | Rx = np.zeros(self.x.shape) 849 | Rx_rev = np.zeros(self.x.shape) 850 | 851 | Rh_Left = np.zeros((T+1, d)) 852 | Rc_Left = np.zeros((T+1, d)) 853 | Rg_Left = np.zeros((T, d)) # gate g only 854 | Rh_Right = np.zeros((T+1, d)) 855 | Rc_Right = np.zeros((T+1, d)) 856 | Rg_Right = np.zeros((T, d)) # gate g only 857 | 858 | Rout_mask = np.zeros((C)) 859 | Rout_mask[LRP_class] = 1.0 860 | 861 | s = self.s.copy() 862 | 863 | gates_Left = self.gates_Left.copy() 864 | gates_pre_Left = self.gates_pre_Left.copy() 865 | c_Left = self.c_Left.copy() 866 | h_Left = self.h_Left.copy() 867 | 868 | gates_Right = self.gates_Right.copy() 869 | gates_pre_Right = self.gates_pre_Right.copy() 870 | c_Right = self.c_Right.copy() 871 | h_Right = self.h_Right.copy() 872 | 873 | if self.mode == "dl": 874 | s -= self.ref.s 875 | 876 | gates_Left[:,2*d:] -= self.ref.gates_Left[:,2*d:] 877 | gates_pre_Left[:,2*d:] -= self.ref.gates_pre_Left[:,2*d:] 878 | c_Left -= self.ref.c_Left 879 | h_Left -= self.ref.h_Left 880 | 881 | gates_Right[:,2*d:] -= self.ref.gates_Right[:,2*d:] 882 | gates_pre_Right[:,2*d:] -= self.ref.gates_pre_Right[:,2*d:] 883 | c_Right -= self.ref.c_Right 884 | h_Right -= self.ref.h_Right 885 | 886 | # format reminder: lrp_linear(hin, w, b, hout, Rout, bias_nb_units, eps, bias_factor) 887 | Rh_Left[T-1] = lrp_linear(h_Left[T-1], self.Why_Left.T , np.zeros((C)), s, s*Rout_mask, 2*d, eps, bias_factor, debug=False) 888 | Rh_Right[T-1] = lrp_linear(h_Right[T-1], self.Why_Right.T, np.zeros((C)), s, s*Rout_mask, 2*d, eps, bias_factor, debug=False) 889 | 890 | for t in reversed(range(T)): 891 | Rh_Left[t-1] = lrp_linear(gates_Left[t,:d]*h_Left[t-1], np.identity(d), np.zeros((d)), h_Left[t], Rh_Left[t], 2*d, eps, bias_factor, debug=False) 892 | Rg_Left[t] = lrp_linear((1-gates_Left[t,:d])*gates_Left[t,2*d:], np.identity(d), np.zeros((d)), h_Left[t], Rh_Left[t], 2*d, eps, bias_factor, debug=False) 893 | Rx[t] = lrp_linear(self.x[t], self.Wxh_Left[2*d:].T, self.bxh_Left[2*d:]+self.bhh_Left[2*d:], gates_pre_Left[t,2*d:], Rg_Left[t], d+e, eps, bias_factor, debug=False) 894 | Rh_Left[t-1] += lrp_linear(h_Left[t-1] * gates_Left[t,d:2*d], self.Whh_Left[2*d:].T, self.bxh_Left[2*d:]+self.bhh_Left[2*d:], gates_pre_Left[t,2*d:], Rg_Left[t], d+e, eps, bias_factor, debug=False) 895 | 896 | Rh_Right[t-1] = lrp_linear(gates_Right[t,:d]*h_Right[t-1], np.identity(d), np.zeros((d)), h_Right[t], Rh_Right[t], 2*d, eps, bias_factor, debug=False) 897 | Rg_Right[t] = lrp_linear((1-gates_Right[t,:d])*gates_Right[t,2*d:], np.identity(d), np.zeros((d)), h_Right[t], Rh_Right[t], 2*d, eps, bias_factor, debug=False) 898 | Rx_rev[t] = lrp_linear(self.x_rev[t], self.Wxh_Right[2*d:].T, self.bxh_Right[2*d:]+self.bhh_Right[2*d:], gates_pre_Right[t,2*d:], Rg_Right[t], d+e, eps, bias_factor, debug=False) 899 | Rh_Right[t-1] += lrp_linear(h_Right[t-1] * gates_Right[t,d:2*d], self.Whh_Right[2*d:].T, self.bxh_Right[2*d:]+self.bhh_Right[2*d:], gates_pre_Right[t,2*d:], Rg_Right[t], d+e, eps, bias_factor, debug=False) 900 | 901 | a, b, c = Rx, Rx_rev[::-1,:], Rh_Left[-1].sum()+Rc_Left[-1].sum()+Rh_Right[-1].sum()+Rc_Right[-1].sum() 902 | 903 | summed = np.sum(a+b, axis = -1) 904 | relpeak = summed.argmax(-1) / len(summed) 905 | return a, b, c 906 | 907 | 908 | if __name__ == "__main__": 909 | 910 | import _pickle 911 | import sys 912 | X = _pickle.load(open("../Inputs/yelp/X", "rb"))["dev"] 913 | Y = _pickle.load(open("../Inputs/yelp/Y", "rb"))["dev"] 914 | 915 | from keras.models import load_model 916 | from progressbar import ProgressBar 917 | 918 | if sys.argv[1] == "GRU": 919 | model = load_model("../Models/GRU_yelp.hdf5") 920 | weights = keras_to_weights(model, [0,1,2]) 921 | lrp_model = BIGRU_np(weights, mode = "lrp") 922 | elif sys.argv[1] == "LSTM": 923 | model = load_model("../Models/LSTM_yelp.hdf5") 924 | weights = keras_to_weights(model, [0,2,1,3]) 925 | lrp_model = BILSTM_np(weights, mode = "lrp") 926 | elif sys.argv[1] == "QGRU": 927 | model = load_model("../Models/QGRU_yelp.hdf5") 928 | weights = keras_to_weights(model, [0,1]) 929 | lrp_model = BIQGRU_np(weights, mode = "lrp") 930 | elif sys.argv[1] == "QLSTM": 931 | model = load_model("../Models/QLSTM_yelp.hdf5") 932 | weights = keras_to_weights(model, [0,2,1,3]) 933 | lrp_model = BIQLSTM_np(weights, mode = "lrp") 934 | elif sys.argv[1] == "CNN": 935 | model = load_model("../Models/CNN_yelp.hdf5") 936 | weights = keras_to_c2weights(model) 937 | lrp_model = BICNN_np(weights, mode = "lrp") 938 | 939 | corr = 0 940 | bar = ProgressBar() 941 | for x,y in bar(list(zip(X,Y))): 942 | prob = lrp_model.predict(x) 943 | corr += int(prob.argmax() == y) 944 | 945 | print("Accuracy", corr, "/", len(X), "=", corr / len(X)) 946 | --------------------------------------------------------------------------------