fgg blog

finetune_llm_2

Table of Contents

简历中提到对模型进行私域数据微调(finetuning)的,要能够说出所以然来。

# CN-CLIP 微调 + Swin-V2 魔改(多标签预测)

CN-CLIP 模型的使用:

目的:标注本地图像和扩增训练集样本(全手动标注成本高周期长)

背景: 领域开源数据集(130万样本规模管道缺陷图像数据集) 的多标签与本项目图像的多标签需要进行对齐,即缺陷类型并非完全一致(开源数据集的标签类型共 17个,仅有三分之一左右标签与本地数据集标签一致),要利用这个开源数据集,就要先完成标签数 据的对齐。

CN-CLIP本身是用CLIP模型在大规模中文语料(约2亿图文对数据)上进行训练得到,本身具备一定的 跨模态表征能力;应用到领域数据时,只需要在领域图像上进行进一步的微调,可以获得较好的图像 文本跨模态表征,从而在零样本图像分类任务上具有较高的迁移价值。

具体到本项目,项目业务需求是能够提供管道图像缺陷的多标签预测,需要从技术层面提供解决方案。 但多标签图像训练集样本量严重不足,人工标注成本高周期长,需要寻找更经济的训练样本构建途径。 当时选择的技术路径就是利用CLIP的零样本预测能力+领域开源数据微调来构建本项目的训练数据集, 再利用开源的图像预训练模型来完成推理。

为什么不直接基于图片相似度进行多标签赋值?实际上,我们的方案里也包括这部分的工作,但能增 加的样本量不多。毕竟国内外管道管材等属性以及缺陷类型存在一定差异,直接进行图像相似度的方 法不能提供很好的效果。

# CLIP 基础知识和应用

关于CLIP模型的基础知识,参考CLIP论文精读笔记

# CN-CLIP的训练(微调)

CN-CLIP是CLIP模型的中文版本,在大规模中文语料 (约2亿图文对数据)上进行训练得到,并针对中文领域数据以及在中文数据上实现更好的效果做了 优化。

  • 代码组织
Chinese-CLIP/
├── run_scripts/
│   ├── muge_finetune_vit-b-16_rbt-base.sh
│   ├── flickr30k_finetune_vit-b-16_rbt-base.sh
│   └── ...             # 更多finetune或评测脚本...
└── cn_clip/
    ├── clip/
    ├── eval/
    ├── preprocess/
    └── training/

${DATAPATH}
├── pretrained_weights/ # 存放对应模型ckpt
├── experiments/
├── deploy/	            # 用于存放ONNX & TensorRT部署模型
└── datasets/
    ├── MUGE/
    ├── Flickr30k-CN/
    └── .../            # 更多自定义数据集...
  • 数据集格式预处理
${DATAPATH}                     # 如:Flickr30k-CN/
└── datasets/
    └── ${dataset_name}/
        ├── train_imgs.tsv      # 图片id & 图片内容
        ├── train_texts.jsonl   # 文本id & 文本内容,连同匹配的图片id列表
        ├── valid_imgs.tsv
        ├── valid_texts.jsonl
        ├── test_imgs.tsv
        └── test_texts.jsonl

为保证文件处理效率,我们不是将图片以大量的小文件方式存放,而是将训练/验证/测试图片以 base64形式分别存放在${split}_imgs.tsv文件中。文件每行表示一张图片,包含图片id(int型)与 图片base64,以tab隔开,格式如下:

1000002	/9j/4AAQSkZJ...YQj7314oA//2Q==

文本信息及图文对匹配关系则保存在${split}_texts.jsonl文件。文件每行是一行json,格式如下:

{"text_id": 8428, "text": "高级感托特包斜挎", "image_ids": [1076345, 517602]}

