Ultralytics代码库深度解读【一】:onnx模型导出

1. 前言

  1. 本帖所讲的代码库是我在购买亚博智能的jetson orin super开发套件的时候附赠的代码库,并非官方标准版,但因其Ultralytics官方代码相差不大,对于官方代码的学习仍然具有参考价值。
  2. 本帖以常见的YOLO V8模型导出为例,其他模型导出同理。

我们先来看看软件动态行为关系图吧(不严谨版本,纯手绘):
在这里插入图片描述

其中橙色表示外部库的组件,绿色表示本项目工程内部组件,红色表示非常重要的软件接口。

导出onnx代码相关文件的部分文件结构(脚本文件除外):

c:\Users\Mystic\Desktop\pycharm_pro\
├── cfg/
│   └── __init__.py  ←── 包含entrypoint函数
├── engine/
│   ├── model.py     ←── 包含Model类和export方法
│   └── exporter.py  ←── 包含Exporter类

本文要解决的两个重点问题:

  1. 运行时命令是如何被解析的?
  2. onnx格式的模型文件是如何生成的?

2. 命令解析

2.1 命令解析大致过程

YOLOv8的模型导出为例:

yolo export model=yolov8n.pt format=onnx imgsz=640

当执行上述命令时,系统按以下步骤解析参数:

  1. Entry Point启动:通过Python的entry points机制,调用cfg/init.py中的entrypoint()函数
  2. 参数分离:将命令行参数分解为模式(export)和配置项
  3. 参数合并:按优先级合并配置:命令行参数 > 方法默认值 > 模型参数 > 全局默认值
  4. 模型实例化:创建YOLO模型实例并加载权重文件()

2.2 命令解析的底层逻辑

既然知道了参数解析的大致过程,那我们不禁要好奇,为什么这套代码可以解析参数?在整个虚拟环境中,我找到了这个txt文件:

/home/jetson/venvs/ultralytics-env/lib/python3.10/site-packages/ultralytics-8.3.189.dist-info/entry_points.txt

里面有这样一段内容:

[console_scripts]
ultralytics = ultralytics.cfg:entrypoint
yolo = ultralytics.cfg:entrypoint

这不就是说,我们在命令行输入了相关格式的内容,就会跳到相关的路径下去执行代码吗?但这还远远不够,txt文件的内容是如何被解析的呢?起到了什么作用呢?
查阅相关资料,发现当pip安装ultralytics包时,具体分成以下步骤:

第一步:pip解析包的配置文件(setup.py或pyproject.toml)
第二步:pip读取其中的entrypoints配置
第三步:pip安装包文件到 site-packages/ultralytics/
第四步:pip创建 ultralytics-{version}.dist-info/ 目录
第五步:pip在dist-info中生成 entry_points.txt 等元数据文件
第六步:pip根据entry_points生成可执行脚本(如/bin/yolo)

首先,第一个问题,为什么pip会乖乖地创建entry_points.txt文件?

在github中打开项目的pyproject.toml我们会有新的发现:
在这里插入图片描述
这就都解释得通了,当用pip命令安装代码包的时候,pip会读pyproject.toml文件,而根据文件的描述,在命令行输入yoloultralytics 时,直接跳到对应的文件路径中去执行。

从第四步到第六笔就可以发现,这部分都与我们的entry_points有关系,创建了entry_points相关文件,并据此生成了可执行脚本,然后我们就可以在bin文件夹下找到相关的文件。

在bin文件夹下,确实找到了ultralytics和yolo文件
在这里插入图片描述
这两个文件具有相同的内容:

#!/home/jetson/venvs/ultralytics-env/bin/python3
import sys
from ultralytics.cfg import entrypoint
if __name__ == '__main__':
    if sys.argv[0].endswith('.exe'):
        sys.argv[0] = sys.argv[0][:-4]
    sys.exit(entrypoint())

这两个相同的脚本文件是如何生成的呢(两个相同,仅以yolo为例)?
首先,pip内部有一个标准模板:

#!/{python_exe}
import sys
from {
   
   module} import {
   
   function}
if __name__ == '__main__':
    if sys.argv[0].endswith('.exe'):
        sys.argv[0] = sys.argv[0][:-4]  # Windows兼容性
    sys.exit({
   
   function}())

当解析我们的配置文件pyproject.toml时候,看到上述的配置:

[project.scripts]
yolo = "ultralytics.cfg:entrypoint"

pip会自动替换模板中的变量:

  • {python_exe}/home/jetson/venvs/ultralytics-env/bin/python3
  • {module}ultralytics.cfg
  • {function}entrypoint

最终生成我们看到的/bin/yolo脚本。
具体的执行流程大致是这样的:

用户输入: yolo export model=yolov8n.pt format=onnx imgsz=640
    ↓
系统找到: /home/jetson/venvs/ultralytics-env/bin/yolo
    ↓  
执行脚本: #!/home/jetson/venvs/ultralytics-env/bin/python3
    ↓
运行代码: from ultralytics.cfg import entrypoint
    ↓
Python导入: cfg/__init__.py 中的 entrypoint 函数
    ↓
调用函数: sys.exit(entrypoint())
    ↓
进入代码: entrypoint函数开始处理命令行参数

3. 权重文件加载与预处理

现在我们就是顺利运行到了项目的具体代码(cfg/__init__.py)中,代码总用有1000多行,其实重要的代码并没有太多,很多都是判别输入命令的正确性的。让我们来看看cfg/__init__.pyentrypoint函数是如何一步步解析并执行我们的命令的。

3.1 命令拆分与合法性检查

