/*
* Copyright (c) 2004, The University of Sheffield.
*
* This file is part of GATE (see http://gate.ac.uk/), and is free
* software, licenced under the GNU Library General Public License,
* Version 2, June 1991 (in the distribution as file licence.html,
* and also available at http://gate.ac.uk/gate/licence.html).
*
* Mike Dowman 30-03-2004
*
* $Id: MaxentWrapper.java 6990 2005-10-25 15:03:18 +0000 (Tue, 25 Oct 2005) julien_nioche $
*
*/
package gate.creole.ml.maxent;
import gate.creole.ml.*;
import gate.util.GateException;
import gate.creole.ExecutionException;
import gate.gui.MainFrame;
import java.util.List;
import java.util.Iterator;
/**
* Wrapper class for the Maxent machine learning algorithm.
* @see <a href="http://maxent.sourceforge.net/index.html">Maxent homepage</a>
*/
public class MaxentWrapper
implements AdvancedMLEngine, gate.gui.ActionsPublisher {
boolean DEBUG=false;
/**
* This constructor sets up action list so that these actions (loading and
* saving models and data) will be available from a context menu in the
* gui).
*
* There is no option to load or save data sets, as maxent does not support
* this. If there is a need to save data sets, then this can be done using
* weka.wrapper instead.
*/
public MaxentWrapper() {
actionsList = new java.util.ArrayList();
actionsList.add(new LoadModelAction());
actionsList.add(new SaveModelAction());
actionsList.add(null);
}
/**
* No clean up is needed for this wrapper, so this is just added because its
* in the interface.
*/
public void cleanUp() {
}
/**
* Some wrappers allow batch classification, but this one doesn't, so if
* it's ever called just inform the user about this by throwing an exception.
*
* @param instances This parameter is not used.
* @return Nothing is ever returned - an exception is always thrown.
* @throws ExecutionException
*/
public List batchClassifyInstances(java.util.List instances)
throws ExecutionException {
throw new ExecutionException("The Maxent wrapper does not support "+
"batch classification. Remove the "+
"<BATCH-MODE-CLASSIFICATION/> entry "+
"from the XML configuration file and "+
"try again.");
}
/**
* Take a representation of the part of the XML configuration file
* which corresponds to <OPTIONS>, and store it.
*
* @throws GateException
*/
public void setOptions(org.jdom.Element optionsElem) {
this.optionsElement = optionsElem;
}
/**
* Extract the options from the stored Element, and verifiy that they are
* all valid. Store them in the class's fields.
*
* @throws ResourceInstansitaionException
*/
private void extractAndCheckOptions() throws gate.creole.
ResourceInstantiationException {
setCutoff(optionsElement);
setConfidenceThreshold(optionsElement);
setVerbose(optionsElement);
setIterations(optionsElement);
setSmoothing(optionsElement);
setSmoothingObservation(optionsElement);
}
/**
* Set the verbose field appropriately, depending on whether <VERBOSE> is
* specified in the configuration file.
*/
private void setVerbose(org.jdom.Element optionsElem) {
if (optionsElem.getChild("VERBOSE") == null) {
verbose = false;
}
else {
verbose = true;
}
}
/**
* Set the smoothing field appropriately, depending on whether <SMOOTHING> is
* specified in the configuration file.
*/
private void setSmoothing(org.jdom.Element optionsElem) {
if (optionsElem.getChild("SMOOTHING") == null) {
smoothing = false;
}
else {
smoothing = true;
}
}
/**
* Set the smoothing observation field appropriately, depending on what value
* is specified for <SMOOTHING-OBSERVATION> in the configuration file.
*/
private void setSmoothingObservation(org.jdom.Element optionsElem) throws
gate.creole.ResourceInstantiationException {
String smoothingObservationString
= optionsElem.getChildTextTrim("SMOOTHING-OBSERVATION");
if (smoothingObservationString != null) {
try {
smoothingObservation = Double.parseDouble(smoothingObservationString);
}
catch (NumberFormatException e) {
throw new gate.creole.ResourceInstantiationException("Unable to parse " +
"<SMOOTHING-OBSERVATION> value in maxent configuration file.");
}
}
else {
smoothingObservation = 0.0;
}
}
/**
* See if a cutoff is specified in the congif file. If it is set the cutoff
* field, otherwise set cutoff to its default value.
*/
private void setConfidenceThreshold(org.jdom.Element optionsElem) throws gate.
creole.ResourceInstantiationException {
String confidenceThresholdString
= optionsElem.getChildTextTrim("CONFIDENCE-THRESHOLD");
if (confidenceThresholdString != null) {
try {
confidenceThreshold = Double.parseDouble(confidenceThresholdString);
}
catch (NumberFormatException e) {
throw new gate.creole.ResourceInstantiationException("Unable to parse " +
"<CONFIDENCE-THRESHOLD> value in maxent configuration file.");
}
if (confidenceThreshold < 0.0 || confidenceThreshold > 1) {
throw new gate.creole.ResourceInstantiationException(
"<CONFIDENCE-THRESHOLD> in maxent configuration"
+ " file must be set to a value between 0 and 1."
+ " (It is a probability.)");
}
}
else {
confidenceThreshold = 0.0;
}
}
/**
* See if a cutoff is specified in the congif file. If it is set the cutoff
* field, otherwise set cutoff to its default value.
*/
private void setCutoff(org.jdom.Element optionsElem) throws gate.creole.
ResourceInstantiationException {
String cutoffString = optionsElem.getChildTextTrim("CUT-OFF");
if (cutoffString != null) {
try {
cutoff = Integer.parseInt(cutoffString);
}
catch (NumberFormatException e) {
throw new gate.creole.ResourceInstantiationException(
"Unable to parse <CUT-OFF> value in maxent " +
"configuration file. It must be an integer.");
}
}
else {
cutoff = 0;
}
}
/**
* See if a value for how many iterations should be performed during training
* is specified in the congif file. If it is set the iterations field,
* otherwise set it to its default value, 10.
*/
private void setIterations(org.jdom.Element optionsElem) throws gate.creole.
ResourceInstantiationException {
String iterationsString = optionsElem.getChildTextTrim("ITERATIONS");
if (iterationsString != null) {
try {
iterations = Integer.parseInt(iterationsString);
}
catch (NumberFormatException e) {
throw new gate.creole.ResourceInstantiationException(
"Unable to parse <ITERATIONS> value in maxent " +
"configuration file. It must be an integer.");
}
}
else {
iterations = 0;
}
}
/**
* This is called to add a new training instance to the data set collected
* in this wrapper object.
*
* @param attributeValues A list of String objects, each of which corresponds
* to an attribute value. For boolean attributes the values will be true or
* false.
*/
public void addTrainingInstance(List attributeValues) {
markIndicesOnFeatures(attributeValues);
trainingData.add(attributeValues);
datasetChanged = true;
}
/**
* Annotate the features (but not the outcome), by prepending the index of
* their location in the list of attributes, followed by a colon. This is
* because all features are true or false, but it is important that maxent
* does not confuse a true in one position with a true in another when, for
* example, calculating the cutoff.
*
* @param attributeValues a list of String objects listing all the
* feature values and the outcome value for an instance.
*/
void markIndicesOnFeatures(List attributeValues) {
for (int i=0; i<attributeValues.size(); ++i) {
// Skip the outcome (a.k.a. the class).
if (i != datasetDefinition.getClassIndex())
attributeValues.set(i, i+":"+(String)attributeValues.get(i));
}
}
/**
* Set the data set defition for this classifier.
*
* @param definition A specification of the types and allowable values of
* all the attributes, as specified in the <DATASET> part of the configuration
* file.
*/
public void setDatasetDefinition(DatasetDefintion definition) {
this.datasetDefinition = definition;
}
/**
* Tests that the attributes specified in the DatasetDefinition are valid for
* maxent. That is that all the attributes except for the class attribute are
* boolean, and that class is boolean or nominal, as that is a requirement of
* the maxent implementation used.
*/
private void checkDatasetDefinition() throws gate.creole.
ResourceInstantiationException {
// Now go through the dataset definition, and check that each attribute is
// of the right kind.
List attributes = datasetDefinition.getAttributes();
Iterator attributeIterator = attributes.iterator();
while (attributeIterator.hasNext()) {
gate.creole.ml.Attribute currentAttribute
= (gate.creole.ml.Attribute) attributeIterator.next();
if (currentAttribute.semanticType() != gate.creole.ml.Attribute.BOOLEAN) {
if (currentAttribute.semanticType() != gate.creole.ml.Attribute.NOMINAL
|| !currentAttribute.isClass()) {
throw new gate.creole.ResourceInstantiationException(
"Error in maxent configuration file. All " +
"attributes except the <CLASS/> attribute " +
"must be boolean, and the <CLASS/> attribute" +
" must be boolean or nominal");
}
}
}
}
/**
* This method first sets the static parameters of GIS to reflect those
* specified in the configuration file, then it trains the model using the
* data collected up to this point, and stores the model in maxentClassifier.
*/
private void initialiseAndTrainClassifier() {
opennlp.maxent.GIS.PRINT_MESSAGES = verbose;
opennlp.maxent.GIS.SMOOTHING_OBSERVATION = smoothingObservation;
// Actually create and train the model, and store it for later use.
if (DEBUG) {
System.out.println("Number of training instances: "+trainingData.size());
System.out.println("Class index: "+datasetDefinition.getClassIndex());
System.out.println("Iterations: "+iterations);
System.out.println("Cutoff: "+cutoff);
System.out.println("Confidence threshold: "+confidenceThreshold);
System.out.println("Verbose: "+verbose);
System.out.println("Smoothing: "+smoothing);
System.out.println("Smoothing observation: "+smoothingObservation);
System.out.println("");
System.out.println("TRAINING DATA\n");
System.out.println(trainingData);
}
maxentClassifier = opennlp.maxent.GIS.trainModel(
new GateEventStream(trainingData, datasetDefinition.getClassIndex()),
iterations, cutoff,smoothing,verbose);
}
/**
* Decide on the outcome for the instance, based on the values of all the
* maxent features.
*
* N.B. Unless this function was previously called, and there has been no new
* data added since, the model will be trained when it is called. This could
* result in calls to this function taking a long time to execute.
*
* @param attributeValues A list of all the attributes, including the one that
* corresponds to the maxent outcome (the <CLASS/> attribute). The value of
* outcome is arbitrary.
*
* @return A string value giving the nominal value of the outcome or, if the
* outcome is boolean, a java String with value "true" or "false"
*
* @throws ExecutionException
*/
public Object classifyInstance(List attributeValues) throws
ExecutionException {
// First we need to check whether we need to create a new model.
// If either we've never made a model, or some new data has been added, then
// we need to train a new model.
if (maxentClassifier == null || datasetChanged)
initialiseAndTrainClassifier();
// The data now reflects the model, so keep a note of this so we don't
// have to retrain the model if using the same data.
datasetChanged=false;
// We need to mark indices on the features, so that they will be
// consistent with those on which the model was trained.
markIndicesOnFeatures(attributeValues);
// When classifying, we need to remove the outcome from the List of
// attributes. (N.B. we must do this after marking indices, so that
// we don't end up with different indices for features after the class.
attributeValues.remove(datasetDefinition.getClassIndex());
// Then try to classify stuff.
if (confidenceThreshold == 0) { // If no confidence threshold has been set
// then just use simple classification.
return maxentClassifier.
getBestOutcome(maxentClassifier.eval(
(String[])attributeValues.toArray(new String[0])));
}
else { // Otherwise, add all outcomes that are over the threshold.
double[] outcomeProbabilities = maxentClassifier.eval(
(String[]) attributeValues.toArray(new String[0]));
List allOutcomesOverThreshold = new java.util.ArrayList();
for (int i = 0; i < outcomeProbabilities.length; i++) {
if (outcomeProbabilities[i] >= confidenceThreshold) {
allOutcomesOverThreshold.add(maxentClassifier.getOutcome(i));
}
}
return allOutcomesOverThreshold;
}
} // classifyInstance
/**
* Initialises the classifier and prepares for running. Before calling this
* method, the datasetDefinition and optionsElement fields should have been
* set using calls to the appropriate methods.
* @throws GateException If it is not possible to initialise the classifier
* for any reason.
*/
public void init() throws GateException {
//see if we can shout about what we're doing
sListener = null;
java.util.Map listeners = gate.gui.MainFrame.getListeners();
if (listeners != null) {
sListener = (gate.event.StatusListener)
listeners.get("gate.event.StatusListener");
}
if (sListener != null) {
sListener.statusChanged("Setting classifier options...");
}
extractAndCheckOptions();
if (sListener != null) {
sListener.statusChanged("Checking dataset definition...");
}
checkDatasetDefinition();
// N.B. We don't initialise the classifier here, because maxent classifiers,
// are both initialised and trained at the same time. Hence initialisation
// takes place in the method classifyInstance.
//initialise the dataset
if (sListener != null) {
sListener.statusChanged("Initialising dataset...");
}
trainingData = new java.util.ArrayList();
if (sListener != null) {
sListener.statusChanged("");
}
} // init
/**
* Loads the state of this engine from previously saved data.
* @param is An open InputStream from which the model will be loaded.
*/
public void load(java.io.InputStream is) throws java.io.IOException {
if (sListener != null) {
sListener.statusChanged("Loading model...");
}
java.io.ObjectInputStream ois = new java.io.ObjectInputStream(is);
try {
maxentClassifier = (opennlp.maxent.MaxentModel) ois.readObject();
trainingData = (java.util.List) ois.readObject();
datasetDefinition = (DatasetDefintion) ois.readObject();
datasetChanged = ois.readBoolean();
cutoff = ois.readInt();
confidenceThreshold = ois.readDouble();
iterations = ois.readInt();
verbose = ois.readBoolean();
smoothing = ois.readBoolean();
smoothingObservation = ois.readDouble();
}
catch (ClassNotFoundException cnfe) {
throw new gate.util.GateRuntimeException(cnfe.toString());
}
ois.close();
if (sListener != null) {
sListener.statusChanged("");
}
}
/**
* Saves the state of the engine for reuse at a later time.
* @param os An open output stream to which the model will be saved.
*/
public void save(java.io.OutputStream os) throws java.io.IOException {
if (sListener != null) {
sListener.statusChanged("Saving model...");
}
java.io.ObjectOutputStream oos = new java.io.ObjectOutputStream(os);
oos.writeObject(maxentClassifier);
oos.writeObject(trainingData);
oos.writeObject(datasetDefinition);
oos.writeBoolean(datasetChanged);
oos.writeInt(cutoff);
oos.writeDouble(confidenceThreshold);
oos.writeInt(iterations);
oos.writeBoolean(verbose);
oos.writeBoolean(smoothing);
oos.writeDouble(smoothingObservation);
oos.flush();
oos.close();
if (sListener != null) {
sListener.statusChanged("");
}
}
/**
* Gets the list of actions that can be performed on this resource.
* @return a List of Action objects (or null values)
*/
public java.util.List getActions() {
return actionsList;
}
/**
* Registers the PR using the engine with the engine itself.
* @param pr the processing resource that owns this engine.
*/
public void setOwnerPR(gate.ProcessingResource pr) {
this.owner = pr;
}
public DatasetDefintion getDatasetDefinition() {
return datasetDefinition;
}
public boolean supportsBatchMode(){
return false;
}
/**
* This allows the model, including its parameters to be saved to a file.
*/
protected class SaveModelAction
extends javax.swing.AbstractAction {
public SaveModelAction() {
super("Save model");
putValue(SHORT_DESCRIPTION, "Saves the ML model to a file");
}
/**
* This function will open a file chooser, and then call the save function
* to actually save the model. (It is not normally called directly by the
* user, but will be called as the result of the save model menu option
* being selected.)
*/
public void actionPerformed(java.awt.event.ActionEvent evt) {
Runnable runnable = new Runnable() {
public void run() {
javax.swing.JFileChooser fileChooser
= gate.gui.MainFrame.getFileChooser();
fileChooser.setFileFilter(fileChooser.getAcceptAllFileFilter());
fileChooser.setFileSelectionMode(javax.swing.JFileChooser.FILES_ONLY);
fileChooser.setMultiSelectionEnabled(false);
if (fileChooser.showSaveDialog(null)
== javax.swing.JFileChooser.APPROVE_OPTION) {
java.io.File file = fileChooser.getSelectedFile();
try {
gate.gui.MainFrame.lockGUI("Saving ML model...");
save(new java.util.zip.GZIPOutputStream(
new java.io.FileOutputStream(
file.getCanonicalPath(), false)));
}
catch (java.io.IOException ioe) {
javax.swing.JOptionPane.showMessageDialog(MainFrame.getInstance(),
"Error!\n" +
ioe.toString(),
"GATE", javax.swing.JOptionPane.ERROR_MESSAGE);
ioe.printStackTrace(gate.util.Err.getPrintWriter());
}
finally {
gate.gui.MainFrame.unlockGUI();
}
}
}
};
Thread thread = new Thread(runnable, "ModelSaver(serialisation)");
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();
}
}
/**
* This reloads a file that was previously saved using the SaveModelAction
* class. A maxent ml processing resource must already exist before this
* action can be performed.
*/
protected class LoadModelAction
extends javax.swing.AbstractAction {
public LoadModelAction() {
super("Load model");
putValue(SHORT_DESCRIPTION, "Loads a ML model from a file");
}
/**
* This function will open a file chooser, and then call the load function
* to actually load the model. (It is not normally called directly by the
* user, but will be called as the result of the load model menu option
* being selected.)
*/
public void actionPerformed(java.awt.event.ActionEvent evt) {
Runnable runnable = new Runnable() {
public void run() {
javax.swing.JFileChooser fileChooser
= gate.gui.MainFrame.getFileChooser();
fileChooser.setFileFilter(fileChooser.getAcceptAllFileFilter());
fileChooser.setFileSelectionMode(javax.swing.JFileChooser.FILES_ONLY);
fileChooser.setMultiSelectionEnabled(false);
if (fileChooser.showOpenDialog(null)
== javax.swing.JFileChooser.APPROVE_OPTION) {
java.io.File file = fileChooser.getSelectedFile();
try {
gate.gui.MainFrame.lockGUI("Loading model...");
load(new java.util.zip.GZIPInputStream(
new java.io.FileInputStream(file)));
}
catch (java.io.IOException ioe) {
javax.swing.JOptionPane.showMessageDialog(MainFrame.getInstance(),
"Error!\n" +
ioe.toString(),
"GATE", javax.swing.JOptionPane.ERROR_MESSAGE);
ioe.printStackTrace(gate.util.Err.getPrintWriter());
}
finally {
gate.gui.MainFrame.unlockGUI();
}
}
}
};
Thread thread = new Thread(runnable, "ModelLoader(serialisation)");
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();
}
}
protected gate.creole.ml.DatasetDefintion datasetDefinition;
/**
* The Maxent classifier used by this wrapper
*/
protected opennlp.maxent.MaxentModel maxentClassifier;
/**
* This List stores all the data that has been collected. Each item is a
* List of Strings, each of which is an attribute. In maxent terms, these
* are the features and the outcome - the position of the outcome can be found
* by referring to the the datasetDefition object.
*/
protected List trainingData;
/**
* The JDom element contaning the options fro this wrapper.
*/
protected org.jdom.Element optionsElement;
/**
* Marks whether the dataset was changed since the last time the classifier
* was built.
*/
protected boolean datasetChanged = false;
/*
* This list stores the actions that will be available on the context menu
* in the GUI.
*/
protected List actionsList;
protected gate.ProcessingResource owner;
protected gate.event.StatusListener sListener;
/**
* The following members are set by the <OPTIONS> part of the config file,
* and control the parameters used for training the model, and for
* classifying instances. They are initialised with their default values,
* but may be changed when setOptions is called.
*/
protected int cutoff = 0;
protected double confidenceThreshold = 0;
protected int iterations = 10;
protected boolean verbose = false;
protected boolean smoothing = false;
protected double smoothingObservation = 0.1;
} // MaxentWrapper