Karinasdog 2025-08-21 16:41 采纳率: 0%
浏览 7

mobilefacenet

我訓練MobileFaceNet模型時遇到特徵崩潰的問題導致分辨不出人臉,因為要部署在HT32F49395上,所以我有把他轉成INT8,一開始以為是訓練問題有更改過各種參數還是一樣 以下是輸出結果

 人數: 3
  向量形狀 (N,D): (3, 64)
INFO: Created TensorFlow Lite XNNPACK delegate for CPU.
TFLite: E:\python\TinyMobileFaceNet\runs\2025-08-20_tmfn\tflite\tmfn_fp16.tflite
  模型輸出維度: 64  dtype: float32
  L2 範數 min/max: 0.9999999403953552 1.0
  維度方差均值: 1.0199328670523755e-07
  兩兩餘弦(非對角) mean/max/min: 0.9999902844429016 0.9999929666519165 0.9999876022338867

這是完整的訓練程式

# train_rescue.py  
import os, random, glob, cv2, numpy as np, argparse
from pathlib import Path
from datetime import datetime
import tensorflow as tf
from tensorflow.keras import layers, models

# ========= 路徑與超參 =========
IMG_SIZE   = 112
EMB_DIM    = 64
DATA_DIR   = Path("data") / "dataset"   
BATCH      = 64                         # P×K:由 P_CLASSES 與 BATCH 推得 K
P_CLASSES  = 16
STEPS_PER_EPOCH = 150
VAL_SPLIT  = 0.15
EPOCHS_A, EPOCHS_B = 15, 70
LR_A, LR_B = 1e-3, 1.25e-4
MARGIN = 0.4

# 解析命令列
ap = argparse.ArgumentParser()
ap.add_argument("--data", type=str)
ap.add_argument("--epA", type=int)
ap.add_argument("--epB", type=int)
ap.add_argument("--lrA", type=float)
ap.add_argument("--lrB", type=float)
ap.add_argument("--margin", type=float)
args, _ = ap.parse_known_args()
if args.data:   DATA_DIR = Path(args.data)
if args.epA:    EPOCHS_A = args.epA
if args.epB:    EPOCHS_B = args.epB
if args.lrA:    LR_A = args.lrA
if args.lrB:    LR_B = args.lrB
if args.margin: MARGIN = args.margin

RUN_DIR    = Path("runs") / (datetime.now().strftime("%Y-%m-%d") + "_tmfn")
MODELS_DIR = RUN_DIR / "models"
LOGS_DIR   = RUN_DIR / "logs"
for d in [MODELS_DIR, LOGS_DIR]: d.mkdir(parents=True, exist_ok=True)
BEST_KERAS = MODELS_DIR / "best.keras"
LAST_KERAS = MODELS_DIR / "last.keras"

# ========= 小工具 =========
def _imread_u(p): return cv2.imdecode(np.fromfile(p, np.uint8), cv2.IMREAD_COLOR)
def _pre_bgr(im, size=IMG_SIZE):
    im = cv2.cvtColor(im, cv2.COLOR_BGR2RGB)
    im = cv2.resize(im, (size, size), interpolation=cv2.INTER_AREA)
    return (im.astype(np.float32)/127.5) - 1.0

def bn_relu(x):
    x = layers.BatchNormalization()(x)
    return layers.ReLU(6.0)(x)

def dw_sep(x, out_ch, stride=1):
    x = layers.DepthwiseConv2D(3, strides=stride, padding="same", use_bias=False)(x); x = bn_relu(x)
    x = layers.Conv2D(out_ch, 1, padding="same", use_bias=False)(x); x = bn_relu(x)
    return x

def tiny_mobilefacenet(input_shape=(IMG_SIZE, IMG_SIZE, 3), emb_dim=EMB_DIM):
    inp = layers.Input(input_shape)
    x = layers.Conv2D(16, 3, strides=2, padding="same", use_bias=False)(inp); x = bn_relu(x)
    x = dw_sep(x, 32, 1)
    x = dw_sep(x, 64, 2)
    x = dw_sep(x, 64, 1)
    x = dw_sep(x, 128, 2)
    x = dw_sep(x, 128, 1)
    x = dw_sep(x, 128, 1)
    x = dw_sep(x, 256, 2)
    x = dw_sep(x, 256, 1)
    x = layers.GlobalAveragePooling2D()(x)
    feat = layers.Dense(emb_dim, use_bias=False, name="feat")(x)        # 未正規化特徵
    emb  = layers.Lambda(lambda t: tf.math.l2_normalize(t, axis=1), name="emb")(feat)  # L2
    return models.Model(inp, [emb, feat], name="TinyMobileFaceNet")