entrypoint函数中,首先对我们输入的命令进行了拆分,并对其合法性进行了检查,在输入命令不合法的时候,给予适当的提示,这部分代码如下:

    args = (debug.split(" ") if debug else ARGV)[1:]
    if not args:  # no arguments passed
        LOGGER.info(CLI_HELP_MSG)
        return

    special = {
   
   
        "help": lambda: LOGGER.info(CLI_HELP_MSG),
        "checks": checks.collect_system_info,
        "version": lambda: LOGGER.info(__version__),
        "settings": lambda: handle_yolo_settings(args[1:]),
        "cfg": lambda: yaml_print(DEFAULT_CFG_PATH),
        "hub": lambda: handle_yolo_hub(args[1:]),
        "login": lambda: handle_yolo_hub(args),
        "logout": lambda: handle_yolo_hub(args),
        "copy-cfg": copy_default_cfg,
        "solutions": lambda: handle_yolo_solutions(args[1:]),
    }
    full_args_dict = {
   
   **DEFAULT_CFG_DICT, **{
   
   k: None for k in TASKS}, **{
   
   k: None for k in MODES}, **special}

    # Define common misuses of special commands, i.e. -h, -help, --help
    special.update({
   
   k[0]: v for k, v in special.items()})  # singular
    special.update({
   
   k[:-1]: v for k, v in special.items() if len(k) > 1 and k.endswith("s")})  # singular
    special = {
   
   **special, **{
   
   f"-{
     
     k}": v for k, v in special.items()}, **{
   
   f"--{
     
     k}": v for k, v in special.items()}}

    overrides = {
   
   }  # basic overrides, i.e. imgsz=320
    for a in merge_equals_args(args):  # merge spaces around '=' sign
        if a.startswith("--"):
            LOGGER.warning(f"WARNING ⚠️ argument '{
     
     a}' does not require leading dashes '--', updating to '{
     
     a[2:]}'.")
            a = a[2:]
        if a.endswith(","):
            LOGGER.warning(f"WARNING ⚠️ argument '{
     
     a}' does not require trailing comma ',', updating to '{
     
     a[:-1]}'.")
            a = a[:-1]
        if "=" in a:
            try:
                k, v = parse_key_value_pair(a)
                if k == "cfg" and v is not None:  # custom.yaml passed
                    LOGGER.info(f"Overriding {
     
     DEFAULT_CFG_PATH} with {
     
     v}")
                    overrides = {
   
   k: val for k, val in yaml_load(checks.check_yaml(v)).items() if k != "cfg"}
                else:
                    overrides[k] = v
            except (NameError, SyntaxError, ValueError, AssertionError) as e:
                check_dict_alignment(full_args_dict, {
   
   a: ""}, e)

        elif a in TASKS:
            overrides["task"] = a
        elif a in MODES:
            overrides["mode"] = a
        elif a.lower() in special:
            special[a.lower()]()
            return
        elif a in DEFAULT_CFG_DICT and isinstance(DEFAULT_CFG_DICT[a], bool):
            overrides[a] = True  # auto-True for default bool args, i.e. 'yolo show' sets show=True
        elif a in DEFAULT_CFG_DICT:
            raise SyntaxError(
                f"'{
     
     colorstr('red', 'bold', a)}' is a valid YOLO argument but is missing an '=' sign "
                f"to set its value, i.e. try '{
     
     a}={
     
     DEFAULT_CFG_DICT[a]}'\n{
     
     CLI_HELP_MSG}"
            )
        else:
            check_dict_alignment(full_args_dict, {
   
   a: ""})

    # Check keys
    check_dict_alignment(full_args_dict, overrides)

首先是参数拆分:

 args = (debug.split(" ") if debug else ARGV)[1:]

输入的命令被args保存起来,变成了这样的一个列表:

args = ['export', 'model=yolov8n.pt', 'format=engine', 'imgsz=640'] 

仅仅这样还不够,还要做进一步的处理,将args进一步通过=号拆分成键值对,然后将其保存到一个overrides字典中,解析后字典变成了:

overrides = {
   
   "mode": "export", "model": "yolov8n.pt", "format": "onnx", "imgsz": 640}

最后将字典传入函数check_dict_alignment()中,对其进行对其合法性检查:

def check_dict_alignment(base: Dict, custom: Dict, e=None):
    custom = _handle_deprecation(custom)
    base_keys, custom_keys = (set(x.keys()) for x in (base, custom))
    mismatched = [k for k in custom_keys if k not in base_keys]
    if mismatched:
        from difflib import get_close_matches

        string = ""
        for x in mismatched:
            matches = get_close_matches(x, base_keys)  # key list
            matches = [f"{
     
     k}={
     
     base[k]}" if base.get(k) is not None else k for k in matches]
            match_str = f"Similar arguments are i.e. {
     
     matches}." if matches else ""
            string += f"'{
     
     colorstr('red', 'bold', x)}' is not a valid YOLO argument. {
     
     match_str}\n"
        raise SyntaxError(string + CLI_HELP_MSG) from e

紧接着,是领取主要任务:

    # Mode
    mode = overrides.get("mode")

然后加载相关模型:

    # Model
    model = overrides.pop("model", DEFAULT_CFG.model)
    if model is None:
        model = "yolo11n.pt"
        LOGGER.warning(f"WARNING ⚠️ 'model' argument is missing. Using default 'model={
     
     model}'.")
    overrides["model"] = model
    stem = Path(model).stem.lower()
    if "rtdetr" in stem:  # guess architecture
        from ultralytics import RTDETR

        model = RTDETR(model)  # no task argument
    elif "fastsam" in stem:
  
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值