Clasificación de tokens
{#if fw === 'pt'}
{:else}
{/if}
La primera aplicación que exploraremos es la clasificación de tokens. Esta tarea genérica abarca cualquier problema que pueda formularse como "atribuir una etiqueta a cada token en una oración", como:
- Reconocimiento de entidades nombradas (NER): Encuentra las entidades (como personas, ubicaciones u organizaciones) en una oración. Esto puede formularse atribuyendo una etiqueta a cada token, con una clase por entidad y una clase para "ninguna entidad".
- Etiquetado de partes del discurso (POS): Marca cada palabra en una oración como correspondiente a una parte del discurso particular (como sustantivo, verbo, adjetivo, etc.).
- Chunking: Encuentra los tokens que pertenecen a la misma entidad. Esta tarea (que puede combinarse con POS o NER) puede formularse atribuyendo una etiqueta (generalmente
B-) a cualquier token que esté al principio de un chunk, otra etiqueta (generalmenteI-) a los tokens que estén dentro de un chunk, y una tercera etiqueta (generalmenteO) a los tokens que no pertenezcan a ningún chunk.
Por supuesto, existen muchos otros tipos de problemas de clasificación de tokens; estos son solo algunos ejemplos representativos. En esta sección, ajustaremos un modelo (BERT) en una tarea NER, que luego podrá calcular predicciones como esta:
Puedes encontrar el modelo que entrenaremos y subiremos al Hub, y verificar sus predicciones aquí.
Preparando los datos[[preparing-the-data]]
Primero que nada, necesitamos un dataset adecuado para la clasificación de tokens. En esta sección usaremos el dataset CoNLL-2003, que contiene noticias de Reuters.
[!TIP] 💡 Siempre que tu dataset consista en textos divididos en palabras con sus etiquetas correspondientes, podrás adaptar los procedimientos de procesamiento de datos descritos aquí a tu propio dataset. Consulta el Capítulo 5 si necesitas un repaso sobre cómo cargar tus propios datos personalizados en un
Dataset.
El dataset CoNLL-2003[[the-conll-2003-dataset]]
Para cargar el dataset CoNLL-2003, usamos el método load_dataset() de la biblioteca 🤗 Datasets:
from datasets import load_dataset
raw_datasets = load_dataset("conll2003")
Esto descargará y almacenará en caché el dataset, como vimos en el Capítulo 3 para el dataset GLUE MRPC. Al inspeccionar este objeto, vemos las columnas presentes y la división entre los conjuntos de entrenamiento, validación y prueba:
raw_datasets
DatasetDict({
train: Dataset({
features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'],
num_rows: 14041
})
validation: Dataset({
features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'],
num_rows: 3250
})
test: Dataset({
features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'],
num_rows: 3453
})
})
En particular, podemos ver que el dataset contiene etiquetas para las tres tareas que mencionamos anteriormente: NER, POS y chunking. Una gran diferencia con otros datasets es que los textos de entrada no se presentan como oraciones o documentos, sino como listas de palabras (la última columna se llama tokens, pero contiene palabras en el sentido de que son entradas pre-tokenizadas que aún deben pasar por el tokenizador para la tokenización de subpalabras).
Veamos el primer elemento del conjunto de entrenamiento:
raw_datasets["train"][0]["tokens"]
['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.']
Dado que queremos realizar reconocimiento de entidades nombradas, veremos las etiquetas NER:
raw_datasets["train"][0]["ner_tags"]
[3, 0, 7, 0, 0, 0, 7, 0, 0]
Esas son las etiquetas como números enteros listas para el entrenamiento, pero no son necesariamente útiles cuando queremos inspeccionar los datos. Al igual que para la clasificación de texto, podemos acceder a la correspondencia entre esos números enteros y los nombres de las etiquetas mirando el atributo features de nuestro dataset:
ner_feature = raw_datasets["train"].features["ner_tags"]
ner_feature
Sequence(feature=ClassLabel(num_classes=9, names=['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC'], names_file=None, id=None), length=-1, id=None)
Así que esta columna contiene elementos que son secuencias de ClassLabels. El tipo de los elementos de la secuencia está en el atributo feature de este ner_feature, y podemos acceder a la lista de nombres mirando el atributo names de ese feature:
label_names = ner_feature.feature.names
label_names
['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC']
Ya vimos estas etiquetas al profundizar en el pipeline token-classification en el Capítulo 6, pero para un rápido repaso:
Osignifica que la palabra no corresponde a ninguna entidad.B-PER/I-PERsignifica que la palabra corresponde al principio de/está dentro de una entidad de persona.B-ORG/I-ORGsignifica que la palabra corresponde al principio de/está dentro de una entidad de organización.B-LOC/I-LOCsignifica que la palabra corresponde al principio de/está dentro de una entidad de ubicación.B-MISC/I-MISCsignifica que la palabra corresponde al principio de/está dentro de una entidad miscelánea.
Ahora, decodificando las etiquetas que vimos anteriormente, obtenemos esto:
words = raw_datasets["train"][0]["tokens"]
labels = raw_datasets["train"][0]["ner_tags"]
line1 = ""
line2 = ""
for word, label in zip(words, labels):
full_label = label_names[label]
max_length = max(len(word), len(full_label))
line1 += word + " " * (max_length - len(word) + 1)
line2 += full_label + " " * (max_length - len(full_label) + 1)
print(line1)
print(line2)
'EU rejects German call to boycott British lamb .'
'B-ORG O B-MISC O O O B-MISC O O'
Y para un ejemplo que mezcla etiquetas B- y I-, aquí está lo que el mismo código nos da en el elemento del conjunto de entrenamiento en el índice 4:
'Germany \'s representative to the European Union \'s veterinary committee Werner Zwingmann said on Wednesday consumers should buy sheepmeat from countries other than Britain until the scientific advice was clearer .'
'B-LOC O O O O B-ORG I-ORG O O O B-PER I-PER O O O O O O O O O O O B-LOC O O O O O O O'
Como podemos ver, las entidades que abarcan dos palabras, como "European Union" y "Werner Zwingmann", se les atribuye una etiqueta B- para la primera palabra y una etiqueta I- para la segunda.
[!TIP] ✏️ ¡Tu turno! Imprime las mismas dos oraciones con sus etiquetas POS o de chunking.
Procesando los datos[[processing-the-data]]
Como de costumbre, nuestros textos deben convertirse a IDs de token antes de que el modelo pueda entenderlos. Como vimos en el Capítulo 6, una gran diferencia en el caso de las tareas de clasificación de tokens es que tenemos entradas pre-tokenizadas. Afortunadamente, la API del tokenizador puede manejar eso con bastante facilidad; solo necesitamos advertir al tokenizer con un flag especial.
Para empezar, creemos nuestro objeto tokenizer. Como dijimos antes, usaremos un modelo BERT preentrenado, así que comenzaremos descargando y almacenando en caché el tokenizador asociado:
from transformers import AutoTokenizer
model_checkpoint = "bert-base-cased"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
Puedes reemplazar el model_checkpoint con cualquier otro modelo que prefieras del Hub, o con una carpeta local en la que hayas guardado un modelo preentrenado y un tokenizador. La única restricción es que el tokenizador debe estar respaldado por la biblioteca 🤗 Tokenizers, por lo que hay una versión "rápida" disponible. Puedes ver todas las arquitecturas que vienen con una versión rápida en esta gran tabla, y para verificar que el objeto tokenizer que estás usando está realmente respaldado por 🤗 Tokenizers, puedes mirar su atributo is_fast:
tokenizer.is_fast
True
Para tokenizar una entrada pre-tokenizada, podemos usar nuestro tokenizer como de costumbre y simplemente agregar is_split_into_words=True:
inputs = tokenizer(raw_datasets["train"][0]["tokens"], is_split_into_words=True)
inputs.tokens()
['[CLS]', 'EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'la', '##mb', '.', '[SEP]']
Como podemos ver, el tokenizador añadió los tokens especiales usados por el modelo ([CLS] al principio y [SEP] al final) y dejó la mayoría de las palabras intactas. La palabra lamb, sin embargo, fue tokenizada en dos subpalabras, la y ##mb. Esto introduce una falta de coincidencia entre nuestras entradas y las etiquetas: la lista de etiquetas tiene solo 9 elementos, mientras que nuestra entrada ahora tiene 12 tokens. Contabilizar los tokens especiales es fácil (sabemos que están al principio y al final), pero también necesitamos asegurarnos de alinear todas las etiquetas con las palabras adecuadas.
Afortunadamente, como estamos usando un tokenizador rápido, tenemos acceso a los superpoderes de 🤗 Tokenizers, lo que significa que podemos mapear fácilmente cada token a su palabra correspondiente (como se vio en el Capítulo 6):
inputs.word_ids()
[None, 0, 1, 2, 3, 4, 5, 6, 7, 7, 8, None]
Con un poco de trabajo, podemos expandir nuestra lista de etiquetas para que coincida con los tokens. La primera regla que aplicaremos es que los tokens especiales obtienen una etiqueta de -100. Esto se debe a que, por defecto, -100 es un índice que se ignora en la función de pérdida que usaremos (entropía cruzada). Luego, cada token obtiene la misma etiqueta que el token que inició la palabra en la que se encuentra, ya que son parte de la misma entidad. Para los tokens dentro de una palabra pero no al principio, reemplazamos el B- con I- (ya que el token no comienza la entidad):
def align_labels_with_tokens(labels, word_ids):
new_labels = []
current_word = None
for word_id in word_ids:
if word_id != current_word:
# Start of a new word!
current_word = word_id
label = -100 if word_id is None else labels[word_id]
new_labels.append(label)
elif word_id is None:
# Special token
new_labels.append(-100)
else:
# Same word as previous token
label = labels[word_id]
# If the label is B-XXX we change it to I-XXX
if label % 2 == 1:
label += 1
new_labels.append(label)
return new_labels
Probémoslo en nuestra primera oración:
labels = raw_datasets["train"][0]["ner_tags"]
word_ids = inputs.word_ids()
print(labels)
print(align_labels_with_tokens(labels, word_ids))
[3, 0, 7, 0, 0, 0, 7, 0, 0]
[-100, 3, 0, 7, 0, 0, 0, 7, 0, 0, 0, -100]
Como podemos ver, nuestra función añadió el -100 para los dos tokens especiales al principio y al final, y un nuevo 0 para nuestra palabra que se dividió en dos tokens.
[!TIP] ✏️ ¡Tu turno! Algunos investigadores prefieren atribuir solo una etiqueta por palabra y asignar
-100a los otros subtokens en una palabra dada. Esto es para evitar que las palabras largas que se dividen en muchos subtokens contribuyan en gran medida a la pérdida. Cambia la función anterior para alinear las etiquetas con los IDs de entrada siguiendo esta regla.
Para preprocesar todo nuestro dataset, necesitamos tokenizar todas las entradas y aplicar align_labels_with_tokens() a todas las etiquetas. Para aprovechar la velocidad de nuestro tokenizador rápido, es mejor tokenizar muchos textos al mismo tiempo, así que escribiremos una función que procese una lista de ejemplos y usaremos el método Dataset.map() con la opción batched=True. Lo único diferente de nuestro ejemplo anterior es que la función word_ids() necesita obtener el índice del ejemplo del que queremos los IDs de palabra cuando las entradas al tokenizador son listas de textos (o en nuestro caso, listas de listas de palabras), así que también lo agregamos:
def tokenize_and_align_labels(examples):
tokenized_inputs = tokenizer(
examples["tokens"], truncation=True, is_split_into_words=True
)
all_labels = examples["ner_tags"]
new_labels = []
for i, labels in enumerate(all_labels):
word_ids = tokenized_inputs.word_ids(i)
new_labels.append(align_labels_with_tokens(labels, word_ids))
tokenized_inputs["labels"] = new_labels
return tokenized_inputs
Ten en cuenta que aún no hemos rellenado nuestras entradas; lo haremos más tarde, al crear los lotes con un recopilador de datos.
Ahora podemos aplicar todo ese preprocesamiento de una sola vez en las otras divisiones de nuestro dataset:
tokenized_datasets = raw_datasets.map(
tokenize_and_align_labels,
batched=True,
remove_columns=raw_datasets["train"].column_names,
)
¡Hemos hecho la parte más difícil! Ahora que los datos han sido preprocesados, el entrenamiento real se parecerá mucho a lo que hicimos en el Capítulo 3.
{#if fw === 'pt'}
Ajustando el modelo con la API Trainer[[fine-tuning-the-model-with-the-trainer-api]]
El código real usando el Trainer será el mismo que antes; los únicos cambios son la forma en que los datos se agrupan en un lote y la función de cálculo de métricas.
{:else}
Ajustando el modelo con Keras[[fine-tuning-the-model-with-keras]]
El código real usando Keras será muy similar al anterior; los únicos cambios son la forma en que se agrupan los datos en un lote y la función de cálculo de métricas.
{/if}
Agrupación de datos[[data-collation]]
No podemos usar un DataCollatorWithPadding como en el Capítulo 3 porque eso solo rellena las entradas (IDs de entrada, máscara de atención e IDs de tipo de token). Aquí, nuestras etiquetas deben rellenarse exactamente de la misma manera que las entradas para que mantengan el mismo tamaño, usando -100 como valor para que las predicciones correspondientes se ignoren en el cálculo de la pérdida.
Todo esto lo hace un DataCollatorForTokenClassification. Al igual que el DataCollatorWithPadding, toma el tokenizer usado para preprocesar las entradas:
{#if fw === 'pt'}
from transformers import DataCollatorForTokenClassification
data_collator = DataCollatorForTokenClassification(tokenizer=tokenizer)
{:else}
from transformers import DataCollatorForTokenClassification
data_collator = DataCollatorForTokenClassification(
tokenizer=tokenizer, return_tensors="tf"
)
{/if}
Para probar esto en algunas muestras, podemos simplemente llamarlo en una lista de ejemplos de nuestro conjunto de entrenamiento tokenizado:
batch = data_collator([tokenized_datasets["train"][i] for i in range(2)])
batch["labels"]
tensor([[-100, 3, 0, 7, 0, 0, 0, 7, 0, 0, 0, -100],
[-100, 1, 2, -100, -100, -100, -100, -100, -100, -100, -100, -100]])
Comparemos esto con las etiquetas para el primer y segundo elemento de nuestro dataset:
for i in range(2):
print(tokenized_datasets["train"][i]["labels"])
[-100, 3, 0, 7, 0, 0, 0, 7, 0, 0, 0, -100]
[-100, 1, 2, -100]
{#if fw === 'pt'}
Como podemos ver, el segundo conjunto de etiquetas ha sido rellenado a la longitud del primero usando -100s.
{:else}
¡Nuestro recopilador de datos está listo! Ahora usémoslo para crear un tf.data.Dataset con el método to_tf_dataset(). También puedes usar model.prepare_tf_dataset() para hacer esto con un poco menos de código repetitivo; lo verás en algunas de las otras secciones de este capítulo.
tf_train_dataset = tokenized_datasets["train"].to_tf_dataset(
columns=["attention_mask", "input_ids", "labels", "token_type_ids"],
collate_fn=data_collator,
shuffle=True,
batch_size=16,
)
tf_eval_dataset = tokenized_datasets["validation"].to_tf_dataset(
columns=["attention_mask", "input_ids", "labels", "token_type_ids"],
collate_fn=data_collator,
shuffle=False,
batch_size=16,
)
Siguiente parada: el modelo en sí.
{/if}
{#if fw === 'tf'}
Definiendo el modelo[[defining-the-model]]
Dado que estamos trabajando en un problema de clasificación de tokens, usaremos la clase TFAutoModelForTokenClassification. Lo principal a recordar al definir este modelo es pasar información sobre el número de etiquetas que tenemos. La forma más fácil de hacerlo es pasar ese número con el argumento num_labels, pero si queremos un widget de inferencia agradable que funcione como el que vimos al principio de esta sección, es mejor establecer las correspondencias de etiquetas correctas en su lugar.
Deben establecerse mediante dos diccionarios, id2label y label2id, que contienen el mapeo de ID a etiqueta y viceversa:
id2label = {i: label for i, label in enumerate(label_names)}
label2id = {v: k for k, v in id2label.items()}
Ahora podemos simplemente pasarlos al método TFAutoModelForTokenClassification.from_pretrained(), y se establecerán en la configuración del modelo, luego se guardarán y subirán correctamente al Hub:
from transformers import TFAutoModelForTokenClassification
model = TFAutoModelForTokenClassification.from_pretrained(
model_checkpoint,
id2label=id2label,
label2id=label2id,
)
Al igual que cuando definimos nuestro TFAutoModelForSequenceClassification en el Capítulo 3, la creación del modelo emite una advertencia de que algunos pesos no se usaron (los de la cabeza de preentrenamiento) y algunos otros pesos se inicializan aleatoriamente (los de la nueva cabeza de clasificación de tokens), y que este modelo debe ser entrenado. Haremos eso en un minuto, pero primero, verifiquemos que nuestro modelo tiene el número correcto de etiquetas:
model.config.num_labels
9
[!WARNING] ⚠️ Si tienes un modelo con un número incorrecto de etiquetas, obtendrás un error oscuro al llamar a
model.fit()más tarde. Esto puede ser molesto de depurar, así que asegúrate de hacer esta verificación para confirmar que tienes el número esperado de etiquetas.
Ajustando el modelo[[fine-tuning-the-model]]
¡Ahora estamos listos para entrenar nuestro modelo! Sin embargo, solo nos queda un poco más de trabajo de preparación: debemos iniciar sesión en Hugging Face y definir nuestros hiperparámetros de entrenamiento. Si estás trabajando en un notebook, hay una función de conveniencia para ayudarte con esto:
from huggingface_hub import notebook_login
notebook_login()
Esto mostrará un widget donde puedes ingresar tus credenciales de inicio de sesión de Hugging Face.
Si no estás trabajando en un notebook, simplemente escribe la siguiente línea en tu terminal:
huggingface-cli login
Después de iniciar sesión, podemos preparar todo lo que necesitamos para compilar nuestro modelo. 🤗 Transformers proporciona una función conveniente create_optimizer() que te dará un optimizador AdamW con configuraciones apropiadas para la disminución del peso y la disminución de la tasa de aprendizaje, las cuales mejorarán el rendimiento de tu modelo en comparación con el optimizador Adam incorporado:
from transformers import create_optimizer
# Train in mixed-precision float16
# Comment this line out if you're using a GPU that will not benefit from this
tf.keras.mixed_precision.set_global_policy("mixed_float16")
# The number of training steps is the number of samples in the dataset, divided by the batch size then multiplied
# by the total number of epochs. Note that the tf_train_dataset here is a batched tf.data.Dataset,
# not the original Hugging Face Dataset, so its len() is already num_samples // batch_size.
num_epochs = 3
num_train_steps = len(tf_train_dataset) * num_epochs
optimizer, schedule = create_optimizer(
init_lr=2e-5,
num_warmup_steps=0,
num_train_steps=num_train_steps,
weight_decay_rate=0.01,
)
model.compile(optimizer=optimizer)
Ten en cuenta también que no proporcionamos un argumento loss a compile(). Esto se debe a que los modelos pueden calcular la pérdida internamente; si compilas sin una pérdida y proporcionas tus etiquetas en el diccionario de entrada (como hacemos en nuestros datasets), entonces el modelo se entrenará usando esa pérdida interna, que será apropiada para la tarea y el tipo de modelo que hayas elegido.
A continuación, definimos un PushToHubCallback para subir nuestro modelo al Hub durante el entrenamiento, y ajustamos el modelo con ese callback:
from transformers.keras_callbacks import PushToHubCallback
callback = PushToHubCallback(output_dir="bert-finetuned-ner", tokenizer=tokenizer)
model.fit(
tf_train_dataset,
validation_data=tf_eval_dataset,
callbacks=[callback],
epochs=num_epochs,
)
Puedes especificar el nombre completo del repositorio al que quieres subir con el argumento hub_model_id (en particular, tendrás que usar este argumento para subir a una organización). Por ejemplo, cuando subimos el modelo a la organización huggingface-course, añadimos hub_model_id="huggingface-course/bert-finetuned-ner". Por defecto, el repositorio usado estará en tu espacio de nombres y se nombrará según el directorio de salida que establezcas, por ejemplo "cool_huggingface_user/bert-finetuned-ner".
[!TIP] 💡 Si el directorio de salida que estás usando ya existe, debe ser un clon local del repositorio al que quieres subir. Si no lo es, obtendrás un error al llamar a
model.fit()y necesitarás establecer un nuevo nombre.
Ten en cuenta que mientras ocurre el entrenamiento, cada vez que se guarda el modelo (aquí, en cada época) se sube al Hub en segundo plano. De esta manera, podrás reanudar tu entrenamiento en otra máquina si es necesario.
En esta etapa, puedes usar el widget de inferencia en el Model Hub para probar tu modelo y compartirlo con tus amigos. ¡Has ajustado con éxito un modelo en una tarea de clasificación de tokens, felicidades! Pero, ¿qué tan bueno es realmente nuestro modelo? Deberíamos evaluar algunas métricas para averiguarlo.
{/if}
Métricas[[metrics]]
{#if fw === 'pt'}
Para que el Trainer calcule una métrica en cada época, necesitaremos definir una función compute_metrics() que tome los arrays de predicciones y etiquetas, y devuelva un diccionario con los nombres y valores de las métricas.
El framework tradicional utilizado para evaluar la predicción de clasificación de tokens es seqeval. Para usar esta métrica, primero necesitamos instalar la biblioteca seqeval:
!pip install seqeval
Luego podemos cargarlo a través de la función evaluate.load() como hicimos en el Capítulo 3:
{:else}
El framework tradicional utilizado para evaluar la predicción de clasificación de tokens es seqeval. Para usar esta métrica, primero necesitamos instalar la biblioteca seqeval:
!pip install seqeval
Luego podemos cargarlo a través de la función evaluate.load() como hicimos en el Capítulo 3:
{/if}
metric = evaluate.load("seqeval")
Esta métrica no se comporta como la precisión estándar: en realidad tomará las listas de etiquetas como cadenas, no como enteros, por lo que tendremos que decodificar completamente las predicciones y las etiquetas antes de pasarlas a la métrica. Veamos cómo funciona. Primero, obtendremos las etiquetas para nuestro primer ejemplo de entrenamiento:
labels = raw_datasets["train"][0]["ner_tags"]
labels = [label_names[i] for i in labels]
labels
['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O', 'O']
Luego podemos crear predicciones falsas para esos simplemente cambiando el valor en el índice 2:
predictions = labels.copy()
predictions[2] = "O"
metric.compute(predictions=[predictions], references=[labels])
Ten en cuenta que la métrica toma una lista de predicciones (no solo una) y una lista de etiquetas. Aquí está la salida:
{'MISC': {'precision': 1.0, 'recall': 0.5, 'f1': 0.67, 'number': 2},
'ORG': {'precision': 1.0, 'recall': 1.0, 'f1': 1.0, 'number': 1},
'overall_precision': 1.0,
'overall_recall': 0.67,
'overall_f1': 0.8,
'overall_accuracy': 0.89}
{#if fw === 'pt'}
¡Esto devuelve mucha información! Obtenemos la precisión, el recall y la puntuación F1 para cada entidad separada, así como el total. Para nuestro cálculo de métricas, solo mantendremos la puntuación total, pero siéntete libre de ajustar la función compute_metrics() para que devuelva todas las métricas que desees reportar.
Esta función compute_metrics() primero toma el argmax de los logits para convertirlos en predicciones (como de costumbre, los logits y las probabilidades están en el mismo orden, por lo que no necesitamos aplicar el softmax). Luego tenemos que convertir tanto las etiquetas como las predicciones de enteros a cadenas. Eliminamos todos los valores donde la etiqueta es -100, luego pasamos los resultados al método metric.compute():
def compute_metrics(eval_preds):
logits, labels = eval_preds
predictions = np.argmax(logits, axis=-1)
# Remove ignored index (special tokens) and convert to labels
true_labels = [[label_names[l] for l in label if l != -100] for label in labels]
true_predictions = [
[label_names[p] for (p, l) in zip(prediction, label) if l != -100]
for prediction, label in zip(predictions, labels)
]
all_metrics = metric.compute(predictions=true_predictions, references=true_labels)
return {
"precision": all_metrics["overall_precision"],
"recall": all_metrics["overall_recall"],
"f1": all_metrics["overall_f1"],
"accuracy": all_metrics["overall_accuracy"],
}
Ahora que esto está hecho, estamos casi listos para definir nuestro Trainer. ¡Solo necesitamos un model para ajustar!
{:else}
¡Esto devuelve mucha información! Obtenemos la precisión, el recall y la puntuación F1 para cada entidad separada, así como el total. Ahora veamos qué sucede si intentamos usar las predicciones reales de nuestro modelo para calcular algunas puntuaciones reales.
A TensorFlow no le gusta concatenar nuestras predicciones, porque tienen longitudes de secuencia variables. Esto significa que no podemos simplemente usar model.predict(), pero eso no nos detendrá. Obtendremos algunas predicciones por lotes y las concatenaremos en una gran lista a medida que avanzamos, eliminando los tokens -100 que indican enmascaramiento/relleno, luego calcularemos las métricas en la lista al final:
all_predictions = []
all_labels = []
for batch in tf_eval_dataset:
logits = model.predict_on_batch(batch)["logits"]
labels = batch["labels"]
predictions = np.argmax(logits, axis=-1)
for prediction, label in zip(predictions, labels):
for predicted_idx, label_idx in zip(prediction, label):
if label_idx == -100:
continue
all_predictions.append(label_names[predicted_idx])
all_labels.append(label_names[label_idx])
metric.compute(predictions=[all_predictions], references=[all_labels])
{'LOC': {'precision': 0.91, 'recall': 0.92, 'f1': 0.91, 'number': 1668},
'MISC': {'precision': 0.70, 'recall': 0.79, 'f1': 0.74, 'number': 702},
'ORG': {'precision': 0.85, 'recall': 0.90, 'f1': 0.88, 'number': 1661},
'PER': {'precision': 0.95, 'recall': 0.95, 'f1': 0.95, 'number': 1617},
'overall_precision': 0.87,
'overall_recall': 0.91,
'overall_f1': 0.89,
'overall_accuracy': 0.97}
¿Cómo le fue a tu modelo, comparado con el nuestro? Si obtuviste números similares, ¡tu entrenamiento fue un éxito!
{/if}
{#if fw === 'pt'}
Definiendo el modelo[[defining-the-model]]
Dado que estamos trabajando en un problema de clasificación de tokens, usaremos la clase AutoModelForTokenClassification. Lo principal a recordar al definir este modelo es pasar información sobre el número de etiquetas que tenemos. La forma más fácil de hacerlo es pasar ese número con el argumento num_labels, pero si queremos un widget de inferencia agradable que funcione como el que vimos al principio de esta sección, es mejor establecer las correspondencias de etiquetas correctas en su lugar.
Deben establecerse mediante dos diccionarios, id2label y label2id, que contienen los mapeos de ID a etiqueta y viceversa:
id2label = {i: label for i, label in enumerate(label_names)}
label2id = {v: k for k, v in id2label.items()}
Ahora podemos simplemente pasarlos al método AutoModelForTokenClassification.from_pretrained(), y se establecerán en la configuración del modelo y luego se guardarán y subirán correctamente al Hub:
from transformers import AutoModelForTokenClassification
model = AutoModelForTokenClassification.from_pretrained(
model_checkpoint,
id2label=id2label,
label2id=label2id,
)
Al igual que cuando definimos nuestro AutoModelForSequenceClassification en el Capítulo 3, la creación del modelo emite una advertencia de que algunos pesos no se usaron (los de la cabeza de preentrenamiento) y algunos otros pesos se inicializan aleatoriamente (los de la nueva cabeza de clasificación de tokens), y que este modelo debe ser entrenado. Haremos eso en un minuto, pero primero, verifiquemos que nuestro modelo tiene el número correcto de etiquetas:
model.config.num_labels
9
[!WARNING] ⚠️ Si tienes un modelo con un número incorrecto de etiquetas, obtendrás un error oscuro al llamar al método
Trainer.train()más adelante (algo como "CUDA error: device-side assert triggered"). Esta es la causa número uno de errores reportados por los usuarios para tales errores, así que asegúrate de hacer esta verificación para confirmar que tienes el número esperado de etiquetas.
Ajustando el modelo[[fine-tuning-the-model]]
¡Ahora estamos listos para entrenar nuestro modelo! Solo necesitamos hacer dos últimas cosas antes de definir nuestro Trainer: iniciar sesión en Hugging Face y definir nuestros argumentos de entrenamiento. Si estás trabajando en un notebook, hay una función de conveniencia para ayudarte con esto:
from huggingface_hub import notebook_login
notebook_login()
Esto mostrará un widget donde puedes ingresar tus credenciales de inicio de sesión de Hugging Face.
Si no estás trabajando en un notebook, simplemente escribe la siguiente línea en tu terminal:
huggingface-cli login
Una vez hecho esto, podemos definir nuestro TrainingArguments:
from transformers import TrainingArguments
args = TrainingArguments(
"bert-finetuned-ner",
evaluation_strategy="epoch",
save_strategy="epoch",
learning_rate=2e-5,
num_train_epochs=3,
weight_decay=0.01,
push_to_hub=True,
)
Ya has visto la mayoría de estos antes: establecemos algunos hiperparámetros (como la tasa de aprendizaje, el número de épocas a entrenar y la disminución de peso), y especificamos push_to_hub=True para indicar que queremos guardar el modelo y evaluarlo al final de cada época, y que queremos subir nuestros resultados al Model Hub. Ten en cuenta que puedes especificar el nombre del repositorio al que quieres subir con el argumento hub_model_id (en particular, tendrás que usar este argumento para subir a una organización). Por ejemplo, cuando subimos el modelo a la organización huggingface-course, añadimos hub_model_id="huggingface-course/bert-finetuned-ner" a TrainingArguments. Por defecto, el repositorio usado estará en tu espacio de nombres y se nombrará según el directorio de salida que establezcas, así que en nuestro caso será "sgugger/bert-finetuned-ner".
[!TIP] 💡 Si el directorio de salida que estás usando ya existe, debe ser un clon local del repositorio al que quieres subir. Si no lo es, obtendrás un error al definir tu
Trainery necesitarás establecer un nuevo nombre.
Finalmente, simplemente pasamos todo al Trainer y lanzamos el entrenamiento:
from transformers import Trainer
trainer = Trainer(
model=model,
args=args,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["validation"],
data_collator=data_collator,
compute_metrics=compute_metrics,
processing_class=tokenizer,
)
trainer.train()
Ten en cuenta que mientras ocurre el entrenamiento, cada vez que se guarda el modelo (aquí, en cada época) se sube al Hub en segundo plano. De esta manera, podrás reanudar tu entrenamiento en otra máquina si es necesario.
Una vez que el entrenamiento se completa, usamos el método push_to_hub() para asegurarnos de subir la versión más reciente del modelo:
trainer.push_to_hub(commit_message="Training complete")
Este comando devuelve la URL del commit que acaba de hacer, si quieres inspeccionarlo:
'https://huggingface.co/sgugger/bert-finetuned-ner/commit/26ab21e5b1568f9afeccdaed2d8715f571d786ed'
El Trainer también redacta una tarjeta de modelo con todos los resultados de la evaluación y la sube. En esta etapa, puedes usar el widget de inferencia en el Model Hub para probar tu modelo y compartirlo con tus amigos. ¡Has ajustado con éxito un modelo en una tarea de clasificación de tokens, felicidades!
Si quieres profundizar un poco más en el bucle de entrenamiento, ahora te mostraremos cómo hacer lo mismo usando 🤗 Accelerate.
Un bucle de entrenamiento personalizado[[a-custom-training-loop]]
Ahora echemos un vistazo al bucle de entrenamiento completo, para que puedas personalizar fácilmente las partes que necesites. Se parecerá mucho a lo que hicimos en el Capítulo 3, con algunos cambios para la evaluación.
Preparando todo para el entrenamiento[[preparing-everything-for-training]]
Primero, necesitamos construir los DataLoaders a partir de nuestros conjuntos de datos. Reutilizaremos nuestro data_collator como collate_fn y mezclaremos el conjunto de entrenamiento, pero no el conjunto de validación:
from torch.utils.data import DataLoader
train_dataloader = DataLoader(
tokenized_datasets["train"],
shuffle=True,
collate_fn=data_collator,
batch_size=8,
)
eval_dataloader = DataLoader(
tokenized_datasets["validation"], collate_fn=data_collator, batch_size=8
)
Luego, volvemos a instanciar nuestro modelo para asegurarnos de no continuar el fine-tuning anterior, sino de empezar de nuevo desde el modelo BERT preentrenado:
model = AutoModelForTokenClassification.from_pretrained(
model_checkpoint,
id2label=id2label,
label2id=label2id,
)
Después, necesitaremos un optimizador. Usaremos el clásico AdamW, que es como Adam, pero con una corrección en la forma en que se aplica la disminución de peso (weight decay):
from torch.optim import AdamW
optimizer = AdamW(model.parameters(), lr=2e-5)
Una vez que tengamos todos esos objetos, podemos enviarlos al método accelerator.prepare():
from accelerate import Accelerator
accelerator = Accelerator()
model, optimizer, train_dataloader, eval_dataloader = accelerator.prepare(
model, optimizer, train_dataloader, eval_dataloader
)
[!TIP] 🚨 Si estás entrenando en una TPU, necesitarás mover todo el código a partir de la celda anterior a una función de entrenamiento dedicada. Consulta el Capítulo 3 para más detalles.
Ahora que hemos enviado nuestro train_dataloader a accelerator.prepare(), podemos usar su longitud para calcular el número de pasos de entrenamiento. Recuerda que siempre debemos hacer esto después de preparar el dataloader, ya que ese método cambiará su longitud. Usamos un programa lineal clásico desde la tasa de aprendizaje hasta 0:
from transformers import get_scheduler
num_train_epochs = 3
num_update_steps_per_epoch = len(train_dataloader)
num_training_steps = num_train_epochs * num_update_steps_per_epoch
lr_scheduler = get_scheduler(
"linear",
optimizer=optimizer,
num_warmup_steps=0,
num_training_steps=num_training_steps,
)
Por último, para subir nuestro modelo al Hub, necesitaremos crear un objeto Repository en una carpeta de trabajo. Primero, inicia sesión en Hugging Face, si no lo has hecho ya. Determinaremos el nombre del repositorio a partir del ID del modelo que queremos darle a nuestro modelo (siéntete libre de reemplazar el repo_name con tu propia elección; solo necesita contener tu nombre de usuario, que es lo que hace la función get_full_repo_name()):
from huggingface_hub import Repository, get_full_repo_name
model_name = "bert-finetuned-ner-accelerate"
repo_name = get_full_repo_name(model_name)
repo_name
'sgugger/bert-finetuned-ner-accelerate'
Luego podemos clonar ese repositorio en una carpeta local. Si ya existe, esta carpeta local debe ser un clon existente del repositorio con el que estamos trabajando:
output_dir = "bert-finetuned-ner-accelerate"
repo = Repository(output_dir, clone_from=repo_name)
Ahora podemos subir cualquier cosa que guardemos en output_dir llamando al método repo.push_to_hub(). Esto nos ayudará a subir los modelos intermedios al final de cada época.
Bucle de entrenamiento[[training-loop]]
Ahora estamos listos para escribir el bucle de entrenamiento completo. Para simplificar su parte de evaluación, definimos esta función postprocess() que toma predicciones y etiquetas y las convierte en listas de cadenas, como espera nuestro objeto metric:
def postprocess(predictions, labels):
predictions = predictions.detach().cpu().clone().numpy()
labels = labels.detach().cpu().clone().numpy()
# Remove ignored index (special tokens) and convert to labels
true_labels = [[label_names[l] for l in label if l != -100] for label in labels]
true_predictions = [
[label_names[p] for (p, l) in zip(prediction, label) if l != -100]
for prediction, label in zip(predictions, labels)
]
return true_labels, true_predictions
Luego podemos escribir el bucle de entrenamiento. Después de definir una barra de progreso para seguir cómo va el entrenamiento, el bucle tiene tres partes:
- El entrenamiento en sí, que es la iteración clásica sobre el
train_dataloader, el paso hacia adelante a través del modelo, luego el paso hacia atrás y el paso del optimizador. - La evaluación, en la que hay una novedad después de obtener las salidas de nuestro modelo en un lote: dado que dos procesos pueden haber rellenado las entradas y etiquetas con diferentes formas, necesitamos usar
accelerator.pad_across_processes()para que las predicciones y etiquetas tengan la misma forma antes de llamar al métodogather(). Si no hacemos esto, la evaluación dará un error o se colgará para siempre. Luego enviamos los resultados ametric.add_batch()y llamamos ametric.compute()una vez que el bucle de evaluación haya terminado. - Guardar y subir, donde primero guardamos el modelo y el tokenizador, luego llamamos a
repo.push_to_hub(). Observa que usamos el argumentoblocking=Falsepara indicarle a la biblioteca 🤗 Hub que haga el push en un proceso asíncrono. De esta manera, el entrenamiento continúa normalmente y esta instrucción (larga) se ejecuta en segundo plano.
Aquí está el código completo para el bucle de entrenamiento:
from tqdm.auto import tqdm
progress_bar = tqdm(range(num_training_steps))
for epoch in range(num_train_epochs):
# Training
model.train()
for batch in train_dataloader:
outputs = model(**batch)
loss = outputs.loss
accelerator.backward(loss)
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
progress_bar.update(1)
# Evaluation
model.eval()
for batch in eval_dataloader:
with torch.no_grad():
outputs = model(**batch)
predictions = outputs.logits.argmax(dim=-1)
labels = batch["labels"]
# Necessary to pad predictions and labels for being gathered
predictions = accelerator.pad_across_processes(predictions, dim=1, pad_index=-100)
labels = accelerator.pad_across_processes(labels, dim=1, pad_index=-100)
predictions_gathered = accelerator.gather(predictions)
labels_gathered = accelerator.gather(labels)
true_predictions, true_labels = postprocess(predictions_gathered, labels_gathered)
metric.add_batch(predictions=true_predictions, references=true_labels)
results = metric.compute()
print(
f"epoch {epoch}:",
{
key: results[f"overall_{key}"]
for key in ["precision", "recall", "f1", "accuracy"]
},
)
# Save and upload
accelerator.wait_for_everyone()
unwrapped_model = accelerator.unwrap_model(model)
unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save)
if accelerator.is_main_process:
tokenizer.save_pretrained(output_dir)
repo.push_to_hub(
commit_message=f"Training in progress epoch {epoch}", blocking=False
)
En caso de que sea la primera vez que ves un modelo guardado con 🤗 Accelerate, tomemos un momento para inspeccionar las tres líneas de código que lo acompañan:
accelerator.wait_for_everyone()
unwrapped_model = accelerator.unwrap_model(model)
unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save)
La primera línea se explica por sí misma: les dice a todos los procesos que esperen hasta que todos estén en esa etapa antes de continuar. Esto es para asegurarnos de tener el mismo modelo en cada proceso antes de guardar. Luego tomamos el unwrapped_model, que es el modelo base que definimos. El método accelerator.prepare() cambia el modelo para que funcione en entrenamiento distribuido, por lo que ya no tendrá el método save_pretrained(); el método accelerator.unwrap_model() deshace ese paso. Por último, llamamos a save_pretrained() pero le decimos a ese método que use accelerator.save() en lugar de torch.save().
Una vez hecho esto, deberías tener un modelo que produzca resultados bastante similares al entrenado con el Trainer. Puedes verificar el modelo que entrenamos usando este código en huggingface-course/bert-finetuned-ner-accelerate. Y si quieres probar cualquier ajuste al bucle de entrenamiento, ¡puedes implementarlos directamente editando el código mostrado arriba!
{/if}
Usando el modelo fine-tuned[[using-the-fine-tuned-model]]
Ya te hemos mostrado cómo puedes usar el modelo que ajustamos en el Model Hub con el widget de inferencia. Para usarlo localmente en un pipeline, solo tienes que especificar el identificador de modelo adecuado:
from transformers import pipeline
# Replace this with your own checkpoint
model_checkpoint = "huggingface-course/bert-finetuned-ner"
token_classifier = pipeline(
"token-classification", model=model_checkpoint, aggregation_strategy="simple"
)
token_classifier("My name is Sylvain and I work at Hugging Face in Brooklyn.")
[{'entity_group': 'PER', 'score': 0.9988506, 'word': 'Sylvain', 'start': 11, 'end': 18},
{'entity_group': 'ORG', 'score': 0.9647625, 'word': 'Hugging Face', 'start': 33, 'end': 45},
{'entity_group': 'LOC', 'score': 0.9986118, 'word': 'Brooklyn', 'start': 49, 'end': 57}]
¡Genial! ¡Nuestro modelo funciona tan bien como el predeterminado para esta pipeline!