ProjektBaut auf Katas: Dictionaries aufIm Handbuch: Reguläre Ausdrücke
image: teaser.webp image_alt: "Das Wort »Es« mit Pfeilen zu den Wörtern, die in den Märchen der Brüder Grimm darauf folgen: »war« mit dem dicksten Pfeil und der Zahl 41, dann »ist« mit 21, »trug« mit 13, »soll« und »wird« mit je 5." kind: projekt duration: 2 Doppelstunden languages: [ruby, python] curriculum: "Sek I: Algorithmisches Problemlösen (Zeichenkettenbearbeitung, Implementieren von Algorithmen), Information und Daten (Daten, Information, Nachricht), Geschichte der Informatik (Lebensläufe und Leistungen bedeutender Persönlichkeiten: Markow und Shannon zählen Buchstaben und Wörter); Oberstufe: Vertiefung Künstliche Intelligenz (ein Sprachmodell aus Wortpaaren), Vertiefung Informatik und Gesellschaft (woher die Texte kommen, was ein Modell daraus übernimmt)" topics: [ki, zeichenketten, information, gesellschaft, geschichte] age: Sek I requires: [kata-dicts] reference: [regex]

Ein winziges Sprachmodell

Wie kann ein Programm einen Satz weiterschreiben? Du zählst in den Märchen der Brüder Grimm, welches Wort auf welches folgt, und lässt dein Programm daraus neue Märchen schreiben, Wort für Wort. Dabei siehst du, was so ein Modell kann, wo es nur abschreibt und was es aus seinem Text übernimmt.

1. Das nächste Wort

Tippst du auf dem Handy eine Nachricht, schlägt die Tastatur über den Buchstaben drei Wörter vor, die als Nächstes kommen könnten. Ein Chatbot macht im Grunde dasselbe, nur sehr viel besser: Er schreibt ein Wort, schaut, was bisher dasteht, schreibt das nächste Wort, und so weiter, bis die Antwort fertig ist. Ein Programm, das zu einem Text das nächste Wort vorhersagt, heißt Sprachmodell.

Du baust heute ein winziges Sprachmodell. Es lernt aus den Märchen der Brüder Grimm: 54 Märchen mit zusammen etwa 80 000 Wörtern, von »Marienkind« bis »Des Herrn und des Teufels Getier«. Lernen heißt hier: zählen, welches Wort wie oft auf welches folgt.

Vermutung 1Was folgt auf »Es«?

In den Märchen beginnen viele Sätze mit »Es«. Welches Wort folgt darauf am häufigsten?

Was folgt auf »Es«?

Was so ein Modell zählt, merkt es sich in einem Dictionary, genauer in einem Dictionary von Dictionaries. Wie Dictionaries gehen, übst du in den Katas: Dictionaries.

2. Einrichten

Lege einen Ordner an und lade die Märchen herunter:

mkdir -p ~/sprachmodell
cd ~/sprachmodell
wget https://workspace.hackschule.de/sprachmodell/maerchen.txt
touch model.rb story.rb

In model.rb kommen die Funktionen deines Modells, in story.rb das Programm, das damit Märchen schreibt. Ausprobieren kannst du die Funktionen in irb, das model.rb vorher lädt:

irb -r ./model.rb

Nach einer Änderung an model.rb beendest du irb mit StrgD und startest es neu.

Lege einen Ordner an und lade die Märchen herunter:

mkdir -p ~/sprachmodell
cd ~/sprachmodell
wget https://workspace.hackschule.de/sprachmodell/maerchen.txt
touch model.py story.py

In model.py kommen die Funktionen deines Modells, in story.py das Programm, das damit Märchen schreibt. Ausprobieren kannst du die Funktionen in Python selbst, das model.py vorher ausführt:

python3 -i model.py

Nach einer Änderung an model.py beendest du Python mit StrgD und startest es neu.

Die Märchen stehen in der Schreibweise eines Buchs von 1921: »daß« statt »dass«, »Türe« statt »Tür«. Der Text kommt von Project Gutenberg, wo Freiwillige alte Bücher abtippen, die niemandem mehr gehören; die vier Märchen auf Plattdeutsch sind herausgenommen, damit das Modell nur eine Sprache lernt.

3. Aus Text werden Wörter

