Atualizar seu app para usar um modelo de machine learning com filtragem de spam

1. Antes de começar

Neste codelab, você vai atualizar o app criado nos codelabs anteriores de Introdução à classificação de texto em dispositivos móveis.

Pré-requisitos

  • Este codelab foi criado para desenvolvedores experientes que ainda não conhecem o machine learning.
  • O codelab faz parte de um Programa de treinamentos sequencial. Se você ainda não concluiu "Criar um app de estilo de mensagens básico" ou "Criar um modelo de machine learning para spam de comentários", pare e faça isso agora.

O que você vai [criar ou aprender]

  • Você vai aprender a integrar seu modelo personalizado ao app criado nas etapas anteriores.

O que é necessário

2. Abra o app Android atual

Para conseguir o código, siga o Codelab 1 ou clone este repositório e carregue o app em TextClassificationStep1.

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

Você encontra isso no caminho TextClassificationOnMobile->Android.

O código finalizado também está disponível para você como TextClassificationStep2.

Depois de abrir, você pode passar para a etapa 2.

3. Importar o arquivo de modelo e os metadados

No codelab "Criar um modelo de machine learning para spam de comentários", você criou um modelo .TFLITE.

Você precisa ter baixado o arquivo modelo. Se você não tiver, acesse o repositório deste codelab. O modelo está disponível aqui.

Adicione-o ao seu projeto criando um diretório de assets.

  1. No navegador de projetos, verifique se Android está selecionado na parte de cima.
  2. Clique com o botão direito do mouse na pasta app. Selecione New > Directory.

d7c3e9f21035fc15.png

  1. Na caixa de diálogo Novo diretório, selecione src/main/assets.

2137f956a1ba4ef0.png

Uma nova pasta assets vai estar disponível no app.

ae858835e1a90445.png

  1. Clique com o botão direito do mouse em recursos.
  2. No menu que aparece, clique em Mostrar no Finder (no Mac). Selecione. No Windows, a opção é Mostrar no Explorer. No Ubuntu, é Mostrar em arquivos.

e61aaa3b73c5ab68.png

O Finder será aberto para mostrar o local dos arquivos (Explorador de Arquivos no Windows, Arquivos no Linux).

  1. Copie os arquivos labels.txt, model.tflite e vocab para esse diretório.

14f382cc19552a56.png

  1. Volte ao Android Studio. Eles vão estar disponíveis na pasta assets.

150ed2a1d2f7a10d.png

4. Atualizar o build.gradle para usar o TensorFlow Lite

Para usar o TensorFlow Lite e as bibliotecas de tarefas compatíveis, atualize o arquivo build.gradle.

Os projetos do Android geralmente têm mais de um, então encontre o nível app. No explorador de projetos na visualização do Android, encontre-o na seção Gradle Scripts. O correto será rotulado com .app, como mostrado aqui:

6426051e614bc42f.png

Você vai precisar fazer duas mudanças nesse arquivo. A primeira está na seção dependencies na parte de baixo. Adicione um texto implementation para a biblioteca de tarefas do TensorFlow Lite, assim:

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

O número da versão pode ter mudado desde que isso foi escrito. Por isso, confira a versão mais recente em https://www.tensorflow.org/lite/inference_with_metadata/task_library/nl_classifier.

As bibliotecas de tarefas também exigem uma versão mínima do SDK de 21. Encontre essa configuração em android > default config e mude para 21:

c100b68450b8812f.png

Agora você tem todas as dependências. É hora de começar a programar.

5. Adicionar uma classe auxiliar

Para separar a lógica de inferência, em que o app usa o modelo, da interface do usuário, crie outra classe para processar a inferência do modelo. Chame isso de classe "helper".

  1. Clique com o botão direito do mouse no nome do pacote em que o código MainActivity está.
  2. Selecione New > Package.

