Проблеми конвертації в Core ML
Отримати .mlpackage з PyTorch або TensorFlow — задача з конкретними кроками, де кожен може зламатися з неочевидної причини. coremltools не підтримує всі операції — наприклад, einsum, згортки з динамічними розмірами або розгалуження в графі. Пряма конвертація часто видає помилку або призводить до падіння точності на 5–10 %. Ми накопичили досвід вирішення таких кейсів: за 5+ років роботи ми конвертували понад 50 моделей для фінансового сектора, рітейлу та AR-додатків. У середньому інференс на iPhone пришвидшується на 30–50 % після оптимізації під Apple Neural Engine. Замовте конвертацію — отримайте готовий .mlpackage з верифікацією точності.
Чому конвертація в Core ML потребує знань?
coremltools не підтримує всі операції PyTorch/TensorFlow, а для інших потрібне правильне налаштування параметрів. Пряма конвертація часто призводить до помилок або погіршення точності. Ми вирішуємо ці проблеми, підбираючи оптимальні параметри та при необхідності використовуючи кастомні шари.
Підготовка моделі до конвертації
Перед конвертацією модель має бути в eval-режимі з фіксованими вагами. torch.jit.trace вимагає прикладу вхідних даних — він записує граф для конкретного shape. Apple Core ML Tools рекомендують використовувати TorchScript для експорту:
import torch
import torchvision
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) використовуйте torch.jit.script:
# scripted_model = torch.jit.script(model)
# Конвертація в mlprogram
mlmodel = ct.convert(
traced_model,
inputs=[ct.ImageType(
name="input",
shape=ct.Shape(shape=(1, 3, 224, 224)),
color_layout=ct.colorlayout.RGB,
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"
)
mlmodel.short_description = "Image classifier"
mlmodel.input_description["input"] = "RGB image 224x224"
mlmodel.output_description["logits"] = "Class probabilities"
mlmodel.save("MyModel.mlpackage")
Якщо пряма конвертація не працює, використовуйте ONNX як проміжний крок: torch.onnx.export(model, example_input, "model.onnx", opset_version=17), потім ct.converters.onnx.convert.
Перевірка коректності конвертації
Порівняйте виходи оригінальної моделі та Core ML на тестовому зображенні:
import numpy as np
import PIL.Image
img = PIL.Image.open("test.jpg").resize((224, 224))
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()
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 — перевірте нормалізацію в ct.ImageType.
Налаштування змінних розмірів входу
Код для гнучких розмірів
# Діапазон розмірів
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)])
Кастомні операції
Якщо модель містить операцію, яку coremltools не знає, додайте кастомний шар на Swift. На Python реєструємо операцію:
@ct.converters.mil.register_torch_op()
def my_custom_op(context, node):
x = context[node.inputs[0]]
result = mb.custom(params={"...": "..."}, inputs={"x": x}, ...)
context.add(result)
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 — для продуктивності краще використовувати стандартні операції.
Порівняння продуктивності на пристроях
Apple Neural Engine пришвидшує інференс у 10–20 разів порівняно з CPU, що значно краще за GPU. Наприклад, для ResNet-50 mlprogram працює в 2 рази швидше за neuralnetwork на ANE. Результати для моделі ResNet-50 (224x224):
| Пристрій | CPU (ms) | GPU (ms) | ANE (ms) |
|---|---|---|---|
| iPhone 14 Pro | 45 | 30 | 12 |
| iPhone 13 | 60 | 40 | 18 |
| iPhone SE (3rd gen) | 90 | 65 | 30 |
Порівняння форматів Core ML
| Параметр | mlprogram | neuralnetwork |
|---|---|---|
| iOS версія | 15+ | 12+ |
| ANE підтримка | Так | Ні |
| FP16 | Так | Тільки FP32 |
| Розмір | Менше | Більше |
| Продуктивність | Вища | Нижча |
Що входить у роботу з конвертації?
- Аудит моделі — аналіз графа, виявлення несумісних операцій, рекомендації щодо рефакторингу. Перевірка на підтримку ANE.
- Конвертація — підбір точності (FP16/INT8), розміру входу, вирішення помилок coremltools. За потреби — кастомні шари.
- Верифікація точності — порівняння на 100+ тестових прикладах, звіт з max diff та метриками (accuracy, mAP).
- Оптимізація під ANE — заміна Reshape/Permute на ANE-сумісні, усунення вузьких місць, квантування.
- Документація — опис параметрів моделі, інтеграції в Xcode, приклад використання.
Чек-лист конвертації
- [ ] Модель в eval-режимі, ваги заморожені
- [ ] Приклад вхідних даних підготовлений
- [ ] Обраний format mlprogram (якщо iOS >= 15)
- [ ] Перевірена підтримка операцій через
PYTORCH_OPS_REGISTRY - [ ] Виконана числова верифікація (max diff < 0.01)
- [ ] Протестовано на цільових пристроях (CPU/GPU/ANE)
- [ ] Результати порівняння часу інференсу задокументовані
Квантування: INT8 vs FP16
Окрім конвертації в mlprogram, квантування дозволяє додатково зменшити розмір моделі та прискорити інференс. FP16 (float16) скорочує обсяг ваг удвічі без помітної втрати точності — це наш стандарт за замовчуванням. INT8 зменшує модель ще в 2 рази, але потребує калібрувального датасету для оцінки похибки квантування. На Apple Neural Engine INT8 працює особливо швидко: приріст швидкості становить 1,5–2× порівняно з FP16. Для задач класифікації та детекції об'єктів втрата точності при INT8 зазвичай не перевищує 1–2%. Для задач з текстом або генерацією — FP16 переважніший.
# Квантування в INT8 через coremltools
mlmodel_int8 = ct.compression_utils.affine_quantize_weights(mlmodel, mode="linear_symmetric")
mlmodel_int8.save("MyModel_INT8.mlpackage")
Наша команда проводить порівняльне тестування обох форматів на вашому конкретному пристрої та обирає оптимальний варіант. Вартість квантування включена у вартість конвертації — загальна ціна починається від 500$.
Отримайте консультацію інженера по вашій моделі — оцінимо складність конвертації та підберемо оптимальні параметри. Ми гарантуємо якість та сертифікований досвід. Звертайтесь до нас для аудиту та конвертації.







