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}