📝

(Swift)iPhoneでCreate MLを用いて独自の画像分類モデルの訓練と推論を行う

2024/07/29に公開

はじめに

iPhoneでCreate MLのMLImageClassifierを用いて画像分類モデルの訓練と推論行うサンプルアプリを作成しました。iOS 15以上のデバイスで動作します。

https://github.com/moutend/TrainingMLImageClassifier

モデルの訓練から推論までをiPhone上で完結させる例はあまり見かけません。参考になれば幸いです。

不具合と思われる事象

さて記事の本題に進みたいところですが、不具合と思われる事象に遭遇したので共有します。実装の要点だけ知りたい場合は読み飛ばしてください。

1. 訓練中のエラー「Must allow 2048-element vector as output」

iOS 17が搭載されたデバイスでモデルを訓練すると、Xcodeのコンソールに以下のエラーが表示されて訓練が中断します。

MLModelAsset: load failed with error Error Domain=com.apple.CoreML Code=0 "Must allow 2048-element vector as output" UserInfo={NSLocalizedDescription=Must allow 2048-element vector as output}

このエラーはMLImageClassifier.FeatureExtractorType.scenePrint(revision: nil)のように、リビジョン番号にnilを指定すると発生します。

Apple Developerドキュメントによると、リビジョン番号としてnilが指定された場合は最新の番号が指定されたものとして扱われるそうです。

The sceneprint version. The supported versions include 1 and 2. If nil defaults to the latest version.

引用元 https://developer.apple.com/documentation/createml/mlimageclassifier/featureextractortype/sceneprint(revision:)

しかし、iOS 17でnilを指定すると上記のエラーが発生します。このエラーを回避するにはリビジョン番号として12を指定してください。

なお、iOS 15の場合はリビジョン番号にnilを指定してもエラーは発生しません。詳細については実装の要点で改めて説明します。

2. 訓練が極端に遅い

不具合に含めるか微妙なところですが、iOS 17でモデルの訓練にかかる時間が遅くなる事象を確認しています。サンプルアプリを例にすると以下のような結果になりました。

  • iOS 15 / iPhone 13→9544 ms
  • iOS 17 / iPhone 15 Pro→16071 ms

詳細は実装の要点で説明しますが、上記の結果はscenePrintのリビジョン番号を1に揃えた場合の結果です。測定にあたり他のアプリを閉じてバッテリー残量を100 %に揃えるなど、可能な限り計測する際の環境が同じになるよう配慮しています。

Appleのドキュメントによると、iPhone 13に搭載されているA15 Bionicチップの性能は15.8 TFLOPSです。iPhone 15 Proに搭載されているA17 ProチップのFLOPS性能は公表されていませんが、A15世代と比較して2倍の性能だそうです。

従って、iPhone 15 Proの結果は訓練にかかる時間が長すぎると判断できます。ハードウェアではなくiOSに原因がありそうです。Create MLフレームワークの内部的なパラメータが変更されて訓練が遅くなっているのかもしれません。

詳細な原因の切り分けができていませんがひとまず事象の共有でした。もし何かご存知の方がいましたら、コメントなどで教えていただけると幸いです。

この記事について

Create MLのMLImageClassifierの使い方についてはサンプルアプリをご確認ください。Logic/ImageTrainer.swiftが訓練、Logic/ImageClassifier.swiftが推論の実装になります。

この記事では実装の要点や開発中に生じる疑問についてお答えします。Create MLが専門的な知識なしで手軽に扱えるフレームワークですから、記事を読み進めるための前提知識は不要です。

ただし、機械学習の入門を済ませておくと記事を早く読み進められるかと思います。例えば書籍「ゼロから作るDeep Learning」がおすすめです。隅から隅まで読み込む必要はなく、概要を把握するだけでも十分です。

訓練から推論までの流れ

それではCreate MLを利用して画像分類器を作成・利用する流れを説明します。手順は以下の3ステップです。

  1. 訓練データを用意します。具体的にはFileManagerで任意の場所にディレクトリを作成し、その中に画像ファイルを保存します。ディレクトリの名前が分類ラベルの名前になります。
  2. 諸々のパラメータを設定して訓練を開始します。訓練が完了すると.mlmodelファイルが作成されます。
  3. 作成した.mlmodelファイルをコンパイルします。その後、コンパイルされたモデルを読み込むと推論が可能な状態になります。

