| # Copyright (c) Meta Platforms, Inc. | |
| # All rights reserved. | |
| from typing import List, Optional, Sequence | |
| import torch | |
| import torch.nn.functional as F | |
| from torch import Tensor | |
| from torch.nn.modules.loss import _Loss | |
| # If you kept the adapter around only for scheduler access you can drop it entirely, | |
| # because we pass precomputed_weight from training.step. Keeping it is harmless though. | |
| class EditFlowsLoss(_Loss): | |
| """ | |
| Edit Flows loss (Eq. 23), ragged version: | |
| L_i = (sum_j λ_ins[i][j] + sum_j λ_del[i][j] + sum_j λ_sub[i][j]) | |
| - w_i * ( sum_{ins events e} [log λ_ins[i][slot_e] + log Q_ins[i](y_e)] | |
| + sum_{del j} [log λ_del[i][j]] | |
| + sum_{sub j} [log λ_sub[i][j] + log Q_sub[i](y_j)] ) | |
| """ | |
| def __init__(self, reduction: str = "mean") -> None: | |
| super().__init__(None, None, reduction) | |
| def forward( | |
| self, | |
| lam_ins: torch.Tensor, # (B, L) | |
| logits_ins: torch.Tensor, # (B, L, V) | |
| lam_del: torch.Tensor, # (B, L) | |
| lam_sub: torch.Tensor, # (B, L) | |
| logits_sub: torch.Tensor, # (B, L, V) | |
| z_t: torch.Tensor, # (B, N) aligned, with eps_id | |
| z_1: torch.Tensor, # (B, N) aligned target, with eps_id | |
| x_t: torch.Tensor, # (B, L) | |
| valid_mask: torch.Tensor, # (B, L) bool, False==padding, True==valid | |
| precomputed_weight: torch.Tensor, # (B,) or () = kappa_dot/(1-kappa) | |
| eps_id: int, | |
| bos_id: int, | |
| eos_id: int, | |
| ) -> torch.Tensor: | |
| """ | |
| Implements Eq. 23 style loss for Edit Flows. | |
| We: | |
| 1) penalize total outgoing rate on x_t | |
| 2) for every column in (z_t, z_1) that still differs, we map it to an x_t edit | |
| and add - w * log u_required | |
| BOS/EOS handling: | |
| - z_t and z_1 are aligned and already contain BOS/EOS | |
| - we do NOT allow edits that delete/replace BOS/EOS | |
| - we also skip columns whose target token is BOS/EOS (they should already match) | |
| """ | |
| device = lam_ins.device | |
| B, L = x_t.shape | |
| _, N = z_t.shape | |
| # 1. ----- RATE TERM ----- | |
| # valid_mask: True = real token, False = pad | |
| valid_f = valid_mask.to(lam_ins.dtype) # (B, L) 1.0 for valid, 0.0 for pad | |
| # total outgoing rate at each position | |
| total_rate_pos = lam_ins + lam_del + lam_sub # (B, L) | |
| total_rate_pos = total_rate_pos * valid_f # zero out pads | |
| loss_rate = total_rate_pos.sum(dim=1) # (B,) | |
| # 2. ----- EDIT TERM ----- | |
| # precompute log-softmax for tokens (better numerics) | |
| logp_ins = F.log_softmax(logits_ins, dim=-1) # (B, L, V) | |
| logp_sub = F.log_softmax(logits_sub, dim=-1) # (B, L, V) | |
| # Make weight shape nice | |
| if precomputed_weight.dim() == 0: | |
| precomputed_weight = precomputed_weight.view(1).expand(B).to(device) | |
| else: | |
| precomputed_weight = precomputed_weight.to(device) | |
| # use float for accumulation | |
| loss_edit = torch.zeros(B, dtype=torch.float32, device=device) | |
| for b in range(B): | |
| # how many *valid* tokens in x_t[b] | |
| valid_len = int(valid_mask[b].sum().item()) | |
| prefix_non_eps = 0 # = number of non-ε seen in z_t[b, :i] | |
| for i in range(N): | |
| zt = int(z_t[b, i].item()) | |
| z1 = int(z_1[b, i].item()) | |
| # map this column to an x_t position | |
| if zt != eps_id: | |
| # this column corresponds to x_t[b, prefix_non_eps] | |
| x_pos = prefix_non_eps | |
| is_token = True | |
| prefix_non_eps += 1 | |
| else: | |
| # this is a gap column, sits BETWEEN tokens | |
| x_pos = prefix_non_eps | |
| is_token = False | |
| # if already matched -> no term | |
| if zt == z1: | |
| continue | |
| # --- BOS/EOS guards on the target side --- | |
| # if the target token is BOS or EOS, we shouldn't try to force an edit here | |
| if z1 == bos_id or z1 == eos_id: | |
| # skip this column; aligned BOS/EOS should already match | |
| continue | |
| # figure out which edit we need | |
| # CASE 1: deletion: z_t has token, z_1 has ε | |
| if is_token and (z1 == eps_id): | |
| # delete token at x_pos | |
| if x_pos >= valid_len: | |
| # out of range (shouldn't happen if alignment & mask match) | |
| raise NotImplementedError | |
| x_token = int(x_t[b, x_pos].item()) | |
| # do NOT delete BOS/EOS | |
| if x_token == bos_id or x_token == eos_id: | |
| continue | |
| lam = lam_del[b, x_pos].clamp_min(1e-12) | |
| log_u_req = torch.log(lam) | |
| # CASE 2: substitution: token -> different token | |
| elif is_token and (z1 != eps_id) and (zt != z1): | |
| if x_pos >= valid_len: | |
| raise NotImplementedError | |
| x_token = int(x_t[b, x_pos].item()) | |
| # do NOT substitute BOS/EOS | |
| if x_token == bos_id or x_token == eos_id: | |
| continue | |
| lam = lam_sub[b, x_pos].clamp_min(1e-12) | |
| logp_tok = logp_sub[b, x_pos, z1] | |
| log_u_req = torch.log(lam) + logp_tok | |
| # CASE 3: insertion: ε -> token | |
| elif (not is_token) and (z1 != eps_id): | |
| # insertion in the gap after token (x_pos - 1) | |
| # if x_pos == 0, we insert after "BOS"/at start -> map to position 0 | |
| ins_pos = x_pos - 1 | |
| if ins_pos < 0: | |
| ins_pos = 0 | |
| # clamp to last valid position if needed | |
| if valid_len == 0: | |
| # degenerate, but avoid -1 | |
| ins_pos = 0 | |
| elif ins_pos >= valid_len: | |
| ins_pos = valid_len - 1 | |
| # also don't insert "after" EOS if ins_pos currently points to EOS | |
| x_token = int(x_t[b, ins_pos].item()) | |
| if x_token == eos_id: | |
| # simplest policy: skip this insertion supervision | |
| continue | |
| lam = lam_ins[b, ins_pos].clamp_min(1e-12) | |
| logp_tok = logp_ins[b, ins_pos, z1] | |
| log_u_req = torch.log(lam) + logp_tok | |
| else: | |
| # unknown pattern (shouldn't happen with proper alignment) | |
| raise NotImplementedError | |
| w = precomputed_weight[b] | |
| loss_edit[b] += - w * log_u_req | |
| # 3. ----- COMBINE ----- | |
| loss = loss_rate + loss_edit # (B,) | |
| loss = loss.mean() | |
| return loss | |
| class EditFlowsLossReParam(_Loss): | |
| """ | |
| Edit Flows loss (Eq. 23) but parameterized by: | |
| - lam_total: (B, L) >= 0 | |
| - pi_type: (B, L, 3) prob over {ins, del, sub}, sums to 1 | |
| Token distributions are still given by logits_ins/logits_sub. | |
| Required-edit log rate: | |
| del: log u = log lam_total[pos] + log pi_del[pos] | |
| sub: log u = log lam_total[pos] + log pi_sub[pos] + log Q_sub(token) | |
| ins: log u = log lam_total[pos] + log pi_ins[pos] + log Q_ins(token) | |
| """ | |
| def __init__(self, reduction: str = "mean", gamma_rate: float = 1.0, eps: float = 1e-12) -> None: | |
| super().__init__(None, None, reduction) | |
| self.gamma_rate = float(gamma_rate) | |
| self.eps = float(eps) | |
| def forward( | |
| self, | |
| lam_total: torch.Tensor, # (B, L) | |
| pi_type: torch.Tensor, # (B, L, 3) probabilities over {ins, del, sub} | |
| logits_ins: torch.Tensor, # (B, L, V) | |
| logits_sub: torch.Tensor, # (B, L, V) | |
| z_t: torch.Tensor, # (B, N) aligned, with eps_id | |
| z_1: torch.Tensor, # (B, N) aligned target, with eps_id | |
| x_t: torch.Tensor, # (B, L) | |
| valid_mask: torch.Tensor, # (B, L) bool, False==padding, True==valid (same as your old loss) | |
| precomputed_weight: torch.Tensor, # (B,) or () = kappa_dot/(1-kappa) | |
| eps_id: int, | |
| bos_id: int, | |
| eos_id: int, | |
| ) -> torch.Tensor: | |
| device = lam_total.device | |
| B, L = x_t.shape | |
| _, N = z_t.shape | |
| # --- RATE TERM --- | |
| valid_f = valid_mask.to(lam_total.dtype) # True=valid -> 1.0 | |
| loss_rate = (lam_total * valid_f).sum(dim=1) # (B,) | |
| loss_rate = self.gamma_rate * loss_rate | |
| # --- EDIT TERM --- | |
| logp_ins = F.log_softmax(logits_ins, dim=-1) # (B, L, V) | |
| logp_sub = F.log_softmax(logits_sub, dim=-1) # (B, L, V) | |
| # normalize pi_type defensively (in case caller passes slightly off-simplex) | |
| pi_sum = pi_type.sum(dim=-1, keepdim=True).clamp_min(self.eps) | |
| pi_type = pi_type / pi_sum | |
| log_lam = torch.log(lam_total.clamp_min(self.eps)) # (B, L) | |
| log_pi = torch.log(pi_type.clamp_min(self.eps)) # (B, L, 3) | |
| # weight shape | |
| if precomputed_weight.dim() == 0: | |
| precomputed_weight = precomputed_weight.view(1).expand(B).to(device) | |
| else: | |
| precomputed_weight = precomputed_weight.to(device) | |
| loss_edit = torch.zeros(B, dtype=torch.float32, device=device) | |
| for b in range(B): | |
| valid_len = int(valid_mask[b].sum().item()) | |
| prefix_non_eps = 0 | |
| for i in range(N): | |
| zt = int(z_t[b, i].item()) | |
| z1 = int(z_1[b, i].item()) | |
| # map this alignment column to an x_t position | |
| if zt != eps_id: | |
| x_pos = prefix_non_eps | |
| is_token = True | |
| prefix_non_eps += 1 | |
| else: | |
| x_pos = prefix_non_eps | |
| is_token = False | |
| # already matched -> no supervision | |
| if zt == z1: | |
| continue | |
| # don't force edits where target is BOS/EOS | |
| if z1 == bos_id or z1 == eos_id: | |
| continue | |
| # CASE 1: deletion (token -> eps) | |
| if is_token and (z1 == eps_id): | |
| if x_pos >= valid_len: | |
| raise NotImplementedError | |
| x_token = int(x_t[b, x_pos].item()) | |
| if x_token == bos_id or x_token == eos_id: | |
| continue | |
| log_u_req = log_lam[b, x_pos] + log_pi[b, x_pos, 1] # del index = 1 | |
| # CASE 2: substitution (token -> different token) | |
| elif is_token and (z1 != eps_id) and (zt != z1): | |
| if x_pos >= valid_len: | |
| raise NotImplementedError | |
| x_token = int(x_t[b, x_pos].item()) | |
| if x_token == bos_id or x_token == eos_id: | |
| continue | |
| log_u_req = ( | |
| log_lam[b, x_pos] | |
| + log_pi[b, x_pos, 2] # sub index = 2 | |
| + logp_sub[b, x_pos, z1] | |
| ) | |
| # CASE 3: insertion (eps -> token), map gap to "after token (x_pos-1)" | |
| elif (not is_token) and (z1 != eps_id): | |
| ins_pos = x_pos - 1 | |
| if ins_pos < 0: | |
| ins_pos = 0 | |
| if valid_len == 0: | |
| ins_pos = 0 | |
| elif ins_pos >= valid_len: | |
| ins_pos = valid_len - 1 | |
| x_token = int(x_t[b, ins_pos].item()) | |
| if x_token == eos_id: | |
| continue | |
| log_u_req = ( | |
| log_lam[b, ins_pos] | |
| + log_pi[b, ins_pos, 0] # ins index = 0 | |
| + logp_ins[b, ins_pos, z1] | |
| ) | |
| else: | |
| raise NotImplementedError | |
| w = precomputed_weight[b] | |
| loss_edit[b] += -w * log_u_req | |
| loss = loss_rate + loss_edit | |
| return loss.mean() |
Xet Storage Details
- Size:
- 12.5 kB
- Xet hash:
- 44a8db48c8169a9405f23ff93ef05abc3f3cde2c1922590662d3c427dd5c2882
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.