diff --git a/README.md b/README.md index cabdcace6..d9b11b427 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -[//]: # (


|
## Contributors
-|
|
|
|
-|:------------------------------------------------------------------:|:---------------------------------------------------------------:|:--------------------------------------------------------------:|:-------------------------------------------------------:|:-----------------------------------------------------------:|:------------------------------------------------------:|
+|
|
|
|
|
+| :----------------------------------------------------------------: | :-------------------------------------------------------------: | :-----------------------------------------------------------: | :-----------------------------------------------------: | :-------------------------------------------------------: | :----------------------------------------------------: |
The contributors can be found in [contributors list](./Acknowledge.md)
## License
+
This project is licensed under [The MIT License](https://opensource.org/licenses/MIT). FunASR also contains various third-party components and some code modified from other repos under other open source licenses.
The use of pretraining model is subject to [model license](./MODEL_LICENSE)
-
## Citations
-``` bibtex
+
+```bibtex
@inproceedings{gao2023funasr,
author={Zhifu Gao and Zerui Li and Jiaming Wang and Haoneng Luo and Xian Shi and Mengzhe Chen and Yabin Li and Lingyun Zuo and Zhihao Du and Zhangyu Xiao and Shiliang Zhang},
title={FunASR: A Fundamental End-to-End Speech Recognition Toolkit},
diff --git a/README_zh.md b/README_zh.md
index b36700890..ecdba3ea0 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -1,21 +1,18 @@
-[//]: # (

|
## 社区贡献者
-|
|
|
|
|
|
|
+| :----------------------------------------------------------------: | :-------------------------------------------------------------: | :-----------------------------------------------------------: | :-----------------------------------------------------: | :-------------------------------------------------------: | :----------------------------------------------------: |
贡献者名单请参考([致谢名单](./Acknowledge.md))
-
## 许可协议
-项目遵循[The MIT License](https://opensource.org/licenses/MIT)开源协议,模型许可协议请参考([模型协议](./MODEL_LICENSE))
+项目遵循[The MIT License](https://opensource.org/licenses/MIT)开源协议,模型许可协议请参考([模型协议](./MODEL_LICENSE))
## 论文引用
-``` bibtex
+```bibtex
@inproceedings{gao2023funasr,
author={Zhifu Gao and Zerui Li and Jiaming Wang and Haoneng Luo and Xian Shi and Mengzhe Chen and Yabin Li and Lingyun Zuo and Zhihao Du and Zhangyu Xiao and Shiliang Zhang},
title={FunASR: A Fundamental End-to-End Speech Recognition Toolkit},
diff --git a/funasr/models/fun_asr_nano/__init__.py b/funasr/models/fun_asr_nano/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/funasr/models/fun_asr_nano/model.py b/funasr/models/fun_asr_nano/model.py
new file mode 100644
index 000000000..70f234910
--- /dev/null
+++ b/funasr/models/fun_asr_nano/model.py
@@ -0,0 +1,682 @@
+import json
+import logging
+import os
+import random
+import re
+import string
+import time
+import traceback
+
+import torch
+import torch.nn as nn
+from funasr import AutoModel
+from funasr.metrics.compute_acc import compute_accuracy
+from funasr.register import tables
+from funasr.train_utils.device_funcs import force_gatherable, to_device
+from funasr.utils.datadir_writer import DatadirWriter
+from funasr.utils.load_utils import extract_fbank, load_audio_text_image_video
+
+dtype_map = {"bf16": torch.bfloat16, "fp16": torch.float16, "fp32": torch.float32}
+
+
+@tables.register("model_classes", "FunASRNano")
+class FunASRNano(nn.Module):
+ def __init__(
+ self,
+ audio_encoder: str = None,
+ audio_encoder_conf: dict = None,
+ audio_adaptor: str = None,
+ audio_adaptor_conf: dict = None,
+ llm: str = None,
+ llm_conf: dict = None,
+ input_size: int = 80,
+ length_normalized_loss: bool = False,
+ **kwargs,
+ ):
+ super().__init__()
+
+ # audio encoder
+ hub = audio_encoder_conf.get("hub", None)
+ self.audio_encoder_activation_checkpoint = audio_encoder_conf.get(
+ "activation_checkpoint", False
+ )
+ if hub == "ms":
+ model = AutoModel(model=audio_encoder, model_revision="master")
+ audio_encoder_output_size = (
+ model.model.encoder_output_size
+ if hasattr(model.model, "encoder_output_size")
+ else -1
+ )
+ audio_encoder = (
+ model.model.model.encoder
+ if hasattr(model.model, "model")
+ else model.model.encoder
+ )
+ else:
+ encoder_class = tables.encoder_classes.get(audio_encoder)
+ audio_encoder = encoder_class(input_size=input_size, **audio_encoder_conf)
+ audio_encoder_output_size = audio_encoder.output_size()
+ freeze = audio_encoder_conf.get("freeze", True)
+ freeze_layer_num = int(audio_encoder_conf.get("freeze_layer_num", -1))
+
+ if freeze:
+ for name, param in audio_encoder.named_parameters():
+ param.requires_grad = False
+ audio_encoder.eval()
+ self.audio_encoder = audio_encoder
+ # llm
+ self.llm = None
+ init_param_path = llm_conf.get("init_param_path", None)
+ llm_dim = None
+
+ from transformers import AutoModelForCausalLM
+
+ llm_load_kwargs = llm_conf.get("load_kwargs", {})
+ model = AutoModelForCausalLM.from_pretrained(
+ init_param_path,
+ load_in_8bit=None,
+ device_map=None,
+ use_cache=None,
+ **llm_load_kwargs,
+ )
+
+ freeze = llm_conf.get("freeze", True)
+ if freeze:
+ for name, param in model.named_parameters():
+ param.requires_grad = False
+ model.eval()
+ logging.info(f"use_lora: {llm_conf.get('use_lora', False)}")
+ if llm_conf.get("use_lora", False):
+ from omegaconf import DictConfig, OmegaConf
+
+ lora_conf = llm_conf.get("lora_conf", {})
+ if isinstance(lora_conf, (OmegaConf, DictConfig)):
+ lora_conf = OmegaConf.to_container(lora_conf, resolve=True)
+ from peft import LoraConfig, PeftModel, get_peft_model
+
+ lora_init_param_path = lora_conf.get("init_param_path", None)
+ if lora_init_param_path is not None:
+ logging.info(f"lora_init_param_path: {lora_init_param_path}")
+ model = PeftModel.from_pretrained(model, lora_init_param_path)
+ for name, param in model.named_parameters():
+ if not lora_conf.get("freeze_lora", False):
+ if "lora_" in name:
+ param.requires_grad = True
+ else:
+ peft_config = LoraConfig(**lora_conf)
+ model = get_peft_model(model, peft_config)
+ model.print_trainable_parameters()
+
+ if llm_conf.get("activation_checkpoint", False):
+ model.gradient_checkpointing_enable()
+
+ self.llm_dtype = llm_conf.get("llm_dtype", "fp32")
+ self.llm = model.to(dtype_map[self.llm_dtype])
+ llm_dim = model.get_input_embeddings().weight.shape[-1]
+
+ # adaptor
+ adaptor_class = tables.adaptor_classes.get(audio_adaptor)
+ if audio_encoder_output_size > 0:
+ audio_adaptor_conf["encoder_dim"] = audio_encoder_output_size
+ audio_adaptor_conf["llm_dim"] = (
+ llm_dim if llm_dim is not None else audio_adaptor_conf["llm_dim"]
+ )
+ audio_adaptor = adaptor_class(**audio_adaptor_conf)
+ init_param_path = audio_adaptor_conf.get("init_param_path", None)
+ if init_param_path is not None:
+ src_state = torch.load(init_param_path, map_location="cpu")
+ flag = audio_adaptor.load_state_dict(src_state, strict=False)
+ logging.info(
+ f"Loading audio_adaptor ckpt: {init_param_path}, status: {flag}"
+ )
+ freeze = audio_adaptor_conf.get("freeze", False)
+ if freeze:
+ for name, param in audio_adaptor.named_parameters():
+ param.requires_grad = False
+ audio_adaptor.eval()
+ self.audio_adaptor = audio_adaptor
+
+ self.length_normalized_loss = length_normalized_loss
+ self.feat_permute = audio_encoder_conf.get("feat_permute", True)
+ rank = int(os.environ.get("RANK", 0))
+ logging.info(f"rank: {rank}, model is builded.")
+
+ def forward(
+ self,
+ speech: torch.Tensor = None,
+ speech_lengths: torch.Tensor = None,
+ input_ids: torch.Tensor = None,
+ attention_mask: torch.Tensor = None,
+ labels_ids: torch.Tensor = None,
+ fbank_beg: torch.Tensor = None,
+ fbank_mask: torch.Tensor = None,
+ **kwargs,
+ ):
+ batch_size, token_num = input_ids.shape
+ stats = {}
+ input_ids[input_ids < 0] = 0
+ inputs_embeds = self.llm.model.get_input_embeddings()(input_ids)
+ if speech is not None:
+ if len(speech_lengths.size()) > 1:
+ speech_lengths = speech_lengths[:, 0]
+ batch_size_speech, frames, _ = speech.shape
+
+ # audio encoder
+ if self.audio_encoder_activation_checkpoint:
+ from torch.utils.checkpoint import checkpoint
+
+ encoder_out, encoder_out_lens = checkpoint(
+ self.encode, speech, speech_lengths, use_reentrant=False
+ )
+ else:
+ encoder_out, encoder_out_lens = self.encode(speech, speech_lengths)
+
+ # audio_adaptor
+ encoder_out, encoder_out_lens = self.audio_adaptor(
+ encoder_out, encoder_out_lens
+ )
+
+ batch_size, token_num, dims = inputs_embeds.shape
+ fake_token_len = kwargs.get("fake_token_len")
+ fake_token_len[fake_token_len < 0] = 0
+ fbank_beg[fbank_beg < 0] = 0
+
+ speech_idx = 0
+ for batch_idx in range(batch_size):
+ for turn_id in range(fbank_beg.shape[1]):
+ fbank_beg_idx = fbank_beg[batch_idx, turn_id].item()
+ if fbank_beg_idx > 0:
+ speech_token_len = fake_token_len[batch_idx, turn_id]
+ speech_token = encoder_out[speech_idx, :speech_token_len, :]
+
+ try:
+ inputs_embeds[
+ batch_idx,
+ fbank_beg_idx : fbank_beg_idx + speech_token_len,
+ :,
+ ] = speech_token
+ except Exception as e:
+ logging.error(f"{str(e)}, {traceback.format_exc()}")
+ logging.info(
+ f"batch_idx: {batch_idx}, inputs_embeds: {inputs_embeds.shape}, fbank_beg_idx: {fbank_beg_idx}, speech_token_len: {speech_token_len}, encoder_out: {encoder_out.shape}, encoder_out_lens: {encoder_out_lens}, fake_token_len: {fake_token_len}, speech_lengths: {speech_lengths}"
+ )
+ speech_token_len = encoder_out_lens[speech_idx].item()
+ speech_token = encoder_out[speech_idx, :speech_token_len, :]
+ inputs_embeds[
+ batch_idx,
+ fbank_beg_idx : fbank_beg_idx + speech_token_len,
+ :,
+ ] = speech_token
+
+ speech_idx += 1
+
+ stats["batch_size_speech"] = batch_size_speech
+ stats["batch_size_x_frames"] = frames * batch_size_speech
+ stats["batch_size_real_frames"] = speech_lengths.sum().item()
+ stats["padding_frames"] = (
+ stats["batch_size_x_frames"] - stats["batch_size_real_frames"]
+ )
+
+ with torch.cuda.amp.autocast(
+ enabled=True if self.llm_dtype != "fp32" else False,
+ dtype=dtype_map[self.llm_dtype],
+ ):
+ labels_ids[labels_ids == -1] = -100
+ attention_mask[attention_mask < 0] = 0
+ model_outputs = self.llm(
+ inputs_embeds=inputs_embeds.to(dtype_map[self.llm_dtype]),
+ attention_mask=attention_mask,
+ labels=labels_ids,
+ )
+ loss = model_outputs.loss
+
+ with torch.no_grad():
+ preds = torch.argmax(model_outputs.logits, -1)
+ acc_att = compute_accuracy(
+ preds[:, :-1], labels_ids[:, 1:], ignore_label=-100
+ )
+ stats["acc"] = acc_att
+
+ stats["loss"] = torch.clone(loss.detach())
+ stats["batch_size"] = batch_size
+
+ stats["batch_size_x_tokens"] = token_num * batch_size
+ stats["batch_size_real_tokens"] = attention_mask.sum().item()
+ stats["padding_tokens"] = (
+ stats["batch_size_x_tokens"] - stats["batch_size_real_tokens"]
+ )
+
+ dialog_turns = (fbank_beg > 0).sum(-1)
+ dialog_turns_max = torch.max(dialog_turns).int().item()
+ dialog_turns_avg = dialog_turns.sum().item() / batch_size
+ stats["dialog_turns_max"] = dialog_turns_max
+ stats["dialog_turns_avg"] = dialog_turns_avg
+
+ # force_gatherable: to-device and to-tensor if scalar for DataParallel
+ if self.length_normalized_loss:
+ batch_size = int((labels_ids > 0 + 1).sum())
+ loss, stats, weight = force_gatherable((loss, stats, batch_size), loss.device)
+ return loss, stats, weight
+
+ def forward_export(self, speech, speech_lengths, **kwargs):
+ x, olens = self.audio_encoder(speech, speech_lengths)
+ encoder_out, encoder_out_lens = self.audio_adaptor(x, olens)
+ return encoder_out, encoder_out_lens
+
+ def encode(self, speech, speech_lengths):
+ # audio encoder
+ if self.feat_permute:
+ encoder_out, encoder_out_lens = self.audio_encoder(
+ speech.permute(0, 2, 1), speech_lengths
+ )
+ else:
+ encoder_out, encoder_out_lens = self.audio_encoder(speech, speech_lengths)
+
+ return encoder_out, encoder_out_lens
+
+ def data_template(self, data):
+ system, user, assistant = [], [], []
+ for i, item in enumerate(data):
+ role = item["role"]
+ content = item["content"]
+ if role == "system":
+ system.append(content)
+ elif role == "user":
+ if "audio" in item:
+ audio = item["audio"]
+ content = [content, audio]
+ user.append(content)
+ elif role == "assistant":
+ assistant.append(content)
+
+ system = system * len(user)
+
+ contents = {
+ "system": system,
+ "user": user,
+ "assistant": assistant,
+ }
+
+ return contents
+
+ def data_load_speech(
+ self, contents: dict, tokenizer, frontend, meta_data={}, **kwargs
+ ):
+ system = contents["system"]
+ user = contents["user"]
+ assistant = contents["assistant"]
+ pattern = re.compile(r"(<\|startofspeech\|>.*?<\|endofspeech\|>)")
+ do_think = True
+ sys_prompt = True
+ if "dataset_conf" in kwargs:
+ do_think = kwargs["dataset_conf"].get("do_think", True)
+ sys_prompt = kwargs["dataset_conf"].get("sys_prompt", True)
+
+ input_ids, labels, fbank, fbank_lens, fbank_mask, fbank_beg, fake_token_len = (
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ [],
+ )
+ input_source_ids = []
+ for i, (system_prompt, user_prompt, target_out) in enumerate(
+ zip(system, user, assistant)
+ ):
+ if i >= kwargs.get("multiturn_num_max", 5):
+ break
+ if len(input_ids) > kwargs.get("max_token_length", 1500):
+ break
+ if isinstance(user_prompt, (list, tuple)):
+ user_prompt, audio = user_prompt
+ if i == 0:
+ if kwargs.get("infer_with_assistant_input", False):
+ source_input = f"<|im_start|>system\n{system_prompt}<|im_end|>\n<|im_start|>user\n{user_prompt}"
+ if not sys_prompt:
+ source_input = f"<|im_start|>user\n{user_prompt}"
+ else:
+ source_input = f"<|im_start|>system\n{system_prompt}<|im_end|>\n<|im_start|>user\n{user_prompt}<|im_end|>\n<|im_start|>assistant\n"
+ if not sys_prompt:
+ source_input = f"<|im_start|>user\n{user_prompt}<|im_end|>\n<|im_start|>assistant\n"
+ else:
+ if kwargs.get("infer_with_assistant_input", False):
+ source_input = f"<|im_start|>user\n{user_prompt}"
+ else:
+ source_input = f"<|im_start|>user\n{user_prompt}<|im_end|>\n<|im_start|>assistant\n"
+ if not do_think:
+ source_input += "