paint-brush
Nhiệm vụ ML trên Swift không có Python, mạng thần kinh và thư việnby@pichukov
552
552

Nhiệm vụ ML trên Swift không có Python, mạng thần kinh và thư viện

Aleksei Pichukov15m2024/01/04
Read on Terminal Reader

Bài viết này không phải là hướng dẫn cách viết nội dung nào đó trong Swift; đúng hơn, nó giống như một suy nghĩ về suy nghĩ hiện tại của nhiều nhà phát triển coi Python là cầu nối dẫn đến giải pháp tối ưu cho các thư viện ML sẽ giải quyết mọi vấn đề hoặc nhiệm vụ mà họ gặp phải, bất kể ngôn ngữ họ đang sử dụng. Tôi cá rằng hầu hết các nhà phát triển thích đầu tư thời gian vào việc tìm cách tích hợp thư viện Python vào ngôn ngữ/môi trường của họ hơn là xem xét các giải pháp thay thế mà không có chúng. Mặc dù điều này vốn không tệ – việc tái sử dụng đã là động lực thúc đẩy tiến bộ đáng kể trong lĩnh vực CNTT trong vài thập kỷ qua – nhưng tôi bắt đầu cảm thấy rằng nhiều nhà phát triển thậm chí không còn xem xét các giải pháp thay thế nữa. Tư duy này càng trở nên cố định hơn với tình trạng hiện tại và những tiến bộ trong Mô hình ngôn ngữ lớn. Chúng tôi sẽ thực hiện một nhiệm vụ ML cổ điển và giải quyết nó bằng cách sử dụng ngôn ngữ Swift và không có Thư viện.
featured image - Nhiệm vụ ML trên Swift không có Python, mạng thần kinh và thư viện
Aleksei Pichukov HackerNoon profile picture
0-item
1-item

Mạng thần kinh đang đi đầu trong Machine Learning (ML) ngày nay và Python chắc chắn là ngôn ngữ lập trình phù hợp cho bất kỳ tác vụ ML nào, bất kể người ta có ý định sử dụng Mạng thần kinh để giải quyết nó hay không. Có rất nhiều thư viện Python có sẵn bao gồm toàn bộ các tác vụ ML, chẳng hạn như NumPy, Pandas, Keras, TensorFlow, PyTorch, v.v. Các thư viện này thường dựa vào việc triển khai các thuật toán và phương pháp tiếp cận ML bằng C hoặc C++ vì Python quá chậm đối với chúng. Tuy nhiên, Python không phải là ngôn ngữ lập trình duy nhất tồn tại và nó không phải là ngôn ngữ tôi sử dụng trong công việc hàng ngày.


Bài viết này không phải là hướng dẫn cách viết nội dung nào đó trong Swift; đúng hơn, nó giống như một suy nghĩ về suy nghĩ hiện tại của nhiều nhà phát triển coi Python là cầu nối dẫn đến giải pháp tối ưu cho các thư viện ML sẽ giải quyết mọi vấn đề hoặc nhiệm vụ mà họ gặp phải, bất kể ngôn ngữ họ đang sử dụng. Tôi cá rằng hầu hết các nhà phát triển thích đầu tư thời gian vào việc tìm cách tích hợp thư viện Python vào ngôn ngữ/môi trường của họ hơn là xem xét các giải pháp thay thế mà không có chúng. Mặc dù điều này vốn không tệ – việc tái sử dụng đã là động lực thúc đẩy tiến bộ đáng kể trong lĩnh vực CNTT trong vài thập kỷ qua – nhưng tôi bắt đầu cảm thấy rằng nhiều nhà phát triển thậm chí không còn xem xét các giải pháp thay thế nữa. Tư duy này càng trở nên cố định hơn với tình trạng hiện tại và những tiến bộ trong Mô hình ngôn ngữ lớn.


Sự cân bằng đang thiếu; chúng tôi đang gấp rút yêu cầu LLM giải quyết các vấn đề của chúng tôi, lấy một số mã Python, sao chép nó và tận hưởng năng suất của chúng tôi với chi phí tiềm tàng đáng kể từ các phụ thuộc không cần thiết.


