Compartilhamento de tecnologia

VGG16 implementa implementação pytorch de classificação de imagens e explica as etapas em detalhes

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

VGG16 implementa classificação de imagens

Aqui implementamos uma rede VGG-16 para classificar o conjunto de dados CIFAR

Introdução à rede VGG16

Prefácio

《Redes convolucionais muito profundas para reconhecimento de imagens em larga escala》

ICLR 2015

VGGÉ de OxfordVisualGeometriaG Proposto pelo grupo do grupo (você deverá conseguir ver a origem do nome VGG). Esta rede é um trabalho relacionado no ILSVRC 2014. O trabalho principal é provar que aumentar a profundidade da rede pode afetar até certo ponto o desempenho final da rede. O VGG possui duas estruturas, nomeadamente VGG16 e VGG19. Não há diferença essencial entre os dois, mas a profundidade da rede é diferente.

Princípio VGG

Uma melhoria do VGG16 em comparação com AlexNet é queUse vários kernels de convolução 3x3 consecutivos para substituir os kernels de convolução maiores no AlexNet (11x11, 7x7, 5x5) . Para um determinado campo receptivo (o tamanho local da imagem de entrada em relação à saída), o uso de pequenos núcleos de convolução empilhados é melhor do que o uso de grandes núcleos de convolução, porque múltiplas camadas não lineares podem aumentar a profundidade da rede para garantir um modo de aprendizado mais complexo e. o custo é relativamente pequeno (menos parâmetros).

Simplificando, no VGG, três kernels de convolução 3x3 são usados ​​para substituir o kernel de convolução 7x7, e dois kernels de convolução 3x3 são usados ​​para substituir o kernel de convolução 5*5. Do campo receptivo, a profundidade da rede é melhorada e o efeito da rede neural é melhorado até certo ponto.

Por exemplo, a superposição camada por camada de três núcleos de convolução 3x3 com um passo de 1 pode ser considerada como um campo receptivo de tamanho 7 (na verdade, isso significa que três convoluções contínuas 3x3 são equivalentes a uma convolução 7x7), e seu os parâmetros totais são A quantidade é 3x (9xC ^ 2). Se o kernel de convolução 7x7 for usado diretamente, o número total de parâmetros é 49xC ^ 2, onde C se refere ao número de canais de entrada e saída.Obviamente, 27xC2 menos de 49xC2, ou seja, os parâmetros são reduzidos e o kernel de convolução 3x3 contribui para melhor manter as propriedades da imagem;

Aqui está uma explicação de por que dois kernels de convolução 3x3 podem ser usados ​​em vez de kernels de convolução 5*5:

A convolução 5x5 é considerada uma pequena rede totalmente conectada que desliza na área 5x5. Podemos primeiro convolucionar com um filtro de convolução 3x3 e, em seguida, usar uma camada totalmente conectada para conectar a saída de convolução 3x3. ser visto como uma camada convolucional 3x3. Desta forma, podemos colocar em cascata (sobrepor) duas convoluções 3x3 em vez de uma convolução 5x5.

Os detalhes são mostrados na figura abaixo:

Insira a descrição da imagem aqui

Estrutura da rede VGG

Insira a descrição da imagem aqui

A seguir está a estrutura da rede VGG (VGG16 e VGG19 estão presentes):

Insira a descrição da imagem aqui

Estrutura da rede GG

VGG16 contém 16 camadas ocultas (13 camadas convolucionais e 3 camadas totalmente conectadas), conforme mostrado na coluna D da figura acima

VGG19 contém 19 camadas ocultas (16 camadas convolucionais e 3 camadas totalmente conectadas), conforme mostrado na coluna E na figura acima

A estrutura da rede VGG é muito consistente, usando convolução 3x3 e pooling máximo de 2x2 do início ao fim.

Vantagens do VGG

