Sets en een taalmodel

Deze pagina bevat uitvoerbare code.

In het eerste college van deze week telde je met een dictionary hoe vaak elk woord in een lijst voorkomt. Nu stel je er twee nieuwe vragen over. Welke woorden komen erin voor, zonder ze te tellen? En welk woord volgt op welk woord? Met dat laatste bouw je een taalmodel dat zelf tekst maakt.

Sets

In het eerste college telde je de woorden in ["spam", "spam", "taart", "spam"] en kreeg je word_count = {"spam": 3, "taart": 1}. Het aantal verschillende woorden is het aantal sleutels: 2. Een dictionary is daar eigenlijk net iets te zwaar voor: hij houdt ook een aantal per woord bij, en dat heb je hier niet nodig. Voor “welke waarden komen voor, zonder duplicaten” is er een lichter datatype: de set.

list_of_words = ["spam", "spam", "taart", "spam"]
unique_words = set(list_of_words)
unique_words
{'spam', 'taart'}

Een set heeft geen volgorde en geen duplicaten. len(unique_words) is daarom 2, net zoveel als het aantal sleutels van word_count, maar dan zonder dat je een dictionary nodig had om het te tellen.

Python toont een set tussen accolades, en zo schrijf je hem ook zelf op: {"appel", "peer"}. Dat lijkt op een dictionary, maar er staan alleen waarden in, geen paren sleutel: waarde. Een lege set schrijf je als set(), want {} is een lege dictionary.

len(unique_words)
2

Zoeken gaat met in, net als bij een lijst of dictionary. En je breidt een set uit met .add():

"taart" in unique_words
True
unique_words.add("koekje")
unique_words
{'koekje', 'spam', 'taart'}

Unie en doorsnede

.add() breidt één set uit. Twee sets combineer je met unie en doorsnede - allebei zowel als methode als als operator:

groep_a = {"appel", "peer", "banaan"}
groep_b = {"appel", "kiwi", "banaan"}

De unie: alles wat in minstens één van de twee zit.

groep_a.union(groep_b)
{'appel', 'banaan', 'kiwi', 'peer'}
groep_a | groep_b
{'appel', 'banaan', 'kiwi', 'peer'}

De doorsnede: alles wat in allebei zit.

groep_a.intersection(groep_b)
{'appel', 'banaan'}
groep_a & groep_b
{'appel', 'banaan'}

Unie en doorsnede leveren een nieuwe set op, uit twee bestaande. Dat is iets anders dan .add(), dat er één set aan het uitbreiden was.

Woorden tellen in een tekst

Neem deze tekst:

text = "Ik wil taarten en 42 en spam. Ik krijg toch spam en taarten voor de vakantie? Ik wil 42 taarten!"

Dit is het tellen uit het eerste college, nu met .get en voor alle woorden van de tekst:

list_of_words = text.split()
word_count = {}

for word in list_of_words:
    word_count[word] = word_count.get(word, 0) + 1

print(f"Er zijn {len(word_count)} verschillende woorden")

word_count
Er zijn 13 verschillende woorden
{'Ik': 3,
 'wil': 2,
 'taarten': 2,
 'en': 3,
 '42': 2,
 'spam.': 1,
 'krijg': 1,
 'toch': 1,
 'spam': 1,
 'voor': 1,
 'de': 1,
 'vakantie?': 1,
 'taarten!': 1}

Van tellen naar opvolgers

Met een paar aanpassingen van het programma dat woorden telt, is het mogelijk om een eenvoudige taalgenerator te maken.

In plaats van het tellen van woorden wordt gekeken welk woord na een voorgaand woord wordt gebruikt.

Het programma staat al klaar in het bestand assets/markov.py. De regel %run assets/markov.py hieronder is een opdracht van Jupyter, geen Python: hij voert dat bestand uit. Daarna kun je de twee functies gebruiken die erin staan:

Functie

Doet

create_dictionary(text)

maakt het model van een tekst: welk woord volgt op welk woord

generate_text(words_follow, n)

maakt met dat model een nieuwe tekst van n woorden

In het werkcollege schrijf je deze functies zelf. In het model staat de sleutel "$" voor het begin van een zin: in zijn lijst staan de woorden waarmee een zin begint.

%run assets/markov.py
words_follow = create_dictionary(text)
words_follow
{'$': ['Ik', 'Ik', 'Ik'],
 'Ik': ['wil', 'krijg', 'wil'],
 'wil': ['taarten', '42'],
 'taarten': ['en', 'voor'],
 'en': ['42', 'spam.', 'taarten'],
 '42': ['en', 'taarten!'],
 'krijg': ['toch'],
 'toch': ['spam'],
 'spam': ['en'],
 'voor': ['de'],
 'de': ['vakantie?']}

Het stappenplan voor het maken van een taalmodel is als volgt:

  1. begin met het vorige woord previous_word als “$”

  2. voor elk volgend woord in de lijst van woorden voeg het toe aan …

  3. maak previous_word gelijk aan new_word

    • behalve als new_word[-1] punctuatie is, maak previous_word dan gelijk aan …

Tekst genereren

Een $ staat voor het begin van een zin. Dit model heeft voor elk woord een lijst van woorden waar uit gekozen kan worden. Als we dit random doen, wordt er een random tekst gegenereerd.

generate_text(words_follow, 42)
'Ik wil 42 en taarten en spam. Ik krijg toch spam en spam. Ik wil taarten en taarten voor de vakantie? Ik wil taarten en spam. Ik wil 42 en taarten en spam. Ik krijg toch spam en spam. Ik wil taarten '

Als we een veel grotere tekst gebruiken om onze database mee te vullen, kunnen we betere teksten genereren.

Het stappenplan voor het genereren van tekst is als volgt:

  1. begin met previous_word als de “$” string

  2. kies random een new_word dat volgt op previous_word en voeg het toe aan …

  3. maak previous_word gelijk aan new_word

    • behalve als new_word[-1] punctuatie is, maak dan previous_word gelijk aan …

Opdrachten

Opdracht 1

Dit is opdracht 1 uit het eerste college, nu voor sets. In de cel hieronder staan twee sets, s en t, met twee aanroepen erop.

Voorspel eerst voor elke aanroep of s erdoor verandert, en wat de aanroep teruggeeft:

Aanroep

Verandert s?

Wat komt er terug?

s.add("koekje")

s.union(t)

Voer daarna de cel hieronder uit en vergelijk de uitvoer met je voorspelling. Een set heeft geen volgorde, dus de volgorde van de woorden kan bij jou anders zijn.

s = {"spam", "taart"}
t = {"taart", "thee"}

print(s.add("koekje"))
print(s)
print(s.union(t))
print(s)

Opdracht 2

Van twee avonden is bijgehouden wie er kwam, in de sets monday en thursday in de cel hieronder.

Maak met unie en doorsnede twee sets: both, met wie op beide avonden kwam, en at_least_once, met wie minstens één keer kwam. De test eronder slaagt als ze kloppen.

Leg daarna in één zin uit waarom je hier een set gebruikt en geen dictionary. Je antwoord klopt als erin staat dat je per naam alleen wilt weten óf iemand er was, en dus geen waarde per naam nodig hebt.

monday = {"Anne", "Bram", "Cas", "Daan"}
thursday = {"Bram", "Daan", "Eva"}

# jouw oplossing
assert both == {"Bram", "Daan"}
assert at_least_once == {"Anne", "Bram", "Cas", "Daan", "Eva"}

Opdracht 3

Maak het model van deze tekst met de hand, zonder create_dictionary aan te roepen:

short_text = "De kat slaapt. De kat eet. De hond blaft."

Schrijf de dictionary letterlijk uit in my_model. De test eronder vergelijkt jouw model met wat create_dictionary ervan maakt. create_dictionary volgt deze regels:

Regel

In short_text

De tekst wordt op spaties gesplitst, dus een leesteken hoort bij het woord

slaapt. is een ander woord dan slaapt

Het eerste woord van de tekst volgt op "$"

"De" staat in de lijst van "$"

Een woord dat eindigt op ., ? of ! sluit de zin af: het woord daarna volgt weer op "$"

na slaapt. volgt "De" op "$", niet op "slaapt."

Een woord dat een zin afsluit, wordt dus geen sleutel

"slaapt." is geen sleutel

Volgt een woord vaker op hetzelfde woord, dan staat het vaker in de lijst

Hint

Loop de tekst woord voor woord langs en houd bij wat het vorige woord was. Kijk ter vergelijking naar words_follow hierboven: "spam." en "vakantie?" zijn daar geen sleutel, en de lijst van "$" is ["Ik", "Ik", "Ik"].

short_text = "De kat slaapt. De kat eet. De hond blaft."

# jouw oplossing
my_model = {}
assert my_model == create_dictionary(short_text)

Opdracht 4

Het model words_follow kan alleen woorden achter elkaar zetten die in de tekst ook zo achter elkaar stonden. Welke van deze zinnen kan het model maken?

  1. Ik wil 42 en spam.

  2. Ik krijg taarten!

  3. Ik krijg toch spam en 42 taarten!

Beslis het eerst door words_follow te lezen. Controleer het daarna met in: voor elk woord in de zin moet het volgende woord in de lijst van dat woord staan, en het eerste woord in de lijst van "$". Eén van de drie kan het model niet maken.

Let op: een leesteken hoort bij het woord. Bij zin 1 vraag je dus of "spam." op "en" kan volgen, niet "spam".

Hint

"wil" in words_follow["Ik"] zegt of wil op Ik kan volgen.

# jouw oplossing

Opdracht 5

In words_follow staat "wil" twee keer in de lijst van "Ik". Een set zou dat dubbele woord weglaten.

  1. Druk words_follow["Ik"] en set(words_follow["Ik"]) af.

  2. Het model kiest willekeurig een woord uit de lijst. Hoe groot is de kans dat het na Ik het woord wil kiest? En hoe groot zou die kans zijn als het uit de set koos?

  3. Leg in één zin uit waarom create_dictionary een lijst gebruikt en geen set.

Je antwoord bij 2 klopt als de twee kansen verschillen en de kans bij de lijst de grootste is. Je antwoord bij 3 klopt als erin staat dat de lijst bijhoudt hoe vaak een woord volgt, en dat een set dat verschil weggooit.

# jouw oplossing

Tot slot

In het werkcollege werk je dit uit tot een programma dat een heel essay genereert.