摘要
本周围绕机器学习中的迁移学习、模型微调及数据操作展开,重点介绍迁移学习的核心逻辑与微调策略,并结合具体案例实现模型训练与评估,同时系统阐述数据增强的方法、流程及应用场景。
Abstract
This week focuses on transfer learning, model fine-tuning, and data manipulation in machine learning, focusing on the core logic and fine-tuning strategies of transfer learning, and implementing model training and evaluation based on specific cases, while systematically explaining the methods, processes, and application scenarios of data enhancement.
1迁移学习和微调
预训练模型是一个之前基于大型数据集(通常是大型图像分类任务)训练的已保存网络。可以按原样使用预训练模型,也可以使用迁移学习针对给定任务自定义此模型。
用于图像分类的迁移学习背后的理念是,如果一个模型是基于足够大且通用的数据集训练的,那么该模型将有效地充当视觉世界的通用模型。随后,可以利用这些学习到的特征映射,而不必通过基于大型数据集训练大型模型而从头开始。
通过以下两种方式来自定义预训练模型:
特征提取:使用先前网络学习的表示从新样本中提取有意义的特征。只训练网络的顶层,网络的其余部分保持不变。当新的数据集相对较小且与原始数据集相似时,考虑使用特征提取方法。在这种情况下,从原始数据集中学习到的高级特征应该能很好地转移到新的数据集中。
微调:解冻已冻结模型库的一些顶层,并共同训练新添加的分类器层和基础模型的最后几层。这样,便能“微调”基础模型中的高阶特征表示,以使其与特定任务更相关。
就是先把之前预训练模型的参数冻结,然后加入新的层进行训练,对最后一部分新加入的层进行训练到一定程度,然后解冻参数,一起进行训练,就是微调。
(1) 目标数据集较小且与原始训练集相似
将最后一个全连接层用一个新的全连接层代替,该层分类数与目标数据集的类数相匹配。用随机权重初始化旧的权重。迁移学习可以作为一种避免过拟合的策略,尤其当数据集较小的时候。
图1.1 迁移学习
(2) 目标数据集较小且与原始训练集不同
如果目标数据集较小,但是与原始数据集类型不同,例如原始数据集是dog图像,而新(目标)数据集是 flower图像,那么执行以下操作:
将网络的大多数初始化层分为不同组。
向剩余的预训练层添加一个新的全连接层,该层分类数与目标数据集的类数相匹配。随机化新的全连接层的权重,并冻结所有预先训练好的网络的权重。
训练网络以更新新的全连接层的权重。
由于数据集较小,过拟合在这里仍然是一个问题。为了解决这个问题,我们将保原始预训练网络的权值不变,只更新新的全连接层的权重,
只对网络的高级部分调优。这是因为开始层的设计是为了提取更通用的特征。一般来说,卷积神经网络的第一层并不针对一个数据集。
(3) 目标数据集很大且与原始训练集相似
不用担心过拟合,因为数据集很大。所以在这种情况下,可以重新训练整个网络)
删除最后一个全连接层,并用一个新的全连接层来代替,该层分类数与目标数据集的类数相匹配。
随机初始化这个新添加的全连接层的权重,用预先训练好的权重初始化其余权重
训练整个网络。
(4)目标数据集很大且与原始训练集不同
如果目标数据集很大并且与原始训练集不同
删除最后一个全连接层,并用一个新的全连接层来代替,该层分类数与目标数据集的类数相匹配。
用随机初始化的权重从头开始训练这个网络。
微调的结果已经是在最优解附近,不要设置太长的训练时间和太高的学习率,避免模型离最优解走得太远了。
import os
import torch
import timm
import torchvision
import torch.nn as nn
import torch.optim as optim
from torchvision import transforms, datasets
from torch.utils.data import DataLoader, random_split
import matplotlib.pyplot as plt
import numpy as np
import zipfile
from tqdm import tqdm
# 下载并解压 Animals-10 数据集
def extract_dataset():
zip_path = "archive.zip"
extract_path = "animals10"
if not os.path.exists(extract_path):
print("解压数据集...")
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(extract_path)
os.remove(zip_path)
print("数据集准备完成!")
else:
print("数据集已存在,跳过下载")
return extract_path
# 下载数据集
data_dir = extract_dataset()
# 数据预处理
def get_data_loaders(data_dir, batch_size=32, img_size=224):
# 数据增强和归一化
transform = transforms.Compose([
transforms.Resize((img_size, img_size)),
transforms.RandomHorizontalFlip(),
transforms.RandomRotation(10),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
# 加载数据集
full_dataset = datasets.ImageFolder(root=data_dir, transform=transform)
# 划分训练集和验证集 (80% 训练, 20% 验证)
train_size = int(0.8 * len(full_dataset))
val_size = len(full_dataset) - train_size
train_dataset, val_dataset = random_split(full_dataset, [train_size, val_size])
# 创建数据加载器
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=4)
return train_loader, val_loader, full_dataset.classes
# 设置参数
batch_size = 32
img_size = 224
# 获取数据加载器
train_loader, val_loader, class_names = get_data_loaders(data_dir, batch_size, img_size)
print(f"类别数量: {len(class_names)}")
print(f"类别名称: {class_names}")
# 创建模型
def create_model(num_classes, device, pretrained=True, freeze_backbone=False):
# 使用 timm 创建 ResNet18 模型
model = timm.create_model('resnet18', pretrained=pretrained, num_classes=num_classes)
# 冻结预训练层的权重(可选)
if freeze_backbone:
for param in model.parameters():
param.requires_grad = False
# 解冻最后的全连接层
for param in model.fc.parameters():
param.requires_grad = True
# 将模型移动到设备
model = model.to(device)
return model
# 设置设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {device}")
# 创建模型
num_classes = len(class_names)
model = create_model(num_classes, device, pretrained=True, freeze_backbone=False)
def train_model(model, train_loader, val_loader, criterion, optimizer, num_epochs=10):
best_acc = 0.0
train_losses, val_losses = [], []
train_accs, val_accs = [], []
for epoch in range(num_epochs):
print(f"Epoch {epoch+1}/{num_epochs}")
print('-' * 10)
# 训练阶段
model.train()
running_loss = 0.0
running_corrects = 0
for inputs, labels in tqdm(train_loader, desc="训练"):
inputs = inputs.to(device)
labels = labels.to(device)
optimizer.zero_grad()
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(preds == labels.data)
epoch_loss = running_loss / len(train_loader.dataset)
epoch_acc = running_corrects.double() / len(train_loader.dataset)
train_losses.append(epoch_loss)
train_accs.append(epoch_acc.item())
print(f"训练损失: {epoch_loss:.4f}, 准确率: {epoch_acc:.4f}")
# 验证阶段
model.eval()
running_loss = 0.0
running_corrects = 0
with torch.no_grad():
for inputs, labels in tqdm(val_loader, desc="验证"):
inputs = inputs.to(device)
labels = labels.to(device)
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
loss = criterion(outputs, labels)
running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(preds == labels.data)
epoch_loss = running_loss / len(val_loader.dataset)
epoch_acc = running_corrects.double() / len(val_loader.dataset)
val_losses.append(epoch_loss)
val_accs.append(epoch_acc.item())
print(f"验证损失: {epoch_loss:.4f}, 准确率: {epoch_acc:.4f}")
# 保存最佳模型
if epoch_acc > best_acc:
best_acc = epoch_acc
torch.save(model.state_dict(), 'best_model.pth')
print("保存最佳模型")
print(f"最佳验证准确率: {best_acc:.4f}")
return {
'model': model,
'train_losses': train_losses,
'val_losses': val_losses,
'train_accs': train_accs,
'val_accs': val_accs,
'best_acc': best_acc
}
# 设置训练参数
num_epochs = 10
learning_rate = 0.001
# 损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
# 训练模型
results = train_model(
model,
train_loader,
val_loader,
criterion,
optimizer,
num_epochs=num_epochs
)
def plot_training_results(results):
# 创建图表
plt.figure(figsize=(12, 5))
# 损失曲线
plt.subplot(1, 2, 1)
plt.plot(results['train_losses'], label='训练损失')
plt.plot(results['val_losses'], label='验证损失')
plt.title('训练和验证损失')
plt.xlabel('Epoch')
plt.ylabel('损失')
plt.legend()
# 准确率曲线
plt.subplot(1, 2, 2)
plt.plot(results['train_accs'], label='训练准确率')
plt.plot(results['val_accs'], label='验证准确率')
plt.title('训练和验证准确率')
plt.xlabel('Epoch')
plt.ylabel('准确率')
plt.legend()
plt.tight_layout()
plt.savefig('training_results.png')
plt.show()
# 绘制结果
plot_training_results(results)
def test_model(model, val_loader, class_names):
model.eval()
correct = 0
total = 0
all_preds = []
all_labels = []
with torch.no_grad():
for inputs, labels in tqdm(val_loader, desc="测试"):
inputs = inputs.to(device)
labels = labels.to(device)
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
total += labels.size(0)
correct += (preds == labels).sum().item()
all_preds.extend(preds.cpu().numpy())
all_labels.extend(labels.cpu().numpy())
accuracy = correct / total
print(f"测试准确率: {accuracy:.4f}")
# 可视化一些预测结果
visualize_predictions(val_loader, model, class_names, num_images=9)
return accuracy
def visualize_predictions(data_loader, model, class_names, num_images=9):
model.eval()
images_so_far = 0
plt.figure(figsize=(15, 15))
with torch.no_grad():
for inputs, labels in data_loader:
inputs = inputs.to(device)
labels = labels.to(device)
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
for j in range(inputs.size()[0]):
if images_so_far >= num_images:
return
images_so_far += 1
ax = plt.subplot(3, 3, images_so_far)
ax.axis('off')
# 反归一化图像
img = inputs.cpu().data[j].numpy().transpose((1, 2, 0))
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
img = std * img + mean
img = np.clip(img, 0, 1)
ax.imshow(img)
ax.set_title(f'预测: {class_names[preds[j]]}\n真实: {class_names[labels[j]]}')
if images_so_far == num_images:
plt.savefig('predictions.png')
plt.show()
return
# 加载最佳模型
model.load_state_dict(torch.load('best_model.pth'))
model = model.to(device)
# 测试模型
test_accuracy = test_model(model, val_loader, class_names)
下载kaggle的animals10数据集和timm的resnet18的预训练模型,通过resnet18模型,替换最后的全连接层来适应数据集,使用 Adam 优化器,随机水平翻转和旋转数据增强。最后对测试结果的准确律为1。
2数据操作
一种通过应用随机(但真实)的变换(例如图像旋转)来增加训练集多样性的技术。为了获得更多的数据,只要对现有的数据集进行微小的改变。比如旋转(flips)、移位(translations)、旋转(rotations)等微小的改变。模型会认为这是不同的图片。
2.1常用技巧(按数据类型划分)
数据增强的方法高度依赖于数据的类型。
1. 图像数据
这是应用最广泛、技巧最丰富的领域。
几何变换:
翻转(Flip):水平翻转(最常用)、垂直翻转(适用于某些场景,如天文图像)。
旋转(Rotation):随机旋转一定角度(如 ±15°)。需注意旋转后可能出现的黑边处理(填充、裁剪等)。
裁剪(Cropping):随机裁剪图像的一部分,然后缩放到原始尺寸。也叫随机裁剪(Random Crop)。
缩放(Zooming):随机放大或缩小图像。
平移(Translation):在水平或垂直方向上随机移动图像。
拉伸(Shearing):沿某一轴拉伸图像,产生形变。
像素/色彩变换:
亮度、对比度、饱和度调整:随机微调这些值。
色彩抖动(Color Jittering):综合随机调整亮度、对比度、饱和度和色调。
添加噪声:加入高斯噪声、椒盐噪声等,使模型对噪声更鲁棒。
模糊(Blurring):使用高斯模糊、均值模糊等模拟失焦情况。
锐化(Sharpening):相反的操作,增强边缘。
高级/混合技术:
Cutout:随机将图像中的一块矩形区域置黑(或填充其他值),迫使模型不只关注最明显的特征。
MixUp:将两张图像按一定比例混合(像素值加权求和),同时其标签也进行相应比例的混合。这是一种在标签空间也进行增强的方法。
CutMix:将一张图像的一部分随机裁剪出来,粘贴到另一张图像的对应位置。标签则根据裁剪区域的比例进行混合(如,狗的图像被裁剪了30%的区域并粘贴到猫的图像上,新图像的标签可能是 [0.3(狗), 0.7(猫)])。
风格迁移(Style Transfer):改变图像的纹理和风格,同时保留内容,用于增加数据风格的多样性。
2. 文本数据
文本增强相对更复杂,因为需要保持语义的连贯性和语法正确性。
词汇级:
同义词替换(SR: Synonym Replacement):随机选择一些非停用词,用其同义词替换。
随机插入(RI: Random Insertion):随机选择一个词的同义词,插入句子的随机位置。
随机交换(RS: Random Swap):随机交换句子中两个词的位置。
随机删除(RD: Random Deletion):以一定概率随机删除句子中的词。
句子级:
回译(Back Translation):将文本翻译成另一种语言(如中文->德文),再翻译回原语言(德文->中文)。由于语言模型的差异,回译后的句子会与原文表述不同但含义相近。
文本生成:使用预训练的语言模型(如GPT、T5)根据原句生成 paraphrase(复述)句子。
语法树操作:通过解析和生成语法树来改变句子结构。
3. 音频数据
时域变换:加速、减速、添加时间偏移、裁剪片段。
频域变换:添加背景噪声、改变音高(Pitch)、调整音量和速度、应用音频滤波器(如低通、高通)。
更高级的技术:如 SpecAugment,专门针对频谱图(音频的视觉表示)进行增强,包括掩蔽时间帧和频率通道。
2.2数据增强的流程
数据增强不是一个独立的步骤,而是深度集成在训练流程中的。其标准流程如下:
数据准备与加载
收集和清洗原始训练数据集。
将数据划分为训练集、验证集和测试集。非常重要的一点:数据增强通常只应用于训练集。 验证集和测试集应保持原始数据,用于 unbiased(无偏)地评估模型性能。
定义增强变换管道(Pipeline)
根据任务的特性和数据的特性,选择上述一种或多种增强技巧。
为每种技巧定义参数范围(例如,旋转角度范围为 -15° to +15°
)。
使用深度学习框架(如 TensorFlow/Keras, PyTorch, Torchvision)提供的工具(如 ImageDataGenerator
, torchvision.transforms
)来构建一个随机增强管道。
示例:
from torchvision import transforms
train_transforms = transforms.Compose([
transforms.RandomHorizontalFlip(p=0.5), # 50%概率水平翻转
transforms.RandomRotation(degrees=15), # 随机旋转 ±15度
transforms.ColorJitter(brightness=0.2, contrast=0.2), # 色彩抖动
transforms.RandomCrop(32, padding=4), # 随机裁剪
transforms.ToTensor(), # 转换为张量
transforms.Normalize(mean, std) # 标准化
])
实时/在线增强(On-the-fly Augmentation)
这是最常用的方式。在每个训练周期(Epoch) 中,当数据被加载到内存或GPU之前,随机应用管道中定义的变换。
关键:每个Epoch的变换都是随机的,这意味着模型在每个Epoch看到的同一张图片的增强版本都是不同的。这极大地增加了数据的多样性。
这与离线增强(提前生成所有增强后的图片并保存到硬盘)相比,节省存储空间,且多样性无限。
模型训练
训练流程保持不变。模型接收增强后的批量(Batch)数据,计算损失,并反向传播更新权重。
模型通过学习这些“扭曲”过的数据,逐渐学会关注更本质的特征,而不是记忆训练集中的特定细节。
验证与测试
使用未增强的原始数据进行验证和测试,以监控模型的真实泛化能力并给出最终评价。
总结
本周学习了迁移学习、模型微调和数据增强方面的知识,后面将会对音频方面进行学习。