File size: 9,977 Bytes
97df2ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e26f4cc
97df2ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e26f4cc
97df2ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e26f4cc
97df2ae
 
 
 
 
 
e26f4cc
97df2ae
 
 
 
 
e26f4cc
97df2ae
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
//! BPE byte-level próprio do KHTST_RUST (contrato do `dados/tokenizador.py`
//! do v23: vocabulário próprio, especiais reservados). Sem dependências
//! externas; treino incremental por contagem de pares com índice de posições.
//!
//! IDs especiais: 0=<pad> 1=<bos> 2=<eos> 3=<unk>; bytes 4..260; merges a partir de 260.

use std::collections::{HashMap, HashSet};

pub const PAD: u32 = 0;
pub const BOS: u32 = 1;
pub const EOS: u32 = 2;
pub const UNK: u32 = 3;
pub const BASE_BYTES: u32 = 4;
const ESPECIAIS: [&str; 4] = ["<pad>", "<bos>", "<eos>", "<unk>"];

#[derive(Debug, Clone, Default)]
pub struct Bpe {
    /// token por id (string; bytes crus para os 256 iniciais)
    pub vocab: Vec<String>,
    /// merges na ordem de aprendizado: (id_a, id_b) -> id_novo implícito = 260+índice
    pub merges: Vec<(u32, u32)>,
    rank: HashMap<(u32, u32), u32>,
    id_por_token: HashMap<String, u32>,
    cache: HashMap<String, Vec<u32>>,
}

impl Bpe {
    pub fn novo() -> Self {
        let mut b = Bpe::default();
        for (i, s) in ESPECIAIS.iter().enumerate() {
            b.id_por_token.insert(s.to_string(), i as u32);
            b.vocab.push(s.to_string());
        }
        for i in 0..256u32 {
            let s = String::from_utf8_lossy(&[i as u8]).to_string();
            b.id_por_token.insert(s.clone(), BASE_BYTES + i);
            b.vocab.push(s);
        }
        b
    }

    pub fn n_vocab(&self) -> usize {
        self.vocab.len()
    }

    fn id_merge(&self, indice_merge: usize) -> u32 {
        260 + indice_merge as u32
    }

    /// Treina BPE em `textos` até `vocab_alvo` tokens (inclui especiais + bytes).
    /// Contagem incremental: cada merge só visita as posições do par afetado.
    pub fn treinar(textos: &[String], vocab_alvo: usize) -> Self {
        let mut b = Bpe::novo();
        // palavras (por espaço em branco; o espaço separa palavras — sem marcador final)
        let mut contagem_palavra: HashMap<String, u32> = HashMap::new();
        for t in textos {
            for w in t.split_whitespace() {
                *contagem_palavra.entry(w.to_string()).or_insert(0) += 1;
            }
        }
        // estado: sequências por palavra + índice global de pares
        let mut seqs: Vec<Vec<u32>> = vec![];
        let mut pesos: Vec<u32> = vec![];
        for (w, c) in &contagem_palavra {
            seqs.push(w.as_bytes().iter().map(|&x| BASE_BYTES + x as u32).collect());
            pesos.push(*c);
        }
        // contagem de pares e posições
        let mut cont: HashMap<(u32, u32), u64> = HashMap::new();
        let mut pos: HashMap<(u32, u32), Vec<(usize, usize)>> = HashMap::new();
        for (si, s) in seqs.iter().enumerate() {
            for j in 0..s.len().saturating_sub(1) {
                let p = (s[j], s[j + 1]);
                *cont.entry(p).or_insert(0) += pesos[si] as u64;
                pos.entry(p).or_default().push((si, j));
            }
        }
        let n_merges_alvo = vocab_alvo.saturating_sub(b.vocab.len());
        for k in 0..n_merges_alvo {
            // melhor par por (frequência, ordem de criação implícita)
            let melhor = cont.iter().filter(|(_, &c)| c > 0).max_by_key(|(p, &c)| (c, std::cmp::Reverse(*p)));
            let (par, _) = match melhor {
                Some((p, c)) => (*p, *c),
                None => break,
            };
            let novo_id = b.id_merge(b.merges.len());
            let concatenado = {
                let a = b.vocab.get(par.0 as usize).cloned().unwrap_or_default();
                let c = b.vocab.get(par.1 as usize).cloned().unwrap_or_default();
                format!("{a}{c}")
            };
            // aplica o merge nas posições registradas (por baixo para índices estáveis)
            let mut ocupadas: HashSet<(usize, usize)> = HashSet::new();
            let mut afetadas: Vec<(u32, u32)> = vec![];
            if let Some(posicoes) = pos.get(&par).cloned() {
                for (si, j) in posicoes {
                    if ocupadas.contains(&(si, j)) {
                        continue;
                    }
                    let s = &mut seqs[si];
                    if j + 1 >= s.len() || (s[j], s[j + 1]) != par {
                        continue;
                    }
                    // remove pares vizinhos antigos
                    if j > 0 {
                        let esq = (s[j - 1], s[j]);
                        if let Some(c) = cont.get_mut(&esq) {
                            *c = c.saturating_sub(pesos[si] as u64);
                        }
                        afetadas.push(esq);
                    }
                    if j + 2 < s.len() {
                        let dir = (s[j + 1], s[j + 2]);
                        if let Some(c) = cont.get_mut(&dir) {
                            *c = c.saturating_sub(peso_como_u64(pesos[si]));
                        }
                        afetadas.push(dir);
                    }
                    s[j] = novo_id;
                    s.remove(j + 1);
                    ocupadas.insert((si, j));
                    ocupadas.insert((si, j.saturating_sub(1)));
                    // novos pares
                    if j > 0 {
                        let esq = (s[j - 1], novo_id);
                        *cont.entry(esq).or_insert(0) += pesos[si] as u64;
                        pos.entry(esq).or_default().push((si, j - 1));
                    }
                    if j + 1 < s.len() {
                        let dir = (novo_id, s[j + 1]);
                        *cont.entry(dir).or_insert(0) += pesos[si] as u64;
                        pos.entry(dir).or_default().push((si, j));
                    }
                }
            }
            let _ = afetadas;
            cont.remove(&par);
            pos.remove(&par);
            b.merges.push(par);
            b.vocab.push(concatenado.clone());
            b.id_por_token.insert(concatenado, novo_id);
            if k % 512 == 0 {
                log::info!("bpe: merge {k}/{} (vocab {})", n_merges_alvo, b.vocab.len());
            }
        }
        b
    }

