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.liblinear; 018 019import com.oracle.labs.mlrg.olcut.config.Config; 020import com.oracle.labs.mlrg.olcut.provenance.Provenance; 021import com.oracle.labs.mlrg.olcut.util.Pair; 022import org.tribuo.Dataset; 023import org.tribuo.Example; 024import org.tribuo.Feature; 025import org.tribuo.ImmutableFeatureMap; 026import org.tribuo.ImmutableOutputInfo; 027import org.tribuo.Output; 028import org.tribuo.Trainer; 029import org.tribuo.provenance.ModelProvenance; 030import org.tribuo.provenance.TrainerProvenance; 031import org.tribuo.provenance.impl.TrainerProvenanceImpl; 032import org.tribuo.util.Util; 033import de.bwaldvogel.liblinear.FeatureNode; 034import de.bwaldvogel.liblinear.Linear; 035import de.bwaldvogel.liblinear.Parameter; 036 037import java.time.OffsetDateTime; 038import java.util.ArrayList; 039import java.util.Collections; 040import java.util.List; 041import java.util.Map; 042import java.util.Random; 043import java.util.SplittableRandom; 044import java.util.logging.Logger; 045 046/** 047 * A {@link Trainer} which wraps a liblinear-java trainer. 048 * <p> 049 * See: 050 * <pre> 051 * Fan RE, Chang KW, Hsieh CJ, Wang XR, Lin CJ. 052 * "LIBLINEAR: A library for Large Linear Classification" 053 * Journal of Machine Learning Research, 2008. 054 * </pre> 055 * and for the original algorithm: 056 * <pre> 057 * Cortes C, Vapnik V. 058 * "Support-Vector Networks" 059 * Machine Learning, 1995. 060 * </pre> 061 */ 062public abstract class LibLinearTrainer<T extends Output<T>> implements Trainer<T> { 063 064 private static final Logger logger = Logger.getLogger(LibLinearTrainer.class.getName()); 065 066 protected Parameter libLinearParams; 067 068 @Config(description="Algorithm to use.") 069 protected LibLinearType<T> trainerType; 070 071 @Config(description="Cost penalty for misclassifications.") 072 protected double cost = 1; 073 074 @Config(description="Maximum number of iterations before terminating.") 075 protected int maxIterations = 1000; 076 077 @Config(description="Stop iterating when the loss score decreases by less than this value.") 078 protected double terminationCriterion = 0.1; 079 080 @Config(description="Epsilon insensitivity in the regression cost function.") 081 protected double epsilon = 0.1; 082 083 @Config(description="RNG seed.") 084 protected long seed = Trainer.DEFAULT_SEED; 085 086 private SplittableRandom rng; 087 088 private int trainInvocationCount = 0; 089 090 /** 091 * For OLCUT 092 */ 093 protected LibLinearTrainer() {} 094 095 /** 096 * Creates a trainer for a LibLinear model 097 * <p> 098 * Uses {@link Trainer#DEFAULT_SEED} as the RNG seed, and 0.1 as epsilon. 099 * @param trainerType Loss function and optimisation method combination. 100 * @param cost Cost penalty for each incorrectly classified training point. 101 * @param maxIterations The maximum number of dataset iterations. 102 * @param terminationCriterion How close does the optimisation function need to be before terminating that subproblem (usually set to 0.1). 103 */ 104 protected LibLinearTrainer(LibLinearType<T> trainerType, double cost, int maxIterations, double terminationCriterion) { 105 this(trainerType,cost,maxIterations,terminationCriterion,0.1); 106 } 107 108 /** 109 * Creates a trainer for a LibLinear model 110 * @param trainerType Loss function and optimisation method combination. 111 * @param cost Cost penalty for each incorrectly classified training point. 112 * @param maxIterations The maximum number of dataset iterations. 113 * @param terminationCriterion How close does the optimisation function need to be before terminating that subproblem (usually set to 0.1). 114 */ 115 protected LibLinearTrainer(LibLinearType<T> trainerType, double cost, int maxIterations, double terminationCriterion, long seed) { 116 this(trainerType,cost,maxIterations,terminationCriterion,0.1, seed); 117 } 118 119 /** 120 * Creates a trainer for a LibLinear model 121 * <p> 122 * Uses {@link Trainer#DEFAULT_SEED} as the RNG seed. 123 * @param trainerType Loss function and optimisation method combination. 124 * @param cost Cost penalty for each incorrectly classified training point. 125 * @param maxIterations The maximum number of dataset iterations. 126 * @param terminationCriterion How close does the optimisation function need to be before terminating that subproblem (usually set to 0.1). 127 * @param epsilon The insensitivity of the regression loss to small differences. 128 */ 129 protected LibLinearTrainer(LibLinearType<T> trainerType, double cost, int maxIterations, double terminationCriterion, double epsilon) { 130 this(trainerType,cost,maxIterations,terminationCriterion,epsilon,Trainer.DEFAULT_SEED); 131 } 132 133 /** 134 * Creates a trainer for a LibLinear model 135 * @param trainerType Loss function and optimisation method combination. 136 * @param cost Cost penalty for each incorrectly classified training point. 137 * @param maxIterations The maximum number of dataset iterations. 138 * @param terminationCriterion How close does the optimisation function need to be before terminating that subproblem (usually set to 0.1). 139 * @param epsilon The insensitivity of the regression loss to small differences. 140 * @param seed The RNG seed. 141 */ 142 protected LibLinearTrainer(LibLinearType<T> trainerType, double cost, int maxIterations, double terminationCriterion, double epsilon, long seed) { 143 this.trainerType = trainerType; 144 this.cost = cost; 145 this.maxIterations = maxIterations; 146 this.terminationCriterion = terminationCriterion; 147 this.epsilon = epsilon; 148 this.seed = seed; 149 postConfig(); 150 } 151 152 /** 153 * Used by the OLCUT configuration system, and should not be called by external code. 154 */ 155 @Override 156 public void postConfig() { 157 libLinearParams = new Parameter(trainerType.getSolverType(),cost,terminationCriterion,maxIterations,epsilon); 158 rng = new SplittableRandom(seed); 159 Linear.disableDebugOutput(); 160 } 161 162 @Override 163 public LibLinearModel<T> train(Dataset<T> examples) { 164 return train(examples, Collections.emptyMap()); 165 } 166 167 @Override 168 public LibLinearModel<T> train(Dataset<T> examples, Map<String, Provenance> runProvenance) { 169 return(train(examples, runProvenance, INCREMENT_INVOCATION_COUNT)); 170 } 171 172 @Override 173 public LibLinearModel<T> train(Dataset<T> examples, Map<String, Provenance> runProvenance, int invocationCount) { 174 if (examples.getOutputInfo().getUnknownCount() > 0) { 175 throw new IllegalArgumentException("The supplied Dataset contained unknown Outputs, and this Trainer is supervised."); 176 } 177 178 // Creates a new RNG, adds one to the invocation count. 179 TrainerProvenance trainerProvenance; 180 SplittableRandom localRNG; 181 synchronized(this) { 182 if(invocationCount != INCREMENT_INVOCATION_COUNT) { 183 setInvocationCount(invocationCount); 184 } 185 localRNG = rng.split(); 186 trainerProvenance = getProvenance(); 187 trainInvocationCount++; 188 } 189 190 ImmutableFeatureMap featureIDMap = examples.getFeatureIDMap(); 191 ImmutableOutputInfo<T> outputIDInfo = examples.getOutputIDInfo(); 192 193 // Setup parameters and RNG 194 Parameter curParams = setupParameters(outputIDInfo); 195 curParams.setRandom(new Random(localRNG.nextLong())); 196 197 ModelProvenance provenance = new ModelProvenance(LibLinearModel.class.getName(), OffsetDateTime.now(), examples.getProvenance(), trainerProvenance, runProvenance); 198 199 Pair<FeatureNode[][],double[][]> data = extractData(examples,outputIDInfo,featureIDMap); 200 201 List<de.bwaldvogel.liblinear.Model> models = trainModels(curParams, featureIDMap.size() + 1, data.getA(), data.getB()); 202 203 return createModel(provenance,featureIDMap,outputIDInfo,models); 204 } 205 206 @Override 207 public int getInvocationCount() { 208 return trainInvocationCount; 209 } 210 211 @Override 212 public synchronized void setInvocationCount(int invocationCount) { 213 if(invocationCount < 0){ 214 throw new IllegalArgumentException("The supplied invocationCount is less than zero."); 215 } 216 217 rng = new SplittableRandom(seed); 218 219 for (trainInvocationCount = 0; trainInvocationCount < invocationCount; trainInvocationCount++){ 220 SplittableRandom localRNG = rng.split(); 221 } 222 } 223 224 @Override 225 public String toString() { 226 StringBuilder buffer = new StringBuilder(); 227 228 buffer.append("LibLinearTrainer("); 229 buffer.append("solver="); 230 buffer.append(libLinearParams.getSolverType()); 231 buffer.append(",cost="); 232 buffer.append(libLinearParams.getC()); 233 buffer.append(",terminationCriterion="); 234 buffer.append(libLinearParams.getEps()); 235 buffer.append(",maxIterations="); 236 buffer.append(libLinearParams.getMaxIters()); 237 buffer.append(",regression-epsilon="); 238 buffer.append(libLinearParams.getP()); 239 buffer.append(",seed="); 240 buffer.append(seed); 241 buffer.append(')'); 242 243 return buffer.toString(); 244 } 245 246 /** 247 * Train all the liblinear instances necessary for this dataset. 248 * @param curParams The LibLinear parameters. 249 * @param numFeatures The number of features in this dataset. 250 * @param features The features themselves. 251 * @param outputs The outputs. 252 * @return A list of liblinear models. 253 */ 254 protected abstract List<de.bwaldvogel.liblinear.Model> trainModels(Parameter curParams, int numFeatures, FeatureNode[][] features, double[][] outputs); 255 256 /** 257 * Construct the appropriate subtype of LibLinearModel for the prediction task. 258 * @param provenance The model provenance. 259 * @param featureIDMap The feature id map. 260 * @param outputIDInfo The output id info. 261 * @param models The list of linear models. 262 * @return An implementation of LibLinearModel. 263 */ 264 protected abstract LibLinearModel<T> createModel(ModelProvenance provenance, ImmutableFeatureMap featureIDMap, ImmutableOutputInfo<T> outputIDInfo, List<de.bwaldvogel.liblinear.Model> models); 265 266 /** 267 * Extracts the features and {@link Output}s in LibLinear's format. 268 * @param data The input data. 269 * @param outputInfo The output info. 270 * @param featureMap The feature info. 271 * @return The features and outputs. 272 */ 273 protected abstract Pair<FeatureNode[][],double[][]> extractData(Dataset<T> data, ImmutableOutputInfo<T> outputInfo, ImmutableFeatureMap featureMap); 274 275 /** 276 * Constructs the parameters. Most of the time this just clones the existing ones, but 277 * classification overrides it to incorporate label weights if they exist. 278 * @param info The output info. 279 * @return The Parameters to use for training. 280 */ 281 protected Parameter setupParameters(ImmutableOutputInfo<T> info) { 282 return libLinearParams.clone(); 283 } 284 285 /** 286 * Converts a Tribuo {@link Example} into a liblinear {@code FeatureNode} array, including a bias feature. 287 * <p> 288 * If there is a collision between feature ids (i.e., if there is feature hashing or some other mechanism changing 289 * the feature ids) then the feature values are summed. 290 * @param example The input example. 291 * @param featureIDMap The feature id map which contains the example's indices. 292 * @param features A buffer. If null then an array list is created and used internally. 293 * @param <T> The output type. 294 * @return The features suitable for use in liblinear. 295 */ 296 public static <T extends Output<T>> FeatureNode[] exampleToNodes(Example<T> example, ImmutableFeatureMap featureIDMap, List<FeatureNode> features) { 297 int biasIndex = featureIDMap.size()+1; 298 299 if (features == null) { 300 features = new ArrayList<>(); 301 } 302 features.clear(); 303 304 int prevIdx = -1; 305 for (Feature f : example) { 306 int id = featureIDMap.getID(f.getName()); 307 if (id > prevIdx){ 308 prevIdx = id; 309 features.add(new FeatureNode(id + 1, f.getValue())); 310 } else if (id > -1) { 311 // 312 // Collision, deal with it. 313 int collisionIdx = Util.binarySearch(features,id+1, FeatureNode::getIndex); 314 if (collisionIdx < 0) { 315 // 316 // Collision but not present in features 317 // move data and bump i 318 collisionIdx = - (collisionIdx + 1); 319 features.add(collisionIdx,new FeatureNode(id + 1, f.getValue())); 320 } else { 321 // 322 // Collision present in features 323 // add the values. 324 FeatureNode n = features.get(collisionIdx); 325 n.setValue(n.getValue() + f.getValue()); 326 } 327 } 328 } 329 330 features.add(new FeatureNode(biasIndex,1.0)); 331 332 return features.toArray(new FeatureNode[0]); 333 } 334 335 @Override 336 public TrainerProvenance getProvenance() { 337 return new TrainerProvenanceImpl(this); 338 } 339}