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}