package edu.stanford.nlp.sempre;
import com.google.common.base.Joiner;
import com.google.common.collect.Maps;
import fig.basic.*;
import fig.exec.Execution;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* The main learning loop. Goes over a dataset multiple times, calling the
* parser and updating parameters.
*
* @author Percy Liang
*/
public class Learner {
public static class Options {
@Option(gloss = "Number of iterations to train")
public int maxTrainIters = 0;
@Option(gloss = "When using mini-batch updates for SGD, this is the batch size")
public int batchSize = 1; // Default is SGD
@Option(gloss = "Write predDerivations to examples file (huge)")
public boolean outputPredDerivations = false;
@Option(gloss = "Dump all features and compatibility scores")
public boolean dumpFeaturesAndCompatibility = false;
@Option(gloss = "Whether to add feedback")
public boolean addFeedback = false;
@Option(gloss = "Whether to sort on feedback")
public boolean sortOnFeedback = true;
@Option(gloss = "Verbosity") public int verbose = 0;
@Option(gloss = "Initialize with these parameters")
public List<Pair<String, Double>> initialization;
@Option(gloss = "Whether to update weights")
public boolean updateWeights = true;
@Option(gloss = "Whether to check gradient")
public boolean checkGradient = false;
@Option(gloss = "Whether to skip the 'train' group in the last iteration and non-'train' groups in other iterations")
public boolean skipUnnecessaryGroups = false;
@Option(gloss = "Number of threads to parallelize")
public int numParallelThreads = 1;
}
public static Options opts = new Options();
private Parser parser;
private final Params params;
private final Dataset dataset;
private final PrintWriter eventsOut; // For printing a machine-readable log
private final List<SemanticFn> semFuncsToUpdate;
public Learner(Parser parser, Params params, Dataset dataset) {
this.parser = parser;
this.params = params;
this.dataset = dataset;
this.eventsOut = IOUtils.openOutAppendEasy(Execution.getFile("learner.events"));
if (opts.initialization != null && this.params.isEmpty())
this.params.init(opts.initialization);
// Collect all semantic functions to update.
semFuncsToUpdate = new ArrayList<>();
for (Rule rule : parser.grammar.getRules()) {
SemanticFn currSemFn = rule.getSem();
boolean toAdd = true;
for (SemanticFn semFuncToUpdate : semFuncsToUpdate) {
if (semFuncToUpdate.getClass().equals(currSemFn.getClass())) {
toAdd = false;
break;
}
}
if (toAdd)
semFuncsToUpdate.add(currSemFn);
}
}
public void learn() {
learn(opts.maxTrainIters, Maps.newHashMap());
}
/**
* @param evaluations Evaluations per iteration per group.
*/
public void learn(int numIters, Map<String, List<Evaluation>> evaluations) {
LogInfo.begin_track("Learner.learn()");
// if when we start we have parameters already - need to sort the semantic functions.
if (!params.isEmpty())
sortOnFeedback();
// For each iteration, go through the groups and parse (updating if train).
for (int iter = 0; iter <= numIters; iter++) {
LogInfo.begin_track("Iteration %s/%s", iter, numIters);
Execution.putOutput("iter", iter);
// Averaged over all iterations
// Group -> evaluation for that group.
Map<String, Evaluation> meanEvaluations = Maps.newHashMap();
// Clear
for (String group : dataset.groups())
meanEvaluations.put(group, new Evaluation());
// Test and train
for (String group : dataset.groups()) {
boolean lastIter = (iter == numIters);
boolean updateWeights = opts.updateWeights && group.equals("train") && !lastIter; // Don't train on last iteration
if (opts.skipUnnecessaryGroups) {
if ((group.equals("train") && lastIter) || (!group.equals("train") && !lastIter))
continue;
}
// Allow the parser to change behavior based on current group and iteration
parser.onBeginDataGroup(iter, numIters, group);
Evaluation eval = processExamples(iter, group, dataset.examples(group), updateWeights);
MapUtils.addToList(evaluations, group, eval);
meanEvaluations.get(group).add(eval);
StopWatchSet.logStats();
writeParams(iter);
}
LogInfo.end_track();
}
LogInfo.end_track();
}
private void writeParams(int iter) {
String path = Execution.getFile("params." + iter);
if (path != null) {
params.write(path);
Utils.systemHard("ln -sf params." + iter + " " + Execution.getFile("params"));
}
}
public void onlineLearnExample(Example ex) {
LogInfo.begin_track("onlineLearnExample: %s derivations", ex.predDerivations.size());
HashMap<String, Double> counts = new HashMap<>();
for (Derivation deriv : ex.predDerivations)
deriv.compatibility = parser.valueEvaluator.getCompatibility(ex.targetValue, deriv.value);
ParserState.computeExpectedCounts(ex.predDerivations, counts);
params.update(counts);
LogInfo.end_track();
}
public void onlineLearnExampleByFormula(Example ex, List<Formula> formulas) {
HashMap<String, Double> counts = new HashMap<>();
for (Derivation deriv : ex.predDerivations)
deriv.compatibility = formulas.contains(deriv.formula)? 1 : 0;
ParserState.computeExpectedCounts(ex.predDerivations, counts);
params.update(counts);
}
private Evaluation processExamples(int iter, String group,
List<Example> examples, boolean computeExpectedCounts) {
Evaluation evaluation = new Evaluation();
if (examples.size() == 0)
return evaluation;
final String prefix = "iter=" + iter + "." + group;
Execution.putOutput("group", group);
LogInfo.begin_track_printAll(
"Processing %s: %s examples", prefix, examples.size());
LogInfo.begin_track("Examples");
if (opts.numParallelThreads > 1) {
// Parallelize!
Parallelizer<Example> paral = new Parallelizer<>(opts.numParallelThreads);
LearnerParallelProcessor processor = new LearnerParallelProcessor(
parser, params, prefix, computeExpectedCounts, evaluation);
LogInfo.begin_threads();
paral.process(examples, processor);
LogInfo.end_threads();
} else {
// Original code (single-threaded)
Map<String, Double> counts = new HashMap<>();
int batchSize = 0;
for (int e = 0; e < examples.size(); e++) {
Example ex = examples.get(e);
LogInfo.begin_track_printAll(
"%s: example %s/%s: %s", prefix, e, examples.size(), ex.id);
ex.log();
Execution.putOutput("example", e);
ParserState state = parseExample(params, ex, computeExpectedCounts);
if (computeExpectedCounts) {
if (opts.checkGradient) {
LogInfo.begin_track("Checking gradient");
checkGradient(ex, state);
LogInfo.end_track();
}
SempreUtils.addToDoubleMap(counts, state.expectedCounts);
batchSize++;
if (batchSize >= opts.batchSize) {
// Gathered enough examples, update parameters
updateWeights(counts);
batchSize = 0;
}
}
LogInfo.logs("Current: %s", ex.evaluation.summary());
evaluation.add(ex.evaluation);
LogInfo.logs("Cumulative(%s): %s", prefix, evaluation.summary());
printLearnerEventsIter(ex, iter, group);
LogInfo.end_track();
if (opts.addFeedback && computeExpectedCounts)
addFeedback(ex);
// Write out examples and predictions
if (opts.outputPredDerivations && Builder.opts.parser.equals("FloatingParser")) {
ExampleUtils.writeParaphraseSDF(iter, group, ex, opts.outputPredDerivations);
}
// To save memory
ex.predDerivations.clear();
}
if (computeExpectedCounts && batchSize > 0)
updateWeights(counts);
}
params.finalizeWeights();
if (opts.sortOnFeedback && computeExpectedCounts)
sortOnFeedback();
LogInfo.end_track();
logEvaluationStats(evaluation, prefix);
evaluation.putOutput(prefix.replace('.', '-'));
printLearnerEventsSummary(evaluation, iter, group);
ExampleUtils.writeEvaluationSDF(iter, group, evaluation, examples.size());
LogInfo.end_track();
return evaluation;
}
private void checkGradient(Example ex, ParserState state) {
double eps = 1e-2;
for (String feature : state.expectedCounts.keySet()) {
LogInfo.begin_track("feature=%s", feature);
double computedGradient = state.expectedCounts.get(feature);
Params perturbedParams = this.params.copyParams();
perturbedParams.getWeights().put(feature, perturbedParams.getWeight(feature) + eps);
ParserState perturbedState = parseExample(perturbedParams, ex, true);
double checkedGradient = (perturbedState.objectiveValue - state.objectiveValue) / eps;
LogInfo.logs("Learner.checkGradient(): weight=%s, pertWeight=%s, obj=%s, pertObj=%s, feature=%s, computed=%s, checked=%s, diff=%s",
params.getWeight(feature), perturbedParams.getWeight(feature),
state.objectiveValue, perturbedState.objectiveValue,
feature,
computedGradient, checkedGradient, Math.abs(checkedGradient - computedGradient));
LogInfo.end_track();
}
}
private void sortOnFeedback() {
for (SemanticFn semFn : semFuncsToUpdate) {
semFn.sortOnFeedback(parser.getSearchParams(params));
}
}
private void addFeedback(Example ex) {
for (SemanticFn semFn : semFuncsToUpdate) {
semFn.addFeedback(ex);
}
}
private ParserState parseExample(Params params, Example ex, boolean computeExpectedCounts) {
StopWatchSet.begin("Parser.parse");
ParserState res = this.parser.parse(params, ex, computeExpectedCounts);
StopWatchSet.end();
return res;
}
private void updateWeights(Map<String, Double> counts) {
StopWatchSet.begin("Learner.updateWeights");
LogInfo.begin_track("Updating learner weights");
double sum = 0;
for (double v : counts.values()) sum += v * v;
if (opts.verbose >= 2)
SempreUtils.logMap(counts, "gradient");
LogInfo.logs("L2 norm: %s", Math.sqrt(sum));
params.update(counts);
if (opts.verbose >= 2)
params.log();
counts.clear();
LogInfo.end_track();
StopWatchSet.end();
}
// Print summary over all examples
private void logEvaluationStats(Evaluation evaluation, String prefix) {
LogInfo.logs("Stats for %s: %s", prefix, evaluation.summary());
// evaluation.add(LexiconFn.lexEval);
evaluation.logStats(prefix);
evaluation.putOutput(prefix);
}
private void printLearnerEventsIter(Example ex, int iter, String group) {
if (eventsOut == null)
return;
List<String> fields = new ArrayList<>();
fields.add("iter=" + iter);
fields.add("group=" + group);
fields.add("utterance=" + ex.utterance);
fields.add("targetValue=" + ex.targetValue);
if (ex.predDerivations.size() > 0) {
Derivation deriv = ex.predDerivations.get(0);
fields.add("predValue=" + deriv.value);
fields.add("predFormula=" + deriv.formula);
}
fields.add(ex.evaluation.summary("\t"));
eventsOut.println(Joiner.on('\t').join(fields));
eventsOut.flush();
// Print out features and the compatibility across all the derivations
if (opts.dumpFeaturesAndCompatibility) {
for (Derivation deriv : ex.predDerivations) {
fields = new ArrayList<>();
fields.add("iter=" + iter);
fields.add("group=" + group);
fields.add("utterance=" + ex.utterance);
Map<String, Double> features = new HashMap<>();
deriv.incrementAllFeatureVector(1, features);
for (String f : features.keySet()) {
double v = features.get(f);
fields.add(f + "=" + v);
}
fields.add("comp=" + deriv.compatibility);
eventsOut.println(Joiner.on('\t').join(fields));
}
}
}
private void printLearnerEventsSummary(Evaluation evaluation,
int iter,
String group) {
if (eventsOut == null)
return;
List<String> fields = new ArrayList<>();
fields.add("iter=" + iter);
fields.add("group=" + group);
fields.add(evaluation.summary("\t"));
eventsOut.println(Joiner.on('\t').join(fields));
eventsOut.flush();
}
}