Supernova11c's picture
Publish Supernova Mathos V6.1
77784b9 verified
Raw History Blame Contribute Delete
12.9 kB
import re
import numpy as np
from fractions import Fraction
HIDDEN = 128
BATCH_SIZE = 128
LEARNING_RATE = 0.003
OP_NAMES = ['+', '-', '*', '/']
INPUT_SIZE = 34
def encode(a, b, state, op):
x = np.zeros(INPUT_SIZE, dtype=np.float64)
x[a] = 1.0
x[10 + b] = 1.0
x[20 + state] = 1.0
x[30 + op] = 1.0
return x
def build_data():
datasets = {op: [] for op in range(4)}
for a in range(10):
for b in range(10):
for carry in range(2):
total = a + b + carry
datasets[0].append((encode(a, b, carry, 0), total % 10, total // 10))
for a in range(10):
for b in range(10):
for borrow in range(2):
total = a - b - borrow
digit = total % 10
next_borrow = int(total < 0)
datasets[1].append((encode(a, b, borrow, 1), digit, next_borrow))
for a in range(10):
for b in range(10):
for carry in range(9):
total = a * b + carry
datasets[2].append((encode(a, b, carry, 2), total % 10, total // 10))
for divisor in range(1, 10):
for remainder in range(divisor):
for next_digit in range(10):
current = remainder * 10 + next_digit
quotient_digit = current // divisor
next_remainder = current % divisor
datasets[3].append((encode(next_digit, divisor, remainder, 3), quotient_digit, next_remainder))
result = {}
for op, rows in datasets.items():
result[op] = (np.stack([r[0] for r in rows]), np.array([r[1] for r in rows], dtype=np.int64), np.array([r[2] for r in rows], dtype=np.int64))
return result
DATA = build_data()
def softmax(z):
z = z - np.max(z, axis=1, keepdims=True)
e = np.exp(z)
return e / np.sum(e, axis=1, keepdims=True)
class MathosV6:
def __init__(self, seed=77):
r = np.random.default_rng(seed)
def init(nin, nout):
limit = np.sqrt(6.0 / (nin + nout))
return r.uniform(-limit, limit, (nin, nout))
self.p = {'W1': init(INPUT_SIZE, HIDDEN), 'b1': np.zeros((1, HIDDEN)), 'Wd': init(HIDDEN, 10), 'bd': np.zeros((1, 10)), 'Ws': init(HIDDEN, 10), 'bs': np.zeros((1, 10))}
self.m = {k: np.zeros_like(v) for k, v in self.p.items()}
self.v = {k: np.zeros_like(v) for k, v in self.p.items()}
self.step = 0
self.experiences_seen = 0
def forward(self, X):
h = np.tanh(X @ self.p['W1'] + self.p['b1'])
return (h, h @ self.p['Wd'] + self.p['bd'], h @ self.p['Ws'] + self.p['bs'])
def train_batch(self, X, yd, ys):
n = len(X)
h, dl, sl = self.forward(X)
pd, ps = (softmax(dl), softmax(sl))
rows = np.arange(n)
gd, gs = (pd.copy(), ps.copy())
gd[rows, yd] -= 1.0
gs[rows, ys] -= 1.0
gd /= n
gs /= n
grads = {'Wd': h.T @ gd, 'bd': gd.sum(axis=0, keepdims=True), 'Ws': h.T @ gs, 'bs': gs.sum(axis=0, keepdims=True)}
dh = gd @ self.p['Wd'].T + gs @ self.p['Ws'].T
dz = dh * (1.0 - h * h)
grads['W1'] = X.T @ dz
grads['b1'] = dz.sum(axis=0, keepdims=True)
self.step += 1
for k in self.p:
g = grads[k]
self.m[k] = 0.9 * self.m[k] + 0.1 * g
self.v[k] = 0.999 * self.v[k] + 0.001 * g * g
mhat = self.m[k] / (1.0 - 0.9 ** self.step)
vhat = self.v[k] / (1.0 - 0.999 ** self.step)
self.p[k] -= LEARNING_RATE * mhat / (np.sqrt(vhat) + 1e-08)
self.experiences_seen += n
loss_d = -np.log(pd[rows, yd] + 1e-12).mean()
loss_s = -np.log(ps[rows, ys] + 1e-12).mean()
return float(loss_d + loss_s)
def transition(self, a, b, state, op):
x = encode(a, b, state, op).reshape(1, -1)
_, dl, sl = self.forward(x)
return (int(dl[0].argmax()), int(sl[0].argmax()))
def transition_accuracy(self, op=None):
selected = range(4) if op is None else [op]
correct, total = (0, 0)
for operation in selected:
X, yd, ys = DATA[operation]
_, dl, sl = self.forward(X)
pd, ps = (dl.argmax(axis=1), sl.argmax(axis=1))
correct += int(np.sum((pd == yd) & (ps == ys)))
total += len(X)
return correct / total
def train_operation(self, op, epochs, seed):
X, yd, ys = DATA[op]
r = np.random.default_rng(seed)
name = OP_NAMES[op]
for ep in range(1, epochs + 1):
order = r.permutation(len(X))
total_loss = 0.0
for start in range(0, len(X), BATCH_SIZE):
ids = order[start:start + BATCH_SIZE]
loss = self.train_batch(X[ids], yd[ids], ys[ids])
total_loss += loss * len(ids)
total_loss /= len(X)
if ep == 1 or ep % 50 == 0 or ep == epochs:
acc = self.transition_accuracy(op)
print(f'Learn {name} | Epoch {ep:03d}/{epochs} | loss={total_loss:.5f} | transition accuracy={100 * acc:.2f}%')
def add(self, a, b):
if a < 0 or b < 0:
raise ValueError('Addition accepts nonnegative integers.')
result, carry, pos = (0, 0, 0)
while pos < max(len(str(a)), len(str(b))) or carry:
da = a // 10 ** pos % 10
db = b // 10 ** pos % 10
digit, carry = self.transition(da, db, carry, 0)
result += digit * 10 ** pos
pos += 1
return result
def subtract(self, a, b):
if a < b or b < 0:
raise ValueError('Subtraction requires a >= b >= 0.')
result, borrow = (0, 0)
for pos in range(len(str(a))):
da = a // 10 ** pos % 10
db = b // 10 ** pos % 10
digit, borrow = self.transition(da, db, borrow, 1)
result += digit * 10 ** pos
if borrow != 0:
raise ArithmeticError('Learned subtraction ended with a borrow.')
return result
def multiply_digit(self, n, digit):
if not 0 <= digit <= 9:
raise ValueError('Single-digit multiplier must be 0..9.')
result, carry, pos = (0, 0, 0)
while pos < len(str(n)) or carry:
da = n // 10 ** pos % 10
out_digit, carry = self.transition(da, digit, carry, 2)
result += out_digit * 10 ** pos
pos += 1
return result
def multiply(self, a, b):
if a < 0 or b < 0:
raise ValueError('Multiplication accepts nonnegative integers.')
result = 0
for pos, ch in enumerate(str(b)[::-1]):
partial = self.multiply_digit(a, int(ch))
for _ in range(pos):
partial *= 10
result = self.add(result, partial)
return result
def divide(self, dividend, divisor):
if dividend < 0 or divisor <= 0:
raise ValueError('Division requires dividend >= 0, divisor > 0.')
quotient, remainder = (0, 0)
for ch in str(dividend):
digit = int(ch)
if divisor <= 9:
qdigit, new_remainder = self.transition(digit, divisor, remainder, 3)
else:
current = int(str(remainder) + ch)
qdigit = 0
for trial in range(9, -1, -1):
product = self.multiply(divisor, trial)
if product <= current:
qdigit = trial
break
product = self.multiply(divisor, qdigit)
new_remainder = self.subtract(current, product)
quotient = quotient * 10 + qdigit
remainder = new_remainder
return (quotient, remainder)
def save(self, path):
np.savez(path, **self.p, experiences_seen=np.array(self.experiences_seen))
def signed_add(model, x, y, subtract=False):
if subtract:
y = -y
sx = -1 if x < 0 else 1
sy = -1 if y < 0 else 1
ax, ay = (abs(x), abs(y))
if sx == sy:
return sx * model.add(ax, ay)
if ax >= ay:
return sx * model.subtract(ax, ay)
return sy * model.subtract(ay, ax)
def signed_multiply(model, x, y):
sign = -1 if (x < 0) != (y < 0) else 1
return sign * model.multiply(abs(x), abs(y))
def fraction_operation(model, left, right, op):
left, right = (Fraction(left), Fraction(right))
if op == '+' or op == '-':
n1 = signed_multiply(model, left.numerator, right.denominator)
n2 = signed_multiply(model, right.numerator, left.denominator)
numerator = signed_add(model, n1, n2, subtract=op == '-')
denominator = model.multiply(left.denominator, right.denominator)
return Fraction(numerator, denominator)
if op == '*':
numerator = signed_multiply(model, left.numerator, right.numerator)
denominator = model.multiply(left.denominator, right.denominator)
return Fraction(numerator, denominator)
if op == '/':
if right == 0:
raise ZeroDivisionError('Division by zero.')
numerator = signed_multiply(model, left.numerator, right.denominator)
denominator = signed_multiply(model, left.denominator, right.numerator)
return Fraction(numerator, denominator)
raise ValueError('Unsupported operation.')
def fraction_power(model, base, exponent):
base = Fraction(base)
exponent = Fraction(exponent)
if exponent.denominator != 1:
raise ValueError('Only integer powers are supported.')
power = exponent.numerator
if abs(power) > 20:
raise ValueError('Power magnitude is limited to 20.')
if power < 0:
if base == 0:
raise ZeroDivisionError('Zero cannot have a negative power.')
base = Fraction(base.denominator, base.numerator)
power = -power
result = Fraction(1, 1)
for _ in range(power):
result = fraction_operation(model, result, base, '*')
return result
def tokenize(expression):
cleaned = re.sub('\\s+', '', expression)
tokens = re.findall('\\d+|[()+*/^%-]', cleaned)
if ''.join(tokens) != cleaned:
raise ValueError('Invalid character in expression.')
return tokens
def bodmas(model, expression):
tokens = tokenize(expression)
index = 0
def parse_expression():
nonlocal index
value = parse_term()
while index < len(tokens) and tokens[index] in ('+', '-'):
op = tokens[index]
index += 1
rhs = parse_term()
value = fraction_operation(model, value, rhs, op)
return value
def parse_term():
nonlocal index
value = parse_power()
while index < len(tokens) and tokens[index] in ('*', '/'):
op = tokens[index]
index += 1
rhs = parse_power()
value = fraction_operation(model, value, rhs, op)
return value
def parse_power():
nonlocal index
value = parse_factor()
if index < len(tokens) and tokens[index] == '^':
index += 1
exponent = parse_power()
value = fraction_power(model, value, exponent)
return value
def parse_factor():
nonlocal index
if index >= len(tokens):
raise ValueError('Incomplete expression.')
if tokens[index] == '+':
index += 1
return parse_factor()
if tokens[index] == '-':
index += 1
return -parse_factor()
if tokens[index] == '(':
index += 1
value = parse_expression()
if index >= len(tokens) or tokens[index] != ')':
raise ValueError('Missing closing parenthesis.')
index += 1
return value
token = tokens[index]
if not token.isdigit():
raise ValueError(f'Unexpected token: {token}')
index += 1
return Fraction(int(token), 1)
result = parse_expression()
if index != len(tokens):
raise ValueError('Unexpected trailing tokens.')
return result
from importlib.resources import files
class mathos:
"""Supernova Mathos V6.1 with bundled trained NumPy weights."""
def __init__(self):
self.model = MathosV6(seed=77)
weight_path = files("supernova_mathos").joinpath(
"weights/supernova_mathos_v6_1.npz"
)
with np.load(weight_path) as saved:
self.model.p = {
key: saved[key].copy()
for key in ("W1", "b1", "Wd", "bd", "Ws", "bs")
}
if "experiences_seen" in saved:
self.model.experiences_seen = int(saved["experiences_seen"])
def __getattr__(self, name):
return getattr(self.model, name)
def calculate(self, expression):
return bodmas(self.model, expression)