vgg16使用和微调


vgg16是一个预训练的图片对象识别模型,它能区分一千种类型的物体,根据识别到的物体返回编号,编号对应的物体名称存放在vgg16_imagenet_class.json中

vgg16的使用

python
import torch
import torchvision.transforms as transforms
from torchvision.models import vgg16
from PIL import Image
import json

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  # 设置设备为 GPU(如果可用)或 CPU
torch.cuda.is_available()  # 检查是否有可用的 GPU

# 假设你已经下载好了 vgg16 的 pth 文件,并保存在指定路径
local_weights_path = 'vgg16.pth'  # 本地预训练权重文件的路径

# 加载 VGG16 模型(不加载预训练权重)
model = vgg16(pretrained=False)
# 加载本地的预训练权重
model.load_state_dict(torch.load(local_weights_path))
model.eval()  # 设置模型为评估模式

# 定义图像预处理步骤
preprocess = transforms.Compose([
    transforms.Resize(256),  # 调整图像大小
    transforms.CenterCrop(224),  # 中心裁剪
    transforms.ToTensor(),  # 转换为张量
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])  # 标准化
])

# 加载并预处理图像
img_path = 'data/presidential_doggy_door/valid/not_bo/137.jpg'  # 替换为你的图像路径
img = Image.open(img_path)
img_tensor = preprocess(img)
img_tensor = img_tensor.unsqueeze(0)  # 添加批次维度

# 进行预测
with torch.no_grad():
    output = model(img_tensor)

# 解释预测结果
# 加载 ImageNet 类别标签
with open('data/vgg16_imagenet_class.json') as f:
    labels = json.load(f)

# 获取预测的类别
_, predicted_idx = torch.max(output, 1)
print('predicted_idx:',predicted_idx.item())
# predicted_label = labels[str(predicted_idx.item())]

# print(f'预测结果: {predicted_label}')

vgg16迁移训练微调

在下面的微调中,我冻结原模型参数,我在一千层后,添加一个全新连接层,指定输出类别是1,即只区分是名叫“bo”这只狗,如果是其它动物或者其它狗就不会是1,最终这个模型就会变成只区分这是不是“bo”这只狗

python
import torch
import torch.nn as nn
from torch.optim import Adam
from torch.utils.data import Dataset, DataLoader
import torchvision.transforms.v2 as transforms
# import torchvision.io as tv_io

import glob
# import json
from PIL import Image

# import utils
from torchvision.models import vgg16
from torchvision.models import VGG16_Weights

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  # 设置设备为 GPU(如果可用)或 CPU
torch.cuda.is_available()  # 检查是否有可用的 GPU


# 加载预训练的 VGG16 网络(基于 ImageNet 数据集)
weights = VGG16_Weights.DEFAULT  # 获取 VGG16 的默认预训练权重

# 假设你已经下载好了 vgg16 的 pth 文件,并保存在指定路径
local_weights_path = 'vgg16.pth'  # 本地预训练权重文件的路径

# 加载 VGG16 模型
vgg_model = vgg16()  # 初始化 VGG16 模型
# 加载本地的预训练权重
vgg_model.load_state_dict(torch.load(local_weights_path))  # 加载本地预训练权重
vgg_model.to(device)  # 将模型移动到指定设备


# 只训练新的层,所以把之前的层训练选项关掉,True 为修改前面的参数,False 为不修改前面的参数
######################################################
vgg_model.requires_grad_(False)  # 冻结 VGG16 模型的参数,这里决定是否修改原模型的参数
######################################################
print("VGG16 解冻")  # 打印冻结信息

# 定义输出类别数,因为VGG16 模型的最后一层全连接层的输出维度是 1000(对应 ImageNet 数据集的 1000 个类别)
N_CLASSES = 1

my_model = nn.Sequential(  # 构建新的模型,包含 VGG16 和一个新的全连接层
    vgg_model,
    nn.Linear(1000, N_CLASSES)  # 添加一个新的全连接层,输出类别数为1
)

# 打印模型的参数是否被冻结
# for idx, param in enumerate(my_model.parameters()):
#     print(idx, param.requires_grad)
loss_function = nn.BCEWithLogitsLoss()  # 定义损失函数为二元交叉熵损失
# 定义优化器为 Adam, 可选参数学习率为 0.000001,因为我们只训练最后一层,所以学习率可以设置的很小
#学习率很小的作用是,不会对原模型的参数造成太大的影响,尤其是原模型的参数已经很好了,我们只需要进行微调
######################################################
optimizer = Adam(my_model.parameters(), lr=.000002)
######################################################
my_model = my_model.to(device)  # 将模型移动到指定设备

pre_trans = weights.transforms()  # 获取预训练权重的预处理变换


# 定义数据集
DATA_LABELS = ["bo", "not_bo"]  # 定义数据标签
    
class MyDataset(Dataset):  # 自定义数据集类
    def __init__(self, data_dir):  # 初始化函数,接受数据目录
        self.imgs = []  # 初始化图像列表
        self.labels = []  # 初始化标签列表
        
        for l_idx, label in enumerate(DATA_LABELS):  # 遍历每个标签
            data_paths = glob.glob(data_dir + label + '/*.jpg', recursive=True)  # 获取所有图像路径
            for path in data_paths:  # 遍历每个图像路径
                img = Image.open(path)  # 打开图像
                self.imgs.append(pre_trans(img).to(device))  # 预处理图像并添加到列表
                self.labels.append(torch.tensor(l_idx).to(device).float())  # 添加标签到列表


    def __getitem__(self, idx):  # 获取指定索引的数据
        img = self.imgs[idx]  # 获取图像
        label = self.labels[idx]  # 获取标签
        return img, label  # 返回图像和标签

    def __len__(self):  # 获取数据集的长度
        return len(self.imgs)  # 返回图像列表的长度
    


