Skip to content

add support regnety#3424

Merged
zhangyubo0722 merged 2 commits into
PaddlePaddle:developfrom
learncat163:feature/add-regenety
Jul 14, 2026
Merged

add support regnety#3424
zhangyubo0722 merged 2 commits into
PaddlePaddle:developfrom
learncat163:feature/add-regenety

Conversation

@learncat163

@learncat163 learncat163 commented Jun 15, 2026

Copy link
Copy Markdown

RegNetY模型权重

paddle-aistudio在线演示demo

1. 模型描述

RegNet中的RegNetX已经在paddle中实现。而RegNetY 在 RegNetX 基础上引入 Squeeze-and-Excitation (SE) 注意力模块,但是PaddleClas还没有实现。本提交在原有的代码基础上,增加了RegNetY的支持。

2. 预训练权重

2.1 整体权重概览

权重按来源分为两类:

  • RegNetX: PaddleClas 官方的已有代码内置,代码在:ppcls/arch/backbone/model_zoo/regnet.py 这块不再处理,已经内置,可http自动下载
  • RegNetY: 本工程的主要目的,从 timm 权重转换过来到paddle项目

RegNetY(timm 权重转换)

PaddleClas 模型 原始timm 模型 paddle转换权重 文件大小
RegNetY_200 timm/regnety_002.pycls_in1k regnety_002.pdparams 13M
RegNetY_400 timm/regnety_004.pycls_in1k regnety_004.pdparams 17M
RegNetY_600 timm/regnety_006.pycls_in1k regnety_006.pdparams 24M
RegNetY_800 timm/regnety_008.pycls_in1k regnety_008.pdparams 25M
RegNetY_1600 timm/regnety_016.pycls_in1k regnety_016.pdparams 43M
RegNetY_3200 timm/regnety_032.pycls_in1k regnety_032.pdparams 75M
RegNetY_4000 timm/regnety_040.pycls_in1k regnety_040.pdparams 80M
RegNetY_6400 timm/regnety_064.pycls_in1k regnety_064.pdparams 118M
RegNetY_8000 timm/regnety_080.pycls_in1k regnety_080.pdparams 150M
RegNetY_12000 timm/regnety_120.pycls_in1k regnety_120.pdparams 199M
RegNetY_16000 timm/regnety_160.pycls_in1k regnety_160.pdparams 320M
RegNetY_32000 timm/regnety_320.pycls_in1k regnety_320.pdparams 554M

2.2 权重获取说明

RegNetX - 从 PaddleClas 官方 URL 直接下载(定义在 ppcls/arch/backbone/model_zoo/regnet.pyMODEL_URLS):

RegNetY - 先下载 timm 权重,再转换为 Paddle 格式。

转换过程中的关键处理:转换的过程,尽可能遵循了原来在处理RegNetX的原有风格,尽可能少的改动原有代码的基础上,实现了对RegNetY的支持。

3. 精度对齐

3.1 原版的RegNetX的精度对齐

因为本提交是在原版的RegNetX的基础上,迭代增强出的支持RegNetY,所以在新增了RegNetY之后,必须能和原版的RegNetX保持严格对齐。

经过分析,PaddleClas内置的RegNetX上的权重和Timm上类似名称的权重值不能一一对应,可能是来自其他的权重数据。

所以原版RegNetX的对齐要求,这里使用原始的git代码仓库中的老版本代码和本次提交的新代码,分别对同一个pdparams的权重进行加载推理,并对比输出的logit的差异。

3.1.1 输入随机向量的top1 acc对比

PaddleClas 名称 修改前Top-1输出 修改后Top-1输出
RegNetX_200MF 539 539
RegNetX_400MF 794 794
RegNetX_600MF 885 885
RegNetX_800MF 735 735
RegNetX_1600MF 488 488
RegNetX_3200MF 904 904
RegNetX_4GF 490 490
RegNetX_6400MF 904 904
RegNetX_8GF 904 904
RegNetX_12GF 904 904
RegNetX_16GF 488 488
RegNetX_32GF 904 904

所有模型的top1相同。

3.1.2 使用相同的输入进行推理,对比logit张量值

"""对比原始 regnet.py 和当前修改版 regnet.py 的推理输出。

流程:
  1. 用原始代码(git aae1e95 导出)构建模型 + 加载官方权重 -> 输出 logits 张量
  2. 用当前代码(ppcls/arch/backbone/model_zoo/regnet.py)构建模型 + 加载权重
  3. 逐元素对比两者 logits, 验证 max_diff == 0 且 np.array_equal == True
"""
import numpy as np
import paddle

# 在独立子进程中分别用原始代码和当前代码执行推理,老代码使用 git aae1e95 签出的源代码
# 保存 logits 张量到 npy 文件
np.save("regnetx_002_original.npy", original_output)  # 原始代码的 logits
np.save("regnetx_002_current.npy", current_output)    # 当前代码的 logits

# 逐元素对比
orig_tensor = np.load("regnetx_002_original.npy")
curr_tensor = np.load("regnetx_002_current.npy")
diff = np.abs(orig_tensor - curr_tensor)

max_diff = float(diff.max())
mean_diff = float(diff.mean())
exact_equal = np.array_equal(orig_tensor, curr_tensor)
top1_match = np.argmax(orig_tensor) == np.argmax(curr_tensor)

assert max_diff == 0.0, "张量存在差异,代码对齐失败"
assert exact_equal,    "张量不完全相等,代码对齐失败"
assert top1_match,     "Top-1 预测不一致,代码对齐失败"

已经经过验证,虽然支持升级了RegNetY,但是原来的RegNetX模型输出的张量,在新旧代码上的值完全一致。

3.2 前向推理精度对齐

通过加载转换后的 Paddle 权重,与 PyTorch timm 原版模型进行前向推理对比:

RegNetY

模型 输入尺寸 最大绝对误差 平均绝对误差
regnety_002 224x224 2.74e-06 6.99e-07
regnety_004 224x224 3.34e-06 7.30e-07
regnety_006 224x224 3.10e-06 6.71e-07
regnety_008 224x224 2.62e-06 5.51e-07
regnety_016 224x224 2.74e-06 6.28e-07
regnety_032 224x224 5.36e-06 9.48e-07
regnety_040 224x224 3.10e-06 7.39e-07
regnety_064 224x224 3.58e-06 7.50e-07
regnety_080 224x224 3.22e-06 7.08e-07
regnety_120 224x224 3.34e-06 7.33e-07
regnety_160 224x224 6.68e-06 9.70e-07
regnety_320 224x224 5.48e-06 9.70e-07

3.3 ImageNet 验证集精度验证

在 ImageNet 验证集(5万张)上验证模型的 Top-1 准确率:

RegNetY

模型 输入尺寸 Paddle Acc Timm Acc 误差
regnety_002 224x224 70.24% 70.24% 0.00%
regnety_004 224x224 73.72% 73.72% 0.00%
regnety_006 224x224 75.19% 75.19% 0.00%
regnety_008 224x224 76.23% 76.23% 0.00%
regnety_016 224x224 77.78% 77.78% 0.00%
regnety_032 224x224 78.86% 78.86% 0.00%
regnety_040 224x224 79.39% 79.39% 0.00%
regnety_064 224x224 79.63% 79.63% 0.00%
regnety_080 224x224 79.73% 79.73% 0.00%
regnety_120 224x224 80.34% 80.34% 0.00%
regnety_160 224x224 80.34% 80.34% 0.00%
regnety_320 224x224 80.84% 80.84% 0.00%

所有模型的 Top-1 准确率与 timm 版本完全一致,误差为 0.00%,满足精度要求。

@learncat163
learncat163 force-pushed the feature/add-regenety branch from cbc5dd5 to de2f4da Compare June 15, 2026 11:32
@learncat163

Copy link
Copy Markdown
Author

RegNetY 精度对齐验证

1. 随机输入 logit 对比:回归原版regenetx模型和新加入的regnety

单张随机输入 (seed=42, [1,3,224,224]),对比 timm 与 Paddle 最后一层输出。

RegNetX(12/12 通过,最大差异范围: 4.77e-06 ~ 2.19e-05)

模型 最大差异 平均差异 Top1 一致
regnetx_002 8.58e-06 1.14e-06
regnetx_004 4.77e-06 1.06e-06
regnetx_006 9.54e-06 1.46e-06
regnetx_008 7.63e-06 1.57e-06
regnetx_016 7.63e-06 1.07e-06
regnetx_032 8.11e-06 1.37e-06
regnetx_040 9.06e-06 1.29e-06
regnetx_064 9.54e-06 1.42e-06
regnetx_080 2.00e-05 2.14e-06
regnetx_120 9.06e-06 1.28e-06
regnetx_160 1.24e-05 1.52e-06
regnetx_320 2.19e-05 1.91e-06

RegNetY(12/12 通过,最大差异范围: 2.62e-06 ~ 6.68e-06)

模型 最大差异 平均差异 Top1 一致
regnety_002 2.74e-06 6.99e-07
regnety_004 3.34e-06 7.30e-07
regnety_006 3.10e-06 6.71e-07
regnety_008 2.62e-06 5.51e-07
regnety_016 2.74e-06 6.28e-07
regnety_032 5.36e-06 9.48e-07
regnety_040 3.10e-06 7.39e-07
regnety_064 3.58e-06 7.50e-07
regnety_080 3.22e-06 7.08e-07
regnety_120 3.34e-06 7.33e-07
regnety_160 6.68e-06 9.70e-07
regnety_320 5.48e-06 9.70e-07
验证代码
import os
import sys
import re
import argparse
import subprocess
import json
import numpy as np
from pathlib import Path
from datetime import datetime

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJ_ROOT = os.path.join(SCRIPT_DIR, "..", "..", "..", "..")
CONVERTED_DIR = os.path.join(SCRIPT_DIR, "paddle")
DOWNLOAD_DIR = os.path.join(SCRIPT_DIR, "timm")
REPORT_PATH = os.path.join(SCRIPT_DIR, "..", "docs", "diff_report.md")
LOG_PATH = os.path.join(SCRIPT_DIR, "..", "logs",
                        f"diff_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")

