icalens 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- icalens-0.1.0/.github/workflows/release.yml +32 -0
- icalens-0.1.0/.gitignore +12 -0
- icalens-0.1.0/LICENSE +21 -0
- icalens-0.1.0/PKG-INFO +74 -0
- icalens-0.1.0/README.md +51 -0
- icalens-0.1.0/THIRD_PARTY_NOTICES.md +23 -0
- icalens-0.1.0/demo/README.md +95 -0
- icalens-0.1.0/demo/apply.py +92 -0
- icalens-0.1.0/demo/fit.py +317 -0
- icalens-0.1.0/docs/api.md +125 -0
- icalens-0.1.0/docs/artifact-format.md +145 -0
- icalens-0.1.0/docs/naming.md +32 -0
- icalens-0.1.0/pyproject.toml +63 -0
- icalens-0.1.0/src/icalens/__init__.py +7 -0
- icalens-0.1.0/src/icalens/_arrays.py +52 -0
- icalens-0.1.0/src/icalens/_artifact.py +186 -0
- icalens-0.1.0/src/icalens/_fastica.py +420 -0
- icalens-0.1.0/src/icalens/exceptions.py +13 -0
- icalens-0.1.0/src/icalens/lens.py +412 -0
- icalens-0.1.0/src/icalens/py.typed +1 -0
- icalens-0.1.0/tests/conftest.py +19 -0
- icalens-0.1.0/tests/test_artifacts.py +56 -0
- icalens-0.1.0/tests/test_lens.py +100 -0
- icalens-0.1.0/tests/test_public_api.py +8 -0
- icalens-0.1.0/uv.lock +3133 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
name: Build and publish
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
environment:
|
|
15
|
+
name: pypi
|
|
16
|
+
url: https://pypi.org/p/icalens
|
|
17
|
+
permissions:
|
|
18
|
+
id-token: write
|
|
19
|
+
steps:
|
|
20
|
+
- name: Check out repository
|
|
21
|
+
uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Install uv
|
|
24
|
+
uses: astral-sh/setup-uv@v7
|
|
25
|
+
with:
|
|
26
|
+
enable-cache: true
|
|
27
|
+
|
|
28
|
+
- name: Build distributions
|
|
29
|
+
run: uv build
|
|
30
|
+
|
|
31
|
+
- name: Publish distributions to PyPI
|
|
32
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
icalens-0.1.0/.gitignore
ADDED
icalens-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sida Liu and Feijiang Han
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
icalens-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: icalens
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fit, share, and apply ICA lenses for language-model activations.
|
|
5
|
+
Project-URL: Homepage, https://liusida.github.io/ica-lens-paper/
|
|
6
|
+
Project-URL: Repository, https://github.com/liusida/icalens
|
|
7
|
+
Author: Sida Liu, Feijiang Han
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
License-File: THIRD_PARTY_NOTICES.md
|
|
11
|
+
Keywords: ICA,activations,interpretability,language-models
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: huggingface-hub>=0.25
|
|
18
|
+
Requires-Dist: numpy>=1.24
|
|
19
|
+
Requires-Dist: safetensors>=0.4
|
|
20
|
+
Requires-Dist: torch>=2.1
|
|
21
|
+
Requires-Dist: tqdm>=4.66
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# ICA Lens
|
|
25
|
+
|
|
26
|
+
ICA Lens fits, shares, and applies Independent Component Analysis bases for
|
|
27
|
+
language-model activations. Version 0.1 operates on activations supplied by the
|
|
28
|
+
caller; it does not load language models or capture activations.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv add icalens
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Load a published lens:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from icalens import ICALens
|
|
38
|
+
|
|
39
|
+
lens = ICALens.from_pretrained("liusida/icalens-gpt2-small")
|
|
40
|
+
scores = lens.transform(activations, layer=6)
|
|
41
|
+
reconstructed = lens.inverse_transform(scores, layer=6)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Fit and publish your own:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from icalens import ICALens
|
|
48
|
+
|
|
49
|
+
lens = ICALens(
|
|
50
|
+
base_model="openai-community/gpt2",
|
|
51
|
+
base_model_revision="FULL_COMMIT_HASH",
|
|
52
|
+
activation_site="resid_post",
|
|
53
|
+
)
|
|
54
|
+
lens.fit(activations, layer=6, random_state=0)
|
|
55
|
+
lens.save("./my-icalens")
|
|
56
|
+
lens.push_to_hub("username/icalens-gpt2-small")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Inputs may be NumPy arrays or PyTorch tensors. Leading dimensions are treated
|
|
60
|
+
as sample dimensions and the final dimension must be the model hidden size.
|
|
61
|
+
Fitting uses ICA Lens's built-in PyTorch FastICA implementation and can run on
|
|
62
|
+
the input tensor's device. NumPy inputs are fitted on CPU. ICA Lens does not
|
|
63
|
+
depend on scikit-learn or SciPy.
|
|
64
|
+
|
|
65
|
+
See [`docs/api.md`](docs/api.md) and
|
|
66
|
+
[`docs/artifact-format.md`](docs/artifact-format.md) for the initial API and
|
|
67
|
+
portable artifact format.
|
|
68
|
+
|
|
69
|
+
For the 1,000-token GPT-2/Pile-10k fitting demo, run:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
uv sync
|
|
73
|
+
uv run python demo/fit.py
|
|
74
|
+
```
|
icalens-0.1.0/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# ICA Lens
|
|
2
|
+
|
|
3
|
+
ICA Lens fits, shares, and applies Independent Component Analysis bases for
|
|
4
|
+
language-model activations. Version 0.1 operates on activations supplied by the
|
|
5
|
+
caller; it does not load language models or capture activations.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv add icalens
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Load a published lens:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from icalens import ICALens
|
|
15
|
+
|
|
16
|
+
lens = ICALens.from_pretrained("liusida/icalens-gpt2-small")
|
|
17
|
+
scores = lens.transform(activations, layer=6)
|
|
18
|
+
reconstructed = lens.inverse_transform(scores, layer=6)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Fit and publish your own:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from icalens import ICALens
|
|
25
|
+
|
|
26
|
+
lens = ICALens(
|
|
27
|
+
base_model="openai-community/gpt2",
|
|
28
|
+
base_model_revision="FULL_COMMIT_HASH",
|
|
29
|
+
activation_site="resid_post",
|
|
30
|
+
)
|
|
31
|
+
lens.fit(activations, layer=6, random_state=0)
|
|
32
|
+
lens.save("./my-icalens")
|
|
33
|
+
lens.push_to_hub("username/icalens-gpt2-small")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Inputs may be NumPy arrays or PyTorch tensors. Leading dimensions are treated
|
|
37
|
+
as sample dimensions and the final dimension must be the model hidden size.
|
|
38
|
+
Fitting uses ICA Lens's built-in PyTorch FastICA implementation and can run on
|
|
39
|
+
the input tensor's device. NumPy inputs are fitted on CPU. ICA Lens does not
|
|
40
|
+
depend on scikit-learn or SciPy.
|
|
41
|
+
|
|
42
|
+
See [`docs/api.md`](docs/api.md) and
|
|
43
|
+
[`docs/artifact-format.md`](docs/artifact-format.md) for the initial API and
|
|
44
|
+
portable artifact format.
|
|
45
|
+
|
|
46
|
+
For the 1,000-token GPT-2/Pile-10k fitting demo, run:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv sync
|
|
50
|
+
uv run python demo/fit.py
|
|
51
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
The internal FastICA implementation in `src/icalens/_fastica.py` is adapted
|
|
4
|
+
from [FastICA_torch](https://github.com/liusida/FastICA_torch), copyright 2024
|
|
5
|
+
Richard Hakim, under the MIT License:
|
|
6
|
+
|
|
7
|
+
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
> of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
> in the Software without restriction, including without limitation the rights
|
|
10
|
+
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
> copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
> furnished to do so, subject to the following conditions:
|
|
13
|
+
>
|
|
14
|
+
> The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
> copies or substantial portions of the Software.
|
|
16
|
+
>
|
|
17
|
+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
> SOFTWARE.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# ICA Lens fitting demo
|
|
2
|
+
|
|
3
|
+
Install the development dependencies and fit a lens from 1,000 Pile-10k tokens:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
uv sync
|
|
7
|
+
uv run python demo/fit.py
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
By default, the demo:
|
|
11
|
+
|
|
12
|
+
1. Streams text from `NeelNanda/pile-10k`.
|
|
13
|
+
2. Builds a 1,000-token candidate pool from independently tokenized documents.
|
|
14
|
+
3. Uses all 1,000 positions for fitting by default.
|
|
15
|
+
4. Preserves each sampled token's original left context during activation capture.
|
|
16
|
+
5. Loads `openai-community/gpt2` on CUDA through `gb10-load-llm`.
|
|
17
|
+
6. Captures `outputs.hidden_states[7]`, recorded as ICA Lens layer 6 after
|
|
18
|
+
excluding the initial embedding state.
|
|
19
|
+
7. Fits a full 768-component ICA Lens.
|
|
20
|
+
8. Saves it under `demo/output/icalens-gpt2-small-1k`.
|
|
21
|
+
|
|
22
|
+
By default, `--candidate-tokens` equals `--token-budget`. Set it explicitly to
|
|
23
|
+
sample the fitting tokens from a larger pool:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv run python demo/fit.py \
|
|
27
|
+
--candidate-tokens 10000 \
|
|
28
|
+
--token-budget 1000 \
|
|
29
|
+
--max-iter 1000 \
|
|
30
|
+
--seed 0
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`--max-iter` controls the fixed number of FastICA fixed-point iterations.
|
|
34
|
+
The fitting demo displays a tqdm bar with the current convergence limit and
|
|
35
|
+
mean contrast objective (`obj`). ICA Lens deliberately runs exactly
|
|
36
|
+
`--max-iter` iterations: the classical FastICA tolerance criterion is treated
|
|
37
|
+
as diagnostic-only because it is not an appropriate stopping rule in the LLM
|
|
38
|
+
activation regime. The displayed objective uses the selected FastICA contrast:
|
|
39
|
+
`log(cosh(x))`, `-exp(-x²/2)`, or `x⁴/4`.
|
|
40
|
+
|
|
41
|
+
The demo also displays token-rate progress bars while building the Pile-10k
|
|
42
|
+
candidate pool and capturing the sampled GPT-2 activations.
|
|
43
|
+
|
|
44
|
+
To test whether a fit stays within an ordinary 16 GiB GPU budget, cap the
|
|
45
|
+
PyTorch CUDA allocator:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv run python demo/fit.py \
|
|
49
|
+
--layers 6 \
|
|
50
|
+
--token-budget 1000000 \
|
|
51
|
+
--fit-batch-size 8192 \
|
|
52
|
+
--max-vram-gb 16
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The script reports peak PyTorch CUDA memory at the end. This cap covers
|
|
56
|
+
allocations managed by PyTorch, but not CUDA context memory or allocations made
|
|
57
|
+
directly by non-PyTorch libraries, so it is a close OOM test rather than a full
|
|
58
|
+
hardware emulator.
|
|
59
|
+
|
|
60
|
+
Selected activations are retained in CPU memory. Whitening statistics,
|
|
61
|
+
FastICA updates, and final source scaling are computed in repeated CUDA batches,
|
|
62
|
+
so GPU memory scales with `--fit-batch-size` rather than the total token count.
|
|
63
|
+
|
|
64
|
+
Fit several layers with a comma-separated list:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uv run python demo/fit.py --layers 0,6,11
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Use `--layers all` to fit all 12 GPT-2 transformer blocks. The default single
|
|
71
|
+
layer is intended as a quick end-to-end check; a full published artifact should
|
|
72
|
+
use a larger, explicitly sampled token corpus.
|
|
73
|
+
|
|
74
|
+
The demo requires network access for Hugging Face downloads and a CUDA device.
|
|
75
|
+
|
|
76
|
+
## Apply the saved lens
|
|
77
|
+
|
|
78
|
+
After fitting layer 6, apply it to fresh text:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uv run python demo/apply.py
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The script loads the local artifact, captures GPT-2 activations from the exact
|
|
85
|
+
base-model revision recorded in its manifest, and prints the largest signed ICA
|
|
86
|
+
component scores at each token.
|
|
87
|
+
|
|
88
|
+
Use custom text or another saved artifact with:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
uv run python demo/apply.py \
|
|
92
|
+
--lens demo/output/icalens-gpt2-small-1k \
|
|
93
|
+
--layer 6 \
|
|
94
|
+
--text "The boat reached the bank before sunset."
|
|
95
|
+
```
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Apply a saved ICA Lens to fresh GPT-2 text."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import torch
|
|
9
|
+
from gb10_load_llm import load_model_to_cuda
|
|
10
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
11
|
+
|
|
12
|
+
from icalens import ICALens
|
|
13
|
+
|
|
14
|
+
DEFAULT_LENS = Path(__file__).parent / "output" / "icalens-gpt2-small"
|
|
15
|
+
DEFAULT_TEXT = "She deposited the check at the bank before walking along the river bank."
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_args() -> argparse.Namespace:
|
|
19
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
20
|
+
parser.add_argument("--lens", type=Path, default=DEFAULT_LENS)
|
|
21
|
+
parser.add_argument("--text", default=DEFAULT_TEXT)
|
|
22
|
+
parser.add_argument("--layer", type=int, default=6)
|
|
23
|
+
parser.add_argument("--top-k", type=int, default=5)
|
|
24
|
+
return parser.parse_args()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> None:
|
|
28
|
+
args = parse_args()
|
|
29
|
+
if not torch.cuda.is_available():
|
|
30
|
+
raise RuntimeError("This GB10 demo requires a CUDA device.")
|
|
31
|
+
if args.top_k <= 0:
|
|
32
|
+
raise ValueError("--top-k must be positive")
|
|
33
|
+
|
|
34
|
+
lens = ICALens.from_pretrained(args.lens)
|
|
35
|
+
if args.layer not in lens.available_layers:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"layer {args.layer} is not in this lens; available layers: {lens.available_layers}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
tokenizer = AutoTokenizer.from_pretrained(
|
|
41
|
+
lens.base_model,
|
|
42
|
+
revision=lens.base_model_revision,
|
|
43
|
+
)
|
|
44
|
+
model = load_model_to_cuda(
|
|
45
|
+
AutoModelForCausalLM,
|
|
46
|
+
lens.base_model,
|
|
47
|
+
revision=lens.base_model_revision,
|
|
48
|
+
device="cuda",
|
|
49
|
+
dtype=torch.bfloat16,
|
|
50
|
+
touch="auto",
|
|
51
|
+
low_cpu_mem_usage=True,
|
|
52
|
+
)
|
|
53
|
+
model.eval()
|
|
54
|
+
|
|
55
|
+
encoded = tokenizer(
|
|
56
|
+
args.text,
|
|
57
|
+
return_tensors="pt",
|
|
58
|
+
truncation=True,
|
|
59
|
+
max_length=tokenizer.model_max_length,
|
|
60
|
+
)
|
|
61
|
+
input_ids = encoded["input_ids"].to("cuda")
|
|
62
|
+
attention_mask = encoded["attention_mask"].to("cuda")
|
|
63
|
+
with torch.inference_mode():
|
|
64
|
+
outputs = model(
|
|
65
|
+
input_ids=input_ids,
|
|
66
|
+
attention_mask=attention_mask,
|
|
67
|
+
output_hidden_states=True,
|
|
68
|
+
use_cache=False,
|
|
69
|
+
)
|
|
70
|
+
if outputs.hidden_states is None:
|
|
71
|
+
raise RuntimeError("GPT-2 did not return hidden states.")
|
|
72
|
+
|
|
73
|
+
activations = outputs.hidden_states[args.layer + 1][0].to(dtype=torch.float32)
|
|
74
|
+
scores = lens.transform(activations, layer=args.layer)
|
|
75
|
+
top_k = min(args.top_k, scores.shape[-1])
|
|
76
|
+
top_indices = torch.topk(scores.abs(), k=top_k, dim=-1).indices
|
|
77
|
+
tokens = tokenizer.convert_ids_to_tokens(input_ids[0])
|
|
78
|
+
|
|
79
|
+
print(f"Lens: {args.lens}")
|
|
80
|
+
print(f"Base model: {lens.base_model}@{lens.base_model_revision}")
|
|
81
|
+
print(f"Layer: {args.layer}")
|
|
82
|
+
print()
|
|
83
|
+
for position, token in enumerate(tokens):
|
|
84
|
+
entries = [
|
|
85
|
+
f"C{component.item()}={scores[position, component].item():+.3f}"
|
|
86
|
+
for component in top_indices[position]
|
|
87
|
+
]
|
|
88
|
+
print(f"{position:>3} {token!r:<18} {' '.join(entries)}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
main()
|