File size: 10,230 Bytes
843b037 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | """Compact Devanagari lexicon + pruned backoff word-bigram LM (BhashaCore Track B/C assets), format v2.
Word ids: words sorted by (unigram bucket = round(-2*ln count) i.e. frequency class, then alphabetical) so
ids are roughly frequency-ordered (small ids for frequent words -> small LM deltas), while the list is still
front-coded within each bucket. Unigram ln-prob is per bucket (implicit; no per-word bytes).
lex.bin : header | per-bucket (n_words varint, ln-prob f16) | front-coded list (1 byte/char = codepoint-0x0900,
last suffix byte has bit 7 set; 0x7F = empty suffix)
lm.bin : header | successor count per history (varint; history N = sentence start) | delta-coded successor
ids (varint) | 4-bit codebook ln p(w|v) (16 centroids, f32) | 4-bit codebook ln backoff for histories
with successors (16 centroids)
"""
import gzip, math, struct
import numpy as np
HK = 1 << 21 # in-memory bigram key = history * HK + successor (lexicons up to 2M words; file format unchanged)
def varints(vals):
out = bytearray()
for v in vals:
v = int(v)
while v >= 0x80: out.append((v & 0x7F) | 0x80); v >>= 7
out.append(v)
return bytes(out)
def read_varints(b, n, pos=0):
out = [0] * n; i = 0
while i < n:
v = sh = 0
while True:
c = b[pos]; pos += 1; v |= (c & 0x7F) << sh; sh += 7
if c < 0x80: break
out[i] = v; i += 1
return out, pos
def kmeans1d(x, k=16, it=30):
x = np.asarray(x, np.float64); c = np.quantile(x, (np.arange(k) + 0.5) / k)
for _ in range(it):
a = np.abs(x[:, None] - c[None]).argmin(1)
for j in range(k):
if (a == j).any(): c[j] = x[a == j].mean()
c = np.sort(c); a = np.abs(x[:, None] - c[None]).argmin(1) if len(x) < 2_000_000 else None
return c.astype(np.float32), a.astype(np.uint8)
def pack4(a):
a = np.r_[a, np.zeros(len(a) % 2, np.uint8)].astype(np.uint8); return (a[0::2] * 16 + a[1::2]).astype(np.uint8).tobytes()
def unpack4(b, n):
a = np.frombuffer(b, np.uint8); return np.stack([a >> 4, a & 15], 1).reshape(-1)[:n]
def build(words_by_freq, counts_by_freq, bigram_pairs, bigram_counts, n_lex, maxbi, D=0.75, cmin=2, K=262144, alpha=False):
"""words_by_freq[i] has count id i+2 in bigram_pairs (0=boundary, 1=unk)."""
N = n_lex; ws = words_by_freq[:N]; cs = counts_by_freq[:N]
bk = [-int(round(2 * math.log(c))) for c in cs]
if alpha: # format v4: one global alphabetical front-coded list + 16-level (4-bit) uniform log-frequency band per word
order = sorted(range(N), key=lambda i: ws[i])
else:
order = sorted(range(N), key=lambda i: (bk[i], ws[i]))
nid = np.empty(N + 2, np.int64); nid[0] = N; nid[1] = -1
for a, i in enumerate(order): nid[i + 2] = a
tot = float(sum(cs)) * 1.02 # ~2% mass reserved for OOV words
buckets = []
for i in order:
if not buckets or buckets[-1][0] != bk[i]: buckets.append([bk[i], 0, 0.0])
buckets[-1][1] += 1; buckets[-1][2] += cs[i]
b_lp = [math.log(s / n / tot) for _, n, s in buckets]
unk_lp = math.log(0.02 / 50000)
puni = np.empty(N + 2); puni[0] = math.log(0.04); puni[1] = unk_lp; j = 0
if alpha:
lc = np.log(np.array(cs, np.float64) / tot); NB = 16 # 16 uniform log-frequency bands (4 bits/word)
ucb = np.linspace(lc.min(), lc.max(), NB).astype(np.float32)
ua = np.abs(lc[:, None] - ucb[None].astype(np.float64)).argmin(1).astype(np.uint8)
puni[2:] = ucb[ua]
else:
for (_, n, _), lp in zip(buckets, b_lp):
for _ in range(n): puni[[order[j] + 2]] = lp; j += 1
v = bigram_pairs // K; w = bigram_pairs % K; c = bigram_counts.astype(np.float64)
inlex = (v - 2 < N) & (w - 2 < N)
hist_tot = np.bincount(np.where(v - 2 < N, v, 1), weights=c, minlength=N + 2)
keep = inlex & (v != 1) & (w != 1) & (c >= cmin)
pk_all = np.log(np.maximum(c - D, 0.1) / np.maximum(hist_tot[np.minimum(v, N + 1)], 1))
gain = np.where(keep, c * (pk_all - puni[np.minimum(w, N + 1)]), -np.inf)
if keep.sum() > maxbi:
thr = np.partition(gain, len(gain) - maxbi)[len(gain) - maxbi]; keep &= gain >= thr
kv, kw, pk = v[keep], w[keep], pk_all[keep]
av = nid[kv]; aw = nid[kw] # history boundary 0 -> N ; successor boundary 0 -> N (= </s>)
o = np.lexsort((aw, av)); av, aw, pk = av[o], aw[o], pk[o]
cb, qa = kmeans1d(pk); pkd = cb[qa]
cnt = np.bincount(av, minlength=N + 1)
first = np.r_[True, av[1:] != av[:-1]]; delta = np.where(first, aw, aw - np.r_[0, aw[:-1]] - 1)
# backoff for histories with successors (dequantised probs; unigram of successors in new-id space)
pu_new = np.empty(N + 1); pu_new[nid[np.arange(2, N + 2)]] = puni[2:]; pu_new[N] = puni[0]
s_bi = np.bincount(av, weights=np.exp(pkd), minlength=N + 1); s_un = np.bincount(av, weights=np.exp(pu_new[aw]), minlength=N + 1)
hs = np.nonzero(cnt)[0]
bow = np.log(np.maximum(1 - s_bi[hs], 1e-4) / np.maximum(1 - s_un[hs], 1e-4))
cbb, qb = kmeans1d(bow)
fc = bytearray(); prev = b""
for i in order:
bb = bytes(ord(ch) - 0x0900 for ch in ws[i]); k = 0
while k < min(len(bb), len(prev), 127) and bb[k] == prev[k]: k += 1
suf = bytearray(bb[k:]) or bytearray([0x7F]); suf[-1] |= 0x80; fc.append(k); fc += suf; prev = bb
bh = varints([n for _, n, _ in buckets]) + np.array(b_lp, np.float16).tobytes()
if alpha: # v4: prefix-length stream and suffix stream stored separately (compresses ~8% better)
P = bytearray(); S = bytearray(); prev = b""
for i in order:
bb = bytes(ord(ch) - 0x0900 for ch in ws[i]); k = 0
while k < min(len(bb), len(prev), 127) and bb[k] == prev[k]: k += 1
suf = bytearray(bb[k:]) or bytearray([0x7F]); suf[-1] |= 0x80; P.append(k); S += suf; prev = bb
lex = b"LX4\0" + struct.pack("<IIfI", N, len(S), unk_lp, NB) + ucb.astype(np.float32).tobytes() + pack4(ua[order]) + bytes(P) + bytes(S)
else:
lex = struct.pack("<IIIf", N, len(buckets), len(fc), unk_lp) + bh + bytes(fc)
b1, b2 = varints(cnt), varints(delta)
lm = (struct.pack("<IIIII", N, len(av), len(b1), len(b2), len(hs)) + cb.tobytes() + cbb.tobytes() + b1 + b2
+ pack4(qa) + pack4(qb))
return lex, lm, {"lex_words": N, "bigrams": int(len(av)), "histories": int(len(hs)), "buckets": len(buckets)}
class LexLM:
def __init__(self, lex_path, lm_path=None):
if lex_path is None: # core-only (no dictionary pack): empty lexicon, no LM
self.words = []; self.N = 0; self.id = {}; self.uni = []; self.unk = 0.0; self.bi = {}; self.bow = {}; return
b = open(lex_path, "rb").read()
if b[:4] == b"LX4\0":
N, nfc, self.unk, NB = struct.unpack("<IIfI", b[4:20]); pos = 20; ucb = np.frombuffer(b, np.float32, NB, pos); pos += 4 * NB
band = unpack4(b[pos:pos + (N + 1) // 2], N); pos += (N + 1) // 2; uni = ucb[band].tolist(); bn = None
P = b[pos:pos + N]; S = b[pos + N:pos + N + nfc]; words = []; prev = []; j = 0
for k in P:
cur = prev[:k]
while True:
c = S[j]; j += 1; ch = c & 0x7F
if ch != 0x7F: cur.append(ch)
if c & 0x80: break
words.append("".join(chr(0x0900 + x) for x in cur)); prev = cur
pos = end = 0
else:
N, nb, nfc, self.unk = struct.unpack("<IIIf", b[:16]); pos = 16
bn, pos = read_varints(b, nb, pos); blp = np.frombuffer(b, np.float16, nb, pos).astype(np.float32).tolist(); pos += 2 * nb
if bn is not None: words = []; prev = []; end = pos + nfc
while pos < end:
k = b[pos]; pos += 1; cur = prev[:k]
while True:
c = b[pos]; pos += 1; ch = c & 0x7F
if ch != 0x7F: cur.append(ch)
if c & 0x80: break
words.append("".join(chr(0x0900 + x) for x in cur)); prev = cur
self.words = words; self.N = N; self.id = {w: i for i, w in enumerate(words)}
self.uni = uni if bn is None else [lp for n, lp in zip(bn, blp) for _ in range(n)]
self.bi = {}; self.bow = {}
if lm_path:
b = open(lm_path, "rb").read(); N2, nbg, l1, l2, nh = struct.unpack("<IIIII", b[:20]); pos = 20
cb = np.frombuffer(b, np.float32, 16, pos); pos += 64; cbb = np.frombuffer(b, np.float32, 16, pos); pos += 64
cnt, pos = read_varints(b, N + 1, pos); delta, pos = read_varints(b, nbg, pos)
q = cb[unpack4(b[pos:pos + (nbg + 1) // 2], nbg)].tolist(); pos += (nbg + 1) // 2
qbw = cbb[unpack4(b[pos:pos + (nh + 1) // 2], nh)].tolist()
i = 0; hi = 0
for h, n in enumerate(cnt):
if not n: continue
self.bow[h] = qbw[hi]; hi += 1; acc = -1
for _ in range(n):
acc = delta[i] if acc < 0 else acc + delta[i] + 1
self.bi[h * HK + acc] = q[i]; i += 1
def unigram(self, wid):
return self.unk if wid is None else (math.log(0.04) if wid == self.N else self.uni[wid])
def logp(self, prev, wid):
"""ln P(wid | prev). prev None = sentence start; wid None = OOV word; wid N = </s>."""
h = self.N if prev is None else prev
if wid is not None:
p = self.bi.get(h * HK + wid)
if p is not None: return p
return self.bow.get(h, 0.0) + self.unigram(wid)
def write_assets(out_dir, n_lex, maxbi):
import os
root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
rows = [l.rstrip("\n").split("\t") for l in open(f"{root}/data/processed/trackB/hi_wordfreq.tsv", encoding="utf-8")]
d = np.load(f"{root}/data/processed/trackB/lm/bigrams.npz")
lex, lm, st = build([r[0] for r in rows], [int(r[1]) for r in rows], d["pairs"], d["counts"], n_lex, maxbi)
os.makedirs(out_dir, exist_ok=True)
open(f"{out_dir}/lex.bin", "wb").write(lex); open(f"{out_dir}/lm.bin", "wb").write(lm)
st.update(lex_bytes=len(lex), lex_gz=len(gzip.compress(lex, 9)), lm_bytes=len(lm), lm_gz=len(gzip.compress(lm, 9)))
return st
|