Pytorch entrena a ResNet con sus propios datos

1. Introducción al algoritmo ResNet

La Red Neural Residual (ResNet) fue propuesta por He Yuming y otros de Microsoft Research. ResNet ganó el campeonato en el ILSVRC 2015.

A través de experimentos, con la profundización continua de la capa de red de ResNet, la precisión del modelo primero se mejora continuamente, alcanzando el valor máximo (saturación de precisión), y luego, a medida que la profundidad de la red continúa aumentando, la precisión del modelo aparece sin advertencia.reducir. Este fenómeno es obviamente contradictorio y entra en conflicto con la creencia de que "cuanto más profunda es la red, mayor es la tasa de precisión". El equipo de ResNet llama a este fenómeno "Degradación".

Es razonable agregar más capas a la red. El espacio de solución de la red superficial se incluye en el espacio de solución de la red profunda. Se convierte en un mapeo de identidad, y los pesos de otras capas se copian intactos en la red superficial, y se puede obtener el mismo rendimiento que la red somera. Obviamente existe una mejor explicación, ¿por qué no puedo encontrarla? ¿Encontrar una solución peor en su lugar?

La degradación del rendimiento en el conjunto de entrenamiento puede descartar el sobreajuste, y la introducción de la capa BN resuelve básicamente los problemas de desaparición y explosión de gradiente de la red simple. Si no es causado por el sobreajuste y la desaparición del gradiente, ¿cuál es la razón?

Obviamente, este es un problema de optimización, lo que refleja que la dificultad de optimización de modelos con estructuras similares es diferente, y el aumento de dificultad no es lineal, y cuanto más profundo es el modelo, más difícil es optimizar.

Hay dos soluciones, una es ajustar el método de solución, como una mejor inicialización, un mejor algoritmo de descenso de gradiente, etc.; la otra es ajustar la estructura del modelo para que el modelo sea más fácil de optimizar; cambiar la estructura del modelo en realidad cambia el forma de la superficie de error.

ResNet propone el concepto de bloque residual desde la perspectiva de ajustar la estructura del modelo. El principio real es dejar que cada bloque residual en la capa profunda de la red aprenda la identidad tanto como sea posible. Esto equivale a simplificar la tarea y la profundidad de la red puede ser mayor.

 ¿Por qué el bloque residual está diseñado de esta manera?

El propósito de ResNet es diseñar una red con mapeo de identidad, pero la tarea de ajustar la identidad de la red neuronal es más complicada, es mejor aprender directamente el mapeo del residual. Entonces, el propósito de la red es hacer que el residual sea igual a cero, lo que es equivalente a una red de mapeo de identidad. Como se muestra en la Figura 2, es un bloque residual, F(x) representa la ruta de aprendizaje residual, x representa la ruta de acceso directo y la relación de mapeo obtenida después del aprendizaje es:

x:=F(x)+x

En el documento original, la ruta residual se puede dividir aproximadamente en dos tipos, uno tiene una estructura de cuello de botella, es decir, la capa convolucional 1×1 a la derecha de la figura a continuación, que se usa para reducir la dimensión primero y luego aumentar la dimensión, principalmente por la realidad de reducir la complejidad computacional Considere , llámelo " bloque de cuello de botella ", otra estructura sin cuello de botella, como se muestra a la izquierda de la figura a continuación, llámelo " bloque básico ". El bloque básico consta de dos capas convolucionales de 3×3.

ResNet es una serie de múltiples Bloques Residuales. Su estructura es muy fácil de modificar y expandir. Al ajustar la cantidad de canales en el bloque y la cantidad de bloques apilados, puede ajustar fácilmente el ancho y la profundidad de la red para obtener redes con diferentes capacidades expresivas En lugar de preocuparse demasiado por la "degeneración" de la red, siempre que los datos de entrenamiento sean suficientes y la red se profundice gradualmente, se puede obtener un mejor rendimiento. En la actualidad, ResNet se usa más comúnmente como la columna vertebral de la red de detección. Las estructuras de uso común incluyen ResNet-50, ResNet-101, etc.

2. Introducción del conjunto de datos

Este experimento utiliza un conjunto de datos de código abierto para el reconocimiento de gestos para entrenar un clasificador de gestos. El conjunto de datos proviene del proyecto https://codechina.csdn.net/EricLee/classification , con un total de 2850 muestras divididas en 14 categorías.

No hay nada que decir sobre la definición de pytorch de los datos, los pasos básicos. Simplemente reescriba algunas funciones de acuerdo con sus propias características de datos. En este experimento, las muestras se dividen en conjunto de entrenamiento y conjunto de verificación de acuerdo con la proporción de 5:1.

import torch
import torch.nn as nn
from torch.utils.data import DataLoader,Dataset
from torchvision import transforms as T
import matplotlib.pyplot as plt
import os
from PIL import Image
import numpy as np
import random
 
class hand_pose(Dataset):
    def __init__(self, root, train=True, transforms=None):
        imgs = []
        for path in os.listdir(root):
            path_prefix = path[:3]
            if path_prefix == "000":
                label = 0
            elif path_prefix == "001":
                label = 1
            elif path_prefix == "002":
                label = 2
            elif path_prefix == "003":
                label = 3
            elif path_prefix == "004":
                label = 4
            elif path_prefix == "005":
                label = 5
            elif path_prefix == "006":
                label = 6
            elif path_prefix == "007":
                label = 7
            elif path_prefix == "008":
                label = 8
            elif path_prefix == "009":
                label = 9
            elif path_prefix == "010":
                label = 10
            elif path_prefix == "011":
                label = 11
            elif path_prefix == "012":
                label = 12
            elif path_prefix == "013":
                label = 13
            else:
                print("data label error")
 
            childpath = os.path.join(root, path)
            for imgpath in os.listdir(childpath):
                imgs.append((os.path.join(childpath, imgpath), label))
        
        train_path_list, val_path_list = self._split_data_set(imgs)
        if train:
            self.imgs = train_path_list
        else:
            self.imgs = val_path_list

        if transforms is None:
            normalize = T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
 
            self.transforms = T.Compose([
                    T.Resize(256),
                    T.CenterCrop(224),
                    T.ToTensor(),
                    normalize
            ])
        else:
            self.transforms = transforms
             
    def __getitem__(self, index):
        img_path = self.imgs[index][0]
        label = self.imgs[index][1]
 
        data = Image.open(img_path)
        if data.mode != "RGB":
            data = data.convert("RGB")
        data = self.transforms(data)
        return data,label
 
    def __len__(self):
        return len(self.imgs)

    def _split_data_set(self, imags):
        """
        分类数据为训练集和验证集,根据个人数据特点设计,不通用。
        """
        val_path_list = imags[::5]
        train_path_list = []
        for item in imags:
            if item not in val_path_list:
                train_path_list.append(item)
        return train_path_list, val_path_list
 
if __name__ == "__main__":
    root = "handpose_x_gesture_v1"
   
    train_dataset = hand_pose(root, train=False)
    train_dataloader = DataLoader(train_dataset, batch_size=32, shuffle=True)
    for data, label in train_dataloader:
        print(data.shape)
        print(label)
        break

Debido a que nn.CrossEntroyLoss contiene procesamiento de codificación softmax y ont-hot, no es necesario realizar un procesamiento ont-hot durante la definición de datos, y las categorías se pueden ordenar según int (0, 1, 2,...)

3. Formación modelo

3.1 Definición de red modelo

import torch
from torch import nn

class Bottleneck(nn.Module):
    # 残差块定义
    extention = 4
    def __init__(self, inplanes, planes, stride, downsample=None):
        super(Bottleneck, self).__init__()
        self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, stride=stride, bias=False)
        self.bn1 = nn.BatchNorm2d(planes)

        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(planes)

        self.conv3 = nn.Conv2d(planes, planes*self.extention, kernel_size=1, stride=1, bias=False)
        self.bn3 = nn.BatchNorm2d(planes*self.extention)

        self.relu = nn.ReLU(inplace=True)

        self.downsample = downsample
        self.stride = stride

    def forward(self, x):
        shortcut = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        out = self.conv3(out)
        out = self.bn3(out)
        out = self.relu(out)

        if self.downsample is not None:
            shortcut = self.downsample(x)
        
        out = out + shortcut   # 不能写作out+=shortcut
        out = self.relu(out)
        return out


