code wiki / (root) / nx_absa_lib.nx

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}