接口参数在 Aggregator.py 中：global_merging=True：是否在“global attention”阶段允许执行合并（默认允许）merging=0：在第几个注意力 block 之后开始执行合并实际用到 token merging 相关逻辑的地方只在于 global attention 中。相关逻辑调用在 layers/attention.py 中，部分函数实现在 merging/merge.py 中attention.py他这里还实现了分 chunk 计算。应该也有一部分对于最终提速的贡献。当然他论文是一点没说啊，但既然他这样干了，我们也没必要改回去吃这个哑巴亏。具体实现流程：调用 token_merge_bipartite2d 返回一对 (merge, unmerge) 函数用 merge() 对 q、k、v 的“token 维度”进行merging（将一部分 src token 聚合到 dst token），得到更短的 $N_m$用mering后的 q,k,v 做 scaled dot-product attention注意力输出后，再调用 unmerge() 把输出恢复到合并前的原始顺序与长度if global_merging is not None and global_merging in merge_num: generator = torch.Generator(device=x.device) generator.manual_seed(33) merge_ratio = 0.9 r = int(x.shape[1] * merge_ratio) m, u = token_merge_bipartite2d( x, self.patch_width, self.patch_height, 2, 2, r, False, generator, enable_protection=True, ) m_a, u_a = (m, u) B_q, H_q, N_q, D_q = q.shape q_merge_in = q.permute(0, 2, 1, 3).reshape(B_q, N_q, H_q * D_q) k_merge_in = k.permute(0, 2, 1, 3).reshape(B_q, N_q, H_q * D_q) v_merge_in = v.permute(0, 2, 1, 3).reshape(B_q, N_q, H_q * D_q) q_out, k_out, v_out = m_a( q_merge_in, mode="mean", extra_tensors=k_merge_in, extra_tensors_2=v_merge_in, ) del q_merge_in, k_merge_in, v_merge_in N_m = q_out.shape[1] q = q_out.reshape(B_q, N_m, H_q, D_q).permute(0, 2, 1, 3) k = k_out.reshape(B_q, N_m, H_q, D_q).permute(0, 2, 1, 3) v = v_out.reshape(B_q, N_m, H_q, D_q).permute(0, 2, 1, 3) del q_out, k_out, v_out N = N_mtoken_merge_barpartite()函数声明将标记分为源（src）组和目标（dst）组，并从源组向目标组合并r个标记。目标标记通过从每个（sx，sy）区域随机选择一个标记来选择。可选地，根据重要性得分保护顶部10%的标记不被合并。 参数： - metric [B, N, C]：用于相似度计算的张量，B = 批量大小，N = 标记数量，C = 特征维度 - w：图像宽度的标记数 - h：图像高度的标记数 - sx：目标在x维度的步长，必须能均匀整除w - sy：目标在y维度的步长，必须能均匀整除h - r：通过合并要移除的标记数量 - no_rand：如果为True，禁用随机性（仅使用左上角标记） - generator：如果no_rand为False且不为None时的随机数生成器 - enable_protection：如果为True，启用重要性保护特征 返回： - (merge, unmerge)：两个函数，分别用于合并标记和恢复合并前状态Function call:m, u = token_merge_bipartite2d( x, // tokens self.patch_width, self.patch_height, 2, //sx 2, //sy r, // r = int(x.shape[1] * merge_ratio) 即要“移除”并合并的 src token 数约占 90% False, generator, enable_protection=True, //实现上会均匀抽取 N 的 10% 的 token 索引为 protected_indices )具体实现论文中提到固定 stride=10%N 作为 protected tokens（保持 spatial consistency），首先实现于：# 是否启用保护：当前实现为均匀抽样 N 的 10% 作为 protected 索引，防止被合并 if enable_protection: num_protected = int(N * 0.1) step = max(1, N // num_protected) # 均匀步进采样出受保护的索引列表 protected_indices = torch.arange(0, N, step, device=metric.device)[ :num_protected ]接下来开始划分 dst 和 src token、区分 frame 0 和 其他 frame# idx_buffer_seq：长度为 N 的标记向量；-1 表示 dst，0 表示 src idx_buffer_seq = torch.zeros(N, device=metric.device, dtype=torch.int64) # hsy、wsx：分别是以 sy、sx 网格步幅划分后，每张图上的网格块数量（高、宽方向） hsy, wsx = h // sy, w // sx # Number of blocks within each image # 第一张图像（含 5 个特殊 token + 所有 patch）全部作为 dst 聚合目标 if num_imgs > 0: idx_buffer_seq[:tokens_per_img] = -1 # 因为 frame 0 的 tokens (including cam/reg) 都在 [0:tokens_per_img] # 其他 Frame： # 1) 每张图的 5 个特殊 token 标记为 dst # 2) 在每个 (sy, sx) 网格中选择 1 个 token 为 dst（其余默认 src） if num_imgs > 1: # 计算所有其它图像的 5 个特殊 token 的全局索引，并标记为 dst cls_indices = ( torch.arange(1, num_imgs, device=metric.device) * tokens_per_img ) cls_indices = cls_indices[:, None] + torch.arange(5, device=metric.device) idx_buffer_seq[cls_indices.flatten()] = -1 # 有效网格尺寸（避免越界）：当 h 或 w 不能被 sy/sx 完整整除时，按可覆盖区域截断 effective_h = min(hsy * sy, h) effective_w = min(wsx * sx, w) effective_grid_size = effective_h * effective_w if no_rand: # 非随机：每个网格只选择固定（左上）位置为 dst。因为没有调用，遂掠过 else: # 随机：在每个 (sy, sx) 子网格里随机选一个索引作为 dst total_other_imgs = num_imgs - 1 all_rand_idx = torch.randint( sy * sx, size=(total_other_imgs, hsy, wsx), device=metric.device, generator=generator, ) # scatter_src 为 -1，用于把被选中的网格位置标记为 dst scatter_src = -torch.ones( total_other_imgs, hsy, wsx, device=metric.device, dtype=torch.int64 ) # idx_buffer_batch 的最后一维大小为 sy*sx，代表每个网格内的所有位置； # 先在该维度上做 scatter，把随机选中的位置置为 -1（dst），其余保持 0（src） idx_buffer_batch = torch.zeros( total_other_imgs, hsy, wsx, sy * sx, device=metric.device, dtype=torch.int64, ) idx_buffer_batch.scatter_( dim=3, index=all_rand_idx.unsqueeze(-1), src=scatter_src.unsqueeze(-1), ) # 将每个网格的 sy*sx 展开成二维平面，并按 (H, W) 的顺序重排（transpose + reshape） idx_buffer_batch = ( idx_buffer_batch.view(total_other_imgs, hsy, wsx, sy, sx) .transpose(2, 3) .reshape(total_other_imgs, hsy * sy, wsx * sx) ) # 将每张图对应的二维标记平面（-1 或 0）批量写入到全局 idx_buffer_seq 中对应的 patch 区域 # 注意 grid_start 跳过了前 5 个特殊 token for i in range(total_other_imgs): img_idx = i + 1 grid_start = img_idx * tokens_per_img + 5 flat_view = idx_buffer_batch[ i, :effective_h, :effective_w ].flatten() idx_buffer_seq[grid_start : grid_start + effective_grid_size] = ( flat_view )说结论：如果要修改的话，可以仿照这一段的写法。可以是实现一个函数返回一个 indicies ，表示这些位置都需要设成 dst tokens。即现在代码的：scatter_src最后这一部分简单说一下几个不太会用的点：_scatter : 然后过度一下，拼接一下 dst token 和 src token，并且判断是否需要使用 protection 机制。最后记录 dst/src token 分别的数量。# 通过 argsort 将 -1（dst）排在前面，0（src）排在后面，得到两侧的索引集合 rand_idx = idx_buffer_seq.reshape(1, -1, 1).argsort(dim=1) num_dst_orig = int((idx_buffer_seq == -1).sum()) # 原始 src 与 dst 的索引切分（形状 [1, K, 1]） a_idx_orig = rand_idx[:, num_dst_orig:, :] b_idx_orig = rand_idx[:, :num_dst_orig, :] a_idx = a_idx_orig b_idx = b_idx_orig # 若启用保护，构造受保护索引的形状以适配 gather（[1, P, 1]） if enable_protection: protected_idx = protected_indices.unsqueeze(0).unsqueeze(-1) num_protected_actual = protected_idx.shape[1] else: protected_idx = None num_protected_actual = 0 # 记录两侧的数量，便于后续分割与 scatter/gather num_src = a_idx.shape[1] num_dst = b_idx.shape[1] # 内部辅助函数：根据 a_idx/b_idx/protected_idx 将输入 x 分割为 src/dst/(protected) def split(x): C = x.shape[-1] if enable_protection: src = gather(x, dim=1, index=a_idx.expand(B, num_src, C)) dst = gather(x, dim=1, index=b_idx.expand(B, num_dst, C)) protected = gather( x, dim=1, index=protected_idx.expand(B, num_protected_actual, C) ) return src, dst, protected else: src = gather(x, dim=1, index=a_idx.expand(B, num_src, C)) dst = gather(x, dim=1, index=b_idx.expand(B, num_dst, C)) return src, dst然后开始计算 src 和 dst token 之间的 similarity 以及对应的 dst token 的 index# 创建未初始化的张量用于存储每个 src 的最大相似度与对应的 dst 索引 node_max = torch.empty(B, num_src_actual, device=a.device, dtype=a.dtype) node_idx = torch.empty(B, num_src_actual, device=a.device, dtype=torch.long) # 将 dst 转置为 [B, C, num_dst]，用于 a 与 bmm 计算 b_transposed = b.transpose(-1, -2) # 分块计算每个 src 的最大相似度与 argmax dst node_max, node_idx = fast_similarity_chunks(a, b_transposed, chunk_size) # 按最大相似度由大到小排序，得到 src 的优先级队列（edge_idx） edge_idx = node_max.argsort(dim=-1, descending=True)[..., None]similarity 的代码等会儿讲。最后保存结果：如果要修改构图 or merging 的方式，则应该开始修改接下来这一段代码# 若启用保护：过滤掉属于保护集合的 src，保证这些 token 不被合并 if enable_protection: src_indices = a_idx[0, :, 0] protected_mask_src = torch.isin(src_indices, protected_indices) edge_flat = edge_idx[0, :, 0] # 仅保留未被保护的边 valid_mask = ~protected_mask_src[edge_flat] # 取反 valid_edges = edge_flat[valid_mask] valid_count = valid_edges.shape[0] r_actual = min(r, valid_count) # 根据 r_actual 切分：前 r_actual 为待合并 src，后面为未合并 src unm_idx = valid_edges[r_actual:].unsqueeze(0).unsqueeze(-1) src_idx = valid_edges[:r_actual].unsqueeze(0).unsqueeze(-1) else: # 无保护时，直接用 edge_idx 切分；前 r 为待合并 src，其余为未合并 src unm_idx = edge_idx[..., r:, :] src_idx = edge_idx[..., :r, :] r_actual = r # 为每个待合并的 src 查出它指向的 argmax dst 索引 dst_idx = gather(node_idx[..., None], dim=-2, index=src_idx) # 用 r_actual 回写 r，便于后续一致性（extra_tensors 同步） r = r_actual最终参与 merge 的 tensor注意到，这里实际上最后出来了三个存 token 的 tensor：src, umn, dst 如果 enable_protection = True，则还会有一个额外的 protected tensorsrc 对应要 merge 掉的 tensorumn 对应：当前的合并比例是 $r$，那么剩下的 $N \times (1 - r)$ 个 tensor 就是不合并的，存储在 umn 中dst 对应：src token 要合并到 dst token 上merge()函数声明# 定义合并函数：把选中的 src 汇聚到对应的 dst（默认 reduce=mean），并返回拼接结果 def merge( x: torch.Tensor, mode: str = "mean", extra_tensors=None, # 额外同步合并的 tensor，保证后续 q@k 的序列长度一致。 extra_tensors_2=None, # 额外同步合并的 tensor 2，同上 ) -> Union[ torch.Tensor, Tuple[torch.Tensor, torch.Tensor], Tuple[torch.Tensor, torch.Tensor, torch.Tensor], ]:Function call (in attention.py)：q_merge_in = q.permute(0, 2, 1, 3).reshape(B_q, N_q, H_q * D_q) k_merge_in = k.permute(0, 2, 1, 3).reshape(B_q, N_q, H_q * D_q) v_merge_in = v.permute(0, 2, 1, 3).reshape(B_q, N_q, H_q * D_q) q_out, k_out, v_out = m_a( q_merge_in, mode="mean", extra_tensors=k_merge_in, extra_tensors_2=v_merge_in, )具体实现# 按与 metric 相同的分割策略拆分 x if enable_protection: src, dst, protected = split(x) else: src, dst = split(x) n, t1, c = src.shape # 未合并的 src：根据 unm_idx 取出保留的 src 片段 unm_len = unm_idx.shape[1] unm = gather(src, dim=-2, index=unm_idx.expand(n, unm_len, c)) # 待合并的 src：根据 src_idx 选出前 r 个 src_len = src_idx.shape[1] src = gather(src, dim=-2, index=src_idx.expand(n, src_len, c)) # 将待合并的 src 聚合到对应的 dst；mode 默认为 mean，可改为 sum 等 dst = dst.scatter_reduce(-2, dst_idx.expand(n, src_len, c), src, reduce=mode)后面 extra_tensors / extra_tensors1 的流程是一样的。unmerge()# 先按顺序切分短序列：未合并 src（unm）、聚合后 dst、（可选）protected unm_len = unm_idx.shape[1] dst_len = num_dst src_len = src_idx.shape[1] unm = x[..., :unm_len, :] dst = x[..., unm_len : unm_len + dst_len, :] if enable_protection: protected = x[ ..., unm_len + dst_len : unm_len + dst_len + num_protected_actual, : ] # 用 dst 的聚合结果反向还原被合并的 src：根据 dst_idx 把 dst 拷回合适位置 _, _, c = unm.shape src = gather(dst, dim=-2, index=dst_idx.expand(B, src_len, c)) # 准备完整输出，并逐块 scatter 到原始索引位置 out = torch.zeros(B, N, c, device=x.device, dtype=x.dtype) # 1) 把 dst 放回原来的 b_idx（目标侧）位置 out.scatter_(dim=-2, index=b_idx.expand(B, num_dst, c), src=dst) # 2) 把未合并的 src 放回 a_idx 对应的 unm_idx 位置 out.scatter_( dim=-2, index=gather( a_idx.expand(B, a_idx.shape[1], 1), dim=1, index=unm_idx ).expand(B, unm_len, c), src=unm, ) # 3) 把被合并的 src（从 dst 反向恢复出来的片段）放回 a_idx 对应的 src_idx 位置 out.scatter_( dim=-2, index=gather( a_idx.expand(B, a_idx.shape[1], 1), dim=1, index=src_idx ).expand(B, src_len, c), src=src, ) # 4) 若启用保护，将 protected 片段 scatter 回 protected_idx 对应位置 if enable_protection: out.scatter_( dim=-2, index=protected_idx.expand(B, num_protected_actual, c), src=protected, ) return outfast_similarity_chunks()函数声明def fast_similarity_chunks( a: torch.Tensor, b_transposed: torch.Tensor, chunk_size: int ) -> Tuple[torch.Tensor, torch.Tensor]:Function call:node_max, node_idx = fast_similarity_chunks(a, b_transposed, chunk_size)具体实现注：torch.bmm 的全称是 batch matrix-matrix product，也就是 批量矩阵乘法。它一次性对一批（batch）里的多个矩阵做乘法运算。输入：两个三维张量 input 和 mat2形状分别是 (b, n, m) 和 (b, m, p)输出：三维张量，形状是 (b, n, p)其中 b 表示 batch size，也就是一批里有多少个矩阵要相乘。 每个 batch 内对应位置的矩阵会被相乘。核心代码：# Process in chunks for i in range(0, num_src, chunk_size): end_i = min(i + chunk_size, num_src) a_chunk = a_bf16[:, i:end_i, :] # [B, chunk_size, C] scores_chunk = torch.bmm(a_chunk, b_transposed_bf16) # 由于外层已对 a、b 做 L2 归一化，点积近似余弦相似度 chunk_max_bf16, chunk_idx = torch.max(scores_chunk, dim=2) # 在每个 src 行上找出最大值及其列索引（对应某个 dst chunk_max = chunk_max_bf16.to(original_dtype) node_max[:, i:end_i] = chunk_max node_idx[:, i:end_i] = chunk_idx return node_max, node_idx总结如果考虑要修改 protected token 的代码，那么所有 enable_protection 的代码都应该被考虑修改...
Deep Generative Models - 2: Diffusion Models
总结Diffusion 的操作其实非常简单，核心算法的框架伪代码如下图所示：其中 Training 的部分，主要包含了 Add Noise 这个步骤（Forward Process）。每次我们选取一张图 $x_0$ （真值），一个 $[1, T]$ 的数 $t$，和一个高斯噪声 $\epsilon \sim \mathcal{N}(0, I)$ 。每次把高斯噪声 $\epsilon$ 加给 $x_0$ 的步骤就是 add noise。一个比较精巧（奇妙）的操作时这个 $t$。当然，如果从 Autoregressive 的角度来看待 Diffusion，这个 $t$ 是非常合理的。在 Sampling 部分，主要是包含了一个 Denoise 的步骤 （Reverse Process）。其中，一个用以预测整体噪声的网络 $\epsilon_{\theta}$ 是我们优化的目标。$\epsilon_{\theta}$ 要生成在 Training 阶段对应位置加过噪声的图片中，真实的噪声分布。我们不可以把 Denoise 简单的看成是一个 add noise 的逆过程。原因有两点：Denoise 过程中消去的噪声是 $\epsilon_{\theta}$ 的预测值而非真实值。注意到 Sampling 阶段的末尾加上了一个神秘 $\sigma_t\mathbf{z}$。这个东西非常有说法。在这里我想提供李宏毅老师课中的一个解释。以 GPT 为例，如果预测的下一个位置永远是 Confidence 最高的 candidate，那么生成出来的文本很有可能是一段不断重复一个意思的段落。因此，LLM 中也会类似地加上一个这样的随机噪声。同理可得，如果没有在 denoise 阶段加上这个随机噪声，那么我们生成的图像可能是一张单一颜色的图像。因此对于 denoise 过程，我目前的理解是：由于真实的图像的噪声可必然是一个比我们目前假设的噪声分布——即高斯分布，更加复杂的分布。 因此，Diffusion 的 denoise 实际上是在产生一个其噪声符合一个低级（简单）分布的图像。Add NoiseDefinition在 DDPM（Denoising Diffusion Probabilistic Models）中，前向过程定义为一个 马尔科夫链：$$ q(x_t|x_{t-1}) = \mathcal{N}(x_t; \sqrt{1 - \beta_t} x_{t-1}, \beta_t\mathbf{I}) $$其中：$\beta_t$ 是一个噪声方差的超参数（通常很小，且逐渐变小）条件分布是高斯分布，均值缩小原输入，方差是 $\beta_t$也就是说，每次我们加上去一个高斯噪声 $\epsilon \sim \mathcal{N}(0, \mathbf{I})$ 给 $x_{t}$:$$ x_t = \sqrt{1 - \beta_t} x_{t - 1} + \beta_t\epsilon $$从这个式子我们可以看出：均值：$\sqrt{1 - \beta_t} x_{t - 1}$，表示对输入的逐步衰减。方差: $\beta_t$ ，表示每一步加的高斯噪声强度。这样设计的好处是：整个过程可以解析化地推出任意 $t$ 时刻的分布：$$ q(x_t|x_0) = \mathcal{N}(\sqrt{\bar{\alpha_t}}x_0, (1 - \bar{\alpha_t})\mathbf{I}) $$其中：$\bar{\alpha_t} = \prod_{i = 1}^{t} (1 - \beta_i)$ 这个原理是说，假如我们已知 $x_0$ （即输入的图像），由于我们整个 add noise 的过程是一个线性高斯马尔科夫链，假如我们用 $x_1: x_{T}$ 来表示中间步的结果，那么：$$ q(x_1:x_T|x_0) = q(x_1|x_0) q(x_2|x_1) \dots q(x_T|x_{T- 1}) $$因此，我们将这个式子的 $x_i, i \in [0, T)$ 拆出来之后，就发现 $\alpha_i = \sqrt{1 - \beta_1}\sqrt{1 - \beta_2}\cdots\sqrt{1 - \beta_t}$Scheduler在上文中我们提到了 $t \sim \text{Uniform} (\{1, \dots, T\})$ ，这里的 $t$ 是一个时间步。$t$ 对于 Diffusion models 非常关键，因为其控制随时间演化噪声的分配方式。常见的 schedule 有：LinearCosine （噪声增加得更平滑）自适应 schedule（后来改进的，基于信噪比设计）这里给出一个为什么 schedule 关键的解释。schedule 使得：不同时间步的信噪比分布合理；模型能在全程学到有用的去噪任务；反向采样过程稳定，最终生成质量高。Reverse ProcessDefinition我们现在的目标是要学习一个反向马尔科夫链：$$ p_{\theta}(x_{t-1}|x_t), t = T, T - 1, \dots, 1 $$使得：$$ p_{\theta}(x_{0:T}) = p(x_T) \prod_{t=1}^{T} p_{\theta}(x_{t-1}|x_t) $$其中，$p(x_T) \sim \mathcal{N}(0, \mathbf{I})$我们有 Forward Process 的式子（线性高斯马尔可夫链），我的理解是：相当于一个有先验概率得后验概率。那么 Reverse Process 就是一个 Bayes 公式（因为我们是要从 $x_{t}$ 推导 $x_{t-1}$。那么有：$$ q(x_{t - 1} | x_t, x_0) = \mathcal{N}(x_{t-1}; \tilde{\mu_t}(x_t, x_0), \tilde{\beta_t}\mathbf{I}) $$其中：$$ \tilde{\mu_t} = \frac{\sqrt{\alpha_t}(1 - \bar{\alpha_{t-1}})}{1 - \bar{\alpha_t}} x_t + \frac{\sqrt{\bar{\alpha_{t-1}}} \beta_t}{1 - \bar{\alpha_t}}x_0 $$$$ \tilde{\beta_t} = \frac{1 - \bar{\alpha_{t-1}}}{1 - \bar{\alpha_{t}}} \beta_t $$这里面大概是涉及多个高斯分布的联合分布的推导，具体不展开了。比较神秘。DDPM 的附录里面有提到。在此不多赘述。解释：$\tilde{\mu}_t$ 是反向高斯分布的均值，由当前 noisy 样本 $x_t$ 和真实干净样本 $x_0$ 决定。$\tilde{\beta}_t$ 是方差，依赖于 schedule。问题是：反向过程需要 $x_0$，但生成时我们并不知道 $x_0$。Learn a Distribution因此，Diffusion 会训练一个用以预测整体噪声的网络 $\epsilon_{\theta}(x_t, t)$，这里就再次体现了时间步 $t$ 的重要性。因为 $t$ 是被当做参数一起传入 $\epsilon_{\theta}$ 中的。根据 Forward Process ，移项可得：$$ x_0 \approx \frac{1}{\sqrt{\bar{\alpha_t}}} (x_t - \sqrt{1 - \bar{\alpha_t}}\epsilon_{\theta}(x_t, t)) $$再把这个 $x_0$ 代入 $\tilde{\mu_t}$ 中，就得到了反向分布的近似均值：$$ \mu_{\theta}(x_t, t) = \frac{1}{\sqrt{\alpha_t}} \left(x_t - \frac{1 - \alpha_t}{\sqrt{1 - \bar{\alpha_t}}} \epsilon_{\theta}(x_t, t) \right) $$因此，最终的 $p_{\theta}(x_{t-1}|x_t)$ 就可以表示为：$$ p_{\theta}(x_{t-1}|x_t) = \mathcal{N} (x_{t-1}; \mu_{\theta}(x_t, t), \tilde{\beta}_t\mathbf{I}) $$Train Loss论文中有提到如何转换成 ELBO 形式。由于过于复杂，中间的主要步骤是通过类似于累乘过程中分母分子抵消来花间，这里不过多赘述。这里主要展示最后的结果：$$ \mathcal{L} = \mathbb{E}_{x_0, t, \epsilon} \left[ || \epsilon - \epsilon_{\theta} (x_t, t) ||^2\right] $$
Deep Generative Models - 1: Variational Autoencoder (VAE)
前言乱扯几句，最近发现 Anything + 生成式 还是有许多工作可以做的，算是一片蓝海吧。虽然感觉自己做什么都比别人慢几拍，前两年追热点死活追不上，不过也不能完全这么说，因为刚进大学时还屁都不懂呢。3DGS是2023年的文章，Diffusion 甚至比 GPT 要早一年（2022）就在学术圈火爆了。离开学还有一段时间，打算慢慢补一些生成式相关的知识。遂写下一些笔记，纯当是自己在这个世界存在的证明。Motivation如果我们有一个这样的 Data Generation 的过程，其中我们给 Generator 的输入是一个 Latent Vector （Embedding） $z$，最终输出的是图像/视频/音频之类的数据，记为 $x$Process假设：给定的所有训练数据 $x$ 构成的分布记为 $p_{data}$所有的 Latent Vectors $z_i$ 构成的分布为 $q(z)$，即：$z_i \sim q(z)$，其中 $p(z_i)$ 指的是从这个分布中 sample 到 $z_i$ 的概率密度。最终的生成结果 $x' = p_{\theta}(x|z)$记所有的 $x'$ 构成的分布为 $p_{\theta}(x)$，其中 $\theta$ 为 Decoder 的 Learnable Parameters我们最终的训练目标，是 $p_{\theta}$ 接近 $p_{data}$ 的分布。我们希望用一个量化的指标来衡量两个分布的接近程度，此处选择 KL-DivergenceQuestion: 是否有别的 Metrics?形式化地，我们的目标为：$$ \min_{\theta} D_{KL}(P_{data} || P_{\theta}) $$我们的训练目标 $\theta$ 即为：$$ \begin{align} & \arg\min_{\theta} D_{KL}(P_{data} || P_{\theta}) \\ = & \arg\min_{\theta} \sum_{x} P_{data}(x) \log(\frac{P_{data}(x)}{P_{\theta}(x)}) \\ = & \arg\min_{\theta} \sum_{x} -P_{data}(x) \log P_{\theta}(x) + C \\ = & \arg\max_{\theta} \sum_{x} \log P_{\theta}(x) \\ = & \arg\max_{\theta} \mathbb{E}_{x \sum P_{data}(x)} \log P_{\theta}(x) \end{align} $$其中，很明显 $P_{data}$ 是我们已知的（训练集已知），因此完全不与 $P_{\theta}$ 相关的项都写成一个常数 $C$。进一步地，由于我们此处只关心 $\theta$ ，因此 $-P_{data}(x)$ 也可以视为一个常系数。现在我们只关心 $P_{\theta}(x)$，由于其全部由条件概率 $P_{\theta}(x|z)$ 给出，由全概率公式：$$ P_{\theta}(x) = \int_z P_{\theta}(x|z)P(z)\mathrm{d}z $$回到主题，我们现在的麻烦是计算 $\log P_{\theta}(x)$，我们有：$$ \begin{align} \log P_{\theta}(x) & = \int_z \log P_{\theta}(x) q(z) \mathrm{d}z \\ & = \int_z \log \left( \frac{P_{\theta}(x|z)P_{\theta}(z)}{P_{\theta}(x)} \right) q(z) \mathrm{d}z \\ & = \int_z \log \left( \frac{P_{\theta}(x|z)P_{\theta}(z)}{P_{\theta}(x)} \cdot \frac{q(z)}{q(z)}\right) q(z) \mathrm{d}z \\ & = \int_z \left( \log P_{\theta}(x|z)+ \log(\frac{P_{\theta}(z)}{q(z)}) + \log(\frac{q(z)}{P_{\theta}(z|x)}) \right) q(z) \mathrm{d}z \\ & = \mathbb{E}_{z \sim q(z)} \left[ \log P_{\theta}(x|z) \right] - D_{KL}\left(q(z) || P_{\theta}(z)\right) + D_{KL}\left( q(z) || P_{\theta}(z|x) \right) \end{align} $$注意第三步用到了一个很经典的 $\cdot 1$ 的技巧，此处是为了避免拆 log 的时候相关变量无法分离（导致 +/- 和括号同时产生的大混乱情况）+ 与外面的 $q(z)$ 写成 KL-散度。我们称 Evidence Lower Bound：$$ RHS = \text{ELBO} = \mathbb{E}_{z \sim q(z)} \left[ \log P_{\theta}(x|z) \right] - D_{KL}\left(q(z) || P_{\theta}(z)\right) $$原因很显然，最后一项通过移项到 LHS 后显然会出现一个 $\log P_{\theta}(x)$ 的下界的形式：$$ LHS =\log P_{\theta}(x) -D_{KL}\left( q(z) || P_{\theta}(z|x) \right) $$观察到：LHS 中的 $\log P_{\theta}(x)$ 显然是无法计算的（不然根本没有这么些步骤），$P_{\theta}(z|x)$ 这个后验概率的分布显然也是无法表示的。那么希望寄托在 ELBO 身上。此外，另一个关键的问题是，$z$ 作为一个 Embedding 具有以下特征：$z$ 会是一个高维向量$q(z)$ 无法量化计算因此，我们引入了 Latent Variable Models，也就是变分自编码器的由来。既然我们无法确定一个“真”的 $q(z)$，如果我们可以让一个 $q_{\phi}(z|x)$ 输出一个“可控的”结果作为 $q(z)$ 的 PDF 的值，那么这个结果是可以接受的。我们可以将期望项视作一个 Reconstruction Loss由于我们将 $q(z)$ 认为是可以近似为一个 Learnable Distribution $q_{\phi}(z|x)$ ，其中 $\phi$ 是学习的参数，那么后一项可以视为一个 Regularization Loss。其鼓励 $\phi$ 导出的网络表示的分布与真实的 $q(z)$ 越来越接近。至此，整个流程就结束了。下面我们来具体关注这两个 Loss 的实现细节。由于 Reconstruction Loss 和 Regularization Loss 的方法是一样的，下面以 Reconstruction Loss 为例。我们关注的是：$$ - \mathbb{E}_{z \sim q(z)} \left[ \log P_{\theta}(x|z) \right] $$我们现在已经用一个易采样的近似分布：$q_{\phi}(z|x)$ 了，这里的 $\phi$ 导出的网络通常会输出分布的参数，比如说是 Gaussian 的话，那么就输出 $\mu$ 和 $\sigma$：$$ q_{\phi}(z|x) = \mathcal{N}(\mu_{\phi}(x), \sigma^2_{\phi}(x)I) $$这样的好处是：我们可以直接采样 $z^{(i)} \sim q_{\phi}(z|x)$采到的 $z$ 可以直接输入生成网络 $P_\theta$ 去算概率由此，这个期望就可以用 One-Step Monte Carlo 法计算：采样 $z^{(i)}$用生成网络 $P_{\theta}$ 计算 $\log (P_{\theta}(x, z^{(i)}))$：由于：$P(x, z^{(i)}) = P(x) \cdot P(z^{(i)})$再计算 $\log q_{\phi} (z^{(i)}|x)$用平均值近似期望现在的问题是，由于 $\phi$ 导出的分布参数是抖动的（即 $z$ 是取决于 $\mu_{\phi}$ 和 $\sigma_{\phi}$ 生成的），但这个采样操作里有噪声，计算梯度时不能直接对采样过程求导，反向传播会断掉。这里我们就要用到一个技巧：重参数化。具体地，把采样过程重写为“可导的确定性变换 + 独立噪声”：先采一个与参数无关的随机变量：$\epsilon \sim \mathcal{N}(0, I)$再用可导的线性变换得到 $z$：$$ z = \mu_{\phi}(x) + \sigma_{\phi}(x) \odot \epsilon $$这样的话：随机性来自于 $\epsilon$$\mu_{\phi}$ 和 $\sigma_{\phi}$ 直接参与计算 $z$，且过程可导反向传播时，梯度可以从$ z$ 传到 $\mu_{\phi}$ 和 $\sigma_{\phi}$ ，从而更新 encoder 参数现在得到 $z$ 以后，我们假设 $P_{\theta}(x|z)$ 是一个 Gaussian 分布：$$ P_{\theta}(x|z) = \mathcal{N}(x | \mu_{\theta}(z), \sigma^2I) $$接下来，使用负对数似然 (Negative Log-Likelihood, NLL) 作为损失。在 Gaussian 分布的这个假设下又有：$$ - \log P_{\theta}(x|z) = \frac{1}{2\sigma^2} ||x - \mu_{\theta}(z)||^2 + C $$这等价于 L2 Loss，常数项不影响优化。
Background Knowledge for Neural Video Compression
不同帧类型在视频编码中，I帧、P帧和B帧是三种不同类型的帧，每种类型的帧在视频压缩中扮演着不同的角色。下面是对这三种帧的详细解释：I帧（Intra帧或I-frame）：I帧是独立帧，因为在编码时，它不需要参考其他帧。I帧包含完整的视频帧信息，可以独立进行解码和显示。I帧通常用于关键帧对齐，以便在视频流中断或解码器重启动时能够恢复视频的正确播放。解码时，仅需要I帧自身的数据即可恢复完整的图像。P帧（Predictive帧或P-frame）：P帧是预测帧，需要参考前一或前几帧来解码。P帧通过运动补偿来压缩，利用前一帧或前几帧的像素数据来推算当前帧的内容。P帧减少了需要存储或传输的数据量，因为它们只包含运动矢量信息和残差信息。解码时，需要前一帧或前几帧的数据来恢复当前帧。B帧（Bidirectional帧或B-frame）：B帧是双向预测帧，可以参考前一帧和后一帧来解码。B帧同样通过运动补偿来压缩，但它使用了参考帧的多重信息来优化压缩效率。B帧提供了最高效的压缩，因为它们可以利用前后参考帧的更多信息。解码时，需要前一帧和后一帧的数据来恢复当前帧。示例解释I帧：包含完整的帧信息，不需要参考其他帧。P帧：通过运动补偿参考前一帧来压缩。B帧：通过运动补偿参考前一帧和后一帧来压缩。实际应用I帧：通常在视频编码中的关键位置放置I帧，以便解码器能够在需要时迅速恢复视频内容。P帧和B帧：用于在视频流中高效压缩信息，减少带宽和存储需求。例子假设有三帧的视频序列：帧1是I帧，帧2是P帧，帧3是B帧。帧1（I帧）：包含完整的帧信息。帧2（P帧）：只包含运动矢量和残差信息，需要参考帧1。帧3（B帧）：既包含参考前一帧（帧2）的信息，也包含参考下一帧（理论上是后续帧）的信息。通过这种方式，I帧、P帧和B帧共同协作，有效实现了视频压缩并保持了视频质量。编码配置在视频压缩中，AI、LD 和 RA 是三种常见的编码配置（Configuration），主要用于定义视频编码的帧间预测结构和应用场景。它们常见于 H.265/HEVC、H.264/AVC 等标准中，以下是它们的详细解释：1. AI（All Intra，全帧内编码）全称：All Intra特点：所有帧均为 I 帧（关键帧），不依赖其他帧进行预测（仅通过帧内预测压缩）。优点：最高图像质量，适合后期编辑（每帧独立可解码）。随机访问能力强（任何帧均可作为起点）。缺点：压缩效率最低，文件体积最大（无帧间预测）。应用场景：专业视频制作（如电影母版、蓝光光盘）。需要逐帧精确编辑的场景（如影视后期）。2. LD（Low Delay，低延迟模式）全称：Low Delay特点：使用 IPPP 结构（I 帧 + 前向预测的 P 帧），无 B 帧（双向预测帧）。编码顺序与播放顺序一致，避免缓冲区延迟。优点：极低编码和解码延迟，适合实时传输。比 AI 压缩率更高（利用 P 帧预测）。缺点：压缩效率仍低于 RA（无 B 帧优化）。应用场景：实时视频通信（如视频会议、直播）。需要端到端低延迟的交互场景。3. RA（Random Access，随机访问模式）全称：Random Access特点：使用 分层 B 帧结构（如 I-B-B-P 或 GOP 结构），允许随机访问点（如定期插入 IDR 帧）。支持双向预测（B 帧）提升压缩效率。优点：压缩效率高（利用 B 帧和长时预测）。支持随机访问（用户可从任意 GOP 起点开始解码）。缺点：解码延迟较高（需缓存多帧处理 B 帧）。应用场景：流媒体点播（如 Netflix、YouTube）。需要快进/快退的视频服务。对比总结配置帧类型延迟压缩率典型场景AI全 I 帧高最低专业制作、后期编辑LDI/P 帧，无 B 帧最低中等实时通信、直播RAI/P/B 帧中等最高流媒体点播、存储扩展知识GOP（Group of Pictures）：RA 配置中通常定义 GOP 长度（如 1 秒内的帧组），影响随机访问的频率。B 帧：双向预测帧，参考前后帧，提升压缩率但增加计算复杂度。IDR 帧：一种特殊的 I 帧，标记 GOP 的起点，确保解码器可从此处开始独立解码。根据需求选择合适的配置，可平衡视频质量、延迟和文件大小。例如，直播选 LD，存档选 AI，流媒体选 RA。
Is common U-Net proper for regression in segmentation task?
小结近期的工作主要是想把在 3D Vision 中很火的 Signed Distance Function 引入到 Medical Image Segmentation 中。不意外的当然是，已经有一些前人做了相关工作。然而，这些工作许多发表在期刊上，TMI 这样的顶刊甚至不占少数。发表在期刊上就意味着许多代码是不开源的。经过这一段时间的实验复刻，发现许多文章实际上无法达到标称的效果（声明：也许是我技术不过关）因此，我开始反思 U-Net 这样一个 feature extraction 用处的结构，到底适不适合用以 regression？因为实际上，这些工作并没有解决 SDM 的计算问题，而是训练 U-Net 输出与真实 SDM 接近的理想模型。Is common U-Net proper for regression task.pdf
Research report on Semi-supervised Medical Image Segmentation-20241124
AbstractSigned Distance Funtion 用来处理边界的问题是相当好的。虽然之前已经有研究者尝试引入医疗图像分割了，但是都比较古早，或者浅尝即止了。本次分享主要对 SDF 做了一个简单介绍，回顾了SDF在半监督医疗图像分割领域的应用。组会分享课件组会分享课件
Leveraging CORAL-Correlation Consistency Network for Semi-Supervised Left Atrium MRI Segmentation
前言这是我的第一篇工作，开始于 2024 年 1 月 4 日。当时苏老师喊我过去开组会，中午和师兄师姐一起吃了午饭（吃白食当然是很开心），走的时候苏老师让我下午一点去办公室。苏老师说，3月8日有一个 MICCAI 的 DDL，让我冲一冲赶一赶。当时我还不知道什么是 MICCAI。当时我还刚刚看完 UMich eecs 498-007 / 598-005: Deep Learning for Computer Vision 的 CNN。心里很慌，但又感到十分的激动。决定挑战自己一下，就答应了下来。当时距离 DDL 大概是两个月左右，先花了两周的时间迅速过完了那门课的所有内容。然后开始着手于选题。现在回过头来看，真得好好感谢当时的自己。选择>>>>>>>>>>>努力。具体来说，我当时的方法论是这样的：第一次做，不选新任务做，选成熟的任务做。这样之前的工作多，可供参考的文章、代码都丰富。尽量选择比较简单的 Backbone/Framework。对于第一个问题，我选择了 Segmentation 这个任务来做。对于第二个问题，很幸运的是时至今日医疗图像分割的主流还是 U-Net ，一个以 CNN 为基础的模型。因为当时对科研的流程是什么样的完全不了解，所以纯粹是无头苍蝇到处乱创。所以又有了方法论3。其实不是我的方法论，是雷神的：世界上 $99\%$ 的问题都有标准答案，找个懂的人问问。所以当时我找到了我的师兄润林，和系里技术最最最最好的伍导求教。当时苏老师给的建议是，如果实在是找不到创新点，就看图有什么缺陷。但是一个问题是，当时我基本没有怎么看过文章，也没有见到过真的 MRI，所以盯帧也看不出个所以然出来。结论是，还是从自己熟悉的领域出发。于是，我决定先看几篇文章，从算法的缺陷上突破。因为当时看文章速度太慢了，所以这篇文章初期开工的时候，我其实只看过两篇文章。一篇是 MICCAI 的 SS-Net，还有一篇也是 MICCAI 的 CAML。这就不得不感谢一些当时我的运气了，这两篇文章有用相同的数据集做测试。于是乎，先找苏老师要了卡（哎哟给我爽到了，两张 A100 轮上寒假大家工作强度不高的时候天天独享），把代码数据集弄下来，有的地方还跑不通。魔改了两三天终于是通了，有一些组件因为当时还不知道怎么配环境，所以有一些组件是手写的（乐）。有的觉得源代码实现的比较糟糕的地方，被我自己实现了一下。最后训练出来直接涨点了（难绷）。后来就是花了一周的时间构思了一些创新点（都写在 Paper 里面了）。整个过年都在做实验（大年夜还在让卡加班真是辛苦它了）。想起来，现在的自己也没有，可能以后也没有这样的激情在大年夜跑实验了。等哪一天我有顶会的水平了，可能会有吧。接下来就是比较痛苦的一个环节，写 Paper。因为完全没实战过 Academic Writing，写作水平不佳。我当时写的是 Method 部分，因为这一部分的~故事性~ 不强，主打的是把方法讲清楚。所以基本对着别的文章讲方法的流程讲了一遍。好不容易花了一周把 Paper 写完了，交给苏老师看，结果是基本要大修。从标题到摘要，以及 Introduction ，全部都得推倒重来。因为当时不知道串联的两个组件应该怎么做消融实验，所以当时的文章是没有消融实验的（这也为第一次投稿被拒绝埋下了很大的伏笔）。最后是换了一种方法讲故事，前后改了大概15版，在 DDL 前交了上去。第一次投稿: MICCAI第一次投稿还是挺紧张的，生怕出什么差错。在DDL前的两个小时还在 T7 的实验室跟苏老师伍导润林师兄讨论最后的版本。最后在 DDL 前的 15 分钟交稿了。后面就是一个多月左右的等待，收到进入 Rebuttal 的通知。其实这个项目能做到这里我已经很满意了，因为我知道自己的工作，姑且不论工作的质量是不是令人满意的，至少一些实验、写作说明、论证都是不怎么完善的。MICCAI 审稿意见和 Rebuttal 结果现在是 2024 年 11 月 16 日 突然发现自己其实更博听不勤快的，大家可以时不时可以催催我。博客现在也是一个挺小众的方式了，如果能收到陌生人的催更邮件我肯定会回复的（不保证更新）。话归正题，当时的 Rebuttal 其实面临了这篇文章当时的几大核心缺陷：叙事：本身效果并没有得到广泛的验证。叙事有一点点过于宏大，导致卖点不突出，信服度也不强。量化实验：投 MICCAI 的时候，我们没有说注重于 Left Atrium，然而实验又只在 LA 数据集上做（这我就要点名批评 CAML 和他的审稿人了，凭什么他们也说自己是算法，也只在 LA 上做的实验，就给中了），但是论文像是在提出一种新的算法。所以有效性被质疑了。消融实验：当时觉得串联的不好做消融实验，干脆是没做还是把 CORAL 换成了 Cosine Similarity 来着。后患无穷，反正是被狠狠质疑了。综上，现在回过头来看，这篇文章能进到 MICCAI 的 Rebuttal 都是一个奇迹了。硬伤太多了，真没办法。所以当时的 Rebuttal 简直跟跑打差不多。反正就是你审稿人说我这里有问题，我就硬说没有。失败自然也是必然的。但是在这里还是要十分感谢苏老师和我们的团队，真的给了我很多帮助。可以说这篇文章 $90\%$ 的功劳都是他们的此外，第一次投稿就遇到了逆天审稿人。哪怕文章当时写的不好，但是超参数的设置我是一五一十的写在了 Experiments 里。第二个审稿人还在问:What is the temperature?就一个 Reject 还是他给的，confidence 分数还给自己贼高。就一纯纯普信男，真不想多说什么。本科生审稿亮点不会抓只喜欢看天气预报是吧。后来就是一直懒惰+期末考试快来了，磨磨蹭蹭就来到了六月份出结果。当然是喜提一个 Reject！人生里程碑，达成！第二次投稿：BIBM哎呀呀，当时 ccfddl 上的接受率太吓人了，搞得我不敢投。当时我想投灌水小将宝藏会议 ICASSP。你看这诱人的接受率，能不心动吗？ 唯一可以蛐蛐的点在于使在印度开会吧，可能要自备泡面 or 肠胃药。 但是当时苏老师和朱老师都建议我投 BIBM。但是 BIBM 的接受率只有 $21\%$ 的样子，当时不知道没算 Short Paper，所以不敢投。实际上，当时我的跑步进入 PhD 的方案是这样的：6.14 MICCAI 出结果：中了，皆大欢喜；没中：6.18 BIBM 第一轮投稿8.15 BIBM 第一轮出结果：中了，皆大欢喜；没中：8.16 AAAI 投稿当时正值 Mamba 一炮当红之际，各行各业灌水小将都在涌入这条赛道。当时我也一头扎进了 Mamba 的赛道，同一时期群友 yaw （21AI，我们的 NUS 生信大师兄）也在做 Graph Mamba 相关的工作。大概 6.14 晚上接近通宵，基本设计了一个新的联合 Mamba 的模型。然后睡了三四个钟，第二天继续激情的战斗。大概是晚上的时候把代码跑通了开始做实验。我记得当时我改的是 Mamba U-Net （港科广的那一篇在 MICCAI 2024 上的，他们被 Early Accept 了，果然手速快就是好）。好景不长，发现加了 Mamba 还降点了！wdnmd，然后开始坚信是超参数没有调好的原因。并且尝试了大概 $3 \times 4 \times 4 = 48$ 种不同的 seed/Lambda(温控参数)/Loss Weight Parameter/Epoch Number(因为出现了梯度消失) 。最后在 6.17 承认失败。妙哉，第一次跑步进入 PhD 失败了。然后开始了暑假漫长的咕咕咕，直到：然后开始了长达半个月的修改论文。期间苏老师至少给我改了十多版，在这里给他磕个头。真的无敌感动！最后就交了，检查好格式。开始等待。终于等到中稿还记得那天是周一，10.15，下午正在上无聊的 DCN 的 Lab。正好是课間，手表震了一下。然后在 Mac 上看到了那封 Short Paper Accepted 的邮件。激动的喊了一声”中了！“，范进中举似的。就此，完结。Paper with CodePaper 已经准备上 arxiv 了，预计下周一上线。正式版要等12月。Code 已经开源：Repository for CORN先挂到 Arxiv 上了：Paper (Arxiv)
这个学期也想试试多模态的一些任务了，打算从比较熟悉，框架也还比较简单的多模态图像融合任务开始。最开始的想法是做医学图像多模态融合。但是后来发现这个领域医学图像跟自然图像其实差别不大。如果从自然图像开始讲故事还有可能冲击一下 CVPR/AAAI/MM 这样的会议吧。万一中了呢。此外，在这里想锐评一下 CVPR2024 这篇 EMMA，简直是完全不科学。原因是：直接提出一个假设，然后整篇文章的方法和实验都基于这个假设，最后做出来实验效果好，就说自己提出的假设是对的。诶，这个实在还是有点扯了。简直就像，我假设我正式考试的时候有标准答案，所以我模拟考试的时候都对着标准答案写，最后做出来实验效果好。简直是扯犊子。组会汇报
PLAN for Novel Few-shot Segmentation Method for Medical Image
log2024.6.1023:51 SegMamba 量化实验测试plan2024.6.11检查 SegMamba 量化实验测试阅读 SegMamba 代码，并给出详细注释阅读 CAT-Net 代码，并给出详细注释阅读 VTM 代码，并给出详细注释初步构思代码实现方案2024.6.12 - 6.14确定模型框架图，简单用 PowerPoint 绘制requirement阅读代码Think Twice, Code Once感觉之前的代码都是囫囵吞枣式的写法，写不会就 GPT，错了也要靠 GPT。这样效率很低，而且并不能提升自己。这一次的阅读代码，基本要求是至少写一份详细的代码报告。要求阐述以下内容：代码框架：各部分作用代码细节：如：各种 size，哪个部分的输入是什么？输出是什么？怎么实现的？
主要介绍了 UNet 架构下，Mamba 的在 Medical Image Segmentation 的应用。建议重点关注 Mamba Block 的设计。组会