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

1. Antes de começar

Neste codelab, você atualizará o app criado nos codelabs anteriores de Primeiros passos com a classificação de texto para dispositivos móveis.

Prerequisites

  • Este codelab foi projetado para desenvolvedores experientes novos no machine learning.
  • O codelab faz parte de um programa sequenciado. Se você ainda não tiver concluído a criação de um app básico de mensagens ou um modelo de aprendizado de máquina de comentários, pare e faça isso agora.

O que você irá [criar ou aprender]

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

Pré-requisitos

2. Abrir o app Android existente

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

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

Você pode encontrá-lo no caminho TextClassificationOnMobile->Android.

O código finished também está disponível para TextClassificationStep2.

Depois que ele for aberto, você poderá seguir para a etapa 2.

3. Importar o arquivo de modelo e os metadados

No codelab "Modelo de aprendizado de máquina para spam de comentários", você criou um modelo .TFLITE.

Você provavelmente fez o download do arquivo de modelo. Caso você não o tenha, consulte-o no repositório deste codelab, e o modelo está disponível neste link.

Para adicioná-lo ao seu projeto, crie um diretório de recursos.

  1. Usando o navegador de projetos, verifique se a opção Android está selecionada 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 Novo diretório, selecione src/main/assets.

2137f956a1ba4ef0.png

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

ae858835e1a90445.png

  1. Clique com o botão direito do mouse em assets.
  2. No menu exibido, você verá, no Mac, Reveal in Finder. Selecione-o. No Windows, ela será Mostrar no Explorador. No Ubuntu, ela exibirá Mostrar no Files.

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 este diretório.

14f382cc19552a56.png

  1. Volte para o Android Studio e veja as opções disponíveis na sua pasta assets.

150ed2a1d2f7a10d.png

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

Para usar o TensorFlow Lite e as bibliotecas de tarefas do TensorFlow Lite compatíveis, será necessário atualizar seu arquivo build.gradle.

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

6426051e614bc42f.png

Você precisará fazer duas mudanças nesse arquivo. A primeira está na seção dependencies na parte inferior. Adicione um texto implementation à biblioteca de tarefas do TensorFlow Lite desta forma:

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

O número da versão pode ter mudado desde que ele foi escrito. Portanto, não deixe de conferir o site https://www.tensorflow.org/lite/inference_with_metadata/task_library/nl_classifier para ver as informações mais recentes.

As bibliotecas de tarefas também exigem uma versão mínima do SDK de 21. Encontre esta configuração em 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, onde o app usa o modelo da interface do usuário, crie outra classe para processar a inferência de modelo. Chame isso de "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. Você verá uma caixa de diálogo no centro da tela solicitando que você insira o nome do pacote. Adicione-o ao fim do nome do pacote atual. Neste caso, ela se chama auxiliares.

3b9f1f822f99b371.png.

  1. Depois de fazer isso, clique com o botão direito na pasta helpers no explorador de projetos.
  2. Selecione New > Java Class e chame-o de TextClassificationClient. Você editará o arquivo na próxima etapa.

A classe auxiliar TextClassificationClient 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 ao interpretador do TensorFlow Lite, carregando o modelo e abstraindo a complexidade do gerenciamento da troca de dados entre seu app e o modelo.

No método load(), ela instanciará um novo tipo de 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 na conversão da string em tokens usando o comprimento correto da sequência, transmitindo-a para o modelo e analisando os resultados.

Para mais detalhes, revise a seção "Criar um modelo de machine learning para spam de comentários".

A classificação é realizada no método de classificação, em que você transmite uma string e 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ê enviar uma mensagem que pareça spam, receberá de volta uma lista com duas respostas: um com a probabilidade de ser spam e o outro com probabilidade de não ser. A categoria "Spam"/"Não é spam" é uma categoria, então a List retornada conterá essas probabilidades. Você analisará isso mais tarde.

Agora que você tem a classe auxiliar, volte para o MainActivity e atualize-o para usar esse método de classificação do texto. Você verá isso na próxima etapa.

6. Classificar o texto

No MainActivity, é necessário importar os auxiliares que você acabou de criar.

  1. Na parte superior de MainActivity.kt, com as outras importações, adicione:
import com.google.devrel.textclassificationstep2.helpers.TextClassificationClient
import org.tensorflow.lite.support.label.Category
  1. Em seguida, você precisará carregar os auxiliares. No 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 ficará assim:

btnSendText.setOnClickListener {
     var toSend:String = txtInput.text.toString()
     txtOutput.text = toSend
 }
  1. Atualize-o para que ele tenha esta aparência:
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 esta linha, você vai converter a string que o usuário inseriu no modelo em resultados:
var results:List<Category> = client.classify(toSend)

Há apenas duas categorias: False e True.

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

  1. Para saber a pontuação da probabilidade de o valor ser True, analise os resultados[1].score assim:
    val score = results[1].score
  1. Escolheram um valor limite (neste caso, 0,8), em que você diz que, se a pontuação da categoria "True" for superior ao limite (0,8), a mensagem será spam. Caso contrário, ele 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. Veja o modelo em ação aqui. A mensagem "Acesse meu blog para comprar produtos!" foi sinalizado como uma alta probabilidade de spam:

1fb0b5de9e566e.png

Por outro lado, "Ei, tutorial divertido, obrigado!" foi considerada muito provável de ser spam:

73f38bdb488b29b3.png

7. Atualizar seu 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 do TextClassificationStep1. Você pode encontrá-lo no caminho TextClassificationOnMobile->iOS.

O código finished também está disponível para TextClassificationStep2.

No codelab "Modelo de aprendizado de máquina para spam de comentários", você criou um app muito simples que permitiu que o usuário digite uma mensagem em uma UITextView e a transmite para uma saída sem filtros.

Agora você atualizará esse app para usar um modelo do TensorFlow Lite para detectar spam de comentários no texto antes do envio. Para simular o envio nesse app, renderize o texto em um rótulo de saída. No entanto, um app real pode ter um quadro de avisos, um chat ou algo semelhante.

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

Para incorporar o TensorFlow Lite, você usará o CocoaPods. Se você ainda não os instalou, 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 será semelhante a este:
target 'TextClassificationStep2' do
  use_frameworks!

  # Pods for NLPClassifier
    pod 'TensorFlowLiteSwift'

end

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

No 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ê usará isso em vez de .xcproject.

Se ocorrer uma falha, verifique se o Podfile está no mesmo diretório em que .xcproject estava. O podfile no diretório incorreto, ou o nome do destino incorreto, geralmente são os principais culpados.

8. Adicionar o modelo e os arquivos de vocabulário

Quando você criou o modelo com o TensorFlow Lite Model Maker, conseguiu gerar o modelo (como model.tflite) e o vocabulário (como vocab.txt).

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

1ee9eaa00ee79859.png

Depois de concluído, eles aparecerão no projeto:

b63502b23911fd42.png

  1. Verifique se ele foi adicionado ao pacote (para que seja implantado em um dispositivo) selecionando o projeto (na captura de tela acima, o ícone azul TextClassificationStep2). Veja a Criar fases:

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 à medida que o modelo é treinado. A maioria dos modelos tem vocabulários diferentes. Por isso, é importante usar o vocabulário que foi gerado no momento do treinamento. Esse é o arquivo vocab.txt que você acabou de adicionar ao app.

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

Quando o usuário digita palavras, convém codificá-las com esse vocabulário antes de enviá-las ao modelo a ser classificado.

Vamos analisar o 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 nesse 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 executá-lo, 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 digitarão palavras como uma frase que se tornará uma string. Cada palavra da frase, se presente no dicionário, será codificada no valor-chave da palavra, conforme definido no vocabulário.

Um modelo PLN geralmente aceita uma duração fixa de sequência. Há exceções com modelos criados usando ragged tensors, mas, na maioria dos casos, ele foi corrigido. Quando foi criado o modelo, você especificou esse comprimento. Use a mesma duração no seu app iOS.

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

let SEQUENCE_LENGTH = 20

Adicione este func, que transformará a string em letras minúsculas e eliminará 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á Int32. Isso é escolhido intencionalmente porque, ao transmitir valores para o TensorFlow Lite, você lidará com memória de baixo nível, e o TensorFlow Lite tratará os números inteiros em uma sequência de strings como números inteiros de 32 bits. Isso facilitará um pouco o processo de transmissão de strings para o modelo.

11. Faça a classificação

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

Agora, você precisará analisar a frase e analisá-la para analisar o resultado.

Isso usará o interpretador do TensorFlow Lite, que você precisará importar:

import TensorFlowLite

Comece com uma 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 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 para um buffer chamado myData,. Assim, ela poderá ser transmitida para um tensor, Ao implementar o pod do TensorFlow Lite e o intérprete, você tem acesso a um Tensor Type.

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 depois invocar o interpretador 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 estiver concluída, será possível ver a saída do intérprete para ver os resultados.

Esses serão os valores brutos (4 bytes por neurônio) que você precisará ler e converter. Como este modelo específico tem dois neurônios de saída, você vai precisar fazer a leitura em 8 bytes que serão convertidos em Float32 para análise. Como você está lidando com memória de baixo nível, o unsafeData é usado.

// 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. Então, você pode analisar 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

Por conveniência, este é 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 para o tipo de dados para permitir que você copie os bits brutos de uma matriz Int32 em 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 inicializar uma matriz de dados não seguros. Essa extensão possibilita 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 correr bem, você verá o app no seu dispositivo da seguinte forma:

74cbd28d9b1592ed.png

Onde a mensagem "Compre meu livro para aprender a trabalhar on-line!" for enviado, o app retornará um alerta de spam detectado com uma probabilidade de 0,99%.

14. Parabéns!

Você criou um app muito simples que filtra texto de 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 do desenvolvedor é explorar o que é necessário para personalizar o modelo com base nos dados encontrados na sua própria comunidade. Você verá como fazer isso na próxima atividade de caminho.