WWDC Quick Look 💓 By SwiftGGTeam
Tune your Core ML models

Tune your Core ML models

观看原视频

Highlight

Core ML 引入 MLShapedArray 替代 MLMultiArray 处理多维数据,新增 ML Package 格式分离模型组件便于版本控制,并推出 ML Program 模型类型支持 typed execution,让开发者能在转换时精确控制 Float16/Float32 精度。

核心内容

MLMultiArray 的 Swift 化替代

Core ML 模型经常输出多维数组数据,比如目标检测模型 YOLO 会输出一个二维数组,每行代表一个检测框,每列代表对不同类别的置信度。以前用 MLMultiArray 处理这种数据,代码写起来很别扭:初始化时要传运行时类型,索引时要用 NSNumber,没有类型安全。

WWDC2021 推出了 MLShapedArray,一个纯 Swift 的多维数组类型(01:34)。它是值类型,支持 copy-on-write,有和 Array 类似的切片语法。最重要的是,MLShapedArray 和 MLMultiArray 完全兼容,可以互相转换。

ML Package:模型文件的版本控制方案

传统的 .mlmodel 文件是一个 protobuf 二进制文件,把元数据、接口定义、网络架构和权重参数全部打包在一起。这带来一个问题:改一个 metadata 字段(比如作者名字),整个二进制文件都会变,git diff 会显示几十 MB 的变更。

ML Package 用 macOS 的 package 功能把模型组件拆分成独立文件(05:30):架构、权重、元数据各存一个文件。改 metadata 只改一个 JSON 文件,git diff 清晰可读。Xcode 直接支持在 UI 里编辑 metadata 和输入输出描述。

ML Program:面向未来的模型格式

ML Program 是一种新的模型表示方式,把神经网络描述为一系列强类型的操作(ops),而不是传统的层(layers)(10:05)。权重单独序列化到二进制文件,中间张量的类型在转换时就确定下来。

Typed execution 的核心优势是精度控制。传统神经网络模型的中间张量类型由运行时计算单元决定:Neural Engine 和 GPU 用 Float16,CPU 用 Float32。如果模型对 Float16 精度敏感,只能把 computeUnits 设为 .cpuOnly,牺牲大量性能。

ML Program 的中间张量类型在模型转换时就固定下来。Core ML 运行时可以把 Float32 的 ops 分配到 GPU 执行,同时保留 Float16 的 ops 在 Neural Engine 上执行,不再需要全局的 computeUnits 设置。

详细内容

MLShapedArray 的基本用法

01:55

// 初始化一个 2x3 的 Float ShapedArray
let shapedArray = MLShapedArray<Float>(
    [[1.0, 2.0, 3.0],
     [4.0, 5.0, 6.0]]
)

// 访问第二行作为切片
let secondRow = shapedArray[1]

// 访问多行多列的切片
let subArray = shapedArray[0..<2, 1..<3]

关键点:

  • MLShapedArray<Float> 是泛型类型,编译时就确定元素类型,不需要运行时传类型参数
  • 支持多维索引和范围切片,语法和 Swift Array 一致
  • 值类型的 copy-on-write 语义,切片操作不会复制底层数据

MLShapedArray 与 MLMultiArray 互转

02:31

// MLMultiArray 转 MLShapedArray
let multiArray: MLMultiArray = ...
let shapedArray = MLShapedArray(multiArray)

// MLShapedArray 转 MLMultiArray
let shapedArray: MLShapedArray<Float> = ...
let multiArray = MLMultiArray(shapedArray)

// 类型转换:Double MultiArray 转 Float ShapedArray
let floatShapedArray = MLShapedArray<Float>(multiArrayOfDoubles)

关键点:

  • 互转通过初始化器完成,不需要手动遍历数据
  • 支持跨类型转换,比如 Double 转 Float
  • Xcode 自动生成的模型包装类会为每个 MultiArray 输出新增对应的 ShapedArray 属性

YOLO 模型输出处理对比

02:51

MLMultiArray 版本:

// 用 MLMultiArray 处理 YOLO 输出
func bestLabels(from confidence: MLMultiArray) -> [String] {
    var results: [String] = []
    let rowCount = confidence.shape[0].intValue
    let columnCount = confidence.shape[1].intValue

    for row in 0..<rowCount {
        var maxConfidence: Double = 0
        var bestLabel = ""
        for column in 0..<columnCount {
            let value = confidence[[NSNumber(value: row), NSNumber(value: column)]].doubleValue
            if value > maxConfidence {
                maxConfidence = value
                bestLabel = labels[column]
            }
        }
        results.append(bestLabel)
    }
    return results
}

