ニューラル ネットワークは今日機械学習 (ML) の最前線にあり、ニューラル ネットワークを使用して解決するかどうかに関係なく、Python は間違いなくあらゆる ML タスクに最適なプログラミング言語です。 NumPy、Pandas、Keras、TensorFlow、PyTorch など、ML タスクの全範囲をカバーする膨大な数の Python ライブラリが利用可能です。これらのライブラリは通常、ML アルゴリズムの C または C++ 実装に依存しており、Python では遅すぎるため、内部でのアプローチが行われます。ただし、存在するプログラミング言語は Python だけではなく、私が日常の仕事で使用している言語でもありません。
この記事は、Swift で何かを書く方法に関するガイドではありません。むしろ、これは、使用している言語に関係なく、遭遇するあらゆる問題やタスクを解決する ML ライブラリの究極のソリューションへの架け橋として Python を考える多くの開発者の現在の考え方についての考察記事のようなものです。ほとんどの開発者は、Python ライブラリを使用しない代替ソリューションを検討するよりも、Python ライブラリを自分の言語/環境に統合する方法を見つけることに時間を投資することを好むと私は賭けます。これは本質的に悪いことではありませんが、再利用は過去数十年にわたる IT の進歩の重要な原動力であり、多くの開発者がもはや代替ソリューションを検討すらしていないように感じ始めています。この考え方は、大規模言語モデルの現状と進歩によってさらに定着しています。
バランスが欠けています。私たちは問題の解決を LLM に依頼し、Python コードを入手してコピーし、不必要な依存関係による潜在的に重大なオーバーヘッドを伴いながら生産性を享受しています。
Swift と数学のみを使用し、他のツールを使用せずに、目の前のタスクを解決するための代替アプローチを検討してみましょう。
ニューラル ネットワークの学習を始めるとき、ほとんどのチュートリアルや入門資料で見つけることができる 2 つの古典的な Hello World の例があります。 1 つ目は手書き数字の認識です。 2 つ目はデータの分類です。この記事では 2 番目の解決策に焦点を当てますが、これから説明する解決策は最初の解決策にも適用できます。
この非常に優れた視覚的な例は、TensorFlow Playground にあります。そこでは、さまざまなニューラル ネットワーク構造を試して、結果のモデルがタスクをどの程度うまく解決するかを視覚的に観察できます。
異なる色の画像上のこれらのドットの実際的な意味は何なのか疑問に思うかもしれません。重要なのは、これは一部のデータセットを視覚的に表現したものであるということです。特定の製品や音楽の好みを購入する人々のソーシャル グループなど、さまざまな種類のデータをまったく同じまたは類似した方法で提示できます。私は主にモバイル iOS の開発に焦点を当てているため、同様の方法で視覚的に表現できる、私が解決していた実際のタスクの例も示します。それは、携帯電話のジャイロスコープと磁力計を使用して壁内の電線を見つけることです。この特定の例では、見つかったワイヤに関連する一連のパラメータがあり、壁の内側には何も関係のない別のパラメータ セットがあります。
使用するデータを見てみましょう。
ここには、赤い点と青い点の 2 種類のデータがあります。上で説明したように、これはあらゆる種類の機密データの視覚的表現である可能性があります。たとえば、壁に電線がある場合は磁力計とジャイロスコープからの信号が届く領域を赤い領域、ない場合を青の領域とします。
これらの点が何らかの形でグループ化され、ある種の赤と青の形を形成していることがわかります。これらのドットは、次の画像からランダムな点を取得することによって生成されます。
モデルをトレーニングするためにランダムな点を取得し、トレーニングされたモデルをテストするために他のランダムな点を取得することにより、この画像をトレーニング プロセスのランダム モデルとして使用します。
元の画像は 300 x 300 ピクセルで、90,000 個のドット (点) が含まれています。トレーニング目的では、これらのドットのうち 0.2% (100 ポイント未満) のみを使用します。モデルのパフォーマンスをより深く理解するために、ランダムに 3000 個の点を選択し、画像上でその周りに円を描きます。この視覚的表現により、結果についてより包括的なアイデアが得られます。精度のパーセンテージを測定して、モデルの効率を検証することもできます。
模型はどうやって作るの?これら 2 つの画像を一緒に見てタスクを単純化しようとすると、実際のタスクは、私たちが持っているデータ (赤と青の点のバッチ) から原点の画像を再作成することであることがわかります。そして、モデルから得られる画像が元の画像に近づくほど、モデルの動作がより正確になります。また、テスト データを元の画像の極度に圧縮されたバージョンとみなして、それを解凍して戻すという目標を持つこともできます。
私たちがやろうとしていることは、ドットをコード内で配列またはベクトルとして表現される数学関数に変換することです (本文中でベクトル用語を使用するのは、それが数学の世界の関数とソフトウェア開発の配列の間にあるためです)。次に、これらのベクトルを使用してすべてのテスト ポイントにチャレンジし、どのベクトルに属するかを特定します。
データを変換するために、離散コサイン変換 (DCT) を試してみます。必要に応じて簡単に情報を見つけることができるので、それが何であるか、またどのように機能するかについては数学的な説明は行いません。ただし、それがどのように役立つのか、なぜ役立つのかを簡単な言葉で説明することはできます。 DCT は、画像圧縮 (JPEG 形式など) を含む多くの分野で使用されます。画像の重要な部分のみを保持し、重要でない詳細を削除することで、データをよりコンパクトな形式に変換します。赤い点のみを含む 300x300 の画像に DCT を適用すると、各行を個別に取得することで配列 (またはベクトル) に変換できる値の 300x300 行列が得られます。
最後にコードを書いてみましょう。まず、点 (ドット) を表す単純なオブジェクトを作成する必要があります。
enum Category { case red case blue case none } struct Point: Hashable { let x: Int let y: Int let category: Category }
none
という追加のカテゴリがあることに気づくかもしれません。実際には、最終的に 3 つのベクトルを作成します。1 つはred
点用、2 つ目はblue
点用、3 つ目はnone
で表されるその他のもの用です。それらを 2 つだけ使用することもできますが、赤と青以外のトレーニング済みベクトルを用意すると、作業が少し簡単になります。
テスト ベクトル内に同じ座標を持つ点が存在することを避けるために、 Set
を使用するために `Point` をHashable
プロトコルに準拠させます。
func randomPoints(from points: [Point], percentage: Double) -> [Point] { let count = Int(Double(points.count) * percentage) var result = Set<Point>() while result.count < count { let index = Int.random(in: 0 ..< points.count) result.insert(points[index]) } return Array<Point>(result) }
これを使用して、元の画像から赤、青、およびなしのポイントとして0.2%
ランダムなポイントを取得できます。
redTrainPoints = randomPoints(from: redPoints, percentage: 0.002) blueTrainPoints = randomPoints(from: bluePoints, percentage: 0.002) noneTrainPoints = randomPoints(from: nonePoints, percentage: 0.002)
DCT を使用してこれらのトレーニング データを変換する準備が整いました。以下にその実装を示します。
final class CosTransform { private var sqrtWidthFactorForZero: Double = 0 private var sqrtWidthFactorForNotZero: Double = 0 private var sqrtHeightFactorForZero: Double = 0 private var sqrtHeightFactorForNotZero: Double = 0 private let cosLimit: Int init(cosLimit: Int) { self.cosLimit = cosLimit } func discreteCosTransform(for points: [Point], width: Int, height: Int) -> [[Double]] { if sqrtWidthFactorForZero == 0 { prepareSupportData(width: width, height: height) } var result = Array(repeating: Array(repeating: Double(0), count: width), count: height) for y in 0..<height { for x in 0..<width { let cos = cosSum( points: points, width: width, height: height, x: x, y: y ) result[y][x] = cFactorHeight(index: y) * cFactorWidth(index: x) * cos } } return result } func shortArray(matrix: [[Double]]) -> [Double] { let height = matrix.count guard let width = matrix.first?.count else { return [] } var array: [Double] = [] for y in 0..<height { for x in 0..<width { if y + x <= cosLimit { array.append(matrix[y][x]) } } } return array } private func prepareSupportData(width: Int, height: Int) { sqrtWidthFactorForZero = Double(sqrt(1 / CGFloat(width))) sqrtWidthFactorForNotZero = Double(sqrt(2 / CGFloat(width))) sqrtHeightFactorForZero = Double(sqrt(1 / CGFloat(height))) sqrtHeightFactorForNotZero = Double(sqrt(2 / CGFloat(height))) } private func cFactorWidth(index: Int) -> Double { return index == 0 ? sqrtWidthFactorForZero : sqrtWidthFactorForNotZero } private func cFactorHeight(index: Int) -> Double { return index == 0 ? sqrtHeightFactorForZero : sqrtHeightFactorForNotZero } private func cosSum( points: [Point], width: Int, height: Int, x: Int, y: Int ) -> Double { var result: Double = 0 for point in points { result += cosItem(point.x, x, height) * cosItem(point.y, y, width) } return result } private func cosItem( _ firstParam: Int, _ secondParam: Int, _ lenght: Int ) -> Double { return cos((Double(2 * firstParam + 1) * Double(secondParam) * Double.pi) / Double(2 * lenght)) } }
CosTransform
オブジェクトのインスタンスを作成してテストしてみましょう。
let math = CosTransform(cosLimit: Int.max) ... redCosArray = cosFunction(points: redTrainPoints) blueCosArray = cosFunction(points: blueTrainPoints) noneCosArray = cosFunction(points: noneTrainPoints)
ここではいくつかの単純なヘルパー関数を使用します。
func cosFunction(points: [Point]) -> [Double] { return math.shortArray( matrix: math.discreteCosTransform( for: points, width: 300, height: 300 ) ) }
CosTransform
には shortArray 関数内で使用されるcosLimit
パラメータがあります。その目的については後で説明します。ここでは無視して、作成したベクトルredCosArray
、 blueCosArray
、およびnoneCosArray
に対して元の画像からの 3000 個のランダム ポイントの結果を確認しましょう。これを機能させるには、元の画像から取得した 1 つの点から別の DCT ベクトルを作成する必要があります。これは、 Red
、 Blue
、およびNone
cos Vector に対してすでに実行したのと同じ関数を使用して、まったく同じ方法で実行します。しかし、この新しいベクトルがどれに属するかをどうやって見つけられるのでしょうか?これには非常に単純な数学的アプローチがあります: Dot Product
。 2 つのベクトルを比較し、最も類似したペアを見つけるというタスクがあるため、内積はまさにこれを示します。 2 つの同一のベクトルに内積演算を適用すると、同じベクトルや異なる値を持つ他のベクトルに適用される他の内積結果よりも大きな正の値が得られます。そして、直交するベクトル (互いに共通点を持たないベクトル) にドット積を適用すると、結果として 0 が得られます。これを考慮すると、次のような簡単なアルゴリズムを考え出すことができます。
redCosArray
、次にblueCosArray
、そしてnoneCosArray
を使用して、このベクトルの内積を適用します。Red
、 Blue
、 None
を示します。
ここで欠けている機能は内積だけです。そのための簡単な関数を書いてみましょう。
func dotProduct(_ first: [Double], _ second: [Double]) -> Double { guard first.count == second.count else { return 0 } var result: Double = 0 for i in 0..<first.count { result += first[i] * second[i] } return result }
そして、アルゴリズムの実装は次のとおりです。
var count = 0 while count < 3000 { let index = Int.random(in: 0 ..< allPoints.count) let point = allPoints[index] count += 1 let testArray = math.shortArray( matrix: math.discreteCosTransform( for: [point], width: 300, height: 300 ) ) let redResult = dotProduct(redCosArray, testArray) let blueResult = dotProduct(blueCosArray, testArray) let noneResult = dotProduct(noneCosArray, testArray) var maxValue = redResult var result: Category = .red if blueResult > maxValue { maxValue = blueResult result = .blue } if noneResult > maxValue { maxValue = noneResult result = .none } fillPoints.append(Point(x: point.x, y: point.y, category: result)) }
ここで行う必要があるのは、 fillPoints
から画像を描画することだけです。使用したトレイン ポイント、トレイン データから作成した DCT ベクトル、および最終結果を見てみましょう。
うーん、ランダムノイズっぽいですね。しかし、ベクトルの視覚的表現を見てみましょう。そこにいくつかのスパイクが見られます。これはまさに、DCT 結果からほとんどのノイズに注目して除去する必要がある情報です。 DCT マトリックスの単純な視覚的表現を見ると、最も有用な情報 (画像の固有の特徴を説明する情報) が左上隅に集中していることがわかります。
ここで、一歩下がってshortArray
関数をもう一度確認してみましょう。ここでcosLimit
パラメータを使用するのは、DCT 行列の左上隅を取得し、ベクトルを一意にする最もアクティブなパラメータだけを使用するためです。
func shortArray(matrix: [[Double]]) -> [Double] { let height = matrix.count guard let width = matrix.first?.count else { return [] } var array: [Double] = [] for y in 0..<height { for x in 0..<width { if y + x <= cosLimit { array.append(matrix[y][x]) } } } return array }
異なるcosLimit
を使用してmath
オブジェクトを作成しましょう。
let math = CosTransform(cosLimit: 30)
ここでは、90,000 個の値をすべて使用する代わりに、DCT マトリックスの左上隅から30 x 30 / 2 = 450
個だけを使用します。得られた結果を見てみましょう。
ご覧のとおり、すでに良くなりました。また、ベクトルをユニークなものにするスパイクのほとんどが依然として前部に位置していることもわかります (図の緑色で選択されているように) CosTransform(cosLimit: 6)
を使用してみます。これは、 6 x 6 / 2 = 18
のみを使用することを意味します。 90,000 のうち6 x 6 / 2 = 18
値を計算し、結果を確認します。
現在はかなり改善されており、元のイメージに非常に近づいています。ただし、小さな問題が 1 つだけあります。それは、この実装が遅いということです。 DCT が時間のかかる演算であることを理解するのにアルゴリズムの複雑さの専門家である必要はありませんが、Swift 配列を使用して大きなベクトルを扱う場合は、線形時間計算量を持つ内積ですら十分な速度が得られません。良いニュースは、標準ライブラリとしてすでに用意されている Apple のAccelerate
フレームワークのvDSP
を使用することで、実装がはるかに高速かつ簡単に実行できることです。 vDSP
についてはここで読むことができますが、簡単に言うと、デジタル信号処理タスクを非常に高速に実行するための一連のメソッドです。内部には、大規模なデータセットに最適な低レベルの最適化が数多く組み込まれています。 vDSP
を使用してドット積と DCT を実装しましょう。
infix operator • public func •(left: [Double], right: [Double]) -> Double { return vDSP.dot(left, right) } prefix operator ->> public prefix func ->>(value: [Double]) -> [Double] { let setup = vDSP.DCT(count: value.count, transformType: .II) return setup!.transform(value.compactMap { Float($0) }).compactMap { Double($0) } }
退屈を軽減するために、いくつかの演算子を使用して読みやすくしました。これらの関数を次の方法で使用できるようになりました。
let cosRedArray = ->> redValues let redResult = redCosArray • testArray
新しい DCT 実装には、現在の行列サイズに関して問題があります。 300 x 300 の画像は、2 の累乗である特定のサイズで動作するように最適化されているため、この画像では動作しません。したがって、画像を新しいメソッドに渡す前に、画像を拡大縮小するための努力が必要になります。
今までなんとかこの文章を読んでくださった方、あるいは怠け者で読まずにスクロールしてしまった方に感謝します。この記事の目的は、ネイティブのツールを使用して解決することを人々が考えていない多くのタスクが、最小限の労力で解決できることを示すことでした。代替ソリューションを探すのは楽しいことであり、そのようなタスクを解決するための唯一のオプションとして Python ライブラリの統合に心を限定しないでください。