はじめに

Create MLを初めて触ってみたので、まずは一番シンプルそうな「画像分類(Image Classification)」で、モデルを作ってアプリに組み込むところまで試してみました。
今回は “精度を突き詰める” というよりも、学習 → 書き出し → iOSで推論 の流れを一通り通すことが目的です。

この記事のゴールは、写真を1枚選ぶと 「犬 / ライオン / キリン」 を分類して、Top3と確率を表示できる状態にするところまでです。

今回作るもの(完成イメージ)

作るのは、かなり小さなデモアプリです。

  • PhotosPickerで画像を選択
  • 画像を表示
  • 分類結果(1位のラベル+確率)を表示
  • Top3(ラベル+確率)を表示

クラス(分類ラベル)は以下の3つにしました。

  • ライオン
  • キリン

Create MLでモデルを作る

まず今回使用する画像分類のモデルを作成します。XcodeではCreate MLという機能を用いて機械学習モデルを簡単に作成することができます。

上タブのXcode > Open Developer Tool > Create MLを選択して開きます。

Create Projectを選ぶと、まず「作成するモデルの種類」を選択する画面が表示されます。今回は画像をラベルごとに分類したいので、Image Classification を選びます。

次へ進むと、モデル名を入力する画面になります。モデル名を決めて進むと、学習データを指定してトレーニングを行うメイン画面に移ります。この画面で、トレーニングに使うフォルダを指定してモデルを学習させます。

今回のデータは、次のようにラベルごとにフォルダを分けて用意しました。フォルダ名がそのまま分類結果のラベル名として使われるので、「犬」「ライオン」「キリン」という日本語フォルダ名で作成しています。日本語でも特に問題なく扱えました。画像自体はAIで生成したものを使っています。

  • キリン/
  • 犬/
  • ライオン/

フォルダを選択できたら、画面上部(タブの左側)にある 再生ボタン(▶︎) を押してトレーニングを開始します。学習が完了したら、Create MLの Preview 機能を使って、その場で簡単に動作確認ができます。Previewで画像を入れてみると、犬・ライオン・キリンがそれぞれ意図した通りに分類されることが確認できました。

動作確認ができたら、最後にこのモデルをXcodeで使えるように 書き出し(Export) します。Create MLの画面から .mlmodel としてエクスポートし、Xcodeのプロジェクトに追加すれば、iOSアプリ側で推論に利用できる状態になります。今回はここまでで、モデル作成側の作業は完了です。

iOSアプリに組み込む準備

1 .mlmodel をXcodeに追加

Create MLで書き出した .mlmodel を、Xcodeのプロジェクトにドラッグ&ドロップします。

2 生成されたクラスを使う

.mlmodel を追加すると、Xcodeがモデル用のクラス(例:AnimalClassifier)を自動で作ってくれます。
今回はこのクラスを使ってモデルを読み込みます。

3 推論はVision経由で行う

Core MLを直接呼ぶこともできますが、画像を渡す前にサイズ調整などが必要になります。
今回はそのあたりを楽にするため、Vision経由で分類します。

推論部分の実装(Vision経由)

推論は ImageClassifier にまとめました。流れはシンプルです。

  1. モデルを VNCoreMLModel にする
  2. VNCoreMLRequest を作る
  3. VNImageRequestHandler で実行する
  4. 結果(VNClassificationObservation)からTop3を作る
ImageClassifier.swift
import Foundation
import Vision
import CoreML
import UIKit
final class ImageClassifier {
struct Best {
let label: String
let confidence: Double
}
struct Output {
let best: Best?
let top3: [(String, Double)]
}
// ① Create MLで作ったモデル(Core MLモデル)を Vision で扱える形に変換して保持しておく
// - AnimalClassifier は .mlmodel をXcodeに入れたときに自動生成されるクラス
// - VNCoreMLModel にすることで、Vision のリクエスト(VNCoreMLRequest)として実行できる
private lazy var vnModel: VNCoreMLModel = {
do {
let model = try AnimalClassifier(configuration: MLModelConfiguration()).model
return try VNCoreMLModel(for: model)
} catch {
fatalError("モデルのロード失敗: \(error)")
}
}()
func classify(_ image: UIImage) async throws -> Output {
// ② Vision は CGImage を扱うのが得意なので、UIImage から CGImage を取り出す
// - ここが取れないケースもあるので guard で安全に抜ける
guard let cgImage = image.cgImage else {
return Output(best: nil, top3: [])
}
// ③ 「このモデルで分類してね」というリクエストを作成
// - imageCropAndScaleOption は入力画像をモデル用サイズに合わせる時の方針
// - .centerCrop は中央を切り出して正方形寄りにして渡す(画像分類でよく使う)
let request = VNCoreMLRequest(model: vnModel)
request.imageCropAndScaleOption = .centerCrop
// ④ 画像と向き情報(orientation)を Vision に渡す準備をする
// - Photos から取得した画像は向き情報がメタデータで付いていることが多い
// - これを渡しておかないと、入力が回転した状態で推論されて精度が落ちることがある
let handler = VNImageRequestHandler(
cgImage: cgImage,
orientation: CGImagePropertyOrientation(image.imageOrientation),
options: [:]
)
// ⑤ ここで実際に推論が走る(同期実行)
// - 実行後、request.results に推論結果が入る
try handler.perform([request])
// ⑥ 結果を取り出す
// - 画像分類の場合、VNClassificationObservation の配列として返ってくる
// - identifier: ラベル名(今回は「犬」「ライオン」「キリン」など)
// - confidence: 0〜1 のスコア(大きいほどそれっぽい)
let results = (request.results as? [VNClassificationObservation]) ?? []
// ⑦ confidence が高い順に並べ替えて、Top3 を作る
let sorted = results.sorted { $0.confidence > $1.confidence }
let top3 = Array(sorted.prefix(3)).map { ($0.identifier, Double($0.confidence)) }
// ⑧ 1位(ベスト)だけ取り出す
let best: Best?
if let first = sorted.first {
best = Best(label: first.identifier, confidence: Double(first.confidence))
} else {
best = nil
}
return Output(best: best, top3: top3)
}
}
// UIImageの向きをVisionへ渡す変換
extension CGImagePropertyOrientation {
init(_ orientation: UIImage.Orientation) {
switch orientation {
case .up: self = .up
case .down: self = .down
case .left: self = .left
case .right: self = .right
case .upMirrored: self = .upMirrored
case .downMirrored: self = .downMirrored
case .leftMirrored: self = .leftMirrored
case .rightMirrored: self = .rightMirrored
@unknown default: self = .up
}
}
}

画像の扱いは imageCropAndScaleOption = .centerCrop にしています。中央を切り出してモデルに渡す設定です。

結果は VNClassificationObservation の配列で返ってきます。主に確認するのは次の2点です。

  • identifier:ラベル名(犬 / ライオン / キリン)
  • confidence:0〜1のスコア(大きいほどそれっぽい)

画面側の実装(PhotosPicker)

UI側は ContentView で、画像選択と結果表示だけ行います。

  • PhotosPickerで画像を選択
  • UIImage に変換
  • classifier.classify(image) を呼ぶ
  • 返ってきた besttop3 を表示

表示については、1位の結果を大きく出しつつ、Top3を並べるだけでも“それっぽい”見た目になります。

ContentView.swift
import SwiftUI
import PhotosUI
struct ContentView: View {
@State private var pickerItem: PhotosPickerItem?
@State private var image: UIImage?
@State private var resultText: String = "未推論"
@State private var top3: [(String, Double)] = []
@State private var isRunning = false
private let classifier = ImageClassifier()
var body: some View {
NavigationStack {
VStack(spacing: 16) {
PhotosPicker(selection: $pickerItem, matching: .images) {
Text("画像を選んで分類する")
.font(.headline)
}
if let image {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(maxHeight: 320)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
if isRunning {
ProgressView("分類中…")
} else {
Text(resultText)
.font(.title3)
.bold()
}
if !top3.isEmpty {
VStack(alignment: .leading, spacing: 8) {
Text("Top3")
.font(.headline)
ForEach(Array(top3.enumerated()), id: \.offset) { i, item in
HStack {
Text("\(i + 1). \(item.0)")
Spacer()
Text(String(format: "%.1f%%", item.1 * 100))
.monospacedDigit()
}
}
}
.padding()
.background(.thinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
Spacer()
}
.padding()
.navigationTitle("画像分類(1枚)")
.onChange(of: pickerItem) { _, newValue in
guard let newValue else { return }
Task { await run(item: newValue) }
}
}
}
@MainActor
private func run(item: PhotosPickerItem) async {
isRunning = true
defer { isRunning = false }
do {
guard let data = try await item.loadTransferable(type: Data.self),
let uiImage = UIImage(data: data) else {
resultText = "画像の読み込みに失敗しました"
top3 = []
return
}
image = uiImage
let output = try await classifier.classify(uiImage)
if let best = output.best {
resultText = "結果: \(best.label) \(Int(best.confidence * 100))%"
} else {
resultText = "分類結果が取得できませんでした"
}
top3 = output.top3
} catch {
resultText = "エラー: \(error.localizedDescription)"
top3 = []
}
}
}

完成アプリ

犬・ライオン・キリンを分類するアプリケーションが完成しました。実際にいくつか画像を試してみたところ、意図した通りに分類できていることが確認できました。
特に犬については、犬種が異なる画像でも「犬」として判定できており、サンプル数が各クラス3枚と少ない中でも、ある程度特徴を学習できているように感じました。

まとめ

Create MLで画像分類モデルを作って、iOSアプリで推論するところまでを一通り試しました。
画像分類は、モデル作成〜組み込みまでの流れがシンプルで、初回の取っ掛かりとしてちょうど良かったです。

次にやるなら、まずは学習データを増やして精度を上げるか、カメラ入力にしてリアルタイム分類にしてみたいと思います。



ギャップロを運営しているアップフロンティア株式会社では、一緒に働いてくれる仲間を随時、募集しています。 興味がある!一緒に働いてみたい!という方は下記よりご応募お待ちしております。
採用情報をみる