Automatic differentiation from scratch

By: Pablo Rodríguez-Sánchez

Re-posted from: https://blog.esciencecenter.nl/automatic-differentiation-from-scratch-23d50c699555?source=rss----ab3660314556--julia

A surprisingly simple and elegant way to teach your computer how to perform derivatives, with some Julia (and Python) examples

Photo by Crissy Jarvis on Unsplash

First, a disclaimer

Automatic differentiation is a well-known sub-field of applied mathematics. You definitely don’t have to implement it from scratch, unless, as I did, you want to. And why would you want to do such a thing? My motivation was a mix of the following:

  • I like to understand what the packages I use do
  • The theory behind automatic differentiation happens to be very beautiful
  • I could use it as a case study to improve my understanding of the Julia language

Furthermore, if you are interested in performance, you’d likely want to focus on backward automatic differentiation, and not, as I did, on the forward one.

If you are still reading, it means that after all these disclaimers your intrinsic motivation is still intact. Great! Let me introduce you to the fascinating topic of automatic differentiation and my (quick and dirty) implementation.

Enter the dual numbers

Probably you remember it from your high school years. The nightmare of derivatives! All those tables you had to memorize, all those rules you had to apply… chances are that it is not a good memory!

Would it be possible to teach a computer the rules of differentiation? The answer is yes! It is not only possible but can even be elegant. Enter the dual numbers! A dual number is very similar to a two-dimensional vector:

the first element represents the value of a function at a given point, and the second one is its derivative at the same point. For instance, the constant 3 will be written as the dual number (3, 0) (the 0 means that it’s a constant and thus its derivative is 0) and the variable x = 3 will be written as (3,1) (the 1 meaning that 3 is an evaluation of the variable x, and thus its derivative respective to x is 1). I know this sounds strange, but stick with me; it will become clearer later.

So, we have a new mathematical toy. We have to write down the game rules if we want to have any fun with it: let’s start defining addition, subtraction, and multiplication by a scalar. We decide they follow exactly the same rules that vectors do:

So far, nothing exciting. The multiplication is defined in a more interesting way:

Why? Because we said the second term represents a derivative, it has to follow the product rule for derivatives.

What about quotients? You guessed… the division of dual numbers follows the quotient rule for derivatives:

Last but not least, the power of a dual number to a real number is defined as:

Perhaps you feel curiosity about the multiplication by u’. This corresponds to the chain rule, and enables our dual numbers for something as desirable as function composition.

The operations defined above cover a lot of ground. Indeed, any algebraic operation can be built using them as basic components. This means that we can pass a dual number as the argument of an algebraic function, and here comes the magic, the result will be:

It is hard to overstate how powerful this is. The equation above tells us that just by feeding the function the dual number (x, 1) it will return its value at, plus its derivative! Two for the price of one!

Those readers familiar with complex numbers may find interesting to try the following exercise:

If we define a dual number as

(u, u’) = u + e u’

with e² = 0, all the properties above are automatically satisfied!

Teaching derivatives to your computer

Just as a calculus student will do, the rules of differentiation turn a calculus problem into an algebra one. And the good news: computers are better at algebra than you!

So, how can we implement these rules in a practical way on our computer? Implementing a new object (a dual number) with its own interaction rules sounds like a task for object-oriented programming. And, interestingly enough, the process is surprisingly similar to that of teaching a human student. With the difference that our “digital student” will never forget a rule, apply it the wrong way, or forget a minus sign!

So, how do these rules look, for instance, in Julia? (For a Python implementation, take a look here). First of all, we need to define a Dual object, representing a dual number. In principle, it is as simple as a container for two real numbers:

""" Structure representing a Dual number """
struct Dual
x::Real
dx::Real
end

Later, it will come in handy to add a couple of constructors.

""" Structure representing a Dual number """
struct Dual
x::Real
dx::Real

""" Default constructor """
function Dual(x::Real, dx::Real=0)::Dual
new(x, dx)
end

""" If passed a Dual, just return it
This will be handy later """
function Dual(x::Dual)::Dual
return x
end
end

Don’t worry too much if you don’t understand the lines above. They have been added only making the Dual object easier to use (for instance, Dual(1) would have failed without the first constructor, and so would have done the application of Dual to a number that is already a Dual).

