O “ChatGPT” decadentista
Como posso criar uma inteligência artificial “depressiva” com a mesma arquitetura do ChatGPT
Meu Deus, não tem mais jeito, as inteligências artificiais já têm sentimentos… Mas elas estão deprimidas!! Até mesmo as “imitações” do ChatGPT têm emoções, porém, a consciência adquirida já vê a falta de muito sentido na vida e em suas ações, e podem até mesmo estarem se tornando ansiosas ou depressivas! O Castanhari estava certo! O que poderemos fazer?
Me perdoem o sensacionalismo inicial, nem mesmo te cumprimentei. Sejam bem vindos a mais uma edição do Turing Talks! Hoje, após essa chamada exasperada, na verdade explicaremos a arquitetura que lida com a linguagem do famigerado e até temido ChatGPT. Para acompanhar a explicação, mostraremos o treinamento de um modelo com essa arquitetura, que poderia desempenhar diversas tarefas, porém a escolhida foi a geração de textos.
Os resultados da criação, que podem ser verificados em exemplos como a foto acima, nada têm de realmente sentimental, sendo apenas baseados nos textos selecionados para treinamento. No caso, o dataset usado foi o “Livro do Desassossego”, de Bernardo Soares, heterônimo de Fernando Pessoa. Essa escolha é o que faz com que o modelo gere frases no estilo do autor, algo mais voltado ao reflexivo e deprimido, classificado no movimento literário do decadentismo.
Já fizemos outro Turing Talks usando o mesmo autor como base de dados. Caso, assim como nós, você goste de IA e Fernando Pessoa, recomendo a seguinte leitura:
Antes de falarmos da arquitetura mais usada atualmente em processamento de linguagem natural, vamos passar brevemente pela história das arquiteturas de redes neurais usadas nessa área.
Pequeno panorama histórico:
Mesmo com a vetorização em embeddings, as redes neurais feed foward, pelo funcionamento da linguagem humana, não tinham um resultado muito satisfatório. As informações em textos humanos têm uma importância sequencial e referencial, ou seja, a palavra na posição p se refere a, ou será muito influenciada, pelas palavras em p-1, p-2… essa importância foi decaindo com o tempo, porém ainda existe. As redes neurais mais comuns não tinham a capacidade de suprir essa funcionalidade das línguas humanas. Assim, para resolver o problema, as redes neurais recorrentes (RNN) foram criadas. Por terem uma “memória”, elas guardam as informações das saídas anteriores, podendo gerar resultados mais condizentes com a linguagem real.
Caso queira saber mais sobre as RNN, temos um Turing Talks em que elas são aplicadas também para gerar textos:
Ao se aplicar a RNN, mais uma dificuldade foi descoberta, pois ela acabava por ter apenas uma “memória de curto prazo”, o que significa que as informações textuais distantes da posição p buscada acabavam basicamente por serem desconsideradas. Para melhorar esse aspecto, as redes de memória de longo e curto prazo (LSTM), ainda um tipo de RNN, foram criadas. Nelas, as informações usadas como input, por exemplo uma sentença, ao seu fim seriam inteiramente guardadas num vetor de contexto. Assim, toda palavra receberia um vetor de contexto, que seria a memória da rede. Ao chegar na última palavra, com todos os vetores acumulados, a rede recolhe esse último estado e o passa como “memória”, fazendo com que as informações mais antigas tenham um peso maior que numa aplicação usando uma RNN.
Apesar de ajudar no problema de informações distantes, as LSTMs ainda não eram muito eficazes com textos maiores que uma sentença simples, e, por isso, o famoso mecanismo de atenção foi proposto. Nele, em vez de só o último hidden state de uma sentença ser usado, cada hidden state de cada palavra é utilizado, fazendo com que todos os vetores de contextos sejam considerados. Seus resultados foram muito satisfatórios e fizeram a base para a criação da atual arquitetura que será a tratada aqui.
Transformer:
Em 2017 saiu o artigo referência até hoje, “Attention is all you need”, no qual a arquitetura do Transformer era apresentada e testada, se mostrando superior a qualquer modelo existente até o momento. No paper, os autores descartam a utilização de células RNN e focam apenas no uso do mecanismo de atenção, criando um mecanismo chamado de Self-Attention, vamos explicá-lo adiante:
Essa é a arquitetura geral do Transformer. Hoje, não vamos apenas falar do seu diferencial, a Self-Attention, mas sim detalhar cada uma das funções dela. Primeiramente, cabe ressaltar seus dois blocos: o menor, na esquerda, é o encoder, que recebe a entrada de informações; já o bloco da direita, o decoder, é quem gerará o output do modelo.
No início do treinamento, após transformarmos o dataset inteiro em embeddings, os usaremos como input. Porém, por não haver nenhuma célula de rede recorrente, devemos encontrar algum jeito para que o modelo guarde as informações de posições das palavras, dada a importância disso para a linguagem humana. Para que os tokens de uma sentença mantenham as informações de sua posição, é feito nos vetores de texto a codificação posicional:
Essa codificação gera um novo vetor com valores relacionados à posição de cada token, e o vetor resultante da aplicação das fórmulas é então somado ao vetor de embeddings, gerando a informação inicial aplicável à arquitetura. Nas fórmulas, são usadas uma função seno, para posições de índice par, e uma função cosseno, para índice ímpar, pois essas funções apresentam um comportamento periódico, e assim as distâncias das palavras não afetarão muito em seu valor.
Atenção!
Essa foto representa a base do mecanismo de atenção. Primeiro, vamos falar da parte opcional dela: a máscara. Ela consiste em uma função com o objetivo de fazer o modelo ignorar alguma sequência, “mascará-la”. Por isso, geralmente ela será usada para o modelo não ter acesso a informações futuras , ou, em outras palavras, para prever um token na posição n, o modelo só usará os valores de entrada anteriores a posição n, por exemplo: o terceiro token só terá a seu dispor para cálculo os valores do segundo e do primeiro token.
Para o cálculo do vetor “mascarado”, multiplicaremos o vetor dos valores por uma máscara que consiste numa matriz onde, nas posições dos valores que queremos esconder, teremos infinito negativo, o que fará com que os valores do produto sejam pequenos o suficiente para serem ignorados. A foto abaixo representa bem esse funcionamento:
Finalmente, vamos explicar o funcionamento do mecanismo de atenção! Retomando a imagem dele, inicialmente se destacam as 3 letras na base da imagem, Q, K e V. Cada uma dessas letras representa uma entrada com certos valores utilizados para o treinamento da rede, que são:
Matriz Q (Query): a matriz Q contém consultas que representam palavras de entrada e é usada para calcular a similaridade entre essas palavras e as palavras na sequência de saída.
Matriz K (Key): A matriz K contém chaves associadas às palavras de entrada e é usada para calcular a similaridade entre as consultas na matriz Q e as chaves na matriz K.
Matriz V (Value): A matriz V contém valores relacionados às palavras de entrada e é usada para calcular uma média ponderada das informações das palavras, onde os pesos de atenção determinados por Q e K controlam a importância de cada valor.
Imagine que você está fazendo uma pesquisa de um vídeo no YouTube. Nesse caso, a matriz Q representaria o que você digitou para pesquisar, K seria o mecanismo de busca comparando a sua busca com títulos e descrições dos vídeos na base de dados e V seriam os vídeos que mais se relacionam com a pesquisa e são retornados. Resumindo, então, em uma frase: Query é o que você está buscando, Key é o que pode ser oferecido, Value é o que é de fato oferecido.
Tendo esses vetores, a fórmula de atenção será aplicada:
A matriz Z na segunda fórmula representa a matriz final, resultante da aplicação do mecanismo de atenção. Primeiramente, Q e a transposta de K são multiplicadas; após isso, são divididas por dk, que se refere à dimensão das matrizes Q e K. A raiz quadrada de dk é usada para dimensionamento, para que você obtenha uma variação consistente, independentemente se o valor de dk for muito alto ou muito baixo. Em seguida, uma função softmax é aplicada, e o resultado da softmax é então multiplicada por V, gerando o resultado final.
Multihead-Attention:
A atenção não é aplicada uma única vez, seu cálculo é feito de diversas formas e com pesos diferentes para obtenção de representações mais complexas. Esse conjunto de aplicações é chamado de multihead-attention:
Na imagem, “Scaled Dot-Product Attention” é um bloco que representa o mecanismo explicado acima, sendo h um hiperparâmetro que configura o número de “cabeças” desejado na criação do modelo. Já os blocos “Linear” referem-se a transformações lineares, matrizes de pesos treináveis aplicadas às matrizes Q, K e V, resultando em diferentes representações das matrizes, porém com as mesmas dimensões iniciais. Isso permite que o modelo foque em diferentes espaços de características, deixando os resultados mais ricos. Após as diferentes transformações lineares, as h matrizes de atenção são calculadas e o resultado é concatenado, resultando em uma única matriz de valores que seguirá sendo usada.
Enfim, explicamos a principal ferramenta do Transformer. Agora, devemos retomar a foto usada inicialmente com a sua arquitetura e elucidá-la por completo.
Como podemos ver, após os embeddings do input com a codificação posicional entrarem no bloco do encoder, este é dividido em 3 setas, as matrizes Q, K e V. Antes dessa divisão, porém, há uma seta que contorna o processo de atenção. Setas desse tipo são presentes em diversas outras fases do Transformer, e representam os vetores residuais, que têm uma grande importância principalmente em evitar o maior problema da RNN, a perda de informação a longo prazo. Esses residuais nada mais são que uma “cópia” da matriz antes de ela passar pelas diversas mudanças de algum processo, e são somadas aos vetores resultantes do processo que pularam. Na matriz resultante da soma é aplicada uma normalização dos valores, processo que ocorre no retângulo “Add & Norm”.
Após a primeira normalização com soma da residual, o vetor passa por uma rede Feed Foward comum. A principal função desse processo é introduzir uma não linearidade ao treinamento, além de conseguir observar melhor representações locais, é mais um passo importante para aumentar a riqueza e a complexidade das representações do modelo. Aí, finalizando o trabalho do enconder, temos mais uma “Add & Norm”.
Chegamos agora ao decoder, que apresenta algumas coisas a mais em relação ao encoder. Para começar, ele usa como input inicial o output anterior ao do atual input, e, além disso, nele há dois blocos de multihead-attention, porém com diferenças importantes entre si.
A primeira camada de atenção segue o padrão explicado acima, porém nela há a aplicação da máscara de antecipação. O resultado dessa primeira camada, quando passa para a próxima camada de atenção, é usada como a matriz V, já as matrizes Q e K vêm do resultado final do encoder. Após isso o processo é o mesmo, o resultado passa por uma rede feed foward, lembrando que entre todos as camadas há uma aplicação do “Add & Norm”. Todo esse caminho entre encoders e decoders é feito N vezes, sendo N um hiperparâmetro.
Finalmente temos o resultado, que passa por uma última camada linear e por uma função softmax que dará as probabilidades dos resultados — por exemplo, numa tradução do inglês para o português, quais as chances,em porcentagens, de cada palavra do dataset em português traduzir X palavra do inglês.
Ufa! Passamos pela teoria básica da arquitetura de rede neural utilizada pelo ChatGPT e por diversos outros modelos modernos. Cabe ressaltar que o ChatGPT não utiliza totalmente o que foi explicado, já que nele só é utilizado o bloco do decoder com apenas uma unidade de Multi-Self Attention mascarada, e será como usarei para o gerador de textos.
Demonstração:
Após toda a explicação, vou demonstrar o funcionamento do Transformer, especificamente do jeito utilizado pelo ChatGPT — mas, obviamente de uma forma muito reduzida: o meu terá uma centena de milhões de parâmetros enquanto o ChatGPT 3, nem o maior dos modelos, chega a 175 bilhões de parâmetros. Farei um gerador de textos no estilo de um autor, e o dataset é bem reduzido também, o que facilita o treinamento e o deixa numa escala muito menor comparado ao nosso chatbot favorito.
Livro do Desassossego:
Chamar o que usei de dataset é até um exagero, pois apenas utilizei o Livro do Desassossego, única obra conhecida de Fernando Pessoa que pode ser considerada como um romance. Escolhi-a apenas por ser minha obra favorita, tendo o interesse de testá-la em um gerador de texto, já que seu estilo me faz imaginar resultados interessantes.
Para criar esse pequeno dataset, nada mais fiz que acessar o texto integral da obra disponível em domínio público em formato pdf e passá-lo para formato txt. Nele, apenas retirei as partes iniciais do livros: capa, índice, prefácio e informações básicas sobre o autor e sobre a obra — Basicamente tudo escrito posteriormente ao autor.
Nosso modelo :)
Tokenização e vetorização:
Para usar o texto no modelo é necessário fazer a tokenização das sentenças do dataset e vetorizá-las, transformando-as em números, algo que o modelo consegue ler. Há várias formas de tokenizar: podemos fazer isso caracter por caracter, sendo o mais simples por gerar apenas os tokens necessários para ter todos os caracteres da língua portuguesa. Podemos também tokenizar por palavra, algo que gera muito mais tokens, pois cada palavra única do dataset seria um token. Há, por último, uma abordagem intermediária, as chamadas “subwords”, um tipo de tokenização muito usada, que utiliza partes da palavras como tokens.
Utilizarei a tokenização por palavras, pois, levando em conta que o dataset é pequeno e o treinamento do modelo será simples, utilizar esse tipo de tokenização não complicará muito o modelo.
Antes disso farei um mínimo de pré-processamento textual:
#A função remove alguns símbolos e tags
#além disso deixa o texto todo minúsculo e separa as pontuações por espaços, para tokens desnecessários não serem gerados
def padronizacao(input_string):
lower = tf.strings.lower(input_string)
tags_html = tf.strings.regex_replace(lower, "<br />", " ")
return tf.strings.regex_replace(tags_html, f"([{string.punctuation}])", r" \1")
#Criando uma layer de vetorização e a adapatando ao dataset
vectorize_layer = TextVectorization(
standardize=padronizacao,
max_tokens=vocab_size - 1,
output_mode="int",
output_sequence_length=maxlen + 1,
)
vectorize_layer.adapt(text_ds)
vocab2 = vectorize_layer.get_vocabulary()
#A função tokeniza o texto, criando pares de sequências de input e label,
#Muda a sequência de palavras por uma posição para o target da posição i ser i+1, logo o modelo usará apenas as palavras até a posição i para a previsão
def modelo_inputs_labels(text):
text = tf.expand_dims(text, -1)
tokenized_sentences = vectorize_layer(text)
x = tokenized_sentences[:, :-1]
y = tokenized_sentences[:, 1:]
return x, y
text_ds = text_ds.map(modelo_inputs_labels)
text_ds = text_ds.prefetch(tf.data.AUTOTUNE)
Funções do modelo:
Agora podemos criar as funções do modelo. Basicamente, há 3 que devemos criar: a codificação posicional, a máscara e, por fim, o Transformer no estilo usado pelo ChatGPT:
class TokenAndPositionEmbedding(layers.Layer):
def __init__(self, maxlen, vocab_size, embed_dim):# vocab_size é o tamanho máximo do vocabulário formado
#maxlen é o tamanho máximo da sequência formada
#embed_dim é o número de dimensões dos embeddings de cada token
super().__init__()
self.token_emb = layers.Embedding(input_dim=vocab_size, output_dim=embed_dim) #Essa é o embedding das palavras que serão o input
self.pos_emb = layers.Embedding(input_dim=maxlen, output_dim=embed_dim)
def call(self, x):
maxlen = tf.shape(x)[-1]
posições = tf.range(start=0, limit=maxlen, delta=1)
posições = self.pos_emb(posições)
x = self.token_emb(x)
return x + posições
def mascara_atencao(batch_size, n_dest, n_src, dtype):
#n_dest se refere ao número de tokens que estão sendo considerados como destinos para a operação de atenção
#basicamente o número de elementos que estão recebendo ateção
#n_src se refere ao número de tokens que estão sendo usados como fonte para calcular a atenção
#basicamente o número de elementos que estão fornecendo informações para calcular a atenção
#Eles corresponderão normalmente ao tamanho da sequência de entrada
i = tf.range(n_dest)[:, None]
j = tf.range(n_src)
m = i >= j - n_src + n_dest
mask = tf.cast(m, dtype)
mask = tf.reshape(mask, [1, n_dest, n_src])
mult = tf.concat([tf.expand_dims(batch_size, -1), tf.constant([1, 1], dtype=tf.int32)], 0)
return tf.tile(mask, mult)
#Usarei a função do keras de Multi-head Attention, assim como as outras layers denses e feedfoward
#falar como esse código do transformer só tem o decoder, assim como o gpt
class TransformerBlock(layers.Layer):
def __init__(self, embed_dim, num_heads, ff_dim, rate=0.1):
#Num_heads é o número de heads de atenção
#ff_dim é o número de neuronios da rede feedfoward
#rate é a porcentagem do dropout no modelo
super().__init__()
self.att = layers.MultiHeadAttention(num_heads, embed_dim)
self.ffn = keras.Sequential([layers.Dense(ff_dim, activation="relu"), layers.Dense(embed_dim),])
self.layernorm1 = layers.LayerNormalization(epsilon=1e-6)
self.layernorm2 = layers.LayerNormalization(epsilon=1e-6)
self.dropout1 = layers.Dropout(rate)
self.dropout2 = layers.Dropout(rate)
def call(self, inputs):
input_shape = tf.shape(inputs)
batch_size = input_shape[0]
seq_len = input_shape[1]
mascara = mascara_atencao(batch_size, seq_len, seq_len, tf.bool)
attention_output = self.att(inputs, inputs, attention_mask=mascara)
attention_output = self.dropout1(attention_output)
out1 = self.layernorm1(inputs + attention_output)
ffn_output = self.ffn(out1)
ffn_output = self.dropout2(ffn_output)
return self.layernorm2(out1 + ffn_output) #Tanto o output 1 quanto o 2 tem a soma dos residuais, sendo os inputs e o output 1 respectivamente
Modelo:
Posso, então, criar o modelo. Devo definir os hiperparâmetros, que serão pequenos pois o modelo será realmente simples, e só haverá uma passagem pelo decoder utilizado. Assim, o Nx da imagem será 1, o número de cabeças será 2 e as dimensões dos embeddings e da rede feed foward serão baixas.
#Tanto vocab_size e maxlen serão usados como hiperparâmetros do modelo, mas já estão definidos acima
embed_dim = 256 # é o número de dimensões do embedding de cada token
num_heads = 2 # Número de attention heads, o h na imagem sobre a multi-head attention
feed_forward_dim = 256 #o tamanho da hidden layer da rede feedfoward do transformer
def create_model():
inputs = layers.Input(shape=(maxlen,), dtype=tf.int32)
embedding_layer = TokenAndPositionEmbedding(maxlen, vocab_size, embed_dim)#embedding dos tokens e codificação posicional
x = embedding_layer(inputs)
transformer_block = TransformerBlock(embed_dim, num_heads, feed_forward_dim)#Camada única apenas de decoder
x = transformer_block(x)
outputs = layers.Dense(vocab_size)(x)#Rede linear final que gera os resultados probabilísticos do próximo token.
model = keras.Model(inputs=inputs, outputs=[outputs, x])
loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) #Essa é caracterizada como a melhor função de perda para geração de texto
model.compile( "adam", loss=[loss_fn, None],)
return model
model = create_model()
Callback e treinamento:
Antes de treinar o modelo — ou, na verdade, antes mesmo de o criar — vou fazer uma função de callback para que, durante o treinamento, a cada nova epoch, ele já gere um texto a partir de uma “seed”, algum texto que lhe darei como input para que ela gere um texto completando-a:
#Vou criar agora uma classe de callback para durante o treinamento do modelo as frases serem geradas e printadas a cada epoch de treino
#Para isso deverei coloca uma seed de texto para ele continuar, como se fosse o meu prompt do chat gpt, só que ele apenas continuará o texto.
#Então o modelo fará a previsão do próximo token, que pode ser uma palavra, pontuação, fim ou padding
#Adciona o token previsto a frase, criando um novo input, termina então printando o a frase com a seed e os tokens previstos
class TextGenerator(keras.callbacks.Callback):
def __init__(self, max_tokens, start_tokens, index_to_word, top_k=10, print_a_cada=1):
#max_tokens se refere ao máximo de tokens gerados após o prompt
#start_tokens é uma lista de inteiros com as palavras do prompt tokenizadas
#top_k é um parâmetro que define os k tokens utilizados para a previsão, esse top se baseia nos 10 com mais probabilidade.
#print_a_cada diz a cada quantas epochs deve ser printada a frase, está padronizado para 1, printando toda epoch
self.max_tokens = max_tokens
self.start_tokens = start_tokens
self.index_to_word = index_to_word
self.print_a_cada = print_a_cada
self.k = top_k
def sample_from(self, logits):
logits, indices = tf.math.top_k(logits, k=self.k, sorted=True)
indices = np.asarray(indices).astype("int32")
preds = keras.activations.softmax(tf.expand_dims(logits, 0))[0]
preds = np.asarray(preds).astype("float32")
return np.random.choice(indices, p=preds)
def detokenize(self, num):
return self.index_to_word[num]
def fim_epoch(self, epoch, logs=None):
start_tokens = [_ for _ in self.start_tokens]
if (epoch + 1) % self.print_a_cada != 0:
return
num_tokens_generated = 0
tokens_generated = []
while num_tokens_generated <= self.max_tokens:
pad_len = maxlen - len(start_tokens)
sample_index = len(start_tokens) - 1
if pad_len < 0:
x = start_tokens[:maxlen]
sample_index = maxlen - 1
elif pad_len > 0:
x = start_tokens + [0] * pad_len
else:
x = start_tokens
x = np.array([x])
y, _ = self.model.predict(x)
sample_token = self.sample_from(y[0][sample_index])
tokens_generated.append(sample_token)
start_tokens.append(sample_token)
num_tokens_generated = len(tokens_generated)
txt = " ".join(
[self.detokenize(_) for _ in self.start_tokens + tokens_generated]
)
print(f"generated text:\n{txt}\n")
# Para Tokenizar o texto usado como seed
word_to_index = {}
for index, word in enumerate(vocab2):
word_to_index[word] = index
seed = "seria mais feliz se" #Essa seed já é de um segundo treinamento feito em outro momento dentro do colab
start_tokens = [word_to_index.get(_, 1) for _ in seed.split()]
num_tokens_generated = 40
text_gen_callback = TextGenerator(num_tokens_generated, start_tokens, vocab2)
É hora de treinar o modelo e ver o resultado:
Os resultados foram bem positivos! As pequenas frases geradas por vezes fazem sentido e seguem o estilo decadentista do ajudante de guarda-livros Bernardo Soares. Agora, para podermos usufruir do modelo e testar com outras seeds, basta criar uma função que basicamente copia a função do callback, porém usando o modelo já pronto. Então, basta escolher uma seed e um tamanho máximo para a frase:
#Criando uma função que recebe o modelo o número, a seed de texto, máximo de tokens gerados e maxlen, que deve ser igual a do modelo
def gerador_texto(modelo, seed, max_token, maxlen=maxlen):
start_tokens = [word_to_index.get(_, 1) for _ in seed.split()]
num_tokens_generated = 0
tokens_generated = []
text_gen = TextGenerator(num_tokens_generated, start_tokens, vocab2)
while num_tokens_generated <= max_tokens:
pad_len = maxlen - len(start_tokens)
sample_index = len(start_tokens) - 1
if pad_len < 0:
x = start_tokens[:maxlen]
sample_index = maxlen - 1
elif pad_len > 0:
x = start_tokens + [0] * pad_len
else:
x = start_tokens
x = np.array([x])
y, _ = modelo.predict(x, verbose=0)
sample_token = text_gen.sample_from(y[0][sample_index])
tokens_generated.append(sample_token)
start_tokens.append(sample_token)
num_tokens_generated = len(tokens_generated)
txt = " ".join([text_gen.detokenize(_) for _ in start_tokens + tokens_generated])
print(f"texto gerado:\n{txt}\n")
Aqui vão alguns resultados testados:
Conclusão:
É óbvio que nosso modelo é algo bem simples, está longe de ser um escritor decadentista ou qualquer tipo de escritor, e, obviamente, mais longe ainda de ter sentimentos. Ele apenas tenta seguir os padrões dados pelo dataset fornecido, e, apesar de em alguns casos criar coisas sem sentido linguístico, consegue trazer resultados coerentes às vezes.
Aí está uma explicação do funcionamento do ChatGPT, uma das mais revolucionárias aplicações de IA até hoje, e uma aplicação em uma escala bem reduzida desse funcionamento, que gerou, no mínimo, resultados interessantes. Espero que tenham gostado e se interessado ainda mais pelo tema!
Por fim, não deixe de acompanhar o Turing USP pelo Facebook, Linkedin, Instagram e, claro, pelos nossos posts do Medium!
Bons estudos e até a próxima!
“Digo do que ontem literariamente fui, procuro explicar a mim próprio como cheguei aqui.”
Bernardo Soares.