如何使用Pytorch迅速實現Mnist資料及分類器
阿新 • • 發佈:2020-03-08
# 如何使用Pytorch迅速寫一個Mnist資料分類器
一段時間沒有更新博文,想著也該寫兩篇文章玩玩了。而從一個簡單的例子作為開端是一個比較不錯的選擇。本文章會手把手地教讀者構建一個簡單的Mnist(Fashion-Mnist同理)的分類器,並且會使用相對完整的Pytorch訓練框架,因此對於初學者來說應該會是一個方便入門且便於閱讀的文章。本文的程式碼來源於我剛學Pytorch時的小專案,可能在形式上會有引用一些github上的小程式碼。同時文風可能會和我之前看的一些外國部落格有點相近。
###### 本文適用物件: 剛入門的Pytorch新手,想要用Pytorch來完成作業的魚乾。
那麼就開始coding吧。
首先,你需要安裝好Python 3+,Pytorch 1.0+,我個人使用的是Pytorch1.4,我想1.0以上的版本都可以使用。
然後在想要的位置,新建一個main.py的檔案,然後就可以開始敲鍵盤了。
```python
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets
from torchvision import transforms
import torch.utils.data
import argparse
```
第一步自然是匯入相應的包。前面的都是Pytorch的包,最後一句匯入的argparse便於用來修改訓練的引數,這在Pytorch復現深度學習模型時非常常見。
```python
model_names = ['Net','Net1']
parser = argparse.ArgumentParser(description='PyTorch Mnist Training')
parser.add_argument('-a', '--arch', metavar='ARCH', default='Net', choices=model_names,
help='model archtecture: ' + '|'.join(model_names) + '(default:Net)')
parser.add_argument('--epochs', default=5, type=int, metavar='N', help='number of total epochs')
parser.add_argument('--momentum', default=0.9, type=float, metavar='M', help='momentum')
parser.add_argument('-b', '--batchsize', default=32, type=int, metavar='N', help='mini-batch size')
parser.add_argument('--lr', '--learning-rate', default=1e-2, type=float, metavar='LR', help='initial learning rata',
dest='lr')
args=vars(parser.parse_args())
```
第一行的`model_names`是一個list,用來儲存我們之後會實現的兩種網路結構的名字。然後我定義了一個argparse的物件,關於argparse可以自尋一些教程觀看,大概只需要知道可以從指令行輸入引數即可。在parser中又定義了arch(使用的網路),epochs(迭代輪次),momentum(梯度動量大小),batchsize(一次送入的圖片量大小),learning-rate(學習率)引數。之前的`model_name`也正是用在arch引數中,限定了網路框架將會從此二者中選擇其一。
```python
def main():
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
#parameter
batch_size = args["batchsize"]
lr = args["lr"]
momentum = args["momentum"]
num_epochs = args["epochs"]
```
主函式中,先定義cuda物件,便於使用gpu並行運算。在#parameter中,我們把一些從命令列中獲得的引數引入到相應的變數中,以便後續書寫。
```python
#prepare the dataset
mnist_data = datasets.MNIST("./mnist_data",train=True, download=True,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean=(0.13,),std=(0.31,))
]))
'''
mnist_data = datasets.FashionMNIST("./fashion_mnist_data", train=True, download=True,
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean=(,), std=(,))
]))
'''
train_loader = torch.utils.data.DataLoader(
mnist_data,batch_size=batch_size,shuffle=True, num_workers=1,pin_memory=True
)
test_loader = torch.utils.data.DataLoader(
mnist_data,batch_size=batch_size,shuffle=True, num_workers=1,pin_memory=True
)
```
之後將引入Pytorch中datasets包自帶的MNIST集,`download`引數設定為`True`,以便於本地沒有Mnist資料集時直接下載,之後會在當前目錄下建立一個mnist_data的資料夾以存放資料,。`transform`中的`transforms.ToTensor()`是用於將圖片形式的資料轉換成tensor型別,而`transforms.Normalize(mean=(0.13,),std=(0.31,))`則是將tensor型別的資料進行歸一化,這裡的0.13和0.31可以直接使用。如果你想要使用註釋中的FashionMNIST資料集則需要使用的是註釋中的內容,當然,mean和std需要另外求解。
之後,定義`train_loader`和`test_loader`,將資料集作為可迭代的物件使用。`shuffle=True`以實現亂序讀取資料,一般都會這麼設定。`num_workers`和`pin_memory`都會影響到資料讀取速度,前者是會在讀取時建立多少個程序,後者是影響到資料讀入到GPU中,一般來說,對於這個專案前者設定為1已經夠用,後者設定為True和False都不影響。在更大型的專案中,如果裝置較好,前者可以設定大一些。
```python
model = Net1().to(device) if args["arch"]=='Net1' else Net().to(device)
optimizer = torch.optim.SGD(model.parameters(),lr=lr,momentum=momentum)
criteon = nn.CrossEntropyLoss().to(device)
```
第一行是`model`例項化,並且會根據`args["arch"]`選擇是用`Net`還是`Net1`,`to(device)`會將`model`放置於`device`上執行。第二行定義了一個優化器,使用的是SGD,並且放入`model`的引數、學習率和動量大小。`criteon`定義損失函式,這邊使用的是交叉熵函式,這一損失函式在分類問題中十分常用。
```python
#train
for epoch in range(num_epochs):
train(model,device,train_loader,optimizer,epoch,criteon)
test(model,device,test_loader,criteon)
torch.save(model.state_dict(), "mnist_{}.pth".format(num_epochs))
```
這就是訓練過程,在其中又使用了`train`和`test`兩個函式(下面會說),根據`num_epochs`數目進行迴圈。迴圈結束後,`torch.save`將會把模型的引數`model.state_dict()`以`mnist_{}.pth`的形式存放到當前資料夾下。
```python
def train(model,device,train_loader,optimizer,epoch,criteon):
class_name = model.__class__.__name__
model.train()
loss = 0
for idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
pred = model(data)
if class_name == 'Net':
loss = F.nll_loss(pred, target)
elif class_name == 'Net1':
loss = criteon(pred, target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if idx % 100 == 0:
print("train epoch: {}, iteration: {}, loss: {}".format(epoch, idx, loss.item()))
```
這裡定義了`train`函式的訓練過程。class_name中存放了當前使用的模型名字。 `model.train()`開啟訓練模式。在`for idx, (data, target) in enumerate(train_loader):`取出當前資料集的idx,data和種類target。迴圈中,先把data和target放置於`device`上,`pred = model(data)`會進行一次前傳,獲得相應資料的預測種類`pred`。
對不同的模型,我採用了不同定義損失函式的方式,這裡需要結合下面的模型結構來看。`optimizer.zero_grad()`會將上輪累計的梯度清空,之後`loss.backward()`梯度反向傳播,利用`optimizer.step()`更新引數。而當`if idx % 100 == 0:`也就是迭代的資料批次到達100的倍數了,就會輸出相關資訊。
```python
def test(model,device,test_loader,criteon):
class_name = model.__class__.__name__
model.eval()
total_loss = 0 #caculate total loss
correct = 0
with torch.no_grad():
for idx, (data, target) in enumerate(test_loader):
data, target = data.to(device), target.to(device)
pred = model(data)
if class_name == 'Net':
total_loss += F.nll_loss(pred, target,reduction="sum").item()
elif class_name == 'Net1':
total_loss += criteon(pred, target).item()
correct += pred.argmax(dim=1).eq(target).sum().item()
total_loss /= len(test_loader.dataset)
acc = correct/len(test_loader.dataset)
print("Test loss: {}, Accuracy: {}%".format(total_loss,acc*100))
```
test函式總體結構類似,`model.eval()`將會把模型調整測試模式,`with torch.no_grad():`來宣告測試模式下不需要積累梯度資訊。`correct += pred.argmax(dim=1).eq(target).sum().item()`則是會計算出預測對了的數目,之後通過`total_loss`計算總誤差和`acc`計算準確率。
```python
class Net(nn.Module):
def __init__(self):
super(Net,self).__init__()
self.conv1 = nn.Conv2d(1, 20, kernel_size=5,stride=1)
self.conv2 = nn.Conv2d(20,50,kernel_size=5,stride=1)
self.fc1 = nn.Linear(4*4*50, 500)
self.fc2 = nn.Linear(500, 10)
def forward(self, x):
x = F.relu(self.conv1(x))
x = F.max_pool2d(x,2,2)
x = F.relu(self.conv2(x))
x = F.max_pool2d(x,2,2)
x = x.view(-1,4*4*50)
x = F.relu(self.fc1(x))
x = self.fc2(x)
x = F.log_softmax(x,dim=1)
return x
```
Net不過是一個具有兩個卷積層和兩個線性全連層的網路。`self.conv1 = nn.Conv2d(1, 20, kernel_size=5,stride=1)`表示conv1是一個接受1個channel的tensor輸出20個channel的tensor,且卷積大小為5,步長為1的卷積層。`self.fc1 = nn.Linear(4*4*50, 500)`則是接收一個`4 * 4 * 50`長的一維tensor並且輸出長為500的一維tensor。
前傳函式`forward`中,x作為輸入的資料,輸入後會通過` conv1-> relu->pooling->conv2->relu->pooling->view將多維tensor轉化成一維tensor->fc1->relu->fc2->log_softmax`來獲得最終的x的值。這裡就需要提train和test函式中的if和elif語句了。使用的時Net時,`loss = F.nll_loss(pred, target)`,這是因為`log_softmax`之後使用`nll_loss`和直接使用 `nn.CrossEntropyLoss()`是等效的,因此:
```python
class Net1(nn.Module):
def __init__(self):
super(Net1,self).__init__()
self.conv_unit=nn.Sequential(
nn.Conv2d(1, 20, kernel_size=5,stride=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2,stride=2),
nn.Conv2d(20,50,kernel_size=5,stride=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)
)
self.fc_unit=nn.Sequential(
nn.Linear(4*4*50, 500),
nn.ReLU(),
nn.Linear(500, 10)
)
def forward(self, x):
x = self.conv_unit(x)
x = x.view(-1,4*4*50)
x = self.fc_unit(x)
return x
```
Net1中最後並沒有使用`log_softmax`,是因為直接在train的過程中,使用了`nn.CrossEntropyLoss()`。此外,Net1和Net不同的地方也就是在結構中使用了`nn.Sequential()`來單元化卷積層和全連層。
```python
if __name__ == '__main__':
main()
```
之後就可以使用了!
在命令列中使用:
```cmd
$ python main.py
```
就會按照預設的引數訓練一個Mnist分類器了。
第三輪的效果:
```cmd
train epoch: 2, iteration: 1300, loss: 0.010509848594665527
train epoch: 2, iteration: 1400, loss: 0.0020529627799987793
train epoch: 2, iteration: 1500, loss: 0.0027058571577072144
train epoch: 2, iteration: 1600, loss: 0.010049819946289062
train epoch: 2, iteration: 1700, loss: 0.0352507084608078
train epoch: 2, iteration: 1800, loss: 0.009431719779968262
Test loss: 0.01797709318200747, Accuracy: 99.42833333333333%
```
如果希望檢視引數列表,則可以在命令列使用:
```cmd
$ python main.py -h
```
就會出現:
```cmd
usage: main.py [-h] [-a ARCH] [--epochs N] [--momentum M] [-b N] [--lr LR]
PyTorch Mnist Training
optional arguments:
-h, --help show this help message and exit
-a ARCH, --arch ARCH model archtecture: Net|Net1(default:Net)
--epochs N number of total epochs
--momentum M momentum
-b N, --batchsize N mini-batch size
--lr LR, --learning-rate LR
initial learning rata
```
於是如果想要使用Net1,lr為0.001的方式訓練,就可以按照這樣:
```cmd
$ python main.py -a Net1 --lr 0.001
```
第三輪結果:
```cmd
train epoch: 2, iteration: 1200, loss: 0.03096039593219757
train epoch: 2, iteration: 1300, loss: 0.060124486684799194
train epoch: 2, iteration: 1400, loss: 0.08865253627300262
train epoch: 2, iteration: 1500, loss: 0.13717596232891083
train epoch: 2, iteration: 1600, loss: 0.003894627094268799
train epoch: 2, iteration: 1700, loss: 0.06881710141897202
train epoch: 2, iteration: 1800, loss: 0.03184908628463745
Test loss: 0.0013615453808257978, Accuracy: 98.69666666666667%
```
至此,你獲得了一個Mnist訓練器的訓練