skweak: Software toolkit for weak supervision in NLP

repository·main·Indexed 21 days ago

https://github.com/norskregnesentral/skweak

A framework for weak supervision in NLP designed to build high-quality NER models without manually labeled data. It aggregates multiple noisy supervision sources—such as spaCy models, gazetteers, and custom heuristic functions—using Hidden Markov Models (HMM) or majority voting to produce unified labels. The toolkit includes specialized annotators for token and span constraints, document-level consistency, and utilities for exporting aggregated annotations to spaCy DocBin format.

Tokens
16.4K
Snippets
54
Records
63
Agent score
73%

What's inside skweak

  1. Install skweak via pip

    main

    You can install the core skweak library using pip. If you want to install directly from the GitHub repository, use the git URL. Note that some examples and tests may require trained spaCy pipelines, which can be downloaded separately.

    # Install core library
    pip install skweak
    
    # Install from repo
    pip install --user git+https://github.com/NorskRegnesentral/skweak
    
    # Download a spaCy model if needed
    python -m spacy download en_core_web_sm
  2. Aggregate labels using an HMM model

    main

    After applying multiple labelling functions, use an aggregation model to resolve conflicts and produce a single set of annotations.

    skweak.aggregation.HMM (Hidden Markov Model) can be used to estimate a generative model. You can define 'underspecified labels'—labels that represent a group of more specific labels (e.g., an ORG label that could be either a COMPANY or an OTHER_ORG).

    Use .fit_and_aggregate(docs) to train the model on your annotated corpus and update the Doc objects with the aggregated results.

    import skweak
    
    # Define the model with the specific labels you want to predict
    model = skweak.aggregation.HMM("hmm", ["COMPANY", "OTHER_ORG"])
    
    # Map an underspecified label to a set of specific labels
    model.add_underspecified_label("ORG", ["COMPANY", "OTHER_ORG"])
    
    # Fit the model and update the docs
    docs = model.fit_and_aggregate(docs)
  3. How AbstractAnnotator works

    main

    The AbstractAnnotator is the base class for all annotation or aggregation sources in skweak.

    Core Interface:

    • __init__(name: str): Every annotator must have a unique name used to store its results in doc.spans[name].
    • __call__(doc: Doc) -> Doc: The primary method to annotate a single spaCy Doc.
    • pipe(docs: Iterable[Doc]) -> Iterable[Doc]: The preferred method for processing large batches of documents. The default implementation iterates through the stream and calls __call__ on each document.
  4. How AbstractAggregator works

    main

    An AbstractAggregator is an AbstractAnnotator that combines multiple weak supervision sources (labeling functions) into a single prediction.

    Lifecycle and Workflow:

    1. Initialization: You provide a name and a list of labels (the target output labels).
    2. Fitting (Optional): If the aggregator is parametric (like HMM), call .fit(docs) to learn parameters from a collection of documents. Use .fit_and_aggregate(docs) to perform both steps in one go.
    3. Aggregation (Inference): When the aggregator is called on a Doc (via __call__), it:
      • Identifies valid sources in doc.spans (filtering out those marked aggregated=True or avoid_in_aggregation=True).
      • Extracts observations into a dataframe via get_observation_df.
      • Runs the aggregate logic.
      • Converts results into Spans and attaches them to doc.spans[self.name].
      • Attaches metadata to the spans: probs (the full probability distribution), aggregated (set to True), and sources (the list of contributing sources).

    Label Groups: You can use add_label_group(coarse_label, sub_labels) to handle underspecified labels. For example, if a labeling function returns ENT, you can map it to [PER, ORG] so the aggregator treats a vote for ENT as a partial vote for its constituents.

  5. Use GenerativeModelMixin for custom generative aggregators

    main

    The GenerativeModelMixin is a base class for implementing aggregation methods based on generative models (where states are latent 'true' labels and observations are predictions from labeling sources).

    Note: This class should not be instantiated directly; it is intended to be subclassed (e.g., by NaiveBayes or HMM).

    Key features include:

    • Weighting: Supports initial_weights (a dictionary mapping source names to weights in range [0, +inf)) and a redundancy_factor to penalize correlated labeling functions.
    • Aggregation: The aggregate(obs) method takes a 2D DataFrame of observations and returns a 2D DataFrame of label probabilities.
    • Training: The _fit(all_obs, ...) method implements the Expectation-Maximization (EM) algorithm to learn model parameters from a collection of observations.
    class MyCustomAggregator(GenerativeModelMixin, SomeOtherMixin):
        def get_posteriors(self, X: Dict[str, np.ndarray]) -> np.ndarray:
            # Implement posterior inference logic
            pass
    
        def _reset_counts(self, sources):
            # Implement parameter reset logic
            pass
    
        def _add_mv_counts(self, all_obs):
            # Implement initial count logic from majority voter
            pass
    
        def _accumulate_statistics(self, X: Dict[str, np.ndarray]) -> float:
            # Implement E-step statistics accumulation
            pass
    
        def _do_mstep_latent(self):
            # Implement M-step for latent parameters
            pass
  6. Use SequenceAggregatorMixin for token-level sequence labelling

    main

    When performing token-level sequence labeling (e.g., NER with BIO/BILOU schemes), use a class that inherits from SequenceAggregatorMixin.

    Key Features:

    • Tagging Schemes: Supports IO, BIO, BILUO, and BILOU prefixes. You specify this via the prefixes argument during initialization.
    • Label Prefixing: Automatically handles the expansion of your base labels into prefixed versions (e.g., if labels are [ORG] and prefixes are BIO, it manages B-ORG, I-ORG, and O).
    • Observation Extraction: Uses utils.spans_to_array to convert the document's spans into a token-level array for the aggregator.
  7. Use TextAggregatorMixin for span classification

    main

    When performing span-level classification (where you predict labels for specific text segments), use a class that inherits from TextAggregatorMixin. This mixin provides a standard implementation for get_observation_df, _get_spans, and _get_probs based on unique spans found in the document.

    It expects the input Doc to have spans from various sources that share identical boundaries.

  8. Use MultilabelAggregatorMixin for multi-label tasks

    main

    For tasks where multiple labels can be true for the same span or token, use MultilabelAggregatorMixin.

    How it works: It decomposes the multi-label problem into multiple single-label sub-problems. For each target label, it creates a dedicated sub-model that treats the task as a binary choice: [label] vs [NOT/label] (or [prefix-label] vs [prefix-O] for sequence labeling).

    Key Methods:

    • set_exclusive_labels(exclusive_labels: Set[str]): Defines a set of labels that cannot co-occur. The aggregator will ensure that probabilities for these labels are normalized so they do not exceed 1.0 when combined with the 'null' label.
  9. Export aggregated annotations for spaCy training

    main

    Once you have aggregated your labels, you can convert the skweak annotations back into standard spaCy DocBin format to train a final production model.

    1. Map the aggregated spans (e.g., from the hmm key) to the standard doc.ents attribute.
    2. Use skweak.utils.docbin_writer(docs, path) to save the data.
    # Transfer aggregated spans to standard spaCy entities
    for doc in docs:
        doc.ents = doc.spans["hmm"]
    
    # Save to a spaCy DocBin file
    skweak.utils.docbin_writer(docs, "../data/reuters_small.spacy")
  10. Workflow for weak supervision in NLP

    main

    A typical workflow for using skweak for sentiment analysis or NER involves these steps:

    1. Data Preparation: Convert your raw data (e.g., CSV/TSV) into spaCy DocBin objects. Store ground truth labels in doc.user_data["gold"] if available.
    2. Weak Labeling: Create or use annotators (e.g., DocBOWAnnotator, BERT-based models) to generate predictions. These predictions are saved into new DocBins.
    3. Aggregation: Use skweak.aggregation (like HMM or MajorityVoter) to combine the predictions from all weak labelers into a single set of labels.
    4. Evaluation: Compare the aggregated predictions against the gold labels using standard metrics like f1_score.
  11. Configure initial weights for voting aggregators

    main

    All majority voting aggregators accept an initial_weights argument.

    • Type: Optional[Dict[str, float]]
    • Behavior: A dictionary mapping source names (the column names in your input DataFrame) to numerical weights in the range [0, +inf).
    • Default: If None, all sources are assumed to have a weight of 1.
    • Disabling Sources: To ignore a specific labelling source, set its weight to 0 in the dictionary.
  12. How weak supervision works in skweak

    main

    The skweak workflow follows a four-step process to generate labels from unlabelled text:

    1. Data Preparation: Convert your raw text into SpaCy Doc objects.
    2. Labeling (Step 1): Define multiple labelling functions (LFs) that take Doc objects and annotate spans with labels. LFs can be heuristics, gazetteers, or machine learning models.
    3. Aggregation (Step 2): Apply the LFs to your corpus. Use a generative model (like an HMM) to aggregate the potentially conflicting results from all LFs into a single, probabilistic annotation layer. The model estimates the accuracy and confusion of each LF.
    4. Model Training (Step 3): Use the aggregated labels as a