001/* 002 * Copyright (c) 2015-2020, 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 org.tribuo.Example; 021import org.tribuo.Output; 022import org.tribuo.Prediction; 023import org.tribuo.common.tree.protos.LeafNodeProto; 024import org.tribuo.common.tree.protos.TreeNodeProto; 025import org.tribuo.math.la.SparseVector; 026import org.tribuo.protos.core.OutputProto; 027 028import java.util.Collections; 029import java.util.HashMap; 030import java.util.Map; 031import java.util.Objects; 032 033/** 034 * An immutable leaf {@link Node} that can create a prediction. 035 * <p> 036 * {@link LeafNode#equals} uses the {@link Output#fullEquals(Output)} method 037 * to determine equality of two leaves. 038 */ 039public class LeafNode<T extends Output<T>> implements Node<T> { 040 private static final long serialVersionUID = 4L; 041 042 /** 043 * Protobuf serialization version. 044 */ 045 public static final int CURRENT_VERSION = 0; 046 047 private final double impurity; 048 049 private final T output; 050 private final Map<String,T> scores; 051 private final boolean generatesProbabilities; 052 053 /** 054 * Constructs a leaf node. 055 * @param impurity The impurity value calculated at training time. 056 * @param output The output value from this node. 057 * @param scores The score map for the other outputs. 058 * @param generatesProbabilities If the scores are probabilities. 059 */ 060 public LeafNode(double impurity, T output, Map<String,T> scores, boolean generatesProbabilities) { 061 this.impurity = impurity; 062 this.output = output; 063 this.scores = Collections.unmodifiableMap(scores); 064 this.generatesProbabilities = generatesProbabilities; 065 } 066 067 @Override 068 public boolean equals(Object o) { 069 if (this == o) return true; 070 if (o == null || getClass() != o.getClass()) return false; 071 LeafNode<?> leafNode = (LeafNode<?>) o; 072 if (output.getClass().equals(leafNode.output.getClass())) { 073 @SuppressWarnings("unchecked") //guarded by class check 074 LeafNode<T> typedLeafNode = (LeafNode<T>) leafNode; 075 // If the scores have the same keys. 076 if (scores.keySet().equals(typedLeafNode.scores.keySet())) { 077 // Check the values are the same. 078 boolean valueEquals = true; 079 for (Map.Entry<String,T> e : scores.entrySet()) { 080 valueEquals &= e.getValue().fullEquals(typedLeafNode.scores.get(e.getKey())); 081 } 082 // Check the rest of the object. 083 return valueEquals && 084 Double.compare(typedLeafNode.impurity, impurity) == 0 && 085 generatesProbabilities == typedLeafNode.generatesProbabilities && 086 output.fullEquals(typedLeafNode.output); 087 } 088 } 089 return false; 090 } 091 092 @Override 093 public int hashCode() { 094 return Objects.hash(impurity, output, scores, generatesProbabilities); 095 } 096 097 @Override 098 public Node<T> getNextNode(SparseVector e) { 099 return null; 100 } 101 102 @Override 103 public boolean isLeaf() { 104 return true; 105 } 106 107 @Override 108 public double getImpurity() { 109 return impurity; 110 } 111 112 @Override 113 public LeafNode<T> copy() { 114 return new LeafNode<>(impurity,output.copy(),new HashMap<>(scores),generatesProbabilities); 115 } 116 117 /** 118 * Gets the output in this node. 119 * @return The output. 120 */ 121 public T getOutput() { 122 return output; 123 } 124 125 /** 126 * Gets the distribution over scores in this node. 127 * @return The score distribution. 128 */ 129 public Map<String,T> getDistribution() { 130 return scores; 131 } 132 133 /** 134 * Constructs a new prediction object based on this node's scores. 135 * @param numUsed The number of features used. 136 * @param example The example to be scored. 137 * @return The prediction for the supplied example. 138 */ 139 public Prediction<T> getPrediction(int numUsed, Example<T> example) { 140 return new Prediction<>(output,scores,numUsed,example,generatesProbabilities); 141 } 142 143 @Override 144 public String toString() { 145 return "LeafNode(impurity="+impurity+",output="+output.toString()+",scores="+scores.toString()+",probability="+generatesProbabilities+")"; 146 } 147 148 TreeNodeProto serialize(int parentIdx, int curIdx) { 149 LeafNodeProto.Builder nodeBuilder = LeafNodeProto.newBuilder(); 150 nodeBuilder.setParentIdx(parentIdx); 151 nodeBuilder.setCurIdx(curIdx); 152 nodeBuilder.setOutput(output.serialize()); 153 for (Map.Entry<String, T> e : scores.entrySet()) { 154 nodeBuilder.putScore(e.getKey(), e.getValue().serialize()); 155 } 156 nodeBuilder.setGeneratesProbabilities(generatesProbabilities); 157 nodeBuilder.setImpurity(impurity); 158 159 160 TreeNodeProto.Builder builder = TreeNodeProto.newBuilder(); 161 builder.setVersion(CURRENT_VERSION); 162 builder.setClassName(LeafNode.class.getName()); 163 builder.setSerializedData(Any.pack(nodeBuilder.build())); 164 165 return builder.build(); 166 } 167 168 static final class LeafNodeBuilder<T extends Output<T>> extends TreeModel.NodeBuilder implements Node<T> { 169 private final int parentIdx; 170 private final int curIdx; 171 private final double impurity; 172 private final T output; 173 private final Map<String,T> scores; 174 private final boolean generatesProbabilities; 175 176 @SuppressWarnings("unchecked") 177 LeafNodeBuilder(LeafNodeProto proto) { 178 this.parentIdx = proto.getParentIdx(); 179 this.curIdx = proto.getCurIdx(); 180 this.impurity = proto.getImpurity(); 181 this.output = (T) Output.deserialize(proto.getOutput()); 182 this.scores = new HashMap<>(); 183 for (Map.Entry<String, OutputProto> e : proto.getScoreMap().entrySet()) { 184 Output<?> curOutput = Output.deserialize(e.getValue()); 185 if (!curOutput.getClass().equals(output.getClass())) { 186 throw new IllegalStateException("Invalid protobuf, scores were not the same type as the most likely output, found " + curOutput.getClass() + ", expected " + output.getClass()); 187 } 188 this.scores.put(e.getKey(), (T) curOutput); 189 } 190 this.generatesProbabilities = proto.getGeneratesProbabilities(); 191 } 192 193 LeafNodeBuilder(int parentIdx, int curIdx, double impurity, T output, Map<String, T> scores, boolean generatesProbabilities) { 194 this.parentIdx = parentIdx; 195 this.curIdx = curIdx; 196 this.impurity = impurity; 197 this.output = output; 198 this.scores = scores; 199 this.generatesProbabilities = generatesProbabilities; 200 } 201 202 @Override 203 public boolean isLeaf() { 204 return true; 205 } 206 207 @Override 208 public Node<T> getNextNode(SparseVector example) { 209 return null; 210 } 211 212 @Override 213 public double getImpurity() { 214 return impurity; 215 } 216 217 @Override 218 public LeafNodeBuilder<T> copy() { 219 return new LeafNodeBuilder<>(parentIdx,curIdx,impurity,output.copy(),new HashMap<>(scores),generatesProbabilities); 220 } 221 222 /** 223 * Gets the index of the parent node. 224 * @return The parent index. 225 */ 226 int getParentIdx() { 227 return parentIdx; 228 } 229 230 /** 231 * Gets the index of this node. 232 * @return The current node index. 233 */ 234 int getCurIdx() { 235 return curIdx; 236 } 237 238 /** 239 * Builds this builder into a leaf node. 240 * @return The leaf node. 241 */ 242 LeafNode<T> build() { 243 return new LeafNode<>(impurity,output.copy(),new HashMap<>(scores),generatesProbabilities); 244 } 245 } 246}