Обмен технологиями

VGG16 реализует реализацию классификации изображений с помощью pytorch и подробно объясняет шаги.

2024-07-12

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

VGG16 реализует классификацию изображений.

Здесь мы реализуем сеть VGG-16 для классификации набора данных CIFAR.

Введение в сеть VGG16

Предисловие

《Очень глубокие сверточные сети для крупномасштабного распознавания изображений》

МКЗР 2015

ВГГЭто из ОксфордаВвизуальныйггеометрияг Предложено группой roup (вы должны увидеть происхождение названия VGG). Эта сеть является связанной работой на ILSVRC 2014. Основная работа заключается в том, чтобы доказать, что увеличение глубины сети может в определенной степени повлиять на конечную производительность сети. VGG имеет две структуры, а именно VGG16 и VGG19. Между ними нет существенной разницы, но глубина сети различна.

Принцип ВГГ

Улучшение VGG16 по сравнению с AlexNet заключается в том, чтоИспользуйте несколько последовательных ядер свертки 3x3 для замены более крупных ядер свертки в AlexNet (11x11, 7x7, 5x5). . Для данного восприимчивого поля (локального размера входного изображения относительно выходного) использование сложенных небольших ядер свертки лучше, чем использование больших ядер свертки, поскольку несколько нелинейных слоев могут увеличить глубину сети, чтобы обеспечить более сложный режим обучения, и стоимость сравнительно небольшая (меньше параметров).

Проще говоря, в VGG три ядра свертки 3х3 используются для замены ядра свертки 7х7, а два ядра свертки 3х3 используются для замены ядра свертки 5*5. Основная цель этого — обеспечить то же самое при условии. Рецептивного поля глубина сети улучшается, а эффект нейронной сети в определенной степени улучшается.

Например, послойную суперпозицию трех ядер свертки 3х3 с шагом 1 можно рассматривать как рецептивное поле размера 7 (фактически это означает, что три непрерывные свертки 3х3 эквивалентны свертке 7х7), а ее общее количество параметров равно 3x(9xC^2). Если ядро ​​свертки 7x7 используется напрямую, общее количество параметров равно 49xC^2, где C относится к количеству входных и выходных каналов.Очевидно, 27xC2 менее 49xC2, то есть параметры уменьшены, а ядро ​​свертки 3х3 способствует лучшему сохранению свойств изображения;

Вот объяснение того, почему можно использовать два ядра свертки 3х3 вместо ядер свертки 5*5:

Свертка 5x5 рассматривается как небольшая полносвязная сеть, скользящая в области 5x5. Мы можем сначала выполнить свертку с помощью фильтра свертки 3x3, а затем использовать полносвязный слой для подключения выхода свертки 3x3. рассматриваться как сверточный слой 3x3. Таким образом, мы можем каскадировать (накладывать) две свертки 3x3 вместо одной свертки 5x5.

Подробности показаны на рисунке ниже:

Вставьте сюда описание изображения

Структура сети VGG

Вставьте сюда описание изображения

Ниже представлена ​​структура сети VGG (присутствуют как VGG16, так и VGG19):

Вставьте сюда описание изображения

Структура сети ГГ

VGG16 содержит 16 скрытых слоев (13 сверточных слоев и 3 полностью связанных слоя), как показано в столбце D на рисунке выше.

VGG19 содержит 19 скрытых слоев (16 сверточных слоев и 3 полностью связанных слоя), как показано в столбце E на рисунке выше.

Структура сети VGG очень последовательная: от начала до конца используется свертка 3x3 и максимальный пул 2x2.

Преимущества ВГГ

Структура VGGNet очень проста. Вся сеть использует одинаковый размер ядра свертки (3x3) и максимальный размер пула (2x2).

Комбинация нескольких небольших сверточных слоев фильтра (3x3) лучше, чем один большой сверточный слой фильтра (5x5 или 7x7):

Подтверждено, что производительность можно повысить за счет постоянного углубления структуры сети.

Недостатки ВГГ

VGG потребляет больше вычислительных ресурсов и использует больше параметров (это не свертка 3x3), что приводит к большему использованию памяти (140 МБ).

Обработка набора данных

Введение в набор данных

Набор данных CIFAR (Канадский институт перспективных исследований) — это небольшой набор данных изображений, широко используемый в области компьютерного зрения. Он в основном используется для обучения алгоритмов машинного обучения и компьютерного зрения, особенно в таких задачах, как распознавание и классификация изображений. Набор данных CIFAR состоит из двух основных частей: CIFAR-10 и CIFAR-100.

CIFAR-10 — это набор данных, содержащий 60 000 цветных изображений размером 32x32, которые разделены на 10 категорий, каждая категория содержит 6000 изображений. 10 категорий: самолеты, автомобили, птицы, кошки, олени, собаки, лягушки, лошади, лодки и грузовики. В наборе данных 50 000 изображений используются для обучения и 10 000 изображений — для тестирования. Набор данных CIFAR-10 стал одним из очень популярных наборов данных в исследованиях и преподавании в области компьютерного зрения благодаря своему умеренному размеру и богатой информации о классах.

