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}