You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mahout.apache.org by ad...@apache.org on 2011/12/26 17:24:37 UTC

svn commit: r1224751 - in /mahout/trunk/core/src: main/java/org/apache/mahout/classifier/df/tools/ test/java/org/apache/mahout/classifier/df/tools/

Author: adeneche
Date: Mon Dec 26 16:24:37 2011
New Revision: 1224751

URL: http://svn.apache.org/viewvc?rev=1224751&view=rev
Log:
MAHOUT-926 Tree/Forest Visualizer

Added:
    mahout/trunk/core/src/main/java/org/apache/mahout/classifier/df/tools/ForestVisualizer.java
    mahout/trunk/core/src/main/java/org/apache/mahout/classifier/df/tools/TreeVisualizer.java
    mahout/trunk/core/src/test/java/org/apache/mahout/classifier/df/tools/
    mahout/trunk/core/src/test/java/org/apache/mahout/classifier/df/tools/VisualizerTest.java

Added: mahout/trunk/core/src/main/java/org/apache/mahout/classifier/df/tools/ForestVisualizer.java
URL: http://svn.apache.org/viewvc/mahout/trunk/core/src/main/java/org/apache/mahout/classifier/df/tools/ForestVisualizer.java?rev=1224751&view=auto
==============================================================================
--- mahout/trunk/core/src/main/java/org/apache/mahout/classifier/df/tools/ForestVisualizer.java (added)
+++ mahout/trunk/core/src/main/java/org/apache/mahout/classifier/df/tools/ForestVisualizer.java Mon Dec 26 16:24:37 2011
@@ -0,0 +1,149 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mahout.classifier.df.tools;
+
+import java.lang.reflect.Method;
+import java.util.List;
+
+import org.apache.commons.cli2.CommandLine;
+import org.apache.commons.cli2.Group;
+import org.apache.commons.cli2.Option;
+import org.apache.commons.cli2.builder.ArgumentBuilder;
+import org.apache.commons.cli2.builder.DefaultOptionBuilder;
+import org.apache.commons.cli2.builder.GroupBuilder;
+import org.apache.commons.cli2.commandline.Parser;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.apache.mahout.classifier.df.DecisionForest;
+import org.apache.mahout.classifier.df.data.Dataset;
+import org.apache.mahout.classifier.df.node.Node;
+import org.apache.mahout.common.CommandLineUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This tool is to visualize the Decision Forest
+ */
+public final class ForestVisualizer {
+
+  private static final Logger log = LoggerFactory.getLogger(ForestVisualizer.class);
+
+  private ForestVisualizer() {
+  }
+
+  public static String toString(DecisionForest forest, Dataset dataset, String[] attrNames)
+    throws Exception {
+    Method getTrees = forest.getClass().getDeclaredMethod("getTrees");
+    getTrees.setAccessible(true);
+    @SuppressWarnings("unchecked")
+    List<Node> trees = (List<Node>) getTrees.invoke(forest);
+    
+    int cnt = 1;
+    StringBuffer buff = new StringBuffer();
+    for (Node tree : trees) {
+      buff.append("Tree[" + cnt + "]:");
+      buff.append(TreeVisualizer.toString(tree, dataset, attrNames));
+      buff.append('\n');
+      cnt++;
+    }
+    return buff.toString();
+  }
+
+  /**
+   * Decision Forest to String
+   * @param forestPath
+   *          path to the Decision Forest
+   * @param datasetPath
+   *          dataset path
+   * @param attrNames
+   *          attribute names
+   */
+  public static String toString(String forestPath, String datasetPath, String[] attrNames)
+    throws Exception {
+    Configuration conf = new Configuration();
+    DecisionForest forest = DecisionForest.load(conf, new Path(forestPath));
+    Dataset dataset = Dataset.load(conf, new Path(datasetPath));
+    return toString(forest, dataset, attrNames);
+  }
+
+  /**
+   * Print Decision Forest
+   * @param forestPath
+   *          path to the Decision Forest
+   * @param datasetPath
+   *          dataset path
+   * @param attrNames
+   *          attribute names
+   */
+  public static void print(String forestPath, String datasetPath, String[] attrNames)
+    throws Exception {
+    System.out.println(toString(forestPath, datasetPath, attrNames));
+  }
+  
+  public static void main(String[] args) {
+    DefaultOptionBuilder obuilder = new DefaultOptionBuilder();
+    ArgumentBuilder abuilder = new ArgumentBuilder();
+    GroupBuilder gbuilder = new GroupBuilder();
+
+    Option datasetOpt = obuilder.withLongName("dataset").withShortName("ds").withRequired(true)
+      .withArgument(abuilder.withName("dataset").withMinimum(1).withMaximum(1).create())
+      .withDescription("Dataset path").create();
+
+    Option modelOpt = obuilder.withLongName("model").withShortName("m").withRequired(true)
+      .withArgument(abuilder.withName("path").withMinimum(1).withMaximum(1).create())
+      .withDescription("Path to the Decision Forest").create();
+
+    Option attrNamesOpt = obuilder.withLongName("names").withShortName("n").withRequired(false)
+      .withArgument(abuilder.withName("names").withMinimum(1).create())
+      .withDescription("Optional, Attribute names").create();
+
+    Option helpOpt = obuilder.withLongName("help").withShortName("h")
+      .withDescription("Print out help").create();
+  
+    Group group = gbuilder.withName("Options").withOption(datasetOpt).withOption(modelOpt)
+      .withOption(attrNamesOpt).withOption(helpOpt).create();
+  
+    try {
+      Parser parser = new Parser();
+      parser.setGroup(group);
+      CommandLine cmdLine = parser.parse(args);
+      
+      if (cmdLine.hasOption("help")) {
+        CommandLineUtil.printHelp(group);
+        return;
+      }
+  
+      String datasetName = cmdLine.getValue(datasetOpt).toString();
+      String modelName = cmdLine.getValue(modelOpt).toString();
+      String[] attrNames = null;
+      if (cmdLine.hasOption(attrNamesOpt)) {
+        List<?> names = cmdLine.getValues(attrNamesOpt);
+        if (!names.isEmpty()) {
+          attrNames = new String[names.size()];
+          names.toArray(attrNames);
+        }
+      }
+      
+      print(modelName, datasetName, attrNames);
+    } catch (Exception e) {
+      log.error("Exception", e);
+      CommandLineUtil.printHelp(group);
+      return;
+    }
+  }
+}

