Разработчики PyTorch предоставили модуль torch.sparse
для работы с разреженными тензорами, где большинство элементов – нули. Зачем это нужно? Представьте матрицу смежности графа, сильно обрезанную сеть или облако точек – хранить такие данные плотным массивом без надобности расточительно. Разрежённая структура сохраняет только ненулевые элементы и их индексы, что сильно экономит память и ускоряет вычисления. Например, матрица размером 10,000 на 10,000 с 100 000 ненулевых float-значений в разрежённом COO-формате займёт не 400 МБ, а около 2 МБ.
Несмотря на перспективы, API разрежённых тензоров в PyTorch пока в бете и может менять крошечные детали. Будьте к этому готовы: часть операций поддерживается, часть – нет, и некоторые автоград-ячейки пока работают только для COO, а для CSR, например, градиент не считается. Но обо всём по порядку.
Основные форматы разрежённых тензоров
PyTorch поддерживает несколько форматов хранения sparse-данных: COO, CSR, CSC, а также блочно-разреженные BSR и BSC.
COO (coordinate) – самый базовый. Два 1D-тензора indices
и values
хранят индексы ненулей и их значения. indices
имеет форму (ndim, nnz)
, где nnz
– число ненулевых элементов, а values
– (nnz,)
(для скалярных значений) или (nnz, *dense_dims)
для гибридных (tensor-valued) форм. Типы: индексы – int64
, значения – любой числовой. Пустой COO-тензор можно создать, указав только размер. Пример создания и использования COO:
import torch
# создаём разреженный COO-тензор: ненулевые 3,4,5 в позициях (0,2),(1,0),(1,2) матрицы 2x3
i = [[0, 1, 1], # строки ненулей
[2, 0, 2]] # столбцы ненулей
v = [3, 4, 5] # значения ненулей
s = torch.sparse_coo_tensor(i, v, size=(2, 3))
print(s)
# tensor(indices=tensor([[0, 1, 1],
# [2, 0, 2]]),
# values=tensor([3, 4, 5]),
# size=(2, 3), nnz=3, layout=torch.sparse_coo)
print("Плотная матрица:\n", s.to_dense())
# Плотная матрица:
# tensor([[0, 0, 3],
# [4, 0, 5]])
Явно передаём индексы и значения. Метод to_dense()
восстанавливает обычный (плотный) тензор. Если индексы неотсортированы или дублируются, у COO-тензора есть понятие uncoalesced и coalesced. В некой sense, некоалесцированный тензор может иметь несколько записей для одной и той же позиции (их интерпретируют как суммы). Метод s.coalesce()
объединит дубликаты, просуммировав значения. Например:
# Пример: создаём COO с дублированием индексов (ненулевых элемент 1,1 два раза):
i = [[1, 1]] # оба индекса (1,1)
v = [3, 4] # два значения для одной позиции
s = torch.sparse_coo_tensor(i, v, (3,)) # одномерный 3-элементный тензор
print("Неcoalesced:", s)
# tensor(indices=tensor([[1, 1]]),
# values=tensor([3, 4]),
# size=(3,), nnz=2, layout=torch.sparse_coo)
s2 = s.coalesce()
print("Coalesced:", s2)
# tensor(indices=tensor([[1]]),
# values=tensor([7]), # 3+4=7
# size=(3,), nnz=1, layout=torch.sparse_coo)
После coalesce()
индексы становятся уникальными и отсортированными. Большинство операций с разрежёнными данными автоматически коалесцируются по необходимости (для линейных операций это безопасно). Следует лишь учесть: не коалесцированные тензоры могут не поддерживать все методы (например, нельзя просто взять .indices()
без coalesce()
). Учтите, что to_dense()
всё равно вернёт правильный результат, не важно, coalesced тензор или нет.
CSR – хранит данные в CSR-формате: два вектора индексов (строк и столбцов) и значения. Конструкция: crow_indices
размера (nrows+1,)
, col_indices
длины nnz
и values
длины nnz
. crow_indices[i]
– это индекс в values/col_indices
, с которого начинается строка i
. Основное преимущество CSR – умножение разреженной матрицы на вектор/матрицу. В PyTorch множества sparse-операций в CSR идут быстрее, чем для COO. Пример создания CSR-тензора:
# Создаём разреженный CSR-тензор вручную для матрицы 2x2:
crow = [0, 2, 4] # crow_indices: 0-я строка с 0 по 1, 1-я строка с 2 по 3 (nnz=4)
cols = [0, 1, 0, 1] # индексы столбцов для каждого ненулевого значения
vals = [1, 2, 3, 4] # сами значения
csr = torch.sparse_csr_tensor(
torch.tensor(crow, dtype=torch.int64),
torch.tensor(cols, dtype=torch.int64),
torch.tensor(vals, dtype=torch.float32)
)
print(csr)
# tensor(crow_indices=tensor([0, 2, 4]),
# col_indices=tensor([0, 1, 0, 1]),
# values=tensor([1., 2., 3., 4.]), size=(2, 2), nnz=4,
# dtype=torch.float32, layout=torch.sparse_csr)
Или проще: любая плотная матрица или COO-тензор A
может быть сконвертирован в CSR одним методом A.to_sparse_csr()
. Пример:
dense = torch.tensor([[0,1,0],[2,0,3]], dtype=torch.float32)
csr2 = dense.to_sparse_csr() # плотный -> CSR
print(csr2)
В CSR автоматом воспринимаются все нули как отсутствующие элементы. Стандартно crow_indices
и col_indices
имеют тип int64
, хотя для совместимости с MKL можно использовать int32
. PyTorch требует, чтобы в CSR индексы были целыми (по умолчанию torch.int64
).
CSR хорош для умножения разреженной матрицы на вектор или другую матрицу. В PyTorch для CSR доступны операции вида csr.matmul(vec)
или torch.sparse.mm(csr, B)
. При умножении разреженная матрица плотная даётся обычная плотная матрица, а разрежённая разрежённая выдаёт разрежённую. Например:
# Умножение CSR на плотную матрицу
a = torch.tensor([[1.,0,2],[0,3,0]]).to_sparse_csr()
b = torch.tensor([[0., 1.], [2., 0.], [0., 0.]])
y = torch.sparse.mm(a, b)
print(y)
# tensor([[0., 1.],
# [6., 0.]])
Здесь a
– CSR тензор 2×3, b
– плотная 3×2, результат – плотная 2×2 (как и Dense×Dense). При этом torch.sparse.mm
поддерживает и CSR, и COO форматы. Учтите, что бэпроп (градиент) пока НЕ считается по CSR-матрице.
CSC – аналогично CSR, но сжатие по столбцам. Формат: тензоры ccol_indices
(размер (ncols+1,)
), row_indices
(длины nnz
) и values
(длины nnz
). CSR и CSC – транспонированные друг друга версии. PyTorch позволяет создать CSC напрямую через torch.sparse_csc_tensor
. Вот пример:
ccol = torch.tensor([0, 2, 4], dtype=torch.int64) # ccol_indices
rows = torch.tensor([0, 1, 0, 1], dtype=torch.int64) # row_indices
vals = torch.tensor([1.,2.,3.,4.], dtype=torch.float32)
csc = torch.sparse_csc_tensor(ccol, rows, vals)
print(csc)
# tensor(ccol_indices=tensor([0, 2, 4]),
# row_indices=tensor([0, 1, 0, 1]),
# values=tensor([1., 2., 3., 4.]), size=(2, 2), nnz=4,
# dtype=torch.float32, layout=torch.sparse_csc)
print(csc.to_dense())
# tensor([[1., 3.],
# [2., 4.]])
CSC выигрывает там же, где CSR – особенно при операциях по столбцам. Во многом, если нужна разреженная матричная операция, имеет смысл использовать CSR или CSC, а не COO напрямую.
BSR/BSC – разрежённые тензоры с блочной структурой. Аналогично CSR/CSC, но values
– двухмерные блоки. PyTorch поддерживает torch.sparse_bsr_tensor
и torch.sparse_bsc_tensor
(с параметром blocksize
). Блочные форматы экономят индексы, когда ненулевые элементы сгруппированы в равные блоки. Их использование специализируется на некоторых оптимизациях, но применяются реже. Словом, они есть, но для основного ознакомления хватит знаний про CSR/CSC.
Как конвертировать и проверять
Из dense в sparse: любой обычный (плотный) тензор X
можно перевести в разрежённый формат. По умолчанию X.to_sparse()
делает COO-сжатие (считаются все ненули). Похожее: X.to_sparse_coo()
, X.to_sparse_csr()
, X.to_sparse_csc()
и т.д. Например:
dense = torch.tensor([[1,0,2],[0,3,0]])
sparse_coo = dense.to_sparse_coo()
print(sparse_coo) # COO-версия
sparse_csr = dense.to_sparse_csr()
print(sparse_csr) # CSR-версия
Метод to_sparse_csr()
автоматически учитывает, сколько у матрицы разреженных измерений. Обратите внимание, что при конверсии все нулевые элементы теряются: например, у a = torch.tensor([[0,0,1],[2,3,0]])
при .to_sparse_csr()
будут сохранены только ненулевые значения 1, 2, 3, а сама позиция других – отсутствует (предполагается 0).
Из sparse в dense: оазрежённый тензор всегда можно превратить обратно в обычный через метод .to_dense()
. Обычно это используется для отладки или когда потеря преимуществ sparse больше, чем затраты на конвертацию.
Методы проверки: чтобы не ошибиться с размерностями, есть у разрежённого тензора свойства sparse_dim()
, dense_dim()
, s.is_sparse
, s.is_sparse_csr
и т.д. И например s.layout
покажет формат (torch.sparse_coo
, torch.sparse_csr
и т.п.). Проверки инвариантов индексов можно включать (через аргумент check_invariants=True
при создании) – это помогает отловить несогласованность форм, но по умолчанию они отключены.
Операции с разреженными тензорами
В большинстве случаев операции с разрежёнными тензорами семантически аналогичны обычным. Но обратите внимание:
Умножение матриц: как мы уже видели,
torch.sparse.mm(A, B)
позволяет умножать разреженную матрицуA
на любую другуюB
. Если оба исходника разрежены, ответ тоже разрежен; если второй плотный, ответ – плотный. Бэпроп работает по COO-операндам, но с CSR-матрицами градиент не считается (ограничение текущей реализации). Также PyTorch поддерживаетtorch.sparse.addmm(C, A, B)
,torch.sparse.sum(sparse, dim)
и ряд других операций из модуляtorch.sparse
.Сумма и добавление: разрежённые тензоры можно складывать:
A + B
для двух COO- или CSR-матриц (с одним и тем же размером). Если есть совпадения индексов, то после операции получается некоалесцированный результат – PyTorch просто конкатенирует индексы и значения. Иногда полезно потом вызвать.coalesce()
, чтобы объединить дубликаты и не раздувать структуру. Добавление с плотной матрицей по дефолту приводит к плотному результату – это фича, если хочется жесткой экономии, нужно заранее перевести вторую матрицу тоже в sparse и пользоватьсяtorch.sparse.mm
.Другие операции: множество базовых операций (
transpose
,reshape
,sum
,max
и т.д.) над sparse-тензорами поддерживаются, но не все.
В общем важно помнить: сохраняйте тензор в разреженном формате как можно дольше и используйте специальные функции для sparse.
Заключение
Итак, torch.sparse
имеет смысл там, где нулей много, а математика простая и регулярная. Форматы выбирают так: COO для построения и обновлений, CSR/CSC для линейной алгебры, BSR — когда есть устойчивые блоки. Если плотность растёт — возвращаемся к dense.
Машинное обучение выглядит красиво в презентациях, но в продакшене всё быстро упирается в вопросы: как заставить алгоритм работать на реальных данных, как не сломаться на масштабе и как объяснить результат бизнесу. Чтобы ответить на эти боли, мы готовим практические разборы сложных тем — без воды и с кодом. Участие бесплатное, записывайтесь:
1 сентября в 18:00 — Практические методы построения рекомендательных систем
15 сентября в 20:00 — Машина времени: как рекуррентные сети учатся помнить прошлое
Научиться работать с важнейшими моделями машинного обучения, NLP, DL, рекомендательными системами на практике с реальными данными можно на онлайн-курсе "Machine Learning. Professional".
Пройдите вступительный тест, чтобы оценить свой уровень и узнать, подойдет ли вам программа курса.