PEFT实战LoRA微调OpenAI Whisper 中文语音识别

OpenAI Whisper模型介绍

OpenAI 的 Whisper 模型是自动语音识别系统。拥有以下特点

  • 大规模训练数据: 使用了从互联网收集的 680,000 小时 多语言、多任务的带标签数据进行训练。

  • 强大鲁棒性: 庞大的数据量使得模型对口音、背景噪音和专业术语具有更好的识别能力。

  • 多功能: 不仅能够将语音转录成文本,还能进行多种语言到英语的翻译。

  • 开源: OpenAI 开源了模型和推理代码,以促进相关应用和进一步的研究。

Whisper自动语音识别模型使用典型的 编码器-解码器 Transformer结构,输入音频被分割为 30秒的块 ,转换为 对数梅尔频谱图(Log-Mel Spectrogram) ,然后输入编码器,使用一个解码器来预测文本标题,并夹杂特殊标记(如语言标识、时间戳等),使单一模型能完成多种语音识别、翻译、语言检测等任务。

鲁棒性实验如下

实验数据解读:Wav2vec 2.0 :一个重要的自监督学习语音识别模型。Whisper :本次介绍的主角。表格中的数值代表 词错误率相对降低的百分比 。这个百分比是相对于 Wav2vec 2.0 模型的表现来计算的。数值越大越好,正数表示错误率降低(性能提升),负数表示错误率增加(性能下降)举例 :在 AMI SDM1(会议录音,单麦克风)数据集上,Whisper 的词错误率比 Wav2vec 2.0 降低了 46.2%,这是一个巨大的提升。

结论:Whisper 在 在未经特定数据集训练的情况下(零样本)场景下表现出色 :这意味着它没有使用表格中这些特定数据集进行过训练,直接拿来测试,但效果非常好,其性能更接近人类水平:特别是在处理多样化、有挑战性的真实世界语音数据时。

数据集Common Voice介绍

Common Voice 11.0 数据集包含许多不同语言的录音,总时长达数小时。它在语音技术领域扮演着"基础设施"的角色。

实战

0、介绍

使用 LoRA 在 OpenAI Whisper-large-v2 模型上实现语音识别 (ASR) 任务的微调训练,还结合了 int8 量化进一步降低训练过程资源开销,同时保证了精度几乎不受影响。

1、全参数设置

python 复制代码
# 原始模型保存路径
model_name_or_path = "openai/whisper-large-v2"
# 微调后模型保存路径
model_dir = "models/whisper-large-v2-asr-int8"

language = "Chinese (China)"
language_abbr = "zh-CN"
# 指定任务为转录
task = "transcribe"
# 指定微调的数据集
dataset_name = "mozilla-foundation/common_voice_11_0"
# 指定批次
batch_size=64

2、下载数据集 Common Voice

Common Voice 11.0 数据集包含许多不同语言的录音,总时长达数小时。当前以中文数据为例,展示如何使用 LoRA 在 Whisper-large-v2 上进行微调训练。

python 复制代码
from datasets import load_dataset
from datasets import load_dataset, DatasetDict

# 初始化一个 DatasetDict 结构
common_voice = DatasetDict()
# 将训练集(将训练+验证拆分为训练集)和测试集拆分好,按照中文数据集构建配置加载到内存中
common_voice["train"] = load_dataset(dataset_name, language_abbr, split="train+validation")
common_voice["test"] = load_dataset(dataset_name, language_abbr, split="test")

3、预处理训练数据集

python 复制代码
from transformers import AutoFeatureExtractor, AutoTokenizer, AutoProcessor

# 从预训练模型加载特征提取器
feature_extractor = AutoFeatureExtractor.from_pretrained(model_name_or_path)

# 从预训练模型加载分词器,可以指定语言和任务以获得最适合特定需求的分词器配置
tokenizer = AutoTokenizer.from_pretrained(  
    model_name_or_path, language=language, task=task)

