icalens 0.2.2__tar.gz → 0.3.0.dev1__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.
Files changed (54) hide show
  1. {icalens-0.2.2 → icalens-0.3.0.dev1}/.gitignore +3 -1
  2. icalens-0.3.0.dev1/.readthedocs.yaml +13 -0
  3. {icalens-0.2.2 → icalens-0.3.0.dev1}/PKG-INFO +36 -7
  4. {icalens-0.2.2 → icalens-0.3.0.dev1}/README.md +33 -5
  5. {icalens-0.2.2 → icalens-0.3.0.dev1}/demo/README.md +83 -11
  6. {icalens-0.2.2 → icalens-0.3.0.dev1}/demo/apply.py +19 -27
  7. icalens-0.3.0.dev1/demo/apply_chat.py +308 -0
  8. {icalens-0.2.2 → icalens-0.3.0.dev1}/demo/fit.py +15 -1
  9. {icalens-0.2.2 → icalens-0.3.0.dev1}/demo/fit_chat.py +18 -4
  10. icalens-0.3.0.dev1/demo/get_started.py +41 -0
  11. icalens-0.3.0.dev1/demo/plot_objective.py +138 -0
  12. icalens-0.3.0.dev1/docs/api.md +87 -0
  13. icalens-0.3.0.dev1/docs/fit-and-publish.md +67 -0
  14. icalens-0.3.0.dev1/docs/getting-started.md +61 -0
  15. icalens-0.3.0.dev1/docs/index.md +44 -0
  16. icalens-0.3.0.dev1/docs/requirements.txt +1 -0
  17. icalens-0.3.0.dev1/docs/scores-and-energy.md +47 -0
  18. icalens-0.3.0.dev1/docs/text-and-chat.md +63 -0
  19. icalens-0.3.0.dev1/mkdocs.yml +56 -0
  20. {icalens-0.2.2/docs → icalens-0.3.0.dev1/notes}/api.md +15 -1
  21. {icalens-0.2.2/docs → icalens-0.3.0.dev1/notes}/artifact-format.md +12 -1
  22. {icalens-0.2.2 → icalens-0.3.0.dev1}/pyproject.toml +8 -1
  23. {icalens-0.2.2 → icalens-0.3.0.dev1}/src/icalens/__init__.py +1 -1
  24. {icalens-0.2.2 → icalens-0.3.0.dev1}/src/icalens/_capture.py +22 -5
  25. {icalens-0.2.2 → icalens-0.3.0.dev1}/src/icalens/_fastica.py +174 -35
  26. {icalens-0.2.2 → icalens-0.3.0.dev1}/src/icalens/analysis.py +62 -9
  27. icalens-0.3.0.dev1/src/icalens/html.py +299 -0
  28. {icalens-0.2.2 → icalens-0.3.0.dev1}/src/icalens/lens.py +23 -3
  29. icalens-0.3.0.dev1/src/icalens/smoke_test.py +138 -0
  30. {icalens-0.2.2 → icalens-0.3.0.dev1}/tests/test_analysis.py +21 -0
  31. icalens-0.3.0.dev1/tests/test_capture.py +104 -0
  32. icalens-0.3.0.dev1/tests/test_html_explorer.py +108 -0
  33. {icalens-0.2.2 → icalens-0.3.0.dev1}/tests/test_lens.py +36 -0
  34. {icalens-0.2.2 → icalens-0.3.0.dev1}/tests/test_public_api.py +1 -1
  35. {icalens-0.2.2 → icalens-0.3.0.dev1}/uv.lock +201 -1
  36. icalens-0.2.2/demo/apply_chat.py +0 -184
  37. icalens-0.2.2/demo/html_explorer.py +0 -168
  38. icalens-0.2.2/tests/test_capture.py +0 -40
  39. icalens-0.2.2/tests/test_html_explorer.py +0 -29
  40. {icalens-0.2.2 → icalens-0.3.0.dev1}/.env.template +0 -0
  41. {icalens-0.2.2 → icalens-0.3.0.dev1}/.github/workflows/release.yml +0 -0
  42. {icalens-0.2.2 → icalens-0.3.0.dev1}/LICENSE +0 -0
  43. {icalens-0.2.2 → icalens-0.3.0.dev1}/THIRD_PARTY_NOTICES.md +0 -0
  44. {icalens-0.2.2 → icalens-0.3.0.dev1}/demo/__init__.py +0 -0
  45. {icalens-0.2.2 → icalens-0.3.0.dev1}/demo/publish.py +0 -0
  46. {icalens-0.2.2/docs → icalens-0.3.0.dev1/notes}/naming.md +0 -0
  47. {icalens-0.2.2 → icalens-0.3.0.dev1}/src/icalens/_arrays.py +0 -0
  48. {icalens-0.2.2 → icalens-0.3.0.dev1}/src/icalens/_artifact.py +0 -0
  49. {icalens-0.2.2 → icalens-0.3.0.dev1}/src/icalens/exceptions.py +0 -0
  50. {icalens-0.2.2 → icalens-0.3.0.dev1}/src/icalens/py.typed +0 -0
  51. {icalens-0.2.2 → icalens-0.3.0.dev1}/tests/conftest.py +0 -0
  52. {icalens-0.2.2 → icalens-0.3.0.dev1}/tests/test_artifacts.py +0 -0
  53. {icalens-0.2.2 → icalens-0.3.0.dev1}/tests/test_demo_fit.py +0 -0
  54. {icalens-0.2.2 → icalens-0.3.0.dev1}/tests/test_demo_fit_chat.py +0 -0
