Log in Help
Print
Homereleasesgate-5.1-beta2-build3402-ALLpluginsMachine_Learningsrcgatecreolemlmaxent 〉 MaxentWrapper.java
 
/*
 *  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