Характеристики набора данных
  • средний размер: небольшой размер изображения набора данных CIFAR (32x32) делает их идеальными для быстрого обучения и тестирования новых алгоритмов компьютерного зрения.
  • Различные категории: CIFAR-10 обеспечивает базовые задачи классификации изображений, а CIFAR-100 еще больше бросает вызов возможностям алгоритма мелкозернистой классификации.
  • широко используемый: Благодаря этим характеристикам набор данных CIFAR широко используется в исследованиях и преподавании в области компьютерного зрения, машинного обучения, глубокого обучения и других областях.
сцены, которые будут использоваться

Набор данных CIFAR обычно используется для таких задач, как классификация изображений, распознавание объектов, а также обучение и тестирование сверточных нейронных сетей (CNN). Благодаря умеренному размеру и богатой информации о категориях он идеально подходит для новичков и исследователей, изучающих алгоритмы распознавания изображений. Кроме того, многие соревнования по компьютерному зрению и машинному обучению также используют набор данных CIFAR в качестве эталона для оценки эффективности алгоритмов участников.

Для подготовки набора данных я его уже скачал. Если не получится, просто скачайте с официального сайта, или я вам его дам напрямую.

Если вам нужен набор данных, свяжитесь по электронной почте: [email protected].

Мой набор данных изначально был сгенерирован посредством загруженных данных в torchvision. Мне сейчас не очень хочется это делать, я хочу поэтапно реализовать определение набора данных и загрузку DataLoader, разобраться в этом процессе и разобраться. процесс обработки набора данных может помочь вам глубже понять глубокое обучение.

Стиль набора данных следующий:

Вставьте сюда описание изображения

Разобрать все метки набора данных

Категория меток набора данных использует.metaФайл сохранен, поэтому нам нужно проанализировать .meta файл для чтения всех данных тега. Код синтаксического анализа выглядит следующим образом:

# 首先了解所有的标签,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

Результаты анализа следующие:

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

Загрузите один пакет данных для простого тестирования данных.

Наш набор данных загружен, поэтому нам нужно прочитать содержимое файла. Поскольку файл является двоичным, нам нужно использовать двоичный режим чтения для его чтения.

Код чтения следующий:

# 载入单个批次的数据
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

результат:

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

Загрузить все данные

После приведенного выше теста мы знаем, как загружать данные. Теперь давайте загрузим все данные.

Загрузка обучающего набора:

# 整合所有批次的数据
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

Выход:

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

Загрузка тестового набора:

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

Выход:

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

Определить подкласс набора данных

Определение подкласса класса Dataset предназначено для облегчения последующей загрузки Dataloader для пакетного обучения.

Есть три метода, которые должны реализовать подклассы Dataset.

  • __init__()конструктор класса
  • __len__()Возвращает длину набора данных
  • __getitem__()Получить часть данных из набора данных

Здесь моя реализация выглядит следующим образом:

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

Загрузите набор данных как Dataloader

  1. Определите преобразование для улучшения данных. Сначала это обучающий набор. Обучающий набор необходимо расширить на 4 пикселя, нормализовать, перевернуть по горизонтали, обработать в оттенках серого и, наконец, вернуть к исходным пикселям 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. Поскольку он включает обработку изображений, а данные, которые мы считываем из двоичного файла, представляют собой пустые данные, нам необходимо преобразовать массив numpy в данные изображения, чтобы облегчить обработку изображений. Обработайте следующим образом:
# 把数据集变成 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. Получите обученный загрузчик данных
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. Получение тестового набора загрузчиков данных не требует слишком большой обработки. Код приведен непосредственно здесь.
# 测试集的预处理
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

Определить сеть

Мы реализуем фреймворк Pytorch на основе упомянутой выше сети VGG16.

в основном разделены:

  • слой свертки
  • Полностью связный слой
  • слой классификации

Реализация следующая:

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

обучение и тестирование

Для обучения и тестирования вам нужно только создать экземпляр модели, затем определить функцию оптимизации, функцию потерь и уровень потерь, а затем выполнить обучение и тестирование.

код показан ниже:

Определение гиперпараметра:

# 定义模型进行训练
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

Функция тестирования:

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

Эпохи обучения:

# 定义训练步骤
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

Сохраните обученную модель:

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

тест

  1. Определить модель
model_my_vgg = VGG16()
  • 1
  1. Добавьте данные обученной модели
model_my_vgg.load_state_dict(torch.load('./my-VGG16-best.pth',map_location='cpu'))
  • 1
  1. Обрабатывая проверочные изображения, которые я нашел сам
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

Выход:

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. Распечатать результаты
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

Вставьте сюда описание изображения

Проверить лошадь

Вставьте сюда описание изображения

проверочная собака

Вставьте сюда описание изображения