对于测试集只有文本,不知道图文对匹配关系的情况,每行的image_ids字段处理为空列表即可,即 “image_ids”: []。

  • tsv和jsonl文件的序列化 最后,我们还需要将tsv和jsonl文件一起序列化,转换为内存索引的LMDB数据库文件,方便训练时的 随机读取:
python cn_clip/preprocess/build_lmdb_dataset.py \
    --data_dir ${DATAPATH}/datasets/${dataset_name}
    --splits train,valid,test
  • 示例 例如对于MUGE数据集,则${dataset_name}设为MUGE,–splits指定需要转换的数据集划分,以逗号 不加空格分隔。转换后,数据集文件夹下会对应增加以下LMDB序列化文件:
${DATAPATH}
└── datasets/
    └── ${dataset_name}/
        └── lmdb/
            ├── train
            │   ├── imgs
            │   └── pairs
            ├── valid
            └── test

# CN-CLIP 微调细节

# 准备finetune相关配置,详见https://github.com/OFA-Sys/Chinese-CLIP#模型finetune
# 指定机器数 & 卡数
GPUS_PER_NODE=1 # 卡数
WORKER_CNT=1 # 机器数
MASTER_ADDR="localhost"
MASTER_PORT=8514 # 同台机器同时起多个任务,请分别分配不同的端口号
RANK=0

# 刚刚创建过的目录,存放了预训练参数和预处理好的数据集
DATAPATH="../fmhData"
DATASET="Flickr30k-CN"

# 指定LMDB格式的训练集和验证集路径(存放了LMDB格式的图片和图文对数据)
train_data=f"{DATAPATH}/datasets/{DATASET}/lmdb/train"
val_data=f"{DATAPATH}/datasets/{DATASET}/lmdb/valid"
num_workers=4 # 训练集pytorch dataloader的进程数,设置为>0,以减小训练时读取数据的时间开销
valid_num_workers=4 # 验证集pytorch dataloader的进程数,设置为>0,以减小验证时读取数据的时间开销

# 指定刚刚下载好的Chinese-CLIP预训练权重的路径
resume=f"{DATAPATH}/pretrained_weights/clip_cn_vit-h-14.pt"
reset_data_offset="--reset-data-offset" # 从头读取训练数据
reset_optimizer="--reset-optimizer" # 重新初始化AdamW优化器

# 指定输出相关配置
batchsize=64
output_base_dir=f"{DATAPATH}/experiments/"
#name=f"flickr30kcn_finetune_vit-l-14_roberta-base_batchsize{batchsize}_1gpu" # finetune超参、日志、ckpt将保存在../datapath/experiments/
name=f"flickr30kcn_finetune_vit-h-14_roberta-large_batchsize{batchsize}_1gpu" # finetune超参、日志、ckpt将保存在../datapath/experiments/
save_step_frequency=999999 # disable it
save_epoch_frequency=1 # 每轮保存一个finetune ckpt
log_interval=10 # 日志打印间隔步数
report_training_batch_acc="--report-training-batch-acc" # 训练中,报告训练batch的in-batch准确率

# 指定训练超参数
context_length=52 # 序列长度,这里指定为Chinese-CLIP默认的52
warmup=100 # warmup步数
batch_size=batchsize # 训练单卡batch size
valid_batch_size=batchsize # 验证单卡batch size
lr=3e-6 # 学习率,因为这里我们使用的对比学习batch size很小,所以对应的学习率也调低一些
wd=0.001 # weight decay
max_epochs=1 # 训练轮数,也可通过--max-steps指定训练步数
valid_step_interval=1000 # 验证步数间隔
valid_epoch_interval=1 # 验证轮数间隔
#vision_model="ViT-L-14-336" # 指定视觉侧结构为ViT-L/14@336
vision_model="ViT-H-14" # 指定视觉侧结构为ViT-H/14
#text_model="RoBERTa-wwm-ext-base-chinese" # 指定文本侧结构为RoBERTa-base
text_model="RoBERTa-wwm-ext-large-chinese" # 指定文本侧结构为RoBERTa-large
use_augment="--use-augment" # 对图像使用数据增强
grad_checkpointing="--grad-checkpointing" # 激活重计算策略,用更多训练时间换取更小的显存开销

