Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update GNNChain #202

Merged
merged 7 commits into from
Jul 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "GraphNeuralNetworks"
uuid = "cffab07f-9bc2-4db1-8861-388f63bf7694"
authors = ["Carlo Lucibello and contributors"]
version = "0.4.4"
version = "0.4.5"

[deps]
Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e"
Expand Down Expand Up @@ -29,7 +29,7 @@ Adapt = "3"
CUDA = "3.3"
ChainRulesCore = "1"
DataStructures = "0.18"
Flux = "0.13"
Flux = "0.13.4"
Functors = "0.2, 0.3"
Graphs = "1.4"
KrylovKit = "0.5"
Expand Down
2 changes: 1 addition & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ model = GNNChain(GCNConv(16 => 64),
Dense(64, 1)) |> device

ps = Flux.params(model)
opt = ADAM(1f-4)
opt = Adam(1f-4)
```

### Training
Expand Down
4 changes: 2 additions & 2 deletions docs/src/tutorials/gnn_intro_pluto.jl
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ Since everything in our model is differentiable and parameterized, we can add so
Here, we make use of a semi-supervised or transductive learning procedure: We simply train against one node per class, but are allowed to make use of the complete input graph data.

Training our model is very similar to any other Flux model.
In addition to defining our network architecture, we define a loss criterion (here, `logitcrossentropy` and initialize a stochastic gradient optimizer (here, `ADAM`).
In addition to defining our network architecture, we define a loss criterion (here, `logitcrossentropy` and initialize a stochastic gradient optimizer (here, `Adam`).
After that, we perform multiple rounds of optimization, where each round consists of a forward and backward pass to compute the gradients of our model parameters w.r.t. to the loss derived from the forward pass.
If you are not new to Flux, this scheme should appear familar to you.

Expand All @@ -285,7 +285,7 @@ Let us now start training and see how our node embeddings evolve over time (best
begin
model = GCN(num_features, num_classes)
ps = Flux.params(model)
opt = ADAM(1e-2)
opt = Adam(1e-2)
epochs = 2000

emb = h
Expand Down
2 changes: 1 addition & 1 deletion docs/src/tutorials/graph_classification_pluto.jl
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ function train!(model; epochs=200, η=1e-2, infotime=10)
device = Flux.cpu
model = model |> device
ps = Flux.params(model)
opt = ADAM(1e-3)
opt = Adam(1e-3)


function report(epoch)
Expand Down
2 changes: 1 addition & 1 deletion examples/graph_classification_tudataset.jl
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ function train(; kws...)
Dense(nhidden, 1)) |> device

ps = Flux.params(model)
opt = ADAM(args.η)
opt = Adam(args.η)

# LOGGING FUNCTION

Expand Down
2 changes: 1 addition & 1 deletion examples/link_prediction_pubmed.jl
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function train(; kws...)
pred = DotPredictor()

ps = Flux.params(model)
opt = ADAM(args.η)
opt = Adam(args.η)

### LOSS FUNCTION ############

Expand Down
2 changes: 1 addition & 1 deletion examples/neural_ode_cora.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ model = GNNChain(GCNConv(nin => nhidden, relu),
ps = Flux.params(model);

# ## Optimizer
opt = ADAM(0.01)
opt = Adam(0.01)


function eval_loss_accuracy(X, y, mask)
Expand Down
2 changes: 1 addition & 1 deletion examples/node_classification_cora.jl
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function train(; kws...)
Dense(nhidden, nout)) |> device

ps = Flux.params(model)
opt = ADAM(args.η)
opt = Adam(args.η)

display(g)

Expand Down
2 changes: 1 addition & 1 deletion perf/neural_ode_mnist.jl
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ model = Chain(Flux.flatten,
ps = Flux.params(model);

# ## Optimizer
opt = ADAM(0.01)
opt = Adam(0.01)

function eval_loss_accuracy(X, y)
ŷ = model(X)
Expand Down
2 changes: 1 addition & 1 deletion perf/node_classification_cora_geometricflux.jl
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function train(; kws...)
Dense(nhidden, nout)) |> device

ps = Flux.params(model)
opt = ADAM(args.η)
opt = Adam(args.η)

@info g

Expand Down
134 changes: 79 additions & 55 deletions src/layers/basic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,6 @@ WithGraph(model, g::GNNGraph; traingraph=false) = WithGraph(model, g, traingraph
@functor WithGraph
Flux.trainable(l::WithGraph) = l.traingraph ? (; l.model, l.g) : (; l.model,)

# Work around
# https://github.com/FluxML/Flux.jl/issues/1733
# Revisit after
# https://github.com/FluxML/Flux.jl/pull/1742
function Flux.destructure(m::WithGraph)
@assert m.traingraph == false # TODO
p, re = Flux.destructure(m.model)
function re_withgraph(x)
WithGraph(re(x), m.g, m.traingraph)
end

return p, re_withgraph
end

(l::WithGraph)(g::GNNGraph, x...; kws...) = l.model(g, x...; kws...)
(l::WithGraph)(x...; kws...) = l.model(l.g, x...; kws...)

Expand All @@ -85,74 +71,112 @@ and if names are given, `m[:name] == m[1]` etc.
# Examples

```juliarepl
julia> m = GNNChain(GCNConv(2=>5), BatchNorm(5), x -> relu.(x), Dense(5, 4));
julia> using Flux, GraphNeuralNetworks

