Cette page a été traduite par l'API Cloud Translation.
Switch to English

Différenciation personnalisée

Voir sur TensorFlow.org Exécuter dans Google Colab Afficher la source sur GitHub

Ce didacticiel vous montrera comment définir vos propres dérivés personnalisés, effectuer une chirurgie dérivée et implémenter votre propre API de point de contrôle de gradient en seulement 5 lignes de Swift.

Déclaration de dérivés personnalisés

Vous pouvez définir des dérivés personnalisés pour toute fonction Swift ayant des paramètres et des résultats différentiables. En faisant cela, vous pouvez même importer une fonction C et la rendre différentiable.

import Glibc

func sillyExp(_ x: Float) -> Float {
    let 𝑒 = Float(M_E)
    print("Taking 𝑒(\(𝑒)) to the power of \(x)!")
    return pow(𝑒, x)
}

@derivative(of: sillyExp)
func sillyDerivative(_ x: Float) -> (value: Float, pullback: (Float) -> Float) {
    let y = sillyExp(x)
    return (value: y, pullback: { v in v * y })
}

print("exp(3) =", sillyExp(3))
print("𝛁exp(3) =", gradient(of: sillyExp)(3))
Taking 𝑒(2.7182817) to the power of 3.0!
exp(3) = 20.085535
Taking 𝑒(2.7182817) to the power of 3.0!
𝛁exp(3) = 20.085535

Empêcher les dérivés de se propager

Communément appelée "stop gradient" dans les cas d'utilisation du machine learning, la méthode withoutDerivative(at:) arrête la propagation des dérivés.

De plus, withoutDerivative(at:) peut parfois aider le compilateur Swift à identifier ce qu'il ne faut pas différencier et à produire des dérivés plus efficaces. Lorsqu'il est détectable que le dérivé d'une fonction sera toujours zéro, le compilateur Swift produira un avertissement. L'utilisation explicite de withoutDerivative(at:) taire cet avertissement.

let x: Float = 2.0
let y: Float = 3.0
gradient(at: x, y) { x, y in
    sin(sin(sin(x))) + withoutDerivative(at: cos(cos(cos(y))))
}
▿ 2 elements

  - .0 : -0.18009877
  - .1 : 0.0

Chirurgie dérivée

La méthode withDerivative(_:) exécute des opérations arbitraires (y compris la mutation) sur le gradient à une valeur pendant la rétropropagation de la fonction englobante.

Utilisez ceci pour déboguer ou apporter des modifications expérimentales à la rétropropagation.

Cela fonctionne partout

Toutes les API de différenciation fournies par la bibliothèque standard sont définies de manière générique sur tous les types conformes au protocole Differentiable : Float , Double , Float80 , vecteurs SIMD, et même vos propres types!

Lisez le document technique Types différentiables pour plus d'informations sur le protocole Differentiable .

var x: Float = 30
gradient(at: x) { x -> Float in
    // Print the partial derivative with respect to the result of `sin(x)`.
    let a = sin(x).withDerivative { print("∂+/∂sin = \($0)") } 
    // Force the partial derivative with respect to `x` to be `0.5`.
    let b = log(x.withDerivative { (dx: inout Float) in
        print("∂log/∂x = \(dx), but rewritten to 0.5");
        dx = 0.5
    })
    return a + b
}
∂log/∂x = 0.033333335, but rewritten to 0.5
∂+/∂sin = 1.0

0.65425146

Utilisez-le dans un module de réseau neuronal

Tout comme nous l'avons utilisé dans une simple fonction Float , nous pouvons l'utiliser dans n'importe quelle application numérique, comme le réseau neuronal suivant construit à l'aide de la bibliothèque d'apprentissage profond Swift pour TensorFlow .

import TensorFlow

struct MLP: Layer {
    var layer1 = Dense<Float>(inputSize: 2, outputSize: 10, activation: relu)
    var layer2 = Dense<Float>(inputSize: 10, outputSize: 1, activation: relu)
    
    @differentiable
    func callAsFunction(_ input: Tensor<Float>) -> Tensor<Float> {
        let h0 = layer1(input).withDerivative { print("∂L/∂layer1 =", $0) }
        return layer2(h0)
    }
}

var classifier = MLP()
let optimizer = SGD(for: classifier, learningRate: 0.02)

let x: Tensor<Float> = [[0, 0], [0, 1], [1, 0], [1, 1]]
let y: Tensor<Float> = [0, 1, 1, 0]

for _ in 0..<10 {
    let 𝛁model = gradient(at: classifier) { classifier -> Tensor<Float> in
        let ŷ = classifier(x).withDerivative { print("∂L/∂ŷ =", $0) }
        let loss = (ŷ - y).squared().mean()
        print("Loss: \(loss)")
        return loss
    }
    optimizer.update(&classifier, along: 𝛁model)
}
Loss: 0.33274758
∂L/∂ŷ = [[       -0.25],
 [  -0.0162234],
 [-0.105033934],
 [ 0.094616234]]
∂L/∂layer1 = [[          0.0,           0.0,           0.0,           0.0,           0.0,           0.0,
            0.0,           0.0,           0.0,           0.0],
 [-0.0056895884,    0.01067573,  -0.008025628, -0.0065082344,  0.0038597002,  0.0076597244,
   -0.008850923,  -0.008645675, 0.00091177685,  -0.003947232],
 [  -0.03683567,    0.06911708,  -0.051959712,   -0.04213577,   0.024988564,   0.049590774,
    -0.05730286,   -0.05597404,  0.0059030475,  -0.025555266],
 [   0.03318216,  -0.062261757,   0.046806134,    0.03795657,  -0.022510095,  -0.044672154,
    0.051619325,   0.050422303,  -0.005317559,   0.023020588]]
Loss: 0.33206546
∂L/∂ŷ = [[ -0.24949601],
 [-0.017163903],
 [ -0.10388964],
 [  0.09343198]]
∂L/∂layer1 = [[ -0.087382756,    0.16425455,   -0.12343603,  -0.099958554,   0.059345044,    0.11769368,
    -0.13612662,     -0.132847,   0.014168547,  -0.060705103],
 [ -0.006011435,   0.011299776,  -0.008491694,  -0.006876578,  0.0040826006,   0.008096653,
   -0.009364735,  -0.009139116, 0.00097471516,  -0.004176165],
 [    -0.036386,   0.068395264,  -0.051398512,   -0.04162254,   0.024711158,    0.04900741,
   -0.056682855,  -0.055317223,   0.005899754,  -0.025277482],
 [   0.03272334,  -0.061510514,   0.046224676,   0.037432764,  -0.022223702,  -0.044074263,
     0.05097709,   0.049748924,  -0.005305878,    0.02273302]]
Loss: 0.32871217
∂L/∂ŷ = [[  -0.2439641],
 [-0.013019085],
 [ -0.09845731],
 [   0.0965938]]
∂L/∂layer1 = [[ -0.085335776,    0.16068377,    -0.1207117,   -0.09762091,   0.058017734,    0.11498504,
    -0.13311927,   -0.12979509,   0.013993774,  -0.059361998],
 [ -0.004553923,    0.00857485, -0.0064417506, -0.0052095163,  0.0030961023,  0.0061361487,
  -0.0071038776,  -0.006926483,  0.0007467743, -0.0031678386],
 [  -0.03443921,    0.06484762,  -0.048715975,  -0.039397158,   0.023414386,    0.04640485,
   -0.053723335,   -0.05238178,  0.0056475084,  -0.023956895],
 [  0.033787373,   -0.06362024,   0.047793925,   0.038651485,   -0.02297122,  -0.045526538,
     0.05270651,   0.051390346, -0.0055406173,    0.02350346]]
Loss: 0.32567233
∂L/∂ŷ = [[  -0.2387768],
 [-0.009471901],
 [ -0.09363252],
 [  0.09900099]]
∂L/∂layer1 = [[  -0.08339106,    0.15736054,   -0.11814025,    -0.0954092,    0.05677393,    0.11244918,
    -0.13030054,   -0.12691614,   0.013847596,  -0.058072347],
 [-0.0033079926,  0.0062422454,  -0.004686438,  -0.003784733,  0.0022521326,   0.004460682,
  -0.0051688175,  -0.005034564,  0.0005493124,  -0.002303639],
 [  -0.03270048,   0.061706427,  -0.046326816,    -0.0374132,   0.022262992,    0.04409515,
   -0.051095277,  -0.049768142,  0.0054301145,  -0.022772146],
 [  0.034575377,    -0.0652444,    0.04898299,   0.039558303,  -0.023539452,   -0.04662337,
    0.054024853,   0.052621625,  -0.005741453,     0.0240778]]
Loss: 0.3228815
∂L/∂ŷ = [[ -0.23389274],
 [-0.006436102],
 [  -0.0893316],
 [  0.10076761]]
∂L/∂layer1 = [[  -0.08153905,    0.15425265,  -0.115705006,   -0.09331068,   0.055603556,    0.11006514,
     -0.1276478,   -0.12419132,   0.013724609,   -0.05683275],
 [-0.0022437365,    0.00424462,  -0.003183892,   -0.00256766,   0.001530061,   0.003028698,
   -0.003512526, -0.0034174128, 0.00037766452, -0.0015638851],
 [ -0.031142538,   0.058914337,   -0.04419168,  -0.035638522,    0.02123689,    0.04203762,
    -0.04875304,  -0.047432892,  0.0052418956,  -0.021706361],
 [   0.03512933,   -0.06645641,   0.049848992,   0.040200878,  -0.023955585,  -0.047419176,
     0.05499429,   0.053505134, -0.0059129503,   0.024485158]]
Loss: 0.32029176
∂L/∂ŷ = [[  -0.22927606],
 [-0.0038376972],
 [  -0.08548254],
 [  0.101991385]]
∂L/∂layer1 = [[   -0.07977117,     0.15133253,   -0.113391355,    -0.09131406,     0.05449789,
      0.10781483,    -0.12514149,    -0.12160411,    0.013620339,     -0.0556399],
 [ -0.0013352358,    0.002533053,  -0.0018979814,  -0.0015284444,   0.0009122034,
    0.0018046397,   -0.002094659,   -0.002035449,  0.00022798167, -0.00093131873],
 [  -0.029741623,    0.056422323,   -0.042276464,   -0.034045234,    0.020318815,
      0.04019733,   -0.046657346,   -0.045338478,    0.005078163,     -0.0207446],
 [   0.035485484,    -0.06731891,     0.05044112,    0.040620234,   -0.024242895,
    -0.047960456,    0.055668063,     0.05409449,  -0.0060588853,    0.024750907]]
Loss: 0.31786746
∂L/∂ŷ = [[  -0.22489586],
 [-0.0016133115],
 [   -0.0820235],
 [   0.10275632]]
∂L/∂layer1 = [[   -0.07807983,     0.14857662,   -0.111186594,    -0.08940941,    0.053449426,
      0.10568272,   -0.122764714,    -0.11914019,    0.013531086,    -0.05449069],
 [ -0.0005601129,   0.0010658281, -0.00079760735,  -0.0006413868,  0.00038342446,
   0.00075812486,  -0.0008806641, -0.00085466326,  9.7066506e-05, -0.00039089404],
 [  -0.028477093,     0.05418852,    -0.04055172,   -0.032609195,    0.019493952,
      0.03854436,    -0.04477446,   -0.043452535,    0.004935026,    -0.01987372],
 [   0.035675157,    -0.06788558,    0.050801847,    0.040851716,   -0.024421375,
    -0.048287094,     0.05609196,    0.054435894,  -0.0061824373,    0.024897136]]
Loss: 0.31558186
∂L/∂ŷ = [[ -0.22072542],
 [0.0002914667],
 [ -0.07890124],
 [ 0.103134096]]
∂L/∂layer1 = [[  -0.076458275,     0.14596471,     -0.1090796,    -0.08758796,    0.052451674,
      0.10365538,    -0.12050286,    -0.11678703,    0.013453777,   -0.053382203],
 [ 0.00010096274, -0.00019274562,  0.00014403902,  0.00011565942,  -6.926215e-05,
  -0.00013687638,  0.00015912336,  0.00015421664,  -1.776564e-05,   7.049091e-05],
 [  -0.027331028,     0.05217703,   -0.038991954,    -0.03130948,    0.018749548,
     0.037052996,    -0.04307535,    -0.04174708,    0.004809232,    -0.01908218],
 [    0.03572518,    -0.06820211,    0.050967515,    0.040925533,    -0.02450808,
    -0.048433047,     0.05630504,    0.054568816,  -0.0062862863,     0.02494287]]
Loss: 0.31341466
∂L/∂ŷ = [[ -0.21674162],
 [0.0019231141],
 [ -0.07607014],
 [  0.10318613]]
∂L/∂layer1 = [[  -0.074900545,     0.14347956,    -0.10706067,    -0.08584198,     0.05149903,
      0.10172125,     -0.1183433,    -0.11453373,    0.013385869,    -0.05231174],
 [  0.0006645807,  -0.0012730714,   0.0009499324,   0.0007616623, -0.00045694277,
   -0.0009025565,   0.0010500414,   0.0010162396, -0.00011877069,  0.00046415377],
 [  -0.026287958,    0.050357237,   -0.037575245,   -0.030128093,    0.018074695,
      0.03570126,   -0.041535128,   -0.040198077,   0.0046980586,   -0.018359931],
 [   0.035658576,     -0.0683076,    0.050969336,    0.040867567,   -0.024517607,
    -0.048427347,    0.056340758,      0.0545271,  -0.0063727307,     0.02490452]]
Loss: 0.31138343
∂L/∂ŷ = [[ -0.21300167],
 [0.0033213794],
 [ -0.07349101],
 [  0.10296476]]
∂L/∂layer1 = [[   -0.07342794,     0.14115742,    -0.10515943,   -0.084195204,    0.050604995,
      0.09990652,    -0.11631727,    -0.11241147,    0.013330075,   -0.051295396],
 [  0.0011449772,   -0.002201097,   0.0016397729,   0.0013128734, -0.00078909425,
   -0.0015578632,   0.0018137594,   0.0017528555, -0.00020785864,   0.0007998598],
 [  -0.025334511,     0.04870291,   -0.036282685,   -0.029049493,    0.017460015,
     0.034470297,   -0.040132426,   -0.038784824,    0.004599216,   -0.017698219],
 [    0.03549498,    -0.06823534,    0.050833948,    0.040699866,   -0.024462396,
    -0.048294697,    0.056227632,    0.054339573,   -0.006443743,    0.024796136]]

Recalculer les activations lors de la rétropropagation pour économiser de la mémoire (point de contrôle)

Le point de contrôle est une technique traditionnelle de différenciation automatique en mode inverse pour économiser la mémoire. Plutôt que d'enregistrer de grandes valeurs intermédiaires dans le calcul d'origine pour le calcul des dérivés, les valeurs intermédiaires sont recalculées au besoin lors de la rétropropagation.

Cette technique a également été réalisée dans des bibliothèques modernes d'apprentissage en profondeur. Dans Swift, l'API withRecomputationInPullbacks(_:) vous permet de contrôler ce qu'il faut recalculer pendant la rétropropagation, et elle est disponible sur tous Differentiable types Differentiable .

Mais aujourd'hui, apprenons à définir nos propres API de point de contrôle de gradient à partir de zéro, en seulement quelques lignes de code.

Notre API de point de contrôle de gradient

Nous pouvons définir notre propre API de point de contrôle de gradient, makeRecomputedInGradient(_:) , en termes de fonction de bibliothèque standard differentiableFunction(from:) , qui est un raccourci pour créer une fonction différentiable directement à partir d'une fonction dérivée (également appelée "produits vectoriels jacobiens (VJP) ").

Comme nous l'avons vu précédemment, la fonction dérivée retourne un tuple du résultat de la fonction d'origine et une fermeture de retrait. Nous retournons original(x) dans value: pullback(at:in:) et appelons pullback(at:in:) sur original pour évaluer à nouveau la fonction d'origine et obtenir un pullback.

/// Given a differentiable function, returns the same differentiable function except when
/// derivatives of this function are being computed. In that case, values in the original function needed
/// for computing the derivatives will be recomputed, instead of being captured by the differential or pullback.
///
/// - Parameter body: The body of the differentiable function.
/// - Returns: The same differentiable function whose derivatives, when computed, will recompute
///   some values from the original function.
func makeRecomputedInGradient<T: Differentiable, U: Differentiable>(
    _ original: @escaping @differentiable (T) -> U
) -> @differentiable (T) -> U {
    return differentiableFunction { x in
        (value: original(x), pullback: { v in pullback(at: x, in: original)(v) })
    }
}

Vérifiez que cela fonctionne

let input: Float = 10.0
print("Running original computation...")

// Differentiable multiplication with checkpointing.
let square = makeRecomputedInGradient { (x: Float) -> Float in
    print("  Computing square...")
    return x * x
}

// Differentiate `f(x) = (cos(x))^2`.
let (output, backprop) = valueWithPullback(at: input) { input -> Float in
    return square(cos(input))
}
print("Running backpropagation...")
let grad = backprop(1)
print("Gradient = \(grad)")
Running original computation...
  Computing square...
Running backpropagation...
  Computing square...
Gradient = -0.9129453

Étendez-le aux modules de réseau neuronal

Dans cet exemple, nous définissons un réseau de neurones convolutif simple.

struct Model: Layer {
    var conv = Conv2D<Float>(filterShape: (5, 5, 3, 6))
    var maxPool = MaxPool2D<Float>(poolSize: (2, 2), strides: (2, 2))
    var flatten = Flatten<Float>()
    var dense = Dense<Float>(inputSize: 36 * 6, outputSize: 10)

    @differentiable
    func call(_ input: Tensor<Float>) -> Tensor<Float> {
        return input.sequenced(through: conv, maxPool, flatten, dense)
    }
}

Nous voulons que les activations dans la couche de convolution ( conv ) soient recalculées lors de la rétropropagation. Cependant, l'utilisation de makeRecomputedInGradient(_:) peut rendre le code résultant encombrant, en particulier lorsque nous voulons appliquer des couches de manière séquentielle en utilisant sequenced(in:through:_:_:_:_:) .

input.sequenced(in: context, through: conv, maxPool, flatten, dense)

Alors, pourquoi ne définissons-nous pas un type de calque spécial qui enveloppe un calque et fait recalculer ses activations lors de la rétropropagation? Faisons le.

Tout d'abord, nous définissons une fonction makeRecomputedInGradient(_:) qui prend une fonction binaire.

// Same as the previous `makeRecomputedInGradient(_:)`, except it's for binary functions.
func makeRecomputedInGradient<T: Differentiable, U: Differentiable, V: Differentiable>(
    _ original: @escaping @differentiable (T, U) -> V
) -> @differentiable (T, U) -> V {
    return differentiableFunction { x, y in
        (value: original(x, y), pullback: { v in pullback(at: x, y, in: original)(v) })
    }
}

Ensuite, nous définissons une couche générique ActivationDiscarding<Wrapped> .

import TensorFlow

/// A layer wrapper that makes the underlying layer's activations be discarded during application
/// and recomputed during backpropagation.
struct ActivationDiscarding<Wrapped: Layer>: Layer {
    /// The wrapped layer.
    var wrapped: Wrapped

    @differentiable
    func callAsFunction(_ input: Wrapped.Input) -> Wrapped.Output {
        let apply = makeRecomputedInGradient { (layer: Wrapped, input: Input) -> Wrapped.Output in
            print("    Applying \(Wrapped.self) layer...")
            return layer(input)
        }
        return apply(wrapped, input)
    }
}

Enfin, nous pouvons ajouter une méthode sur tous les calques qui renvoie le même calque sauf que ses activations sont ignorées lors de l'application et recalculées lors de la rétropropagation.

extension Layer {
    func discardingActivations() -> ActivationDiscarding<Self> {
        return ActivationDiscarding(wrapped: self)
    }
}

De retour dans le modèle, tout ce que nous avons à changer est d'envelopper la couche de convolution dans la couche d'activation-recalcul.

var conv = Conv2D<Float>(filterShape: (5, 5, 3, 6)).discardingActivations()

Maintenant, utilisez-le simplement dans le modèle!

struct Model: Layer {
    var conv = Conv2D<Float>(filterShape: (5, 5, 3, 6)).discardingActivations()
    var maxPool = MaxPool2D<Float>(poolSize: (2, 2), strides: (2, 2))
    var flatten = Flatten<Float>()
    var dense = Dense<Float>(inputSize: 36 * 6, outputSize: 10)

    @differentiable
    func callAsFunction(_ input: Tensor<Float>) -> Tensor<Float> {
        return input.sequenced(through: conv, maxPool, flatten, dense)
    }
}

Lorsque nous exécutons une boucle d'apprentissage, nous pouvons voir que les activations de la couche de convolution sont calculées deux fois: une fois pendant l'application de la couche, et une fois pendant la rétropropagation.

// Use random training data.
let x = Tensor<Float>(randomNormal: [10, 16, 16, 3])
let y = Tensor<Int32>(rangeFrom: 0, to: 10, stride: 1)

var model = Model()
let opt = SGD(for: model)

for i in 1...5 {
    print("Starting training step \(i)")
    print("  Running original computation...")
    let (logits, backprop) = model.appliedForBackpropagation(to: x)
    let (loss, dL_dŷ) = valueWithGradient(at: logits) { logits in
        softmaxCrossEntropy(logits: logits, labels: y)
    }
    print("  Loss: \(loss)")
    print("  Running backpropagation...")
    let (dL_dθ, _) = backprop(dL_dŷ)
    
    opt.update(&model, along: dL_dθ)
}
Starting training step 1
  Running original computation...
    Applying Conv2D<Float> layer...
  Loss: 3.3274093
  Running backpropagation...
    Applying Conv2D<Float> layer...
Starting training step 2
  Running original computation...
    Applying Conv2D<Float> layer...
  Loss: 2.8839695
  Running backpropagation...
    Applying Conv2D<Float> layer...
Starting training step 3
  Running original computation...
    Applying Conv2D<Float> layer...
  Loss: 2.554683
  Running backpropagation...
    Applying Conv2D<Float> layer...
Starting training step 4
  Running original computation...
    Applying Conv2D<Float> layer...
  Loss: 2.2845771
  Running backpropagation...
    Applying Conv2D<Float> layer...
Starting training step 5
  Running original computation...
    Applying Conv2D<Float> layer...
  Loss: 2.0549595
  Running backpropagation...
    Applying Conv2D<Float> layer...

Juste comme ça, il est très facile de définir des bibliothèques de programmation génériques différentiables pour différents domaines.