nx_absa_lib.nx source
↩ module page · 883 lines · 35526 B
1// nx_absa_lib.nx -- the SCORING MATH for SemEval-2014 Task 4 subtask 1 (aspect term extraction), as a shared lib
2// so the CLI (nx_absa_bench) and the gate (nx_absa_bench_gate) share ONE definition of P, R and F1 and one gold
3// parser. The metric is the paper's own (Pontiki et al. 2014, eq. 1-2): P=|S and G|/|S|, R=|S and G|/|G|,
4// F1=2PR/(P+R), which for a set overlap is exactly 2*inter/(|S|+|G|) -- computed from counts so there is no
5// double rounding. Everything is permil (1000 = 1.00). Nothing here is a threshold: the lib reports numbers, the
6// gate says what a correct scorer must return, and the CLI prints the number beside the published winners.
7//
8// THE GOLD FORMAT (ABSA-PyTorch .seg mirror of the official test set): three lines per record --
9// line 0 mod 3: the sentence with the aspect term replaced by the placeholder $T$
10// line 1 mod 3: the aspect term (single or multi word, e.g. "hard disk", "delivery times")
11// line 2 mod 3: the polarity (1 / 0 / -1)
12// The gold aspect-term SET G is the distinct normalised terms across the term lines (footnote 3 of the paper
13// counts occurrences; this is the distinct-term variant, DECLARED, a related but not identical metric).
14//
15// THE PREDICTOR is a UNIGRAM DOCUMENT-FREQUENCY baseline over the test sentences: it composes the ONE tokenizer
16// (rm_next_token via rm_mine_doc) and the ONE frequency ranker (rm_mine_rank) from nx_reviewmine_lib, so no
17// second tokenizer or ranker exists. It is a weak proxy for Hu-Liu (2004) frequent-noun-phrase mining -- our
18// miner does not yet extract multi-word noun phrases (rung rm_aspects), so a multi-word gold term is UNMATCHABLE
19// by a unigram prediction under exact-set match. That is the honest reason this baseline sits far below the
20// supervised systems, and the gap IS the un-landed rung. license_tier: ORIGINAL No hw writes (Rule 26). LIB.
21import "nx_syscalls.nx"
22import "nx_reviewmine_lib.nx"
23
24const AB_PERMIL: i64 = 1000
25const AB_P: i64 = 0
26const AB_R: i64 = 1
27const AB_F1: i64 = 2
28const AB_PRF_N: i64 = 3
29// out[] indices for ab_eval
30const AB_O_INTER: i64 = 0
31const AB_O_NS: i64 = 1
32const AB_O_NG: i64 = 2
33const AB_O_P: i64 = 3
34const AB_O_R: i64 = 4
35const AB_O_F1: i64 = 5
36const AB_O_K: i64 = 6
37const AB_O_VOCAB: i64 = 7
38const AB_O_N: i64 = 8
39
40const AB_SET_SLOTS: i64 = 65536 // power of two; a SemEval domain has ~1-2k distinct aspect terms
41const AB_SET_ARENA: i64 = 4194304 // AB_SET_SLOTS * avg term bytes, generous (multi-word terms allowed)
42const AB_TERM_MAX: i64 = 256 // longest aspect term stored
43const AB_RECORD_LINES: i64 = 3 // the .seg record is three lines
44const AB_TERM_LINE: i64 = 1 // lineno mod 3 == 1 is the aspect-term line
45const AB_SENT_LINE: i64 = 0 // lineno mod 3 == 0 is the sentence line
46const AB_MIN_SUPPORT: i64 = 1 // a candidate term must appear in at least one sentence
47const AB_ARENA_TAIL: i64 = 1 // the NUL after a stored term
48
49const AB_STOPWORDS: *u8 = "knowledge/reviewmine/stopwords.conf"
50
51static ab_hash: *i64
52static ab_off: *i64
53static ab_len: *i64
54static ab_arena: *u8
55static ab_used: i64
56static ab_cnt: i64
57static ab_nbuf: *u8
58
59// normalise a raw term span into out: lowercase ASCII, trim ends, collapse internal whitespace to one space,
60// strip a trailing CR (a gold file may arrive CRLF from any host). Returns the normalised length.
61func ab_norm(src: *u8, n: i64, out: *u8, cap: i64) -> i64 {
62 var m: i64 = n
63 if m > 0 { if (src[m - 1] as i64) == RM_CR { m = m - 1 } }
64 var o: i64 = 0
65 var pend_sp: i64 = 0
66 var started: i64 = 0
67 var i: i64 = 0
68 while i < m {
69 var c: i64 = src[i] as i64
70 if c >= RM_UPPER_A { if c <= RM_UPPER_Z { c = c + RM_CASE_DELTA } }
71 if c == RM_SP { if started == 1 { pend_sp = 1 } }
72 else { if c == RM_TAB { if started == 1 { pend_sp = 1 } }
73 else {
74 if pend_sp == 1 { if o < cap - 1 { out[o] = RM_SP as u8; o = o + 1 } pend_sp = 0 }
75 if o < cap - 1 { out[o] = c as u8; o = o + 1 }
76 started = 1
77 } }
78 i = i + 1
79 }
80 out[o] = 0 as u8
81 return o
82}
83
84func ab_set_reset() -> i64 {
85 if (ab_hash as i64) == 0 {
86 ab_hash = sys_mmap(AB_SET_SLOTS * RM_I64_BYTES) as *i64
87 ab_off = sys_mmap(AB_SET_SLOTS * RM_I64_BYTES) as *i64
88 ab_len = sys_mmap(AB_SET_SLOTS * RM_I64_BYTES) as *i64
89 ab_arena = sys_mmap(AB_SET_ARENA)
90 ab_nbuf = sys_mmap(AB_TERM_MAX)
91 } else {
92 var i: i64 = 0
93 while i < AB_SET_SLOTS { ab_hash[i] = 0; i = i + 1 }
94 }
95 ab_used = 0
96 ab_cnt = 0
97 return 0
98}
99
100// insert an ALREADY-normalised term (length n). Returns 1 if newly added, 0 if a duplicate or the arena is full.
101func ab_set_add_norm(s: *u8, n: i64) -> i64 {
102 if n <= 0 { return 0 }
103 let h: i64 = rm_hash(s, n)
104 var slot: i64 = h % AB_SET_SLOTS
105 var tries: i64 = 0
106 while tries < AB_SET_SLOTS {
107 if ab_hash[slot] == 0 {
108 if ab_used + n + AB_ARENA_TAIL > AB_SET_ARENA { return 0 }
109 ab_hash[slot] = h
110 ab_off[slot] = ab_used
111 ab_len[slot] = n
112 rm_catn(ab_arena, ab_used, s, n)
113 ab_arena[ab_used + n] = 0 as u8
114 ab_used = ab_used + n + AB_ARENA_TAIL
115 ab_cnt = ab_cnt + 1
116 return 1
117 }
118 if ab_hash[slot] == h { if ab_len[slot] == n {
119 var same: i64 = 1
120 var j: i64 = 0
121 while j < n { if ab_arena[ab_off[slot] + j] != s[j] { same = 0 } j = j + 1 }
122 if same == 1 { return 0 }
123 } }
124 slot = (slot + 1) % AB_SET_SLOTS
125 tries = tries + 1
126 }
127 return 0
128}
129
130func ab_set_add_raw(src: *u8, n: i64) -> i64 {
131 let m: i64 = ab_norm(src, n, ab_nbuf, AB_TERM_MAX)
132 return ab_set_add_norm(ab_nbuf, m)
133}
134
135// membership of a raw (un-normalised) term span in the gold set
136func ab_set_has_raw(src: *u8, n: i64) -> i64 {
137 let m: i64 = ab_norm(src, n, ab_nbuf, AB_TERM_MAX)
138 if m <= 0 { return 0 }
139 let h: i64 = rm_hash(ab_nbuf, m)
140 var slot: i64 = h % AB_SET_SLOTS
141 var tries: i64 = 0
142 while tries < AB_SET_SLOTS {
143 if ab_hash[slot] == 0 { return 0 }
144 if ab_hash[slot] == h { if ab_len[slot] == m {
145 var same: i64 = 1
146 var j: i64 = 0
147 while j < m { if ab_arena[ab_off[slot] + j] != ab_nbuf[j] { same = 0 } j = j + 1 }
148 if same == 1 { return 1 }
149 } }
150 slot = (slot + 1) % AB_SET_SLOTS
151 tries = tries + 1
152 }
153 return 0
154}
155
156func ab_set_count() -> i64 { return ab_cnt }
157
158// the paper's F1 for a set overlap, from counts, in permil. Zero-prediction (ns==0) and no-gold (ng==0) both
159// give F1=0 rather than a divide -- the zero-prediction control the rung's done-rule names.
160func ab_prf(inter: i64, ns: i64, ng: i64, out: *i64) -> i64 {
161 if ns <= 0 { out[AB_P] = 0 } else { out[AB_P] = inter * AB_PERMIL / ns }
162 if ng <= 0 { out[AB_R] = 0 } else { out[AB_R] = inter * AB_PERMIL / ng }
163 let denom: i64 = ns + ng
164 if denom <= 0 { out[AB_F1] = 0 } else { out[AB_F1] = (2 * inter * AB_PERMIL) / denom }
165 return 0
166}
167
168// load the gold aspect-term SET from a .seg file. Returns |G| (distinct normalised terms).
169func ab_gold_load(path: *u8) -> i64 {
170 ab_set_reset()
171 let lp: *i64 = sys_mmap(RM_I64_PAIR) as *i64
172 lp[0] = 0
173 let b: *u8 = sys_read_file(path, lp)
174 if (b as i64) == 0 { return 0 }
175 let n: i64 = lp[0]
176 var i: i64 = 0
177 var lineno: i64 = 0
178 while i < n {
179 let e: i64 = rm_line_end(b, n, i)
180 if (lineno % AB_RECORD_LINES) == AB_TERM_LINE { if e > i { ab_set_add_raw((b as i64 + i) as *u8, e - i) } }
181 i = e + 1
182 lineno = lineno + 1
183 }
184 return ab_set_count()
185}
186
187const AB_RECON_CAP: i64 = 8192 // a reconstructed review sentence
188const AB_DOLLAR: i64 = 36 // '$'
189const AB_UPPER_T: i64 = 84 // 'T' -- the $T$ placeholder the .seg masks the aspect term with
190const AB_PLACEHOLDER_LEN: i64 = 3 // the three bytes of $T$
191
192// rebuild the real review sentence by substituting the aspect term back into the FIRST $T$ placeholder, so the
193// term actually appears in the corpus the frequency baseline mines (the .seg masks it out otherwise). Trailing
194// CR is stripped from both spans. No placeholder -> the sentence is copied as-is. Returns the byte length.
195func ab_reconstruct(sent: *u8, sl: i64, term: *u8, tl: i64, out: *u8, cap: i64) -> i64 {
196 var m: i64 = sl
197 if m > 0 { if (sent[m - 1] as i64) == RM_CR { m = m - 1 } }
198 var tm: i64 = tl
199 if tm > 0 { if (term[tm - 1] as i64) == RM_CR { tm = tm - 1 } }
200 var pos: i64 = 0 - 1
201 var found: i64 = 0
202 var i: i64 = 0
203 while i + AB_PLACEHOLDER_LEN <= m {
204 if found == 0 {
205 if (sent[i] as i64) == AB_DOLLAR { if (sent[i + 1] as i64) == AB_UPPER_T { if (sent[i + 2] as i64) == AB_DOLLAR { pos = i; found = 1 } } }
206 }
207 i = i + 1
208 }
209 var o: i64 = 0
210 if pos < 0 {
211 var j: i64 = 0
212 while j < m { if o < cap - 1 { out[o] = sent[j]; o = o + 1 } j = j + 1 }
213 } else {
214 var a: i64 = 0
215 while a < pos { if o < cap - 1 { out[o] = sent[a]; o = o + 1 } a = a + 1 }
216 var t: i64 = 0
217 while t < tm { if o < cap - 1 { out[o] = term[t]; o = o + 1 } t = t + 1 }
218 var z: i64 = pos + AB_PLACEHOLDER_LEN
219 while z < m { if o < cap - 1 { out[o] = sent[z]; o = o + 1 } z = z + 1 }
220 }
221 out[o] = 0 as u8
222 return o
223}
224
225// full pipeline: load the gold set, predict the top-K frequent unigrams over the reconstructed sentences, score.
226// k<=0 means K = |G| (a balanced baseline that predicts as many terms as the gold has). out is AB_O_N wide.
227func ab_eval(path: *u8, k: i64, out: *i64) -> i64 {
228 var q: i64 = 0
229 while q < AB_O_N { out[q] = 0; q = q + 1 }
230 let ng: i64 = ab_gold_load(path)
231 out[AB_O_NG] = ng
232 var kk: i64 = k
233 if kk <= 0 { kk = ng }
234 out[AB_O_K] = kk
235 if ng <= 0 { return 0 }
236 let lp: *i64 = sys_mmap(RM_I64_PAIR) as *i64
237 lp[0] = 0
238 let b: *u8 = sys_read_file(path, lp)
239 if (b as i64) == 0 { return 0 }
240 let n: i64 = lp[0]
241 rm_vocab_reset()
242 let sw: i64 = rm_stopwords_load(AB_STOPWORDS)
243 let rbuf: *u8 = sys_mmap(AB_RECON_CAP)
244 var i: i64 = 0
245 var docid: i64 = 0
246 var more: i64 = 1
247 while more == 1 {
248 if i >= n { more = 0 } else {
249 let e0: i64 = rm_line_end(b, n, i)
250 let s0: i64 = i
251 let l0: i64 = e0 - i
252 i = e0 + 1
253 var l1: i64 = 0
254 var s1: i64 = i
255 if i < n {
256 let e1: i64 = rm_line_end(b, n, i)
257 s1 = i
258 l1 = e1 - i
259 i = e1 + 1
260 if i < n { let e2: i64 = rm_line_end(b, n, i); i = e2 + 1 } // skip the polarity line
261 }
262 docid = docid + 1
263 let rl: i64 = ab_reconstruct((b as i64 + s0) as *u8, l0, (b as i64 + s1) as *u8, l1, rbuf, AB_RECON_CAP)
264 if rl > 0 { rm_mine_doc(rbuf, rl, 1, docid) }
265 }
266 }
267 out[AB_O_VOCAB] = rm_vocab_size() - sw // content terms only; stopword slots do not count
268 // top-kk slots by frequency: every sentence was up=1, so rm_mine_rank in PRAISE mode ranks by document count
269 let slots: *i64 = sys_mmap((kk + 1) * RM_I64_BYTES) as *i64
270 let ns: i64 = rm_mine_rank(RM_MODE_PRAISE, kk, AB_MIN_SUPPORT, slots)
271 var inter: i64 = 0
272 var r: i64 = 0
273 while r < ns {
274 let tk: *u8 = rm_vocab_tok(slots[r])
275 let tl: i64 = rm_slen(tk)
276 if ab_set_has_raw(tk, tl) == 1 { inter = inter + 1 }
277 r = r + 1
278 }
279 out[AB_O_INTER] = inter
280 out[AB_O_NS] = ns
281 let prf: *i64 = sys_mmap(AB_PRF_N * RM_I64_BYTES) as *i64
282 ab_prf(inter, ns, ng, prf)
283 out[AB_O_P] = prf[AB_P]
284 out[AB_O_R] = prf[AB_R]
285 out[AB_O_F1] = prf[AB_F1]
286 return 0
287}
288
289// ---- subtask 2: aspect term polarity (SB2), scored as ACCURACY over the gold aspect terms, two arms: the MAJORITY
290// class (the paper's own majority baseline, so the arm is comparable in kind) and the estate's LEXICON arm -- the
291// DEFECT and NOT-MET cue lexicons vote negative, the EXCEED lexicon votes positive, a tie or no cue is neutral; the
292// cues are read through the ONE tokenizer and the lexicon bits rm_lexicon_load sets on the vocab, so no second
293// lexicon reader exists. A gold polarity the .seg does not define is OTHER and is never counted correct.
294const AB_POL_POS: i64 = 0
295const AB_POL_NEG: i64 = 1
296const AB_POL_NEU: i64 = 2
297const AB_POL_OTHER: i64 = 3
298const AB_POL_N: i64 = 4
299const AB_Q_RECORDS: i64 = 0
300const AB_Q_POS: i64 = 1
301const AB_Q_NEG: i64 = 2
302const AB_Q_NEU: i64 = 3
303const AB_Q_OTHER: i64 = 4
304const AB_Q_MAJ_CLASS: i64 = 5
305const AB_Q_MAJ_ACC: i64 = 6
306const AB_Q_LEX_ACC: i64 = 7
307const AB_Q_LEX_PRED_POS: i64 = 8
308const AB_Q_LEX_PRED_NEG: i64 = 9
309const AB_Q_LEX_PRED_NEU: i64 = 10
310const AB_Q_LEX_TERMS: i64 = 11 // lexicon terms loaded; 0 means the lexicon arm had no cues and says so
311const AB_Q_N: i64 = 12
312const AB_LEX_DIR_DEFAULT: *u8 = "knowledge/reviewmine/"
313const AB_LEX_DEFECT_FILE: *u8 = "lexicon_defect.conf"
314const AB_LEX_NOTMEET_FILE: *u8 = "lexicon_notmeet.conf"
315const AB_LEX_EXCEED_FILE: *u8 = "lexicon_exceed.conf"
316const AB_PATH_CAP: i64 = 512
317const AB_CH_ONE: i64 = 49 // '1'
318const AB_POL_LEN_ONE: i64 = 1 // "1" and "0" are one byte
319const AB_POL_LEN_NEG: i64 = 2 // "-1" is two bytes
320
321func ab_pol_class(p: *u8, n: i64) -> i64 {
322 var m: i64 = n
323 if m > 0 { if (p[m - 1] as i64) == RM_CR { m = m - 1 } }
324 if m == AB_POL_LEN_ONE {
325 if (p[0] as i64) == AB_CH_ONE { return AB_POL_POS }
326 if (p[0] as i64) == RM_ZERO { return AB_POL_NEU }
327 return AB_POL_OTHER
328 }
329 if m == AB_POL_LEN_NEG { if (p[0] as i64) == RM_MINUS { if (p[1] as i64) == AB_CH_ONE { return AB_POL_NEG } } }
330 return AB_POL_OTHER
331}
332func ab_pol_name(c: i64) -> *u8 {
333 if c == AB_POL_POS { return "positive" as *u8 }
334 if c == AB_POL_NEG { return "negative" as *u8 }
335 if c == AB_POL_NEU { return "neutral" as *u8 }
336 return "other" as *u8
337}
338func ab_lex_path(dir: *u8, file: *u8, out: *u8) -> i64 {
339 var o: i64 = rm_cat(out, 0, dir)
340 o = rm_cat(out, o, file)
341 out[o] = 0 as u8
342 return o
343}
344// cues[0] = exceed cues, cues[1] = defect-or-not-met cues in one reconstructed sentence
345func ab_lex_cues(text: *u8, n: i64, tok: *u8, ip: *i64, cues: *i64) -> i64 {
346 cues[0] = 0
347 cues[1] = 0
348 let negmask: i64 = RM_LEX_DEFECT + RM_LEX_NOTMEET
349 ip[0] = 0
350 var tl: i64 = rm_next_token(text, n, ip, tok)
351 while tl > 0 {
352 let s: i64 = rm_vocab_slot(tok, tl)
353 if s >= 0 {
354 let lex: i64 = rm_vocab_lex(s)
355 if (lex & negmask) != 0 { cues[1] = cues[1] + 1 }
356 if (lex & RM_LEX_EXCEED) != 0 { cues[0] = cues[0] + 1 }
357 }
358 tl = rm_next_token(text, n, ip, tok)
359 }
360 return 0
361}
362func ab_polarity(path: *u8, lexdir: *u8, out: *i64) -> i64 {
363 var q: i64 = 0
364 while q < AB_Q_N { out[q] = 0; q = q + 1 }
365 let lp: *i64 = sys_mmap(RM_I64_PAIR) as *i64
366 lp[0] = 0
367 let b: *u8 = sys_read_file(path, lp)
368 if (b as i64) == 0 { return 0 }
369 let n: i64 = lp[0]
370 rm_vocab_reset()
371 let pb: *u8 = sys_mmap(AB_PATH_CAP)
372 var terms: i64 = 0
373 ab_lex_path(lexdir, AB_LEX_DEFECT_FILE, pb)
374 terms = terms + rm_lexicon_load(pb, RM_LEX_DEFECT)
375 ab_lex_path(lexdir, AB_LEX_NOTMEET_FILE, pb)
376 terms = terms + rm_lexicon_load(pb, RM_LEX_NOTMEET)
377 ab_lex_path(lexdir, AB_LEX_EXCEED_FILE, pb)
378 terms = terms + rm_lexicon_load(pb, RM_LEX_EXCEED)
379 out[AB_Q_LEX_TERMS] = terms
380 let rbuf: *u8 = sys_mmap(AB_RECON_CAP)
381 let tok: *u8 = sys_mmap(RM_TOK_MAX + RM_TOK_BUF_SPARE)
382 let ip: *i64 = sys_mmap(RM_I64_PAIR) as *i64
383 let cues: *i64 = sys_mmap(RM_I64_PAIR) as *i64
384 let cls: *i64 = sys_mmap(AB_POL_N * RM_I64_BYTES) as *i64
385 var k: i64 = 0
386 while k < AB_POL_N { cls[k] = 0; k = k + 1 }
387 var correct: i64 = 0
388 var i: i64 = 0
389 var more: i64 = 1
390 while more == 1 {
391 if i >= n { more = 0 } else {
392 let e0: i64 = rm_line_end(b, n, i)
393 let s0: i64 = i
394 let l0: i64 = e0 - i
395 i = e0 + 1
396 var s1: i64 = i
397 var l1: i64 = 0
398 var s2: i64 = i
399 var l2: i64 = 0
400 if i < n {
401 let e1: i64 = rm_line_end(b, n, i)
402 s1 = i
403 l1 = e1 - i
404 i = e1 + 1
405 if i < n { let e2: i64 = rm_line_end(b, n, i); s2 = i; l2 = e2 - i; i = e2 + 1 }
406 }
407 let g: i64 = ab_pol_class((b as i64 + s2) as *u8, l2)
408 cls[g] = cls[g] + 1
409 let rl: i64 = ab_reconstruct((b as i64 + s0) as *u8, l0, (b as i64 + s1) as *u8, l1, rbuf, AB_RECON_CAP)
410 ab_lex_cues(rbuf, rl, tok, ip, cues)
411 var pred: i64 = AB_POL_NEU
412 if cues[0] > cues[1] { pred = AB_POL_POS }
413 if cues[1] > cues[0] { pred = AB_POL_NEG }
414 if pred == AB_POL_POS { out[AB_Q_LEX_PRED_POS] = out[AB_Q_LEX_PRED_POS] + 1 }
415 if pred == AB_POL_NEG { out[AB_Q_LEX_PRED_NEG] = out[AB_Q_LEX_PRED_NEG] + 1 }
416 if pred == AB_POL_NEU { out[AB_Q_LEX_PRED_NEU] = out[AB_Q_LEX_PRED_NEU] + 1 }
417 if pred == g { correct = correct + 1 }
418 }
419 }
420 let records: i64 = cls[AB_POL_POS] + cls[AB_POL_NEG] + cls[AB_POL_NEU] + cls[AB_POL_OTHER]
421 out[AB_Q_RECORDS] = records
422 out[AB_Q_POS] = cls[AB_POL_POS]
423 out[AB_Q_NEG] = cls[AB_POL_NEG]
424 out[AB_Q_NEU] = cls[AB_POL_NEU]
425 out[AB_Q_OTHER] = cls[AB_POL_OTHER]
426 var maj: i64 = AB_POL_POS
427 if cls[AB_POL_NEG] > cls[maj] { maj = AB_POL_NEG }
428 if cls[AB_POL_NEU] > cls[maj] { maj = AB_POL_NEU }
429 out[AB_Q_MAJ_CLASS] = maj
430 if records > 0 {
431 out[AB_Q_MAJ_ACC] = cls[maj] * AB_PERMIL / records
432 out[AB_Q_LEX_ACC] = correct * AB_PERMIL / records
433 }
434 return 0
435}
436
437// ---- rung rm_aspects, first cut: FREQUENT N-GRAM CANDIDATES with Hu and Liu (2004) redundancy pruning ----
438// Candidates are adjacent token pairs (bigrams) beside unigrams, counted once per record over the reconstructed
439// sentences through the ONE tokenizer and stored in the ONE vocab (a bigram is the string "left right" in the same
440// arena, so no second hash exists); a stopword ends a phrase. Redundancy pruning is Hu and Liu's p-support: a
441// unigram that sits inside a frequent bigram and rarely occurs on its own is a fragment, not an aspect. Minimum
442// support is derived from the corpus (Hu and Liu: one percent of sentences) unless the caller pins it. The unigram
443// arm above stays the runnable incumbent; this arm prints beside it, never instead of it.
444const AB_NG_MINSUP_PERMIL: i64 = 10 // Hu and Liu 2004: a frequent feature appears in at least 1 percent of sentences
445const AB_NG_MINSUP_FLOOR: i64 = 1
446const AB_NG_MIN_PSUPPORT: i64 = 3 // Hu and Liu 2004: a subset phrase with p-support below 3 is redundant
447const AB_NG_JOIN: i64 = 32 // the space between the two words of a stored bigram
448const AB_NG_MAX_TOKENS: i64 = 512 // tokens one record carries into the n-gram pass; more is counted truncated
449const AB_NG_BG_CAP: i64 = 66 // RM_TOK_MAX * 2 + a space + a NUL
450const AB_NG_STOP: i64 = 0 - 1 // a stopword in the token list: a phrase boundary
451const AB_REC_N: i64 = 6 // s0 l0 s1 l1 s2 l2
452const AB_G_INTER: i64 = 0
453const AB_G_NS: i64 = 1
454const AB_G_NG: i64 = 2
455const AB_G_P: i64 = 3
456const AB_G_R: i64 = 4
457const AB_G_F1: i64 = 5
458const AB_G_K: i64 = 6
459const AB_G_CAND: i64 = 7 // candidates after support and pruning
460const AB_G_BIGRAMS: i64 = 8 // frequent bigrams among the candidates
461const AB_G_PRUNED: i64 = 9 // unigrams pruned by p-support
462const AB_G_MINSUP: i64 = 10
463const AB_G_MATCHED_BIGRAMS: i64 = 11
464const AB_G_TRUNC: i64 = 12 // records whose token list overflowed
465const AB_G_RECORDS: i64 = 13
466const AB_G_N: i64 = 14
467
468static ab_df: *i64
469static ab_last: *i64
470static ab_ps: *i64
471static ab_pslast: *i64
472static ab_isbig: *i64
473static ab_sel: *i64
474static ab_inbig: *i64
475static ab_toks: *i64
476static ab_cov: *i64
477static ab_bg: *u8
478
479func ab_ng_reset() -> i64 {
480 if (ab_df as i64) == 0 {
481 ab_df = sys_mmap(RM_VOCAB_SLOTS * RM_I64_BYTES) as *i64
482 ab_last = sys_mmap(RM_VOCAB_SLOTS * RM_I64_BYTES) as *i64
483 ab_ps = sys_mmap(RM_VOCAB_SLOTS * RM_I64_BYTES) as *i64
484 ab_pslast = sys_mmap(RM_VOCAB_SLOTS * RM_I64_BYTES) as *i64
485 ab_isbig = sys_mmap(RM_VOCAB_SLOTS * RM_I64_BYTES) as *i64
486 ab_sel = sys_mmap(RM_VOCAB_SLOTS * RM_I64_BYTES) as *i64
487 ab_inbig = sys_mmap(RM_VOCAB_SLOTS * RM_I64_BYTES) as *i64
488 ab_toks = sys_mmap(AB_NG_MAX_TOKENS * RM_I64_BYTES) as *i64
489 ab_cov = sys_mmap(AB_NG_MAX_TOKENS * RM_I64_BYTES) as *i64
490 ab_bg = sys_mmap(AB_NG_BG_CAP)
491 } else {
492 var i: i64 = 0
493 while i < RM_VOCAB_SLOTS { ab_df[i] = 0; ab_last[i] = 0; ab_ps[i] = 0; ab_pslast[i] = 0; ab_isbig[i] = 0; ab_sel[i] = 0; ab_inbig[i] = 0; i = i + 1 }
494 }
495 return 0
496}
497// one record's tokens as vocab slots into ab_toks (a stopword becomes AB_NG_STOP); returns the count, trunc[0]=1 on overflow
498func ab_ng_tokens(text: *u8, n: i64, tok: *u8, ip: *i64, trunc: *i64) -> i64 {
499 ip[0] = 0
500 var cnt: i64 = 0
501 var tl: i64 = rm_next_token(text, n, ip, tok)
502 while tl > 0 {
503 if cnt < AB_NG_MAX_TOKENS {
504 let s: i64 = rm_vocab_slot(tok, tl)
505 var v: i64 = AB_NG_STOP
506 if s >= 0 { if rm_vocab_stop(s) == 0 { v = s } }
507 ab_toks[cnt] = v
508 cnt = cnt + 1
509 } else { trunc[0] = 1 }
510 tl = rm_next_token(text, n, ip, tok)
511 }
512 return cnt
513}
514// the vocab slot of the bigram "left right" (inserted when absent), -1 when the vocab is full
515func ab_ng_bigram(left: i64, right: i64) -> i64 {
516 var o: i64 = rm_cat(ab_bg, 0, rm_vocab_tok(left))
517 ab_bg[o] = AB_NG_JOIN as u8
518 o = o + 1
519 o = rm_cat(ab_bg, o, rm_vocab_tok(right))
520 ab_bg[o] = 0 as u8
521 return rm_vocab_slot(ab_bg, o)
522}
523func ab_ng_bump(slot: i64, doc: i64) -> i64 { if ab_last[slot] != doc { ab_last[slot] = doc; ab_df[slot] = ab_df[slot] + 1 } return 0 }
524func ab_ng_psbump(slot: i64, doc: i64) -> i64 { if ab_pslast[slot] != doc { ab_pslast[slot] = doc; ab_ps[slot] = ab_ps[slot] + 1 } return 0 }
525// walk one three-line record from ip[0]; r = s0 l0 s1 l1 s2 l2; returns 1, or 0 at the end
526func ab_rec_next(b: *u8, n: i64, ip: *i64, r: *i64) -> i64 {
527 var i: i64 = ip[0]
528 if i >= n { return 0 }
529 let e0: i64 = rm_line_end(b, n, i)
530 r[0] = i; r[1] = e0 - i; i = e0 + 1
531 r[2] = i; r[3] = 0; r[4] = i; r[5] = 0
532 if i < n {
533 let e1: i64 = rm_line_end(b, n, i)
534 r[2] = i; r[3] = e1 - i; i = e1 + 1
535 if i < n { let e2: i64 = rm_line_end(b, n, i); r[4] = i; r[5] = e2 - i; i = e2 + 1 }
536 }
537 ip[0] = i
538 return 1
539}
540// the frequent n-gram arm: k<=0 -> K=|G|; minsup_pin<=0 -> derived from the record count. out is AB_G_N wide.
541func ab_ngram_eval(path: *u8, k: i64, minsup_pin: i64, out: *i64) -> i64 {
542 var q: i64 = 0
543 while q < AB_G_N { out[q] = 0; q = q + 1 }
544 let ng: i64 = ab_gold_load(path)
545 out[AB_G_NG] = ng
546 var kk: i64 = k
547 if kk <= 0 { kk = ng }
548 out[AB_G_K] = kk
549 if ng <= 0 { return 0 }
550 let lp: *i64 = sys_mmap(RM_I64_PAIR) as *i64
551 lp[0] = 0
552 let b: *u8 = sys_read_file(path, lp)
553 if (b as i64) == 0 { return 0 }
554 let n: i64 = lp[0]
555 rm_vocab_reset()
556 rm_stopwords_load(AB_STOPWORDS)
557 ab_ng_reset()
558 let rbuf: *u8 = sys_mmap(AB_RECON_CAP)
559 let tok: *u8 = sys_mmap(RM_TOK_MAX + RM_TOK_BUF_SPARE)
560 let ip: *i64 = sys_mmap(RM_I64_PAIR) as *i64
561 let rp: *i64 = sys_mmap(RM_I64_PAIR) as *i64
562 let r: *i64 = sys_mmap(AB_REC_N * RM_I64_BYTES) as *i64
563 let trunc: *i64 = sys_mmap(RM_I64_PAIR) as *i64
564 rp[0] = 0
565 var doc: i64 = 0
566 while ab_rec_next(b, n, rp, r) == 1 {
567 doc = doc + 1
568 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)
569 trunc[0] = 0
570 let cnt: i64 = ab_ng_tokens(rbuf, rl, tok, ip, trunc)
571 if trunc[0] == 1 { out[AB_G_TRUNC] = out[AB_G_TRUNC] + 1 }
572 var i: i64 = 0
573 while i < cnt {
574 let s: i64 = ab_toks[i]
575 if s >= 0 {
576 ab_ng_bump(s, doc)
577 if i + 1 < cnt { let t: i64 = ab_toks[i + 1]; if t >= 0 {
578 let bs: i64 = ab_ng_bigram(s, t)
579 if bs >= 0 { ab_isbig[bs] = 1; ab_ng_bump(bs, doc) }
580 } }
581 }
582 i = i + 1
583 }
584 }
585 out[AB_G_RECORDS] = doc
586 var minsup: i64 = minsup_pin
587 if minsup <= 0 { minsup = doc * AB_NG_MINSUP_PERMIL / AB_PERMIL; if minsup < AB_NG_MINSUP_FLOOR { minsup = AB_NG_MINSUP_FLOOR } }
588 out[AB_G_MINSUP] = minsup
589 var s2: i64 = 0
590 var nbig: i64 = 0
591 while s2 < RM_VOCAB_SLOTS { if ab_isbig[s2] == 1 { if ab_df[s2] >= minsup { ab_sel[s2] = 1; nbig = nbig + 1 } } s2 = s2 + 1 }
592 out[AB_G_BIGRAMS] = nbig
593 rp[0] = 0
594 doc = 0
595 while ab_rec_next(b, n, rp, r) == 1 {
596 doc = doc + 1
597 let rl2: 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)
598 trunc[0] = 0
599 let cnt2: i64 = ab_ng_tokens(rbuf, rl2, tok, ip, trunc)
600 var j: i64 = 0
601 while j < cnt2 { ab_cov[j] = 0; j = j + 1 }
602 j = 0
603 while j + 1 < cnt2 {
604 let s: i64 = ab_toks[j]
605 let t: i64 = ab_toks[j + 1]
606 if s >= 0 { if t >= 0 { let bs: i64 = ab_ng_bigram(s, t); if bs >= 0 { if ab_sel[bs] == 1 { ab_cov[j] = 1; ab_cov[j + 1] = 1; ab_inbig[s] = 1; ab_inbig[t] = 1 } } } }
607 j = j + 1
608 }
609 j = 0
610 while j < cnt2 { let s: i64 = ab_toks[j]; if s >= 0 { if ab_cov[j] == 0 { ab_ng_psbump(s, doc) } } j = j + 1 }
611 }
612 let cand: *i64 = sys_mmap((rm_vocab_size() + 1) * RM_I64_BYTES) as *i64
613 var nc: i64 = 0
614 var pruned: i64 = 0
615 var s3: i64 = 0
616 while s3 < RM_VOCAB_SLOTS {
617 if ab_df[s3] >= minsup {
618 if ab_isbig[s3] == 1 { cand[nc] = s3; nc = nc + 1 }
619 else {
620 if ab_inbig[s3] == 0 { cand[nc] = s3; nc = nc + 1 }
621 else { if ab_ps[s3] >= AB_NG_MIN_PSUPPORT { cand[nc] = s3; nc = nc + 1 } else { pruned = pruned + 1 } }
622 }
623 }
624 s3 = s3 + 1
625 }
626 out[AB_G_CAND] = nc
627 out[AB_G_PRUNED] = pruned
628 var inter: i64 = 0
629 var mbig: i64 = 0
630 var ns: i64 = 0
631 var round: i64 = 0
632 while round < kk {
633 var best: i64 = 0 - 1
634 var bi: i64 = 0 - 1
635 var cc: i64 = 0
636 while cc < nc {
637 let s: i64 = cand[cc]
638 if s >= 0 {
639 var better: i64 = 0
640 if best < 0 { better = 1 }
641 else {
642 if ab_df[s] > ab_df[best] { better = 1 }
643 if ab_df[s] == ab_df[best] { if ab_isbig[s] > ab_isbig[best] { better = 1 } }
644 }
645 if better == 1 { best = s; bi = cc }
646 }
647 cc = cc + 1
648 }
649 if best < 0 { round = kk } else {
650 cand[bi] = 0 - 1
651 ns = ns + 1
652 let tk: *u8 = rm_vocab_tok(best)
653 if ab_set_has_raw(tk, rm_slen(tk)) == 1 { inter = inter + 1; if ab_isbig[best] == 1 { mbig = mbig + 1 } }
654 round = round + 1
655 }
656 }
657 out[AB_G_INTER] = inter
658 out[AB_G_NS] = ns
659 out[AB_G_MATCHED_BIGRAMS] = mbig
660 let prf: *i64 = sys_mmap(AB_PRF_N * RM_I64_BYTES) as *i64
661 ab_prf(inter, ns, ng, prf)
662 out[AB_G_P] = prf[AB_P]
663 out[AB_G_R] = prf[AB_R]
664 out[AB_G_F1] = prf[AB_F1]
665 return 0
666}
667
668// ---- rung IM28 first cut: the SUPERVISED training-DICTIONARY tagger (the organisers' own SB1 baseline) ----
669const HS_HASH: i64 = 0
670const HS_OFF: i64 = 1
671const HS_LEN: i64 = 2
672const HS_ARENA: i64 = 3
673const HS_USED: i64 = 4
674const HS_CNT: i64 = 5
675const HS_FIELDS: i64 = 6
676const AB_GRAM_CAP: i64 = 512
677const AB_D_INTER: i64 = 0
678const AB_D_NS: i64 = 1
679const AB_D_NG: i64 = 2
680const AB_D_P: i64 = 3
681const AB_D_R: i64 = 4
682const AB_D_F1: i64 = 5
683const AB_D_DICT: i64 = 6
684const AB_D_RECORDS: i64 = 7
685const AB_D_N: i64 = 8
686
687func hs_new() -> *i64 {
688 let h: *i64 = sys_mmap(HS_FIELDS * RM_I64_BYTES) as *i64
689 h[HS_HASH] = sys_mmap(AB_SET_SLOTS * RM_I64_BYTES) as i64
690 h[HS_OFF] = sys_mmap(AB_SET_SLOTS * RM_I64_BYTES) as i64
691 h[HS_LEN] = sys_mmap(AB_SET_SLOTS * RM_I64_BYTES) as i64
692 h[HS_ARENA] = sys_mmap(AB_SET_ARENA) as i64
693 h[HS_USED] = 0
694 h[HS_CNT] = 0
695 return h
696}
697func hs_reset(h: *i64) -> i64 {
698 let hash: *i64 = h[HS_HASH] as *i64
699 var i: i64 = 0
700 while i < AB_SET_SLOTS { hash[i] = 0; i = i + 1 }
701 h[HS_USED] = 0
702 h[HS_CNT] = 0
703 return 0
704}
705func hs_add_norm(h: *i64, s: *u8, n: i64) -> i64 {
706 if n <= 0 { return 0 }
707 let hash: *i64 = h[HS_HASH] as *i64
708 let off: *i64 = h[HS_OFF] as *i64
709 let len: *i64 = h[HS_LEN] as *i64
710 let arena: *u8 = h[HS_ARENA] as *u8
711 let hh: i64 = rm_hash(s, n)
712 var slot: i64 = hh % AB_SET_SLOTS
713 var tries: i64 = 0
714 while tries < AB_SET_SLOTS {
715 if hash[slot] == 0 {
716 if h[HS_USED] + n + AB_ARENA_TAIL > AB_SET_ARENA { return 0 }
717 hash[slot] = hh
718 off[slot] = h[HS_USED]
719 len[slot] = n
720 rm_catn(arena, h[HS_USED], s, n)
721 arena[h[HS_USED] + n] = 0 as u8
722 h[HS_USED] = h[HS_USED] + n + AB_ARENA_TAIL
723 h[HS_CNT] = h[HS_CNT] + 1
724 return 1
725 }
726 if hash[slot] == hh { if len[slot] == n {
727 var same: i64 = 1
728 var j: i64 = 0
729 while j < n { if arena[off[slot] + j] != s[j] { same = 0 } j = j + 1 }
730 if same == 1 { return 0 }
731 } }
732 slot = (slot + 1) % AB_SET_SLOTS
733 tries = tries + 1
734 }
735 return 0
736}
737func hs_add_raw(h: *i64, s: *u8, n: i64) -> i64 { let m: i64 = ab_norm(s, n, ab_nbuf, AB_TERM_MAX); return hs_add_norm(h, ab_nbuf, m) }
738func hs_has_raw(h: *i64, s: *u8, n: i64) -> i64 {
739 let m: i64 = ab_norm(s, n, ab_nbuf, AB_TERM_MAX)
740 if m <= 0 { return 0 }
741 let hash: *i64 = h[HS_HASH] as *i64
742 let off: *i64 = h[HS_OFF] as *i64
743 let len: *i64 = h[HS_LEN] as *i64
744 let arena: *u8 = h[HS_ARENA] as *u8
745 let hh: i64 = rm_hash(ab_nbuf, m)
746 var slot: i64 = hh % AB_SET_SLOTS
747 var tries: i64 = 0
748 while tries < AB_SET_SLOTS {
749 if hash[slot] == 0 { return 0 }
750 if hash[slot] == hh { if len[slot] == m {
751 var same: i64 = 1
752 var j: i64 = 0
753 while j < m { if arena[off[slot] + j] != ab_nbuf[j] { same = 0 } j = j + 1 }
754 if same == 1 { return 1 }
755 } }
756 slot = (slot + 1) % AB_SET_SLOTS
757 tries = tries + 1
758 }
759 return 0
760}
761func hs_count(h: *i64) -> i64 { return h[HS_CNT] }
762// the SLOT a term occupies (or -1): lets a caller keep per-term counters in arrays parallel to the set
763func hs_slot_raw(h: *i64, s: *u8, n: i64) -> i64 {
764 let m: i64 = ab_norm(s, n, ab_nbuf, AB_TERM_MAX)
765 if m <= 0 { return 0 - 1 }
766 let hash: *i64 = h[HS_HASH] as *i64
767 let off: *i64 = h[HS_OFF] as *i64
768 let len: *i64 = h[HS_LEN] as *i64
769 let arena: *u8 = h[HS_ARENA] as *u8
770 let hh: i64 = rm_hash(ab_nbuf, m)
771 var slot: i64 = hh % AB_SET_SLOTS
772 var tries: i64 = 0
773 while tries < AB_SET_SLOTS {
774 if hash[slot] == 0 { return 0 - 1 }
775 if hash[slot] == hh { if len[slot] == m {
776 var same: i64 = 1
777 var j: i64 = 0
778 while j < m { if arena[off[slot] + j] != ab_nbuf[j] { same = 0 } j = j + 1 }
779 if same == 1 { return slot }
780 } }
781 slot = (slot + 1) % AB_SET_SLOTS
782 tries = tries + 1
783 }
784 return 0 - 1
785}
786func ab_dict_load(path: *u8, h: *i64) -> i64 {
787 hs_reset(h)
788 let lp: *i64 = sys_mmap(RM_I64_PAIR) as *i64
789 lp[0] = 0
790 let b: *u8 = sys_read_file(path, lp)
791 if (b as i64) == 0 { return 0 }
792 let n: i64 = lp[0]
793 var i: i64 = 0
794 var lineno: i64 = 0
795 while i < n {
796 let e: i64 = rm_line_end(b, n, i)
797 if (lineno % AB_RECORD_LINES) == AB_TERM_LINE { if e > i { hs_add_raw(h, (b as i64 + i) as *u8, e - i) } }
798 i = e + 1
799 lineno = lineno + 1
800 }
801 return hs_count(h)
802}
803func ab_dict_try(dic: *i64, pred: *i64, g: *u8, n: i64) -> i64 {
804 if hs_has_raw(dic, g, n) == 1 { if hs_has_raw(pred, g, n) == 0 { hs_add_raw(pred, g, n) } }
805 return 0
806}
807func ab_dict_eval(train: *u8, test: *u8, out: *i64) -> i64 {
808 var q: i64 = 0
809 while q < AB_D_N { out[q] = 0; q = q + 1 }
810 let ng: i64 = ab_gold_load(test)
811 out[AB_D_NG] = ng
812 if ng <= 0 { return 0 }
813 let dic: *i64 = hs_new()
814 out[AB_D_DICT] = ab_dict_load(train, dic)
815 let pred: *i64 = hs_new()
816 hs_reset(pred)
817 let lp: *i64 = sys_mmap(RM_I64_PAIR) as *i64
818 lp[0] = 0
819 let b: *u8 = sys_read_file(test, lp)
820 if (b as i64) == 0 { return 0 }
821 let n: i64 = lp[0]
822 let rbuf: *u8 = sys_mmap(AB_RECON_CAP)
823 let tok: *u8 = sys_mmap(RM_TOK_MAX + RM_TOK_BUF_SPARE)
824 let ip: *i64 = sys_mmap(RM_I64_PAIR) as *i64
825 let p1: *u8 = sys_mmap(RM_TOK_MAX + RM_TOK_BUF_SPARE)
826 let p2: *u8 = sys_mmap(RM_TOK_MAX + RM_TOK_BUF_SPARE)
827 let gram: *u8 = sys_mmap(AB_GRAM_CAP)
828 let rp: *i64 = sys_mmap(RM_I64_PAIR) as *i64
829 let r: *i64 = sys_mmap(AB_REC_N * RM_I64_BYTES) as *i64
830 rp[0] = 0
831 var doc: i64 = 0
832 while ab_rec_next(b, n, rp, r) == 1 {
833 doc = doc + 1
834 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)
835 var l1: i64 = 0
836 var l2: i64 = 0
837 ip[0] = 0
838 var tl: i64 = rm_next_token(rbuf, rl, ip, tok)
839 while tl > 0 {
840 ab_dict_try(dic, pred, tok, tl)
841 if l1 > 0 {
842 var go: i64 = rm_catn(gram, 0, p1, l1)
843 gram[go] = AB_NG_JOIN as u8; go = go + 1
844 go = rm_catn(gram, go, tok, tl)
845 ab_dict_try(dic, pred, gram, go)
846 if l2 > 0 {
847 var to: i64 = rm_catn(gram, 0, p2, l2)
848 gram[to] = AB_NG_JOIN as u8; to = to + 1
849 to = rm_catn(gram, to, p1, l1)
850 gram[to] = AB_NG_JOIN as u8; to = to + 1
851 to = rm_catn(gram, to, tok, tl)
852 ab_dict_try(dic, pred, gram, to)
853 }
854 }
855 l2 = l1
856 var w2: i64 = 0
857 while w2 < l1 { p2[w2] = p1[w2]; w2 = w2 + 1 }
858 l1 = tl
859 var w1: i64 = 0
860 while w1 < tl { p1[w1] = tok[w1]; w1 = w1 + 1 }
861 tl = rm_next_token(rbuf, rl, ip, tok)
862 }
863 }
864 out[AB_D_RECORDS] = doc
865 out[AB_D_NS] = hs_count(pred)
866 let phash: *i64 = pred[HS_HASH] as *i64
867 let poff: *i64 = pred[HS_OFF] as *i64
868 let plen: *i64 = pred[HS_LEN] as *i64
869 let parena: *u8 = pred[HS_ARENA] as *u8
870 var inter: i64 = 0
871 var slot: i64 = 0
872 while slot < AB_SET_SLOTS {
873 if phash[slot] != 0 { if ab_set_has_raw((parena as i64 + poff[slot]) as *u8, plen[slot]) == 1 { inter = inter + 1 } }
874 slot = slot + 1
875 }
876 out[AB_D_INTER] = inter
877 let prf: *i64 = sys_mmap(AB_PRF_N * RM_I64_BYTES) as *i64
878 ab_prf(inter, hs_count(pred), ng, prf)
879 out[AB_D_P] = prf[AB_P]
880 out[AB_D_R] = prf[AB_R]
881 out[AB_D_F1] = prf[AB_F1]
882 return 0
883}