スパムフィルタの機械学習モデルを使用するようにアプリを更新する

1. 始める前に

この Codelab では、前のステップ「モバイル テキスト分類のスタートガイド」で作成したアプリを更新します。

前提条件

  • この Codelab は、機械学習に不慣れなデベロッパーを対象にしています。
  • Codelab は、シーケンスされたパスウェイの一部です。基本的なメッセージ スタイルのアプリの作成、またはコメントスパムの機械学習モデルの構築をまだ完了していない場合は、今すぐ中止してください。

概要 [構築または学習]

  • 前の手順で構築したカスタムモデルをアプリに統合する方法について学びます。

必要なもの

  • Android Studio(iOS の場合は CocoaPods

2. 既存の Android アプリを開く

コードを取得するには、Codelab 1 に沿って操作するか、このリポジトリのクローンを作成し、TextClassificationStep1 からアプリを読み込みます。

git clone https://github.com/googlecodelabs/odml-pathways

これは TextClassificationOnMobile->Android パスにあります。

完成したコードは TextClassificationStep2 としても利用できます。

開封したら、手順 2 に進むことができます。

3. モデルファイルとメタデータをインポートする

コメントスパム機械学習モデルの構築では、.TFLITE モデルを作成しました。

モデルファイルがダウンロードされているはずです。まだお持ちでない場合は、この Codelab のリポジトリから入手できます。モデルはこちらから入手できます。

アセット ディレクトリを作成してプロジェクトに追加します。

  1. プロジェクト ナビゲータを使用して、上部で [Android] が選択されていることを確認します。
  2. app フォルダを右クリックします。[新規] > [ディレクトリ] を選択します。

d7c3e9f21035fc15.png

  1. [New Directory] ダイアログで [src/main/assets] を選択します。

2137f956a1ba4ef0.png

新しい assets フォルダがアプリ内に表示されます。

ae858835e1a90445.png

  1. [assets] を右クリックします。
  2. 開いたメニューに(Mac の場合は)Finder に表示されます。選択してください。(Windows では [エクスプローラに表示]、Ubuntu では [ファイルに表示] と表示されます)。

e61aaa3b73c5ab68.png

Finder が起動し、ファイルの場所が表示されます(Windows ではファイル エクスプローラー、Linux ではファイル)。

  1. labels.txtmodel.tflitevocab ファイルをこのディレクトリにコピーします。

14f382cc19552a56.png

  1. Android Studio に戻ると、assets フォルダで確認できます。

150ed2a1d2f7a10d.png

4.TensorFlow Lite を使用するように build.gradle を更新する

TensorFlow Lite と、それをサポートする TensorFlow Lite タスク ライブラリを使用するには、build.gradle ファイルを更新する必要があります。

Android プロジェクトは複数存在することが多いため、アプリレベル 1 は必ずご確認ください。[Android] ビューのプロジェクト エクスプローラで、[Gradle Scripts] セクションに表示されます。以下のように、正しい方には .app というラベルが付けられます。

6426051e614bc42f.png

このファイルには 2 つの変更を加える必要があります。1 つ目は、下部の [dependencies] セクションにあります。次のように、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 が返されます。機械学習モデルを使用して、コンテンツをスパムかどうかを判別するコンテンツを分類する場合、すべての回答と高い確率で回答が返されるのが一般的です。たとえば、迷惑メールと思われるメッセージを渡すと、2 つの回答リストが返されます。 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)

2 つのカテゴリ(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

反対に、「ありがとう、チュートリアル!」と言います。スパムである可能性は非常に低いと判断されました。

73 F 38bdb488b29b3.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

アプリの名前は 1 行目に、「TextClassificationStep2」ではなく「1 行目」にしてください。

Terminal を使用して、そのディレクトリに移動して pod install を実行します。成功すると、Pod という新しいディレクトリと、新しい .xcworkspace ファイルが作成されます。これは、今後 .xcproject の代わりに使用します。

失敗した場合は、.xcproject と同じディレクトリに Podfile があることを確認してください。通常は、ディレクトリ内の Podfile が正しくないか、ターゲット名が正しくないことが主な原因です。

8. モデルファイルと Vocab ファイルを追加する

TensorFlow Lite Model Maker を使用してモデルを作成した場合、モデル(model.tflite)と語彙(vocab.txt)を出力できました。

  1. Finder からプロジェクト ウィンドウにドラッグ&ドロップして、プロジェクトに追加できます。[ターゲットに追加] チェックボックスがオンになっていることを確認します。

1ee9eaa00ee79859.png

完了すると、プロジェクトに次のように表示されます。

b63502b23911fd42.png

  1. プロジェクトを選択して(上記のスクリーンショットでは、青いアイコン TextClassificationStep2)、[Build Phases] タブ:

20b7cb603d49b457.png

9. Vocab を読み込む

NLP 分類を行う場合、モデルは、ベクトルにエンコードされた単語でトレーニングされます。モデルは、トレーニングされたときに学習される特定の名前と値のセットを使用して、単語をエンコードします。モデルによって語彙は異なります。そのため、トレーニング時に生成されたモデルの語彙を使用することが重要です。これは、先ほどアプリに追加した vocab.txt ファイルです。

Xcode でファイルを開いて、エンコードを確認できます。「song」などの単語は 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 のデフォルトは 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

シーケンスを受け取る 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 Pod とインタープリタを実装する際、Tensor Type にアクセスできました。

次のようにコードを開始します(まだ 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 つの出力があります。1 つ目はメッセージが迷惑メールでない可能性を示し、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. 完了

これで、スパムスパムに使用されるデータでトレーニングされたモデルを使用して、コメントスパムのテキストをフィルタリングする、非常にシンプルなアプリを作成できました。

一般的なデベロッパー ライフサイクルの次のステップは、独自のコミュニティで見つかったデータに基づいて、モデルをカスタマイズするために必要な情報を探ることです。その方法については、次のパスウェイ アクティビティで説明します。