Wenn wir irgendetwas mit Texten machen wollen, ist es fast immer notwendig zu wissen, in welcher Sprache die texte geschrieben sind. Egal ob wir eine Rechtschreibkorrektur machen, einen Suchindex aufbauen oder Personennamen im Text finden wollen, die Verarbeitung der Texte ist sprachspezifisch. Manchmal gibt es Metadaten zu Texten, in denen die Sprache genannt wird. Diese sind aber oft nicht korrekt und meistens haben wir übehaupt keine Sprachangaben für die Texte, mit denen wir etwas machen wollen. In vielen Fällen ist es aber einfach, die Sprache des Textes automatisch zu erkennen.
Was ist die genaue Aufgabe?
Wenn wir aus zwei möglichen Sprachen wählen müssen, ist die Aufgabe einfacher als wenn wir aus 3000 Sprachen wählen müssen. In vielen praktischen Fällen muss man tatsächlich nur aus einigen Sprachen wählen. Wenn wir z.B. Social Media zu einem Deutschen Thema oder von einer deutschen Seite aus sammeln, können wir davon ausgehen, dass 95% aller Berichte deutsch oder englisch sind. Das gleiche gilt z.B. auch wenn wir Abstracts im Katalog einer deutschen Bibliothek analysieren.
Im folgenden werden wir versuchen aus einer kleinen Anzahl West-Europäischer Sprachen zu wählen.
Im schlimmsten Fall haben wir eine Datei von der wir nicht mal wissen, wie die Zeichen kodiert sind. Wir lesen also Bits und wissen nicht, ob wir die Bits nach der ASCII-, UTF8-, Latin2-Tabelle usw. interpretieren müssen. Wir müssen jetzt sowohl die Sprache und die Kodierung erraten. Im folgenden vereinfachen wir das Problem, und gehen davon aus, dass alle Text, die wir bekommen in UTF8 kodiert sind.
Ein einfaches Verfahren, dass man sich vorstellen könnte wäre folgendes: Wir schauen in einem Text, wie viele häufig vorkommende deutsche Wörter wir finden. Hierzu muss der Text aber erstens lang genug sein, um eine zuverlässige Aussagen über die Sprache machen zu können, da manche Wörter in verschiedenen Sprachen vorkommen. Eine in vielen Internetforen häufig vorgeschlagene Variante ist die, dass man Stopwörter (Stopwörter sind sehr häufig vorkommende Wörter ohne eigene Bedeutung, wie Artikel, Pronomina, usw.) zählt, das Stopwörter häufig vorkommen und Stopwortlisten für vielen Sprachen leicht erhältlich sind. Für kürzere Text funktioniert dieser Ansatz leider nicht. Wörter wie de und en kommen sowohl in niederländischen wir auch in französischen Stopwortlisten vor. Solche Doppelungen gibt es oft, das Stopwörter meist ein- oder zweisiblige Wörter sind, und die Zahl der möglichen einsilbigen Wörter natürlich nicht unbegrenzt ist. Ein Buchtitel wie
De kleine prins en de grote drakejacht
könnte nach dem Stopwortverfahren genau so gut niederländisch als französisch sein, während es für jemand, der beide Sprachen einigermaßen kennt, sofort klar ist, das es sich beim Beispiel um eine niederlänische Phrase handelt.
Bessere Ergebnisse bekommen wir, wenn wir uns nicht auf Wörter konzentrieren, sondern uns die Verteilung von Buchstaben anschauen. Im Englischen gibt es häufig einen y im Deutschen finden wir viele Buchstaben mit Umlauten usw. Noch besser funktionieren kombinationen von 2 und 3 Buchstaben, sogenannt Bi- und Trigramme. Wir können sogar einzelne Buchstaben, Bi- und Trigramme kombinieren. Dieser Ansatz ist beschrieben im folgenden Paper:
Cavnar, W.B., Trenkle, J.M.: N-gram-based text categorization. In: Proceedings of SDAIR-94, 3rd Annual Symposium on Document Analysis and Information Retrieval. pp. 161-175 (1994) http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.21.3248&rep=rep1&type=pdf
Folgende Funktion extrahiert n-Gramme aus einem String:
def ngram(string,n):
liste = []
if n < len(string):
for p in range(len(string) - n + 1) :
tg = string[p:p+n]
liste.append(tg)
return liste
Wir testen die Funktion:
text = "Die Verfasserin unternimmt es in diesem Buche, die Geschichte des Kautschuks in Menschenschicksalen zu erzählen."
trigramme = ngram(text,3)
print(trigramme)
Laut Cavnar e.a. bekommen wir die beste Ergebnisse, wenn wir Mono-, Bi- und Trigramme im Kombination nutzen. Wir schreiben also eine Funktion, die alle n-Gramme, für $1 \leq n < 4$ extrahiert:
def xgram(string):
liste = []
for n in range(1,4) :
liste = liste + ngramn(string.lower(),n)
return liste
Etwas pythonischer können wir die Funktion auch schreiben als:
def xgram(string):
return [w for n in range(1,4) for w in ngram(string.lower(),n)]
xgramme = xgram(text)
print(xgramme)
Ein Modell für eine Sprache ist jetzt eine Menge solcher n-Gramme, mit dazu jeweils die Wahrscheinlichkeit, dass diese n-Gramme vorkommen. Ein Modell ist in Python also ein Dictionary.
def buildmodel(text):
model = {}
xgramme = xgram(text)
nr_of_ngs = len(xgramme)
for w in xgramme:
f = 1 + model.get(w,0)
model[w] = f
for w in model:
model[w] = float(model[w]) / float(nr_of_ngs)
return model
Wir testen die Funktion und lassen uns das Ergebnis anzeigen:
model = buildmodel(text)
print(model)
Alternativ können wir auch eine Bibliothek nutzen:
import collections
def buildmodel(text):
model = collections.Counter(xgram(text))
nr_of_ngs = sum(model.values())
for w in model:
model[w] = float(model[w]) / float(nr_of_ngs)
return model
model = buildmodel(text)
print(model)
Wenn wir jetzt ausreichend Text haben, bekommen wir typische Werte für eine Sprache. Die Werte in einem unbekannten Text können wir hiermit vergleichen.
Im NLTK-Paket ist die Erklärung der Menschenrechte in 300 Sprachen enthalten. Hiermit können wir jetzt mal einige Sprachmodelle bauen.
Wenn Sie das Paket nltk zum ersten mal nutzen, oder bisher keine Corpora benutzt haben, bekommen Sie vielleicht eine Fehlermeldung, die darauf zurückgeführt werden kann, dass nicht alle Ressourcen aus dem Paket installiert sind. Sie können in dem Fall einmalig den folgenden Code ausführen. Es erscheint jetzt ein Fenster, in dem Sie auswählen können, welche Ressourcen Sie herunterladen möchte. Die Option 'Book' ist für uns ausreichend.
# Ggf. auskommentieren und einmalig ausführen
#
#import nltk
#
#nltk.download()
from nltk.corpus import udhr
#print udhr.fileids()
languages = ['english','german','dutch','french','italian','spanish']
english_udhr = udhr.raw('English-Latin1')
german_udhr = udhr.raw('German_Deutsch-Latin1')
dutch_udhr = udhr.raw('Dutch_Nederlands-Latin1')
french_udhr = udhr.raw('French_Francais-Latin1')
italian_udhr = udhr.raw('Italian_Italiano-Latin1')
spanish_udhr = udhr.raw('Spanish_Espanol-Latin1')
texts = {'english':english_udhr,'german':german_udhr,'dutch':dutch_udhr,'french':french_udhr,'italian':italian_udhr,'spanish':spanish_udhr}
models = {lang:buildmodel(texts[lang]) for lang in languages}
Die Texte sind sehr kurz und entalten nur ein Bruchteil der Wörter, die in einer Sprache vorkommen. Fürs Deutsche haben wir gerade mal 10 000 Zeichen:
print(len(german_udhr))
Wir müssen jetzt die n-Gram Frequenzen eiens Textes mit den Frequenzen der Modelle vergleichen. Um die Modelle zu vergleichen berechnen wir den Cosinus: $$ cos(a,b) = \frac{\sum_i a_i b_i}{\sqrt{\sum_i a_i^2}\sqrt{\sum_i b_i^2}} $$
import math
def cosinus(a,b):
return sum([a[k]*b[k] for k in a if k in b]) / (math.sqrt(sum([a[k]**2 for k in a])) * math.sqrt(sum([b[k]**2 for k in b])))
print(text)
textmodel = buildmodel(text)
for m in models:
print(m, cosinus(models[m],textmodel))
Wir sehen, dass es jetzt schon funktioniert! Zuverlässiger wird es natürlich, wenn wir längere Texte und mehr Genres nehmen um die Sprachmodelle zu bauen.
Wir brauchen schließlich eine Funktion, die einfach sagt, in welcher Sprache ein Text geschrieben ist.
def guess_language(text):
textmodel = buildmodel(text)
lang = "english"
best = 0
for m in models:
c = cosinus(models[m],textmodel)
if c > best:
best = c
lang = m
return lang
t = "Wie zijn leven voltooid vindt en met een consulent in gesprek gaat over zelfdoding, stelt zelfeuthanasie vaak uit of ziet ervan af"
print(guess_language(t))
t = u"L’ancien candidat écologiste à la primaire de la gauche s’était engagé à soutenir le vainqueur de ce scrutin à la fin janvier, en l’occurrence Benoît Hamon."
print(guess_language(t))
t = "Una capra al posto del giardiniere"
print(guess_language(t))
Das letzte Ergebnis ist falsch! Italienisch wäre richtig. Der Text ist zu kurz und das Modell basiert auf zu wenig Wörtern.
Schließlich wollen wir die Modelle nicht jedes mal neu berechnen. Wir speichern die Modelle daher in eine Datei.
import pickle
pickle.dump(models, open('langidmodels.p', 'wb'))
Wir können die Modelle jetzt auch wieder einlesen:
models = pickle.load(open('langidmodels.p', 'rb'))
Sie können dieses Corpus wie folgt einlesen:
import nltk
root = 'Corpora'
fileid = 'tiger.16012013.conll09'
columntypes = ['ignore','words','ignore','ignore','pos']
tiger_corpus = nltk.corpus.ConllCorpusReader(root,fileid,columntypes,encoding='utf8')
Sie können eine Liste von Wörtern bekommen und müssen jetzt entweder hieraus einen Text basteln, oder das Modell aus einer Liste von Wörtern aufbauen. Es ist dann sinnvoll jedes Wort mit einem Wortanfang- und Wortendesymbol zu erweitern. Aus Diktator wird also #Diktator#. Sie haben dann auch Bigramme, wie #D und r#. Daraus sieht man, dass ein Deutsches Wort mit D anfangen und auf r enden kann.
text_german = tiger_corpus.words()
print(len(text_german))
print(text_german[:10])