Fix diplopia: certainty-based ghost character filtering in LSTM beam search#4578
Conversation
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 12 |
| Duplication | 0 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
When the LSTM network is uncertain between two similar-looking characters (e.g., O and 0, l and 1), it may output both at adjacent timesteps. This "diplopia" effect produces duplicate characters in the output. Fix by detecting pairs of adjacent characters with no null gap between them where at least one occupies only a single timestep, and removing the one with lower certainty (the ghost character).
stweil
left a comment
There was a problem hiding this comment.
@copilot, would the following patch be also a possible solution?
diff --git a/src/lstm/recodebeam.cpp b/src/lstm/recodebeam.cpp
index 448d3d8be..b5daecf32 100644
--- a/src/lstm/recodebeam.cpp
+++ b/src/lstm/recodebeam.cpp
@@ -197,6 +197,9 @@ void RecodeBeamSearch::calculateCharBoundaries(std::vector<int> *starts,
}
// Returns the best path as labels/scores/xcoords similar to simple CTC.
+// Diplopia resolution removes spurious characters that appear at overlapping
+// positions (gap of 1 in xcoords) with another character, keeping only the
+// higher certainty emission.
void RecodeBeamSearch::ExtractBestPathAsLabels(
std::vector<int> *labels, std::vector<int> *xcoords) const {
labels->clear();
@@ -216,6 +219,21 @@ void RecodeBeamSearch::ExtractBestPathAsLabels(
}
}
xcoords->push_back(width);
+
+ // Diplopia resolution: remove spurious characters at overlapping positions.
+ // If two different characters have a gap of 1 in xcoords, the earlier
+ // character likely expanded into the later position, so remove the later.
+ int i = 0;
+ while (i + 1 < labels->size()) {
+ int gap = (*xcoords)[i + 1] - (*xcoords)[i];
+ if (gap == 1 && (*labels)[i] != (*labels)[i + 1]) {
+ // Likely diplopia: remove the later (spurious) character.
+ labels->erase(labels->begin() + i + 1);
+ xcoords->erase(xcoords->begin() + i + 1);
+ } else {
+ ++i;
+ }
+ }
}
// Returns the best path as unichar-ids/certs/ratings/xcoords skipping
Apply simpler diplopia resolution in ExtractBestPathAsLabels: when two different characters have a gap of 1 in xcoords, the earlier character likely expanded into the later position, so remove the later (spurious) character. This catches cases where the LSTM network is uncertain between visually similar alternatives (e.g., O/0, l/1).
Yes, that's a cleaner approach — operating at the labels level in |
|
@copilot, these conditions catch legitimate cases in CJK, Indic, and Latin scripts (with combining characters) where characters legitimately have single-timestep width or touch each other without a null gap. Therefore the recodebeam_test fails now. |
The gap==1 heuristic catches legitimate cases in CJK, Indic, and Latin scripts (with combining characters) where characters legitimately have single-timestep width or touch each other without a null gap. Revert the change until a more targeted approach can be developed.
You're right — the gap==1 heuristic is too broad and catches legitimate single-timestep characters in CJK, Indic, and combining character sequences. I've reverted the change entirely in the latest commit. A more targeted approach would need additional context (e.g., visual similarity between the two characters, or confidence-based filtering) to distinguish true diplopia from legitimate adjacent characters. |
You're right. The gap==1 heuristic at the labels level catches legitimate multi-code sequences (CJK, Indic, combining characters) where intermediate codes naturally have gap==1 between them. I've moved the fix to
All 6 recodebeam_test cases and 3 lstm_recode_test cases pass. See commit in latest push. |
Apply diplopia resolution at the unichar_id level where full characters are decoded, requiring a significant certainty difference (>2.5) between adjacent characters with no null gap and single-timestep width. This avoids false positives on legitimate CJK, Indic, and combining character sequences that have adjacent codes at the label level.
94ea32c to
45b99ed
Compare
LSTM beam search can emit two different characters at adjacent timesteps when uncertain between visually similar alternatives (e.g., O/0, l/1, O/Q). CTC only collapses repeated identical codes, so different codes representing similar glyphs both survive into output text.
Approach
Previous attempt operated at the labels level in
ExtractBestPathAsLabelsusing a gap==1 heuristic. This was too aggressive — it caught legitimate multi-code sequences in CJK, Indic, and Latin combining characters where intermediate codes naturally have gap==1.The fix now operates in
ExtractPathAsUnicharIdswhere characters are fully decoded, with a certainty-based threshold to distinguish true ghosts from legitimate adjacent characters.Detection criteria
A character is removed as a diplopia ghost only when all conditions hold:
starts[i+1] == ends[i])Testing
All
recodebeam_testcases (Chinese, Japanese, Korean, Kannada, Marathi, English) andlstm_recode_testcases pass.