every Tech Blog

株式会社エブリーのTech Blogです。

Android で機械学習機能を実装してみる

はじめに

こんにちは、デリッシュキッチンでクライアントエンジニアを担当している kikuchi です。

近年 AI 技術の発展が著しく、中でも生成 AI がかなりの勢いで発展し、普段使いや仕事で ChatGPT などの生成 AI のサービスを取り入れる方や企業が多くなってきました。
今回は多くの場合で生成 AI の機能を実現している機械学習 (ML : Machine Learning) について、Android アプリで簡単に実装する方法に触れてみたいと思います。

なお、弊社は開発生産性の向上などを目的として Cursor を導入するなど、積極的な AI の活用を取り入れていますので、ご興味がある方はこちらの記事も見ていただけると嬉しいです。
エブリー、AIエディタ「Cursor」を全エンジニアおよびプロダクトマネージャーに導入

アプリに組み込む方法

「機械学習」という言葉を使うと難しい印象がありますが、既に機械学習の機能を提供するフレームワークは多く存在しており、中でも Google が提供する MediaPipe というフレームワークを使うことで簡単に生成 AI といった機能を実装することができます。
また MediaPipe では様々なソリューションが提供されており、2025 年 7 月時点では全てのソリューションが Android の端末で利用可能となります。

以下公式サイトにて、利用可能なソリューションがまとまっています。
https://ai.google.dev/edge/mediapipe/solutions/guide?hl=ja#available_solutions

今回は、その中でも動きがイメージしやすい 画像分類 (Tasks API)生成 AI (LLM Inference API) について触れてみたいと思います。

画像分類 (Tasks API) の実装方法

まずは画像分類 (Tasks API) の実装方法についてまとめていきます。

1. モデルデータを app/src/main/assets に追加

機械学習のデータを動かすためには当然モデルデータが必要となります。
今回は Google AI for Developers が提供している画像分類モデルを使用したいと思います。

https://ai.google.dev/edge/mediapipe/solutions/vision/image_classifier/index?hl=ja#efficientnet-lite0_model_recommended

モデルには int8 と float32 が存在しますが

  • int8 … ファイルサイズが軽量で、処理速度が速いが精度はわずかに低い
  • float32 … ファイルサイズが大きく、処理速度が遅いが精度は高い

という特徴があり、スマホなどストレージやメモリが限られている場合は int8 を使用するとよいかと思います。
本記事でも int8 を導入する前提での実装方法をまとめていきます。

2. app レベルの build.gradle に Vision Task ライブラリを追加

dependencies {
    implementation("com.google.mediapipe:tasks-vision:0.10.26")
}

3. ImageClassifier の初期化

ImageClassifier は画像分類タスクを実行するためのクラスとなります。
モデルデータを読み込ませて初期化するため、モデルデータ自体のサイズにもよりますがやや初期化コストが高くなるので、一度だけ実行して再利用する形が良いです。

lateinit var imageClassifier: ImageClassifier

fun initImageClassifier(context: Context) {
    val options = ImageClassifier.ImageClassifierOptions.builder()
        .setBaseOptions(BaseOptions.builder().setModelAssetPath("efficientnet_lite0.tflite").build())  // ① … 読み込ませるモデルデータの設定
        .setRunningMode(RunningMode.IMAGE)  // ② … 分類する画像データの種別
        .setMaxResults(3)                   // ③ … 返却するレスポンスの数
        .setScoreThreshold(0.5F)            // ④ … 出力結果の確信度のしきい値
        .build()
    try {
        imageClassifier = ImageClassifier.createFromOptions(context, options)
    } catch (e: IllegalStateException) {
        Log.e("ImageClassifier", "TFLite failed to load model with error: " + e.message)
    }
}

パラメータが多いため、細かく確認していきたいと思います。

①については、モデルデータの読み込みとなるため assets ファイルに配置したファイルを指定しています。

