nx_absa_seq.nx source
↩ module page · 1447 lines · 70273 B
1// nx_absa_seq.nx -- rung IM28: a SUPERVISED SEQUENCE model for aspect-term extraction (SemEval-2014 Task 4 SB1),
2// the step that closes the gap from the dictionary baseline (~46 F1) toward the CRF winners (74-84 F1). It is an
3// AVERAGED STRUCTURED PERCEPTRON with Viterbi BIO decoding -- integer weights only, so it runs in the nofloat
4// estate; the CRF winners add a probabilistic objective and POS/parse features, which we do not have a tagger for,
5// so this is the honest reachable model, not a claim of parity. Trained on the TRAIN split, decoded on the TEST
6// split, scored by the same set-overlap F1 (nx_absa_lib) the other arms use so the numbers are comparable.
7//
8// FEATURES per token (the hashing trick, one big integer weight table): word, 3-char prefix and suffix, previous
9// and next word, a has-digit shape, dictionary membership (the supervised dictionary signal folded in as a
10// feature), and a bias. TRANSITIONS between BIO tags, with I only reachable from B or I. Averaging is the standard
11// lazy scheme (a weight's average is its running sum plus its current value times the time since it last changed);
12// for the Viterbi argmax the common divisor is dropped. license_tier: ORIGINAL No hw writes (Rule 26). LIB.
13import "nx_syscalls.nx"
14import "nx_reviewmine_lib.nx"
15import "nx_absa_lib.nx"
16import "nx_postag.nx"
17import "nx_depparse.nx"
18import "nx_wordclust.nx"
19import "nx_logadd_lib.nx"
20import "nx_embfeat_lib.nx"
21
22const SP_W: i64 = 1048576 // 2^20 feature-weight slots
23const SP_WMASK: i64 = 1048575
24const SP_NTAGS: i64 = 3
25const SP_O: i64 = 0
26const SP_B: i64 = 1
27const SP_I: i64 = 2
28const SP_NTRANS: i64 = 9 // SP_NTAGS * SP_NTAGS
29const SP_EPOCHS: i64 = 16 // 8 left a third of sentences still updating; doubled for convergence
30const SP_TOK_CAP: i64 = 256 // tokens per sentence
31const SP_TOKBUF: i64 = 16384
32const SP_FEATBUF: i64 = 128
33const SP_NFEAT: i64 = 53 // 8 base + prev2, next2, prev+cur, cur+next, 4-char suffix, dictionary reliability, 5 POS, 5 parse, 5 cluster, 24 embedding
34// DENSE EMBEDDING features (a MODE, off by default): the estate's own trained word vectors (nx_embed_train's embed_v1.bin,
35// 24 Q10 coordinates per word, row index shared with the PPMI vocabulary) read through nx_embfeat_lib, each coordinate
36// quantised to a signed bucket at half-RMS steps derived from the table. One hashed family per coordinate; a word the
37// vocabulary does not know spells '@' at every coordinate and degrades to the base features.
38const SP_PFX_EMB: i64 = 108 // 'l' embedding coordinate families (the payload carries the coordinate index)
39const SP_EMB_BASE: i64 = 29 // first embedding feature index
40const SP_EMB_DIMS: i64 = 24 // the table's coordinate count; a table of another width refuses the mode
41// WORD-CLUSTER features (the winners' other lever, name lists and Brown clusters from in-domain text): when a PPMI model is
42// set (sp_set_clusters), nx_wordclust clusters the training vocabulary by PPMI cosine at three granularities and every
43// token reads its cluster id at all three plus its neighbours' at the middle one -- five hashed families. A word the model
44// does not know spells '@', so an unclustered token degrades to the base features, never to garbage.
45const SP_PFX_CL0: i64 = 90 // 'Z' this token's cluster at the coarsest level
46const SP_PFX_CL1: i64 = 97 // 'a' this token's cluster at the middle level
47const SP_PFX_CL2: i64 = 105 // 'i' this token's cluster at the finest level
48const SP_PFX_CLP: i64 = 106 // 'j' previous token's cluster, middle level
49const SP_PFX_CLN: i64 = 107 // 'k' next token's cluster, middle level
50const SP_CLUST_BASE: i64 = 24 // first cluster feature index
51const SP_CL_MID: i64 = 1 // the middle granularity's level index
52// CRF OBJECTIVE (a MODE, off by default): the same hashed features and transition table, the same lazy averaging and the
53// same Viterbi decoder, trained on the conditional log-likelihood instead of the perceptron's mistake-driven votes. In
54// this mode a weight is Q10 nats (nx_logadd_lib's scale), forward-backward runs in the log domain, and every merged
55// sentence takes one stochastic-gradient step w += eta * (empirical - expected) with the expected counts read off the
56// token and pair marginals. The step is DERIVED, not tuned: eta_0 = 1/4 nat per unit of gradient (a full-confidence error
57// moves a weight a quarter nat, so four such errors in one epoch reach one nat, the perceptron's single vote in this
58// scale), decayed as eta_0 / (1 + epoch) -- the Robbins-Monro schedule every SGD text derives -- and the averaging already
59// in place plays the regulariser's part. Neither constant is moved against the test split.
60const SP_CRF_ETA_NUM: i64 = 1
61const SP_CRF_ETA_DEN: i64 = 4
62const SP_NPAIR: i64 = 9 // SP_NTAGS * SP_NTAGS pair marginals per position
63const SP_CRF_NLL_SLACK: i64 = 8 // Q10 units a chain of log-adds may lose: a sentence NLL below -slack is impossible and counted
64// DEPENDENCY features (DLIREC's lever): when a treebank is set the parser (nx_depparse) is trained on it after the tagger
65// and every sentence is parsed after tagging; this token's relation, its head word, its head's tag, relation+own tag and
66// the relations of its leftmost and rightmost dependents enter as their own families. sp_set_parse(0) keeps the tagger
67// and drops the parser, the ablation switch that separates the two levers on one binary.
68const SP_PFX_REL: i64 = 82 // 'R' relation to the head
69const SP_PFX_HW: i64 = 72 // 'H' head word ('^' for the root)
70const SP_PFX_HT: i64 = 71 // 'G' head tag ('$' for the root)
71const SP_PFX_RELT: i64 = 74 // 'J' relation + own tag
72const SP_PFX_CHR: i64 = 75 // 'K' relations of the leftmost and rightmost dependents
73const SP_PARSE_BASE: i64 = 19 // first parse feature index
74// PART-OF-SPEECH features (the winners' missing lever): when a treebank is set (sp_set_treebank), nx_postag is trained on
75// it once and every sentence is tagged after tokenising; the tag of this token, the previous and the next, and the two
76// tag bigrams enter the weight table as their own families. With no treebank every POS byte reads '^' (tag -1), so the
77// model degrades to the 14-feature form rather than to garbage.
78const SP_PFX_POS: i64 = 84 // 'T' tag of this token
79const SP_PFX_POSP: i64 = 85 // 'U' previous token's tag
80const SP_PFX_POSN: i64 = 86 // 'V' next token's tag
81const SP_PFX_POSPC: i64 = 88 // 'X' previous+current tag bigram
82const SP_PFX_POSCN: i64 = 89 // 'Y' current+next tag bigram
83const SP_POS_BASE: i64 = 14 // first POS feature index
84// DICTIONARY RELIABILITY: the training dictionary is every term that was EVER an aspect, including words that are mostly
85// not aspects in the very training data (laptops: union precision 466 from that). The train split already knows each
86// term's reliability -- occurrences as a gold aspect over occurrences at all -- so it is (a) a GRADED feature the
87// perceptron weighs instead of a binary in-dictionary bit, and (b) the MAJORITY RULE for the union: a dictionary hit is
88// admitted only if the term was an aspect in at least half its training occurrences. A derived bar, not a tuned one.
89const SP_BUCKETS: i64 = 4 // reliability quantised to fifths: 0..4 of SP_BUCKETS
90const SP_PFX_E: i64 = 101 // 'e' dictionary reliability bucket
91const SP_CH_DASH: i64 = 45 // '-' the bucket of a token not in the dictionary
92const SP_MAJ_NUM: i64 = 2 // gold * 2 >= seen <=> an aspect in at least half its occurrences
93const SP_AFF: i64 = 3 // affix length
94const SP_AFF4: i64 = 4 // the longer suffix (a second affix family, not a replacement)
95const SP_NEG: i64 = 0 - 1000000000 // Viterbi -infinity
96const SP_GRAM_CAP: i64 = 512
97// feature prefix bytes (distinguish the feature families in the shared hash space)
98const SP_PFX_W: i64 = 119 // 'w'
99const SP_PFX_P: i64 = 112 // 'p' prefix
100const SP_PFX_S: i64 = 115 // 's' suffix
101const SP_PFX_PW: i64 = 80 // 'P' previous word
102const SP_PFX_NW: i64 = 78 // 'N' next word
103const SP_PFX_SH: i64 = 104 // 'h' shape
104const SP_PFX_D: i64 = 100 // 'd' dictionary membership
105const SP_PFX_B: i64 = 98 // 'b' bias
106const SP_PFX_PW2: i64 = 81 // 'Q' word two back
107const SP_PFX_NW2: i64 = 77 // 'M' word two ahead
108const SP_PFX_PC: i64 = 99 // 'c' previous+current conjunction
109const SP_PFX_CN: i64 = 67 // 'C' current+next conjunction
110const SP_PFX_S4: i64 = 83 // 'S' 4-char suffix
111const SP_CH_DIGIT0: i64 = 48
112const SP_CH_DIGIT9: i64 = 57
113const SP_CH_ONE: i64 = 49
114const SP_CH_ZERO: i64 = 48
115const SP_CH_CARET: i64 = 94 // '^' BOS
116const SP_CH_DOLLAR2: i64 = 36 // '$' EOS
117// out[] indices
118const SP_O_INTER: i64 = 0
119const SP_O_NS: i64 = 1
120const SP_O_NG: i64 = 2
121const SP_O_P: i64 = 3
122const SP_O_R: i64 = 4
123const SP_O_F1: i64 = 5
124const SP_O_DICT: i64 = 6
125const SP_O_TRAINREC: i64 = 7 // train records used (term found and labelled)
126const SP_O_TRAINSKIP: i64 = 8 // train records skipped (term not a contiguous token run)
127const SP_O_TESTREC: i64 = 9
128const SP_O_EPOCHS: i64 = 10
129const SP_O_UPDATES: i64 = 11 // perceptron updates in the last epoch
130const SP_O_TRAINSENT: i64 = 12 // MERGED training sentences (consecutive same-sentence records folded into one)
131// the UNION arm: perceptron predictions unioned with the training-dictionary tagger's, scored as a SECOND line so the
132// perceptron-only number above stays comparable across runs
133const SP_O_U_INTER: i64 = 13
134const SP_O_U_NS: i64 = 14
135const SP_O_U_P: i64 = 15
136const SP_O_U_R: i64 = 16
137const SP_O_U_F1: i64 = 17
138// the PAPER's metric: per-OCCURRENCE (SemEval-2014 Task 4 eq. 1-2 compares the sets of aspect-term annotations
139// across the test sentences, so a term that appears in forty sentences is forty gold items). The distinct-set lines
140// above count each term once and so under-score a model that gets frequent terms right; both are published.
141const SP_O_OC_TP: i64 = 18
142const SP_O_OC_NS: i64 = 19
143const SP_O_OC_NG: i64 = 20
144const SP_O_OC_P: i64 = 21
145const SP_O_OC_R: i64 = 22
146const SP_O_OC_F1: i64 = 23
147const SP_O_OCU_TP: i64 = 24
148const SP_O_OCU_NS: i64 = 25
149const SP_O_OCU_P: i64 = 26
150const SP_O_OCU_R: i64 = 27
151const SP_O_OCU_F1: i64 = 28
152const SP_O_TESTSENT: i64 = 29 // merged test sentences
153const SP_O_DICT_MAJ: i64 = 30 // dictionary terms passing the majority rule (an aspect in >= half their training occurrences)
154const SP_O_POS: i64 = 31 // 1 when a treebank was set and the tagger trained, else 0
155const SP_O_POS_TOK: i64 = 32 // tagger training tokens (0 without a treebank)
156const SP_O_PARSE: i64 = 33 // 1 when the parser trained and every sentence was parsed, else 0
157const SP_O_PARSE_SENT: i64 = 34 // parser training sentences (0 without a treebank or with the parse switch off)
158const SP_O_GROUPS: i64 = 35 // merged-sentence groups the first pass recorded (the shuffle's unit; counts unusable ones too)
159const SP_O_SHUF: i64 = 36 // 1 when epochs 1 and later walked the shuffled order, 0 in file order (the default)
160const SP_O_CLUST: i64 = 37 // 1 when a PPMI model was set, loaded and the vocabulary clustered, else 0
161const SP_O_CLUST_VOCAB: i64 = 38 // training vocabulary words the clusterer holds
162const SP_O_CLUST_INMODEL: i64 = 39 // of those, words the PPMI model knows
163const SP_O_CRF: i64 = 40 // 1 when the CRF objective trained the weights, 0 for the perceptron (the default)
164const SP_O_CRF_NLL_FIRST: i64 = 41 // epoch-0 total negative log-likelihood, Q10 (0 in perceptron mode)
165const SP_O_CRF_NLL_LAST: i64 = 42 // last-epoch total negative log-likelihood, Q10
166const SP_O_CRF_MARGDEV: i64 = 43 // largest |sum of a token's marginals - S| seen, Q10 (a partition witness)
167const SP_O_CRF_NEGNLL: i64 = 44 // sentences whose partition read below their gold path score beyond the slack (must be 0)
168const SP_O_EMB: i64 = 45 // 1 when the embedding table was set, loaded at the declared width and its features were on
169const SP_O_EMB_DIM: i64 = 46 // the loaded table's coordinate count (0 when none)
170const SP_O_EMB_LOOKUPS: i64 = 47 // training-pass token lookups against the vocabulary
171const SP_O_EMB_HITS: i64 = 48 // of those, tokens the vocabulary knows (coverage numerator)
172// THE ERROR CENSUS (an instrument, not a feature): every gold occurrence the perceptron arm missed, partitioned by WHY it
173// could be missed -- the term is not a contiguous token run of its sentence (a tokenisation ceiling no model can cross), or
174// it is a single-token or multi-token term the training dictionary has never seen (generalisation), or one it has seen
175// (a modelling miss the union arm exists to catch). The five parts sum to gold_occurrences - matched by construction.
176const SP_O_EC_UNTOK: i64 = 49 // missed and not tokenisable in its own sentence
177const SP_O_EC_UNSEEN1: i64 = 50 // missed, single-token, absent from the training dictionary
178const SP_O_EC_UNSEENM: i64 = 51 // missed, multi-token, absent from the training dictionary
179const SP_O_EC_SEEN1: i64 = 52 // missed, single-token, present in the training dictionary
180const SP_O_EC_SEENM: i64 = 53 // missed, multi-token, present in the training dictionary
181const SP_O_EC_UNIONMISS: i64 = 54 // gold occurrences the UNION arm missed too
182const SP_O_N: i64 = 55
183const SP_CH_SPACE: i64 = 32
184// PER-EPOCH SHUFFLE (the tagger's lesson applied here): a structured perceptron trained sixteen epochs in file order learns
185// the file's order. Epoch 0 walks the file in order and records where every merged-sentence group starts; every later epoch
186// walks the groups in a fresh DETERMINISTIC permutation (nx_postag's pt_permute, seeded by the epoch), so two runs still
187// agree and the gate's determinism tooth holds.
188const SP_GROUP_CAP: i64 = 65536 // groups the shuffle can hold (SemEval restaurants train is 2018)
189// per-sentence span lists (small, deduped, byte-compared): header = [buf, offs, lens, count, used]
190const SL_BUF: i64 = 0
191const SL_OFFS: i64 = 1
192const SL_LENS: i64 = 2
193const SL_COUNT: i64 = 3
194const SL_USED: i64 = 4
195const SL_FIELDS: i64 = 5
196const SL_CAP: i64 = 64 // spans per sentence
197const SL_BUFCAP: i64 = 8192
198const SP_CTR_N: i64 = 4 // first-pass flag + records used + records skipped + merged sentences
199
200static sp_w: *i64
201static sp_wsum: *i64
202static sp_wtime: *i64
203static sp_tw: *i64
204static sp_twsum: *i64
205static sp_twtime: *i64
206static sp_clock: i64
207static sp_T: i64
208static sp_featbuf: *u8
209static sp_tokbuf: *u8
210static sp_offs: *i64
211static sp_lens: *i64
212static sp_fhs: *i64 // SP_TOK_CAP * SP_NFEAT base feature hashes for the current sentence
213static sp_dp: *i64
214static sp_bp: *i64
215static sp_gold: *i64
216static sp_pred: *i64
217static sp_gram: *u8
218// the record MERGE: a .seg emits ONE record per aspect, so a sentence with three aspects appears three times, each
219// labelling the other two as O -- contradictory supervision a sequence model can never converge on (measured: 2915
220// of 3607 sentences still updating in epoch 8, F1 294 with P 796 / R 180). Consecutive records that reconstruct to the
221// SAME sentence are folded into ONE example carrying every aspect.
222static sp_prevsent: *u8
223static sp_prevlen: i64
224static sp_prevcnt: i64
225static sp_goldacc: *i64
226static sp_pending: i64
227// per-sentence lists for the occurrence metric: this sentence's gold terms, predicted spans, union spans
228static sp_lg: *i64
229static sp_lp: *i64
230static sp_lu: *i64
231static sp_normbuf: *u8
232// per-dictionary-term counters, parallel to the dictionary set's slots: sentences the term appears in / times it is gold
233static sp_dseen: *i64
234static sp_dgold: *i64
235// POS: the treebank path (0 = none), the trained flag, and this sentence's tags (parallel to sp_offs/sp_lens)
236static sp_treebank: *u8
237static sp_pos_on: i64
238static sp_postags: *i64
239// PARSE: the switch (default on whenever a treebank is set), the trained flag, and this sentence's heads and relations
240static sp_parse_want: i64
241static sp_parse_on: i64
242static sp_heads: *i64
243static sp_rels: *i64
244// the shuffle: byte offset of every merged-sentence group's first record, the per-epoch order, and the group trainer's
245// scratch (allocated once; a group is trained thousands of times per run)
246static sp_gstarts: *i64
247static sp_gorder: *i64
248static sp_ngroups: i64
249static sp_grbuf: *u8
250static sp_grp: *i64
251static sp_grr: *i64
252// the shuffle switch: OFF by default. MEASURED 2026-09-06 on one binary against its pre-declared rule (union F1 must rise on
253// both domains): restaurants 683 -> 679, laptops 532 -> 536 -- one down, one up, both inside the ten-permil band, so file
254// order stays the default and the shuffle is a mode (sp_set_shuffle(1), the CLI's shuffle token)
255static sp_shuffle_want: i64
256
257// add (1) or drop (0) the per-epoch shuffle of the merged-sentence groups; off unless asked
258func sp_set_shuffle(on: i64) -> i64 { sp_shuffle_want = on; return 0 }
259// CLUSTERS: the PPMI model path (0 = none, the default) and the trained flag
260static sp_clust_model: *u8
261static sp_clust_on: i64
262// set (or clear with 0) the PPMI model whose clusters feed the five cluster families; read by sp_eval
263func sp_set_clusters(path: *u8) -> i64 { sp_clust_model = path; return 0 }
264// one pass over the TRAIN split adding every token to the clusterer (frequencies seed the medoids)
265func sp_clust_vocab(train: *u8) -> i64 {
266 let lp: *i64 = sys_mmap(RM_I64_PAIR) as *i64
267 lp[0] = 0
268 let b: *u8 = sys_read_file(train, lp)
269 if (b as i64) == 0 { return 0 }
270 let n: i64 = lp[0]
271 let rbuf: *u8 = sys_mmap(AB_RECON_CAP)
272 let rp: *i64 = sys_mmap(RM_I64_PAIR) as *i64
273 let r: *i64 = sys_mmap(AB_REC_N * RM_I64_BYTES) as *i64
274 rp[0] = 0
275 var added: i64 = 0
276 while ab_rec_next(b, n, rp, r) == 1 {
277 let rl: i64 = ab_reconstruct((b as i64 + r[0]) as *u8, r[1], (b as i64 + r[2]) as *u8, r[3], rbuf, AB_RECON_CAP)
278 let cnt: i64 = sp_tokenize(rbuf, rl)
279 var i: i64 = 0
280 while i < cnt { wc_add((sp_tokbuf as i64 + sp_offs[i]) as *u8, sp_lens[i]); added = added + 1; i = i + 1 }
281 }
282 return added
283}
284
285// CRF: the mode flag, the epoch the step size decays on, the log-domain lattices and the marginals
286static sp_crf_want: i64
287static sp_crf_on: i64
288static sp_crf_epoch: i64
289static sp_em: *i64 // emission cache SP_TOK_CAP * SP_NTAGS for the current sentence
290static sp_alpha: *i64 // forward lattice, Q10 log domain
291static sp_beta: *i64 // backward lattice
292static sp_marg: *i64 // Q10 token marginals SP_TOK_CAP * SP_NTAGS
293static sp_pair: *i64 // Q10 pair marginals SP_TOK_CAP * SP_NPAIR (row i: previous tag * SP_NTAGS + tag)
294static sp_crf_nll: i64 // the current epoch's summed NLL, Q10
295static sp_crf_margdev: i64 // largest |sum of marginals - S| seen this run
296static sp_crf_negnll: i64 // sentences with NLL < -SP_CRF_NLL_SLACK (impossible, counted)
297// add (1) or drop (0) the CRF objective; off unless asked
298func sp_set_crf(on: i64) -> i64 { sp_crf_want = on; return 0 }
299// EMBEDDINGS: the PPMI vocabulary and the embedding table (0 = none, the default) and the loaded flag
300static sp_emb_ppmi: *u8
301static sp_emb_path: *u8
302static sp_emb_on: i64
303static sp_fmask: *i64 // SP_TOK_CAP * SP_NFEAT: 1 when the slot carries a feature, 0 when the slot is absent
304 // (an absent feature casts no vote in scoring or updates -- so a mode that is off
305 // leaves the model byte-identical to the model without the mode)
306// set (or clear with 0, 0) the vocabulary and table whose coordinates feed the embedding families; read by sp_eval
307func sp_set_embed(ppmi: *u8, emb: *u8) -> i64 { sp_emb_ppmi = ppmi; sp_emb_path = emb; return 0 }
308
309// set (or clear with 0) the CoNLL-U treebank the POS tagger trains on; read by sp_eval. The parser stays OFF by default:
310// MEASURED 2026-09-06 on one binary, full SemEval splits, the parse features from the 5-epoch parser (UAS 804) LOWERED the
311// paper-metric union F1 (restaurants 687 -> 683, laptops 528 -> 521), so the default is the arm that measured best and the
312// parser is a mode (sp_set_parse(1), the CLI's parse argument) until a stronger parser earns the default back
313func sp_set_treebank(path: *u8) -> i64 { sp_treebank = path; sp_parse_want = 0; return 0 }
314// drop (0) or add (1) the parser on top of the tagger: the ablation switch; sp_set_treebank resets it to off
315func sp_set_parse(on: i64) -> i64 { sp_parse_want = on; return 0 }
316// tag, then parse, the tokens currently in sp_tokbuf; without a trained tagger every tag reads -1, without a trained
317// parser every head and relation reads -1
318func sp_tag_sentence(cnt: i64) -> i64 {
319 var i: i64 = 0
320 while i < cnt { sp_postags[i] = PT_TAG_NONE; sp_heads[i] = DP_NONE; sp_rels[i] = DP_NONE; i = i + 1 }
321 if sp_pos_on == 1 { pt_tag_stream(sp_tokbuf, sp_offs, sp_lens, cnt, sp_postags) }
322 if sp_parse_on == 1 { dp_parse_stream(sp_tokbuf, sp_offs, sp_lens, sp_postags, cnt, sp_heads, sp_rels) }
323 return 0
324}
325
326func sl_new() -> *i64 {
327 let h: *i64 = sys_mmap(SL_FIELDS * RM_I64_BYTES) as *i64
328 h[SL_BUF] = sys_mmap(SL_BUFCAP) as i64
329 h[SL_OFFS] = sys_mmap(SL_CAP * RM_I64_BYTES) as i64
330 h[SL_LENS] = sys_mmap(SL_CAP * RM_I64_BYTES) as i64
331 h[SL_COUNT] = 0
332 h[SL_USED] = 0
333 return h
334}
335func sl_reset(h: *i64) -> i64 { h[SL_COUNT] = 0; h[SL_USED] = 0; return 0 }
336func sl_has(h: *i64, s: *u8, n: i64) -> i64 {
337 let buf: *u8 = h[SL_BUF] as *u8
338 let offs: *i64 = h[SL_OFFS] as *i64
339 let lens: *i64 = h[SL_LENS] as *i64
340 var i: i64 = 0
341 while i < h[SL_COUNT] {
342 if lens[i] == n {
343 var e: i64 = 0
344 var same: i64 = 1
345 while e < n { if buf[offs[i] + e] != s[e] { same = 0; e = n } else { e = e + 1 } }
346 if same == 1 { return 1 }
347 }
348 i = i + 1
349 }
350 return 0
351}
352// add if absent (dedupe within the sentence); a full list drops silently but COUNTS it, so a cap is never a lie
353func sl_add(h: *i64, s: *u8, n: i64) -> i64 {
354 if n <= 0 { return 0 }
355 if sl_has(h, s, n) == 1 { return 0 }
356 if h[SL_COUNT] >= SL_CAP { return 0 }
357 if h[SL_USED] + n > SL_BUFCAP { return 0 }
358 let buf: *u8 = h[SL_BUF] as *u8
359 let offs: *i64 = h[SL_OFFS] as *i64
360 let lens: *i64 = h[SL_LENS] as *i64
361 rm_catn(buf, h[SL_USED], s, n)
362 offs[h[SL_COUNT]] = h[SL_USED]
363 lens[h[SL_COUNT]] = n
364 h[SL_USED] = h[SL_USED] + n
365 h[SL_COUNT] = h[SL_COUNT] + 1
366 return 1
367}
368// how many entries of a are also in b
369func sl_inter(a: *i64, b: *i64) -> i64 {
370 let buf: *u8 = a[SL_BUF] as *u8
371 let offs: *i64 = a[SL_OFFS] as *i64
372 let lens: *i64 = a[SL_LENS] as *i64
373 var k: i64 = 0
374 var i: i64 = 0
375 while i < a[SL_COUNT] { if sl_has(b, (buf as i64 + offs[i]) as *u8, lens[i]) == 1 { k = k + 1 } i = i + 1 }
376 return k
377}
378
379func sp_reset() -> i64 {
380 if (sp_w as i64) == 0 {
381 sp_w = sys_mmap(SP_W * RM_I64_BYTES) as *i64
382 sp_wsum = sys_mmap(SP_W * RM_I64_BYTES) as *i64
383 sp_wtime = sys_mmap(SP_W * RM_I64_BYTES) as *i64
384 sp_tw = sys_mmap(SP_NTRANS * RM_I64_BYTES) as *i64
385 sp_twsum = sys_mmap(SP_NTRANS * RM_I64_BYTES) as *i64
386 sp_twtime = sys_mmap(SP_NTRANS * RM_I64_BYTES) as *i64
387 sp_featbuf = sys_mmap(SP_FEATBUF)
388 sp_tokbuf = sys_mmap(SP_TOKBUF)
389 sp_offs = sys_mmap(SP_TOK_CAP * RM_I64_BYTES) as *i64
390 sp_lens = sys_mmap(SP_TOK_CAP * RM_I64_BYTES) as *i64
391 sp_fhs = sys_mmap(SP_TOK_CAP * SP_NFEAT * RM_I64_BYTES) as *i64
392 sp_dp = sys_mmap(SP_TOK_CAP * SP_NTAGS * RM_I64_BYTES) as *i64
393 sp_bp = sys_mmap(SP_TOK_CAP * SP_NTAGS * RM_I64_BYTES) as *i64
394 sp_gold = sys_mmap(SP_TOK_CAP * RM_I64_BYTES) as *i64
395 sp_pred = sys_mmap(SP_TOK_CAP * RM_I64_BYTES) as *i64
396 sp_gram = sys_mmap(SP_GRAM_CAP)
397 sp_prevsent = sys_mmap(AB_RECON_CAP)
398 sp_goldacc = sys_mmap(SP_TOK_CAP * RM_I64_BYTES) as *i64
399 sp_lg = sl_new()
400 sp_lp = sl_new()
401 sp_lu = sl_new()
402 sp_normbuf = sys_mmap(AB_TERM_MAX)
403 sp_dseen = sys_mmap(AB_SET_SLOTS * RM_I64_BYTES) as *i64
404 sp_dgold = sys_mmap(AB_SET_SLOTS * RM_I64_BYTES) as *i64
405 sp_postags = sys_mmap(SP_TOK_CAP * RM_I64_BYTES) as *i64
406 sp_heads = sys_mmap(SP_TOK_CAP * RM_I64_BYTES) as *i64
407 sp_rels = sys_mmap(SP_TOK_CAP * RM_I64_BYTES) as *i64
408 sp_gstarts = sys_mmap(SP_GROUP_CAP * RM_I64_BYTES) as *i64
409 sp_gorder = sys_mmap(SP_GROUP_CAP * RM_I64_BYTES) as *i64
410 sp_grbuf = sys_mmap(AB_RECON_CAP)
411 sp_grp = sys_mmap(RM_I64_PAIR) as *i64
412 sp_grr = sys_mmap(AB_REC_N * RM_I64_BYTES) as *i64
413 sp_em = sys_mmap(SP_TOK_CAP * SP_NTAGS * RM_I64_BYTES) as *i64
414 sp_alpha = sys_mmap(SP_TOK_CAP * SP_NTAGS * RM_I64_BYTES) as *i64
415 sp_beta = sys_mmap(SP_TOK_CAP * SP_NTAGS * RM_I64_BYTES) as *i64
416 sp_marg = sys_mmap(SP_TOK_CAP * SP_NTAGS * RM_I64_BYTES) as *i64
417 sp_pair = sys_mmap(SP_TOK_CAP * SP_NPAIR * RM_I64_BYTES) as *i64
418 sp_fmask = sys_mmap(SP_TOK_CAP * SP_NFEAT * RM_I64_BYTES) as *i64
419 } else {
420 var i: i64 = 0
421 while i < SP_W { sp_w[i] = 0; sp_wsum[i] = 0; sp_wtime[i] = 0; i = i + 1 }
422 }
423 var d: i64 = 0
424 while d < AB_SET_SLOTS { sp_dseen[d] = 0; sp_dgold[d] = 0; d = d + 1 }
425 var t: i64 = 0
426 while t < SP_NTRANS { sp_tw[t] = 0; sp_twsum[t] = 0; sp_twtime[t] = 0; t = t + 1 }
427 sp_clock = 0
428 sp_T = 0
429 sp_prevlen = 0
430 sp_prevcnt = 0
431 sp_pending = 0
432 return 0
433}
434func sp_acc_reset() -> i64 {
435 var i: i64 = 0
436 while i < SP_TOK_CAP { sp_goldacc[i] = SP_O; i = i + 1 }
437 return 0
438}
439// fold this record's labels (sp_gold) into the accumulated example: a B or I never reverts to O
440func sp_acc_or(cnt: i64) -> i64 {
441 var i: i64 = 0
442 while i < cnt { if sp_gold[i] != SP_O { sp_goldacc[i] = sp_gold[i] } i = i + 1 }
443 return 0
444}
445func sp_same_sent(rbuf: *u8, rl: i64) -> i64 {
446 if rl != sp_prevlen { return 0 }
447 var i: i64 = 0
448 while i < rl { if rbuf[i] != sp_prevsent[i] { return 0 } i = i + 1 }
449 return 1
450}
451// train ONE merged example: the tokens in sp_tokbuf (still the previous sentence) with the accumulated gold
452func sp_flush(cnt: i64, dic: *i64) -> i64 {
453 var i: i64 = 0
454 while i < cnt { sp_gold[i] = sp_goldacc[i]; i = i + 1 }
455 i = 0
456 while i < cnt { sp_feats(cnt, i, dic); i = i + 1 }
457 if sp_crf_on == 1 {
458 // CRF: partition and marginals under the current weights, the mistake count under the same weights (the receipt's
459 // last_epoch_updates keeps its meaning), then one gradient step
460 let logz: i64 = sp_crf_fb(cnt)
461 let nll: i64 = logz - sp_crf_gold_score(cnt)
462 sp_crf_nll = sp_crf_nll + nll
463 if nll < (0 - SP_CRF_NLL_SLACK) { sp_crf_negnll = sp_crf_negnll + 1 }
464 sp_viterbi(cnt, 0)
465 var d: i64 = 0
466 i = 0
467 while i < cnt { if sp_pred[i] != sp_gold[i] { d = 1 } i = i + 1 }
468 sp_crf_update(cnt)
469 sp_clock = sp_clock + 1
470 return d
471 }
472 sp_viterbi(cnt, 0)
473 let u: i64 = sp_update(cnt)
474 sp_clock = sp_clock + 1
475 return u
476}
477// tokenise a reconstructed sentence into sp_tokbuf, filling sp_offs/sp_lens; returns the token count (capped)
478func sp_tokenize(sent: *u8, n: i64) -> i64 {
479 let ip: *i64 = sys_mmap(RM_I64_PAIR) as *i64
480 ip[0] = 0
481 let tok: *u8 = sys_mmap(RM_TOK_MAX + RM_TOK_BUF_SPARE)
482 var cnt: i64 = 0
483 var used: i64 = 0
484 var tl: i64 = rm_next_token(sent, n, ip, tok)
485 while tl > 0 {
486 if cnt < SP_TOK_CAP { if used + tl < SP_TOKBUF {
487 sp_offs[cnt] = used
488 sp_lens[cnt] = tl
489 rm_catn(sp_tokbuf, used, tok, tl)
490 used = used + tl
491 cnt = cnt + 1
492 } }
493 tl = rm_next_token(sent, n, ip, tok)
494 }
495 return cnt
496}
497// the base hash of one feature: prefix byte then the span
498func sp_fh(pfx: i64, s: *u8, n: i64) -> i64 {
499 sp_featbuf[0] = pfx as u8
500 var i: i64 = 0
501 while i < n { if i + 1 < SP_FEATBUF { sp_featbuf[i + 1] = s[i] } i = i + 1 }
502 return rm_hash(sp_featbuf, n + 1)
503}
504func sp_has_digit(s: *u8, n: i64) -> i64 {
505 var i: i64 = 0
506 while i < n { let c: i64 = s[i] as i64; if c >= SP_CH_DIGIT0 { if c <= SP_CH_DIGIT9 { return 1 } } i = i + 1 }
507 return 0
508}
509// compute the SP_NFEAT base feature hashes for position i into sp_fhs[i*SP_NFEAT ..]
510func sp_feats(cnt: i64, i: i64, dic: *i64) -> i64 {
511 let base: i64 = i * SP_NFEAT
512 let toff: i64 = sp_offs[i]
513 let tlen: i64 = sp_lens[i]
514 let tp: *u8 = (sp_tokbuf as i64 + toff) as *u8
515 sp_fhs[base + 0] = sp_fh(SP_PFX_W, tp, tlen)
516 var af: i64 = SP_AFF
517 if tlen < af { af = tlen }
518 sp_fhs[base + 1] = sp_fh(SP_PFX_P, tp, af)
519 sp_fhs[base + 2] = sp_fh(SP_PFX_S, (sp_tokbuf as i64 + toff + tlen - af) as *u8, af)
520 if i > 0 { sp_fhs[base + 3] = sp_fh(SP_PFX_PW, (sp_tokbuf as i64 + sp_offs[i - 1]) as *u8, sp_lens[i - 1]) }
521 else { let bos: *u8 = sys_mmap(2); bos[0] = SP_CH_CARET as u8; sp_fhs[base + 3] = sp_fh(SP_PFX_PW, bos, 1) }
522 if i < cnt - 1 { sp_fhs[base + 4] = sp_fh(SP_PFX_NW, (sp_tokbuf as i64 + sp_offs[i + 1]) as *u8, sp_lens[i + 1]) }
523 else { let eos: *u8 = sys_mmap(2); eos[0] = SP_CH_DOLLAR2 as u8; sp_fhs[base + 4] = sp_fh(SP_PFX_NW, eos, 1) }
524 let sh: *u8 = sys_mmap(2)
525 if sp_has_digit(tp, tlen) == 1 { sh[0] = SP_CH_ONE as u8 } else { sh[0] = SP_CH_ZERO as u8 }
526 sp_fhs[base + 5] = sp_fh(SP_PFX_SH, sh, 1)
527 var dslot: i64 = hs_slot_raw(dic, tp, tlen)
528 if dslot < 0 { if i < cnt - 1 {
529 var go: i64 = rm_catn(sp_gram, 0, tp, tlen)
530 sp_gram[go] = AB_NG_JOIN as u8; go = go + 1
531 go = rm_catn(sp_gram, go, (sp_tokbuf as i64 + sp_offs[i + 1]) as *u8, sp_lens[i + 1])
532 dslot = hs_slot_raw(dic, sp_gram, go)
533 } }
534 var indict: i64 = 0
535 if dslot >= 0 { indict = 1 }
536 let dch: *u8 = sys_mmap(2)
537 if indict == 1 { dch[0] = SP_CH_ONE as u8 } else { dch[0] = SP_CH_ZERO as u8 }
538 sp_fhs[base + 6] = sp_fh(SP_PFX_D, dch, 1)
539 // the graded reliability of that dictionary term in the TRAIN split (bucket 0..SP_BUCKETS), '-' when not in it
540 let ech: *u8 = sys_mmap(2)
541 ech[0] = SP_CH_DASH as u8
542 if dslot >= 0 {
543 var bucket: i64 = 0
544 if sp_dseen[dslot] > 0 { bucket = (sp_dgold[dslot] * SP_BUCKETS) / sp_dseen[dslot] }
545 if bucket > SP_BUCKETS { bucket = SP_BUCKETS }
546 ech[0] = (SP_CH_ZERO + bucket) as u8
547 }
548 sp_fhs[base + 13] = sp_fh(SP_PFX_E, ech, 1)
549 sp_fhs[base + 7] = sp_fh(SP_PFX_B, sp_featbuf, 0)
550 // wider context: two back, two ahead (BOS/EOS markers reused when the window runs off the sentence)
551 let bos2: *u8 = sys_mmap(2); bos2[0] = SP_CH_CARET as u8
552 let eos2: *u8 = sys_mmap(2); eos2[0] = SP_CH_DOLLAR2 as u8
553 if i > 1 { sp_fhs[base + 8] = sp_fh(SP_PFX_PW2, (sp_tokbuf as i64 + sp_offs[i - 2]) as *u8, sp_lens[i - 2]) }
554 else { sp_fhs[base + 8] = sp_fh(SP_PFX_PW2, bos2, 1) }
555 if i < cnt - 2 { sp_fhs[base + 9] = sp_fh(SP_PFX_NW2, (sp_tokbuf as i64 + sp_offs[i + 2]) as *u8, sp_lens[i + 2]) }
556 else { sp_fhs[base + 9] = sp_fh(SP_PFX_NW2, eos2, 1) }
557 // conjunctions: previous+current and current+next, joined through sp_gram
558 var cj: i64 = 0
559 if i > 0 { cj = rm_catn(sp_gram, 0, (sp_tokbuf as i64 + sp_offs[i - 1]) as *u8, sp_lens[i - 1]) } else { cj = rm_catn(sp_gram, 0, bos2, 1) }
560 sp_gram[cj] = AB_NG_JOIN as u8; cj = cj + 1
561 cj = rm_catn(sp_gram, cj, tp, tlen)
562 sp_fhs[base + 10] = sp_fh(SP_PFX_PC, sp_gram, cj)
563 cj = rm_catn(sp_gram, 0, tp, tlen)
564 sp_gram[cj] = AB_NG_JOIN as u8; cj = cj + 1
565 if i < cnt - 1 { cj = rm_catn(sp_gram, cj, (sp_tokbuf as i64 + sp_offs[i + 1]) as *u8, sp_lens[i + 1]) } else { cj = rm_catn(sp_gram, cj, eos2, 1) }
566 sp_fhs[base + 11] = sp_fh(SP_PFX_CN, sp_gram, cj)
567 // the 4-char suffix as its own family (the 3-char one stays)
568 var af4: i64 = SP_AFF4
569 if tlen < af4 { af4 = tlen }
570 sp_fhs[base + 12] = sp_fh(SP_PFX_S4, (sp_tokbuf as i64 + toff + tlen - af4) as *u8, af4)
571 // POS of this, the previous and the next token, and the two tag bigrams (tag -1 encodes as '^')
572 let pb: *u8 = sys_mmap(4)
573 var pc: i64 = PT_TAG_NONE
574 var pp: i64 = PT_TAG_NONE
575 var pn: i64 = PT_TAG_NONE
576 if sp_pos_on == 1 {
577 pc = sp_postags[i]
578 if i > 0 { pp = sp_postags[i - 1] }
579 if i < cnt - 1 { pn = sp_postags[i + 1] }
580 }
581 pb[0] = (SP_CH_CARET + pc + 1) as u8
582 sp_fhs[base + SP_POS_BASE + 0] = sp_fh(SP_PFX_POS, pb, 1)
583 pb[0] = (SP_CH_CARET + pp + 1) as u8
584 sp_fhs[base + SP_POS_BASE + 1] = sp_fh(SP_PFX_POSP, pb, 1)
585 pb[0] = (SP_CH_CARET + pn + 1) as u8
586 sp_fhs[base + SP_POS_BASE + 2] = sp_fh(SP_PFX_POSN, pb, 1)
587 pb[0] = (SP_CH_CARET + pp + 1) as u8
588 pb[1] = (SP_CH_CARET + pc + 1) as u8
589 sp_fhs[base + SP_POS_BASE + 3] = sp_fh(SP_PFX_POSPC, pb, 2)
590 pb[0] = (SP_CH_CARET + pc + 1) as u8
591 pb[1] = (SP_CH_CARET + pn + 1) as u8
592 sp_fhs[base + SP_POS_BASE + 4] = sp_fh(SP_PFX_POSCN, pb, 2)
593 // DEPENDENCY features: relation, head word, head tag, relation+own tag, the relations of the outermost dependents
594 // (every byte reads '@' or '^' when no parser ran, so the no-parser model degrades to the 19-feature form)
595 var rb: i64 = DP_CH_AT
596 var hidx: i64 = DP_NONE
597 var lchild: i64 = DP_NONE
598 var rchild: i64 = DP_NONE
599 if sp_parse_on == 1 {
600 if sp_rels[i] >= 0 { rb = DP_CH_A + sp_rels[i] }
601 hidx = sp_heads[i]
602 var j: i64 = 0
603 while j < cnt { if sp_heads[j] == i { if lchild < 0 { lchild = j } rchild = j } j = j + 1 }
604 }
605 pb[0] = rb as u8
606 sp_fhs[base + SP_PARSE_BASE + 0] = sp_fh(SP_PFX_REL, pb, 1)
607 if hidx >= 0 { sp_fhs[base + SP_PARSE_BASE + 1] = sp_fh(SP_PFX_HW, (sp_tokbuf as i64 + sp_offs[hidx]) as *u8, sp_lens[hidx]) }
608 else { pb[0] = SP_CH_CARET as u8; sp_fhs[base + SP_PARSE_BASE + 1] = sp_fh(SP_PFX_HW, pb, 1) }
609 if hidx >= 0 { pb[0] = (SP_CH_CARET + sp_postags[hidx] + 1) as u8 } else { pb[0] = SP_CH_DOLLAR2 as u8 }
610 sp_fhs[base + SP_PARSE_BASE + 2] = sp_fh(SP_PFX_HT, pb, 1)
611 pb[0] = rb as u8
612 pb[1] = (SP_CH_CARET + pc + 1) as u8
613 sp_fhs[base + SP_PARSE_BASE + 3] = sp_fh(SP_PFX_RELT, pb, 2)
614 var lrb: i64 = DP_CH_AT
615 var rrb: i64 = DP_CH_AT
616 if lchild >= 0 { if sp_rels[lchild] >= 0 { lrb = DP_CH_A + sp_rels[lchild] } }
617 if rchild >= 0 { if sp_rels[rchild] >= 0 { rrb = DP_CH_A + sp_rels[rchild] } }
618 pb[0] = lrb as u8
619 pb[1] = rrb as u8
620 sp_fhs[base + SP_PARSE_BASE + 4] = sp_fh(SP_PFX_CHR, pb, 2)
621 // WORD CLUSTERS: this token at three granularities, its neighbours at the middle one ('@' when unclustered)
622 var cl0: i64 = WC_NONE
623 var cl1: i64 = WC_NONE
624 var cl2: i64 = WC_NONE
625 var clp: i64 = WC_NONE
626 var cln: i64 = WC_NONE
627 if sp_clust_on == 1 {
628 cl0 = wc_cluster(tp, tlen, 0)
629 cl1 = wc_cluster(tp, tlen, SP_CL_MID)
630 cl2 = wc_cluster(tp, tlen, 2)
631 if i > 0 { clp = wc_cluster((sp_tokbuf as i64 + sp_offs[i - 1]) as *u8, sp_lens[i - 1], SP_CL_MID) }
632 if i < cnt - 1 { cln = wc_cluster((sp_tokbuf as i64 + sp_offs[i + 1]) as *u8, sp_lens[i + 1], SP_CL_MID) }
633 }
634 sp_fhs[base + SP_CLUST_BASE + 0] = sp_fh(SP_PFX_CL0, pb, wc_spell(cl0, pb))
635 sp_fhs[base + SP_CLUST_BASE + 1] = sp_fh(SP_PFX_CL1, pb, wc_spell(cl1, pb))
636 sp_fhs[base + SP_CLUST_BASE + 2] = sp_fh(SP_PFX_CL2, pb, wc_spell(cl2, pb))
637 if i > 0 { sp_fhs[base + SP_CLUST_BASE + 3] = sp_fh(SP_PFX_CLP, pb, wc_spell(clp, pb)) }
638 else { pb[0] = SP_CH_CARET as u8; sp_fhs[base + SP_CLUST_BASE + 3] = sp_fh(SP_PFX_CLP, pb, 1) }
639 if i < cnt - 1 { sp_fhs[base + SP_CLUST_BASE + 4] = sp_fh(SP_PFX_CLN, pb, wc_spell(cln, pb)) }
640 else { pb[0] = SP_CH_DOLLAR2 as u8; sp_fhs[base + SP_CLUST_BASE + 4] = sp_fh(SP_PFX_CLN, pb, 1) }
641 // every slot below the embedding block always carries a feature
642 var mk: i64 = 0
643 while mk < SP_EMB_BASE { sp_fmask[base + mk] = 1; mk = mk + 1 }
644 // DENSE EMBEDDING: this token's coordinates as buckets, one family per coordinate. With the mode off every embedding
645 // slot is ABSENT (masked out, no vote), so the default model is the model without the mode; with the mode on an
646 // unknown word casts ONE marker vote (its first slot spells '@') and leaves the other slots absent, never twenty-four
647 // copies of the same ignorance
648 var ew: i64 = EF_NONE
649 if sp_emb_on == 1 { ew = ef_wid(tp, tlen) }
650 var ed: i64 = 0
651 while ed < SP_EMB_DIMS {
652 var on: i64 = 0
653 if sp_emb_on == 1 { if ew >= 0 { on = 1 } else { if ed == 0 { on = 1 } } }
654 if on == 1 { sp_fhs[base + SP_EMB_BASE + ed] = sp_fh(SP_PFX_EMB, pb, ef_spell(ew, ed, pb)) } else { sp_fhs[base + SP_EMB_BASE + ed] = 0 }
655 sp_fmask[base + SP_EMB_BASE + ed] = on
656 ed = ed + 1
657 }
658 return SP_NFEAT
659}
660func sp_w_at(idx: i64, useavg: i64) -> i64 {
661 if useavg == 1 { return sp_wsum[idx] + sp_w[idx] * (sp_T - sp_wtime[idx]) }
662 return sp_w[idx]
663}
664func sp_bump(idx: i64, delta: i64) -> i64 {
665 sp_wsum[idx] = sp_wsum[idx] + sp_w[idx] * (sp_clock - sp_wtime[idx])
666 sp_wtime[idx] = sp_clock
667 sp_w[idx] = sp_w[idx] + delta
668 return 0
669}
670func sp_tw_at(pt: i64, t: i64, useavg: i64) -> i64 {
671 let idx: i64 = pt * SP_NTAGS + t
672 if useavg == 1 { return sp_twsum[idx] + sp_tw[idx] * (sp_T - sp_twtime[idx]) }
673 return sp_tw[idx]
674}
675func sp_tbump(pt: i64, t: i64, delta: i64) -> i64 {
676 let idx: i64 = pt * SP_NTAGS + t
677 sp_twsum[idx] = sp_twsum[idx] + sp_tw[idx] * (sp_clock - sp_twtime[idx])
678 sp_twtime[idx] = sp_clock
679 sp_tw[idx] = sp_tw[idx] + delta
680 return 0
681}
682func sp_emission(i: i64, tag: i64, useavg: i64) -> i64 {
683 let base: i64 = i * SP_NFEAT
684 var s: i64 = 0
685 var k: i64 = 0
686 while k < SP_NFEAT { if sp_fmask[base + k] == 1 { s = s + sp_w_at(((sp_fhs[base + k] * SP_NTAGS + tag) & SP_WMASK), useavg) } k = k + 1 }
687 return s
688}
689// Viterbi over the current sentence (features already in sp_fhs); writes the best tags into sp_pred
690func sp_viterbi(cnt: i64, useavg: i64) -> i64 {
691 if cnt <= 0 { return 0 }
692 var t: i64 = 0
693 while t < SP_NTAGS {
694 var e: i64 = sp_emission(0, t, useavg)
695 if t == SP_I { e = SP_NEG } // a sentence cannot start with I
696 sp_dp[t] = e
697 sp_bp[t] = 0 - 1
698 t = t + 1
699 }
700 var i: i64 = 1
701 while i < cnt {
702 var t2: i64 = 0
703 while t2 < SP_NTAGS {
704 let em: i64 = sp_emission(i, t2, useavg)
705 var best: i64 = SP_NEG
706 var bpt: i64 = 0
707 var pt: i64 = 0
708 while pt < SP_NTAGS {
709 var ok: i64 = 1
710 if t2 == SP_I { if pt == SP_O { ok = 0 } }
711 if ok == 1 {
712 let sc: i64 = sp_dp[(i - 1) * SP_NTAGS + pt] + sp_tw_at(pt, t2, useavg) + em
713 if sc > best { best = sc; bpt = pt }
714 }
715 pt = pt + 1
716 }
717 sp_dp[i * SP_NTAGS + t2] = best
718 sp_bp[i * SP_NTAGS + t2] = bpt
719 t2 = t2 + 1
720 }
721 i = i + 1
722 }
723 var bestt: i64 = 0
724 var bestv: i64 = sp_dp[(cnt - 1) * SP_NTAGS + 0]
725 var tt: i64 = 1
726 while tt < SP_NTAGS { let v: i64 = sp_dp[(cnt - 1) * SP_NTAGS + tt]; if v > bestv { bestv = v; bestt = tt } tt = tt + 1 }
727 var j: i64 = cnt - 1
728 var cur: i64 = bestt
729 while j >= 0 { sp_pred[j] = cur; cur = sp_bp[j * SP_NTAGS + cur]; if cur < 0 { cur = SP_O } j = j - 1 }
730 return 0
731}
732// build gold BIO for one record whose aspect term is [term,tlen]; returns 1 if the term was found and labelled
733func sp_gold_bio(cnt: i64, term: *u8, tlen: i64) -> i64 {
734 var i: i64 = 0
735 while i < cnt { sp_gold[i] = SP_O; i = i + 1 }
736 // tokenise the term into a small list by walking it the same way
737 let ip: *i64 = sys_mmap(RM_I64_PAIR) as *i64
738 ip[0] = 0
739 let tbuf: *u8 = sys_mmap(RM_TOK_MAX + RM_TOK_BUF_SPARE)
740 let toffs: *i64 = sys_mmap(SP_TOK_CAP * RM_I64_BYTES) as *i64
741 let tlens: *i64 = sys_mmap(SP_TOK_CAP * RM_I64_BYTES) as *i64
742 let tstore: *u8 = sys_mmap(SP_TOKBUF)
743 var tc: i64 = 0
744 var used: i64 = 0
745 var tl: i64 = rm_next_token(term, tlen, ip, tbuf)
746 while tl > 0 {
747 if tc < SP_TOK_CAP { if used + tl < SP_TOKBUF { toffs[tc] = used; tlens[tc] = tl; rm_catn(tstore, used, tbuf, tl); used = used + tl; tc = tc + 1 } }
748 tl = rm_next_token(term, tlen, ip, tbuf)
749 }
750 if tc == 0 { return 0 }
751 // find a contiguous run in the sentence tokens equal to the term tokens
752 var start: i64 = 0
753 while start + tc <= cnt {
754 var ok: i64 = 1
755 var k: i64 = 0
756 while k < tc {
757 if tlens[k] != sp_lens[start + k] { ok = 0 } else {
758 var e: i64 = 0
759 while e < tlens[k] { if tstore[toffs[k] + e] != sp_tokbuf[sp_offs[start + k] + e] { ok = 0 } e = e + 1 }
760 }
761 k = k + 1
762 }
763 if ok == 1 {
764 sp_gold[start] = SP_B
765 var m: i64 = 1
766 while m < tc { sp_gold[start + m] = SP_I; m = m + 1 }
767 return 1
768 }
769 start = start + 1
770 }
771 return 0
772}
773// one perceptron update comparing sp_pred to sp_gold over the current sentence; returns 1 if any tag differed
774func sp_update(cnt: i64) -> i64 {
775 var diff: i64 = 0
776 var i: i64 = 0
777 while i < cnt { if sp_pred[i] != sp_gold[i] { diff = 1 } i = i + 1 }
778 if diff == 0 { return 0 }
779 i = 0
780 while i < cnt {
781 if sp_pred[i] != sp_gold[i] {
782 let base: i64 = i * SP_NFEAT
783 var k: i64 = 0
784 while k < SP_NFEAT {
785 if sp_fmask[base + k] == 1 {
786 sp_bump(((sp_fhs[base + k] * SP_NTAGS + sp_gold[i]) & SP_WMASK), 1)
787 sp_bump(((sp_fhs[base + k] * SP_NTAGS + sp_pred[i]) & SP_WMASK), 0 - 1)
788 }
789 k = k + 1
790 }
791 }
792 if i > 0 {
793 if (sp_gold[i - 1] != sp_pred[i - 1]) | (sp_gold[i] != sp_pred[i]) {
794 sp_tbump(sp_gold[i - 1], sp_gold[i], 1)
795 sp_tbump(sp_pred[i - 1], sp_pred[i], 0 - 1)
796 }
797 }
798 i = i + 1
799 }
800 return 1
801}
802// CRF forward-backward over the current sentence (features in sp_fhs, weights read raw): fills sp_em, sp_alpha, sp_beta,
803// the token marginals sp_marg and the pair marginals sp_pair (Q10), and returns log Z (Q10). The hard constraints the
804// decoder applies (no I at the start, no O -> I) are the same masks here, so the CRF never puts mass on a path Viterbi
805// could not return.
806func sp_crf_fb(cnt: i64) -> i64 {
807 var i: i64 = 0
808 while i < cnt {
809 var t: i64 = 0
810 while t < SP_NTAGS { sp_em[i * SP_NTAGS + t] = sp_emission(i, t, 0); t = t + 1 }
811 i = i + 1
812 }
813 var t0: i64 = 0
814 while t0 < SP_NTAGS {
815 var a0: i64 = sp_em[t0]
816 if t0 == SP_I { a0 = LA_NEG_INF }
817 sp_alpha[t0] = a0
818 t0 = t0 + 1
819 }
820 i = 1
821 while i < cnt {
822 var t2: i64 = 0
823 while t2 < SP_NTAGS {
824 var acc: i64 = LA_NEG_INF
825 var pt: i64 = 0
826 while pt < SP_NTAGS {
827 var ok: i64 = 1
828 if t2 == SP_I { if pt == SP_O { ok = 0 } }
829 if ok == 1 {
830 let ap: i64 = sp_alpha[(i - 1) * SP_NTAGS + pt]
831 if la_is_neg_inf(ap) == 0 { acc = la_lse(acc, ap + sp_tw_at(pt, t2, 0)) }
832 }
833 pt = pt + 1
834 }
835 var v: i64 = LA_NEG_INF
836 if la_is_neg_inf(acc) == 0 { v = acc + sp_em[i * SP_NTAGS + t2] }
837 sp_alpha[i * SP_NTAGS + t2] = v
838 t2 = t2 + 1
839 }
840 i = i + 1
841 }
842 var tl: i64 = 0
843 while tl < SP_NTAGS { sp_beta[(cnt - 1) * SP_NTAGS + tl] = 0; tl = tl + 1 }
844 i = cnt - 2
845 while i >= 0 {
846 var t3: i64 = 0
847 while t3 < SP_NTAGS {
848 var acc2: i64 = LA_NEG_INF
849 var nt: i64 = 0
850 while nt < SP_NTAGS {
851 var ok2: i64 = 1
852 if nt == SP_I { if t3 == SP_O { ok2 = 0 } }
853 if ok2 == 1 {
854 let bn: i64 = sp_beta[(i + 1) * SP_NTAGS + nt]
855 if la_is_neg_inf(bn) == 0 { acc2 = la_lse(acc2, sp_tw_at(t3, nt, 0) + sp_em[(i + 1) * SP_NTAGS + nt] + bn) }
856 }
857 nt = nt + 1
858 }
859 sp_beta[i * SP_NTAGS + t3] = acc2
860 t3 = t3 + 1
861 }
862 i = i - 1
863 }
864 var logz: i64 = LA_NEG_INF
865 var tz: i64 = 0
866 while tz < SP_NTAGS { logz = la_lse(logz, sp_alpha[(cnt - 1) * SP_NTAGS + tz]); tz = tz + 1 }
867 i = 0
868 while i < cnt {
869 var sum: i64 = 0
870 var tm: i64 = 0
871 while tm < SP_NTAGS {
872 var m: i64 = 0
873 let a2: i64 = sp_alpha[i * SP_NTAGS + tm]
874 let b2: i64 = sp_beta[i * SP_NTAGS + tm]
875 if (la_is_neg_inf(a2) == 0) & (la_is_neg_inf(b2) == 0) { m = la_exp_neg(a2 + b2 - logz) }
876 sp_marg[i * SP_NTAGS + tm] = m
877 sum = sum + m
878 tm = tm + 1
879 }
880 var dev: i64 = sum - LA_S
881 if dev < 0 { dev = 0 - dev }
882 if dev > sp_crf_margdev { sp_crf_margdev = dev }
883 if i > 0 {
884 var pp: i64 = 0
885 while pp < SP_NTAGS {
886 var tt: i64 = 0
887 while tt < SP_NTAGS {
888 var pm: i64 = 0
889 var ok3: i64 = 1
890 if tt == SP_I { if pp == SP_O { ok3 = 0 } }
891 if ok3 == 1 {
892 let ap2: i64 = sp_alpha[(i - 1) * SP_NTAGS + pp]
893 let bt: i64 = sp_beta[i * SP_NTAGS + tt]
894 if (la_is_neg_inf(ap2) == 0) & (la_is_neg_inf(bt) == 0) { pm = la_exp_neg(ap2 + sp_tw_at(pp, tt, 0) + sp_em[i * SP_NTAGS + tt] + bt - logz) }
895 }
896 sp_pair[i * SP_NPAIR + pp * SP_NTAGS + tt] = pm
897 tt = tt + 1
898 }
899 pp = pp + 1
900 }
901 }
902 i = i + 1
903 }
904 return logz
905}
906// the gold path's score under the current weights (emissions from the cache sp_crf_fb filled), Q10
907func sp_crf_gold_score(cnt: i64) -> i64 {
908 var s: i64 = 0
909 var i: i64 = 0
910 while i < cnt {
911 s = s + sp_em[i * SP_NTAGS + sp_gold[i]]
912 if i > 0 { s = s + sp_tw_at(sp_gold[i - 1], sp_gold[i], 0) }
913 i = i + 1
914 }
915 return s
916}
917// one stochastic-gradient step: w += eta * (empirical - expected), the expected counts from the marginals sp_crf_fb left
918func sp_crf_update(cnt: i64) -> i64 {
919 let den: i64 = SP_CRF_ETA_DEN * (1 + sp_crf_epoch)
920 var i: i64 = 0
921 while i < cnt {
922 let base: i64 = i * SP_NFEAT
923 var t: i64 = 0
924 while t < SP_NTAGS {
925 var target: i64 = 0
926 if sp_gold[i] == t { target = LA_S }
927 let delta: i64 = ((target - sp_marg[i * SP_NTAGS + t]) * SP_CRF_ETA_NUM) / den
928 if delta != 0 {
929 var k: i64 = 0
930 while k < SP_NFEAT { if sp_fmask[base + k] == 1 { sp_bump(((sp_fhs[base + k] * SP_NTAGS + t) & SP_WMASK), delta) } k = k + 1 }
931 }
932 t = t + 1
933 }
934 if i > 0 {
935 var pt: i64 = 0
936 while pt < SP_NTAGS {
937 var t2: i64 = 0
938 while t2 < SP_NTAGS {
939 var tg: i64 = 0
940 if (sp_gold[i - 1] == pt) & (sp_gold[i] == t2) { tg = LA_S }
941 let d2: i64 = ((tg - sp_pair[i * SP_NPAIR + pt * SP_NTAGS + t2]) * SP_CRF_ETA_NUM) / den
942 if d2 != 0 { sp_tbump(pt, t2, d2) }
943 t2 = t2 + 1
944 }
945 pt = pt + 1
946 }
947 }
948 i = i + 1
949 }
950 return 1
951}
952// extract predicted spans from sp_pred into a per-sentence LIST (the occurrence metric's unit)
953func sp_extract_list(cnt: i64, lst: *i64) -> i64 {
954 var i: i64 = 0
955 while i < cnt {
956 if sp_pred[i] == SP_B {
957 var go: i64 = rm_catn(sp_gram, 0, (sp_tokbuf as i64 + sp_offs[i]) as *u8, sp_lens[i])
958 var j: i64 = i + 1
959 while j < cnt { if sp_pred[j] == SP_I {
960 sp_gram[go] = AB_NG_JOIN as u8; go = go + 1
961 go = rm_catn(sp_gram, go, (sp_tokbuf as i64 + sp_offs[j]) as *u8, sp_lens[j])
962 j = j + 1
963 } else { j = cnt } }
964 sl_add(lst, sp_gram, go)
965 var e: i64 = i + 1
966 while e < cnt { if sp_pred[e] == SP_I { e = e + 1 } else { i = e; e = cnt } }
967 if e >= cnt { i = cnt }
968 } else { i = i + 1 }
969 }
970 return 0
971}
972// a dictionary term passes the MAJORITY RULE when it was a gold aspect in at least half its training occurrences
973func sp_dict_majority(slot: i64) -> i64 {
974 if slot < 0 { return 0 }
975 if sp_dgold[slot] * SP_MAJ_NUM >= sp_dseen[slot] { return 1 }
976 return 0
977}
978// the dictionary arm's hits over the current tokens, into a per-sentence list. majority=1 admits only terms that pass
979// the majority rule; majority=0 admits every dictionary hit (the census pass that COUNTS occurrences)
980func sp_dict_list(cnt: i64, dic: *i64, lst: *i64, majority: i64) -> i64 {
981 var u: i64 = 0
982 while u < cnt {
983 let up: *u8 = (sp_tokbuf as i64 + sp_offs[u]) as *u8
984 var s1: i64 = hs_slot_raw(dic, up, sp_lens[u])
985 if s1 >= 0 { if (majority == 0) | (sp_dict_majority(s1) == 1) { sl_add(lst, up, sp_lens[u]) } }
986 if u + 1 < cnt {
987 var g2: i64 = rm_catn(sp_gram, 0, up, sp_lens[u])
988 sp_gram[g2] = AB_NG_JOIN as u8; g2 = g2 + 1
989 g2 = rm_catn(sp_gram, g2, (sp_tokbuf as i64 + sp_offs[u + 1]) as *u8, sp_lens[u + 1])
990 var s2: i64 = hs_slot_raw(dic, sp_gram, g2)
991 if s2 >= 0 { if (majority == 0) | (sp_dict_majority(s2) == 1) { sl_add(lst, sp_gram, g2) } }
992 if u + 2 < cnt {
993 sp_gram[g2] = AB_NG_JOIN as u8; g2 = g2 + 1
994 g2 = rm_catn(sp_gram, g2, (sp_tokbuf as i64 + sp_offs[u + 2]) as *u8, sp_lens[u + 2])
995 var s3: i64 = hs_slot_raw(dic, sp_gram, g2)
996 if s3 >= 0 { if (majority == 0) | (sp_dict_majority(s3) == 1) { sl_add(lst, sp_gram, g2) } }
997 }
998 }
999 u = u + 1
1000 }
1001 return 0
1002}
1003// one pass over the TRAIN split counting, per dictionary term, the sentences it appears in and the times it is gold
1004// (both deduped within a sentence); returns the number of dictionary terms that pass the majority rule
1005func sp_dict_stats(train: *u8, dic: *i64) -> i64 {
1006 let lp: *i64 = sys_mmap(RM_I64_PAIR) as *i64
1007 lp[0] = 0
1008 let b: *u8 = sys_read_file(train, lp)
1009 if (b as i64) == 0 { return 0 }
1010 let n: i64 = lp[0]
1011 let rbuf: *u8 = sys_mmap(AB_RECON_CAP)
1012 let rp: *i64 = sys_mmap(RM_I64_PAIR) as *i64
1013 let r: *i64 = sys_mmap(AB_REC_N * RM_I64_BYTES) as *i64
1014 rp[0] = 0
1015 var pend: i64 = 0
1016 sp_prevlen = 0
1017 sp_prevcnt = 0
1018 while ab_rec_next(b, n, rp, r) == 1 {
1019 let rl: i64 = ab_reconstruct((b as i64 + r[0]) as *u8, r[1], (b as i64 + r[2]) as *u8, r[3], rbuf, AB_RECON_CAP)
1020 if pend == 1 { if sp_same_sent(rbuf, rl) == 0 {
1021 sp_dict_stats_flush(dic)
1022 pend = 0
1023 } }
1024 if pend == 0 {
1025 let cnt: i64 = sp_tokenize(rbuf, rl)
1026 var cpy: i64 = 0
1027 while cpy < rl { sp_prevsent[cpy] = rbuf[cpy]; cpy = cpy + 1 }
1028 sp_prevlen = rl
1029 sp_prevcnt = cnt
1030 sl_reset(sp_lg)
1031 sl_reset(sp_lu)
1032 if cnt > 0 { sp_dict_list(cnt, dic, sp_lu, 0) }
1033 pend = 1
1034 }
1035 let gl: i64 = ab_norm((b as i64 + r[2]) as *u8, r[3], sp_normbuf, AB_TERM_MAX)
1036 sl_add(sp_lg, sp_normbuf, gl)
1037 }
1038 if pend == 1 { sp_dict_stats_flush(dic) }
1039 // census: how many dictionary terms pass the majority rule
1040 let hash: *i64 = dic[HS_HASH] as *i64
1041 var maj: i64 = 0
1042 var s: i64 = 0
1043 while s < AB_SET_SLOTS { if hash[s] != 0 { if sp_dict_majority(s) == 1 { maj = maj + 1 } } s = s + 1 }
1044 return maj
1045}
1046func sp_dict_stats_flush(dic: *i64) -> i64 {
1047 let ubuf: *u8 = sp_lu[SL_BUF] as *u8
1048 let uoffs: *i64 = sp_lu[SL_OFFS] as *i64
1049 let ulens: *i64 = sp_lu[SL_LENS] as *i64
1050 var i: i64 = 0
1051 while i < sp_lu[SL_COUNT] {
1052 let s: i64 = hs_slot_raw(dic, (ubuf as i64 + uoffs[i]) as *u8, ulens[i])
1053 if s >= 0 { sp_dseen[s] = sp_dseen[s] + 1 }
1054 i = i + 1
1055 }
1056 let gbuf: *u8 = sp_lg[SL_BUF] as *u8
1057 let goffs: *i64 = sp_lg[SL_OFFS] as *i64
1058 let glens: *i64 = sp_lg[SL_LENS] as *i64
1059 i = 0
1060 while i < sp_lg[SL_COUNT] {
1061 let s: i64 = hs_slot_raw(dic, (gbuf as i64 + goffs[i]) as *u8, glens[i])
1062 if s >= 0 { sp_dgold[s] = sp_dgold[s] + 1 }
1063 i = i + 1
1064 }
1065 return 0
1066}
1067// extract predicted aspect-term spans from sp_pred into the predicted set (deduped, normalised)
1068func sp_extract(cnt: i64, pred_set: *i64) -> i64 {
1069 var i: i64 = 0
1070 while i < cnt {
1071 if sp_pred[i] == SP_B {
1072 var go: i64 = rm_catn(sp_gram, 0, (sp_tokbuf as i64 + sp_offs[i]) as *u8, sp_lens[i])
1073 var j: i64 = i + 1
1074 while j < cnt { if sp_pred[j] == SP_I {
1075 sp_gram[go] = AB_NG_JOIN as u8; go = go + 1
1076 go = rm_catn(sp_gram, go, (sp_tokbuf as i64 + sp_offs[j]) as *u8, sp_lens[j])
1077 j = j + 1
1078 } else { j = cnt } }
1079 if hs_has_raw(pred_set, sp_gram, go) == 0 { hs_add_raw(pred_set, sp_gram, go) }
1080 // advance past this span
1081 var e: i64 = i + 1
1082 while e < cnt { if sp_pred[e] == SP_I { e = e + 1 } else { i = e; e = cnt } }
1083 if e >= cnt { i = cnt }
1084 } else { i = i + 1 }
1085 }
1086 return 0
1087}
1088// one pass over the train file: reconstruct, tokenise, gold BIO, decode with current weights, update. Returns updates.
1089func sp_train_pass(train: *u8, dic: *i64, first: *i64) -> i64 {
1090 let lp: *i64 = sys_mmap(RM_I64_PAIR) as *i64
1091 lp[0] = 0
1092 let b: *u8 = sys_read_file(train, lp)
1093 if (b as i64) == 0 { return 0 }
1094 let n: i64 = lp[0]
1095 let rbuf: *u8 = sys_mmap(AB_RECON_CAP)
1096 let rp: *i64 = sys_mmap(RM_I64_PAIR) as *i64
1097 let r: *i64 = sys_mmap(AB_REC_N * RM_I64_BYTES) as *i64
1098 rp[0] = 0
1099 var updates: i64 = 0
1100 sp_prevlen = 0
1101 sp_prevcnt = 0
1102 sp_pending = 0
1103 if first[0] == 1 { sp_ngroups = 0 }
1104 var cur: i64 = rp[0]
1105 while ab_rec_next(b, n, rp, r) == 1 {
1106 let rl: i64 = ab_reconstruct((b as i64 + r[0]) as *u8, r[1], (b as i64 + r[2]) as *u8, r[3], rbuf, AB_RECON_CAP)
1107 if sp_pending == 1 { if sp_same_sent(rbuf, rl) == 0 {
1108 // a NEW sentence: train the accumulated one first -- sp_tokbuf still holds its tokens
1109 updates = updates + sp_flush(sp_prevcnt, dic)
1110 if first[0] == 1 { first[3] = first[3] + 1 }
1111 sp_pending = 0
1112 } }
1113 var cnt: i64 = sp_prevcnt
1114 if sp_pending == 0 {
1115 // a group begins at the record just read: remember where it starts (first pass only) for the shuffled epochs
1116 if first[0] == 1 { if sp_same_sent(rbuf, rl) == 0 { if sp_ngroups < SP_GROUP_CAP { sp_gstarts[sp_ngroups] = cur; sp_ngroups = sp_ngroups + 1 } } }
1117 cnt = sp_tokenize(rbuf, rl)
1118 sp_tag_sentence(cnt)
1119 var cpy: i64 = 0
1120 while cpy < rl { sp_prevsent[cpy] = rbuf[cpy]; cpy = cpy + 1 }
1121 sp_prevlen = rl
1122 sp_prevcnt = cnt
1123 sp_acc_reset()
1124 }
1125 if cnt > 0 {
1126 if sp_gold_bio(cnt, (b as i64 + r[2]) as *u8, r[3]) == 1 {
1127 if first[0] == 1 { first[1] = first[1] + 1 } // count usable train records once
1128 sp_acc_or(cnt)
1129 sp_pending = 1
1130 } else { if first[0] == 1 { first[2] = first[2] + 1 } }
1131 }
1132 cur = rp[0]
1133 }
1134 if sp_pending == 1 {
1135 updates = updates + sp_flush(sp_prevcnt, dic)
1136 if first[0] == 1 { first[3] = first[3] + 1 }
1137 sp_pending = 0
1138 }
1139 return updates
1140}
1141// train ONE merged-sentence group whose first record starts at byte offset start: tokenise and tag once, fold every
1142// consecutive same-sentence record's gold into the accumulator, flush once. Returns the update count (0 when the group
1143// carries no usable record). The file is already in memory (b, n).
1144func sp_train_group(b: *u8, n: i64, start: i64, dic: *i64) -> i64 {
1145 sp_grp[0] = start
1146 if ab_rec_next(b, n, sp_grp, sp_grr) != 1 { return 0 }
1147 let rl: i64 = ab_reconstruct((b as i64 + sp_grr[0]) as *u8, sp_grr[1], (b as i64 + sp_grr[2]) as *u8, sp_grr[3], sp_grbuf, AB_RECON_CAP)
1148 let cnt: i64 = sp_tokenize(sp_grbuf, rl)
1149 sp_tag_sentence(cnt)
1150 var cpy: i64 = 0
1151 while cpy < rl { sp_prevsent[cpy] = sp_grbuf[cpy]; cpy = cpy + 1 }
1152 sp_prevlen = rl
1153 sp_prevcnt = cnt
1154 sp_acc_reset()
1155 var any: i64 = 0
1156 if cnt > 0 { if sp_gold_bio(cnt, (b as i64 + sp_grr[2]) as *u8, sp_grr[3]) == 1 { sp_acc_or(cnt); any = 1 } }
1157 var more: i64 = 1
1158 while more == 1 {
1159 if ab_rec_next(b, n, sp_grp, sp_grr) == 1 {
1160 let rl2: i64 = ab_reconstruct((b as i64 + sp_grr[0]) as *u8, sp_grr[1], (b as i64 + sp_grr[2]) as *u8, sp_grr[3], sp_grbuf, AB_RECON_CAP)
1161 if sp_same_sent(sp_grbuf, rl2) == 1 {
1162 if cnt > 0 { if sp_gold_bio(cnt, (b as i64 + sp_grr[2]) as *u8, sp_grr[3]) == 1 { sp_acc_or(cnt); any = 1 } }
1163 } else { more = 0 }
1164 } else { more = 0 }
1165 }
1166 if any == 1 { return sp_flush(cnt, dic) }
1167 return 0
1168}
1169// train the perceptron and evaluate on the test split. out is SP_O_N wide.
1170func sp_eval(train: *u8, test: *u8, out: *i64) -> i64 {
1171 var q: i64 = 0
1172 while q < SP_O_N { out[q] = 0; q = q + 1 }
1173 let ng: i64 = ab_gold_load(test) // gold set G in the global ab_set
1174 out[SP_O_NG] = ng
1175 if ng <= 0 { return 0 }
1176 sp_reset()
1177 // the POS tagger: trained once on the treebank set by sp_set_treebank (none -> every POS byte reads '^')
1178 sp_pos_on = 0
1179 if (sp_treebank as i64) != 0 {
1180 let pto: *i64 = sys_mmap(PT_O_N * RM_I64_BYTES) as *i64
1181 var pq: i64 = 0
1182 while pq < PT_O_N { pto[pq] = 0; pq = pq + 1 }
1183 pt_reset()
1184 if pt_train(sp_treebank, pto) >= 0 { sp_pos_on = 1; out[SP_O_POS_TOK] = pto[PT_O_TRAINTOK] }
1185 }
1186 out[SP_O_POS] = sp_pos_on
1187 // the parser: trained once on the same treebank, reading the tagger's tags, unless the parse switch is off
1188 sp_parse_on = 0
1189 if (sp_pos_on == 1) & (sp_parse_want == 1) {
1190 let dpo: *i64 = sys_mmap(DP_O_N * RM_I64_BYTES) as *i64
1191 var dq: i64 = 0
1192 while dq < DP_O_N { dpo[dq] = 0; dq = dq + 1 }
1193 dp_reset()
1194 if dp_train(sp_treebank, dpo) >= 0 { sp_parse_on = 1; out[SP_O_PARSE_SENT] = dpo[DP_O_TRAINSENT] }
1195 }
1196 out[SP_O_PARSE] = sp_parse_on
1197 // the clusters: the training vocabulary clustered once by the PPMI model set by sp_set_clusters (none -> every
1198 // cluster byte reads '@')
1199 sp_clust_on = 0
1200 if (sp_clust_model as i64) != 0 {
1201 wc_reset()
1202 if wc_load_model(sp_clust_model) == 1 {
1203 sp_clust_vocab(train)
1204 let wco: *i64 = sys_mmap(WC_O_N * RM_I64_BYTES) as *i64
1205 if wc_build(wco) == 1 {
1206 sp_clust_on = 1
1207 out[SP_O_CLUST_VOCAB] = wco[WC_O_VOCAB]
1208 out[SP_O_CLUST_INMODEL] = wco[WC_O_INMODEL]
1209 }
1210 }
1211 }
1212 out[SP_O_CLUST] = sp_clust_on
1213 // the objective: the perceptron unless the CRF mode is set; the log-domain tables are built once, idempotently
1214 sp_crf_on = sp_crf_want
1215 sp_crf_epoch = 0
1216 sp_crf_nll = 0
1217 sp_crf_margdev = 0
1218 sp_crf_negnll = 0
1219 if sp_crf_on == 1 { la_init() }
1220 // the embeddings: the table set by sp_set_embed, loaded through the ONE reader and admitted only at the declared width
1221 sp_emb_on = 0
1222 ef_reset()
1223 if (sp_emb_path as i64) != 0 {
1224 if ef_load(sp_emb_ppmi, sp_emb_path) == 1 { if ef_dim_of() == SP_EMB_DIMS { sp_emb_on = 1 } }
1225 }
1226 out[SP_O_EMB] = sp_emb_on
1227 out[SP_O_EMB_DIM] = ef_dim_of()
1228 let dic: *i64 = hs_new()
1229 out[SP_O_DICT] = ab_dict_load(train, dic)
1230 out[SP_O_DICT_MAJ] = sp_dict_stats(train, dic)
1231 let ctr: *i64 = sys_mmap(SP_CTR_N * RM_I64_BYTES) as *i64
1232 ctr[0] = 1; ctr[1] = 0; ctr[2] = 0; ctr[3] = 0 // first-pass flag + used + skipped + merged sentences
1233 var ep: i64 = 0
1234 var lastupd: i64 = 0
1235 // epoch 0 in file order (it records the groups); the later epochs walk the file again by default, or the groups in
1236 // a fresh deterministic order when the shuffle mode is set
1237 lastupd = sp_train_pass(train, dic, ctr)
1238 ctr[0] = 0
1239 out[SP_O_CRF_NLL_FIRST] = sp_crf_nll
1240 out[SP_O_GROUPS] = sp_ngroups
1241 out[SP_O_SHUF] = sp_shuffle_want
1242 if sp_shuffle_want == 1 {
1243 let glp: *i64 = sys_mmap(RM_I64_PAIR) as *i64
1244 glp[0] = 0
1245 let gb: *u8 = sys_read_file(train, glp)
1246 if (gb as i64) != 0 {
1247 let gn: i64 = glp[0]
1248 ep = 1
1249 while ep < SP_EPOCHS {
1250 pt_permute(sp_gorder, sp_ngroups, ep)
1251 sp_crf_epoch = ep
1252 sp_crf_nll = 0
1253 lastupd = 0
1254 var gi: i64 = 0
1255 while gi < sp_ngroups { lastupd = lastupd + sp_train_group(gb, gn, sp_gstarts[sp_gorder[gi]], dic); gi = gi + 1 }
1256 ep = ep + 1
1257 }
1258 }
1259 } else {
1260 ep = 1
1261 while ep < SP_EPOCHS {
1262 sp_crf_epoch = ep
1263 sp_crf_nll = 0
1264 lastupd = sp_train_pass(train, dic, ctr)
1265 ep = ep + 1
1266 }
1267 }
1268 out[SP_O_CRF] = sp_crf_on
1269 out[SP_O_CRF_NLL_LAST] = sp_crf_nll
1270 out[SP_O_CRF_MARGDEV] = sp_crf_margdev
1271 out[SP_O_CRF_NEGNLL] = sp_crf_negnll
1272 out[SP_O_EMB_LOOKUPS] = ef_lookup_count()
1273 out[SP_O_EMB_HITS] = ef_hit_count()
1274 out[SP_O_TRAINREC] = ctr[1]
1275 out[SP_O_TRAINSKIP] = ctr[2]
1276 out[SP_O_TRAINSENT] = ctr[3]
1277 out[SP_O_EPOCHS] = SP_EPOCHS
1278 out[SP_O_UPDATES] = lastupd
1279 sp_T = sp_clock
1280 if sp_T <= 0 { sp_T = 1 }
1281 // decode the test split with the averaged weights, collect predicted spans; the UNION set also carries the
1282 // training-dictionary tagger's n<=3-gram hits over the same tokens
1283 let pred_set: *i64 = hs_new()
1284 hs_reset(pred_set)
1285 let union_set: *i64 = hs_new()
1286 hs_reset(union_set)
1287 let lp: *i64 = sys_mmap(RM_I64_PAIR) as *i64
1288 lp[0] = 0
1289 let b: *u8 = sys_read_file(test, lp)
1290 if (b as i64) == 0 { return 0 }
1291 let n: i64 = lp[0]
1292 let rbuf: *u8 = sys_mmap(AB_RECON_CAP)
1293 let rp: *i64 = sys_mmap(RM_I64_PAIR) as *i64
1294 let r: *i64 = sys_mmap(AB_REC_N * RM_I64_BYTES) as *i64
1295 rp[0] = 0
1296 // test records of ONE sentence are consecutive (one per aspect): decode each sentence ONCE, collect its gold terms,
1297 // and score per OCCURRENCE (the paper's unit) beside the distinct-term sets
1298 var doc: i64 = 0
1299 var oc_tp: i64 = 0
1300 var oc_ns: i64 = 0
1301 var oc_ng: i64 = 0
1302 var ocu_tp: i64 = 0
1303 var ocu_ns: i64 = 0
1304 var tsent: i64 = 0
1305 var tpend: i64 = 0
1306 var ec_untok: i64 = 0
1307 var ec_unseen1: i64 = 0
1308 var ec_unseenm: i64 = 0
1309 var ec_seen1: i64 = 0
1310 var ec_seenm: i64 = 0
1311 var ec_unionmiss: i64 = 0
1312 sp_prevlen = 0
1313 sp_prevcnt = 0
1314 while ab_rec_next(b, n, rp, r) == 1 {
1315 doc = doc + 1
1316 let rl: i64 = ab_reconstruct((b as i64 + r[0]) as *u8, r[1], (b as i64 + r[2]) as *u8, r[3], rbuf, AB_RECON_CAP)
1317 if tpend == 1 { if sp_same_sent(rbuf, rl) == 0 {
1318 oc_ng = oc_ng + sp_lg[SL_COUNT]
1319 oc_ns = oc_ns + sp_lp[SL_COUNT]
1320 ocu_ns = ocu_ns + sp_lu[SL_COUNT]
1321 oc_tp = oc_tp + sl_inter(sp_lp, sp_lg)
1322 ocu_tp = ocu_tp + sl_inter(sp_lu, sp_lg)
1323 tsent = tsent + 1
1324 tpend = 0
1325 } }
1326 if tpend == 0 {
1327 let cnt: i64 = sp_tokenize(rbuf, rl)
1328 sp_tag_sentence(cnt)
1329 var cpy: i64 = 0
1330 while cpy < rl { sp_prevsent[cpy] = rbuf[cpy]; cpy = cpy + 1 }
1331 sp_prevlen = rl
1332 sp_prevcnt = cnt
1333 sl_reset(sp_lg)
1334 sl_reset(sp_lp)
1335 sl_reset(sp_lu)
1336 if cnt > 0 {
1337 var i: i64 = 0
1338 while i < cnt { sp_feats(cnt, i, dic); i = i + 1 }
1339 sp_viterbi(cnt, 1)
1340 sp_extract(cnt, pred_set)
1341 sp_extract_list(cnt, sp_lp)
1342 sp_extract_list(cnt, sp_lu)
1343 sp_dict_list(cnt, dic, sp_lu, 1)
1344 // fold this sentence's union spans into the corpus-level union SET (the distinct-term line)
1345 let ubuf: *u8 = sp_lu[SL_BUF] as *u8
1346 let uoffs: *i64 = sp_lu[SL_OFFS] as *i64
1347 let ulens: *i64 = sp_lu[SL_LENS] as *i64
1348 var q2: i64 = 0
1349 while q2 < sp_lu[SL_COUNT] {
1350 if hs_has_raw(union_set, (ubuf as i64 + uoffs[q2]) as *u8, ulens[q2]) == 0 { hs_add_raw(union_set, (ubuf as i64 + uoffs[q2]) as *u8, ulens[q2]) }
1351 q2 = q2 + 1
1352 }
1353 }
1354 tpend = 1
1355 }
1356 // this record's gold term, normalised the way the gold set is, into the sentence's gold list
1357 let gl: i64 = ab_norm((b as i64 + r[2]) as *u8, r[3], sp_normbuf, AB_TERM_MAX)
1358 // a term repeated as the gold of one sentence is ONE gold occurrence to the scorer (the list dedupes), so the census
1359 // counts it once too: a duplicate record is skipped, and the five parts equal gold minus matched by construction
1360 let dupgold: i64 = sl_has(sp_lg, sp_normbuf, gl)
1361 sl_add(sp_lg, sp_normbuf, gl)
1362 // the error census: this occurrence against the sentence's decoded lists (decoded when the sentence began), with
1363 // the same predicate the scorer uses, then the reason it could be missed
1364 if (dupgold == 0) & (sl_has(sp_lp, sp_normbuf, gl) == 0) {
1365 var multi: i64 = 0
1366 var q3: i64 = 0
1367 while q3 < gl { if sp_normbuf[q3] == (AB_NG_JOIN as u8) { multi = 1 } q3 = q3 + 1 }
1368 if sp_gold_bio(sp_prevcnt, (b as i64 + r[2]) as *u8, r[3]) == 0 { ec_untok = ec_untok + 1 }
1369 else {
1370 if hs_has_raw(dic, (b as i64 + r[2]) as *u8, r[3]) == 1 {
1371 if multi == 1 { ec_seenm = ec_seenm + 1 } else { ec_seen1 = ec_seen1 + 1 }
1372 } else {
1373 if multi == 1 { ec_unseenm = ec_unseenm + 1 } else { ec_unseen1 = ec_unseen1 + 1 }
1374 }
1375 }
1376 }
1377 if (dupgold == 0) & (sl_has(sp_lu, sp_normbuf, gl) == 0) { ec_unionmiss = ec_unionmiss + 1 }
1378 }
1379 out[SP_O_EC_UNTOK] = ec_untok
1380 out[SP_O_EC_UNSEEN1] = ec_unseen1
1381 out[SP_O_EC_UNSEENM] = ec_unseenm
1382 out[SP_O_EC_SEEN1] = ec_seen1
1383 out[SP_O_EC_SEENM] = ec_seenm
1384 out[SP_O_EC_UNIONMISS] = ec_unionmiss
1385 if tpend == 1 {
1386 oc_ng = oc_ng + sp_lg[SL_COUNT]
1387 oc_ns = oc_ns + sp_lp[SL_COUNT]
1388 ocu_ns = ocu_ns + sp_lu[SL_COUNT]
1389 oc_tp = oc_tp + sl_inter(sp_lp, sp_lg)
1390 ocu_tp = ocu_tp + sl_inter(sp_lu, sp_lg)
1391 tsent = tsent + 1
1392 tpend = 0
1393 }
1394 out[SP_O_TESTREC] = doc
1395 out[SP_O_TESTSENT] = tsent
1396 out[SP_O_OC_TP] = oc_tp
1397 out[SP_O_OC_NS] = oc_ns
1398 out[SP_O_OC_NG] = oc_ng
1399 let ocprf: *i64 = sys_mmap(AB_PRF_N * RM_I64_BYTES) as *i64
1400 ab_prf(oc_tp, oc_ns, oc_ng, ocprf)
1401 out[SP_O_OC_P] = ocprf[AB_P]
1402 out[SP_O_OC_R] = ocprf[AB_R]
1403 out[SP_O_OC_F1] = ocprf[AB_F1]
1404 out[SP_O_OCU_TP] = ocu_tp
1405 out[SP_O_OCU_NS] = ocu_ns
1406 let ocuprf: *i64 = sys_mmap(AB_PRF_N * RM_I64_BYTES) as *i64
1407 ab_prf(ocu_tp, ocu_ns, oc_ng, ocuprf)
1408 out[SP_O_OCU_P] = ocuprf[AB_P]
1409 out[SP_O_OCU_R] = ocuprf[AB_R]
1410 out[SP_O_OCU_F1] = ocuprf[AB_F1]
1411 out[SP_O_NS] = hs_count(pred_set)
1412 // score the union arm
1413 let uhash: *i64 = union_set[HS_HASH] as *i64
1414 let uoff: *i64 = union_set[HS_OFF] as *i64
1415 let ulen: *i64 = union_set[HS_LEN] as *i64
1416 let uarena: *u8 = union_set[HS_ARENA] as *u8
1417 var uinter: i64 = 0
1418 var uslot: i64 = 0
1419 while uslot < AB_SET_SLOTS {
1420 if uhash[uslot] != 0 { if ab_set_has_raw((uarena as i64 + uoff[uslot]) as *u8, ulen[uslot]) == 1 { uinter = uinter + 1 } }
1421 uslot = uslot + 1
1422 }
1423 out[SP_O_U_INTER] = uinter
1424 out[SP_O_U_NS] = hs_count(union_set)
1425 let uprf: *i64 = sys_mmap(AB_PRF_N * RM_I64_BYTES) as *i64
1426 ab_prf(uinter, hs_count(union_set), ng, uprf)
1427 out[SP_O_U_P] = uprf[AB_P]
1428 out[SP_O_U_R] = uprf[AB_R]
1429 out[SP_O_U_F1] = uprf[AB_F1]
1430 let phash: *i64 = pred_set[HS_HASH] as *i64
1431 let poff: *i64 = pred_set[HS_OFF] as *i64
1432 let plen: *i64 = pred_set[HS_LEN] as *i64
1433 let parena: *u8 = pred_set[HS_ARENA] as *u8
1434 var inter: i64 = 0
1435 var slot: i64 = 0
1436 while slot < AB_SET_SLOTS {
1437 if phash[slot] != 0 { if ab_set_has_raw((parena as i64 + poff[slot]) as *u8, plen[slot]) == 1 { inter = inter + 1 } }
1438 slot = slot + 1
1439 }
1440 out[SP_O_INTER] = inter
1441 let prf: *i64 = sys_mmap(AB_PRF_N * RM_I64_BYTES) as *i64
1442 ab_prf(inter, hs_count(pred_set), ng, prf)
1443 out[SP_O_P] = prf[AB_P]
1444 out[SP_O_R] = prf[AB_R]
1445 out[SP_O_F1] = prf[AB_F1]
1446 return 0
1447}