Sangbum Daniel Choi

Writing

Why an OCR model drops rows that repeat: 14 attention masks

When every row of a table is the same, PaddleOCR-VL-1.5 stops a row early in 39 of 40 tables. I hid parts of its attention 14 different ways to find out why. One mask brought back about half the missing rows without needing row positions; three others only looked like they helped.

OCR models that turn table images into text have an odd habit. Give one a table where the same row repeats, and it writes one row too few. Think of a transaction list with the same date and amount on eight lines. Change a single value per row and it reads every row fine.

I looked into why PaddleOCR-VL-1.5 (a 0.9B document-parsing model) does this. The method is attention masking. When the model writes a token, I block part of what it can look at, then see how the output changes. The weights stay the same; the masks apply only at inference.

Every table in these experiments is synthetic, and the values are made up.

The symptom: identical-row tables come out a row short

The model writes tables as OTSL-style tokens:

  • <fcel>: a cell with text
  • <ecel>: an empty cell
  • <lcel>, <ucel>: a cell merged with the one to its left or above
  • <nl>: the end of a row

Writing EOS (the end-of-output token) after the last <nl> ends the table.

I made two kinds of 8-row table, 40 of each. In one kind every row is identical. In the other every row is different.

TableWrote all 8 rowsRows missingRows written
Every row different40 (100%)08 rows in all 40
Every row identical1 (2.5%)39 (97.5%)7 rows in 35, 6 in 4, 8 in 1

On identical-row tables the model usually stops after 7 rows. The text it writes is right; it’s the row count that’s short.

Its gaze runs ahead of what it writes

I measured which image row the model looks at while it writes each row.

  • How: the correct output goes in as history (teacher forcing), and attention is averaged over all 18 layers.
  • Size: 60 table pairs.

Switch the table type and the moment in the figure below.

Each row's image attention is scaled to 100%. The last image row always gets a little attention, even on tables with unique rows.

On unique-row tables the pattern is a diagonal. While writing row 3, the model looks at image row 3 most.

On identical-row tables the gaze gets ahead from row 4 on.

  • While writing row 4, it looks at image row 5 most.
  • While writing row 6, it’s already on row 8, the last one.
  • The row-end <nl> shows the same thing. At the <nl> that ends row 5, the most-watched row is already row 8. It should be row 6.

The reason is plausible. In an identical-row table, rows 4 and 5 look exactly the same in the image. So it’s easy to lose track of where you are. The gaze reaches the last row early, so the model decides it’s done and stops.

The probability of stopping, P(EOS), fits this picture:

TableAfter row 6After row 7After row 8 (the real end)
Every row different0.0%0.0%99.9%
Every row identical19.8%88.6%77.9%

On identical-row tables the model stops after row 7 with 88.6% probability.

Attention only points at a cause, though. It doesn’t prove one. So the next step was to apply masks.

Fourteen masks

There are two kinds of mask:

  • Masks on every output token.
  • Masks only on the row-end <nl> token. These touch only the moment the model decides whether to stop.

I added two controls that hide the same number of positions at random. They separate the effect of what’s hidden from the effect of how much is hidden.

This time the model wrote the whole output on its own. I used 40 pairs of 8-row tables.

  • Exact: the output matches the answer token for token.
  • Text similarity: a character-level match that includes the cell separators.
MaskIdentical rows: row count rightShortOverExactText similarityUnique rows: exactText similarity
No mask2.597.502.586.397.599.8
Only the current row (needs row positions)100008099.18599.3
<nl> can’t see earlier <nl>4552.52.54592.897.599.8
Hide rows already read01000074.19099.8
At <nl>, hide rows already read01000082.297.599.8
At <nl>, see only the next row01000049.897.599.8
Hide rows not yet read07525035.4033.8
At <nl>, hide rows not yet read01000048.72.569.7
At <nl>, hide the image5950034.2061.5
<nl> can’t see earlier text097.52.5056.7066.7
<nl> can’t see any earlier output00100075.4084.0
Control: <nl> loses random earlier output00100075.5084.0
Hide earlier rows’ output00100076.4049.2
Control: at <nl>, hide random image tokens01000078.227.591.0
Hide the whole image0955023.0022.9

All values are percentages of 40 tables, so a gap of a few points can be chance.

