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 Cómo comenzar a usar los codelabs de clasificación de texto para dispositivos móviles.

Requisitos previos

  • Este codelab se diseñó para desarrolladores experimentados que no han usado el aprendizaje automático.
  • El codelab forma parte de una ruta de aprendizaje secuenciada. Si aún no completaste las actividades Crear una app de estilo de mensajería básica o Crear un modelo de aprendizaje automático para detectar spam en comentarios, hazlo ahora.

Qué [crearás o aprenderás]

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

Requisitos

2. Abre la app para Android existente

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

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

Puedes encontrar esta información en la ruta de acceso TextClassificationOnMobile->Android.

El código final también está disponible para ti como TextClassificationStep2.

Una vez que se abra, estará todo listo para pasar al paso 2.

3. Importa el archivo de modelo y los metadatos

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

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

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

  1. En el navegador de proyectos, asegúrate de que esté seleccionado Android en la parte superior.
  2. Haz clic con el botón derecho en la carpeta app. Selecciona New > Directory.

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) Mostrar en Finder. Selecciónalo. (En Windows, dirá Mostrar en el Explorador; en Ubuntu, dirá 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 en este directorio.

14f382cc19552a56.png

  1. Regresa a Android Studio y los verás disponibles en la 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 de la vista de Android, búscala 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ó este artículo, así que asegúrate de consultar https://www.tensorflow.org/lite/inference_with_metadata/task_library/nl_classifier para obtener la versió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 tienes todas tus dependencias, así que 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. Llámala clase “auxiliar”.

  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. 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 denominan ayudantes).

3b9f1f822f99b371.png

  1. Una vez hecho esto, haz clic con el botón derecho en la carpeta helpers del explorador de proyectos.
  2. Selecciona New > 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, cargará el modelo y abstraerá la complejidad de administrar el intercambio de datos entre tu app y el modelo.

En el método load(), creará una instancia de un nuevo tipo NLClassifier a partir de la ruta de acceso 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 cadena en tokens, usar la longitud de secuencia correcta, pasarla al modelo y analizar los resultados.

(Para obtener más detalles sobre estos, consulta Cómo crear un modelo de aprendizaje automático para detectar 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 usas modelos de aprendizaje automático para clasificar contenido en el que deseas determinar si una cadena 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: una con la probabilidad de que sea spam y otra con la probabilidad de que no lo sea. Spam/No spam son categorías, por lo que el List que se muestra contendrá estas probabilidades. Analizarás eso más adelante.

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

6. Clasifica 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 ayudantes. En onCreate, inmediatamente después de la línea setContentView, agrega estas líneas para crear una instancia de la clase de ayuda y cargarla:
val client = TextClassificationClient(applicationContext)
client.load()

En este 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 mostrar 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 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. Para obtener la puntuación de la probabilidad de que el valor sea True, puedes consultar results[1].score de la siguiente manera:
    val score = results[1].score
  1. Elige un valor de umbral (en este caso, 0.8) en el que indiques que, si la puntuación de la categoría Verdadero es superior al valor de umbral (0.8), el mensaje es spam. De lo contrario, no es spam y el mensaje es seguro para enviar:
    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 cosas" se marcó como spam con alta probabilidad:

1fb0b5de9e566e.png

Por el contrario, “Hey, fun tutorial, thanks!” tenía una probabilidad muy baja de ser spam:

73f38bdb488b29b3.png

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

Para obtener el código, sigue el codelab 1 o clona este repositorio y carga la app desde TextClassificationStep1. Puedes encontrar esta información en la ruta de acceso TextClassificationOnMobile->iOS.

El código final también está disponible para ti como TextClassificationStep2.

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

Ahora, actualizarás esa app para usar 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, necesitarás 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 con 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 se realiza correctamente, se creará un directorio nuevo llamado Pods y un archivo .xcworkspace nuevo. La usarás en el futuro en lugar de .xcproject.

Si falló, asegúrate de tener Podfile en el mismo directorio en el que estaba .xcproject. Por lo general, el podfile en el directorio incorrecto o el nombre de destino incorrecto son los principales culpables.

8. Agrega los archivos del modelo y del vocabulario

Cuando creaste el modelo con Model Maker 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 desde el Finder hasta la ventana del 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. Para verificar que se hayan agregado al paquete (de modo que se implementen en un dispositivo), selecciona tu proyecto (en la captura de pantalla anterior, es el ícono azul TextClassificationStep2) y consulta la pestaña Build Phases:

20b7cb603d49b457.png

9. Carga el 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 el modelo se entrena. Ten en cuenta que la mayoría de los modelos tendrán diferentes vocabularios, y es importante que uses el vocabulario de tu modelo que se generó en el 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 en 6 y "amor" en 12. En realidad, el orden es el orden de frecuencia, por lo que “I” fue la palabra más común en el conjunto de datos, seguida de “check”.

Cuando el usuario escriba palabras, deberá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 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, llámalo desde viewDidLoad:
override func viewDidLoad() {
    super.viewDidLoad()
    txtInput.delegate = self
    loadVocab()
}

10. Convierte una cadena en una secuencia de tokens

Los 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 valor clave 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 tu 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á a 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á Int32. Se elige de forma deliberada porque, cuando se trata de pasar valores a TensorFlow Lite, se trata de memoria de bajo nivel, y TensorFlow Lite trata los números enteros en una secuencia de cadenas como números 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 se debe convertir en una secuencia de tokens según las palabras que la componen. 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 tu secuencia, que era un array 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
  }

Con esta acción, se cargará el archivo de modelo desde el paquete y, además, 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.

Comienza el código de esta manera (aún en 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 realizar 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 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 8 bytes que se convertirán en Float32 para el análisis. Estás trabajando con memoria de bajo nivel, de ahí el 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 tiene la probabilidad de que el mensaje no sea spam y el segundo, la probabilidad de que lo sea. Por lo tanto, puedes mirar 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

El código anterior usó una extensión del tipo de datos para permitirte copiar los bits sin procesar de un array 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 datos "no seguros", 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

En el caso del mensaje "Buy my book to learn online trading!", la app envía una alerta de spam detectado con una probabilidad del 99%.

14. ¡Felicitaciones!

Ahora creaste una app muy simple que filtra el texto en busca de comentarios spam con un modelo que se entrenó con datos que se usan para enviar spam a blogs.

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