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.classification.dtree.impl; 018 019import com.oracle.labs.mlrg.olcut.util.Pair; 020import org.tribuo.Dataset; 021import org.tribuo.Example; 022import org.tribuo.ImmutableFeatureMap; 023import org.tribuo.ImmutableOutputInfo; 024import org.tribuo.classification.Label; 025import org.tribuo.classification.dtree.impurity.LabelImpurity; 026import org.tribuo.common.tree.AbstractTrainingNode; 027import org.tribuo.common.tree.LeafNode; 028import org.tribuo.common.tree.Node; 029import org.tribuo.common.tree.SplitNode; 030import org.tribuo.common.tree.impl.IntArrayContainer; 031import org.tribuo.math.la.SparseVector; 032import org.tribuo.math.la.VectorTuple; 033import org.tribuo.util.Util; 034 035import java.io.IOException; 036import java.io.NotSerializableException; 037import java.util.ArrayList; 038import java.util.Arrays; 039import java.util.Collections; 040import java.util.LinkedHashMap; 041import java.util.List; 042import java.util.Map; 043import java.util.SplittableRandom; 044import java.util.logging.Logger; 045 046/** 047 * A decision tree node used at training time. 048 * Contains a list of the example indices currently found in this node, 049 * the current impurity and a bunch of other statistics. 050 */ 051public class ClassifierTrainingNode extends AbstractTrainingNode<Label> { 052 private static final long serialVersionUID = 1L; 053 054 private static final Logger logger = Logger.getLogger(ClassifierTrainingNode.class.getName()); 055 056 private static final ThreadLocal<IntArrayContainer> mergeBufferOne = ThreadLocal.withInitial(() -> new IntArrayContainer(DEFAULT_SIZE)); 057 private static final ThreadLocal<IntArrayContainer> mergeBufferTwo = ThreadLocal.withInitial(() -> new IntArrayContainer(DEFAULT_SIZE)); 058 private static final ThreadLocal<IntArrayContainer> mergeBufferThree = ThreadLocal.withInitial(() -> new IntArrayContainer(DEFAULT_SIZE)); 059 060 private transient ArrayList<TreeFeature> data; 061 062 private final ImmutableOutputInfo<Label> labelIDMap; 063 064 private final ImmutableFeatureMap featureIDMap; 065 066 private final LabelImpurity impurity; 067 068 private final float[] weightedLabelCounts; 069 070 private final float weightSum; 071 072 /** 073 * Constructor which creates the inverted file. 074 * @param impurity The impurity function to use. 075 * @param examples The training data. 076 * @param leafDeterminer Contains parameters needed to determine whether a child node will be a leaf. 077 */ 078 public ClassifierTrainingNode(LabelImpurity impurity, Dataset<Label> examples, LeafDeterminer leafDeterminer) { 079 this(impurity,invertData(examples), examples.size(), 0, examples.getFeatureIDMap(), 080 examples.getOutputIDInfo(),leafDeterminer); 081 } 082 083 private ClassifierTrainingNode(LabelImpurity impurity, ArrayList<TreeFeature> data, int numExamples, int depth, 084 ImmutableFeatureMap featureIDMap, ImmutableOutputInfo<Label> labelIDMap, 085 LeafDeterminer leafDeterminer) { 086 super(depth, numExamples, leafDeterminer); 087 this.data = data; 088 this.featureIDMap = featureIDMap; 089 this.labelIDMap = labelIDMap; 090 this.impurity = impurity; 091 this.weightedLabelCounts = data.get(0).getWeightedLabelCounts(); 092 this.weightSum = Util.sum(weightedLabelCounts); 093 this.impurityScore = impurity.impurity(weightedLabelCounts); 094 } 095 096 private ClassifierTrainingNode(LabelImpurity impurity, ArrayList<TreeFeature> data, int numExamples, int depth, 097 ImmutableFeatureMap featureIDMap, ImmutableOutputInfo<Label> labelIDMap, 098 LeafDeterminer leafDeterminer, float[] weightedLabelCounts, float weightSum, 099 double impurityScore) { 100 super(depth, numExamples, leafDeterminer); 101 this.data = data; 102 this.featureIDMap = featureIDMap; 103 this.labelIDMap = labelIDMap; 104 this.impurity = impurity; 105 this.weightedLabelCounts = weightedLabelCounts; 106 this.weightSum = weightSum; 107 this.impurityScore = impurityScore; 108 } 109 110 @Override 111 public float getWeightSum() { 112 return weightSum; 113 } 114 115 @Override 116 public double getImpurity() { 117 return impurityScore; 118 } 119 120 /** 121 * Builds a tree according to CART (as it does not do multi-way splits on categorical values like C4.5). 122 * @param featureIDs Indices of the features available in this split. 123 * @param rng Splittable random number generator. 124 * @param useRandomSplitPoints Whether to choose split points for features at random. 125 * @return A possibly empty list of TrainingNodes. 126 */ 127 @Override 128 public List<AbstractTrainingNode<Label>> buildTree(int[] featureIDs, SplittableRandom rng, 129 boolean useRandomSplitPoints) { 130 if (useRandomSplitPoints) { 131 return buildRandomTree(featureIDs, rng); 132 } else { 133 return buildGreedyTree(featureIDs); 134 } 135 } 136 137 /** 138 * Builds a tree according to CART 139 * @param featureIDs Indices of the features available in this split. 140 * @return A possibly empty list of TrainingNodes. 141 */ 142 private List<AbstractTrainingNode<Label>> buildGreedyTree(int[] featureIDs) { 143 int bestID = -1; 144 double bestSplitValue = 0.0; 145 double bestScore = getImpurity(); 146 float[] lessThanCountsOfBest = new float[weightedLabelCounts.length]; 147 float[] greaterThanCountsOfBest = new float[weightedLabelCounts.length]; 148 float[] lessThanCounts = new float[weightedLabelCounts.length]; 149 float[] greaterThanCounts = new float[weightedLabelCounts.length]; 150 for (int i = 0; i < featureIDs.length; i++) { 151 List<InvertedFeature> feature = data.get(featureIDs[i]).getFeature(); 152 Arrays.fill(lessThanCounts,0.0f); 153 System.arraycopy(weightedLabelCounts, 0, greaterThanCounts, 0, weightedLabelCounts.length); 154 // searching for the intervals between features. 155 for (int j = 0; j < feature.size()-1; j++) { 156 InvertedFeature f = feature.get(j); 157 float[] featureCounts = f.getWeightedLabelCounts(); 158 Util.inPlaceAdd(lessThanCounts,featureCounts); 159 Util.inPlaceSubtract(greaterThanCounts,featureCounts); 160 // impurityWeighted rescales the impurity by the sum, in order to average properly when you add the 161 // left side & right side together, and then divide by the sum of the counts 162 double lessThanScore = impurity.impurityWeighted(lessThanCounts); 163 double greaterThanScore = impurity.impurityWeighted(greaterThanCounts); 164 double score = (lessThanScore + greaterThanScore) / weightSum; 165 if (score < bestScore) { 166 bestID = i; 167 bestScore = score; 168 System.arraycopy(lessThanCounts,0,lessThanCountsOfBest,0,lessThanCounts.length); 169 System.arraycopy(greaterThanCounts,0,greaterThanCountsOfBest,0,greaterThanCounts.length); 170 bestSplitValue = (f.value + feature.get(j + 1).value) / 2.0; 171 } 172 } 173 } 174 175 List<AbstractTrainingNode<Label>> output; 176 double impurityDecrease = weightSum * (getImpurity() - bestScore); 177 // If we found a split better than the current impurity. 178 if ((bestID != -1) && (impurityDecrease >= leafDeterminer.getScaledMinImpurityDecrease())) { 179 output = splitAtBest(featureIDs, bestID, bestSplitValue, lessThanCountsOfBest, greaterThanCountsOfBest); 180 } else { 181 output = Collections.emptyList(); 182 } 183 data = null; 184 return output; 185 } 186 187 /** 188 * Builds a CART tree with randomly chosen split points. 189 * @param featureIDs Indices of the features available in this split. 190 * @param rng Splittable random number generator. 191 * @return A possibly empty list of TrainingNodes. 192 */ 193 public List<AbstractTrainingNode<Label>> buildRandomTree(int[] featureIDs, SplittableRandom rng) { 194 int bestID = -1; 195 double bestSplitValue = 0.0; 196 double bestScore = getImpurity(); 197 float[] lessThanCountsOfBest = new float[weightedLabelCounts.length]; 198 float[] greaterThanCountsOfBest = new float[weightedLabelCounts.length]; 199 float[] lessThanCounts = new float[weightedLabelCounts.length]; 200 float[] greaterThanCounts = new float[weightedLabelCounts.length]; 201 202 // split each feature once randomly and record the least impure amongst these 203 for (int i = 0; i < featureIDs.length; i++) { 204 List<InvertedFeature> feature = data.get(featureIDs[i]).getFeature(); 205 206 // if there is only 1 inverted feature for this feature, it has only 1 value, so cannot be split 207 if (feature.size() == 1) { 208 continue; 209 } 210 211 Arrays.fill(lessThanCounts,0.0f); 212 System.arraycopy(weightedLabelCounts, 0, greaterThanCounts, 0, weightedLabelCounts.length); 213 214 int splitIdx = rng.nextInt(feature.size()-1); 215 for (int j = 0; j < splitIdx + 1; j++) { 216 InvertedFeature vf = feature.get(j); 217 float[] countsBelowOrEqual = vf.getWeightedLabelCounts(); 218 Util.inPlaceAdd(lessThanCounts, countsBelowOrEqual); 219 Util.inPlaceSubtract(greaterThanCounts, countsBelowOrEqual); 220 } 221 double lessThanScore = impurity.impurityWeighted(lessThanCounts); 222 double greaterThanScore = impurity.impurityWeighted(greaterThanCounts); 223 double score = (lessThanScore + greaterThanScore) / weightSum; 224 if (score < bestScore) { 225 bestID = i; 226 bestScore = score; 227 System.arraycopy(lessThanCounts,0,lessThanCountsOfBest,0,lessThanCounts.length); 228 System.arraycopy(greaterThanCounts,0,greaterThanCountsOfBest,0,greaterThanCounts.length); 229 bestSplitValue = (feature.get(splitIdx).value + feature.get(splitIdx + 1).value) / 2.0; 230 } 231 } 232 233 List<AbstractTrainingNode<Label>> output; 234 double impurityDecrease = weightSum * (getImpurity() - bestScore); 235 // If we found a split better than the current impurity. 236 if ((bestID != -1) && (impurityDecrease >= leafDeterminer.getScaledMinImpurityDecrease())) { 237 output = splitAtBest(featureIDs, bestID, bestSplitValue, lessThanCountsOfBest, greaterThanCountsOfBest); 238 } else { 239 output = Collections.emptyList(); 240 } 241 data = null; 242 return output; 243 } 244 245 /** 246 * Splits the data to form two nodes. 247 * @param featureIDs Indices of the features available in this split. 248 * @param bestID ID of the feature on which the split should be based. 249 * @param bestSplitValue Feature value to use for splitting the data. 250 * @param lessThanCounts Weighted label counts for data less than or equal to the split value for the given feature. 251 * @param greaterThanCounts Weighted label counts for data greater than the split value for the given feature. 252 * @return A list of training nodes resulting from the split. 253 */ 254 private List<AbstractTrainingNode<Label>> splitAtBest(int[] featureIDs, int bestID, double bestSplitValue, 255 float[] lessThanCounts, float[] greaterThanCounts) { 256 splitID = featureIDs[bestID]; 257 split = true; 258 splitValue = bestSplitValue; 259 260 float lessThanWeightSum = Util.sum(lessThanCounts); 261 double lessThanImpurityScore = impurity.impurity(lessThanCounts); 262 263 float greaterThanWeightSum = Util.sum(greaterThanCounts); 264 double greaterThanImpurityScore = impurity.impurity(greaterThanCounts); 265 266 boolean shouldMakeLessThanLeaf = shouldMakeLeaf(lessThanImpurityScore, lessThanWeightSum); 267 boolean shouldMakeGreaterThanLeaf = shouldMakeLeaf(greaterThanImpurityScore, greaterThanWeightSum); 268 269 if (shouldMakeLessThanLeaf && shouldMakeGreaterThanLeaf) { 270 lessThanOrEqual = createLeaf(lessThanImpurityScore, lessThanCounts); 271 greaterThan = createLeaf(greaterThanImpurityScore, greaterThanCounts); 272 return Collections.emptyList(); 273 } 274 275 IntArrayContainer lessThanIndices = mergeBufferOne.get(); 276 lessThanIndices.size = 0; 277 IntArrayContainer buffer = mergeBufferTwo.get(); 278 buffer.size = 0; 279 for (InvertedFeature f : data.get(splitID)) { 280 if (f.value < splitValue) { 281 int[] indices = f.indices(); 282 IntArrayContainer.merge(lessThanIndices,indices,buffer); 283 // Swap the buffers 284 IntArrayContainer tmp = lessThanIndices; 285 lessThanIndices = buffer; 286 buffer = tmp; 287 } else { 288 break; 289 } 290 } 291 //logger.info("Splitting on feature " + maxID + " with value " + maxSplitValue + " at depth " + depth + ", " + numExamples + " examples in node."); 292 //logger.info("left indices length = " + lessThanIndices.size); 293 IntArrayContainer secondBuffer = mergeBufferThree.get(); 294 secondBuffer.grow(lessThanIndices.size); 295 ArrayList<TreeFeature> lessThanData = new ArrayList<>(data.size()); 296 ArrayList<TreeFeature> greaterThanData = new ArrayList<>(data.size()); 297 for (TreeFeature feature : data) { 298 Pair<TreeFeature,TreeFeature> split = feature.split(lessThanIndices,buffer,secondBuffer); 299 lessThanData.add(split.getA()); 300 greaterThanData.add(split.getB()); 301 } 302 303 List<AbstractTrainingNode<Label>> output = new ArrayList<>(2); 304 if (shouldMakeLessThanLeaf) { 305 lessThanOrEqual = createLeaf(lessThanImpurityScore, lessThanCounts); 306 } else { 307 AbstractTrainingNode<Label> tmpNode = new ClassifierTrainingNode(impurity, lessThanData, lessThanIndices.size, depth + 1, 308 featureIDMap, labelIDMap, leafDeterminer, lessThanCounts, lessThanWeightSum, lessThanImpurityScore); 309 lessThanOrEqual = tmpNode; 310 output.add(tmpNode); 311 } 312 313 if (shouldMakeGreaterThanLeaf) { 314 greaterThan = createLeaf(greaterThanImpurityScore, greaterThanCounts); 315 } else { 316 AbstractTrainingNode<Label> tmpNode = new ClassifierTrainingNode(impurity, greaterThanData, numExamples - lessThanIndices.size, 317 depth + 1, featureIDMap, labelIDMap, leafDeterminer, greaterThanCounts, greaterThanWeightSum, greaterThanImpurityScore); 318 greaterThan = tmpNode; 319 output.add(tmpNode); 320 } 321 return output; 322 } 323 324 /** 325 * Makes a {@link LeafNode} 326 * @param impurityScore the impurity score for the node. 327 * @param weightedCounts the weighted label counts of the data in the node. 328 * @return a {@link LeafNode} 329 */ 330 private LeafNode<Label> createLeaf(double impurityScore, float[] weightedCounts) { 331 double[] normedCounts = Util.normalizeToDistribution(weightedCounts); 332 double maxScore = Double.NEGATIVE_INFINITY; 333 Label maxLabel = null; 334 Map<String,Label> counts = new LinkedHashMap<>(); 335 for (int i = 0; i < weightedCounts.length; i++) { 336 final double curCount = normedCounts[i]; 337 String name = labelIDMap.getOutput(i).getLabel(); 338 Label label = new Label(name,curCount); 339 counts.put(name, label); 340 if (curCount > maxScore) { 341 maxScore = curCount; 342 maxLabel = label; 343 } 344 } 345 return new LeafNode<>(impurityScore,maxLabel,counts,true); 346 } 347 348 /** 349 * Generates a test time tree (made of {@link SplitNode} and {@link LeafNode}) from the tree rooted at this node. 350 * @return A subtree using the SplitNode and LeafNode classes. 351 */ 352 @Override 353 public Node<Label> convertTree() { 354 if (split) { 355 return createSplitNode(); 356 } else { 357 return createLeaf(getImpurity(), weightedLabelCounts); 358 } 359 } 360 361 /** 362 * Inverts a training dataset from row major to column major. This partially de-sparsifies the dataset 363 * so it's very expensive in terms of memory. 364 * @param examples An input dataset. 365 * @return A list of TreeFeatures which contain {@link InvertedFeature}s. 366 */ 367 private static ArrayList<TreeFeature> invertData(Dataset<Label> examples) { 368 ImmutableFeatureMap featureInfos = examples.getFeatureIDMap(); 369 ImmutableOutputInfo<Label> labelInfo = examples.getOutputIDInfo(); 370 int numLabels = labelInfo.size(); 371 int numFeatures = featureInfos.size(); 372 int numExamples = examples.size(); 373 374 int[] labels = new int[numExamples]; 375 float[] weights = new float[numExamples]; 376 377 int k = 0; 378 for (Example<Label> e : examples) { 379 weights[k] = e.getWeight(); 380 labels[k] = labelInfo.getID(e.getOutput()); 381 k++; 382 } 383 384 logger.fine("Building initial List<TreeFeature> for " + numFeatures + " features and " + numLabels + " classes"); 385 ArrayList<TreeFeature> data = new ArrayList<>(featureInfos.size()); 386 387 for (int i = 0; i < featureInfos.size(); i++) { 388 data.add(new TreeFeature(i,numLabels,labels,weights)); 389 } 390 391 for (int i = 0; i < examples.size(); i++) { 392 Example<Label> e = examples.getExample(i); 393 SparseVector vec = SparseVector.createSparseVector(e,featureInfos,false); 394 int lastID = 0; 395 for (VectorTuple f : vec) { 396 int curID = f.index; 397 for (int j = lastID; j < curID; j++) { 398 data.get(j).observeValue(0.0,i); 399 } 400 data.get(curID).observeValue(f.value,i); 401 // 402 // These two checks should never occur as SparseVector deals with collisions, and Dataset prevents 403 // repeated features. 404 // They are left in just to make sure. 405 if (lastID > curID) { 406 logger.severe("Example = " + e); 407 throw new IllegalStateException("Features aren't ordered. At id " + i + ", lastID = " + lastID + ", curID = " + curID); 408 } else if (lastID-1 == curID) { 409 logger.severe("Example = " + e); 410 throw new IllegalStateException("Features are repeated. At id " + i + ", lastID = " + lastID + ", curID = " + curID); 411 } 412 lastID = curID + 1; 413 } 414 for (int j = lastID; j < numFeatures; j++) { 415 data.get(j).observeValue(0.0,i); 416 } 417 if (i % 1000 == 0) { 418 logger.fine("Processed example " + i); 419 } 420 } 421 422 logger.fine("Sorting features"); 423 424 data.forEach(TreeFeature::sort); 425 426 logger.fine("Fixing InvertedFeature sizes"); 427 428 data.forEach(TreeFeature::fixSize); 429 430 logger.fine("Built initial List<TreeFeature>"); 431 432 return data; 433 } 434 435 private void writeObject(java.io.ObjectOutputStream stream) 436 throws IOException { 437 throw new NotSerializableException("ClassifierTrainingNode is a runtime class only, and should not be serialized."); 438 } 439}