A estrutura do VGGNet é muito simples. Toda a rede usa o mesmo tamanho de kernel de convolução (3x3) e tamanho máximo de pooling (2x2).

A combinação de várias camadas convolucionais de filtro pequeno (3x3) é melhor do que uma camada convolucional de filtro grande (5x5 ou 7x7):

Verifica-se que o desempenho pode ser melhorado aprofundando continuamente a estrutura da rede.

Desvantagens do VGG

O VGG consome mais recursos de computação e usa mais parâmetros (este não é o pote da convolução 3x3), resultando em mais uso de memória (140M).

Processamento de conjunto de dados

Introdução ao conjunto de dados

O conjunto de dados CIFAR (Instituto Canadense de Pesquisa Avançada) é um pequeno conjunto de dados de imagens amplamente utilizado no campo de visão computacional. É usado principalmente para treinar algoritmos de aprendizado de máquina e visão computacional, especialmente em tarefas como reconhecimento e classificação de imagens. O conjunto de dados CIFAR consiste em duas partes principais: CIFAR-10 e CIFAR-100.

CIFAR-10 é um conjunto de dados contendo 60.000 imagens coloridas 32x32, que são divididas em 10 categorias, cada categoria contendo 6.000 imagens. As 10 categorias são: aviões, carros, pássaros, gatos, veados, cachorros, sapos, cavalos, barcos e caminhões. No conjunto de dados, 50.000 imagens são usadas para treinamento e 10.000 imagens são usadas para teste. O conjunto de dados CIFAR-10 tornou-se um dos conjuntos de dados muito populares em pesquisa e ensino na área de visão computacional devido ao seu tamanho moderado e rica informação de classe.

Características do conjunto de dados
  • tamanho médio: O pequeno tamanho de imagem do conjunto de dados CIFAR (32x32) os torna ideais para treinar e testar rapidamente novos algoritmos de visão computacional.
  • Várias categorias: O CIFAR-10 fornece tarefas básicas de classificação de imagens, enquanto o CIFAR-100 desafia ainda mais os recursos de classificação refinada do algoritmo.
  • amplamente utilizado: Devido a essas características, o conjunto de dados CIFAR é amplamente utilizado em pesquisa e ensino em visão computacional, aprendizado de máquina, aprendizado profundo e outras áreas.
cenas a serem usadas

O conjunto de dados CIFAR é comumente usado para tarefas como classificação de imagens, reconhecimento de objetos e treinamento e teste de redes neurais convolucionais (CNN). Devido ao seu tamanho moderado e rica informação de categoria, é ideal para iniciantes e pesquisadores que exploram algoritmos de reconhecimento de imagem. Além disso, muitas competições de visão computacional e aprendizado de máquina também usam o conjunto de dados CIFAR como referência para avaliar o desempenho dos algoritmos dos concorrentes.

Para preparar o conjunto de dados, já baixei. Se não funcionar, basta baixá-lo do site oficial ou entregarei diretamente para você.

Caso necessite do conjunto de dados, entre em contato com o email: [email protected]

Meu conjunto de dados foi originalmente gerado por meio dos dados baixados no torchvision. Eu realmente não quero fazer isso agora, quero implementar a definição do conjunto de dados de dados e o carregamento do DataLoader passo a passo, entender esse processo e entender. o processo de processamento do conjunto de dados pode torná-lo mais profundo sobre o aprendizado profundo.

O estilo do conjunto de dados é o seguinte:

Insira a descrição da imagem aqui

Analise todos os rótulos do conjunto de dados

A categoria de rótulo do conjunto de dados usa um.metaO arquivo está armazenado, então precisamos analisar .meta arquivo para ler todos os dados da tag. O código de análise é o seguinte:

# 首先了解所有的标签,TODO 可以详细了解一下这个解包的过程
import pickle


def unpickle(file):
    with open(file, 'rb') as fo:
        dict = pickle.load(fo, encoding='bytes')
    return dict


