在之前的两篇文章中(关于感知哈希在Chat Control背景下的用途和关于其对抗性碰撞问题),我们探索了如何仅用40行Python代码解决了一个被行业大规模部署的问题。感知哈希是一个廉价的确定性层的典型例子:无需模型、无需GPU、无需沉重的依赖。

哈希解决了重复数据和相似性检测的问题。但在实际图像处理管线中,有些任务无法用传统算法完成:检测水印的边界框,用持续上下文的方式填补空缺,分类人脸是否为成人,以及提取语义属性等。这些任务需要大型神经网络。

本文要解决的问题是:如何在Apple Silicon上部署这些神经网络,避免云API的依赖,不为每张图像支付费用,并充分利用Apple Neural Engine(ANE)?

简单回答

PyTorch → ONNX → CoreML,只需三条指令。

深入分析

真正的价值不在技术,而在架构。今年我在情感分析和图像过滤领域应用了这个模式。看似不相关,但实质是同样的核心方法。


模式:两层架构,一种哲学

几周前,我发布了SentimentKit,这是一种用于情感分析的库。我发现Apple的NLTagger会将“删除临时文件”评分为-0.8(非常消极)。这个问题并非Apple独有,而是一整类预训练ML模型中的系统性偏差。

解决方法不是“寻找更好的模型”,而是架构上的。SentimentKit的管线有四层:

  1. 确定性关键字检测器。 包含了经过人工选择的脏话、消极表达与正面词汇的字典,涵盖8种语言。容量约20 KB。作为最先运行的部分。
  2. Apple的NLTagger(修正技术偏差)。 如果消息未触发第一层,则交给这部分处理。
  3. 特定领域模型(基于规则和技术领域的启发式算法)。
  4. 基础模型(Apple的本地LLM)。仅在前三层返回模糊结果时调用。

模式:先运行廉价的确定性层,仅在简单规则无法决策时才调用机器学习。 在第一层中有70-80%的流量被处理,无需涉及机器学习模型,带来了能耗、延迟和避免偏差的多方面好处。

几周后,当我开始设计一个规模化的图像清洗管线时,发现自己无意间又应用了同样的模式:

  1. 确定性验证(Pillow验证、magic bytes、尺寸限制)。剔除明显无效的数据。
  2. 感知哈希(aHash + dHash + pHash)。大约每张图像10-15毫秒,按相似性分组。
  3. 经典OCR配合已知Token的正则表达式检测水印。
  4. 神经网络(YOLOv11用于检测,LaMa用于图像修复)。仅应用于通过前几层过滤的图像,占比约10-20%。

以上并不足以一以贯之。这是相同哲学在不同领域的应用,其本质问题是一致的:

如何以最低成本实现深度学习模型的推理。

在Apple Silicon上,最优的方式是通过CoreML将计算任务分配至ANE。

ONNX:被低估的中间语言

ONNX(Open Neural Network Exchange)是神经网络模型交换的格式,由Microsoft、Meta、NVIDIA等组织维护。其规范描述了如何在便携的文件中序列化模型的计算图,包括操作、连接关系及权重。

作用:分离训练过程与部署过程。

在ONNX出现之前,如果你使用PyTorch训练模型,就必须在生产中使用PyTorch。如果要迁移到TensorFlow Serving、CoreML或嵌入式运行时,需要逐一手工实现操作。这种过程费时容易出错。

ONNX的流程重新定义了路径:

PyTorch(训练) → ONNX(交换格式) → Runtime X(生产环境)

其中,Runtime X可以是:onnxruntime(支持多种后端)、TensorRT(NVIDIA)、OpenVINO(Intel)、CoreML(Apple)、TFLite(移动设备)、Triton(服务器)或WebAssembly(浏览器)等。

只需一次导出即可,由运行时选择针对硬件的最佳后端。在Apple Silicon上,我们需要的是CoreML Execution Provider,它基于CPU、Metal GPU和Apple Neural Engine规划实现。

实际限制

并非所有PyTorch的操作都能直接映射到ONNX。现代架构使用的某些自定义操作(例如flash attention或非传统卷积)可能无法被干净地导出。实际解决方案:如果你的模型是YOLO、ResNet、Transformer标准架构或U-Net,导出无难度。如果包含不常见的操作,在假设模型可导出前,请先检查兼容矩阵。

对本文涉及的YOLOv11和LaMa模型,两者都提供了高质量的ONNX导出支持。对于YOLOv11,ultralytics可一键完成导出;对于LaMa,Carve AI团队已在Hugging Face上发布了ONNX格式,无需重新导出。

