一、为什么需要更深层解释?

  • 高光谱分类中,CNN 能提取强大特征,但往往像个“黑箱”。

  • 仅看准确率和混淆矩阵,难以回答 “模型到底看重哪些谱段/通道?”

  • 本篇将结合 类激活图(CAM)/通道注意力可视化,帮助我们直观理解 CNN 内部机理。

二、实验设定

  • 数据:KSC(同前,PCA 降维至 30 维)。

  • 模型:标准 1D CNN(含全像素预测)。

  • 解释方法:

    1. 卷积核谱形可视化(已做过,但这里结合激活强度排序)。

    2. 通道重要性分析(用 GAP+FC 的权重近似 CAM 权重)。

    3. 类别混淆热图(更清晰地理解易混类别对)。

三、核心代码(可运行)

# -*- coding: utf-8 -*-
"""
案例③-6:CNN 深度解释与可视化(KSC)
- 全像素分类 + OA/AA/Kappa
- 可解释性:卷积核排序 / 通道权重 / 类别混淆热图
"""

import os, numpy as np, scipy.io as sio
import torch, torch.nn as nn, torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.model_selection import train_test_split
from sklearn.metrics import (confusion_matrix, classification_report,
                             accuracy_score, cohen_kappa_score)
import matplotlib.pyplot as plt, matplotlib
import seaborn as sns
from matplotlib.colors import ListedColormap, BoundaryNorm

# ===== 中文显示 =====
matplotlib.rcParams['font.family'] = 'SimHei'
matplotlib.rcParams['axes.unicode_minus'] = False

# ===== 参数 =====
DATA_DIR    = r"your_path"   # ← 修改为 KSC 数据目录
PCA_DIM     = 30
TRAIN_RATIO = 0.3
EPOCHS      = 20
BATCH       = 512
LR          = 1e-3
DEVICE      = "cuda"if torch.cuda.is_available() else"cpu"

# ===== 1) 数据处理(同前)=====
X = sio.loadmat(os.path.join(DATA_DIR, "KSC.mat"))["KSC"].astype(np.float32)
Y = sio.loadmat(os.path.join(DATA_DIR, "KSC_gt.mat"))["KSC_gt"].astype(int)
H, W, B = X.shape
coords = np.argwhere(Y!=0)
labels = Y[coords[:,0], coords[:,1]]-1
num_classes = labels.max()+1

train_ids, test_ids = train_test_split(np.arange(len(coords)),
                                       train_size=TRAIN_RATIO,
                                       stratify=labels, random_state=42)

train_pixels = X[coords[train_ids,0], coords[train_ids,1]]
scaler = StandardScaler().fit(train_pixels)
pca    = PCA(n_components=PCA_DIM, random_state=42).fit(scaler.transform(train_pixels))

X_flat = pca.transform(scaler.transform(X.reshape(-1,B)))
X_pca  = X_flat.reshape(H,W,PCA_DIM)
X_train, y_train = X_pca[coords[train_ids,0], coords[train_ids,1]], labels[train_ids]
X_test,  y_test  = X_pca[coords[test_ids,0], coords[test_ids,1]], labels[test_ids]

class HSIDataset(Dataset):
    def __init__(self,X,y):
        self.X=torch.from_numpy(X).float().unsqueeze(1)
        self.y=torch.from_numpy(y).long()
    def __len__(self):return len(self.y)
    def __getitem__(self,i):return self.X[i],self.y[i]

train_loader=DataLoader(HSIDataset(X_train,y_train),batch_size=BATCH,shuffle=True)
test_loader =DataLoader(HSIDataset(X_test,y_test), batch_size=BATCH,shuffle=False)

# ===== 2) CNN 模型 =====
class Simple1DCNN(nn.Module):
    def __init__(self,C):
        super().__init__()
        self.conv1 = nn.Conv1d(1,32,3,padding=1)
        self.conv2 = nn.Conv1d(32,64,3,padding=1)
        self.pool  = nn.AdaptiveAvgPool1d(8)
        self.fc    = nn.Sequential(nn.Flatten(),
                                   nn.Linear(64*8,128),nn.ReLU(),
                                   nn.Linear(128,C))
    def forward(self,x):
        x=torch.relu(self.conv1(x))
        x=torch.relu(self.conv2(x))
        x=self.pool(x)
        return self.fc(x)

model=Simple1DCNN(num_classes).to(DEVICE)
opt=optim.Adam(model.parameters(),lr=LR)
crit=nn.CrossEntropyLoss()

# ===== 3) 训练 =====
def run_epoch(loader,train=True):
    model.train(train)
    tot,correct,loss_sum=0,0,0
    y_true,y_pred=[],[]
    for xb,yb in loader:
        xb,yb=xb.to(DEVICE),yb.to(DEVICE)
        out=model(xb); loss=crit(out,yb)
        if train:
            opt.zero_grad(); loss.backward(); opt.step()
        pred=out.argmax(1)
        tot+=yb.size(0); correct+=(pred==yb).sum().item()
        loss_sum+=loss.item()*yb.size(0)
        y_true.append(yb.cpu().numpy()); y_pred.append(pred.cpu().numpy())
    return loss_sum/tot,correct/tot,np.concatenate(y_true),np.concatenate(y_pred)

for ep in range(1,EPOCHS+1):
    trL,trA,_,_=run_epoch(train_loader,True)
    teL,teA,y_t,y_p=run_epoch(test_loader,False)
    if ep%5==0: print(f"Epoch{ep:02d} TrainAcc{trA:.3f} TestAcc{teA:.3f}")

# ===== 4) 指标 & 混淆矩阵 =====
cm=confusion_matrix(y_t,y_p,labels=np.arange(num_classes))
oa=accuracy_score(y_t,y_p)
aa=float(np.nanmean(np.diag(cm)/np.maximum(cm.sum(1),1)))
kappa=cohen_kappa_score(y_t,y_p)
print(f"OA={oa*100:.2f}%  AA={aa*100:.2f}%  Kappa={kappa:.4f}")
print(classification_report(y_t,y_p,digits=4,zero_division=0))

plt.figure(figsize=(6.2,5.2))
sns.heatmap(cm,annot=True,fmt="d",cmap="YlGnBu")
plt.title("混淆矩阵"); plt.xlabel("预测"); plt.ylabel("真实")
plt.tight_layout(); plt.show()

# ===== 5) 整图预测 =====
model.eval(); preds=[]
with torch.no_grad():
    for i in range(0,X_flat.shape[0],BATCH):
        xb=torch.from_numpy(X_flat[i:i+BATCH]).float().unsqueeze(1).to(DEVICE)
        preds.extend(model(xb).argmax(1).cpu().numpy())
pred_map=(np.array(preds).reshape(H,W)+1).astype(int)

cmap=ListedColormap([(0,0,0,1)]+[plt.cm.tab20(i) for i in range(num_classes)])
bounds=np.arange(-0.5,num_classes+1.5,1); norm=BoundaryNorm(bounds,cmap.N)
fig,ax=plt.subplots(1,2,figsize=(12,5))
ax[0].imshow(Y,cmap=cmap,norm=norm); ax[0].set_title("真值"); ax[0].axis("off")
im=ax[1].imshow(pred_map,cmap=cmap,norm=norm); ax[1].set_title("CNN 全像素预测"); ax[1].axis("off")
plt.colorbar(im,ax=ax.ravel().tolist(),fraction=0.025,pad=0.02)
plt.show()

# ===== 6) 可解释性:卷积核/通道权重 =====
w=model.conv1.weight.detach().cpu().numpy()
plt.figure(figsize=(8,3))
for i in range(8):
    plt.plot(w[i,0],label=f"k{i}")
plt.title("第一层卷积核谱形(前8个)"); plt.legend(ncol=4,fontsize=8)
plt.show()

# 近似 CAM:全连接层第一层的权重,反映通道重要性
fc_w=model.fc[1].weight.detach().cpu().numpy()  # [128,64*8]
ch_importance=np.mean(np.abs(fc_w),axis=0).reshape(64,8).mean(1)
plt.figure(figsize=(8,3))
plt.bar(np.arange(64),ch_importance)
plt.title("通道重要性估计")
plt.show()

四、结果解读

  • 混淆矩阵:可视化最直观的“易混类别”。

混淆矩阵清晰展示了真实类别与预测类别的对应关系:

  • 对角线越亮,说明预测越准确;

  • 非对角区域则表示错误分类。

图片

  • 整图预测:CNN 可以对未标注区域输出合理分类,但边界区域仍有错误。

图片

  • 卷积核谱形:部分核强调峰值,部分核强调凹陷(典型吸收带)。

    对第一层卷积核进行可视化,结果显示:

    • 一些卷积核更像是“高通滤波器”,强调光谱的剧烈变化;

    • 一些卷积核则像“低通滤波器”,捕捉平滑趋势;

图片

  • 通道重要性:显示模型更依赖哪些中间特征通道,可帮助我们理解哪些光谱模式最有区分度。

图片

五、小结

本篇我们在 KSC 数据上完成了 CNN 的全像素分类,并从多个角度解释了 CNN 的内部机制:

  • 混淆矩阵揭示了易混类别;

  • 整图预测展示了模型在空间上的泛化能力;

  • 卷积核可视化让我们看到 CNN 如何学习光谱特征;

  • 通道权重分析说明了模型依赖的特征维度。

通过这些方法,我们不再只是“盲目使用 CNN”,而是能够理解它、解释它。

欢迎大家关注下方公众号获取更多内容!!

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