/* * 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. */ /* * Ridor.java * Copyright (C) 2001 University of Waikato, Hamilton, New Zealand * */ package weka.classifiers.rules; import weka.classifiers.Classifier; import weka.core.AdditionalMeasureProducer; 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.RevisionHandler; import weka.core.RevisionUtils; import weka.core.UnsupportedClassTypeException; import weka.core.Utils; import weka.core.WeightedInstancesHandler; import weka.core.Capabilities.Capability; import java.io.Serializable; import java.util.Enumeration; import java.util.Random; import java.util.Vector; import weka.classifiers.AbstractClassifier; /** <!-- globalinfo-start --> * An implementation of a RIpple-DOwn Rule learner.<br/> * <br/> * It generates a default rule first and then the exceptions for the default rule with the least (weighted) error rate. Then it generates the "best" exceptions for each exception and iterates until pure. Thus it performs a tree-like expansion of exceptions.The exceptions are a set of rules that predict classes other than the default. IREP is used to generate the exceptions.<br/> * <br/> * For more information about Ripple-Down Rules, see:<br/> * <br/> * Brian R. Gaines, Paul Compton (1995). Induction of Ripple-Down Rules Applied to Modeling Large Databases. J. Intell. Inf. Syst.. 5(3):211-228. * <p/> <!-- globalinfo-end --> * * There are five inner classes defined in this class. <br> * The first is Ridor_node, which implements one node in the Ridor tree. It's basically * composed of a default class and a set of exception rules to the default class.<br> * The second inner class is RidorRule, which implements a single exception rule * using REP.<br> * The last three inner classes are only used in RidorRule. They are Antd, NumericAntd * and NominalAntd, which all implement a single antecedent in the RidorRule. <br> * The Antd class is an abstract class, which has two subclasses, NumericAntd and * NominalAntd, to implement the corresponding abstract functions. These two subclasses * implement the functions related to a antecedent with a nominal attribute and a numeric * attribute respectively.<p> * <!-- options-start --> * Valid options are: <p/> * * <pre> -F <number of folds> * Set number of folds for IREP * One fold is used as pruning set. * (default 3)</pre> * * <pre> -S <number of shuffles> * Set number of shuffles to randomize * the data in order to get better rule. * (default 10)</pre> * * <pre> -A * Set flag of whether use the error rate * of all the data to select the default class * in each step. If not set, the learner will only use the error rate in the pruning data</pre> * * <pre> -M * Set flag of whether use the majority class as * the default class in each step instead of * choosing default class based on the error rate * (if the flag is not set)</pre> * * <pre> -N <min. weights> * Set the minimal weights of instances * within a split. * (default 2.0)</pre> * <!-- options-end --> * * @author Xin XU (xx5@cs.waikato.ac.nz) * @version $Revision: 5529 $ */ public class Ridor extends AbstractClassifier implements AdditionalMeasureProducer, WeightedInstancesHandler { /** for serialization */ static final long serialVersionUID = -7261533075088314436L; /** The number of folds to split data into Grow and Prune for IREP */ private int m_Folds = 3; /** The number of shuffles performed on the data for randomization */ private int m_Shuffle = 1; /** Random object for randomization */ private Random m_Random = null; /** The seed to perform randomization */ private int m_Seed = 1; /** Whether use error rate on all the data */ private boolean m_IsAllErr = false; /** Whether use majority class as default class */ private boolean m_IsMajority = false; /** The root of Ridor */ private Ridor_node m_Root = null; /** The class attribute of the data */ private Attribute m_Class; /** Statistics of the data */ private double m_Cover, m_Err; /** The minimal number of instance weights within a split*/ private double m_MinNo = 2.0; /** * Returns a string describing classifier * @return a description suitable for * displaying in the explorer/experimenter gui */ public String globalInfo() { return "An implementation of a RIpple-DOwn Rule learner.\n\n" + "It generates a default rule first and then the exceptions for the default rule " + "with the least (weighted) error rate. Then it generates the \"best\" exceptions for " + "each exception and iterates until pure. Thus it performs a tree-like expansion of " + "exceptions." + "The exceptions are a set of rules that predict classes other than the default. " + "IREP is used to generate the exceptions.\n\n" + "For more information about Ripple-Down Rules, see:\n\n"; } /** * Private class implementing the single node of Ridor. * It consists of a default class label, a set of exceptions to the default rule * and the exceptions to each exception */ private class Ridor_node implements Serializable, RevisionHandler { /** for serialization */ static final long serialVersionUID = -581370560157467677L; /** The default class label */ private double defClass = Double.NaN; /** The set of exceptions of the default rule. Each element also has its own exceptions and the consequent of each rule is determined by its exceptions */ private RidorRule[] rules = null; /** The exceptions of the exception rules */ private Ridor_node[] excepts = null; /** The level of this node */ private int level; /** * Gets the default class label * * @return the default class label */ public double getDefClass() { return defClass; } /** * Gets the set of exceptions * * @return the set of exceptions */ public RidorRule[] getRules() { return rules; } /** * Gets the exceptions of the exceptions rules * * @return the exceptions of the exceptions rules */ public Ridor_node[] getExcepts() { return excepts; } /** * Builds a ripple-down manner rule learner. * * @param dataByClass the divided data by their class label. The real class * labels of the instances are all set to 0 * @param lvl the level of the parent node * @throws Exception if ruleset of this node cannot be built */ public void findRules(Instances[] dataByClass, int lvl) throws Exception { Vector finalRules = null; int clas = -1; double[] isPure = new double[dataByClass.length]; int numMajority = 0; level = lvl + 1; for(int h=0; h < dataByClass.length; h++){ isPure[h] = dataByClass[h].sumOfWeights(); if(Utils.grOrEq(isPure[h], m_Folds)) numMajority++; // Count how many class labels have enough instances } if(numMajority <= 1){ // The data is pure or not enough defClass = (double)Utils.maxIndex(isPure); return; } double total = Utils.sum(isPure); if(m_IsMajority){ defClass = (double)Utils.maxIndex(isPure); Instances data = new Instances(dataByClass[(int)defClass]); int index = data.classIndex(); for(int j=0; j<data.numInstances(); j++) data.instance(j).setClassValue(1); // Set one class as default for(int k=0; k < dataByClass.length; k++) // Merge into one dataset if(k != (int)defClass){ if(data.numInstances() >= dataByClass[k].numInstances()) data = append(data, dataByClass[k]); else data = append(dataByClass[k], data); } data.setClassIndex(index); // Position new class label double classCount = total - isPure[(int)defClass]; finalRules = new Vector(); buildRuleset(data, classCount, finalRules); if(finalRules.size() == 0) // No good rules built return; } else{ double maxAcRt = isPure[Utils.maxIndex(isPure)] / total; // Find default class for(int i=0; i < dataByClass.length; i++){ if(isPure[i] >= m_Folds){ Instances data = new Instances(dataByClass[i]); int index = data.classIndex(); for(int j=0; j<data.numInstances(); j++) data.instance(j).setClassValue(1); // Set one class as default for(int k=0; k < dataByClass.length; k++) // Merge into one dataset if(k != i){ if(data.numInstances() >= dataByClass[k].numInstances()) data = append(data, dataByClass[k]); else data = append(dataByClass[k], data); } data.setClassIndex(index); // Position new class label /* Build a set of rules */ double classCount = data.sumOfWeights() - isPure[i]; Vector ruleset = new Vector(); double wAcRt = buildRuleset(data, classCount, ruleset); if(Utils.gr(wAcRt, maxAcRt)){ finalRules = ruleset; maxAcRt = wAcRt; clas = i; } } } if(finalRules == null){ // No good rules found, set majority class as default defClass = (double)Utils.maxIndex(isPure); return; } defClass = (double)clas; } /* Store the exception rules and default class in this node */ int size = finalRules.size(); rules = new RidorRule[size]; excepts = new Ridor_node[size]; for(int l=0; l < size; l++) rules[l] = (RidorRule)finalRules.elementAt(l); /* Build exceptions for each exception rule */ Instances[] uncovered = dataByClass; if(level == 1) // The error of default rule m_Err = total - uncovered[(int)defClass].sumOfWeights(); uncovered[(int)defClass] = new Instances(uncovered[(int)defClass], 0); for(int m=0; m < size; m++){ /* The data covered by this rule, they are also deducted from the original data */ Instances[][] dvdData = divide(rules[m], uncovered); Instances[] covered = dvdData[0]; // Data covered by the rule //uncovered = dvdData[1]; // Data not covered by the rule excepts[m] = new Ridor_node(); excepts[m].findRules(covered, level);// Find exceptions on the covered data } } /** * Private function to build a rule set and return the weighted avg of accuracy * rate of rules in the set. * * @param insts the data used to build ruleset * @param classCount the counts of the instances with the predicted class but not * yet covered by the ruleset * @param ruleset the ruleset to be built * @return the weighted accuracy rate of the ruleset * @throws Exception if the rules cannot be built properly */ private double buildRuleset(Instances insts, double classCount, Vector ruleset) throws Exception { Instances data = new Instances(insts); double wAcRt = 0; // The weighted accuracy rate of this ruleset double total = data.sumOfWeights(); while( classCount >= m_Folds ){ // Data is not pure RidorRule bestRule = null; double bestWorthRate= -1; // The best worth achieved by double bestWorth = -1; // randomization of the data RidorRule rule = new RidorRule(); rule.setPredictedClass(0); // Predict the classes other than default for(int j = 0; j < m_Shuffle; j++){ if(m_Shuffle > 1) data.randomize(m_Random); rule.buildClassifier(data); double wr, w; // Worth rate and worth if(m_IsAllErr){ wr = (rule.getWorth()+rule.getAccuG()) / (rule.getCoverP()+rule.getCoverG()); w = rule.getWorth() + rule.getAccuG(); } else{ wr = rule.getWorthRate(); w = rule.getWorth(); } if(Utils.gr(wr, bestWorthRate) || (Utils.eq(wr, bestWorthRate) && Utils.gr(w, bestWorth))){ bestRule = rule; bestWorthRate = wr; bestWorth = w; } } if (bestRule == null) throw new Exception("Something wrong here inside findRule()!"); if(Utils.sm(bestWorthRate, 0.5) || (!bestRule.hasAntds())) break; // No more good rules generated Instances newData = new Instances(data); data = new Instances(newData, 0);// Empty the data classCount = 0; double cover = 0; // Coverage of this rule on whole data for(int l=0; l<newData.numInstances(); l++){ Instance datum = newData.instance(l); if(!bestRule.isCover(datum)){// Data not covered by the previous rule data.add(datum); if(Utils.eq(datum.classValue(), 0)) classCount += datum.weight(); // The predicted class in the data } else cover += datum.weight(); } wAcRt += computeWeightedAcRt(bestWorthRate, cover, total); ruleset.addElement(bestRule); } /* The weighted def. accuracy */ double wDefAcRt = (data.sumOfWeights()-classCount) / total; wAcRt += wDefAcRt; return wAcRt; } /** * Private function to combine two data * * @param data1 the data to which data2 is appended * @param data2 the data to be appended to data1 * @return the merged data */ private Instances append(Instances data1, Instances data2){ Instances data = new Instances(data1); for(int i=0; i<data2.numInstances(); i++) data.add(data2.instance(i)); return data; } /** * Compute the weighted average of accuracy rate of a certain rule * Each rule is weighted by its coverage proportion in the whole data. * So the accuracy rate of one ruleset is actually * * (worth rate) * (coverage proportion) * * coverage of the rule on the whole data * where coverage proportion = ----------------------------------------- * the whole data size fed into the ruleset * * @param worthRt the worth rate * @param cover the coverage of the rule on the whole data * @param total the total data size fed into the ruleset * @return the weighted accuracy rate of this rule */ private double computeWeightedAcRt(double worthRt, double cover, double total){ return (worthRt * (cover/total)); } /** * Builds an array of data according to their true class label * Each bag of data is filtered through the rule specified and * is totally covered by this rule. * Both the data covered and uncovered by the rule will be returned * by the procedure. * * @param rule the rule covering the data * @param dataByClass the array of data to be covered by the rule * @return the arrays of data both covered and not covered by the rule */ private Instances[][] divide(RidorRule rule, Instances[] dataByClass){ int len = dataByClass.length; Instances[][] dataBags = new Instances[2][len]; for(int i=0; i < len; i++){ Instances[] dvdData = rule.coveredByRule(dataByClass[i]); dataBags[0][i] = dvdData[0]; // Covered by the rule dataBags[1][i] = dvdData[1]; // Not covered by the rule } return dataBags; } /** * The size of the certain node of Ridor, i.e. the * number of rules generated within and below this node * * @return the size of this node */ public int size(){ int size = 0; if(rules != null){ for(int i=0; i < rules.length; i++) size += excepts[i].size(); // The children's size size += rules.length; // This node's size } return size; } /** * Prints the all the rules of one node of Ridor. * * @return a textual description of one node of Ridor */ public String toString(){ StringBuffer text = new StringBuffer(); if(level == 1) text.append(m_Class.name() + " = " + m_Class.value((int)getDefClass())+ " ("+m_Cover+"/"+m_Err+")\n"); if(rules != null){ for(int i=0; i < rules.length; i++){ for(int j=0; j < level; j++) text.append(" "); String cl = m_Class.value((int)(excepts[i].getDefClass())); text.append(" Except " + rules[i].toString(m_Class.name(), cl)+ "\n" + excepts[i].toString()); } } return text.toString(); } /** * Returns the revision string. * * @return the revision */ public String getRevision() { return RevisionUtils.extract("$Revision: 5529 $"); } } /** * This class implements a single rule that predicts the 2-class distribution. * * A rule consists of antecedents "AND"ed together and the consequent (class value) * for the classification. In this case, the consequent is the distribution of * the available classes (always 2 classes) in the dataset. * In this class, the Information Gain (p*[log(p/t) - log(P/T)]) is used to select * an antecedent and Reduced Error Prunning (REP) is used to prune the rule. * */ private class RidorRule implements WeightedInstancesHandler, Serializable, RevisionHandler { /** for serialization */ static final long serialVersionUID = 4375199423973848157L; /** The internal representation of the class label to be predicted*/ private double m_Class = -1; /** The class attribute of the data*/ private Attribute m_ClassAttribute; /** The vector of antecedents of this rule*/ protected FastVector m_Antds = null; /** The worth rate of this rule, in this case, accuracy rate in the pruning data*/ private double m_WorthRate = 0; /** The worth value of this rule, in this case, accurate # in pruning data*/ private double m_Worth = 0; /** The sum of weights of the data covered by this rule in the pruning data */ private double m_CoverP = 0; /** The accurate and covered data of this rule in the growing data */ private double m_CoverG = 0, m_AccuG = 0; /** The access functions for parameters */ public void setPredictedClass(double cl){ m_Class = cl; } public double getPredictedClass(){ return m_Class; } /** * Builds a single rule learner with REP dealing with 2 classes. * This rule learner always tries to predict the class with label * m_Class. * * @param instances the training data * @throws Exception if classifier can't be built successfully */ public void buildClassifier(Instances instances) throws Exception { m_ClassAttribute = instances.classAttribute(); if (!m_ClassAttribute.isNominal()) throw new UnsupportedClassTypeException(" Only nominal class, please."); if(instances.numClasses() != 2) throw new Exception(" Only 2 classes, please."); Instances data = new Instances(instances); if(Utils.eq(data.sumOfWeights(),0)) throw new Exception(" No training data."); data.deleteWithMissingClass(); if(Utils.eq(data.sumOfWeights(),0)) throw new Exception(" The class labels of all the training data are missing."); if(data.numInstances() < m_Folds) throw new Exception(" Not enough data for REP."); m_Antds = new FastVector(); /* Split data into Grow and Prune*/ m_Random = new Random(m_Seed); data.randomize(m_Random); data.stratify(m_Folds); Instances growData=data.trainCV(m_Folds, m_Folds-1, m_Random); Instances pruneData=data.testCV(m_Folds, m_Folds-1); grow(growData); // Build this rule prune(pruneData); // Prune this rule } /** * Find all the instances in the dataset covered by this rule. * The instances not covered will also be deducted from the the original data * and returned by this procedure. * * @param insts the dataset to be covered by this rule. * @return the instances covered and not covered by this rule */ public Instances[] coveredByRule(Instances insts){ Instances[] data = new Instances[2]; data[0] = new Instances(insts, insts.numInstances()); data[1] = new Instances(insts, insts.numInstances()); for(int i=0; i<insts.numInstances(); i++){ Instance datum = insts.instance(i); if(isCover(datum)) data[0].add(datum); // Covered by this rule else data[1].add(datum); // Not covered by this rule } return data; } /** * Whether the instance covered by this rule * * @param inst the instance in question * @return the boolean value indicating whether the instance is covered by this rule */ public boolean isCover(Instance datum){ boolean isCover=true; for(int i=0; i<m_Antds.size(); i++){ Antd antd = (Antd)m_Antds.elementAt(i); if(!antd.isCover(datum)){ isCover = false; break; } } return isCover; } /** * Whether this rule has antecedents, i.e. whether it is a default rule * * @return the boolean value indicating whether the rule has antecedents */ public boolean hasAntds(){ if (m_Antds == null) return false; else return (m_Antds.size() > 0); } /** * Build one rule using the growing data * * @param data the growing data used to build the rule */ private void grow(Instances data){ Instances growData = new Instances(data); m_AccuG = computeDefAccu(growData); m_CoverG = growData.sumOfWeights(); /* Compute the default accurate rate of the growing data */ double defAcRt= m_AccuG / m_CoverG; /* Keep the record of which attributes have already been used*/ boolean[] used=new boolean [growData.numAttributes()]; for (int k=0; k<used.length; k++) used[k]=false; int numUnused=used.length; double maxInfoGain; boolean isContinue = true; // The stopping criterion of this rule while (isContinue){ maxInfoGain = 0; // We require that infoGain be positive /* Build a list of antecedents */ Antd oneAntd=null; Instances coverData = null; Enumeration enumAttr=growData.enumerateAttributes(); int index=-1; /* Build one condition based on all attributes not used yet*/ while (enumAttr.hasMoreElements()){ Attribute att= (Attribute)(enumAttr.nextElement()); index++; Antd antd =null; if(att.isNumeric()) antd = new NumericAntd(att); else antd = new NominalAntd(att); if(!used[index]){ /* Compute the best information gain for each attribute, it's stored in the antecedent formed by this attribute. This procedure returns the data covered by the antecedent*/ Instances coveredData = computeInfoGain(growData, defAcRt, antd); if(coveredData != null){ double infoGain = antd.getMaxInfoGain(); if(Utils.gr(infoGain, maxInfoGain)){ oneAntd=antd; coverData = coveredData; maxInfoGain = infoGain; } } } } if(oneAntd == null) return; //Numeric attributes can be used more than once if(!oneAntd.getAttr().isNumeric()){ used[oneAntd.getAttr().index()]=true; numUnused--; } m_Antds.addElement((Object)oneAntd); growData = coverData;// Grow data size is shrinking defAcRt = oneAntd.getAccuRate(); /* Stop if no more data, rule perfect, no more attributes */ if(Utils.eq(growData.sumOfWeights(), 0.0) || Utils.eq(defAcRt, 1.0) || (numUnused == 0)) isContinue = false; } } /** * Compute the best information gain for the specified antecedent * * @param data the data based on which the infoGain is computed * @param defAcRt the default accuracy rate of data * @param antd the specific antecedent * @return the data covered by the antecedent */ private Instances computeInfoGain(Instances instances, double defAcRt, Antd antd){ Instances data = new Instances(instances); /* Split the data into bags. The information gain of each bag is also calculated in this procedure */ Instances[] splitData = antd.splitData(data, defAcRt, m_Class); /* Get the bag of data to be used for next antecedents */ if(splitData != null) return splitData[(int)antd.getAttrValue()]; else return null; } /** * Prune the rule using the pruning data and update the worth parameters for this rule * The accuracy rate is used to prune the rule. * * @param pruneData the pruning data used to prune the rule */ private void prune(Instances pruneData){ Instances data=new Instances(pruneData); double total = data.sumOfWeights(); /* The default accurate# and the the accuracy rate on pruning data */ double defAccu=0, defAccuRate=0; int size=m_Antds.size(); if(size == 0) return; // Default rule before pruning double[] worthRt = new double[size]; double[] coverage = new double[size]; double[] worthValue = new double[size]; for(int w=0; w<size; w++){ worthRt[w]=coverage[w]=worthValue[w]=0.0; } /* Calculate accuracy parameters for all the antecedents in this rule */ for(int x=0; x<size; x++){ Antd antd=(Antd)m_Antds.elementAt(x); Attribute attr= antd.getAttr(); Instances newData = new Instances(data); data = new Instances(newData, newData.numInstances()); // Make data empty for(int y=0; y<newData.numInstances(); y++){ Instance ins=newData.instance(y); if(!ins.isMissing(attr)){ // Attribute not missing if(antd.isCover(ins)){ // Covered by this antecedent coverage[x] += ins.weight(); data.add(ins); // Add to data for further pruning if(Utils.eq(ins.classValue(), m_Class)) // Accurate prediction worthValue[x] += ins.weight(); } } } if(coverage[x] != 0) worthRt[x] = worthValue[x]/coverage[x]; } /* Prune the antecedents according to the accuracy parameters */ for(int z=(size-1); z > 0; z--) if(Utils.sm(worthRt[z], worthRt[z-1])) m_Antds.removeElementAt(z); else break; /* Check whether this rule is a default rule */ if(m_Antds.size() == 1){ defAccu = computeDefAccu(pruneData); defAccuRate = defAccu/total; // Compute def. accuracy if(Utils.sm(worthRt[0], defAccuRate)){ // Becomes a default rule m_Antds.removeAllElements(); } } /* Update the worth parameters of this rule*/ int antdsSize = m_Antds.size(); if(antdsSize != 0){ // Not a default rule m_Worth = worthValue[antdsSize-1]; // WorthValues of the last antecedent m_WorthRate = worthRt[antdsSize-1]; m_CoverP = coverage[antdsSize-1]; Antd last = (Antd)m_Antds.lastElement(); m_CoverG = last.getCover(); m_AccuG = last.getAccu(); } else{ // Default rule m_Worth = defAccu; // Default WorthValues m_WorthRate = defAccuRate; m_CoverP = total; } } /** * Private function to compute default number of accurate instances * in the specified data for m_Class * * @param data the data in question * @return the default accuracy number */ private double computeDefAccu(Instances data){ double defAccu=0; for(int i=0; i<data.numInstances(); i++){ Instance inst = data.instance(i); if(Utils.eq(inst.classValue(), m_Class)) defAccu += inst.weight(); } return defAccu; } /** The following are get functions after prune() has set the value of worthRate and worth*/ public double getWorthRate(){ return m_WorthRate; } public double getWorth(){ return m_Worth; } public double getCoverP(){ return m_CoverP; } public double getCoverG(){ return m_CoverG; } public double getAccuG(){ return m_AccuG; } /** * Prints this rule with the specified class label * * @param att the string standing for attribute in the consequent of this rule * @param cl the string standing for value in the consequent of this rule * @return a textual description of this rule with the specified class label */ public String toString(String att, String cl) { StringBuffer text = new StringBuffer(); if(m_Antds.size() > 0){ for(int j=0; j< (m_Antds.size()-1); j++) text.append("(" + ((Antd)(m_Antds.elementAt(j))).toString()+ ") and "); text.append("("+((Antd)(m_Antds.lastElement())).toString() + ")"); } text.append(" => " + att + " = " + cl); text.append(" ("+m_CoverG+"/"+(m_CoverG - m_AccuG)+") ["+ m_CoverP+"/"+(m_CoverP - m_Worth)+"]"); return text.toString(); } /** * Prints this rule * * @return a textual description of this rule */ public String toString() { return toString(m_ClassAttribute.name(), m_ClassAttribute.value((int)m_Class)); } /** * Returns the revision string. * * @return the revision */ public String getRevision() { return RevisionUtils.extract("$Revision: 5529 $"); } } /** * The single antecedent in the rule, which is composed of an attribute and * the corresponding value. There are two inherited classes, namely NumericAntd * and NominalAntd in which the attributes are numeric and nominal respectively. */ private abstract class Antd implements Serializable, RevisionHandler { /** for serialization */ private static final long serialVersionUID = 5317379013858933369L; /** The attribute of the antecedent */ protected Attribute att; /** The attribute value of the antecedent. For numeric attribute, value is either 0(1st bag) or 1(2nd bag) */ protected double value; /** The maximum infoGain achieved by this antecedent test */ protected double maxInfoGain; /** The accurate rate of this antecedent test on the growing data */ protected double accuRate; /** The coverage of this antecedent */ protected double cover; /** The accurate data for this antecedent */ protected double accu; /** Constructor*/ public Antd(Attribute a){ att=a; value=Double.NaN; maxInfoGain = 0; accuRate = Double.NaN; cover = Double.NaN; accu = Double.NaN; } /* The abstract members for inheritance */ public abstract Instances[] splitData(Instances data, double defAcRt, double cla); public abstract boolean isCover(Instance inst); public abstract String toString(); /* Get functions of this antecedent */ public Attribute getAttr(){ return att; } public double getAttrValue(){ return value; } public double getMaxInfoGain(){ return maxInfoGain; } public double getAccuRate(){ return accuRate; } public double getAccu(){ return accu; } public double getCover(){ return cover; } /** * Returns the revision string. * * @return the revision */ public String getRevision() { return RevisionUtils.extract("$Revision: 5529 $"); } } /** * The antecedent with numeric attribute */ private class NumericAntd extends Antd { /** for serialization */ static final long serialVersionUID = 1968761518014492214L; /** The split point for this numeric antecedent */ private double splitPoint; /** Constructor*/ public NumericAntd(Attribute a){ super(a); splitPoint = Double.NaN; } /** Get split point of this numeric antecedent */ public double getSplitPoint(){ return splitPoint; } /** * Implements the splitData function. * This procedure is to split the data into two bags according * to the information gain of the numeric attribute value * The maximum infoGain is also calculated. * * @param insts the data to be split * @param defAcRt the default accuracy rate for data * @param cl the class label to be predicted * @return the array of data after split */ public Instances[] splitData(Instances insts, double defAcRt, double cl){ Instances data = new Instances(insts); data.sort(att); int total=data.numInstances();// Total number of instances without // missing value for att int split=1; // Current split position int prev=0; // Previous split position int finalSplit=split; // Final split position maxInfoGain = 0; value = 0; // Compute minimum number of Instances required in each split double minSplit = 0.1 * (data.sumOfWeights()) / 2.0; if (Utils.smOrEq(minSplit,m_MinNo)) minSplit = m_MinNo; else if (Utils.gr(minSplit,25)) minSplit = 25; double fstCover=0, sndCover=0, fstAccu=0, sndAccu=0; for(int x=0; x<data.numInstances(); x++){ Instance inst = data.instance(x); if(inst.isMissing(att)){ total = x; break; } sndCover += inst.weight(); if(Utils.eq(inst.classValue(), cl)) sndAccu += inst.weight(); } // Enough Instances with known values? if (Utils.sm(sndCover,(2*minSplit))) return null; if(total == 0) return null; // Data all missing for the attribute splitPoint = data.instance(total-1).value(att); for(; split < total; split++){ if(!Utils.eq(data.instance(split).value(att), data.instance(prev).value(att))){ // Can't split within same value for(int y=prev; y<split; y++){ Instance inst = data.instance(y); fstCover += inst.weight(); sndCover -= inst.weight(); if(Utils.eq(data.instance(y).classValue(), cl)){ fstAccu += inst.weight(); // First bag positive# ++ sndAccu -= inst.weight(); // Second bag positive# -- } } if(Utils.sm(fstCover, minSplit) || Utils.sm(sndCover, minSplit)){ prev=split; // Cannot split because either continue; // split has not enough data } double fstAccuRate = 0, sndAccuRate = 0; if(!Utils.eq(fstCover,0)) fstAccuRate = fstAccu/fstCover; if(!Utils.eq(sndCover,0)) sndAccuRate = sndAccu/sndCover; /* Which bag has higher information gain? */ boolean isFirst; double fstInfoGain, sndInfoGain; double accRate, infoGain, coverage, accurate; fstInfoGain = Utils.eq(fstAccuRate, 0) ? 0 : (fstAccu*(Utils.log2(fstAccuRate) - Utils.log2(defAcRt))); sndInfoGain = Utils.eq(sndAccuRate, 0) ? 0 : (sndAccu*(Utils.log2(sndAccuRate) - Utils.log2(defAcRt))); if(Utils.gr(fstInfoGain,sndInfoGain) || (Utils.eq(fstInfoGain,sndInfoGain)&&(Utils.grOrEq(fstAccuRate,sndAccuRate)))){ isFirst = true; infoGain = fstInfoGain; accRate = fstAccuRate; accurate = fstAccu; coverage = fstCover; } else{ isFirst = false; infoGain = sndInfoGain; accRate = sndAccuRate; accurate = sndAccu; coverage = sndCover; } boolean isUpdate = Utils.gr(infoGain, maxInfoGain); /* Check whether so far the max infoGain */ if(isUpdate){ splitPoint = (data.instance(split).value(att) + data.instance(prev).value(att))/2; value = ((isFirst) ? 0 : 1); accuRate = accRate; accu = accurate; cover = coverage; maxInfoGain = infoGain; finalSplit = split; } prev=split; } } /* Split the data */ Instances[] splitData = new Instances[2]; splitData[0] = new Instances(data, 0, finalSplit); splitData[1] = new Instances(data, finalSplit, total-finalSplit); return splitData; } /** * Whether the instance is covered by this antecedent * * @param inst the instance in question * @return the boolean value indicating whether the instance is covered * by this antecedent */ public boolean isCover(Instance inst){ boolean isCover=false; if(!inst.isMissing(att)){ if(Utils.eq(value, 0)){ if(Utils.smOrEq(inst.value(att), splitPoint)) isCover=true; } else if(Utils.gr(inst.value(att), splitPoint)) isCover=true; } return isCover; } /** * Prints this antecedent * * @return a textual description of this antecedent */ public String toString() { String symbol = Utils.eq(value, 0.0) ? " <= " : " > "; return (att.name() + symbol + Utils.doubleToString(splitPoint, 6)); } /** * Returns the revision string. * * @return the revision */ public String getRevision() { return RevisionUtils.extract("$Revision: 5529 $"); } } /** * The antecedent with nominal attribute */ private class NominalAntd extends Antd { /** for serialization */ static final long serialVersionUID = -256386137196078004L; /* The parameters of infoGain calculated for each attribute value */ private double[] accurate; private double[] coverage; private double[] infoGain; /** Constructor*/ public NominalAntd(Attribute a){ super(a); int bag = att.numValues(); accurate = new double[bag]; coverage = new double[bag]; infoGain = new double[bag]; } /** * Implements the splitData function. * This procedure is to split the data into bags according * to the nominal attribute value * The infoGain for each bag is also calculated. * * @param data the data to be split * @param defAcRt the default accuracy rate for data * @param cl the class label to be predicted * @return the array of data after split */ public Instances[] splitData(Instances data, double defAcRt, double cl){ int bag = att.numValues(); Instances[] splitData = new Instances[bag]; for(int x=0; x<bag; x++){ accurate[x] = coverage[x] = infoGain[x] = 0; splitData[x] = new Instances(data, data.numInstances()); } for(int x=0; x<data.numInstances(); x++){ Instance inst=data.instance(x); if(!inst.isMissing(att)){ int v = (int)inst.value(att); splitData[v].add(inst); coverage[v] += inst.weight(); if(Utils.eq(inst.classValue(), cl)) accurate[v] += inst.weight(); } } // Check if >=2 splits have more than the minimal data int count=0; for(int x=0; x<bag; x++){ double t = coverage[x]; if(Utils.grOrEq(t, m_MinNo)){ double p = accurate[x]; if(!Utils.eq(t, 0.0)) infoGain[x] = p *((Utils.log2(p/t)) - (Utils.log2(defAcRt))); ++count; } } if(count < 2) // Don't split return null; value = (double)Utils.maxIndex(infoGain); cover = coverage[(int)value]; accu = accurate[(int)value]; if(!Utils.eq(cover,0)) accuRate = accu / cover; else accuRate = 0; maxInfoGain = infoGain [(int)value]; return splitData; } /** * Whether the instance is covered by this antecedent * * @param inst the instance in question * @return the boolean value indicating whether the instance is covered * by this antecedent */ public boolean isCover(Instance inst){ boolean isCover=false; if(!inst.isMissing(att)){ if(Utils.eq(inst.value(att), value)) isCover=true; } return isCover; } /** * Prints this antecedent * * @return a textual description of this antecedent */ public String toString() { return (att.name() + " = " +att.value((int)value)); } /** * Returns the revision string. * * @return the revision */ public String getRevision() { return RevisionUtils.extract("$Revision: 5529 $"); } } /** * Returns default capabilities of the classifier. * * @return the capabilities of this classifier */ public Capabilities getCapabilities() { Capabilities result = super.getCapabilities(); result.disableAll(); // attributes result.enable(Capability.NOMINAL_ATTRIBUTES); result.enable(Capability.NUMERIC_ATTRIBUTES); result.enable(Capability.DATE_ATTRIBUTES); result.enable(Capability.MISSING_VALUES); // class result.enable(Capability.NOMINAL_CLASS); result.enable(Capability.MISSING_CLASS_VALUES); return result; } /** * Builds a ripple-down manner rule learner. * * @param instances the training data * @throws Exception if classifier can't be built successfully */ public void buildClassifier(Instances instances) throws Exception { // can classifier handle the data? getCapabilities().testWithFail(instances); // remove instances with missing class Instances data = new Instances(instances); data.deleteWithMissingClass(); int numCl = data.numClasses(); m_Root = new Ridor_node(); m_Class = instances.classAttribute(); // The original class label int index = data.classIndex(); m_Cover = data.sumOfWeights(); m_Random = new Random(m_Seed); /* Create a binary attribute */ FastVector binary_values = new FastVector(2); binary_values.addElement("otherClasses"); binary_values.addElement("defClass"); Attribute attr = new Attribute ("newClass", binary_values); data.insertAttributeAt(attr, index); data.setClassIndex(index); // The new class label /* Partition the data into bags according to their original class values */ Instances[] dataByClass = new Instances[numCl]; for(int i=0; i < numCl; i++) dataByClass[i] = new Instances(data, data.numInstances()); // Empty bags for(int i=0; i < data.numInstances(); i++){ // Partitioning Instance inst = data.instance(i); inst.setClassValue(0); // Set new class vaue to be 0 dataByClass[(int)inst.value(index+1)].add(inst); } for(int i=0; i < numCl; i++) dataByClass[i].deleteAttributeAt(index+1); // Delete original class m_Root.findRules(dataByClass, 0); } /** * Classify the test instance with the rule learner * * @param datum the instance to be classified * @return the classification */ public double classifyInstance(Instance datum){ return classify(m_Root, datum); } /** * Classify the test instance with one node of Ridor * * @param node the node of Ridor to classify the test instance * @param datum the instance to be classified * @return the classification */ private double classify(Ridor_node node, Instance datum){ double classValue = node.getDefClass(); RidorRule[] rules = node.getRules(); if(rules != null){ Ridor_node[] excepts = node.getExcepts(); for(int i=0; i < excepts.length; i++){ if(rules[i].isCover(datum)){ classValue = classify(excepts[i], datum); break; } } } return classValue; } /** * Returns an enumeration describing the available options * Valid options are: <p> * * -F number <br> * Set number of folds for reduced error pruning. One fold is * used as the pruning set. (Default: 3) <p> * * -S number <br> * Set number of shuffles for randomization. (Default: 10) <p> * * -A <br> * Set flag of whether use the error rate of all the data to select * the default class in each step. If not set, the learner will only use * the error rate in the pruning data <p> * * -M <br> * Set flag of whether use the majority class as the default class * in each step instead of choosing default class based on the error rate * (if the flag is not set) <p> * * -N number <br> * Set the minimal weights of instances within a split. * (Default: 2) <p> * * @return an enumeration of all the available options */ public Enumeration listOptions() { Vector newVector = new Vector(5); newVector.addElement(new Option("\tSet number of folds for IREP\n" + "\tOne fold is used as pruning set.\n" + "\t(default 3)","F", 1, "-F <number of folds>")); newVector.addElement(new Option("\tSet number of shuffles to randomize\n" + "\tthe data in order to get better rule.\n" + "\t(default 10)","S", 1, "-S <number of shuffles>")); newVector.addElement(new Option("\tSet flag of whether use the error rate \n"+ "\tof all the data to select the default class\n"+ "\tin each step. If not set, the learner will only use"+ "\tthe error rate in the pruning data","A", 0, "-A")); newVector.addElement(new Option("\t Set flag of whether use the majority class as\n"+ "\tthe default class in each step instead of \n"+ "\tchoosing default class based on the error rate\n"+ "\t(if the flag is not set)","M", 0, "-M")); newVector.addElement(new Option("\tSet the minimal weights of instances\n" + "\twithin a split.\n" + "\t(default 2.0)","N", 1, "-N <min. weights>")); return newVector.elements(); } /** * Parses a given list of options. <p/> * <!-- options-start --> * Valid options are: <p/> * * <pre> -F <number of folds> * Set number of folds for IREP * One fold is used as pruning set. * (default 3)</pre> * * <pre> -S <number of shuffles> * Set number of shuffles to randomize * the data in order to get better rule. * (default 10)</pre> * * <pre> -A * Set flag of whether use the error rate * of all the data to select the default class * in each step. If not set, the learner will only use the error rate in the pruning data</pre> * * <pre> -M * Set flag of whether use the majority class as * the default class in each step instead of * choosing default class based on the error rate * (if the flag is not set)</pre> * * <pre> -N <min. weights> * Set the minimal weights of instances * within a split. * (default 2.0)</pre> * <!-- options-end --> * * @param options the list of options as an array of strings * @throws Exception if an option is not supported */ public void setOptions(String[] options) throws Exception { String numFoldsString = Utils.getOption('F', options); if (numFoldsString.length() != 0) m_Folds = Integer.parseInt(numFoldsString); else m_Folds = 3; String numShuffleString = Utils.getOption('S', options); if (numShuffleString.length() != 0) m_Shuffle = Integer.parseInt(numShuffleString); else m_Shuffle = 1; String seedString = Utils.getOption('s', options); if (seedString.length() != 0) m_Seed = Integer.parseInt(seedString); else m_Seed = 1; String minNoString = Utils.getOption('N', options); if (minNoString.length() != 0) m_MinNo = Double.parseDouble(minNoString); else m_MinNo = 2.0; m_IsAllErr = Utils.getFlag('A', options); m_IsMajority = Utils.getFlag('M', options); } /** * Gets the current settings of the Classifier. * * @return an array of strings suitable for passing to setOptions */ public String [] getOptions() { String [] options = new String [8]; int current = 0; options[current++] = "-F"; options[current++] = "" + m_Folds; options[current++] = "-S"; options[current++] = "" + m_Shuffle; options[current++] = "-N"; options[current++] = "" + m_MinNo; if(m_IsAllErr) options[current++] = "-A"; if(m_IsMajority) options[current++] = "-M"; while (current < options.length) options[current++] = ""; return options; } /** Set and get members for parameters */ /** * Returns the tip text for this property * @return tip text for this property suitable for * displaying in the explorer/experimenter gui */ public String foldsTipText() { return "Determines the amount of data used for pruning. One fold is used for " + "pruning, the rest for growing the rules."; } public void setFolds(int fold){ m_Folds = fold; } public int getFolds(){ return m_Folds; } /** * Returns the tip text for this property * @return tip text for this property suitable for * displaying in the explorer/experimenter gui */ public String shuffleTipText() { return "Determines how often the data is shuffled before a rule " + "is chosen. If > 1, a rule is learned multiple times and the " + "most accurate rule is chosen."; } public void setShuffle(int sh){ m_Shuffle = sh; } public int getShuffle(){ return m_Shuffle; } /** * 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 "The seed used for randomizing the data."; } public void setSeed(int s){ m_Seed = s; } public int getSeed(){ return m_Seed; } /** * Returns the tip text for this property * @return tip text for this property suitable for * displaying in the explorer/experimenter gui */ public String wholeDataErrTipText() { return "Whether worth of rule is computed based on all the data " + "or just based on data covered by rule."; } public void setWholeDataErr(boolean a){ m_IsAllErr = a; } public boolean getWholeDataErr(){ return m_IsAllErr; } /** * Returns the tip text for this property * @return tip text for this property suitable for * displaying in the explorer/experimenter gui */ public String majorityClassTipText() { return "Whether the majority class is used as default."; } public void setMajorityClass(boolean m){ m_IsMajority = m; } public boolean getMajorityClass(){ return m_IsMajority; } /** * Returns the tip text for this property * @return tip text for this property suitable for * displaying in the explorer/experimenter gui */ public String minNoTipText() { return "The minimum total weight of the instances in a rule."; } public void setMinNo(double m){ m_MinNo = m; } public double getMinNo(){ return m_MinNo; } /** * Returns an enumeration of the additional measure names * @return an enumeration of the measure names */ public Enumeration enumerateMeasures() { Vector newVector = new Vector(1); newVector.addElement("measureNumRules"); return newVector.elements(); } /** * Returns the value of the named measure * @param additionalMeasureName the name of the measure to query for its value * @return the value of the named measure * @throws IllegalArgumentException if the named measure is not supported */ public double getMeasure(String additionalMeasureName) { if (additionalMeasureName.compareToIgnoreCase("measureNumRules") == 0) return numRules(); else throw new IllegalArgumentException(additionalMeasureName+" not supported (Ripple down rule learner)"); } /** * Measure the number of rules in total in the model * * @return the number of rules */ private double numRules(){ int size = 0; if(m_Root != null) size = m_Root.size(); return (double)(size+1); // Add the default rule } /** * Prints the all the rules of the rule learner. * * @return a textual description of the classifier */ public String toString() { if (m_Root == null) return "RIpple DOwn Rule Learner(Ridor): No model built yet."; return ("RIpple DOwn Rule Learner(Ridor) rules\n"+ "--------------------------------------\n\n" + m_Root.toString() + "\nTotal number of rules (incl. the default rule): " + (int)numRules()); } /** * Returns the revision string. * * @return the revision */ public String getRevision() { return RevisionUtils.extract("$Revision: 5529 $"); } /** * Main method. * * @param args the options for the classifier */ public static void main(String[] args) { runClassifier(new Ridor(), args); } }