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}