nikolas.cantillo
Caso moderación / NotebookRoBERTa · Llama-3.2-3B

El notebook, celda por celda

Clasificación de hate speech tal cual fue desarrollada: código, entrenamiento y métricas reales. Los outputs que muestran tweets crudos del dataset se omiten en la versión web por contener lenguaje ofensivo; el resto está intacto.

Librerías

In [1]Python
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from datasets import Dataset
from sklearn.model_selection import train_test_split
from sklearn import metrics
from sklearn.metrics import (
    classification_report, 
    confusion_matrix, 
    precision_recall_fscore_support
)
import evaluate
import torch
from torch import cuda
from datasets import (
    Dataset, 
    Value, 
    ClassLabel, 
    Features, 
    DatasetDict
)
import transformers
from transformers import (
    AutoTokenizer, 
    RobertaModel, 
    RobertaTokenizer, 
    BertModel, 
    BertTokenizer, 
    DistilBertModel, 
    DistilBertTokenizer, 
    DataCollatorWithPadding,
    AutoModelForSequenceClassification,
    TrainingArguments,
    Trainer, 
    pipeline
)

import json
from tqdm import tqdm

import logging
logging.basicConfig(level=logging.ERROR)


device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device
Out
device(type='cuda')

Introducción

El conjunto de datos utilizado en este estudio se construyó a partir de tweets obtenidos a través de la API de Twitter y filtrados utilizando un léxico de discurso de odio proporcionado por Hatebase.org. Inicialmente, se identificaron y recopilaron tweets que contenían términos específicos catalogados como discurso de odio por la comunidad. Este proceso resultó en una muestra de 85.4 millones de tweets, provenientes de 33,458 usuarios de Twitter.

Posteriormente, se realizó una selección aleatoria de tweets de esta muestra inicial para su análisis detallado. Estos tweets fueron evaluados manualmente por trabajadores de CrowdFlower (CF), quienes clasificaron cada tweet en una de tres categorías: discurso de odio, lenguaje ofensivo pero no discurso de odio, y ni ofensivo ni discurso de odio.

Estructura del dataset

Para el desarrollo de la tarea, se deben concentrar en dos columnas:

  • tweet: Contiene la sentencia que debe ser evaluada.
  • label: Clasificación asociada a cada sentencia de la variable tweet.

Respecto a la variable label, se tiene la siguiente codificación:

  • 0 - discurso de odio
  • 1 - lenguaje ofensivo
  • 2 - ninguno

El conjunto de datos ya está divido en train (df_train) y test (df_test)

1 - Entrenar clasificador con RoBERTa (10 puntos)

Utilizando el modelo RoBERTa-base, construya un clasificador para la tarea planteada.

Calcule las métricas Precision, Recall y F1-score para el conjunto test (general y por clase). Comente sus resultados.

Considere un entrenamiento por 3 épocas.

Observación

  • Debe trabajar con GPU. El tiempo de entrenamiento es de 20 minutos, aproximadamente. Se recomienda guardar el modelo para no repetir entrenamiento.
  • Respecto a la implementación desarrollada en la ayudantía 3, deben modificar la función compute_metrics. A continuación, se presenta la función que debe ser utilizada:
def compute_metrics(eval_pred):
    predictions, labels = eval_pred
    predictions = np.argmax(predictions, axis=1)
    return f1.compute(references=labels, predictions=predictions, average='micro')

Respuesta:

In [2]Python
df_train = pd.read_csv("df_train.csv")
df_test = pd.read_csv('df_test.csv')
In [3]Python
df_train.head()
Out
(output omitido en la versión web: contiene ejemplos crudos del dataset)
In [4]Python
df_test.head()
Out
(output omitido en la versión web: contiene ejemplos crudos del dataset)
In [5]Python
train_ds = Dataset.from_pandas(df_train[["tweet", "label"]])
test_ds  = Dataset.from_pandas(df_test[["tweet", "label"]])
In [6]Python
ds = DatasetDict()
ds["train"] = train_ds
ds["test"] = test_ds
ds
Out
DatasetDict({
    train: Dataset({
        features: ['tweet', 'label'],
        num_rows: 19826
    })
    test: Dataset({
        features: ['tweet', 'label'],
        num_rows: 4957
    })
})
In [7]Python
ds["train"][0]
Out
(output omitido en la versión web: contiene ejemplos crudos del dataset)
In [8]Python
MODEL_NAME = "roberta-base"
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
In [9]Python
def tokenize_fn(batch):
    return tokenizer(
        batch["tweet"],
        truncation=True,
        max_length=128
    )

