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.oracle.labs.mlrg.olcut.config.Config;
020import com.oracle.labs.mlrg.olcut.provenance.Provenance;
021import org.tribuo.Dataset;
022import org.tribuo.Example;
023import org.tribuo.ImmutableFeatureMap;
024import org.tribuo.ImmutableOutputInfo;
025import org.tribuo.Output;
026import org.tribuo.Trainer;
027import org.tribuo.provenance.ModelProvenance;
028import org.tribuo.provenance.SkeletalTrainerProvenance;
029import org.tribuo.provenance.TrainerProvenance;
030import org.tribuo.util.Util;
031
032import java.time.OffsetDateTime;
033import java.util.ArrayDeque;
034import java.util.Collections;
035import java.util.Deque;
036import java.util.List;
037import java.util.Map;
038import java.util.SplittableRandom;
039
040/**
041 * Base class for {@link org.tribuo.Trainer}'s that use an approximation of the CART algorithm to build a decision tree.
042 * <p>
043 * See:
044 * <pre>
045 * J. Friedman, T. Hastie, &amp; R. Tibshirani.
046 * "The Elements of Statistical Learning"
047 * Springer 2001. <a href="http://web.stanford.edu/~hastie/ElemStatLearn/">PDF</a>
048 * </pre>
049 */
050public abstract class AbstractCARTTrainer<T extends Output<T>> implements DecisionTreeTrainer<T> {
051
052    /**
053     * Default minimum weight of examples allowed in a leaf node.
054     */
055    public static final int MIN_EXAMPLES = 5;
056
057    /**
058     * Minimum weight of examples allowed in a leaf.
059     */
060    @Config(description="The minimum weight allowed in a child node.")
061    protected float minChildWeight = MIN_EXAMPLES;
062
063    /**
064     * Maximum tree depth. Integer.MAX_VALUE indicates the depth is unlimited.
065     */
066    @Config(description="The maximum depth of the tree.")
067    protected int maxDepth = Integer.MAX_VALUE;
068
069    /**
070     * Minimum impurity decrease. The decrease in impurity needed in order to split the node.
071     */
072    @Config(description="The decrease in impurity needed in order to split the node.")
073    protected float minImpurityDecrease = 0.0f;
074
075    /**
076     * Number of features to sample per split. 1 indicates all features are considered.
077     */
078    @Config(description="The fraction of features to consider in each split. 1.0f indicates all features are considered.")
079    protected float fractionFeaturesInSplit = 1.0f;
080
081    /**
082     * Whether to choose split points for features at random.
083     */
084    @Config(description="Whether to choose split points for features at random.")
085    protected boolean useRandomSplitPoints = false;
086
087    @Config(description="The RNG seed to use when sampling features in a split.")
088    protected long seed = Trainer.DEFAULT_SEED;
089
090    protected SplittableRandom rng;
091
092    protected int trainInvocationCounter;
093
094    /**
095     * After calls to this superconstructor subclasses must call postConfig().
096     * @param maxDepth The maximum depth of the tree.
097     * @param minChildWeight The minimum child weight allowed.
098     * @param minImpurityDecrease The minimum decrease in impurity necessary to split a node.
099     * @param fractionFeaturesInSplit The fraction of features to consider at each split.
100     * @param useRandomSplitPoints Whether to choose split points for features at random.
101     * @param seed The seed for the feature subsampling RNG.
102     */
103    protected AbstractCARTTrainer(int maxDepth, float minChildWeight, float minImpurityDecrease,
104                                  float fractionFeaturesInSplit, boolean useRandomSplitPoints, long seed) {
105        this.maxDepth = maxDepth;
106        this.fractionFeaturesInSplit = fractionFeaturesInSplit;
107        this.useRandomSplitPoints = useRandomSplitPoints;
108        this.minChildWeight = minChildWeight;
109        this.minImpurityDecrease = minImpurityDecrease;
110        this.seed = seed;
111    }
112
113    /**
114     * Used by the OLCUT configuration system, and should not be called by external code.
115     */
116    @Override
117    public synchronized void postConfig() {
118        this.rng = new SplittableRandom(seed);
119
120        if ((fractionFeaturesInSplit <= 0.0f) || (this.fractionFeaturesInSplit > 1.0f)) {
121            throw new IllegalArgumentException("fractionFeaturesInSplit must be greater than 0 and less than or equal" +
122                    " to 1");
123        }
124
125        if (minImpurityDecrease < 0.0f) {
126            throw new IllegalArgumentException("minImpurityDecrease must be greater than or equal to 0");
127        }
128
129        if (maxDepth < 0) {
130            throw new IllegalArgumentException("maxDepth must be non-negative");
131        }
132
133        if (minChildWeight <= 0.0f) {
134            throw new IllegalArgumentException("minChildWeight must be greater than 0");
135        }
136    }
137
138    @Override
139    public int getInvocationCount() {
140        return trainInvocationCounter;
141    }
142
143    @Override
144    public synchronized void setInvocationCount(int invocationCount){
145        if(invocationCount < 0){
146            throw new IllegalArgumentException("The supplied invocationCount is less than zero.");
147        }
148
149        rng = new SplittableRandom(seed);
150
151        for (trainInvocationCounter = 0; trainInvocationCounter < invocationCount; trainInvocationCounter++){
152            SplittableRandom localRNG = rng.split();
153        }
154
155    }
156
157    @Override
158    public float getFractionFeaturesInSplit() {
159        return fractionFeaturesInSplit;
160    }
161
162    @Override
163    public boolean getUseRandomSplitPoints() {
164        return useRandomSplitPoints;
165    }
166
167    @Override
168    public float getMinImpurityDecrease() {
169        return minImpurityDecrease;
170    }
171
172    @Override
173    public TreeModel<T> train(Dataset<T> examples) {
174        return train(examples, Collections.emptyMap());
175    }
176
177    @Override
178    public TreeModel<T> train(Dataset<T> examples, Map<String, Provenance> runProvenance) {
179        return train(examples, runProvenance, INCREMENT_INVOCATION_COUNT);
180    }
181
182    @Override
183    public TreeModel<T> train(Dataset<T> examples, Map<String, Provenance> runProvenance, int invocationCount) {
184        if (examples.getOutputInfo().getUnknownCount() > 0) {
185            throw new IllegalArgumentException("The supplied Dataset contained unknown Outputs, and this Trainer is supervised.");
186        }
187        // Creates a new RNG, adds one to the invocation count.
188        SplittableRandom localRNG;
189        TrainerProvenance trainerProvenance;
190        synchronized(this) {
191            if(invocationCount != INCREMENT_INVOCATION_COUNT) {
192                setInvocationCount(invocationCount);
193            }
194            localRNG = rng.split();
195            trainerProvenance = getProvenance();
196            trainInvocationCounter++;
197        }
198
199        ImmutableFeatureMap featureIDMap = examples.getFeatureIDMap();
200        ImmutableOutputInfo<T> outputIDInfo = examples.getOutputIDInfo();
201
202        int numFeaturesInSplit = Math.min(Math.round(fractionFeaturesInSplit * featureIDMap.size()),featureIDMap.size());
203        int[] indices;
204        int[] originalIndices = new int[featureIDMap.size()];
205        for (int i = 0; i < originalIndices.length; i++) {
206            originalIndices[i] = i;
207        }
208        if (numFeaturesInSplit != featureIDMap.size()) {
209            indices = new int[numFeaturesInSplit];
210        } else {
211            indices = originalIndices;
212        }
213
214        float weightSum = 0.0f;
215        for (Example<T> e : examples) {
216            weightSum += e.getWeight();
217        }
218        float scaledMinImpurityDecrease = getMinImpurityDecrease() * weightSum;
219        AbstractTrainingNode.LeafDeterminer leafDeterminer = new AbstractTrainingNode.LeafDeterminer(maxDepth,
220                minChildWeight, scaledMinImpurityDecrease);
221
222        AbstractTrainingNode<T> root = mkTrainingNode(examples, leafDeterminer);
223        Deque<AbstractTrainingNode<T>> queue = new ArrayDeque<>();
224        queue.add(root);
225
226        while (!queue.isEmpty()) {
227            AbstractTrainingNode<T> node = queue.poll();
228            if ((node.getImpurity() > 0.0) && (node.getDepth() < maxDepth) &&
229                    (node.getWeightSum() >= minChildWeight)) {
230                if (numFeaturesInSplit != featureIDMap.size()) {
231                    Util.randpermInPlace(originalIndices, localRNG);
232                    System.arraycopy(originalIndices, 0, indices, 0, numFeaturesInSplit);
233                }
234                List<AbstractTrainingNode<T>> nodes = node.buildTree(indices, localRNG, getUseRandomSplitPoints());
235                // Use the queue as a stack to improve cache locality.
236                // Building depth first.
237                for (AbstractTrainingNode<T> newNode : nodes) {
238                    queue.addFirst(newNode);
239                }
240            }
241        }
242
243        ModelProvenance provenance = new ModelProvenance(TreeModel.class.getName(), OffsetDateTime.now(), examples.getProvenance(), trainerProvenance, runProvenance);
244        return new TreeModel<>("cart-tree", provenance, featureIDMap, outputIDInfo, false, root.convertTree());
245    }
246
247    /**
248     * Makes the initial training node.
249     * @param examples The dataset to use.
250     * @param leafDeterminer The leaf determination function.
251     * @return The initial training node.
252     */
253    protected abstract AbstractTrainingNode<T> mkTrainingNode(Dataset<T> examples,
254                                                              AbstractTrainingNode.LeafDeterminer leafDeterminer);
255
256    /**
257     * Provenance for {@link AbstractCARTTrainer}. No longer used.
258     */
259    @Deprecated
260    protected static abstract class AbstractCARTTrainerProvenance extends SkeletalTrainerProvenance {
261        private static final long serialVersionUID = 1L;
262
263        /**
264         * Constructs a provenance for the host AbstractCARTTrainer.
265         * @param host The host trainer.
266         * @param <T> The trainer type.
267         */
268        protected <T extends Output<T>> AbstractCARTTrainerProvenance(AbstractCARTTrainer<T> host) {
269            super(host);
270        }
271
272        /**
273         * Deserialization constructor.
274         * @param map The provenance map.
275         */
276        protected AbstractCARTTrainerProvenance(Map<String,Provenance> map) {
277            super(map);
278        }
279    }
280}