# 从预训练模型加载处理器,处理器通常结合了特征提取器和分词器,为特定任务提供一站式的数据预处理
processor = AutoProcessor.from_pretrained(  
    model_name_or_path, language=language, task=task)

针对不想要的数据集标题,可以移除

python 复制代码
common_voice = common_voice.remove_columns(
    ["accent", "age", "client_id", "down_votes", "gender", "locale", "path", "segment", "up_votes"]
)

降采样音频数据

查看 common_voice 数据集介绍,你会发现其音频是以48kHz的采样率进行采样的。而 Whisper 模型是在16kHz的音频输入上预训练的,因此我们需要将音频输入降采样以匹配模型预训练时使用的采样率。通过在音频列上使用 cast_column 方法,并将 sampling_rate 设置为16kHz来对音频进行降采样。

python 复制代码
from datasets import Audio
common_voice = common_voice.cast_column("audio", Audio(sampling_rate=16000))

定义数据预处理函数:用于将音频和文本数据转换为模型训练所需的格式,

  • 通过加载音频列将音频输入重新采样为16kHZ。
  • 使用特征提取器从音频数组计算输入特征。
  • 将句子列标记化为输入标签。
python 复制代码
def prepare_dataset(batch):
    audio = batch["audio"]
    batch["input_features"] = feature_extractor(audio["array"], sampling_rate=audio["sampling_rate"]).input_features[0]
    batch["labels"] = tokenizer(batch["sentence"]).input_ids
    return batch

给到dataset.map()方法

python 复制代码
tokenized_common_voice = common_voice.map(prepare_dataset, num_proc=8)

定义一个针对语音到文本(Seq2Seq) 模型的自定义数据整理器类,特别适用于输入为语音特征、输出为文本序列的数据集。

这个整理器 (DataCollatorSpeechSeq2SeqWithPadding) 旨在将数据点批量打包,将每个批次中的 attention_mask 填充到最大长度,以保持批处理中张量形状的一致性,并用 -100 替换填充值,以便在损失函数中被忽略。这对于神经网络的高效训练至关重要。

python 复制代码
import torch
from dataclasses import dataclass
from typing import Any, Dict, List, Union

# 定义一个针对语音到文本任务的数据整理器类
@dataclass
class DataCollatorSpeechSeq2SeqWithPadding:
    processor: Any  # 处理器结合了特征提取器和分词器

    # 整理器函数,将特征列表处理成一个批次
    def __call__(self, features: List[Dict[str, Union[List[int], torch.Tensor]]]) -> Dict[str, torch.Tensor]:
        # 从特征列表中提取输入特征,并填充以使它们具有相同的形状
        input_features = [{"input_features": feature["input_features"]} for feature in features]
        batch = self.processor.feature_extractor.pad(input_features, return_tensors="pt")

        # 从特征列表中提取标签特征(文本令牌),并进行填充
        label_features = [{"input_ids": feature["labels"]} for feature in features]
        labels_batch = self.processor.tokenizer.pad(label_features, return_tensors="pt")

        # 使用-100替换标签中的填充区域,-100通常用于在损失计算中忽略填充令牌
        labels = labels_batch["input_ids"].masked_fill(labels_batch.attention_mask.ne(1), -100)

        # 如果批次中的所有序列都以句子开始令牌开头,则移除它
        if (labels[:, 0] == self.processor.tokenizer.bos_token_id).all().cpu().item():
            labels = labels[:, 1:]

        # 将处理过的标签添加到批次中
        batch["labels"] = labels

        return batch  # 返回最终的批次,准备好进行训练或评估
python 复制代码
# 用指定的处理器实例化数据整理器
data_collator = DataCollatorSpeechSeq2SeqWithPadding(processor=processor)

4、模型准备

1、加载预训练模型(int8 精度)

使用 int8 精度加载预训练模型,进一步降低显存需求。

python 复制代码
from transformers import AutoModelForSpeechSeq2Seq
model = AutoModeLForSpeechSeq2Seq.from_pretrained(model_name_or_path, load_in_8bit=True, device_map="auto")

# 设置模型配置中的forced_decoder_ids属性为None,这通常用于指定在解码(生成文本)过程中必须使用的特定token的ID,设置为None表示没有这样的强制要求
model.config.forced_decoder_ids = None

# 设置模型配置中的suppress_tokens列表为空,这用于指定在生成过程中应被抑制(不生成)的token的列表,设置为空列表表示没有要抑制的token
model.config.suppress_tokens = []
2、PEFT 微调前的模型处理

在使用 peft 训练 int8 模型之前,需要进行一些预处理:

  • 将所有非 int8 精度模块转换为全精度(fp32)以保证稳定性
  • 为输入嵌入层添加一个 forward_book,以启用输入隐藏状态的梯度计算
  • 启用梯度检查点以实现更高效的内存训练

使用 peft 库预定义的工具函数 prepare_model_for_int8_training,便可自动完成以上模型处理工作。

python 复制代码
from peft import prepare_model_for_int8_training  
model = prepare_model_for_int8_training(model)
3、LoRA Adapter 配置

peft 中使用 LoRA 非常简捷,借助 PeftModel 抽象,我们可以快速使用低秩适配器(LoRA)到任意模型。

通过使用 peft 中的 get_peft_model 工具函数来实现。

关于 LoRA 超参数的说明:

MatWu1(B,A) * Scaling

​ Scaling = LoRA_Alpha / Rank

创建一个LoraConfig对象,用于设置LoRA(Low-Rank Adaptation)的配置参数

python 复制代码
from peft import LoraConfig, PeftModel, LoraModel, LoraConfig, get_peft_model

# 创建一个LoraConfig对象,用于设置LoRA(Low-Rank Adaptation)的配置参数
config = LoraConfig(
    r=8, # LoRA的秩,影响LoRA矩阵的大小
    lora_alpha=64, # LoRA适应的比例因子
    # 指定将LoRA应用到的模型模块,通常是attention和全连接层的投影。
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05, # 在LoRA模块中使用的dropout率
    bias="none", # 设置bias的使用方式,这里没有使用bias
)
4、使用get_peft_model函数和给定的配置来获取一个PEFT模型
python 复制代码
# 使用get_peft_model函数和给定的配置来获取一个PEFT模型
peft_model = get_peft_model(model, config)

# 打印 LoRA 微调训练的模型参数,可以看到到底要用多少的参数进行训练
# peft_model.print_trainable_parameters()

5、模型训练

1、训练参数

关于设置训练步数和评估步数

python 复制代码
# 基于 epochs 设置:
num_train_epochs=3, # 训练的总轮数  
evaluation_strategy="epoch", # 设置评估策略,这里是在每个epoch结束时进行评估  
warmup_steps=50, # 在训练初期增加学习率的步数,有助于稳定训练

# 基于 steps 设置:
max_steps=100, # 训练总步数
evaluation_strategy="steps",  # 评估策略
eval_steps=25, # 评估步数

设置序列到序列模型训练的参数

python 复制代码
from transforms import Seq2SeqTrainingArguments

# 设置序列到序列模型训练的参数
training_args = Seq2SeqTrainingArguments(
    output_dir=model_dir, # 指定模型输出和保存的目录
    per_device_train_batch_size=batch_size, # 每个设备上的训练批量大小
    learning_rate=1e-3, # 学习率
    num_train_epochs=1, # 训练的总轮数,实际可用3轮
    evaluation_strategy="epoch", # 设置评估策略,这里是在每个epoch结束时进行评估
    # warmup_steps=50, # 在训练初期增加学习率的步数,有助于稳定训练
    # fp16=True, # 启用混合精度训练,可以提高训练速度,同时减少内存使用
    per_device_eval_batch_size=batch_size, # 每个设备上的评估批量大小
    generation_max_length=128, # 生成任务的最大长度
    logging_steps=10, # 指定日志记录的步骤,用于跟踪训练进度
    remove_unused_columns=False, # 是否删除不使用的列,以减少数据处理开销
    label_names="labels", # 指定标签列的名称,用于训练过程中
    # evaluation_strategy="steps",
    # eval_steps=25,
)
2、实例化 Seq2SeqTrainer 训练器开始训练
python 复制代码
from transforms import Seq2SeqTrainer