REGNETX_MODELS = {
    "regnetx_002": {
        "paddle_name": "RegNetX_200MF",
        "cfg": {"w_a": 36.44, "w_0": 24, "w_m": 2.49, "d": 13, "group_w": 8},
    },
    "regnetx_004": {
        "paddle_name": "RegNetX_400MF",
        "cfg": {"w_a": 24.48, "w_0": 24, "w_m": 2.54, "d": 22, "group_w": 16},
    },
    "regnetx_006": {
        "paddle_name": "RegNetX_600MF",
        "cfg": {"w_a": 36.97, "w_0": 48, "w_m": 2.24, "d": 16, "group_w": 24},
    },
    "regnetx_008": {
        "paddle_name": "RegNetX_800MF",
        "cfg": {"w_a": 35.73, "w_0": 56, "w_m": 2.28, "d": 16, "group_w": 16},
    },
    "regnetx_016": {
        "paddle_name": "RegNetX_1600MF",
        "cfg": {"w_a": 34.01, "w_0": 80, "w_m": 2.25, "d": 18, "group_w": 24},
    },
    "regnetx_032": {
        "paddle_name": "RegNetX_3200MF",
        "cfg": {"w_a": 26.31, "w_0": 88, "w_m": 2.25, "d": 25, "group_w": 48},
    },
    "regnetx_040": {
        "paddle_name": "RegNetX_4GF",
        "cfg": {"w_a": 38.65, "w_0": 96, "w_m": 2.43, "d": 23, "group_w": 40},
    },
    "regnetx_064": {
        "paddle_name": "RegNetX_6400MF",
        "cfg": {"w_a": 60.83, "w_0": 184, "w_m": 2.07, "d": 17, "group_w": 56},
    },
    "regnetx_080": {
        "paddle_name": "RegNetX_8GF",
        "cfg": {"w_a": 49.56, "w_0": 80, "w_m": 2.88, "d": 23, "group_w": 120},
    },
    "regnetx_120": {
        "paddle_name": "RegNetX_12GF",
        "cfg": {"w_a": 73.36, "w_0": 168, "w_m": 2.37, "d": 19, "group_w": 112},
    },
    "regnetx_160": {
        "paddle_name": "RegNetX_16GF",
        "cfg": {"w_a": 55.59, "w_0": 216, "w_m": 2.1, "d": 22, "group_w": 128},
    },
    "regnetx_320": {
        "paddle_name": "RegNetX_32GF",
        "cfg": {"w_a": 69.86, "w_0": 320, "w_m": 2.0, "d": 23, "group_w": 168},
    },
}

REGNETY_MODELS = {
    "regnety_002": {"w_a": 36.44, "w_0": 24, "w_m": 2.49, "d": 13, "group_w": 8},
    "regnety_004": {"w_a": 27.89, "w_0": 48, "w_m": 2.09, "d": 16, "group_w": 8},
    "regnety_006": {"w_a": 32.54, "w_0": 48, "w_m": 2.32, "d": 15, "group_w": 16},
    "regnety_008": {"w_a": 38.84, "w_0": 56, "w_m": 2.4, "d": 14, "group_w": 16},
    "regnety_016": {"w_a": 20.71, "w_0": 48, "w_m": 2.65, "d": 27, "group_w": 24},
    "regnety_032": {"w_a": 42.63, "w_0": 80, "w_m": 2.66, "d": 21, "group_w": 24},
    "regnety_040": {"w_a": 31.41, "w_0": 96, "w_m": 2.24, "d": 22, "group_w": 64},
    "regnety_064": {"w_a": 33.22, "w_0": 112, "w_m": 2.27, "d": 25, "group_w": 72},
    "regnety_080": {"w_a": 76.82, "w_0": 192, "w_m": 2.19, "d": 17, "group_w": 56},
    "regnety_120": {"w_a": 73.36, "w_0": 168, "w_m": 2.37, "d": 19, "group_w": 112},
    "regnety_160": {"w_a": 106.23, "w_0": 200, "w_m": 2.48, "d": 18, "group_w": 112},
    "regnety_320": {"w_a": 115.89, "w_0": 232, "w_m": 2.53, "d": 20, "group_w": 232},
}

MODEL_URLS = {
    "RegNetX_200MF": "https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/RegNetX_200MF_pretrained.pdparams",
    "RegNetX_400MF": "https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/RegNetX_400MF_pretrained.pdparams",
    "RegNetX_600MF": "https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/RegNetX_600MF_pretrained.pdparams",
    "RegNetX_800MF": "https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/RegNetX_800MF_pretrained.pdparams",
    "RegNetX_1600MF": "https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/RegNetX_1600MF_pretrained.pdparams",
    "RegNetX_3200MF": "https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/RegNetX_3200MF_pretrained.pdparams",
    "RegNetX_4GF": "https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/RegNetX_4GF_pretrained.pdparams",
    "RegNetX_6400MF": "https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/RegNetX_6400MF_pretrained.pdparams",
    "RegNetX_8GF": "https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/RegNetX_8GF_pretrained.pdparams",
    "RegNetX_12GF": "https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/RegNetX_12GF_pretrained.pdparams",
    "RegNetX_16GF": "https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/RegNetX_16GF_pretrained.pdparams",
    "RegNetX_32GF": "https://paddle-imagenet-models-name.bj.bcebos.com/dygraph/RegNetX_32GF_pretrained.pdparams",
}

def log(msg):
    print(msg)
    os.makedirs(os.path.dirname(LOG_PATH), exist_ok=True)
    with open(LOG_PATH, "a") as f:
        f.write(msg + "\n")

def find_timm_weight(model_name):
    """Find timm weight in HuggingFace cache."""
    hf_id = f"timm/{model_name}.pycls_in1k"
    cache_dir = os.path.expanduser("~/.cache/huggingface/hub")
    model_dir = os.path.join(cache_dir, f"models--{hf_id.replace('/', '--')}")
    if not os.path.exists(model_dir):
        return None
    snapshots_dir = os.path.join(model_dir, "snapshots")
    if not os.path.exists(snapshots_dir):
        return None
    for snap in os.listdir(snapshots_dir):
        cand = os.path.join(snapshots_dir, snap, "model.safetensors")
        if os.path.exists(cand):
            return cand
    return None

def download_timm_weight(model_name):
    """Download timm weight to HF cache."""
    log(f"  Downloading timm weight: {model_name}.pycls_in1k ...")
    os.environ.setdefault("HF_ENDPOINT", "https://hf-mirror.com")
    from huggingface_hub import hf_hub_download
    hf_hub_download(f"timm/{model_name}.pycls_in1k", "model.safetensors")
    log(f"  Downloaded.")
    return find_timm_weight(model_name)

def download_regnetx_pdparams(paddle_name):
    """Download RegNetX pdparams from PaddleClas MODEL_URLS."""
    url = MODEL_URLS.get(paddle_name)
    if not url:
        log(f"  [ERROR] No URL for {paddle_name}")
        return None
    fname = f"{paddle_name}_pretrained.pdparams"
    out_path = os.path.join(DOWNLOAD_DIR, fname)
    if os.path.exists(out_path):
        return out_path
    log(f"  Downloading {paddle_name} from MODEL_URLS ...")
    import urllib.request
    os.makedirs(DOWNLOAD_DIR, exist_ok=True)
    urllib.request.urlretrieve(url, out_path)
    log(f"  Downloaded ({os.path.getsize(out_path) // 1024 // 1024} MB)")
    return out_path

def convert_regnety_if_needed(model_name):
    """Convert RegNetY from timm to pdparams if not in converted/."""
    out_path = os.path.join(CONVERTED_DIR, f"{model_name}.pdparams")
    if os.path.exists(out_path):
        return out_path

    weight_path = find_timm_weight(model_name)
    if weight_path is None:
        weight_path = download_timm_weight(model_name)
    if weight_path is None:
        return None

    log(f"  Converting {model_name} timm -> paddle ...")
    from safetensors.torch import load_file
    import torch
    import paddle

    from importlib.util import spec_from_file_location, module_from_spec
    conv_script = os.path.join(SCRIPT_DIR, "..", "..", "weight", "scripts",
                               "20260608-convert-weight.py")
    spec = spec_from_file_location("conv", conv_script)
    conv_mod = module_from_spec(spec)
    spec.loader.exec_module(conv_mod)

    timm_sd = load_file(weight_path)
    paddle_sd = {}
    for timm_key, tensor in timm_sd.items():
        if "num_batches_tracked" in timm_key:
            continue
        paddle_key = conv_mod.convert_key(timm_key)
        if paddle_key is None:
            continue
        value = tensor.numpy()
        value = conv_mod.convert_weight_value(timm_key, value)
        paddle_sd[paddle_key] = paddle.to_tensor(value)

    os.makedirs(CONVERTED_DIR, exist_ok=True)
    paddle.save(paddle_sd, out_path)
    log(f"  Converted and saved to {out_path}")
    return out_path

def prepare_weights(to_test):
    """Ensure all required weights are available. Returns skip list."""
    skip = []
    log("\n--- Preparing weights ---")

    for name, info in to_test.items():
        is_regnetx = name.startswith("regnetx")
        log(f"\n  [{name}]")

        timm_path = find_timm_weight(name)
        if timm_path is None:
            log(f"  timm weight not in cache, downloading...")
            timm_path = download_timm_weight(name)
        if timm_path is None:
            log(f"  [SKIP] Cannot get timm weight for {name}")
            skip.append(name)
            continue
        else:
            log(f"  timm: OK")

        if is_regnetx:
            paddle_name = info["paddle_name"]
            pdparams_path = os.path.join(DOWNLOAD_DIR,
                                         f"{paddle_name}_pretrained.pdparams")
            if not os.path.exists(pdparams_path):
                pdparams_path = download_regnetx_pdparams(paddle_name)
        else:
            pdparams_path = convert_regnety_if_needed(name)

        if pdparams_path is None or not os.path.exists(pdparams_path):
            log(f"  [SKIP] Cannot get pdparams for {name}")
            skip.append(name)
        else:
            log(f"  pdparams: OK ({pdparams_path})")

    return skip

def test_model_subprocess(model_name, cfg, se_on, pdparams_path):
    """Run diff test in subprocess to avoid ParamAttr naming conflicts."""
    is_regnetx = model_name.startswith("regnetx")

    test_code = f'''
import sys, os, gc
sys.path.insert(0, {repr(PROJ_ROOT)})
os.environ["CUDA_VISIBLE_DEVICES"] = ""
import numpy as np
import paddle
paddle.set_device("cpu")
import torch
import timm

cfg = {cfg!r}
se_on = {se_on!r}
model_name = {model_name!r}
pdparams_path = {pdparams_path!r}

timm_model = timm.create_model(f"{{model_name}}.pycls_in1k", pretrained=True)
timm_model.eval()

np.random.seed(42)
img_np = np.random.randn(1, 3, 224, 224).astype(np.float32)
with torch.no_grad():
    timm_out = timm_model(torch.from_numpy(img_np)).cpu().numpy()
del timm_model
gc.collect()

from ppcls.arch.backbone.model_zoo.regnet import RegNet
paddle_model = RegNet(
    w_a=cfg["w_a"], w_0=cfg["w_0"], w_m=cfg["w_m"],
    d=cfg["d"], group_w=cfg["group_w"],
    bot_mul=1.0, q=8, se_on=se_on, class_num=1000)
param_dict = paddle.load(pdparams_path)
paddle_model.set_state_dict(param_dict)
paddle_model.eval()

with paddle.no_grad():
    paddle_out = paddle_model(paddle.to_tensor(img_np)).numpy()

del paddle_model
gc.collect()

diff = np.abs(timm_out - paddle_out)
top1_match = np.argmax(timm_out) == np.argmax(paddle_out)
import json
print("JSON_RESULT:" + json.dumps({{
    "max_diff": float(diff.max()),
    "mean_diff": float(diff.mean()),
    "top1_match": bool(top1_match)
}}))
'''
    result = subprocess.run(
        [sys.executable, "-c", test_code],
        capture_output=True, text=True, timeout=600,
        env={**os.environ, "CUDA_VISIBLE_DEVICES": ""})

    if result.returncode != 0:
        err_lines = result.stderr.strip().split('\n')
        return None, None, None, err_lines[-1] if err_lines else "unknown error"

    for line in result.stdout.strip().split('\n'):
        if line.startswith("JSON_RESULT:"):
            data = json.loads(line[len("JSON_RESULT:"):])
            return data["max_diff"], data["mean_diff"], data["top1_match"], None

    return None, None, None, "No JSON_RESULT in output"

