001/*
002 * Copyright (c) 2015-2021, 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.libsvm;
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 libsvm.svm_model;
023import libsvm.svm_node;
024import libsvm.svm_parameter;
025import org.tribuo.Dataset;
026import org.tribuo.Example;
027import org.tribuo.Feature;
028import org.tribuo.ImmutableFeatureMap;
029import org.tribuo.ImmutableOutputInfo;
030import org.tribuo.Output;
031import org.tribuo.Trainer;
032import org.tribuo.provenance.ModelProvenance;
033import org.tribuo.provenance.TrainerProvenance;
034import org.tribuo.provenance.impl.TrainerProvenanceImpl;
035import org.tribuo.util.Util;
036
037import java.time.OffsetDateTime;
038import java.util.ArrayList;
039import java.util.Collections;
040import java.util.List;
041import java.util.Map;
042import java.util.SplittableRandom;
043import java.util.logging.Logger;
044
045/**
046 * A trainer that will train using libsvm's Java implementation.
047 * <p>
048 * Note the train method is synchronized on {@code LibSVMTrainer.class} due to a global RNG in LibSVM.
049 * This is insufficient to ensure reproducibility if LibSVM is used directly in the same JVM as Tribuo, but
050 * avoids locking on classes Tribuo does not control.
051 * <p>
052 * See:
053 * <pre>
054 * Chang CC, Lin CJ.
055 * "LIBSVM: a library for Support Vector Machines"
056 * ACM transactions on intelligent systems and technology (TIST), 2011.
057 * </pre>
058 * for the nu-svm algorithm:
059 * <pre>
060 * Schölkopf B, Smola A, Williamson R, Bartlett P L.
061 * "New support vector algorithms"
062 * Neural Computation, 2000, 1207-1245.
063 * </pre>
064 * and for the original algorithm:
065 * <pre>
066 * Cortes C, Vapnik V.
067 * "Support-Vector Networks"
068 * Machine Learning, 1995.
069 * </pre>
070 */
071public abstract class LibSVMTrainer<T extends Output<T>> implements Trainer<T> {
072    
073    private static final Logger logger = Logger.getLogger(LibSVMTrainer.class.getName());
074
075    /**
076     * The SVM parameters suitable for use by LibSVM.
077     */
078    protected svm_parameter parameters;
079
080    /**
081     * The type of SVM algorithm.
082     */
083    @Config(mandatory=true,description="Type of SVM algorithm.")
084    protected SVMType<T> svmType;
085
086    @Config(description="Type of Kernel.")
087    private KernelType kernelType = KernelType.LINEAR;
088
089    @Config(description="Polynomial degree.")
090    private int degree = 3;
091
092    @Config(description="Width of the RBF kernel, or scalar on sigmoid kernel.")
093    private double gamma = 0.0;
094
095    @Config(description="Polynomial coefficient or shift in sigmoid kernel.")
096    private double coef0 = 0.0;
097
098    @Config(description="nu value in NU SVM.")
099    private double nu = 0.5;
100
101    @Config(description="Internal cache size, most of the time should be left at default.")
102    private double cache_size = 500;
103
104    @Config(description="Cost parameter for incorrect predictions.")
105    private double cost = 1.0; // aka svm_parameters.C
106
107    @Config(description="Tolerance of the termination criterion.")
108    private double eps = 1e-3;
109
110    @Config(description="Epsilon in EPSILON_SVR.")
111    private double p = 1e-3;
112
113    @Config(description="Regularise the weight parameters.")
114    private boolean shrinking = true;
115
116    @Config(description="Generate probability estimates.")
117    private boolean probability = false;
118
119    @Config(description="RNG seed.")
120    private long seed = Trainer.DEFAULT_SEED;
121
122    private SplittableRandom rng;
123
124    private int trainInvocationCounter = 0;
125
126    /**
127     * For olcut.
128     */
129    protected LibSVMTrainer() {}
130
131    /**
132     * Constructs a LibSVMTrainer from the parameters.
133     * @param parameters The SVM parameters.
134     * @param seed The RNG seed.
135     */
136    protected LibSVMTrainer(SVMParameters<T> parameters, long seed) {
137        this.parameters = parameters.getParameters();
138        // Unpack the parameters for the provenance system.
139        this.svmType = parameters.getSvmType();
140        this.kernelType = parameters.getKernelType();
141        this.degree = this.parameters.degree;
142        this.gamma = parameters.getGamma();
143        this.coef0 = this.parameters.coef0;
144        this.nu = this.parameters.nu;
145        this.cache_size = this.parameters.cache_size;
146        this.cost = this.parameters.C;
147        this.eps = this.parameters.eps;
148        this.p = this.parameters.p;
149        this.shrinking = this.parameters.shrinking == 1;
150        this.probability = this.parameters.probability == 1;
151        this.seed = seed;
152        this.rng = new SplittableRandom(seed);
153    }
154
155    /**
156     * Used by the OLCUT configuration system, and should not be called by external code.
157     */
158    @Override
159    public void postConfig() {
160        parameters = new svm_parameter();
161        parameters.svm_type = svmType.getNativeType();
162        parameters.kernel_type = kernelType.getNativeType();
163        parameters.degree = degree;
164        parameters.gamma = gamma;
165        parameters.coef0 = coef0;
166        parameters.nu = nu;
167        parameters.cache_size = cache_size;
168        parameters.C = cost;
169        parameters.eps = eps;
170        parameters.p = p;
171        parameters.shrinking = shrinking ? 1 : 0;
172        parameters.probability = probability ? 1 : 0;
173        this.rng = new SplittableRandom(seed);
174    }
175
176    @Override
177    public String toString() {
178        StringBuilder buffer = new StringBuilder();
179
180        buffer.append("LibSVMTrainer(");
181        buffer.append("svm_params=");
182        buffer.append(SVMParameters.svmParamsToString(parameters));
183        buffer.append(",seed=");
184        buffer.append(seed);
185        buffer.append(")");
186
187        return buffer.toString();
188    }
189
190    @Override
191    public LibSVMModel<T> train(Dataset<T> examples) {
192        return train(examples, Collections.emptyMap());
193    }
194
195    @Override
196    public LibSVMModel<T> train(Dataset<T> examples, Map<String, Provenance> runProvenance) {
197        return (train(examples, runProvenance, INCREMENT_INVOCATION_COUNT));
198    }
199
200    @Override
201    public LibSVMModel<T> train(Dataset<T> examples, Map<String, Provenance> runProvenance, int invocationCount) {
202        if (examples.getOutputInfo().getUnknownCount() > 0) {
203            throw new IllegalArgumentException("The supplied Dataset contained unknown Outputs, and this Trainer is supervised.");
204        }
205        ImmutableFeatureMap featureIDMap = examples.getFeatureIDMap();
206        ImmutableOutputInfo<T> outputIDInfo = examples.getOutputIDInfo();
207
208        // Creates a new RNG, adds one to the invocation count, generates a local optimiser.
209        TrainerProvenance trainerProvenance;
210        SplittableRandom localRNG;
211        synchronized(this) {
212            if(invocationCount != INCREMENT_INVOCATION_COUNT) {
213                setInvocationCount(invocationCount);
214            }
215            localRNG = rng.split();
216            trainerProvenance = getProvenance();
217            trainInvocationCounter++;
218        }
219
220        svm_parameter curParams = setupParameters(outputIDInfo);
221
222        Pair<svm_node[][],double[][]> data = extractData(examples,outputIDInfo,featureIDMap);
223
224        List<svm_model> models;
225        synchronized (LibSVMTrainer.class) {
226            // localRNG is used to seed LibSVM's global RNG before each call to svm.svm_train
227            models = trainModels(curParams, featureIDMap.size() + 1, data.getA(), data.getB(), localRNG);
228        }
229
230        ModelProvenance provenance = new ModelProvenance(LibSVMModel.class.getName(), OffsetDateTime.now(), examples.getProvenance(), trainerProvenance, runProvenance);
231        return createModel(provenance,featureIDMap,outputIDInfo,models);
232    }
233
234    /**
235     * Construct the appropriate subtype of LibSVMModel for the prediction task.
236     * @param provenance The model provenance.
237     * @param featureIDMap The feature id map.
238     * @param outputIDInfo The output id info.
239     * @param models The svm models.
240     * @return An implementation of LibSVMModel.
241     */
242    protected abstract LibSVMModel<T> createModel(ModelProvenance provenance, ImmutableFeatureMap featureIDMap, ImmutableOutputInfo<T> outputIDInfo, List<svm_model> models);
243
244    /**
245     * Train all the LibSVM instances necessary for this dataset.
246     * @param curParams The LibSVM parameters.
247     * @param numFeatures The number of features in this dataset.
248     * @param features The features themselves.
249     * @param outputs The outputs.
250     * @param localRNG The RNG to use for seeding LibSVM's RNG.
251     * @return A list of LibSVM models.
252     */
253    protected abstract List<svm_model> trainModels(svm_parameter curParams, int numFeatures, svm_node[][] features, double[][] outputs, SplittableRandom localRNG);
254
255    /**
256     * Extracts the features and {@link Output}s in LibSVM's format.
257     * @param data The input data.
258     * @param outputInfo The output info.
259     * @param featureMap The feature info.
260     * @return The features and outputs.
261     */
262    protected abstract Pair<svm_node[][],double[][]> extractData(Dataset<T> data, ImmutableOutputInfo<T> outputInfo, ImmutableFeatureMap featureMap);
263
264    /**
265     * Constructs the svm_parameter. Most of the time this is a no-op, but
266     * classification overrides it to incorporate label weights if they exist.
267     * @param info The output info.
268     * @return The svm_parameters to use for training.
269     */
270    protected svm_parameter setupParameters(ImmutableOutputInfo<T> info) {
271        return SVMParameters.copyParameters(parameters);
272    }
273
274    @Override
275    public int getInvocationCount() {
276        return trainInvocationCounter;
277    }
278
279    @Override
280    public synchronized void setInvocationCount(int invocationCount) {
281        if(invocationCount < 0){
282            throw new IllegalArgumentException("The supplied invocationCount is less than zero.");
283        }
284
285        rng = new SplittableRandom(seed);
286
287        for (trainInvocationCounter = 0; trainInvocationCounter < invocationCount; trainInvocationCounter++){
288            SplittableRandom localRNG = rng.split();
289        }
290    }
291
292    /**
293     * Convert the example into an array of svm_node which represents a sparse feature vector.
294     * <p>
295     * If there are collisions in the feature ids then the values are summed.
296     * @param example The example to convert.
297     * @param featureIDMap The feature id map which holds the indices.
298     * @param features A buffer to use.
299     * @param <T> The type of the ouput.
300     * @return A sparse feature vector.
301     */
302    public static <T extends Output<T>> svm_node[] exampleToNodes(Example<T> example, ImmutableFeatureMap featureIDMap, List<svm_node> features) {
303        if (features == null) {
304            features = new ArrayList<>();
305        }
306        features.clear();
307        int prevIdx = -1;
308        for (Feature f : example) {
309            int id = featureIDMap.getID(f.getName());
310            double value = f.getValue();
311            if (id > prevIdx){
312                prevIdx = id;
313                svm_node n = new svm_node();
314                n.index = id;
315                n.value = value;
316                features.add(n);
317            } else if (id > -1) {
318                //
319                // Collision, deal with it.
320                int collisionIdx = Util.binarySearch(features,id,(svm_node n) -> n.index);
321                if (collisionIdx < 0) {
322                    //
323                    // Collision but not present in features
324                    // move data and bump i
325                    collisionIdx = - (collisionIdx + 1);
326                    svm_node n = new svm_node();
327                    n.index = id;
328                    n.value = value;
329                    features.add(collisionIdx,n);
330                } else {
331                    //
332                    // Collision present in features
333                    // add the values.
334                    svm_node n = features.get(collisionIdx);
335                    n.value += value;
336                }
337            }
338        }
339        return features.toArray(new svm_node[0]);
340    }
341
342    @Override
343    public TrainerProvenance getProvenance() {
344        return new TrainerProvenanceImpl(this);
345    }
346}
347