Compares a rule-based method, langdetect, langid and lingua on the corpus. langid with a pre-filter and a 0.6 threshold flags 0 of 300 English comments, 44/45 Danish and 41/45 Malay. The comparison script is kept next to the corpus. Refs #198 Task: MIL-013#2
121 lines
4.5 KiB
Python
121 lines
4.5 KiB
Python
"""MIL-013 task 2 (ADR-0012): compare detection methods on the corpus.
|
|
|
|
Needs langdetect, langid and lingua-language-detector installed (not part of the
|
|
test requirements). Usage: python compare_detectors.py [-v]
|
|
"""
|
|
|
|
import csv
|
|
import re
|
|
import sys
|
|
import time
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
CORPUS = Path(__file__).resolve().parent
|
|
rows = []
|
|
for name in ("english.tsv", "foreign.tsv"):
|
|
with (CORPUS / name).open(encoding="utf-8", newline="") as handle:
|
|
rows += [(r["lang"], r["text"]) for r in csv.DictReader(handle, delimiter="\t", quoting=csv.QUOTE_NONE)]
|
|
|
|
# ---------- A: rule-based
|
|
NON_ENGLISH_LETTERS = re.compile(r"[æøåäöüßéèêàâçñôîûœ]", re.I)
|
|
STOP = {
|
|
"en": "the a an of to and is in it that this for on with as are be by or if not from at which can will we you "
|
|
"when then use used only each all any its into has have was were been do does done so but also than how what".split(),
|
|
"da": "og er det en at i jeg til på for med af ikke den der som vi skal kan har de om men et fra hvis her kun "
|
|
"efter før bliver være dette denne mappen filen listen brugeren serveren tjek hent gem slet læs opret".split(),
|
|
"ms": "dan yang untuk dengan ini itu di dalam pada akan atau tidak dari ke kita jika boleh adalah mesti sama "
|
|
"semak fail pelayan pengguna senarai kira baris guna supaya apabila sebelum selepas daripada".split(),
|
|
"other": "der die das und ist nicht ein eine mit von zu den dem auf für wir och att som en är inte för med "
|
|
"på av vi le la les des est une pour pas dans que qui nous sur om det jeg å ikke vi må".split(),
|
|
}
|
|
|
|
|
|
def rule_based(text: str) -> bool:
|
|
if NON_ENGLISH_LETTERS.search(text):
|
|
return True
|
|
words = re.findall(r"[a-zA-ZæøåÆØÅ']+", text.lower())
|
|
score = {k: sum(w in set(v) for w in words) for k, v in STOP.items()}
|
|
foreign = max(score["da"], score["ms"], score["other"])
|
|
return foreign >= 2 and foreign > score["en"]
|
|
|
|
|
|
# ---------- B: langdetect
|
|
def make_langdetect():
|
|
from langdetect import DetectorFactory, detect_langs
|
|
|
|
DetectorFactory.seed = 0
|
|
|
|
def run(text: str, threshold: float = 0.8) -> bool:
|
|
try:
|
|
best = detect_langs(text)[0]
|
|
except Exception:
|
|
return False
|
|
return best.lang != "en" and best.prob >= threshold
|
|
|
|
return run
|
|
|
|
|
|
# ---------- C: langid
|
|
def make_langid():
|
|
import langid
|
|
|
|
langid.set_languages(["en", "da", "no", "sv", "de", "fr", "ms", "id", "nl"])
|
|
|
|
def run(text: str, threshold: float = 0.0) -> bool:
|
|
lang, _score = langid.classify(text)
|
|
return lang != "en"
|
|
|
|
return run
|
|
|
|
|
|
# ---------- D: lingua
|
|
def make_lingua():
|
|
from lingua import Language, LanguageDetectorBuilder
|
|
|
|
names = ["ENGLISH", "DANISH", "BOKMAL", "NYNORSK", "SWEDISH", "GERMAN", "FRENCH", "MALAY", "INDONESIAN", "DUTCH"]
|
|
languages = [getattr(Language, n) for n in names if hasattr(Language, n)]
|
|
print("lingua languages:", [l.name for l in languages], file=sys.stderr)
|
|
detector = LanguageDetectorBuilder.from_languages(*languages).build()
|
|
|
|
def run(text: str, threshold: float = 0.5) -> bool:
|
|
values = detector.compute_language_confidence_values(text)
|
|
if not values:
|
|
return False
|
|
top = values[0]
|
|
return top.language != Language.ENGLISH and top.value >= threshold
|
|
|
|
return run
|
|
|
|
|
|
METHODS = {"rule": rule_based}
|
|
for name, factory in (("langdetect", make_langdetect), ("langid", make_langid), ("lingua", make_lingua)):
|
|
try:
|
|
METHODS[name] = factory()
|
|
except Exception as error: # a method that cannot be built is reported, not hidden
|
|
print("cannot build", name, error, file=sys.stderr)
|
|
|
|
|
|
def report(name: str, predict) -> None:
|
|
started = time.perf_counter()
|
|
flagged = defaultdict(lambda: [0, 0]) # lang -> [flagged, total]
|
|
wrong = []
|
|
for lang, text in rows:
|
|
is_flagged = predict(text)
|
|
flagged[lang][1] += 1
|
|
if is_flagged:
|
|
flagged[lang][0] += 1
|
|
if (lang == "en") == is_flagged:
|
|
wrong.append((lang, text))
|
|
elapsed = time.perf_counter() - started
|
|
foreign = [k for k in flagged if k != "en"]
|
|
line = f"{name:12s} " + " ".join(f"{k}={flagged[k][0]}/{flagged[k][1]}" for k in sorted(foreign))
|
|
print(f"{line} FALSE-POSITIVES en={flagged['en'][0]}/{flagged['en'][1]} {elapsed:.2f}s/{len(rows)}")
|
|
if "-v" in sys.argv:
|
|
for lang, text in wrong[:25]:
|
|
print(" ", lang, "|", text[:80])
|
|
|
|
|
|
for name, predict in METHODS.items():
|
|
report(name, predict)
|