adyoi commited on
Commit
c7c5a39
·
verified ·
1 Parent(s): b844ae7

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -14,49 +14,50 @@ datasets:
14
 
15
  # Indigo
16
 
17
- Model bahasa kecil GPT-style yang dibangun **dari nol** (tanpa library transformers) sebagai proyek pembelajaran. Dual-backend: **PyTorch** dan **TensorFlow/Keras**, dengan format bobot aman `.safetensors`.
18
 
19
  ## Arsitektur
20
 
21
  | | Nilai default |
22
  |---|---|
23
- | Tipe | Decoder-only transformer (pre-LN) |
24
  | Parameter | ~0.81M |
25
  | Layer / Head | 4 / 4 |
26
  | Dimensi | 128 |
27
  | Konteks | 96 token |
28
- | Tokenizer | Level karakter (~90 vocab) |
29
 
30
  ## File penting
31
 
32
  ```
33
- indigo/model.py arsitektur PyTorch (SDPA)
34
- indigo/model_keras.py arsitektur Keras 3
35
- train.py / train_tf.py training per backend
36
- generate.py / generate_tf.py generasi teks
 
37
  out/indigo_best.safetensors bobot terbaik + _meta.json
38
  ```
39
 
40
  ## Cara pakai
41
 
42
  ```bash
43
- pip install -r requirements.txt # torch (+ tensorflow opsional)
44
 
45
- # PyTorch
46
  python train.py --data data/sample.txt --steps 2000
47
- python generate.py --prompt "Indigo" --max-new 300
48
 
49
- # TensorFlow/Keras
50
- python train_tf.py --data data/sample.txt --out out_tf
51
- python generate_tf.py --prompt "Indigo"
 
52
  ```
53
 
54
- Fitur: pembersihan teks otomatis, split validasi per-file, best-checkpoint, resume (`--init-from`), cosine LR + warmup, top-k sampling, pilihan `--device`.
55
 
56
  ## Batasan
57
 
58
- - Dilatih pada data sangat kecil (~16 ribu token) → output belum koheren, cocok untuk edukasi bukan produksi.
59
- - Checkpoint berlabel `_best` dipilih berdasarkan validasi; gunakan itu, bukan checkpoint akhir.
60
 
61
  ## Keamanan
62
 
 
14
 
15
  # Indigo
16
 