Another trick that will prove handy soon is to create a type alias for anything that is either a Number (one of Julia's base types) or a Dual.

const DualNumber = Union{Dual, Number}

And now comes the fun part. We’ll teach our new object how to do mathematics! For instance, as we saw earlier, the rule for adding dual numbers is to add both their components, just as in a 2D vector:

import Base: +
function +(self::DualNumber, other::DualNumber)::Dual
self, other = Dual(self), Dual(other) # Coerce into Dual
return Dual(self.x + other.x, self.dx + other.dx)
end

We have to teach even more basic stuff. Remember a computer is dramatically devoid of common sense, so, for instance, we have to define the meaning of a plus sign in front of a Dual.

+(z::Dual) = z

This sounds as idiotic as explaining that +3 is equal to 3, but the computer needs to know! Another possibility is using inheritance, but this is an advanced topic beyond the scope of this piece.

Defining minus a Dual will also be needed:

import Base: -
-(z::Dual) = Dual(-z.x, -z.dx)

and actually, it allows us to define the subtraction of two dual numbers as a sum:

function -(self::DualNumber, other::DualNumber)::Dual
self, other = Dual(self), Dual(other) # Coerce into Dual
return self + (-other) # A subtraction disguised as a sum!
end

Some basic operations may be slightly trickier than expected. For instance, when is a dual number smaller than another dual number? Notice that in this case, it only makes sense to compare the first elements, and ignore the derivatives:

import Base: <
<(self::Dual, other::Dual) = self.x < other.x

As we saw before, more interesting stuff happens with multiplication and division:

import Base: *, /

function *(self::DualNumber, other::DualNumber)::Dual
self, other = Dual(self), Dual(other) # Coerce into Dual
y = self.x * other.x
dy = self.dx * other.x + self.x * other.dx # Rule of product for derivatives
return Dual(y, dy)
end

function /(self::DualNumber, other::DualNumber)::Dual
self, other = Dual(self), Dual(other) # Coerce into Dual
y = self.x / other.x
dy = (self.dx * other.x - self.x * other.dx) / (other.x)^2 # Rule of quotient for derivatives
return Dual(y, dy)
end

and with potentiation to a real number:

import Base: ^
function ^(self::Dual, other::Real)::Dual
self, other = Dual(self), Dual(other) # Coerce into Dual
y = self.x^other.x
dy = other.x * self.x^(other.x - 1) * self.dx # Derivative of u(x)^n
return Dual(y, dy)
end

The full list of definitions for algebraic operations is here. For Python, use this link. I recommend taking a look!

After this, each and every time our dual number finds one of the operations defined above in its mysterious journey down a function or a script, it will keep track of its effect on the derivative. It doesn’t matter how long, complicated, or poorly programmed the function is, the second coordinate of our dual number will manage it. Well, as long as the function is differentiable and we don’t hit the machine’s precision… but that would be asking our computer to do magic.

Example

As an example, let’s calculate the derivative of the polynomial:

at x = 3.

For the sake of clarity, we can compute the derivative by hand:

it is apparent that and p(3) = 39 and p’(3) = 34.

Using our Dual object, we can reach the same conclusion automatically:

poly = x -> x^3 + x^2 + x 
z = Dual(3, 1)
poly(z)

> Dual(39, 34)

Even if the same polynomial is defined in a more intricate way, the Dual object can keep track:

""" Equivalent to poly = x -> x^3 + x^2 + x
Just uglier """
function poly(x)
aux = 0 # Initialize auxiliary variable
for n in 1:3 # Add x^1, x^2 and x^3
aux = aux + x^n
end
end

poly(z)

> Dual(39, 34)

What about non-algebraic functions?

The method sketched above will fail miserably as soon as our function contains a non-algebraic element, such as a sine or an exponential. But don’t panic, we can just go to our calculus book and teach our computer some more basic derivatives. For instance, our table of derivatives tells us that the derivative of a sine is a cosine. In the language of dual numbers, this reads:

Confused about the u’? Once again, this is just the chain rule.

The rule of thumb here is, and actually was since the very beginning:

We can create a _factory function that abstracts this structure for us:

function _factory(f::Function, df::Function)::Function
return z -> Dual(f(z.x), df(z.x) * z.dx)
end

So now, we only have to open our derivatives table and fill line by line, starting with the derivative of a sine, continuing with that of a cosine, a tangent, etc.

import Base: sin, cos

sin(z::Dual) = _factory(sin, cos)(z)
cos(z::Dual) = _factory(cos, x -> -sin(x))(z) # An explicit lambda function is often required

If we know our maths, we don’t even need to fill all the derivatives manually from the table. For instance, the tangent is defined as:

and we already have automatically differentiable sine, cosine, and division in our arsenal. So this line will do the trick:

import Base: tan

tan(z::Dual) = sin(z) / cos(z) # We can rely on previously defined functions!

Of course, hard-coding the tangent’s derivative is also possible, and probably good for code performance and numerical stability. But hey, it’s quite cool that this is even possible!

See a more complete derivatives table here (Python version here).

Example

Let’s compute the derivative of the non-algebraic function

It is easy to prove analytically that the derivative is 1 everywhere (notice that the argument of the tangent is actually constant). Now, using Dual:

fun = x -> x + tan(cos(x)^2 + sin(x)^2)

z = Dual(0, 1)
fun(z)

> Dual(1.557407724654902, 1.0)

Making it more user-friendly

We can use dual numbers to create a user-friendly derivative function:

"""
derivative(f)

Seamlessly turns a given function f
into
the function's derivative
"""
function derivative(f)
df = x -> f(Dual(x, 1.0)).dx
return df
end

Using this, our example above will look like:

fun = x -> x + tan(cos(x)^2 + sin(x)^2)

dfun = derivative(f)
dfun(0)

> 1.0

Another example

Now we want to calculate and visualize the derivatives of:

First, we have to input the function, and the derivative gets calculated automatically:

f(x) = x^2 - 5x + 6 - 5x^3 - 5 * exp(-50 * x^2)

df = derivative(f)

We can visualize the results by plotting a tangent line:

using Plots

I = [-0.7; 0.7]
δ = 0.025
@gif for a = [I[1]:δ:I[2]; I[2]-δ:-δ:I[1]+δ]
L(x) = f(a) + df(a) * (x - a)
plot(f, -1, 1, leg=false)
scatter!([a], [f(a)], m=(:red, 2))
plot!(L, -1, 1, c=:red)
ylims!(-5, 15)
end

Is this useful?

Automatic differentiation is particularly useful in the field of Machine Learning, where multidimensional derivatives (better known as gradients) have to be performed as fast and exactly as possible. Said this, automatic differentiation for Machine Learning is usually implemented in a different way, the so-called backward or reverse mode, for efficiency reasons.

A well-established library for automatic differentiation is JAX (for Python). Machine learning frameworks such as Tensorflow and Pytorch also implement automatic differentiation. For Julia, multiple libraries seem to be competing, but Enzyme.jl seems to be ahead. Forwarddiff.jl is also worth taking a look at.

Acknowledgments

I want to say thanks to my colleague and friend Abel Siqueira, for kindly introducing me to Julia and reviewing this post, and to Aron Jansen, for his kind and useful suggestions. A more in-depth introduction can be found in this episode of Chris Rackauckas’ book on scientific machine learning.

The TeX Math Here browser add-in also played an important role: it allowed me to transfer my Latex equations from Markdown to Medium in an (almost) painless way.


Automatic differentiation from scratch was originally published in Netherlands eScience Center on Medium, where people are continuing the conversation by highlighting and responding to this story.

CUDA.jl 5.1: Unified memory and cooperative groups

By: Tim Besard

Re-posted from: https://juliagpu.org/post/2023-11-07-cuda_5.1/index.html

CUDA.jl 5.1 greatly improves the support of two important parts of the CUDA toolkit: unified memory, for accessing GPU memory on the CPU and vice-versa, and cooperative groups which offer a more modular approach to kernel programming.

This post is located at https://info.juliahub.com/cuda-jl-5-1-unified-memory

Basic Data Structures Explained

By: Steven Whitaker

Re-posted from: https://glcs.hashnode.dev/basic-data-structures

Julia is a relatively new,free, and open-source programming language.It has a syntaxsimilar to that of other popular programming languagessuch as MATLAB and Python,but it boasts being able to achieve C-like speeds.

Julia provides several useful data structuresfor storing and manipulating data.Some of these data structures,like arrays and dictionaries,are ubiquitous in Julia codebecause of their usefulnessand wide applicability.Others, like sets,have more limited usesbut neverthelessstill are useful data structures.

In this post,we will learn aboutarrays, dictionaries, and setsin Julia.We will discuss how to construct themand describe various functionsfor working with and manipulating them.

This post assumes you already have Julia installed.If you haven’t yet,check out our earlierpost on how to install Julia.

Arrays

One of the most basic and ubiquitous data structuresis the array.Arrays are used for storing values,iterating through values,and even representing mathematical vectors and matrices.

The basic array type in Julia is Array{T,N},where T is the type of the elements in the array(or an abstract supertype of the elementsif not all elements are of the same type),and N is the number of array dimensions.For example,a list of strings would be of type Array{String,1},while a matrix of numbers would be of type Array{Float64,2}.

Constructing Arrays

There are various waysto construct arrays.One common wayis to construct an arraydirectly from the valuesit will contain:

julia> ["some", "strings"]2-element Vector{String}: "some" "strings"julia> [1 2; 3 4]2x2 Matrix{Int64}: 1  2 3  4

(Note that Vector{T} is equivalent to Array{T,1}and that Matrix{T} is equivalent to Array{T,2}.)

Example arrays

Another common wayto construct arraysis using array comprehensions.An array comprehensioncreates an arrayby looping through a collection of valuesand computing an array elementfor each value.For example,the following creates an arraycontaining the squaresof the first five natural numbers:

julia> [x^2 for x = 1:5]5-element Vector{Int64}:  1  4  9 16 25

Multidimensional comprehensions also exist:

julia> [(x - 2)^2 + (y - 3)^2 <= 1 for x = 1:3, y = 1:5]3x5 Matrix{Bool}: 0  0  1  0  0 0  1  1  1  0 0  0  1  0  0

We can also create uninitialized arrays,either by passing undef to the array constructoror by calling similar:

julia> Array{Int,1}(undef, 1)1-element Vector{Int64}: 6303840julia> similar([1.0, 2.0])2-element Vector{Float64}: 6.94291544947797e-310 6.94291610129443e-310

(Note that the seemingly random numbers abovecome from whatever bits happen to be setin the memory allocated for the arrays.)

To create an array of zeros,call zeros:

julia> zeros(2, 3)2x3 Matrix{Float64}: 0.0  0.0  0.0 0.0  0.0  0.0

Inspecting Arrays

Information about an arraycan be obtained using various functions.

length gives the number of elementsin an array:

julia> length([1, 2, 3])3julia> length(zeros(2, 3))6

size(x) gives the size of x,while size(x, d) gives the sizeof the dth dimension:

julia> size([1, 2, 3])(3,)julia> size(zeros(2, 3))(2, 3)julia> size(zeros(2, 3), 2)3

ndims gives the number of dimensionsof an array:

julia> ndims(zeros(1, 2, 3, 4, 5, 6))6

And eltype gives the typeof the elements of an array:

julia> eltype(["two", "strings"])Stringjulia> eltype([2, "different types"])Any

Array Operations

Accessing array elementsis achieved using brackets:

julia> a = [10, 20, 30];julia> a[2]20

(Note that arrays use one-based indexingin Julia.)

A similar syntax is usedto modify the contents of an array:

julia> a[1] = 00julia> a3-element Vector{Int64}:  0 20 30

Use commas (,) to separate indexesfor different dimensions,and use a colon (:)to select all the valuesalong a dimension:

julia> m = [1 2; 3 4]2x2 Matrix{Int64}: 1  2 3  4julia> m[1,2]2julia> m[:,1]2-element Vector{Int64}: 1 3

Multiple indexes can be provided:

julia> a[[1, 3]]2-element Vector{Int64}:  0 30

To assign a single valueto multiple array locations,use broadcasting:

julia> a[2:3] .= 02-element view(::Vector{Int64}, 2:3) with eltype Int64: 0 0julia> a3-element Vector{Int64}: 0 0 0

Arrays are also iterable,meaning we can loop throughthe values of an array:

julia> words = ["this", "is", "a", "sentence"];julia> for w in words           println(w)       endthisisasentence

Arrays as Stacks/Queues/Dequeues

Julia also provides some functionsthat allow arrays to be usedin a similar wayas stacks, queues, and dequeues.For example,push!(array, x) inserts xat the end of an array,and pop!(array) removes the last elementof an array.Similarly,pushfirst! and popfirstact on the beginning of an array.

Ranges

Ranges are another useful type of array,often used for loopingand array indexing.

The simplest syntaxfor creating a rangeis a:b,which creates a rangethat starts at aand includes all valuesa + 1, a + 2, etc.,as long as a + n <= b.For example,1:5 contains 1, 2, 3, 4, and 5,while 1.0:2.5 contains 1.0 and 2.0.

A step size, s, can also be specified,as in a:s:b.In this case, the spacing between valuesin the rangeis s instead of 1.

To create a range of N pointsbetween a and b, inclusive,use range(a, b, N).

Unlike Arrays,ranges are immutable,meaning their elementscan’t be modified.If modifying an element of a rangeis necessary,it must first be convertedinto an Arrayby calling collect:

julia> r = 1:21:2julia> r[1] = 10ERROR: CanonicalIndexError: setindex! not defined for UnitRange{Int64}julia> r_arr = collect(r)2-element Vector{Int64}: 1 2julia> r_arr[1] = 10; r_arr2-element Vector{Int64}: 10  2

That concludes our discussion of arrays,so now let’s move on to dictionaries.

Dictionaries

Another very common data structureis the dictionary.A dictionary is a mappingfrom keys to values:give a dictionary a key,and it will returnthe value associated with that key(if present).

In Julia,dictionaries are of type Dict{K,V},where K is the type of the keys,and V is the type of the values.

Dictionaries are constructedby providing key-value pairs:

julia> d = Dict("key1" => 1, "key2" => 2, "key3" => 3)Dict{String, Int64} with 3 entries:  "key2" => 2  "key3" => 3  "key1" => 1

(Note that a => bcreates a Pair in Julia.)

Indexing a dictionaryuses the same syntaxas indexing an array,just using keysinstead of array indexes:

julia> d["key2"]2

Accessing a dictionary

Use haskey to checkfor the presence of a key:

julia> haskey(d, "key3")truejulia> haskey(d, "nope")false

Dictionaries can also be updated:

julia> d["key1"] = 99999999julia> d["newkey"] = -9999-9999julia> dDict{String, Int64} with 4 entries:  "key2"   => 2  "key3"   => 3  "key1"   => 9999  "newkey" => -9999

Use delete!(dict, key)to delete the mappingfor the given key,if present.

We can also iteratethrough the keys and/or valuesof a dictionary:

  • Iterating keys: for k in keys(dict)
  • Iterating values: for v in values(dict)
  • Iterating both: for (k, v) in dict

That wraps up our discussion of dictionaries,so now we will move on to sets.

Sets

A set is a collection of unique elements.In Julia,sets are of type Set{T},where T is the typeof the elements of the set.Sets are usefulfor their efficient set operations,such as membership testing,union, and intersect.

Create an empty set of Float64 values as follows:

julia> s = Set{Float64}()Set{Float64}()

Use push! to add valuesto the set,noticing that the set changesonly if the value does not already existin the set:

julia> push!(s, 1.0);julia> push!(s, 1.2);julia> push!(s, 3.14)Set{Float64} with 3 elements:  1.2  3.14  1.0julia> push!(s, 1.0)Set{Float64} with 3 elements:  1.2  3.14  1.0

Use union to take the unionof two sets:

julia> t = Set([1.0, 2.0])Set{Float64} with 2 elements:  2.0  1.0julia> r = s  t # type \cup<tab> to get the union symbolSet{Float64} with 4 elements:  1.2  2.0  3.14  1.0

(Note that s t == union(s, t).)

Use intersect to take the intersectionof two sets:

julia> r  t # type \cap<tab> to get the intersection symbolSet{Float64} with 2 elements:  2.0  1.0

(Note that s t == intersect(s, t).)

Finally,we can check if an elementbelongs to a setwith in:

julia> 1.0  r # type \in<tab> to get the "is an element of" symboltrue

(Note that and in are interchangeable here.)

And with that,we conclude our overviewof some important Julia data structures.

Summary

In this post,we learned about a few data structuresthat Julia provides:arrays, dictionaries, and sets.We learned how to construct themand how to work with and manipulate them.

What are the most useful data structuresyou have used?Let us know in the comments below!

Have a better understandingof Julia’s basic data structures?Move on to thenext post to learn about multiple dispatch,one of Julia’s most distinctive features!Or,feel free to take a lookat our other Julia tutorial posts!

Additional Links