Hãy khám phá phương pháp thay thế để giải quyết nhiệm vụ hiện tại chỉ bằng Swift, toán học và không sử dụng công cụ nào khác.


Khi mọi người bắt đầu tìm hiểu Mạng nơ-ron, có hai ví dụ Hello World cổ điển mà bạn có thể tìm thấy trong hầu hết các hướng dẫn và tài liệu giới thiệu về nó. Đầu tiên là nhận dạng chữ số viết tay. Thứ hai là phân loại dữ liệu. Tôi sẽ tập trung vào vấn đề thứ hai trong bài viết này, nhưng giải pháp mà tôi sẽ thực hiện cũng sẽ hiệu quả với vấn đề đầu tiên.


Bạn có thể tìm thấy ví dụ trực quan rất hay về nó trong TensorFlow Playground, nơi bạn có thể thử nghiệm với các cấu trúc mạng thần kinh khác nhau và quan sát trực quan xem mô hình kết quả giải quyết nhiệm vụ tốt như thế nào.


Ví dụ về Sân chơi TensorFlow


Bạn có thể hỏi ý nghĩa thực tế của những chấm này trên một hình ảnh với các màu sắc khác nhau là gì? Vấn đề là nó là sự thể hiện trực quan của một số bộ dữ liệu. Bạn có thể trình bày nhiều loại dữ liệu khác nhau theo cách giống hệt hoặc tương tự nhau, chẳng hạn như các nhóm xã hội gồm những người mua sản phẩm cụ thể hoặc sở thích âm nhạc. Vì tôi chủ yếu tập trung vào phát triển iOS trên thiết bị di động nên tôi cũng sẽ đưa ra ví dụ về một nhiệm vụ thực tế mà tôi đang giải quyết có thể được trình bày trực quan theo cách tương tự: tìm dây điện bên trong tường bằng con quay hồi chuyển và từ kế trên điện thoại di động. Trong ví dụ cụ thể này, chúng ta có một bộ tham số liên quan đến dây được tìm thấy và một bộ tham số khác cho không có gì bên trong bức tường.


Chúng ta hãy xem dữ liệu chúng ta sẽ sử dụng.

Kiểm tra ML


Chúng tôi có hai loại dữ liệu ở đây: chấm đỏ và chấm xanh. Như tôi đã mô tả ở trên, nó có thể là sự thể hiện trực quan của bất kỳ loại dữ liệu mật nào. Ví dụ: hãy lấy vùng màu đỏ làm nơi chúng ta có tín hiệu từ từ kế và con quay hồi chuyển trong trường hợp chúng ta có dây điện trên tường và vùng màu xanh lam trong trường hợp chúng ta không có.


Chúng ta có thể thấy rằng bằng cách nào đó những chấm này được nhóm lại với nhau và tạo thành một số hình dạng màu đỏ và xanh. Cách các dấu chấm này được tạo ra là lấy các điểm ngẫu nhiên từ hình ảnh sau:


Các dấu chấm được nhóm lại với nhau


Chúng tôi sẽ sử dụng hình ảnh này làm mô hình ngẫu nhiên cho quá trình huấn luyện của mình bằng cách lấy các điểm ngẫu nhiên để huấn luyện mô hình và các điểm ngẫu nhiên khác để kiểm tra mô hình đã huấn luyện của chúng tôi.


Ảnh gốc có kích thước 300 x 300 pixel, chứa 90.000 điểm (điểm). Vì mục đích đào tạo, chúng tôi sẽ chỉ sử dụng 0,2% số chấm này, tức là dưới 100 điểm. Để hiểu rõ hơn về hiệu suất của mô hình, chúng tôi sẽ chọn ngẫu nhiên 3000 điểm và vẽ các vòng tròn xung quanh chúng trên hình. Sự trình bày trực quan này sẽ cung cấp cho chúng ta ý tưởng toàn diện hơn về kết quả. Chúng ta cũng có thể đo lường phần trăm độ chính xác để xác minh tính hiệu quả của mô hình.


Chúng ta sẽ tạo ra một mô hình như thế nào? Nếu chúng ta nhìn hai hình ảnh này cùng nhau và cố gắng đơn giản hóa nhiệm vụ của mình, chúng ta sẽ phát hiện ra rằng trên thực tế, nhiệm vụ là tạo lại hình ảnh Origin từ dữ liệu chúng ta có (lô chấm đỏ và xanh). Và hình ảnh chúng ta có được từ mô hình của mình càng gần với hình ảnh ban đầu thì mô hình của chúng ta hoạt động càng chính xác hơn. Chúng tôi cũng có thể coi dữ liệu thử nghiệm của mình như một loại phiên bản cực kỳ nén của hình ảnh gốc và có mục tiêu giải nén nó trở lại.


Những gì chúng ta sắp làm là chuyển đổi các dấu chấm của chúng ta thành các hàm toán học sẽ được biểu diễn trong mã dưới dạng mảng hoặc vectơ (tôi sẽ sử dụng thuật ngữ vectơ ở đây trong văn bản chỉ vì nó nằm giữa hàm từ thế giới toán học và mảng từ phát triển phần mềm). Sau đó, chúng tôi sẽ sử dụng các vectơ này để thách thức mọi điểm kiểm tra và xác định xem nó thuộc về vectơ nào nhiều hơn.


Để chuyển đổi dữ liệu của chúng tôi, tôi sẽ thử Biến đổi Cosine rời rạc (DCT). Tôi sẽ không đi sâu vào bất kỳ lời giải thích toán học nào về nó là gì và nó hoạt động như thế nào, vì bạn có thể dễ dàng tìm thấy thông tin đó nếu muốn. Tuy nhiên, tôi có thể giải thích một cách đơn giản nó có thể giúp chúng ta như thế nào và tại sao nó lại hữu ích. DCT được sử dụng trong nhiều lĩnh vực, bao gồm cả nén hình ảnh (chẳng hạn như định dạng JPEG). Nó chuyển đổi dữ liệu sang định dạng nhỏ gọn hơn bằng cách chỉ giữ lại những phần quan trọng của hình ảnh trong khi loại bỏ những chi tiết không quan trọng. Nếu chúng ta áp dụng DCT cho hình ảnh 300x300 chỉ chứa các chấm đỏ, chúng ta sẽ nhận được ma trận các giá trị 300x300 có thể được chuyển đổi thành một mảng (hoặc vectơ) bằng cách lấy riêng từng hàng.


Cuối cùng chúng ta hãy viết một số mã cho nó. Đầu tiên, chúng ta cần tạo một đối tượng đơn giản đại diện cho điểm (dấu chấm) của chúng ta.


 enum Category { case red case blue case none } struct Point: Hashable { let x: Int let y: Int let category: Category }


Bạn có thể nhận thấy rằng chúng tôi có một danh mục bổ sung được gọi là none . Thực tế, cuối cùng chúng ta sẽ tạo ba vectơ: một cho các điểm red , thứ hai cho các điểm blue và vectơ thứ ba cho bất kỳ thứ gì khác được biểu thị bằng none . Mặc dù chúng ta chỉ có thể có hai trong số chúng, nhưng việc có một vectơ được đào tạo không phải màu đỏ và không phải màu xanh lam sẽ khiến mọi việc đơn giản hơn một chút.


Chúng tôi có `Point` tuân theo giao thức Hashable để sử dụng Set nhằm tránh có các điểm có cùng tọa độ trong vectơ thử nghiệm của chúng tôi.


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


Bây giờ chúng ta có thể sử dụng nó để lấy 0.2% điểm ngẫu nhiên từ hình ảnh ban đầu cho màu đỏ, xanh lam và không có điểm nào.


 redTrainPoints = randomPoints(from: redPoints, percentage: 0.002) blueTrainPoints = randomPoints(from: bluePoints, percentage: 0.002) noneTrainPoints = randomPoints(from: nonePoints, percentage: 0.002)


Chúng tôi sẵn sàng chuyển đổi những dữ liệu đào tạo này bằng DCT. Đây là một triển khai của nó:


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


Hãy tạo một thể hiện của đối tượng CosTransform và kiểm tra nó.


 let math = CosTransform(cosLimit: Int.max) ... redCosArray = cosFunction(points: redTrainPoints) blueCosArray = cosFunction(points: blueTrainPoints) noneCosArray = cosFunction(points: noneTrainPoints)


Chúng tôi sử dụng một số chức năng trợ giúp đơn giản ở đây:


 func cosFunction(points: [Point]) -> [Double] { return math.shortArray( matrix: math.discreteCosTransform( for: points, width: 300, height: 300 ) ) }


Có một tham số cosLimit trong CosTransform được sử dụng bên trong hàm shortArray, tôi sẽ giải thích mục đích của nó sau, bây giờ hãy bỏ qua nó và kiểm tra kết quả của 3000 điểm ngẫu nhiên từ hình ảnh gốc so với các Vector redCosArray , blueCosArraynoneCosArray đã tạo của chúng tôi. Để làm cho nó hoạt động, chúng ta cần tạo một vectơ DCT khác từ một điểm duy nhất được lấy từ ảnh gốc. Điều này chúng tôi thực hiện chính xác theo cùng một cách và sử dụng các chức năng tương tự mà chúng tôi đã thực hiện cho các Vector cos Red , BlueNone . Nhưng làm thế nào chúng ta có thể tìm ra vectơ mới này thuộc về cái nào? Có một cách tiếp cận toán học rất đơn giản cho nó: Dot Product . Vì chúng ta có nhiệm vụ so sánh hai Vector và tìm ra cặp giống nhau nhất nên Dot Product sẽ cung cấp cho chúng ta chính xác điều này. Nếu bạn áp dụng thao tác Sản phẩm chấm cho hai Vector giống hệt nhau, nó sẽ cung cấp cho bạn một số giá trị dương sẽ lớn hơn bất kỳ kết quả Sản phẩm chấm nào khác áp dụng cho cùng một Vector và bất kỳ Vector nào khác có các giá trị khác nhau. Và nếu bạn áp dụng Tích số chấm cho các vectơ trực giao (các vectơ không có điểm chung nào giữa nhau), kết quả là bạn sẽ nhận được điểm 0. Cân nhắc điều này, chúng ta có thể đưa ra một thuật toán đơn giản:


  1. Đi qua tất cả 3000 điểm ngẫu nhiên của chúng tôi từng điểm một.
  2. Tạo một vectơ từ ma trận 300x300 chỉ với một điểm duy nhất bằng cách sử dụng DCT (Biến đổi Cosine rời rạc).
  3. Áp dụng tích chấm cho vectơ này với redCosArray , sau đó với blueCosArray , rồi với noneCosArray .
  4. Kết quả tốt nhất của bước trước sẽ chỉ cho chúng ta câu trả lời đúng: Red , Blue , None .


Chức năng duy nhất còn thiếu ở đây là Dot Product, hãy viết một hàm đơn giản cho nó:


 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 }


Và đây là cách triển khai thuật toán:


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


Tất cả những gì chúng ta cần làm bây giờ là vẽ một hình ảnh từ fillPoints . Chúng ta hãy xem các điểm tàu mà chúng tôi đã sử dụng, vectơ DCT mà chúng tôi đã tạo từ dữ liệu tàu của mình và kết quả cuối cùng mà chúng tôi nhận được:

Kết quả

Vâng, có vẻ như tiếng ồn ngẫu nhiên. Nhưng chúng ta hãy nhìn vào biểu diễn trực quan của vectơ. Bạn có thể thấy một số đột biến ở đó, đó chính xác là thông tin chúng tôi cần tập trung vào và loại bỏ hầu hết nhiễu khỏi kết quả DCT của chúng tôi. Nếu nhìn vào cách biểu diễn trực quan đơn giản của ma trận DCT, chúng ta sẽ thấy rằng thông tin hữu ích nhất (thông tin mô tả các đặc điểm độc đáo của hình ảnh) tập trung ở góc trên cùng bên trái:


Sự tập trung


Bây giờ hãy lùi lại một bước và kiểm tra hàm shortArray một lần nữa. Chúng tôi sử dụng tham số cosLimit ở đây chính xác vì lý do lấy góc trên cùng bên trái của ma trận DCT và chỉ sử dụng các tham số hoạt động mạnh nhất làm cho vectơ của chúng tôi trở thành duy nhất.


 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 }


Hãy tạo đối tượng math của chúng ta với cosLimit khác nhau:


 let math = CosTransform(cosLimit: 30)


Bây giờ thay vì sử dụng tất cả 90.000 giá trị, chúng ta sẽ chỉ sử dụng 30 x 30 / 2 = 450 trong số chúng từ góc trên cùng bên trái của ma trận DCT. Hãy cùng xem kết quả chúng ta đã thu được:

Kết quả

Như bạn có thể thấy, nó đã tốt hơn rồi. Chúng ta cũng có thể quan sát thấy rằng hầu hết các gai tạo nên sự độc đáo của Vector vẫn nằm ở phần phía trước (như được chọn với màu xanh lá cây trong hình), hãy thử sử dụng CosTransform(cosLimit: 6) có nghĩa là chúng ta sẽ chỉ sử dụng 6 x 6 / 2 = 18 giá trị trên 90.000 và kiểm tra kết quả:

Thành công

Bây giờ thì tốt hơn nhiều rồi, rất gần với ảnh gốc. Tuy nhiên, chỉ có một vấn đề nhỏ - việc triển khai này diễn ra chậm. Bạn sẽ không cần phải là chuyên gia về độ phức tạp của thuật toán để nhận ra rằng DCT là một hoạt động tốn thời gian, nhưng ngay cả tích số chấm, có độ phức tạp về thời gian tuyến tính, cũng không đủ nhanh khi làm việc với các vectơ lớn bằng mảng Swift. Tin vui là chúng tôi có thể thực hiện việc này nhanh hơn và đơn giản hơn nhiều bằng cách sử dụng vDSP từ khung Accelerate của Apple mà chúng tôi đã có dưới dạng thư viện tiêu chuẩn. Bạn có thể đọc về vDSP tại đây , nhưng nói một cách đơn giản, đó là một tập hợp các phương pháp để thực hiện các tác vụ xử lý tín hiệu số một cách rất nhanh. Nó có rất nhiều tính năng tối ưu hóa cấp thấp hoạt động hoàn hảo với các tập dữ liệu lớn. Hãy triển khai sản phẩm chấm và DCT của chúng tôi bằng vDSP :


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


Để làm cho nó bớt tẻ nhạt hơn, tôi đã sử dụng một số toán tử để làm cho nó dễ đọc hơn. Bây giờ bạn có thể sử dụng các chức năng này theo cách sau:


 let cosRedArray = ->> redValues let redResult = redCosArray • testArray


Có vấn đề với việc triển khai DCT mới liên quan đến kích thước ma trận hiện tại của chúng tôi. Nó sẽ không hoạt động với hình ảnh 300 x 300 của chúng tôi vì nó được tối ưu hóa để hoạt động với các kích thước cụ thể có lũy thừa bằng 2. Do đó, chúng tôi sẽ cần nỗ lực để chia tỷ lệ hình ảnh trước khi đưa nó vào phương pháp mới.

Bản tóm tắt

Cảm ơn bất cứ ai đã đọc được văn bản này cho đến bây giờ hoặc lười biếng lướt qua mà không đọc. Mục đích của bài viết này là để chỉ ra rằng nhiều nhiệm vụ mà mọi người không nghĩ đến việc giải quyết bằng một số công cụ gốc có thể được giải quyết với nỗ lực tối thiểu. Thật thú vị khi tìm kiếm các giải pháp thay thế và đừng giới hạn tâm trí của bạn vào việc tích hợp thư viện Python như là lựa chọn duy nhất để giải quyết các tác vụ như vậy.