LSTMs para Generar Texto en PyTorch

16 minute read

Generación Automática de Texto en PyTorch

En este notebook, construiremos un LSTM (Long Short Term Memory) en PyTorch para generar texto de manera automática. Esta red utilizará un texto para entrenamiento y procesará este texto cáracter por cáracter, aprederá de él y como resultado generará texto de la misma manera. Para esto usaremos el Premio Nobel de la Literatura: Cien años de Soledad del gran Gabriel García Márquez para entrenar nuestra red. Para ejecutar este notebook, usé Google Colab – que es básicamente un Jupyter notebook que puede ser ejecutado en el navegador (Chrome). Colab requiere un poco más de trabajo inicial (set-up) pero ofrece el uso gratuito de un GPU. Algo que realmente vale la pena y recomiendo ser usado.

Importemos los módulos necesarios para cargar los datos y generar nuestro modelo.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

import torch
from torch import nn
import torch.nn.functional as F

# Cargar el archivo y leerlo como texto
with open ('/content/gabo_soledad.txt', 'r') as f:
    texto = f.read()
# Leer los primeros 100 cáracteres
texto[:100]
'Gabriel García Márquez \nCien años de soledad \nMuchos años después, frente al pelotón de fusilamiento'
# Codificar el texto y asignar a cada cáracter un número entero y viceversa

# Crear dos dictionarios
# 1- int2char, que asigna un número entero a un cáracter
# 2- char2int, que asigna un cáracter a un número entero único
chars = tuple(set(texto))
int2char = dict(enumerate(chars))
char2int = {ch: ii for ii, ch in int2char.items()}

# encode the text
codificado = np.array([char2int[ch] for ch in texto])
codificado[:100]
array([31, 37, 65, 41, 51, 82, 23, 46, 31, 37, 41, 53, 67, 37, 46, 35, 14,
       41,  2, 55, 82, 42, 46, 84, 45, 51, 82, 66, 46, 37,  7, 19, 64, 46,
       73, 82, 46, 64, 19, 23, 82, 73, 37, 73, 46, 84, 35, 55, 53, 72, 19,
       64, 46, 37,  7, 19, 64, 46, 73, 82, 64, 78, 55, 71, 64, 28, 46, 81,
       41, 82, 66, 75, 82, 46, 37, 23, 46, 78, 82, 23, 19, 75, 10, 66, 46,
       73, 82, 46, 81, 55, 64, 51, 23, 37, 26, 51, 82, 66, 75, 19])
# Pre-procesamiento de Data

def one_hot_encode(arr, n_labels):
    # Iniciar el array codificado
    one_hot = np.zeros((np.multiply(* arr.shape), n_labels), dtype = np.float32)
    
    # Rellenar los elementos apropiados con el número uno
    one_hot[np.arange(one_hot.shape[0]), arr.flatten()] = 1.
    
    # Regresar el array a su forma original
    one_hot = one_hot.reshape((*arr.shape, n_labels))
    
    return one_hot
# Poner a prueba la función
test_seq = np.array([[3, 5, 1]])
one_hot = one_hot_encode(test_seq, 8)

print(one_hot)
[[[0. 0. 0. 1. 0. 0. 0. 0.]
  [0. 0. 0. 0. 0. 1. 0. 0.]
  [0. 1. 0. 0. 0. 0. 0. 0.]]]
def get_batches(arr, batch_size, seq_length):
    """
      Crear un generador que regresa lotes/batches de size
      batch_size x seq_length del arr.

      Argumentos:
      arr -- array del cual vamos a hacer batches
      batch_size -- Batch size, el número de sequencias por batch
      seq_length -- Número de cáracteres codificados en la sequencia
    """
    
    batch_size_total = batch_size * seq_length
    # Número Total de batches que podemos hacer:
    n_batches = len(arr)//batch_size_total
    
    # Mantener solamento los cáracteres que son suficientes para hacer un batch completo
    arr = arr[:n_batches * batch_size_total]
    # Cambiar la forma a batch_size rows
    arr = arr.reshape((batch_size, -1))
    
    # Iterar a través del array, una sequencia a la vez
    for n in range (0, arr.shape[1], seq_length):
        # Cáracteristicas
        x = arr[:, n:n+seq_length]
        # Objetivos desplazados por uno
        y = np.zeros_like(x)
        try:
            y[:, :-1], y[:, -1] = x[:, 1:], arr[:, n+seq_length]
        except IndexError:
            y[:, :-1], y[:, -1] = x[:, 1:], arr[:, 0]
        yield x,y
      