Ein Modell zählt Wörter, also muss der Text zuerst in Wörter zerlegt werden. Satzzeichen zählen dabei als eigene Wörter: Wer weiß, dass nach »König« oft ein Komma kommt und nach einem Punkt ein neuer Satz, schreibt Sätze, die enden. Anführungszeichen lässt das Modell weg, denn es kann sich nicht merken, ob es gerade eines geöffnet hat.

Schreib in model.rb:

# Der Text als Liste von Wörtern und Satzzeichen
def words(text)
  text.scan(/\p{L}+|[.,;:!?]/)
end

scan findet im Text alles, was zu dem Muster zwischen den Schrägstrichen passt, einem regulären Ausdruck: \p{L}+ ist ein oder mehrere Buchstaben hintereinander, auch Umlaute und ß, [.,;:!?] ein Satzzeichen.

Schreib in model.py:

import random
import re


# Der Text als Liste von Wörtern und Satzzeichen
def words(text):
    return re.findall(r"[^\W\d_]+|[.,;:!?]", text)

re.findall findet im Text alles, was zu dem Muster passt, einem regulären Ausdruck: [^\W\d_]+ ist ein oder mehrere Buchstaben hintereinander, auch Umlaute und ß, [.,;:!?] ein Satzzeichen. random brauchst du später.

Ausprobieren: Ruf words("Es war einmal ein König.") auf. Achte darauf, was mit dem Punkt am Ende passiert.
Aufgabe 2Wie viele Wörter?

Wie viele Wörter und Satzzeichen findet words in den ganzen Märchen? Lies dazu maerchen.txt ein, ruf words damit auf und lass dir die Länge der Liste geben.

Tipp

In Ruby liest File.read("maerchen.txt") die Datei als einen String, in Python open("maerchen.txt").read(). Die Länge einer Liste gibt .size in Ruby und len(…) in Python.

4. Wer folgt auf wen

Das Modell soll sich für jedes Wort merken, welche Wörter danach kamen und wie oft. Für den kleinen Text »Der Wolf kam. Der Wolf fraß. Der Jäger kam.« sieht das so aus:

{
  "Der" => { "Wolf" => 2, "Jäger" => 1 },
  "Wolf" => { "kam" => 1, "fraß" => 1 },
  "kam" => { "." => 2 },
  "." => { "Der" => 2 },
  "fraß" => { "." => 1 },
  "Jäger" => { "kam" => 1 }
}
{
    "Der": {"Wolf": 2, "Jäger": 1},
    "Wolf": {"kam": 1, "fraß": 1},
    "kam": {".": 2},
    ".": {"Der": 2},
    "fraß": {".": 1},
    "Jäger": {"kam": 1},
}

Ein Dictionary, dessen Werte wieder Dictionaries sind: Unter jedem Wort steht, was darauf folgte, und unter jedem Nachfolger, wie oft. Nach dem letzten Punkt kommt nichts mehr, darum zählt er nicht mit.

Aufgabe 3Von Hand gezählt

Was steht im Dictionary des Textes »Der Wolf kam. Der Jäger kam. Der Wolf lief.« unter »kam«?

Von Hand gezählt
Jetzt bist du dran: Schreib die Funktion follow_counts(tokens), die zu einer Liste von Wörtern dieses Dictionary zurückgibt.
Tipp

Geh die Liste mit einem Index durch, vom ersten bis zum vorletzten Wort: Das Wort ist tokens[i], sein Nachfolger tokens[i + 1].

Nächster Tipp

Kommt ein Wort zum ersten Mal vor, gibt es für es noch kein Dictionary. Leg es dann an, mit counts[word] ||= {}. Ein Nachfolger, der noch fehlt, hat die Anzahl 0: (counts[word][following] || 0) + 1.

Nächster Tipp
def follow_counts(tokens)
  counts = {}
  (0...tokens.size - 1).each do |i|
    word = tokens[i]
    following = tokens[i + 1]
    # hier: das Dictionary für word anlegen, wenn es fehlt, und following zählen
  end
  counts
end
Lösung anzeigen
# Für jedes Wort: welche Wörter darauf folgen, und wie oft
def follow_counts(tokens)
  counts = {}
  (0...tokens.size - 1).each do |i|
    word = tokens[i]
    following = tokens[i + 1]
    counts[word] ||= {}
    counts[word][following] = (counts[word][following] || 0) + 1
  end
  counts
end
Tipp

Geh die Liste mit einem Index durch, vom ersten bis zum vorletzten Wort: Das Wort ist tokens[i], sein Nachfolger tokens[i + 1].

Nächster Tipp

Kommt ein Wort zum ersten Mal vor, gibt es für es noch kein Dictionary. Leg es dann an, wenn word not in counts ist. Ein Nachfolger, der noch fehlt, hat die Anzahl 0: counts[word].get(following, 0) + 1.

Nächster Tipp
def follow_counts(tokens):
    counts = {}
    for i in range(len(tokens) - 1):
        word = tokens[i]
        following = tokens[i + 1]
        # hier: das Dictionary für word anlegen, wenn es fehlt, und following zählen
    return counts
Lösung anzeigen
# Für jedes Wort: welche Wörter darauf folgen, und wie oft
def follow_counts(tokens):
    counts = {}
    for i in range(len(tokens) - 1):
        word = tokens[i]
        following = tokens[i + 1]
        if word not in counts:
            counts[word] = {}
        counts[word][following] = counts[word].get(following, 0) + 1
    return counts
Aufgabe 4Wer folgt auf wen

Schreib in sprachmodell/model.rb die Funktion follow_counts(tokens). Die Prüfung ruft sie mit kurzen Listen auf und vergleicht das Dictionary, das herauskommt.

Aufgabe 4Wer folgt auf wen

Schreib in sprachmodell/model.py die Funktion follow_counts(tokens). Die Prüfung ruft sie mit kurzen Listen auf und vergleicht das Dictionary, das herauskommt.

In die Märchen schauen

Jetzt zählt dein Modell die ganzen Märchen. Damit du nachsehen kannst, was auf ein Wort folgt, schreib noch diese Funktion dazu; sie gibt die n häufigsten Nachfolger mit ihrer Anzahl zurück:

# Die n häufigsten Nachfolger von word, mit ihrer Anzahl
def top(counts, word, n)
  (counts[word] || {}).sort_by { |following, count| -count }.first(n)
end

In irb:

counts = follow_counts(words(File.read("maerchen.txt")))
top(counts, "Es", 5)
# Die n häufigsten Nachfolger von word, mit ihrer Anzahl
def top(counts, word, n):
    followers = counts.get(word, {})
    return sorted(followers.items(), key=lambda pair: -pair[1])[:n]

In Python:

counts = follow_counts(words(open("maerchen.txt").read()))
top(counts, "Es", 5)

Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.

Auf »Es« folgt 41-mal »war«, 21-mal »ist« und 13-mal »trug«; zusammen kommt »Es« 150-mal vor.

Ausprobieren: Schau nach, was auf »Wolf«, »Königin« und »Frau« folgt. Achte darauf, wie oft ein Satzzeichen unter den Nachfolgern ganz oben steht.
Aufgabe 5Nach »sprach«

Was folgt in den Märchen am häufigsten auf »sprach«? Schau mit top nach.

Nach »sprach«

5. Ein Märchen schreiben

Ein Modell, das zu jedem Wort seine Nachfolger kennt, kann schreiben: Es beginnt mit einem Wort, nimmt einen Nachfolger, dann dessen Nachfolger, und so weiter. Am einfachsten nimmt es jedes Mal den häufigsten.

Jetzt bist du dran: Schreib die Funktion most_likely(counts, word), die den häufigsten Nachfolger von word zurückgibt, oder nil (in Python None), wenn das Wort nie vorkam.
Tipp

counts[word] ist das Dictionary der Nachfolger. Von seinen Paaren suchst du das mit der größten Anzahl: max_by { |following, count| count } gibt dieses Paar zurück, [following, count].

Lösung anzeigen
# Der häufigste Nachfolger von word, nil für ein unbekanntes Wort
def most_likely(counts, word)
  return nil unless counts.key?(word)

  counts[word].max_by { |following, count| count }[0]
end
Tipp

counts[word] ist das Dictionary der Nachfolger. max findet seinen größten Schlüssel; mit key=counts[word].get vergleicht es stattdessen die Anzahlen.

Lösung anzeigen
# Der häufigste Nachfolger von word, None für ein unbekanntes Wort
def most_likely(counts, word):
    if word not in counts:
        return None
    return max(counts[word], key=counts[word].get)