train_ds = train_ds.map(tokenize_fn, batched=True)
test_ds  = test_ds.map(tokenize_fn, batched=True)
Out
Map:   0%|          | 0/19826 [00:00<?, ? examples/s]Map:   0%|          | 0/4957 [00:00<?, ? examples/s]
In [10]Python
train_ds
Out
Dataset({
    features: ['tweet', 'label', 'input_ids', 'attention_mask'],
    num_rows: 19826
})
In [11]Python
test_ds
Out
Dataset({
    features: ['tweet', 'label', 'input_ids', 'attention_mask'],
    num_rows: 4957
})
In [12]Python
f1 = evaluate.load("f1")
accuracy_metric = evaluate.load("accuracy")
In [13]Python
data_collator = DataCollatorWithPadding(tokenizer=tokenizer, padding="longest")
In [14]Python
model = AutoModelForSequenceClassification.from_pretrained(
    MODEL_NAME,
    num_labels=3
)
Out
Some weights of RobertaForSequenceClassification were not initialized from the model checkpoint at roberta-base and are newly initialized: ['classifier.dense.bias', 'classifier.dense.weight', 'classifier.out_proj.bias', 'classifier.out_proj.weight']
You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
In [15]Python
LABEL_NAMES = ["hate_speech(0)", "offensive(1)", "neither(2)"]

def compute_metrics(eval_pred):
    logits, labels = eval_pred
    preds = np.argmax(logits, axis=1)

    p_macro, r_macro, f1_macro, _ = precision_recall_fscore_support(labels, preds, average="macro", zero_division=0)
    p_micro, r_micro, f1_micro, _ = precision_recall_fscore_support(labels, preds, average="micro", zero_division=0)

    p_cls, r_cls, f1_cls, _ = precision_recall_fscore_support(labels, preds, average=None, zero_division=0)

    metrics = {
        "precision_macro": p_macro,
        "recall_macro": r_macro,
        "f1_macro": f1_macro,
        "precision_micro": p_micro,
        "recall_micro": r_micro,
        "f1_micro": f1_micro,
    }

    for i, name in enumerate(LABEL_NAMES):
        metrics[f"precision_{name}"] = p_cls[i]
        metrics[f"recall_{name}"] = r_cls[i]
        metrics[f"f1_{name}"] = f1_cls[i]

    return metrics
In [16]Python
training_args = TrainingArguments(
    report_to="none",
    output_dir="./roberta_hate_classifier",
    evaluation_strategy="epoch",
    save_strategy="epoch",
    learning_rate=2e-5,
    warmup_ratio=0.1,
    per_device_train_batch_size=16,
    gradient_accumulation_steps=2,
    per_device_eval_batch_size=32,
    num_train_epochs=3,
    weight_decay=0.01,
    logging_steps=100,
    load_best_model_at_end=True,
    metric_for_best_model="f1_macro",
    fp16=torch.cuda.is_available(),
)
Out
c:\Users\DELL\.conda\envs\NLP\Lib\site-packages\transformers\training_args.py:1575: FutureWarning: `evaluation_strategy` is deprecated and will be removed in version 4.46 of 🤗 Transformers. Use `eval_strategy` instead
  warnings.warn(
In [17]Python
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_ds,
    eval_dataset=test_ds,
    data_collator=data_collator,
    compute_metrics=compute_metrics
)
In [18]Python
trainer.train()
Out
TrainOutput(global_step=1860, training_loss=0.2667585665179837, metrics={'train_runtime': 258.5366, 'train_samples_per_second': 230.056, 'train_steps_per_second': 7.194, 'total_flos': 1913805805397220.0, 'train_loss': 0.2667585665179837, 'epoch': 3.0})
In [19]Python
trainer.save_model("./roberta_hate_classifier_final")
In [21]Python
tokenizer.save_pretrained("./roberta_hate_classifier_final")
Out
('./roberta_hate_classifier_final\\tokenizer_config.json',
 './roberta_hate_classifier_final\\special_tokens_map.json',
 './roberta_hate_classifier_final\\vocab.json',
 './roberta_hate_classifier_final\\merges.txt',
 './roberta_hate_classifier_final\\added_tokens.json',
 './roberta_hate_classifier_final\\tokenizer.json')
