Aggiorna l'app per utilizzare un modello di machine learning per filtrare lo spam

1. Prima di iniziare

In questo codelab, aggiornerai l'app che hai creato nei precedenti codelab per iniziare a usare la classificazione dei testi sui dispositivi mobili.

Prerequisiti

  • Questo codelab è stato progettato per sviluppatori esperti che non hanno mai utilizzato il machine learning.
  • Il codelab fa parte di un percorso in sequenza. Se non hai ancora completato lo sviluppo di un'app per lo stile di messaggistica di base o la creazione di un modello di machine learning come spam nei commenti, mettiti subito al lavoro.

Che cosa [creerai o apprenderai]

  • Imparerai a integrare il tuo modello personalizzato nella tua app seguendo i passaggi precedenti.

Che cosa ti serve

2. Apri l'app per Android esistente

Puoi ottenere il codice seguendo il codelab 1 oppure clonando questo repository e caricando l'app da TextClassificationStep1.

git clone https://github.com/googlecodelabs/odml-pathways

Puoi trovarlo nel percorso TextClassificationOnMobile->Android.

Il codice completo è disponibile anche per te come TextClassificationStep2.

Dopo l'apertura, potrai andare al passaggio 2.

3. Importa il file del modello e i metadati

Nel codelab di un modello di machine learning "Crea un commento spam", hai creato un modello .TFLITE.

Dovresti aver scaricato il file del modello. Se non ce l'hai, puoi recuperarlo dal repository per questo codelab e il modello è disponibile qui.

Aggiungila al progetto creando una directory degli asset.

  1. Utilizzando la barra di navigazione dei progetti, assicurati che Android sia selezionato in alto.
  2. Fai clic con il tasto destro del mouse sulla cartella app. Seleziona Nuovo > Directory.

d7c3e9f21035fc15.png

  1. Nella finestra di dialogo Nuova directory, seleziona src/main/assets.

2137f956a1ba4ef0.png

Vedrai che ora è disponibile una nuova cartella degli asset nell'app.

ae858835e1a90445.png

  1. Fai clic con il tasto destro del mouse sugli asset.
  2. Nel menu che si apre, vedrai (su Mac) Mostra nel Finder. Selezionala. Su Windows sarà visualizzato Mostra in Explorer, su Ubuntu sarà Mostra in File.

e61aaa3b73c5ab68.png

Si aprirà il Finder che mostra la posizione dei file (Esplora file su Windows, File su Linux).

  1. Copia i file labels.txt, model.tflite e vocab in questa directory.

14f382cc19552a56.png

  1. Torna ad Android Studio e le vedrai disponibili nella cartella assets.

150ed2a1d2f7a10d.png

4. Aggiorna il file build.gradle per utilizzare TensorFlow Lite

Per utilizzare TensorFlow Lite e le librerie di attività TensorFlow Lite che lo supportano, devi aggiornare il file build.gradle.

I progetti Android spesso ne hanno più di uno, quindi assicurati di trovare quello per l'app di livello uno. In Esplora progetti nella vista Android, individualo nella sezione Gradle Script. Quello corretto sarà etichettato con .app, come mostrato qui:

6426051e614bc42f.png

Devi apportare due modifiche a questo file. Il primo si trova nella sezione dipendenze in basso. Aggiungi un testo implementation per la libreria di attività TensorFlow Lite, in questo modo:

implementation 'org.tensorflow:tensorflow-lite-task-text:0.1.0'

Il numero di versione potrebbe essere cambiato da quando è stato scritto, quindi assicurati di controllare https://www.tensorflow.org/lite/inference_with_metadata/task_library/nl_classifier per avere la versione più recente.

Le librerie delle attività richiedono anche una versione minima dell'SDK pari a 21. Puoi trovare questa impostazione in android > default config e imposta 21:

c100b68450b8812f.png

Ora hai tutte le dipendenze, è ora di iniziare a programmare!

5. Aggiungi un corso Helper

Per separare la logica di inferenza, in cui l'app utilizza il modello, dall'interfaccia utente, crea un'altra classe per gestire l'inferenza del modello. Chiamalo "aiutante" .

  1. Fai clic con il pulsante destro del mouse sul nome del pacchetto che contiene il codice MainActivity.
  2. Seleziona Nuovo > Pacchetto.

