classone 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- classone/__init__.py +48 -0
- classone/data/__init__.py +1 -0
- classone/data/dataset.py +175 -0
- classone/modeling/__init__.py +27 -0
- classone/modeling/configuration_classone.py +29 -0
- classone/modeling/loss.py +131 -0
- classone/modeling/modeling_classone.py +357 -0
- classone/schemas.py +143 -0
- classone/sdk.py +290 -0
- classone/server/__init__.py +1 -0
- classone/server/app.py +131 -0
- classone/tokenizer.py +215 -0
- classone/train/__init__.py +1 -0
- classone/train/trainer.py +334 -0
- classone-0.1.0.dist-info/METADATA +152 -0
- classone-0.1.0.dist-info/RECORD +19 -0
- classone-0.1.0.dist-info/WHEEL +5 -0
- classone-0.1.0.dist-info/licenses/LICENSE +189 -0
- classone-0.1.0.dist-info/top_level.txt +1 -0
classone/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""ClassOne: Open-source System 1 Decision Model Architecture built on Gemma 4 E2B."""
|
|
2
|
+
|
|
3
|
+
from classone.modeling import ClassOneConfig, ClassOneModel
|
|
4
|
+
from classone.schemas import (
|
|
5
|
+
ChoiceQuestion,
|
|
6
|
+
ChoiceResult,
|
|
7
|
+
DecisionRequest,
|
|
8
|
+
DecisionResponse,
|
|
9
|
+
DecisionUsage,
|
|
10
|
+
NoulQuestion,
|
|
11
|
+
NoulResult,
|
|
12
|
+
ScoreQuestion,
|
|
13
|
+
ScoreResult,
|
|
14
|
+
)
|
|
15
|
+
from classone.sdk import (
|
|
16
|
+
AsyncClassOneClient,
|
|
17
|
+
Choice,
|
|
18
|
+
ChoiceAnswer,
|
|
19
|
+
ClassOneClient,
|
|
20
|
+
ClassOneResponse,
|
|
21
|
+
Noul,
|
|
22
|
+
NoulAnswer,
|
|
23
|
+
Score,
|
|
24
|
+
ScoreAnswer,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"Choice",
|
|
29
|
+
"ChoiceAnswer",
|
|
30
|
+
"ChoiceQuestion",
|
|
31
|
+
"ChoiceResult",
|
|
32
|
+
"ClassOneClient",
|
|
33
|
+
"AsyncClassOneClient",
|
|
34
|
+
"ClassOneConfig",
|
|
35
|
+
"ClassOneModel",
|
|
36
|
+
"ClassOneResponse",
|
|
37
|
+
"DecisionRequest",
|
|
38
|
+
"DecisionResponse",
|
|
39
|
+
"DecisionUsage",
|
|
40
|
+
"Noul",
|
|
41
|
+
"NoulAnswer",
|
|
42
|
+
"NoulQuestion",
|
|
43
|
+
"NoulResult",
|
|
44
|
+
"Score",
|
|
45
|
+
"ScoreAnswer",
|
|
46
|
+
"ScoreQuestion",
|
|
47
|
+
"ScoreResult",
|
|
48
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Data handling and formatting pipelines for ClassOne."""
|
classone/data/dataset.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Dataset pipeline and task formatters for ClassOne System One training.
|
|
2
|
+
|
|
3
|
+
Provides:
|
|
4
|
+
- Converters for Intent/Routing datasets into Choice questions
|
|
5
|
+
- Converters for Moderation/Verification datasets into Noul questions
|
|
6
|
+
- Converters for Rating/Severity datasets into Score questions
|
|
7
|
+
- Multi-task sample combiners (merging multiple questions per state)
|
|
8
|
+
- PyTorch Dataset and DataLoader integration
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import random
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from torch.utils.data import DataLoader, Dataset
|
|
18
|
+
|
|
19
|
+
from classone.schemas import ChoiceQuestion, NoulQuestion, Question, ScoreQuestion
|
|
20
|
+
from classone.train.trainer import TrainingItem
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_noul_item(
|
|
24
|
+
state: str | dict[str, Any] | list[Any],
|
|
25
|
+
instruction: str,
|
|
26
|
+
target: bool | float,
|
|
27
|
+
question_id: str = "q_noul",
|
|
28
|
+
) -> TrainingItem:
|
|
29
|
+
"""Creates a TrainingItem with a single Noul boolean verification question."""
|
|
30
|
+
target_val = 1.0 if target is True or target == 1 else (0.0 if target is False or target == 0 else float(target))
|
|
31
|
+
return TrainingItem(
|
|
32
|
+
state=state,
|
|
33
|
+
questions={question_id: NoulQuestion(instructions=instruction)},
|
|
34
|
+
targets={question_id: target_val},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def create_choice_item(
|
|
39
|
+
state: str | dict[str, Any] | list[Any],
|
|
40
|
+
instruction: str,
|
|
41
|
+
target_key: str,
|
|
42
|
+
all_criteria: dict[str, str],
|
|
43
|
+
max_options: int | None = None,
|
|
44
|
+
question_id: str = "q_choice",
|
|
45
|
+
) -> TrainingItem:
|
|
46
|
+
"""Creates a TrainingItem with a Choice classification question.
|
|
47
|
+
|
|
48
|
+
If max_options is specified and smaller than total categories, subsamples
|
|
49
|
+
distractor options while always including the target_key.
|
|
50
|
+
"""
|
|
51
|
+
if target_key not in all_criteria:
|
|
52
|
+
raise ValueError(f"target_key '{target_key}' not found in all_criteria keys.")
|
|
53
|
+
|
|
54
|
+
if max_options and max_options < len(all_criteria):
|
|
55
|
+
distractor_keys = [k for k in all_criteria if k != target_key]
|
|
56
|
+
num_distractors = min(len(distractor_keys), max(1, max_options - 1))
|
|
57
|
+
sampled_keys = random.sample(distractor_keys, num_distractors) + [target_key]
|
|
58
|
+
random.shuffle(sampled_keys)
|
|
59
|
+
criteria = {k: all_criteria[k] for k in sampled_keys}
|
|
60
|
+
else:
|
|
61
|
+
criteria = dict(all_criteria)
|
|
62
|
+
|
|
63
|
+
return TrainingItem(
|
|
64
|
+
state=state,
|
|
65
|
+
questions={
|
|
66
|
+
question_id: ChoiceQuestion(
|
|
67
|
+
instructions=instruction,
|
|
68
|
+
criteria=criteria,
|
|
69
|
+
)
|
|
70
|
+
},
|
|
71
|
+
targets={question_id: target_key},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def create_score_item(
|
|
76
|
+
state: str | dict[str, Any] | list[Any],
|
|
77
|
+
instruction: str,
|
|
78
|
+
target_level: float | str,
|
|
79
|
+
rubric: list[str],
|
|
80
|
+
question_id: str = "q_score",
|
|
81
|
+
) -> TrainingItem:
|
|
82
|
+
"""Creates a TrainingItem with an ordinal Score rubric question."""
|
|
83
|
+
return TrainingItem(
|
|
84
|
+
state=state,
|
|
85
|
+
questions={
|
|
86
|
+
question_id: ScoreQuestion(
|
|
87
|
+
instructions=instruction,
|
|
88
|
+
criteria=rubric,
|
|
89
|
+
)
|
|
90
|
+
},
|
|
91
|
+
targets={question_id: target_level},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def bundle_multi_task_items(
|
|
96
|
+
state: str | dict[str, Any] | list[Any],
|
|
97
|
+
questions: dict[str, Question],
|
|
98
|
+
targets: dict[str, int | float | str],
|
|
99
|
+
) -> TrainingItem:
|
|
100
|
+
"""Bundles multiple parallel questions over a single shared input state."""
|
|
101
|
+
return TrainingItem(state=state, questions=questions, targets=targets)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ClassOneDataset(Dataset):
|
|
105
|
+
"""PyTorch Dataset containing ClassOne TrainingItems."""
|
|
106
|
+
|
|
107
|
+
def __init__(self, items: list[TrainingItem]):
|
|
108
|
+
self.items = items
|
|
109
|
+
|
|
110
|
+
def __len__(self) -> int:
|
|
111
|
+
return len(self.items)
|
|
112
|
+
|
|
113
|
+
def __getitem__(self, idx: int) -> TrainingItem:
|
|
114
|
+
return self.items[idx]
|
|
115
|
+
|
|
116
|
+
def save_jsonl(self, path: str):
|
|
117
|
+
"""Exports dataset items to JSONL format."""
|
|
118
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
119
|
+
for item in self.items:
|
|
120
|
+
serialized_questions = {}
|
|
121
|
+
for q_id, q in item.questions.items():
|
|
122
|
+
serialized_questions[q_id] = q.model_dump()
|
|
123
|
+
|
|
124
|
+
row = {
|
|
125
|
+
"state": item.state,
|
|
126
|
+
"questions": serialized_questions,
|
|
127
|
+
"targets": item.targets,
|
|
128
|
+
}
|
|
129
|
+
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def load_jsonl(cls, path: str) -> ClassOneDataset:
|
|
133
|
+
"""Loads dataset from JSONL format."""
|
|
134
|
+
items: list[TrainingItem] = []
|
|
135
|
+
with open(path, encoding="utf-8") as f:
|
|
136
|
+
for line in f:
|
|
137
|
+
if not line.strip():
|
|
138
|
+
continue
|
|
139
|
+
row = json.loads(line)
|
|
140
|
+
questions: dict[str, Question] = {}
|
|
141
|
+
for q_id, q_data in row["questions"].items():
|
|
142
|
+
q_type = q_data.get("type")
|
|
143
|
+
if q_type == "noul":
|
|
144
|
+
questions[q_id] = NoulQuestion(**q_data)
|
|
145
|
+
elif q_type == "choice":
|
|
146
|
+
questions[q_id] = ChoiceQuestion(**q_data)
|
|
147
|
+
elif q_type == "score":
|
|
148
|
+
questions[q_id] = ScoreQuestion(**q_data)
|
|
149
|
+
items.append(
|
|
150
|
+
TrainingItem(
|
|
151
|
+
state=row["state"],
|
|
152
|
+
questions=questions,
|
|
153
|
+
targets=row["targets"],
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
return cls(items)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def classone_collate_fn(batch: list[TrainingItem]) -> list[TrainingItem]:
|
|
160
|
+
"""Collate function for PyTorch DataLoader preserving List[TrainingItem]."""
|
|
161
|
+
return batch
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def create_classone_dataloader(
|
|
165
|
+
dataset: ClassOneDataset,
|
|
166
|
+
batch_size: int = 4,
|
|
167
|
+
shuffle: bool = True,
|
|
168
|
+
) -> DataLoader:
|
|
169
|
+
"""Creates a PyTorch DataLoader yielding batches of TrainingItem."""
|
|
170
|
+
return DataLoader(
|
|
171
|
+
dataset,
|
|
172
|
+
batch_size=batch_size,
|
|
173
|
+
shuffle=shuffle,
|
|
174
|
+
collate_fn=classone_collate_fn,
|
|
175
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Modeling components for ClassOne System 1 decision models."""
|
|
2
|
+
|
|
3
|
+
from classone.modeling.configuration_classone import ClassOneConfig
|
|
4
|
+
from classone.modeling.loss import (
|
|
5
|
+
RLCDLoss,
|
|
6
|
+
brier_score_binary,
|
|
7
|
+
brier_score_multiclass,
|
|
8
|
+
expected_calibration_error,
|
|
9
|
+
)
|
|
10
|
+
from classone.modeling.modeling_classone import (
|
|
11
|
+
ChoiceHead,
|
|
12
|
+
ClassOneModel,
|
|
13
|
+
NoulHead,
|
|
14
|
+
ScoreHead,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ClassOneModel",
|
|
19
|
+
"ClassOneConfig",
|
|
20
|
+
"NoulHead",
|
|
21
|
+
"ChoiceHead",
|
|
22
|
+
"ScoreHead",
|
|
23
|
+
"RLCDLoss",
|
|
24
|
+
"brier_score_binary",
|
|
25
|
+
"brier_score_multiclass",
|
|
26
|
+
"expected_calibration_error",
|
|
27
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Configuration for ClassOne System 1 decision models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from transformers import PretrainedConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ClassOneConfig(PretrainedConfig):
|
|
9
|
+
"""Configuration class for ClassOne decision model."""
|
|
10
|
+
|
|
11
|
+
model_type = "classone"
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
base_model_name_or_path: str = "google/gemma-2-2b-it",
|
|
16
|
+
hidden_size: int = 2048,
|
|
17
|
+
head_hidden_size: int = 512,
|
|
18
|
+
dropout: float = 0.1,
|
|
19
|
+
temperature: float = 1.0,
|
|
20
|
+
use_temperature_scaling: bool = True,
|
|
21
|
+
**kwargs,
|
|
22
|
+
):
|
|
23
|
+
super().__init__(**kwargs)
|
|
24
|
+
self.base_model_name_or_path = base_model_name_or_path
|
|
25
|
+
self.hidden_size = hidden_size
|
|
26
|
+
self.head_hidden_size = head_hidden_size
|
|
27
|
+
self.dropout = dropout
|
|
28
|
+
self.temperature = temperature
|
|
29
|
+
self.use_temperature_scaling = use_temperature_scaling
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Proper scoring rules and calibration loss functions for RLCD.
|
|
2
|
+
|
|
3
|
+
Implements:
|
|
4
|
+
- Brier Score Loss (normalized mean squared error of probability distributions)
|
|
5
|
+
Reference: Brier, G. W. (1950). Verification of forecasts expressed in terms of probability.
|
|
6
|
+
Reference: Gneiting, T., & Raftery, A. E. (2007). Strictly proper scoring rules, prediction, and estimation.
|
|
7
|
+
- Logarithmic Scoring Rule (Cross-Entropy / Negative Log-Likelihood)
|
|
8
|
+
- Expected Calibration Error (ECE)
|
|
9
|
+
Reference: Guo, C., Pleiss, G., Sun, Y., & Weinberger, K. Q. (2017). On calibration of modern neural networks.
|
|
10
|
+
- Hybrid RLCD Loss combining log-loss and Brier penalty
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import torch
|
|
16
|
+
import torch.nn.functional as F
|
|
17
|
+
from torch import nn
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def brier_score_binary(probs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
21
|
+
"""Computes binary Brier score: (p - y)^2 in [0, 1].
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
probs: Predicted probability tensor of shape (N,) or (N, 1), in [0, 1].
|
|
25
|
+
targets: Binary label tensor of shape (N,) or (N, 1), in {0, 1}.
|
|
26
|
+
"""
|
|
27
|
+
return torch.mean((probs.view(-1).float() - targets.view(-1).float()) ** 2)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def brier_score_multiclass(probs: torch.Tensor, targets: torch.Tensor, num_classes: int) -> torch.Tensor:
|
|
31
|
+
"""Computes normalized multiclass Brier score: 0.5 * sum_k (p_k - y_k)^2 in [0, 1].
|
|
32
|
+
|
|
33
|
+
Dividing by 2 is the standard normalization in modern probabilistic forecasting
|
|
34
|
+
and machine learning (e.g. Gneiting & Raftery 2007), ensuring that for K=2 classes
|
|
35
|
+
it reduces identically to binary Brier score (p - y)^2.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
probs: Probability distribution tensor of shape (N, K).
|
|
39
|
+
targets: Ground truth class index tensor of shape (N,).
|
|
40
|
+
num_classes: Number of candidate classes K.
|
|
41
|
+
"""
|
|
42
|
+
probs_f = probs.float()
|
|
43
|
+
one_hot = F.one_hot(targets, num_classes=num_classes).float()
|
|
44
|
+
return 0.5 * torch.mean(torch.sum((probs_f - one_hot) ** 2, dim=-1))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def expected_calibration_error(probs: torch.Tensor, targets: torch.Tensor, n_bins: int = 10) -> float:
|
|
48
|
+
"""Calculates the Expected Calibration Error (ECE) across confidence bins.
|
|
49
|
+
|
|
50
|
+
Reference: Guo et al. (ICML 2017) "On Calibration of Modern Neural Networks"
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
probs: Model confidence/probability for the predicted class, shape (N,).
|
|
54
|
+
targets: 1 if prediction is correct, 0 otherwise, shape (N,).
|
|
55
|
+
n_bins: Number of confidence bins.
|
|
56
|
+
"""
|
|
57
|
+
probs = probs.view(-1).detach().cpu().float()
|
|
58
|
+
targets = targets.view(-1).detach().cpu().float()
|
|
59
|
+
|
|
60
|
+
n_samples = len(probs)
|
|
61
|
+
if n_samples == 0:
|
|
62
|
+
return 0.0
|
|
63
|
+
|
|
64
|
+
bin_boundaries = torch.linspace(0, 1, n_bins + 1)
|
|
65
|
+
ece = 0.0
|
|
66
|
+
|
|
67
|
+
for i in range(n_bins):
|
|
68
|
+
bin_lower = bin_boundaries[i]
|
|
69
|
+
bin_upper = bin_boundaries[i + 1]
|
|
70
|
+
|
|
71
|
+
# For the first bin, include 0.0 with >= to prevent exclusion of zero predictions
|
|
72
|
+
if i == 0:
|
|
73
|
+
in_bin = (probs >= bin_lower) & (probs <= bin_upper)
|
|
74
|
+
else:
|
|
75
|
+
in_bin = (probs > bin_lower) & (probs <= bin_upper)
|
|
76
|
+
|
|
77
|
+
count_in_bin = in_bin.sum().item()
|
|
78
|
+
if count_in_bin > 0:
|
|
79
|
+
accuracy_in_bin = targets[in_bin].mean().item()
|
|
80
|
+
avg_confidence_in_bin = probs[in_bin].mean().item()
|
|
81
|
+
ece += abs(avg_confidence_in_bin - accuracy_in_bin) * (count_in_bin / n_samples)
|
|
82
|
+
|
|
83
|
+
return float(ece)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class RLCDLoss(nn.Module):
|
|
87
|
+
"""Reinforcement Learning for Calibrated Decisions (RLCD) multi-objective loss.
|
|
88
|
+
|
|
89
|
+
Combines Logarithmic Scoring (proper scoring rule rewarding true likelihood)
|
|
90
|
+
with Brier Score (penalizing probability deviation).
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
brier_weight: float = 0.5,
|
|
96
|
+
log_weight: float = 1.0,
|
|
97
|
+
eps: float = 1e-6,
|
|
98
|
+
):
|
|
99
|
+
super().__init__()
|
|
100
|
+
self.brier_weight = brier_weight
|
|
101
|
+
self.log_weight = log_weight
|
|
102
|
+
self.eps = eps
|
|
103
|
+
|
|
104
|
+
def forward_binary(self, prob: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
|
|
105
|
+
"""Loss for Noul boolean decisions."""
|
|
106
|
+
prob_f = prob.view(-1).float()
|
|
107
|
+
target_f = target.view(-1).float()
|
|
108
|
+
prob_clamped = torch.clamp(prob_f, self.eps, 1.0 - self.eps)
|
|
109
|
+
|
|
110
|
+
# Binary log loss
|
|
111
|
+
log_loss = -(target_f * torch.log(prob_clamped) + (1.0 - target_f) * torch.log(1.0 - prob_clamped)).mean()
|
|
112
|
+
|
|
113
|
+
# Un-clamped Brier loss for pure proper scoring
|
|
114
|
+
brier = torch.mean((prob_f - target_f) ** 2)
|
|
115
|
+
|
|
116
|
+
return self.log_weight * log_loss + self.brier_weight * brier
|
|
117
|
+
|
|
118
|
+
def forward_multiclass(self, probs: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
|
|
119
|
+
"""Loss for Choice / Score decisions."""
|
|
120
|
+
num_classes = probs.shape[-1]
|
|
121
|
+
probs_f = probs.float()
|
|
122
|
+
one_hot = F.one_hot(targets, num_classes=num_classes).float()
|
|
123
|
+
|
|
124
|
+
# Log loss computed with stable FP32 clamping
|
|
125
|
+
probs_clamped = torch.clamp(probs_f, self.eps, 1.0 - self.eps)
|
|
126
|
+
log_loss = -torch.sum(one_hot * torch.log(probs_clamped), dim=-1).mean()
|
|
127
|
+
|
|
128
|
+
# Normalized multiclass Brier score (0.5 * sum squared difference)
|
|
129
|
+
brier = 0.5 * torch.mean(torch.sum((probs_f - one_hot) ** 2, dim=-1))
|
|
130
|
+
|
|
131
|
+
return self.log_weight * log_loss + self.brier_weight * brier
|