Обновите свое приложение, чтобы использовать модель машинного обучения для фильтрации спама.

1. Прежде чем начать

В этом практическом занятии вы обновите приложение, созданное вами в предыдущих практических занятиях по началу работы с мобильной классификацией текста.

Предварительные требования

  • Данный практический курс предназначен для опытных разработчиков, только начинающих осваивать машинное обучение.
  • Этот практический курс является частью последовательного обучения. Если вы еще не выполнили задания «Создание простого приложения для обмена сообщениями» или «Создание модели машинного обучения для борьбы со спамом в комментариях», пожалуйста, остановитесь и сделайте это прямо сейчас.

Что вы [построите или узнаете]

  • Вы узнаете, как интегрировать созданную на предыдущих этапах пользовательскую модель в ваше приложение.

Что вам понадобится

2. Откройте существующее приложение Android.

Код можно получить, следуя инструкциям из Codelab 1, или клонировав этот репозиторий и загрузив приложение из TextClassificationStep1 .

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

Это можно найти по пути TextClassificationOnMobile->Android .

Готовый код также доступен вам в виде файла TextClassificationStep2 .

После того, как окно будет открыто, вы готовы перейти к шагу 2.

3. Импортируйте файл модели и метаданные.

В практическом задании «Создание модели машинного обучения для борьбы со спамом в комментариях» вы создали модель в формате .TFLITE.

Вы должны были скачать файл модели. Если у вас его нет, вы можете получить его из репозитория этого практического задания, модель доступна здесь .

Добавьте его в свой проект, создав каталог assets.

  1. В навигаторе проекта убедитесь, что вверху выбран пункт "Android" .
  2. Щелкните правой кнопкой мыши папку приложения . Выберите Создать > Каталог.

d7c3e9f21035fc15.png

  1. В диалоговом окне «Создать каталог» выберите src/main/assets .

2137f956a1ba4ef0.png

Вы увидите, что в приложении появилась новая папка с ресурсами .

ae858835e1a90445.png

  1. Щелкните правой кнопкой мыши по ресурсам.
  2. В открывшемся меню вы увидите (на Mac) «Показать в Finder» . Выберите его. (В Windows это будет «Показать в проводнике» , в Ubuntu — «Показать в файлах» .)

e61aaa3b73c5ab68.png

Запустится Finder , который покажет расположение файлов ( File Explorer в Windows, Files в Linux).

  1. Скопируйте файлы labels.txt , model.tflite и vocab в эту директорию.

14f382cc19552a56.png

  1. Вернитесь в Android Studio, и вы увидите их в папке assets .

150ed2a1d2f7a10d.png

4. Обновите файл build.gradle, чтобы использовать TensorFlow Lite.

Для использования TensorFlow Lite и поддерживающих его библиотек задач TensorFlow Lite вам потребуется обновить файл build.gradle .

В проектах Android часто бывает несколько таких скриптов, поэтому обязательно найдите тот, который относится к уровню приложения . В обозревателе проектов в режиме Android найдите его в разделе Gradle Scripts . Правильный скрипт будет помечен как .app , как показано здесь:

6426051e614bc42f.png

Вам потребуется внести два изменения в этот файл. Первое — в раздел зависимостей внизу. Добавьте текстовую implementation библиотеки задач TensorFlow Lite, например, так:

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

Номер версии мог измениться с момента написания этого текста, поэтому обязательно проверьте актуальную версию на странице https://www.tensorflow.org/lite/inference_with_metadata/task_library/nl_classifier .

Для работы библиотек задач также требуется минимальная версия SDK 21. Найдите этот параметр в разделе android > default config и измените его на 21:

c100b68450b8812f.png

Теперь у вас есть все необходимые зависимости, так что пора начинать программировать!

5. Добавьте вспомогательный класс.

Чтобы отделить логику вывода модели , где ваше приложение использует модель, от пользовательского интерфейса, создайте отдельный класс для обработки вывода модели. Назовите его вспомогательным классом.

  1. Щелкните правой кнопкой мыши по имени пакета, в котором находится код MainActivity .
  2. Выберите Создать > Пакет .