# Poner a prueba la implementación
batches = get_batches(codificado, 8, 50)
x, y = next(batches)
# Mostrar los primeros 10 elementos de la sequencia
print('x\n', x[:10, :10])
print('\ny\n', y[:10, :10])
x
 [[31 37 65 41 51 82 23 46 31 37]
 [66 51 53 37 46 75 82 10 41 51]
 [64 55 64 46 78 41 19 53 23 37]
 [78 37 64 37 73 19 28 46 77 41]
 [73 19 46 84 78 41 51 26 19 41]
 [66 81 37 66 53 51 37 46 51 66]
 [46 53 51 82 23 19 46 82 66 46]
 [66 73 19 46 55 66 46 66 51  7]]

y
 [[37 65 41 51 82 23 46 31 37 41]
 [51 53 37 46 75 82 10 41 51 53]
 [55 64 46 78 41 19 53 23 37 26]
 [37 64 37 73 19 28 46 77 41 64]
 [19 46 84 78 41 51 26 19 41 19]
 [81 37 66 53 51 37 46 51 66 86]
 [53 51 82 23 19 46 82 66 46 55]
 [73 19 46 55 66 46 66 51  7 19]]
# Verificar si tenemos GPU disponible

entrenar_en_gpu = torch.cuda.is_available()
if (entrenar_en_gpu):
    print ('Entrenando en GPU')
else:
    print('No hay GPU disponible :( -- entrenando en CPU')
Entrenando en GPU
class CharRNN(nn.Module):
    
    def __init__(self, tokens, n_hidden=256, n_layers=2,
                               drop_prob=0.5, lr=0.001):
        super().__init__()
        
        """
          Definir la arquitectura de nuestra red.
          
          Argumentos:
          n_hidden: Número de capas escondidas
          n_layers: Número de capas
          drop_prob: Dropout layer para prevenir overfitting
          lr = Learning Rate/ Velocidad de apredizaje
        """
        
        self.drop_prob = drop_prob
        self.n_layers = n_layers
        self.n_hidden = n_hidden
        self.lr = lr
        
        # creating character dictionaries
        self.chars = tokens
        self.int2char = dict(enumerate(self.chars))
        self.char2int = {ch: ii for ii, ch in self.int2char.items()}
        
        ## TODO: define the LSTM
        self.lstm = nn.LSTM(len(self.chars), n_hidden, n_layers, 
                            dropout=drop_prob, batch_first=True)
        
        ## TODO: define a dropout layer
        self.dropout = nn.Dropout(drop_prob)
        
        ## TODO: define the final, fully-connected output layer
        self.fc = nn.Linear(n_hidden, len(self.chars))
      
    
    def forward(self, x, hidden):
        ''' Avanzar a través de la red
            Los inputs son x, y el estado de hidden/cell `hidden`. '''
                
        # Obtener los outputs del nuevo estado de hidden del LSTM
        r_output, hidden = self.lstm(x, hidden)
        
        # Avanzar a travês del la capa de dropout
        out = self.dropout(r_output)
        
        # Apilar los outputs del LSTM usando 'view'
        # y 'contiguous' para cambiar la forma del output
        out = out.contiguous().view(-1, self.n_hidden)
        
        # Pasar x a travês de la capa fully-connected
        out = self.fc(out)
        
        # Regresar el output final y el estado de hidden
        return out, hidden
    
    
    def init_hidden(self, batch_size):
        ''' Inicia el estado de hidden'''
        # Crear dos Tensors nuevos con los sizes de n_layers x batch_size x n_hidden,
        # iniciar en zero, para el estado de hidden y cell del LSTM
        weight = next(self.parameters()).data
        
        if (entrenar_en_gpu):
            hidden = (weight.new(self.n_layers, batch_size, self.n_hidden).zero_().cuda(),
                      weight.new(self.n_layers, batch_size, self.n_hidden).zero_().cuda())
        else:
            hidden = (weight.new(self.n_layers, batch_size, self.n_hidden).zero_(),
                      weight.new(self.n_layers, batch_size, self.n_hidden).zero_())
        
        return hidden

Hora de Entrenar!

La función ‘train’ (entrenar) nos da la opción de definir el número de epochs, la velocidad de aprendizaje, y otros parámetros.

Utlizaremos el Adam Optimizer y Cross Entropy Loss. Calcularemos la perdida y haremos retro-propagación.

