PS:要轉載請註明出處,本人版權所有。

PS: 這個只是基於《我自己》的理解,

如果和你的原則及想法相沖突,請諒解,勿噴。

前置說明

  本文作為本人csdn blog的主站的備份。(BlogID=110)

環境說明
  • Windows 10
  • VSCode
  • Python 3.8.10
  • Pytorch 1.8.1
  • Cuda 10.2

前言


  本文是此基礎補全計劃的最終篇,因為從我的角度來說,如果前面這些基礎知識都能夠了解及理解,再加上本文的這篇基礎知識,那麼我們算是小半隻腳踏入了大門。從這個時候,其實我們就已經可以做影象上的基本的分類任務了。除了分類任務,我們還有兩類重要的影象任務是目標檢測和影象分割,這兩項任務都和分類任務有一定的關聯,可以說,分類可以說是這兩類的基礎。

  卷積神經網路是一個專門為處理影象資料的網路。下面我們簡單的來看看卷積、池化的含義和怎麼計算的,然後我們通過一個LeNet5的經典網路,訓練一個分類模型。

卷積


  卷積是一種運算,類似加減乘除。卷積是一種運算,類似加減乘除。卷積是一種運算,類似加減乘除。重要的事情說三次。

  在數學上的定義是:連續n的情況\((f*g)(x) = \int f(n)g(x-n)dn\), 離散n的情況\((f*g)(x) = \sum\limits_{n} f(n)g(x-n)\)。從這裡我們可以看到,卷積就是測量函式f和函式g的翻轉且平移x後的重疊。其二維離散a,b的表達是\((f*g)(x1,x2) = \sum\limits_{a}\sum\limits_{b} f(a, b)g(x1-a, x2-b)\)

  卷積是一種運算,類似加減乘除。卷積是一種運算,類似加減乘除。卷積是一種運算,類似加減乘除。重要的事情再說三次。

  我們再次想一想,在之前的文章中,我們普遍都建立了一種想法是,把輸入資料拉成一條直線輸入的,這就意味著我們在之前的任務裡面只建立了相鄰輸入資料之間的左右關聯。但是我們可以想一想,是不是所有的資料只建立左右關聯就行了呢?顯而易見的,並不是這樣的,比如我們圖片,可能上下左右4個畫素加起來才是一個貓,如果我們只關聯了左右,那麼它可能是狗或者貓。那麼我們應該通過什麼樣的方式來對圖片畫素的這種二維關聯性進行描述或者計算呢?這種方法就是卷積運算。

  卷積網上有許許多多的介紹,大家都做了許多詳細的解答,包含訊號分析、複利、概率以及影象濾波等等方面的解釋。我個人認為我們可以拋開這些方面,從資料之間的關聯性來看這個問題可能是最好理解的,因為我們之前只關注了資料之間左右關聯,我們應該同時關注上下左右的關聯才對,我們要從空間的角度來考慮資料之間的關聯性。而卷積作為一種數學運算,他恰好是計算了資料的上下左右關聯性,因此卷積這種數學運算很適合拿來代替之前的一條線的線性運算。

  下面我們來看一下一個基本的卷積計算過程是什麼樣子的。

