工場裏のアーカイブス

素人によるiPhoneアプリ開発の学習記 あと機械学習とかM5Stackとか

Core MLを用いた、iPhone での機械学習あれこれ(3) Create ML編

前回記事の末尾で触れた通り、Create MLを使った機械学習についても試してみました。

Create ML上にテンプレートが用意されている機械学習(画像分類、オブジェクト検出、テキスト分類など)であれば、自分でコードを書いたりネットワークモデルを構築したりすることなく、画面操作だけでCore MLモデルファイル(.mlmodel)を作ることまで出来てしまい、非常にお手軽です。もちろんVisionでも利用可能です。

本記事では題材として、Create MLを活用して、iPhoneのカメラに映った「ラーメン」と「スパゲッティ」を見分けるアプリを作成してみます。目の前に麺料理が置かれているけれど、どちらか分からない、そんな良くあるシチュエーションで役立つかもしれません(0点)。
 

下準備:学習用の画像データ準備

Create MLに進む前に、まずは学習に用いる画像データを用意しておく必要があります。本記事の題材では、大量のラーメンとスパゲッティの画像を用意しなくてはなりません。

また、これらだけを学習させたモデルでは、何であっても無理矢理ラーメンかスパゲッティのどちらかに分類してしまいます。これを避けるために、ここでは「どちらでもない物」の画像、すなわち単なる風景や人物の写真も用意して一緒に学習させてみます。

ラーメンとスパゲッティについては、Google画像検索で出てくる画像を利用させて頂く方針とします。なお2019年1月より施行された著作権法の改正により、第三者著作権を有する画像であっても、機械学習に限った利用(学習用データとしたり、それにより得られた学習済みモデルを公開したり)であれば合法的に行えるようになったようです。

ただし、画像を1つ1つ手作業でダウンロードしていたら大変ですので、ツールを使った方が良いと思います。ここでは私が試してみて便利だと感じた外部ツールを2つ紹介します(使用法などの詳細までは立ち入りませんが)。

  • Image Downloader
    • こちらはChrome拡張機能であり、開いているページ内の画像をまとめてダウンロードすることが出来ます。チェックボックスで選択した画像や、指定したサイズの範囲に入る画像のみをダウンロードするなど、細かい設定も可能です。

  • google-image-download
    • こちらはコマンドラインから使用するツールです。こちらのサイトで導入・使用方法が詳しく解説されています。ただし2020/04/08現在、記事通りにpipでインストールしたものを使用しても、画像のダウンロードが上手くいかないようです(Google側の仕様変更が原因である模様)。その場合はこちらのサイトで解説されている通りに、パッチ版を入れたら上手く行くようになりました。


ここではラーメン、スパゲッティの画像をそれぞれ60枚用意します(50枚はトレーニング用、10枚はテスト用とする)。機械学習用としては枚数が少なく思えますが、Create MLではこの程度の量でも学習出来るのがウリとのことですので、これでやってみます。
f:id:fleron:20200405180735p:plain

なおCreate MLで用いる画像データの指針が公式のドキュメントに詳しく書かれています。重要そうな箇所をピックアップしてざっくり意訳すると以下のようになります。

  • 各ラベル(分類)の画像はそれぞれ約80%をトレーニング用、20%をテスト用に回す。
  • 各ラベルのトレーニング用画像は、それぞれ少なくとも10枚を用意する。
  • 各ラベル毎の画像数はバランスを取る(本記事の例では、ラーメンの画像1000枚、スパゲッティの画像10枚のように偏るのはNG)


そして先述の通り、今回はラーメンとスパゲッティのどちらでもない物の画像を用意します。ここでは手許にあったCOCO データセット(2017)から、風景や人物が写った画像を60枚適当にピックアップして使用してみます。
f:id:fleron:20200405223414p:plain

そして下図のようにトレーニング用、テスト用の画像を格納するためのフォルダを作成し、それらの下には各画像のラベルとなる名前を付けたフォルダをそれぞれ作成します。ここではラーメンは”ramen”、スパゲッティは”spaghetti”、どちらでもない物は”other"フォルダとしました。これらの名前はそのままCreate MLに取り込まれ、出力されるCore MLモデルでも使用されるので注意が必要です
f:id:fleron:20200405225603p:plain

後はそれぞれのフォルダに、集めた画像をトレーニング用とテスト用に分けて格納しておきます。
 

Create MLでラーメンとスパゲッティを分類するモデル作成

  • いよいよCreate MLを実際に使ってみます。Xcode(本記事執筆時点:Version 11.4)メニューの「Open Developer Tool」から起動することが出来ます。

f:id:fleron:20200405230333p:plain
 

  • 最初にファイル選択用のダイアログが表示されますので、新しく学習を行う場合は、左下の「New Document」を選択します。

f:id:fleron:20200406223452p:plain
 

  • 実行したい機械学習のテンプレートを選択するダイアログが表示されます。本記事では画像分類を行いたいので「Image Classifier」を選択します。

f:id:fleron:20200406223654p:plain
 

  • プロジェクト名を入力するダイアログが表示されますので、適当な名前を付けて保存します。ここでは「NoodleClassifier」という名前にしました(スパゲッティとラーメンにしか対応しませんが)。このプロジェクト名は、後々出力するモデルファイル名にも付与されます。

f:id:fleron:20200406224123p:plain
 

  • プロジェクトを保存すると、Create MLのメイン画面が開きます。

f:id:fleron:20200406224719p:plain
 

  • 「Training Data」の枠に、あらかじめ準備したトレーニング用画像を格納したフォルダを、ドラッグ&ドロップで投入します(下にある「Choose」の部分を操作することで、ダイアログからフォルダを選択することも出来ます)。

f:id:fleron:20200406230833p:plain
 

  • 同様に「Testing Data」にも、テスト用画像を格納したフォルダを投入します。正常に行けば下図のようになります。今回トレーニング画像は60枚 x ラベル3種 = 180枚、テスト画像は10枚 x ラベル3種 = 30枚であり、正しい数が表示されています。「Validation Data」は自動選択(Auto)に任せることにします。

f:id:fleron:20200406230144p:plain
 

  • 後はトレーニングの最大繰り返し回数(Maximum Iterations)やデータオーギュメンテーションの設定をします。これらについては公式ドキュメント(日本語)にも簡単な解説があります。回数を大きくしたり、オーギュメンテーションの設定を適用したりすることで、モデルの画像分類精度を向上できる可能性はありますが、もちろんそれだけ学習に時間が掛かるようになります。ここでは全く根拠はありませんが、下図の設定で学習を行ってみます。

f:id:fleron:20200407230845p:plain
 

  • 後は画面上部の「Train」ボタンをクリックするだけで学習が始まります。下図のように進捗バーが表示されますので、終わるまで気長に待ちましょう。今回の学習は、私の環境(MacBook Air、13-inch、2018)で約30分でした。

f:id:fleron:20200407001247p:plain
 

  • 学習後は下図のように結果が表示されます。今回はトレーニング用画像に対する精度は100%。テスト用画像に対する精度は97%となりました(同じ設定でも、実行するたびに乱数によって結果は微妙に異なってきます)。少ない画像数と適当な設定の割には中々のものです。

f:id:fleron:20200407230916p:plain
 

  • 画面右上の「Output」にCore MLモデルが生成されていますので、フォルダに対してドラッグ&ドロップすれば取得出来ます。これにてCreate MLを用いた学習の手順が一通り完了しました。

f:id:fleron:20200407003840p:plain
 

iPhoneカメラに映ったラーメンとスパゲッティを見分ける

早速、生成したCore MLモデルを用いて、iPhoneカメラに映ったラーメンとスパゲッティを見分けるアプリを実装します。Xcodeで「Single View App」の新規プロジェクトを作成し、NoodleClassifierのCore MLモデルを追加しておきます。

そしてStoryboard上に、適当なサイズのUIView、UITextViewを配置します(下図の例では青い正方形がUIView、白い長方形がUITextViewです)。このView上にiPhoneカメラに映っているものを表示します。
f:id:fleron:20200406234224p:plain
 
後はAVFoundationを用いて、iPhoneカメラの映像をリアルタイムに取得し、フレーム毎に前記事と同様の手順で、Vision上でNoodlClassifierを用いた画像分類を行います。そこにラーメン or スパゲッティが映っているのを検出したら「ラーメン発見!」 or 「スパゲッティ発見!」とUITextViewに表示します。以下にコードを示します。

import UIKit
import AVFoundation
import CoreML
import Vision

class ViewController: UIViewController {
    
    //Storyboardに配置したUIView、UITextViewのアウトレット
    @IBOutlet weak var videoView: UIView!
    @IBOutlet weak var resultTextView: UITextView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        self.Capture()
    }
    
    func Capture(){
        //AVCaptureSessionはデバイスからの入出力管理を担う
        let captureSession = AVCaptureSession()
        //画質の設定。ここでは高解像度の画像出力が可能な.photoを設定
        captureSession.sessionPreset = .photo
        
        //入力の設定。ここではデバイスとしてビデオカメラを使用する。
        //guard文で設定が正しく行われたか、複数判定を同時に実施。
        guard let captureDevice = AVCaptureDevice.default(for: .video),
            let captureDeviceInput = try? AVCaptureDeviceInput(device: captureDevice),
            captureSession.canAddInput(captureDeviceInput) else {
                fatalError("Error: 入力デバイスの設定に失敗しました")
            }
        captureSession.addInput(captureDeviceInput)
        
        //出力の設定。
        let videoDataOutput = AVCaptureVideoDataOutput()
        //AVCaptureVideoDataOutputSampleBufferDelegateプロトコルに適合する
        //デリゲート先を設定(ここではselfとして、viewController自身に設定)
        //デリゲート先で、フレーム毎にcaptureOutputメソッドが呼ばれるようになる
        videoDataOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "VideoQueue"))
        
        guard captureSession.canAddOutput(videoDataOutput) else {
            fatalError("Error: 出力デバイスの設定に失敗しました")
        }
        captureSession.addOutput(videoDataOutput)
        
        // Storyboardに配置したUIView上に、プレビューを表示するように設定する。
        let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        previewLayer.videoGravity = .resizeAspectFill
        previewLayer.frame = self.videoView.bounds
        self.videoView.layer.insertSublayer(previewLayer, at: 0)
        
        // キャプチャ開始
        captureSession.startRunning()
    }
}

//setSampleBufferDelegateで設定した通り、AVCapture〜Delegateプロトコルを指定
//フレーム毎に呼ばれるcaputeOutputメソッドを実装し、ここでCore MLによる
//画像判定の処理を行う
extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate {
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        //CMSampleBufferをCVPixelBufferに変換し、Core MLの入力と出来るようにする
        //(返り値はCVImageBufferとなっており、CVPixelBufferは元々そのタイプエイリアス)
        guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
            fatalError("Error: バッファの変換に失敗しました")
        
        }
        // NoodleClassifierのモデルをVisionで利用出来るようにロード
        guard let model = try? VNCoreMLModel(for: NoodleClassifier().model) else {
            fatalError("Error: Core MLモデルのロードに失敗しました")
        }
        
        // Core MLモデルを用いた画像認識リクエストを作成
        let request = VNCoreMLRequest(model: model) { [weak self] (request: VNRequest, error: Error?) in
            guard let results = request.results as? [VNClassificationObservation] else { return }
            
            var text : String = ""
            
            //ラーメン or スパゲッティのconfidenceが0.8を超えた場合は、それを検出したと見做す。
            if let result = results.first{
                if result.identifier == "ramen" && result.confidence > 0.8{
                    text = "ラーメン発見!"
                }else if result.identifier == "spaghetti" && result.confidence > 0.8{
                    text = "スパゲッティ発見!"
                }
            }
            
            DispatchQueue.main.async {
                self?.resultTextView.text = text
            }
        }
        request.imageCropAndScaleOption = .centerCrop
        
        //リクエストを処理するためのハンドラ.
        let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer)
       
        do{
            try handler.perform([request])
        } catch {
            fatalError("Error: 画像認識リクエストの処理に失敗しました")
        }
    }
}

※本コードの実装においては、以下のサイトを参考にさせて頂きました。
iOS11のVision.frameworkを使ってみる - Qiita
Swiftでカメラアプリを作成する(1) - Qiita
AVCaptureVideoDataOutputの使い方 - Qiita
 
 
iPhoneの実機で早速アプリを試してみます。最初にごはんの類を映してみましたが何の反応も示しません。これを麺類と言い張られても困るのでOKですね。
f:id:fleron:20200408231830p:plain
 
一方でラーメンの類を映してみたところ、きちんと発見してくれました。このようなパッケージ写真であっても正しく検出出来るようです。
f:id:fleron:20200408231842p:plain
 
スパゲッティの類は手許に無かったので、某ファミレスのオンラインメニューを映してみました。カメラが遠いと、表示がラーメンとスパゲッティの間で激しくブレたりしますが、このぐらい近づけるとスパゲッティで安定しました。
f:id:fleron:20200408231854p:plain
 
アプリが判断に迷いそうなネタとして、スープスパゲッティも試してみました(写真はcookpadより)。色が濃い目のスープたっぷりであり、カメラが少し動くと表示が激しくブレたりしますが、カメラをあまり動かないようにすると、スパゲッティで安定しました(これは意外でした)。
f:id:fleron:20200408231907p:plain
 
というわけで、無事にラーメンとスパゲッティを見分けるアプリが完成しました(実際のところ、蕎麦やうどんなど他の麺類もラーメンかスパゲッティ認定されるなど、改良の余地は多々ありますが…)。

PyTorchやTensorFlowなどでモデルや学習のコードを自力実装する手間と比べると、恐ろしい程お手軽に機械学習を活用したアプリが作れてしまいました。Create MLでは学習の中身はブラックボックスであり(おそらく転移学習の一種?)、現時点ではやれることの種類も限られておりますが、それでも今後の開発の幅を広げてくれそうなツールだと感じます。