目錄
一、引言(環(huán)境)
?二、正文
1. 代碼基本情況介紹
2. MNIST數(shù)據(jù)集介紹?? ? ??
?3. 代碼輸出結(jié)果介紹
數(shù)據(jù)集取樣:
訓(xùn)練信息輸出:
前三次訓(xùn)練成果以及預(yù)測:
八次訓(xùn)練的結(jié)果:
?4. 代碼拆解講解
基本的參數(shù)設(shè)定
MNIST數(shù)據(jù)集下載、保存與加載
神經(jīng)網(wǎng)絡(luò)模型
訓(xùn)練前的準(zhǔn)備
樣本訓(xùn)練函數(shù)?
測試函數(shù)
模型的正式訓(xùn)練、測試、訓(xùn)練測試過程可視化、模型的使用
從磁盤中加載模型并繼續(xù)訓(xùn)練
5. 總體代碼
一、引言(環(huán)境)
- 本代碼基于Pytorch構(gòu)成,IDE為VSCode,請在學(xué)習(xí)代碼前尋找相應(yīng)的教程完成環(huán)境配置。Anaconda和Pytorch的安裝教程一抓一大把,這里給一個他人使用VSCode編輯器的教程:vscode+pytorch使用經(jīng)驗記錄(個人記錄+不定時更新)
- 本代碼本體來源指路:用PyTorch實現(xiàn)MNIST手寫數(shù)字識別(非常詳細)
- 本教程目的在于看懂每一行代碼甚至每一個參數(shù)。拒絕含混過關(guān)!非常適合初學(xué)者學(xué)習(xí)。
?二、正文
1. 代碼基本情況介紹
此代碼在Pytorch中構(gòu)建的是一個卷積神經(jīng)網(wǎng)絡(luò)(CNN),使用了兩個卷積層、兩個線性層,同時中間附帶了Dropout2d層防止過擬合。優(yōu)化器方面選擇的是帶有動量的隨機梯度下降法(SGD),損失函數(shù)使用的是負(fù)對數(shù)似然損失函數(shù)(negative log likelihood loss)
上面很多專有名詞聽不懂?沒關(guān)系,下面會一個一個解釋!
在功能上,代碼包含了MNIST數(shù)據(jù)集的下載與保存與加載、卷積神經(jīng)網(wǎng)路的構(gòu)建、模型的訓(xùn)練、模型的測試、模型的保存、模型的加載與繼續(xù)訓(xùn)練和測試、模型訓(xùn)練過程、測試過程的可視化、模型的使用。
2. MNIST數(shù)據(jù)集介紹?? ? ??
MNIST數(shù)據(jù)集是一個手寫數(shù)字識別數(shù)據(jù)集,由60,000個訓(xùn)練圖像和10,000個測試圖像組成。每個圖像都是28x28像素的灰度圖像,表示0到9之間的數(shù)字。數(shù)據(jù)集中的圖像已經(jīng)被標(biāo)記為它們所代表的數(shù)字。
該數(shù)據(jù)集最初由Yann LeCun等人創(chuàng)建,用于評估機器學(xué)習(xí)算法在手寫數(shù)字識別任務(wù)上的性能。由于其簡單性和廣泛使用,它已成為機器學(xué)習(xí)領(lǐng)域的基準(zhǔn)數(shù)據(jù)集之一,也被廣泛用于計算機視覺和深度學(xué)習(xí)的教學(xué)和研究。
?3. 代碼輸出結(jié)果介紹
為了更方便更直觀的感受本代碼的運行,并考察本代碼是否滿足需求,這里提供代碼的輸出,如果對輸出結(jié)果也一頭霧水也不要緊,直接去看代碼!
數(shù)據(jù)集取樣:
這里抽取了6個MNIST數(shù)據(jù)集中的內(nèi)容,Ground Truth代表其圖片本身所表示的正確數(shù)字,也是所謂的“標(biāo)簽”。?
訓(xùn)練信息輸出:
這是訓(xùn)練和測試的信息輸出,第一波訓(xùn)練會進行【3】次 ,第二波加載模型和優(yōu)化器后繼續(xù)訓(xùn)練會訓(xùn)練【5】次。訓(xùn)練信息包括當(dāng)前epoch、batch號、已處理的樣本數(shù)、總樣本數(shù)、當(dāng)前batch的損失值等;測試信息包括測試集的平均損失、正確預(yù)測的樣本數(shù)量、測試集的總樣本數(shù)量以及正確預(yù)測的樣本占比。
前三次訓(xùn)練成果以及預(yù)測:
前一張圖像是對前三次訓(xùn)練過程進行可視化,y軸是負(fù)對數(shù)似然損失函數(shù)值,即代表模型的訓(xùn)練效果,損失越小越好;x軸則是參與訓(xùn)練的樣本數(shù)量。藍色的線是訓(xùn)練loss,紅色的點是測試loss。
后一張圖片則是隨機抽樣并對圖片進行預(yù)測。上方的prediction后的數(shù)字就是用模型預(yù)測出來的值,可以跟下方的圖片進行對照觀察模型預(yù)測是否正確。
八次訓(xùn)練的結(jié)果:
這是在進行了8次訓(xùn)練后的結(jié)果圖像。
?4. 代碼拆解講解
這里將會分部分對代碼進行拆解講解,大家也可以滑倒最下面直接復(fù)制走完整代碼進行學(xué)習(xí),完整代碼中的注釋將包含本拆解講解的所有內(nèi)容。 如果代碼注釋中出現(xiàn)“具體看上面”等字眼,那么說明這里有對概念作詳細的解釋,解釋內(nèi)容以引用塊的形式放在了代碼下方。
基本的參數(shù)設(shè)定
# 循環(huán)整個訓(xùn)練數(shù)據(jù)集的次數(shù)
n_epochs = 3
# 一次訓(xùn)練的樣本數(shù)量
batch_size_train = 64
# 一次測試的樣本數(shù)量
batch_size_test = 1000
# 學(xué)習(xí)率,也可以理解為梯度下降法的速度
learning_rate = 0.01
# 動量,在后續(xù)的SGD中會使用
momentum = 0.9
# 記錄的頻率,后續(xù)會看到
log_interval = 10
# 為所有隨機數(shù)操作設(shè)置隨機種子
random_seed = 1
torch.manual_seed(random_seed)
這里包含了代碼需要用到的所有重要參數(shù),包括訓(xùn)練次數(shù)、訓(xùn)練batch(訓(xùn)練將被拆解成多個批次(batch))設(shè)置、測試batch設(shè)置、學(xué)習(xí)率(即優(yōu)化器,也即梯度下降的速度)、動量、記錄頻率等。
MNIST數(shù)據(jù)集下載、保存與加載
# batch_size (int): 每個批次的大小,即每次迭代中返回的數(shù)據(jù)樣本數(shù)。
# shuffle (bool): 是否在每個 epoch 之前打亂數(shù)據(jù)。如果設(shè)置為 True,則在每個 epoch 之前重新排列數(shù)據(jù)集以獲得更好的訓(xùn)練效果。
# torchvision.datasets.MNIST
# root:下載數(shù)據(jù)的目錄;
# train決定是否下載的是訓(xùn)練集;
# download為true時會主動下載數(shù)據(jù)集到指定目錄中,如果已存在則不會下載
# transform是接收PIL圖片并返回轉(zhuǎn)換后版本圖片的轉(zhuǎn)換函數(shù),
# transform 函數(shù)是一個 PyTorch 轉(zhuǎn)換操作,它將圖像轉(zhuǎn)換為張量并對其進行標(biāo)準(zhǔn)化,其中均值為 0.1307,標(biāo)準(zhǔn)差為 0.3081。
# 即將每一個圖像像素的值減去均值,除以標(biāo)準(zhǔn)差,如此使它們有相似的尺度,從而更容易地訓(xùn)練模型。
train_loader = torch.utils.data.DataLoader(
torchvision.datasets.MNIST('./data/', train=True, download=True,
transform=torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(
(0.1307,), (0.3081,))
])),
batch_size=batch_size_train, shuffle=True)
test_loader = torch.utils.data.DataLoader(
torchvision.datasets.MNIST('./data/', train=False, download=True,
transform=torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(
(0.1307,), (0.3081,))
])),
batch_size=batch_size_test, shuffle=True)
?torch.utils.data.DataLoader 是 PyTorch 中的一個數(shù)據(jù)加載器,用于將數(shù)據(jù)集封裝成可迭代對象,方便數(shù)據(jù)的批量讀取和處理。它可以自動進行數(shù)據(jù)的分批、打亂順序、并行加載等操作,同時還支持多進程加速。通常在訓(xùn)練神經(jīng)網(wǎng)絡(luò)時會使用 DataLoader 來讀取數(shù)據(jù)集,并配合 Dataset 類一起使用。
epoch的解釋:一個 epoch 表示對整個數(shù)據(jù)集進行一次完整的訓(xùn)練。通常情況下,一個 epoch 的迭代次數(shù)等于數(shù)據(jù)集的大小除以批次大小。例如,如果數(shù)據(jù)集包含 1000 個樣本,批次大小為 10,則一個 epoch 的迭代次數(shù)為 100。
·
在訓(xùn)練深度神經(jīng)網(wǎng)絡(luò)時,需要多次執(zhí)行 epoch 過程。通過多次迭代整個數(shù)據(jù)集,模型可以在訓(xùn)練過程中不斷優(yōu)化參數(shù),不斷提升預(yù)測性能。一個 epoch 包括將整個數(shù)據(jù)集中的所有樣本都輸入到模型中進行前向傳播、計算損失、反向傳播更新參數(shù)的過程。在完成一個 epoch 后,模型就會基于整個數(shù)據(jù)集上的訓(xùn)練結(jié)果進行了一次更新。
PIL的解釋:PIL(Python Imaging Library)是一個用于處理圖像的Python庫,它提供了幾種常見的圖像處理操作,例如縮放、剪裁、旋轉(zhuǎn)和濾鏡等。PIL圖像是由PIL庫加載的圖像對象,可以使用PIL中提供的方法對其進行各種操作和處理。
神經(jīng)網(wǎng)絡(luò)模型
# 開始建立神經(jīng)網(wǎng)絡(luò)模型,Net類繼承nn.Module類,
class Net(nn.Module):
def __init__(self):
# 完成一些模型初始化和必要的內(nèi)存分配等工作。確保Net類正確繼承了nn.Module的所有功能。
super(Net, self).__init__()
# 定義了一個名為self.conv1的卷積層對象,輸入通道數(shù)為1(因為是灰度圖像),輸出通道數(shù)為10,卷積核大小為5x5。
# 這意味著該卷積層將對輸入進行一次5x5的卷積操作,并將結(jié)果映射到10個輸出通道上。
# 不過這里的卷積操作有說每個卷積核都會隨機初始化權(quán)重和偏置項。這里我比較好奇是什么意思, 是一開始隨即賦值,然后在模型的反向傳播過程中,它們會被更新以最小化損失函數(shù),并使神經(jīng)網(wǎng)絡(luò)能夠更準(zhǔn)確地進行預(yù)測嗎
self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
# 同理,第二行代碼定義了一個名為self.conv2的卷積層對象,輸入通道數(shù)為10(因為self.conv1的輸出通道數(shù)為10),
# 輸出通道數(shù)為20,卷積核大小為5x5。該卷積層也將對其輸入進行一次5x5的卷積操作,并將結(jié)果映射到20個輸出通道上。
self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
# 設(shè)置Dropout2d層,這個的解釋可以看最上面,作用是隨機丟棄部分?jǐn)?shù)據(jù),防止過擬合
self.conv2_drop = nn.Dropout2d()
# 創(chuàng)建線性層(全連接層)接收一個320維的輸入張量,并將其映射到一個50維的特征空間中。
self.fc1 = nn.Linear(320, 50)
# 同理,此線性層接收50維的特征向量,并將其映射到10維的輸出空間中。
self.fc2 = nn.Linear(50, 10)
def forward(self, x):
# relu函數(shù)用于執(zhí)行ReLU(Rectified Linear Unit)激活函數(shù),將小于0的數(shù)據(jù)替換為0。max_pool2d函數(shù)用于執(zhí)行最大池化層采樣操作,具體含義見上
# 這里就是先對輸入數(shù)據(jù)x執(zhí)行一次卷積操作,將其映射到10個輸出通道上,然后對卷積操作的結(jié)果進行2x2的最大池化操作。
# 最大池化操作中參數(shù)為:input,輸入張量;kernel_size,池化層窗口大小,stride:步幅,默認(rèn)為kernel_size
x = F.relu(F.max_pool2d(self.conv1(x), 2))
# 和上面同理,不同的是增加了Dropout2d層,防止數(shù)據(jù)過擬合
x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
# 將輸入x的形狀從(batch_size, num_channels, height, width)變換為(batch_size, 320),
# 其中batch_size表示輸入的數(shù)據(jù)樣本數(shù),num_channels表示輸入數(shù)據(jù)的通道數(shù),height表示輸入數(shù)據(jù)的高度,width表示輸入數(shù)據(jù)的寬度。
# 在這里,-1參數(shù)表示自動推斷該維度上的大小,因為可以根據(jù)輸入數(shù)據(jù)的大小自動確定batch_size的大小。
# 另外,320的大小是通過卷積層和池化層的輸出計算得到的。具體計算過程可以看上面。
x = x.view(-1, 320)
# 將輸入x通過全連接層self.fc1進行線性變換,然后使用ReLU激活函數(shù)對輸出結(jié)果進行非線性變換
x = F.relu(self.fc1(x))
# 在傳遞給全連接層之前,在輸入張量x上應(yīng)用dropout操作。
# 其中,self.training是Net類中的一個布爾值參數(shù),用于指示當(dāng)前模型是否處于訓(xùn)練模式。
# 當(dāng)self.training為True時,dropout操作將被啟用,否則所有神經(jīng)元都被保留,不進行dropout操作。
# 這樣,在測試或評估模型的時候,dropout操作就會被關(guān)閉,模型將使用完整的權(quán)重來進行預(yù)測,以提高預(yù)測準(zhǔn)確性。
# 對于其的補充說明可以看上面
x = F.dropout(x, training=self.training)
# 將輸入x通過全連接層self.fc1進行線性變換,最終映射至10個通道中,這是因為我們想將其分為10個類別
x = self.fc2(x)
# 進行softmax操作然后再取對數(shù),softmax這個操作的含義可以看上面,簡而言之就是求取每個類別的概率并歸一化
# dim=1 意思是對x的第二維度進行操作,x現(xiàn)在為(batch_size, 10),也就是在10(num_classes)這個地方進行操作。
# 取對數(shù)的原因也是為了更方便計算和求取損失,具體可以看上面的補充解釋。
return F.log_softmax(x, dim=1)
torch.nn:torch.nn是PyTorch深度學(xué)習(xí)框架中的一個模塊,它提供了各種用于搭建神經(jīng)網(wǎng)絡(luò)的類和函數(shù),例如各種層(如全連接層、卷積層等)、激活函數(shù)(如ReLU、sigmoid等)以及損失函數(shù)(如交叉熵、均方誤差等),可以幫助用戶更方便地搭建、訓(xùn)練和評估神經(jīng)網(wǎng)絡(luò)模型。
卷積層的工作原理:卷積層是神經(jīng)網(wǎng)絡(luò)中常用的一種層類型,它通過卷積操作對輸入數(shù)據(jù)進行特征提取和轉(zhuǎn)換。在卷積層中,輸入數(shù)據(jù)通常是一個多通道的二維矩陣,例如一張彩色圖像。卷積層包括若干個卷積核(也稱過濾器),每個卷積核都是一個小的二維矩陣,其大小一般遠小于輸入數(shù)據(jù)的大小。卷積操作就是將卷積核在輸入數(shù)據(jù)上滑動并按元素相乘得到一個新的二維矩陣。這個過程可以看作是一種局部的線性變換,將輸入數(shù)據(jù)中的某些位置和周圍鄰域的信息合并起來得到新的特征表示。
·
在卷積操作后,可以使用激活函數(shù)(如ReLU)對結(jié)果進行非線性變換,從而增強卷積層的表達能力。此外,還可以加入偏置項進行平移操作,以進一步增強模型的靈活性和表達能力。卷積層的輸出通常會被送入下一層進行處理,或者直接作為模型的輸出結(jié)果。由于卷積核的參數(shù)可以共享,卷積層具有很強的參數(shù)復(fù)用性,可以大大減少模型的參數(shù)數(shù)量和計算量,從而提高模型的效率和性能。
Dropout2d層:Dropout2d層是深度學(xué)習(xí)中常用的一種正則化技術(shù),可以隨機地“丟棄”(即將它們的值設(shè)置為0)輸入張量中的部分特征圖。這有助于減少模型對輸入數(shù)據(jù)的過擬合程度,提高泛化能力。
·
Dropout2d層的補充說明:正則化:
在機器學(xué)習(xí)中,正則化是一種用于減少過擬合的技術(shù)。在訓(xùn)練模型時,如果模型過度擬合訓(xùn)練數(shù)據(jù),那么它將不能很好地泛化到新的數(shù)據(jù)上,這將導(dǎo)致性能下降。正則化技術(shù)通過修改模型的損失函數(shù),在訓(xùn)練過程中“懲罰”模型參數(shù)的大小或復(fù)雜性,從而限制模型的學(xué)習(xí)能力,防止其過擬合訓(xùn)練數(shù)據(jù)。正則化技術(shù)可以應(yīng)用于不同類型的模型和任務(wù),包括線性回歸、邏輯回歸、神經(jīng)網(wǎng)絡(luò)等,在實踐中被廣泛使用。
線性層(全連接層):線性層是深度學(xué)習(xí)神經(jīng)網(wǎng)絡(luò)的基本組成部分之一,其工作原理是將輸入數(shù)據(jù)進行矩陣乘法并加上一個偏置向量,得到輸出結(jié)果。具體地說,每個輸入特征都與權(quán)重矩陣中的對應(yīng)元素相乘,然后將所有結(jié)果相加,在加上偏置向量。這個過程可以用下面的公式表示:
·
????????????????????????y = Wx + b
其中,W是權(quán)重矩陣,x是輸入向量,b是偏置向量,y是輸出向量。
·
線性層的作用在于對輸入數(shù)據(jù)進行線性轉(zhuǎn)換,從而為神經(jīng)網(wǎng)絡(luò)提供更高級別的特征。權(quán)重矩陣和偏置項是通過訓(xùn)練神經(jīng)網(wǎng)絡(luò)來確定的。在訓(xùn)練過程中,神經(jīng)網(wǎng)絡(luò)根據(jù)給定的輸入和正確的輸出來不斷調(diào)整權(quán)重矩陣和偏置項,以使得網(wǎng)絡(luò)能夠更準(zhǔn)確地預(yù)測輸出。訓(xùn)練使用反向傳播算法進行,該算法計算每個權(quán)重對誤差的貢獻,并相應(yīng)地更新權(quán)重矩陣和偏置項。最終,經(jīng)過足夠的訓(xùn)練,權(quán)重矩陣和偏置項將被優(yōu)化以使得神經(jīng)網(wǎng)絡(luò)能夠更好地適應(yīng)輸入輸出的關(guān)系。
最大池化操作:在卷積神經(jīng)網(wǎng)絡(luò)中,池化(Pooling)是一種常用的操作。其中最大池化(Max Pooling)是一種常見的降采樣函數(shù),通常用于減小特征圖的空間尺寸大小并增強模型的位置不變性。最大池化層的工作原理是在輸入的局部感受野中選取最大響應(yīng)值,并將其作為輸出。池化窗口在輸入張量上滑動,并計算每個窗口內(nèi)元素的最大值。具體來說,最大池化層將目標(biāo)圖像的每個矩形區(qū)域替換為該區(qū)域內(nèi)的最大的數(shù)值。池化窗口的大小和步幅是可以調(diào)節(jié)的,通常在池化過程中間隔著一個固定的滑動窗口,它的大小和步幅可以設(shè)置。
·
例如,對于一個2x2的池化窗口,如果輸入數(shù)據(jù)是4x4大小,一般會將池化窗口從左到右、從上到下遍歷整個輸入數(shù)據(jù),然后選出每個窗口內(nèi)的最大值作為池化操作的輸出。這樣,就能夠?qū)⑤斎霐?shù)據(jù)的尺寸縮小到原來的1/4,而且仍然保留了最顯著的特征信息。需要注意的是,最大池化層并不會學(xué)習(xí)任何參數(shù),它只是對輸入進行固定的數(shù)值計算。因此,最大池化層通常被用于特征降維和提取。
?這里的ReLU激活函數(shù):當(dāng)輸入張量中元素小于零時,該函數(shù)將它們替換為零;否則保持不變。ReLU激活函數(shù)在深度學(xué)習(xí)中被廣泛使用,因為它可以增加模型的稀疏性和非線性特征表達能力,并且計算速度快。
?320是如何計算出來的:假設(shè)輸入數(shù)據(jù)的大小為(1, 1, 28, 28),其中1表示batch_size,1表示輸入數(shù)據(jù)的通道數(shù),28表示輸入數(shù)據(jù)的高度,28表示輸入數(shù)據(jù)的寬度。事實上,在MNIST數(shù)據(jù)集中,每張圖片也是28*28的灰度圖片
·
在經(jīng)過第一個卷積層之后,輸出的形狀為(1, 10, 24, 24),其中10表示輸出特征圖的數(shù)量,24表示特征圖的高度和寬度都減少了4個像素(由于卷積核大小為5,步長為1,因此特征圖的高度和寬度都會減少4個像素)。
·
接著,經(jīng)過第一個池化層之后,輸出的形狀為(1, 10, 12, 12),其中12=24/2,即特征圖的高度和寬度都減少了一半。同理,經(jīng)過第二個卷積層之后,輸出的形狀為(1, 20, 8, 8),其中20表示輸出特征圖的數(shù)量,8=12-2x2,即特征圖的高度和寬度都減少了4個像素。最后,經(jīng)過第二個池化層之后,輸出的形狀為(1, 20, 4, 4),其中4=8/2,即特征圖的高度和寬度都減少了一半。
·
因此,假設(shè)輸入數(shù)據(jù)的大小為(1, 1, 28, 28),并且經(jīng)過了兩個卷積層和兩個池化層之后,其輸出大小為(1, 20, 4, 4)。因此320 = 20 * 4 * 4,將每個樣本的所有通道的像素值拉成一行,最終得到一個(batch_size, 320)的輸出張量。
F.dropout函數(shù)補充說明(其實和之前的dropout層差不多):在神經(jīng)網(wǎng)絡(luò)中,Dropout是一種常用的正則化技術(shù),它可以隨機地在神經(jīng)網(wǎng)絡(luò)的某些神經(jīng)元之間添加斷開連接。這個過程可以強制使神經(jīng)網(wǎng)絡(luò)學(xué)習(xí)到更加健壯和泛化的特征,因為它會防止任何一個單獨的神經(jīng)元對結(jié)果產(chǎn)生太大的影響。
softmax操作:softmax操作是一種常用的歸一化方法,將一個向量中的每個元素轉(zhuǎn)化為一個介于0~1之間的值,并且這些值的和等于1。它通常用于多分類問題中,以便計算損失函數(shù)和模型預(yù)測的準(zhǔn)確性。具體來說,對于輸入x的每一行,softmax操作將計算其指數(shù)值(exp(xi)),并將其除以該行所有元素的指數(shù)之和(也稱為歸一化常數(shù))
·
補充:接著通常也會取對數(shù),這是為了防止出現(xiàn)概率太小的情況影響計算,于是我們一般取對數(shù)以便更容易計算和優(yōu)化損失。
訓(xùn)練前的準(zhǔn)備
# 創(chuàng)建神經(jīng)網(wǎng)絡(luò)
network = Net()
# 使用SGD(隨機梯度下降)優(yōu)化器,注意這里的SGD是帶動量的,具體解釋可以看上面,還有SGD和GD的區(qū)別
# network.parameters()是要訓(xùn)練的參數(shù),lr是學(xué)習(xí)率,超參數(shù)之一;momentum是動量,也是超參數(shù)之一。不過這里的動量最好設(shè)置為0.9
# network.parameters()返回一個包含了Net類中所有可訓(xùn)練參數(shù)的迭代器。這些可訓(xùn)練參數(shù)包括神經(jīng)網(wǎng)絡(luò)中的權(quán)重和偏置項等。
# 在優(yōu)化器中使用這個迭代器,可以告訴優(yōu)化器需要更新哪些參數(shù)以最小化損失函數(shù)。
optimizer = optim.SGD(network.parameters(), lr=learning_rate, momentum=momentum)
# 用于記錄和繪圖的參數(shù)
train_losses = []
train_counter = []
test_losses = []
# [0, 60000, 120000, 180000]
test_counter = [i * len(train_loader.dataset) for i in range(n_epochs + 1)]
# print(test_counter)
# breakpoint1 = input("print(test_counter): ") # 看看這里是什么
帶動量的SGD:帶動量的SGD算法(Momentum SGD)是SGD的一種改進,它可以加速模型的收斂并減少參數(shù)更新的震蕩。SGD是隨機梯度下降法,每次會抽取一定樣本計算它們的梯度平均值進行更新,這也是隨機梯度下降法和梯度下降法的區(qū)別,梯度下降法每次是計算所有樣本。
·
具體來說,帶動量的SGD算法在計算參數(shù)更新時,不僅考慮當(dāng)前時刻的梯度,還會考慮之前梯度更新的方向。這個方向被稱為“動量”。動量是為了進行指數(shù)加權(quán)平均操作解釋可以看https://zhuanlan.zhihu.com/p/73264637,這個里面解釋的比較詳細,梯度每次更新的方向中都會包含前面梯度更新的方向的信息
·
補充,SGD和GD區(qū)別:普通的梯度下降法每次需要遍歷全部訓(xùn)練數(shù)據(jù)來計算梯度并更新模型參數(shù),而隨機梯度下降法每次只使用一個樣本來計算梯度并更新模型參數(shù)。經(jīng)典的梯度下降法在每次對模型參數(shù)進行更新時,需要遍歷所有的訓(xùn)練數(shù)據(jù)。當(dāng)M很大的時候,就需要耗費巨大的計算資源和計算時間,這在實際過程中基本不可行。
·
隨機梯度下降法(Stochastic Gradient Descent, SGD)應(yīng)運而生。它采用單個訓(xùn)練樣本的損失來近似平均損失相對于普通的梯度下降法,隨機梯度下降法的優(yōu)勢在于速度更快、更容易逃離局部極小點、能夠處理大規(guī)模數(shù)據(jù)集以及可以實時學(xué)習(xí)在線數(shù)據(jù)。然而,由于隨機梯度下降法所使用的樣本是隨機選擇的,因此其更新過程具有一定的噪聲性質(zhì),需要更多的迭代次數(shù)才能收斂到全局最優(yōu)解。同時,隨機梯度下降法也比較難以用于處理稀疏數(shù)據(jù)。
樣本訓(xùn)練函數(shù)?
def train(epoch):
# 用于將神經(jīng)網(wǎng)絡(luò)設(shè)置為訓(xùn)練模式。在訓(xùn)練神經(jīng)網(wǎng)絡(luò)時,需要將其切換到訓(xùn)練模式,以便啟用Batch Normalization和Dropout等高級優(yōu)化技術(shù),
# 并且在每個batch處理完畢后,可以清除所有中間狀態(tài)(如梯度)以準(zhǔn)備下一次訓(xùn)練。
network.train()
for batch_idx, (data, target) in enumerate(train_loader):
# 是用于清除模型參數(shù)的梯度信息。在訓(xùn)練過程中,優(yōu)化器會累加每個參數(shù)的梯度,
# 因此在每個 batch 計算結(jié)束后,需要使用 zero_grad() 清空之前累計的梯度,避免對下一個 batch 的計算造成影響。
optimizer.zero_grad()
# 獲取訓(xùn)練結(jié)果
output = network(data)
# 損失函數(shù)定義,這里使用的是負(fù)對數(shù)似然損失函數(shù)(negative log likelihood loss),具體解釋可以看上面
# F.nll_loss()函數(shù)計算了模型輸出和目標(biāo)輸出之間的差異,并返回一個標(biāo)量值作為損失值,該值越小表示模型越接近目標(biāo)。
loss = F.nll_loss(output, target)
# loss.backward() 就是用來計算當(dāng)前 mini-batch 的損失函數(shù)關(guān)于模型參數(shù)的梯度的代碼。
# 該函數(shù)會在計算完梯度之后將它們存儲在參數(shù)的 grad 屬性中。接著,我們可以使用 optimizer.step() 函數(shù)來更新模型參數(shù)。
# 這里使用的是反向傳播算法(backpropagation)來計算網(wǎng)絡(luò)參數(shù)的梯度,
loss.backward()
# 使用優(yōu)化器更新模型參數(shù)
optimizer.step()
# 每隔log_interval個batch打印一次訓(xùn)練信息,包括當(dāng)前epoch、batch號、已處理的樣本數(shù)、總樣本數(shù)、當(dāng)前batch的損失值等
if batch_idx % log_interval == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(epoch, batch_idx * len(data),
len(train_loader.dataset),
100. * batch_idx / len(train_loader),
loss.item()))
# 記錄損失值
train_losses.append(loss.item())
# 記錄目前已訓(xùn)練完成的樣本數(shù)量
train_counter.append((batch_idx * 64) + ((epoch - 1) * len(train_loader.dataset)))
# 將神經(jīng)網(wǎng)絡(luò)的權(quán)重和優(yōu)化器的狀態(tài)保存到硬盤上,方便之后加載和使用。
# network.state_dict() 返回一個字典,其中包含了神經(jīng)網(wǎng)絡(luò)的所有參數(shù)(即權(quán)重和偏置項),以及它們對應(yīng)的名稱。這些參數(shù)可以用來恢復(fù)網(wǎng)絡(luò)的狀態(tài)。
# torch.save() 函數(shù)將字典對象保存到指定的文件路徑上。第一個參數(shù)是要保存的對象,第二個參數(shù)是文件路徑。
torch.save(network.state_dict(), './model.pth')
# 這是在保存優(yōu)化器的狀態(tài)。
torch.save(optimizer.state_dict(), './optimizer.pth')
負(fù)對數(shù)似然損失函數(shù)(negative log likelihood loss):通常用于多分類問題。它的基本思想是將模型輸出的概率分布與真實標(biāo)簽的 one-hot 編碼進行比較,計算兩者之間的差異。該損失函數(shù)先取每個樣本真實標(biāo)簽對應(yīng)的預(yù)測概率的自然對數(shù),再對它們求平均值并取相反數(shù),所以稱為“負(fù)對數(shù)似然”損失函數(shù)。使用該損失函數(shù)最終得到的模型會使得正確分類的樣本的損失越小越好,而錯誤分類的樣本的損失越大越好,因此能夠有效地訓(xùn)練出具有良好泛化性能的模型。
測試函數(shù)
def test():
# 將神經(jīng)網(wǎng)絡(luò)設(shè)置為評估模式。在評估模式下,模型會停用特定步驟,如Dropout層、Batch Normalization層等,
# 并且使用訓(xùn)練期間學(xué)到的參數(shù)來生成預(yù)測,而不是在訓(xùn)練集上進行梯度反向傳播和權(quán)重更新。
network.eval()
test_loss = 0
correct = 0
# 關(guān)閉梯度計算,以減少內(nèi)存消耗和加快模型評估過程。這意味著,在使用torch.no_grad()時,模型的參數(shù)不會被更新和優(yōu)化;
# 同時,計算圖也不會被跟蹤和記錄,這使得前向傳播的速度更快。
with torch.no_grad():
for data, target in test_loader:
output = network(data)
# 記錄模型的損失值。reduction='sum'表示將每個樣本的損失求和后再返回。
test_loss += F.nll_loss(output, target, reduction='sum').item()
# 找到輸出結(jié)果(張量)中,每一行(即第1維度)最大的那個數(shù)以及它所在的位置,返回一個元組。其中元組的第一個元素就是最大的那個數(shù),第二個元素是最大數(shù)的位置。
# 這里的 keepdim=True 參數(shù)表示保持維度不變,也就是說返回的結(jié)果張量維度與原始張量相同。
# [1] 表示取出這個元組的第二個元素,也就是位置信息,賦給了變量 pred。這個 pred 變量就是神經(jīng)網(wǎng)絡(luò)預(yù)測出來的類別標(biāo)簽,
# 它的維度與原始張量第 1 維度相同,表示每一個輸入數(shù)據(jù)樣本對應(yīng)的類別標(biāo)簽。
pred = output.data.max(1, keepdim=True)[1]
# target.data.view_as(pred)將 target 張量按照 pred 張量的形狀進行重塑(reshape)。
# 具體地說,它會返回一個和 pred 張量形狀相同、但數(shù)據(jù)來自 target 張量的新張量。
# .eq()方法來進行張量之間的逐元素比較,得到一個由布爾值組成的張量,表示pred和target.data.view_as(pred)中的每個元素是否相等。
# 如果該元素相等,則對應(yīng)位置為True,否則為False。
# .sum():對前一步得到的True/False tensor沿著所有維度求和,得到預(yù)測正確的樣本數(shù)。
# +=:將這個batch中預(yù)測正確的樣本數(shù)添加到之前已經(jīng)處理過的樣本中,累加得到整個數(shù)據(jù)集中預(yù)測正確的樣本數(shù)(correct)。
correct += pred.eq(target.data.view_as(pred)).sum()
# 計算樣本的平均損失
test_loss /= len(test_loader.dataset)
# 記錄本次的測試結(jié)果
test_losses.append(test_loss)
# 打印本次測試結(jié)果,包括測試集的平均損失、正確預(yù)測的樣本數(shù)量、測試集的總樣本數(shù)量以及正確預(yù)測的樣本占比。
print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))
模型的正式訓(xùn)練、測試、訓(xùn)練測試過程可視化、模型的使用
test() # 不加這個,后面畫圖就會報錯:x and y must be the same size,因為test_counter中包含了模型未經(jīng)訓(xùn)練時的情況(上面打印中的“0”)
# 進入正式的訓(xùn)練、測試過程,根據(jù)設(shè)定的epoch進行訓(xùn)練
for epoch in range(1, n_epochs + 1):
train(epoch)
test()
# 繪制整個訓(xùn)練和測試的圖像,包括記錄訓(xùn)練的損失變化、測試的損失變化
fig = plt.figure()
plt.plot(train_counter, train_losses, color='blue')
plt.scatter(test_counter, test_losses, color='red')
plt.legend(['Train Loss', 'Test Loss'], loc='upper right')
plt.xlabel('number of training examples seen')
plt.ylabel('negative log likelihood loss')
plt.show()
從磁盤中加載模型并繼續(xù)訓(xùn)練
# ----------------------------------------------------------- #
# 檢查點的持續(xù)訓(xùn)練,繼續(xù)對網(wǎng)絡(luò)進行訓(xùn)練,或者看看如何從第一次培訓(xùn)運行時保存的state_dicts中繼續(xù)進行訓(xùn)練。
# 初始化一組新的網(wǎng)絡(luò)和優(yōu)化器。
continued_network = Net()
continued_optimizer = optim.SGD(network.parameters(), lr=learning_rate, momentum=momentum)
# 加載網(wǎng)絡(luò)的內(nèi)部狀態(tài)、優(yōu)化器的內(nèi)部狀態(tài)
network_state_dict = torch.load('model.pth')
continued_network.load_state_dict(network_state_dict)
optimizer_state_dict = torch.load('optimizer.pth')
continued_optimizer.load_state_dict(optimizer_state_dict)
# 為什么是“4”開始呢,因為n_epochs=3,上面用了[1, n_epochs + 1)
# 繼續(xù)進行5次訓(xùn)練
for i in range(4, 9):
test_counter.append(i*len(train_loader.dataset))
train(i)
test()
# 繪制訓(xùn)練的曲線
fig = plt.figure()
plt.plot(train_counter, train_losses, color='blue')
plt.scatter(test_counter, test_losses, color='red')
plt.legend(['Train Loss', 'Test Loss'], loc='upper right')
plt.xlabel('number of training examples seen')
plt.ylabel('negative log likelihood loss')
plt.show()
5. 總體代碼
注意,下面這段代碼已經(jīng)包含上方所有科普信息和幾乎每一段代碼的注釋,包含函數(shù)所使用的所有參數(shù)所表達的含義。大家可以直接把代碼拿走一點一點看就行啦。文章來源:http://www.zghlxwxcb.cn/news/detail-418877.html
代碼上半部分集中解釋了部分的概念,但還是建議大家從第一行正式的代碼看起,注釋中如果有涉及到概念解釋的都會進行標(biāo)注,大家如果有需要可以再回到上方觀看概念性注釋。文章來源地址http://www.zghlxwxcb.cn/news/detail-418877.html
import torch
import torchvision
from torch.utils.data import DataLoader
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import matplotlib.pyplot as plt
# epoch的解釋:
# 一個 epoch 表示對整個數(shù)據(jù)集進行一次完整的訓(xùn)練。通常情況下,一個 epoch 的迭代次數(shù)等于數(shù)據(jù)集的大小除以批次大小。
# 例如,如果數(shù)據(jù)集包含 1000 個樣本,批次大小為 10,則一個 epoch 的迭代次數(shù)為 100。
# 在訓(xùn)練深度神經(jīng)網(wǎng)絡(luò)時,需要多次執(zhí)行 epoch 過程。通過多次迭代整個數(shù)據(jù)集,模型可以在訓(xùn)練過程中不斷優(yōu)化參數(shù),不斷提升預(yù)測性能。
# 一個 epoch 包括將整個數(shù)據(jù)集中的所有樣本都輸入到模型中進行前向傳播、計算損失、反向傳播更新參數(shù)的過程。
# 在完成一個 epoch 后,模型就會基于整個數(shù)據(jù)集上的訓(xùn)練結(jié)果進行了一次更新。
# PIL的解釋:
# PIL(Python Imaging Library)是一個用于處理圖像的Python庫,它提供了幾種常見的圖像處理操作,例如縮放、剪裁、旋轉(zhuǎn)和濾鏡等。
# PIL圖像是由PIL庫加載的圖像對象,可以使用PIL中提供的方法對其進行各種操作和處理。
# torch.nn
# torch.nn是PyTorch深度學(xué)習(xí)框架中的一個模塊,它提供了各種用于搭建神經(jīng)網(wǎng)絡(luò)的類和函數(shù),
# 例如各種層(如全連接層、卷積層等)、激活函數(shù)(如ReLU、sigmoid等)以及損失函數(shù)(如交叉熵、均方誤差等),
# 可以幫助用戶更方便地搭建、訓(xùn)練和評估神經(jīng)網(wǎng)絡(luò)模型。
# 卷積層的工作原理:
# 卷積層是神經(jīng)網(wǎng)絡(luò)中常用的一種層類型,它通過卷積操作對輸入數(shù)據(jù)進行特征提取和轉(zhuǎn)換。
# 在卷積層中,輸入數(shù)據(jù)通常是一個多通道的二維矩陣,例如一張彩色圖像。卷積層包括若干個卷積核(也稱過濾器),每個卷積核都是一個小的二維矩陣,其大小一般遠小于輸入數(shù)據(jù)的大小。
# 卷積操作就是將卷積核在輸入數(shù)據(jù)上滑動并按元素相乘得到一個新的二維矩陣。這個過程可以看作是一種局部的線性變換,將輸入數(shù)據(jù)中的某些位置和周圍鄰域的信息合并起來得到新的特征表示。
# 在卷積操作后,可以使用激活函數(shù)(如ReLU)對結(jié)果進行非線性變換,從而增強卷積層的表達能力。
# 此外,還可以加入偏置項進行平移操作,以進一步增強模型的靈活性和表達能力。
# 卷積層的輸出通常會被送入下一層進行處理,或者直接作為模型的輸出結(jié)果。由于卷積核的參數(shù)可以共享,卷積層具有很強的參數(shù)復(fù)用性,
# 可以大大減少模型的參數(shù)數(shù)量和計算量,從而提高模型的效率和性能。
# Dropout2d層:
# Dropout2d層是深度學(xué)習(xí)中常用的一種正則化技術(shù),可以隨機地“丟棄”(即將它們的值設(shè)置為0)輸入張量中的部分特征圖。
# 這有助于減少模型對輸入數(shù)據(jù)的過擬合程度,提高泛化能力。
# Dropout2d層的補充說明:正則化:
# 在機器學(xué)習(xí)中,正則化是一種用于減少過擬合的技術(shù)。在訓(xùn)練模型時,如果模型過度擬合訓(xùn)練數(shù)據(jù),那么它將不能很好地泛化到新的數(shù)據(jù)上,這將導(dǎo)致性能下降。
# 正則化技術(shù)通過修改模型的損失函數(shù),在訓(xùn)練過程中“懲罰”模型參數(shù)的大小或復(fù)雜性,從而限制模型的學(xué)習(xí)能力,防止其過擬合訓(xùn)練數(shù)據(jù)。
# 正則化技術(shù)可以應(yīng)用于不同類型的模型和任務(wù),包括線性回歸、邏輯回歸、神經(jīng)網(wǎng)絡(luò)等,在實踐中被廣泛使用。
# 線性層(全連接層):
# 線性層是深度學(xué)習(xí)神經(jīng)網(wǎng)絡(luò)的基本組成部分之一,其工作原理是將輸入數(shù)據(jù)進行矩陣乘法并加上一個偏置向量,得到輸出結(jié)果。
# 具體地說,每個輸入特征都與權(quán)重矩陣中的對應(yīng)元素相乘,然后將所有結(jié)果相加,在加上偏置向量。這個過程可以用下面的公式表示:
# y = Wx + b
# 其中,W是權(quán)重矩陣,x是輸入向量,b是偏置向量,y是輸出向量。
# 線性層的作用在于對輸入數(shù)據(jù)進行線性轉(zhuǎn)換,從而為神經(jīng)網(wǎng)絡(luò)提供更高級別的特征。
# 權(quán)重矩陣和偏置項是通過訓(xùn)練神經(jīng)網(wǎng)絡(luò)來確定的。在訓(xùn)練過程中,神經(jīng)網(wǎng)絡(luò)根據(jù)給定的輸入和正確的輸出來不斷調(diào)整權(quán)重矩陣和偏置項,
# 以使得網(wǎng)絡(luò)能夠更準(zhǔn)確地預(yù)測輸出。訓(xùn)練使用反向傳播算法進行,該算法計算每個權(quán)重對誤差的貢獻,并相應(yīng)地更新權(quán)重矩陣和偏置項。
# 最終,經(jīng)過足夠的訓(xùn)練,權(quán)重矩陣和偏置項將被優(yōu)化以使得神經(jīng)網(wǎng)絡(luò)能夠更好地適應(yīng)輸入輸出的關(guān)系。
# 最大池化操作:
# 在卷積神經(jīng)網(wǎng)絡(luò)中,池化(Pooling)是一種常用的操作。其中最大池化(Max Pooling)是一種常見的降采樣函數(shù),通常用于減小特征圖的空間尺寸大小并增強模型的位置不變性。
# 最大池化層的工作原理是在輸入的局部感受野中選取最大響應(yīng)值,并將其作為輸出。池化窗口在輸入張量上滑動,并計算每個窗口內(nèi)元素的最大值。
# 具體來說,最大池化層將目標(biāo)圖像的每個矩形區(qū)域替換為該區(qū)域內(nèi)的最大的數(shù)值。池化窗口的大小和步幅是可以調(diào)節(jié)的,通常在池化過程中間隔著一個固定的滑動窗口,它的大小和步幅可以設(shè)置。
# 例如,對于一個2x2的池化窗口,如果輸入數(shù)據(jù)是4x4大小,一般會將池化窗口從左到右、從上到下遍歷整個輸入數(shù)據(jù),然后選出每個窗口內(nèi)的最大值作為池化操作的輸出。
# 這樣,就能夠?qū)⑤斎霐?shù)據(jù)的尺寸縮小到原來的1/4,而且仍然保留了最顯著的特征信息。
# 需要注意的是,最大池化層并不會學(xué)習(xí)任何參數(shù),它只是對輸入進行固定的數(shù)值計算。因此,最大池化層通常被用于特征降維和提取。
# 這里的ReLU激活函數(shù):
# 當(dāng)輸入張量中元素小于零時,該函數(shù)將它們替換為零;否則保持不變。ReLU激活函數(shù)在深度學(xué)習(xí)中被廣泛使用,因為它可以增加模型的稀疏性和非線性特征表達能力,并且計算速度快。
# 320是如何計算出來的:
# 假設(shè)輸入數(shù)據(jù)的大小為(1, 1, 28, 28),其中1表示batch_size,1表示輸入數(shù)據(jù)的通道數(shù),28表示輸入數(shù)據(jù)的高度,28表示輸入數(shù)據(jù)的寬度。
# 事實上,在MNIST數(shù)據(jù)集中,每張圖片也是28*28的灰度圖片
# 在經(jīng)過第一個卷積層之后,輸出的形狀為(1, 10, 24, 24),其中10表示輸出特征圖的數(shù)量,
# 24表示特征圖的高度和寬度都減少了4個像素(由于卷積核大小為5,步長為1,因此特征圖的高度和寬度都會減少4個像素)。
# 接著,經(jīng)過第一個池化層之后,輸出的形狀為(1, 10, 12, 12),其中12=24/2,即特征圖的高度和寬度都減少了一半。
# 同理,經(jīng)過第二個卷積層之后,輸出的形狀為(1, 20, 8, 8),其中20表示輸出特征圖的數(shù)量,8=12-2x2,即特征圖的高度和寬度都減少了4個像素。
# 最后,經(jīng)過第二個池化層之后,輸出的形狀為(1, 20, 4, 4),其中4=8/2,即特征圖的高度和寬度都減少了一半。
# 因此,假設(shè)輸入數(shù)據(jù)的大小為(1, 1, 28, 28),并且經(jīng)過了兩個卷積層和兩個池化層之后,其輸出大小為(1, 20, 4, 4)。
# 因此320 = 20 * 4 * 4,將每個樣本的所有通道的像素值拉成一行,最終得到一個(batch_size, 320)的輸出張量。
# F.dropout函數(shù)補充說明(其實和之前的dropout層差不多):
# 在神經(jīng)網(wǎng)絡(luò)中,Dropout是一種常用的正則化技術(shù),它可以隨機地在神經(jīng)網(wǎng)絡(luò)的某些神經(jīng)元之間添加斷開連接。
# 這個過程可以強制使神經(jīng)網(wǎng)絡(luò)學(xué)習(xí)到更加健壯和泛化的特征,因為它會防止任何一個單獨的神經(jīng)元對結(jié)果產(chǎn)生太大的影響。
# softmax操作:
# softmax操作是一種常用的歸一化方法,將一個向量中的每個元素轉(zhuǎn)化為一個介于0~1之間的值,并且這些值的和等于1。它通常用于多分類問題中,以便計算損失函數(shù)和模型預(yù)測的準(zhǔn)確性。
# 具體來說,對于輸入x的每一行,softmax操作將計算其指數(shù)值(exp(xi)),并將其除以該行所有元素的指數(shù)之和(也稱為歸一化常數(shù))
# 補充:接著通常也會取對數(shù),這是為了防止出現(xiàn)概率太小的情況影響計算,于是我們一般取對數(shù)以便更容易計算和優(yōu)化損失。
# 帶動量的SGD:
# 帶動量的SGD算法(Momentum SGD)是SGD的一種改進,它可以加速模型的收斂并減少參數(shù)更新的震蕩。
# SGD是隨機梯度下降法,每次會抽取一定樣本計算它們的梯度平均值進行更新,這也是隨機梯度下降法和梯度下降法的區(qū)別,梯度下降法每次是計算所有樣本。
# 具體來說,帶動量的SGD算法在計算參數(shù)更新時,不僅考慮當(dāng)前時刻的梯度,還會考慮之前梯度更新的方向。這個方向被稱為“動量”。動量是為了進行指數(shù)加權(quán)平均操作
# 解釋可以看https://zhuanlan.zhihu.com/p/73264637,這個里面解釋的比較詳細,梯度每次更新的方向中都會包含前面梯度更新的方向的信息
# 補充,SGD和GD區(qū)別:
# 普通的梯度下降法每次需要遍歷全部訓(xùn)練數(shù)據(jù)來計算梯度并更新模型參數(shù),而隨機梯度下降法每次只使用一個樣本來計算梯度并更新模型參數(shù)。
# 經(jīng)典的梯度下降法在每次對模型參數(shù)進行更新時,需要遍歷所有的訓(xùn)練數(shù)據(jù)。當(dāng)M很大的時候,就需要耗費巨大的計算資源和計算時間,這在實際過程中基本不可行。
# 隨機梯度下降法(Stochastic Gradient Descent, SGD)應(yīng)運而生。它采用單個訓(xùn)練樣本的損失來近似平均損失
# 相對于普通的梯度下降法,隨機梯度下降法的優(yōu)勢在于速度更快、更容易逃離局部極小點、能夠處理大規(guī)模數(shù)據(jù)集以及可以實時學(xué)習(xí)在線數(shù)據(jù)。
# 然而,由于隨機梯度下降法所使用的樣本是隨機選擇的,因此其更新過程具有一定的噪聲性質(zhì),需要更多的迭代次數(shù)才能收斂到全局最優(yōu)解。
# 同時,隨機梯度下降法也比較難以用于處理稀疏數(shù)據(jù)。
# 負(fù)對數(shù)似然損失函數(shù)(negative log likelihood loss):
# 通常用于多分類問題。它的基本思想是將模型輸出的概率分布與真實標(biāo)簽的 one-hot 編碼進行比較,計算兩者之間的差異。
# 該損失函數(shù)先取每個樣本真實標(biāo)簽對應(yīng)的預(yù)測概率的自然對數(shù),再對它們求平均值并取相反數(shù),所以稱為“負(fù)對數(shù)似然”損失函數(shù)。
# 使用該損失函數(shù)最終得到的模型會使得正確分類的樣本的損失越小越好,而錯誤分類的樣本的損失越大越好,因此能夠有效地訓(xùn)練出具有良好泛化性能的模型。
# 循環(huán)整個訓(xùn)練數(shù)據(jù)集的次數(shù)
n_epochs = 3
# 一次訓(xùn)練的樣本數(shù)量
batch_size_train = 64
# 一次測試的樣本數(shù)量
batch_size_test = 1000
# 學(xué)習(xí)率,也可以理解為梯度下降法的速度
learning_rate = 0.01
# 動量,在后續(xù)的SGD中會使用
momentum = 0.9
# 記錄的頻率,后續(xù)會看到
log_interval = 10
# 為所有隨機數(shù)操作設(shè)置隨機種子
random_seed = 1
torch.manual_seed(random_seed)
# torch.utils.data.DataLoader 是 PyTorch 中的一個數(shù)據(jù)加載器,用于將數(shù)據(jù)集封裝成可迭代對象,方便數(shù)據(jù)的批量讀取和處理。
# 它可以自動進行數(shù)據(jù)的分批、打亂順序、并行加載等操作,同時還支持多進程加速。通常在訓(xùn)練神經(jīng)網(wǎng)絡(luò)時會使用 DataLoader 來讀取數(shù)據(jù)集,并配合 Dataset 類一起使用。
# batch_size (int): 每個批次的大小,即每次迭代中返回的數(shù)據(jù)樣本數(shù)。
# shuffle (bool): 是否在每個 epoch 之前打亂數(shù)據(jù)。如果設(shè)置為 True,則在每個 epoch 之前重新排列數(shù)據(jù)集以獲得更好的訓(xùn)練效果。
# torchvision.datasets.MNIST
# root:下載數(shù)據(jù)的目錄;
# train決定是否下載的是訓(xùn)練集;
# download為true時會主動下載數(shù)據(jù)集到指定目錄中,如果已存在則不會下載
# transform是接收PIL圖片并返回轉(zhuǎn)換后版本圖片的轉(zhuǎn)換函數(shù),
# transform 函數(shù)是一個 PyTorch 轉(zhuǎn)換操作,它將圖像轉(zhuǎn)換為張量并對其進行標(biāo)準(zhǔn)化,其中均值為 0.1307,標(biāo)準(zhǔn)差為 0.3081。
# 即將每一個圖像像素的值減去均值,除以標(biāo)準(zhǔn)差,如此使它們有相似的尺度,從而更容易地訓(xùn)練模型。
train_loader = torch.utils.data.DataLoader(
torchvision.datasets.MNIST('./data/', train=True, download=True,
transform=torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(
(0.1307,), (0.3081,))
])),
batch_size=batch_size_train, shuffle=True)
test_loader = torch.utils.data.DataLoader(
torchvision.datasets.MNIST('./data/', train=False, download=True,
transform=torchvision.transforms.Compose([
torchvision.transforms.ToTensor(),
torchvision.transforms.Normalize(
(0.1307,), (0.3081,))
])),
batch_size=batch_size_test, shuffle=True)
# 使用了Python內(nèi)置函數(shù)"enumerate"將test_loader轉(zhuǎn)換為帶有索引的可迭代對象,然后使用"next"函數(shù)獲取下一個批次的數(shù)據(jù)和目標(biāo)。
# 最終,example_data里是這個batch的數(shù)據(jù),example_targets里是這個batch對應(yīng)的所有標(biāo)簽
examples = enumerate(test_loader)
batch_idx, (example_data, example_targets) = next(examples)
# print(example_targets)
# print(example_data.shape)
# 這是在繪制剛剛獲取的樣本batch的內(nèi)容
fig = plt.figure()
for i in range(6):
plt.subplot(2, 3, i + 1)
plt.tight_layout()
plt.imshow(example_data[i][0], cmap='gray', interpolation='none')
plt.title("Ground Truth: {}".format(example_targets[i]))
plt.xticks([])
plt.yticks([])
plt.show()
# 開始建立神經(jīng)網(wǎng)絡(luò)模型,Net類繼承nn.Module類,
class Net(nn.Module):
def __init__(self):
# 完成一些模型初始化和必要的內(nèi)存分配等工作。確保Net類正確繼承了nn.Module的所有功能。
super(Net, self).__init__()
# 定義了一個名為self.conv1的卷積層對象,輸入通道數(shù)為1(因為是灰度圖像),輸出通道數(shù)為10,卷積核大小為5x5。
# 這意味著該卷積層將對輸入進行一次5x5的卷積操作,并將結(jié)果映射到10個輸出通道上。
# 不過這里的卷積操作有說每個卷積核都會隨機初始化權(quán)重和偏置項。這里我比較好奇是什么意思, 是一開始隨即賦值,然后在模型的反向傳播過程中,它們會被更新以最小化損失函數(shù),并使神經(jīng)網(wǎng)絡(luò)能夠更準(zhǔn)確地進行預(yù)測嗎
self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
# 同理,第二行代碼定義了一個名為self.conv2的卷積層對象,輸入通道數(shù)為10(因為self.conv1的輸出通道數(shù)為10),
# 輸出通道數(shù)為20,卷積核大小為5x5。該卷積層也將對其輸入進行一次5x5的卷積操作,并將結(jié)果映射到20個輸出通道上。
self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
# 設(shè)置Dropout2d層,這個的解釋可以看最上面,作用是隨機丟棄部分?jǐn)?shù)據(jù),防止過擬合
self.conv2_drop = nn.Dropout2d()
# 創(chuàng)建線性層(全連接層)接收一個320維的輸入張量,并將其映射到一個50維的特征空間中。
self.fc1 = nn.Linear(320, 50)
# 同理,此線性層接收50維的特征向量,并將其映射到10維的輸出空間中。
self.fc2 = nn.Linear(50, 10)
def forward(self, x):
# relu函數(shù)用于執(zhí)行ReLU(Rectified Linear Unit)激活函數(shù),將小于0的數(shù)據(jù)替換為0。max_pool2d函數(shù)用于執(zhí)行最大池化層采樣操作,具體含義見上
# 這里就是先對輸入數(shù)據(jù)x執(zhí)行一次卷積操作,將其映射到10個輸出通道上,然后對卷積操作的結(jié)果進行2x2的最大池化操作。
# 最大池化操作中參數(shù)為:input,輸入張量;kernel_size,池化層窗口大小,stride:步幅,默認(rèn)為kernel_size
x = F.relu(F.max_pool2d(self.conv1(x), 2))
# 和上面同理,不同的是增加了Dropout2d層,防止數(shù)據(jù)過擬合
x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
# 將輸入x的形狀從(batch_size, num_channels, height, width)變換為(batch_size, 320),
# 其中batch_size表示輸入的數(shù)據(jù)樣本數(shù),num_channels表示輸入數(shù)據(jù)的通道數(shù),height表示輸入數(shù)據(jù)的高度,width表示輸入數(shù)據(jù)的寬度。
# 在這里,-1參數(shù)表示自動推斷該維度上的大小,因為可以根據(jù)輸入數(shù)據(jù)的大小自動確定batch_size的大小。
# 另外,320的大小是通過卷積層和池化層的輸出計算得到的。具體計算過程可以看上面。
x = x.view(-1, 320)
# 將輸入x通過全連接層self.fc1進行線性變換,然后使用ReLU激活函數(shù)對輸出結(jié)果進行非線性變換
x = F.relu(self.fc1(x))
# 在傳遞給全連接層之前,在輸入張量x上應(yīng)用dropout操作。
# 其中,self.training是Net類中的一個布爾值參數(shù),用于指示當(dāng)前模型是否處于訓(xùn)練模式。
# 當(dāng)self.training為True時,dropout操作將被啟用,否則所有神經(jīng)元都被保留,不進行dropout操作。
# 這樣,在測試或評估模型的時候,dropout操作就會被關(guān)閉,模型將使用完整的權(quán)重來進行預(yù)測,以提高預(yù)測準(zhǔn)確性。
# 對于其的補充說明可以看上面
x = F.dropout(x, training=self.training)
# 將輸入x通過全連接層self.fc1進行線性變換,最終映射至10個通道中,這是因為我們想將其分為10個類別
x = self.fc2(x)
# 進行softmax操作然后再取對數(shù),softmax這個操作的含義可以看上面,簡而言之就是求取每個類別的概率并歸一化
# dim=1 意思是對x的第二維度進行操作,x現(xiàn)在為(batch_size, 10),也就是在10(num_classes)這個地方進行操作。
# 取對數(shù)的原因也是為了更方便計算和求取損失,具體可以看上面的補充解釋。
return F.log_softmax(x, dim=1)
# 創(chuàng)建神經(jīng)網(wǎng)絡(luò)
network = Net()
# 使用SGD(隨機梯度下降)優(yōu)化器,注意這里的SGD是帶動量的,具體解釋可以看上面,還有SGD和GD的區(qū)別
# network.parameters()是要訓(xùn)練的參數(shù),lr是學(xué)習(xí)率,超參數(shù)之一;momentum是動量,也是超參數(shù)之一。不過這里的動量最好設(shè)置為0.9
# network.parameters()返回一個包含了Net類中所有可訓(xùn)練參數(shù)的迭代器。這些可訓(xùn)練參數(shù)包括神經(jīng)網(wǎng)絡(luò)中的權(quán)重和偏置項等。
# 在優(yōu)化器中使用這個迭代器,可以告訴優(yōu)化器需要更新哪些參數(shù)以最小化損失函數(shù)。
optimizer = optim.SGD(network.parameters(), lr=learning_rate, momentum=momentum)
train_losses = []
train_counter = []
test_losses = []
# [0, 60000, 120000, 180000]
test_counter = [i * len(train_loader.dataset) for i in range(n_epochs + 1)]
# print(test_counter)
# breakpoint1 = input("print(test_counter): ") # 看看這里是什么
def train(epoch):
# 用于將神經(jīng)網(wǎng)絡(luò)設(shè)置為訓(xùn)練模式。在訓(xùn)練神經(jīng)網(wǎng)絡(luò)時,需要將其切換到訓(xùn)練模式,以便啟用Batch Normalization和Dropout等高級優(yōu)化技術(shù),
# 并且在每個batch處理完畢后,可以清除所有中間狀態(tài)(如梯度)以準(zhǔn)備下一次訓(xùn)練。
network.train()
for batch_idx, (data, target) in enumerate(train_loader):
# 是用于清除模型參數(shù)的梯度信息。在訓(xùn)練過程中,優(yōu)化器會累加每個參數(shù)的梯度,
# 因此在每個 batch 計算結(jié)束后,需要使用 zero_grad() 清空之前累計的梯度,避免對下一個 batch 的計算造成影響。
optimizer.zero_grad()
# 獲取訓(xùn)練結(jié)果
output = network(data)
# 損失函數(shù)定義,這里使用的是負(fù)對數(shù)似然損失函數(shù)(negative log likelihood loss),具體解釋可以看上面
# F.nll_loss()函數(shù)計算了模型輸出和目標(biāo)輸出之間的差異,并返回一個標(biāo)量值作為損失值,該值越小表示模型越接近目標(biāo)。
loss = F.nll_loss(output, target)
# loss.backward() 就是用來計算當(dāng)前 mini-batch 的損失函數(shù)關(guān)于模型參數(shù)的梯度的代碼。
# 該函數(shù)會在計算完梯度之后將它們存儲在參數(shù)的 grad 屬性中。接著,我們可以使用 optimizer.step() 函數(shù)來更新模型參數(shù)。
# 這里使用的是反向傳播算法(backpropagation)來計算網(wǎng)絡(luò)參數(shù)的梯度,
loss.backward()
# 使用優(yōu)化器更新模型參數(shù)
optimizer.step()
# 每隔log_interval個batch打印一次訓(xùn)練信息,包括當(dāng)前epoch、batch號、已處理的樣本數(shù)、總樣本數(shù)、當(dāng)前batch的損失值等
if batch_idx % log_interval == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(epoch, batch_idx * len(data),
len(train_loader.dataset),
100. * batch_idx / len(train_loader),
loss.item()))
# 記錄損失值
train_losses.append(loss.item())
# 記錄目前已訓(xùn)練完成的樣本數(shù)量
train_counter.append((batch_idx * 64) + ((epoch - 1) * len(train_loader.dataset)))
# 將神經(jīng)網(wǎng)絡(luò)的權(quán)重和優(yōu)化器的狀態(tài)保存到硬盤上,方便之后加載和使用。
# network.state_dict() 返回一個字典,其中包含了神經(jīng)網(wǎng)絡(luò)的所有參數(shù)(即權(quán)重和偏置項),以及它們對應(yīng)的名稱。這些參數(shù)可以用來恢復(fù)網(wǎng)絡(luò)的狀態(tài)。
# torch.save() 函數(shù)將字典對象保存到指定的文件路徑上。第一個參數(shù)是要保存的對象,第二個參數(shù)是文件路徑。
torch.save(network.state_dict(), './model.pth')
# 這是在保存優(yōu)化器的狀態(tài)。
torch.save(optimizer.state_dict(), './optimizer.pth')
def test():
# 將神經(jīng)網(wǎng)絡(luò)設(shè)置為評估模式。在評估模式下,模型會停用特定步驟,如Dropout層、Batch Normalization層等,
# 并且使用訓(xùn)練期間學(xué)到的參數(shù)來生成預(yù)測,而不是在訓(xùn)練集上進行梯度反向傳播和權(quán)重更新。
network.eval()
test_loss = 0
correct = 0
# 關(guān)閉梯度計算,以減少內(nèi)存消耗和加快模型評估過程。這意味著,在使用torch.no_grad()時,模型的參數(shù)不會被更新和優(yōu)化;
# 同時,計算圖也不會被跟蹤和記錄,這使得前向傳播的速度更快。
with torch.no_grad():
for data, target in test_loader:
output = network(data)
# 記錄模型的損失值。reduction='sum'表示將每個樣本的損失求和后再返回。
test_loss += F.nll_loss(output, target, reduction='sum').item()
# 找到輸出結(jié)果(張量)中,每一行(即第1維度)最大的那個數(shù)以及它所在的位置,返回一個元組。其中元組的第一個元素就是最大的那個數(shù),第二個元素是最大數(shù)的位置。
# 這里的 keepdim=True 參數(shù)表示保持維度不變,也就是說返回的結(jié)果張量維度與原始張量相同。
# [1] 表示取出這個元組的第二個元素,也就是位置信息,賦給了變量 pred。這個 pred 變量就是神經(jīng)網(wǎng)絡(luò)預(yù)測出來的類別標(biāo)簽,
# 它的維度與原始張量第 1 維度相同,表示每一個輸入數(shù)據(jù)樣本對應(yīng)的類別標(biāo)簽。
pred = output.data.max(1, keepdim=True)[1]
# target.data.view_as(pred)將 target 張量按照 pred 張量的形狀進行重塑(reshape)。
# 具體地說,它會返回一個和 pred 張量形狀相同、但數(shù)據(jù)來自 target 張量的新張量。
# .eq()方法來進行張量之間的逐元素比較,得到一個由布爾值組成的張量,表示pred和target.data.view_as(pred)中的每個元素是否相等。
# 如果該元素相等,則對應(yīng)位置為True,否則為False。
# .sum():對前一步得到的True/False tensor沿著所有維度求和,得到預(yù)測正確的樣本數(shù)。
# +=:將這個batch中預(yù)測正確的樣本數(shù)添加到之前已經(jīng)處理過的樣本中,累加得到整個數(shù)據(jù)集中預(yù)測正確的樣本數(shù)(correct)。
correct += pred.eq(target.data.view_as(pred)).sum()
# 計算樣本的平均損失
test_loss /= len(test_loader.dataset)
# 記錄本次的測試結(jié)果
test_losses.append(test_loss)
# 打印本次測試結(jié)果,包括測試集的平均損失、正確預(yù)測的樣本數(shù)量、測試集的總樣本數(shù)量以及正確預(yù)測的樣本占比。
print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))
test() # 不加這個,后面畫圖就會報錯:x and y must be the same size,因為test_counter中包含了模型未經(jīng)訓(xùn)練時的情況(上面打印中的“0”)
# 進入正式的訓(xùn)練、測試過程,根據(jù)設(shè)定的epoch進行訓(xùn)練
for epoch in range(1, n_epochs + 1):
train(epoch)
test()
# 繪制整個訓(xùn)練和測試的圖像,包括記錄訓(xùn)練的損失變化、測試的損失變化
fig = plt.figure()
plt.plot(train_counter, train_losses, color='blue')
plt.scatter(test_counter, test_losses, color='red')
plt.legend(['Train Loss', 'Test Loss'], loc='upper right')
plt.xlabel('number of training examples seen')
plt.ylabel('negative log likelihood loss')
plt.show()
# 選取6個樣本進行測試,即模型的使用
examples = enumerate(test_loader)
batch_idx, (example_data, example_targets) = next(examples)
with torch.no_grad():
output = network(example_data)
fig = plt.figure()
for i in range(6):
plt.subplot(2, 3, i + 1)
plt.tight_layout()
plt.imshow(example_data[i][0], cmap='gray', interpolation='none')
plt.title("Prediction: {}".format(output.data.max(1, keepdim=True)[1][i].item()))
plt.xticks([])
plt.yticks([])
plt.show()
# ----------------------------------------------------------- #
# 檢查點的持續(xù)訓(xùn)練,繼續(xù)對網(wǎng)絡(luò)進行訓(xùn)練,或者看看如何從第一次培訓(xùn)運行時保存的state_dicts中繼續(xù)進行訓(xùn)練。
# 初始化一組新的網(wǎng)絡(luò)和優(yōu)化器。
continued_network = Net()
continued_optimizer = optim.SGD(network.parameters(), lr=learning_rate, momentum=momentum)
# 加載網(wǎng)絡(luò)的內(nèi)部狀態(tài)、優(yōu)化器的內(nèi)部狀態(tài)
network_state_dict = torch.load('model.pth')
continued_network.load_state_dict(network_state_dict)
optimizer_state_dict = torch.load('optimizer.pth')
continued_optimizer.load_state_dict(optimizer_state_dict)
# 為什么是“4”開始呢,因為n_epochs=3,上面用了[1, n_epochs + 1)
# 繼續(xù)進行5次訓(xùn)練
for i in range(4, 9):
test_counter.append(i*len(train_loader.dataset))
train(i)
test()
# 繪制訓(xùn)練的曲線
fig = plt.figure()
plt.plot(train_counter, train_losses, color='blue')
plt.scatter(test_counter, test_losses, color='red')
plt.legend(['Train Loss', 'Test Loss'], loc='upper right')
plt.xlabel('number of training examples seen')
plt.ylabel('negative log likelihood loss')
plt.show()
到了這里,關(guān)于真的不能再詳細了,2W字保姆級帶你一步步用Pytorch搭建卷積神經(jīng)網(wǎng)絡(luò)實現(xiàn)MNIST手寫數(shù)字識別的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!