从零开始构建仓储视觉识别系统:从图像标注到模型部署

目录

从零开始构建仓储视觉识别系统:从图像标注到模型部署


📚 系列文章导航

本系列围绕企业知识助手、售后智能体和仓储视觉识别,介绍从数据准备、模型开发到部署评测的实践流程,并提供配套排障指南。有修订版的文章,建议优先阅读修订版。

一、企业知识助手

从业务资料整理开始,逐步完成问答数据构建、模型微调、权重合并、量化与本地部署。

👉 从零开始构建企业知识助手:业务资料整理、模型微调与本地部署(修订版)

二、售后智能体

围绕售后业务,学习工具注册、智能体构建以及上线评测。

👉 从零开始构建售后智能体:从工具注册到上线评测

三、仓储视觉识别

从图像标注开始,逐步完成视觉模型训练、导出与部署。

👉 从零开始构建仓储视觉识别系统:从图像标注到模型部署(修订版)

四、模型开发排障

遇到环境、依赖、训练、推理、量化或部署问题时,可以按故障所在环节查阅。

👉 模型开发实用排障指南:从 Python(编程语言)环境到训练、推理与部署(修订版)

历史版本


仓库希望从现场照片中找出纸箱、周转箱和托盘的位置,供工作人员核对。我们将围绕这个业务,完成图片整理、人工标注、训练、评估、模型压缩和运行端迁移。这是一份可以边做边查的学习记录,不要求先会编程;示例场景不附带真实业务数据,也不预设任何识别成绩。

这里做的是Object Detection(目标检测):既判断物体是什么,也给出它在图里的位置。人先用矩形写出参考答案,再让模型从这些图片学习;学习后用另一批图片检查效果,最后把模型交给独立程序处理新图片。

文章先给出一条完整实践主线,再集中解释参数与可选做法,最后按现象排查问题。主线统一使用三类目标、640像素输入、一次离线增强、固定版本训练框架、处理器上的独立推理。示例数字只是让各段能衔接,不是普遍最优配置。

Python(编程语言)代码写进文件,终端命令在命令窗口执行,两者不要混贴。代码框上方会标明新建或追加的文件;同名文件按出现顺序拼接,保留缩进,不复制代码框边界。术语在文字中解释,代码标识符和文件名保持程序要求的原样。文中图片为教学示意,不是实际软件截图。

第一部分:按顺序完成实践

1. 建立本地和服务器环境

目标:准备能标注、能运行脚本的工作位置。

操作位置:本地Windows(桌面系统)使用PowerShell 7(命令终端);训练端使用Ubuntu(服务器系统)的Bash(命令解释器)。

先修改:连接地址、端口、账号及可写目录必须换成自己的;主线本地目录为D:\warehouse_vision,服务器目录为/home/user/projects/warehouse_vision

参数与原理见对应参考。下面按顺序操作。

先认识三个类别:0 → carton(纸箱)、1 → tote(周转箱)、2 → pallet(托盘)。后续标注、数据配置、评估和推理必须使用同一顺序。改变业务类别时先看类别与坐标约定,不能只改界面上的名称。

在本地建立标注环境

以下命令假设已安装Conda(环境管理工具)。找不到命令时先按环境准备与替代方案完成安装或初始化,再回来继续;已有可用环境应先核对版本,不在原环境盲目升级。

conda create -n warehouse_label python=3.10 -y
conda activate warehouse_label
python -m pip install "numpy==1.26.4" "labelme==5.6.1" "opencv-python==4.10.0.84" "pandas==2.2.3"
python -m pip check
New-Item -ItemType Directory -Force D:\warehouse_vision\raw,D:\warehouse_vision\curated,D:\warehouse_vision\review,D:\warehouse_vision\reports | Out-Null
Set-Location D:\warehouse_vision
python -c "import sys; print(sys.executable)"

raw(原件目录)存照片;curated(整理目录)存待标注图片;review(复查目录)存暂不采用的图片;reports(记录目录)存检查记录。这几个目录首次使用应为空。

连接服务器并准备训练环境

另开一个本地终端,输入连接命令。密码输入通常不显示字符,按提示完成登录,不把密码写入代码。

ssh -p 实际端口 实际账号@实际服务器地址
hostname
pwd
nvidia-smi
free -h
df -h
conda env list
mkdir -p /home/user/projects/warehouse_vision/curated /home/user/projects/warehouse_vision/models /home/user/projects/warehouse_vision/reports /home/user/projects/warehouse_vision/exports /home/user/projects/warehouse_vision/deployment
cd /home/user/projects/warehouse_vision
conda create -n warehouse_train python=3.10 -y
conda activate warehouse_train

主线假设服务器有可供PyTorch(训练框架)使用的NVIDIA(显卡厂商)显卡,并且驱动兼容CUDA(显卡计算平台)12.1构建。先让环境提供者确认这一条件;不同硬件请使用对应安装分支,不要直接套用下面的构建。确认兼容后执行:

python -m pip install "torch==2.5.1" "torchvision==0.20.1" --index-url https://download.pytorch.org/whl/cu121
python -m pip install "numpy==1.26.4" "opencv-python==4.10.0.84" "ultralytics==8.3.40" "pandas==2.2.3" "matplotlib==3.9.2" "scikit-learn==1.5.2" "onnx==1.17.0" "onnxruntime==1.20.1" "pillow==11.0.0" "pyyaml==6.0.2"
python -m pip check
python -c "import torch, ultralytics; print(torch.__version__, torch.cuda.is_available(), ultralytics.__version__)"
python -m pip freeze > reports/environment.txt

版本组合用于明确教学接口,不代表已在你的机器上安装成功。后续训练使用显卡编号0;独立推理使用CPU(中央处理器),不能把这两个阶段的速度直接比较。

学会保存和运行代码

本地用文本编辑器打开项目文件夹,新建指定的.py文件,按UTF-8(统一字符编码)保存。不要保存成.py.txt。服务器可用nano 文件名.py(打开文本编辑器);粘贴代码后按Ctrl+O(保存)、回车,再按Ctrl+X(退出)。初次不熟悉编辑器时,先看编辑器具体操作

保存只会写文件。运行必须回到对应终端,在项目根目录执行python 文件名.py。每次重新连接服务器,都先激活环境并进入项目根目录。

完成检查:本地解释器路径属于标注环境,服务器版本符合所选组合;显卡主线的可用性检查返回True(是),目录可写。安装失败先排错,不带着依赖错误开始训练。

2. 收集图片并保留清理记录

目标:形成可解码、命名一致、经过人工复核的图片集合。

操作位置:本地标注环境,工作目录D:\warehouse_vision

先修改:把原始图片放进raw;按采集组命名,例如session01__0001.jpg,双下划线前是同一段视频或同一次连拍的组名。

参数与原理见对应参考。下面按顺序操作。

收集不同光照、距离、遮挡和背景的场景;准备各类别的正例,也保留人工确认没有目标的负例。相邻帧不能当作独立场景。主线从现成照片开始;只有视频时先执行可选抽帧,再回到这里。

定位原始图片和输出目录

新建项目根目录的prepare_images.py

Path(路径对象)帮助程序找到文件;ROOT(项目根目录变量)始终跟随脚本位置。此脚本只处理原始目录第一层。

from pathlib import Path
from PIL import Image, ImageOps
import hashlib
import numpy as np
import cv2
import pandas as pd

ROOT = Path(__file__).resolve().parent
output = ROOT / "curated"
output.mkdir(exist_ok=True)
if any(output.iterdir()):
    raise ValueError("整理目录已有文件,请使用新的实验目录,避免覆盖已有标注")
files = sorted(p for p in (ROOT / "raw").iterdir()
               if p.suffix.lower() in {".jpg", ".jpeg", ".png", ".bmp"})
if not files:
    raise ValueError("原始图片目录为空")
seen, names, records = {}, set(), []

读取图片、修正方向并检查重复

prepare_images.py文件末尾追加,紧接该文件上一个示例。

读取失败记为待复查;像素完全相同记为重复。清晰度数值只用于人工排序,不设置自动删图阈值。

for path in files:
    try:
        with Image.open(path) as im:
            im = ImageOps.exif_transpose(im).convert("RGB")
            array = np.array(im)
        digest = hashlib.sha256(str(array.shape).encode() + array.tobytes()).hexdigest()
        if digest in seen:
            records.append({"source": path.name, "status": "duplicate", "detail": seen[digest]})
            continue
        if "__" not in path.stem or path.stem in names:
            raise ValueError("名称应包含采集组双下划线,且转换后不能重名")
        gray = cv2.cvtColor(array, cv2.COLOR_RGB2GRAY)
        sharpness = float(cv2.Laplacian(gray, cv2.CV_64F).var())
        im.save(output / (path.stem + ".png"))
        names.add(path.stem)
        seen[digest] = path.name
        records.append({"source": path.name, "status": "candidate", "detail": sharpness})
    except Exception as error:
        records.append({"source": path.name, "status": "review", "detail": str(error)})

保存清理清单

prepare_images.py文件末尾追加,紧接该文件上一个示例。

(ROOT / "reports").mkdir(exist_ok=True)
pd.DataFrame(records).to_csv(ROOT / "reports/image_inventory.csv", index=False, encoding="utf-8-sig")
print("记录已保存,请人工复查候选图片与失败项。")
Set-Location D:\warehouse_vision
python prepare_images.py

打开reports/image_inventory.csv(图片清单),查看candidate(候选)、duplicate(重复)和review(待复查)。再用图片查看器逐张检查整理目录,把不采用的图片移到复查目录。重复图和坏图的原件仍在原件目录。修正方向以后再画框。

完成检查:清理记录与候选图片相符,严重坏图已移出,困难但可辨认的样本没有被一概排除;每个采集组仍能从文件名追溯。

3. 使用标注工具画框、修正并保存

目标:给每个目标建立正确的矩形和类别答案。

操作位置:本地Labelme(图像标注工具)5.6.1窗口。

先修改:输入和输出目录均为D:\warehouse_vision\curated;使用矩形、手动保存、不嵌入图片、不保留上一张标注。

参数与原理见对应参考。下面按顺序操作。

约定画框规则并打开目录

一个独立目标画一个框。两个纸箱分开画,纸箱和托盘可分别画框且允许重叠。本例统一框住可辨认目标的可见外接范围,不猜测遮挡后的完整轮廓。边缘目标框到图片边界;无法确认类别的图片先移入复查集合。

02_边界框质量示意

conda activate warehouse_label
labelme

01_标注界面导览

  1. 点击File(文件)→ Open Dir(打开目录),选择整理目录,在File List(文件列表)中单击图片。
  2. 点击File(文件)→ Change Output Dir(更改输出目录),选择同一个整理目录。
  3. 取消Save With Image Data(保存时嵌入图片数据);关闭自动保存,先练习手动保存。
  4. 确认Keep Previous Annotation(保留上一张标注)未启用。Label List(类别列表)是类别名称,Polygon Labels(图形标注列表)才是当前图中已标的对象,不要混淆。

给一个物体画矩形

  1. 点击Edit(编辑)→ Create Rectangle(创建矩形)。
  2. 在可见外轮廓左上角单击,再移动到右下角单击,完成两个角点。
  3. 标签窗口中输入carton(纸箱),点击OK(确定)。其他类别分别填写tote(周转箱)、pallet(托盘)。若工具直接复用了已选类别,立即检查结果。
  4. 查看画布及图形标注列表。目标应完整,额外背景尽量少。再次选择创建矩形,为下一个对象画框。

调整大小、位置和类别

先点击Edit(编辑)→ Edit Polygons(编辑图形)。选中框后拖动角点改大小,拖动框内部改位置;双击右侧对应标注条目,或选中后用Edit Label(编辑标签)改类别。选中多余的框,使用Delete Polygons(删除图形)删除。

未完成的形状用Esc(取消)退出。Delete File(删除文件)针对整份标注文件,不能用来代替删除单个框。放大检查边缘,完成后用Fit Window(适应窗口)恢复整图,从左到右再检查漏标。更多快捷键及撤销条件见标注工具参考

保存后重新打开验证

点击Save(保存),确认文件名与图片同名,仅扩展名变为.json。在系统文件管理器中确认图片和标注都在整理目录。点击Next Image(下一张)后再返回,最后关闭工具并重新打开一张已标图片,核对框能恢复。

没有目标的图片也要人工确认并保存空标注。普通保存不可用时尝试Save As(另存为);打开文件确认shapes(图形列表)为[](空列表)。没有标注文件表示尚未确认,不自动当成负样本。

完成检查:每张采用的图片都有同名标注,重新打开可显示;多个对象分别有框,类别拼写一致,空标注确实对应无目标图片。

4. 传输标注并构建训练、验证数据

目标:把矩形答案转换成模型可读标签,按采集组划分并画回图片复核。

操作位置:先在本地文件传输客户端上传;后面的代码在训练服务器项目根目录编写并运行。

先修改:代码中的NAMES(类别顺序)保持三类约定;VAL_GROUPS(验证采集组)换成真实存在、覆盖全部类别的一部分采集组,必须留出训练组。

