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.

Vision Framework et CoreML pour le machine learning on-device iOS

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.

Structure de ce guide

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.

CoreMLBasics.swiftswift
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.

python
# 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.

MLModelVsVNCoreML.swiftswift
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] ?? []
}
Quand utiliser quoi ?

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.

CoreMLVersioning.swiftswift
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.

VisionRequests.swiftswift
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.

ObjectTracking.swiftswift
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.

VisionOptimization.swiftswift
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.

PoseDetection.swiftswift
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.

python
# 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")
QuantizationComparison.swiftswift
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.

MemoryOptimization.swiftswift
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
    }
}
Attention aux fuites mémoire

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.

CreateMLComponents.swiftswift
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.

MLModelTests.swiftswift
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.

MLArchitecture.swiftswift
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.

MLMonitoring.swiftswift
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

#vision
#coreml
#ios
#machine-learning
#interview

Partager

Articles similaires