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.classification.sgd.linear;
018
019import com.google.protobuf.Any;
020import com.google.protobuf.InvalidProtocolBufferException;
021import org.tribuo.Example;
022import org.tribuo.ImmutableFeatureMap;
023import org.tribuo.ImmutableOutputInfo;
024import org.tribuo.ONNXExportable;
025import org.tribuo.Prediction;
026import org.tribuo.classification.Label;
027import org.tribuo.classification.sgd.protos.ClassificationLinearSGDProto;
028import org.tribuo.common.sgd.AbstractLinearSGDModel;
029import org.tribuo.impl.ModelDataCarrier;
030import org.tribuo.math.LinearParameters;
031import org.tribuo.math.Parameters;
032import org.tribuo.math.la.DenseMatrix;
033import org.tribuo.math.la.DenseVector;
034import org.tribuo.math.util.VectorNormalizer;
035import org.tribuo.protos.core.ModelProto;
036import org.tribuo.provenance.ModelProvenance;
037import org.tribuo.util.onnx.ONNXNode;
038
039import java.io.IOException;
040import java.util.LinkedHashMap;
041import java.util.Map;
042
043/**
044 * The inference time version of a linear model trained using SGD.
045 * <p>
046 * See:
047 * <pre>
048 * Bottou L.
049 * "Large-Scale Machine Learning with Stochastic Gradient Descent"
050 * Proceedings of COMPSTAT, 2010.
051 * </pre>
052 */
053public class LinearSGDModel extends AbstractLinearSGDModel<Label> implements ONNXExportable {
054    private static final long serialVersionUID = 2L;
055
056    /**
057     * Protobuf serialization version.
058     */
059    public static final int CURRENT_VERSION = 0;
060
061    private final VectorNormalizer normalizer;
062
063    // Unused as the weights now live in AbstractSGDModel
064    // It remains for serialization compatibility with Tribuo 4.0
065    @Deprecated
066    private DenseMatrix weights = null;
067
068    /**
069     * Constructs a linear classification model trained via SGD.
070     * @param name The model name.
071     * @param provenance The model provenance.
072     * @param featureIDMap The feature domain.
073     * @param outputIDInfo The output domain.
074     * @param parameters The model parameters (i.e., the weight matrix).
075     * @param normalizer The normalization function.
076     * @param generatesProbabilities Does this model generate probabilities?
077     */
078    LinearSGDModel(String name, ModelProvenance provenance,
079                   ImmutableFeatureMap featureIDMap, ImmutableOutputInfo<Label> outputIDInfo,
080                   LinearParameters parameters, VectorNormalizer normalizer, boolean generatesProbabilities) {
081        super(name, provenance, featureIDMap, outputIDInfo, parameters, generatesProbabilities);
082        this.normalizer = normalizer;
083    }
084
085    /**
086     * Deserialization factory.
087     * @param version The serialized object version.
088     * @param className The class name.
089     * @param message The serialized data.
090     * @throws InvalidProtocolBufferException If the protobuf could not be parsed from the {@code message}.
091     * @return The deserialized object.
092     */
093    public static LinearSGDModel deserializeFromProto(int version, String className, Any message) throws InvalidProtocolBufferException {
094        if (version < 0 || version > CURRENT_VERSION) {
095            throw new IllegalArgumentException("Unknown version " + version + ", this class supports at most version " + CURRENT_VERSION);
096        }
097        ClassificationLinearSGDProto proto = message.unpack(ClassificationLinearSGDProto.class);
098
099        ModelDataCarrier<?> carrier = ModelDataCarrier.deserialize(proto.getMetadata());
100        if (!carrier.outputDomain().getOutput(0).getClass().equals(Label.class)) {
101            throw new IllegalStateException("Invalid protobuf, output domain is not a label domain, found " + carrier.outputDomain().getClass());
102        }
103        @SuppressWarnings("unchecked") // guarded by getClass
104        ImmutableOutputInfo<Label> outputDomain = (ImmutableOutputInfo<Label>) carrier.outputDomain();
105
106        Parameters params = Parameters.deserialize(proto.getParams());
107        if (!(params instanceof LinearParameters)) {
108            throw new IllegalStateException("Invalid protobuf, parameters must be LinearParameters, found " + params.getClass());
109        }
110
111        VectorNormalizer normalizer = VectorNormalizer.deserialize(proto.getNormalizer());
112
113        return new LinearSGDModel(carrier.name(),carrier.provenance(),carrier.featureDomain(),outputDomain,(LinearParameters) params, normalizer, carrier.generatesProbabilities());
114    }
115
116    @Override
117    public Prediction<Label> predict(Example<Label> example) {
118        PredAndActive predTuple = predictSingle(example);
119        DenseVector prediction = predTuple.prediction;
120        prediction.normalize(normalizer);
121
122        double maxScore = Double.NEGATIVE_INFINITY;
123        Label maxLabel = null;
124        Map<String,Label> predMap = new LinkedHashMap<>();
125        for (int i = 0; i < prediction.size(); i++) {
126            String labelName = outputIDInfo.getOutput(i).getLabel();
127            double score = prediction.get(i);
128            Label label = new Label(labelName, score);
129            predMap.put(labelName,label);
130            if (score > maxScore) {
131                maxScore = score;
132                maxLabel = label;
133            }
134        }
135        return new Prediction<>(maxLabel, predMap, predTuple.numActiveFeatures-1, example, generatesProbabilities);
136    }
137
138    @Override
139    public ModelProto serialize() {
140        ModelDataCarrier<Label> carrier = createDataCarrier();
141        ClassificationLinearSGDProto.Builder modelBuilder = ClassificationLinearSGDProto.newBuilder();
142        modelBuilder.setMetadata(carrier.serialize());
143        modelBuilder.setParams(modelParameters.serialize());
144        modelBuilder.setNormalizer(normalizer.serialize());
145
146        ModelProto.Builder builder = ModelProto.newBuilder();
147        builder.setVersion(CURRENT_VERSION);
148        builder.setClassName(LinearSGDModel.class.getName());
149        builder.setSerializedData(Any.pack(modelBuilder.build()));
150
151        return builder.build();
152    }
153
154    @Override
155    protected LinearSGDModel copy(String newName, ModelProvenance newProvenance) {
156        return new LinearSGDModel(newName,newProvenance,featureIDMap,outputIDInfo,(LinearParameters)modelParameters.copy(),normalizer,generatesProbabilities);
157    }
158
159    @Override
160    protected String getDimensionName(int index) {
161        return outputIDInfo.getOutput(index).getLabel();
162    }
163
164    @Override
165    protected ONNXNode onnxOutput(ONNXNode input) {
166        return normalizer.exportNormalizer(input);
167    }
168
169    @Override
170    protected String onnxModelName() {
171        return "Classification-LinearSGDModel";
172    }
173
174    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
175        in.defaultReadObject();
176
177        // Bounce old 4.0 style models into the new 4.1 style models
178        if (weights != null && modelParameters == null) {
179            modelParameters = new LinearParameters(weights);
180            weights = null;
181            addBias = true;
182        }
183    }
184}