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

1. 始める前に

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

前提条件

  • この Codelab は、機械学習に慣れていないデベロッパーを対象としています。
  • この Codelab は、順序付けられたパスウェイの一部です。まだ「基本的なメッセージ スタイルのアプリを作成する」または「コメント スパムの ML モデルを作成する」を完了していない場合は、ここで作業を中断して、まずそちらを完了してください。

[作成または学習] する内容

  • 前の手順で作成したアプリにカスタムモデルを統合する方法について説明します。

必要なもの

  • Android Studio、または iOS の場合は CocoaPods

2. 既存の Android アプリを開きます

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

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

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

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

開いたら、手順 2 に進みます。

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

コメント スパムの ML モデルを作成する Codelab では、.TFLITE モデルを作成しました。

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

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

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

d7c3e9f21035fc15.png

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

2137f956a1ba4ef0.png

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

ae858835e1a90445.png

  1. [アセット] を右クリックします。
  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 つ目は、一番下の 依存関係セクションにあります。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. 完了したら、Project Explorer で [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 モデルを作成するをご覧ください)。

分類は、分類メソッドで行われます。分類メソッドに文字列を渡すと、List が返されます。機械学習モデルを使用してコンテンツを分類する際、文字列がスパムかどうかを判断したい場合、すべての回答に確率を割り当てて返すのが一般的です。たとえば、スパムのように見えるメッセージを渡すと、2 つの回答のリストが返されます。1 つはスパムである確率、もう 1 つはスパムではない確率です。「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. 次に、ヘルパーを読み込みます。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 としても利用できます。

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

ユーザーは単語を入力して文章を作成します。この文章が文字列になります。文中の各単語が辞書に存在する場合、その単語は語彙で定義されている単語の Key-Value にエンコードされます。

NLP モデルは通常、固定のシーケンス長を受け入れます。ragged tensors を使用してビルドされたモデルには例外がありますが、ほとんどの場合は修正されています。モデルの作成時にこの長さを指定しました。iOS アプリでも同じ長さを使用してください。

前回使用した Colab の 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

まず、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 つの出力ニューロンがあるため、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 つはメッセージがスパムではない可能性、もう 1 つはスパムである可能性です。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 拡張機能を追加する

上記のコードでは、Data 型の拡張機能を使用して、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. 完了

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

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