trainer = Seq2SeqTrainer(
    args=training_args,
    model=peft_model,
    train_dataset=tokenized_common_voice["train"],
    eval_dataset=tokenized_common_voice["validation"],
    data_collator=data_collator,
    tokenizer=processor.feature_extractor,
)
peft_model.config.use_cachef= False

trainer.train()
3、保存训练的模型
复制代码
trainer.save_model(model_dir)

6、使用微调好的模型

1、加载模型
  • 使用 PeftConfig 加载 LoRA Adapter 配置参数,使用 PeftModel 加载微调后 Whisper 模型
python 复制代码
model_dir = "models/whisper-large-v2-asr-int8"

language = "Chinese (China)"
language_abbr = "zh-CN"
language_decode = "chinese"
task = "transcribe"

from transformers import AutoMode[ForSpeechSeq2Seq, AutoTokenizer, AutoProcessor
from peft import PeftConfig, PeftModel

peft_config = PeftConfig.from_pretrained(model_dir)
# base_model_name_or_path这是模型自带的常量
base_model = AutoMode[ForSpeechSeq2Seq.from_pretrained(
    peft_config.base_model_name_or_path, load_in_8bit=True, device_map="auto"
)

peft_model = PeftModel.from_pretrained(base_model, model_dir)
tokenizer = AutoTokenizer.from_pretrained(peft_config.base_model_name_or_path, language=language, task=task)
processor = AutoProcessor.from_pretrained(peft_config.base_model_name_or_path, language=language, task=task)
feature_extractor = processor.feature_extractor
2、使用 Pipeline API 部署微调后 Whisper 实现中文语音识别任务
python 复制代码
test_audio = "data/audio/test_zh.flac"

from transformers import AutomaticSpeechRecognitionPipeline

pipeline = AutomaticSpeechRecognitionPipeline(model=peft_model, tokenizer=tokenizer, feature_extractor=feature_extractor)

forced_decoder_ids = processor.get_decoder_prompt_ids(language=language_decode, task=task)

import torch

with torch.cuda.amp.autocast():
    text = pipeline(test_audio, max_new_tokens=255)["text"]

7、评估微调好的模型

python 复制代码
mode_name_or_path = "openai/whisper-large-v2"
model_dir = "models/whisper-large-v2-asr-int8"

language = "Chinese (China)"
language_abbr = "zh-CN"
task = "transcribe"
dataset_name = "mozilla-foundation/common_voice_11_0"

batch_size=16
python 复制代码
from transformers import AutoModeLForSpeechSeq2Seq, AutoTokenizer, AutoProcessor
from peft import PeftConfig, PeftModel

peft_config = PeftConfig.from_pretrained(model_dir)

base_model = AutoModeLForSpeechSeq2Seq.from_pretrained(
    peft_config.base_model_name_or_path, load_in_8bit=True, device_map="auto"
)
# 权重不用做反向传播,很多步骤可以省略
base_model.requires_grad(false)
python 复制代码
trainer.save_model(model_dir)
复制代码
tokenizer = AutoTokenizer.from_pretrained(peft_config.base_model_name_or_path, language=language, task=task)
processor = AutoProcessor.from_pretrained(peft_config.base_model_name_or_path, language=language, task=task)
feature_extractor = processor.feature_extractor

评估数据集处理

python 复制代码
from datasets import load_dataset, DatasetDict, Audio

common_voice = DatasetDict()
common_voice["test"] = load_dataset(dataset_name, language_abbr, split="test", trust_remote_code=True)
common_voice = common_voice.remove_columns(
    ["accent", "age", "client_id", "down_votes", "gender", "locale", "path", "segment", "up_votes"]
)
common_voice = common_voice.cast_column("audio", Audio(sampling_rate=16000))
python 复制代码
def prepare_dataset(batch):
    audio = batch["audio"]
    batch["input_features"] = feature_extractor(audio["array"], sampling_rate=audio["sampling_rate"].input_features[0]
    batch["labels"] = tokenizer(batch["sentence"].input_ids
    return batch

small_common_voice = DatasetDict()

small_common_voice["test"] = common_voice["test"].shuffle(seed=16).select(range(328))

tokenized_common_voice = small_common_voice.map(prepare_dataset)

评估模型

python 复制代码
import evaluate

# 词错误率 (NER) 是评估ASR模型常用的指标。从 Evaluate 加载 MER 指标
metric = evaluate.load("wer")

from torch.utils.data import DataLoader
from tqdm import tqdm
import numpy as np
import gc

eval_dataloader = DataLoader(tokenized_common_voice["test"], batch_size=batch_size, collate_fm=data_collator)
python 复制代码
# 遍历评估数据加载器中的所有批次
for step, batch in enumerate(tqdm(eval_dataloader)):
    # 使用自动混合精度来加速计算,并减少显存使用
    with torch.cuda.amp.autocast():
        # 不计算梯度,以节省计算资源,仅用于推理和评估
        with torch.no_grad():
            # 生成预测的标记(tokens),这里使用模型的generate函数进行文本生成
            generated_tokens = (
                peft_model.generate(
                    input_features=batch["input_features"].to("cuda"),    # 将输入特征移动到GPU上
                    decoder_input_ids=batch["labels"][:,:4].to("cuda"),    # 提供解码器的初始输入
                    max_new_tokens=255,    # 设置生成的最大新标记数量
                )
                .cpu()    # 将生成的标记移回CPU
                .numpy()    # 转换为Numpy数组以便进一步处理
            )
            # 获取批次中的标签,并将其移回CPU
            labels = batch["labels"].cpu().numpy()
            # 将标签中的-100替换为填充标记的ID,-100通常用于忽略计算损失的标记
            labels = np.where(labels != -100, labels, tokenizer.pad_token_id)
            # 使用分词器解码生成的标记和标签,以获得可读的文本
            decoded_preds = tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)
            decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)
            # 将预测和参考添加到评估指标中,用于后续的性能评估
            metric.add_batch(
                predictions=decoded_preds,
                references=decoded_labels,
            )
    # 删除不同需要的变量以释放内存
    del generated_tokens, labels, batch
    # 手动触发垃圾收集,进一步清理内存
    gc.collect()

使用全量数据微调后,对比 WER 指标降低了多少

python 复制代码
# 计算词错误率 (WER) 指标,并将结果转换为百分比形式
wer = 100 * metric.compute()

# 打印词错误率,f"{wer=}"是一种格式化字符串的简洁写法,它会展示变量名和值
print(f"{wer=}%")
相关推荐
奔跑中的小象2 小时前
统信UOS + 天数AI卡部署SGLang服务手册
人工智能·uos·sglang·天数智芯
DevSecOps选型指南2 小时前
中国版Mythos,为何是悬镜安全灵脉CodeAI?
人工智能·安全
Shockang2 小时前
LangGraph 状态机实战
人工智能
刹那芳华19922 小时前
循环神经网络的从零开始实现(RNN)
人工智能·rnn·深度学习
阿童木写作3 小时前
跨境图片翻译工具多合一,批量图片视频字幕翻译加智能抠图
人工智能·python·音视频·语音识别
科技之门3 小时前
AI3D从建模到贴图、绑骨和动画的完整流程怎么做?V2Fun完整工作流指南
人工智能·3d·贴图
宇宙第一小趴菜3 小时前
二、机器学习的应用领域和发展史
人工智能·机器学习
前端开发江鸟3 小时前
我写过 MCP Server,却一直以为 MCP 只有 Tool
人工智能
天国梦3 小时前
自习室智能化升级避坑指南:天学网AI智习室方案实测与选型建议
大数据·人工智能
阿里云云原生4 小时前
可用性从 99.9% 跃升至 99.995%:畅捷通如何用 AI 重塑运维底座?
运维·网络·人工智能