def scan_classes(root):
    cs = [d for d in sorted(os.listdir(root)) if os.path.isdir(os.path.join(root,d))]
    if not cs: raise RuntimeError(f"資料夾是空的:{root}")
    return cs, {c:i for i,c in enumerate(cs)}

def make_val_dataset(path, batch=BATCH, val_split=VAL_SPLIT, seed=42):
    _ = tf.keras.preprocessing.image_dataset_from_directory(
        path, label_mode="int", image_size=(IMG_SIZE, IMG_SIZE),
        batch_size=batch, shuffle=True, validation_split=val_split, subset="training", seed=seed)
    val_ds = tf.keras.preprocessing.image_dataset_from_directory(
        path, label_mode="int", image_size=(IMG_SIZE, IMG_SIZE),
        batch_size=batch, shuffle=False, validation_split=val_split, subset="validation", seed=seed)
    def _pre_tf(x,y): return (tf.cast(x, tf.float32)/127.5 - 1.0, y)
    val_ds = val_ds.map(_pre_tf, num_parallel_calls=tf.data.AUTOTUNE)
    val_ds = val_ds.cache().prefetch(tf.data.AUTOTUNE)
    val_ds = val_ds.map(lambda x,y: (x, {"emb":y, "cls":y}))
    return val_ds

def make_pk_dataset(root, batch=BATCH, P=P_CLASSES, steps=STEPS_PER_EPOCH, seed=42):
    rng = random.Random(seed)
    classes, cls2idx = scan_classes(root)
    P_eff = min(P, len(classes))
    assert batch % P_eff == 0, f"BATCH({batch}) 必須能被有效 P({P_eff}) 整除"
    K = batch // P_eff
    assert K >= 2, "Triplet 需要 K >= 2"
    pool = {}
    for c in classes:
        files = []
        for e in ("*.jpg","*.jpeg","*.png","*.bmp","*.pgm","*.ppm","*.JPG","*.PNG","*.JPEG","*.BMP"):
            files += glob.glob(os.path.join(root, c, "**", e), recursive=True)
        if files: pool[c]=files
    keys = list(pool.keys())

    def gen():
        while True:
            chosen = rng.sample(keys, P_eff) if len(keys)>=P_eff else [rng.choice(keys) for _ in range(P_eff)]
            X,Y = [],[]
            for c in chosen:
                fs = pool[c]
                pick = rng.sample(fs, K) if len(fs)>=K else [rng.choice(fs) for _ in range(K)]
                for p in pick:
                    im = _imread_u(p)
                    if im is None: continue
                    X.append(_pre_bgr(im)); Y.append(cls2idx[c])
            while len(X)<batch:
                c=rng.choice(keys); im=_imread_u(rng.choice(pool[c]))
                if im is None: continue
                X.append(_pre_bgr(im)); Y.append(cls2idx[c])
            x = np.stack(X,0).astype(np.float32); y = np.array(Y,np.int32)
            yield (x, {"emb":y, "cls":y})
    spec = (
        tf.TensorSpec((batch,IMG_SIZE,IMG_SIZE,3), tf.float32),
        {"emb": tf.TensorSpec((batch,), tf.int32), "cls": tf.TensorSpec((batch,), tf.int32)}
    )
    ds = tf.data.Dataset.from_generator(gen, output_signature=spec).prefetch(tf.data.AUTOTUNE)
    return ds, steps, P_eff, K, len(classes)

# ----- Triplet(修正遮罩) -----
def make_batch_hard_triplet_loss(margin=MARGIN):
    @tf.function
    def loss(y_true, y_pred):
        labels = tf.cast(y_true, tf.int32)                  # [B]
        sim = tf.matmul(y_pred, y_pred, transpose_b=True)   # cosine
        dist = tf.maximum(2.0 - 2.0*sim, 0.0)
        B = tf.shape(labels)[0]
        l1 = tf.expand_dims(labels,0); l2 = tf.expand_dims(labels,1)
        mask_pos = tf.logical_and(tf.equal(l1,l2), tf.logical_not(tf.eye(B,dtype=tf.bool)))
        mask_neg = tf.logical_not(tf.equal(l1,l2))
        pos = tf.where(mask_pos, dist, tf.fill(tf.shape(dist), tf.constant(-1e9, tf.float32)))
        neg = tf.where(mask_neg, dist, tf.fill(tf.shape(dist), tf.constant( 1e9, tf.float32)))
        hardest_pos = tf.reduce_max(pos, axis=1)
        hardest_neg = tf.reduce_min(neg, axis=1)
        return tf.reduce_mean(tf.nn.relu(margin + hardest_pos - hardest_neg))
    return loss