②については、読み込ませる画像データの種別を設定するもので、静止画 (IMAGE) / 動画 (VIDEO) / ストリーム (LIVE_STREAM) を設定できます。
今回は静止画を読み込ませるため、静止画 (IMAGE) を設定しています。

③については、返却するレスポンスの数となり、3 を設定した場合は確信度 (スコア) を降順で 3 つ返却する形となります。

④については、③でも触れた確信度の事で、指定した数値以上の確信度の項目のみレスポンスに含めます。
0.5F を指定した場合、確信度が 50% 以上のもののみレスポンスに含める、という形となります。

4. データの分類実行

3 までで必要な設定は完了したため、最後にデータの分類を実行します。

fun classifyImage(bitmap: Bitmap) {
    // 初期化完了済みかチェック
    if (!::imageClassifier.isInitialized) {
        return
    }

    // データを分類してログ出力
    val mpImage = BitmapImageBuilder(bitmap).build()
    val result = imageClassifier.classify(mpImage)
    result.classificationResult().classifications().forEach { classification ->
        classification.categories().forEach { category ->
            Log.d(
                "ImageClassifier",
                "Category: ${category.categoryName()}, Score: ${category.score()}"
            )
        }
    }
}

Bitmap のデータを ImageClassifier$classify メソッドで読み込ませ、結果をログで出力する形となります。

実行結果

今回はモデルファイルと同様に、以下の Google AI for Developers の画像分類タスクガイドのページに設置されているフラミンゴの画像を分類してみたいと思います。

https://ai.google.dev/edge/mediapipe/solutions/vision/image_classifier

出力ログは以下のようになりました。

89.4% という高い確信度で flamingo と分類されました。
(公式の 95% という数値よりやや下がっているのは、おそらく文字と動物が混在してしまっている影響だと考えます)

他にも色々画像を読み込ませましたが、

  • 対象の動物以外 (木や草など) は写り込まない方が精度が高い
  • 正面をはっきり向いていて、顔が識別できる方が精度が高い
  • 犬は確信度がかなり低い (毎回 30% 程度)
  • ライオンは確信度がかなり高い (木や草が写り込んでも 90% 以上となる)

となり、提供されているモデルデータでは分類できる・できない動物がはっきりしている、という興味深い結果となることがわかりました。

画像分類 (Tasks API) の実装方法については以上となります。

生成 AI (LLM Inference API) の実装方法

次に生成 AI (LLM Inference API) の実装方法についてまとめていきます。
こちらも基本的な実装の流れは画像分類 (Tasks API) と同様で、今回はチャット形式の挙動を実装してみたいと思います。

1. モデルデータを app/src/main/assets に追加

こちらも Google AI for Developers からモデルデータをダウンロードします。
※今回は Gemma-3 1B というモデルデータを使用しますが、Hugging Face のアカウントが必要となる点にご注意ください。

https://ai.google.dev/edge/mediapipe/solutions/genai/llm_inference?hl=ja#gemma-3_1b

こちらですが、画像分類と違ってモデルデータが 500MB 以上になるなどかなりサイズが大きくなります。
モデルデータが複数存在するため詳細は割愛しますが、今回は dynamic_int4 QAT というものを採用しました。

2. app レベルの build.gradle に Gen AI Task ライブラリを追加

dependencies {
    implementation("com.google.mediapipe:tasks-genai:0.10.25")
}

3. モデルデータを assets から cache にコピーする

LLM Inference API については直接 assets フォルダからモデルデータを参照する方法がないため、一度 cache フォルダにデータをコピーしてから読み込ませます。

private fun copyModelFromAssetsToCache(context: Context, modelName: String): String {
    val outputFile = File(context.cacheDir, modelName)
    if (outputFile.exists()) {
        return outputFile.absolutePath
    }

    context.assets.open(modelName).use { inputStream ->
        FileOutputStream(outputFile).use { outputStream ->
            inputStream.copyTo(outputStream)
        }
    }
    return outputFile.absolutePath
}

