스팸 필터링 머신러닝 모델을 사용하도록 앱 업데이트

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 모델을 만들었습니다.

모델 파일을 다운로드했어야 합니다. TensorFlow가 없다면 이 Codelab의 저장소에서 가져올 수 있으며 모델은 여기에서 확인할 수 있습니다.

애셋 디렉터리를 만들어 프로젝트에 추가합니다.

  1. 프로젝트 탐색기를 사용하여 상단에서 Android가 선택되어 있는지 확인합니다.
  2. app 폴더를 마우스 오른쪽 버튼으로 클릭합니다. New > Directory를 선택합니다.

d7c3e9f21035fc15.png

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

2137f956a1ba4ef0.png

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

ae858835e1a90445.png

  1. 확장 소재를 마우스 오른쪽 버튼으로 클릭합니다.
  2. 메뉴가 열리면 Mac의 경우 Finder에서 표시가 표시됩니다. 선택합니다. Windows에서는 Show in Explorer(탐색기에서 표시, Ubuntu에서는 Show in Files)가 표시됩니다.

e61aaa3b73c5ab68.png

Finder가 실행되어 파일 위치를 표시합니다 (Windows의 경우 File Explorer, Linux의 경우 Files).

  1. labels.txt, model.tflite, vocab 파일을 이 디렉터리에 복사합니다.

14f382cc19552a56.png

  1. Android 스튜디오로 돌아가면 애셋 폴더에서 사용할 수 있는 애셋이 표시됩니다.

150ed2a1d2f7a10d.png

4. TensorFlow Lite를 사용하도록 build.gradle 업데이트

TensorFlow Lite 및 이를 지원하는 TensorFlow Lite 작업 라이브러리를 사용하려면 build.gradle 파일을 업데이트해야 합니다.

Android 프로젝트에는 수준 1이 하나 이상 있는 경우가 많으므로 수준 1을 찾아야 합니다. Android 뷰의 프로젝트 탐색기에서 Gradle 스크립트 섹션에서 찾습니다. 올바른 앱에는 아래와 같이 .app 라벨이 지정됩니다.

6426051e614bc42f.png

이 파일에서 두 가지를 변경해야 합니다. 첫 번째는 하단에 있는 종속 항목 섹션에 있습니다. 다음과 같이 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로 변경합니다.

c100b68450b8812f.png

이제 모든 종속 항목이 있으므로 코딩을 시작할 시간입니다.

5. 도우미 클래스 추가

앱에서 모델을 사용하는 추론 로직을 사용자 인터페이스에서 분리하려면 모델 추론을 처리하는 다른 클래스를 만듭니다. 이를 '도우미' 클래스라고 합니다.

  1. MainActivity 코드가 있는 패키지 이름을 마우스 오른쪽 버튼으로 클릭합니다.
  2. New > Package를 선택합니다.

d5911ded56b5df35.png

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

3b9f1f822f99b371.png

  1. 완료되면 프로젝트 탐색기에서 helpers 폴더를 마우스 오른쪽 버튼으로 클릭합니다.
  2. New > Java Class를 선택하고 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를 반환합니다. 머신러닝 모델을 사용하여 문자열이 스팸인지 여부를 판단할 콘텐츠를 분류하는 경우 일반적으로 할당된 확률과 함께 모든 답변이 반환됩니다. 예를 들어 스팸처럼 보이는 메시지를 전달하면 스팸일 확률이 있는 답변 1개와 스팸이 아닐 확률이 있는 답변 1개가 포함된 답변 목록이 반환됩니다. 스팸/스팸 아님은 카테고리이므로 반환된 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)

카테고리는 FalseTrue 두 가지뿐입니다.

. (TensorFlow는 알파벳순으로 정렬하므로 False는 항목 0이고 True는 항목 1입니다.)

  1. 값이 True일 확률의 점수를 가져오려면 다음과 같이 results[1].score를 확인하면 됩니다.
    val score = results[1].score
  1. 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()
    }
  1. 여기에서 모델이 작동하는 모습을 확인하세요. '내 블로그를 방문하여 물건을 구매하세요'라는 메일이 스팸일 가능성이 높다는 신고가 접수되었습니다.

1fb0b5de9e566e.png

반대로 '재미있는 튜토리얼입니다. 감사합니다!'는 스팸일 가능성이 매우 낮습니다.

73f38bdb488b29b3.png

7. TensorFlow Lite 모델을 사용하도록 iOS 앱 업데이트

Codelab 1을 따르거나 저장소를 클론하고 TextClassificationStep1에서 앱을 로드하여 이 코드를 가져올 수 있습니다. TextClassificationOnMobile->iOS 경로에서 확인할 수 있습니다.

완료된 코드는 TextClassificationStep2로도 사용할 수 있습니다.

댓글 스팸 머신러닝 모델 빌드 Codelab에서는 사용자가 UITextView에 메시지를 입력하고 필터링 없이 출력으로 전달할 수 있는 매우 간단한 앱을 만들었습니다.

이제 TensorFlow Lite 모델을 사용하여 전송하기 전에 텍스트에서 댓글 스팸을 감지하도록 앱을 업데이트합니다. 출력 라벨에 텍스트를 렌더링하여 이 앱에서 전송을 시뮬레이션합니다. 하지만 실제 앱에는 게시판, 채팅 등이 있을 수 있습니다.

시작하려면 1단계의 앱이 필요하며 앱은 저장소에서 클론할 수 있습니다.

TensorFlow Lite를 통합하려면 CocoaPods를 사용합니다. 아직 설치하지 않았다면 https://cocoapods.org/의 안내에 따라 설치할 수 있습니다.

  1. CocoaPods를 설치한 후 TextClassification 앱의 .xcproject와 동일한 디렉터리에 Podfile이라는 파일을 만듭니다. 이 파일의 콘텐츠는 다음과 같이 표시됩니다.
target 'TextClassificationStep2' do
  use_frameworks!

  # Pods for NLPClassifier
    pod 'TensorFlowLiteSwift'

end

앱 이름은 첫 번째 줄에 'TextClassificationStep2' 대신 표시되어야 합니다.

터미널을 사용하여 해당 디렉터리로 이동하고 pod install를 실행합니다. 성공하면 포드라는 새 디렉터리와 새 .xcworkspace 파일이 생성됩니다. 향후 .xcproject 대신 사용할 것입니다.

실패했다면 .xcproject이 있던 디렉터리와 동일한 디렉터리에 Podfile이 있는지 확인하세요. 잘못된 디렉터리에 있는 podfile 또는 잘못된 타겟 이름이 일반적으로 주된 원인입니다.

8. 모델 및 Vocab 파일 추가

TensorFlow Lite Model Maker로 모델을 만들 때 모델 (model.tflite)과 Vocab (vocab.txt)을 출력할 수 있었습니다.

  1. Finder에서 프로젝트 창으로 드래그 앤 드롭하여 프로젝트에 추가합니다. 대상에 추가가 선택되어 있는지 확인합니다.

1ee9eaa00ee79859.png

완료하면 프로젝트에 다음과 같이 표시됩니다.

b63502b23911fd42.png

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

20b7cb603d49b457.png

9. 어휘 로드

NLP 분류를 수행할 때 모델은 벡터로 인코딩된 단어로 학습됩니다. 모델은 모델이 학습하면서 학습된 특정 이름 및 값 집합으로 단어를 인코딩합니다. 대부분의 모델은 다른 어휘집을 가지며, 학습 시 생성된 모델의 어휘집을 사용하는 것이 중요합니다. 방금 앱에 추가한 vocab.txt 파일입니다.

Xcode에서 파일을 열어 인코딩을 확인할 수 있습니다. '노래'와 같은 단어는 6으로, '사랑'은 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 앱에서도 동일한 길이를 사용해야 합니다.

앞서 사용한 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 포드를 구현할 때 Tensor 유형에 액세스할 수 있었습니다.

다음과 같이 코드를 시작합니다 (여전히 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 앱 실행

앱을 실행하고 테스트합니다.

문제가 없다면 기기에 다음과 같이 앱이 표시됩니다.

74cbd28d9b1592ed.png

'도서를 사서 온라인 거래를 배우세요!'라는 메시지가 전송되면 앱은 0.99%의 확률로 스팸 감지 알림을 다시 보냅니다.

14. 축하합니다.

이제 블로그 스팸에 사용된 데이터로 학습된 모델을 사용하여 댓글 스팸 텍스트를 필터링하는 매우 간단한 앱을 만들었습니다.

일반적인 개발자 수명 주기의 다음 단계는 자체 커뮤니티에서 찾은 데이터를 기반으로 모델을 맞춤설정하는 데 필요한 사항을 살펴보는 것입니다. 다음 개발자 과정 활동에서 방법을 알아보세요.