Comment déployer une IA CoreML avec Vapor

Vous avez envie de toucher à l'intelligence artificielle d'une façon simple ? CoreML est fait pour vous !

Comment déployer une IA CoreML avec Vapor
Cet article fait suite à l'article "Créer une intelligence artificielle en 5 minutes sans coder" qui nous a permis de créer un modèle de machine learning que l'on va intégrer et déployer via une API Rest ici.

Les outils d'Apple pour faire de l'intelligence artificielle sont époustouflants 😍. Mais malheureusement, ils ne fonctionnent que sur des appareils de la firme !

Alors pour contrer ça, il est possible de créer une API avec Vapor et CoreML, pour pouvoir profiter de ces outils sur d'autres plateformes (Windows, Android ou le web par exemple 😏)

0. Accéder à un Mac - Partez pas c'est pas si cher

Notre idée est de pouvoir profiter de CoreML sur n'importe quel système, on va donc créer une API Rest qui tournera sur un serveur Mac. Ce qui est cool c'est que Scaleway (et d'autres) offrent ce genre de service.

Apple silicon M1 as-a-Service. Cloud Mac | Scaleway
Testez, développez et publiez vos Apps iOS/macOS. Disponible en moins de 5 minutes à 0,10 € de l’heure. Support assurés.
Vous pouvez louer un Mac pour vous en servir de serveur

De notre coté, on a utilisé Scaleway qui permet de louer un Mac Mini M1 (donc performant) qui fera office de serveur avec sa propre IP pour pouvoir déployer notre API CoreML.

1. Installer Vapor

Avant de l'installer, commençons par expliquer ce que c'est !
Vapor est un framework web open-source développé en Swift, donc (malheureusement) utilisable uniquement si vous avez un Mac 🍏

Ce framework permet de créer des API Rest, des applications web ou encore de mettre en place du temps réel assez facilement, et tout ça en Swift 🔥

Pour installer Vapor, nous vous conseillons d'utiliser Homebrew, ce n'est pas indispensable, mais c'est un outil qui vous permettra d'installer des dépendances pour ce projet et que vous retrouverez régulièrement dans pleins de tutoriels pour installer des dépendances sur un Mac.

Si vous n'avez pas encore Homebrew d'installé, ça se passe par ici

Une fois que vous avez Homebrew, vous pouvez installer Vapor tout simplement avec ces 2 petites lignes :

brew tap vapor/tap
brew install vapor
Terminal

2. Créer un projet Vapor

Pour créer un projet vapor, c'est très simple ! Dans un terminal, placez-vous où vous voulez le créer et tapez cette commande (CoreMLAPI est le nom du projet, choisissez ce que vous voulez) :

vapor new CoreMLAPI
Terminal

3. Importer un modèle CoreML

Vous n'avez jamais créé de modèle CoreML ? Regardez notre article et créez votre propre modèle : Créer une intelligence artificielle en 5 minutes sans coder

Vous pouvez également utiliser un modèle déjà existant. Voici 2 petits liens sympas avec des modèles prêts à être utilisés 😎


Pour ce tutoriel, nous avons déjà créé un modèle nommé "CatsDogsNet", qui parvient à savoir si l'image donnée représente un chat ou un chien.

Dans le dossier Resources, créez un dossier CoreML et ajoutez-y votre modèle (.mlmodel).

Une fois le fichier du modèle ajouté, nous allons le compiler pour pouvoir l'utiliser sur un projet Xcode. Pour ça, placez-vous à la racine de votre projet Vapor, et lancez cette commande :

xcrun coremlcompiler compile Resources/coreml/VotreModele.mlmodel Resources/coreml
Terminal

Des fichiers ont du être créés et votre dossier CoreML doit maintenant contenir deux fichiers (un .mlmodel et un .mlmodelc) comme ici :

Pour finir, générons le code source de ce modèle pour pouvoir le modifier après en Swift si besoin.
Pour ça, toujours à la racine, lancez cette commande :

xcrun coremlcompiler generate Resources/coreml/VotreModele.mlmodel --language Swift Sources/App/
Terminal

Un fichier nommé VotreModele a du être généré dans le dossier Sources/App

Ce fichier Swift permet de faire appel facilement aux fonctions du modèle depuis votre code.

4. Adapter le fichier CoreML

Maintenant que le modèle a été généré, nous allons faire petite modification pour bien l'inclure dans le projet.

Ouvrez le fichier VotreModel dans Sources > App
Tout en haut du fichier, importez Vapor

import Vapor

Puis recherchez urlOfModelInThisBundle et remplacez son contenu par

class var urlOfModelInThisBundle : URL {
    let directory = DirectoryConfiguration.detect()
    return URL(fileURLWithPath: directory.workingDirectory)
        .appendingPathComponent("Resources/coreml/VotreModele.mlmodelc",
                                isDirectory: true
        )
}

5. Créer la route qui classifie

Dans le dossier Sources > App > Controllers, ajoute un nouveau fichier "ImageClassificationController.swift" avec ce contenu :

import Vapor

struct ImageClassificationController: RouteCollection {

   // Création de la route d'API /classify_image qui collecte une image	
   func boot(routes: RoutesBuilder) throws {
        routes.on(
            .POST,
            "classify_image",
            body: .collect(maxSize: ByteCount(value: 2000*1024)),
            use: imageClassification
        )
    }
    
    // Décodage le fichier reçu pour pouvoir ensuite le classifier
    func imageClassification(req: Request) throws -> EventLoopFuture<[ClassificationResult]> {
        let eventLoop = req.eventLoop.makePromise(of: [ClassificationResult].self)

        let request = try req.content.decode(ClassifyRequest.self)

        let image = request.file
        let data = Data(buffer: image.data)
        guard let orientation = CGImagePropertyOrientation(rawValue: 0) else { throw Abort(.internalServerError) }

        ImageClassifier().getClassification(forImageData: data, orientation: orientation) { result in
            switch result {
            case .success(let classificationResults):
                eventLoop.succeed(classificationResults)
            case .failure(let error):
                eventLoop.fail(error)
            }
        }

        return eventLoop.futureResult
    }
}
ImageClassificationController.swift

Ajoutez ensuite la route dans le fichier routes.swift. Pour ça remplacez la fonction routes du fichier par :

func routes(_ app: Application) throws {
    try app.register(collection: ClassificationController())
}
routes.swift

6. Classifier les images

Maintenant que l'on a le modèle et la route, il ne nous reste plus qu'à classifier, pour ça, commencez par créer un fichier ClassifyRequest dans lequel vous mettez :

import Vapor

struct ClassifyRequest: Content {
    let image: File
}
ClassifyRequest.swift

Ce fichier va servir à convertir ce qu'on reçoit depuis la requête (le fichier de l'image)
On est bientôt au bout ! 😎

