LH-Tech-AI commited on
Commit
03224f5
·
verified ·
1 Parent(s): c5e469a

Upload htmllm_v2_124m.ipynb

Browse files
Files changed (1) hide show
  1. htmllm_v2_124m.ipynb +454 -0
htmllm_v2_124m.ipynb ADDED
@@ -0,0 +1,454 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "metadata": {
7
+ "_cell_guid": "b1076dfc-b9ad-4769-8c92-a6c4dae69d19",
8
+ "_uuid": "8f2839f25d086af736a60e9eeb907d3b93b6e0e5",
9
+ "trusted": true
10
+ },
11
+ "outputs": [],
12
+ "source": [
13
+ "!git clone https://github.com/karpathy/nanoGPT.git\n",
14
+ "%cd nanoGPT\n",
15
+ "!pip install -U tiktoken datasets tqdm transformers huggingface_hub"
16
+ ]
17
+ },
18
+ {
19
+ "cell_type": "code",
20
+ "execution_count": null,
21
+ "metadata": {
22
+ "trusted": true
23
+ },
24
+ "outputs": [],
25
+ "source": [
26
+ "import os\n",
27
+ "import numpy as np\n",
28
+ "import tiktoken\n",
29
+ "from datasets import load_dataset\n",
30
+ "from tqdm import tqdm\n",
31
+ "from huggingface_hub import login\n",
32
+ "\n",
33
+ "# --- SETUP ---\n",
34
+ "HF_TOKEN = \"TOKEN_HERE\"\n",
35
+ "login(token=HF_TOKEN)\n",
36
+ "\n",
37
+ "# Config\n",
38
+ "target_tokens = 250_000_000\n",
39
+ "sft_ratio = 0.15\n",
40
+ "data_dir = os.path.join('data', 'html_v2_mixed')\n",
41
+ "os.makedirs(data_dir, exist_ok=True)\n",
42
+ "\n",
43
+ "enc = tiktoken.get_encoding(\"gpt2\")\n",
44
+ "\n",
45
+ "def process_and_save():\n",
46
+ " train_file = os.path.join(data_dir, 'train.bin')\n",
47
+ " val_file = os.path.join(data_dir, 'val.bin')\n",
48
+ " \n",
49
+ " print(\"Lade Datensätze...\")\n",
50
+ " ds_stack = load_dataset(\"bigcode/the-stack-smol\", data_dir=\"data/html\", split=\"train\", streaming=True, token=HF_TOKEN)\n",
51
+ " ds_sft = load_dataset(\"ttbui/html_alpaca\", split=\"train\", streaming=True)\n",
52
+ " \n",
53
+ " all_tokens_np = np.zeros(target_tokens, dtype=np.uint16)\n",
54
+ " curr_idx = 0\n",
55
+ " \n",
56
+ " sft_target = int(target_tokens * sft_ratio)\n",
57
+ " print(f\"Tokenisiere SFT-Daten (Ziel: {sft_target} Tokens)...\")\n",
58
+ " \n",
59
+ " pbar_sft = tqdm(total=sft_target)\n",
60
+ " for ex in ds_sft:\n",
61
+ " instr = ex.get('instruction', '')\n",
62
+ " resp = ex.get('output', ex.get('code', ''))\n",
63
+ " if not resp: continue\n",
64
+ " \n",
65
+ " text = f\"### Instruction:\\n{instr}\\n\\n### Response:\\n{resp}<|endoftext|>\"\n",
66
+ " tokens = enc.encode_ordinary(text)\n",
67
+ " \n",
68
+ " take = min(len(tokens), sft_target - curr_idx)\n",
69
+ " all_tokens_np[curr_idx:curr_idx+take] = np.array(tokens[:take], dtype=np.uint16)\n",
70
+ " curr_idx += take\n",
71
+ " pbar_sft.update(take)\n",
72
+ " \n",
73
+ " if curr_idx >= sft_target:\n",
74
+ " break\n",
75
+ " pbar_sft.close()\n",
76
+ " \n",
77
+ " print(f\"Tokenisiere Raw HTML (Rest bis {target_tokens} Tokens)...\")\n",
78
+ " pbar_stack = tqdm(total=target_tokens - curr_idx)\n",
79
+ " \n",
80
+ " for entry in ds_stack:\n",
81
+ " text = entry.get('content', '')\n",
82
+ " if not text: continue\n",
83
+ " \n",
84
+ " tokens = enc.encode_ordinary(text)\n",
85
+ " tokens.append(enc.eot_token)\n",
86
+ " \n",
87
+ " take = min(len(tokens), target_tokens - curr_idx)\n",
88
+ " all_tokens_np[curr_idx:curr_idx+take] = np.array(tokens[:take], dtype=np.uint16)\n",
89
+ " \n",
90
+ " curr_idx += take\n",
91
+ " pbar_stack.update(take)\n",
92
+ " \n",
93
+ " if curr_idx >= target_tokens:\n",
94
+ " break\n",
95
+ " pbar_stack.close()\n",
96
+ "\n",
97
+ " print(f\"\\nSorting and Shuffling (Simulation via Block-Shuffling)...\")\n",
98
+ " \n",
99
+ " n = curr_idx\n",
100
+ " split_idx = int(n * 0.95) # 5% Validation\n",
101
+ " train_data = all_tokens_np[:split_idx]\n",
102
+ " val_data = all_tokens_np[split_idx:n]\n",
103
+ "\n",
104
+ " print(f\"Speichere train.bin ({len(train_data)} Tokens)...\")\n",
105
+ " train_data.tofile(train_file)\n",
106
+ " print(f\"Speichere val.bin ({len(val_data)} Tokens)...\")\n",
107
+ " val_data.tofile(val_file)\n",
108
+ " \n",
109
+ " print(f\"Success! v2 dataset ready in {data_dir}\")\n",
110
+ " print(f\"Verhältnis: {sft_ratio*100}% SFT / {(1-sft_ratio)*100}% Raw HTML\")\n",
111
+ "\n",
112
+ "if __name__ == \"__main__\":\n",
113
+ " process_and_save()"
114
+ ]
115
+ },
116
+ {
117
+ "cell_type": "code",
118
+ "execution_count": null,
119
+ "metadata": {
120
+ "trusted": true
121
+ },
122
+ "outputs": [],
123
+ "source": [
124
+ "import os\n",
125
+ "import shutil\n",
126
+ "\n",
127
+ "# 1. Pfade anpassen\n",
128
+ "UPLOADED_MODEL_PATH = '/kaggle/input/notebooks/leoheinrich/htmllm-v2-124m-base/nanoGPT/out-html/ckpt.pt'\n",
129
+ "os.makedirs(\"out-html\", exist_ok=True)\n",
130
+ "\n",
131
+ "# Checkpoint kopieren, damit nanoGPT ihn findet (init_from='resume')\n",
132
+ "if os.path.exists(UPLOADED_MODEL_PATH):\n",
133
+ " shutil.copy(UPLOADED_MODEL_PATH, os.path.join(\"out-html\", 'ckpt.pt'))\n",
134
+ " print(\"Checkpoint erfolgreich geladen.\")\n",
135
+ "else:\n",
136
+ " print(\"FEHLER: Checkpoint nicht gefunden! Pfad prüfen.\")"
137
+ ]
138
+ },
139
+ {
140
+ "cell_type": "code",
141
+ "execution_count": null,
142
+ "metadata": {
143
+ "trusted": true
144
+ },
145
+ "outputs": [],
146
+ "source": [
147
+ "config_content = \"\"\"\n",
148
+ "out_dir = 'out-html'\n",
149
+ "eval_interval = 500\n",
150
+ "eval_iters = 40\n",
151
+ "log_interval = 1\n",
152
+ "always_save_checkpoint = False\n",
153
+ "\n",
154
+ "dataset = 'html_v2_mixed'\n",
155
+ "gradient_accumulation_steps = 8\n",
156
+ "batch_size = 16\n",
157
+ "block_size = 1024\n",
158
+ "\n",
159
+ "# Architektur (~124M Params)\n",
160
+ "n_layer = 12\n",
161
+ "n_head = 12\n",
162
+ "n_embd = 768\n",
163
+ "dropout = 0.1\n",
164
+ "\n",
165
+ "learning_rate = 6e-4\n",
166
+ "max_iters = 15000\n",
167
+ "lr_decay_iters = 15000\n",
168
+ "min_lr = 6e-5\n",
169
+ "beta2 = 0.99\n",
170
+ "warmup_iters = 500\n",
171
+ "device = 'cuda'\n",
172
+ "compile = True\n",
173
+ "dtype = 'float16'\n",
174
+ "\n",
175
+ "init_from = 'scratch'\n",
176
+ "\"\"\"\n",
177
+ "\n",
178
+ "with open('config/train_html.py', 'w') as f:\n",
179
+ " f.write(config_content)"
180
+ ]
181
+ },
182
+ {
183
+ "cell_type": "code",
184
+ "execution_count": null,
185
+ "metadata": {
186
+ "trusted": true
187
+ },
188
+ "outputs": [],
189
+ "source": [
190
+ "with open('train.py', 'r') as f:\n",
191
+ " lines = f.readlines()\n",
192
+ "\n",
193
+ "new_lines = []\n",
194
+ "for line in lines:\n",
195
+ " new_lines.append(line)\n",
196
+ " if \"if losses['val'] < best_val_loss or always_save_checkpoint:\" in line:\n",
197
+ " # Code-Einschub für Live-Sampling v2\n",
198
+ " new_lines.append(\" print('\\\\n--- v2 LIVE SAMPLES ---')\\n\")\n",
199
+ " new_lines.append(\" import tiktoken\\n\")\n",
200
+ " new_lines.append(\" enc = tiktoken.get_encoding('gpt2')\\n\")\n",
201
+ " new_lines.append(\" # Test 1: Klassischer Autocomplete\\n\")\n",
202
+ " new_lines.append(\" s1 = enc.encode('<!DOCTYPE html>\\\\n<html>', allowed_special={'<|endoftext|>'})\\n\")\n",
203
+ " new_lines.append(\" # Test 2: SFT-Modus\\n\")\n",
204
+ " new_lines.append(\" s2 = enc.encode('### Instruction:\\\\nCreate a blue button.\\\\n\\\\n### Response:\\\\n', allowed_special={'<|endoftext|>'})\\n\")\n",
205
+ " new_lines.append(\" for prompt_ids, label in [(s1, 'AUTOCOMPLETE'), (s2, 'INSTRUCT')]:\\n\")\n",
206
+ " new_lines.append(\" print(f'>> Mode: {label}')\\n\")\n",
207
+ " new_lines.append(\" x = torch.tensor(prompt_ids, dtype=torch.long, device=device)[None, ...]\\n\")\n",
208
+ " new_lines.append(\" with torch.no_grad():\\n\")\n",
209
+ " new_lines.append(\" y = model.generate(x, 250, temperature=0.5, top_k=40)\\n\")\n",
210
+ " new_lines.append(\" print(enc.decode(y[0].tolist()))\\n\")\n",
211
+ " new_lines.append(\" print('-----------------------\\\\n')\\n\")\n",
212
+ "\n",
213
+ "with open('train_modified.py', 'w') as f:\n",
214
+ " f.writelines(new_lines)"
215
+ ]
216
+ },
217
+ {
218
+ "cell_type": "code",
219
+ "execution_count": null,
220
+ "metadata": {
221
+ "trusted": true
222
+ },
223
+ "outputs": [],
224
+ "source": [
225
+ "import torch\n",
226
+ "import torch.nn.functional as F\n",
227
+ "import tiktoken\n",
228
+ "from model import GPT, GPTConfig\n",
229
+ "\n",
230
+ "def generate_with_penalty(model, idx, max_new_tokens, temperature=0.4, repetition_penalty=1.5, top_k=40):\n",
231
+ " for _ in range(max_new_tokens):\n",
232
+ " idx_cond = idx if idx.size(1) <= model.config.block_size else idx[:, -model.config.block_size:]\n",
233
+ " logits, _ = model(idx_cond)\n",
234
+ " logits = logits[:, -1, :] / temperature\n",
235
+ " \n",
236
+ " # Stärkere Penalty für bereits erschienene Tokens\n",
237
+ " for token_id in set(idx[0].tolist()):\n",
238
+ " logits[0, token_id] -= repetition_penalty # Subtraktion statt Division ist oft stabiler\n",
239
+ "\n",
240
+ " # Top-K Filter\n",
241
+ " v, _ = torch.topk(logits, min(top_k, logits.size(-1)))\n",
242
+ " logits[logits < v[:, [-1]]] = -float('Inf')\n",
243
+ " \n",
244
+ " probs = F.softmax(logits, dim=-1)\n",
245
+ " idx_next = torch.multinomial(probs, num_samples=1)\n",
246
+ " idx = torch.cat((idx, idx_next), dim=1)\n",
247
+ " \n",
248
+ " if idx_next.item() == 50256: # <|endoftext|>\n",
249
+ " break\n",
250
+ " return idx\n",
251
+ "\n",
252
+ "def test_v2(checkpoint_path, prompt, max_tokens=512, temp=0.7, penalty=1.3):\n",
253
+ " device = 'cuda'\n",
254
+ " checkpoint = torch.load(checkpoint_path, map_location=device)\n",
255
+ " model = GPT(GPTConfig(**checkpoint['model_args']))\n",
256
+ " \n",
257
+ " state_dict = checkpoint['model']\n",
258
+ " unwanted_prefix = '_orig_mod.'\n",
259
+ " for k,v in list(state_dict.items()):\n",
260
+ " if k.startswith(unwanted_prefix):\n",
261
+ " state_dict[k[len(unwanted_prefix):]] = state_dict.pop(k)\n",
262
+ " \n",
263
+ " model.load_state_dict(state_dict)\n",
264
+ " model.to(device).eval()\n",
265
+ " \n",
266
+ " enc = tiktoken.get_encoding(\"gpt2\")\n",
267
+ " start_ids = enc.encode(prompt, allowed_special={'<|endoftext|>'})\n",
268
+ " x = torch.tensor(start_ids, dtype=torch.long, device=device)[None, ...]\n",
269
+ " \n",
270
+ " print(f\"\\n--- TESTING WITH TEMP {temp} AND PENALTY {penalty} ---\")\n",
271
+ " with torch.no_grad():\n",
272
+ " y = generate_with_penalty(model, x, max_tokens, temperature=temp, top_k=40, repetition_penalty=penalty)\n",
273
+ " print(enc.decode(y[0].tolist()))\n",
274
+ "\n",
275
+ "# --- DEIN TEST-PLAN ---\n",
276
+ "path = 'out-html/ckpt.pt'\n",
277
+ "\n",
278
+ "# Test 1: Design-Fokus (Bootstrap)\n",
279
+ "#test_v2(path, \"### Instruction:\\nCreate a hero section with a dark background and a 'Learn More' button.\\n\\n### Response:\\n\", temp=0.8, penalty=1.4)\n",
280
+ "\n",
281
+ "# Test 2: CSS-Logik (Inline Styles)\n",
282
+ "#test_v2(path, \"<!DOCTYPE html>\\n<html>\\n<head>\\n<style>\\n .card { background-color:\", max_tokens=200, temp=0.5, penalty=1.2)\n",
283
+ "\n",
284
+ "test_v2(path, \"<form class=\\\"p-4 border rounded\\\">\\n <div class=\\\"mb-3\\\">\\n <label class=\\\"form-label\\\">Email</label>\", 300, 0.8, 1.5)"
285
+ ]
286
+ },
287
+ {
288
+ "cell_type": "code",
289
+ "execution_count": null,
290
+ "metadata": {
291
+ "trusted": true
292
+ },
293
+ "outputs": [],
294
+ "source": [
295
+ "!python train_modified.py config/train_html.py"
296
+ ]
297
+ },
298
+ {
299
+ "cell_type": "code",
300
+ "execution_count": 9,
301
+ "metadata": {
302
+ "execution": {
303
+ "iopub.execute_input": "2026-03-13T13:58:30.581852Z",
304
+ "iopub.status.busy": "2026-03-13T13:58:30.581555Z",
305
+ "iopub.status.idle": "2026-03-13T13:58:30.586681Z",
306
+ "shell.execute_reply": "2026-03-13T13:58:30.585908Z",
307
+ "shell.execute_reply.started": "2026-03-13T13:58:30.581821Z"
308
+ },
309
+ "trusted": true
310
+ },
311
+ "outputs": [],
312
+ "source": [
313
+ "# NOW: Resume after iteration 2500:\n",
314
+ "config_content = \"\"\"\n",
315
+ "out_dir = 'out-html'\n",
316
+ "eval_interval = 500\n",
317
+ "eval_iters = 40\n",
318
+ "log_interval = 1\n",
319
+ "always_save_checkpoint = False\n",
320
+ "\n",
321
+ "dataset = 'html_v2_mixed'\n",
322
+ "gradient_accumulation_steps = 8\n",
323
+ "batch_size = 16\n",
324
+ "block_size = 1024\n",
325
+ "\n",
326
+ "# Architektur (~124M Params)\n",
327
+ "n_layer = 12\n",
328
+ "n_head = 12\n",
329
+ "n_embd = 768\n",
330
+ "dropout = 0.1\n",
331
+ "\n",
332
+ "learning_rate = 1e-4\n",
333
+ "max_iters = 5000\n",
334
+ "lr_decay_iters = 5000\n",
335
+ "min_lr = 1e-6\n",
336
+ "beta2 = 0.99\n",
337
+ "warmup_iters = 500\n",
338
+ "device = 'cuda'\n",
339
+ "compile = True\n",
340
+ "dtype = 'float16'\n",
341
+ "\n",
342
+ "init_from = 'resume'\n",
343
+ "\"\"\"\n",
344
+ "\n",
345
+ "with open('config/train_html.py', 'w') as f:\n",
346
+ " f.write(config_content)"
347
+ ]
348
+ },
349
+ {
350
+ "cell_type": "code",
351
+ "execution_count": null,
352
+ "metadata": {
353
+ "execution": {
354
+ "iopub.execute_input": "2026-03-13T13:58:34.059934Z",
355
+ "iopub.status.busy": "2026-03-13T13:58:34.059199Z"
356
+ },
357
+ "trusted": true
358
+ },
359
+ "outputs": [
360
+ {
361
+ "name": "stdout",
362
+ "output_type": "stream",
363
+ "text": [
364
+ "Overriding config with config/train_html.py:\n",
365
+ "\n",
366
+ "out_dir = 'out-html'\n",
367
+ "eval_interval = 500\n",
368
+ "eval_iters = 40\n",
369
+ "log_interval = 1\n",
370
+ "always_save_checkpoint = False\n",
371
+ "\n",
372
+ "dataset = 'html_v2_mixed'\n",
373
+ "gradient_accumulation_steps = 8\n",
374
+ "batch_size = 16\n",
375
+ "block_size = 1024\n",
376
+ "\n",
377
+ "# Architektur (~124M Params)\n",
378
+ "n_layer = 12\n",
379
+ "n_head = 12\n",
380
+ "n_embd = 768\n",
381
+ "dropout = 0.1\n",
382
+ "\n",
383
+ "learning_rate = 1e-4\n",
384
+ "max_iters = 5000\n",
385
+ "lr_decay_iters = 5000\n",
386
+ "min_lr = 1e-6\n",
387
+ "beta2 = 0.99\n",
388
+ "warmup_iters = 500\n",
389
+ "device = 'cuda'\n",
390
+ "compile = True\n",
391
+ "dtype = 'float16'\n",
392
+ "\n",
393
+ "init_from = 'resume'\n",
394
+ "\n",
395
+ "tokens per iteration will be: 131,072\n",
396
+ "/usr/local/lib/python3.12/dist-packages/torch/backends/__init__.py:46: UserWarning: Please use the new API settings to control TF32 behavior, such as torch.backends.cudnn.conv.fp32_precision = 'tf32' or torch.backends.cuda.matmul.fp32_precision = 'ieee'. Old settings, e.g, torch.backends.cuda.matmul.allow_tf32 = True, torch.backends.cudnn.allow_tf32 = True, allowTF32CuDNN() and allowTF32CuBLAS() will be deprecated after Pytorch 2.9. Please see https://pytorch.org/docs/main/notes/cuda.html#tensorfloat-32-tf32-on-ampere-and-later-devices (Triggered internally at /pytorch/aten/src/ATen/Context.cpp:80.)\n",
397
+ " self.setter(val)\n",
398
+ "Resuming training from out-html\n",
399
+ "number of parameters: 123.59M\n",
400
+ "/kaggle/working/nanoGPT/train_modified.py:196: FutureWarning: `torch.cuda.amp.GradScaler(args...)` is deprecated. Please use `torch.amp.GradScaler('cuda', args...)` instead.\n",
401
+ " scaler = torch.cuda.amp.GradScaler(enabled=(dtype == 'float16'))\n",
402
+ "num decayed parameter tensors: 50, with 124,354,560 parameters\n",
403
+ "num non-decayed parameter tensors: 25, with 19,200 parameters\n",
404
+ "using fused AdamW: True\n",
405
+ "compiling the model... (takes a ~minute)\n",
406
+ "step 2500: train loss 0.8834, val loss 1.0695\n",
407
+ "iter 2500: loss 1.0691, time 32466.99ms, mfu -100.00%\n",
408
+ "iter 2501: loss 0.6317, time 5796.15ms, mfu -100.00%\n",
409
+ "iter 2502: loss 1.0973, time 7771.45ms, mfu -100.00%\n",
410
+ "iter 2503: loss 0.8611, time 7912.30ms, mfu -100.00%\n"
411
+ ]
412
+ }
413
+ ],
414
+ "source": [
415
+ "!python train_modified.py config/train_html.py"
416
+ ]
417
+ }
418
+ ],
419
+ "metadata": {
420
+ "kaggle": {
421
+ "accelerator": "nvidiaTeslaT4",
422
+ "dataSources": [
423
+ {
424
+ "isSourceIdPinned": false,
425
+ "sourceId": 303214451,
426
+ "sourceType": "kernelVersion"
427
+ }
428
+ ],
429
+ "isGpuEnabled": true,
430
+ "isInternetEnabled": true,
431
+ "language": "python",
432
+ "sourceType": "notebook"
433
+ },
434
+ "kernelspec": {
435
+ "display_name": "Python 3",
436
+ "language": "python",
437
+ "name": "python3"
438
+ },
439
+ "language_info": {
440
+ "codemirror_mode": {
441
+ "name": "ipython",
442
+ "version": 3
443
+ },
444
+ "file_extension": ".py",
445
+ "mimetype": "text/x-python",
446
+ "name": "python",
447
+ "nbconvert_exporter": "python",
448
+ "pygments_lexer": "ipython3",
449
+ "version": "3.12.12"
450
+ }
451
+ },
452
+ "nbformat": 4,
453
+ "nbformat_minor": 4
454
+ }