如何在TensorFlow中为多分类与二分类任务实现Macro F1 Score?
修复TensorFlow Addons F1Score使用问题及Macro F1计算方案
错误原因
tfa.metrics.F1Score默认要求输入的标签为one-hot编码格式,且预测输出的维度需与类别数一致。而你的两个任务中:
- 多分类任务使用了稀疏整数标签(
y_train形状为(N,)),损失函数为sparse_categorical_crossentropy,但F1Score未配置对应参数处理稀疏标签; - 二分类任务使用了
sigmoid输出(形状为(N,1)),但F1Score(num_classes=2)期望预测输出为(N,2)的one-hot格式,导致形状不匹配。
针对两个任务的修复方案
1. 多分类任务(3类)
方案A:转换标签为one-hot编码
将整数标签转为one-hot格式,同时修改损失函数为categorical_crossentropy:
from tensorflow.keras.utils import to_categorical from tensorflow_addons.metrics import F1Score # 转换标签 y_train_onehot = to_categorical(y_train, num_classes=3) y_val_onehot = to_categorical(y_val, num_classes=3) # 模型定义与编译 preds = layers.Dense(3, activation="softmax")(x) model = keras.Model(int_sequences_input, preds) f1_macro = F1Score(num_classes=3, average='macro') model.compile( loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy', f1_macro] ) # 训练 model.fit(X_train, y_train_onehot, validation_data=(X_val, y_val_onehot), ...)
方案B:使用稀疏标签参数(TensorFlow Addons 0.12+支持)
无需修改标签,直接给F1Score添加sparse=True参数,适配稀疏整数标签:
from tensorflow_addons.metrics import F1Score preds = layers.Dense(3, activation="softmax")(x) model = keras.Model(int_sequences_input, preds) # 配置sparse参数处理整数标签 f1_macro = F1Score(num_classes=3, average='macro', sparse=True) model.compile( loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy', f1_macro] ) # 训练 model.fit(X_train, y_train, ...)
2. 二分类任务(2类)
方案A:改用softmax输出+one-hot标签
调整网络输出为2类softmax,转换标签为one-hot,适配F1Score:
from tensorflow.keras.utils import to_categorical from tensorflow_addons.metrics import F1Score # 转换标签 y_train_onehot = to_categorical(y_train, num_classes=2) y_val_onehot = to_categorical(y_val, num_classes=2) # 模型定义与编译 preds = layers.Dense(2, activation="softmax")(x) model = keras.Model(int_sequences_input, preds) f1_macro = F1Score(num_classes=2, average='macro') model.compile( loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy', f1_macro] ) # 训练 model.fit(X_train, y_train_onehot, ...)
方案B:使用BinaryF1Score适配sigmoid输出
保持sigmoid输出不变,改用tfa.metrics.BinaryF1Score(二分类的Macro F1与Binary F1结果一致):
from tensorflow_addons.metrics import BinaryF1Score preds = layers.Dense(1, activation="sigmoid")(x) model = keras.Model(int_sequences_input, preds) # 设置分类阈值(可根据需求调整) f1_macro = BinaryF1Score(threshold=0.5) model.compile( loss='binary_crossentropy', optimizer='adam', metrics=['accuracy', f1_macro] ) # 训练 model.fit(X_train, y_train, ...)
自定义Macro F1指标(替代方案)
如果不想依赖TensorFlow Addons,可以自定义Keras指标类,更灵活适配不同标签格式:
多分类自定义Macro F1
import tensorflow as tf class MacroF1(tf.keras.metrics.Metric): def __init__(self, num_classes, name='macro_f1', **kwargs): super().__init__(name=name, **kwargs) self.num_classes = num_classes self.precisions = self.add_weight(name='precisions', shape=(num_classes,), initializer='zeros') self.recalls = self.add_weight(name='recalls', shape=(num_classes,), initializer='zeros') self.class_counts = self.add_weight(name='class_counts', shape=(num_classes,), initializer='zeros') def update_state(self, y_true, y_pred, sample_weight=None): # 处理稀疏整数标签转one-hot if y_true.shape.rank == 1: y_true = tf.one_hot(tf.cast(y_true, tf.int32), depth=self.num_classes) # 预测结果转one-hot y_pred = tf.one_hot(tf.argmax(y_pred, axis=-1), depth=self.num_classes) true_pos = tf.reduce_sum(y_true * y_pred, axis=0) pred_pos = tf.reduce_sum(y_pred, axis=0) actual_pos = tf.reduce_sum(y_true, axis=0) # 避免除以0 precision = tf.where(pred_pos > 0, true_pos / pred_pos, 0.0) recall = tf.where(actual_pos > 0, true_pos / actual_pos, 0.0) self.precisions.assign_add(precision) self.recalls.assign_add(recall) self.class_counts.assign_add(tf.cast(actual_pos > 0, tf.float32)) def result(self): f1_scores = 2 * (self.precisions * self.recalls) / (self.precisions + self.recalls + tf.keras.backend.epsilon()) return tf.reduce_sum(f1_scores) / tf.reduce_sum(self.class_counts) # 使用示例 f1_macro = MacroF1(num_classes=3) model.compile(loss='sparse_categorical_crossentropy', optimizer='adam', metrics=['accuracy', f1_macro])
二分类自定义Macro F1
import tensorflow as tf class BinaryMacroF1(tf.keras.metrics.Metric): def __init__(self, threshold=0.5, name='binary_macro_f1', **kwargs): super().__init__(name=name, **kwargs) self.threshold = threshold self.tp = self.add_weight(name='tp', initializer='zeros') self.tn = self.add_weight(name='tn', initializer='zeros') self.fp = self.add_weight(name='fp', initializer='zeros') self.fn = self.add_weight(name='fn', initializer='zeros') def update_state(self, y_true, y_pred, sample_weight=None): y_pred = tf.cast(y_pred >= self.threshold, tf.float32) y_true = tf.cast(y_true, tf.float32) self.tp.assign_add(tf.reduce_sum(y_true * y_pred)) self.tn.assign_add(tf.reduce_sum((1 - y_true) * (1 - y_pred))) self.fp.assign_add(tf.reduce_sum((1 - y_true) * y_pred)) self.fn.assign_add(tf.reduce_sum(y_true * (1 - y_pred))) def result(self): # 计算两个类的F1并平均 precision_0 = self.tn / (self.tn + self.fn + tf.keras.backend.epsilon()) recall_0 = self.tn / (self.tn + self.fp + tf.keras.backend.epsilon()) f1_0 = 2 * (precision_0 * recall_0) / (precision_0 + recall_0 + tf.keras.backend.epsilon()) precision_1 = self.tp / (self.tp + self.fp + tf.keras.backend.epsilon()) recall_1 = self.tp / (self.tp + self.fn + tf.keras.backend.epsilon()) f1_1 = 2 * (precision_1 * recall_1) / (precision_1 + recall_1 + tf.keras.backend.epsilon()) return (f1_0 + f1_1) / 2 # 使用示例 f1_macro = BinaryMacroF1(threshold=0.5) model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy', f1_macro])
基于Sklearn的回调计算方案
如果只需要在验证集上计算Macro F1,可以结合Sklearn的f1_score和Keras回调:
from sklearn.metrics import f1_score from tensorflow.keras.callbacks import Callback class MacroF1Callback(Callback): def __init__(self, X_val, y_val, is_binary=False): super().__init__() self.X_val = X_val self.y_val = y_val self.is_binary = is_binary def on_epoch_end(self, epoch, logs=None): y_pred = self.model.predict(self.X_val, verbose=0) if self.is_binary: y_pred = (y_pred >= 0.5).astype(int).flatten() else: y_pred = y_pred.argmax(axis=1) macro_f1 = f1_score(self.y_val, y_pred, average='macro') logs['val_macro_f1'] = macro_f1 print(f"\nEpoch {epoch+1}: Validation Macro F1 = {macro_f1:.4f}") # 多分类使用 val_callback = MacroF1Callback(X_val, y_val) model.fit(X_train, y_train, validation_data=(X_val, y_val), callbacks=[val_callback], ...) # 二分类使用 val_callback = MacroF1Callback(X_val, y_val, is_binary=True) model.fit(X_train, y_train, validation_data=(X_val, y_val), callbacks=[val_callback], ...)
内容的提问来源于stack exchange,提问作者Filcenzo
相关产品推荐
相关产品推荐

