AdatHuszár

Gépi tanulás és minden jó

N-gram + Markov lánc = Szöveg generátor

2014. november 07. 01:00 - adathuszár

word-write-letterz-n-shit-yo.jpg

Write letterz n shit, yo!

A minap eszembe jutott, hogy régen olvastam egy angol srác blogján, hogy készített egy hülye kis szöveg generátort ami a "megtanult" szöveg után valószínűségi alapon képes volt mondatokat generálni. Nem volt benne nagy varázslat de egy kis játszásnak elmegy.

A módszer 2 részből áll:

1. N-gramok:

Az N-gram egy véges gyűjteménye hosszú dolgoknak. A dolog lehet betű, szó, mondat, protein szekvencia, stb. Első sorban szöveg feldolgozásnál használják és nagyobb szöveges dokumentumokból generálják le. Mindezek mellett hasznos még pl. helyesírás/kifejezés korrekcióknál, szöveg hasonlóság vizsgálatnál, stb.

Vegyük alapul a következő szöveget:

Hogyha lopnék kókuszdiót
Nem mondanám meg, hogy ki vót
A legnagyobb tragédia
Anyukámnak nincsen fia.

Ha n=2 és a szavak alapján akarjuk létrehozni, akkor 2-gramos (bigram) szekvenciát készítünk belőle ami nagyjából így nézne ki:

Hogyha lopnék

lopnék kókuszdiót

kókuszdiót Nem

Nem mondanám

...

Láthatjuk, hogy nincs benne semmi magic, egyszerűen végiglépkedünk kettesével a szöveg szavain (ha szavak alapján akarjuk generálni). 

Természetesen ha betűkkel szeretnénk dolgozni, akkor a harácsolás szóból a következő lesz 3-gramos (trigram) bontásnál:

har

ará

rác

ács

cso

sol

olá

lás

2. Markov láncok

Markov láncoknak hívjuk azokat a modelleket, ahol meghatározott állapotok között p(i) valószínűséggel történhet átmenet, ahol két állapot közötti átmenetet jelöli. Egy állapot akár a saját állapotát is megtarthatja bizonyos valószínűséggel, nem kötelező neki váltani. Egy státuszból induló állapotátmenetek valószínűségeinek összege mindig 1 (100%)

Egyszerű példa:

státus jelölje az evést, jelölje az alvást. (Igen, csak lusta voltam készíteni egy saját ábrát, helyette itt van ez. Örüljetek! :) ) Az ábra alapján ha eszem, 30% eséllyel folytatom tovább, 70% eséllyel elalszom utána. Amennyiben alszom, 40% eséllyel felkelek és eszek, 60% eséllyel alszom tovább. 

Forrás Cél Valószínűség
E E 30%
E A 70%
A A 40%
A E 60%

 

Hogy lesz ebből szöveg generátor?

Mivel egy szöveg generátort akarunk létrehozni ezért kell valami módszer arra, hogy ha kiválasztunk egy random kezdő szót akkor az algoritmusunk úgy pakolja utána a szavakat, hogy a végén valami "értelmes" jöjjön ki belőle. Az N-gramjaink segítségével képesek vagyunk betanítani az algoritmusunkat arra, hogy minden egyes szólánchoz eltárolja, hogy milyen szavak követték a forrás szövegben. Ezen gyűjteményből képesek leszünk valószínűségi alapon eldönteni, hogy milyen szó kövesse a szóláncunkat egy véletlen kiválasztással. Az így kialakított mondatok természetesen maximum pszeudo-értelmesek lesznek, hiszen tartalmilag nem fogunk sok kohéziót találni bennük, mindössze a forrás alapján megtanult szófordulatok fognak visszaköszönni ránk.

Bigramoknál egy nagyobb forrásszövegben biztosan többször is elő fog fordulni egy szópáros (pl.: "azt hogy", "nem tudom", "és akkor"). Ha az összes szólánchoz rögzítjük, hogy milyen szavak követték, akkor a következő adatszerkezethez hasonlót kapunk:

[("azt", "hogy"): ("ne", "soha", "jó", "ne"),

("nem", "tudom"): ("hogy", "hogy", "miért") 

...]

Mint látható, ha egy szó többször követi a szóláncunkat akkor azt ugyanúgy többször letároljuk, ezzel is növelve az esélyét annak, hogy azt a szót dobja nekünk az algoritmus következőnek. Minél nagyobb szöveget választunk ki forrásnak, annál több és változatosabb fordulatokat tanul meg. 

Érdemes kicsi értéket választani generáláshoz, különben nagyon sűrűn fog ismétlődni a szöveg. Nem valószínű, hogy túl sokszor fordul elő az "ó mily csodás teazsúr" kifejezés bármilyen szövegben. Minden ritka szóláncot szinte biztosan csak egy szó fog követni, ezzel megölve a generált szöveg változatosságát.

Lássuk a medvét

