Add Tiny LLM weights, code and model card
Browse files
README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
library_name: pytorch
|
| 6 |
+
pipeline_tag: text-generation
|
| 7 |
+
tags:
|
| 8 |
+
- tiny
|
| 9 |
+
- character-level
|
| 10 |
+
- gpt
|
| 11 |
+
- from-scratch
|
| 12 |
+
- educational
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
# Tiny LLM
|
| 16 |
+
|
| 17 |
+
A very small GPT-style language model, trained from scratch in about 15 minutes
|
| 18 |
+
on a laptop CPU. It writes one character at a time and only knows simple
|
| 19 |
+
greetings: hello, how are you, what is your name, thanks, goodbye.
|
| 20 |
+
|
| 21 |
+
It is a learning project, not a useful assistant. Anything that is not a
|
| 22 |
+
greeting gets an unrelated greeting-style reply.
|
| 23 |
+
|
| 24 |
+
**Try it in your browser:** [Tiny LLM demo Space](https://huggingface.co/spaces/Neural-Labs/NerualLabs)
|
| 25 |
+
|
| 26 |
+
## Model details
|
| 27 |
+
|
| 28 |
+
| | |
|
| 29 |
+
| --- | --- |
|
| 30 |
+
| Type | Decoder-only transformer (GPT-style), character level |
|
| 31 |
+
| Parameters | 824,120 |
|
| 32 |
+
| Layers | 4 |
|
| 33 |
+
| Attention heads | 4 |
|
| 34 |
+
| Embedding width | 128 |
|
| 35 |
+
| Context length | 128 characters |
|
| 36 |
+
| Vocabulary | 56 characters (the distinct characters in the training text) |
|
| 37 |
+
| Language | English |
|
| 38 |
+
| License | MIT |
|
| 39 |
+
|
| 40 |
+
Each block uses pre-layer-norm, causal self-attention, and an MLP
|
| 41 |
+
(128 → 512 → 128) with GELU. Token and position embeddings are both learned.
|
| 42 |
+
|
| 43 |
+
## Files
|
| 44 |
+
|
| 45 |
+
| File | What it is |
|
| 46 |
+
| --- | --- |
|
| 47 |
+
| `model.pt` | Trained weights: a dict with `state_dict`, `chars` (the vocabulary) and `config` |
|
| 48 |
+
| `model.py` | The model code in PyTorch, about 60 lines |
|
| 49 |
+
|
| 50 |
+
## How to use
|
| 51 |
+
|
| 52 |
+
This is a custom PyTorch model, so it does not load with `transformers`.
|
| 53 |
+
Download `model.py` and `model.pt`, then:
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
pip install torch huggingface_hub
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
```python
|
| 60 |
+
import torch
|
| 61 |
+
from huggingface_hub import hf_hub_download
|
| 62 |
+
|
| 63 |
+
repo = "Neural-Labs/tiny-llm"
|
| 64 |
+
hf_hub_download(repo, "model.py", local_dir=".")
|
| 65 |
+
weights = hf_hub_download(repo, "model.pt")
|
| 66 |
+
|
| 67 |
+
from model import TinyGPT
|
| 68 |
+
|
| 69 |
+
ckpt = torch.load(weights, map_location="cpu", weights_only=True)
|
| 70 |
+
chars = ckpt["chars"]
|
| 71 |
+
stoi = {c: i for i, c in enumerate(chars)}
|
| 72 |
+
model = TinyGPT(len(chars), **ckpt["config"])
|
| 73 |
+
model.load_state_dict(ckpt["state_dict"])
|
| 74 |
+
model.eval()
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def reply(message):
|
| 78 |
+
# Characters the model never saw are dropped.
|
| 79 |
+
clean = "".join(c for c in message.replace("\n", " ") if c in stoi).strip() or "hi"
|
| 80 |
+
prompt = f"User: {clean[:80]}\nBot:"
|
| 81 |
+
idx = torch.tensor([[stoi[c] for c in prompt]])
|
| 82 |
+
out = ""
|
| 83 |
+
for tok in model.generate(idx, max_new_tokens=100, temperature=0.5, top_k=5):
|
| 84 |
+
if chars[tok] == "\n": # end of the bot's line
|
| 85 |
+
break
|
| 86 |
+
out += chars[tok]
|
| 87 |
+
return out.strip()
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
print(reply("Hello!"))
|
| 91 |
+
print(reply("How are you?"))
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
The prompt format must match the training data: `User: <message>\nBot:`.
|
| 95 |
+
Generation stops at the first newline.
|
| 96 |
+
|
| 97 |
+
## Training data
|
| 98 |
+
|
| 99 |
+
12,000 generated exchanges in the form
|
| 100 |
+
|
| 101 |
+
```
|
| 102 |
+
User: <message>
|
| 103 |
+
Bot: <reply>
|
| 104 |
+
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
built from a short hand-written list of greeting phrases and replies
|
| 108 |
+
(hello, how are you, what is your name, thanks, goodbye, and similar).
|
| 109 |
+
No external dataset was used.
|
| 110 |
+
|
| 111 |
+
## Training
|
| 112 |
+
|
| 113 |
+
| | |
|
| 114 |
+
| --- | --- |
|
| 115 |
+
| Steps | 4,000 |
|
| 116 |
+
| Batch size | 32 |
|
| 117 |
+
| Optimizer | AdamW |
|
| 118 |
+
| Learning rate | 1e-3 with cosine decay |
|
| 119 |
+
| Hardware | 4-core CPU |
|
| 120 |
+
| Time | 14.5 minutes |
|
| 121 |
+
| Final loss | train 0.165, validation 0.170 |
|
| 122 |
+
|
| 123 |
+
## Limitations
|
| 124 |
+
|
| 125 |
+
- It only knows greetings. Any other message gets a greeting-style reply that
|
| 126 |
+
has nothing to do with the question.
|
| 127 |
+
- It works on characters, and drops any character that was not in its training
|
| 128 |
+
text (for example most non-English letters and emoji).
|
| 129 |
+
- It has no memory of earlier messages: each reply sees only the latest message.
|
| 130 |
+
- It is far too small to know facts, follow instructions, or reason.
|
model.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a6b71ffe18697f2e8fd1afdbd8658ad4734a1d2c82d66f9353f8ecef78c71e87
|
| 3 |
+
size 3311561
|
model.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""A very small GPT-style language model (character level)."""
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Block(nn.Module):
|
| 8 |
+
"""One transformer block: self-attention, then a small feed-forward net."""
|
| 9 |
+
|
| 10 |
+
def __init__(self, n_embd, n_head, dropout):
|
| 11 |
+
super().__init__()
|
| 12 |
+
self.n_head = n_head
|
| 13 |
+
self.ln1 = nn.LayerNorm(n_embd)
|
| 14 |
+
self.qkv = nn.Linear(n_embd, 3 * n_embd)
|
| 15 |
+
self.proj = nn.Linear(n_embd, n_embd)
|
| 16 |
+
self.ln2 = nn.LayerNorm(n_embd)
|
| 17 |
+
self.mlp = nn.Sequential(
|
| 18 |
+
nn.Linear(n_embd, 4 * n_embd), nn.GELU(), nn.Linear(4 * n_embd, n_embd)
|
| 19 |
+
)
|
| 20 |
+
self.dropout = dropout
|
| 21 |
+
|
| 22 |
+
def forward(self, x):
|
| 23 |
+
B, T, C = x.shape
|
| 24 |
+
q, k, v = self.qkv(self.ln1(x)).split(C, dim=2)
|
| 25 |
+
q, k, v = (t.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) for t in (q, k, v))
|
| 26 |
+
# is_causal=True: each character may only look at the ones before it
|
| 27 |
+
a = F.scaled_dot_product_attention(
|
| 28 |
+
q, k, v, is_causal=True, dropout_p=self.dropout if self.training else 0.0
|
| 29 |
+
)
|
| 30 |
+
x = x + self.proj(a.transpose(1, 2).reshape(B, T, C))
|
| 31 |
+
return x + self.mlp(self.ln2(x))
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class TinyGPT(nn.Module):
|
| 35 |
+
def __init__(self, vocab_size, block_size=128, n_layer=4, n_head=4, n_embd=128, dropout=0.1):
|
| 36 |
+
super().__init__()
|
| 37 |
+
self.block_size = block_size
|
| 38 |
+
self.tok_emb = nn.Embedding(vocab_size, n_embd)
|
| 39 |
+
self.pos_emb = nn.Embedding(block_size, n_embd)
|
| 40 |
+
self.blocks = nn.ModuleList(Block(n_embd, n_head, dropout) for _ in range(n_layer))
|
| 41 |
+
self.ln_f = nn.LayerNorm(n_embd)
|
| 42 |
+
self.head = nn.Linear(n_embd, vocab_size)
|
| 43 |
+
|
| 44 |
+
def forward(self, idx, targets=None):
|
| 45 |
+
T = idx.shape[1]
|
| 46 |
+
x = self.tok_emb(idx) + self.pos_emb(torch.arange(T, device=idx.device))
|
| 47 |
+
for block in self.blocks:
|
| 48 |
+
x = block(x)
|
| 49 |
+
logits = self.head(self.ln_f(x))
|
| 50 |
+
loss = None
|
| 51 |
+
if targets is not None:
|
| 52 |
+
loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
|
| 53 |
+
return logits, loss
|
| 54 |
+
|
| 55 |
+
@torch.no_grad()
|
| 56 |
+
def generate(self, idx, max_new_tokens, temperature=0.8, top_k=None):
|
| 57 |
+
"""Yield one new token id at a time, feeding each back in as input."""
|
| 58 |
+
for _ in range(max_new_tokens):
|
| 59 |
+
logits, _ = self(idx[:, -self.block_size:])
|
| 60 |
+
logits = logits[:, -1, :] / max(temperature, 1e-5)
|
| 61 |
+
if top_k:
|
| 62 |
+
kth = torch.topk(logits, min(top_k, logits.size(-1))).values[:, [-1]]
|
| 63 |
+
logits[logits < kth] = -float("inf")
|
| 64 |
+
nxt = torch.multinomial(F.softmax(logits, dim=-1), 1)
|
| 65 |
+
idx = torch.cat([idx, nxt], dim=1)
|
| 66 |
+
yield nxt.item()
|