paint-brush
Python, 신경망 및 라이브러리 없이 Swift에서 ML 작업by@pichukov
552
552

Python, 신경망 및 라이브러리 없이 Swift에서 ML 작업

Aleksei Pichukov15m2024/01/04
Read on Terminal Reader

이 글은 Swift로 무언가를 작성하는 방법에 대한 가이드가 아닙니다. 오히려 Python을 사용하는 언어에 관계없이 직면하는 모든 문제나 작업을 해결할 ML 라이브러리의 궁극적인 솔루션에 대한 다리로 보는 많은 개발자의 현재 사고 방식에 대한 생각에 가깝습니다. 대부분의 개발자는 Python 라이브러리 없이 대체 솔루션을 고려하기보다는 Python 라이브러리를 자신의 언어/환경에 통합하는 방법을 찾는 데 시간을 투자하는 것을 선호한다고 장담합니다. 이것이 본질적으로 나쁜 것은 아니지만 재사용은 지난 수십 년 동안 IT 발전의 중요한 원동력이었습니다. 저는 많은 개발자가 더 이상 대체 솔루션을 고려하지 않는다는 것을 느끼기 시작했습니다. 이러한 사고방식은 대규모 언어 모델의 현재 상태와 발전으로 인해 더욱 확고해졌습니다. 우리는 고전적인 ML 작업을 수행하고 라이브러리 없이 Swift 언어를 사용하여 이를 해결합니다.
featured image - Python, 신경망 및 라이브러리 없이 Swift에서 ML 작업
Aleksei Pichukov HackerNoon profile picture
0-item
1-item

신경망은 오늘날 기계 학습(ML)의 최전선에 있으며, 신경망을 사용하여 문제를 해결할 것인지 여부에 관계없이 Python은 의심할 여지 없이 모든 ML 작업에 적합한 프로그래밍 언어입니다. NumPy, Pandas, Keras, TensorFlow, PyTorch 등과 같이 ML 작업의 전체 스펙트럼을 포괄하는 광범위한 Python 라이브러리가 있습니다. 이러한 라이브러리는 일반적으로 ML 알고리즘의 C 또는 C++ 구현과 내부 접근 방식에 의존합니다. 왜냐하면 Python이 너무 느리기 때문입니다. 그러나 Python은 존재하는 유일한 프로그래밍 언어도 아니고 제가 일상 업무에 사용하는 언어도 아닙니다.


이 글은 Swift로 무언가를 작성하는 방법에 대한 가이드가 아닙니다. 오히려 이는 Python을 사용하는 언어에 관계없이 직면하는 모든 문제나 작업을 해결할 ML 라이브러리의 궁극적인 솔루션에 대한 다리로 보는 많은 개발자의 현재 사고 방식에 대한 사고 방식에 가깝습니다. 대부분의 개발자는 Python 라이브러리 없이 대체 솔루션을 고려하기보다는 Python 라이브러리를 자신의 언어/환경에 통합하는 방법을 찾는 데 시간을 투자하는 것을 선호한다고 장담합니다. 이것이 본질적으로 나쁜 것은 아니지만 재사용은 지난 수십 년 동안 IT 발전의 중요한 원동력이었습니다. 저는 많은 개발자가 더 이상 대체 솔루션을 고려하지 않는다는 것을 느끼기 시작했습니다. 이러한 사고방식은 대규모 언어 모델의 현재 상태와 발전으로 인해 더욱 확고해졌습니다.


균형이 부족합니다. 우리는 LLM에게 문제 해결을 요청하고 일부 Python 코드를 가져와 복사하고 불필요한 종속성으로 인한 잠재적으로 상당한 오버헤드로 생산성을 즐기기 위해 서두르고 있습니다.


다른 도구 없이 Swift, 수학만을 사용하여 당면한 작업을 해결하기 위한 대체 접근 방식을 살펴보겠습니다.


사람들이 신경망을 배우기 시작하면 대부분의 튜토리얼과 입문 자료에서 찾을 수 있는 두 가지 고전적인 Hello World 예제가 있습니다. 첫 번째는 손으로 쓴 숫자 인식입니다. 두 번째는 데이터 분류이다. 이 기사에서는 두 번째 문제에 중점을 두겠지만, 제가 다룰 솔루션은 첫 번째 문제에도 적용됩니다.