Create MLのお手軽さが伝わったでしょうか。ただし手軽だからといって、使いこなすのが簡単とは限りません。その点を補足していきます。

実装の要点

ここからは実装の要点を説明します。項目は以下の5つです。

  1. 実質的にロジスティック回帰しか選択肢がない
  2. ModelParametersのデフォルト値に注意
  3. MLTrainingSessionParametersのデフォルト値に注意
  4. MLJobのcheckpointsプロパティで訓練の進捗を取得するにはMLImageClassifierのtrain()にsessionParameters引数を指定する必要がある。
  5. MLJobのresultプロパティで訓練が完了した際の結果を受け取るにはAnyCancellableインスタンスを保持しておく必要がある。

1. 実質的にロジスティック回帰しか選択肢がない

Apple Developerドキュメントから引用します。

enum MLImageClassifier.ModelParameters.ClassifierType

case logisticRegressor -- Logistic regression is a statistical model that classifies input feature vector into different categories.
case multilayerPerceptron(layerSizes: [Int]) -- Multilayer perceptron, layerSizes holds a list of positive integers that represent the number of hidden units in each layer. An additional fully connected layer with a Softmax activation output will be added that maps to probabilities of sound categories.

引用元 https://developer.apple.com/documentation/createml/mlimageclassifier/modelparameters-swift.struct/classifiertype

ロジスティック回帰は機械学習の手法の一種です。流行りのディープラーニング登場以前からある、技術的に枯れた手法です。

現状のCreate MLは2つの選択肢があり、1番目のlogisticRegressorがロジスティック回帰です。2番目のmultilayerPerceptronがニューラルネットワークです。しかし、2番目の説明を最後まで読むと「音声を分類するため...」と書かれています。

つまり、2番目の選択肢は画像処理でよく使われる畳み込みニューラルネットワークなどではなく、文字通りレイヤーを重ねただけの素朴なニューラルネットワークです。何にでも使えるので、とりあえず選択肢として含まれているようです。

(2ばん目の選択肢は名前がマルチレーやーパーセプトロンとなっていますが、活性化関数の違いでパーセプトロンやニューラルネットワークと呼び名が変わると理解しています。間違っていたらすみません。)

余談になりますが、書籍「ゼロから作るDeep Learning」では素朴な2層のニューラルネットワークを用いてMNIST画像の分類気を実装する例が登場します。それなりに機能することが確認できておもしろい実験です。

2. ModelParametersのデフォルト値に注意

MLImageClassifier.ModelParametersについての話題です。まずはinit()の定義を確認しましょう。

init(
  validation: MLImageClassifier.ModelParameters.ValidationData = __Defaults.validation,
  maxIterations: Int = __Defaults.maximumIterations,
  augmentation: MLImageClassifier.ImageAugmentationOptions,
  algorithm: MLImageClassifier.ModelParameters.ModelAlgorithmType = __Defaults.algorithm
)

引数のデフォルト値は次のとおりです。

  • validation: .split(strategy: .automatic)
  • maxIterations: 25
  • augmentation: [.crop, .blur]
  • algorithm: .transferLearning(featureExtractor: .scenePrint(revision: 1), classifier: .logisticRegressor)

それぞれの引数の役割は次のとおりです。

  • validation: 機械学習ではモデルの汎化能力を獲得するため、訓練データとテストデータを分割する必要があります。validationのデフォルト値は訓練データからテストデータを自動的に良い塩梅で分割することを指示します。通常はデフォルト値のままで問題ありません。
  • maxIterations: 訓練のイテレーションを繰り返す回数の最大値を意味します。25という数値には特に根拠がなく、おそらく仮の値として置かれているだけです。この値は各自で調整する必要があります。
  • augmentation: 訓練データの水増しを行うか指示します。画像のぼかしなどを指示できますが、どれくらいボケを加えるのかといった細かな指示はできません。
  • algorithm: モデルの訓練に使用するアルゴリズムを指定します。enumで選択肢が定義されていますが、今のところ利用できるのはこの選択肢のみです。あるいは自分で特徴量の抽出方法をカスタマイズすることもできます。

(補足)scenePrintのリビジョンについて

記事の冒頭でも説明しましたが、iOS 17でリビジョン番号としてnilを指定するとエラーが発生してモデルの訓練が失敗します。iOS 15では問題なく動作するため、おそらくドキュメントの文面は正しく、Create MLフレームワークの内部的な実装に不具合があるものと思われます。

リビジョン番号と利用可能なiOSのバージョンは次のとおりです。

  • Revision 1: iOS 15以降で利用可能
  • Revision 2: iOS 17以降で利用可能

iOSバージョンごとの挙動は次のとおりです。

  • iOS 15: 1またはnilを指定できる。2を指定するとエラーが発生します。
  • iOS 16: 手元に実機がないため未確認です。
  • iOS 17: 1または2を指定できる。nilを指定するとエラーが発生しますが、このエラーはおそらく不具合です。

なお、サポートされていないリビジョン番号を指定すると以下のようなエラーメッセージが表示されます。

(iOS 15でリビジョン番号として2を指定)
Revision 2 is not supported. Please try revision 1.

(補足)scenePrintの用途

Transfer Learning(転移学習)は、ある程度訓練したモデルに追加の訓練を行う手法です。少量の訓練データでそれなりに良い結果が得られるのはこの手法のおかげです。

Apple Developerドキュメントによると、scenePrintは実写の画像データで事前に訓練されているそうです。手書き文字認識などのタスクには不向きだそうなので、用途によっては別のフレームワークを選択してください。

The scene print feature extractor works best with images of real world objects because it trained on millions of such images. Scene print is not suitable for character recognition, because the input images are highly binary in nature (pixels are either on or off).

引用元 https://developer.apple.com/documentation/createml/mlimageclassifier/featureextractortype/sceneprint(revision:)

(補足)scenePrintのリビジョンによって性能差はあるか?

前提として機械学習モデルの性能を評価するのは簡単ではありません。その点を踏まえて、あくまでサンプルアプリで試した範囲ですが、リビジョンによる明らかな性能の差はなさそうでした。

実際、Apple Developerドキュメントにはリビジョン1と2を比較してどのような違いがあり、パフォーマンスにどのような影響が生じるのか説明がありません。とはいえ新しい方が何らか改善されているはずですので、各自で実験してみてください。

3. MLTrainingSessionParametersのデフォルト値に注意

MLImageClassifierのクラスメソッドtrain()についての話題です。シグネチャは次の通りです。

static func train(
  trainingData: MLImageClassifier.DataSource,
  parameters: MLImageClassifier.ModelParameters = ModelParameters(
      validation: .split(strategy: .automatic),
      augmentation: [],
      algorithm: .transferLearning(
        featureExtractor: .scenePrint(revision: 1),
        classifier: .logisticRegressor
      )
    ),
  sessionParameters: MLTrainingSessionParameters = _defaultSessionParameters
) throws -> MLJob<MLImageClassifier>

ここで最後の引数sessionParametersに注目してください。デフォルト値が設定されていますが、具体的にどのような値なのかApple Developerのドキュメントに説明がありません。

続いてMLTrainingSessionParametersinit()がどのように定義されているか確認しましょう。

init(
  sessionDirectory: URL? = nil,
  reportInterval: Int = 5,
  checkpointInterval: Int = 10,
  iterations: Int = 1000
)

さて、_defaultSessionParametersとMLTrainingSessionParametersのinit()に定義されたデフォルト値は同じなのでしょうか?

答えはNOです。両者のデフォルト値は異なります。

どのようなアプリを実装するかに依存しますが、おそらくinit()が返すデフォルト値は通常のユースケースでは頻度が細かすぎる上にイテレーションの回数が多すぎます。特に理由がなければtrain()sessionParametersは省略して問題ありません。

4. MLJobのcheckpointsプロパティで訓練の進捗を取得するにはMLImageClassifierのtrain()メソッドにsessionParameters引数を指定する必要がある

MLImageClassifierのtrain()で訓練を開始した場合の話題です。sessionParametersは省略するのがおすすめと説明しましたが、省略した場合は訓練の進捗が取得できません。訓練に相当な時間が必要になると見込まれる場合はsessionParametersを設定して、MLJobのcheckpointsで進捗を受け取りましょう。

なおsessionParametersでセッションを保存するように指示する場合、訓練途中に中断するケースの考慮が必要になります。具体的にはMLTrainingSessionParametersのsessionDirectoryで指示したディレクトリにセッションが記録されたファイルが残ります。

この状態で再度MLImageClassifierのtrain()を実行するとエラーが発生します。途中からではなく訓練を最初から再実行したい場合はセッションが記録されたファイルを削除する必要があります。

5. MLJobのresultプロパティで訓練が完了した際の結果を受け取るにはAnyCancellableインスタンスを保持しておく必要がある

Create MLとは関係のない話題ですが、AnyPublisher型のプロパティから値を取得する場合の話です。PublisherはCombineフレームワークが提供するバインディング機構です。詳細については以下のドキュメントを参照してください。

なお、MLJobの訓練を中断したい場合はMLJobのcancel()メソッドを実行してください。

実装例

// Good: 動作する
self.resultCancellable = self.job?.result.sink(
  receiveCompletion: { [weak self] completion in
    // ...
  },
  receiveValue: { [weak self] classifier in
    // ...
  }
)

// Bad: 動作しない
self.job?.result.sink(
  receiveCompletion: { [weak self] completion in
    // ...
  },
  receiveValue: { [weak self] classifier in
    // ...
  }
)

よくある質問と回答

ここからは開発中に生じる疑問についてお答えします。

1. 訓練データの解像度に制限はあるか?

解像度に制限はありません。ただし、訓練データの画像は縦横299 x 299ピクセルの正方形に拡大または縮小された後にモデルへ入力されます。また、ドキュメントには書かれていませんがMLModelのインスタンスをprint()で表示すると訓練に使用された入力データの形式が確認できます。

When you train an image classifier using scene print, or make predictions with the resulting model, use images with 299x299 pixels or more. The model upscales smaller images, to 299x299 before it feeds them to the feature extractor, which may result in poor accuracy.

引用元 https://developer.apple.com/documentation/createml/mlimageclassifier/featureextractortype/sceneprint(revision:)

画像の内容によっては拡大縮小の前後でモアレやジャギーが発生して見た目が変化する恐れがあります。Create MLが内部でどのように画像をリサイズするのかは非公表のため、予期しない結果を防ぐには事前に画像の解像度を299 x 299に揃えてから訓練を開始することをおすすめします。

2. 長方形の画像はどのように扱われるか?

例えばiPhoneで撮影した縦方向に長い画像の場合、リサイズ方法は以下のパターンが考えられます。

  1. 上下を切り取って正方形に加工した後に目的の解像度299 x 299へ縮小する。
  2. 切り取りを行わず目的の解像度299 x 299へ縮小する。結果、上下を押しつぶしたような形の画像になる。

Create MLフレームワークがどのように画像を加工するのかは明言されていません。その代わり、以下のような説明があります。

For example, if your app classifies images captured with an iPhone camera, train your model using images captured in the same way, if possible.

引用元 https://developer.apple.com/documentation/createml/mlimageclassifier/featureextractortype/sceneprint(revision:)

ここで注目する点は、可能ならば同じ手段で用意した画像を使うようにと説明されている点です。

例えば、仮に上下が押し潰されて変形した画像で訓練したとしても、すべての訓練データの画像の解像度が一致していれば問題はありません。機械学習のモデルは変形前の画像がどのようなものか知らないため、最初からそのような画像として訓練が進むためです。

問題が発生するのは訓練データの解像度がバラバラの場合です。訓練データの中に縦長の画像と横長の画像が混ざっている場合、上下を押しつぶされた画像や左右を押しつぶされた画像で訓練が進む恐れがあります。

例えばボールの種類を判別するモデルがあるとして、画像が意図せず押しつぶされてバスケットボールがラグビーボールとして識別されてしまったら困るはずです。訓練前に解像度を揃える作業は必要に応じて我々の責任で行う必要があります。

3. 訓練データとして使用できる画像ファイルの形式は?

以下の形式が利用可能です。

  • HEIF (.heic)
  • JPEG (.jpg)
  • PNG (.png)
  • TIFF (.tiff)

Core ImageフレームワークのCIContextで書き出せる形式の画像ファイルであればすべて対応しているようです。

4. 訓練データに異なる画像ファイルの形式を混ぜて問題ないか?

問題ありません。ただし、例えば訓練データの中に低画質なJPEG画像と高画質なPNG画像を混ぜて訓練した場合、JPEG画像のノイズが特徴量として学習されるかもしれません。この場合であれば、PNG画像を事前にJPEG画像に圧縮して、訓練データの品質のばらつきを抑えるなどの工夫が必要になるかもしれません。

5. 訓練用の画像を作成中に「fopen failed for data file: errno = 2 (No such file or directory)」が表示される

Create MLとは関係ありませんが、遭遇する可能性が高いので説明します。訓練用の画像ファイルを大量に作成すると、以下のエラーが表示されるはずです。ネットからダウンロードしたりカメラで撮影したり、手段は関係ありません。

(日付などのプレフィックスは省略しています)

fopen failed for data file: errno = 2 (No such file or directory)
Errors found! Invalidating cache...

エラーが発生しているように見えますが、エラーではありません。紛らわしいのですが、無視して問題ありません。

iOSはアプリごとにサンドボックス環境が作成されます。そのファイルシステムではキャッシュ管理にSQLLite(ファイルベースのRDB)が使用されています。

キャッシュ管理はある程度ファイルの読み書きをしなければ発動しません。そのためアプリのインストール直後はSQLLiteファイルが存在せず、ある程度画像ファイルが作成された段階で上記のエラーメッセージが表示されます。

6. 何も設定していないのに訓練の進捗状況がXcodeのコンソールに表示される

Create MLのお節介な機能です。iOS 15とXcode 14.2の組み合わせであれば以下のようなメッセージが出力されます。無効化はできません。

...
Processing 10 files
Processing 10 files
Processing 10 files
Processing 8 files
Extracting image features from training data.
Extracting image features from user validation data.
Beginning model training on processed features. 
Calibrating solver; this may take some time.
+-----------+--------------+-------------------+---------------------+
| Iteration | Elapsed Time | Training Accuracy | Validation Accuracy |
+-----------+--------------+-------------------+---------------------+
| 0         | 1.113087     | 0.932418          | 0.938596            |
| 1         | 1.303130     | 0.961985          | 0.912281            |
| 2         | 1.600825     | 1.000000          | 1.000000            |
| 3         | 1.682178     | 1.000000          | 1.000000            |
| 4         | 1.768306     | 1.000000          | 1.000000            |
| 9         | 2.360121     | 1.000000          | 1.000000            |
| 10        | 2.443858     | 1.000000          | 1.000000            |
+-----------+--------------+-------------------+---------------------+
Trained model successfully saved at /var/mobile/Containers/Data/Application/87BA36EA-275F-4396-8361-8963FF4A67EE/Documents/MLModel/NewsModelV1.mlmodel.

iOS 17とXcode 15の組み合わせでは上記のような進捗状況のメッセージは表示されません。また、iOS 16以降であればXcode InstrumentsでCore MLのパフォーマンスをモニタリングできます。Command+IでXcode Instrumentsを開き、Core MLを選択してください。

7. scenePrintのリビジョンによって作成されるモデルに違いはあるか?

モデルの性能ではなく作成される.mlmodelファイルについての話題です。以下2点の違いがありました。なお比較には記事の冒頭で紹介したサンプルアプリを使用しています。

1点目は生成される.mlmodelファイルのサイズです。約1 / 3になります。

  • リビジョン1: 48 KB
  • リビジョン2: 18 KB

2点目は訓練にかかる時間です。リビジョン2はリビジョン1と比較して、およそ1.2倍の時間を要します。

  • リビジョン1
    • iOS 17 / iPhone SE 第2世代→26215 ms
    • iOS 17 / iPhone 15 Pro→16071 ms
  • リビジョン2
    • iOS 17 / iPhone SE 第2世代→33320 ms
    • iOS 17 / iPhone 15 Pro→20247 ms

8. 訓練にかかる時間を短くするには?

汎用的に使える訓練時間を短くする手法はありませんが、意図せず訓練時間が長くなることを防ぐ方法はあります。それには熱対策が最も有効です。

連続してモデルの訓練を行うとデバイスが急激に発熱します。するとサーマルスロットリングが発動するらしく、1買い目の訓練と比較して訓練にかかる時間が1.2倍ほど伸びます。

訓練の時間を短く保つには、訓練の間隔を空けて熱が下がるのを待つのが効果的です。具体的にどれくらいの間隔が効果的かは実装するアプリごとに異なりますので、各自で計測してみてください。

9. mlmodelファイルのコンパイルにかかる時間は?

次のような結果になりました。記事冒頭のサンプルアプリを使用して何度かコンパイルを繰り返した結果の中央値です。

  • iOS 15 / iPhone 13→30 ms
  • iOS 17 / iPhone 15 Pro→58 ms
  • iOS 17 / iPhone SE 第2世代→101 ms

scenePrintのリビジョンによる差はなさそうでした。ただし、やはりiOS 17は訓練と同様にコンパイルも遅くなる傾向があるようです。

10. mlmodelファイルをコンパイルした結果はどこに保存される?

場所はFileManager.default.temporaryDirectoryが返すディレクトリです。作成先のディレクトリはiOS 15とiOS 17で違いがなかったので、今後iOSの新しいバージョンが登場しても変化しないものと思われます。

なおコンパイルするとモデルの名前.mlmodelcが作成されます。.mlmodelcという拡張子がついていますが、実態はファイルではなくディレクトリです。

11. mlmodelcのサイズは?

mlmodelcディレクトリの中にはバイナリファイルがいくつか格納されています。記事冒頭で紹介したサンプルアプリの場合、ディレクトリに含まれるファイルサイズの合計は64 KBでした。iOS 15とiOS 17で違いはありませんでした。

12. mlmodelcを使いまわして問題ないか?

問題ありません。一度コンパイルすれば、mlmodelcディレクトリを指定することでコンパイルの手順をスキップできます。

ただし、記事の冒頭で報告したscenePrintの件のように、ドキュメントに書かれている内容と実態が異なり不具合が生じるかもしれません。データの互換性は今後も維持されるはずですが、アプリケーションの要件が許すのであればコンパイル完了後も.mlmodelファイルは残した方が無難かと思います。

13. 推論にかかる時間は?

記事冒頭のサンプルアプリの場合、以下のような結果になりました。しばらく推論を続けた場合の中央値です。

  • リビジョン1:
    • iOS 15 / iPhone 13→12 ms
    • iOS 17 / iPhone 15 Pro→12 ms
    • iOS 17 / iPhone SE 第2世代→17 ms
  • リビジョン2:
    • iOS 17 / iPhone 15 Pro→18 ms
    • iOS 17 / iPhone SE 第2世代→21 ms

興味深い点はiPhone SE 第2世代のパフォーマンスの高さです。2020年に発売された廉価版機種のiPhone SE 第2世代と2023年に発売された最上位機種のiPhone 15 Proにほとんど性能の差がありません。ただし、この結果はあくまでサンプルアプリのモデルに限った話ですので、モデルの内容次第で変化する可能性があります。

おわりに

内容の間違いや誤字脱字などありましたら指摘いただけると助かります。

(余談)ChatGPT、Claude、Perplexity AIなどの生成系AIにCreate MLの話題を訪ねると全員が間違った回答を返す

Create MLのドキュメントやサンプル実装が不足しているのが原因でしょうか、いわゆる生成系AIにCreate MLの質問をすると、ほぼ確実に間違った回答を返します。特にMLImageClassifierの使い方などを尋ねると百発百中で嘘の回答を返します。

特に面白いのがPerplexity AIです。普段は理路整然と参考資料へのリンクを提示しつつ回答してくれるのですが、恭しい態度の挨拶を返しつつApple Developerのドキュメントを完全に無視したSwiftコードを提案してくれます。参考資料へのリンクを提示しない回答が出力されるパターンは初めて遭遇しました。

例えばMLImageClassifierにはクラスメソッドtrain()が定義されていますが、なぜかインスタンスメソッドのtrain()が定義されているかのようなコードを提案してくれます。AIの種類に関係なく、全員が同じような嘘のコードを提案してくれます。昔のドキュメントが見つけられないのですが、Create MLがベータ版だった頃の資料を学習していたりするのでしょうか。

参考資料

  1. Create ML - Apple Developer Documentation
  2. Creating an Image Classifier Model - Apple Developer Documentation
  3. MLJob - Apple Developer Documentation
  4. MLImageClassifier - Apple Developer Documentation
  5. MLImageClassifier.ModelParameters - Apple Developer Documentation
  6. MLImageClassifier.FeatureExtractorType - Apple Developer Documentation
  7. MLImageClassifier.ModelParameters.ModelAlgorithmType - Apple Developer Documentation

Discussion