Aufgabe 6Das häufigste Wort

Schreib in sprachmodell/model.rb die Funktion most_likely(counts, word). Die Prüfung ruft sie mit kleinen Dictionaries auf, auch mit einem Wort, das darin fehlt.

Aufgabe 6Das häufigste Wort

Schreib in sprachmodell/model.py die Funktion most_likely(counts, word). Die Prüfung ruft sie mit kleinen Dictionaries auf, auch mit einem Wort, das darin fehlt.

Zum Schreiben noch eine Funktion, die eine Liste von Wörtern wieder als Text ausgibt, ohne Leerzeichen vor einem Satzzeichen:

# Wörter als Text, ohne Leerzeichen vor einem Satzzeichen
def text(tokens)
  tokens.join(" ").gsub(/ ([.,;:!?])/, '\1')
end
# Wörter als Text, ohne Leerzeichen vor einem Satzzeichen
def text(tokens):
    return re.sub(r" ([.,;:!?])", r"\1", " ".join(tokens))
Vermutung 7Immer das häufigste Wort

Das Programm beginnt mit »Es« und hängt 30-mal das häufigste nächste Wort an. Was kommt heraus?

Immer das häufigste Wort

Schreib das Programm in story.rb; es lädt model.rb mit require_relative:

require_relative "model"

counts = follow_counts(words(File.read("maerchen.txt")))
tokens = ["Es"]
30.times do
  tokens << most_likely(counts, tokens.last)
end
puts text(tokens)
ruby story.rb

Schreib das Programm in story.py; es holt die Funktionen aus model.py mit import:

from model import *

counts = follow_counts(words(open("maerchen.txt").read()))
tokens = ["Es"]
for _ in range(30):
    tokens.append(most_likely(counts, tokens[-1]))
print(text(tokens))
python3 story.py

Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.

Es war, und sprach: Ich bin ich will ich will ich will ich will ich will ich will ich will ich will ich will ich will ich will ich

Auf »ich« folgt am häufigsten »will«, auf »will« am häufigsten »ich«: Das Modell ist in einer Schleife gefangen und kommt nie wieder heraus. Und es schreibt jedes Mal genau diesen Text, denn es entscheidet nie anders.

6. Würfeln statt immer das Häufigste

Ein Modell, das nur das häufigste Wort nimmt, schreibt immer dasselbe. Besser: Es würfelt, aber so, dass ein häufiger Nachfolger öfter drankommt. Auf »Es« folgt 41-mal »war« und 21-mal »ist«; dann soll »war« ungefähr doppelt so oft gewählt werden wie »ist«.

Das geht mit einer Zufallszahl. Folgte auf ein Wort sechsmal »war«, dreimal »ist« und einmal »trug«, zusammen zehnmal, zieht das Modell eine Zahl von 0 bis 9 und geht die Nachfolger der Reihe nach durch:

Zufallszahl Nachfolger
0, 1, 2, 3, 4, 5 war
6, 7, 8 ist
9 trug

Ist die Zahl kleiner als die Anzahl des ersten Nachfolgers, nimmt das Modell diesen Nachfolger. Sonst zieht es dessen Anzahl von der Zahl ab und vergleicht mit der Anzahl des nächsten Nachfolgers.

Jetzt bist du dran: Schreib die Funktion next_word(counts, word), die einen Nachfolger von word so zieht, dass jeder so oft drankommt, wie er gezählt wurde, und nil (in Python None) für ein Wort, das nie vorkam.
Tipp

Die Summe der Anzahlen ist counts[word].values.sum. rand(total) gibt eine ganze Zufallszahl von 0 bis total - 1.

Nächster Tipp
def next_word(counts, word)
  return nil unless counts.key?(word)

  r = rand(counts[word].values.sum)
  counts[word].each do |following, count|
    # hier: following zurückgeben, wenn r kleiner als count ist,
    # sonst count von r abziehen
  end
end
Lösung anzeigen
# Ein Nachfolger von word, gezogen nach seiner Anzahl; nil für ein unbekanntes Wort
def next_word(counts, word)
  return nil unless counts.key?(word)

  r = rand(counts[word].values.sum)
  counts[word].each do |following, count|
    return following if r < count

    r -= count
  end
end
Tipp

