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

引言

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

開始

  在只會一些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):用於重新評估模型並返回損失的一個閉包