Qwen-Audio:一种新的大规模音频-语言模型

1. 方案简介

现有的多任务语言模型主要关注特定类型的音频(如人类语音)或特定任务(如语音识别和字幕生成),限制了模型的通用性和交互能力。于是提出了一个新颖的音频-语言模型,该模型拥有通用音频理解模型的能力,结构图如下。

从上图可以看出Qwen-Audio结合了一个音频编码器和一个基于Qwen-7B的大型语言模型。Qwen-Audio在超过30个任务和多种音频类型上进行预训练,包括人类语音、自然声音、音乐和歌曲,以促进通用音频理解能力,从论文(参考文献-1)及官方Demo体验来看效果还是非常不错的。

2. 对比LTU

从架构图上来看,Qwen-Audio和LTU-AS非常像,音频编码都是基于Whisper,然后接一个LLM。这里简单总结一下两者区别:

  • 推理方式
    • Qwen-Audio并没有输出识别结果,而是将Encode后的序列直接送入LLM
    • LTU-AS分为两步分,并将两个部分的结果和指令一同编码送入LLM
      1. 输出识别结果
      2. 对Whisper Encode输出送入轻量级的音频标记模型(at-model),生成音频标记序列
  • 训练方式
    • Qwen-Audio是采用的联合训练的方式,所有参数都需要训练,训练成本比较高
    • LTU-AS模型维持Whisper和LLaMA不变,只训练LoRA、TLTR部分及投影层,训练成本比较低
  • 开放度
    • Qwen-Audio模型是开源的,开放的仓库只提供了eval部分,不过Qwen提供了finetune脚本
    • LTU-AS模型都是开源的,提供了完整的环境,train、finetune、eval等过程可以快速验证
  • 效果上
    • Qwen-Audio基于Qwen-7B,词表151936,中文效果明显好于LTU-AS
    • LTU-AS是LLM是基于LLaMA,词表只有32000,主要针对英文场景,中文基本不可用

3. 推理运行环境

    • https://github.com/QwenLM/Qwen-Audio
    • 运行平台:Linux
    • 库依赖:按照requirements.txt/requirements_web_demo.txt进行安装,最好在一个独立的docker或conda虚拟环境中安装
    • 只提供了eval脚本,没有提供finetune和train脚本,应该和Qwen-7B比较接近:https://github.com/QwenLM/Qwen
    • 模型下载地址
      • 为方便调试并没有采用gradio的方式进行部署,直接运行一个简单示例,故作如下修改
    • 示例
      • 📎1.wav
      • 指令"请问这个音频想表达什么?"
    • 部署遇到问题
      • TypeError: Audio.init() got an unexpected keyword argument 'source'
      • 修复方案
        • pip install numpy==1.24.4
        • pip install gradio==3.41.2

4. 推理源码解读

从架构图上可以看出,可以将推理过程分为3个步骤:音频编码、指令编码、LLM推理,其中音频编码和指令编码并无依赖关系,不过在Qwen-Audio代码实现过程中,需要获取音频编码后的长度,所以将音频编码放在步骤一。

步骤1:音频编码

编码模块基于Whisper encode部分(参考文献-2),"特征提取+CNN降低维度+Encode"部分一致,这里不再赘述,差异点是Qwen-Audio在Encode后增加降维模块"avg_pooler+proj", 如下所示:

复制代码
    (audio): AudioEncoder(
      (conv1): Conv1d(80, 1280, kernel_size=(3,), stride=(1,), padding=(1,))
      (conv2): Conv1d(1280, 1280, kernel_size=(3,), stride=(2,), padding=(1,))
      (blocks): ModuleList(
        (0-31): 32 x ResidualAttentionBlock(
          (attn): MultiHeadAttention(
            (query): Linear(in_features=1280, out_features=1280, bias=True)
            (key): Linear(in_features=1280, out_features=1280, bias=False)
            (value): Linear(in_features=1280, out_features=1280, bias=True)
            (out): Linear(in_features=1280, out_features=1280, bias=True)
          )
          (attn_ln): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)
          (mlp): Sequential(
            (0): Linear(in_features=1280, out_features=5120, bias=True)
            (1): GELU(approximate='none')
            (2): Linear(in_features=5120, out_features=1280, bias=True)
          )
          (mlp_ln): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)
        )
      )
      (ln_post): LayerNorm((1280,), eps=1e-05, elementwise_affine=True)
      (avg_pooler): AvgPool1d(kernel_size=(2,), stride=(2,), padding=(0,))
      (proj): Linear(in_features=1280, out_features=4096, bias=True)
      (audio_bos_eos_token): Embedding(2, 4096)
    )

以1.wav为例,采样率为16000,共包含51236个样本点,帧移是10ms(也就是对应160个点),所以有效帧数L_in=51236/160=320,那么经过"CNN降低维度+avg_pooler"降低维度后帧数应该为320/4=80,增加开始和结束标志后变为82。所以Qwen-Audio AudioEncocerm模块将长度320帧的音频,转成长度82帧维度为4096的序列。

  • /root/.cache/huggingface/modules/transformers_modules/Qwen-Audio-Chat/tokenization_qwen.py:550
  • /root/.cache/huggingface/modules/transformers_modules/Qwen-Audio-Chat/modeling_qwen.py:764

步骤2:构建Prompt

该步骤可分为两步:prompt生成、prompt序列化

  1. prompt生成

prompt由"音频信息和指令"构成,如本例中音频是"./1.wav",指令是"请问这个音频想表达什么?",那么添加对应的tag后,如下:

  • 音频信息:'Audio {1}: <audio>{"./1.wav"}</audio>'
  • 指令:'请问这个音频想表达什么?'

拼接后构成的prompt为 'Audio {1}: <audio>{"./1.wav"}</audio>\n请问这个音频想表达什么?' 。

  1. prompt序列化

将prompt按照特定格式打包,并序列化为token_id:

复制代码
def make_context():
  # 以history=None为例
  #'system\nYou are a helpful assistant.'==》[151644, 8948, 198, 2610, 525, 264, 10950, 17847, 13, 151645]
  context_tokens = system_tokens
  context_tokens += (
      nl_tokens
      + im_start_tokens
      + _tokenize_str("user", query)[1]
      + im_end_tokens
      + nl_tokens
      + im_start_tokens
      + tokenizer.encode("assistant")
      + nl_tokens
  )

# /root/.cache/huggingface/modules/transformers_modules/Qwen-Audio-Chat/modeling_qwen.py:1191
# 位于函数:QWenLMHeadModel::chat
# 输入query形如:'Audio 1: <audio>./1.wav</audio>\n请问这个音频想表达什么?'
# 输出context_tokens形如:'[151644, 8948, 198, ...'
raw_text, context_tokens, audio_info = make_context(
    tokenizer,
    query,
    history=history,
    system=system,
    max_window_size=max_window_size,
    chat_format=generation_config.chat_format,
)

执行后make_context生成prompt序列为:

复制代码
[151644, 8948, 198, 2610, 525, 264, 10950, 17847, 13, 151645, 198, 151644, 872, 198, 14755, 220, 16, 25, 220, 155163, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 151851, 155164, 198, 109194, 99487, 111268, 99172, 102124, 99245, 11319, 151645, 198, 151644, 77091, 198]

序列化后的token_id含义如下表,这里仅列出本次任务所涉及token,如果需要了获取更多可以通过函数tokenizer._convert_id_to_token(int)获取

|----------------------------|----------|---------------------|
| token | token_id | 含义 |
| system | 8948 | 系统 |
| user | 872 | 用户 |
| assistant | 77091 | 智能助手 |
| <|im_start|> | 151644 | 一般用于角色交替,角色开始说话 |
| <|im_end|> | 151645 | 一般用于角色交替,角色说话结束 |
| <audio> | 155163 | 标识音频的开始位置 |
| </audio> | 155164 | 标识音频结束的位置 |
| [[[AUDIO:modality]]] | 151851 | 音频占位符,后边用音频编码后的序列填充 |
| Audio | 14755 | 标识音频名字 |
| \n | 198 | 换行 |
| | 220 | 空格 |

步骤3:LLM推理

该模块是将序列化后的Prompt输入LLM(基于Qwen-7B )进行预测。下边是QWenModel模型结构,可以看出推理过程主要是一个32层维度为4096的残差注意力块(QWenBlock)。

复制代码
<bound method QWenLMHeadModel.generate of QWenLMHeadModel(
  (transformer): QWenModel(
    (wte): Embedding(155947, 4096)
    (drop): Dropout(p=0.0, inplace=False)
    (rotary_emb): RotaryEmbedding()
    (h): ModuleList(
      (0-31): 32 x QWenBlock(
        (ln_1): RMSNorm()
        (attn): QWenAttention(
          (c_attn): Linear(in_features=4096, out_features=12288, bias=True)
          (c_proj): Linear(in_features=4096, out_features=4096, bias=False)
          (attn_dropout): Dropout(p=0.0, inplace=False)
        )
        (ln_2): RMSNorm()
        (mlp): QWenMLP(
          (w1): Linear(in_features=4096, out_features=11008, bias=False)
          (w2): Linear(in_features=4096, out_features=11008, bias=False)
          (c_proj): Linear(in_features=11008, out_features=4096, bias=False)
        )
      )
    )
    (ln_f): RMSNorm()
  )
  (lm_head): Linear(in_features=4096, out_features=155947, bias=False)
)

