Hi All,
I implemented the paper BiRank: Towards Ranking on Bipartite Graphs in Julia (R and Python implementations already exist here). I was wondering if there are any obvious performance problems I missed? I noticed large differences when the eltype
of my Sparse W
is UInt32
(much slower) compared to Float64
. Also should I try to integrate it with Graphs.jl given that it is specifically for bipartite graphs? Or create a separate package?
Thanks!
module BiRank
using LinearAlgebra
using SparseArrays
using ProgressMeter
export birank
"""
birank(W; method="BiRank",
α=0.85, β=0.85, u⁰=nothing, p⁰=nothing,
max_iter=200, tol=1.0e-10)
Implements Algorithm 1 in
BiRank: Towards Ranking on Bipartite Graphs by
Xiangnan He, Ming Gao Member, Min-Yen Kan Member, and Dingxian Wang
- W: Weighted adjacency matrix (dim: |U| x |P|)
- method: one of "HITS", "CoHITS", "BGER", "BGRM", "BiRank"
- α: damping factor for 'p'
- β: damping factor for 'u'
- u⁰: query vector for 'u'. Defaults to 1/|U| for all elements.
- p⁰: query vector for 'p'. Defaults to 1/|P| for all elements.
- max_iter: maximum number of iterations
- tol: tolerance for convergence
"""
function birank(W; method="BiRank",
α=0.85, β=0.85,
u⁰=nothing, p⁰=nothing,
max_iter=200, tol=1.0e-10)
method = lowercase(method)
Wᵀ = transpose(W)
## Weighted degrees
Dₚ_vec = sum(W; dims=1) |> vec
Dᵤ_vec = sum(W; dims=2) |> vec
## Avoid division by 0
Dₚ_vec[Dₚ_vec.==zero(eltype(Dₚ_vec))] .= one(eltype(Dₚ_vec))
Dᵤ_vec[Dᵤ_vec.==zero(eltype(Dᵤ_vec))] .= one(eltype(Dᵤ_vec))
Dₚ⁻¹ = Diagonal(one(eltype(Dₚ_vec)) ./ Dₚ_vec)
Dᵤ⁻¹ = Diagonal(one(eltype(Dᵤ_vec)) ./ Dᵤ_vec)
## Table 1 in "BiRank: Towards Ranking on Bipartite Graphs"
is_hits = method == "hits"
if is_hits
S = W
Sᵀ = Wᵀ
elseif method == "cohits"
## S = W Dₚ⁻¹
## Sᵀ = Wᵀ Dᵤ⁻¹
S = W * Dₚ⁻¹
Sᵀ = Wᵀ * Dᵤ⁻¹
elseif method == "bger"
## S = Dᵤ⁻¹ W
## Sᵀ = Dₚ⁻¹ Wᵀ
S = Dᵤ⁻¹ * W
Sᵀ = Dₚ⁻¹ * Wᵀ
elseif method == "bgrm"
## S = Dᵤ⁻¹ W Dₚ⁻¹
## Sᵀ = Dₚ⁻¹ Wᵀ Dᵤ⁻¹
S = (Dᵤ⁻¹ * W) * Dₚ⁻¹
Sᵀ = transpose(S)
elseif method == "birank"
## S = sqrt(Dᵤ)⁻¹ W sqrt(Dₚ)⁻¹
## Sᵀ = sqrt(Dₚ)⁻¹ Wᵀ sqrt(Dᵤ)⁻¹
sqrtDᵤ⁻¹ = Diagonal(one(eltype(Dᵤ_vec)) ./ sqrt.(abs.(Dᵤ_vec)))
sqrtDₚ⁻¹ = Diagonal(one(eltype(Dₚ_vec)) ./ sqrt.(abs.(Dₚ_vec)))
S = (sqrtDᵤ⁻¹ * W) * sqrtDₚ⁻¹
Sᵀ = transpose(S)
else
error("""method must be one of "HITS", "CoHITS", "BGER", "BGRM", "BiRank\"""")
end
isnothing(u⁰) && (u⁰ = fill(one(eltype(Sᵀ)) / size(W, 1), size(W, 1)))
uₗ = copy(u⁰)
isnothing(p⁰) && (p⁰ = fill(one(eltype(S)) / size(W, 2), size(W, 2)))
pₗ = copy(p⁰)
normalizer_names = Dict("hits" => "HITS", "cohits" => "CoHITS", "bger" => "BGER", "bgrm" => "BGRM", "birank" => "BiRank")
progress = ProgressUnknown("Running $(normalizer_names[method])...")
let u = copy(uₗ), p = copy(pₗ)
for i in 1:max_iter
p .= α * (Sᵀ * u) + (one(α) - α) * p⁰
is_hits && (p ./= sum(p))
u .= β * (S * p) + (one(β) - β) * u⁰
is_hits && (u ./= sum(u))
εₚ = sum(abs.(p - pₗ))
εᵤ = sum(abs.(u - uₗ))
if εₚ < tol && εᵤ < tol
ProgressMeter.next!(progress)
ProgressMeter.finish!(progress)
@info "Converged after $i iterations"
break
end
ProgressMeter.next!(progress)
uₗ .= u
pₗ .= p
if i == max_iter
ProgressMeter.finish!(progress)
@warn "Not converged after $max_iter iterations. Consider increasing max_iter."
end
end
return u, p
end
end
end