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}