我訓練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())