d5911ded56b5df35.png

  1. Uma caixa de diálogo vai aparecer no centro da tela pedindo para você inserir o nome do pacote. Adicione-o ao final do nome do pacote atual. Aqui, ele é chamado de helpers.

3b9f1f822f99b371.png

  1. Depois disso, clique com o botão direito do mouse na pasta helpers no Project Explorer.
  2. Selecione New > Java Class e nomeie como TextClassificationClient. Você vai editar o arquivo na próxima etapa.

Sua classe auxiliar TextClassificationClient vai ficar assim, embora o nome do pacote possa ser diferente.

package com.google.devrel.textclassificationstep1.helpers;

public class TextClassificationClient {
}
  1. Atualize o arquivo com 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;
    }

}

Essa classe vai fornecer um wrapper para o intérprete do TensorFlow Lite, carregando o modelo e abstraindo a complexidade do gerenciamento da troca de dados entre o app e o modelo.

No método load(), ele vai instanciar um novo tipo NLClassifier do caminho do modelo. O caminho do modelo é simplesmente o nome dele, model.tflite. O tipo NLClassifier faz parte das bibliotecas de tarefas de texto e ajuda você a converter sua string em tokens, usando o comprimento de sequência correto, transmitindo-o ao modelo e analisando os resultados.

Para mais detalhes sobre isso, consulte "Criar um modelo de machine learning para spam de comentários".

A classificação é realizada no método "classify", em que você transmite uma string, e ele retorna um List. Ao usar modelos de machine learning para classificar conteúdo e determinar se uma string é spam ou não, é comum que todas as respostas sejam retornadas, com probabilidades atribuídas. Por exemplo, se você enviar uma mensagem que parece spam, vai receber uma lista de duas respostas: uma com a probabilidade de ser spam e outra com a probabilidade de não ser. "Spam" e "Não é spam" são categorias, então o List retornado vai conter essas probabilidades. Você vai analisar isso mais tarde.

Agora que você tem a classe auxiliar, volte ao MainActivity e atualize-o para usar isso e classificar seu texto. Você vai ver isso na próxima etapa.

6. Classificar o texto

No MainActivity, importe os helpers que você acabou de criar.

  1. Na parte de cima de MainActivity.kt, junto com as outras importações, adicione:
import com.google.devrel.textclassificationstep2.helpers.TextClassificationClient
import org.tensorflow.lite.support.label.Category
  1. Em seguida, carregue os helpers. Em onCreate, imediatamente após a linha setContentView, adicione estas linhas para instanciar e carregar a classe auxiliar:
val client = TextClassificationClient(applicationContext)
client.load()

No momento, o onClickListener do botão deve estar assim:

btnSendText.setOnClickListener {
     var toSend:String = txtInput.text.toString()
     txtOutput.text = toSend
 }
  1. Atualize para que fique assim:
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()
}

Isso muda a funcionalidade de apenas gerar a entrada do usuário para classificá-la primeiro.

  1. Com essa linha, você vai pegar a string que o usuário inseriu e passar para o modelo, recebendo resultados:
var results:List<Category> = client.classify(toSend)

Há apenas duas categorias: False e True

. (O TensorFlow os classifica em ordem alfabética. Portanto, "False" é o item 0 e "True" é o item 1.)

  1. Para receber a pontuação da probabilidade de que o valor seja True, consulte results[1].score desta forma:
    val score = results[1].score
  1. Escolha um valor de limite (neste caso, 0,8). Se a pontuação da categoria "Verdadeiro" for maior que o valor de limite (0,8), a mensagem será considerada spam. Caso contrário, não é spam e a mensagem pode ser enviada:
    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. Confira o modelo em ação aqui. A mensagem "Visite meu blog para comprar coisas!" foi sinalizada como spam com alta probabilidade:

1fb0b5de9e566e.png

Por outro lado, "Ei, tutorial divertido, obrigado!" foi considerado como tendo uma probabilidade muito baixa de ser spam:

73f38bdb488b29b3.png

