File size: 10,228 Bytes
8948114
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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