常见的分类网络都可以分为两部分,一部分是特征提取部分,另一部分是分类部分。
1、特征提取部分的功能是对输入进来的图片进行特征提取 , 优秀的特征可以帮助更容易区分目标,所以特征提取部分一般由各类卷积组成,卷积拥有强大的特征提取能力 ;
2、分类部分会利用特征提取部分获取到的特征进行分类 ,分类部分一般由全连接组成, 特征提取部分获取到的特征一般是一维向量,可以直接进行全连接分类。
通常情况下,特征提取部分就是我们平常了解到的各种神经网络,比如VGG、Mobilenet、Resnet等等;而分类部分就是一次或者几次的全连接,最终我们会获得一个长度为num_classes的一维向量。
接下来让我们从头开始定义我们的神经网络:
# 导入库:
import torch
import torchvision
import torchvision.models
from matplotlib import pyplot as plt
from tqdm import tqdm
from torch import nn
from torch.utils.data import DataLoader
from torchvision.transforms import transforms
# 图像预处理方法:
data_transform = {
"train": transforms.Compose([transforms.RandomResizedCrop(120),
#先随机采集裁剪,然后对得到的图像缩放为同一大小
transforms.RandomHorizontalFlip(),
#以给定的概率随机水平旋转给定的PIL的图像,默认为0.5;
transforms.ToTensor(),#将给定图像转为Tensor
transforms.Normalize((0.5, 0.5, 0.5),
(0.5, 0.5, 0.5))]), # 归一化处理
"val": transforms.Compose([transforms.Resize((120, 120)),
# 这种预处理的地方尽量别修改,修改意味着需要修改网络结构的参数
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])}
# 数据导入方法: 导入自己的数据,自己的数据放在跟代码相同的文件夹下新建一个data文件夹,
#data文件夹里的新建一个train文件夹用于放置训练集的图片。也可以导入自带的CIFAR10数据集。
#同理新建一个val文件夹用于放置测试集的图片。
train_data = torchvision.datasets.CIFAR10(root = "../data" , train = True ,
download = False,transform=data_transform["train"])
#train_data = torchvision.datasets.ImageFolder(root="./data", transform=data_transform["train"])
traindata = DataLoader(dataset=train_data, batch_size=32, shuffle=True, num_workers=0)
test_data = torchvision.datasets.CIFAR10(root = "../data" , train = False ,
download = False,transform=data_transform["val"])
#test_data = torchvision.datasets.ImageFolder(root="../data", transform=data_transform["val"])
testdata = DataLoader(dataset=test_data, batch_size=32, shuffle=True,num_workers=0) # windows系统下,num_workers设置为0,linux系统下可以设置多进程
train_size = len(train_data) # 求出训练集的长度
test_size = len(test_data) # 求出测试集的长度
print(train_size) # 输出训练集的长度
print(test_size) # 输出测试集的长度
# 设置调用GPU,如果有GPU就调用GPU,如果没有GPU则调用CPU训练模型
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print("using {} device.".format(device))
输出结果:
10000
50000
using cuda:0 device.
(1)在图像预处理方法中,对图像操作过程详解 :
transforms.RandomResizedCrop(224) 将给定图像随机裁剪为不同的大小和宽高比,然后缩放所裁剪得到的图像为制定的大小;(即先随机采集,然后对裁剪得到的图像缩放为同一大小)。该操作的含义在于:即使只是该物体的一部分,也认为这是该类物体;
from matplotlib import pyplot as plt
from torchvision.transforms import transforms
from PIL import Image
img = Image.open("./11.PNG")
print("原图大小:",img.size)
data1 = transforms.RandomResizedCrop(224)(img)
print("随机裁剪后的大小:",data1.size)
data2 = transforms.RandomResizedCrop(120)(img)
data3 = transforms.RandomResizedCrop(64)(img)
plt.subplot(2,2,1),plt.imshow(img),plt.title("原图")
plt.subplot(2,2,2),plt.imshow(data1),plt.title("图1")
plt.subplot(2,2,3),plt.imshow(data2),plt.title("图2")
plt.subplot(2,2,4),plt.imshow(data3),plt.title("图3")
plt.show()
输出结果:
transforms.RandomHorizontalFlip() 以给定的概率随机水平旋转给定的PIL的图像,默认为0.5;
from matplotlib import pyplot as plt
from torchvision.transforms import transforms
from PIL import Image
img = Image.open("./11.PNG")
img1 = transforms.RandomHorizontalFlip()(img)
img2 = transforms.RandomHorizontalFlip()(img)
img3 = transforms.RandomHorizontalFlip()(img)
plt.subplot(2,2,1),plt.imshow(img),plt.title("原图")
plt.subplot(2,2,2),plt.imshow(img1),plt.title("图1")
plt.subplot(2,2,3),plt.imshow(img2),plt.title("图2")
plt.subplot(2,2,4),plt.imshow(img3),plt.title("图3")
plt.show()
(2)数据导入方法:
PyTorch基础-自定义数据集和数据加载器(2)_一只小小的土拨鼠的博客-CSDN博客
(3)导入指定设备
CPU和GPU的训练差别太大了,推荐使用GPU训练,例如cpu2分钟10%,gpu 1:30训练一轮。
torch.device用法总结:
使用GPU进行深度学习环境的搭建_一只小小的土拨鼠的博客-CSDN博客
torch.device
代表将 torch.Tensor
分配到的设备的对象。 torch.device
包含一个设备类型( 'cpu'
或 'cuda'
设备类型)和可选的设备的序号。如果设备序号不存在,则为当前设备;
#通过字符串构造设备
torch.device('cpu')
torch.device('cuda') # current cuda device
#通过字符串+序号构造设备
torch.device('cuda', 0)
torch.device('cpu', 0)
#可直接使用字符串构建
cuda1 = torch.device('cuda:0')
torch.randn((2,3), device=cuda1)
#有cuda的也可直接输入序号
torch.device(1)
判断使用的设备是哪一种:
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print("using {} device.".format(device))
该模型参加2014年的 ImageNet图像分类与定位挑战赛,取得了优异成绩:在分类任务上排名第二,在定位任务上排名第一。VGG的Classification模型从原理上并没有与传统的CNN模型有太大不同:
训练:各种数据Augmentation(剪裁,不同大小,调亮度,饱和度,对比度,偏色),剪裁送入CNN模型,Softmax,Backprop。
测试:尽量把测试数据又各种Augmenting(剪裁,不同大小),把测试数据各种Augmenting后在训练的不同模型上的结果再继续Averaging出最后的结果。
本例程使用pytorch框架复现了一下VGG代码。同时画出训练集的loss 、accuracy 和测试集的loss、accuracy的折线图
它的结构如下图所示:
这是一个VGG16的结构图,很好的反应了VGG16的结构,整个VGG16由三种不同的层组成,分别是卷积层、最大池化层、全连接层。
VGG16的具体执行方式如下:
这里我的电脑gpu显存不够,所以图片大小只有(120,120),最终特征层net输出为(3,3,512)
class VGG(nn.Module):
def __init__(self, features, num_classes=10, init_weights=False): # 自己是几种就把这个7改成几
super(VGG, self).__init__()
self.features = features
#self.avgpool = nn.AdaptiveAvgPool2d((3, 3))
self.classifier = nn.Sequential(
nn.Linear(512*3*3, 4096),
nn.ReLU(True),
nn.Dropout(p=0.5),
nn.Linear(4096, 4096),
nn.ReLU(True),
nn.Dropout(p=0.5),
nn.Linear(4096, num_classes)
)
if init_weights:
self._initialize_weights() # 参数初始化
def forward(self, x):
# N x 3 x 224 x 224
x = self.features(x)
#x = self.avgpool(x)
# N x 512 x 7 x 7
x = torch.flatten(x, start_dim=1) # x = torch.flatten(x, 1)
# N x 512*7*7
x = self.classifier(x)
return x
def _initialize_weights(self):
for m in self.modules(): # 遍历各个层进行参数初始化
if isinstance(m, nn.Conv2d): # 如果是卷积层的话 进行下方初始化
# nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')# 正态分布初始化
nn.init.xavier_uniform_(m.weight) # 均匀分布初始化
if m.bias is not None:
nn.init.constant_(m.bias, 0) # 如果偏置不是0 将偏置置成0 相当于对偏置进行初始化
elif isinstance(m, nn.Linear): # 如果是全连接层
nn.init.xavier_uniform_(m.weight) # 也进行正态分布初始化
# nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0) # 将所有偏执置为0
def make_features(cfg: list):
layers = []
in_channels = 3
for v in cfg:
if v == "M":
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
else:
conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
layers += [conv2d, nn.ReLU(True)]
in_channels = v
return nn.Sequential(*layers)
cfgs = {
'vgg11': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'vgg13': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'vgg16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
'vgg19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}
def vgg(model_name="vgg19", **kwargs):
assert model_name in cfgs, "Warning: model number {} not in cfgs dict!".format(model_name)
cfg = cfgs[model_name]
model = VGG(make_features(cfg), **kwargs)
return model
VGGnet = vgg(num_classes=10, init_weights=True) # 将模型命名为#自己是几种就把这个7改成几
VGGnet.to(device)
print(VGGnet.to(device)) # 输出模型结构
# 设置训练需要的参数,epoch,学习率learning 优化器。损失函数。
epoch = 1 # 这里是训练的轮数
learning = 0.0001 # 学习率
optimizer = torch.optim.Adam(VGGnet.parameters(), lr=learning) # 优化器
loss = nn.CrossEntropyLoss() # 损失函数
#设置四个空数组,用来存放训练集的loss和accuracy 测试集的loss和 accuracy
train_loss_all = []
train_accur_all = []
test_loss_all = []
test_accur_all = []
#开始训练:
for i in range(epoch): # 开始迭代
train_loss = 0 # 训练集的损失初始设为0
train_num = 0.0 #
train_accuracy = 0.0 # 训练集的准确率初始设为0
VGGnet.train() # 将模型设置成 训练模式
train_bar = tqdm(traindata) # 用于进度条显示,没啥实际用处
for step, data in enumerate(train_bar): # 开始迭代跑, enumerate这个函数不懂可以查查,将训练集分为 data是序号,data是数据
img, target = data # 将data 分位 img图片,target标签
optimizer.zero_grad() # 清空历史梯度
outputs = VGGnet(img.to(device)) # 将图片打入网络进行训练,outputs是输出的结果
loss1 = loss(outputs, target.to(device)) # 计算神经网络输出的结果outputs与图片真实标签target的差别-这就是我们通常情况下称为的损失
outputs = torch.argmax(outputs, 1) # 会输出10个值,最大的值就是我们预测的结果 求最大值
loss1.backward() # 神经网络反向传播
optimizer.step() # 梯度优化 用上面的abam优化
train_loss += abs(loss1.item()) * img.size(0) # 将所有损失的绝对值加起来
accuracy = torch.sum(outputs == target.to(device)) # outputs == target的 即使预测正确的,统计预测正确的个数,从而计算准确率
train_accuracy = train_accuracy + accuracy # 求训练集的准确率
train_num += img.size(0) #
print("epoch:{} , train-Loss:{} , train-accuracy:{}".format(i + 1, train_loss / train_num, # 输出训练情况
train_accuracy / train_num))
train_loss_all.append(train_loss / train_num) # 将训练的损失放到一个列表里 方便后续画图
train_accur_all.append(train_accuracy.double().item() / train_num) # 训练集的准确率
test_loss = 0 # 同上 测试损失
test_accuracy = 0.0 # 测试准确率
test_num = 0
VGGnet.eval() # 将模型调整为测试模型
with torch.no_grad(): # 清空历史梯度,进行测试 与训练最大的区别是测试过程中取消了反向传播
test_bar = tqdm(testdata)
for data in test_bar:
img, target = data
outputs = VGGnet(img.to(device))
loss2 = loss(outputs, target.to(device))
outputs = torch.argmax(outputs, 1)
test_loss = test_loss + abs(loss2.item()) * img.size(0)
accuracy = torch.sum(outputs == target.to(device))
test_accuracy = test_accuracy + accuracy
test_num += img.size(0)
print("test-Loss:{} , test-accuracy:{}".format(test_loss / test_num, test_accuracy / test_num))
test_loss_all.append(test_loss / test_num)
test_accur_all.append(test_accuracy.double().item() / test_num)
# 下面的是画图过程,将上述存放的列表 画出来即可
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(range(epoch), train_loss_all,
"ro-", label="Train loss")
plt.plot(range(epoch), test_loss_all,
"bs-", label="test loss")
plt.legend()
plt.xlabel("epoch")
plt.ylabel("Loss")
plt.subplot(1, 2, 2)
plt.plot(range(epoch), train_accur_all,
"ro-", label="Train accur")
plt.plot(range(epoch), test_accur_all,
"bs-", label="test accur")
plt.xlabel("epoch")
plt.ylabel("acc")
plt.legend()
plt.show()
torch.save(VGGnet, "VGG.pth")
print("模型已保存")
这里是gitub源码:GitHub - Brave-you/VGG16
池化层主要分为两类:最大值(Max)池化层,均值(Avg)池化层。前者用取最大值的方式抽取样本,后者用平均值的方式抽取样本。 其中nn.AdaptiveAvgPool2d是二维自适应平均池化运算 ,自适应池化层。函数通过输入原始尺寸和目标尺寸,自适应地计算核的大小和每次移动的步长。对于任何输入大小,输出大小均为指定的H×W大小。
#torch.nn.AdaptiveAvgPool2d(output_size)
import torch
from torch import nn
img=torch.arange(24,dtype=torch.float).reshape(1,1,4,6)
pool_1=nn.AdaptiveAvgPool2d((2,3))
pool_2=nn.AdaptiveAvgPool2d(2)
img_1=pool_1(img)
img_2=pool_2(img)
print(img)
print(img_1)
print(img_2)
输出结果:
tensor([[[[ 0., 1., 2., 3., 4., 5.],
[ 6., 7., 8., 9., 10., 11.],
[12., 13., 14., 15., 16., 17.],
[18., 19., 20., 21., 22., 23.]]]])
tensor([[[[ 3.5000, 5.5000, 7.5000],
[15.5000, 17.5000, 19.5000]]]])
tensor([[[[ 4., 7.],
[16., 19.]]]])
output_size
:指定的输出大小,可以是元组(H,W),或者是单个的数,如果是单个的数,则表示输出的高和宽尺寸一样, output_size
大小可以大于输入的图片尺寸大小。 当 output_size
指定为1时,相当于全局平均池化
第一种情况-输入尺寸是输出尺寸的整数倍
自适应层的核的大小相同且不重叠,AdaptivePooling可以改写为General Pooling,核的大小和步长的计算规则如下:
#input_size: 输入尺寸 output_size: 输出尺寸
stride = intput_size // output_size #步长
kernel_size = input_size - ( output_size -1 ) * stride #核的尺寸
padding = 0
第二种情况-输入尺寸不是输出尺寸的整数倍
在这种情况下,自适应层的核是可变大小的,且可能互相重叠,此时固定步长和核的尺寸的General Pooling不能改写AdaptivePooling。详细解释如下:
AdaptiveAvgPool2d理解(中网、外网整合)_xiyou__的博客-CSDN博客_adaptiveavgpool2d
权重初始化代码 init.kaiming_uniform_和kaiming_normal_ 可以使得输入值x的方差和经过网络层后的输出值y的方差一致。 神经网络要优化一个非常复杂的非线性模型,而且基本没有全局最优解,初始化在其中扮演着非常重要的作用,尤其在没有BN等技术的早期,它直接影响模型能否收敛。
好的初始化应该满足以下两个条件: (1) 让神经元各层激活值不会出现饱和现象; (2) 各层激活值也不能为0。 权重初始化的目的是防止在深度神经网络的正向(前向)传播过程中层激活函数的输出损失梯度出现爆炸或消失。随机初始化就是搞一些很小的值进行初始化,实验表明大了就容易饱和,小的就激活不动。 如果使用了BatchNorm的话,不同的初始化方法结果差不多 ,说明使用BN可以使得初始化不那么敏感了。
pytorch默认使用 kaiming正态分布 初始化卷积层参数。,所以不用手动初始化,权重是用的0均值高斯分布, 偏置是0均值0方差的均匀分布 。
torch.nn.init.kaiming_normal_ # kaiming正态分布 :
(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu')
torch.nn.init.xavier_normal_(w, #xavier正态分布
gain=torch.nn.init.calculate_gain('relu'))
torch.nn.init.kaiming_uniform_ # kaiming均匀分布:
(tensor, a=0, mode='fan_in', nonlinearity='leaky_relu')
torch.nn.init.xavier_uniform_(w, #xavier均匀分布
gain=torch.nn.init.calculate_gain('relu'))
选择"fan_in"可以保留前向计算中权重方差的大小。选择"fan_out"将保留后向传播的方差大小。 对于Linear模型,fan_in就是输入size,fan_out就是输出size。 详细解释如下:pytorch学习:xavier分布和kaiming分布_UQI-LIUWJ的博客-CSDN博客_xavier分布
#torch.nn.init.constant_(tensor,val),初始化参数使其为常值,即每个参数值都相同。
#一般是给网络中bias进行初始化。val:常量数值
nn.init.constant_(m.bias, 0)
#torch.init.normal_(tensor,mean=,std=) ,给tensor初始化,一般是给网络中参数weight初始化,
#初始化参数值符合正态分布。mean:均值,std:正态分布的标准差
nn.init.normal_(m.weight, 0, 0.01)
import torch
import torch.nn as nn
l=nn.Conv2d(2,2,kernel_size=1)
a=l.weight
b=l.bias
print("a:",a)
print("b:",b)
c=nn.init.normal_(l.weight,mean=0,std=0.01)
d=nn.init.constant_(l.bias,val=1)
print("c:",c)
print("d:",d)
输出结果:
a: Parameter containing:
tensor([[[[ 0.4417]],
[[-0.6113]]],
[[[-0.2329]],
[[ 0.3184]]]], requires_grad=True)
b: Parameter containing:
tensor([ 0.3144, -0.1925], requires_grad=True)
c: Parameter containing:
tensor([[[[ 0.0011]],
[[-0.0047]]],
[[[-0.0147]],
[[ 0.0061]]]], requires_grad=True)
d: Parameter containing:
tensor([1., 1.], requires_grad=True)
nn.BatchNorm2D() 进行的操作是对channel维度就行批归一化,卷积层之后总会添加BatchNorm2d进行数据的归一化处理,这使得数据在进行Relu之前不会因为数据过大而导致网络性能的不稳定。
torch.nn.BatchNorm2d(features, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
具体的计算过程是:
其中,是之前的元素, 是channel维度上的均值, σ 是channel 维度上的标准差, ϵ 是一个系数因子, 防止分母为0,默认为 。gamma和beta为一维数组。
import torch
import torch.nn as nn
m=nn.BatchNorm2d(2,affine=True) #affine参数设为True表示weight和bias将被使用
input=torch.randn(1,2,2,3)
output=m(input)
print(input)#输出当前张量
print(m.weight)#未经过反向传播,权重全为1 gamma
print(m.bias) #偏置全为0 beta
print(output) #经过BatchNorm2d后的输出
print("输入的第一个维度:")
print(input[0][0]) #这个数据是第一个2*3的二维矩阵,即input的
#求第一个维度的均值和方差
firstDimenMean=torch.Tensor.mean(input[0][0])#第一个维度的均值
firstDimenVar=torch.Tensor.var(input[0][0],False) #false表示贝塞尔校正不会被使用,第一个维度的方差
print(m) # 输出BatchNorm2d的结构参数
print(firstDimenMean)
print(firstDimenVar)
batchnormone=((input[0][0][0][0]-firstDimenMean)/(torch.pow(firstDimenVar,0.5)+m.eps))\
*m.weight[0]+m.bias[0] #根据公式计算第一个维度的输出
print(batchnormone)
输出结果:[(-1.3369+0.4498)/( 0.9422 + )]*1+0=0.9139
tensor([[[[-1.3369, 1.0957, -0.8165],
[-0.5185, 0.5142, -1.6370]],
[[ 1.0270, 0.1169, -0.1541],
[-0.7898, -1.7316, 1.3420]]]])
Parameter containing:
tensor([1., 1.], requires_grad=True)
Parameter containing:
tensor([0., 0.], requires_grad=True)
tensor([[[[-0.9139, 1.5922, -0.3777],
[-0.0707, 0.9931, -1.2230]],
[[ 1.0163, 0.1426, -0.1176],
[-0.7280, -1.6321, 1.3188]]]], grad_fn=)
输入的第一个维度:
tensor([[-1.3369, 1.0957, -0.8165],
[-0.5185, 0.5142, -1.6370]])
BatchNorm2d(2, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
tensor(-0.4498)
tensor(0.9422)
tensor(-0.9139, grad_fn=)
net.train()、net.eval():两者将网络中每一层的training分别设置为True和False
#一般在训练模型的代码段加入:
model.train()
#在测试模型时候加入:
model.eval()
因此,在使用PyTorch进行训练和测试时一定要记得把实例化的model指定train/eval。model.eval() 负责改变batchnorm、dropout的工作方式,在eval()模式下,dropout是不工作的。
Tqdm 是一个快速,可扩展的Python进度条,可以在 Python 长循环中添加一个进度提示信息,用户只需要封装任意的迭代器 tqdm(iterator)。
from tqdm import tqdm
for i in tqdm(range(1000)):
#do something
pass
plt.figure()参数使用详解
plt. plot()函数,plt.plot(x, y, format_string, **kwargs)。x:X轴数据,列表或数组。y:Y轴数据,列表或数组 。format_string:
控制曲线的格式字符串。
xticks()函数设置X轴方法--刻度、标签 ,一个是刻标(locs),一个是刻度标签(tick labels)。xticks(ticks, [labels], **kwargs)。参数说明 : ticks:数组类型,用于设置X轴刻度间隔 。[labels]:数组类型,用于设置每个间隔的显示标签 。**kwargs:用于设置标签字体倾斜度和颜色等外观属性。
plt.legend()的几种用法 ,设置图列位置 ,设置图例边框及背景,设置图例标题 ,设置图例名字及对应关系 。详细用法如下:
plt.legend()的几种用法_没有梦想的研究生的博客-CSDN博客_plt.legend
from matplotlib import pyplot as plt
import random
#3 创建另一个曲线的数据
x = range(60)
y = [random.uniform(15,20)for i in x]
y_another = [random.uniform(5,10)for i in x]
#输入两个曲线的信息
plt.figure( figsize=(12,8), dpi=80 )
plt.plot(x, y, color='r', linestyle='--', label = 'ShangHai')
plt.plot(x, y_another, color='g', linestyle='-.', label = 'BeiJing')
#显示图例
plt.legend() #默认loc=Best
#设置刻度及步长
z = range(40)
x_label = ['11:{}'.format(i) for i in x]
plt.xticks( x[::5], x_label[::5])#刻度0-60,间隔为5,刻度标签0-60,间隔为5
plt.yticks(z[::5]) #5是步长,#刻度0-40,间隔为5
#添加网格信息
plt.grid(True, linestyle='--', alpha=0.5) #默认是True,风格设置为虚线,alpha为透明度
#添加标题
plt.xlabel('Time')
plt.ylabel('Temperature')
plt.title('Curve of Temperature Change with Time')
plt.show()