Clasificación de texto con BERT Tokenizer y TF 2.0 en Python

    Este es el artículo número 23 de mi serie de artículos sobre Python para PNL. En el artículo anterior de esta serie, expliqué cómo realizar la traducción automática neuronal utilizando arquitectura seq2seq con la biblioteca Keras de Python para el aprendizaje profundo.

    En este artículo estudiaremos BERT, que significa Representaciones de codificador bidireccional de Transformers y su aplicación a la clasificación de texto. BERT es una técnica de representación de texto como Word Embeddings. Si no tiene idea de cómo funcionan las incrustaciones de palabras, eche un vistazo a mi artículo sobre incrustaciones de palabras.

    Al igual que las incrustaciones de palabras, BERT es también una técnica de representación de texto que es una fusión de una variedad de algoritmos de aprendizaje profundo de última generación, como el codificador bidireccional LSTM y Transformers. BERT fue desarrollado por investigadores de Google en 2018 y se ha demostrado que es el estado de la técnica para una variedad de tareas de procesamiento del lenguaje natural, como clasificación de texto, resumen de texto, generación de texto, etc. Recientemente, Google anunció que BERT se utiliza como parte central de su algoritmo de búsqueda para comprender mejor las consultas.

    En este artículo no entraremos en los detalles matemáticos de cómo se implementa BERT, ya que hay muchos recursos disponibles en línea. Más bien, veremos cómo realizar la clasificación de texto utilizando BERT Tokenizer. En este artículo, verá cómo se puede utilizar BERT Tokenizer para crear un modelo de clasificación de texto. En el próximo artículo explicaré cómo el BERT Tokenizer, junto con la capa de incrustación BERT, se puede utilizar para crear modelos de PNL aún más eficientes.

    Nota: Todos los scripts de este artículo se han probado utilizando Google Colab entorno, con el tiempo de ejecución de Python configurado en GPU.

    El conjunto de datos

    El conjunto de datos utilizado en este artículo se puede descargar de este enlace de Kaggle.

    Si descarga el conjunto de datos y extrae el archivo comprimido, verá un archivo CSV. El archivo contiene 50.000 registros y dos columnas: revisión y opinión. La columna de opinión contiene texto para la opinión y la columna de opinión contiene la opinión de la opinión. La columna de sentimiento puede tener dos valores, es decir, “positivo” y “negativo”, lo que hace que nuestro problema sea un problema de clasificación binaria.

    Anteriormente, realizamos un análisis sentimental de este conjunto de datos en un artículo anterior en el que logramos una precisión máxima del 92% en el conjunto de entrenamiento mediante una técnica de incrustación de palabras y una red neuronal convolucional. En el conjunto de prueba, la precisión máxima lograda fue del 85,40% utilizando la incrustación de palabras y un LSTM único con 128 nodes. Veamos si podemos obtener una mayor precisión utilizando la representación BERT.

    Instalación e importación de bibliotecas necesarias

    Antes de que pueda usar la representación de texto BERT, debe instalar BERT para TensorFlow 2.0. Ejecute los siguientes comandos pip en su terminal para instalar BERT para TensorFlow 2.0.

    !pip install bert-for-tf2
    !pip install sentencepiece
    

    A continuación, debe asegurarse de ejecutar TensorFlow 2.0. Google Colab, de forma predeterminada, no ejecuta su script en TensorFlow 2.0. Por lo tanto, para asegurarse de que está ejecutando su secuencia de comandos a través de TensorFlow 2.0, ejecute la siguiente secuencia de comandos:

    try:
        %tensorflow_version 2.x
    except Exception:
        pass
    import tensorflow as tf
    
    import tensorflow_hub as hub
    
    from tensorflow.keras import layers
    import bert
    

    En el script anterior, además de TensorFlow 2.0, también importamos tensorflow_hub, que básicamente es un lugar donde puedes encontrar todos los modelos precompilados y preentrenados desarrollados en TensorFlow. Importaremos y usaremos un modelo BERT integrado desde TF hub. Finalmente, si en la salida ve la siguiente salida, está listo para comenzar:

    TensorFlow 2.x selected.
    

    Importación y preprocesamiento del conjunto de datos

    El siguiente script importa el conjunto de datos usando el read_csv() método del marco de datos de Pandas. El script también imprime la forma del conjunto de datos.

    movie_reviews = pd.read_csv("/content/drive/My Drive/Colab Datasets/IMDB Dataset.csv")
    
    movie_reviews.isnull().values.any()
    
    movie_reviews.shape
    

    Salida

    (50000, 2)
    

    El resultado muestra que nuestro conjunto de datos tiene 50.000 filas y 2 columnas.

    A continuación, preprocesaremos nuestros datos para eliminar cualquier puntuación y caracteres especiales. Para ello, definiremos una función que toma como entrada una revisión de texto sin procesar y devuelve la correspondiente revisión de texto limpio.

    def preprocess_text(sen):
        # Removing html tags
        sentence = remove_tags(sen)
    
        # Remove punctuations and numbers
        sentence = re.sub('[^a-zA-Z]', ' ', sentence)
    
        # Single character removal
        sentence = re.sub(r"s+[a-zA-Z]s+", ' ', sentence)
    
        # Removing multiple spaces
        sentence = re.sub(r's+', ' ', sentence)
    
        return sentence
    
    TAG_RE = re.compile(r'<[^>]+>')
    
    def remove_tags(text):
        return TAG_RE.sub('', text)
    

    El siguiente script limpia todas las revisiones de texto:

    reviews = []
    sentences = list(movie_reviews['review'])
    for sen in sentences:
        reviews.append(preprocess_text(sen))
    

    Nuestro conjunto de datos contiene dos columnas, como se puede verificar con el siguiente script:

    print(movie_reviews.columns.values)
    

    Salida:

    ['review' 'sentiment']
    

    los review columna contiene texto mientras que sentiment La columna contiene sentimientos. La columna de sentimientos contiene valores en forma de texto. El siguiente script muestra valores únicos en el sentiment columna:

    movie_reviews.sentiment.unique()
    

    Salida:

    array(['positive', 'negative'], dtype=object)
    

    Puede ver que la columna de opinión contiene dos valores únicos, es decir positive y negative. Los algoritmos de aprendizaje profundo funcionan con números. Como solo tenemos dos valores únicos en la salida, podemos convertirlos en 1 y 0. La siguiente secuencia de comandos reemplaza positive sentimiento por 1 y el sentimiento negativo de 0.

    y = movie_reviews['sentiment']
    
    y = np.array(list(map(lambda x: 1 if x=="positive" else 0, y)))
    

    Ahora el reviews variable contienen revisiones de texto mientras que la y variable contiene las etiquetas correspondientes. Imprimamos una reseña al azar.

    print(reviews[10])
    

    Salida:

    Phil the Alien is one of those quirky films where the humour is based around the oddness of everything rather than actual punchlines At first it was very odd and pretty funny but as the movie progressed didn find the jokes or oddness funny anymore Its low budget film thats never problem in itself there were some pretty interesting characters but eventually just lost interest imagine this film would appeal to stoner who is currently partaking For something similar but better try Brother from another planet 
    

    Claramente parece una reseña negativa. Confirmémoslo imprimiendo el valor de etiqueta correspondiente:

    print(y[10])
    

    Salida:

    0
    

    La salida 0 confirma que se trata de una revisión negativa. Ahora hemos preprocesado nuestros datos y ahora estamos listos para crear representaciones BERT a partir de nuestros datos de texto.

    Creación de un tokenizador BERT

    Para usar las incrustaciones de texto BERT como entrada para entrenar el modelo de clasificación de texto, necesitamos tokenizar nuestras revisiones de texto. La tokenización se refiere a dividir una oración en palabras individuales. Para tokenizar nuestro texto, usaremos el tokenizador BERT. Mira el siguiente guión:

    BertTokenizer = bert.bert_tokenization.FullTokenizer
    bert_layer = hub.KerasLayer("https://tfhub.dev/tensorflow/bert_en_uncased_L-12_H-768_A-12/1",
                                trainable=False)
    vocabulary_file = bert_layer.resolved_object.vocab_file.asset_path.numpy()
    to_lower_case = bert_layer.resolved_object.do_lower_case.numpy()
    tokenizer = BertTokenizer(vocabulary_file, to_lower_case)
    

    En el script de arriba, primero creamos un objeto del FullTokenizer clase de la bert.bert_tokenization módulo. A continuación, creamos una capa de incrustación BERT importando el modelo BERT desde hub.KerasLayer. los trainable el parámetro está establecido en False, lo que significa que no capacitaremos a la incorporación de BERT. En la siguiente línea, creamos un archivo de vocabulario BERT en forma de matriz numpy. Luego configuramos el texto en minúsculas y finalmente pasamos nuestro vocabulary_file y to_lower_case variables a la BertTokenizer objeto.

    Es pertinente mencionar que en este artículo solo usaremos BERT Tokenizer. En el próximo artículo usaremos BERT Embeddings junto con tokenizer.

    Veamos ahora si nuestro tokenizador BERT está realmente funcionando. Para hacerlo, convertiremos en token una oración aleatoria, como se muestra a continuación:

    tokenizer.tokenize("don't be so judgmental")
    

    Salida:

    ['don', "'", 't', 'be', 'so', 'judgment', '##al']
    

    Puede ver que el texto se ha tokenizado correctamente. También puede obtener los ID de los tokens usando el convert_tokens_to_ids() del objeto tokenizador. Mira el siguiente guión:

    tokenizer.convert_tokens_to_ids(tokenizer.tokenize("dont be so judgmental"))
    

    Salida:

    [2123, 2102, 2022, 2061, 8689, 2389]
    

    Ahora definirá una función que acepta una sola revisión de texto y devuelve los identificadores de las palabras tokenizadas en la revisión. Ejecute el siguiente script:

    def tokenize_reviews(text_reviews):
        return tokenizer.convert_tokens_to_ids(tokenizer.tokenize(text_reviews))
    

    Y ejecute el siguiente script para convertir en token todas las revisiones en el conjunto de datos de entrada:

    tokenized_reviews = [tokenize_reviews(review) for review in reviews]
    

    Preparación de datos para entrenamiento

    Las revisiones en nuestro conjunto de datos tienen diferentes longitudes. Algunas reseñas son muy pequeñas mientras que otras son muy largas. Para entrenar el modelo, las oraciones de entrada deben tener la misma longitud. Para crear oraciones de igual longitud, una forma es rellenar las oraciones más cortas con ceros. Sin embargo, esto puede resultar en una matriz dispersa que contenga un gran número de ceros. La otra forma es rellenar oraciones dentro de cada lote. Dado que entrenaremos el modelo en lotes, podemos rellenar las oraciones dentro del lote de entrenamiento localmente dependiendo de la longitud de la oración más larga. Para hacerlo, primero necesitamos encontrar la longitud de cada oración.

    La siguiente secuencia de comandos crea una lista de listas donde cada sublista contiene la revisión tokenizada, la etiqueta de la revisión y la duración de la revisión:

    reviews_with_len = [[review, y[i], len(review)]
                     for i, review in enumerate(tokenized_reviews)]
    

    En nuestro conjunto de datos, la primera mitad de las reseñas son positivas, mientras que la última mitad contiene reseñas negativas. Por lo tanto, para tener reseñas tanto positivas como negativas en los lotes de capacitación, necesitamos mezclar las reseñas. La siguiente secuencia de comandos baraja los datos de forma aleatoria:

    random.shuffle(reviews_with_len)
    

    Una vez que se barajan los datos, los ordenaremos según la duración de las revisiones. Para hacerlo, usaremos el sort() función de la lista y le dirá que queremos ordenar la lista con respecto al tercer elemento de la sublista, es decir, la duración de la revisión.

    reviews_with_len.sort(key=lambda x: x[2])
    

    Una vez que las reseñas están ordenadas por longitud, podemos eliminar el atributo de longitud de todas las reseñas. Ejecute el siguiente script para hacerlo:

    sorted_reviews_labels = [(review_lab[0], review_lab[1]) for review_lab in reviews_with_len]
    

    Una vez ordenadas las revisiones, convertiremos el conjunto de datos d para que pueda usarse para entrenar modelos de TensorFlow 2.0. Ejecute el siguiente código para convertir el conjunto de datos ordenado en una forma de conjunto de datos de entrada compatible con TensorFlow 2.0.

    processed_dataset = tf.data.Dataset.from_generator(lambda: sorted_reviews_labels, output_types=(tf.int32, tf.int32))
    

    Finalmente, ahora podemos rellenar nuestro conjunto de datos para cada lote. El tamaño de lote que vamos a utilizar es 32, lo que significa que después de procesar 32 revisiones, se actualizarán los pesos de la red neuronal. Para rellenar las revisiones localmente con respecto a los lotes, ejecute lo siguiente:

    BATCH_SIZE = 32
    batched_dataset = processed_dataset.padded_batch(BATCH_SIZE, padded_shapes=((None, ), ()))
    

    Imprimamos el primer lote y veamos cómo se le ha aplicado el relleno:

    next(iter(batched_dataset))
    

    Salida:

    (<tf.Tensor: shape=(32, 21), dtype=int32, numpy=
     array([[ 2054,  5896,  2054,  2466,  2054,  6752,     0,     0,     0,
                 0,     0,     0,     0,     0,     0,     0,     0,     0,
                 0,     0,     0],
            [ 3078,  5436,  3078,  3257,  3532,  7613,     0,     0,     0,
                 0,     0,     0,     0,     0,     0,     0,     0,     0,
                 0,     0,     0],
            [ 3191,  1996,  2338,  5293,  1996,  3185,     0,     0,     0,
                 0,     0,     0,     0,     0,     0,     0,     0,     0,
                 0,     0,     0],
            [ 2062, 23873,  3993,  2062, 11259,  2172,  2172,  2062, 14888,
                 0,     0,     0,     0,     0,     0,     0,     0,     0,
                 0,     0,     0],
            [ 1045,  2876,  9278,  2023,  2028,  2130,  2006,  7922, 12635,
              2305,     0,     0,     0,     0,     0,     0,     0,     0,
                 0,     0,     0],
          ......
          
            [ 7244,  2092,  2856, 10828,  1997, 10904,  2402,  2472,  3135,
              2293,  2466,  2007, 10958,  8428, 10102,  1999,  1996,  4281,
              4276,  3773,     0],
            [ 2005,  5760,  7788,  4393,  8808,  2498,  2064, 12826,  2000,
              1996, 11056,  3152,  3811, 16755,  2169,  1998,  2296,  2028,
              1997,  2068,     0],
            [ 2307,  3185,  2926,  1996,  2189,  3802,  2696,  2508,  2012,
              2197,  2023,  8847,  6702,  2043,  2017,  2031,  2633,  2179,
              2008,  2569,  2619],
            [ 2028,  1997,  1996,  4569, 15580,  2102,  5691,  2081,  1999,
              3522,  2086,  2204, 23191,  5436,  1998, 11813,  6370,  2191,
              2023,  2028,  4438],
            [ 2023,  3185,  2097,  2467,  2022,  5934,  1998,  3185,  4438,
              2004,  2146,  2004,  2045,  2024,  2145,  2111,  2040,  6170,
              3153,  1998,  2552]], dtype=int32)>,
     <tf.Tensor: shape=(32,), dtype=int32, numpy=
     array([0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1,
            1, 1, 0, 1, 0, 1, 1, 0, 1, 1], dtype=int32)>)
    

    El resultado anterior muestra las cinco primeras y las cinco últimas revisiones acolchadas. En las últimas cinco revisiones, puede ver que el número total de palabras en la oración más grande fue 21. Por lo tanto, en las primeras cinco revisiones, los 0 se agregan al final de las oraciones para que su longitud total también sea 21. El relleno para el siguiente lote será diferente según el tamaño de la oración más grande del lote.

    Una vez que hemos aplicado el relleno a nuestro conjunto de datos, el siguiente paso es dividir el conjunto de datos en conjuntos de prueba y entrenamiento. Podemos hacer eso con la ayuda del siguiente código:

    TOTAL_BATCHES = math.ceil(len(sorted_reviews_labels) / BATCH_SIZE)
    TEST_BATCHES = TOTAL_BATCHES // 10
    batched_dataset.shuffle(TOTAL_BATCHES)
    test_data = batched_dataset.take(TEST_BATCHES)
    train_data = batched_dataset.skip(TEST_BATCHES)
    

    En el código anterior, primero encontramos el número total de lotes dividiendo el total de registros por 32. Luego, el 10% de los datos se deja a un lado para la prueba. Para hacerlo, usamos el take() método de batched_dataset() objeto de almacenar el 10% de los datos en el test_data variable. Los datos restantes se almacenan en el train_data objeto de entrenamiento utilizando el skip() método.

    El conjunto de datos se ha preparado y ahora estamos listos para crear nuestro modelo de clasificación de texto.

    Creando el modelo

    Ahora estamos listos para crear nuestro modelo. Para hacerlo, crearemos una clase llamada TEXT_MODEL que hereda del tf.keras.Model clase. Dentro de la clase definiremos nuestras capas de modelo. Nuestro modelo constará de tres capas de redes neuronales convolucionales. En su lugar, puede usar capas LSTM y también puede aumentar o disminuir el número de capas. He copiado el número y los tipos de capas de Cuaderno colab de Google de SuperDataScience y esta arquitectura también parece funcionar bastante bien para el conjunto de datos de reseñas de películas de IMDB.

    Ahora creemos nuestra clase modelo:

    class TEXT_MODEL(tf.keras.Model):
        
        def __init__(self,
                     vocabulary_size,
                     embedding_dimensions=128,
                     cnn_filters=50,
                     dnn_units=512,
                     model_output_classes=2,
                     dropout_rate=0.1,
                     training=False,
                     name="text_model"):
            super(TEXT_MODEL, self).__init__(name=name)
            
            self.embedding = layers.Embedding(vocabulary_size,
                                              embedding_dimensions)
            self.cnn_layer1 = layers.Conv1D(filters=cnn_filters,
                                            kernel_size=2,
                                            padding="valid",
                                            activation="relu")
            self.cnn_layer2 = layers.Conv1D(filters=cnn_filters,
                                            kernel_size=3,
                                            padding="valid",
                                            activation="relu")
            self.cnn_layer3 = layers.Conv1D(filters=cnn_filters,
                                            kernel_size=4,
                                            padding="valid",
                                            activation="relu")
            self.pool = layers.GlobalMaxPool1D()
            
            self.dense_1 = layers.Dense(units=dnn_units, activation="relu")
            self.dropout = layers.Dropout(rate=dropout_rate)
            if model_output_classes == 2:
                self.last_dense = layers.Dense(units=1,
                                               activation="sigmoid")
            else:
                self.last_dense = layers.Dense(units=model_output_classes,
                                               activation="softmax")
        
        def call(self, inputs, training):
            l = self.embedding(inputs)
            l_1 = self.cnn_layer1(l) 
            l_1 = self.pool(l_1) 
            l_2 = self.cnn_layer2(l) 
            l_2 = self.pool(l_2)
            l_3 = self.cnn_layer3(l)
            l_3 = self.pool(l_3) 
            
            concatenated = tf.concat([l_1, l_2, l_3], axis=-1) # (batch_size, 3 * cnn_filters)
            concatenated = self.dense_1(concatenated)
            concatenated = self.dropout(concatenated, training)
            model_output = self.last_dense(concatenated)
            
            return model_output
    

    El script anterior es bastante sencillo. En el constructor de la clase, inicializamos algunos atributos con valores predeterminados. Estos valores serán reemplazados más adelante por los valores pasados ​​cuando el objeto del TEXT_MODEL se crea la clase.

    A continuación, se han inicializado tres capas de red neuronal convolucional con los valores de kernel o filtro de 2, 3 y 4, respectivamente. Nuevamente, puede cambiar los tamaños de filtro si lo desea.

    A continuación, dentro del call() función, la agrupación máxima global se aplica a la salida de cada una de las capas de red neuronal convolucional. Finalmente, las tres capas de la red neuronal convolucional se concatenan juntas y su salida se alimenta a la primera red neuronal densamente conectada. La segunda red neuronal densamente conectada se utiliza para predecir el sentimiento de salida, ya que solo contiene 2 clases. En caso de que tenga más clases en la salida, puede actualizar el output_classes variable en consecuencia.

    Definamos ahora los valores para los hiperparámetros de nuestro modelo.

    VOCAB_LENGTH = len(tokenizer.vocab)
    EMB_DIM = 200
    CNN_FILTERS = 100
    DNN_UNITS = 256
    OUTPUT_CLASSES = 2
    
    DROPOUT_RATE = 0.2
    
    NB_EPOCHS = 5
    

    A continuación, necesitamos crear un objeto de la TEXT_MODEL class y pasar los valores de hiperparámetros que definimos en el último paso al constructor del TEXT_MODEL clase.

    text_model = TEXT_MODEL(vocabulary_size=VOCAB_LENGTH,
                            embedding_dimensions=EMB_DIM,
                            cnn_filters=CNN_FILTERS,
                            dnn_units=DNN_UNITS,
                            model_output_classes=OUTPUT_CLASSES,
                            dropout_rate=DROPOUT_RATE)
    

    Antes de que podamos entrenar el modelo, necesitamos compilarlo. El siguiente script compila el modelo:

    if OUTPUT_CLASSES == 2:
        text_model.compile(loss="binary_crossentropy",
                           optimizer="adam",
                           metrics=["accuracy"])
    else:
        text_model.compile(loss="sparse_categorical_crossentropy",
                           optimizer="adam",
                           metrics=["sparse_categorical_accuracy"])
    

    Finalmente para entrenar nuestro modelo, podemos usar el fit método de la clase modelo.

    text_model.fit(train_data, epochs=NB_EPOCHS)
    

    Aquí está el resultado después de 5 épocas:

    Epoch 1/5
    1407/1407 [==============================] - 381s 271ms/step - loss: 0.3037 - accuracy: 0.8661
    Epoch 2/5
    1407/1407 [==============================] - 381s 271ms/step - loss: 0.1341 - accuracy: 0.9521
    Epoch 3/5
    1407/1407 [==============================] - 383s 272ms/step - loss: 0.0732 - accuracy: 0.9742
    Epoch 4/5
    1407/1407 [==============================] - 381s 271ms/step - loss: 0.0376 - accuracy: 0.9865
    Epoch 5/5
    1407/1407 [==============================] - 383s 272ms/step - loss: 0.0193 - accuracy: 0.9931
    <tensorflow.python.keras.callbacks.History at 0x7f5f65690048>
    

    Puede ver que obtuvimos una precisión del 99,31% en el conjunto de entrenamiento.

    Evaluemos ahora el rendimiento de nuestro modelo en el conjunto de prueba:

    results = text_model.evaluate(test_dataset)
    print(results)
    

    Salida:

    156/Unknown - 4s 28ms/step - loss: 0.4428 - accuracy: 0.8926[0.442786190037926, 0.8926282]
    

    A partir de la salida, podemos ver que obtuvimos una precisión del 89,26% en el conjunto de prueba.

    Conclusión

    En este artículo, vio cómo podemos usar BERT Tokenizer para crear incrustaciones de palabras que se pueden usar para realizar la clasificación de texto. Realizamos un análisis sentimental de las reseñas de películas de IMDB y logramos una precisión del 89,26% en el conjunto de prueba. En este artículo no usamos incrustaciones de BERT, solo usamos BERT Tokenizer para tokenizar las palabras. En el siguiente artículo, verá cómo BERT Tokenizer junto con BERT Embeddings se pueden utilizar para realizar la clasificación de texto.

     

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *