package edu.stanford.nlp.sempre; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import fig.basic.*; import java.util.*; /** * A FeatureVector represents a mapping from feature (string) to value * (double). * * We enforce the convention that each feature is (domain, name), * so that the key space isn't a free-for-all. * * @author Percy Liang * @author Jonathan Berant */ public class FeatureVector { public static class Options { @Option(gloss = "When logging, ignore features with zero weight") public boolean ignoreZeroWeight = false; @Option(gloss = "Log only this number of top and bottom features") public int logFeaturesLimit = Integer.MAX_VALUE; } public static Options opts = new Options(); // These features map to the value 1 (most common case in NLP). private ArrayList<String> indicatorFeatures; // General features private ArrayList<Pair<String, Double>> generalFeatures; // A dense array of features to save memory private double[] denseFeatures; private static final String DENSE_NAME = "Dns"; public FeatureVector() { } // constructor that does nothing public FeatureVector(int numOfDenseFeatures) { denseFeatures = new double[numOfDenseFeatures]; Arrays.fill(denseFeatures, 0d); } private static String toFeature(String domain, String name) { return domain + " :: " + name; } public void add(String domain, String name) { add(toFeature(domain, name)); } private void add(String feature) { if (indicatorFeatures == null) indicatorFeatures = new ArrayList<>(); indicatorFeatures.add(feature); } public void add(String domain, String name, double value) { add(toFeature(domain, name), value); } private void add(String feature, double value) { if (generalFeatures == null) generalFeatures = new ArrayList<>(); generalFeatures.add(Pair.newPair(feature, value)); } public void addWithBias(String domain, String name, double value) { add(domain, name, value); add(domain, name + "-bias", 1); } // Add histogram features, e.g., domain :: name>=4 public void addHistogram(String domain, String name, double value) { addHistogram(domain, name, value, 2, 10, true); } public void addHistogram(String domain, String name, double value, int initBinSize, int numBins, boolean exp) { double upper = initBinSize; String bin = null; int sign = value > 0 ? +1 : -1; value = Math.abs(value); for (int i = 0; i < numBins; i++) { double lastUpper = upper; if (i > 0) { if (exp) upper *= initBinSize; else upper += initBinSize; } if (value < upper) { bin = (sign > 0) ? lastUpper + ":" + upper : (-upper) + ":" + (-lastUpper); break; } } if (bin == null) bin = (sign > 0) ? ">=" + upper : "<=" + (-upper); add(domain, name + bin); } public void addFromString(String feature, double value) { assert feature.contains(" :: ") : feature; if (value == 1) add(feature); else add(feature, value); } public void addDenseFeature(int index, double value) { denseFeatures[index] += value; } public void add(FeatureVector that) { add(that, AllFeatureMatcher.matcher); } public void add(double scale, FeatureVector that) { add(scale, that, AllFeatureMatcher.matcher); } public void add(FeatureVector that, FeatureMatcher matcher) { add(1, that, matcher); } public void add(double scale, FeatureVector that, FeatureMatcher matcher) { if (that.indicatorFeatures != null) { for (String f : that.indicatorFeatures) if (matcher.matches(f)) { if (scale == 1) add(f); else add(f, scale); } } if (that.generalFeatures != null) { for (Pair<String, Double> pair : that.generalFeatures) if (matcher.matches(pair.getFirst())) add(pair.getFirst(), scale * pair.getSecond()); } // dense features are always added if (that.denseFeatures != null) { for (int i = 0; i < denseFeatures.length; ++i) denseFeatures[i] += scale * that.denseFeatures[i]; } } // Return the dot product between this feature vector and the weight vector (parameters). public double dotProduct(Params params) { double sum = 0; if (indicatorFeatures != null) { for (String f : indicatorFeatures) sum += params.getWeight(f); } if (generalFeatures != null) { for (Pair<String, Double> pair : generalFeatures) sum += params.getWeight(pair.getFirst()) * pair.getSecond(); } if (denseFeatures != null) { for (int i = 0; i < denseFeatures.length; ++i) sum += params.getWeight(DENSE_NAME + "_" + i) * denseFeatures[i]; } return sum; } // Increment |map| by a factor times this feature vector. // converts the dense features to a non-dense representation public void increment(double factor, Map<String, Double> map) { increment(factor, map, AllFeatureMatcher.matcher); } public void increment(double factor, Map<String, Double> map, FeatureMatcher matcher) { if (indicatorFeatures != null) { for (String feature : indicatorFeatures) if (matcher.matches(feature)) MapUtils.incr(map, feature, factor); } if (generalFeatures != null) { for (Pair<String, Double> pair : generalFeatures) if (matcher.matches(pair.getFirst())) MapUtils.incr(map, pair.getFirst(), factor * pair.getSecond()); } if (denseFeatures != null) { for (int i = 0; i < denseFeatures.length; ++i) MapUtils.incr(map, DENSE_NAME + "_" + i, factor * denseFeatures[i]); } } // returns a feature vector where all features are prefixed public FeatureVector addPrefix(String prefix) { FeatureVector res = new FeatureVector(); if (indicatorFeatures != null) { for (String feature : indicatorFeatures) res.add(prefix + feature); } if (generalFeatures != null) { for (Pair<String, Double> pair : generalFeatures) { res.add(prefix + pair.getFirst(), pair.getSecond()); } } return res; } @JsonValue public Map<String, Double> toMap() { HashMap<String, Double> map = new HashMap<String, Double>(); increment(1, map); if (denseFeatures != null) { for (int i = 0; i < denseFeatures.length; ++i) { map.put(DENSE_NAME + "_" + i, denseFeatures[i]); } } return map; } @JsonCreator public static FeatureVector fromMap(Map<String, Double> m) { // TODO (rf): // Encoding is lossy. We guess that value of 1 means indicator, but we // could be wrong. // TODO(joberant) - takes care of dense features in a non efficient way int maxDenseFeaturesIndex = -1; for (Map.Entry<String, Double> entry : m.entrySet()) { if (isDenseFeature(entry.getKey())) { int index = denseFeatureIndex(entry.getKey()); if (index > maxDenseFeaturesIndex) maxDenseFeaturesIndex = index; } } FeatureVector fv = maxDenseFeaturesIndex == -1 ? new FeatureVector() : new FeatureVector(maxDenseFeaturesIndex + 1); for (Map.Entry<String, Double> entry : m.entrySet()) { if (isDenseFeature(entry.getKey())) { fv.addDenseFeature(denseFeatureIndex(entry.getKey()), entry.getValue()); } else { if (entry.getValue() == 1.0d) fv.add(entry.getKey()); else fv.add(entry.getKey(), entry.getValue()); } } return fv; } private static boolean isDenseFeature(String f) { return f.startsWith(DENSE_NAME); } private static int denseFeatureIndex(String denseFeature) { assert denseFeature.startsWith(DENSE_NAME); return Integer.parseInt(denseFeature.split("_")[1]); } public static void logChoices(String prefix, Map<String, Integer> choices) { LogInfo.begin_track("%s choices", prefix); for (Map.Entry<String, Integer> e : choices.entrySet()) { int value = e.getValue(); if (value == 0) continue; LogInfo.logs("%s %s", value > 0 ? "+" + value : value, e.getKey()); } LogInfo.end_track(); } public static void logFeatureWeights(String prefix, Map<String, Double> features, Params params) { List<Map.Entry<String, Double>> entries = new ArrayList<Map.Entry<String, Double>>(); double sumValue = 0; for (Map.Entry<String, Double> entry : features.entrySet()) { String feature = entry.getKey(); if (entry.getValue() == 0) continue; double value = entry.getValue() * params.getWeight(feature); if (opts.ignoreZeroWeight && value == 0) continue; sumValue += value; entries.add(new java.util.AbstractMap.SimpleEntry<String, Double>(feature, value)); } Collections.sort(entries, new ValueComparator<String, Double>(false)); LogInfo.begin_track_printAll("%s features [sum = %s] (format is feature value * weight)", prefix, Fmt.D(sumValue)); if (entries.size() / 2 > opts.logFeaturesLimit) { for (Map.Entry<String, Double> entry : entries.subList(0, opts.logFeaturesLimit)) { String feature = entry.getKey(); double value = entry.getValue(); double weight = params.getWeight(feature); LogInfo.logs("%-50s %6s = %s * %s", "[ " + feature + " ]", Fmt.D(value), Fmt.D(MapUtils.getDouble(features, feature, 0)), Fmt.D(weight)); } LogInfo.logs("... (%d more features) ...", entries.size() - 2 * opts.logFeaturesLimit); for (Map.Entry<String, Double> entry : entries.subList(entries.size() - opts.logFeaturesLimit, entries.size())) { String feature = entry.getKey(); double value = entry.getValue(); double weight = params.getWeight(feature); LogInfo.logs("%-50s %6s = %s * %s", "[ " + feature + " ]", Fmt.D(value), Fmt.D(MapUtils.getDouble(features, feature, 0)), Fmt.D(weight)); } } else { for (Map.Entry<String, Double> entry : entries) { String feature = entry.getKey(); double value = entry.getValue(); double weight = params.getWeight(feature); LogInfo.logs("%-50s %6s = %s * %s", "[ " + feature + " ]", Fmt.D(value), Fmt.D(MapUtils.getDouble(features, feature, 0)), Fmt.D(weight)); } } LogInfo.end_track(); } public static void logFeatures(Map<String, Double> features) { for (String key : features.keySet()) { LogInfo.logs("%s\t%s", key, features.get(key)); } } public void clear() { if (indicatorFeatures != null) indicatorFeatures.clear(); if (generalFeatures != null) generalFeatures.clear(); denseFeatures = null; } }