なぜ assets から直接参照できないのか、という点ですが、アプリで巨大なデータを効率的に扱うためにはメモリマッピングという技術が必要となり、その技術は実ファイルパスが必要なため assets ファイルでは使用できなくなっており、一度 cache の領域にコピーしてからそちらのファイルに対してアクセスする必要があります。
おそらく LLM Inference API でも巨大なモデルデータを扱う想定で、assets に直接アクセスするメソッドを蓋閉じしているものと推測しています。

4. LlmInference の初期化

LlmInference は生成 AI のタスクを実行するためのクラスとなります。
モデルデータがかなり大きく初期化に数秒はかかってしまうため、こちらも一度だけ実行して再利用する形が良いです。

private lateinit var llmInference: LlmInference

fun initLlmInference(context: Context) {
    // LLM Inference の初期化
    val taskOptions = LlmInference.LlmInferenceOptions.builder()
        .setModelPath(copyModelFromAssetsToCache(context, "gemma3-1b-it-int4.task"))
        .build()

    llmInference = LlmInference.createFromOptions(context, taskOptions)
}

5. 生成 AI 実行

4 までで必要な設定は完了したため、最後に生成 AI を実行します。

private val _updateResponse: MutableSharedFlow<String> = MutableSharedFlow()
val updateResponse: Flow<String> = _updateResponse

fun generateResponse(text: String) {
    viewModelScope.launch(Dispatchers.IO) {
        val result = llmInference.generateResponse(text)
        _updateResponse.emit(result)
    }
}

こちらはテキストで受け取った文字列を解析し、レスポンスデータを生成する流れとなっています。
今回は UI 上で表現したかったため、Flow で Fragment 側にデータを送っています。

実行結果

実際にやり取りした結果を画像で載せたいと思います。

このモデルデータでは日本語に対しては日本語で、かつ顔文字も使って回答を生成してくれることが分かります。

生成 AI (LLM Inference API) の実装方法については以上となります。

モデルデータを組み込むメリットについて

アプリにモデルデータを組み込む一番のメリットは オフラインでも結果を取得できる事 だと考えます。
今では通信してデータを取得する流れが当たり前となっていますが、電波が遮断されている、あるいは通信が安定しない環境下でも適切な答えを取得できる仕組みであれば、ユーザは通信環境を気にせずアプリを触ることができるため、ユーザフレンドリーにもなります。

市場に公開するレベルまでモデルデータを学習させることは難しいかと思いますが、何らかの方法でアプリに組み込んだモデルデータを差し替える仕組みさえ確立すれば、アプリ単体で常に最新モデルを取り扱うことができるようになります。

課題について

一番の問題はやはり モデルデータが大きい事 です。
今回は 500MB ものデータを assets に組み込むという方法で無理やり実装しましたが、アプリに内包するということは当然アプリダウンロード時のバイナリサイズも大きくなるということなので、運用方法を検討する必要があります。

方法として考えられることは

  1. assets に組み込む (本記事のやり方)
  2. アプリ起動後にモデルデータをダウンロードする

あたりがありますが、別の問題として 1 のケースでも 2 のケースでも モデルデータという重要な資産を抜き取られることを防ぐ必要がある というセキュリティ面の課題が発生します。
暗号化を実施する、ネイティブコードを活用するなど回避する方法はいくつかあると思いますが、いずれにせよモデルデータをアプリで取り扱うため検討する事項は多くありそうです。

おわりに

今回は MediaPipe のフレームワークを使い、Android アプリに機械学習の機能を実装する方法をまとめてみましたが、以前は機械学習について専門知識や複雑なコードが必要でしたが、今では簡単に実装できることが分かりました。
MediaPipe には今回紹介した画像分類や生成 AI 以外にも、物体検出顔検出ジェスチャー認識 など、すぐに使える強力な機能が数多く提供されていますので、ぜひ公式ドキュメントで色々と確認してみてください。

本記事の情報が皆様のお役に立てれば幸いです。