Actualiza tu app para usar un modelo de aprendizaje automático con filtrado de spam

1. Antes de comenzar

En este codelab, actualizarás la app que compilaste en los codelabs anteriores de clasificación de textos para dispositivos móviles.

Requisitos previos

  • Este codelab se diseñó para desarrolladores experimentados que no tienen experiencia en el aprendizaje automático.
  • El codelab es parte de una ruta de secuencia. Si todavía no completaste las instrucciones para crear una app básica de estilo de mensajes o un modelo de aprendizaje automático de comentarios spam, detente y hazlo ahora.

Qué [compilarás o aprenderás]

  • Aprenderás a integrar el modelo personalizado en tu app, integrado en los pasos anteriores.

Requisitos

2. Abrir la app para Android existente

Para obtener el código correspondiente, sigue el Codelab 1 o clona este repositorio y carga la app desde TextClassificationStep1.

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

Puedes encontrarlo en la ruta TextClassificationOnMobile->Android.

El código finished también está disponible como TextClassificationStep2.

Una vez que se abra, podrás continuar con el paso 2.

3. Importa el archivo de modelo y los metadatos

En el codelab sobre cómo crear un modelo de aprendizaje automático de comentarios spam, creaste un modelo .TFLITE.

Debería haber descargado el archivo del modelo. Si no la tienes, puedes obtenerla en el repositorio de este codelab, y el modelo está disponible aquí.

Para agregarlo a tu proyecto, crea un directorio de elementos.

  1. Con el navegador del proyecto, asegúrate de que la opción Android esté seleccionada en la parte superior.
  2. Haz clic con el botón derecho en la carpeta de la app. Selecciona New > Directory.

d7c3e9f21035fc15.png

  1. En el diálogo New directory, selecciona src/main/assets.

2137f956a1ba4ef0.png

Ahora, está disponible una nueva carpeta assets en la app.

ae858835e1a90445.png

  1. Haz clic con el botón derecho en assets.
  2. En el menú emergente, se mostrará (en Mac) Revelar en Finder. Selecciónala. En Windows, dirá Show in Explorer. En Ubuntu, aparecerá en Show in Files.

e61aaa3b73c5ab68.png

Se iniciará Finder para mostrar la ubicación de los archivos (File Explorer en Windows, Files en Linux).

  1. Copia los archivos labels.txt, model.tflite y vocab en este directorio.

14f382cc19552a56.png

  1. Regresa a Android Studio y verás que están disponibles en tu carpeta assets.

150ed2a1d2f7a10d.png

4. Actualiza build.gradle para usar TensorFlow Lite

Para usar TensorFlow Lite y las bibliotecas de tareas de TensorFlow Lite que lo admiten, debes actualizar el archivo build.gradle.

Los proyectos de Android suelen tener más de uno, por lo que debes asegurarte de encontrar el nivel de la app uno. En el explorador del proyecto, en la vista de Android, búscalo en la sección Gradle Scripts. El correcto se etiquetará con .app como se muestra a continuación:

6426051e614bc42f.png

Deberá realizar dos cambios a este archivo. La primera está en la sección dependencias, en la parte inferior. Agrega un texto implementation para la biblioteca de tareas de TensorFlow Lite, como se muestra a continuación:

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

Es posible que el número de versión haya cambiado desde que se escribió, por lo que debes asegurarte de consultar https://www.tensorflow.org/lite/inference_with_metadata/task_library/nl_classifier para obtener la información más reciente.

Las bibliotecas de tareas también requieren una versión mínima de SDK de 21. Busca esta configuración en android > default config y cámbiala a 21:

c100b68450b8812f.png

Ahora que tienes todas tus dependencias, es hora de comenzar a programar.

5. Cómo agregar una clase auxiliar

A fin de separar la lógica de inferencia, en la que tu app usa el modelo, desde la interfaz de usuario, crea otra clase para controlar la inferencia del modelo. Llama a esta clase "ayudante".

  1. Haz clic con el botón derecho en el nombre del paquete en el que se encuentra tu código MainActivity.
  2. Selecciona New > Package.

