实验4:MobileNet & ShuffleNet
| 姓名和学号? | |
|---|---|
| 本实验属于哪门课程? | 中国海洋大学25秋《软件工程原理与实践》 |
| 实验名称? | 实验4:MobileNet & ShuffleNet |
| 博客链接: |
学习要求
- CNN的基本结构:卷积、池化、全连接
- 典型的⽹络结构:AlexNet、VGG、GoogleNet、ResNet
实验内容
代码作业
1.定义 HybridSN 类
根据图中所示,数据处理流程可以这样描述:
- 首先连续进行三次3D卷积运算,,旨在从原始输入数据中逐步抽取出具有判别力的空谱联合特征。
- 网络首层使用8个尺寸为7×3×3的三维卷积核对输入数据进行操作
- 第二层采用16个尺寸为5×3×3的卷积核,对初级特征进行更深层次的加工与抽象
- 第三层配置32个尺寸为3×3×3的卷积核,进一步细化所提取的特征
- 重塑为标准的二维格式,以便后续的二维卷积处理。
- 模型使用64个3×3的二维卷积核对重塑后的特征图进行空间卷积
- 之后将经过所有卷积操作后得到的二维特征图展平为一个一维长向量,为全连接层的分类做准备。
- 最终输出结果为16个节点的最终分类类别数。
下面是代码实现:
import torch
import torch.nn as nnclass_num = 16class HybridSN(nn.Module):''' your code here '''def __init__(self):super(HybridSN, self).__init__()self.conv3d_1 = nn.Conv3d(1, 8, (7, 3, 3))self.conv3d_2 = nn.Conv3d(8, 16, (5, 3, 3))self.conv3d_3 = nn.Conv3d(16, 32, (3, 3, 3))self.conv2d = nn.Conv2d(576, 64, (3, 3))self.linear_1 = nn.Linear(18496, 256)self.linear_2 = nn.Linear(256, 128)self.linear_3 = nn.Linear(128, class_num)# Activation and dropoutself.activation_fn = nn.ReLU()self.dropout_layer = nn.Dropout(0.4)def forward(self, x):# 3D convolutional operationsx = self.activation_fn(self.conv3d_1(x))x = self.activation_fn(self.conv3d_2(x))x = self.activation_fn(self.conv3d_3(x))batch_size = x.shape[0]x = x.view(batch_size, -1, x.shape[3], x.shape[4])x = self.activation_fn(self.conv2d(x))x = x.view(batch_size, -1)x = self.activation_fn(self.linear_1(x))x = self.dropout_layer(x)x = self.activation_fn(self.linear_2(x))x = self.dropout_layer(x)x = self.linear_3(x)return x''' your code here '''# 随机输入, 测试网络结构是否通
x = torch.randn(1, 1, 30, 25, 25)
net = HybridSN()
y = net(x)
print(y.shape)
2.创建数据集
首先对高光谱数据实施PCA降维;然后创建 keras 方便处理的数据格式;然后随机抽取 10% 数据做为训练集,剩余的做为测试集。
- 首先定义基本函数
# 对高光谱数据 X 应用 PCA 变换
def applyPCA(X, numComponents):newX = np.reshape(X, (-1, X.shape[2]))pca = PCA(n_components=numComponents, whiten=True)newX = pca.fit_transform(newX)newX = np.reshape(newX, (X.shape[0], X.shape[1], numComponents))return newX# 对单个像素周围提取 patch 时,边缘像素就无法取了,因此,给这部分像素进行 padding 操作
def padWithZeros(X, margin=2):newX = np.zeros((X.shape[0] + 2 * margin, X.shape[1] + 2* margin, X.shape[2]))x_offset = marginy_offset = marginnewX[x_offset:X.shape[0] + x_offset, y_offset:X.shape[1] + y_offset, :] = Xreturn newX# 在每个像素周围提取 patch ,然后创建成符合 keras 处理的格式
def createImageCubes(X, y, windowSize=5, removeZeroLabels = True):# 给 X 做 paddingmargin = int((windowSize - 1) / 2)zeroPaddedX = padWithZeros(X, margin=margin)# split patchespatchesData = np.zeros((X.shape[0] * X.shape[1], windowSize, windowSize, X.shape[2]))patchesLabels = np.zeros((X.shape[0] * X.shape[1]))patchIndex = 0for r in range(margin, zeroPaddedX.shape[0] - margin):for c in range(margin, zeroPaddedX.shape[1] - margin):patch = zeroPaddedX[r - margin:r + margin + 1, c - margin:c + margin + 1] patchesData[patchIndex, :, :, :] = patchpatchesLabels[patchIndex] = y[r-margin, c-margin]patchIndex = patchIndex + 1if removeZeroLabels:patchesData = patchesData[patchesLabels>0,:,:,:]patchesLabels = patchesLabels[patchesLabels>0]patchesLabels -= 1return patchesData, patchesLabelsdef splitTrainTestSet(X, y, testRatio, randomState=345):X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=testRatio, random_state=randomState, stratify=y)return X_train, X_test, y_train, y_test
然后读取并创建数据集:
# 地物类别
class_num = 16
X = sio.loadmat('Indian_pines_corrected.mat')['indian_pines_corrected']
y = sio.loadmat('Indian_pines_gt.mat')['indian_pines_gt']# 用于测试样本的比例
test_ratio = 0.90
# 每个像素周围提取 patch 的尺寸
patch_size = 25
# 使用 PCA 降维,得到主成分的数量
pca_components = 30print('Hyperspectral data shape: ', X.shape)
print('Label shape: ', y.shape)print('\n... ... PCA tranformation ... ...')
X_pca = applyPCA(X, numComponents=pca_components)
print('Data shape after PCA: ', X_pca.shape)print('\n... ... create data cubes ... ...')
X_pca, y = createImageCubes(X_pca, y, windowSize=patch_size)
print('Data cube X shape: ', X_pca.shape)
print('Data cube y shape: ', y.shape)print('\n... ... create train & test data ... ...')
Xtrain, Xtest, ytrain, ytest = splitTrainTestSet(X_pca, y, test_ratio)
print('Xtrain shape: ', Xtrain.shape)
print('Xtest shape: ', Xtest.shape)# 改变 Xtrain, Ytrain 的形状,以符合 keras 的要求
Xtrain = Xtrain.reshape(-1, patch_size, patch_size, pca_components, 1)
Xtest = Xtest.reshape(-1, patch_size, patch_size, pca_components, 1)
print('before transpose: Xtrain shape: ', Xtrain.shape)
print('before transpose: Xtest shape: ', Xtest.shape) # 为了适应 pytorch 结构,数据要做 transpose
Xtrain = Xtrain.transpose(0, 4, 3, 1, 2)
Xtest = Xtest.transpose(0, 4, 3, 1, 2)
print('after transpose: Xtrain shape: ', Xtrain.shape)
print('after transpose: Xtest shape: ', Xtest.shape) """ Training dataset"""
class TrainDS(torch.utils.data.Dataset): def __init__(self):self.len = Xtrain.shape[0]self.x_data = torch.FloatTensor(Xtrain)self.y_data = torch.LongTensor(ytrain) def __getitem__(self, index):# 根据索引返回数据和对应的标签return self.x_data[index], self.y_data[index]def __len__(self): # 返回文件数据的数目return self.len""" Testing dataset"""
class TestDS(torch.utils.data.Dataset): def __init__(self):self.len = Xtest.shape[0]self.x_data = torch.FloatTensor(Xtest)self.y_data = torch.LongTensor(ytest)def __getitem__(self, index):# 根据索引返回数据和对应的标签return self.x_data[index], self.y_data[index]def __len__(self): # 返回文件数据的数目return self.len# 创建 trainloader 和 testloader
trainset = TrainDS()
testset = TestDS()
train_loader = torch.utils.data.DataLoader(dataset=trainset, batch_size=128, shuffle=True, num_workers=2)
test_loader = torch.utils.data.DataLoader(dataset=testset, batch_size=128, shuffle=False, num_workers=2)
创建结果:

3.开始训练
我们使用的是GPU训练。代码如下:
# 使用GPU训练,可以在菜单 "代码执行工具" -> "更改运行时类型" 里进行设置
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")# 网络放到GPU上
net = HybridSN().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(net.parameters(), lr=0.001)# 开始训练
total_loss = 0
for epoch in range(100):for i, (inputs, labels) in enumerate(train_loader):inputs = inputs.to(device)labels = labels.to(device)# 优化器梯度归零optimizer.zero_grad()# 正向传播 + 反向传播 + 优化 outputs = net(inputs)loss = criterion(outputs, labels)loss.backward()optimizer.step()total_loss += loss.item()print('[Epoch: %d] [loss avg: %.4f] [current loss: %.4f]' %(epoch + 1, total_loss/(epoch+1), loss.item()))print('Finished Training')
4.模型测试
首先,设置一个标志变量 count 并初始化为 0,用于追踪当前处理的是第几个批次。随后,在模型测试的迭代过程中,每个批次的数据都会被送入 GPU 并通过模型计算得到输出。对于模型产生的预测结果,处理规则如下:若是首个批次(count 等于 0),则将其直接作为总体预测结果的初始值;若不是首个批次,则将该批次的预测结果与之前累积的总体预测结果进行拼接。以此类推,循环结束后即可得到涵盖所有测试样本的最终预测数组 y_pred_test。
count = 0
# 模型测试
for inputs, _ in test_loader:inputs = inputs.to(device)outputs = net(inputs)outputs = np.argmax(outputs.detach().cpu().numpy(), axis=1)if count == 0:y_pred_test = outputscount = 1else:y_pred_test = np.concatenate( (y_pred_test, outputs) )# 生成分类报告
classification = classification_report(ytest, y_pred_test, digits=4)
print(classification)
模型测试结果:

观察测试结果不难看出,该模型的整体分类准确率达到95.14%,F1-score加权平均值也达到95.12%,两项指标均超过95%的水平,表明模型在绝大多数情况下具有良好的分类性能。然而,不同类别之间的表现存在一定差异,例如类别8的准确率为81.82%,但其召回率仅为50%,这意味着该类别中有一半的样本未被正确识别。总体而言,模型在样本数量充足的类别上表现出色,能够达到很高的识别精度,但在样本量相对不足的类别上,其性能表现则有所下降。
下面代码用于展示分类结果
# load the original image
X = sio.loadmat('Indian_pines_corrected.mat')['indian_pines_corrected']
y = sio.loadmat('Indian_pines_gt.mat')['indian_pines_gt']height = y.shape[0]
width = y.shape[1]X = applyPCA(X, numComponents= pca_components)
X = padWithZeros(X, patch_size//2)# 逐像素预测类别
outputs = np.zeros((height,width))
for i in range(height):for j in range(width):if int(y[i,j]) == 0:continueelse :image_patch = X[i:i+patch_size, j:j+patch_size, :]image_patch = image_patch.reshape(1,image_patch.shape[0],image_patch.shape[1], image_patch.shape[2], 1)X_test_image = torch.FloatTensor(image_patch.transpose(0, 4, 3, 1, 2)).to(device) prediction = net(X_test_image)prediction = np.argmax(prediction.detach().cpu().numpy(), axis=1)outputs[i][j] = prediction+1if i % 20 == 0:print('... ... row ', i, ' handling ... ...')

思考题
训练HybridSN,然后多测试⼏次,会发现每次分类的结果都不⼀样,请思考为什么?
第一次的结果:
和上图一致。
第二次的结果:

why?
- 每次训练时,卷积层、全连接层的初始权重都是随机生成的,不同的初始点会导致梯度下降走向不同的局部最优解,这是结果差异的主要来源。
- Dropout层在训练时随机"关闭"部分神经元,每次前向传播时,被关闭的神经元组合不同,相当于每次都在训练不同的"子模型"。
- 每个epoch样本的呈现顺序不同,影响梯度下降的路径和收敛速度。
如果想要进⼀步提升⾼光谱图像的分类性能,可以如何改进?
- 注意力机制增强:引⼊通道注意力、空间注意力、光谱注意力机制设计三维注意力模块,同时处理空间-光谱维度使⽤⾃注意力机制捕获长距离依赖
- 多尺度特征融合:加入空洞卷积扩⼤感受野构建特征金字塔网络多分⽀结构提取不同尺度的特征
- 数据增强优化:针对⾼光谱特点设计增强策略:光谱抖动、波段丢弃使⽤MixUp、CutMix等先进增强技术⽣成对抗网络进⾏数据扩充
- 网络架构改进: 增加3D卷积层数,网络架构更深;引入残差连接
depth-wise conv 和 分组卷积有什么区别与联系?
区别:
Depth-wise卷积: 输⼊通道数 = 分组数 = 输出通道数
分组卷积: 分组数 < 输⼊通道数
Depth-wise卷积 (极端情况) input_channels = groups = output_channels
分组卷积 (⼀般情况) input_channels > groups, output_channels > groups
联系:
都是标准卷积的泛化形式;都通过减少通道间连接来降低计算量;Depth-wise是分组卷积在groups=C_in时的特例。
SENet 的注意⼒是不是可以加在空间位置上?
答: 不可以
在 ShuffleNet 中,通道的 shuffle 如何⽤代码实现?
shuffle的核心思想是将分组卷积后的特征图在通道维度上重新洗牌,促进组间信息交流。
标准实现如下:
import torch
import torch.nn as nndef channel_shuffle(x, groups):batch_size, num_channels, height, width = x.size()# 检查通道数是否能被分组数整除assert num_channels % groups == 0# 计算每组的通道数channels_per_group = num_channels // groups# 重塑: [batch, groups, channels_per_group, height, width]x = x.view(batch_size, groups, channels_per_group, height, width)# 转置: [batch, channels_per_group, groups, height, width] x = x.transpose(1, 2).contiguous()# 重塑回原始维度: [batch, channels, height, width]x = x.view(batch_size, -1, height, width)return x
问题总结
体会
心得体会
本次试验让我接触到多种新型网络模型,包括 MobileNet 系列的 V1、V2 及 V3 版本,还有 ShuffleNet、SENet 与 CBAM 模型,并对它们的核心结构、优势特性及适用场景建立了系统认知。同时,通过深入学习 HybridSN 网络模型,我了解了其 “3D 卷积 - 2D 卷积 - 全连接层” 的组合网络架构。
我完成了该网络类代码的编写与运行,直观观察到它在高光谱图像分类任务中的实际表现,也进一步深化了对 2D 卷积和 3D 卷积的理解,可以梳理出两者在原理及应用上的差异。
在完成思考题的过程中,我对 HybridSN 模型的认知得到进一步提升。我主动搜索并学习了优化高光谱图像分类性能的多种思路,理清了深度可分离卷积与分组卷积的异同点,整体收获十分丰富。