MLShapedArray 版本:

// 用 MLShapedArray 处理同样的输出
func bestLabels(from confidence: MLShapedArray<Float>) -> [String] {
    return confidence.map { row in
        let maxIndex = row.argmax()
        return labels[maxIndex]
    }
}

关键点:

  • MLMultiArray 版本需要手动处理 NSNumber 转换和二维索引,代码冗长
  • MLShapedArray 版本利用 Swift 集合协议(mapargmax),一行完成同样逻辑
  • 模型预测结果如果输出 ShapedArray,直接就能用 Swift 标准库方法处理

转换 TensorFlow 模型为 ML Program

16:54

import coremltools as ct

# 加载 TensorFlow 模型
model_path = "style_transfer_model"
image_path = "test_image.jpg"

# 设置输入类型
input_type = ct.ImageType(
    shape=(1, 256, 256, 3),
    bias=[-1, -1, -1],
    scale=1/127.0
)

# 转换为 ML Program(iOS 15 / macOS 12 起)
ml_program = ct.convert(
    model_path,
    inputs=[input_type],
    minimum_deployment_target=ct.target.iOS15
)

# 默认生成 Float16 精度的 ML Program
# 保存模型
ml_program.save("StyleTransfer.mlmodel")

关键点:

  • minimum_deployment_target 设为 iOS15 或 macOS12 时,converter 输出 ML Program 格式
  • 默认启用 FP16ComputePrecision pass,所有 Float32 张量转为 Float16
  • Float16 模型可在 Neural Engine 上执行,获得显著性能提升和功耗降低

控制 ML Program 精度

20:16

# 生成 Float32 精度的 ML Program
ml_program_fp32 = ct.convert(
    model_path,
    inputs=[input_type],
    minimum_deployment_target=ct.target.iOS15,
    compute_precision=ct.precision.FLOAT32
)

# 评估精度差异
# Float16 版本的 SNR 通常在 70+,Float32 版本在 100+
# 对大多数深度学习模型,Float16 精度损失肉眼不可见

关键点:

  • compute_precision=FLOAT32 禁用 Float16 转换,保持完整精度
  • Float32 ML Program 在 GPU 和 CPU 上执行,不会在 Neural Engine 上运行
  • 如果模型对 Float16 敏感,不需要在 App 代码里改 computeUnits,转换时指定精度即可
  • 还支持混合精度:指定特定 ops 用 Float32,其余用 Float16

核心启发

1. 升级现有 Core ML 模型到 ML Package

  • 做什么:把项目中的 .mlmodel 文件转换为 .mlpackage,获得 metadata 编辑和版本控制优势
  • 为什么值得做:改 metadata 不再产生几十 MB 的 git diff,团队成员可以 review 模型变更
  • 怎么开始:在 Xcode 中选中 .mlmodel 文件,点击 Edit 按钮,Xcode 会自动提示转换为 ML Package

2. 用 MLShapedArray 简化模型输出处理

  • 做什么:把处理 MLMultiArray 的代码迁移到 MLShapedArray,减少代码量并提高可读性
  • 为什么值得做:MLShapedArray 支持 Swift 标准集合协议,可以用 mapfilterargmax 等原生方法,代码从几十行减到几行
  • 怎么开始:检查 Xcode 自动生成的模型类,使用新增的 xxxShapedArray 属性替代 xxx MultiArray 属性

3. 为精度敏感模型采用 Float32 ML Program

  • 做什么:识别对数值精度敏感的模型(如科学计算、金融预测),在转换时指定 Float32 精度
  • 为什么值得做:不再需要把 computeUnits 限制为 .cpuOnly,Float32 ML Program 可以在 GPU 上执行,保留性能的同时确保精度
  • 怎么开始:用 coremltools 转换时设置 compute_precision=FLOAT32,对比 Float16 和 Float32 的 SNR 指标

4. 构建模型版本管理系统

  • 做什么:利用 ML Package 的分离文件结构,建立模型的 CI/CD 流程
  • 为什么值得做:架构文件(JSON)和权重文件(二进制)分开存储,架构变更可以 code review,权重变更可以走独立的发布流程
  • 怎么开始:把 .mlpackage 目录加入 git,metadata.json 和特征描述 JSON 正常 review,大权重文件用 Git LFS 管理

关联 Session

评论

GitHub Issues · utterances