Tests

赞助商

我们非常感谢赞助商的支持

成为赞助商

要成为赞助商并支持 Neuron 的开发,只需点击此 GitHub 仓库顶部的“Sponsor”按钮。

支持

欢迎随时向我发送关于如何改进的建议。我非常乐意了解更多信息!!您也可以随意在此处分配 issue。运行单元测试也可以帮助您了解项目的工作原理!

完整文档

重要的开发者注意事项!

当使用 Neuron 时,以 RELEASE 方案运行时速度大约快 10 倍。这是因为编译器优化被设置为最高优化值。如果您发现 Neuron 运行速度有些慢,这可能是原因所在。

开始开发之前

运行 ./scripts/onboard.sh 以安装 Neuron 提供的 Xcode 模板,以便快速生成层代码模板。

贡献政策

提交 Issue

欢迎随时在此处提交关于框架的 issue,或通过 Discord 联系我。我乐于接受所有关于如何改进框架的建议。

Pull Requests

当创建 PR 到 developmaster 分支时,会自动运行自动化测试。这些测试必须通过后,PR 才能被合并。所有 PR 必须合并到 develop 分支。

分支

所有功能都必须从 develop 分支分叉出来。

背景

多年来,Neuron 一直是我的个人项目。我开始学习机器学习的基础知识,并且认为学习它的最好方法是自己实现它。我选择了 Swift,因为它是我最熟悉的语言,而且我知道针对 ML 进行优化会很有挑战性,因为它有很多开销。您在此仓库中看到的是我过去两年左右的工作积累。它是我的心血结晶。我决定将其开源,因为我想与 ML 和 Swift 社区分享我所学到的知识。我想让这个框架的用户有机会在他们的项目或应用程序中学习和实现 ML。玩得开心!

这个框架仍然缺少很多东西,但是通过这次重写,我为框架带来了更多的灵活性,以便可以尝试不同的架构和模型。框架提供了一些示例模型,例如 Classifier、GAN、WGAN 和 WGANGP。我一直在从事这个项目,并将继续提供更新。

示例项目

Neuron 演示

PokePal

示例

GAN、WGAN、WGANGP

从 WGAN 生成的 7。在 MNIST 7 上训练了 10 个 epoch。生成器上 16 - 32 个内核。

重要提示:GPU 支持(WIP)

目前还没有 GPU 执行,至少不像我希望的那样。一切都在 CPU 上运行,某些数学函数进行了一些 C 优化。Neuron 将在 CPU 上以多线程方式运行,速度相当不错,具体取决于模型。但是,一个非常大的模型,具有多个内核和卷积,将需要一段时间。这是我希望尽快解决的问题,但是 Metal 非常难以使用,特别是以我有限的知识以及我想要从头开始编写一切的愿望而言。

快速入门指南

要开始使用 Neuron,一切都从设置一个 Sequential 对象开始。此对象负责组织通过网络的前向和后向传播。

构建网络

让我们构建一个 MNIST 分类器网络。我们需要构建一个 Sequential 对象来处理我们的层。

let network = Sequential {
  [
    Conv2d(filterCount: 16,
            inputSize: TensorSize(array: [28,28,1]),
            padding: .same,
            initializer: initializer),
    BatchNormalize(),
    LeakyReLu(limit: 0.2),
    MaxPool(),
    Conv2d(filterCount: 32,
            padding: .same,
            initializer: initializer),
    BatchNormalize(),
    LeakyReLu(limit: 0.2),
    Dropout(0.5),
    MaxPool(),
    Flatten(),
    Dense(64, initializer: initializer),
    LeakyReLu(limit: 0.2),
    Dense(10, initializer: initializer),
    Softmax()
  ]
}

Sequential 接受一个属性,该属性是一个返回 Layer 类型数组的 block。init(_ layers: () -> [Layer])。这里的顺序很重要。第一层是 Conv2d 层,具有 16 个滤波器,填充 .same 和一个初始化器。默认初始化器是 .heNormal

第一层注意事项:

您可以在这里看到,只有第一层指定了 inputSize。这是因为所有其他层的 inputSize 在添加到 Optimizer 时都会自动计算。

选择优化器

Neuron 使用一个 protocol 来定义 Opitmizer 所需的内容。目前 Neuron 捆绑了三个提供的优化器。

所有优化器都是可互换的。优化器是网络的“大脑”。所有训练网络的功能调用都应通过您特定的 Optimizer 调用。让我们为此分类器构建一个 Adam 优化器。