Algunos detalles a considerar:

  • En el loop del lote, separaremos el estado escondido de su historia y lo modificaremos para que su valor sea igual a una variable en un nuevo tuple por que el LSTM tiene un estado escondido que es un tuple con los valores de los estados de escondido en cells.
  • Usaremos clip_grad_norm para prevenir que los gradients ‘exploten’.
def train(net, data, epochs=10, batch_size=10, seq_length=50, lr=0.001, clip=5, val_frac=0.1, print_every=10):
    ''' Entrenar la red
    
        Argumentos:
            
        net: Red CharRNN 
        data: Texto usado para entrenar la red
        epochs: Número de epochs a entrenar
        batch_size: Número de mini-sequencias por mini-batch
        seq_length: Número de pasos de cáracter por mini-batch
        lr: Velocidad de aprendizaje
        clip: Recorte de gradient
        val_frac: Fracción de datos que seran separados para usar en validación
        print_every: Número de pasos en los que mostraremos los valores de pérdida en entrnamiento y validación
    
    '''
    net.train()
    
    opt = torch.optim.Adam(net.parameters(), lr=lr)
    criterion = nn.CrossEntropyLoss()
    
    # Crear datos para entranmiento y validación
    val_idx = int(len(data)*(1-val_frac))
    data, val_data = data[:val_idx], data[val_idx:]
    
    if(entrenar_en_gpu):
        net.cuda()
    
    counter = 0
    n_chars = len(net.chars)
    for e in range(epochs):
        # Inicial el estado de hidden
        h = net.init_hidden(batch_size)
        
        for x, y in get_batches(data, batch_size, seq_length):
            counter += 1
            
            # Codificar One-hot los datos y convertirlos en Torch tensors
            x = one_hot_encode(x, n_chars)
            inputs, targets = torch.from_numpy(x), torch.from_numpy(y)
            
            if(entrenar_en_gpu):
                inputs, targets = inputs.cuda(), targets.cuda()

            # Crear nuevas variables para el hidden state, de lo contrario
            # retro-propagaremos toda la historia de entrenamiento
            h = tuple([each.data for each in h])

            # Gradients acumuladas en zero
            net.zero_grad()
            
            # Obtener el output del modelo
            output, h = net(inputs, h)
            
            # Calcular la pérdida y realizar retro-propogación
            loss = criterion(output, targets.view(batch_size*seq_length))
            loss.backward()
            # `clip_grad_norm` nos ayuda a prevenir el problema de 'exploding gradient' típico en RNNs / LSTMs.
            nn.utils.clip_grad_norm_(net.parameters(), clip)
            opt.step()
            
            # Estadísticas en Pérdidas
            if counter % print_every == 0:
                # Obtener la pérdida en validación
                val_h = net.init_hidden(batch_size)
                val_losses = []
                net.eval()
                for x, y in get_batches(val_data, batch_size, seq_length):
                    # Codificar One-hot encode los datos y convertirlos en Torch tensors
                    x = one_hot_encode(x, n_chars)
                    x, y = torch.from_numpy(x), torch.from_numpy(y)
                    
                    # Crear nuevas variables para el hidden state, al contrario
                    # retro-propagaremos toda la historia de entrenamiento
                    val_h = tuple([each.data for each in val_h])
                    
                    inputs, targets = x, y
                    if(entrenar_en_gpu):
                        inputs, targets = inputs.cuda(), targets.cuda()

                    output, val_h = net(inputs, val_h)
                    val_loss = criterion(output, targets.view(batch_size*seq_length))
                
                    val_losses.append(val_loss.item())
                
                net.train() # Cambiar a train mode despues de iterar a través de los datos de  validación                
                print("Epoch: {}/{}...".format(e+1, epochs),
                      "Step: {}...".format(counter),
                      "Loss: {:.4f}...".format(loss.item()),
                      "Val Loss: {:.4f}".format(np.mean(val_losses)))
# Definir y mostrar la red
n_hidden = 512
n_layers = 2

net = CharRNN(chars, n_hidden, n_layers)
print(net)
CharRNN(
  (lstm): LSTM(88, 512, num_layers=2, batch_first=True, dropout=0.5)
  (dropout): Dropout(p=0.5)
  (fc): Linear(in_features=512, out_features=88, bias=True)
)
batch_size = 128
seq_length = 100
n_epochs = 20 # Empezar con pocos epochs si solo queremos  comprobar el comportamiento de la red 

