Cómo construimos un modelo IA de clasificación de titulares (en una semana)

6 April 2024| Tags: IA, BERT, BETO, Clasificación, NLP, Clickbait

Introducción

Dentro de la filosofía de aprender haciendo hace una semana nos planteamos construir un modelo de IA que clasificara titulares de noticias como clickbait o no.

Puedes probarlo aquí: Predicción de Titulares de Noticias como Clickbait

En este post vamos a contar cómo lo hemos hecho y qué hemos aprendido en el proceso.

Fase 1: Recopilando datos

Sin dudarlo, ésta es la parte más importante junto con la de etiquetar y limpiar el corpus que luego utilizaremos para entrenar el modelo. Para conseguir los datos hemos buceado y buscado por la web, en concreto en:

Kaggle:

Un clásico para encontrar datasets de todo tipo en concreto éste: Clickbait News Detection Competition

Este dataset tiene 24000 noticias con titular, texto y etiqueta, osea “alguien” o “algo” los ha etiquetado como clickbait o no, osea que para ti pueden ser clickbait algo que para el dataset no lo es y viceversa.

En todo caso lo traemos y los limpiamo un poco para quedarnos con los titulares y las etiquetas y los traducimos para guardar en un csv.

huggingface:

Otra fuente de datasets y modelos de IA. En concreto hemos encontrado un dataset de headlines (titulares).

Lo mismo que en el caso anterior hemos traído estos 32000 registros, los hemos limpiado y traducido para guardar en otro csv.

github

Buscando buscando hemos encontrado un dataset de titulares de noticias en español en el proyecto clickbait headline generator del hindú Praveen

SerpApi

SERPAPI es una API que te permite extraer datos de los resultados de búsqueda de Google. Hemos usado esta API para extraer titulares de noticias de diferentes medios y etiquetarlos como clickbait o no. En concreto hemos sacado las noticias para España de Google News

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from serpapi import GoogleSearch
import json

params = {
  "engine": "google_news",
  "api_key": "WHATEVERYOURKEYIS",
  "topic_token": "CAAqIQgKIhtDQkFTRGdvSUwyMHZNRFp0YTJvU0FtVnpLQUFQAQ", # Spain
  "gl":"es",
}

search = GoogleSearch(params)
results = search.get_dict()
news_results = results["news_results"]

json.dump(news_results, open('news.json', 'w'), indent=4)

for i in news_results:
    # print(i)
    titulo=None
    fuente=None
    # si no hay stories, no hay title
    if 'stories' in i:
        titulo = i['stories'][0]['title']
        fuente = i['stories'][0]['source']['name']

    else:
        titulo = i['title']
        fuente = i['source']['name']

    if titulo and len(titulo) > 30:
        titulo.replace('"', "'")
        print(f'"{titulo}" , "{fuente}",0')

import pandas as pd
def tratar_titulos():
    df = pd.read_csv("noticias_es.csv", header=None, names=['titulo','fuente','clickbait'])

Y esto lo hemos ido ejecutando durante unos días para tener un corpus de titulares de noticias en español.

1
python3 get_news.py >> data/noticias_es.csv

Fase 2: Preprocesado de los datos

Tenemos corpus etiquetados y tenemos corpus sin etiquetar. Vamos a juntarlos y a limpiarlos. También vamos a evitar paliza de etiquetado manual.

Lo primero traducir:

  • Un parte lo hemos traducido con la API de Google Translate integrada en Google Sheets. Funciona que no veas.
  • Después de pegarnos con diferentes APIs e incluso con openAI (lento y caro), hemos usado transformers de HuggingFace, sencillos, certeros y rápidos.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from transformers import pipeline
import pandas as pd

llm = pipeline("translation_en_to_es", model="Helsinki-NLP/opus-mt-en-es")

df=pd.read_csv('data/titulares_en.csv',header=0,encoding='utf-8', names=['titulo','clickbait'])

df['titulo'] = df['titulo'].apply(lambda x: llm(x, clean_up_tokenization_spaces=True)[0]['translation_text'])

df.to_csv('data/titulares_es.csv', index=False, quotechar='"')

Etiquetado Para esto usamos la librería Snorkel, que nos permite etiquetar automáticamente los datos con reglas que nosotros mismos definimos. Snorkel es una empresa deliciosa, con un modelo de negocio y un acercamiento a los datos muy profesional, con una plataforma de etiquetado de datos y de entrenamiento de modelos de IA que funciona estupendamente.

Muy recomendable revisar sus “papers”, su documentación y sus casos de uso.

Snorkel basa el cuidado de los datos en el concepto de Weak Supervision en el que etiquetamos los datos con reglas que nosotros mismos definimos y vamos curando los resultados con un proceso iterativo.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100

Las reglas que hemos usado son:

- Si el titular viene ya etiquetado por kaggle, la regla "kaggle_clickbait" es ```True```.
- Si el titular viene ya etiquetado por huggingface, la regla "huggingface_clickbait" es ```True```.
- Si el titular es clickbait en español, la regla "es_clickbait" es ```True```.
- Si el titular es del estilo "10 cosas que no sabías de..." o "5 trucos para..." o "30 razones que" es clickbait, la regla "n_razones" es ```True```.
- Si el titular contiene una lista de palabras como "increíble", "repugnante", "asombroso", "impresionante" la regla "palabras_clickbait" es ```True```.

También hemos utilizado reglas basadas en otros modelos de IA que nos dicen si un titular es clickbait o no o si el análisis de sentimiento del titular es muy positivo o muy negativo.

También le hemos pasado los titulares a ChatGPT con un prompt de "Este titular es clickbait" y hemos etiquetado los resultados.


Con esto nos hemos hecho con un corpus de 26000 titulares en español etiquetados como clickbait o no en el que hemos eliminado conflictos y errores.

En el código os haceís una idea de cómo lo hemos hecho:

```python
import pandas as pd
import re
from snorkel.labeling import PandasLFApplier
from snorkel.labeling import labeling_function

# from sentiment_analysis_spanish import sentiment_analysis
from snorkel.preprocess import preprocessor
from openai import OpenAI
from transformers import pipeline
import dotenv

dotenv.load_dotenv()

client = OpenAI()
sentiment = sentiment_analysis.SentimentAnalysisSpanish()
bert_classifier = pipeline("text-classification", model='nlptown/bert-base-multilingual-uncased-sentiment')
m47labs_classifier = pipeline("text-classification", model='M47Labs/spanish_news_classification_headlines')

ABSTAIN = -1
OK = 0
CLICKBAIT = 1

def cadena_a_ascii(x):
    trans = str.maketrans('áéíóúüñö','aeiouuno')
    return x.lower().translate(trans)

@labeling_function()
def palabras_clickbait_a(x):
    palabras_chungas = ['increibles', ............  'nunca adivi','meme','wtf','asqueroso','repugnante','poderoso']
    y = cadena_a_ascii(x.titulo)
    return CLICKBAIT if any(word in y for word in palabras_chungas) else ABSTAIN


@labeling_function()
def n_razones(x):
    return CLICKBAIT if re.search(r"^\d* razones|cosas|canciones|memes|mandamientos|imagenes|fotos|preguntas.*", # Muchas más aquí!!
                                  cadena_a_ascii(x.titulo), flags=re.I) else ABSTAIN

@labeling_function()
def openai_es_clickbait(x):
    # Llama al completado de OpenAI para obtener la respuesta
    response = client.completions.create(
      model="gpt-3.5-turbo-instruct",
      prompt=f"Titular: {x.titulo}\n¿Es este titular clickbait (engañoso) o no? Devuelve 0 si no es clickbait y 1 si lo es.Si no está nada claro devuelve 9\nRespuesta:",
      temperature=0,
      max_tokens=20,
      top_p=1,
      frequency_penalty=0,
      presence_penalty=0
    )

    # Analiza la respuesta de OpenAI para determinar si es clickbait o no
    if response.choices[0].text.strip() == "0":
        return OK
    elif response.choices[0].text.strip() == "1":
        return CLICKBAIT
    else:
        print("UNO RARO:",x.titulo)
        return ABSTAIN

### Más código por aquí....
### ....

# Todas las reglas de etiquetado
lfs = [palabras_clickbait_a, n_razones, openai_es_clickbait, huggingface_m47labclickbait, huggingface_bertnlp,text_sentiment]
df_train = pd.read_csv("data/data_es.csv", header=None, names=['titulo','clickbait'])
applier = PandasLFApplier(lfs=lfs)
L_train = applier.apply(df=df_train)

from snorkel.labeling import LFAnalysis
print(LFAnalysis(L=L_train, lfs=lfs).lf_summary())

from snorkel.labeling import LFAnalysis
print(LFAnalysis(L=L_train, lfs=lfs).lf_summary())

from snorkel.labeling.model import LabelModel
label_model = LabelModel(cardinality=2, verbose=True)
label_model.fit(L_train, n_epochs=500, log_freq=50, seed=123)
df_train["label"] = label_model.predict(L=L_train, tie_break_policy="abstain") # Varios modelos...

df_train.to_csv("data/data_es_labeled.csv", index=False)

Fase 3: Entrenamiento del modelo

Entrenamos un BETO que es un BERT en español:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
Setup

!pip install -U transformers datasets evaluate accelerate
!pip install scikit-learn
!pip install tensorboard

Imports

from datasets import load_dataset
from transformers import (
    AutoTokenizer,
    DataCollatorWithPadding,
    AutoModelForSequenceClassification,
    TrainingArguments,
    Trainer,
    pipeline,
)

import evaluate
import glob
import numpy as np

Hyperparameters

BATCH_SIZE = 100
NUM_PROCS = 32
LR = 0.00005
EPOCHS = 5
MODEL = 'dccuchile/bert-base-spanish-wwm-cased'
OUT_DIR = 'clickbait_bert'

Download the Dataset

import pandas as pd
from sklearn.model_selection import train_test_split
from datasets import Dataset


df = pd.read_csv("train_final_clickbait_es.csv")

# Definir la proporción de datos para cada conjunto
train_size = 0.7
val_size = 0.15
test_size = 0.15

# Dividir el dataset en train y test
X_train, X_test, y_train, y_test = train_test_split(df.text, df.label, test_size=test_size, random_state=42)

# Dividir el conjunto de train en train y validación
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=val_size/train_size, random_state=42)

train_dataset = pd.DataFrame({"text": X_train, "label": y_train})
valid_dataset = pd.DataFrame({"text": X_val, "label": y_val})
test_dataset = pd.DataFrame({"text": X_test, "label": y_test})

train_dataset = Dataset.from_pandas(train_dataset)
valid_dataset = Dataset.from_pandas(valid_dataset)
test_dataset = Dataset.from_pandas(test_dataset)


# Visualize a sample.
train_dataset[0]

Dataset Information

id2label = {
    0: "NO Clickbait",
    1: "Clickbait"
}
label2id = {
    "NO Clickbait": 0,
    "Clickbait": 1
}

Tokenize the Dataset

tokenizer = AutoTokenizer.from_pretrained(MODEL)

# Helper function for preprocessing.
def preprocess_function(examples):
    return tokenizer(
        examples["text"],
        truncation=True,
    )

tokenized_train = train_dataset.map(
    preprocess_function,
    batched=True,
    batch_size=BATCH_SIZE,
    num_proc=NUM_PROCS
)

tokenized_valid = valid_dataset.map(
    preprocess_function,
    batched=True,
    batch_size=BATCH_SIZE,
    num_proc=NUM_PROCS
)

tokenized_test = test_dataset.map(
    preprocess_function,
    batched=True,
    batch_size=BATCH_SIZE,
    num_proc=NUM_PROCS
)

data_collator = DataCollatorWithPadding(tokenizer=tokenizer)

Sample Tokenization Example

tokenized_sample = preprocess_function(train_dataset[0])

print(tokenized_sample)
print(f"Length of tokenized IDs: {len(tokenized_sample.input_ids)}")
print(f"Length of attention mask: {len(tokenized_sample.attention_mask)}")

Evaluation Metrics

accuracy = evaluate.load('accuracy')

def compute_metrics(eval_pred):
    predictions, labels = eval_pred
    predictions = np.argmax(predictions, axis=1)
    return accuracy.compute(predictions=predictions, references=labels)

Model

model = AutoModelForSequenceClassification.from_pretrained(
    MODEL,
    num_labels=2,
    id2label=id2label,
    label2id=label2id,
)

# Total parameters and trainable parameters.
total_params = sum(p.numel() for p in model.parameters())
print(f"{total_params:,} total parameters.")
total_trainable_params = sum(
    p.numel() for p in model.parameters() if p.requires_grad)
print(f"{total_trainable_params:,} training parameters.")

Training Arguments

training_args = TrainingArguments(
    output_dir=OUT_DIR,
    learning_rate=LR,
    per_device_train_batch_size=BATCH_SIZE,
    per_device_eval_batch_size=BATCH_SIZE,
    num_train_epochs=EPOCHS,
    weight_decay=0.01,
    evaluation_strategy="epoch",
    save_strategy="epoch",
    load_best_model_at_end=True,
    save_total_limit=3,
    report_to='tensorboard',
    fp16=True
)

Training

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_train,
    eval_dataset=tokenized_valid,
    tokenizer=tokenizer,
    data_collator=data_collator,
    compute_metrics=compute_metrics,
)

history = trainer.train()

Evaluate

trainer.evaluate(tokenized_test)

 Inference

print(history.global_step)

Inference

model = AutoModelForSequenceClassification.from_pretrained(f"clickbait_es/checkpoint-last")

access_token='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'

model.push_to_hub("jpancorbTaniwa/clickbait_es",token=access_token)

tokenizer = AutoTokenizer.from_pretrained('clickbait_es/checkpoint-last')

access_token='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'

tokenizer.push_to_hub("jpancorbTaniwa/clickbait_es",token=access_token)


import torch

from transformers import (
    AutoTokenizer,
    AutoModelForSequenceClassification,
    TextClassificationPipeline,
)

tokenizerDOS = AutoTokenizer.from_pretrained("taniwasl/clickbait_es")
modelDOS = AutoModelForSequenceClassification.from_pretrained("taniwasl/clickbait_es")

tokenizerDOS.save_pretrained('.')
torch.save(modelDOS.state_dict(), 'pytorch_model.bin')

review_text = 'La explosión destruye parcialmente el edificio, Egipto'

nlp = TextClassificationPipeline(task = "text-classification",
                model = modelDOS,
                tokenizer = tokenizerDOS,
                max_length = 25,
                truncation=True,
                add_special_tokens=True
                )

print(nlp(review_text))

# tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')
# classify = pipeline(task='text-classification', model=model, tokenizer=tokenizer)

# all_files = glob.glob('inference_data/*')
# for file_name in all_files:
#     file = open(file_name)
#     content = file.read()
#     print(content)
#     result = classify(content)
#     print('PRED: ', result)
#     print('GT: ', file_name.split('_')[-1].split('.txt')[0])
#     print('\n')

Fase 4: Uso del modelo

Así de sencillo una vez subido a HuggingFace nuestro modelo taniwasl/clickbait_es con 110 Millones de parámetros.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from transformers import (
    AutoTokenizer,
    AutoModelForSequenceClassification,
    TextClassificationPipeline,
)

tokenizerDOS = AutoTokenizer.from_pretrained("taniwasl/clickbait_es")
modelDOS = AutoModelForSequenceClassification.from_pretrained("taniwasl/clickbait_es")

review_text = 'La explosión destruye parcialmente el edificio, Egipto'

nlp = TextClassificationPipeline(task = "text-classification",
                model = modelDOS,
                tokenizer = tokenizerDOS,
                max_length = 25,
                truncation=True,
                add_special_tokens=True
                )

print(nlp(review_text))

Finalmente lo hemos encapsulado en un microservicio con Dash y Docker para poder usarlo en producción y hemos trasteado con los medios de comunicación españoles para ver sus índices de clickbait.

Predicción de Titulares de Noticias como Clickbait

Si quieres saber más sobre cómo podemos ayudarte con tus datos y tus proyectos de IA, no dudes en contactar con nosotros en hola@taniwa.es

Foto de Foto de Andrea Piacquadio en Pexels

SO WHAT DO YOU THINK ?

Contact us and challenge us with a problem
+34 644 237 135
hola@taniwa.es

CONTACT TANIWA