이에 대한 매우 좋은 시각적 예는 TensorFlow Playground에서 찾을 수 있습니다. 여기서는 다양한 신경망 구조를 가지고 놀고 결과 모델이 작업을 얼마나 잘 해결하는지 시각적으로 관찰할 수 있습니다.


TensorFlow 플레이그라운드 예시


서로 다른 색상을 지닌 이미지에 있는 이 점들의 실제적인 의미가 무엇인지 궁금하실 것입니다. 문제는 일부 데이터 세트를 시각적으로 표현한 것입니다. 특정 제품을 구매하는 사람들의 소셜 그룹이나 음악 선호도 등 다양한 유형의 데이터를 정확히 동일하거나 유사한 방식으로 제시할 수 있습니다. 나는 주로 모바일 iOS 개발에 초점을 맞추고 있기 때문에 유사한 방식으로 시각적으로 표현될 수 있는 제가 해결하고 있던 실제 작업의 예도 제시하겠습니다. 즉, 휴대폰의 자이로스코프와 자력계를 사용하여 벽 내부의 전선을 찾는 것입니다. 이 특정 예에서는 발견된 와이어와 관련된 매개변수 세트가 있고 벽 내부에는 아무것도 없는 또 다른 매개변수 세트가 있습니다.


우리가 사용할 데이터를 살펴보겠습니다.

ML-테스트


여기에는 빨간색 점과 파란색 점이라는 두 가지 유형의 데이터가 있습니다. 위에서 설명한 대로 이는 모든 종류의 분류된 데이터를 시각적으로 표현한 것일 수 있습니다. 예를 들어 빨간색 영역을 벽에 전선이 있는 경우 자력계와 자이로스코프의 신호가 있는 영역으로, 파란색 영역은 그렇지 않은 영역으로 가정하겠습니다.


우리는 이 점들이 어떻게든 함께 그룹화되어 일종의 빨간색과 파란색 모양을 형성한다는 것을 알 수 있습니다. 이러한 점들이 생성된 방식은 다음 이미지에서 임의의 점을 취하는 것입니다.


점들이 함께 그룹화됨


모델 훈련을 위한 무작위 포인트와 훈련된 모델 테스트를 위한 다른 무작위 포인트를 취하여 이 그림을 훈련 프로세스의 무작위 모델로 사용할 것입니다.


원본 사진은 300 x 300픽셀이고 90,000개의 도트(포인트)를 포함합니다. 훈련 목적으로 이러한 점 중 100점 미만인 0.2%만 사용하겠습니다. 모델의 성능을 더 잘 이해하기 위해 무작위로 3000개의 점을 선택하고 그림에서 그 주위에 원을 그립니다. 이 시각적 표현은 결과에 대한 보다 포괄적인 아이디어를 제공합니다. 모델의 효율성을 검증하기 위해 정확도의 백분율을 측정할 수도 있습니다.


모델을 어떻게 만들까요? 이 두 이미지를 함께 살펴보고 작업을 단순화하려고 하면 실제로 작업은 우리가 가지고 있는 데이터(빨간색과 파란색 점의 배치)에서 Origin 그림을 다시 만드는 것임을 알 수 있습니다. 그리고 모델에서 얻은 그림이 원본 그림에 더 가까워질수록 모델이 작동하는 속도가 더 정확해집니다. 또한 테스트 데이터를 원본 이미지의 일종의 극도로 압축된 버전으로 간주하고 다시 압축을 풀려는 목표를 가질 수도 있습니다.


우리가 하려는 일은 점을 코드에서 배열 또는 벡터로 표시되는 수학적 함수로 변환하는 것입니다. (여기서 벡터 용어는 수학 세계의 함수와 소프트웨어 개발의 배열 사이에 있기 때문에 여기서는 벡터 용어를 사용하겠습니다.) 그런 다음 이러한 벡터를 사용하여 모든 테스트 포인트를 확인하고 해당 벡터가 더 많이 속하는 벡터를 식별합니다.


데이터를 변환하기 위해 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 이라는 추가 범주가 있다는 것을 알 수 있습니다. 실제로 우리는 세 개의 벡터를 생성할 것입니다. 하나는 red 점, 두 번째는 blue 점, 세 번째는 none 으로 표시되는 모든 항목에 대한 것입니다. 그 중 두 개만 가질 수도 있지만 빨간색이 아닌 파란색이 아닌 훈련된 벡터를 사용하면 작업이 좀 더 간단해집니다.


우리는 'Point'가 Hashable 프로토콜을 준수하여 테스트 벡터에서 동일한 좌표를 가진 포인트를 피하기 위해 Set 사용하도록 했습니다.


 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 ) ) }


shortArray 함수 내에서 사용되는 CosTransform 에는 cosLimit 매개변수가 있습니다. 이 매개변수의 목적은 나중에 설명하겠습니다. 지금은 이를 무시하고 생성된 벡터 redCosArray , blueCosArraynoneCosArray 에 대해 원본 이미지에서 무작위로 3000개 포인트의 결과를 확인하겠습니다. 이를 작동시키려면 원본 이미지에서 가져온 단일 점에서 또 다른 DCT 벡터를 만들어야 합니다. Red , BlueNone cos 벡터에 대해 이미 수행한 것과 동일한 기능을 사용하여 정확히 동일한 방식으로 이 작업을 수행합니다. 하지만 이 새로운 벡터가 어느 벡터에 속하는지 어떻게 알 수 있을까요? 이에 대한 매우 간단한 수학 접근 방식이 있습니다: Dot Product . 두 개의 벡터를 비교하고 가장 유사한 쌍을 찾는 작업이 있으므로 Dot Product는 이를 정확하게 제공합니다. 두 개의 동일한 벡터에 대해 내적 연산을 적용하면 동일한 벡터 및 다른 값을 갖는 다른 벡터에 적용되는 다른 내적 결과보다 더 큰 양수 값이 제공됩니다. 그리고 직교 벡터(서로 공통점이 없는 벡터)에 내적을 적용하면 결과는 0이 됩니다. 이를 고려하여 간단한 알고리즘을 생각해 낼 수 있습니다.


  1. 3000개의 무작위 포인트를 하나씩 살펴보세요.
  2. DCT(이산 코사인 변환)를 사용하여 단일 점이 하나만 있는 300x300 행렬에서 벡터를 만듭니다.
  3. redCosArray , blueCosArray , noneCosArray 를 사용하여 이 벡터에 내적을 적용합니다.
  4. 이전 단계의 가장 큰 결과는 Red , Blue , None 이라는 정답을 알려줄 것입니다.


여기서 유일하게 누락된 기능은 Dot Product입니다. 이에 대한 간단한 함수를 작성해 보겠습니다.


 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 함수를 다시 한 번 확인해 보겠습니다. 여기서는 DCT 행렬의 왼쪽 상단 모서리를 취하고 벡터를 고유하게 만드는 가장 활성 매개변수만 사용하는 이유로 cosLimit 매개변수를 사용합니다.


 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 값이고 결과를 확인합니다.

성공

지금은 훨씬 좋아져서 원본 이미지와 매우 가깝습니다. 그러나 작은 문제가 하나 있습니다. 이 구현은 느립니다. 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 구현에 문제가 있습니다. 2의 거듭제곱인 특정 크기에서 작동하도록 최적화되어 있기 때문에 300 x 300 이미지에서는 작동하지 않습니다. 따라서 새로운 방법을 적용하기 전에 이미지 크기를 조정하기 위해 약간의 노력을 기울여야 합니다.

요약

지금까지 이 글을 읽어주셨거나 읽지 않고 스크롤할 정도로 게으른 분들께 감사드립니다. 이 글의 목적은 사람들이 일부 기본 도구로는 해결할 수 없다고 생각하는 많은 작업을 최소한의 노력으로 해결할 수 있다는 것을 보여주는 것이었습니다. 대체 솔루션을 찾는 것은 즐거운 일이며, 이러한 작업을 해결하기 위한 유일한 옵션으로 Python 라이브러리 통합에만 마음을 제한하지 마십시오.