# Entrenar el modelo
train(net, codificado, epochs=n_epochs, batch_size=batch_size, seq_length=seq_length, lr=0.001, print_every=10)
Epoch: 1/20... Step: 10... Loss: 3.1425... Val Loss: 3.0601
Epoch: 1/20... Step: 20... Loss: 3.0525... Val Loss: 2.9922
Epoch: 1/20... Step: 30... Loss: 3.0246... Val Loss: 2.9815
Epoch: 1/20... Step: 40... Loss: 3.0168... Val Loss: 2.9799
Epoch: 1/20... Step: 50... Loss: 3.0222... Val Loss: 2.9760
Epoch: 2/20... Step: 60... Loss: 3.0107... Val Loss: 2.9747
Epoch: 2/20... Step: 70... Loss: 2.9995... Val Loss: 2.9712
Epoch: 2/20... Step: 80... Loss: 2.9981... Val Loss: 2.9634
Epoch: 2/20... Step: 90... Loss: 2.9897... Val Loss: 2.9458
Epoch: 2/20... Step: 100... Loss: 2.9237... Val Loss: 2.9066
Epoch: 2/20... Step: 110... Loss: 2.8630... Val Loss: 2.8131
Epoch: 3/20... Step: 120... Loss: 2.7786... Val Loss: 2.7101
Epoch: 3/20... Step: 130... Loss: 2.6624... Val Loss: 2.5798
Epoch: 3/20... Step: 140... Loss: 2.5254... Val Loss: 2.4647
Epoch: 3/20... Step: 150... Loss: 2.4574... Val Loss: 2.3905
Epoch: 3/20... Step: 160... Loss: 2.3795... Val Loss: 2.3302
Epoch: 3/20... Step: 170... Loss: 2.3533... Val Loss: 2.2900
Epoch: 4/20... Step: 180... Loss: 2.3228... Val Loss: 2.2581
Epoch: 4/20... Step: 190... Loss: 2.3195... Val Loss: 2.2505
Epoch: 4/20... Step: 200... Loss: 2.2577... Val Loss: 2.2125
Epoch: 4/20... Step: 210... Loss: 2.2460... Val Loss: 2.1785
Epoch: 4/20... Step: 220... Loss: 2.2113... Val Loss: 2.1514
Epoch: 5/20... Step: 230... Loss: 2.1920... Val Loss: 2.1279
Epoch: 5/20... Step: 240... Loss: 2.1626... Val Loss: 2.1041
Epoch: 5/20... Step: 250... Loss: 2.1420... Val Loss: 2.0820
Epoch: 5/20... Step: 260... Loss: 2.1125... Val Loss: 2.0618
Epoch: 5/20... Step: 270... Loss: 2.0837... Val Loss: 2.0487
Epoch: 5/20... Step: 280... Loss: 2.0884... Val Loss: 2.0260
...
...
...
Epoch: 18/20... Step: 1000... Loss: 1.4956... Val Loss: 1.4810
Epoch: 18/20... Step: 1010... Loss: 1.5043... Val Loss: 1.4802
Epoch: 18/20... Step: 1020... Loss: 1.5053... Val Loss: 1.4753
Epoch: 19/20... Step: 1030... Loss: 1.4914... Val Loss: 1.4740
Epoch: 19/20... Step: 1040... Loss: 1.5201... Val Loss: 1.4681
Epoch: 19/20... Step: 1050... Loss: 1.4710... Val Loss: 1.4663
Epoch: 19/20... Step: 1060... Loss: 1.4651... Val Loss: 1.4623
Epoch: 19/20... Step: 1070... Loss: 1.4798... Val Loss: 1.4612
Epoch: 19/20... Step: 1080... Loss: 1.4772... Val Loss: 1.4586
Epoch: 20/20... Step: 1090... Loss: 1.4229... Val Loss: 1.4561
Epoch: 20/20... Step: 1100... Loss: 1.4444... Val Loss: 1.4541
Epoch: 20/20... Step: 1110... Loss: 1.4557... Val Loss: 1.4498
Epoch: 20/20... Step: 1120... Loss: 1.4478... Val Loss: 1.4476
Epoch: 20/20... Step: 1130... Loss: 1.4762... Val Loss: 1.4442
Epoch: 20/20... Step: 1140... Loss: 1.4559... Val Loss: 1.4435

Obtener el mejor modelo

Si queremos ajustar los híper-parámetros para que nos den una mejor performance, debemos mirar las pérdidas en entrenamiento y validación. Si las pérdidas de entrenamiento son mucho menores a las pérdidas de validación– estamos overfitting. Para rectificar, podemos incrementar regularización (dropout) o usar un red mas pequeña. Al contrario, si las pérdidas de entrenamiento y validación son similares, estamos underfitting entonces podemos incrementar el tamaño de nuestra red.

Híper-parámetros

Estos son los híper-parámetros de nuestra red.

Cuando definimos el modelo:

  • n_hidden – número de unidades en las capas escondidas.
  • n_layers – número de capas escondidas de LSTM a usar.

Probabilidades de dropout y velocidad de apredizaje estan en sus valores default.

Cuando entrenamos el modelo:

  • batch_size – número de sequencias que pasan por la red en cada paso.
  • seq_length – número de cáracteres en la sequencia que la red esta siendo entrenada. Típicamente, mientra la red es más grande es mejor. La red aprenderá dependencias de largo rango. Pero tomará mucho mas tiempo entrenarla. Cien es un buen número en este paso.
  • lr – velocidad de apredizaje.

Punto de Control

Después del entrenamiento, es posible guardar el modelo para poder cargarlo déspues si es necesario. En este ejemplo guardaremos los parámetros necesarios para crear la misma arquitectura de nuestra red, los híper-parámetros, capas, y cáracteres.

# Cambiar el nombre, para guardar multiples archivos
model_name = 'rnn_20_epoch.net'
checkpoint = {'n_hidden':net.n_hidden,
              'n_layers':net.n_layers,
              'state_dict':net.state_dict(),
              'tokens':net.chars}

with open(model_name, 'wb') as f:
    torch.save(checkpoint, f)

Predicciones

Ahora que tenemos el modelo entrenado, podemos usarlo para hacer predicciones. Para hacer esto, pasamos un cáracter a la red para que esta haga la predicción del siguiente cáracter. Repetimos este proceso hasta generar una buena cantidad de texto.

def predict(net, char, h=None, top_k=None):
    '''
      Dado un cáracter, predecir el siguiente cáracter
      Regresa el predicho cáracter y el hidden state
      '''
    
    # Tensor inputs
    x = np.array([[net.char2int[char]]])
    x = one_hot_encode(x, len(net.chars))
    inputs = torch.from_numpy(x)
    
    if (entrenar_en_gpu):
        inputs = inputs.cuda()
        
        # Separar el hidden state de su historia
        h = tuple(each.data for each in h)
        # Obtener el output del modelo
        out, h = net(inputs, h)
        
        # Obtener las probabilidades del cáracter
        p = F.softmax(out, dim = 1).data
        if (entrenar_en_gpu):
            p = p.cpu() # pasar al CPU
            
        # Obtener top cáracters
        if top_k is None:
            top_ch = np.arange(len(net.chars))
        else:
            p, top_ch = p.topk(top_k)
            top_ch = top_ch.numpy().squeeze()
        
        # Obtener el siguiente cáracter mas probable con ago de aleatoriedad
        p = p.numpy().squeeze()
        char = np.random.choice(top_ch, p = p/p.sum())
        
        # Regresar el valor codificado del cáracter predicho y el hidden state 
        return net.int2char[char], h
        
            
    

Crear una muestra y generar texto

Idealmente, debemos crear una muestra en la red para crear el ‘hidden state’. De otra manera, la red comenzará a generar cáracteres de manera aleatoria.
Usualmente, los primeros cáracteres son un poco torpes por que la red todavía no a construido una larga lista de cáracteres de la cual pueda predecir.

def sample(net, size, prime = 'El', top_k = None):
    if (entrenar_en_gpu):
        net.cuda()
    else:
        net.cpu()
        
    net.eval() # poner la red in modo de evaluación
    
    # Primero, pasar a través del cáracter preparado
    chars = [ch for ch in prime]
    h = net.init_hidden(1)
    for ch in prime:
        char, h = predict(net, ch, h, top_k = top_k)
    
    chars.append(char)
    
    # Ahora,  pasar el cáracter previo y obtener un nuevo
    for ii in range(size):
        char, h = predict(net, chars[-1], h, top_k = top_k)
        chars.append(char)
        
    return ''.join(chars)   
    