# ========= 主程式 =========
if __name__ == "__main__":
    # 路徑健檢(相對 → 絕對)
    ABS_DATA = DATA_DIR if DATA_DIR.is_absolute() else (Path.cwd() / DATA_DIR).resolve()
    if not ABS_DATA.exists():
        raise FileNotFoundError(f"找不到資料集資料夾:{ABS_DATA}\n請建立 {ABS_DATA}\\<人名>\\*.jpg")
    print(f"Dataset 目錄:{ABS_DATA}")

    # Threads/GPU 設定
    n = os.cpu_count() or 4
    tf.config.threading.set_intra_op_parallelism_threads(n)
    tf.config.threading.set_inter_op_parallelism_threads(max(2,n//2))
    for g in tf.config.list_physical_devices('GPU'):
        try: tf.config.experimental.set_memory_growth(g, True)
        except: pass

    print("Loading dataset from:", ABS_DATA)
    val_ds = make_val_dataset(str(ABS_DATA))
    train_pk, STEPS, P_eff, K_each, NUM_CLASSES = make_pk_dataset(str(ABS_DATA))
    print(f"P×K:P_eff={P_eff}, K={K_each}, batch={BATCH}, steps/epoch={STEPS}, classes={NUM_CLASSES}")

    # 建模:兩個輸出 —— emb (L2) + cls(分類頭)
    base = tiny_mobilefacenet()
    emb, feat = base.output  # emb: L2, feat: 未正規化
    cls = layers.Dense(NUM_CLASSES, activation="softmax", name="cls")(feat)
    model = models.Model(base.input, outputs={"emb": emb, "cls": cls})

    # Callbacks
    ckpt_best = tf.keras.callbacks.ModelCheckpoint(filepath=str(BEST_KERAS), monitor="val_cls_loss",
                                                   mode="min", save_best_only=True)
    ckpt_last = tf.keras.callbacks.ModelCheckpoint(filepath=str(LAST_KERAS), save_best_only=False)
    tb  = tf.keras.callbacks.TensorBoard(log_dir=str(LOGS_DIR))
    rlr = tf.keras.callbacks.ReduceLROnPlateau(monitor="val_loss", factor=0.5, patience=5, min_lr=1e-5)
    es  = tf.keras.callbacks.EarlyStopping(monitor="val_loss", patience=12, restore_best_weights=True)

    # ===== Phase A:分類暖身(僅 CE)=====
    optA = tf.keras.optimizers.Adam(LR_A, clipnorm=1.0)
    model.compile(optimizer=optA,
                  loss={"emb": make_batch_hard_triplet_loss(margin=MARGIN), "cls": tf.keras.losses.SparseCategoricalCrossentropy()},
                  loss_weights={"emb": 0.0, "cls": 1.0},
                  metrics={"cls": "accuracy"})
    print("\n=== Phase A: CE warm-up ===")
    model.fit(train_pk, steps_per_epoch=STEPS, validation_data=val_ds, epochs=EPOCHS_A,
              callbacks=[ckpt_last, tb])

    # ===== Phase B:Triplet 微調(CE 做穩定器)=====
    optB = tf.keras.optimizers.Adam(LR_B, clipnorm=1.0)
    model.compile(optimizer=optB,
                  loss={"emb": make_batch_hard_triplet_loss(margin=MARGIN), "cls": tf.keras.losses.SparseCategoricalCrossentropy()},
                  loss_weights={"emb": 1.0, "cls": 0.1},
                  metrics={"cls": "accuracy"})
    print("\n=== Phase B: Triplet fine-tune ===")
    model.fit(train_pk, steps_per_epoch=STEPS, validation_data=val_ds, epochs=EPOCHS_B,
              callbacks=[ckpt_best, ckpt_last, tb, rlr, es])

    print("✅ Saved best/last to:")
    print(" -", BEST_KERAS.resolve())
    print(" -", LAST_KERAS.resolve())
    print("Logs:", LOGS_DIR.resolve())
  • 写回答

5条回答 默认 最新

  • 檀越@新空间 2025-08-21 16:42
    关注

    下午好🌅🌅🌅
    本答案参考通义千问

    你提到在训练 MobileFaceNet 模型时遇到了**特征崩溃(Feature Collapse)**的问题,导致无法正确识别人脸。并且你将模型转换为 INT8 量化版本 部署到 HT32F49395 上,但问题依旧存在。以下是详细的分析和解决方案:


    一、特征崩溃的可能原因

    1. 训练过程中损失函数设计不当

    • MobileFaceNet 使用的是 ArcFace 或 CosFace 等具有角度边界的损失函数。
    • 如果这些损失函数的参数设置不合理(如 margin、scale 等),可能导致特征向量过于集中,造成“特征崩溃”。

    2. 量化过程中的精度丢失

    • 将模型从 FP32 转换为 INT8 时,精度丢失可能导致特征向量变得模糊或重复。
    • 特别是在量化前没有进行 校准(calibration),或者校准数据不够多样,会导致量化后的特征空间不准确。

    3. 数据分布不平衡

    • 如果训练数据中某些类别的样本过少,模型无法学习到足够的区分性特征,导致特征向量相似度高。

    4. 模型结构或训练策略问题

    • 如网络深度不足、正则化方法(如 Dropout、L2 正则化)使用不当,也可能导致特征崩溃。

    二、解决特征崩溃的详细步骤

    1. 检查并优化损失函数配置

    重点:确保损失函数参数合理

    # 示例:ArcFace 损失函数配置
    import torch
    from torch.nn import functional as F
    
    def arcface_loss(logits, labels, s=30.0, m=0.5):
        cosine = F.normalize(logits, dim=1)
        one_hot = torch.zeros_like(cosine)
        one_hot.scatter_(1, labels.view(-1, 1).long(), 1)
        theta = torch.acos(torch.clamp(cosine * one_hot, -1.0 + 1e-7, 1.0 - 1e-7))
        target_logit = torch.cos(theta + m)
        logits = (one_hot * target_logit) + ((1.0 - one_hot) * cosine)
        logits *= s
        return logits
    

    建议:

    • s(scale) 建议设为 30~60;
    • m(margin) 建议设为 0.5~0.8;
    • 若使用 CosFace,可调整 m 为 0.3~0.5。

    2. 量化前进行校准(Calibration)

    重点:量化前必须用真实数据进行校准,否则特征会严重退化。

    步骤:

    1. 准备校准数据集:使用与训练集相似但未用于训练的数据。
    2. 运行量化流程
      # 使用 TensorFlow Lite 的量化工具
      tflite_convert \
        --input_file=model_float32.tflite \
        --output_file=model_int8_quantized.tflite \
        --input_shapes=1,112,112,3 \
        --default_ranges_min=-10 \
        --default_ranges_max=10 \
        --quantize=True \
        --calibrate_dataset=path_to_calibration_data
      

    注意: 校准数据应包含足够多的类别样本,以确保模型能学到不同的特征分布。


    3. 检查训练数据的分布情况

    重点:数据多样性不足会导致特征崩溃

    • 使用以下代码检查数据分布:
      from collections import Counter
      
      # 假设 labels 是训练数据的标签列表
      label_counts = Counter(labels)
      print(label_counts)
      

    建议:

    • 确保每个类别至少有 100 个样本;
    • 若类别不平衡,考虑使用 重采样(oversampling)加权损失函数

    4. 添加正则化项增强特征区分性

    重点:增加 L2 正则化或 Dropout

    # 在模型定义中加入 L2 正则化
    model = MobileFaceNet()
    for param in model.parameters():
        param.register_hook(lambda grad: grad * 0.1)  # 可选:L2 正则化
    
    # 或者在训练时添加 L2 正则化
    loss = criterion(outputs, labels) + 0.001 * sum(p.pow(2).sum() for p in model.parameters())
    

    5. 检查模型输出特征的分布

    重点:使用可视化工具观察特征分布

    import matplotlib.pyplot as plt
    import numpy as np
    
    # 假设 features 是模型的输出特征(N, D)
    features = np.random.randn(3, 64)  # 示例数据
    
    plt.scatter(features[:, 0], features[:, 1])
    plt.title("Feature Distribution")
    plt.show()
    

    如果特征点高度集中,说明模型未学到有效特征,需重新训练或调整损失函数。


    三、量化后模型的验证建议

    1. 验证量化模型的输出特征

    import tensorflow as tf
    
    interpreter = tf.lite.Interpreter(model_path="model_int8_quantized.tflite")
    interpreter.allocate_tensors()
    
    input_details = interpreter.get_input_details()
    output_details = interpreter.get_output_details()
    
    # 输入数据
    input_data = np.random.rand(1, 112, 112, 3).astype(input_details[0]["dtype"])
    
    interpreter.set_tensor(input_details[0]["index"], input_data)
    interpreter.invoke()
    
    output_data = interpreter.get_tensor(output_details[0]["index"])
    print("Quantized Output Shape:", output_data.shape)
    print("Output Min/Max:", output_data.min(), output_data.max())
    

    注意: 如果输出特征范围过小(如接近 0),可能是量化导致信息丢失。


    四、总结:关键改进点

    1. 优化损失函数参数,确保边界清晰;
    2. 进行量化前的校准,避免特征退化;
    3. 检查训练数据分布,保证类别平衡;
    4. 添加正则化机制,提升特征区分性;
    5. 验证量化后的模型输出,确保特征仍具备判别力。

    如果你愿意提供训练脚本或模型结构,我可以进一步帮助你定位问题。希望这些建议能帮助你解决 MobileFaceNet 的特征崩溃问题!

    评论

报告相同问题?

问题事件

  • 修改了问题 8月21日
  • 修改了问题 8月21日
  • 创建了问题 8月21日