Pour finir, on va créer les 3 fichiers qui vont nous permettre de classifier ce qui est reçu par l'API :

Le fichier ImageClassfierError.swift, qui permet d'avoir une raison en cas d'erreur dans la classification

enum ImageClassifierError: Error {
    case nothingRecognised
    case unableToClassify
    case invalidImage
}
ImageClassifierError.swift

Le fichier ClassificationResult.swift, qui permet de classifier les résultats avec un identifier, et une confidence.
Dans notre cas, l'identifier sera cats, dogs ou junk, et la confidence est le % de confiance du modèle dans la prédiction.

import Vapor

struct ClassificationResult: Content {
    let identifier: String
    let confidence: Float
}
ClassificationResult.swift

Et enfin, le fichier le plus important : ImageClassifier.swift, c'est lui qui va s'occuper de lancer la classification grâce au modèle.

import Vapor
import CoreML
import Vision
import CoreImage


class ImageClassifier {
    typealias classificationCompletion = ((Result<[ClassificationResult], ImageClassifierError>) -> Void)
    private var completionClosure: classificationCompletion?

    // C'est ici que CoreML entre en jeu et fait initie le modèle
    lazy var request: VNCoreMLRequest = {
        let config = MLModelConfiguration()
        config.computeUnits = .all

        do {
            let model = try VNCoreMLModel(for: VotreModele(configuration: config).model)
            let request = VNCoreMLRequest(model: model, completionHandler: handleClassifications)
            request.imageCropAndScaleOption = .centerCrop

            return request
        } catch {
            fatalError("Failed to load the model. Error: \(error.localizedDescription)")
        }
    }()

    // Requête faite avec le modèle pour classifier l'image reçue
    func getClassification(forImageData imageData: Data, orientation: CGImagePropertyOrientation, completion: @escaping classificationCompletion) {
        guard let ciImage = CIImage(data: imageData) else {
            completion(.failure(.invalidImage))
            return
        }

        completionClosure = completion

        let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
        do {
            try handler.perform([self.request])
        } catch {
            print("Failed to perform classification.\n\(error.localizedDescription)")
        }
    }
}

private extension ImageClassifier {
    func handleClassifications(forRequest request: VNRequest, error: Error?) {
        guard
            let results = request.results,
            let classifications = results as? [VNClassificationObservation]
        else {
            debugPrint("Unable to classify image.\n\(error!.localizedDescription)")
            completionClosure?(.failure(.unableToClassify))
            return
        }

		// Filtrer et garder les classifications les plus hautes
        if classifications.isEmpty {
            completionClosure?(.failure(.nothingRecognised))
        } else {
            let topClassifications = classifications.prefix(2)
            let results = topClassifications.map { classification in
                ClassificationResult(identifier: classification.identifier, confidence: classification.confidence)
            }
            completionClosure?(.success(results))
        }
    }
}
ImageClassifier.swift

7. Il n'y a plus qu'à tester

Votre API est prête, on peut lui passer un fichier (l'image), et elle doit nous retourner la classification de l'image (chat, chien ou junk) avec la précision.

Pour tester, il nous suffit d'exécuter notre projet Vapor avec Xcode (l'API doit se lancer en local avec http://localhost:8080/classify_image comme URL) puis d'utiliser un outil comme Postman pour faire une requête post avec un paramètre image qui est le fichier (un chat, un chien, ou n'importe quoi d'autre).

Vous pouvez aussi faire votre requête en utilisant curl, directement depuis un terminal.

Par exemple, nous allons le faire avec ce magnifique chat :

curl --form image='@monmagnifiquechat.png' http://localhost:8080/classify_image
Terminal dans le dossier où l'image se trouve

Si tout s'est bien passé, vous devez avoir ce genre de résultats:

[{"confidence":0.99956685304641724,"identifier":"cat"},{"confidence":0.00034059779136441648,"identifier":"dog"}]
Le réseau est sûr à 99% que c'est un chat. Il a pas tord ;)

Et voilà ! Vous avez déployé votre premier réseau de neurone avec CoreML.

Ce process est applicable pour d'autres modèles comme de la classification de son, d'activité, de la détection d'objet, etc.

Si cela vous intéresse d'aller plus loin hésitez pas à nous contacter pour en discuter :)

Soutenez notre travail ❤️

🔥 Suivez nous sur Instagram, Facebook, LinkedIn ou abonnez-vous à notre newsletter en devenant membre de notre blog :)