File size: 4,004 Bytes
1ad8795
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
---
language:
- multilingual
library_name: transformers
pipeline_tag: text-classification
base_model: MoritzLaurer/multilingual-MiniLMv2-L6-mnli-xnli
tags:
- document-ai
- ocr
- cross-page
- table
- text-classification
---

# BERT for OCR cross-page continuity — EP5

This is a binary XLM-RoBERTa/MiniLM classifier fine-tuned to decide whether two
adjacent OCR fragments should be merged across a page boundary.

## Labels

- `0`: `not_continuous` — do not merge
- `1`: `continuous` — merge

The model output is converted to `P(continuous)` with softmax. For conservative
merging, use `P(continuous) > 0.8`, equivalent to `score = 2P - 1 > 0.6`.

## Training configuration

- Base model: `MoritzLaurer/multilingual-MiniLMv2-L6-mnli-xnli`
- Epoch checkpoint: 5
- Training/validation pairs: 18,049 / 2,005
- Learning rate: `2e-5`
- Batch size: `8`
- Maximum sequence length: `512` tokens
- Boundary character window: previous tail `800`, next head `800`
- Random seed: `42`
- HTML table markup was preserved during training and validation.

Long pairs use boundary-aware truncation: retain the tail tokens of the previous
fragment and the head tokens of the next fragment. With two sequences and a
512-token limit, the normal allocation is 254 tokens from each side; unused
space from one side is transferred to the other.

## Validation results

The values below are measured on the fixed 2,005-pair validation split. This is
not an independent test set.

At the default probability threshold of 0.5:

| Accuracy | Precision | Recall | F1 | TN | FP | FN | TP |
|---:|---:|---:|---:|---:|---:|---:|---:|
| 0.894 | 0.878 | 0.900 | 0.889 | 941 | 118 | 95 | 851 |

At the recommended conservative threshold of 0.8:

| Group | N | Precision | Recall | F0.5 | FPR | TN | FP | FN | TP |
|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
| Overall | 2,005 | 0.943 | 0.809 | 0.913 | 4.34% | 1,013 | 46 | 181 | 765 |
| Table | 1,808 | 0.946 | 0.809 | 0.915 | 4.63% | 865 | 42 | 172 | 729 |
| Text / OCRFlux bench | 197 | 0.900 | 0.800 | 0.878 | 2.63% | 148 | 4 | 9 | 36 |

The OCRFlux bench source group may include some table-like page elements and
should not be interpreted as a perfectly pure prose-only subset.

## Inference

Use the fine-tuned model as a two-class sequence-pair classifier and reproduce
the boundary-aware truncation described above. The merge probability is:

```python
import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer

model_id = "lemoncoda/bertforocr-continuity-ep5"
tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=True)
model = AutoModelForSequenceClassification.from_pretrained(model_id).eval()


def encode_boundary_pair(previous: str, following: str, max_length: int = 512):
    previous_ids = tokenizer.encode(previous, add_special_tokens=False)
    following_ids = tokenizer.encode(following, add_special_tokens=False)
    budget = max_length - tokenizer.num_special_tokens_to_add(pair=True)
    previous_budget = budget // 2
    following_budget = budget - previous_budget

    if len(previous_ids) < previous_budget:
        following_budget += previous_budget - len(previous_ids)
    if len(following_ids) < following_budget:
        previous_budget += following_budget - len(following_ids)

    previous_ids = previous_ids[-previous_budget:]
    following_ids = following_ids[:following_budget]
    return tokenizer.prepare_for_model(
        previous_ids,
        following_ids,
        add_special_tokens=True,
        max_length=max_length,
        truncation="longest_first",
        return_tensors="pt",
    )


encoded = encode_boundary_pair(previous_fragment, next_fragment)
with torch.inference_mode():
    probability = torch.softmax(model(**encoded).logits, dim=-1)[0, 1].item()

should_merge = probability > 0.8
score = 2 * probability - 1
```

For exact project integration, preserve the original HTML markup in table
fragments and apply the 800-character previous-tail/next-head windows before
tokenization.