d5911ded56b5df35.png

  1. В центре экрана появится диалоговое окно с запросом на ввод имени пакета. Добавьте его в конец имени текущего пакета. (В данном случае он называется helpers .)

3b9f1f822f99b371.png

  1. После этого щелкните правой кнопкой мыши папку helpers в обозревателе проектов.
  2. Выберите «Создать» > «Класс Java» и назовите его TextClassificationClient . Вы отредактируете файл на следующем шаге.

Ваш вспомогательный класс TextClassificationClient будет выглядеть примерно так (хотя имя вашего пакета может отличаться).

package com.google.devrel.textclassificationstep1.helpers;

public class TextClassificationClient {
}
  1. Обновите файл, добавив следующий код:
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;
    }

}

Этот класс предоставит оболочку для интерпретатора TensorFlow Lite, загружая модель и абстрагируя сложность управления обменом данными между вашим приложением и моделью.

В методе load() будет создан новый экземпляр типа NLClassifier на основе пути к модели. Путь к модели — это просто имя модели, model.tflite . Тип NLClassifier является частью библиотек для работы с текстом и помогает преобразовывать строку в токены, используя правильную длину последовательности, передавая их модели и анализируя результаты.

(Более подробную информацию можно найти в статье «Создание модели машинного обучения для борьбы со спамом в комментариях».)

Классификация выполняется в методе `classify`, которому передается строка, и он возвращает List ). При использовании моделей машинного обучения для классификации контента, где необходимо определить, является ли строка спамом или нет, обычно возвращаются все ответы с присвоенными вероятностями. Например, если вы передадите сообщение, похожее на спам, вы получите список из двух ответов: один с вероятностью того, что это спам, и один с вероятностью того, что это не спам. Спам/Не спам — это категории, поэтому возвращаемый List будет содержать эти вероятности. Вы разберёте его позже.

Теперь, когда у вас есть вспомогательный класс, вернитесь в MainActivity и обновите его, чтобы он использовал этот класс для классификации текста. Вы увидите это на следующем шаге!

6. Классифицируйте текст.

В MainActivity сначала нужно импортировать только что созданные вспомогательные функции!

  1. В верхней части MainActivity.kt , помимо остальных импортируемых элементов, добавьте:
import com.google.devrel.textclassificationstep2.helpers.TextClassificationClient
import org.tensorflow.lite.support.label.Category
  1. Далее вам потребуется загрузить вспомогательные классы. В onCreate , сразу после строки setContentView , добавьте следующие строки для создания экземпляра и загрузки вспомогательного класса:
val client = TextClassificationClient(applicationContext)
client.load()

В данный момент onClickListener для вашей кнопки должен выглядеть следующим образом:

btnSendText.setOnClickListener {
     var toSend:String = txtInput.text.toString()
     txtOutput.text = toSend
 }
  1. Обновите его так, чтобы он выглядел следующим образом:
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()
}

Это меняет функциональность: вместо простого вывода пользовательского ввода теперь осуществляется его предварительная классификация.

  1. Эта строка кода возьмет введенную пользователем строку и передаст ее модели, получив в ответ результат:
var results:List<Category> = client.classify(toSend)

Существует всего две категории: False и True

( TensorFlow сортирует их по алфавиту, поэтому False будет элементом 0, а True — элементом 1.)

  1. Чтобы получить оценку вероятности того, что значение True , вы можете посмотреть результаты[1].score следующим образом:
    val score = results[1].score
  1. Выбрано пороговое значение (в данном случае 0,8), согласно которому, если оценка для категории «Истина» превышает пороговое значение (0,8), то сообщение является спамом. В противном случае, это не спам, и сообщение можно безопасно отправлять.
    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. Посмотреть, как работает эта модель, можно здесь. Сообщение "Посетите мой блог, чтобы купить что-нибудь!" было помечено как спам с высокой вероятностью:

1fb0b5de9e566e.png

И наоборот, фраза "Привет, интересный урок, спасибо!" воспринималась как сообщение с очень низкой вероятностью спама:

73f38bdb488b29b3.png

7. Обновите ваше iOS-приложение, чтобы оно использовало модель TensorFlow Lite.

