Actualiza tu aplicación para usar un modelo de aprendizaje automático para filtrar spam.

1. Antes de comenzar

En este codelab, actualizarás la app que compilaste en el codelab anterior Comienza a usar la clasificación de texto para dispositivos móviles.

Requisitos previos

  • Este codelab se diseñó para desarrolladores experimentados que no tienen experiencia con el aprendizaje automático.
  • El codelab es parte de una ruta de aprendizaje secuenciada. Si aún no completaste el proceso de compilación de una app de estilo de mensajería básica o Crea un modelo de aprendizaje automático de comentarios spam, detente y hazlo ahora.

Qué [desarrollarás o aprenderás]

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

Requisitos

2. Abre la app para Android existente

Puedes obtener el código correspondiente siguiendo el codelab 1 o clonando este repositorio y cargando la app desde TextClassificationStep1.

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

Puedes encontrarlo en la ruta de acceso 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 del modelo y los metadatos

En el codelab Compila un modelo de aprendizaje automático de spam de comentarios, creaste un modelo .TFLITE.

Deberías haber descargado el archivo de modelo. Si no lo tienes, puedes obtenerlo en el repositorio de este codelab. El modelo está disponible aquí.

Crea un directorio de recursos para agregarlo a tu proyecto.

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

d7c3e9f21035fc15.png

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

2137f956a1ba4ef0.png

Verás que hay una nueva carpeta assets disponible en la app.

ae858835e1a90445.png

  1. Haz clic con el botón derecho en Recursos.
  2. En el menú que se abre, verás (en Mac) Reveal in Finder (Mostrar en Finder). Selecciónalo. (En Windows, aparecerá Mostrar en Explorer, en Ubuntu, aparecerá Mostrar en Archivos).

e61aaa3b73c5ab68.png

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

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

14f382cc19552a56.png

  1. Regresa a Android Studio y verás los elementos disponibles en tu carpeta assets.

150ed2a1d2f7a10d.png

4. Actualiza build.gradle para usar TensorFlow Lite

Deberás actualizar el archivo build.gradle para usar TensorFlow Lite y las bibliotecas de tareas de TensorFlow Lite que lo admiten.

Los proyectos de Android a menudo tienen más de uno, así que asegúrate de encontrar el nivel uno de la app. En el explorador de proyectos en la vista de Android, búscalo en la sección Gradle Scripts. La correcta estará etiquetada con .app, como se muestra a continuación:

6426051e614bc42f.png

Deberás realizar dos cambios en este archivo. El primero se encuentra en la sección dependencias, en la parte inferior. Agrega un texto implementation para la biblioteca de tareas de TensorFlow Lite, como el siguiente:

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ó esta información, 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 del SDK de 21. Busca este parámetro de configuración en android > default config y cámbialo a 21:

c100b68450b8812f.png

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

5. Agregar una clase de asistente

Para separar la lógica de inferencia, en la que tu app usa el modelo, de la interfaz de usuario, crea otra clase para controlar la inferencia del modelo. Asígnale el nombre "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 Nueva > Paquete

d5911ded56b5df35.png

  1. En el centro de la pantalla, verás un diálogo en el que se te solicitará que ingreses el nombre del paquete. Agrégalo al final del nombre del paquete actual. (aquí se llaman asistentes).

3b9f1f822f99b371.png

  1. Luego, haz clic con el botón derecho en la carpeta helpers en el explorador de proyectos.
  2. Selecciona Nueva > Java Class y llámalo TextClassificationClient. Editarás el archivo en el siguiente paso.

Tu clase de ayuda 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, 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 instancia de un nuevo tipo NLClassifier desde la ruta del modelo. La ruta 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 cadena en tokens, usar la longitud de secuencia correcta, pasarla al modelo y analizar los resultados.

(Para obtener más detalles al respecto, consulta la sección Crea un modelo de aprendizaje automático de comentarios spam).

