paint-brush
无需 Python、神经网络和库的 Swift 上的 ML 任务经过@pichukov
584 讀數
584 讀數

无需 Python、神经网络和库的 Swift 上的 ML 任务

经过 Aleksei Pichukov15m2024/01/04
Read on Terminal Reader

太長; 讀書

本文并不是关于如何使用 Swift 编写内容的指南;而是关于如何使用 Swift 编写内容的指南。相反,它更像是对许多开发人员当前心态的思考,他们将 Python 视为通向 ML 库最终解决方案的桥梁,无论他们使用哪种语言,该库都将解决他们遇到的任何问题或任务。我敢打赌,大多数开发人员更愿意投入时间寻找将 Python 库集成到他们的语言/环境中的方法,而不是考虑没有它们的替代解决方案。虽然这本质上并不是坏事——过去几十年来,重用一直是 IT 进步的重要推动力——但我开始感觉到许多开发人员甚至不再考虑替代解决方案。随着大型语言模型的现状和进步,这种心态变得更加根深蒂固。我们将采用一个经典的 ML 任务,并使用 Swift 语言而不是库来解决它。
featured image - 无需 Python、神经网络和库的 Swift 上的 ML 任务
Aleksei Pichukov HackerNoon profile picture
0-item
1-item

神经网络处于当今机器学习 (ML) 的最前沿,而 Python 无疑是任何 ML 任务的首选编程语言,无论是否打算使用神经网络来解决它。有大量可用的 Python 库涵盖了整个 ML 任务,例如 NumPy、Pandas、Keras、TensorFlow、PyTorch 等。这些库通常依赖于 ML 算法和方法的 C 或 C++ 实现,因为 Python 对它们来说太慢了。然而,Python 并不是现有的唯一编程语言,也不是我在日常工作中使用的语言。


本文不是关于如何使用 Swift 编写内容的指南;而是关于如何使用 Swift 编写内容的指南。相反,它更像是对许多开发人员当前心态的思考,他们将 Python 视为通向 ML 库最终解决方案的桥梁,无论他们使用哪种语言,该库都将解决他们遇到的任何问题或任务。我敢打赌,大多数开发人员更愿意投入时间寻找将 Python 库集成到他们的语言/环境中的方法,而不是考虑没有它们的替代解决方案。虽然这本质上并不是坏事——过去几十年来,重用一直是 IT 进步的重要推动力——但我开始感觉到许多开发人员甚至不再考虑替代解决方案。随着大型语言模型的现状和进步,这种心态变得更加根深蒂固。


缺乏平衡;我们急于要求法学硕士解决我们的问题,获取一些 Python 代码,复制它,并享受我们的生产力,但不必要的依赖项可能会带来巨大的开销。


让我们探索仅使用 Swift、数学而不使用其他工具来解决手头任务的替代方法。


当人们开始学习神经网络时,您可以在大多数教程和介绍材料中找到两个经典的 Hello World 示例。第一个是手写数字识别。二是数据分类。我将在本文中重点讨论第二个,但我将介绍的解决方案也适用于第一个。


可以在 TensorFlow Playground 中找到非常好的视觉示例,您可以在其中尝试不同的神经网络结构,并直观地观察生成的模型解决任务的效果。


TensorFlow Playground 示例


您可能会问不同颜色的图像上的这些点的实际意义是什么?问题是它是某些数据集的视觉表示。您可以以完全相同或相似的方式呈现许多不同类型的数据,例如购买特定产品或音乐偏好的人群的社会群体。由于我主要关注移动 iOS 开发,因此我还将举一个我正在解决的实际任务的示例,该任务可以以类似的方式直观地表示:使用手机上的陀螺仪和磁力计查找墙内的电线。在这个特定的例子中,我们有一组与找到的电线相关的参数,另一组参数与墙内的任何东西无关。


让我们看一下我们将使用的数据。

ML测试


我们这里有两种类型的数据:红点和蓝点。正如我上面所描述的,它可以是任何类型的分类数据的视觉表示。例如,当墙上有电线时,我们将红色区域视为从磁力计和陀螺仪接收到信号的区域;如果墙上没有电线,则将蓝色区域视为接收来自磁力计和陀螺仪的信号的区域。


我们可以看到这些点以某种方式组合在一起并形成某种红色和蓝色的形状。这些点的生成方式是从下图中随机获取点:


点组合在一起


我们将使用这张图片作为我们训练过程的随机模型,通过随机点来训练模型,并使用其他随机点来测试我们训练的模型。


原始图片为 300 x 300 像素,包含 90,000 个点(点)。出于训练目的,我们将仅使用这些点的 0.2%,即不到 100 个点。为了更好地了解模型的性能,我们将随机选择 3000 个点并在图片上在它们周围画圆圈。这种视觉表示将使我们对结果有更全面的了解。我们还可以测量准确率来验证模型的效率。


我们要如何制作模型?如果我们一起看这两个图像并尝试简化我们的任务,我们会发现该任务实际上是根据我们拥有的数据(一批红点和蓝点)重新创建原始图片。我们从模型得到的图片越接近原始图片,我们的模型就越准确。我们还可以将测试数据视为原始图像的某种极度压缩版本,并以将其解压回来为目标。


我们要做的是将点转换为数学函数,这些函数将在代码中表示为数组或向量(我将在文本中使用向量术语,因为它介于数学世界的函数和软件开发的数组之间)。然后,我们将使用这些向量来挑战每个测试点并确定它更属于哪个向量。


为了转换我们的数据,我将尝试离散余弦变换(DCT)。我不会对它是什么以及它如何工作进行任何数学解释,因为如果您愿意,您可以轻松找到该信息。不过,我可以用简单的术语解释它如何帮助我们以及它为什么有用。 DCT被用于许多领域,包括图像压缩(例如JPEG格式)。它通过仅保留图像的重要部分,同时删除不重要的细节,将数据转换为更紧凑的格式。如果我们将 DCT 应用于仅包含红点的 300x300 图像,我们将得到一个 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 ) ) }


CosTransform中有一个cosLimit参数,在 ShortArray 函数中使用,稍后我将解释它的用途,现在让我们忽略它,并根据我们创建的向量redCosArrayblueCosArraynoneCosArray检查原始图像中的 3000 个随机点的结果。为了使其工作,我们需要从原始图像中的单个点创建另一个 DCT 向量。我们以完全相同的方式执行此操作,并使用我们已经为RedBlueNone cos 向量所做的相同函数。但是我们怎样才能找到这个新向量属于哪一个呢?有一个非常简单的数学方法: Dot Product 。由于我们的任务是比较两个向量并找到最相似的对,因此点积将为我们提供准确的结果。如果您对两个相同的向量应用点积运算,它将给您一些正值,该值将大于应用于相同向量和具有不同值的任何其他向量的任何其他点积结果。如果您将点积应用于正交向量(彼此之间没有任何共同点的向量),您将得到 0 结果。考虑到这一点,我们可以提出一个简单的算法:


  1. 一一检查我们所有的 3000 个随机点。
  2. 使用 DCT(离散余弦变换)从只有一个点的 300x300 矩阵创建一个向量。
  3. redCosArrayblueCosArraynoneCosArray对此向量应用点积。
  4. 上一步的最大结果将为我们指出正确的答案: RedBlueNone


这里唯一缺少的功能是点积,让我们为它编写一个简单的函数:


 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值并检查结果:

成功

现在好多了,非常接近原始图像。然而,只有一个小问题——这种实现速度很慢。您不需要成为算法复杂性方面的专家就能意识到 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 库集成作为解决此类任务的唯一选择。