影象邊緣檢測例項

  計算程式碼如下:

  1. def corr2d(X, K): #@save
  2. """計算⼆維互相關運算。"""
  3. h, w = K.shape
  4. Y = np.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
  5. for i in range(Y.shape[0]):
  6. for j in range(Y.shape[1]):
  7. Y[i, j] = (X[i:i + h, j:j + w] * K).sum()
  8. return Y
  9. _X = np.ones((6, 8))
  10. _X[0:2, 2:6] = 0
  11. _X[3:, 2:6] = 0
  12. print(_X)
  13. _K = np.array([[1.0, -1.0]])
  14. _Y = corr2d(_X, _K)
  15. print(_Y)
  16. _Y = corr2d(_X, _K.T)
  17. print(_Y)

  結果如圖:

  我們可以分別的看到,影象邊緣的數值在經過我們手動構造的濾波器後,成功的檢測到邊緣資訊。

  在實際情況中,我們可能要學習邊緣,角點等等特徵,這個時候我們不可能手動去構造我們的濾波器,那麼我們可不可以通過學習的方式把濾波器學習出來呢?下面通過例項來演示:

  1. _X = np.ones((6, 8))
  2. _X[0:2, 2:6] = 0
  3. _X[3:, 2:6] = 0
  4. print(_X)
  5. _K = np.array([[1.0, -1.0]])
  6. _Y = corr2d(_X, _K)
  7. print(_Y)
  8. # _Y = corr2d(_X, _K.T)
  9. # print(_Y)
  10. X = torch.from_numpy(_X)
  11. X.requires_grad = True
  12. X = X.to(dtype=torch.float32)
  13. X = X.reshape(1, 1, 6, 8)
  14. Y = torch.from_numpy(_Y)
  15. Y.requires_grad = True
  16. conv2d = torch.nn.Conv2d(1, 1, (1, 2), bias=False)
  17. for i in range(20):
  18. y_train = conv2d(X)
  19. l = (y_train - Y)**2
  20. conv2d.zero_grad()
  21. # print(l.shape)
  22. l.backward(torch.ones_like(l))
  23. # print(conv2d.weight)
  24. with torch.no_grad():
  25. # print('grad = ', conv2d.weight.grad)
  26. conv2d.weight[:] -= 0.02 * conv2d.weight.grad
  27. # print(conv2d.weight)
  28. # print(conv2d.weight.shape)
  29. if (i + 1) % 2 == 0:
  30. print(f'batch {i+1}, loss {float(l.sum()):.3f}')
  31. print(conv2d.weight)

  結果如圖:

  我們通過corr2d函式構造出特徵Y,然後我們通過訓練特徵Y,我們可以看到最終卷積層的權重就是接近與1和-1,恰好等於我們構造的特殊濾波器。

  這個例項說明了,我們可以通過學習的方式來學習出一個我們想要的濾波器,不需要手動構造。

  此外卷積還有卷積核、步長、填充等等資料,我就不造輪子了,網上有很多大佬寫的很好的,大家去看看。此外這裡有個公式非常有用:N=(W-K+2P)/S+1。

池化


  我們在上文知道了卷積的輸出結果代表了一片上下左右資料的關聯性,比如一個畫素和之前的9個畫素有關聯,比如一個\(9*9\)的圖,經過一個卷積後,假如還是\(9*9\),這個時候輸出的\(9*9\)裡面的每個畫素我們已經和之前對應位置的一片畫素建立了關聯。但是某些時候,我們希望這種關聯性聚合起來,通過求最大值或者平均等等,這就是池化的概念。以之前例子為例:卷積輸出了\(9*9\)的畫素,經過池化之後,假如變成了\(3*3\),我們可以看到池化輸出的每個畫素代表之前卷積輸出的\(3*3\)個畫素,這代表我們的資訊聚集了,因為一個畫素代表了上一層的多個畫素。

  注意池化,我們還可以從視野的角度來看待,還是和上面的例子一樣,假如原圖上的貓是\(9*9\)的畫素,經過卷積池化之後,假如變成了\(3*3\), 這意味著我們從畫素的角度來說,之前81個畫素代表貓,現在9個畫素就可以代表了,也就是之前的一個畫素和現在的一個畫素代表的原圖視野不一樣了,形成了視野放大的感覺。但是有一個缺點就是,這可能導致小目標丟失了,這個在目標檢測裡面會關注到。

