dewmal commited on
Commit
3155e2a
·
verified ·
1 Parent(s): 0d55260

Upload Concept Memory base model

Browse files
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 model code and the Hub client:
29
 
30
  ```bash
31
- pip install "git+https://github.com/ceylonai/rgcm.git" huggingface_hub
32
  ```
33
 
34
- Download the checkpoint, construct a question with the criteria you want the
35
- model to judge, and inspect its prediction and probabilities:
 
 
36
 
37
  ```python
38
  import torch
39
- from huggingface_hub import hf_hub_download
 
 
 
 
 
 
 
40
  from concept_memory import ConceptMemoryModel
41
  from concept_memory.types import Question
42
 
43
- repo_id = "Syigen/rgcm"
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