影像分類學習:X光胸片診斷識別----遷移學習

騎碼的佳俊發表於2021-01-26

引言

  剛進入人工智慧實驗室,不知道是在學習機器學習還是深度學習,想來他倆可能是一個東西,查閱之後才知道這是兩個領域,或許也有些交叉,畢竟我也剛接觸,不甚瞭解。
  在我還是個純度小白之時,寫下這篇文章,希望後來同現在的我一樣,剛剛涉足此領域的同學能夠在這,跨越時空,在小白與小白的交流中得到些許幫助。

開始

  在只會一些python語法,其他啥都沒有,第一週老師講了一些機器學習和深度學習的瞭解性內容,就給了一個實驗,讓我們一週內弄懂並跑出來,其實老師的程式碼已經完成了,我們可以直接放進Pycharm裡跑出來,但是程式碼細節並沒有講,俗話說師傅領進門,修行在個人。那就從最基本的開始,把這個程式碼弄懂,把實驗理解。
  下面我會先把整個程式碼貼出來,之後一步一步去分析每個模組,每個函式的作用。

說明

  本實驗來自此處部落格,我們的實驗也是基於這個部落格的內容學習的。

一、資料集

資料來源於kaggle,可在此連結自行下載

二、執行程式碼

三、解析

從頭開始,先來看這段程式碼(註釋進行解釋):