    /// Codifica uma palavra (com cache).
    fn codificar_palavra(&mut self, w: &str) -> Vec<u32> {
        if let Some(v) = self.cache.get(w) {
            return v.clone();
        }
        let mut s: Vec<u32> = w.as_bytes().iter().map(|&x| BASE_BYTES + x as u32).collect();
        loop {
            // par de menor rank
            let melhor = s.windows(2).map(|p| (p[0], p[1])).filter_map(|p| self.rank.get(&p).map(|r| (r, p))).min_by_key(|(r, _)| *r);
            match melhor {
                Some((_, (a, b))) => {
                    let alvo = self.rank[&(a, b)] as usize;
                    let novo = self.id_merge(alvo);
                    let mut out = vec![];
                    let mut i = 0;
                    while i < s.len() {
                        if i + 1 < s.len() && s[i] == a && s[i + 1] == b {
                            out.push(novo);
                            i += 2;
                        } else {
                            out.push(s[i]);
                            i += 1;
                        }
                    }
                    s = out;
                }
                None => break,
            }
            if s.len() <= 1 {
                break;
            }
        }
        self.cache.insert(w.to_string(), s.clone());
        s
    }

    pub fn reconstruir_indices(&mut self) {
        self.rank.clear();
        for (i, (a, b)) in self.merges.iter().enumerate() {
            self.rank.insert((*a, *b), i as u32);
        }
    }

    pub fn codificar(&mut self, texto: &str) -> Vec<u32> {
        let espaco = BASE_BYTES + b' ' as u32;
        let mut ids = vec![BOS];
        for w in texto.split_whitespace() {
            ids.extend(self.codificar_palavra(w));
            ids.push(espaco);
        }
        ids.push(EOS);
        ids
    }

    pub fn decodificar(&self, ids: &[u32]) -> String {
        let mut out = String::new();
        for &i in ids {
            match i {
                PAD | BOS | EOS => {}
                _ => {
                    if let Some(s) = self.vocab.get(i as usize) {
                        if !ESPECIAIS.contains(&s.as_str()) {
                            out.push_str(s);
                        }
                    }
                }
            }
        }
        out.trim().to_string()
    }

    pub fn para_json(&self) -> serde_json::Value {
        serde_json::json!({
            "vocab": self.vocab,
            "merges": self.merges,
        })
    }

    pub fn de_json(v: &serde_json::Value) -> Option<Self> {
        let vocab: Vec<String> = v.get("vocab")?.as_array()?.iter().map(|x| x.as_str().unwrap_or_default().to_string()).collect();
        let merges: Vec<(u32, u32)> = v.get("merges")?.as_array()?.iter().filter_map(|x| {
            let a = x.get(0)?.as_u64()? as u32;
            let b = x.get(1)?.as_u64()? as u32;
            Some((a, b))
        }).collect();
        let mut b = Bpe { vocab, merges, ..Default::default() };
        for (i, s) in b.vocab.iter().enumerate() {
            b.id_por_token.insert(s.clone(), i as u32);
        }
        b.reconstruir_indices();
        Some(b)
    }
}

fn peso_como_u64(p: u32) -> u64 {
    p as u64
}

#[cfg(test)]
mod testes {
    use super::*;

    #[test]
    fn treina_e_recodifica() {
        let textos: Vec<String> = ["o gato come o peixe e o cão come o osso",
             "o gato mira o peixe e o cão mira o osso",
             "peixe osso gato cão o e come mira"].iter().map(|s| s.to_string()).collect();
        let mut b = Bpe::treinar(&textos, 300);
        let ids = b.codificar("o gato come o peixe");
        assert_eq!(ids.first(), Some(&BOS));
        assert_eq!(ids.last(), Some(&EOS));
        let texto = b.decodificar(&ids);
        assert_eq!(texto, "o gato come o peixe");
        assert!(b.n_vocab() >= 260);
    }
}