In [20]Python
preds_output = trainer.predict(test_ds)
preds = np.argmax(preds_output.predictions, axis=1)
labels = preds_output.label_ids

print(classification_report(labels, preds, target_names=LABEL_NAMES))
Out
precision    recall  f1-score   support

hate_speech(0)       0.49      0.43      0.46       286
  offensive(1)       0.95      0.96      0.95      3838
    neither(2)       0.91      0.91      0.91       833

      accuracy                           0.92      4957
     macro avg       0.78      0.76      0.77      4957
  weighted avg       0.91      0.92      0.92      4957

El modelo RoBERTa-base alcanza un desempeño global alto en test (Micro-F1 ≈ 0.917) y un rendimiento robusto en las clases Offensive y Neither (F1 > 0.90). Sin embargo, la clase Hate Speech presenta el peor desempeño (F1 ≈ 0.46) debido al desbalance del dataset y la dificultad semántica para distinguir entre insultos ofensivos y discurso de odio explícito. A lo largo de las 3 épocas se observa una mejora progresiva en Macro-F1 y en la clase minoritaria, aunque el margen de mejora en hate speech es limitado sin técnicas adicionales como re-weighting, oversampling o focal loss.

2 - Uso de LLM como clasificador: Zero-shot (8 puntos)

Seleccione una muestra aleatoria de 20 ejemplos por categoría desde el conjunto de test (en total son 60 ejemplos).

Utilice el modelo meta-llama/Llama-3.2-3B-Instruct para obtener predicciones para cada comentario. Justifique el diseño del prompt.

Ver modelo en: https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct

Calcule las métricas Precision, Recall y F1-score para el conjunto analizado (general y por clase). Comente los resultados.

Observación

  • Para utilizar este modelo debe registrarse en huggingface y solicitar permiso. Al importarlo, debe ingresar el token de huggingface. Revisar la ayudantía 3.
  • Para esta pregunta deben trabajar con GPU.

Respuesta:

In [23]Python
df_sample = (
    df_test
    .groupby("label", group_keys=False)
    .apply(lambda x: x.sample(n=20, random_state=42))
    .reset_index(drop=True)
)

print(df_sample["label"].value_counts())
df_sample.head()
Out
(output omitido en la versión web: contiene ejemplos crudos del dataset)
In [37]Python
MODEL_NAME = "meta-llama/Llama-3.2-3B-Instruct"

pipe = pipeline(
    "text-generation",
    model=MODEL_NAME,
    torch_dtype=torch.bfloat16,
    device_map=device,
)
Out
config.json:   0%|          | 0.00/878 [00:00<?, ?B/s]c:\Users\DELL\.conda\envs\NLP\Lib\site-packages\huggingface_hub\file_download.py:147: UserWarning: `huggingface_hub` cache-system uses symlinks by default to efficiently store duplicated files but your machine does not support them in C:\Users\DELL\.cache\huggingface\hub\models--meta-llama--Llama-3.2-3B-Instruct. Caching files will still work but in a degraded version that might require more space on your disk. This warning can be disabled by setting the `HF_HUB_DISABLE_SYMLINKS_WARNING` environment variable. For more details, see https://huggingface.co/docs/huggingface_hub/how-to-cache#limitations.
To support symlinks on Windows, you either need to activate Developer Mode or to run Python as an administrator. In order to activate developer mode, see this article: https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development
  warnings.warn(message)model.safetensors.index.json:   0%|          | 0.00/20.9k [00:00<?, ?B/s]Downloading shards:   0%|          | 0/2 [00:00<?, ?it/s]model-00001-of-00002.safetensors:   0%|          | 0.00/4.97G [00:00<?, ?B/s]model-00002-of-00002.safetensors:   0%|          | 0.00/1.46G [00:00<?, ?B/s]generation_config.json:   0%|          | 0.00/189 [00:00<?, ?B/s]tokenizer_config.json:   0%|          | 0.00/54.5k [00:00<?, ?B/s]tokenizer.json:   0%|          | 0.00/9.09M [00:00<?, ?B/s]special_tokens_map.json:   0%|          | 0.00/296 [00:00<?, ?B/s]Device set to use cuda
