blob: 0490dcb1b3a1d3c4fe8c6c6ea21f76252d67a8c3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package opennlp.tools.coref.resolver;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
//import opennlp.maxent.GIS;
//import opennlp.maxent.io.SuffixSensitiveGISModelReader;
//import opennlp.maxent.io.SuffixSensitiveGISModelWriter;
//import opennlp.model.EventStream;
//import opennlp.model.MaxentModel;
import opennlp.tools.ml.maxent.GIS;
import opennlp.tools.ml.maxent.io.SuffixSensitiveGISModelWriter;
import opennlp.tools.ml.maxent.io.SuffixSensitiveGISModelReader;
//import opennlp.maxent.GIS;
//import opennlp.maxent.io.SuffixSensitiveGISModelReader;
//import opennlp.maxent.io.SuffixSensitiveGISModelWriter;
//import opennlp.model.Event;
//import opennlp.model.MaxentModel;
import opennlp.tools.ml.model.MaxentModel;
import opennlp.tools.ml.model.EventStream;
import opennlp.tools.coref.DiscourseEntity;
import opennlp.tools.coref.DiscourseModel;
import opennlp.tools.coref.mention.MentionContext;
import opennlp.tools.coref.sim.TestSimilarityModel;
import opennlp.tools.ml.model.Event;
import opennlp.tools.util.CollectionEventStream;
/**
* Provides common functionality used by classes which implement the {@link Resolver} class and use maximum entropy models to make resolution decisions.
*/
public abstract class MaxentResolver extends AbstractResolver {
/** Outcomes when two mentions are coreferent. */
public static final String SAME = "same";
/** Outcome when two mentions are not coreferent. */
public static final String DIFF = "diff";
/** Default feature value. */
public static final String DEFAULT = "default";
private static boolean debugOn=false;
private String modelName;
private MaxentModel model;
private double[] candProbs;
private int sameIndex;
private ResolverMode mode;
private List<opennlp.tools.ml.model.Event> events;
/** When true, this designates that the resolver should use the first referent encountered which it
* more preferable than non-reference. When false all non-excluded referents within this resolvers range
* are considered.
*/
protected boolean preferFirstReferent;
/** When true, this designates that training should consist of a single positive and a single negative example
* (when possible) for each mention. */
protected boolean pairedSampleSelection;
/** When true, this designates that the same maximum entropy model should be used non-reference
* events (the pairing of a mention and the "null" reference) as is used for potentially
* referential pairs. When false a separate model is created for these events.
*/
protected boolean useSameModelForNonRef;
private static TestSimilarityModel simModel = null;
/** The model for computing non-referential probabilities. */
protected NonReferentialResolver nonReferentialResolver;
private static final String modelExtension = ".bin.gz";
/**
* Creates a maximum-entropy-based resolver which will look the specified number of entities back for a referent.
* This constructor is only used for unit testing.
* @param numberOfEntitiesBack
* @param preferFirstReferent
*/
protected MaxentResolver(int numberOfEntitiesBack, boolean preferFirstReferent) {
super(numberOfEntitiesBack);
this.preferFirstReferent = preferFirstReferent;
}
/**
* Creates a maximum-entropy-based resolver with the specified model name, using the
* specified mode, which will look the specified number of entities back for a referent and
* prefer the first referent if specified.
* @param modelDirectory The name of the directory where the resolver models are stored.
* @param name The name of the file where this model will be read or written.
* @param mode The mode this resolver is being using in (training, testing).
* @param numberOfEntitiesBack The number of entities back in the text that this resolver will look
* for a referent.
* @param preferFirstReferent Set to true if the resolver should prefer the first referent which is more
* likely than non-reference. This only affects testing.
* @param nonReferentialResolver Determines how likely it is that this entity is non-referential.
* @throws IOException If the model file is not found or can not be written to.
*/
public MaxentResolver(String modelDirectory, String name, ResolverMode mode, int numberOfEntitiesBack, boolean preferFirstReferent, NonReferentialResolver nonReferentialResolver) throws IOException {
super(numberOfEntitiesBack);
this.preferFirstReferent = preferFirstReferent;
this.nonReferentialResolver = nonReferentialResolver;
this.mode = mode;
this.modelName = modelDirectory+"/"+name;
if (ResolverMode.TEST == this.mode) {
model = (new SuffixSensitiveGISModelReader(new File(modelName+modelExtension))).getModel();
sameIndex = model.getIndex(SAME);
}
else if (ResolverMode.TRAIN == this.mode) {
events = new ArrayList<Event>();
}
else {
System.err.println("Unknown mode: " + this.mode);
}
//add one for non-referent possibility
candProbs = new double[getNumEntities() + 1];
}
/**
* Creates a maximum-entropy-based resolver with the specified model name, using the
* specified mode, which will look the specified number of entities back for a referent.
* @param modelDirectory The name of the directory where the resover models are stored.
* @param modelName The name of the file where this model will be read or written.
* @param mode The mode this resolver is being using in (training, testing).
* @param numberEntitiesBack The number of entities back in the text that this resolver will look
* for a referent.
* @throws IOException If the model file is not found or can not be written to.
*/
public MaxentResolver(String modelDirectory, String modelName, ResolverMode mode, int numberEntitiesBack) throws IOException {
this(modelDirectory, modelName, mode, numberEntitiesBack, false);
}
public MaxentResolver(String modelDirectory, String modelName, ResolverMode mode, int numberEntitiesBack, NonReferentialResolver nonReferentialResolver) throws IOException {
this(modelDirectory, modelName, mode, numberEntitiesBack, false,nonReferentialResolver);
}
public MaxentResolver(String modelDirectory, String modelName, ResolverMode mode, int numberEntitiesBack, boolean preferFirstReferent) throws IOException {
//this(projectName, modelName, mode, numberEntitiesBack, preferFirstReferent, SingletonNonReferentialResolver.getInstance(projectName,mode));
this(modelDirectory, modelName, mode, numberEntitiesBack, preferFirstReferent, new DefaultNonReferentialResolver(modelDirectory, modelName, mode));
}
public MaxentResolver(String modelDirectory, String modelName, ResolverMode mode, int numberEntitiesBack, boolean preferFirstReferent, double nonReferentialProbability) throws IOException {
//this(projectName, modelName, mode, numberEntitiesBack, preferFirstReferent, SingletonNonReferentialResolver.getInstance(projectName,mode));
this(modelDirectory, modelName, mode, numberEntitiesBack, preferFirstReferent, new FixedNonReferentialResolver(nonReferentialProbability));
}
public DiscourseEntity resolve(MentionContext ec, DiscourseModel dm) {
DiscourseEntity de;
int ei = 0;
double nonReferentialProbability = nonReferentialResolver.getNonReferentialProbability(ec);
if (debugOn) {
System.err.println(this +".resolve: " + ec.toText() + " -> " + "null "+nonReferentialProbability);
}
for (; ei < getNumEntities(dm); ei++) {
de = dm.getEntity(ei);
if (outOfRange(ec, de)) {
break;
}
if (excluded(ec, de)) {
candProbs[ei] = 0;
if (debugOn) {
System.err.println("excluded "+this +".resolve: " + ec.toText() + " -> " + de + " " + candProbs[ei]);
}
}
else {
List<String> lfeatures = getFeatures(ec, de);
String[] features = lfeatures.toArray(new String[lfeatures.size()]);
try {
candProbs[ei] = model.eval(features)[sameIndex];
}
catch (ArrayIndexOutOfBoundsException e) {
candProbs[ei] = 0;
}
if (debugOn) {
System.err.println(this +".resolve: " + ec.toText() + " -> " + de + " ("+ec.getGender()+","+de.getGender()+") " + candProbs[ei] + " " + lfeatures);
}
}
if (preferFirstReferent && candProbs[ei] > nonReferentialProbability) {
ei++; //update for nonRef assignment
break;
}
}
candProbs[ei] = nonReferentialProbability;
// find max
int maxCandIndex = 0;
for (int k = 1; k <= ei; k++) {
if (candProbs[k] > candProbs[maxCandIndex]) {
maxCandIndex = k;
}
}
if (maxCandIndex == ei) { // no referent
return (null);
}
else {
de = dm.getEntity(maxCandIndex);
return (de);
}
}
/**
* Returns whether the specified entity satisfies the criteria for being a default referent.
* This criteria is used to perform sample selection on the training data and to select a single
* non-referent entity. Typically the criteria is a heuristic for a likely referent.
* @param de The discourse entity being considered for non-reference.
* @return True if the entity should be used as a default referent, false otherwise.
*/
protected boolean defaultReferent(DiscourseEntity de) {
MentionContext ec = de.getLastExtent();
if (ec.getNounPhraseSentenceIndex() == 0) {
return (true);
}
return (false);
}
@Override
public DiscourseEntity retain(MentionContext mention, DiscourseModel dm) {
//System.err.println(this+".retain("+ec+") "+mode);
if (ResolverMode.TRAIN == mode) {
DiscourseEntity de = null;
boolean referentFound = false;
boolean hasReferentialCandidate = false;
boolean nonReferentFound = false;
for (int ei = 0; ei < getNumEntities(dm); ei++) {
DiscourseEntity cde = dm.getEntity(ei);
MentionContext entityMention = cde.getLastExtent();
if (outOfRange(mention, cde)) {
if (mention.getId() != -1 && !referentFound) {
//System.err.println("retain: Referent out of range: "+ec.toText()+" "+ec.parse.getSpan());
}
break;
}
if (excluded(mention, cde)) {
if (showExclusions) {
if (mention.getId() != -1 && entityMention.getId() == mention.getId()) {
System.err.println(this +".retain: Referent excluded: (" + mention.getId() + ") " + mention.toText() + " " + mention.getIndexSpan() + " -> (" + entityMention.getId() + ") " + entityMention.toText() + " " + entityMention.getSpan() + " " + this);
}
}
}
else {
hasReferentialCandidate = true;
boolean useAsDifferentExample = defaultReferent(cde);
//if (!sampleSelection || (mention.getId() != -1 && entityMention.getId() == mention.getId()) || (!nonReferentFound && useAsDifferentExample)) {
List<String> features = getFeatures(mention, cde);
//add Event to Model
if (debugOn) {
System.err.println(this +".retain: " + mention.getId() + " " + mention.toText() + " -> " + entityMention.getId() + " " + cde);
}
if (mention.getId() != -1 && entityMention.getId() == mention.getId()) {
referentFound = true;
events.add(new Event(SAME, features.toArray(new String[features.size()])));
de = cde;
//System.err.println("MaxentResolver.retain: resolved at "+ei);
distances.add(ei);
}
else if (!pairedSampleSelection || (!nonReferentFound && useAsDifferentExample)) {
nonReferentFound = true;
events.add(new Event(DIFF, features.toArray(new String[features.size()])));
}
//}
}
if (pairedSampleSelection && referentFound && nonReferentFound) {
break;
}
if (preferFirstReferent && referentFound) {
break;
}
}
// doesn't refer to anything
if (hasReferentialCandidate) {
nonReferentialResolver.addEvent(mention);
}
return (de);
}
else {
return (super.retain(mention, dm));
}
}
/**
* Returns a list of features for deciding whether the specified mention refers to the specified discourse entity.
* @param mention the mention being considers as possibly referential.
* @param entity The discourse entity with which the mention is being considered referential.
* @return a list of features used to predict reference between the specified mention and entity.
*/
protected List<String> getFeatures(MentionContext mention, DiscourseEntity entity) {
List<String> features = new ArrayList<String>();
features.add(DEFAULT);
features.addAll(ResolverUtils.getCompatibilityFeatures(mention, entity,simModel));
return features;
}
@Override
public void train() throws IOException {
if (ResolverMode.TRAIN == mode) {
if (debugOn) {
System.err.println(this +" referential");
FileWriter writer = new FileWriter(modelName+".events");
for (Iterator<Event> ei=events.iterator();ei.hasNext();) {
Event e = ei.next();
writer.write(e.toString()+"\n");
}
writer.close();
}
(new SuffixSensitiveGISModelWriter(GIS.trainModel((EventStream)new CollectionEventStream(events),100,10),new File(modelName+modelExtension))).persist();
nonReferentialResolver.train();
}
}
public static void setSimilarityModel(TestSimilarityModel sm) {
simModel = sm;
}
@Override
protected boolean excluded(MentionContext ec, DiscourseEntity de) {
if (super.excluded(ec, de)) {
return true;
}
return false;
/*
else {
if (GEN_INCOMPATIBLE == getGenderCompatibilityFeature(ec,de)) {
return true;
}
else if (NUM_INCOMPATIBLE == getNumberCompatibilityFeature(ec,de)) {
return true;
}
else if (SIM_INCOMPATIBLE == getSemanticCompatibilityFeature(ec,de)) {
return true;
}
return false;
}
*/
}
}