The figure below shows P(EOS) right after each row, per mask. The gray line is the identical-row table with nothing hidden. Switch masks and compare the points after rows 7 and 8.

Average over 60 identical-row table pairs, measured with the correct output as history. A good line stays low through row 7 and jumps at row 8.

Pinning the gaze to the current row fixes every table

“Only the current row” gets the row count right on all 40 identical-row tables. Exact matches go from 2.5% to 80%. Here’s what it does:

  • While the model writes a row’s cells, it can see only that row of the image.
  • At <nl>, it can see only the next row.

P(EOS) is 0.1% after row 7 and 99.6% after row 8. When the gaze can’t run ahead, the symptom goes away. That fits the “it loses its place” explanation.

But this mask is a diagnostic, not a fix.

  • It needs the answer. You have to tell it where each row is in the image.
  • It hurts other tables. On unique-row tables, exact matches drop from 97.5% to 85%.
  • It’s worse on realistic tables. On a separate, more realistic set, unique-row tables dropped from 72% to 28%.

Hiding earlier <nl> tokens from <nl> brings back about half

Of the masks that don’t need row positions, this was the only one that helped. It does one thing: the row-end <nl> token can’t see the <nl> tokens written before it.

  • Exact matches on identical-row tables go from 2.5% to 45%.
  • Unique-row tables are unchanged: row count 100%, exact 97.5%.
  • P(EOS) after row 7 falls from 88.6% to 46.4%.

I haven’t confirmed why it works. My guess: earlier <nl> tokens may carry a running sense of which row the model is on. On identical-row tables that sense has already drifted a row or two ahead. A new <nl> that reads it inherits the error. Hiding the earlier <nl> tokens cuts that chain. Testing this needs its own experiment.

I also tried one synthetic table of each of a few other shapes. It’s one table per shape, so don’t read these as rates.

Table shapeRowsNo mask<nl> can’t see earlier <nl>Only the current row
Header + 8 identical rows98 rows (1 short)exactexact
Merged first column + identical rows98 rows (1 short)exact9 rows, one cell wrong
Only the middle 4 rows identical87 rows (1 short)8 rows, one merge token differsexact
Repayment schedule9exactexactexact
Every row different10exactexactexact

Three masks that only looked like fixes

When <nl> can’t see any earlier output, the “short” rate on identical-row tables drops to 0%. The same is true for the random control and for “hide earlier rows’ output”. If you count only short outputs, it looks solved.

But all three run over on all 40 tables. Most never stop and get cut off at the length limit. With every earlier output hidden from <nl>, P(EOS) never goes above 0.1% after any row. If the model can’t see what it already wrote, it can’t tell that it’s done.

So in experiments like this, always count overshoot along with undershoot.

Fixing only the stop decision makes it worse

When <nl> can see only the next image row, identical-row tables stop even earlier.

  • The model writes 3 to 5 rows, and text similarity falls from 86.3 to 49.8.
  • P(EOS) is already 35% after row 3.

The next row looks exactly like the one just written, so the model seems to treat it as already done.

The other masks tell us a few more things:

  • Hiding rows already read changes nothing. So confusing a row with the previous one isn’t the cause.
  • Hiding rows not yet read makes it stop earlier. The model keeps writing only while it can see rows left.
  • Hiding the whole image makes it invent tables. It fills cells with the numbers 1 to 99, or repeats one word over and over.

What this experiment can’t tell you

  • One model, synthetic tables only. I didn’t measure dropped rows on real documents. Real tables that lose rows without any repetition aren’t explained by this.
  • The samples are small. Rates come from 40 pairs, attention from 60 pairs, and the shape examples are one table each.
  • The attention runs and the full runs use different conditions. Attention and P(EOS) were measured with the correct output as history. They may differ when the model writes on its own.
  • It isn’t ready for serving. To use “<nl> can’t see earlier <nl>”, the inference server (vLLM, for example) has to support a different attention mask per token.

Takeaways

  1. When rows repeat, the model loses its place. Its gaze runs a row or two ahead, reaches the last row early, and it stops.
  2. Pinning the gaze to the current row removes the symptom. It needs row positions, though, so it’s a diagnostic.
  3. Hiding earlier <nl> tokens from <nl> brought back about half the missing rows without row positions, and didn’t hurt other tables. It’s worth testing on real tables.
  4. If you count only short outputs, a mask that never stops looks like a fix. Count overshoot too.