1. Прежде чем начать
В этом практическом занятии вы обновите приложение, созданное вами в предыдущих практических занятиях по началу работы с мобильной классификацией текста.
Предварительные требования
- Данный практический курс предназначен для опытных разработчиков, только начинающих осваивать машинное обучение.
- Этот практический курс является частью последовательного обучения. Если вы еще не выполнили задания «Создание простого приложения для обмена сообщениями» или «Создание модели машинного обучения для борьбы со спамом в комментариях», пожалуйста, остановитесь и сделайте это прямо сейчас.
Что вы [построите или узнаете]
- Вы узнаете, как интегрировать созданную на предыдущих этапах пользовательскую модель в ваше приложение.
Что вам понадобится
- Android Studio или CocoaPods для iOS
2. Откройте существующее приложение Android.
Код можно получить, следуя инструкциям из Codelab 1, или клонировав этот репозиторий и загрузив приложение из TextClassificationStep1 .
git clone https://github.com/googlecodelabs/odml-pathways
Это можно найти по пути TextClassificationOnMobile->Android .
Готовый код также доступен вам в виде файла TextClassificationStep2 .
После того, как окно будет открыто, вы готовы перейти к шагу 2.
3. Импортируйте файл модели и метаданные.
В практическом задании «Создание модели машинного обучения для борьбы со спамом в комментариях» вы создали модель в формате .TFLITE.
Вы должны были скачать файл модели. Если у вас его нет, вы можете получить его из репозитория этого практического задания, модель доступна здесь .
Добавьте его в свой проект, создав каталог assets.
- В навигаторе проекта убедитесь, что вверху выбран пункт "Android" .
- Щелкните правой кнопкой мыши папку приложения . Выберите Создать > Каталог.

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

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

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

Запустится Finder , который покажет расположение файлов ( File Explorer в Windows, Files в Linux).
- Скопируйте файлы
labels.txt,model.tfliteиvocabв эту директорию.

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

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

Вам потребуется внести два изменения в этот файл. Первое — в раздел зависимостей внизу. Добавьте текстовую 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:

Теперь у вас есть все необходимые зависимости, так что пора начинать программировать!
5. Добавьте вспомогательный класс.
Чтобы отделить логику вывода модели , где ваше приложение использует модель, от пользовательского интерфейса, создайте отдельный класс для обработки вывода модели. Назовите его вспомогательным классом.
- Щелкните правой кнопкой мыши по имени пакета, в котором находится код
MainActivity. - Выберите Создать > Пакет .

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

- После этого щелкните правой кнопкой мыши папку helpers в обозревателе проектов.
- Выберите «Создать» > «Класс Java» и назовите его
TextClassificationClient. Вы отредактируете файл на следующем шаге.
Ваш вспомогательный класс TextClassificationClient будет выглядеть примерно так (хотя имя вашего пакета может отличаться).
package com.google.devrel.textclassificationstep1.helpers;
public class TextClassificationClient {
}
- Обновите файл, добавив следующий код:
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 сначала нужно импортировать только что созданные вспомогательные функции!
- В верхней части
MainActivity.kt, помимо остальных импортируемых элементов, добавьте:
import com.google.devrel.textclassificationstep2.helpers.TextClassificationClient
import org.tensorflow.lite.support.label.Category
- Далее вам потребуется загрузить вспомогательные классы. В
onCreate, сразу после строкиsetContentView, добавьте следующие строки для создания экземпляра и загрузки вспомогательного класса:
val client = TextClassificationClient(applicationContext)
client.load()
В данный момент onClickListener для вашей кнопки должен выглядеть следующим образом:
btnSendText.setOnClickListener {
var toSend:String = txtInput.text.toString()
txtOutput.text = toSend
}
- Обновите его так, чтобы он выглядел следующим образом:
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()
}
Это меняет функциональность: вместо простого вывода пользовательского ввода теперь осуществляется его предварительная классификация.
- Эта строка кода возьмет введенную пользователем строку и передаст ее модели, получив в ответ результат:
var results:List<Category> = client.classify(toSend)
Существует всего две категории: False и True
( TensorFlow сортирует их по алфавиту, поэтому False будет элементом 0, а True — элементом 1.)
- Чтобы получить оценку вероятности того, что значение
True, вы можете посмотреть результаты[1].score следующим образом:
val score = results[1].score
- Выбрано пороговое значение (в данном случае 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()
}
- Посмотреть, как работает эта модель, можно здесь. Сообщение "Посетите мой блог, чтобы купить что-нибудь!" было помечено как спам с высокой вероятностью:

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

7. Обновите ваше iOS-приложение, чтобы оно использовало модель TensorFlow Lite.
Код можно получить, следуя инструкциям из Codelab 1, или клонировав этот репозиторий и загрузив приложение из TextClassificationStep1 . Вы найдете его по пути TextClassificationOnMobile->iOS .
Готовый код также доступен вам в виде файла TextClassificationStep2 .
В практическом задании по созданию модели машинного обучения для борьбы со спамом в комментариях вы разработали очень простое приложение, которое позволяло пользователю вводить сообщение в UITextView , и оно передавалось на выход без какой-либо фильтрации.
Теперь обновите это приложение, чтобы оно использовало модель TensorFlow Lite для обнаружения спама в комментариях перед отправкой. Просто имитируйте отправку в этом приложении, отображая текст в выходной метке (но в реальном приложении может быть доска объявлений, чат или что-то подобное).
Для начала вам понадобится приложение из шага 1, которое вы можете клонировать из репозитория.
Для интеграции TensorFlow Lite вам потребуется использовать CocoaPods. Если они у вас еще не установлены, вы можете сделать это, следуя инструкциям на сайте https://cocoapods.org/ .
- После установки 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 ).
- Добавьте их в свой проект, перетащив из Finder в окно проекта. Убедитесь, что флажок «Добавить в целевые объекты» установлен.

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

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

9. Загрузите словарный запас.
При классификации в рамках НЛП модель обучается на словах, закодированных в векторы. Модель кодирует слова с помощью определенного набора имен и значений, которые изучаются в процессе обучения. Обратите внимание, что большинство моделей имеют разные словари, и важно использовать словарь, сгенерированный для вашей модели во время обучения. Это файл vocab.txt который вы только что добавили в свое приложение.
Вы можете открыть файл в Xcode, чтобы увидеть кодировки. Слова типа «song» закодированы в 6-значный код, а «love» — в 12-значный. Порядок фактически соответствует частотному порядку , поэтому «I» было самым распространенным словом в наборе данных, за ним следовало «check».
Когда пользователь вводит слова, вам необходимо закодировать их с помощью этого словаря, прежде чем отправлять их в модель для классификации.
Давайте разберем этот код. Начнем с загрузки словаря.
- Определите переменную уровня класса для хранения словаря:
var words_dictionary = [String : Int]()
- Затем создайте в классе
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")
}
}
- Вы можете запустить это, вызвав соответствующую функцию внутри
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
Запустите и протестируйте приложение.
Если всё прошло успешно, вы должны увидеть приложение на своём устройстве примерно в таком виде:

В случае отправки сообщения "Купите мою книгу, чтобы научиться онлайн-торговле!" приложение с вероятностью 0,99% отправляет предупреждение об обнаружении спама!
14. Поздравляем!
Теперь вы создали очень простое приложение, которое фильтрует текст на предмет спама в комментариях, используя модель, обученную на данных, используемых для рассылки спама в блогах.
Следующий шаг в типичном жизненном цикле разработчика — это изучение того, что потребуется для настройки модели на основе данных, полученных в вашем собственном сообществе. Вы узнаете, как это сделать, в следующем задании.