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.crf; 018 019import com.google.protobuf.Any; 020import com.google.protobuf.InvalidProtocolBufferException; 021import com.oracle.labs.mlrg.olcut.util.Pair; 022import org.tribuo.Example; 023import org.tribuo.ImmutableFeatureMap; 024import org.tribuo.ImmutableOutputInfo; 025import org.tribuo.Output; 026import org.tribuo.Prediction; 027import org.tribuo.classification.Label; 028import org.tribuo.classification.sequence.ConfidencePredictingSequenceModel; 029import org.tribuo.classification.sgd.protos.CRFModelProto; 030import org.tribuo.impl.ModelDataCarrier; 031import org.tribuo.math.Parameters; 032import org.tribuo.math.la.DenseVector; 033import org.tribuo.math.la.SGDVector; 034import org.tribuo.math.la.SparseVector; 035import org.tribuo.math.la.Tensor; 036import org.tribuo.protos.core.SequenceModelProto; 037import org.tribuo.provenance.ModelProvenance; 038import org.tribuo.sequence.SequenceExample; 039 040import java.util.ArrayList; 041import java.util.Collections; 042import java.util.Comparator; 043import java.util.HashMap; 044import java.util.LinkedHashMap; 045import java.util.List; 046import java.util.Map; 047import java.util.PriorityQueue; 048import java.util.logging.Logger; 049 050import static org.tribuo.Model.BIAS_FEATURE; 051 052/** 053 * An inference time model for a linear chain CRF trained using SGD. 054 * <p> 055 * Can be switched to use Viterbi, belief propagation, or constrained BP at test time. By default it uses Viterbi. 056 * <p> 057 * See: 058 * <pre> 059 * Lafferty J, McCallum A, Pereira FC. 060 * "Conditional Random Fields: Probabilistic Models for Segmenting and Labeling Sequence Data" 061 * Proceedings of the 18th International Conference on Machine Learning 2001 (ICML 2001). 062 * </pre> 063 */ 064public class CRFModel extends ConfidencePredictingSequenceModel { 065 private static final Logger logger = Logger.getLogger(CRFModel.class.getName()); 066 private static final long serialVersionUID = 2L; 067 068 /** 069 * Protobuf serialization version. 070 */ 071 public static final int CURRENT_VERSION = 0; 072 073 private final CRFParameters parameters; 074 075 /** 076 * The type of subsequence level confidence to predict. 077 */ 078 public enum ConfidenceType { 079 /** 080 * No confidence predction. 081 */ 082 NONE, 083 /** 084 * Belief Propagation 085 */ 086 MULTIPLY, 087 /** 088 * Constrained Belief Propagation from "Confidence Estimation for Information Extraction" Culotta and McCallum 2004. 089 */ 090 CONSTRAINED_BP 091 } 092 093 private ConfidenceType confidenceType; 094 095 CRFModel(String name, ModelProvenance description, ImmutableFeatureMap featureIDMap, ImmutableOutputInfo<Label> labelIDMap, CRFParameters parameters) { 096 super(name, description, featureIDMap, labelIDMap); 097 this.parameters = parameters; 098 this.confidenceType = ConfidenceType.NONE; 099 } 100 101 /** 102 * Deserialization factory. 103 * @param version The serialized object version. 104 * @param className The class name. 105 * @param message The serialized data. 106 * @throws InvalidProtocolBufferException If the protobuf could not be parsed from the {@code message}. 107 * @return The deserialized object. 108 */ 109 public static CRFModel deserializeFromProto(int version, String className, Any message) throws InvalidProtocolBufferException { 110 if (version < 0 || version > CURRENT_VERSION) { 111 throw new IllegalArgumentException("Unknown version " + version + ", this class supports at most version " + CURRENT_VERSION); 112 } 113 CRFModelProto proto = message.unpack(CRFModelProto.class); 114 115 ModelDataCarrier<?> carrier = ModelDataCarrier.deserialize(proto.getMetadata()); 116 if (!carrier.outputDomain().getOutput(0).getClass().equals(Label.class)) { 117 throw new IllegalStateException("Invalid protobuf, output domain is not a label domain, found " + carrier.outputDomain().getClass()); 118 } 119 @SuppressWarnings("unchecked") // guarded by getClass 120 ImmutableOutputInfo<Label> outputDomain = (ImmutableOutputInfo<Label>) carrier.outputDomain(); 121 122 Parameters params = Parameters.deserialize(proto.getParams()); 123 if (!(params instanceof CRFParameters)) { 124 throw new IllegalStateException("Invalid protobuf, parameters must be CRFParameters, found " + params.getClass()); 125 } 126 127 ConfidenceType confidenceType = ConfidenceType.valueOf(proto.getConfidenceType()); 128 129 CRFModel model = new CRFModel(carrier.name(),carrier.provenance(),carrier.featureDomain(),outputDomain,(CRFParameters) params); 130 model.confidenceType = confidenceType; 131 return model; 132 } 133 134 /** 135 * Sets the inference method used for confidence prediction. 136 * If CONSTRAINED_BP uses the constrained belief propagation algorithm from Culotta and McCallum 2004, 137 * if MULTIPLY multiplies the maximum marginal for each token, if NONE uses Viterbi. 138 * 139 * @param type Enum specifying the confidence type. 140 */ 141 public void setConfidenceType(ConfidenceType type) { 142 this.confidenceType = type; 143 } 144 145 /** 146 * Get a copy of the weights for feature {@code featureID}. 147 * @param featureID The feature ID. 148 * @return The per class weights. 149 */ 150 public DenseVector getFeatureWeights(int featureID) { 151 if (featureID < 0 || featureID >= featureIDMap.size()) { 152 logger.warning("Unknown feature"); 153 return new DenseVector(0); 154 } else { 155 return parameters.getFeatureWeights(featureID); 156 } 157 } 158 159 /** 160 * Get a copy of the weights for feature named {@code featureName}. 161 * @param featureName The feature name. 162 * @return The per class weights. 163 */ 164 public DenseVector getFeatureWeights(String featureName) { 165 int id = featureIDMap.getID(featureName); 166 if (id > -1) { 167 return getFeatureWeights(featureIDMap.getID(featureName)); 168 } else { 169 logger.warning("Unknown feature"); 170 return new DenseVector(0); 171 } 172 } 173 174 @Override 175 public List<Prediction<Label>> predict(SequenceExample<Label> example) { 176 SGDVector[] features = convertToVector(example,featureIDMap); 177 List<Prediction<Label>> output = new ArrayList<>(); 178 if (confidenceType == ConfidenceType.MULTIPLY) { 179 DenseVector[] marginals = parameters.predictMarginals(features); 180 181 for (int i = 0; i < marginals.length; i++) { 182 double maxScore = Double.NEGATIVE_INFINITY; 183 Label maxLabel = null; 184 Map<String,Label> predMap = new LinkedHashMap<>(); 185 for (int j = 0; j < marginals[i].size(); j++) { 186 String labelName = outputIDMap.getOutput(j).getLabel(); 187 Label label = new Label(labelName, marginals[i].get(j)); 188 predMap.put(labelName, label); 189 if (label.getScore() > maxScore) { 190 maxScore = label.getScore(); 191 maxLabel = label; 192 } 193 } 194 output.add(new Prediction<>(maxLabel, predMap, features[i].numActiveElements(), example.get(i), true)); 195 } 196 } else { 197 int[] predLabels = parameters.predict(features); 198 199 for (int i = 0; i < predLabels.length; i++) { 200 output.add(new Prediction<>(outputIDMap.getOutput(predLabels[i]),features[i].numActiveElements(),example.get(i))); 201 } 202 } 203 204 return output; 205 } 206 207 @Override 208 public Map<String, List<Pair<String, Double>>> getTopFeatures(int n) { 209 int maxFeatures = n < 0 ? featureIDMap.size() + 1 : n; 210 211 // Uses a standard comparator rather than a Math.abs comparator as it's a log-linear model 212 // so nothing is actually negative. 213 Comparator<Pair<String,Double>> comparator = Comparator.comparing(Pair::getB); 214 215 // 216 // Use a priority queue to find the top N features. 217 int numClasses = outputIDMap.size(); 218 int numFeatures = featureIDMap.size(); 219 Map<String, List<Pair<String,Double>>> map = new HashMap<>(); 220 for (int i = 0; i < numClasses; i++) { 221 PriorityQueue<Pair<String,Double>> q = new PriorityQueue<>(maxFeatures, comparator); 222 223 for (int j = 0; j < numFeatures; j++) { 224 Pair<String,Double> curr = new Pair<>(featureIDMap.get(j).getName(), parameters.getWeight(i,j)); 225 226 if (q.size() < maxFeatures) { 227 q.offer(curr); 228 } else if (comparator.compare(curr, q.peek()) > 0) { 229 q.poll(); 230 q.offer(curr); 231 } 232 } 233 Pair<String,Double> curr = new Pair<>(BIAS_FEATURE, parameters.getBias(i)); 234 235 if (q.size() < maxFeatures) { 236 q.offer(curr); 237 } else if (comparator.compare(curr, q.peek()) > 0) { 238 q.poll(); 239 q.offer(curr); 240 } 241 List<Pair<String,Double>> b = new ArrayList<>(); 242 while (q.size() > 0) { 243 b.add(q.poll()); 244 } 245 246 Collections.reverse(b); 247 map.put(outputIDMap.getOutput(i).getLabel(), b); 248 } 249 return map; 250 } 251 252 @Override 253 public <SUB extends Subsequence> List<Double> scoreSubsequences(SequenceExample<Label> example, List<Prediction<Label>> predictions, List<SUB> subsequences) { 254 if (confidenceType == ConfidenceType.CONSTRAINED_BP) { 255 List<Chunk> chunks = new ArrayList<>(); 256 for(Subsequence subsequence : subsequences) { 257 int[] ids = new int[subsequence.length()]; 258 for(int i=0; i<ids.length; i++) { 259 ids[i] = outputIDMap.getID(predictions.get(i+subsequence.begin).getOutput()); 260 } 261 chunks.add(new Chunk(subsequence.begin, ids)); 262 } 263 return scoreChunks(example, chunks); 264 } else { 265 return ConfidencePredictingSequenceModel.multiplyWeights(predictions, subsequences); 266 } 267 } 268 269 /** 270 * Scores the chunks using constrained belief propagation. 271 * @param example The example to score. 272 * @param chunks The predicted chunks. 273 * @return The scores. 274 */ 275 public List<Double> scoreChunks(SequenceExample<Label> example, List<Chunk> chunks) { 276 SGDVector[] features = convertToVector(example,featureIDMap); 277 return parameters.predictConfidenceUsingCBP(features,chunks); 278 } 279 280 /** 281 * Generates a human readable string containing all the weights in this model. 282 * @return A string containing all the weight values. 283 */ 284 public String generateWeightsString() { 285 StringBuilder buffer = new StringBuilder(); 286 287 Tensor[] weights = parameters.get(); 288 289 buffer.append("Biases = "); 290 buffer.append(weights[0].toString()); 291 buffer.append('\n'); 292 293 buffer.append("Feature-Label weights = \n"); 294 buffer.append(weights[1].toString()); 295 buffer.append('\n'); 296 297 buffer.append("Label-Label weights = \n"); 298 buffer.append(weights[2].toString()); 299 buffer.append('\n'); 300 301 return buffer.toString(); 302 } 303 304 @Override 305 public SequenceModelProto serialize() { 306 ModelDataCarrier<Label> carrier = createDataCarrier(); 307 CRFModelProto.Builder modelBuilder = CRFModelProto.newBuilder(); 308 modelBuilder.setConfidenceType(confidenceType.name()); 309 modelBuilder.setMetadata(carrier.serialize()); 310 modelBuilder.setParams(parameters.serialize()); 311 312 SequenceModelProto.Builder builder = SequenceModelProto.newBuilder(); 313 builder.setVersion(CURRENT_VERSION); 314 builder.setClassName(CRFModel.class.getName()); 315 builder.setSerializedData(Any.pack(modelBuilder.build())); 316 317 return builder.build(); 318 } 319 320 /** 321 * Converts a {@link SequenceExample} into an array of {@link SparseVector}s suitable for CRF prediction. 322 * @deprecated As it's replaced with {@link #convertToVector} which is more flexible. 323 * @param example The sequence example to convert 324 * @param featureIDMap The feature id map, used to discover the number of features. 325 * @param <T> The type parameter of the sequence example. 326 * @return An array of {@link SparseVector}. 327 */ 328 @Deprecated 329 public static <T extends Output<T>> SparseVector[] convert(SequenceExample<T> example, ImmutableFeatureMap featureIDMap) { 330 int length = example.size(); 331 if (length == 0) { 332 throw new IllegalArgumentException("SequenceExample is empty, " + example.toString()); 333 } 334 SparseVector[] features = new SparseVector[length]; 335 int i = 0; 336 for (Example<T> e : example) { 337 features[i] = SparseVector.createSparseVector(e,featureIDMap,false); 338 if (features[i].numActiveElements() == 0) { 339 throw new IllegalArgumentException("No features found in Example " + e.toString()); 340 } 341 i++; 342 } 343 return features; 344 } 345 346 /** 347 * Converts a {@link SequenceExample} into an array of {@link SparseVector}s and labels suitable for CRF prediction. 348 * @deprecated As it's replaced with {@link #convertToVector} which is more flexible. 349 * @param example The sequence example to convert 350 * @param featureIDMap The feature id map, used to discover the number of features. 351 * @param labelIDMap The label id map, used to get the index of the labels. 352 * @return A {@link Pair} of an int array of labels and an array of {@link SparseVector}. 353 */ 354 @Deprecated 355 public static Pair<int[],SparseVector[]> convert(SequenceExample<Label> example, ImmutableFeatureMap featureIDMap, ImmutableOutputInfo<Label> labelIDMap) { 356 int length = example.size(); 357 if (length == 0) { 358 throw new IllegalArgumentException("SequenceExample is empty, " + example.toString()); 359 } 360 int[] labels = new int[length]; 361 SparseVector[] features = new SparseVector[length]; 362 int i = 0; 363 for (Example<Label> e : example) { 364 labels[i] = labelIDMap.getID(e.getOutput()); 365 features[i] = SparseVector.createSparseVector(e,featureIDMap,false); 366 if (features[i].numActiveElements() == 0) { 367 throw new IllegalArgumentException("No features found in Example " + e.toString()); 368 } 369 i++; 370 } 371 return new Pair<>(labels,features); 372 } 373 374 /** 375 * Converts a {@link SequenceExample} into an array of {@link SGDVector}s suitable for CRF prediction. 376 * @param example The sequence example to convert 377 * @param featureIDMap The feature id map, used to discover the number of features. 378 * @param <T> The type parameter of the sequence example. 379 * @return An array of {@link SGDVector}. 380 */ 381 public static <T extends Output<T>> SGDVector[] convertToVector(SequenceExample<T> example, ImmutableFeatureMap featureIDMap) { 382 int length = example.size(); 383 if (length == 0) { 384 throw new IllegalArgumentException("SequenceExample is empty, " + example.toString()); 385 } 386 int featureSpaceSize = featureIDMap.size(); 387 SGDVector[] features = new SGDVector[length]; 388 int i = 0; 389 for (Example<T> e : example) { 390 if (e.size() == featureSpaceSize) { 391 features[i] = DenseVector.createDenseVector(e, featureIDMap, false); 392 } else { 393 features[i] = SparseVector.createSparseVector(e, featureIDMap, false); 394 } 395 if (features[i].numActiveElements() == 0) { 396 throw new IllegalArgumentException("No features found in Example " + e.toString()); 397 } 398 i++; 399 } 400 return features; 401 } 402 403 /** 404 * Converts a {@link SequenceExample} into an array of {@link SGDVector}s and labels suitable for CRF prediction. 405 * @param example The sequence example to convert 406 * @param featureIDMap The feature id map, used to discover the number of features. 407 * @param labelIDMap The label id map, used to get the index of the labels. 408 * @return A {@link Pair} of an int array of labels and an array of {@link SparseVector}. 409 */ 410 public static Pair<int[],SGDVector[]> convertToVector(SequenceExample<Label> example, ImmutableFeatureMap featureIDMap, ImmutableOutputInfo<Label> labelIDMap) { 411 int length = example.size(); 412 if (length == 0) { 413 throw new IllegalArgumentException("SequenceExample is empty, " + example.toString()); 414 } 415 int featureSpaceSize = featureIDMap.size(); 416 int[] labels = new int[length]; 417 SGDVector[] features = new SGDVector[length]; 418 int i = 0; 419 for (Example<Label> e : example) { 420 labels[i] = labelIDMap.getID(e.getOutput()); 421 if (e.size() == featureSpaceSize) { 422 features[i] = DenseVector.createDenseVector(e, featureIDMap, false); 423 } else { 424 features[i] = SparseVector.createSparseVector(e, featureIDMap, false); 425 } 426 if (features[i].numActiveElements() == 0) { 427 throw new IllegalArgumentException("No features found in Example " + e.toString()); 428 } 429 i++; 430 } 431 return new Pair<>(labels,features); 432 } 433}