1. Zanim zaczniesz
W tym ćwiczeniu z programowania zaktualizujesz aplikację utworzoną w poprzednich ćwiczeniach z programowania z serii „Rozpocznij korzystanie z mobilnej klasyfikacji tekstu”.
Wymagania wstępne
- Te warsztaty zostały opracowane z myślą o doświadczonych programistach, którzy dopiero zaczynają przygodę z uczeniem maszynowym.
- Codelab jest częścią ścieżki szkoleniowej. Jeśli nie masz jeszcze za sobą samouczków Tworzenie podstawowej aplikacji do przesyłania wiadomości i Tworzenie modelu systemu uczącego się do wykrywania spamu w komentarzach, przerwij i zrób to teraz.
Co [utworzysz lub czego się nauczysz]
- Dowiesz się, jak zintegrować model niestandardowy z aplikacją utworzoną w poprzednich krokach.
Czego potrzebujesz
- Android Studio lub CocoaPods w przypadku iOS.
2. Otwieranie istniejącej aplikacji na Androida
Kod możesz uzyskać, wykonując ćwiczenie 1, lub sklonować to repozytorium i wczytać aplikację z TextClassificationStep1.
git clone https://github.com/googlecodelabs/odml-pathways
Znajdziesz go na ścieżce TextClassificationOnMobile->Android.
Kod finished jest też dostępny jako TextClassificationStep2.
Gdy się otworzy, możesz przejść do kroku 2.
3. Importowanie pliku modelu i metadanych
W module Build a comment spam machine learning model (Tworzenie modelu systemu uczącego się do wykrywania spamu w komentarzach) utworzyliśmy model .TFLITE.
Plik modelu powinien zostać pobrany. Jeśli go nie masz, możesz go pobrać z repozytorium tego laboratorium. Model jest dostępny tutaj.
Dodaj go do projektu, tworząc katalog zasobów.
- W nawigatorze projektu sprawdź, czy u góry jest wybrana opcja Android.
- Kliknij prawym przyciskiem myszy folder app. Kliknij Nowy > Katalog.

- W oknie New Directory (Nowy katalog) wybierz src/main/assets.

W aplikacji pojawi się nowy folder assets.

- Kliknij prawym przyciskiem myszy zasoby.
- W menu, które się otworzy, zobaczysz (na komputerze Mac) Pokaż w Finderze. Wybierz ją. (W systemie Windows będzie to Pokaż w Eksploratorze, a w Ubuntu – Pokaż w plikach).

Uruchomi się Finder, który wyświetli lokalizację plików (Eksplorator plików w systemie Windows, Pliki w systemie Linux).
- Skopiuj do tego katalogu pliki
labels.txt,model.tfliteivocab.

- Wróć do Android Studio. Zobaczysz, że są dostępne w folderze assets.

4. Aktualizacja pliku build.gradle w celu używania TensorFlow Lite
Aby korzystać z TensorFlow Lite i bibliotek zadań TensorFlow Lite, które go obsługują, musisz zaktualizować plik build.gradle.
Projekty na Androida często mają więcej niż jeden plik build.gradle, więc znajdź ten na poziomie aplikacji. W eksploratorze projektu w widoku Androida znajdź go w sekcji Skrypty Gradle. Prawidłowa wersja będzie oznaczona etykietą .app, jak pokazano poniżej:

W tym pliku musisz wprowadzić 2 zmiany. Pierwsza znajduje się na dole w sekcji dependencies. Dodaj tekst implementation do biblioteki zadań TensorFlow Lite, np. tak:
implementation 'org.tensorflow:tensorflow-lite-task-text:0.1.0'
Numer wersji mógł się zmienić od czasu napisania tego artykułu, więc sprawdź najnowszą wersję na stronie https://www.tensorflow.org/lite/inference_with_metadata/task_library/nl_classifier.
Biblioteki zadań wymagają też minimalnej wersji pakietu SDK 21. To ustawienie znajdziesz w sekcji android > default config. Zmień je na 21:

Masz już wszystkie zależności, więc możesz zacząć pisać kod.
5. Dodawanie klasy pomocniczej
Aby oddzielić logikę wnioskowania, w której aplikacja korzysta z modelu, od interfejsu użytkownika, utwórz inną klasę do obsługi wnioskowania modelu. Nazwij ją klasą „pomocniczą”.
- Kliknij prawym przyciskiem myszy nazwę pakietu, w którym znajduje się kod
MainActivity. - Kliknij Nowy > Pakiet.

- Na środku ekranu pojawi się okno z prośbą o wpisanie nazwy pakietu. Dodaj ją na końcu bieżącej nazwy pakietu. (W tym przypadku są to pomocnicy).

