🗂

(Swift)機械学習を利用して音声を識別する

2023/08/27に公開

はじめに

この記事は機械学習の初心者に向けた記事です。Appleの機械学習フレームワークCore MLを利用して音声を識別する手順を説明します。例えば人の声が記録されていたら「speech」、音楽が記録されていたら「music」が識別結果になります。

記事を読み進める上で機械学習の知識は不要です。訓練済みモデルを利用するだけならば機械学習をアプリケーションに組み込むのは簡単だと実感してもらうのが記事の目的になります。

「おいおい待ってくれ、モデルって何だ?」

そう思いましたね?オライリー・ジャパンから発売中の書籍「ディープラーニング実践ガイド」から引用しましょう。

簡単に言ってしまうなら、モデルとは関数です。1つまたは複数の入力を受け取り、出力を1つ返します。

入力されるデータは、テキストや画像、音声、動画などさまざまです。出力されるのは何らかの予測の結果です。

1章 人工知能の概観より引用

どうです、簡単でしょう?訓練済みモデルとは予測ができる準備が整ったモデルという意味です。

Appleは訓練済みモデルを提供しているので、それを利用します。謎めいた数式が登場することは一切ありませんので、ご安心ください。

環境

記事の投稿にあたり、以下の環境で動作確認しました。

  • Apple Swift 5.7
  • macOS Monterey 12.6.8
  • MacBook Pro 2021 M1PRO

実装

まずは以下のコードをmain.swiftとして保存しましょう。このコードが何をしているのかは後ほど解説します。

import SoundAnalysis

class ResultsObserver: NSObject, SNResultsObserving {
  var results: [SNClassificationResult] = []

  func request(_ request: SNRequest, didProduce result: SNResult) {
    guard let result = result as? SNClassificationResult else {
      return
    }

    self.results.append(result)
  }
  func request(_ request: SNRequest, didFailWithError error: Error) {
    print("The analysis failed: \(error.localizedDescription)")
  }
  func requestDidComplete(_ request: SNRequest) {
    print("The request completed successfully!")
  }
}

func run() {
  if CommandLine.arguments.count < 2 {
    return
  }

  let audioFileURL = URL(fileURLWithPath: CommandLine.arguments[1])
  let resultsObserver = ResultsObserver()

  do {
    let classifySoundRequest = try SNClassifySoundRequest(classifierIdentifier: .version1)

    classifySoundRequest.windowDuration = CMTimeMake(value: 2, timescale: 1)
    classifySoundRequest.overlapFactor = 0.5

    let audioFileAnalyzer = try SNAudioFileAnalyzer(url: audioFileURL)

    try audioFileAnalyzer.add(classifySoundRequest, withObserver: resultsObserver)

    audioFileAnalyzer.analyze()
  } catch let error {
    print(error)
  }
  if resultsObserver.results.isEmpty {
    print("Oops! The input file might be too short.")
  }

  print("\nTime\tIdentifier\tConfidence")

  for result in resultsObserver.results {
    guard let classification = result.classifications.first else {
      continue
    }

    let timeInSeconds = String(format: "%.2f", result.timeRange.start.seconds)
    let percent = String(format: "%.1f%%", classification.confidence * 100.0)

    print("\(timeInSeconds)\t\(classification.identifier)\t\(percent)")
  }
}

run()

動作確認

早速コードを実行してみましょう。ターミナルを開いて以下のコマンドを入力します。

$ swift main.swift <音声ファイルのパス>

音声ファイルの形式はafplayコマンドで再生できるものであれば、何でも構いません。.wav.mp3などが利用できます。

また、音声ファイルはモノラルとステレオどちらでも構いません。サンプリング周波数と量子化ビット数についても制限はありません。

後ほどコードの解説をしますが、今回は推論を1秒間隔で実行します。推論に利用する音声バッファの長さは2秒としました。

つまり、0秒目から2秒目、1秒目から3秒目、2秒目から4秒目、...といった区間ごとに推論を行います。もちろん、推論の実行間隔や音声バッファの長さは自由に変更できます。

参考までに、いくつか実行例を示します。

(実行例その1)話し声

まずは人の話し声が正しく識別されるか試します。

音声ファイルの準備

ボイスメモアプリを開いて録音しても構わないのですが、あえて意地悪してみましょう。音声合成した声の識別です。

macOSには音声合成コマンドsayが搭載されています。合成された音声ファイルがどのように識別されるか試します。以下のコマンドを実行してください。

$ say -o voice.aiff -v Kyoko 'こんにちは、私の名前はKyokoです。日本語の音声をお届けします。'

-oフラグで出力ファイル、-v`フラグで話者名を指定しています。最後の引数には読み上げる文章を指定します。

上記のコマンドが失敗する場合、以下のコマンドを実行してください。利用可能な話者名の一覧が表示されます。

$ say -v '?'

以下のように「話者名 言語 #サンプル文章」の形式で一覧が表示されます。

Alex                en_US    # Most people recognize me by my voice.
Alice               it_IT    # Salve, mi chiamo Alice e sono una voce italiana.
Alva                sv_SE    # Hej, jag heter Alva. Jag är en svensk röst.
...
Kyoko               ja_JP    # こんにちは、私の名前はKyokoです。日本語の音声をお届けします。
...
Otoya               ja_JP    # こんにちは、私の名前はOtoyaです。日本語の音声をお届けします。
...

音声ファイルの生成コマンドはあくまで一例です。話者の指定は自由ですので、必ずしもKyokoを指定する必要はありません。

結果

以下のコマンドを実行します。

$ swift main.swift voice.aiff

次のような結果が得られます。

The request completed successfully!

Time	Identifier	Confidence
0.00	speech	95.1%
1.00	speech	95.2%
2.00	speech	94.3%
3.00	speech	91.2%

結果は3行目から表示されます。

  • 1列目: 推論を実行した再生位置(秒)
  • 2列目: 推論された音声の種類
  • 3列目: 結果の確信度

素晴らしい結果です。正しくspeech(話し声)として識別されています。確信度も90 %をこえています。

(実行例その2)ピンクノイズ

次はピンクノイズがどのように識別されるか試します。ピンクノイズは「ゴー」という音色のノイズです。激しい雨や滝の流れに似た音色のノイズです。

音声ファイルの準備

音声ファイルの作成にはsoxコマンドが便利です。Homebrewでインストールできます。

$ brew install sox

以下のコマンドを実行するとピンクノイズが10秒間記録された音声ファイルが作成されます。フォーマットは44.1 kHz / 2 ch / 16 bit signed-integer PCMとしました。

$ sox -n -r 44100 -c 2 -b 16 noise.wav synth pinknoise trim 0 10

結果

以下のコマンドを実行します。

$ swift main.swift noise.wav

次のような結果が得られます。

The request completed successfully!

Time	Identifier	Confidence
0.00	music	15.7%
1.00	waterfall	23.4%
2.00	breathing	13.0%
3.00	mechanical_fan	12.6%
4.00	mechanical_fan	10.5%
5.00	waterfall	13.8%
6.00	water	28.2%
7.00	waterfall	15.5%
8.00	breathing	14.5%

結果は3行目から表示されます。

  • 1列目: 推論を実行した再生位置(秒)
  • 2列目: 推論された音声の種類
  • 3列目: 結果の確信度

すべての確信度が30 %未満です。識別された音声の種類も一定ではありません。

  • music(音楽) ... 15.7%
  • waterfall(滝) ... 23.4%
  • breathing(呼吸) ... 13.0%
  • mechanical_fan(送風機) ... 12.6%
  • mechanical_fan(送風機) ... 10.5%
  • waterfall(滝) ... 13.8%
  • water(水) ... 28.2%
  • waterfall(滝) ... 15.5%
  • breathing(呼吸) ... 14.5%

とはいえ、完全に的外れでもありません。たしかに「ゴー」という音色は送風機の音や呼吸するときの音と似ています。

実は、今回利用した訓練済みモデルに音声の種類として「pink_noise」は含まれていません。その前提を踏まえると、妥当な結果と言えるかもしれません。

コードの解説

main.swiftの実装を解説します。処理の大まかな流れは以下になります。

  1. 分類機のモデルを選ぶ
  2. 推論の実行間隔を設定する
  3. 推論する

1. 分類機のモデルを選ぶ

まずはモデルの選択です。30行目のSNClassifySoundRequest(classifierIdentifier: .version1)が該当します。

将来的にAppleがモデルを追加するかもしれませんが、今のところ音声の分類気として訓練されたモデルは.version1の1種類のみです。

もちろん、独自に訓練したモデルを利用できます。Appleの機械学習フレームワークはは.mlmodel形式の訓練済みモデルを読み込むことができます。また、Tensorflowで学習したモデルを.mlmodelに変換したり、XcodeのCreate MLアプリで直接.mlmodelを作成したりできます。

モデルの訓練についてはMacBookやiPhone上で行うことができます。ただし、完全に新規のモデルではなく、訓練済みモデルを利用した連合学習を行うことになります。

2. 推論の実行間隔を設定する

main.swiftの32・33行目が該当します。

classifySoundRequest.windowDuration = CMTimeMake(value: 2, timescale: 1)
classifySoundRequest.overlapFactor = 0.5

上記の例では音声バッファの長さ(windowDuration)を2秒に指定しています。サンプリング周波数が44.1 kHzであれば88200サンプルが分析の対象になります。

次の行はオーバーラップの割合を指定しています。windowDurationというプロパティ名が表しているとおり、窓関数によって分析対象のバッファは両端が少し削られます。

つまり、バッファの長さとして2秒を指定しても実際に分析されるのは0.1秒目から1.9秒目までかもしれません。そのような分析の取りこぼしを防ぐため、オーバーラップの割合を指定します。

上記の例については、overlapFactor = 0.5ですから50 %のオーバーラップを意味します。2秒の50 %ですから1秒が推論の実行間隔になります。

なお、windowDurationの値が尊重されるとは限らない点に注意してください。例えばバッファの長さを2秒から200ミリ秒に変更したとします。推論の実行間隔は100ミリ秒になることを期待しますが、私の環境(M1PROを搭載したMacBook Pro 2021年モデル)の場合、500ミリ秒の感覚で推論が実行されました。

Apple Developerのドキュメントには言及がありませんが、ハードウェアの世代によって推論の実行間隔が上下するのかもしれません。アプリケーションに機械学習を組み込む際は可能な限り対象デバイスの実機テストをお勧めします。

さらに余談になりますが、overlapFactorの範囲は0.0以上1.0未満です。公式ドキュメントには「supports values in the range [0.0, 1.0]」という曖昧な表現がされていますが、1.0を指定するとランタイムエラーが発生します。

ところで、overlapFactorとして0.99999のような1.0に近い値を指定すると高頻度で推論が実行されることになります。私の環境で試したところ、プログラムがハングしてしばらく応答なしになりました。特別な理由がない限り、overlapFactorは0.5で固定するのがお勧めです。

3. 推論する

main.swiftの39行目が該当します。SNAudioFileAnalyzerのanalyzeインスタンスメソッドを呼び出すと推論が開始されます。

コードをシンプルにするため実装例では同期的に実行しましたが、analyzeには非同期のメソッドも定義されています。詳細についてはApple DeveloperのSNAudioFileAnalyzerのドキュメントを参照してください。

識別できる音声の種類について

おっと失礼、どのような音声の種類を識別できるのか説明していませんでした。

以下のコードを実行すると識別子の一覧を表示できます。list.swiftなど適当な名前で保存してください。

import SoundAnalysis

do {
  print(try SNClassifySoundRequest(classifierIdentifier: .version1).knownClassifications)
} catch {
  print(error)
}

以下のように実行します。読みやすくするためjqで整形してsortしています。

$ swift list.swift | jq -r '.[]' | sort

以下のような結果が得られます。

accordion
acoustic_guitar
air_conditioner
air_horn
aircraft
...

私の環境では303種類の識別子が表示されました。楽器から生活音まで幅広く識別できることがわかります。

とはいえ、アプリケーションによっては分類が大雑把すぎて使いづらいかもしれません。その場合はCreate ML Componentを利用して独自のモデルを訓練し、それを利用して推論します。記事の種子から外れるので説明はしませんが、独自モデルの訓練方法については別記事で解説予定です。

ANE(Apple Neural Engine)について

Apple製品にはANE(Apple Neural Engine)とよばれる機械学習に特化したハードウェアアクセラレータが搭載されています。Core MLを利用する際はANEの性能を踏まえた検討が必要になります。

iOSデバイスについてはA11プロセッサ(iPhone X)から、MacデバイスについてはM1プロセッサ(2020年に発売されたARMアーキテクチャのモデル)からANEが搭載されました。ANEが搭載されたことで機械学習が実用に耐えるようになりました。

ただし、専用ハードウェアアクセラレータとはいえNVIDIA製のGPUと比較すると性能は圧倒的に劣ります。消費電力など条件が異なるので公平な比較にはなりませんが、純粋な性能についてはFLOPSで比較できます。

例えば直近であれば、2022年に登場したNVIDIAのRTX 4090は82 TFLOPSです。一方、2023年に登場したAppleのM2 Ultraは31 TFLOPSです。

参考

ANEのモニタリング

Appleが注力しているANEですが、残念なことにパフォーマンスをモニタリングする手段が提供されていません。

唯一、消費電力だけは取得できます。macOS標準コマンドのpowermetricsを実行すると消費電力がmW単位で表示されます。

$ sudo powermetrics -n 1 -i 1 | grep ANE 
ANE Power: 0 mW

しかし、今回の音声ファイル識別のような瞬発的なタスクについては消費電力がほぼ0で変化ありません。ANEが効果的に働いているかの指標としては役に立ちません。

世代別iPhoneのFLOPS比較

参考までに、iPhoneに搭載されたANEのFLOPS性能を示します。以下はApple Machine Learning Researchからの引用です。

機種名 チップの世代 FLOPS
iPhone 13 Pro A15 15.8 TFLOPS
iPhone 12 Pro A14 11.66 TFLOPS
iPhone 11 Pro A13 5.4 TFLOPS
iPhone XS A12 5.4 TFLOPS
\ iPhone X A11 0.6 TFLOPS

参考資料

  1. Classifying Sounds in an Audio File - Apple Developer
  2. Classifying Sounds in an Audio Stream - Apple Developer
  3. SNClassifySoundRequest - Apple Developer
  4. SNAudioFileAnalyzer - Apple Developer
  5. On-device APIs - Apple Developer
  6. Core ML - Apple Developer
  7. Deploying Transformers on the Apple Neural Engine - Apple Machine Learning Research

Discussion