卷积神经网络CNN是我们深度学习过程中 的入门网络模型。它在视觉识别任务上的表现很好。一个复杂的CNN网络是带有上百万参数和许多隐含层的。
对于CNN模型,开始训练时我们可以使用一个很大的数据集如ImageNet(CNN模型的两个特点:神经元间的权重共享和卷积层之间的稀疏连接。大部分的CNN模型都需要很大的内存和计算量,特别是在训练过程。要想尽可能的训练精度高,就要在计算量上增加。
AlexNet,VGG,Inception和ResNet是一些流行的CNN网络。接下来我们先初步了解一下VGG。
下面这张图是VGG的网络结构图
VGG模型的特点是全部采用33的卷积核和22的池化,
通过将卷积层数量加大,使深度更深。这样采用连续的几个3x3的卷积核代替AlexNet中的较大卷积核(11x11,5x5)。如采用2个33的卷积核来代替1个55的较大卷积核,3个33的卷积核来替代1个1111的卷积核。采用堆积的小卷积核是优于采用大的卷积核,因为多层非线性层可以增加网络深度来保证学习更复杂的模式,同时参数还更少,计算代价也小。
从这张图中我们可以看到,VGG卷积层之间使用了一种块结构block来连接,每一个block之间卷积结构相同。都是多次重复使用3*3的卷积核来提取特征,只不过通道数成倍的增加(从64-128-256-512),然后每经过一个下采样或者池化层特征图大小成倍地减小,VGG卷积层之后是3个全连接层。kVGG之后一个明显的趋势是采用这种 模块结构,这是一种很好的设计典范,采用模块化结构可以减少我们网络的设计空间,另外一个点是模块里面使用瓶颈层可以降低计算量。
代码实现
下面是采用VGG16模型实现自定义数据集图像分类(宝可梦图片分类)
-
项目结构目录如下:
-
model.py
import torch.nn as nn
class VGG(nn.Module):
def __init__(self):
super(VGG, self).__init__()
# class torch.nn.Conv1d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)
# in_channels(int) – 输入信号的通道。在文本分类中,即为词向量的维度
# out_channels(int) – 卷积产生的通道。有多少个out_channels,就需要多少个1维卷积
# kerner_size(int or tuple) - 卷积核的尺寸,卷积核的大小为(k, ),第二个维度是由in_channels来决定的,所以实际上卷积大小为kerner_size * in_channels
# stride- 卷积步长
# padding- 输入的每一条边补充0的层数
# dilation– 卷积核元素之间的间距
# groups(int, optional) – 从输入通道到输出通道的阻塞连接数
# bias(bool, optional) - 如果bias = True,添加偏置
#block1
self.conv1 = nn.Sequential( # 利用Sequential 迅速搭建
nn.Conv2d(3, 64, 3, 1, 1),
# nn.ReLU(True),
nn.Conv2d(64, 64, 3, 1, 1), #224*224*64
# nn.ReLU(True),
)
#block2
self.conv2 = nn.Sequential(
nn.MaxPool2d(2, 2),
nn.Conv2d(64, 128, 3, 1, 1),
nn.Conv2d(128, 128, 3, 1, 1),
# nn.ReLU(True),
#128*112*112
)
#block3
self.conv3 = nn.Sequential(
nn.MaxPool2d(2, 2),
nn.Conv2d(128, 256, 3, 1, 1),
nn.Conv2d(256, 256, 3, 1, 1),
nn.Conv2d(256, 256, 3, 1, 1), #256*56*56
# nn.ReLU(True),
)
#block4
self.conv4 = nn.Sequential(
nn.MaxPool2d(2, 2),
nn.Conv2d(256, 512, 3, 1, 1),
nn.Conv2d(512, 512, 3, 1, 1),
nn.Conv2d(512, 512, 3, 1, 1), #512*28*28
)
#block5
self.conv5 = nn.Sequential(
nn.MaxPool2d(2, 2),
nn.Conv2d(512, 512, 3, 1, 1),
nn.Conv2d(512, 512, 3, 1, 1),
nn.Conv2d(512, 512, 3, 1, 1), #512*14*14
# nn.ReLU(True),
nn.MaxPool2d(2, 2) #512*7*7
)
self.classifier = nn.Sequential( #3个全连接层
nn.Linear(512 * 7 * 7, 4096),
nn.ReLU(True),
nn.Dropout(0.5),
nn.Linear(4096, 4096),
nn.ReLU(True),
nn.Dropout(0.5),
nn.Linear(4096, 5)
)
def forward(self, x):
x = self.conv1(x) # 3*224*224(输入)--->64*224*224 输出图像尺寸=(输入-卷积核大小+2*padding)/步长+1
x = self.conv2(x) # 64*224*224(输入)-->64*112*112--->128*112*112
x = self.conv3(x) # 128*112*112(输入)--->128*56*56--->256*56*56---> 256 *56*56 如果stride=1,padding=(kernel_size-1)/2,则图像卷积后大小不变
x = self.conv4(x) # 256*56*56(输入)--->256*28*28(池化)--->512*28*28--->512*28*28--->512*28*28
x = self.conv5(x) # 512*28*28(输入)--池化--->512*14*14--->512*14*14--->512*14*14--->512*7*7 (池化)
# print(x.shape) 这里可以打印测试一些shape 以面后面的维度出错
x = x.view(x.size(0), 512*7*7) # 将多维度的Tensor展平成一维,才放入全连接层
x = self.classifier(x)
# x = nn.Softmax(dim=1)
return x
- trian.py
import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as Data # 数据集的抽象类
import torchvision
from torchvision.datasets import ImageFolder
from torch.utils.data import Dataset
import torchvision.transforms as transforms
import warnings
from PIL import Image
import random
from model import VGG # 从自定义的modle.py文件中导入网络模型
from Visualize import plot_with_labels # 从自定义的Visualizee.py文件中导入函数
# GPU or CPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 利用过滤器来忽略warnings
warnings.filterwarnings('ignore')
# 超参数设置
EPOCH = 10 # 一共训练10次
best_acc = 0.75
BATCH_SIZE = 21 # 21个图片为一个batch 一共1050张图片 共50个batch
LR = 0.01 # learning rate
Data_PATH = 'data/pokemon' # 图像文件路径
test_num = 150 # 测试集的数量
# 加载数据集
# 数据增强。对训练集进行预处理
transform_train = transforms.Compose([
transforms.Resize(256),
# transforms.RandomResizedCrop(224),
transforms.CenterCrop(224), # 先四周填充 将图像居中裁剪成224*224
transforms.RandomHorizontalFlip(), # 随机水平翻转 默认概率0.5
transforms.ToTensor(), # C H W格式 [0,1】数据范围
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # 用给定的均值和标准差对每个通道的数据进行正则化
]) # Normalized_image=(image-mean)/std
# 对测试集进行数据处理
transform_test = transforms.Compose([
transforms.Resize(256), # 数据集图像大小不一,加载测试集时也要进行了Resize and Crop ,否则会报错
# transforms.RandomResizedCrop(224),
transforms.CenterCrop(224), # 先四周填充 将图像居中裁剪成224*224
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
# 用Dataset自带的方法ImageFolder对分好类的文件夹进行读取 作为训练集
train_set = torchvision.datasets.ImageFolder(root=Data_PATH, transform=transform_train)
train_loader = torch.utils.data.DataLoader(train_set, batch_size=BATCH_SIZE, shuffle=True, num_workers=2,
pin_memory=True) # 将数据保存在pin_memory中
# 测试集没有分好类 所以不能用ImageFolder直接读取
# testset = torchvision.datasets.ImageFolder(root='data/test', transform=transform_test)
# test_loader = torch.utils.data.DataLoader(testset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2,
# pin_memory=True) # 数据加载器在返回前将张量复制到CUDA固定内存中
# 从pokemon文件夹中提取测试集
dataset = ImageFolder(Data_PATH)
# print(dataset.class_to_idx)
test_data = random.sample(dataset.samples, test_num) # 随机选取150个作为测试集
# print(test_data)
test_inputs = [] # 测试集的输入 ,一个列表 存放图像的路径
test_labels = [] # 测试集的输出,标签,一个列表 存放图像对应的标签(0-4)
for x, y in test_data: # 遍历test_data -->一个元素是元组的列表,每个元组第一个元素是图片的路径,第二个是该图片对应的标签
test_inputs.append(x)
test_labels.append(y)
# 自定义数据读取类 要实现__len__ 和__getitem__方法
class MyDataset(Dataset):
def __init__(self, file_path, labels, transform):
self.file_path = file_path
self.labels = labels
self.transform = transform
def __len__(self):
return len(self.file_path)
def __getitem__(self, idx): # 索引数据集中的某一个数据
image = Image.open(self.file_path[idx]).convert('RGB')
image = self.transform(image)
return image, torch.tensor(self.labels[idx])
test_loader = torch.utils.data.DataLoader(MyDataset(test_inputs, test_labels, transform_test), #先转化成torch能识别的
batch_size=BATCH_SIZE, # dataset 再批处理
shuffle=False, num_workers=2,
pin_memory=True)
# 实例化
net = VGG()
# 定义损失函数和优化方式
loss_func = nn.CrossEntropyLoss() # 损失函数为交叉熵 内置了softmax层
optimizer = optim.SGD(net.parameters(), lr=LR, momentum=0.9, # net.parameters()可迭代的variable指定因优化哪些参数
weight_decay=5e-4) # 优化方式为mini-batch momentum-SGD小批量梯度下降,weight_decay并采用L2正则化(权值衰减)
# 开始训练
if __name__ == '__main__':
with open('model_params.txt', 'w') as f4: # 将模型参数写入model_params.txt文件
for parameters in net.parameters(): # 模块参数的迭代器
f4.write(str(parameters))
f4.write('\n')
for name, parameters in net.named_parameters():
f4.write(name + ':' + str(parameters.size()))
f4.write('\n')
f4.flush()
f4.close()
with open("acc.txt", "w") as f1: # 将测试准确率写入acc.txt文件
with open("log.txt", "w")as f2: # 训练日志
save_train_Acc = []
save_test_Acc = []
save_train_Loss = []
save_test_Loss = []
for epoch in range(EPOCH):
net.train() # 开始训练
train_corr = 0.
total_loss = 0.
for i, data in enumerate(train_loader):
train_input, target = data
# train_input, target = Variable(train_input), Variable(target)
train_input, target = train_input.to(device), target.to(device) # GPU
output = net(train_input)
loss = loss_func(output, target)
total_loss += loss.item() # 总的损失 用item()累加成数字
result = torch.max(output, 1)[1] # 返回概率最大值位置的索引 one-hot 编码---》标签
train_corr += (result == target).sum() # 训练的结果与原来的标签相等的个数
# SGD
optimizer.zero_grad()
loss.backward() # 计算梯度
optimizer.step() # 更新参数
# 每五个batch打印一下
# print(len(train_loader)) # 共50个batch
if i % 5 == 0:
print('Epoch: %d | step: %d | train_loss: %.4f | train_acc: %.2f '
% (epoch, i, loss.item(), float(train_corr) / ((i + 1) * BATCH_SIZE)))
f2.write('Epoch: %d | step: %d | train_loss: %.4f | train_acc: %.2f'
% (epoch, i, loss.item(), float(train_corr) / ((i + 1) * BATCH_SIZE)))
f2.write('\n')
f2.flush()
# 训练完一个epoch 就打印总的损失值 准确率
print('Epoch: %d | iteration: %d | total_loss: %.4f | train_acc: %.2f'
% (epoch, i, total_loss, float(train_corr) / len(train_set)))
f2.write('Epoch: %d | step: %d | total_loss: %.4f | train_acc: %.2f'
% (epoch, i, total_loss, float(train_corr) / len(train_set)))
f2.write('\n')
f2.flush()
save_train_Acc.append(round(float(train_corr) / len(train_set), 2))
save_train_Loss.append(round(float(total_loss) / len(train_set), 2))
with torch.no_grad(): # 显示地取消模型变量的梯度 测试时不要在梯度更新
net.eval() # 开始测试
print("Starting testing!")
test_loss = 0.
correct = 0.
accuracy = 0.
for i, data in enumerate(test_loader):
test_x, target = data
# test_x, target = Variable(test_x), Variable(target)
test_x, target = test_x.to(device), target.to(device)
output = net(test_x)
_, pred = torch.max(output.data, 1) # 或者pred =output.argmax(dim=1)
# 计算准确率
loss = loss_func(output, target)
test_loss += loss.item()
correct += pred.eq(target.data).sum()
test_loss /= len(test_inputs)
accuracy = float(correct) / float(len(test_inputs)) # 注意如果不加float accuracy 为0.00 !!!
save_test_Acc.append(round(accuracy, 2))
save_test_Loss.append(round(test_loss, 2))
torch.save(net.state_dict(), 'params.pkl') # 仅保存和加载模型参数
# 打印 总的损失值 正确的个数 和测试准确率 并写入到acc.txt文件中去
print('Epoch: ', epoch, '| test_loss: %.4f' % test_loss, '| correct: %d' % correct,
'| test accuracy: %.3f' % accuracy)
f1.write('Epoch: %d | test_loss: %.4f | correct: %d | test accuracy: %.3f'
% (epoch, test_loss, correct, accuracy))
f1.write('\n')
f1.flush() # 将缓冲区写入
# plot_with_labels(save_train_Loss, save_train_Acc, save_test_Loss, save_test_Acc)
if accuracy > best_acc:
f3 = open("best_acc.txt", "w")
f3.write("EPOCH=%d,best_acc= %.3f" % (epoch + 1, accuracy))
f3.close()
best_acc = accuracy
print("Congratulating! Training Finished, TotalEPOCH=%d" % EPOCH)
plot_with_labels(save_train_Loss, save_train_Acc, save_test_Loss, save_test_Acc)
- visualize.py 画一个简单的折线图来看一下训练和测试的精度
import matplotlib.pyplot as plt
import numpy as np
def plot_with_labels(save_train_loss, save_train_acc, save_test_loss, save_test_acc):
X1 = np.arange(len(save_train_loss))
Y1 = np.array(save_train_loss)
Y2 = np.array(save_train_acc)
Y3 = np.array(save_test_loss)
Y4 = np.array(save_test_acc)
# 生成图形
plt.figure('Visualize loss and acc')
# 第一张子图 训练效果图
plt.subplot(1, 2, 1)
plt.plot(X1, Y1, 'b^-', label='train_loss_rate', linewidth=2) # 颜色蓝色,点形三角形,线性实线,设置图例显示内容,线条宽度为2
plt.plot(X1, Y2, 'ro-', label='train_acc_rate', linewidth=2) # 颜色红色,点形圆形,线性虚线,设置图例显示内容左上角,线条宽度为2
plt.xlabel('Epochs')
plt.ylabel('Train Loss / Acc')
plt.legend(loc='upper left') # 左上角显示图例
plt.xticks(np.arange(0, len(save_train_loss), 1)) # 设置横坐标轴的刻度为 0 到 10 的数组
plt.ylim([0, 1]) # 设置纵坐标轴范围为 0 到 1
plt.grid() # 显示网格
plt.title('train') # 图形的标题
# 第二张子图 测试图
plt.subplot(1, 2, 2)
plt.title('test')
plt.xlabel('Epochs')
plt.ylabel('Test Loss / Acc')
plt.xticks(np.arange(0, len(save_test_loss), 1)) # 设置横坐标轴的刻度为 0 到 10 的数组
plt.ylim([0, 1]) # 设置纵坐标轴范围为 0到 1
plt.plot(X1, Y3, 'b^-', label='test_loss_rate', linewidth=2) # 颜色蓝色,点形三角形,线性实线,设置图例显示内容,线条宽度为2
plt.plot(X1, Y4, 'ro-', label='test_acc_rate', linewidth=2)
plt.legend(loc='upper left')
plt.grid() # 显示网格
plt.savefig("temp.png", dpi=500, bbox_inches='tight')
plt.show()
- 训练结果:
(因为时间有限 这里只运行了一会儿)
当然这里还有关于VGG模型的很多地方我还没有搞明白,为什么使用了Multi-Scale的方法做数据增强 而且有的结构中还采用了1*1的卷积核,还有初始化参数,训练过程中使用的mini-batch的梯度下降法等等,还需要深入其中。