用PyTorch實(shí)現(xiàn)一個(gè)簡(jiǎn)單的分類器
回想了一下自己關(guān)于 pytorch 的學(xué)習(xí)路線,一開(kāi)始找的各種資料,寫(xiě)下來(lái)都能跑,但是卻沒(méi)有給自己體會(huì)到學(xué)習(xí)的過(guò)程。有的教程一上來(lái)就是寫(xiě)一個(gè) cnn,雖然其實(shí)內(nèi)容很簡(jiǎn)單,但是直接上手容易讓人找不到重點(diǎn),學(xué)的云里霧里。有的教程又淺嘗輒止,師傅領(lǐng)到了門(mén)檻跟前,總感覺(jué)自己還沒(méi)有進(jìn)門(mén),教程就結(jié)束了。
所以我總結(jié)了一下自己當(dāng)初學(xué)習(xí)的路線,準(zhǔn)備繼續(xù)深入鞏固自己的 pytorch 基礎(chǔ);另一方面,也想從頭整理一個(gè)教程,從沒(méi)有接觸過(guò) pytorch 開(kāi)始,到完成一些最新論文里面的工作。以自己的學(xué)習(xí)筆記整理為主線,大家可以針對(duì)參考。
第一篇筆記,我們先完成一個(gè)簡(jiǎn)單的分類器。主要流程分為以下三個(gè)部分:
1,自定義生成一個(gè)訓(xùn)練集,具體為在二維平面上的一些點(diǎn),分為兩類;
2,構(gòu)建一個(gè)淺層神經(jīng)網(wǎng)絡(luò),實(shí)現(xiàn)對(duì)特征的擬合,主要是明白在 pytorch 中網(wǎng)絡(luò)結(jié)構(gòu)如何搭建;
3,完成訓(xùn)練和測(cè)試部分的工作,熟悉 pytorch 如何對(duì)網(wǎng)絡(luò)進(jìn)行訓(xùn)練和測(cè)試。
1. 自定義生成數(shù)據(jù)集
- n_data = torch.ones(100, 2)
- x0 = torch.normal(2*n_data, 1)
- y0 = torch.zeros(100)
- x1 = torch.normal(-2*n_data, 1)
- y1 = torch.ones(100)
- x = torch.cat((x0, x1)).type(torch.FloatTensor)
- y = torch.cat((y0, y1)).type(torch.LongTensor)
這篇文章我們先考慮在一個(gè)自己定義的簡(jiǎn)單數(shù)據(jù)集上實(shí)現(xiàn)分類,這樣子可以最簡(jiǎn)單的了解一個(gè)神經(jīng)網(wǎng)絡(luò)的模型,如何用 pytorch 搭建起來(lái)。
這個(gè)代碼對(duì) numpy 比較熟悉的同學(xué)應(yīng)該也可以猜出來(lái)它的內(nèi)容,只是在 numpy 中是一個(gè) numpy array,在 pytorch 中是一個(gè) tensor。這里我簡(jiǎn)單的介紹一下這幾行代碼的作用,給有需要的同學(xué)捋順?biāo)悸贰?/p>
首先 n_data 是基準(zhǔn)數(shù)據(jù),用來(lái)生成其它數(shù)據(jù),內(nèi)容為一個(gè) 100 行 2 列 的 tensor,其中的值都為 1。x0 是一類數(shù)據(jù)的坐標(biāo)值,通過(guò)這個(gè) n_data 來(lái)生成。
具體的生成的辦法是用 torch.normal() 這個(gè)函數(shù),第一個(gè)參數(shù)為 mean,第二個(gè)參數(shù)是 std。所以返回的結(jié)果 x0 是一個(gè)和 n_data 形狀一樣,但是其中的數(shù)據(jù)在以 2 為平均值,以 1 為標(biāo)準(zhǔn)差的正態(tài)分布中隨機(jī)選取的。y0 則是一個(gè) 100 維的 tensor,其中的值都為 0。
我們可以這樣理解 x0 和 y0,x0 的形狀是 100 行 2 列的 tensor,其中的值以 2 為中心進(jìn)行隨機(jī)分布,符合正態(tài)分布,而這些點(diǎn)的標(biāo)簽我們?cè)O(shè)置為 y0,也就是 0。與此相反,x1 對(duì)應(yīng)的中心為 -2,且標(biāo)簽為 y1,也就是每個(gè)點(diǎn)的標(biāo)簽都為 1。
最后生成的 x 和 y,就是將所有的數(shù)據(jù)合并起來(lái),x0 和 x1 合并起來(lái)作為數(shù)據(jù),y0 和 y1 合并起來(lái)作為標(biāo)簽。
2. 構(gòu)建一個(gè)淺層神經(jīng)網(wǎng)絡(luò)
- class Net(torch.nn.Module):
- def __init__(self, n_feature, n_hidden, n_output):
- super(Net, self).__init__()
- self.n_hidden = torch.nn.Linear(n_feature, n_hidden)
- self.out = torch.nn.Linear(n_hidden, n_output)
- def forward(self, x_layer):
- x_layer = torch.relu(self.n_hidden(x_layer))
- x_layer = self.out(x_layer)
- x_layer = torch.nn.functional.softmax(x_layer)
- return x_layer
- net = Net(n_feature=2, n_hidden=10, n_output=2)
- # print(net)
- optimizer = torch.optim.SGD(net.parameters(), lr=0.02)
- loss_func = torch.nn.CrossEntropyLoss()
上面的 Net() 類就是如何構(gòu)建一個(gè)神經(jīng)網(wǎng)絡(luò)的步驟。我們?nèi)绻堑谝淮斡?pytorch 寫(xiě)一個(gè)神經(jīng)網(wǎng)絡(luò),那么這個(gè)就是一個(gè)足夠簡(jiǎn)單的例子了。其中的內(nèi)容由兩部分組成,分別是 __init__() 函數(shù)和 forward() 函數(shù)。
大家可以簡(jiǎn)單的這樣子理解:__init__() 函數(shù)中,是對(duì)網(wǎng)絡(luò)結(jié)構(gòu)的定義,都有哪些層,每層又有什么功能。例如這個(gè)函數(shù)中,self.n_hidden 就是定義了一個(gè)線性擬合函數(shù),也就是全連接層,在此處相當(dāng)于一個(gè)向隱藏層的映射。輸入是 n_feature,輸出是隱藏層的神經(jīng)元個(gè)數(shù) n_hidden。然后 self.out 也一樣是一個(gè)全連接層,輸入是剛才的隱藏層的神經(jīng)元個(gè)數(shù) n_hidden,輸出是最后的輸出結(jié)果 n_output。
接著就是 forward() 函數(shù),在這里相當(dāng)于定義我們神經(jīng)網(wǎng)絡(luò)的執(zhí)行順序。所以這里可以看到,先對(duì)輸入的 x_layer 執(zhí)行上面的隱藏層函數(shù),也就是第一個(gè)全連接 self.n_hidden(),然后對(duì)輸出再執(zhí)行激活函數(shù) relu。接下來(lái)如法炮制,經(jīng)過(guò)一個(gè)輸出層 self.out(),得到最后的輸出。然后將輸出 x_layer 返回。
optimizer 就是這里定義的優(yōu)化方式,其中的 lr 是學(xué)習(xí)率的參數(shù)。然后損失函數(shù)我們選擇交叉熵?fù)p失函數(shù),也就是上面的最后一行代碼。優(yōu)化算法和損失函數(shù),可以在 pytorch 中直接選擇不同的 api 接口,形式上直接參考上面這種固定形式便可。
3. 完成訓(xùn)練和測(cè)試
- for i in range(100):
- out = net(x)
- # print(out.shape, y.shape)
- loss = loss_func(out, y)
- optimizer.zero_grad()
- loss.backward()
- optimizer.step()
接下來(lái)我們看一下如何進(jìn)行訓(xùn)練的過(guò)程。net() 是我們對(duì) Net() 類實(shí)例化出來(lái)的一個(gè)對(duì)象,所以利用 net() 可以直接完成模型的運(yùn)行,out 就是模型預(yù)測(cè)出來(lái)的結(jié)果,loss 則是和真實(shí)值按照交叉熵?fù)p失函數(shù)計(jì)算出來(lái)的誤差。
下面的三行代碼是一個(gè)標(biāo)準(zhǔn)形式,表示了如何進(jìn)行梯度的反向傳播。到此其實(shí)我們的訓(xùn)練已經(jīng)完成了,這個(gè)網(wǎng)絡(luò)現(xiàn)在可以直接拿來(lái)對(duì)測(cè)試的數(shù)據(jù)集進(jìn)行預(yù)測(cè)分類了。
- # train result
- train_result = net(x)
- # print(train_result.shape)
- train_predict = torch.max(train_result, 1)[1]
- plt.scatter(x.data.numpy()[:, 0], x.data.numpy()[:, 1], c=train_predict.data.numpy(), s=100, lw=0, cmap='RdYlGn')
- plt.show()
為了讓大家更好的理解這個(gè)模型的作用,這里我們來(lái)做一些可視化的工作,看看一個(gè)模型的學(xué)習(xí)效果。通過(guò) python 很常見(jiàn)的一個(gè)數(shù)據(jù)可視化的庫(kù) matplotlib 可以實(shí)現(xiàn)這個(gè)目標(biāo),具體的 matplotlib 的用法就不介紹了。
這里的作用是顯示出來(lái)訓(xùn)練好的模型對(duì)訓(xùn)練集的分類效果,可以理解為訓(xùn)練誤差。
- # test
- t_data = torch.zeros(100, 2)
- test_data = torch.normal(t_data, 5)
- test_result = net(test_data)
- prediction = torch.max(test_result, 1)[1]
- plt.scatter(test_data[:, 0], test_data[:, 1], s=100, c=prediction.data.numpy(), lw=0, cmap='RdYlGn')
- plt.show()
然后我們以 0 為 mean,隨機(jī)生成一些數(shù)據(jù),來(lái)看看模型會(huì)怎么去分類這些數(shù)據(jù)點(diǎn)。
雖然沒(méi)有畫(huà)出來(lái)那條訓(xùn)練好的分割線,但是我們也可以看到模型學(xué)習(xí)了一個(gè)分割的界面,來(lái)將數(shù)據(jù)劃分為兩類。