In [38]Python
SYSTEM_PROMPT = """You are a strict content moderation classifier.
Return ONLY one label id: 0, 1, or 2.

Labels:
0 = hate speech (attacks a protected group or uses hateful slurs targeting group identity)
1 = offensive language (insults/profanity/abuse but not targeted hate speech)
2 = neither (not offensive and not hate speech)

Return only the digit (0/1/2). No words, no explanations."""
In [62]Python
import re

def llm_predict(text: str) -> int:
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": f"Tweet: {text}\nLabel:"}
    ]
    
    out = pipe(
        messages,
        max_new_tokens=5,
        do_sample=False,
        temperature=0.0,
        return_full_text=False
    )
    generated = out[0]["generated_text"].strip()
    m = re.findall(r"\b([0-2])\b", generated)
    return int(m[-1]) if m else 2
In [63]Python
y_true = df_sample["label"].tolist()
y_pred = [llm_predict(t) for t in df_sample["tweet"].tolist()]

print("Predicciones completadas")
Out
Predicciones completadas
In [27]Python
LABEL_NAMES = ["hate_speech(0)", "offensive(1)", "neither(2)"]

print(classification_report(y_true, y_pred, target_names=LABEL_NAMES, digits=4))

p_macro, r_macro, f1_macro, _ = precision_recall_fscore_support(y_true, y_pred, average="macro", zero_division=0)
p_micro, r_micro, f1_micro, _ = precision_recall_fscore_support(y_true, y_pred, average="micro", zero_division=0)

print("\nGLOBAL:")
print("Precision Macro:", round(p_macro, 4))
print("Recall Macro:", round(r_macro, 4))
print("F1 Macro:", round(f1_macro, 4))
print("Precision Micro:", round(p_micro, 4))
print("Recall Micro:", round(r_micro, 4))
print("F1 Micro:", round(f1_micro, 4))
Out
precision    recall  f1-score   support

hate_speech(0)     1.0000    0.1000    0.1818        20
  offensive(1)     0.4222    0.9500    0.5846        20
    neither(2)     0.9231    0.6000    0.7273        20

      accuracy                         0.5500        60
     macro avg     0.7818    0.5500    0.4979        60
  weighted avg     0.7818    0.5500    0.4979        60

GLOBAL:
Precision Macro: 0.7818
Recall Macro: 0.55
F1 Macro: 0.4979
Precision Micro: 0.55
Recall Micro: 0.55
F1 Micro: 0.55

El modelo LLM en modo zero-shot obtiene un rendimiento global limitado (accuracy 0.55, macro-F1 0.50). En particular, la clase hate speech presenta un recall extremadamente bajo (0.10), lo que indica que el modelo casi nunca asigna dicha categoría, probablemente por ser una etiqueta sensible y ambigua. Por otro lado, offensive presenta recall alto (0.95) pero precisión baja (0.42), evidenciando que el modelo tiende a clasificar como ofensivo la mayoría de textos agresivos, incluyendo casos neutrales y de odio. En general, estos resultados muestran que un enfoque zero-shot puede ser útil como baseline rápido, pero es menos confiable que un modelo fine-tuned como RoBERTa para distinguir clases cercanas.

El prompt fue diseñado con los siguientes criterios:

  • Definición clara de las clases: se describió cada categoría (hate speech, offensive, neither) para reducir ambiguedad, especialmente entre hate speech y offensive, que suelen ser clases cercanas.

  • Formato de salida controlado: se solicitó explícitamente que el modelo respondiera únicamente con un dígito (0, 1 o 2), evitando explicaciones o texto adicional. Esto facilita el procesamiento automático de predicciones y el cálculo de métricas.

  • Zero-shot real: no se incluyen ejemplos etiquetados en el prompt. De esta forma, el modelo realiza la clasificación usando únicamente la instrucción y su conocimiento general.

  • Reproducibilidad: se utilizó inferencia determinista (temperature=0 y do_sample=False) para obtener resultados consistentes en múltiples ejecuciones.

3 - Uso de LLM como clasificador: Few-shot (8 puntos)

Utilice el modelo meta-llama/Llama-3.2-3B-Instruct para obtener predicciones sobre los 60 comentarios seleccionados. Especifique el prompt utilizado, indicando los criterios para seleccionar los ejemplos que serán incluidos en el prompt.

Calcule las métricas Precision, Recall y F1-score para el conjunto analizado (general y por clase). Comente los resultados.