# 進行一系列資料增強,然後生成訓練(train)、驗證(val)、和測試(test)資料集
data_transforms = {
    'train': transforms.Compose([
        transforms.RandomResizedCrop(input_size),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    'val': transforms.Compose([
        transforms.Resize(input_size),
        transforms.CenterCrop(input_size),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    'test': transforms.Compose([
        transforms.Resize(input_size),
        transforms.CenterCrop(input_size),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
}

這是一個字典資料型別,其中鍵分別為:train、val、test對應資料集裡的三個資料夾train、val、test
鍵train對應的值是一個操作transfroms.Compose( [列表] )
引數為一個列表,列表中的元素為四個操作:

transforms.RandomResizedCrop()
transforms.RandomHorizontalFlip()
transforms.ToTensor
transforms.ToTensortransforms.Normalize()

transforms在torchvision中,一個影像處理包,可以通過它呼叫一些影像處理函式,對影像進行處理

transfroms.Compose( [列表] ):此函式存在於torchvision.transforms中,一般用Compose函式把多個步驟整合到一起

transforms.RandomResizedCrop(數字):將給定影像隨機裁剪為不同的大小和寬高比,然後縮放所裁剪得到的影像為制定的大小;(即先隨機採集,然後對裁剪得到的影像縮放為同一大小)
例如:

transforms.RandomHorizontalFlip():以給定的概率隨機水平旋轉給定的PIL的影像,預設為0.5;
例如:

transforms.ToTensor:將給定影像轉為Tensor(一個資料型別,類似有深度的矩陣)
例如:

transforms.ToTensortransforms.Normalize():歸一化處理
例如:
函式詳細內容請檢視此處文章

可以看出,這一塊的程式碼就是定義一種操作集合,將一張圖片進行剪裁、旋轉、轉為一種資料、資料歸一化

繼續往下看,相應的解釋都在註釋中

image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x), data_transforms[x]) for x in
                  ['train', 'val', 'test']}
dataloaders_dict = {
    x: torch.utils.data.DataLoader(image_datasets[x], batch_size=batch_size, shuffle=True, num_workers=0) for x in
    ['train', 'val', 'test']}

首先是資料匯入部分,這裡採用官方寫好的torchvision.datasets.ImageFolder介面實現資料匯入。這個介面需要你提供影像所在的資料夾
x是字典的鍵,從後面的for迭代的範圍中獲取,有'train', 'val', 'test'三個值
datasets.ImageFolder(os.path.join(data_dir, x), data_transforms[x])高亮部分是資料集的資料夾路徑,找到檔案後,確定x,在執行第二個引數data_transforms[x],把x進行一些列處理
前面torchvision.datasets.ImageFolder只是返回列表,列表是不能作為模型輸入的(我也不知道為什麼),因此在PyTorch中需要用另一個類來封裝列表,那就是:torch.utils.data.DataLoader
torch.utils.data.DataLoader類可以將列表型別的輸入資料封裝成Tensor資料格式,以備模型使用。

好,我們繼續往下看

# 定義一個檢視圖片和標籤的函式
def imshow(inp, title=None):
    # transpose(0,1,2),0是x軸,1是y軸,2是z軸,由(0,1,2)變為(1,2,0)就是x和z軸先交換,x和y軸再交換
    inp = inp.numpy().transpose((1, 2, 0))
    mean = np.array([0.485, 0.456, 0.406])  # 建立一個陣列[0.485, 0.456, 0.406]
    std = np.array([0.229, 0.224, 0.225])  # 同樣也是建立一個陣列
    inp = std * inp + mean  # 調整影像尺寸大小等
    inp = np.clip(inp, 0, 1)  # 小於0的都為0,大於1的都為1,之間的不變
    plt.imshow(inp)  # 設定影像為灰色
    if title is not None:  # 如果影像有標題則顯示標題
        plt.title(title) # 設定影像標題
    plt.pause(0.001)  # 視窗繪製後停留0.001秒


imgs, labels = next(iter(dataloaders_dict['train']))  # 自動往下迭代引數物件
out = torchvision.utils.make_grid(imgs[:8])  # 將8個圖拼成一張圖片
classes = image_datasets['test'].classes  # 每個影像的檔名
# out是一個8個圖片拼成的長圖,經過imshow()處理後附加標題(圖片檔名的前8個字母)輸出
# imshow(out, title=[classes[x] for x in labels[:8]])

輸出後,在IDE中是這樣的(右上角):

好,想在繼續往下走

下面呢給出了四個訓練模型,實戰中我們只需要挑其中一個進行訓練就好,其他的模型要註釋掉,下面程式碼上四個模型我都會分析

  

# inception------------------------------------------------------inception模型,有趣的是它可以翻譯為盜夢空間
model = models.inception_v3(pretrained=True)  
# inception_v3是一個預訓練模型, pretrained=True執行後會把模型下載到我們的電腦上
model.aux_logits = False  # 是否給模型建立輔助,具體增麼個輔助太複雜,請觀眾老爺們自行谷歌
num_fc_in = model.fc.in_features  # 提取fc層固定的引數
#  改變全連線層,2分類問題,out_features = 2
model.fc = nn.Linear(num_fc_in, num_classes)  # 修改fc層引數為num_classes = 4(最前面前面定義了)




# alexnet--------------------------------------------------------alexnet模型
model = models.alexnet(pretrained=True)  # alexnet是一個預訓練模型, pretrained=True執行後會把模型下載到我們的電腦上
num_fc_in = model.classifier[6].in_features  # 提取fc層固定的引數
model.fc = torch.nn.Linear(num_fc_in, num_classes)  # 修改fc層引數為num_classes = 4(最前面前面定義了)
model.classifier[6] = model.fc
#將圖層初始化為model.fc
#相當於model.classifier[6] = torch.nn.Linear(num_fc_in, num_classes)




# 建立VGG16遷移學習模型------------------------------------------------vgg16模型
model = torchvision.models.vgg16(pretrained=True)# vgg16是一個預訓練模型, pretrained=True執行後會把模型下載到我們的電腦上
# 先將模型引數改為不可更新
for param in model.parameters():
    param.requires_grad = False
# 再更改最後一層的輸出,至此網路只能更改該層引數
model.classifier[6] = nn.Linear(4096, num_classes)
model.classifier = torch.nn.Sequential(  # 修改全連線層 自動梯度會恢復為預設值
    torch.nn.Linear(25088, 4096),
    torch.nn.ReLU(),
    torch.nn.Dropout(p=0.5),
    torch.nn.Linear(4096, 4096),
    torch.nn.Dropout(p=0.5),
    torch.nn.Linear(4096, num_classes))




# resnet18---------------------------------------------------------------resnet模型(和前幾個模型差不多,自己腦部吧)
model = models.resnet18(pretrained=True)
#  全連線層的輸入通道in_channels個數
num_fc_in = model.fc.in_features
#  改變全連線層,2分類問題,out_features = 2
model.fc = nn.Linear(num_fc_in, num_classes)

繼續,解釋都在註釋裡了

# 定義訓練函式
def train_model(model, dataloaders, criterion, optimizer, mundde_epochs=25):
    since = time.time()  # 返回當前時間的時間戳(1970紀元後經過的浮點秒數)
    # state_dict變數存放訓練過程中需要學習的權重和偏執係數,state_dict作為python的字典物件將每一層的引數對映成tensor張量,
    # 需要注意的是torch.nn.Module模組中的state_dict只包含卷積層和全連線層的引數
    best_model_wts = copy.deepcopy(model.state_dict())  # copy是一個複製函式
    best_acc = 0.0
    # 下面這個迭代就是一個進度條的輸出,從0到9顯示進度
    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1))
        print('-' * 10)
        # 下面這個迭代,範圍就兩個'train', 'val',對應不執行不同的訓練模式
        for phase in ['train', 'val']:

            if phase == 'train':
                model.train()
            else:
                model.eval()

            running_loss = 0.0
            running_corrects = 0.0


            for inputs, labels in dataloaders[phase]:
                # 下面這行程式碼的意思是將所有最開始讀取資料時的tensor變數copy一份到device所指定的GPU或CPU上去,
                # 之後的運算都在GPU或CPU上進行
                inputs, labels = inputs.to(device), labels.to(device)

                optimizer.zero_grad()  # 模型梯度設為0

                # 接下來所有的tensor運算產生的新的節點都是不可求導的
                with torch.set_grad_enabled(phase == 'train'):
                    outputs = model(inputs)  # output等於把inputs放到指定裝置上去運算
                    loss = criterion(outputs, labels)  # loss為outputs和labels的交叉熵損失
                    # 舉例:output = torch.max(input, dim)
                    # 輸入
                    # input是softmax函式輸出的一個tensor
                    # dim是max函式索引的維度0 / 1,0是每列的最大值,1是每行的最大值
                    #  輸出
                    # 函式會返回兩個tensor,第一個tensor是每行的最大值,softmax的輸出中最大的是1,所以第一個tensor是全1的tensor;
                    # 第二個tensor是每行最大值的索引。
                    _, preds = torch.max(outputs, 1)
                    if phase == 'train':
                        loss.backward()  # 反向傳播計算得到每個引數的梯度值
                        optimizer.step()  # 通過梯度下降執行一步引數更新

                running_loss += loss.item() * inputs.size(0)
                running_corrects += (preds == labels).sum().item()
            epoch_loss = running_loss / len(dataloaders[phase].dataset)
            epoch_acc = running_corrects / len(dataloaders[phase].dataset)

            print('{} loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))

            if phase == 'val' and epoch_acc > best_acc:
                best_acc = epoch_acc
                best_model_wts = copy.deepcopy(model.state_dict())
        print()
    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
    print('Best val Acc: {:.4f}'.format(best_acc))

    model.load_state_dict(best_model_wts)
    return model

繼續往下看

# 定義優化器和損失函式
model = model.to(device)  # 前面解釋過了
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
# optimizer = optim.Adam(model.classifier.parameters(), lr=0.0001)

# sched = optim.lr_scheduler.StepLR(optimizer, step_size=4, gamma=0.1)
criterion = nn.CrossEntropyLoss()  # 交叉熵損失函式

引用此文章
class torch.optim.SGD(params, lr=, momentum=0, dampening=0, weight_decay=0, nesterov=False)[source]
實現隨機梯度下降演算法(momentum可選)。
引數:
params (iterable) – 待優化引數的iterable或者是定義了引數組的dict
lr (float) – 學習率
momentum (float, 可選) – 動量因子(預設:0)
weight_decay (float, 可選) – 權重衰減(L2懲罰)(預設:0)
dampening (float, 可選) – 動量的抑制因子(預設:0)
nesterov (bool, 可選) – 使用Nesterov動量(預設:False)
例子:

optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
optimizer.zero_grad()
loss_fn(model(input), target).backward()
optimizer.step()  

class torch.optim.Adam(params, lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0)
adam演算法來源:Adam: A Method for Stochastic Optimization
Adam(Adaptive Moment Estimation)本質上是帶有動量項的RMSprop,它利用梯度的一階矩估計和二階矩估計動態調整每個引數的學習率。它的優點主要在於經過偏置校正後,每一次迭代學習率都有個確定範圍,使得引數比較平穩。
其公式如下:

引數:

params(iterable):可用於迭代優化的引數或者定義引數組的dicts。
lr (float, optional) :學習率(預設: 1e-3)
betas (Tuple[float, float], optional):用於計算梯度的平均和平方的係數(預設: (0.9, 0.999))
eps (float, optional):為了提高數值穩定性而新增到分母的一個項(預設: 1e-8)
weight_decay (float, optional):權重衰減(如L2懲罰)(預設: 0)
step(closure=None)函式:執行單一的優化步驟
closure (callable, optional):用於重新評估模型並返回損失的一個閉包 

相關文章