CoreML:Apple的ML运行时与Neural Engine

CoreML是iOS和macOS的机器学习运行时,自iOS 11(2017年)推出。自A11 Bionic芯片(2017年发布的iPhone 8)及所有Apple Silicon以来,CoreML可在三种不同的芯片单元上运行模型:

  • CPU: 总是可用的,适合非并行计算或超小型模型。
  • GPU Metal: 图形处理单元也能承担通用性计算,非常适合大批量处理任务。
  • Apple Neural Engine(ANE): 为张量操作设计的专用协处理器,优化于低功耗场景下的单次推理。M系列芯片上有16个核,M4的峰值计算能力约为18 TOPS。专为卷积、矩阵运算与精度较低推理(FP16,INT8)优化。

CoreML会自动决定操作运行的位置。操作兼容ANE则运行于ANE;否则回退至GPU或CPU。开发者无需手动指定调度器,CoreML已优化好。

ANE相较GPU Metal的关键优势是能效。在单次推理任务下,根据Apple和第三方公开基准(Geekbench ML、CreateML),ANE的能耗约为GPU Metal的3-5倍。同时,以一台有电池的MacBook Pro为例,使用ANE可以允许每晚运行处理管线而不会耗尽电量。

ANE的缺点:

  1. 无法加速训练。
  2. 并非所有操作都被支持。回退到CPU的操作会削弱能效优势。因此,对于量化到FP16/INT8的标准架构,导出优化良好的模型尤为重要。

onnxruntime-coreml的角色

Microsoft维护的onnxruntime包含一组**执行后端(Execution Providers)**以实现对硬件的抽象。通过安装onnxruntime-coreml(自2022年推出),运行时可以无缝使用CoreML作为后端:

import onnxruntime as ort

session = ort.InferenceSession(
    "yolo11x-watermark.onnx",
    providers=["CoreMLExecutionProvider", "CPUExecutionProvider"],
)

output = session.run(None, {"images": input_tensor})

只需以上三行代码,ONNX模型即可在Apple Neural Engine上运行。对于不支持的操作也会回退至CPU。无需编写Swift代码,无需使用Xcode,无需打包.mlmodel。仅需Python + ONNX + onnxruntime-coreml。

三条真实的转换命令

以下是我在生产中实际操作的完整流程:使用改进YOLOv11的水印检测模型。

步骤1:获取PyTorch模型

对于YOLOv11,我使用corzent/yolo11x_watermark_detection——此为基于精心挑选的水印数据集的YOLOv11模型(大小114 MB,MIT许可):

hf download corzent/yolo11x_watermark_detection --local-dir ./models

此命令会下载best.pt(PyTorch模型及元数据)。此为公开HuggingFace仓库,不需要认证令牌。

步骤2:导出到ONNX格式

ultralytics(YOLOv11的官方库)可一键完成导出:

from ultralytics import YOLO

model = YOLO("./models/best.pt")
model.export(format="onnx", imgsz=640, simplify=True)
# -> ./models/best.onnx (228 MB)

参数simplify=True会折叠冗余操作并提升与ANE的兼容性。在一台M3上测量,完成时间约为2-3秒。需额外安装依赖库onnx与onnxslim。

步骤3:使用CoreML Execution Provider进行推理

import onnxruntime as ort
import numpy as np
from PIL import Image

session = ort.InferenceSession(
    "./models/best.onnx",
    providers=["CoreMLExecutionProvider", "CPUExecutionProvider"],
)

img = Image.open("photo.jpg").resize((640, 640))
tensor = np.asarray(img, dtype=np.float32).transpose(2, 0, 1)[None] / 255.0

outputs = session.run(None, {"images": tensor})
# outputs[0] shape: (1, 5, 8400) — cx, cy, w, h, score per detection

加载会话时,onnxruntime会打印以下日志,演示操作分布:

CoreMLExecutionProvider::GetCapability
  number of partitions supported by CoreML: 7
  number of nodes in the graph: 617
  number of nodes supported by CoreML: 609

即:共有617个操作节点,609(98.7%)操作节点通过CoreML运行(进而由ANE处理),仅8个操作回退到CPU。因部分操作需使用CPU,计算图被分割成7段子图。

在M3机型上的时间指标(预热后):每张图像42-48毫秒。首次执行需要额外支付~55毫秒的CoreML计算图编译开销,在后续推断中摊销。


更详细内容请参考完整文章。

本文原文为西班牙语,借助AI翻译。