/* * 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. */ /* * MultiClassClassifier.java * Copyright (C) 1999 Eibe Frank,Len Trigg * */ package weka.classifiers.meta; import weka.classifiers.DistributionClassifier; import weka.classifiers.Classifier; import weka.classifiers.Evaluation; import weka.classifiers.rules.ZeroR; import java.io.Serializable; import java.util.Enumeration; import java.util.Random; import java.util.Vector; import weka.core.Attribute; import weka.core.AttributeStats; import weka.core.Instance; import weka.core.Instances; import weka.core.Option; import weka.core.OptionHandler; import weka.core.SelectedTag; import weka.core.Tag; import weka.core.Utils; import weka.core.FastVector; import weka.core.Range; import weka.filters.unsupervised.attribute.MakeIndicator; import weka.filters.unsupervised.instance.RemoveWithValues; import weka.filters.Filter; /** * Class for handling multi-class datasets with 2-class distribution * classifiers.<p> * * Valid options are:<p> * * -M num <br> * Sets the method to use. Valid values are 0 (1-against-all), * 1 (random codes), 2 (exhaustive code), and 3 (1-against-1). (default 0) <p> * * -R num <br> * Sets the multiplier when using random codes. (default 2.0)<p> * * -W classname <br> * Specify the full class name of a classifier as the basis for * the multi-class classifier (required).<p> * * -Q seed <br> * Random number seed (default 1).<p> * * @author Eibe Frank (eibe@cs.waikato.ac.nz) * @author Len Trigg (len@reeltwo.com) * @author Richard Kirkby (rkirkby@cs.waikato.ac.nz) * @version $Revision: 1.1.1.1 $ */ public class MultiClassClassifier extends DistributionClassifier implements OptionHandler { /** The classifiers. */ private Classifier [] m_Classifiers; /** The filters used to transform the class. */ private Filter[] m_ClassFilters; /** The class name of the base classifier. */ private DistributionClassifier m_Classifier = new weka.classifiers.rules.ZeroR(); /** ZeroR classifier for when all base classifier return zero probability. */ private ZeroR m_ZeroR; /** Internal copy of the class attribute for output purposes */ private Attribute m_ClassAttribute; /** A transformed dataset header used by the 1-against-1 method */ private Instances m_TwoClassDataset; /** Random number seed */ protected int m_Seed = 1; /** * The multiplier when generating random codes. Will generate * numClasses * m_RandomWidthFactor codes */ private double m_RandomWidthFactor = 2.0; /** The multiclass method to use */ private int m_Method = METHOD_1_AGAINST_ALL; /** The error correction modes */ public static final int METHOD_1_AGAINST_ALL = 0; public static final int METHOD_ERROR_RANDOM = 1; public static final int METHOD_ERROR_EXHAUSTIVE = 2; public static final int METHOD_1_AGAINST_1 = 3; public static final Tag [] TAGS_METHOD = { new Tag(METHOD_1_AGAINST_ALL, "1-against-all"), new Tag(METHOD_ERROR_RANDOM, "Random correction code"), new Tag(METHOD_ERROR_EXHAUSTIVE, "Exhaustive correction code"), new Tag(METHOD_1_AGAINST_1, "1-against-1") }; /** Interface for the code constructors */ private abstract class Code implements Serializable { /** * Subclasses must allocate and fill these. * First dimension is number of codes. * Second dimension is number of classes. */ protected boolean [][]m_Codebits; /** Returns the number of codes. */ public int size() { return m_Codebits.length; } /** * Returns the indices of the values set to true for this code, * using 1-based indexing (for input to Range). */ public String getIndices(int which) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < m_Codebits[which].length; i++) { if (m_Codebits[which][i]) { if (sb.length() != 0) { sb.append(','); } sb.append(i + 1); } } return sb.toString(); } /** Returns a human-readable representation of the codes. */ public String toString() { StringBuffer sb = new StringBuffer(); for(int i = 0; i < m_Codebits[0].length; i++) { for (int j = 0; j < m_Codebits.length; j++) { sb.append(m_Codebits[j][i] ? " 1" : " 0"); } sb.append('\n'); } return sb.toString(); } } /** Constructs a code with no error correction */ private class StandardCode extends Code { public StandardCode(int numClasses) { m_Codebits = new boolean[numClasses][numClasses]; for (int i = 0; i < numClasses; i++) { m_Codebits[i][i] = true; } System.err.println("Code:\n" + this); } } /** Constructs a random code assignment */ private class RandomCode extends Code { Random r = new Random(m_Seed); public RandomCode(int numClasses, int numCodes) { numCodes = Math.max(numClasses, numCodes); m_Codebits = new boolean[numCodes][numClasses]; int i = 0; do { randomize(); //System.err.println(this); } while (!good() && (i++ < 100)); System.err.println("Code:\n" + this); } private boolean good() { boolean [] ninClass = new boolean[m_Codebits[0].length]; boolean [] ainClass = new boolean[m_Codebits[0].length]; for (int i = 0; i < ainClass.length; i++) { ainClass[i] = true; } for (int i = 0; i < m_Codebits.length; i++) { boolean ninCode = false; boolean ainCode = true; for (int j = 0; j < m_Codebits[i].length; j++) { boolean current = m_Codebits[i][j]; ninCode = ninCode || current; ainCode = ainCode && current; ninClass[j] = ninClass[j] || current; ainClass[j] = ainClass[j] && current; } if (!ninCode || ainCode) { return false; } } for (int j = 0; j < ninClass.length; j++) { if (!ninClass[j] || ainClass[j]) { return false; } } return true; } private void randomize() { for (int i = 0; i < m_Codebits.length; i++) { for (int j = 0; j < m_Codebits[i].length; j++) { double temp = r.nextDouble(); m_Codebits[i][j] = (temp < 0.5) ? false : true; } } } } /** * TODO: Constructs codes as per: * Bose, R.C., Ray Chaudhuri (1960), On a class of error-correcting * binary group codes, Information and Control, 3, 68-79. * Hocquenghem, A. (1959) Codes corecteurs d'erreurs, Chiffres, 2, 147-156. */ //private class BCHCode extends Code {...} /** Constructs an exhaustive code assignment */ private class ExhaustiveCode extends Code { public ExhaustiveCode(int numClasses) { int width = (int)Math.pow(2, numClasses - 1) - 1; m_Codebits = new boolean[width][numClasses]; for (int j = 0; j < width; j++) { m_Codebits[j][0] = true; } for (int i = 1; i < numClasses; i++) { int skip = (int) Math.pow(2, numClasses - (i + 1)); for(int j = 0; j < width; j++) { m_Codebits[j][i] = ((j / skip) % 2 != 0); } } System.err.println("Code:\n" + this); } } /** * Builds the classifiers. * * @param insts the training data. * @exception Exception if a classifier can't be built */ public void buildClassifier(Instances insts) throws Exception { Instances newInsts; if (m_Classifier == null) { throw new Exception("No base classifier has been set!"); } m_ZeroR = new ZeroR(); m_ZeroR.buildClassifier(insts); m_TwoClassDataset = null; int numClassifiers = insts.numClasses(); if (numClassifiers <= 2) { m_Classifiers = Classifier.makeCopies(m_Classifier, 1); m_Classifiers[0].buildClassifier(insts); m_ClassFilters = null; } else if (m_Method == METHOD_1_AGAINST_1) { // generate fastvector of pairs FastVector pairs = new FastVector(); for (int i=0; i<insts.numClasses(); i++) { for (int j=0; j<insts.numClasses(); j++) { if (j<=i) continue; int[] pair = new int[2]; pair[0] = i; pair[1] = j; pairs.addElement(pair); } } numClassifiers = pairs.size(); m_Classifiers = Classifier.makeCopies(m_Classifier, numClassifiers); m_ClassFilters = new Filter[numClassifiers]; // generate the classifiers for (int i=0; i<numClassifiers; i++) { RemoveWithValues classFilter = new RemoveWithValues(); classFilter.setAttributeIndex(insts.classIndex()); classFilter.setModifyHeader(true); classFilter.setInvertSelection(false); classFilter.setNominalIndicesArr((int[])pairs.elementAt(i)); int[] pair = (int[])pairs.elementAt(i); Instances tempInstances = new Instances(insts, 0); tempInstances.setClassIndex(-1); classFilter.setInputFormat(tempInstances); newInsts = Filter.useFilter(insts, classFilter); newInsts.setClassIndex(insts.classIndex()); m_Classifiers[i].buildClassifier(newInsts); m_ClassFilters[i] = classFilter; } // construct a two-class header version of the dataset m_TwoClassDataset = new Instances(insts, 0); int classIndex = m_TwoClassDataset.classIndex(); m_TwoClassDataset.setClassIndex(-1); m_TwoClassDataset.deleteAttributeAt(classIndex); FastVector classLabels = new FastVector(); classLabels.addElement("class0"); classLabels.addElement("class1"); m_TwoClassDataset.insertAttributeAt(new Attribute("class", classLabels), classIndex); m_TwoClassDataset.setClassIndex(classIndex); } else { // use error correcting code style methods Code code = null; switch (m_Method) { case METHOD_ERROR_EXHAUSTIVE: code = new ExhaustiveCode(numClassifiers); break; case METHOD_ERROR_RANDOM: code = new RandomCode(numClassifiers, (int)(numClassifiers * m_RandomWidthFactor)); break; case METHOD_1_AGAINST_ALL: code = new StandardCode(numClassifiers); break; default: throw new Exception("Unrecognized correction code type"); } numClassifiers = code.size(); m_Classifiers = Classifier.makeCopies(m_Classifier, numClassifiers); m_ClassFilters = new MakeIndicator[numClassifiers]; AttributeStats classStats = insts.attributeStats(insts.classIndex()); for (int i = 0; i < m_Classifiers.length; i++) { if ((m_Method == METHOD_1_AGAINST_ALL) && (classStats.nominalCounts[i] == 0)) { m_Classifiers[i] = null; } else { m_ClassFilters[i] = new MakeIndicator(); MakeIndicator classFilter = (MakeIndicator) m_ClassFilters[i]; classFilter.setAttributeIndex(insts.classIndex()); classFilter.setValueIndices(code.getIndices(i)); classFilter.setNumeric(false); classFilter.setInputFormat(insts); newInsts = Filter.useFilter(insts, m_ClassFilters[i]); m_Classifiers[i].buildClassifier(newInsts); } } } m_ClassAttribute = insts.classAttribute(); } /** * Returns the individual predictions of the base classifiers * for an instance. Used by StackedMultiClassClassifier. * Returns the probability for the second "class" predicted * by each base classifier. * * @exception Exception if the predictions can't be computed successfully */ public double[] individualPredictions(Instance inst) throws Exception { double[] result = null; if (m_Classifiers.length == 1) { result = new double[1]; result[0] = ((DistributionClassifier)m_Classifiers[0]) .distributionForInstance(inst)[1]; } else { result = new double[m_ClassFilters.length]; for(int i = 0; i < m_ClassFilters.length; i++) { if (m_Classifiers[i] != null) { if (m_Method == METHOD_1_AGAINST_1) { Instance tempInst = new Instance(inst); tempInst.setDataset(m_TwoClassDataset); result[i] = ((DistributionClassifier)m_Classifiers[i]) .distributionForInstance(tempInst)[1]; } else { m_ClassFilters[i].input(inst); m_ClassFilters[i].batchFinished(); result[i] = ((DistributionClassifier)m_Classifiers[i]) .distributionForInstance(m_ClassFilters[i].output())[1]; } } } } return result; } /** * Returns the distribution for an instance. * * @exception Exception if the distribution can't be computed successfully */ public double[] distributionForInstance(Instance inst) throws Exception { if (m_Classifiers.length == 1) { return ((DistributionClassifier)m_Classifiers[0]) .distributionForInstance(inst); } double[] probs = new double[inst.numClasses()]; if (m_Method == METHOD_1_AGAINST_1) { for(int i = 0; i < m_ClassFilters.length; i++) { if (m_Classifiers[i] != null) { Instance tempInst = new Instance(inst); tempInst.setDataset(m_TwoClassDataset); double [] current = ((DistributionClassifier)m_Classifiers[i]) .distributionForInstance(tempInst); Range range = new Range(((RemoveWithValues)m_ClassFilters[i]) .getNominalIndices()); range.setUpper(m_ClassAttribute.numValues()); int[] pair = range.getSelection(); if (current[0] > current[1]) probs[pair[0]] += 1.0; else if (current[1] > current[0]) probs[pair[1]] += 1.0; } } } else { // error correcting style methods for(int i = 0; i < m_ClassFilters.length; i++) { if (m_Classifiers[i] != null) { m_ClassFilters[i].input(inst); m_ClassFilters[i].batchFinished(); double [] current = ((DistributionClassifier)m_Classifiers[i]) .distributionForInstance(m_ClassFilters[i].output()); for (int j = 0; j < m_ClassAttribute.numValues(); j++) { if (((MakeIndicator)m_ClassFilters[i]).getValueRange().isInRange(j)) { probs[j] += current[1]; } else { probs[j] += current[0]; } } } } } if (Utils.gr(Utils.sum(probs), 0)) { Utils.normalize(probs); return probs; } else { return m_ZeroR.distributionForInstance(inst); } } /** * Prints the classifiers. */ public String toString() { if (m_Classifiers == null) { return "MultiClassClassifier: No model built yet."; } StringBuffer text = new StringBuffer(); text.append("MultiClassClassifier\n\n"); for (int i = 0; i < m_Classifiers.length; i++) { text.append("Classifier ").append(i + 1); if (m_Classifiers[i] != null) { if ((m_ClassFilters != null) && (m_ClassFilters[i] != null)) { if (m_ClassFilters[i] instanceof RemoveWithValues) { Range range = new Range(((RemoveWithValues)m_ClassFilters[i]) .getNominalIndices()); range.setUpper(m_ClassAttribute.numValues()); int[] pair = range.getSelection(); text.append(", " + (pair[0]+1) + " vs " + (pair[1]+1)); } else if (m_ClassFilters[i] instanceof MakeIndicator) { text.append(", using indicator values: "); text.append(((MakeIndicator)m_ClassFilters[i]).getValueRange()); } } text.append('\n'); text.append(m_Classifiers[i].toString() + "\n\n"); } else { text.append(" Skipped (no training examples)\n"); } } return text.toString(); } /** * Returns an enumeration describing the available options * * @return an enumeration of all the available options */ public Enumeration listOptions() { Vector vec = new Vector(3); Object c; vec.addElement(new Option( "\tSets the method to use. Valid values are 0 (1-against-all),\n" +"\t1 (random codes), 2 (exhaustive code), and 3 (1-against-1). (default 0)\n", "M", 1, "-M <num>")); vec.addElement(new Option( "\tSets the multiplier when using random codes. (default 2.0)", "R", 1, "-R <num>")); vec.addElement(new Option( "\tSets the base classifier.", "W", 1, "-W <base classifier>")); vec.addElement(new Option( "\tSets the random number seed for random codes.", "Q", 1, "-Q <random number seed>")); if (m_Classifier != null) { try { vec.addElement(new Option("", "", 0, "\nOptions specific to classifier " + m_Classifier.getClass().getName() + ":")); Enumeration enum = ((OptionHandler)m_Classifier).listOptions(); while (enum.hasMoreElements()) { vec.addElement(enum.nextElement()); } } catch (Exception e) { } } return vec.elements(); } /** * Parses a given list of options. Valid options are:<p> * * -M num <br> * Sets the method to use. Valid values are 0 (1-against-all), * 1 (random codes), 2 (exhaustive code), and 3 (1-against-1). (default 0) <p> * * -R num <br> * Sets the multiplier when using random codes. (default 2.0)<p> * * -W classname <br> * Specify the full class name of a learner as the basis for * the multiclassclassifier (required).<p> * * -Q seed <br> * Random number seed (default 1).<p> * * @param options the list of options as an array of strings * @exception Exception if an option is not supported */ public void setOptions(String[] options) throws Exception { String errorString = Utils.getOption('M', options); if (errorString.length() != 0) { setMethod(new SelectedTag(Integer.parseInt(errorString), TAGS_METHOD)); } else { setMethod(new SelectedTag(METHOD_1_AGAINST_ALL, TAGS_METHOD)); } String rfactorString = Utils.getOption('R', options); if (rfactorString.length() != 0) { setRandomWidthFactor((new Double(rfactorString)).doubleValue()); } else { setRandomWidthFactor(2.0); } String randomString = Utils.getOption('Q', options); if (randomString.length() != 0) { setSeed(Integer.parseInt(randomString)); } else { setSeed(1); } String classifierName = Utils.getOption('W', options); if (classifierName.length() == 0) { throw new Exception("A classifier must be specified with" + " the -W option."); } setDistributionClassifier((DistributionClassifier) Classifier.forName(classifierName, Utils.partitionOptions(options))); } /** * Gets the current settings of the Classifier. * * @return an array of strings suitable for passing to setOptions */ public String [] getOptions() { String [] classifierOptions = new String [0]; if ((m_Classifier != null) && (m_Classifier instanceof OptionHandler)) { classifierOptions = ((OptionHandler)m_Classifier).getOptions(); } String [] options = new String [classifierOptions.length + 9]; int current = 0; options[current++] = "-M"; options[current++] = "" + m_Method; options[current++] = "-R"; options[current++] = "" + m_RandomWidthFactor; options[current++] = "-Q"; options[current++] = "" + getSeed(); if (getDistributionClassifier() != null) { options[current++] = "-W"; options[current++] = getDistributionClassifier().getClass().getName(); } options[current++] = "--"; System.arraycopy(classifierOptions, 0, options, current, classifierOptions.length); current += classifierOptions.length; while (current < options.length) { options[current++] = ""; } return options; } /** * @return a description of the classifier suitable for * displaying in the explorer/experimenter gui */ public String globalInfo() { return "A metaclassifier for handling multi-class datasets with 2-class " + "distribution classifiers. This classifier is also capable of " + "applying error correcting output codes for increased accuracy."; } /** * @return tip text for this property suitable for * displaying in the explorer/experimenter gui */ public String randomWidthFactorTipText() { return "Sets the width multiplier when using random codes. The number " + "of codes generated will be thus number multiplied by the number of " + "classes."; } /** * Gets the multiplier when generating random codes. Will generate * numClasses * m_RandomWidthFactor codes. * * @return the width multiplier */ public double getRandomWidthFactor() { return m_RandomWidthFactor; } /** * Sets the multiplier when generating random codes. Will generate * numClasses * m_RandomWidthFactor codes. * * @param newRandomWidthFactor the new width multiplier */ public void setRandomWidthFactor(double newRandomWidthFactor) { m_RandomWidthFactor = newRandomWidthFactor; } /** * @return tip text for this property suitable for * displaying in the explorer/experimenter gui */ public String methodTipText() { return "Sets the method to use for transforming the multi-class problem into " + "several 2-class ones."; } /** * Gets the method used. Will be one of METHOD_1_AGAINST_ALL, * METHOD_ERROR_RANDOM, METHOD_ERROR_EXHAUSTIVE, or METHOD_1_AGAINST_1. * * @return the current method. */ public SelectedTag getMethod() { return new SelectedTag(m_Method, TAGS_METHOD); } /** * Sets the method used. Will be one of METHOD_1_AGAINST_ALL, * METHOD_ERROR_RANDOM, METHOD_ERROR_EXHAUSTIVE, or METHOD_1_AGAINST_1. * * @param newMethod the new method. */ public void setMethod(SelectedTag newMethod) { if (newMethod.getTags() == TAGS_METHOD) { m_Method = newMethod.getSelectedTag().getID(); } } /** * @return tip text for this property suitable for * displaying in the explorer/experimenter gui */ public String distributionClassifierTipText() { return "Sets the DistributionClassifier used as the basis for " + "the multi-class classifier."; } /** * Set the base classifier. * * @param newClassifier the Classifier to use. */ public void setDistributionClassifier(DistributionClassifier newClassifier) { m_Classifier = newClassifier; } /** * Get the classifier used as the classifier * * @return the classifier used as the classifier */ public DistributionClassifier getDistributionClassifier() { return m_Classifier; } /** * Sets the seed for random number generation. * * @param seed the random number seed */ public void setSeed(int seed) { m_Seed = seed;; } /** * Gets the random number seed. * * @return the random number seed */ public int getSeed() { return m_Seed; } /** * Main method for testing this class. * * @param argv the options */ public static void main(String [] argv) { DistributionClassifier scheme; try { scheme = new MultiClassClassifier(); System.out.println(Evaluation.evaluateModel(scheme, argv)); } catch (Exception e) { System.err.println(e.getMessage()); e.printStackTrace(); } } }