- Gdy to zrobisz, kliknij prawym przyciskiem myszy folder helpers w eksploratorze projektu.
- Wybierz New > Java Class i nadaj mu nazwę
TextClassificationClient. W następnym kroku zmodyfikujesz ten plik.
Twoja TextClassificationClientklasa pomocnicza będzie wyglądać tak (chociaż nazwa pakietu może być inna):
package com.google.devrel.textclassificationstep1.helpers;
public class TextClassificationClient {
}
- Zaktualizuj plik za pomocą tego kodu:
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;
}
}
Ta klasa będzie zawierać otokę interpretera TensorFlow Lite, która wczytuje model i ukrywa złożoność zarządzania wymianą danych między aplikacją a modelem.
W metodzie load() utworzy nową instancję typu NLClassifier na podstawie ścieżki modelu. Ścieżka modelu to po prostu nazwa modelu, model.tflite. Typ NLClassifier jest częścią bibliotek zadań tekstowych. Pomaga on w przekształcaniu ciągu znaków w tokeny, używaniu prawidłowej długości sekwencji, przekazywaniu jej do modelu i parsowaniu wyników.
(Więcej informacji na ten temat znajdziesz w artykule Tworzenie modelu systemu uczącego się do wykrywania spamu w komentarzach).
Klasyfikacja jest przeprowadzana w metodzie classify, do której przekazujesz ciąg znaków, a ona zwraca obiekt List. Gdy używasz modeli uczenia maszynowego do klasyfikowania treści, w przypadku których chcesz określić, czy ciąg znaków jest spamem, czy nie, zwykle zwracane są wszystkie odpowiedzi z przypisanymi prawdopodobieństwami. Jeśli na przykład przekażesz mu wiadomość, która wygląda jak spam, otrzymasz listę 2 odpowiedzi: jedną z prawdopodobieństwem, że jest to spam, a drugą z prawdopodobieństwem, że nie jest to spam. Spam/Nie spam to kategorie, więc zwrócona wartość List będzie zawierać te prawdopodobieństwa. Zrobisz to później.
Teraz, gdy masz już klasę pomocniczą, wróć do MainActivity i zaktualizuj ją, aby używać jej do klasyfikowania tekstu. Zobaczysz to w następnym kroku.
6. Klasyfikowanie tekstu
W MainActivity najpierw zaimportuj utworzone przed chwilą funkcje pomocnicze.
- U góry pliku
MainActivity.ktdodaj te instrukcje importu:
import com.google.devrel.textclassificationstep2.helpers.TextClassificationClient
import org.tensorflow.lite.support.label.Category
- Następnie wczytaj pomocników. W
onCreate, bezpośrednio po wierszusetContentView, dodaj te wiersze, aby utworzyć instancję klasy pomocniczej i ją wczytać:
val client = TextClassificationClient(applicationContext)
client.load()
Obecnie przycisk onClickListener powinien wyglądać tak:
btnSendText.setOnClickListener {
var toSend:String = txtInput.text.toString()
txtOutput.text = toSend
}
- Zmień go tak, aby wyglądał tak:
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()
}
Zmienia to funkcję z zwracania danych wejściowych użytkownika na ich klasyfikowanie.
- Ten wiersz kodu pobiera ciąg znaków wpisany przez użytkownika i przekazuje go do modelu, który zwraca wyniki:
var results:List<Category> = client.classify(toSend)
Istnieją tylko 2 kategorie: False i True.
. (TensorFlow sortuje je alfabetycznie, więc False będzie elementem 0, a True – elementem 1).
- Aby uzyskać wynik prawdopodobieństwa, że wartość to
True, możesz sprawdzić wartość results[1].score w ten sposób:
val score = results[1].score
- Wybrana wartość progowa (w tym przypadku 0,8), która oznacza, że jeśli wynik dla kategorii „Prawda” jest powyżej wartości progowej (0,8), wiadomość jest spamem. W przeciwnym razie nie jest to spam i wiadomość można bezpiecznie wysł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()
}
- Zobacz model w akcji Wiadomość „Odwiedź mojego bloga, aby kupić produkty!” została oznaczona jako wysoce prawdopodobny spam:

Z kolei komentarz „Hej, fajny samouczek, dzięki!” miał bardzo niskie prawdopodobieństwo bycia spamem:

7. Aktualizowanie aplikacji na iOS, aby korzystać z modelu TensorFlow Lite
Kod możesz uzyskać, wykonując ćwiczenie 1, lub sklonować to repozytorium i wczytać aplikację z TextClassificationStep1. Znajdziesz go na ścieżce TextClassificationOnMobile->iOS.
Kod finished jest też dostępny jako TextClassificationStep2.
W samouczku Tworzenie modelu uczenia maszynowego do wykrywania spamu w komentarzach utworzyliśmy bardzo prostą aplikację, która umożliwiała użytkownikowi wpisanie wiadomości w UITextView i przekazanie jej do wyjścia bez filtrowania.
Teraz zaktualizujesz tę aplikację, aby używała modelu TensorFlow Lite do wykrywania spamu w komentarzach w tekście przed jego wysłaniem. W tej aplikacji wystarczy symulować wysyłanie, renderując tekst w etykiecie wyjściowej (ale prawdziwa aplikacja może mieć tablicę ogłoszeń, czat lub coś podobnego).
Na początek potrzebujesz aplikacji z kroku 1, którą możesz sklonować z repozytorium.
Aby zintegrować TensorFlow Lite, użyjesz CocoaPods. Jeśli nie masz jeszcze tych narzędzi, możesz je zainstalować, postępując zgodnie z instrukcjami na stronie https://cocoapods.org/.
- Po zainstalowaniu CocoaPods utwórz plik o nazwie Podfile w tym samym katalogu co plik
.xcprojectaplikacji TextClassification. Zawartość tego pliku powinna wyglądać tak:
target 'TextClassificationStep2' do
use_frameworks!
# Pods for NLPClassifier
pod 'TensorFlowLiteSwift'
end
W pierwszym wierszu powinna znajdować się nazwa aplikacji, a nie „TextClassificationStep2”.
Za pomocą terminala przejdź do tego katalogu i uruchom polecenie pod install. Jeśli się to uda, utworzony zostanie nowy katalog o nazwie Pods i nowy plik .xcworkspace. W przyszłości będziesz używać tego symbolu zamiast .xcproject.
Jeśli się nie udało, sprawdź, czy plik Podfile znajduje się w tym samym katalogu co .xcproject. Zwykle główną przyczyną jest plik Podfile w nieprawidłowym katalogu lub nieprawidłowa nazwa projektu.
8. Dodawanie plików modelu i słownika
Gdy tworzysz model za pomocą narzędzia TensorFlow Lite Model Maker, możesz wyeksportować model (jako model.tflite) i słownik (jako vocab.txt).
- Dodaj je do projektu, przeciągając je z Findera do okna projektu. Sprawdź, czy jest zaznaczone pole Dodaj do miejsc docelowych:

Gdy skończysz, powinny być widoczne w projekcie:

- Sprawdź, czy zostały dodane do pakietu (aby można było je wdrożyć na urządzeniu). W tym celu wybierz projekt (na powyższym zrzucie ekranu jest to niebieska ikona TextClassificationStep2) i otwórz kartę Fazy kompilacji:

9. Wczytywanie słownictwa
W przypadku klasyfikacji NLP model jest trenowany przy użyciu słów zakodowanych w wektorach. Model koduje słowa za pomocą określonego zestawu nazw i wartości, które są wyznaczane podczas trenowania modelu. Pamiętaj, że większość modeli ma różne słowniki. Ważne jest, aby używać słownika modelu, który został wygenerowany w momencie trenowania. To plik vocab.txt, który został właśnie dodany do aplikacji.
Aby zobaczyć kodowanie, możesz otworzyć plik w Xcode. Słowo „song” jest kodowane jako 6, a „love” jako 12. Kolejność jest w rzeczywistości kolejnością częstotliwości, więc „I” było najczęstszym słowem w zbiorze danych, a za nim było „check”.
Gdy użytkownik wpisze słowa, przed wysłaniem ich do modelu w celu klasyfikacji musisz je zakodować za pomocą tego słownika.
Przyjrzyjmy się temu kodowi. Zacznij od wczytania słownictwa.
- Zdefiniuj zmienną na poziomie klasy, aby przechowywać słownik:
var words_dictionary = [String : Int]()
- Następnie utwórz
funcw klasie, aby załadować słownictwo do tego słownika:
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")
}
}
- Możesz uruchomić tę funkcję, wywołując ją z poziomu
viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
txtInput.delegate = self
loadVocab()
}
10. Przekształcanie ciągu w sekwencję tokenów
Użytkownicy będą wpisywać słowa w formie zdania, które stanie się ciągiem znaków. Każde słowo w zdaniu, jeśli występuje w słowniku, zostanie zakodowane w wartości klucza dla tego słowa zgodnie z definicją w słowniku.
Model NLP zwykle akceptuje sekwencje o stałej długości. Istnieją wyjątki w przypadku modeli utworzonych za pomocą ragged tensors, ale w większości przypadków jest on stały. Długość została określona podczas tworzenia modelu. Upewnij się, że w aplikacji na iOS używasz tej samej długości.
W Colab dla TensorFlow Lite Model Maker, którego używasz, domyślna wartość to 20, więc ustaw ją też tutaj:
let SEQUENCE_LENGTH = 20
Dodaj ten kod func, który pobierze ciąg tekstowy, przekonwertuje go na małe litery i usunie z niego znaki interpunkcyjne:
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
}
Pamiętaj, że sekwencja będzie zawierać wartości Int32. Zostało to celowo wybrane, ponieważ w przypadku przekazywania wartości do TensorFlow Lite będziesz mieć do czynienia z pamięcią niskiego poziomu, a TensorFlow Lite traktuje liczby całkowite w ciągu znaków jako 32-bitowe liczby całkowite. Ułatwi Ci to (nieco) przekazywanie ciągów znaków do modelu.
11. Przeprowadź klasyfikację
Aby sklasyfikować zdanie, musisz najpierw przekonwertować je na sekwencję tokenów na podstawie słów w zdaniu. Zostało to zrobione w kroku 9.
Teraz weź zdanie i przekaż je do modelu, aby przeprowadzić wnioskowanie i przeanalizować wyniki.
W tym celu użyjemy interpretera TensorFlow Lite, który musisz zaimportować:
import TensorFlowLite
Zacznij od func, która przyjmuje sekwencję, czyli tablicę typów 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
}
Spowoduje to wczytanie pliku modelu z pakietu i wywołanie interpretera.
Następnym krokiem będzie skopiowanie pamięci bazowej przechowywanej w sekwencji do bufora o nazwie myData,, aby można było przekazać ją do tensora. Podczas wdrażania poda TensorFlow Lite, a także interpretera, uzyskujesz dostęp do typu tensora.
Zacznij kod w ten sposób (nadal w funkcji classify func):
let tSequence = Array(sequence)
let myData = Data(copyingBufferOf: tSequence.map { Int32($0) })
let outputTensor: Tensor
Nie martw się, jeśli na stronie copyingBufferOf pojawi się błąd. Wdrożymy to później jako rozszerzenie.
Teraz możesz przydzielić tensory w interpreterze, skopiować utworzony właśnie bufor danych do tensora wejściowego, a następnie wywołać interpreter, aby przeprowadzić wnioskowanie:
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()
Po zakończeniu wywołania możesz sprawdzić dane wyjściowe interpretera, aby zobaczyć wyniki.
Będą to wartości surowe (4 bajty na neuron), które musisz odczytać i przekonwertować. Ten model ma 2 neurony wyjściowe, więc musisz odczytać 8 bajtów, które zostaną przekonwertowane na liczby zmiennoprzecinkowe 32-bitowe na potrzeby analizy. Masz do czynienia z pamięcią niskiego poziomu, stąd 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) ?? []
Teraz stosunkowo łatwo jest przeanalizować dane, aby określić jakość spamu. Model ma 2 wartości wyjściowe: pierwsza to prawdopodobieństwo, że wiadomość nie jest spamem, a druga to prawdopodobieństwo, że jest. Wartość spamu możesz znaleźć w 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
Dla ułatwienia podajemy pełną metodę:
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. Dodawanie rozszerzeń Swift
Powyższy kod używa rozszerzenia typu danych, aby umożliwić kopiowanie surowych bitów tablicy Int32 do Data. Oto kod tego rozszerzenia:
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)
}
}
W przypadku pamięci niskiego poziomu używasz „niebezpiecznych” danych, a powyższy kod wymaga zainicjowania tablicy niebezpiecznych danych. To rozszerzenie umożliwia:
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. Uruchamianie aplikacji na iOS
Uruchom i przetestuj aplikację.
Jeśli wszystko przebiegło pomyślnie, aplikacja powinna wyglądać na urządzeniu tak:

Gdy wiadomość „Kup moją książkę, aby nauczyć się handlu online!” zostanie wysłana, aplikacja zwraca alert o wykryciu spamu z prawdopodobieństwem 0,99.
14. Gratulacje!
Utworzyliśmy bardzo prostą aplikację, która filtruje tekst pod kątem spamu w komentarzach za pomocą modelu wytrenowanego na danych używanych do spamowania blogów.
Kolejnym krokiem w typowym cyklu życia dewelopera jest sprawdzenie, jak dostosować model na podstawie danych znalezionych we własnej społeczności. W następnym ćwiczeniu dowiesz się, jak to zrobić.