---
title: "Decode CTC output in C#"
url: https://recursiveai.org/onemodel/cookbook/decode-ctc-output/
description: "Decode CTC scores in C# with greedy and prefix-beam search. Handle repeated labels and blanks, choose logits or log probabilities, and interpret confidence."
version: "1.3.0"
---

# Decode CTC output in C#

**OneModel** is a free, open-source neural-network runtime for .NET from Recursive AI that runs ONNX models and trains models. MIT or Apache-2.0, no native dependencies.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneModel) · [GitHub](https://github.com/recursive-ai-oss/onemodel-csharp) · [Docs](https://recursiveai.org/onemodel/docs/) · [Download example (.zip)](https://recursiveai.org/onemodel/cookbook/decode-ctc-output/decode-ctc-output.zip)

CTC decoding merges repeated labels and removes blanks from per-step class scores.

```shell
dotnet add package RecursiveAI.OneModel
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using RecursiveAI.OneModel.Tensors;

// Three steps; blank=0, label=1.
float[] scores = [0, 4, 4, 0, 0, 4];
var greedy = Ctc.Greedy(scores, steps: 3, classes: 2);
var beam = Ctc.BeamSearch(scores, steps: 3, classes: 2);
Console.WriteLine(
    string.Join(",", greedy.Labels()));
Console.WriteLine(
    string.Join(",", beam[0].Labels));
```

```text
1,1
1,1
```

Decodes label, blank, label into two copies of label 1, using logits and blank index 0. `greedy.Labels()` is a method that returns a copy of the label indices; `beam[0].Labels` is the first hypothesis’s array property.

Unzip [the example](https://recursiveai.org/onemodel/cookbook/decode-ctc-output/decode-ctc-output.zip), then `dotnet run`.

## The full program

The download builds a row-major `[6, 3]` score array directly in `Program.cs`. Its columns are blank, A and B, with class indices 0, 1 and 2. Its best path is A, A, blank, A, B, B. The blank separates the two A runs; the consecutive B steps become one label.

```csharp
using RecursiveAI.OneModel.Tensors;
using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
// Rows: blank, A, B. Best path: A A blank A B B.
float[] scores = [0, 4, 0, 0, 3, 0, 4, 0, 0,
                  0, 4, 0, 0, 0, 4, 0, 0, 3];
var greedy = Ctc.Greedy(scores, steps: 6, classes: 3);
Console.WriteLine($"Greedy: {string.Join(",", greedy.Labels())}");
Console.WriteLine($"Greedy confidence: {greedy.Confidence:F6}");
foreach (var run in greedy.Runs)
    Console.WriteLine($"Label {run.Label}: steps {run.FirstStep}-{run.LastStep}, confidence {run.Confidence:F6}");
var beam = Ctc.BeamSearch(scores, steps: 6, classes: 3,
    beamWidth: 8, topN: 2, blank: 0, kind: CtcScores.Logits);
foreach (var hypothesis in beam)
    Console.WriteLine($"Beam: {string.Join(",", hypothesis.Labels)}, logP {hypothesis.LogProbability:F6}, confidence {hypothesis.Confidence:F6}");
```

```text
Greedy: 1,1,2
Greedy confidence: 0.964663
Label 1: steps 0-1, confidence 0.964663
Label 1: steps 3-3, confidence 0.964663
Label 2: steps 4-5, confidence 0.964663
Beam: 1,1,2, logP -0.183798, confidence 0.940573
Beam: 1,2,1,2, logP -2.888170, confidence 0.485759
```

Invalid dimensions or blank indices throw argument exceptions; beam search also rejects NaN scores rather than producing a hypothesis from them.

## Variations

**Supply natural-log probabilities.** This complete snippet has two rows whose probabilities are each 0.2 for blank and 0.8 for label 1:

```csharp
using RecursiveAI.OneModel.Tensors;

float[] logProbabilities = [
    MathF.Log(0.2f), MathF.Log(0.8f),
    MathF.Log(0.2f), MathF.Log(0.8f)];
var result = Ctc.Greedy(logProbabilities, 2, 2,
    blank: 0, kind: CtcScores.LogProbabilities);
Console.WriteLine(string.Join(",", result.Labels()));
```

```text
1
```

**Use a different blank index.** Here class 1 is blank, so class 0 survives on both sides of it:

```csharp
using RecursiveAI.OneModel.Tensors;

float[] scores = [4, 0, 0, 4, 4, 0];
var result = Ctc.Greedy(scores, 3, 2, blank: 1);
Console.WriteLine(string.Join(",", result.Labels()));
```

```text
0,0
```

**Decode an ONNX output tensor.** The zip includes `ctc-identity.onnx`, a tiny ONNX graph with one `Identity` node: input `scores` → output `logits`, both float tensors of shape `[6, 3]`. It passes the supplied scores through unchanged; it is not a trained recognizer. This complete snippet runs the model, copies its output into a flat `float[]`, and maps decoded indices through an alphabet:

```csharp
using RecursiveAI.OneModel.Graph;
using RecursiveAI.OneModel.Tensors;

float[] scores = [0, 4, 0, 0, 3, 0, 4, 0, 0,
                  0, 4, 0, 0, 0, 4, 0, 0, 3];
string[] alphabet = ["", "A", "B"];
using var session = Session.Load("ctc-identity.onnx");
using var input = Tensor.FromSpan<float>(scores, 6, 3);
using var outputs = session.Run(
    new Dictionary<string, Tensor> { ["scores"] = input });
float[] logits = outputs["logits"].ToArray<float>();
var decoded = Ctc.Greedy(logits, steps: 6, classes: 3);
Console.WriteLine(string.Concat(
    decoded.Labels().Select(i => alphabet[i])));
```

```text
AAB
```

For a trained CTC model, use its actual input preprocessing, output name, blank index and alphabet. This snippet assumes a single sequence laid out as `[steps, classes]`; reorder or slice a different output layout before decoding.

## Related recipes

- [Run an ONNX model](https://recursiveai.org/onemodel/cookbook/run-onnx-model/)
- [Read named model inputs and outputs](https://recursiveai.org/onemodel/cookbook/named-inputs-and-outputs/)

---

## The short answer

Use `Ctc.Greedy` for the best per-step path or `Ctc.BeamSearch` for prefix-beam decoding. Both take a flat, row-major score array with explicit step and class counts. The defaults are logits and blank class 0. The result is a sequence of class indices; map those indices through the alphabet or token table belonging to your model.

The full program deliberately uses hand-built scores so you can see the repeat and blank rules without downloading a model. It also prints the two different meanings of confidence. The [CTC API guide](https://recursiveai.org/onemodel/docs/ctc/) defines both algorithms and their score conventions.

## What is CTC in deep learning?

Connectionist temporal classification is used for sequence problems where the input steps do not come with a fixed label-by-label alignment. Speech and handwriting recognition are common examples; [this visual explanation of CTC](https://distill.pub/2017/ctc/) shows how alignments collapse into output sequences.

For decoding, you start with a score for every class at every step. Blank is a real output class in that table, but it is not emitted as a label. Greedy decoding first merges consecutive equal classes into runs, then drops blank runs. Order matters: removing blanks before merging would lose repeated characters that the model separated deliberately.

In the full example, A A blank A B B becomes A A B. The first A occupies steps 0–1, the second occupies step 3, and B occupies steps 4–5. These run bounds are step indices, not audio timestamps. You need the model’s own step-to-input relationship to interpret them as positions in an image or recording.

## How does the CTC decoding algorithm choose greedy or beam-search output?

Greedy decoding chooses the largest score at each step, using the lowest class index on ties. It then collapses that one path. Prefix beam search instead keeps candidate label prefixes and combines probabilities from different alignments that reach the same prefix. A high-probability text can therefore differ from the text obtained by collapsing the single best path.

OneModel’s search has no language model. `beamWidth` controls how many prefixes survive each step, while `topN` controls how many final hypotheses you request. A finite beam prunes candidates; it is not a promise to find the globally most likely text. The full program asks for two hypotheses with beam width 8 and prints their log probabilities as well as their labels.

Use logits when your scores have not been normalized. OneModel takes a softmax where probabilities are needed. Use `CtcScores.LogProbabilities` only when each row already contains natural-log probabilities normalized over that row’s classes. Ordinary probabilities are neither logits nor log probabilities; passing them under the wrong score kind changes the confidence calculation.

Greedy run confidence is the largest chosen-class probability within that run. Its sequence confidence is the arithmetic mean of those run confidences. Beam confidence is different: for a nonempty hypothesis, it is `exp(LogProbability / labelCount)`, a per-label geometric mean based on the alignments the search retained. Both return zero confidence for empty output. Do not compare their printed confidence values as if they measured the same quantity.

Beam width must be between 1 and 4096, and the search’s prefix bound is `steps * beamWidth * (classes - 1) + 1 <= 2^24`, as specified in the [pinned CTC documentation](https://github.com/recursive-ai-oss/onemodel-csharp/blob/f453bf255cf2de843f4de083754d09356d53962a/docs/ctc.md). Choose a valid width for your actual sequence size, then evaluate decoded labels on representative model output rather than treating a larger beam as an automatic correction for poor scores.
