/*
* 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.
*/
/*
* OLM.java
* Copyright (C) 2004 Stijn Lievens
*
*/
package weka.classifiers.misc;
import weka.classifiers.RandomizableClassifier;
import weka.classifiers.misc.monotone.Coordinates;
import weka.classifiers.misc.monotone.DiscreteDistribution;
import weka.classifiers.misc.monotone.EnumerationIterator;
import weka.classifiers.misc.monotone.InstancesComparator;
import weka.classifiers.misc.monotone.InstancesUtil;
import weka.classifiers.misc.monotone.MultiDimensionalSort;
import weka.core.Attribute;
import weka.core.Capabilities;
import weka.core.FastVector;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.Option;
import weka.core.RevisionUtils;
import weka.core.SelectedTag;
import weka.core.Tag;
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.estimators.DiscreteEstimator;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.Vector;
/**
<!-- globalinfo-start -->
* This class is an implementation of the Ordinal Learning Method<br/>
* Further information regarding the algorithm and variants can be found in:<br/>
* <br/>
* Arie Ben-David (1992). Automatic Generation of Symbolic Multiattribute Ordinal Knowledge-Based DSSs: methodology and Applications. Decision Sciences. 23:1357-1372.<br/>
* <br/>
* Lievens, Stijn (2003-2004). Studie en implementatie van instantie-gebaseerde algoritmen voor gesuperviseerd rangschikken..
* <p/>
<!-- globalinfo-end -->
*
<!-- technical-bibtex-start -->
* BibTeX:
* <pre>
* @article{Ben-David1992,
* author = {Arie Ben-David},
* journal = {Decision Sciences},
* pages = {1357-1372},
* title = {Automatic Generation of Symbolic Multiattribute Ordinal Knowledge-Based DSSs: methodology and Applications},
* volume = {23},
* year = {1992}
* }
*
* @mastersthesis{Lievens2003-2004,
* author = {Lievens, Stijn},
* school = {Ghent University},
* title = {Studie en implementatie van instantie-gebaseerde algoritmen voor gesuperviseerd rangschikken.},
* year = {2003-2004}
* }
* </pre>
* <p/>
<!-- technical-bibtex-end -->
*
<!-- options-start -->
* Valid options are: <p/>
*
* <pre> -S <num>
* Random number seed.
* (default 1)</pre>
*
* <pre> -D
* If set, classifier is run in debug mode and
* may output additional info to the console</pre>
*
* <pre> -C <CL|REG>
* Sets the classification type to be used.
* (Default: REG)</pre>
*
* <pre> -A <MEAN|MED|MAX>
* Sets the averaging type used in phase 1 of the classifier.
* (Default: MEAN)</pre>
*
* <pre> -N <NONE|EUCL|HAM>
* If different from NONE, a nearest neighbour rule is fired when the
* rule base doesn't contain an example smaller than the instance
* to be classified
* (Default: NONE).</pre>
*
* <pre> -E <MIN|MAX|BOTH>
* Sets the extension type, i.e. the rule base to use.
* (Default: MIN)</pre>
*
* <pre> -sort
* If set, the instances are also sorted within the same class
* before building the rule bases</pre>
*
<!-- options-end -->
*
* @author Stijn Lievens (stijn.lievens@ugent.be)
* @version $Revision: 1.2 $
*/
public class OLM
extends RandomizableClassifier
implements TechnicalInformationHandler {
/** for serialization */
private static final long serialVersionUID = 3722951802290935192L;
/**
* Round the real value that is returned by the original algorithm
* to the nearest label.
*/
public static final int CT_ROUNDED = 0;
/**
* No rounding is performed during classification, this is the
* classification is done in a regression like way.
*/
public static final int CT_REAL = 1;
/** the classification types */
public static final Tag[] TAGS_CLASSIFICATIONTYPES = {
new Tag(CT_ROUNDED, "CL", "Round to nearest label"),
new Tag(CT_REAL, "REG", "Regression-like classification")
};
/**
* Use the mean for averaging in phase 1. This is in fact a
* non ordinal procedure. The scores used for averaging are the internal
* values of WEKA.
*/
public static final int AT_MEAN = 0;
/**
* Use the median for averaging in phase 1. The possible values
* are in the extended set of labels, this is labels in between the
* original labels are possible.
*/
public static final int AT_MEDIAN = 1;
/**
* Use the mode for averaging in phase 1. The label
* that has maximum frequency is used. If there is more
* than one label that has maximum frequency, the lowest
* one is prefered.
*/
public static final int AT_MAXPROB = 2;
/** the averaging types */
public static final Tag[] TAGS_AVERAGINGTYPES = {
new Tag(AT_MEAN, "MEAN", "Mean"),
new Tag(AT_MEDIAN, "MED","Median"),
new Tag(AT_MAXPROB, "MAX", "Max probability")
};
/**
* No nearest neighbour rule will be fired when
* classifying an instance for which there is no smaller rule
* in the rule base?
*/
public static final int DT_NONE = -1;
/**
* Use the Euclidian distance whenever a nearest neighbour
* rule is fired.
*/
public static final int DT_EUCLID = 0;
/**
* Use the Hamming distance, this is the number of
* positions in which the instances differ, whenever a
* nearest neighbour rule is fired
*/
public static final int DT_HAMMING = 1;
/** the distance types */
public static final Tag[] TAGS_DISTANCETYPES = {
new Tag(DT_NONE, "NONE", "No nearest neighbor"),
new Tag(DT_EUCLID, "EUCL", "Euclidean"),
new Tag(DT_HAMMING, "HAM", "Hamming")
};
/**
* Use only the minimal extension, as in the original algorithm
* of Ben-David.
*/
public static final int ET_MIN = 0;
/**
* Use only the maximal extension. In this case an algorithm
* dual to the original one is performed.
*/
public static final int ET_MAX = 1;
/**
* Combine both the minimal and maximal extension, and use the
* midpoint of the resulting interval as prediction.
*/
public static final int ET_BOTH = 2;
/** the mode types */
public static final Tag[] TAGS_EXTENSIONTYPES = {
new Tag(ET_MIN, "MIN", "Minimal extension"),
new Tag(ET_MAX, "MAX", "Maximal extension"),
new Tag(ET_BOTH, "BOTH", "Minimal and maximal extension")
};
/**
* The training examples, used temporarily.
* m_train is cleared after the rule base is built.
*/
private Instances m_train;
/** Number of classes in the original m_train */
private int m_numClasses;
/**
* The rule base, should be consistent and contain no
* redundant rules. This is the rule base as in the original
* algorithm of Ben-David.
*/
private Instances m_baseMin;
/**
* This is a complentary rule base, using the maximal rather
* than the minimal extension.
*/
private Instances m_baseMax;
/**
* Map used in the method buildClassifier in order to quickly
* gather all info needed for phase 1. This is a map containing
* (Coordinates, DiscreteEstimator)-pairs.
*/
private Map m_estimatedDistributions;
/** classification type */
private int m_ctype = CT_REAL;
/** averaging type */
private int m_atype = AT_MEAN;
/** distance type */
private int m_dtype = DT_EUCLID;
/** mode type */
private int m_etype = ET_MIN;
/**
* Should the instances be sorted such that minimal (resp. maximal)
* elements (per class) are treated first when building m_baseMin
* (resp. m_baseMax).
*/
private boolean m_sort = false;
/**
* Returns a string describing the classifier.
* @return a description suitable for displaying in the
* explorer/experimenter gui
*/
public String globalInfo() {
return "This class is an implementation of the Ordinal Learning "
+ "Method\n"
+ "Further information regarding the algorithm and variants "
+ "can be found in:\n\n"
+ getTechnicalInformation().toString();
}
/**
* Returns default capabilities of the classifier.
*
* @return the capabilities of this classifier
*/
public Capabilities getCapabilities() {
Capabilities result = super.getCapabilities();
// attributes
result.enable(Capability.NOMINAL_ATTRIBUTES);
// class
result.enable(Capability.NOMINAL_CLASS);
result.enable(Capability.MISSING_CLASS_VALUES);
// instances
result.setMinimumNumberInstances(0);
return result;
}
/**
* 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;
TechnicalInformation additional;
result = new TechnicalInformation(Type.ARTICLE);
result.setValue(Field.AUTHOR, "Arie Ben-David");
result.setValue(Field.YEAR, "1992");
result.setValue(Field.TITLE, "Automatic Generation of Symbolic Multiattribute Ordinal Knowledge-Based DSSs: methodology and Applications");
result.setValue(Field.JOURNAL, "Decision Sciences");
result.setValue(Field.PAGES, "1357-1372");
result.setValue(Field.VOLUME, "23");
additional = result.add(Type.MASTERSTHESIS);
additional.setValue(Field.AUTHOR, "Lievens, Stijn");
additional.setValue(Field.YEAR, "2003-2004");
additional.setValue(Field.TITLE, "Studie en implementatie van instantie-gebaseerde algoritmen voor gesuperviseerd rangschikken.");
additional.setValue(Field.SCHOOL, "Ghent University");
return result;
}
/**
* Returns the tip text for this property.
*
* @return tip text for this property suitable for
* displaying in the explorer/experimenter gui
*/
public String classificationTypeTipText() {
return "Sets the classification type.";
}
/**
* Sets the classification type.
*
* @param value the classification type to be set.
*/
public void setClassificationType(SelectedTag value) {
if (value.getTags() == TAGS_CLASSIFICATIONTYPES)
m_ctype = value.getSelectedTag().getID();
}
/**
* Gets the classification type.
*
* @return the classification type
*/
public SelectedTag getClassificationType() {
return new SelectedTag(m_ctype, TAGS_CLASSIFICATIONTYPES);
}
/**
* Returns the tip text for this property.
*
* @return tip text for this property suitable for
* displaying in the explorer/experimenter gui
*/
public String averagingTypeTipText() {
return "Choses the way in which the distributions are averaged in "
+ "the first phase of the algorithm.";
}
/**
* Sets the averaging type to use in phase 1 of the algorithm.
*
* @param value the averaging type to use
*/
public void setAveragingType(SelectedTag value) {
if (value.getTags() == TAGS_AVERAGINGTYPES)
m_atype = value.getSelectedTag().getID();
}
/**
* Gets the averaging type.
*
* @return the averaging type
*/
public SelectedTag getAveragingType() {
return new SelectedTag(m_atype, TAGS_AVERAGINGTYPES);
}
/**
* Returns the tip text for this property.
*
* @return tip text for this property suitable for
* displaying in the explorer/experimenter gui
*/
public String distanceTypeTipText() {
return "Sets the distance that is to be used by the nearest neighbour "
+ "rule";
}
/**
* Sets the distance type to be used by a nearest neighbour rule (if any).
*
* @param value the distance type to use
*/
public void setDistanceType(SelectedTag value) {
if (value.getTags() == TAGS_DISTANCETYPES)
m_dtype = value.getSelectedTag().getID();
}
/**
* Gets the distance type used by a nearest neighbour rule (if any).
*
* @return the distance type
*/
public SelectedTag getDistanceType() {
return new SelectedTag(m_dtype, TAGS_DISTANCETYPES);
}
/**
* Returns the tip text for this property.
*
* @return tip text for this property suitable for
* displaying in the explorer/experimenter gui
*/
public String extensionTypeTipText() {
return "Sets the extension type to use.";
}
/**
* Sets the extension type to use.
* The minimal extension is the one used by
* Ben-David in the original algorithm. The maximal extension is
* a completely dual variant of the minimal extension. When using
* both, then the midpoint of the interval determined by both
* extensions is returned.
*
* @param value the extension type to use
*/
public void setExtensionType(SelectedTag value) {
if (value.getTags() == TAGS_EXTENSIONTYPES)
m_etype = value.getSelectedTag().getID();
}
/**
* Gets the extension type.
*
* @return the extension type
*/
public SelectedTag getExtensionType() {
return new SelectedTag(m_etype, TAGS_EXTENSIONTYPES);
}
/**
* Returns the tip text for this property.
*
* @return tip text for this property suitable for
* displaying in the explorer/experimenter gui
*/
public String sortTipText() {
return "If true, the instances are also sorted within the classes "
+ "prior to building the rule bases.";
}
/**
* Sets if the instances are to be sorted prior to building the rule bases.
*
* @param sort if <code> true </code> the instances will be sorted
*/
public void setSort(boolean sort) {
m_sort = sort;
}
/**
* Returns if the instances are sorted prior to building the rule bases.
*
* @return <code> true </code> if instances are sorted prior to building
* the rule bases, <code> false </code> otherwise.
*/
public boolean getSort() {
return m_sort;
}
/**
* Returns the tip text for this property.
*
* @return tip text for this property suitable for
* displaying in the explorer/experimenter gui
*/
public String seedTipText() {
return "Sets the seed that is used to randomize the instances prior "
+ "to building the rule bases";
}
/**
* Return the number of examples in the minimal rule base.
* The minimal rule base is the one that corresponds to the
* rule base of Ben-David.
*
* @return the number of examples in the minimal rule base
*/
public int getSizeRuleBaseMin() {
return m_baseMin.numInstances();
}
/**
* Return the number of examples in the maximal rule base.
* The maximal rule base is built using an algorithm
* dual to that for building the minimal rule base.
*
* @return the number of examples in the maximal rule base
*/
public int getSizeRuleBaseMax() {
return m_baseMax.numInstances();
}
/**
* Classifies a given instance according to the current settings
* of the classifier.
*
* @param instance the instance to be classified
* @return a <code> double </code> that represents the classification,
* this could either be the internal value of a label, when rounding is
* on, or a real number.
*/
public double classifyInstance(Instance instance) {
double classValueMin = -1;
double classValueMax = -1;
double classValue;
if (m_etype == ET_MIN || m_etype == ET_BOTH) {
classValueMin = classifyInstanceMin(instance);
}
if (m_etype == ET_MAX || m_etype == ET_BOTH) {
classValueMax = classifyInstanceMax(instance);
}
switch (m_etype) {
case ET_MIN:
classValue = classValueMin;
break;
case ET_MAX:
classValue = classValueMax;
break;
case ET_BOTH:
classValue = (classValueMin + classValueMax) / 2;
break;
default:
throw new IllegalStateException("Illegal mode type!");
}
// round if necessary and return
return (m_ctype == CT_ROUNDED ? Utils.round(classValue) : classValue);
}
/**
* Classify <code> instance </code> using the minimal rule base.
* Rounding is never performed, this is the responsability
* of <code> classifyInstance </code>.
*
* @param instance the instance to be classified
* @return the classification according to the minimal rule base
*/
private double classifyInstanceMin(Instance instance) {
double classValue = -1;
if (m_baseMin == null) {
throw new IllegalStateException
("Classifier has not yet been built");
}
Iterator it = new EnumerationIterator(m_baseMin.enumerateInstances());
while (it.hasNext()) {
Instance r = (Instance) it.next();
// we assume that rules are ordered in decreasing class value order
// so that the first one that we encounter is immediately the
// one with the biggest class value
if (InstancesUtil.smallerOrEqual(r, instance)) {
classValue = r.classValue();
break;
}
}
// there is no smaller rule in the database
if (classValue == -1) {
if (m_dtype != DT_NONE) {
Instance[] nn = nearestRules(instance, m_baseMin);
classValue = 0;
// XXX for the moment we only use the mean to extract a
// classValue; other possibilities might be included later
for (int i = 0; i < nn.length; i++) {
classValue += nn[i].classValue();
}
classValue /= nn.length;
}
else {
classValue = 0; // minimal class value
}
}
return classValue; // no rounding!
}
/**
* Classify <code> instance </code> using the maximal rule base.
* Rounding is never performed, this is the responsability
* of <code> classifyInstance </code>.
*
* @param instance the instance to be classified
* @return the classification according to the maximal rule base
*/
private double classifyInstanceMax(Instance instance) {
double classValue = -1;
if (m_baseMax == null) {
throw new IllegalStateException
("Classifier has not yet been built");
}
Iterator it = new EnumerationIterator(m_baseMax.enumerateInstances());
while (it.hasNext()) {
Instance r = (Instance) it.next();
// we assume that rules are ordered in increasing class value order
// so that the first bigger one we encounter will be the one
// with the smallest label
if (InstancesUtil.smallerOrEqual(instance, r)) {
classValue = r.classValue();
break;
}
}
// there is no bigger rule in the database
if (classValue == -1) {
if (m_dtype != DT_NONE) {
// XXX see remark in classifyInstanceMin
Instance[] nn = nearestRules(instance, m_baseMax);
classValue = 0;
for (int i = 0; i < nn.length; i++) {
classValue += nn[i].classValue();
}
classValue /= nn.length;
}
else {
classValue = m_numClasses - 1; // maximal label
}
}
return classValue;
}
/**
* Find the instances in <code> base </code> that are,
* according to the current distance type, closest
* to <code> instance </code>.
*
* @param instance the instance around which one looks
* @param base the instances to choose from
* @return an array of <code> Instance </code> which contains
* the instances closest to <code> instance </code>
*/
private Instance[] nearestRules(Instance instance, Instances base) {
double min = Double.POSITIVE_INFINITY;
double dist = 0;
double[] instanceDouble = InstancesUtil.toDataDouble(instance);
ArrayList nn = new ArrayList();
Iterator it = new EnumerationIterator(base.enumerateInstances());
while(it.hasNext()) {
Instance r = (Instance) it.next();
double[] rDouble = InstancesUtil.toDataDouble(r);
switch (m_dtype) {
case DT_EUCLID:
dist = euclidDistance(instanceDouble, rDouble);
break;
case DT_HAMMING:
dist = hammingDistance(instanceDouble, rDouble);
break;
default:
throw new IllegalArgumentException("distance type is not valid");
}
if (dist < min) {
min = dist;
nn.clear();
nn.add(r);
} else if (dist == min) {
nn.add(r);
}
}
nn.trimToSize();
return (Instance[]) nn.toArray(new Instance[0]);
}
/**
* Build the OLM classifier, meaning that the rule bases
* are built.
*
* @param instances the instances to use for building the rule base
* @throws Exception if <code> instances </code> cannot be handled by
* the classifier.
*/
public void buildClassifier(Instances instances) throws Exception {
getCapabilities().testWithFail(instances);
// copy the dataset
m_train = new Instances(instances);
m_numClasses = m_train.numClasses();
// new dataset in which examples with missing class value are removed
m_train.deleteWithMissingClass();
// build the Map for the estimatedDistributions
m_estimatedDistributions = new HashMap(m_train.numInstances() / 2);
// cycle through all instances
Iterator it = new EnumerationIterator(m_train.enumerateInstances());
while (it.hasNext() == true) {
Instance instance = (Instance) it.next();
Coordinates c = new Coordinates(instance);
// get DiscreteEstimator from the map
DiscreteEstimator df =
(DiscreteEstimator) m_estimatedDistributions.get(c);
// if no DiscreteEstimator is present in the map, create one
if (df == null) {
df = new DiscreteEstimator(instances.numClasses(), 0);
}
df.addValue(instance.classValue(), instance.weight()); // update
m_estimatedDistributions.put(c, df); // put back in map
}
// Create the attributes for m_baseMin and m_baseMax.
// These are identical to those of m_train, except that the
// class is set to 'numeric'
// The class attribute is moved to the back
FastVector newAtts = new FastVector(m_train.numAttributes());
Attribute classAttribute = null;
for (int i = 0; i < m_train.numAttributes(); i++) {
Attribute att = m_train.attribute(i);
if (i != m_train.classIndex()) {
newAtts.addElement(att.copy());
} else {
classAttribute = new Attribute(att.name()); //numeric attribute
}
}
newAtts.addElement(classAttribute);
// original training instances are replaced by an empty set
// of instances
m_train = new Instances(m_train.relationName(), newAtts,
m_estimatedDistributions.size());
m_train.setClassIndex(m_train.numAttributes() - 1);
// We cycle through the map of estimatedDistributions and
// create one Instance for each entry in the map, with
// a class value that is calculated from the distribution of
// the class values
it = m_estimatedDistributions.keySet().iterator();
while(it.hasNext()) {
// XXX attValues must be here, otherwise things go wrong
double[] attValues = new double[m_train.numAttributes()];
Coordinates cc = (Coordinates) it.next();
DiscreteEstimator df =
(DiscreteEstimator) m_estimatedDistributions.get(cc);
cc.getValues(attValues);
switch(m_atype) {
case AT_MEAN:
attValues[attValues.length - 1] = (new DiscreteDistribution(df)).mean();
break;
case AT_MEDIAN:
attValues[attValues.length - 1] = (new DiscreteDistribution(df)).median();
break;
case AT_MAXPROB:
attValues[attValues.length - 1] = (new DiscreteDistribution(df)).modes()[0];
break;
default:
throw new IllegalStateException("Not a valid averaging type");
}
// add the instance, we give it weight one
m_train.add(new Instance(1, attValues));
}
if (m_Debug == true) {
System.out.println("The dataset after phase 1 :");
System.out.println(m_train.toString());
}
/* Shuffle to training instances, to prevent the order dictated
* by the map to play an important role in how the rule bases
* are built.
*/
m_train.randomize(new Random(getSeed()));
if (m_sort == false) {
// sort the instances only in increasing class order
m_train.sort(m_train.classIndex());
} else {
// sort instances completely
Comparator[] cc = new Comparator[m_train.numAttributes()];
// sort the class, increasing
cc[0] = new InstancesComparator(m_train.classIndex());
// sort the attributes, decreasing
for (int i = 1; i < cc.length; i++) {
cc[i] = new InstancesComparator(i - 1, true);
}
// copy instances into an array
Instance[] tmp = new Instance[m_train.numInstances()];
for (int i = 0; i < tmp.length; i++) {
tmp[i] = m_train.instance(i);
}
MultiDimensionalSort.multiDimensionalSort(tmp, cc);
// copy sorted array back into Instances
m_train.delete();
for (int i = 0; i < tmp.length; i++) {
m_train.add(tmp[i]);
}
}
// phase 2: building the rule bases themselves
m_baseMin =
new Instances(m_train, m_estimatedDistributions.size() / 4);
phaseTwoMin();
m_baseMax =
new Instances(m_train, m_estimatedDistributions.size() / 4);
phaseTwoMax();
}
/**
* This implements the second phase of the OLM algorithm.
* We build the rule base m_baseMin, according to the conflict
* resolution mechanism described in the thesis.
*/
private void phaseTwoMin() {
// loop through instances backwards, this is biggest class labels first
for (int i = m_train.numInstances() - 1; i >=0; i--) {
Instance e = m_train.instance(i);
// if the example is redundant with m_base, we discard it
if (isRedundant(e) == false) {
// how many examples are redundant if we would add e
int[] redundancies = makesRedundant(e);
if (redundancies[0] == 1
&& causesReversedPreference(e) == false) {
// there is one example made redundant be e, and
// adding e doesn't cause reversed preferences
// so we replace the indicated rule by e
m_baseMin.delete(redundancies[1]);
m_baseMin.add(e);
continue;
}
if (redundancies[0] == 0) {
// adding e causes no redundancies, what about
// reversed preferences ?
int[] revPref = reversedPreferences(e);
if (revPref[0] == 1) {
// there would be one reversed preference, we
// prefer the example e because it has a lower label
m_baseMin.delete(revPref[1]);
m_baseMin.add(e);
continue;
}
if (revPref[0] == 0) {
// this means: e causes no redundancies and no
// reversed preferences. We can simply add it.
m_baseMin.add(e);
}
}
}
}
}
/**
* This implements the second phase of the OLM algorithm.
* We build the rule base m_baseMax .
*/
private void phaseTwoMax() {
// loop through instances, smallest class labels first
for (int i = 0; i < m_train.numInstances(); i++) {
Instance e = m_train.instance(i);
// if the example is redundant with m_base, we discard it
if (isRedundantMax(e) == false) {
// how many examples are redundant if we would add e
int[] redundancies = makesRedundantMax(e);
if (redundancies[0] == 1
&& causesReversedPreferenceMax(e) == false) {
// there is one example made redundant be e, and
// adding e doesn't cause reversed preferences
// so we replace the indicated rule by e
m_baseMax.delete(redundancies[1]);
m_baseMax.add(e);
continue;
}
if (redundancies[0] == 0) {
// adding e causes no redundancies, what about
// reversed preferences ?
int[] revPref = reversedPreferencesMax(e);
if (revPref[0] == 1) {
// there would be one reversed preference, we
// prefer the example e because it has a lower label
m_baseMax.delete(revPref[1]);
m_baseMax.add(e);
continue;
}
if (revPref[0] == 0) {
// this means: e causes no redundancies and no
// reversed preferences. We can simply add it.
m_baseMax.add(e);
}
}
}
}
}
/**
* Returns a string description of the classifier. In debug
* mode, the rule bases are added to the string representation
* as well. This means that the description can become rather
* lengthy.
*
* @return a <code> String </code> describing the classifier.
*/
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("OLM\n===\n\n");
if (m_etype == ET_MIN || m_etype == ET_BOTH) {
if (m_baseMin != null) {
sb.append("Number of examples in the minimal rule base = "
+ m_baseMin.numInstances() + "\n");
} else {
sb.append("minimal rule base not yet created");
}
}
if (m_etype == ET_MAX || m_etype == ET_BOTH) {
if (m_baseMax != null) {
sb.append("Number of examples in the maximal rule base = "
+ m_baseMax.numInstances() + "\n");
} else {
sb.append("maximal rule base not yet created");
}
}
if (m_Debug == true) {
if (m_etype == ET_MIN || m_etype == ET_BOTH) {
sb.append("The minimal rule base is \n");
if (m_baseMin != null) {
sb.append(m_baseMin.toString());
} else {
sb.append(".... not yet created");
}
}
if (m_etype == ET_MAX || m_etype == ET_BOTH) {
sb.append("The second rule base is \n");
if (m_baseMax != null) {
sb.append(m_baseMax.toString());
} else {
sb.append(".... not yet created");
}
}
}
sb.append("\n");
return sb.toString();
}
/**
* Is <code> instance </code> redundant wrt the
* current <code> m_baseMin </code>.
* This mean we are looking if there is an element in
* <code> m_baseMin </code> smaller than <code> instance </code>
* and with the same class.
*
* @param instance the instance of which the redundancy needs to be
* checked
* @return <code> true </code> if <code> instance </code>
* is redundant, <code> false </code> otherwise
*/
private boolean isRedundant(Instance instance) {
Iterator it = new EnumerationIterator(m_baseMin.enumerateInstances());
while(it.hasNext()) {
Instance r = (Instance) it.next();
if (instance.classValue() == r.classValue()
&& InstancesUtil.smallerOrEqual(r, instance) ) {
return true;
}
}
return false;
}
/**
* Is <code> instance </code> redundant wrt the
* current <code> m_baseMax </code>.
* This is dual to the previous method, this means that
* <code> instance </code> is redundant if there exists
* and example greater of equal than <code> instance </code>
* with the same class value.
*
* @param instance the instance of which the redundancy needs to be
* checked
* @return <code> true </code> if <code> instance </code>
* is redundant, <code> false </code> otherwise
*/
private boolean isRedundantMax(Instance instance) {
Iterator it = new EnumerationIterator(m_baseMax.enumerateInstances());
while(it.hasNext()) {
Instance r = (Instance) it.next();
if (instance.classValue() == r.classValue()
&& InstancesUtil.smallerOrEqual(instance, r) ) {
return true;
}
}
return false;
}
/**
* If we were to add <code> instance </code>, how many
* and which elements from <code> m_baseMin </code> would
* be made redundant by <code> instance </code>.
*
* @param instance the instance of which the influence is to be determined
* @return an array containing two components
* [0] is 0 if instance makes no elements redundant;
* is 1 if there is one rule that is made redundant by instance;
* is 2 if there is more than one rule that is made redundant;
* [1] if [0] == 1, this entry gives the element that is made redundant
*/
private int[] makesRedundant(Instance instance) {
int[] ret = new int[2];
for (int i = 0; i < m_baseMin.numInstances(); i++) {
Instance r = m_baseMin.instance(i);
if (r.classValue() == instance.classValue() &&
InstancesUtil.smallerOrEqual(instance, r)) {
if (ret[0] == 0) {
ret[0] = 1;
ret[1] = i;
} else { // this means ret[0] == 1
ret[0] = 2;
return ret;
}
}
}
return ret;
}
/**
* If we were to add <code> instance </code>, how many
* and which elements from <code> m_baseMax </code> would
* be made redundant by <code> instance </code>.
* This method is simply dual to <code> makesRedundant </code>.
*
* @param instance the instance to add
* @return an array containing two components
* [0] is 0 if instance makes no elements redundant;
* is 1 if there is one rule that is made redundant by instance;
* is 2 if there is more than one rule that is made redundant;
* [1] if [0] == 1, this entry gives the element that is made redundant
*/
private int[] makesRedundantMax(Instance instance) {
int[] ret = new int[2];
for (int i = 0; i < m_baseMax.numInstances(); i++) {
Instance r = m_baseMax.instance(i);
if (r.classValue() == instance.classValue() &&
InstancesUtil.smallerOrEqual(r, instance)) {
if (ret[0] == 0) {
ret[0] = 1;
ret[1] = i;
} else { // this means ret[0] == 1
ret[0] = 2;
return ret;
}
}
}
return ret;
}
/**
* Checks if adding <code> instance </code> to <code> m_baseMin </code>
* causes reversed preference amongst the elements of <code>
* m_baseMin </code>.
*
* @param instance the instance of which the influence needs to be
* determined
* @return <code> true </code> if adding <code> instance </code>
* to the rule base would cause reversed preference among the rules,
* <code> false </code> otherwise.
*/
private boolean causesReversedPreference(Instance instance) {
Iterator it = new EnumerationIterator(m_baseMin.enumerateInstances());
while (it.hasNext()) {
Instance r = (Instance) it.next();
if (instance.classValue() > r.classValue() &&
InstancesUtil.smallerOrEqual(instance, r)) {
// in the original version of OLM this should not happen
System.err.println
("Should not happen in the original OLM algorithm");
return true;
} else if (r.classValue() > instance.classValue() &&
InstancesUtil.smallerOrEqual(r, instance)) {
return true;
}
}
return false;
}
/**
* Checks if adding <code> instance </code> to <code> m_baseMax </code>
* causes reversed preference amongst the elements of
* <code> m_baseMax </code>.
*
* @param instance the instance to add
* @return true if adding <code> instance </code> to the rule
* base would cause reversed preference among the rules,
* false otherwise.
*/
private boolean causesReversedPreferenceMax(Instance instance) {
Iterator it = new EnumerationIterator(m_baseMax.enumerateInstances());
while (it.hasNext()) {
Instance r = (Instance) it.next();
if (instance.classValue() > r.classValue() &&
InstancesUtil.smallerOrEqual(instance, r)) {
return true;
} else if (r.classValue() > instance.classValue() &&
InstancesUtil.smallerOrEqual(r, instance)) {
return true;
}
}
return false;
}
/**
* Find indices of the elements from <code> m_baseMin </code>
* that are inconsistent with instance.
*
* @param instance the instance of which the influence needs to be
* determined
* @return an array containing two components
* [0] is 0 if instance is consistent with all present elements
* is 1 if instance is inconsistent (reversed preference) with
* exactly one element
* is 2 if instance is inconsistent with more than one element
* [1] if [0] == 1, this entry gives the index of the
* element that has reversed preference wrt to
* <code> instance </code>
*/
private int[] reversedPreferences(Instance instance) {
int[] revPreferences = new int[2];
for (int i = 0; i < m_baseMin.numInstances(); i++) {
Instance r = m_baseMin.instance(i);
if (instance.classValue() < r.classValue() &&
InstancesUtil.smallerOrEqual(r, instance) ) {
if (revPreferences[0] == 0) {
revPreferences[0] = 1;
revPreferences[1] = i;
} else {
revPreferences[0] = 2;
return revPreferences;
}
}
}
return revPreferences;
}
/**
* Find indices of the elements from <code> m_baseMin </code>
* that are inconsistent with instance.
*
* @param instance the instance of which the influence needs to be
* determined
* @return an array containing two components
* [0] is 0 if instance is consistent with all present elements
* is 1 if instance is inconsistent (reversed preference) with
* exactly one element
* is 2 if instance is inconsistent with more than one element
* [1] if [0] == 1, this entry gives the index of the
* element that has reversed preference wrt to
* <code> instance </code>
*/
private int[] reversedPreferencesMax(Instance instance) {
int[] revPreferences = new int[2];
for (int i = 0; i < m_baseMax.numInstances(); i++) {
Instance r = m_baseMax.instance(i);
if (instance.classValue() > r.classValue() &&
InstancesUtil.smallerOrEqual(instance, r) ) {
if (revPreferences[0] == 0) {
revPreferences[0] = 1;
revPreferences[1] = i;
} else {
revPreferences[0] = 2;
return revPreferences;
}
}
}
return revPreferences;
}
/**
* Calculates the square of the Euclidian distance between
* two arrays of doubles. The arrays should have the same length.
*
* @param a1 the first array
* @param a2 the second array
* @return the square of the Euclidian distance between the two
* arrays <code> a1 </code> and <code> a2 </code>
*/
private double euclidDistance(double[] a1, double[] a2) {
double dist = 0;
for (int i = 0; i < a1.length; i++) {
dist += (a1[i] - a2[i]) * (a1[i] - a2[i]);
}
return dist;
}
/**
* Calculates the Hamming distances between two arrays, this
* means one counts the number of positions in which these
* two array differ. The arrays should be of the same length.
*
* @param a1 the first array
* @param a2 the second array
* @return the requested Hamming distance
* The Hamming distance between a1 and a2, this is
* the number of positions in which a1 and a2 differ
*/
private int hammingDistance(double[] a1, double[] a2) {
int dist = 0;
for (int i = 0; i < a1.length; i++) {
dist += (a1[i] == a2[i]) ? 0 : 1;
}
return dist;
}
/**
* Get an enumeration of all available options for this classifier.
*
* @return an enumeration of available options
*/
public Enumeration listOptions() {
Vector options = new Vector();
Enumeration enm = super.listOptions();
while (enm.hasMoreElements())
options.addElement(enm.nextElement());
String description =
"\tSets the classification type to be used.\n" +
"\t(Default: " + new SelectedTag(CT_REAL, TAGS_CLASSIFICATIONTYPES) + ")";
String synopsis = "-C " + Tag.toOptionList(TAGS_CLASSIFICATIONTYPES);
String name = "C";
options.addElement(new Option(description, name, 1, synopsis));
description =
"\tSets the averaging type used in phase 1 of the classifier.\n" +
"\t(Default: " + new SelectedTag(AT_MEAN, TAGS_AVERAGINGTYPES) + ")";
synopsis = "-A " + Tag.toOptionList(TAGS_AVERAGINGTYPES);
name = "A";
options.addElement(new Option(description, name, 1, synopsis));
description =
"\tIf different from " + new SelectedTag(DT_NONE, TAGS_DISTANCETYPES) + ", a nearest neighbour rule is fired when the\n" +
"\trule base doesn't contain an example smaller than the instance\n" +
"\tto be classified\n" +
"\t(Default: " + new SelectedTag(DT_NONE, TAGS_DISTANCETYPES) + ").";
synopsis = "-N " + Tag.toOptionList(TAGS_DISTANCETYPES);
name = "N";
options.addElement(new Option(description, name, 1, synopsis));
description = "\tSets the extension type, i.e. the rule base to use."
+ "\n\t(Default: " + new SelectedTag(ET_MIN, TAGS_EXTENSIONTYPES) + ")";
synopsis = "-E " + Tag.toOptionList(TAGS_EXTENSIONTYPES);
name = "E";
options.addElement(new Option(description, name, 1, synopsis));
description =
"\tIf set, the instances are also sorted within the same class\n"
+ "\tbefore building the rule bases";
synopsis = "-sort";
name = "sort";
options.addElement(new Option(description, name, 0, synopsis));
return options.elements();
}
/**
* Parses the options for this object. <p/>
*
<!-- options-start -->
* Valid options are: <p/>
*
* <pre> -S <num>
* Random number seed.
* (default 1)</pre>
*
* <pre> -D
* If set, classifier is run in debug mode and
* may output additional info to the console</pre>
*
* <pre> -C <CL|REG>
* Sets the classification type to be used.
* (Default: REG)</pre>
*
* <pre> -A <MEAN|MED|MAX>
* Sets the averaging type used in phase 1 of the classifier.
* (Default: MEAN)</pre>
*
* <pre> -N <NONE|EUCL|HAM>
* If different from NONE, a nearest neighbour rule is fired when the
* rule base doesn't contain an example smaller than the instance
* to be classified
* (Default: NONE).</pre>
*
* <pre> -E <MIN|MAX|BOTH>
* Sets the extension type, i.e. the rule base to use.
* (Default: MIN)</pre>
*
* <pre> -sort
* If set, the instances are also sorted within the same class
* before building the rule bases</pre>
*
<!-- options-end -->
*
* @param options an array of strings containing the options
* @throws Exception if there are options that have invalid arguments.
*/
public void setOptions(String[] options) throws Exception {
String args;
// classification type
args = Utils.getOption('C', options);
if (args.length() != 0)
setClassificationType(new SelectedTag(args, TAGS_CLASSIFICATIONTYPES));
else
setClassificationType(new SelectedTag(CT_REAL, TAGS_CLASSIFICATIONTYPES));
// averaging type
args = Utils.getOption('A', options);
if (args.length() != 0)
setAveragingType(new SelectedTag(args, TAGS_AVERAGINGTYPES));
else
setAveragingType(new SelectedTag(AT_MEAN, TAGS_AVERAGINGTYPES));
// distance type
args = Utils.getOption('N', options);
if (args.length() != 0)
setDistanceType(new SelectedTag(args, TAGS_DISTANCETYPES));
else
setDistanceType(new SelectedTag(DT_NONE, TAGS_DISTANCETYPES));
// extension type
args = Utils.getOption('E', options);
if (args.length() != 0)
setExtensionType(new SelectedTag(args, TAGS_EXTENSIONTYPES));
else
setExtensionType(new SelectedTag(ET_MIN, TAGS_EXTENSIONTYPES));
// sort ?
setSort(Utils.getFlag("sort", options));
super.setOptions(options);
}
/**
* Gets an array of string with the current options of the classifier.
*
* @return an array suitable as argument for <code> setOptions </code>
*/
public String[] getOptions() {
int i;
Vector result;
String[] options;
result = new Vector();
options = super.getOptions();
for (i = 0; i < options.length; i++)
result.add(options[i]);
// classification type
result.add("-C");
result.add("" + getClassificationType());
result.add("-A");
result.add("" + getAveragingType());
result.add("-N");
result.add("" + getDistanceType());
result.add("-E");
result.add("" + getExtensionType());
if (getSort())
result.add("-sort");
return (String[]) result.toArray(new String[result.size()]);
}
/**
* Returns the revision string.
*
* @return the revision
*/
public String getRevision() {
return RevisionUtils.extract("$Revision: 1.2 $");
}
/**
* Main method for testing this class.
*
* @param args the command line arguments
*/
public static void main(String[] args) {
runClassifier(new OLM(), args);
}
}