@theseus-whale/dsh-continuity 0.0.0-stage → 4.12.2
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.
- package/INSTALL.md +25 -0
- package/PANEL.md +92 -0
- package/README.md +286 -2
- package/RELEASE-NOTES.md +732 -0
- package/bin/bc-acceptance-full.mjs +91 -0
- package/bin/bc-acceptance-tight.mjs +66 -0
- package/bin/bc-acceptance.mjs +22 -0
- package/bin/bc-add-keywords.mjs +21 -0
- package/bin/bc-demo.mjs +47 -0
- package/bin/bc-expand-hint.mjs +179 -0
- package/bin/bc-ingest-index.mjs +35 -0
- package/bin/bc-ingest.mjs +31 -0
- package/bin/bc-label-demo.mjs +18 -0
- package/bin/bc-relabel.mjs +150 -0
- package/bin/bc-sessionize.mjs +59 -0
- package/bin/coref_pipeline.py +47 -0
- package/bin/coref_service.py +57 -0
- package/bin/debug_ckip.py +16 -0
- package/bin/debug_ner_zh.py +18 -0
- package/bin/doctor.mjs +275 -0
- package/bin/en_ner_error_audit.py +33 -0
- package/bin/eval-label-rules.mjs +342 -0
- package/bin/export-turns.py +68 -0
- package/bin/extract-user-paragraph.mjs +14 -0
- package/bin/extraction-composite-smoke.mjs +28 -0
- package/bin/extraction-latency.mjs +58 -0
- package/bin/extraction-user-smoke.mjs +28 -0
- package/bin/extraction_service.py +164 -0
- package/bin/facet-active-experiment.mjs +110 -0
- package/bin/facet-covid.mjs +8 -0
- package/bin/facet-curve.mjs +27 -0
- package/bin/facet-embedding-train.mjs +20 -0
- package/bin/facet-embedding-validate.mjs +83 -0
- package/bin/facet-eval-roleplay.mjs +42 -0
- package/bin/facet-eval.mjs +38 -0
- package/bin/facet-fold.mjs +31 -0
- package/bin/facet-load.mjs +16 -0
- package/bin/facet-mlp-scan.mjs +45 -0
- package/bin/facet-mvp.mjs +34 -0
- package/bin/facet-online-experiments.mjs +78 -0
- package/bin/facet-online-race.mjs +99 -0
- package/bin/facet-online-validate.mjs +51 -0
- package/bin/facet-panel.mjs +22 -0
- package/bin/facet-passage.mjs +41 -0
- package/bin/facet-random-validate.mjs +48 -0
- package/bin/facet-roleplay.mjs +33 -0
- package/bin/facet-scan.mjs +56 -0
- package/bin/facet-segment.mjs +34 -0
- package/bin/facet-train.mjs +19 -0
- package/bin/facet-validate.mjs +103 -0
- package/bin/facet-zeroshot.mjs +14 -0
- package/bin/install-templates.mjs +72 -0
- package/bin/memory-bench.mjs +53 -0
- package/bin/memory-build-summary.mjs +43 -0
- package/bin/memory-build-vector.mjs +31 -0
- package/bin/memory-build.mjs +41 -0
- package/bin/memory-calibration.mjs +70 -0
- package/bin/memory-closed-loop.mjs +119 -0
- package/bin/memory-dry-run.mjs +70 -0
- package/bin/memory-eval.mjs +104 -0
- package/bin/memory-event-alias-test.mjs +89 -0
- package/bin/memory-extraction-bench.mjs +58 -0
- package/bin/memory-load-gate-replay.mjs +137 -0
- package/bin/memory-loop-test.mjs +90 -0
- package/bin/memory-pre-turn-harness.mjs +63 -0
- package/bin/memory-query.mjs +39 -0
- package/bin/memory-replay-hints.mjs +84 -0
- package/bin/memory-scale.mjs +58 -0
- package/bin/memory-scenario-test.mjs +146 -0
- package/bin/memory-user-loop.mjs +95 -0
- package/bin/ner_bench_tiny_zh.py +59 -0
- package/bin/ner_bench_zh.py +60 -0
- package/bin/ner_conll_en.py +139 -0
- package/bin/ner_direct_en.py +123 -0
- package/bin/ner_direct_zh.py +100 -0
- package/bin/ner_pipeline.py +208 -0
- package/bin/noise-selfcheck.mjs +50 -0
- package/bin/pack-integrity.mjs +47 -0
- package/bin/panel-check.mjs +57 -0
- package/bin/plugin-integrity.mjs +74 -0
- package/bin/preview-live.mjs +13 -0
- package/bin/public-install-check.mjs +222 -0
- package/bin/relabel-apply.mjs +272 -0
- package/bin/relabel-dry-run.mjs +381 -0
- package/bin/relik_pipeline.py +39 -0
- package/bin/relik_service.py +52 -0
- package/bin/srl_pipeline.py +55 -0
- package/bin/srl_service.py +68 -0
- package/bin/summary-index.mjs +151 -0
- package/bin/summary-query.mjs +79 -0
- package/bin/vec-check.mjs +27 -0
- package/bin/zh_ner_error_audit.py +36 -0
- package/bin/zh_ner_slot_eval.py +38 -0
- package/client.js +760 -0
- package/cordis.patch.yml +378 -0
- package/docs/API-NOTES.md +1743 -0
- package/docs/ARCHITECTURE.md +355 -0
- package/docs/CACHE.md +253 -0
- package/docs/CONDUCT.md +30 -0
- package/docs/CREATOR-HANDOFF.md +102 -0
- package/docs/EXPERIMENT.md +136 -0
- package/docs/FACET-MVP-REPORT.md +473 -0
- package/docs/GUIDE-0.4.66.md +195 -0
- package/docs/GUIDE.md +127 -0
- package/docs/MECHANISM.md +243 -0
- package/docs/NEW-INSTANCE-HANDOFF.md +219 -0
- package/docs/README.md +37 -0
- package/docs/SESSION-LAYOUT.md +151 -0
- package/docs/history-map.md +249 -0
- package/docs/relabel-migration-wiring.md +366 -0
- package/guides/00-READ-FIRST.md +206 -0
- package/guides/01-INSTALL.md +207 -0
- package/guides/02-TABLES.md +225 -0
- package/guides/03-AUDIT.md +201 -0
- package/guides/04-REBUILD.md +306 -0
- package/guides/05-FIRST-RUN.md +191 -0
- package/guides/06-EMBEDDING.md +146 -0
- package/guides/07-PTC-AND-ASSETS.md +221 -0
- package/icon.svg +7 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/models/README.md +13 -0
- package/models/Xenova/bge-small-zh-v1.5/config.json +31 -0
- package/models/Xenova/bge-small-zh-v1.5/onnx/model_quantized.onnx +0 -0
- package/models/Xenova/bge-small-zh-v1.5/tokenizer.json +21278 -0
- package/models/Xenova/bge-small-zh-v1.5/tokenizer_config.json +15 -0
- package/package.json +139 -4
- package/src/asset-tool.mjs +237 -0
- package/src/assets.mjs +244 -0
- package/src/backfill.mjs +74 -0
- package/src/backup.mjs +80 -0
- package/src/deadend.mjs +95 -0
- package/src/deleted-store.mjs +35 -0
- package/src/engine.mjs +3831 -0
- package/src/facets/classifier.mjs +146 -0
- package/src/facets/embedding-service.mjs +53 -0
- package/src/facets/embedding-worker.mjs +27 -0
- package/src/facets/embedding.mjs +163 -0
- package/src/facets/extract.mjs +80 -0
- package/src/facets/fold-plan.mjs +84 -0
- package/src/facets/hybrid.mjs +29 -0
- package/src/facets/index.mjs +113 -0
- package/src/facets/linear-probe.mjs +43 -0
- package/src/facets/load.mjs +37 -0
- package/src/facets/mlp-probe.mjs +54 -0
- package/src/facets/mlp-service.mjs +53 -0
- package/src/facets/mlp-worker.mjs +26 -0
- package/src/facets/online-knn.mjs +58 -0
- package/src/facets/online-models.mjs +227 -0
- package/src/facets/panel-state.mjs +23 -0
- package/src/facets/panel.mjs +73 -0
- package/src/facets/random.mjs +26 -0
- package/src/facets/scanner.mjs +35 -0
- package/src/facets/schema.mjs +75 -0
- package/src/facets/seed-examples.mjs +58 -0
- package/src/facets/sensitivity.mjs +98 -0
- package/src/facets/service.mjs +53 -0
- package/src/facets/store.mjs +39 -0
- package/src/facets/worker.mjs +23 -0
- package/src/folds-store.mjs +109 -0
- package/src/groups.mjs +167 -0
- package/src/handoff-tool.mjs +51 -0
- package/src/handoff.mjs +127 -0
- package/src/index.mjs +649 -0
- package/src/inject-policy.mjs +17 -0
- package/src/learning.mjs +88 -0
- package/src/memory/alias-proposal.mjs +40 -0
- package/src/memory/bc-audit.mjs +245 -0
- package/src/memory/bc-index-updater.mjs +404 -0
- package/src/memory/bc-index.mjs +48 -0
- package/src/memory/bc-label.mjs +220 -0
- package/src/memory/bc-panel-state.mjs +107 -0
- package/src/memory/bc-pre-step.mjs +463 -0
- package/src/memory/bc-recall.mjs +470 -0
- package/src/memory/bc-shift.mjs +123 -0
- package/src/memory/bc-store.mjs +41 -0
- package/src/memory/bm25.mjs +112 -0
- package/src/memory/calibration.mjs +54 -0
- package/src/memory/chatter.mjs +29 -0
- package/src/memory/cmap-file.mjs +107 -0
- package/src/memory/concepts-file.mjs +41 -0
- package/src/memory/coref-slots.mjs +40 -0
- package/src/memory/event-alias-ledger.mjs +87 -0
- package/src/memory/event-log.mjs +95 -0
- package/src/memory/event-model.mjs +132 -0
- package/src/memory/event-segmenter.mjs +36 -0
- package/src/memory/event-summary-tree.mjs +122 -0
- package/src/memory/extraction-adapter.mjs +76 -0
- package/src/memory/extraction-composite.mjs +75 -0
- package/src/memory/extraction-core.mjs +76 -0
- package/src/memory/extraction-pre-step.mjs +161 -0
- package/src/memory/extraction-provider.mjs +300 -0
- package/src/memory/extraction-score.mjs +36 -0
- package/src/memory/facet-stabilizer.mjs +59 -0
- package/src/memory/history-map.mjs +139 -0
- package/src/memory/language-router.mjs +30 -0
- package/src/memory/load-gate.mjs +161 -0
- package/src/memory/mechanical-note.mjs +104 -0
- package/src/memory/onboarding.mjs +163 -0
- package/src/memory/persona.mjs +78 -0
- package/src/memory/planner.mjs +249 -0
- package/src/memory/pre-turn.mjs +80 -0
- package/src/memory/query-aliases.mjs +39 -0
- package/src/memory/recall-cue.mjs +43 -0
- package/src/memory/recall.mjs +65 -0
- package/src/memory/referent-candidates.mjs +55 -0
- package/src/memory/referent-ledger.mjs +86 -0
- package/src/memory/relabel-migrate.mjs +376 -0
- package/src/memory/relik-adapter.mjs +25 -0
- package/src/memory/special-keywords.mjs +60 -0
- package/src/memory/structured-adapters.mjs +75 -0
- package/src/memory/summary-index.mjs +335 -0
- package/src/memory/summary-tree.mjs +110 -0
- package/src/memory/term-extractor.mjs +44 -0
- package/src/memory/tokenize.mjs +9 -0
- package/src/memory/vector-index.mjs +54 -0
- package/src/memory/window-summary.mjs +133 -0
- package/src/mood.mjs +133 -0
- package/src/peer-tool.mjs +63 -0
- package/src/peer.mjs +144 -0
- package/src/policy.mjs +170 -0
- package/src/preflight.mjs +113 -0
- package/src/provenance.mjs +94 -0
- package/src/range.mjs +75 -0
- package/src/recall-exact-tool.mjs +267 -0
- package/src/recall-tool.mjs +521 -0
- package/src/recheck.mjs +99 -0
- package/src/save-start.mjs +29 -0
- package/src/session-guard.mjs +140 -0
- package/src/session-log-tool.mjs +322 -0
- package/src/session-log.mjs +606 -0
- package/src/skills.mjs +111 -0
- package/src/split-range.mjs +115 -0
- package/src/state.mjs +198 -0
- package/src/t0.mjs +149 -0
- package/src/tag-confirm.mjs +78 -0
- package/src/tag-detect.mjs +42 -0
- package/src/tag-maintenance.mjs +285 -0
- package/src/tag-reject.mjs +73 -0
- package/src/tags-store.mjs +94 -0
- package/src/tags.mjs +692 -0
- package/src/threads-backend.mjs +26 -0
- package/src/threads-core.mjs +1887 -0
- package/src/threads-store.mjs +207 -0
- package/src/threads-tool.mjs +522 -0
- package/src/threads.mjs +929 -0
- package/src/tool-preview.mjs +427 -0
- package/src/topic.mjs +63 -0
- package/src/training.mjs +75 -0
- package/src/turn-tag.mjs +62 -0
- package/src/user-turn.mjs +39 -0
- package/templates/cmap.md +11 -0
- package/templates/concepts.md +12 -0
- package/templates/habits.md +40 -0
- package/templates/keywords.md +6 -0
- package/templates/rebuild.md +134 -0
- package/templates/rules-safe.md +184 -0
- package/templates/rules.md +177 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Scenario replay: new term, new→old, old→new→old, repeated recall,
|
|
4
|
+
* Coo interjection. Gate is before the turn; alias proposal is after it.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { readEventLog } from '../src/memory/event-log.mjs';
|
|
9
|
+
import { MemoryLoadState, latestCandidate, candidateSegment } from '../src/memory/load-gate.mjs';
|
|
10
|
+
import { AliasProposalState } from '../src/memory/alias-proposal.mjs';
|
|
11
|
+
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const argOf = (name) => {
|
|
14
|
+
const i = args.indexOf(name);
|
|
15
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
16
|
+
};
|
|
17
|
+
const dir = argOf('--dir');
|
|
18
|
+
if (!dir) {
|
|
19
|
+
console.error('usage: node bin/memory-scenario-test.mjs --dir .durable-memory');
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
const records = readEventLog(path.join(dir, 'events.jsonl'));
|
|
23
|
+
const dev = { activity: '开发', topic: '技术' };
|
|
24
|
+
const run = { activity: '跑团', topic: '剧情' };
|
|
25
|
+
|
|
26
|
+
const scenarios = [
|
|
27
|
+
{
|
|
28
|
+
id: 'new-term',
|
|
29
|
+
steps: [
|
|
30
|
+
{ text: '我们开个新团,叫星港', facets: run, term: '星港', gate: 'load', alias: 'none' },
|
|
31
|
+
{ text: '星港的规则是掷骰', facets: run, term: '星港', gate: 'none', alias: 'propose' },
|
|
32
|
+
{ text: '星港继续', facets: run, term: '星港', gate: 'none', alias: 'none' },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'new-old-back',
|
|
37
|
+
steps: [
|
|
38
|
+
{ text: '我们开个新团,叫星港', facets: run, term: '星港', gate: 'load', alias: 'none' },
|
|
39
|
+
{ text: '星港里有一扇门', facets: run, term: '星港', gate: 'none', alias: 'propose' },
|
|
40
|
+
{ text: '上次跑团怎么样了', facets: run, gate: 'load', alias: 'none' },
|
|
41
|
+
{ text: '回到星港', facets: run, term: '星港', gate: 'none', alias: 'none' },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'old-new-old',
|
|
46
|
+
steps: [
|
|
47
|
+
{ text: '规则怎么写', facets: dev, gate: 'none', alias: 'none' },
|
|
48
|
+
{ text: '开始跑团', facets: run, gate: 'load', alias: 'none' },
|
|
49
|
+
{ text: '星港的规则是掷骰', facets: run, term: '星港', gate: 'none', alias: 'none' },
|
|
50
|
+
{ text: '星港继续', facets: run, term: '星港', gate: 'none', alias: 'propose' },
|
|
51
|
+
{ text: '上次规则写哪了', facets: dev, gate: 'load', alias: 'none' },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'repeat-recall',
|
|
56
|
+
steps: [
|
|
57
|
+
{ text: '上次跑团怎么样了', facets: run, gate: 'load', alias: 'none' },
|
|
58
|
+
{ text: '上次跑团后来呢', facets: run, gate: 'load', alias: 'none' },
|
|
59
|
+
{ text: '继续跑团', facets: run, gate: 'none', alias: 'none' },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'coo-interject',
|
|
64
|
+
steps: [
|
|
65
|
+
{ text: 'Coo:来一局', facets: run, gate: 'load', alias: 'none', role: 'coo' },
|
|
66
|
+
{ text: 'Coo:灯室见', facets: run, gate: 'none', alias: 'none', role: 'coo' },
|
|
67
|
+
{ text: '我推门进去', facets: run, gate: 'none', alias: 'none' },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'known-alias',
|
|
72
|
+
steps: [
|
|
73
|
+
{ text: 'TRPG 开一把', facets: run, term: 'TRPG', gate: 'load', alias: 'none' },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const output = [];
|
|
79
|
+
for (const scenario of scenarios) {
|
|
80
|
+
const loadState = new MemoryLoadState({ loadedFacets: dev, loadedAtTurn: 0 });
|
|
81
|
+
const aliasState = new AliasProposalState();
|
|
82
|
+
const events = [];
|
|
83
|
+
for (let index = 0; index < scenario.steps.length; index += 1) {
|
|
84
|
+
const step = scenario.steps[index];
|
|
85
|
+
const nowTurn = 100 + index;
|
|
86
|
+
const candidate = latestCandidate(records, 'activity', step.facets.activity);
|
|
87
|
+
const segment = candidateSegment(records, candidate);
|
|
88
|
+
const gate = loadState.decide({
|
|
89
|
+
text: step.text,
|
|
90
|
+
currentFacets: step.facets,
|
|
91
|
+
confidence: 0.9,
|
|
92
|
+
nowTurn,
|
|
93
|
+
hasCandidate: candidate !== null,
|
|
94
|
+
candidate,
|
|
95
|
+
role: step.role ?? 'user',
|
|
96
|
+
});
|
|
97
|
+
if (gate.action === 'load') {
|
|
98
|
+
loadState.recordLoad({ facets: step.facets, seq: segment?.seqStart ?? null, nowTurn, reason: gate.reason });
|
|
99
|
+
}
|
|
100
|
+
const alias = aliasState.observe({ term: step.term, facets: step.facets, nowTurn });
|
|
101
|
+
events.push({
|
|
102
|
+
text: step.text,
|
|
103
|
+
gate: gate.action,
|
|
104
|
+
gateReason: gate.reason,
|
|
105
|
+
expectedGate: step.gate,
|
|
106
|
+
alias: alias.action,
|
|
107
|
+
aliasReason: alias.reason,
|
|
108
|
+
expectedAlias: step.alias,
|
|
109
|
+
aliasText: alias.text ?? null,
|
|
110
|
+
candidateTurn: candidate?.turn ?? null,
|
|
111
|
+
segmentTurns: segment?.turns ?? 0,
|
|
112
|
+
position: { gate: 'before-response', alias: 'after-turn' },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const gatePass = events.filter((event) => event.gate === event.expectedGate).length;
|
|
116
|
+
const aliasPass = events.filter((event) => event.alias === event.expectedAlias).length;
|
|
117
|
+
output.push({
|
|
118
|
+
id: scenario.id,
|
|
119
|
+
steps: events.length,
|
|
120
|
+
gatePass,
|
|
121
|
+
aliasPass,
|
|
122
|
+
pass: gatePass === events.length && aliasPass === events.length,
|
|
123
|
+
events,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const summary = {
|
|
127
|
+
scenarios: output.length,
|
|
128
|
+
steps: output.reduce((sum, item) => sum + item.steps, 0),
|
|
129
|
+
passScenarios: output.filter((item) => item.pass).length,
|
|
130
|
+
failedScenarios: output.filter((item) => !item.pass).map((item) => item.id),
|
|
131
|
+
missedAliasProposals: output.flatMap((item) => item.events.filter((event) => event.expectedAlias === 'propose' && event.alias !== 'propose')).length,
|
|
132
|
+
extraAliasProposals: output.flatMap((item) => item.events.filter((event) => event.expectedAlias !== 'propose' && event.alias === 'propose')).length,
|
|
133
|
+
};
|
|
134
|
+
const result = { dir, summary, output };
|
|
135
|
+
fs.writeFileSync(path.join(dir, 'SCENARIO-TEST.json'), JSON.stringify(result, null, 2));
|
|
136
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
137
|
+
for (const scenario of output) {
|
|
138
|
+
if (!scenario.pass) {
|
|
139
|
+
console.log('FAIL', scenario.id);
|
|
140
|
+
for (const event of scenario.events) {
|
|
141
|
+
if (event.gate !== event.expectedGate || event.alias !== event.expectedAlias) {
|
|
142
|
+
console.log(' ', event.text, 'gate', event.gate, 'expected', event.expectedGate, 'alias', event.alias, 'expected', event.expectedAlias);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Real user-message loop: BGE + MLP facets + NER slots + pre-turn gate.
|
|
4
|
+
* No live session IO.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { readEventLog } from '../src/memory/event-log.mjs';
|
|
9
|
+
import { PreTurnController } from '../src/memory/pre-turn.mjs';
|
|
10
|
+
import { MemoryLoadState } from '../src/memory/load-gate.mjs';
|
|
11
|
+
import { attachExtraction } from '../src/memory/extraction-adapter.mjs';
|
|
12
|
+
import { embedTexts } from '../src/facets/embedding.mjs';
|
|
13
|
+
import { deserializeMlp, predictMlp } from '../src/facets/mlp-probe.mjs';
|
|
14
|
+
import { BM25Index } from '../src/memory/bm25.mjs';
|
|
15
|
+
import { VectorIndex } from '../src/memory/vector-index.mjs';
|
|
16
|
+
import { summaryIndex } from '../src/memory/planner.mjs';
|
|
17
|
+
import { stabilizeFacets } from '../src/memory/facet-stabilizer.mjs';
|
|
18
|
+
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
const argOf = (name) => {
|
|
21
|
+
const i = args.indexOf(name);
|
|
22
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
23
|
+
};
|
|
24
|
+
const dir = argOf('--dir');
|
|
25
|
+
const nerFile = argOf('--ner');
|
|
26
|
+
const modelPath = argOf('--model');
|
|
27
|
+
if (!dir || !nerFile || !modelPath) {
|
|
28
|
+
console.error('usage: node bin/memory-user-loop.mjs --dir .durable-memory --ner USER-NER.json --model MLP-MODEL.json');
|
|
29
|
+
process.exit(2);
|
|
30
|
+
}
|
|
31
|
+
const records = readEventLog(path.join(dir, 'events.jsonl'));
|
|
32
|
+
const ner = JSON.parse(fs.readFileSync(nerFile, 'utf8'));
|
|
33
|
+
const model = deserializeMlp(JSON.parse(fs.readFileSync(modelPath, 'utf8')));
|
|
34
|
+
const bm25 = BM25Index.fromJSON(JSON.parse(fs.readFileSync(path.join(dir, 'bm25.json'), 'utf8')));
|
|
35
|
+
const vectorIndex = VectorIndex.fromJSON(JSON.parse(fs.readFileSync(path.join(dir, 'vectors.json'), 'utf8')));
|
|
36
|
+
const tree = JSON.parse(fs.readFileSync(path.join(dir, 'summary-tree.json'), 'utf8'));
|
|
37
|
+
const summaries = summaryIndex(tree);
|
|
38
|
+
const texts = ner.rows.map((row) => row.text);
|
|
39
|
+
const vectors = await embedTexts(texts);
|
|
40
|
+
const controller = new PreTurnController({
|
|
41
|
+
records,
|
|
42
|
+
gate: new MemoryLoadState({ loadedFacets: { activity: '开发', topic: '技术' }, loadedAtTurn: 0 }),
|
|
43
|
+
});
|
|
44
|
+
const rows = [];
|
|
45
|
+
for (let index = 0; index < ner.rows.length; index += 1) {
|
|
46
|
+
const item = ner.rows[index];
|
|
47
|
+
const prediction = predictMlp(model, vectors[index]);
|
|
48
|
+
const keyword = bm25.search(item.text, { topK: 5, minCoverage: 0.3 });
|
|
49
|
+
const semantic = vectorIndex.search(vectors[index], { topK: 5 });
|
|
50
|
+
const summary = summaries.search(item.text, { topK: 3, minCoverage: 0.3 });
|
|
51
|
+
const stable = stabilizeFacets({
|
|
52
|
+
raw: prediction,
|
|
53
|
+
text: item.text,
|
|
54
|
+
records,
|
|
55
|
+
keyword,
|
|
56
|
+
semantic,
|
|
57
|
+
summary,
|
|
58
|
+
});
|
|
59
|
+
const result = attachExtraction(controller, {
|
|
60
|
+
text: item.text,
|
|
61
|
+
entities: item.entities,
|
|
62
|
+
slots: item.slots,
|
|
63
|
+
facets: stable.facets,
|
|
64
|
+
confidence: stable.confidence.activity,
|
|
65
|
+
nowTurn: 1000 + index,
|
|
66
|
+
});
|
|
67
|
+
rows.push({
|
|
68
|
+
turn: item.turn,
|
|
69
|
+
text: item.text,
|
|
70
|
+
facets: stable.facets,
|
|
71
|
+
rawFacets: prediction.facets,
|
|
72
|
+
confidence: +stable.confidence.activity.toFixed(2),
|
|
73
|
+
stabilizeReason: stable.reason,
|
|
74
|
+
slots: item.slots,
|
|
75
|
+
entities: item.entities,
|
|
76
|
+
ok: result.ok,
|
|
77
|
+
action: result.gate?.action ?? 'invalid',
|
|
78
|
+
reason: result.gate?.reason ?? result.reason,
|
|
79
|
+
boundaryScore: result.gate?.boundaryScore ?? null,
|
|
80
|
+
candidateTurn: result.gate?.candidateTurn ?? null,
|
|
81
|
+
segmentTurns: result.gate?.segment?.turns ?? 0,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const summary = {
|
|
85
|
+
messages: rows.length,
|
|
86
|
+
loads: rows.filter((row) => row.action === 'load').length,
|
|
87
|
+
hints: rows.filter((row) => row.action === 'hint').length,
|
|
88
|
+
none: rows.filter((row) => row.action === 'none').length,
|
|
89
|
+
invalid: rows.filter((row) => row.action === 'invalid').length,
|
|
90
|
+
withEntities: rows.filter((row) => row.entities.length > 0).length,
|
|
91
|
+
loadsSample: rows.filter((row) => row.action === 'load').slice(0, 8).map((row) => ({ turn: row.turn, text: row.text.slice(0, 80), reason: row.reason })),
|
|
92
|
+
};
|
|
93
|
+
const output = { summary, rows };
|
|
94
|
+
fs.writeFileSync(path.join(dir, 'USER-LOOP.json'), JSON.stringify(output, null, 2));
|
|
95
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Tiny local Chinese NER smoke benchmark on CLUE-NER validation (boundary-only)."""
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from datasets import load_dataset
|
|
8
|
+
from tokenizers import BertWordPieceTokenizer
|
|
9
|
+
from transformers import AutoModelForTokenClassification, PreTrainedTokenizerFast, pipeline
|
|
10
|
+
|
|
11
|
+
MODEL_DIR = os.path.join(os.environ["TEMP"], "ckip-tiny")
|
|
12
|
+
LIMIT = int(sys.argv[1]) if len(sys.argv) > 1 else 20
|
|
13
|
+
OUT = sys.argv[2] if len(sys.argv) > 2 else None
|
|
14
|
+
|
|
15
|
+
bw = BertWordPieceTokenizer(vocab=os.path.join(MODEL_DIR, "vocab.txt"), lowercase=False)
|
|
16
|
+
tok = PreTrainedTokenizerFast(
|
|
17
|
+
tokenizer_object=bw,
|
|
18
|
+
unk_token="[UNK]",
|
|
19
|
+
sep_token="[SEP]",
|
|
20
|
+
pad_token="[PAD]",
|
|
21
|
+
cls_token="[CLS]",
|
|
22
|
+
mask_token="[MASK]",
|
|
23
|
+
)
|
|
24
|
+
model = AutoModelForTokenClassification.from_pretrained(MODEL_DIR)
|
|
25
|
+
ner = pipeline("ner", model=model, tokenizer=tok, aggregation_strategy="simple", device=-1)
|
|
26
|
+
ds = load_dataset("xusenlin/clue-ner", split="validation")
|
|
27
|
+
|
|
28
|
+
rows = []
|
|
29
|
+
tp = fp = fn = 0
|
|
30
|
+
for i in range(min(LIMIT, len(ds))):
|
|
31
|
+
item = ds[i]
|
|
32
|
+
pred = ner(item["text"])
|
|
33
|
+
pred_spans = {p["word"].replace(" ", "") for p in pred}
|
|
34
|
+
gold_spans = {e["entity"] for e in item["entities"]}
|
|
35
|
+
tp += len(pred_spans & gold_spans)
|
|
36
|
+
fp += len(pred_spans - gold_spans)
|
|
37
|
+
fn += len(gold_spans - pred_spans)
|
|
38
|
+
rows.append({
|
|
39
|
+
"text": item["text"],
|
|
40
|
+
"pred": [{"text": p["word"].replace(" ", ""), "label": p["entity_group"]} for p in pred],
|
|
41
|
+
"gold": [{"text": e["entity"], "label": e["label"]} for e in item["entities"]],
|
|
42
|
+
"pred_spans": sorted(pred_spans),
|
|
43
|
+
"gold_spans": sorted(gold_spans),
|
|
44
|
+
})
|
|
45
|
+
precision = 0 if tp + fp == 0 else tp / (tp + fp)
|
|
46
|
+
recall = 0 if tp + fn == 0 else tp / (tp + fn)
|
|
47
|
+
f1 = 0 if precision + recall == 0 else 2 * precision * recall / (precision + recall)
|
|
48
|
+
result = {
|
|
49
|
+
"model": "ckiplab/bert-tiny-chinese-ner (local)",
|
|
50
|
+
"dataset": "xusenlin/clue-ner validation",
|
|
51
|
+
"n": len(rows),
|
|
52
|
+
"boundary_only": {"tp": tp, "fp": fp, "fn": fn, "precision": round(precision, 4), "recall": round(recall, 4), "f1": round(f1, 4)},
|
|
53
|
+
"rows": rows,
|
|
54
|
+
}
|
|
55
|
+
if OUT:
|
|
56
|
+
with open(OUT, "w", encoding="utf-8") as f:
|
|
57
|
+
json.dump(result, f, ensure_ascii=False, indent=2)
|
|
58
|
+
print(json.dumps({k: v for k, v in result.items() if k != "rows"}, ensure_ascii=False, indent=2))
|
|
59
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Chinese NER benchmark: UER CLUENER model on clean CLUE-NER validation."""
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
import torch
|
|
7
|
+
from datasets import load_dataset
|
|
8
|
+
from transformers import AutoModelForTokenClassification, AutoTokenizer, pipeline
|
|
9
|
+
|
|
10
|
+
MODEL = "uer/roberta-base-finetuned-cluener2020-chinese"
|
|
11
|
+
DATASET = "xusenlin/clue-ner"
|
|
12
|
+
LIMIT = int(sys.argv[1]) if len(sys.argv) > 1 else 100
|
|
13
|
+
OUT = sys.argv[2] if len(sys.argv) > 2 else None
|
|
14
|
+
|
|
15
|
+
torch.set_num_threads(1)
|
|
16
|
+
print("loading model", flush=True)
|
|
17
|
+
tok = AutoTokenizer.from_pretrained(MODEL)
|
|
18
|
+
model = AutoModelForTokenClassification.from_pretrained(MODEL)
|
|
19
|
+
print("labels", model.config.id2label, flush=True)
|
|
20
|
+
ner = pipeline("ner", model=model, tokenizer=tok, aggregation_strategy="simple", device=-1, truncation=True, max_length=512)
|
|
21
|
+
print("loading data", flush=True)
|
|
22
|
+
ds = load_dataset(DATASET, split="validation")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def spans(pred, gold):
|
|
26
|
+
gold_keys = {(g["entity"], g["label"]) for g in gold}
|
|
27
|
+
pred_keys = {(p["word"].replace(" ", ""), p["entity_group"]) for p in pred}
|
|
28
|
+
tp = len(gold_keys & pred_keys)
|
|
29
|
+
fp = len(pred_keys - gold_keys)
|
|
30
|
+
fn = len(gold_keys - pred_keys)
|
|
31
|
+
precision = 0 if not pred_keys else tp / len(pred_keys)
|
|
32
|
+
recall = 0 if not gold_keys else tp / len(gold_keys)
|
|
33
|
+
f1 = 0 if precision + recall == 0 else 2 * precision * recall / (precision + recall)
|
|
34
|
+
return {"tp": tp, "fp": fp, "fn": fn, "precision": round(precision, 4), "recall": round(recall, 4), "f1": round(f1, 4)}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
rows = []
|
|
38
|
+
for i in range(min(LIMIT, len(ds))):
|
|
39
|
+
item = ds[i]
|
|
40
|
+
pred = ner(item["text"])
|
|
41
|
+
gold = [{"entity": e["entity"], "label": e["label"]} for e in item["entities"]]
|
|
42
|
+
rows.append({
|
|
43
|
+
"text": item["text"],
|
|
44
|
+
"pred": [{"text": p["word"].replace(" ", ""), "label": p["entity_group"]} for p in pred],
|
|
45
|
+
"gold": gold,
|
|
46
|
+
})
|
|
47
|
+
if i < 2:
|
|
48
|
+
print("sample", i, rows[-1]["pred"], rows[-1]["gold"], flush=True)
|
|
49
|
+
|
|
50
|
+
result = {
|
|
51
|
+
"model": MODEL,
|
|
52
|
+
"dataset": DATASET,
|
|
53
|
+
"n": len(rows),
|
|
54
|
+
"score": spans([p for r in rows for p in r["pred"]], [g for r in rows for g in r["gold"]]),
|
|
55
|
+
"rows": rows,
|
|
56
|
+
}
|
|
57
|
+
if OUT:
|
|
58
|
+
with open(OUT, "w", encoding="utf-8") as f:
|
|
59
|
+
json.dump(result, f, ensure_ascii=False, indent=2)
|
|
60
|
+
print(json.dumps({k: v for k, v in result.items() if k != "rows"}, ensure_ascii=False, indent=2), flush=True)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""English NER benchmark: DistilBERT CoNLL03 on the official eng.testa file."""
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
from transformers import AutoModelForTokenClassification, AutoTokenizer
|
|
9
|
+
|
|
10
|
+
MODEL = "elastic/distilbert-base-uncased-finetuned-conll03-english"
|
|
11
|
+
FILE = sys.argv[1]
|
|
12
|
+
LIMIT = int(sys.argv[2]) if len(sys.argv) > 2 else 200
|
|
13
|
+
OUT = sys.argv[3] if len(sys.argv) > 3 else None
|
|
14
|
+
MAX_LEN = 256
|
|
15
|
+
|
|
16
|
+
torch.set_num_threads(1)
|
|
17
|
+
tok = AutoTokenizer.from_pretrained(MODEL)
|
|
18
|
+
model = AutoModelForTokenClassification.from_pretrained(MODEL)
|
|
19
|
+
model.eval()
|
|
20
|
+
id2label = model.config.id2label
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def read_sentences(path):
|
|
24
|
+
sentences = []
|
|
25
|
+
tokens, tags = [], []
|
|
26
|
+
for line in open(path, encoding="utf-8"):
|
|
27
|
+
line = line.rstrip("\n")
|
|
28
|
+
if not line.strip():
|
|
29
|
+
if tokens:
|
|
30
|
+
sentences.append((tokens, tags))
|
|
31
|
+
tokens, tags = [], []
|
|
32
|
+
continue
|
|
33
|
+
parts = line.split()
|
|
34
|
+
if len(parts) < 2:
|
|
35
|
+
continue
|
|
36
|
+
tokens.append(parts[0])
|
|
37
|
+
tags.append(parts[-1])
|
|
38
|
+
if tokens:
|
|
39
|
+
sentences.append((tokens, tags))
|
|
40
|
+
return sentences
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def predict(text):
|
|
44
|
+
enc = tok(text, return_tensors="pt", truncation=True, max_length=MAX_LEN, return_offsets_mapping=True)
|
|
45
|
+
offsets = enc.pop("offset_mapping")[0].tolist()
|
|
46
|
+
with torch.no_grad():
|
|
47
|
+
logits = model(**enc).logits[0]
|
|
48
|
+
ids = logits.argmax(-1).tolist()
|
|
49
|
+
entities = []
|
|
50
|
+
current = None
|
|
51
|
+
for idx, label_id in enumerate(ids):
|
|
52
|
+
label = id2label[int(label_id)]
|
|
53
|
+
start, end = offsets[idx]
|
|
54
|
+
if start == end:
|
|
55
|
+
continue
|
|
56
|
+
if label == "O":
|
|
57
|
+
if current is not None:
|
|
58
|
+
entities.append(current)
|
|
59
|
+
current = None
|
|
60
|
+
continue
|
|
61
|
+
prefix, _, kind = label.partition("-")
|
|
62
|
+
if prefix == "B":
|
|
63
|
+
if current is not None:
|
|
64
|
+
entities.append(current)
|
|
65
|
+
current = {"text": text[start:end], "label": kind, "start": start, "end": end}
|
|
66
|
+
elif prefix == "I" and current is not None and current["label"] == kind:
|
|
67
|
+
current["text"] += text[start:end]
|
|
68
|
+
current["end"] = end
|
|
69
|
+
else:
|
|
70
|
+
if current is not None:
|
|
71
|
+
entities.append(current)
|
|
72
|
+
current = {"text": text[start:end], "label": kind, "start": start, "end": end}
|
|
73
|
+
if current is not None:
|
|
74
|
+
entities.append(current)
|
|
75
|
+
merged = []
|
|
76
|
+
for entity in entities:
|
|
77
|
+
last = merged[-1] if merged else None
|
|
78
|
+
if last is not None and last["label"] == entity["label"] and entity["start"] - last["end"] <= 1:
|
|
79
|
+
last["text"] += entity["text"]
|
|
80
|
+
last["end"] = entity["end"]
|
|
81
|
+
else:
|
|
82
|
+
merged.append(entity)
|
|
83
|
+
return merged
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def gold_entities(tokens, tags):
|
|
87
|
+
out = []
|
|
88
|
+
current = None
|
|
89
|
+
for token, tag in zip(tokens, tags):
|
|
90
|
+
if tag == "O":
|
|
91
|
+
if current is not None:
|
|
92
|
+
out.append(current)
|
|
93
|
+
current = None
|
|
94
|
+
continue
|
|
95
|
+
prefix, _, kind = tag.partition("-")
|
|
96
|
+
if prefix == "B":
|
|
97
|
+
if current is not None:
|
|
98
|
+
out.append(current)
|
|
99
|
+
current = {"text": token, "label": kind}
|
|
100
|
+
elif prefix == "I" and current is not None:
|
|
101
|
+
current["text"] += " " + token
|
|
102
|
+
else:
|
|
103
|
+
if current is not None:
|
|
104
|
+
out.append(current)
|
|
105
|
+
current = {"text": token, "label": kind}
|
|
106
|
+
if current is not None:
|
|
107
|
+
out.append(current)
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def score(pred, gold):
|
|
112
|
+
pred_keys = {(p["text"].replace(" ", ""), p["label"]) for p in pred}
|
|
113
|
+
gold_keys = {(g["text"].replace(" ", ""), g["label"]) for g in gold}
|
|
114
|
+
tp = len(pred_keys & gold_keys)
|
|
115
|
+
fp = len(pred_keys - gold_keys)
|
|
116
|
+
fn = len(gold_keys - pred_keys)
|
|
117
|
+
precision = 0 if not pred_keys else tp / len(pred_keys)
|
|
118
|
+
recall = 0 if not gold_keys else tp / len(gold_keys)
|
|
119
|
+
f1 = 0 if precision + recall == 0 else 2 * precision * recall / (precision + recall)
|
|
120
|
+
return {"tp": tp, "fp": fp, "fn": fn, "precision": round(precision, 4), "recall": round(recall, 4), "f1": round(f1, 4)}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
sentences = read_sentences(FILE)[:LIMIT]
|
|
124
|
+
rows = []
|
|
125
|
+
for i, (tokens, tags) in enumerate(sentences):
|
|
126
|
+
text = " ".join(tokens)
|
|
127
|
+
rows.append({"text": text, "pred": predict(text), "gold": gold_entities(tokens, tags)})
|
|
128
|
+
|
|
129
|
+
result = {
|
|
130
|
+
"model": MODEL,
|
|
131
|
+
"dataset": "CoNLL2003 eng.testa",
|
|
132
|
+
"n": len(rows),
|
|
133
|
+
"score": score([p for r in rows for p in r["pred"]], [g for r in rows for g in r["gold"]]),
|
|
134
|
+
"rows": rows,
|
|
135
|
+
}
|
|
136
|
+
if OUT:
|
|
137
|
+
with open(OUT, "w", encoding="utf-8") as f:
|
|
138
|
+
json.dump(result, f, ensure_ascii=False, indent=2)
|
|
139
|
+
print(json.dumps({k: v for k, v in result.items() if k != "rows"}, ensure_ascii=False, indent=2))
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""English NER benchmark: DistilBERT CoNLL03 on CoNLL2003 validation."""
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
from datasets import load_dataset
|
|
9
|
+
from transformers import AutoModelForTokenClassification, AutoTokenizer
|
|
10
|
+
|
|
11
|
+
MODEL = "elastic/distilbert-base-uncased-finetuned-conll03-english"
|
|
12
|
+
DATASET = "jon-fernandes/conll2003"
|
|
13
|
+
LIMIT = int(sys.argv[1]) if len(sys.argv) > 1 else 200
|
|
14
|
+
OUT = sys.argv[2] if len(sys.argv) > 2 else None
|
|
15
|
+
MAX_LEN = 256
|
|
16
|
+
|
|
17
|
+
torch.set_num_threads(1)
|
|
18
|
+
print("tokenizer", flush=True)
|
|
19
|
+
tok = AutoTokenizer.from_pretrained(MODEL)
|
|
20
|
+
print("model", flush=True)
|
|
21
|
+
model = AutoModelForTokenClassification.from_pretrained(MODEL)
|
|
22
|
+
model.eval()
|
|
23
|
+
id2label = model.config.id2label
|
|
24
|
+
print("data", flush=True)
|
|
25
|
+
ds = load_dataset(DATASET, split="validation")
|
|
26
|
+
print("data ok", len(ds), flush=True)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def decode(text, offsets, ids):
|
|
30
|
+
entities = []
|
|
31
|
+
current = None
|
|
32
|
+
for idx, label_id in enumerate(ids):
|
|
33
|
+
label = id2label[int(label_id)]
|
|
34
|
+
start, end = offsets[idx]
|
|
35
|
+
if start == end:
|
|
36
|
+
continue
|
|
37
|
+
if label == "O":
|
|
38
|
+
if current is not None:
|
|
39
|
+
entities.append(current)
|
|
40
|
+
current = None
|
|
41
|
+
continue
|
|
42
|
+
prefix, _, kind = label.partition("-")
|
|
43
|
+
if prefix == "B":
|
|
44
|
+
if current is not None:
|
|
45
|
+
entities.append(current)
|
|
46
|
+
current = {"text": text[start:end], "label": kind, "start": start, "end": end}
|
|
47
|
+
elif prefix == "I" and current is not None and current["label"] == kind:
|
|
48
|
+
current["text"] += text[start:end]
|
|
49
|
+
current["end"] = end
|
|
50
|
+
else:
|
|
51
|
+
if current is not None:
|
|
52
|
+
entities.append(current)
|
|
53
|
+
current = {"text": text[start:end], "label": kind, "start": start, "end": end}
|
|
54
|
+
if current is not None:
|
|
55
|
+
entities.append(current)
|
|
56
|
+
return entities
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def predict(text):
|
|
60
|
+
enc = tok(text, return_tensors="pt", truncation=True, max_length=MAX_LEN, return_offsets_mapping=True)
|
|
61
|
+
offsets = enc.pop("offset_mapping")[0].tolist()
|
|
62
|
+
with torch.no_grad():
|
|
63
|
+
logits = model(**enc).logits[0]
|
|
64
|
+
return decode(text, offsets, logits.argmax(-1).tolist())
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def gold_entities(tokens, tags):
|
|
68
|
+
out = []
|
|
69
|
+
current = None
|
|
70
|
+
for token, tag in zip(tokens, tags):
|
|
71
|
+
if tag == "O":
|
|
72
|
+
if current is not None:
|
|
73
|
+
out.append(current)
|
|
74
|
+
current = None
|
|
75
|
+
continue
|
|
76
|
+
prefix, _, kind = tag.partition("-")
|
|
77
|
+
if prefix == "B":
|
|
78
|
+
if current is not None:
|
|
79
|
+
out.append(current)
|
|
80
|
+
current = {"text": token, "label": kind}
|
|
81
|
+
elif prefix == "I" and current is not None:
|
|
82
|
+
current["text"] += " " + token
|
|
83
|
+
else:
|
|
84
|
+
if current is not None:
|
|
85
|
+
out.append(current)
|
|
86
|
+
current = {"text": token, "label": kind}
|
|
87
|
+
if current is not None:
|
|
88
|
+
out.append(current)
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def score(pred, gold):
|
|
93
|
+
pred_keys = {(p["text"].replace(" ", ""), p["label"]) for p in pred}
|
|
94
|
+
gold_keys = {(g["text"].replace(" ", ""), g["label"]) for g in gold}
|
|
95
|
+
tp = len(pred_keys & gold_keys)
|
|
96
|
+
fp = len(pred_keys - gold_keys)
|
|
97
|
+
fn = len(gold_keys - pred_keys)
|
|
98
|
+
precision = 0 if not pred_keys else tp / len(pred_keys)
|
|
99
|
+
recall = 0 if not gold_keys else tp / len(gold_keys)
|
|
100
|
+
f1 = 0 if precision + recall == 0 else 2 * precision * recall / (precision + recall)
|
|
101
|
+
return {"tp": tp, "fp": fp, "fn": fn, "precision": round(precision, 4), "recall": round(recall, 4), "f1": round(f1, 4)}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
rows = []
|
|
105
|
+
for i in range(min(LIMIT, len(ds))):
|
|
106
|
+
print("sample", i, flush=True)
|
|
107
|
+
item = ds[i]
|
|
108
|
+
text = " ".join(item["tokens"])
|
|
109
|
+
pred = predict(text)
|
|
110
|
+
gold = gold_entities(item["tokens"], item["ner_tags"])
|
|
111
|
+
rows.append({"text": text, "pred": pred, "gold": gold})
|
|
112
|
+
|
|
113
|
+
result = {
|
|
114
|
+
"model": MODEL,
|
|
115
|
+
"dataset": DATASET,
|
|
116
|
+
"n": len(rows),
|
|
117
|
+
"score": score([p for r in rows for p in r["pred"]], [g for r in rows for g in r["gold"]]),
|
|
118
|
+
"rows": rows,
|
|
119
|
+
}
|
|
120
|
+
if OUT:
|
|
121
|
+
with open(OUT, "w", encoding="utf-8") as f:
|
|
122
|
+
json.dump(result, f, ensure_ascii=False, indent=2)
|
|
123
|
+
print(json.dumps({k: v for k, v in result.items() if k != "rows"}, ensure_ascii=False, indent=2))
|