d5911ded56b5df35.png

  1. Verás un diálogo en el centro de la pantalla que te pedirá que ingreses el nombre del paquete. Agrégalo al final del nombre de paquete actual. (aquí se llama helpers).

3b9f1f822f99b371.png

  1. Cuando lo hagas, haz clic con el botón derecho en la carpeta helpers en el explorador del proyecto.
  2. Selecciona New > Java Class y llámalo TextClassificationClient. En el siguiente paso, editarás el archivo.

Tu clase auxiliar TextClassificationClient se verá de la siguiente manera (aunque el nombre de tu paquete puede ser diferente).

package com.google.devrel.textclassificationstep1.helpers;

public class TextClassificationClient {
}
  1. Actualiza el archivo con este código:
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;
    }

}

Esta clase proporcionará un wrapper al intérprete de TensorFlow Lite, lo que cargará el modelo y abstraerá la complejidad de administrar el intercambio de datos entre tu app y el modelo.

En el método load(), se creará una nueva instancia de un tipo NLClassifier nuevo a partir de la ruta del modelo. La ruta de acceso del modelo es simplemente el nombre del modelo, model.tflite. El tipo NLClassifier forma parte de las bibliotecas de tareas de texto y te ayuda a convertir tu string en tokens con la longitud de secuencia correcta, pasarla al modelo y analizar los resultados.

(Para obtener más detalles, vuelve a compilar un modelo de aprendizaje automático de comentarios spam).

La clasificación se realiza en el método de clasificación, en el que se le pasa una string, y se muestra un List. Cuando se usan modelos de aprendizaje automático para clasificar contenido en el que deseas determinar si una string es spam o no, es común que se muestren todas las respuestas, con probabilidades asignadas. Por ejemplo, si le pasas un mensaje que parece spam, recibirás una lista de 2 respuestas. uno con la probabilidad de que sea spam y otro con la probabilidad de que no lo sea. Las opciones Spam/No spam son categorías, por lo que las List que se muestran contendrán estas probabilidades. Lo analizarás más adelante.

Ahora que tienes la clase auxiliar, vuelve a tu MainActivity y actualízala para clasificar el texto. ¡Verás eso en el paso siguiente!

6. Clasifica el texto

En tu MainActivity, primero importarás los asistentes que acabas de crear.

  1. En la parte superior de MainActivity.kt, junto con las otras importaciones, agrega lo siguiente:
import com.google.devrel.textclassificationstep2.helpers.TextClassificationClient
import org.tensorflow.lite.support.label.Category
  1. A continuación, deberás cargar los asistentes. En onCreate, inmediatamente después de la línea setContentView, agrega estas líneas para crear una instancia y cargar la clase de ayuda:
val client = TextClassificationClient(applicationContext)
client.load()

Por el momento, el botón onClickListener debería verse de la siguiente manera:

btnSendText.setOnClickListener {
     var toSend:String = txtInput.text.toString()
     txtOutput.text = toSend
 }
  1. Actualícela de la siguiente manera:
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()
}

Esto cambia la funcionalidad de simplemente generar la entrada del usuario a clasificarla primero.

  1. Con esta línea, tomarás la string que el usuario ingresó y la pasarás al modelo, y mostrará los resultados:
var results:List<Category> = client.classify(toSend)

Solo hay 2 categorías, False y True

. (TensorFlow los ordena alfabéticamente, por lo que False será el elemento 0 y True será el elemento 1).

  1. Si quieres obtener la puntuación de la probabilidad de que el valor sea True, puedes ver los resultados [1].score como este:
    val score = results[1].score
  1. Elegiste un valor de límite (en este caso, 0.8), donde dices que si la puntuación de la categoría True es superior al valor del umbral (0.8), el mensaje es spam. De lo contrario, no es spam y el mensaje es seguro:
    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. Mira el modelo en acción aquí. El mensaje: "Visita mi blog para comprar artículos" se marcó como alta probabilidad de spam:

1fb0b5de9e566e.png

Por el contrario, di "Instructivo divertido, gracias". se detectó que hay pocas probabilidades de spam.

73f38bdb488b29b3.png

7. Actualiza tu app para iOS a fin de usar el modelo de TensorFlow Lite

