trustgraph-cloze 2.10.14__tar.gz
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.
- trustgraph_cloze-2.10.14/PKG-INFO +20 -0
- trustgraph_cloze-2.10.14/README.md +1 -0
- trustgraph_cloze-2.10.14/pyproject.toml +37 -0
- trustgraph_cloze-2.10.14/setup.cfg +4 -0
- trustgraph_cloze-2.10.14/trustgraph/__init__.py +1 -0
- trustgraph_cloze-2.10.14/trustgraph/cloze_version.py +1 -0
- trustgraph_cloze-2.10.14/trustgraph/model/__init__.py +1 -0
- trustgraph_cloze-2.10.14/trustgraph/model/cloze/__init__.py +1 -0
- trustgraph_cloze-2.10.14/trustgraph/model/cloze/hf/__init__.py +2 -0
- trustgraph_cloze-2.10.14/trustgraph/model/cloze/hf/__main__.py +6 -0
- trustgraph_cloze-2.10.14/trustgraph/model/cloze/hf/service.py +537 -0
- trustgraph_cloze-2.10.14/trustgraph_cloze.egg-info/PKG-INFO +20 -0
- trustgraph_cloze-2.10.14/trustgraph_cloze.egg-info/SOURCES.txt +15 -0
- trustgraph_cloze-2.10.14/trustgraph_cloze.egg-info/dependency_links.txt +1 -0
- trustgraph_cloze-2.10.14/trustgraph_cloze.egg-info/entry_points.txt +2 -0
- trustgraph_cloze-2.10.14/trustgraph_cloze.egg-info/requires.txt +8 -0
- trustgraph_cloze-2.10.14/trustgraph_cloze.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trustgraph-cloze
|
|
3
|
+
Version: 2.10.14
|
|
4
|
+
Summary: Cloze model classification service for TrustGraph.
|
|
5
|
+
Author-email: "trustgraph.ai" <security@trustgraph.ai>
|
|
6
|
+
Project-URL: Homepage, https://github.com/trustgraph-ai/trustgraph
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: trustgraph-base<2.11,>=2.10
|
|
12
|
+
Requires-Dist: trustgraph-flow<2.11,>=2.10
|
|
13
|
+
Requires-Dist: torch
|
|
14
|
+
Requires-Dist: transformers
|
|
15
|
+
Requires-Dist: scikit-learn
|
|
16
|
+
Requires-Dist: pulsar-client
|
|
17
|
+
Requires-Dist: pyyaml
|
|
18
|
+
Requires-Dist: prometheus-client
|
|
19
|
+
|
|
20
|
+
Cloze model service for TrustGraph.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Cloze model service for TrustGraph.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "trustgraph-cloze"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
authors = [{name = "trustgraph.ai", email = "security@trustgraph.ai"}]
|
|
9
|
+
description = "Cloze model classification service for TrustGraph."
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"trustgraph-base>=2.10,<2.11",
|
|
14
|
+
"trustgraph-flow>=2.10,<2.11",
|
|
15
|
+
"torch",
|
|
16
|
+
"transformers",
|
|
17
|
+
"scikit-learn",
|
|
18
|
+
"pulsar-client",
|
|
19
|
+
"pyyaml",
|
|
20
|
+
"prometheus-client",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/trustgraph-ai/trustgraph"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
cloze-hf = "trustgraph.model.cloze.hf:run"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
include = ["trustgraph*"]
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.dynamic]
|
|
37
|
+
version = {attr = "trustgraph.cloze_version.__version__"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.10.14"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
|
|
2
|
+
"""
|
|
3
|
+
Cloze model service. Auto-trains a masked language model from labeled
|
|
4
|
+
examples in config, and serves single-label classification with
|
|
5
|
+
confidence scores.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import torch
|
|
12
|
+
from functools import partial
|
|
13
|
+
from sklearn.model_selection import train_test_split
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
from trustgraph.schema import ClozeRequest, ClozeResponse, ClozeModelInfo, Error
|
|
17
|
+
from trustgraph.base import FlowProcessor, ConsumerSpec, ProducerSpec
|
|
18
|
+
from trustgraph.base import ParameterSpec
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
default_ident = "cloze"
|
|
23
|
+
default_model = "distilbert-base-uncased"
|
|
24
|
+
|
|
25
|
+
TRAINING_CONFIG_TYPE = "cloze-training"
|
|
26
|
+
|
|
27
|
+
DEFAULT_TEST_SPLIT = 0.2
|
|
28
|
+
DEFAULT_EPOCHS = 20
|
|
29
|
+
DEFAULT_BATCH_SIZE = 6
|
|
30
|
+
DEFAULT_BACKBONE_LR = 2e-5
|
|
31
|
+
DEFAULT_HEAD_LR = 1e-4
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class TrainedModel:
|
|
36
|
+
model: object = None
|
|
37
|
+
tokenizer: object = None
|
|
38
|
+
tool_tokens: list = field(default_factory=list)
|
|
39
|
+
tool_token_ids: list = field(default_factory=list)
|
|
40
|
+
device: object = None
|
|
41
|
+
accuracy: float | None = None
|
|
42
|
+
train_size: int = 0
|
|
43
|
+
test_size: int = 0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Processor(FlowProcessor):
|
|
47
|
+
|
|
48
|
+
def __init__(self, **params):
|
|
49
|
+
|
|
50
|
+
model = params.get("model", default_model)
|
|
51
|
+
concurrency = params.get("concurrency", 2)
|
|
52
|
+
|
|
53
|
+
super(Processor, self).__init__(
|
|
54
|
+
**params | {
|
|
55
|
+
"model": model,
|
|
56
|
+
"concurrency": concurrency,
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
self.base_model_name = model
|
|
61
|
+
|
|
62
|
+
self.register_specification(
|
|
63
|
+
ParameterSpec(name="model")
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
self.register_config_handler(
|
|
67
|
+
self.on_training_config, types=[TRAINING_CONFIG_TYPE]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
self.register_specification(
|
|
71
|
+
ConsumerSpec(
|
|
72
|
+
name="request",
|
|
73
|
+
schema=ClozeRequest,
|
|
74
|
+
handler=self.on_request,
|
|
75
|
+
concurrency=concurrency,
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
self.register_specification(
|
|
80
|
+
ProducerSpec(
|
|
81
|
+
name="response",
|
|
82
|
+
schema=ClozeResponse,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Keyed by model instance name
|
|
87
|
+
self.models = {}
|
|
88
|
+
|
|
89
|
+
# Tracks models currently being trained: {(workspace, instance_name)}
|
|
90
|
+
self.training = set()
|
|
91
|
+
|
|
92
|
+
async def on_training_config(self, workspace, config, version):
|
|
93
|
+
|
|
94
|
+
logger.info(
|
|
95
|
+
f"Loading cloze training configuration version {version} "
|
|
96
|
+
f"for workspace {workspace}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if TRAINING_CONFIG_TYPE not in config:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
training_configs = config[TRAINING_CONFIG_TYPE]
|
|
103
|
+
|
|
104
|
+
if workspace not in self.models:
|
|
105
|
+
self.models[workspace] = {}
|
|
106
|
+
|
|
107
|
+
# Track which instances were updated
|
|
108
|
+
updated = set()
|
|
109
|
+
|
|
110
|
+
for instance_name, raw_value in training_configs.items():
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
training_data = json.loads(raw_value)
|
|
114
|
+
training_key = (workspace, instance_name)
|
|
115
|
+
self.training.add(training_key)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
loop = asyncio.get_event_loop()
|
|
119
|
+
trained = await loop.run_in_executor(
|
|
120
|
+
None,
|
|
121
|
+
partial(
|
|
122
|
+
self._train_model, instance_name, training_data,
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
finally:
|
|
126
|
+
self.training.discard(training_key)
|
|
127
|
+
|
|
128
|
+
if trained is not None:
|
|
129
|
+
self.models[workspace][instance_name] = trained
|
|
130
|
+
updated.add(instance_name)
|
|
131
|
+
logger.info(
|
|
132
|
+
f"Model instance '{instance_name}' trained "
|
|
133
|
+
f"for workspace {workspace}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.error(
|
|
138
|
+
f"Failed to train model instance '{instance_name}' "
|
|
139
|
+
f"for workspace {workspace}: {e}"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Remove instances no longer in config
|
|
143
|
+
for name in list(self.models[workspace].keys()):
|
|
144
|
+
if name not in training_configs:
|
|
145
|
+
del self.models[workspace][name]
|
|
146
|
+
logger.info(
|
|
147
|
+
f"Removed model instance '{name}' "
|
|
148
|
+
f"for workspace {workspace}"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def _train_model(self, instance_name, training_data):
|
|
152
|
+
|
|
153
|
+
from transformers import AutoTokenizer, AutoModelForMaskedLM
|
|
154
|
+
from torch.utils.data import Dataset, DataLoader
|
|
155
|
+
from torch.optim import AdamW
|
|
156
|
+
|
|
157
|
+
labels = training_data.get("labels", [])
|
|
158
|
+
if not labels:
|
|
159
|
+
logger.warning(
|
|
160
|
+
f"No labels in training data for '{instance_name}'"
|
|
161
|
+
)
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
min_accuracy = training_data.get("min-accuracy")
|
|
165
|
+
test_split = training_data.get("test-split", DEFAULT_TEST_SPLIT)
|
|
166
|
+
epochs = training_data.get("epochs", DEFAULT_EPOCHS)
|
|
167
|
+
|
|
168
|
+
# Collect all examples and tool tokens
|
|
169
|
+
tool_tokens = []
|
|
170
|
+
seed_words = {}
|
|
171
|
+
all_train_examples = []
|
|
172
|
+
all_test_examples = []
|
|
173
|
+
|
|
174
|
+
for label_def in labels:
|
|
175
|
+
token = label_def["token"]
|
|
176
|
+
seed = label_def["seed"]
|
|
177
|
+
examples = label_def.get("examples", [])
|
|
178
|
+
|
|
179
|
+
tool_tokens.append(token)
|
|
180
|
+
seed_words[token] = seed
|
|
181
|
+
|
|
182
|
+
if test_split > 0 and len(examples) >= 2:
|
|
183
|
+
train_ex, test_ex = train_test_split(
|
|
184
|
+
examples, test_size=test_split, random_state=42
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
train_ex = examples
|
|
188
|
+
test_ex = []
|
|
189
|
+
|
|
190
|
+
for ex in train_ex:
|
|
191
|
+
all_train_examples.append({"query": ex, "tool": token})
|
|
192
|
+
for ex in test_ex:
|
|
193
|
+
all_test_examples.append({"query": ex, "tool": token})
|
|
194
|
+
|
|
195
|
+
if not all_train_examples:
|
|
196
|
+
logger.warning(
|
|
197
|
+
f"No training examples for '{instance_name}'"
|
|
198
|
+
)
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
logger.info(
|
|
202
|
+
f"Training '{instance_name}': {len(all_train_examples)} train, "
|
|
203
|
+
f"{len(all_test_examples)} test examples, "
|
|
204
|
+
f"{len(tool_tokens)} labels"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Load base model and tokenizer
|
|
208
|
+
tokenizer = AutoTokenizer.from_pretrained(self.base_model_name)
|
|
209
|
+
model = AutoModelForMaskedLM.from_pretrained(self.base_model_name)
|
|
210
|
+
|
|
211
|
+
# Register tool tokens
|
|
212
|
+
tokenizer.add_special_tokens(
|
|
213
|
+
{"additional_special_tokens": tool_tokens}
|
|
214
|
+
)
|
|
215
|
+
model.resize_token_embeddings(len(tokenizer))
|
|
216
|
+
|
|
217
|
+
# Warm-start embeddings from seed words
|
|
218
|
+
with torch.no_grad():
|
|
219
|
+
input_embeddings = model.get_input_embeddings().weight
|
|
220
|
+
output_embeddings = (
|
|
221
|
+
model.get_output_embeddings().weight
|
|
222
|
+
if model.get_output_embeddings() is not None
|
|
223
|
+
else input_embeddings
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
for tool_token, base_word in seed_words.items():
|
|
227
|
+
tool_id = tokenizer.convert_tokens_to_ids(tool_token)
|
|
228
|
+
base_id = tokenizer.convert_tokens_to_ids(base_word)
|
|
229
|
+
|
|
230
|
+
input_embeddings[tool_id] = (
|
|
231
|
+
input_embeddings[base_id].clone()
|
|
232
|
+
)
|
|
233
|
+
if output_embeddings is not input_embeddings:
|
|
234
|
+
output_embeddings[tool_id] = (
|
|
235
|
+
output_embeddings[base_id].clone()
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Build datasets
|
|
239
|
+
class ClozeDataset(Dataset):
|
|
240
|
+
def __init__(self, data, tokenizer, max_length=48):
|
|
241
|
+
self.data = data
|
|
242
|
+
self.tokenizer = tokenizer
|
|
243
|
+
self.max_length = max_length
|
|
244
|
+
|
|
245
|
+
def __len__(self):
|
|
246
|
+
return len(self.data)
|
|
247
|
+
|
|
248
|
+
def __getitem__(self, idx):
|
|
249
|
+
item = self.data[idx]
|
|
250
|
+
prompt = (
|
|
251
|
+
f"Query: {item['query']} "
|
|
252
|
+
f"Tool: {self.tokenizer.mask_token}"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
encoding = self.tokenizer(
|
|
256
|
+
prompt,
|
|
257
|
+
padding="max_length",
|
|
258
|
+
truncation=True,
|
|
259
|
+
max_length=self.max_length,
|
|
260
|
+
return_tensors="pt"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
input_ids = encoding["input_ids"].squeeze(0)
|
|
264
|
+
attention_mask = encoding["attention_mask"].squeeze(0)
|
|
265
|
+
|
|
266
|
+
labels = torch.full_like(input_ids, -100)
|
|
267
|
+
mask_positions = (
|
|
268
|
+
input_ids == self.tokenizer.mask_token_id
|
|
269
|
+
).nonzero(as_tuple=True)[0]
|
|
270
|
+
|
|
271
|
+
target_token_id = self.tokenizer.convert_tokens_to_ids(
|
|
272
|
+
item["tool"]
|
|
273
|
+
)
|
|
274
|
+
labels[mask_positions[0]] = target_token_id
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
"input_ids": input_ids,
|
|
278
|
+
"attention_mask": attention_mask,
|
|
279
|
+
"labels": labels,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
device = torch.device(
|
|
283
|
+
"cuda" if torch.cuda.is_available()
|
|
284
|
+
else (
|
|
285
|
+
"mps" if torch.backends.mps.is_available()
|
|
286
|
+
else "cpu"
|
|
287
|
+
)
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
model.to(device)
|
|
291
|
+
|
|
292
|
+
train_dataset = ClozeDataset(all_train_examples, tokenizer)
|
|
293
|
+
train_loader = DataLoader(
|
|
294
|
+
train_dataset, batch_size=DEFAULT_BATCH_SIZE, shuffle=True
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Differential learning rates
|
|
298
|
+
backbone_params = [
|
|
299
|
+
p for n, p in model.named_parameters()
|
|
300
|
+
if "vocab" not in n and "embeddings" not in n
|
|
301
|
+
]
|
|
302
|
+
head_params = [
|
|
303
|
+
p for n, p in model.named_parameters()
|
|
304
|
+
if "vocab" in n or "embeddings" in n
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
optimizer = AdamW([
|
|
308
|
+
{
|
|
309
|
+
"params": backbone_params,
|
|
310
|
+
"lr": DEFAULT_BACKBONE_LR,
|
|
311
|
+
"weight_decay": 0.01,
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
"params": head_params,
|
|
315
|
+
"lr": DEFAULT_HEAD_LR,
|
|
316
|
+
"weight_decay": 0.01,
|
|
317
|
+
},
|
|
318
|
+
])
|
|
319
|
+
|
|
320
|
+
# Training loop
|
|
321
|
+
model.train()
|
|
322
|
+
for epoch in range(1, epochs + 1):
|
|
323
|
+
total_loss = 0.0
|
|
324
|
+
for batch in train_loader:
|
|
325
|
+
optimizer.zero_grad()
|
|
326
|
+
|
|
327
|
+
input_ids = batch["input_ids"].to(device)
|
|
328
|
+
attention_mask = batch["attention_mask"].to(device)
|
|
329
|
+
labels = batch["labels"].to(device)
|
|
330
|
+
|
|
331
|
+
outputs = model(
|
|
332
|
+
input_ids=input_ids,
|
|
333
|
+
attention_mask=attention_mask,
|
|
334
|
+
labels=labels,
|
|
335
|
+
)
|
|
336
|
+
loss = outputs.loss
|
|
337
|
+
|
|
338
|
+
loss.backward()
|
|
339
|
+
optimizer.step()
|
|
340
|
+
total_loss += loss.item()
|
|
341
|
+
|
|
342
|
+
avg_loss = total_loss / len(train_loader)
|
|
343
|
+
if epoch % 10 == 0 or epoch == 1:
|
|
344
|
+
logger.info(
|
|
345
|
+
f" [{instance_name}] Epoch {epoch:02d}/"
|
|
346
|
+
f"{epochs:02d} - Loss: {avg_loss:.4f}"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Evaluate if we have test data
|
|
350
|
+
tool_token_ids = [
|
|
351
|
+
tokenizer.convert_tokens_to_ids(t) for t in tool_tokens
|
|
352
|
+
]
|
|
353
|
+
|
|
354
|
+
accuracy = None
|
|
355
|
+
|
|
356
|
+
if all_test_examples:
|
|
357
|
+
model.eval()
|
|
358
|
+
correct = 0
|
|
359
|
+
|
|
360
|
+
with torch.no_grad():
|
|
361
|
+
for item in all_test_examples:
|
|
362
|
+
prompt = (
|
|
363
|
+
f"Query: {item['query']} "
|
|
364
|
+
f"Tool: {tokenizer.mask_token}"
|
|
365
|
+
)
|
|
366
|
+
inputs = tokenizer(
|
|
367
|
+
prompt, return_tensors="pt"
|
|
368
|
+
).to(device)
|
|
369
|
+
|
|
370
|
+
outputs = model(**inputs)
|
|
371
|
+
logits = outputs.logits
|
|
372
|
+
|
|
373
|
+
mask_idx = (
|
|
374
|
+
inputs["input_ids"] == tokenizer.mask_token_id
|
|
375
|
+
).nonzero(as_tuple=True)[1][0]
|
|
376
|
+
mask_logits = logits[0, mask_idx, :]
|
|
377
|
+
|
|
378
|
+
candidate_logits = mask_logits[tool_token_ids]
|
|
379
|
+
best_tool_idx = torch.argmax(candidate_logits).item()
|
|
380
|
+
best_tool_token = tool_tokens[best_tool_idx]
|
|
381
|
+
|
|
382
|
+
if best_tool_token == item["tool"]:
|
|
383
|
+
correct += 1
|
|
384
|
+
|
|
385
|
+
accuracy = correct / len(all_test_examples)
|
|
386
|
+
logger.info(
|
|
387
|
+
f" [{instance_name}] Test accuracy: "
|
|
388
|
+
f"{correct}/{len(all_test_examples)} ({accuracy:.1%})"
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
if min_accuracy is not None and accuracy < min_accuracy:
|
|
392
|
+
logger.error(
|
|
393
|
+
f" [{instance_name}] Accuracy {accuracy:.1%} below "
|
|
394
|
+
f"minimum threshold {min_accuracy:.1%}, "
|
|
395
|
+
f"rejecting model"
|
|
396
|
+
)
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
return TrainedModel(
|
|
400
|
+
model=model,
|
|
401
|
+
tokenizer=tokenizer,
|
|
402
|
+
tool_tokens=tool_tokens,
|
|
403
|
+
tool_token_ids=tool_token_ids,
|
|
404
|
+
device=device,
|
|
405
|
+
accuracy=accuracy,
|
|
406
|
+
train_size=len(all_train_examples),
|
|
407
|
+
test_size=len(all_test_examples),
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
async def on_request(self, msg, consumer, flow):
|
|
411
|
+
|
|
412
|
+
request = msg.value()
|
|
413
|
+
id = msg.properties()["id"]
|
|
414
|
+
producer = flow("response")
|
|
415
|
+
|
|
416
|
+
workspace = flow.workspace
|
|
417
|
+
operation = request.operation or "predict"
|
|
418
|
+
|
|
419
|
+
if operation == "list-models":
|
|
420
|
+
response = self._list_models(workspace)
|
|
421
|
+
await producer.send(response, properties={"id": id})
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
instance_name = request.model
|
|
425
|
+
query = request.query
|
|
426
|
+
|
|
427
|
+
ws_models = self.models.get(workspace, {})
|
|
428
|
+
trained = ws_models.get(instance_name)
|
|
429
|
+
|
|
430
|
+
if trained is None:
|
|
431
|
+
if (workspace, instance_name) in self.training:
|
|
432
|
+
error_type = "model-training"
|
|
433
|
+
error_message = (
|
|
434
|
+
f"Model instance '{instance_name}' is currently "
|
|
435
|
+
f"training in workspace '{workspace}'"
|
|
436
|
+
)
|
|
437
|
+
else:
|
|
438
|
+
error_type = "model-not-found"
|
|
439
|
+
error_message = (
|
|
440
|
+
f"No trained model instance '{instance_name}' "
|
|
441
|
+
f"in workspace '{workspace}'"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
response = ClozeResponse(
|
|
445
|
+
error=Error(
|
|
446
|
+
type=error_type,
|
|
447
|
+
message=error_message,
|
|
448
|
+
),
|
|
449
|
+
)
|
|
450
|
+
await producer.send(response, properties={"id": id})
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
label, confidence = self._predict(trained, query)
|
|
455
|
+
response = ClozeResponse(
|
|
456
|
+
label=label,
|
|
457
|
+
confidence=confidence,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
except Exception as e:
|
|
461
|
+
logger.error(f"Prediction error: {e}")
|
|
462
|
+
response = ClozeResponse(
|
|
463
|
+
error=Error(
|
|
464
|
+
type="prediction-error",
|
|
465
|
+
message=str(e),
|
|
466
|
+
),
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
await producer.send(response, properties={"id": id})
|
|
470
|
+
|
|
471
|
+
def _list_models(self, workspace):
|
|
472
|
+
|
|
473
|
+
models = []
|
|
474
|
+
|
|
475
|
+
ws_models = self.models.get(workspace, {})
|
|
476
|
+
for name, trained in ws_models.items():
|
|
477
|
+
models.append(ClozeModelInfo(
|
|
478
|
+
name=name,
|
|
479
|
+
state="ready",
|
|
480
|
+
labels=list(trained.tool_tokens),
|
|
481
|
+
accuracy=trained.accuracy,
|
|
482
|
+
train_size=trained.train_size,
|
|
483
|
+
test_size=trained.test_size,
|
|
484
|
+
))
|
|
485
|
+
|
|
486
|
+
for ws, instance_name in self.training:
|
|
487
|
+
if ws == workspace and instance_name not in ws_models:
|
|
488
|
+
models.append(ClozeModelInfo(
|
|
489
|
+
name=instance_name,
|
|
490
|
+
state="training",
|
|
491
|
+
))
|
|
492
|
+
|
|
493
|
+
return ClozeResponse(models=models)
|
|
494
|
+
|
|
495
|
+
def _predict(self, trained, query):
|
|
496
|
+
|
|
497
|
+
trained.model.eval()
|
|
498
|
+
|
|
499
|
+
prompt = (
|
|
500
|
+
f"Query: {query} Tool: {trained.tokenizer.mask_token}"
|
|
501
|
+
)
|
|
502
|
+
inputs = trained.tokenizer(
|
|
503
|
+
prompt, return_tensors="pt"
|
|
504
|
+
).to(trained.device)
|
|
505
|
+
|
|
506
|
+
with torch.no_grad():
|
|
507
|
+
outputs = trained.model(**inputs)
|
|
508
|
+
logits = outputs.logits
|
|
509
|
+
|
|
510
|
+
mask_idx = (
|
|
511
|
+
inputs["input_ids"] == trained.tokenizer.mask_token_id
|
|
512
|
+
).nonzero(as_tuple=True)[1][0]
|
|
513
|
+
mask_logits = logits[0, mask_idx, :]
|
|
514
|
+
|
|
515
|
+
candidate_logits = mask_logits[trained.tool_token_ids]
|
|
516
|
+
candidate_probs = torch.softmax(candidate_logits, dim=-1)
|
|
517
|
+
best_idx = torch.argmax(candidate_probs).item()
|
|
518
|
+
|
|
519
|
+
label = trained.tool_tokens[best_idx]
|
|
520
|
+
confidence = candidate_probs[best_idx].item()
|
|
521
|
+
|
|
522
|
+
return label, confidence
|
|
523
|
+
|
|
524
|
+
@staticmethod
|
|
525
|
+
def add_args(parser):
|
|
526
|
+
|
|
527
|
+
FlowProcessor.add_args(parser)
|
|
528
|
+
|
|
529
|
+
parser.add_argument(
|
|
530
|
+
'-m', '--model',
|
|
531
|
+
default=default_model,
|
|
532
|
+
help=f'Base model for fine-tuning (default: {default_model})'
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
def run():
|
|
536
|
+
|
|
537
|
+
Processor.launch(default_ident, __doc__)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trustgraph-cloze
|
|
3
|
+
Version: 2.10.14
|
|
4
|
+
Summary: Cloze model classification service for TrustGraph.
|
|
5
|
+
Author-email: "trustgraph.ai" <security@trustgraph.ai>
|
|
6
|
+
Project-URL: Homepage, https://github.com/trustgraph-ai/trustgraph
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: trustgraph-base<2.11,>=2.10
|
|
12
|
+
Requires-Dist: trustgraph-flow<2.11,>=2.10
|
|
13
|
+
Requires-Dist: torch
|
|
14
|
+
Requires-Dist: transformers
|
|
15
|
+
Requires-Dist: scikit-learn
|
|
16
|
+
Requires-Dist: pulsar-client
|
|
17
|
+
Requires-Dist: pyyaml
|
|
18
|
+
Requires-Dist: prometheus-client
|
|
19
|
+
|
|
20
|
+
Cloze model service for TrustGraph.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
trustgraph/__init__.py
|
|
4
|
+
trustgraph/cloze_version.py
|
|
5
|
+
trustgraph/model/__init__.py
|
|
6
|
+
trustgraph/model/cloze/__init__.py
|
|
7
|
+
trustgraph/model/cloze/hf/__init__.py
|
|
8
|
+
trustgraph/model/cloze/hf/__main__.py
|
|
9
|
+
trustgraph/model/cloze/hf/service.py
|
|
10
|
+
trustgraph_cloze.egg-info/PKG-INFO
|
|
11
|
+
trustgraph_cloze.egg-info/SOURCES.txt
|
|
12
|
+
trustgraph_cloze.egg-info/dependency_links.txt
|
|
13
|
+
trustgraph_cloze.egg-info/entry_points.txt
|
|
14
|
+
trustgraph_cloze.egg-info/requires.txt
|
|
15
|
+
trustgraph_cloze.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
trustgraph
|