meta_data = unpickle('./dataset_method_1/cifar-10-batches-py/batches.meta')
label_names = meta_data[b'label_names']
# 将字节标签转换为字符串
label_names = [label.decode('utf-8') for label in label_names]
print(label_names)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

Os resultados da análise são os seguintes:

['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
  • 1

Carregue um único lote de dados para testes simples dos dados

Nosso conjunto de dados foi baixado, então precisamos ler o conteúdo do arquivo. Como o arquivo é um arquivo binário, precisamos usar o modo de leitura binária para lê-lo.

O código de leitura é o seguinte:

# 载入单个批次的数据
import numpy as np


def load_data_batch(file):
    with open(file, 'rb') as fo:
        dict = pickle.load(fo, encoding='bytes')
        X = dict[b'data']
        Y = dict[b'labels']
        X = X.reshape(10000, 3, 32, 32).transpose(0, 2, 3, 1)  # reshape and transpose to (10000, 32, 32, 3)
        Y = np.array(Y)
    return X, Y


# 加载第一个数据批次
data_batch_1 = './dataset_method_1/cifar-10-batches-py/data_batch_1'
X1, Y1 = load_data_batch(data_batch_1)

print(f'数据形状: {X1.shape}, 标签形状: {Y1.shape}')

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

resultado:

数据形状: (10000, 32, 32, 3), 标签形状: (10000,)
  • 1

Carregar todos os dados

Após o teste acima, sabemos como carregar os dados. Agora vamos carregar todos os dados.

Carregando o conjunto de treinamento:

# 整合所有批次的数据
def load_all_data_batches(batch_files):
    X_list, Y_list = [], []
    for file in batch_files:
        X, Y = load_data_batch(file)
        X_list.append(X)
        Y_list.append(Y)
    X_all = np.concatenate(X_list)
    Y_all = np.concatenate(Y_list)
    return X_all, Y_all


batch_files = [
    './dataset_method_1/cifar-10-batches-py/data_batch_1',
    './dataset_method_1/cifar-10-batches-py/data_batch_2',
    './dataset_method_1/cifar-10-batches-py/data_batch_3',
    './dataset_method_1/cifar-10-batches-py/data_batch_4',
    './dataset_method_1/cifar-10-batches-py/data_batch_5'
]

X_train, Y_train = load_all_data_batches(batch_files)
print(f'训练数据形状: {X_train.shape}, 训练标签形状: {Y_train.shape}')
Y_train = Y_train.astype(np.int64)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

Saída:

训练数据形状: (50000, 32, 32, 3), 训练标签形状: (50000,)
  • 1

Carregando o conjunto de testes:

test_batch = './dataset_method_1/cifar-10-batches-py/test_batch'
X_test, Y_test = load_data_batch(test_batch)
Y_test = Y_test.astype(np.int64)
print(f'测试数据形状: {X_test.shape}, 测试标签形状: {Y_test.shape}')

  • 1
  • 2
  • 3
  • 4
  • 5

Saída:

测试数据形状: (10000, 32, 32, 3), 测试标签形状: (10000,)
  • 1

Defina uma subclasse de Dataset

Definir uma subclasse da classe Dataset facilita o carregamento subsequente do Dataloader para treinamento em lote.

Existem três métodos que as subclasses do Dataset devem implementar.

  • __init__()construtor de classe
  • __len__()Retorna o comprimento do conjunto de dados
  • __getitem__()Obtenha um dado do conjunto de dados

Aqui minha implementação é a seguinte:

from torch.utils.data import DataLoader, Dataset


# 定义 Pytorch 的数据集 
class CIFARDataset(Dataset):
    def __init__(self, images, labels, transform=None):
        self.images = images
        self.labels = labels
        self.transform = transform

    def __len__(self):
        return len(self.images)

    def __getitem__(self, idx):
        image = self.images[idx]
        label = self.labels[idx]

        if self.transform:
            image = self.transform(image)

        return image, label
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

Carregue o conjunto de dados como Dataloader

  1. Defina uma transformação para aprimorar os dados. Aqui está o conjunto de treinamento primeiro. O conjunto de treinamento precisa ser ampliado em 4px, normalizado, invertido horizontalmente, processado em escala de cinza e, finalmente, retornado aos pixels originais de 32 * 32.
transform_train = transforms.Compose(
    [transforms.Pad(4),
     transforms.ToTensor(),
     transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)),
     transforms.RandomHorizontalFlip(),
     transforms.RandomGrayscale(),
     transforms.RandomCrop(32, padding=4),
     ])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  1. Como envolve processamento de imagem, e os dados que lemos do arquivo binário são dados numpy, precisamos converter a matriz numpy em dados de imagem para facilitar o processamento de imagem. Processe da seguinte forma:
# 把数据集变成 Image 的数组,不然好像不能进行数据的增强
# 改变训练数据
from PIL import Image
def get_PIL_Images(origin_data):
    datas = []
    for i in range(len(origin_data)):
        data = Image.fromarray(origin_data[i])
        datas.append(data)
    return datas
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  1. Obtenha o dataloader treinado
train_data = get_PIL_Images(X_train)
train_loader = DataLoader(CIFARDataset(train_data, Y_train, transform_train), batch_size=24, shuffle=True)
  • 1
  • 2
  1. A obtenção do conjunto de testes do dataloader de teste não requer muito processamento. O código é fornecido diretamente aqui.
# 测试集的预处理
transform_test = transforms.Compose(
    [
        transforms.ToTensor(),
        transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))]
)
test_loader = DataLoader(CIFARDataset(X_test, Y_test, transform_test), batch_size=24, shuffle=False)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

Definir rede

Implementamos a estrutura Pytorch baseada na rede VGG16 mencionada acima.

dividido principalmente:

  • camada de convolução
  • Camada totalmente conectada
  • camada de classificação

A implementação é a seguinte:

class VGG16(nn.Module):
    def __init__(self):
        super(VGG16, self).__init__()
        # 卷积层,这里进行卷积
        self.convolusion = nn.Sequential(
            nn.Conv2d(3, 96, kernel_size=3, padding=1), # 设置为padding=1 卷积完后,数据大小不会变
            nn.BatchNorm2d(96),
            nn.ReLU(inplace=True),
            nn.Conv2d(96, 96, kernel_size=3, padding=1),
            nn.BatchNorm2d(96),
            nn.ReLU(inplace=True),
            nn.Conv2d(96, 96, kernel_size=3, padding=1),
            nn.BatchNorm2d(96),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(96, 128, kernel_size=3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 128, kernel_size=3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 128, kernel_size=3, padding=1),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(128, 256, kernel_size=3, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(256, 512, kernel_size=3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.BatchNorm2d(512),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
            nn.AvgPool2d(kernel_size=1, stride=1)
        )
        # 全连接层
        self.dense = nn.Sequential(
            nn.Linear(512, 4096), # 32*32 的图像大小经过 5 次最大化池化后就只有 1*1 了,所以就是 512 个通道的数据输入全连接层
            nn.ReLU(inplace=True),
            nn.Dropout(0.4),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(0.4),
        )
        # 输出层
        self.classifier = nn.Linear(4096, 10)

    def forward(self, x):
        out = self.convolusion(x)
        out = out.view(out.size(0), -1)
        out = self.dense(out)
        out = self.classifier(out)
        return out
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75

treinamento e teste

Para treinamento e teste, você só precisa instanciar o modelo, definir a função de otimização, a função de perda e a taxa de perda e, em seguida, realizar o treinamento e o teste.

código mostrado abaixo:

Definição de hiperparâmetro:

# 定义模型进行训练
model = VGG16()
# model.load_state_dict(torch.load('./my-VGG16.pth'))
optimizer = optim.SGD(model.parameters(), lr=0.01, weight_decay=5e-3)
loss_func = nn.CrossEntropyLoss()
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.4, last_epoch=-1)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Função de teste:

def test():
    model.eval()
    correct = 0  # 预测正确的图片数
    total = 0  # 总共的图片数
    with torch.no_grad():
        for data in test_loader:
            images, labels = data
            images = images.to(device)
            outputs = model(images).to(device)
            outputs = outputs.cpu()
            outputarr = outputs.numpy()
            _, predicted = torch.max(outputs, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum()
    accuracy = 100 * correct / total
    accuracy_rate.append(accuracy)
    print(f'准确率为:{accuracy}%'.format(accuracy))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

Épocas de treinamento:

# 定义训练步骤
total_times = 40
total = 0
accuracy_rate = []
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

for epoch in range(total_times):
    model.train()
    model.to(device)
    running_loss = 0.0
    total_correct = 0
    total_trainset = 0
    print("epoch: ",epoch)
    for i, (data,labels) in enumerate(train_loader):
        data = data.to(device)
        outputs = model(data).to(device)
        labels = labels.to(device)
        loss = loss_func(outputs,labels).to(device)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
        _,pred = outputs.max(1)
        correct = (pred == labels).sum().item()
        total_correct += correct
        total_trainset += data.shape[0]
        if i % 100 == 0 and i > 0:
            print(f"正在进行第{i}次训练, running_loss={running_loss}".format(i, running_loss))
            running_loss = 0.0
    test()
    scheduler.step()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

Salve o modelo treinado:

torch.save(model.state_dict(), './my-VGG16.pth')
accuracy_rate = np.array(accuracy_rate)
times = np.linspace(1, total_times, total_times)
plt.xlabel('times')
plt.ylabel('accuracy rate')
plt.plot(times, accuracy_rate)
plt.show()
print(accuracy_rate)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

teste

  1. Definir modelo
model_my_vgg = VGG16()
  • 1
  1. Adicione dados do modelo treinado
model_my_vgg.load_state_dict(torch.load('./my-VGG16-best.pth',map_location='cpu'))
  • 1
  1. Processando as imagens de verificação que encontrei
from torchvision import transforms
from PIL import Image

# 定义图像预处理步骤
preprocess = transforms.Compose([
    transforms.Resize((32, 32)),
    transforms.ToTensor(),
    # transforms.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),
])

def load_image(image_path):
    image = Image.open(image_path)
    image = preprocess(image)
    image = image.unsqueeze(0)  # 添加批次维度
    return image

image_data = load_image('./plane2.jpg')
print(image_data.shape)
output = model_my_vgg(image_data)
verify_data = X1[9]
verify_label = Y1[9]
output_verify = model_my_vgg(transform_test(verify_data).unsqueeze(0))
print(output)
print(output_verify)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

Saída:

torch.Size([1, 3, 32, 32])
tensor([[ 1.5990, -0.5269,  0.7254,  0.3432, -0.5036, -0.3267, -0.5302, -0.9417,
          0.4186, -0.1213]], grad_fn=<AddmmBackward0>)
tensor([[-0.6541, -2.0759,  0.6308,  1.9791,  0.8525,  1.2313,  0.1856,  0.3243,
         -1.3374, -1.0211]], grad_fn=<AddmmBackward0>)
  • 1
  • 2
  • 3
  • 4
  • 5
  1. Imprimir resultados
print(label_names[torch.argmax(output,dim=1,keepdim=False)])
print(label_names[verify_label])
print("pred:",label_names[torch.argmax(output_verify,dim=1,keepdim=False)])
  • 1
  • 2
  • 3
airplane
cat
pred: cat
  • 1
  • 2
  • 3

Insira a descrição da imagem aqui

Verifique o cavalo

Insira a descrição da imagem aqui

cão de verificação

Insira a descrição da imagem aqui