La clasificación se realiza en el método de clasificación, en el que le pasas una cadena, y se mostrará un List. Cuando se usan modelos de aprendizaje automático para clasificar contenido y se quiere determinar si una cadena es spam o no, es común que se devuelvan todas las respuestas con probabilidades asignadas. Por ejemplo, si le envías 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 categorías Spam/No spam son categorías, por lo que los List devueltos contendrán estas probabilidades. Lo analizarás más tarde.

Ahora que tienes la clase auxiliar, vuelve a tu MainActivity y actualízala a fin de usarla para clasificar tu texto. Lo verás en el siguiente paso.

6. Clasificar el texto

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

  1. En la parte superior de MainActivity.kt, junto con las demás 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 auxiliar:
val client = TextClassificationClient(applicationContext)
client.load()

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

btnSendText.setOnClickListener {
     var toSend:String = txtInput.text.toString()
     txtOutput.text = toSend
 }
  1. Actualízala para que se vea 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 solo generar la salida de la entrada del usuario a clasificarla primero.

  1. Con esta línea, tomarás la cadena que ingresó el usuario y la pasarás al modelo para obtener 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 Falso corresponderá al elemento 0 y Verdadero, el elemento 1).

  1. Para obtener la puntuación de la probabilidad de que el valor sea True, puedes ver los resultados[1].score de la siguiente manera:
    val score = results[1].score
  1. Elegiste un valor de umbral (en este caso, 0.8), en el que indicas que si la puntuación de la categoría Verdadero está por encima del valor del umbral (0.8), el mensaje es spam. De lo contrario, no es spam y es seguro enviarlo:
    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. Aquí puedes ver el modelo en acción. El mensaje "Visita mi blog para comprar cosas" se marcó con alta probabilidad de spam:

1fb0b5de9e566e.png

Por el contrario, dijeron: "Divertido instructivo, gracias". tenía una baja probabilidad de ser spam.

73f38bdb488b29b3.png

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

Puedes obtener el código correspondiente siguiendo el codelab 1 o clonando este repositorio y cargando la app desde TextClassificationStep1. Puedes encontrarlo en la ruta de acceso TextClassificationOnMobile->iOS.

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

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

Ahora, actualizarás esa app para que use un modelo de TensorFlow Lite a fin de detectar comentarios spam en el texto antes de enviarlo. Simplemente simula el envío en esta app renderizando el texto en una etiqueta de salida (pero una app real puede tener un tablero de anuncios, un chat o algo similar).

Para comenzar, necesitas la app del paso 1, que puedes clonar desde el repositorio.

Para incorporar TensorFlow Lite, usarás CocoaPods. Si aún no los tienes instalados, puedes hacerlo siguiendo las instrucciones que se encuentran en https://cocoapods.org/.

  1. Una vez que hayas instalado CocoaPods, crea un archivo con el nombre Podfile en el mismo directorio que .xcproject para la app de TextClassification. El contenido de este archivo debería verse de la siguiente manera:
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".

Con Terminal, navega a ese directorio y ejecuta pod install. Si lo haces correctamente, tendrás un directorio nuevo llamado Pods y un archivo .xcworkspace nuevo creado para ti. Lo usarás en el futuro en lugar de .xcproject.

Si falló, asegúrate de tener el Podfile en el mismo directorio en el que estaba .xcproject. Por lo general, la principal causa es el Podfile en el directorio equivocado o en el nombre de destino incorrecto.

8. Agrega los archivos de modelo y 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. Agrégalas a tu proyecto arrastrándolas y soltándolas desde Finder en la ventana de tu proyecto. Asegúrate de que la opción Agregar a destinos esté marcada:

1ee9eaa00ee79859.png

Cuando termines, deberías verlos en tu proyecto:

b63502b23911fd42.png

  1. Vuelve a verificar que se agreguen al paquete (para que se implementen en un dispositivo). Para ello, selecciona tu proyecto (en la captura de pantalla anterior, es el ícono azul TextClassificationStep2) y observa la pestaña Build Fases:

