package edu.stanford.nlp.sempre;
import fig.basic.*;
import java.io.PrintWriter;
import java.util.*;
////////////////////////////////////////////////////////////
/**
* A Parser takes an example, parses the sequence of tokens, and stores the
* derivations back in the example. It also computes a gradient with respect
* to some objective function. In this light, Parser it is more than a parser.
*
* @author Percy Liang
*/
public abstract class Parser {
public static class Options {
@Option(gloss = "For debugging, whether to print out all the predicted derivations")
public boolean printAllPredictions;
@Option(gloss = "Maximal number of predictions to print")
public int maxPrintedPredictions = Integer.MAX_VALUE;
@Option(gloss = "Maximal number of correct predictions to print")
public int maxPrintedTrue = Integer.MAX_VALUE;
@Option(gloss = "Use a coarse pass to prune the chart before full parsing")
public boolean coarsePrune = false;
@Option(gloss = "How much output to print")
public int verbose = 0;
@Option(gloss = "Execute only top formula to be cheap (hack at test time for fast demo)")
public boolean executeTopFormulaOnly = false;
@Option(gloss = "Whether to output chart filling visualization (huge file!)")
public boolean visualizeChartFilling = false;
@Option(gloss = "Keep this number of derivations per cell (exact use depends on the parser)")
public int beamSize = 200;
@Option(gloss = "Whether to update based on partial reward (for learning)")
public boolean partialReward = true;
@Option(gloss = "Whether to unroll derivation streams (applies to lazy parsers)")
public boolean unrollStream = false;
@Option(gloss = "Inject random noise into the score (to mix things up a bit)")
public double derivationScoreNoise = 0;
@Option(gloss = "Source of random noise")
public Random derivationScoreRandom = new Random(1);
@Option(gloss = "Prune away error denotations")
public boolean pruneErrorValues = false;
@Option(gloss = "Dump all features (for debugging)")
public boolean dumpAllFeatures = false;
@Option(gloss = "Call SetEvaluation during parsing")
public boolean callSetEvaluation = true;
}
public static final Options opts = new Options();
public boolean verbose(int level) { return opts.verbose >= level; }
// Used to instantiate a parser.
public static class Spec {
public final Grammar grammar;
public final FeatureExtractor extractor;
public final Executor executor;
public final ValueEvaluator valueEvaluator;
public Spec(Grammar grammar, FeatureExtractor extractor, Executor executor, ValueEvaluator valueEvaluator) {
this.grammar = grammar;
this.extractor = extractor;
this.executor = executor;
this.valueEvaluator = valueEvaluator;
}
}
// Inputs to the parser
public final Grammar grammar;
public final FeatureExtractor extractor;
public final Executor executor;
public final ValueEvaluator valueEvaluator;
// Precomputations to make looking up grammar rules faster.
protected List<Rule> catUnaryRules; // Unary rules with category on RHS ($A => $B)
public List<Rule> getCatUnaryRules() { return catUnaryRules; }
// TODO(joberant): move this to a separate class in charge of visualizing charts
public PrintWriter chartFillOut = null; // For printing a machine-readable json file
public Parser(Spec spec) {
this.grammar = spec.grammar;
this.extractor = spec.extractor;
this.executor = spec.executor;
this.valueEvaluator = spec.valueEvaluator;
computeCatUnaryRules();
LogInfo.logs("Parser: %d catUnaryRules (sorted), %d nonCatUnaryRules (in trie)",
catUnaryRules.size(), grammar.rules.size() - catUnaryRules.size());
}
// If grammar changes, then we might need to update aspects of the parser.
public synchronized void addRule(Rule rule) {
if (rule.isCatUnary())
catUnaryRules.add(rule);
}
protected void computeCatUnaryRules() {
// Handle catUnaryRules
catUnaryRules = new ArrayList<>();
Map<String, List<Rule>> graph = new HashMap<>(); // Node from LHS to list of rules
for (Rule rule : grammar.rules)
if (rule.isCatUnary())
MapUtils.addToList(graph, rule.lhs, rule);
// Topologically sort catUnaryRules so that B->C occurs before A->B
Map<String, Boolean> done = new HashMap<>();
for (String node : graph.keySet())
traverse(catUnaryRules, node, graph, done);
}
// Helper function for transitive closure of unary rules.
protected void traverse(List<Rule> catUnaryRules, String node,
Map<String, List<Rule>> graph, Map<String, Boolean> done) {
Boolean d = done.get(node);
if (Boolean.TRUE.equals(d)) return;
if (Boolean.FALSE.equals(d))
throw new RuntimeException("Found cycle of unaries involving " + node);
done.put(node, false);
for (Rule rule : MapUtils.getList(graph, node)) {
traverse(catUnaryRules, rule.rhs.get(0), graph, done);
catUnaryRules.add(rule);
}
done.put(node, true);
}
/**
* Override this method to change the parser's behavior based on current
* group name and iteration number. This method will be called at the
* beginning of each data group.
*/
public void onBeginDataGroup(int iter, int numIters, String group) {
// DEFAULT: Do nothing.
}
// Main thing for parsers to implement.
public abstract ParserState newParserState(Params params, Example ex, boolean computeExpectedCounts);
public Params getSearchParams(Params params) { return params; }
/**
* Parse the given example |ex| using the given parameters |params|
* and populate the fields of |ex| (e.g., predDerivations). Note:
* |ex| is modified in place.
*/
public ParserState parse(Params params, Example ex, boolean computeExpectedCounts) {
// Execute target formula (if applicable).
if (ex.targetFormula != null && ex.targetValue == null)
ex.targetValue = executor.execute(ex.targetFormula, ex.context).value;
// Parse
StopWatch watch = new StopWatch();
watch.start();
LogInfo.begin_track_printAll("Parser.parse: parse");
ParserState state = newParserState(params, ex, computeExpectedCounts);
state.infer();
LogInfo.end_track();
watch.stop();
state.parseTime = watch.getCurrTimeLong();
state.setEvaluation();
ex.predDerivations = state.predDerivations;
Derivation.sortByScore(ex.predDerivations);
// Evaluate
if (opts.callSetEvaluation) {
ex.evaluation = new Evaluation();
addToEvaluation(state, ex.evaluation);
}
// Clean up temporary state used during parsing
ex.clearTempState();
for (Derivation deriv : ex.predDerivations)
deriv.clearTempState();
return state;
}
/**
* Compute the evaluation based on the results of parsing and add it to |evaluation|
*/
public void addToEvaluation(ParserState state, Evaluation evaluation) {
Example ex = state.ex;
List<Derivation> predDerivations = state.predDerivations;
boolean printAllPredictions = opts.printAllPredictions;
int numCandidates = predDerivations.size();
LogInfo.begin_track_printAll("Parser.setEvaluation: %d candidates", numCandidates);
// Each derivation has a compatibility score (in [0, 1]) as well as a model probability.
// Terminology:
// True (correct): compatibility = 1
// Partial: 0 < compatibility < 1
// Wrong: compatibility = 0
// Did we get the answer correct?
int correctIndex = -1; // Index of first correct derivation
int correctIndexAfterParse = -1;
double maxCompatibility = 0.0;
double[] compatibilities = null;
if (ex.targetValue != null) {
compatibilities = new double[numCandidates];
for (int i = 0; i < numCandidates; i++) {
Derivation deriv = predDerivations.get(i);
compatibilities[i] = deriv.compatibility;
// Must be fully compatible to count as correct.
if (compatibilities[i] == 1 && correctIndex == -1)
correctIndex = i;
// record maximum compatibility for partial oracle
maxCompatibility = Math.max(compatibilities[i], maxCompatibility);
}
// What if we only had parsed bottom up?
for (int i = 0; i < numCandidates; i++) {
if (compatibilities[i] == 1) {
correctIndexAfterParse = i;
break;
}
}
}
// Compute probabilities
double[] probs = Derivation.getProbs(predDerivations, 1);
for (int i = 0; i < numCandidates; i++) {
Derivation deriv = predDerivations.get(i);
deriv.prob = probs[i];
}
// Number of derivations which have the same top score
int numTop = 0;
double topMass = 0;
if (ex.targetValue != null) {
while (numTop < numCandidates &&
Math.abs(predDerivations.get(numTop).score - predDerivations.get(0).score) < 1e-10) {
topMass += probs[numTop];
numTop++;
}
}
double correct = 0, partCorrect = 0;
if (ex.targetValue != null) {
for (int i = 0; i < numTop; i++) {
if (compatibilities[i] == 1) correct += probs[i] / topMass;
if (compatibilities[i] > 0)
partCorrect += (compatibilities[i] * probs[i]) / topMass;
}
}
// Print features (note this is only with respect to the first correct, is NOT the gradient).
// Things are not printed if there is only partial compatability.
if (correctIndex != -1 && correct != 1) {
Derivation trueDeriv = predDerivations.get(correctIndex);
Derivation predDeriv = predDerivations.get(0);
HashMap<String, Double> featureDiff = new HashMap<>();
trueDeriv.incrementAllFeatureVector(+1, featureDiff);
predDeriv.incrementAllFeatureVector(-1, featureDiff);
String heading = String.format("TopTrue (%d) - Pred (%d) = Diff", correctIndex, 0);
FeatureVector.logFeatureWeights(heading, featureDiff, state.params);
HashMap<String, Integer> choiceDiff = new LinkedHashMap<>();
trueDeriv.incrementAllChoices(+1, choiceDiff);
predDeriv.incrementAllChoices(-1, choiceDiff);
FeatureVector.logChoices(heading, choiceDiff);
}
// Fully correct
int numPrintedSoFar = 0;
for (int i = 0; i < predDerivations.size(); i++) {
Derivation deriv = predDerivations.get(i);
if (compatibilities != null && compatibilities[i] == 1) {
boolean print = printAllPredictions || (numPrintedSoFar < opts.maxPrintedTrue);
if (print) {
LogInfo.logs(
"True@%04d: %s [score=%s, prob=%s%s]", i, deriv.toString(),
Fmt.D(deriv.score), Fmt.D(probs[i]), compatibilities != null ? ", comp=" + Fmt.D(compatibilities[i]) : "");
numPrintedSoFar++;
if (opts.dumpAllFeatures) FeatureVector.logFeatureWeights("Features", deriv.getAllFeatureVector(), state.params);
}
}
}
// Partially correct
numPrintedSoFar = 0;
for (int i = 0; i < predDerivations.size(); i++) {
Derivation deriv = predDerivations.get(i);
if (compatibilities != null && compatibilities[i] > 0 && compatibilities[i] < 1) {
boolean print = printAllPredictions || (numPrintedSoFar < opts.maxPrintedTrue);
if (print) {
LogInfo.logs(
"Part@%04d: %s [score=%s, prob=%s%s]", i, deriv.toString(),
Fmt.D(deriv.score), Fmt.D(probs[i]), compatibilities != null ? ", comp=" + Fmt.D(compatibilities[i]) : "");
numPrintedSoFar++;
if (opts.dumpAllFeatures) FeatureVector.logFeatureWeights("Features", deriv.getAllFeatureVector(), state.params);
}
}
}
// Anything that's predicted.
for (int i = 0; i < predDerivations.size(); i++) {
Derivation deriv = predDerivations.get(i);
// Either print all predictions or this prediction is worse by some amount.
boolean print = printAllPredictions || ((probs[i] >= probs[0] / 2 || i < 10) && i < opts.maxPrintedPredictions);
if (print) {
LogInfo.logs(
"Pred@%04d: %s [score=%s, prob=%s%s]", i, deriv.toString(),
Fmt.D(deriv.score), Fmt.D(probs[i]), compatibilities != null ? ", comp=" + Fmt.D(compatibilities[i]) : "");
// LogInfo.logs("Derivation tree: %s", deriv.toRecursiveString());
if (opts.dumpAllFeatures) FeatureVector.logFeatureWeights("Features", deriv.getAllFeatureVector(), state.params);
}
}
evaluation.add("correct", correct);
evaluation.add("oracle", correctIndex != -1);
evaluation.add("partCorrect", partCorrect);
evaluation.add("partOracle", maxCompatibility);
if (correctIndexAfterParse != -1)
evaluation.add("correctIndexAfterParse", correctIndexAfterParse);
if (correctIndex != -1) {
evaluation.add("correctMaxBeamPosition", predDerivations.get(correctIndex).maxBeamPosition);
evaluation.add("correctMaxUnsortedBeamPosition", predDerivations.get(correctIndex).maxUnsortedBeamPosition);
}
evaluation.add("parsed", numCandidates > 0);
evaluation.add("numCandidates", numCandidates); // From this parse
if (numCandidates > 0)
evaluation.add("parsedNumCandidates", numCandidates);
// Add parsing stats
evaluation.add(state.evaluation);
// Add executor stats
for (Derivation deriv : predDerivations) {
if (deriv.executorStats != null)
evaluation.add(deriv.executorStats);
}
LogInfo.end_track();
}
}