Die Summe der Anzahlen ist sum(counts[word].values()). random.randrange(total) gibt eine ganze Zufallszahl von 0 bis total - 1.

Nächster Tipp
def next_word(counts, word):
    if word not in counts:
        return None
    r = random.randrange(sum(counts[word].values()))
    for following, count in counts[word].items():
        # hier: following zurückgeben, wenn r kleiner als count ist,
        # sonst count von r abziehen
        ...
Lösung anzeigen
# Ein Nachfolger von word, gezogen nach seiner Anzahl; None für ein unbekanntes Wort
def next_word(counts, word):
    if word not in counts:
        return None
    r = random.randrange(sum(counts[word].values()))
    for following, count in counts[word].items():
        if r < count:
            return following
        r -= count

Python kann das auch selbst: random.choices(list(followers), weights=list(followers.values()))[0] zieht genauso. Hier war es dein Code.

Aufgabe 8Würfeln nach der Häufigkeit

Schreib in sprachmodell/model.rb die Funktion next_word(counts, word). Die Prüfung zieht 4000-mal aus { "a" => 3, "b" => 1 } und erwartet ungefähr dreimal so oft »a« wie »b«, dazu ein Wort mit nur einem Nachfolger und eines, das fehlt.

Aufgabe 8Würfeln nach der Häufigkeit

Schreib in sprachmodell/model.py die Funktion next_word(counts, word). Die Prüfung zieht 4000-mal aus {"a": 3, "b": 1} und erwartet ungefähr dreimal so oft »a« wie »b«, dazu ein Wort mit nur einem Nachfolger und eines, das fehlt.

Jetzt bist du dran: Schreib die Funktion generate(counts, start, n), die mit dem Wort start beginnt und n Wörter mit next_word anhängt. Sie gibt die Liste zurück und hört früher auf, wenn ein Wort keinen Nachfolger hat.
Tipp

Es ist das Programm aus story.rb, mit next_word statt most_likely, als Funktion, die die Liste zurückgibt. Aufhören kann eine Schleife mit break.

Lösung anzeigen
# start und bis zu n gezogene Wörter danach
def generate(counts, start, n)
  tokens = [start]
  n.times do
    following = next_word(counts, tokens.last)
    break if following.nil?

    tokens << following
  end
  tokens
end
Tipp

Es ist das Programm aus story.py, mit next_word statt most_likely, als Funktion, die die Liste zurückgibt. Aufhören kann eine Schleife mit break.

Lösung anzeigen
# start und bis zu n gezogene Wörter danach
def generate(counts, start, n):
    tokens = [start]
    for _ in range(n):
        following = next_word(counts, tokens[-1])
        if following is None:
            break
        tokens.append(following)
    return tokens
Aufgabe 9Ein Märchen schreiben

Schreib in sprachmodell/model.rb die Funktion generate(counts, start, n). Die Prüfung gibt ihr Dictionaries, in denen jedes Wort nur einen Nachfolger hat, so dass das Ergebnis feststeht.

Aufgabe 9Ein Märchen schreiben

Schreib in sprachmodell/model.py die Funktion generate(counts, start, n). Die Prüfung gibt ihr Dictionaries, in denen jedes Wort nur einen Nachfolger hat, so dass das Ergebnis feststeht.

Ändere story.rb, so dass es drei Märchenanfänge schreibt:

counts = follow_counts(words(File.read("maerchen.txt")))3.times do  puts text(generate(counts, "Es", 30))  putsend

Ändere story.py, so dass es drei Märchenanfänge schreibt:

counts = follow_counts(words(open("maerchen.txt").read()))for _ in range(3):    print(text(generate(counts, "Es", 30)))    print()

Zum Beispiel:

Es trug sich um zu dem Richter, dann wollen wir uns doch der König in solche Blume, in einer kurzen Marsch haben will gleich nicht zu den sieben Zwerge

Ausprobieren: Lass das Programm ein paarmal laufen. Lies einen Text Wort für Wort: Passen zwei Wörter nebeneinander zusammen? Und lies ihn dann als Ganzes: Ergibt er einen Sinn?
Aufgabe 10Wie weit sieht das Modell zurück?

Wenn dein Modell das nächste Wort zieht: Was von dem bisher Geschriebenen berücksichtigt es?