20b7cb603d49b457.png

9. Cargar el vocabulario

Cuando se realiza una 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 el modelo entrena. Ten en cuenta que la mayoría de los modelos tienen vocabularios diferentes y es importante que uses el vocabulario para el modelo que se generó al momento del entrenamiento. Este es el archivo vocab.txt que acabas de agregar a tu app.

Puedes abrir el archivo en Xcode para ver la codificación. Palabras como "canción" se codifican como 6 y "love" a 12. En realidad, el orden es orden de frecuencia, por lo que "I" fue la palabra más común en el conjunto de datos, seguida de “verificar”.

Cuando tu usuario escriba palabras, querrás codificarlas con este vocabulario antes de enviarlas al modelo para que las clasifique.

Exploremos ese código. Comienza por cargar el vocabulario.

  1. Define una variable de nivel de 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, llámalo desde viewDidLoad:
override func viewDidLoad() {
    super.viewDidLoad()
    txtInput.delegate = self
    loadVocab()
}

10. Convierte una cadena en una secuencia de tokens

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

Por lo general, un modelo de PLN acepta una longitud de secuencia fija. Existen excepciones con modelos compilados con ragged tensors, pero, en su mayoría, verás que fue fija. 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 Model Maker de TensorFlow Lite que usaste antes era 20, así que configúralo aquí también:

let SEQUENCE_LENGTH = 20

Agrega este func que tomará la cadena, la convertirá en minúsculas y quitará cualquier 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á de Int32. Esta opción se elige de forma deliberada, ya que, cuando se trata de pasar valores a TensorFlow Lite, tendrás que trabajar con memoria de bajo nivel, y TensorFlow Lite trata los números enteros de una secuencia de cadenas como enteros de 32 bits. Esto hará que tu vida sea un poco más fácil cuando se trata de pasar cadenas al modelo.

11. Realiza la clasificación

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

Ahora tomarás la oración y se la pasarás al modelo, harás que haga inferencias sobre la oración y analizarás 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 array de tipos de 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
  }

Con esta acción, se cargará el archivo de modelo desde el paquete y se invocará un intérprete con él.

El siguiente paso será copiar la memoria subyacente almacenada en la secuencia en un búfer llamado myData, para que se pueda pasar 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 (dentro de la clasificación func):

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

No te preocupes si recibes un error en copyingBufferOf. Esto 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 al intérprete para que realice 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, puedes consultar el resultado del intérprete para ver los resultados.

Estos serán valores sin procesar (4 bytes por neurona) que luego tendrás que leer y convertir. Como este modelo particular tiene 2 neuronas de salida, deberás leer 8 bytes que se convertirán en Float32 para su análisis. Estás lidiando con memoria de bajo nivel, por eso, se trata de 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 salidas: la primera, con la probabilidad de que el mensaje no sea spam, y la segunda, con la probabilidad de que lo sea. Por lo tanto, puedes consultar 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 tu 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

En el código anterior, se usó una extensión del tipo de datos para permitirte copiar los bits sin procesar de un array de Int32 en un Data. Este es el código de 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 de datos, y el código anterior requiere que inicialices un array de datos no seguros. Esta extensión lo hace posible:

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 no tuviste inconvenientes, deberías ver la app en tu dispositivo de la siguiente manera:

84cbd28d9b1592ed.png

Donde aparece el mensaje "¡Compra mi libro para aprender a operar en línea!" , la app envía una alerta de detección de spam con una probabilidad de 0.99%.

14. ¡Felicitaciones!

Creaste una app muy simple que filtra texto en busca de comentarios spam con un modelo que se entrenó con datos usados para generar spam en los blogs.

El siguiente paso en el ciclo de vida típico de un desarrollador es explorar lo que se necesita para personalizar el modelo según los datos de tu propia comunidad. En la siguiente actividad de ruta de aprendizaje, verás cómo hacerlo.