package context.arch.intelligibility; import java.util.Map; import context.arch.enactor.Enactor; import context.arch.intelligibility.expression.DNF; import context.arch.intelligibility.expression.Expression; import context.arch.intelligibility.expression.Negated; import context.arch.intelligibility.expression.Parameter; import context.arch.intelligibility.expression.Reason; import context.arch.intelligibility.hmm.HmmExplainer; import context.arch.intelligibility.rules.RulesExplainer; import context.arch.intelligibility.weka.bayes.NaiveBayesExplainer; import context.arch.intelligibility.weka.j48.J48Explainer; import context.arch.storage.Attributes; /** * <p> * Delegate class for Explainers useful for Explainers that generate explanations that are fixed * once the model is non-changing. I.e., all explanations can be pre-generated once the model is * final. If the model is updated (e.g. for dynamic training), then the explanations can be * "refreshed". Explanations are be pre-generated by parsing the original model, and building a * Disjunctive Normal Form expression (as a Disjunction). The model-dependent explanations can * be obtained by reading this data structure. * </p> * <p> * Contrast this with Explainers that can only generate explanations depending on the input states * (e.g. {@link NaiveBayesExplainer}, {@link HmmExplainer}). * </p> * <p> * This is an abstract class that needs to be subclassed to implement a way to build the DNF reasons * for each outcome value. * </p> * * @author Brian Y. Lim * @see RulesExplainer * @see J48Explainer */ public abstract class StaticExplainerDelegate { /** Map of Disjunctions of traces for each class value */ protected Map<String, DNF> reasonsDNF; // made private so that it is not carelessly used prematurely by subclass protected Enactor enactor; protected Explainer explainer; public StaticExplainerDelegate(Enactor enactor) { this.enactor = enactor; // don't set this up yet, as it would lead to a cyclic dependency with constructing the Explainer //this.explainer = enactor.getExplainer(); } /** * Creates the reasons for all outcome values. This would be model-dependent, * and parsed from the underlying model. * Note that this may be called many times, so keep it light-weight. * @param init if true, then it refreshes, and may use heavy-weight parsing to create the reasons; * if false, then it reuses the previously stored reasons * @return map where each key is an outcome value, and each value is the DNF Expression for that outcome value. */ protected Map<String, DNF> getReasonsDNF() { if (reasonsDNF == null) { this.explainer = enactor.getExplainer(); reasonsDNF = initReasonsDNF(); } return reasonsDNF; } /** * Used to init or refresh the reasonsDNF that store explanations in a DNF structure. * @return */ protected abstract Map<String, DNF> initReasonsDNF(); public DNF getWhyExplanation() { String classValue = enactor.getOutcomeValue(); // current class value DNF traces = getReasonsDNF().get(classValue); // get traces applicable to value DNF whyTraces = new DNF(); // get Conjunction of input values (Parameters), i.e. input explanation Reason inputs = explainer.getInputsExplanation(); /* * Iterate through Disjunction (DNF form) for the current class value, * and select Conjunction trace that is satisfied. * There should only be one. */ for (Reason trace : traces) { if (trace.isSatisfiedBy(inputs)) { whyTraces.add(trace); } } return whyTraces; } /** * Scans all traces for those that result in whyNotClassValue. * @return null when whyNotClassValue = actual classValue. Empty collection when there is no solution. */ public DNF getWhyNotExplanations(String altOutcomeValue) { // get traces applicable to alternative value DNF traces = getReasonsDNF().get(altOutcomeValue); DNF whyNotTraces = new DNF(); // get Conjunction of input values (Parameters), i.e. input explanation Reason inputs = explainer.getInputsExplanation(); /* * Iterate through Disjunction (DNF form) * and adding only literal terminals of Conjunctions that are not satisfied, * remembering to negate them to be true */ for (Reason trace : traces) { Reason whyNotTrace = new Reason(); for (Expression child : trace) { // test against each input separately for (Parameter<?> input : inputs) { // if not satisfied, add negated to whyNotTrace if (!child.isSatisfiedBy(input)) { Parameter<?> negated = Negated.negate((Parameter<?>) child); whyNotTrace.add(negated); break; } } } // don't bother adding if empty; but should never have been empty anyway if (!whyNotTrace.isEmpty()) { whyNotTraces.add(whyNotTrace); } } return whyNotTraces; } public DNF getHowToExplanations(String classValue) { DNF traces = getReasonsDNF().get(classValue); // get traces applicable to value return traces; } /** * Constrains How To explanations by pre-conditioning some attribute values. * @param attributes to pre-set as the If condition; it should not have extra Attributes not in the inputs, * but can have "missing" attributes that are to be found by this search * @param classValue * @return DNF with Conjunctions that include all (pre-set and un-set) attributes; Disjunction would be empty if no solution found */ public DNF getHowToIfExplanation(Attributes attributes, String classValue) { Reason whatIfInputs = Reason.fromAttributes(attributes); DNF traces = getReasonsDNF().get(classValue); // get traces applicable to value DNF howToIfTraces = new DNF(); /* * Iterate through Disjunction (DNF form) for the class value, * and add Conjunction traces that are satisfied. */ for (Reason trace : traces) { if (trace.isSatisfiedBy(whatIfInputs)) { // return the first (which should also be the only) howToIfTraces.add(trace); } } return howToIfTraces; } }