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

1. 始める前に

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

前提条件

  • この Codelab は、ML を初めて利用する経験豊富なデベロッパーを対象としています。
  • この 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 フォルダを右クリックします。[新規] を選択 >ディレクトリ。

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 プロジェクトには複数のプロジェクトがあるため、必ず app レベル 1 を見つけるようにしてください。Android ビューのプロジェクト エクスプローラの [Gradle Scripts] セクションで、このファイルを見つけます。以下に示すように、正しいアプリには .app というラベルが付きます。

6426051e614bc42f.png

このファイルに 2 つの変更を加える必要があります。1 つ目は、一番下の dependency セクションにあります。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. 画面の中央にパッケージ名の入力を求めるダイアログが表示されます。現在のパッケージ名の末尾に追加してください。(ここでは、これを helpers と呼んでいます)。

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 型はテキストタスク ライブラリの一部であり、文字列をトークンに変換し、正しいシーケンス長を使用してモデルに渡して、結果を解析するのに役立ちます。

(これらについて詳しくは、「コメントスパムの機械学習モデルの構築」を再確認してください)。

分類は、分類メソッドで行われます。分類メソッドに文字列を渡すと、List が返されます。機械学習モデルを使用してコンテンツを分類する際、文字列がスパムかどうかを判断したい場合、すべての回答に確率を割り当てて返すのが一般的です。たとえば、スパムと思われるメッセージを渡すと、次の 2 つの回答リストが返されます。1 つ目は迷惑メールである確率、2 つ目は迷惑メールではない確率です。「Spam/Not Spam」はカテゴリなので、返される List にはこれらの確率が含まれます。後で解析します。

ヘルパークラスを作成したので、MainActivity に戻り、これを使用してテキストを分類するように更新します。これは次のステップで確認します。

6. テキストを分類する

まず、作成したヘルパーを MainActivity にインポートします。

  1. MainActivity.kt の先頭に、他のインポートとともに以下を追加します。
import com.google.devrel.textclassificationstep2.helpers.TextClassificationClient
import org.tensorflow.lite.support.label.Category
  1. 次に、ヘルパーを読み込みます。onCreatesetContentView 行の直後に次の行を追加して、ヘルパークラスをインスタンス化して読み込みます。
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. ここでモデルの動作を確認できます。「ブログにアクセスして商品を購入!」というメッセージはスパムの可能性が高いと判断されました:

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

1 行目に、「TextClassificationStep2」ではなくアプリ名を記述します。

ターミナルを使用して、そのディレクトリに移動し、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 でファイルを開くと、エンコードを確認できます。「歌」に似た言葉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 Pod とインタープリタを実装するとき、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 つの出力ニューロンがあるため、8 バイトで読み込む必要があります。8 バイトは解析のために Float32 に変換されます。低レベルのメモリを扱っているため、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」コマンドを上のコードでは、安全でないデータの配列を初期化する必要があります。この拡張機能により、次のことが可能になります。

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. 完了

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

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