Para obtener el código correspondiente, sigue el Codelab 1 o clona este repositorio y carga la app desde TextClassificationStep1. Puedes encontrarlo en la ruta TextClassificationOnMobile->iOS.

El código finished también está disponible como TextClassificationStep2.

En el codelab sobre el modelo de aprendizaje automático de comentarios y spam, creaste una app muy simple que le permitió al usuario escribir un mensaje en un UITextView y pasarlo a un resultado sin ningún filtro.

Ahora, actualizarás esa app para usar un modelo de TensorFlow Lite a fin de detectar comentarios spam en el texto antes de enviarlo. Simulan el envío en esta aplicación representando el texto en una etiqueta de salida (pero una aplicación real puede tener una pizarra, un chat o algo similar).

Para comenzar, necesitarás la app del paso 1, que puedes clonar desde el repositorio.

Para incorporar TensorFlow Lite, usarás CocoaPods. Si aún no las instalaste, puedes seguir las instrucciones que aparecen en https://cocoapods.org/.

  1. Una vez que CocoaPods esté instalado, crea un archivo con el nombre Podfile en el mismo directorio que el .xcproject de la app de TextClassification. El contenido del archivo debería verse así:
target 'TextClassificationStep2' do
  use_frameworks!

  # Pods for NLPClassifier
    pod 'TensorFlowLiteSwift'

end

El nombre de tu app debe estar en la primera línea, en lugar de "TextClassificationStep2".

Ve a ese directorio y ejecuta pod install. Si lo haces, tendrás un directorio nuevo llamado Pods y un archivo .xcworkspace nuevo creado para ti. Lo usarás más adelante en lugar de .xcproject.

Si falla, asegúrate de tener Podfile en el mismo directorio en el que había estado .xcproject. El podfile en el directorio incorrecto o en el nombre de destino incorrecto suelen ser los culpables principales.

8. Agrega el modelo y los archivos de vocabulario

Cuando creaste el modelo con el creador de modelos de TensorFlow Lite, pudiste generar el modelo (como model.tflite) y el vocabulario (como vocab.txt).

  1. Para agregarlos a tu proyecto, arrástralos y suéltalos en la ventana de tu proyecto. Asegúrese de que la opción Agregar a objetivos esté marcada:

1ee9eaa00ee79859.png

Cuando termines, deberías verlos en tu proyecto:

b63502b23911fd42.png

  1. Vuelva a verificar que se hayan agregado al paquete (de manera que se implementen en un dispositivo). Para ello, seleccione su proyecto (en la captura de pantalla anterior, es el ícono azul TextClassificationStep2) y observe el Pestaña Fases de compilación:

20b7cb666d49b457.png

9. Carga tu vocabulario

Cuando se realiza la clasificación de PLN, el modelo se entrena con palabras codificadas en vectores. El modelo codifica palabras con un conjunto específico de nombres y valores que se aprenden a medida que se entrena el modelo. Ten en cuenta que la mayoría de los modelos tienen vocabulario diferente y es importante que uses el vocabulario que generó el modelo en el entrenamiento. Este es el archivo vocab.txt que acabas de agregar a tu app.

Para ver el contenido, puedes abrir el archivo en Xcode. Las palabras como "canción" se codifican en 6 y "amor" en 12. En realidad, el orden es el orden de frecuencia, por lo que "I" era la palabra más común en el conjunto de datos, seguida de "verificar".

Cuando el usuario escriba las palabras, deberás codificarlo con este vocabulario antes de enviarlo al modelo para que se lo clasifique.

Exploremos ese código. Primero, carga el vocabulario.

  1. Define una variable a nivel de la clase para almacenar el diccionario:
var words_dictionary = [String : Int]()
  1. Luego, crea un func en la clase para cargar el vocabulario en este diccionario:
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. Para ejecutarlo, llama desde viewDidLoad:
override func viewDidLoad() {
    super.viewDidLoad()
    txtInput.delegate = self
    loadVocab()
}

10. Convierte una string en una secuencia de tokens

