package edu.stanford.nlp.sempre.interactive; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import com.beust.jcommander.internal.Lists; import com.google.common.base.Function; import edu.stanford.nlp.sempre.ActionFormula; import edu.stanford.nlp.sempre.ConstantFn; import edu.stanford.nlp.sempre.Derivation; import edu.stanford.nlp.sempre.Example; import edu.stanford.nlp.sempre.Formula; import edu.stanford.nlp.sempre.Formulas; import edu.stanford.nlp.sempre.IdentityFn; import edu.stanford.nlp.sempre.LambdaFormula; import edu.stanford.nlp.sempre.Rule; import edu.stanford.nlp.sempre.SemanticFn; import edu.stanford.nlp.sempre.VariableFormula; import fig.basic.LispTree; import fig.basic.LogInfo; import fig.basic.Option; /** * Takes two examples, and induce Rules * * @author sidaw */ public class GrammarInducer { public static class Options { @Option(gloss = "categories that can serve as rules") public Set<String> filteredCats = new HashSet<String>(); @Option(gloss = "verbose") public int verbose = 0; @Option(gloss = "cats that never overlaps, and always save to replace") public List<String> simpleCats = Lists.newArrayList("$Color", "$Number", "$Direction"); @Option(gloss = "use best packing") public boolean useBestPacking = true; @Option(gloss = "use simple packing") public boolean useSimplePacking = true; @Option(gloss = "maximum nonterminals in a rule") public long maxNonterminals = 4; } public static Options opts = new Options(); private List<Rule> inducedRules = null; List<String> headTokens; String id; public List<Derivation> matches; Derivation def; // induce rule is possible, // otherwise set the correct status public GrammarInducer(List<String> headTokens, Derivation def, List<Derivation> chartList) { // grammarInfo start and end is used to indicate partial, when using aligner boolean allHead = false; if (def.grammarInfo.start == -1) { def.grammarInfo.start = 0; def.grammarInfo.end = headTokens.size(); allHead = true; } // dont want weird cat unary rules with strange semantics if (headTokens == null || headTokens.isEmpty()) { throw new RuntimeException("The head is empty, refusing to define."); } chartList.removeIf(d -> d.start == def.grammarInfo.start && d.end == def.grammarInfo.end); this.def = def; this.headTokens = headTokens; int numTokens = headTokens.size(); this.matches = new ArrayList<>(); addMatches(def, makeChartMap(chartList)); Collections.reverse(this.matches); inducedRules = new ArrayList<>(); if (allHead && opts.useSimplePacking) { List<Derivation> filteredMatches = this.matches.stream().filter(d -> { return opts.simpleCats.contains(d.cat) && d.allAnchored() && d.end - d.start == 1; }).collect(Collectors.toList()); List<Derivation> packing = new ArrayList<>(); for (int i = 0; i <= headTokens.size(); i++) { for (Derivation d : filteredMatches) { if (d.start == i) { packing.add(d); break; } } } HashMap<String, String> formulaToCat = new HashMap<>(); packing.forEach(d -> formulaToCat.put(catFormulaKey(d), varName(d))); buildFormula(def, formulaToCat); List<Rule> simpleInduced = induceRules(packing, def); for (Rule rule : simpleInduced) { rule.addInfo("simple_packing", 1.0); filterRule(rule); } if (opts.verbose > 1) { LogInfo.logs("Simple Packing", chartList.size()); LogInfo.logs("chartList.size = %d", chartList.size()); LogInfo.log("Potential packings: "); this.matches.forEach(d -> LogInfo.logs("%f: %s\t %s", d.getScore(), d.formula, d.allAnchored())); LogInfo.logs("packing: %s", packing); LogInfo.logs("formulaToCat: %s", formulaToCat); } } if (opts.useBestPacking) { List<Derivation> bestPacking = bestPackingDP(this.matches, numTokens); HashMap<String, String> formulaToCat = new HashMap<>(); bestPacking.forEach(d -> formulaToCat.put(catFormulaKey(d), varName(d))); buildFormula(def, formulaToCat); for (Rule rule : induceRules(bestPacking, def)) { if (rule.rhs.stream().allMatch(s -> Rule.isCat(s))) continue; filterRule(rule); } if (opts.verbose > 1) { LogInfo.logs("chartList.size = %d", chartList.size()); LogInfo.log("Potential packings: "); this.matches.forEach(d -> LogInfo.logs("%f: %s\t", d.getScore(), d.formula)); LogInfo.logs("BestPacking: %s", bestPacking); LogInfo.logs("formulaToCat: %s", formulaToCat); } } } Set<String> RHSs = new HashSet<>(); private void filterRule(Rule rule) { if (rule.isCatUnary()) { LogInfo.logs("GrammarInducer.filterRule: not allowing CatUnary rules %s", rule.toString()); return; } if (RHSs.contains(rule.rhs.toString())) { LogInfo.logs("GrammarInducer.filterRule: already have %s", rule.toString()); return; } int numNT = 0; for (String t : rule.rhs) { if (Rule.isCat(t)) numNT++; } if (numNT > GrammarInducer.opts.maxNonterminals ) { LogInfo.logs("GrammarInducer.filterRule: too many nontermnimals (max %d) %s", GrammarInducer.opts.maxNonterminals, rule.rhs.toString()); return; } inducedRules.add(rule); RHSs.add(rule.rhs.toString()); } static Map<String, List<Derivation>> makeChartMap(List<Derivation> chartList) { Map<String, List<Derivation>> chartMap = new HashMap<>(); for (Derivation d : chartList) { List<Derivation> derivs = chartMap.get(catFormulaKey(d)); derivs = derivs != null ? derivs : new ArrayList<>(); derivs.add(d); chartMap.put(catFormulaKey(d), derivs); } return chartMap; } // this is used to test for matches, same cat, same formula // maybe cat needs to be more flexible static String catFormulaKey(Derivation d) { // return d.formula.toString(); return getNormalCat(d) + "::" + d.formula.toString(); } private String varName(Derivation anchored) { int s = def.grammarInfo.start; return getNormalCat(anchored) + (anchored.start - s) + "_" + (anchored.end - s); } static private String getNormalCat(Derivation def) { // return def.cat; String cat = def.getCat(); if (cat.endsWith("s")) return cat.substring(0, cat.length() - 1); else return cat; } // label the derivation tree with what it matches in chartList private void addMatches(Derivation deriv, Map<String, List<Derivation>> chartMap) { String key = catFormulaKey(deriv); if (chartMap.containsKey(key)) { deriv.grammarInfo.matches.addAll(chartMap.get(key)); deriv.grammarInfo.matched = true; matches.addAll(chartMap.get(key)); } for (Derivation d : deriv.children) { addMatches(d, chartMap); } } class Packing { List<Derivation> packing; double score; public Packing(double score, List<Derivation> packing) { this.score = score; this.packing = packing; } @Override public String toString() { return this.score + ": " + this.packing.toString(); } } // the maximum starting index of every match that ends on or before end private int blockingIndex(List<Derivation> matches, int end) { return matches.stream().filter(d -> d.end <= end).map(d -> d.start).max((s1, s2) -> s1.compareTo(s2)) .orElse(Integer.MAX_VALUE / 2); } // start inclusive, end exclusive private List<Derivation> bestPackingDP(List<Derivation> matches, int length) { List<Packing> bestEndsAtI = new ArrayList<>(length + 1); List<Packing> maximalAtI = new ArrayList<>(length + 1); bestEndsAtI.add(new Packing(Double.NEGATIVE_INFINITY, new ArrayList<Derivation>())); maximalAtI.add(new Packing(0.0, new ArrayList<Derivation>())); @SuppressWarnings("unchecked") List<Derivation>[] endsAtI = new ArrayList[length + 1]; for (Derivation d : matches) { List<Derivation> derivs = endsAtI[d.end]; derivs = derivs != null ? derivs : new ArrayList<>(); derivs.add(d); endsAtI[d.end] = derivs; } for (int i = 1; i <= length; i++) { // the new maximal either uses a derivation that ends at i, plus a // previous maximal Packing bestOverall = new Packing(Double.NEGATIVE_INFINITY, new ArrayList<>()); Derivation bestDerivI = null; if (endsAtI[i] != null) { for (Derivation d : endsAtI[i]) { double score = d.getScore() + maximalAtI.get(d.start).score; if (score >= bestOverall.score) { bestOverall.score = score; bestDerivI = d; } } List<Derivation> bestpacking = new ArrayList<>(maximalAtI.get(bestDerivI.start).packing); bestpacking.add(bestDerivI); bestOverall.packing = bestpacking; } bestEndsAtI.add(i, bestOverall); // or it's a previous bestEndsAtI[j] for i-minLength+1 <= j < i for (int j = blockingIndex(matches, i) + 1; j < i; j++) { // LogInfo.dbgs("BlockingIndex: %d, j=%d, i=%d", blockingIndex(matches, // i), j, i); if (bestEndsAtI.get(j).score >= bestOverall.score) bestOverall = bestEndsAtI.get(j); } if (opts.verbose > 1) LogInfo.logs("maximalAtI[%d] = %f: %s, BlockingIndex: %d", i, bestOverall.score, bestOverall.packing, blockingIndex(matches, i)); if (bestOverall.score > Double.NEGATIVE_INFINITY) maximalAtI.add(i, bestOverall); else { maximalAtI.add(i, new Packing(0, new ArrayList<>())); } } return maximalAtI.get(length).packing; } public List<Rule> getRules() { return inducedRules; } private List<Rule> induceRules(List<Derivation> packings, Derivation defDeriv) { List<String> RHS = getRHS(defDeriv, packings); SemanticFn sem = getSemantics(defDeriv, packings); String cat = getNormalCat(defDeriv); Rule inducedRule = new Rule(cat, RHS, sem); inducedRule.addInfo("induced", 1.0); inducedRule.addInfo("anchored", 1.0); List<Rule> inducedRules = new ArrayList<>(); if (!inducedRule.isCatUnary()) { inducedRules.add(inducedRule); } return inducedRules; } // populate grammarInfo.formula, replacing everything that can be replaced private void buildFormula(Derivation deriv, Map<String, String> replaceMap) { // LogInfo.logs("BUILDING %s at (%d,%d) %s", deriv, deriv.start, deriv.end, // catFormulaKey(deriv)); if (replaceMap.containsKey(catFormulaKey(deriv))) { // LogInfo.logs("Found match %s, %s, %s", catFormulaKey(deriv), // replaceMap, deriv); deriv.grammarInfo.formula = new VariableFormula(replaceMap.get(catFormulaKey(deriv))); return; } if (deriv.children.size() == 0) { deriv.grammarInfo.formula = deriv.formula; } for (Derivation c : deriv.children) { buildFormula(c, replaceMap); // deriv.grammarInfo.start = Math.min(deriv.grammarInfo.start, // c.grammarInfo.start); // deriv.grammarInfo.end = Math.max(deriv.grammarInfo.end, // c.grammarInfo.end); } Rule rule = deriv.rule; List<Derivation> args = deriv.children; // cant use the standard DerivationStream because formula is final if (rule == null || rule.sem == null) { deriv.grammarInfo.formula = deriv.formula; } else if (rule.sem instanceof ApplyFn) { Formula f = Formulas.fromLispTree(((ApplyFn) rule.sem).formula.toLispTree()); for (Derivation arg : args) { if (!(f instanceof LambdaFormula)) throw new RuntimeException("Expected LambdaFormula, but got " + f); Formula after = renameBoundVars(f, new HashSet<>()); // LogInfo.logs("renameBoundVar %s === %s", after, f); f = Formulas.lambdaApply((LambdaFormula) after, arg.grammarInfo.formula); } deriv.grammarInfo.formula = f; } else if (rule.sem instanceof IdentityFn) { deriv.grammarInfo.formula = args.get(0).grammarInfo.formula; } else if (rule.sem instanceof BlockFn) { deriv.grammarInfo.formula = new ActionFormula(((BlockFn) rule.sem).mode, args.stream().map(d -> d.grammarInfo.formula).collect(Collectors.toList())); } else { deriv.grammarInfo.formula = deriv.formula; } // LogInfo.logs("BUILT %s for %s", deriv.grammarInfo.formula, // deriv.formula); // LogInfo.log("built " + deriv.grammarInfo.formula); } private String newName(String s) { return s.endsWith("_") ? s : s + "_"; } private Formula renameBoundVars(Formula formula, Set<String> boundvars) { if (formula instanceof LambdaFormula) { LambdaFormula f = (LambdaFormula) formula; boundvars.add(f.var); return new LambdaFormula(newName(f.var), renameBoundVars(f.body, boundvars)); } else { Formula after = formula.map(new Function<Formula, Formula>() { @Override public Formula apply(Formula formula) { if (formula instanceof VariableFormula) { // Replace variable String name = ((VariableFormula) formula).name; if (boundvars.contains(name)) return new VariableFormula(newName(name)); else return formula; } return null; } }); return after; } } private SemanticFn getSemantics(final Derivation def, List<Derivation> packings) { Formula baseFormula = def.grammarInfo.formula; if (opts.verbose > 0) LogInfo.logs("getSemantics %s", baseFormula); if (packings.size() == 0) { SemanticFn constantFn = new ConstantFn(); LispTree newTree = LispTree.proto.newList(); newTree.addChild("ConstantFn"); newTree.addChild(baseFormula.toLispTree()); constantFn.init(newTree); return constantFn; } for (int i = packings.size() - 1; i >= 0; i--) { baseFormula = new LambdaFormula(varName(packings.get(i)), Formulas.fromLispTree(baseFormula.toLispTree())); } SemanticFn applyFn = new ApplyFn(); LispTree newTree = LispTree.proto.newList(); newTree.addChild("interactive.ApplyFn"); newTree.addChild(baseFormula.toLispTree()); applyFn.init(newTree); return applyFn; } private List<String> getRHS(Derivation def, List<Derivation> packings) { List<String> rhs = new ArrayList<>(headTokens); for (Derivation deriv : packings) { // LogInfo.logs("got (%d,%d):%s:%s", deriv.start, deriv.end, // deriv.formula, deriv.cat); rhs.set(deriv.start, getNormalCat(deriv)); for (int i = deriv.start + 1; i < deriv.end; i++) { rhs.set(i, null); } } return rhs.subList(def.grammarInfo.start, def.grammarInfo.end).stream().filter(s -> s != null) .collect(Collectors.toList()); } public static enum ParseStatus { Nothing, // nothing at all parses in the utterance /// Float, // something parse, no longer used. Induced, // redefining known utterance Core; public static ParseStatus fromString(String status) { for (ParseStatus c : ParseStatus.values()) if (c.name().equalsIgnoreCase(status)) return c; return null; } // define known utterance in core, should reject } public static ParseStatus getParseStatus(Example ex) { return getParseStatus(ex.predDerivations); } public static ParseStatus getParseStatus(List<Derivation> derivs) { if (derivs.size() > 0) { for (Derivation deriv : derivs) { if (deriv.allAnchored()) { return ParseStatus.Core; } } return ParseStatus.Induced; } // could check the chart here set partial, but no need for now return ParseStatus.Nothing; } }