1. Avant de commencer
Dans cet atelier de programmation, vous allez mettre à jour l'application que vous avez créée dans les précédents ateliers de programmation sur la classification de texte sur mobile.
Prérequis
- Cet atelier de programmation s'adresse aux développeurs expérimentés qui ne connaissent pas le machine learning.
- L'atelier de programmation fait partie d'un parcours séquentiel. Si vous n'avez pas encore terminé les ateliers "Créer une application de messagerie de base" ou "Créer un modèle de machine learning pour le spam dans les commentaires", veuillez vous arrêter et le faire maintenant.
Ce que vous allez [créer ou apprendre]
- Vous allez apprendre à intégrer votre modèle personnalisé dans votre application, créé dans les étapes précédentes.
Prérequis
- Android Studio ou CocoaPods pour iOS
2. Ouvrir l'application Android existante
Pour obtenir le code nécessaire, suivez l'atelier de programmation 1 ou clonez ce dépôt et chargez l'application à partir de TextClassificationStep1
.
git clone https://github.com/googlecodelabs/odml-pathways
Vous le trouverez dans le chemin TextClassificationOnMobile->Android
.
Le code finalisé est également disponible sous la forme TextClassificationStep2
.
Une fois l'application ouverte, vous pouvez passer à l'étape 2.
3. Importer le fichier de modèle et les métadonnées
Dans l'atelier de programmation "Créer un modèle de machine learning pour le spam dans les commentaires", vous avez créé un modèle .TFLITE.
Vous devez avoir téléchargé le fichier de modèle. Si vous ne l'avez pas, vous pouvez le télécharger depuis le dépôt de cet atelier de programmation. Le modèle est disponible ici.
Ajoutez-le à votre projet en créant un répertoire d'éléments.
- À l'aide du navigateur de projets, assurez-vous que Android est sélectionné en haut de la page.
- Effectuez un clic droit sur le dossier app. Sélectionnez New > Directory (Nouveau > Répertoire).
- Dans la boîte de dialogue New Directory (Nouveau répertoire), sélectionnez src/main/assets.
Un nouveau dossier assets est désormais disponible dans l'application.
- Effectuez un clic droit sur assets.
- Dans le menu qui s'affiche, vous verrez (sur Mac) Afficher dans le Finder. Sélectionnez-la. (Sur Windows, l'option s'intitule Afficher dans l'explorateur, et sur Ubuntu, Afficher dans les fichiers.)
Le Finder s'ouvre pour afficher l'emplacement des fichiers (File Explorer sous Windows, Files sous Linux).
- Copiez les fichiers
labels.txt
,model.tflite
etvocab
dans ce répertoire.
- Revenez à Android Studio. Vous les verrez dans le dossier assets.
4. Mettre à jour le fichier build.gradle pour utiliser TensorFlow Lite
Pour utiliser TensorFlow Lite et les bibliothèques de tâches TensorFlow Lite compatibles, vous devez mettre à jour votre fichier build.gradle
.
Les projets Android en comportent souvent plusieurs. Veillez donc à trouver le niveau 1 de l'application. Dans l'explorateur de projets de la vue Android, recherchez-le dans la section Scripts Gradle. Le fichier correct sera associé au libellé .app, comme illustré ci-dessous:
Vous devrez apporter deux modifications à ce fichier. Le premier se trouve dans la section dépendances en bas. Ajoutez un implementation
de texte pour la bibliothèque de tâches TensorFlow Lite, comme suit:
implementation 'org.tensorflow:tensorflow-lite-task-text:0.1.0'
Le numéro de version peut avoir changé depuis la rédaction de cet article. Pour connaître la version la plus récente, consultez https://www.tensorflow.org/lite/inference_with_metadata/task_library/nl_classifier.
Les bibliothèques de tâches nécessitent également le SDK 21 ou version ultérieure. Recherchez ce paramètre dans android
> default config
, puis définissez-le sur 21:
Vous disposez désormais de toutes vos dépendances. Il est temps de commencer à coder.
5. Ajouter une classe Helper
Pour séparer la logique d'inférence, où votre application utilise le modèle, de l'interface utilisateur, créez une autre classe pour gérer l'inférence du modèle. Appelez cette classe "helper".
- Effectuez un clic droit sur le nom du package contenant votre code
MainActivity
. - Sélectionnez Nouveau > Package.
- Une boîte de dialogue s'affiche au centre de l'écran et vous invite à saisir le nom du package. Ajoutez-le à la fin du nom du package actuel. (Ici, ils sont appelés aides.)
- Une fois cette opération effectuée, effectuez un clic droit sur le dossier helpers dans l'explorateur de projets.
- Sélectionnez New > Java Class (Nouveau > Classe Java), puis appelez-le
TextClassificationClient
. Vous modifierez le fichier à l'étape suivante.
Votre classe d'assistance TextClassificationClient
se présentera comme suit (bien que votre nom de package puisse être différent).
package com.google.devrel.textclassificationstep1.helpers;
public class TextClassificationClient {
}
- Modifiez le fichier avec le code suivant:
package com.google.devrel.textclassificationstep2.helpers;
import android.content.Context;
import android.util.Log;
import java.io.IOException;
import java.util.List;
import org.tensorflow.lite.support.label.Category;
import org.tensorflow.lite.task.text.nlclassifier.NLClassifier;
public class TextClassificationClient {
private static final String MODEL_PATH = "model.tflite";
private static final String TAG = "CommentSpam";
private final Context context;
NLClassifier classifier;
public TextClassificationClient(Context context) {
this.context = context;
}
public void load() {
try {
classifier = NLClassifier.createFromFile(context, MODEL_PATH);
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
}
public void unload() {
classifier.close();
classifier = null;
}
public List<Category> classify(String text) {
List<Category> apiResults = classifier.classify(text);
return apiResults;
}
}
Cette classe fournit un wrapper à l'interpréteur TensorFlow Lite, charge le modèle et élimine la complexité liée à la gestion de l'échange de données entre votre application et le modèle.
Dans la méthode load()
, un nouveau type NLClassifier
est instancié à partir du chemin d'accès du modèle. Le chemin d'accès au modèle est simplement le nom du modèle, model.tflite
. Le type NLClassifier
fait partie des bibliothèques de tâches de texte. Il vous aide à convertir votre chaîne en jetons, à utiliser la longueur de séquence appropriée, à la transmettre au modèle et à analyser les résultats.
(Pour en savoir plus à ce sujet, consultez Créer un modèle de machine learning pour le spam dans les commentaires.)
La classification est effectuée dans la méthode classify, où vous lui transmettez une chaîne, et elle renvoie un List
. Lorsque vous utilisez des modèles de machine learning pour classer du contenu et déterminer si une chaîne est un spam ou non, il est courant que toutes les réponses soient renvoyées, avec des probabilités attribuées. Par exemple, si vous lui transmettez un message qui ressemble à du spam, vous recevrez une liste de deux réponses : l'une indiquant la probabilité qu'il s'agisse de spam et l'autre indiquant la probabilité qu'il ne s'agisse pas de spam. Les éléments "Spam" et "Non spam" sont des catégories. Le List
renvoyé contiendra donc ces probabilités. Vous analyserez cela plus tard.
Maintenant que vous avez la classe d'assistance, revenez à votre MainActivity
et mettez-la à jour pour l'utiliser pour classer votre texte. Vous le verrez à l'étape suivante.
6. Classer le texte
Dans votre MainActivity
, vous devez d'abord importer les assistants que vous venez de créer.
- En haut de
MainActivity.kt
, avec les autres importations, ajoutez:
import com.google.devrel.textclassificationstep2.helpers.TextClassificationClient
import org.tensorflow.lite.support.label.Category
- Ensuite, vous devez charger les assistants. Dans
onCreate
, juste après la lignesetContentView
, ajoutez les lignes suivantes pour instancier et charger la classe d'assistance:
val client = TextClassificationClient(applicationContext)
client.load()
Pour le moment, l'élément onClickListener
de votre bouton devrait se présenter comme suit:
btnSendText.setOnClickListener {
var toSend:String = txtInput.text.toString()
txtOutput.text = toSend
}
- Modifiez-la comme suit:
btnSendText.setOnClickListener {
var toSend:String = txtInput.text.toString()
var results:List<Category> = client.classify(toSend)
val score = results[1].score
if(score>0.8){
txtOutput.text = "Your message was detected as spam with a score of " + score.toString() + " and not sent!"
} else {
txtOutput.text = "Message sent! \nSpam score was:" + score.toString()
}
txtInput.text.clear()
}
La fonctionnalité ne se contente plus d'afficher la saisie de l'utilisateur, mais la classe d'abord.
- Avec cette ligne, vous allez prendre la chaîne saisie par l'utilisateur et la transmettre au modèle, pour obtenir des résultats:
var results:List<Category> = client.classify(toSend)
Il n'y a que deux catégories : False
et True
.
. (TensorFlow les trie par ordre alphabétique. False correspond donc à l'élément 0 et True à l'élément 1.)
- Pour obtenir le score de la probabilité que la valeur soit
True
, vous pouvez consulter results[1].score comme suit:
val score = results[1].score
- Vous avez choisi une valeur de seuil (dans ce cas, 0,8), indiquant que si le score de la catégorie "True" est supérieur à la valeur de seuil (0,8), le message est considéré comme du spam. Sinon, il ne s'agit pas de spam et vous pouvez envoyer le message:
if(score>0.8){
txtOutput.text = "Your message was detected as spam with a score of " + score.toString() + " and not sent!"
} else {
txtOutput.text = "Message sent! \nSpam score was:" + score.toString()
}
- Découvrez le modèle en action ici. Le message "Visitez mon blog pour faire des achats !" a été signalé comme présentant un risque élevé de spam:
À l'inverse, "Hey, fun tutorial, thanks!" (Salut, super tutoriel, merci !) a été considéré comme très peu susceptible d'être du spam:
7. Mettre à jour votre application iOS pour utiliser le modèle TensorFlow Lite
Pour obtenir le code nécessaire, suivez l'atelier de programmation 1 ou clonez ce dépôt et chargez l'application à partir de TextClassificationStep1
. Vous le trouverez dans le chemin TextClassificationOnMobile->iOS
.
Le code finished est également disponible en tant que TextClassificationStep2
.
Dans l'atelier de programmation "Créer un modèle de machine learning pour le spam dans les commentaires", vous avez créé une application très simple qui permettait à l'utilisateur de saisir un message dans un UITextView
et de le transmettre à une sortie sans aucun filtrage.
Vous allez maintenant modifier cette application pour qu'elle utilise un modèle TensorFlow Lite afin de détecter le spam dans les commentaires avant l'envoi. Il suffit de simuler l'envoi dans cette application en affichant le texte dans un libellé de sortie (mais une application réelle peut comporter un babillard, un chat ou quelque chose de similaire).
Pour commencer, vous aurez besoin de l'application de l'étape 1, que vous pouvez cloner à partir du dépôt.
Pour intégrer TensorFlow Lite, vous utiliserez CocoaPods. Si vous ne les avez pas encore installés, vous pouvez le faire en suivant les instructions sur https://cocoapods.org/.
- Une fois CocoaPods installé, créez un fichier nommé Podfile dans le même répertoire que le
.xcproject
de l'application TextClassification. Le contenu de ce fichier doit se présenter comme suit:
target 'TextClassificationStep2' do
use_frameworks!
# Pods for NLPClassifier
pod 'TensorFlowLiteSwift'
end
Le nom de votre application doit figurer sur la première ligne, au lieu de "TextClassificationStep2".
À l'aide de Terminal, accédez à ce répertoire et exécutez pod install
. Si la commande aboutit, un nouveau répertoire appelé Pods et un nouveau fichier .xcworkspace
sont créés. Vous l'utiliserez à la place de .xcproject
.
En cas d'échec, assurez-vous que le fichier Podfile se trouve dans le répertoire où .xcproject
se trouvait. Le fichier podfile dans le mauvais répertoire ou le mauvais nom de cible sont généralement les principaux coupables.
8. Ajouter les fichiers de modèle et de vocabulaire
Lorsque vous avez créé le modèle avec TensorFlow Lite Model Maker, vous avez pu générer le modèle (au format model.tflite
) et le vocabulaire (au format vocab.txt
).
- Ajoutez-les à votre projet par glisser-déposer depuis le Finder vers la fenêtre de votre projet. Assurez-vous que la case Ajouter aux cibles est cochée:
Une fois que vous avez terminé, vous devriez les voir dans votre projet:
- Vérifiez qu'ils sont ajoutés au bundle (afin qu'ils soient déployés sur un appareil) en sélectionnant votre projet (dans la capture d'écran ci-dessus, il s'agit de l'icône bleue TextClassificationStep2), puis en consultant l'onglet Build Phases (Phases de compilation) :
9. Charger le vocabulaire
Lors de la classification du NLP, le modèle est entraîné avec des mots encodés en vecteurs. Le modèle encode les mots avec un ensemble spécifique de noms et de valeurs qui sont appris au fur et à mesure de l'entraînement du modèle. Notez que la plupart des modèles ont des vocabulaires différents. Il est important que vous utilisiez le vocabulaire généré pour votre modèle au moment de l'entraînement. Il s'agit du fichier vocab.txt
que vous venez d'ajouter à votre application.
Vous pouvez ouvrir le fichier dans Xcode pour afficher les encodages. Les mots tels que "chanson" sont encodés en 6 et "amour" en 12. L'ordre est en fait par fréquence. "I" est donc le mot le plus courant dans le jeu de données, suivi de "check".
Lorsque votre utilisateur saisit des mots, vous devez les encoder avec ce vocabulaire avant de les envoyer au modèle pour qu'il les classe.
Explorons ce code. Commencez par charger le vocabulaire.
- Définissez une variable au niveau de la classe pour stocker le dictionnaire:
var words_dictionary = [String : Int]()
- Créez ensuite un
func
dans la classe pour charger le vocabulaire dans ce dictionnaire:
func loadVocab(){
// This func will take the file at vocab.txt and load it into a has table
// called words_dictionary. This will be used to tokenize the words before passing them
// to the model trained by TensorFlow Lite Model Maker
if let filePath = Bundle.main.path(forResource: "vocab", ofType: "txt") {
do {
let dictionary_contents = try String(contentsOfFile: filePath)
let lines = dictionary_contents.split(whereSeparator: \.isNewline)
for line in lines{
let tokens = line.components(separatedBy: " ")
let key = String(tokens[0])
let value = Int(tokens[1])
words_dictionary[key] = value
}
} catch {
print("Error vocab could not be loaded")
}
} else {
print("Error -- vocab file not found")
}
}
- Vous pouvez l'exécuter en l'appelant depuis
viewDidLoad
:
override func viewDidLoad() {
super.viewDidLoad()
txtInput.delegate = self
loadVocab()
}
10. Transformer une chaîne en séquence de jetons
Vos utilisateurs saisiront des mots dans une phrase qui deviendra une chaîne. Chaque mot de la phrase, s'il est présent dans le dictionnaire, sera encodé dans la valeur de clé du mot, comme défini dans le vocabulaire.
Un modèle de NLP accepte généralement une longueur de séquence fixe. Il existe des exceptions pour les modèles créés avec ragged tensors
, mais dans la plupart des cas, le problème est résolu. Vous avez spécifié cette longueur lorsque vous avez créé votre modèle. Assurez-vous d'utiliser la même durée dans votre application iOS.
La valeur par défaut dans Colab pour TensorFlow Lite Model Maker que vous avez utilisée précédemment était 20. Définissez-la ici également:
let SEQUENCE_LENGTH = 20
Ajoutez func
, qui prendra la chaîne, la convertira en minuscules et supprimera toute ponctuation:
func convert_sentence(sentence: String) -> [Int32]{
// This func will split a sentence into individual words, while stripping punctuation
// If the word is present in the dictionary it's value from the dictionary will be added to
// the sequence. Otherwise we'll continue
// Initialize the sequence to be all 0s, and the length to be determined
// by the const SEQUENCE_LENGTH. This should be the same length as the
// sequences that the model was trained for
var sequence = [Int32](repeating: 0, count: SEQUENCE_LENGTH)
var words : [String] = []
sentence.enumerateSubstrings(
in: sentence.startIndex..<sentence.endIndex,options: .byWords) {
(substring, _, _, _) -> () in words.append(substring!) }
var thisWord = 0
for word in words{
if (thisWord>=SEQUENCE_LENGTH){
break
}
let seekword = word.lowercased()
if let val = words_dictionary[seekword]{
sequence[thisWord]=Int32(val)
thisWord = thisWord + 1
}
}
return sequence
}
Notez que la séquence sera de type Int32. Ce choix est délibéré, car lorsque vous transmettez des valeurs à TensorFlow Lite, vous devez gérer une mémoire de bas niveau. TensorFlow Lite traite les entiers d'une séquence de chaînes comme des entiers 32 bits. Cela facilitera (un peu) le processus de transmission des chaînes au modèle.
11. Effectuer la classification
Pour classer une phrase, elle doit d'abord être convertie en une séquence de jetons en fonction des mots qu'elle contient. Vous l'avez fait à l'étape 9.
Vous allez maintenant prendre la phrase et la transmettre au modèle, lui demander d'effectuer une inférence sur la phrase, puis analyser les résultats.
L'interpréteur TensorFlow Lite, que vous devrez importer, sera utilisé:
import TensorFlowLite
Commencez par un func
qui reçoit votre séquence, qui était un tableau de types Int32:
func classify(sequence: [Int32]){
// Model Path is the location of the model in the bundle
let modelPath = Bundle.main.path(forResource: "model", ofType: "tflite")
var interpreter: Interpreter
do{
interpreter = try Interpreter(modelPath: modelPath!)
} catch _{
print("Error loading model!")
return
}
Le fichier de modèle est alors chargé à partir du bundle et un interpréteur est appelé avec lui.
L'étape suivante consiste à copier la mémoire sous-jacente stockée dans la séquence dans un tampon appelé myData,
afin qu'elle puisse être transmise à un tenseur. Lors de l'implémentation du pod TensorFlow Lite, ainsi que de l'interpréteur, vous avez accès à un type de Tensor.
Démarrez le code comme suit (toujours dans la classification func
):
let tSequence = Array(sequence)
let myData = Data(copyingBufferOf: tSequence.map { Int32($0) })
let outputTensor: Tensor
Ne vous inquiétez pas si une erreur s'affiche sur copyingBufferOf
. Cette fonctionnalité sera implémentée ultérieurement en tant qu'extension.
Il est maintenant temps d'allouer des tenseurs sur l'interprète, de copier le tampon de données que vous venez de créer dans le tenseur d'entrée, puis d'appeler l'interprète pour effectuer l'inférence:
do {
// Allocate memory for the model's input `Tensor`s.
try interpreter.allocateTensors()
// Copy the data to the input `Tensor`.
try interpreter.copy(myData, toInputAt: 0)
// Run inference by invoking the `Interpreter`.
try interpreter.invoke()
Une fois l'appel terminé, vous pouvez consulter la sortie de l'interprète pour voir les résultats.
Il s'agit de valeurs brutes (4 octets par neurone) que vous devrez lire et convertir. Comme ce modèle particulier comporte deux neurones de sortie, vous devez lire huit octets qui seront convertis en Float32 pour l'analyse. Vous travaillez avec une mémoire de bas niveau, d'où le unsafeData
.
// Get the output `Tensor` to process the inference results.
outputTensor = try interpreter.output(at: 0)
// Turn the output tensor into an array. This will have 2 values
// Value at index 0 is the probability of negative sentiment
// Value at index 1 is the probability of positive sentiment
let resultsArray = outputTensor.data
let results: [Float32] = [Float32](unsafeData: resultsArray) ?? []
Il est désormais relativement facile d'analyser les données pour déterminer la qualité du spam. Le modèle comporte deux sorties, la première indiquant la probabilité que le message ne soit pas du spam, et la seconde la probabilité qu'il le soit. Vous pouvez donc consulter results[1]
pour trouver la valeur de spam:
let positiveSpamValue = results[1]
var outputString = ""
if(positiveSpamValue>0.8){
outputString = "Message not sent. Spam detected with probability: " + String(positiveSpamValue)
} else {
outputString = "Message sent!"
}
txtOutput.text = outputString
Pour plus de commodité, voici la méthode complète:
func classify(sequence: [Int32]){
// Model Path is the location of the model in the bundle
let modelPath = Bundle.main.path(forResource: "model", ofType: "tflite")
var interpreter: Interpreter
do{
interpreter = try Interpreter(modelPath: modelPath!)
} catch _{
print("Error loading model!")
Return
}
let tSequence = Array(sequence)
let myData = Data(copyingBufferOf: tSequence.map { Int32($0) })
let outputTensor: Tensor
do {
// Allocate memory for the model's input `Tensor`s.
try interpreter.allocateTensors()
// Copy the data to the input `Tensor`.
try interpreter.copy(myData, toInputAt: 0)
// Run inference by invoking the `Interpreter`.
try interpreter.invoke()
// Get the output `Tensor` to process the inference results.
outputTensor = try interpreter.output(at: 0)
// Turn the output tensor into an array. This will have 2 values
// Value at index 0 is the probability of negative sentiment
// Value at index 1 is the probability of positive sentiment
let resultsArray = outputTensor.data
let results: [Float32] = [Float32](unsafeData: resultsArray) ?? []
let positiveSpamValue = results[1]
var outputString = ""
if(positiveSpamValue>0.8){
outputString = "Message not sent. Spam detected with probability: " +
String(positiveSpamValue)
} else {
outputString = "Message sent!"
}
txtOutput.text = outputString
} catch let error {
print("Failed to invoke the interpreter with error: \(error.localizedDescription)")
}
}
12. Ajouter les extensions Swift
Le code ci-dessus utilise une extension du type de données pour vous permettre de copier les bits bruts d'un tableau Int32 dans un Data
. Voici le code de cette extension:
extension Data {
/// Creates a new buffer by copying the buffer pointer of the given array.
///
/// - Warning: The given array's element type `T` must be trivial in that it can be copied bit
/// for bit with no indirection or reference-counting operations; otherwise, reinterpreting
/// data from the resulting buffer has undefined behavior.
/// - Parameter array: An array with elements of type `T`.
init<T>(copyingBufferOf array: [T]) {
self = array.withUnsafeBufferPointer(Data.init)
}
}
Lorsque vous travaillez avec de la mémoire de bas niveau, vous utilisez des données "non sécurisées". Le code ci-dessus vous oblige à initialiser un tableau de données non sécurisées. Cette extension le permet:
extension Array {
/// Creates a new array from the bytes of the given unsafe data.
///
/// - Warning: The array's `Element` type must be trivial in that it can be copied bit for bit
/// with no indirection or reference-counting operations; otherwise, copying the raw bytes in
/// the `unsafeData`'s buffer to a new array returns an unsafe copy.
/// - Note: Returns `nil` if `unsafeData.count` is not a multiple of
/// `MemoryLayout<Element>.stride`.
/// - Parameter unsafeData: The data containing the bytes to turn into an array.
init?(unsafeData: Data) {
guard unsafeData.count % MemoryLayout<Element>.stride == 0 else { return nil }
#if swift(>=5.0)
self = unsafeData.withUnsafeBytes { .init($0.bindMemory(to: Element.self)) }
#else
self = unsafeData.withUnsafeBytes {
.init(UnsafeBufferPointer<Element>(
start: $0,
count: unsafeData.count / MemoryLayout<Element>.stride
))
}
#endif // swift(>=5.0)
}
}
13. Exécuter l'application iOS
Exécutez et testez l'application.
Si tout s'est déroulé comme prévu, l'application devrait s'afficher sur votre appareil comme suit:
Lorsque le message "Achetez mon livre pour apprendre le trading en ligne !" a été envoyé, l'application renvoie une alerte de spam détecté avec une probabilité de 0,99 %.
14. Félicitations !
Vous venez de créer une application très simple qui filtre le texte en vue d'éliminer le spam dans les commentaires à l'aide d'un modèle entraîné sur des données utilisées pour spammer les blogs.
L'étape suivante du cycle de vie type d'un développeur consiste à explorer ce qu'il faudrait pour personnaliser le modèle en fonction des données de votre propre communauté. Vous découvrirez comment procéder dans l'activité du parcours suivante.