参数与原理见对应参考。下面按顺序操作。

上传图片及同名标注

在文件传输客户端新建连接,选择服务实际支持的SFTP(安全文件传输协议)或FTP(文件传输协议),填写真实地址、端口和账号。进入远端项目根目录,把本地整个整理目录上传。等待队列结束,检查失败项和远端文件数量,防止形成两层同名目录。客户端下载界面和复核动作见文件传输参考

服务器目标路径应是curated/session01__0001.png和同名标注文件。下载回一对图片与标注重开检查,确认传输可用后继续。

填写类别和验证采集组

新建项目根目录的build_dataset.py

示例使用session04session08,只是命名示范。原始数据不同就修改这两个值;不要为了凑名字把同一视频拆成两个组。

import json
import shutil
from pathlib import Path
from collections import Counter
from PIL import Image
import yaml

ROOT = Path(__file__).resolve().parent
NAMES = ["carton", "tote", "pallet"]
VAL_GROUPS = {"session04", "session08"}  # 改成你实际留作验证的采集组
output = ROOT / "dataset"
if output.exists():
    raise ValueError("数据集目录已存在,请使用新目录构建,避免混入旧划分")
images = sorted((ROOT / "curated").glob("*.png"))
if not images:
    raise ValueError("没有整理后的PNG图片")

核对标注并计算中心坐标

build_dataset.py文件末尾追加,紧接该文件上一个示例。

输入是像素角点,输出是相对图片大小的中心和宽高。不认识的类别、非矩形、越界框或缺失标注会报错。计算过程见坐标示例

def read_boxes(image_path):
    annotation = image_path.with_suffix(".json")
    if not annotation.is_file():
        raise ValueError(f"缺少已确认的标注:{image_path.name}")
    data = json.loads(annotation.read_text(encoding="utf-8"))
    with Image.open(image_path) as image:
        width, height = image.size
    if (data["imageWidth"], data["imageHeight"]) != (width, height):
        raise ValueError("图片尺寸与标注记录不同")
    if (annotation.parent / data["imagePath"]).resolve() != image_path.resolve():
        raise ValueError("标注指向另一张图片,请先修正图片路径")
    result = []
    for shape in data["shapes"]:
        if shape["shape_type"] != "rectangle" or len(shape["points"]) != 2:
            raise ValueError("此转换示例只接受两角点矩形,请先统一标注")
        category = NAMES.index(shape["label"])
        (ax, ay), (bx, by) = shape["points"]
        x1, x2 = sorted([float(ax), float(bx)])
        y1, y2 = sorted([float(ay), float(by)])
        if not (0 <= x1 < x2 <= width and 0 <= y1 < y2 <= height):
            raise ValueError("发现越界或面积为零的框")
        result.append((category, (x1+x2)/2/width, (y1+y2)/2/height,
                       (x2-x1)/width, (y2-y1)/height))
    return result

检查训练和验证是否覆盖所有类别

build_dataset.py文件末尾追加,紧接该文件上一个示例。

items, counts, totals = [], {"train": Counter(), "val": Counter()}, Counter()
groups = {p.stem.split("__")[0] for p in images}
if not VAL_GROUPS or not VAL_GROUPS <= groups or groups <= VAL_GROUPS:
    raise ValueError("请选择实际存在的部分采集组作为验证集,并保留训练组")
for path in images:
    if "__" not in path.stem:
        raise ValueError("图片名称缺少采集组信息")
    split = "val" if path.stem.split("__")[0] in VAL_GROUPS else "train"
    boxes = read_boxes(path)
    counts[split].update(box[0] for box in boxes)
    totals[split] += 1
    items.append((path, split, boxes))
for split in ("train", "val"):
    if set(counts[split]) != set(range(len(NAMES))):
        raise ValueError(f"{split}中有类别没有标注实例,请调整真实数据覆盖")
print("图片数量:", dict(totals), "目标数量:", counts)

生成图片目录、标签目录和数据配置

build_dataset.py文件末尾追加,紧接该文件上一个示例。

for split in ("train", "val"):
    (output / "images" / split).mkdir(parents=True)
    (output / "labels" / split).mkdir(parents=True)