@@ -11,6 +11,8 @@ __pycache__/
11
11
  .ruff_cache/
12
12
  .coverage
13
13
  htmlcov/
14
+ site/
14
15
  demo/output/
15
16
  experiments/output/
16
- experiments/
17
+ experiments/
18
+ .worktrees/
@@ -0,0 +1,13 @@
1
+ version: 2
2
+
3
+ build:
4
+ os: ubuntu-24.04
5
+ tools:
6
+ python: "3"
7
+
8
+ python:
9
+ install:
10
+ - requirements: docs/requirements.txt
11
+
12
+ mkdocs:
13
+ configuration: mkdocs.yml
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: icalens
3
- Version: 0.2.2
3
+ Version: 0.3.0.dev1
4
4
  Summary: Fit, share, and apply ICA lenses for language-model activations.
5
5
  Project-URL: Homepage, https://liusida.github.io/ica-lens-paper/
6
+ Project-URL: Documentation, https://icalens.readthedocs.io/
6
7
  Project-URL: Repository, https://github.com/liusida/icalens
7
8
  Author: Sida Liu, Feijiang Han
8
9
  License-Expression: MIT
@@ -43,6 +44,7 @@ result = lens.analyze("She deposited the check at the bank.", layer=6)
43
44
 
44
45
  print(result.tokens)
45
46
  print(result.scores)
47
+ result.to_html("analysis.html")
46
48
  ```
47
49
 
48
50
  Fit and publish your own:
@@ -89,6 +91,8 @@ lens = ICALens(
89
91
  The standard `icalens` installation can capture and analyze text or completed
90
92
  chat conversations directly. `result = lens.analyze(text, layer=6)` returns aligned
91
93
  tokens, activations, signed scores, and per-token component energy shares.
94
+ `result.to_html("analysis.html")` writes a self-contained interactive explorer;
95
+ pass `metric="energy"` to visualize component energy shares instead of scores.
92
96
 
93
97
  Inputs may be NumPy arrays or PyTorch tensors. Leading dimensions are treated
94
98
  as sample dimensions and the final dimension must be the model hidden size.
@@ -96,9 +100,8 @@ Fitting uses ICA Lens's built-in PyTorch FastICA implementation and can run on
96
100
  the input tensor's device. NumPy inputs are fitted on CPU. ICA Lens does not
97
101
  depend on scikit-learn or SciPy.
98
102
 
99
- See [`docs/api.md`](docs/api.md) and
100
- [`docs/artifact-format.md`](docs/artifact-format.md) for the public API and
101
- portable artifact format.
103
+ See the [documentation](https://icalens.readthedocs.io/) for the complete text,
104
+ conversation, fitting, publishing, and HTML-export workflows.
102
105
 
103
106
  For the 1,000-token GPT-2/Pile-10k fitting demo, run:
104
107
 
@@ -107,8 +110,8 @@ uv sync
107
110
  uv run python demo/fit.py
108
111
  ```
109
112
 
110
- For the corresponding instruct-model demo using assistant tokens from
111
- UltraChat conversations, run:
113
+ For the corresponding instruct-model demo using all formatted UltraChat
114
+ conversation tokens, including template markers, run:
112
115
 
113
116
  ```bash
114
117
  uv run python demo/fit_chat.py --layers 12
@@ -122,3 +125,29 @@ uv run python demo/apply_chat.py
122
125
 
123
126
  Both `apply.py` and `apply_chat.py` also write standalone interactive HTML
124
127
  explorers under `demo/output/`; pass `--output-file` to choose another path.
128
+
129
+ ## Installed-package smoke test
130
+
131
+ After installing a wheel or PyPI release in a clean project, run the bundled
132
+ end-to-end check:
133
+
134
+ ```bash
135
+ uv run icalens-smoke-test
136
+ ```
137
+
138
+ By default, the suite checks both public input paths: raw text through the
139
+ published GPT-2 lens and a formatted conversation through the published
140
+ Qwen3.5-2B instruct lens. Each case lazily downloads one ICA layer, verifies
141
+ finite scores and normalized energy, checks reconstruction shape, and writes
142
+ `icalens-smoke-text.html` or `icalens-smoke-chat.html`.
143
+
144
+ Run only one path when iterating locally:
145
+
146
+ ```bash
147
+ uv run icalens-smoke-test text
148
+ uv run icalens-smoke-test chat
149
+ ```
150
+
151
+ Use `--text-lens`, `--text-layer`, `--text-input`, `--chat-lens`,
152
+ `--chat-layer`, `--chat-input`, `--chat-response`, `--device`, and
153
+ `--output-dir` to override the defaults.
@@ -18,6 +18,7 @@ result = lens.analyze("She deposited the check at the bank.", layer=6)
18
18
 
19
19
  print(result.tokens)
20
20
  print(result.scores)
21
+ result.to_html("analysis.html")
21
22
  ```
22
23
 
23
24
  Fit and publish your own:
@@ -64,6 +65,8 @@ lens = ICALens(
64
65
  The standard `icalens` installation can capture and analyze text or completed
65
66
  chat conversations directly. `result = lens.analyze(text, layer=6)` returns aligned
66
67
  tokens, activations, signed scores, and per-token component energy shares.
68
+ `result.to_html("analysis.html")` writes a self-contained interactive explorer;
69
+ pass `metric="energy"` to visualize component energy shares instead of scores.
67
70
 
68
71
  Inputs may be NumPy arrays or PyTorch tensors. Leading dimensions are treated
69
72
  as sample dimensions and the final dimension must be the model hidden size.
@@ -71,9 +74,8 @@ Fitting uses ICA Lens's built-in PyTorch FastICA implementation and can run on
71
74
  the input tensor's device. NumPy inputs are fitted on CPU. ICA Lens does not
72
75
  depend on scikit-learn or SciPy.
73
76
 
74
- See [`docs/api.md`](docs/api.md) and
75
- [`docs/artifact-format.md`](docs/artifact-format.md) for the public API and
76
- portable artifact format.
77
+ See the [documentation](https://icalens.readthedocs.io/) for the complete text,
78
+ conversation, fitting, publishing, and HTML-export workflows.
77
79
 
78
80
  For the 1,000-token GPT-2/Pile-10k fitting demo, run:
79
81
 
@@ -82,8 +84,8 @@ uv sync
82
84
  uv run python demo/fit.py
83
85
  ```
84
86
 
85
- For the corresponding instruct-model demo using assistant tokens from
86
- UltraChat conversations, run:
87
+ For the corresponding instruct-model demo using all formatted UltraChat
88
+ conversation tokens, including template markers, run:
87
89
 
88
90
  ```bash
89
91
  uv run python demo/fit_chat.py --layers 12
@@ -97,3 +99,29 @@ uv run python demo/apply_chat.py
97
99
 
98
100
  Both `apply.py` and `apply_chat.py` also write standalone interactive HTML
99
101
  explorers under `demo/output/`; pass `--output-file` to choose another path.
102
+
103
+ ## Installed-package smoke test
104
+
105
+ After installing a wheel or PyPI release in a clean project, run the bundled
106
+ end-to-end check:
107
+
108
+ ```bash
109
+ uv run icalens-smoke-test
110
+ ```
111
+
112
+ By default, the suite checks both public input paths: raw text through the
113
+ published GPT-2 lens and a formatted conversation through the published
114
+ Qwen3.5-2B instruct lens. Each case lazily downloads one ICA layer, verifies
115
+ finite scores and normalized energy, checks reconstruction shape, and writes
116
+ `icalens-smoke-text.html` or `icalens-smoke-chat.html`.
117
+
118
+ Run only one path when iterating locally:
119
+
120
+ ```bash
121
+ uv run icalens-smoke-test text
122
+ uv run icalens-smoke-test chat
123
+ ```
124
+
125
+ Use `--text-lens`, `--text-layer`, `--text-input`, `--chat-lens`,
126
+ `--chat-layer`, `--chat-input`, `--chat-response`, `--device`, and
127
+ `--output-dir` to override the defaults.
@@ -38,6 +38,32 @@ as diagnostic-only because it is not an appropriate stopping rule in the LLM
38
38
  activation regime. The displayed objective uses the selected FastICA contrast:
39
39
  `log(cosh(x))`, `-exp(-x²/2)`, or `x⁴/4`.
40
40
 
41
+ For parallel FastICA, every saved layer also records an objective curve in
42
+ `icalens.json` under `layers.<layer>.fitting.objective_history`. At each
43
+ recorded iteration, the contrast is averaged over fitting tokens for each
44
+ component, then summarized across components at the minimum, 10th, 20th, ...,
45
+ 90th percentile, and maximum. Its `iterations`, `percentiles`, and `values`
46
+ arrays can be plotted directly as percentile curves or nested colored bands.
47
+ Use `--objective-every N` to record every Nth iteration; the final iteration is
48
+ always included. The default is `1`.
49
+
50
+ After the fixed-point iterations, ICA Lens makes one final blockwise objective
51
+ pass and renumbers components by descending absolute contrast deviation from
52
+ the standard-Gaussian baseline. `C0` is therefore the strongest non-Gaussian
53
+ component within that layer. The manifest retains every component's raw
54
+ objective and baseline-relative strength.
55
+
56
+ Plot the first four available layers from a local, progressively written lens:
57
+
58
+ ```bash
59
+ uv run python demo/plot_objective.py \
60
+ demo/output/icalens-qwen3.5-2b-ultrachat-10m
61
+ ```
62
+
63
+ Use `--layers 0,1,2`, `--first 6`, or `--output path/to/curves.png` to customize
64
+ the selection and output. Nested colored bands show min–max, p10–p90, ...,
65
+ p40–p60, with the median drawn as a line.
66
+
41
67
  The demo also displays token-rate progress bars while building the Pile-10k
42
68
  candidate pool and capturing the sampled GPT-2 activations.
43
69
 
@@ -92,9 +118,16 @@ uv run python demo/fit.py \
92
118
 
93
119
  This captures layers 0–1, fits them, releases their activations, and then
94
120
  repeats for layers 2–3. Smaller groups use less memory but require more complete
95
- passes over the tokenized dataset. The default `0` captures all requested
121
+ passes over the tokenized dataset. Each capture pass stops immediately after
122
+ its highest requested transformer block, so an early-layer group does not run
123
+ the unused remainder of the model. The default `0` captures all requested
96
124
  layers in one pass.
97
125
 
126
+ After each layer finishes fitting, the demos atomically checkpoint the growing
127
+ lens to `--output`, including that layer's objective history. If a later layer
128
+ fails or the run is interrupted, every previously completed layer remains
129
+ loadable from the output directory.
130
+
98
131
  The demo requires network access for Hugging Face downloads and a CUDA device.
99
132
 
100
133
  Use every token available under the demo's per-document context limit without
@@ -112,19 +145,34 @@ reserved memory at the end.
112
145
 
113
146
  ## Fit an instruct-model lens from conversations
114
147
 
115
- Fit a Qwen2.5-0.5B-Instruct lens on assistant-content tokens from streamed
148
+ Fit a Qwen2.5-0.5B-Instruct lens on all formatted tokens from streamed
116
149
  UltraChat 200k conversations:
117
150
 
118
151
  ```bash
119
152
  uv run python demo/fit_chat.py --layers 12
120
153
  ```
121
154
 
155
+ Qwen3.5 multimodal checkpoints can be fitted as text-only language models. The
156
+ loader selects the language backbone, while chat templating and assistant-token
157
+ selection use the same interface:
158
+
159
+ ```bash
160
+ uv run python demo/fit_chat.py \
161
+ --model Qwen/Qwen3.5-2B \
162
+ --layers 12 \
163
+ --token-budget 100000 \
164
+ --max-iter 20 \
165
+ --output demo/output/icalens-qwen3.5-2b-ultrachat-100k
166
+ ```
167
+
168
+ Qwen3.5-2B has 24 language layers indexed from 0 through 23 and hidden size
169
+ 2048. Use at least 2049 fitting tokens for a full-component lens.
170
+
122
171
  The script uses the tokenizer's chat template and offset mapping to distinguish
123
172
  message content from role markers and other template control tokens. The
124
- default `--token-scope assistant` therefore fits only assistant-content token
125
- activations while retaining the complete preceding conversation as context.
126
- Other supported scopes are `user`, `content` (all message content), and `all`
127
- (including template tokens).
173
+ default `--token-scope all` fits every formatted position, including template
174
+ tokens. Other supported scopes are `assistant`, `user`, and `content` (all
175
+ message content but no template markers).
128
176
 
129
177
  As in the plain-text demo, `--candidate-tokens` defaults to `--token-budget`.
130
178
  For a larger deterministic candidate pool, run:
@@ -152,18 +200,18 @@ uv run python demo/fit_chat.py \
152
200
  ```
153
201
 
154
202
  Ask the model to generate a response and apply the resulting instruct lens to
155
- the generated assistant tokens:
203
+ the complete formatted conversation:
156
204
 
157
205
  ```bash
158
206
  uv run python demo/apply_chat.py
159
207
  ```
160
208
 
161
209
  The script first generates a response, then performs a full forward pass over
162
- the completed conversation so each generated token has an activation aligned
210
+ the completed conversation so each formatted token has an activation aligned
163
211
  with the same chat formatting used during fitting. The default output shows
164
- component scores only for generated assistant-content tokens. Pass `--user`
165
- and optionally `--system` to supply a different prompt. `--token-scope` accepts
166
- the same `assistant`, `user`, `content`, and `all` choices as the fitting demo:
212
+ component scores for all formatted tokens. Pass `--user` and optionally
213
+ `--system` to supply a different prompt. `--token-scope` accepts the same
214
+ `assistant`, `user`, `content`, and `all` choices as the fitting demo:
167
215
 
168
216
  ```bash
169
217
  uv run python demo/apply_chat.py \
@@ -178,6 +226,25 @@ The command also writes a self-contained v5-style explorer to
178
226
  different location. The report works directly from disk and does not require
179
227
  the v5 server.
180
228
 
229
+ For a predetermined multi-turn conversation, pass a quoted JSON list. The
230
+ model generates an assistant response after each entry; the next user entry is
231
+ then appended regardless of what the model said:
232
+
233
+ ```bash
234
+ uv run python demo/apply_chat.py \
235
+ --user '["Hi, how are you?", "Nothing."]' \
236
+ --layer 7
237
+ ```
238
+
239
+ Repeating the flag is equivalent and is often easier to type:
240
+
241
+ ```bash
242
+ uv run python demo/apply_chat.py \
243
+ --user "Hi, how are you?" \
244
+ --user "Nothing." \
245
+ --layer 7
246
+ ```
247
+
181
248
  ## Apply the saved lens
182
249
 
183
250
  After fitting layer 6, apply it to fresh text:
@@ -204,6 +271,11 @@ includes responsive token cards, signed component bars, component highlighting,
204
271
  card-width control, and an opacity cutoff. Override the path with
205
272
  `--output-file PATH`.
206
273
 
274
+ Pass `--metric score` (the default) for signed ICA coordinates or
275
+ `--metric energy` for nonnegative per-token squared-score fractions displayed
276
+ as percentages. The selected metric controls ranking, terminal output, and the
277
+ HTML explorer for both `apply.py` and `apply_chat.py`.
278
+
207
279
  ## Publish and verify a chat lens
208
280
 
209
281
  The fitting demo records exact dataset and sampling provenance. Upload through
@@ -7,7 +7,6 @@ from pathlib import Path
7
7
 
8
8
  import torch
9
9
  from gb10_load_llm import load_model_to_cuda
10
- from html_explorer import write_explorer_html
11
10
  from transformers import AutoModelForCausalLM, AutoTokenizer
12
11
 
13
12
  from icalens import ICALens
@@ -23,10 +22,20 @@ def parse_args() -> argparse.Namespace:
23
22
  parser.add_argument("--text", default=DEFAULT_TEXT)
24
23
  parser.add_argument("--layer", type=int, default=6)
25
24
  parser.add_argument("--top-k", type=int, default=5)
25
+ parser.add_argument(
26
+ "--metric",
27
+ choices=("score", "energy"),
28
+ default="score",
29
+ help="Rank and display signed ICA scores or per-token energy shares.",
30
+ )
26
31
  parser.add_argument("--output-file", type=Path, default=DEFAULT_OUTPUT_FILE)
27
32
  return parser.parse_args()
28
33
 
29
34
 
35
+ def format_value(value: float, metric: str) -> str:
36
+ return f"{value:+.3f}" if metric == "score" else f"{value:.2%}"
37
+
38
+
30
39
  def main() -> None:
31
40
  args = parse_args()
32
41
  if not torch.cuda.is_available():
@@ -62,46 +71,29 @@ def main() -> None:
62
71
  tokenizer=tokenizer,
63
72
  context_length=tokenizer.model_max_length,
64
73
  )
65
- scores = result.scores
66
- top_k = min(args.top_k, scores.shape[-1])
67
- top_indices = torch.topk(scores.abs(), k=top_k, dim=-1).indices
68
- token_ids = result.token_ids.tolist()
74
+ values = result.scores if args.metric == "score" else result.energy
75
+ ranking_values = values.abs() if args.metric == "score" else values
76
+ top_k = min(args.top_k, values.shape[-1])
77
+ top_indices = torch.topk(ranking_values, k=top_k, dim=-1).indices
69
78
  tokens = result.tokens
70
79
 
71
80
  print(f"Lens: {args.lens}")
72
81
  print(f"Model: {lens.model_id}@{lens.model_revision} ({lens.model_type})")
73
82
  print(f"Layer: {args.layer}")
83
+ print(f"Metric: {args.metric}")
74
84
  print()
75
85
  for position, token in enumerate(tokens):
76
86
  entries = [
77
- f"C{component.item()}={scores[position, component].item():+.3f}"
87
+ f"C{component.item()}={format_value(values[position, component].item(), args.metric)}"
78
88
  for component in top_indices[position]
79
89
  ]
80
90
  print(f"{position:>3} {token!r:<18} {' '.join(entries)}")
81
91
 
82
- html_tokens = [
83
- {
84
- "position": position,
85
- "token": token,
86
- "token_text": tokenizer.decode([token_ids[position]]),
87
- "top": [
88
- {
89
- "component": int(component),
90
- "score": float(scores[position, component]),
91
- }
92
- for component in top_indices[position]
93
- ],
94
- }
95
- for position, token in enumerate(tokens)
96
- ]
97
- output_file = write_explorer_html(
92
+ output_file = result.to_html(
98
93
  args.output_file,
99
94
  title="ICA Lens Text Explorer",
100
- model=f"{lens.model_id}@{lens.model_revision}",
101
- layer=args.layer,
102
- input_text=args.text,
103
- token_scope="all text tokens",
104
- tokens=html_tokens,
95
+ metric=args.metric,
96
+ top_k=args.top_k,
105
97
  )
106
98
  print()
107
99
  print(f"HTML explorer: {output_file}")