迷惑メールフィルタの機械学習モデルを使用するようにアプリを更新してください

1. 始める前に

この Codelab では、前の「モバイル テキスト分類のスタートガイド」の 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. モデルファイルとメタデータをインポートする

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

モデルファイルをダウンロードしているはずです。ない場合は、この Codelab の リポジトリから取得できます。モデルはこちらで入手できます。

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

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

d7c3e9f21035fc15.png

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

2137f956a1ba4ef0.png

アプリで新しい assets フォルダが使用可能になっていることがわかります。

ae858835e1a90445.png

  1. アセットを右クリックします。
  2. 表示されたメニューで、[Finder で表示](Mac の場合)を選択します。選択します。(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 プロジェクトには複数のものがあることが多いため、アプリレベルのものを探してください。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 型はテキスト タスク ライブラリの一部です。この型は、文字列をトークンに変換し、正しいシーケンス長を使用してモデルに渡し、結果を解析するのに役立ちます。

(これらについて詳しくは、コメントスパムの ML モデルを構築するをご覧ください)。

分類は 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)

カテゴリは FalseTrue の 2 つのみです。

.(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. モデルの動作については、こちらをご覧ください。「Visit my blog to buy stuff!」というメッセージは、スパムの可能性が高いと報告されました。

1fb0b5de9e566e.png

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

73f38bdb488b29b3.png

7. TensorFlow Lite モデルを使用するように iOS アプリを更新する

このコードを取得するには、Codelab 1 に沿って操作するか、こちらのリポジトリのクローンを作成して、TextClassificationStep1 からアプリを読み込みます。これは TextClassificationOnMobile->iOS パスにあります。

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

コメントスパムの ML モデルを構築する 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」ではなく、1 行目に記載する必要があります。

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

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

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

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

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

1ee9eaa00ee79859.png

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

b63502b23911fd42.png

  1. プロジェクト(上のスクリーンショットでは青いアイコン TextClassificationStep2)を選択し、[Build Phases] タブを確認して、バンドルに追加されていること(デバイスにデプロイされること)を再確認します。

20b7cb603d49b457.png

9. 語彙を読み込む

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. 文字列をトークンのシーケンスに変換する

ユーザーが単語を文として入力すると、文字列になります。文中の各単語が辞書に存在する場合、vocab で定義されている単語のキー値にエンコードされます。

通常、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 Pod とインタープリタを実装すると、Tensor 型にアクセスできるようになります。

次のようにコードを開始します(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) ?? []

これで、データを解析してスパムの品質を判断することが比較的容易になります。モデルには 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)
  }
}

低レベルのメモリを扱う場合は「unsafe」データを使用します。上記のコードでは、unsafe データの配列を初期化する必要があります。この拡張機能を使用すると、次のことが可能になります。

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

「Buy my book to learn online trading!」というメッセージが送信された場合、アプリはスパム検出アラートを 99% の確率で返します。

14. 完了

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

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