Instructions to use Syigen/rgcm with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Syigen/rgcm with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="Syigen/rgcm")# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("Syigen/rgcm", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Upload Concept Memory base model
Browse files- README.md +16 -7
- concept_memory/__init__.py +4 -0
- concept_memory/encoding.py +58 -0
- concept_memory/experts.py +50 -0
- concept_memory/model.py +67 -0
- concept_memory/types.py +45 -0
README.md
CHANGED
|
@@ -22,26 +22,35 @@ not a language model pretrained from scratch. Its text encoder is
|
|
| 22 |
|
| 23 |
- `model.pt`: PyTorch state dictionary and training metadata.
|
| 24 |
- `config.json`: encoder, task, and training run metadata.
|
|
|
|
| 25 |
|
| 26 |
## Run a prediction
|
| 27 |
|
| 28 |
-
Use Python 3.11 or newer. Install the
|
| 29 |
|
| 30 |
```bash
|
| 31 |
-
pip install
|
| 32 |
```
|
| 33 |
|
| 34 |
-
Download
|
| 35 |
-
|
|
|
|
|
|
|
| 36 |
|
| 37 |
```python
|
| 38 |
import torch
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
from concept_memory import ConceptMemoryModel
|
| 41 |
from concept_memory.types import Question
|
| 42 |
|
| 43 |
-
|
| 44 |
-
checkpoint = hf_hub_download(repo_id, "model.pt")
|
| 45 |
payload = torch.load(checkpoint, map_location="cpu", weights_only=False)
|
| 46 |
model = ConceptMemoryModel.from_pretrained(
|
| 47 |
payload["encoder"],
|
|
|
|
| 22 |
|
| 23 |
- `model.pt`: PyTorch state dictionary and training metadata.
|
| 24 |
- `config.json`: encoder, task, and training run metadata.
|
| 25 |
+
- `concept_memory/`: the minimal model runtime modules needed for inference.
|
| 26 |
|
| 27 |
## Run a prediction
|
| 28 |
|
| 29 |
+
Use Python 3.11 or newer. Install the runtime dependencies:
|
| 30 |
|
| 31 |
```bash
|
| 32 |
+
pip install torch transformers huggingface_hub
|
| 33 |
```
|
| 34 |
|
| 35 |
+
Download this model repository. It contains only the small `concept_memory`
|
| 36 |
+
runtime package needed to load the checkpoint, rather than the full training
|
| 37 |
+
project. Then construct a question with the criteria you want the model to
|
| 38 |
+
judge and inspect its prediction and probabilities:
|
| 39 |
|
| 40 |
```python
|
| 41 |
import torch
|
| 42 |
+
import sys
|
| 43 |
+
from pathlib import Path
|
| 44 |
+
from huggingface_hub import snapshot_download
|
| 45 |
+
|
| 46 |
+
repo_id = "Syigen/rgcm"
|
| 47 |
+
repo_path = snapshot_download(repo_id)
|
| 48 |
+
sys.path.insert(0, repo_path)
|
| 49 |
+
|
| 50 |
from concept_memory import ConceptMemoryModel
|
| 51 |
from concept_memory.types import Question
|
| 52 |
|
| 53 |
+
checkpoint = Path(repo_path) / "model.pt"
|
|
|
|
| 54 |
payload = torch.load(checkpoint, map_location="cpu", weights_only=False)
|
| 55 |
model = ConceptMemoryModel.from_pretrained(
|
| 56 |
payload["encoder"],
|
concept_memory/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .model import ConceptMemoryModel
|
| 2 |
+
from .types import Criterion, Question, QuestionType, TaskOutput
|
| 3 |
+
|
| 4 |
+
__all__ = ["ConceptMemoryModel", "Criterion", "Question", "QuestionType", "TaskOutput"]
|
concept_memory/encoding.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from collections.abc import Mapping
|
| 5 |
+
|
| 6 |
+
from torch import Tensor, nn
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def serialize_state(state: Mapping[str, object] | str) -> str:
|
| 10 |
+
"""Serialize structured state in stable key order for the text encoder."""
|
| 11 |
+
if isinstance(state, str):
|
| 12 |
+
return state
|
| 13 |
+
parts = []
|
| 14 |
+
for key in sorted(state):
|
| 15 |
+
value = state[key]
|
| 16 |
+
if value is None:
|
| 17 |
+
continue
|
| 18 |
+
if isinstance(value, (Mapping, list, tuple)):
|
| 19 |
+
value = json.dumps(value, ensure_ascii=False, sort_keys=True)
|
| 20 |
+
parts.append(f"[{key}] {value}")
|
| 21 |
+
return "\n".join(parts)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class TransformerTextEncoder(nn.Module):
|
| 25 |
+
"""Shared transformer encoder with attention-mask-aware mean pooling."""
|
| 26 |
+
|
| 27 |
+
def __init__(
|
| 28 |
+
self, model: nn.Module, tokenizer: object, hidden_size: int, max_length: int = 512
|
| 29 |
+
):
|
| 30 |
+
super().__init__()
|
| 31 |
+
self.model = model
|
| 32 |
+
self.tokenizer = tokenizer
|
| 33 |
+
self.hidden_size = hidden_size
|
| 34 |
+
self.max_length = max_length
|
| 35 |
+
|
| 36 |
+
@classmethod
|
| 37 |
+
def from_pretrained(cls, name: str, max_length: int = 512) -> "TransformerTextEncoder":
|
| 38 |
+
from transformers import AutoModel, AutoTokenizer
|
| 39 |
+
|
| 40 |
+
tokenizer = AutoTokenizer.from_pretrained(name)
|
| 41 |
+
model = AutoModel.from_pretrained(name)
|
| 42 |
+
return cls(model, tokenizer, model.config.hidden_size, max_length)
|
| 43 |
+
|
| 44 |
+
def forward(self, texts: list[str]) -> Tensor:
|
| 45 |
+
batch = self.tokenizer(
|
| 46 |
+
texts,
|
| 47 |
+
padding=True,
|
| 48 |
+
truncation=True,
|
| 49 |
+
max_length=self.max_length,
|
| 50 |
+
return_tensors="pt",
|
| 51 |
+
)
|
| 52 |
+
device = next(self.model.parameters()).device
|
| 53 |
+
batch = {key: value.to(device) for key, value in batch.items()}
|
| 54 |
+
outputs = self.model(**batch)
|
| 55 |
+
token_embeddings = outputs.last_hidden_state
|
| 56 |
+
mask = batch["attention_mask"].unsqueeze(-1).to(token_embeddings.dtype)
|
| 57 |
+
pooled = (token_embeddings * mask).sum(dim=1) / mask.sum(dim=1).clamp_min(1)
|
| 58 |
+
return nn.functional.layer_norm(pooled, (self.hidden_size,))
|
concept_memory/experts.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
from torch import Tensor, nn
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class RawExpert(nn.Module):
|
| 8 |
+
def __init__(self, hidden_size: int, decision_size: int):
|
| 9 |
+
super().__init__()
|
| 10 |
+
self.net = nn.Sequential(
|
| 11 |
+
nn.Linear(hidden_size * 2, decision_size),
|
| 12 |
+
nn.GELU(),
|
| 13 |
+
nn.LayerNorm(decision_size),
|
| 14 |
+
nn.Linear(decision_size, decision_size),
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
def forward(self, state: Tensor, question: Tensor) -> Tensor:
|
| 18 |
+
return self.net(torch.cat((state, question), dim=-1))
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class CandidateScorer(nn.Module):
|
| 22 |
+
"""Shared pointwise scorer; candidate permutation only permutes its outputs."""
|
| 23 |
+
|
| 24 |
+
def __init__(self, decision_size: int):
|
| 25 |
+
super().__init__()
|
| 26 |
+
self.net = nn.Sequential(
|
| 27 |
+
nn.Linear(decision_size * 4, decision_size),
|
| 28 |
+
nn.GELU(),
|
| 29 |
+
nn.Linear(decision_size, 1),
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
def forward(self, decision: Tensor, candidates: Tensor) -> Tensor:
|
| 33 |
+
if decision.ndim == 1:
|
| 34 |
+
decision = decision.unsqueeze(0)
|
| 35 |
+
if candidates.ndim == 2:
|
| 36 |
+
candidates = candidates.unsqueeze(0).expand(decision.size(0), -1, -1)
|
| 37 |
+
decision = decision.unsqueeze(1).expand(-1, candidates.size(1), -1)
|
| 38 |
+
features = torch.cat(
|
| 39 |
+
(decision, candidates, decision * candidates, torch.abs(decision - candidates)), dim=-1
|
| 40 |
+
)
|
| 41 |
+
return self.net(features).squeeze(-1).squeeze(0)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class NoulHead(nn.Module):
|
| 45 |
+
def __init__(self, decision_size: int):
|
| 46 |
+
super().__init__()
|
| 47 |
+
self.linear = nn.Linear(decision_size, 1)
|
| 48 |
+
|
| 49 |
+
def forward(self, decision: Tensor) -> Tensor:
|
| 50 |
+
return self.linear(decision).squeeze(-1)
|
concept_memory/model.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from collections.abc import Mapping, Sequence
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
from torch import Tensor, nn
|
| 7 |
+
|
| 8 |
+
from .encoding import TransformerTextEncoder, serialize_state
|
| 9 |
+
from .experts import CandidateScorer, NoulHead, RawExpert
|
| 10 |
+
from .types import Question, TaskOutput
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class ConceptMemoryModel(nn.Module):
|
| 14 |
+
"""Raw-only typed judgment model; state representation is shared per request."""
|
| 15 |
+
|
| 16 |
+
def __init__(self, encoder: TransformerTextEncoder, decision_size: int = 256):
|
| 17 |
+
super().__init__()
|
| 18 |
+
self.encoder = encoder
|
| 19 |
+
self.raw_expert = RawExpert(encoder.hidden_size, decision_size)
|
| 20 |
+
self.criterion_projection = nn.Linear(encoder.hidden_size, decision_size)
|
| 21 |
+
self.candidate_scorer = CandidateScorer(decision_size)
|
| 22 |
+
self.noul_head = NoulHead(decision_size)
|
| 23 |
+
self.decision_size = decision_size
|
| 24 |
+
|
| 25 |
+
@classmethod
|
| 26 |
+
def from_pretrained(
|
| 27 |
+
cls, name: str = "distilbert/distilbert-base-uncased", decision_size: int = 256,
|
| 28 |
+
max_length: int = 512,
|
| 29 |
+
) -> ConceptMemoryModel:
|
| 30 |
+
return cls(TransformerTextEncoder.from_pretrained(name, max_length), decision_size)
|
| 31 |
+
|
| 32 |
+
def encode_state(self, state: Mapping[str, object] | str) -> Tensor:
|
| 33 |
+
return self.encoder([serialize_state(state)])[0]
|
| 34 |
+
|
| 35 |
+
def forward_question(self, state_embedding: Tensor, question: Question) -> TaskOutput:
|
| 36 |
+
question_embedding = self.encoder([question.instructions])[0]
|
| 37 |
+
decision = self.raw_expert(state_embedding, question_embedding)
|
| 38 |
+
if question.type in ("choice", "score"):
|
| 39 |
+
criteria = question.normalized_criteria()
|
| 40 |
+
descriptions = [criterion.description for criterion in criteria]
|
| 41 |
+
criteria_embeddings = self.criterion_projection(self.encoder(descriptions))
|
| 42 |
+
logits = self.candidate_scorer(decision, criteria_embeddings)
|
| 43 |
+
probabilities = logits.softmax(dim=-1)
|
| 44 |
+
prediction = logits.argmax(dim=-1)
|
| 45 |
+
expected_score = None
|
| 46 |
+
if question.type == "score":
|
| 47 |
+
positions = torch.arange(logits.numel(), device=logits.device, dtype=logits.dtype)
|
| 48 |
+
expected_score = (probabilities * positions).sum()
|
| 49 |
+
return TaskOutput(question.id, question.type, logits, probabilities, prediction, expected_score)
|
| 50 |
+
if question.type == "noul":
|
| 51 |
+
logit = self.noul_head(decision)
|
| 52 |
+
probability = logit.sigmoid()
|
| 53 |
+
return TaskOutput(question.id, question.type, logit, probability, probability >= 0.5)
|
| 54 |
+
raise ValueError(f"Unsupported question type: {question.type!r}")
|
| 55 |
+
|
| 56 |
+
def forward(
|
| 57 |
+
self, state: Mapping[str, object] | str, questions: Sequence[Question]
|
| 58 |
+
) -> list[TaskOutput]:
|
| 59 |
+
state_embedding = self.encode_state(state)
|
| 60 |
+
return [self.forward_question(state_embedding, question) for question in questions]
|
| 61 |
+
|
| 62 |
+
@torch.inference_mode()
|
| 63 |
+
def predict(
|
| 64 |
+
self, state: Mapping[str, object] | str, questions: Sequence[Question]
|
| 65 |
+
) -> list[TaskOutput]:
|
| 66 |
+
self.eval()
|
| 67 |
+
return self(state, questions)
|
concept_memory/types.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
from typing import Literal, TypeAlias
|
| 5 |
+
|
| 6 |
+
from torch import Tensor
|
| 7 |
+
|
| 8 |
+
QuestionType: TypeAlias = Literal["choice", "score", "noul"]
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@dataclass(frozen=True)
|
| 12 |
+
class Criterion:
|
| 13 |
+
id: str
|
| 14 |
+
description: str
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass(frozen=True)
|
| 18 |
+
class Question:
|
| 19 |
+
id: str
|
| 20 |
+
type: QuestionType
|
| 21 |
+
instructions: str
|
| 22 |
+
criteria: list[Criterion | str | dict[str, str]] = field(default_factory=list)
|
| 23 |
+
|
| 24 |
+
def normalized_criteria(self) -> list[Criterion]:
|
| 25 |
+
result: list[Criterion] = []
|
| 26 |
+
for index, item in enumerate(self.criteria):
|
| 27 |
+
if isinstance(item, Criterion):
|
| 28 |
+
result.append(item)
|
| 29 |
+
elif isinstance(item, str):
|
| 30 |
+
result.append(Criterion(str(index), item))
|
| 31 |
+
else:
|
| 32 |
+
result.append(Criterion(item.get("id", str(index)), item["description"]))
|
| 33 |
+
if self.type in ("choice", "score") and not result:
|
| 34 |
+
raise ValueError(f"Question {self.id!r} of type {self.type!r} needs criteria")
|
| 35 |
+
return result
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@dataclass
|
| 39 |
+
class TaskOutput:
|
| 40 |
+
question_id: str
|
| 41 |
+
type: QuestionType
|
| 42 |
+
logits: object
|
| 43 |
+
probabilities: Tensor
|
| 44 |
+
prediction: object
|
| 45 |
+
expected_score: object | None = None
|