/*
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program; if not, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*
 *    SVMAttributeEval.java
 *    Copyright (C) 2002 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.attributeSelection;

import weka.classifiers.functions.SMO;
import weka.core.Capabilities;
import weka.core.Instances;
import weka.core.Option;
import weka.core.OptionHandler;
import weka.core.RevisionUtils;
import weka.core.SelectedTag;
import weka.core.TechnicalInformation;
import weka.core.TechnicalInformationHandler;
import weka.core.Utils;
import weka.core.Capabilities.Capability;
import weka.core.TechnicalInformation.Field;
import weka.core.TechnicalInformation.Type;
import weka.filters.Filter;
import weka.filters.unsupervised.attribute.MakeIndicator;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Vector;

/** 
 <!-- globalinfo-start -->
 * SVMAttributeEval :<br/>
 * <br/>
 * Evaluates the worth of an attribute by using an SVM classifier. Attributes are ranked by the square of the weight assigned by the SVM. Attribute selection for multiclass problems is handled by ranking attributes for each class seperately using a one-vs-all method and then "dealing" from the top of each pile to give a final ranking.<br/>
 * <br/>
 * For more information see:<br/>
 * <br/>
 * I. Guyon, J. Weston, S. Barnhill, V. Vapnik (2002). Gene selection for cancer classification using support vector machines. Machine Learning. 46:389-422.
 * <p/>
 <!-- globalinfo-end -->
 *
 <!-- technical-bibtex-start -->
 * BibTeX:
 * <pre>
 * &#64;article{Guyon2002,
 *    author = {I. Guyon and J. Weston and S. Barnhill and V. Vapnik},
 *    journal = {Machine Learning},
 *    pages = {389-422},
 *    title = {Gene selection for cancer classification using support vector machines},
 *    volume = {46},
 *    year = {2002}
 * }
 * </pre>
 * <p/>
 <!-- technical-bibtex-end -->
 *
 <!-- options-start -->
 * Valid options are: <p/>
 * 
 * <pre> -X &lt;constant rate of elimination&gt;
 *  Specify the constant rate of attribute
 *  elimination per invocation of
 *  the support vector machine.
 *  Default = 1.</pre>
 * 
 * <pre> -Y &lt;percent rate of elimination&gt;
 *  Specify the percentage rate of attributes to
 *  elimination per invocation of
 *  the support vector machine.
 *  Trumps constant rate (above threshold).
 *  Default = 0.</pre>
 * 
 * <pre> -Z &lt;threshold for percent elimination&gt;
 *  Specify the threshold below which 
 *  percentage attribute elimination
 *  reverts to the constant method.</pre>
 * 
 * <pre> -P &lt;epsilon&gt;
 *  Specify the value of P (epsilon
 *  parameter) to pass on to the
 *  support vector machine.
 *  Default = 1.0e-25</pre>
 * 
 * <pre> -T &lt;tolerance&gt;
 *  Specify the value of T (tolerance
 *  parameter) to pass on to the
 *  support vector machine.
 *  Default = 1.0e-10</pre>
 * 
 * <pre> -C &lt;complexity&gt;
 *  Specify the value of C (complexity
 *  parameter) to pass on to the
 *  support vector machine.
 *  Default = 1.0</pre>
 * 
 * <pre> -N
 *  Whether the SVM should 0=normalize/1=standardize/2=neither.
 *  (default 0=normalize)</pre>
 * 
 <!-- options-end -->
 *
 * @author Eibe Frank (eibe@cs.waikato.ac.nz)
 * @author Mark Hall (mhall@cs.waikato.ac.nz)
 * @author Kieran Holland
 * @version $Revision: 1.28 $
 */
public class SVMAttributeEval 
  extends ASEvaluation
  implements AttributeEvaluator, 
             OptionHandler, 
             TechnicalInformationHandler {
  
  /** for serialization */
  static final long serialVersionUID = -6489975709033967447L;

  /** The attribute scores */
  private double[] m_attScores;

  /** Constant rate of attribute elimination per iteration */
  private int m_numToEliminate = 1;

  /** Percentage rate of attribute elimination, trumps constant
      rate (above threshold), ignored if = 0  */
  private int m_percentToEliminate = 0;

  /** Threshold below which percent elimination switches to
      constant elimination */
  private int m_percentThreshold = 0;

  /** Complexity parameter to pass on to SMO */
  private double m_smoCParameter = 1.0;

  /** Tolerance parameter to pass on to SMO */
  private double m_smoTParameter = 1.0e-10;

  /** Epsilon parameter to pass on to SMO */
  private double m_smoPParameter = 1.0e-25;

  /** Filter parameter to pass on to SMO */
  private int m_smoFilterType = 0;

  /**
   * Returns a string describing this attribute evaluator
   * @return a description of the evaluator suitable for
   * displaying in the explorer/experimenter gui
   */
  public String globalInfo() {
    return "SVMAttributeEval :\n\nEvaluates the worth of an attribute by "
      + "using an SVM classifier. Attributes are ranked by the square of the "
      + "weight assigned by the SVM. Attribute selection for multiclass "
      + "problems is handled by ranking attributes for each class seperately "
      + "using a one-vs-all method and then \"dealing\" from the top of "
      + "each pile to give a final ranking.\n\n"
      + "For more information see:\n\n"
      + getTechnicalInformation().toString();
  }

  /**
   * Returns an instance of a TechnicalInformation object, containing 
   * detailed information about the technical background of this class,
   * e.g., paper reference or book this class is based on.
   * 
   * @return the technical information about this class
   */
  public TechnicalInformation getTechnicalInformation() {
    TechnicalInformation        result;
    
    result = new TechnicalInformation(Type.ARTICLE);
    result.setValue(Field.AUTHOR, "I. Guyon and J. Weston and S. Barnhill and V. Vapnik");
    result.setValue(Field.YEAR, "2002");
    result.setValue(Field.TITLE, "Gene selection for cancer classification using support vector machines");
    result.setValue(Field.JOURNAL, "Machine Learning");
    result.setValue(Field.VOLUME, "46");
    result.setValue(Field.PAGES, "389-422");
    
    return result;
  }

  /**
   * Constructor
   */
  public SVMAttributeEval() {
    resetOptions();
  }

  /**
   * Returns an enumeration describing all the available options
   *
   * @return an enumeration of options
   */
  public Enumeration listOptions() {
    Vector newVector = new Vector(4);

    newVector.addElement(
                         new Option(
                                    "\tSpecify the constant rate of attribute\n"
                                    + "\telimination per invocation of\n"
                                    + "\tthe support vector machine.\n"
                                    + "\tDefault = 1.",
                                    "X",
                                    1,
                                    "-X <constant rate of elimination>"));

    newVector.addElement(
                         new Option(
                                    "\tSpecify the percentage rate of attributes to\n"
                                    + "\telimination per invocation of\n"
                                    + "\tthe support vector machine.\n"
                                    + "\tTrumps constant rate (above threshold).\n"
                                    + "\tDefault = 0.",
                                    "Y",
                                    1,
                                    "-Y <percent rate of elimination>"));

    newVector.addElement(
                         new Option(
                                    "\tSpecify the threshold below which \n"
                                    + "\tpercentage attribute elimination\n"
                                    + "\treverts to the constant method.",
                                    "Z",
                                    1,
                                    "-Z <threshold for percent elimination>"));


    newVector.addElement(
                         new Option(
                                    "\tSpecify the value of P (epsilon\n"
                                    + "\tparameter) to pass on to the\n"
                                    + "\tsupport vector machine.\n"
                                    + "\tDefault = 1.0e-25",
                                    "P",
                                    1,
                                    "-P <epsilon>"));

    newVector.addElement(
                         new Option(
                                    "\tSpecify the value of T (tolerance\n"
                                    + "\tparameter) to pass on to the\n"
                                    + "\tsupport vector machine.\n"
                                    + "\tDefault = 1.0e-10",
                                    "T",
                                    1,
                                    "-T <tolerance>"));

    newVector.addElement(
                         new Option(
                                    "\tSpecify the value of C (complexity\n"
                                    + "\tparameter) to pass on to the\n"
                                    + "\tsupport vector machine.\n"
                                    + "\tDefault = 1.0",
                                    "C",
                                    1,
                                    "-C <complexity>"));

    newVector.addElement(new Option("\tWhether the SVM should "
                                    + "0=normalize/1=standardize/2=neither.\n"
                                    + "\t(default 0=normalize)",
                                    "N",
                                    1,
                                    "-N"));

    return newVector.elements();
  }

  /**
   * Parses a given list of options. <p/>
   *
   <!-- options-start -->
   * Valid options are: <p/>
   * 
   * <pre> -X &lt;constant rate of elimination&gt;
   *  Specify the constant rate of attribute
   *  elimination per invocation of
   *  the support vector machine.
   *  Default = 1.</pre>
   * 
   * <pre> -Y &lt;percent rate of elimination&gt;
   *  Specify the percentage rate of attributes to
   *  elimination per invocation of
   *  the support vector machine.
   *  Trumps constant rate (above threshold).
   *  Default = 0.</pre>
   * 
   * <pre> -Z &lt;threshold for percent elimination&gt;
   *  Specify the threshold below which 
   *  percentage attribute elimination
   *  reverts to the constant method.</pre>
   * 
   * <pre> -P &lt;epsilon&gt;
   *  Specify the value of P (epsilon
   *  parameter) to pass on to the
   *  support vector machine.
   *  Default = 1.0e-25</pre>
   * 
   * <pre> -T &lt;tolerance&gt;
   *  Specify the value of T (tolerance
   *  parameter) to pass on to the
   *  support vector machine.
   *  Default = 1.0e-10</pre>
   * 
   * <pre> -C &lt;complexity&gt;
   *  Specify the value of C (complexity
   *  parameter) to pass on to the
   *  support vector machine.
   *  Default = 1.0</pre>
   * 
   * <pre> -N
   *  Whether the SVM should 0=normalize/1=standardize/2=neither.
   *  (default 0=normalize)</pre>
   * 
   <!-- options-end -->
   *
   * @param options the list of options as an array of strings
   * @throws Exception if an error occurs
   */
  public void setOptions(String[] options) throws Exception {
    String optionString;

    optionString = Utils.getOption('X', options);
    if (optionString.length() != 0) {
      setAttsToEliminatePerIteration(Integer.parseInt(optionString));
    }

    optionString = Utils.getOption('Y', options);
    if (optionString.length() != 0) {
      setPercentToEliminatePerIteration(Integer.parseInt(optionString));
    }

    optionString = Utils.getOption('Z', options);
    if (optionString.length() != 0) {
      setPercentThreshold(Integer.parseInt(optionString));
    }

    optionString = Utils.getOption('P', options);
    if (optionString.length() != 0) {
      setEpsilonParameter((new Double(optionString)).doubleValue());
    }

    optionString = Utils.getOption('T', options);
    if (optionString.length() != 0) {
      setToleranceParameter((new Double(optionString)).doubleValue());
    }

    optionString = Utils.getOption('C', options);
    if (optionString.length() != 0) {
      setComplexityParameter((new Double(optionString)).doubleValue());
    }

    optionString = Utils.getOption('N', options);
    if (optionString.length() != 0) {
      setFilterType(new SelectedTag(Integer.parseInt(optionString), SMO.TAGS_FILTER));
    } else {
      setFilterType(new SelectedTag(SMO.FILTER_NORMALIZE, SMO.TAGS_FILTER));
    }

    Utils.checkForRemainingOptions(options);
  }

  /**
   * Gets the current settings of SVMAttributeEval
   *
   * @return an array of strings suitable for passing to setOptions() 
   */
  public String[] getOptions() {
    String[] options = new String[14];
    int current = 0;

    options[current++] = "-X";
    options[current++] = "" + getAttsToEliminatePerIteration();

    options[current++] = "-Y";
    options[current++] = "" + getPercentToEliminatePerIteration();

    options[current++] = "-Z";
    options[current++] = "" + getPercentThreshold();
                
    options[current++] = "-P";
    options[current++] = "" + getEpsilonParameter();

    options[current++] = "-T";
    options[current++] = "" + getToleranceParameter();          

    options[current++] = "-C";
    options[current++] = "" + getComplexityParameter();         

    options[current++] = "-N";
    options[current++] = "" + m_smoFilterType;          

    while (current < options.length) {
      options[current++] = "";
    }

    return options;
  }

  //________________________________________________________________________

  /**
   * Returns a tip text for this property suitable for display in the
   * GUI
   *
   * @return tip text string describing this property
   */
  public String attsToEliminatePerIterationTipText() {
    return "Constant rate of attribute elimination.";
  }

  /**
   * Returns a tip text for this property suitable for display in the
   * GUI
   *
   * @return tip text string describing this property
   */
  public String percentToEliminatePerIterationTipText() {
    return "Percent rate of attribute elimination.";
  }

  /**
   * Returns a tip text for this property suitable for display in the
   * GUI
   *
   * @return tip text string describing this property
   */
  public String percentThresholdTipText() {
    return "Threshold below which percent elimination reverts to constant elimination.";
  }

  /**
   * Returns a tip text for this property suitable for display in the
   * GUI
   *
   * @return tip text string describing this property
   */
  public String epsilonParameterTipText() {
    return "P epsilon parameter to pass to the SVM";
  }

  /**
   * Returns a tip text for this property suitable for display in the
   * GUI
   *
   * @return tip text string describing this property
   */
  public String toleranceParameterTipText() {
    return "T tolerance parameter to pass to the SVM";
  }

  /**
   * Returns a tip text for this property suitable for display in the
   * GUI
   *
   * @return tip text string describing this property
   */
  public String complexityParameterTipText() {
    return "C complexity parameter to pass to the SVM";
  }

  /**
   * Returns a tip text for this property suitable for display in the
   * GUI
   *
   * @return tip text string describing this property
   */
  public String filterTypeTipText() {
    return "filtering used by the SVM";
  }

  //________________________________________________________________________

  /**
   * Set the constant rate of attribute elimination per iteration
   *
   * @param cRate the constant rate of attribute elimination per iteration
   */
  public void setAttsToEliminatePerIteration(int cRate) {
    m_numToEliminate = cRate;
  }

  /**
   * Get the constant rate of attribute elimination per iteration
   *
   * @return the constant rate of attribute elimination per iteration
   */
  public int getAttsToEliminatePerIteration() {
    return m_numToEliminate;
  }

  /**
   * Set the percentage of attributes to eliminate per iteration
   *
   * @param pRate percent of attributes to eliminate per iteration
   */
  public void setPercentToEliminatePerIteration(int pRate) {
    m_percentToEliminate = pRate;
  }

  /**
   * Get the percentage rate of attribute elimination per iteration
   *
   * @return the percentage rate of attribute elimination per iteration
   */
  public int getPercentToEliminatePerIteration() {
    return m_percentToEliminate;
  }

  /**
   * Set the threshold below which percentage elimination reverts to
   * constant elimination.
   *
   * @param pThresh percent of attributes to eliminate per iteration
   */
  public void setPercentThreshold(int pThresh) {
    m_percentThreshold = pThresh;
  }

  /**
   * Get the threshold below which percentage elimination reverts to 
   * constant elimination.
   *
   * @return the threshold below which percentage elimination stops
   */
  public int getPercentThreshold() {
    return m_percentThreshold;
  }

  /**
   * Set the value of P for SMO
   *
   * @param svmP the value of P
   */
  public void setEpsilonParameter(double svmP) {
    m_smoPParameter = svmP;
  }

  /**
   * Get the value of P used with SMO
   *
   * @return the value of P
   */
  public double getEpsilonParameter() {
    return m_smoPParameter;
  }
        
  /**
   * Set the value of T for SMO
   *
   * @param svmT the value of T
   */
  public void setToleranceParameter(double svmT) {
    m_smoTParameter = svmT;
  }

  /**
   * Get the value of T used with SMO
   *
   * @return the value of T
   */
  public double getToleranceParameter() {
    return m_smoTParameter;
  }


  /**
   * Set the value of C for SMO
   *
   * @param svmC the value of C
   */
  public void setComplexityParameter(double svmC) {
    m_smoCParameter = svmC;
  }

  /**
   * Get the value of C used with SMO
   *
   * @return the value of C
   */
  public double getComplexityParameter() {
    return m_smoCParameter;
  }

  /**
   * The filtering mode to pass to SMO
   *
   * @param newType the new filtering mode
   */
  public void setFilterType(SelectedTag newType) {
    
    if (newType.getTags() == SMO.TAGS_FILTER) {
      m_smoFilterType = newType.getSelectedTag().getID();
    }
  }

  /**
   * Get the filtering mode passed to SMO
   *
   * @return the filtering mode
   */
  public SelectedTag getFilterType() {

    return new SelectedTag(m_smoFilterType, SMO.TAGS_FILTER);
  }

  //________________________________________________________________________

  /**
   * Returns the capabilities of this evaluator.
   *
   * @return            the capabilities of this evaluator
   * @see               Capabilities
   */
  public Capabilities getCapabilities() {
    Capabilities        result;
    
    result = new SMO().getCapabilities();
    
    result.setOwner(this);
    
    // only binary attributes are allowed, otherwise the NominalToBinary
    // filter inside SMO will increase the number of attributes which in turn
    // will lead to ArrayIndexOutOfBounds-Exceptions.
    result.disable(Capability.NOMINAL_ATTRIBUTES);
    result.enable(Capability.BINARY_ATTRIBUTES);
    result.disableAllAttributeDependencies();
    
    return result;
  }

  /**
   * Initializes the evaluator.
   *
   * @param data set of instances serving as training data 
   * @throws Exception if the evaluator has not been 
   * generated successfully
   */
  public void buildEvaluator(Instances data) throws Exception {
    // can evaluator handle data?
    getCapabilities().testWithFail(data);

    //System.out.println("Class attribute: " + data.attribute(data.classIndex()).name());
    // Check settings           
    m_numToEliminate = (m_numToEliminate > 1) ? m_numToEliminate : 1;
    m_percentToEliminate = (m_percentToEliminate < 100) ? m_percentToEliminate : 100;
    m_percentToEliminate = (m_percentToEliminate > 0) ? m_percentToEliminate : 0;
    m_percentThreshold = (m_percentThreshold < data.numAttributes()) ? m_percentThreshold : data.numAttributes() - 1;
    m_percentThreshold = (m_percentThreshold > 0) ? m_percentThreshold : 0;

    // Get ranked attributes for each class seperately, one-vs-all
    int[][] attScoresByClass;
    int numAttr = data.numAttributes() - 1;
    if(data.numClasses()>2) {
      attScoresByClass = new int[data.numClasses()][numAttr];
      for (int i = 0; i < data.numClasses(); i++) {
        attScoresByClass[i] = rankBySVM(i, data);
      }
    }
    else {
      attScoresByClass = new int[1][numAttr];
      attScoresByClass[0] = rankBySVM(0, data);
    }

    // Cycle through class-specific ranked lists, poping top one off for each class
    // and adding it to the overall ranked attribute list if it's not there already
    ArrayList ordered = new ArrayList(numAttr);
    for (int i = 0; i < numAttr; i++) {
      for (int j = 0; j < (data.numClasses()>2 ? data.numClasses() : 1); j++) {
        Integer rank = new Integer(attScoresByClass[j][i]);
        if (!ordered.contains(rank))
          ordered.add(rank);
      }
    }
    m_attScores = new double[data.numAttributes()];
    Iterator listIt = ordered.iterator();
    for (double i = (double) numAttr; listIt.hasNext(); i = i - 1.0) {
      m_attScores[((Integer) listIt.next()).intValue()] = i;
    }
  }

  /**
   * Get SVM-ranked attribute indexes (best to worst) selected for
   * the class attribute indexed by classInd (one-vs-all).
   */
  private int[] rankBySVM(int classInd, Instances data) {
    // Holds a mapping into the original array of attribute indices
    int[] origIndices = new int[data.numAttributes()];
    for (int i = 0; i < origIndices.length; i++)
      origIndices[i] = i;
    
    // Count down of number of attributes remaining
    int numAttrLeft = data.numAttributes()-1;
    // Ranked attribute indices for this class, one vs.all (highest->lowest)
    int[] attRanks = new int[numAttrLeft];

    try {
      MakeIndicator filter = new MakeIndicator();
      filter.setAttributeIndex("" + (data.classIndex() + 1));
      filter.setNumeric(false);
      filter.setValueIndex(classInd);
      filter.setInputFormat(data);
      Instances trainCopy = Filter.useFilter(data, filter);
      double pctToElim = ((double) m_percentToEliminate) / 100.0;
      while (numAttrLeft > 0) {
        int numToElim;
        if (pctToElim > 0) {
          numToElim = (int) (trainCopy.numAttributes() * pctToElim);
          numToElim = (numToElim > 1) ? numToElim : 1;
          if (numAttrLeft - numToElim <= m_percentThreshold) {
            pctToElim = 0;
            numToElim = numAttrLeft - m_percentThreshold;
          }
        } else {
          numToElim = (numAttrLeft >= m_numToEliminate) ? m_numToEliminate : numAttrLeft;
        }
        
        // Build the linear SVM with default parameters
        SMO smo = new SMO();
                                
        // SMO seems to get stuck if data not normalised when few attributes remain
        // smo.setNormalizeData(numAttrLeft < 40);
        smo.setFilterType(new SelectedTag(m_smoFilterType, SMO.TAGS_FILTER));
        smo.setEpsilon(m_smoPParameter);
        smo.setToleranceParameter(m_smoTParameter);
        smo.setC(m_smoCParameter);
        smo.buildClassifier(trainCopy);
                                
        // Find the attribute with maximum weight^2
        double[] weightsSparse = smo.sparseWeights()[0][1];
        int[] indicesSparse = smo.sparseIndices()[0][1];
        double[] weights = new double[trainCopy.numAttributes()];
        for (int j = 0; j < weightsSparse.length; j++) {
          weights[indicesSparse[j]] = weightsSparse[j] * weightsSparse[j];
        }
        weights[trainCopy.classIndex()] = Double.MAX_VALUE;
        int minWeightIndex;
        int[] featArray = new int[numToElim];
        boolean[] eliminated = new boolean[origIndices.length];
        for (int j = 0; j < numToElim; j++) {
          minWeightIndex = Utils.minIndex(weights);
          attRanks[--numAttrLeft] = origIndices[minWeightIndex];
          featArray[j] = minWeightIndex;
          eliminated[minWeightIndex] = true;
          weights[minWeightIndex] = Double.MAX_VALUE;
        }
                                
        // Delete the worst attributes. 
        weka.filters.unsupervised.attribute.Remove delTransform =
          new weka.filters.unsupervised.attribute.Remove();
        delTransform.setInvertSelection(false);
        delTransform.setAttributeIndicesArray(featArray);
        delTransform.setInputFormat(trainCopy);
        trainCopy = Filter.useFilter(trainCopy, delTransform);
                                
        // Update the array of remaining attribute indices
        int[] temp = new int[origIndices.length - numToElim];
        int k = 0;
        for (int j = 0; j < origIndices.length; j++) {
          if (!eliminated[j]) {
            temp[k++] = origIndices[j];
          }
        }
        origIndices = temp;
      }                 
      // Carefully handle all exceptions
    } catch (Exception e) {
      e.printStackTrace();                      
    }
    return attRanks;
  }

  /**
   * Resets options to defaults.
   */
  protected void resetOptions() {
    m_attScores = null;
  }

  /**
   * Evaluates an attribute by returning the rank of the square of its coefficient in a
   * linear support vector machine.
   *
   * @param attribute the index of the attribute to be evaluated
   * @throws Exception if the attribute could not be evaluated
   */
  public double evaluateAttribute(int attribute) throws Exception {
    return m_attScores[attribute];
  }

  /**
   * Return a description of the evaluator
   * @return description as a string
   */
  public String toString() {

    StringBuffer text = new StringBuffer();
    if (m_attScores == null) {
      text.append("\tSVM feature evaluator has not been built yet");
    } else {
      text.append("\tSVM feature evaluator");
    }

    text.append("\n");
    return text.toString();
  }
  
  /**
   * Returns the revision string.
   * 
   * @return		the revision
   */
  public String getRevision() {
    return RevisionUtils.extract("$Revision: 1.28 $");
  }

  /**
   * Main method for testing this class.
   *
   * @param args the options
   */
  public static void main(String[] args) {
    runEvaluator(new SVMAttributeEval(), args);
  }
}
