Metadata-Version: 2.4
Name: unisson
Version: 0.1.1
Summary: Fast consensus decoding and Minimum Bayes Risk selection over multiple hypotheses
Author: Amine Sehili
License-Expression: MIT
Project-URL: Homepage, https://github.com/amsehili/unisson
Keywords: asr,machine translation,ocr,consensus,rover,mbr,alignment
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Cython
Classifier: Operating System :: OS Independent
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Text Processing :: Linguistic
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Dynamic: license-file

# unisson

Fast combination of multiple hypotheses (ASR transcripts, MT translations, sampled LLM
outputs) into a single output, by **alignment + majority vote** or **Minimum Bayes Risk
selection**.

```python
>>> from unisson import consensus, mbr
>>> hyps = ["the cat is on the mat", "a bat sat on the mat", "the cat sat on a hat"]
>>> consensus(hyps)    # word-level vote over the aligned hypotheses
'the cat sat on the mat'
>>> mbr(hyps, "wer")   # the hypothesis with the lowest expected WER to the others
'the cat is on the mat'
```

## Features

- **Consensus decoding**: progressive multiple alignment of N hypotheses into columns,
  then the most voted word of every column (ROVER-style).
- **Substitution costs**: unit cost, word-embedding cosine distance (binary or soft), or
  any word-level cost function.
- **MBR selection** with `"wer"`, `"cer"`, `"edit"`, `"cosine"` (sentence embeddings), or
  any metric you provide; optional per-hypothesis weights.
- **Fast**: compiled core (Cython) with a bit-parallel dynamic programming kernel; batch
  APIs for whole corpora. A pure-Python implementation with identical results is used when
  the extension is not available.

## Installation

```bash
pip install git+https://github.com/amsehili/unisson.git
# or, from a clone
pip install .
```

Requires Python 3.9+ and a C compiler to build. Runtime dependency: NumPy.

## Consensus

```python
from unisson import consensus, consensus_batch

consensus(hyps)                          # whitespace tokenization
consensus(hyps, tokenize=my_tokenizer)   # any str -> list[str] function
consensus([["the", "cat"], ["a", "cat"]])  # pre-tokenized input

# one call for a corpus: one group of hypotheses per sentence
outputs = consensus_batch(groups)
```

Hypotheses are aligned progressively: the first two, then each further hypothesis against
the columns built so far. A token matches a column when it is one of the column's words.
The output joins the most voted word of each column and drops columns where the gap
token `"<eps>"` wins.

### Substitution costs

By default every substitution costs 1. Similar words can be made cheaper:

```python
from unisson import EmbeddingCost, PairwiseCost

# embed: word -> vector, from any embedding model
cost = EmbeddingCost(embed, threshold=0.3)             # binary: 0 or 1
cost = EmbeddingCost(embed, threshold=0.3, soft=True)  # real-valued
consensus_batch(groups, sub_cost=cost)

consensus(hyps, sub_cost=PairwiseCost(lambda a, b: 0 if a.lower() == b.lower() else 1))
```

With `dist(a, b) = (1 - cos(a, b)) / 2`, `EmbeddingCost` gives cost 0 when
`dist <= threshold` and 1 otherwise; with `soft=True` the cost is `dist` itself below the
threshold. A column's cost against a token is the minimum over the column's words. Any
`sub_cost(word, token)` callable, or an object with a `.matrix(words)` method returning a
`(V, V)` array, is accepted.

### Alignment output

```python
from unisson import align_matrix, align_columns, align_two

align_matrix(hyps)   # one row per hypothesis, equal lengths, "<eps>" for gaps
align_columns(hyps)  # list of Counter: word -> votes, per column
align_two(a, b)      # pairwise alignment
```

## Minimum Bayes Risk

```python
from unisson import mbr, mbr_batch, mbr_scores

mbr(hyps, "wer")                    # also "cer", "edit"
mbr(hyps, "cosine", embed=SentenceTransformer("all-MiniLM-L6-v2").encode)
mbr(hyps, lambda c, r: sacrebleu.sentence_bleu(c, [r]).score, higher_is_better=True)
mbr(hyps, "wer", weights=probabilities)
mbr(hyps, "wer", return_index=True)  # (hypothesis, index)

mbr_batch(groups, "cosine", embed=encode)   # all texts embedded in one call
mbr_scores(hyps, "wer")                     # expected metric of every hypothesis
```

Each hypothesis is scored against all other hypotheses as pseudo-references:

```
score(h_i) = sum_{j != i} w_j * metric(h_i, h_j) / sum_{j != i} w_j
```

The lowest score wins for distances and the highest for similarities; ties go to the
earliest hypothesis. Duplicate hypotheses are scored once. `"wer"` and `"cer"` against an
empty pseudo-reference are 1 (0 if the candidate is empty too), so an empty hypothesis
does not win over non-empty ones that agree with each other.

| metric | definition | direction |
|---|---|---|
| `"wer"` | word edit distance / pseudo-reference length | lower is better |
| `"cer"` | character edit distance / pseudo-reference length | lower is better |
| `"edit"` | word edit distance (unnormalized, symmetric) | lower is better |
| `"cosine"` | cosine similarity of `embed(texts)` vectors | higher is better |
| callable | `metric(candidate, pseudo_reference)`; set `higher_is_better` | user-defined |

Pass `symmetric=True` with a symmetric callable metric to evaluate each pair once. The
edit-distance metrics are computed entirely in compiled code.

## Algorithm notes

- **Order matters**: progressive alignment depends on the order of the hypotheses; put
  the most reliable hypothesis first.
- **Tie-breaking**: the traceback prefers a match, then a gap in the new hypothesis, then a
  new column, then a substitution. In a column, vote ties go to the word added first.
- **Gap votes**: when a hypothesis opens a new column, the gap gets one vote by default.
  `exact_gap_counts=True` gives it one vote per earlier hypothesis instead.
- **Speed**: with unit or binary substitution costs the alignment DP is bit-parallel
  (Myers/Hyyrö, 64 columns per machine word); other costs use a scalar DP. Both give
  identical alignments.

## Performance

```bash
python bench.py
```

Reports the run time of the compiled implementation and its speedup over the pure-Python
implementation on synthetic data. Typical speedups range from about 60x (consensus) to
several hundred times (MBR, long sequences).

## Development

```bash
pip install -e ".[test]"
python -m pytest tests
```

The tests compare the compiled implementation with an independent object-based
implementation and with the pure-Python one on randomized inputs.

## License

MIT
