Implementação do Módulo Backbone do YOLOv5 para Classificação de Imagens

1. Configuração do Disopsitivo

import torch
import torch.nn as nn
import torchvision.transforms as transforms
from torchvision import datasets
import pathlib, warnings

warnings.filterwarnings("ignore")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

2. Carregamento dos Dados

data_dir = pathlib.Path('./data/8-data')
class_names = [p.name for p in data_dir.iterdir() if p.is_dir()]

transform = transforms.Compose([
    transforms.Resize([224, 224]),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

dataset = datasets.ImageFolder('./data/8-data/', transform=transform)

3. Divisão do Conjunto de Dados

train_size = int(0.8 * len(dataset))
test_size = len(dataset) - train_size
train_set, test_set = torch.utils.data.random_split(dataset, [train_size, test_size])

batch_size = 4
train_loader = torch.utils.data.DataLoader(train_set, batch_size=batch_size, shuffle=True, num_workers=3)
test_loader = torch.utils.data.DataLoader(test_set, batch_size=batch_size, shuffle=False, num_workers=3)

Componentes Básicos

def autopad(kernel, padding=None):
    if padding is None:
        padding = kernel // 2 if isinstance(kernel, int) else [k // 2 for k in kernel]
    return padding

class ConvBlock(nn.Module):
    def __init__(self, in_ch, out_ch, kernel=1, stride=1, padding=None, groups=1, activation=True):
        super().__init__()
        self.conv = nn.Conv2d(in_ch, out_ch, kernel, stride, autopad(kernel, padding), groups=groups, bias=False)
        self.norm = nn.BatchNorm2d(out_ch)
        self.act = nn.SiLU() if activation else nn.Identity()

    def forward(self, x):
        return self.act(self.norm(self.conv(x)))

class BottleneckUnit(nn.Module):
    def __init__(self, in_ch, out_ch, use_shortcut=True, groups=1, expansion=0.5):
        super().__init__()
        hidden = int(out_ch * expansion)
        self.conv1 = ConvBlock(in_ch, hidden, 1, 1)
        self.conv2 = ConvBlock(hidden, out_ch, 3, 1, groups=groups)
        self.use_add = use_shortcut and in_ch == out_ch

    def forward(self, x):
        out = self.conv2(self.conv1(x))
        return x + out if self.use_add else out

class CSPBlock(nn.Module):
    def __init__(self, in_ch, out_ch, repeats=1, shortcut=True, groups=1, expansion=0.5):
        super().__init__()
        hidden = int(out_ch * expansion)
        self.branch1 = ConvBlock(in_ch, hidden, 1, 1)
        self.branch2 = ConvBlock(in_ch, hidden, 1, 1)
        self.merge = ConvBlock(2 * hidden, out_ch, 1, 1)
        self.blocks = nn.Sequential(*[BottleneckUnit(hidden, hidden, shortcut, groups, 1.0) for _ in range(repeats)])

    def forward(self, x):
        return self.merge(torch.cat([self.blocks(self.branch1(x)), self.branch2(x)], dim=1))

class FastSPP(nn.Module):
    def __init__(self, in_ch, out_ch, pool_kernel=5):
        super().__init__()
        hidden = in_ch // 2
        self.pre_conv = ConvBlock(in_ch, hidden, 1, 1)
        self.post_conv = ConvBlock(hidden * 4, out_ch, 1, 1)
        self.pool = nn.MaxPool2d(pool_kernel, stride=1, padding=pool_kernel // 2)

    def forward(self, x):
        x = self.pre_conv(x)
        p1 = self.pool(x)
        p2 = self.pool(p1)
        p3 = self.pool(p2)
        return self.post_conv(torch.cat([x, p1, p2, p3], dim=1))

Definição do Backbone do YOLOv5

class YOLOv5Backbone(nn.Module):
    def __init__(self, num_classes=4):
        super().__init__()
        self.stem = ConvBlock(3, 64, 3, 2, padding=2)
        self.stage1 = nn.Sequential(
            ConvBlock(64, 128, 3, 2),
            CSPBlock(128, 128)
        )
        self.stage2 = nn.Sequential(
            ConvBlock(128, 256, 3, 2),
            CSPBlock(256, 256)
        )
        self.stage3 = nn.Sequential(
            ConvBlock(256, 512, 3, 2),
            CSPBlock(512, 512)
        )
        self.stage4 = nn.Sequential(
            ConvBlock(512, 1024, 3, 2),
            CSPBlock(1024, 1024),
            FastSPP(1024, 1024)
        )
        self.classifier = nn.Sequential(
            nn.Linear(1024 * 8 * 8, 100),
            nn.ReLU(),
            nn.Linear(100, num_classes)
        )

    def forward(self, x):
        x = self.stem(x)
        x = self.stage1(x)
        x = self.stage2(x)
        x = self.stage3(x)
        x = self.stage4(x)
        x = torch.flatten(x, 1)
        return self.classifier(x)

model = YOLOv5Backbone().to(device)

Funções de Treinamento e Validação

def train_epoch(loader, model, criterion, optimizer):
    model.train()
    total_loss = 0
    correct = 0
    for inputs, targets in loader:
        inputs, targets = inputs.to(device), targets.to(device)
        outputs = model(inputs)
        loss = criterion(outputs, targets)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
        correct += (outputs.argmax(1) == targets).sum().item()
    acc = correct / len(loader.dataset)
    avg_loss = total_loss / len(loader)
    return acc, avg_loss

def validate(loader, model, criterion):
    model.eval()
    total_loss = 0
    correct = 0
    with torch.no_grad():
        for inputs, targets in loader:
            inputs, targets = inputs.to(device), targets.to(device)
            outputs = model(inputs)
            loss = criterion(outputs, targets)
            total_loss += loss.item()
            correct += (outputs.argmax(1) == targets).sum().item()
    acc = correct / len(loader.dataset)
    avg_loss = total_loss / len(loader)
    return acc, avg_loss

Execução do Treinamento

optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
criterion = nn.CrossEntropyLoss()
epochs = 20

train_acc_history, train_loss_history = [], []
val_acc_history, val_loss_history = [], []
best_accuracy = 0.0

for epoch in range(epochs):
    train_acc, train_loss = train_epoch(train_loader, model, criterion, optimizer)
    val_acc, val_loss = validate(test_loader, model, criterion)
    
    if val_acc > best_accuracy:
        best_accuracy = val_acc
        torch.save(model.state_dict(), 'best_model.pth')
    
    train_acc_history.append(train_acc)
    train_loss_history.append(train_loss)
    val_acc_history.append(val_acc)
    val_loss_history.append(val_loss)
    
    print(f'Epoch {epoch+1:2d} | Train Acc: {train_acc*100:.1f}% | Val Acc: {val_acc*100:.1f}%')

Gráficos de Desempenho

import matplotlib.pyplot as plt

plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(train_acc_history, label='Treino')
plt.plot(val_acc_history, label='Validação')
plt.title('Acurácia por Época')
plt.legend()

plt.subplot(1, 2, 2)
plt.plot(train_loss_history, label='Treino')
plt.plot(val_loss_history, label='Validação')
plt.title('Perda por Época')
plt.legend()
plt.show()

Avaliação do Melhor Modelo

model.load_state_dict(torch.load('best_model.pth', map_location=device))
final_acc, final_loss = validate(test_loader, model, criterion)
print(f'Acurácia final: {final_acc:.4f}')

Tags: YOLOv5 Pytorch ComputerVision DeepLearning ImageClassification

Publicado em 8-22 02:57