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 仍处于起步阶段,因此可能会发生快速变化。