# 定义训练和验证数据集    
n = 32  # 定义批次大小

train_path = "data/presidential_doggy_door/train/"  # 训练数据路径
train_data = MyDataset(train_path)  # 创建训练数据集
train_loader = DataLoader(train_data, batch_size=n, shuffle=True)  # 创建训练数据加载器
train_N = len(train_loader.dataset)  # 获取训练数据集的大小,用于训练计算准确率

valid_path = "data/presidential_doggy_door/valid/"  # 验证数据路径
valid_data = MyDataset(valid_path)  # 创建验证数据集
valid_loader = DataLoader(valid_data, batch_size=n)  # 创建验证数据加载器
valid_N = len(valid_loader.dataset)  # 获取验证数据集的大小,用于验证计算准确率


# 数据增强
IMG_WIDTH, IMG_HEIGHT = (224, 224)  # 定义图像宽度和高度

random_trans = transforms.Compose([  # 定义数据增强变换
    transforms.RandomRotation(25),  # 随机旋转
    transforms.RandomResizedCrop((IMG_WIDTH, IMG_HEIGHT), scale=(.8, 1), ratio=(1, 1)),  # 随机裁剪
    transforms.RandomHorizontalFlip(),  # 随机水平翻转
    transforms.ColorJitter(brightness=.2, contrast=.2, saturation=.2, hue=.2)  # 随机颜色抖动
])

# 定义训练函数
def get_batch_accuracy(output, y, N):  # 计算批次准确率的函数
    zero_tensor = torch.tensor([0]).to(device)  # 创建一个值为 0 的张量
    pred = torch.gt(output, zero_tensor)  # 获取预测结果
    correct = pred.eq(y.view_as(pred)).sum().item()  # 计算预测正确的数量
    return correct / N  # 返回正确预测的比例


# 打印最后一层的梯度
def train(model, check_grad=False):  # 定义训练函数
    loss = 0  # 初始化损失
    accuracy = 0  # 初始化准确率

    model.train()  # 将模型设置为训练模式
    for x, y in train_loader:  # 遍历训练数据加载器中的每个批次
        output = torch.squeeze(model(random_trans(x)))  # 对输入数据进行随机变换并传递给模型
        optimizer.zero_grad()  # 清零梯度
        batch_loss = loss_function(output, y)  # 计算损失
        batch_loss.backward()  # 反向传播
        optimizer.step()  # 更新模型参数

        loss += batch_loss.item()  # 累加损失
        accuracy += get_batch_accuracy(output, y, train_N)  # 累加准确率
    if check_grad:  # 如果需要检查梯度
        print('最后一层梯度:')  # 打印最后一层的梯度
        for param in model.parameters():  # 遍历模型的参数
            print(param.grad)  # 打印参数的梯度

    print('训练 - 损失: {:.4f} 准确率: {:.4f}'.format(loss, accuracy))  # 打印训练结果


def validate(model):  # 定义验证函数
    loss = 0  # 初始化损失
    accuracy = 0  # 初始化准确率

    model.eval()  # 将模型设置为评估模式
    with torch.no_grad():  # 在不计算梯度的上下文中
        for x, y in valid_loader:  # 遍历验证数据加载器中的每个批次
            output = torch.squeeze(model(x))  # 将输入数据传递给模型

            loss += loss_function(output, y.float()).item()  # 累加损失
            accuracy += get_batch_accuracy(output, y, valid_N)  # 累加准确率
    print('验证 - 损失: {:.4f} 准确率: {:.4f}'.format(loss, accuracy))  # 打印验证结果


# 开始训练模型
epochs = 3  # 定义训练的轮数,轮数越多,模型的性能可能越好,但因VGG16是很大的模型,训练过长很容易拟合

for epoch in range(epochs):  # 遍历每个训练轮次
    print('当前: {}'.format(epoch))  # 打印当前轮次
    train(my_model, check_grad=False)  # 训练模型,不检查梯度
    # train(my_model, check_grad=True) # 训练模型,检查梯度
    validate(my_model)  # 验证模型

# # 保存整个模型到一个新的文件中
# torch.save(my_model.state_dict(), 'my_model.pth')  # 保存模型的状态字典到文件

# 预测检测结果
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
# 显示图像
def show_image(image_path):
    image = mpimg.imread(image_path)
    plt.imshow(image)

# 预测实现函数
def make_prediction(file_path):
    show_image(file_path)
    image = Image.open(file_path)
    image = pre_trans(image).to(device)
    image = image.unsqueeze(0)
    output = my_model(image)
    prediction = output.item()
    return prediction

# 判断是否是 Bo
def presidential_doggy_door(image_path):
    pred = make_prediction(image_path)
    if pred < 0:
        print(image_path,"这是 Bo! 欢迎进入!", pred)
    else:
        print(image_path,"这不是 Bo! 请离开!", pred)

presidential_doggy_door('data/presidential_doggy_door/valid/bo/bo_20.jpg')  # 预测结果
presidential_doggy_door('data/presidential_doggy_door/valid/not_bo/121.jpg')