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.classification.sgd.protos.CRFParametersProto; 023import org.tribuo.math.Parameters; 024import org.tribuo.math.la.DenseMatrix; 025import org.tribuo.math.la.DenseSparseMatrix; 026import org.tribuo.math.la.DenseVector; 027import org.tribuo.math.la.Matrix; 028import org.tribuo.math.la.SGDVector; 029import org.tribuo.math.la.Tensor; 030import org.tribuo.math.protos.ParametersProto; 031import org.tribuo.math.util.HeapMerger; 032import org.tribuo.math.util.Merger; 033import org.tribuo.protos.ProtoUtil; 034 035import java.io.Serializable; 036import java.util.ArrayList; 037import java.util.Arrays; 038import java.util.List; 039 040/** 041 * A {@link Parameters} for training a CRF using SGD. 042 */ 043public class CRFParameters implements Parameters, Serializable { 044 private static final long serialVersionUID = 1L; 045 046 /** 047 * Protobuf serialization version. 048 */ 049 public static final int CURRENT_VERSION = 0; 050 051 private final int numLabels; 052 private final int numFeatures; 053 054 private static final Merger merger = new HeapMerger(); 055 056 /** 057 * This variable is an array with 3 elements corresponding to the three weight matrices. 058 */ 059 private Tensor[] weights; 060 061 private DenseVector biases; //weights[0]; 062 private DenseMatrix featureLabelWeights; //weights[1]; 063 private DenseMatrix labelLabelWeights; //weights[2]; 064 065 CRFParameters(int numFeatures, int numLabels) { 066 this.biases = new DenseVector(numLabels); 067 this.featureLabelWeights = new DenseMatrix(numLabels,numFeatures); 068 this.labelLabelWeights = new DenseMatrix(numLabels,numLabels); 069 this.weights = new Tensor[3]; 070 weights[0] = biases; 071 weights[1] = featureLabelWeights; 072 weights[2] = labelLabelWeights; 073 this.numLabels = numLabels; 074 this.numFeatures = numFeatures; 075 } 076 077 private CRFParameters(DenseVector biases, DenseMatrix featureLabelWeights, DenseMatrix labelLabelWeights) { 078 this.weights = new Tensor[3]; 079 weights[0] = biases; 080 weights[1] = featureLabelWeights; 081 weights[2] = labelLabelWeights; 082 this.numLabels = biases.size(); 083 this.numFeatures = featureLabelWeights.getDimension2Size(); 084 this.biases = biases; 085 this.featureLabelWeights = featureLabelWeights; 086 this.labelLabelWeights = labelLabelWeights; 087 } 088 089 /** 090 * Deserialization factory. 091 * @param version The serialized object version. 092 * @param className The class name. 093 * @param message The serialized data. 094 * @throws InvalidProtocolBufferException If the protobuf could not be parsed from the {@code message}. 095 * @return The deserialized object. 096 */ 097 public static CRFParameters deserializeFromProto(int version, String className, Any message) throws InvalidProtocolBufferException { 098 if (version < 0 || version > CURRENT_VERSION) { 099 throw new IllegalArgumentException("Unknown version " + version + ", this class supports at most version " + CURRENT_VERSION); 100 } 101 CRFParametersProto proto = message.unpack(CRFParametersProto.class); 102 int numLabels = proto.getNumLabels(); 103 int numFeatures = proto.getNumFeatures(); 104 Tensor biasTensor = ProtoUtil.deserialize(proto.getBiases()); 105 Tensor featureLabelTensor = ProtoUtil.deserialize(proto.getFeatureLabelWeights()); 106 Tensor labelLabelTensor = ProtoUtil.deserialize(proto.getLabelLabelWeights()); 107 if (!(biasTensor instanceof DenseVector)) { 108 throw new IllegalArgumentException("Invalid protobuf, expected bias vector, found " + biasTensor.getClass().getSimpleName()); 109 } else if (((DenseVector)biasTensor).size() != numLabels) { 110 throw new IllegalArgumentException("Invalid protobuf, expected bias vector with " + numLabels + " elements, but found " + ((DenseVector)biasTensor).size()); 111 } 112 113 if (!(featureLabelTensor instanceof DenseMatrix)) { 114 throw new IllegalArgumentException("Invalid protobuf, expected feature/label matrix, found " + featureLabelTensor.getClass().getSimpleName()); 115 } 116 DenseMatrix featureLabelMatrix = (DenseMatrix) featureLabelTensor; 117 if ((featureLabelMatrix.getDimension1Size() != numLabels) || (featureLabelMatrix.getDimension2Size() != numFeatures)) { 118 throw new IllegalArgumentException("Invalid protobuf, expected feature/label matrix of size [" + numLabels + ", " + numFeatures + "], found " + Arrays.toString(featureLabelMatrix.getShape())); 119 } 120 121 if (!(labelLabelTensor instanceof DenseMatrix)) { 122 throw new IllegalArgumentException("Invalid protobuf, expected label/label matrix, found " + labelLabelTensor.getClass().getSimpleName()); 123 } 124 DenseMatrix labelLabelMatrix = (DenseMatrix) labelLabelTensor; 125 if ((labelLabelMatrix.getDimension1Size() != numLabels) || (labelLabelMatrix.getDimension2Size() != numLabels)) { 126 throw new IllegalArgumentException("Invalid protobuf, expected label/label matrix of size [" + numLabels + ", " + numLabels + "], found " + Arrays.toString(labelLabelMatrix.getShape())); 127 } 128 129 return new CRFParameters((DenseVector) biasTensor, featureLabelMatrix, labelLabelMatrix); 130 } 131 132 @Override 133 public ParametersProto serialize() { 134 ParametersProto.Builder builder = ParametersProto.newBuilder(); 135 136 builder.setVersion(CURRENT_VERSION); 137 builder.setClassName(CRFParameters.class.getName()); 138 CRFParametersProto.Builder crfParamsBuilder = CRFParametersProto.newBuilder(); 139 crfParamsBuilder.setNumFeatures(numFeatures); 140 crfParamsBuilder.setNumLabels(numLabels); 141 crfParamsBuilder.setBiases(biases.serialize()); 142 crfParamsBuilder.setFeatureLabelWeights(featureLabelWeights.serialize()); 143 crfParamsBuilder.setLabelLabelWeights(labelLabelWeights.serialize()); 144 builder.setSerializedData(Any.pack(crfParamsBuilder.build())); 145 146 return builder.build(); 147 } 148 149 /** 150 * Gets a copy of the weights for the specified label id. 151 * @param id The label id. 152 * @return The feature weights. 153 */ 154 public DenseVector getFeatureWeights(int id) { 155 return featureLabelWeights.getColumn(id); 156 } 157 158 /** 159 * Returns the bias for the specified label id. 160 * @param id The label id. 161 * @return The bias. 162 */ 163 public double getBias(int id) { 164 return biases.get(id); 165 } 166 167 /** 168 * Returns the feature/label weight for the specified feature and label id. 169 * @param labelID The label id. 170 * @param featureID The feature id. 171 * @return The feature/label weight. 172 */ 173 public double getWeight(int labelID, int featureID) { 174 return featureLabelWeights.get(labelID, featureID); 175 } 176 177 /** 178 * Generate the local scores (i.e., the linear classifier for each token). 179 * @param features An array of {@link SGDVector}s, one per token. 180 * @return An array of DenseVectors representing the scores per label for each token. 181 */ 182 public DenseVector[] getLocalScores(SGDVector[] features) { 183 DenseVector[] localScores = new DenseVector[features.length]; 184 for (int i = 0; i < features.length; i++) { 185 DenseVector scores = featureLabelWeights.leftMultiply(features[i]); 186 scores.intersectAndAddInPlace(biases); 187 localScores[i] = scores; 188 } 189 return localScores; 190 } 191 192 /** 193 * Generates the local scores and tuples them with the label - label transition weights. 194 * @param features The per token {@link SGDVector} of features. 195 * @return A tuple containing the array of {@link DenseVector} scores and the label - label transition weights. 196 */ 197 public ChainHelper.ChainCliqueValues getCliqueValues(SGDVector[] features) { 198 DenseVector[] localScores = getLocalScores(features); 199 return new ChainHelper.ChainCliqueValues(localScores, labelLabelWeights); 200 } 201 202 /** 203 * Generate a prediction using Viterbi. 204 * @param features The per token {@link SGDVector} of features. 205 * @return An int array giving the predicted label per token. 206 */ 207 public int[] predict(SGDVector[] features) { 208 ChainHelper.ChainViterbiResults result = ChainHelper.viterbi(getCliqueValues(features)); 209 return result.mapValues; 210 } 211 212 /** 213 * Generate a prediction using Belief Propagation. 214 * @param features The per token {@link SGDVector} of features. 215 * @return A {@link DenseVector} per token containing the marginal distribution over labels. 216 */ 217 public DenseVector[] predictMarginals(SGDVector[] features) { 218 ChainHelper.ChainBPResults result = ChainHelper.beliefPropagation(getCliqueValues(features)); 219 DenseVector[] marginals = new DenseVector[features.length]; 220 for (int i = 0; i < features.length; i++) { 221 marginals[i] = result.alphas[i].add(result.betas[i]); 222 marginals[i].expNormalize(result.logZ); 223 } 224 return marginals; 225 } 226 227 /** 228 * This predicts per chunk confidence using the constrained forward backward algorithm from 229 * Culotta and McCallum 2004. 230 * <p> 231 * Runs one pass of BP to get the normalizing constant, and then a further chunks.size() passes 232 * of constrained forward backward. 233 * @param features The per token {@link SGDVector} of features. 234 * @param chunks A list of extracted chunks to pin constrain the labels to. 235 * @return A list containing the confidence value for each chunk. 236 */ 237 public List<Double> predictConfidenceUsingCBP(SGDVector[] features, List<Chunk> chunks) { 238 ChainHelper.ChainCliqueValues cliqueValues = getCliqueValues(features); 239 ChainHelper.ChainBPResults bpResult = ChainHelper.beliefPropagation(cliqueValues); 240 double bpLogZ = bpResult.logZ; 241 242 int[] constraints = new int[features.length]; 243 244 List<Double> output = new ArrayList<>(); 245 for (Chunk chunk : chunks) { 246 Arrays.fill(constraints,-1); 247 chunk.unpack(constraints); 248 double chunkScore = ChainHelper.constrainedBeliefPropagation(cliqueValues,constraints); 249 output.add(Math.exp(chunkScore - bpLogZ)); 250 } 251 252 return output; 253 } 254 255 /** 256 * Generates predictions based on the input features and labels, then scores those predictions to 257 * produce a loss for the example and a gradient update. 258 * <p> 259 * Assumes all the features in this example are either SparseVector or DenseVector. 260 * Mixing the two will cause undefined behaviour. 261 * @param features The per token {@link SGDVector} of features. 262 * @param labels The per token ground truth labels. 263 * @return A {@link Pair} containing the loss for this example and the associated gradient. 264 */ 265 public Pair<Double, Tensor[]> valueAndGradient(SGDVector[] features, int[] labels) { 266 ChainHelper.ChainCliqueValues scores = getCliqueValues(features); 267 // Infer the marginal distribution over labels for each token. 268 ChainHelper.ChainBPResults bpResults = ChainHelper.beliefPropagation(scores); 269 double logZ = bpResults.logZ; 270 DenseVector[] alphas = bpResults.alphas; 271 DenseVector[] betas = bpResults.betas; 272 273 //Calculate the gradients for the parameters. 274 Tensor[] gradient = new Tensor[3]; 275 DenseSparseMatrix[] featureGradients = new DenseSparseMatrix[features.length]; 276 DenseMatrix denseFeatureGradients = null; 277 boolean sparseFeatures = false; 278 gradient[0] = new DenseVector(biases.size()); 279 Matrix transGradient = new DenseMatrix(numLabels, numLabels); 280 gradient[2] = transGradient; 281 double score = -logZ; 282 for (int i = 0; i < features.length; i++) { 283 int curLabel = labels[i]; 284 // Increment the loss based on the score for the true label. 285 DenseVector curLocalScores = scores.localValues[i]; 286 score += curLocalScores.get(curLabel); 287 // Generate the predicted local marginal from the BP run. 288 DenseVector curAlpha = alphas[i]; 289 DenseVector curBeta = betas[i]; 290 DenseVector localMarginal = curAlpha.add(curBeta); 291 // Generate the gradient for the biases based on the true label and predicted label. 292 localMarginal.expNormalize(logZ); 293 localMarginal.scaleInPlace(-1.0); 294 localMarginal.add(curLabel,1.0); 295 gradient[0].intersectAndAddInPlace(localMarginal); 296 // Generate the gradient for the feature - label weights 297 Matrix tmpFeatureGradient = localMarginal.outer(features[i]); 298 if (tmpFeatureGradient instanceof DenseSparseMatrix) { 299 featureGradients[i] = (DenseSparseMatrix) tmpFeatureGradient; 300 sparseFeatures = true; 301 } else { 302 if (denseFeatureGradients == null) { 303 denseFeatureGradients = (DenseMatrix) tmpFeatureGradient; 304 } else { 305 denseFeatureGradients.intersectAndAddInPlace(tmpFeatureGradient); 306 } 307 } 308 // If the sequence has more than one token generate the gradient for the label - label transitions. 309 if (i >= 1) { 310 DenseVector prevAlpha = alphas[i - 1]; 311 for (int ii = 0; ii < numLabels; ii++) { 312 double prevAlphaVal = prevAlpha.get(ii); 313 for (int jj = 0; jj < numLabels; jj++) { 314 double update = -Math.exp(prevAlphaVal + labelLabelWeights.get(ii,jj) + curBeta.get(jj) + curLocalScores.get(jj) - logZ); 315 transGradient.add(ii, jj, update); 316 } 317 } 318 int prevLabel = labels[i-1]; 319 // Increment the loss based on the transition from the previous predicted label to the true label. 320 score += labelLabelWeights.get(prevLabel,curLabel); 321 transGradient.add(prevLabel, curLabel, 1.0); 322 } 323 } 324 325 if (sparseFeatures) { 326 // Merge together all the sparse feature - label gradients. 327 gradient[1] = merger.merge(featureGradients); 328 // throw if we found any dense features as well as the sparse. 329 if (denseFeatureGradients != null) { 330 throw new IllegalStateException("Mixture of dense and sparse features found."); 331 } 332 } else { 333 gradient[1] = denseFeatureGradients; 334 } 335 336 return new Pair<>(score,gradient); 337 } 338 339 /** 340 * Returns a 3 element {@link Tensor} array. 341 * 342 * The first element is a {@link DenseVector} of label biases. 343 * The second element is a {@link DenseMatrix} of feature-label weights. 344 * The third element is a {@link DenseMatrix} of label-label transition weights. 345 * @return A {@link Tensor} array. 346 */ 347 @Override 348 public Tensor[] getEmptyCopy() { 349 Tensor[] output = new Tensor[3]; 350 output[0] = new DenseVector(biases.size()); 351 output[1] = new DenseMatrix(featureLabelWeights.getDimension1Size(),featureLabelWeights.getDimension2Size()); 352 output[2] = new DenseMatrix(labelLabelWeights.getDimension1Size(),labelLabelWeights.getDimension2Size()); 353 return output; 354 } 355 356 @Override 357 public Tensor[] get() { 358 return weights; 359 } 360 361 @Override 362 public void set(Tensor[] newWeights) { 363 if (newWeights.length == weights.length) { 364 weights = newWeights; 365 biases = (DenseVector) weights[0]; 366 featureLabelWeights = (DenseMatrix) weights[1]; 367 labelLabelWeights = (DenseMatrix) weights[2]; 368 } 369 } 370 371 @Override 372 public void update(Tensor[] gradients) { 373 for (int i = 0; i < gradients.length; i++) { 374 weights[i].intersectAndAddInPlace(gradients[i]); 375 } 376 } 377 378 @Override 379 public Tensor[] merge(Tensor[][] gradients, int size) { 380 DenseVector biasUpdate = new DenseVector(biases.size()); 381 List<DenseSparseMatrix> updates = new ArrayList<>(size); 382 DenseMatrix denseUpdates = null; 383 DenseMatrix labelLabelUpdate = new DenseMatrix(labelLabelWeights.getDimension1Size(),labelLabelWeights.getDimension2Size()); 384 for (int j = 0; j < gradients.length; j++) { 385 biasUpdate.intersectAndAddInPlace(gradients[j][0]); 386 Matrix tmpUpdate = (Matrix) gradients[j][1]; 387 if (tmpUpdate instanceof DenseSparseMatrix) { 388 updates.add((DenseSparseMatrix)tmpUpdate); 389 } else { 390 // is dense 391 if (denseUpdates == null) { 392 denseUpdates = (DenseMatrix) tmpUpdate; 393 } else { 394 denseUpdates.intersectAndAddInPlace(tmpUpdate); 395 } 396 } 397 labelLabelUpdate.intersectAndAddInPlace(gradients[j][2]); 398 } 399 400 // Merge the combination of any dense and sparse updates 401 Matrix featureLabelUpdate; 402 if (updates.size() > 0) { 403 featureLabelUpdate = merger.merge(updates.toArray(new DenseSparseMatrix[0])); 404 if (denseUpdates != null) { 405 denseUpdates.intersectAndAddInPlace(featureLabelUpdate); 406 featureLabelUpdate = denseUpdates; 407 } 408 } else { 409 featureLabelUpdate = denseUpdates; 410 } 411 412 return new Tensor[]{biasUpdate,featureLabelUpdate,labelLabelUpdate}; 413 } 414}