本周主要进行了深度学习中卷积神经网络的学习
学习时间
2024.9.13—2024.9.20
学习内容
一、卷积神经网络知识点学习
二、卷积神经网络案例实践
三、残差网络ResNet
四、ResNet案例实践
主要内容
一、卷积神经网络知识点学习
1、卷积神经网络概述
卷积神经网络(Convolutional Neural Network, CNN)是一类包含卷积计算且具有深度结构的前馈神经网络,是深度学习的典型网络结构之一。它主要用于图像、视频、语音等信号数据的分类和识别任务,并在计算机视觉、自然语言处理、语音识别等多个领域展现出卓越的性能。
2、输入层
输入层是CNN的第一层,负责接收原始数据。对于图像数据,输入层通常是一个二维或三维数组,表示图像的宽度、高度和颜色通道(如RGB)。
通道
输入通道:在卷积神经网络的输入层,通道数通常对应于输入数据的维度。对于图像数据,输入通道数通常与图像的颜色通道数相匹配。例如,RGB图像有三个输入通道,分别对应红色、绿色和蓝色。灰度图像则只有一个输入通道,表示灰度值。
输出通道:在卷积层中,每个卷积核会生成一个新的特征映射(feature map),这些特征映射作为输出特征图的通道。因此,输出通道的数量等于卷积核的数量。这些通道代表了网络从输入数据中提取的不同特征。
3、卷积层
卷积操作是通过一个称为卷积核(或滤波器)的小矩阵在输入数据(如图像)上滑动,并在每个位置执行元素级别的乘法后求和(有时加上一个偏置项),然后通过一个激活函数(如ReLU)来引入非线性,从而生成输出特征图(feature map)。这个过程可以看作是对输入数据的局部特征进行提取和增强的过程。
卷积层通过卷积操作来提取输入数据的特征。
卷积核是执行卷积操作时的核心元素。它是一个小的矩阵,其大小、步长和填充是卷积操作中的关键参数之一。卷积核在输入数据(如图像)上滑动,与输入数据的局部区域进行元素级别的乘法操作,然后将结果相加(可能还加上一个偏置项),得到输出特征图中的一个值。这个过程会在输入数据的整个空间范围内重复进行,从而生成完整的输出特征图。
卷积核的作用在于提取输入数据的局部特征。不同的卷积核可以学习到不同的特征。
通过不同的卷积核学习同一张图片可以得到不同的结果。
填充和步幅
在应用多层卷积时,我们常常丢失边缘像素。 由于我们通常使用小卷积核,因此对于任何单个卷积,我们可能只会丢失几个像素。 但随着我们应用许多连续卷积层,累积丢失的像素数就多了。 解决这个问题的简单方法即为填充(padding):在输入图像的边界填充元素(通常填充元素是0)。
在计算互相关时,卷积窗口从输入张量的左上角开始,向下、向右滑动。 一般默认每次滑动一个元素。 但是,有时候为了高效计算或是缩减采样次数,卷积窗口可以跳过中间位置,每次滑动多个元素。我们将每次滑动元素的数量称为步幅(stride)。
垂直步幅为 3,水平步幅为 2 的二维互相关运算
4、池化层
池化层的主要作用是对输入的特征图(feature map)进行下采样(downsampling),通过减少数据的空间尺寸(宽和高)来降低网络中的参数数量和计算量,同时提取出重要的特征信息,从而降低卷积层对位置的敏感性。
常见池化操作包括:
最大池化层
在输入特征图的每个指定大小的区域内(如2x2、3x3等),选择该区域内的最大值作为输出。这种方式可以保留区域内最显著的特征,常用于捕捉纹理信息。
平均池化层
与最大池化类似,但在每个区域内计算平均值而不是最大值。这种方式有助于保留整体数据的特征,减少因邻域大小受限造成的估计值方差增大,常用于背景信息的保留。
5、全连接层
全连接层通常位于CNN的末端,负责将提取到的特征映射到样本标记空间,实现分类或回归等任务。
由于卷积层和池化层的输出是二维或三维的特征图,而全连接层需要一维的输入向量,因此需要对特征图进行展平操作。这一步骤将二维或三维的特征图转换为一维的特征向量,以便输入到全连接层中。
根据具体的任务需求(如分类、回归等),输出层会对全连接层的输出进行进一步的处理。例如,在分类任务中,输出层可能会使用softmax函数将全连接层的输出转换为类别概率分布;在回归任务中,输出层则可能直接输出预测值。
二、卷积神经网络案例实践
基于卷积神经网络对MNIST数据集进行训练
首先读取数据
分别构建训练集和测试集
DataLoader来迭代取数据(DataLoader可以将数据集分成多个小批次(batch),每次迭代只加载一个批次的数据,这样可以减少内存的使用,同时也有利于利用并行计算加速模型的训练。)
#定义超参
input_size = 28 #图像的总尺寸28*28
num_classes = 10 #标签的种类数
num_epochs = 3 #训练的总循环周期
batch_size = 64 #一个批次的大小(64张图片)
#训练集
train_dataset = datasets.MNIST(root='./data', train=True, transform=transforms.ToTensor(), download=True)
#测试集
test_dataset = datasets.MNIST(root='./data', train=False, transform=transforms.ToTensor())
#构建batch数据
train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=True)
卷积网络模块构建
一般卷积层,relu层,池化层可以写成一个模块
由于卷积最后结果是一个特征图,需要把图转换成向量才能做分类或者回归任务
class CNN(nn.Module):
def __init__(self):
super(CNN, self).__init__()
self.conv1 = nn.Sequential( #输入大小(1,28,28)
nn.Conv2d(1, 16, kernel_size=5, stride=1, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2), #输出大小(16,14,14)
)
self.conv2 = nn.Sequential( #下一层输入大小(16,14,14)
nn.Conv2d(16, 32, kernel_size=5, stride=1, padding=2),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2), #输出大小(32,7,7)
)
self.out = nn.Linear(32 * 7 * 7, num_classes) #全连接层得到的结果
def forward(self, x): #前向传播
x = self.conv1(x)
x = self.conv2(x)
x = x.view(x.size(0), -1) #特征图展开
x = self.out(x)
return x
准确率作为评估标准
def accuracy(predictions, labels):
pred = torch.max(predictions, 1)[1]
rights = pred.eq(labels.view_as(pred)).sum()
return rights, len(labels) #rights是某一批次中预测正确的个数,len为该批次中样本的数量
训练网络模型
#实例化
net = CNN()
#损失函数
criterion = nn.CrossEntropyLoss()
#优化器
optimizer = optim.Adam(net.parameters(), lr=0.001) #定义优化器,普通的随机梯度下降算法
#开始训练循环
for epoch in range(num_epochs):
train_rights = [] #当前epoch结果保存下来
for batch_idx, (data, target) in enumerate(train_loader):
net.train()
output = net(data)
loss = criterion(output, target) #计算损失值
optimizer.zero_grad() #参数梯度清零
loss.backward() #计算损失函数相对于模型参数的梯度
optimizer.step() #根据得到的梯度优化参数
right = accuracy(output, target)
train_rights.append(right)
if batch_idx % 100 == 0:
net.eval()
val_rights = []
for(data, target) in test_loader:
output = net(data)
right = accuracy(output, target)
val_rights.append(right)
#准确率计算
train_r = (sum([tup[0] for tup in train_rights]), sum([tup[1] for tup in train_rights]))
val_r = (sum([tup[0] for tup in val_rights]), sum([tup[1] for tup in val_rights]))
print('训练正确率:' + str(train_r[0] / train_r[1]), '测试正确率:' + str(val_r[0] / val_r[1]))
训练结果
三、残差网络ResNet
在深度学习中,随着网络层数的增加,往往会出现梯度消失或梯度爆炸的问题,导致网络难以训练。此外,过深的网络还可能导致过拟合和低级特征丢失,从而引发性能退化。为了解决这些问题,研究者们提出了多种方法,其中最具代表性的是残差网络(ResNet)及其恒等映射机制。
残差块
残差块通常由两部分组成:主路径(Main Path)和跳跃连接(Skip Connection)。
主路径:这是残差块中的主要计算路径,通常包含多个卷积层、批量归一化层(Batch Normalization)和激活函数层。这些层负责提取输入数据的特征,并通过非线性变换产生新的特征表示。
跳跃连接:这是残差块中的关键部分,它将输入信号直接连接到残差块的输出上。具体来说,跳跃连接将输入信号(或经过某种变换后的输入信号)与主路径的输出相加,得到残差块的最终输出。这种设计允许输入信号直接传递到网络的更深层,有助于缓解梯度消失问题。
左正常块,右残差块
批量归一化层
批量归一化层是深度学习中一种非常流行且有效的技术,它能够持续加速深层网络的收敛速度,同时改善模型的性能。
批量归一化层的基本思想是在每次训练迭代中,通过规范化当前小批量的输入数据,使得网络的每一层输入都保持稳定的均值和方差。对于卷积层,批量归一化层则会对每个输出通道进行规范化,确保每个通道内的数据分布保持稳定。
Resnet模型
每个模块有4个卷积层(不包括恒等映射的1×1卷积层)。 加上第一个7×7卷积层和最后一个全连接层,共有18层。 因此,这种模型通常被称为ResNet-18。 通过配置不同的通道数和模块里的残差块数可以得到不同的ResNet模型,例如更深的含152层的ResNet-152。
四、ResNet案例实践
使用Resnet模型对花的图片进行分类
制作数据源
data_transforms中指定了所有图像预处理操作
ImageFolder假设所有的文件按文件夹保存好,每个文件夹下面储存同一类别的图片,文件夹的名字为分类的名字
data_dir = 'flower_data'
train_dir = data_dir + '/train'
valid_dir = data_dir + '/valid'
data_transforms = {
'train': transforms.Compose([transforms.RandomRotation(30),
transforms.CenterCrop(224),
transforms.RandomHorizontalFlip(p=0.5),
transforms.RandomVerticalFlip(p=0.5),
transforms.ColorJitter(brightness=0.2, contrast=0.1, saturation=0.1, hue=0.1),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
'valid': transforms.Compose([transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
}
batch_size = 8
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),data_transforms[x]) for x in ['train', 'valid']}
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=batch_size, shuffle=True) for x in ['train', 'valid']}
dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'valid']}
class_names = image_datasets['train'].classes
print(image_datasets)
打印数据集
读取标签对应的实际名字
with open('cat_to_name.json', 'r') as f:
cat_to_name = json.load(f)
def im_convert(tensor):
image = tensor.to("cpu").clone().detach()
image = image.numpy().squeeze()
image = image.transpose(1, 2, 0)
image = image * np.array((0.229, 0.224, 0.225)) + np.array((0.485, 0.456, 0.406))
image = image.clip(0, 1)
return image
fig = plt.figure(figsize=(20, 12))
colums = 4
rows = 2
dataiter = iter(dataloaders['valid'])
inputs,classes = next(dataiter)
for idx in range(colums*rows):
ax = fig.add_subplot(rows, colums, idx + 1, xticks=[], yticks=[])
ax.set_title(cat_to_name[str(int(classes[idx]))])
plt.imshow(im_convert(inputs[idx]))
plt.show()
展示一个批次图片和其对应类别名
加载Resnet模型
加载models中提供的模型,并且直接用训练好权重当作初始化参数
def set_parameter_requires_grad(model, feature_extract):
if feature_extract:
for param in model.parameters():
param.requires_grad = False #这些参数的梯度将不会被计算,因此它们不会被更新
def model_resnet(num_classes, feature_extract, use_pretrained=True):
model_ft = models.resnet152(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, feature_extract) #不改变resnet中的参数值
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs, num_classes)
input_size = 224
return model_ft, input_size
model_ft, input_size = model_resnet(102, feature_extract=True, use_pretrained=True)
使用GPU训练
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
优化器设置
optimizer_ft = optim.Adam(model_ft.parameters(), lr=0.01)
scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)# 学习率每七个epoch衰减成原来的1/10
criterion = nn.CrossEntropyLoss()
训练模块
def train_model(model, dataloader, criterion, optimizer, num_epoch=25, filename=filename):
since = time.time()
best_acc = 0
model.to(device)
val_acc_history = []
train_acc_history = []
train_losses = []
val_losses = []
LRs = [optimizer.param_groups[0]['lr']]
best_model_wts = copy.deepcopy(model.state_dict())
for epoch in range(num_epoch):
print('Epoch {}/{}'.format(epoch, num_epoch-1))
print('-' * 10)
#训练和验证
for phase in ['train', 'valid']:
if phase == 'train':
model.train()
else:
model.eval()
running_loss = 0.0
running_corrects = 0
for inputs, labels in dataloader[phase]:
inputs, labels = inputs.to(device), labels.to(device)
# 清零
optimizer.zero_grad()
#只有训练的时候计算和更新梯度
with torch.set_grad_enabled(phase == 'train'):
outputs = model(inputs)
loss = criterion(outputs, labels)
_, preds = torch.max(outputs, 1)
#训练阶段更新权重
if phase == 'train':
loss.backward()
optimizer.step()
#计算损失
running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(preds == labels.data)
epoch_loss = running_loss / len(dataloader[phase].dataset)
epoch_acc = running_corrects / len(dataloader[phase].dataset)
time_elapsed = time.time() - since
print('Time elapsed: {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
print('{}, Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))
#得到最好的那次模型
if phase == 'valid' and epoch_acc > best_acc:
best_acc = epoch_acc
best_model_wts = copy.deepcopy(model.state_dict())
state = {
'state_dict': model.state_dict(),
'best_acc': best_acc,
'optimizer': optimizer.state_dict(),
}
torch.save(state, filename)
if phase == 'valid':
val_acc_history.append(epoch_acc)
val_losses.append(epoch_loss)
scheduler.step()
if phase == 'train':
train_acc_history.append(epoch_acc)
train_losses.append(epoch_loss)
print('Optimization learning rate : {:.7f}'.format(optimizer.param_groups[0]['lr']))
LRs.append(optimizer.param_groups[0]['lr'])
print()
time_elapsed = time.time() - since
print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
print('Best val Acc: {:4f}'.format(best_acc))
#训练完后用最好的一次当做模型的最终结果
model.load_state_dict(best_model_wts)
return model, val_acc_history, train_acc_history, val_losses, train_losses, LRs
开始训练
model_ft, val_acc_history, train_acc_history, val_losses, train_losses, LRs = train_model(model_ft, dataloaders, criterion, optimizer_ft, num_epoch=25, filename=filename)
训练结果
最后一次迭代
结果
总结
1、本周主要进行了卷积神经网络的知识点的学习
2、掌握了卷积神经网络的框架和执行流程
3、通过案例实际体会了卷积神经网络的执行过程
4、着重学习了卷积网络模型中的Resnet模型
5、初步了解数据增强和迁移学习相关技术
6、将数据增强和迁移学习融入实际案例,体会了Resnet152模型的执行过程和训练效果