Pytorch快速入门笔记
Pytorch 入門筆記
- 1. Pytorch下載與安裝
- 2. Pytorch的使用教程
- 2.1 Pytorch設計理念及其基本操作
- 2.2 使用torch.nn搭建神經網絡
- 2.3 創建屬于自己的Dataset和DataLoader
- 2.3.1 編寫Dataset類
- 2.3.2 編寫Transform類
- 2.3.3 將Transform融合到Dataset中去
- 2.3.4 編寫DataLoader類
- 2.4 使用Tensorboard來可視化訓練過程
- 2.4.1 Images可視化
- 2.4.2 Graph可視化
- 2.4.3 訓練中的Loss,Accuracy可視化
- 2.5 Pytorch實現DQN算法
- 2.6 Pytorch實現Policy Gradient算法
1. Pytorch下載與安裝
在Pytorch官網進行官方提供的下載方法:Pytorch官網,打開官網后選擇對應的操作系統和cuda版本。如果需要安裝GPU版本的Pytorch則需要下載對應CUDA版本的Torch版本,例如我裝的是CUDA 10.1,Python版本是3.7。
查看已安裝CUDA、cuDNN版本的方法:
Windows:
訪問文件夾 C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA 看該目錄下文件夾名稱,如果是v10.0則代表10.0版本的CUDA;
訪問C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v9.0\include\cudnn.h來查看cuDNN的版本。
Linux:
使用命令 cat /usr/local/cuda/version.txt 查看cuda版本;
使用命令 cat /usr/local/cuda/include/cudnn.h | grep CUDNN_MAJOR -A 2 查看cuDNN版本。
如果沒有安裝cuda和cuDNN,則需要先安裝這兩樣東西,再裝Pytorch。先安裝cuda,進入cuda官網,根據自己的操作系統選擇對應版本(version 一欄代表的是你Windows的操作系統版本):
下載exe(local)安裝程序,之后一路選擇默認安裝路徑就好了。安裝完成后再cmd窗口內輸入:nvcc -V,若看到以下信息證明cuda安裝成功:
cuda安裝好了之后下載對應的cnDNN,進入cuDNN安裝鏈接,這里需要先注冊一個Nvidia的賬號并登錄才能進行下一步,登錄后看到以下網址:
根據安裝的cuda版本選擇對應的cuDNN的版本即可,比如我們演示的是cuda 10.2版本,這里就選擇對應的7.6.5版本的cuDNN就好。下載好后是個壓縮文件,解壓后得到3個文件夾:bin,include,x64,將這三個文件下的內容分別拷貝到安裝的cuda目錄對應的bin,include,x64文件夾下即可,cuda的默認安裝路徑如下:
完成了cuda和cuDNN的安裝后,我們就可以回到Pytorch的安裝中去了,看文章最開始的那張圖,找到對應的版本后,復制圖片最下方的pip命令在cmd窗口執行即可開始進行安裝:
pip install torch===1.3.1 torchvision===0.4.2 -f https://download.pytorch.org/whl/torch_stable.html -i https://pypi.tuna.tsinghua.edu.cn/simple
這里使用了清華源來加速下載,等待一段時間后,Pytorch就安裝完畢了。
2. Pytorch的使用教程
2.1 Pytorch設計理念及其基本操作
Pytorch在設計的時候最大的亮點是在于,其可以替代Numpy庫,numpy庫中的array運算是依賴于將數組編譯成機器碼再到CPU運行進行加速的。而Torch是將array(在Pytorch里面叫Tensor)變成GPU上的變量進行運算的,因此在進行大規模運算是Torch能更快速。
- Tensor的運算
Tensor的定義和運算都和np.array()差不多,很多操作都是一樣的,下面是一些基本操作的介紹:
import torch""" 創建 Tensor 的方法 """ x = torch.tensor([3, 3]) # 創建一個值為[3, 3]的Tensor x = torch.empty(5, 3) # 5x3空矩陣 x = torch.ones(3, 3) # 值全為1的3x3矩陣 y = torch.randn_like(x, dtype=torch.long) # 創建一個和x一樣shape的隨機矩陣,并將每一個數字dtype變成long""" Tensor 的屬性 """ print(x.size()) # 相當于np.array中的.shape屬性,x.size()本質是一個tuple,可以使用tuple的全部屬性""" Resize Tensor """ x = torch.tensor([[1, 2], [3, 4]]) y = x.view(-1, 4) # y = ([1, 2, 3, 4]), 和np.reshape()是一樣的""" Tensor 和 Numpy 的互相轉換 """ x_np = np.arrary([1, 1]) x_tensor = torch.from_numpy(x_np) # 從numpy轉換到tensor x_np_2 = x_tensor.numpy() # 從tensor轉換到numpy""" CPU 到 GPU 的轉換 """ if torch.cuda.is_available():device = torch.device("cuda")x = torch.ones(5, 3)x_dev = x.to(device) # CPU tensor 轉 GPU tensorx_host= x_dev.to("cpu", torch.doule) # GPU tensor 轉 CPU tensor- 自動求導機制
torch.Tensor這個對象擁有自動求導的功能,每個Tensor在被計算的時候都會自動記錄這些運算過程,在最后計算梯度的時候就能夠追蹤的這些計算過程快速的進行梯度計算。如果要打開計算追蹤這個功能需要將Tensor.requires_grad這個屬性設置為True(默認是False的),當該追蹤計算功能打開后,只需要調用.backward()就能夠進行自動的梯度求導了。
在我們訓練的時候通常會為Tenosor打開.requires_grad屬性,但在訓練好一個模型后,我們在使用這個模型的時候,這時候我們就不需要計算梯度了(因為不會再做反向傳播了),這時候我們就可以關閉.requires_grad這個屬性來減少運算時間。使用with torch.no_grad():來關閉Tensor的計算追蹤功能:
2.2 使用torch.nn搭建神經網絡
在Pytorch中,搭建一個神經網絡需要自己寫一個類,該類需要繼承自torch.nn.Module類,nn.Module主要包含以下兩個特點:
通常訓練一個網絡模型的流程如下:
我們使用 torch.nn 來搭建一個卷積神經網絡用于識別 32*32 的單通道灰度圖。首先,我們先來理一下整個網絡的結構:
1. Convolutional2D Shape Change
IwI_wIw?代表輸入圖層的width,OwO_wOw?代表經過Conv2D層后圖層的width,FwF_wFw?代表卷積核Filter的width,SwS_wSw?代表在width上橫跨的步長,PwP_wPw?代表在width方向上的padding值,則有以下公式(Height方向上同理):Ow=Iw?Fw+2PwSw+1O_w=\frac{I_w - F_w + 2P_w}{S_w}+1Ow?=Sw?Iw??Fw?+2Pw??+1
2. Maxpooling Shape Change
IwI_wIw?代表輸入圖層的width,OwO_wOw?代表經過Maxpooling2D層后圖層的width,MwM_wMw?代表池化層的width,SwS_wSw?代表在width上橫跨的步長(通常是等于池化層的寬度的),則有以下公式(Height方向上同理):Ow=Iw?MwSw+1O_w=\frac{I_w - M_w}{S_w}+1Ow?=Sw?Iw??Mw??+1
因此,整個網絡架構如下:
根據上述網絡設計編寫代碼,搭建一個神經網絡需要編寫:
以下是代碼實現:
import torch import torch.nn as nn import torch.nn.functional as Fclass Net(nn.Module):def __init__(self):super(Net, self).__init__()self.Conv1 = nn.Conv2d(1, 6, 3) # params (input_channal_num, out_channal_num, filter_size)self.Conv2 = nn.Conv2d(6, 16, 3)self.fc1 = nn.Linear(6*6*16, 120)self.fc2 = nn.Linear(120, 80)self.fc3 = nn.Linear(80, 10) # predict 10 classesdef forward(self, x):x = F.max_pool2d(F.relu(self.Conv1(x)), 2)x = F.max_pool2d(F.relu(self.Conv2(x)), 2)x = x.view(-1, self.get_flatten_num(x))x = F.relu(self.fc1(x))x = F.relu(self.fc2(x))x = F.relu(self.fc3(x))return xdef get_flatten_num(x):features_num = x[1:] # drop out the batch size (which is x[0])flatten_num = 1for feature_num in features_num:flatten_num *= feature_numreturn flatten_numnet = Net() print(net)這樣我們就定義好了一個神經網絡,打印一下這個神經網絡可以看到:
這樣一來,整個前向傳播就寫完了,現在我們來寫反向傳播的過程,首先我們需要計算 Loss 再反向梯度傳播,下圖是一個簡單的反向傳播示意圖:
Input 層有兩個神經元,hidden layer 也有兩個神經元,最終 output 層有兩個神經元。target 代表 Label 數據。假設我們網絡內不存再bias,只有 weight 是需要學習的,那么我們需要根據 target 和神經網絡的 output 之間的誤差值(Loss)來修改 weight 值,以達到學習的效果。在這里我們以w5w_5w5?這個學習參數為例,通過反向鏈式求導來計算它的梯度:Loss 是指兩個 output 值與 target 之間的 MSE Error。對w5w_5w5?進行反向求導,由于從h1h_1h1?到o1o1o1是先經過LinearLinearLinear層(FC層)再經過一個sigmoidsigmoidsigmoid層,再輸出到o1o_1o1?的。因此從o1o_1o1?到h1h_1h1?的反向求導需要先對sigmoidsigmoidsigmoid這個計算過程求導,再對LinearLinearLinear計算過程求導,這就是為什么上圖中會先對Os1Os_1Os1?求導,再對O1O_1O1?求導。
我們計算隨機生成一個 target 讓其與我們的 output 進行 Loss 計算:
net.zero_grad() # 先清空網絡中的梯度變量中的值target = torch.rand(1, 10) # 隨機生成target標簽 criterion = nn.MSELoss() loss = criterion(target, out) # 計算loss值 loss.backward() # 反向求導函數這時,我們已經計算出所有的梯度了,現在需要我們去 update 這些 weights 的值,最簡單的更新的方式是使用 SGD 的方法,即 weight=weight?gradient?learningrateweight = weight - gradient * learning_rateweight=weight?gradient?learningr?ate,除此之外還有 Adam,RMSProp等等,Pytorch 將這些更新的算法稱作“優化器”(optimizer)封裝在了函數里,我們只需要調用就可以了:
import torch.optim as optimoptimizer = optim.SGD(net.parameters(), lr=0.01)# 每一輪訓練的時候 optimizer.zero_grad() # 一定要記得清空優化器中的上一輪梯度信息 output = net(input) loss = criterion(output, target) loss.backward() optimizer.step() # 更新梯度信息至此我們已經完成了 Pytorch 的安裝并使用 torch 搭建了一個前向神經網絡以及反向傳播的全過程。
2.3 創建屬于自己的Dataset和DataLoader
現在我們已經成功搭建了一個CNN網絡了,問題是我們的CNN網絡只能接收數字類型的 Input,而我們往往數據集都是圖片類型的,因此為了將圖片數據轉換為神經網絡能夠接收的數字類型,我們需要寫 Dataset 類和 DataLoader 類,這兩個類的作用分別為:
Dataset 類的作用
Dataset 用于讀取對應路徑的數據集(比如說某個特定文件夾下的圖片)并轉換成神經網絡能夠接收的數據類型,舉例來說,我們要用人臉識別數據集做訓練,那么我們就需要編寫一個 FaceDataset() 來把人臉的圖片轉換向量數據。
DataLoader 類的作用
往往在我們訓練神經網絡時都會使用到 mini-batch 的方法,一次讀取多個樣本而不是只讀一個樣本,那么如何從數據集中去采樣一個batch的數據樣本就是 DataLoader 要實現的功能了。
我們使用 Face Landmark 的數據集來舉例,下載一個 Landmark 數據集,這個數據集中包含【人臉圖片】和【Landmark】兩個數據,Landmark 是指人臉上的68個特征點(不清楚的可以查查dlib這個庫)。如下圖所示:
Landmark 就是指紅色的68個點,在數據集中存放的是這68個點的(x,y)坐標。
2.3.1 編寫Dataset類
我們現在來寫一個 FaceLandmarksDataset() 類用于讀取數據中的【人臉圖片】和【Landmark】信息,對于 FaceLandmarksDataset() 這個類我們繼承自 torch 的 Dataset 類,并且實現三個方法:__init__, __len__, __getitem__。
- __len__ 方法用于返回這個 Dataset 的數據集長度共有多少個。
- __getitem__ 可以讓你在調用 dataset[i] 的時候直接調用這個方法,非常方便。
這樣我們就寫好了一個讀取自己數據集的 Dataset 類,現在來嘗試使用這個類:
def show_landmarks(sample, ax):print(sample["image"].shape)ax.imshow(sample["image"])ax.scatter(sample["landmarks"][:, 0], sample["landmarks"][:, 1])face_dataset = FaceLandmarksDataset("./faces") # 實例化 for i in range(5):sample = face_dataset[i] # 這里會調用類中的 __getitem__()方法ax = plt.subplot(1, 5, i+1)ax.set_title("Sample #" + str(i+1))show_landmarks(sample, ax)plt.show()結果如下所示,畫出了五張人臉以及他們對應的 landmark 點:
2.3.2 編寫Transform類
為什么會有 Transform 類?這是因為在很多時候我們并不是把原圖傳入神經網絡,在輸入數據之前會對原數據進行一些列的預處理(preprocessing),例如:數據集中的圖片不會全部都是相同尺寸的,但大多神經網絡都需要輸入圖片的尺寸是固定的,因此我們需要對數據集中的做預處理,resize 或是 crop 以保證輸入數據的尺寸是符合神經網絡要求的。Transform 類就是用于做數據預處理而設計的類,我們一共設計三個類來組成 Transform 類:Rescale(), RandomCrop(),ToTensor():
- Rescale類:這個類用于對圖片進行 resize。
- RandomCrop類:這個用于對圖片進行裁剪,且是任意位置隨機裁剪。
- ToTensor類:這個類用于將 np 數組格式轉換成 torch 中 tensor 的格式,圖片的 np 數組是 [height, width, channel],而 torch 中圖片的 tensor 格式是 [channel, height, width]。
這些類中都要實現 __call__ 方法,call 方法可以使得這些類在被實例化的時候就調用 __call__ 函數下的方法。
class Rescale(object):def __init__(self, output_size):assert isinstance(output_size, (list, tuple)), "output size must be tuple or list."self.output_size = output_sizedef __call__(self, sample):image, land_marks = sample["image"], sample["landmarks"]h, w = image.shape[:2]new_h, new_w = self.output_sizeimg = transform.resize(image, (int(new_w), int(new_h)))land_marks = land_marks * [new_w / w, new_h / h]return {'image': img, "landmarks": land_marks}class RandomCrop(object):def __init__(self, output_size):assert isinstance(output_size, (int, tuple)), "output size must be int or tuple."if isinstance(output_size, int):self.output_size = [output_size, output_size]else:assert len(output_size) == 2, "output size tuple's length must be 2."self.output_size = output_sizedef __call__(self, sample):image, land_marks = sample["image"], sample["landmarks"]h, w = image.shape[:2]crop_h, crop_w = self.output_sizetop = np.random.randint(0, h - crop_h)left = np.random.randint(0, w - crop_w)image = image[top:top+crop_h, left:left+crop_w]land_marks -= [left, top]return {"image":image, "landmarks":land_marks}class ToTensor(object):def __call__(self, sample):image, land_marks = sample["image"], sample["landmarks"]image = image.transpose((2, 0, 1))return {"image":image, "landmarks":land_marks}這樣我們就實現好了這三個功能的類,接下來我們將這三個方法合成一個方法,可以依次按順序對一個 sample 做這三個操作。通過 torchvision.transforms() 來融合這三個方法。
scale = Rescale((256, 256)) # 單實現 Resacle 處理 random_crop = RandomCrop(128) # 單實現 RandomCrop 處理 compose = transforms.Compose([Rescale((256, 256)), RandomCrop(224)]) # 對一個sample先Rescale再RandomCropsample = face_dataset[66]for i, tsfm in enumerate([scale, random_crop, compose]): # 同時展示Rescale、RandomCrop和先Resacle再RandomCrop的效果transformed_data = tsfm(sample)ax = plt.subplot(1, 3, i+1)ax.set_title(type(tsfm).__name__)show_landmarks(transformed_data, ax) plt.show()效果如下,Compose 是指結合了前兩種方法,先 resize 后再隨機 Crop 后的樣子:
2.3.3 將Transform融合到Dataset中去
前面提到,Transform 的作用是做數據預處理的,因此我們期望每次從 Dataset 中讀取數據的時候,這些數據都被預處理一次,因此我們可以把 Transform 融合到 Dataset 中去,還記得我們在寫 Dataset 的時候有留一個參數 transform 嗎?我們現在可以傳進去就好了:
transformed_dataset = FaceLandmarksDataset(root='./faces',transform=transforms.Compose([Rescale((256, 256)),RandomCrop(224), ToTensor()]))這樣我們就寫好了一個帶有 Transform 的 Dataset 了。
2.3.4 編寫DataLoader類
在有了 Dataset 類后,我們就可以開始編寫 DataLoader 類了,Dataloader 主要是從Dataset 中去 sample 一個batch 的數據。我們可以直接使用 Pytorch 提供的 Dataloader 類:
dl = torch.utils.data.DataLoader(transformed_dataset, batch_size=4, shuffle=True, num_workers=4) # windows下num_workers參數要等于0,不然會報錯這樣就可以從我們的 Dataset 中取一個batch 的數據了,下面我們來看看怎么用 dataloader 取出 batch data:
def show_landmark_batch(sample_batch):images_batch, landmarks_batch = sample_batch["image"], sample_batch["landmarks"]batch_size = len(images_batch)im_size = images_batch.size(2)grid = utils.make_grid(images_batch)plt.imshow(grid.numpy().transpose(1, 2, 0))grid_border_size = 2for i in range(batch_size):plt.scatter(landmarks_batch[i, :, 0].numpy() + i * im_size + (i + 1) * grid_border_size, landmarks_batch[i, :, 1].numpy() + grid_border_size, s=10)plt.title("Batched Samples")for i_batch, sample_batch in enumerate(dl): # dataloader是一個迭代器,迭代一次取出一個batch的sampleif i_batch == 0:show_landmark_batch(sample_batch)plt.show()可視化結果如下:
2.4 使用Tensorboard來可視化訓練過程
Tensorboard 無疑是一個非常好用且直觀的神經網絡訓練可視化利器,Pytorch 中也集成了使用 Tensorboard 方法,建立一個SummaryWriter() 類即可完成各種類型的可視化,下面我們就一起嘗試使用 Tensorboard 來做可視化。
2.4.1 Images可視化
有時候我們拿到的只是圖片信息的向量信息(例如MNIST數據集),我們想查看這些圖片到底長什么樣,這時候我們可以往 Tensorboard 中添加圖片數據信息來可視化這些圖片,首先我們先建立一個 writer 對象。
from torch.utils.tensorboard SummaryWriter writer = SummaryWriter("run/experience_1") # 將log信息都保存在run/experience_1的目錄下這樣我們就建立好了一個 writer 對象,之后的所有操作都通過 writer 對象來完成。
import matplotlib.pyplot as plt import numpy as np import torch import torchvision import torchvision.transforms as transforms import torch.nn as nn import torch.nn.functional as F import torch.optim as optimtransform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))]) trainset = torchvision.datasets.FashionMNIST('./data', download=True, train=True, transform=transform) # 加載數據集 testset = torchvision.datasets.FashionMNIST('./data', download=True, train=False, transform=transform) # 加載數據集 trainloader = torch.utils.data.DataLoader(trainset, batch_size=4, shuffle=True) testloader = torch.utils.data.DataLoader(testset, batch_size=4, shuffle=True)classes = ('T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat','Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle Boot')dataiter = iter(trainloader) images, labels = dataiter.next() img_grid = torchvision.utils.make_grid(images) # 獲取一個batch的數據并將4張圖片合并成一張圖片writer.add_image("four_fashion_samples", img_grid) # 將圖片數據添加到 Tensorboard->(圖片名稱,圖片數據)這時候我們前往“run”文件夾存放的目錄,在終端中輸入tensorboard --logdir=run來開啟Tensorboard:
打開后如下所示,可以看到在 Images 這一欄已經顯示了我們剛才添加進去的圖片信息:
2.4.2 Graph可視化
如果我們想更清楚的看到我們建立的神經網絡模型長什么樣,我們可以使用writer.add_graph()方法來將模型添加入 Tensorboard:
class Net(nn.Module):def __init__(self):super(Net, self).__init__()self.conv1 = nn.Conv2d(1, 6, 5)self.pool = nn.MaxPool2d(2, 2)self.conv2 = nn.Conv2d(6, 16, 5)self.fc1 = nn.Linear(16 * 4 * 4, 120)self.fc2 = nn.Linear(120, 84)self.fc3 = nn.Linear(84, 10)def forward(self, x):x = self.pool(F.relu(self.conv1(x)))x = self.pool(F.relu(self.conv2(x)))x = x.view(-1, 16 * 4 * 4)x = F.relu(self.fc1(x))x = F.relu(self.fc2(x))x = self.fc3(x)return xnet = Net() # 實例化CNN網絡 criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9) writer.add_graph(net, images) # 將網絡添加入Tensorboard->(神經網絡對象,隨機傳一組Input)之后,我們可以在 Tensorboard 的Graphs這一欄看到我們整個神經網絡的架構:
2.4.3 訓練中的Loss,Accuracy可視化
在模型訓練過程中,我們想實時的監測 Loss,Accuracy 等值的變化,我們可以通過writer.add_scalar()方法往 Tensorboard 中加入監測變量:
running_loss = 0.0 right_num = 0 for epoch in range(1): # loop over the dataset multiple timesfor i, data in enumerate(trainloader, 0):# get the inputs; data is a list of [inputs, labels]inputs, labels = data# zero the parameter gradientsoptimizer.zero_grad()# forward + backward + optimizeoutputs = net(inputs)loss = criterion(outputs, labels)loss.backward()optimizer.step()_, preds_tensor = torch.max(outputs, 1)right_num += (preds_tensor.numpy() == labels.numpy()).sum()running_loss += loss.item()if i % 1000 == 999: # 每1000步更新一次running_loss /= 1000accaracy = right_num / 4000writer.add_scalar('training/loss', running_loss, epoch * len(trainloader) + i) # 添加Loss監測變量->(變量名稱,變量值,step)writer.add_scalar('training/accaracy', accaracy, epoch * len(trainloader) + i) # 添加Accuracy監測變量->(變量名稱,變量值,step)print("[%d] Loss: %.4f Acc: %.2f" % (i, running_loss, accaracy))running_loss = 0.0right_num = 0print('Finished Training')在訓練過程中,在Scalars一欄可以監測“Loss”和“Accuracy”的變化情況(Tensorboard默認每30s刷新一次),訓練結果如下所示:
2.5 Pytorch實現DQN算法
Deep Reinforcement Learning 結合了深度學習和增強學習,使得增強學習能有更好的效用。這次我們使用Pytorch來實現一個DQN,學會玩 CartPole-v0 的立桿游戲。DQN 一共分為target network 和 evaluate network,我們在 DQN 類中會實現這兩個網絡。在更新 evaluate network 時,我們使用qevalq_{eval}qeval? 和 rt+γ?qnextr_t+\gamma*q_{next}rt?+γ?qnext? 之間的差作為 Loss 值來更新網絡,其中 qevalq_{eval}qeval? 來自evaluate network,qnextq_{next}qnext?來自target network。DQN架構圖和 net 示意圖如下所示:(這里默認已經有DQN的基礎,如果不清楚DQN可以參考強化學習入門筆記)。
import torch import torch.nn as nn import torch.nn.functional as F import numpy as np import gym from torch.utils.tensorboard import SummaryWriter# Hyper Parameters EPOCH = 400 BATCH_SIZE = 32 LR = 0.01 # learning rate EPSILON = 0.9 # greedy policy GAMMA = 0.9 # reward discount TARGET_REPLACE_ITER = 100 # target update frequency MEMORY_CAPACITY = 2000 env = gym.make('CartPole-v0') env = env.unwrapped N_ACTIONS = env.action_space.n N_STATES = env.observation_space.shape[0]import 一些包和定義一些超參數,這里要用到 gym 庫,需要先pip install gym。
class Net(nn.Module):def __init__(self):super(Net, self).__init__()self.fc1 = nn.Linear(N_STATES, 20)self.fc1.weight.data.normal_(0, 0.1) self.fc2 = nn.Linear(20, N_ACTIONS)self.fc2.weight.data.normal_(0, 0.1) def forward(self, x):x = F.relu(self.fc1(x))return self.fc2(x)定義一個Net類,這里我們只有一層 Hidden Layer。輸入為 gym 給的 observation 的維度,輸出為 action_space 的維度,下面我們實現 DQN 類。
class DQN(object):def __init__(self):self.target_net, self.evaluate_net = Net(), Net()self.memory = np.zeros((MEMORY_CAPACITY, N_STATES * 2 + 2))self.loss_Function = nn.MSELoss()self.optimizer = torch.optim.Adam(self.evaluate_net.parameters(), lr=LR)self.point = 0self.learn_step = 0def choose_action(self, s):s = torch.unsqueeze(torch.FloatTensor(s), 0) # torch 不支持傳入單樣本,只能傳入batch的data,因此這里單樣本數據需要提升一個維度if np.random.uniform() < EPSILON: # epsilon-greedy探索 return torch.max(self.evaluate_net.forward(s), 1)[1].data.numpy()[0]else:return np.random.randint(0, N_ACTIONS)def store_transition(self, s, a, r, s_):self.memory[self.point % MEMORY_CAPACITY, :] = np.hstack((s, [a, r], s_))self.point += 1def sample_batch_data(self, batch_size):perm_idx = np.random.choice(len(self.memory), batch_size)return self.memory[perm_idx]def learn(self) -> float:if self.learn_step % TARGET_REPLACE_ITER == 0:self.target_net.load_state_dict(self.evaluate_net.state_dict())self.learn_step += 1batch_memory = self.sample_batch_data(BATCH_SIZE)batch_state = torch.FloatTensor(batch_memory[:, :N_STATES])batch_action = torch.LongTensor(batch_memory[:, N_STATES : N_STATES + 1].astype(int))batch_reward = torch.FloatTensor(batch_memory[:, N_STATES + 1 : N_STATES + 2])batch_next_state = torch.FloatTensor(batch_memory[:, -N_STATES:])q_eval = self.evaluate_net(batch_state).gather(1, batch_action) # 由于返回的是對每個action的value,因此用gather()去獲得采取的action對應的valueq_next = self.target_net(batch_next_state).detach() # target network是不做更新的,所以要detach()q_target = batch_reward + GAMMA * q_next.max(1)[0].view(BATCH_SIZE, 1)loss = self.loss_Function(q_eval, q_target)self.optimizer.zero_grad()loss.backward()self.optimizer.step()return loss.data.numpy()DQN中一共有幾個需要注意的部分:memory,target network,evaluate network。其中 memory 是一個環形的隊列,當經驗池滿了之后就會把舊的數據給覆蓋掉。target network會在一定的steps之后把evaluate network的參數直接復制到自己的網絡中。
dqn = DQN()writer = SummaryWriter("run/MemoryCapacity_100_CustomReward/") writer.add_graph(dqn.evaluate_net, torch.randn(1, N_STATES))global_step = 0 for i in range(EPOCH):s = env.reset()running_loss = 0cumulated_reward = 0step = 0while True:global_step += 1env.render()a = dqn.choose_action(s)s_, r, done, _ = env.step(a)# 自定義reward,不用原始的rewardx, x_dot, theta, theta_dot = s_r1 = (env.x_threshold - abs(x)) / env.x_threshold - 0.8r2 = (env.theta_threshold_radians - abs(theta)) / env.theta_threshold_radians - 0.5r = r1 + r2dqn.store_transition(s, a, r, s_)cumulated_reward += rif dqn.point > MEMORY_CAPACITY: # 在經驗池滿了之后才開始進行學習loss = dqn.learn()running_loss += lossif done or step > 2000:print("【FAIL】Episode: %d| Step: %d| Loss: %.4f, Reward: %.2f" % (i, step, running_loss / step, cumulated_reward))writer.add_scalar("training/Loss", running_loss / step, global_step)writer.add_scalar("training/Reward", cumulated_reward, global_step)breakelse:print("\rCollecting experience: %d / %d..." %(dqn.point, MEMORY_CAPACITY), end='')if done:breakif step % 100 == 99:print("Episode: %d| Step: %d| Loss: %.4f, Reward: %.2f" % (i, step, running_loss / step, cumulated_reward))step += 1s = s_以上是學習的過程,在 reward 的那個地方,使用了自定義的 reward,而不是使用 gym 給定的 r,合理的制定 reward 可以幫助模型學的更好的效果,我們后面會對使用不同的 reward 學習的效果進行對比。模型結構如下圖所示:
訓練過程中,按照上一節講的內容利用 tensorboard 追蹤 Loss 和 Reward 兩個變量的值變化情況:
可以看到,Loss 的值在逐步趨于0,這說明模型是趨于收斂的,Reward 值也在總體上升,最高達到600左右(做了 smooth 后的值,不是原本值),倒立擺能夠穩住很長一段時間。下面我們來看一下,如果我們修改部分超參數的值會出現什么樣的訓練效果,我一共做了4個對比試驗:
- 回報值設定對模型效果的影響
在經驗池容量和模型結構都相同的情況下,使用自定義回報(橘色)和默認回報(藍色)的效果如下圖所示:
可以看到,Loss 函數最終都是趨于很小的值,這說明使用兩種方法模型都是收斂的,但是 Reward 值差別非常大。為什么同樣都是收斂的情況下,效果差這么多?這是因為,模型收斂是指:訓練出來的Q-Net總會選擇含有最大Q值的Action(這樣的Loss最小)。但是Action的Q值是通過 Reward 來計算的:rt+argmaxaQπ(s,a)r_t + argmax_aQ^\pi(s, a)rt?+argmaxa?Qπ(s,a)。因此, reward 的制定直接決定了模型效果的好壞。所以,如果 reward 不能很好的與最終效用產生緊密的聯系,模型雖然能有找到最大效用行為的能力,但這個最大效用的行為并不能產生很好的效果。因此,如果看到 Loss 已經很小,但最終效用不怎么好的情況下,一般就考慮重新制定 reward,選擇一個能夠更精準表示最終效用的評價函數。
- 模型復雜度對效用的影響
在經驗池容量相同且均使用默認回報的情況下,嘗試修改原有模型,使得模型變得稍微復雜一些,看看效果會不會更好一些,將模型再加一層 hidden layer 并且增加每一層神經元的個數,代碼如下:
模型結構如下:
使用復雜模型(紅色)和使用簡單模型(藍色)的 Reward 情況對比如下:
可以看到,兩個模型 Loss 都是趨于0,證明模型均收斂,紅色(復雜)模型的得比藍色(簡單)模型要稍微高一些,但和使用自定義回報的模型比起來還是差很遠。
- 經驗池容量對模型效果的影響
對于相同復雜度的模型,使用相同回報值,通過改變經驗池容量來觀察對模型效果有什么影響,容量為100(紅色)和容量為2000(藍色)模型的對比結果如下:
觀察可得,擴展經驗池后的效果會比小經驗池的效果要好一些。因此,通過以上總結,我們可以得出在DQN中不同超參數對模型效果影響程度的結論,即:回報函數制定 > 經驗池容量 > 模型復雜度。
2.6 Pytorch實現Policy Gradient算法
在實現了 DQN 這種 value-based 的算法之后,我們嘗試實現一種 policy-based 的方法:Policy-Gradient。策略梯度是很多經典算法的基石,包括 A3C,PPO在內的多種算法都是基于策略梯度的基本思想來實現的,因此這次我們同樣基于 CartPole 的簡單場景來實現 Policy Gradient 算法(默認已經有PG的基礎,如果不清楚PG可以參考強化學習入門筆記)。
Policy Gradinet 的示意圖如下,根據輸入observation,策略決策網絡會輸出每一個action對應被采取的概率(效用越高的action概率就會被預測的越大)。我們可以根據這個概率來進行行為選擇,這里和上面的DQN不一樣,DQN的行為選擇是一定概率選擇最大效用的行為,一定概率隨機選行為,在PG算法中直接按照概率來選行為,即結合了多選擇高效用的行為的標準,又結合了一定概率進行行為探索的標準。一旦選擇好了一個行為后,我們就去計算這個行為的效用是多少,如果效用高,我們就增加這個動作的概率;反之則降低選擇該行為的概率。
先定義神經網絡層,這里使用1個 hidden layer,10個神經元:
import gym import numpy as np import torch import torch.nn.functional as F import torch.nn as nn import matplotlib.pyplot as plt from torch.utils.tensorboard import SummaryWriterWRITE_TENSORBOARD_FLAG = Trueclass Net(nn.Module):def __init__(self, observation_dim, action_dim):super(Net, self).__init__()self.observation_dim = observation_dimself.action_dim = action_dimself.fc1 = nn.Linear(self.observation_dim, 10)self.fc2 = nn.Linear(10, self.action_dim)def forward(self, x):x = F.tanh(self.fc1(x))return F.softmax(self.fc2(x))隨后我們定義PolicyGradient類,由于Policy Gradient中梯度計算公式為:▽θlogπθ(a∣s)R(a)\bigtriangledown{_\theta}log\pi_\theta(a|s)R(a)▽θ?logπθ?(a∣s)R(a),πθ(a∣s)\pi_\theta(a|s)πθ?(a∣s)是在網絡參數為θ\thetaθ的情況下,行為a被選擇的概率,R(a)R(a)R(a)是行為a選擇后能得到的總回報值,這個回報值通常使用R(a)=∑t=0TγtRnR(a) = \sum_{t=0}^{T}\gamma^tR_nR(a)=∑t=0T?γtRn?來計算,因此我們必須完整的收集了一個Epoch的數據信息后才能進行一次梯度更新(不然無法計算累計回報)。這一點也和DQN不一樣,DQN可以進行單步更新,而PG不可以。
class PolicyGradient(object):def __init__(self, observation_dim, action_dim, learning_rate=0.01, gamma=0.95):self.observation_dim = observation_dimself.action_dim = action_dimself.gamma = gammaself.ep_obs, self.ep_r, self.ep_a = [], [], []self.net = Net(observation_dim, action_dim)self.optimizer = torch.optim.Adam(self.net.parameters(), lr=learning_rate)def choose_action(self, observation):prob_list = self.net(observation)action = np.random.choice(range(prob_list.size(0)), p=prob_list.data.numpy())return actiondef store_transition(self, obs, r, a):self.ep_obs.append(obs)self.ep_r.append(r)self.ep_a.append(a)def learn(self):cumulative_reward_list = self.get_cumulative_reward()batch_obs = torch.FloatTensor(np.vstack(self.ep_obs))batch_a = torch.LongTensor(np.array(self.ep_a).reshape(-1, 1))batch_r = torch.FloatTensor(cumulative_reward_list.reshape(-1, 1))action_prob = self.net(batch_obs)action_prob.gather(1, batch_a)gradient = torch.log(action_prob) * batch_rloss = -torch.mean(gradient) # 網絡會對loss進行minimize,但我們是想做梯度上升,所以加一個負號self.optimizer.zero_grad()loss.backward()self.optimizer.step()self.reset_epoch_memory()return loss.data.numpy()def get_cumulative_reward(self):running_r = 0cumulative_reward = np.zeros_like(self.ep_r)for i in reversed(range(len(self.ep_r))):running_r = running_r * self.gamma + self.ep_r[i]cumulative_reward[i] = running_r# normalize cumulative rewardcumulative_reward -= np.mean(cumulative_reward)cumulative_reward /= np.std(cumulative_reward)return cumulative_rewarddef reset_epoch_memory(self):self.ep_a.clear()self.ep_obs.clear()self.ep_r.clear()值得注意的是,在策略梯度中其實是沒有loss這個概念的(因為根本就沒有標簽),這個loss的梯度是我們通過logπθ(a∣s)R(a)log\pi_\theta(a|s)R(a)logπθ?(a∣s)R(a)算出來的,由于在神經網絡中我們做的是梯度下降:θ?=gradient?LearningRate\theta -= gradient*LearningRateθ?=gradient?LearningRate,但在策略梯度算法中我們是希望做梯度上升的θ+=gradient?LearningRate\theta += gradient*LearningRateθ+=gradient?LearningRate,因此我們在求得了梯度之后要添加一個負號再將其作為loss。最后,我們建立 CartPole 場景并將該算法用在訓練場景上:
def main():DISPLAY_REWARD_THRESHOLD = 200RENDER = Falseenv = gym.make('CartPole-v0')env = env.unwrappedPG = PolicyGradient(env.observation_space.shape[0], env.action_space.n)if WRITE_TENSORBOARD_FLAG:writer = SummaryWriter("run/simple_model_experiment")writer.add_graph(PG.net, torch.rand(env.observation_space.shape))for i_eposide in range(120):obs = env.reset()obs = torch.FloatTensor(obs)while True:if RENDER: env.render()action = PG.choose_action(obs)obs_, reward, done, _ = env.step(action)PG.store_transition(obs, reward, action)if done:ep_r = sum(PG.ep_r)if ep_r > DISPLAY_REWARD_THRESHOLD: RENDER = Trueprint("episode: %d | reward: %d" % (i_eposide, ep_r))loss = PG.learn()if WRITE_TENSORBOARD_FLAG:writer.add_scalar("training/loss", loss)writer.add_scalar("training/reward", ep_r)breakobs = torch.FloatTensor(obs_)if __name__ == '__main__':main()Policy Gradient 訓練結果如下:
loss對應的就是每一次更新的梯度,reward對應的是模型的得分。可以看到使用策略梯度的效果還是蠻不錯的,在100個Epoch之后桿子基本就可以被很好的穩住了。
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的Pytorch快速入门笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 利用Deep Reinforcement
- 下一篇: 如何在Clion中使用C++调用Pyth