1use crate::tagger_data::{pair_mask, LabeledSpan, TaggerExample};
26use crate::tagger_train::{hrm_config_from, MultiHeadTagger, TrainConfig};
27use crate::vocabulary::VocabularySpace;
28use candle_core::{DType, Device, IndexOp, Tensor, D};
29use candle_nn::{loss, ops::softmax, AdamW, Linear, Module, Optimizer, ParamsAdamW, VarBuilder, VarMap};
30use serde::Serialize;
31use std::collections::HashSet;
32use tokenizers::Tokenizer;
33
34pub struct BiaffineHead {
36 w: Tensor,
38 u: Linear,
40 n_rel: usize,
41 hp: usize,
42}
43
44impl BiaffineHead {
45 pub fn new(vb: VarBuilder, hp: usize, n_rel: usize) -> candle_core::Result<Self> {
46 let w = vb.get((n_rel, hp, hp), "biaffine_w")?;
47 let u = candle_nn::linear(2 * hp, n_rel, vb.pp("biaffine_u"))?;
48 Ok(BiaffineHead { w, u, n_rel, hp })
49 }
50
51 pub fn forward(&self, spans: &Tensor) -> candle_core::Result<Tensor> {
53 let (s, hp) = spans.dims2()?;
54 debug_assert_eq!(hp, self.hp);
55 let mut planes: Vec<Tensor> = Vec::with_capacity(self.n_rel);
57 let spans_t = spans.t()?.contiguous()?;
58 for r in 0..self.n_rel {
59 let wr = self.w.i(r)?.contiguous()?;
60 let bil = spans.matmul(&wr)?.matmul(&spans_t)?; planes.push(bil.unsqueeze(2)?); }
63 let bilinear = Tensor::cat(&planes, 2)?; let h_rep = spans.unsqueeze(1)?.expand((s, s, hp))?; let t_rep = spans.unsqueeze(0)?.expand((s, s, hp))?; let cat = Tensor::cat(&[h_rep, t_rep], 2)?.reshape((s * s, 2 * hp))?;
69 let lin = self.u.forward(&cat)?.reshape((s, s, self.n_rel))?;
70 bilinear + lin
71 }
72}
73
74pub fn pool_span(hidden: &Tensor, offsets: &[(usize, usize)], span: &LabeledSpan) -> candle_core::Result<Tensor> {
78 let idx: Vec<u32> = offsets
79 .iter()
80 .enumerate()
81 .filter(|(_, (ts, te))| te > ts && *ts < span.end && span.start < *te)
82 .map(|(i, _)| i as u32)
83 .collect();
84 let h = hidden.i(0)?; if idx.is_empty() {
86 let dim = h.dim(1)?;
88 let z = Tensor::zeros(dim, h.dtype(), h.device())?;
89 return Tensor::cat(&[z.clone(), z], 0);
90 }
91 let sel = Tensor::from_vec(idx.clone(), idx.len(), h.device())?;
92 let toks = h.index_select(&sel, 0)?; let start = toks.i(0)?;
94 let mean = toks.mean(0)?;
95 Tensor::cat(&[start, mean], 0)
96}
97
98fn pair_targets(spec: &VocabularySpace, ex: &TaggerExample) -> Vec<Vec<usize>> {
100 let names: Vec<&str> = spec.relation_facets.iter().map(|r| r.name.as_str()).collect();
101 let n = ex.spans.len();
102 let mut t = vec![vec![0usize; n]; n];
103 for r in &ex.relations {
104 if let Some(ri) = names.iter().position(|n| *n == r.name) {
105 if r.head < n && r.tail < n {
106 t[r.head][r.tail] = ri + 1;
107 }
108 }
109 }
110 t
111}
112
113pub fn type_allowed(spec: &VocabularySpace, mask: &HashSet<(String, String, String)>, fh: &str, ft: &str, class: usize) -> bool {
116 if class == 0 {
117 return true;
118 }
119 match spec.relation_facets.get(class - 1) {
120 Some(r) => mask.contains(&(fh.to_string(), ft.to_string(), r.name.clone())),
121 None => false,
122 }
123}
124
125#[derive(Debug, Clone, Serialize)]
126pub struct RelReport {
127 pub examples: usize,
128 pub pairs: usize,
129 pub classes: usize,
130 pub first_loss: f64,
131 pub last_loss: f64,
132 pub train_acc: f64,
134 pub train_acc_positive: f64,
137 pub dev_examples: usize,
139 pub dev_acc: f64,
143 pub dev_acc_positive: f64,
144}
145
146pub fn train_relations(
150 spec: &VocabularySpace,
151 examples: &[TaggerExample],
152 cfg: &TrainConfig,
153 tagger_dir: &std::path::Path,
154 epochs: usize,
155) -> Result<(VarMap, RelReport), String> {
156 let device = Device::Cpu;
157 let bert_cfg: candle_transformers::models::bert::Config = serde_json::from_slice(
158 &std::fs::read(cfg.base_dir.join("config.json")).map_err(|e| format!("base config: {e}"))?,
159 )
160 .map_err(|e| format!("parse base config: {e}"))?;
161 let tok = Tokenizer::from_file(&cfg.tokenizer).map_err(|e| format!("tokenizer: {e}"))?;
162 let n_a = crate::tagger_data::head_a_labels(spec).len();
163 let n_b = crate::tagger_data::head_b_labels().len();
164
165 let weights = tagger_dir.join("tagger.safetensors");
167 let vb_frozen = unsafe {
168 VarBuilder::from_mmaped_safetensors(&[weights.clone()], DType::F32, &device)
169 .map_err(|e| format!("load {}: {e}", weights.display()))?
170 };
171 let encoder = MultiHeadTagger::new(vb_frozen, &hrm_config_from(&bert_cfg), n_a, n_b).map_err(|e| format!("build encoder: {e}"))?;
172
173 let hp = 2 * bert_cfg.hidden_size;
175 let n_rel = spec.relation_facets.len() + 1;
176 let varmap = VarMap::new();
177 let vb = VarBuilder::from_varmap(&varmap, DType::F32, &device);
178 let head = BiaffineHead::new(vb, hp, n_rel).map_err(|e| format!("build head C: {e}"))?;
179 let mask = pair_mask(spec);
180
181 struct Prepared {
183 spans: Tensor, targets: Vec<Vec<usize>>,
185 facets: Vec<String>,
186 }
187 let mut prepared: Vec<Prepared> = Vec::new();
188 for ex in examples {
189 if ex.spans.len() < 2 {
190 continue;
191 }
192 let enc = match tok.encode(ex.text.as_str(), true) {
193 Ok(e) => e,
194 Err(_) => continue,
195 };
196 let n = enc.get_ids().len().min(cfg.max_len);
197 let ids = Tensor::from_vec(enc.get_ids()[..n].to_vec(), (1, n), &device).map_err(|e| e.to_string())?;
198 let attn = Tensor::from_vec(vec![1u32; n], (1, n), &device).map_err(|e| e.to_string())?;
199 let hidden = encoder.hidden(&ids, &attn, false).map_err(|e| format!("encode: {e}"))?;
200 let offsets: Vec<(usize, usize)> = enc.get_offsets()[..n].to_vec();
201 let reps: Vec<Tensor> = ex
202 .spans
203 .iter()
204 .map(|sp| pool_span(&hidden, &offsets, sp))
205 .collect::<candle_core::Result<Vec<_>>>()
206 .map_err(|e| format!("pool: {e}"))?;
207 let spans = Tensor::stack(&reps, 0).map_err(|e| e.to_string())?.detach();
208 prepared.push(Prepared { spans, targets: pair_targets(spec, ex), facets: ex.spans.iter().map(|s| s.facet.clone()).collect() });
209 }
210 if prepared.len() < 5 {
211 return Err("too few examples with >=2 spans to split train/dev".into());
212 }
213 let mut dev: Vec<Prepared> = Vec::new();
215 let mut train_set: Vec<Prepared> = Vec::new();
216 for (i, p) in prepared.into_iter().enumerate() {
217 if i % 5 == 4 {
218 dev.push(p);
219 } else {
220 train_set.push(p);
221 }
222 }
223 let prepared = train_set;
224
225 let mut opt = AdamW::new(varmap.all_vars(), ParamsAdamW { lr: cfg.lr, ..Default::default() })
226 .map_err(|e| format!("optimizer: {e}"))?;
227 let (mut first_loss, mut last_loss) = (f64::NAN, f64::NAN);
228 let mut total_pairs = 0usize;
229
230 for epoch in 1..=epochs {
231 let mut sum = 0.0f64;
232 let mut steps = 0usize;
233 for p in &prepared {
234 let logits = head.forward(&p.spans).map_err(|e| format!("head C forward: {e}"))?;
235 let s = p.facets.len();
236 let mut rows: Vec<Tensor> = Vec::new();
238 let mut tgts: Vec<u32> = Vec::new();
239 for i in 0..s {
240 for j in 0..s {
241 if i == j {
242 continue;
243 }
244 let cls = p.targets[i][j];
245 let row = logits.i((i, j)).map_err(|e| e.to_string())?; let allow: Vec<f32> = (0..n_rel)
248 .map(|c| if type_allowed(spec, &mask, &p.facets[i], &p.facets[j], c) { 0.0 } else { f32::NEG_INFINITY })
249 .collect();
250 let allow = Tensor::from_vec(allow, n_rel, &device).map_err(|e| e.to_string())?;
251 rows.push((row + allow).map_err(|e| e.to_string())?.unsqueeze(0).map_err(|e| e.to_string())?);
252 tgts.push(cls as u32);
253 }
254 }
255 if rows.is_empty() {
256 continue;
257 }
258 let batch = Tensor::cat(&rows, 0).map_err(|e| e.to_string())?;
259 let tgt = Tensor::from_vec(tgts.clone(), tgts.len(), &device).map_err(|e| e.to_string())?;
260 let l = loss::cross_entropy(&batch, &tgt).map_err(|e| format!("ce: {e}"))?;
261 opt.backward_step(&l).map_err(|e| format!("backward: {e}"))?;
262 sum += l.to_scalar::<f32>().map_err(|e| e.to_string())? as f64;
263 steps += 1;
264 if epoch == 1 {
265 total_pairs += tgts.len();
266 }
267 }
268 let avg = sum / steps.max(1) as f64;
269 if epoch == 1 {
270 first_loss = avg;
271 }
272 last_loss = avg;
273 }
274
275 let score = |set: &[Prepared]| -> Result<(f64, f64), String> {
277 let (mut ok, mut n, mut ok_pos, mut n_pos) = (0usize, 0usize, 0usize, 0usize);
278 for p in set {
279 let logits = head.forward(&p.spans).map_err(|e| e.to_string())?;
280 let s = p.facets.len();
281 for i in 0..s {
282 for j in 0..s {
283 if i == j {
284 continue;
285 }
286 let row = logits.i((i, j)).map_err(|e| e.to_string())?;
287 let allow: Vec<f32> = (0..n_rel)
288 .map(|c| if type_allowed(spec, &mask, &p.facets[i], &p.facets[j], c) { 0.0 } else { f32::NEG_INFINITY })
289 .collect();
290 let allow = Tensor::from_vec(allow, n_rel, &device).map_err(|e| e.to_string())?;
291 let pred = softmax(&(row + allow).map_err(|e| e.to_string())?, D::Minus1)
292 .and_then(|t| t.argmax(D::Minus1))
293 .and_then(|t| t.to_scalar::<u32>())
294 .map_err(|e| e.to_string())? as usize;
295 let want = p.targets[i][j];
296 n += 1;
297 if pred == want {
298 ok += 1;
299 }
300 if want != 0 {
301 n_pos += 1;
302 if pred == want {
303 ok_pos += 1;
304 }
305 }
306 }
307 }
308 }
309 Ok((ok as f64 / n.max(1) as f64, ok_pos as f64 / n_pos.max(1) as f64))
310 };
311 let (dev_acc, dev_acc_pos) = score(&dev)?;
312
313 let (mut ok, mut n, mut ok_pos, mut n_pos) = (0usize, 0usize, 0usize, 0usize);
315 for p in &prepared {
316 let logits = head.forward(&p.spans).map_err(|e| e.to_string())?;
317 let s = p.facets.len();
318 for i in 0..s {
319 for j in 0..s {
320 if i == j {
321 continue;
322 }
323 let row = logits.i((i, j)).map_err(|e| e.to_string())?;
324 let allow: Vec<f32> = (0..n_rel)
325 .map(|c| if type_allowed(spec, &mask, &p.facets[i], &p.facets[j], c) { 0.0 } else { f32::NEG_INFINITY })
326 .collect();
327 let allow = Tensor::from_vec(allow, n_rel, &device).map_err(|e| e.to_string())?;
328 let pred = softmax(&(row + allow).map_err(|e| e.to_string())?, D::Minus1)
329 .and_then(|t| t.argmax(D::Minus1))
330 .and_then(|t| t.to_scalar::<u32>())
331 .map_err(|e| e.to_string())? as usize;
332 let want = p.targets[i][j];
333 n += 1;
334 if pred == want {
335 ok += 1;
336 }
337 if want != 0 {
338 n_pos += 1;
339 if pred == want {
340 ok_pos += 1;
341 }
342 }
343 }
344 }
345 }
346
347 let report = RelReport {
348 examples: prepared.len(),
349 pairs: total_pairs,
350 classes: n_rel,
351 first_loss: round4(first_loss),
352 last_loss: round4(last_loss),
353 train_acc: round4(ok as f64 / n.max(1) as f64),
354 train_acc_positive: round4(ok_pos as f64 / n_pos.max(1) as f64),
355 dev_examples: dev.len(),
356 dev_acc: round4(dev_acc),
357 dev_acc_positive: round4(dev_acc_pos),
358 };
359 Ok((varmap, report))
360}
361
362fn round4(v: f64) -> f64 {
363 (v * 10000.0).round() / 10000.0
364}
365
366pub fn save(varmap: &VarMap, spec: &VocabularySpace, out_dir: &std::path::Path) -> Result<(), String> {
368 std::fs::create_dir_all(out_dir).map_err(|e| e.to_string())?;
369 varmap.save(out_dir.join("relations.safetensors")).map_err(|e| format!("save head C: {e}"))?;
370 let meta = serde_json::json!({
371 "classes": crate::tagger_data::head_c_labels(spec),
372 "relations": spec.relation_facets,
373 });
374 std::fs::write(out_dir.join("relations.json"), serde_json::to_vec_pretty(&meta).map_err(|e| e.to_string())?)
375 .map_err(|e| e.to_string())?;
376 Ok(())
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382 use crate::tagger_data::{Case, RelationLabel};
383 use crate::vocabulary::{EntityFacet, RelationFacet};
384
385 fn spec() -> VocabularySpace {
386 VocabularySpace {
387 version: 1,
388 corpus: "t".into(),
389 entity_facets: vec![
390 EntityFacet { name: "org".into(), parent: None, description: "companies".into(), examples: vec![], structural: false },
391 EntityFacet { name: "system".into(), parent: None, description: "platforms".into(), examples: vec![], structural: false },
392 ],
393 relation_facets: vec![RelationFacet { name: "develops".into(), head: "org".into(), tail: "system".into() }],
394 gazetteer: vec![],
395 metrics: None,
396 }
397 }
398
399 #[test]
400 fn type_mask_makes_reversed_relations_unrepresentable() {
401 let s = spec();
402 let m = pair_mask(&s);
403 assert!(type_allowed(&s, &m, "system", "org", 0));
405 assert!(type_allowed(&s, &m, "org", "system", 1));
407 assert!(!type_allowed(&s, &m, "system", "org", 1));
409 assert!(!type_allowed(&s, &m, "org", "org", 1));
411 }
412
413 #[test]
414 fn pair_targets_are_directional() {
415 let s = spec();
416 let ex = TaggerExample {
417 text: "Boeing develops the MQ-28.".into(),
418 spans: vec![
419 LabeledSpan { start: 0, end: 6, facet: "org".into(), surface: "Boeing".into(), negated: false, hedged: false },
420 LabeledSpan { start: 20, end: 25, facet: "system".into(), surface: "MQ-28".into(), negated: false, hedged: false },
421 ],
422 relations: vec![RelationLabel { head: 0, tail: 1, name: "develops".into() }],
423 case: Case::Normal,
424 };
425 let t = pair_targets(&s, &ex);
426 assert_eq!(t[0][1], 1, "org→system carries the relation");
427 assert_eq!(t[1][0], 0, "system→org is `none`");
428 }
429
430 #[test]
431 fn biaffine_shapes_and_pooling() {
432 let device = Device::Cpu;
433 let varmap = VarMap::new();
434 let vb = VarBuilder::from_varmap(&varmap, DType::F32, &device);
435 let (hp, n_rel, s) = (8usize, 3usize, 4usize);
436 let head = BiaffineHead::new(vb, hp, n_rel).unwrap();
437 let spans = Tensor::rand(0f32, 1f32, (s, hp), &device).unwrap();
438 let logits = head.forward(&spans).unwrap();
439 assert_eq!(logits.dims(), &[s, s, n_rel]);
440
441 let hidden = Tensor::rand(0f32, 1f32, (1, 5, 4), &device).unwrap();
443 let offsets = [(0, 0), (0, 6), (7, 15), (16, 21), (0, 0)];
444 let sp = LabeledSpan { start: 0, end: 6, facet: "org".into(), surface: "Boeing".into(), negated: false, hedged: false };
445 let pooled = pool_span(&hidden, &offsets, &sp).unwrap();
446 assert_eq!(pooled.dims(), &[8]); let far = LabeledSpan { start: 900, end: 905, facet: "org".into(), surface: "x".into(), negated: false, hedged: false };
449 assert_eq!(pool_span(&hidden, &offsets, &far).unwrap().dims(), &[8]);
450 }
451}