Skip to content

Commit 40297ee

Browse files
committed
remove bestMatch
1 parent fd16980 commit 40297ee

2 files changed

Lines changed: 53 additions & 43 deletions

File tree

tika-encoding-detectors/tika-encoding-detector-charsoup/src/main/java/org/apache/tika/langdetect/charsoup/CharSoupEncodingDetector.java

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -290,13 +290,32 @@ private Charset arbitrate(TikaInputStream tis,
290290
}
291291

292292
/**
293-
* Generative-model tiebreaker: for each candidate charset's decoded text,
294-
* detect the most likely language then compute its z-score. The charset
295-
* producing the highest z-score (closest to "real language") wins, provided
296-
* it exceeds {@link #MIN_GENERATIVE_ZSCORE}.
293+
* Generative-model tiebreaker: for each candidate charset's decoded
294+
* text, let the discriminative language classifier pick the most
295+
* likely language, then ask the generative model how natural the
296+
* decoded text is UNDER THAT LANGUAGE. The charset producing the
297+
* highest length-adjusted z-score wins, provided it exceeds
298+
* {@link #MIN_GENERATIVE_ZSCORE}.
297299
*
298-
* @return the winning charset, or {@code null} if no candidate passes the
299-
* threshold or all candidates decode to identical text
300+
* <p>The chaining matters. An earlier revision used
301+
* {@code GLM.bestMatch(text)} to pick the language, which compares
302+
* raw scores across all ~200 languages. That comparison is
303+
* unreliable because out-of-class inputs can produce hash-collision
304+
* scores that exceed in-class scores for another language — the
305+
* classic pathology where real Chinese prose ranks at position 20
306+
* under zho while Sakizaya, Amis, Min-Dong-romanization, and other
307+
* unrelated languages rank ahead of it. The GLM's raw scores were
308+
* never meant for across-language comparison. The discriminative
309+
* classifier was trained explicitly for that job and is far more
310+
* reliable at picking the language. Once a language is picked, the
311+
* GLM's per-language calibrated z-score answers the question it
312+
* actually was designed for: "is this decoded text natural text in
313+
* language X?"
314+
*
315+
* @return the winning charset, or {@code null} if no candidate
316+
* passes the z-score threshold, no candidate yields a
317+
* discriminative language prediction, or all candidates
318+
* decode to identical text
300319
*/
301320
private static <K> K generativeTiebreak(Map<K, String> candidates) {
302321
if (candidates.isEmpty()) {
@@ -323,13 +342,20 @@ private static <K> K generativeTiebreak(Map<K, String> candidates) {
323342
if (CharSoupLanguageDetector.junkRatio(text) > 0.10f) {
324343
continue;
325344
}
326-
Map.Entry<String, Float> match = GLM.bestMatch(text);
327-
if (match == null) {
345+
// Chain: discriminative classifier picks the language,
346+
// GLM scores under that language (not bestMatch across all langs).
347+
List<String> topLangs =
348+
CharSoupLanguageDetector.topShortTextLanguages(text, 1);
349+
if (topLangs.isEmpty()) {
350+
continue;
351+
}
352+
String discLang = topLangs.get(0);
353+
if (discLang == null || discLang.isEmpty()) {
328354
continue;
329355
}
330-
float z = GLM.zScoreLengthAdjusted(text, match.getKey());
331-
LOG.debug("generativeTiebreak: {} -> lang={} z={}",
332-
entry.getKey(), match.getKey(), z);
356+
float z = GLM.zScoreLengthAdjusted(text, discLang);
357+
LOG.debug("generativeTiebreak: {} -> discLang={} z={}",
358+
entry.getKey(), discLang, z);
333359
if (!Float.isNaN(z) && z > bestZ) {
334360
bestZ = z;
335361
bestKey = entry.getKey();

tika-langdetect/tika-langdetect-charsoup-core/src/main/java/org/apache/tika/langdetect/charsoup/GenerativeLanguageModel.java

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -310,38 +310,22 @@ private void scoreV3(String pp, int li, double[] sum, int[] cnt) {
310310
}
311311
}
312312

313-
/**
314-
* Score {@code text} against all languages and return the best match.
315-
*/
316-
public Map.Entry<String, Float> bestMatch(String text) {
317-
String best = null;
318-
float bestScore = Float.NEGATIVE_INFINITY;
319-
for (String lang : langIds) {
320-
float s = score(text, lang);
321-
if (!Float.isNaN(s) && s > bestScore) {
322-
bestScore = s;
323-
best = lang;
324-
}
325-
}
326-
return best == null ? null : Map.entry(best, bestScore);
327-
}
328-
329-
/**
330-
* Average raw score of {@code text} across all CJK languages in the model.
331-
*/
332-
public float avgCjkScore(String text) {
333-
double sum = 0;
334-
int count = 0;
335-
for (int i = 0; i < langIds.size(); i++) {
336-
if (!isCjk[i]) continue;
337-
float s = score(text, langIds.get(i));
338-
if (!Float.isNaN(s)) {
339-
sum += s;
340-
count++;
341-
}
342-
}
343-
return count == 0 ? Float.NaN : (float) (sum / count);
344-
}
313+
// Cross-language score comparison was removed (formerly bestMatch /
314+
// avgCjkScore). The per-class raw scores are not comparable across
315+
// languages: each language's per-bucket log-probabilities are
316+
// normalised by that language's own training-corpus total, so
317+
// small-corpus languages produce systematically higher per-bucket
318+
// log-probs than large-corpus languages on out-of-class input.
319+
// In practice a real Chinese probe scored as {@code zho} would return
320+
// a raw score around -8 while scoring the same probe against a small
321+
// minor-language model would return a raw score around -3 to -5 just
322+
// from the smoothing-denominator artefact. The GLM is designed to
323+
// answer "given it is language X, how natural is this text as
324+
// language X?" — not "which language is this?". Callers that need
325+
// to pick a language first must do so with the discriminative
326+
// classifier (CharSoupLanguageDetector), then pass that language
327+
// explicitly to {@link #score}, {@link #zScore}, or
328+
// {@link #zScoreLengthAdjusted}.
345329

346330
// ---- Z-score API ----
347331

0 commit comments

Comments
 (0)