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)来计算加权 输出。具体计算步骤如下:
- 计算 Q 和 K 的相似度,得到注意力分数矩阵$S=QK^{T}$,这是一个规模为 n×n 的矩阵,其中 n 是输入序列长度。
- 对注意力分数进行
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, …],表示不同类别的存 在与否),所以我们用二元交叉熵损失来计算每个类别的误差。训练过程中,模型会根据每个 类别的概率输出与真实标签之间的差异调整权重。
在这项工作中,我们为基于图像的下水道缺陷分类提供了一个名为Sewer-ML的大型新颖且可公开获 得的多标签分类数据集。Sewer-ML数据集包含130万张图像,这些图像由来自三个不同公用事业公司 的专业下水道检查员在九年中标注。
Label Code Description
Code | Description | CIW |
---|---|---|
VA | Water Level (in percentages) | 0.0310 |
RB | Cracks, breaks, and collapses | 1.0000 |
OB | Surface damage | 0.5518 |
PF | Production error | 0.2896 |
DE | Deformation | 0.1622 |
FS | Displaced joint | 0.6419 |
IS | Intruding sealing material | 0.1847 |
RO | Roots | 0.3559 |
IN | Infiltration | 0.3131 |
AF | Settled deposits | 0.0811 |
BE | Attached deposits | 0.2275 |
FO | Obstacle | 0.2477 |
GR | Branch pipe | 0.0901 |
PH | Chiseled connection | 0.4167 |
PB | Drilled connection | 0.4167 |
OS | Lateral reinstatement cuts | 0.9009 |
OP | Connection with transition profile | 0.3829 |
OK | Connection with construction changes | 0.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就是针对稀少标 签样本训练的模型)。