Код можно получить, следуя инструкциям из Codelab 1, или клонировав этот репозиторий и загрузив приложение из TextClassificationStep1 . Вы найдете его по пути TextClassificationOnMobile->iOS .

Готовый код также доступен вам в виде файла TextClassificationStep2 .

В практическом задании по созданию модели машинного обучения для борьбы со спамом в комментариях вы разработали очень простое приложение, которое позволяло пользователю вводить сообщение в UITextView , и оно передавалось на выход без какой-либо фильтрации.

Теперь обновите это приложение, чтобы оно использовало модель TensorFlow Lite для обнаружения спама в комментариях перед отправкой. Просто имитируйте отправку в этом приложении, отображая текст в выходной метке (но в реальном приложении может быть доска объявлений, чат или что-то подобное).

Для начала вам понадобится приложение из шага 1, которое вы можете клонировать из репозитория.

Для интеграции TensorFlow Lite вам потребуется использовать CocoaPods. Если они у вас еще не установлены, вы можете сделать это, следуя инструкциям на сайте https://cocoapods.org/ .

  1. После установки CocoaPods создайте файл с именем Podfile в той же директории, что и файл .xcproject для приложения TextClassification. Содержимое этого файла должно выглядеть примерно так:
target 'TextClassificationStep2' do
  use_frameworks!

  # Pods for NLPClassifier
    pod 'TensorFlowLiteSwift'

end

Название вашего приложения должно быть в первой строке, а не "TextClassificationStep2".

