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.google.protobuf.Any;
020import com.google.protobuf.ByteString;
021import de.bwaldvogel.liblinear.Linear;
022import de.bwaldvogel.liblinear.SolverType;
023import org.tribuo.Example;
024import org.tribuo.Excuse;
025import org.tribuo.ImmutableFeatureMap;
026import org.tribuo.ImmutableOutputInfo;
027import org.tribuo.Model;
028import org.tribuo.Output;
029import org.tribuo.common.liblinear.protos.LibLinearModelProto;
030import org.tribuo.common.liblinear.protos.LibLinearProto;
031import org.tribuo.impl.ModelDataCarrier;
032import org.tribuo.protos.core.ModelProto;
033import org.tribuo.provenance.ModelProvenance;
034import org.tribuo.util.Util;
035
036import java.io.ByteArrayOutputStream;
037import java.io.IOException;
038import java.io.ObjectOutputStream;
039import java.io.StringReader;
040import java.io.StringWriter;
041import java.lang.reflect.Field;
042import java.util.ArrayList;
043import java.util.Arrays;
044import java.util.Collections;
045import java.util.List;
046import java.util.Optional;
047import java.util.logging.Logger;
048import java.util.stream.Collectors;
049
050/**
051 * A {@link Model} which wraps a LibLinear-java model.
052 * <p>
053 * It disables the LibLinear debug output as it's very chatty.
054 * <p>
055 * See:
056 * <pre>
057 * Fan RE, Chang KW, Hsieh CJ, Wang XR, Lin CJ.
058 * "LIBLINEAR: A library for Large Linear Classification"
059 * Journal of Machine Learning Research, 2008.
060 * </pre>
061 * and for the original algorithm:
062 * <pre>
063 * Cortes C, Vapnik V.
064 * "Support-Vector Networks"
065 * Machine Learning, 1995.
066 * </pre>
067 */
068public abstract class LibLinearModel<T extends Output<T>> extends Model<T> {
069    private static final long serialVersionUID = 3L;
070
071    private static final Logger logger = Logger.getLogger(LibLinearModel.class.getName());
072
073    /**
074     * Protobuf serialization version.
075     */
076    public static final int CURRENT_VERSION = 0;
077
078    /**
079     * The list of LibLinear models. Multiple models are used by multi-label and multidimensional regression outputs.
080     * <p>
081     * Not final to support deserialization reordering of multidimensional regression models which have an incorrect id mapping.
082     * Will be final again in some future version which doesn't maintain serialization compatibility with 4.X.
083     */
084    protected List<de.bwaldvogel.liblinear.Model> models;
085
086    /**
087     * Constructs a LibLinear model from the supplied arguments.
088     * @param name The model name.
089     * @param description The model provenance.
090     * @param featureIDMap The features this model knows about.
091     * @param labelIDMap The outputs this model produces.
092     * @param generatesProbabilities Does this model generate probabilities?
093     * @param models The liblinear models themselves.
094     */
095    protected LibLinearModel(String name, ModelProvenance description, ImmutableFeatureMap featureIDMap, ImmutableOutputInfo<T> labelIDMap, boolean generatesProbabilities, List<de.bwaldvogel.liblinear.Model> models) {
096        super(name, description, featureIDMap, labelIDMap, generatesProbabilities);
097        this.models = models;
098        Linear.disableDebugOutput();
099    }
100
101    /**
102     * Returns an unmodifiable list containing a copy of each model.
103     * <p>
104     * As liblinear-java models don't expose a copy constructor this requires
105     * serializing each model to a String and rebuilding it, and is thus quite expensive.
106     *
107     * @return A copy of all of the models.
108     */
109    public List<de.bwaldvogel.liblinear.Model> getInnerModels() {
110        List<de.bwaldvogel.liblinear.Model> copy = new ArrayList<>();
111
112        for (de.bwaldvogel.liblinear.Model m : models) {
113            copy.add(copyModel(m));
114        }
115
116        return Collections.unmodifiableList(copy);
117    }
118
119    /**
120     * This call is expensive as it copies out the weight matrix from the
121     * LibLinear model.
122     * <p>
123     * Prefer {@link LibLinearModel#getExcuses} to get multiple excuses.
124     * @param e The example to excuse.
125     * @return An {@link Excuse} for this example.
126     */
127    @Override
128    public Optional<Excuse<T>> getExcuse(Example<T> e) {
129        double[][] featureWeights = getFeatureWeights();
130        return Optional.of(innerGetExcuse(e, featureWeights));
131    }
132
133    @Override
134    public Optional<List<Excuse<T>>> getExcuses(Iterable<Example<T>> examples) {
135        //This call copies out the weights, so it's better to do it once
136        double[][] featureWeights = getFeatureWeights();
137        List<Excuse<T>> excuses = new ArrayList<>();
138
139        for (Example<T> e : examples) {
140            excuses.add(innerGetExcuse(e, featureWeights));
141        }
142
143        return Optional.of(excuses);
144    }
145
146    /**
147     * Copies the model by writing it out to a String and loading it back in.
148     * <p>
149     * Unfortunately liblinear-java doesn't have a copy constructor on it's model.
150     * @param model The model to copy.
151     * @return A deep copy of the model.
152     */
153    protected static de.bwaldvogel.liblinear.Model copyModel(de.bwaldvogel.liblinear.Model model) {
154        try {
155            StringWriter writer = new StringWriter();
156            Linear.saveModel(writer,model);
157            String modelString = writer.toString();
158            StringReader reader = new StringReader(modelString);
159            return Linear.loadModel(reader);
160        } catch (IOException e) {
161            throw new IllegalStateException("IOException found when copying the model in memory via a String.",e);
162        }
163    }
164
165    /**
166     * Extracts the feature weights from the models.
167     * The first dimension corresponds to the model index.
168     * @return The feature weights.
169     */
170    protected abstract double[][] getFeatureWeights();
171
172    /**
173     * The call to getFeatureWeights in the public methods copies the
174     * weights array so this inner method exists to save the copy in getExcuses.
175     * <p>
176     * If it becomes a problem then we could cache the feature weights in the
177     * model.
178     *
179     * @param e The example.
180     * @param featureWeights The per dimension feature weights.
181     * @return An excuse for this example.
182     */
183    protected abstract Excuse<T> innerGetExcuse(Example<T> e, double[][] featureWeights);
184
185    @Override
186    public ModelProto serialize() {
187        ModelDataCarrier<T> carrier = createDataCarrier();
188
189        LibLinearModelProto.Builder modelBuilder = LibLinearModelProto.newBuilder();
190        modelBuilder.setMetadata(carrier.serialize());
191        for (de.bwaldvogel.liblinear.Model m : models) {
192            try {
193                ByteArrayOutputStream baos = new ByteArrayOutputStream();
194                ObjectOutputStream oos = new ObjectOutputStream(baos);
195                oos.writeObject(m);
196                oos.close();
197                modelBuilder.addModels(ByteString.copyFrom(baos.toByteArray()));
198            } catch (IOException e) {
199                throw new IllegalStateException("Could not serialize liblinear model to byte array");
200            }
201        }
202
203        ModelProto.Builder builder = ModelProto.newBuilder();
204        builder.setSerializedData(Any.pack(modelBuilder.build()));
205        builder.setClassName(this.getClass().getName());
206        builder.setVersion(CURRENT_VERSION);
207
208        return builder.build();
209    }
210
211    /**
212     * Serialize the LibLinear model into a protobuf.
213     * <p>
214     * Note deserializing {@link LibLinearProto} requires reflective access into {@code de.bwaldvogel.liblinear.Model}
215     * and thus requires additional command line permissions when running with the module system. For the time
216     * being we use Java serialization to a byte array rather than this method and {@code LibLinearProto}.
217     * @param model The model to serialize.
218     * @return The protobuf.
219     */
220    private static LibLinearProto serializeModel(de.bwaldvogel.liblinear.Model model) {
221        LibLinearProto.Builder builder = LibLinearProto.newBuilder();
222
223        builder.setBias(model.getBias());
224        builder.addAllLabel(Arrays.stream(model.getLabels()).boxed().collect(Collectors.toList()));
225        builder.setNrClass(model.getNrClass());
226        builder.setNrFeature(model.getNrFeature());
227        builder.setSolverType(model.getSolverType().name());
228        builder.addAllW(Arrays.stream(model.getFeatureWeights()).boxed().collect(Collectors.toList()));
229        if (model.getSolverType().isOneClass()) {
230            builder.setRho(model.getDecfunRho());
231        }
232
233        return builder.build();
234    }
235
236    /**
237     * Deserialize a LibLinear model from a protobuf.
238     * <p>
239     * Note this method requires reflective access into {@code de.bwaldvogel.liblinear.Model} and thus
240     * requires additional command line permissions when running with the module system. For the time
241     * being we use Java serialization to a byte array rather than this method and {@code LibLinearProto}.
242     * @param proto The protobuf to deserialize.
243     * @return The model.
244     */
245    private static de.bwaldvogel.liblinear.Model deserializeModels(LibLinearProto proto) {
246        de.bwaldvogel.liblinear.Model model = new de.bwaldvogel.liblinear.Model();
247        Class<de.bwaldvogel.liblinear.Model> modelClass = de.bwaldvogel.liblinear.Model.class;
248        setField(modelClass, "bias", model, proto.getBias());
249        setField(modelClass, "label", model, Util.toPrimitiveInt(proto.getLabelList()));
250        setField(modelClass, "nr_class", model, proto.getNrClass());
251        setField(modelClass, "nr_feature", model, proto.getNrFeature());
252        setField(modelClass, "solverType", model, SolverType.valueOf(proto.getSolverType()));
253        setField(modelClass, "w", model, Util.toPrimitiveDouble(proto.getWList()));
254        setField(modelClass, "rho", model, proto.getRho());
255        return model;
256    }
257
258    /**
259     * Sets a field on the supplied object.
260     * <p>
261     * Wraps any exceptions in {@link IllegalStateException}.
262     * @param clazz The class.
263     * @param fieldName The field name to set.
264     * @param host The host object.
265     * @param value The new field value.
266     */
267    private static <U> void setField(Class<U> clazz, String fieldName, U host, Object value) {
268        try {
269            Field biasField = clazz.getField(fieldName);
270            biasField.setAccessible(true);
271            biasField.set(host, value);
272            biasField.setAccessible(false);
273        } catch (NoSuchFieldException | IllegalAccessException e) {
274            throw new IllegalStateException("Failed to write to field " + fieldName, e);
275        }
276    }
277}