1. 程式人生 > >【小白學PyTorch】8 實戰之MNIST小試牛刀

【小白學PyTorch】8 實戰之MNIST小試牛刀

文章來自微信公眾號【機器學習煉丹術】。有什麼問題都可以諮詢作者WX:cyx645016617。想交個朋友佔一個好友位也是可以的~好友位快滿了不過。 參考目錄: [TOC] 在這個文章中,主要是來做一下MNIST手寫數字集的分類任務。這是一個基礎的、經典的分類任務。建議大家一定要跟著程式碼做一做,原始碼已經上傳到公眾號。 ## 1 探索性資料分析 一般在進行模型訓練之前,都要做一個數據集分析的任務。這個在英文中一般縮寫為**EDA**,也就是Exploring Data Analysis(好像是這個)。 **資料集獲取方面**,這裡本來是要使用之前課程提到的```torchvision.datasets.MNIST()```,但是考慮到這個torchvision提供的MNIST完整下載下來需要200M的大小,所以我就直接提供了MNIST的資料的CSV檔案(包含```train.csv```和```test.csv```),大小壓縮成```.zip```之後只有14M,程式碼就基於了這個資料檔案。 ### 1.1 資料集基本資訊 ```python import pandas as pd # 讀取訓練集 train_df = pd.read_csv('./MNIST_csv/train.csv') n_train = len(train_df) n_pixels = len(train_df.columns) - 1 n_class = len(set(train_df['label'])) print('Number of training samples: {0}'.format(n_train)) print('Number of training pixels: {0}'.format(n_pixels)) print('Number of classes: {0}'.format(n_class)) # 讀取測試集 test_df = pd.read_csv('./MNIST_csv/test.csv') n_test = len(test_df) n_pixels = len(test_df.columns) print('Number of test samples: {0}'.format(n_test)) print('Number of test pixels: {0}'.format(n_pixels)) ``` 輸出結果: ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c27c5f9236e343cd9f03e232c20997a2~tplv-k3u1fbpfcp-zoom-1.image) 訓練集有42000個圖片,每個圖片有784個畫素(所以變成圖片的話需要將784的畫素變成$28\times 28$),樣本總共有10個類別,也就是0到9。測試集中有28000個樣本。 ### 1.2 資料集視覺化 ```python # 展示一些圖片 import numpy as np from torchvision.utils import make_grid import torch import matplotlib.pyplot as plt random_sel = np.random.randint(len(train_df), size=8) data = (train_df.iloc[random_sel,1:].values.reshape(-1,1,28,28)/255.) grid = make_grid(torch.Tensor(data), nrow=8) plt.rcParams['figure.figsize'] = (16, 2) plt.imshow(grid.numpy().transpose((1,2,0))) plt.axis('off') plt.show() print(*list(train_df.iloc[random_sel, 0].values), sep = ', ') ``` 輸出結果有一個圖片: ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bf1248bc7277498e98c4bc2e6d7bcf82~tplv-k3u1fbpfcp-zoom-1.image) 以及一行列印: ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0afa41b84c554994907b63ba9c943cae~tplv-k3u1fbpfcp-zoom-1.image) 隨機挑選了8個樣本進行視覺化,然後打印出來的是樣本對應的標籤值。 ### 1.3 類別是否均衡 然後我們需要檢查一下訓練樣本中類別是否均衡,利用直方圖來檢查: ```python # 檢查類別是否不均衡 plt.figure(figsize=(8,5)) plt.bar(train_df['label'].value_counts().index, train_df['label'].value_counts()) plt.xticks(np.arange(n_class)) plt.xlabel('Class', fontsize=16) plt.ylabel('Count', fontsize=16) plt.grid('on', axis='y') plt.show() ``` 輸出影象: ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ea4756a640ee47a2ad298f5f6af74b11~tplv-k3u1fbpfcp-zoom-1.image) 基本沒毛病,是均衡的。 ## 2 訓練與推理 ### 2.1 構建dataset 我們可以重新寫一個python指令碼,首先還是匯入庫和讀取檔案: ```python import pandas as pd train_df = pd.read_csv('./MNIST_csv/train.csv') test_df = pd.read_csv('./MNIST_csv/test.csv') n_train = len(train_df) n_test = len(test_df) n_pixels = len(train_df.columns) - 1 n_class = len(set(train_df['label'])) ``` 然後構建一個Dataset,Dataset和Dataloader的知識前面的課程已經講過了,這裡直接構建一個: ```python import torch from torch.utils.data import Dataset,DataLoader from torchvision import transforms class MNIST_data(Dataset): def __init__(self, file_path, transform=transforms.Compose([transforms.ToPILImage(), transforms.ToTensor(), transforms.Normalize(mean=(0.5,), std=(0.5,))]) ): df = pd.read_csv(file_path) if len(df.columns) == n_pixels: # test data self.X = df.values.reshape((-1, 28, 28)).astype(np.uint8)[:, :, :, None] self.y = None else: # training data self.X = df.iloc[:, 1:].values.reshape((-1, 28, 28)).astype(np.uint8)[:, :, :, None] self.y = torch.from_numpy(df.iloc[:, 0].values) self.transform = transform def __len__(self): return len(self.X) def __getitem__(self, idx): if self.y is not None: return self.transform(self.X[idx]), self.y[idx] else: return self.transform(self.X[idx]) ``` 可以看到,這個dataset中,根據是否有標籤分成返回兩個不同的值。(訓練集的話,同時返回資料和標籤,測試集中僅僅返回資料)。 ```python batch_size = 64 train_dataset = MNIST_data('./MNIST_csv/train.csv', transform= transforms.Compose([ transforms.ToPILImage(), transforms.RandomRotation(degrees=20), transforms.ToTensor(), transforms.Normalize(mean=(0.5,), std=(0.5,))])) test_dataset = MNIST_data('./MNIST_csv/test.csv') train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True) test_loader = torch.utils.data.DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=False) ``` 關於這段程式碼: - 構建了一個train的dataset和test的dataset,然後再分別構建對應的dataloader - train_dataset中使用了隨機旋轉,因為這個函式是作用在PIL圖片上的,所以需要將資料先轉成PIL再進行旋轉,然後轉成Tensor做標準化,這裡標準化就隨便選取了0.5,有需要的可以做進一步的更改。 - 需要注意的是,轉成PIL之前的資料是numpy的格式,所以資料應該是$W\times H \times C$的形式,因為這裡是單通道影象,所以資料的shape為:(72000,28,28,1).(72000為樣本數量) - 像是旋轉、縮放等影象增強方法在訓練集中才會使用,這是增強模型訓練難度的操作,讓模型增加魯棒性;在測試集中常規情況是不使用旋轉、縮放這樣的影象增強方法的。**(訓練階段是讓模型學到內容,測試階段主要目的是提高預測的準確度,這句話感覺是廢話。。。)** ### 2.2 構建模型類 ```python import torch.nn as nn class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.features1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1) self.features = nn.Sequential( nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.Conv2d(32, 32, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.Conv2d(64, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2) ) self.classifier = nn.Sequential( nn.Dropout(p=0.5), nn.Linear(64 * 7 * 7, 512), nn.BatchNorm1d(512), nn.ReLU(inplace=True), nn.Dropout(p=0.5), nn.Linear(512, 512), nn.BatchNorm1d(512), nn.ReLU(inplace=True), nn.Dropout(p=0.5), nn.Linear(512, 10), ) for m in self.modules(): if isinstance(m, nn.Conv2d) or isinstance(m, nn.Linear): nn.init.xavier_uniform_(m.weight) elif isinstance(m, nn.BatchNorm2d): m.weight.data.fill_(1) m.bias.data.zero_() def forward(self, x): x = self.features1(x) x = self.features(x) x = x.view(x.size(0), -1) x = self.classifier(x) return x ``` 這個模型類整體來看中規中矩,都是之前講到的方法。**小測試:還記得xavier初始化時怎麼回事嗎?xavier初始化方法是一個非常常用的方法,在之前的文章中也詳細的推導了這個。** 之後呢,我們對模型例項化,然後給模型的引數傳到優化器中,然後設定一個學習率衰減的策略,**學習率衰減就是訓練的epoch越多,學習率就越低的這樣一個方法,在後面的文章中會詳細講述** 。 ```python import torch.optim as optim device = 'cuda' if torch.cuda.is_available() else 'cpu' model = Net().to(device) # model = torchvision.models.resnet50(pretrained=True).to(device) optimizer = optim.Adam(model.parameters(), lr=0.003) criterion = nn.CrossEntropyLoss().to(device) exp_lr_scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.1) print(model) ``` 執行結果自然是把整個模型打印出來了: ```python Net( (features1): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (features): Sequential( (0): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (1): ReLU(inplace=True) (2): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (3): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (4): ReLU(inplace=True) (5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) (6): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (7): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (8): ReLU(inplace=True) (9): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)) (10): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (11): ReLU(inplace=True) (12): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False) ) (classifier): Sequential( (0): Dropout(p=0.5, inplace=False) (1): Linear(in_features=3136, out_features=512, bias=True) (2): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (3): ReLU(inplace=True) (4): Dropout(p=0.5, inplace=False) (5): Linear(in_features=512, out_features=512, bias=True) (6): BatchNorm1d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (7): ReLU(inplace=True) (8): Dropout(p=0.5, inplace=False) (9): Linear(in_features=512, out_features=10, bias=True) ) ) ``` ### 2.3 訓練模型 ```python def train(epoch): model.train() for batch_idx, (data, target) in enumerate(train_loader): # 讀入資料 data = data.to(device) target = target.to(device) # 計算模型預測結果和損失 output = model(data) loss = criterion(output, target) optimizer.zero_grad() # 計算圖梯度清零 loss.backward() # 損失反向傳播 optimizer.step()# 然後更新引數 if (batch_idx + 1) % 50 == 0: print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format( epoch, (batch_idx + 1) * len(data), len(train_loader.dataset), 100. * (batch_idx + 1) / len(train_loader), loss.item())) exp_lr_scheduler.step() ``` 先定義了一個訓練一個epoch的函式,然後下面是訓練10個epoch的主函式程式碼。 ```python log = [] # 記錄一下loss的變化情況 n_epochs = 2 for epoch in range(n_epochs): train(epoch) # 把log化成折線圖 import matplotlib.pyplot as plt plt.plot(log) plt.show() ``` **注意注意**,這時候會報一個錯誤,我們來看一下,我詳細標註了我個人看報錯時候的一個習慣: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6ea52c43c678487bb05561235eede59f~tplv-k3u1fbpfcp-zoom-1.image) 這時候我大概可以猜到,因為我們這個圖片是灰度圖片,是單通道的,可能這個RandomRotate函式要求輸入圖片是3個通道的(這個官方API上也沒有細說),怎麼辦呢?完全可以直接在轉成PIL格式之前,把numpy的那個(72000,28,28,1)複製第四維度,變成(72000,28,28,3).但是這裡我想用上一節課教的一個方法```torchvision.transforms.GrayScale(num_output_channels)```, **活學活用嘛**. 所以把train_dataset那一塊改成: ```python train_dataset = MNIST_data('./MNIST_csv/train.csv', transform= transforms.Compose([ transforms.ToPILImage(), transforms.Grayscale(num_output_channels=3), transforms.RandomRotation(degrees=20), transforms.ToTensor(), transforms.Normalize(mean=(0.5,), std=(0.5,))])) test_dataset = MNIST_data('./MNIST_csv/test.csv', transform=transforms.Compose([ transforms.ToPILImage(), transforms.Grayscale(num_output_channels=3), transforms.ToTensor(), transforms.Normalize(mean=(0.5,), std=(0.5,))])) ``` 然後不要忘記把模型類中的第一個卷積層的輸入通道改成3哦~ ```python # self.features1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1) self.features1 = nn.Conv2d(3, 32, kernel_size=3, stride=1, padding=1) ``` 然後重新執行程式碼,發現可以正常訓練了,列印輸出的部分截圖如下: ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a1a89f944ac844af8f5a2c655d8b8f3f~tplv-k3u1fbpfcp-zoom-1.image) 然後看一下損失下降的情況,算是收斂了,訓練的epoch更多應該會更好: ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ea173a7801d245479475ef9750460085~tplv-k3u1fbpfcp-zoom-1.image) 發現訓練是收斂的。這裡需要注意的是,現在用全部的資料進行訓練,沒有使用驗證集的做法,是有可能過擬合情況出現的(但是這裡只是訓練了10個epoch應該不會過擬合),**更穩妥的做法是把資料分成訓練集和驗證機(可以是2:1,3:1,4:1)都可以,4:1比較常用,這也就是n-fold的方法。** 在之後的學習中會詳細介紹這個,不過這個知識點也不難,也可以自行查閱。 ### 2.4 推理預測 ```python def prediciton(data_loader): model.eval() test_pred = torch.LongTensor() for i, data in enumerate(data_loader): data = data.to(device) output = model(data) pred = output.cpu().data.max(1, keepdim=True)[1] test_pred = torch.cat((test_pred, pred), dim=0) return test_pred test_pred = prediciton(test_loader) ``` 類似trian,寫一個預測的函式,返回預測的值。然後像是在EDA中那樣,抽取測試集的8個數字,看看影象和預測結果的匹配情況 ```python from torchvision.utils import make_grid random_sel = np.random.randint(len(test_df), size=8) data = (test_df.iloc[random_sel,:].values.reshape(-1,1,28,28)/255.) grid = make_grid(torch.Tensor(data), nrow=8) plt.rcParams['figure.figsize'] = (16, 2) plt.imshow(grid.numpy().transpose((1,2,0))) plt.axis('off') plt.show() print(*list(test_pred[random_sel].numpy()), sep = ', ') ``` 輸出影象是: ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/361ef06d093442f89e47bf626fea7136~tplv-k3u1fbpfcp-zoom-1.image) 列印輸出: ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e02081708a4b42f5a247f6f0e0aded89~tplv-k3u1fbpfcp-zoom-1.image) OK了,恭喜你,完成了MNIST手寫數字集的