Flux.jl

GitHub

Relax! Flux is the ML library that doesn't make you tensor

AI Prompts & Endpoints
CodeWiki Knowledge Base

Old Tutorials/2021 02 07 Convnet

Tutorial: A Simple ConvNet

In this tutorial, we build a simple Convolutional Neural Network (ConvNet) to classify the MNIST dataset. This model has a simple architecture with three feature detection layers (Conv -> ReLU -> MaxPool) followed by a final dense layer that classifies MNIST handwritten digits. Note that this model, while simple, should hit around 99% test accuracy after training for approximately 20 epochs.

This example writes out the saved model to the file mnist_conv.bson. Also, it demonstrates basic model construction, training, saving, conditional early-exit, and learning rate scheduling.

To run this example, we need the following packages:

julia
using Flux, MLDatasets, Statistics
using Flux: onehotbatch, onecold, logitcrossentropy, params
using MLDatasets: MNIST
using Base.Iterators: partition
using Printf, BSON
using CUDA
CUDA.allowscalar(false)


We set default values for learning rate, batch size, number of epochs, and path for saving the file mnist_conv.bson:

julia
Base.@kwdef mutable struct TrainArgs
lr::Float64 = 3e-3
epochs::Int = 20
batch_size = 128
savepath::String = "./"
end

Data

To train our model, we need to bundle images together with their labels and group them into mini-batches (makes the training process faster). We define the function make_minibatch that takes as inputs the images (X) and their labels (Y) as well as the indices for the mini-batches (idx):

julia
function make_minibatch(X, Y, idxs)
X_batch = Array{Float32}(undef, size(X)[1:end-1]..., 1, length(idxs))
for i in 1:length(idxs)
X_batch[:, :, :, i] = Float32.(X[:,:,idxs[i]])
end
Y_batch = onehotbatch(Y[idxs], 0:9)
return (X_batch, Y_batch)
end

make_minibatch takes the following steps:

* Creates the X_batch array of size 28x28x1x128 to store the mini-batches.
* Stores the mini-batches in X_batch.
* One hot encodes the labels of the images.
* Stores the labels in Y_batch.



get_processed_data loads the train and test data from Flux.Data.MNIST. First, it loads the images and labels of the train data set, and creates an array that contains the indices of the train images that correspond to each mini-batch (of size args.batch_size). Then, it calls the make_minibatch function to create all of the train mini-batches. Finally, it loads the test images and creates one mini-batch that contains them all.

julia
function get_processed_data(args)
# Load labels and images
train_imgs, train_labels = MNIST.traindata()
mb_idxs = partition(1:length(train_labels), args.batch_size)
train_set = [make_minibatch(train_imgs, train_labels, i) for i in mb_idxs]

# Prepare test set as one giant minibatch:
test_imgs, test_labels = MNIST.testdata()
test_set = make_minibatch(test_imgs, test_labels, 1:length(test_labels))

return train_set, test_set

end

Model



Now, we define the build_model function that creates a ConvNet model which is composed of three convolution layers (feature detection) and one classification layer. The input layer size is 28x28. The images are grayscale, which means there is only one channel (compared to 3 for RGB) in every data point. Combined together, the convolutional layer structure would look like Conv(kernel, input_channels => output_channels, ...). Each convolution layer reduces the size of the image by applying the Rectified Linear unit (ReLU) and MaxPool operations.
On the other hand, the classification layer outputs a vector of 10 dimensions (a dense layer), that is, the number of classes that the model will be able to predict.


julia
function build_model(args; imgsize = (28,28,1), nclasses = 10)
cnn_output_size = Int.(floor.([imgsize[1]/8,imgsize[2]/8,32]))

return Chain(
# First convolution, operating upon a 28x28 image
Conv((3, 3), imgsize[3]=>16, pad=(1,1), relu),
MaxPool((2,2)),

# Second convolution, operating upon a 14x14 image
Conv((3, 3), 16=>32, pad=(1,1), relu),
MaxPool((2,2)),

# Third convolution, operating upon a 7x7 image
Conv((3, 3), 32=>32, pad=(1,1), relu),
MaxPool((2,2)),

# Reshape 3d array into a 2d one using Flux.flatten, at this point it should be (3, 3, 32, N)
flatten,
Dense(prod(cnn_output_size), 10))
end

To chain the layers of a model we use the Flux function Chain. It enables us to call the layers in sequence on a given input. Also, we use the function flatten to reshape the output image from the last convolution layer. Finally, we call the Dense function to create the classification layer.

Training

Before training our model, we need to define a few functions that will be helpful for the process:

* augment adds gaussian random noise to our image, to make it more robust:
* anynan checks whether any element of the params is NaN or not:
* accuracy computes the proportion of inputs x correctly classified by our ConvNet:

julia
augment(x) = x .+ gpu(0.1f0*randn(eltype(x), size(x)))
anynan(x) = any(y -> any(isnan, y), x)
accuracy(x, y, model) = mean(onecold(cpu(model(x))) .== onecold(cpu(y)))

Finally, we define the train function:

text
/ Detailed source-code truncated for AI context efficiency. /


train calls the functions we defined above and trains our model. It stops when the model achieves 99% accuracy (early-exiting) or after performing 20 steps. More specifically, it performs the following steps:

* Loads the MNIST dataset.
* Builds our ConvNet model (as described above).
* Loads the train and test data sets as well as our model onto a GPU (if available).
* Defines a loss function that calculates the crossentropy between our prediction and the ground truth.
* Sets the Adam optimiser to train the model with learning rate args.lr.
* Runs the training loop. For each step (or epoch), it executes the following:
* Calls Flux.train! function to execute one training step.
* If any of the parameters of our model is NaN, then the training process is terminated.
* Calculates the model accuracy.
* If the model accuracy is >= 0.999, then early-exiting is executed.
* If the actual accuracy is the best so far, then the model is saved to mnist_conv.bson. Also, the new best accuracy and the current epoch is saved.
* If there has not been any improvement for the last 5 epochs, then the learning rate is dropped and the process waits a little longer for the accuracy to improve.
* If the last improvement was more than 10 epochs ago, then the process is terminated.


Testing

Finally, to test our model we define the test function:

julia
function test(; kws...)
args = TrainArgs(; kws...)

# Loading the test data
_,test_set = get_processed_data(args)

# Re-constructing the model with random initial weights
model = build_model(args)

# Loading the saved parameters
BSON.@load joinpath(args.savepath, "mnist_conv.bson") params

# Loading parameters onto the model
Flux.loadparams!(model, params)

test_set = gpu.(test_set)
model = gpu(model)
@show accuracy(test_set...,model)
end

test loads the MNIST test data set, reconstructs the model, and loads the saved parameters (in mnist_conv.bson) onto it. Finally, it computes our model's predictions for the test set and shows the test accuracy (around 99%).

To see the full version of this example, see Simple ConvNets - model-zoo.

Resources

* Neural Networks in Flux.jl with Huda Nassar (working with the MNIST dataset)
* Convolutional Neural Networks (CNNs / ConvNets).
* Convolutional Neural Networks Tutorial in PyTorch.

!!! info
Originally published at fluxml.ai on 7 February 2021.
Written by Elliot Saba, Adarsh Kumar, Mike J Innes, Dhairya Gandhi, Sudhanshu Agrawal, Sambit Kumar Dash, fps.io, Carlo Lucibello, Andrew Dinhobl, Liliana Badillo

---

Old Tutorials/2021 10 08 Dcgan Mnist

Deep Convolutional Generative Adversarial Network (DCGAN)

This is a beginner level tutorial for generating images of handwritten digits using a Deep Convolutional Generative Adversarial Network inspired by the TensorFlow tutorial on DCGAN.

What are GANs?


Generative Adversarial Neural Networks or simply GANs introduced by Goodfellow et al. is one of the most innovative ideas in modern-day machine learning. GANs are used extensively in the field of image and audio processing to generate high-quality synthetic data that can easily be passed off as real data.

A GAN is composed of two sub-models - the generator and the discriminator acting against one another. The generator can be considered as an artist who draws (generates) new images that look real, whereas the discriminator is a critic who learns to tell real images apart from fakes.

The GAN starts with a generator and discriminator which have very little or no idea about the underlying data. During training, the generator progressively becomes better at creating images that look real, while the discriminator becomes better at telling them apart. The process reaches equilibrium when the discriminator can no longer distinguish real images from fakes.


[[source]](https://www.tensorflow.org/tutorials/generative/dcgan)

This tutorial demonstrates the process of training a DC-GAN on the MNIST dataset for handwritten digits. The following animation shows a series of images produced by the generator as it was trained for 25 epochs. The images begin as random noise, but over time, the images become increasingly similar to handwritten numbers.

Setup

We need to install some Julia packages before we start with our implementation of DCGAN.

julia
using Pkg

Activate a new project environment in the current directory


Pkg.activate(".")

Add the required packages to the environment


Pkg.add(["Images", "Flux", "MLDatasets", "CUDA", "Parameters"])

Note: Depending on your internet speed, it may take a few minutes for the packages install.


After installing the libraries, load the required packages and functions:

julia
using Base.Iterators: partition
using Printf
using Statistics
using Random
using Images
using Flux: params, DataLoader
using Flux.Optimise: update!
using Flux.Losses: logitbinarycrossentropy
using MLDatasets: MNIST
using CUDA

Now we set default values for the learning rates, batch size, epochs, the usage of a GPU (if available) and other hyperparameters for our model.

julia
Base.@kwdef struct HyperParams
batch_size::Int = 128
latent_dim::Int = 100
epochs::Int = 25
verbose_freq::Int = 1000
output_dim::Int = 5
disc_lr::Float64 = 0.0002
gen_lr::Float64 = 0.0002
device::Function = gpu
end

Loading the data


As mentioned before, we will be using the MNIST dataset for handwritten digits. So we begin with a simple function for loading and pre-processing the MNIST images:
julia
function load_MNIST_images(hparams)
images = MNIST.traintensor(Float32)

# Normalize the images to (-1, 1)
normalized_images = @. 2f0 * images - 1f0
image_tensor = reshape(normalized_images, 28, 28, 1, :)

# Create a dataloader that iterates over mini-batches of the image tensor
dataloader = DataLoader(image_tensor, batchsize=hparams.batch_size, shuffle=true)

return dataloader
end


To learn more about loading images in Flux, you can check out this tutorial.

Note: The data returned from the dataloader is loaded is on the CPU. To train on the GPU, we need to transfer the data to the GPU beforehand.

Create the models


Generator

Our generator, a.k.a. the artist, is a neural network that maps low dimensional data to a high dimensional form.

- This low dimensional data (seed) is generally a vector of random values sampled from a normal distribution.
- The high dimensional data is the generated image.

The Dense layer is used for taking the seed as an input which is upsampled several times using the ConvTranspose layer until we reach the desired output size (in our case, 28x28x1). Furthermore, after each ConvTranspose layer, we apply the Batch Normalization to stabilize the learning process.

We will be using the relu activation function for each layer except the output layer, where we use tanh activation.

We will also apply the weight initialization method mentioned in the original DCGAN paper.

julia

Function for initializing the model weights with values


sampled from a Gaussian distribution with μ=0 and σ=0.02


dcgan_init(shape...) = randn(Float32, shape) * 0.02f0

julia
function Generator(latent_dim)
Chain(
Dense(latent_dim => 77256, bias=false),
BatchNorm(77256, relu),

x -> reshape(x, 7, 7, 256, :),

ConvTranspose((5, 5), 256 => 128; stride = 1, pad = 2, init = dcgan_init, bias=false),
BatchNorm(128, relu),

ConvTranspose((4, 4), 128 => 64; stride = 2, pad = 1, init = dcgan_init, bias=false),
BatchNorm(64, relu),

# The tanh activation ensures that output is in range of (-1, 1)
ConvTranspose((4, 4), 64 => 1, tanh; stride = 2, pad = 1, init = dcgan_init, bias=false),
)
end

Time for a small test!! We create a dummy generator and feed a random vector as a seed to the generator. If our generator is initialized correctly it will return an array of size (28, 28, 1, batch_size). The @assert macro in Julia will raise an exception for the wrong output size.

julia

Create a dummy generator of latent dim 100


generator = Generator(100)
noise = randn(Float32, 100, 3) # The last axis is the batch size

Feed the random noise to the generator


gen_image = generator(noise)
@assert size(gen_image) == (28, 28, 1, 3)

Our generator model is yet to learn the correct weights, so it does not produce a recognizable image for now. To train our poor generator we need its equal rival, the discriminator.

Discriminator

The Discriminator is a simple CNN based image classifier. The Conv layer a is used with a leakyrelu activation function.

julia
function Discriminator()
Chain(
Conv((4, 4), 1 => 64; stride = 2, pad = 1, init = dcgan_init),
x->leakyrelu.(x, 0.2f0),
Dropout(0.3),

Conv((4, 4), 64 => 128; stride = 2, pad = 1, init = dcgan_init),
x->leakyrelu.(x, 0.2f0),
Dropout(0.3),

# The output is now of the shape (7, 7, 128, batch_size)
flatten,
Dense(7 7 128, 1)
)
end


For a more detailed implementation of a CNN-based image classifier, you can refer to this tutorial.

Now let us check if our discriminator is working:

julia

Dummy Discriminator


discriminator = Discriminator()

We pass the generated image to the discriminator


logits = discriminator(gen_image)
@assert size(logits) == (1, 3)

Just like our dummy generator, the untrained discriminator has no idea about what is a real or fake image. It needs to be trained alongside the generator to output positive values for real images, and negative values for fake images.

Loss functions for GAN

In a GAN problem, there are only two labels involved: fake and real. So Binary CrossEntropy is an easy choice for a preliminary loss function.

But even if Flux's binarycrossentropy does the job for us, due to numerical stability it is always preferred to compute cross-entropy using logits. Flux provides logitbinarycrossentropy specifically for this purpose. Mathematically it is equivalent to binarycrossentropy(σ(ŷ), y, kwargs...).

Discriminator Loss

The discriminator loss quantifies how well the discriminator can distinguish real images from fakes. It compares

- discriminator's predictions on real images to an array of 1s, and
- discriminator's predictions on fake (generated) images to an array of 0s.

These two losses are summed together to give a scalar loss. So we can write the loss function of the discriminator as:

julia
function discriminator_loss(real_output, fake_output)
real_loss = logitbinarycrossentropy(real_output, 1)
fake_loss = logitbinarycrossentropy(fake_output, 0)
return real_loss + fake_loss
end

Generator Loss

The generator's loss quantifies how well it was able to trick the discriminator. Intuitively, if the generator is performing well, the discriminator will classify the fake images as real (or 1).

julia
generator_loss(fake_output) = logitbinarycrossentropy(fake_output, 1)

We also need optimisers for our network. Why you may ask? Read more here. For both the generator and discriminator, we will use the ADAM optimiser.

Utility functions

The output of the generator ranges from (-1, 1), so it needs to be de-normalized before we can display it as an image. To make things a bit easier, we define a function to visualize the output of the generator as a grid of images.

julia
function create_output_image(gen, fixed_noise, hparams)
fake_images = cpu(gen.(fixed_noise))
image_array = reduce(vcat, reduce.(hcat, partition(fake_images, hparams.output_dim)))
image_array = permutedims(dropdims(image_array; dims=(3, 4)), (2, 1))
image_array = @. Gray(image_array + 1f0) / 2f0
return image_array
end

Training

For the sake of simplifying our training problem, we will divide the generator and discriminator training into two separate functions.

julia
function train_discriminator!(gen, disc, real_img, fake_img, opt, ps, hparams)

disc_loss, grads = Flux.withgradient(ps) do
discriminator_loss(disc(real_img), disc(fake_img))
end

# Update the discriminator parameters
update!(opt, ps, grads)
return disc_loss
end

We define a similar function for the generator.

julia
function train_generator!(gen, disc, fake_img, opt, ps, hparams)

gen_loss, grads = Flux.withgradient(ps) do
generator_loss(disc(fake_img))
end

update!(opt, ps, grads)
return gen_loss
end

Now that we have defined every function we need, we integrate everything into a single train function where we first set up all the models and optimisers and then train the GAN for a specified number of epochs.

text
/ Detailed source-code truncated for AI context efficiency. /

Now we finally get to train the GAN:

julia

Define the hyper-parameters (here, we go with the default ones)


hparams = HyperParams()
train(hparams)

Output


The generated images are stored inside the output folder. To visualize the output of the generator over time, we create a gif of the generated images.

julia
folder = "output"

Get the image filenames from the folder


img_paths = readdir(folder, join=true)

Load all the images as an array


images = load.(img_paths)

Join all the images in the array to create a matrix of images


gif_mat = cat(images..., dims=3)
save("./output.gif", gif_mat)

Resources & References


- The DCGAN implementation in the Model Zoo.

!!! info
Originally published at fluxml.ai on 8 October 2021,
by Deeptendu Santra

---

Old Tutorials/2021 10 14 Vanilla Gan

Tutorial: Generative Adversarial Networks)

This tutorial describes how to implement a vanilla Generative Adversarial
Network using Flux and how train it on the MNIST dataset. It is based on this
Pytorch tutorial. The original GAN paper by Goodfellow et al. is a great resource that describes the motivation and theory behind GANs:

In the proposed adversarial nets framework, the generative model is pitted against an adversary: a

discriminative model that learns to determine whether a sample is from the model distribution or the

data distribution. The generative model can be thought of as analogous to a team of counterfeiters,

trying to produce fake currency and use it without detection, while the discriminative model is

analogous to the police, trying to detect the counterfeit currency. Competition in this game drives

both teams to improve their methods until the counterfeits are indistinguishable from the genuine

articles.

Let's implement a GAN in Flux. To get started we first import a few useful packages:


julia
using MLDatasets: MNIST
using Flux.Data: DataLoader
using Flux
using CUDA
using Zygote
using UnicodePlots

To download a package in the Julia REPL, type ] to enter package mode and then
type add MLDatasets or perform this operation with the Pkg module like this

julia
import Pkg

Pkg.add("MLDatasets")

While UnicodePlots is not necessary, it can be used to plot generated samples
into the terminal during training. Having direct feedback, instead of looking
at plots in a separate window, use fantastic for debugging.


Next, let us define values for learning rate, batch size, epochs, and other
hyper-parameters. While we are at it, we also define optimisers for the generator
and discriminator network. More on what these are later.

julia
lr_g = 2e-4          # Learning rate of the generator network
lr_d = 2e-4 # Learning rate of the discriminator network
batch_size = 128 # batch size
num_epochs = 1000 # Number of epochs to train for
output_period = 100 # Period length for plots of generator samples
n_features = 28 * 28# Number of pixels in each sample of the MNIST dataset
latent_dim = 100 # Dimension of latent space
opt_dscr = ADAM(lr_d)# Optimiser for the discriminator
opt_gen = ADAM(lr_g) # Optimiser for the generator


In this tutorial I'm assuming that a CUDA-enabled GPU is available on the
system where the script is running. If this is not the case, simply remove
the |>gpu decorators: piping.

Data loading


The MNIST data set is available from MLDatasets. The first time you instantiate it you will be prompted
if you want to download it. You should agree to this.

GANs can be trained unsupervised. Therefore only keep the images from the training
set and discard the labels.

After we load the training data we re-scale the data from values in [0:1]
to values in [-1:1]. GANs are notoriously tricky to train and this re-scaling
is a recommended GAN hack. The
re-scaled data is used to define a data loader which handles batching
and shuffling the data.

julia
# Load the dataset
train_x, _ = MNIST.traindata(Float32);
# This dataset has pixel values ∈ [0:1]. Map these to [-1:1]
train_x = 2f0 * reshape(train_x, 28, 28, 1, :) .- 1f0 |>gpu;
# DataLoader allows to access data batch-wise and handles shuffling.
train_loader = DataLoader(train_x, batchsize=batch_size, shuffle=true);


Defining the Networks


A vanilla GAN, the discriminator and the generator are both plain, feed-forward
multilayer perceptrons
. We use leaky rectified linear units leakyrelu to ensure out model is non-linear.

Here, the coefficient α (in the leakyrelu below), is set to 0.2. Empirically,
this value allows for good training of the network (based on prior experiments).
It has also been found that Dropout ensures a good generalization of the learned
network, so we will use that below. Dropout is usually active when training a
model and inactive in inference. Flux automatically sets the training mode when
calling the model in a gradient context. As a final non-linearity, we use the
sigmoid activation function.

julia
discriminator = Chain(Dense(n_features => 1024, x -> leakyrelu(x, 0.2f0)),
Dropout(0.3),
Dense(1024 => 512, x -> leakyrelu(x, 0.2f0)),
Dropout(0.3),
Dense(512 => 256, x -> leakyrelu(x, 0.2f0)),
Dropout(0.3),
Dense(256 => 1, sigmoid)) |> gpu

Let's define the generator in a similar fashion. This network maps a latent
variable (a variable that is not directly observed but instead inferred) to the
image space and we set the input and output dimension accordingly. A tanh squashes
the output of the final layer to values in [-1:1], the same range that we squashed
the training data onto.

julia
generator = Chain(Dense(latent_dim, 256, x -> leakyrelu(x, 0.2f0)),
Dense(256 => 512, x -> leakyrelu(x, 0.2f0)),
Dense(512 => 1024, x -> leakyrelu(x, 0.2f0)),
Dense(1024 => n_features, tanh)) |> gpu

Training functions for the networks

To train the discriminator, we present it with real data from the MNIST
data set and with fake data and reward it by predicting the correct labels for
each sample. The correct labels are of course 1 for in-distribution data
and 0 for out-of-distribution data coming from the generator.
Binary cross entropy
is the loss function of choice. While the Flux documentation suggests to use
Logit binary cross entropy,
the GAN seems to be difficult to train with this loss function.
This function returns the discriminator loss for logging purposes. We can
calculate the loss in the same call as evaluating the pullback and resort
to getting the pullback directly from Zygote instead of calling
Flux.train! on the model. To calculate the gradients of the loss
function with respect to the parameters of the discriminator we then only have to
evaluate the pullback with a seed gradient of 1.0. These gradients are used
to update the model parameters


julia
function train_dscr!(discriminator, real_data, fake_data)
this_batch = size(real_data)[end] # Number of samples in the batch
# Concatenate real and fake data into one big vector
all_data = hcat(real_data, fake_data)

# Target vector for predictions: 1 for real data, 0 for fake data.
all_target = [ones(eltype(real_data), 1, this_batch) zeros(eltype(fake_data), 1, this_batch)] |> gpu;

ps = Flux.params(discriminator)
loss, pullback = Zygote.pullback(ps) do
preds = discriminator(all_data)
loss = Flux.Losses.binarycrossentropy(preds, all_target)
end
# To get the gradients we evaluate the pullback with 1.0 as a seed gradient.
grads = pullback(1f0)

# Update the parameters of the discriminator with the gradients we calculated above
Flux.update!(opt_dscr, Flux.params(discriminator), grads)

return loss
end


Now we need to define a function to train the generator network. The job of the
generator is to fool the discriminator so we reward the generator when the discriminator
predicts a high probability for its samples to be real data. In the training function
we first need to sample some noise, i.e. normally distributed data. This has
to be done outside the pullback since we don't want to get the gradients with
respect to the noise, but to the generator parameters. Inside the pullback we need
to first apply the generator to the noise since we will take the gradient with respect
to the parameters of the generator. We also need to call the discriminator in order
to evaluate the loss function inside the pullback. Here we need to remember to deactivate
the dropout layers of the discriminator. We do this by setting the discriminator into
test mode before the pullback. Immediately after the pullback we set it back into training
mode. Then we evaluate the pullback, call it with a seed gradient of 1.0 as above, update the
parameters of the generator network and return the loss.


julia
function train_gen!(discriminator, generator)
# Sample noise
noise = randn(latent_dim, batch_size) |> gpu;

# Define parameters and get the pullback
ps = Flux.params(generator)
# Set discriminator into test mode to disable dropout layers
testmode!(discriminator)
# Evaluate the loss function while calculating the pullback. We get the loss for free
loss, back = Zygote.pullback(ps) do
preds = discriminator(generator(noise));
loss = Flux.Losses.binarycrossentropy(preds, 1.)
end
# Evaluate the pullback with a seed-gradient of 1.0 to get the gradients for
# the parameters of the generator
grads = back(1.0f0)
Flux.update!(opt_gen, Flux.params(generator), grads)
# Set discriminator back into automatic mode
trainmode!(discriminator, mode=:auto)
return loss
end

Training


Now we are ready to train the GAN. In the training loop we keep track
of the per-sample loss of the generator and the discriminator, where
we use the batch loss returned by the two training functions defined above.
In each epoch we iterate over the mini-batches given by the data loader.
Only minimal data processing needs to be done before the training functions
can be called.

julia
lossvec_gen = zeros(num_epochs)
lossvec_dscr = zeros(num_epochs)

for n in 1:num_epochs
loss_sum_gen = 0.0f0
loss_sum_dscr = 0.0f0

for x in train_loader
# - Flatten the images from 28x28xbatchsize to 784xbatchsize
real_data = flatten(x);

# Train the discriminator
noise = randn(latent_dim, size(x)[end]) |> gpu
fake_data = generator(noise)
loss_dscr = train_dscr!(discriminator, real_data, fake_data)
loss_sum_dscr += loss_dscr

# Train the generator
loss_gen = train_gen!(discriminator, generator)
loss_sum_gen += loss_gen
end

# Add the per-sample loss of the generator and discriminator
lossvec_gen[n] = loss_sum_gen / size(train_x)[end]
lossvec_dscr[n] = loss_sum_dscr / size(train_x)[end]

if n % output_period == 0
@show n
noise = randn(latent_dim, 4) |> gpu;
fake_data = reshape(generator(noise), 28, 4*28);
p = heatmap(fake_data, colormap=:inferno)
print(p)
end
end

For the hyper-parameters shown in this example, the generator produces useful
images after about 1000 epochs. And after about 5000 epochs the result look
indistinguishable from real MNIST data. Using a Nvidia V100 GPU on a 2.7
GHz Power9 CPU with 32 hardware threads, training 100 epochs takes about 80
seconds when using the GPU. The GPU utilization is between 30 and 40%.
To observe the network more frequently during training you can for example set
output_period=20. Training the GAN using the CPU takes about 10 minutes
per epoch and is not recommended.

Results

Below you can see what some of the images output may look like after different numbers of epochs.

Resources

* A collection of GANs in Flux
* Wikipedia
* GAN hacks

!!! info
Originally published at fluxml.ai on 14 October 2021,
by Ralph Kube.

---

Old Tutorials/2024 04 10 Blitz

Deep Learning with Julia & Flux: A 60 Minute Blitz

This is a quick intro to Flux loosely based on PyTorch's tutorial. It introduces basic Julia programming, as well Zygote, a source-to-source automatic differentiation (AD) framework in Julia. We'll use these tools to build a very simple neural network.

Arrays

The starting point for all of our models is the Array (sometimes referred to as a Tensor in other frameworks). This is really just a list of numbers, which might be arranged into a shape like a square. Let's write down an array with three elements.

julia
x = [1, 2, 3]

Here's a matrix – a square array with four elements.

julia
x = [1 2; 3 4]

We often work with arrays of thousands of elements, and don't usually write them down by hand. Here's how we can create an array of 5×3 = 15 elements, each a random number from zero to one.

julia
x = rand(5, 3)

There's a few functions like this; try replacing rand with ones, zeros, or randn to see what they do.

By default, Julia works stores numbers is a high-precision format called Float64. In ML we often don't need all those digits, and can ask Julia to work with Float32 instead. We can even ask for more digits using BigFloat.

julia
x = rand(BigFloat, 5, 3)

x = rand(Float32, 5, 3)

We can ask the array how many elements it has.

julia
length(x)

Or, more specifically, what size it has.

julia
size(x)

We sometimes want to see some elements of the array on their own.

julia
x

x[2, 3]

This means get the second row and the third column. We can also get every row of the third column.

julia
x[:, 3]

We can add arrays, and subtract them, which adds or subtracts each element of the array.

julia
x + x

x - x

Julia supports a feature called broadcasting, using the . syntax. This tiles small arrays (or single numbers) to fill bigger ones.

julia
x .+ 1

We can see Julia tile the column vector 1:5 across all rows of the larger array.

julia
zeros(5,5) .+ (1:5)

The x' syntax is used to transpose a column 1:5 into an equivalent row, and Julia will tile that across columns.

julia
zeros(5,5) .+ (1:5)'

We can use this to make a times table.

julia
(1:5) .* (1:5)'

Finally, and importantly for machine learning, we can conveniently do things like matrix multiply.

julia
W = randn(5, 10)
x = rand(10)
W * x

Julia's arrays are very powerful, and you can learn more about what they can do here.

CUDA Arrays

CUDA functionality is provided separately by the CUDA package. If you have a GPU and CUDA available, you can run ] add CUDA in a REPL or IJulia to get it.

Once CUDA is loaded you can move any array to the GPU with the cu function, and it supports all of the above operations with the same syntax.

julia
using CUDA
x = cu(rand(5, 3))

Automatic Differentiation

You probably learned to take derivatives in school. We start with a simple mathematical function like

julia
f(x) = 3x^2 + 2x + 1

f(5)

In simple cases it's pretty easy to work out the gradient by hand – here it's 6x+2. But it's much easier to make Flux do the work for us!

julia
using Flux: gradient

df(x) = gradient(f, x)[1]

df(5)

You can try this with a few different inputs to make sure it's really the same as 6x+2. We can even do this multiple times (but the second derivative is a fairly boring 6).

julia
ddf(x) = gradient(df, x)[1]

ddf(5)

Flux's AD can handle any Julia code you throw at it, including loops, recursion and custom layers, so long as the mathematical functions you call are differentiable. For example, we can differentiate a Taylor approximation to the sin function.

julia
mysin(x) = sum((-1)^k*x^(1+2k)/factorial(1+2k) for k in 0:5)

x = 0.5

mysin(x), gradient(mysin, x)

sin(x), cos(x)

You can see that the derivative we calculated is very close to cos(x), as we expect.

This gets more interesting when we consider functions that take arrays as inputs, rather than just a single number. For example, here's a function that takes a matrix and two vectors (the definition itself is arbitrary)

julia
myloss(W, b, x) = sum(W * x .+ b)

W = randn(3, 5)
b = zeros(3)
x = rand(5)

gradient(myloss, W, b, x)

Now we get gradients for each of the inputs W, b and x, which will come in handy when we want to train models.

ML models can contain hundreds of parameter arrays, therefore it is handy to group them into layers.
A layer is just a handy container for some parameters. For example, Dense does a linear transform for you.

julia
using Flux

m = Dense(10 => 5)

x = rand(Float32, 10)

We can easily get the parameters of any layer or model with trainables.

julia
Flux.trainables(m)

It very easy to calculate the gradient for all parameters in a network, even if it has many parameters.
The function gradient is not limited to array but can compute the gradient with respect to generic composite types.

julia
using Flux
using Flux: logitcrossentropy, trainables, getkeypath

x = rand(Float32, 10)
model = Chain(Dense(10 => 5, relu), Dense(5 => 2))
loss(model, x) = logitcrossentropy(model(x), [0.5, 0.5])
grad = gradient(m -> loss(m, x), model)[1]
for (k, p) in trainables(model, path=true)
println("$k => $(getkeypath(grad, k))")
end

You don't have to use layers, but they can be convenient for many simple kinds of models and fast iteration.

The next step is to update our weights and perform optimisation. As you might be familiar, Gradient Descent is a simple algorithm that takes the weights and steps using a learning rate and the gradients. weights = weights - learning_rate * gradient.

julia
η = 0.1
for (k, p) in trainables(model, path=true)
p .+= -η * getkeypath(grad, p)
end

While this is a valid way of updating our weights, it can get more complicated as the algorithms we use get more involved.

Flux comes with a bunch of pre-defined optimisers and makes writing our own really simple. We just give it the learning rate η:

julia
opt_state = Flux.setup(Descent(η), model)

Training a network reduces down to iterating on a dataset multiple times, performing these steps in order. Just for a quick implementation, let’s train a network that learns to predict 0.5 for every input of 10 floats. Flux defines the train! function to do it for us.

julia
data, labels = rand(10, 100), fill(0.5, 2, 100)
loss(m, x, y) = logitcrossentropy(m(x), y)
Flux.train!(loss, model, [(data, labels)], opt_state)

You don't have to use train!. In cases where arbitrary logic might be better suited, you could open up this training loop like so:

julia
for d in training_set # assuming d looks like (data, labels)
# our super logic
g = gradient(model) do model
l = loss(model, d...)
end[1]
Flux.update!(opt_state, model, g)
end

The do block is a closure, which is a way of defining a function inline. It's a very powerful feature of Julia, and you can learn more about it here.

Training a Classifier

Getting a real classifier to work might help cement the workflow a bit more. CIFAR10 is a dataset of 50k tiny training images split into 10 classes.

We will do the following steps in order:

* Load CIFAR10 training and test datasets
* Define a Convolution Neural Network
* Define a loss function
* Train the network on the training data
* Test the network on the test data

Loading the Dataset

julia
using Statistics
using Flux
using MLDatasets: CIFAR10
using ImageCore: colorview, RGB
using Flux: onehotbatch, onecold, DataLoader
using Plots: plot
using MLUtils: splitobs, numobs

using CUDA # Uncomment if you have CUDA installed. Can also use AMDGPU or Metal instead


using AMDGPU


using Metal

This image will give us an idea of what we are dealing with.

julia
train_x, train_y = CIFAR10(:train)[:]
labels = onehotbatch(train_y, 0:9)

The train_x contains 50000 images converted to 32 X 32 X 3 arrays with the third dimension being the 3 channels (R,G,B). Let's take a look at a random image from the train_x. For this, we need to permute the dimensions to 3 X 32 X 32 and use colorview to convert it back to an image.

julia
image(x) = colorview(RGB, permutedims(x, (3, 2, 1)))
plot(image(train_x[:,:,:,rand(1:end)]))

We can now arrange the training data in batches of say, 256 and keep a validation set to track our progress. This process is called minibatch learning, which is a popular method of training large neural networks. Rather that sending the entire dataset at once, we break it down into smaller chunks (called minibatches) that are typically chosen at random, and train only on them. It is shown to help with escaping saddle points.

The first 45k images (in batches of 256) will be our training set, and the rest is for validation.
The DataLoader function will help us load the data in batches.

julia
trainset, valset = splitobs((train_x, labels), at = 45000)
trainloader = DataLoader(trainset, batchsize = 1000, shuffle = true)
valloader = DataLoader(trainset, batchsize = 1000)

Defining the Classifier

Now we can define our Convolutional Neural Network (CNN).

A convolutional neural network is one which defines a kernel and slides it across a matrix to create an intermediate representation to extract features from. It creates higher order features as it goes into deeper layers, making it suitable for images, where the strucure of the subject is what will help us determine which class it belongs to.

julia
model = Chain(
Conv((5,5), 3 => 16, relu),
MaxPool((2, 2)),
Conv((5, 5), 16 => 8, relu),
MaxPool((2,2)),
x -> reshape(x, :, size(x, 4)),
Dense(200 => 120),
Dense(120 => 84),
Dense(84 => 10)) |> gpu

We will use a crossentropy loss and an Momentum optimiser here. Crossentropy will be a good option when it comes to working with mulitple independent classes. Momentum gradually lowers the learning rate as we proceed with the training. It helps maintain a bit of adaptivity in our optimisation, preventing us from over shooting from our desired destination.

julia
using Flux: logitcrossentropy, Momentum

loss(m, x, y) = logitcrossentropy(m(x), y)
opt_state = Flux.setup(Momentum(0.01), model)

We can start writing our train loop where we will keep track of some basic accuracy numbers about our model. We can define an accuracy function for it like so:

julia
function accuracy(model, loader)
n = 0
acc = 0
for batch in loader
x, y = batch |> gpu
ŷ = model(x)
acc += sum(onecold(ŷ) .== onecold(y))
n += numobs(x)
end
return acc / n
end

Training the Classifier


Training is where we do a bunch of the interesting operations we defined earlier, and see what our net is capable of. We will loop over the dataset 10 times and feed the inputs to the neural network and optimise.

julia
epochs = 10

for epoch in 1:epochs
for batch in trainloader
x, y = batch |> gpu
g = gradient(model) do m
loss(m, x, y)
end[1]
Flux.update!(opt_state, model, g)
end
@show accuracy(model, valloader)
end

Seeing our training routine unfold gives us an idea of how the network learnt the function. This is not bad for a small hand-written network, trained for a limited time.

Training on a GPU

The gpu functions you see sprinkled through this bit of the code tell Flux to move these entities to an available GPU, and subsequently train on it. No extra faffing about required! The same bit of code would work on any hardware with some small annotations like you saw here.

If you're not using CUDA, be sure to configure Flux.jl for your GPU backend.

Testing the Network

We have trained the network for 100 passes over the training dataset. But we need to check if the network has learnt anything at all.

We will check this by predicting the class label that the neural network outputs, and checking it against the ground-truth. If the prediction is correct, we add the sample to the list of correct predictions. This will be done on a yet unseen section of data.

Okay, first step. Let us perform the exact same preprocessing on this set, as we did on our training set.

julia
test_x, test_y = CIFAR10(:test)[:]
test_labels = onehotbatch(test_y, 0:9)
testloader = DataLoader((test_x, test_labels), batchsize = 1000, shuffle = true)

Next, display an image from the test set.

julia
plot(image(test_x[:,:,:,rand(1:end)]))

The outputs of the networks are (log)likelihoods for the 10 classes. Higher the energy for a class, the more the network thinks that the image is of the particular class. Every column corresponds to the output of one image, with the 10 floats in the column being the energies.

Let's see how the model fared.

julia
ids = rand(1:10000, 5)
rand_test = test_x[:,:,:,ids] |> gpu
rand_truth = test_y[ids]
model(rand_test)

This looks similar to how we would expect the results to be. At this point, it's a good idea to see how our net actually performs on new data, that we have prepared.

julia
accuracy(model, testloader)

This is much better than random chance set at 10% (since we only have 10 classes), and not bad at all for a small hand written network like ours.

Let's take a look at how the net performed on all the classes performed individually.

julia
confusion_matrix = zeros(Int, 10, 10)
m = model |> cpu
for batch in testloader
@show numobs(batch)
x, y = batch
preds = m(x)
ŷ = onecold(preds)
y = onecold(y)
for (yi, ŷi) in zip(y, ŷ)
confusion_matrix[yi, ŷi] += 1
end
end

confusion_matrix

The spread seems pretty good, with certain classes performing significantly better than the others. Why should that be?

!!! info
Originally published at fluxml.ai on 15 November 2020.
Written by Saswat Das, Mike Innes, Andrew Dinhobl, Ygor Canalli, Sudhanshu Agrawal, João Felipe Santos.

---

Old Tutorials/2024 04 10 Mlp

Tutorial: Simple Multi-layer Perceptron

In this example, we create a simple multi-layer perceptron%20is,artificial%20neural%20network%20(ANN).&text=An%20MLP%20consists%20of%20at,uses%20a%20nonlinear%20activation%20function.) (MLP) that classifies handwritten digits using the MNIST dataset. A MLP consists of at least three layers of stacked perceptrons: Input, hidden, and output. Each neuron of an MLP has parameters (weights and bias) and uses an activation function to compute its output.

To run this example, we need the following packages:

julia
using Flux, Statistics
using Flux: DataLoader
using Flux: onehotbatch, onecold, logitcrossentropy

using CUDA # Uncomment this line if you have a nvidia GPU. Also AMDGPU and Metal are supported.


using MLDatasets: MNIST
using MLUtils


We set default values for learning rate, batch size, epochs, and the usage of a GPU (if available) for our model:

julia
Base.@kwdef mutable struct Args
rate::Float64 = 3e-4 # learning rate
batchsize::Int = 1024 # batch size
epochs::Int = 10 # number of epochs
usegpu::Bool = true
end


If a GPU is available on our local system, then Flux uses it for computing the loss and updating the weights and biases when training our model.

Data

We create the function getdata to load the MNIST train and test data sets from MLDatasets and prepare them for the training process. In addition, we set mini-batches of the data sets by loading them onto a DataLoader object.

julia
function getdata(args)
ENV["DATADEPS_ALWAYS_ACCEPT"] = "true"

# Loading Dataset
xtrain, ytrain = MNIST(:train)[:]
xtest, ytest = MNIST(:test)[:]

# Reshape Data in order to flatten each image into a linear array
xtrain = Flux.flatten(xtrain)
xtest = Flux.flatten(xtest)

# One-hot-encode the labels
ytrain, ytest = onehotbatch(ytrain, 0:9), onehotbatch(ytest, 0:9)

# Batching
train_loader = DataLoader((xtrain, ytrain), batchsize=args.batchsize, shuffle=true)
test_loader = DataLoader((xtest, ytest), batchsize=args.batchsize)

return train_loader, test_loader
end


getdata performs the following steps:

* Loads MNIST data set: Loads the train and test set tensors. The shape of train data is 28x28x60000 and test data is 28X28X10000.
* Reshapes the train and test data: Uses the flatten function to reshape the train data set into a 784x60000 array and test data set into a 784x10000. Notice that we reshape the data so that we can pass these as arguments for the input layer of our model (a simple MLP expects a vector as an input).
* One-hot encodes the train and test labels: Creates a batch of one-hot vectors so we can pass the labels of the data as arguments for the loss function. For this example, we use the logitcrossentropy function and it expects data to be one-hot encoded.
* Creates batches of data: Creates two DataLoader objects (train and test) that handle data mini-batches of size 1024 (as defined above). We create these two objects so that we can pass the entire data set through the loss function at once when training our model. Also, it shuffles the data points during each iteration (shuffle=true).

Model

As we mentioned above, a MLP consist of three layers that are fully connected. For this example, we define out model with the following layers and dimensions:

* Input: It has 784 perceptrons (the MNIST image size is 28x28). We flatten the train and test data so that we can pass them as arguments to this layer.
* Hidden: It has 32 perceptrons that use the relu activation function.
* Output: It has 10 perceptrons that output the model's prediction or probability that a digit is 0 to 9.


We define our model with the build_model function:

julia
function build_model(; imgsize=(28,28,1), nclasses=10)
return Chain(
Dense(prod(imgsize) => 32, relu),
Dense(32 => nclasses))
end


Note that we use the functions Dense so that our model is densely (or fully) connected and Chain to chain the computation of the three layers.

Loss functions

Now, we define the loss function loss_all. It expects a DataLoader object and the model function we defined above as arguments. Notice that this function iterates through the DataLoader object in mini-batches and uses the function logitcrossentropy to compute the difference between the predicted and actual values.

