morph-hrr / src /morph_hrr /tokenizer.py
thebasedcapital's picture
morph-hrr v0.1.0: compositional HRR morpheme tokenizer
783c92f verified
Raw
History Blame Contribute Delete
3.78 kB
"""Compositional HRR morpheme tokenizer.
Each word is represented as a single fixed-width "holistic" vector:
word_vec(word) = bundle(
prefix_role (x) bytes(prefix),
root_role (x) bytes(root),
suffix_role (x) bytes(suffix),
)
where ``(x)`` is circular convolution (``hrr.bind``) and ``bundle`` is normalized
superposition. Because the roles are unitary, ``unbind(word_vec, role)`` recovers
that role's filler — enabling algebraic morpheme manipulation (strip a prefix,
swap a suffix, build a vector for an out-of-vocabulary word from its pieces).
NOTE: this is an input **representation / embedding**, not a HuggingFace
text<->ID tokenizer. It emits dense vectors (one per word), intended to feed
HRR-native or experimental models.
"""
from __future__ import annotations
import re
import mlx.core as mx
import numpy as np
from .hrr import bind, bundle, make_unitary, normalize
from .morphemes import segment as _segment
_WORD_RE = re.compile(r"[A-Za-z]+|\S")
class MorphemeTokenizer:
"""Map text -> HRR morpheme vectors of width ``dim`` (deterministic given ``seed``)."""
def __init__(self, dim: int = 2048, seed: int = 0):
self.dim = dim
self.seed = seed
# Fixed random base vectors for each byte value; the "filler" alphabet.
rng = np.random.default_rng(seed)
byte_vectors = rng.normal(size=(256, dim)).astype(np.float32)
self.byte_vectors = normalize(mx.array(byte_vectors))
# Unitary role vectors for prefix / root / suffix slots.
self.prefix_role = make_unitary(dim, seed=seed + 1_001)
self.root_role = make_unitary(dim, seed=seed + 1_002)
self.suffix_role = make_unitary(dim, seed=seed + 1_003)
# -- morphemes ---------------------------------------------------------
def segment(self, word: str) -> tuple[str, str, str]:
"""Return ``(prefix, root, suffix)`` for ``word``."""
return _segment(word)
def bytes_vector(self, text: str) -> mx.array:
"""Fixed vector for a string: normalized fold-bind of its byte vectors."""
if not text:
return mx.zeros((self.dim,), dtype=mx.float32)
acc = self.byte_vectors[ord(text[0]) % 256]
for ch in text[1:]:
acc = bind(acc, self.byte_vectors[ord(ch) % 256])
return normalize(acc)
# -- composition -------------------------------------------------------
def word_vector(self, word: str) -> mx.array:
"""Holistic HRR vector for ``word`` (composes its morphemes by role)."""
prefix, root, suffix = _segment(word)
pieces: list[mx.array] = []
if prefix:
pieces.append(bind(self.prefix_role, self.bytes_vector(prefix)))
if root:
pieces.append(bind(self.root_role, self.bytes_vector(root)))
if suffix:
pieces.append(bind(self.suffix_role, self.bytes_vector(suffix)))
if not pieces:
return self.bytes_vector(word)
return bundle(*pieces)
# -- text -> vectors ---------------------------------------------------
def iter_vectors(self, text: str):
"""Yield one ``float16`` word vector per token in ``text`` (word or punct)."""
for word in _WORD_RE.findall(text):
vector = self.word_vector(word).astype(mx.float16)
mx.eval(vector)
yield mx.stop_gradient(vector)
def encode(self, text: str) -> mx.array:
"""Stacked word vectors, shape ``(n_words, dim)`` in ``float16``."""
vectors = list(self.iter_vectors(text))
if not vectors:
return mx.zeros((0, self.dim), dtype=mx.float16)
return mx.stack(vectors)
# Backwards-compatible alias.
HolographicMorphemeTokenizer = MorphemeTokenizer