Kaggle). Ces données comprennent les caractéristiques de chaque projet KickStarter tel que le but à atteindre, la date de démarrage, la deadline etc. mais aussi le statut final de la campagne (si l’objectif de contribution a été atteint ou pas).
Nous utiliserons comme features, la durée du projet KickStarter, le nombre de contributeur ainsi que le but à atteindre.
Le modèle sera construit à partir de la librairie sklearn, librairie python dédié au machine learning.
NB : Avant de pouvoir utiliser cette librairie dans un script python, il faut l’installer sur son poste de développement
pip install -U scikit-learn
La première étape de la construction du modèle est de déterminer l’algorithme à utiliser.
Nous avons des données en entrée (caractéristiques des projets KickStarter) que l’on associe à un libellé (Succès du projet) : il s’agit de ML supervisé. Le libellé que l’on cherche à prédire a une valeur discrète : nous allons utiliser un algorithme de classification.
Sklearn algorithm flowchart - Source : http://scikit-learn.org/stable/tutorial/machine_learning_map/index.html
Nous allons maintenant tester plusieurs algorithmes de classification afin de déterminer celui qui fournit les meilleurs résultats. Pour cela, pour chaque algorithme de cette famille, nous allons construire le modèle puis mesurer sa performance.
Import des données:
En chargeant les données depuis notre dataset CSV, on obtient alors un Dataframe que l’on va manipuler pour ajouter la feature calculée duration et supprimer les colonnes inutiles.
#Lecture des donnees depuis le dataset CSV dataframe = pandas.read_csv('../data/kickstarter.csv') #On calcule la duree du projet KickStarter pour chaque ligne dataframe['duration'] = dataframe['deadline'] - dataframe['launched_at'] #On supprime les colonnes inutiles dataframe = dataframe.drop(['project_id', 'name', 'desc', 'keywords', 'country','currency', 'disable_communication', 'deadline', 'launched_at', 'state_changed_at','created_at'], axis=1) #Creation d'une matrice de features X = numpy.array(dataframe.drop(['final_status'],1)) #Creation d'une matrice de libelle final y = numpy.array(dataframe['final_status'])
Comparaison des algorithmes
Pour chaque algorithme, nous utilisons une technique de cross-validation K-Fold pour obtenir un score et ainsi déterminer le meilleur algorithme à utiliser. Cette technique permet de ne pas “gaspiller” une partie des données du dataset pour construire un dataset de tests. En effet, la technique k-Fold consiste à découper les données en k parties et à mesurer le score de l’algorithme k fois en utilisant les k-1 parties des données pour construire le modèle et à le tester avec les données restantes.
K-Fold Cross validation
models = [] models.append(('LR', LogisticRegression())) models.append(('LDA', LinearDiscriminantAnalysis())) models.append(('KNC', KNeighborsClassifier())) models.append(('DTC', DecisionTreeClassifier())) models.append(('NB', GaussianNB()))
results = [] names = [] for name, model in models: #Selection des indices pour la cross validation kfold = model_selection.KFold(n_splits=10, random_state=7) #Calcul des scores cross_val_results = model_selection.cross_val_score(model, X, y, cv=kfold, scoring=’neg_log_loss’) results.append(cross_val_results) names.append(name)
Le score déterminé par cross-validation correspond à la moyenne des k scores obtenues qui correspond elle-même au score calculé par chaque algorithme. Lorsqu’on utilise la métrique log_loss, plus le score est proche de 0 meilleur est l’algorithme.
Le graphe ci-dessous montre le résultat de la comparaison des 5 algorithmes (pour chaque algorithme, le score est compris dans l’écart-type obtenu par la cross-validation) :
L’algorithme obtenant les meilleurs résultats pour notre use case est le LogisticRegression, c’est donc celui-ci sera utilisé pour construire le modèle CoreML.
A noter qu'on ne cherche pas ici à optimiser ici les paramètres de l’algorithme retenu (ce qui permettrait d’en améliorer les performances).
Par ailleurs, nous ignorons ici volontairement un risque lié à la cross-validation : dans le cas où le temps est une variable importante pour le modèle alors il y a un risque d'introduire une leak temporelle qui aurait pour impact d'obtenir des résultats trop optimistes lors du choix du modèle.
Nous allons maintenant écrire un script permettant la création du modèle et son export CoreML.
L’étape d’import de données est la même que dans le script précédent.
Initialisation et entrainement du modèle
On initialise le modèle et on l’entraine avec les données d’entrées : les 3 features (durée, objectif et nombre de contributeurs) et les données de sortie : succès de la campagne.
A noter que notre modèle a déjà été testé dans l’étape précédente (cross-validation), on utilisera donc l’intégralité des données sans réserver une partie pour le tester.
#Initialisation de l'algorithme logisticRegression = LogisticRegression() #Entrainement du modele a partir des donnees logisticRegression.fit(X, y)
Conversion du modèle vers CoreML
A présent, nous allons convertir le modèle entrainé au format CoreML. La librairie coremltools permet cette conversion ainsi que le renseignement d’informations sur les entrées - sorties du modèle qui seront utilisables dans le projet Xcode.
#Conversion du modele au format CoreML coreml_model = coremltools.converters.sklearn.convert(logisticRegression, ['goal', 'backersCount', 'duration'], ['prediction', 'precision']) coreml_model.author = 'OCTO Technology' coreml_model.license = 'Unknown' coreml_model.short_description = 'KickStarter success prediction using LogisticRegression' coreml_model.input_description['goal'] = 'Goal of the project.' coreml_model.input_description['backersCount'] = 'Contributors count for the project.' coreml_model.input_description['duration'] = 'Duration of the project.' coreml_model.output_description['prediction'] = 'Prediction of the success for the project' #Sauvegarde du fichier mlmodel coreml_model.save('KickStarterMLModel.mlmodel')
Le fichier KickStarterMLModel.mlmodel ainsi obtenu pourra être utilisé dans un projet iOS pour utilisation du modèle.
Un projet iOS utilisant CoreML ne nécessite pas de configuration particulière, si ce n’est que de cibler iOS 11 ou plus.
L’application iOS pour illustrer cet article est relativement simple : elle contient un écran qui liste les campagnes KickStarter avec pour chaque campagne, son nom ainsi que quelques informations relatives à celle-ci. La couleur de l’arrière plan de chaque cellule changera en fonction de la prédiction du succès : rouge si un échec est prédit, vert dans le cas contraire.
Une fois le projet créé et la récupération de la liste des projets KickStarter réalisée (sans prédiction pour le moment), nous importons le fichier KickStarterMLModel.mlmodel dans Xcode, comme n’importe quelle ressource.
En sélectionnant le fichier importé, on accède aux détails du modèle (on retrouve les informations ajoutées lors de la conversion du modèle avec coremltools). On peut également accéder à la définition des classes générées par Xcode lors de l’import du fichier mlmodel dans le projet.
Détails du modèle CoreML importé dans le projet
Xcode a généré les 3 classes représentant :
class KickStarterMLModelInput : MLFeatureProvider { /// Goal of the project. as double value var goal: Double /// Contributors count for the project. as double value var backersCount: Double /// Duration of the project. as double value var duration: Double var featureNames: Set { get { return ["goal", "backersCount", "duration"] } }
func featureValue(for featureName: String) -> MLFeatureValue? { if (featureName == "goal") { return MLFeatureValue(double: goal) } if (featureName == "backersCount") { return MLFeatureValue(double: backersCount) } if (featureName == "duration") { return MLFeatureValue(double: duration) } return nil }
init(goal: Double, backersCount: Double, duration: Double) { self.goal = goal self.backersCount = backersCount self.duration = duration } }
class KickStarterMLModelOutput : MLFeatureProvider { /// Prediction of the success for the project as integer value let prediction: Int64 /// precision as dictionary of 64-bit integers to doubles let precision: [Int64 : Double] var featureNames: Set { get { return ["prediction", "precision"] } }
func featureValue(for featureName: String) -> MLFeatureValue? { if (featureName == "prediction") { return MLFeatureValue(int64: prediction) } if (featureName == "precision") { return try! MLFeatureValue(dictionary: precision as [NSObject : NSNumber]) } return nil }
init(prediction: Int64, precision: [Int64 : Double]) { self.prediction = prediction self.precision = precision } }
class KickStarterMLModel { var model: MLModel
init(contentsOf url: URL) throws { self.model = try MLModel(contentsOf: url) }
convenience init() { let bundle = Bundle(for: KickStarterMLModel.self) let assetPath = bundle.url(forResource: "KickStarterMLModel", withExtension:"mlmodelc") try! self.init(contentsOf: assetPath!) }
func prediction(input: KickStarterMLModelInput) throws -> KickStarterMLModelOutput { let outFeatures = try model.prediction(from: input) let result = KickStarterMLModelOutput(prediction: outFeatures.featureValue(for: "prediction")!.int64Value, precision: outFeatures.featureValue(for: "precision")!.dictionaryValue as! [Int64 : Double]) return result }
func prediction(goal: Double, backersCount: Double, duration: Double) throws -> KickStarterMLModelOutput { let input_ = KickStarterMLModelInput(goal: goal, backersCount: backersCount, duration: duration) return try self.prediction(input: input_) } }
On créé une classe qui représente un projet KickStarter, au delà des attributs du projet (nom, objectif, etc), elle possède un attribut predictedSuccess qui sera mis à jour via notre modèle CoreML.
var identifier: Int! var name: String! …. var predictedSuccess: Bool?
La mise à jour s’effectuera par l’intermédiaire d’une méthode predictSuccess qui prend en paramètre le modèle CoreML (pour des raisons de performance, le modèle n’est chargé qu’une seul fois dans l’application).
func predictSuccess(model: KickStarterMLModel) { //Predict success using the CoreML model let prediction = try? model.prediction(goal: goal, backersCount: Double(backersCount), duration: duration) //Save result in predictedSuccess attribute if let success = prediction?.prediction { predictedSuccess = success == 1 } }
L’API de KickStarter nous permet d’obtenir la liste des projets par page de 10. Pour chaque appel à l’API (qui se fait au fur et à mesure du scroll de l’utilisateur dans la liste des projets), on met à jour la prédiction du succès de la campagne. Afin de ne pas bloquer l’UI, la prédiction via CoreML s'effectue en background.
private func loadProjects() { self.isLoadingProjects = true kickStartProjectsRepository.getNextProjects() { result in self.isLoadingProjects = false self.tableView.reloadData() } }
func getNextProjects(completionHandler: @escaping (Result<[KickStarterProject]?>) -> Void) { let page = (lastResponse?.page ?? 0)+1 let url = "https://www.kickstarter.com/projects/search.json?page=\(page)" Alamofire.request(url).responseObject { (response: DataResponse) in ... self.lastResponse = response.result.value let newProjects = self.lastResponse?.projects newProjects?.forEach { self.projects.append($0) } //Predict success for each projects in background DispatchQueue.global(qos: .background).async { newProjects?.forEach { $0.predictSuccess(model: self.mlModel) } DispatchQueue.main.async { //Call Handler in main queue completionHandler(Result.success(newProjects)) } } } }
Rendu final de l’application
Temps de prédiction :
La prédiction du succès d’une campagne prend entre 3 et 10 ms sur nos tests sur iPhone 7. Un temps relativement faible mais qu’il ne faut pas généraliser. En effet le temps de prédiction dépendra d’une multitude de facteur : nombre d’entrées, type de données à prédire, algorithme utilisé, taille du dataset initial, …
Difficile donc de prédire l’impact de l’utilisation importante de CoreML sur la batterie car cela dépend du modèle. On peut bien sûr imaginer que l’impact sera négligeable dans notre cas d’utilisation relativement simple mais plus important sur des uses cases de reconnaissance d’image par exemple.
Taille du modèle :
Le fichier .mlmodel ajouté au projet pèse 500Ko, taille assez faible mais qui dépend également du type de modèle. On trouve par exemple sur le site https://coreml.store (mettant à disposition des modèles CoreML prêts à être intégrés dans des applications), des modèles de plusieurs dizaines de Mo.
Mise à jour du modèle :
On a vu que Xcode génère les classes permettant l’utilisation du modèle lors de l’import de celui-ci dans le projet, on pourrait croire que cela rend impossible la mise à jour du modèle sans passer par une mise à jour de l’Application. Ce n’est pas tout à fait exacte.
En regardant l’initialisation du modèle KickStarterMLModel, on voit qu’il dispose d’une méthode init prenant en paramètre une URL, on dispose donc d’un point d’entrée pour initialiser un objet KickStarterMLModel avec un autre fichier.
Cependant l'initialisation n’attend pas l’URL vers un fichier .mlmodel mais vers un fichier .mlmodelc qui est un format interne (compilé) à CoreML utilisé par le framework.
En effet, lorsque l’on importe un fichier .mlmodel, Xcode le compile, créant ainsi un fichier .mlmodelc et génère les classes pour l’utiliser.
Apple fournit également une méthode permettant la compilation du modèle durant l'exécution d’une application_._ On pourra donc mettre à jour le modèle via cette méthode après avoir téléchargé un fichier .mlmodel à jour.
let compiledUrl = try MLModel.compileModel(at: modelUrl) let model = try MLModel(contentsOf: compiledUrl)
La seule limitation, qui peut être bloquante selon les usages, est que l’interface du modèle (entrées, sorties) ne peut pas changer. En effet, la compilation au sein de l’application va créer un nouveau fichier .mlmodelc mais les classes permettant l’utilisation du modèle (générées par Xcode lors de son import) resteront les mêmes. Lorsque l’on souhaitera mettre à jour son modèle en ajoutant des variables, il faudra donc passer par une mise à jour de l’application.
L’intégration du Machine Learning par Apple sur iOS est intéressante : elle repose sur des solutions open-source éprouvés par la communauté Data Science.
L’effort d’intégration, une fois le modèle construit, est quasiment négligeable.
L’arrivée de solutions mobiles de Machine Learning (CoreML, TensorFlow, etc.) provoque un intérêt certain dans la communauté, mais quels sont les uses cases pour les utilisateurs finaux de nos applications ?
Parmi les uses cases mis en avant par Apple, on retrouve ceux liés aux traitements automatiques du langage et traitement d’images : face tracking, détection de visages, paysages, détection de texte dans une image, détection de forme, scan de code barre, object tracking, etc.
Ces usages sont intéressants techniquement mais il est difficile d’en déduire de nouvelles fonctionnalités dans les applications les plus utilisées.
Le réel gain pour l’utilisateur final se situe au niveau de l’amélioration de l’expérience utilisateur. Le machine learning réalisé localement va permettre de lui fournir des applications plus intelligentes : gain de temps, propositions pertinentes, prédiction de comportement, aide à la navigation, ...
On peut se demander de l’intérêt de passer par une solution locale plutôt qu’une solution côté Serveur/API. Mon point de vue est que cela dépend des uses cases.
Lorsque qu’on peut techniquement intégrer du ML localement et que cela présente un intérêt pour l’expérience utilisateur, c’est forcément positif.
Rien n’empêche d’avoir une solution serveur et une solution plus souple côté mobile pour le même usage, uniquement pour améliorer l’expérience. Prenons l’exemple d’un formulaire de souscription dans lequel l’utilisateur doit uploader un document d’un certain type, on pourra utiliser sur mobile un modèle ML pour vérifier la validité du document mais également disposer d’une solution côté API effectuant cette vérification afin de sécuriser la souscription via l’API.
Il y a d’autres uses cases où l’intégration est forcément côté mobile : la détection de numéros de carte bancaire à partir de la caméra pour pré-remplir un champ dans un parcours d’achat.
Et d’autres sont forcément côté serveur/API : un algorithme de recommandation de contenus basés sur la consommation des autres utilisateurs pourrait difficilement être intégré localement.
La réponse à la question “doit-on intégrer du ML côté API ou côté mobile” passera donc entre autres par une interrogation sur l’expérience utilisateur.
Le code source du projet est disponible ici : https://github.com/gdesmaziers/KickStarterPrediction.