d5911ded56b5df35.png

  1. Al centro dello schermo viene visualizzata una finestra di dialogo che ti chiede di inserire il nome del pacchetto. Aggiungilo alla fine del nome del pacchetto corrente. In questo caso la chiamata è helper.

3b9f1f822f99b371.png

  1. Al termine, fai clic con il tasto destro del mouse sulla cartella helpers in Esplora progetto.
  2. Seleziona Nuovo > Java, denominala TextClassificationClient. Modificherai il file nel passaggio successivo.

La tua classe helper TextClassificationClient sarà simile a questa (anche se il nome del pacchetto potrebbe essere diverso).

package com.google.devrel.textclassificationstep1.helpers;

public class TextClassificationClient {
}
  1. Aggiorna il file con questo codice:
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;
    }

}

Questa classe fornirà un wrapper all'interprete di TensorFlow Lite, caricando il modello e astraendo la complessità della gestione dell'interscambio di dati tra la tua app e il modello.

Nel metodo load(), crea un'istanza di un nuovo tipo NLClassifier dal percorso del modello. Il percorso del modello è semplicemente il nome del modello, model.tflite. Il tipo NLClassifier fa parte delle librerie delle attività di testo e ti aiuta a convertire la stringa in token, a utilizzare la lunghezza di sequenza corretta, a passarla al modello e ad analizzare i risultati.

Per saperne di più, consulta l'articolo Sviluppare un modello di machine learning basato sullo spam dei commenti.

La classificazione viene eseguita utilizzando il metodo "Class", in cui viene passata una stringa e viene restituito un valore List. Quando utilizzi modelli di machine learning per classificare contenuti in cui vuoi determinare se una stringa è spam o meno, è normale che vengano restituite tutte le risposte, con probabilità assegnate. Ad esempio, se passi un messaggio che sembra spam, riceverai un elenco di 2 risposte; una con la probabilità che si tratti di spam e una con la probabilità che non lo sia. Le categorie Spam/Non spam sono categorie, quindi i valori List restituiti conterrà queste probabilità. Lo analizzerai in seguito.

Ora che hai il corso helper, torna al tuo MainActivity e aggiornalo in modo da utilizzarlo per classificare il tuo testo. Questa informazione sarà visibile nel passaggio successivo.

6. Classifica il testo

Nel tuo MainActivity, dovrai prima importare gli helper che hai appena creato.

  1. Nella parte superiore di MainActivity.kt, insieme alle altre importazioni, aggiungi:
import com.google.devrel.textclassificationstep2.helpers.TextClassificationClient
import org.tensorflow.lite.support.label.Category
  1. Ora vorrai caricare gli assistenti. In onCreate, subito dopo la riga setContentView, aggiungi queste righe per creare un'istanza e caricare la classe helper:
val client = TextClassificationClient(applicationContext)
client.load()

Al momento, l'elemento onClickListener del pulsante dovrebbe avere il seguente aspetto:

btnSendText.setOnClickListener {
     var toSend:String = txtInput.text.toString()
     txtOutput.text = toSend
 }
  1. Aggiornalo in modo che abbia il seguente aspetto:
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()
}

In questo modo la funzionalità passa dal semplice output dell'input dell'utente alla sua prima classificazione.

  1. Con questa riga, prendi la stringa inserita dall'utente e la passi al modello, ottenendo i risultati:
var results:List<Category> = client.classify(toSend)

Esistono solo due categorie, False e True.

. (TensorFlow li ordina in ordine alfabetico, quindi False sarà l'elemento 0 e Vero sarà l'elemento 1.)

  1. Per ottenere il punteggio con la probabilità che il valore sia True, puoi esaminare i risultati[1].score come questo:
    val score = results[1].score
  1. È stato scelto un valore di soglia (in questo caso 0,8), in cui si specifica che se il punteggio della categoria True è superiore al valore di soglia (0,8), il messaggio è spam. Altrimenti, non si tratta di spam e il messaggio può essere inviato in sicurezza:
    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()
    }
  1. Guarda il modello in azione qui. Il messaggio "Visita il mio blog per comprare cose!" è stato segnalato con un'alta probabilità di spam:

1fb0b5de9e566e.png

E al contrario, "Ehi, divertente tutorial, grazie!". risultava essere una probabilità molto bassa di essere spam:

73f38bdb488b29b3.png

7. Aggiorna l'app per iOS per utilizzare il modello TensorFlow Lite

Puoi ottenere il codice seguendo il codelab 1 oppure clonando questo repository e caricando l'app da TextClassificationStep1. Puoi trovarlo nel percorso TextClassificationOnMobile->iOS.

Il codice completo è disponibile anche per te come TextClassificationStep2.

Nel codelab di un modello di machine learning "Build a comment spam", hai creato un'app molto semplice che consentiva all'utente di digitare un messaggio in un UITextView e di trasmetterlo a un output senza alcun filtro.

Ora aggiorni l'app in modo che utilizzi un modello TensorFlow Lite per rilevare lo spam dei commenti nel testo prima dell'invio. Simula l'invio in questa app eseguendo il rendering del testo in un'etichetta di output (ma un'app reale potrebbe avere una bacheca, una chat o qualcosa di simile).

Per iniziare, ti servirà l'app del passaggio 1, che puoi clonare dal repository.

Per incorporare TensorFlow Lite, userai CocoaPods. Se non li hai già installati, puoi farlo seguendo le istruzioni all'indirizzo https://cocoapods.org/.

  1. Dopo aver installato CocoaPods, crea un file con il nome Podfile nella stessa directory di .xcproject per l'app TextClassification. I contenuti di questo file dovrebbero essere simili ai seguenti:
target 'TextClassificationStep2' do
  use_frameworks!

  # Pods for NLPClassifier
    pod 'TensorFlowLiteSwift'

end

Il nome dell'app deve essere nella prima riga, invece di "TextClassificationStep2".

Utilizzando Terminale, vai alla directory ed esegui pod install. Se l'operazione ha esito positivo, avrai una nuova directory chiamata Pod e un nuovo file .xcworkspace creato per te. Lo utilizzerai in futuro al posto di .xcproject.

Se l'operazione non riesce, assicurati che il podfile sia nella stessa directory in cui si trovava .xcproject. Il podfile nella directory sbagliata o il nome di destinazione sbagliato sono di solito le cause principali.

8. Aggiungi i file del modello e dei vocaboli

Quando hai creato il modello con TensorFlow Lite Model Maker, sei in grado di generare il modello (come model.tflite) e il vocabolario (come vocab.txt).

  1. Aggiungili al tuo progetto trascinandoli dal Finder nella finestra del progetto. Assicurati che l'opzione Aggiungi ai target sia selezionata:

1ee9eaa00ee79859.png

Al termine, dovresti vederle nel tuo progetto:

b63502b23911fd42.png

  1. Verifica che siano aggiunti al bundle (in modo che ne venga eseguito il deployment su un dispositivo) selezionando il tuo progetto (nello screenshot in alto è presente l'icona blu TextClassificationStep2) e osservando la scheda Build Fases:

20b7cb603d49b457.png

9. Carica il vocabolario

Durante la classificazione NLP, il modello viene addestrato con parole codificate in vettori. Il modello codifica le parole con un insieme specifico di nomi e valori che vengono appresi durante l'addestramento del modello. Tieni presente che la maggior parte dei modelli ha vocabolari diversi ed è importante utilizzare per il modello il vocabolario generato al momento dell'addestramento. Questo è il file vocab.txt che hai appena aggiunto alla tua app.

Puoi aprire il file in Xcode per vedere le codifiche. Parole come "canzone" sono codificati come 6 e "love" a 12. Si tratta in realtà dell'ordine di frequenza, quindi "I" era la parola più comune nel set di dati, seguita da "check".

Quando gli utenti digitano delle parole, ti consigliamo di codificarle con questo vocabolario prima di inviarli al modello per la classificazione.

Esaminiamo questo codice. Inizia caricando il vocabolario.

  1. Definisci una variabile a livello di classe per memorizzare il dizionario:
var words_dictionary = [String : Int]()
  1. Quindi crea un func nel corso per caricare il vocabolario in questo dizionario:
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")

    }
}
  1. Puoi eseguirlo chiamandolo dall'interno di viewDidLoad:
override func viewDidLoad() {
    super.viewDidLoad()
    txtInput.delegate = self
    loadVocab()
}

10. Trasforma una stringa in una sequenza di token

Gli utenti digiteranno le parole come frase che diventerà una stringa. Ogni parola della frase, se presente nel dizionario, verrà codificata nel valore chiave della parola come definito nel vocabolario.

Un modello NLP in genere accetta una lunghezza di sequenza fissa. Esistono eccezioni per i modelli creati utilizzando ragged tensors, ma nella maggior parte dei casi noterai che il problema è risolto. Quando hai creato il modello, hai specificato questa lunghezza. Assicurati di utilizzare la stessa lunghezza anche nell'app per iOS.

Il valore predefinito nel Model Maker di Colab per TensorFlow Lite utilizzato in precedenza era 20, quindi impostalo anche qui:

let SEQUENCE_LENGTH = 20

Aggiungi questo func che acquisirà la stringa, la convertirà in minuscolo e rimuoverà eventuali segni di punteggiatura:

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
}

Tieni presente che la sequenza sarà di tipo Int32. La scelta è voluta perché per quanto riguarda il passaggio dei valori a TensorFlow Lite, occorre avere a che fare con memoria di basso livello e TensorFlow Lite tratta i numeri interi in una sequenza di stringhe come numeri interi a 32 bit. Questo ti semplificherà (un po') la vita quando si tratta di passare le stringhe al modello.

11. Esegui la classificazione

Per classificare una frase, deve prima essere convertita in una sequenza di token basati sulle parole che compongono la frase. Questa operazione è stata eseguita nel passaggio 9.

Ora prenderai la frase e la passerai al modello, quindi il modello eseguirà l'inferenza sulla frase e analizzerà i risultati.

Verrà utilizzato l'interprete di TensorFlow Lite, che dovrai importare:

import TensorFlowLite

Inizia con un func che prende la tua sequenza, che era un array di tipi 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
  }

In questo modo il file del modello verrà caricato dal bundle e utilizzato per richiamare un interprete.

Il passaggio successivo sarà copiare la memoria sottostante archiviata nella sequenza in un buffer chiamato myData, in modo che possa essere passata a un tensore. Durante l'implementazione del pod TensorFlow Lite, e anche dell'interprete, hai avuto accesso a un Tensor Type.

Avvia il codice in questo modo (sempre nella categoria func):

let tSequence = Array(sequence)
let myData = Data(copyingBufferOf: tSequence.map { Int32($0) })
let outputTensor: Tensor

Non preoccuparti se ricevi un errore su copyingBufferOf. Questa funzionalità verrà implementata come estensione in un secondo momento.

Ora è il momento di allocare i tensori sull'interprete, copiare il buffer di dati appena creato nel tensore di input e richiamare l'interprete per eseguire l'inferenza:

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()

Una volta completata la chiamata, puoi guardare l'output dell'interprete per vedere i risultati.

Si tratta di valori non elaborati (4 byte per neurone) che dovrai leggere e convertire. Poiché questo particolare modello ha 2 neuroni di output, dovrai leggere in 8 byte che verranno convertiti in Float32 per l'analisi. Hai a che fare con memoria di basso livello, da cui dipende 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) ?? []

Ora è relativamente facile analizzare i dati per determinare la qualità dello spam. Il modello ha 2 output: il primo con la probabilità che il messaggio non sia spam, il secondo con la probabilità che lo sia. Per trovare il valore dello spam, puoi consultare results[1]:

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

Per praticità, ecco il metodo completo:

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. Aggiungi le estensioni Swift

Il codice riportato sopra utilizzava un'estensione del tipo di dati per consentirti di copiare i bit non elaborati di un array Int32 in un Data. Ecco il codice per l'estensione:

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)
  }
}

Quando si ha a che fare con memoria di basso livello, usi il termine "non sicuro" e il codice riportato sopra richiede che tu inizializzassi un array di dati non sicuri. Questa estensione rende possibile:

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. Esegui l'app per iOS

Esegui e testa l'app.

Se tutto è andato a buon fine, dovresti vedere l'app sul tuo dispositivo nel seguente modo:

74cbd28d9b1592ed.png

In corrispondenza del messaggio "Compra il mio libro per imparare a fare trading online!" l'app restituisce un avviso di spam rilevato con una probabilità dello 0, 99%.

14. Complimenti!

Ora hai creato un'app molto semplice che filtra il testo per i commenti spam utilizzando un modello che è stato addestrato sui dati utilizzati per inviare spam ai blog.

Il passaggio successivo nel tipico ciclo di vita dello sviluppatore è quindi esplorare ciò che occorre per personalizzare il modello in base ai dati trovati nella tua community. Vedrai come farlo nella prossima attività del percorso.