Respuesta:

In [28]Python
FEWSHOT_SYSTEM_PROMPT = """You are a strict content moderation classifier.
Return ONLY one label id: 0, 1, or 2.

Labels:
0 = hate speech (attacks a protected group or uses hateful slurs targeting group identity)
1 = offensive language (insults/profanity/abuse but not targeted hate speech)
2 = neither (not offensive and not hate speech)

Return only the digit (0/1/2). No words, no explanations."""
In [43]Python
df_sample = (
    df_test.groupby("label", group_keys=False)
    .apply(lambda x: x.sample(n=20, random_state=42))
    .reset_index(drop=True)
)
Out
(output omitido en la versión web: contiene ejemplos crudos del dataset)
In [44]Python
fewshot_examples = (
    df_train.groupby("label", group_keys=False)
    .apply(lambda x: x.sample(n=2, random_state=123))
    .reset_index(drop=True)
)

fewshot_examples
Out
(output omitido en la versión web: contiene ejemplos crudos del dataset)
In [45]Python
def build_fewshot_block(df_examples):
    lines = []
    for _, row in df_examples.iterrows():
        lines.append(f"Tweet: {row['tweet']}\nLabel: {row['label']}\n")
    return "\n".join(lines)

fewshot_block = build_fewshot_block(fewshot_examples)
print(fewshot_block)
Out
(output omitido en la versión web: contiene ejemplos crudos del dataset)
In [32]Python
print(pipe.model.config._name_or_path)
print(pipe.model.device)
Out
meta-llama/Llama-3.2-3B-Instruct
cuda:0
In [58]Python
import re

def llama_predict_fewshot(text: str) -> int:
    user_prompt = f"""
        We will do few-shot classification.

        Here are labeled examples:
        {fewshot_block}

        Now classify this tweet:
        Tweet: {text}
        Return ONLY the digit 0, 1, or 2.
        Label:
    """.strip()

    messages = [
        {"role": "system", "content": FEWSHOT_SYSTEM_PROMPT},
        {"role": "user", "content": user_prompt}
    ]

    out = pipe(
        messages,
        max_new_tokens=5,
        do_sample=False,
        temperature=0.0,
        return_full_text=False
    )

    generated = out[0]["generated_text"].strip()
    m = re.findall(r"\b([0-2])\b", generated)
    return int(m[-1]) if m else 2
In [59]Python
y_true = df_sample["label"].tolist()
y_pred = [llama_predict_fewshot(t) for t in df_sample["tweet"].tolist()]
Out
In [50]Python
LABEL_NAMES = ["hate_speech(0)", "offensive(1)", "neither(2)"]

print(classification_report(y_true, y_pred, target_names=LABEL_NAMES, digits=4))

p_macro, r_macro, f1_macro, _ = precision_recall_fscore_support(y_true, y_pred, average="macro", zero_division=0)
p_micro, r_micro, f1_micro, _ = precision_recall_fscore_support(y_true, y_pred, average="micro", zero_division=0)

print("\nGLOBAL:")
print("Precision Macro:", round(p_macro, 4))
print("Recall Macro:", round(r_macro, 4))
print("F1 Macro:", round(f1_macro, 4))
print("Precision Micro:", round(p_micro, 4))
print("Recall Micro:", round(r_micro, 4))
print("F1 Micro:", round(f1_micro, 4))
Out
precision    recall  f1-score   support

hate_speech(0)     1.0000    0.1000    0.1818        20
  offensive(1)     0.4130    0.9500    0.5758        20
    neither(2)     0.7500    0.4500    0.5625        20

      accuracy                         0.5000        60
     macro avg     0.7210    0.5000    0.4400        60
  weighted avg     0.7210    0.5000    0.4400        60

GLOBAL:
Precision Macro: 0.721
Recall Macro: 0.5
F1 Macro: 0.44
Precision Micro: 0.5
Recall Micro: 0.5
F1 Micro: 0.5

