Cosmo's Blog

Back

Record#

https://github.com/Cosmo-klara/Qwen_R 可以获取本周的代码变动

2025-12-24#

  • 核验 longvale 代码,确认其在训练是使用单轮对话多轮对话混合训练,在评估时仅使用单轮对话。

  • 重新修改为单轮对话训练,训练数据长度如下:

print(len(train_dataset))
# 75075 ( 如果是多轮对话,长度 = 视频数 7240)
py

这里不同于给出的 91,863 events 是因为在训练数据中存在 captioning 的任务(一个 A 中会包含多个事件)

  • 进行一些 baseline 中对于 Qwen2.5-omni的修改的合理性的验证

    总的来说调整的地方如下:

    在 bulid 对话的时候添加参数 max_frames,这样在经过 process_mm_info 时会自动限制最大帧数目为 50 帧。

    {"type": "video", "video": sample["video_path"], "max_frames": 50},
    py
    ========== Omni Batch Debug ==========
    [Text]
    input_ids: torch.Size([1, 8153])
    attention_mask: torch.Size([1, 8153])
    labels: torch.Size([1, 8153])
    label tokens: 14
    
    [Video]
    pixel_values_videos: torch.Size([6400, 1176])
    dtype: torch.float32
    video_grid_thw: tensor([[25, 16, 16]])
    video tensor size: 28.71 MB
    
    [Audio]
    input_features: torch.Size([1, 128, 30000]), 14.65 MB
    =====================================
    
    
    [SYSTEM]
    "\nYou are Qwen, a virtual human developed by the Qwen Team, Alibaba Group, capab..."
    "\n"
    
    [USER]
    "\n"
    <|vision_bos|>
    ├─ VIDEO × 128
    ├─ AUDIO × 50
    ├─ VIDEO × 128
    ├─ AUDIO × 50
    ├─ VIDEO × 128
    ├─ AUDIO × 50
    ├─ VIDEO × 128
    ├─ AUDIO × 50
    ├─ VIDEO × 128
    ├─ AUDIO × 50
    ├─ VIDEO × 128
    ├─ AUDIO × 50
    ├─ VIDEO × 128
    ├─ AUDIO × 50
    ├─ VIDEO × 128
    ├─ AUDIO × 50
    ├─ VIDEO × 128
    ├─ AUDIO × 50
    ├─ VIDEO × 128
    ├─ AUDIO × 50
    ├─ VIDEO × 128
    ├─ AUDIO × 50
    ├─ VIDEO × 128
    ├─ AUDIO × 50
    ├─ VIDEO × 64
    ├─ AUDIO × 5822
    <|audio_eos|>
    "<video>\nDuring which frames in the video can we observe someone lifts a freshly..."
    "\n"
    
    [ASSISTANT]
    *"\nFrom 83.1% to 85.0%."
    "\n"
    py

    ```py
    video_grid_thw: tensor([[25, 16, 16]])
    tensor([[T, H, W]])
    T = 时间方向 patch 数, temporal_patch_size = 2 时间上合并一次 token,所以 T = 50 / 2 = 25
    H = 高方向 patch 数, 原始 ViT patch, video_grid_thw 这里过完 processor 应该已经 merge 了,照理来说应该是 8,但是这里是 16,迷惑
    W = 宽方向 patch 数
plaintext

有两个问题:

  1. 为什么 H / W = 16,而不是 8?
  2. AUDIO × 5822 最后剩的有点多,几次变更采样帧数目,时间块内的音频 token 数目还是 50 没有变化,时间窗口的大小没有随着采样帧数目更新吗?

数据记录:

2025-12-25#

  • 测试之前多轮对话训练 epoch 1 的结果,训练配置如下:
config = LoraConfig(
    r=16,
    lora_alpha=16,
    target_modules=["q_proj", "v_proj", "k_proj", "o_proj", "up_proj", "down_proj"],
    lora_dropout=0.05,
    bias="none",
    # task_type="CAUSAL_LM"
)

args = TrainingArguments(
    output_dir="./r_models",
    remove_unused_columns=False,
    eval_strategy="no",
    save_strategy="epoch",
    learning_rate=1e-4,
    per_device_train_batch_size=batch_size,
    gradient_accumulation_steps=1,
    bf16=True,
    fp16=False,
    fp16_full_eval=False,
    num_train_epochs=2,
    logging_steps=5,
    load_best_model_at_end=False,
)
py

loss 曲线、学习率曲线和梯度范数曲线如下:

loss 曲线学习率曲线梯度范数曲线
loss 曲线学习率曲线梯度范数曲线

多轮对话加载数据集,训练一个 epoch 的时间: 15:18:57

没空卡没跑评估

  • video_grid_thw 形状的问题:

跟踪了一下 video_grid_thw,发现它返回的是 merge(VL processor 中) 前的尺寸,在 2.5-omni 自己的 processor 里手动除以 merge_size^2 来计算实际 token 数。

merge_length_video = self.video_processor.merge_size**2
# ...
if not use_audio_in_video:
    video_seq_length = next(video_grid_thw).prod() // merge_length_video
    sample = sample.replace(self.video_token, "<|video_placeholder|>" * video_seq_length, 1)
else:
    audio_token_indices = np.arange(next(audio_lengths))
    curr_video_grid_thw = next(video_grid_thw)
    height = curr_video_grid_thw[1] // self.video_processor.merge_size
    width = curr_video_grid_thw[2] // self.video_processor.merge_size
    video_token_indices = np.arange(curr_video_grid_thw[0]).reshape(-1, 1, 1)
    video_token_indices = np.broadcast_to(
        video_token_indices, (video_token_indices.shape[0], height, width)
    ).reshape(-1)
py

所以上面的 video_grid_thw: tensor([[25, 16, 16]]) 是对的, 16 = 8 * 2(merge size); 因为是 4 个 patch 合成了一个新 patch(28 * 28)

计算的时候就是 16 / 2(merge size) * 28(patch size) = 224, 符合 resize 到 224 * 224 的处理

  • AUDIO token 长度:

    添加新的 debug 打印如下:

        [Audio Length]
        valid mel frames: [25689]
        audio seconds (approx): [256.8900146484375]
        audio tokens after encoder: [6422]
    py

    首先是音频总长度 50 * 12 + 5822 = 6442,这个的话和视频总时长有关,视频时长是 25689 * 10ms = 256.89s, audio tokens = (((25689-1) // 2 + 1)-2) // 2 + 1 = 6422 ( 1 个音频 token 对应真实 40ms ),所以之前的 50 个音频 token 对应的就是 2s 的时间窗口

        if audio is not None:
            output_kwargs["audio_kwargs"]["padding"] = "max_length"  # Support "max_length" padding only here
            audio_inputs = self.feature_extractor(audio, **output_kwargs["audio_kwargs"])
            audio_inputs["feature_attention_mask"] = audio_inputs.pop(
                "attention_mask"
            )  # rename feature_attention_mask to prevent conflicts later on
            audio_inputs["input_features"] = audio_inputs.pop(
                "input_features"
            )  # rename input_features to prevent conflicts later on
            input_lengths = (audio_inputs["feature_attention_mask"].sum(-1) - 1) // 2 + 1
            audio_lengths = iter((input_lengths - 2) // 2 + 1)
    py

2025-12-26#

开大组会,正好提到 Qwen2.5-VL 的动态分辨率,我之前的实现是直接 resize 到 224 * 224,感觉可以直接用它的动态分辨率改参数,之前试过以此,但是不知道为什么给 processor 传 max_pixel 没有效果

读源码,试着用传

"size" :{
    "shortest_edge": 64 * 28 * 28,
    "longest_edge": 128 * 28 * 28,
}
json

这样是可以动态 resize 的,video_grid_thw 的变成 tensor([[25, 16, 30]]), 动态 resize 到和原比例近似的且能被 28 整除的分辨率;比如这里这个就是 [640, 360] 的原始分辨率,resize 成 [15 * 28, 8 * 28], 8 / 15 = 0.5333 近似 360 / 640 = 0.5625;

接续 25 号的 debug

所以固定帧采样更新的时间窗口对应的真实时间并没有同步到音频,源码实际上做了这一步操作: len(video_grid_thw) = nframes(帧数) / 2 (temporal_patch_size) = T(视频时长) * fps / 2 (temporal_patch_size)

    second_per_grid_ts = [self.video_processor.temporal_patch_size / fps] * len(video_grid_thw)
    videos_inputs["video_second_per_grid"] = second_per_grid_ts
py

相当于默认你的帧数目就是视频总的时长,但是很奇怪的是 seconds_per_chunk 又设定的是 2; 完全没用上 second_per_grid_ts 来算时间,所以视频还是 2 帧合成一个 token(认为是 2fps 采样)对应真实时间一秒,接上音频的 50个 token;但是实际的时间关系错了;

<|vision_bos|>
├─ VIDEO × 240
├─ AUDIO × 50
├─ VIDEO × 240
├─ AUDIO × 50
├─ VIDEO × 240
├─ AUDIO × 50
├─ VIDEO × 240
├─ AUDIO × 50
├─ VIDEO × 240
├─ AUDIO × 50
├─ VIDEO × 240
├─ AUDIO × 50
├─ VIDEO × 240
├─ AUDIO × 50
├─ VIDEO × 240
├─ AUDIO × 50
├─ VIDEO × 240
├─ AUDIO × 50
├─ VIDEO × 240
├─ AUDIO × 50
├─ VIDEO × 240
├─ AUDIO × 50
├─ VIDEO × 240
├─ AUDIO × 50
├─ VIDEO × 120
├─ AUDIO × 5822
<|audio_eos|>
py

修改成用 fps 采样,限制最大帧数 50 帧 fps = max_frames(50) / duration

    batch = self.processor(
        text=texts,
        videos=videos,
        audio=audios,
        padding=True,
        return_tensors="pt",
        videos_kwargs={
            "max_frames": max_frames,
            "fps": fps,
            "do_sample_frames": True, # 必须
            "do_resize": True,
            "size" :{
                "shortest_edge": 64 * 28 * 28,
                "longest_edge": 128 * 28 * 28,
            },
            "min_pixels": 224 * 224,
            "max_pixels": 448 * 224,
            "use_audio_in_video": True, # 在外部设置无效了
        },
        # use_audio_in_video=True,
    )
python

报错,需要传 metadata 传进去,能拿到 metadata.fps, 但是没用 metadata.total_num_frames, 能解决;但是好像可以直接在处理视频的时候直接采样 50 帧,然后关闭 processor 中的 do_sample_frames,这样就可以不过使用 metadata.fps 和 metadata.total_num_frames 的计算步了

效果应该是一样的,记录一下目前的配置:

{"type": "video", "video": sample["video_path"], "nframes": 50},
# ...
duration = sample["duration"]
seconds_per_chunk = duration / num_frames # 这里不用整除 //;时间表示更细
fps = num_frames / duration if duration and duration > 0 else 2.0
batch = self.processor(
    text=texts,
    videos=videos,
    audio=audios,
    padding=True,
    return_tensors="pt",
    videos_kwargs={
        "seconds_per_chunk": seconds_per_chunk * 2, # 这里需要 * 2,是因为 temporal_patch_size = 2, 
        "max_frames": num_frames,
        "fps": fps,
        "do_resize": True,
        "size" :{
            "shortest_edge": 64 * 28 * 28,
            "longest_edge": 128 * 28 * 28,
        },
        "min_pixels": 224 * 224,
        "max_pixels": 448 * 224,
        "use_audio_in_video": True,
    },
)
python
  • “seconds_per_chunk”: seconds_per_chunk, 时间区间变小,但是因为 temporal_patch_size = 2, 两帧融合导致只有一帧是在多出来的时间窗口内

    <|vision_bos|>
    ├─ VIDEO × 120
    ├─ AUDIO × 128
    ├─ VIDEO × 1
    ├─ AUDIO × 128
    ├─ VIDEO × 119
    ├─ AUDIO × 128
    ├─ VIDEO × 1
    ├─ AUDIO × 128
    ├─ VIDEO × 119
    ├─ AUDIO × 128
    ├─ VIDEO × 1
    ├─ AUDIO × 128
    ├─ VIDEO × 119
    ├─ AUDIO × 128
    ├─ VIDEO × 1
    # ...
    python
  • 目前的结果:

========== Omni Batch Debug ==========
[Text]
input_ids: torch.Size([1, 8828])
attention_mask: torch.Size([1, 8828])
labels: torch.Size([1, 8828])
label tokens: 14
valid text tokens: [8828]
label tokens (not -100): [14]

[Video]
pixel_values_videos: torch.Size([9100, 1176])
dtype: torch.float32
video_grid_thw: tensor([[25, 14, 26]])
video tensor size: 40.82 MB

[Audio]
input_features: torch.Size([1, 128, 30000]), 14.65 MB

[Audio Length]
valid mel frames: [25689]
audio seconds (approx): [256.8900146484375]
audio tokens after encoder: [6422]

<|vision_bos|>
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 256
├─ VIDEO × 91
├─ AUDIO × 278
<|audio_eos|>
py

问题 1#

我比较了一下在 processor 中进行采样和在 process_mm_info 数据提取中进行帧采样

他们在实现上稍微有所不同,

  • process_mm_info 数据提取(这个内存效率高一些)

idx = torch.linspace(0, total_frames - 1, nframes).round().long() 生成索引, linspace 是固定首尾,中间均匀插值,保证第 0 帧和最后一帧一定会被选中。索引是整数帧位置,直接映射到视频文件的帧

  • processor 中进行采样

indices = torch.arange(0, total_num_frames, total_num_frames / num_frames).int(), arange 是固定步长,可能因取整误差导致最后一帧未被选中(比如总帧数不是 50 的整数倍时)。

这样得到的视频帧数据稍有偏差

2025-12-29 - 2025-12-31#

看一些和记音频 token 压缩相关的论文:

  • Token Stacking:将多个连续的 token 沿着 token 的隐藏维度进行堆叠,需要 MLP 重新调整维度。eg. SLAM-ASR、LLaMA-Omni、Llama-AVSR

  • Pooling: Qwen2-audio 和 Qwen2.5-Omni 就是步长为 2 的池化;

  • Temporal Convolution:可学习;在时间维度上应用 1 维卷积减少 token 数量。 eg. SpeechVerseOSUMLUCY

  • Similarity-based audio-centric Compression: A-ToMe 在 MHSA 和 FFN 中间插入一个 token 合并模块,利用自注意力计算中使用的 key values 来计算相邻 token 之间的余弦相似度,合并具有高余弦相似度的相邻音频 token(平均值)

    每三层嵌入一次 A-ToMe,具体在层 2、5、8、11、14 和 17,每层合并比例的上限为 50%

    在语音信息密集的地方,token密度仍然较高,而在语音信息稀疏或冗余的地方,token密度显著降低。

  • Attention-based audio-centric Compression:

    Attention in Encoder: Token Pruning in Audio Transformers :TopK token 剪枝, 仅保留按注意力分数幅度排名前 K 的音频 token

  • Query-based audio-centric Compression:

    • Token Distillation: 利用可学习的查询 Token 将全面的音频信息蒸馏为紧凑的固定长度表示; eg. SALMONN 系列

    由于 longvale 数据集音频存在指导时间边界的作用, QFomer 压缩会丢失细粒度的时间信息?

    MRC Q-Former: 从不同时间分辨率的多长度输入中提取视听特征。详细结构如图所示。音频和视频帧在每个视频帧(即每 0.5 秒, 2fps)处同步,通过零填充使序列具有相同的长度(256,25)

    同步输入流在多个不同分辨率下被划分为固定长度的窗口,例如每隔 1、5 或 10 秒

    高分辨率滑动窗口覆盖 k = 5每个窗口覆盖 5 帧,使用两个查询向量;低分辨率 Q-Former 覆盖 k = 25,使用 10 个查询向量。这样对于更小的时间窗口,关注局部关键特征,对于长时间窗口,关注更多的特征。(有点像 slowfast 高分辨率低帧率,低分辨率高帧率)

    在将所有分辨率的输出查询向量发送至 LLM 之前,会使用一个投影层将它们组合起来

  • Cross-Modal Selection: Speechprune: 这是通过计算基于余弦相似性的跨模态相似度矩阵(音频-文本相关性)来实现的,然后用这个矩阵指导音频标记的压缩。(时序信息能在文本中体现出来吗? seg_captioning 和 grounding 的话应该是可以;但是单纯 captioning 的话..)

    检查语音和文本 token 嵌入之间的余弦相似度来移除语义上不相关的语音 token,然后还有一次注意力剪枝, 用第一层 Transformer 提供的注意力分数对音频 token 进行 top k 剪枝

    没有源码

2025-01-04#

然后是两篇比较新的paper

MMS-LLaMA: AV QFormer 预先合并视觉音频模态,然后接 QFormer; 感觉时间上信息损失多, 和 benchmark 可能匹配性不好

这篇我得单独看看:

EchoingPixels: Cross-Modal Adaptive Token Reduction for Efficient Audio-Visual LLMs

跨模态编码器, 可以借鉴的是同时段的音视频 token 基于重要性分别打分, 然后分配不同的数目, 可以结合 top k 剪枝

2025-01-05#

2025-01-06#

baseline 支持
https://astro-pure.js.org/blog/exp5
Author Cosmo
Published at December 24, 2025