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}