直播笔记:CNN原理与进阶技巧
神经网络是由具有适应性的简单单元组成的广泛并行互连的网络,他的组织能够模拟生物神经系统对真实世界物体所作出的交互反应。
训练过程
反向传播(BP, back propagation)
基本思想:
(1)计算每一层的状态和激活值,直到最后一层(即信号是前向传播的);
(2)计算每一层的误差,误差的计算过程是从最后一层向前推进的(即误差是反向传播的);
(3)计算每个神经元连接权重的梯度;
(4)根据梯度下降法则更新参数(目标是误差变小)。
迭代以上步骤,直到满足停止准则(比如相邻两次迭代的误差的差别很小)。
公式详解
所有训练数据的总体代价:
目标是调整权重和偏置使得总体代价(误差)变小,求得总体代价取最小值时对应各个神经元的参数(即权重和偏置项)。 由梯度下降优化算法,有:
参考:深度学习 | 反向传播详解 - 知乎 (zhihu.com)
梯度下降
寻找目标函数最小化。利用梯度信息,通过不断迭代调整参数来寻找合适的目标值。
基本概念
步长(Learning rate):每一步梯度下降时向目标方向前行的长度
损失函数(loss function): 为了评估模型的好坏,通常用损失函数来度量拟合的程度。损失函数最小化,意味着拟合程度最好,对应的模型参数即为最优参数。
面临的问题
局部最小值和鞍点。
局部最小值
这是梯度下降法最常遇到的一个问题,当一个函数存在多个局部最小值,很可能梯度下降法只是找到其中的一个局部最小值而停止。
规避局部最小值:可以多次用不同的初始值执行算法,选择损失函数最小的初始值。
鞍点
数学含义是:目标函数在此点的梯度为0,但从该点出发的一个方向存在函数极大值点,而另一个方向是函数的极小值点。
常见梯度下降法
批量梯度下降(Batch Gradient Descent BGD)
上面所介绍的算法其实就是批量梯度下降。需要首先计算所有数据上的损失值,然后再进行梯度下降,具体的操作步骤是:遍历全部数据集算一次损失函数,然后算函数对各个参数的梯度,更新梯度。这种方法每更新一次参数,都要把数据集里的所有样本计算一遍,计算量大,计算速度慢,不支持在线学习。
随机梯度下降(Stochastic Gradient Descent SGD)
不使用全量的样本来计算梯度,而使用单一样本来近似估计梯度,可以极大地减少计算量,提高计算效率。具体的操作步骤是:每次从训练集中随机选择一个样本,计算其对应的损失和梯度,进行参数更新,反复迭代。
这种方式在数据规模比较大时可以减少计算复杂度,从概率意义上来说的单个样本的梯度是对整个数据集合梯度的无偏估计,但是它存在着一定的不确定性,因此收敛速率比批梯度下降得更慢。
小批量梯度下降(Mini-batch Gradient Descent)
为了克服上面两种方法的缺点,采用的一种折中手段:将数据分为若干批次,按批次更新参数,每一批次中的一组数据共同决定了本次梯度的方向,下降起来就不容易跑偏,减少了随机性,另一方面,因为批的样本数比整个数据集少了很多,计算量也不是很大。
CNN讲解
全连接神经网络处理大尺寸图像具有三个明显的缺点:
(1)首先将图像展开为向量会丢失空间信息
(2)其次参数过多效率低下,训练困难
(3)同时大量的参数也很快会导致网络过拟合
由此推出卷积神经网络。
结构:
卷积神经网络的各层中的神经元是3维排列的:宽度、高度和深度。
卷积是一个二维模板,但是在卷积神经网络中的深度指的是激活数据体的第三个维度,而不是整个网络的深度,整个网络的深度指的是网络的层数。
组成:
输入层
卷积层——核心
卷积层的参数是有一些可学习的滤波器集合构成的。每个滤波器在空间上(宽度和高度)都比较小,但是深度和输入数据一致。
可以被看做是神经元的一个输出。神经元只观察输入数据中的一小部分,并且和空间上左右两边的所有神经元共享参数
降低参数的数量。这个由于卷积具有“权值共享”这样的特性,可以降低参数数量,达到降低计算开销,防止由于参数过多而造成过拟合
感受野
ReLU层
池化(Pooling)层
全连接层
CNN赛题中的应用:三维卷积
代码解读
# step1 自定义数据集
import os, sys, glob, argparse
import pandas as pd
import numpy as np
from tqdm import tqdm
import cv2
from PIL import Image
from sklearn.model_selection import train_test_split, StratifiedKFold, KFold
import torch
torch.manual_seed(0)
torch.backends.cudnn.deterministic = False
torch.backends.cudnn.benchmark = True
import torchvision.models as models
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.autograd import Variable
from torch.utils.data.dataset import Dataset
import nibabel as nib
from nibabel.viewers import OrthoSlicer3D
train_path = glob.glob('./data/Train/*/*')
test_path = glob.glob('./data/Test/*')
np.random.shuffle(train_path)
np.random.shuffle(test_path)
DATA_CACHE = {}
class XunFeiDataset(Dataset):
def __init__(self, img_path, transform=None):
self.img_path = img_path
if transform is not None:
self.transform = transform
else:
self.transform = None
def __getitem__(self, index):
if self.img_path[index] in DATA_CACHE:
img = DATA_CACHE[self.img_path[index]]
else:
img = nib.load(self.img_path[index])
img = img.dataobj[:, :, :, 0]
DATA_CACHE[self.img_path[index]] = img
# 随机选择一些通道
idx = np.random.choice(range(img.shape[-1]), 50)
img = img[:, :, idx]
img = img.astype(np.float32)
if self.transform is not None:
img = self.transform(image=img)['image']
img = img.transpose([2, 0, 1])
return img, torch.from_numpy(np.array(int('NC' in self.img_path[index])))
def __len__(self):
return len(self.img_path)
import albumentations as A
train_loader = torch.utils.data.DataLoader(
XunFeiDataset(train_path[:-10],
A.Compose([
A.RandomRotate90(),
A.RandomCrop(120, 120),
A.HorizontalFlip(p=0.5),
A.RandomContrast(p=0.5),
A.RandomBrightnessContrast(p=0.5),
])
), batch_size=2, shuffle=True, num_workers=0, pin_memory=False
)
val_loader = torch.utils.data.DataLoader(
XunFeiDataset(train_path[-10:],
A.Compose([
A.RandomCrop(120, 120),
])
), batch_size=2, shuffle=False, num_workers=0, pin_memory=False
)
test_loader = torch.utils.data.DataLoader(
XunFeiDataset(test_path,
A.Compose([
A.RandomCrop(128, 128),
A.HorizontalFlip(p=0.5),
A.RandomContrast(p=0.5),
])
), batch_size=2, shuffle=False, num_workers=0, pin_memory=False
)
# step2 自定义CNN模型
class XunFeiNet(nn.Module):
def __init__(self):
super(XunFeiNet, self).__init__()
model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)
model.conv1 = torch.nn.Conv2d(50, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
model.avgpool = nn.AdaptiveAvgPool2d(1)
model.fc = nn.Linear(512, 2)
self.resnet = model
def forward(self, img):
out = self.resnet(img)
return out
model = XunFeiNet()
model = model.to('cuda')
criterion = nn.CrossEntropyLoss().cuda()
optimizer = torch.optim.AdamW(model.parameters(), 0.001)
# step3 模型训练与验证
def train(train_loader, model, criterion, optimizer):
model.train()
train_loss = 0.0
for i, (input, target) in enumerate(train_loader):
input = input.cuda(non_blocking=True)
target = target.cuda(non_blocking=True)
output = model(input)
loss = criterion(output, target.long())
optimizer.zero_grad()
loss.backward()
optimizer.step()
if i % 20 == 0:
print(loss.item())
train_loss += loss.item()
return train_loss / len(train_loader)
def validate(val_loader, model, criterion):
model.eval()
val_acc = 0.0
with torch.no_grad():
for i, (input, target) in enumerate(val_loader):
input = input.cuda()
target = target.cuda()
# compute output
output = model(input)
loss = criterion(output, target.long())
val_acc += (output.argmax(1) == target).sum().item()
return val_acc / len(val_loader.dataset)
for _ in range(3):
train_loss = train(train_loader, model, criterion, optimizer)
val_acc = validate(val_loader, model, criterion)
train_acc = validate(train_loader, model, criterion)
print(train_loss, train_acc, val_acc)
# step4 模型预测与提交
def predict(test_loader, model, criterion):
model.eval()
val_acc = 0.0
test_pred = []
with torch.no_grad():
for i, (input, target) in enumerate(test_loader):
input = input.cuda()
target = target.cuda()
output = model(input)
test_pred.append(output.data.cpu().numpy())
return np.vstack(test_pred)
pred = None
for _ in range(10):
if pred is None:
pred = predict(test_loader, model, criterion)
else:
pred += predict(test_loader, model, criterion)
submit = pd.DataFrame(
{
'uuid': [int(x.split('\\')[-1][:-4]) for x in test_path],
'label': pred.argmax(1)
})
submit['label'] = submit['label'].map({1: 'NC', 0: 'MCI'})
submit = submit.sort_values(by='uuid')
submit.to_csv('submit2.csv', index=None)
导包
tqdm库: 显示循环的进度条的库
两种运行模式
1、基于迭代对象运行 tqdm(iterator)
2、手动进行更新
PIL库:Python Imaging Library 图像处理库
针对python3的版本 pillow
torch库: 基于 Python 的科学计算库
Tensors (张量)相当于Numpy的多维数组,可以应用到 GPU 上加快计算速度
Albumentations : 处理图像
快
VerticalFlip 围绕X轴垂直翻转
HorizontalFlip 围绕Y轴水平翻转
Flip 垂直或水平和垂直翻转
compose() ---- 串联多个图片变换
更多及参考:(82条消息) Pytorch使用albumentations实现数据增强_padifneeded_zhangyuexiang123的博客-CSDN博客
数据增强方法:
像素级变换:仅更改输入图像,并且将使所有其他目标(蒙版,边界框和关键点)保持不变
包括:模糊,色彩抖动,图像压缩,高斯噪声,倒置,归一化,随机亮度对比,锐化,色相饱和度值
空间级变换:同时更改输入图像以及其他目标,例如蒙版,边界框和关键点
深度神经网络需要大量的训练数据才能获得良好的结果并防止过拟合。但是,通常很难获得足够的训练样本,主要难点在于收集和标注数据集的成本过高。此时可以通过图像增强来制作新样本。
部分代码详解
torch.manual_seed(0) -------- CPU生成随机数的种子。方便下次复现实验结果。
cuDNN是GPU加速库
在使用GPU的时候,PyTorch会默认使用cuDNN加速,但是,在使用 cuDNN 的时候,torch.backends.cudnn.deterministic模式是为False。设置这个 flag 为True,我们就可以在 PyTorch 中对模型里的卷积层进行预先的优化,也就是在每一个卷积层中测试 cuDNN 提供的所有卷积实现算法,然后选择最快的那个。
torch.backends.cudnn.deterministic 将这个 flag 置为 True 的话,每次返回的卷积算法将是确定的,即默认算法。如果配合上设置 Torch 的随机种子为固定值的话,应该可以保证每次运行网络的时候相同输入的输出是固定的。
设置torch.backends.cudnn.benchmark = True将会让程序在开始时花费一点额外时间,为整个网络的每个卷积层搜索最适合它的卷积实现算法,进而实现网络的加速。
torch.utils.data.DataLoader() 对数据进行batch的划分,可以快速的迭代数据。
参数
batch_size=2 批训练
shuffle=True 要不要打乱数据
num_workers=1 多线程来读数据
pin_memory=False 是否把把数据存放在锁页内存中
ResNet-18
pretrained参数 表示是否载入在ImageNet上预训练的模型。
torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)
in_channels 输入图像通道数
out_channels 卷积产生的通道数
kernel_size 卷积核尺寸
stride 卷积步长
padding 填充操作
bias 为真,则在输出中添加一个可学习的偏差
nn.AdaptiveAvgPool2d(output_size) 自适应平均池化
nn.Linear(in_features, out_features) 设置网络中的全连接层
总结感悟:
通过直播的讲解以及在众多群友的帮助下,完成了CNN代码在windows本地部署。最后提交的csv文件中得到的结果全是NC,不过最终成绩比一开始的baseline高多了。 关于机器学习和深度学习的代码还是需要进一步的研究和学习。