Added: mahout/trunk/core/src/main/java/org/apache/mahout/classifier/df/tools/TreeVisualizer.java
URL: http://svn.apache.org/viewvc/mahout/trunk/core/src/main/java/org/apache/mahout/classifier/df/tools/TreeVisualizer.java?rev=1224751&view=auto
==============================================================================
--- mahout/trunk/core/src/main/java/org/apache/mahout/classifier/df/tools/TreeVisualizer.java (added)
+++ mahout/trunk/core/src/main/java/org/apache/mahout/classifier/df/tools/TreeVisualizer.java Mon Dec 26 16:24:37 2011
@@ -0,0 +1,241 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mahout.classifier.df.tools;
+
+import java.lang.reflect.Field;
+import java.text.DecimalFormat;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.mahout.classifier.df.data.Data;
+import org.apache.mahout.classifier.df.data.Dataset;
+import org.apache.mahout.classifier.df.data.Instance;
+import org.apache.mahout.classifier.df.node.CategoricalNode;
+import org.apache.mahout.classifier.df.node.Leaf;
+import org.apache.mahout.classifier.df.node.Node;
+import org.apache.mahout.classifier.df.node.NumericalNode;
+
+/**
+ * This tool is to visualize the Decision tree
+ */
+public final class TreeVisualizer {
+
+  private TreeVisualizer() {
+  }
+
+  private static String doubleToString(double value) {
+    DecimalFormat df = new DecimalFormat("0.##");
+    return df.format(value);
+  }
+
+  private static String toStringNode(Node node, Dataset dataset, String[] attrNames,
+    Map<String, Field> fields, int layer) throws IllegalAccessException {
+    StringBuffer buff = new StringBuffer();
+
+    if (node instanceof CategoricalNode) {
+      CategoricalNode cnode = (CategoricalNode) node;
+      int attr = (Integer) fields.get("CategoricalNode.attr").get(cnode);
+      double[] values = (double[]) fields.get("CategoricalNode.values").get(cnode);
+      Node[] childs = (Node[]) fields.get("CategoricalNode.childs").get(cnode);
+      String[][] attrValues = (String[][]) fields.get("Dataset.values").get(dataset);
+      for (int i = 0; i < childs.length; i++) {
+        buff.append("\n");
+        for (int j = 0; j < layer; j++) {
+          buff.append("|   ");
+        }
+        buff.append((attrNames == null ? attr : attrNames[attr]) + " = " + attrValues[attr][i]);
+        int index = ArrayUtils.indexOf(values, i);
+        if (index >= 0) {
+          buff.append(toStringNode(childs[index], dataset, attrNames, fields, layer + 1));
+        }
+      }
+    } else if (node instanceof NumericalNode) {
+      NumericalNode nnode = (NumericalNode) node;
+      int attr = (Integer) fields.get("NumericalNode.attr").get(nnode);
+      double split = (Double) fields.get("NumericalNode.split").get(nnode);
+      Node loChild = (Node) fields.get("NumericalNode.loChild").get(nnode);
+      Node hiChild = (Node) fields.get("NumericalNode.hiChild").get(nnode);
+      buff.append("\n");
+      for (int j = 0; j < layer; j++) {
+        buff.append("|   ");
+      }
+      buff.append((attrNames == null ? attr : attrNames[attr]) + " < " + doubleToString(split));
+      buff.append(toStringNode(loChild, dataset, attrNames, fields, layer + 1));
+      buff.append("\n");
+      for (int j = 0; j < layer; j++) {
+        buff.append("|   ");
+      }
+      buff.append((attrNames == null ? attr : attrNames[attr]) + " >= " + doubleToString(split));
+      buff.append(toStringNode(hiChild, dataset, attrNames, fields, layer + 1));
+    } else if (node instanceof Leaf) {
+      Leaf leaf = (Leaf) node;
+      double label = (Double) fields.get("Leaf.label").get(leaf);
+      if (dataset.isNumerical(dataset.getLabelId())) {
+        buff.append(" : " + doubleToString(label));
+      } else {
+        buff.append(" : " + dataset.getLabelString((int) label));
+      }
+    }
+
+    return buff.toString();
+  }
+
+  private static Map<String, Field> getReflectMap() throws Exception {
+    Map<String, Field> fields = new HashMap<String, Field>();
+
+    Field m = CategoricalNode.class.getDeclaredField("attr");
+    m.setAccessible(true);
+    fields.put("CategoricalNode.attr", m);
+    m = CategoricalNode.class.getDeclaredField("values");
+    m.setAccessible(true);
+    fields.put("CategoricalNode.values", m);
+    m = CategoricalNode.class.getDeclaredField("childs");
+    m.setAccessible(true);
+    fields.put("CategoricalNode.childs", m);
+    m = NumericalNode.class.getDeclaredField("attr");
+    m.setAccessible(true);
+    fields.put("NumericalNode.attr", m);
+    m = NumericalNode.class.getDeclaredField("split");
+    m.setAccessible(true);
+    fields.put("NumericalNode.split", m);
+    m = NumericalNode.class.getDeclaredField("loChild");
+    m.setAccessible(true);
+    fields.put("NumericalNode.loChild", m);
+    m = NumericalNode.class.getDeclaredField("hiChild");
+    m.setAccessible(true);
+    fields.put("NumericalNode.hiChild", m);
+    m = Leaf.class.getDeclaredField("label");
+    m.setAccessible(true);
+    fields.put("Leaf.label", m);
+    m = Dataset.class.getDeclaredField("values");
+    m.setAccessible(true);
+    fields.put("Dataset.values", m);
+    
+    return fields;
+  }
+  
+  /**
+   * Decision tree to String
+   * @param tree
+   *          Node of tree
+   * @param dataset
+   * @param attrNames
+   *          attribute names
+   */
+  public static String toString(Node tree, Dataset dataset, String[] attrNames)
+    throws Exception {
+    return toStringNode(tree, dataset, attrNames, getReflectMap(), 0);
+  }
+
+  /**
+   * Print Decision tree
+   * @param tree
+   *          Node of tree
+   * @param dataset
+   * @param attrNames
+   *          attribute names
+   */
+  public static void print(Node tree, Dataset dataset, String[] attrNames) throws Exception {
+    System.out.println(toString(tree, dataset, attrNames));
+  }
+
+  private static String toStringPredict(Node node, Instance instance, Dataset dataset,
+    String[] attrNames, Map<String, Field> fields) throws IllegalAccessException {
+    StringBuffer buff = new StringBuffer();
+
+    if (node instanceof CategoricalNode) {
+      CategoricalNode cnode = (CategoricalNode) node;
+      int attr = (Integer) fields.get("CategoricalNode.attr").get(cnode);
+      double[] values = (double[]) fields.get("CategoricalNode.values").get(cnode);
+      Node[] childs = (Node[]) fields.get("CategoricalNode.childs").get(cnode);
+      String[][] attrValues = (String[][]) fields.get("Dataset.values").get(dataset);
+
+      int index = ArrayUtils.indexOf(values, instance.get(attr));
+      if (index >= 0) {
+        buff.append((attrNames == null ? attr : attrNames[attr]) + " = "
+          + attrValues[attr][(int) instance.get(attr)]);
+        buff.append(" -> ");
+        buff.append(toStringPredict(childs[index], instance, dataset, attrNames, fields));
+      }
+    } else if (node instanceof NumericalNode) {
+      NumericalNode nnode = (NumericalNode) node;
+      int attr = (Integer) fields.get("NumericalNode.attr").get(nnode);
+      double split = (Double) fields.get("NumericalNode.split").get(nnode);
+      Node loChild = (Node) fields.get("NumericalNode.loChild").get(nnode);
+      Node hiChild = (Node) fields.get("NumericalNode.hiChild").get(nnode);
+
+      if (instance.get(attr) < split) {
+        buff.append("(" + (attrNames == null ? attr : attrNames[attr]) + " = "
+          + doubleToString(instance.get(attr)) + ") < " + doubleToString(split));
+        buff.append(" -> ");
+        buff.append(toStringPredict(loChild, instance, dataset, attrNames, fields));
+      } else {
+        buff.append("(" + (attrNames == null ? attr : attrNames[attr]) + " = "
+          + doubleToString(instance.get(attr)) + ") >= " + doubleToString(split));
+        buff.append(" -> ");
+        buff.append(toStringPredict(hiChild, instance, dataset, attrNames, fields));
+      }
+    } else if (node instanceof Leaf) {
+      Leaf leaf = (Leaf) node;
+      double label = (Double) fields.get("Leaf.label").get(leaf);
+      if (dataset.isNumerical(dataset.getLabelId())) {
+        buff.append(doubleToString(label));
+      } else {
+        buff.append(dataset.getLabelString((int) label));
+      }
+    }
+
+    return buff.toString();
+  }
+
+  /**
+   * Predict trace to String
+   * @param tree
+   *          Node of tree
+   * @param data
+   * @param attrNames
+   *          attribute names
+   */
+  public static String[] predictTrace(Node tree, Data data, String[] attrNames)
+    throws Exception {
+    Map<String, Field> reflectMap = getReflectMap();
+    String[] prediction = new String[data.size()];
+    for (int i = 0; i < data.size(); i++) {
+      prediction[i] = toStringPredict(tree, data.get(i), data.getDataset(), attrNames, reflectMap);
+    }
+    return prediction;
+  }
+
+  /**
+   * Print predict trace
+   * @param tree
+   *          Node of tree
+   * @param data
+   * @param attrNames
+   *          attribute names
+   */
+  public static void predictTracePrint(Node tree, Data data, String[] attrNames)
+    throws Exception {
+    Map<String, Field> reflectMap = getReflectMap();
+    for (int i = 0; i < data.size(); i++) {
+      System.out.println(toStringPredict(tree, data.get(i), data.getDataset(), attrNames,
+        reflectMap));
+    }
+  }
+}

