ch07-Habilidades de entrenamiento de Pytorch

0 Prefacio

1. Guardado y carga del modelo.

Esta sección presenta principalmente la serialización y deserialización, así como las dos formas de guardar y cargar el modelo en PyTorch, y el entrenamiento continuo del punto de interrupción del modelo.

1.1 Serialización y deserialización

El modelo se guarda en la memoria como una estructura lógica de objetos, pero en el disco duro se guarda como un flujo binario.

  • La serialización se refiere a guardar datos en la memoria del disco duro en forma de secuencias binarias. La preservación del modelo de PyTorch es la serialización.
  • La deserialización se refiere a cargar la secuencia binaria del disco duro a la memoria para obtener el objeto modelo. La carga del modelo de PyTorch es la deserialización.

Insertar descripción de la imagen aquí

1.2 Guardado y carga del modelo en PyTorch

Insertar descripción de la imagen aquí

(1) Antorcha para guardar el modelo.save

  • torch.save(obj, f, pickle_module, pickle_protocol=2, _use_new_zipfile_serialization=False)
  • Los principales parámetros:
    • obj: el objeto guardado, que puede ser un modelo. También puede ser un dictado. Porque generalmente al guardar un modelo no solo se debe guardar el modelo, sino que también se deben guardar parámetros como el optimizador y la época correspondiente. En este momento, puedes envolverlo con dict.
    • f: ruta de salida

Hay dos formas de guardar el modelo:

  • Guarde el módulo completo: este método requiere más tiempo y el archivo guardado es grande:torch.savev(net, path)
  • Guarde solo los parámetros del modelo: se recomienda este método porque se ejecuta más rápido y guarda archivos más pequeños.
    state_sict = net.state_dict()
    torch.savev(state_sict, path)
    

A continuación se muestra un ejemplo de cómo guardar LeNet. En la inicialización de la red, establezca los pesos en 2020 y luego guarde el modelo.

import torch
import numpy as np
import torch.nn as nn
from common_tools import set_seed

class LeNet2(nn.Module):
    def __init__(self, classes):
        super(LeNet2, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 6, 5),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),
            nn.Conv2d(6, 16, 5),
            nn.ReLU(),
            nn.MaxPool2d(2, 2)
        )
        self.classifier = nn.Sequential(
            nn.Linear(16*5*5, 120),
            nn.ReLU(),
            nn.Linear(120, 84),
            nn.ReLU(),
            nn.Linear(84, classes)
        )

    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size()[0], -1)
        x = self.classifier(x)
        return x

    def initialize(self):
        for p in self.parameters():
            p.data.fill_(2020)

net = LeNet2(classes=2019)
# "训练"
print("训练前: ", net.features[0].weight[0, ...])
net.initialize()
print("训练后: ", net.features[0].weight[0, ...])

path_model = "./model.pkl"
path_state_dict = "./model_state_dict.pkl"
# 保存整个模型
torch.save(net, path_model)
# 保存模型参数
net_state_dict = net.state_dict()
torch.save(net_state_dict, path_state_dict)

Después de ejecutar, se genera la carpeta model.pkl和model_state_dict.pkly toda la red y los parámetros de red se guardan respectivamente.

(2) Modelo de carga antorcha.carga

  • torch.load(f, map_location=None, pickle_module, **pickle_load_args)
  • Los principales parámetros:
    • f: ruta del archivo
    • map_location: especifica la presencia de CPU o GPU.

También hay dos formas de cargar modelos:

  • Cargar todo el módulo

Si todo el modelo se guarda al guardar, entonces todo el modelo se cargará al cargar. Este método no requiere crear un objeto modelo por adelantado, ni necesita conocer la estructura del modelo, el código es el siguiente:

path_model = "./model.pkl"
net_load = torch.load(path_model)

print(net_load)

El resultado es el siguiente:

LeNet2(
  (features): Sequential(
    (0): Conv2d(3, 6, kernel_size=(5, 5), stride=(1, 1))
    (1): ReLU()
    (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (3): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
    (4): ReLU()
    (5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (classifier): Sequential(
    (0): Linear(in_features=400, out_features=120, bias=True)
    (1): ReLU()
    (2): Linear(in_features=120, out_features=84, bias=True)
    (3): ReLU()
    (4): Linear(in_features=84, out_features=2019, bias=True)
  )
)
  • Solo cargar parámetros del modelo.

Si los parámetros del modelo se guardan al guardar, entonces los parámetros se usarán al cargar. Este método requiere crear un objeto modelo por adelantado y luego usar el método load_state_dict () del modelo para cargar parámetros en el modelo. El código es el siguiente:

path_state_dict = "./model_state_dict.pkl"
state_dict_load = torch.load(path_state_dict)
net_new = LeNet2(classes=2019)

print("加载前: ", net_new.features[0].weight[0, ...])
net_new.load_state_dict(state_dict_load)
print("加载后: ", net_new.features[0].weight[0, ...])

1.3 Continuar entrenando en los puntos de interrupción del modelo.

Durante el proceso de capacitación, la capacitación puede finalizar debido a razones inesperadas, como puntos de interrupción, etc. En este caso, es necesario reiniciar la capacitación. El entrenamiento de reanudación del punto de interrupción guarda los parámetros del modelo y los parámetros del optimizador cada cierto número de épocas durante el proceso de entrenamiento , de modo que si el entrenamiento finaliza inesperadamente, la próxima vez se pueden recargar los últimos parámetros del modelo y los parámetros del optimizador y continuar entrenando sobre esta base.

En el código siguiente, se guarda cada 5 épocas. Lo que se guarda es un dict, que incluye los parámetros del modelo, los parámetros del optimizador y las épocas. Luego, cuando la época es mayor que 5, la pausa simula la finalización inesperada del entrenamiento. El código clave es el siguiente:

    if (epoch+1) % checkpoint_interval == 0:

        checkpoint = {
    
    "model_state_dict": net.state_dict(),
                      "optimizer_state_dict": optimizer.state_dict(),
                      "epoch": epoch}
        path_checkpoint = "./checkpoint_{}_epoch.pkl".format(epoch)
        torch.save(checkpoint, path_checkpoint)

Cuando la época es mayor que 5, la pausa simula una terminación inesperada del entrenamiento.

    if epoch > 5:
        print("训练意外中断...")
        break

El código de recuperación para reanudar el entrenamiento desde un punto de interrupción es el siguiente:

path_checkpoint = "./checkpoint_4_epoch.pkl"
checkpoint = torch.load(path_checkpoint)

net.load_state_dict(checkpoint['model_state_dict'])

optimizer.load_state_dict(checkpoint['optimizer_state_dict'])

start_epoch = checkpoint['epoch']

scheduler.last_epoch = start_epoch

Cabe señalar que el parámetro Scheduler.last_epoch también debe configurarse en la época guardada. La época inicial del entrenamiento del modelo también debe cambiarse a la época guardada.

2. Ajuste del modelo

  • mLa tarea principal de esta sección: comprender el aprendizaje por transferencia y el ajuste del modelo.

  • Introducción detallada: el método de ajuste fino del modelo de aprendizaje (Finetune) y comprensión de la relación entre Transfer Learning (aprendizaje por transferencia) y Model Finetune.

2.1.Transferir aprendizaje y ajustar el modelo

Transferir aprendizaje: una rama del aprendizaje automático que estudia cómo se aplica el conocimiento del dominio de origen al dominio de destino.

Insertar descripción de la imagen aquí

El llamado ajuste fino del modelo es en realidad la transferencia de aprendizaje del modelo. En el aprendizaje profundo, a través de iteraciones continuas, los pesos en la capa base del volumen se actualizan. Los pesos aquí se pueden llamar conocimiento, y luego podemos migrar estos conocimientos. Principalmente El propósito es aplicar estos conocimientos a nuevos modelos, que no solo pueden reducir el fenómeno de sobreajuste causado por un volumen de datos insuficiente, sino también acelerar el entrenamiento del modelo.

Insertar descripción de la imagen aquí

Por ejemplo, para el reconocimiento facial, ImageNet puede considerarse como el dominio de origen y el conjunto de datos faciales como el dominio de destino. En términos generales, el dominio de origen es mucho más grande que el dominio de destino. La red entrenada por ImageNet se puede utilizar en reconocimiento facial.

Específicamente, para las redes neuronales convolucionales, podemos considerar la capa base convolucional y la capa de agrupación anteriores como un extractor de características, que es una parte muy común. Obtenga una serie de mapas de características.
La capa completamente conectada que sigue se puede llamar clasificador y está relacionado con la tarea específica. Y cambie la salida de la última capa completamente conectada para adaptarla a la tarea objetivo y entrene el peso del clasificador posterior: esto es Finetune. Por lo general, los datos en el dominio de destino son relativamente pequeños y no son suficientes para entrenar todos los parámetros, lo que puede conducir fácilmente a un sobreajuste, por lo que el peso del extractor de características no cambia.

Insertar descripción de la imagen aquí

Los pasos de ajuste son los siguientes:

  • 1. Obtenga los parámetros del modelo previamente entrenado.
  • 2. Utilice load_state_dict() para cargar parámetros en el modelo.
  • 3. Modificar la capa de salida.
  • 4. Se corrigieron los parámetros del extractor de funciones. Generalmente hay dos enfoques para esta parte:
    • 1. Se corrigieron los parámetros de preentrenamiento de la capa convolucional. Puede configurar require_grad=False o lr=0
    • 2. Puede establecer una tasa de aprendizaje menor para el extractor de funciones a través de params_group

A continuación, ResNet-18 se ajusta para la clasificación binaria de imágenes de abejas y hormigas. El conjunto de entrenamiento contiene 120 imágenes de cada tipo de datos y el conjunto de validación contiene 70 imágenes de cada categoría de datos.

Insertar descripción de la imagen aquí

La estructura del modelo Resnet-18 se muestra en la siguiente figura:

Las primeras cuatro capas son extracción de características, las siguientes cuatro capas (capa1 ~ capa4) son redes residuales, luego la capa de agrupación avgpool y finalmente la clasificación FC (el modelo original tiene 1000 categorías, entrenadas en ImageNet).

Insertar descripción de la imagen aquí

(1) No usar Finetune

Por primera vez, no usamos Finetune primero, sino que comenzamos a entrenar el modelo desde cero, en este momento solo necesitamos modificar la capa completamente conectada:

# 首先拿到 fc 层的输入个数
num_ftrs = resnet18_ft.fc.in_features
# 然后构造新的 fc 层替换原来的 fc 层
resnet18_ft.fc = nn.Linear(num_ftrs, classes)

El resultado es el siguiente:

use device :cpu
Training:Epoch[000/025] Iteration[010/016] Loss: 0.7192 Acc:47.50%
Valid:  Epoch[000/025] Iteration[010/010] Loss: 0.6885 Acc:51.63%
...
Valid:  Epoch[024/025] Iteration[010/010] Loss: 0.5923 Acc:70.59%

La precisión después del entrenamiento durante 25 épocas: 70,59%.

La curva de pérdida de entrenamiento es la siguiente:

Insertar descripción de la imagen aquí

El valor de pérdida siempre ronda el 0,6 y la precisión obtenida es solo del 70%.

(2) Utilice el ajuste fino

Luego cargamos los parámetros del modelo descargado en el modelo:

path_pretrained_model = enviroments.resnet18_path
state_dict_load = torch.load(path_pretrained_model)
resnet18_ft.load_state_dict(state_dict_load)

No congelar capas convolucionales

En este momento, no congelamos la capa convolucional, todas las capas usan la misma tasa de aprendizaje y el resultado es el siguiente:

use device :cpu
Training:Epoch[000/025] Iteration[010/016] Loss: 0.6299 Acc:65.62%
...
Valid:  Epoch[024/025] Iteration[010/010] Loss: 0.1808 Acc:96.08%

La precisión después del entrenamiento durante 25 épocas: 96,08%.

La curva de pérdida de entrenamiento es la siguiente:

Se puede ver que el valor de la pérdida finalmente convergió a alrededor de 0,2 y la precisión alcanzó el 90% en la segunda época.

Insertar descripción de la imagen aquí

Insertar descripción de la imagen aquí

2.2 Ajuste fino en PyTorch


  • capa de convolución congelada

  • Establecer require_grad=Falso

Aquí, primero se congelan todos los parámetros y luego se reemplaza la capa completamente conectada, lo que equivale a congelar los parámetros de la capa convolucional:

for param in resnet18_ft.parameters():
 param.requires_grad = False
 # 首先拿到 fc 层的输入个数
num_ftrs = resnet18_ft.fc.in_features
# 然后构造新的 fc 层替换原来的 fc 层
resnet18_ft.fc = nn.Linear(num_ftrs, classes)

Los resultados experimentales no se proporcionan aquí.


  • Establecer la tasa de aprendizaje en 0

Aquí, la tasa de aprendizaje de la capa convolucional se establece en 0 y es necesario configurar diferentes tasas de aprendizaje en el optimizador. Primero obtenga la dirección de los parámetros de la capa completamente conectada y luego use el filtro para filtrar los parámetros que no pertenecen a la capa completamente conectada, es decir, retenga los parámetros de la capa convolucional; luego establezca la tasa de aprendizaje grupal del optimizador y pase en una lista que contiene 2 elementos, cada elemento es un diccionario, correspondiente a 2 grupos de parámetros. La tasa de aprendizaje de la capa convolucional se establece en 0,1 veces la de la capa completamente conectada.

# 首先获取全连接层参数的地址
fc_params_id = list(map(id, resnet18_ft.fc.parameters()))     # 返回的是parameters的 内存地址
# 然后使用 filter 过滤不属于全连接层的参数,也就是保留卷积层的参数
base_params = filter(lambda p: id(p) not in fc_params_id, resnet18_ft.parameters())
# 设置优化器的分组学习率,传入一个 list,包含 2 个元素,每个元素是字典,对应 2 个参数组
optimizer = optim.SGD([{
    
    'params': base_params, 'lr': 0}, {
    
    'params': resnet18_ft.fc.parameters(), 'lr': LR}], momentum=0.9)

Los resultados experimentales no se proporcionan aquí.


  • Utilice la tasa de aprendizaje grupal

Aquí, la capa convolucional no se congela, pero se usa una tasa de aprendizaje menor para la capa convolucional y una tasa de aprendizaje mayor para la capa completamente conectada. Es necesario establecer diferentes tasas de aprendizaje en el optimizador. Primero obtenga la dirección de los parámetros de la capa completamente conectada y luego use el filtro para filtrar los parámetros que no pertenecen a la capa completamente conectada, es decir, retenga los parámetros de la capa convolucional; luego establezca la tasa de aprendizaje grupal del optimizador y pase en una lista que contiene 2 elementos, cada elemento es un diccionario, correspondiente a 2 grupos de parámetros. La tasa de aprendizaje de la capa convolucional se establece en 0,1 veces la de la capa completamente conectada.

# 首先获取全连接层参数的地址
fc_params_id = list(map(id, resnet18_ft.fc.parameters()))     # 返回的是parameters的 内存地址
# 然后使用 filter 过滤不属于全连接层的参数,也就是保留卷积层的参数
base_params = filter(lambda p: id(p) not in fc_params_id, resnet18_ft.parameters())
# 设置优化器的分组学习率,传入一个 list,包含 2 个元素,每个元素是字典,对应 2 个参数组
optimizer = optim.SGD([{
    
    'params': base_params, 'lr': LR*0}, {
    
    'params': resnet18_ft.fc.parameters(), 'lr': LR}], momentum=0.9)

Los resultados experimentales no se proporcionan aquí.


  • Consejos para usar GPU

El modelo PyTorch usa GPU y se puede dividir en 3 pasos:

  • Primero consigue el dispositivo:device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
  • Cargue el modelo endevice:model.to(device)
  • En el bucle de recuperación de datos data_loader, cargue los datos y la etiqueta de cada mini lote en el dispositivo:inputs, labels = inputs.to(device), labels.to(device)

3. Utilice GPU para entrenar el modelo.

Esta sección presenta principalmente el uso de GPU.

3.1.CPU a GPU

  • CPU (Unidad Central de Procesamiento, unidad central de procesamiento): incluye principalmente controladores y unidades aritméticas
  • GPU (Unidad de procesamiento de gráficos, procesador de gráficos): maneja operaciones de datos unificadas e independientes a gran escala

El diagrama de estructura de los dos es el siguiente: puede ver que la parte verde (unidad de computación) de la GPU es obviamente más que la CPU.

Insertar descripción de la imagen aquí

La GPU tiene más ALU (unidades de operación aritmética) que la CPU, y la CPU tiene más áreas de caché, que se utilizan para acelerar la ejecución de programas. Ambos son adecuados para diferentes tareas. Los programas computacionalmente intensivos y los programas que son fáciles de paralelizar son Generalmente se completa en la GPU.

3.2 Migración de datos a GPU

Cuando se están procesando dos datos, se deben almacenar en el mismo dispositivo al mismo tiempo, ya sea la CPU o la GPU al mismo tiempo. Y los datos y modelo deben estar en el mismo dispositivo. Los datos y modelos se pueden transferir de un dispositivo a otro utilizando el método to(). El método de datos to() también puede convertir tipos de datos.

Insertar descripción de la imagen aquí

  • De la CPU a la GPU
    device = torch.device("cuda")
    tensor = tensor.to(device)
    module.to(device)
    
  • De la GPU a la CPU
    device = torch.device(cpu)
    tensor = tensor.to("cpu")
    module.to("cpu")
    

  • .to()Función: Convertir tipo de datos o dispositivo
    Insertar descripción de la imagen aquí
x=torch.ones((3,3))#定义一个张量
x=x.to(torch.float64)#把默认的float32转换为float64
x=torch.ones(3,3)#定义一个张量
x=x.to("cuda")#迁移到GPU

linear=nn.Linear(2,2)#定义一个module
linear.to(torch.double)#把module中所有的参数从默认的float32转换为float64(double就是float64)

gpu1=torch.device("cuda")#定义设备
linear.to(gpu1)#迁移到gpu
  • Puede ver que en los dos ejemplos anteriores, tensornecesita usar el signo igual para la asignación, modulepero puede ejecutar directamente la función to.

  • La diferencia entre los métodos to() de tensor y módulo es que tensor.to() no realiza una operación in situ, por lo que se requiere asignación; module.to() realiza una operación in situ.

  • tensor.to() y módulo.to()

Primero importe la biblioteca para obtener el dispositivo GPU

import torch
import torch.nn as nn
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
  • El siguiente código es para ejecutar el método to() de Tensor
x_cpu = torch.ones((3, 3))
print("x_cpu:\ndevice: {} is_cuda: {} id: {}".format(x_cpu.device, x_cpu.is_cuda, id(x_cpu)))

x_gpu = x_cpu.to(device)
print("x_gpu:\ndevice: {} is_cuda: {} id: {}".format(x_gpu.device, x_gpu.is_cuda, id(x_gpu)))

El resultado es el siguiente:

x_cpu:
device: cpu is_cuda: False id: 1415020820304
x_gpu:
device: cpu is_cuda: True id: 2700061800153

Puede ver que el método to() de Tensor no es una operación in situ y que las direcciones de memoria de x_cpu y x_gpu son diferentes.

  • El siguiente código ejecuta el método to() del Módulo
net = nn.Sequential(nn.Linear(3, 3))

print("\nid:{} is_cuda: {}".format(id(net), next(net.parameters()).is_cuda))

net.to(device)
print("\nid:{} is_cuda: {}".format(id(net), next(net.parameters()).is_cuda))

El resultado es el siguiente:

id:2325748158192 is_cuda: False
id:2325748158192 is_cuda: True

Puede ver que el método to() del Módulo es una operación in situ y la dirección de memoria es la misma.


métodos comunes de torch.cuda

  • torch.cuda.device_count(): Devuelve el número de GPU actualmente visibles y disponibles

  • torch.cuda.get_device_name(): obtiene el nombre de la GPU

  • torch.cuda.manual_seed(): establece una semilla aleatoria para la GPU actual

  • torch.cuda.manual_seed_all(): establece semillas aleatorias para todas las GPU visibles

  • torch.cuda.set_device(): establece qué GPU física es la GPU principal. Este método no se recomienda.

  • os.environ.setdefault("CUDA_VISIBLE_DEVICES", "2", "3"): establece GPU visible

En PyTorch, hay GPU físicas y GPU lógicas, y se puede establecer la correspondencia entre ellas.

Insertar descripción de la imagen aquí

En la figura anterior, si se ejecuta os.environ.setdefault("CUDA_VISIBLE_DEVICES", "2", "3"), la cantidad de GPU visibles es solo 2. La relación correspondiente es la siguiente:

Insertar descripción de la imagen aquí

Si se ejecuta os.environ.setdefault("CUDA_VISIBLE_DEVICES", "0", "3", "2"), la cantidad de GPU visibles es solo 3. La relación correspondiente es la siguiente:

Insertar descripción de la imagen aquí

El motivo de esta configuración es que puede haber muchos usuarios y tareas que utilizan la GPU en el sistema. Configurar el número de GPU puede asignar la GPU de manera razonable. Normalmente, la gpu0 predeterminada es la GPU principal. El concepto de GPU principal está relacionado con el mecanismo paralelo distribuido de múltiples GPU.

3.3 Paralelismo de distribución de múltiples GPU

En términos generales, hay tres pasos para la operación paralela de múltiples GPU: distribución → operación paralela → recuperación de resultados

  • Distribución: distribuye datos de la GPU principal a cada GPU
  • Operación paralela: cada GPU realiza operaciones por separado
  • Reciclaje de resultados: cada GPU envía los resultados obtenidos por la operación a la GPU principal

Implementación de PyTorch:

  • torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)

  • Función: Modelo de empaque para implementar mecanismo de distribución paralela. Los datos se pueden distribuir uniformemente en cada GPU. El volumen de datos real de cada GPU es el tamaño de lote _ número de GPU \frac {batch\_size}{número de GPU}Número de GPUba t c h _ tamaño _ _, para lograr computación paralela.

  • Los principales parámetros:

    • módulo: modelo que necesita ser empaquetado y distribuido
    • device_ids: GPU distribuibles, distribuidas a todas las GPU visibles y disponibles de forma predeterminada
    • dispositivo_salida: dispositivo de salida de resultados

Cabe señalar que DataParallelal usar, devicedebe especificar una GPU como GPU principal; de lo contrario, se informará un error:

RuntimeError: module must have its parameters and buffers on device cuda:1 (device_ids[0]) but found one of them on device: cuda:2

Esto se debe a que el uso de varias GPU requiere una GPU principal para distribuir los datos de cada lote a cada GPU y recopilar los resultados calculados de cada GPU. Si no se especifica la GPU principal, los datos se distribuirán directamente a cada GPU, lo que hará que algunos datos estén en una determinada GPU y otros datos en otras GPU, lo que provocará errores de cálculo.

El siguiente código establece dos GPU visibles, el tamaño del lote es 2, luego la cantidad de datos obtenidos por cada GPU por lote es 8 y la cantidad de datos se imprime en la propagación hacia adelante del modelo.

# 设置 2 个可见 GPU
    gpu_list = [0,1]
    gpu_list_str = ','.join(map(str, gpu_list))
    os.environ.setdefault("CUDA_VISIBLE_DEVICES", gpu_list_str)
    # 这里注意,需要指定一个 GPU 作为主 GPU。
    # 否则会报错:module must have its parameters and buffers on device cuda:1 (device_ids[0]) but found one of them on device: cuda:2
    # 参考:https://stackoverflow.com/questions/59249563/runtimeerror-module-must-have-its-parameters-and-buffers-on-device-cuda1-devi
    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

    batch_size = 16

    # data
    inputs = torch.randn(batch_size, 3)
    labels = torch.randn(batch_size, 3)

    inputs, labels = inputs.to(device), labels.to(device)

    # model
    net = FooNet(neural_num=3, layers=3)
    net = nn.DataParallel(net)
    net.to(device)

    # training
    for epoch in range(1):

        outputs = net(inputs)

        print("model outputs.size: {}".format(outputs.size()))

    print("CUDA_VISIBLE_DEVICES :{}".format(os.environ["CUDA_VISIBLE_DEVICES"]))
    print("device_count :{}".format(torch.cuda.device_count()))

El resultado es el siguiente:

batch size in forward: 8
model outputs.size: torch.Size([16, 3])
CUDA_VISIBLE_DEVICES :0,1
device_count :2

El siguiente código se ordena según la memoria restante de la GPU.

 def get_gpu_memory():
        import platform
        if 'Windows' != platform.system():
            import os
            os.system('nvidia-smi -q -d Memory | grep -A4 GPU | grep Free > tmp.txt')
            memory_gpu = [int(x.split()[2]) for x in open('tmp.txt', 'r').readlines()]
            os.system('rm tmp.txt')
        else:
            memory_gpu = False
            print("显存计算功能暂不支持windows操作系统")
        return memory_gpu


    gpu_memory = get_gpu_memory()
    if not gpu_memory:
        print("\ngpu free memory: {}".format(gpu_memory))
        gpu_list = np.argsort(gpu_memory)[::-1]

        gpu_list_str = ','.join(map(str, gpu_list))
        os.environ.setdefault("CUDA_VISIBLE_DEVICES", gpu_list_str)
        device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

Entre ellos nvidia-smi -q -d Memory, consulta la información de la memoria de todas las GPU, -qindica la consulta -dy especifica el contenido de la consulta.

nvidia-smi -q -d Memory | grep -A4 GPUIntercepta las primeras 4 líneas de la GPU, de la siguiente manera:

Attached GPUs                       : 2
GPU 00000000:1A:00.0
    FB Memory Usage
        Total                       : 24220 MiB
        Used                        : 845 MiB
        Free                        : 23375 MiB
--
GPU 00000000:68:00.0
    FB Memory Usage
        Total                       : 24217 MiB
        Used                        : 50 MiB
        Free                        : 24167 MiB

nvidia-smi -q -d Memory | grep -A4 GPU | grep FreeEs extraer la línea donde se encuentra Free, es decir, extraer la información de la memoria restante, de la siguiente manera:

        Free                        : 23375 MiB
        Free                        : 24167 MiB

nvidia-smi -q -d Memory | grep -A4 GPU | grep Free > tmp.txtGuarda la información de la memoria restante en tmp.txt.

[int(x.split()[2]) for x in open('tmp.txt', 'r').readlines()]Cada fila se procesa utilizando una expresión de lista.

Suponiendo x="Free: 23375 MiB", entonces x.split() divide por espacios de forma predeterminada y el resultado es:

['Free', ':', '23375', 'MiB']

El resultado de x.split()[2] es 23375.

Supongamos gpu_memory=['5','9','3']que np.argsort(gpu_memory)el resultado es array([2, 0, 1], dtype=int64)que el índice ordenado se toma de pequeño a grande. np.argsort(gpu_memory)[::-1]El resultado es array([1, 0, 2], dtype=int64)que se invierte el orden de los elementos.

En Python, list[<start>:<stop>:<step>]significa eliminar elementos de principio a fin, y el intervalo es paso. Paso = -1 significa eliminar elementos de principio a fin. start toma por defecto la posición del primer elemento y stop toma por defecto la posición del último elemento.

El resultado de ','.join(map(str, gpu_list)) es '1,0,2'.

Finalmente, os.environ.setdefault("CUDA_VISIBLE_DEVICES", gpu_list_str) establece la relación correspondiente de mayor a menor según la memoria restante de la GPU, de modo que la GPU con la mayor memoria restante es la GPU principal de forma predeterminada.

Insertar descripción de la imagen aquí

Utilice el comando Aumentar la utilización de GPU
nvidia-smi para ver la utilización de GPU, como se muestra en la siguiente figura.

Insertar descripción de la imagen aquí

En la captura de pantalla anterior, hay dos tarjetas gráficas (GPU), la parte superior muestra la información de las tarjetas gráficas y la parte inferior muestra los procesos que se ejecutan en cada tarjeta gráfica . Puede ver que la GPU número 0 está ejecutando el proceso con PID 14383. Memory UsageIndica la tasa de uso de la memoria de video. La GPU numerada 0 usa 16555 MBmemoria de video y la tasa de utilización de la memoria de video es aproximadamente del 70%. Volatile GPU-UtilIndica el cálculo de la utilización real de la potencia informática de la GPU. La GPU numerada 0 tiene solo el 27% de uso.

Aunque el uso de la GPU puede acelerar el entrenamiento del modelo, si el uso de memoria y la utilidad de GPU volátil de la GPU son demasiado bajos, significa que la GPU no se utiliza por completo.

Por lo tanto, cuando utilice una GPU para entrenar un modelo, debe maximizar los dos indicadores de uso de memoria de GPU y GPU-Util volátil, lo que puede acelerar aún más su proceso de entrenamiento.

Hablemos de cómo mejorar estos dos indicadores.

El uso de memoria
es un indicador de que la cantidad de datos está determinada principalmente por el tamaño del modelo y el tamaño de la cantidad de datos.

El tamaño del modelo está determinado por los parámetros y la estructura de la red: cuanto más grande es el modelo, más lento es el entrenamiento.

Lo que ajustamos principalmente es el tamaño de los datos de entrenamiento para cada lote, que es tamaño_lote.

Cuando la estructura del modelo sea fija, intente establecer el tamaño del lote lo más grande posible para aprovechar al máximo la memoria de la GPU.


Establecer un tamaño de lote relativamente grande en Volatile GPU-Util puede aumentar el uso de memoria de la GPU, pero no necesariamente aumenta el uso de la unidad informática de la GPU.

Como puede ver desde el frente, nuestros datos se leen primero en la CPU y, durante el entrenamiento del bucle, se cargan de la CPU a la CPU a través del método tensor.to(), como se muestra en el siguiente código.

# 遍历 train_loader 取数据
for i, data in enumerate(train_loader):
    inputs, labels = data
    inputs = inputs.to(device) # 把数据从 CPU 加载到 GPU
    labels = labels.to(device) # 把数据从 CPU 加载到 GPU
    .
    .
    .

Si el tamaño del lote es relativamente grande, entonces en Dataset y DataLoader, la CPU procesará un lote de datos muy lentamente. En este momento, encontrará que el valor de Volatile GPU-Util será 0%, 20%, 70 %, 95%, 0% sigue cambiando.

El comando nvidia-smi puede verificar la utilización de la GPU, pero no puede actualizar dinámicamente la pantalla. Si desea actualizar la visualización de la información de la GPU cada segundo, puede usar watch -n 1 nvidia-smi.

De hecho, esto se debe a que la GPU procesa los datos muy rápidamente, mientras que la CPU los procesa lentamente. Cada vez que la GPU recibe un lote de datos, la tasa de uso aumenta gradualmente. Después de procesar los datos de este lote, la tasa de uso disminuye gradualmente hasta que la CPU transmite los datos del siguiente lote.

La solución es: configurar Dataloaderlos dos parámetros:

  • num_workers: De forma predeterminada, solo se utiliza una CPU para leer y procesar datos. Se puede configurar en 4, 8, 16 y otros parámetros. Pero el número de hilos no siempre es mejor. Debido a que el procesamiento de múltiples núcleos necesita distribuir datos a cada CPU, una vez completado el procesamiento, es necesario recopilar datos de varias CPU, y este proceso también lleva tiempo. Si num_workers se establece en un valor demasiado grande, operaciones como la distribución y recopilación de datos llevarán demasiado tiempo, lo que reducirá la eficiencia.
  • pin_memory: si la memoria es grande, se recomienda configurarla en Verdadero.
    • Establecerlo en Verdadero significa que los datos se asignan directamente al bloque de memoria relevante de la GPU, lo que ahorra un poco de tiempo de transmisión de datos.
    • Establecido en Falso, lo que significa que los datos se transfieren desde la CPU a la RAM de caché y luego a la GPU.

Informes de errores y soluciones al cargar modelos de GPU

  • Error 1:

Si el modelo se guarda en la GPU y se usa torch.load(path_state_dict) para cargar el modelo en un dispositivo sin GPU, aparecerá el siguiente error:

RuntimeError: Attempting to deserialize object on a CUDA device but torch.cuda.is_available() is False. If you are running on a CPU-only machine, please use torch.load with map_location=torch.device('cpu') to map your storages to the CPU.

Posible motivo: después de guardar el modelo entrenado por GPU, no se puede cargar directamente en el dispositivo sin GPU. La solución es establecermap_location="cpu":torch.load(path_state_dict, map_location="cpu")

  • Error 2:

Si el modelo está empaquetado con net = nn.DataParallel(net), entonces se agregará mmodule. delante de los nombres de todas las capas de red. Si guarda el modelo y lo carga nuevamente sin usar el empaquetado nn.DataParallel(), la carga fallará porque los nombres de los parámetros en state_dict no coinciden.

Missing key(s) in state_dict: xxxxxxxxxx

Unexpected key(s) in state_dict:xxxxxxxxxx

La solución es recorrer los parámetros de state_dict después de cargar los parámetros. Si el nombre comienza con módulo, elimine el módulo. El código se muestra a continuación:


from collections import OrderedDict
new_state_dict = OrderedDict()
for k, v in state_dict.items():
    namekey = k[7:] if k.startswith('module.') else k
    new_state_dict[namekey] = v

Luego cargue los parámetros en el modelo.

4. Errores comunes en pytorch

Supongo que te gusta

Origin blog.csdn.net/fb_941219/article/details/130642201
Recomendado
Clasificación