001/* 002 * Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.tribuo.common.tree; 018 019import com.google.protobuf.Any; 020import com.google.protobuf.InvalidProtocolBufferException; 021import com.oracle.labs.mlrg.olcut.util.Pair; 022import org.tribuo.Example; 023import org.tribuo.Excuse; 024import org.tribuo.ImmutableFeatureMap; 025import org.tribuo.ImmutableOutputInfo; 026import org.tribuo.Model; 027import org.tribuo.Output; 028import org.tribuo.Prediction; 029import org.tribuo.SparseModel; 030import org.tribuo.common.tree.protos.LeafNodeProto; 031import org.tribuo.common.tree.protos.SplitNodeProto; 032import org.tribuo.common.tree.protos.TreeModelProto; 033import org.tribuo.common.tree.protos.TreeNodeProto; 034import org.tribuo.impl.ModelDataCarrier; 035import org.tribuo.math.la.SparseVector; 036import org.tribuo.protos.core.ModelProto; 037import org.tribuo.provenance.ModelProvenance; 038 039import java.util.ArrayDeque; 040import java.util.ArrayList; 041import java.util.Arrays; 042import java.util.Collections; 043import java.util.Comparator; 044import java.util.HashMap; 045import java.util.HashSet; 046import java.util.LinkedHashSet; 047import java.util.LinkedList; 048import java.util.List; 049import java.util.Map; 050import java.util.Optional; 051import java.util.PriorityQueue; 052import java.util.Queue; 053import java.util.Set; 054 055/** 056 * A {@link Model} wrapped around a decision tree root {@link Node}. 057 */ 058public class TreeModel<T extends Output<T>> extends SparseModel<T> { 059 private static final long serialVersionUID = 3L; 060 061 /** 062 * Protobuf serialization version. 063 */ 064 public static final int CURRENT_VERSION = 0; 065 066 private final Node<T> root; 067 068 /** 069 * Constructs a trained decision tree model. 070 * @param name The model name. 071 * @param description The model provenance. 072 * @param featureIDMap The feature id map. 073 * @param outputIDInfo The output info. 074 * @param generatesProbabilities Does this model emit probabilities. 075 * @param root The root node of the tree. 076 */ 077 TreeModel(String name, ModelProvenance description, ImmutableFeatureMap featureIDMap, ImmutableOutputInfo<T> outputIDInfo, boolean generatesProbabilities, Node<T> root) { 078 super(name, description, featureIDMap, outputIDInfo, generatesProbabilities, gatherActiveFeatures(featureIDMap,root)); 079 this.root = root; 080 } 081 082 /** 083 * Constructs a trained decision tree model. 084 * <p> 085 * Only used when the tree has multiple roots, should only be called from 086 * subclasses when *all* other methods are overridden. 087 * @param name The model name. 088 * @param description The model provenance. 089 * @param featureIDMap The feature id map. 090 * @param outputIDInfo The output info. 091 * @param generatesProbabilities Does this model emit probabilities. 092 * @param activeFeatures The active feature set of the model. 093 */ 094 protected TreeModel(String name, ModelProvenance description, 095 ImmutableFeatureMap featureIDMap, ImmutableOutputInfo<T> outputIDInfo, 096 boolean generatesProbabilities, Map<String,List<String>> activeFeatures) { 097 super(name, description, featureIDMap, outputIDInfo, generatesProbabilities, activeFeatures); 098 this.root = null; 099 } 100 101 /** 102 * Deserialization factory. 103 * @param version The serialized object version. 104 * @param className The class name. 105 * @param message The serialized data. 106 * @throws InvalidProtocolBufferException If the protobuf could not be parsed from the {@code message}. 107 * @return The deserialized object. 108 */ 109 @SuppressWarnings({"unchecked","rawtypes"}) // guarded by getClass to ensure all the output types are the same. 110 public static TreeModel<?> deserializeFromProto(int version, String className, Any message) throws InvalidProtocolBufferException { 111 if (version < 0 || version > CURRENT_VERSION) { 112 throw new IllegalArgumentException("Unknown version " + version + ", this class supports at most version " + CURRENT_VERSION); 113 } 114 TreeModelProto proto = message.unpack(TreeModelProto.class); 115 116 ModelDataCarrier<?> carrier = ModelDataCarrier.deserialize(proto.getMetadata()); 117 Class<?> outputClass = carrier.outputDomain().getOutput(0).getClass(); 118 119 if (proto.getNodesCount() == 0) { 120 throw new IllegalStateException("Invalid protobuf, tree must contain nodes"); 121 } 122 123 List<TreeNodeProto> nodeProtos = proto.getNodesList(); 124 List<Node<?>> nodes = deserializeFromProtos(nodeProtos, (Class) outputClass); 125 126 return new TreeModel(carrier.name(),carrier.provenance(),carrier.featureDomain(),carrier.outputDomain(),carrier.generatesProbabilities(),nodes.get(0)); 127 } 128 129 private static Node<?> deserializeNodeProto(TreeNodeProto proto) throws InvalidProtocolBufferException { 130 int version = proto.getVersion(); 131 String className = proto.getClassName(); 132 Any message = proto.getSerializedData(); 133 if (message.is(SplitNodeProto.class)) { 134 SplitNodeProto splitProto = message.unpack(SplitNodeProto.class); 135 return new SplitNode.SplitNodeBuilder<>(splitProto); 136 } else if (message.is(LeafNodeProto.class)) { 137 LeafNodeProto leafProto = message.unpack(LeafNodeProto.class); 138 return new LeafNode.LeafNodeBuilder<>(leafProto); 139 } else { 140 throw new IllegalStateException("Invalid protobuf, expected leaf or split node, found " + message.getTypeUrl()); 141 } 142 } 143 144 /** 145 * We will start off with a list of node builders that we will replace item-by-item with the nodes 146 * that they built. We will start with the leaf nodes and add split nodes as they become ready 147 * to build. In this way we will travel up the tree and only attempt to build split nodes when 148 * both of their child nodes are available. It may seem a bit tortured to do it this way, but this 149 * approach preserves the immutability of the built nodes (the split nodes in particular). The split node 150 * builder only knows the index of its children when it is deserialized but must be given the actual 151 * nodes before their build method can be called. Note that we only add split node builders to the queue 152 * once they can be built because both children have been created and provided to the builder. 153 * <p> 154 * This approach should traverse the entire tree in the correct order but we check at the end of the method 155 * that everything looks good. 156 * @param nodeProtos The node protos to deserialize. 157 * @param outputClass The output type. 158 * @param <U> The output type of the nodes. 159 * @return The nodes. 160 * @throws InvalidProtocolBufferException If an unexpected proto is found. 161 */ 162 //@SuppressWarnings({"unchecked","rawtypes"}) // guarded by getClass to ensure all the output types are the same. 163 protected static <U extends Output<U>> List<Node<U>> deserializeFromProtos(List<TreeNodeProto> nodeProtos, Class<U> outputClass) throws InvalidProtocolBufferException { 164 List<Node<U>> nodes = new ArrayList<>(nodeProtos.size()); 165 166 for (TreeNodeProto p : nodeProtos) { 167 @SuppressWarnings("unchecked") // we'll catch this later with the getClass check 168 Node<U> curNode = (Node<U>) deserializeNodeProto(p); 169 nodes.add(curNode); 170 } 171 172 Queue<Node<U>> nodeQueue = new ArrayDeque<>(); 173 for (Node<U> node : nodes) { 174 if (node instanceof LeafNode.LeafNodeBuilder) { 175 nodeQueue.offer(node); 176 } 177 } 178 179 while (!nodeQueue.isEmpty()) { 180 Node<U> nodeBuilder = nodeQueue.poll(); 181 int curIdx = -1; 182 Node<U> parent = null; 183 Node<U> builtNode = null; 184 if (nodeBuilder instanceof LeafNode.LeafNodeBuilder) { 185 // build leaf node 186 LeafNode.LeafNodeBuilder<U> builder = (LeafNode.LeafNodeBuilder<U>) nodeBuilder; 187 LeafNode<U> leaf = builder.build(); 188 nodes.set(builder.getCurIdx(), leaf); 189 builtNode = leaf; 190 curIdx = builder.getCurIdx(); 191 // update parent 192 int parentIdx = builder.getParentIdx(); 193 if (parentIdx != -1) { 194 parent = nodes.get(parentIdx); 195 } 196 } else if (nodeBuilder instanceof SplitNode.SplitNodeBuilder) { 197 // build split node now the children are ready 198 SplitNode.SplitNodeBuilder<U> builder = (SplitNode.SplitNodeBuilder<U>) nodeBuilder; 199 SplitNode<U> split = builder.build(); 200 nodes.set(builder.getCurIdx(), split); 201 builtNode = split; 202 curIdx = builder.getCurIdx(); 203 // update parent 204 int parentIdx = builder.getParentIdx(); 205 if (parentIdx != -1) { 206 parent = nodes.get(parentIdx); 207 } 208 } else { 209 throw new IllegalStateException("Invalid protobuf, found a constructed node was added to the build queue, found " + nodeBuilder.getClass()); 210 } 211 if (parent instanceof SplitNode.SplitNodeBuilder) { 212 SplitNode.SplitNodeBuilder<U> splitBuilder = (SplitNode.SplitNodeBuilder<U>) parent; 213 if (curIdx == splitBuilder.getGreaterThanIdx()) { 214 splitBuilder.setGreaterThan(builtNode); 215 } else if (curIdx == splitBuilder.getLessThanOrEqualIdx()) { 216 splitBuilder.setLessThanOrEqual(builtNode); 217 } else { 218 throw new IllegalStateException("Invalid protobuf, found a child node which didn't map into a parent"); 219 } 220 // If we can build this split node pop it on the queue. 221 if (splitBuilder.canBuild()) { 222 nodeQueue.offer(splitBuilder); 223 } 224 } else if (parent != null) { 225 throw new IllegalStateException("Invalid protobuf, found a " + parent.getClass() + " when a SplitNodeBuilder was expected"); 226 } 227 } 228 229 for (Node<U> node : nodes) { 230 if (!(node instanceof SplitNode || node instanceof LeafNode)) { 231 throw new IllegalStateException("Invalid protobuf, found unbuilt node, " + node); 232 } else if (node instanceof LeafNode) { 233 U cur = ((LeafNode<U>) node).getOutput(); 234 if (!outputClass.isAssignableFrom(cur.getClass())) { 235 throw new IllegalStateException("Invalid protobuf, node output did not match output domain, found " + cur.getClass() + ", expected " + outputClass); 236 } 237 } 238 } 239 240 return nodes; 241 } 242 243 private static <T extends Output<T>> Map<String,List<String>> gatherActiveFeatures(ImmutableFeatureMap fMap, Node<T> root) { 244 Set<String> activeFeatures = new LinkedHashSet<>(); 245 246 Queue<Node<T>> nodeQueue = new LinkedList<>(); 247 248 nodeQueue.offer(root); 249 250 while (!nodeQueue.isEmpty()) { 251 Node<T> node = nodeQueue.poll(); 252 if ((node != null) && (!node.isLeaf())) { 253 SplitNode<T> splitNode = (SplitNode<T>) node; 254 String featureName = fMap.get(splitNode.getFeatureID()).getName(); 255 activeFeatures.add(featureName); 256 nodeQueue.offer(splitNode.getGreaterThan()); 257 nodeQueue.offer(splitNode.getLessThanOrEqual()); 258 } 259 } 260 return Collections.singletonMap(Model.ALL_OUTPUTS,new ArrayList<>(activeFeatures)); 261 } 262 263 /** 264 * Probes the tree to find the depth. 265 * @return The depth of the tree. 266 */ 267 public int getDepth() { 268 return computeDepth(0,root); 269 } 270 271 /** 272 * Computes the depth of the tree. 273 * @param initialDepth The current depth. 274 * @param root The root to probe. 275 * @return The tree depth. 276 * @param <T> The output type of the tree. 277 */ 278 protected static <T extends Output<T>> int computeDepth(int initialDepth, Node<T> root) { 279 int maxDepth = initialDepth; 280 Queue<Pair<Integer,Node<T>>> nodeQueue = new LinkedList<>(); 281 282 nodeQueue.offer(new Pair<>(initialDepth,root)); 283 284 while (!nodeQueue.isEmpty()) { 285 Pair<Integer,Node<T>> nodePair = nodeQueue.poll(); 286 int curDepth = nodePair.getA() + 1; 287 Node<T> node = nodePair.getB(); 288 if ((node != null) && !node.isLeaf()) { 289 SplitNode<T> splitNode = (SplitNode<T>) node; 290 Node<T> greaterThan = splitNode.getGreaterThan(); 291 Node<T> lessThan = splitNode.getLessThanOrEqual(); 292 if (greaterThan instanceof LeafNode) { 293 if (maxDepth < curDepth) { 294 maxDepth = curDepth; 295 } 296 } else { 297 nodeQueue.offer(new Pair<>(curDepth,greaterThan)); 298 } 299 if (lessThan instanceof LeafNode) { 300 if (maxDepth < curDepth) { 301 maxDepth = curDepth; 302 } 303 } else { 304 nodeQueue.offer(new Pair<>(curDepth,lessThan)); 305 } 306 } 307 } 308 309 return maxDepth; 310 } 311 312 @Override 313 public Prediction<T> predict(Example<T> example) { 314 // 315 // Ensures we handle collisions correctly 316 SparseVector vec = SparseVector.createSparseVector(example,featureIDMap,false); 317 if (vec.numActiveElements() == 0) { 318 throw new IllegalArgumentException("No features found in Example " + example.toString()); 319 } 320 Node<T> oldNode = root; 321 Node<T> curNode = root; 322 323 while (curNode != null) { 324 oldNode = curNode; 325 curNode = oldNode.getNextNode(vec); 326 } 327 328 // 329 // oldNode must be a LeafNode. 330 return ((LeafNode<T>) oldNode).getPrediction(vec.numActiveElements(),example); 331 } 332 333 @Override 334 public Map<String, List<Pair<String,Double>>> getTopFeatures(int n) { 335 int maxFeatures = n < 0 ? featureIDMap.size() : n; 336 Map<String,Integer> featureCounts = new HashMap<>(); 337 338 Queue<Node<T>> nodeQueue = new LinkedList<>(); 339 340 nodeQueue.offer(root); 341 342 while (!nodeQueue.isEmpty()) { 343 Node<T> node = nodeQueue.poll(); 344 if ((node != null) && !node.isLeaf()) { 345 SplitNode<T> splitNode = (SplitNode<T>) node; 346 String featureName = featureIDMap.get(splitNode.getFeatureID()).getName(); 347 featureCounts.put(featureName, featureCounts.getOrDefault(featureName, 0) + 1); 348 nodeQueue.offer(splitNode.getGreaterThan()); 349 nodeQueue.offer(splitNode.getLessThanOrEqual()); 350 } 351 } 352 353 Comparator<Pair<String,Double>> comparator = Comparator.comparingDouble(p -> Math.abs(p.getB())); 354 PriorityQueue<Pair<String,Double>> q = new PriorityQueue<>(maxFeatures, comparator); 355 356 for (Map.Entry<String, Integer> e : featureCounts.entrySet()) { 357 Pair<String,Double> cur = new Pair<>(e.getKey(), (double) e.getValue()); 358 if (q.size() < maxFeatures) { 359 q.offer(cur); 360 } else if (comparator.compare(cur, q.peek()) > 0) { 361 q.poll(); 362 q.offer(cur); 363 } 364 } 365 List<Pair<String,Double>> list = new ArrayList<>(); 366 while (q.size() > 0) { 367 list.add(q.poll()); 368 } 369 Collections.reverse(list); 370 371 Map<String,List<Pair<String,Double>>> map = new HashMap<>(); 372 map.put(Model.ALL_OUTPUTS, list); 373 374 return map; 375 } 376 377 @Override 378 public Optional<Excuse<T>> getExcuse(Example<T> example) { 379 List<String> list = new ArrayList<>(); 380 // 381 // Ensures we handle collisions correctly 382 SparseVector vec = SparseVector.createSparseVector(example,featureIDMap,false); 383 Node<T> oldNode = root; 384 Node<T> curNode = root; 385 386 while (curNode != null) { 387 oldNode = curNode; 388 if (oldNode instanceof SplitNode) { 389 SplitNode<T> node = (SplitNode<T>) curNode; 390 list.add(featureIDMap.get(node.getFeatureID()).getName()); 391 } 392 curNode = oldNode.getNextNode(vec); 393 } 394 395 // 396 // oldNode must be a LeafNode. 397 Prediction<T> pred = ((LeafNode<T>) oldNode).getPrediction(vec.numActiveElements(),example); 398 399 List<Pair<String,Double>> pairs = new ArrayList<>(); 400 int i = list.size() + 1; 401 for (String s : list) { 402 pairs.add(new Pair<>(s,i+0.0)); 403 i--; 404 } 405 406 Map<String,List<Pair<String,Double>>> map = new HashMap<>(); 407 map.put(Model.ALL_OUTPUTS,pairs); 408 409 return Optional.of(new Excuse<>(example,pred,map)); 410 } 411 412 @Override 413 protected TreeModel<T> copy(String newName, ModelProvenance newProvenance) { 414 return new TreeModel<>(newName,newProvenance,featureIDMap,outputIDInfo,generatesProbabilities,root.copy()); 415 } 416 417 /** 418 * Returns the set of features which are split on in this tree. 419 * @return The feature names used by this tree. 420 */ 421 public Set<String> getFeatures() { 422 Set<String> features = new HashSet<>(); 423 424 Queue<Node<T>> nodeQueue = new LinkedList<>(); 425 426 nodeQueue.offer(root); 427 428 while (!nodeQueue.isEmpty()) { 429 Node<T> node = nodeQueue.poll(); 430 if ((node != null) && !node.isLeaf()) { 431 SplitNode<T> splitNode = (SplitNode<T>) node; 432 features.add(featureIDMap.get(splitNode.getFeatureID()).getName()); 433 nodeQueue.offer(splitNode.getGreaterThan()); 434 nodeQueue.offer(splitNode.getLessThanOrEqual()); 435 } 436 } 437 438 return features; 439 } 440 441 /** 442 * Counts the number of nodes in the tree rooted at the supplied node, including that node. 443 * @param root The tree root. 444 * @return The number of nodes. 445 */ 446 public int countNodes(Node<T> root) { 447 Queue<Node<T>> nodeQueue = new LinkedList<>(); 448 449 int counter = 0; 450 nodeQueue.offer(root); 451 452 while (!nodeQueue.isEmpty()) { 453 Node<T> node = nodeQueue.poll(); 454 if (node != null) { 455 counter++; 456 if (!node.isLeaf()) { 457 SplitNode<T> splitNode = (SplitNode<T>) node; 458 nodeQueue.offer(splitNode.getGreaterThan()); 459 nodeQueue.offer(splitNode.getLessThanOrEqual()); 460 } 461 } 462 } 463 464 return counter; 465 } 466 467 @Override 468 public String toString() { 469 return "TreeModel(description="+provenance.toString()+",\n\t\ttree="+root.toString()+")"; 470 } 471 472 /** 473 * Returns the root node of this tree. 474 * @return The root node. 475 */ 476 public Node<T> getRoot() { 477 return root; 478 } 479 480 @Override 481 public ModelProto serialize() { 482 ModelDataCarrier<T> carrier = createDataCarrier(); 483 484 TreeModelProto.Builder modelBuilder = TreeModelProto.newBuilder(); 485 modelBuilder.setMetadata(carrier.serialize()); 486 modelBuilder.addAllNodes(serializeToNodes(root)); 487 488 ModelProto.Builder builder = ModelProto.newBuilder(); 489 builder.setSerializedData(Any.pack(modelBuilder.build())); 490 builder.setClassName(TreeModel.class.getName()); 491 builder.setVersion(CURRENT_VERSION); 492 493 return builder.build(); 494 } 495 496 /** 497 * Serializes the supplied node tree into a list of protobufs. 498 * @param root The root of the tree to serialize. 499 * @return The protobuf list. 500 */ 501 protected List<TreeNodeProto> serializeToNodes(Node<T> root) { 502 int numNodes = countNodes(root); 503 TreeNodeProto[] protos = new TreeNodeProto[numNodes]; 504 505 int counter = 0; 506 Queue<SerializationState<T>> nodeQueue = new ArrayDeque<>(); 507 nodeQueue.offer(new SerializationState<>(-1,counter,root)); 508 while (!nodeQueue.isEmpty()) { 509 SerializationState<T> state = nodeQueue.poll(); 510 if (state.node instanceof SplitNode) { 511 SplitNode<T> node = (SplitNode<T>) state.node; 512 int greaterIdx = ++counter; 513 int lessIdx = ++counter; 514 TreeNodeProto proto = node.serialize(state.parentIdx, state.curIdx, greaterIdx, lessIdx); 515 protos[state.curIdx] = proto; 516 nodeQueue.offer(new SerializationState<>(state.curIdx, greaterIdx, node.getGreaterThan())); 517 nodeQueue.offer(new SerializationState<>(state.curIdx, lessIdx, node.getLessThanOrEqual())); 518 } else if (state.node instanceof LeafNode) { 519 LeafNode<T> node = (LeafNode<T>) state.node; 520 TreeNodeProto proto = node.serialize(state.parentIdx, state.curIdx); 521 protos[state.curIdx] = proto; 522 } else { 523 throw new IllegalStateException("Invalid tree structure, contained a node which wasn't a SplitNode or a LeafNode, found " + state.node.getClass()); 524 } 525 } 526 527 return Arrays.asList(protos); 528 } 529 530 private static final class SerializationState<T extends Output<T>> { 531 final int parentIdx; 532 final int curIdx; 533 final Node<T> node; 534 535 SerializationState(int parentIdx, int curIdx, Node<T> node) { 536 this.parentIdx = parentIdx; 537 this.curIdx = curIdx; 538 this.node = node; 539 } 540 } 541 542 static abstract class NodeBuilder { 543 abstract int getParentIdx(); 544 abstract int getCurIdx(); 545 abstract Node<?> build(); 546 } 547}