print(sample(net, 1000, prime='Anthony', top_k=5))
Anthony el cieno a los cretolas de la casa eran el acostorno, y los cuantos en la caleza de paladas, porque soporado por el mundo de su padre. Al preginto y luego de concura de la mana de su mujer, su esconte se desprocharan la palabre, el ciero..
El 
tamano, y sabía en el carato con sun derenciante, y se entaban en lar asindos de la 
pusa a todo las manos año desde la 
acuerde. Las consenciantas de sue 
compaño con la ciana 
pertaban de la mado con el calávir. Lo encontraba a el compañororo. 
-No para la mañana dentro de caba dos distamentes de la pasa, para el patio de la carado, y la mano, pero no hubiera decidioron las compres del contraño y la 
parede con un pramos de sus hordas de cullurada en una porembiendo, como el mando ero un caso de su halga excantar las amanes, y la pronte con las patoles que no estaban años antes de las canacersa de las mismos para el coronel Aureliano Buendía lo dejo al 
momendo a lechas por el díse de luegros 
de la mujer, pero encontró el domario que no era pr

Cargar el Punto de Control

# Cargar el modelo que entrenamos y gurdamos previamente: `rnn_20_epoch.net`
with open('rnn_20_epoch.net', 'rb') as f:
    checkpoint = torch.load(f)
    
loaded = CharRNN(checkpoint['tokens'], n_hidden=checkpoint['n_hidden'], n_layers=checkpoint['n_layers'])
loaded.load_state_dict(checkpoint['state_dict'])
# Generar texto
print(sample(loaded, 3000, top_k=5, prime="Y el dijo: "))
Y el dijo: 
-No se le pesió, por entoreles que el padre 
Amaranta los humado en que la calle el padie 
su hurmiero. Pera ella los heranes sin ponos con como dincurar en el 
cualtito. Pentra Coter la mierta del calabro, y le procurió en la casa de calle en la marra de liger 
a la patra de las pruelas con sus maricias de 
los mandolos de la conversión, presondía en la casa de 
los cuerros de la cuerda y cuando se los pueron, y en 
contentrarle a la muerte, y lo decierdo de las cosas de sus pellites y sin embarios, peso a la mano. 
Sólo, y las pantales, el calor y el día en la pierna. En los desegrados encendado, pero el palor. Los 
alguios de 
la pendar. Amaranta a los carajos de 
cama de acabillas. El pueblo decorios en la 
aludancia y almanzó en su carta, y llevaba a la premonidad de amaneras de convanel, para si minicas 
en la muerte y estaba, sientra de cuardo en sus puebros de labre. Aulescon se le dijo, sabieron en la casta, 
por una mujo que le había premundo el amor la partera comersa que había parecido a su misiario, lo 
muerte, conseguró las preseras, y sen entontes en su procosaria. La cina de las cortualles, y la 
cinción de la prameda casa de la mano a cuando el mienterio de sus procos pesos de los compros desdeles al conterto del albanto, y se escambaba la pullir de su paricientos y los mismos descuando en las maridas de cuerre. 
-Así qui esticira en un poco sin muy concuerdo, y los 
alimperes a cantarlo a sabal desprevillidos. Es encontraro la ciera de la cuella y estaba encontró el cisto de acordar las 
pareciados desde que nadie la mujer de 
la puerta del cartito, y le había de cuadra la paleca del damino de las cosas dinabres, lo había de sus mujureros, y le decoritaron a 
la pendia, y ella entendiendo con los meses. En la casa, y lo dijo que no estaba en el dormitorio. La misma decía a sus comprendos. En ela de la pariel y se sentía en un cina 
esta dos mediores y con el cargano ellos, sel ponque en un coronel 
Meme sin 
muerte en solo de la cual de amar llevó con su humiento. Pon 
pare que lo había dispulido a llagurla a cercas de si se este apreventiral desdu sus aposas y las malas 
después. Estaba pristo de caballes, contrabieron. 
El coronel Aureliano, en las presegantes. Era un calor a su 
coronel Grincedo 
de 
su cuarto. Aureliano Segundo en la 
colla de la puerta de la muerda, por la casa de acardarse, y los hijos, sino propesas de asu abrento en un trandimiento de cerra del muerto, las almigrates, porque la consuguir de la vencidad y 
explicaban la ver a la pertinidad, pero solo en la mana, y su parre del pordero de 
la ver en las 
calles y como del discertido de 
cuande estaba la casa. El cuarto y antes compariós de su pasa de las para del convillaroror de la casa de la misma cuenda de su hermano y cuando se era 
cuenta de que llevaba los mardes armanos en la mano de los precosos para que le 
semana en la cuerta, el prando en la cinco de calonas por el coronel Aureliano Buendía en la casa de la casa. El coronel Aureliano Buendia.