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

1. Prima di iniziare

In questo codelab aggiornerai l'app creata nei precedenti codelab Inizia a utilizzare la classificazione del testo mobile.

Prerequisiti

  • Questo codelab è stato progettato per sviluppatori esperti che non hanno dimestichezza con il machine learning.
  • Il codelab fa parte di un percorso sequenziale. Se non hai ancora completato Crea un'app di messaggistica di base o Crea un modello di machine learning per i commenti spam, interrompi e procedi ora.

Cosa [creerai o imparerai]

  • Scoprirai come integrare il modello personalizzato nell'app creato nei 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 completato è disponibile anche come TextClassificationStep2.

Dopo l'apertura, potrai andare al passaggio 2.

3. Importa il file del modello e i metadati

Nel codelab Crea un modello di machine learning per i commenti 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 di asset.

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

d7c3e9f21035fc15.png

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

2137f956a1ba4ef0.png

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

ae858835e1a90445.png

  1. Fai clic con il tasto destro del mouse su asset.
  2. Nel menu visualizzato, 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 asset.

150ed2a1d2f7a10d.png

4. Aggiorna 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 di livello uno per l'app. Nell'esploratore del progetto nella visualizzazione Android, lo trovi nella sezione Script Gradle. Quella corretta sarà etichettata con .app, come mostrato di seguito:

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, ad esempio:

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

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

Le librerie di attività richiedono inoltre una versione dell'SDK pari a 21 o successive. Trova questa impostazione in android > default config e impostala su 21:

c100b68450b8812f.png

Ora hai tutte le dipendenze, quindi è il momento di iniziare a scrivere codice.

5. Aggiungere una classe di supporto

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. Chiama questo corso "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 della schermata 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 progetti.
  2. Seleziona Nuovo > Classe Java e denominala TextClassificationClient. Modificherai il file nel passaggio successivo.

La classe di assistenza TextClassificationClient avrà questo aspetto (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 per l'interprete TensorFlow Lite, caricando il modello e astraendo la complessità della gestione dello scambio di dati tra l'app e il modello.

Nel metodo load(), verrà creato 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 maggiori dettagli, consulta l'articolo Creare un modello di machine learning per i commenti spam.

La classificazione viene eseguita utilizzando il metodo classify, in cui viene passata una stringa e viene restituito un 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 l'altra con la probabilità che non lo sia. Spam/Non spam sono categorie, quindi il List restituito conterrà queste probabilità. Lo analizzerai in un secondo momento.

Ora che hai la classe di supporto, torna a MainActivity e aggiornala per utilizzarla per classificare il testo. Lo vedrai nel passaggio successivo.

6. Classifica il testo

In MainActivity, devi prima importare gli aiuti 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. A questo punto, devi caricare gli aiuti. In onCreate, subito dopo la riga setContentView, aggiungi queste righe per creare un'istanza e caricare la classe di supporto:
val client = TextClassificationClient(applicationContext)
client.load()

Al momento, 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à non si limita più a stampare l'input dell'utente, ma lo classifica prima.

  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 True sarà l'elemento 1).

  1. Per ottenere il punteggio della probabilità che il valore sia True, puoi esaminare results[1].score come segue:
    val score = results[1].score
  1. Hai scelto un valore di soglia (in questo caso 0,8), che indica che se il punteggio per la categoria True è superiore al valore di soglia (0,8), il messaggio è spam. In caso contrario, non si tratta di spam e il messaggio può essere inviato in tutta 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 acquistare qualcosa" è stato segnalato come spam con elevata probabilità:

1fb0b5de9e566e.png

Al contrario, "Hey, bel tutorial, grazie!" ha una probabilità molto bassa di essere spam:

73f38bdb488b29b3.png

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

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

Il codice completato è disponibile anche 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. Basta simulare 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, utilizzerai CocoaPods. Se non li hai ancora installati, puoi farlo seguendo le istruzioni riportate all'indirizzo https://cocoapods.org/.

  1. Dopo aver installato CocoaPods, crea un file denominato Podfile nella stessa directory del file .xcproject per l'app TextClassification. I contenuti di questo file dovrebbero avere il seguente aspetto:
target 'TextClassificationStep2' do
  use_frameworks!

  # Pods for NLPClassifier
    pod 'TensorFlowLiteSwift'

end

Il nome dell'app deve essere nella prima riga, anziché "TextClassificationStep2".

Utilizzando Terminale, vai alla directory ed esegui pod install. Se l'operazione va a buon fine, verrà creata una nuova directory denominata Pods e un nuovo file .xcworkspace. 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 del vocabolario

Quando hai creato il modello con lo strumento Model Maker di TensorFlow Lite, hai potuto generare il modello (come model.tflite) e il vocabolario (come vocab.txt).

  1. Aggiungili al progetto trascinandolo dal Finder alla 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 vengano implementati su un dispositivo) selezionando il progetto (nello screenshot sopra, è l'icona blu TextClassificationStep2) e controllando la scheda Fasi di compilazione:

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 avrà vocabolari diversi ed è importante utilizzare il vocabolario per il modello generato al momento dell'addestramento. Questo è il file vocab.txt che hai appena aggiunto alla tua app.

Puoi aprire il file in Xcode per visualizzare le codifiche. Parole come "canzone" vengono codificate come 6 e "amore" come 12. L'ordine è in realtà ordine di frequenza, quindi "I" era la parola più comune nel set di dati, seguita da "check".

Quando l'utente digita le parole, devi codificarle con questo vocabolario prima di inviarle 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. Poi 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 una frase che diventerà una stringa. Ogni parola nella frase, se presente nel dizionario, verrà codificata nel valore della chiave per la parola come definito nel vocabolario.

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

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

let SEQUENCE_LENGTH = 20

Aggiungi questo func che prenderà la stringa, la convertirà in minuscolo ed eliminerà la 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. Questa scelta è deliberata perché, quando si tratta di passare valori a TensorFlow Lite, dovrai gestire la memoria di basso livello e TensorFlow Lite tratta gli interi in una sequenza di stringhe come interi a 32 bit. In questo modo, avrai (un po') più di facilità a 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 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
  }

Verrà caricato il file del modello dal bundle e verrà invocato un interprete.

Il passaggio successivo consisterà nel copiare la memoria sottostante memorizzata 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.

Inizia il codice in questo modo (sempre in classify func):

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

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

Ora è il momento di allocare i tensori nell'interprete, copiare il buffer di dati appena creato nel tensore di input e poi chiamare 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()

Al termine dell'invocazione, puoi esaminare 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 modello specifico ha 2 neuroni di output, dovrai leggere 8 byte che verranno convertiti in Float32 per l'analisi. Hai a che fare con la memoria a basso livello, da qui il 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 due output, il primo con la probabilità che il messaggio non sia spam e il secondo con la probabilità che lo sia. Puoi quindi esaminare results[1] per trovare il valore 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

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, si usano dati "non sicuri" e il codice riportato sopra richiede che inizializzare un array di dati non sicuri. Questa estensione lo 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 è andato tutto bene, dovresti vedere l'app sul tuo dispositivo come segue:

74cbd28d9b1592ed.png

Se viene inviato il messaggio "Acquista il mio libro per imparare il trading online", l'app restituisce un avviso di spam rilevato con una probabilità del 99%.

14. Complimenti!

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

Il passaggio successivo nel ciclo di vita tipico dello sviluppatore è esplorare cosa serve per personalizzare il modello in base ai dati trovati nella tua community. Scoprirai come farlo nell'attività del percorso successiva.