Observed since 20 Oct, 20:50 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
aesg
Adaptive External Semantic Graph (AESG) - A persistent, self-organizing external memory layer for neural networks with pre-assembled architectures, professional training, and modular memory packs.
A persistent, self-organizing external memory layer for neural networks. AESG is a semantic graph that grows, prunes, and reorganizes itself alongside your model's training — acting as long-term memory that your model can query at every forward pass.
AESG (Adaptive External Semantic Graph) is a Python library that gives neural networks a persistent external memory in the form of a semantic graph. Unlike attention mechanisms that only look at the current input window, AESG maintains a growing knowledge structure on disk that persists between training runs, can be shared across models, and evolves over time.
The core idea: Every time your model processes data, AESG decides whether the input represents something genuinely new. If so, it creates a new "concept" in the graph. Over time, related concepts form connections. Old, unused concepts get pruned. The result is a self-organizing knowledge base that the model queries at every step.
What makes AESG different from other memory systems:
| Feature | AESG | Attention/KV Cache | Memory Networks |
|---|---|---|---|
| Persists across sessions | ✓ | ✗ | ✗ |
| Self-organizes (prunes, merges) | ✓ | ✗ | ✗ |
| Scales to millions of concepts | ✓ | Limited | Limited |
| Lives on disk (mmap) | ✓ | RAM only | RAM only |
| Portable (packs) | ✓ | ✗ | ✗ |
| Works with any architecture | ✓ | Transformers only | Specific arch |
Target simplicity:
from aesg import AESGTransformer, AESGTrainer
model = AESGTransformer.small(vocab_size=10000)
trainer = AESGTrainer(model)
trainer.fit(train_data, epochs=5)
trainer.evaluate(test_data)
That's it. Three lines to create a Transformer with persistent semantic memory, train it, and evaluate it. No manual graph configuration, no memory management, no custom training loops.
pip install aesg
from aesg import AESGTransformer
# This creates a Transformer with:
# - 128-dim hidden layers, 1 layer, 4 attention heads
# - An internal AESGMemory with vector_dim=64 and max 100k concepts
# - Memory stored on disk at ./aesg_memory/
model = AESGTransformer.small(vocab_size=10000)
from aesg import AESGTrainer
import torch
# Create dummy training data: list of (input_tokens, target_tokens) tuples
train_data = [
(torch.randint(0, 10000, (32,)), torch.randint(0, 10000, (32,)))
for _ in range(100)
]
# AESGTrainer handles everything: optimizer, loss, memory topology updates
trainer = AESGTrainer(model)
losses = trainer.fit(train_data, epochs=5)
print(f"Final loss: {losses[-1]:.4f}")
test_data = [
(torch.randint(0, 10000, (32,)), torch.randint(0, 10000, (32,)))
for _ in range(20)
]
metrics = trainer.evaluate(test_data)
print(f"Test loss: {metrics['loss']:.4f}")
pip install aesg
pip install torchvision # For image architectures with real datasets
pip install tensorboard # For TensorBoardCallback logging
pip install wandb # For Weights & Biases logging
Before diving into the API, here's what you need to understand about how AESG works under the hood.
A concept is a node in the AESG graph. Each concept is a vector (like an embedding) plus metadata: when it was created, how often it's been activated, how relevant it is. Concepts represent "things the model has learned." They're created automatically when the model encounters genuinely novel patterns.
When your model needs to retrieve context from memory, AESG doesn't do a simple nearest-neighbor lookup. Instead, it finds the most relevant seed node and then propagates activation energy outward through edges. Nearby concepts that share strong connections get activated together. This gives you richer, more structured context than a flat lookup would.
At every forward pass, AESG computes an "explanation score" — how well the current graph can explain the incoming input. If the score is too low (the input is novel), AESG increments a novelty buffer. If the buffer reaches a threshold, a new concept is born. This means the graph only grows when needed, not on every input.
Periodically, AESG runs a consolidation pass that:
This prevents unbounded growth. The graph evolves: useful concepts survive, unused ones disappear.
The entire graph lives on disk as memory-mapped files (numpy mmap). This means:
AESG is organized in layers:
┌─────────────────────────────────────────────────────────┐
│ Application Layer │
│ AESGTrainer · Callbacks · Evaluator · Benchmark │
├─────────────────────────────────────────────────────────┤
│ Architecture Layer │
│ AESGTransformer · AESGGRUText · AESGCNNClassifier etc. │
├─────────────────────────────────────────────────────────┤
│ Core Layer │
│ MemoryModes · PackManager · Validation │
├─────────────────────────────────────────────────────────┤
│ Cognitive Core (Preserved from V2) │
│ AESGMemory · CognitiveEngine · Navigator · Storage │
└─────────────────────────────────────────────────────────┘
RetrievedContextupdate_topology() runs consolidationInput Tensor ──→ Model Layer ──→ query_proj ──→ AESGMemory.retrieve()
↑ │
│ ↓
│ Navigator (spreading activation)
│ │
└── context injection ←── RetrievedContext
│
↓
Novelty Detection
│
↓ (if novel enough)
Create Sensory Concept
AESG provides five pre-assembled text architectures. Each one integrates AESG memory directly into its forward pass, so the model queries long-term knowledge at every step.
.small(), .medium(), .large() meanThese are factory methods that create a model with preset sizes:
| Size | hidden_size | num_layers | vector_dim | nhead (Transformer) |
|---|---|---|---|---|
| small | 128 | 1 | 64 | 4 |
| medium | 256 | 2 | 128 | 8 |
| large | 512 | 4 | 256 | 16 |
When you call .small(vocab_size=10000), it:
AESGConfig(vector_dim=64, max_concepts=100_000)AESGMemory("./aesg_memory", config)nn.ModuleWhat it is: A GRU (Gated Recurrent Unit) that queries AESG memory at every timestep. The hidden state is used to generate a memory query, and the retrieved context is concatenated with the input before being fed to the GRU cell.
How it works internally:
For each timestep t:
1. h_prev → query_proj → query_vector
2. AESGMemory.retrieve(query_vector) → context
3. context.aggregate() → memory_vector (collapsed to 1D)
4. concat(input_embedding[t], memory_vector) → gru_input
5. GRUCell(gru_input, h_prev) → h_new
6. output_proj(h_new) → logits[t]
When to use it: Sequential tasks where you want the model to accumulate knowledge across training runs. Good for language modeling, sequence classification, and simple text generation.
Complete example:
from aesg import AESGGRUText, AESGTrainer
import torch
# Create model: vocabulary of 5000 tokens, small size
model = AESGGRUText.small(vocab_size=5000)
# Input: batch of 4 sequences, each 20 tokens long
x = torch.randint(0, 5000, (4, 20))
# Forward pass produces logits for each position
logits = model(x)
print(logits.shape) # torch.Size([4, 20, 5000])
# → For each of the 4 sequences, at each of the 20 positions,
# we get a probability distribution over 5000 tokens
# Training
trainer = AESGTrainer(model, learning_rate=0.001)
train_data = [(torch.randint(0, 5000, (20,)), torch.randint(0, 5000, (20,))) for _ in range(200)]
losses = trainer.fit(train_data, epochs=3)
What it is: Same concept as AESGGRUText but uses an LSTM cell instead of GRU. The LSTM maintains both a hidden state (h) and a cell state (c), giving it more capacity for long-range dependencies.
Key difference from GRU: The LSTM's cell state provides a "highway" for gradient flow, making it better for longer sequences where GRU might struggle.
from aesg import AESGLSTMText, AESGTrainer
import torch
model = AESGLSTMText.medium(vocab_size=8000)
# Same interface as GRU — the complexity is hidden inside
x = torch.randint(0, 8000, (4, 30))
logits = model(x)
print(logits.shape) # torch.Size([4, 30, 8000])
What it is: An encoder-decoder architecture. The encoder is a standard GRU (no AESG). The decoder is an AESG-enhanced GRU that generates output step by step while querying semantic memory.
When to use it: Translation, summarization, or any sequence-to-sequence task where the input and output are different sequences.
How it works internally:
Encoder:
standard GRU processes input → produces enc_hidden
Decoder (with AESG):
For each output timestep:
1. AESG_GRU(dec_input, h_prev) → h_new
2. output_proj(h_new) → logits[t]
3. Next input = teacher-forced ground truth (during training)
from aesg import AESGSeq2Seq, AESGTrainer
import torch
model = AESGSeq2Seq.medium(vocab_size=10000)
# Input: source sequence (to be "translated")
src = torch.randint(0, 10000, (4, 25))
logits = model(src)
print(logits.shape) # torch.Size([4, 25, 10000])
What it is: A decoder-only (causal) language model. Processes tokens left-to-right, predicting the next token at each position. This is the architecture used by GPT-style models, but at smaller scale and with AESG memory.
When to use it: Text generation, auto-completion, causal language modeling.
from aesg import AESGDecoderLM, AESGTrainer
import torch
model = AESGDecoderLM.large(vocab_size=30000)
# Generate: feed a prompt, get logits for next tokens
prompt = torch.randint(0, 30000, (1, 50))
logits = model(prompt)
# logits[0, -1, :] contains the probability distribution for the next token
next_token_probs = torch.softmax(logits[0, -1, :], dim=0)
predicted_token = next_token_probs.argmax().item()
print(f"Predicted next token ID: {predicted_token}")
What it is: A Transformer encoder with AESG memory integrated via cross-attention. At each layer, after self-attention over the sequence, the model performs cross-attention against the memory concepts retrieved from the AESG graph. This is the most powerful architecture because it doesn't collapse the memory into a single vector — it attends to individual concepts.
How it works internally:
For each layer:
1. Self-attention(src, src, src) → attended
2. query_proj(mean(attended)) → memory_query
3. AESGMemory.retrieve(memory_query) → RetrievedContext
4. Cross-attention(attended, memory_vectors, memory_vectors) → enriched
5. FeedForward(enriched) → output
When to use it: Any task where you want maximum expressiveness. The cross-attention mechanism lets the model selectively attend to relevant past concepts rather than being forced to use a single aggregated vector.
from aesg import AESGTransformer, AESGTrainer
import torch
# Small: 128 hidden, 1 layer, 4 heads, 64-dim memory
model = AESGTransformer.small(vocab_size=10000)
# Medium: 256 hidden, 2 layers, 8 heads, 128-dim memory
model = AESGTransformer.medium(vocab_size=10000)
# Large: 512 hidden, 4 layers, 16 heads, 256-dim memory
model = AESGTransformer.large(vocab_size=10000)
# Custom storage location
model = AESGTransformer.medium(vocab_size=10000, storage_dir="./my_transformer_memory")
# Forward pass
x = torch.randint(0, 10000, (4, 64)) # batch=4, seq_len=64
logits = model(x)
print(logits.shape) # torch.Size([4, 64, 10000])
| Task | Recommended Model | Why |
|---|---|---|
| Simple sequence classification | AESGGRUText.small | Fast, lightweight |
| Language modeling (next-token) | AESGDecoderLM.medium | Causal, good capacity |
| Translation / summarization | AESGSeq2Seq.medium | Encoder-decoder |
| Best quality, any task | AESGTransformer.large | Cross-attention to memory |
| Long-range dependencies | AESGLSTMText.medium | Cell state highway |
AESG provides three pre-assembled image architectures. Each one injects semantic memory at the CNN bottleneck — the point where spatial features are most compressed and abstract.
In image models, AESG memory is injected at the bottleneck layer:
Input Image (B, C, H, W)
↓
Encoder (Conv + BN + ReLU + MaxPool) × N blocks
↓
Bottleneck feature maps (B, channels, small_H, small_W)
↓
AESG_CNNLayer:
1. Flatten spatial features → query vector
2. AESGMemory.retrieve(query) → context
3. context.aggregate() → memory vector
4. Project memory back to spatial shape
5. Add memory spatial tensor to conv output
↓
Decoder (ConvTranspose + BN + ReLU) × N blocks
↓
Output Image
The memory injection happens at the most abstract level of the CNN, where the features are highest-level and most semantically meaningful. This lets the memory influence high-level decisions (like "this region should be blue because I've seen sky before") rather than low-level pixel details.
What it is: Takes a grayscale image (1 channel) and produces a colorized version (3 channels, RGB). The encoder compresses the image, AESG provides semantic context about what colors to use, and the decoder reconstructs the full-color image.
Input/Output:
(batch_size, 1, height, width) — grayscale image, values in [0, 1](batch_size, 3, height, width) — RGB color image, values in [0, 1]Architecture by size:
| Size | Base Filters | Blocks | vector_dim | Bottleneck spatial |
|---|---|---|---|---|
| small | 32 | 3 | 64 | 16×16 |
| medium | 64 | 4 | 128 | 8×8 |
| large | 128 | 5 | 256 | 4×4 |
from aesg import AESGColorizationNet, AESGTrainer
import torch
# Create model
model = AESGColorizationNet.small()
# Input: batch of 8 grayscale images, 128×128
gray_images = torch.rand(8, 1, 128, 128)
# Output: colorized images
color_output = model(gray_images)
print(color_output.shape) # torch.Size([8, 3, 128, 128])
print(color_output.min()) # tensor(~0.03) — sigmoid ensures [0,1]
print(color_output.max()) # tensor(~0.97)
# Train with MSE loss (pixel reconstruction)
trainer = AESGTrainer(model, criterion=torch.nn.MSELoss())
# Each sample: (grayscale_image, color_image)
data = [(torch.rand(1, 128, 128), torch.rand(3, 128, 128)) for _ in range(100)]
losses = trainer.fit(data, epochs=3)
What it is: A CNN that classifies images into categories. The encoder extracts features, AESG provides semantic context at the bottleneck, and a classification head produces class logits.
Input/Output:
(batch_size, 3, height, width) — RGB image(batch_size, num_classes) — raw logits (use softmax for probabilities)num_classes parameter: You specify how many categories to classify into. Default is 10 (like CIFAR-10).
from aesg import AESGCNNClassifier, AESGTrainer
import torch
# CIFAR-10 style: 10 classes
model = AESGCNNClassifier.small(num_classes=10)
# ImageNet style: 1000 classes
model = AESGCNNClassifier.large(num_classes=1000)
# Forward pass
images = torch.rand(8, 3, 128, 128)
logits = model(images)
print(logits.shape) # torch.Size([8, 10])
# Get predicted classes
predictions = logits.argmax(dim=1)
print(predictions) # tensor([3, 7, 2, 5, 1, 0, 4, 6])
# Train with CrossEntropyLoss (default)
trainer = AESGTrainer(model)
data = [(torch.rand(3, 128, 128), torch.tensor(label)) for label in range(100) for _ in range(1)]
losses = trainer.fit(data, epochs=5)
What it is: A symmetric encoder-decoder that reconstructs its input. The model learns a compressed representation at the bottleneck, with AESG memory augmenting that representation. Useful for anomaly detection, denoising, and learning representations.
Input/Output:
(batch_size, channels, height, width) — any image(batch_size, channels, height, width) — reconstructed image, same dimensionsfrom aesg import AESGCNNAutoencoder, AESGTrainer
import torch
model = AESGCNNAutoencoder.small()
# Input and target are the same image (reconstruction task)
images = torch.rand(8, 3, 128, 128)
reconstructed = model(images)
print(reconstructed.shape) # torch.Size([8, 3, 128, 128])
# Train — target is the same as input
trainer = AESGTrainer(model, criterion=torch.nn.MSELoss())
data = [(img, img) for img in [torch.rand(3, 128, 128) for _ in range(100)]]
losses = trainer.fit(data, epochs=5)
AESGTrainer is the professional training interface for AESG models. It handles everything: the training loop, memory topology updates, data format adaptation, checkpointing, and callbacks.
from aesg import AESGTrainer
trainer = AESGTrainer(
model, # Any nn.Module (with or without AESGMemory)
optimizer=None, # Default: Adam(lr=learning_rate)
criterion=None, # Default: CrossEntropyLoss
callbacks=None, # Default: [] (no callbacks)
device="cpu", # "cpu" or "cuda" or "cuda:0"
batch_size=32, # Used when adapting lists/Datasets to DataLoaders
learning_rate=1e-3, # Used by default Adam optimizer
)
What happens if your model doesn't have AESGMemory? The trainer still works — it just operates in "neural-only" mode and emits a warning. You can use AESGTrainer for any PyTorch model, not just AESG models.
losses = trainer.fit(train_data, epochs=10)
What it does step by step:
on_train_start callbackson_epoch_starton_epoch_end (with loss in logs)on_train_endData format flexibility:
# Option 1: PyTorch DataLoader (used directly, no conversion)
from torch.utils.data import DataLoader, TensorDataset
dataset = TensorDataset(inputs, targets)
loader = DataLoader(dataset, batch_size=64, shuffle=True)
trainer.fit(loader, epochs=5)
# Option 2: PyTorch Dataset (wrapped in DataLoader with trainer's batch_size)
trainer.fit(dataset, epochs=5)
# Option 3: Simple list of tuples (most convenient for prototyping)
data = [(input_tensor, target_tensor) for ...]
trainer.fit(data, epochs=5)
metrics = trainer.evaluate(test_data)
# Returns: {"loss": 0.5432}
What it does:
torch.no_grad()output = trainer.predict(input_tensor)
What it does:
torch.no_grad() (no gradient computation)# Save everything in one call
trainer.save("./checkpoints/epoch_10")
# What gets saved:
# ./checkpoints/epoch_10/
# ├── model_weights.pt (PyTorch state_dict)
# ├── optimizer_state.pt (optimizer state)
# ├── config.json (AESGConfig + trainer metadata)
# ├── training_state.json (epoch, global_step, best_loss)
# └── memory/ (copy of the AESG memory directory)
# Load everything back
trainer.load("./checkpoints/epoch_10")
# Resume from where you left off
start_epoch = trainer.resume("./checkpoints/epoch_10")
# start_epoch = 10 (the epoch to continue from)
losses = trainer.fit(train_data, epochs=10)
# This will train epochs 10-19
Callbacks let you hook into the training lifecycle without modifying the trainer. They're called in registration order.
Available lifecycle hooks:
on_train_start(logs) — Once, before first epochon_train_end(logs) — Once, after all epochson_epoch_start(epoch, logs) — Start of each epochon_epoch_end(epoch, logs) — End of each epoch (has "loss" in logs)on_batch_start(batch, logs) — Start of each batchon_batch_end(batch, logs) — End of each batchEarlyStopping: Monitors a metric and stops training if it doesn't improve.
from aesg import EarlyStopping
# Stop if loss doesn't improve for 5 consecutive epochs
es = EarlyStopping(patience=5, monitor="loss")
trainer = AESGTrainer(model, callbacks=[es])
losses = trainer.fit(data, epochs=100) # Will stop early if loss plateaus
Checkpoint: Saves the model at regular intervals.
from aesg import Checkpoint
# Save every 2 epochs
ckpt = Checkpoint(save_every=2, path="./checkpoints")
trainer = AESGTrainer(model, callbacks=[ckpt])
trainer.fit(data, epochs=10)
# Creates: ./checkpoints/epoch_2, ./checkpoints/epoch_4, ...
Custom callbacks: Implement the Callback base class.
from aesg import Callback
class PrintProgress(Callback):
def on_epoch_end(self, epoch, logs):
loss = logs.get("loss", "?")
print(f"Epoch {epoch}: loss = {loss:.4f}")
trainer = AESGTrainer(model, callbacks=[PrintProgress()])
Memory modes control what operations AESG is allowed to perform. This prevents accidental writes during inference and enables fine-tuning without destroying existing knowledge.
TRAIN — Full power. All operations enabled.
FINETUNE — Careful learning. Adds knowledge without disrupting structure.
INFERENCE — Read-only. Zero modification to memory.
ONLINE — Continuous learning. Learns at full speed but doesn't reorganize.
from aesg import AESGTransformer
model = AESGTransformer.small(vocab_size=10000)
# Access the memory object
memory = model.memory
# Check current mode
print(memory.mode) # MemoryMode.TRAIN (default)
# Switch modes
memory.set_mode("INFERENCE")
# Now retrieve() works but no concepts are created
memory.set_mode("FINETUNE")
# Concepts can be created slowly, but structure is preserved
memory.set_mode("TRAIN")
# Full learning restored
If you call something that the current mode doesn't allow (e.g., trying to create a concept in INFERENCE mode), it's a silent no-op. No exception is raised, no error logged. The operation simply doesn't execute. This makes it safe to use the same model code in both training and inference without conditional logic.
You don't need to manage modes manually when using AESGTrainer:
trainer.fit() → automatically sets TRAINtrainer.evaluate() → automatically sets INFERENCE, then restorestrainer.predict() → automatically sets INFERENCE, then restoresPacks are portable memory snapshots. They let you:
A .aesgpack file contains a serialized subgraph of concepts and their connections. It includes:
After training a model, you can export its memory (or a filtered subset) as a pack:
from aesg import AESGTransformer, AESGTrainer
# Train a model on medical text
model = AESGTransformer.medium(vocab_size=30000, storage_dir="./medical_memory")
trainer = AESGTrainer(model)
trainer.fit(medical_data, epochs=20)
# Export ALL learned concepts
model.memory.export_pack("./packs/medical_full.aesgpack")
# Export only highly-relevant concepts (relevance >= 0.5)
model.memory.export_pack("./packs/medical_core.aesgpack", min_relevance=0.5)
# Export only concepts from region 2 (if region detection has run)
model.memory.export_pack("./packs/medical_region2.aesgpack", region_id=2)
from aesg import AESGTransformer
# Create a fresh model
model = AESGTransformer.medium(vocab_size=30000, storage_dir="./new_model_memory")
# Load the pack (validates checksum, checks vector_dim compatibility)
pack = model.memory.load_pack("./packs/medical_core.aesgpack")
print(f"Loaded: {pack.name}, {len(pack.nodes)} concepts")
# Attach with priority (higher = more influence during retrieval)
model.memory.attach_pack(pack, priority=80)
# Now the model can access medical concepts during inference!
# Load specialized packs
medical = model.memory.load_pack("./packs/medical.aesgpack")
chemistry = model.memory.load_pack("./packs/chemistry.aesgpack")
general = model.memory.load_pack("./packs/general.aesgpack")
# Attach with different priorities
model.memory.attach_pack(medical, priority=90) # Highest influence
model.memory.attach_pack(chemistry, priority=60) # Medium influence
model.memory.attach_pack(general, priority=20) # Background knowledge
# During spreading activation, pack node energies are weighted by
# normalized priority: medical gets 90/(90+60+20) = 53% weight
# Remove a pack (returns it to "loaded" state, doesn't delete it)
model.memory.detach_pack("medical")
# The base memory and other packs are unaffected
vector_dim MUST match the model's vector_dimAESGStorageError is raisedThe Evaluator class computes domain-specific quality metrics automatically based on what type of model you're evaluating.
The Evaluator looks at your model's class name to figure out which metrics to compute:
| Model class contains... | Detected domain | Metrics computed |
|---|---|---|
| "Transformer", "GRUText", "LSTMText", "Seq2Seq", "DecoderLM" | text | BLEU, ROUGE, Accuracy, Perplexity |
| "Classifier" | classification | Accuracy, Precision, Recall, F1 |
| "Colorization", "Autoencoder" | image | PSNR, SSIM, MSE, MAE |
from aesg import Evaluator
# Predictions and references as lists of strings
predictions = ["the cat sat on the mat", "hello world"]
references = ["the cat sat on a mat", "hello world"]
metrics = Evaluator.compute_text_metrics(predictions, references)
print(f"BLEU: {metrics['bleu']:.4f}") # ~0.5-0.8
print(f"ROUGE: {metrics['rouge']:.4f}") # ~0.8-1.0
print(f"Accuracy: {metrics['accuracy']:.4f}") # 0.5 (1 exact match out of 2)
print(f"Perplexity: {metrics['perplexity']:.2f}")
import torch
from aesg import Evaluator
# Model predictions and ground truth (both in [0, 1] range)
predicted_images = torch.rand(16, 3, 128, 128)
target_images = torch.rand(16, 3, 128, 128)
metrics = Evaluator.compute_image_metrics(predicted_images, target_images)
print(f"PSNR: {metrics['psnr']:.2f} dB")
print(f"SSIM: {metrics['ssim']:.4f}")
print(f"MSE: {metrics['mse']:.6f}")
print(f"MAE: {metrics['mae']:.6f}")
import torch
from aesg import Evaluator
# Model outputs raw logits (before softmax), targets are class indices
logits = torch.randn(200, 10) # 200 samples, 10 classes
targets = torch.randint(0, 10, (200,))
metrics = Evaluator.compute_classification_metrics(logits, targets)
print(f"Accuracy: {metrics['accuracy']:.4f}")
print(f"Precision: {metrics['precision']:.4f}")
print(f"Recall: {metrics['recall']:.4f}")
print(f"F1: {metrics['f1']:.4f}")
from aesg import AESGCNNClassifier, Evaluator
import torch
model = AESGCNNClassifier.small(num_classes=5)
evaluator = Evaluator()
# The evaluator detects "Classifier" in the class name → uses classification metrics
logits = torch.randn(50, 5)
targets = torch.randint(0, 5, (50,))
metrics = evaluator.evaluate(model, logits, targets)
# Returns: {"accuracy": ..., "precision": ..., "recall": ..., "f1": ...}
AESGConfig is the single source of truth for all AESG parameters. It's a frozen dataclass — once created, you cannot modify its values. This prevents accidental mid-training changes.
from aesg import AESGConfig
# Default configuration (good for most cases)
config = AESGConfig()
# Domain-specific presets
config = AESGConfig.for_text() # vector_dim=128, max_concepts=500k
config = AESGConfig.for_image() # vector_dim=256, max_concepts=200k
config = AESGConfig.for_classification() # vector_dim=64, max_concepts=100k
# Fully custom
config = AESGConfig(
vector_dim=128,
max_concepts=500_000,
spreading_activation_steps=4,
novelty_birth_threshold=5,
learning_rate=5e-4,
)
config = AESGConfig(vector_dim=128)
config.vector_dim = 256 # ← FrozenInstanceError! Cannot modify.
This is intentional. Changing config mid-training could corrupt the memory graph (e.g., changing vector_dim after nodes were already created). If you need different settings, create a new AESGConfig.
Memory parameters:
| Parameter | Default | What it controls |
|---|---|---|
vector_dim | 256 | Dimension of concept vectors. Must match your model's internal dimension. Range: [1, 8192]. |
max_concepts | 1,000,000 | Maximum number of concepts in the graph. When exceeded, evolutionary pressure prunes the weakest. |
max_edges_per_node | 1000 | If a node has more edges than this, it may be split into subgraphs. |
Navigation parameters:
| Parameter | Default | What it controls |
|---|---|---|
spreading_activation_steps | 3 | How many hops activation propagates. More hops = broader context but slower. |
spreading_activation_decay | 0.8 | How much energy is lost per hop. 0.8 means 80% survives each hop. Range: [0.0, 1.0]. |
region_facilitation_multiplier | 1.5 | Bonus for traversing within the same semantic region. >1.0 means intra-region paths are preferred. |
Novelty parameters:
| Parameter | Default | What it controls |
|---|---|---|
novelty_explanation_threshold | 0.6 | If the graph explains the input below this score, it's considered novel. Range: [0.0, 1.0]. |
novelty_birth_threshold | 3 | How many times a novel input must persist before a concept is created. Prevents single outliers from creating nodes. |
Evolutionary pressure:
| Parameter | Default | What it controls |
|---|---|---|
survival_threshold_relevance | 0.05 | Concepts with relevance below this (and old enough) get pruned. |
survival_threshold_frequency | 5 | Concepts activated fewer times than this (and old enough) get pruned. |
Training parameters:
| Parameter | Default | What it controls |
|---|---|---|
learning_rate | 1e-3 | Default learning rate for the optimizer. |
batch_size | 32 | Default batch size when AESGTrainer wraps data. |
memory_mode | "TRAIN" | Initial memory mode. |
Save and restore configurations:
from aesg import AESGConfig
config = AESGConfig(vector_dim=128, max_concepts=200_000)
# Save to JSON string
json_str = config.to_json()
print(json_str)
# {
# "vector_dim": 128,
# "max_concepts": 200000,
# ...
# }
# Restore from JSON string
restored = AESGConfig.from_json(json_str)
assert config == restored # True — exact round-trip
If you pass invalid values, you get a clear error:
from aesg import AESGConfig, AESGMemory
# This creates the config (frozen dataclass allows any values at creation)
config = AESGConfig(vector_dim=-1, spreading_activation_decay=5.0)
# Validation happens when you use it:
try:
memory = AESGMemory("./test", config) # ← Validator runs here
except Exception as e:
print(e)
# Configuration validation failed:
# Parameter 'vector_dim': got -1, expected integer >= 1
# Parameter 'spreading_activation_decay': got 5.0, expected float in [0.0, 1.0]
AESG has a clean exception hierarchy. Every error raised by AESG inherits from AESGError, so you can catch everything with one except clause, or be specific per subsystem.
AESGError (base)
├── AESGConfigError → Invalid configuration
├── AESGMemoryError → Memory operation failures
├── AESGNavigationError → Spreading activation / retrieval issues
├── AESGTrainingError → Training loop issues (bad data, etc.)
└── AESGStorageError → File I/O, corrupt packs, checkpoint errors
e.message — Human-readable description (max 500 chars)e.subsystem — Which subsystem ("config", "memory", "navigation", "trainer", "storage")from aesg import (
AESGError, AESGConfigError, AESGMemoryError,
AESGTrainingError, AESGStorageError
)
# 1. Invalid vocab_size
from aesg import AESGTransformer
try:
model = AESGTransformer.small(vocab_size=0)
except AESGConfigError as e:
print(f"[{e.subsystem}] {e.message}")
# [config] vocab_size must be >= 1
# 2. Invalid memory mode
from aesg import AESGMemory, AESGConfig
memory = AESGMemory("./test", AESGConfig(vector_dim=16, max_concepts=100))
try:
memory.set_mode("INVALID")
except AESGMemoryError as e:
print(f"[{e.subsystem}] {e.message}")
# [memory] Invalid memory mode 'INVALID'. Valid modes: ['TRAIN', 'FINETUNE', 'INFERENCE', 'ONLINE']
# 3. Empty training data
from aesg import AESGTrainer, AESGGRUText
model = AESGGRUText.small(vocab_size=100)
trainer = AESGTrainer(model)
try:
trainer.fit([], epochs=5)
except AESGTrainingError as e:
print(f"[{e.subsystem}] {e.message}")
# [trainer] Empty dataset provided. Training requires at least one sample.
# 4. Corrupt pack file
try:
memory.load_pack("./not_a_real_pack.aesgpack")
except AESGStorageError as e:
print(f"[{e.subsystem}] {e.message}")
# [storage] Failed to read pack file './not_a_real_pack.aesgpack': ...
# 5. Catch ANY AESG error (broad handler)
try:
# ... any AESG operation ...
pass
except AESGError as e:
print(f"AESG error in {e.subsystem}: {e.message}")
AESG includes a built-in benchmark that compares a CNN with AESG memory vs a plain CNN on image colorization.
AESGColorizationNet.small() for 3 epochsBaselineCNN (same architecture, no AESG) for 3 epochsfrom aesg import ColorizationBenchmark
benchmark = ColorizationBenchmark(
dataset_name="cifar10", # Uses CIFAR-10 if torchvision installed
max_images=2000, # Limit for fast execution
image_size=128, # All images resized to 128×128
max_epochs=3, # Quick training run
output_dir="./benchmark_results", # Where results go
)
results = benchmark.run()
# Results contain metrics for both models
print(f"AESG — PSNR: {results['aesg'].psnr:.2f} dB, SSIM: {results['aesg'].ssim:.4f}")
print(f"Base — PSNR: {results['baseline'].psnr:.2f} dB, SSIM: {results['baseline'].ssim:.4f}")
print(f"AESG Memory: {results['aesg'].memory_nodes} concepts created")
./benchmark_results/
├── benchmark_report.md # Markdown comparison report
├── results.json # Raw metrics as JSON
└── samples/ # Visual comparison images
├── sample_000.png # [original | grayscale | AESG color | baseline color]
├── sample_001.png
└── ...
When you create an AESGMemory, it writes to disk immediately:
./aesg_memory/
├── nodes.aesg # All concept nodes (mmap file, structured numpy array)
├── edges.aesg # All edges between nodes (mmap file, linked list)
├── meta.npy # Metadata: node_count, edge_count, capacity, vector_dim
└── evolution.aesglog # Binary event log (32 bytes per event)
nodes.aesg — Each node stores: ID, vector (D floats), created_at, modified_at, use_frequency, relevance, age, stability, region_id, is_active, head_edge_idx.
edges.aesg — Each edge stores: source_id, target_id, weight, confidence, use_count, next_edge_idx (linked list).
meta.npy — NumPy file with counts and capacities.
evolution.aesglog — Binary log of all evolutionary events (CREATE, MERGE, SPLIT, PRUNE, CONSOLIDATE, RESTRUCTURE). Each event is exactly 32 bytes.
The files are memory-mapped (numpy.memmap), which means:
memory.save() or the trainer finishes an epochWhen AESGTrainer.save() is called:
./checkpoints/epoch_10/
├── model_weights.pt # PyTorch state_dict of the full model
├── optimizer_state.pt # Optimizer state (momentum, etc.)
├── config.json # AESGConfig + trainer metadata
├── training_state.json # {"epoch": 10, "global_step": 1000, "best_loss": 0.5}
└── memory/ # Copy of the entire memory directory
├── nodes.aesg
├── edges.aesg
├── meta.npy
└── evolution.aesglog
AESG models are standard nn.Module instances. They work with everything PyTorch offers.
from aesg import AESGTransformer, AESGTrainer
import torch.optim as optim
model = AESGTransformer.medium(vocab_size=10000)
# Use any PyTorch optimizer
optimizer = optim.AdamW(model.parameters(), lr=5e-4, weight_decay=0.01)
trainer = AESGTrainer(model, optimizer=optimizer)
import torch.nn as nn
from aesg import AESGColorizationNet, AESGTrainer
model = AESGColorizationNet.small()
# Use L1 loss instead of default CrossEntropyLoss
criterion = nn.L1Loss()
trainer = AESGTrainer(model, criterion=criterion)
from aesg import AESGTransformer, AESGTrainer
model = AESGTransformer.large(vocab_size=30000)
trainer = AESGTrainer(model, device="cuda")
# Model is automatically moved to GPU
# Data tensors are automatically moved to GPU during training
import torch
from torch.utils.data import DataLoader, TensorDataset
from aesg import AESGGRUText, AESGTrainer
model = AESGGRUText.medium(vocab_size=5000)
# Standard PyTorch Dataset
inputs = torch.randint(0, 5000, (1000, 50))
targets = torch.randint(0, 5000, (1000, 50))
dataset = TensorDataset(inputs, targets)
loader = DataLoader(dataset, batch_size=64, shuffle=True, num_workers=4)
# AESGTrainer accepts DataLoader directly
trainer = AESGTrainer(model)
losses = trainer.fit(loader, epochs=10)
If you need full control, you can use the model directly:
import torch
import torch.nn as nn
from aesg import AESGGRUText
model = AESGGRUText.small(vocab_size=5000)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.CrossEntropyLoss()
for epoch in range(10):
model.train()
model.memory.set_mode("TRAIN")
model.memory.reset_state()
x = torch.randint(0, 5000, (32, 20))
y = torch.randint(0, 5000, (32, 20))
optimizer.zero_grad()
output = model(x) # (32, 20, 5000)
loss = criterion(output.reshape(-1, 5000), y.reshape(-1))
loss.backward()
optimizer.step()
# Don't forget topology update!
model.memory.update_topology()
print(f"Epoch {epoch}: loss={loss.item():.4f}")
# Save memory to disk
model.memory.save()
from aesg import AESGTransformer
model = AESGTransformer.small(vocab_size=5000)
# After training...
memory = model.memory
print(f"Total concepts: {memory.storage.node_count}")
print(f"Total edges: {memory.storage.edge_count}")
print(f"Current mode: {memory.mode.value}")
print(f"Vector dim: {memory.vector_dim}")
# Read the evolution log
history = memory.logger.get_history()
print(f"Total evolutionary events: {len(history)}")
for event in history[:5]:
print(f" {event['type']} at {event['timestamp']}")
| Model | Size | Forward (ms/batch) | Memory Overhead | Graph Nodes (10 epochs) |
|---|---|---|---|---|
| AESGGRUText | small | ~2.1 | +12 MB | ~500 |
| AESGGRUText | large | ~8.4 | +48 MB | ~2,000 |
| AESGTransformer | small | ~3.5 | +12 MB | ~500 |
| AESGTransformer | large | ~15.2 | +48 MB | ~2,000 |
| AESGCNNClassifier | small | ~4.8 | +12 MB | ~300 |
| AESGColorizationNet | small | ~6.2 | +12 MB | ~400 |
| Concepts | RAM (vector_dim=16) | Disk |
|---|---|---|
| 10,000 | ~2 MB | ~3 MB |
| 100,000 | ~17 MB | ~32 MB |
| 500,000 | ~80 MB | ~151 MB |
| 1,000,000 | ~173 MB | ~314 MB |
| Spreading activation hops | Average latency | p99 latency |
|---|---|---|
| 1 | ~1 ms | ~3 ms |
| 2 | ~4 ms | ~7 ms |
| 4 | ~9 ms | ~20 ms |
| 8 | ~49 ms | ~139 ms |
spreading_activation_steps=2 for real-time applicationsspreading_activation_steps=4 for maximum quality (offline)vector_dim small (64-128) for text tasksmax_concepts wisely — 100k-500k is sufficient for most tasksMIT License — Copyright (c) 2026 bueormnew
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED.
Author: bueormnew · Homepage: github.com/bueormnew/aesg
Download counts and popularity will appear here as the collector receives them.
| Package / target | Requirement | Relationship |
|---|---|---|
torchEnvironment{
"extras": "",
"marker": null,
"requires_python": ">=3.8",
"evaluation": "NOT_EVALUATED"
} | torch>=1.10.0 | runtime |
numpyEnvironment{
"extras": "",
"marker": null,
"requires_python": ">=3.8",
"evaluation": "NOT_EVALUATED"
} | numpy>=1.20.0 | runtime |
networkxEnvironment{
"extras": "",
"marker": null,
"requires_python": ">=3.8",
"evaluation": "NOT_EVALUATED"
} | networkx>=2.5 | runtime |
torchvisionEnvironment{
"extras": "",
"marker": "extra == \"image\"",
"requires_python": ">=3.8",
"evaluation": "NOT_EVALUATED"
} | torchvision>=0.11.0; extra == "image" | runtime |
tensorboardEnvironment{
"extras": "",
"marker": "extra == \"tensorboard\"",
"requires_python": ">=3.8",
"evaluation": "NOT_EVALUATED"
} | tensorboard>=2.0; extra == "tensorboard" | runtime |
wandbEnvironment{
"extras": "",
"marker": "extra == \"wandb\"",
"requires_python": ">=3.8",
"evaluation": "NOT_EVALUATED"
} | wandb>=0.12; extra == "wandb" | runtime |
pytestEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.8",
"evaluation": "NOT_EVALUATED"
} | pytest>=7.0; extra == "dev" | runtime |
hypothesisEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.8",
"evaluation": "NOT_EVALUATED"
} | hypothesis>=6.0; extra == "dev" | runtime |
torchvisionEnvironment{
"extras": "",
"marker": "extra == \"all\"",
"requires_python": ">=3.8",
"evaluation": "NOT_EVALUATED"
} | torchvision>=0.11.0; extra == "all" | runtime |
tensorboardEnvironment{
"extras": "",
"marker": "extra == \"all\"",
"requires_python": ">=3.8",
"evaluation": "NOT_EVALUATED"
} | tensorboard>=2.0; extra == "all" | runtime |
wandbEnvironment{
"extras": "",
"marker": "extra == \"all\"",
"requires_python": ">=3.8",
"evaluation": "NOT_EVALUATED"
} | wandb>=0.12; extra == "all" | runtime |
bdist_wheel · 74.03 KB · Python py3
sdist · 94.72 KB · Python source
Observed since 20 Oct, 20:50 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package_version:f798579c7decff975367f76ebd9f207abaa4bf9ce6da1974aaf8a94911024347