
简介本资源是一份高分机器学习课程设计项目面向计算机、人工智能、自动化等专业的在校学生及初学者提供从掌纹图像预处理、特征提取PCA/滤波器、CNN建模、分类与认证到结果评估的完整实现方案。压缩包共18个文件含11个Jupyter Notebook覆盖图像预处理、海明距离计算、多进程加速、模型融合等关键实验、5个Python脚本如feature_extraction.py、classify.py等核心功能模块及LICENSE等辅助文件总大小201KB结构清晰、模块解耦便于逐环节理解与二次开发。已有183人下载学习项目源自作者高分毕设答辩均分96所有代码均经实测运行通过配套详细文档说明支持远程教学与答疑。读者可直接复现高精度掌纹识别流程亦可基于现有CNN预训练模型、分类器融合策略及评估脚本进行算法优化或迁移至其他生物特征识别任务。1. 掌纹识别不是“拍张照片就能认人”一个高分机器学习大作业的真实落地门槛你交上去的“掌纹识别”大作业如果只是调用 OpenCV 读图 sklearn.fit() 跑完就截图交差老师大概率会打个 75 分——不是不及格但离“高分项目”差三道硬门槛图像预处理不可跳过、特征维度必须可控、分类器得在小样本下稳定泛化。这不是人脸识别那种“千万级数据ResNet”的堆算力游戏而是一个典型的小样本、高噪声、低分辨率生物特征识别任务学生自己采集的掌纹图常带反光、手指遮挡、拍摄角度歪斜分辨率普遍在 640×480 以下单类样本常不足 20 张。标题里强调的“高分项目”核心不在模型多深而在全流程可复现、每步有依据、结果可解释、错误可追溯。我带过 3 届课程设计翻车最多的是PCA 降维后直接喂 SVM 却没做归一化CNN 输入尺寸硬塞 224×224 导致关键纹理失真或者把整张手掌图当训练图——其实真正判别区域只在掌心三角区即“掌褶交汇区”。本文就按你交作业前最后一周的实操节奏来写从原始图像怎么裁、PCA 怎么选主成分数、CNN 输入尺寸怎么定、验证集怎么构造才不被老师质疑“数据泄露”全部落到命令、参数、报错信息和截图位置。不讲理论推导只讲你明天打开 PyCharm 就能跑通的步骤。2. 图像预处理为什么必须手动裁剪掌心三角区而不是直接喂整图掌纹识别的物理基础是掌褶flexion creases的拓扑结构与走向而非皮肤纹理细节。公开数据集如 PolyU已标注掌心区域但学生自采图90%无标注。若直接用整张手图训练模型会学偏——比如把袖口、桌面反光、手指边缘当关键特征。我们用 OpenCV 做轻量级定位不依赖深度模型确保本地 CPU 10 秒内完成 100 张图预处理。2.1 手掌粗定位基于 HSV 颜色空间的肤色分割手掌肤色在 HSV 空间比 RGB 更鲁棒。关键不是阈值调到“看起来像”而是让后续二值化后连通域数量稳定在 13 个排除背景干扰。以下代码针对常见白/黄种人肤色避免用cv2.inRange硬设固定 HSV 范围import cv2 import numpy as np def skin_segment_hsv(img_bgr): # 转 HSV 并自适应调整 S/V 下限应对光照不均 hsv cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV) h, s, v cv2.split(hsv) # 动态计算 S 和 V 的下限取中位数的 0.6 倍避免过曝区域误判 s_thresh int(np.median(s) * 0.6) v_thresh int(np.median(v) * 0.5) # H 通道范围0-10 和 160-180覆盖红黄肤色S阈值V阈值 mask_h1 cv2.inRange(hsv, (0, s_thresh, v_thresh), (10, 255, 255)) mask_h2 cv2.inRange(hsv, (160, s_thresh, v_thresh), (180, 255, 255)) mask_skin cv2.bitwise_or(mask_h1, mask_h2) # 形态学闭运算填充小孔开运算去噪点 kernel np.ones((3,3), np.uint8) mask_clean cv2.morphologyEx(mask_skin, cv2.MORPH_CLOSE, kernel) mask_clean cv2.morphologyEx(mask_clean, cv2.MORPH_OPEN, kernel) return mask_clean # 示例调用 img cv2.imread(hand_raw.jpg) mask skin_segment_hsv(img) cv2.imwrite(skin_mask.jpg, mask) # 保存掩膜用于调试逻辑说明s_thresh和v_thresh动态计算是关键。固定阈值如S30,V50在阴天或台灯下极易失效动态取中位数×系数能适配不同光照。mask_clean输出是二值图白色区域为疑似手掌区域。2.2 掌心三角区精裁基于最大连通域重心与轮廓凸包手掌在掩膜中通常是最大连通域但需排除“手背朝上”导致的误判。我们用轮廓凸包convex hull找手掌外轮廓再通过重心坐标约束裁剪区域——掌心三角区中心约在手掌重心向下 1/3 处宽高比固定为 1:1.2实测最优def crop_palm_triangle(mask_binary, img_bgr, output_size(128, 154)): # 找最大连通域轮廓 contours, _ cv2.findContours(mask_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: raise ValueError(未检测到手掌区域请检查光照或背景) # 按面积排序取最大轮廓 contours sorted(contours, keycv2.contourArea, reverseTrue) largest_contour contours[0] # 计算凸包和重心 hull cv2.convexHull(largest_contour) M cv2.moments(largest_contour) if M[m00] 0: raise ValueError(轮廓面积为零可能掩膜全黑) cx, cy int(M[m10]/M[m00]), int(M[m01]/M[m00]) # 掌心三角区中心重心向下偏移 1/3 手掌高度 x, y, w, h cv2.boundingRect(largest_contour) center_y cy int(h * 0.33) # 向掌心方向偏移 # 裁剪区域以 (cx, center_y) 为中心output_size 宽高 half_w, half_h output_size[0]//2, output_size[1]//2 left max(0, cx - half_w) right min(img_bgr.shape[1], cx half_w) top max(0, center_y - half_h) bottom min(img_bgr.shape[0], center_y half_h) cropped img_bgr[top:bottom, left:right] return cv2.resize(cropped, (128, 154)) # 统一输出尺寸 # 示例调用 cropped_img crop_palm_triangle(mask, img) cv2.imwrite(palm_crop.jpg, cropped_img)参数说明output_size(128, 154)是经验值——128×128 会丢失掌褶纵向延伸信息154 高度保留关键三角区生命线/智慧线/感情线交汇处。center_y偏移量h * 0.33来自 PolyU 数据集统计掌心三角区质心纵坐标均值在手掌 bounding box 纵向 67% 位置。若你的样本手掌平铺角度大可微调为0.25~0.4。2.3 灰度归一化与 Gabor 滤波增强掌纹本质是明暗对比形成的褶皱RGB 信息冗余。转灰度后需解决两个问题1局部对比度低如掌心汗液反光区2褶皱方向多样性。Gabor 滤波器组4 方向 × 3 频率是经典解法但直接调skimage.filters.gabor易因尺度不匹配导致纹理模糊。我们手动构建滤波器核控制响应强度from scipy import ndimage import numpy as np def gabor_enhance(gray_img): # 定义 4 个方向0°,45°,90°,135°和 3 个频率0.1,0.2,0.3 orientations [0, np.pi/4, np.pi/2, 3*np.pi/4] frequencies [0.1, 0.2, 0.3] enhanced np.zeros_like(gray_img, dtypenp.float32) for theta in orientations: for freq in frequencies: # 构建 Gabor 核sigma2, gamma0.5, lambda1/freq sigma_x sigma_y 2.0 kernel cv2.getGaborKernel( (21, 21), sigma_x, theta, 1/freq, 0.5, 0, ktypecv2.CV_32F ) # 滤波并取绝对值增强响应 filtered cv2.filter2D(gray_img, cv2.CV_32F, kernel) enhanced np.maximum(enhanced, np.abs(filtered)) # 归一化到 0-255 并转 uint8 enhanced cv2.normalize(enhanced, None, 0, 255, cv2.NORM_MINMAX) return enhanced.astype(np.uint8) # 示例调用 gray cv2.cvtColor(cropped_img, cv2.COLOR_BGR2GRAY) enhanced gabor_enhance(gray) cv2.imwrite(palm_enhanced.jpg, enhanced)为什么不用直方图均衡化CLAHE 对局部对比度提升有限且会放大噪声Gabor 滤波定向增强褶皱同时抑制非方向性噪声。cv2.getGaborKernel参数中lambda1/freq确保频率与波长匹配gamma0.5控制椭圆率避免方向过度发散。3. 特征工程PCA 不是“降维万金油”主成分数必须用交叉验证定标题里“PCA”常被误解为“把图片变小就好”。实际在掌纹识别中PCA 作用有三1去除像素间强相关性2压缩特征维度以适配小样本 SVM3提供可解释性主成分即“特征掌纹”。但盲目设n_components50或0.95会导致两类问题过拟合保留噪声或欠拟合丢失关键褶皱。我们必须用留一法LOO交叉验证选n_components。3.1 构建 PCA 训练集为什么必须用“增强后图像”而非原始图PCA 的输入必须是同一预处理流程的图像。若用原始图训练 PCA再用增强图 transform协方差矩阵不匹配降维后特征分布偏移。以下代码确保 pipeline 一致性import numpy as np from sklearn.decomposition import PCA from sklearn.model_selection import StratifiedKFold from sklearn.svm import SVC from sklearn.metrics import accuracy_score def build_pca_dataset(image_paths, enhance_func): 批量加载、增强、展平图像返回 (n_samples, n_features) 矩阵 X_flat [] for path in image_paths: img cv2.imread(path, cv2.IMREAD_GRAYSCALE) if img is None: continue enhanced enhance_func(img) # 复用 2.3 节的 gabor_enhance # 裁剪到统一尺寸确保所有图同 shape resized cv2.resize(enhanced, (128, 154)) X_flat.append(resized.flatten()) return np.array(X_flat) # 示例假设 train_paths 是预处理后的图像路径列表 X_train_flat build_pca_dataset(train_paths, gabor_enhance) y_train [...] # 对应标签列表注意resized.flatten()输出是 128×15419712 维向量。这是 PCA 的输入维度不是最终特征维数。3.2 用留一法LOO确定最优主成分数小样本场景每类 30 张下k-fold 交叉验证方差大。LOO 虽计算量大但对 100 张以内数据完全可行且结果稳定from sklearn.model_selection import LeaveOneOut from sklearn.pipeline import Pipeline def find_best_pca_components(X, y, max_components200, step10): 用 LOO 交叉验证找最优 n_components loo LeaveOneOut() scores [] components_range range(10, min(max_components, X.shape[1]), step) for n_comp in components_range: # 构建 pipelinePCA SVM pipe Pipeline([ (pca, PCA(n_componentsn_comp)), (svm, SVC(kernelrbf, C1.0, gammascale)) ]) # LOO 循环 loo_scores [] for train_idx, test_idx in loo.split(X): X_train_loo, X_test_loo X[train_idx], X[test_idx] y_train_loo, y_test_loo y[train_idx], y[test_idx] pipe.fit(X_train_loo, y_train_loo) pred pipe.predict(X_test_loo) loo_scores.append(accuracy_score(y_test_loo, pred)) scores.append(np.mean(loo_scores)) print(fn_components{n_comp}, LOO Accuracy{np.mean(loo_scores):.3f}) best_n components_range[np.argmax(scores)] return best_n, scores # 执行搜索 best_n_comp, score_list find_best_pca_components(X_train_flat, y_train) print(f最优主成分数: {best_n_comp})血泪经验在 20 类、每类 15 张的典型课程数据集上best_n_comp通常在 4080 之间。设max_components200是为防极端情况如某类样本异常多step10平衡搜索粒度与耗时。LOO 结果比 5-fold 稳定 ±3%尤其当某类样本仅 5 张时fold 划分易导致某 fold 无该类样本。3.3 可视化“特征掌纹”验证 PCA 是否学到有效结构PCA 主成分本身是向量需 reshape 回图像尺寸观察其物理意义。这是答辩时展示“模型可解释性”的关键图def plot_pca_components(pca_model, n_components9, img_shape(154, 128)): 绘制前 n_components 个主成分reshape 为图像 fig, axes plt.subplots(3, 3, figsize(12, 12)) axes axes.ravel() for i in range(n_components): # 取第 i 个主成分向量reshape 为图像 component pca_model.components_[i].reshape(img_shape) # 归一化到 0-255 便于显示 comp_norm cv2.normalize(component, None, 0, 255, cv2.NORM_MINMAX) axes[i].imshow(comp_norm, cmapgray) axes[i].set_title(fPC {i1}) axes[i].axis(off) plt.tight_layout() plt.savefig(pca_components.png, dpi300, bbox_inchestight) plt.show() # 训练最终 PCA 模型 pca_final PCA(n_componentsbest_n_comp) X_train_pca pca_final.fit_transform(X_train_flat) plot_pca_components(pca_final)玄学判断标准前 3 个 PC 应呈现宏观掌形如整体明暗分布PC 410 开始出现方向性条纹对应主要掌褶走向PC 20 后变为高频噪声斑点。若 PC1 就是随机噪点说明预处理失败或样本质量太差。4. 模型构建与训练CNN 不是越大越好小网络数据增强才是课程作业正解标题中“CNN”常被学生理解为“必须用 VGG/ResNet”。但在 100 张图、20 类的课程数据上VGG16 全连接层参数量超 100 万远超样本量必然过拟合。我们用轻量级 CNN参数量 5 万配合针对性数据增强效果反超大模型。4.1 自定义轻量 CNN 结构为什么用 3 层卷积而非 5 层参考 MobileNet 思路但去掉深度可分离卷积课程作业无需极致轻量聚焦感受野匹配掌褶尺度import torch import torch.nn as nn import torch.nn.functional as F class PalmCNN(nn.Module): def __init__(self, num_classes20): super(PalmCNN, self).__init__() # 输入1×154×128灰度图 self.conv1 nn.Conv2d(1, 16, kernel_size5, stride1, padding2) # 154×128 → 154×128 self.bn1 nn.BatchNorm2d(16) self.pool1 nn.MaxPool2d(2, 2) # → 77×64 self.conv2 nn.Conv2d(16, 32, kernel_size5, stride1, padding2) # → 77×64 self.bn2 nn.BatchNorm2d(32) self.pool2 nn.MaxPool2d(2, 2) # → 38×32 self.conv3 nn.Conv2d(32, 64, kernel_size3, stride1, padding1) # → 38×32 self.bn3 nn.BatchNorm2d(64) self.pool3 nn.MaxPool2d(2, 2) # → 19×16 # 全连接层64×19×16 19456 → 128 → num_classes self.fc1 nn.Linear(64 * 19 * 16, 128) self.dropout nn.Dropout(0.5) self.fc2 nn.Linear(128, num_classes) def forward(self, x): x F.relu(self.bn1(self.conv1(x))) x self.pool1(x) x F.relu(self.bn2(self.conv2(x))) x self.pool2(x) x F.relu(self.bn3(self.conv3(x))) x self.pool3(x) x x.view(x.size(0), -1) # flatten x F.relu(self.fc1(x)) x self.dropout(x) x self.fc2(x) return x # 初始化模型 model PalmCNN(num_classeslen(np.unique(y_train))) print(f模型参数量: {sum(p.numel() for p in model.parameters())}) # 输出约 48,000结构设计理由kernel_size5匹配掌褶宽度实测 37 像素padding2保尺寸MaxPool2d(2,2)三次下采样最终特征图 19×16足够容纳掌心三角区拓扑fc1128是经验值小于类别数 20 的 6 倍避免全连接层过拟合Dropout0.5在 fc 层卷积层不加小网络 dropout 易破坏特征提取。4.2 针对掌纹的数据增强策略旋转仿射亮度扰动通用增强如 RandomHorizontalFlip对掌纹无效——左右手掌纹不对称。我们只用三种增强且限制幅度from torchvision import transforms # 定义增强仅对训练集 train_transform transforms.Compose([ transforms.ToPILImage(), # 旋转±10° 内模拟拍摄角度偏差 transforms.RandomRotation(degrees(-10, 10), fill0), # 仿射变换轻微缩放平移模拟距离变化 transforms.RandomAffine( degrees0, # 不额外旋转 translate(0.1, 0.1), # 最大平移 10% scale(0.9, 1.1), # 缩放 90%~110% fillcolor0 ), # 亮度对比度模拟不同灯光 transforms.ColorJitter(brightness0.2, contrast0.2, saturation0, hue0), transforms.ToTensor(), # 标准化用训练集均值方差非 ImageNet transforms.Normalize(mean[0.45], std[0.25]) # 实测掌纹图均值≈0.45std≈0.25 ]) # 测试集仅 resize tensor normalize val_transform transforms.Compose([ transforms.ToPILImage(), transforms.Resize((154, 128)), transforms.ToTensor(), transforms.Normalize(mean[0.45], std[0.25]) ])为什么不用 Cutout掌纹关键区域三角区一旦被 cut模型无法恢复准确率暴跌。ColorJitter关闭saturation和hue因灰度图无色彩信息。4.3 训练循环与早停监控验证集 loss而非 accuracy小样本下 accuracy 波动大loss 更稳定。早停 patience 设为 15避免过早终止def train_cnn(model, train_loader, val_loader, epochs100, lr0.001): device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lrlr) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience10, verboseTrue ) best_val_loss float(inf) patience_counter 0 train_losses, val_losses [], [] for epoch in range(epochs): # 训练 model.train() running_loss 0.0 for inputs, labels in train_loader: inputs, labels inputs.to(device), labels.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() # 验证 model.eval() val_loss 0.0 with torch.no_grad(): for inputs, labels in val_loader: inputs, labels inputs.to(device), labels.to(device) outputs model(inputs) loss criterion(outputs, labels) val_loss loss.item() avg_train_loss running_loss / len(train_loader) avg_val_loss val_loss / len(val_loader) train_losses.append(avg_train_loss) val_losses.append(avg_val_loss) # 早停逻辑 if avg_val_loss best_val_loss: best_val_loss avg_val_loss patience_counter 0 torch.save(model.state_dict(), best_cnn.pth) # 保存最佳模型 else: patience_counter 1 if patience_counter 15: print(f早停于 epoch {epoch1}) break scheduler.step(avg_val_loss) if epoch % 10 0: print(fEpoch {epoch1}/{epochs}, Train Loss: {avg_train_loss:.4f}, Val Loss: {avg_val_loss:.4f}) return train_losses, val_losses # 调用训练 train_losses, val_losses train_cnn(model, train_loader, val_loader)关键参数patience15是实测平衡点——小于 10 易早停loss 波动正常大于 20 易过拟合。scheduler用ReduceLROnPlateau比StepLR更适应小数据波动。5. 避坑指南掌纹识别项目里 5 个让你重做的真实翻车现场做这个项目时我见过太多同学卡在最后一步模型跑通了但老师一句“你这数据怎么划分的”直接让分数掉档。以下是答辩前必须自查的 5 个坑每个都附现象、原因和救急方案。5.1 现象PCA 降维后 SVM 准确率 99%但测试集全是同一个人的图原因数据划分时未按“人”隔离。课程作业常要求每人提供 5 张掌纹图若随机划分训练/测试集同一人的图可能同时出现在两边造成数据泄露。解决用sklearn.model_selection.GroupShuffleSplit以“人ID”为 groupfrom sklearn.model_selection import GroupShuffleSplit gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(gss.split(X, y, groupsperson_ids)) # person_ids 是每张图对应的人ID列表5.2 现象CNN 训练 loss 下降快但验证 loss 一直不降甚至上升原因transforms.Normalize用了 ImageNet 的 mean/std[0.485,0.456,0.406]而掌纹图均值约 0.45std 约 0.25。标准化后输入分布偏移BN 层失效。解决计算自己数据集的均值方差# 计算训练集 mean/std train_mean np.mean([np.mean(img) for img in train_images]) train_std np.mean([np.std(img) for img in train_images]) # 用 train_mean, train_std 替换 Normalize 参数5.3 现象Gabor 滤波后图像全黑或全白原因cv2.filter2D输出类型为cv2.CV_32F但np.abs()后未归一化直接cv2.imwrite会因数据范围超出 [0,255] 而截断。解决必须用cv2.normalize(..., 0, 255, cv2.NORM_MINMAX)或手动 clipfiltered np.clip(np.abs(filtered), 0, 255).astype(np.uint8)5.4 现象OpenCVcv2.findContours找不到手掌轮廓mask 全黑原因cv2.inRange返回的是uint8二值图但cv2.findContours要求输入为uint8且非零像素为 255。若mask_skin中白色是 1 而非 255则轮廓检测失败。解决强制转换mask_clean (mask_clean 0).astype(np.uint8) * 255 # 确保白色2555.5 现象PyTorch DataLoader 报错Expected object of scalar type Float but got scalar type Byte原因cv2.imread默认读 BGRcv2.cvtColor转灰度后是uint8但ToTensor()要求输入为PIL.Image或numpy.ndarraywithfloat32。若直接torch.tensor(img)类型不匹配。解决在transforms.ToTensor()前确保图像是uint8ToTensor会自动除以 255 并转float32# 正确先转 PIL再 ToTensor transforms.ToPILImage()(img_gray) # img_gray 是 uint8 numpy array # 错误直接 tensor(img_gray) 会保留 uint86. 高分交付技巧如何用 3 张图说服老师“你真的懂掌纹识别”答辩时老师不会看你代码行数而是看三个瞬间1你能否解释为什么选这个预处理流程2你能否指出模型哪里可能出错3你能否用可视化证明特征有效性。以下是我压箱底的 3 张必交图每张都有明确目的和制作方法。6.1 图1预处理流程对比图左→右原图→肤色分割→掌心裁剪→Gabor 增强目的证明你理解“掌纹识别不是通用 CV 任务”每步都有生物医学依据。制作要点四子图严格等宽用plt.subplot(1,4,1)在每张图下方用小号字体标注物理意义“原图存在袖口干扰”、“肤色分割HSV 空间抑制背景”、“掌心裁剪聚焦三角区生命线/智慧线交汇”、“Gabor 增强突出 0°/45° 方向褶皱”关键在“掌心裁剪”图上用红色矩形框标出三角区并画箭头指向“生命线起点”。6.2 图2混淆矩阵热力图 顶部 Top-3 最易混淆类别目的暴露模型弱点体现你做了深度分析而非只报 accuracy。制作代码用sklearn.metrics.confusion_matrixfrom sklearn.metrics import confusion_matrix import seaborn as sns y_pred model.predict(X_test_pca) # PCASVM 情况 cm confusion_matrix(y_test, y_pred) plt.figure(figsize(10,8)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsclass_names, yticklabelsclass_names) plt.title(Confusion Matrix) plt.ylabel(True Label) plt.xlabel(Predicted Label) plt.savefig(confusion_matrix.png, dpi300, bbox_inchestight) # 找 Top-3 最易混淆对 cm_sum np.sum(cm, axis1, keepdimsTrue) cm_norm cm / cm_sum np.fill_diagonal(cm_norm, 0) # 掩盖对角线 top3 np.unravel_index(np.argsort(cm_norm.ravel())[-3:], cm_norm.shape) for i, (r,c) in enumerate(zip(*top3)): print(fTop-{i1} 混淆: {class_names[r]} → {class_names[c]} ({cm_norm[r,c]:.3f}))答辩话术“老师模型把 A 类错判为 B 类的概率是 12%我们检查了这 5 张错判图发现 A 类样本中 3 张有汗液反光覆盖了生命线B 类样本恰好也有类似反光——这说明 Gabor 滤波对反光敏感下一步可加 specular removal。”6.3 图3CNN 中间层特征图可视化Layer2 输出 Layer3 输出目的证明 CNN 学到了层次化特征不是黑匣子。制作方法用torchvision.utils.make_grid# 提取 layer2 和 layer3 的输出 layer2_output model.conv2(model.bn1(model.conv1(input_tensor))) # input_tensor 是 batch of 4 张图 layer3_output model.conv3(model.bn2(model.conv2(model.pool1(layer2_output)))) # 取前 8 个通道可视化 grid2 torchvision.utils.make_grid(layer2_output[:4, :8], nrow4, normalizeTrue) grid3 torchvision.utils.make_grid(layer3_output[:4, :8], nrow4, normalizeTrue) plt.figure(figsize(12,5)) plt.subplot(1,2,1) plt.imshow(grid2.permute(1,2,0).detach().cpu().numpy()) plt.title(Conv2 输出检测局部褶皱片段) plt.subplot(1,2,2) plt.imshow(grid3.permute(1,2,0).detach().cpu().numpy()) plt.title(Conv3 输出组合成掌褶走向) plt.savefig(cnn_features.png, dpi300, bbox_inchestight)关键提示normalizeTrue让特征图对比度拉满否则全灰。permute(1,2,0)是因为make_grid输出是 C×H×Wmatplotlib 要 H×W×C。最后说句实在话我当年交这个作业时文档里写了 3 行字——“PCA 主成分数经 LOO 交叉验证选定为 64因在此数值下验证集准确率方差最小±0.8%”。老师看到这句直接给了 95 分。不是因为代码多炫而是他相信你真的试过、调过、验证过。掌纹识别的“高分”从来不在模型多深而在你敢不敢把每一步的决策依据清清楚楚写在文档里。希望帮到你。本文还有配套的精品资源点击获取