7. Atualizar o app iOS para usar o modelo do TensorFlow Lite

Para conseguir o código, siga o Codelab 1 ou clone este repositório e carregue o app em TextClassificationStep1. Você encontra isso no caminho TextClassificationOnMobile->iOS.

O código finalizado também está disponível para você como TextClassificationStep2.

No codelab "Criar um modelo de machine learning para spam de comentários", você criou um app muito simples que permitia ao usuário digitar uma mensagem em um UITextView e transmiti-la para uma saída sem filtragem.

Agora, você vai atualizar esse app para usar um modelo do TensorFlow Lite e detectar spam de comentários no texto antes de enviar. Simule o envio no app renderizando o texto em um rótulo de saída. Um app real pode ter um quadro de avisos, um chat ou algo semelhante.

Para começar, você vai precisar do app da etapa 1, que pode ser clonado do repositório.

Para incorporar o TensorFlow Lite, use o CocoaPods. Se você ainda não tiver feito isso, siga as instruções em https://cocoapods.org/.

  1. Depois de instalar o CocoaPods, crie um arquivo chamado Podfile no mesmo diretório do .xcproject para o app TextClassification. O conteúdo desse arquivo deve ser assim:
target 'TextClassificationStep2' do
  use_frameworks!

  # Pods for NLPClassifier
    pod 'TensorFlowLiteSwift'

end

O nome do app precisa estar na primeira linha, em vez de "TextClassificationStep2".

Usando o terminal, navegue até esse diretório e execute pod install. Se for bem-sucedido, você terá um novo diretório chamado Pods e um novo arquivo .xcworkspace criado para você. Você vai usar isso no futuro em vez do .xcproject.

Se a operação falhar, verifique se você tem o Podfile no mesmo diretório em que .xcproject estava. O podfile no diretório errado ou o nome de destino incorreto geralmente são os principais culpados.

8. Adicionar os arquivos de modelo e vocabulário

Ao criar o modelo com o TensorFlow Lite Model Maker, você pôde gerar o modelo (como model.tflite) e o vocabulário (como vocab.txt).

  1. Arraste e solte os arquivos do Finder na janela do projeto para adicioná-los. Verifique se a opção Adicionar a destinos está marcada:

1ee9eaa00ee79859.png

Quando terminar, você vai encontrá-los no seu projeto:

b63502b23911fd42.png

  1. Confira se eles foram adicionados ao pacote (para serem implantados em um dispositivo). Para isso, selecione seu projeto (na captura de tela acima, é o ícone azul TextClassificationStep2) e confira a guia Build Phases:

20b7cb603d49b457.png

9. Carregar o vocabulário

Na classificação de PLN, o modelo é treinado com palavras codificadas em vetores. O modelo codifica palavras com um conjunto específico de nomes e valores que são aprendidos durante o treinamento. A maioria dos modelos tem vocabulários diferentes, e é importante usar o vocabulário do modelo gerado no momento do treinamento. Esse é o arquivo vocab.txt que você acabou de adicionar ao app.

Abra o arquivo no Xcode para conferir as codificações. Palavras como "música" são codificadas como 6 e "amor" como 12. A ordem é de frequência. Portanto, "I" foi a palavra mais comum no conjunto de dados, seguida por "check".

Quando o usuário digitar palavras, codifique-as com esse vocabulário antes de enviá-las ao modelo para classificação.

Vamos analisar esse código. Comece carregando o vocabulário.

  1. Defina uma variável no nível da classe para armazenar o dicionário:
var words_dictionary = [String : Int]()
  1. Em seguida, crie um func na classe para carregar o vocabulário neste dicionário:
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 executar, chame-o de dentro de viewDidLoad:
override func viewDidLoad() {
    super.viewDidLoad()
    txtInput.delegate = self
    loadVocab()
}

10. Transformar uma string em uma sequência de tokens

