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

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의 저장소에서 가져올 수 있습니다. 모델은 여기에서 확인할 수 있습니다.

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

  1. 프로젝트 탐색기를 사용하여 맨 위에 Android가 선택되어 있는지 확인합니다.
  2. app 폴더를 마우스 오른쪽 버튼으로 클릭합니다. 새로 만들기 > 디렉터리.

d7c3e9f21035fc15.png

  1. New Directory 대화상자에서 src/main/assets를 선택합니다.

2137f956a1ba4ef0.png

이제 앱에서 사용할 수 있는 새 assets 폴더가 표시됩니다.

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 스튜디오로 돌아가면 assets 폴더에서 사용할 수 있는 항목을 확인할 수 있습니다.

150ed2a1d2f7a10d.png

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

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

Android 프로젝트에는 앱이 두 개 이상 있는 경우가 많으므로 수준 하나를 찾아야 합니다. Android 뷰의 프로젝트 탐색기에서 Gradle Scripts(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. 새로 만들기 > 패키지에 포함되어 있습니다.

d5911ded56b5df35.png

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

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)

두 가지 카테고리, FalseTrue만 있습니다.

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

  1. 값이 True일 확률의 점수를 구하려면 다음과 같이 results[1].score를 확인합니다.
    val score = results[1].score
  1. 임계값 (이 경우 0.8)을 선택했습니다. 즉, True 카테고리의 점수가 기준 값 (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' 대신에 입력해야 합니다.

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

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

8. 모델 및 용어 파일 추가

TensorFlow Lite Model maker를 사용하여 모델을 만들면 모델 (model.tflite)과 어휘 (vocab.txt)를 출력할 수 있었습니다.

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

1ee9eaa00ee79859.png

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

b63502b23911fd42.png

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

20b7cb603d49b457.png

9. 어휘 로드

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

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

이전에 사용한 TensorFlow Lite Model Maker용 Colab의 기본값은 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 포드와 인터프리터를 구현하면 텐서 유형에 액세스할 수 있습니다.

다음과 같이 코드를 시작합니다 (여전히 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) ?? []

이제 데이터를 파싱하여 스팸 품질을 결정하는 것이 비교적 쉽습니다. 모델에는 출력이 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 확장 프로그램 추가

위의 코드에서는 데이터 유형 확장 프로그램을 사용하여 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. 축하합니다.

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

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