17
+ Model bahasa kecil GPT-style yang dibangun **dari nol** (tanpa library transformers) sebagai proyek pembelajaran. Backend **PyTorch**; tersedia juga varian [TensorFlow/Keras terpisah](https://huggingface.co/adyoi/indigo-tf). Bobot disimpan dalam format aman `.safetensors`.
18
 
19
  ## Arsitektur
20
 
21
  | | Nilai default |
22
  |---|---|
23
+ | Tipe | Decoder-only transformer (pre-LN, SDPA) |
24
  | Parameter | ~0.81M |
25
  | Layer / Head | 4 / 4 |
26
  | Dimensi | 128 |
27
  | Konteks | 96 token |
28
+ | Tokenizer | karakter atau BPE (`--tokenizer bpe`) |
29
 
30
  ## File penting
31
 
32
  ```
33
+ indigo/model.py arsitektur GPT (attention kausal + KV-cache)
34
+ indigo/bpe.py tokenizer BPE byte-level minimal
35
+ indigo/tokenizer.py tokenizer karakter
36
+ train.py training (best-checkpoint, resume penuh, split val per-file)
37
+ generate.py generasi (top-k, top-p, repetition penalty)
38
  out/indigo_best.safetensors bobot terbaik + _meta.json
39
  ```
40
 
41
  ## Cara pakai
42
 
43
  ```bash
44
+ pip install -r requirements.txt
45
 
 
46
  python train.py --data data/sample.txt --steps 2000
47
+ python train.py --data data/tekskamu.txt --tokenizer bpe --vocab-size 512
48
 
49
+ python generate.py --prompt "Indigo" --max-new 300 \
50
+ --temperature 0.8 --top-k 40 --top-p 0.9 --repetition-penalty 1.2
51
+
52
+ python train.py --init-from out/indigo_best.safetensors --steps 1000 # lanjutkan training
53
  ```
54
 
55
+ Generasi memakai KV-cache sehingga cepat untuk output panjang.
56
 
57
  ## Batasan
58
 
59
+ - Dilatih pada data sangat kecil → output belum koheren; cocok untuk edukasi bukan produksi.
60
+ - Gunakan checkpoint `indigo_best` (terpilih berdasarkan validasi), bukan checkpoint akhir.
61
 
62
  ## Keamanan
63
 
generate.py CHANGED
@@ -1,11 +1,8 @@
1
- import argparse
2
- import json
3
- import os
4
-
5
  import torch
 
6
 
 
7
  from indigo.model import GPT, GPTConfig
8
- from indigo.tokenizer import CharTokenizer
9
 
10
 
11
  def load_model(path):
@@ -13,11 +10,20 @@ def load_model(path):
13
  from safetensors.torch import load_file
14
 
15
  state = load_file(path)
16
- with open(os.path.splitext(path)[0] + "_meta.json", encoding="utf-8") as f:
17
- meta = json.load(f)
18
- return state, meta["config"], meta["vocab"]
19
- ckpt = torch.load(path, map_location="cpu", weights_only=True)
20
- return ckpt["model"], ckpt["config"], ckpt["vocab"]
 
 
 
 
 
 
 
 
 
21
 
22
 
23
  def main():
@@ -27,6 +33,8 @@ def main():
27
  parser.add_argument("--max-new", type=int, default=300)
28
  parser.add_argument("--temperature", type=float, default=0.8)
29
  parser.add_argument("--top-k", type=int, default=40)
 
 
30
  parser.add_argument("--seed", type=int, default=None)
31
  parser.add_argument("--device", default="auto", choices=["auto", "cpu", "cuda"])
32
  args = parser.parse_args()
@@ -35,15 +43,19 @@ def main():
35
  torch.manual_seed(args.seed)
36
  device = "cuda" if torch.cuda.is_available() else "cpu" if args.device == "auto" else args.device
37
 
38
- state, config_d, vocab = load_model(args.ckpt)
39
- model = GPT(GPTConfig(**config_d))
40
- model.load_state_dict(state, strict=False)
41
  model = model.to(device)
42
- tokenizer = CharTokenizer(vocab)
43
 
44
  ids = tokenizer.encode(args.prompt) or [0]
45
  idx = torch.tensor([ids], dtype=torch.long, device=device)
46
- out = model.generate(idx, args.max_new, temperature=args.temperature, top_k=args.top_k)
 
 
 
 
 
 
 
47
  print(tokenizer.decode(out[0].tolist()))
48
 
49
 
 
 
 
 
 
1
  import torch
2
+ import argparse
3
 
4
+ from indigo.common import load_meta, build_tokenizer
5
  from indigo.model import GPT, GPTConfig
 
6
 
7
 
8
  def load_model(path):
 
10
  from safetensors.torch import load_file
11
 
12
  state = load_file(path)
13
+ meta = load_meta(path)
14
+ config_d = meta["config"]
15
+ tinfo = meta.get("tokenizer") or {"type": "char"}
16
+ vocab = meta.get("vocab")
17
+ else:
18
+ ckpt = torch.load(path, map_location="cpu", weights_only=True)
19
+ state = ckpt["model"]
20
+ config_d = ckpt["config"]
21
+ tinfo = ckpt.get("tokenizer") or {"type": "char"}
22
+ vocab = ckpt.get("vocab")
23
+ tokenizer = build_tokenizer(tinfo, vocab)
24
+ model = GPT(GPTConfig(**config_d))
25
+ model.load_state_dict(state, strict=False)
26
+ return model, tokenizer
27
 
28
 
29
  def main():
 
33
  parser.add_argument("--max-new", type=int, default=300)
34
  parser.add_argument("--temperature", type=float, default=0.8)
35
  parser.add_argument("--top-k", type=int, default=40)
36
+ parser.add_argument("--top-p", type=float, default=1.0)
37
+ parser.add_argument("--repetition-penalty", type=float, default=1.0)
38
  parser.add_argument("--seed", type=int, default=None)
39
  parser.add_argument("--device", default="auto", choices=["auto", "cpu", "cuda"])
40
  args = parser.parse_args()
 
43
  torch.manual_seed(args.seed)
44
  device = "cuda" if torch.cuda.is_available() else "cpu" if args.device == "auto" else args.device
45
 
46
+ model, tokenizer = load_model(args.ckpt)
 
 
47
  model = model.to(device)
 
48
 
49
  ids = tokenizer.encode(args.prompt) or [0]
50
  idx = torch.tensor([ids], dtype=torch.long, device=device)
51
+ out = model.generate(
52
+ idx,
53
+ args.max_new,
54
+ temperature=args.temperature,
55
+ top_k=args.top_k,
56
+ top_p=args.top_p,
57
+ repetition_penalty=args.repetition_penalty,
58
+ )
59
  print(tokenizer.decode(out[0].tolist()))
60
 
61
 
indigo/bpe.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def _get_stats(ids):
2
+ stats = {}
3
+ for pair in zip(ids, ids[1:]):
4
+ stats[pair] = stats.get(pair, 0) + 1
5
+ return stats
6
+
7
+
8
+ def _merge(ids, pair, idx):
9
+ out = []
10
+ i = 0
11
+ while i < len(ids):
12
+ if i < len(ids) - 1 and ids[i] == pair[0] and ids[i + 1] == pair[1]:
13
+ out.append(idx)
14
+ i += 2
15
+ else:
16
+ out.append(ids[i])
17
+ i += 1
18
+ return out
19
+
20
+
21
+ class BPETokenizer:
22
+ def __init__(self, merges=None):
23
+ self.merges = [tuple(p) for p in (merges or [])]
24
+ self.ranks = {pair: i for i, pair in enumerate(self.merges)}
25
+ self.vocab = [bytes([i]) for i in range(256)]
26
+ for a, b in self.merges:
27
+ self.vocab.append(self.vocab[a] + self.vocab[b])
28
+
29
+ @classmethod
30
+ def train(cls, text, vocab_size):
31
+ tok = cls()
32
+ ids = list(text.encode("utf-8"))
33
+ next_id = 256
34
+ while next_id < vocab_size and len(ids) > 1:
35
+ stats = _get_stats(ids)
36
+ pair = max(stats, key=stats.get)
37
+ if stats[pair] < 2:
38
+ break
39
+ ids = _merge(ids, pair, next_id)
40
+ tok.ranks[pair] = len(tok.merges)
41
+ tok.merges.append(pair)
42
+ tok.vocab.append(tok.vocab[pair[0]] + tok.vocab[pair[1]])
43
+ next_id += 1
44
+ return tok
45
+
46
+ @property
47
+ def vocab_size(self):
48
+ return 256 + len(self.merges)
49
+
50
+ def _encode_chunk(self, ids):
51
+ while len(ids) >= 2:
52
+ best = None
53
+ best_rank = None
54
+ for pair in zip(ids, ids[1:]):
55
+ rank = self.ranks.get(pair)
56
+ if rank is not None and (best_rank is None or rank < best_rank):
57
+ best = pair
58
+ best_rank = rank
59
+ if best is None:
60
+ break
61
+ ids = _merge(ids, best, 256 + best_rank)
62
+ return ids
63
+
64
+ def encode(self, text):
65
+ return self._encode_chunk(list(text.encode("utf-8")))
66
+
67
+ def decode(self, ids):
68
+ return b"".join(self.vocab[i] for i in ids).decode("utf-8", errors="replace")
69
+
70
+ def state(self):
71
+ return {"type": "bpe", "merges": [list(p) for p in self.merges]}
72
+
73
+ @classmethod
74
+ def from_state(cls, state):
75
+ return cls(state["merges"])
indigo/common.py CHANGED
@@ -28,13 +28,14 @@ def read_clean(path):
28
  return clean_text(f.read())
29
 
30
 
31
- def save_meta(base_path, config, vocab, step, val_loss, backend):
32
  meta = {
33
  "config": config,
34
  "vocab": vocab,
35
  "step": step,
36
  "val_loss": val_loss,
37
  "backend": backend,
 
38
  }
39
  with open(os.path.splitext(base_path)[0] + "_meta.json", "w", encoding="utf-8") as f:
40
  json.dump(meta, f, ensure_ascii=False)
@@ -43,3 +44,13 @@ def save_meta(base_path, config, vocab, step, val_loss, backend):
43
  def load_meta(path):
44
  with open(os.path.splitext(path)[0] + "_meta.json", encoding="utf-8") as f:
45
  return json.load(f)
 
 
 
 
 
 
 
 
 
 
 
28
  return clean_text(f.read())
29
 
30
 
31
+ def save_meta(base_path, config, vocab, step, val_loss, backend, tokenizer=None):
32
  meta = {
33
  "config": config,
34
  "vocab": vocab,
35
  "step": step,
36
  "val_loss": val_loss,
37
  "backend": backend,
38
+ "tokenizer": tokenizer or {"type": "char"},
39
  }
40
  with open(os.path.splitext(base_path)[0] + "_meta.json", "w", encoding="utf-8") as f:
41
  json.dump(meta, f, ensure_ascii=False)
 
44
  def load_meta(path):
45
  with open(os.path.splitext(path)[0] + "_meta.json", encoding="utf-8") as f:
46
  return json.load(f)
47
+
48
+
49
+ def build_tokenizer(tokenizer_info, vocab):
50
+ if tokenizer_info.get("type") == "bpe":
51
+ from indigo.bpe import BPETokenizer
52
+
53
+ return BPETokenizer.from_state(tokenizer_info)
54
+ from indigo.tokenizer import CharTokenizer
55
+
56
+ return CharTokenizer(vocab)
indigo/model.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from dataclasses import dataclass
2
 
3
  import torch
@@ -27,19 +28,30 @@ class CausalSelfAttention(nn.Module):
27
  self.attn_dropout = nn.Dropout(config.dropout)
28
  self.resid_dropout = nn.Dropout(config.dropout)
29
 
30
- def forward(self, x):
31
  B, T, C = x.shape
32
  q, k, v = self.qkv(x).split(self.n_embd, dim=2)
33
  q = q.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
34
  k = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
35
  v = v.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
36
- y = F.scaled_dot_product_attention(
37
- q, k, v,
38
- dropout_p=self.attn_dropout.p if self.training else 0.0,
39
- is_causal=True,
40
- )
 
 
 
 
 
 
 
 
 
 
 
41
  y = y.transpose(1, 2).contiguous().view(B, T, C)
42
- return self.resid_dropout(self.proj(y))
43
 
44
 
45
  class MLP(nn.Module):
@@ -61,10 +73,35 @@ class Block(nn.Module):
61
  self.ln2 = nn.LayerNorm(config.n_embd, bias=config.bias)
62
  self.mlp = MLP(config)
63
 
64
- def forward(self, x):
65
- x = x + self.attn(self.ln1(x))
 
66
  x = x + self.mlp(self.ln2(x))
67
- return x
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
 
70
  class GPT(nn.Module):
@@ -88,32 +125,49 @@ class GPT(nn.Module):
88
  elif isinstance(module, nn.Embedding):
89
  nn.init.normal_(module.weight, mean=0.0, std=0.02)
90
 
91
- def forward(self, idx, targets=None):
92
  B, T = idx.shape
93
- pos = torch.arange(T, device=idx.device)
 
94
  x = self.drop(self.tok_emb(idx) + self.pos_emb(pos))
95
- for block in self.blocks:
96
- x = block(x)
 
 
97
  logits = self.head(self.ln_f(x))
98
  loss = None
99
  if targets is not None:
100
  loss = F.cross_entropy(logits.reshape(-1, logits.size(-1)), targets.reshape(-1))
 
 
101
  return logits, loss
102
 
103
  @torch.no_grad()
104
- def generate(self, idx, max_new_tokens, temperature=1.0, top_k=None):
 
 
 
 
 
 
 
 
105
  self.eval()
106
- for _ in range(max_new_tokens):
107
- idx_cond = idx[:, -self.config.block_size:]
108
- logits, _ = self(idx_cond)
109
- logits = logits[:, -1, :] / max(temperature, 1e-8)
110
- if top_k is not None:
111
- v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
112
- logits[logits < v[:, [-1]]] = float("-inf")
113
- probs = F.softmax(logits, dim=-1)
114
- next_id = torch.multinomial(probs, num_samples=1)
115
- idx = torch.cat((idx, next_id), dim=1)
116
- return idx
 
 
 
 
117
 
118
  def num_params(self):
119
  return sum(p.numel() for p in self.parameters())
 
1
+ import math
2
  from dataclasses import dataclass
3
 
4
  import torch
 
28
  self.attn_dropout = nn.Dropout(config.dropout)
29
  self.resid_dropout = nn.Dropout(config.dropout)
30
 
31
+ def forward(self, x, kv=None):
32
  B, T, C = x.shape
33
  q, k, v = self.qkv(x).split(self.n_embd, dim=2)
34
  q = q.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
35
  k = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
36
  v = v.view(B, T, self.n_head, C // self.n_head).transpose(1, 2)
37
+ if kv is None:
38
+ y = F.scaled_dot_product_attention(
39
+ q, k, v,
40
+ dropout_p=self.attn_dropout.p if self.training else 0.0,
41
+ is_causal=True,
42
+ )
43
+ else:
44
+ pk, pv = kv
45
+ k = torch.cat((pk, k), dim=2)
46
+ v = torch.cat((pv, v), dim=2)
47
+ Tq, Tk = q.size(2), k.size(2)
48
+ mask = torch.ones(Tq, Tk, dtype=torch.bool, device=x.device).tril(diagonal=Tk - Tq)
49
+ att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
50
+ att = att.masked_fill(~mask, float("-inf"))
51
+ att = F.softmax(att, dim=-1)
52
+ y = att @ v
53
  y = y.transpose(1, 2).contiguous().view(B, T, C)
54
+ return self.resid_dropout(self.proj(y)), (k, v)
55
 
56
 
57
  class MLP(nn.Module):
 
73
  self.ln2 = nn.LayerNorm(config.n_embd, bias=config.bias)
74
  self.mlp = MLP(config)
75
 
76
+ def forward(self, x, kv=None):
77
+ a, kv_new = self.attn(self.ln1(x), kv)
78
+ x = x + a
79
  x = x + self.mlp(self.ln2(x))
80
+ return x, kv_new
81
+
82
+
83
+ def _sample_token(logits, temperature, top_k=None, top_p=None):
84
+ logits = logits / max(temperature, 1e-8)
85
+ if top_k is not None:
86
+ v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
87
+ logits[logits < v[:, [-1]]] = float("-inf")
88
+ if top_p is not None and top_p < 1.0:
89
+ sorted_logits, sorted_idx = torch.sort(logits, descending=True, dim=-1)
90
+ probs = F.softmax(sorted_logits, dim=-1)
91
+ cum = torch.cumsum(probs, dim=-1)
92
+ remove = (cum - probs) > top_p
93
+ sorted_logits[remove] = float("-inf")
94
+ logits = torch.full_like(logits, float("-inf")).scatter_(1, sorted_idx, sorted_logits)
95
+ probs = F.softmax(logits, dim=-1)
96
+ return torch.multinomial(probs, num_samples=1)
97
+
98
+
99
+ def _apply_repetition_penalty(logits, tokens, penalty, window):
100
+ recent = set(tokens[0, -window:].tolist())
101
+ for t in recent:
102
+ val = logits[0, t]
103
+ logits[0, t] = torch.where(val > 0, val / penalty, val * penalty)
104
+ return logits
105
 
106
 
107
  class GPT(nn.Module):
 
125
  elif isinstance(module, nn.Embedding):
126
  nn.init.normal_(module.weight, mean=0.0, std=0.02)
127
 
128
+ def forward(self, idx, targets=None, caches=None, return_caches=False):
129
  B, T = idx.shape
130
+ start = caches[0][0].size(2) if caches else 0
131
+ pos = torch.arange(start, start + T, device=idx.device)
132
  x = self.drop(self.tok_emb(idx) + self.pos_emb(pos))
133
+ new_caches = []
134
+ for i, block in enumerate(self.blocks):
135
+ x, c = block(x, caches[i] if caches is not None else None)
136
+ new_caches.append(c)
137
  logits = self.head(self.ln_f(x))
138
  loss = None
139
  if targets is not None:
140
  loss = F.cross_entropy(logits.reshape(-1, logits.size(-1)), targets.reshape(-1))
141
+ if return_caches:
142
+ return logits, loss, new_caches
143
  return logits, loss
144
 
145
  @torch.no_grad()
146
+ def generate(
147
+ self,
148
+ idx,
149
+ max_new_tokens,
150
+ temperature=1.0,
151
+ top_k=None,
152
+ top_p=None,
153
+ repetition_penalty=1.0,
154
+ ):
155
  self.eval()
156
+ idx_cond = idx[:, -self.config.block_size:]
157
+ logits, _, caches = self(idx_cond, return_caches=True)
158
+ tokens = idx_cond
159
+ for i in range(max_new_tokens):
160
+ next_logits = logits[:, -1, :]
161
+ if repetition_penalty != 1.0:
162
+ next_logits = _apply_repetition_penalty(
163
+ next_logits.clone(), tokens, repetition_penalty, self.config.block_size
164
+ )
165
+ next_id = _sample_token(next_logits, temperature, top_k, top_p)
166
+ tokens = torch.cat((tokens, next_id), dim=1)
167
+ if i == max_new_tokens - 1:
168
+ break
169
+ logits, _, caches = self(next_id, caches=caches, return_caches=True)
170
+ return tokens
171
 
172
  def num_params(self):
173
  return sum(p.numel() for p in self.parameters())
out/indigo.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:8438242a855ba259d59d07b7e8a9a29461311c2f008993cb20d5e9f5828dbc72
3
- size 3294144
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1ff2d9c4acdba3bb129caf2163ca2b66885821121a749e0d15d4c282a2d0f71d
3
+ size 3726272
out/indigo_best.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:c2f67a103c33e00f21bb78c13716b70ff97c0de33352a27f0b6fff8c01c480ab
3
- size 3294144
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6f6f8306cadd2b1130775c53814c35e7fe49e049fe86889e08a73a002a276f2d
3
+ size 3726272
out/indigo_best_meta.json CHANGED
@@ -1 +1 @@
1
- {"config": {"vocab_size": 90, "block_size": 96, "n_layer": 4, "n_head": 4, "n_embd": 128, "dropout": 0.1, "bias": false}, "vocab": ["\n", " ", "!", "\"", "#", "%", "&", "'", "(", ")", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", ">", "?", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "]", "_", "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "°"], "step": 900, "val_loss": 3.390139579772949}
 
1
+ {"config": {"vocab_size": 512, "block_size": 96, "n_layer": 4, "n_head": 4, "n_embd": 128, "dropout": 0.1, "bias": false}, "vocab": null, "step": 900, "val_loss": 5.525297069549561, "backend": "pytorch", "tokenizer": {"type": "bpe", "merges": [[97, 110], [101, 114], [97, 32], [97, 116], [101, 110], [256, 32], [105, 110], [97, 108], [105, 32], [58, 32], [97, 114], [116, 105], [111, 114], [111, 110], [101, 115], [44, 32], [97, 109], [97, 104], [256, 103], [101, 32], [101, 109], [101, 107], [97, 107], [117, 110], [117, 32], [101, 108], [97, 115], [97, 100], [263, 32], [260, 103], [105, 115], [32, 109], [262, 103], [32, 40], [32, 115], [32, 100], [105, 116], [101, 116], [32, 65], [101, 99], [117, 104], [117, 116], [46, 10], [32, 83], [257, 277], [117, 115], [116, 257], [117, 114], [105, 108], [97, 112], [121, 274], [41, 32], [105, 99], [259, 32], [98, 257], [34, 10], [114, 111], [46, 32], [260, 116], [300, 258], [105, 107], [41, 10], [84, 73], [32, 80], [100, 105], [100, 261], [107, 101], [32, 112], [97, 99], [117, 107], [32, 116], [32, 67], [97, 103], [115, 116], [117, 108], [82, 69], [279, 103], [105, 100], [265, 34], [84, 69], [267, 269], [121, 258], [32, 69], [259, 105], [101, 100], [261, 109], [108, 111], [32, 38], [270, 115], [256, 100], [108, 273], [84, 65], [111, 109], [115, 101], [65, 76], [339, 269], [272, 264], [83, 73], [79, 78], [105, 109], [121, 32], [76, 76], [306, 32], [44, 10], [105, 104], [32, 68], [65, 82], [32, 107], [107, 261], [58, 10], [259, 258], [266, 264], [117, 100], [32, 77], [65, 121], [370, 309], [69, 82], [287, 285], [97, 98], [112, 297], [114, 105], [115, 281], [109, 315], [99, 104], [65, 73], [266, 258], [269, 116], [271, 321], [267, 100], [32, 78], [117, 112], [112, 283], [121, 97], [311, 371], [117, 109], [32, 99], [259, 101], [32, 98], [65, 78], [32, 75], [384, 278], [103, 262], [97, 280], [69, 83], [276, 98], [268, 274], [32, 73], [115, 105], [101, 112], [282, 264], [111, 108], [97, 119], [260, 267], [265, 39], [377, 102], [69, 78], [32, 265], [295, 116], [105, 114], [116, 104], [101, 102], [288, 103], [116, 114], [276, 112], [267, 118], [304, 108], [421, 266], [262, 116], [117, 259], [101, 120], [110, 397], [105, 103], [278, 261], [101, 98], [100, 264], [76, 69], [273, 32], [342, 103], [111, 301], [279, 116], [111, 100], [267, 99], [107, 259], [268, 32], [291, 261], [103, 117], [73, 78], [294, 67], [270, 32], [40, 77], [322, 387], [285, 107], [84, 296], [272, 280], [68, 261], [46, 112], [32, 66], [267, 102], [256, 99], [277, 115], [99, 105], [263, 272], [268, 275], [256, 116], [112, 257], [262, 100], [97, 262], [103, 296], [389, 49], [305, 258], [300, 97], [408, 116], [301, 257], [288, 32], [114, 270], [32, 70], [327, 79], [108, 346], [45, 78], [272, 109], [332, 463], [46, 39], [114, 101], [65, 83], [435, 325], [262, 302], [121, 110], [82, 73], [97, 121], [109, 314], [265, 77], [32, 84], [32, 10], [276, 268], [278, 341], [79, 84], [296, 475], [492, 283], [110, 337], [447, 398], [75, 352], [91, 380], [77, 80], [468, 95], [84, 85], [85, 357], [272, 258], [116, 345], [266, 100], [283, 258], [396, 32], [267, 107], [99, 344], [111, 103], [110, 105], [112, 269]]}}
out/indigo_meta.json CHANGED
@@ -1 +1 @@
1
- {"config": {"vocab_size": 90, "block_size": 96, "n_layer": 4, "n_head": 4, "n_embd": 128, "dropout": 0.1, "bias": false}, "vocab": ["\n", " ", "!", "\"", "#", "%", "&", "'", "(", ")", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", ">", "?", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "]", "_", "`", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|", "}", "°"], "step": 900, "val_loss": 3.423954129219055}
 
1
+ {"config": {"vocab_size": 512, "block_size": 96, "n_layer": 4, "n_head": 4, "n_embd": 128, "dropout": 0.1, "bias": false}, "vocab": null, "step": 900, "val_loss": 5.809607744216919, "backend": "pytorch", "tokenizer": {"type": "bpe", "merges": [[97, 110], [101, 114], [97, 32], [97, 116], [101, 110], [256, 32], [105, 110], [97, 108], [105, 32], [58, 32], [97, 114], [116, 105], [111, 114], [111, 110], [101, 115], [44, 32], [97, 109], [97, 104], [256, 103], [101, 32], [101, 109], [101, 107], [97, 107], [117, 110], [117, 32], [101, 108], [97, 115], [97, 100], [263, 32], [260, 103], [105, 115], [32, 109], [262, 103], [32, 40], [32, 115], [32, 100], [105, 116], [101, 116], [32, 65], [101, 99], [117, 104], [117, 116], [46, 10], [32, 83], [257, 277], [117, 115], [116, 257], [117, 114], [105, 108], [97, 112], [121, 274], [41, 32], [105, 99], [259, 32], [98, 257], [34, 10], [114, 111], [46, 32], [260, 116], [300, 258], [105, 107], [41, 10], [84, 73], [32, 80], [100, 105], [100, 261], [107, 101], [32, 112], [97, 99], [117, 107], [32, 116], [32, 67], [97, 103], [115, 116], [117, 108], [82, 69], [279, 103], [105, 100], [265, 34], [84, 69], [267, 269], [121, 258], [32, 69], [259, 105], [101, 100], [261, 109], [108, 111], [32, 38], [270, 115], [256, 100], [108, 273], [84, 65], [111, 109], [115, 101], [65, 76], [339, 269], [272, 264], [83, 73], [79, 78], [105, 109], [121, 32], [76, 76], [306, 32], [44, 10], [105, 104], [32, 68], [65, 82], [32, 107], [107, 261], [58, 10], [259, 258], [266, 264], [117, 100], [32, 77], [65, 121], [370, 309], [69, 82], [287, 285], [97, 98], [112, 297], [114, 105], [115, 281], [109, 315], [99, 104], [65, 73], [266, 258], [269, 116], [271, 321], [267, 100], [32, 78], [117, 112], [112, 283], [121, 97], [311, 371], [117, 109], [32, 99], [259, 101], [32, 98], [65, 78], [32, 75], [384, 278], [103, 262], [97, 280], [69, 83], [276, 98], [268, 274], [32, 73], [115, 105], [101, 112], [282, 264], [111, 108], [97, 119], [260, 267], [265, 39], [377, 102], [69, 78], [32, 265], [295, 116], [105, 114], [116, 104], [101, 102], [288, 103], [116, 114], [276, 112], [267, 118], [304, 108], [421, 266], [262, 116], [117, 259], [101, 120], [110, 397], [105, 103], [278, 261], [101, 98], [100, 264], [76, 69], [273, 32], [342, 103], [111, 301], [279, 116], [111, 100], [267, 99], [107, 259], [268, 32], [291, 261], [103, 117], [73, 78], [294, 67], [270, 32], [40, 77], [322, 387], [285, 107], [84, 296], [272, 280], [68, 261], [46, 112], [32, 66], [267, 102], [256, 99], [277, 115], [99, 105], [263, 272], [268, 275], [256, 116], [112, 257], [262, 100], [97, 262], [103, 296], [389, 49], [305, 258], [300, 97], [408, 116], [301, 257], [288, 32], [114, 270], [32, 70], [327, 79], [108, 346], [45, 78], [272, 109], [332, 463], [46, 39], [114, 101], [65, 83], [435, 325], [262, 302], [121, 110], [82, 73], [97, 121], [109, 314], [265, 77], [32, 84], [32, 10], [276, 268], [278, 341], [79, 84], [296, 475], [492, 283], [110, 337], [447, 398], [75, 352], [91, 380], [77, 80], [468, 95], [84, 85], [85, 357], [272, 258], [116, 345], [266, 100], [283, 258], [396, 32], [267, 107], [99, 344], [111, 103], [110, 105], [112, 269]]}}
requirements.txt CHANGED
@@ -1,3 +1,2 @@
1
  torch>=2.0
2
  safetensors>=0.4
3
- tensorflow>=2.16
 
1
  torch>=2.0
2
  safetensors>=0.4
 
train.py CHANGED
@@ -1,13 +1,19 @@
1
- import argparse
2
- import math
3
  import os
4
- import random
5
  import time
6
-
7
  import torch
 
 
 
8
  from safetensors.torch import save_file
9
 
10
- from indigo.common import collect_text_files, load_meta, read_clean, save_meta
 
 
 
 
 
 
11
  from indigo.model import GPT, GPTConfig
12
  from indigo.tokenizer import CharTokenizer
13
 
@@ -25,9 +31,15 @@ def load_init(path):
25
  opt = torch.load(opt_path, map_location="cpu", weights_only=True)
26
  except Exception as e:
27
  print(f"optimizer state dilewati: {e}")
28
- return state, meta["config"], meta.get("step", 0), opt
29
  ckpt = torch.load(path, map_location="cpu", weights_only=True)
30
- return ckpt["model"], ckpt["config"], ckpt.get("step", 0), ckpt.get("optimizer")
 
 
 
 
 
 
31
 
32
 
33
  def get_batch(data, block_size, batch_size, device):
@@ -68,6 +80,8 @@ def main():
68
  parser.add_argument("--seed", type=int, default=1337)
69
  parser.add_argument("--init-from", default=None, help="checkpoint untuk melanjutkan training")
70
  parser.add_argument("--device", default="auto", choices=["auto", "cpu", "cuda"])
 
 
71
  parser.add_argument("--val-fraction", type=float, default=0.1, help="proporsi file untuk validasi")
72
  args = parser.parse_args()
73
 
@@ -90,36 +104,56 @@ def main():
90
 
91
  train_text = "".join(read_clean(p) for p in files[n_val:])
92
  val_text = "".join(read_clean(p) for p in files[:n_val])
93
-
94
- tokenizer = CharTokenizer.from_text(train_text + val_text)
95
- train_data = torch.tensor(tokenizer.encode(train_text), dtype=torch.long)
96
- val_data = torch.tensor(tokenizer.encode(val_text), dtype=torch.long)
97
- if len(train_data) < args.block_size * 2:
98
- raise SystemExit(f"data latih terlalu pendek ({len(train_data)} token), minimal {args.block_size * 2}")
99
- print(
100
- f"tokens latih={len(train_data):,} | tokens validasi={len(val_data):,} | vocab={tokenizer.vocab_size}"
101
- )
102
 
103
  init_state = None
104
  init_opt = None
105
  start_step = 0
 
 
106
  if args.init_from:
107
- init_state, init_config, start_step, init_opt = load_init(args.init_from)
108
- config = GPTConfig(**init_config)
 
109
  print(f"melanjutkan dari {args.init_from} (step {start_step})")
 
 
110
  else:
111
- config = GPTConfig(
112
- vocab_size=tokenizer.vocab_size,
113
- block_size=args.block_size,
114
- n_layer=args.n_layer,
115
- n_head=args.n_head,
116
- n_embd=args.n_embd,
117
- dropout=args.dropout,
118
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  if config.vocab_size != tokenizer.vocab_size:
120
  raise SystemExit(
121
- f"vocab tidak cocok: checkpoint={config.vocab_size}, data={tokenizer.vocab_size}"
122
  )
 
 
 
 
 
 
 
 
 
123
  model = GPT(config)
124
  if init_state is not None:
125
  missing, unexpected = model.load_state_dict(init_state, strict=False)
@@ -145,7 +179,15 @@ def main():
145
  def save_model(base_path, val_loss):
146
  tensors = {k: v.detach().clone().contiguous() for k, v in model.state_dict().items()}
147
  save_file(tensors, base_path)
148
- save_meta(base_path, config.__dict__, tokenizer.itos, total_steps, val_loss, backend="pytorch")
 
 
 
 
 
 
 
 
149
 
150
  def lr_at(step):
151
  if step < args.warmup:
 
 
 
1
  import os
 
2
  import time
3
+ import math
4
  import torch
5
+ import random
6
+ import argparse
7
+
8
  from safetensors.torch import save_file
9
 
10
+ from indigo.common import (
11
+ build_tokenizer,
12
+ collect_text_files,
13
+ load_meta,
14
+ read_clean,
15
+ save_meta,
16
+ )
17
  from indigo.model import GPT, GPTConfig
18
  from indigo.tokenizer import CharTokenizer
19
 
 
31
  opt = torch.load(opt_path, map_location="cpu", weights_only=True)
32
  except Exception as e:
33
  print(f"optimizer state dilewati: {e}")
34
+ return state, meta, opt
35
  ckpt = torch.load(path, map_location="cpu", weights_only=True)
36
+ meta = {
37
+ "config": ckpt["config"],
38
+ "vocab": ckpt["vocab"],
39
+ "step": ckpt.get("step", 0),
40
+ "tokenizer": ckpt.get("tokenizer"),
41
+ }
42
+ return ckpt["model"], meta, ckpt.get("optimizer")
43
 
44
 
45
  def get_batch(data, block_size, batch_size, device):
 
80
  parser.add_argument("--seed", type=int, default=1337)
81
  parser.add_argument("--init-from", default=None, help="checkpoint untuk melanjutkan training")
82
  parser.add_argument("--device", default="auto", choices=["auto", "cpu", "cuda"])
83
+ parser.add_argument("--tokenizer", default="char", choices=["char", "bpe"])
84
+ parser.add_argument("--vocab-size", type=int, default=512, help="ukuran vocab untuk tokenizer bpe")
85
  parser.add_argument("--val-fraction", type=float, default=0.1, help="proporsi file untuk validasi")
86
  args = parser.parse_args()
87
 
 
104
 
105
  train_text = "".join(read_clean(p) for p in files[n_val:])
106
  val_text = "".join(read_clean(p) for p in files[:n_val])
107
+ all_text = train_text + val_text
 
 
 
 
 
 
 
 
108
 
109
  init_state = None
110
  init_opt = None
111
  start_step = 0
112
+ init_meta = None
113
+ config = None
114
  if args.init_from:
115
+ init_state, init_meta, init_opt = load_init(args.init_from)
116
+ config = GPTConfig(**init_meta["config"])
117
+ start_step = init_meta.get("step", 0)
118
  print(f"melanjutkan dari {args.init_from} (step {start_step})")
119
+ tokenizer = build_tokenizer(init_meta.get("tokenizer") or {"type": "char"}, init_meta["vocab"])
120
+ tinfo = init_meta.get("tokenizer") or {"type": "char"}
121
  else:
122
+ if args.tokenizer == "bpe":
123
+ from indigo.bpe import BPETokenizer
124
+
125
+ tokenizer = BPETokenizer.train(all_text, args.vocab_size)
126
+ tinfo = tokenizer.state()
127
+ n_chars = len(all_text.encode("utf-8"))
128
+ print(
129
+ f"tokenizer=bpe | vocab={tokenizer.vocab_size} | "
130
+ f"kompresi {n_chars:,} karakter -> rasio {n_chars / max(1, len(tokenizer.encode(all_text))):.2f}x"
131
+ )
132
+ else:
133
+ tokenizer = CharTokenizer.from_text(all_text)
134
+ tinfo = {"type": "char"}
135
+ if config is None:
136
+ config = GPTConfig(
137
+ vocab_size=tokenizer.vocab_size,
138
+ block_size=args.block_size,
139
+ n_layer=args.n_layer,
140
+ n_head=args.n_head,
141
+ n_embd=args.n_embd,
142
+ dropout=args.dropout,
143
+ )
144
  if config.vocab_size != tokenizer.vocab_size:
145
  raise SystemExit(
146
+ f"vocab tidak cocok: checkpoint={config.vocab_size}, tokenizer={tokenizer.vocab_size}"
147
  )
148
+
149
+ train_data = torch.tensor(tokenizer.encode(train_text), dtype=torch.long)
150
+ val_data = torch.tensor(tokenizer.encode(val_text), dtype=torch.long)
151
+ if len(train_data) < args.block_size * 2:
152
+ raise SystemExit(f"data latih terlalu pendek ({len(train_data)} token), minimal {args.block_size * 2}")
153
+ print(
154
+ f"tokens latih={len(train_data):,} | tokens validasi={len(val_data):,}"
155
+ )
156
+
157
  model = GPT(config)
158
  if init_state is not None:
159
  missing, unexpected = model.load_state_dict(init_state, strict=False)
 
179
  def save_model(base_path, val_loss):
180
  tensors = {k: v.detach().clone().contiguous() for k, v in model.state_dict().items()}
181
  save_file(tensors, base_path)
182
+ save_meta(
183
+ base_path,
184
+ config.__dict__,
185
+ tokenizer.itos if hasattr(tokenizer, "itos") else None,
186
+ total_steps,
187
+ val_loss,
188
+ backend="pytorch",
189
+ tokenizer=tinfo,
190
+ )
191
 
192
  def lr_at(step):
193
  if step < args.warmup: