המרת מודלי ML ל-Core ML עבור iOS
בעיות בהמרה ל-Core ML
קבלת קובץ .mlpackage מ-PyTorch או TensorFlow כוללת שלבים ספציפיים, וכל אחד מהם עלול להיכשל מסיבות לא ברורות. הכלי coremltools (כלי ההמרה הרשמי) של Apple אינו תומך בכל הפעולות — לדוגמה, einsum, קונבולוציות עם גדלים דינמיים, או ענפים בגרף. המרה ישירה גורמת לעיתים קרובות לשגיאות או לירידה של 5–10% בדיוק. צברנו ניסיון בפתרון מקרים כאלה: במשך למעלה מ-5 שנות עבודה, המרנו יותר מ-50 מודלים למגזר הפיננסי, לקמעונאות וליישומי AR. בממוצע, ההסקה באייפון מואצת ב-30–50% לאחר אופטימיזציה ל-Apple Neural Engine — וזה טוב משמעותית מכלי ההמרה הסטנדרטיים. החל מ-$200, ההשקעה מחזירה את עצמה לעיתים קרובות תוך שבועות בזכות שיפור חוויית המשתמש והפחתת עלויות השרת. לדוגמה, לקוח קמעונאי אחד הפחית את עלויות השרת ב-$500 לחודש לאחר המרת מודל ההמלצות שלהם. עלות ההמרה נעה בין $200 ל-$1000 בהתאם למורכבות המודל. הזמינו המרה וקבלו קובץ .mlpackage מוכן עם אימות דיוק.
מדוע המרה ל-Core ML דורשת מומחיות
כלי ההמרה אינו תומך בכל פעולות PyTorch/TensorFlow, ואחרות דורשות הגדרת פרמטרים נכונה. המרה ישירה מובילה לעיתים קרובות לשגיאות או לפגיעה בדיוק. אנו פותרים בעיות אלה על ידי בחירת פרמטרים אופטימליים ושימוש בפעולות מותאמות אישית בעת הצורך.
הכנת המודל להמרה
לפני ההמרה, המודל חייב להיות במצב eval עם משקלים קפואים. torch.jit.trace דורש קלט לדוגמה — הוא מתעד את הגרף עבור צורה ספציפית:
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 כשלב ביניים: 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") , ולאחר מכן 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}") .
אימות נכונות ההמרה
כדי להבטיח נאמנות, השוו את הפלטים של המודל המקורי ושל 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)]) עם @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 { } } עבור ממדי קלט גמישים. לדוגמה:
# Диапазон размеров
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, רשמו את הפעולה באמצעות PYTORCH_OPS_REGISTRY. דוגמה למימוש ב-Swift:
@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)שכבה מותאמת אישית רצה על CPU — לביצועים, העדיפו פעולות סטנדרטיות. בפרויקט אחד, שכבת attention מותאמת אישית דרשה עטיפת המודל כדי להחליף את einops בפעולות סטנדרטיות — פתרנו זאת על ידי tracing עם אורך רצף קבוע.
השוואת ביצועים במכשירים
ה-Apple Neural Engine מאיץ את ההסקה פי 10–20 בהשוואה ל-CPU. תוצאות עבור ResNet-50 (224x224):
| מכשיר | CPU (ms) | GPU (ms) | ANE (ms) |
|---|---|---|---|
| iPhone 14 Pro | 45 | 30 | 12 |
| iPhone 13 | 60 | 40 | 18 |
| iPhone SE (דור 3) | 90 | 65 | 30 |
השוואת פורמטי Core ML
פרטי פורמט (mlprogram לעומת neuralnetwork)
| פרמטר | mlprogram | neuralnetwork |
|---|---|---|
| גרסת iOS | 15+ | 12+ |
| תמיכה ב-ANE | כן | לא |
| FP16 | כן | FP32 בלבד |
| גודל | קטן יותר | גדול יותר |
| ביצועים | גבוהים יותר | נמוכים יותר |
מה כלול בעבודת ההמרה?
- ביקורת מודל — ניתוח גרף, זיהוי פעולות לא תואמות, המלצות ל-refactoring. בדיקת תמיכה ב-ANE.
- המרה — בחירת דיוק (FP16/INT8), גודל קלט, פתרון שגיאות. פעולות מותאמות אישית במידת הצורך.
- אימות דיוק — השוואה על 100+ דגימות בדיקה, דוח עם הפרש מקסימלי ומדדים (accuracy, mAP).
- אופטימיזציה ל-ANE — החלפת Reshape/Permute בפעולות תואמות ANE, הסרת צווארי בקבוק, קוונטיזציה.
- תיעוד — תיאור פרמטרי המודל, אינטגרציה ב-Xcode, דוגמת שימוש.
הלקוחות שלנו רואים בדרך כלל הפחתת עלויות שרת של $500–$2000 לחודש. מחיר ההמרה הממוצע נע בין $200 ל-$1000 בהתאם למורכבות.
תהליך ההמרה: שלב אחר שלב
- ייצאו את המודל ל-TorchScript באמצעות
# Квантование в INT8 через coremltools mlmodel_int8 = ct.compression_utils.affine_quantize_weights(mlmodel, mode="linear_symmetric") mlmodel_int8.save("MyModel_INT8.mlpackage")אוtorch.jit.script. ודאו שהמודל במצב eval עם משקלים קפואים. - הריצו המרה עם סוגי קלט/פלט מתאימים, דיוק (FP16 מומלץ) ויעד פריסה.
- בצעו אימות נומרי על ידי השוואת פלטים על קבוצת בדיקה מייצגת. ודאו שההפרש המקסימלי < 0.01.
- אם מתרחשות שגיאות, זהו פעולות לא נתמכות והחליפו אותן או השתמשו ב-ONNX כשלב ביניים.
- לאחר המרה מוצלחת, בצעו קוונטיזציה ל-INT8 אם נדרשת האצה נוספת, ולאחר מכן בדקו על מכשירי היעד.
קוונטיזציה: INT8 לעומת FP16
בנוסף להמרה ל-mlprogram, קוונטיזציה מפחיתה עוד יותר את גודל המודל ומאיצה את ההסקה. FP16 מקטין את נפח המשקלים בחצי עם אובדן דיוק זניח — זהו התקן המוגדר כברירת מחדל שלנו. INT8 מקטין את המודל בעוד פי 2 אך דורש מערך כיול להערכת שגיאת הקוונטיזציה. ב-Apple Neural Engine, INT8 מהיר במיוחד: רווח המהירות הוא פי 1.5–2 בהשוואה ל-FP16. עבור משימות סיווג וזיהוי עצמים, אובדן הדיוק של INT8 בדרך כלל אינו עולה על 1–2%. עבור משימות טקסט או יצירה, FP16 עדיף.
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 {
}
}הצוות שלנו מבצע בדיקות השוואתיות של שני הפורמטים על המכשיר הספציפי שלכם ובוחר את האפשרות האופטימלית. עלות הקוונטיזציה כלולה במחיר ההמרה (החל מ-$200) ואינה מחויבת בנפרד.
הצוות שלנו סיפק בהצלחה המרות Core ML ליותר מ-20 לקוחות בתחום הפיננסים והקמעונאות. אנו מבטיחים שהדיוק לא יפגע ביותר מ-0.01 הפרש מקסימלי בהשוואה למודל המקורי. קבלו ייעוץ ממהנדס לגבי המודל שלכם — נעריך את מורכבות ההמרה ונבחר פרמטרים אופטימליים. צרו קשר לביקורת והמרה.







