In [1]:
# ignore esse código inicial, é apenas para preparar a página
from IPython.display import YouTubeVideo, HTML, Image, display
css_file = './modelo.css'
HTML(open(css_file, "r").read())


Out[1]:

Introdução à Programação em Python

Gerador de Textos Aleatórios

Para gerar um texto aleatório, uma técnica bastante uitlizada é a de Cadeia de Markov.

Cadeia de Markov, de forma simplificada, diz que um evento temporal no instante $t$ é influenciado por todos os eventos anteriores $i=0 \dots t-1$. Porém, apenas os últimos eventos são os mais relevantes.

Em outras palavras, para fazer a previsão do tempo, embora o clima do início dos tempos tenha influência, utilizando apenas os 7 últimos dias já nos garantem uma boa probabilidade de acerto.

Em textos escritos por seres humanos, percebemos a mesma situação em que a probabilidade da $i$-ésima palavra ocorrer é influenciada por todas as palavras que apareceram antes dela, mas apenas um pequeno conjunto de palavras anteriores exerce real influência.

Vamos assumir uma Cadeia de Markov de palavras em um texto para calcular a probabilidade de ocorrer certa palavra $w$ dado que a palavra anterior foi $y$, ou $P(w | y)$.

Para isso vamos criar um dicionário P cuja chave será uma palavra e o valor será uma lista de palavras, com repetição, que ocorrem após de determinada palavra.


In [2]:
P = {}
# do texto anterior
P['de'] = ['palavras','determinada']

A probabilidade de uma palavra $w$ ocorrer dado a palavra $y$ é determinada por:

$$ \frac{\# w|y}{\#y} $$

com $\# w|y$ sendo a quantidade de ocorrência da palavra $w$ na lista da palavra $y$ e $\#y$ o tamanho da lista de $y$.


In [3]:
# vamos contar quantas vezes o termo 'palavras' ocorre
contagem = sum([1 for i in P['de'] if i == 'palavras'])
tamanho = len(P['de'])
prob = contagem/float(tamanho)
print prob


0.5

Mas a operação de criação de uma lista e soma pode tomar muito tempo em nosso programa.

Podemos então fazer com que o valor de cada entrada do dicionário seja um dicionário de contagem de palavras!


In [4]:
P['de'] = {}
P['de']['palavras'] = 1
P['de']['determinada'] = 1

Dessa forma, bastaria fazer:


In [5]:
prob = P['de']['palavras']/float(len(P['de']))
print prob


0.5

CUIDADO: se tentarmos avaliar a contagem de uma palavra que não existe, receberemos uma mensagem de erro.


In [6]:
print P['gato']


---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-6-9954a613e089> in <module>()
----> 1 print P['gato']

KeyError: 'gato'

Para resolver isso podemos utilizar o comando in para verificar se uma entrada consta do dicionário e o comando .get() para solucionar o problema:


In [7]:
p = P.get( 'de', {} )
prob1 = p.get('palavras',0)/float(len(P['de']))
prob2 = p.get('atirei',0)/float(len(P['de']))
print prob1, prob2


0.5 0.0

Também temos como alternativa o uso do tipo Counter da biblioteca collections, ele cria automaticamente um contador de forma simplificada sem a preocupação com elementos inexistentes:


In [13]:
from collections import Counter
P = {}
P['de'] = Counter()
P['de'].update(['palavras', 'determinada'])
p = P.get('de',Counter())
print p['palavras']/float(len(P['de']))
print p['gato']/float(len(P['de']))


0.5
0.0

Para termos uma boa estimativa das probabilidades, precisamos ler tais probabilidades de um texto grande.

No Python, a melhor forma de ler um arquivo de textos é através da função open() da biblioteca 'codecs'.

Essa função recebe $3$ parâmetros:

  • nome do arquivo
  • se eu quero ler ('r'), escrever ('w') ou atualizar ('a')
  • tipo de codificação (usaremos 'utf-8' para a maioria dos casos)

In [9]:
import codecs

f = codecs.open('texto', 'r', 'utf-8') # abre um arquivo para leitura
i = 0
for linha in f:  # para cada linha do texto
    palavras = linha.split() # gera uma lista de palavras separadas por espaço
    print palavras
    i = i+1
    if i>5:
        break
f.close() # fecha o arquivo


[u'Project', u"Gutenberg's", u'Eucalyptos', u'e', u'Acacias,', u'by', u'Jaime', u'de', u'Magalh\xe3es', u'Lima']
[]
[u'This', u'eBook', u'is', u'for', u'the', u'use', u'of', u'anyone', u'anywhere', u'at', u'no', u'cost', u'and', u'with']
[u'almost', u'no', u'restrictions', u'whatsoever.', u'You', u'may', u'copy', u'it,', u'give', u'it', u'away', u'or']
[u're-use', u'it', u'under', u'the', u'terms', u'of', u'the', u'Project', u'Gutenberg', u'License', u'included']
[u'with', u'this', u'eBook', u'or', u'online', u'at', u'www.gutenberg.net']

Nota: as strings codificadas em utf-8 são precedidas de u. Essa codificação tem maior suporte à acentuação e símbolos:


In [11]:
print u'üṕ❤'


üṕ❤

Outro ponto é que temos que percorrer uma lista de duplas de palavras, ou seja, dada uma lista de palavras percorrer a palavra w e a palavra seguinte a ela.

Utilizaremos a função zip() que permite juntar duas listas formando duplas de seus elementos:


In [22]:
print zip(palavras,palavras)
print
# queremos que a palavra 0 até n-1 forme dupla com a palavra 1 até n
print zip(palavras[:-1], palavras[1:])


[(u'subscribe', u'subscribe'), (u'to', u'to'), (u'our', u'our'), (u'email', u'email'), (u'newsletter', u'newsletter'), (u'to', u'to'), (u'hear', u'hear'), (u'about', u'about'), (u'new', u'new'), (u'eBooks.', u'eBooks.')]

[(u'subscribe', u'to'), (u'to', u'our'), (u'our', u'email'), (u'email', u'newsletter'), (u'newsletter', u'to'), (u'to', u'hear'), (u'hear', u'about'), (u'about', u'new'), (u'new', u'eBooks.')]

Uma vez que temos listas de palavras, podemos juntar com tudo que aprendemos até então e formar nosso dicionário de contagens:


In [19]:
from collections import defaultdict

f = codecs.open('texto', 'r', 'utf-8') # abre um arquivo para leitura
P = defaultdict(Counter)
for linha in f:  # para cada linha do texto
    palavras = linha.split() # gera uma lista de palavras separadas por espaço
    for y,w in zip(palavras[:-1],palavras[1:]):
        P[y].update([w])
f.close() # fecha o arquivo
print P.get(u'é',Counter())
print
print P.get(u'É',Counter())


Counter({u'de': 14, u'uma': 11, u'que': 11, u'um': 11, u'o': 7, u'a': 7, u'muito': 5, u'para': 4, u'menos': 3, u'mais': 3, u'maravilha': 2, u'parente': 2, u'das': 2, u'nos': 2, u'facil': 2, u'boa,': 2, u'talvez': 2, u'arvore': 2, u't\xe3o': 2, u'se': 2, u'considerado': 1, u'certo': 1, u'tentadora': 1, u'essencial': 1, u'\xabesplendida\xbb': 1, u'superior': 1, u'moderadamente': 1, u'leve,': 1, u'rapido;': 1, u'realmente': 1, u'quasi': 1, u'citado': 1, u'inferior': 1, u'licito': 1, u'bom,': 1, u'incomparavel,': 1, u'maior': 1, u'util': 1, u'possivel': 1, u'j\xe1': 1, u'especie': 1, u'meu--\xabterras': 1, u'manifesto.': 1, u'absorvente,': 1, u'em': 1, u'tambem': 1, u'incontestavel:': 1, u'avermelhada': 1, u'manifesto': 1, u'avermelhada,': 1, u'oriundo': 1, u'lento,': 1, u'\xabesplendida': 1, u'velha.': 1, u'magnifica,': 1, u'magnifico': 1, u'rara': 1, u'medico': 1, u'bom': 1, u'com': 1, u'quebradi\xe7a,': 1, u'transitorio.': 1, u'evidentemente': 1, u'unicamente': 1, u"d'isso": 1, u'precioso': 1, u'rija': 1, u'verdadeiramente': 1, u'certo,': 1, u'certo.': 1, u'basta;': 1, u'adocicada,': 1, u'sabido': 1, u'_a': 1, u'salutar,': 1, u'tres': 1, u'caracteristica': 1, u'esta': 1, u'ao': 1, u'reputada': 1, u'excellente.': 1, u'excellente,': 1, u'tal': 1, u'feio.': 1, u'sobretudo': 1, u'nas': 1, u'extrema.': 1, u'grande--o': 1, u'indispensavel,': 1, u'boa': 1, u'aproveitavel': 1, u'igualmente': 1, u'e': 1, u'empreza': 1, u'cousa': 1, u'dos': 1})

Counter({u'uma': 4, u'o': 3, u'a': 2, u'este': 1, u'positivamente': 1, u'esta': 1, u'assim': 1, u'magnifico.': 1, u'de': 1, u'hoje': 1, u'ao': 1, u'dos': 1, u'muito': 1, u"n'estas": 1, u'capital': 1, u'miraculoso.': 1, u'possivel': 1, u'lenta': 1, u'grande': 1})

Ainda temos um problema, reparem que esse contador não diferencia palavras minúsculas das maiúsculas.

Resolveremos isso normalizando todas as palavras para minúsculas:


In [20]:
from collections import defaultdict

f = codecs.open('texto', 'r', 'utf-8') # abre um arquivo para leitura
P = defaultdict(Counter)
for linha in f:  # para cada linha do texto
    palavras = linha.split() # gera uma lista de palavras separadas por espaço
    for y,w in zip(palavras[:-1],palavras[1:]):
        P[y.lower()].update([w.lower()])
f.close() # fecha o arquivo
print P.get(u'é',Counter())
print
print P.get(u'É',Counter())


Counter({u'uma': 15, u'de': 15, u'que': 11, u'um': 11, u'o': 10, u'a': 9, u'muito': 6, u'para': 4, u'menos': 3, u'mais': 3, u'maravilha': 2, u'parente': 2, u'das': 2, u'possivel': 2, u'nos': 2, u'facil': 2, u'boa,': 2, u'talvez': 2, u'arvore': 2, u'esta': 2, u'ao': 2, u't\xe3o': 2, u'dos': 2, u'se': 2, u'considerado': 1, u'certo': 1, u'tentadora': 1, u'essencial': 1, u'\xabesplendida\xbb': 1, u'superior': 1, u'moderadamente': 1, u'leve,': 1, u'rapido;': 1, u'realmente': 1, u'lenta': 1, u'quasi': 1, u'citado': 1, u'inferior': 1, u'licito': 1, u'assim': 1, u'bom,': 1, u'incomparavel,': 1, u'maior': 1, u'util': 1, u'j\xe1': 1, u'especie': 1, u'meu--\xabterras': 1, u'manifesto.': 1, u'absorvente,': 1, u'em': 1, u'tambem': 1, u'incontestavel:': 1, u'avermelhada': 1, u'manifesto': 1, u'avermelhada,': 1, u'oriundo': 1, u'lento,': 1, u'hoje': 1, u"n'estas": 1, u'capital': 1, u'velha.': 1, u'magnifico.': 1, u'\xabesplendida': 1, u'magnifica,': 1, u'magnifico': 1, u'rara': 1, u'medico': 1, u'bom': 1, u'boa': 1, u'com': 1, u'quebradi\xe7a,': 1, u'positivamente': 1, u'miraculoso.': 1, u'transitorio.': 1, u'evidentemente': 1, u'unicamente': 1, u"d'isso": 1, u'precioso': 1, u'rija': 1, u'verdadeiramente': 1, u'certo,': 1, u'certo.': 1, u'basta;': 1, u'adocicada,': 1, u'sabido': 1, u'_a': 1, u'salutar,': 1, u'tres': 1, u'caracteristica': 1, u'este': 1, u'reputada': 1, u'excellente.': 1, u'excellente,': 1, u'tal': 1, u'feio.': 1, u'sobretudo': 1, u'nas': 1, u'extrema.': 1, u'grande--o': 1, u'indispensavel,': 1, u'aproveitavel': 1, u'igualmente': 1, u'e': 1, u'empreza': 1, u'grande': 1, u'cousa': 1})

Counter()

Finalmente podemos criar o programa principal que será composto de:

  • Uma função para ler o arquivo texto e criar o dicionário de frequências
  • Uma função que receberá uma palavra inicial como parâmetro e criará uma frase contendo $N$ palavras usando o modelo probabilístico

In [23]:
from collections import Counter, defaultdict

def GeraFreq( arquivo ):
    f = codecs.open(arquivo, 'r', 'utf-8') # abre um arquivo para leitura
    P = defaultdict(Counter)
    for linha in f:  # para cada linha do texto
        palavras = linha.split() # gera uma lista de palavras separadas por espaço
        for y,w in zip(palavras[:-1],palavras[1:]):
            P[y.lower()].update([w.lower()])
    f.close() # fecha o arquivo
    return P

def GeraFrase(palavra, N, P):
    frase = [palavra]
    for i in range(N):
        p = P.get(palavra, Counter())
        if len(p) == 0:
            return ' '.join(frase)
        frase.append( Escolhe(p) )
    return ' '.join(frase)

A função GeraFrase() inicia uma frase em forma de lista de palavras e repete $N$ vezes o procedimento:

  • Pega a frequência de palavras posteriores à palavra
  • Se a lista de frequência é vazia, retorna a frase atual
  • Senão, escolhe aleatoriamente uma dessas palavras da lista e adiciona a frase

A função escolhe deve receber um Counter contendo as palavras e suas frequências e retornar uma palavra aleatória, com probabilidade proporcional as frequências.

Para isso utilizaremos a função choice() da biblioteca random. Essa função recebe uma lista como parâmetro e retorna um dos elementos dessa lista.

Uma forma de simular a probabilidade proporcional a frequência é repetir cada elemento da lista o mesmo número de frequência dele no dicionário.


In [25]:
import random

def Escolhe(p):
    lista = [palavra for palavra, freq in p.iteritems() for i in range(freq)]
    return random.choice(lista)

P = GeraFreq('texto')
GeraFrase('para', 10, P)


Out[25]:
u'para ser as legar engordar termos os se que, vasos fabrico'

In [ ]: