2022.9.19-2022.9.25学习记录
1. 本周总结
1.1 深度学习方面
- 复习之前学习过的经典架构与深度学习基础知识
- 除梯度下降外更强大的优化算法
- 将自己的数据集转化为tensor
- 迁移学习的使用与应用场合
- 完成一个经典的识别案例
1.2 脑电方面
- EEGNet论文
2. 下周学习内容
- 一个正在学习的案例:医学影响识别
- 在论文上查看学习EEGNet的使用
3. 本周学习难点与重点
3.1 深度学习
3.1.1 数据格式转换
-
从图像png/jpg到四维tensor
-
从二维表(csv/txt)到四维tensor
-
从mat/pt/lmdb到四维tensor
3.1.2 预训练/迁移学习
大多数情况下,我们能够用于训练模型的算力和数据都很有限,要完成一个大型神经网络的训练非常困难,因此我们希望能够尽量重复利用已经训练好的神经网络以节约训练和数据资源。如果我们在执行预测任务时,能够找到一个曾经执行过相似任务、并被训练得很好的大型架构,那我们就可以使用这个大型架构中位置较浅的那些层来帮助我们构筑自己的网络。借用已经训练好的模型来构筑新架构的技术就叫做“迁移学习”(transfer learning),也叫做预训练(pre-train)。预训练是我们训练大型模型时、用于降低数据需求以及加快训练速度的关键技术之一。
- 核心思路:借用训练好的模型上的权重
- 两种选择
- 将迁移层上的权重作为初始化工具(Initialization tool):将迁移层的权重作为新架构的初始化权重,在此基础上对所有层进行训练,给模型指一条“明路”。在最严谨的文献中,借用原始架构权重,并对所有层进行再训练的流程被称为“预训练”。
- 将迁移层作为固定的特征提取工具(fixed feature extractor):我们将迁移过来的层上的权重“固定”起来,不让这些权重受到反向传播等过程的影响,而让它们作为架构中的“固定知识”被一直使用。相对的,我们自己添加的、或我们自己选择的层则可以像普通的网络架构一样初始化参数并参与训练,并在每一次迭代中逐渐找到适合自己的权重。在最严谨的文献中,这个流程被称为“迁移学习”。
3.1.3 模型选择
确定应该使用什么架构之前,我们需要完成模型选择的工作
- 首先,需要对各个模型有较为清晰的认知
- 第二,需要关注数据的复杂程度
- 第三,需要关注数据的规模和我们拥有的算力。
一个完整的流程
3.1.4 SVHN识别算法
- 简单介绍:是深度学习诞生初期被创造出来的众多数字识别数据集中的一个,也是唯一一个基于实拍图片制作而成的数字识别数据集.整个数据集支持识别、检测、无监督三种任务.
- Size:32X32X3
- 样本量:十万左右
- 原始数据集文件格式:.mat
- 完整流程(只截取部分重点代码)*
1.导入数据
train = torchvision.datasets.SVHN(root ='E:\DeepLearning\Datasets\SVHN' #root的用法
,split ="train"
,download = True
,transform = trainT
)
2.查看数据特点
# 打印其中一个样本查看其文件格式,3个通道,32X32
for x,y in train:
print(x.shape)
print(y)
break
结果:
#让每个数据集随机显示5张图像
def plotsample(data): #只能够接受tensor格式的图像
fig, axs = plt.subplots(1,5,figsize=(10,10)) #建立子图
for i in range(5):
num = random.randint(0,len(data)-1) #首先选取随机数,随机选取五次
#抽取数据中对应的图像对象,make_grid函数可将任意格式的图像的通道数升为3,而不改变图像原始的数据
#而展示图像用的imshow函数最常见的输入格式也是3通道
npimg = torchvision.utils.make_grid(data[num][0]).numpy()
nplabel = data[num][1] #提取标签
#将图像由(3, weight, height)转化为(weight, height, 3),并放入imshow函数中读取
axs[i].imshow(np.transpose(npimg, (1, 2, 0)))
axs[i].set_title(nplabel) #给每个子图加上标签
axs[i].axis("off") #消除每个子图的坐标轴
结果:
做数据增强后的图片(随机裁剪与旋转):
- 3.定义神经网络架构
- 初步拟定两种:ResNet18、VGG13,比较两种架构
基于ResNet定义的机构:
class MyResNet(nn.Module):
def __init__(self):
super().__init__()
self.block1 = nn.Sequential(nn.Conv2d(3,64,kernel_size=3
,stride=1,padding=1,bias=False)
,resnet18_.bn1
,resnet18_.relu) #删除池化层
#后续的架构直接从经典架构中选
#对尺寸很小的数据集而言,我们的深度本来就不深,因此可以试着在特征图数量上有所增加(增加宽度)
self.block2 = resnet18_.layer2 #2个残差单元
self.block3 = resnet18_.layer3 #2个残差单元
#自适应平均池化+线性层,此处都与残差网络一致
self.avgpool = resnet18_.avgpool
#输出的线性层自己写,以确保输出的类别数量正确
self.fc = nn.Linear(in_features=256, out_features=10, bias=True)
def forward(self,x):
x = self.block1(x)
x = self.block2(x)
x = self.block3(x)
x = self.avgpool(x)
x = x.view(x.shape[0],256)
x = self.fc(x)
return x
- 4.设置提前停止
lass EarlyStopping():
def __init__(self, patience = 5, tol = 0.0005): #惯例地定义我们所需要的一切变量/属性\
#当连续patience次迭代时,这一轮迭代的损失与历史最低损失之间的差值小于阈值时
#就触发提前停止
self.patience = patience
self.tol = tol #tolerance,累积5次都低于tol才会触发停止
self.counter = 0 #计数,计算现在已经累积了counter次
self.lowest_loss = None
self.early_stop = False #True - 提前停止,False - 不要提前停止
def __call__(self,val_loss):
if self.lowest_loss == None: #这是第一轮迭代
self.lowest_loss = val_loss
elif self.lowest_loss - val_loss > self.tol:
self.lowest_loss = val_loss
self.counter = 0
elif self.lowest_loss - val_loss < self.tol:
self.counter += 1
print("\t NOTICE: Early stopping counter {} of {}".format(self.counter,self.patience))
if self.counter >= self.patience:
print('\t NOTICE: Early Stopping Actived')
self.early_stop = True
return self.early_stop
#这一轮迭代的损失与历史最低损失之间的差 - 阈值
- 5.定义训练函数
神经网络的训练流程
1、向前传播
2、求解损失函数
3、反向传播 - 求解梯度
4、基于梯度,对权重进行迭代
5、梯度清零
测试过程
确定没有相关的梯度会生成/阻止计算图的追踪
1、向前传播
2、求解损失函数
所需参数 net,batchdata,testdata,criterion,opt,epochs,tol,modelname,PATH
def fit_test(net,batchdata,testdata,criterion,opt,epochs,tol,modelname,PATH):
"""
对模型进行训练,并在每个epoch后输出训练集和测试集上的准确率/损失
以实现对模型的监控
实现模型的保存
参数说明:
net: 实例化后的网络
batchdata:使用Dataloader分割后的训练数据
testdata:使用Dataloader分割后的测试数据
criterion:所使用的损失函数
opt:所使用的优化算法
epochs:一共要使用完整数据集epochs次
tol:提前停止时测试集上loss下降的阈值,连续5次loss下降不超过tol就会触发提前停止
modelname:现在正在运行的模型名称,用于保存权重时作为文件名
PATH:将权重文件保存在path目录下
"""
SamplePerEpoch = batchdata.dataset.__len__() #整个epoch里有多少个样本
allsamples = SamplePerEpoch*epochs
trainedsamples = 0
trainlosslist = []
testlosslist = []
early_stopping = EarlyStopping(tol=tol)
highestacc = None
for epoch in range(1,epochs+1):
net.train()
correct_train = 0
loss_train = 0
for batch_idx, (x, y) in enumerate(batchdata):
y = y.view(x.shape[0])
correct, loss = IterOnce(net,criterion,opt,x,y)
trainedsamples += x.shape[0]
loss_train += loss
correct_train += correct
if (batch_idx+1) % 125 == 0:
#现在进行到了哪个epoch
#现在训练到了多少个样本
#总共要训练多少个样本
#现在的训练的样本占总共需要训练的样本的百分比
print('Epoch{}:[{}/{}({:.0f}%)]'.format(epoch
,trainedsamples
,allsamples
,100*trainedsamples/allsamples))
TrainAccThisEpoch = float(correct_train*100)/SamplePerEpoch
TrainLossThisEpoch = float(loss_train*100)/SamplePerEpoch #平均每个样本上的损失
trainlosslist.append(TrainLossThisEpoch)
#每次训练完一个epoch,就在测试集上验证一下模型现在的效果
net.eval()
loss_test = 0
correct_test = 0
loss_test = 0
TestSample = testdata.dataset.__len__()
for x,y in testdata:
y = y.view(x.shape[0])
correct, loss = TestOnce(net,criterion,x,y)
loss_test += loss
correct_test += correct
TestAccThisEpoch = float(correct_test * 100)/TestSample
TestLossThisEpoch = float(loss_test * 100)/TestSample
testlosslist.append(TestLossThisEpoch)
#对每一个epoch,打印训练和测试的结果
#训练集上的损失,测试集上的损失,训练集上的准确率,测试集上的准确率
print("\t Train Loss:{:.6f}, Test Loss:{:.6f}, Train Acc:{:.3f}%, Test Acc:{:.3f}%".format(TrainLossThisEpoch
,TestLossThisEpoch
,TrainAccThisEpoch
,TestAccThisEpoch))
#如果测试集准确率出现新高/测试集loss出现新低,那我会保存现在的这一组权重
if highestacc == None: #首次进行测试
highestacc = TestAccThisEpoch
if highestacc < TestAccThisEpoch:
highestacc = TestAccThisEpoch
torch.save(net.state_dict(),os.path.join(PATH,modelname+".pt"))
print("\t Weight Saved")
#提前停止
early_stop = early_stopping(TestLossThisEpoch)
if early_stop == "True":
break
print("Complete")
return trainlosslist, testlosslist
定义优化算法和可视化过程省略,调试过程由于电脑性能省略。
3.2 EEGNet经典论文阅读
-
简单介绍:EEGnet是为专门一般的脑电图识别任务而设计的通用且紧凑的卷积神经网络。(设计思路借鉴了MobileNet)
-下图是EEGnet的整体结构图,只有三个卷积模块,重点是depthwise conv 和separable conv这两个卷积模块。
-
结构细节
-
激活函数:EEGnet有使用ELU激活函数,其具有如ReLU的正值特性,所以一样可以缓解梯度消失问题,而ELU还有负值,可以让激活单元输出的均值更接近0,从而达到正则化的效果。且ELU的负值计算是指数函数,不会发生突变,所以对输入变化或噪声更鲁棒。(SELU输出的均值不但更接近0,其方差也更接近单位方差1,进一步达到正则化效果,提升收敛速度等)
-
实验结果:
个体内实验结果:
跨个体实验结果:
- 总结
- Depthwise Convolution的一个卷积核负责一个通道,即一个通道只被一个卷积核卷积。
Pointwise Convolution的卷积核的shape为 1×1×M×C,M为上一层的通道数,C为卷积核的数目。
EEGnet由1个普通卷积conv + 1个Depthwise conv + 1个separable conv组成,
其中, separable Convolution(深度分离卷积层)由一个Depthwise Convolution和一个Pointwise Convolution组成。
参数量对比:
相同放入5×5像素、三通道的输入,普通卷积层和separable Convolution同样得到4个Feature map
普通卷积层的参数量: 4 × 3 × 3 × 3 + 4 = 112
Depthwise conv的参数量 = 3× 1 × 3 × 3 + 3 = 30
Pointwise conv的参数量 = 4× 3 × 1 × 1 + 4 = 16
Separable conv的参数量 = Depthwise conv + Pointwise conv = 46
所以Separable Convolution可以显著减少卷积层的参数数量