run_command = "export PYTHONPATH=${PYTHONPATH}:`pwd`/cn_clip;" + \
f"""
python3 -m torch.distributed.run --nproc_per_node={GPUS_PER_NODE} --nnodes={WORKER_CNT} --node_rank={RANK} \
      --master_addr={MASTER_ADDR} --master_port={MASTER_PORT} cn_clip/training/main.py \
      --train-data={train_data} \
      --val-data={val_data} \
      --num-workers={num_workers} \
      --valid-num-workers={valid_num_workers} \
      --resume={resume} \
      {reset_data_offset} \
      {reset_optimizer} \
      --logs={output_base_dir} \
      --name={name} \
      --save-step-frequency={save_step_frequency} \
      --save-epoch-frequency={save_epoch_frequency} \
      --log-interval={log_interval} \
      {report_training_batch_acc} \
      --context-length={context_length} \
      --warmup={warmup} \
      --batch-size={batch_size} \
      --valid-batch-size={valid_batch_size} \
      --valid-step-interval={valid_step_interval} \
      --valid-epoch-interval={valid_epoch_interval} \
      --lr={lr} \
      --wd={wd} \
      --max-epochs={max_epochs} \
      --vision-model={vision_model} \
      {use_augment} \
      {grad_checkpointing} \
      --text-model={text_model}
""".lstrip()
print(run_command)

# jupyterlab 执行finetune流程  # torch==2.1.0
!{run_command}

# 梯度重计算策略

grad-checkpointing: 使用重计算策略,在前向过程中不保存中间结果,以训练时间换取更小的显存 开销,适用于显存不足的情况。(store_true参数,直接在脚本中加上–grad-checkpointing即可, 目前要求Pytorch>1.8.0)

实际上torch.utils.checkpoint模块的工作原理可以归结为:通过部分丢弃前向传播的中间激活 值来减少显存占用,并在反向传播时重新计算这些丢弃的激活值。这种策略在深度学习训练中是可 行的,因为 PyTorch 的计算图是动态生成的,可以灵活地指定哪些部分的前向传播需要“重计算”。

关于前向传播、后向传播和计算图的更多内容参考博文前向传播_反向传播_计算图

# FlashAttention

FlashAttention 是一种专为高效计算自注意力机制(Self-Attention)而设计的优化算法,由 Tri Dao 等人在 2022 年提出。它能够在不改变自注意力输出结果的前提下,大幅度减少内存占用并加速 计算,尤其在处理长序列输入时效果显著。FlashAttention 的出现主要是为了解决大模型中自注意 力计算的显存和计算瓶颈问题,使得训练更高效。

自注意力机制的核心操作是通过输入序列的查询向量(Q)、键向量(K)和值向量(V)来计算加权 输出。具体计算步骤如下:

  1. 计算 Q 和 K 的相似度,得到注意力分数矩阵$S=QK^{T}$,这是一个规模为 n×n 的矩阵,其中 n 是输入序列长度。
  2. 对注意力分数进行Softmax,然后将 Softmax 结果与 矩阵V 相乘,得到输出。

传统自注意力机制的两个主要问题是:

  • 显存占用大:需要存储整个 n×n 的注意力分数矩阵,显存需求是二次增长的 O($n^2$)。
  • 计算开销高:随着输入序列变长,矩阵乘法的计算复杂度也快速增加。

FlashAttention 的工作流程

  • 将输入序列分块:将 Q、K 和 V 分成多个较小的子块。
  • 逐块计算注意力矩阵:按顺序计算每个子块的注意力分数,并在计算过程中逐步更新 Softmax。
  • 汇总计算结果:对所有子块的计算结果进行汇总,从而得到最终的注意力输出。

