记录本人学习深度学习的过程,采用的是Pytorch框架,代码中间会有自己的理解,可能会有错误,希望各位大佬批评指正,也与各位在AI路上的学习者共勉!
这是在命令行或者终端中输入的代码,可以用于查询自己GPU的显存、利用率和CUDA的版本,仅对NVIDIA英伟达独立显卡有用,集成显卡是不能用的。一般来说,显卡的种类决定了CUDA的版本,CUDA的版本跟要安装的Pytorch版本有关系,而GPU的利用率越高,比如100%,说明了处于满负荷的状态,能够较大的发挥显卡的性能,可以实时查看一下,如果利用率很低就要检查一下自己的代码了。
!nvidia-smi
正常会输出类似的东西,可以看到CUDA的Version是11.2,,显存是15109MiB,相当于是15G了,右边显示0%,说明目前的显卡利用率是0%,并且下面的Processes显示没有进程。
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 460.32.03 Driver Version: 460.32.03 CUDA Version: 11.2 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 Tesla T4 Off | 00000000:00:04.0 Off | 0 |
| N/A 59C P0 28W / 70W | 0MiB / 15109MiB | 0% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| No running processes found |
+-----------------------------------------------------------------------------+
导入一些需要用到的函数库,比如torch、torch.nn、DataLoader等等,这里导入time函数库是为了记录一下训练网络的时间。
import torch
import torch.nn as nn
import torchvision
from torch.utils.data import DataLoader
import time
import torch.nn.functional as F
如果只是自己学习,这一步应该是不需要的,固定随机种子的目的是为了保证自己的模型结果是可以复现的,这在科研工作中十分重要,如果你的实验只有你自己能做出最好的结果,别人都做不出来,那你的研究就没有什么意义了。
说到固定随机种子,就想到一篇很有意思的论文《Torch.manual_seed(3407) is all you need: On the influence of random seeds in deep learning architectures for computer vision》,中文译名是Torch.manual_seed(3407) is all you need:论随机种子对计算机视觉深度学习架构的影响,作者探究了随机种子选择对准确性的影响,用了10000个种子进行搜索,最终发现3407这个种子相较于平均值特别好,所以如果不知道设置什么种子,不如就设置它哈哈哈。
随机种子固定后,随机梯度下降的方向就会一样的,不带有随机性了(避免深度炼丹),而且用到Dataloader的时候也会让它加载的数据序列是一样的。
import os
import random
import numpy as np
def my_seed(seed=3407):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
# 调用一下
my_seed(seed=3407)
个人的写代码顺序比较喜欢先写网络结构,毕竟它是我们之后学习和“魔改”的主要对象。下面写一个自己设计的简单网络,考虑到我们等会用的数据集是CIFAR10,是一个10分类的数据集,所以out_channels是10,而我们用的数据集图片是3个RGB通道的,所以in_channels是3。
我们自己定义网络结构的时候,要首先继承nn.Module,并且重写构造函数__init__和forward方法。
(1)__init__构造函数中包含了网络中可学习参数的层,比如全连接、池化层、卷积层等,不含可学习参数的层其实也可以放在里面,比如各种激活函数,但是个人更推荐直接放在forward方法里面,用nn.functional as F来方便调用,形式就像F.relu()。
(2)forward方法是必须要重写的,它是实现网络模型具体功能的方法,用于数据流通、连接各个层的。一定要按照顺序来写。
class DeepNet(nn.Module):
def __init__(self, in_channels, out_channels):
super(DeepNet, self).__init__()
self.conv1 = nn.Conv2d(in_channels, 64, 3)
self.conv2 = nn.Conv2d(64, 64, 3, stride=1, padding=1)
self.max_pooling = nn.MaxPool2d(2, stride=2, padding=1)
self.bn = nn.BatchNorm2d(64)
self.fc1 = nn.Linear(112*112*64, 64)
self.fc2 = nn.Linear(64, out_channels)
def forward(self, x):
x = self.conv1(x)
x = self.bn(x)
x = F.relu(x)
x = self.conv2(x)
x = self.bn(x)
x = F.relu(x)
x = self.max_pooling(x)
x = x.view(x.shape[0],-1)
x = self.fc1(x)
x = F.relu(x)
x = self.fc2(x)
return x
这里输入的图像大小是224x224(写到这里,突然觉应该先写数据处理和数据集,因为网络结构跟数据有关系┭┮﹏┭┮,之后一定要注意),解释一下nn.Conv2d(in_channels, 64, 3),这里说明图像进入的通道数是in_channels=3,这很合理,然后出去的通道数是64,升维度了,然后卷积核Kernel_size是3,其他都是默认,比如padding默认是0,stride默认是1,经过这第一个卷积,图像的大小发生改变了,具体的变化跟kernel_size、padding、stride有关系,总之这里的输出的特征图大小由原本的224x224变为了222x222。
图像的输出长宽 = (图像输入的长宽-卷积核kernel_size+2*padding)/步长stride+1
222 = (224-3+0)/ 1+1
这里先定义一个网络,然后把它打印出来看看:
net=DeepNet(3,10)
print(net)
DeepNet(
(conv1): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1))
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(max_pooling): MaxPool2d(kernel_size=2, stride=2, padding=1, dilation=1, ceil_mode=False)
(bn): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(fc1): Linear(in_features=802816, out_features=64, bias=True)
(fc2): Linear(in_features=64, out_features=10, bias=True)
)
定义transform的相关操作,会在datasets中得到使用,这里定义了转换数据为tensor、规定图像的大小、均值化和随机水平翻转的操作。
transform = torchvision.transforms.Compose(
[
torchvision.transforms.ToTensor(), # 将数据转换为pytorch的tensor
torchvision.transforms.Resize(size=([224, 224])),
torchvision.transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]), # 均值、方差
torchvision.transforms.RandomHorizontalFlip() # 随机水平翻转
]
)
train_data=torchvision.datasets.CIFAR10(root="dataset",train=True,transform=transform,download=True)
test_data=torchvision.datasets.CIFAR10(root="dataset",train=False,transform=transform,download=True)
train_data_size=len(train_data)
test_data_size=len(test_data)
print("训练集的长度为{}".format(train_data_size))
print("测试集的长度为{}".format(test_data_size))
训练集的长度为50000
测试集的长度为10000
train_dataloader=DataLoader(train_data,batch_size=64)
test_dataloader=DataLoader(test_data,batch_size=64)
这里.cuda()是将网络和损失函数放到显卡里面去计算,如果没有显卡的话这里可以删除.cuda()
model=DeepNet(3,10).cuda()
loss_fn=nn.CrossEntropyLoss()
loss_fn=loss_fn.cuda()
lr_rate=1e-4
optimizer=torch.optim.Adam(model.parameters(),lr=lr_rate)
total_train_step=0
total_test_step=0
epoch=10
start_time=time.time()
min_loss=100
本质上其实是两层循环,首先第一层训练是训练次数i的循环,在这一层循环中,如果模型中有BN层(Batch Normalization)和Dropout,需要在训练时添加model.train(),将模型调整为训练状态,从而启用BN层和Dropout,我们这里有用BN层,所以要用。同时,在测试时添加model.eval(),作用是不启用BN和Dropout层。
我们都知道,在训练模式中,BN层作用是去计算数据的方差和均值,对不同样本的同一个通道直接做归一化处理;Dropout会按照设置的参数决定保留激活单元的概率。那么在model.eval()下,Dropout层会让所有的激活单元都通过,而BN层会停止计算和更新数据的方差和均值。同时的话,模型在测试模式下不会更新权重(不可能在测试集上进行训练),但是不会影响各层的梯度计算行为,会像训练模式一样梯度计算和存储,只是不进行反向传播,因此才能在测试模型下计算损失。
第二层训练就是数据在dataloader中的迭代。当一个完整的数据集通过了神经网络一次并且返回了一次,这个过程称为一轮epoch,而一轮epoch训练中包含了多次的迭代过程,这就有了batch的概念,也就是批次。当不能将数据一次性通过神经网络的时候(因此显卡的显存是有限的),就需要将数据集分成几个batch,比如设置batch=100,训练集的总量是1000,那么一轮epoch训练就有1000/100=10次迭代(iteration)。
在每轮epoch中,我们都要获得数据和标签,这里的数据形式是图像,也就是imgs,标签是targets,把它们存到显卡中计算,然后去计算图像在模型中训练获得的结果,对应了outputs=model(imgs),outputs就是获得的模型预测标签,既然是训练就可能有误差,从而我们计算损失,对应了loss=loss_fn(outputs, targets),注意这里的targets是指数据集中给出的正确标签,去计算它们之间的差距。
optimizer.zero_grad()、loss.backward()和optimizer.step()分别是说,先将梯度归零,然后反向传播计算每个参数的梯度值,最后通过梯度下降进行参数更新,也就完成了一次迭代过程。
测试过程是类似的,只不过多了计算accuracy的过程,accuracy=(outputs.argmax(1)==targets).sum()是指判断模型返回向量中值最大的那个标签,是否跟真实标签一致,如果是那么就求和算进accuracy。
for i in range(epoch):
print("==========================第{}轮训练开始==========================".format(i+1))
model.train()
for data in train_dataloader:
imgs, targets = data
imgs=imgs.cuda()
targets=targets.cuda()
outputs=model(imgs)
loss=loss_fn(outputs,targets)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_train_step+=1
if total_train_step%100==0:
end_time=time.time()
print("训练时间:{}".format(end_time-start_time))
print("训练次数:{},Loss:{}".format(total_train_step, loss))
model.eval()
total_test_loss = 0
total_accuracy = 0
with torch.no_grad():
for data in test_dataloader:
imgs, targets = data
imgs=imgs.cuda()
targets=targets.cuda()
outputs=model(imgs)
loss=loss_fn(outputs,targets)
total_test_loss=total_test_loss+loss
accuracy=(outputs.argmax(1)==targets).sum()
total_accuracy+=accuracy
print("整体测试集上的loss:{}".format(total_test_loss))
print("整体测试集上的accuracy:{}".format(total_accuracy/test_data_size))
total_test_step+=1
if total_test_loss<min_loss:
min_loss=total_test_loss
torch.save(model,"model_{}".format(i))
print("最好的模型已经保存,在第{}轮".format(i))
后续会尝试用tqdm函数库来显示训练的进度条,以提升炼丹过程中的愉悦感,这套代码将网络结构换一下就可以接着用,不过整个框架还不是非常完善,还缺少单独的predict等可视化操作,还有待进一步改进!
import torch
import torch.nn as nn
import torchvision
from torch.utils.data import DataLoader
import time
import torch.nn.functional as F
class DeepNet(nn.Module):
def __init__(self, in_channels, out_channels):
super(DeepNet, self).__init__()
self.conv1 = nn.Conv2d(in_channels, 64, 3)
self.conv2 = nn.Conv2d(64, 64, 3, stride=1, padding=1)
self.max_pooling = nn.MaxPool2d(2, stride=2, padding=1)
self.bn = nn.BatchNorm2d(64)
self.fc1 = nn.Linear(112*112*64, 64)
self.fc2 = nn.Linear(64, 10)
def forward(self, x):
x = self.conv1(x)
x = self.bn(x)
x = F.relu(x)
x = self.conv2(x)
x = self.bn(x)
x = F.relu(x)
x = self.max_pooling(x)
x = x.view(x.shape[0],-1)
x = self.fc1(x)
x = F.relu(x)
x = self.fc2(x)
return x
transform = torchvision.transforms.Compose(
[
torchvision.transforms.ToTensor(), # 将数据转换为pytorch的tensor
torchvision.transforms.Resize(size=([224, 224])), # 将图像的大小设置为224*224
torchvision.transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]), # 均值、方差
torchvision.transforms.RandomHorizontalFlip() # 随机水平翻转
]
)
train_data=torchvision.datasets.CIFAR10(root="dataset",train=True,transform=transform,download=True)
test_data=torchvision.datasets.CIFAR10(root="dataset",train=False,transform=transform,download=True)
train_data_size=len(train_data)
test_data_size=len(test_data)
print("训练集的长度为{}".format(train_data_size))
print("测试集的长度为{}".format(test_data_size))
train_dataloader=DataLoader(train_data,batch_size=64)
test_dataloader=DataLoader(test_data,batch_size=64)
model=DeepNet(3,10).cuda()
loss_fn=nn.CrossEntropyLoss()
loss_fn=loss_fn.cuda()
lr_rate=1e-4
optimizer=torch.optim.Adam(model.parameters(),lr=lr_rate)
total_train_step=0
total_test_step=0
epoch=10
start_time=time.time()
min_loss=100
for i in range(epoch):
print("==========================第{}轮训练开始==========================".format(i+1))
model.train()
for data in train_dataloader:
imgs, targets = data
imgs=imgs.cuda()
targets=targets.cuda()
outputs=model(imgs)
loss=loss_fn(outputs,targets)
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_train_step+=1
if total_train_step%100==0:
end_time=time.time()
print("训练时间:{}".format(end_time-start_time))
print("训练次数:{},Loss:{}".format(total_train_step, loss))
model.eval()
total_test_loss = 0
total_accuracy = 0
with torch.no_grad():
for data in test_dataloader:
imgs, targets = data
imgs=imgs.cuda()
targets=targets.cuda()
outputs=model(imgs)
loss=loss_fn(outputs,targets)
total_test_loss=total_test_loss+loss
accuracy=(outputs.argmax(1)==targets).sum()
total_accuracy+=accuracy
print("整体测试集上的loss:{}".format(total_test_loss))
print("整体测试集上的accuracy:{}".format(total_accuracy/test_data_size))
total_test_step+=1
if total_test_loss<min_loss:
min_loss=total_test_loss
torch.save(model,"model_{}".format(i))
print("最好的模型已经保存,在第{}轮".format(i))