简介:面向计算机视觉课程大作业与毕业设计场景,这份资料包围绕卫星云层图像的理解与识别任务,提供了完整可运行的 Python 源码、文档说明、报告 PPT 与实验报告。项目代码经测试可正常执行,覆盖数据预处理、模型训练与结果分析等环节,适合计科、人工智能、通信工程等专业学生在课程设计、项目初期立项或答辩演示中参考,也便于在已有代码基础上进行二次扩展功能。包内共 140 个文件,以 70 个 Python 源码文件为主,附有编译生成的 pyc、Shell 脚本、CSV 数据表、YAML 配置文件、PNG 结果图以及 README、实验报告 PDF 和 PPTX 演示文稿,整体约 62.3 MB。文件组织方式清晰,便于按代码、数据、配置和说明文档分模块查阅,能节省整理资料的时间。目前已有 239 人学习下载。从内容预览看,内含训练与测试的 CSV 数据、多组模型测试结果、实验报告 PDF 和结果图片,读者可直接获取项目全貌、复现识别流程,并参考其文档与答辩材料完善自己的大作业或毕设展示。
1. 卫星云图识别大作业:从CSV到可跑的CNN全流程
拿到“计算机视觉大作业:卫星云层图像的理解与识别”这套资源时,绝大多数人的第一反应是找现成的图片文件夹,结果打开train_700.ver0.csv才发现数据全在文本表格里。这不是数据坏了,而是小规模课程数据集常用的保存方式:700张图,每张图按像素拉平成一行,标签放在第一列。整套资源包含训练集、四个测试CSV、Python源码、实验报告PDF和汇报PPT,目标是让一个没有完整跑过深度学习流程的人,在一天内把卫星云图分类从数据加载做到测试评估。它适合正在做计算机视觉课设、毕设初期演示,或者想用一份规范实验报告模板的同学。接下来的章节,我按自己拆这套项目时的顺序来讲:先处理数据,再选模型,然后训练、评估,最后是几个必须避开的坑。
2. 先把数据集吃透:CSV图像读取、还原与划分的三个实操点
2.1 为什么用CSV装图像
卫星云图大作业选CSV而不是图片文件夹,我一开始也疑惑。后来拆完发现,CSV对小数据集有实打实的好处。第一是版本管理方便,700个样本的像素改动在git diff里一目了然,这对课设答辩时讲“我改过什么”很有价值;第二是pandas直接能读,做类别分布统计、像素均值方差分析不用另外写IO逻辑;第三是省去了图片路径在不同操作系统上的兼容问题,Windows和Linux下不会出现路径分隔符翻车。当然它的边界很明显,CSV按文本存像素,一张512×512的图拉平就是26万列,文件会迅速膨胀,所以这种格式只适合几百到几千张图的小样本,超出这个量级还是老老实实用TFRecord或者按文件夹组织图片的ImageFolder方案。
2.2 读取CSV并还原成图像张量
常见的做法是先读进来看看shape,再决定怎么reshape。我一般用pandas读入,打印前几行确认标签列:
import pandas as pd import numpy as np df = pd.read_csv('train_700.ver0.csv') print('shape:', df.shape) print(df.head(2))第一次打印shape时要注意两个数字:行数是样本数700,列数减1就是单张图像的像素总数。如果列数是4097,说明每张图4096个像素,开方正是64,那就是单通道64×64;如果不能整除,就得考虑是不是RGB三通道。这一步判断决定了后面的reshape参数,值得花30秒确认。
接下来把标签和像素拆开。这里有个细节:读入像素要用float32,不要用int,因为CSV里有些值可能带小数,用int会直接截断,云层灰度细节会丢得干干净净。
labels = df.iloc[:, 0].values.astype(np.int64) pixels = df.iloc[:, 1:].values.astype(np.float32) img_size = (64, 64)然后写一个标准Dataset类,把一行像素还原成图像张量:
import torch from torch.utils.data import Dataset class CloudDataset(Dataset): def __init__(self, pixels, labels, img_size=(64, 64)): self.pixels = pixels self.labels = labels self.img_size = img_size def __len__(self): return len(self.labels) def __getitem__(self, idx): img = self.pixels[idx].reshape(1, *self.img_size) img = torch.from_numpy(img) / 255.0 label = torch.tensor(self.labels[idx], dtype=torch.long) return img, label这段代码的逻辑说明:reshape成(1, 64, 64)是PyTorch的NCHW约定,单通道要放最前面;除以255把像素归一到[0, 1]区间,让反向传播更稳定。为什么不在Dataset里做标准化而要放到后面单独做?因为标准化用的均值和标准差只能从训练集算,在Dataset里直接套用全局数据,极容易污染验证和测试数据。如果CSV列数不是4096,把img_size改成对应分辨率即可。这一节里我踩过的坑是把图像尺寸写死,换数据集时报维度不匹配,所以建议在类外面定义img_size并打印验证。
2.3 训练集与验证集划分:标准化必须在划分之后
数据加载类写完后不要急着训练,先做train/val划分。我一般用sklearn的train_test_split,并且一定要开stratify,保证云层每个类别在训练集和验证集里的比例一致:
from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val = train_test_split( pixels, labels, test_size=0.2, stratify=labels, random_state=42) mean = X_train.mean() std = X_train.std() X_train = (X_train - mean) / (std + 1e-8) X_val = (X_val - mean) / (std + 1e-8) train_dataset = CloudDataset(X_train, y_train) val_dataset = CloudDataset(X_val, y_val)这里有两个关键点:第一,mean和std只能用X_train算,哪怕X_val的信息就摆在手边也不能合起来算,否则验证集的分布信息渗进特征标准化环节,属于典型的数据泄露,后面评估结果会虚高;第二,标准差分母加1e-8是为了防止像素值全相同导致除零。划分完后打印一下每个类别的样本数量,确认没有哪个类只剩个位数的样本,不然训练阶段模型会直接忽略它。至于random_state,我习惯固定成42,这样多次复现结果完全一致,报告里写“随机种子42”也显得严谨,别人照着跑能拿到同一份数字。
3. 训练一个能用的CNN:模型结构、训练参数与日志记录
3.1 CNN为什么适合卫星云图
卫星云图的识别本质是纹理分类:云层的厚度、边界、卷云和积云在局部区域内有明显的图案差异。CNN通过卷积核在整张图上滑动提取局部纹理,再用池化逐步聚合,天然具备平移不变性,云在画面偏左还是偏右不影响分类结果。如果换成全连接网络,64×64的图输入层就有4096个节点,第一层全连接的权重就是百万级,在700张训练图上是灾难。所以在这个任务里,两层卷积加两层全连接是性价比很高的结构:卷积负责提取特征,全连接负责做最终分类。自己搭而不是一上来就上ResNet,是因为课设场景里老师往往要求核心模型可以讲清楚前向流程,而且这么大的数据量用深层网络反而是负优化。
3.2 从零搭一个轻量CNN
import torch.nn as nn import torch.nn.functional as F class CloudCNN(nn.Module): def __init__(self, num_classes=2): super().__init__() self.conv1 = nn.Conv2d(1, 16, kernel_size=3, padding=1) self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1) self.pool = nn.MaxPool2d(2) self.fc1 = nn.Linear(32 * 16 * 16, 64) self.fc2 = nn.Linear(64, num_classes) self.dropout = nn.Dropout(0.3) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) # 64x64 -> 32x32 x = self.pool(F.relu(self.conv2(x))) # 32x32 -> 16x16 x = x.view(x.size(0), -1) x = self.dropout(F.relu(self.fc1(x))) return self.fc2(x)逻辑说明:conv1从1通道变成16通道,padding=1保持尺寸不变,经过第一层池化变成32×32;conv2扩到32通道,再池化成16×16,所以全连接层的输入维度是32×16×16=8192。加Dropout是为后面过拟合留的缓冲:小数据集上模型记忆训练数据太容易,0.3的丢弃比例通常够用。注释里标出了每一步的空间尺寸变化,这是答辩时老师必问的东西——如果讲不清特征图怎么从64变到16,报告会扣分。建议第一轮跑通后打印model摘要,看每层输出形状是不是和预期一致,再进完整训练。
3.3 训练参数怎么定
训练参数我用下表给出推荐值,这样报告里可以直接引用:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| batch_size | 32 | 700张图太小,64会让梯度震荡变小 |
| epochs | 30~50 | 小数据集上过50轮后验证loss通常开始回升 |
| optimizer | Adam | 自适应学习率,省去手动调整的麻烦 |
| lr | 1e-3 | Adam下1e-3是稳妥起点,跑不稳再降到3e-4 |
| scheduler | CosineAnnealingLR | 让学习率在训练后期平滑衰减 |
| loss | CrossEntropyLoss | PyTorch里该损失自带Softmax |
损失函数这块有个初学者常走的弯路:在模型forward里先过一遍Softmax,又在CrossEntropyLoss里过一次,两个Softmax叠在一起反而让梯度变小。CrossEntropyLoss已经把Softmax和交叉熵合并了,所以模型的最后一层就输出原始logits,不要额外加激活函数。
3.4 训练循环与日志记录
训练循环我习惯封装成一个函数,把每个epoch的loss和acc存下来:
def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total = 0.0, 0, 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() out = model(imgs) loss = criterion(out, labels) loss.backward() optimizer.step() total_loss += loss.item() * imgs.size(0) correct += (out.argmax(1) == labels).sum().item() total += imgs.size(0) return total_loss / total, correct / total这个函数的逻辑是:每批数据先清零梯度,前向得到out,和目标标签算交叉熵loss,反向传播后更新参数。计算正确率时用argmax(1)取每个样本得分最高的类别,再跟labels比较。用out.argmax(1)而不是torch.max(out, 1)的原因是前者直接返回索引,代码意图清晰。调用时每轮验证集也跑一遍,把结果追加到日志列表:
train_log, val_log = [], [] best_acc = 0.0 for epoch in range(30): tr_loss, tr_acc = train_one_epoch(model, train_loader, optimizer, criterion, device) va_loss, va_acc = evaluate(model, val_loader, criterion, device) train_log.append((tr_loss, tr_acc)) val_log.append((va_loss, va_acc)) if va_acc > best_acc: best_acc = va_acc torch.save(model.state_dict(), 'best_model.pth') print(f'epoch {epoch:02d} | train {tr_loss:.4f}/{tr_acc:.4f} | val {va_loss:.4f}/{va_acc:.4f}')设备管理这里注意,在一开始就把device写成torch.device('cuda' if torch.cuda.is_available() else 'cpu'),千万别在脚本里到处写死cuda,否则在无GPU的机器上整个脚本直接崩掉。保存模型时只存state_dict,不存整个model对象,这样换机器加载方便,也不会因为类定义位置不同而反序列化失败。
4. 测试集评估与实验报告:四个测试文件怎么用、指标怎么看
4.1 四个测试CSV的关系
资源里同时出现了model_test_1.csv、model_test_3.csv、model_test_4.csv和test.csv,第一次看到时我也愣了一下。看名字,前三个更可能是不同实验阶段(第1轮、第3轮、第4轮)留出的测试分片,test.csv是最终统一的测试集。怎么验证这个判断?简单办法是分别读进来,看行数和列数是否一致,如果数据来源相同,特征分布应当接近。我倾向于把它们当四份独立测试集分别跑预测,再对比结果;如果评分环节只要求一个数字,就以test.csv为准,其他三个作为调试参考。这样做还有一个额外的好处:答辩时能拿“多测试集交叉验证”当作加分项,说明你考虑了模型的稳定性。
4.2 预测与评估指标
加载训练好的权重,在测试CSV上做预测:
def predict_csv(model, csv_path, mean, std, img_size=(64, 64), device='cpu'): df = pd.read_csv(csv_path) pixels = df.iloc[:, 1:].values.astype(np.float32) pixels = (pixels - mean) / (std + 1e-8) imgs = torch.from_numpy(pixels).view(-1, 1, *img_size) model.eval() with torch.no_grad(): preds = model(imgs.to(device)).argmax(1).cpu().numpy() return preds注意这里标准化用了训练集算好的mean和std,原因和验证集一样:不能拿测试集自己的均值来标准化,否则等价于把测试集的分布信息提前暴露给模型。跑完后打印预测结果的类别分布,如果某一类占比异常高,说明模型预测有偏,要回到混淆矩阵排查。
评估不能只看准确率。卫星云图类别常有不均衡,某一类占80%时,全猜这一类的准确率也有80%,看起来很漂亮但模型没有泛化能力。我一般会打印混淆矩阵和F1分数:
from sklearn.metrics import classification_report, confusion_matrix print(classification_report(y_val, val_preds, digits=3)) print(confusion_matrix(y_val, val_preds))classification_report会按类别给出precision、recall、f1-score,哪个类别拖后腿一目了然。判断模型是否可用时我的标准是:多数类准确率高不算达标,少数类的recall必须也有实际值,否则答辩时老师挑一个样本追问就能难住你。
| 指标 | 关注点 | 场景 |
|---|---|---|
| Accuracy | 总体正确比例 | 类别均衡时参考 |
| Precision | 预测为某类的可信度 | 误报成本高的场景 |
| Recall | 某类能不能找全 | 不均衡数据必看 |
| F1 | 两者的调和平均 | 综合排名用 |
4.3 实验报告的组织
资源里的实验报告PDF是一个现成的写作模板,我拆下来觉得它的结构值得复用。这类课设报告的套路是:实验目的与数据集介绍、预处理流程、模型设计、实验参数、结果分析与对比。最关键的是不能只贴训练好的最终结果,得有中间对照,比如把只用两层卷积和三层卷积都跑一遍,在报告里放两个准确率,再解释为什么在这个数据量下浅层更稳。报告里的图表无非是训练曲线、混淆矩阵、几张预测结果的样例可视化,用matplotlib就能出,重点是每张图下面必须有结论性说明,而不是只写“如图所示”。这份报告PDF正好是个结构参考,比从零排版省事得多。
5. 卫星云图识别避坑指南:五个翻车现场的排查记录
这一章把拆这套资源时实际遇到的坑按“现象→原因→解决”整理出来,每一条都是先有现象再有结论,可以按编号快速对照。
5.1 数据泄露:验证集准确率虚高的真相
现象:训练集、验证集准确率都到0.98,换到test.csv只有0.7,差距大得不正常。原因:数据标准化时手滑用了全部数据的mean/std,验证集和测试集的分布信息已经参与计算,模型在验证集上被“剧透”了。解决:严格按划分后的数据分别计算,先train_test_split,再取X_train.mean()和X_train.std()。从那以后我每次写预处理都在代码里加注释,标注mean和std的来源,防止回头自己都忘了数据是怎么算的。
5.2 类别不均衡让准确率失真
现象:训练日志显示准确率一直上升,但查看混淆矩阵发现有一整类几乎没预测对。原因:云层数据里某类占绝大多数,模型把所有样本都推向多数类就能拿到高准确率,少数类的梯度信号被淹没。解决:打印每个类的样本数,如果差距超过10倍,给DataLoader加WeightedRandomSampler,让少数类样本在每轮中被抽到的概率提高;效果不理想时再给损失函数加class_weight作为补充。
5.3 小数据集上的过拟合
现象:训练loss降到0.05,验证loss反而从0.3涨到0.8,训练曲线后期完全背离。原因:两层卷积加两层全连接对700张图来说容量已经偏大,epoch跑到后期模型开始背训练样本而不是学泛化特征。解决:把Dropout从0.3提到0.5,减少conv1的通道数(比如从16降到8),同时监控验证loss,连续5轮不降就early stop,保存历史最优权重而不是最后一轮权重。
5.4 设备写死导致换机器直接崩
现象:在自己笔记本上跑得好好的脚本,拿到有GPU的机器上反而报错;或者反过来,GPU环境下写的代码在CPU机器上直接RuntimeError。原因:代码里出现了imgs.cuda()这类写死GPU的调用,环境一换就失效。解决:所有张量和模型统一走device = torch.device('cuda' if torch.cuda.is_available() else 'cpu'),数据加载后执行.to(device)。习惯上我在脚本最开头集中定义device,后面一律引用变量,不要再出现device字样以外的硬编码。
5.5 CSV像素值精度丢失与噪声
现象:训练loss下降奇慢,把预测错误的图片画出来发现全是颗粒状噪点,云层纹理完全看不清。原因:读取像素时用了int类型,CSV里的小数被截断;有些列本身存在缺失值,直接读成了0。解决:读入用astype(np.float32),再画一张图检查像素分布是否连续,直方图集中在几个孤立峰值就说明精度丢了。缺失值先看df.isna().sum(),把带有缺失值的行删掉或按列均值填充,别让0值混进正常像素。
6. 迁移学习与可视化验证:不做重复实验的进阶做法
6.1 把ResNet18接在云图数据上
如果老师允许用预训练权重,最省事的进阶路线是用torchvision自带的ResNet18做迁移学习。卫星云图和ImageNet的纹理结构不完全一样,但底层边缘、颜色过渡这些特征是可以复用的。常见做法是冻结backbone,只替换最后的全连接层:
import torchvision.models as models model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT) for param in model.parameters(): param.requires_grad = False model.fc = nn.Linear(512, num_classes)这段代码逻辑:weights=models.ResNet18_Weights.DEFAULT会加载ImageNet预训练权重;冻结全部参数后,只有最后接入的Linear层可训练,前面多层学到的通用特征不需要再调整。这里需要注意resnet18默认输入是3通道,而CSV里只有单通道灰度。两种处理方式:一是把单通道复制三次变成3通道,二是修改backbone第一个卷积层的输入通道为1并加载权重时删掉对应参数。第一种稳妥,第二种省内存但要额外处理权重名,新手建议直接用第一种。
6.2 用曲线和样例图验证结论
训练完成后,把train_log里的loss和验证准确率画出来,两条曲线一起看。训练损失还在下降但验证曲线开始抬升,是过拟合的明确信号;损失曲线出现锯齿状波动,说明学习率偏大,把lr降到3e-4重跑一轮。再把混淆矩阵画成热力图,哪些类互相搞混一目了然,比如卷云和薄云经常混在一起,可以在报告里写成“两类特征相近,需要增加纹理方向的增强”。最后随机挑几张测试图片,把预测标签和置信度标在图上,这是答辩时最直观的展示素材。
从那以后,我每次拿到这类课设源码,都先做一个单样本前向确认输入维度和数据范围,画一张样本图,再跑完整训练循环,节省下来的排错时间比这点步骤多得多。这套资源从数据到报告模板都是现成的,下载后对照README把文件放到位,按我上面的顺序跑一遍就能出完整结果。希望帮到你。
本文还有配套的精品资源,点击获取