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.classification.sgd.kernel;
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.Trainer;
026import org.tribuo.WeightedExamples;
027import org.tribuo.classification.Label;
028import org.tribuo.classification.sgd.Util;
029import org.tribuo.math.kernel.Kernel;
030import org.tribuo.math.la.DenseMatrix;
031import org.tribuo.math.la.DenseVector;
032import org.tribuo.math.la.SGDVector;
033import org.tribuo.math.la.SparseVector;
034import org.tribuo.provenance.ModelProvenance;
035import org.tribuo.provenance.TrainerProvenance;
036import org.tribuo.provenance.impl.TrainerProvenanceImpl;
037
038import java.time.OffsetDateTime;
039import java.util.HashMap;
040import java.util.Map;
041import java.util.SplittableRandom;
042import java.util.logging.Logger;
043
044/**
045 * A trainer for a kernelised model using the Pegasos optimiser.
046 * <p>
047 * The Pegasos optimiser is extremely sensitive to the lambda parameter, and this
048 * value must be tuned to get good performance.
049 * <p>
050 * See:
051 * <pre>
052 * Shalev-Shwartz S, Singer Y, Srebro N, Cotter A
053 * "Pegasos: Primal Estimated Sub-Gradient Solver for SVM"
054 * Mathematical Programming, 2011.
055 * </pre>
056 */
057public class KernelSVMTrainer implements Trainer<Label>, WeightedExamples {
058    private static final Logger logger = Logger.getLogger(KernelSVMTrainer.class.getName());
059
060    @Config(mandatory = true,description="SVM kernel.")
061    private Kernel kernel;
062
063    @Config(mandatory = true,description="Step size.")
064    private double lambda;
065
066    @Config(description="Number of SGD epochs.")
067    private int epochs = 5;
068
069    @Config(description="Log values after this many updates.")
070    private int loggingInterval = -1;
071
072    @Config(mandatory = true,description="Seed for the RNG used to shuffle elements.")
073    private long seed;
074
075    @Config(description="Shuffle the data before each epoch. Only turn off for debugging.")
076    private boolean shuffle = true;
077
078    private SplittableRandom rng;
079
080    private int trainInvocationCounter;
081
082    /**
083     * Constructs a trainer for a kernel SVM model.
084     * @param kernel The kernel function to use as a similarity measure.
085     * @param epochs The number of epochs (complete passes through the training data).
086     * @param lambda l2 regulariser on the support vectors.
087     * @param loggingInterval Log the loss after this many iterations. If -1 don't log anything.
088     * @param seed A seed for the random number generator, used to shuffle the examples before each epoch.
089     */
090    public KernelSVMTrainer(Kernel kernel, double lambda, int epochs, int loggingInterval, long seed) {
091        this.kernel = kernel;
092        this.lambda = lambda;
093        this.epochs = epochs;
094        this.loggingInterval = loggingInterval;
095        this.seed = seed;
096        postConfig();
097    }
098
099    /**
100     * Constructs a trainer for a kernel SVM model.
101     * Sets the logging interval to 1000.
102     * @param kernel The kernel function to use as a similarity measure.
103     * @param lambda l2 regulariser on the support vectors.
104     * @param epochs The number of epochs (complete passes through the training data).
105     * @param seed A seed for the random number generator, used to shuffle the examples before each epoch.
106     */
107    public KernelSVMTrainer(Kernel kernel, double lambda, int epochs, long seed) {
108        this(kernel,lambda,epochs,1000,seed);
109    }
110
111    /**
112     * For olcut.
113     */
114    private KernelSVMTrainer() { }
115
116    /**
117     * Used by the OLCUT configuration system, and should not be called by external code.
118     */
119    @Override
120    public synchronized void postConfig() {
121        this.rng = new SplittableRandom(seed);
122    }
123
124    /**
125     * Turn on or off shuffling of examples.
126     * <p>
127     * This isn't exposed in the constructor as it defaults to on.
128     * This method should only be used for debugging.
129     * @param shuffle If true shuffle the examples, if false leave them in their current order.
130     */
131    public void setShuffle(boolean shuffle) {
132        this.shuffle = shuffle;
133    }
134
135    @Override
136    public KernelSVMModel train(Dataset<Label> examples, Map<String, Provenance> runProvenance) {
137        return(train(examples, runProvenance, INCREMENT_INVOCATION_COUNT));
138    }
139
140    @Override
141    public KernelSVMModel train(Dataset<Label> examples, Map<String, Provenance> runProvenance, int invocationCount) {
142        if (examples.getOutputInfo().getUnknownCount() > 0) {
143            throw new IllegalArgumentException("The supplied Dataset contained unknown Outputs, and this Trainer is supervised.");
144        }
145        // Creates a new RNG, adds one to the invocation count.
146        TrainerProvenance trainerProvenance;
147        SplittableRandom localRNG;
148        synchronized(this) {
149            if(invocationCount != INCREMENT_INVOCATION_COUNT) {
150                setInvocationCount(invocationCount);
151            }
152            localRNG = rng.split();
153            trainerProvenance = getProvenance();
154            trainInvocationCounter++;
155        }
156        ImmutableOutputInfo<Label> labelIDMap = examples.getOutputIDInfo();
157        ImmutableFeatureMap featureIDMap = examples.getFeatureIDMap();
158        SparseVector[] sgdFeatures = new SparseVector[examples.size()];
159        int[] sgdLabels = new int[examples.size()];
160        double[] weights = new double[examples.size()];
161        int[] indices = new int[examples.size()];
162        int n = 0;
163        for (Example<Label> example : examples) {
164            weights[n] = example.getWeight();
165            sgdFeatures[n] = SparseVector.createSparseVector(example,featureIDMap,true);
166            sgdLabels[n] = labelIDMap.getID(example.getOutput());
167            indices[n] = n;
168            n++;
169        }
170        logger.info(String.format("Training Kernel SVM with %d examples", n));
171        logger.info(labelIDMap.toReadableString());
172
173        double loss = 0.0;
174        int iteration = 0;
175        Map<Integer,SparseVector> supportVectors = new HashMap<>();
176        double[][] alphas = new double[labelIDMap.size()][examples.size()];
177
178        for (int i = 0; i < epochs; i++) {
179            if (shuffle) {
180                Util.shuffleInPlace(sgdFeatures, sgdLabels, weights, indices, localRNG);
181            }
182            for (int j = 0; j < sgdFeatures.length; j++) {
183                SGDVector pred = predict(sgdFeatures[j],supportVectors,alphas);
184                pred.add(sgdLabels[j],-1.0);
185                int predIndex = pred.indexOfMax();
186
187                if (sgdLabels[j] != predIndex) {
188                    loss += (pred.get(sgdLabels[j]) - pred.get(predIndex)) * weights[j];
189                    supportVectors.putIfAbsent(indices[j],sgdFeatures[j]);
190                    alphas[sgdLabels[j]][indices[j]] += weights[j];
191                }
192
193                iteration++;
194                if ((loggingInterval != -1) && (iteration % loggingInterval == 0)) {
195                    logger.info("At iteration " + iteration + ", average loss = " + loss/loggingInterval + " with " + supportVectors.size() + " support vectors.");
196                    loss = 0.0;
197                }
198            }
199            logger.fine("Finished epoch " + i);
200        }
201
202        DenseMatrix alphaMatrix = new DenseMatrix(alphas.length,supportVectors.size());
203        for (int i = 0; i < alphas.length; i++) {
204            int rowCounter = 0;
205            for (int j = 0; j < sgdFeatures.length; j++) {
206                if (supportVectors.containsKey(j)) {
207                    alphaMatrix.set(i, rowCounter, alphas[i][j]);
208                    rowCounter++;
209                }
210            }
211        }
212
213        int counter = 0;
214        SparseVector[] supportArray = new SparseVector[supportVectors.size()];
215        for (int i = 0; i < sgdFeatures.length; i++) {
216            SparseVector value = supportVectors.get(i);
217            if (value != null) {
218                supportArray[counter] = value;
219                counter++;
220            }
221        }
222
223        ModelProvenance provenance = new ModelProvenance(KernelSVMModel.class.getName(), OffsetDateTime.now(), examples.getProvenance(), trainerProvenance, runProvenance);
224        //public KernelSVMModel(String name, String description, ImmutableInfoMap featureIDMap, ImmutableInfoMap outputIDInfo, Kernel kernel, SparseVector[] supportVectors, DenseMatrix weights)
225        KernelSVMModel model = new KernelSVMModel("kernel-model",provenance,featureIDMap,labelIDMap,kernel,supportArray,alphaMatrix);
226        return model;
227    }
228
229    @Override
230    public int getInvocationCount() {
231        return trainInvocationCounter;
232    }
233
234    @Override
235    public synchronized void setInvocationCount(int invocationCount){
236        if(invocationCount < 0){
237            throw new IllegalArgumentException("The supplied invocationCount is less than zero.");
238        }
239
240        rng = new SplittableRandom(seed);
241
242        for (trainInvocationCounter = 0; trainInvocationCounter < invocationCount; trainInvocationCounter++){
243            SplittableRandom localRNG = rng.split();
244        }
245
246    }
247
248    @Override
249    public String toString() {
250        return "KernelSVMTrainer(kernel="+kernel.toString()+",lambda="+lambda+",epochs="+epochs+",seed="+seed+")";
251    }
252
253    private SGDVector predict(SparseVector features, Map<Integer,SparseVector> sv, double[][] alphas) {
254        double[] score = new double[alphas.length];
255
256        for (Map.Entry<Integer, SparseVector> e : sv.entrySet()) {
257            double distance = kernel.similarity(features,e.getValue());
258            for (int i = 0; i < alphas.length; i++) {
259                score[i] += alphas[i][e.getKey()] * distance;
260            }
261        }
262
263        return DenseVector.createDenseVector(score);
264    }
265
266    @Override
267    public TrainerProvenance getProvenance() {
268        return new TrainerProvenanceImpl(this);
269    }
270}