Wie weit sieht das Modell zurück?

Der Mathematiker Claude Shannon hat 1948 genau solche Texte erzeugt, ohne Computer: Er schlug ein Buch an einer zufälligen Stelle auf, las bis zu dem Wort, das er zuletzt aufgeschrieben hatte, und schrieb das Wort dahinter auf. Dann blätterte er weiter zu einer anderen Stelle, suchte dort dieses Wort, und so weiter. Ein Buch würfelt so von selbst nach der Häufigkeit: Ein Wort, das oft folgt, findet man oft.

Schwarzweißporträt von Claude Shannon als junger Mann in Anzug und Krawatte.

Claude Shannon (1916–2001). Wie viele Bits eine Nachricht mindestens braucht, rechnete er 1948 aus und gründete damit die Informationstheorie; das Wort Bit machte er bekannt. Er fuhr Einrad, jonglierte und baute die Maus Theseus, die selbst lernte, durch ein Labyrinth zu finden.

Einen Code mit so wenigen Bits wie möglich suchten er und Robert Fano fast gleichzeitig, jeder für sich. Der Shannon-Fano-Code kam nah heran, aber nicht immer ganz.

In derselben Arbeit von 1948 ließ er englische Texte Wort für Wort entstehen, jedes Wort nach dem vorigen aus einem Buch gezogen: ein winziges Sprachmodell, von Hand.

Foto: Tekniska museet, CC BY 2.0, zugeschnitten (Wikimedia Commons)

A Mathematical Theory of Communication, Claude E. Shannon, Bell System Technical Journal, 1948. Der Aufsatz, mit dem die Informationstheorie beginnt; in Abschnitt 3 stehen englische Texte aus Buchstaben und aus Wörtern, jedes nach dem vorigen gezogen, wie bei dir: PDF, auch bei Wiley.

Gezählt hatte vor ihm schon Andrei Markow: 1913 ging er die ersten 20 000 Buchstaben von Puschkins Versroman »Eugen Onegin« durch und zählte, wie oft auf einen Vokal ein Vokal folgt und wie oft ein Konsonant. Eine Kette, in der jedes Glied nur vom vorigen abhängt, heißt seitdem Markow-Kette, und dein Modell ist eine Markow-Kette: Welches Wort es zieht, hängt nur vom Wort davor ab.

Andrei Markow 1886, eine alte Fotografie: ein junger Mann mit Vollbart und zurückgekämmtem Haar, in dunklem Anzug, leicht zur Seite blickend.

Andrei Markow (1856–1922), russischer Mathematiker in Sankt Petersburg. 1913 ging er die ersten 20 000 Buchstaben von Puschkins »Eugen Onegin« durch und zählte, wie oft auf einen Vokal ein Vokal folgt und wie oft ein Konsonant: Der nächste Buchstabe hängt vom vorigen ab, und mit dieser Abhängigkeit kann man rechnen.

Eine Kette, in der jedes Glied nur vom vorigen abhängt, heißt nach ihm Markow-Kette. Auch ein Sprachmodell aus Wortpaaren ist eine Markow-Kette.

Foto: unbekannt, 1886, gemeinfrei, zugeschnitten (Wikimedia Commons)

7. Mehr Kontext: zwei Wörter

Dein Modell schaut nur auf das letzte Wort. Was vorher stand, hat es vergessen. Schaut es auf die letzten zwei, weiß es mehr: Nach »sprach der« kommt ein anderes Wort als nach »Da sprach«. Der Kontext ist jetzt ein Paar von Wörtern, und als Schlüssel nimmt das Modell beide zusammen, mit einem Leerzeichen dazwischen: Unter "sprach der" steht, was nach »sprach der« kam.

Jetzt bist du dran: Schreib die Funktion pair_counts(tokens): wie follow_counts, aber der Schlüssel sind zwei Wörter hintereinander, mit einem Leerzeichen dazwischen, und gezählt wird das Wort danach.
Tipp

Der Index läuft jetzt nur bis zum drittletzten Wort. Der Schlüssel ist "#{tokens[i]} #{tokens[i + 1]}", der Nachfolger tokens[i + 2].

