Конвертація ML-моделі в Core ML формат для iOS
Конвертування в .mlpackage з PyTorch або TensorFlow — це задача з конкретними кроками, кожен з яких може зломатися з конкретної причини. coremltools конвертує не все та не завжди правильно. Розглянемо реальний процес конвертації, включаючи те, що йде не так.
Підготовка моделі до конвертації
Перед конвертацією модель повинна бути в режимі eval з фіксованими вагами. torch.jit.trace вимагає прикладів входів — записує граф виконання для конкретного shape:
import torch
import coremltools as ct
model = MyModel()
model.load_state_dict(torch.load("weights.pth", map_location="cpu"))
model.eval()
# trace — фіксує граф для конкретного shape
example_input = torch.zeros(1, 3, 224, 224)
traced_model = torch.jit.trace(model, example_input)
# Для моделей з умовними гілками (if/else залежної від даних) — script:
# scripted_model = torch.jit.script(model) # аналізує код, не дані
torch.jit.trace не працює з динамічними гілками — якщо forward() містить if x.shape[1] > 512: ..., trace запам'ятовує тільки одну гілку. Використовуйте torch.jit.script, який аналізує весь код статично.
Конвертація через coremltools
mlmodel = ct.convert(
traced_model,
inputs=[ct.ImageType(
name="input",
shape=ct.Shape(shape=(1, 3, 224, 224)),
color_layout=ct.colorlayout.RGB,
# Нормалізація вбудована в модель — не потрібна в Swift
bias=[-0.485/0.229, -0.456/0.224, -0.406/0.225],
scale=1/(255.0 * 0.229)
)],
outputs=[ct.TensorType(name="logits")],
compute_precision=ct.precision.FLOAT16,
minimum_deployment_target=ct.target.iOS16,
convert_to="mlprogram" # новий формат .mlpackage
)
# Додайте метаданні
mlmodel.short_description = "Image classifier"
mlmodel.input_description["input"] = "RGB image 224x224"
mlmodel.output_description["logits"] = "Class probabilities"
mlmodel.save("MyModel.mlpackage")
convert_to="mlprogram" vs "neuralnetwork": mlprogram — новий IR, підтримує FP16 ANE операції, вимагає iOS 15+. neuralnetwork — старий формат, iOS 12+. Для нових проектів — mlprogram.
Поширені помилки конвертації
ValueError: TorchScript conversion failed — зазвичай через нестандартні операції. Перевірте через:
# Непідтримувані операції:
print(ct.converters.mil.frontend.torch.ops.PYTORCH_OPS_REGISTRY.keys())
# Якщо відсутня — потрібна користувацька реалізація
RuntimeError: Only tuple/list outputs are supported — модель повертає dict. Потрібна обгортка:
class ModelWrapper(torch.nn.Module):
def __init__(self, model):
super().__init__()
self.model = model
def forward(self, x):
out = self.model(x)
if isinstance(out, dict):
return out["logits"] # витяг потрібного ключа
return out
Unsupported op: aten::einsum — einsum у PyTorch. Переписуйте через стандартні matmul/bmm або додайте користувацьку операцію.
ONNX як промідний крок — коли прямої конвертації не працює:
# PyTorch → ONNX → Core ML
torch.onnx.export(model, example_input, "model.onnx", opset_version=17)
mlmodel = ct.converters.onnx.convert(
model="model.onnx",
minimum_ios_deployment_target="15.0"
)
Конвертація через ONNX іноді вирішує проблеми з непідтримуваними PyTorch операціями.
Верифікація коректності конвертації
import numpy as np
import PIL.Image
# Завантажте тестове зображення
img = PIL.Image.open("test.jpg").resize((224, 224))
# Оригінальний PyTorch вихід
transform = torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
tensor = transform(img).unsqueeze(0)
with torch.no_grad():
pytorch_out = model(tensor).numpy()
# Core ML вихід
coreml_out = mlmodel.predict({"input": img})["logits"]
# Порівняйте
max_diff = np.max(np.abs(pytorch_out - coreml_out))
print(f"Max difference: {max_diff}")
# FP16 норма: < 0.01
# Якщо > 0.05 — щось пішло не так при конвертації
Якщо різниця велика — перевірте нормалізацію: bias та scale в ct.ImageType можуть не відповідати нормалізації pytorch transform.
Змінні розміри входу
Для моделей, які працюють з зображеннями різних розмірів:
# Діапазон розмірів
flexible_shape = ct.Shape(
shape=(1, 3, ct.RangeDim(min_val=64, max_val=1024), ct.RangeDim(min_val=64, max_val=1024))
)
# Або набір конкретних розмірів
enumerated_shapes = ct.EnumeratedShapes(
shapes=[
ct.Shape(shape=(1, 3, 224, 224)),
ct.Shape(shape=(1, 3, 384, 384)),
ct.Shape(shape=(1, 3, 512, 512)),
]
)
mlmodel = ct.convert(traced_model, inputs=[ct.TensorType(name="input", shape=enumerated_shapes)])
EnumeratedShapes дозволяє Core ML попередньо оптимізувати граф для кожного розміру. RangeDim — більш гнучкий, але оптимізація гірша.
Користувацькі операції
Якщо модель містить операцію, яку coremltools не знає — додайте користувацький шар в Swift/Objective-C:
# Python: реєстрація користувацької операції
@ct.converters.mil.register_torch_op()
def my_custom_op(context, node):
# Реалізація через MIL операції
x = context[node.inputs[0]]
result = mb.custom(params={"...": "..."}, inputs={"x": x}, ...)
context.add(result)
// Swift: реалізація користувацького шару
import CoreML
@objc(MyCustomLayer)
class MyCustomLayer: NSObject, MLCustomLayer {
required init(parameters: [String: Any]) throws { }
func setWeightData(_ weights: [Data]) throws { }
func outputShapes(forInputShapes inputShapes: [[NSNumber]]) throws -> [[NSNumber]] { ... }
func evaluate(inputs: [MLMultiArray], outputs: [MLMultiArray]) throws { ... }
}
Користувацькі шари завжди виконуються на CPU, без ANE/GPU прискорення. Якщо користувацька операція — вузька місце інференсу, краще переписати модель зі стандартними операціями.
Інтеграція .mlpackage в Xcode
Файл .mlpackage додається в проект — Xcode автоматично генерує Swift-клас з typed API. Ім'я файлу MyModel.mlpackage → клас MyModel з методом prediction(input:).
Розмір .mlpackage в bundle впливає на час завантаження App Store та розмір завантаження додатку. Для великих моделей (>50 МБ) — розташовуйте поза bundle, завантажуйте при першому запуску через URLSession, зберігайте в Application Support.
Процес
Аудит сумісності моделі → конвертація з підбором параметрів → верифікація числової точності → тестування на цільових iOS-версіях → профілювання в Xcode Core ML Instrument.
Орієнтири за часом
Стандартна модель без нестандартних операцій — 3–7 днів. Модель зі складним графом, користувацькими операціями, змінними розмірами — 2–4 тижні.







