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 "Introdução à classificação de texto em dispositivos móveis".

Pré-requisitos

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

O que você vai [criar ou aprender]

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

O que é necessário

2. Abrir o app Android

Você pode receber o código seguindo o Codelab 1 ou clonando este repositório e carregando o app em TextClassificationStep1.

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

Você pode encontrar isso no caminho TextClassificationOnMobile->Android.

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

Quando ele abrir, você poderá seguir 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 feito o download do arquivo de modelo. Se você não tiver, poderá fazer o download no repo deste codelab, e o modelo está disponível aqui.

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

  1. Usando o navegador do projeto, verifique se Android está selecionado na parte superior.
  2. Clique com o botão direito do mouse na pasta app. Selecione New > Directory.

d7c3e9f21035fc15.png

  1. Na caixa de diálogo New Directory, selecione src/main/assets.

2137f956a1ba4ef0.png

Você verá que uma nova pasta assets está disponível no app.

ae858835e1a90445.png

  1. Clique com o botão direito do mouse em Recursos.
  2. No menu exibido, você verá (no Mac) Reveal in Finder. Selecione-o. No Windows, a opção Show in Explorer (Mostrar no Explorador) aparece. No Ubuntu, a opção Show in Files aparece.

e61aaa3b73c5ab68.png

O Finder será iniciado para mostrar o local dos arquivos (File Explorer no Windows, Files no Linux).

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

14f382cc19552a56.png

  1. Volte ao Android Studio e 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 do TensorFlow Lite que oferecem suporte a ele, é necessário atualizar 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 Android, encontre-o na seção Gradle Scripts. O correto será marcado com .app, como mostrado aqui:

6426051e614bc42f.png

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

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

O número da versão pode ter mudado desde a escrita deste artigo. 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, então é 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 essa classe de "auxiliar".

  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 que você insira o nome do pacote. Adicione-o ao final do nome do pacote atual. Aqui, eles são chamados de helpers.

3b9f1f822f99b371.png

  1. Em seguida, clique com o botão direito do mouse na pasta helpers no explorador de projetos.
  2. Selecione New > Java Class e dê o nome de TextClassificationClient a ela. 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 fornecerá um wrapper para o intérprete do TensorFlow Lite, carregando o modelo e abstraindo a complexidade de gerenciar a troca de dados entre seu app e o modelo.

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

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

A classificação é realizada no método de classificação, em que você transmite uma string, e ela retorna um List. Ao usar modelos de aprendizado de máquina para classificar conteúdo em que você quer determinar se uma string é spam ou não, é comum que todas as respostas sejam retornadas com probabilidades atribuídas. Por exemplo, se você transmitir uma mensagem que parece spam, receberá uma lista com duas respostas: uma com a probabilidade de spam e a outra com a probabilidade de não ser. Spam/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 para a MainActivity e atualize-a para usar a classe auxiliar na classificação do seu texto. Você vai conferir isso na próxima etapa.

6. Classificar o texto

No MainActivity, primeiro 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 auxiliares. 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, a onClickListener do botão vai ficar assim:

btnSendText.setOnClickListener {
     var toSend:String = txtInput.text.toString()
     txtOutput.text = toSend
 }
  1. Atualize para ficar 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 digitou e transmiti-la ao modelo, recebendo os resultados:
var results:List<Category> = client.classify(toSend)

Há apenas duas categorias, False e True

. O TensorFlow os classifica alfabeticamente, portanto, False será o item 0 e True será o item 1.

  1. Para conseguir a pontuação da probabilidade de o valor ser True, analise results[1].score assim:
    val score = results[1].score
  1. Escolha um valor de limite (neste caso, 0,8), em que você diz que, se a pontuação da categoria "True" estiver acima do valor de limite (0,8), a mensagem é 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, "Hey, fun tutorial, thanks!" tem uma probabilidade muito baixa de ser spam:

73f38bdb488b29b3.png

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

Você pode receber o código seguindo o Codelab 1 ou clonando este repositório e carregando o app em TextClassificationStep1. Você pode encontrar isso no caminho TextClassificationOnMobile->iOS.

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

No codelab "Criar um modelo de aprendizado de máquina 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 do envio. Basta simular o envio neste aplicativo, renderizando o texto em um marcador de saída (mas um aplicativo real pode ter um quadro de avisos, um bate-papo ou algo semelhante).

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

Para incorporar o TensorFlow Lite, você vai usar o CocoaPods. Se você ainda não tiver esses componentes instalados, siga as instruções em https://cocoapods.org/.

  1. Depois de instalar o CocoaPods, crie um arquivo com o nome Podfile no mesmo diretório que o .xcproject do app TextClassification. O conteúdo do arquivo será semelhante a este:
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, um novo diretório chamado Pods e um novo arquivo .xcworkspace serão criados. Você vai usar isso no futuro em vez de .xcproject.

Se isso falhar, verifique se o Podfile está no mesmo diretório em que o .xcproject estava. O podfile no diretório errado ou o nome de destino errado 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ê conseguiu gerar o modelo (como model.tflite) e o vocabulário (como vocab.txt).

  1. Para adicionar os arquivos ao projeto, arraste e solte-os do Finder para a janela do projeto. Verifique se a opção Adicionar aos destinos está marcada:

1ee9eaa00ee79859.png

Quando terminar, eles vão aparecer no seu projeto:

b63502b23911fd42.png

  1. Verifique se elas foram adicionadas ao pacote (para que sejam implantadas em um dispositivo). Para isso, selecione seu projeto (na captura de tela acima, é o ícone azul TextClassificationStep2) e consulte a guia Build Phases:

20b7cb603d49b457.png

9. Carregar o Vocab

Ao fazer a 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 à medida que o modelo é treinado. 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.

É possível abrir o arquivo no Xcode para conferir as codificações. Palavras como "song" são codificadas como 6 e "love" como 12. A ordem é ordem de frequência, então "I" foi a palavra mais comum no conjunto de dados, seguida por "check".

Quando o usuário digitar palavras, você vai querer codificá-las com esse vocabulário antes de enviar para o modelo ser classificado.

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 fazer isso, chame-o em 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 vai 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 NLP geralmente aceita uma duração de sequência fixa. Há exceções com modelos criados usando ragged tensors, mas, na maioria das vezes, o problema é corrigido. Quando você criou o modelo, especificou esse comprimento. Use o mesmo tamanho no app iOS.

O padrão no Colab para o TensorFlow Lite Model Maker que você usou anteriormente foi 20, então configure isso aqui também:

let SEQUENCE_LENGTH = 20

Adicione este func, que vai converter a string em letras 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
}

Observe que a sequência será de Int32. Isso é escolhido deliberadamente 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 string como números inteiros de 32 bits. Isso vai facilitar (um pouco) sua vida quando se trata de transmitir strings para o modelo.

11. Faça a classificação

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

Agora você vai transmitir a frase para o modelo, fazer com que ele faça a inferência na frase e analisar os resultados.

Essa ação usa o intérprete do TensorFlow Lite, que você precisará importar:

import TensorFlowLite

Comece com um func que recebe 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 de modelo do pacote e invocar um intérprete com ele.

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

Inicie o código assim (ainda na func de classificação):

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

Quando a invocação for concluída, você poderá conferir a saída do interpretador para ver os resultados.

Esses são valores brutos (4 bytes por neurônio) que você precisa ler e converter. Como esse modelo específico tem dois neurônios de saída, você precisa ler 8 bytes que serão convertidos em Float32 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 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, confira 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ê copie os bits brutos de uma matriz Int32 para um Data. Veja 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 precisa que você inicialize uma matriz de dados não seguros. Essa extensão permite o seguinte:

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 tiver corrido bem, você verá o app no seu dispositivo assim:

74cbd28d9b1592ed.png

Quando a mensagem "Compre meu livro para aprender sobre negociação on-line" foi enviada, o app enviou um alerta de spam detectado com uma probabilidade de 0,99%.

14. Parabéns!

Você criou um app muito simples que filtra textos de 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 é descobrir o que é necessário para personalizar o modelo com base nos dados encontrados na sua comunidade. Você vai aprender a fazer isso na próxima atividade do Programa de treinamentos.