1. 시작하기 전에
이 Codelab에서는 이전 모바일 텍스트 분류 시작하기 Codelab에서 빌드한 앱을 업데이트합니다.
기본 요건
- 이 Codelab은 머신러닝을 처음 접하는 숙련된 개발자를 대상으로 설계되었습니다.
- 이 Codelab은 순차적 학습 과정의 일부입니다. 기본 메시지 스타일 앱 빌드 또는 댓글 스팸 머신러닝 모델 빌드를 아직 완료하지 않았다면 지금 중지하고 완료하세요.
[빌드하거나 학습할] 내용
- 이전 단계에서 빌드한 앱에 맞춤 모델을 통합하는 방법을 알아봅니다.
필요한 항목
- Android 스튜디오 또는 iOS용 CocoaPods
2. 기존 Android 앱 열기
Codelab 1을 따르거나 이 저장소를 클론하고 TextClassificationStep1에서 앱을 로드하여 코드를 가져올 수 있습니다.
git clone https://github.com/googlecodelabs/odml-pathways
TextClassificationOnMobile->Android 경로에서 확인할 수 있습니다.
완성된 코드는 TextClassificationStep2로도 제공됩니다.
열리면 2단계로 이동할 수 있습니다.
3. 모델 파일 및 메타데이터 가져오기
댓글 스팸 머신러닝 모델 빌드 Codelab에서 .TFLITE 모델을 만들었습니다.
모델 파일을 다운로드해야 합니다. 이 모델이 없는 경우 이 Codelab의 저장소에서 가져올 수 있으며 모델은 여기에서 확인할 수 있습니다.
애셋 디렉터리를 만들어 프로젝트에 추가합니다.
- 프로젝트 탐색기를 사용하여 상단에 Android가 선택되어 있는지 확인합니다.
- app 폴더를 마우스 오른쪽 버튼으로 클릭합니다. New(새로 만들기) > Directory(디렉터리)를 선택합니다.

- 새 디렉터리 대화상자에서 src/main/assets를 선택합니다.

이제 앱에서 새 assets 폴더를 사용할 수 있습니다.

- assets를 마우스 오른쪽 버튼으로 클릭합니다.
- 메뉴가 열리면 Mac의 경우 Finder에서 표시가 표시됩니다. 선택합니다. (Windows에서는 탐색기에서 표시, Ubuntu에서는 파일에서 표시라고 표시됩니다.)

Finder가 실행되어 파일 위치를 표시합니다 (Windows의 경우 파일 탐색기, Linux의 경우 파일).
labels.txt,model.tflite,vocab파일을 이 디렉터리에 복사합니다.

- Android 스튜디오로 돌아가면 assets 폴더에서 사용할 수 있습니다.

4. TensorFlow Lite를 사용하도록 build.gradle 업데이트
TensorFlow Lite와 이를 지원하는 TensorFlow Lite 작업 라이브러리를 사용하려면 build.gradle 파일을 업데이트해야 합니다.
Android 프로젝트에는 여러 개가 있는 경우가 많으므로 앱 수준을 찾아야 합니다. Android 뷰의 프로젝트 탐색기에서 Gradle 스크립트 섹션에 있습니다. 올바른 앱에는 다음과 같이 .app 라벨이 지정됩니다.

이 파일에서 두 가지를 변경해야 합니다. 첫 번째는 하단의 종속 항목 섹션에 있습니다. 다음과 같이 TensorFlow Lite 작업 라이브러리의 텍스트 implementation를 추가합니다.
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코드가 있는 패키지 이름을 마우스 오른쪽 버튼으로 클릭합니다.- New > Package를 선택합니다.

- 화면 중앙에 패키지 이름을 입력하라는 대화상자가 표시됩니다. 현재 패키지 이름 끝에 추가합니다. 여기서는 도우미라고 합니다.

- 이 작업이 완료되면 프로젝트 탐색기에서 helpers 폴더를 마우스 오른쪽 버튼으로 클릭합니다.
- New > Java Class를 선택하고
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일 확률의 점수를 확인하려면 다음과 같이 results[1].score를 확인하면 됩니다.
val score = results[1].score
- True 카테고리의 점수가 임곗값 (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. TensorFlow Lite 모델을 사용하도록 iOS 앱 업데이트
Codelab 1을 따르거나 이 저장소를 클론하고 TextClassificationStep1에서 앱을 로드하여 코드를 가져올 수 있습니다. TextClassificationOnMobile->iOS 경로에서 확인할 수 있습니다.
완성된 코드는 TextClassificationStep2로도 제공됩니다.
댓글 스팸 머신러닝 모델 빌드 Codelab에서는 사용자가 UITextView에 메시지를 입력하고 필터링 없이 출력으로 전달할 수 있는 매우 간단한 앱을 만들었습니다.
이제 TensorFlow Lite 모델을 사용하여 전송 전에 텍스트에서 댓글 스팸을 감지하도록 앱을 업데이트합니다. 출력 라벨에 텍스트를 렌더링하여 이 앱에서 전송을 시뮬레이션합니다 (실제 앱에는 게시판, 채팅 등이 있을 수 있음).
시작하려면 1단계의 앱이 필요하며, 이 앱은 저장소에서 클론할 수 있습니다.
TensorFlow Lite를 통합하려면 CocoaPods를 사용합니다. 아직 설치하지 않았다면 https://cocoapods.org/의 안내에 따라 설치할 수 있습니다.
- CocoaPods를 설치한 후 TextClassification 앱의
.xcproject와 동일한 디렉터리에 Podfile이라는 이름의 파일을 만듭니다. 이 파일의 내용은 다음과 같아야 합니다.
target 'TextClassificationStep2' do
use_frameworks!
# Pods for NLPClassifier
pod 'TensorFlowLiteSwift'
end
앱 이름은 'TextClassificationStep2' 대신 첫 번째 줄에 있어야 합니다.
터미널을 사용하여 해당 디렉터리로 이동하고 pod install을 실행합니다. 성공하면 Pods라는 새 디렉터리와 새 .xcworkspace 파일이 생성됩니다. 앞으로 .xcproject 대신 이 값을 사용합니다.
실패한 경우 .xcproject이 있던 동일한 디렉터리에 Podfile이 있는지 확인하세요. 잘못된 디렉터리의 podfile 또는 잘못된 타겟 이름이 보통 주범입니다.
8. 모델 및 어휘 파일 추가
TensorFlow Lite Model Maker로 모델을 만들 때 모델 (model.tflite)과 어휘 (vocab.txt)를 출력할 수 있었습니다.
- Finder에서 프로젝트 창으로 드래그 앤 드롭하여 프로젝트에 추가합니다. 타겟에 추가가 선택되어 있는지 확인합니다.

완료되면 프로젝트에 표시됩니다.

- 프로젝트 (위 스크린샷에서는 파란색 아이콘 TextClassificationStep2)를 선택하고 빌드 단계 탭을 확인하여 번들에 추가되었는지 (기기에 배포되도록) 다시 한번 확인합니다.

9. 어휘 로드
NLP 분류를 수행할 때 모델은 벡터로 인코딩된 단어로 학습됩니다. 모델은 모델이 학습될 때 학습된 특정 이름과 값 집합으로 단어를 인코딩합니다. 대부분의 모델은 어휘가 다르므로 학습 시 생성된 모델의 어휘를 사용하는 것이 중요합니다. 방금 앱에 추가한 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 앱에서 동일한 길이를 사용해야 합니다.
앞서 사용한 TensorFlow Lite Model Maker의 Colab 기본값은 20이므로 여기에서도 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
}
시퀀스는 Int32입니다. TensorFlow Lite에 값을 전달할 때는 하위 수준 메모리를 처리하고 TensorFlow Lite는 문자열 시퀀스의 정수를 32비트 정수로 처리하므로 의도적으로 선택했습니다. 이렇게 하면 모델에 문자열을 전달할 때 조금 더 편리해집니다.
11. 분류 실행
문장을 분류하려면 먼저 문장의 단어를 기반으로 토큰 시퀀스로 변환해야 합니다. 9단계에서 이 작업을 완료했을 것입니다.
이제 문장을 가져와 모델에 전달하고, 모델이 문장에 대한 추론을 실행하도록 하고, 결과를 파싱합니다.
이렇게 하면 가져와야 하는 TensorFlow Lite 인터프리터가 사용됩니다.
import TensorFlowLite
Int32 유형의 배열인 시퀀스를 가져오는 func로 시작합니다.
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 포드와 인터프리터를 구현할 때 텐서 유형에 액세스할 수 있었습니다.
다음과 같이 코드를 시작합니다 (여전히 classify func에 있음).
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개 있으므로 파싱을 위해 Float32로 변환될 8바이트를 읽어와야 합니다. 하위 수준 메모리를 처리하므로 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) ?? []
이제 데이터를 파싱하여 스팸 품질을 비교적 쉽게 확인할 수 있습니다. 모델에는 두 개의 출력이 있습니다. 첫 번째는 메시지가 스팸이 아닐 확률이고 두 번째는 스팸일 확률입니다. 따라서 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 확장 프로그램 추가
위 코드는 데이터 유형의 확장 프로그램을 사용하여 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 앱 실행
앱을 실행하고 테스트합니다.
모두 잘 진행되면 기기에 앱이 다음과 같이 표시됩니다.

'온라인 거래를 배우려면 내 책을 구매하세요'라는 메시지가 전송된 경우 앱에서 스팸 감지 알림을 99%의 확률로 다시 전송합니다.
14. 축하합니다.
이제 블로그 스팸에 사용된 데이터로 학습된 모델을 사용하여 댓글 스팸을 텍스트에서 필터링하는 매우 간단한 앱을 만들었습니다.
일반적인 개발자 수명 주기의 다음 단계는 자체 커뮤니티에서 찾은 데이터를 기반으로 모델을 맞춤설정하는 데 필요한 사항을 살펴보는 것입니다. 다음 학습 과정 활동에서 이 작업을 수행하는 방법을 알아봅니다.