推理后输出每个token的得分(QWen词表长度155947),并将得分送入decode_tokens得出文本(通常采用greedy-search),都是标准过程这里不再赘述。下边给出对应的代码位置。

复制代码
def forward():
  # 残差注意力模块:由于是递归输出1个维度为[1, 1, 4096]的序列
  # /root/.cache/huggingface/modules/transformers_modules/Qwen-Audio-Chat/modeling_qwen.py:892
  # 位于函数: QWenModel::forward
  for i, (block, layer_past) in enumerate(zip(self.h, past_key_values)):
  
            if output_hidden_states:
                all_hidden_states = all_hidden_states + (hidden_states,)
                ......
                outputs = block(   # 32层残差注意力块
                    hidden_states,
                    layer_past=layer_past,
                    rotary_pos_emb_list=rotary_pos_emb_list,
                    attention_mask=attention_mask,
                    head_mask=head_mask[i],
                    encoder_hidden_states=encoder_hidden_states,
                    encoder_attention_mask=encoder_attention_mask,
                    use_cache=use_cache,
                    output_attentions=output_attentions,
                )
                ......
  
  
  
  # 将[1, 4096]序列映射成[1, 1, 155947]的序列,即获取每个token的得分
  # /root/.cache/huggingface/modules/transformers_modules/Qwen-Audio-Chat/modeling_qwen.py:1125
  # QWenLMHeadModel::forward
  lm_logits = self.lm_head(hidden_states)
  
  
  # 如果是greedy-search,获取得分最高的token
  # /usr/local/lib/python3.10/site-packages/transformers/generation/logits_process.py:419
  

# 自回归解码
# /usr/local/lib/python3.10/site-packages/transformers/generation/utils.py:2709
# 位于函数: QWenLMHeadModel::chat->QWenLMHeadModel::generate->GenerationMixin::generate->GenerationMixin::sample
while True:
  model_inputs = self.prepare_inputs_for_generation(input_ids, **model_kwargs)
  # forward pass to get next token
  outputs = forward(    # 也就是代码里的self
            **model_inputs,
            return_dict=True,
            output_attentions=output_attentions,
            output_hidden_states=output_hidden_states)


# token解码:将自回归解码获得的token序列转成文本
# /root/.cache/huggingface/modules/transformers_modules/Qwen-Audio-Chat/modeling_qwen.py:1213
# 位于函数: QWenLMHeadModel::chat
# outputs形如: [151644, 8948, 198, 2610, 525, 264, 10950, 17847, 13, ...]
# response形如: '这个音频想表达的是一个男人在说话,说的内容是"i have a dream that one day"。'
response = decode_tokens(
              outputs[0],
              tokenizer, ...)

5. 参考文献

  1. Qwen-Audio: https://arxiv.org/pdf/2311.07919.pdf
  2. Whisper推理源码解读
相关推荐
lpfasd1233 小时前
从OpenAI发布会看AI未来:中国就业市场的重构与突围
人工智能·重构
春末的南方城市3 小时前
清华&字节开源HuMo: 打造多模态可控的人物视频,输入文字、图片、音频,生成电影级的视频,Demo、代码、模型、数据全开源。
人工智能·深度学习·机器学习·计算机视觉·aigc
whltaoin3 小时前
Java 后端与 AI 融合:技术路径、实战案例与未来趋势
java·开发语言·人工智能·编程思想·ai生态
中杯可乐多加冰3 小时前
smardaten AI + 无代码开发实践:基于自然语言交互快速开发【苏超赛事管理系统】
人工智能
Hy行者勇哥4 小时前
数据中台的数据源与数据处理流程
大数据·前端·人工智能·学习·个人开发
岁月宁静4 小时前
AI 时代,每个程序员都该拥有个人提示词库:从效率工具到战略资产的蜕变
前端·人工智能·ai编程
双向334 小时前
Trae Solo+豆包Version1.6+Seedream4.0打造"AI识菜通"
人工智能
AutoMQ4 小时前
10.17 上海 Google Meetup:从数据出发,解锁 AI 助力增长的新边界
大数据·人工智能
m0_743106464 小时前
LOBE-GS:分块&致密化效率提升
人工智能·算法·计算机视觉·3d·几何学