在每个子块的计算中,FlashAttention 会复用 GPU 的高速缓存以减少全局显存访问,这极大地提升 了性能。

更多关于FlashAttention的内容可以参考其官网和论文(底层硬件的东西,超出咱的知识范畴咯)。

# CN-CLIP微调后的应用

借助开源数据集中标注好的多标签样本,CN-CLIP在这样的数据上微调,是为了能够将其用于零样本 分类预测, 为本地项目的图像生成标签(亦即用CN-CLIP完成样本多标签标注)。为什么不直接用来进行推理? 因为效果不佳。为什么效果不佳?我们分析是因为国内国外管道的差异以及拍摄图像的质量差异,导 致微调的模型没法得到很准确的预测结果(根据F2-CIW得分衡量)。

基于CN-CLIP微调的模型来完成项目中最受关注的类别进行标注,结合一定的手工校正,有效降低了 标注成本。

# Swin 基础知识和应用

Swin Transformer 同样是嫌弃 Vision Transformer(ViT)模型(论文:An Image is Worth 16x16 Words) 中原始自注意力机制计算复杂度过高而进行的改良。它是从ViT模型演变而来的,将自注意力机制 (Self-Attention)应用于图像识别任务中,旨在提升对高分辨率图像的处理效率。

Swin Transformer 的主要特点包括:

  • 分层结构(Hierarchical Architecture):与传统的 Transformer 不同,Swin Transformer 采用了分层的方式来处理图像。图像在多个尺度上被处理,每一层的特征图尺寸逐渐减小,类似于卷积神经网络(CNN)中的分层结构。这种设计有助于捕捉不同尺度的信息。

  • 窗口注意力(Window-based Attention):Swin Transformer 将图像划分为多个固定大小的窗口,每个窗口内独立应用自注意力。这样可以减少计算复杂度,使模型能够更高效地处理高分辨率图像。

  • 跨窗口交互(Shifted Windows Mechanism):为了实现不同窗口之间的交互,Swin Transformer 使用了“平移窗口”机制。在相邻层中,通过平移窗口的方式,使得每个窗口的边界区域可以共享信息,从而增强了不同区域之间的联系。

  • 计算效率高:由于窗口划分和分层结构的使用,Swin Transformer 相比传统的 Vision Transformer 能够在不显著增加计算量的情况下提升对大图像的处理能力。

更多细节参考SwinTransformer论文精读笔记

# 适配多标签预测

  • 单标签分类与多标签分类的区别

    • 单标签分类(Single-label Classification):模型每次只需要预测一个类别,即图像只属 于一个类别。例如,猫和狗的分类任务中,图像要么是猫,要么是狗。我们通常使用Softmax 激活函数,它将所有类别的概率加起来等于1。

    • 多标签分类(Multi-label Classification):图像可能同时属于多个类别,比如一张图片中 可能同时有“猫”和“狗”。这种情况下,每个类别是独立的,即我们需要判断每个类别的“是否 存在”而不是“唯一所属”,所以不能使用Softmax。

  • 使用Sigmoid激活函数扩展多标签分类

    • 输出层结构:传统单标签分类网络的输出层通常是一个大小为类别数的全连接层,例如,如果 有10个类别,输出层的节点数就是10。但对于多标签分类,我们仍可以使用同样的全连接层, 只是激活函数和输出方式会不同。

    • 激活函数的改变:为了支持多标签分类,我们在输出层中使用Sigmoid激活函数,而不是 Softmax。 为什么使用Sigmoid? Softmax会将所有类别的输出概率归一化,确保总和为1,适合用于单标签分类。 Sigmoid则不归一化,它将每个节点的输出映射到0到1之间的概率,每个类别的预测是独立的。 因此,Sigmoid更适合多标签分类,因为每个类别的概率独立存在,并不需要相加为1。

    • 训练过程: 对于每个类别的预测,使用二元交叉熵损失(Binary Cross-Entropy Loss)来训练模型。 每个类别的标签都是0或1,表示该类别是否在图像中存在。 在多标签任务中,每个类别的标签都是独立的(比如[1, 0, 1, 0, …],表示不同类别的存 在与否),所以我们用二元交叉熵损失来计算每个类别的误差。训练过程中,模型会根据每个 类别的概率输出与真实标签之间的差异调整权重。

  • SewerML-dataset CVPR2021

