/* * 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. */ /* * VFI.java * Copyright (C) 2000 Mark Hall. * */ package weka.classifiers.misc; import weka.classifiers.Evaluation; import weka.classifiers.Classifier; import weka.classifiers.DistributionClassifier; import weka.core.Attribute; import weka.core.Instance; import weka.core.Instances; import weka.core.Utils; import weka.core.OptionHandler; import weka.core.Option; import weka.core.WeightedInstancesHandler; import weka.core.UnsupportedClassTypeException; import java.io.*; import java.util.Enumeration; import java.util.Vector; /** * Class implementing the voting feature interval classifier. For numeric * attributes, upper and lower boundaries (intervals) are constructed * around each class. Discrete attributes have point intervals. Class counts * are recorded for each interval on each feature. Classification is by * voting. Missing values are ignored. Does not handle numeric class. <p> * * Have added a simple attribute weighting scheme. Higher weight is assigned * to more confident intervals, where confidence is a function of entropy: * weight (att_i) = (entropy of class distrib att_i / max uncertainty)^-bias. * <p> * * Faster than NaiveBayes but slower than HyperPipes. <p><p> * * <pre> * Confidence: 0.01 (two tailed) * * Dataset (1) VFI '-B | (2) Hyper (3) Naive * ------------------------------------ * anneal.ORIG (10) 74.56 | 97.88 v 74.77 * anneal (10) 71.83 | 97.88 v 86.51 v * audiology (10) 51.69 | 66.26 v 72.25 v * autos (10) 57.63 | 62.79 v 57.76 * balance-scale (10) 68.72 | 46.08 * 90.5 v * breast-cancer (10) 67.25 | 69.84 v 73.12 v * wisconsin-breast-cancer (10) 95.72 | 88.31 * 96.05 v * horse-colic.ORIG (10) 66.13 | 70.41 v 66.12 * horse-colic (10) 78.36 | 62.07 * 78.28 * credit-rating (10) 85.17 | 44.58 * 77.84 * * german_credit (10) 70.81 | 69.89 * 74.98 v * pima_diabetes (10) 62.13 | 65.47 v 75.73 v * Glass (10) 56.82 | 50.19 * 47.43 * * cleveland-14-heart-diseas (10) 80.01 | 55.18 * 83.83 v * hungarian-14-heart-diseas (10) 82.8 | 65.55 * 84.37 v * heart-statlog (10) 79.37 | 55.56 * 84.37 v * hepatitis (10) 83.78 | 63.73 * 83.87 * hypothyroid (10) 92.64 | 93.33 v 95.29 v * ionosphere (10) 94.16 | 35.9 * 82.6 * * iris (10) 96.2 | 91.47 * 95.27 * * kr-vs-kp (10) 88.22 | 54.1 * 87.84 * * labor (10) 86.73 | 87.67 93.93 v * lymphography (10) 78.48 | 58.18 * 83.24 v * mushroom (10) 99.85 | 99.77 * 95.77 * * primary-tumor (10) 29 | 24.78 * 49.35 v * segment (10) 77.42 | 75.15 * 80.1 v * sick (10) 65.92 | 93.85 v 92.71 v * sonar (10) 58.02 | 57.17 67.97 v * soybean (10) 86.81 | 86.12 * 92.9 v * splice (10) 88.61 | 41.97 * 95.41 v * vehicle (10) 52.94 | 32.77 * 44.8 * * vote (10) 91.5 | 61.38 * 90.19 * * vowel (10) 57.56 | 36.34 * 62.81 v * waveform (10) 56.33 | 46.11 * 80.02 v * zoo (10) 94.05 | 94.26 95.04 v * ------------------------------------ * (v| |*) | (9|3|23) (22|5|8) * </pre> * <p> * * For more information, see <p> * * Demiroz, G. and Guvenir, A. (1997) "Classification by voting feature * intervals", <i>ECML-97</i>. <p> * * Valid options are:<p> * * -C <br> * Don't Weight voting intervals by confidence. <p> * * -B <bias> <br> * Set exponential bias towards confident intervals. default = 1.0 <p> * * @author Mark Hall (mhall@cs.waikato.ac.nz) * @version $Revision: 1.1.1.1 $ */ public class VFI extends DistributionClassifier implements OptionHandler, WeightedInstancesHandler { /** The index of the class attribute */ protected int m_ClassIndex; /** The number of classes */ protected int m_NumClasses; /** The training data */ protected Instances m_Instances = null; /** The class counts for each interval of each attribute */ protected double [][][] m_counts; /** The global class counts */ protected double [] m_globalCounts; /** The lower bounds for each attribute */ protected double [][] m_intervalBounds; /** The maximum entropy for the class */ protected double m_maxEntrop; /** Exponentially bias more confident intervals */ protected boolean m_weightByConfidence = true; /** Bias towards more confident intervals */ protected double m_bias = -0.6; private double TINY = 0.1e-10; /** * Returns a string describing this search method * @return a description of the search method suitable for * displaying in the explorer/experimenter gui */ public String globalInfo() { return "Classification by voting feature intervals. Intervals are " +"constucted around each class for each attribute (" +"basically discretization). Class counts are " +"recorded for each interval on each attribute. Classification is by " +"voting. For more info see Demiroz, G. and Guvenir, A. (1997) " +"\"Classification by voting feature intervals\", ECML-97.\n\n" +"Have added a simple attribute weighting scheme. Higher weight is " +"assigned to more confident intervals, where confidence is a function " +"of entropy:\nweight (att_i) = (entropy of class distrib att_i / " +"max uncertainty)^-bias"; } /** * Returns an enumeration describing the available options. * * @return an enumeration of all the available options. */ public Enumeration listOptions() { Vector newVector = new Vector(2); newVector.addElement( new Option("\tDon't weight voting intervals by confidence", "C", 0,"-C")); newVector.addElement( new Option("\tSet exponential bias towards confident intervals\n" +"\t(default = 1.0)", "B", 1,"-B <bias>")); return newVector.elements(); } /** * Parses a given list of options. Valid options are:<p> * * -C <br> * Don't weight voting intervals by confidence. <p> * * -B <bias> <br> * Set exponential bias towards confident intervals. default = 1.0 <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 optionString; setWeightByConfidence(!Utils.getFlag('C', options)); optionString = Utils.getOption('B', options); if (optionString.length() != 0) { Double temp = new Double(optionString); setBias(temp.doubleValue()); } Utils.checkForRemainingOptions(options); } /** * Returns the tip text for this property * @return tip text for this property suitable for * displaying in the explorer/experimenter gui */ public String weightByConfidenceTipText() { return "Weight feature intervals by confidence"; } /** * Set weighting by confidence * @param c true if feature intervals are to be weighted by confidence */ public void setWeightByConfidence(boolean c) { m_weightByConfidence = c; } /** * Get whether feature intervals are being weighted by confidence * @return true if weighting by confidence is selected */ public boolean getWeightByConfidence() { return m_weightByConfidence; } /** * Returns the tip text for this property * @return tip text for this property suitable for * displaying in the explorer/experimenter gui */ public String biasTipText() { return "Strength of bias towards more confident features"; } /** * Set the value of the exponential bias towards more confident intervals * @param b the value of the bias parameter */ public void setBias(double b) { m_bias = -b; } /** * Get the value of the bias parameter * @return the bias parameter */ public double getBias() { return -m_bias; } /** * Gets the current settings of VFI * * @return an array of strings suitable for passing to setOptions() */ public String[] getOptions () { String[] options = new String[3]; int current = 0; if (!getWeightByConfidence()) { options[current++] = "-C"; } options[current++] = "-B"; options[current++] = ""+getBias(); while (current < options.length) { options[current++] = ""; } return options; } /** * Generates the classifier. * * @param instances set of instances serving as training data * @exception Exception if the classifier has not been generated successfully */ public void buildClassifier(Instances instances) throws Exception { if (!m_weightByConfidence) { TINY = 0.0; } if (instances.classIndex() == -1) { throw new Exception("No class attribute assigned"); } if (!instances.classAttribute().isNominal()) { throw new UnsupportedClassTypeException("VFI: class attribute needs to be nominal!"); } instances = new Instances(instances); instances.deleteWithMissingClass(); m_ClassIndex = instances.classIndex(); m_NumClasses = instances.numClasses(); m_globalCounts = new double [m_NumClasses]; m_maxEntrop = Math.log(m_NumClasses) / Math.log(2); m_Instances = new Instances(instances, 0); // Copy the structure for ref m_intervalBounds = new double[instances.numAttributes()][2+(2*m_NumClasses)]; for (int j = 0; j < instances.numAttributes(); j++) { boolean alt = false; for (int i = 0; i < m_NumClasses*2+2; i++) { if (i == 0) { m_intervalBounds[j][i] = Double.NEGATIVE_INFINITY; } else if (i == m_NumClasses*2+1) { m_intervalBounds[j][i] = Double.POSITIVE_INFINITY; } else { if (alt) { m_intervalBounds[j][i] = Double.NEGATIVE_INFINITY; alt = false; } else { m_intervalBounds[j][i] = Double.POSITIVE_INFINITY; alt = true; } } } } // find upper and lower bounds for numeric attributes for (int j = 0; j < instances.numAttributes(); j++) { if (j != m_ClassIndex && instances.attribute(j).isNumeric()) { for (int i = 0; i < instances.numInstances(); i++) { Instance inst = instances.instance(i); if (!inst.isMissing(j)) { if (inst.value(j) < m_intervalBounds[j][((int)inst.classValue()*2+1)]) { m_intervalBounds[j][((int)inst.classValue()*2+1)] = inst.value(j); } if (inst.value(j) > m_intervalBounds[j][((int)inst.classValue()*2+2)]) { m_intervalBounds[j][((int)inst.classValue()*2+2)] = inst.value(j); } } } } } m_counts = new double [instances.numAttributes()][][]; // sort intervals for (int i = 0 ; i < instances.numAttributes(); i++) { if (instances.attribute(i).isNumeric()) { int [] sortedIntervals = Utils.sort(m_intervalBounds[i]); // remove any duplicate bounds int count = 1; for (int j = 1; j < sortedIntervals.length; j++) { if (m_intervalBounds[i][sortedIntervals[j]] != m_intervalBounds[i][sortedIntervals[j-1]]) { count++; } } double [] reordered = new double [count]; count = 1; reordered[0] = m_intervalBounds[i][sortedIntervals[0]]; for (int j = 1; j < sortedIntervals.length; j++) { if (m_intervalBounds[i][sortedIntervals[j]] != m_intervalBounds[i][sortedIntervals[j-1]]) { reordered[count] = m_intervalBounds[i][sortedIntervals[j]]; count++; } } m_intervalBounds[i] = reordered; m_counts[i] = new double [count][m_NumClasses]; } else if (i != m_ClassIndex) { // nominal attribute m_counts[i] = new double [instances.attribute(i).numValues()][m_NumClasses]; } } // collect class counts for (int i = 0; i < instances.numInstances(); i++) { Instance inst = instances.instance(i); m_globalCounts[(int)instances.instance(i).classValue()] += inst.weight(); for (int j = 0; j < instances.numAttributes(); j++) { if (!inst.isMissing(j) && j != m_ClassIndex) { if (instances.attribute(j).isNumeric()) { double val = inst.value(j); int k; boolean ok = false; for (k = m_intervalBounds[j].length-1; k >= 0; k--) { if (val > m_intervalBounds[j][k]) { ok = true; m_counts[j][k][(int)inst.classValue()] += inst.weight(); break; } else if (val == m_intervalBounds[j][k]) { ok = true; m_counts[j][k][(int)inst.classValue()] += (inst.weight() / 2.0); m_counts[j][k-1][(int)inst.classValue()] += (inst.weight() / 2.0);; break; } } } else { // nominal attribute m_counts[j][(int)inst.value(j)][(int)inst.classValue()] += inst.weight();; } } } } } /** * Returns a description of this classifier. * * @return a description of this classifier as a string. */ public String toString() { if (m_Instances == null) { return "FVI: Classifier not built yet!"; } StringBuffer sb = new StringBuffer("Voting feature intervals classifier\n"); /* Output the intervals and class counts for each attribute */ /* for (int i = 0; i < m_Instances.numAttributes(); i++) { if (i != m_ClassIndex) { sb.append("\n"+m_Instances.attribute(i).name()+" :\n"); if (m_Instances.attribute(i).isNumeric()) { for (int j = 0; j < m_intervalBounds[i].length; j++) { sb.append(m_intervalBounds[i][j]).append("\n"); if (j != m_intervalBounds[i].length-1) { for (int k = 0; k < m_NumClasses; k++) { sb.append(m_counts[i][j][k]+" "); } } sb.append("\n"); } } else { for (int j = 0; j < m_Instances.attribute(i).numValues(); j++) { sb.append(m_Instances.attribute(i).value(j)).append("\n"); for (int k = 0; k < m_NumClasses; k++) { sb.append(m_counts[i][j][k]+" "); } sb.append("\n"); } } } } */ return sb.toString(); } /** * Classifies the given test instance. * * @param instance the instance to be classified * @return the predicted class for the instance * @exception Exception if the instance can't be classified */ public double [] distributionForInstance(Instance instance) throws Exception { double [] dist = new double[m_NumClasses]; double [] temp = new double[m_NumClasses]; double totalWeight = 0.0; double weight = 1.0; for (int i = 0; i < instance.numAttributes(); i++) { if (i != m_ClassIndex && !instance.isMissing(i)) { double val = instance.value(i); boolean ok = false; if (instance.attribute(i).isNumeric()) { int k; for (k = m_intervalBounds[i].length-1; k >= 0; k--) { if (val > m_intervalBounds[i][k]) { for (int j = 0; j < m_NumClasses; j++) { if (m_globalCounts[j] > 0) { temp[j] = ((m_counts[i][k][j]+TINY) / (m_globalCounts[j]+TINY)); } } ok = true; break; } else if (val == m_intervalBounds[i][k]) { for (int j = 0; j < m_NumClasses; j++) { if (m_globalCounts[j] > 0) { temp[j] = ((m_counts[i][k][j] + m_counts[i][k-1][j]) / 2.0) + TINY; temp[j] /= (m_globalCounts[j]+TINY); } } ok = true; break; } } if (!ok) { throw new Exception("This shouldn't happen"); } } else { // nominal attribute ok = true; for (int j = 0; j < m_NumClasses; j++) { if (m_globalCounts[j] > 0) { temp[j] = ((m_counts[i][(int)val][j]+TINY) / (m_globalCounts[j]+TINY)); } } } Utils.normalize(temp); if (m_weightByConfidence) { weight = weka.core.ContingencyTables.entropy(temp); weight = Math.pow(weight, m_bias); if (weight < 1.0) { weight = 1.0; } } for (int j = 0; j < m_NumClasses; j++) { dist[j] += (temp[j] * weight); } } } Utils.normalize(dist); return dist; } /** * Main method for testing this class. * * @param args should contain command line arguments for evaluation * (see Evaluation). */ public static void main(String [] args) { try { System.out.println(Evaluation.evaluateModel(new VFI(), args)); } catch (Exception e) { e.printStackTrace(); } } }