def generate_report(results, report_path):
    """Generate markdown diff report."""
    now = datetime.now().strftime("%Y-%m-%d %H:%M")
    lines = [
"
        f"> Generated: {now}\n\n",
"
        "- Random input (seed=42): `[1, 3, 224, 224]` from N(0,1)\n",
        "- RegNetX: paddle weight from `download/` (PaddleClas official)\n",
        "- RegNetY: paddle weight from `converted/` (timm converted)\n",
        "- timm model loaded from HuggingFace cache\n",
        "- Target: max abs diff < 1e-4\n\n",
    ]

    for variant, prefix in [("RegNetX", "regnetx"), ("RegNetY", "regnety")]:
        variant_results = {k: v for k, v in results.items() if k.startswith(prefix)}
        if not variant_results:
            continue

lines.append(f"
        lines.append("| Model | Max Diff | Mean Diff | Top1 Match | Result |\n")
        lines.append("|-------|----------|-----------|------------|--------|\n")

        for name, r in variant_results.items():
            if r.get("error"):
                lines.append(f"| {name} | ERROR | ERROR | - | FAIL: {r['error'][:50]} |\n")
            else:
                status = "PASS" if r["max_diff"] < 1e-4 else "FAIL"
                top1 = "Yes" if r["top1_match"] else "No"
                lines.append(
                    f"| {name} | {r['max_diff']:.2e} | {r['mean_diff']:.2e} "
                    f"| {top1} | {status} |\n"
                )
        lines.append("\n")

    total = len(results)
    passed = sum(1 for r in results.values()
                 if not r.get("error") and r["max_diff"] < 1e-4)
    skipped = sum(1 for r in results.values() if r.get("skipped"))
lines.append(f"
                 f" (max diff < 1e-4), {skipped} skipped.\n")

    os.makedirs(os.path.dirname(report_path), exist_ok=True)
    with open(report_path, "w", encoding="utf-8") as f:
        f.writelines(lines)
    log(f"\nReport saved to: {report_path}")

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--model", nargs="*", default=None)
    parser.add_argument("--regnetx", action="store_true")
    parser.add_argument("--regnety", action="store_true")
    parser.add_argument("--all", action="store_true", default=True)
    args = parser.parse_args()

    to_test = {}
    for name, info in REGNETX_MODELS.items():
        to_test[name] = {"cfg": info["cfg"], "se_on": False,
                         "paddle_name": info["paddle_name"]}
    for name, cfg in REGNETY_MODELS.items():
        to_test[name] = {"cfg": cfg, "se_on": True}

    if args.model:
        to_test = {k: v for k, v in to_test.items() if k in args.model}
    elif args.regnetx and not args.regnety:
        to_test = {k: v for k, v in to_test.items() if k.startswith("regnetx")}
    elif args.regnety and not args.regnetx:
        to_test = {k: v for k, v in to_test.items() if k.startswith("regnety")}

    log("=" * 70)
    log("RegNet Diff Test: timm vs paddle (new weight loading)")
    log("=" * 70)
    log(f"Models to test: {len(to_test)}")
    log(f"  RegNetX: {sum(1 for k in to_test if k.startswith('regnetx'))}")
    log(f"  RegNetY: {sum(1 for k in to_test if k.startswith('regnety'))}")

    skip = prepare_weights(to_test)

    log("\n--- Running diff tests ---")
    header = f"{'Model':<16} {'Max Diff':<12} {'Mean Diff':<12} {'Top1':<6} {'Result'}"
    log(header)
    log("-" * len(header))

    results = {}
    for name, info in to_test.items():
        if name in skip:
            log(f"{name:<16} SKIPPED (missing weights)")
            results[name] = {"error": "skipped", "skipped": True}
            continue

        is_regnetx = name.startswith("regnetx")
        if is_regnetx:
            pdparams_path = os.path.join(
                DOWNLOAD_DIR, f"{info['paddle_name']}_pretrained.pdparams")
        else:
            pdparams_path = os.path.join(CONVERTED_DIR, f"{name}.pdparams")

        try:
            max_diff, mean_diff, top1_match, error = test_model_subprocess(
                name, info["cfg"], info["se_on"], pdparams_path)
            if error:
                log(f"{name:<16} ERROR: {error[:60]}")
                results[name] = {"error": error}
            else:
                status = "PASS" if max_diff < 1e-4 else "FAIL"
                top1_str = "Yes" if top1_match else "No"
                log(f"{name:<16} {max_diff:<12.2e} {mean_diff:<12.2e} "
                    f"{top1_str:<6} {status}")
                results[name] = {
                    "max_diff": float(max_diff),
                    "mean_diff": float(mean_diff),
                    "top1_match": bool(top1_match),
                }
        except Exception as e:
            log(f"{name:<16} EXCEPTION: {e}")
            results[name] = {"error": str(e)}

    generate_report(results, REPORT_PATH)

if __name__ == "__main__":
    main()

2. ImageNet 验证集 RegNetY logit 逐样本对比

在完整 ImageNet val (50000 张) 上,逐样本对比 HG cache (timm) 权重与转换后的 Paddle pdparams 的最后一层 logit 差异。

模型 timm Acc Paddle Acc Acc Diff Max Logit Diff Mean Logit Diff 预测一致率 Logit Acc
regnety_002 70.24% 70.24% 0.00% 3.53e-05 1.26e-06 50000/50000 PASS PASS
regnety_004 73.72% 73.72% 0.00% 3.43e-05 1.16e-06 50000/50000 PASS PASS
regnety_006 75.19% 75.19% 0.00% 4.01e-05 1.10e-06 50000/50000 PASS PASS
regnety_008 76.23% 76.23% 0.00% 4.91e-05 1.09e-06 50000/50000 PASS PASS
regnety_016 77.78% 77.78% 0.00% 3.05e-05 1.08e-06 49999/50000 PASS PASS
regnety_032 78.86% 78.86% 0.00% 4.20e-05 1.09e-06 50000/50000 PASS PASS
regnety_040 79.39% 79.39% 0.00% 7.89e-05 1.55e-06 50000/50000 PASS PASS
regnety_064 79.63% 79.63% 0.00% 4.39e-05 1.36e-06 50000/50000 PASS PASS
regnety_080 79.73% 79.73% 0.00% 8.20e-05 1.32e-06 50000/50000 PASS PASS
regnety_120 80.34% 80.34% 0.00% 6.10e-05 1.43e-06 50000/50000 PASS PASS
regnety_160 80.34% 80.34% 0.00% 5.72e-05 1.35e-06 50000/50000 PASS PASS
regnety_320 80.84% 80.84% 0.00% 6.68e-05 1.49e-06 50000/50000 PASS PASS

关键指标:

  • logit max abs diff 最大值 8.20e-05 (regnety_080),为阈值的 82%
  • Top-1 Acc 差异全部为 0.00%,预测一致率 >= 99.998%
  • 600000 张推理 (50000 x 12) 中无一超过 logit 阈值
验证代码
import os
import sys
import json
import argparse
import subprocess
import time
from pathlib import Path
from datetime import datetime

import numpy as np

SCRIPT_DIR = Path(__file__).resolve().parent
PROJ_ROOT = SCRIPT_DIR.parents[3]
HIS = SCRIPT_DIR.parents[2]
CONVERTED_DIR = HIS / "regnety" / "weight" / "converted"
IMAGENET_DIR = PROJ_ROOT / "dataset" / "imagenet"
DATA_DIR = SCRIPT_DIR.parent / "data"
LOG_DIR = SCRIPT_DIR.parent / "logs"
DOCS_DIR = SCRIPT_DIR.parent / "docs"

REGNETY_MODELS = {
    "regnety_002": {"w_a": 36.44, "w_0": 24, "w_m": 2.49, "d": 13, "group_w": 8},
    "regnety_004": {"w_a": 27.89, "w_0": 48, "w_m": 2.09, "d": 16, "group_w": 8},
    "regnety_006": {"w_a": 32.54, "w_0": 48, "w_m": 2.32, "d": 15, "group_w": 16},
    "regnety_008": {"w_a": 38.84, "w_0": 56, "w_m": 2.4, "d": 14, "group_w": 16},
    "regnety_016": {"w_a": 20.71, "w_0": 48, "w_m": 2.65, "d": 27, "group_w": 24},
    "regnety_032": {"w_a": 42.63, "w_0": 80, "w_m": 2.66, "d": 21, "group_w": 24},
    "regnety_040": {"w_a": 31.41, "w_0": 96, "w_m": 2.24, "d": 22, "group_w": 64},
    "regnety_064": {"w_a": 33.22, "w_0": 112, "w_m": 2.27, "d": 25, "group_w": 72},
    "regnety_080": {"w_a": 76.82, "w_0": 192, "w_m": 2.19, "d": 17, "group_w": 56},
    "regnety_120": {"w_a": 73.36, "w_0": 168, "w_m": 2.37, "d": 19, "group_w": 112},
    "regnety_160": {"w_a": 106.23, "w_0": 200, "w_m": 2.48, "d": 18, "group_w": 112},
    "regnety_320": {"w_a": 115.89, "w_0": 232, "w_m": 2.53, "d": 20, "group_w": 232},
}

LOGIT_THRESHOLD = 1e-4
ACC_THRESHOLD = 0.2

def log(msg):
    print(msg)
    LOG_DIR.mkdir(parents=True, exist_ok=True)
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    log_file = LOG_DIR / f"regnety_logit_diff_{ts}.log"
    with open(log_file, "a") as f:
        f.write(msg + "\n")

def build_image_list(num_samples=None):
    """Build sorted image list and ground truth from ImageNet val."""
    synset_dirs = sorted([d for d in IMAGENET_DIR.iterdir()
                          if d.is_dir() and d.name.startswith('n')])
    if not synset_dirs:
        raise FileNotFoundError(f"No synset dirs in {IMAGENET_DIR}")

    synset_to_id = {d.name: idx for idx, d in enumerate(synset_dirs)}
    image_paths = []
    gt_labels = []
    for d in synset_dirs:
        cid = synset_to_id[d.name]
        for img in sorted(d.glob('*.JPEG')):
            image_paths.append(str(img))
            gt_labels.append(cid)

    if num_samples and num_samples < len(image_paths):
        image_paths = image_paths[:num_samples]
        gt_labels = gt_labels[:num_samples]

    return image_paths, gt_labels

def run_diff_subprocess(model_name, cfg, pdparams_path, image_paths, gt_labels, batch_size):
    """Run timm + Paddle logit diff in an isolated subprocess.

    Steps inside subprocess:
      1. timm inference -> save all logits to temp file
      2. Paddle inference -> load timm logits, compare per-batch
      3. Output JSON stats
    """
    tmp_logits = DATA_DIR / "tmp" / f"_timm_logits_{model_name}.npy"
    tmp_logits.parent.mkdir(parents=True, exist_ok=True)

    paths_file = DATA_DIR / "tmp" / f"_paths_{model_name}.txt"
    gt_file = DATA_DIR / "tmp" / f"_gt_{model_name}.npy"
    with open(paths_file, 'w') as f:
        for p in image_paths:
            f.write(p + '\n')
    np.save(gt_file, np.array(gt_labels, dtype=np.int32))

    test_code = f'''
import sys, os, gc, json, time
sys.path.insert(0, {repr(str(PROJ_ROOT))})
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"

import numpy as np
import torch
import paddle
paddle.set_device("gpu")

model_name = {model_name!r}
cfg = {cfg!r}
pdparams_path = {repr(str(pdparams_path))}
paths_file = {repr(str(paths_file))}
gt_file = {repr(str(gt_file))}
logits_file = {repr(str(tmp_logits))}
batch_size = {batch_size}

IMAGENET_MEAN = [0.485, 0.456, 0.406]
IMAGENET_STD = [0.229, 0.224, 0.225]

with open(paths_file) as f:
    image_paths = [l.strip() for l in f if l.strip()]
gt = np.load(gt_file)
n = len(image_paths)

import timm
from torch.utils.data import Dataset as TD, DataLoader as TDL
from PIL import Image as PILImage
from torchvision import transforms as tv_t

timm_name = f"{{model_name}}.pycls_in1k"
timm_model = timm.create_model(timm_name, pretrained=True)
timm_model.eval()
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
timm_model = timm_model.to(device)

crop_pct = 0.9
scale_size = int(224 / crop_pct)
transform = tv_t.Compose([
    tv_t.Resize(scale_size, interpolation=tv_t.InterpolationMode.BICUBIC),
    tv_t.CenterCrop(224),
    tv_t.ToTensor(),
    tv_t.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
])

class _TDataset(TD):
    def __init__(self, paths):
        self.paths = paths
    def __len__(self):
        return len(self.paths)
    def __getitem__(self, idx):
        try:
            img = PILImage.open(self.paths[idx]).convert('RGB')
            return transform(img), idx
        except:
            return torch.zeros(3, 224, 224), idx

ds = _TDataset(image_paths)
loader = TDL(ds, batch_size=batch_size, shuffle=False, num_workers=4, drop_last=False)

timm_logits = np.zeros((n, 1000), dtype=np.float32)
timm_preds = np.zeros(n, dtype=np.int32)
t0 = time.time()
with torch.no_grad():
    for imgs, idxs in loader:
        imgs = imgs.to(device)
        logits = timm_model(imgs).cpu().numpy()
        idxs = idxs.numpy()
        timm_logits[idxs] = logits
        timm_preds[idxs] = logits.argmax(axis=1)
timm_time = time.time() - t0

timm_correct = int((timm_preds == gt).sum())
timm_acc = timm_correct / n * 100

np.save(logits_file, timm_logits)

del timm_model
gc.collect()
torch.cuda.empty_cache()

from ppcls.arch.backbone.model_zoo.regnet import RegNet
from paddle.io import Dataset as PD, DataLoader as PDL
from paddle.vision import transforms as paddle_t

paddle_model = RegNet(
    w_a=cfg["w_a"], w_0=cfg["w_0"], w_m=cfg["w_m"],
    d=cfg["d"], group_w=cfg["group_w"],
    bot_mul=1.0, q=8, se_on=True, class_num=1000)
param_dict = paddle.load(pdparams_path)
paddle_model.set_dict(param_dict)
paddle_model.eval()

ss = int(224 / 0.9)
p_transform = paddle_t.Compose([
    paddle_t.Resize(ss, interpolation='bicubic'),
    paddle_t.CenterCrop(224),
    paddle_t.ToTensor(),
    paddle_t.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
])

class _PDataset(PD):
    def __init__(self, paths):
        self.paths = paths
    def __len__(self):
        return len(self.paths)
    def __getitem__(self, idx):
        try:
            img = PILImage.open(self.paths[idx]).convert('RGB')
            return p_transform(img), idx
        except:
            return paddle.zeros([3, 224, 224], dtype='float32'), idx

p_ds = _PDataset(image_paths)
p_loader = PDL(p_ds, batch_size=batch_size, shuffle=False, num_workers=4, drop_last=False)

paddle_preds = np.zeros(n, dtype=np.int32)
global_max_diff = 0.0
global_mean_diff = 0.0
global_max_diff_sample = -1
sample_max_diffs = np.zeros(n, dtype=np.float32)
batch_count = 0

t0 = time.time()
for batch_imgs, batch_idxs in p_loader:
    with paddle.no_grad():
        p_logits = paddle_model(batch_imgs).numpy()

    idxs = batch_idxs.numpy()
    paddle_preds[idxs] = p_logits.argmax(axis=1)

    t_logits_batch = timm_logits[idxs]
    diff = np.abs(t_logits_batch - p_logits)
    per_sample_max = diff.max(axis=1)
    sample_max_diffs[idxs] = per_sample_max

    batch_max = float(diff.max())
    batch_mean = float(diff.mean())
    if batch_max > global_max_diff:
        global_max_diff = batch_max
        global_max_diff_sample = int(idxs[per_sample_max.argmax()])
    global_mean_diff += batch_mean
    batch_count += 1

paddle_time = time.time() - t0
global_mean_diff /= batch_count

paddle_correct = int((paddle_preds == gt).sum())
paddle_acc = paddle_correct / n * 100

agree = int((timm_preds == paddle_preds).sum())

over_threshold = int((sample_max_diffs > 1e-4).sum())

import os
if os.path.exists(logits_file):
    os.remove(logits_file)

result = {{
    "timm_acc": round(timm_acc, 4),
    "timm_correct": timm_correct,
    "paddle_acc": round(paddle_acc, 4),
    "paddle_correct": paddle_correct,
    "acc_diff": round(abs(timm_acc - paddle_acc), 4),
    "max_logit_diff": float(global_max_diff),
    "mean_logit_diff": float(global_mean_diff),
    "worst_sample_idx": global_max_diff_sample,
    "agreement": agree,
    "total": n,
    "over_threshold": over_threshold,
    "timm_time": round(timm_time, 2),
    "paddle_time": round(paddle_time, 2),
}}
print("JSON_RESULT:" + json.dumps(result))
'''
    env = {**os.environ, "CUDA_VISIBLE_DEVICES": "0"}

    result = subprocess.run(
        [sys.executable, "-c", test_code],
        capture_output=True, text=True, timeout=1800, env=env)

    for f in [paths_file, gt_file, tmp_logits]:
        if f.exists():
            f.unlink()

    if result.returncode != 0:
        err = result.stderr.strip().split('\n')[-1] if result.stderr.strip() else "unknown"
        return {"error": err}

    for line in result.stdout.strip().split('\n'):
        if line.startswith("JSON_RESULT:"):
            return json.loads(line[len("JSON_RESULT:"):])

    return {"error": "No JSON_RESULT in output"}

def generate_report(results, start_time, end_time):
    """Generate markdown comparison report."""
    DOCS_DIR.mkdir(parents=True, exist_ok=True)
    report_path = DOCS_DIR / "regnety_logit_diff_report.md"

    now = end_time.strftime("%Y-%m-%d %H:%M:%S")
    duration = (end_time - start_time).total_seconds()

    lines = [
"
        f"> Generated: {now}\n",
        f"> Duration: {duration:.1f}s\n\n",
"
        "- **Dataset**: ImageNet val (50000 images, synset sorted)\n",
        "- **HG Cache**: timm weights from HuggingFace cache\n",
        "- **Paddle Params**: converted pdparams from `weight/converted/`\n",
        "- **Thresholds**:\n",
        f"  - Last-layer logit max abs diff <= {LOGIT_THRESHOLD:.0e}\n",
        f"  - Top-1 Acc diff < {ACC_THRESHOLD}%\n",
        "- **Preprocessing**: Resize(248, BICUBIC) -> CenterCrop(224) -> Normalize(ImageNet)\n\n",
"
    ]

    all_pass = all(
        r.get("max_logit_diff", 1) <= LOGIT_THRESHOLD and r.get("acc_diff", 100) < ACC_THRESHOLD
        for r in results.values() if "error" not in r
    )
    pass_count = sum(
        1 for r in results.values()
        if "error" not in r and r["max_logit_diff"] <= LOGIT_THRESHOLD and r["acc_diff"] < ACC_THRESHOLD
    )
    fail_count = sum(1 for r in results.values() if "error" in r)
    error_count = fail_count

    lines.append(f"**{pass_count}/{len(results)} models passed** all thresholds.")
    if error_count:
        lines.append(f" {error_count} models had errors.")
    lines.append("\n\n")

lines.append("
    lines.append("| Model | timm Acc | Paddle Acc | Acc Diff | Max Logit Diff | Mean Logit Diff | Agree | Logit | Acc |\n")
    lines.append("|-------|----------|-----------|---------|---------------|----------------|-------|-------|-----|\n")

    for name, r in results.items():
        if "error" in r:
            lines.append(f"| {name} | ERROR | ERROR | - | - | - | - | FAIL | FAIL |\n")
            continue
        acc_sign = "+" if r["paddle_acc"] >= r["timm_acc"] else "-"
        logit_ok = "PASS" if r["max_logit_diff"] <= LOGIT_THRESHOLD else "FAIL"
        acc_ok = "PASS" if r["acc_diff"] < ACC_THRESHOLD else "FAIL"
        lines.append(
            f"| {name} | {r['timm_acc']:.2f}% | {r['paddle_acc']:.2f}% "
            f"| {acc_sign}{r['acc_diff']:.2f}% "
            f"| {r['max_logit_diff']:.2e} | {r['mean_logit_diff']:.2e} "
            f"| {r['agreement']}/{r['total']} "
            f"| {logit_ok} | {acc_ok} |\n"
        )
    lines.append("\n")

lines.append("
    for name, r in results.items():
lines.append(f"
        if "error" in r:
            lines.append(f"**Error**: {r['error']}\n\n")
            continue

        lines.append("| Metric | Value |\n")
        lines.append("|--------|-------|\n")
        lines.append(f"| timm Top-1 Acc | {r['timm_acc']:.2f}% ({r['timm_correct']}/{r['total']}) |\n")
        lines.append(f"| Paddle Top-1 Acc | {r['paddle_acc']:.2f}% ({r['paddle_correct']}/{r['total']}) |\n")
        lines.append(f"| Acc Diff | {r['acc_diff']:.4f}% |\n")
        lines.append(f"| Max Logit Diff | {r['max_logit_diff']:.6e} |\n")
        lines.append(f"| Mean Logit Diff | {r['mean_logit_diff']:.6e} |\n")
        lines.append(f"| Worst Sample Idx | {r['worst_sample_idx']} |\n")
        lines.append(f"| Agreement | {r['agreement']}/{r['total']} ({r['agreement']/r['total']*100:.2f}%) |\n")
        lines.append(f"| Samples > 1e-4 | {r['over_threshold']}/{r['total']} |\n")
        lines.append(f"| timm Time | {r['timm_time']:.1f}s ({r['total']/r['timm_time']:.0f} img/s) |\n")
        lines.append(f"| Paddle Time | {r['paddle_time']:.1f}s ({r['total']/r['paddle_time']:.0f} img/s) |\n\n")

        logit_ok = "PASS" if r["max_logit_diff"] <= LOGIT_THRESHOLD else "FAIL"
        acc_ok = "PASS" if r["acc_diff"] < ACC_THRESHOLD else "FAIL"
        lines.append(f"**Verdict**: Logit={logit_ok}, Acc={acc_ok}\n\n")

    lines.append("---\n\n")
    lines.append(f"*Report generated: {now}*\n")

    with open(report_path, "w", encoding="utf-8") as f:
        f.writelines(lines)
    log(f"Report saved: {report_path}")
    return report_path

def main():
    parser = argparse.ArgumentParser(description="RegNetY HG vs Paddle Logit Diff")
    parser.add_argument("--model", nargs="*", default=None,
                        help="Specific model(s) to test")
    parser.add_argument("--all", action="store_true",
                        help="Test all RegNetY models")
    parser.add_argument("--batch-size", type=int, default=32)
    parser.add_argument("--num-samples", type=int, default=0,
                        help="Limit samples (0=all)")
    args = parser.parse_args()

    if args.model:
        models = {k: v for k, v in REGNETY_MODELS.items() if k in args.model}
    else:
        models = REGNETY_MODELS

    if not models:
        log("No models selected")
        return 1

    log("=" * 70)
    log("RegNetY HG Cache vs Paddle Params - Logit Diff Validation")
    log("=" * 70)
    log(f"Models: {len(models)} ({', '.join(models.keys())})")

    num_samples = args.num_samples if args.num_samples > 0 else None
    image_paths, gt_labels = build_image_list(num_samples)
    log(f"Images: {len(image_paths)}")

    start_time = datetime.now()
    results = {}

    for i, (name, cfg) in enumerate(models.items(), 1):
        pdparams = CONVERTED_DIR / f"{name}.pdparams"
        if not pdparams.exists():
            log(f"\n[{i}/{len(models)}] {name}: SKIP (no pdparams)")
            results[name] = {"error": f"pdparams not found: {pdparams}"}
            continue

        log(f"\n[{i}/{len(models)}] {name}")
        log(f"  pdparams: {pdparams} ({pdparams.stat().st_size // 1024 // 1024} MB)")

        t0 = time.time()
        r = run_diff_subprocess(name, cfg, str(pdparams), image_paths, gt_labels, args.batch_size)
        elapsed = time.time() - t0

        if "error" in r:
            log(f"  ERROR: {r['error']}")
        else:
            logit_ok = "PASS" if r["max_logit_diff"] <= LOGIT_THRESHOLD else "FAIL"
            acc_ok = "PASS" if r["acc_diff"] < ACC_THRESHOLD else "FAIL"
            log(f"  timm Acc: {r['timm_acc']:.2f}%  Paddle Acc: {r['paddle_acc']:.2f}%  "
                f"Diff: {r['acc_diff']:.4f}% [{acc_ok}]")
            log(f"  Max Logit Diff: {r['max_logit_diff']:.2e}  "
                f"Mean: {r['mean_logit_diff']:.2e}  [{logit_ok}]")
            log(f"  Agreement: {r['agreement']}/{r['total']}  "
                f"Over threshold: {r['over_threshold']}")
            log(f"  Time: {elapsed:.1f}s (timm {r['timm_time']:.1f}s + paddle {r['paddle_time']:.1f}s)")

        results[name] = r

    end_time = datetime.now()
    report_path = generate_report(results, start_time, end_time)

    log("\n" + "=" * 70)
    log("Final Summary")
    log("=" * 70)
    header = f"{'Model':<16} {'timm Acc':>10} {'Paddle Acc':>10} {'Acc Diff':>10} {'Max Diff':>12} {'Logit':>6} {'Acc':>6}"
    log(header)
    log("-" * len(header))
    for name, r in results.items():
        if "error" in r:
            log(f"{name:<16} {'ERROR':>10} {'ERROR':>10} {'-':>10} {'-':>12} {'FAIL':>6} {'FAIL':>6}")
            continue
        logit_ok = "PASS" if r["max_logit_diff"] <= LOGIT_THRESHOLD else "FAIL"
        acc_ok = "PASS" if r["acc_diff"] < ACC_THRESHOLD else "FAIL"
        log(f"{name:<16} {r['timm_acc']:>9.2f}% {r['paddle_acc']:>9.2f}% "
            f"{r['acc_diff']:>9.4f}% {r['max_logit_diff']:>12.2e} {logit_ok:>6} {acc_ok:>6}")

    log(f"\nReport: {report_path}")
    return 0

if __name__ == "__main__":
    sys.exit(main())

@learncat163

Copy link
Copy Markdown
Author

3. RegNetX 代码回归测试

新增 RegNetY 入口函数后,原有 RegNetX 12 个模型加载相同官方权重,logit 输出完全相同 (diff=0)。

验证代码
import os
import sys
import subprocess
import json
import numpy as np
from datetime import datetime

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJ_ROOT = os.path.join(SCRIPT_DIR, "..", "..", "..", "..")
DOWNLOAD_DIR = os.path.join(SCRIPT_DIR, "..", "..", "weight", "download")
ORIGINAL_CODE = os.path.join(SCRIPT_DIR, "..", "..", "0-code", "raw-code",
                             "regnet_original.py")
CURRENT_CODE = os.path.join(PROJ_ROOT, "ppcls", "arch", "backbone", "model_zoo",
                            "regnet.py")
DOCS_DIR = os.path.join(SCRIPT_DIR, "..", "docs")
LOG_PATH = os.path.join(SCRIPT_DIR, "..", "logs",
                        f"code_diff_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")

REGNETX_MODELS = {
    "regnetx_002": {"paddle_name": "RegNetX_200MF",
                    "cfg": {"w_a": 36.44, "w_0": 24, "w_m": 2.49, "d": 13, "group_w": 8}},
    "regnetx_004": {"paddle_name": "RegNetX_400MF",
                    "cfg": {"w_a": 24.48, "w_0": 24, "w_m": 2.54, "d": 22, "group_w": 16}},
    "regnetx_006": {"paddle_name": "RegNetX_600MF",
                    "cfg": {"w_a": 36.97, "w_0": 48, "w_m": 2.24, "d": 16, "group_w": 24}},
    "regnetx_008": {"paddle_name": "RegNetX_800MF",
                    "cfg": {"w_a": 35.73, "w_0": 56, "w_m": 2.28, "d": 16, "group_w": 16}},
    "regnetx_016": {"paddle_name": "RegNetX_1600MF",
                    "cfg": {"w_a": 34.01, "w_0": 80, "w_m": 2.25, "d": 18, "group_w": 24}},
    "regnetx_032": {"paddle_name": "RegNetX_3200MF",
                    "cfg": {"w_a": 26.31, "w_0": 88, "w_m": 2.25, "d": 25, "group_w": 48}},
    "regnetx_040": {"paddle_name": "RegNetX_4GF",
                    "cfg": {"w_a": 38.65, "w_0": 96, "w_m": 2.43, "d": 23, "group_w": 40}},
    "regnetx_064": {"paddle_name": "RegNetX_6400MF",
                    "cfg": {"w_a": 60.83, "w_0": 184, "w_m": 2.07, "d": 17, "group_w": 56}},
    "regnetx_080": {"paddle_name": "RegNetX_8GF",
                    "cfg": {"w_a": 49.56, "w_0": 80, "w_m": 2.88, "d": 23, "group_w": 120}},
    "regnetx_120": {"paddle_name": "RegNetX_12GF",
                    "cfg": {"w_a": 73.36, "w_0": 168, "w_m": 2.37, "d": 19, "group_w": 112}},
    "regnetx_160": {"paddle_name": "RegNetX_16GF",
                    "cfg": {"w_a": 55.59, "w_0": 216, "w_m": 2.1, "d": 22, "group_w": 128}},
    "regnetx_320": {"paddle_name": "RegNetX_32GF",
                    "cfg": {"w_a": 69.86, "w_0": 320, "w_m": 2.0, "d": 23, "group_w": 168}},
}

# 使用不同代码版本推理的子进程代码模板
INFERENCE_CODE = '''
import sys, os, gc, json
os.environ["CUDA_VISIBLE_DEVICES"] = ""
import numpy as np
import paddle
paddle.set_device("cpu")

# 读取指定版本的 regnet.py 源码,替换掉跨包导入后 exec
code_path = {code_path!r}
with open(code_path) as f:
    source = f.read()

# 替换跨包导入为空实现,避免 ImportError
source = source.replace(
    "from ....utils.save_load import load_dygraph_pretrain",
    "def load_dygraph_pretrain(*a, **k): pass"
)

# 创建一个临时命名空间执行源码
_mod_ns = {{
    "__name__": "regnet_version",
    "__file__": code_path,
}}
exec(source, _mod_ns)
RegNet = _mod_ns["RegNet"]

cfg = {cfg!r}
pdparams_path = {pdparams_path!r}

np.random.seed(42)
img_np = np.random.randn(1, 3, 224, 224).astype("float32")

model = RegNet(
    w_a=cfg["w_a"], w_0=cfg["w_0"], w_m=cfg["w_m"],
    d=cfg["d"], group_w=cfg["group_w"],
    bot_mul=1.0, q=8, se_on=False, class_num=1000)
param_dict = paddle.load(pdparams_path)
res = model.set_state_dict(param_dict)
model.eval()

with paddle.no_grad():
    out = model(paddle.to_tensor(img_np)).numpy()[0]

# 保存完整张量到 npy 文件用于后续逐元素对比
out_path = {out_path!r}
os.makedirs(os.path.dirname(out_path), exist_ok=True)
np.save(out_path, out)

del model
gc.collect()

print("JSON_RESULT:" + json.dumps({{
        "max": float(out.max()),
        "min": float(out.min()),
        "mean": float(out.mean()),
        "top1": int(np.argmax(out)),
        "top5": [int(x) for x in np.argsort(out)[-5:][::-1]],
        "missing_keys": len(res[0]) if res else 0,
        "unexpected_keys": len(res[1]) if res else 0,
    }}))
'''

def log(msg):
    print(msg, flush=True)
    os.makedirs(os.path.dirname(LOG_PATH), exist_ok=True)
    with open(LOG_PATH, "a") as f:
        f.write(msg + "\n")

def run_one(model_name, cfg, pdparams_path, code_path, version_label, out_path):
    """Run inference in subprocess with a specific code version."""
    code = INFERENCE_CODE.format(
        proj_root=PROJ_ROOT, code_path=code_path,
        cfg=cfg, pdparams_path=pdparams_path, out_path=out_path)

    result = subprocess.run(
        [sys.executable, "-c", code],
        capture_output=True, text=True, timeout=300,
        env={**os.environ, "CUDA_VISIBLE_DEVICES": ""})

    if result.returncode != 0:
        err = result.stderr.strip().split('\n')[-1] if result.stderr else "unknown"
        return {"error": err[:200]}

    for line in result.stdout.strip().split('\n'):
        if line.startswith("JSON_RESULT:"):
            return json.loads(line[len("JSON_RESULT:"):])

    return {"error": "No JSON_RESULT"}

def generate_report(results, report_path):
    """Generate markdown report."""
    now = datetime.now().strftime("%Y-%m-%d %H:%M")
    lines = [
        "# RegNetX 代码对比报告:原始代码 vs 当前代码\n\n",
        f"> 生成时间: {now}\n\n",
        "## 说明\n\n",
        "从 git `aae1e95` 导出原始 regnet.py,与当前修改后的 regnet.py 对比。\n",
        "两者加载相同的 PaddleClas 官方权重,使用相同的随机输入 (seed=42),记录推理输出。\n\n",
        "## 结果\n\n",
        "| 模型 | 原始 Top-1 | 当前 Top-1 | Top-1 一致 | 最大张量误差 | 平均张量误差 | 完全相同 |\n",
        "|------|-----------|-----------|-----------|------------|------------|----------|\n",
    ]

    for name, r in results.items():
        if r.get("error"):
            lines.append(f"| {name} | ERROR | - | - | - | - | - | - | - |\n")
            continue
        orig = r["original"]
        curr = r["current"]

        if orig.get("error") or curr.get("error"):
            o_err = orig.get("error", "OK")[:30]
            c_err = curr.get("error", "OK")[:30]
            lines.append(f"| {name} | {o_err} | {c_err} | - | - | - | - |\n")
            continue

        top1_match = "Yes" if orig["top1"] == curr["top1"] else "No"
        td = r.get("tensor_diff")
        if td:
            eq = "Yes" if td["exact_equal"] else "No"
            lines.append(
                f"| {name} | {orig['top1']} | {curr['top1']} "
                f"| {top1_match} | {td['max_diff']:.2e} | {td['mean_diff']:.2e} | {eq} |\n"
            )
        else:
            lines.append(
                f"| {name} | {orig['top1']} | {curr['top1']} "
                f"| {top1_match} | - | - | - |\n"
            )

    lines.append("\n## 结论\n\n")
    all_match = all(
        r.get("original", {}).get("top1") == r.get("current", {}).get("top1")
        for r in results.values() if not r.get("error")
    )
    if all_match:
        lines.append("所有模型的推理结果完全一致,**代码修改未破坏 RegNetX 的推理行为**。\n")
    else:
        mismatches = [n for n, r in results.items()
                      if not r.get("error") and r.get("original", {}).get("top1") != r.get("current", {}).get("top1")]
        lines.append(f"有 {len(mismatches)} 个模型的 Top-1 预测不一致:{', '.join(mismatches)}\n")

    os.makedirs(os.path.dirname(report_path), exist_ok=True)
    with open(report_path, "w", encoding="utf-8") as f:
        f.writelines(lines)
    log(f"\nReport saved to: {report_path}")

def main():
    log("=" * 70)
    log("RegNetX Code Diff: Original vs Current regnet.py")
    log("=" * 70)
    log(f"Original: {ORIGINAL_CODE}")
    log(f"Current:  {CURRENT_CODE}")
    log(f"Models: {len(REGNETX_MODELS)}")

    # 临时目录保存张量
    tensor_dir = os.path.join(SCRIPT_DIR, "..", "data", "tensors")
    os.makedirs(tensor_dir, exist_ok=True)

    results = {}
    for name, info in REGNETX_MODELS.items():
        cfg = info["cfg"]
        paddle_name = info["paddle_name"]
        pdparams_path = os.path.join(DOWNLOAD_DIR, f"{paddle_name}_pretrained.pdparams")

        if not os.path.exists(pdparams_path):
            log(f"\n{name}: SKIP (pdparams not found)")
            results[name] = {"error": "pdparams not found"}
            continue

        orig_npy = os.path.join(tensor_dir, f"{name}_original.npy")
        curr_npy = os.path.join(tensor_dir, f"{name}_current.npy")

        log(f"\n{'='*50}")
        log(f"Model: {name} ({paddle_name})")

        # 串行执行:先原始,再当前
        log(f"  [1/2] Original code...")
        orig = run_one(name, cfg, pdparams_path, ORIGINAL_CODE, "original", orig_npy)

        log(f"  [2/2] Current code...")
        curr = run_one(name, cfg, pdparams_path, CURRENT_CODE, "current", curr_npy)

        if orig.get("error"):
            log(f"  Original ERROR: {orig['error'][:80]}")
        else:
            log(f"  Original: top1={orig['top1']} top5={orig['top5']} "
                f"range=[{orig['min']:.3f},{orig['max']:.3f}]")

        if curr.get("error"):
            log(f"  Current  ERROR: {curr['error'][:80]}")
        else:
            log(f"  Current:  top1={curr['top1']} top5={curr['top5']} "
                f"range=[{curr['min']:.3f},{curr['max']:.3f}]")

        # 逐元素张量对比
        tensor_diff = None
        if not orig.get("error") and not curr.get("error"):
            if os.path.exists(orig_npy) and os.path.exists(curr_npy):
                orig_tensor = np.load(orig_npy)
                curr_tensor = np.load(curr_npy)
                diff = np.abs(orig_tensor - curr_tensor)
                tensor_diff = {
                    "max_diff": float(diff.max()),
                    "mean_diff": float(diff.mean()),
                    "exact_equal": bool(np.array_equal(orig_tensor, curr_tensor)),
                }
                eq_str = "IDENTICAL" if tensor_diff["exact_equal"] else f"max_diff={diff.max():.2e}"
                match = "MATCH" if orig["top1"] == curr["top1"] else "DIFF"
                log(f"  Top-1: {match} | Tensor: {eq_str}")
            else:
                match = "MATCH" if orig["top1"] == curr["top1"] else "DIFF"
                log(f"  Top-1: {match} | Tensor: npy files not found")

        results[name] = {"original": orig, "current": curr, "tensor_diff": tensor_diff}

    report_path = os.path.join(DOCS_DIR, "code_diff_report.md")
    generate_report(results, report_path)

if __name__ == "__main__":
    main()

4. ImageNet 验证集 Top-1 Acc 全量对比

12 RegNetX + 12 RegNetY 共 24 个模型,Paddle 与 timm 的 Top-1 Acc 完全一致,Agreement >= 99.998%。

模型 Paddle Top-1 timm Top-1 Diff Pass
regnetx_002 68.72% 68.72% +0.00% PASS
regnetx_004 72.48% 72.48% +0.00% PASS
regnetx_006 73.97% 73.97% +0.00% PASS
regnetx_008 74.96% 74.96% +0.00% PASS
regnetx_016 76.86% 76.86% +0.00% PASS
regnetx_032 77.96% 77.96% +0.00% PASS
regnetx_040 78.67% 78.67% +0.00% PASS
regnetx_064 79.09% 79.09% +0.00% PASS
regnetx_080 79.14% 79.14% +0.00% PASS
regnetx_120 79.64% 79.64% +0.00% PASS
regnetx_160 79.91% 79.91% +0.00% PASS
regnetx_320 80.28% 80.28% +0.00% PASS
regnety_002 70.24% 70.24% +0.00% PASS
regnety_004 73.72% 73.72% +0.00% PASS
regnety_006 75.19% 75.19% +0.00% PASS
regnety_008 76.23% 76.23% +0.00% PASS
regnety_016 77.78% 77.78% +0.00% PASS
regnety_032 78.86% 78.86% +0.00% PASS
regnety_040 79.39% 79.39% +0.00% PASS
regnety_064 79.63% 79.63% +0.00% PASS
regnety_080 79.73% 79.73% +0.00% PASS
regnety_120 80.34% 80.34% +0.00% PASS
regnety_160 80.34% 80.34% +0.00% PASS
regnety_320 80.84% 80.84% +0.00% PASS
验证代码
import os
import sys
import logging
import time
import argparse
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional, Tuple

import numpy as np
import cv2
try:
    from tqdm import tqdm
except ImportError:
    tqdm = None

os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
os.environ['CUDA_VISIBLE_DEVICES'] = '0'

PROJECT_ROOT = Path(__file__).resolve().parents[4]
sys.path.insert(0, str(PROJECT_ROOT))

IMAGENET_DEFAULT_MEAN = (0.485, 0.456, 0.406)
IMAGENET_DEFAULT_STD = (0.229, 0.224, 0.225)

REGNETX_CONFIGS = {
    "regnetx_002": {"w_a": 36.44, "w_0": 24, "w_m": 2.49, "d": 13, "group_w": 8},
    "regnetx_004": {"w_a": 24.48, "w_0": 24, "w_m": 2.54, "d": 22, "group_w": 16},
    "regnetx_006": {"w_a": 36.97, "w_0": 48, "w_m": 2.24, "d": 16, "group_w": 24},
    "regnetx_008": {"w_a": 35.73, "w_0": 56, "w_m": 2.28, "d": 16, "group_w": 16},
    "regnetx_016": {"w_a": 34.01, "w_0": 80, "w_m": 2.25, "d": 18, "group_w": 24},
    "regnetx_032": {"w_a": 26.31, "w_0": 88, "w_m": 2.25, "d": 25, "group_w": 48},
    "regnetx_040": {"w_a": 38.65, "w_0": 96, "w_m": 2.43, "d": 23, "group_w": 40},
    "regnetx_064": {"w_a": 60.83, "w_0": 184, "w_m": 2.07, "d": 17, "group_w": 56},
    "regnetx_080": {"w_a": 49.56, "w_0": 80, "w_m": 2.88, "d": 23, "group_w": 120},
    "regnetx_120": {"w_a": 73.36, "w_0": 168, "w_m": 2.37, "d": 19, "group_w": 112},
    "regnetx_160": {"w_a": 55.59, "w_0": 216, "w_m": 2.1, "d": 22, "group_w": 128},
    "regnetx_320": {"w_a": 69.86, "w_0": 320, "w_m": 2.0, "d": 23, "group_w": 168},
}

REGNETY_CONFIGS = {
    "regnety_002": {"w_a": 36.44, "w_0": 24, "w_m": 2.49, "d": 13, "group_w": 8},
    "regnety_004": {"w_a": 27.89, "w_0": 48, "w_m": 2.09, "d": 16, "group_w": 8},
    "regnety_006": {"w_a": 32.54, "w_0": 48, "w_m": 2.32, "d": 15, "group_w": 16},
    "regnety_008": {"w_a": 38.84, "w_0": 56, "w_m": 2.4, "d": 14, "group_w": 16},
    "regnety_016": {"w_a": 20.71, "w_0": 48, "w_m": 2.65, "d": 27, "group_w": 24},
    "regnety_032": {"w_a": 42.63, "w_0": 80, "w_m": 2.66, "d": 21, "group_w": 24},
    "regnety_040": {"w_a": 31.41, "w_0": 96, "w_m": 2.24, "d": 22, "group_w": 64},
    "regnety_064": {"w_a": 33.22, "w_0": 112, "w_m": 2.27, "d": 25, "group_w": 72},
    "regnety_080": {"w_a": 76.82, "w_0": 192, "w_m": 2.19, "d": 17, "group_w": 56},
    "regnety_120": {"w_a": 73.36, "w_0": 168, "w_m": 2.37, "d": 19, "group_w": 112},
    "regnety_160": {"w_a": 106.23, "w_0": 200, "w_m": 2.48, "d": 18, "group_w": 112},
    "regnety_320": {"w_a": 115.89, "w_0": 232, "w_m": 2.53, "d": 20, "group_w": 232},
}

ALL_MODEL_NAMES = list(REGNETX_CONFIGS.keys()) + list(REGNETY_CONFIGS.keys())

def setup_logging(log_file: str) -> logging.Logger:
    log_dir = Path(log_file).parent
    log_dir.mkdir(parents=True, exist_ok=True)

    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(levelname)s - %(message)s',
        handlers=[
            logging.FileHandler(log_file, encoding='utf-8'),
            logging.StreamHandler()
        ]
    )
    return logging.getLogger(__name__)

logger = None

def get_model_cfg(model_name: str) -> Tuple[dict, bool]:
    """Return (config_dict, se_on) for a given model name."""
    if model_name in REGNETX_CONFIGS:
        return REGNETX_CONFIGS[model_name], False
    if model_name in REGNETY_CONFIGS:
        return REGNETY_CONFIGS[model_name], True
    raise ValueError(f"Unknown model: {model_name}")

def paddle_inference_subprocess(
    model_name: str,
    weight_path: str,
    image_paths: List[Path],
    ground_truth: List[List[int]],
    batch_size: int = 32,
    predictions_dir: Optional[Path] = None
) -> dict:
    """Run Paddle inference in a subprocess to avoid ParamAttr name conflicts.

    PaddlePaddle does not allow two models with the same ParamAttr(name=...)
    in one process. Each model runs in an isolated subprocess.
    """
    import subprocess

    cfg, se_on = get_model_cfg(model_name)
    num_images = len(image_paths)
    gt_flat = [gt[0] for gt in ground_truth]

    tmp_dir = Path(__file__).parent.parent / 'data' / 'tmp'
    tmp_dir.mkdir(parents=True, exist_ok=True)
    paths_file = tmp_dir / f"_topacc_{model_name}_paths.txt"
    gt_file = tmp_dir / f"_topacc_{model_name}_gt.npy"
    pred_out = tmp_dir / f"_topacc_{model_name}_pred.npy"

    with open(paths_file, 'w') as f:
        for p in image_paths:
            f.write(str(p) + '\n')
    np.save(gt_file, np.array(gt_flat, dtype=np.int32))

    test_code = f'''
import sys, os, json
sys.path.insert(0, {repr(str(PROJECT_ROOT))})
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
import numpy as np
import paddle
paddle.set_device("gpu")

from ppcls.arch.backbone.model_zoo.regnet import RegNet
from paddle.io import Dataset, DataLoader
from PIL import Image as PILImage
from paddle.vision import transforms as paddle_transforms

cfg = {cfg!r}
se_on = {se_on!r}
model_name = {model_name!r}
weight_path = {repr(str(weight_path))}
batch_size = {batch_size}
paths_file = {repr(str(paths_file))}
gt_file = {repr(str(gt_file))}
pred_out = {repr(str(pred_out))}

IMAGENET_MEAN = {list(IMAGENET_DEFAULT_MEAN)!r}
IMAGENET_STD = {list(IMAGENET_DEFAULT_STD)!r}

with open(paths_file) as f:
    image_paths = [line.strip() for line in f if line.strip()]
gt = np.load(gt_file)

model = RegNet(
    w_a=cfg["w_a"], w_0=cfg["w_0"], w_m=cfg["w_m"],
    d=cfg["d"], group_w=cfg["group_w"],
    bot_mul=1.0, q=8, se_on=se_on, class_num=1000)
param_dict = paddle.load(weight_path)
model.set_dict(param_dict)
model.eval()

class _ValDS(Dataset):
    def __init__(self, paths, size=224, crop_pct=0.9):
        self.paths = paths
        ss = int(size / crop_pct)
        self.transform = paddle_transforms.Compose([
            paddle_transforms.Resize(ss, interpolation='bicubic'),
            paddle_transforms.CenterCrop(size),
            paddle_transforms.ToTensor(),
            paddle_transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
        ])
    def __len__(self):
        return len(self.paths)
    def __getitem__(self, idx):
        try:
            img = PILImage.open(self.paths[idx]).convert('RGB')
            return self.transform(img), idx
        except Exception:
            return paddle.zeros([3, 224, 224], dtype='float32'), idx

predictions = np.zeros(len(image_paths), dtype=np.int32)
ds = _ValDS(image_paths)
loader = DataLoader(ds, batch_size=batch_size, shuffle=False,
                    num_workers=4, drop_last=False)
for batch_imgs, batch_idxs in loader:
    with paddle.no_grad():
        logits = model(batch_imgs)
    preds = logits.argmax(axis=1).numpy()
    idxs = batch_idxs.numpy()
    predictions[idxs] = preds.astype(np.int32)

correct = int(np.sum(predictions == gt))
accuracy = correct / len(gt) * 100 if len(gt) > 0 else 0.0
np.save(pred_out, predictions)
print("JSON_RESULT:" + json.dumps({{
    "correct": correct, "num_images": len(image_paths),
    "top1_acc": float(accuracy)
}}))
'''
    result = subprocess.run(
        [sys.executable, "-c", test_code],
        capture_output=True, text=True, timeout=600,
        env={**os.environ, "CUDA_VISIBLE_DEVICES": "0"})

    for f in [paths_file, gt_file]:
        if f.exists():
            f.unlink()

    if result.returncode != 0:
        err = result.stderr.strip().split('\n')[-1] if result.stderr.strip() else "unknown"
        raise RuntimeError(f"Paddle subprocess failed: {err}")

    for line in result.stdout.strip().split('\n'):
        if line.startswith("JSON_RESULT:"):
            import json
            data = json.loads(line[len("JSON_RESULT:"):])
            predictions = None
            if pred_out.exists():
                predictions = np.load(pred_out)
                pred_out.unlink()

            result_dict = {
                'model_variant': model_name,
                'framework': 'paddle',
                'num_images': data['num_images'],
                'valid_images': data['num_images'],
                'correct': data['correct'],
                'top1_acc': data['top1_acc'],
                'predictions': predictions,
            }

            if predictions is not None and predictions_dir:
                predictions_dir.mkdir(parents=True, exist_ok=True)
                np.save(predictions_dir / f"{model_name}_predictions.npy", predictions)
                logger.info(f"Predictions saved")

            return result_dict

    raise RuntimeError("No JSON_RESULT found in Paddle subprocess output")

def batch_inference_timm(
    model_name: str,
    image_paths: List[Path],
    ground_truth: List[List[int]],
    batch_size: int = 32,
    predictions_dir: Optional[Path] = None
) -> dict:
    """timm batch inference (PyTorch)."""
    import torch
    import timm as _timm
    from torch.utils.data import Dataset as TorchDataset, DataLoader as TorchDataLoader
    from PIL import Image as PILImage
    from torchvision import transforms as tv_transforms

    timm_name = f"{model_name}.pycls_in1k"
    logger.info(f"Loading timm model: {timm_name}")

    model = _timm.create_model(timm_name, pretrained=True)
    model.eval()
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model = model.to(device)
    logger.info(f"timm model loaded, device={device}")

    IMAGENET_MEAN = list(IMAGENET_DEFAULT_MEAN)
    IMAGENET_STD = list(IMAGENET_DEFAULT_STD)
    crop_pct = 0.9
    scale_size = int(224 / crop_pct)

    transform = tv_transforms.Compose([
        tv_transforms.Resize(scale_size, interpolation=tv_transforms.InterpolationMode.BICUBIC),
        tv_transforms.CenterCrop(224),
        tv_transforms.ToTensor(),
        tv_transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),
    ])

    class _TimmValDataset(TorchDataset):
        def __init__(self, paths):
            self.paths = paths

        def __len__(self):
            return len(self.paths)

        def __getitem__(self, idx):
            try:
                img = PILImage.open(str(self.paths[idx])).convert('RGB')
                return transform(img), idx
            except Exception:
                return torch.zeros(3, 224, 224), idx

    num_images = len(image_paths)
    predictions = np.zeros(num_images, dtype=np.int32)

    logger.info(f"timm inference: {num_images} images, batch_size={batch_size}")

    dataset = _TimmValDataset(image_paths)
    loader = TorchDataLoader(dataset, batch_size=batch_size, shuffle=False,
                             num_workers=4, drop_last=False)

    iter_obj = tqdm(loader, desc=f"timm {model_name}") if tqdm else loader

    with torch.no_grad():
        for batch_imgs, batch_idxs in iter_obj:
            batch_imgs = batch_imgs.to(device)
            logits = model(batch_imgs)
            preds = logits.argmax(dim=1).cpu().numpy()
            idxs = batch_idxs.numpy()
            predictions[idxs] = preds.astype(np.int32)

    correct = sum(1 for i, pred in enumerate(predictions)
                  if ground_truth[i] and int(pred) in ground_truth[i])
    valid = sum(1 for gt in ground_truth if gt)
    accuracy = correct / valid * 100 if valid > 0 else 0.0

    result = {
        'model_variant': model_name,
        'framework': 'timm',
        'num_images': num_images,
        'valid_images': valid,
        'correct': correct,
        'top1_acc': float(accuracy),
        'predictions': predictions,
    }

    if predictions_dir:
        predictions_dir.mkdir(parents=True, exist_ok=True)
        np.save(predictions_dir / f"{model_name}_timm_predictions.npy", predictions)
        logger.info("timm predictions saved")

    return result

def generate_report(results: List[Dict], start_time: datetime, end_time: datetime, args):
    """Generate markdown validation report."""
    output_dir = Path(__file__).parent.parent / 'docs'
    output_dir.mkdir(parents=True, exist_ok=True)
    report_file = output_dir / 'topacc_validation_report.md'

    paddle_results = {r['model_variant']: r for r in results if r.get('framework') == 'paddle'}
    timm_results = {r['model_variant']: r for r in results if r.get('framework') == 'timm'}
    all_variants = list(dict.fromkeys(r['model_variant'] for r in results))

    def sort_key(name):
        family = 0 if name.startswith('regnetx') else 1
        size = int(name.split('_')[1])
        return (family, size)
    all_variants.sort(key=sort_key)

    with open(report_file, 'w', encoding='utf-8') as f:
f.write("
        f.write(f"**Generated**: {end_time.strftime('%Y-%m-%d %H:%M:%S')}\n")
        f.write(f"**Duration**: {(end_time - start_time).total_seconds():.2f}s\n\n")
        f.write("---\n\n")

f.write("
        f.write(f"- **Dataset**: ImageNet val (synset sorted, single-label)\n")
        f.write(f"- **Images**: {results[0]['num_images'] if results else 'N/A'}\n")
        f.write(f"- **Batch size**: {args.batch_size}\n")
        f.write(f"- **Models**: {len(all_variants)}\n")
        f.write(f"- **Threshold**: |Paddle - timm| < 0.2%\n\n")

f.write("
        f.write("| Model | Paddle Top-1 | timm Top-1 | Diff | Pass |\n")
        f.write("|-------|-------------|-----------|------|------|\n")
        for variant in all_variants:
            p = paddle_results.get(variant)
            t = timm_results.get(variant)
            if p and t:
                diff = abs(p['top1_acc'] - t['top1_acc'])
                passed = "PASS" if diff < 0.2 else "FAIL"
                sign = "+" if p['top1_acc'] >= t['top1_acc'] else "-"
                f.write(f"| {variant} | {p['top1_acc']:.2f}% | {t['top1_acc']:.2f}% "
                        f"| {sign}{diff:.2f}% | {passed} |\n")
            elif p:
                f.write(f"| {variant} | {p['top1_acc']:.2f}% | N/A | N/A | N/A |\n")
            elif t:
                f.write(f"| {variant} | N/A | {t['top1_acc']:.2f}% | N/A | N/A |\n")
        f.write("\n")

f.write("
        for variant in all_variants:
f.write(f"
            for tag, res in [('Paddle', paddle_results.get(variant)),
                             ('timm', timm_results.get(variant))]:
                if not res:
                    continue
                f.write(f"**{tag}**:\n")
                f.write(f"- Correct / Total: {res['correct']} / {res['num_images']}\n")
                f.write(f"- Top-1 Acc: {res['top1_acc']:.2f}%\n")
                if 'inference_time' in res:
                    f.write(f"- Time: {res['inference_time']:.2f}s "
                            f"({res['num_images']/res['inference_time']:.0f} img/s)\n")
            if variant in paddle_results and variant in timm_results:
                p_preds = paddle_results[variant].get('predictions')
                t_preds = timm_results[variant].get('predictions')
                if p_preds is not None and t_preds is not None:
                    agree = int(np.sum(p_preds == t_preds))
                    total = len(p_preds)
                    f.write(f"\n**Agreement**: {agree}/{total} ({agree/total*100:.2f}%)\n")
            f.write("\n")

f.write("
        f.write("```\n")
        f.write("crop_pct=0.9, scale_size=int(224/0.9)=248\n")
        f.write("1. Resize(short edge to 248, BICUBIC)\n")
        f.write("2. CenterCrop(224)\n")
        f.write("3. Normalize(mean=(0.485,0.456,0.406), std=(0.229,0.224,0.225))\n")
        f.write("```\n\n")

        f.write("---\n\n")
        f.write(f"*Report generated: {end_time.strftime('%Y-%m-%d %H:%M:%S')}*\n")

    logger.info(f"Report saved: {report_file}")
    return report_file

def main():
    parser = argparse.ArgumentParser(
        description='RegNet ImageNet Top-1 Accuracy Validation',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python topacc.py --model all
  python topacc.py --model regnety_002
  python topacc.py --model regnety_002 --num-samples 100
  python topacc.py --model all --paddle-only
        """
    )
    parser.add_argument(
        '--model', type=str, default='all',
        choices=['all'] + ALL_MODEL_NAMES,
        help='Model variant (default: all)')
    parser.add_argument(
        '--batch-size', type=int, default=32,
        help='Batch size (default: 32)')
    parser.add_argument(
        '--num-samples', type=int, default=50000,
        help='Limit validation images (default: 50000)')
    parser.add_argument(
        '--paddle-only', action='store_true',
        help='Only run Paddle inference')
    parser.add_argument(
        '--timm-only', action='store_true',
        help='Only run timm inference')

    args = parser.parse_args()

    HIS_dir = Path(__file__).parents[3]
    base_dir = HIS_dir / 'regnety' / '3-diff'
    weight_dir = HIS_dir / 'regnety' / 'weight' / 'converted'
    output_dir = base_dir / 'docs'
    log_dir = base_dir / 'logs'
    imagenet_base = PROJECT_ROOT / 'dataset' / 'imagenet'

    log_dir.mkdir(parents=True, exist_ok=True)
    output_dir.mkdir(parents=True, exist_ok=True)

    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    log_file = log_dir / f'topacc_{timestamp}.log'
    global logger
    logger = setup_logging(str(log_file))

    start_time = datetime.now()
    logger.info("=" * 80)
    logger.info("RegNet ImageNet Top-1 Accuracy Validation")
    logger.info("=" * 80)
    logger.info(f"Start: {start_time.strftime('%Y-%m-%d %H:%M:%S')}")
    logger.info(f"Model: {args.model}")
    logger.info(f"Batch size: {args.batch_size}")
    logger.info(f"Samples: {args.num_samples}")

    logger.info("\n" + "=" * 60)
    logger.info("Step 1: Prepare validation data")
    logger.info("=" * 60)

    synset_dirs = sorted([d for d in imagenet_base.iterdir()
                          if d.is_dir() and d.name.startswith('n')])
    if not synset_dirs:
        logger.error(f"No synset dirs found: {imagenet_base}")
        return 1
    logger.info(f"Found {len(synset_dirs)} synset dirs")

    synset_to_classid = {d.name: idx for idx, d in enumerate(synset_dirs)}

    all_image_paths = []
    all_gt_labels = []
    for d in synset_dirs:
        class_id = synset_to_classid[d.name]
        for img_path in sorted(d.glob('*.JPEG')):
            all_image_paths.append(img_path)
            all_gt_labels.append([class_id])

    logger.info(f"Found {len(all_image_paths)} images (all with GT labels)")

    num_samples = min(args.num_samples, len(all_image_paths))
    image_files = all_image_paths[:num_samples]
    ground_truth = all_gt_labels[:num_samples]
    logger.info(f"Validation samples: {num_samples}")

    if args.model == 'all':
        model_names = ALL_MODEL_NAMES
    else:
        model_names = [args.model]

    logger.info(f"\nModels to validate: {', '.join(model_names)}")

    logger.info("\n" + "=" * 60)
    logger.info("Step 2: CUDA warmup")
    logger.info("=" * 60)
    import paddle
    _dummy = paddle.zeros([1], dtype='float32')
    _dummy = paddle.nn.functional.relu(_dummy)
    del _dummy
    logger.info("CUDA warmup done")

    logger.info("\n" + "=" * 60)
    logger.info("Step 3: Batch inference")
    logger.info("=" * 60)

    predictions_dir = base_dir / 'data' / 'predictions'
    predictions_dir.mkdir(parents=True, exist_ok=True)

    all_results = []

    for model_name in model_names:
        logger.info(f"\n{'=' * 50}")
        logger.info(f"Model: {model_name}")
        logger.info(f"{'=' * 50}")

        if not args.paddle_only:
            try:
                t_start = time.time()
                timm_result = batch_inference_timm(
                    model_name, image_files, ground_truth,
                    args.batch_size, predictions_dir)
                timm_result['inference_time'] = time.time() - t_start
                all_results.append(timm_result)
                logger.info(f"  [timm] Top-1: {timm_result['top1_acc']:.2f}%  "
                            f"({timm_result['correct']}/{timm_result['num_images']})")
            except Exception as e:
                logger.error(f"timm inference failed for {model_name}: {e}")
                import traceback; traceback.print_exc()

        if not args.timm_only:
            weight_path = weight_dir / f"{model_name}.pdparams"
            try:
                p_start = time.time()
                result = paddle_inference_subprocess(
                    model_name, str(weight_path), image_files, ground_truth,
                    args.batch_size, predictions_dir)
                result['inference_time'] = time.time() - p_start
                all_results.append(result)
                logger.info(f"  [Paddle] Top-1: {result['top1_acc']:.2f}%  "
                            f"({result['correct']}/{result['num_images']})")

                timm_acc = next((r['top1_acc'] for r in all_results
                                 if r['model_variant'] == model_name
                                 and r.get('framework') == 'timm'), None)
                if timm_acc is not None:
                    diff = abs(result['top1_acc'] - timm_acc)
                    status = "PASS" if diff < 0.2 else "FAIL"
                    logger.info(f"  [Diff]  |Paddle - timm| = {diff:.2f}%  -> {status} (<0.2%)")

            except Exception as e:
                logger.error(f"Paddle inference failed for {model_name}: {e}")
                import traceback; traceback.print_exc()

    if all_results:
        logger.info("\n" + "=" * 60)
        logger.info("Step 4: Generate report")
        logger.info("=" * 60)

        end_time = datetime.now()
        generate_report(all_results, start_time, end_time, args)

    logger.info("\n" + "=" * 80)
    logger.info("Validation complete")
    logger.info("=" * 80)

    if all_results:
        paddle_map = {r['model_variant']: r for r in all_results
                      if r.get('framework') == 'paddle'}
        timm_map = {r['model_variant']: r for r in all_results
                    if r.get('framework') == 'timm'}

        logger.info(f"\n{'Model':<20} {'Paddle':>10} {'timm':>10} {'Diff':>8} {'Pass':>6}")
        logger.info("  " + "-" * 58)
        for model_name in model_names:
            p_acc = paddle_map[model_name]['top1_acc'] if model_name in paddle_map else float('nan')
            t_acc = timm_map[model_name]['top1_acc'] if model_name in timm_map else float('nan')
            if not (p_acc != p_acc or t_acc != t_acc):
                diff = abs(p_acc - t_acc)
                ok = "OK" if diff < 0.2 else "FAIL"
                logger.info(f"  {model_name:<20} {p_acc:>9.2f}% {t_acc:>9.2f}% {diff:>7.2f}% {ok:>6}")
            elif p_acc == p_acc:
                logger.info(f"  {model_name:<20} {p_acc:>9.2f}% {'N/A':>10} {'N/A':>8} {'N/A':>6}")
            else:
                logger.info(f"  {model_name:<20} {'N/A':>10} {t_acc:>9.2f}% {'N/A':>8} {'N/A':>6}")

    end_time = datetime.now()
    logger.info(f"\nTotal: {(end_time - start_time).total_seconds():.2f}s")
    logger.info(f"Report: {output_dir / 'topacc_validation_report.md'}")

    return 0

if __name__ == "__main__":
    sys.exit(main())

@zhangyubo0722
zhangyubo0722 merged commit 8fdefd2 into PaddlePaddle:develop Jul 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants