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 no codelab anterior "Introdução à classificação de textos para dispositivos móveis".

Pré-requisitos

  • Este codelab foi projetado para desenvolvedores experientes e iniciantes em machine learning.
  • O codelab faz parte de um caminho sequenciado. Se você ainda não concluiu Criar um aplicativo de mensagens básico ou Criar um modelo de aprendizado de máquina de spam de comentários, pare e faça isso agora.

O que você vai [criar ou aprender]

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

O que é necessário

2. Abrir o app Android atual

Para acessar 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

É possível encontrar isso no caminho TextClassificationOnMobile->Android.

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

Quando ela estiver aberta, você poderá passar para a etapa 2.

3. Importar o arquivo e os metadados do modelo

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

Você deve ter feito o download do arquivo modelo. Caso não o tenha, você pode obtê-lo no repositório deste codelab. 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 Novo > Diretório.

d7c3e9f21035fc15.png

  1. Na caixa de diálogo Novo diretório, 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á 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. Retorne ao Android Studio. Eles vão estar disponíveis na sua 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, atualize o arquivo build.gradle.

Os projetos Android geralmente têm mais de um, por isso, é importante encontrar o nível do app. No explorador de projetos na visualização do Android, encontre-o na seção Gradle Scripts. O correto será marcado com .app, conforme mostrado aqui:

6426051e614bc42f.png

Você precisa fazer duas alterações nesse arquivo. O primeiro está na seção de dependências, na parte inferior. Adicione um texto implementation à biblioteca de tarefas do TensorFlow Lite da seguinte forma:

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

Talvez o número da versão tenha mudado desde que ela foi escrita. Consulte https://www.tensorflow.org/lite/inference_with_metadata/task_library/nl_classifier para conferir as atualizações mais recentes.

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

c100b68450b8812f.png

Agora que 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 "auxiliar" .

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

d5911ded56b5df35.png

  1. Uma caixa de diálogo vai aparecer no centro da tela pedindo para você inserir o nome do pacote. Adicione-o no final do nome do pacote atual. Aqui, 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 Novo > classe Java e chamá-la de TextClassificationClient. Você vai editar o arquivo na próxima etapa.

A classe auxiliar TextClassificationClient será semelhante a esta, 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 vai instanciar um novo tipo NLClassifier no 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 você convertendo sua string em tokens usando o comprimento de sequência correto, transmitindo-a para o 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 é feita no método de classificação, em que você transmite uma string a ela, e ele 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ê passar uma mensagem que parece spam, receberá uma lista com duas respostas. um com a probabilidade de que seja spam e outro com a probabilidade de não ser. As categorias "Spam" ou "Não é spam" são categorias, portanto, o List retornado terá 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 texto. Você verá isso na próxima etapa.

6. Classificar o texto

No MainActivity, primeiro importe os auxiliares 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, o onClickListener do botão vai ficar 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ê pegará a string inserida pelo usuário e a transmitirá ao modelo, obtendo resultados:
var results:List<Category> = client.classify(toSend)

Há apenas duas categorias, False e True

. O TensorFlow os classifica em ordem alfabética, então "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. Foi escolhido um valor limite (neste caso, 0,8), em que você diz que, se a pontuação da categoria "True" estiver acima do valor limite (0,8), a mensagem é spam. Caso contrário, não é spam e a mensagem pode ser enviada com segurança:
    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 "Acesse meu blog para comprar coisas!" foi sinalizado como de alta probabilidade de spam:

1fb0b5de9e566e.png

E vice-versa: "Ei, tutorial divertido, obrigado!" foi considerada uma probabilidade muito baixa de spam:

73f38bdb488b29b3.png

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

Para acessar o código, siga o codelab 1 ou clone este repositório e carregue o app em TextClassificationStep1. É possível 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 machine learning de spam de comentários", você criou um app muito simples que permite ao usuário digitar uma mensagem em um UITextView e fazer com que ela seja transmitida para uma saída sem filtros.

Agora você 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 recursos 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 desse arquivo deve ser semelhante ao seguinte:
target 'TextClassificationStep2' do
  use_frameworks!

  # Pods for NLPClassifier
    pod 'TensorFlowLiteSwift'

end

O nome do app precisa estar na primeira linha, e não em "TextClassificationStep2".

No Terminal, navegue até esse diretório e execute pod install. Se tudo der certo, você terá um novo diretório chamado Pods e um novo arquivo .xcworkspace criado para você. Você vai usar isso no futuro em vez de .xcproject.

Se isso falhar, confira se o Podfile está no mesmo diretório em que .xcproject estava. Geralmente os principais culpados são o podfile no diretório errado ou o nome de destino incorreto.

8. Adicione os arquivos de modelo e vocabulário

Ao criar o modelo com o TensorFlow Lite Model Maker, foi possível gerar a saída do modelo (como model.tflite) e do vocabulário (como vocab.txt).

  1. Adicione-os ao seu projeto arrastando-os e soltando-os do Finder na janela do projeto. Verifique se a opção adicionar a destinos está marcada:

1ee9eaa00ee79859.png

Quando terminar, eles vão aparecer no 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 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. Este é o arquivo vocab.txt que você acabou de adicionar ao app.

É possível abrir o arquivo no Xcode para ver as codificações. Palavras como "música" são codificadas como 6 e "love" para 12. Na verdade, a ordem é a ordem de frequência, então "I" era a palavra mais comum no conjunto de dados, seguida por "verificar".

Quando os usuários digitarem palavras, será necessário codificá-las com esse vocabulário antes de enviá-las ao modelo para classificação.

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

  1. Defina uma variável de nível de 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, basta chamar em viewDidLoad:
override func viewDidLoad() {
    super.viewDidLoad()
    txtInput.delegate = self
    loadVocab()
}

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

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

Um modelo de PLN costuma aceitar um tamanho de sequência fixo. Existem exceções em modelos criados com ragged tensors, mas na maioria das vezes você verá que ele foi corrigido. Quando você criou o modelo, especificou esse comprimento. Use o mesmo tamanho no seu app iOS.

O padrão do Colab para o TensorFlow Lite Model Maker que você usou antes era 20. Defina esse valor 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. Essa opção foi escolhida deliberadamente porque, quando se trata de transmitir valores para o TensorFlow Lite, você lida 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 quando se trata de passar 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 dela. Isso será feito na etapa 9.

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

Você vai usar o intérprete do TensorFlow Lite, que precisará ser importado:

import TensorFlowLite

Comece com uma func que use 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 subjacente armazenada na sequência em um buffer chamado myData, para que ela possa ser transmitida a um tensor. Ao implementar o pod do TensorFlow Lite e o intérprete, você teve acesso a um tipo de tensor.

Inicie o código desta forma (ainda na classificação 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()

Quando a invocação é concluída, é possível analisar a saída do intérprete para conferir os resultados.

Esses serão valores brutos (4 bytes por neurônio) que você vai precisar 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. Como você está lidando com memória de baixo nível, 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 que seja. Então, você pode verificar 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 facilitar, 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 do 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, use "não seguro" dados, e o código acima precisa que você inicialize uma matriz de dados não seguros. Com esta extensão, 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 tiver dado certo, você verá o aplicativo em seu dispositivo assim:

74cbd28d9b1592ed.png

Em que a mensagem "Compre meu livro para aprender negociação on-line!" é enviado, o app retorna um alerta de spam detectado com uma probabilidade de 0,99%.

14. Parabéns!

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

A próxima etapa no ciclo de vida típico de um desenvolvedor é explorar o que é 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.