一個經典神經網路LeNet5


  在2017年12月份,我的這篇文章中《LeNet-5 論文及原理分析(笨鳥角度)》 ( https://blog.csdn.net/u011728480/article/details/78799672 )其實當時我為了學習一些基本知識,也對LeNet5的論文中網路結構部分做了細緻的分析。

  注意本文中的C3層和論文中的C3層不一樣。本文的C3層是\(16*6*(5*5+1) = 2496\)個引數。論文原文是\(6*(3*5*5+1)+6*(4*5*5+1)+3*(4*5*5+1)+1* (6*5*5+1)=1516\)個引數。

  訓練程式碼如下:

  1. import numpy as np
  2. from numpy.lib.utils import lookfor
  3. import torch
  4. from torchvision.transforms import ToTensor
  5. import os
  6. import torch
  7. from torch import nn
  8. from torch.nn.modules import activation
  9. from torch.nn.modules import linear
  10. from torch.nn.modules.linear import Linear
  11. from torch.utils.data import DataLoader
  12. from torchvision import datasets, transforms
  13. import visdom
  14. vis = visdom.Visdom(env='main')
  15. title = 'LeNet5 on ' + 'FashionMNIST'
  16. legend = ['TrainLoss', 'TestLoss', 'TestAcc']
  17. epoch_plot_window = vis.line(
  18. X=torch.zeros((1, 3)).cpu(),
  19. Y=torch.zeros((1, 3)).cpu(),
  20. win='epoch_win',
  21. opts=dict(
  22. xlabel='Epoch',
  23. ylabel='Loss/Acc',
  24. title=title,
  25. legend=legend
  26. ))
  27. def corr2d(X, W): #@save
  28. """計算⼆維互相關運算。"""
  29. h, w = W.shape
  30. Y = np.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
  31. for i in range(Y.shape[0]):
  32. for j in range(Y.shape[1]):
  33. Y[i, j] = (X[i:i + h, j:j + w] * W).sum()
  34. return Y
  35. def TrainConv2d():
  36. _X = np.ones((6, 8))
  37. _X[0:2, 2:6] = 0
  38. _X[3:, 2:6] = 0
  39. print(_X)
  40. _K = np.array([[1.0, -1.0]])
  41. _Y = corr2d(_X, _K)
  42. print(_Y)
  43. # _Y = corr2d(_X, _K.T)
  44. # print(_Y)
  45. X = torch.from_numpy(_X)
  46. X.requires_grad = True
  47. X = X.to(dtype=torch.float32)
  48. X = X.reshape(1, 1, 6, 8)
  49. Y = torch.from_numpy(_Y)
  50. Y.requires_grad = True
  51. conv2d = torch.nn.Conv2d(1, 1, (1, 2), bias=False)
  52. for i in range(20):
  53. y_train = conv2d(X)
  54. l = (y_train - Y)**2
  55. conv2d.zero_grad()
  56. # print(l.shape)
  57. l.backward(torch.ones_like(l))
  58. # print(conv2d.weight)
  59. with torch.no_grad():
  60. # print('grad = ', conv2d.weight.grad)
  61. conv2d.weight[:] -= 0.02 * conv2d.weight.grad
  62. # print(conv2d.weight)
  63. # print(conv2d.weight.shape)
  64. if (i + 1) % 2 == 0:
  65. print(f'batch {i+1}, loss {float(l.sum()):.3f}')
  66. print(conv2d.weight)
  67. class NeuralNetwork(nn.Module):
  68. def __init__(self):
  69. super(NeuralNetwork, self).__init__()
  70. self.lenet5 = nn.Sequential(
  71. # 6*28*28---->6*28*28
  72. nn.Conv2d(1, 6, (5, 5), stride=1, padding=2),
  73. nn.Sigmoid(),
  74. # 6*28*28----->6*14*14
  75. nn.AvgPool2d((2, 2), stride=2, padding=0),
  76. # 6*14*14----->16*10*10
  77. nn.Conv2d(6, 16, (5, 5), stride=1),
  78. nn.Sigmoid(),
  79. # 16*10*10------>16*5*5
  80. nn.AvgPool2d((2, 2), stride=2, padding=0),
  81. nn.Flatten(),
  82. nn.Linear(16*5*5, 1*120),
  83. nn.Sigmoid(),
  84. nn.Linear(1*120, 1*84),
  85. nn.Sigmoid(),
  86. nn.Linear(1*84, 1*10)
  87. )
  88. def forward(self, x):
  89. logits = self.lenet5(x)
  90. return logits
  91. def LoadFashionMNISTByTorchApi():
  92. # 60000*28*28
  93. training_data = datasets.FashionMNIST(
  94. root="..\data",
  95. train=True,
  96. download=True,
  97. transform=ToTensor()
  98. )
  99. # 10000*28*28
  100. test_data = datasets.FashionMNIST(
  101. root="..\data",
  102. train=False,
  103. download=True,
  104. transform=ToTensor()
  105. )
  106. # labels_map = {
  107. # 0: "T-Shirt",
  108. # 1: "Trouser",
  109. # 2: "Pullover",
  110. # 3: "Dress",
  111. # 4: "Coat",
  112. # 5: "Sandal",
  113. # 6: "Shirt",
  114. # 7: "Sneaker",
  115. # 8: "Bag",
  116. # 9: "Ankle Boot",
  117. # }
  118. # figure = plt.figure(figsize=(8, 8))
  119. # cols, rows = 3, 3
  120. # for i in range(1, cols * rows + 1):
  121. # sample_idx = torch.randint(len(training_data), size=(1,)).item()
  122. # img, label = training_data[sample_idx]
  123. # figure.add_subplot(rows, cols, i)
  124. # plt.title(labels_map[label])
  125. # plt.axis("off")
  126. # plt.imshow(img.squeeze(), cmap="gray")
  127. # plt.show()
  128. return training_data, test_data
  129. def train_loop(dataloader, model, loss_fn, optimizer):
  130. size = len(dataloader.dataset)
  131. num_batches = len(dataloader)
  132. loss_sum = 0
  133. for batch, (X, y) in enumerate(dataloader):
  134. # move X, y to gpu
  135. if torch.cuda.is_available():
  136. X = X.to('cuda')
  137. y = y.to('cuda')
  138. # Compute prediction and loss
  139. pred = model(X)
  140. loss = loss_fn(pred, y)
  141. # Backpropagation
  142. optimizer.zero_grad()
  143. loss.backward()
  144. optimizer.step()
  145. loss_sum += loss.item()
  146. if batch % 100 == 0:
  147. loss1, current = loss.item(), batch * len(X)
  148. print(f"loss: {loss1:>7f} [{current:>5d}/{size:>5d}]")
  149. return loss_sum/num_batches
  150. def test_loop(dataloader, model, loss_fn):
  151. size = len(dataloader.dataset)
  152. num_batches = len(dataloader)
  153. test_loss, correct = 0, 0
  154. with torch.no_grad():
  155. for X, y in dataloader:
  156. # move X, y to gpu
  157. if torch.cuda.is_available():
  158. X = X.to('cuda')
  159. y = y.to('cuda')
  160. pred = model(X)
  161. test_loss += loss_fn(pred, y).item()
  162. correct += (pred.argmax(1) == y).type(torch.float).sum().item()
  163. test_loss /= num_batches
  164. correct /= size
  165. print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")
  166. return test_loss, correct
  167. if __name__ == '__main__':
  168. # TrainConv2d()
  169. device = 'cuda' if torch.cuda.is_available() else 'cpu'
  170. print('Using {} device'.format(device))
  171. def init_weights(m):
  172. if type(m) == nn.Linear or type(m) == nn.Conv2d:
  173. nn.init.xavier_uniform_(m.weight)
  174. model = NeuralNetwork()
  175. model.apply(init_weights)
  176. model = model.to(device)
  177. print(model)
  178. batch_size = 200
  179. learning_rate = 0.9
  180. training_data, test_data = LoadFashionMNISTByTorchApi()
  181. train_dataloader = DataLoader(training_data, batch_size, shuffle=True)
  182. test_dataloader = DataLoader(test_data, batch_size, shuffle=True)
  183. loss_fn = nn.CrossEntropyLoss()
  184. optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
  185. epochs = 1000
  186. model.train()
  187. for t in range(epochs):
  188. print(f"Epoch {t+1}\n-------------------------------")
  189. train_loss = train_loop(train_dataloader, model, loss_fn, optimizer)
  190. test_loss, test_acc = test_loop(test_dataloader, model, loss_fn)
  191. vis.line(np.array([train_loss, test_loss, test_acc]).reshape(1, 3),
  192. np.ones((1, 3))*t,
  193. win='epoch_win',
  194. update=None if t == 0 else 'append',
  195. opts=dict(
  196. xlabel='Epoch',
  197. ylabel='Loss/Acc',
  198. title=title,
  199. legend=legend
  200. )
  201. )
  202. print("Done!")
  203. # only save param
  204. torch.save(model.state_dict(), 'lenet5.pth')
  205. # save param and net
  206. torch.save(model, 'lenet5-all.pth')
  207. # export onnx
  208. input_image = torch.zeros((1,1,28,28))
  209. input_image = input_image.to(device)
  210. torch.onnx.export(model, input_image, 'model.onnx')

  結果如圖:

  我們從訓練視覺化介面上可以看到,我們的模型確實是收斂了,但是不幸的是準確率大概有90%左右,而且存在過擬合現象。注意這裡我們這個模型,由於有Sigmoid層,導致了很容易出現梯度消失的情況,為了加快訓練,所以學習率設定的很大。

後記


  整理本系列的基礎知識的原因是需要加深對深度學習的理解。同時跟著參考資料,重複試驗,重複執行。對我個人而言,只有真實的寫了程式碼之後,才能夠理解的更加透徹。

  本文也是此係列的終篇,以後更新隨緣。

參考文獻


打賞、訂閱、收藏、丟香蕉、硬幣,請關注公眾號(攻城獅的搬磚之路)

PS: 請尊重原創,不喜勿噴。

PS: 要轉載請註明出處,本人版權所有。

PS: 有問題請留言,看到後我會第一時間回覆。