package edu.stanford.nlp.sempre; import com.google.common.base.Joiner; import com.google.common.collect.Sets; import fig.basic.*; import java.util.*; /** * A FeatureExtractor specifies a mapping from derivations to feature vectors. * * [Using features] * - Specify the feature domains in the featureDomains option. * - If the features are defined in a separate FeatureComputer class, also specify * the class name in the featureComputers option. * * [Implementing new features] There are 3 ways to implement new features: * 1) Define features in SemanticFn, which is called when the SemanticFn is called. * 2) Create a FeatureComputer class, which is called on each sub-Derivation if the class name * is specified in the featureComputers option. * 3) Add a method to this class. The method is called on each sub-Derivation. * * @author Percy Liang */ public class FeatureExtractor { // Global place to specify features. public static class Options { @Option(gloss = "Set of feature domains to include") public Set<String> featureDomains = new HashSet<>(); @Option(gloss = "Set of feature computer classes to load") public Set<String> featureComputers = Sets.newHashSet("DerivOpCountFeatureComputer"); @Option(gloss = "Disable denotation features") public boolean disableDenotationFeatures = false; @Option(gloss = "Use all possible features, regardless of what featureDomains says") public boolean useAllFeatures = false; @Option(gloss = "For bigram features in paraphrased utterances, maximum distance to consider") public int maxBigramDistance = 3; @Option(gloss = "Whether or not paraphrasing and bigram features should be lexicalized") public boolean lexicalBigramParaphrase = true; } private Executor executor; private List<FeatureComputer> featureComputers = new ArrayList<>(); public FeatureExtractor(Executor executor) { this.executor = executor; for (String featureComputer : opts.featureComputers) { featureComputers.add((FeatureComputer) Utils.newInstanceHard(SempreUtils.resolveClassName(featureComputer))); } } public static Options opts = new Options(); public static boolean containsDomain(String domain) { if (opts.disableDenotationFeatures && domain.equals("denotation")) return false; return opts.useAllFeatures || opts.featureDomains.contains(domain); } // This function is called on every sub-Derivation, so we should extract only // features which depend in some way on |deriv|, not just on its children. public void extractLocal(Example ex, Derivation deriv) { StopWatchSet.begin("FeatureExtractor.extractLocal"); extractRuleFeatures(ex, deriv); extractSpanFeatures(ex, deriv); extractDenotationFeatures(ex, deriv); extractDependencyFeatures(ex, deriv); extractWhTypeFeatures(ex, deriv); conjoinLemmaAndBinary(ex, deriv); extractBigramFeatures(ex, deriv); for (FeatureComputer featureComputer : featureComputers) featureComputer.extractLocal(ex, deriv); StopWatchSet.end(); } // Add an indicator for each applied rule. void extractRuleFeatures(Example ex, Derivation deriv) { if (!containsDomain("rule")) return; if (deriv.rule != Rule.nullRule) { deriv.addFeature("rule", "fire"); deriv.addFeature("rule", deriv.rule.toString()); } } // Extract features on the linguistic information of the spanned (anchored) tokens. // (Not applicable for floating rules) void extractSpanFeatures(Example ex, Derivation deriv) { if (!containsDomain("span") || deriv.start == -1) return; deriv.addFeature("span", "cat=" + deriv.cat + ",#tokens=" + (deriv.end - deriv.start)); deriv.addFeature("span", "cat=" + deriv.cat + ",POS=" + ex.posTag(deriv.start) + "..." + ex.posTag(deriv.end - 1)); } // Extract features on the denotation of the logical form produced. // (For example, number of items in the list) void extractDenotationFeatures(Example ex, Derivation deriv) { if (!containsDomain("denotation")) return; if (!deriv.isRoot(ex.numTokens())) return; deriv.ensureExecuted(executor, ex.context); if (deriv.value instanceof ErrorValue) { deriv.addFeature("denotation", "error"); return; } if (deriv.value instanceof StringValue) { if (((StringValue) deriv.value).value.equals("[]") || ((StringValue) deriv.value).value.equals("[null]")) deriv.addFeature("denotation", "empty"); return; } if (deriv.value instanceof ListValue) { ListValue list = (ListValue) deriv.value; if (list.values.size() == 1 && list.values.get(0) instanceof NumberValue) { int count = getNumber(list.values.get(0)); deriv.addFeature("denotation", "count-size" + (count <= 1 ? "=" + count : ">1")); } else { int size = list.values.size(); deriv.addFeature("denotation", "size" + (size < 3 ? "=" + size : ">=" + 3)); } } } int getNumber(Value value) { if (value instanceof NumberValue) return (int) ((NumberValue) value).value; if (value instanceof ListValue) return getNumber(((ListValue) value).values.get(0)); throw new RuntimeException("Can't extract number from " + value); } // Add an indicator for each alignment between a syntactic dependency (produced by the // Stanford dependency parser) and the application of a semantic function. void extractDependencyFeatures(Example ex, Derivation deriv) { if (!containsDomain("dependencyParse") && !containsDomain("fullDependencyParse")) return; if (deriv.rule != Rule.nullRule) { for (Derivation child : deriv.children) { for (int i = child.start; i < child.end; i++) { for (LanguageInfo.DependencyEdge dependency : ex.languageInfo.dependencyChildren.get(i)) { if (!child.containsIndex(dependency.modifier)) { String direction = dependency.modifier > i ? "forward" : "backward"; String containment = deriv.containsIndex(dependency.modifier) ? "internal" : "external"; if (containsDomain("fullDependencyParse")) addAllDependencyFeatures(dependency, direction, containment, deriv); else deriv.addFeature("dependencyParse", "(" + dependency.label + " " + direction + " " + containment + ") --- " + deriv.getRule().toString()); } } } } } } private void addAllDependencyFeatures(LanguageInfo.DependencyEdge dependency, String direction, String containment, Derivation deriv) { String[] types = {dependency.label, "*"}; String[] directions = {" " + direction, ""}; String[] containments = {" " + containment, ""}; String[] rules = {deriv.getRule().toString(), ""}; for (String typePresent : types) { for (String directionPresent : directions) { for (String containmentPresent : containments) { for (String rulePresent : rules) { deriv.addFeature("fullDependencyParse", "(" + typePresent + directionPresent + containmentPresent + ") --- " + rulePresent); } } } } } // Conjunction of wh-question word and type // (For example, "who" should go with PERSON and not DATE) void extractWhTypeFeatures(Example ex, Derivation deriv) { if (!containsDomain("whType")) return; if (!deriv.isRoot(ex.numTokens())) return; if (ex.posTag(0).startsWith("W")) { deriv.addFeature("whType", "token0=" + ex.token(0) + "," + "type=" + coarseType(deriv.type.toString())); } } public static final String PERSON = "fb:people.person"; public static final String LOC = "fb:location.location"; public static final String ORG = "fb:organization.organization"; public static String coarseType(String type) { Set<String> superTypes = SemTypeHierarchy.singleton.getSupertypes(type); if (superTypes != null) { if (superTypes.contains(PERSON)) return PERSON; if (superTypes.contains(LOC)) return LOC; if (superTypes.contains(ORG)) return ORG; if (superTypes.contains(CanonicalNames.NUMBER)) return CanonicalNames.NUMBER; if (superTypes.contains(CanonicalNames.DATE)) return CanonicalNames.DATE; } return "OTHER"; } //used in Berant et al., 2013 and in the RL parser //conjoins all binaries in the logical form with all non-entity lemmas void conjoinLemmaAndBinary(Example ex, Derivation deriv) { if (!containsDomain("lemmaAndBinaries")) return; if (!deriv.isRoot(ex.numTokens())) return; List<String> nonEntityLemmas = new LinkedList<>(); extractNonEntityLemmas(ex, deriv, nonEntityLemmas); List<String> binaries = extractBinaries(deriv.formula); if (!binaries.isEmpty()) { String binariesStr = Joiner.on('_').join(binaries); for (String nonEntityLemma : nonEntityLemmas) { deriv.addFeature("lemmaAndBinaries", "nonEntitylemmas=" + nonEntityLemma + ",binaries=" + binariesStr); } } } // Extract the utterance that the derivation generates (not necessarily the // one in the input utterance). private void extractUtterance(Derivation deriv, List<String> utterance) { if (deriv.rule == Rule.nullRule) return; int c = 0; // Index into children for (String item : deriv.rule.rhs) { if (Rule.isCat(item)) extractUtterance(deriv.children.get(c++), utterance); else utterance.add(item); } } //Used in Berant et., EMNLP 2013, and in the agenda RL parser //Extracts all content-word lemmas in the derivation tree not dominated by the category $Entity private void extractNonEntityLemmas(Example ex, Derivation deriv, List<String> nonEntityLemmas) { if (deriv.children.size() == 0) { // base case this means it is a word that should be appended for (int i = deriv.start; i < deriv.end; i++) { String pos = ex.languageInfo.posTags.get(i); if ((pos.startsWith("N") || pos.startsWith("V") || pos.startsWith("W") || pos.startsWith("A") || pos.equals("IN")) && !ex.languageInfo.lemmaTokens.get(i).equals("be")) nonEntityLemmas.add(ex.languageInfo.lemmaTokens.get(i)); } } else { // recursion for (Derivation child : deriv.children) { if (child.rule.lhs == null || !child.rule.lhs.equals("$Entity")) { extractNonEntityLemmas(ex, child, nonEntityLemmas); } else if (child.rule.lhs.equals("$Entity")) { nonEntityLemmas.add("E"); } } } } //Used in Berant et al., 2013 and in agenda-based RL parser private List<String> extractBinaries(Formula formula) { List<String> res = new LinkedList<>(); Set<String> atomicElements = Formulas.extractAtomicFreebaseElements(formula); for (String atomicElement : atomicElements) { if (atomicElement.split("\\.").length == 3 && !atomicElement.equals("fb:type.object.type")) res.add(atomicElement); } return res; } /** * Add an indicator for each pair of bigrams that can be aligned from the original * utterance and two (not necessarily contiguous) lemmas in the generated utterance */ private void extractBigramFeatures(Example ex, Derivation deriv) { if (!containsDomain("bigram")) return; if (!deriv.cat.equals(Rule.rootCat)) return; LanguageInfo derivInfo = LanguageAnalyzer.getSingleton().analyze(deriv.canonicalUtterance); List<String> derivLemmas = derivInfo.lemmaTokens; List<String> exLemmas = ex.languageInfo.lemmaTokens; Map<Integer, Integer> bigramCounts = new HashMap<Integer, Integer>(); for (int i = 0; i < exLemmas.size() - 1; i++) { for (int j = 0; j < derivLemmas.size() - 1; j++) { if (derivLemmas.get(j).equals(exLemmas.get(i))) { // Consider bigrams separated by up to maxBigramDistance in generated utterance for (int k = 1; j + k < derivLemmas.size() && k <= opts.maxBigramDistance; k++) { if (derivLemmas.get(j + k).equals(exLemmas.get(i + 1))) { if (opts.lexicalBigramParaphrase) deriv.addFeature("bigram", exLemmas.get(i) + "," + exLemmas.get(i + 1) + " - " + k); else MapUtils.incr(bigramCounts, k, 1); } } } } } if (!opts.lexicalBigramParaphrase) { for (Integer dist : bigramCounts.keySet()) deriv.addFeature("bigram", "distance " + dist + " - " + bigramCounts.get(dist)); } } // Joins arrayList of strings into string String join(List<String> l, String delimiter) { StringBuilder sb = new StringBuilder(l.get(0)); for (int i = 1; i < l.size(); i++) { sb.append(delimiter); sb.append(l.get(i)); } return sb.toString(); } }