julia> m = GNNChain(GCNConv(2=>5),
BatchNorm(5),
x -> relu.(x),
Dense(5, 4))
GNNChain(GCNConv(2 => 5), BatchNorm(5), #7, Dense(5 => 4))

julia> x = randn(Float32, 2, 3);

julia> g = GNNGraph([1,1,2,3], [2,3,1,1]);
julia> g = rand_graph(3, 6)
GNNGraph:
num_nodes = 3
num_edges = 6

julia> m(g, x)
4×3 Matrix{Float32}:
0.157941 0.15443 0.193471
0.0819516 0.0503105 0.122523
0.225933 0.267901 0.241878
-0.0134364 -0.0120716 -0.0172505
-0.795592 -0.795592 -0.795592
-0.736409 -0.736409 -0.736409
0.994925 0.994925 0.994925
0.857549 0.857549 0.857549

julia> m2 = GNNChain(enc = m,
dec = DotDecoder())
GNNChain(enc = GNNChain(GCNConv(2 => 5), BatchNorm(5), #7, Dense(5 => 4)), dec = DotDecoder())

julia> m2(g, x)
1×6 Matrix{Float32}:
2.90053 2.90053 2.90053 2.90053 2.90053 2.90053

julia> m2[:enc](g, x) == m(g, x)
true
```
"""
struct GNNChain{T} <: GNNLayer
struct GNNChain{T<:Union{Tuple, NamedTuple, AbstractVector}} <: GNNLayer
layers::T

GNNChain(xs...) = new{typeof(xs)}(xs)

function GNNChain(; kw...)
:layers in Base.keys(kw) && throw(ArgumentError("a GNNChain cannot have a named layer called `layers`"))
isempty(kw) && return new{Tuple{}}(())
new{typeof(values(kw))}(values(kw))
end
end

@forward GNNChain.layers Base.getindex, Base.length, Base.first, Base.last,
Base.iterate, Base.lastindex, Base.keys
@functor GNNChain

Flux.functor(::Type{<:GNNChain}, c) = c.layers, ls -> GNNChain(ls...)
Flux.functor(::Type{<:GNNChain}, c::Tuple) = c, ls -> GNNChain(ls...)
GNNChain(xs...) = GNNChain(xs)

# input from graph
applylayer(l, g::GNNGraph) = GNNGraph(g, ndata=l(node_features(g)))
applylayer(l::GNNLayer, g::GNNGraph) = l(g)
function GNNChain(; kw...)
:layers in Base.keys(kw) && throw(ArgumentError("a GNNChain cannot have a named layer called `layers`"))
isempty(kw) && return GNNChain(())
GNNChain(values(kw))
end

@forward GNNChain.layers Base.getindex, Base.length, Base.first, Base.last,
Base.iterate, Base.lastindex, Base.keys, Base.firstindex

(c::GNNChain)(g::GNNGraph, x) = _applychain(c.layers, g, x)
(c::GNNChain)(g::GNNGraph) = _applychain(c.layers, g)

## TODO see if this is faster for small chains
## see https://github.com/FluxML/Flux.jl/pull/1809#discussion_r781691180
# @generated function _applychain(layers::Tuple{Vararg{<:Any,N}}, g::GNNGraph, x) where {N}
# symbols = vcat(:x, [gensym() for _ in 1:N])
# calls = [:($(symbols[i+1]) = _applylayer(layers[$i], $(symbols[i]))) for i in 1:N]
# Expr(:block, calls...)
# end
# _applychain(layers::NamedTuple, g, x) = _applychain(Tuple(layers), x)
Comment on lines +128 to +135
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note to myself: remember to benchmark this before merging

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmarked on a small graph / net

        n, deg = 10, 4
        din, d, dout = 10, 3, 4, 2

        g = GNNGraph(random_regular_graph(n, deg), 
                    graph_type=GRAPH_T,
                    ndata= randn(Float32, din, n))
        x = g.ndata.x

        gnn = GNNChain(GCNConv(din => d),
                       BatchNorm(d),
                       x -> tanh.(x),
                       GraphConv(d => d, tanh),
                       Dropout(0.5),
                       Dense(d, dout))

There is a performance increase with the generated _applychain but not large enough for the change to be worthwhile

julia> using BenchmarkTools

### without @generated _applychain

julia> @btime gnn(g, x)
  7.469 μs (84 allocations: 12.48 KiB)
2×10 Matrix{Float32}:
 -0.8186    -0.570312  -0.777638    -0.641642  -0.684857  -0.975505
  0.305567   0.559996   0.631279      0.4687     0.479899   0.321139

julia> @btime gradient(x -> sum(gnn(g, x)), x)
  515.917 μs (2422 allocations: 160.52 KiB)
(Float32[0.3974119 -0.5917164  -0.9200875 1.1957061; -0.54502636 -1.5056851  -2.6915464 2.5114572; -0.97105116 0.7726713  1.0995824 -1.5013595],)

### with @generated _applychain

julia> @btime gnn(g, x)
  6.825 μs (73 allocations: 11.55 KiB)
2×10 Matrix{Float32}:
 -0.8186    -0.570312  -0.777638    -0.641642  -0.684857  -0.975505
  0.305567   0.559996   0.631279      0.4687     0.479899   0.321139

julia> @btime gradient(x -> sum(gnn(g, x)), x)
  454.750 μs (2157 allocations: 161.00 KiB)
(Float32[-0.564121 0.3105453  0.19531891 -0.22819248; -0.6428803 0.13550264  0.9421329 -0.79201597; 0.7816532 -0.4734739  0.23667078 0.033573348],)

In both cases the gradient is very slow, this should be further investigated


function _applychain(layers, g::GNNGraph, x) # type-unstable path, helps compile times
for l in layers
x = _applylayer(l, g, x)
end
return x
end

# explicit input
applylayer(l, g::GNNGraph, x) = l(x)
applylayer(l::GNNLayer, g::GNNGraph, x) = l(g, x)
function _applychain(layers, g::GNNGraph) # type-unstable path, helps compile times
for l in layers
g = _applylayer(l, g)
end
return g
end

# Handle Flux.Parallel
applylayer(l::Parallel, g::GNNGraph) = GNNGraph(g, ndata=applylayer(l, g, node_features(g)))
applylayer(l::Parallel, g::GNNGraph, x::AbstractArray) = mapreduce(f -> applylayer(f, g, x), l.connection, l.layers)
# # explicit input
_applylayer(l, g::GNNGraph, x) = l(x)
_applylayer(l::GNNLayer, g::GNNGraph, x) = l(g, x)

# input from graph
applychain(::Tuple{}, g::GNNGraph) = g
applychain(fs::Tuple, g::GNNGraph) = applychain(tail(fs), applylayer(first(fs), g))

# explicit input
applychain(::Tuple{}, g::GNNGraph, x) = x
applychain(fs::Tuple, g::GNNGraph, x) = applychain(tail(fs), g, applylayer(first(fs), g, x))
_applylayer(l, g::GNNGraph) = GNNGraph(g, ndata=l(node_features(g)))
_applylayer(l::GNNLayer, g::GNNGraph) = l(g)

(c::GNNChain)(g::GNNGraph, x) = applychain(Tuple(c.layers), g, x)
(c::GNNChain)(g::GNNGraph) = applychain(Tuple(c.layers), g)
# # Handle Flux.Parallel
_applylayer(l::Parallel, g::GNNGraph) = GNNGraph(g, ndata=_applylayer(l, g, node_features(g)))

function _applylayer(l::Parallel, g::GNNGraph, x::AbstractArray)
closures = map(f -> (x -> _applylayer(f, g, x)), l.layers)
return Parallel(l.connection, closures)(x)
end

Base.getindex(c::GNNChain, i::AbstractArray) = GNNChain(c.layers[i]...)
Base.getindex(c::GNNChain{<:NamedTuple}, i::AbstractArray) =
GNNChain(; NamedTuple{Base.keys(c)[i]}(Tuple(c.layers)[i])...)
Base.getindex(c::GNNChain, i::AbstractArray) = GNNChain(c.layers[i])
Base.getindex(c::GNNChain{<:NamedTuple}, i::AbstractArray) =
GNNChain(NamedTuple{keys(c)[i]}(Tuple(c.layers)[i]))

function Base.show(io::IO, c::GNNChain)
print(io, "GNNChain(")
_show_layers(io, c.layers)
print(io, ")")
end

_show_layers(io, layers::Tuple) = join(io, layers, ", ")
_show_layers(io, layers::NamedTuple) = join(io, ["$k = $v" for (k, v) in pairs(layers)], ", ")

_show_layers(io, layers::AbstractVector) = (print(io, "["); join(io, layers, ", "); print(io, "]"))

"""
DotDecoder()
Expand Down Expand Up @@ -181,5 +205,5 @@ struct DotDecoder <: GNNLayer end

function (::DotDecoder)(g, x)
check_num_nodes(g, x)
apply_edges(xi_dot_xj, g, xi=x, xj=x)
return apply_edges(xi_dot_xj, g, xi=x, xj=x)
end
2 changes: 1 addition & 1 deletion test/examples/node_classification_cora.jl
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function train(Layer; verbose=false, kws...)
Dense(nhidden, nout)) |> device

ps = Flux.params(model)
opt = ADAM(args.η)
opt = Adam(args.η)


## TRAINING
Expand Down
27 changes: 25 additions & 2 deletions test/layers/basic.jl
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
@testset "basic" begin
@testset "GNNChain" begin
n, din, d, dout = 10, 3, 4, 2
deg = 4

g = GNNGraph(random_regular_graph(n, 4),
g = GNNGraph(random_regular_graph(n, deg),
graph_type=GRAPH_T,
ndata= randn(Float32, din, n))

x = g.ndata.x

gnn = GNNChain(GCNConv(din => d),
BatchNorm(d),
x -> tanh.(x),
Expand All @@ -17,6 +19,27 @@

test_layer(gnn, g, rtol=1e-5, exclude_grad_fields=[:μ, :σ²])

@testset "constructor with names" begin
m = GNNChain(GCNConv(din=>d),
BatchNorm(d),
x -> relu.(x),
Dense(d, dout))

m2 = GNNChain(enc = m,
dec = DotDecoder())

@test m2[:enc] === m
@test m2(g, x) == m2[:dec](g, m2[:enc](g, x))
end

@testset "constructor with vector" begin
m = GNNChain(GCNConv(din=>d),
BatchNorm(d),
x -> relu.(x),
Dense(d, dout))
m2 = GNNChain([m.layers...])
@test m2(g, x) == m(g, x)
end

@testset "Parallel" begin
AddResidual(l) = Parallel(+, identity, l)
Expand Down