Raw text
→ TextPreprocessor (lowercase, expand contractions, synonyms, stop-word removal)
→ HybridTokenizer (word-level lookup + WordPiece fallback, 1,155 tokens)
→ Transformer (3-layer encoder, embed_dim=64, 4 heads)
→ RegressionHead (pool → Linear → SiLU → Linear → Tanh → rescale)
→ 9 float params
→ SVG German Shepherd animates"don't" → "do not"), strips punctuation, applies a domain synonym map ("furious" → "angry", "doggo" → "dog"), removes stop words while preserving emotion-bearing terms ("not", "very", "never", etc.)[START] <tokens> [SEP] [END] [PAD…] (max 32 tokens).[0,1] params from (-1,1) range.| # | Parameter | Range | Meaning |
|---|---|---|---|
| 0 | mouth | [-1, 1] | -1 = deep frown, 1 = big smile |
| 1 | eyebrow | [-1, 1] | -1 = angry/furrowed, 1 = raised/worried |
| 2 | eyelid | [0, 1] | 0 = fully open, 1 = fully closed |
| 3 | pupil | [0, 1] | 0 = constricted, 1 = dilated |
| 4 | ear | [-1, 1] | -1 = perked/alert, 1 = droopy/sad |
| 5 | tail | [0, 1] | 0 = still, 1 = wagging fast |
| 6 | tongue | [0, 1] | 0 = hidden, 1 = fully out |
| 7 | blush | [0, 1] | 0 = none, 1 = visible cheek blush |
| 8 | tilt | [-1, 1] | -1 = tilt left, 1 = tilt right |
[0.0, 0.0, 0.5, 0.5, 0.0, 0.3, 0.0, 0.0, 0.0]DogEmotionModel
├── DogModelEmbeddings
│ ├── Token embedding (1155 × 64)
│ ├── Learned positional (32 × 64)
│ └── State projection Linear(9 → 64) ← previous dog state as token
├── MiniTransformerEncoder (3 × TransformerEncoderLayer)
│ ├── MultiHeadAttention (4 heads, embed_dim=64)
│ ├── FeedForward (64 → 128 → 64, SiLU)
│ ├── LayerNorm × 2
│ └── Dropout (0.1)
└── RegressionHead
├── Global average pool
├── Linear(64 → 32) + SiLU + Dropout
├── Linear(32 → 9) + Tanh
└── Rescale [0,1] params: (x + 1) / 2| Property | Value |
|---|---|
| Total parameters | 178,825 |
| Embedding dim | 64 |
| Attention heads | 4 |
| Transformer layers | 3 |
| Feedforward dim | 128 |
| Max sequence length | 32 |
| Vocab size | 1,155 tokens |
| Activation | SiLU |
| Output activation | Tanh + rescale |
| ONNX opset | 18 |
| Model size (ONNX) | 0.88 MB |
| Model size (PyTorch) | 0.70 MB |
| Property | Value |
|---|---|
| Training samples | 20,000 (synthetic) |
| Validation samples | 2,000 |
| Test samples | 2,000 |
| Emotion categories | 15 |
| Optimizer | AdamW |
| Learning rate | 3e-4 |
| Weight decay | 1e-4 |
| Batch size | 64 |
| Epochs trained | 100 (early stop at epoch 89) |
| Scheduler | Cosine annealing with 200-step linear warmup |
| Loss | Weighted MSE (mouth ×2.0, eyebrow ×1.5, others ×1.0, blush/tilt ×0.8) |
| Epoch | Train Loss | Val Loss |
|---|---|---|
| 1 | 0.1758 | 0.1141 |
| 10 | 0.0324 | 0.0225 |
| 25 | 0.0177 | 0.0117 |
| 50 | 0.0120 | 0.0073 |
| 75 | 0.0097 | 0.0059 |
| 89 ⭐ | 0.0093 | 0.0057 |
"wagging" → ["wag", "##g", "##ing"]).vocab.json (1,155 tokens). WordPiece token IDs are offset by the word vocab size to prevent collisions.[PAD]=0 [UNK]=1 [START]=2 [END]=3 [SEP]=4static/index.html) runs the full inference pipeline client-side using ONNX Runtime Web. No server round-trips after initial page load.Page load
→ fetch vocab.json + contractions.json + synonym_map.json (~50 KB total)
→ fetch model.onnx (0.88 MB)
→ ort.InferenceSession.create() (WASM compile)
→ input unlocks, readystatic/index.html is a self-contained frontend. To run it with any HTTP server:1# Clone the repo
2git clone https://huggingface.co/spaces/your-username/useless-dog-178K-onnx
3cd useless-dog-178K-onnx
4
5# Install and run
6pip install flask
7python server.py
8# Open http://localhost:7860requestAnimationFrame sinusoidal loop1import json, re, numpy as np
2import onnxruntime as ort
3
4# Load assets
5with open("hf_export/tokenizer/vocab.json") as f: vocab = json.load(f)
6with open("hf_export/tokenizer/tokenizer_config.json") as f: cfg = json.load(f)
7with open("hf_export/preprocessor/contractions.json") as f: contractions = json.load(f)
8with open("hf_export/preprocessor/synonym_map.json") as f:
9 raw = json.load(f)
10 synonym_map = {}
11 for cat in raw.values(): synonym_map.update(cat)
12
13MAX_SEQ_LEN = cfg["max_seq_len"] # 32
14PAD, START, END, SEP, UNK = (vocab[t] for t in ["[PAD]","[START]","[END]","[SEP]","[UNK]"])
15
16STOP = {"the","a","an","please","can","you","i","want","to","my","your","is","are",
17 "was","be","have","has","do","will","would","of","in","on","at","for",
18 "with","by","as","it","he","she","they","we"}
19KEEP = {"not","more","less","very","slightly","now","also","but","and","too","no","never","stop"}
20
21def preprocess(text):
22 text = text.lower()
23 for c, e in contractions.items(): text = text.split(c).join(e) if c in text else text
24 text = re.sub(r"[^a-z0-9\s]", " ", text).strip()
25 words = [synonym_map.get(w, w) for w in text.split()]
26 return [w for w in words if w in KEEP or w not in STOP]
27
28def encode(words):
29 ids = [vocab.get(w, UNK) for w in words][: MAX_SEQ_LEN - 3]
30 seq = [START] + ids + [SEP, END]
31 pad = MAX_SEQ_LEN - len(seq)
32 return seq + [PAD]*pad, [1]*len(seq) + [0]*pad
33
34sess = ort.InferenceSession("hf_export/model.onnx", providers=["CPUExecutionProvider"])
35NEUTRAL = [0.0, 0.0, 0.5, 0.5, 0.0, 0.3, 0.0, 0.0, 0.0]
36state = NEUTRAL[:]
37NAMES = ["mouth","eyebrow","eyelid","pupil","ear","tail","tongue","blush","tilt"]
38
39def predict(text):
40 global state
41 words = preprocess(text) or ["neutral"]
42 ids, mask = encode(words)
43 out = sess.run(["params"], {
44 "input_ids": np.array([ids], dtype=np.int64),
45 "attention_mask": np.array([mask], dtype=np.int64),
46 "previous_state": np.array([state], dtype=np.float32),
47 })
48 state = out[0][0].tolist()
49 return dict(zip(NAMES, state))
50
51print(predict("make the dog happy and excited"))
52# {'mouth': 0.72, 'eyebrow': 0.18, 'eyelid': 0.12, 'pupil': 0.81,
53# 'ear': -0.43, 'tail': 0.91, 'tongue': 0.67, 'blush': 0.44, 'tilt': 0.09}1pip install -r requirements.txt
2python export_to_hf.py --verify
3# Outputs: hf_export/model.onnx (single self-contained file, no .data sidecar)model/ Transformer architecture (built from scratch)
dog_model.py Full model: embeddings + transformer + head
transformer.py MiniTransformerEncoder (3-layer)
embeddings.py Token + positional + state embeddings
regression_head.py Pool → MLP → Tanh → rescale
tokenizer/ Custom tokenizer (no HuggingFace tokenizers library)
word_tokenizer.py Word-level vocab lookup
wordpiece_tokenizer.py WordPiece trained from scratch
hybrid_tokenizer.py Combines both into unified vocab
vocab.json Unified 1,155-token vocab
preprocessor/
preprocessor.py Text normalization pipeline
contractions.json Contraction expansion map
synonym_map.json Domain synonym normalization
training/
trainer.py Training loop (AdamW, cosine LR, weighted MSE, early stop)
config.py All hyperparameters
data/
dataset_generator.py Synthetic dataset generator (15 categories, 20K samples)
generated/ train.json / val.json / test.json
hf_export/ Browser-ready package
model.onnx Self-contained ONNX model (0.88 MB)
config.json Architecture + param metadata
tokenizer/ vocab.json + tokenizer_config.json
preprocessor/ contractions.json + synonym_map.json
static/
index.html Full-page frontend (nature scene + SVG dog + HUD)
checkpoints/
best_model.pt PyTorch checkpoint (epoch 89)
training_log.json Full train/val loss history
server.py Flask server (static file serving only)
export_to_hf.py ONNX export + HF packaging script
requirements.txt Python dependencies
Dockerfile HuggingFace Docker Space config| Component | Choice | Why |
|---|---|---|
| Model framework | PyTorch (from scratch) | Full control over architecture, no pretrained weight baggage |
| Tokenizer | Custom Hybrid (word + WordPiece) | Domain vocab is small and fixed; no need for a general tokenizer |
| Activation | SiLU | Smoother gradients than ReLU, better for small models |
| Output | Tanh + rescale | Constrains all 9 params to their valid ranges in one pass |
| Loss | Weighted MSE | Mouth and eyebrow are most visually impactful — given higher weight |
| Browser inference | ONNX Runtime Web (WASM) | Runs on any device, no server needed, 0.88 MB model loads in ~1s |
| Frontend | Pure SVG + CSS | No canvas library, no React — just math driving SVG attributes |
| Server | Flask | Minimal — only serves static files, all inference is client-side |
1@misc{saravanan2025uselessdog,
2 author = {Saravanan Arjuna Ravi},
3 title = {useless-dog-178K-onnx: Natural Language to Animated German Shepherd via Tiny Transformer},
4 year = {2026},
5 publisher = {Hugging Face},
6 howpublished = {\url{https://huggingface.co/spaces/saravanan-arjuna-ravi/useless-dog-178K-onnx}}
7}