Vision Framework et CoreML : questions d'entretien ML on-device iOS
Préparez vos entretiens iOS avec les questions essentielles sur Vision Framework et CoreML : reconnaissance d'images, détection d'objets et ML on-device.

Le machine learning on-device représente un avantage compétitif majeur pour les applications iOS modernes. Vision Framework et CoreML permettent d'exécuter des modèles directement sur l'appareil, garantissant confidentialité des données et performance en temps réel. Ces questions d'entretien couvrent les concepts essentiels que tout développeur iOS senior doit maîtriser.
Les questions sont organisées par thème : fondamentaux CoreML, Vision Framework, optimisation des performances, et cas pratiques. Chaque réponse inclut du code Swift moderne et des explications détaillées.
Fondamentaux CoreML
1. Qu'est-ce que CoreML et quels sont ses avantages ?
CoreML est le framework d'Apple pour intégrer des modèles de machine learning dans les applications iOS, macOS, watchOS et tvOS. Il optimise automatiquement les modèles pour le matériel Apple (CPU, GPU, Neural Engine) et garantit l'exécution on-device sans connexion réseau.
Les principaux avantages incluent la confidentialité des données (aucune donnée ne quitte l'appareil), la latence réduite (pas de round-trip réseau), et l'optimisation automatique pour le Neural Engine des puces Apple Silicon.
import CoreML
// Chargement d'un modèle CoreML compilé (.mlmodelc)
class ImageClassifier {
// Le modèle est compilé à la build time pour optimiser le chargement
private let model: VNCoreMLModel
init() throws {
// Configuration pour utiliser le Neural Engine si disponible
let config = MLModelConfiguration()
config.computeUnits = .all // CPU + GPU + Neural Engine
// Chargement du modèle avec configuration personnalisée
let mlModel = try MobileNetV2(configuration: config).model
model = try VNCoreMLModel(for: mlModel)
}
// Méthode pour classifier une image
func classify(image: CGImage) async throws -> [(String, Float)] {
// Création de la requête Vision avec le modèle CoreML
let request = VNCoreMLRequest(model: model)
request.imageCropAndScaleOption = .centerCrop
// Handler pour traiter l'image
let handler = VNImageRequestHandler(cgImage: image, options: [:])
try handler.perform([request])
// Extraction des résultats
guard let results = request.results as? [VNClassificationObservation] else {
return []
}
// Retourne les 5 meilleures prédictions avec leur confiance
return results.prefix(5).map { ($0.identifier, $0.confidence) }
}
}2. Comment convertir un modèle TensorFlow ou PyTorch vers CoreML ?
La conversion utilise coremltools, un package Python officiel d'Apple. Il supporte TensorFlow, PyTorch, ONNX et d'autres formats populaires. La conversion peut inclure des optimisations comme la quantification pour réduire la taille du modèle.
# convert_model.py
import coremltools as ct
import torch
# Conversion depuis PyTorch
class MyClassifier(torch.nn.Module):
def __init__(self):
super().__init__()
self.conv = torch.nn.Conv2d(3, 64, 3)
self.fc = torch.nn.Linear(64, 10)
def forward(self, x):
x = self.conv(x)
x = x.mean([2, 3]) # Global average pooling
return self.fc(x)
# Exemple d'entrée pour le tracing
example_input = torch.rand(1, 3, 224, 224)
# Trace le modèle PyTorch
traced_model = torch.jit.trace(MyClassifier(), example_input)
# Conversion vers CoreML avec métadonnées
mlmodel = ct.convert(
traced_model,
inputs=[ct.ImageType(name="image", shape=(1, 3, 224, 224))],
classifier_config=ct.ClassifierConfig(["cat", "dog", "bird"]),
minimum_deployment_target=ct.target.iOS17
)
# Sauvegarde du modèle avec compression
mlmodel.save("MyClassifier.mlpackage")Le modèle .mlpackage peut ensuite être ajouté directement au projet Xcode, qui génère automatiquement une classe Swift typée.
3. Quelle est la différence entre MLModel et VNCoreMLModel ?
MLModel est la classe de base CoreML pour charger et exécuter des modèles ML. VNCoreMLModel est un wrapper qui permet d'utiliser un modèle CoreML avec Vision Framework, offrant un preprocessing automatique des images et une intégration avec les pipelines Vision.
import CoreML
import Vision
// Utilisation directe de MLModel (bas niveau)
func predictWithMLModel(features: MLFeatureProvider) async throws -> String {
let config = MLModelConfiguration()
let model = try MyModel(configuration: config)
// Prédiction directe avec un feature provider
let prediction = try model.prediction(from: features)
// Accès manuel aux outputs
guard let output = prediction.featureValue(for: "classLabel")?.stringValue else {
throw PredictionError.invalidOutput
}
return output
}
// Utilisation avec VNCoreMLModel (haut niveau, recommandé pour images)
func predictWithVision(image: CGImage) async throws -> [VNClassificationObservation] {
let config = MLModelConfiguration()
let mlModel = try MyModel(configuration: config).model
// Wrapper pour utiliser avec Vision
let visionModel = try VNCoreMLModel(for: mlModel)
// Vision gère automatiquement le redimensionnement et le preprocessing
let request = VNCoreMLRequest(model: visionModel)
request.imageCropAndScaleOption = .scaleFill
let handler = VNImageRequestHandler(cgImage: image)
try handler.perform([request])
return request.results as? [VNClassificationObservation] ?? []
}MLModel direct pour des données tabulaires ou des entrées non-image. VNCoreMLModel pour tout ce qui implique des images, car Vision gère automatiquement les conversions de format et le preprocessing.
4. Comment gérer les différentes versions iOS avec CoreML ?
CoreML évolue avec chaque version iOS. Il est essentiel de définir une cible de déploiement minimale lors de la conversion et de gérer les fonctionnalités non disponibles sur les anciennes versions.
import CoreML
class AdaptiveMLManager {
// Vérifie les capacités du modèle selon la version iOS
func loadOptimalModel() throws -> MLModel {
let config = MLModelConfiguration()
// iOS 17+ : Neural Engine optimisé avec compute budget
if #available(iOS 17, *) {
config.computeUnits = .cpuAndNeuralEngine
// Nouveau en iOS 17 : limite de puissance de calcul
config.allowLowPrecisionAccumulationOnGPU = true
return try AdvancedModel(configuration: config).model
}
// iOS 16 : support GPU amélioré
else if #available(iOS 16, *) {
config.computeUnits = .all
return try StandardModel(configuration: config).model
}
// iOS 15 : fallback CPU uniquement pour fiabilité
else {
config.computeUnits = .cpuOnly
return try LegacyModel(configuration: config).model
}
}
// Vérifie si le Neural Engine est disponible
var hasNeuralEngine: Bool {
if #available(iOS 16, *) {
// Les appareils avec A11+ ont le Neural Engine
var sysinfo = utsname()
uname(&sysinfo)
let machine = String(bytes: Data(bytes: &sysinfo.machine,
count: Int(_SYS_NAMELEN)), encoding: .ascii)?
.trimmingCharacters(in: .controlCharacters) ?? ""
// iPhone X et ultérieurs ont le Neural Engine
return machine.contains("iPhone10") ||
machine.hasPrefix("iPhone1") && machine.count > 7
}
return false
}
}Vision Framework
5. Quels types de requêtes Vision Framework supporte-t-il ?
Vision Framework offre une large gamme de requêtes pour l'analyse d'images. Les principales catégories incluent la détection de visages, la reconnaissance de texte (OCR), la détection d'objets, le suivi d'objets vidéo, et l'analyse de similarité d'images.
import Vision
class VisionAnalyzer {
// Détection de visages avec landmarks
func detectFaces(in image: CGImage) async throws -> [VNFaceObservation] {
let request = VNDetectFaceLandmarksRequest()
request.revision = VNDetectFaceLandmarksRequestRevision3
let handler = VNImageRequestHandler(cgImage: image)
try handler.perform([request])
return request.results ?? []
}
// Reconnaissance de texte (OCR)
func recognizeText(in image: CGImage) async throws -> [String] {
let request = VNRecognizeTextRequest()
request.recognitionLevel = .accurate // .fast pour temps réel
request.recognitionLanguages = ["fr-FR", "en-US"]
request.usesLanguageCorrection = true
let handler = VNImageRequestHandler(cgImage: image)
try handler.perform([request])
return request.results?.compactMap { observation in
observation.topCandidates(1).first?.string
} ?? []
}
// Détection et classification d'objets
func detectObjects(in image: CGImage) async throws -> [VNRecognizedObjectObservation] {
// Utilise un modèle CoreML pour la détection
let config = MLModelConfiguration()
let detector = try YOLOv8(configuration: config)
let visionModel = try VNCoreMLModel(for: detector.model)
let request = VNCoreMLRequest(model: visionModel)
request.imageCropAndScaleOption = .scaleFill
let handler = VNImageRequestHandler(cgImage: image)
try handler.perform([request])
return request.results as? [VNRecognizedObjectObservation] ?? []
}
// Calcul de similarité entre images
func computeSimilarity(image1: CGImage, image2: CGImage) async throws -> Float {
// Génère les feature prints des deux images
let request = VNGenerateImageFeaturePrintRequest()
let handler1 = VNImageRequestHandler(cgImage: image1)
try handler1.perform([request])
guard let print1 = request.results?.first else { throw VisionError.noResults }
let handler2 = VNImageRequestHandler(cgImage: image2)
try handler2.perform([request])
guard let print2 = request.results?.first else { throw VisionError.noResults }
// Calcule la distance entre les deux embeddings
var distance: Float = 0
try print1.computeDistance(&distance, to: print2)
// Convertit la distance en score de similarité (0-1)
return 1.0 / (1.0 + distance)
}
}6. Comment implémenter le suivi d'objets en temps réel avec Vision ?
Le suivi d'objets utilise VNTrackObjectRequest pour suivre un objet détecté à travers les frames d'une vidéo. L'initialisation se fait avec une observation de détection, puis les frames suivantes utilisent la même requête pour le suivi.
import Vision
import AVFoundation
class ObjectTracker: NSObject {
private var trackingRequest: VNTrackObjectRequest?
private let sequenceHandler = VNSequenceRequestHandler()
// Callback pour notifier les mises à jour de position
var onTrackingUpdate: ((CGRect) -> Void)?
var onTrackingLost: (() -> Void)?
// Initialise le suivi avec une détection initiale
func startTracking(observation: VNDetectedObjectObservation) {
// Crée la requête de suivi à partir de l'observation
trackingRequest = VNTrackObjectRequest(
detectedObjectObservation: observation
) { [weak self] request, error in
self?.handleTrackingResult(request: request, error: error)
}
// Configuration du suivi
trackingRequest?.trackingLevel = .accurate // .fast pour 60fps
}
// Traite chaque nouvelle frame vidéo
func processFrame(_ pixelBuffer: CVPixelBuffer) {
guard let request = trackingRequest else { return }
do {
// Le sequence handler maintient le contexte entre les frames
try sequenceHandler.perform([request], on: pixelBuffer)
} catch {
onTrackingLost?()
trackingRequest = nil
}
}
private func handleTrackingResult(request: VNRequest, error: Error?) {
guard let result = request.results?.first as? VNDetectedObjectObservation else {
onTrackingLost?()
return
}
// Vérifie la confiance du suivi
if result.confidence < 0.3 {
onTrackingLost?()
trackingRequest = nil
return
}
// Met à jour la requête pour la prochaine frame
trackingRequest = VNTrackObjectRequest(detectedObjectObservation: result) {
[weak self] request, error in
self?.handleTrackingResult(request: request, error: error)
}
// Notifie la nouvelle position (coordonnées normalisées)
DispatchQueue.main.async { [weak self] in
self?.onTrackingUpdate?(result.boundingBox)
}
}
}
// Intégration avec AVCaptureSession
extension ObjectTracker: AVCaptureVideoDataOutputSampleBufferDelegate {
func captureOutput(
_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return
}
processFrame(pixelBuffer)
}
}Prêt à réussir tes entretiens iOS ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
7. Comment optimiser les performances de Vision pour le temps réel ?
L'optimisation passe par plusieurs techniques : utiliser le bon niveau de reconnaissance, traiter les frames sur une queue dédiée, et limiter le nombre de requêtes simultanées. Le choix entre précision et vitesse dépend du cas d'usage.
import Vision
import AVFoundation
class OptimizedVisionPipeline {
// Queue dédiée pour le traitement Vision (évite le main thread)
private let processingQueue = DispatchQueue(
label: "com.app.vision",
qos: .userInteractive,
attributes: .concurrent
)
// Limite le nombre de frames traitées simultanément
private let semaphore = DispatchSemaphore(value: 2)
// Réutilise les requêtes pour éviter les allocations
private lazy var textRequest: VNRecognizeTextRequest = {
let request = VNRecognizeTextRequest()
request.recognitionLevel = .fast // .accurate si précision > vitesse
request.usesLanguageCorrection = false // Désactive pour +20% perf
request.minimumTextHeight = 0.05 // Ignore le texte trop petit
return request
}()
// Réutilise le handler de séquence pour le suivi
private let sequenceHandler = VNSequenceRequestHandler()
// Traitement optimisé d'une frame
func processFrame(_ pixelBuffer: CVPixelBuffer) {
// Skip si le pipeline est saturé
guard semaphore.wait(timeout: .now()) == .success else {
return // Drop la frame plutôt que bloquer
}
processingQueue.async { [weak self] in
defer { self?.semaphore.signal() }
guard let self = self else { return }
do {
// Utilise le sequence handler pour de meilleures performances
try self.sequenceHandler.perform(
[self.textRequest],
on: pixelBuffer,
orientation: .up
)
// Traite les résultats
if let results = self.textRequest.results {
self.handleResults(results)
}
} catch {
print("Vision error: \(error)")
}
}
}
// Batch processing pour les images statiques
func processImages(_ images: [CGImage]) async throws -> [[VNObservation]] {
// Traitement parallèle avec TaskGroup
try await withThrowingTaskGroup(of: (Int, [VNObservation]).self) { group in
for (index, image) in images.enumerated() {
group.addTask {
let handler = VNImageRequestHandler(cgImage: image)
let request = VNDetectFaceRectanglesRequest()
try handler.perform([request])
return (index, request.results ?? [])
}
}
// Collecte les résultats dans l'ordre original
var results = [[VNObservation]](repeating: [], count: images.count)
for try await (index, observations) in group {
results[index] = observations
}
return results
}
}
private func handleResults(_ results: [VNRecognizedTextObservation]) {
// Traitement asynchrone des résultats
}
}8. Comment implémenter la détection de pose humaine avec Vision ?
Vision Framework iOS 14+ offre VNDetectHumanBodyPoseRequest pour détecter les articulations du corps. Cette fonctionnalité est utilisée pour les applications fitness, les jeux AR, et l'analyse de mouvements.
import Vision
struct DetectedPose {
let joints: [VNHumanBodyPoseObservation.JointName: CGPoint]
let confidence: Float
// Calcule l'angle entre trois articulations
func angleBetween(
_ joint1: VNHumanBodyPoseObservation.JointName,
_ joint2: VNHumanBodyPoseObservation.JointName,
_ joint3: VNHumanBodyPoseObservation.JointName
) -> Double? {
guard let p1 = joints[joint1],
let p2 = joints[joint2],
let p3 = joints[joint3] else { return nil }
let v1 = CGVector(dx: p1.x - p2.x, dy: p1.y - p2.y)
let v2 = CGVector(dx: p3.x - p2.x, dy: p3.y - p2.y)
let dot = v1.dx * v2.dx + v1.dy * v2.dy
let mag1 = sqrt(v1.dx * v1.dx + v1.dy * v1.dy)
let mag2 = sqrt(v2.dx * v2.dx + v2.dy * v2.dy)
return acos(dot / (mag1 * mag2)) * 180 / .pi
}
}
class PoseDetector {
private let request = VNDetectHumanBodyPoseRequest()
func detectPose(in image: CGImage) async throws -> DetectedPose? {
let handler = VNImageRequestHandler(cgImage: image)
try handler.perform([request])
guard let observation = request.results?.first else { return nil }
// Extrait toutes les articulations détectées
var joints: [VNHumanBodyPoseObservation.JointName: CGPoint] = [:]
// Liste des articulations principales
let jointNames: [VNHumanBodyPoseObservation.JointName] = [
.nose, .neck,
.leftShoulder, .rightShoulder,
.leftElbow, .rightElbow,
.leftWrist, .rightWrist,
.leftHip, .rightHip,
.leftKnee, .rightKnee,
.leftAnkle, .rightAnkle
]
for jointName in jointNames {
if let point = try? observation.recognizedPoint(jointName),
point.confidence > 0.3 {
// Convertit les coordonnées normalisées en points
joints[jointName] = CGPoint(x: point.x, y: point.y)
}
}
return DetectedPose(
joints: joints,
confidence: observation.confidence
)
}
// Détecte si la personne fait un squat
func isSquatting(pose: DetectedPose) -> Bool {
guard let kneeAngle = pose.angleBetween(
.leftHip, .leftKnee, .leftAnkle
) else { return false }
// Un squat a typiquement un angle au genou < 100°
return kneeAngle < 100
}
}Optimisation et Production
9. Comment quantifier un modèle CoreML pour réduire sa taille ?
La quantification réduit la précision des poids (de Float32 à Float16 ou Int8) pour diminuer la taille du modèle et accélérer l'inférence. Le trade-off est une légère perte de précision.
# quantize_model.py
import coremltools as ct
from coremltools.models.neural_network import quantization_utils
# Charge le modèle existant
model = ct.models.MLModel("MyModel.mlpackage")
# Quantification Float16 (recommandé, bonne balance taille/précision)
model_fp16 = ct.models.neural_network.quantization_utils.quantize_weights(
model,
nbits=16,
quantization_mode="linear"
)
model_fp16.save("MyModel_FP16.mlpackage")
# Quantification Int8 (plus petite taille, perte de précision possible)
# Nécessite un dataset de calibration pour de meilleurs résultats
def calibration_data():
import numpy as np
for _ in range(100):
yield {"image": np.random.rand(1, 3, 224, 224).astype(np.float32)}
model_int8 = ct.compression_utils.affine_quantize_weights(
model,
mode="linear_symmetric",
dtype=ct.converters.mil.mil.types.int8
)
model_int8.save("MyModel_INT8.mlpackage")import CoreML
class ModelBenchmark {
// Compare les performances des différentes versions
func benchmark() async throws {
let configs: [(String, URL)] = [
("Full Precision", Bundle.main.url(forResource: "Model", withExtension: "mlmodelc")!),
("Float16", Bundle.main.url(forResource: "Model_FP16", withExtension: "mlmodelc")!),
("Int8", Bundle.main.url(forResource: "Model_INT8", withExtension: "mlmodelc")!)
]
for (name, url) in configs {
let model = try MLModel(contentsOf: url)
// Mesure le temps d'inférence moyen sur 100 itérations
let startTime = CFAbsoluteTimeGetCurrent()
for _ in 0..<100 {
let input = try prepareInput()
_ = try model.prediction(from: input)
}
let elapsed = CFAbsoluteTimeGetCurrent() - startTime
// Taille du modèle
let size = try FileManager.default.attributesOfItem(atPath: url.path)[.size] as? Int ?? 0
print("\(name): \(elapsed/100*1000)ms/inference, \(size/1024/1024)MB")
}
}
private func prepareInput() throws -> MLFeatureProvider {
// Prépare un input de test
fatalError("Implement based on model requirements")
}
}10. Comment gérer la mémoire lors du traitement de grandes images ?
Le traitement d'images haute résolution peut causer des pics mémoire. Les techniques incluent le downsampling intelligent, le traitement par tuiles, et la libération proactive des ressources.
import Vision
import CoreImage
class MemoryEfficientProcessor {
// Context CoreImage réutilisable pour éviter les allocations
private let ciContext = CIContext(options: [
.useSoftwareRenderer: false,
.cacheIntermediates: false // Réduit l'usage mémoire
])
// Downsample intelligent d'une grande image
func downsampleImage(at url: URL, to maxDimension: CGFloat) -> CGImage? {
// Options pour le downsampling à la lecture (évite de charger l'image entière)
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimension,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceShouldCacheImmediately: false
]
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil),
let image = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
return nil
}
return image
}
// Traitement par tuiles pour les très grandes images
func processByTiles(
image: CGImage,
tileSize: CGSize,
processor: (CGImage) throws -> [VNObservation]
) throws -> [VNObservation] {
var allObservations: [VNObservation] = []
let imageWidth = CGFloat(image.width)
let imageHeight = CGFloat(image.height)
// Parcourt l'image par tuiles
var y: CGFloat = 0
while y < imageHeight {
var x: CGFloat = 0
while x < imageWidth {
// Calcule le rectangle de la tuile
let tileRect = CGRect(
x: x, y: y,
width: min(tileSize.width, imageWidth - x),
height: min(tileSize.height, imageHeight - y)
)
// Extrait la tuile
autoreleasepool {
if let tile = image.cropping(to: tileRect) {
do {
let observations = try processor(tile)
// Ajuste les coordonnées relatives à l'image complète
let adjusted = observations.compactMap { obs -> VNObservation? in
guard let detected = obs as? VNDetectedObjectObservation else {
return obs
}
// Recalcule le bounding box en coordonnées globales
var box = detected.boundingBox
box.origin.x = (box.origin.x * tileRect.width + x) / imageWidth
box.origin.y = (box.origin.y * tileRect.height + y) / imageHeight
box.size.width = box.size.width * tileRect.width / imageWidth
box.size.height = box.size.height * tileRect.height / imageHeight
return detected
}
allObservations.append(contentsOf: adjusted)
} catch {
print("Tile processing error: \(error)")
}
}
}
x += tileSize.width * 0.9 // Overlap de 10% pour éviter de couper les objets
}
y += tileSize.height * 0.9
}
return allObservations
}
}Toujours utiliser autoreleasepool dans les boucles de traitement d'images et vérifier les retain cycles dans les closures des requêtes Vision.
11. Comment implémenter un pipeline ML avec Create ML Components ?
Create ML Components (iOS 16+) permet de créer des pipelines ML modulaires avec des transformateurs prédéfinis. C'est plus flexible que les modèles monolithiques traditionnels.
import CreateMLComponents
import CoreImage
@available(iOS 16.0, *)
class MLPipeline {
// Pipeline de classification d'images avec preprocessing
func createImageClassificationPipeline() throws -> some Transformer<CGImage, String> {
// Composition de transformateurs
let pipeline = ImageReader()
.appending(ImageScaler(targetSize: .init(width: 224, height: 224)))
.appending(ImageNormalizer(mean: [0.485, 0.456, 0.406],
std: [0.229, 0.224, 0.225]))
.appending(try ImageFeaturePrint())
.appending(try NearestNeighborClassifier<String>
.load(from: trainingDataURL))
return pipeline
}
// Pipeline personnalisé avec étapes custom
func createCustomPipeline() -> some Transformer<CIImage, AnalysisResult> {
// Étape 1: Prétraitement
let preprocess = CIImageTransformer { image in
// Applique des filtres CoreImage
let adjusted = image
.applyingFilter("CIColorControls", parameters: [
kCIInputContrastKey: 1.2,
kCIInputSaturationKey: 1.1
])
return adjusted
}
// Étape 2: Détection
let detect = VisionTransformer<CIImage, [VNFaceObservation]> { image in
let request = VNDetectFaceRectanglesRequest()
let handler = VNImageRequestHandler(ciImage: image)
try handler.perform([request])
return request.results ?? []
}
// Étape 3: Analyse
let analyze = ResultTransformer<[VNFaceObservation], AnalysisResult> { faces in
AnalysisResult(
faceCount: faces.count,
averageConfidence: faces.map(\.confidence).reduce(0, +) / Float(faces.count)
)
}
return preprocess
.appending(detect)
.appending(analyze)
}
}
struct AnalysisResult {
let faceCount: Int
let averageConfidence: Float
}12. Comment tester et valider un modèle CoreML ?
Les tests incluent la validation de la précision, des tests de performance, et des tests d'intégration. Il est crucial de tester sur différents appareils et conditions.
import XCTest
import CoreML
import Vision
class CoreMLModelTests: XCTestCase {
var model: VNCoreMLModel!
override func setUpWithError() throws {
let config = MLModelConfiguration()
config.computeUnits = .cpuOnly // Reproductible sur CI
let mlModel = try MyClassifier(configuration: config).model
model = try VNCoreMLModel(for: mlModel)
}
// Test de précision avec dataset de validation
func testClassificationAccuracy() async throws {
let testCases: [(imageName: String, expectedClass: String)] = [
("cat_001", "cat"),
("dog_001", "dog"),
("bird_001", "bird")
]
var correct = 0
for testCase in testCases {
let image = try loadTestImage(named: testCase.imageName)
let prediction = try await classify(image: image)
if prediction == testCase.expectedClass {
correct += 1
}
}
let accuracy = Double(correct) / Double(testCases.count)
XCTAssertGreaterThan(accuracy, 0.95, "Accuracy should be > 95%")
}
// Test de performance (temps d'inférence)
func testInferencePerformance() throws {
let image = try loadTestImage(named: "test_image")
measure(metrics: [XCTClockMetric(), XCTMemoryMetric()]) {
let request = VNCoreMLRequest(model: model)
let handler = VNImageRequestHandler(cgImage: image)
try? handler.perform([request])
}
}
// Test de robustesse aux transformations
func testRobustness() async throws {
let originalImage = try loadTestImage(named: "cat_001")
let originalPrediction = try await classify(image: originalImage)
// Teste avec rotation
let rotated = try applyTransform(originalImage, rotation: .pi / 6)
let rotatedPrediction = try await classify(image: rotated)
XCTAssertEqual(originalPrediction, rotatedPrediction)
// Teste avec bruit
let noisy = try addNoise(to: originalImage, intensity: 0.1)
let noisyPrediction = try await classify(image: noisy)
XCTAssertEqual(originalPrediction, noisyPrediction)
}
// Test de gestion des cas limites
func testEdgeCases() async throws {
// Image très petite
let smallImage = try loadTestImage(named: "tiny_10x10")
let smallResult = try await classify(image: smallImage)
XCTAssertNotNil(smallResult)
// Image monochrome
let monoImage = try loadTestImage(named: "grayscale")
let monoResult = try await classify(image: monoImage)
XCTAssertNotNil(monoResult)
}
// Helpers
private func classify(image: CGImage) async throws -> String {
let request = VNCoreMLRequest(model: model)
let handler = VNImageRequestHandler(cgImage: image)
try handler.perform([request])
guard let results = request.results as? [VNClassificationObservation],
let top = results.first else {
throw TestError.noResults
}
return top.identifier
}
private func loadTestImage(named: String) throws -> CGImage {
guard let url = Bundle(for: type(of: self))
.url(forResource: named, withExtension: "jpg"),
let source = CGImageSourceCreateWithURL(url as CFURL, nil),
let image = CGImageSourceCreateImageAtIndex(source, 0, nil) else {
throw TestError.imageNotFound
}
return image
}
}Prêt à réussir tes entretiens iOS ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Questions de conception système
13. Comment concevoir une architecture ML on-device pour une app de production ?
Une architecture ML robuste sépare les préoccupations : modèle, preprocessing, postprocessing, et caching. Elle doit gérer les mises à jour de modèles et le fallback gracieux.
import CoreML
import Vision
// Protocol pour l'abstraction des modèles
protocol MLModelProvider {
associatedtype Input
associatedtype Output
func predict(_ input: Input) async throws -> Output
var modelVersion: String { get }
}
// Gestionnaire de modèles avec mise à jour OTA
class ModelManager {
static let shared = ModelManager()
private var models: [String: any MLModel] = [:]
private let modelDirectory: URL
private init() {
modelDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("MLModels")
try? FileManager.default.createDirectory(at: modelDirectory, withIntermediateDirectories: true)
}
// Charge un modèle avec fallback vers la version bundled
func loadModel<T: MLModel>(
named name: String,
type: T.Type
) async throws -> T {
// Vérifie si une version téléchargée existe
let downloadedURL = modelDirectory.appendingPathComponent("\(name).mlmodelc")
if FileManager.default.fileExists(atPath: downloadedURL.path) {
// Valide l'intégrité du modèle téléchargé
do {
let model = try await loadAndValidate(from: downloadedURL, type: type)
return model
} catch {
// Fallback vers version bundled si corrupted
print("Downloaded model corrupted, falling back to bundled version")
try? FileManager.default.removeItem(at: downloadedURL)
}
}
// Charge la version bundled
guard let bundledURL = Bundle.main.url(forResource: name, withExtension: "mlmodelc") else {
throw ModelError.modelNotFound(name)
}
return try await loadAndValidate(from: bundledURL, type: type)
}
// Télécharge et installe une nouvelle version du modèle
func updateModel(named name: String, from url: URL) async throws {
// Télécharge le modèle
let (tempURL, _) = try await URLSession.shared.download(from: url)
// Compile le modèle si nécessaire
let compiledURL: URL
if tempURL.pathExtension == "mlmodel" {
compiledURL = try MLModel.compileModel(at: tempURL)
} else {
compiledURL = tempURL
}
// Valide avant installation
let config = MLModelConfiguration()
_ = try MLModel(contentsOf: compiledURL, configuration: config)
// Installe dans le répertoire des modèles
let destURL = modelDirectory.appendingPathComponent("\(name).mlmodelc")
try? FileManager.default.removeItem(at: destURL)
try FileManager.default.moveItem(at: compiledURL, to: destURL)
// Notifie l'app de la mise à jour
NotificationCenter.default.post(name: .modelUpdated, object: name)
}
private func loadAndValidate<T: MLModel>(
from url: URL,
type: T.Type
) async throws -> T {
let config = MLModelConfiguration()
config.computeUnits = .all
let model = try T(contentsOf: url, configuration: config)
// Validation basique du modèle
// Vérifier que les inputs/outputs correspondent aux attentes
return model
}
}
extension Notification.Name {
static let modelUpdated = Notification.Name("MLModelUpdated")
}14. Comment gérer les erreurs et le monitoring en production ?
Un système de monitoring robuste capture les métriques de performance, les erreurs, et permet le debugging à distance. L'intégration avec des outils d'analytics est essentielle.
import OSLog
class MLMonitor {
static let shared = MLMonitor()
private let logger = Logger(subsystem: "com.app.ml", category: "inference")
private var metrics: [InferenceMetric] = []
struct InferenceMetric: Codable {
let modelName: String
let inferenceTime: Double
let inputSize: CGSize?
let confidence: Float?
let timestamp: Date
let success: Bool
let errorDescription: String?
}
// Enregistre une inférence
func recordInference(
model: String,
duration: TimeInterval,
inputSize: CGSize? = nil,
confidence: Float? = nil,
error: Error? = nil
) {
let metric = InferenceMetric(
modelName: model,
inferenceTime: duration,
inputSize: inputSize,
confidence: confidence,
timestamp: Date(),
success: error == nil,
errorDescription: error?.localizedDescription
)
metrics.append(metric)
// Log pour debugging
if let error = error {
logger.error("ML inference failed: \(model) - \(error.localizedDescription)")
} else {
logger.info("ML inference: \(model) completed in \(duration)s")
}
// Détecte les anomalies
checkForAnomalies(metric)
}
// Wrapper pour mesurer automatiquement
func measure<T>(
model: String,
inputSize: CGSize? = nil,
operation: () async throws -> T
) async rethrows -> T {
let start = CFAbsoluteTimeGetCurrent()
do {
let result = try await operation()
let duration = CFAbsoluteTimeGetCurrent() - start
recordInference(
model: model,
duration: duration,
inputSize: inputSize
)
return result
} catch {
let duration = CFAbsoluteTimeGetCurrent() - start
recordInference(
model: model,
duration: duration,
inputSize: inputSize,
error: error
)
throw error
}
}
// Détecte les problèmes de performance
private func checkForAnomalies(_ metric: InferenceMetric) {
// Alerte si le temps d'inférence dépasse le seuil
if metric.inferenceTime > 1.0 {
logger.warning("Slow inference detected: \(metric.modelName) took \(metric.inferenceTime)s")
// Envoie une alerte si disponible
Task {
await AnalyticsService.shared.reportAnomaly(
type: .slowInference,
details: metric
)
}
}
// Alerte si la confiance est trop basse
if let confidence = metric.confidence, confidence < 0.5 {
logger.info("Low confidence prediction: \(confidence) for \(metric.modelName)")
}
}
// Génère un rapport de performance
func generateReport() -> PerformanceReport {
let recentMetrics = metrics.filter {
$0.timestamp > Date().addingTimeInterval(-3600) // Dernière heure
}
let avgInferenceTime = recentMetrics.map(\.inferenceTime).reduce(0, +) / Double(recentMetrics.count)
let successRate = Double(recentMetrics.filter(\.success).count) / Double(recentMetrics.count)
return PerformanceReport(
totalInferences: recentMetrics.count,
averageInferenceTime: avgInferenceTime,
successRate: successRate,
modelBreakdown: Dictionary(grouping: recentMetrics, by: \.modelName)
)
}
}
struct PerformanceReport {
let totalInferences: Int
let averageInferenceTime: Double
let successRate: Double
let modelBreakdown: [String: [MLMonitor.InferenceMetric]]
}Conclusion
Vision Framework et CoreML représentent la fondation du machine learning on-device sur iOS. Maîtriser ces technologies est essentiel pour développer des applications modernes qui respectent la vie privée des utilisateurs tout en offrant des fonctionnalités ML avancées.
Checklist de révision
- ✅ Comprendre CoreML et ses avantages (confidentialité, latence, offline)
- ✅ Savoir convertir des modèles TensorFlow/PyTorch vers CoreML
- ✅ Maîtriser les requêtes Vision (détection visages, OCR, classification)
- ✅ Implémenter le suivi d'objets en temps réel
- ✅ Optimiser les performances (quantification, gestion mémoire)
- ✅ Concevoir des architectures ML robustes pour la production
- ✅ Mettre en place le monitoring et la gestion des erreurs
Points clés à retenir
La performance on-device dépend fortement du choix entre CPU, GPU et Neural Engine. La quantification des modèles offre un excellent compromis taille/performance. Le monitoring en production est crucial pour détecter les régressions.
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Tags
Partager
Articles similaires

StoreKit 2 en entretien : gestion des abonnements et receipts validation
Préparez vos entretiens iOS avec ce guide complet sur StoreKit 2, la gestion des abonnements, la validation des receipts et les questions techniques fréquentes.

iOS Push Notifications en entretien en 2026 : APNs, tokens et troubleshooting
Préparez vos entretiens iOS avec ce guide complet sur les Push Notifications, APNs, la gestion des tokens et le troubleshooting. Questions fréquentes et réponses détaillées.

Top 25 questions d'entretien Swift pour développeurs iOS
Préparez vos entretiens iOS avec les 25 questions Swift les plus posées : optionals, closures, ARC, protocols, async/await et patterns avancés.