julia
function loss_all(dataloader, model)
l = 0f0
n = 0
for (x, y) in dataloader
l += logitcrossentropy(model(x), y, agg=sum)
n += MLUtils.numobs(x)
end
return l / n
end


In addition, we define the function (accuracy) to report the accuracy of our model during the training process. To compute the accuracy, we need to decode the output of our model using the onecold function.

julia
function accuracy(dataloader, model)
acc = 0
n = 0
for (x, y) in dataloader
acc += sum(onecold(cpu(model(x))) .== onecold(cpu(y)))
n += MLUtils.numobs(x)
end
return acc / n
end


Train our model

Finally, we create the train function that calls the functions we defined and trains the model.

julia
function train(; kws...)
# Initializing Model parameters
args = Args(; kws...)

device = args.usegpu ? Flux.get_device() : Flux.get_device("CPU")

# Load Data
train_loader, test_loader = getdata(args)

# Construct model
model = build_model() |> device

loss(model, x, y) = logitcrossentropy(model(x), y)

## Training
opt_state = Flux.setup(Adam(args.rate), model)

for epoch in 1:args.epochs
@info "Epoch $epoch"
for d in train_loader
x, y = d |> device
g = gradient(m -> loss(m, x, y), model)[1]
Flux.update!(opt_state, model, g)
end
@show accuracy(train_loader, model)
@show accuracy(test_loader, model)
end
end

train()


train performs the following steps:

* Initializes the model parameters: Creates the args object that contains the default values for training our model.
* Loads the train and test data: Calls the function getdata we defined above.
* Constructs the model: Builds the model and loads the train and test data sets, and our model onto the GPU (if available).
* Trains the model: Sets Adam as the optimiser for training out model, runs the training process for 10 epochs (as defined in the args object) and shows the accuracy value for the train and test data.


To see the full version of this example, see Simple multi-layer perceptron - model-zoo.

Resources



* 3Blue1Brown Neural networks videos.
* Neural Networks and Deep Learning.

!!! info
Originally published at fluxml.ai on 26 January 2021.
Written by Adarsh Kumar, Mike J Innes, Andrew Dinhobl, Jerry Ling, natema, Zhang Shitian, Liliana Badillo, Dhairya Gandhi

---

Old Tutorials/README

These tutorials are hard to mantain
and overlapping with model-zoo examples.

Some of the tutorials are outdated.

Mantainance would be simplified by moving them
to Literate.jl and CI testing them.

---

Src/Guide/Models/Basics

How Flux Works: Parameters, Gradients, and Layers

A neural network is a function with parameters.
That is, it takes some input x and gives you some output y,
whose value also depends on some other numbers θ.

A sufficiently flexible function can, by adjusting the parameters just right,
be made to do many things. And the one magic trick for adjusting parameters
is to follow a gradient.

This page describes Flux's take on how to construct such flexible functions
containing many parameters, and how to handle their gradients.

Parameterised Functions

Let's start with very simple functions. This is a polynomial in x::Real,
returning another real number y which depends on some coefficients stored in a vector:

``jldoctest poly; output = false
θ = [10, 1, 0.1]

poly1(x::Real) = θ[1] + θ[2]x + θ[3]x^2

poly1(5) == 17.5 # true

output

true

text
Here the parameters are a global variable θ. They could be handled in other ways,
for instance by explicitly passing them as an additional argument to the function:
jldoctest poly; output = false
poly2(x::Real, θ2) = evalpoly(x, θ2) # built-in, from Base.Math

poly2(5, θ) == 17.5 # true

output

true

text
Flux chooses a third path, by encapsulating the parameters within the function.
The simplest way to do this is a closure, an anonymous function which Julia knows
to depend on some local variable
θ3:
jldoctest poly; output = false
poly3 = let θ3 = [10, 1, 0.1]
x -> evalpoly(x, θ3)
end

poly3(5) == 17.5 # true

output

true

text
An equivalent, but tidier, way is to construct a struct in which to store the parameters.
Any struct can be made callable, allowing its instances to act just like function:
jldoctest poly; output = false
struct Poly3{T} # container struct
θ3::T
end
(p::Poly3)(x::Real) = evalpoly(x, p.θ3) # make this callable

poly3s = Poly3([10, 1, 0.1]) # construct an instance

poly3s(5) == 17.5 # true

output

true

text
Internally, there is little difference between a closure and a struct.
They have the same fields, and equivalent methods:
julia
dump(poly3), dump(poly3s) # both contain θ3: Array
poly3s.θ3 == poly3.θ3 == θ # field called :θ3 has same value
methods(poly3)
methods(poly3s) # each has 1 method, accepting x
text
The virtue of encapsulation is that it makes composition very easy.
We can make more complicated functions by combining simple ones,
and each will keep track of its own parameters.
Juia writes function composition as
, for instance (inv ∘ sin)(pi/6) ≈ 2,
and we can use exactly this for our parameterised polynomials:
jldoctest poly; output = false
poly4 = Poly3([1, 0.5, 0]) ∘ Poly3([10, 1, 0.1])

poly4 isa ComposedFunction # ∘ creates another struct...
poly4.outer.θ3 == θ # which has fields :inner & :outer

poly4(5) == 9.75 # true

output

true

text
Flux models are precisely made by such function composition.
In fact,
poly3 and poly4 are already valid Flux models.


Structural Gradients

The derivative of a scalar function is its slope: how fast the output changes as the input is changed slightly.
This may be found approximately by evaluating at two nearby points, and exactly by taking the limit in
which the distance between them approaches zero:

jldoctest poly
julia> (poly1(5 + 0.1) - poly1(5)) / 0.1
2.010000000000005

julia> (poly1(5 + 0.001) - poly1(5)) / 0.001 # answer is getting close to 2
2.000100000003613

text
Flux's gradient(f, x) works this out for f(x), and gives exactly ∂f/∂x = 2.0 here:
jldoctest poly
julia> using Flux

julia> gradient(poly1, 5)
(2.0,)

text
The reason gradient returns a tuple, not just the number 2.0, is to allow for
functions taking several arguments. (That's also why it's not called "derivative".)
For instance, this returns
∂f/∂x, ∂f/∂y, ∂f/∂z:
jldoctest poly
julia> gradient((x,y,z) -> (x*y)+z, 30, 40, 50)
(40.0, 30.0, 1.0)
text
For our parameterised polynomial, we have ∂f/∂x but we are really more interested
in
∂f/∂θ, as this will tell us about how the parameters are affecting the answer.
It is not impossible to track gradients with respect to global
θ, but much clearer to track explicit arguments.
Here's how this works for
poly2 (which takes θ as a 2nd argument) and poly3 (which encapsulates θ):
jldoctest poly
julia> grad2 = gradient(poly2, 5, θ)
(2.0, [1.0, 5.0, 25.0])

julia> grad3 = gradient((x,p) -> p(x), 5, poly3s)
(2.0, (θ3 = [1.0, 5.0, 25.0],))

text
The first entry is ∂f/∂x as before, but the second entry is more interesting.
For
poly2, we get ∂f/∂θ as grad2[2] directly.
It is a vector, because
θ is a vector, and has elements [∂f/∂θ[1], ∂f/∂θ[2], ∂f/∂θ[3]].

For poly3s, however, we get a NamedTuple whose fields correspond to those of the struct Poly3.
This is called a structural gradient. And the nice thing about them is that they work for
arbitrarily complicated structures, for instance:

jldoctest poly
julia> grad4 = gradient(|>, 5, poly4)
(1.0, (outer = (θ3 = [1.0, 17.5, 306.25],), inner = (θ3 = [0.5, 2.5, 12.5],)))
text
Here grad4[2].inner.θ3 corresponds to poly4.inner.θ3.
These matching nested structures are at the core of how Flux works.

!!! note "Implicit gradients"
Earlier versions of Flux used a different way to relate parameters and gradients,
which looks like this:

julia
g1 = gradient(() -> poly1(5), Params([θ]))
g1[θ] == [1.0, 5.0, 25.0]
text
Here Params is a set of references to global variables using objectid,
and
g1 isa Grads is a dictionary from these to their gradients.
This method of
gradient takes a zero-argument function, which only implicitly
depends on
θ.

Automatic Differentiation

Flux's gradient function by default calls a companion packages called Zygote.
Zygote performs source-to-source automatic differentiation, meaning that
gradient(f, x)
hooks into Julia's compiler to find out what operations
f contains, and transforms this
to produce code for computing
∂f/∂x.

Zygote can in principle differentiate almost any Julia code. However, it's not perfect,
and you may eventually want to read its page about limitations.
In particular, a major limitation is that mutating an array is not allowed.

Flux can also be used with other automatic differentiation (AD) packages.
It was originally written using Tracker, a more traditional operator-overloading approach.
The future might be Enzyme, and Flux now builds in an easy way to use this instead, turned on by wrapping the model in
Duplicated. (For details, see the Enzyme page in the manual.)

julia-repl
julia> using Enzyme: Const, Duplicated

julia> grad3e = Flux.gradient((x,p) -> p(x), Const(5.0), Duplicated(poly3s))
(nothing, (θ3 = [1.0, 5.0, 25.0],))

text
Flux.gradient follows Zygote's convention that arguments with no derivative are marked nothing.
Here, this is because
Const(5.0) is explicitly constant.
Below, we will see an example where
nothing shows up because the model struct has fields containing things other than parameters, such as an activation function.
(It also adopts the convention that
gradient(f, x, y) returns a tuple (∂f/∂x, ∂f/∂y), without a "∂f/∂f" term for the function. This is why we had to write gradient(|>, 5, poly4) above, not just gradient(poly4, 5).)

The function withgradient works the same way, but also returns the value of the function:

jldoctest poly
julia> Flux.withgradient((x,p) -> p(x), 5.0, poly3s)
(val = 17.5, grad = (2.0, (θ3 = [1.0, 5.0, 25.0],)))
text
One can also directly specify which AD backend to use, by passing an adtype among the supported ones
(
AutoMooncake, AutoEnzyme, AutoZygote, AutoFiniteDifferences) as the second argument.
The corresponding AD package has to be loaded first.

Here is an example using Mooncake:

jldoctest poly
julia> using Mooncake

julia> Flux.withgradient((x,p) -> p(x), AutoMooncake(), 5.0, poly3s)
(val = 17.5, grad = (2.0, (θ3 = [1.0, 5.0, 25.0],)))

text
and here is the same example using Enzyme:
julia-repl
julia> using Enzyme

julia> Flux.withgradient((x,p) -> p(x), AutoEnzyme(), 5.0, poly3s)
(val = 17.5, grad = (2.0, Poly3{Vector{Float64}}([1.0, 5.0, 25.0])))

text

Simple Neural Networks

The polynomial functions above send a number x to another a number y.
Neural networks typically take a vector of numbers, mix them all up, and return another vector.
Here's a very simple one, which will take a vector like
x = [1.0, 2.0, 3.0]
and return another vector
y = layer1(x) with length(y) == 2:

jldoctest poly; output = false
W = randn(2, 3)
b = zeros(2)

sigmoid(x::Real) = 1 / (1 + exp(-x))
layer1(x) = sigmoid.(W*x .+ b)

output

layer1 (generic function with 1 method)

text
Here sigmoid is a nonlinear function, applied element-wise
because it is called with
.(), called broadcasting.

Like poly1 above, this layer1 has as its parameters the global variables W, b.
We can similarly define a version which takes these as arguments (like
poly2),
and a version which encapsulates them (like
poly3 above):

jldoctest poly; output = false
layer2(x, W2, b2) = sigmoid.(W2*x .+ b2) # explicit parameter arguments

layer3 = let
W3 = randn(2, 3)
b3 = zeros(2)
x -> sigmoid.(W3*x .+ b3) # closure over local variables
end

layer3([1.0, 2.0, 3.0]) isa Vector # check that it runs

output

true

text
This third way is precisely a Flux model. And we can again make a tidier version
using a
struct to hold the parameters:
jldoctest poly; output = false, filter = r"[+-]?([0-9][.])?[0-9]+(f[+-][0-9])?"
struct Layer # container struct
W::Matrix
b::Vector
act::Function
end

(d::Layer)(x) = d.act.(d.W*x .+ d.b) # make it callabale

Layer(in::Int, out::Int, act::Function=sigmoid) =
Layer(randn(Float32, out, in), zeros(Float32, out), act)

layer3s = Layer(3, 2) # instance with its own parameters

output

Layer(Float32[0.6911411 0.47683495 -0.75600505; 0.5247729 1.2508286 0.27635413], Float32[0.0, 0.0], sigmoid)

text
The one new thing here is a friendly constructor Layer(in, out, act).
This is because we anticipate composing several instances of this thing,
with independent parameter arrays, of different sizes and different
random initial parameters.

Let's try this out, and look at its gradient:

jldoctest poly; output = false, filter = r"[+-]?([0-9][.])?[0-9]+(f[+-][0-9])?"
x = Float32[0.1, 0.2, 0.3] # input

layer3s(x) # output, 2-element Vector{Float32}

Flux.gradient((x,d) -> d(x)[1], x, layer3s)[2] # NamedTuple{(:W, :b, :act)}

output

(W = Float32[0.024975738 0.049951475 0.07492722; 0.0 0.0 0.0], b = Float32[0.24975738, 0.0], act = nothing)

text
This ∂f/∂layer3s is a named tuple with the same fields as Layer.
Within it, the gradient with respect to
W is a matrix of seemingly random numbers.
Notice that there is also an entry for
act, which is nothing,
as this field of the struct is not a smoothly adjustable parameter.

We can compose these layers just as we did the polynomials above, in poly4.
Here's a composition of 3 functions, in which the last step is the function
only
which takes a 1-element vector and gives us the number inside:

jldoctest poly; output = false, filter = r"[+-]?([0-9][.])?[0-9]+(f[+-][0-9])?"
model1 = only ∘ Layer(20, 1, identity) ∘ Layer(1, 20)

y = model1(Float32[0.1]) # output is a Float32 number

grad = Flux.gradient(|>, [1f0], model1)[2]

output

(outer = (outer = nothing, inner = (W = Float32[0.058179587 0.1276911 … 0.08071162 0.034993216], b = Float32[0.14223717], act = nothing)), inner = (W = Float32[-0.048111934; -0.0008379104; … ; 0.017658396; -0.015104223;;], b = Float32[-0.048111934, -0.0008379104, 0.017207285, 0.026828118, -0.024858447, -0.015956078, 0.0020494608, -0.012577536, -0.044770215, 0.01478136, 0.034534186, -0.004748393, 0.026848236, -0.016794706, -0.041044597, 0.016186379, -0.036814954, 0.034786277, 0.017658396, -0.015104223], act = nothing))

text
This gradient is starting to be a complicated nested structure.
But it works just like before:
grad.outer.inner.W corresponds to model1.outer.inner.W.

We don't have to use (which makes a ComposedFunction struct) to combine layers.
Instead, we could define our own container struct, or use a closure.
This
model2 will work the same way (although its fields have different names):

jldoctest poly; output = false, filter = r"[+-]?([0-9][.])?[0-9]+(f[+-][0-9])?"
model2 = let
lay1 = Layer(1, 20) # local variables containing layers
lay2 = Layer(20, 1, identity)
function fwd(x) # equivalent to x -> only(lay2(lay1(x)))
mid = lay1(x)
lay2(mid) |> only
end
end

model2(Float32[0.1])

Flux.gradient(|>, [1f0], model2)[2]

output

(lay2 = (W = Float32[0.051824596 0.03971491 … 0.038365345 0.051143322], b = Float32[0.09477656], act = nothing), lay1 = (W = Float32[-0.00049770635; 0.002891017; … ; -0.0022540581; 0.0039325757;;], b = Float32[-0.00049770635, 0.002891017, -0.00865399, -0.015051818, -0.005504916, -0.004188145, -0.01533527, -0.0059600063, -0.003092169, -0.00697084, -0.012470333, -0.0048766206, -0.010671042, -0.006604657, -0.0086712, -0.0044975257, -0.0028462198, -0.009992857, -0.0022540581, 0.0039325757], act = nothing))

text
@raw html
<h3><img src="https://github.com/FluxML/Optimisers.jl/blob/master/docs/src/assets/logo.png?raw=true" width="40px"/>&nbsp;<a href="../../../reference/models/layers/">Flux's layers</a></h3>
text
Rather than define everything from scratch every time, Flux provides a library of
commonly used layers. The same model could be defined:
jldoctest poly; output = false
model3 = Chain(Dense(1 => 20, σ), Dense(20 => 1), only)

output

Chain(
Dense(1 => 20, σ), # 40 parameters
Dense(20 => 1), # 21 parameters
only,
) # Total: 4 arrays, 61 parameters, 452 bytes.

text
How does this model3 differ from the model1 we had before?

* Flux's Chain works left-to-right, the reverse of Base's .
Its contents is stored in a tuple, thus
model3.layers[1].weight is an array.
* Flux's layer
Dense has only minor differences from our struct Layer:
- Like
struct Poly3{T} above, it has type parameters for its fields -- the compiler does not know exactly what type layer3s.W will be, which costs speed.
- Its initialisation uses not
randn (normal distribution) but glorot_uniform by default.
- It reshapes some inputs (to allow several batch dimensions), and produces more friendly errors on wrong-size input.
- And it has some performance tricks: making sure element types match, and re-using some memory.
* The function
σ is calculated in a slightly better way,
and has a rule telling Zygote how to differentiate it efficiently.
* Flux overloads
Base.show so to give pretty printing at the REPL prompt.
Calling
Flux.@layer Layer will add this, and some other niceties.

All Flux layers accept a batch of samples: Instead of mapping one sample x::Vector to one output y::Vector, they map columns of a matrix xs::Matrix to columns of the output. This looks like f(xs) ≈ stack(f(x) for x in eachcol(xs)) but is done more efficiently.

If what you need isn't covered by Flux's built-in layers, it's easy to write your own.
There are more details later, but the steps are invariably those shown for
struct Layer above:
1. Define a
struct which will hold the parameters.
2. Make it callable, to define how it uses them to transform the input
x
3. Define a constructor which initialises the parameters (if the default constructor doesn't do what you want).
4. Annotate with
@layer to opt-in to pretty printing, and other enhancements.

@raw html
<h3><img src="https://github.com/FluxML/Optimisers.jl/blob/master/docs/src/assets/logo.png?raw=true" width="40px"/>&nbsp;<a href="https://github.com/FluxML/Functors.jl">Functors.jl</a></h3>
text
To deal with such nested structures, Flux relies heavily on an associated package
called Functors. Its basic function is
fmap,
which generalises
map(f, x) to work on almost anything.

For example, this is how gpu moves all arrays within a model to the GPU,
reconstructing another
only ∘ Layer(...) ∘ Layer(...) (or a Chain etc.) around the new CuArrays:

julia
using CUDA, Functors
fmap(cu, model1)
text
And this is a very simple gradient update of the parameters, walking over model and grad simultaneously:
julia
fmap((x, dx) -> x isa Array ? (x - dx/100) : x, model, grad)
text
!!! note
Before Flux v0.15 (and Functors v0.5), this exploration of structs was opt-in.
After defining
struct Layer it was necessary to call @functor Layer (or @layer Layer) before Flux would look inside.
This has now changed to be opt-out: Functors (and hence Flux) will explore arbitrary structs, unless told not to (using
Functors.@leaf).
This is why even "anonymous structs" created by closures, like
poly3 and layer3 above, are now valid Flux models, although the use of named structs is still recommended practice.

Curve Fitting

Above we took gradients of the output, or sometimes to the first element
of the output -- it must be a number, not a vector. Adjusting the parameters
to make this smaller won't lead us anywhere interesting. Instead, we should minimise
some loss function which compares the actual output to our desired output.

Perhaps the simplest example is curve fitting. The previous page fitted
a linear model to data. With our two-layer model, we can fit a nonlinear function.
For example, let us use
f(x) = 2x - x^3 evaluated at some points x in -2:0.1:2 as the data,
and adjust the parameters of
model3 from above so that its output is similar.

jldoctest poly; output = false
data = [([x], 2x-x^3) for x in -2:0.1f0:2] # training points (x, y)

for _ in 1:1000 # adjust parameters to minimise the error:
Flux.train!((m,x,y) -> (m(x) - y)^2, model3, data, Descent(0.01))
end

output

text
The same code will also work with model1 or model2 instead.
Here's how to plot the desired and actual outputs:
julia
using Plots
plot(x -> 2x-x^3, -2, 2, label="truth")
scatter!(x -> model3([x]), -2:0.1f0:2, label="fitted")
text
More detail about what exactly the function train! is doing, and how to use rules other than simple Descent, is what the next page in this guide is about: training.

---

Src/Guide/Models/Overview

Flux Overview: Fitting a Straight Line

Flux is a pure Julia ML stack that allows you to build predictive models. Here are the steps for a typical Flux program:

1. Provide training and test data
2. Build a model with configurable parameters to make predictions
3. Iteratively train the model by tweaking the parameters to improve predictions
4. Verify your model

Under the hood, Flux uses a technique called automatic differentiation to take gradients that help improve predictions. Flux is also fully written in Julia so you can easily replace any layer of Flux with your own code to improve your understanding or satisfy special requirements.

Here's how you'd use Flux to build and train the most basic of models, step by step.

A Trivial Prediction

This example will predict the output of the function 4x + 2. Making such predictions is called "linear regression", and is really too simple to need a neural network. But it's a nice toy example.

First, import Flux and define the function we want to simulate:

jldoctest overview
julia> using Flux

julia> actual(x) = 4x + 2
actual (generic function with 1 method)

text
This example will build a model to approximate the actual function.

1. Provide Training and Test Data

Use the actual function to build sets of data for training and verification:

jldoctest overview
julia> x_train, x_test = hcat(0:5...), hcat(6:10...)
([0 1 … 4 5], [6 7 … 9 10])

julia> y_train, y_test = actual.(x_train), actual.(x_test)
([2 6 … 18 22], [26 30 … 38 42])

text
Normally, your training and test data come from real world observations, but here we simulate them.

2. Build a Model to Make Predictions

Now, build a model to make predictions with 1 input and 1 output:

jldoctest overview; filter = r"[+-]?([0-9][.])?[0-9]+(f[+-][0-9])?"
julia> model = Dense(1 => 1)
Dense(1 => 1) # 2 parameters

julia> model.weight
1×1 Matrix{Float32}:
0.95041317

julia> model.bias
1-element Vector{Float32}:
0.0

text
Under the hood, a dense layer is a struct with fields weight and bias. weight represents a weights' matrix and bias represents a bias vector. There's another way to think about a model. In Flux, models are conceptually predictive functions:
jldoctest overview
julia> predict = Dense(1 => 1)
Dense(1 => 1) # 2 parameters
text
Dense(1 => 1) also implements the function σ(Wx+b) where W and b are the weights and biases. σ is an activation function (more on activations later). Our model has one weight and one bias, but typical models will have many more. Think of weights and biases as knobs and levers Flux can use to tune predictions. Activation functions are transformations that tailor models to your needs. 

This model will already make predictions, though not accurate ones yet:

jldoctest overview; filter = r"[+-]?([0-9][.])?[0-9]+(f[+-][0-9])?"
julia> predict(x_train)
1×6 Matrix{Float32}:
0.0 0.906654 1.81331 2.71996 3.62662 4.53327
text
In order to make better predictions, you'll need to provide a loss function to tell Flux how to objectively evaluate the quality of a prediction. Loss functions compute the cumulative distance between actual values and predictions.
jldoctest overview; filter = r"[+-]?([0-9][.])?[0-9]+(f[+-][0-9])?"
julia> using Statistics

julia> loss(model, x, y) = mean(abs2.(model(x) .- y));

julia> loss(predict, x_train, y_train)
122.64734f0

text
More accurate predictions will yield a lower loss. You can write your own loss functions or rely on those already provided by Flux. This loss function is called mean squared error (and built-in as mse). Flux works by iteratively reducing the loss through training.

3. Improve the Prediction

Under the hood, the Flux Flux.train! function uses a loss function and training data to improve the parameters of your model based on a pluggable optimiser:

jldoctest overview
julia> using Flux: train!

julia> opt = Descent()
Descent(0.1f0)

julia> data = [(x_train, y_train)]
1-element Vector{Tuple{Matrix{Int64}, Matrix{Int64}}}:
([0 1 … 4 5], [2 6 … 18 22])

text
Now, we have the optimiser and data we'll pass to train!. All that remains are the parameters of the model. Remember, each model is a Julia struct with a function and configurable parameters. Remember, the dense layer has weights and biases that depend on the dimensions of the inputs and outputs:
jldoctest overview; filter = r"[+-]?([0-9][.])?[0-9]+(f[+-][0-9])?"
julia> predict.weight
1×1 Matrix{Float32}:
0.9066542

julia> predict.bias
1-element Vector{Float32}:
0.0

text
The dimensions of these model parameters depend on the number of inputs and outputs.

Flux will adjust predictions by iteratively changing these parameters according to the optimiser.

This optimiser implements the classic gradient descent strategy. Now improve the parameters of the model with a call to Flux.train! like this:

jldoctest overview
julia> train!(loss, predict, data, opt)
text
And check the loss:
jldoctest overview; filter = r"[+-]?([0-9][.])?[0-9]+(f[+-][0-9])?"
julia> loss(predict, x_train, y_train)
116.38745f0
text
It went down. Why?
jldoctest overview; filter = r"[+-]?([0-9][.])?[0-9]+(f[+-][0-9])?"
julia> predict.weight, predict.bias
(Float32[7.246838;;], Float32[1.748103])
text
The parameters have changed. This single step is the essence of machine learning.

3+. Iteratively Train the Model

In the previous section, we made a single call to train! which iterates over the data we passed in just once. An epoch refers to one pass over the dataset. Typically, we will run the training for multiple epochs to drive the loss down even further. Let's run it a few more times:

jldoctest overview; filter = r"[+-]?([0-9][.])?[0-9]+(f[+-][0-9])?"
julia> for epoch in 1:200
train!(loss, predict, data, opt)
end

julia> loss(predict, x_train, y_train)
0.00339581f0

julia> predict.weight, predict.bias
(Float32[4.0159144;;], Float32[2.004479])

text
After 200 training steps, the loss went down, and the parameters are getting close to those in the function the model is built to predict.

4. Verify the Results

Now, let's verify the predictions:

jldoctest overview; filter = r"[+-]?([0-9][.])?[0-9]+(f[+-][0-9])?"
julia> predict(x_test)
1×5 Matrix{Float32}:
26.1121 30.13 34.1479 38.1657 42.1836

julia> y_test
1×5 Matrix{Int64}:
26 30 34 38 42

text
The predictions are good. Here's how we got there. 

First, we gathered real-world data into the variables x_train, y_train, x_test, and y_test. The x_ data defines inputs, and the y_ data defines outputs. The _train data is for training the model, and the _test data is for verifying the model. Our data was based on the function 4x + 2.

Then, we built a single input, single output predictive model, predict = Dense(1 => 1). The initial predictions weren't accurate, because we had not trained the model yet.

After building the model, we trained it with train!(loss, predict, data, opt). The loss function is first, followed by the model itself, the training data, and the Descent optimiser provided by Flux. We ran the training step once, and observed that the parameters changed and the loss went down. Then, we ran the train! many times to finish the training process.

After we trained the model, we verified it with the test data to verify the results.

This overall flow represents how Flux works. Let's drill down a bit to understand what's going on inside the individual layers of Flux.

---

Src/Guide/Models/Quickstart

A Neural Network in One Minute

If you have used neural networks before, then this simple example might be helpful for seeing how the major parts of Flux work together. Try pasting the code into the REPL prompt.

If you haven't, then you might prefer the Fitting a Straight Line page.


/ Detailed source-code truncated for AI context efficiency. /
text
julia
using Plots # to draw the above figure

p_true = scatter(noisy[1,:], noisy[2,:], zcolor=truth, title="True classification", legend=false)
p_raw = scatter(noisy[1,:], noisy[2,:], zcolor=probs1[1,:], title="Untrained network", label="", clims=(0,1))
p_done = scatter(noisy[1,:], noisy[2,:], zcolor=probs2[1,:], title="Trained network", legend=false)

plot(p_true, p_raw, p_done, layout=(1,3), size=(1000,330))

text
@raw html
<img align="right" width="300px" src="../../../assets/quickstart/loss.png">
text
Here's the loss during training:
julia
plot(losses; xaxis=(:log10, "iteration"),
yaxis="loss", label="per batch")
n = length(loader)
plot!(n:n:length(losses), mean.(Iterators.partition(losses, n)),
label="epoch mean", dpi=200)
text
This XOR ("exclusive or") problem is a variant of the famous one which drove Minsky and Papert to invent deep neural networks in 1969. For small values of "deep" -- this has one hidden layer, while earlier perceptrons had none. (What they call a hidden layer, Flux calls the output of the first layer, model1.)

Since then things have developed a little.

Features to Note

Some things to notice in this example are:

* The batch dimension of data is always the last one. Thus a 2×1000 Matrix is a thousand observations, each a column of length 2. Flux defaults to Float32, but most of Julia to Float64.

* The model can be called like a function, y = model(x). Each layer like Dense is an ordinary struct, which encapsulates some arrays of parameters (and possibly other state, as for BatchNorm).

* But the model does not contain the loss function, nor the optimisation rule. The momenta needed by Adam are stored in the object returned by setup. And Flux.logitcrossentropy is an ordinary function that combines the softmax and crossentropy functions.

* The do block creates an anonymous function, as the first argument of gradient. Anything executed within this is differentiated.

Instead of calling gradient and update! separately, there is a convenience function train!. If we didn't want anything extra (like logging the loss), we could replace the training loop with the following:

julia
for epoch in 1:1_000
Flux.train!(model, loader |> device, opt_state) do m, x, y
y_hat = m(x)
Flux.logitcrossentropy(y_hat, y)
end
end
text
If you want to keep control of the loop — for logging, early stopping, or a custom schedule — but would rather not spell out gradient and update! yourself, trainstep! is the per-step primitive that train! is built on. It evaluates the loss, computes the gradient, and updates model and opt_state in place, returning the loss. The batch is passed as a tuple and spliced into the loss after the model (so it is called as loss(m, x, y)):
julia
losses = []
@showprogress for epoch in 1:1_000
for xy_cpu in loader
x, y = xy_cpu |> device
loss = Flux.trainstep!(model, (x, y), opt_state) do m, x, y
y_hat = m(x)
Flux.logitcrossentropy(y_hat, y)
end
push!(losses, loss)
end
end
text
This is exactly the original training loop above, with the withgradient/update! pair collapsed into a single call. Use trainstep_withgradient! if you also need the gradient. On a Reactant device, trainstep! (and train!) additionally compile and cache the whole step into one executable.

* Notice that the full dataset noisy lives on the CPU, and is moved to the GPU one batch at a time, by xy_cpu |> device. This is generally what you want for large datasets. Calling loader |> device similarly modifies the DataLoader to move one batch at a time.

* In our simple example, we conveniently created the model has a Chain of layers.
For more complex models, you can define a custom struct
MyModel containing layers and arrays and implement the call operator (::MyModel)(x) = ... to define the forward pass. This is all that is needed for Flux to work. Marking the struct with Flux.@layer will add some more functionality, like pretty printing and the ability to mark some internal fields as trainable or not (also see trainable).

---

Src/Guide/Models/Recurrence

Recurrent Models

Recurrent cells

To introduce Flux's recurrence functionalities, we will consider the following vanilla recurrent neural network structure:

In the above, we have a sequence of length 3, where x1 to x3 represent the input at each step. It could be a timestamp or a word in a sentence encoded as vectors. y1 to y3 are their respective outputs.

An aspect to recognise is that in such a model, the recurrent cells A all refer to the same structure. What distinguishes it from a simple dense layer is that the cell A is fed, in addition to an input x, with information from the previous state of the model (hidden state denoted as h1 & h2 in the diagram).

In the most basic RNN case, cell A could be defined by the following:

julia
output_size = 5
input_size = 2
Wxh = randn(Float32, output_size, input_size)
Whh = randn(Float32, output_size, output_size)
b = zeros(Float32, output_size)

function rnn_cell(x, h)
h = tanh.(Wxh x .+ Whh h .+ b)
return h, h
end

seq_len = 3

dummy input data


x = [rand(Float32, input_size) for i = 1:seq_len]

random initial hidden state


h0 = zeros(Float32, output_size)

y = []
ht = h0
for xt in x
yt, ht = rnn_cell(xt, ht)
y = [y; [yt]] # concatenate in non-mutating (AD friendly) way
end

text
Notice how the above is essentially a Dense layer that acts on two inputs, xt and ht.
The result of the forward pass at each time step, is a tuple containing the output
yt and the updated state ht. The updated state is used as an input in next iteration. In the simple case of a vanilla RNN, the
output and the state are the same. In more complex cells, such as
LSTMCell, the state can contain multiple arrays.

There are various recurrent cells available in Flux, notably RNNCell, LSTMCell and GRUCell, which are documented in the layer reference. The hand-written example above can be replaced with:

julia
using Flux

output_size = 5
input_size = 2
seq_len = 3
x = [rand(Float32, input_size) for i = 1:seq_len]
h0 = zeros(Float32, output_size)

rnn_cell = Flux.RNNCell(input_size => output_size)

y = []
ht = h0
for xt in x
yt, ht = rnn_cell(xt, ht)
y = [y; [yt]]
end

text
The entire output y or just the last output y[end] can be used for further processing, such as classification or regression. 

Using a cell as part of a model

Let's consider a simple model that is trained to predict a scalar quantity for each time step in a sequence. The model will have a single RNN cell, followed by a dense layer to produce the output.
Since the
RNNCell can deal with batches of data, we can define the model to accept an input where
at each time step, the input is a matrix of size
(input_size, batch_size).

julia
struct RecurrentCellModel{H,C,D}
h0::H
cell::C
dense::D
end

we choose to not train the initial hidden state


Flux.@layer RecurrentCellModel trainable=(cell, dense)

function RecurrentCellModel(input_size::Int, hidden_size::Int)
return RecurrentCellModel(
zeros(Float32, hidden_size),
RNNCell(input_size => hidden_size),
Dense(hidden_size => 1))
end

function (m::RecurrentCellModel)(x)
z = []
ht = m.h0
for xt in x
yt, ht = m.cell(xt, ht)
z = [z; [yt]]
end
z = stack(z, dims=2) # [hidden_size, seq_len, batch_size] or [hidden_size, seq_len]
ŷ = m.dense(z) # [1, seq_len, batch_size] or [1, seq_len]
return ŷ
end

text
Notice that we stack the hidden states z to form a tensor of size (hidden_size, seq_len, batch_size). This can speed up the final classification, since we then process all the outputs at once with a single forward pass of the dense layer. 

Let's now define the training loop for this model:

julia
using Optimisers: AdamW

function loss(model, x, y)
ŷ = model(x)
y = stack(y, dims=2)
return Flux.mse(ŷ, y)
end

create dummy data


seq_len, batch_size, input_size = 3, 4, 2
x = [rand(Float32, input_size, batch_size) for _ = 1:seq_len]
y = [rand(Float32, 1, batch_size) for _ = 1:seq_len]

initialize the model and optimizer


model = RecurrentCellModel(input_size, 5)
opt_state = Flux.setup(AdamW(1e-3), model)

compute the gradient and update the model


g = gradient(m -> loss(m, x, y), model)[1]
Flux.update!(opt_state, model, g)
text

Handling the whole sequence at once

In the above example, we processed the sequence one time step at a time using a recurrent cell. However, it is possible to process the entire sequence at once. This can be done by stacking the input data x to form a tensor of size (input_size, seq_len) or (input_size, seq_len, batch_size).
One can then use the
RNN, LSTM or GRU layers to process the entire input tensor.

Let's consider the same example as above, but this time we use an RNN layer instead of an RNNCell:

julia
struct RecurrentModel{H,C,D}
h0::H
rnn::C
dense::D
end

Flux.@layer RecurrentModel trainable=(rnn, dense)

function RecurrentModel(input_size::Int, hidden_size::Int)
return RecurrentModel(
zeros(Float32, hidden_size),
RNN(input_size => hidden_size),
Dense(hidden_size => 1))
end

function (m::RecurrentModel)(x)
z = m.rnn(x, m.h0) # [hidden_size, seq_len, batch_size] or [hidden_size, seq_len]
ŷ = m.dense(z) # [1, seq_len, batch_size] or [1, seq_len]
return ŷ
end

seq_len, batch_size, input_size = 3, 4, 2
x = rand(Float32, input_size, seq_len, batch_size)
y = rand(Float32, 1, seq_len, batch_size)

model = RecurrentModel(input_size, 5)
opt_state = Flux.setup(AdamW(1e-3), model)

g = gradient(m -> Flux.mse(m(x), y), model)[1]
Flux.update!(opt_state, model, g)

text
Finally, the Recurrence layer can be used wrap any recurrent cell to process the entire sequence at once. For instance, a type behaving the same as the LSTM layer can be defined as follows:
julia
rnn = Recurrence(LSTMCell(2 => 3)) # similar to LSTM(2 => 3)
x = rand(Float32, 2, 4, 3)
y = rnn(x)
text

Stacking recurrent layers

Recurrent layers can be stacked to form a deeper model by simply chaining them together using the Chain layer. The output of a layer is fed as input to the next layer in the chain.
For instance, a model with two LSTM layers can be defined as follows:

julia
stacked_rnn = Chain(LSTM(3 => 5), Dropout(0.5), LSTM(5 => 5))
x = rand(Float32, 3, 4)
y = stacked_rnn(x)
text
If more fine grained control is needed, for instance to have a trainable initial hidden state, one can define a custom model as follows:
julia
struct StackedRNN{L,S}
layers::L
states0::S
end

Flux.@layer StackedRNN

function StackedRNN(d::Int; num_layers::Int)
layers = [LSTM(d => d) for _ in 1:num_layers]
states0 = [Flux.initialstates(l) for l in layers]
return StackedRNN(layers, states0)
end

function (rnn::StackedRNN)(x)
for (layer, state0) in zip(rnn.layers, rnn.states0)
x = layer(x, state0)
end
return x
end

rnn = StackedRNN(3; num_layers=2)
x = rand(Float32, 3, 10)
y = rnn(x)

text
---

Src/Guide/Training/Training

Training a Flux Model

Training refers to the process of slowly adjusting the parameters of a model to make it work better.
Besides the model itself, we will need three things:

An objective function* that evaluates how well a model is doing on some input.
An optimisation rule* which describes how the model's parameters should be adjusted.
Some training data* to use as the input during this process.

Usually the training data is some collection of examples (or batches of examples) which
are handled one-by-one. One epoch of training means that each example is used once,
something like this:

julia

Initialise the optimiser for this model:


opt_state = Flux.setup(rule, model)

for data in train_set
# Unpack this element (for supervised training):
input, label = data

# Calculate the gradient of the objective
# with respect to the parameters within the model:
grads = Flux.gradient(model) do m
result = m(input)
loss(result, label)
end

# Update the parameters so as to reduce the objective,
# according the chosen optimisation rule:
Flux.update!(opt_state, model, grads[1])
end

text
This loop can also be written using the function train!,
but it's helpful to understand the pieces first:
julia
train!(model, train_set, opt_state) do m, x, y
loss(m(x), y)
end
text

Model Gradients

Fist recall from the section on taking gradients that
Flux.gradient(f, a, b) always calls f(a, b), and returns a tuple (∂f_∂a, ∂f_∂b).
In the code above, the function
f passed to gradient is an anonymous function with
one argument, created by the
do block, hence grads is a tuple with one element.
Instead of a
do block, we could have written:

julia
grads = Flux.gradient(m -> loss(m(input), label), model)
text
Since the model is some nested set of layers, grads[1] is a similarly nested set of
NamedTuples, ultimately containing gradient components. If (for example)
θ = model.layers[1].weight[2,3] is one scalar parameter, an entry in a matrix of weights,
then the derivative of the loss with respect to it is
∂f_∂θ = grads[1].layers[1].weight[2,3].

It is important that the execution of the model takes place inside the call to gradient,
in order for the influence of the model's parameters to be observed by Zygote.

It is also important that every update! step receives a newly computed gradient,
as it will change whenever the model's parameters are changed, and for each new data point.


Loss Functions

The objective function must return a number representing how far the model is from
the desired result. This is termed the loss of the model.

This number can be produced by any ordinary Julia code, but this must be executed
within the call to
gradient. For instance, we could define a function

julia
loss(y_hat, y) = sum((y_hat .- y).^2)
text
or write this directly inside the do block above. Many commonly used functions,
like
mse for mean-squared error or crossentropy for cross-entropy loss,
are available from the
Flux.Losses module.


Optimisation Rules

The simplest kind of optimisation using the gradient is termed gradient descent
(or sometimes stochastic gradient descent when, as here, it is not applied to the entire dataset at once).

Gradient descent needs a learning rate which is a small number describing how fast to walk downhill,
usually written as the Greek letter "eta",
η. This is often described as a hyperparameter,
to distinguish it from the parameters which are being updated
θ = θ - η * ∂loss_∂θ.
We want to update all the parameters in the model, like this:

julia
η = 0.01 # learning rate

For each parameter array, update


according to the corresponding gradient:


fmap(model, grads[1]) do p, g
p .= p .- η .* g
end
text
A slightly more refined version of this loop to update all the parameters is wrapped up as a function update!(opt_state, model, grads[1]).
And the learning rate is the only thing stored in the
Descent struct.

However, there are many other optimisation rules, which adjust the step size and
direction in various clever ways.
Most require some memory of the gradients from earlier steps, rather than always
walking straight downhill --
Momentum is the simplest.
The function
setup creates the necessary storage for this, for a particular model.
It should be called once, before training, and returns a tree-like object which is the
first argument of
update!. Like this:

julia

Initialise momentum


opt_state = Flux.setup(Momentum(0.01, 0.9), model)

for data in train_set
grads = [...]

# Update both model parameters and optimiser state:
Flux.update!(opt_state, model, grads[1])
end

text
Many commonly-used optimisation rules, such as Adam, are built-in.
These are listed on the optimisers page.

!!! compat "Implicit-style optimiser state"
This
setup makes another tree-like structure. Old versions of Flux did not do this,
and instead stored a dictionary-like structure within the optimiser
Adam(0.001).
This was initialised on first use of the version of
update! for "implicit" parameters.


Datasets & Batches

The loop above iterates through train_set, expecting at each step a tuple (input, label).
The very simplest such object is a vector of tuples, such as this:

julia
x = randn(28, 28)
y = rand(10)
data = [(x, y)]
text
or data = [(x, y), (x, y), (x, y)] for the same values three times.

Very often, the initial data is large arrays which you need to slice into examples.
To produce one iterator of pairs
(x, y), you might want zip:

julia
X = rand(28, 28, 60_000); # many images, each 28 × 28
Y = rand(10, 60_000)
data = zip(eachslice(X; dims=3), eachcol(Y))

first(data) isa Tuple{AbstractMatrix, AbstractVector} # true

text
Here each iteration will use one matrix x (an image, perhaps) and one vector y.
It is very common to instead train on batches of such inputs (or mini-batches,
the two words mean the same thing) both for efficiency and for better results.
This can be easily done using the
DataLoader:
julia
data = Flux.DataLoader((X, Y), batchsize=32)

x1, y1 = first(data)
size(x1) == (28, 28, 32)
length(data) == 1875 === 60_000 ÷ 32

text
Flux's layers are set up to accept such a batch of input data,
and the convolutional layers such as
Conv require it.
The batch index is always the last dimension.

Training Loops

Simple training loops like the one above can be written compactly using
the
train! function. Including setup, this reads:

julia
opt_state = Flux.setup(Adam(), model)

for epoch in 1:100
Flux.train!(model, train_set, opt_state) do m, x, y
loss(m(x), y)
end
end

text
Or explicitly writing the anonymous function which this do block creates,
train!((m,x,y) -> loss(m(x),y), model, train_set, opt_state) is exactly equivalent.

If you want to write the loop yourself but keep the convenience of a single gradient +
update call,
trainstep! is the per-step primitive that
train! is built on. It differentiates the loss, updates model and opt_state in place,
and returns the loss:

julia
opt_state = Flux.setup(Adam(), model)

for epoch in 1:100
for (x, y) in train_set
l = Flux.trainstep!((m, x, y) -> loss(m(x), y), model, (x, y), opt_state)
end
end

text
Use trainstep_withgradient! if you also need the
gradient (it returns
(loss, grad)). On a Reactant device both compile and cache the whole step,
just like
train!.

Like withgradient, the loss passed to trainstep! may return auxiliary
data alongside the scalar loss — return a
Tuple or NamedTuple whose first element is the loss,
and the gradient is taken of the loss alone while the whole value is returned. This is convenient
for logging a metric computed during the forward pass:

julia
l, stats = Flux.trainstep!(model, (x, y), opt_state) do m, x, y
ŷ = m(x)
loss(ŷ, y), (; acc = accuracy(ŷ, y))
end
text
Real training loops often need more flexibility, and the best way to do this is just
to write the loop. This is ordinary Julia code, without any need to work through some
callback API. Here is an example, in which it may be helpful to note:

* The function withgradient is like gradient but also
returns the value of the function, for logging or diagnostic use.
* Logging or printing is best done outside of the
gradient call,
as there is no need to differentiate these commands.
* To use
result for logging purposes, you could change the do block to end with
return my_loss(result, label), result, i.e. make the function passed to withgradient
return a tuple. The first element is always the loss.
* Julia's
break and continue keywords let you exit from parts of the loop.

julia
opt_state = Flux.setup(Adam(), model)

my_log = []
for epoch in 1:100
losses = Float32[]
for (i, data) in enumerate(train_set)
input, label = data

val, grads = Flux.withgradient(model) do m
# Any code inside here is differentiated.
# Evaluation of the model and loss must be inside!
result = m(input)
my_loss(result, label)
end

# Save the loss from the forward pass. (Done outside of gradient.)
push!(losses, val)

# Detect loss of Inf or NaN. Print a warning, and then skip update!
if !isfinite(val)
@warn "loss is $val on item $i" epoch
continue
end

Flux.update!(opt_state, model, grads[1])
end

# Compute some accuracy, and save details as a NamedTuple
acc = my_accuracy(model, train_set)
push!(my_log, (; acc, losses))

# Stop training when some criterion is reached
if acc > 0.95
println("stopping after $epoch epochs")
break
end
end

text

Regularisation

The term regularisation covers a wide variety of techniques aiming to improve the
result of training. This is often done to avoid overfitting.

Some of these can be implemented by simply modifying the loss function.
L₂ regularisation (sometimes called ridge regression) adds to the loss a penalty
proportional to
θ^2 for every scalar parameter.
A very simple model could be implemented as follows:

julia
grads = Flux.gradient(densemodel) do m
result = m(input)
penalty = sum(abs2, m.weight)/2 + sum(abs2, m.bias)/2
my_loss(result, label) + 0.42f0 * penalty
end
text
Accessing each individual parameter array by hand won't work well for large models.
Instead, we can use
Flux.trainables to collect all of them,
and then apply a function to each one, and sum the result:
julia
pen_l2(x::AbstractArray) = sum(abs2, x)/2

grads = Flux.gradient(model) do m
result = m(input)
penalty = sum(pen_l2, Flux.trainables(m))
my_loss(result, label) + 0.42f0 * penalty
end

text
However, the gradient of this penalty term is very simple: It is proportional to the original weights.
So there is a simpler way to implement exactly the same thing, by modifying the optimiser
instead of the loss function. This is done by replacing this:
julia
opt_state = Flux.setup(Adam(0.1), model)
text
with this:
julia
decay_opt_state = Flux.setup(OptimiserChain(WeightDecay(0.42), Adam(0.1)), model)
text
Flux's optimisers are really modifications applied to the gradient before using it to update
the parameters, and
OptimiserChain applies two such modifications.
The first,
WeightDecay adds 0.42 times the original parameter to the gradient,
matching the gradient of the penalty above (with the same, unrealistically large, constant).
After that, in either case,
Adam computes the final update.

The same trick works for L₁ regularisation (also called Lasso), where the penalty is
pen_l1(x::AbstractArray) = sum(abs, x) instead. This is implemented by SignDecay(0.42).

The same OptimiserChain mechanism can be used for other purposes, such as gradient clipping with ClipGrad or ClipNorm.

Besides L1 / L2 / weight decay, another common and quite different kind of regularisation is
provided by the
Dropout layer. This turns off some outputs of the
previous layer during training.
It should switch automatically, but see
trainmode! / testmode! to manually enable or disable this layer.

Learning Rate Schedules

Finer control of training, you may wish to alter the learning rate mid-way through training.
This can be done with
adjust!, like this:

julia
opt_state = Flux.setup(Adam(0.1), model) # initialise once

for epoch in 1:1000
train!([...], state) # Train with η = 0.1 for first 100,
if epoch == 100 # then change to use η = 0.01 for the rest.
Flux.adjust!(opt_state, 0.01)
end
end

text
Other hyper-parameters can also be adjusted, such as Flux.adjust!(opt_state, beta = (0.8, 0.99)).
And such modifications can be applied to just one part of the model.
For instance, this sets a different learning rate for the encoder and the decoder:
julia

Consider some model with two parts:


bimodel = Chain(enc = [...], dec = [...])

This returns a tree whose structure matches the model:


opt_state = Flux.setup(Adam(0.02), bimodel)

Adjust the learning rate to be used for bimodel.layers.enc


Flux.adjust!(opt_state.layers.enc, 0.03)
text

Scheduling Optimisers

In practice, it is fairly common to schedule the learning rate of an optimiser to obtain faster convergence. There are a variety of popular scheduling policies, and you can find implementations of them in ParameterSchedulers.jl. The documentation for ParameterSchedulers.jl provides a more detailed overview of the different scheduling policies, and how to use them with Flux optimisers. Below, we provide a brief snippet illustrating a cosine annealing schedule with a momentum optimiser.

First, we import ParameterSchedulers.jl and initialize a cosine annealing schedule to vary the learning rate between 1e-4 and 1e-2 every 10 epochs. We also create a new Momentum optimiser.

julia
using ParameterSchedulers

opt_state = Flux.setup(Momentum(), model)
schedule = Cos(λ0 = 1e-4, λ1 = 1e-2, period = 10)
for (eta, epoch) in zip(schedule, 1:100)
Flux.adjust!(opt_state, eta)
# your training code here
end

text
schedule can also be indexed (e.g. schedule(100)) or iterated like any iterator in Julia.

ParameterSchedulers.jl schedules are stateless (they don't store their iteration state). If you want a _stateful_ schedule, you can use ParameterSchedulers.Stateful:

julia
using ParameterSchedulers: Stateful, next!

schedule = Stateful(Cos(λ0 = 1e-4, λ1 = 1e-2, period = 10))
for epoch in 1:100
Flux.adjust!(opt_state, next!(schedule))
# your training code here
end

text
Finally, a scheduling function can be incorporated into the optimser's state, advanced at each gradient update step, and possibly passed to the train! function. See this section of ParameterSchedulers.jl documentation for more details.

ParameterSchedulers.jl allows for many more scheduling policies including arbitrary functions, looping any function with a given period, or sequences of many schedules. See the ParameterSchedulers.jl documentation for more info.

Freezing layer parameters

To completely disable training of some part of the model, use freeze!.
This is a temporary modification, reversed by
thaw!:

julia
Flux.freeze!(opt_state.layers.enc)

Now training won't update parameters in bimodel.layers.enc


train!(loss, bimodel, data, opt_state)

Un-freeze the entire model:


Flux.thaw!(opt_state)
text
While adjust! and freeze!/thaw! make temporary modifications to the optimiser state,
permanently removing some fields of a new layer type from training is usually done
when defining the layer, by calling for example
@layer NewLayer trainable=(weight,).

---

Src/Guide/Gpu

GPU Support

Most work on neural networks involves the use of GPUs, as they can typically perform the required computation much faster.
This page describes how Flux co-operates with various other packages, which talk to GPU hardware.

For those in a hurry, see the quickstart page. Or do using CUDA and then call gpu on both the model and the data.

Basic GPU use: from Array to CuArray

Julia's GPU packages work with special array types, in place of the built-in Array.
The most used is
CuArray provided by CUDA.jl, for GPUs made by NVIDIA.
That package provides a function
cu which converts an ordinary Array (living in CPu memory) to a CuArray (living in GPU memory).
Functions like
* and broadcasting specialise so that, when given CuArrays, all the computation happens on the GPU:

julia
W = randn(3, 4) # some weights, on CPU: 3×4 Array{Float64, 2}
x = randn(4) # fake data
y = tanh.(W * x) # computation on the CPU

using CUDA

cu(W) isa CuArray{Float32}
(cW, cx) = (W, x) |> cu # move both to GPU
cy = tanh.(cW * cx) # computation on the GPU

text
Notice that cu doesn't only move arrays, it also recurses into many structures, such as the tuple (W, x) above.
(Notice also that it converts Julia's default
Float64 numbers to Float32, as this is what most GPUs support efficiently -- it calls itself "opinionated". Flux defaults to Float32 in all cases.)

To use CUDA with Flux, you can simply use cu to move both the model, and the data.
It will create a copy of the Flux model, with all of its parameter arrays moved to the GPU:

julia
using Pkg; Pkg.add(["CUDA", "cuDNN"]) # do this once

using Flux, CUDA
CUDA.allowscalar(false) # recommended

model = Dense(W, true, tanh) # wrap the same matrix W in a Flux layer
model(x) ≈ y # same result, still on CPU

c_model = cu(model) # move all the arrays within model to the GPU
c_model(cx) # computation on the GPU

text
Notice that you need using CUDA (every time) but also ] add cuDNN (once, when installing packages).
This is a quirk of how these packages are set up.
(The
cuDNN.jl sub-package handles operations such as convolutions, called by Flux via NNlib.jl.)

Flux's gradient, and training functions like setup, update!, and train!, are all equally happy to accept GPU arrays and GPU models, and then perform all computations on the GPU.
It is recommended that you move the model to the GPU before calling
setup.

julia
grads = Flux.gradient((f,x) -> sum(abs2, f(x)), model, x) # on CPU
c_grads = Flux.gradient((f,x) -> sum(abs2, f(x)), c_model, cx) # same result, all on GPU

c_opt = Flux.setup(Adam(), c_model) # setup optimiser after moving model to GPU

Flux.update!(c_opt, c_model, c_grads[1]) # mutates c_model but not model

text
To move arrays and other objects back to the CPU, Flux provides a function cpu.
This is recommended when saving models,
Flux.state(c_model |> cpu), see below.
julia
cpu(cW) isa Array{Float32, 2}

model2 = cpu(c_model) # copy model back to CPU
model2(x)

text
!!! compat "Flux ≤ 0.13"
Old versions of Flux automatically loaded CUDA.jl to provide GPU support. Starting from Flux v0.14, it has to be loaded separately. Julia's package extensions) allow Flux to automatically load some GPU-specific code when needed.

Other GPU packages for AMD & Apple

Non-NVIDIA graphics cards are supported by other packages. Each provides its own function which behaves like cu.
AMD GPU support provided by AMDGPU.jl, on systems with ROCm and MIOpen installed.
This package has a function
roc which converts Array to ROCArray:

julia
using Flux, AMDGPU
AMDGPU.allowscalar(false)

r_model = roc(model)
r_model(roc(x))

Flux.gradient((f,x) -> sum(abs2, f(x)), r_model, roc(x))

text
Experimental support for Apple devices with M-series chips is provided by  Metal.jl. This has a function mtl which works like cu, converting Array to MtlArray:
julia
using Flux, Metal
Metal.allowscalar(false)

m_model = mtl(model)
m_y = m_model(mtl(x))

Flux.gradient((f,x) -> sum(abs2, f(x)), m_model, mtl(x))

text
!!! danger "Experimental"
Metal support in Flux is experimental and many features are not yet available.
AMD support is improving, but likely to have more rough edges than CUDA.

If you want your model to work with any brand of GPU, or none, then you may not wish to write cu everywhere.
One simple way to be generic is, at the top of the file, to un-comment one of several lines which import a package and assign its "adaptor" to the same name:

julia
using CUDA: cu as device # after this,
device === cu

using AMDGPU: roc as device


device = identity # do-nothing, for CPU

using Flux
model = Chain(...) |> device

text
!!! note "Adapt.jl"
The functions
cu, mtl, roc all use Adapt.jl, to work within various wrappers.
The reason they work on Flux models is that
Flux.@layer Layer defines methods of Adapt.adapt_structure(to, lay::Layer).


Automatic GPU choice with gpu and gpu_device

Flux also provides a more automatic way of choosing which GPU (or none) to use. This is the function gpu:
* By default it does nothing.
* If the package CUDA is loaded, and
CUDA.functional() === true, then it behaves like cu.
* If the package AMDGPU is loaded, and
AMDGPU.functional() === true, then it behaves like roc.
* If the package Metal is loaded, and
Metal.functional() === true, then it behaves like mtl.
* If two different GPU packages are loaded, the first one takes priority.

For the most part, this means that a script which says model |> gpu and data |> gpu will just work.
It should always run, and if a GPU package is loaded (and finds the correct hardware) then that will be used.

The function gpu uses a lower-level function called gpu_device from MLDataDevices.jl,
which checks what to do and then returns some device object. In fact, the entire implementation is just this:

julia
gpu(x) = gpu_device()(x)
cpu(x) = cpu_device()(x)
text
Automatic backend selection through gpu is not type-stable. That doesn't matter if you do it once, or once per large batch -- it costs a few microseconds. But it might matter if you do it within some loop.

To avoid this, you can first obtain a "device object" with device = gpu_device(), once, and then use this as the function to transfer data. Something like this:

julia
to_device = gpu_device()
gpu_model = model |> to_device

for epoch in 1:num_epochs
for (x, y) in dataloader
x_gpu, y_gpu = (x, y) |> to_device
# training code...

text
Finally, setting a backend prefence with gpu_backend! gives type stability to the whole pipeline.

Transferring Training Data

In order to train the model using the GPU both model and the training data have to be transferred to GPU memory. Moving the data can be done in two different ways:

1. Iterating over the batches in a DataLoader object transferring each one of the training batches at a time to the GPU. This is recommended for large datasets. Done by hand, it might look like this:

julia
train_loader = Flux.DataLoader((X, Y), batchsize=64, shuffle=true)
# ... model definition, optimiser setup
for epoch in 1:epochs
for (x_cpu, y_cpu) in train_loader
x = gpu(x_cpu)
y = gpu(y_cpu)
grads = gradient(m -> loss(m, x, y), model)
Flux.update!(opt_state, model, grads[1])
end
end
text
Rather than write this out every time, you can just call gpu(::DataLoader):
julia
gpu_train_loader = Flux.DataLoader((X, Y), batchsize=64, shuffle=true) |> gpu
# ... model definition, optimiser setup
for epoch in 1:epochs
for (x, y) in gpu_train_loader
grads = gradient(m -> loss(m, x, y), model)
Flux.update!(opt_state, model, grads[1])
end
end
text
This is equivalent to DataLoader(MLUtils.mapobs(gpu, (X, Y)); keywords...).
Something similar can also be done with
CUDA.CuIterator, gpu_train_loader = CUDA.CuIterator(train_loader). However, this only works with a limited number of data types: first(train_loader) should be a tuple (or NamedTuple) of arrays.

2. Transferring all training data to the GPU at once before creating the DataLoader. This is usually performed for smaller datasets which are sure to fit in the available GPU memory.

julia
gpu_train_loader = Flux.DataLoader((X, Y) |> gpu, batchsize = 32)
# ...
for epoch in 1:epochs
for (x, y) in gpu_train_loader
# ...
text
Here (X, Y) |> gpu applies gpu to both arrays, as it recurses into structures.

Saving GPU-Trained Models

After the training process is done, we must always transfer the trained model back to the CPU memory before serializing or saving to disk. This can be done with cpu:

julia
model = cpu(model) # or model = model |> cpu
text
and then
julia
using BSON

...


BSON.@save "./path/to/trained_model.bson" model

in this approach the cpu-transferred model (referenced by the variable model)


only exists inside the let statement


let model = cpu(model)
# ...
BSON.@save "./path/to/trained_model.bson" model
end

is equivalent to the above, but uses key=value storing directive from BSON.jl


BSON.@save "./path/to/trained_model.bson" model = cpu(model)
text
The reason behind this is that models trained in the GPU but not transferred to the CPU memory scope will expect CuArrays as input. In other words, Flux models expect input data coming from the same kind device in which they were trained on.

In controlled scenarios in which the data fed to the loaded models is guaranteed to be in the GPU there's no need to transfer them back to CPU memory scope, however in production environments, where artifacts are shared among different processes, equipments or configurations, there is no guarantee that the CUDA.jl package will be available for the process performing inference on the model loaded from the disk.


Disabling CUDA or choosing which GPUs are visible to Flux

Sometimes it is required to control which GPUs are visible to julia on a system with multiple GPUs or disable GPUs entirely. This can be achieved with an environment variable CUDA_VISIBLE_DEVICES.

To disable all devices:


$ export CUDA_VISIBLE_DEVICES='-1'
text
To select specific devices by device id:

$ export CUDA_VISIBLE_DEVICES='0,1'
text
More information for conditional use of GPUs in CUDA.jl can be found in its documentation, and information about the specific use of the variable is described in the Nvidia CUDA blog post.


Data movement across GPU devices

Flux also supports getting handles to specific GPU devices, and transferring models from one GPU device to another GPU device from the same backend. Let's try it out for NVIDIA GPUs. First, we list all the available devices:

julia-repl
julia> using Flux, CUDA;

julia> CUDA.devices()
CUDA.DeviceIterator() for 3 devices:
0. NVIDIA TITAN RTX
1. NVIDIA TITAN RTX
2. NVIDIA TITAN RTX

text
Then, let's select the device with id 0:
julia-repl
julia> device0 = gpu_device(1)
(::CUDADevice{CuDevice}) (generic function with 4 methods)

julia> device0.device
CuDevice(0): NVIDIA TITAN RTX

text
Notice that indexing starts from 0 in the CUDA.devices() output, but gpu_device! expects the device id starting from 1.

Then, let's move a simple dense layer to the GPU represented by device0:

julia-repl
julia> dense_model = Dense(2 => 3)
Dense(2 => 3) # 9 parameters

julia> dense_model = dense_model |> device0;

julia> dense_model.weight
3×2 CuArray{Float32, 2, CUDA.DeviceMemory}:
-0.142062 -0.131455
-0.828134 -1.06552
0.608595 -1.05375

julia> CUDA.device(dense_model.weight) # check the GPU to which dense_model is attached
CuDevice(0): NVIDIA TITAN RTX

text
Next, we'll get a handle to the device with id 1, and move dense_model to that device:
julia-repl
julia> device1 = gpu_device(2)
(::CUDADevice{CuDevice}) (generic function with 4 methods)

julia> dense_model = dense_model |> device1; # don't directly print the model; see warning below

julia> CUDA.device(dense_model.weight)
CuDevice(1): NVIDIA TITAN RTX

text
Due to a limitation in Metal.jl, currently this kind of data movement across devices is only supported for CUDA and AMDGPU backends.


Distributed data parallel training

!!! danger "Experimental"

Distributed support is experimental and could change in the future.


Flux supports now distributed data parallel training with
DistributedUtils module.
If you want to run your code on multiple GPUs, you have to install
MPI.jl (see docs for more info).

julia-repl
julia> using MPI

julia> MPI.install_mpiexecjl()

text
Now you can run your code with mpiexecjl --project=. -n <np> julia <filename>.jl from CLI.

You can use either the MPIBackend or NCCLBackend, the latter only if also NCCL.jl is loaded. First, initialize a backend with DistributedUtils.initialize, e.g.

julia-repl
julia> using Flux, MPI, NCCL, CUDA

julia> CUDA.allowscalar(false)

julia> DistributedUtils.initialize(NCCLBackend)

julia> backend = DistributedUtils.get_distributed_backend(NCCLBackend)
NCCLBackend{Communicator, MPIBackend{MPI.Comm}}(Communicator(Ptr{NCCL.LibNCCL.ncclComm} @0x000000000607a660), MPIBackend{MPI.Comm}(MPI.Comm(1140850688)))

text
Pass your model, as well as any data to GPU device.
julia-repl
julia> model = Chain(Dense(1 => 256, tanh), Dense(256 => 1)) |> gpu
Chain(
Dense(1 => 256, tanh), # 512 parameters
Dense(256 => 1), # 257 parameters
) # Total: 4 arrays, 769 parameters, 744 bytes.

julia> x = rand(Float32, 1, 16) |> gpu
1×16 CUDA.CuArray{Float32, 2, CUDA.DeviceMemory}:
0.239324 0.331029 0.924996 0.55593 0.853093 0.874513 0.810269 0.935858 0.477176 0.564591 0.678907 0.729682 0.96809 0.115833 0.66191 0.75822

julia> y = x .^ 3
1×16 CUDA.CuArray{Float32, 2, CUDA.DeviceMemory}:
0.0137076 0.0362744 0.791443 0.171815 0.620854 0.668804 0.53197 0.819654 0.108651 0.179971 0.312918 0.388508 0.907292 0.00155418 0.29 0.435899

text
In this case, we are training on a total of 16 * number of processes samples. You can also use DistributedUtils.DistributedDataContainer to split the data uniformly across processes (or do it manually).
julia-repl
julia> data = DistributedUtils.DistributedDataContainer(backend, x)
Flux.DistributedUtils.DistributedDataContainer(Float32[0.23932439 0.33102947 … 0.66191036 0.75822026], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])
text
You have to wrap your model in DistributedUtils.FluxDistributedModel and synchronize it (broadcast across all processes):
julia-repl
julia> model = DistributedUtils.synchronize!!(backend, DistributedUtils.FluxDistributedModel(model); root=0)
Chain(
Dense(1 => 256, tanh), # 512 parameters

Dense(256 => 1), # 257 parameters
) # Total: 4 arrays, 769 parameters, 744 bytes.

text
Time to set up an optimizer by using DistributedUtils.DistributedOptimizer and synchronize it as well.

/ Detailed source-code truncated for AI context efficiency. /
text
Now you can define loss and train the model.
julia-repl
julia> loss(model) = mean((model(x) .- y).^2)
loss (generic function with 1 method)

julia> for epoch in 1:100
global model, st_opt
l, grad = Zygote.withgradient(loss, model)
println("Epoch $epoch: Loss $l")
st_opt, model = Optimisers.update(st_opt, model, grad[1])
end
Epoch 1: Loss 0.011638729
Epoch 2: Loss 0.0116432225
Epoch 3: Loss 0.012763695
...

text
Remember that in order to run it on multiple GPUs you have to run from CLI mpiexecjl --project=. -n <np> julia <filename>.jl,
where
<np> is the number of processes that you want to use. The number of processes usually corresponds to the number of gpus.

By default MPI.jl MPI installation is CUDA-unaware so if you want to run it in CUDA-aware mode, read more here on custom installation and rebuilding MPI.jl.
Then test if your MPI is CUDA-aware by

julia-repl
julia> import Pkg
julia> Pkg.test("MPI"; test_args=["--backend=CUDA"])
text
If it is, set your local preference as below
julia-repl
julia> using Preferences
julia> set_preferences!("Flux", "FluxDistributedMPICUDAAware" => true)
text
!!! warning "Known shortcomings"

We don't run CUDA-aware tests so you're running it at own risk.


Checking GPU Availability

By default, Flux will run the checks on your system to see if it can support GPU functionality. You can check if Flux identified a valid GPU setup by typing the following:

julia-repl
julia> using CUDA

julia> CUDA.functional()
true

text
For AMD GPU:
julia-repl
julia> using AMDGPU

julia> AMDGPU.functional()
true

julia> AMDGPU.functional(:MIOpen)
true

text
For Metal GPU:
julia-repl
julia> using Metal

julia> Metal.functional()
true

text
---

Src/Guide/Performance

Performance Tips

All the usual Julia performance tips apply.
As always profiling your code is generally a useful way of finding bottlenecks.
Below follow some Flux specific tips/reminders.

Don't use more precision than you need

Flux works great with all kinds of number types.
But often you do not need to be working with say
Float64 (let alone BigFloat).
Switching to
Float32 can give you a significant speed up,
not because the operations are faster, but because the memory usage is halved.
Which means allocations occur much faster.
And you use less memory.

Preserve inputs' types

Not only should your activation and loss functions be type-stable,
they should also preserve the type of their inputs.

A very artificial example using an activation function like

julia
my_tanh(x) = Float64(tanh(x))
text
will result in performance on Float32 input orders of magnitude slower than the normal tanh would,
because it results in having to use slow mixed type multiplication in the dense layers.
Similar situations can occur in the loss function during backpropagation.

Which means if you change your data say from Float64 to Float32 (which should give a speedup: see above),
you will see a large slow-down.

This can occur sneakily, because you can cause type-promotion by interacting with a numeric literals.
E.g. the following will have run into the same problem as above:

julia
leaky_tanh(x) = 0.01*x + tanh(x)
text
While one could change the activation function (e.g. to use 0.01f0*x), the idiomatic (and safe way)  to avoid type casts whenever inputs changes is to use oftype:
julia
leaky_tanh(x) = oftype(x/1, 0.01)*x + tanh(x)
text

Evaluate batches as matrices of features

While it can sometimes be tempting to process your observations (feature vectors) one at a time
e.g.

julia
function loss_total(xs::AbstractVector{<:Vector}, ys::AbstractVector{<:Vector})
sum(zip(xs, ys)) do (x, y_target)
y_pred = model(x) # evaluate the model
return loss(y_pred, y_target)
end
end
text
It is much faster to concatenate them into a matrix,
as this will hit BLAS matrix-matrix multiplication, which is much faster than the equivalent sequence of matrix-vector multiplications.
The improvement is enough that it is worthwhile allocating new memory to store them contiguously.
julia
x_batch = reduce(hcat, xs)
y_batch = reduce(hcat, ys)
...
function loss_total(x_batch::Matrix, y_batch::Matrix)
y_preds = model(x_batch)
sum(loss.(y_preds, y_batch))
end
text
When doing this kind of concatenation use reduce(hcat, xs) rather than hcat(xs...).
This will avoid the splatting penalty, and will hit the optimised
reduce method.

Be aware of GPU memory inefficiencies

Currently, GPU memory is not handled as well as system memory. If your training loop is allocating significantly on the GPU, you can quickly fill your GPU memory and the piecemeal reclamation and shuffling of data between GPU and system memory can become extremely slow. If profiling shows that a significant portion of time is spent in the gpu function and your data sizes are not large, this may be the cause. Running an incremental garbage collection manually (GC.gc(false)) at regular intervals can keep your GPU memory free and responsive. See other tips for CUDA memory management here.

Compile your training with Reactant

The tips above tune eager execution. For the largest speedups, compile your model with Reactant.jl: it traces your code and lowers it — through MLIR and XLA — into a single fused, kernel-optimised executable that is cached and reused across calls. For training, the forward pass, the Enzyme reverse pass and the optimiser update are compiled together, which removes per-operation dispatch and kernel-launch overhead and lets the compiler fuse operations and plan buffers across the whole step.

You do not have to invoke the compiler yourself: when the model lives on a Reactant device, trainstep! — and train!, which is built on it — compile and cache the fused step automatically.

julia
using Flux, Reactant

dev = reactant_device()
model = model |> dev
opt_state = Flux.setup(Adam(1f-3), model) # set up the optimiser after moving to the device
loader = DataLoader((X, Y); batchsize=32) |> dev # move batches to the device during iteration
loss(m, x, y) = Flux.logitcrossentropy(m(x), y)
trainmode!(model)
for (x, y) in loader
Flux.trainstep!(loss, model, (x, y), opt_state) # compiled on the first call, reused after
end

text
The cached executable is keyed on the batch shape, so keeping batch sizes fixed avoids recompilation (a smaller final batch simply compiles one extra executable). The first call pays a one-time compilation cost that can be large, so this pays off over many steps rather than a handful. See Compiling Flux with Reactant for the full guide, including inference and manual @compile.

---

Src/Guide/Reactant

Compiling Flux with Reactant

Reactant.jl traces your Julia code and compiles it — through MLIR and XLA — into a single optimised executable that runs on CPU, NVIDIA/AMD GPUs, or TPUs. For Flux this means the whole forward pass (and, for training, the Enzyme reverse pass and the optimiser update) is fused, kernel-optimised, and reused across calls, which is often substantially faster and more memory-efficient than eager execution.

This guide builds up in three steps:

1. A manually compiled example, so you can see exactly what Reactant does.
2. The
trainstep! API, which compiles and caches a single training step for you.
3.
train!, the full training loop, which is built on top of trainstep!.

Installation

Reactant is a normal dependency — add it and load it alongside Flux:

julia
using Pkg; Pkg.add("Reactant") # do this once

using Flux, Reactant

text
Reactant automatically selects a GPU backend if one is available, falling back to the CPU otherwise. See the Reactant GPU configuration docs for how to control this.

Moving data and models to the device

Just like gpu/cpu, Flux provides a device object that moves a model (or any nested structure of arrays) onto the Reactant device, converting its arrays into Reactant's ConcreteRArrays:

julia
using Flux, Reactant

dev = reactant_device() # a Reactant device object; call once and reuse

model = Chain(Dense(4 => 8, tanh), Dense(8 => 2))
x = randn(Float32, 4, 16)

model_re = model |> dev # a copy of the model with Reactant arrays
x_re = x |> dev

text
As with the GPU adaptors, dev recurses into structures, so (x, y) |> dev moves both, and it uses Float32 by default. Everything you feed to a compiled function must already live on the device.

1. A manually compiled example

Reactant does not run your code eagerly. Instead you compile a function once for a given set of input shapes and types, and then call the returned executable. The two entry points are:

- Reactant.@compile f(args...) — trace and compile f, returning a callable executable. Call it later with device-resident arguments of the same shapes.
-
Reactant.@jit f(args...) — compile and immediately run, a convenient shortcut for one-off calls.

Compiling the forward pass (inference)

julia
using Flux, Reactant

dev = reactant_device()
model = Chain(Dense(4 => 8, tanh), Dense(8 => 2)) |> dev
x = randn(Float32, 4, 16) |> dev

Compile once...


forward = Reactant.@compile model(x)

...then call the compiled executable (fast, no recompilation):


y = forward(x) # a Reactant array on the device
y_host = y |> cpu # move the result back to the host

@jit compiles and runs in one go — handy for a one-off evaluation:


y2 = Reactant.@jit model(x)
text
Note the call is forward(x), not forward(model, x): in @compile model(x) the model is the callee, so Reactant captures it (and its parameter arrays) inside the compiled executable, and you pass only the remaining arguments. Because the model is captured by reference, mutating its parameters in place — as training does — is reflected the next time you call forward. (Contrast this with the training-step examples below, where the model sits in an argument position, e.g. @compile my_step!(loss, model, x, y, opt_state), and so must be passed at call time.)

A compiled executable is specialised to the shapes of its inputs. Calling forward with a differently-shaped x (e.g. a smaller final batch) requires compiling a separate executable for that shape.

!!! note "trainmode / testmode"
Layers such as
Dropout and BatchNorm behave differently during training and inference. Call testmode!(model) before compiling an inference function, and trainmode!(model) before a training step, exactly as you would without Reactant.

Compiling a training step manually

To differentiate under Reactant, use Flux's withgradient with the AutoEnzyme backend — Reactant relies on Enzyme for AD. You can compile the value-and-gradient computation, or a whole step that also updates the model in place:

julia
using Flux, Reactant, Optimisers

dev = reactant_device()
model = Chain(Dense(4 => 8, tanh), Dense(8 => 2)) |> dev
x, y = randn(Float32, 4, 16) |> dev, randn(Float32, 2, 16) |> dev

loss(m, x, y) = Flux.mse(m(x), y)
opt_state = Flux.setup(Adam(1f-2), model)

A plain Julia function describing one optimisation step. It differentiates the loss


with Enzyme, updates the model and optimiser state in place, and returns the loss.


function my_step!(loss, model, x, y, opt_state)
l, grads = Flux.withgradient(m -> loss(m, x, y), AutoEnzyme(), model)
Optimisers.update!(opt_state, model, grads[1])
return l
end

trainmode!(model)
step! = Reactant.@compile my_step!(loss, model, x, y, opt_state) # compile once

for epoch in 1:100
l = step!(loss, model, x, y, opt_state) # reuse the executable each epoch
@info "epoch $epoch" loss=Reactant.to_number(l)
end

text
Reactant traces the mutation of model and opt_state and fuses the whole step. Writing this by hand gives you full control, but it is boilerplate that Flux can handle for you — which is what the next section is about.

2. The trainstep! API

trainstep! performs exactly the step above — differentiate the loss, update model and opt_state in place, return the loss — and when the model lives on a Reactant device it compiles and caches the fused step automatically. You do not write @compile yourself, and repeated calls with the same model, optimiser, loss and batch shape reuse the cached executable.

julia
using Flux, Reactant

dev = reactant_device()
model = Chain(Dense(4 => 8, tanh), Dense(8 => 2)) |> dev
x, y = randn(Float32, 4, 16) |> dev, randn(Float32, 2, 16) |> dev

loss(m, x, y) = Flux.mse(m(x), y)

Move the model to the device before setup, so the optimiser is set up for Reactant arrays.


opt_state = Flux.setup(Adam(1f-2), model)

trainmode!(model)
for epoch in 1:100
l = Flux.trainstep!(loss, model, (x, y), opt_state) # returns the host-side loss scalar
@info "epoch $epoch" loss=l
end

text
The batch is passed as a tuple (x, y) and spliced into the loss as loss(model, x, y). The returned loss is read back to the host as an ordinary number.

Use trainstep_withgradient! if you also need the gradient — it returns (loss, grad), with the gradient left on the device. (Returning the gradient makes it an output of the compiled step and raises peak memory, so prefer trainstep! when you don't need it.)

Auxiliary loss outputs

Like withgradient, the loss may return auxiliary data alongside the scalar loss: return a Tuple or NamedTuple whose first element is the loss, and the gradient is taken of the loss alone while the whole value is returned. This is convenient for logging a metric computed in the forward pass — and, on Reactant, the metric is computed on-device as part of the same compiled forward:

julia
function loss(m, x, y)
ŷ = m(x)
Flux.mse(ŷ, y), (; acc = mean(onecold(ŷ) .== onecold(y)))
end

l, stats = Flux.trainstep!(loss, model, (x, y), opt_state) # l == (loss, stats); stats.acc read to host

text

3. The train! API

train! is a loop over the data built on top of trainstep!, so on a Reactant device it inherits the same automatic compile-and-cache. Move the model to the device and call setup first, and provide device-resident data — train! rejects host arrays on the Reactant path.

julia
using Flux, Reactant

dev = reactant_device()
model = Chain(Dense(4 => 8, tanh), Dense(8 => 2)) |> dev
opt_state = Flux.setup(Adam(1f-2), model)

loss(m, x, y) = Flux.mse(m(x), y)

Move every batch to the device. In a real loop use a DataLoader wrapped with the device,


so each batch is moved lazily and the previous one is freed:


train_loader = DataLoader((X, Y); batchsize=32) |> dev


X, Y = randn(Float32, 4, 128), randn(Float32, 2, 128)
data = [(X[:, i:i+15], Y[:, i:i+15]) |> dev for i in 1:16:128]

Flux.train!(loss, model, data, opt_state)

text
train! runs a single step per batch, shows a progress bar, and stops with a DomainError if the loss becomes non-finite. Because the compiled step is cached and keyed on the batch shape, multi-epoch training does not recompile, and a smaller final batch simply compiles one additional executable that is then reused.

Tips and gotchas

- setup after moving to the device. Always model |> reactant_device() before Flux.setup, so the optimiser state is created from Reactant arrays (this keeps stateful rules like Adam's bias-correction term on the device).
- Device-resident data. Every input to a compiled step must already live on the device. Move batches with
|> reactant_device(); a common pattern is to wrap a DataLoader with the device so batches are moved lazily.
- Compilation is keyed on shape. The first call for each distinct batch shape compiles; subsequent calls reuse the executable. Keeping batch sizes fixed (or accepting one extra compile for a smaller final batch) avoids repeated compilation.
- Watch the compile cache. Flux caches one executable per distinct
(model, optimiser, loss, batch-shape). If you rebuild the model or optimiser every iteration, each iteration compiles a fresh step — Flux warns once the cache grows past a handful of entries. Entries are freed automatically when their model is garbage-collected.
- Reading results back.
trainstep!/train! return host-side numbers already. For a manually compiled function, move device arrays back with cpu (or Reactant.to_number for a scalar).
- AD backend. On a Reactant device training differentiates with Enzyme by default. Passing an explicit
adtype such as AutoZygote() or AutoMooncake() compilation is likely to fail. Duplicated models are not supported here — pass the plain model that already lives on the device.

See also the resnet_tinyimagenet example for a complete Reactant training script, including compiling a separate evaluation executable per batch shape.

---

Src/Guide/Saving

Saving and Loading Models

You may wish to save models so that they can be loaded and run in a later
session. Flux provides a number of ways to do this.
The recommended way, which is the most robust one for long term storage,
is to use
Flux.state in combination with a serialization format like
JLD2.jl or
BSON.jl.

Save a model:

jldoctest saving
julia> using Flux

julia> struct MyModel
net
end

julia> Flux.@layer MyModel

julia> MyModel() = MyModel(Chain(Dense(10 => 5, relu), Dense(5 => 2)));

julia> model = MyModel()
MyModel(
Chain(
Dense(10 => 5, relu), # 55 parameters
Dense(5 => 2), # 12 parameters
),
) # Total: 4 arrays, 67 parameters, 484 bytes.

julia> model_state = Flux.state(model);

julia> using JLD2

julia> jldsave("mymodel.jld2"; model_state)

text
Load it again in a new session using Flux.loadmodel!:
jldoctest saving
julia> using Flux, JLD2

julia> model_state = JLD2.load("mymodel.jld2", "model_state");

julia> model = MyModel(); # MyModel definition must be available

julia> Flux.loadmodel!(model, model_state);

text
!!! note

If a saved model's parameters are stored on the GPU, the model will not load
later on if there is no GPU support available. It's best to move your model
to the CPU
with
cpu(model) before saving it.


Checkpointing

In longer training runs it's a good idea to periodically save your model, so that you can resume if training is interrupted (for example, if there's a power cut).

jldoctest saving
julia> using Flux: throttle

julia> using JLD2

julia> m = Chain(Dense(10 => 5, relu), Dense(5 => 2))
Chain(
Dense(10 => 5, relu), # 55 parameters
Dense(5 => 2), # 12 parameters
) # Total: 4 arrays, 67 parameters, 476 bytes.

julia> for epoch in 1:10
# ... train model ...
jldsave("model-checkpoint.jld2", model_state = Flux.state(m))
end;

text
This will update the "model-checkpoint.jld2" every epoch.

You can get more advanced by saving a series of models throughout training, for example

julia
jldsave("model-$(now()).jld2", model_state = Flux.state(m))
text
will produce a series of models like "model-2018-03-06T02:57:10.41.jld2". You
could also store the current test set loss, so that it's easy to (for example)
revert to an older copy of the model if it starts to overfit.
julia
jldsave("model-$(now()).jld2", model_state = Flux.state(m), loss = testloss())
text
Note that to resume a model's training, you might need to restore other stateful parts of your training loop. Possible examples are the optimiser state and the randomness used to partition the original data into the training and validation sets.

You can store the optimiser state alongside the model, to resume training
exactly where you left off:

julia
model = MyModel()
opt_state = Flux.setup(AdamW(), model)

... train model ...

model_state = Flux.state(model)
jldsave("checkpoint_epoch=42.jld2"; model_state, opt_state)

text

Saving Models as Julia Structs

Models are just normal Julia structs, so it's fine to use any Julia storage
format to save the struct as it is instead of saving the state returned by
Flux.state.
BSON.jl is particularly convenient for this,
since it can also save anonymous functions, which are sometimes part of a model definition.

Save a model:

jldoctest saving
julia> using Flux

julia> model = Chain(Dense(10 => 5, NNlib.relu), Dense(5 => 2));

julia> using BSON: @save

julia> @save "mymodel.bson" model

text
Load it again in a new session:
jldoctest saving
julia> using Flux, BSON

julia> BSON.@load "mymodel.bson" model

julia> model
Chain(
Dense(10 => 5, relu), # 55 parameters
Dense(5 => 2), # 12 parameters
) # Total: 4 arrays, 67 parameters, 476 bytes.

text
!!! warning
Saving models this way could lead to compatibility issues across julia versions
and across Flux versions if some of the Flux layers' internals are changed.
It is therefore not recommended for long term storage, use
Flux.state instead.

---

Src/Reference/Data/Mldatadevices

@meta
CurrentModule = MLDataDevices
CollapsedDocStrings = true
text

Transferring data across devices

Flux relies on the MLDataDevices.jl package to manage devices and transfer data across them. You don't have to explicitly use the package, as Flux re-exports the necessary functions and types.

@docs
MLDataDevices.cpu_device
MLDataDevices.default_device_rng
MLDataDevices.functional
MLDataDevices.get_device
MLDataDevices.gpu_device
MLDataDevices.gpu_backend!
MLDataDevices.get_device_type
MLDataDevices.isleaf
MLDataDevices.loaded
MLDataDevices.reset_gpu_device!
MLDataDevices.set_device!
MLDataDevices.supported_gpu_backends
MLDataDevices.DeviceIterator
text
---

Src/Reference/Data/Mlutils

@meta
CurrentModule = Flux
CollapsedDocStrings = true
text

Working with Data, using MLUtils.jl

Flux re-exports the DataLoader type and utility functions for working with
data from MLUtils.

DataLoader

The DataLoader can be used to create mini-batches of data, in the format train! expects.

@docs
MLUtils.DataLoader
text

Utility Functions

The utility functions are meant to be used while working with data;
these functions help create inputs for your models or batch your dataset.

@docs
MLUtils.batch
MLUtils.batchsize
MLUtils.batchseq
MLUtils.batch_sequence
MLUtils.BatchView
MLUtils.chunk
MLUtils.eachobs
MLUtils.fill_like
MLUtils.filterobs
Flux.flatten
MLUtils.flatten
MLCore.getobs
MLCore.getobs!
MLUtils.joinobs
MLUtils.group_counts
MLUtils.group_indices
MLUtils.groupobs
MLUtils.kfolds
MLUtils.leavepout
MLUtils.mapobs
MLCore.numobs
MLUtils.normalise
MLUtils.obsview
MLUtils.ObsView
MLUtils.ones_like
MLUtils.oversample
MLUtils.randobs
MLUtils.rand_like
MLUtils.randn_like
MLUtils.rpad_constant
MLUtils.shuffleobs
MLUtils.splitobs
MLUtils.unbatch
MLUtils.undersample
MLUtils.unsqueeze
MLUtils.unstack
MLUtils.zeros_like
text
---

Src/Reference/Data/Onehot

@meta
CollapsedDocStrings = true
text

One-Hot Encoding with OneHotArrays.jl

It's common to encode categorical variables (like true, false or cat, dog) in "one-of-k" or "one-hot" form. OneHotArrays.jl provides the onehot function to make this easy.

jldoctest onehot
julia> using OneHotArrays

julia> onehot(:b, [:a, :b, :c])
3-element OneHotVector(::UInt32) with eltype Bool:

1

julia> onehot(:c, [:a, :b, :c])
3-element OneHotVector(::UInt32) with eltype Bool:


1

text
There is also a onecold function, which is an inverse of onehot. It can also be given an array of numbers instead of booleans, in which case it performs an argmax-like operation, returning the label with the highest corresponding weight.
jldoctest onehot
julia> onecold(ans, [:a, :b, :c])
:c

julia> onecold([true, false, false], [:a, :b, :c])
:a

julia> onecold([0.3, 0.2, 0.5], [:a, :b, :c])
:c

text
For multiple samples at once, onehotbatch creates a batch (matrix) of one-hot vectors, and onecold treats matrices as batches.
jldoctest onehot
julia> using OneHotArrays

julia> onehotbatch([:b, :a, :b], [:a, :b, :c])
3×3 OneHotMatrix(::Vector{UInt32}) with eltype Bool:
⋅ 1 ⋅
1 ⋅ 1
⋅ ⋅ ⋅

julia> onecold(ans, [:a, :b, :c])
3-element Vector{Symbol}:
:b
:a
:b

text
Note that these operations returned OneHotVector and OneHotMatrix rather than Arrays. OneHotVectors behave like normal vectors but avoid any unnecessary cost compared to using an integer index directly. For example, multiplying a matrix with a one-hot vector simply slices out the relevant row of the matrix under the hood.

Function listing

@docs
OneHotArrays.onehot
OneHotArrays.onecold
OneHotArrays.onehotbatch
OneHotArrays.OneHotArray
OneHotArrays.OneHotVector
OneHotArrays.OneHotMatrix
text
---

Src/Reference/Models/Activation

@meta
CollapsedDocStrings = true
text

Activation Functions from NNlib.jl

These non-linearities used between layers of your model are exported by the NNlib package.

Note that, unless otherwise stated, activation functions operate on scalars. To apply them to an array you can call σ.(xs), relu.(xs) and so on. Alternatively, they can be passed to a layer like Dense(784 => 1024, relu) which will handle this broadcasting.

Functions like softmax are sometimes described as activation functions, but not by Flux. They must see all the outputs, and hence cannot be broadcasted. See the next page for details.

Alphabetical Listing

@docs
celu
elu
gelu
hardsigmoid
hardswish
hardtanh
leakyrelu
lisht
logcosh
logsigmoid
mish
relu
relu6
rrelu
selu
sigmoid
sigmoid_fast
softplus
softshrink
softsign
swish
tanhshrink
tanh_fast
trelu
text

One More

Julia's Base.Math also provides tanh, which can be used as an activation function.

Note that many Flux layers will automatically replace this with NNlib.tanh_fast when called, as Base's tanh is slow enough to sometimes be a bottleneck.

julia-repl
julia> using UnicodePlots

julia> lineplot(tanh, -3, 3, height=7)
┌────────────────────────────────────────┐
1 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⣀⠤⠔⠒⠒⠉⠉⠉⠉⠉⠉⠉⠉⠉│ tanh(x)
│⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⠀⠀⡠⠖⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
│⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡇⡰⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
f(x) │⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⡤⡯⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤⠤│
│⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠎⠁⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
│⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠴⠊⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
-1 │⣀⣀⣀⣀⣀⣀⣀⣀⣀⡤⠤⠔⠒⠉⠁⠀⠀⠀⠀⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│
└────────────────────────────────────────┘
⠀-3⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀3⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀x⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀

text
---

Src/Reference/Models/Functors

@meta
CollapsedDocStrings = true
text

Recursive transformations from Functors.jl

Flux models are deeply nested structures, and Functors.jl provides tools needed to explore such objects, apply functions to the parameters they contain (e.g. for moving them to gpu), and re-build them.

!!! compat "Flux ≤ v0.14"
All layers were previously defined with the
Functors.@functor macro.
This still works, but it is recommended that you use the new
Flux.@layer macro instead.
Both allow
Flux.setup to see the parameters inside, and gpu to move them to the GPU, but Flux.@layer also overloads printing,
and offers a way to define
trainable at the same time.

!!! compat "Functors v0.5"
With Functors.jl v0.5, which is required by Flux v0.15 and later, every custom type is a functor by default. This means that applying
Flux.@layer to a type is no longer strictly necessary, but it is still recommended for addictional features like pretty-printing.

Functors.jl has its own notes on basic usage for more details. Additionally, the Advanced Model Building and Customisation page covers the use cases of Functors in greater details.

@docs
Flux.@layer
Functors.@leaf
Functors.@functor
Functors.fmap
Functors.fmap_with_path
Functors.isleaf
Functors.children
Functors.fcollect
Functors.functor
Functors.fmapstructure
Functors.fmapstructure_with_path
Functors.execute
Functors.AbstractWalk
Functors.ExcludeWalk
Functors.CachedWalk
text

Moving models, or data, to the GPU

Flux provides some convenience functions based on fmap. Some (f16, bf16, f32, f64) change the precision of all arrays in a model. Others are used for moving a model to of from GPU memory:

@docs
cpu
gpu(::Any)
gpu(::Flux.DataLoader)
text
---

Src/Reference/Models/Layers

@meta
CollapsedDocStrings = true
text

Built-in Layer Types

If you started at the beginning of the guide, then you have already met the
basic
Dense layer, and seen Chain for combining layers.
These core layers form the foundation of almost all neural networks.

The Dense exemplifies several features:

* It contains an an activation function, which is broadcasted over the output. Because this broadcast can be fused with other operations, doing so is more efficient than applying the activation function separately.

* It take an init keyword, which accepts a function acting like rand. That is, init(2,3,4) should create an array of this size. Flux has many such functions built-in. All make a CPU array, moved later with gpu if desired.

* The bias vector is always initialised Flux.zeros32. The keyword bias=false will turn this off, i.e. keeping the bias permanently zero.

* It is annotated with @layer, which means that Flux.setup will see the contents, and gpu will move their arrays to the GPU.

By contrast, Chain itself contains no parameters, but connects other layers together.
The section on dataflow layers introduces others like this.

Fully Connected

@docs
Dense
Flux.Bilinear
Flux.Scale
text
Perhaps Scale isn't quite fully connected, but it may be thought of as Dense(Diagonal(s.weights), s.bias), and LinearAlgebra's Diagonal is a matrix which just happens to contain many zeros.


Convolution Models

These layers are used to build convolutional neural networks (CNNs).

They all expect images in what is called WHCN order: a batch of 32 colour images, each 50 x 50 pixels, will have size(x) == (50, 50, 3, 32). A single grayscale image might instead have size(x) == (28, 28, 1, 1).

Besides images, 2D data, they also work with 1D data, where for instance stereo sound recording with 1000 samples might have size(x) == (1000, 2, 1). They will also work with 3D data, ndims(x) == 5, where again the last two dimensions are channel and batch.

To understand how strides and padding work, the article by Dumoulin & Visin has great illustrations.

@docs
Conv
ConvTranspose
CrossCor
DepthwiseConv
text

MultiHeadAttention

The basic blocks needed to implement Transformer architectures. See also the functional counterparts
documented in NNlib's Attention section.

@docs
MultiHeadAttention
text

Pooling

These layers are commonly used after a convolution layer, and reduce the size of its output. They have no trainable parameters.

@docs
AdaptiveMaxPool
MaxPool
GlobalMaxPool
AdaptiveMeanPool
MeanPool
GlobalMeanPool
text

Upsampling

The opposite of pooling, these layers increase the size of an array. They have no trainable parameters.

@docs
Upsample
PixelShuffle
text

Embedding Vectors

These layers accept an index, and return a vector (or several indices, and several vectors). The possible embedding vectors are learned parameters.

@docs
Flux.Embedding
Flux.EmbeddingBag
text

Dataflow Layers, or Containers

The basic Chain(F, G, H) applies the layers it contains in sequence, equivalent to H ∘ G ∘ F. Flux has some other layers which contain layers, but connect them up in a more complicated way: SkipConnection allows ResNet's residual connection.

@docs
Chain
Flux.activations
Maxout
SkipConnection
Parallel
PairwiseFusion
text

Recurrent Models

Much like the core layers above, but can be used to process sequence data (as well as other kinds of structured data).

@docs
Recurrence
RNNCell
RNN
LSTMCell
LSTM
GRUCell
GRU
GRUv3Cell
GRUv3
Flux.initialstates
text

Normalisation & Regularisation

These layers don't affect the structure of the network but may improve training times or reduce overfitting. Some of them contain trainable parameters, while others do not.

@docs
BatchNorm
Dropout
AlphaDropout
LayerNorm
InstanceNorm
GroupNorm
WeightNorm
Flux.remove_weight_norms
Flux.normalise
text

Test vs. Train

Several normalisation layers behave differently under training and inference (testing). By default, Flux will automatically determine when a layer evaluation is part of training or inference.

!!! warning
This automatic train/test detection works best with Zygote, the default
automatic differentiation package. It may not work with other packages
such as Tracker, Yota, or ForwardDiff.

The functions Flux.trainmode! and Flux.testmode! let you manually specify which behaviour you want. When called on a model, they will place all layers within the model into the specified mode.

@docs
testmode!
trainmode!
text
---

Src/Reference/Models/Losses

@meta
CollapsedDocStrings = true
text

Loss Functions

Flux provides a large number of common loss functions used for training machine learning models.
They are grouped together in the
Flux.Losses module.

Loss functions for supervised learning typically expect as inputs a target y, and a prediction from your model.
In Flux's convention, the order of the arguments is the following

julia
loss(ŷ, y)
text
Most loss functions in Flux have an optional argument agg, denoting the type of aggregation performed over the
batch:
julia
loss(ŷ, y) # defaults to
mean
loss(ŷ, y, agg=sum) # use
sum for reduction
loss(ŷ, y, agg=x->sum(x, dims=2)) # partial reduction
loss(ŷ, y, agg=x->mean(w .* x)) # weighted mean
loss(ŷ, y, agg=identity) # no aggregation.
text

Function listing

@docs
Flux.Losses.mae
Flux.Losses.mse
Flux.Losses.msle
Flux.Losses.huber_loss
Flux.Losses.label_smoothing
Flux.Losses.crossentropy
Flux.Losses.logitcrossentropy
Flux.Losses.binarycrossentropy
Flux.Losses.logitbinarycrossentropy
Flux.Losses.kldivergence
Flux.Losses.poisson_loss
Flux.Losses.hinge_loss
Flux.Losses.squared_hinge_loss
Flux.Losses.dice_coeff_loss
Flux.Losses.tversky_loss
Flux.Losses.binary_focal_loss
Flux.Losses.focal_loss
Flux.Losses.siamese_contrastive_loss
text
---

Src/Reference/Models/Nnlib

@meta
CollapsedDocStrings = true
text

Neural Network primitives from NNlib.jl

Flux re-exports all of the functions exported by the NNlib package. This includes activation functions, described on their own page. Many of the functions on this page exist primarily as the internal implementation of Flux layer, but can also be used independently.


Attention

Primitives for the MultiHeadAttention layer.

@docs
NNlib.dot_product_attention
NNlib.dot_product_attention_scores
NNlib.make_causal_mask
text

Softmax

Flux's Flux.logitcrossentropy uses NNlib.logsoftmax internally.

@docs
softmax
logsoftmax
text

Pooling

Flux's AdaptiveMaxPool, AdaptiveMeanPool, GlobalMaxPool, GlobalMeanPool,
MaxPool, and MeanPool use NNlib.PoolDims, NNlib.maxpool, and NNlib.meanpool as their backend.

@docs
NNlib.PoolDims
NNlib.lpnormpool
NNlib.maxpool
NNlib.meanpool
text

Padding

@docs
NNlib.pad_circular
NNlib.pad_constant
NNlib.pad_reflect
NNlib.pad_repeat
NNlib.pad_symmetric
NNlib.pad_zeros
text

Convolution

Flux's Conv and CrossCor layers use NNlib.DenseConvDims and NNlib.conv internally.

@docs
conv
ConvDims
depthwiseconv
DepthwiseConvDims
DenseConvDims
text

Dropout

@docs
NNlib.dropout
NNlib.dropout!
text

Normalization

Flux's BatchNorm, InstanceNorm, GroupNorm, and LayerNorm layers wrap the functional normalization operators below as their backend.

@docs
NNlib.normalise
NNlib.batchnorm
NNlib.instancenorm
NNlib.groupnorm
NNlib.layernorm
text

Upsampling

Flux's Upsample layer uses NNlib.upsample_nearest, NNlib.upsample_bilinear, and NNlib.upsample_trilinear as its backend. Additionally, Flux's PixelShuffle layer uses NNlib.pixel_shuffle as its backend.

@docs
upsample_nearest
upsample_linear
∇upsample_linear
upsample_bilinear
∇upsample_bilinear
upsample_trilinear
∇upsample_trilinear
pixel_shuffle
text

Batched Operations

Flux's Flux.Bilinear layer uses NNlib.batched_mul internally.

@docs
batched_mul
batched_mul!
batched_adjoint
batched_transpose
batched_vec
text

Gather and Scatter

Flux's Embedding layer uses NNlib.gather as its backend.

@docs
NNlib.gather
NNlib.gather!
NNlib.scatter
NNlib.scatter!
text

Sampling

@docs
grid_sample
∇grid_sample
text

Losses

@docs
ctc_loss
text

Miscellaneous

@docs
logsumexp
NNlib.glu
text
---

Src/Reference/Training/Callbacks

@meta
CollapsedDocStrings = true
@docs
Flux.throttle
text

Patience Helpers

Flux provides utilities for controlling your training procedure according to some monitored condition and a maximum patience. For example, you can use early_stopping to stop training when the model is converging or deteriorating, or you can use plateau to check if the model is stagnating.

For example, below we create a pseudo-loss function that decreases, bottoms out, and then increases. The early stopping trigger will break the loop before the loss increases too much.

julia

create a pseudo-loss that decreases for 4 calls, then starts increasing


we call this like loss()


loss = let t = 0
() -> begin
t += 1
(t - 4) ^ 2
end
end

create an early stopping trigger


returns true when the loss increases for two consecutive steps


es = early_stopping(loss, 2; init_score = 9)

this will stop at the 6th (4 decreasing + 2 increasing calls) epoch


for epoch in 1:10
es() && break
end
text
The keyword argument distance of early_stopping is a function of the form distance(best_score, score). By default distance is - and init_score is Inf, meaning that the monitored metric f is expected to be decreasing and minimized. If you use a metric such that improvement is shown by increasing values (e.g. accuracy), you can customize the distance function and the init_score value to, for example, (best_score, score) -> score - best_score and -Inf, respectively.
julia

create a pseudo-accuracy that increases by 0.01 each time from 0 to 1


we call this like acc()


acc = let v = 0
() -> v = max(1, v + 0.01)
end

create an early stopping trigger for accuracy


es = early_stopping(acc, 3; delta = (best_score, score) -> score - best_score, init_score = -Inf)

this will iterate until the 10th epoch


for epoch in 1:10
es() && break
end
text
early_stopping and plateau are both built on top of patience. You can use patience to build your own triggers that use a patient counter. For example, if you want to trigger when the loss is below a threshold for several consecutive iterations:
julia
threshold(f, thresh, delay) = patience(delay) do
f() < thresh
end
text
Both predicate in patience and f in early_stopping / plateau can accept extra arguments. You can pass such extra arguments to predicate or f through the returned function:
julia
trigger = patience((a; b) -> a > b, 3)

this will iterate until the 10th epoch


for epoch in 1:10
trigger(1; b = 2) && break
end

this will stop at the 3rd epoch


for epoch in 1:10
trigger(3; b = 2) && break
end
text
@docs
Flux.patience
Flux.early_stopping
Flux.plateau
text
---

Src/Reference/Training/Gradients

@meta
CollapsedDocStrings = true
text

Automatic Differentiation in Flux

Flux's gradient function uses Zygote by default, and also uses this function within train! to differentiate the model.
Zygote has its own documentation, in particular listing some important limitations.

Flux also has support for Enzyme.jl, documented below and for Mooncake.jl.


Generic Gradient Interface

@docs
Flux.gradient(f, adtype::AbstractADType, args::Any...)
Flux.withgradient(f, adtype::AbstractADType, args::Any...)
text

Automatic Differentiation using Zygote.jl

The default AD backend in Flux is Zygote. Besides gradient calculation, Zygote also supports
higher-order derivatives, Jacobians, Hessians, and pullbacks.

@docs
Zygote.jacobian(f, args...)
Zygote.withjacobian(f, args...)
Zygote.hessian
Zygote.hessian_reverse
Zygote.diaghessian
Zygote.pullback
text

ChainRules for Zygote

Zygote uses ChainRules.jl to define how to differentiate functions.

Sometimes it is necessary to exclude some code, or a whole function, from automatic differentiation.
This can be done using the following methods:

@docs
ChainRulesCore.ignore_derivatives
ChainRulesCore.@non_differentiable
text
To manually supply the gradient for one function, you should define a method of rrule. ChainRules has detailed documentation on how this works.
@docs
ChainRulesCore.rrule
ChainRulesCore.frule
ChainRulesCore.@scalar_rule
ChainRulesCore.NoTangent
ChainRulesCore.ZeroTangent
ChainRulesCore.RuleConfig
ChainRulesCore.Tangent
ChainRulesCore.canonicalize
text
Gradient customization for other AD packages such as Enzyme and Mooncake has to be done according to their own documentation.

Automatic Differentiation using Enzyme.jl

Enzyme.jl is a new package for automatic differentiation.
Like Zygote.jl, calling
gradient(f, x) causes it to hooks into the compiler and transform code that is executed while calculating f(x), in order to produce code for ∂f/∂x.
But it does so much later in the optimisation process (on LLVM instead of Julia's untyped IR) which you can read about here].
It needs far fewer custom rules than Zygote/ChainRules, and in particular is able to support mutation of arrays.

Flux now builds in support for this, using Enzyme's own Duplicated type.
Calling
Duplicated on any Flux model which was defined using @layer will allocate space for the gradient,
and passing that to
gradient (or withgradient, or train!) will then use Enzyme instead of Zygote.
The gradient functions still return the gradient as usual, which can then be passed to
update!:

julia-repl
julia> using Flux, Enzyme

julia> model = Chain(Dense(28^2 => 32, sigmoid), Dense(32 => 10), softmax); # from model zoo

julia> dup_model = Enzyme.Duplicated(model) # this allocates space for the gradient
Duplicated(
Chain(
Dense(784 => 32, σ), # 25_120 parameters
Dense(32 => 10), # 330 parameters
NNlib.softmax,
),
# norm(∇) ≈ 0.0f0
) # Total: 4 arrays, 25_450 parameters, 199.391 KiB.

julia> x1 = randn32(28*28, 1); # fake image

julia> y1 = [i==3 for i in 0:9]; # fake label

julia> grads_f = Flux.gradient((m,x,y) -> sum(abs2, m(x) .- y), dup_model, Const(x1), Const(y1)) # uses Enzyme
((layers = ((weight = Float32[-0.010354728 0.032972857 …
-0.0014538406], σ = nothing), nothing),), nothing, nothing)

text
The gradient returned here is also stored within dup_model.
Both share the same arrays -- what is returned is not a copy, just a view of the same memory (wrapped in
NamedTuples instead of structs).
They will all be set to zero when you call
gradient again, then replaced with the new values.
Alternatively,
gradient(f, args...; zero=false) will add the new gradient to what's already stored.

Writing Const(x1) is optional, just plain x1 is implicitly constant.
Any set of
Duplicated and Const arguments may appear in any order, so long as there is at least one Duplicated.

The gradient grads_f[1] can be passed to update! as usual.
But for convenience, you may also use what is stored within
Duplicated.
These are equivalent ways to perform an update step:

julia-repl
julia> opt_state = Flux.setup(Adam(), model)

julia> ans == Flux.setup(Adam(), dup_model)

julia> Flux.update!(opt_state, model, grads_f[1]) # exactly as for Zygote gradients

julia> Flux.update!(opt_state, dup_model) # equivalent new path, Enzyme only

text
Instead of using these FLux functions, you can also use Enzyme's own functions directly.
Enzyme.gradient works like this:
julia-repl
julia> grads_e = Enzyme.gradient(Reverse, (m,x,y) -> sum(abs2, m(x) .- y), model, Const(x1), Const(y1))
(Chain(Dense(784 => 32, σ), Dense(32 => 10), softmax), nothing, nothing)

julia> grads_f[1].layers[2].bias ≈ grads_e[1].layers[2].bias
true

text
Note that what Enzyme.gradient returns is an object like deepcopy(model) of the same type, grads_e[1] isa Chain.
But its fields contain the same gradient.
@docs
Flux.gradient(f, args::Union{Flux.EnzymeCore.Const, Flux.EnzymeCore.Duplicated}...)
Flux.withgradient(f, args::Union{Flux.EnzymeCore.Const, Flux.EnzymeCore.Duplicated}...)
text
Enzyme.jl has its own extensive documentation.


Second-order AD

If you calculate a gradient within the loss function, then training will involve 2nd derivatives.
While this is in principle supported by Zygote.jl, there are many bugs, and Enzyme.jl is probably a better choice.

---

Src/Reference/Training/Optimisers

@meta
CurrentModule = Flux
CollapsedDocStrings = true
text

Optimisation Rules

Any optimization rule from Optimisers.jl can be used with train! and
other training functions.

For full details of how the interface works, see the Optimisers.jl documentation.


Optimisers Reference

All optimisers return an object that, when passed to train!, will update the parameters passed to it.

@docs
Optimisers.Descent
Optimisers.Momentum
Optimisers.Nesterov
Optimisers.RMSProp
Optimisers.Adam
Optimisers.RAdam
Optimisers.AdaMax
Optimisers.AdaGrad
Optimisers.AdaDelta
Optimisers.AMSGrad
Optimisers.NAdam
Optimisers.AdamW
Optimisers.OAdam
Optimisers.AdaBelief
Optimisers.Lion
text

Composing Optimisers

Flux (through Optimisers.jl) defines a special kind of optimiser called OptimiserChain which takes in arbitrary optimisers as input. Its behaviour is similar to the usual optimisers, but differs in that it acts by calling the optimisers listed in it sequentially. Each optimiser produces a modified gradient
that will be fed into the next, and the resultant update will be applied to the parameter as usual. A classic use case is where adding decays is desirable. Optimisers.jl defines the basic decay corresponding to an $L_2$ regularization in the loss as
WeightDecay.

julia
opt = OptimiserChain(WeightDecay(1e-4), Descent())
text
Here we apply the weight decay to the Descent optimiser. 
The resulting optimiser
opt can be used as any optimiser.
julia
w = [randn(10, 10), randn(10, 10)]
opt_state = Flux.setup(opt, w)

loss(w, x) = Flux.mse(w[1] x, w[2] x)

loss(w, rand(10)) # around 0.9

for t = 1:10^5
g = gradient(w -> loss(w[1], w[2], rand(10)), w)
Flux.update!(opt_state, w, g)
end

loss(w, rand(10)) # around 0.9

text
It is possible to compose optimisers for some added flexibility.
@docs
Optimisers.OptimiserChain
text

Decays

Similar to optimisers, Flux also defines some simple decays that can be used in conjunction with other optimisers, or standalone.

@docs
Optimisers.SignDecay
Optimisers.WeightDecay
text

Gradient Clipping

Gradient clipping is useful for training recurrent neural networks, which have a tendency to suffer from the exploding gradient problem. An example usage is

julia
opt = OptimiserChain(ClipGrad(1e-3), Adam(1e-3))
text
@docs
Optimisers.ClipGrad
Optimisers.ClipNorm
text
---

Src/Reference/Training/Reference

@meta
CollapsedDocStrings = true
text

Training API Reference

The new version of Flux's training code was written as an independent package, Optimisers.jl.
Only the function
train! belongs to Flux itself.

The Optimisers package is designed to allow for immutable objects. But at present all Flux models contain parameter arrays (such as Arrays and CuArrays) which can be updated in-place.
Because of this:

* The objects returned by Optimisers.update! can be ignored.
* Flux defines its own version of
setup which checks this assumption.
(Using instead
Optimisers.setup will also work, they return the same thing.)

The available optimization rules are listed the optimisation rules page here. See the Optimisers documentation for details on how the rules work.

@docs
Flux.Train.setup
Flux.Train.train!
Flux.Train.trainstep!
Flux.Train.trainstep_withgradient!
Optimisers.update
Optimisers.update!
Optimisers.setup
text
train! uses @progress which should show a progress bar in VSCode automatically.
To see one in a terminal, you will need to install TerminalLoggers.jl
and follow its setup instructions.


There is also a method of
train! which similarly takes Duplicated(model) and uses Enzyme.jl for differentiation (see (@ref autodiff-enzyme)):

julia-repl
julia> opt_state = Flux.setup(Adam(0), model);

julia> Flux.train!((m,x,y) -> sum(abs2, m(x) .- y), dup_model, [(x1, y1)], opt_state)

text

Optimisation Modifiers

The state returned by setup can be modified to temporarily prevent training of
some parts of the model, or to change the learning rate or other hyperparameter.
The functions for doing so may be accessed as
Flux.freeze!, Flux.thaw!, and Flux.adjust!.
All mutate the state (or part of it) and return
nothing.

@docs
Optimisers.adjust!
Optimisers.freeze!
Optimisers.thaw!
text
---

Src/Reference/Destructure

@meta
CurrentModule = Flux
CollapsedDocStrings = true
text

Flat vs. Nested Structures


A Flux model is a nested structure, with parameters stored within many layers. Sometimes you may want a flat representation of them, to interact with functions expecting just one vector. This is provided by
destructure:

julia-repl
julia> model = Chain(Dense(2=>1, tanh), Dense(1=>1))
Chain(
Dense(2 => 1, tanh), # 3 parameters
Dense(1 => 1), # 2 parameters
) # Total: 4 arrays, 5 parameters, 276 bytes.

julia> flat, rebuild = Flux.destructure(model)
(Float32[0.863101, 1.2454957, 0.0, -1.6345707, 0.0], Restructure(Chain, ..., 5))

julia> rebuild(zeros(5)) # same structure, new parameters
Chain(
Dense(2 => 1, tanh), # 3 parameters (all zero)
Dense(1 => 1), # 2 parameters (all zero)
) # Total: 4 arrays, 5 parameters, 276 bytes.

text
Both destructure and the Restructure function can be used within gradient computations. For instance, this computes the Hessian ∂²L/∂θᵢ∂θⱼ of some loss function, with respect to all parameters of the Flux model. The resulting matrix has off-diagonal entries, which cannot really be expressed in a nested structure:
julia-repl
julia> x = rand(Float32, 2, 16);

julia> grad = gradient(m -> sum(abs2, m(x)), model) # nested gradient
((layers = ((weight = Float32[10.339018 11.379145], bias = Float32[22.845667], σ = nothing), (weight = Float32[-29.565302;;], bias = Float32[-37.644184], σ = nothing)),),)

julia> function loss(v::Vector)
m = rebuild(v)
y = m(x)
sum(abs2, y)
end;

julia> gradient(loss, flat) # flat gradient, same numbers
(Float32[10.339018, 11.379145, 22.845667, -29.565302, -37.644184],)

julia> Zygote.hessian(loss, flat) # second derivative
5×5 Matrix{Float32}:
-7.13131 -5.54714 -11.1393 -12.6504 -8.13492
-5.54714 -7.11092 -11.0208 -13.9231 -9.36316
-11.1393 -11.0208 -13.7126 -27.9531 -22.741
-12.6504 -13.9231 -27.9531 18.0875 23.03
-8.13492 -9.36316 -22.741 23.03 32.0

julia> Flux.destructure(grad) # acts on non-models, too
(Float32[10.339018, 11.379145, 22.845667, -29.565302, -37.644184], Restructure(Tuple, ..., 5))

text
In order to collect all parameters of a model into a list instead, you can use the trainables function:
julia-repl
julia> Flux.trainables(model)
5-element Vector{AbstractArray}:
[0.863101 1.2454957]
[0.0]
[1.290355429422727;;]
[0.0]
text
Any mutation of the elements of the resulting list will affect the model's parameters.

All Parameters

The functions destructure and trainables live in Optimisers.jl.

@docs
Optimisers.destructure
Optimisers.trainable
Optimisers.trainables
Optimisers.isnumeric
Flux.params
text

All Layers

Another kind of flat view of a nested model is provided by the modules command. This extracts a list of all layers:

@docs
Flux.modules
text

Save and Load

@docs
Flux.state
Flux.loadmodel!
text

KeyPath

@docs
Functors.KeyPath
Functors.getkeypath
Functors.haskeypath
Functors.setkeypath!
text
---

Src/Reference/Outputsize

@meta
CollapsedDocStrings = true
text

Shape Inference

Flux has some tools to help generate models in an automated fashion, by inferring the size
of arrays that layers will receive, without doing any computation.
This is especially useful for convolutional models, where the same
Conv layer
accepts any size of image, but the next layer may not.

The higher-level tool is a macro @autosize which acts on the code defining the layers,
and replaces each appearance of
_ with the relevant size. This simple example returns a model
with
Dense(845 => 10) as the last layer:

julia
@autosize (28, 28, 1, 32) Chain(Conv((3, 3), _ => 5, relu, stride=2), Flux.flatten, Dense(_ => 10))
text
The input size may be provided at runtime, like @autosize (sz..., 1, 32) Chain(Conv(..., but all the
layer constructors containing
_ must be explicitly written out -- the macro sees the code as written.

This macro relies on a lower-level function outputsize, which you can also use directly:

julia
c = Conv((3, 3), 1 => 5, relu, stride=2)
Flux.outputsize(c, (28, 28, 1, 32)) # returns (13, 13, 5, 32)
text
The function outputsize works by passing a "dummy" array into the model, which propagates through very cheaply.
It should work for all layers, including custom layers, out of the box.

An example of how to automate model building is this:

jldoctest; output = false, setup = :(using Flux)
"""
make_model(width, height, [inchannels, nclasses; layer_config])

Create a CNN for a given set of configuration parameters. Arguments:
-
width, height: the input image size in pixels
-
inchannels: the number of channels in the input image, default 1
-
nclasses: the number of output classes, default 10
- Keyword
layer_config: a vector of the number of channels per layer, default [16, 16, 32, 64]
"""
function make_model(width, height, inchannels = 1, nclasses = 10;
layer_config = [16, 16, 32, 64])
# construct a vector of layers:
conv_layers = []
push!(conv_layers, Conv((5, 5), inchannels => layer_config[1], relu, pad=:same))
for (inch, outch) in zip(layer_config, layer_config[2:end])
push!(conv_layers, Conv((3, 3), inch => outch, sigmoid, stride=2))
end

# compute the output dimensions after these conv layers:
conv_outsize = Flux.outputsize(conv_layers, (width, height, inchannels); padbatch=true)

# use this to define appropriate Dense layer:
last_layer = Dense(prod(conv_outsize) => nclasses)
return Chain(conv_layers..., Flux.flatten, last_layer)
end

m = make_model(28, 28, 3, layer_config = [9, 17, 33, 65])

Flux.outputsize(m, (28, 28, 3, 42)) == (10, 42) == size(m(randn(Float32, 28, 28, 3, 42)))

output

true

text
Alternatively, using the macro, the definition of make_model could end with:

# compute the output dimensions & construct appropriate Dense layer:
return @autosize (width, height, inchannels, 1) Chain(conv_layers..., Flux.flatten, Dense(_ => nclasses))
end
text

Listing

@docs
Flux.@autosize
Flux.outputsize
text
---

Src/Reference/Utilities

@meta
CurrentModule = Flux
CollapsedDocStrings = true
text

Random Weight Initialisation

Flux initialises convolutional layers and recurrent cells with glorot_uniform by default.
Most layers accept a function as an
init keyword, which replaces this default. For example:

jldoctest; setup = :(using Flux)
julia> conv = Conv((3, 3), 3 => 2, relu; init=Flux.glorot_normal)
Conv((3, 3), 3 => 2, relu) # 56 parameters

julia> conv.bias
2-element Vector{Float32}:
0.0
0.0

text
Note that init creates the weight array, but not the bias vector.

Many of the initialisation functions accept keywords such as gain,
and a random number generator. To make it easy to pass these to layers,
there are methods which return a function:

jldoctest; setup = :(using Flux, Random)
julia> Dense(4 => 5, tanh; init=Flux.glorot_uniform(gain=2))
Dense(4 => 5, tanh) # 25 parameters

julia> Dense(4 => 5, tanh; init=Flux.randn32(MersenneTwister(1)))
Dense(4 => 5, tanh) # 25 parameters

text

Initialisation functions

@docs
Flux.glorot_uniform
Flux.glorot_normal
Flux.kaiming_uniform
Flux.kaiming_normal
Flux.truncated_normal
Flux.lecun_normal
Flux.orthogonal
Flux.sparse_init
Flux.identity_init
Flux.ones32
Flux.zeros32
Flux.rand32
Flux.randn32
Flux.create_bias
text
These functions call:
@docs
Flux.rng_from_array
Flux.nfan
text

Changing the type of all parameters

The default eltype for models is Float32 since models are often trained/run on GPUs.
The
eltype of model m can be changed to Float64 by f64(m):

@docs
Flux.f64
Flux.f32
Flux.f16
Flux.bf16
text
---

Src/Tutorials/Custom Layers

Defining Customised Layers

Here we will try and describe usage of some more advanced features that Flux provides to give more control over model building.

Custom Model Example

Here is a basic example of a custom model. It simply adds the input to the result from the neural network.

julia
struct CustomModel{T <: Chain} # Parameter to avoid type instability
chain::T
end

function (m::CustomModel)(x)
# Arbitrary code can go here, but note that everything will be differentiated.
# Zygote does not allow some operations, like mutating arrays.

return m.chain(x) + x
end

This is optional but recommended for pretty printing and other niceties


Flux.@layer CustomModel
text
Notice that we parameterized the type of the chain field. This is necessary for fast Julia code, so that that struct field can be given a concrete type. Chains have a type parameter fully specifying the types of the layers they contain. By using a type parameter, we are freeing Julia to determine the correct concrete type, so that we do not need to specify the full, possibly quite long, type ourselves.

You can then use the model like:

julia
chain = Chain(Dense(10 => 10, relu), Dense(10 => 10))
model = CustomModel(chain)
model(rand(Float32, 10))
text
For an intro to Flux and automatic differentiation, see this tutorial.

Customising Parameter Collection for a Model

Taking reference from our example Affine layer from the basics.

By default all the fields in the Affine type are collected as its parameters, however, in some cases it may be desired to hold other metadata in our "layers" that may not be needed for training, and are hence supposed to be ignored while the parameters are collected. With Flux, the way to mark some fields of our layer as trainable is through overloading the trainable function:

julia-repl
julia> struct Affine
W
b
end

julia> Affine(in::Int, out::Int) = Affine(randn(out, in), randn(out));

julia> (m::Affine)(x) = m.W * x .+ m.b;

julia> Flux.@layer Affine

julia> a = Affine(Float32[1 2; 3 4; 5 6], Float32[7, 8, 9])
Affine(Float32[1.0 2.0; 3.0 4.0; 5.0 6.0], Float32[7.0, 8.0, 9.0])

julia> Flux.trainable(a) # default behavior
(W = Float32[1.0 2.0; 3.0 4.0; 5.0 6.0], b = Float32[7.0, 8.0, 9.0])

julia> Flux.trainable(a::Affine) = (; W = a.W) # returns a NamedTuple using the field's name

julia> Flux.trainable(a)
(W = Float32[1.0 2.0; 3.0 4.0; 5.0 6.0],)

text
Only the fields returned by trainable will be seen by Flux.setup and Flux.update! for training. But all fields will be seen by gpu and similar functions, for example:
julia-repl
julia> a |> f16
Affine(Float16[1.0 2.0; 3.0 4.0; 5.0 6.0], Float16[7.0, 8.0, 9.0])
text
Note that there is no need to overload trainable to hide fields which do not contain numerical array (for example, activation functions, or Boolean flags). These are always ignored by training.

The exact same method of trainable can also be defined using the macro, for convenience:

julia
Flux.@layer Affine trainable=(W,)
text
There is a second, more severe, kind of restriction possible. This is not recommended, but is included here for completeness. Calling Functors.@functor Affine (W,) means that no exploration of the model will ever visit the other fields: They will not be moved to the GPU by gpu, and their precision will not be changed by f32. This requires the struct to have a corresponding constructor that accepts only W as an argument.

Custom multiple input or output layer

Sometimes a model needs to receive several separate inputs at once or produce several separate outputs at once. In other words, there multiple paths within this high-level layer, each processing a different input or producing a different output. A simple example of this in machine learning literature is the inception module.

We could have a struct that stores the weights of along each path and implement the joining/splitting in the forward pass function. That would mean a new struct for each different block,
e.g. one would have a
TransformerBlock struct for a transformer block, and a ResNetBlock struct for a ResNet block, each block being composed by smaller sub-blocks. This is often the simplest and cleanest way to implement complex models.

This guide instead will show you how to construct a high-level layer (like Chain) that is made of multiple sub-layers for each path. It may be the case that using the layers described as follows makes the definition of your model harder to read and to change. In that case, consider using the simpler approach of defining a custom structure described above.

Multiple inputs: a custom Join layer

Our custom Join layer will accept multiple inputs at once, pass each input through a separate path, then combine the results together. Note that this layer can already be constructed using Parallel, but we will first walk through how do this manually.

We start by defining a new struct, Join, that stores the different paths and a combine operation as its fields.

julia
using Flux
using CUDA

custom join layer


struct Join{T, F}
combine::F
paths::T
end

allow Join(op, m1, m2, ...) as a constructor


Join(combine, paths...) = Join(combine, paths)
text
Notice again that we parameterized the type of the combine and paths fields. In addition to the performance considerations of concrete types, this allows either field to be Vectors, Tuples, or one of each - we don't need to pay attention to which.

The next step is to use Flux.@layer to make our struct behave like a Flux layer.
In Flux < v0.15 this used to be important so that calling
Flux.setup on a Join maps over the underlying trainable arrays on each path. Since Flux v0.15, this is no longer necessary, since now Functors.jl automatically traverses custom types. However, Flux.@layer is still recommended for pretty printing and other niceties.

julia
Flux.@layer Join
text
Finally, we define the forward pass. For Join, this means applying each path in paths to each input array, then using combine to merge the results.
julia
(m::Join)(xs::Tuple) = m.combine(map((f, x) -> f(x), m.paths, xs)...)
(m::Join)(xs...) = m(xs)
text
Lastly, we can test our new layer. Thanks to the proper abstractions in Julia, our layer works on GPU arrays out of the box!
julia
model = Chain(
Join(vcat,
Chain(Dense(1 => 5, relu), Dense(5 => 1)), # branch 1
Dense(1 => 2), # branch 2
Dense(1 => 1) # branch 3
),
Dense(4 => 1)
) |> gpu

xs = map(gpu, (rand(1), rand(1), rand(1)))

model(xs)

returns a single float vector with one value


text
!!! note
This
Join layer is available from the Fluxperimental.jl package.


#### Using
Parallel

Flux already provides Parallel that can offer the same functionality. In this case, Join is going to just be syntactic sugar for Parallel.

julia
Join(combine, paths) = Parallel(combine, paths)
Join(combine, paths...) = Join(combine, paths)

use vararg/tuple version of Parallel forward pass


model = Chain(
Join(vcat,
Chain(Dense(1 => 5, relu), Dense(5 => 1)),
Dense(1 => 2),
Dense(1 => 1)
),
Dense(4 => 1)
) |> gpu

xs = map(gpu, (rand(1), rand(1), rand(1)))

model(xs)

returns a single float vector with one value


text

Multiple outputs: a custom Split layer

Our custom Split layer will accept a single input, then pass the input through a separate path to produce multiple outputs.

We start by following the same steps as the Join layer: define a struct, use Flux.@layer, and define the forward pass.

julia
using Flux
using CUDA

custom split layer


struct Split{T}
paths::T
end

Split(paths...) = Split(paths)

Flux.@layer Split

(m::Split)(x::AbstractArray) = map(f -> f(x), m.paths)

text
Now we can test to see that our Split does indeed produce multiple outputs.
julia
model = Chain(
Dense(10 => 5),
Split(Dense(5 => 1, tanh), Dense(5 => 3, tanh), Dense(5 => 2))
) |> gpu

model(gpu(rand(10)))

returns a tuple with three float vectors


text
A custom loss function for the multiple outputs may look like this:
julia
using Statistics

assuming model returns the output of a Split


x is a single input


ys is a tuple of outputs


function loss(x, ys, model)
# rms over all the mse
ŷs = model(x)
return sqrt(mean(Flux.mse(y, ŷ) for (y, ŷ) in zip(ys, ŷs)))
end
`

!!! note
This
Split` layer is available from the Fluxperimental.jl package.

---