Tus usuarios escribirán palabras como una oración que se convertirá en una string. Cada palabra de la oración, si está presente en el diccionario, se codificará en el par clave-valor de la palabra, tal como se define en el vocabulario.

Por lo general, un modelo de PLN acepta una longitud de secuencia fija. Existen excepciones con los modelos compilados con ragged tensors, pero en la mayoría de las ocasiones verás que se corrigió. Cuando creaste el modelo, especificaste esta longitud. Asegúrate de usar la misma longitud en tu app para iOS.

El valor predeterminado de Colab para TensorFlow Lite Model Maker que usaste antes era 20, así que configúralo aquí también:

let SEQUENCE_LENGTH = 20

Agrega el elemento func que tomará la string, la convertirá en minúscula y eliminará la puntuación:

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
}

Ten en cuenta que la secuencia será Int32. Se elige deliberadamente porque, cuando se trata de pasar valores a TensorFlow Lite, se trata de una memoria de bajo nivel, y TensorFlow Lite trata los números enteros en una secuencia de strings como números enteros de 32 bits. Esto hará que tu vida (un poco) sea más fácil cuando se pasen strings al modelo.

11. Realice la clasificación

Para clasificar una oración, primero debe convertirse en una secuencia de tokens basada en las palabras de la oración. Esto se habrá hecho en el paso 9.

Ahora, tomará la oración y la pasará al modelo. Luego, realizará su inferencia y analizará los resultados.

Se usará el intérprete de TensorFlow Lite, que deberás importar:

import TensorFlowLite

Comienza con un func que tome en tu secuencia, que era un arreglo de tipos 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
  }

De esta manera, se cargará el archivo del modelo del paquete y se invocará un intérprete.

El siguiente paso será copiar la memoria subyacente almacenada en la secuencia en un búfer llamado myData, para que pueda pasarse a un tensor. Cuando implementaste el pod de TensorFlow Lite, así como el intérprete, obtuviste acceso a un tipo de tensor.

Inicia el código de la siguiente manera (todavía en la clasificación func):

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

No te preocupes si se produce un error en copyingBufferOf. Esta se implementará como una extensión más adelante.

Ahora es el momento de asignar tensores en el intérprete, copiar el búfer de datos que acabas de crear en el tensor de entrada y, luego, invocar el intérprete para hacer la inferencia:

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 vez que se complete la invocación, podrás ver el resultado del intérprete para ver los resultados.

Estos serán valores sin procesar (4 bytes por neurona) que luego deberás leer y convertir. Como este modelo en particular tiene 2 neuronas de salida, deberás leer en 8 bytes, que se convertirán en Float32 para analizarlos. Se trata de memoria de bajo nivel, por lo tanto, 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) ?? []

Ahora, es relativamente fácil analizar los datos para determinar la calidad del spam. El modelo tiene 2 resultados: el primero con la probabilidad de que el mensaje no sea spam y el segundo con la probabilidad de que lo sea. Por lo tanto, puedes buscar en results[1] para encontrar el valor 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

Para mayor comodidad, este es el método 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. Agrega las extensiones de Swift

El código anterior usó una extensión del tipo Data para permitirte copiar los bits sin procesar de un arreglo Int32 en un Data. Este es el código para esa extensión:

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

Cuando trabajas con memoria de bajo nivel, usas datos “no seguros”, y el código anterior necesita que inicialices un arreglo de datos no seguros. Esta extensión permite lo siguiente:

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. Ejecuta la app para iOS

Ejecuta y prueba la app.

Si todo salió bien, deberías ver la app en tu dispositivo de la siguiente manera:

74cbd26d9b1592ed.png

Donde aparece el mensaje "Compre mi libro para aprender operaciones comerciales en línea" se envió la app y reenvió una alerta de spam detectado con una probabilidad del 0.99%.

14. ¡Felicitaciones!

Creaste una app muy simple que filtra el texto por spam de comentarios con un modelo que se entrenó con datos usados para blogs de spam.

El próximo paso en el ciclo de vida típico del desarrollador es explorar lo que se necesitaría para personalizar el modelo en función de los datos que se encuentran en su comunidad. Verás cómo hacerlo en la próxima actividad del recorrido.