С помощью Терминала перейдите в указанную директорию и выполните pod install . Если установка пройдет успешно, у вас появится новая директория с именем Pods и новый файл ` .xcworkspace . В дальнейшем вы будете использовать его вместо файла .xcproject .

Если это не удалось, убедитесь, что файл Podfile находится в той же директории, где находился файл .xcproject . Обычно виной тому является неправильное расположение файла Podfile в директории или неправильное имя цели!

8. Добавьте файлы модели и словаря.

При создании модели с помощью TensorFlow Lite Model Maker вы могли получить в результате модель (в формате model.tflite ) и словарь (в формате vocab.txt ).

  1. Добавьте их в свой проект, перетащив из Finder в окно проекта. Убедитесь, что флажок «Добавить в целевые объекты» установлен.

1ee9eaa00ee79859.png

Когда вы закончите, вы должны увидеть их в своем проекте:

b63502b23911fd42.png

  1. Убедитесь, что они добавлены в пакет (чтобы их развернули на устройстве), выбрав свой проект (на скриншоте выше это синяя иконка TextClassificationStep2 ) и посмотрев вкладку «Этапы сборки» :

20b7cb603d49b457.png

9. Загрузите словарный запас.

При классификации в рамках НЛП модель обучается на словах, закодированных в векторы. Модель кодирует слова с помощью определенного набора имен и значений, которые изучаются в процессе обучения. Обратите внимание, что большинство моделей имеют разные словари, и важно использовать словарь, сгенерированный для вашей модели во время обучения. Это файл vocab.txt который вы только что добавили в свое приложение.

Вы можете открыть файл в Xcode, чтобы увидеть кодировки. Слова типа «song» закодированы в 6-значный код, а «love» — в 12-значный. Порядок фактически соответствует частотному порядку , поэтому «I» было самым распространенным словом в наборе данных, за ним следовало «check».

Когда пользователь вводит слова, вам необходимо закодировать их с помощью этого словаря, прежде чем отправлять их в модель для классификации.

Давайте разберем этот код. Начнем с загрузки словаря.

  1. Определите переменную уровня класса для хранения словаря:
var words_dictionary = [String : Int]()
  1. Затем создайте в классе func для загрузки словаря в этот словарь:
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. Вы можете запустить это, вызвав соответствующую функцию внутри viewDidLoad :
override func viewDidLoad() {
    super.viewDidLoad()
    txtInput.delegate = self
    loadVocab()
}

10. Преобразуйте строку в последовательность токенов.

Пользователи будут вводить слова в виде предложений, которые затем преобразуются в строку. Каждое слово в предложении, если оно присутствует в словаре, будет закодировано в ключевое значение этого слова, как оно определено в словаре.

Модель обработки естественного языка (NLP) обычно принимает последовательность фиксированной длины. Исключения составляют модели, построенные с использованием ragged tensors , но в большинстве случаев длина последовательности фиксирована. При создании модели вы указали эту длину. Убедитесь, что вы используете ту же длину в своем iOS-приложении.

Значение по умолчанию в используемом вами ранее инструменте Colab for TensorFlow Lite Model Maker было равно 20, поэтому установите его и здесь:

let SEQUENCE_LENGTH = 20

Добавьте эту func , которая будет принимать строку, преобразовывать её в нижний регистр и удалять все знаки препинания:

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
}

Обратите внимание, что последовательность будет представлять собой 32-битные целые числа. Это сделано намеренно, потому что при передаче значений в TensorFlow Lite вы будете иметь дело с низкоуровневой памятью, а TensorFlow Lite обрабатывает целые числа в строковой последовательности как 32-битные целые числа. Это немного упростит вам жизнь при передаче строк в модель.

11. Проведите классификацию.

Для классификации предложения его необходимо сначала преобразовать в последовательность токенов на основе слов, входящих в это предложение. Это будет сделано на шаге 9.

Теперь вы возьмете предложение и передадите его модели, модель выполнит логический вывод на основе предложения и проанализирует результаты.

Для этого будет использоваться интерпретатор TensorFlow Lite, который вам потребуется импортировать:

import TensorFlowLite

Начните с func , которая принимает вашу последовательность, представляющую собой массив типов 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
  }

Это загрузит файл модели из пакета и запустит интерпретатор, используя его.

Следующим шагом будет копирование данных из памяти, хранящихся в последовательности, в буфер с именем myData, чтобы его можно было передать в тензор. При реализации модуля TensorFlow Lite, а также интерпретатора, вы получаете доступ к типу Tensor.

Начните код следующим образом (все еще в func classify):

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

Не беспокойтесь, если при вызове copyingBufferOf возникнет ошибка. Это будет реализовано в виде расширения позже.

Теперь пришло время выделить тензоры в интерпретаторе, скопировать только что созданный буфер данных во входной тензор, а затем вызвать интерпретатор для выполнения вывода:

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

После завершения вызова вы можете просмотреть вывод интерпретатора, чтобы увидеть результаты.

Это будут необработанные значения (4 байта на нейрон), которые вам затем нужно будет считать и преобразовать. Поскольку эта конкретная модель имеет 2 выходных нейрона, вам потребуется считать 8 байтов, которые будут преобразованы в Float32 для анализа. Вы работаете с низкоуровневой памятью, отсюда и 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) ?? []

Теперь относительно легко проанализировать данные, чтобы определить качество спама. Модель имеет 2 выхода: первый — вероятность того, что сообщение не является спамом, второй — вероятность того, что оно является спамом. Таким образом, вы можете посмотреть results[1] , чтобы найти значение спама:

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

Для удобства, вот полный метод:

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. Добавьте расширения Swift.

Приведённый выше код использует расширение типа Data, позволяющее копировать исходные данные массива Int32 в Data . Вот код для этого расширения:

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

При работе с низкоуровневой памятью используются «небезопасные» данные, и приведенный выше код требует инициализации массива небезопасных данных. Это расширение делает это возможным:

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. Запустите приложение iOS

Запустите и протестируйте приложение.

Если всё прошло успешно, вы должны увидеть приложение на своём устройстве примерно в таком виде:

74cbd28d9b1592ed.png

В случае отправки сообщения "Купите мою книгу, чтобы научиться онлайн-торговле!" приложение с вероятностью 0,99% отправляет предупреждение об обнаружении спама!

14. Поздравляем!

Теперь вы создали очень простое приложение, которое фильтрует текст на предмет спама в комментариях, используя модель, обученную на данных, используемых для рассылки спама в блогах.

Следующий шаг в типичном жизненном цикле разработчика — это изучение того, что потребуется для настройки модели на основе данных, полученных в вашем собственном сообществе. Вы узнаете, как это сделать, в следующем задании.