Bordes afilados en Swift diferenciable

Swift diferenciable ha recorrido un largo camino en términos de usabilidad. Aquí hay un aviso sobre las partes que aún no son obvias. A medida que avance, esta guía será cada vez más pequeña y podrá escribir código diferenciable sin necesidad de una sintaxis especial.

Bucles

Los bucles son diferenciables, solo hay que conocer un detalle. Cuando escriba el bucle, ajuste el bit donde especifica lo que está repitiendo withoutDerivative(at:)

var a: [Float] = [1,2,3]

Por ejemplo:

for _ in a.indices 
{}

se convierte

for _ in withoutDerivative(at: a.indices) 
{}

o:

for _ in 0..<a.count 
{}

se convierte

for _ in 0..<withoutDerivative(at: a.count) 
{}

Esto es necesario porque el miembro Array.count no contribuye a la derivada con respecto a la matriz. Sólo los elementos reales de la matriz contribuyen a la derivada.

Si tiene un bucle en el que usa manualmente un número entero como límite superior, no es necesario usarlo withoutDerivative(at:) :

let iterations: Int = 10
for _ in 0..<iterations {} //this is fine as-is.

Mapear y Reducir

map y reduce tienen versiones diferenciables especiales que funcionan exactamente como estás acostumbrado:

a = [1,2,3]
let aPlusOne = a.differentiableMap {$0 + 1}
let aSum = a.differentiableReduce(0, +)
print("aPlusOne", aPlusOne)
print("aSum", aSum)
aPlusOne [2.0, 3.0, 4.0]
aSum 6.0

Conjuntos de subíndices de matriz

Los conjuntos de subíndices de matriz ( array[0] = 0 ) no son diferenciables de fábrica, pero puedes pegar esta extensión:

extension Array where Element: Differentiable {
    @differentiable(where Element: Differentiable)
    mutating func updated(at index: Int, with newValue: Element) {
        self[index] = newValue
    }

    @derivative(of: updated)
    mutating func vjpUpdated(at index: Int, with newValue: Element)
      -> (value: Void, pullback: (inout TangentVector) -> (Element.TangentVector))
    {
        self.updated(at: index, with: newValue)
        return ((), { v in
            let dElement = v[index]
            v.base[index] = .zero
            return dElement
        })
    }
}

y luego la sintaxis de la solución es la siguiente:

var b: [Float] = [1,2,3]

en lugar de esto:

b[0] = 17

escribe esto:

b.updated(at: 0, with: 17)

Asegurémonos de que funciona:

func plusOne(array: [Float]) -> Float{
  var array = array
  array.updated(at: 0, with: array[0] + 1)
  return array[0]
}

let plusOneValAndGrad = valueWithGradient(at: [2], in: plusOne)
print(plusOneValAndGrad)
(value: 3.0, gradient: [1.0])

El error que obtendrá sin esta solución es Differentiation of coroutine calls is not yet supported . Aquí está el enlace para ver el progreso para hacer que esta solución sea innecesaria: https://bugs.swift.org/browse/TF-1277 (habla sobre Array.subscript._modify, que es lo que se llama detrás de escena cuando haces una matriz conjunto de subíndices).

Float <-> Conversiones Double

Si estás cambiando entre Float y Double , sus constructores aún no son diferenciables. Aquí hay una función que le permitirá pasar de Float a Double de manera diferenciada.

(Cambie Float y Double en el siguiente código y obtendrá una función que convierte de Double a Float ).

Puede realizar convertidores similares para cualquier otro tipo numérico real.

@differentiable
func convertToDouble(_ a: Float) -> Double {
    return Double(a)
}

@derivative(of: convertToDouble)
func convertToDoubleVJP(_ a: Float) -> (value: Double, pullback: (Double) -> Float) {
    func pullback(_ v: Double) -> Float{
        return Float(v)
    }
    return (value: Double(a), pullback: pullback)
}

A continuación se muestra un ejemplo de uso:

@differentiable
func timesTwo(a: Float) -> Double {
  return convertToDouble(a * 2)
}
let input: Float = 3
let valAndGrad = valueWithGradient(at: input, in: timesTwo)
print("grad", valAndGrad.gradient)
print("type of input:", type(of: input))
print("type of output:", type(of: valAndGrad.value))
print("type of gradient:", type(of: valAndGrad.gradient))
grad 2.0
type of input: Float
type of output: Double
type of gradient: Float

Funciones trascendentales y otras (sin, cos, abs, max)

Ya se han diferenciado muchas funciones trascendentales y otras funciones integradas comunes para Float y Double . Hay menos para Double que para Float . Algunos no están disponibles para ninguno de los dos. Así que aquí hay algunas definiciones de derivados manuales para darle una idea de cómo hacer lo que necesita, en caso de que aún no esté incluido:

pow (ver enlace para explicación derivada)

import Foundation

@usableFromInline
@derivative(of: pow) 
func powVJP(_ base: Double, _ exponent: Double) -> (value: Double, pullback: (Double) -> (Double, Double)) {
    let output: Double = pow(base, exponent)
    func pullback(_ vector: Double) -> (Double, Double) {
        let baseDerivative = vector * (exponent * pow(base, exponent - 1))
        let exponentDerivative = vector * output * log(base)
        return (baseDerivative, exponentDerivative)
    }

    return (value: output, pullback: pullback)
}

máximo

@usableFromInline
@derivative(of: max)
func maxVJP<T: Comparable & Differentiable>(_ x: T, _ y: T) -> (value: T, pullback: (T.TangentVector)
  -> (T.TangentVector, T.TangentVector))
{
    func pullback(_ v: T.TangentVector) -> (T.TangentVector, T.TangentVector) {
        if x < y {
            return (.zero, v)
        } else {
            return (v, .zero)
        }
    }
    return (value: max(x, y), pullback: pullback)
}

abdominales

@usableFromInline
@derivative(of: abs)
func absVJP<T: Comparable & SignedNumeric & Differentiable>(_ x: T)
  -> (value: T, pullback: (T.TangentVector) -> T.TangentVector)
{
    func pullback(_ v: T.TangentVector) -> T.TangentVector{
        if x < 0 {
            return .zero - v
        }
        else {
            return v
        }
    }
    return (value: abs(x), pullback: pullback)
}

sqrt (ver enlace para explicación derivada)

@usableFromInline
@derivative(of: sqrt) 
func sqrtVJP(_ x: Double) -> (value: Double, pullback: (Double) -> Double) {
    let output = sqrt(x)
    func pullback(_ v: Double) -> Double {
        return v / (2 * output)
    }
    return (value: output, pullback: pullback)
}

Comprobemos que estos funcionan:

let powGrad = gradient(at: 2, 2, in: pow)
print("pow gradient: ", powGrad, "which is", powGrad == (4.0, 2.772588722239781) ? "correct" : "incorrect")

let maxGrad = gradient(at: 1, 2, in: max)
print("max gradient: ", maxGrad, "which is", maxGrad == (0.0, 1.0) ? "correct" : "incorrect")

let absGrad = gradient(at: 2, in: abs)
print("abs gradient: ", absGrad, "which is", absGrad == 1.0 ? "correct" : "incorrect")

let sqrtGrad = gradient(at: 4, in: sqrt)
print("sqrt gradient: ", sqrtGrad, "which is", sqrtGrad == 0.25 ? "correct" : "incorrect")
pow gradient:  (4.0, 2.772588722239781) which is correct
max gradient:  (0.0, 1.0) which is correct
abs gradient:  1.0 which is correct
sqrt gradient:  0.25 which is correct

El error del compilador que le alerta de la necesidad de algo como esto es: Expression is not differentiable. Cannot differentiate functions that have not been marked '@differentiable' and that are defined in other files

Subíndice KeyPath

Los subíndices KeyPath (get o set) no funcionan de inmediato, pero una vez más, hay algunas extensiones que puede agregar y luego usar una sintaxis alternativa. Aquí lo tienes:

https://github.com/tensorflow/swift/issues/530#issuecomment-687400701

Esta solución es un poco más fea que las demás. Solo funciona para objetos personalizados, que deben ajustarse a la aritmética diferenciable y aditiva. Debe agregar un miembro .tmp y una función .read() , y usar el miembro .tmp como almacenamiento intermedio cuando obtiene el subíndice KeyPath (hay un ejemplo en el código vinculado). Los conjuntos de subíndices KeyPath funcionan de forma bastante sencilla con una función .write() .