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 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."""
@@ -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