let optim = Adam(network,
                 learningRate: 0.0001,
                 l2Normalize: false)

这里的第一个参数是我们上面定义的网络。learningRate 是调用 .step() 时优化器将采取的步长大小。l2Normalize 定义优化器是否会在将梯度应用于权重之前对其进行归一化。此属性的默认值为 falseAdam 还接受其 beta1beta2epsilon 属性的属性。

衰减函数

Optimizer 具有一个可选属性,用于设置学习率衰减函数。

public protocol DecayFunction {
  var decayedLearningRate: Tensor.Scalar { get }
  func reset()
  func step()
}

目前只有一个可用的 DecayFunction,那就是 ExponentialDecay。您可以通过设置 Optimizer 上的 decayFunction 属性来设置衰减函数。一旦设置,就无需再做任何事情。Optimizer 将负责更新和调用函数对象。

设置训练模型

至此,您已准备好训练 Optimizer 和网络。您可以创建自己的训练循环来创建分类器,也可以使用内置的 Classifier 模型。

Classifier 模型是一个完全可选的类,它在训练网络时为您完成大量繁重的工作。现在让我们使用它。

let classifier = Classifier(optimizer: optim,
                            epochs: 10,
                            batchSize: 32,
                            threadWorkers: 8,
                            log: false)

在这里,我们创建一个 Classifier 对象。我们传入之前定义的 Adam Optimizer、要训练的 epochs 数量、批大小以及要使用的 multi-threaded 工作线程数。816 通常对于此操作来说已经足够了。这将把批次拆分到多个线程上,从而实现更快的执行速度。

构建 MNIST 数据集

注意:请务必导入 NeuronDatasets 以获取 MNIST 和其他数据集。

下一步是获取 MNIST 数据集。Neuron 通过 MNIST() 对象在本地为您提供此数据集。

let data = await MNIST().build()

为了简单起见,我们可以使用 Async/Await 构建函数。Datasets 也支持 Combine 发布者。

开始训练!

要使用 Classifier 对象训练网络,只需调用 classifier.fit(data.training, data.val)。就这么简单!现在,Classifier 将训练指定的 epochs 数量,并在设置 MetricProvider 的情况下向其报告。

检索指标

所有 Optimizers 都支持添加 MetricsReporter 对象。此对象将跟踪您在初始化期间要求的所有指标。如果您的网络设置不支持该指标,它将报告 0

let reporter = MetricsReporter(frequency: 1,
                                metricsToGather: [.loss,
                                                  .accuracy,
                                                  .valAccuracy,
                                                  .valLoss])

optim.metricsReporter = reporter

optim.metricsReporter?.receive = { metrics in
  let accuracy = metrics[.accuracy] ?? 0
  let loss = metrics[.loss] ?? 0
  //let valLoss = metrics[.valLoss] ?? 0
  
  print("training -> ", "loss: ", loss, "accuracy: ", accuracy)
}

metricsToGather 数组是 Metric 定义的 Set

public enum Metric: String {
  case loss = "Training Loss"
  case accuracy = "Accuracy"
  case valLoss = "Validation Loss"
  case generatorLoss = "Generator Loss"
  case criticLoss = "Critic Loss"
  case gradientPenalty = "Gradient Penalty"
  case realImageLoss = "Real Image Loss"
  case fakeImageLoss = "Fake Image Loss"
  case valAccuracy = "Validation Accuracy"
}

MetricReporter 将在更新时调用 receive

远程指标日志记录

您可以使用 NeuronRemoteLogger 记录到远程服务,例如 Weights and Biases。请按照该仓库中 README 中的说明了解如何开始使用!

导出您的模型

一旦模型训练到您满意的程度,您可以将模型导出到 .smodel 文件。然后可以使用 Sequential 初始化器稍后导入此模型。导出不会导出您的 Optimizer 设置,只会导出 Optimizer 中指定的 Trainable

Neuron 提供了一个用于导出的辅助对象,称为 ExportHelper。用法很简单

// defined: 
public static func getModel<T: Codable>(filename: String = "model", model: T) -> URL?

// usage:
ExportHelper.export(filename: "my_model", model: network)

这将返回一个 URL,供您访问您的 .smodel 文件。

漂亮地打印您的网络

您还可以通过在 Sequential 对象上调用 print 将您的网络打印到控制台。它将漂亮地打印您的网络,如下所示

总结

继续尝试您的新模型,享受网络吧!在 Discord 上分享您的模型,或询问其他人制作的其他模型!

基础知识背景

Neuron 如何工作?

张量

Neuron 的主要骨干是 Tensor 对象。此对象基本上是一个经过优化的三维数字数组。所有 Tensor 对象都是 3D 数组,但它们可以包含任何类型的中间数组。它的大小由 TensorSize 对象定义,该对象定义 columnsrowsdepth

public class Tensor: Equatable, Codable {
  ...
    
  public init() {
    self.value = []
    self.context = TensorContext()
  }
  
  public init(_ data: Scalar? = nil, context: TensorContext = TensorContext()) {
    if let data = data {
      self.value = [[[data]]]
    } else {
      self.value = []
    }
    
    self.context = context
  }
  
  public init(_ data: [Scalar], context: TensorContext = TensorContext()) {
    self.value = [[data]]
    self.context = context
  }
  
  public init(_ data: [[Scalar]], context: TensorContext = TensorContext()) {
    self.value = [data]
    self.context = context
  }
  
  public init(_ data: Data, context: TensorContext = TensorContext()) {
    self.value = data
    self.context = context
  }
}

以上是 Tensor 支持的初始化器。有关 Tensor 的更深入文档,请访问 此处

算术

您也可以直接对 Tensor 对象执行基本算术运算。

static func * (Tensor, Tensor.Scalar) -> Tensor
static func * (Tensor, Tensor) -> Tensor
static func + (Tensor, Tensor) -> Tensor
static func + (Tensor, Tensor.Scalar) -> Tensor
static func - (Tensor, Tensor.Scalar) -> Tensor
static func - (Tensor, Tensor) -> Tensor
static func / (Tensor, Tensor.Scalar) -> Tensor
static func / (Tensor, Tensor) -> Tensor
static func == (Tensor, Tensor) -> Bool

构建反向传播图

您可以通过在您想要设置 graphTensor 上调用 setGraph(_ tensor: Tensor),将一个 Tensor 附加到另一个 Tensor 的图。

let inputTensor = Tensor([1,2,3,4])
var outputTensor = Tensor([2])

outputTensor.setGraph(inputTensor)

这样做会将 inputTensor 设置为 outputTensorgraph。这意味着当在 outputTensor 上调用 .gradients 时,操作将如下所示

delta -> outputTensor.context(inputTensor) -> gradients w.r.t to inputTensor

除非您自己构建图或进行自定义操作,否则您永远不必自己设置图。这将由 Sequential 对象处理。

梯度

有关更深入的 TensorContext 文档,请访问 此处

Neuron 使用 Tensor 对象及其随附的 TensorContext 执行梯度下降操作。Tensor 对象包含一个名为 context 的内部属性,其类型为 TensorContextTensorContext 是一个包含给定 Tensor 的反向传播信息的对象。截至目前,Neuron 还没有完整的自动求导设置,但是具有 TensorContextTensor 对象提供了一些类型的自动求导。

public struct TensorContext: Codable {
  public typealias TensorBackpropResult = (input: Tensor, weight: Tensor)
  public typealias TensorContextFunction = (_ inputs: Tensor, _ gradient: Tensor) -> TensorBackpropResult
  var backpropagate: TensorContextFunction
  
  public init(backpropagate: TensorContextFunction? = nil) {
    let defaultFunction = { (input: Tensor, gradient: Tensor) in
      return (Tensor(gradient.value), Tensor())
    }
    
    self.backpropagate = backpropagate ?? defaultFunction
  }
  
  public func encode(to encoder: Encoder) throws {}
  
  public init(from decoder: Decoder) throws {
    self = TensorContext()
  }
}

当在具有附加 graphTensor 上调用 .gradients(delta: SomeTensor) 时,它将自动反向传播整个 graph 并返回 Tensor.Gradient 对象。

public struct Gradient {
  let input: [Tensor]
  let weights: [Tensor]
  let biases: [Tensor]
  
  public init(input: [Tensor] = [],
              weights: [Tensor] = [],
              biases: [Tensor] = []) {
    self.input = input
    self.weights = weights
    self.biases = biases
  }
}

Tensor.Gradient 对象将包含您需要在 Optimizer 中执行反向传播步骤所需的所有梯度。此对象包含关于 input、关于 weights 和关于图的 biases 的梯度。