我们非常感谢赞助商的支持
要成为赞助商并支持 Neuron 的开发,只需点击此 GitHub 仓库顶部的“Sponsor”按钮。
欢迎随时向我发送关于如何改进的建议。我非常乐意了解更多信息!!您也可以随意在此处分配 issue。运行单元测试也可以帮助您了解项目的工作原理!
运行 ./scripts/onboard.sh
以安装 Neuron
提供的 Xcode 模板,以便快速生成层代码模板。
欢迎随时在此处提交关于框架的 issue,或通过 Discord 联系我。我乐于接受所有关于如何改进框架的建议。
当创建 PR 到 develop
或 master
分支时,会自动运行自动化测试。这些测试必须通过后,PR 才能被合并。所有 PR 必须合并到 develop
分支。
所有功能都必须从 develop
分支分叉出来。
多年来,Neuron 一直是我的个人项目。我开始学习机器学习的基础知识,并且认为学习它的最好方法是自己实现它。我选择了 Swift,因为它是我最熟悉的语言,而且我知道针对 ML 进行优化会很有挑战性,因为它有很多开销。您在此仓库中看到的是我过去两年左右的工作积累。它是我的心血结晶。我决定将其开源,因为我想与 ML 和 Swift 社区分享我所学到的知识。我想让这个框架的用户有机会在他们的项目或应用程序中学习和实现 ML。玩得开心!
这个框架仍然缺少很多东西,但是通过这次重写,我为框架带来了更多的灵活性,以便可以尝试不同的架构和模型。框架提供了一些示例模型,例如 Classifier、GAN、WGAN 和 WGANGP。我一直在从事这个项目,并将继续提供更新。
从 WGAN 生成的 7。在 MNIST 7 上训练了 10 个 epoch。生成器上 16 - 32 个内核。
目前还没有 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 捆绑了三个提供的优化器。
Adam
SGD
RMSProp
所有优化器都是可互换的。优化器是网络的“大脑”。所有训练网络的功能调用都应通过您特定的 Optimizer
调用。让我们为此分类器构建一个 Adam
优化器。
let optim = Adam(network,
learningRate: 0.0001,
l2Normalize: false)
这里的第一个参数是我们上面定义的网络。learningRate
是调用 .step()
时优化器将采取的步长大小。l2Normalize
定义优化器是否会在将梯度应用于权重之前对其进行归一化。此属性的默认值为 false
。Adam
还接受其 beta1
、beta2
和 epsilon
属性的属性。
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
工作线程数。8
或 16
通常对于此操作来说已经足够了。这将把批次拆分到多个线程上,从而实现更快的执行速度。
注意:请务必导入 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 的主要骨干是 Tensor
对象。此对象基本上是一个经过优化的三维数字数组。所有 Tensor
对象都是 3D 数组,但它们可以包含任何类型的中间数组。它的大小由 TensorSize
对象定义,该对象定义 columns
、rows
、depth
。
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
您可以通过在您想要设置 graph
的 Tensor
上调用 setGraph(_ tensor: Tensor)
,将一个 Tensor
附加到另一个 Tensor
的图。
let inputTensor = Tensor([1,2,3,4])
var outputTensor = Tensor([2])
outputTensor.setGraph(inputTensor)
这样做会将 inputTensor
设置为 outputTensor
的 graph
。这意味着当在 outputTensor
上调用 .gradients
时,操作将如下所示
delta -> outputTensor.context(inputTensor) -> gradients w.r.t to inputTensor
除非您自己构建图或进行自定义操作,否则您永远不必自己设置图。这将由 Sequential
对象处理。
有关更深入的 TensorContext
文档,请访问 此处。
Neuron 使用 Tensor
对象及其随附的 TensorContext
执行梯度下降操作。Tensor
对象包含一个名为 context
的内部属性,其类型为 TensorContext
。TensorContext
是一个包含给定 Tensor
的反向传播信息的对象。截至目前,Neuron 还没有完整的自动求导设置,但是具有 TensorContext
的 Tensor
对象提供了一些类型的自动求导。
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()
}
}
当在具有附加 graph
的 Tensor
上调用 .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
的梯度。