From 3a0408bf927a0ff29d72948af530844bb453d797 Mon Sep 17 00:00:00 2001 From: Aurora Rossi Date: Fri, 24 Jul 2026 13:22:41 +0200 Subject: [PATCH 1/5] Learnable epsilon --- GNNlib/src/layers/conv.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/GNNlib/src/layers/conv.jl b/GNNlib/src/layers/conv.jl index 0827b4592..f9d4bcea6 100644 --- a/GNNlib/src/layers/conv.jl +++ b/GNNlib/src/layers/conv.jl @@ -252,7 +252,8 @@ function gin_conv(l, g::AbstractGNNGraph, x) xj, xi = expand_srcdst(g, x) m = propagate(copy_xj, g, l.aggr, xj = xj) - return l.nn((1 .+ ofeltype(xi, l.ϵ)) .* xi .+ m) + eps = convert.(float(eltype(xi)), l.eps) + return l.nn((1 .+ eps) .* xi .+ m) end ####################### NNConv ###################################### From 9782473c8ea0cc901aa6b2be8d997c01c5ba3218 Mon Sep 17 00:00:00 2001 From: Aurora Rossi Date: Fri, 24 Jul 2026 13:23:09 +0200 Subject: [PATCH 2/5] Learnable epsilon and flag train --- GNNLux/src/layers/conv.jl | 36 ++++++++++++++++++++++++++++-------- GNNLux/test/layers/conv.jl | 11 ++++++++++- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/GNNLux/src/layers/conv.jl b/GNNLux/src/layers/conv.jl index 3b70747c8..0d76365c0 100644 --- a/GNNLux/src/layers/conv.jl +++ b/GNNLux/src/layers/conv.jl @@ -1277,7 +1277,7 @@ function Base.show(io::IO, l::GatedGraphConv) end @doc raw""" - GINConv(f, ϵ; aggr=+) + GINConv(f, [eps]; aggr=+, train_eps=false) Graph Isomorphism convolutional layer from paper [How Powerful are Graph Neural Networks?](https://arxiv.org/pdf/1810.00826.pdf). @@ -1290,7 +1290,9 @@ where ``f_\Theta`` typically denotes a learnable function, e.g. a linear layer o # Arguments - `f`: A (possibly learnable) function acting on node features. -- `ϵ`: Weighting factor. +- `eps`: Initial value of the weighting factor ``\epsilon``. Default `0.0f0`. +- `train_eps`: If `true`, ``\epsilon`` is trainable. Default `false`. +- `aggr`: Neighborhood aggregation function. Default `+`. # Examples: @@ -1312,7 +1314,7 @@ x = randn(rng, Float32, in_channel, g.num_nodes) nn = Dense(in_channel, out_channel) # create layer -l = GINConv(nn, 0.01f0, aggr = mean) +l = GINConv(nn; eps = 0.01f0, train_eps = true, aggr = mean) # setup layer ps, st = LuxCore.setup(rng, l) @@ -1323,15 +1325,31 @@ y, st = l(g, x, ps, st) # size: out_channel × num_nodes """ @concrete struct GINConv <: GNNContainerLayer{(:nn,)} nn <: AbstractLuxLayer - ϵ <: Real + eps aggr + train_eps::Bool end -GINConv(nn, ϵ; aggr = +) = GINConv(nn, ϵ, aggr) +GINConv(nn, eps::Real; aggr = +, train_eps::Bool = false) = + GINConv(nn, train_eps ? [eps] : eps, aggr, train_eps) + +GINConv(nn, eps::Real, aggr) = GINConv(nn, eps; aggr) + +GINConv(nn; eps::Real = 0.0f0, aggr = +, train_eps::Bool = false) = + GINConv(nn, eps; aggr, train_eps) + +function LuxCore.initialparameters(rng::AbstractRNG, l::GINConv) + nn = LuxCore.initialparameters(rng, l.nn) + return l.train_eps ? (; nn, eps = l.eps) : (; nn) +end + +LuxCore.parameterlength(l::GINConv) = parameterlength(l.nn) + l.train_eps +LuxCore.statelength(l::GINConv) = statelength(l.nn) function (l::GINConv)(g, x, ps, st) nn = StatefulLuxLayer{true}(l.nn, ps.nn, st.nn) - m = (; nn, l.ϵ, l.aggr) + eps = l.train_eps ? ps.eps : l.eps + m = (; nn, eps, l.aggr) y = GNNlib.gin_conv(m, g, x) stnew = (; nn = _getstate(nn)) return y, stnew @@ -1339,7 +1357,9 @@ end function Base.show(io::IO, l::GINConv) print(io, "GINConv($(l.nn)") - print(io, ", $(l.ϵ)") + print(io, ", eps=$(l.eps)") + l.train_eps && print(io, ", train_eps=true") + l.aggr == (+) || print(io, ", aggr=$(l.aggr)") print(io, ")") end @@ -1869,4 +1889,4 @@ function (l::SAGEConv)(g, x, ps, st) m = (; ps.weight, bias = _getbias(ps), l.σ, l.aggr) return GNNlib.sage_conv(m, g, x), st -end \ No newline at end of file +end diff --git a/GNNLux/test/layers/conv.jl b/GNNLux/test/layers/conv.jl index e97206c45..d96a6aae6 100644 --- a/GNNLux/test/layers/conv.jl +++ b/GNNLux/test/layers/conv.jl @@ -93,8 +93,17 @@ @testset "GINConv" begin nn = Chain(Dense(in_dims => out_dims, tanh), Dense(out_dims => out_dims)) - l = GINConv(nn, 0.5) + l = GINConv(nn; eps = 0.5) test_lux_layer(rng, l, g, x, sizey=(out_dims,g.num_nodes), container=true) + ps = LuxCore.initialparameters(rng, l) + @test !hasproperty(ps, :eps) + + l = GINConv(nn; train_eps = true) + test_lux_layer(rng, l, g, x, sizey=(out_dims,g.num_nodes), container=true) + ps = LuxCore.initialparameters(rng, l) + @test ps.eps == [0.0f0] + + @test GINConv(nn, 0.5).eps == 0.5 end @testset "MEGNetConv" begin From fd49d7c2f52b3624c3c40f44095a9aa4e993dadd Mon Sep 17 00:00:00 2001 From: Aurora Rossi Date: Fri, 24 Jul 2026 13:23:28 +0200 Subject: [PATCH 3/5] Learnable epsilon and flag --- GraphNeuralNetworks/src/layers/conv.jl | 27 +++++++++++++++++-------- GraphNeuralNetworks/test/layers/conv.jl | 18 ++++++++++++++--- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/GraphNeuralNetworks/src/layers/conv.jl b/GraphNeuralNetworks/src/layers/conv.jl index e3cf30fea..d56dd0e69 100644 --- a/GraphNeuralNetworks/src/layers/conv.jl +++ b/GraphNeuralNetworks/src/layers/conv.jl @@ -590,7 +590,7 @@ function Base.show(io::IO, l::EdgeConv) end @doc raw""" - GINConv(f, ϵ; aggr=+) + GINConv(f, [eps]; aggr=+, train_eps=false) Graph Isomorphism convolutional layer from paper [How Powerful are Graph Neural Networks?](https://arxiv.org/pdf/1810.00826.pdf). @@ -603,7 +603,9 @@ where ``f_\Theta`` typically denotes a learnable function, e.g. a linear layer o # Arguments - `f`: A (possibly learnable) function acting on node features. -- `ϵ`: Weighting factor. +- `eps`: Initial value of the weighting factor ``\epsilon``. Default `0.0f0`. +- `train_eps`: If `true`, ``\epsilon`` is trainable. Default `false`. +- `aggr`: Neighborhood aggregation function. Default `+`. # Examples: @@ -619,28 +621,37 @@ g = GNNGraph(s, t) nn = Dense(in_channel, out_channel) # create layer -l = GINConv(nn, 0.01f0, aggr = mean) +l = GINConv(nn; eps = 0.01f0, train_eps = true, aggr = mean) # forward pass y = l(g, x) ``` """ -struct GINConv{R <: Real, NN, A} <: GNNLayer +struct GINConv{E, NN, A} <: GNNLayer nn::NN - ϵ::R + eps::E aggr::A + train_eps::Bool end Flux.@layer :expand GINConv -Flux.trainable(l::GINConv) = (nn = l.nn,) +Flux.trainable(l::GINConv) = l.train_eps ? (; l.nn, l.eps) : (; l.nn) -GINConv(nn, ϵ; aggr = +) = GINConv(nn, ϵ, aggr) +GINConv(nn, eps::Real; aggr = +, train_eps::Bool = false) = + GINConv(nn, train_eps ? [eps] : eps, aggr, train_eps) + +GINConv(nn, eps::Real, aggr) = GINConv(nn, eps; aggr) + +GINConv(nn; eps::Real = 0.0f0, aggr = +, train_eps::Bool = false) = + GINConv(nn, eps; aggr, train_eps) (l::GINConv)(g, x) = GNNlib.gin_conv(l, g, x) function Base.show(io::IO, l::GINConv) print(io, "GINConv($(l.nn)") - print(io, ", $(l.ϵ)") + print(io, ", eps=$(l.eps)") + l.train_eps && print(io, ", train_eps=true") + l.aggr == (+) || print(io, ", aggr=$(l.aggr)") print(io, ")") end diff --git a/GraphNeuralNetworks/test/layers/conv.jl b/GraphNeuralNetworks/test/layers/conv.jl index be7c4f1d5..f9c906c89 100644 --- a/GraphNeuralNetworks/test/layers/conv.jl +++ b/GraphNeuralNetworks/test/layers/conv.jl @@ -278,19 +278,31 @@ end using .TestModule nn = Dense(D_IN, D_OUT) - l = GINConv(nn, 0.01, aggr = mean) + l = GINConv(nn; eps = 0.01, aggr = mean) for g in TEST_GRAPHS @test size(l(g, g.x)) == (D_OUT, g.num_nodes) test_gradients(l, g, g.x, rtol = RTOL_HIGH) end + @test keys(Flux.trainable(l)) == (:nn,) - @test !in(:eps, Flux.trainable(l)) + l = GINConv(Dense(D_IN, D_OUT); train_eps = true, aggr = mean) + @test keys(Flux.trainable(l)) == (:nn, :eps) + @test l.eps == [0.0f0] + for g in TEST_GRAPHS + @test size(l(g, g.x)) == (D_OUT, g.num_nodes) + end + g = first(TEST_GRAPHS) + grad = Flux.gradient(l -> mean(l(g, g.x)), l)[1] + @test grad.eps isa AbstractVector + @test length(grad.eps) == 1 + + @test GINConv(nn, 0.5).eps == 0.5 end @testitem "GINConv GPU" setup=[TolSnippet, TestModule] tags=[:gpu] begin using .TestModule nn = Dense(D_IN, D_OUT) - l = GINConv(nn, 0.01, aggr = mean) + l = GINConv(nn; train_eps = true, aggr = mean) for g in TEST_GRAPHS g.graph isa AbstractSparseMatrix && continue @test size(l(g, g.x)) == (D_OUT, g.num_nodes) From 82f23cf164b74d26aa9969bc5c03c753605c3a7b Mon Sep 17 00:00:00 2001 From: Aurora Rossi Date: Fri, 24 Jul 2026 14:01:15 +0200 Subject: [PATCH 4/5] Change eps to unicode char --- GNNLux/src/layers/conv.jl | 26 ++++++++++++------------- GNNLux/test/layers/conv.jl | 8 ++++---- GNNlib/src/layers/conv.jl | 4 ++-- GraphNeuralNetworks/src/layers/conv.jl | 22 ++++++++++----------- GraphNeuralNetworks/test/layers/conv.jl | 12 ++++++------ 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/GNNLux/src/layers/conv.jl b/GNNLux/src/layers/conv.jl index 199494926..73a78f223 100644 --- a/GNNLux/src/layers/conv.jl +++ b/GNNLux/src/layers/conv.jl @@ -1287,7 +1287,7 @@ function Base.show(io::IO, l::GatedGraphConv) end @doc raw""" - GINConv(f, [eps]; aggr=+, train_eps=false) + GINConv(f, [ϵ]; aggr=+, train_eps=false) Graph Isomorphism convolutional layer from paper [How Powerful are Graph Neural Networks?](https://arxiv.org/pdf/1810.00826.pdf). @@ -1300,7 +1300,7 @@ where ``f_\Theta`` typically denotes a learnable function, e.g. a linear layer o # Arguments - `f`: A (possibly learnable) function acting on node features. -- `eps`: Initial value of the weighting factor ``\epsilon``. Default `0.0f0`. +- `ϵ`: Initial value of the weighting factor ``\epsilon``. Default `0.0f0`. - `train_eps`: If `true`, ``\epsilon`` is trainable. Default `false`. - `aggr`: Neighborhood aggregation function. Default `+`. @@ -1324,7 +1324,7 @@ x = randn(rng, Float32, in_channel, g.num_nodes) nn = Dense(in_channel, out_channel) # create layer -l = GINConv(nn; eps = 0.01f0, train_eps = true, aggr = mean) +l = GINConv(nn; ϵ = 0.01f0, train_eps = true, aggr = mean) # setup layer ps, st = LuxCore.setup(rng, l) @@ -1335,22 +1335,22 @@ y, st = l(g, x, ps, st) # size: out_channel × num_nodes """ @concrete struct GINConv <: GNNContainerLayer{(:nn,)} nn <: AbstractLuxLayer - eps + ϵ aggr train_eps::Bool end -GINConv(nn, eps::Real; aggr = +, train_eps::Bool = false) = - GINConv(nn, train_eps ? [eps] : eps, aggr, train_eps) +GINConv(nn, ϵ::Real; aggr = +, train_eps::Bool = false) = + GINConv(nn, train_eps ? [ϵ] : ϵ, aggr, train_eps) -GINConv(nn, eps::Real, aggr) = GINConv(nn, eps; aggr) +GINConv(nn, ϵ::Real, aggr) = GINConv(nn, ϵ; aggr) -GINConv(nn; eps::Real = 0.0f0, aggr = +, train_eps::Bool = false) = - GINConv(nn, eps; aggr, train_eps) +GINConv(nn; ϵ::Real = 0.0f0, aggr = +, train_eps::Bool = false) = + GINConv(nn, ϵ; aggr, train_eps) function LuxCore.initialparameters(rng::AbstractRNG, l::GINConv) nn = LuxCore.initialparameters(rng, l.nn) - return l.train_eps ? (; nn, eps = l.eps) : (; nn) + return l.train_eps ? (; nn, ϵ = l.ϵ) : (; nn) end LuxCore.parameterlength(l::GINConv) = parameterlength(l.nn) + l.train_eps @@ -1358,8 +1358,8 @@ LuxCore.statelength(l::GINConv) = statelength(l.nn) function (l::GINConv)(g, x, ps, st) nn = StatefulLuxLayer{true}(l.nn, ps.nn, st.nn) - eps = l.train_eps ? ps.eps : l.eps - m = (; nn, eps, l.aggr) + ϵ = l.train_eps ? ps.ϵ : l.ϵ + m = (; nn, ϵ, l.aggr) y = GNNlib.gin_conv(m, g, x) stnew = (; nn = _getstate(nn)) return y, stnew @@ -1367,7 +1367,7 @@ end function Base.show(io::IO, l::GINConv) print(io, "GINConv($(l.nn)") - print(io, ", eps=$(l.eps)") + print(io, ", ϵ=$(l.ϵ)") l.train_eps && print(io, ", train_eps=true") l.aggr == (+) || print(io, ", aggr=$(l.aggr)") print(io, ")") diff --git a/GNNLux/test/layers/conv.jl b/GNNLux/test/layers/conv.jl index 959a0f79a..d32c2772e 100644 --- a/GNNLux/test/layers/conv.jl +++ b/GNNLux/test/layers/conv.jl @@ -92,17 +92,17 @@ @testset "GINConv" begin nn = Chain(Dense(in_dims => out_dims, tanh), Dense(out_dims => out_dims)) - l = GINConv(nn; eps = 0.5) + l = GINConv(nn; ϵ = 0.5) test_lux_layer(rng, l, g, x, sizey=(out_dims,g.num_nodes), container=true) ps = LuxCore.initialparameters(rng, l) - @test !hasproperty(ps, :eps) + @test !hasproperty(ps, :ϵ) l = GINConv(nn; train_eps = true) test_lux_layer(rng, l, g, x, sizey=(out_dims,g.num_nodes), container=true) ps = LuxCore.initialparameters(rng, l) - @test ps.eps == [0.0f0] + @test ps.ϵ == [0.0f0] - @test GINConv(nn, 0.5).eps == 0.5 + @test GINConv(nn, 0.5).ϵ == 0.5 end @testset "MEGNetConv" begin diff --git a/GNNlib/src/layers/conv.jl b/GNNlib/src/layers/conv.jl index 3b9c5b256..9b34d79be 100644 --- a/GNNlib/src/layers/conv.jl +++ b/GNNlib/src/layers/conv.jl @@ -252,8 +252,8 @@ function gin_conv(l, g::AbstractGNNGraph, x) xj, xi = expand_srcdst(g, x) m = propagate(copy_xj, g, l.aggr, xj = xj) - eps = convert.(float(eltype(xi)), l.eps) - return l.nn((1 .+ eps) .* xi .+ m) + ϵ = convert.(float(eltype(xi)), l.ϵ) + return l.nn((1 .+ ϵ) .* xi .+ m) end ####################### NNConv ###################################### diff --git a/GraphNeuralNetworks/src/layers/conv.jl b/GraphNeuralNetworks/src/layers/conv.jl index f9a5f4cd4..0af8668e9 100644 --- a/GraphNeuralNetworks/src/layers/conv.jl +++ b/GraphNeuralNetworks/src/layers/conv.jl @@ -590,7 +590,7 @@ function Base.show(io::IO, l::EdgeConv) end @doc raw""" - GINConv(f, [eps]; aggr=+, train_eps=false) + GINConv(f, [ϵ]; aggr=+, train_eps=false) Graph Isomorphism convolutional layer from paper [How Powerful are Graph Neural Networks?](https://arxiv.org/pdf/1810.00826.pdf). @@ -603,7 +603,7 @@ where ``f_\Theta`` typically denotes a learnable function, e.g. a linear layer o # Arguments - `f`: A (possibly learnable) function acting on node features. -- `eps`: Initial value of the weighting factor ``\epsilon``. Default `0.0f0`. +- `ϵ`: Initial value of the weighting factor ``\epsilon``. Default `0.0f0`. - `train_eps`: If `true`, ``\epsilon`` is trainable. Default `false`. - `aggr`: Neighborhood aggregation function. Default `+`. @@ -621,7 +621,7 @@ g = GNNGraph(s, t) nn = Dense(in_channel, out_channel) # create layer -l = GINConv(nn; eps = 0.01f0, train_eps = true, aggr = mean) +l = GINConv(nn; ϵ = 0.01f0, train_eps = true, aggr = mean) # forward pass y = l(g, x) @@ -629,27 +629,27 @@ y = l(g, x) """ struct GINConv{E, NN, A} <: GNNLayer nn::NN - eps::E + ϵ::E aggr::A train_eps::Bool end Flux.@layer :expand GINConv -Flux.trainable(l::GINConv) = l.train_eps ? (; l.nn, l.eps) : (; l.nn) +Flux.trainable(l::GINConv) = l.train_eps ? (; l.nn, l.ϵ) : (; l.nn) -GINConv(nn, eps::Real; aggr = +, train_eps::Bool = false) = - GINConv(nn, train_eps ? [eps] : eps, aggr, train_eps) +GINConv(nn, ϵ::Real; aggr = +, train_eps::Bool = false) = + GINConv(nn, train_eps ? [ϵ] : ϵ, aggr, train_eps) -GINConv(nn, eps::Real, aggr) = GINConv(nn, eps; aggr) +GINConv(nn, ϵ::Real, aggr) = GINConv(nn, ϵ; aggr) -GINConv(nn; eps::Real = 0.0f0, aggr = +, train_eps::Bool = false) = - GINConv(nn, eps; aggr, train_eps) +GINConv(nn; ϵ::Real = 0.0f0, aggr = +, train_eps::Bool = false) = + GINConv(nn, ϵ; aggr, train_eps) (l::GINConv)(g, x) = GNNlib.gin_conv(l, g, x) function Base.show(io::IO, l::GINConv) print(io, "GINConv($(l.nn)") - print(io, ", eps=$(l.eps)") + print(io, ", ϵ=$(l.ϵ)") l.train_eps && print(io, ", train_eps=true") l.aggr == (+) || print(io, ", aggr=$(l.aggr)") print(io, ")") diff --git a/GraphNeuralNetworks/test/layers/conv.jl b/GraphNeuralNetworks/test/layers/conv.jl index fafd68fcd..7cecaf143 100644 --- a/GraphNeuralNetworks/test/layers/conv.jl +++ b/GraphNeuralNetworks/test/layers/conv.jl @@ -278,7 +278,7 @@ end using .TestModule nn = Dense(D_IN, D_OUT) - l = GINConv(nn; eps = 0.01, aggr = mean) + l = GINConv(nn; ϵ = 0.01, aggr = mean) for g in TEST_GRAPHS @test size(l(g, g.x)) == (D_OUT, g.num_nodes) test_gradients(l, g, g.x, rtol = RTOL_HIGH) @@ -286,17 +286,17 @@ end @test keys(Flux.trainable(l)) == (:nn,) l = GINConv(Dense(D_IN, D_OUT); train_eps = true, aggr = mean) - @test keys(Flux.trainable(l)) == (:nn, :eps) - @test l.eps == [0.0f0] + @test keys(Flux.trainable(l)) == (:nn, :ϵ) + @test l.ϵ == [0.0f0] for g in TEST_GRAPHS @test size(l(g, g.x)) == (D_OUT, g.num_nodes) end g = first(TEST_GRAPHS) grad = Flux.gradient(l -> mean(l(g, g.x)), l)[1] - @test grad.eps isa AbstractVector - @test length(grad.eps) == 1 + @test grad.ϵ isa AbstractVector + @test length(grad.ϵ) == 1 - @test GINConv(nn, 0.5).eps == 0.5 + @test GINConv(nn, 0.5).ϵ == 0.5 end @testitem "GINConv GPU" setup=[TolSnippet, TestModule] tags=[:gpu] begin From f0c867608599e7ac24fab1eab98a7bb92242b3a6 Mon Sep 17 00:00:00 2001 From: Aurora Rossi Date: Fri, 24 Jul 2026 19:40:50 +0200 Subject: [PATCH 5/5] Fix gpu problem --- GNNlib/src/layers/conv.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GNNlib/src/layers/conv.jl b/GNNlib/src/layers/conv.jl index 9b34d79be..c3b1b77cc 100644 --- a/GNNlib/src/layers/conv.jl +++ b/GNNlib/src/layers/conv.jl @@ -252,7 +252,7 @@ function gin_conv(l, g::AbstractGNNGraph, x) xj, xi = expand_srcdst(g, x) m = propagate(copy_xj, g, l.aggr, xj = xj) - ϵ = convert.(float(eltype(xi)), l.ϵ) + ϵ = l.ϵ isa Real ? ofeltype(xi, l.ϵ) : l.ϵ return l.nn((1 .+ ϵ) .* xi .+ m) end