Tweetando como Proust

Utilização de um modelo estatístico simples alimentado com a obra "Em busca do tempo perdido" para gerar Tweets com o estilo de Marcel Proust.

Há uns dias vi um notebook do @yaovgo sobre como um modelo linguístico usando apenas a probabilidade da ocorrência do próximo caractere pode gerar textos relativamente bons.

O nome dessa abordagem é “unsmoothed maximum-liklihood character level language models”. Ou seja, modelos linguísticos com granularidade de caractere (frases são montadas caractere por caractere), utilizando máxima-verossimilhança (o próximo caractere é aquele com a maior probabilidade, que foi calculada com o texto de treino) e não-suavizado, pois se determinada letra nunca aparece após determinada cadeia de caracteres a probabilidade que associamos à ela será zero.

Resumindo, decidimos uma janela, a ordem do modelo, que será quantos caracteres usaremos como base para levantar as probabilidades. Se esta ordem for de 4, então dividiremos o texto em cadeias de 4 caracteres e olharemos para o próximo. É feita uma contagem e depois normalizamos, assim, terminamos com uma probabilidade. Abaixo, o código quase idêntico ao exemplo base.

def train_char_lm(fname, order=4):
    data = file(fname).read()
    #dealing with encoding
    data = unicode(data, "utf-8")
    lm = defaultdict(Counter)
    pad = "~" * order
    data = pad + data
    for i in xrange(len(data)-order):
        history, char = data[i:i+order], data[i+order]
        lm[history][char]+=1
    def normalize(counter):
        s = float(sum(counter.values()))
        return [(c,cnt/s) for c,cnt in counter.iteritems()]
    outlm = {hist:normalize(chars) for hist, chars in lm.iteritems()}
    return outlm

Para treinar esse modelo, usaremos o texto de “Em busca do tempo perdido” de Marcel Proust. A idéia é conseguir gerar trechos baseados nos temas e escolhas de palavra do autor, mas que não sejam idênticos aos que aparecem na obra. Os dados e o código usado estão no github

Exemplificando a saída do modelo, vamos treiná-lo com uma janela de 4 caracteres e consultar o dicionário para “amig”. A saída será a seguinte:

o : 0.574675324675
a : 0.408441558442
u : 0.0116883116883
á : 0.00454545454545
  : 0.000649350649351

O que é consequência das palavras amigo(s), amiga(s), amiguinho(s), amigável e a estranha presença do termo “jamig”, que causou a presença do caractere “ “ (vazio) na lista.

Para gerar os textos, na verdade, não é utilizado o caractere mais provável, mas sim o primeiro da distribuição de probabilidades que tem probabilidade maior do que o acaso. A função original foi modificada para aceitar que entremos com uma cadeia inicial para gerar o texto. A palavra ou palavras da cadeia inicial devem estar contidas no texto de treino. Nesse caso:

print generate_text(lm, 4, nletters=300, history="Gord")

“Gordomo, de dogarto dia às que monóculos Verdurin passas imaginar a no uma japontava poder acabava-se que as se ela no prema essa,”


Muitas palavras estão corretas, mas as algumas acabam ficando erradas. Isso acontece por, por exemplo, com “Gordomo”, que a janela pequena faz com que o modelo opte por gerar a palavra “Mordomo” quando está enxergando apenas o “ordo”. Essa mudança de palavra no meio de uma palavra é ruim, isso é sequer seguir o contexto intra-palavra, se é que esse conceito existe.

A solução é aumentar a janela. Mas até quanto? Quando vamos aumentando a janela, a tendência é que as palavras saiam todas corretas, depois que elas comecem a interagir com o sentido de outras palavras próximas e por fim… copiar exatamente o texto de treino. Isso acontece pelo fato de que uma janela muito grande acaba tornando-se uma evidência muito especifica do conjunto de treino e fica inevitável que ele não se reproduza. Portanto, é preciso equilirar a ordem do modelo para escapar do underfitting e do overfitting. Um bom valor foi 15.


“Gordo; porém raspara os bigodes e bastara isso para fazer-me ir a alguma parte — continuou em voz baixa —, embora tenha eu pleno direito à minha liberdade. É certo que abdiquei dela por outra forma - acrescentou, testemunhando-lhe os seus sentimentos. Mas é uma criatura deliciosa, uma mulher sustentada, e por um velho tão orgulhoso com os aristocratas, afeiçoa-se a eles, que se mostram logo uns ingratos.”


E como essa saída se compara ao livro em si? Novamente, não queremos simplesmente reproduzir o texto, mas reorganizar de maneira lógica e que lembre o estilo do autor.

No texto original aparece:


“No entanto, era ele mesmo, apenas embranquecido e gordo; porém raspara os bigodes e bastara isso para fazê-lo perder sua personalidade.”

“— Naturalmente, quando me perseguem vinte vezes seguidas para fazer-me ir a alguma parte — continuou em voz baixa —, embora tenha eu pleno direito à minha liberdade, não posso em todo caso agir como um grosseirão.”

“E, não aceitando outro que eu lhe propusera, o senhor, sem querer, prestou-me um imenso serviço, deu-me a minha liberdade. É certo que abdiquei dela por outra forma - acrescentou num tom melancólico onde transparecia o desejo de fazer confidências -;”


Esse resultado parece conseguir misturar suficientemente bem cadeias do texto, mantém um contexto razoável e gera trecho razoável.

Agora precisamos fazer Proust caber em um tweet, o que talvez seria a parte que mais o desagradasse. Mais fácil seria imaginar uma rede social chamada “N’allez pas trop vite”, na qual os usuários tem no mínimo 5 mil caracteres para expressarem uma idéia.

def gen_tweet(lm, order, theme, max_iter=20):
    tweet = generate_text(lm, order, nletters=order+1, history=theme)
    iter = 0
    while len(tweet) > 140 and iter < max_iter and len(tweet) < 70:
        tweet = generate_text(lm, order, nletters=order+1, history=theme)
        iter += 1
    return tweet

Basicamente, vamos gerar trechos até que eles respeitem todas as condições: mais que 70 e menos que 140 caracteres e já com a imposição de terminar em um ponto final. Agora é só aproveitar o gerador para movimentar sua conta no twitter!


“…desse sentimento que não podemos considerá-lo como dantes.”

“É terrível ter a vida de outra pessoa ligada à nossa como uma bomba que não podemos largar sem cometer um crime.”


O que nos rendeu um profundo tweet:

E outros um pouco mais psicodélicos: