Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
bd0855c
Initial work on SparK
johnsutor Dec 5, 2024
42bd849
feat(spark): More work on spark
johnsutor Dec 17, 2024
7e73b83
Merge branch 'spark' of github.com:johnsutor/lightly
gabrielfruet Feb 5, 2026
724819c
refactor: should not nest modules.
gabrielfruet Feb 5, 2026
b0023f0
refactor: removed empty file
gabrielfruet Feb 5, 2026
8120990
refactor: adhered to correct directory structure.
gabrielfruet Feb 5, 2026
a7138d0
feat: put everything into the sparse spark module.
gabrielfruet Feb 5, 2026
9b869e9
refactor: removed redundant super calls with class.
gabrielfruet Feb 5, 2026
36cb94c
refactor: removed spark code.
gabrielfruet Feb 5, 2026
0afc280
refactor: removing redundant super calls
gabrielfruet Feb 5, 2026
280a479
refactor: remove empty file
gabrielfruet Feb 6, 2026
7e0f2bc
refactor: porting original code. starting from scratch
gabrielfruet Feb 6, 2026
9a9dff6
refactor: fixing type hint problems.
gabrielfruet Feb 6, 2026
407a4c0
refactor: removing unecessary redundant super
gabrielfruet Feb 6, 2026
47eecf3
fix: indentation
gabrielfruet Feb 6, 2026
7a956b3
feat: working module
gabrielfruet Feb 6, 2026
9d0f5c4
refactor: using library already implemente masking
gabrielfruet Feb 9, 2026
4aa69f7
feat: using patchify
gabrielfruet Feb 9, 2026
8b95a51
refactor: putting densification into a single module
gabrielfruet Feb 9, 2026
014f724
typo: raito -> ratio
gabrielfruet Feb 9, 2026
de01b0a
feat: encapsulated logic to single dnesifier module
gabrielfruet Feb 9, 2026
445bda7
refactor: cleaning code.
gabrielfruet Feb 9, 2026
741d531
refactor: letting sparse encoder be repsonsible for sizes and etc
gabrielfruet Feb 9, 2026
fb5c90d
feat: resnet18
gabrielfruet Feb 9, 2026
deeb4ef
refactor: removing unused code
gabrielfruet Feb 9, 2026
64df2e3
fix: bool tensor is inconvenient
gabrielfruet Feb 9, 2026
45b6eb8
refactor: documenting
gabrielfruet Feb 9, 2026
fb7903a
refactor: masking as a module
gabrielfruet Feb 9, 2026
138380f
refactor: removing unused variables
gabrielfruet Feb 9, 2026
6b3dbdf
refactor: removing unecessary module dependency
gabrielfruet Feb 9, 2026
ed8691a
refactor: loss as module
gabrielfruet Feb 9, 2026
3dcd5c3
refactor: spark visualization decoding logic as module
gabrielfruet Feb 9, 2026
1c316da
refactor: remove unused
gabrielfruet Feb 9, 2026
3ba1839
refactor
gabrielfruet Feb 9, 2026
70d32dd
refactor: removed big module and refactored timm funcs
gabrielfruet Feb 9, 2026
2f73d15
feat: example script
gabrielfruet Feb 9, 2026
b48d4eb
refactor: removed unused code and added opyrights
gabrielfruet Feb 9, 2026
25687d3
doc: improved documentation and type hinting
gabrielfruet Feb 12, 2026
96739fe
fix: type hinting
gabrielfruet Feb 12, 2026
ed70f9a
tests: testing active ex
gabrielfruet Feb 13, 2026
4fb16d7
tests: sp conv forward test
gabrielfruet Feb 13, 2026
6188aa4
refactor: using fixture instead of context manager
gabrielfruet Feb 13, 2026
7cfc508
format: formatting
gabrielfruet Feb 13, 2026
0098425
feat: moved patch recon loss to loss module
gabrielfruet Feb 14, 2026
dea0173
fix: no need of this sparse argument since its always sparse.
gabrielfruet Feb 14, 2026
83bdd60
format
gabrielfruet Feb 14, 2026
4b7f119
feat: init module access
gabrielfruet Feb 14, 2026
86386b7
feat: removed sparse encoder since it adds no necessary logic.
gabrielfruet Feb 14, 2026
92ebd99
refactor: removing dense model to sparse
gabrielfruet Feb 14, 2026
93c7ee5
fix: renamin to unet decoder
gabrielfruet Feb 14, 2026
2710f6b
format
gabrielfruet Feb 14, 2026
3a59d6a
fix: removed inplace operation
gabrielfruet Feb 24, 2026
34ed5f6
fix: using proper eps
gabrielfruet Feb 24, 2026
2ff68e9
docs: rst for loss
gabrielfruet Feb 24, 2026
de92784
feat: annotations
gabrielfruet Feb 24, 2026
ad4c9ac
typos
gabrielfruet Feb 24, 2026
8b2eca5
typing issues
liopeer Mar 2, 2026
b16c6b3
formatting
liopeer Mar 2, 2026
1b9f53a
context manager for global tensor + typing
liopeer Mar 2, 2026
4e9cde1
format
gabrielfruet Mar 2, 2026
25e7fbb
Merge branch 'feat/1462-spark-implementation' of github.com:gabrielfr…
gabrielfruet Mar 2, 2026
6abe8fc
refactor: better naming
gabrielfruet Mar 2, 2026
c9c99d2
refactor: lenght, not levels
gabrielfruet Mar 2, 2026
73da36e
fix: remove unecessary target transform
gabrielfruet Mar 2, 2026
708a18e
test: testing loss, comparing with reference and distributed testing
gabrielfruet Mar 2, 2026
0c10ea0
doc: where i took the loss from
gabrielfruet Mar 2, 2026
bbe0b07
refactor: make example simpler
gabrielfruet Mar 2, 2026
bde5292
typo: spark masking output
gabrielfruet Mar 4, 2026
73b0bb1
fix: imports according to #1895
gabrielfruet Mar 4, 2026
86ea8a9
fix: format
gabrielfruet Mar 4, 2026
edbfdcf
fix: tests now uses context manager for sparse mask
gabrielfruet Mar 4, 2026
e28391a
fix: using Tensor instead of torch.Tensor
gabrielfruet Mar 7, 2026
a1f82a0
doc: referencing original author
gabrielfruet Mar 7, 2026
9ce0f80
test: spark masking test
gabrielfruet Mar 7, 2026
e66d84a
doc: removed unecessary comment
gabrielfruet Mar 7, 2026
86934d2
feat: testing densify block
gabrielfruet Mar 7, 2026
a984e0d
fix: removed unecessary none handling
gabrielfruet Mar 7, 2026
09103d6
fix(test): sparse spark test with incorrect shapes
gabrielfruet Apr 1, 2026
e43d7a3
test: spark densifier
gabrielfruet Apr 1, 2026
ef88d62
format
gabrielfruet Apr 2, 2026
5e8f28e
feat: notebook example
gabrielfruet Apr 2, 2026
441e608
format and mypy
gabrielfruet Apr 2, 2026
f21faaa
fix: skipping tests on minimal and scoped imports
gabrielfruet Apr 3, 2026
3e962a8
fix: missing trunc_normal_ import
gabrielfruet Apr 3, 2026
4e5e067
Fix Python 3.7 type hints compatibility in sparse_spark tests
gabrielfruet Apr 3, 2026
e5f0b04
format
gabrielfruet Apr 7, 2026
9687035
doc: removed copyright docstring, we already reference authoer on the…
gabrielfruet Apr 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/source/lightly.loss.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ lightly.loss
.. autoclass:: lightly.loss.regularizer.co2.CO2Regularizer
:members:

.. autoclass:: lightly.loss.sparse_spark.SparKPatchReconLoss
:members:

.. autoclass:: lightly.loss.swav_loss.SwaVLoss
:members:

Expand Down
302 changes: 302 additions & 0 deletions examples/notebooks/pytorch_lightning/spark.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "0",
"metadata": {},
"source": [
"This example requires the following dependencies to be installed:\n",
"pip install \"lightly[timm]\""
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1",
"metadata": {},
"outputs": [],
"source": [
"!pip install \"lightly[timm]\""
]
},
{
"cell_type": "markdown",
"id": "2",
"metadata": {},
"source": [
"Note: The model and training settings do not follow the reference settings\n",
"from the paper. The settings are chosen such that the example can easily be\n",
"run on a small dataset with a single GPU."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3",
"metadata": {},
"outputs": [],
"source": [
"import pytorch_lightning as pl\n",
"import timm\n",
"import torch\n",
"import torchvision\n",
"from pytorch_lightning import LightningModule\n",
"from torch import Tensor\n",
"from torch.nn import Module\n",
"from torchvision.transforms import v2"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4",
"metadata": {},
"outputs": [],
"source": [
"import lightly.models.utils as model_utils\n",
"from lightly.loss.sparse_spark import SparKPatchReconLoss\n",
"from lightly.models.modules import sparse_spark"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5",
"metadata": {},
"outputs": [],
"source": [
"## The global projection head is the same as the Barlow Twins one\n",
"from lightly.models.modules.sparse_spark import (\n",
" SparKDensifier,\n",
" SparKMasker,\n",
" SparKMaskingOutput,\n",
" SparKOutputDecoder,\n",
" UNetDecoder,\n",
" sparse_layer_context,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6",
"metadata": {},
"outputs": [],
"source": [
"def _get_downsample_ratio_from_timm_model(model: Module) -> int:\n",
" if not hasattr(model, \"feature_info\"):\n",
" raise ValueError(\n",
" \"The provided model does not have the required 'feature_info' attribute.\"\n",
" )\n",
" return model.feature_info[-1][\"reduction\"]"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7",
"metadata": {},
"outputs": [],
"source": [
"def _get_enc_feat_map_chs_from_timm_model(model: Module) -> list[int]:\n",
" if not hasattr(model, \"feature_info\"):\n",
" raise ValueError(\n",
" \"The provided model does not have the required 'feature_info' attribute.\"\n",
" )\n",
" return [fi[\"num_chs\"] for fi in model.feature_info]"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8",
"metadata": {},
"outputs": [],
"source": [
"class SparseSparK(LightningModule):\n",
" def __init__(\n",
" self,\n",
" input_size: int = 416,\n",
" mask_ratio: float = 0.6,\n",
" densify_norm: str = \"bn\",\n",
" sbn=False,\n",
" ):\n",
" super().__init__()\n",
" backbone = timm.create_model(\n",
" model_name=\"resnet18\", drop_path_rate=0.05, features_only=True\n",
" )\n",
" downsample_ratio = _get_downsample_ratio_from_timm_model(backbone)\n",
" enc_feat_map_chs = _get_enc_feat_map_chs_from_timm_model(backbone)\n",
" self.sparse_encoder = sparse_spark.dense_model_to_sparse(\n",
" m=backbone, sbn=sbn, verbose=True\n",
" )\n",
" self.fmap_h = input_size // downsample_ratio\n",
" self.fmap_w = input_size // downsample_ratio\n",
" self.dense_decoder = UNetDecoder(\n",
" up_sample_ratio=downsample_ratio,\n",
" width=enc_feat_map_chs[-1],\n",
" )\n",
" self.masker = SparKMasker(\n",
" feature_map_size=(self.fmap_h, self.fmap_w),\n",
" downsample_ratio=downsample_ratio,\n",
" mask_ratio=mask_ratio,\n",
" )\n",
" self.densifier = SparKDensifier(\n",
" encoder_in_channels=enc_feat_map_chs,\n",
" decoder_in_channel=self.dense_decoder.width,\n",
" densify_norm_str=densify_norm.lower(),\n",
" sbn=sbn,\n",
" )\n",
" self.downsample_ratio = downsample_ratio\n",
" # loss module for patch reconstruction\n",
" self.recon_loss_fn = SparKPatchReconLoss()\n",
" # output decoder for visualization (pass minimal spatial props)\n",
" self.output_decoder = SparKOutputDecoder(\n",
" fmap_h=self.fmap_h,\n",
" fmap_w=self.fmap_w,\n",
" downsample_ratio=downsample_ratio,\n",
" )\n",
"\n",
" def forward(\n",
" self,\n",
" inp_bchw: Tensor,\n",
" vis=False,\n",
" ):\n",
" # step1. Mask\n",
" mask_out: SparKMaskingOutput = self.masker(inp_bchw)\n",
" masked_bchw, per_level_mask = mask_out\n",
" active_b1fHfW = per_level_mask[0]\n",
" active_b1hw = per_level_mask[-1]\n",
" # step2. Encode: get hierarchical encoded sparse features (a list containing 4 feature maps at 4 scales)\n",
" # Use sparse_layer_context to provide the mask to the sparse encoder and densifier.\n",
" with sparse_layer_context(active_mask=active_b1fHfW):\n",
" fea_bcffs: list[Tensor] = self.sparse_encoder(masked_bchw)\n",
" # step3. Densify: get hierarchical dense features for decoding\n",
" to_dec = self.densifier(fea_bcffs)\n",
" # step4. Decode and reconstruct\n",
" rec_bchw = self.dense_decoder(to_dec)\n",
" inp, rec = (\n",
" model_utils.patchify(inp_bchw, self.downsample_ratio),\n",
" model_utils.patchify(rec_bchw, self.downsample_ratio),\n",
" ) # inp and rec: (B, L = f*f, N = C*downsample_ratio**2)\n",
"\n",
" recon_loss, mean, var = self.recon_loss_fn(\n",
" inp_patches=inp, rec_patches=rec, active_mask=active_b1fHfW\n",
" )\n",
"\n",
" if vis:\n",
" return self.output_decoder(\n",
" rec_patches=rec,\n",
" mean=mean,\n",
" var=var,\n",
" inp_bchw=inp_bchw,\n",
" active_mask_full=active_b1hw,\n",
" )\n",
" else:\n",
" return recon_loss\n",
"\n",
" def training_step(self, batch: tuple[Tensor, Tensor], batch_index: int) -> Tensor:\n",
" img, _ = batch\n",
" recon_loss = self.forward(img)\n",
" # Log the training loss to logger and progress bar (per-step and per-epoch)\n",
" self.log(\n",
" \"train_loss\",\n",
" recon_loss,\n",
" on_step=True,\n",
" on_epoch=True,\n",
" prog_bar=True,\n",
" logger=True,\n",
" )\n",
" return recon_loss\n",
"\n",
" def configure_optimizers(self):\n",
" return torch.optim.SGD(\n",
" self.parameters(), lr=0.03, momentum=0.9, weight_decay=1e-4\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9",
"metadata": {},
"outputs": [],
"source": [
"model = SparseSparK(input_size=416)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "10",
"metadata": {},
"outputs": [],
"source": [
"dataset = torchvision.datasets.Caltech101(\n",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The download of Caltech101 currently does not seem to work for me. If it worked for you, ignore this comment – might open an issue with torchvision after checking if it has something to do with my local setup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that torchvision is deprecating some datasets. So, some links are broken pytorch/vision#9335

" \"datasets/caltech101\",\n",
" download=True,\n",
" transform=v2.Compose(\n",
" [\n",
" v2.Resize((416, 416)),\n",
" v2.RGB(),\n",
" v2.ToTensor(),\n",
" v2.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n",
" ]\n",
" ),\n",
")\n",
"# or create a dataset from a folder containing images or videos:\n",
"# dataset = LightlyDataset(\"path/to/folder\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "11",
"metadata": {},
"outputs": [],
"source": [
"dataloader = torch.utils.data.DataLoader(\n",
" dataset,\n",
" batch_size=4,\n",
" shuffle=True,\n",
" drop_last=True,\n",
" num_workers=8,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "12",
"metadata": {},
"outputs": [],
"source": [
"trainer = pl.Trainer(\n",
" max_epochs=30,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "13",
"metadata": {},
"outputs": [],
"source": [
"trainer.fit(\n",
" model=model,\n",
" train_dataloaders=dataloader,\n",
")"
]
}
],
"metadata": {
"jupytext": {
"cell_metadata_filter": "-all",
"main_language": "python",
"notebook_metadata_filter": "-all"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading
Loading