Lösung anzeigen
# Für jedes Paar von Wörtern: welche Wörter darauf folgen, und wie oft
def pair_counts(tokens)
  counts = {}
  (0...tokens.size - 2).each do |i|
    pair = "#{tokens[i]} #{tokens[i + 1]}"
    following = tokens[i + 2]
    counts[pair] ||= {}
    counts[pair][following] = (counts[pair][following] || 0) + 1
  end
  counts
end
Tipp

Der Index läuft jetzt nur bis zum drittletzten Wort. Der Schlüssel ist tokens[i] + " " + tokens[i + 1], der Nachfolger tokens[i + 2].

Lösung anzeigen
# Für jedes Paar von Wörtern: welche Wörter darauf folgen, und wie oft
def pair_counts(tokens):
    counts = {}
    for i in range(len(tokens) - 2):
        pair = tokens[i] + " " + tokens[i + 1]
        following = tokens[i + 2]
        if pair not in counts:
            counts[pair] = {}
        counts[pair][following] = counts[pair].get(following, 0) + 1
    return counts
Aufgabe 11Zwei Wörter Kontext

Schreib in sprachmodell/model.rb die Funktion pair_counts(tokens). Die Prüfung ruft sie mit kurzen Listen auf und vergleicht das Dictionary.

Aufgabe 11Zwei Wörter Kontext

Schreib in sprachmodell/model.py die Funktion pair_counts(tokens). Die Prüfung ruft sie mit kurzen Listen auf und vergleicht das Dictionary.

Vermutung 12Mit zwei Wörtern

Gleich schreibt dein Modell mit zwei Wörtern Kontext. Wie wird der Text?

Mit zwei Wörtern

Zum Schreiben braucht das Modell eine zweite Fassung von generate, die mit zwei Wörtern beginnt und jedes Mal die letzten beiden als Schlüssel nimmt:

# first, second und bis zu n gezogene Wörter danach, mit zwei Wörtern Kontext
def generate_pairs(counts, first, second, n)
  tokens = [first, second]
  n.times do
    following = next_word(counts, tokens.last(2).join(" "))
    break if following.nil?

    tokens << following
  end
  tokens
end

Und in story.rb:

pairs = pair_counts(words(File.read("maerchen.txt")))
3.times do
  puts text(generate_pairs(pairs, "Es", "war", 30))
  puts
end
# first, second und bis zu n gezogene Wörter danach, mit zwei Wörtern Kontext
def generate_pairs(counts, first, second, n):
    tokens = [first, second]
    for _ in range(n):
        following = next_word(counts, " ".join(tokens[-2:]))
        if following is None:
            break
        tokens.append(following)
    return tokens

Und in story.py:

pairs = pair_counts(words(open("maerchen.txt").read()))
for _ in range(3):
    print(text(generate_pairs(pairs, "Es", "war", 30)))
    print()

Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.

Es war da ganz finster und brannte auch kein Schiffchen, antwortete der Wolf wollte, ließ er sich auf den Herd und guckte nach dem Sperling. Aber der Sperling:

Je zwei, drei Wörter hintereinander klingen jetzt wie ein Märchen, ganze Satzstücke sogar. Nur ob das Ganze einen Sinn ergibt, weiß das Modell immer noch nicht.

Abgeschrieben?

Klingt der Text besser, weil das Modell mehr verstanden hat? Schau nach, wie viel Auswahl es noch hat. Hat ein Paar von Wörtern in den Märchen nur einen einzigen Nachfolger, kann das Modell nach diesem Paar nur genau das Wort schreiben, das im Märchen steht: Es schreibt ab.

Aufgabe 13Wie viel Auswahl bleibt?

Wie viel Prozent der Paare in pair_counts haben in den Märchen genau einen Nachfolger? Runde auf eine ganze Zahl.

%
Tipp

Ein Paar hat genau einen Nachfolger, wenn sein Dictionary nur einen Eintrag hat. Zähl diese Paare und teile durch die Zahl aller Paare.

Nächster Tipp

In Ruby: pairs.count { |pair, followers| followers.size == 1 }, in Python sum(1 for followers in pairs.values() if len(followers) == 1). Die Zahl aller Paare ist pairs.size oder len(pairs); mal 100.

Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.

Bei 78 von 100 Paaren gibt es nur einen Nachfolger, bei einzelnen Wörtern nur bei 55 von 100. Mit zwei Wörtern Kontext schreibt das Modell also meistens ab, was in den Märchen steht, und nur ab und zu biegt es an einem häufigen Paar wie »sprach der« in ein anderes Märchen ab. Mit drei Wörtern wäre es fast nur noch Abschreiben.