class ResNet50(nn.Module):
    def __init__(self, block, layers, num_class):
        self.inplane = 64
        super(ResNet50,self).__init__()

        self.block = block
        self.layers = layers

        self.conv1 = nn.Conv2d(3, self.inplane, kernel_size=7, stride=2, padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(self.inplane)
        self.relu = nn.ReLU()
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        self.stage1=self.make_layer(self.block,64,layers[0],stride=1)
        self.stage2=self.make_layer(self.block,128,layers[1],stride=2)
        self.stage3=self.make_layer(self.block,256,layers[2],stride=2)
        self.stage4=self.make_layer(self.block,512,layers[3],stride=2)

        self.avgpool = nn.AvgPool2d(7)
        self.fc = nn.Linear(512*block.extention, num_class)

    def forward(self, x):
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.maxpool(out)

        #block部分
        out=self.stage1(out)
        out=self.stage2(out)
        out=self.stage3(out)
        out=self.stage4(out)

        out=self.avgpool(out)
        out=torch.flatten(out,1)
        out=self.fc(out)

        return out

    def make_layer(self, block, plane, block_num, stride=1):
        block_list = []
        downsample = None
        if(stride!=1 or self.inplane!=plane*block.extention):
            downsample = nn.Sequential(
                nn.Conv2d(self.inplane, plane*block.extention, stride=stride, kernel_size=1, bias=False),
                nn.BatchNorm2d(plane*block.extention)
            )
        conv_block = block(self.inplane, plane, stride=stride, downsample=downsample)
        block_list.append(conv_block)
        self.inplane = plane*block.extention

        for i in range(1,block_num):
            block_list.append(block(self.inplane, plane, stride=1))

        return nn.Sequential(*block_list)


if __name__ == "__main__":
    resnet = ResNet50(Bottleneck,[3,4,6,3],14)
    x = torch.randn(64,3,224,224)
    x = resnet(x)
    print(x.shape)

La definición de red consta de dos partes: el cuello de botella es el módulo básico de la red residual y Resnet50 es la arquitectura de red completa, que corresponde a la estructura de red de la figura siguiente. 

Tenga en cuenta que al definir el cuello de botella del bloque residual, la parte adicional conectada por salto del atajo no se puede escribir como out += atajo La razón específica es que la salida se debe guardar para el cálculo del gradiente del backend, y += es un operación inplace que cambia la variable.

Si usa el método de escritura en el lugar, se informará un error y el mensaje de error es:

RuntimeError: one of the variables needed for gradient computation has been modified by an inplace operation:

3.2 Formación

import torch
import torch.nn as nn
from torch.utils.data import DataLoader,Dataset
from Data import hand_pose
from Model import ResNet50, Bottleneck
import os


def main():
    # 1. load dataset
    root = "handpose_x_gesture_v1"
    batch_size = 64
    train_data = hand_pose(root, train=True)
    val_data = hand_pose(root, train=False)
    train_dataloader = DataLoader(train_data,batch_size=batch_size,shuffle=True)
    val_dataloader = DataLoader(val_data,batch_size=batch_size,shuffle=True)
    
    # 2. load model
    num_class = 14
    model = ResNet50(Bottleneck,[3,4,6,3], num_class)
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model = model.to(device)
    
    # 3. prepare super parameters
    criterion = nn.CrossEntropyLoss()
    learning_rate = 1e-3
    optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
    epoch = 30

    # 4. train
    val_acc_list = []
    out_dir = "checkpoints/"
    if not os.path.exists(out_dir):
        os.makedirs(out_dir)
    for epoch in range(0, epoch):
        print('\nEpoch: %d' % (epoch + 1))
        model.train()
        sum_loss = 0.0
        correct = 0.0
        total = 0.0
        for batch_idx, (images, labels) in enumerate(train_dataloader):
            length = len(train_dataloader)
            images, labels = images.to(device), labels.to(device)
            optimizer.zero_grad()
            outputs = model(images) # torch.size([batch_size, num_class])
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()
        
            sum_loss += loss.item()
            _, predicted = torch.max(outputs.data, dim=1)
            total += labels.size(0)
            correct += predicted.eq(labels.data).cpu().sum()
            print('[epoch:%d, iter:%d] Loss: %.03f | Acc: %.3f%% ' 
                % (epoch + 1, (batch_idx + 1 + epoch * length), sum_loss / (batch_idx + 1), 100. * correct / total))
            
        #get the ac with testdataset in each epoch
        print('Waiting Val...')
        with torch.no_grad():
            correct = 0.0
            total = 0.0
            for batch_idx, (images, labels) in enumerate(val_dataloader):
                model.eval()
                images, labels = images.to(device), labels.to(device)
                outputs = model(images)
                _, predicted = torch.max(outputs.data, dim=1)
                total += labels.size(0)
                correct += (predicted == labels).sum()
            print('Val\'s ac is: %.3f%%' % (100 * correct / total))
            
            acc_val = 100 * correct / total
            val_acc_list.append(acc_val)


        torch.save(model.state_dict(), out_dir+"last.pt")
        if acc_val == max(val_acc_list):
            torch.save(model.state_dict(), out_dir+"best.pt")
            print("save epoch {} model".format(epoch))

if __name__ == "__main__":
    main()

Durante el entrenamiento, cada época prueba la precisión en el conjunto de entrenamiento y el conjunto de validación, respectivamente, y guarda el modelo.

Los resultados finales del entrenamiento son los siguientes.

La tasa de precisión del conjunto de entrenamiento es 88, y el conjunto de verificación es solo 72. 6. No hay duda de que el modelo tiene algo de sobreajuste . La razón es que la cantidad de datos es demasiado pequeña, un total de 2850 muestras, que se dividen en conjunto de entrenamiento y conjunto de verificación de acuerdo con la proporción de 5:1. Si usa el aprendizaje por transferencia, inicializa el modelo con un modelo previamente entrenado y luego entrena, el efecto debería ser mucho mejor.

3.3 Transferencia de aprendizaje

 Transferir el aprendizaje, como su nombre lo indica, es transferir los parámetros del modelo entrenado a un nuevo modelo para ayudar al entrenamiento del nuevo modelo. Teniendo en cuenta que la mayoría de los datos o tareas están relacionados, a través del aprendizaje por transferencia podemos compartir los parámetros del modelo aprendido (también entendido como el conocimiento aprendido por el modelo) al nuevo modelo de cierta manera para acelerar y La eficiencia de aprendizaje del optimizado. El modelo no necesita ser aprendido desde cero como la mayoría de las redes.

Ventajas: 1. Acelera la velocidad de entrenamiento y la pérdida converge rápidamente 2. Puede reducir el sobreajuste y obtener un modelo con mayor capacidad de generalización.

Debido a que el modelo que definimos es diferente del resnet50 en el documento, no es posible cargar directamente el modelo previamente entrenado en Internet. Aquí usamos la red resnet50 que viene con torchvision, luego cargamos el modelo previamente entrenado, cambiamos la última capa completamente conectada y luego entrenamos. Simplemente cargue el modelo en train.py y modifíquelo aquí.

# 2. load model
    num_class = 14
    # model = ResNet50(Bottleneck,[3,4,6,3], num_class)
    model = models.resnet50(pretrained=True)
    fc_inputs = model.fc.in_features
    model.fc = nn.Linear(fc_inputs, num_class)
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    model = model.to(device)

Los resultados finales del entrenamiento son los siguientes: cuando la época llega a 22, la pérdida es muy pequeña, la tasa de precisión del conjunto de verificación es 88 y la tasa de precisión del conjunto de entrenamiento es 99.

¿Por qué es tan grande la brecha entre el aprendizaje desde cero y el aprendizaje preciso? El tamaño de la función de pérdida es diez veces diferente y la precisión del conjunto de verificación es 20 veces peor. Personalmente, creo que tiene más que ver con la inicialización.La inicialización evita que la pérdida gire en un mínimo local y encuentra un punto más bajo, lo que mejora el rendimiento del modelo, pero el problema del sobreajuste sigue existiendo.

 

Supongo que te gusta

Origin blog.csdn.net/Eyesleft_being/article/details/119996210
Recomendado
Clasificación