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 codelab precedenti Inizia a utilizzare la classificazione del testo mobile.

Prerequisiti

  • Questo codelab è stato progettato per sviluppatori esperti che non hanno familiarità 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 lo spam nei commenti, interrompi la procedura e fallo ora.

Cosa [creerai o imparerai]

  • Imparerai a integrare il modello personalizzato nell'app creata nei passaggi precedenti.

Che cosa ti serve

2. Apri l'app per Android esistente.

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

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

Puoi trovare questa impostazione nel percorso TextClassificationOnMobile->Android.

Il codice finito è disponibile anche per te come TextClassificationStep2.

Una volta aperto, puoi passare al passaggio 2.

3. Importa il file del modello e i metadati

Nel codelab Crea un modello di machine learning per lo spam nei commenti, hai creato un modello .TFLITE.

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

Aggiungilo al tuo progetto creando una directory degli asset.

  1. Utilizzando il navigatore del progetto, 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

Ora nell'app è disponibile una nuova cartella asset.

ae858835e1a90445.png

  1. Fai clic con il tasto destro del mouse su asset.
  2. Nel menu che si apre, vedrai (su Mac) Mostra nel Finder. Selezionalo. In Windows viene visualizzato Mostra in Esplora risorse, mentre in Ubuntu Mostra in File.

e61aaa3b73c5ab68.png

Verrà avviato Finder per mostrare 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 li 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 a livello di app. Nel riquadro di esplorazione del progetto nella visualizzazione Android, individua la sezione Gradle Scripts. Quello corretto sarà etichettato con .app, come mostrato qui:

6426051e614bc42f.png

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

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 https://www.tensorflow.org/lite/inference_with_metadata/task_library/nl_classifier per la versione più recente.

Le librerie di attività richiedono anche una versione dell'SDK pari a 21 o successive. Trova questa impostazione in android > default config e modificala impostando il valore 21:

c100b68450b8812f.png

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

5. Aggiungere una classe helper

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

  1. Fai clic con il tasto destro del mouse sul nome del pacchetto in cui si trova il codice MainActivity.
  2. Seleziona Nuovo > Pacchetto.

d5911ded56b5df35.png

  1. Al centro dello schermo verrà visualizzata una finestra di dialogo che ti chiede di inserire il nome del pacchetto. Aggiungilo alla fine del nome del pacchetto attuale. Qui si chiamano aiutanti.

3b9f1f822f99b371.png

  1. Una volta fatto, fai clic con il tasto destro del mouse sulla cartella helpers in Esplora progetti.
  2. Seleziona Nuovo > Classe Java e chiamala TextClassificationClient. Modificherai il file nel passaggio successivo.

La tua classe helper 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 all'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à creata 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 di attività di testo e ti aiuta a convertire la stringa in token, utilizzando la lunghezza della sequenza corretta, passandola al modello e analizzando i risultati.

Per maggiori dettagli, consulta la sezione Creare un modello di machine learning per i commenti spam.

La classificazione viene eseguita nel metodo classify, a cui passi una stringa e che restituisce un List. Quando utilizzi modelli di machine learning per classificare i contenuti in cui vuoi determinare se una stringa è spam o meno, è normale che vengano restituite tutte le risposte, con le probabilità assegnate. Ad esempio, se invii un messaggio che sembra spam, riceverai un elenco di due risposte: una con la probabilità che sia spam e una con la probabilità che non lo sia. Spam/Non spam sono categorie, quindi List restituito conterrà queste probabilità. Lo analizzerai in un secondo momento.

Ora che hai la classe helper, torna al tuo MainActivity e aggiornalo per utilizzarla per classificare il testo. Lo vedrai nel passaggio successivo.

6. Classifica il testo

Nel tuo MainActivity, devi 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. Successivamente, carica gli helper. 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, il 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 restituire 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 alfabeticamente, quindi False sarà l'elemento 0 e True l'elemento 1.)

  1. Per ottenere il punteggio per la probabilità che il valore sia True, puoi esaminare results[1].score in questo modo:
    val score = results[1].score
  1. Hai scelto un valore di soglia (in questo caso 0,8), in base al quale 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 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 contrassegnato come spam con alta probabilità:

1fb0b5de9e566e.png

Al contrario, "Ehi, bel tutorial, grazie!" è stato considerato con una probabilità molto bassa di essere spam:

73f38bdb488b29b3.png

7. Aggiornare l'app per iOS in modo che utilizzi il modello TensorFlow Lite

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

Il codice finito è disponibile anche per te come TextClassificationStep2.

Nel codelab Crea un modello di machine learning per lo spam nei commenti, hai creato un'app molto semplice che consentiva all'utente di digitare un messaggio in un UITextView e di passarlo a un output senza alcun filtro.