Das ist das Dilemma jedes Sprachmodells: Mehr Kontext macht den Text besser, aber jeder längere Kontext ist im Text seltener, und das Modell hat weniger, aus dem es lernen kann. Mit 80 000 Wörtern ist bei zwei Wörtern Schluss. Große Sprachmodelle lernen darum aus Billionen von Wörtern, und auch sie geben manchmal ganze Absätze aus ihren Texten wörtlich wieder.

8. Was das Modell aus seinem Text lernt

Dein Modell kennt nur die Märchen. Was dort oft steht, schreibt es oft; was dort nie steht, schreibt es nie. Es hat keine Meinung, aber es gibt die Meinungen seines Textes weiter.

Vermutung 14Was ist böse?

Was folgt in den Märchen am häufigsten auf »böse«? Vermute erst, dann schau mit top(counts, "böse", 4) nach.

Was ist böse?

Hier geht es weiter, sobald du deine Antwort oben abgegeben hast.

Auf »böse« folgen in den Märchen am häufigsten »Weib« und »Hexe«, je dreimal, dann »Stiefmutter« und »Buben«, je zweimal: vor allem Frauen. Von den 14 Malen, die »böse« in den Märchen steht, ist es achtmal eine Frau. Schreibt das Modell »böse«, schreibt es danach also meistens eine Frau. Die Märchen sind 200 Jahre alt, aber große Sprachmodelle lernen genauso aus dem, was Menschen geschrieben haben, und übernehmen, was darin häufig ist, auch Vorurteile. Wer ein solches Modell baut, muss nachsehen, was es gelernt hat, und entscheiden, was er dagegen tut.

Woher bekommt jemand, der ein großes Sprachmodell baut, Billionen von Wörtern? Wer hat diese Texte geschrieben, und hat jemand sie gefragt? Was gehört dir an einem Text, den du ins Netz gestellt hast, wenn ein Modell daraus lernt?
Dein Modell schreibt »Der Anführer war recht klug.«, in richtigem Deutsch, aber in keinem Märchen steht dieser Satz. Woran erkennt jemand, der so einen Satz liest, ob er stimmt? Und woran erkennt es das Modell?

9. Was ein großes Sprachmodell anders macht

Ein Chatbot rechnet auch nur aus, welches Wort als Nächstes kommt, und würfelt dann. Aber fast alles andere ist größer:

Dein Modell Ein großes Sprachmodell
Text zum Lernen 80 000 Wörter Märchen Billionen von Wörtern aus dem Netz, aus Büchern, aus Code
Kontext das letzte Wort oder die letzten zwei Hunderttausende Wörter, das ganze Gespräch bis jetzt
Was es sich merkt eine Tabelle: wie oft welches Wort folgte ein neuronales Netz mit Milliarden von Zahlen, die beim Training eingestellt werden
Ein unbekannter Kontext kein Nachfolger, Schluss ein Kontext, der ähnlichen Texten gleicht, bekommt ähnliche Nachfolger
Wörter ganze Wörter und Satzzeichen Tokens: häufige Wörter ganz, seltene in Stücken

Die Tabelle deines Modells kann für einen Kontext, der nie vorkam, nichts sagen. Ein neuronales Netz kann es, weil es nicht jeden Kontext einzeln speichert, sondern lernt, welche Kontexte einander ähneln. Gleich bleibt die Idee: aus vielen Texten lernen, wie es weitergeht, und dann Wort für Wort ziehen.

10. Dein eigener Text Extra

Ein Extra für Schnelle. Wer es auslässt, springt zu Zum Nachschlagen.

Dein Modell lernt aus jedem Text, nicht nur aus Märchen. Lade einen anderen herunter, etwa ein Buch von Project Gutenberg, oder nehmt als Klasse eure eigenen Texte, und ersetze maerchen.txt in story.rb (in Python story.py) durch die neue Datei.

Ausprobieren: Lass das Modell mit einem Text schreiben, der viel kürzer ist als die Märchen, und dann mit einem, der viel länger ist. Achte darauf, bei welchem es öfter wörtlich abschreibt.

11. Zum Nachschlagen