DeepSwift 是一个 Swift 库,用于实现无需编译器魔法的可微编程。 用于编写新的可微类型的“底层”API 如下所示:
struct Foo : Layer {
/*
typealias Input = Float
typealias Output = Float
typealias Adjustment = Float
typealias AuxiliaryData = Float
*/
func inspectableApply(_ input: Float) -> (result: Float, auxiliaryData: Float) {
//...
}
func adjustment(input: Float, auxiliaryData: Float, gradient: Float) -> (adjustment: Float, backprop: Float) {
//...
}
mutating func move(_ adjustment: Float) {
//...
}
// optional methods
func apply(_ input: Float) -> Float {
//...
}
func auxData(at input: Float) -> Float {
//...
}
func backprop(input: Float, auxiliaryData: Float, gradient: Float) -> Float {
//...
}
}
这实际上就是实现反向传播所需的全部内容。 以下是各个方法的作用:
inspectableApply 应该产生一个“结果”,该结果应该等同于 apply 方法。 此外,它可以产生辅助数据。 对于损失函数,这应该是导数,以便我们可以将其输入到反向传播中。 在所有其他情况下,辅助数据应被视为层内部类型,除了该层本身之外,没有人关心它。adjustment 在您使用 inspectableApply 产生结果,并且收到了关于您的输出“好”的反馈后被调用。 此反馈表示为名为 gradient 的参数,您可以将其视为对您提供的输出的请求更改。 adjustment 方法的工作是将此输出更改转换为对此层和此层输入的更改。 直观地说,您需要在这里估计输入或层参数的微小变化会如何改变输出 - 然后将其与输出中请求的更改进行比较。 通常,人们可以使用诸如“微积分”或“导数”之类的具有吓人名称的工具来解决合理的数学答案。move 负责实际执行更改。其他方法具有默认实现,但我建议至少实现 backprop,这样在冻结此层时可以加快速度。
注意:Layer 协议要求 Input 和 Output 符合 Movable - 也就是说,它们也必须提供 move 方法。 adjustment/backprop 中的 gradient 的类型将为 Output.Adjustment,并且返回的 backprop 值将具有类型 Input.Adjustment。
以下是三个强制性方法如何协同工作:
extension Layer {
mutating func learn<Loss : Layer>(examples: Input, loss: Loss) where
Loss.Adjustment == Void,
Loss.AuxiliaryData == Output.Adjustment,
Loss.Input == Output {
let (result, derivative) = inspectableApply(examples)
let (adjustment, _) = adjustment(input: examples, auxiliaryData: derivative, gradient: loss.auxData(at: result))
move(adjustment)
}
}
如果要实现纯函数层,则可以使用 Function 协议,该协议不会要求您实现 move 方法。 相反,您需要指定 Adjustment 类型为 NoAdjustment<Scalar>,并使用适当的标量类型。
虽然 DeepSwift 将具体层和优化器的设计以及高效数值的实现留给其他(下游)仓库,但 DeepSwift 提供了您需要的高级 API,因此您可以编写:
struct MyModel : Learner {
var body = SomeLayer() |> SomeOtherLayer() |> ThirdLayer()
}
此 API 仍处于起步阶段,因此可能会发生快速变化。