Demystifying torch.sparse.spdiags: Creating Sparse Diagonal Matrices in PyTorch


torch.sparse.spdiags Function

This function constructs a sparse matrix from diagonals and offsets. It's particularly useful when you want to represent matrices where most elements are zero, improving memory efficiency and computational speed.

Parameters

  • shape (tuple of 2 ints): A tuple specifying the desired shape (rows, cols) of the output sparse matrix.
  • offsets (Tensor): A 1D integer tensor of length num_diagonals specifying the offsets of the diagonals from the main diagonal. Positive offsets represent diagonals above the main diagonal, and negative offsets represent diagonals below.
  • diagonals (Tensor): A tensor of shape (num_diagonals, min(shape)) storing the diagonals row-wise. num_diagonals is the number of diagonals you want to include in the sparse matrix, and min(shape) is the minimum dimension (rows or columns) of the desired output matrix.

Example

import torch

diagonals = torch.tensor([1, 2, 3])  # Three diagonals
offsets = torch.tensor([-1, 0, 1])  # One below, main, and one above
shape = (5, 5)  # Desired output shape

sparse_matrix = torch.sparse.spdiags(diagonals, offsets, shape)

print(sparse_matrix)

This code will create a sparse matrix with the following structure:

     0     1     2     3     4
0  1.0   0.0   0.0   0.0   0.0
1  0.0   2.0   0.0   0.0   0.0
2  0.0   0.0   3.0   0.0   0.0
3  0.0   0.0   0.0   1.0   0.0
4  0.0   0.0   0.0   0.0   1.0

As you can see, only the elements on the specified diagonals (-1, 0, and 1) have non-zero values (1, 2, and 3).

  • PyTorch doesn't currently have a built-in torch.sparse.diags function for constructing sparse diagonal matrices from a single diagonal array. You can achieve this by creating a tensor with the desired diagonal elements and using spdiags with an offset of 0.
  • Offsets cannot be repeated, meaning you cannot have multiple diagonals at the same position.
  • The number of rows in diagonals must match the length of offsets.


Sparse Diagonal Matrix from a Single Diagonal

import torch

diagonal_values = torch.tensor([2, 4, 6, 8])  # Elements of the diagonal
shape = (4, 4)  # Desired output shape

# Create a tensor with the diagonal values and offset of 0
diagonals = torch.diag(diagonal_values)
offsets = torch.zeros(1)  # Offset of 0 for the main diagonal

sparse_matrix = torch.sparse.spdiags(diagonals, offsets, shape)

print(sparse_matrix)

This code will create a sparse matrix with non-zero values only on the main diagonal ([2, 4, 6, 8]).

Creating a Sparse Identity Matrix

You can use torch.sparse.spdiags to create a sparse identity matrix:

import torch

size = 5  # Size of the identity matrix

identity_diagonals = torch.ones(1)  # One diagonal with all ones
offsets = torch.zeros(1)  # Offset of 0 for the main diagonal

sparse_identity = torch.sparse.spdiags(identity_diagonals, offsets, (size, size))

print(sparse_identity)

This code will create a sparse identity matrix with shape (5, 5).

Sparse Matrix with Multiple Off-Diagonal Elements

import torch

diagonals = torch.tensor([3, 2, 1])  # Three diagonals
offsets = torch.tensor([-2, 0, 1])  # Two below, main, and one above

shape = (5, 5)  # Desired output shape

sparse_matrix = torch.sparse.spdiags(diagonals, offsets, shape)

print(sparse_matrix)

This code will create a sparse matrix with non-zero values on diagonals -2, 0, and 1.



Manual Construction with torch.sparse_coo_tensor (Efficient for Small Matrices)

import torch

diagonal_values = torch.tensor([2, 4, 6, 8])  # Elements for the main diagonal
shape = (4, 4)  # Desired output shape

# Calculate indices for the diagonal entries
i = torch.arange(shape[0])
j = i

# Create sparse COO tensor with diagonal indices and values
sparse_matrix = torch.sparse_coo_tensor(torch.stack([i, j]), diagonal_values, shape)

print(sparse_matrix)

torch.diag_embed (Convenient for Diagonal Embedding)

If you want to embed a diagonal vector into a larger square matrix, torch.diag_embed is a suitable choice:

import torch

diagonal_values = torch.tensor([2, 4, 6, 8])

# Embed the diagonal into a larger matrix (5x5 in this example)
sparse_matrix = torch.diag_embed(diagonal_values, dim1=5, dim2=5)

print(sparse_matrix)

torch.sparse.sum with Dense Diagonal (Efficient for Large Matrices)

For larger sparse matrices, a combination of torch.sparse.sum with a dense diagonal tensor can be memory-efficient:

import torch

dense_diagonal = torch.diag(torch.arange(1, 101))  # Example dense diagonal
sparse_matrix = A  # Your existing sparse matrix (replace A with your actual matrix)

# Efficiently multiply sparse and dense matrices
sparse_diagonal = torch.sparse.sum(sparse_matrix * dense_diagonal, dim=-1)

# Combine the sparse diagonal with the original sparse matrix (if needed)
combined_matrix = torch.sparse.block_diag(sparse_matrix, sparse_diagonal.unsqueeze(-1))

print(combined_matrix)  # Output will depend on the structure of your sparse matrix A

This approach avoids creating a large sparse diagonal matrix, potentially saving memory.

  • Large Sparse Matrices with Sparse-Dense Multiplication
    Consider torch.sparse.sum with a dense diagonal for memory efficiency.
  • Diagonal Embedding
    Use torch.diag_embed for its convenience.
  • Small Matrices
    Use torch.sparse_coo_tensor for its simplicity.