Ora aggiornerai l'app in modo che utilizzi un modello TensorFlow Lite per rilevare lo spam nei 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, utilizzerai CocoaPods. Se non li hai ancora installati, puoi farlo seguendo le istruzioni riportate all'indirizzo https://cocoapods.org/.

  1. Una volta installato CocoaPods, crea un file denominato Podfile nella stessa directory di .xcproject per l'app TextClassification. Il contenuto di questo file dovrebbe essere simile al seguente:
target 'TextClassificationStep2' do
  use_frameworks!

  # Pods for NLPClassifier
    pod 'TensorFlowLiteSwift'

end

Il nome della tua app deve essere nella prima riga, anziché "TextClassificationStep2".

Utilizzando il terminale, vai a questa 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 è riuscita, assicurati di avere Podfile nella stessa directory in cui si trovava .xcproject. Il podfile nella directory errata o il nome del target errato sono in genere i principali responsabili.

8. Aggiungere i file del modello e del vocabolario

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

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

1ee9eaa00ee79859.png

Al termine, dovresti visualizzarli nel progetto:

b63502b23911fd42.png

  1. Verifica che siano stati aggiunti al bundle (in modo che vengano implementati su un dispositivo) selezionando il tuo progetto (nello screenshot precedente, è l'icona blu TextClassificationStep2) e controllando la scheda Fasi di compilazione:

20b7cb603d49b457.png

9. Caricare il vocabolario

Quando esegue 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. Tieni presente che la maggior parte dei modelli avrà vocabolari diversi ed è importante utilizzare il vocabolario del modello generato al momento dell'addestramento. Questo è il file vocab.txt che hai appena aggiunto all'app.

Puoi aprire il file in Xcode per visualizzare le codifiche. Parole come "song" sono codificate in 6 e "love" in 12. L'ordine è in realtà ordine di frequenza, quindi "I" è stata 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.

Esploriamo questo codice. Inizia caricando il vocabolario.

  1. Definisci una variabile a livello di classe per archiviare 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 da viewDidLoad:
override func viewDidLoad() {
    super.viewDidLoad()
    txtInput.delegate = self
    loadVocab()
}

10. Trasforma una stringa in una sequenza di token

Gli utenti digitano le parole in 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 NLP in genere accetta una lunghezza di sequenza fissa. Esistono eccezioni con i modelli creati utilizzando ragged tensors, ma per la maggior parte vedrai che è stato corretto. Hai specificato questa lunghezza quando hai creato il modello. Assicurati di utilizzare la stessa durata nell'app per iOS.

Il valore predefinito in Colab per TensorFlow Lite Model Maker 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à qualsiasi segno 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. Questa scelta è intenzionale perché, quando si tratta di passare valori a TensorFlow Lite, si ha a che fare con la memoria di basso livello e TensorFlow Lite tratta gli interi in una sequenza di stringhe come interi a 32 bit. In questo modo, la tua vita sarà (un po') più facile quando si tratta di passare stringhe al modello.

11. Eseguire la classificazione

Per classificare una frase, è necessario prima convertirla in una sequenza di token in base alle parole della frase. Questa operazione è stata eseguita nel passaggio 9.

Ora prenderai la frase e la passerai al modello, farai in modo che il modello esegua l'inferenza sulla frase e analizzerai i risultati.

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

import TensorFlowLite

Inizia con un func che accetta la 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
  }

Viene caricato il file del modello dal bundle e viene richiamato un interprete.

Il passaggio successivo consiste nel copiare la memoria sottostante memorizzata nella sequenza in un buffer chiamato myData, in modo che possa essere passato a un tensore. Quando implementi il pod TensorFlow Lite, oltre all'interprete, ottieni l'accesso a un tipo di tensore.

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 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 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 l'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 due neuroni di output, dovrai leggere 8 byte che verranno convertiti in Float32 per l'analisi. Stai lavorando con la memoria di basso livello, da cui 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, il secondo con la probabilità che lo sia. Puoi quindi esaminare results[1] per trovare il valore di 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 comodità, 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. Aggiungere le estensioni Swift

Il codice precedente 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 questa 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 gestisci la memoria di basso livello, utilizzi dati "non sicuri" e il codice precedente richiede di 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 tutto è andato a buon fine, dovresti vedere l'app sul tuo dispositivo in questo modo:

74cbd28d9b1592ed.png

Nel punto in cui è stato inviato il messaggio "Acquista il mio libro per imparare il trading online!", l'app invia un avviso di spam rilevato con una probabilità del 99%.

14. Complimenti!

Ora hai creato un'app molto semplice che filtra il testo per lo spam nei commenti utilizzando un modello addestrato su dati utilizzati per lo spam nei blog.

Il passaggio successivo nel ciclo di vita tipico di uno sviluppatore è esplorare cosa servirebbe per personalizzare il modello in base ai dati trovati nella tua community. Vedrai come farlo nella prossima attività del percorso.