在这项工作中,我们为基于图像的下水道缺陷分类提供了一个名为Sewer-ML的大型新颖且可公开获 得的多标签分类数据集。Sewer-ML数据集包含130万张图像,这些图像由来自三个不同公用事业公司 的专业下水道检查员在九年中标注。

Label Code Description

CodeDescriptionCIW
VAWater Level (in percentages)0.0310
RBCracks, breaks, and collapses1.0000
OBSurface damage0.5518
PFProduction error0.2896
DEDeformation0.1622
FSDisplaced joint0.6419
ISIntruding sealing material0.1847
RORoots0.3559
INInfiltration0.3131
AFSettled deposits0.0811
BEAttached deposits0.2275
FOObstacle0.2477
GRBranch pipe0.0901
PHChiseled connection0.4167
PBDrilled connection0.4167
OSLateral reinstatement cuts0.9009
OPConnection with transition profile0.3829
OKConnection with construction changes0.4396

**CIW(class importance weight)**是事先给定的评估权重,目的是使模型关注那些少见但代价很高 的缺陷类型。关于标签类别的更多解释,可查看开放访问版本论文

这个数据集对于本项目是个巨大助力(利用得当的话),但如前文所述,此数据集没办法直接利用, 需要做数据和模型层面的调整。

为了使用swin-transformer进行多标签分类,需要将其分类头输出层进行调整,由(softmax)改为 (sigmoid),同时损失函数从cross-entropy改变binary-cross-entropy。实际上就是说,原模型是 预测一个one-hot向量,而多标签分类要求预测一个multi-hot向量,所以改为对每个多标签进行二分 预测,因此输出层调整为sigmoid+BCE。

cfgfile = "./configs/swinv2_model_eval_config.pkl"  # eval mode
pthfile = "./models/swinv2/swinv2_base_patch4_window12_192_22k.pth"
with open(cfgfile, "rb") as f:
    config = pickle.load(f)
# conifg.EVAL_MODE == True
if config.EVAL_MODE is True:    # turn-off EVAL mode
    config.defrost()
    config.EVAL_MODE = False
    config.freeze()

model = build_model(config)
checkpoint = torch.load(pthfile, map_location="cpu")
model.load_state_dict(checkpoint["model"], strict=False)

# Assuming num_labels is the number of classes for the multi-label task
NUM_LABELS = 20  # Adjust this based on your specific task: SewerML dataset
# Modify the output layer of the SwinV2 model
model.head = nn.Sequential(
    nn.Linear(model.head.in_features, NUM_LABELS),
    nn.Sigmoid(),  # Apply sigmoid for multi-label classification
)

# Binary Cross Entropy Loss and Optimizer
criterion = nn.BCELoss()  # Use BCE Loss for multi-label classification
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)

# Training loop
num_epochs = 4  # Specify the number of epochs
model.to(device)
for epoch in range(num_epochs):
    print(f"Epoch {epoch+1}/{num_epochs}")
    # ....

# 长尾分布问题

普通的“过采样/欠采样”对多标签样本集效果不佳:1)过采样(保持比例不变)和欠采样都不能增加 稀有标签的样本,而且欠采样可能会加重不均衡。

多模型集成:将所有稀少标签样本重组为同一个类别“sp”,在样本量正常的训练集训练模型A, 当模型A预测新样本为“sp”时,把这个样本进一步输入给模型B进行预测(模型B就是针对稀少标 签样本训练的模型)。