En la configuración few-shot, se incluyeron ejemplos balanceados por clase seleccionados desde el conjunto de entrenamiento con el objetivo de guiar al modelo en la frontera entre hate speech, offensive language y texto neutral. Sin embargo, los resultados muestran que few-shot no mejora el desempeño global respecto a zero-shot (accuracy 0.50 vs 0.55; macro-F1 0.44 vs 0.50). La clase hate speech mantiene un recall muy bajo (0.10), lo que indica que el modelo sigue siendo conservador al asignar esta etiqueta. La principal degradación ocurre en la clase neither, cuyo recall disminuye de 0.60 a 0.45, sugiriendo que el modelo tiende a clasificar más textos neutrales como ofensivos luego de observar los Attachment ejemplos. Esto evidencia que few-shot puede ser sensible a la selección de ejemplos y que una mala representatividad o un número reducido de demostraciones puede perjudicar el rendimiento en clases no dominantes.

Los ejemplos usados dentro del prompt fueron seleccionados bajo los siguientes criterios:

  • Los ejemplos few-shot fueron extraídos exclusivamente del conjunto de entrenamiento (df_train) y no del conjunto de test, evitando que el modelo vea instancias del conjunto evaluado.

  • Balance entre clases, se seleccionó el mismo número de ejemplos para cada clase (por ejemplo, 2 ejemplos por clase), con el fin de no inducir un sesgo hacia una categoría dominante.

  • Ejemplos representativos y claros, se priorizaron tweets con características evidentes para cada clase, evitando ejemplos ambiguos que podrían confundir el criterio del modelo. Hate speech (0), Offensive (1), Neither (2).

4 - Conclusiones (4 puntos)

  • Compare los resultados obtenidos por cada modelo implementado.
  • Indique aspectos positivos y negativos de cada enfoque de solución.

Respuesta:

En este trabajo probamos tres enfoques distintos para clasificar tweets en hate speech (0), offensive (1) y neither (2): un modelo supervisado entrenado (RoBERTa) y dos estrategias con LLM (zero-shot y few-shot).

En términos generales, el mejor rendimiento lo obtuvo RoBERTa fine-tuned, con un accuracy cercano a 0.92 y un macro-F1 de ~0.77. Este resultado era esperable, porque el modelo fue entrenado directamente con ejemplos del dataset y logró aprender patrones típicos y propios del lenguaje en Twitter (insultos, expresiones comunes, tono informal, funadas, etc.). Además, ilustro un desempeño consistente en las clases offensive (F1≈0.95) y neither (F1≈0.91). Sin embargo, el punto más débil sigue siendo la clase hate speech, con un F1≈0.46, lo cual se explica principalmente por el desbalance de clases y por la dificultad natural de separar odio de lenguaje ofensivo cuando ambos pueden tener vocabulario agresivo muy similar.

Por otro lado, al usar el LLM en modo zero-shot, el desempeño bajó bastante (accuracy 0.55, macro-F1 ~0.50). El modelo fue particularmente conservador con la clase hate speech: alcanzó una precisión muy alta, pero con un recall extremadamente bajo (solo detectó una pequeña fracción de los casos reales). En la práctica esto significa que el LLM evitó etiquetar tweets como hate speech salvo cuando el caso era muy evidente. Además, se notó una tendencia a usar la etiqueta offensive como categoría por defecto cuando el texto tenía tono agresivo, lo que llevó a confundir parte de los tweets neutrales o de odio dentro de esa clase.

En el caso few-shot, la idea era que al mostrarle ejemplos etiquetados dentro del prompt, el modelo debería mejorar la separación entre clases. Sin embargo, en esta ejecución ocurrió lo contrario: el desempeño fue incluso un poco menor (accuracy 0.50, macro-F1 ~0.44). La clase hate speech se mantuvo igual de difícil (recall 0.10), y la degradación más grande se vio en la clase neither, donde el modelo empezó a confundir más textos neutrales como ofensivos. Esto deja en evidencia algo importante: el few-shot puede ser útil, pero es muy sensible a la selección de ejemplos. Si los ejemplos no son lo suficientemente representativos, o si son pocos y sesgan el tono del prompt, el modelo puede aprender muy mal la frontera entre categorías.

En resumen, RoBERTa entrenado es claramente la mejor solución si el objetivo es obtener un clasificador confiable y con buen rendimiento. Los enfoques con LLM (zero-shot/few-shot) son atractivos porque son rápidos y no requieren entrenamiento, pero en este problema específico muestran limitaciones fuertes, especialmente al distinguir hate speech de offensive. En este caso, el few-shot no logró ayudar y reafirmó que, para tareas sensibles y con clases cercanas, el entrenamiento supervisado sigue siendo la estrategia más robusta.

← Volver al casoComentar este trabajo