- AI 应用
- 机器学习
- 模型优化
【免费下载链接】coremltools
Core ML tools contain supporting tools for Core ML model conversion, editing, and validation.
导读
本文基于 coremltools 官方指南 updatable-neural-network-classifier-on-mnist-dataset.md,完整演示如何把一个用 Keras 训练好的 MNIST 手写数字分类卷积模型,转换为 Core ML 模型,再通过 coremltools 的NeuralNetworkBuilder将其标记为可更新(updatable),从而支持在设备端使用MLUpdateTask进行个人化微调(on-device personalization)。读完本文,你将掌握可更新模型的基本原理、make_updatable的完整调用链、损失层与优化器的配置方式,以及如何用 builder 的inspect_*系列方法逐层验证模型的可更新状态。
可更新模型的概念对应官方文档 updatable-model-examples.rst 中提到的三类可更新模型之一(神经网络、pipeline 模型、最近邻模型),而本文聚焦于神经网络这一分支。
什么是可更新的神经网络模型
神经网络本质上是一系列层的集合,每一层都包含一组权重(weights),权重与其他输入共同作用产生输出。要让某一层的权重能够在训练过程中被调整,就必须先把这层标记为updatable(可更新)——这是设备端微调的前提。
更新过程依赖两个关键组件:
- 损失函数(Loss function):衡量给定输入时网络输出与期望结果之间的偏差。
- 优化器(Optimizer):接收损失值,结合当前输出去调整各层的权重,使网络输出越来越贴近期望。
用公式可以概括:损失(loss) → 优化器(optimizer) → 更新权重(weights),如此循环迭代若干个 epoch。
根据官方指南中的支持范围声明,当前 coremltools 支持的可更新能力包括:
| 类别 | 支持范围 |
|---|---|
| 可更新层 | 卷积层(convolution)与全连接层(fully-connected / innerProduct) |
| 损失函数 | 均方误差(mean squared error)与分类交叉熵(categorical cross-entropy) |
| 优化器 | 随机梯度下降(Stochastic Gradient Descent, SGD)与 Adam |
这一限制在源码中有直接体现:coremltools/models/neural_network/builder.py第 26 行定义了_SUPPORTED_UPDATABLE_LAYERS = ["innerProduct", "convolution"],而make_updatable()方法在标记某一层前会先校验其类型是否在该列表中,否则抛出ValueError(见 builder.py)。测试用例test_nn_set_softmax_updatable_invalid也验证了把 softmax 层标记为可更新会报错(见 test_model_updatable.py)。
实战案例总览
下面将用 MNIST 手写数字数据集演示完整流程:
- 用 Keras 创建一个简单的卷积模型,并在 MNIST 数据上训练(本文聚焦模型结构定义与保存);
- 用 coremltools 的 Keras 转换器将模型转换为 Core ML 模型(
.mlmodel); - 通过
NeuralNetworkBuilder把模型改造成可更新模型。
改造后的可更新模型具有两个可更新层(两个全连接层dense_1、dense_2),使用分类交叉熵损失和SGD 优化器。
第一步:用 Keras 创建基础模型
首先定义一个函数,创建并保存 Keras 卷积模型。模型接收 28×28 的单通道灰度图像,输出 10 个数字类别的概率分布:
def create_keras_base_model(url): """This method creates a convolutional neural network model using Keras. url - The URL that the keras model will be saved as h5 file. """ import keras from keras.models import Sequential from keras.layers import Dense, Dropout, Flatten, Conv2D, MaxPooling2D keras.backend.clear_session() model = Sequential() model.add(Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=(28, 28, 1))) model.add(Conv2D(64, (3, 3), activation='relu')) model.add(MaxPooling2D(pool_size=(2, 2))) model.add(Dropout(0.25)) model.add(Flatten()) model.add(Dense(128, activation='relu')) model.add(Dropout(0.5)) model.add(Dense(10, activation='softmax')) model.compile(loss=keras.losses.categorical_crossentropy, optimizer=keras.optimizers.SGD(lr=0.01), metrics=['accuracy']) model.save(url) keras_model_path = './KerasMnist.h5' create_keras_base_model(keras_model_path)说明:
keras.backend.clear_session()用于清理上一个会话的图,避免多次创建模型时出现状态残留。模型在真实场景中应先在 MNIST 训练集上完成训练再保存,这里为演示直接以编译后的结构保存为 h5 文件。
第二步:转换为 Core ML 模型格式
使用 coremltools 的 Keras 转换器把模型转换为.mlmodel。转换时需要指定输入输出名称、类别标签以及预测特征名:
def convert_keras_to_mlmodel(keras_url, mlmodel_url): """This method simply converts the keras model to a mlmodel using coremltools. keras_url - The URL the keras model will be loaded. mlmodel_url - the URL the Core ML model will be saved. """ from keras.models import load_model keras_model = load_model(keras_url) from coremltools.converters import keras as keras_converter class_labels = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] mlmodel = keras_converter.convert(keras_model, input_names=['image'], output_names=['digitProbabilities'], class_labels=class_labels, predicted_feature_name='digit') mlmodel.save(mlmodel_url) coreml_model_path = './MNISTDigitClassifier.mlmodel' convert_keras_to_mlmodel(keras_model_path , coreml_model_path)关键参数含义:
input_names=['image']:模型输入特征名为image;output_names=['digitProbabilities']:softmax 输出的概率向量特征名为digitProbabilities;class_labels:10 个数字'0'~'9'作为分类标签;predicted_feature_name='digit':预测的数字类别输出特征名。
转换得到的是一个**普通(非可更新)**的 Core ML 模型,后续步骤将围绕它进行改造。
第三步:加载 Spec 并应用设置
可更新改造的核心是操作模型的 protobuf 规格(spec)。coremltools 提供了coremltools.utils.load_spec来从.mlmodel文件加载规格,其底层实现在 models/utils.py,支持从.mlmodel文件或.mlpackage目录加载。
import coremltools spec = coremltools.utils.load_spec(coreml_model_path) builder = coremltools.models.neural_network.NeuralNetworkBuilder(spec=spec)1. 检查模型末尾的层
builder.inspect_layers(last=3)输出显示最后三层都是非可更新的(Updatable: False),其中dense_2是 innerProduct(全连接)层,dense_1__activation__和dense_2__activation__是 activation 层:
[Id: 9], Name: dense_2__activation__ (Type: softmax) Updatable: False Input blobs: [u'dense_2_output'] Output blobs: [u'digitProbabilities'] [Id: 8], Name: dense_2 (Type: innerProduct) Updatable: False Input blobs: [u'dense_1__activation___output'] Output blobs: [u'dense_2_output'] [Id: 7], Name: dense_1__activation__ (Type: activation) Updatable: False Input blobs: [u'dense_1_output'] Output blobs: [u'dense_1__activation___output']inspect_layers(last=n)的实现会从最后一层开始倒序打印 n 层的类型、可更新标记以及输入输出 blob,源码见 builder.py。
2. 检查模型输入特征
这一步获取输入信息,供后续make_updatable()使用:
builder.inspect_input_features() neuralnetwork_spec = builder.spec[Id: 0] Name: image Type: multiArrayType { shape: 1 shape: 28 shape: 28 dataType: DOUBLE }转换后模型的输入image目前是形状为1×28×28的 multiArray(DOUBLE 类型),而非图像类型。
3. 将输入改为 28×28 灰度图像
为了让模型能以标准图像形式接受 28×28 的灰度图,需要直接修改 spec 中的输入描述:
# change the input so the model can accept 28x28 grayscale images neuralnetwork_spec.description.input[0].type.imageType.width = 28 neuralnetwork_spec.description.input[0].type.imageType.height = 28 from coremltools.proto import FeatureTypes_pb2 as _FeatureTypes_pb2 grayscale = _FeatureTypes_pb2.ImageFeatureType.ColorSpace.Value('GRAYSCALE') neuralnetwork_spec.description.input[0].type.imageType.colorSpace = grayscale # let's inspect the input again to confirm the change in input type builder.inspect_input_features()[Id: 0] Name: image Type: imageType { width: 28 height: 28 colorSpace: GRAYSCALE }这里直接操作 protobuf 的imageType字段(宽度、高度、色彩空间),将ColorSpace设为GRAYSCALE。
4. 设置输入输出描述
为输入输出特征补充人类可读的描述:
neuralnetwork_spec.description.input[0].shortDescription = 'Input image of the handwriten digit to classify' neuralnetwork_spec.description.output[0].shortDescription = 'Probabilities / score for each possible digit' neuralnetwork_spec.description.output[1].shortDescription = 'Predicted digit'5. 提供元数据
neuralnetwork_spec.description.metadata.author = 'Core ML Tools' neuralnetwork_spec.description.metadata.license = 'MIT' neuralnetwork_spec.description.metadata.shortDescription = ( 'An updatable hand-written digit classifier setup to train or be fine-tuned on MNIST like data.')第四步:定义 make_updatable 改造函数
这是整个流程的核心。make_updatable()函数依次完成:标记可更新层、设置损失层、设置优化器、设置 epoch 数、配置训练输入描述,最后保存为新的可更新模型。
def make_updatable(builder, mlmodel_url, mlmodel_updatable_path): """This method makes an existing non-updatable mlmodel updatable. mlmodel_url - the path the Core ML model is stored. mlmodel_updatable_path - the path the updatable Core ML model will be saved. """ import coremltools model_spec = builder.spec # make_updatable method is used to make a layer updatable. It requires a list of layer names. # dense_1 and dense_2 are two innerProduct layer in this example and we make them updatable. builder.make_updatable(['dense_1', 'dense_2']) # Categorical Cross Entropy or Mean Squared Error can be chosen for the loss layer. # Categorical Cross Entropy is used on this example. CCE requires two inputs: 'name' and 'input'. # name must be a string and will be the name associated with the loss layer # input must be the output of a softmax layer in the case of CCE. # The loss's target will be provided automatically as a part of the model's training inputs. builder.set_categorical_cross_entropy_loss(name='lossLayer', input='digitProbabilities') # in addition of the loss layer, an optimizer must also be defined. SGD and Adam optimizers are supported. # SGD has been used for this example. To use SGD, one must set lr(learningRate) and batch(miniBatchSize) (momentum is an optional parameter). from coremltools.models.neural_network import SgdParams builder.set_sgd_optimizer(SgdParams(lr=0.01, batch=32)) # Finally, the number of epochs must be set as follows. builder.set_epochs(10) # Set training inputs descriptions model_spec.description.trainingInput[0].shortDescription = 'Example image of handwritten digit' model_spec.description.trainingInput[1].shortDescription = 'Associated true label (digit) of example image' # save the updated spec from coremltools.models import MLModel mlmodel_updatable = MLModel(model_spec) mlmodel_updatable.save(mlmodel_updatable_path) coreml_updatable_model_path = './UpdatableMNISTDigitClassifier.mlmodel' make_updatable(builder, coreml_model_path, coreml_updatable_model_path)make_updatable:标记可训练层
builder.make_updatable(['dense_1', 'dense_2'])接收层名列表,把这些层标记为可训练。从源码看(builder.py),该方法内部会依次执行:
- 检查模型中是否存在 FP16 权重——若有 FP16 权重则会抛出
ValueError,因为这类网络会被优化为全 FP16 格式,不支持标记为可更新(见 builder.py); - 将
self.spec.isUpdatable置为True; - 若当前
specificationVersion低于可更新模型的最低版本要求,则自动提升——该最低版本为_MINIMUM_UPDATABLE_SPEC_VERSION = 4,即 CoreML 3(见 coremltools/init.py); - 调用
set_shuffle()设置训练数据洗牌; - 遍历层名列表:校验层存在、类型属于
_SUPPORTED_UPDATABLE_LAYERS(仅innerProduct与convolution),随后把该层及其WeightParams权重字段全部标记为isUpdatable = True。
设置分类交叉熵损失
set_categorical_cross_entropy_loss(name='lossLayer', input='digitProbabilities'):
name:损失层的名字(字符串),本例为lossLayer;input:损失层的输入,在 CCE 场景下必须是某个 softmax 层的输出,本例为digitProbabilities。
从源码(builder.py)可以看到,该方法会自动生成训练目标:target = input + "_true",即digitProbabilities_true会被自动添加到模型的trainingInput中,作为 CCE 损失的真值输入——这正是后文验证输出中出现的Loss Target: digitProbabilities_true的来源。其数学形式为:
Loss_CCE(input, target) = -sum(i=1..N) (target == i) * log(input[i]) = -log(input[target])方法内部还会做两类校验(builder.py):
input必须是某个 softmax 层的输出,否则抛ValueError;target不能与模型现有输出重名。
测试用例test_nn_set_cce_invalid验证了输入非 softmax 输出时抛错的场景(见 test_model_updatable.py)。
补充:如果任务不是分类而是回归/逐点拟合,可改用set_mean_squared_error_loss(name, input_feature),其中input_feature是(名称, datatypes.Array)元组,描述损失所挂接的张量与形状(见 builder.py)。
设置 SGD 优化器
from coremltools.models.neural_network import SgdParams builder.set_sgd_optimizer(SgdParams(lr=0.01, batch=32))SgdParams定义在 update_optimizer_utils.py,参数如下:
| 参数 | 默认值 | 说明 |
|---|---|---|
lr | 0.01 | 学习率,控制学习步长,可在训练过程中调整 |
batch | 10 | mini-batch 大小,即计算单次梯度步所用的样本数 |
momentum | 0 | 动量因子,帮助梯度向量沿正确方向加速(可选) |
set_sgd_optimizer内部(builder.py)会把上述值写入 spec 的sgdOptimizer字段:学习率与动量写入defaultValue及range.minValue/maxValue,batch 写入defaultValue与set.values(即allowed_set)。
若选用 Adam 优化器,可使用AdamParams(update_optimizer_utils.py),其参数为lr=0.01、batch=10、beta1=0.9、beta2=0.999、eps=1e-8,并通过builder.set_adam_optimizer(AdamParams(...))设置(对应实现见 builder.py)。
设置训练轮数
builder.set_epochs(10)设置训练 epoch 数为 10。源码(builder.py)显示,该方法把epochs.defaultValue设为传入值,默认allowed_set只包含该值本身;也可传入allowed_set列表以提供更多可选 epoch 数(测试用例test_updatable_model_creation_ce_sgd中就使用了set_epochs(20, allowed_set=[10, 20, 30, 40]),见 test_model_updatable.py)。
保存可更新模型
最后用MLModel(model_spec)包装改造后的 spec,并通过mlmodel_updatable.save(mlmodel_updatable_path)保存为./UpdatableMNISTDigitClassifier.mlmodel。
第五步:验证模型已可更新
重新加载新保存的模型,用 builder 的inspect_*系列方法逐一验证。
1. 检查损失层
import coremltools spec = coremltools.utils.load_spec(coreml_updatable_model_path) builder = coremltools.models.neural_network.NeuralNetworkBuilder(spec=spec) builder.inspect_loss_layers()[Id: 0], Name: lossLayer (Type: categoricalCrossEntropyLossLayer) Loss Input: digitProbabilities Loss Target: digitProbabilities_true可以看到digitProbabilities_true已经自动成为 CCE 损失层的 target。inspect_loss_layers的实现同时支持 CCE 与 MSE 两类损失层的打印(见 builder.py)。
2. 检查优化器
builder.inspect_optimizer()Optimizer Type: sgdOptimizer lr: 0.01, min: 0.0, max: 1.0 batch: 32, allowed_set: [32L] momentum: 0.0, min: 0.0, max: 1.0输出确认优化器类型为sgdOptimizer,学习率 0.01、batch 32、动量 0.0 均与SgdParams(lr=0.01, batch=32)的设置一致(inspect_optimizer对 Adam 类型同样会打印 beta1、beta2、eps 等参数,见 builder.py)。
3. 检查可更新层
builder.inspect_updatable_layers()Name: dense_2 (Type: innerProduct) Input blobs: [u'dense_1__activation___output'] Output blobs: [u'dense_2_output'] Name: dense_1 (Type: innerProduct) Input blobs: [u'flatten_1_output'] Output blobs: [u'dense_1_output']inspect_updatable_layers只打印isUpdatable == True的层(见 builder.py)。本例中两个 innerProduct 层dense_1与dense_2均已被标记为可更新,改造成功。
在设备端使用可更新模型
可更新模型保存后,即可在 iOS/macOS 应用中使用 Apple 的MLUpdateTaskAPI 在设备端进行训练与个人化微调(on-device updates),核心概念参见 updatable-model-examples.rst。训练所需的输入即为模型自动生成的trainingInput:图像样本image与真值标签digitProbabilities_true。训练时MLUpdateTask会按照 spec 中配置的损失函数、优化器、batch 大小、epoch 数与数据洗牌设置迭代更新dense_1、dense_2两个全连接层的权重。
常见限制与注意事项
- 可更新层类型受限:目前只有
innerProduct(全连接)与convolution(卷积)两类层可被标记为可更新,其余层类型会在make_updatable时抛错(builder.py)。 - FP16 权重限制:若模型存在 FP16 格式的权重/偏置,无法标记为可更新;此时应避免 FP16
WeightParams,或把网络拆分为两个模型,仅将可更新部分单独建模(builder.py)。 - specification 版本:可更新模型要求最低 specification 版本为 4(CoreML 3),
make_updatable会自动提升版本(coremltools/init.py)。 - 损失层输入约束:CCE 损失层的输入必须是 softmax 层的输出;MSE 损失则需提供
(名称, 形状)元组描述目标张量。 - 版本兼容性:本指南中的 Keras 转换接口(
coremltools.converters.keras)属于神经网络转换路径,适用于与本仓库兼容的 Keras 版本;新版 coremltools 推荐使用统一的coremltools.convert与 MIL 前端进行转换,本示例重点在于演示可更新模型的改造机制本身。
总结
通过本示例,你可以掌握一条完整的技术路径:Keras 建模 → coremltools 转换为 Core ML → 直接操作 protobuf spec 调整输入类型与元数据 → 用make_updatable标记可更新层 → 配置 CCE 损失与 SGD 优化器 → 用inspect_*系列方法验证改造结果。这套机制使得模型可以在用户设备上持续接收新样本进行微调,实现真正意义上的设备端个人化。
- AI 应用
- 机器学习
- 模型优化
【免费下载链接】coremltools
Core ML tools contain supporting tools for Core ML model conversion, editing, and validation.
相关推荐
d2l-en 实战:从零与简洁实现多层感知机(MLP)——参数初始化、ReLU 前向与训练循环全解
d2l en 实战:从零与简洁实现多层感知机(MLP)——参数初始化、ReLU 前向与训练循环全解 多层感知机(Multilayer Perceptron,ML
教程文档示例工程使用PyTorch构建第一个神经网络:MNIST手写数字分类实战
使用PyTorch构建第一个神经网络:MNIST手写数字分类实战 前言 在机器学习领域,MNIST手写数字数据集堪称"Hello World"级别的入门项目。本
示例工程机器学习深度学习教程Carbone多语言支持:如何实现一个模板多种语言的国际化报告
Carbone多语言支持:如何实现一个模板多种语言的国际化报告 Carbone是一款快速且简单的报告生成器,能从JSON数据生成PDF、XLSX、DOCX、OD
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考