Írtam pythonban egy egyszerű kis alkalmazást ami a fenti logika alapján beolvas egy forrás szöveget, megtisztítja azt, legenerálja a megadott hosszúságú gramjainkat majd kiad általunk meghatározott hosszúságú mondatokat. (Aki nem szereti az angol nyelvet az tegye túl magát az angol kommenteken. Minden ami forráskód az csak angol nálam)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class ngramGenerator(object):

    def __init__(self, n):
        self.n = n
        self.words = list()
        self.ngrams = dict()

    def get_n(self):
        return self.n

    def get_ngrams(self):
        return self.ngrams

    def get_words(self):
        return self.words

    def read(self, file_path):
        file = open(file_path, 'r')
        lines = file.readlines()
        words = list()
        for line in lines:
            words.extend(line.split(' '))
        self.words = words

    def clear(self):
        if not len(self.words):
            raise IOError("No file was loaded. Use read(file)")
        cleaned_words = list()
        for word in self.words:
            word = word.lower()
            # Remove numbers and 'weird characters'
            word = re.sub(r'[\d,:;"()-_]+', '', word)
            # Remove apostrophes from beginning of words
            word = re.sub(r'([^\w]+|^)\'(\w+)', self.removeBeginningApostrophes, word)
            # Remove apostrophes from end of words
            word = re.sub(r'(\w+)\'([^\w]+|$)', self.removeEndingApostrophes, word)
            # Strip whitespace chars
            word = word.strip()
            if len(word):
                cleaned_words.append(word)
        self.words = cleaned_words

    def removeBeginningApostrophes(self, match):
        if not len(match.groups()):
            return match.group(0)
        return ' ' + match.group(1)

    def removeEndingApostrophes(self, match):
        if not len(match.groups()):
            return match.group(0)
        return match.group(1) + ' '

    def ngramize(self):
        for index in range(len(self.words) - self.n):
            ngram = tuple(self.words[index:index + self.n])
            next = self.words[index + self.n]
            if ngram not in self.ngrams:
                self.ngrams[ngram] = [next]
            else:
                self.ngrams[ngram].append(next)

Az ngramGenerator feladata, hogy beolvassa a forrásfájlt, eltávolítsa a zajt belőle majd legenerálja a szóláncainkat.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class markovGenerator(object):
    def __init__(self, words_to_generate, ngramGenerator):
        self.words_to_generate = words_to_generate
        self.ngram_generator = ngramGenerator

    def generate(self):
        from random import choice

        current = self.__get_random_ngram()
        output = list(current)
        for i in range(self.words_to_generate):
            if current in self.ngram_generator.get_ngrams():
                possibilities_next = self.ngram_generator.get_ngrams()[current]
                next = choice(possibilities_next)
                output.append(next)
                if re.match(r'[\w]+[.?!]', next) is not None:
                    current = self.__get_random_ngram()
                    output.append(' '.join(current))
                    continue
                current = tuple(output[-self.ngram_generator.get_n():])
            else:
                break

        return ' '.join(output)

    def __get_random_ngram(self):
        from random import randrange
        word_count = len(self.ngram_generator.get_words())
        word_index = randrange(word_count - self.ngram_generator.get_n())
        return tuple(self.ngram_generator.get_words()[word_index:word_index + self.ngram_generator.get_n()])

markovGenerator feladata maga a mondatok generálása.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ngram_generator = ngramGenerator(2)

file_path = 'koldus_es_kiralyfi'
ngram_generator.read(file_path)
ngram_generator.clear()
ngram_generator.ngramize()

markov_generator = markovGenerator(30, ngram_generator)
generated = markov_generator.generate()
print generated

A futtatásnál pedig a fenti 2 osztályt használom. Beolvasom a koldus_es_kiralyfi fájl tartalmát, megtisztítom a fölösleges karakterektől, majd legenerálom az N-gramokat. Ezt követően 30 szó hosszú "mondatokat" szeretnék létrehozni, amit a generate metódus ad vissza nekünk.

Konklúzió

A fenti kis gagyi példa egy egyszerű ki alkalmazás ami meglévő modelleket használ fel egy érdekes feladathoz. Próbáljátok ki különböző méretű forrás dokumentumokkal és nézzétek meg, hogy milyen mondatokat generál.

A forrást le tudod tölteni innen

Szólj hozzá!

A bejegyzés trackback címe:

https://adathuszar.blog.hu/api/trackback/id/tr536872653

Kommentek:

A hozzászólások a vonatkozó jogszabályok  értelmében felhasználói tartalomnak minősülnek, értük a szolgáltatás technikai  üzemeltetője semmilyen felelősséget nem vállal, azokat nem ellenőrzi. Kifogás esetén forduljon a blog szerkesztőjéhez. Részletek a  Felhasználási feltételekben és az adatvédelmi tájékoztatóban.

Nincsenek hozzászólások.
süti beállítások módosítása