SONUB home EN English 한국어 日本語 中文 हिन्दी বাংলা اردو Español Português Français Deutsch Русский العربية Bahasa Indonesia Tiếng Việt ไทย Türkçe

How to enable notifications

Notifications are unavailable in this browser or on this address. Try a supported browser on the secure SONUB site.

If notifications are blocked, allow them in your browser settings.

  1. Open the site controls beside the address bar, or your browser’s site settings. Set Notifications for this site to Allow.
  2. In your device settings, allow notifications for your browser and check Do Not Disturb or Focus.
  3. Return to SONUB, reload the page, and use the bell to enable notifications.

On iPhone or iPad, add SONUB to your Home Screen, open it there, then allow notifications.

XSML · From your first line to a conversation

Now, let's build your very own AI.

Type your own language model in this browser: prepare text, train a tiny GPT, then run your Runner and XChat. No pretrained weights or external AI API.

5

GPT architecture

Write a complete tiny decoder Transformer and its gradients.

looks up a of numbers for each token. A position vector records where it appears. Query asks what to look for, key describes each earlier position, and value carries its information. weights combine those values.

The model uses one causal attention head and one block. keys and values contain only the current and earlier positions, so the model cannot peek ahead. Residual additions preserve the input; a two-layer MLP changes features. Layer normalization stabilizes their scale; the output head scores vocabulary tokens. For readable , this version uses , -free normalization and no dropout.

Type and try

Write engine.py first and run its derivative check; then write model.py. In Value, each operation remembers its local derivative. backward visits the graph in reverse order and adds contributions, so shared parameters learn from every use. Trace token → position → attention → MLP → head in forward.

cd ~/xsml
nano engine.py
nano model.py
python3 engine.py
python3 -c "from model import GPT; m=GPT(10, 8, 12); print(len(m.forward([1, 2])), len(m.forward([1, 2])[0]))"
engine.pyComplete source to type

data is a number; grad is its effect on the loss; parents store local derivatives. Addition distributes a gradient; multiplication scales it by the other input. softmax converts scores to probabilities.

import math


class Value:
    def __init__(self, data, parents=()):
        self.data = float(data)
        self.grad = 0.0
        self.parents = parents

    def __add__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        return Value(self.data + other.data, ((self, 1), (other, 1)))

    def __mul__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        return Value(self.data * other.data,
                     ((self, other.data), (other, self.data)))

    def __pow__(self, power):
        return Value(self.data ** power,
                     ((self, power * self.data ** (power - 1)),))

    def __neg__(self):
        return self * -1

    def __sub__(self, other):
        return self + (-other)

    def __truediv__(self, other):
        return self * (other ** -1)

    __radd__ = __add__
    __rmul__ = __mul__

    def exp(self):
        result = math.exp(self.data)
        return Value(result, ((self, result),))

    def log(self):
        return Value(math.log(self.data), ((self, 1 / self.data),))

    def relu(self):
        return Value(max(0, self.data), ((self, float(self.data > 0)),))

    def backward(self):
        order, seen, stack = [], set(), [(self, False)]
        while stack:
            node, expanded = stack.pop()
            if expanded:
                order.append(node)
            elif node not in seen:
                seen.add(node)
                stack.append((node, True))
                stack.extend((parent, False) for parent, _ in node.parents)
        for node in order:
            node.grad = 0.0
        self.grad = 1.0
        for node in reversed(order):
            for parent, derivative in node.parents:
                parent.grad += derivative * node.grad


def softmax(values):
    peak = max(v.data for v in values)
    exps = [(v - peak).exp() for v in values]
    total = sum(exps)
    return [v / total for v in exps]


def linear(x, weights):
    return [sum(a * b for a, b in zip(x, row)) for row in weights]


def norm(x):
    mean = sum(x) / len(x)
    variance = sum((v - mean) ** 2 for v in x) / len(x)
    return [(v - mean) * (variance + 1e-5) ** -0.5 for v in x]


if __name__ == '__main__':
    x = Value(3)
    y = x * x + 2 * x
    y.backward()
    print('y =', y.data, 'dy/dx =', x.grad)
    assert y.data == 15 and x.grad == 8
model.pyComplete source to type

Read shapes first, then forward. Every Value in weights is trainable. Lists store vectors and matrices. loss compares each position with its next-token target. state and load serialize numbers, not Python objects.

import random
from engine import Value, linear, norm, softmax


class GPT:
    def __init__(self, vocab_size, width, context, seed=7):
        self.width, self.context = width, context
        rng = random.Random(seed)
        shapes = {
            'token': (vocab_size, width), 'position': (context, width),
            'query': (width, width), 'key': (width, width),
            'value': (width, width), 'project': (width, width),
            'expand': (2 * width, width), 'contract': (width, 2 * width),
            'head': (vocab_size, width),
        }
        self.weights = {
            name: [[Value(rng.gauss(0, 0.1)) for _ in range(cols)]
                   for _ in range(rows)] for name, (rows, cols) in shapes.items()
        }
        self.parameters = [v for matrix in self.weights.values()
                           for row in matrix for v in row]

    def forward(self, ids):
        assert 0 < len(ids) <= self.context
        w, keys, values, output = self.weights, [], [], []
        for position, token in enumerate(ids):
            x = [a + b for a, b in zip(w['token'][token], w['position'][position])]
            h = norm(x)
            q = linear(h, w['query'])
            keys.append(linear(h, w['key']))
            values.append(linear(h, w['value']))
            scores = [sum(a * b for a, b in zip(q, k)) / self.width ** 0.5
                      for k in keys]
            attention = softmax(scores)
            mixed = [sum(p * v[i] for p, v in zip(attention, values))
                     for i in range(self.width)]
            x = [a + b for a, b in zip(x, linear(mixed, w['project']))]
            hidden = [v.relu() for v in linear(norm(x), w['expand'])]
            x = [a + b for a, b in zip(x, linear(hidden, w['contract']))]
            output.append(linear(norm(x), w['head']))
        return output

    def loss(self, inputs, targets):
        logits = self.forward(inputs)
        losses = [-softmax(row)[target].log()
                  for row, target in zip(logits, targets)]
        return sum(losses) / len(losses)

    def state(self):
        return {name: [[v.data for v in row] for row in matrix]
                for name, matrix in self.weights.items()}

    def load(self, state):
        assert set(state) == set(self.weights), 'checkpoint keys'
        for name, matrix in self.weights.items():
            assert len(state[name]) == len(matrix), 'checkpoint rows'
            for row, saved in zip(matrix, state[name]):
                assert len(saved) == len(row), 'checkpoint columns'
                for value, number in zip(row, saved):
                    value.data = float(number)

Check your understanding

engine.py must print y = 15.0 and dy/dx = 8.0. The model shape check prints 2 10: two positions, ten vocabulary scores each. Explain why appending a future token must not change the earlier scores.

If you get stuck

Keep matrix rows and zip inputs aligned. A count-based bigram is not a substitute for these layers. This model is deliberately tiny; all embedding, attention, MLP and head weights still learn through backpropagation.