目录
一、网络模型
1.1 torch.nn.Conv2d() 二维卷积
1.1.1参数
1.1.2 输入输出
1.2 torch.nn.ReLU() 激活函数
1.3 torch.nn.MaxPool2d() 最大池化层
1.3.1参数
1.3.2 输入输出
1.4 torch.nn.Flatten() & torch.nn.Dropout()
1.4.1 Flatten参数
1.4.2 Dropout参数
1.5 torch.nn.Linear() 线性全连接层
1.5.1 参数
1.6 torchsummary
二、数据集预处理
2.1 拆分训练集和测试集
2.2 图片预处理
2.2.1 transforms
2.2.2 转为数据集
三、训练和测试
(来自网络)
网络模型如上图所示 ,共包括5个卷积网络层+3个全连接层;输入的数据是(3,224,224)的图像数据,3是通道数(RGB),224×224为宽高。因为没用gpu,所以网络模型图中上下两部分的通道数是加在一起的,每层卷积网络的(in_channels,out_channels)分别是(3,96)->(96,256)->(356,384)->(384,384)->(384,256)。
构建网络的代码(注释了每层输入输出的维度,想看得直观的话可以用1.6里的torchsummary):
import torch
from torch import nn
class alexnet(nn.Module):
def __init__(self) -> None:
super(alexnet,self).__init__() #都是标准写法
self.nets = nn.Sequential( #构建网络
#第一层(batch_size,3,224,224)->(b,96,55,55) [227+2x2-(11-1)-1]/4+1=55
nn.Conv2d(3,96,kernel_size=11,stride=4,padding=2),
nn.ReLU(inplace=True),
#第一个maxpooling (b,96,55,55)->(b,96,27,27) [55-(3-1)-1]/2+1=27
nn.MaxPool2d(kernel_size=3,stride=2),
#第二层(b,96,27,27)->(b,256,27,27)
nn.Conv2d(96,256,5,padding=2),
nn.ReLU(inplace=True),
#第二个maxpooling (b,256,27,27)->(b,256,13,13)
nn.MaxPool2d(3,2),
#第三层(b,256,13,13)->(b,384,13,13)
nn.Conv2d(256,384,3,padding=1),
nn.ReLU(inplace=True),
#第四层输出维度不变
nn.Conv2d(384,384,3,padding=1),
nn.ReLU(inplace=True),
#第五层(b,384,13,13)->(b,256,13,13)
nn.Conv2d(384,256,3,padding=1),
nn.ReLU(inplace=True),
#第三个maxpooling(b,256,13,13)->(b,256,6,6)
nn.MaxPool2d(3,2),
#展平(b,256,6,6)->(b,256*6*6)
nn.Flatten(),
nn.Dropout(p=0.5),
#第一个全连接层(b,9216)->(b,4096)
nn.Linear(9216,4096),
nn.ReLU(inplace=True),
nn.Dropout(p=0.5),
#第二个FC (b,4096)->(b,2048)
nn.Linear(4096,2048),
nn.ReLU(inplace=True),
#第三个FC,这里是2分类,就(b,2048)->(b,2)
nn.Linear(2048,2)
)
def forward(self,input_x):
x = self.nets(input_x)
return x
torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)
以AlexNet第一层卷积层为例,
参数(=第一层网络的取值) | 类型 | 解释 |
in_channels = 3 | int | 输入图像矩阵通道数,这里是三通道RGB |
out_channel = 96 | int | 输出图像矩阵通道数,本层就是输入3通道输出96通道 |
kernel_size = 11 | int or tuple | 卷积核的大小,用于和数据进行二维卷积的卷积核维度,如果是方阵可以直接输入维度int,不是就写作(w,h) |
stride=4 |
(int or tuple, optional) | 步长,就是卷积一次后,往某个方向挪几步进行下一次卷积,默认stride=1 |
padding=2 | (int or tuple, optional) | 填补,在某一维度头尾各填补P个0,在本层中把227x227的图像填补到了231x231;输入(w,h)的话就是在左右两侧各补w列0,在上下各补h行0。默认padding=0 |
padding_mode | (string, optional) |
填补方式,默认是补0,可选‘zeros’, ‘reflect’镜像(以边缘点为中心), ‘replicate’复制边缘点的值 or ‘circular’循环每行/列 |
dilation | (int or tuple, optional) | 扩张,即卷积核里面点之间的间距,默认是1,以dilation=3为例,起点为(0,0),进行卷积核的点就是(0,0)(3,0)(6,0)(9,0)(3,3)(6,3)(9,3)....就是卷积核从田变成了㗊0.0 |
groups | (int, optional) | 分组,默认是1,把Nin个input features分为G组,每组内Nin/G个features与卷积核卷积输出Nout/G个output features,再把G个组的输出作为最终的输出 |
bias | (bool, optional) | 偏差,默认True,在输出添加一个可学习的偏置量 |
torch.nn.Conv2d()要输入一个四维的张量input(N,C,H,W),对卷积神经网络的输入样本就是(batch_size, channels,height,weight),其中channels = 3, height = width =224
输出的张量维度与输入维度的关系是
所以对(N,3,224,224)的输入数据,经过Conv2d(3,96,11,4,2),输出的Hout/Wout是
[224 + 2x2 - (11-1) - 1] / 4 + 1 = 55
激活函数ReLU(),函数比较简单,更详细地资料可以自查~
if input > 0:
return input
else:
return 0
torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False,
ceil_mode=False)
其实和卷积层挺像的,只不过这里输出的是每个窗口范围内的最大值,其余的值全都抛弃。
这样一来池化之后维度变小了, 冗余信息量、计算量、内存占用量云云的都降低了,而且也缓解了对数据特征位置的敏感性(我个人理解就是特征位置如果稍有变化,只要在池化时在同一个窗口里,最后都还是能输出特征的值)。
参数 | 类型 | 解释 |
kernel_size | int or tuple | 最大池化窗口的尺寸,int即为正方形,tuple就是(h,w) |
stride |
(int or tuple, optional) | 步长,池化后挪动的步数,跟跟Conv2d基本一样,默认stride=1 |
padding | (int or tuple, optional) | 填补,补0,跟Conv2d基本一样,默认padding=0 |
dilation | (int or tuple, optional) | 扩张,同Conv2d,默认是1 |
return_indices | (bool, optional) | 返回最大位置索引,默认False |
ceil_mode | (bool, optional) | 启用向上取整,默认False,即默认向下取整 |
同样类似Conv2d,torch.nn.MaxPool2d()输入一个四维的张量input(N,C,H,W),输出数据的维度计算的公式竟然是一模一样的...
torch.nn.Flatten(start_dim=1, end_dim=- 1)
不需要输入参数,就默认把第一个之后的维度都拉平,也就是不动第一个维度,第二个维度的数量是其他维度乘积
torch.nn.Dropout(p=0.5, inplace=False)
按概率p将元素随机置为0,用于防止过拟合。
全连接层的作用摘自其他文章:
全连接层的主要作用就是将前层(卷积、池化等层)计算得到的特征空间映射样本标记空间。简单的说就是将特征表示整合成一个值,其优点在于减少特征位置对于分类结果的影响,提高了整个网络的鲁棒性。
大概就是寻找一个映射,把卷积层中提取出的分布在各通道上的特征,全部映射到我们标记的分类,毕竟卷积网络输出的二维的特征图,最终还是要输出到一维上。
torch.nn.Linear(in_features, out_features, bias=True, device=None, dtype=None)
全连接层就是输入的每一个神经元都参与到每个输出的计算,用torch.nn.Linear()的话就是按照y = xA^T + b的线性关系计算。参数也比较简单,输入数据的size,输出的size,bias为True就是加偏置量,也就是b。
AlexNet里用了3个全连接层,最后一个全连接层输出的size就是分类问题类别的数量。至于为什么要用三层,查了很久大概的说法是层数少了没办法做到很好的把特征归纳分类,特别是第一层后面还跟着个非线性的ReLU(也许之后要看看论文才行
模型分析完啦,最后可以用torchsummary.summary来查看每层的输出尺寸和参数个数
summary(model, input_size: Any, batch_size: int = -1, device: str = "cuda")
from torchsummary import summary
net = alexnet()
print("Input Shape: [-1, 3, 224, 224]")
print(summary(net,(3,224,224),-1,device='cpu'))
Input Shape: [-1, 3, 224, 224]
----------------------------------------------------------------------------------------------------------------
Layer (type) Output Shape Param #
================================================================
Conv2d-1 [-1, 96, 55, 55] 34,944
ReLU-2 [-1, 96, 55, 55] 0
MaxPool2d-3 [-1, 96, 27, 27] 0
Conv2d-4 [-1, 256, 27, 27] 614,656
ReLU-5 [-1, 256, 27, 27] 0
Conv2d-9 [-1, 384, 13, 13] 1,327,488
ReLU-10 [-1, 384, 13, 13] 0
Conv2d-11 [-1, 256, 13, 13] 884,992
ReLU-12 [-1, 256, 13, 13] 0
MaxPool2d-13 [-1, 256, 6, 6] 0
Flatten-14 [-1, 9216] 0
Dropout-15 [-1, 9216] 0
Linear-16 [-1, 4096] 37,752,832
ReLU-17 [-1, 4096] 0
Dropout-18 [-1, 4096] 0
Linear-19 [-1, 2048] 8,390,656
ReLU-20 [-1, 2048] 0
Linear-21 [-1, 2] 4,098
================================================================
Total params: 49,894,786
Trainable params: 49,894,786
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.57
Forward/backward pass size (MB): 11.12
Params size (MB): 190.33
Estimated Total Size (MB): 202.03
----------------------------------------------------------------
这里假设拿到的数据文件夹里面有2个子文件夹代表2个分类,里面有各自分类的数据图片。以我做的这个简单数据为例,文件夹名..\\RMB,里面两个子文件夹1和100分别装着1块钱和100块的RMB图像。
接下来按照8/2的比例把他们分成训练集和测试集,并保存在.\\RMB_split数据下面。
import os
import random
import shutil
ROOT = os.getcwd()+'\\RMB'
for root,dirs,files in os.walk(ROOT): #root根地址,dirs子文件夹,files子文件
for d in dirs:
#d应是包含子文件夹名的list,按举例是1和100,即两个label
d_full = os.path.join(ROOT,d)
img_names = os.listdir(d_full)
img_num = len(img_names)
random.shuffle(img_names) #打乱顺序
for index,img in enumerate(img_names):
img_path = os.path.join(d_full,img) #原图片的路径
label = d
if index < img_num*0.8: #80%训练集
#RMB_split是拆分后的文件夹,里面包含train和test
#train和test各自包含两个文件夹1和100
path_split = os.getcwd()+'\\RMB_split\\train\\'+label
else:
path_split = os.getcwd()+'\\RMB_split\\test\\'+label
os.makedirs(path_split,exist_ok=True) #新建子文件夹
shutil.copy(img_path,path_split) #复制到新文件夹里
仿照别人的做法= =,对训练集和测试集作变换:
from torchvision import transforms
data_transforms = {
# transforms.Compose()里要用列表
'train': transforms.Compose([
#随机裁剪面积为原图scale范围内倍数的图片,宽高比为ratio,尺寸为size
transforms.RandomResizedCrop(size=224,scale=(0.9,1),ratio=(1.9/1,2.1/1)),
#随机灰化
#transforms.RandomGrayscale(p=0.5),
#随机水平翻转
transforms.RandomHorizontalFlip(p=0.5),
#将尺寸为(H,W,C)且取值为[0, 255]的PIL图片
#或者数据类型为np.uint8的NumPy数组转换为
#尺寸为(C,H,W)、数据类型为torch.float32、取值范围在[0.0, 1.0]之间的Tensor
transforms.ToTensor(),
#标准化
transforms.Normalize(mean = [0.5,0.5,0.5],std = [0.5,0.5,0.5])
]),
'test':transforms.Compose([
#测试就简单处理啦,改变尺寸,标准化
transforms.Resize((224,224)),
transforms.ToTensor(),
transforms.Normalize([0.5,0.5,0.5],[0.5,0.5,0.5])])
}
看看处理之后长什么样的:
显示图片的用的代码是
from torchvision.transforms import ToPILImage
for data in train_data:
img,label = data
img1 = img[0] #选一张看
img1 = ToPILImage()(img1)
img1.show()
break #不然会看完所有的训练集
from torchvision.utils.data import DataLoader
from torchvision.datasets import ImageFolder
train_dataset = ImageFolder(root = './RMB_split/train',transform=data_transforms['train'])
train_data = DataLoader(dataset=train_dataset,batch_size=32,shuffle=True)
test_dataset = ImageFolder(root = './RMB_split/test',transform=data_transforms['test'])
test_data = DataLoader(dataset=test_dataset)
ImageFolder对输入的root里的子文件夹进行读取,并进行transform的变换。对输入数据要求的格式是子文件夹名为分类,例如:
'root/cat/...', 'root/dog/...', 'root/jojo/...', ......
DataLoader就是把数据转化为iterator啦,batch_size就是对应网络输入数据的第0维N,一次训练将输入batch_size张图片,这里还用shuffle打乱了顺序。输出里面的每个元素包含变换后的img和label。
我把网络模型中的代码单独保存为MyAlexNet.py,在使用中需要调用。
训练和测试的程序如下(基本上是参考这一篇中代码,稍作修改适合自己的数据 ):对于pytorch中nn.CrossEntropyLoss()与nn.BCELoss()的理解和使用_东城西阙的博客-CSDN博客在pytorch中nn.CrossEntropyLoss()为交叉熵损失函数,用于解决多分类问题,也可用于解决二分类问题。nn.BCELoss()为二元交叉熵损失函数,只能解决二分类问题。https://blog.csdn.net/qq_35605081/article/details/108368276
import torch
import torch.nn as nn
from MyAlexNet import alexnet
import torchvision
from torch.utils.data import DataLoader
from torchvision import transforms
#图片预处理部分
data_transforms = {
'train': transforms.Compose([
transforms.RandomResizedCrop(size=224,scale=(0.9,1),ratio=(1.9/1,2.1/1)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.ToTensor(),
transforms.Normalize(mean = [0.5,0.5,0.5],std = [0.5,0.5,0.5])
]),
'test':transforms.Compose([
transforms.Resize((224,224)),
transforms.ToTensor(),
transforms.Normalize([0.5,0.5,0.5],[0.5,0.5,0.5])])
}
train_dataset = ImageFolder(root ='./RMB_split/train',transform=data_transforms['train'])
train_data = DataLoader(dataset=train_dataset,batch_size=32,shuffle=True)
test_dataset = ImageFolder(root = './RMB_split/test',transform=data_transforms['test'])
test_data = DataLoader(dataset=test_dataset)
#训练
def trainAlexNet(epoch=10):
model = alexnet()
l_r = 0.0001
optimizer = torch.optim.Adam(model.parameters(),lr=l_r)
lossfn = nn.CrossEntropyLoss()
for i in range(epoch):
train_loss = 0
train_num = 0
train_acc = 0.0
model.train() #训练
train_bar = tqdm(train_dataset) #用于显示进度条,train_bar还是个iterable
for step, data in enumerate(train_bar):
img,target = data
optimizer.zero_grad() #梯度清零
out = model(img) #out维度(batch_size,classes)
lossv = lossfn(out,target) #损失函数
out = torch.argmax(out,1) #输出最大的index
lossv.backward() #反向传播
optimizer.step() #梯度下降
train_loss += abs(lossv.item())*img.size(0)
acc = torch.sum(out == target) #一次训练中,判断结果正确个数
train_acc += acc
train_num += img.size(0)
print("epoch:{} , train-Loss:{},train-accuracy:{}".format(i+1,
train_loss/train_num , train_acc/train_num))
torch.save(model,'AlexNetForRMB.m')
#测试
def testAlexNet(test_data):
model = torch.load('AlexNetForRMB.m')
test_loss = 0
test_acc = 0.0
test_num = 0
lossfn = torch.nn.CrossEntropyLoss()
model.eval() #测试
with torch.no_grad(): #清除梯度
test_bar = tqdm(testset)
for _,data in enumerate(test_bar):
img,label = data
out = model(img)
lossv = lossfn(out,label)
test_loss += abs(lossv.item())*img.size(0)
rs = torch.argmax(out,dim = 1) #哪一个值最大,他的序号对应的就是类别
acc = torch.sum(rs == label)
test_acc += acc
test_num += img.size(0)
#test_loss_all.append(test_loss/test_num)
#test_acc_all.append(test_acc.double().item()/test_num)
print('test-loss:{:.6f}, test-accuracy:
{:.4%}'.format(test_loss/test_num,test_acc/test_num))
trainAlexNet(epoch=10)
model_test(test_data)