for path, split, boxes in items:
    shutil.copy2(path, output / "images" / split / path.name)
    lines = [str(b[0]) + " " + " ".join(f"{v:.6f}" for v in b[1:]) for b in boxes]
    (output / "labels" / split / (path.stem + ".txt")).write_text(
        "\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
config = {"path": str(output.resolve()), "train": "images/train", "val": "images/val",
          "names": dict(enumerate(NAMES))}
(ROOT / "warehouse.yaml").write_text(yaml.safe_dump(config, allow_unicode=True), encoding="utf-8")
(ROOT / "reports/split_manifest.json").write_text(
    json.dumps({p.name: s for p, s, _ in items}, ensure_ascii=False, indent=2), encoding="utf-8")
cd /home/user/projects/warehouse_vision
python build_dataset.py
cat warehouse.yaml

应得到dataset/images/train(训练图片)、dataset/images/val(验证图片)及对应的dataset/labels(标签目录),以及warehouse.yaml(数据配置)。配置的绝对路径要指向服务器实际数据位置。

准备标签重绘目录

新建项目根目录的preview_labels.py

from pathlib import Path
from PIL import Image, ImageDraw

ROOT = Path(__file__).resolve().parent
names = ["carton", "tote", "pallet"]
output = ROOT / "reports/label_previews"
output.mkdir(exist_ok=True)

把转换标签画回原图

preview_labels.py文件末尾追加,紧接该文件上一个示例。

for split in ("train", "val"):
    for path in sorted((ROOT / "dataset/images" / split).glob("*.png")):
        image = Image.open(path).convert("RGB")
        draw = ImageDraw.Draw(image)
        width, height = image.size
        label = ROOT / "dataset/labels" / split / (path.stem + ".txt")
        for line in label.read_text(encoding="utf-8").splitlines():
            category, x, y, w, h = map(float, line.split())
            box = ((x-w/2)*width, (y-h/2)*height, (x+w/2)*width, (y+h/2)*height)
            draw.rectangle(box, outline="lime", width=3)
            draw.text((box[0], max(0, box[1]-14)), names[int(category)], fill="red")
        image.save(output / (split + "__" + path.name))
python preview_labels.py

完成检查:下载并查看reports/label_previews(标签预览),框的位置和类别与标注一致;reports/split_manifest.json(划分记录)中同一采集组没有跨训练、验证。修正时改原始标注,在新的输出目录重新构建,不改预览图冒充标签修正。

5. 保存训练集增强副本

目标:用一次可追溯的亮度变化与水平翻转扩充训练图片,保持验证集不动。

操作位置:训练服务器项目根目录。

先修改:主线使用固定随机种子42、翻转概率0.5、亮度范围0.85—1.15;只处理训练图片。

参数与原理见对应参考。下面按顺序操作。

准备增强输入和固定随机过程

新建项目根目录的augment_training.py

from pathlib import Path
from PIL import Image, ImageEnhance, ImageOps
import random
import pandas as pd

ROOT = Path(__file__).resolve().parent
images = ROOT / "dataset/images/train"
labels = ROOT / "dataset/labels/train"
if list(images.glob("*__aug.png")):
    raise ValueError("已经有增强副本,请不要重复增强这些副本")
paths = sorted(images.glob("*.png"))
rng = random.Random(42)
records = []

同步变换图片和边界框

augment_training.py文件末尾追加,紧接该文件上一个示例。

for path in paths:
    flip = rng.random() < 0.5
    brightness = rng.uniform(0.85, 1.15)
    image = Image.open(path).convert("RGB")
    if flip:
        image = ImageOps.mirror(image)
    image = ImageEnhance.Brightness(image).enhance(brightness)
    lines = []
    for line in (labels / (path.stem+".txt")).read_text(encoding="utf-8").splitlines():
        category, x, y, w, h = map(float, line.split())
        x = 1-x if flip else x
        lines.append(f"{int(category)} {x:.6f} {y:.6f} {w:.6f} {h:.6f}")
    name = path.stem + "__aug"
    image.save(images / (name+".png"))
    (labels / (name+".txt")).write_text("\n".join(lines)+("\n" if lines else ""), encoding="utf-8")
    records.append({"source": path.name, "augmented": name+".png", "flip": flip, "brightness": brightness})
pd.DataFrame(records).to_csv(ROOT / "reports/augmentation_manifest.csv", index=False, encoding="utf-8-sig")
python augment_training.py
python preview_labels.py

打开增强记录,查看新生成的__aug.png(增强副本)和同名标签。翻转必须同步改变框的横坐标;亮度变化不改框。后面的训练示例已经关闭同类在线增强,不需要再手动协调两套设置。方向有业务含义时先把翻转关闭,再重新构建本轮数据。

完成检查:新增副本仅在训练集,重绘后框仍贴合;再次运行会被阻止,防止副本继续叠加。此时可按归档方法备份图片、原始标注、数据配置、划分及增强记录。

6. 加载预训练模型并训练

目标:从已有权重学习仓储三类目标,保留日志、参数和模型文件。

操作位置:训练服务器项目根目录,已激活训练环境。

先修改:准备与Ultralytics(检测框架)8.3.40兼容的YOLOv8n(轻量检测模型)权重,保存为models/yolov8n.pt;核对数据配置与显卡编号0。

参数与原理见对应参考。下面按顺序操作。

预训练权重是已有学习结果,不是标注答案。从可信模型发布方获取兼容文件并保留来源;重命名其他模型不等于转换成兼容结构。主线参数固定为40轮、批量8、输入640、不冻结、不开混合精度,并明确关闭重复在线增强。

导入训练框架并定位项目

新建项目根目录的train_detector.py

from pathlib import Path
from ultralytics import YOLO

ROOT = Path(__file__).resolve().parent

配置训练并设置运行入口

train_detector.py文件末尾追加,紧接该文件上一个示例。

main(主函数)把训练步骤放在一起;最下面的入口判断让直接运行脚本时才启动,避免多进程重复执行。

def main():
    weight = ROOT / "models/yolov8n.pt"
    if not weight.is_file():
        raise FileNotFoundError("请先准备与框架匹配的预训练权重")
    if (ROOT / "runs/warehouse_v1").exists():
        raise ValueError("实验目录已存在,请归档旧实验并同步后续路径后再运行")
    model = YOLO(str(weight))
    model.info()
    model.train(
        data=str(ROOT / "warehouse.yaml"), imgsz=640,
        epochs=40, batch=8, device=0, workers=2,
        optimizer="SGD", lr0=0.005, momentum=0.9, weight_decay=0.0005,
        patience=10, seed=42, amp=False, freeze=None,
        hsv_h=0.0, hsv_s=0.0, hsv_v=0.0,
        degrees=0.0, translate=0.0, scale=0.0,
        fliplr=0.0, flipud=0.0, mosaic=0.0, mixup=0.0,
        project=str(ROOT / "runs"), name="warehouse_v1", exist_ok=False,
        plots=True, save=True,
    )

if __name__ == "__main__":
    main()
python -m py_compile train_detector.py
python -u train_detector.py

语法检查通过后,训练终端应显示数据扫描、每轮损失和验证记录。损失用于观察学习过程,验证指标用于判断泛化;不能只看训练损失。目录已存在时先保留旧实验,再按重训与恢复处理,不覆盖旧结果。

完成检查:检查runs/warehouse_v1/results.csv(逐轮日志)、args.yaml(实际参数)、weights/best.pt(框架按验证准则选出的权重)和last.pt(最后检查点)。没有这些实际文件就先排查训练,不继续导出。

7. 在验证集生成指标、矩阵和曲线

目标:从当前模型的真实预测生成评估文件,并记录统计定义。

操作位置:训练服务器项目根目录。

先修改:默认模型为runs/warehouse_v1/weights/best.pt;报告名为baseline(基准),类别顺序保持一致。

参数与原理见对应参考。下面按顺序操作。

本例同时记录mAP(平均精度均值)、固定阈值F1(精确率与召回率的调和平均)、混淆矩阵和PR(精确率—召回率曲线)。ROC(受试者工作特征曲线)与AUC(曲线下面积)另按‘每张图是否出现某类’计算;它不衡量定位精度,不能与检测mAP混为一谈。详细公式和阈值见指标参考

计算真实框和预测框的重叠比例

新建项目根目录的evaluation_helpers.py

import numpy as np

def pairwise_iou(a, b):
    a = np.asarray(a, dtype=float).reshape(-1, 4)
    b = np.asarray(b, dtype=float).reshape(-1, 4)
    left_top = np.maximum(a[:, None, :2], b[None, :, :2])
    right_bottom = np.minimum(a[:, None, 2:], b[None, :, 2:])
    intersection = np.maximum(right_bottom-left_top, 0).prod(axis=2)
    area_a = np.maximum(a[:, 2:]-a[:, :2], 0).prod(axis=1)
    area_b = np.maximum(b[:, 2:]-b[:, :2], 0).prod(axis=1)
    return intersection / np.maximum(area_a[:, None]+area_b[None, :]-intersection, 1e-12)

读取验证图片的真实标签

evaluation_helpers.py文件末尾追加,紧接该文件上一个示例。

def read_truth(label_path, width, height):
    classes, boxes = [], []
    for line in label_path.read_text(encoding="utf-8").splitlines():
        category, x, y, w, h = map(float, line.split())
        classes.append(int(category))
        boxes.append([(x-w/2)*width, (y-h/2)*height,
                      (x+w/2)*width, (y+h/2)*height])
    return np.array(classes, dtype=int), np.array(boxes, dtype=float).reshape(-1, 4)

记录命中、错类、漏检和误检

evaluation_helpers.py文件末尾追加,紧接该文件上一个示例。

def add_confusion(matrix, gt_classes, gt_boxes, pred_classes, pred_boxes, threshold=0.5):
    overlaps = pairwise_iou(gt_boxes, pred_boxes)
    candidates = np.argwhere(overlaps >= threshold)
    candidates = sorted(candidates, key=lambda p: overlaps[p[0], p[1]], reverse=True)
    used_gt, used_pred = set(), set()
    for g, p in candidates:
        if g in used_gt or p in used_pred:
            continue
        matrix[gt_classes[g], pred_classes[p]] += 1
        used_gt.add(g)
        used_pred.add(p)
    background = len(matrix)-1
    for g, category in enumerate(gt_classes):
        if g not in used_gt:
            matrix[category, background] += 1
    for p, category in enumerate(pred_classes):
        if p not in used_pred:
            matrix[background, category] += 1

建立评估入口和报告目录

新建项目根目录的evaluate_detector.py

import argparse
import json
import hashlib
from pathlib import Path
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from sklearn.metrics import roc_curve, roc_auc_score
from ultralytics import YOLO
from evaluation_helpers import read_truth, add_confusion

ROOT = Path(__file__).resolve().parent
parser = argparse.ArgumentParser()
parser.add_argument("--model", default=str(ROOT / "runs/warehouse_v1/weights/best.pt"))
parser.add_argument("--name", default="baseline")
args = parser.parse_args()
OUT = ROOT / "reports" / args.name
OUT.mkdir(parents=True, exist_ok=False)
NAMES = ["carton", "tote", "pallet"]

调用框架验证当前模型

evaluate_detector.py文件末尾追加,紧接该文件上一个示例。

model = YOLO(args.model, task="detect")
metrics = model.val(data=str(ROOT / "warehouse.yaml"), imgsz=640, batch=1,
                    device="cpu", workers=0, rect=False, conf=0.001, iou=0.45,
                    plots=True, project=str(OUT), name="framework", exist_ok=False)
if [model.names[i] for i in range(len(model.names))] != NAMES:
    raise ValueError("模型类别顺序与本项目不一致")
matrix = np.zeros((len(NAMES)+1, len(NAMES)+1), dtype=int)
presence_truth, presence_scores = [], []
paths = sorted((ROOT / "dataset/images/val").glob("*.png"))
if not paths:
    raise ValueError("验证图片为空")

收集每张图片的预测和类别分数

evaluate_detector.py文件末尾追加,紧接该文件上一个示例。

for path in paths:
    prediction = model.predict(str(path), imgsz=640, device="cpu", conf=0.001,
                               iou=0.45, rect=False, max_det=300, verbose=False)[0]
    height, width = prediction.orig_shape
    label = ROOT / "dataset/labels/val" / (path.stem + ".txt")
    gt_classes, gt_boxes = read_truth(label, width, height)
    pred_classes = prediction.boxes.cls.cpu().numpy().astype(int)
    pred_boxes = prediction.boxes.xyxy.cpu().numpy()
    scores = prediction.boxes.conf.cpu().numpy()
    keep = scores >= 0.25
    add_confusion(matrix, gt_classes, gt_boxes, pred_classes[keep], pred_boxes[keep])
    present = np.zeros(len(NAMES), dtype=int)
    present[np.unique(gt_classes)] = 1
    highest = np.zeros(len(NAMES), dtype=float)
    for category, score in zip(pred_classes, scores):
        highest[category] = max(highest[category], float(score))
    presence_truth.append(present)
    presence_scores.append(highest)

保存混淆矩阵与图像级曲线

evaluate_detector.py文件末尾追加,紧接该文件上一个示例。

labels = NAMES + ["background"]
pd.DataFrame(matrix, index=labels, columns=labels).to_csv(OUT / "confusion_counts.csv", encoding="utf-8-sig")
y, score = np.array(presence_truth), np.array(presence_scores)
auc_values, auc_status = [], []
plt.figure(figsize=(7, 5))
for c, name in enumerate(NAMES):
    if len(np.unique(y[:, c])) < 2:
        auc_values.append(np.nan)
        auc_status.append("undefined: need positive and negative images")
        continue
    fpr, tpr, _ = roc_curve(y[:, c], score[:, c])
    area = float(roc_auc_score(y[:, c], score[:, c]))
    auc_values.append(area)
    auc_status.append("valid")
    plt.plot(fpr, tpr, label=f"{name}: {area:.3f}")
plt.plot([0, 1], [0, 1], "--", color="gray", label="reference")
plt.xlabel("False positive rate")
plt.ylabel("True positive rate")
plt.title("Image-level class presence ROC")
plt.legend()
plt.tight_layout()
plt.savefig(OUT / "presence_roc.jpg", dpi=160)
plt.close()

保存每类检测指标

evaluate_detector.py文件末尾追加,紧接该文件上一个示例。

ap_by_class = {int(c): np.array(ap) for c, ap in zip(metrics.box.ap_class_index, metrics.box.all_ap)}
rows = []
for c, name in enumerate(NAMES):
    tp = int(matrix[c, c])
    fp = int(matrix[:, c].sum())-tp
    fn = int(matrix[c, :].sum())-tp
    precision = tp/(tp+fp) if tp+fp else 0.0
    recall = tp/(tp+fn) if tp+fn else 0.0
    f1 = 2*precision*recall/(precision+recall) if precision+recall else 0.0
    ap = ap_by_class.get(c, np.zeros(10))
    rows.append({"class": name, "AP50": float(ap[0]), "AP50_95": float(ap.mean()),
                 "precision_at025": precision, "recall_at025": recall, "F1_at025": f1,
                 "image_ROC_AUC": auc_values[c], "AUC_status": auc_status[c],
                 "TP": tp, "FP": fp, "FN": fn})
pd.DataFrame(rows).to_csv(OUT / "class_metrics.csv", index=False, encoding="utf-8-sig")

输出检测曲线和总体指标

evaluate_detector.py文件末尾追加,紧接该文件上一个示例。

plt.figure(figsize=(7, 5))
if len(metrics.box.ap_class_index):
    for index, c in enumerate(metrics.box.ap_class_index):
        plt.plot(metrics.box.r_curve[index], metrics.box.p_curve[index], label=NAMES[int(c)])
    plt.legend()
else:
    plt.text(0.1, 0.5, "No valid PR curve; inspect predictions and labels")
plt.xlabel("Recall")
plt.ylabel("Precision")
plt.title("Detection PR at IoU 0.5")
plt.xlim(0, 1)
plt.ylim(0, 1)
plt.tight_layout()
plt.savefig(OUT / "detection_pr.jpg", dpi=160)
plt.close()
summary = {"model": str(Path(args.model).resolve()),
           "model_sha256": hashlib.sha256(Path(args.model).read_bytes()).hexdigest(),
           "mAP50": float(metrics.box.map50), "mAP50_95": float(metrics.box.map),
           "macro_F1_at025": float(np.mean([r["F1_at025"] for r in rows]))}
(OUT / "summary.json").write_text(json.dumps(summary, indent=2), encoding="utf-8")

记录评估口径

evaluate_detector.py文件末尾追加,紧接该文件上一个示例。

method = """检测AP/mAP:使用固定版本框架验证,候选置信度0.001,去重IoU为0.45。
同时记录mAP50与mAP50_95。逐类AP不是跨类别平均值。
矩阵:行真实、列预测;背景列为漏检,背景行为多报;按几何IoU降序一对一匹配。
F1:预测置信度0.25,匹配IoU为0.5;无预测时精确率按0记。
ROC/AUC:图像级类别存在性,一图一例,分数为该类候选框的最高置信度,无候选记0。
AUC缺正例或负例时留空并标记未定义;不是检测定位AUC,不替代mAP。
本报告不把背景-背景格当成检测真负例。
"""
(OUT / "metric_definitions.txt").write_text(method, encoding="utf-8")
print("真实评估输出已写入:", OUT)
python evaluate_detector.py --name baseline

完成检查:报告在reports/baseline:矩阵、每类指标、总体摘要、两张曲线和口径说明都对应当前权重。若AUC_status(曲线面积状态)提示缺少正例或负例,应补足独立验证场景并重算,不能填一个虚构数值。详细文件含义见报告解释

8. 写好独立推理函数、入口和测试脚本

目标:让运行端可以接收任意图片路径,恢复原图坐标,并先检查代码逻辑。

操作位置:训练服务器项目根目录;本阶段只写代码并测试,不要求模型已经导出。

先修改:输入固定[1, 3, 640, 640],输出固定[1, 4+类别数, 候选数];这是本文模型接口,不适用于所有检测模型。

参数与原理见对应参考。下面按顺序操作。

读取带中文路径的图片

新建项目根目录的vision_runtime.py

from pathlib import Path
import cv2
import numpy as np

def read_image(path):
    image = cv2.imdecode(np.fromfile(str(path), dtype=np.uint8), cv2.IMREAD_COLOR)
    if image is None:
        raise ValueError(f"无法读取图片:{path}")
    return image

等比例缩放、填充并调整通道

vision_runtime.py文件末尾追加,紧接该文件上一个示例。

def preprocess(image, size=640):
    height, width = image.shape[:2]
    ratio = min(size/width, size/height)
    new_width, new_height = round(width*ratio), round(height*ratio)
    resized = cv2.resize(image, (new_width, new_height), interpolation=cv2.INTER_LINEAR)
    left = round((size-new_width)/2-0.1)
    top = round((size-new_height)/2-0.1)
    canvas = np.full((size, size, 3), 114, dtype=np.uint8)
    canvas[top:top+new_height, left:left+new_width] = resized
    rgb = canvas[:, :, ::-1]
    tensor = np.ascontiguousarray(rgb.transpose(2, 0, 1)[None], dtype=np.float32)/255.0
    return tensor, (ratio, left, top, width, height)

删除同类别重复框

vision_runtime.py文件末尾追加,紧接该文件上一个示例。

def nms(boxes, scores, threshold=0.45):
    order = scores.argsort()[::-1]
    keep = []
    while len(order):
        i = int(order[0])
        keep.append(i)
        rest = order[1:]
        lt = np.maximum(boxes[i, :2], boxes[rest, :2])
        rb = np.minimum(boxes[i, 2:], boxes[rest, 2:])
        intersection = np.maximum(rb-lt, 0).prod(axis=1)
        area_i = np.maximum(boxes[i, 2:]-boxes[i, :2], 0).prod()
        area_rest = np.maximum(boxes[rest, 2:]-boxes[rest, :2], 0).prod(axis=1)
        iou = intersection/np.maximum(area_i+area_rest-intersection, 1e-12)
        order = rest[iou <= threshold]
    return keep

读取候选框和类别分数

vision_runtime.py文件末尾追加,紧接该文件上一个示例。

def candidates(output, class_count, confidence=0.25):
    output = np.asarray(output)
    if output.ndim != 3 or output.shape[0] != 1 or output.shape[1] != 4+class_count:
        raise ValueError(f"不是本教程约定的YOLOv8检测输出:{output.shape}")
    if not np.isfinite(output).all():
        raise ValueError("模型输出包含非有限数值")
    data = output[0].T
    categories = data[:, 4:].argmax(axis=1)
    scores = data[np.arange(len(data)), categories+4]
    good = scores >= confidence
    centers, scores, categories = data[good, :4], scores[good], categories[good]
    boxes = np.column_stack((centers[:, :2]-centers[:, 2:]/2,
                             centers[:, :2]+centers[:, 2:]/2))
    return boxes, scores, categories

把坐标恢复到原图

vision_runtime.py文件末尾追加,紧接该文件上一个示例。

def decode(output, info, names, confidence=0.25, iou=0.45):
    boxes, scores, categories = candidates(output, len(names), confidence)
    chosen = []
    for category in np.unique(categories):
        indexes = np.where(categories == category)[0]
        chosen.extend(indexes[nms(boxes[indexes], scores[indexes], iou)].tolist())
    chosen = sorted(chosen, key=lambda i: scores[i], reverse=True)[:300]
    ratio, left, top, width, height = info
    result = []
    for i in chosen:
        box = (boxes[i]-np.array([left, top, left, top]))/ratio
        box[[0, 2]] = np.clip(box[[0, 2]], 0, width)
        box[[1, 3]] = np.clip(box[[1, 3]], 0, height)
        if box[2] <= box[0] or box[3] <= box[1]:
            continue
        category = int(categories[i])
        result.append({"class_id": category, "class_name": names[category],
                       "confidence": float(scores[i]), "box": box.tolist()})
    return result

加载固定接口的运行库会话

vision_runtime.py文件末尾追加,紧接该文件上一个示例。

def create_session(model_path, size=640, threads=4):
    import onnxruntime as ort
    options = ort.SessionOptions()
    options.intra_op_num_threads = threads
    options.inter_op_num_threads = 1
    session = ort.InferenceSession(str(model_path), sess_options=options,
                                   providers=["CPUExecutionProvider"])
    inputs = session.get_inputs()
    if len(inputs) != 1 or inputs[0].type != "tensor(float)":
        raise ValueError("入口要求一个浮点图片输入")
    if inputs[0].shape != [1, 3, size, size] or len(session.get_outputs()) != 1:
        raise ValueError("模型的输入或输出接口与本教程不一致")
    return session, inputs[0].name

这些函数完整放在同一文件中。后面的量化读取器和推理入口共用它,避免不同环节采用不同预处理。NMS(非极大值抑制)负责去掉同类重复框,数学过程放在推理参考

创建测试文件并检查颜色与空结果

新建项目根目录的test_runtime.py

这是保留在项目中的逻辑测试文件,使用人工构造的数值,不是用模型识别准确率作断言。

import unittest
import numpy as np
from vision_runtime import preprocess, decode

class RuntimeTests(unittest.TestCase):
    def test_color_and_empty_output(self):
        image = np.full((601, 1000, 3), (10, 20, 30), dtype=np.uint8)
        tensor, info = preprocess(image)
        self.assertEqual(tensor.shape, (1, 3, 640, 640))
        np.testing.assert_allclose(tensor[0, :, 320, 320], [30/255, 20/255, 10/255])
        result = decode(np.zeros((1, 7, 1)), info, ["carton", "tote", "pallet"])
        self.assertEqual(result, [])

检查坐标恢复和重复框处理

test_runtime.py文件末尾追加,紧接该文件上一个示例。本段第一个方法仍在上面的测试类内部,保留四个空格缩进;最后的入口判断回到行首。

    def test_coordinates_and_duplicate_boxes(self):
        _, info = preprocess(np.zeros((600, 1000, 3), dtype=np.uint8))
        ratio, left, top, _, _ = info
        candidate = [300*ratio+left, 270*ratio+top, 400*ratio, 300*ratio, .9, .1, .01]
        output = np.repeat(np.array(candidate, dtype=np.float32)[None, :, None], 2, axis=2)
        result = decode(output, info, ["carton", "tote", "pallet"])
        self.assertEqual(len(result), 1)
        np.testing.assert_allclose(result[0]["box"], [100, 120, 500, 420], atol=0.001)

if __name__ == "__main__":
    unittest.main()
python test_runtime.py

应通过两个测试。失败先修正拼接、缩进或辅助函数;即使通过,也还没有验证任何真实模型。

接收模型、图片和结果路径

新建项目根目录的run_detector.py

# 依赖安装:python -m pip install -r requirements.txt
# 运行示例:python run_detector.py --input /absolute/path/image.png --output result.json
import argparse
import hashlib
import json
import time
from pathlib import Path
import cv2
from vision_runtime import read_image, preprocess, decode, create_session

ROOT = Path(__file__).resolve().parent
parser = argparse.ArgumentParser()
parser.add_argument("--model", default=str(ROOT / "model.onnx"))
parser.add_argument("--config", default=str(ROOT / "runtime.json"))
parser.add_argument("--input", required=True)
parser.add_argument("--output", required=True)
parser.add_argument("--visual")
parser.add_argument("--repeat", type=int, default=1)
parser.add_argument("--threads", type=int, default=4)
args = parser.parse_args()
if args.repeat < 1 or args.threads < 1:
    parser.error("重复次数和线程数必须大于0")

预热模型并执行图片识别

run_detector.py文件末尾追加,紧接该文件上一个示例。

settings = json.loads(Path(args.config).read_text(encoding="utf-8"))
image = read_image(args.input)
session, input_name = create_session(args.model, settings["imgsz"], args.threads)

def detect_once():
    tensor, info = preprocess(image, settings["imgsz"])
    outputs = session.run(None, {input_name: tensor})
    return decode(outputs[0], info, settings["names"], settings["confidence"], settings["nms_iou"])

for _ in range(2):
    detect_once()
seconds = []
for _ in range(args.repeat):
    started = time.perf_counter()
    detections = detect_once()
    seconds.append(time.perf_counter()-started)
mean_seconds = sum(seconds)/len(seconds)

写出结果文件和预览图

run_detector.py文件末尾追加,紧接该文件上一个示例。

record = {"model": str(Path(args.model).resolve()),
          "model_sha256": hashlib.sha256(Path(args.model).read_bytes()).hexdigest(),
          "input": str(Path(args.input).resolve()), "settings": settings,
          "threads": args.threads, "repeat": args.repeat, "mean_seconds": mean_seconds,
          "images_per_second": 1/mean_seconds, "detections": detections}
destination = Path(args.output)
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding="utf-8")
if args.visual:
    canvas = image.copy()
    for item in detections:
        x1, y1, x2, y2 = map(lambda x: int(round(x)), item["box"])
        cv2.rectangle(canvas, (x1, y1), (x2, y2), (0, 255, 0), 2)
        cv2.putText(canvas, item["class_name"], (x1, max(y1-5, 15)),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
    visual = Path(args.visual)
    visual.parent.mkdir(parents=True, exist_ok=True)
    ok, encoded = cv2.imencode(".png", canvas)
    if not ok:
        raise RuntimeError("预览图编码失败")
    encoded.tofile(str(visual))
print(json.dumps(record, ensure_ascii=False))
python -m py_compile vision_runtime.py test_runtime.py run_detector.py
python run_detector.py --help

完成检查:辅助函数和测试均已写完,入口能显示参数帮助。--help(帮助)和逻辑测试都不等于模型加载成功;下一阶段必须运行导出模型。

9. 导出浮点模型并实际运行入口

目标:保留浮点导出基线,检查格式转换及自写后处理。

操作位置:训练服务器项目根目录。

先修改:源模型是同一份最佳权重;固定输入640、批量1、算子规范17、不开动态尺寸和额外简化。

参数与原理见对应参考。下面按顺序操作。

准备浮点模型输出路径

新建项目根目录的export_detector.py

from pathlib import Path
import shutil
import json
import onnx
from ultralytics import YOLO

ROOT = Path(__file__).resolve().parent
OUT = ROOT / "exports"
OUT.mkdir(exist_ok=True)
weight = ROOT / "runs/warehouse_v1/weights/best.pt"
model = YOLO(str(weight))
target = OUT / "warehouse_fp32.onnx"
if target.exists():
    raise ValueError("导出目标已存在,请给新实验使用新位置")

导出模型并保存运行配置

export_detector.py文件末尾追加,紧接该文件上一个示例。

exported = model.export(format="onnx", imgsz=640, batch=1, dynamic=False,
                        simplify=False, opset=17, half=False, device="cpu")
shutil.copy2(exported, target)
onnx.checker.check_model(str(target))
names = [model.names[i] for i in range(len(model.names))]
settings = {"names": names, "imgsz": 640, "confidence": 0.25, "nms_iou": 0.45,
            "layout": "NCHW", "color": "RGB", "scale": "divide_by_255",
            "padding": 114, "output": "batch_channels_candidates"}
(OUT / "runtime.json").write_text(json.dumps(settings, indent=2), encoding="utf-8")
print("浮点模型已导出并通过结构检查:", target)
python export_detector.py
python evaluate_detector.py --model exports/warehouse_fp32.onnx --name exported_fp32

将下面的验证图片名换成实际存在的文件。浮点模型与后面的量化模型使用同一张图片、相同配置和重复次数。

python run_detector.py --model exports/warehouse_fp32.onnx --config exports/runtime.json --input dataset/images/val/session04__0001.png --output reports/fp32_run.json --visual reports/fp32_view.png --repeat 10

完成检查:exports/warehouse_fp32.onnx(浮点模型)通过结构检查,实际入口生成结果和预览图;比较baselineexported_fp32(浮点导出报告)的指标,确认没有无法解释的退化,再继续量化。还要换无目标、多目标和边缘目标图片检查。

10. 使用训练图片量化并比较变化

目标:生成真实量化模型,对比质量、体积及同条件运行延迟。

操作位置:训练服务器项目根目录。

先修改:新建calibration_files.txt(校准清单),每行填写一张真实训练图片相对项目的路径。主线使用有代表性的原始训练图片,不用验证集调校准范围。

参数与原理见对应参考。下面按顺序操作。

下列是清单格式示例,不是声称两张图片就足够;路径必须替换为实际存在的图片。小项目先覆盖几十到一两百张不同场景图片,数量有限时使用可用训练图并记录局限,范围解释见参考部分。

dataset/images/train/session01__0001.png
dataset/images/train/session02__0007.png

检查校准清单的来源

新建项目根目录的quantize_detector.py

import json
from pathlib import Path
import onnx
import onnxruntime as ort
from onnxruntime.quantization import CalibrationDataReader, CalibrationMethod
from onnxruntime.quantization import quantize_static, QuantFormat, QuantType
from onnxruntime.quantization.shape_inference import quant_pre_process
from vision_runtime import read_image, preprocess

ROOT = Path(__file__).resolve().parent
OUT = ROOT / "exports"
train_root = (ROOT / "dataset/images/train").resolve()
lines = (ROOT / "calibration_files.txt").read_text(encoding="utf-8-sig").splitlines()
paths = [(ROOT / line.strip()).resolve() for line in lines if line.strip()]
if not paths or len(set(paths)) != len(paths):
    raise ValueError("校准清单为空或含重复图片")
if any(not p.is_file() or not p.is_relative_to(train_root) for p in paths):
    raise ValueError("校准图片必须存在并来自本项目训练集")

准备模型并逐张读取校准图片

quantize_detector.py文件末尾追加,紧接该文件上一个示例。

prepared = OUT / "warehouse_prepared.onnx"
target = OUT / "warehouse_int8.onnx"
if prepared.exists() or target.exists():
    raise ValueError("量化输出已存在,请先归档或换用新的实验目录")
quant_pre_process(str(OUT / "warehouse_fp32.onnx"), str(prepared), skip_optimization=False)
session = ort.InferenceSession(str(prepared), providers=["CPUExecutionProvider"])
input_name = session.get_inputs()[0].name

class ImageReader(CalibrationDataReader):
    def __init__(self, images):
        self.images = list(images)
        self.rewind()

    def get_next(self):
        path = next(self.iterator, None)
        if path is None:
            return None
        tensor, _ = preprocess(read_image(path), 640)
        return {input_name: tensor}

    def rewind(self):
        self.iterator = iter(self.images)

量化支持的节点并保存记录

quantize_detector.py文件末尾追加,紧接该文件上一个示例。

quantize_static(
    str(prepared), str(target), ImageReader(paths),
    quant_format=QuantFormat.QDQ,
    activation_type=QuantType.QInt8, weight_type=QuantType.QInt8,
    per_channel=True, calibrate_method=CalibrationMethod.MinMax,
    op_types_to_quantize=["Conv", "MatMul"],
)
onnx.checker.check_model(str(target))
graph = onnx.load(str(target)).graph
types = {node.op_type for node in graph.node}
if "QuantizeLinear" not in types or "DequantizeLinear" not in types:
    raise ValueError("没有发现预期的量化节点,请检查是否真正执行量化")
record = {"format": "QDQ", "activation": "int8", "weight": "int8", "per_channel": True,
          "calibration": "MinMax", "images": [str(p.relative_to(ROOT)) for p in paths],
          "before_bytes": (OUT / "warehouse_fp32.onnx").stat().st_size,
          "after_bytes": target.stat().st_size}
(OUT / "quantization_record.json").write_text(json.dumps(record, indent=2), encoding="utf-8")
python quantize_detector.py
python evaluate_detector.py --model exports/warehouse_int8.onnx --name quantized_int8
python run_detector.py --model exports/warehouse_int8.onnx --config exports/runtime.json --input dataset/images/val/session04__0001.png --output reports/int8_run.json --visual reports/int8_view.png --repeat 10

确认前后测试使用相同条件

新建项目根目录的compare_runs.py

import json
from pathlib import Path
import pandas as pd

ROOT = Path(__file__).resolve().parent
def read(relative):
    return json.loads((ROOT / relative).read_text(encoding="utf-8"))

before = read("reports/fp32_run.json")
after = read("reports/int8_run.json")
for key in ("input", "settings", "threads", "repeat"):
    if before[key] != after[key]:
        raise ValueError(f"速度比较条件不同:{key}")

合并真实精度、延迟和体积记录

compare_runs.py文件末尾追加,紧接该文件上一个示例。

rows = []
for name, run, summary_file in [
    ("fp32", before, "reports/exported_fp32/summary.json"),
    ("int8", after, "reports/quantized_int8/summary.json"),
]:
    summary = read(summary_file)
    if summary["model_sha256"] != run["model_sha256"]:
        raise ValueError("速度记录与精度记录不是同一份模型,请重新测量")
    rows.append({"model": name, "mAP50": summary["mAP50"],
                 "mAP50_95": summary["mAP50_95"], "mean_seconds": run["mean_seconds"],
                 "images_per_second": run["images_per_second"],
                 "bytes": Path(run["model"]).stat().st_size})
pd.DataFrame(rows).to_csv(ROOT / "reports/compression_comparison.csv", index=False, encoding="utf-8-sig")
python compare_runs.py

完成检查:同时保留原权重、浮点模型和八位模型;查看quantization_record.json(量化记录)与reports/compression_comparison.csv(前后对比)。图结构确有量化节点,两份模型都能加载,质量和资源收益才有比较意义。变慢或退化也如实记录,不能预写‘更快、更准’。

11. 封装运行目录并从其他位置复测

目标:把实际运行依赖收齐,使入口离开训练目录后仍可使用。

操作位置:先在训练服务器项目根目录打包,再切到临时目录调用。

先修改:主线准备部署量化模型;必须先审阅前后结果、确认满足业务需要。若改用浮点模型,按参考部分同步更换模型和两份报告。

参数与原理见对应参考。下面按顺序操作。

核对模型与评估记录的内容摘要

新建项目根目录的package_detector.py

import hashlib
import importlib.metadata
import json
import platform
import shutil
from pathlib import Path

ROOT = Path(__file__).resolve().parent
source = ROOT / "exports/warehouse_int8.onnx"
summary = json.loads((ROOT / "reports/quantized_int8/summary.json").read_text(encoding="utf-8"))
inference = json.loads((ROOT / "reports/int8_run.json").read_text(encoding="utf-8"))
digest = hashlib.sha256(source.read_bytes()).hexdigest()
if digest != summary["model_sha256"] or digest != inference["model_sha256"]:
    raise ValueError("模型与最近评估、独立推理记录不一致,请先重新验证")
config = json.loads((ROOT / "exports/runtime.json").read_text(encoding="utf-8"))
if config != inference["settings"]:
    raise ValueError("运行配置与最近推理记录不同")
output = ROOT / "deployment"
output.mkdir(exist_ok=True)
if any(output.iterdir()):
    raise ValueError("部署目录不为空,请先归档旧版本")

复制模型、配置、入口和测试依赖

package_detector.py文件末尾追加,紧接该文件上一个示例。

shutil.copy2(source, output / "model.onnx")
shutil.copy2(ROOT / "exports/runtime.json", output / "runtime.json")
for name in ("run_detector.py", "vision_runtime.py", "test_runtime.py"):
    shutil.copy2(ROOT / name, output / name)
packages = ["numpy", "opencv-python", "onnxruntime"]
requirements = [name + "==" + importlib.metadata.version(name) for name in packages]
(output / "requirements.txt").write_text("\n".join(requirements)+"\n", encoding="utf-8")

生成可复现的运行说明

package_detector.py文件末尾追加,紧接该文件上一个示例。

readme = f"""# 仓储容器识别运行说明

解释器:Python(编程语言){platform.python_version()}。
本次记录系统:{platform.platform()}。
推理后端:处理器;不自动假设目标机具有显卡运行能力。
依赖安装:python -m pip install -r requirements.txt
逻辑测试:python test_runtime.py(使用合成输入,不衡量识别精度)
运行示例:python run_detector.py --input /absolute/path/image.png --output result.json --visual preview.png
模型与配置默认从脚本所在目录读取,输入和输出使用调用者给定的路径。
类别、尺寸、置信度和去重阈值见runtime.json,必须与已验证版本保持一致。
输出包含原图像素坐标、类别、置信度和计时记录;无目标返回空列表。
计时不包括加载模型、读图和写文件;包含预处理、推理与后处理。
模型摘要:{digest}
部署前请补充:实际业务验收结论、允许的精度变化和目标硬件复测结果。
"""
(output / "README.md").write_text(readme, encoding="utf-8")
print("已封装,请测试部署副本:", output)
python package_detector.py
cd /tmp
python /home/user/projects/warehouse_vision/deployment/test_runtime.py
python /home/user/projects/warehouse_vision/deployment/run_detector.py --input /home/user/projects/warehouse_vision/dataset/images/val/session04__0001.png --output /home/user/projects/warehouse_vision/reports/deployment_run.json --visual /home/user/projects/warehouse_vision/reports/deployment_view.png
cd /home/user/projects/warehouse_vision

运行目录应含model.onnx(模型)、runtime.json(配置)、run_detector.py(入口)、vision_runtime.py(辅助函数)、test_runtime.py(逻辑测试)、requirements.txt(依赖清单)和README.md(使用说明)。不要只拿走模型,把辅助文件留在服务器上。

完成检查:从其他工作目录运行也能找到模型和配置;打开实际结果、查看预览图,再用一张新图片试运行。使用说明中补充真实验收结论和运行硬件,不用占位说明代替测试。

12. 归档、传输并在接收端重新验证

目标:确认文件传输完整,而且目标环境能实际推理。

操作位置:源服务器生成内容摘要,本地传输客户端搬运,目标服务器独立安装与执行。

先修改:接收目录、目标端解释器和输入图片换成真实信息;保留主线的同一套类别、输入尺寸与后处理配置。

参数与原理见对应参考。下面按顺序操作。

cd /home/user/projects/warehouse_vision/deployment
sha256sum model.onnx runtime.json run_detector.py vision_runtime.py test_runtime.py requirements.txt README.md > ../deployment_checksums.txt

将整个运行目录传到接收端,并把摘要清单放在其上一级。训练图片、日志、模型优化过程另外归档,方法见分阶段归档。目标服务器首次准备独立环境:

conda create -n warehouse_infer python=3.10 -y
conda activate warehouse_infer
cd /实际接收目录/deployment
sha256sum -c ../deployment_checksums.txt
python -m pip install -r requirements.txt
python -m pip check
python test_runtime.py
python run_detector.py --input /实际测试图片目录/new_image.png --output verification.json --visual verification.png

目标系统版本可以不同,但必须支持记录下来的依赖与模型算子。安装失败不要直接升级全套库;先查不兼容项,必要时建立新版本配置并重新验收。备份服务若无法运行程序,只能验证文件备份完整,运行验证还要在具备计算环境的机器上做。

完成检查:摘要全部一致,逻辑测试通过,真实图片生成可读结果及正确预览;现场记录接收端实际版本、硬件、运行命令和观察结果。这才完成从数据到迁移复测的一条链路。

第二部分:参数、工具和可选做法参考

环境、版本和安装分支

返回建立环境。Environment(运行环境)是解释器与依赖的组合,激活环境相当于选择这一套工具。Terminal(终端)接受命令;Editor(编辑器)用于写文件;它们不是训练模型本身。

主线约定Python(编程语言)3.10、Labelme(标注工具)5.6.1、Ultralytics(目标检测框架)8.3.40、PyTorch(训练框架)2.5.1和torchvision(视觉扩展)0.20.1。导出使用ONNX(开放神经网络交换格式)库1.17.0,加载与量化使用ONNX Runtime(模型运行库)1.20.1。这里核对的是接口与源码版本,并没有验证这整套组合在所有操作系统和显卡上都可用。

Conda(环境管理工具)未安装时,从官方安装说明选择对应系统,安装后重新打开终端。桌面安装提供的环境管理终端通常已初始化;若PowerShell(命令终端)不能激活环境,在已能执行环境管理命令的终端中运行下面命令,再关闭并重新打开PowerShell 7:

conda init powershell

不要在没有环境管理命令的窗口中反复执行它。重新连接远程机器后,环境和当前位置也要重新确认。

工具与参数 中文含义 工具行为或默认 主线值 常见选择、影响及限制
Conda:-n 环境名称 新建时需给名称或路径 标注、训练、推理分别建环境 名称自定;激活错误环境会把库装到另一位置
Conda:python=3.10 解释器版本约束 不约束时由求解器选择 3.10 版本须被所选依赖支持;不能根据编辑器显示名称推断实际解释器
pip(包安装工具):== 精确版本约束 不写时不固定版本 主线显式固定关键库 固定接口便于复现,仍需检查传递依赖;不等于跨平台二进制兼容
pip:--index-url 软件包索引地址 来自安装工具配置 训练框架使用官方cu121索引 CPU(处理器)、cu118、cu121、cu124等构建按硬件选;镜像地址和计算构建不是同一概念
SSH(安全远程连接):-p 服务端口 通常22,可被服务器修改 真实端口占位 与账号、地址一起向环境提供方确认,不能猜测服务端开放情况
Python:-m pip 用当前解释器调用安装器 无自动选择其他环境的保证 始终使用当前解释器 比单独调用安装命令更容易核对归属;检查解释器路径
Python:-u 无缓冲输出 通常存在缓冲 训练时开启 有助于及时看到日志,不会加速训练

主线的CUDA(显卡计算平台)12.1构建来自训练框架2.5.1安装说明。驱动工具显示的CUDA版本不是当前解释器中已安装的训练框架构建;两者分别检查。硬件不兼容时按官方矩阵选构建,不能只看一个版本数字就宣布可用。

如果只想在没有显卡的机器上先检查少量数据流程,可在另一个新环境使用CPU构建,并把训练参数device(设备)改成"cpu"(处理器):

python -m pip install "torch==2.5.1" "torchvision==0.20.1" --index-url https://download.pytorch.org/whl/cpu

这是替代安装命令,不是在显卡主线后追加的命令。训练速度可能很慢,不能把处理器检查误写成显卡性能结果。非NVIDIA(显卡厂商)设备需要对应硬件的软件栈,本例不提供通用安装保证。

编辑器、终端和代码的基本用法

返回保存与运行代码。File(文件)存代码,Folder(文件夹)整理多个文件,Path(路径)表示文件位置。Absolute Path(绝对路径)从磁盘或系统根目录开始;Relative Path(相对路径)从指定位置开始。本文脚本以自身位置确定项目根目录,但终端中的短命令仍需要先进入项目目录。

使用PyCharm(代码编辑器)时:打开项目文件夹,在项目区右键新建Python文件,确认名称没有重复扩展名;在解释器设置中选择前面创建环境中的解释器;打开终端,激活同一环境,再执行文中的命令。如果点击绿色运行按钮,要额外核对解释器、工作目录和命令行参数,初学阶段采用文中的终端方式更便于对照。

工具操作 具体动作 完成检查与限制
本地创建脚本 编辑器新建指定文件,粘贴当前示例,按UTF-8(统一字符编码)保存 文件名是.py而非.py.txt,内容不是富文本
服务器编辑 执行nano 文件名.py,按顺序追加同名示例 新函数从行首开始;函数体通常缩进四个空格
Nano(文本编辑器)保存 Ctrl+O(写入文件)后按回车 确认底部显示的文件名
Nano退出 Ctrl+X(退出);有未保存提示先保存 不把退出当作自动保存
检查语法 python -m py_compile 文件名.py 只验证能否解析,不检查路径、依赖、指标和模型正确性
运行脚本 python 文件名.py 错误信息要保留最后一段与执行命令,不只看窗口是否关闭
显示参数 python run_detector.py --help 能看帮助不代表加载过模型
查看所在目录 本地执行Get-Location;服务器执行pwd 目录改变只影响当前终端,不会同时改变另一台机器

import(导入)让代码使用其他库;def(定义函数)把可重复动作取名;for(循环)依次处理对象;if(条件判断)决定是否执行;return(返回)交回结果。它们是程序语法,不需要翻译后再写入代码。class(类)把相关方法组织在一起,量化读取器和测试文件都用到了它。True(真)、False(假)、None(无值)有固定大小写。

文中的ROOT(根目录变量)是自定义变量,通常无需修改;调整根目录时移动整个项目比逐行替换路径更可靠。修改脚本变量用编辑器;修改入口参数用终端中的--参数名。这两种操作不要混淆。

数据、类别与采集组参数

返回清理图片构建数据集。Training Set(训练集)用于更新模型;Validation Set(验证集)用于观察效果和调参;Test Set(测试集)应保留到最终独立验收,不能反复用于挑参数。本例构建训练与验证两份数据,实际业务可另外封存测试集。

文件数量不能代表独立样本数量。同一段视频的相邻帧以及同一原图的增强副本彼此相似;拆进不同数据分区会造成Data Leakage(数据泄漏)。先按采集组划分,再只增强训练集。验证集需要覆盖每类目标;若还计算图像级曲线,每类也需要不含该类的负例。

所属脚本或配置 参数与中文含义 默认性质 主线值 常见选择、调整条件、影响及关联限制
图片整理脚本 rawcuratedreview,原件、整理、复查目录 自定义,无工具默认 项目内同名目录 改名须同步脚本与标注界面;不覆盖已有整理目录
图片整理脚本 文件名中的__,采集组分隔符 自定义规则 session01__0001 组名自定;同组图片必须始终使用同一前缀
图片整理脚本 像素摘要,精确去重依据 自定义实现 方向修正后的RGB(红绿蓝)像素和尺寸 可以发现精确重复;压缩差异、裁剪和近似图片要另行人工或算法复查
图片整理脚本 sharpness,清晰度参考数值 自定义记录,无删除阈值 拉普拉斯方差 图片分辨率、纹理会影响数值;没有全场景通用合格线
数据构建脚本 NAMES,类别顺序 自定义,无框架自动推断 三类固定顺序 改类别须同步评估常量、标签和测试样例;运行配置随后由真实模型导出
数据构建脚本 VAL_GROUPS,验证采集组 自定义,无默认比例 两个示例组名 可先按组近似留出20%左右,再检查覆盖;20%是尝试值,不是规定或最优值
数据配置 path,数据根目录 由配置提供 当前数据集绝对路径 搬数据后更新,错误路径会指向旧数据或找不到文件
数据配置 trainval,图片子目录 由配置提供 images/trainimages/val 相对数据根目录;标签放在匹配的标签目录
数据配置 names,编号到类别名称 由配置提供 编号0、1、2 编号连续且从0开始;本例由名称映射确定三类,不另写冲突的类别数
标签转换 shape_type,图形类型 本文转换器约束 rectangle(矩形) 不支持直接把多边形输入当矩形;换类型要明确转换规则
标签转换 imagePath,标注对应图片 来自标注文件 同目录的图片名 换目录后应重新检查引用,不能只改图片尺寸字段掩盖错配

本例预处理把图片保存为PNG(无损图片格式),避免反复有损编码,但文件可能变大。规则不是“所有任务必须转成这一格式”。参考检测数据格式说明

矩形坐标怎样转换

返回转换训练标签。Bounding Box(边界框)是目标的外接矩形。标注工具保存两个角点,YOLO(目标检测模型系列)标签用一行表达一个目标:类别编号 中心横坐标 中心纵坐标 宽度 高度。后四项分别除以原图宽或高,变成0到1之间的比例。

例如宽1000、高600,左上角(100, 120)、右下角(500, 420)。中心是(300, 270),宽400、高300,因此类别0的标签是:

0 0.300000 0.450000 0.400000 0.500000

03_坐标转换示意

一个对象一行,多个对象多行;已确认无目标的图片对应空标签文件。归一化与缩小图片不是同一动作,不能将所有数值都除以640,也不能把左上角直接当中心。图片旋转或裁剪后,旧坐标必须同步变换。

标注工具菜单、快捷键和配置

返回画框实践。以下按Labelme(标注工具)5.6.1的界面源码默认配置核对。用户配置可覆盖默认,面板可拖动,图示不保证与你的布局相同。

所属配置 中文含义 5.6.1默认值 主线值 可选值、影响与注意事项
auto_save 自动保存 false(否) 可开启;主线先练习手动保存并检查实际文件
store_data 标注内嵌图片 true(是) 开启会把图片编码写进标注;关闭时原图片必须一起传输
keep_prev 保留上一张标注 false(否) 连续帧可尝试开启,但必须逐框调整,不能把上一帧当新答案
display_label_popup 弹出标签选择 true(是) 保持开启 弹窗或已选类别的实际行为以窗口为准,完成后检查标签
labels 预设类别列表 null(未设置) 手动输入三类 可以在配置中预设列表;仅预设名称不代表全部图片已标注
输出目录 标注保存位置 与界面状态、打开方式有关 与整理图片同目录 每次新项目核对;改位置后要一起检查图片相对引用
动作 菜单或默认快捷键 怎样操作及限制
打开目录 Open Dir(打开目录),Ctrl+U 选图片目录,文件列表出现图片;不是选择单个标注文件
打开单个文件 Open(打开),Ctrl+O 可打开图片或标注;重开用于验证保存结果
创建矩形 Create Rectangle(创建矩形),Ctrl+R 两次单击两个角点;不是拖住鼠标任意涂抹
编辑图形 Edit Polygons(编辑图形),Ctrl+J 拖角点改大小,拖框内改位置
编辑标签 Edit Label(编辑标签),Ctrl+E 先选目标条目,再改类别
删除图形 Delete Polygons(删除图形),Delete 只针对选中图形;删除整份标注是另一动作
保存 Save(保存),Ctrl+S 检查同名标注存在,再切换图片
另存为 Save As(另存为),Ctrl+Shift+S 核对目录和文件名,避免一张图存成另一张图的标注
下一张/上一张 Next Image(下一张)/Prev Image(上一张),D/A 按键需要画布处于适合接收快捷键的状态;不要在文本框中误输入
放大/缩小 Zoom In(放大)/Zoom Out(缩小),Ctrl++/Ctrl+- 可用菜单按钮,键盘布局不同优先按菜单
恢复整图 Fit Window(适应窗口),Ctrl+F 放大查边缘后再看整图找漏标
撤销 Undo(撤销)或Undo Last Point(撤销上一点),Ctrl+Z 绘制和编辑状态对应不同动作;不是所有文件操作都能撤销
复用上张标注 Keep Previous Annotation(保留上一张标注),Ctrl+P 主线关闭;带Ctrl+Shift的切图组合可能复用标注,勿与普通切图混用

关闭面板后,在View(视图)菜单查找对应面板并重新显示。类别拼错时统一修正原始标注,再重建训练标签;不要只改训练目录中的一份文本,留下两套不一致答案。

离线增强与在线增强的区别

返回保存增强副本。Offline Augmentation(离线增强)先写出新图片和标签,便于人工检查;Online Augmentation(在线增强)在训练读取时随机变化,不必预存所有副本。两者都需要同步处理框,验证集通常不做随机增强。

主线只保存一次离线增强副本,并在训练调用中显式把随机颜色、平移、缩放、翻转、拼图和混合关闭。它仍会执行必要的模型输入缩放与填充,不能因此称为“图片完全不经过变换”。

所属函数或工具 参数与中文含义 工具默认 / 主线示例 可尝试值与条件 影响及关联限制
自定义增强脚本 随机种子 无工具默认 / 42 对照实验保留同一值 固定一次生成过程;不保证不同软件版本逐位一致
自定义增强脚本 水平翻转概率 无工具默认 / 0.5 0、0.25、0.5是尝试点 方向有含义时关闭;翻转后中心横坐标变成1减原值
Pillow(图像库)亮度增强 亮度因子 调用需给值 / 随机0.85—1.15 先小幅比较,如0.9—1.1 小于1变暗、大于1变亮;过强可能失去信息;框不变
Ultralytics训练增强 hsv_h/s/v,色相/饱和度/明度幅度 0.015/0.7/0.4 / 主线全0 在线替代实验可试0.01/0.3/0.25 属于变化幅度,不是变换概率;颜色决定类别时谨慎
同上 degrees,随机旋转角度范围 0 / 0 可从±5度范围试起 参数填5表示正负幅度;检查旋转后的外接框与裁切
同上 translate,相对平移幅度 0.1 / 0 在线替代可试0.05 不是5个像素;可能裁掉边缘目标
同上 scale,随机缩放幅度 0.5 / 0 在线替代可试0.2 0.2近似对应0.8—1.2倍,不是缩成0.2倍
同上 fliplr,左右翻转概率 0.5 / 0 在线替代0或0.5 主线已有离线翻转,不再叠加
同上 flipud,上下翻转概率 0 / 0 仅业务方向允许时尝试 固定相机通常不需要倒置图片
同上 mosaic,拼图概率 1.0 / 0 新实验可比较0、0.5、1 拼接改变尺度与背景组合;小目标可能受益也可能消失
同上 mixup,图片混合概率 0 / 0 有基准后才试小值 混合画面不一定符合业务;不能无依据宣称有效
同上 close_mosaic,末尾关闭拼图轮数 10 / 未覆盖,沿用10 0或若干轮 主线拼图概率为0,所以这项不产生额外拼图变化

这些训练默认来自8.3.40配置,操作含义对应该版本增强实现。表中的尝试点是教学候选,不是工具承诺的有效范围或最优数值。其他未覆盖的训练增强按该版本默认执行;主线适用于普通矩形检测,分割和分类专属增强不能直接套用。

若改用在线增强:重新构建一份没有离线副本的数据目录,跳过主线增强脚本,另起实验名称,再在训练调用中替换需要的增强参数。不要删除当前实验的增强记录,也不要把新结果与旧记录混用。

训练参数:含义、默认与调整依据

返回训练实践。Epoch(训练轮)表示遍历一次训练数据;Batch(批量)是一次前向与更新过程使用的一组图片;Learning Rate(学习率)影响参数更新步幅;Loss(损失)衡量当前预测与答案的差异。权重经过更新不代表能处理未见过的图片,因此还需要独立验证。

下表属于YOLO.train(训练函数),默认值按8.3.40的配置及调用行为核对。配置默认不保证等于最终实际值:优化器自动模式、权重衰减缩放和恢复训练都可能改变有效设置,运行后以实际参数和日志为准。

参数与中文含义 工具默认 主线示例值 常见选择或尝试点 调整条件、影响与关联限制
data,数据配置 未指定 项目数据配置路径 自己的配置文件 改路径要确认指向本轮数据;不能仍读旧划分
imgsz,输入尺寸 640 640 512、640、960等尝试点,注意步长对齐 更大可能改善小目标,也增加显存;导出、校准、测试和推理须同步
batch,每批图片数 16 8 1、2、4、8、16;-1为框架自动估算 显存不足优先降低;自动估算支持取决于设备与模式
epochs,训练轮数 100 40 先检查少量轮,再比较20、40等 看验证趋势;提前停止可能使实际轮数不足设定值
device,计算设备 未指定,由环境选择 0 "cpu"、0、其他可用编号 编号只在当前可见设备中有效;换设备后速度不能直接归因模型变化
workers,数据加载进程数 8 2 0、2、4、8 多进程异常先试0;更多进程不保证更快
optimizer,优化器 auto(自动) SGD(随机梯度下降) SGD、AdamW(解耦权重衰减的自适应优化器)、auto 自动模式可能覆盖学习率和动量选择;本例显式固定便于理解
lr0,初始学习率 0.01 0.005 同一优化器下比较0.001、0.005、0.01 不稳定可尝试降低;切换优化器时不能机械照搬尺度
lrf,末端学习率比例 0.01 未覆盖,沿用0.01 随训练策略对照调整 末端目标与初始学习率相乘;不是独立绝对学习率
momentum,动量 0.937 0.9 SGD下可围绕0.9附近试验 利用历史方向;不同优化器含义不同,不与学习率同时大改
weight_decay,权重衰减 0.0005 0.0005 如0.0001、0.0005、0.001 约束参数,不能代替修复数据泄漏;框架会结合批量和累积缩放实际衰减
warmup_epochs,预热轮数 3.0 未覆盖,沿用3.0 0或若干轮 预热阶段参数与稳定阶段不同;一轮试跑不足以评价最终收敛
patience,提前停止耐心 100 10 10、20等,依据波动与时间 连续多轮框架适应度无改善才停止,不能理解为固定少训10轮
seed,随机种子 0 42 任何合适固定整数 对照实验保持一致,不能保证跨硬件完全一致
deterministic,确定性模式 True(是) 未覆盖,沿用True True或False(否) 可能影响性能或算子支持;不能单凭该值承诺完全复现
amp,自动混合精度 True False 硬件支持时比较开启 可能节省显存,需检查稳定性;训练混合精度与八位量化不同
freeze,冻结模块 None(不指定) None 整数模块数或编号列表 冻结减少可更新部分;框架另有专用层处理,None不代表每个参数都必然可训练
projectname,输出根目录和名称 未指定,由框架组织 项目实验根目录、固定实验名 新实验换名 需同步下游权重路径;本文脚本主动阻止已有同名目录
exist_ok,复用已有目录 False False 新实验通常保持False 框架可能递增名称;本文在调用前先检查目录以避免误读旧权重
saveplots,保存权重和图表 True、True True、True 保留所需记录 不保存会影响后续复现和评估,不要只保留终端截图
resume,恢复训练 False 未开启 从真实最后检查点恢复 恢复与重新加载权重开启新实验不同;见下面说明

以上事实以固定配置训练实现为准。常见尝试点只用于组织对照实验,不是保证适合所有数据的范围。

查看特征层、试跑、重训与恢复(可选)

Backbone(主干特征网络)提取图像特征,Head(检测头)输出位置与类别。冻结模块只是停止部分参数更新,不是重构网络。先查看实际编号,下面是临时观察示例:插在训练脚本的model.info()之后、model.train()之前,保留函数内缩进;看完可删除这两行。

    for index, layer in enumerate(model.model.model):
        print(index, type(layer).__name__)

将训练调用的freeze=None替换为freeze=2,表示冻结前两个编号模块;替换为freeze=[0, 1]表示明确选择编号。这些仅是参数形式示例,不能在没看网络时认定它们就是整个主干。数据少可比较有限冻结;业务与预训练场景差异大时过度冻结可能限制学习。

通道数、模块重复次数和特征图连接属于网络结构。如果要修改,复制与该权重对应的YAML(结构配置)另存,每次改一个地方,以新结构建立模型,再加载兼容权重并检查匹配日志、输出维度与小批运行。本文主线保留结构,避免把未知架构修改写成通用操作。

只做一轮流程检查时,临时把epochs改为1、实验名称改为warehouse_smoke(试运行);该结果不能用于判断训练充分。确认能扫描数据、训练和保存后,将两处值恢复成主线设置,再运行正式实验。更改新实验名称、输出目录或选择的检查点后,评估默认权重、导出脚本和后续记录路径必须一起检查。

恢复被中断的同一次训练,新建项目根目录的resume_training_demo.py(临时恢复示例),只写下面的内容,加载真实last.pt(最后检查点)并调用恢复接口;不要追加到主线训练脚本后重复启动。需要恢复时才在训练环境运行此文件,完成后可移出项目:

from ultralytics import YOLO
model = YOLO("/home/user/projects/warehouse_vision/runs/warehouse_v1/weights/last.pt")
model.train(resume=True)

恢复时框架会读取检查点参数;先确认原进程已停止,避免双重训练。新调参实验则另起名称、保留旧文件,不把恢复当作任意参数修改工具。

需要断线后继续训练时,可使用服务器已有的tmux(持久终端工具)。新建会话后激活环境、进入项目再运行训练;按Ctrl+B(命令前缀)后松开,再按D(分离会话)。重连后附着原会话,不重复启动:

tmux new -s warehouse_training
tmux attach -t warehouse_training

这两条分别用于新建和重连,不需要在新建会话后立即再附着。

验证参数、指标定义与报告解释

返回生成验证报告。IoU(交并比)等于两个框的交集面积除以并集面积,用于表示重叠程度。它在本文有两种用途:匹配预测与答案,以及删除重复预测。两处阈值不能混同。

TP(真正例)是正确命中;FP(假正例)是多报或错类带来的错误预测;FN(假负例)是未正确识别的真实对象。Precision(精确率)为TP除以TP加FP;Recall(召回率)为TP除以TP加FN。F1(调和平均)综合二者。主线遇到零分母按代码约定处理,并将约定写进报告。

AP(单类别平均精度)来自置信度变化下的检测曲线,mAP是跨类别平均。mAP50使用0.5匹配阈值;mAP50_95再对0.50到0.95、间隔0.05的匹配阈值平均。报告里一行一类的AP不是总体mAP,不能改列名冒充同一个指标。

所属函数 参数或约定 工具默认 主线值 调整条件、可选值与影响
model.val(框架验证) conf,候选置信度下限 验证模式通常0.001 0.001 计算曲线保留低分候选;提高下限会截断部分曲线,不是只改变显示
model.predict(预测) conf,返回候选下限 预测模式0.25 评估收集阶段0.001 业务入口另用0.25;不能直接用业务截断结果复算完整曲线
两者的iou 去重重叠阈值 0.7 0.45 可对照0.45、0.6、0.7;降低更容易删重复框,也可能删掉密集真实目标
两者的max_det 每图最多保留框数 300 300 密集场景要核对上限是否截断;提高会增加后处理与评估开销
框架验证 rect,矩形批次处理 配置为False,但检测验证调用可覆盖 显式False 为固定640正方形导出保持处理方式;以实际调用和日志为准
框架验证 batchdeviceworkers 随调用及环境 1、处理器、0 同格式对比保持一致;不能把设备差异归因量化
自定义add_confusion(匹配函数) 匹配交并比 函数默认0.5 0.5 是判断对应关系,不是去重阈值;改值须更新指标说明
自定义评估脚本 F1置信度 自定义,无工具默认 0.25 可比较0.1、0.25、0.5等业务阈值;固定值便于前后对比
自定义评估脚本 ROC统计单位 自定义,无检测通用默认 一张图、一个类别是否存在 每类需有正例和负例;不用于解释框定位质量

框架默认及版本行为见固定配置检测指标实现。本文混淆矩阵按几何重叠降序一对一匹配,行是真值、列是预测;背景列表示漏检,背景行表示多报,右下格不是背景真负例。它是本文明确写出的统计约定,可能与框架默认匹配细节不同。

图像级ROC对每张图片判断“是否含某类”,分数取该类返回候选的最高置信度,没候选为0;这个分数受候选下限、去重及框数上限影响。AUC(曲线下面积)缺少正例或负例时无法定义,主线留空并记录状态,不能填0代替。PR曲线则使用检测框架的曲线数据。定义说明见曲线函数

生成文件的含义:confusion_counts.csv(混淆计数)保存行列标签;class_metrics.csv(各类指标)保存单类AP、固定阈值F1及图像级AUC;summary.json(总体摘要)保存mAP及模型摘要;presence_roc.jpg(图像级曲线)与detection_pr.jpg(检测曲线)各有自己的定义;metric_definitions.txt(口径说明)应随报告保留。

如果新建报告目录已存在,先检查旧报告,另选名称后运行;比较与打包脚本引用的名称也要同步。不要只给文件改一个新名字就认为它来自新模型。

独立推理的预处理、输出与参数

返回编写入口和测试。图片读入后是BGR(蓝绿红)顺序;模型需要RGB(红绿蓝)。主线使用Letterbox(等比例缩放并填充),长边缩放到640附近,用114填充剩余区域,再除以255、改成NCHW(批量、通道、高、宽)。这是预处理协议,不是随意调节的美化参数。

模型输出前三维是[1, 4+类别数, 候选数]:前四项为中心横纵坐标和宽高,其后是类别分数。本例取每个候选的最高分类分数,再按类做NMS(非极大值抑制)。输出不是这种形式时应该停下来核对模型,不能盲目转置或额外乘一个不存在的目标分数。

所属函数或入口 参数与中文含义 默认性质 主线值 可选值、条件与关联限制
preprocess(预处理) size,输入边长 自定义函数默认640 640 改尺寸须重导出、重校准、重测;只改调用会触发固定输入错误
同上 填充值、颜色、比例 自定义协议 114、红绿蓝、除以255 不是随机可选项;必须与模型训练导出约定一致
decode(解析输出) confidence,置信度门槛 自定义默认0.25 配置0.25 更低通常增加召回和误报;修改后重新做业务验收
同上 iou,去重阈值 自定义默认0.45 配置0.45 同类过于密集时检查错误抑制;与验证匹配阈值不同
同上 最多输出框数 代码约定300 300 修改需同步其他统计过程,不能只改一处
create_session(创建运行会话) threads,算子内部线程数 自定义默认4;运行库未设置时另有调度策略 4 比较1、2、4等,受处理器核数和其他负载影响;不是越大越快
同上 算子之间线程数 主线显式设置 1 运行库默认顺序执行模式下,不可把它理解成同时跑多个图片
同上 providers,运行后端 不依赖自动选择 CPUExecutionProvider(处理器后端) 本文不自动回退到显卡;改后端需要匹配依赖与算子支持
命令行入口 --model,模型文件 自定义默认脚本旁的模型 导出阶段显式传路径,部署阶段用默认 相对路径按当前终端解析;默认路径按脚本位置解析
命令行入口 --config,运行配置 自定义默认脚本旁的配置 与当前模型配套 不要拿另一类别顺序或另一尺寸的配置
命令行入口 --input--output,输入图片与结果文件 必填,无自动默认 真实图片、指定结果文件 输出会写入指定文件;新实验换文件名保留旧结果
命令行入口 --visual,预览图路径 可选 明确指定PNG图片 编码固定PNG,文件后缀也用.png
命令行入口 --repeat,重复测量次数 自定义默认1 比较时10 必须大于0;可增加次数观察稳定性,但重复同图不是完整业务负载

主线先预热两次。计时包含预处理、模型运算、后处理,排除加载、读图和写盘;记录的每秒图片数只是同图小实验的倒数延迟,不代表摄像头系统总吞吐。实际服务应使用有代表性的图片集合记录延迟分布。

配置中的names(类别表)、imgsz(尺寸)、confidence(置信度)和nms_iou(去重阈值)被入口读取;其余预处理说明字段主要用于记录协议,并不会自动改写辅助函数实现。改变颜色或布局时要同时改实现并重新验证,不能只改配置文字。

返回结果的box(边界框)是原图像素的左上角与右下角,confidence(置信度)是模型分数,不保证是经过校准的真实概率;class_id(类别编号)与class_name(类别名)必须一致。没有目标返回空列表,是一种合法结果。框架验证可能使用多标签候选,因此主线还要求检查独立入口的真实图片结果,不能仅凭框架mAP证明自写入口正确。

导出参数与模型接口限制

返回浮点导出。Export(导出)改变保存和运行形式;Quantization(量化)改变部分计算的数值表达,二者不是同一动作。本文工具链先导出FP32(三十二位浮点)模型,再在这个格式上量化。

下表属于该版本的model.export(导出函数);核对配置导出实现

参数与中文含义 工具默认或行为 主线值 选择条件、影响与限制
format,导出格式 配置为torchscript(框架脚本格式) onnx 格式要被目标运行端支持;改扩展名不是转换
imgsz,输入大小 配置640,可受模型保存参数影响 640 主线显式设置;修改时同步全部预处理及固定输入检查
batch,批量尺寸 导出调用有自己的默认覆盖,不套用训练批量 显式1 本文只接受单张批量;多张需要重新设计入口
dynamic,动态输入维度 False False True允许部分动态尺寸,但本文固定输入检查会拒绝,不能只改一个开关
simplify,模型简化 True False True依赖对应简化工具;简化后仍需重新比较结果
opset,算子规范版本 未指定,由导出逻辑决定 17 不是软件包版本;目标运行库和导出算子必须支持
half,半精度导出 False False FP16(十六位浮点)需要合适硬件;不能用来冒充INT8(八位整数)量化
device,导出设备 随调用及模型 "cpu" 便于建立浮点基线;导出设备不是最终运行后端

本文不在图里内置去重,辅助函数做后处理。不同模型版本、分割模型、旋转框模型或内置后处理的图,输出都可能不同。结构检查只证明图满足格式约束,必须再加载、对照图片、比较指标。

量化、校准与调整范围

返回量化实践。Calibration(校准)用代表性图片观察模型数值范围,再选择较低位宽的数值表达。全零图片和随机数组可以检查接口,但不能替代业务校准数据。本文只使用训练集校准,保留验证集用于观察改变后的效果。

以下按ONNX Runtime(模型运行库)1.20.1的quantize_static(静态量化函数)实现核对;方法说明见量化文档

所属函数及参数 中文含义 1.20.1工具默认 主线值 可选值、调整条件、影响及限制
自定义校准读取器 图片数量与覆盖 无工具默认数量 真实训练图清单 几十到一两百张仅是小项目起点;先覆盖尺度、类别和光照,再依据误差调整
quant_pre_process(量化预处理) skip_optimization,跳过图优化 False False 模型优化不支持时可另开实验比较True,不能悄悄忽略失败
quantize_staticquant_format 量化表示 QDQ(量化/反量化节点) QDQ 另有QOperator(量化算子);运行端支持不同,不能随意互换
同上:activation_type 激活数值类型 QInt8(有符号八位整数) QInt8 QUInt8(无符号八位整数)等需结合后端;换类型重新评估
同上:weight_type 权重数值类型 QInt8 QInt8 不代表模型每个节点都变成整数
同上:per_channel 按通道量化权重 False True 可对照False;更多缩放参数可能减小部分量化误差,不保证更快
同上:calibrate_method 数值范围估计方法 MinMax(最小最大值) MinMax 可比较Entropy(熵校准)、Percentile(百分位校准);重新校准并保留记录
同上:op_types_to_quantize 要量化的算子类型 None(按支持算子集合处理) Conv(卷积)、MatMul(矩阵乘法) 减少范围可保留敏感部分精度,压缩收益也可能降低
同上:nodes_to_exclude 排除的具体节点 None 未指定 定位敏感节点后可排除,不要随意猜名字

主线不覆盖更多高级量化选项,其行为沿用固定版本;改设置须记录差异。INT8模型在某些处理器上可能变慢,部分算子仍为浮点。是否值得部署,要同时看实际质量、文件大小、延迟和目标后端支持,不靠文件名判定量化成功。

模型输出文件已存在时,先保留旧模型和报告,再为新实验使用新输出路径,并同步验证、比较、封装脚本。不要覆盖权重却保留旧精度记录;脚本用SHA-256(内容摘要)帮助发现这种错配。

封装文件、运行配置与测试范围

返回封装实践。本例入口默认从脚本所在目录读模型和配置,允许调用者从任何目录传入图片。辅助函数、配置和测试文件都属于这个运行包,不是可随意删除的附件。

依赖清单记录实际安装版本,主线使用numpy(数值库)、opencv-python(图像库)和onnxruntime(模型运行库)。不需要在接收端安装整套训练框架。若源环境采用无界面的图像库构建,应按实际包名记录,不能虚构已安装的另一个包。

封装配置 主线示例 默认性质与调整条件
模型源文件 八位模型 自定义选择;质量不合适时可选择浮点模型
精度报告 八位模型评估摘要 改模型必须改对应评估报告
独立推理报告 八位模型运行记录 必须与权重内容及配置相同,不能引用另一轮记录
输出目录 空的部署目录 脚本拒绝非空目录;先归档旧包,避免混入旧辅助文件
默认运行配置 模型导出时保存的配置 类别和输入协议必须与选定模型配套

选择浮点部署时,在封装脚本中将模型源文件、精度摘要和独立推理记录一起换为浮点对应产物;测试通过后再生成新包。不是只改模型后缀。接收端安装与文件摘要检查见迁移实践

测试有不同层次:语法检查只看代码能否解析;test_runtime.py(逻辑测试)只检查人工数值输入下的颜色、空结果、坐标和去重;实际入口运行才加载指定模型;最后还要用独立业务样本判断识别质量。任何一层通过都不自动等于后面几层已通过。

文件传输、分阶段归档和接收端核对

返回上传数据迁移复测。文件传输客户端的名称和界面可不同,下面描述常见连接字段与操作;不要把示意按钮名当成所有版本都一致的界面。

配置字段 中文含义 默认与主线选择 调整条件、影响与限制
Protocol(协议) 使用的传输方式 主线使用服务器实际开放协议 SFTP(安全文件传输协议)与FTP(文件传输协议)、FTPS(加密文件传输协议)不同,不能只换端口假装兼容
Host(主机) 服务器地址 环境提供方给定,无文章默认 地址不包含你本地磁盘路径
Port(端口) 服务监听端口 协议有常用值,但主线用实际值 常见SFTP为22、FTP为21;实际可改,不能自行认定
Username(用户名) 登录账号 实际账号 不在脚本和使用说明中保存密码
Remote directory(远端目录) 接收文件位置 项目目录或接收端运行目录 确认可写权限;注意是否多套一层同名目录
Transfer mode(传输模式) 文件字节传输方式 优先二进制或确认自动模式不会改文件 图片和模型不能做文本换行转换,摘要不一致时优先检查
Overwrite(覆盖) 同名文件处理 新实验用新目录 看清正在覆盖什么;目录名一样不代表内容属于同一轮

新建连接后填写字段,核对服务器身份提示,进入正确远端目录;从本地面板拖入整个文件夹或使用上传菜单。等待传输队列完成,检查Failed transfers(失败传输)列表,比较文件数量和大小。先回传一对图片标签试开,再批量处理,最后用内容摘要确认字节一致。

SSH连接可用且需要直接命令传输时,可以用SCP(安全复制)作为替代。以下在本地执行,中文占位必须替换,注意复制端口参数是大写-P

scp -P 实际端口 -r D:\warehouse_vision\curated 实际账号@实际服务器地址:/home/user/projects/warehouse_vision/

这只是传输方式替代,不能用来绕过接收端指定目录或权限。

数据整理完成,备份整理图片、原始标注、训练标签、配置、划分和增强记录;训练完成,备份日志、实际参数、最佳与最后权重;评估完成,保留指标、曲线及定义;压缩完成,保留前后模型和比较记录。归档是复制,不移动主线仍在使用的文件。一次实验可以在项目根目录使用下面的日期名称示例,日期自行替换,并确保归档目录此前不存在:

mkdir -p archives
mkdir archives/warehouse_v1_20260918
cp -r curated dataset warehouse.yaml reports exports runs deployment archives/warehouse_v1_20260918/

这是全部阶段完成后的归档示例;某个阶段尚未生成目录时,只复制当时已生成的成果,不把命令报错当成归档成功。敏感业务资料按实际访问权限保管。

SHA-256(内容摘要)检查只验证传输前后字节相同。迁移成功还必须在接收端运行逻辑测试和真实图片入口,并检查结果;文件能下载不代表目标硬件能运行。

从视频抽帧和逐帧检测(可选扩展)

此节不是照片主线的必做步骤。抽帧在本地运行,完成后返回图片整理;逐帧推理在服务器主线模型检查通过后运行。

指定视频、采集组和抽帧间隔

新建项目根目录的extract_frames.py

from pathlib import Path
import math
import cv2

ROOT = Path(__file__).resolve().parent
video = ROOT / "warehouse_camera.mp4"
group = "session09"
interval_seconds = 3.0
output = ROOT / "raw"
output.mkdir(exist_ok=True)
if list(output.glob(group + "__*")):
    raise ValueError("该采集组已有图片,请换名称,避免覆盖")
capture = cv2.VideoCapture(str(video))
fps = capture.get(cv2.CAP_PROP_FPS)
if not capture.isOpened() or not math.isfinite(fps) or fps <= 0:
    raise ValueError("视频无法打开或没有有效帧率")
step = max(1, round(fps*interval_seconds))

按间隔写出图片并释放视频资源

extract_frames.py文件末尾追加,紧接该文件上一个示例。

index, saved = 0, 0
try:
    while True:
        ok, frame = capture.read()
        if not ok:
            break
        if index % step == 0:
            name = output / f"{group}__{index:08d}.png"
            if not cv2.imwrite(str(name), frame):
                raise RuntimeError("抽帧文件保存失败")
            saved += 1
        index += 1
finally:
    capture.release()
if not saved:
    raise ValueError("没有得到可用帧")
print("已保存帧数:", saved)
Set-Location D:\warehouse_vision
python extract_frames.py

修改video(视频路径)、group(采集组名)和interval_seconds(抽帧秒数,例3.0)。它们是自定义示例值,没有工具默认;可从1、3、5秒比较覆盖和重复度。同一视频全部帧仍是一个组。该示例按稳定帧率估算;可变帧率应按时间戳处理。

为整段视频创建一次推理会话

新建项目根目录的detect_video.py

from pathlib import Path
import json
import cv2
from vision_runtime import preprocess, decode, create_session

ROOT = Path(__file__).resolve().parent
settings = json.loads((ROOT / "exports/runtime.json").read_text(encoding="utf-8"))
session, input_name = create_session(ROOT / "exports/warehouse_int8.onnx", settings["imgsz"])
capture = cv2.VideoCapture(str(ROOT / "warehouse_camera.mp4"))
if not capture.isOpened():
    raise ValueError("视频无法打开")
output = (ROOT / "reports/video_detections.jsonl").open("w", encoding="utf-8")
index = 0

逐帧写入检测结果

detect_video.py文件末尾追加,紧接该文件上一个示例。

try:
    while True:
        ok, frame = capture.read()
        if not ok:
            break
        tensor, info = preprocess(frame, settings["imgsz"])
        values = session.run(None, {input_name: tensor})[0]
        detections = decode(values, info, settings["names"], settings["confidence"], settings["nms_iou"])
        row = {"frame": index, "detections": detections}
        output.write(json.dumps(row, ensure_ascii=False)+"\n")
        index += 1
finally:
    capture.release()
    output.close()
if index == 0:
    raise ValueError("没有读取到视频帧")
cd /home/user/projects/warehouse_vision
python detect_video.py

输出为JSONL(每行一条结构化记录)。检查帧号与视频帧数相符,抽取若干帧对照位置。逐帧检测不是对象跟踪,连续帧的同一纸箱不能累加为库存数量。

第三部分:按现象排查问题

先保留现场,再定位出错阶段

记录当前机器、环境名、工作目录、完整命令和错误信息。先找最后一条具体异常,再回查上游文件。一轮只改少数因素,不把重装整个环境作为第一步。失败的真实日志应保留,不能改成预期输出。

环境与代码无法运行

找不到环境管理命令或激活失败。回到环境参考,确认命令确已安装及终端已初始化。新窗口不一定继承上一个窗口的激活状态。

ModuleNotFoundError(找不到模块)。先查正在使用哪个解释器,再用同一解释器安装依赖。服务器执行:

python -c "import sys; print(sys.executable)"
python -m pip --version
python -m pip check

IndentationError(缩进错误)或SyntaxError(语法错误)。回到报错行及上一行,检查是否漏了括号、冒号、引号或缩进。追加代码时不要复制说明文字,不要把函数内代码顶到行首;测试文件的第二个方法仍属于同一个类。

文件不存在。先检查是在本地还是服务器,当前目录是否正确,文件扩展名是否重复。代码中的示例图片名需要替换为真实文件,不能原样照抄一个根本不存在的路径。

图片和标注出问题

标注窗口没有图片。确认选择图片目录而不是只含标注的目录;用系统图片查看器试开;在视图菜单恢复文件列表和画布面板。

下一张仍有上一张的框。检查是否启用保留上一张标注,是否用了带复制行为的快捷键;回到标注参考关闭复用并逐图复核,不要直接保存覆盖。

保存后找不到标注。检查输出目录、同名文件和未保存提示。图片与标注不在一起时检查图片引用;不能只搬标注文件。空目标图片应确认存在合法空标注。

转换提示类别、形状或边界错误。回到原始标注,统一名称,确认是两个角点矩形、图片尺寸一致且框在图内。坐标整体偏移时查方向修正、宽高是否交换、归一化是否用了原图大小。按转换示例手算一张再重绘。

构建目录已存在。这是防混入旧数据的检查。先保存旧数据和记录,再选新实验目录或明确归档旧输出;不要直接删除源图片和标注。整个项目复制到新位置后,先检查数据配置中的绝对根路径。

训练启动不了或效果异常

CUDA out of memory(显卡显存不足)。查看其他进程占用,优先把批量从8降到4、2或1;不要停止不属于自己的进程。确需降低输入尺寸时同步后面的导出、校准和推理配置。更改后记录真实参数。

多进程读取报错。先将加载进程数改为0,并保留主函数入口判断;单进程跑通后再增加。数据损坏时进程数调整不能代替修数据。

损失下降,验证却差。检查标签、类别映射、采集场景差异和每类覆盖,查看漏检与误检图片;再考虑学习率、轮数和增强。参考训练参数,不要一次改所有值。

验证结果好得不合理。查同组相邻帧、重复图或增强副本是否跨数据分区;不能用训练集的预测截图替代独立验证。

断线后不确定训练是否还在跑。先检查原持久终端和进程、日志更新时间,再决定恢复。继续同一次训练与开启新实验的处理不同,见恢复方法

指标、导出和推理结果不对

曲线图片存在,但某类AUC为空。检查状态列和正负样本数量;无定义不是0分。图像级曲线不评价定位,不能拿它替代检测mAP。参见指标口径

矩阵与框架图看起来相反。先核对行列方向、背景定义、置信度和匹配规则,再比较数值;不要只为了看起来一致而转置文件。

模型能加载,但框错位或密集满屏。检查颜色、缩放填充、输出布局、类别数、坐标还原,以及是否对已经带后处理的模型再次去重。先运行逻辑测试,再检查真实模型原始输出。未知布局应明确报错,不做任意转置尝试。

浮点导出已经明显退化。先对齐源权重、导出设置和评估路径,确认类别元数据和预处理;修复浮点基线后才量化,不能用量化调参掩盖导出错误。

量化报不支持的算子。保存算子名、版本和后端,核对量化参数。改变量化类型或排除节点后需生成新模型并重算指标,不能静默换回浮点文件却继续写八位结果。

量化后更慢、文件更大或精度下降。这些都可能发生。分别检查硬件支持、图优化、校准覆盖、敏感节点与模型规模。保留真实对比,选择满足业务要求的模型,不伪造压缩收益。

封装和迁移失败

封装时摘要不匹配。模型、精度报告、运行记录或配置被改过;重新评估并运行当前模型,不绕过校验,也不手工改摘要。

换目录后找不到模型或辅助模块。整个运行目录一起复制,核对入口默认路径是否来自脚本位置。仅复制模型不能保留完整推理能力。

目标端摘要不一致。查失败传输、同名覆盖、传输模式和修改时间;重新传输不一致文件,再完整检查。摘要一致后仍需安装依赖、执行逻辑测试和实际推理。

目标环境安装不了固定依赖。查看系统、解释器及处理器架构支持;创建匹配环境,或记录新依赖组合后重新验证。不要宣称源服务器运行成功就等于目标机器可用。

本文检查范围与资料依据

静态检查包括分段代码拼接语法、终端命令格式、章节锚点、图片路径、文件引用以及主线和参数表的一致性。静态通过不表示模型曾经训练,也不能给出业务识别成绩。

运行验证使用临时合成图片和小型合成网络,检查整理、转换、增强、坐标还原、混淆矩阵、静态量化、逻辑测试以及独立入口生成结果文件的行为。它验证的是这些代码路径,不代表真实仓储检测有效。

本文没有执行真实YOLO(目标检测模型)训练、真实检测模型的完整导出评估、实际标注界面操作或远程服务器传输与迁移验收,也没有虚构精度、加速倍数或压缩率。文中固定版本与本地局部检查环境不同的部分,必须在实际使用环境继续核对。

参数依据分别链接在对应参考章节:固定版本的配置与实现优先于不断更新的通用文档。替换版本、模型结构或后端时,重新核对接口与输出形状,不把另一版本的菜单、默认参数或演示结果直接搬过来。

posted @ 2026-09-18 21:24  ai学习123  阅读(3)  评论(0)    收藏  举报