Personalizar Trainer#
La canalización de entrenamiento de Ultralytics está diseñada en torno a BaseTrainer y entrenadores específicos de tareas como DetectionTrainer. Estas clases gestionan el bucle de entrenamiento, la validación, la creación de puntos de control y el registro de datos de forma predeterminada. Cuando necesites un mayor control —como realizar un seguimiento de métricas personalizadas, ajustar la ponderación de las pérdidas o implementar programaciones de tasa de aprendizaje—, puedes crear una subclase del entrenador y anular métodos específicos.
Esta guía repasa siete personalizaciones comunes:
- Registro de métricas personalizadas (puntuación F1) al final de cada época
- Adición de ponderaciones de clases para gestionar el desequilibrio de clases
- Guardado del mejor modelo en función de una métrica diferente
- Congelación de la red troncal durante las primeras N épocas y posterior descongelación
- Especificación de tasas de aprendizaje por capa
- Sincronización de BatchNorm entre GPU para el entrenamiento con múltiples GPU
- Configuración del recorte de gradientes para el ajuste de la estabilidad
Antes de leer esta guía, asegúrate de estar familiarizado con los fundamentos del entrenamiento de modelos YOLO y con la página de Personalización avanzada, que cubre la arquitectura de BaseTrainer.
Cómo funcionan los entrenadores personalizados#
La clase de modelo YOLO acepta un parámetro trainer en el método train(). Esto te permite pasar tu propia clase de entrenador que amplíe el comportamiento predeterminado:
from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer
class CustomTrainer(DetectionTrainer):
"""A custom trainer that extends DetectionTrainer with additional functionality."""
# Add your customizations here
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=10, trainer=CustomTrainer)Tu entrenador personalizado hereda toda la funcionalidad de DetectionTrainer, por lo que solo necesitas anular los métodos específicos que desees personalizar.
Registro de métricas personalizadas#
El paso de validación calcula la precisión, la exhaustividad y el mAP. Si necesitas métricas adicionales como la puntuación F1 por clase, anula validate():
import numpy as np
from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer
from ultralytics.utils import LOGGER
class MetricsTrainer(DetectionTrainer):
"""Custom trainer that computes and logs F1 score at the end of each epoch."""
def validate(self):
"""Run validation and compute per-class F1 scores."""
metrics, fitness = super().validate()
if metrics is None:
return metrics, fitness
if hasattr(self.validator, "metrics") and hasattr(self.validator.metrics, "box"):
box = self.validator.metrics.box
f1_per_class = box.f1
class_indices = box.ap_class_index
names = self.validator.names
valid_f1 = f1_per_class[f1_per_class > 0]
mean_f1 = np.mean(valid_f1) if len(valid_f1) > 0 else 0.0
LOGGER.info(f"Mean F1 Score: {mean_f1:.4f}")
per_class_str = [
f"{names[i]}: {f1_per_class[j]:.3f}" for j, i in enumerate(class_indices) if f1_per_class[j] > 0
]
LOGGER.info(f"Per-class F1: {per_class_str}")
return metrics, fitness
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=5, trainer=MetricsTrainer)Esto registra la puntuación F1 media en todas las clases y un desglose por clase después de cada ejecución de validación.
El validador proporciona acceso a muchas métricas a través de self.validator.metrics.box:
| Atributo | Descripción |
|---|---|
f1 | Puntuación F1 por clase |
image_metrics | Diccionario de métricas por imagen con precisión, recall, F1, TP, FP y FN |
p | Precisión por clase |
r | Recall por clase |
ap50 | AP con IoU 0.5 por clase |
ap | AP con IoU 0.5:0.95 por clase |
mp, mr | Precisión y recall medios |
map50, map | Métricas de AP medio |
Añadir pesos de clase#
Si tu conjunto de datos tiene clases desequilibradas (por ejemplo, un defecto poco común en una inspección de fabricación), puedes dar mayor peso a las clases infrarrepresentadas en la función de pérdida. Esto hace que el modelo penalice con más fuerza las clasificaciones erróneas en las clases raras.
Para personalizar la pérdida, crea una subclase de las clases de pérdida, el modelo y el entrenador:
import torch
from torch import nn
from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer
from ultralytics.nn.tasks import DetectionModel
from ultralytics.utils import RANK
from ultralytics.utils.loss import E2ELoss, v8DetectionLoss
class WeightedDetectionLoss(v8DetectionLoss):
"""Detection loss with class weights applied to BCE classification loss."""
def __init__(self, model, class_weights=None, tal_topk=10, tal_topk2=None):
"""Initialize loss with optional per-class weights for BCE."""
super().__init__(model, tal_topk=tal_topk, tal_topk2=tal_topk2)
if class_weights is not None:
self.bce = nn.BCEWithLogitsLoss(
pos_weight=class_weights.to(self.device),
reduction="none",
)
class WeightedE2ELoss(E2ELoss):
"""E2E Loss with class weights for YOLO26."""
def __init__(self, model, class_weights=None):
"""Initialize E2E loss with weighted detection loss."""
def weighted_loss_fn(model, tal_topk=10, tal_topk2=None):
return WeightedDetectionLoss(model, class_weights=class_weights, tal_topk=tal_topk, tal_topk2=tal_topk2)
super().__init__(model, loss_fn=weighted_loss_fn)
class WeightedDetectionModel(DetectionModel):
"""Detection model that uses class-weighted loss."""
def init_criterion(self):
"""Initialize weighted loss criterion with per-class weights."""
class_weights = torch.ones(self.nc)
class_weights[0] = 2.0 # upweight class 0
class_weights[1] = 3.0 # upweight rare class 1
return WeightedE2ELoss(self, class_weights=class_weights)
class WeightedTrainer(DetectionTrainer):
"""Trainer that returns a WeightedDetectionModel."""
def get_model(self, cfg=None, weights=None, verbose=True):
"""Return a WeightedDetectionModel."""
model = WeightedDetectionModel(cfg, nc=self.data["nc"], verbose=verbose and RANK == -1)
if weights:
model.load(weights)
return model
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=10, trainer=WeightedTrainer)Puedes calcular los pesos de las clases automáticamente a partir de la distribución de etiquetas de tu conjunto de datos. Un enfoque común es la ponderación por frecuencia inversa:
import numpy as np
# class_counts: number of instances per class
class_counts = np.array([5000, 200, 3000])
# Inverse frequency: rarer classes get higher weight
class_weights = max(class_counts) / class_counts
# Result: [1.0, 25.0, 1.67]Las clases personalizadas como WeightedDetectionModel se almacenan en el punto de control por referencia. Cuando se definen en un script de entrenamiento, pertenecen al módulo __main__, por lo que cargar best.pt desde un script diferente genera el error AttributeError: Can't get attribute 'WeightedDetectionModel' on <module '__main__'>.
Define las clases personalizadas en un módulo dedicado para que sigan siendo importables, y asegúrate de que dicho módulo esté en tu PYTHONPATH en el momento de la carga.
# weighted_model.py
from ultralytics.nn.tasks import DetectionModel
class WeightedDetectionModel(DetectionModel):
"""Detection model that uses class-weighted loss."""# inference script
from weighted_model import WeightedDetectionModel # noqa: F401 - must be importable at checkpoint load time
from ultralytics import YOLO
model = YOLO("runs/detect/train/weights/best.pt")
metrics = model.val()Guardar el mejor modelo por métrica personalizada#
El entrenador guarda best.pt en función de la adecuación (fitness), cuyo valor predeterminado para la detección es mAP@0.5:0.95 (pondera [0.0, 0.0, 0.0, 1.0] para [P, R, mAP@0.5, mAP@0.5:0.95]). Para usar una métrica diferente (como mAP@0.5 o la exhaustividad), anula validate() y devuelve la métrica elegida como el valor de adecuación. El elemento integrado save_model() la utilizará automáticamente a continuación:
from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer
class CustomSaveTrainer(DetectionTrainer):
"""Trainer that saves the best model based on mAP@0.5 instead of default fitness."""
def validate(self):
"""Override fitness to use mAP@0.5 for best model selection."""
metrics, fitness = super().validate()
if metrics:
fitness = metrics.get("metrics/mAP50(B)", fitness)
if self.best_fitness is None or fitness > self.best_fitness:
self.best_fitness = fitness
return metrics, fitness
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=20, trainer=CustomSaveTrainer)Las métricas comunes disponibles en self.metrics tras la validación incluyen:
| Clave | Descripción |
|---|---|
metrics/precision(B) | Precisión |
metrics/recall(B) | Recall |
metrics/mAP50(B) | mAP con IoU 0.5 |
metrics/mAP50-95(B) | mAP con IoU 0.5:0.95 |
Congelar y descongelar el backbone#
Los flujos de trabajo de aprendizaje por transferencia suelen beneficiarse de la congelación de la red troncal preentrenada durante las primeras N épocas, lo que permite que la cabeza de detección se adapte antes de realizar el ajuste fino de toda la red. Ultralytics proporciona un parámetro freeze para congelar capas al inicio del entrenamiento, y puedes usar una retrollamada para descongelarlas después de N épocas:
from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer
from ultralytics.utils import LOGGER
FREEZE_EPOCHS = 5
def unfreeze_backbone(trainer):
"""Callback to unfreeze all layers after FREEZE_EPOCHS."""
if trainer.epoch == FREEZE_EPOCHS:
LOGGER.info(f"Epoch {trainer.epoch}: Unfreezing all layers for fine-tuning")
for name, param in trainer.model.named_parameters():
if not param.requires_grad:
param.requires_grad = True
LOGGER.info(f" Unfroze: {name}")
trainer.freeze_layer_names = [".dfl"]
class FreezingTrainer(DetectionTrainer):
"""Trainer with backbone freezing for first N epochs."""
def __init__(self, *args, **kwargs):
"""Initialize and register the unfreeze callback."""
super().__init__(*args, **kwargs)
self.add_callback("on_train_epoch_start", unfreeze_backbone)
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=20, freeze=10, trainer=FreezingTrainer)El parámetro freeze=10 congela las primeras 10 capas (índices 0-9) al inicio del entrenamiento, lo que cubre la mayor parte de la red troncal de YOLO26. La red troncal abarca las capas 0-10, por lo que freeze=10 deja el bloque C2PSA final (capa 10) entrenable; utiliza freeze=11 para congelar toda la red troncal. La retrollamada on_train_epoch_start se ejecuta al principio de cada época y descongela todos los parámetros una vez concluido el periodo de congelación.
freeze=10congela las primeras 10 capas, índices 0-9 (la mayor parte de la red troncal de YOLO26; utilizafreeze=11para incluir el bloque C2PSA final en la capa 10)freeze=[0, 1, 2, 3]congela capas específicas por índice- Unos valores más altos de
FREEZE_EPOCHSotorgan a la cabeza más tiempo para adaptarse antes de que la red troncal cambie
Tasas de aprendizaje por capa#
Diferentes partes de la red pueden beneficiarse de distintas tasas de aprendizaje. Una estrategia común consiste en utilizar una tasa de aprendizaje más baja para la red troncal preentrenada con el fin de preservar las características aprendidas, al tiempo que se permite que la cabeza de detección se adapte más rápidamente con una tasa superior:
import torch
from ultralytics import YOLO
from ultralytics.models.yolo.detect import DetectionTrainer
from ultralytics.utils import LOGGER
from ultralytics.utils.torch_utils import unwrap_model
class PerLayerLRTrainer(DetectionTrainer):
"""Trainer with different learning rates for backbone and head."""
def build_optimizer(self, model, name="auto", lr=0.001, momentum=0.9, decay=1e-5, iterations=1e5):
"""Build optimizer with separate learning rates for backbone and head."""
backbone_params = []
head_params = []
unwrapped = unwrap_model(model)
backbone_len = len(unwrapped.yaml["backbone"]) # YOLO26 backbone spans layers 0-10 (C2PSA at layer 10)
for k, v in unwrapped.named_parameters():
if not v.requires_grad:
continue
is_backbone = any(k.startswith(f"model.{i}.") for i in range(backbone_len))
if is_backbone:
backbone_params.append(v)
else:
head_params.append(v)
backbone_lr = lr * 0.1
optimizer = torch.optim.AdamW(
[
{"params": backbone_params, "lr": backbone_lr, "weight_decay": decay},
{"params": head_params, "lr": lr, "weight_decay": decay},
],
)
LOGGER.info(
f"PerLayerLR optimizer: backbone ({len(backbone_params)} params, lr={backbone_lr}) "
f"| head ({len(head_params)} params, lr={lr})"
)
return optimizer
model = YOLO("yolo26n.pt")
model.train(data="coco8.yaml", epochs=20, trainer=PerLayerLRTrainer)Variante de RT-DETR#
Para RT-DETR el patrón es el mismo con dos mejoras. La longitud de la red troncal se lee de model.yaml["backbone"], por lo que el mismo entrenador funciona en diferentes variantes de RT-DETR (redes troncales RT-DETR-L, RT-DETR-X, ResNet-50/101) sin necesidad de codificar de forma rígida el recuento de capas. Los parámetros también se dividen en grupos de pesos, BatchNorm y sesgos dentro de cada sección para que la descomposición de pesos (weight decay) se excluya de los parámetros de BatchNorm y de los sesgos, lo que coincide con la política del entrenador predeterminado. Esto resulta especialmente útil para el ajuste fino de RT-DETR, donde la cabeza del descodificador suele inicializarse de forma aleatoria, mientras que la red troncal transporta características preentrenadas que se benefician de una tasa de aprendizaje más baja:
import torch
from torch import nn
from ultralytics import RTDETR
from ultralytics.models.rtdetr.train import RTDETRTrainer
from ultralytics.utils import LOGGER, colorstr
from ultralytics.utils.torch_utils import unwrap_model
class RTDETRBackboneLRTrainer(RTDETRTrainer):
"""RT-DETR trainer with a lower learning rate for backbone parameters."""
backbone_lr_ratio = 0.1 # backbone learning rate as a fraction of head learning rate
def build_optimizer(self, model, name="auto", lr=0.001, momentum=0.9, decay=1e-5, iterations=1e5):
"""Build an AdamW optimizer with six param groups: head and backbone x {weight, bn, bias}."""
# Resolve optimizer name; "auto" maps to AdamW with RT-DETR-style defaults
canonical = {"Adam", "Adamax", "AdamW", "NAdam", "RAdam", "auto"}
name = {x.lower(): x for x in canonical}.get(name.lower(), name)
if name == "auto":
name, lr, momentum = "AdamW", 1e-4, 0.9
self.args.warmup_bias_lr = 0.0 # RT-DETR warms biases from 0, unlike YOLO's 0.1
if name not in {"Adam", "Adamax", "AdamW", "NAdam", "RAdam"}:
raise NotImplementedError(f"This trainer only supports AdamW-family optimizers; got {name}")
# Identify backbone parameters from model.yaml and route each param into a (section, kind) group
unwrapped = unwrap_model(model)
backbone_len = len(unwrapped.yaml["backbone"])
norm_types = tuple(v for k, v in nn.__dict__.items() if "Norm" in k)
groups = {f"{s}_{k}": [] for s in ("head", "backbone") for k in ("weight", "bn", "bias")}
for module_name, module in unwrapped.named_modules():
for param_name, param in module.named_parameters(recurse=False):
if not param.requires_grad:
continue
fullname = f"{module_name}.{param_name}" if module_name else param_name
parts = fullname.split(".")
section = (
"backbone"
if len(parts) > 1 and parts[0] == "model" and parts[1].isdigit() and int(parts[1]) < backbone_len
else "head"
)
if "bias" in param_name:
kind = "bias"
elif isinstance(module, norm_types) or "logit_scale" in fullname:
kind = "bn"
else:
kind = "weight"
groups[f"{section}_{kind}"].append(param)
# Build the optimizer with per-group lr and weight decay; backbone groups use lr * backbone_lr_ratio
backbone_lr = lr * self.backbone_lr_ratio
param_groups = [
{"params": groups["head_weight"], "lr": lr, "weight_decay": decay, "param_group": "weight"},
{"params": groups["head_bn"], "lr": lr, "weight_decay": 0.0, "param_group": "bn"},
{"params": groups["head_bias"], "lr": lr, "weight_decay": 0.0, "param_group": "bias"},
{"params": groups["backbone_weight"], "lr": backbone_lr, "weight_decay": decay, "param_group": "weight"},
{"params": groups["backbone_bn"], "lr": backbone_lr, "weight_decay": 0.0, "param_group": "bn"},
{"params": groups["backbone_bias"], "lr": backbone_lr, "weight_decay": 0.0, "param_group": "bias"},
]
param_groups = [pg for pg in param_groups if pg["params"]] # drop empty groups
optimizer = getattr(torch.optim, name)(param_groups, betas=(momentum, 0.999))
LOGGER.info(
f"{colorstr('optimizer:')} {name}(lr={lr}, backbone_lr={backbone_lr}) with parameter groups\n"
f" Head: {len(groups['head_bn'])} bn, {len(groups['head_weight'])} weight(decay={decay}), "
f"{len(groups['head_bias'])} bias (lr={lr})\n"
f" Backbone: {len(groups['backbone_bn'])} bn, {len(groups['backbone_weight'])} weight(decay={decay}), "
f"{len(groups['backbone_bias'])} bias (lr={backbone_lr})"
)
return optimizer
model = RTDETR("rtdetr-l.pt")
model.train(data="coco8.yaml", epochs=20, trainer=RTDETRBackboneLRTrainer)Un punto de partida común es backbone_lr_ratio = 0.1, que coincide con la configuración original de RT-DETR con su red troncal HGNetV2. La literatura sugiere escalar la proporción de forma inversamente proporcional al tamaño de la red troncal y a la escala de los datos de preentrenamiento: las redes troncales grandes preentrenadas en conjuntos de datos muy grandes (por ejemplo, ViT-L/H entrenados con DINO, CLIP o MAE en cientos de millones de imágenes) suelen utilizar proporciones más pequeñas como 0.01 o inferiores para preservar las características bien aprendidas, mientras que las redes troncales más pequeñas con un preentrenamiento más ligero toleran proporciones mayores como 0.5 o superiores.
El programador de tasa de aprendizaje integrado (cosine o linear) sigue aplicándose además de las tasas de aprendizaje base por grupo. Tanto las tasas de aprendizaje de la red troncal como las de la cabeza seguirán la misma programación de decaimiento, manteniendo la proporción entre ambas durante todo el entrenamiento.
Estas personalizaciones pueden combinarse en una única clase de entrenador sobrescribiendo múltiples métodos y añadiendo callbacks según sea necesario.
BatchNorm sincronizado para entrenamiento multi-GPU#
Al entrenar en múltiples GPU con DistributedDataParallel, las capas de BatchNorm2d predeterminadas calculan las estadísticas de forma independiente en cada GPU. Para el ajuste fino de RT-DETR y otras recetas que utilizan tamaños de lote pequeños por GPU, las estadísticas de lotes por GPU pueden ser ruidosas. La función SyncBatchNorm de PyTorch sincroniza la media y la varianza en todos los rangos para obtener una única estadística de lote global, lo que a menudo mejora la convergencia a cambio de un pequeño coste general de comunicación entre GPU.
La conversión debe realizarse después de que el modelo esté en la GPU, pero antes de que DDP lo envuelva. El punto de enlace (hook) más limpio para esto es set_model_attributes(), al que BaseTrainer llama exactamente en esa ventana:
from torch import nn
from ultralytics import RTDETR
from ultralytics.models.rtdetr.train import RTDETRTrainer
class SyncBNTrainer(RTDETRTrainer):
"""RT-DETR trainer that converts BatchNorm to SyncBatchNorm for multi-GPU training."""
def set_model_attributes(self):
"""Run the parent setup, then convert BN to SyncBatchNorm when training on multiple GPUs."""
super().set_model_attributes()
if self.world_size > 1:
self.model = nn.SyncBatchNorm.convert_sync_batchnorm(self.model)
model = RTDETR("rtdetr-l.pt")
model.train(data="coco8.yaml", epochs=20, device=[0, 1], trainer=SyncBNTrainer)La protección world_size > 1 garantiza que el entrenador se pueda utilizar de forma segura también en ejecuciones con una sola GPU; en una sola GPU se omite la conversión y el entrenamiento continúa con el elemento normal BatchNorm2d. El mismo patrón funciona para YOLO cambiando la clase principal a DetectionTrainer.
| Escenario | Recomendación |
|---|---|
| Entrenamiento multi-GPU, batch pequeño por GPU (≤ 16) | Habilitar |
| Entrenamiento multi-GPU, batch grande por GPU (≥ 32) | Opcional; beneficio menor |
| Entrenamiento de una sola GPU | No aplicable (se omite) |
Recorte de gradiente configurable#
El entrenador predeterminado recorta los gradientes a max_norm=10.0 en optimizer_step(), un valor flexible ajustado para los modelos YOLO en los que los gradientes rara vez lo superan. Los detectores de la familia DETR (RT-DETR, DEIM, DINO) suelen utilizar valores mucho más estrictos como 0.1 para estabilizar las capas de atención cruzada del descodificador, donde las magnitudes de los gradientes pueden dispararse. Para anular el valor de recorte, crea una subclase del entrenador y anula optimizer_step():
import torch
from ultralytics import RTDETR
from ultralytics.models.rtdetr.train import RTDETRTrainer
class CustomClipTrainer(RTDETRTrainer):
"""RT-DETR trainer with configurable gradient clipping."""
clip_grad_norm = 0.1 # max gradient norm; set to 0 to disable clipping
def optimizer_step(self):
"""Run an optimizer step with a configurable gradient-norm clip."""
self.scaler.unscale_(self.optimizer)
if self.clip_grad_norm > 0:
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=self.clip_grad_norm)
self.scaler.step(self.optimizer)
self.scaler.update()
self.optimizer.zero_grad()
if self.ema:
self.ema.update(self.model)
model = RTDETR("rtdetr-l.pt")
model.train(data="coco8.yaml", epochs=20, trainer=CustomClipTrainer)El mismo entrenador funciona para YOLO cambiando la clase principal a DetectionTrainer (from ultralytics.models.yolo.detect import DetectionTrainer) y cargando un punto de control de YOLO con YOLO("yolo26n.pt"). El cuerpo de optimizer_step no cambia.
| Familia de arquitectura | max_norm típico |
|---|---|
| Familia RT-DETR / DEIM / DETR | 0.1 |
| YOLO (predeterminado de Ultralytics) | 10.0 |
| Deshabilitar recorte | 0 |
FAQ#
Pasa tu clase de entrenador personalizada (no una instancia) al parámetro
trainerenmodel.train():from ultralytics import YOLO model = YOLO("yolo26n.pt") model.train(data="coco8.yaml", trainer=MyCustomTrainer)La clase
YOLOgestiona la instanciación del entrenador internamente. Consulta la página de Personalización avanzada para obtener más detalles sobre la arquitectura del entrenador.Métodos clave disponibles para personalización:
Método Propósito validate()Ejecuta la validación y devuelve las métricas build_optimizer()Construye el optimizador save_model()Guarda los puntos de control del entrenamiento get_model()Devuelve la instancia del modelo get_validator()Devuelve la instancia del validador get_dataloader()Construye el cargador de datos preprocess_batch()Preprocesa el lote de entrada label_loss_items()Formatea los elementos de pérdida para el registro Para ver la referencia completa de la API, consulta la documentación de
BaseTrainer.Sí, para personalizaciones más sencillas, las retrollamadas suelen ser suficientes. Los eventos de retrollamada disponibles incluyen
on_train_start,on_train_epoch_start,on_train_epoch_end,on_fit_epoch_endyon_model_save. Estos te permiten conectarte al bucle de entrenamiento sin necesidad de crear subclases. El ejemplo de congelación de la red troncal que se muestra arriba ilustra este enfoque.Si tu cambio es más sencillo (como ajustar las ganancias de pérdida), puedes modificar los hiperparámetros directamente:
model.train(data="coco8.yaml", box=10.0, cls=1.5, dfl=2.0)Para realizar cambios estructurales en la función de pérdida (como añadir ponderaciones de clases), debes crear una subclase de la pérdida y del modelo tal como se muestra en la sección de ponderaciones de clases.