From 3377b66146c5f1a70a470f89b5ed7443da6c5b8c Mon Sep 17 00:00:00 2001 From: adrhill Date: Tue, 8 Apr 2025 17:00:00 +0200 Subject: [PATCH 01/12] Add ProgressMeter to NoiseAugmentation --- Project.toml | 2 ++ src/input_augmentation.jl | 14 ++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Project.toml b/Project.toml index 8c56c08..35bd01a 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,7 @@ version = "0.10.1" ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" DifferentiationInterface = "a0c0ee7d-e4b9-4e03-894e-1c5f64a51d63" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" +ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" @@ -16,6 +17,7 @@ XAIBase = "9b48221d-a747-4c1b-9860-46a1d8ba24a7" ADTypes = "1" DifferentiationInterface = "0.6" Distributions = "0.25" +ProgressMeter = "1.10.4" Random = "<0.0.1, 1" Reexport = "1" Statistics = "<0.0.1, 1" diff --git a/src/input_augmentation.jl b/src/input_augmentation.jl index 993c416..3d032b9 100644 --- a/src/input_augmentation.jl +++ b/src/input_augmentation.jl @@ -29,17 +29,20 @@ struct NoiseAugmentation{A<:AbstractXAIMethod,D<:Sampleable,R<:AbstractRNG} <: n::Int distribution::D rng::R + show_progress::Bool function NoiseAugmentation( - analyzer::A, n::Int, distribution::D, rng::R=GLOBAL_RNG + analyzer::A, n::Int, distribution::D, rng::R=GLOBAL_RNG, show_progress=true ) where {A<:AbstractXAIMethod,D<:Sampleable,R<:AbstractRNG} n < 1 && throw(ArgumentError("Number of samples `n` needs to be larger than zero.")) - return new{A,D,R}(analyzer, n, distribution, rng) + return new{A,D,R}(analyzer, n, distribution, rng, show_progress) end end -function NoiseAugmentation(analyzer, n::Int, std::T=1.0f0, rng=GLOBAL_RNG) where {T<:Real} +function NoiseAugmentation( + analyzer, n::Int, std::T=1.0f0, rng=GLOBAL_RNG, show_progress=true +) where {T<:Real} distribution = Normal(zero(T), std^2) - return NoiseAugmentation(analyzer, n, distribution, rng) + return NoiseAugmentation(analyzer, n, distribution, rng, show_progress) end function call_analyzer(input, aug::NoiseAugmentation, ns::AbstractOutputSelector; kwargs...) @@ -48,17 +51,20 @@ function call_analyzer(input, aug::NoiseAugmentation, ns::AbstractOutputSelector output_indices = ns(output) output_selector = AugmentationSelector(output_indices) + p = Progress(aug.n; desc="Sampling NoiseAugmentation...", showspeed=aug.show_progress) # First augmentation input_aug = similar(input) input_aug = sample_noise!(input_aug, input, aug) expl_aug = aug.analyzer(input_aug, output_selector) sum_val = expl_aug.val + next!(p) # Further augmentations for _ in 2:(aug.n) input_aug = sample_noise!(input_aug, input, aug) expl_aug = aug.analyzer(input_aug, output_selector) sum_val += expl_aug.val + next!(p) end # Average explanation From a9a493bd93b44b18a2272490e4cf5aa0ee2bbaf5 Mon Sep 17 00:00:00 2001 From: adrhill Date: Tue, 8 Apr 2025 17:00:09 +0200 Subject: [PATCH 02/12] Add GPU tests --- test/Project.toml | 2 ++ test/runtests.jl | 3 +++ test/test_gpu.jl | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 test/test_gpu.jl diff --git a/test/Project.toml b/test/Project.toml index a8c6b4b..1bf524e 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -4,9 +4,11 @@ BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" Flux = "587475ba-b771-5e3f-ad9e-33799f191a9c" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" +JLArrays = "27aeb0d3-9eb9-45fb-866b-73c2ecf80fcb" JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Metal = "dde4c033-4e86-420c-a63e-0dd931031962" PkgJogger = "10150987-6cc1-4b76-abee-b1c1cbd91c01" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" ReferenceTests = "324d217c-45ce-50fc-942e-d289b448e8cf" diff --git a/test/runtests.jl b/test/runtests.jl index c99e335..356f7dd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -33,6 +33,9 @@ using JET @info "Testing analyzers on batches..." include("test_batches.jl") end + @testset "GPU tests" begin + include("test_gpu.jl") + end @testset "Benchmark correctness" begin @info "Testing whether benchmarks are up-to-date..." include("test_benchmarks.jl") diff --git a/test/test_gpu.jl b/test/test_gpu.jl new file mode 100644 index 0000000..a791d54 --- /dev/null +++ b/test/test_gpu.jl @@ -0,0 +1,39 @@ +using ExplainableAI +using Test + +using Flux +using Metal, JLArrays + +if Metal.functional() + @info "Using Metal as GPU device" + device = mtl # use Apple Metal locally +else + @info "Using JLArrays as GPU device" + device = jl # use JLArrays to fake GPU array +end + +model = Chain(Dense(10 => 32, relu), Dense(32 => 5)) +input = rand(Float32, 10, 8) +@test_nowarn model(input) + +model_gpu = device(model) +input_gpu = device(input) +@test_nowarn model_gpu(input_gpu) + +analyzer_types = (Gradient, SmoothGrad, InputTimesGradient) + +@testset "Run analyzer (CPU)" begin + @testset "$A" for A in analyzer_types + analyzer = A(model) + expl = analyze(input, analyzer) + @test expl isa Explanation + end +end + +@testset "Run analyzer (GPU)" begin + @testset "$A" for A in analyzer_types + analyzer_gpu = A(model_gpu) + expl = analyze(input_gpu, analyzer_gpu) + @test expl isa Explanation + end +end From 5974a12ab47e77f08e30c2d5b14950025894082b Mon Sep 17 00:00:00 2001 From: adrhill Date: Tue, 8 Apr 2025 17:00:22 +0200 Subject: [PATCH 03/12] GPU friendly implementation of SmoothGrad --- src/ExplainableAI.jl | 3 ++- src/input_augmentation.jl | 17 ++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/ExplainableAI.jl b/src/ExplainableAI.jl index 2ed8ea9..f65dd53 100644 --- a/src/ExplainableAI.jl +++ b/src/ExplainableAI.jl @@ -6,7 +6,8 @@ import XAIBase: call_analyzer using Base.Iterators using Distributions: Distribution, Sampleable, Normal -using Random: AbstractRNG, GLOBAL_RNG +using Random: AbstractRNG, GLOBAL_RNG, rand! +using ProgressMeter: Progress, next! # Automatic differentiation using ADTypes: AbstractADType, AutoZygote diff --git a/src/input_augmentation.jl b/src/input_augmentation.jl index 3d032b9..cf8043d 100644 --- a/src/input_augmentation.jl +++ b/src/input_augmentation.jl @@ -52,18 +52,19 @@ function call_analyzer(input, aug::NoiseAugmentation, ns::AbstractOutputSelector output_selector = AugmentationSelector(output_indices) p = Progress(aug.n; desc="Sampling NoiseAugmentation...", showspeed=aug.show_progress) + # First augmentation - input_aug = similar(input) - input_aug = sample_noise!(input_aug, input, aug) - expl_aug = aug.analyzer(input_aug, output_selector) + noisy_input = similar(input) + noisy_input = sample_noise!(noisy_input, input, aug) + expl_aug = aug.analyzer(noisy_input, output_selector) sum_val = expl_aug.val next!(p) # Further augmentations for _ in 2:(aug.n) - input_aug = sample_noise!(input_aug, input, aug) - expl_aug = aug.analyzer(input_aug, output_selector) - sum_val += expl_aug.val + noisy_input = sample_noise!(noisy_input, input, aug) + expl_aug = aug.analyzer(noisy_input, output_selector) + sum_val .+= expl_aug.val next!(p) end @@ -78,7 +79,9 @@ end function sample_noise!( out::A, input::A, aug::NoiseAugmentation ) where {T,A<:AbstractArray{T}} - out .= input .+ rand(aug.rng, aug.distribution, size(input)) + out = rand!(aug.rng, aug.distribution, out) + out .+= input + return out end """ From 032b617221f65d3a376c9c8f7033dbe76096be6f Mon Sep 17 00:00:00 2001 From: adrhill Date: Tue, 8 Apr 2025 17:04:26 +0200 Subject: [PATCH 04/12] Test `IntegratedGradients` as well --- src/input_augmentation.jl | 4 ++-- test/test_gpu.jl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/input_augmentation.jl b/src/input_augmentation.jl index cf8043d..6b37482 100644 --- a/src/input_augmentation.jl +++ b/src/input_augmentation.jl @@ -123,9 +123,9 @@ function call_analyzer( # Further augmentations input_delta = (input - input_ref) / (aug.n - 1) for _ in 1:(aug.n) - input_aug += input_delta + input_aug .+= input_delta expl_aug = aug.analyzer(input_aug, output_selector) - sum_val += expl_aug.val + sum_val .+= expl_aug.val end # Average gradients and compute explanation diff --git a/test/test_gpu.jl b/test/test_gpu.jl index a791d54..b7e112b 100644 --- a/test/test_gpu.jl +++ b/test/test_gpu.jl @@ -20,7 +20,7 @@ model_gpu = device(model) input_gpu = device(input) @test_nowarn model_gpu(input_gpu) -analyzer_types = (Gradient, SmoothGrad, InputTimesGradient) +analyzer_types = (Gradient, SmoothGrad, InputTimesGradient, IntegratedGradients) @testset "Run analyzer (CPU)" begin @testset "$A" for A in analyzer_types From 8d50760aea110641c7970b2bb2778872375f280e Mon Sep 17 00:00:00 2001 From: adrhill Date: Tue, 8 Apr 2025 17:04:36 +0200 Subject: [PATCH 05/12] Bump version --- CHANGELOG.md | 4 ++++ Project.toml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6e04d4..b6ed513 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # ExplainableAI.jl +## Version `v0.10.2` +- ![Feature][badge-feature] Tested GPU support for `Gradient`, `InputTimesGradient`, `SmoothGrad`, `IntegratedGradients` +- ![Feature][badge-feature] `NoiseAugmentation` and `SmoothGrad` show a progress meter by default. + ## Version `v0.10.1` - ![Bugfix][badge-bugfix] Fix bug in `NoiseAugmentation` constructor ([#183]) diff --git a/Project.toml b/Project.toml index 35bd01a..ad994f8 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "ExplainableAI" uuid = "4f1bc3e1-d60d-4ed0-9367-9bdff9846d3b" authors = ["Adrian Hill "] -version = "0.10.1" +version = "0.10.2-DEV" [deps] ADTypes = "47edcb42-4c32-4615-8424-f2b9edc5f35b" From a8424049ea9473dbf6d94636331997e094035ab6 Mon Sep 17 00:00:00 2001 From: adrhill Date: Tue, 8 Apr 2025 17:08:59 +0200 Subject: [PATCH 06/12] Update IG reference --- src/input_augmentation.jl | 1 + .../cnn/IntegratedGradients_max.jld2 | Bin 13127 -> 13127 bytes 2 files changed, 1 insertion(+) diff --git a/src/input_augmentation.jl b/src/input_augmentation.jl index 6b37482..ea910b2 100644 --- a/src/input_augmentation.jl +++ b/src/input_augmentation.jl @@ -22,6 +22,7 @@ e.g. `std = 0.1 * (maximum(input) - minimum(input))`. ## Keyword arguments - `rng::AbstractRNG`: Specify the random number generator that is used to sample noise from the `distribution`. Defaults to `GLOBAL_RNG`. +- `show_progress:Bool`: Show progress meter while sampling augmentations. Defaults to `true`. """ struct NoiseAugmentation{A<:AbstractXAIMethod,D<:Sampleable,R<:AbstractRNG} <: AbstractXAIMethod diff --git a/test/references/cnn/IntegratedGradients_max.jld2 b/test/references/cnn/IntegratedGradients_max.jld2 index 4e3b68de3bf85c335bcb6df0790e02cca2235a9b..ad72aebe3afcac0bd12f4e6384014affb67a458a 100644 GIT binary patch literal 13127 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 From 319419463d31d43b3339c7c8d35c43cf0c5700b6 Mon Sep 17 00:00:00 2001 From: adrhill Date: Tue, 8 Apr 2025 17:11:39 +0200 Subject: [PATCH 07/12] Update CHANGELOG --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6ed513..794ea47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # ExplainableAI.jl ## Version `v0.10.2` -- ![Feature][badge-feature] Tested GPU support for `Gradient`, `InputTimesGradient`, `SmoothGrad`, `IntegratedGradients` -- ![Feature][badge-feature] `NoiseAugmentation` and `SmoothGrad` show a progress meter by default. +- ![Feature][badge-feature] Tested GPU support for `Gradient`, `InputTimesGradient`, `SmoothGrad`, `IntegratedGradients` ([#184]) +- ![Feature][badge-feature] `NoiseAugmentation`s show a progress meter by default. Turn off via `show_progress=false` ([#184]) ## Version `v0.10.1` - ![Bugfix][badge-bugfix] Fix bug in `NoiseAugmentation` constructor ([#183]) @@ -231,6 +231,7 @@ Performance improvements: [VisionHeatmaps]: https://julia-xai.github.io/XAIDocs/VisionHeatmaps/stable/ [TextHeatmaps]: https://julia-xai.github.io/XAIDocs/TextHeatmaps/stable/ +[#184]: https://github.com/Julia-XAI/ExplainableAI.jl/pull/184 [#183]: https://github.com/Julia-XAI/ExplainableAI.jl/pull/183 [#180]: https://github.com/Julia-XAI/ExplainableAI.jl/pull/180 [#179]: https://github.com/Julia-XAI/ExplainableAI.jl/pull/179 From c144e9629298b9a113600dc92129085cac47b74d Mon Sep 17 00:00:00 2001 From: adrhill Date: Tue, 8 Apr 2025 17:33:22 +0200 Subject: [PATCH 08/12] Ignore JET failures on pre-release --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c6f9fb..cfb0363 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: version: - 'lts' - '1' - - 'pre' + # - 'pre' steps: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v2 From 73381464dd6c2eac5120307e9f4e1c7418e4b3bd Mon Sep 17 00:00:00 2001 From: adrhill Date: Tue, 8 Apr 2025 17:39:44 +0200 Subject: [PATCH 09/12] Cleaner fix for JET issue --- .github/workflows/ci.yml | 2 +- test/runtests.jl | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfb0363..7c6f9fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: version: - 'lts' - '1' - # - 'pre' + - 'pre' steps: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v2 diff --git a/test/runtests.jl b/test/runtests.jl index 356f7dd..0ba1e87 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -15,9 +15,11 @@ using JET @info "- running Aqua.jl tests..." Aqua.test_all(ExplainableAI; ambiguities=false) end - @testset "JET tests" begin - @info "- running JET.jl type stability tests..." - JET.test_package(ExplainableAI; target_defined_modules=true) + if VERSION <= v"1.12" # TODO: remove. As of PR #184, JET fails on pre-release builds. + @testset "JET tests" begin + @info "- running JET.jl type stability tests..." + JET.test_package(ExplainableAI; target_defined_modules=true) + end end end From ceb26fab09564583c8b33e9dc5e41415d18e6b9f Mon Sep 17 00:00:00 2001 From: adrhill Date: Tue, 8 Apr 2025 18:10:54 +0200 Subject: [PATCH 10/12] Exclude JET on Julia 1.12 --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 0ba1e87..ca6f855 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -15,7 +15,7 @@ using JET @info "- running Aqua.jl tests..." Aqua.test_all(ExplainableAI; ambiguities=false) end - if VERSION <= v"1.12" # TODO: remove. As of PR #184, JET fails on pre-release builds. + if VERSION < v"1.12" # TODO: remove. As of PR #184, JET fails on pre-release builds. @testset "JET tests" begin @info "- running JET.jl type stability tests..." JET.test_package(ExplainableAI; target_defined_modules=true) From b09f09b6d7d962f67ebb288de84d8c3b3dbc2cb9 Mon Sep 17 00:00:00 2001 From: adrhill Date: Tue, 8 Apr 2025 18:38:14 +0200 Subject: [PATCH 11/12] Exclude pre-release entirely --- .github/workflows/ci.yml | 2 +- test/runtests.jl | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c6f9fb..cfb0363 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: version: - 'lts' - '1' - - 'pre' + # - 'pre' steps: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v2 diff --git a/test/runtests.jl b/test/runtests.jl index ca6f855..356f7dd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -15,11 +15,9 @@ using JET @info "- running Aqua.jl tests..." Aqua.test_all(ExplainableAI; ambiguities=false) end - if VERSION < v"1.12" # TODO: remove. As of PR #184, JET fails on pre-release builds. - @testset "JET tests" begin - @info "- running JET.jl type stability tests..." - JET.test_package(ExplainableAI; target_defined_modules=true) - end + @testset "JET tests" begin + @info "- running JET.jl type stability tests..." + JET.test_package(ExplainableAI; target_defined_modules=true) end end From 3d23d8b162b42e6ddecc49cbd6b94fcb82ffe4e6 Mon Sep 17 00:00:00 2001 From: adrhill Date: Tue, 8 Apr 2025 18:45:47 +0200 Subject: [PATCH 12/12] Fix silencing of progress meter --- src/input_augmentation.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input_augmentation.jl b/src/input_augmentation.jl index ea910b2..8753faa 100644 --- a/src/input_augmentation.jl +++ b/src/input_augmentation.jl @@ -52,7 +52,7 @@ function call_analyzer(input, aug::NoiseAugmentation, ns::AbstractOutputSelector output_indices = ns(output) output_selector = AugmentationSelector(output_indices) - p = Progress(aug.n; desc="Sampling NoiseAugmentation...", showspeed=aug.show_progress) + p = Progress(aug.n; desc="Sampling NoiseAugmentation...", enabled=aug.show_progress) # First augmentation noisy_input = similar(input)