Added: mahout/trunk/core/src/test/java/org/apache/mahout/classifier/df/tools/VisualizerTest.java
URL: http://svn.apache.org/viewvc/mahout/trunk/core/src/test/java/org/apache/mahout/classifier/df/tools/VisualizerTest.java?rev=1224751&view=auto
==============================================================================
--- mahout/trunk/core/src/test/java/org/apache/mahout/classifier/df/tools/VisualizerTest.java (added)
+++ mahout/trunk/core/src/test/java/org/apache/mahout/classifier/df/tools/VisualizerTest.java Mon Dec 26 16:24:37 2011
@@ -0,0 +1,124 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.mahout.classifier.df.tools;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import org.apache.mahout.classifier.df.DecisionForest;
+import org.apache.mahout.classifier.df.builder.DecisionTreeBuilder;
+import org.apache.mahout.classifier.df.data.Data;
+import org.apache.mahout.classifier.df.data.DataLoader;
+import org.apache.mahout.classifier.df.data.Dataset;
+import org.apache.mahout.classifier.df.node.CategoricalNode;
+import org.apache.mahout.classifier.df.node.Leaf;
+import org.apache.mahout.classifier.df.node.Node;
+import org.apache.mahout.classifier.df.node.NumericalNode;
+import org.apache.mahout.common.MahoutTestCase;
+import org.apache.mahout.common.RandomUtils;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public final class VisualizerTest extends MahoutTestCase {
+  
+  private static final String[] TRAIN_DATA = {"sunny,85,85,FALSE,no",
+    "sunny,80,90,TRUE,no", "overcast,83,86,FALSE,yes",
+    "rainy,70,96,FALSE,yes", "rainy,68,80,FALSE,yes", "rainy,65,70,TRUE,no",
+    "overcast,64,65,TRUE,yes", "sunny,72,95,FALSE,no",
+    "sunny,69,70,FALSE,yes", "rainy,75,80,FALSE,yes", "sunny,75,70,TRUE,yes",
+    "overcast,72,90,TRUE,yes", "overcast,81,75,FALSE,yes",
+    "rainy,71,91,TRUE,no"};
+  
+  private static final String[] TEST_DATA = {"rainy,70,96,TRUE,-",
+    "overcast,64,65,TRUE,-", "sunny,75,90,TRUE,-",};
+  
+  private static final String[] ATTR_NAMES = {"outlook", "temperature",
+    "humidity", "windy", "play"};
+  
+  private Random rng;
+  private Data data;
+  private Data testData;
+  
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    
+    rng = RandomUtils.getRandom();
+    
+    // Dataset
+    Dataset dataset = DataLoader.generateDataset("C N N C L", false, TRAIN_DATA);
+    
+    // Training data
+    data = DataLoader.loadData(dataset, TRAIN_DATA);
+    
+    // Test data
+    testData = DataLoader.loadData(dataset, TEST_DATA);
+  }
+  
+  @Test
+  public void testTreeVisualize() throws Exception {
+    // build tree
+    DecisionTreeBuilder builder = new DecisionTreeBuilder();
+    builder.setM(data.getDataset().nbAttributes() - 1);
+    Node tree = builder.build(rng, data);
+    
+    assertEquals(TreeVisualizer.toString(tree, data.getDataset(), ATTR_NAMES),
+      "\noutlook = rainy\n|   windy = FALSE : yes\n|   windy = TRUE : no\n"
+        + "outlook = sunny\n|   humidity < 85 : yes\n|   humidity >= 85 : no\n"
+        + "outlook = overcast : yes");
+  }
+  
+  @Test
+  public void testPredictTrace() throws Exception {
+    // build tree
+    DecisionTreeBuilder builder = new DecisionTreeBuilder();
+    builder.setM(data.getDataset().nbAttributes() - 1);
+    Node tree = builder.build(rng, data);
+    
+    String[] prediction = TreeVisualizer.predictTrace(tree, testData, ATTR_NAMES);
+    Assert.assertArrayEquals(prediction, new String[] {
+      "outlook = rainy -> windy = TRUE -> no", "outlook = overcast -> yes",
+      "outlook = sunny -> (humidity = 90) >= 85 -> no"});
+  }
+  
+  @Test
+  public void testForestVisualize() throws Exception {
+    // Tree
+    NumericalNode root = new NumericalNode(2, 90, new Leaf(0),
+      new CategoricalNode(0, new double[] {0, 1, 2}, new Node[] {
+        new NumericalNode(1, 71, new Leaf(0), new Leaf(1)), new Leaf(1),
+        new Leaf(0)}));
+    List<Node> trees = new ArrayList<Node>();
+    trees.add(root);
+    
+    // Forest
+    DecisionForest forest = new DecisionForest(trees);
+    assertEquals(ForestVisualizer.toString(forest, data.getDataset(), null),
+      "Tree[1]:\n2 < 90 : yes\n2 >= 90\n"
+        + "|   0 = rainy\n|   |   1 < 71 : yes\n|   |   1 >= 71 : no\n"
+        + "|   0 = sunny : no\n" + "|   0 = overcast : yes\n");
+    
+    assertEquals(ForestVisualizer.toString(forest, data.getDataset(), ATTR_NAMES),
+      "Tree[1]:\nhumidity < 90 : yes\nhumidity >= 90\n"
+        + "|   outlook = rainy\n|   |   temperature < 71 : yes\n|   |   temperature >= 71 : no\n"
+        + "|   outlook = sunny : no\n" + "|   outlook = overcast : yes\n");
+  }
+}