CV 醫學影像分類、分割、目標檢測,之【皮膚病分類】項目拆解
- 第1-12行:導入庫
- 第14-17行:讀取標簽文件
- 第19-21行:獲取疾病名稱
- 第23-26行:獲取圖片名列表
- 第28-35行:篩選有標簽的圖片
- 第38-43行:提取標簽
- 第47-51行:創建字典映射
- 第53-59行:創建類別ID映射
- 第61-70行:獲取篩選后圖片的標簽
- 第72-90行:定義數據變換
- 第92-107行:自定義數據集類
- 第114-120行:劃分訓練集測試集
- 第122-130行:創建數據加載器
- 第132-146行:可視化數據
- 第148-151行:加載預訓練模型
- 第153-156行:定義損失和優化器
- 第158-160行:GPU設置
- 第163-211行:訓練函數
- 第213-228行:訓練循環
- 第230-233行:繪制損失曲線
- 第235-260行:加載最佳模型測試
- 第262-305行:預測新圖片
- 核心流程總結
- 替換不同模型
?
目標:構建一個基于深度學習的皮膚病分類系統,能夠自動識別8種皮膚病類型(黑色素瘤、黑素細胞痣、基底細胞癌、光化性角化病、良性角化病、皮膚纖維瘤、血管病變、鱗狀細胞癌)
皮膚病數據集可以在阿里云天池里面搜索獲取。
def skin_classification_main():"""皮膚病分類系統主函數 - 領導式全局規劃"""# 階段1:標簽數據讀取與處理部門label_processor = LabelProcessor()skinDisease, pic_data, df_Key = label_processor.load_and_parse_labels()# 階段2:圖片文件篩選與整理部門 image_organizer = ImageOrganizer()image_organizer.copy_valid_images(pic_data)# 階段3:類別映射構建部門mapping_builder = MappingBuilder()skinLable_dic, class_to_id, id_to_class = mapping_builder.create_mappings(pic_data, df_Key, skinDisease)# 階段4:數據集構建部門dataset_builder = DatasetBuilder()train_loader, test_loader = dataset_builder.create_dataloaders(class_to_id, skinLable_dic)# 階段5:模型構建部門model_builder = ModelBuilder()model, loss_fn, optimizer = model_builder.build_training_components()# 階段6:訓練執行部門trainer = ModelTrainer()train_history = trainer.train_model(model, train_loader, test_loader, epochs=150)# 階段7:訓練結果可視化部門visualizer = TrainingVisualizer()visualizer.plot_training_curves(train_history)# 階段8:模型評估部門evaluator = ModelEvaluator()final_performance = evaluator.evaluate_best_model(model, test_loader)# 階段9:預測展示部門predictor = PredictionDemo()predictor.demo_batch_prediction(model, test_loader, id_to_class)# 階段10:單圖預測部門single_predictor = SingleImagePredictor()result = single_predictor.predict_single_image(model, image_path, id_to_class)return model, result
第1-12行:導入庫
from PIL import Image
問1: PIL是什么縮寫?
答1: Python Imaging Library(Python圖像處理庫)
問2: 為什么用from…import而不是import?
答2: 只導入需要的Image類,避免命名空間污染
問3: Image類能做什么?
答3: 打開、創建、修改、保存各種格式的圖片文件
import torch
問4: torch的核心是什么?
答4: 張量(Tensor)運算和自動微分
問5: 什么是張量?
答5: 多維數組,0維是標量,1維是向量,2維是矩陣,3維以上叫張量
from torch.utils import data
問6: utils是什么?
答6: utilities工具集,data是數據加載工具
問7: 為什么需要專門的數據加載工具?
答7: 批量加載、打亂順序、多線程預處理
import numpy as np
問8: numpy和torch的區別?
答8: numpy在CPU運算,torch可在GPU運算且支持自動求導
import pandas as pd
問9: pandas擅長什么?
答9: 表格數據處理,像Excel一樣操作數據
from torchvision import transforms
問10: transforms是做什么變換?
答10: 圖像預處理:裁剪、旋轉、歸一化等
import torchvision
問11: torchvision和torch的關系?
答11: torchvision是torch的計算機視覺擴展包
import matplotlib.pyplot as plt
問12: pyplot的plt是約定俗成嗎?
答12: 是的,社區約定,便于代碼交流
import torch.nn.functional as F
問13: functional和nn.Module的區別?
答13: functional是無狀態函數,Module是有參數的層
import torch.nn as nn
問14: nn代表什么?
答14: Neural Network,神經網絡模塊
from tqdm import tqdm
問15: tqdm是什么意思?
答15: 阿拉伯語"進展",用來顯示進度條
import os
import glob
import shutil
問16: 這三個都是文件操作,有什么區別?
答16: os基礎操作,glob模式匹配,shutil高級操作
第14-17行:讀取標簽文件
df=pd.read_table('./skin_label.txt',sep='\t',header='infer')
問17: ./是什么路徑?
答17: 當前目錄,相對路徑
問18: header='infer’是什么意思?
答18: 自動推斷第一行是否為列名
df_Key=np.array(df.iloc[:,1:])
問19: iloc和loc的區別?
答19: iloc用整數位置索引,loc用標簽索引
問20: 為什么轉成numpy數組?
答20: numpy運算更快,且后續要用argmax
df_Key.shape
問21: shape返回什么?
答21: 元組(行數, 列數),這里是(6000, 9)
第19-21行:獲取疾病名稱
skinDisease=df.columns[1:].to_numpy()
問22: columns是什么?
答22: DataFrame的列名,Index對象
問23: to_numpy()和values的區別?
答23: to_numpy()是新方法,values將被棄用
skinDisease
問24: 不加print為什么也能輸出?
答24: Jupyter/交互模式下,最后一個表達式自動顯示
第23-26行:獲取圖片名列表
pic_data=np.array(df.iloc[:,0])
問25: 第0列是什么?
答25: 圖片文件名列
pic_data=pic_data.tolist()
問26: 為什么要轉成list?
答26: 后面要用in判斷,list的in操作比array快
len(pic_data)
問27: len對不同對象的含義?
答27: list是元素個數,string是字符數,dict是鍵值對數
第28-35行:篩選有標簽的圖片
imgs=glob.glob('./data/skin_data/*.jpg')
問28: 是什么通配符?
答28: 匹配任意字符,.jpg匹配所有jpg文件
for im in imgs:
問29: im是什么類型?
答29: 字符串,完整文件路徑
im_name=im[17:-4]
問30: 為什么是17?
答30: './data/skin_data/'正好17個字符
問31: -4是什么?
答31: 倒數第4個字符開始,去掉’.jpg’
print(im_name)
問32: 這個print是調試用的?
答32: 是的,確認提取的文件名正確
if im_name in pic_data:
問33: in的時間復雜度?
答33: list是O(n),set是O(1)
print('E:/皮膚病分類/data/clear_skin_data/{}'.format(im_name))
問34: format和f-string的區別?
答34: format是舊語法,f-string(f’{im_name}')更簡潔
shutil.copy(im,'E:/皮膚病分類/data/clear_skin_data/{}.jpg'.format(im_name))
問35: copy和move的區別?
答35: copy保留原文件,move是剪切
第38-43行:提取標簽
skin_label=[]
問36: 為什么用列表不用數組?
答36: 要逐個append,列表動態增長更高效
index=np.argmax(df_Key,axis=1)
問37: argmax返回什么?
答37: 最大值的索引位置
問38: axis=1和axis=0的記憶方法?
答38: axis=0沿著行方向(↓),axis=1沿著列方向(→)
for i in index:skin_index=skinDisease[i]skin_label.append(skin_index)
問39: i是什么值?
答39: 0-8的整數,表示疾病類別索引
問40: append和extend的區別?
答40: append加單個元素,extend加多個元素
第47-51行:創建字典映射
skinLable_dic={}
lableSkin_dic={}
問41: 為什么建兩個字典?
答41: 雙向映射:圖片→標簽,標簽→圖片
for i in range(6000):skinLable_dic[pic_data[i]]=skin_label[i]
問42: range(6000)和range(len(pic_data))哪個好?
答42: range(len(pic_data))更好,自適應數據長度
第53-59行:創建類別ID映射
class_id = list(set(skinLable_dic.values()))
問43: set的作用?
答43: 去重,獲取唯一的疾病類別
問44: 為什么又轉回list?
答44: set無序,list可以索引訪問
id_to_class={}
class_to_id={}
for i,e in enumerate(class_id):class_to_id[e]=iid_to_class[i]=e
問45: enumerate返回什么?
答45: (索引, 元素)的元組
問46: 為什么需要數字ID?
答46: 神經網絡輸出是數字,不是字符串
第61-70行:獲取篩選后圖片的標簽
clear_img_path=glob.glob('./data/clear_skin_data/*.jpg')
問47: 這是第二次glob,為什么?
答47: 獲取篩選后的圖片路徑列表
clear_img_lable=[]
for img in clear_img_path:img_name=img[23:-4]
問48: 23是怎么算的?
答48: './data/clear_skin_data/'是23個字符
classes=skinLable_dic[img_name]ids=class_to_id[classes]clear_img_lable.append(ids)
問49: 這里做了幾次映射?
答49: 兩次:文件名→疾病名→數字ID
第72-90行:定義數據變換
train_transformer=transforms.Compose([ transforms.RandomHorizontalFlip(0.2),
問50: Compose是什么設計模式?
答50: 組合模式,串聯多個變換
問51: 0.2的概率是每張圖片獨立的嗎?
答51: 是的,每次調用獨立決定
transforms.RandomRotation(68),
問52: 為什么是68度不是90度?
答52: 可能是經驗值,避免過度旋轉丟失信息
transforms.RandomGrayscale(0.2),
問53: 灰度化的目的?
答53: 增強模型對顏色變化的魯棒性
transforms.Resize((128,128)),
問54: 為什么是128不是224?
答54: 平衡精度和速度,128夠用且更快
transforms.ToTensor(),
問55: Tensor和array的內存布局區別?
答55: Tensor是CHW(通道-高-寬),array通常是HWC
transforms.Normalize(mean=[0.5,0.5,0.5],std=[0.5,0.5,0.5])
問56: 這個歸一化后的范圍?
答56: (pixel-0.5)/0.5,從[0,1]變為[-1,1]
問57: 為什么要歸一化到[-1,1]?
答57: 零中心化,有助于梯度下降收斂
第92-107行:自定義數據集類
class Skindataset(data.Dataset):
問58: 為什么必須繼承Dataset?
答58: DataLoader需要調用固定接口
def __init__(self, img_paths, labels, transform):self.imgs = clear_img_pathself.labels = clear_img_lable
問59: 這里有bug嗎?
答59: 有!應該用參數img_paths和labels,不是全局變量
def __getitem__(self, index):
問60: 這個方法什么時候被調用?
答60: DataLoader迭代時自動調用
img = self.imgs[index]label = self.labels[index]pil_img = Image.open(img) data = self.transforms(pil_img)
問61: 每次都打開文件會不會慢?
答61: 會,但省內存,是時間換空間
return data, label
問62: 返回順序重要嗎?
答62: 重要,約定是(輸入, 標簽)
def __len__(self):return len(self.imgs)
問63: 為什么需要__len__?
答63: DataLoader需要知道數據集大小來計算批次數
第114-120行:劃分訓練集測試集
s = int(len(clear_img_path)*0.8)
問65: 為什么是0.8?
答65: 經驗值,80%訓練20%測試
問66: int()是向下取整嗎?
答66: 是的,截斷小數部分
train_imgs = clear_img_path[:s]
test_imgs = clear_img_path[s:]
問67: 這樣分割有什么問題?
答67: 沒打亂,可能有順序偏差
第122-130行:創建數據加載器
train = Skindataset(train_imgs, train_labels, train_transformer)
問68: 這里會調用__init__嗎?
答68: 會,創建實例時自動調用
dl_train = data.DataLoader(train,batch_size=32,shuffle=True)
問69: batch_size=32的含義?
答69: 每次送入網絡32張圖片
問70: 為什么要batch不要單張?
答70: 并行計算快,梯度估計更穩定
問71: shuffle=True的作用?
答71: 打亂順序,防止模型記住順序
第132-146行:可視化數據
img, label = next(iter(dl_train))
問72: iter()做了什么?
答72: 創建迭代器對象
問73: next()返回什么?
答73: 一個批次的(圖片張量, 標簽張量)
plt.rcParams['font.sans-serif'] = ['SimHei']
問74: SimHei是什么?
答74: 黑體字體,支持中文顯示
skin_chinese={'MEL':'黑色素瘤','NV':'黑素細胞痣',...}
問75: 字典的鍵為什么用英文縮寫?
答75: 與數據集標簽保持一致
for i,(img,label) in enumerate(zip(img[:8],label[:8])):
問76: zip的作用?
答76: 將兩個序列配對成元組
img=(img.permute(1,2,0).numpy()+1)/2
問77: permute(1,2,0)在做什么?
答77: CHW轉HWC,適配matplotlib
問78: +1再/2是為什么?
答78: [-1,1]恢復到[0,1]用于顯示
plt.subplot(2,4,i+1)
問79: i+1是因為?
答79: subplot索引從1開始,不是0
第148-151行:加載預訓練模型
model=torchvision.models.resnet50()
問80: resnet50的50指什么?
答80: 網絡深度,50層
問81: 預訓練是在什么數據上?
答81: ImageNet,1000類日常物體
model.fc.out_features=8
問82: 這樣直接賦值有用嗎?
答82: 沒用,應該替換整個fc層
問83: 正確寫法是什么?
答83: model.fc = nn.Linear(model.fc.in_features, 8)
第153-156行:定義損失和優化器
loss_fn=nn.CrossEntropyLoss()
問84: 交叉熵適合什么任務?
答84: 多分類任務
問85: 交叉熵的數學本質?
答85: 衡量兩個概率分布的差異
from torch.optim import lr_scheduler
問86: lr_scheduler沒用到?
答86: 是的,導入了但沒使用
optim=torch.optim.Adam(model.parameters(),lr=0.001)
問87: Adam是什么的縮寫?
答87: Adaptive Moment Estimation
問88: lr=0.001是經驗值嗎?
答88: 是的,Adam的常用默認值
第158-160行:GPU設置
if torch.cuda.is_available():model.to('cuda')
問89: cuda是什么?
答89: NVIDIA的并行計算平臺
問90: to(‘cuda’)做了什么?
答90: 把模型參數移到GPU內存
第163-211行:訓練函數
def fit(epoch, model, trainloader, testloader):correct = 0total = 0running_loss = 0
問91: 這三個變量追蹤什么?
答91: 正確數、總數、累積損失
model.train()
問92: train()模式改變什么?
答92: 啟用Dropout和BatchNorm的訓練行為
for x, y in tqdm(trainloader):
問93: tqdm包裝的效果?
答93: 顯示進度條
if torch.cuda.is_available():x, y = x.to('cuda'), y.to('cuda')
問94: 每個batch都要to(‘cuda’)嗎?
答94: 是的,數據在CPU,要移到GPU
y_pred = model(x)
問95: model(x)等價于?
答95: model.forward(x)
loss = loss_fn(y_pred, y)
問96: y_pred和y的形狀?
答96: y_pred是(32,8),y是(32,)
optim.zero_grad()
問97: 不清零會怎樣?
答97: 梯度累加,相當于更大的batch
loss.backward()
問98: backward()計算什么?
答98: loss對所有參數的偏導數
optim.step()
問99: step()的更新公式?
答99: 參數 = 參數 - 學習率 × 梯度
with torch.no_grad():
問100: no_grad()的作用?
答100: 禁用梯度計算,節省內存
y_pred = torch.argmax(y_pred, dim=1)
問101: argmax后的形狀?
答101: 從(32,8)變為(32,)
correct += (y_pred == y).sum().item()
問102: .item()的作用?
答102: 將單元素張量轉為Python數值
epoch_loss = running_loss / len(trainloader.dataset)
問103: 為什么除以dataset長度不是batch數?
答103: 獲得每個樣本的平均損失
model.eval()
問104: eval()改變什么?
答104: 關閉Dropout,BatchNorm用運行均值
torch.save(static_dict,'./data/resnet_Chepoint/{}_train_acc_{}_test_acc_{}.pth'.format(epoch,round(epoch_acc, 3),round(epoch_test_acc,3)))
問105: .pth是什么格式?
答105: PyTorch的模型文件格式
問106: state_dict包含什么?
答106: 所有層的權重和偏置參數
第213-228行:訓練循環
epochs = 150
問107: 150輪夠嗎?
答107: 看驗證集性能,可能過擬合
for epoch in range(epochs):epoch_loss, epoch_acc, epoch_test_loss, epoch_test_acc = fit(...)
問108: 每輪都保存模型?
答108: 是的,可以選最好的
第230-233行:繪制損失曲線
plt.plot(range(1, epochs+1), train_loss, label='train_loss')
問109: range從1開始?
答109: 讓橫軸從1開始,更直觀
第235-260行:加載最佳模型測試
model.load_state_dict(torch.load('./data/resnet_Chepoint/143_train_acc_0.904_test_acc_0.981.pth'))
問110: 為什么選143輪的?
答110: 測試準確率最高(98.1%)
第262-305行:預測新圖片
t_img='C:/Users/MSI-NB/AppData/Local/Temp/vasssssss.jpeg'
問111: 這是什么路徑?
答111: Windows臨時文件夾的圖片
img_tensor=test_transformer(img)
img_tensor=img_tensor.unsqueeze(0)
問112: unsqueeze(0)做什么?
答112: 增加batch維度,(3,128,128)→(1,3,128,128)
pre=torch.argmax(out,axis=1).cpu().numpy()[0]
問113: .cpu()為什么需要?
答113: GPU張量不能直接轉numpy
id_to_class[pre]
問114: 最終輸出什么?
答114: 疾病類別名稱,如’MEL’(黑色素瘤)
核心流程總結
這個項目的本質是一個遷移學習流程:
- 數據準備:圖片+標簽 → 數字化
- 數據增強:翻轉旋轉 → 泛化能力
- 特征提取:ResNet50 → 圖像特征
- 微調分類:1000類 → 8類疾病
- 迭代優化:梯度下降 → 最小損失
- 模型應用:新圖片 → 疾病診斷
替換不同模型
如果只想快速替換模型,最少只需改2處:
- 不同模型架構不同,最后一層名稱不同
- 輸入尺寸要求可能不同
除了最后一層,還有什么要改? 輸入尺寸要求不同!
但手動修改,還是容易出現 BUG。
代碼中哪些地方依賴于ResNet50?
答1: 主要是兩處:
model = torchvision.models.resnet50() # 第148行
model.fc.out_features = 8 # 第150行,這行還有bug
# ResNet系列
model = torchvision.models.resnet50()
model.fc = nn.Linear(model.fc.in_features, 8) # fc層# VGG系列
model = torchvision.models.vgg16()
model.classifier[6] = nn.Linear(4096, 8) # classifier是個Sequential# DenseNet系列
model = torchvision.models.densenet121()
model.classifier = nn.Linear(model.classifier.in_features, 8) # classifier層# EfficientNet系列
model = torchvision.models.efficientnet_b0()
model.classifier[1] = nn.Linear(model.classifier[1].in_features, 8) # classifier[1]# MobileNet系列
model = torchvision.models.mobilenet_v2()
model.classifier[1] = nn.Linear(model.classifier[1].in_features, 8) # classifier[1]
更優雅的解決方案:使用timm庫
import timmdef get_model_timm(model_name='resnet50', num_classes=8, pretrained=True):"""問5:timm是什么?答5:PyTorch Image Models,包含700+預訓練模型問6:為什么用timm更好?答6:統一接口,自動處理最后一層"""model = timm.create_model(model_name,pretrained=pretrained,num_classes=num_classes # 自動替換最后一層!)return model# 使用示例 - 可以用任何模型!
model = get_model_timm('resnet50', num_classes=8)
model = get_model_timm('efficientnet_b7', num_classes=8)
model = get_model_timm('vit_base_patch16_224', num_classes=8) # Vision Transformer!