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}