Os usuários vão digitar palavras como uma frase, que se tornará uma string. Cada palavra na frase, se presente no dicionário, será codificada no valor da chave da palavra, conforme definido no vocabulário.

Um modelo de PNL geralmente aceita um comprimento de sequência fixo. Há exceções com modelos criados usando ragged tensors, mas, na maior parte, você vai notar que o problema foi corrigido. Você especificou esse comprimento ao criar o modelo. Use o mesmo comprimento no seu app iOS.

O padrão no Colab para o TensorFlow Lite Model Maker que você usou antes era 20. Portanto, configure isso aqui também:

let SEQUENCE_LENGTH = 20

Adicione este func, que vai pegar a string, converter em minúsculas e remover qualquer pontuação:

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
}

A sequência será de Int32s. Isso é escolhido propositadamente porque, ao transmitir valores para o TensorFlow Lite, você vai lidar com memória de baixo nível, e o TensorFlow Lite trata os números inteiros em uma sequência de strings como números inteiros de 32 bits. Isso vai facilitar um pouco sua vida ao transmitir strings para o modelo.

11. Fazer a classificação

Para classificar uma frase, ela precisa ser convertida em uma sequência de tokens com base nas palavras dela. Isso foi feito na etapa 9.

Agora você vai pegar a frase e transmiti-la ao modelo, fazer com que ele faça inferências sobre a frase e analise os resultados.

Isso vai usar o interpretador do TensorFlow Lite, que você precisa importar:

import TensorFlowLite

Comece com um func que receba sua sequência, que era uma matriz 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
  }

Isso vai carregar o arquivo modelo do pacote e invocar um interpretador com ele.

A próxima etapa é copiar a memória armazenada na sequência para um buffer chamado myData,, para que possa ser transmitida a um tensor. Ao implementar o pod do TensorFlow Lite e o interpretador, você teve acesso a um tipo de tensor.

Comece o código assim (ainda em classify func):

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

Não se preocupe se você receber um erro em copyingBufferOf. Isso será implementado como uma extensão mais tarde.

Agora é hora de alocar tensores no intérprete, copiar o buffer de dados que você acabou de criar para o tensor de entrada e invocar o intérprete para fazer a inferência:

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

Depois que a invocação for concluída, confira a saída do intérprete para ver os resultados.

Esses serão valores brutos (4 bytes por neurônio) que você precisará ler e converter. Como esse modelo específico tem dois neurônios de saída, é necessário ler 8 bytes que serão convertidos em Float32s para análise. Você está lidando com memória de baixo nível, daí o 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) ?? []

Agora é relativamente fácil analisar os dados para determinar a qualidade do spam. O modelo tem duas saídas: a primeira com a probabilidade de a mensagem não ser spam e a segunda com a probabilidade de ela ser. Portanto, você pode consultar results[1] para encontrar o 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 sua conveniência, aqui está o 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. Adicionar as extensões Swift

O código acima usou uma extensão do tipo de dados para permitir que você copiasse os bits brutos de uma matriz Int32 em um Data. Confira o código dessa extensão:

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

Ao lidar com memória de baixo nível, você usa dados "não seguros", e o código acima exige que você inicialize uma matriz de dados não seguros. Essa extensão torna isso possível:

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. Executar o app iOS

Execute e teste o app.

Se tudo der certo, o app vai aparecer no seu dispositivo assim:

74cbd28d9b1592ed.png

Quando a mensagem "Compre meu livro para aprender a negociar on-line!" é enviada, o app envia um alerta de spam detectado com uma probabilidade de 99%.

14. Parabéns!

Você criou um app muito simples que filtra texto para spam de comentários usando um modelo treinado com dados usados para spam em blogs.

A próxima etapa no ciclo de vida típico do desenvolvedor é explorar o que seria necessário para personalizar o modelo com base nos dados encontrados na sua própria comunidade. Você vai aprender a fazer isso na próxima atividade do Programa de treinamentos.