深度學習基礎從0到0.1
線性回歸
一元線性回歸
線性回歸,公式為Y=Wx+b,這里簡單一點,假設偏置b=0,我們設置損失函數為loss=(y-yi)2,y是真實值,yi是預測值,代入可得loss=(y-W*x)2,帶入x的值和y的值即可得到最終的loss函數,而后求其導數,導數為0時可取極值,進而得到w,通過這樣我們就可以得到最佳的擬合直線,而這就是線性回歸算法,不過這里更為簡單,還沒對w進行訓練。
那么如果考慮偏置b的話,我們又該怎么做呢,實際上很簡單,我們只需要分別對兩個變量求偏導,令其等于0即可,而后聯立方程組解出變量值,示例如下

這里首先得出了loss函數,接下來對其進行求偏導

而后聯立方程求解即可得出最終變量值。

梯度下降
如果按照之前的思路,只要能夠保證函數是線性的,我們無需再對模型進行多次微調,只需要令參數的偏導數全等于0求出函數即可,但為什么深度學習里不這么做呢,原因如下
1、參數可能有很多,成千上萬,計算量過大
2、模型有可能不是線性的,引入了激活函數,比如Relu函數是分段的,對其求導,解方程將什么復雜
示例如下圖,肉眼可見的方程是難以進行計算的

因此,我們這里就需要學習梯度下降,通過它來進行調參。
這里以一元函數為例,如下圖所示,我們假設初始點為Xo=4,

接下來根據其導數進行調整,比如f'(x)>0,那說明函數是增大的,我們是想取f(x)的最小值,則我們需要向左調整,反之則向右調整;
那么我們如何設定這個調整的步長呢,這里有一個簡單的方法,設置步長為負的f'(x),比如x=4的導數值f'(x)≈1,那我們就設置為-1,接下來x的值就會由4變為3,
可以發現離最小值更近了一步,在x=3處的f'(x)為0.7,接下來進行-0.7,變為2.3。這樣不斷逼近最終就抵達了最低點。
學習率
上面可以看出更新的步長貌似還不錯,但是實際上還需要一個參數去調控步長,這個參數一把不大于1,比如0.01,因為有時候直接使用步長操作可能會錯過全局最小值,所以我們引進學習率調控。這里就可以開始更新參數了,我們在求導后,加上了學習率,然后不斷對參數進行更新迭代,直至達到最優版本。

均方誤差
之前我們定義的損失函數是所有樣本的label和預測值的誤差的平方和,實際上為了保證訓練穩定,一般會除以樣本數,而這其實就是我們的均方誤差。

多元線性回歸
剛剛所看的是一元線性回歸,那么多元線性回歸又當如何呢?實際上是一樣的處理過程,具體示例如下
數據如下:
| 溫度 | 價格(元) | 銷量(個) |
|---|---|---|
| 10 | 3 | 60 |
| 20 | 3 | 85 |
| 25 | 3 | 100 |
| 28 | 2.5 | 120 |
| 30 | 2 | 140 |
| 35 | 2.5 | 145 |
| 40 | 2.5 | 163 |
這里用X1表示溫度,X2表示價格,Y表示銷量。
Wo表示截距(初始值),W1表示溫度權重,W2表示價格權重。
預測銷量為Y=Wo+X1*W1+X2*W2。
損失函數loss=1/7∑(y-yi)2,代入就是loss=1/7∑(Wo+X1*W1+X2*W2-yi)2
接下來就是用剛剛所學的梯度下降進行優化,而梯度下降首先就是求導數,所以我們分別對三個變量進行求偏導。

在這之后設置學習率,而后更新參數即可

具體實現代碼如下
# Feature 數據
X = [[10, 3], [20, 3], [25, 3], [28, 2.5], [30, 2], [35, 2.5], [40, 2.5]]
y = [60, 85, 100, 120, 140, 145, 163] # Label 數據
# 初始化參數
w = [0.0, 0.0, 0.0] # w0, w1, w2
lr = 0.0001 # 學習率
num_iterations = 10000 # 迭代次數
# 梯度下降
for i in range(num_iterations):
# 預測值
y_pred = [w[0] + w[1] * x[0] + w[2] * x[1] for x in X]
# 計算損失
loss = sum((y_pred[j] - y[j]) ** 2 for j in range(len(y))) / len(y)
# 計算梯度
grad_w0 = 2 * sum(y_pred[j] - y[j] for j in range(len(y))) / len(y)
grad_w1 = 2 * sum((y_pred[j] - y[j]) * X[j][0] for j in range(len(y))) / len(y)
grad_w2 = 2 * sum((y_pred[j] - y[j]) * X[j][1] for j in range(len(y))) / len(y)
# 更新參數
w[0] -= lr * grad_w0
w[1] -= lr * grad_w1
w[2] -= lr * grad_w2
# 打印損失
if i % 100 == 0:
print(f"Iteration {i}: Loss = {loss}")
# 輸出最終參數
print(f"Final parameters: w0 = {w[0]}, w1 = {w[1]}, w2 = {w[2]}")
邏輯回歸
一元邏輯回歸
之前所學的線性回歸是為了擬合出一條直線/平面來擬合數據,以此達到預測數值的效果,而邏輯回歸則不同,它是為了分類問題,以這里的一元邏輯回歸為例:
| 氣溫 | 是否出門 |
|---|---|
| -10 | 0 |
| 3 | 1 |
| -3 | 0 |
| 5 | 1 |
| -4 | 0 |
| 7 | 1 |
| -6 | 0 |
| 8 | 1 |
以上是數據,當我們構造出圖像時,如下所示

此時的他并不是一個具體的數值,而是非0即1的,這個時候我們就無法再使用線性回歸來預測,所以就引進了邏輯回歸來進行解決。
這里就引入了激活函數sigmoid,效果圖如下

這里還不夠擬合,所以我們可以在e-x前加入w,而后進行訓練不斷逼近即可。

神經網絡
梯度消失/爆炸
梯度消失是指,當參數進行多次迭代更新后,參數的變化已經變的微乎其微,比如100次的乘上0.1,這個數就會無比的小,貼近0,這個時候更新參數十分緩慢,這個就是梯度消失。
而梯度爆炸是指,當參數多次迭代后,由于導數值較大,比如100次的乘上1.2,這個數就會十分巨大,這會使我們的更新跳躍幅度過大,不利于更新。
卷積
當參數過多時,比如我們要處理一張灰白圖片,他的大小是6*6*1,所以我們如果一個像素一個參數處理,就需要處理36個參數,而如果我們使用3*3的卷積核,對每個像素先進行卷積操作,最終就只剩下4*4*1的大小,而且我們只需要處理10個參數(3*3的卷積核+偏置b),就大大縮小了計算量,而且無論多少個像素,我們需要處理的參數也就只有這10個,因此,我們引入了卷積來處理圖像。
其優點具體如下
1、圖片輸入特征多 圖片輸入特征多,但是一個3x3的卷積操作只有10個參數,就可以對整個圖片進行掃描。
2、特征局部性 卷積操作的每個運算只在特定相鄰區域內進行,并不要所有輸入特征都參與運算。
3、平移不變性 卷積操作在整個圖片上進行滑動檢測,就是假設圖片的特征具有平移不變性。
特征圖的尺寸計算
對聯分別的輸出尺寸變化公式如下
輸出尺寸 = (輸入尺寸 - 卷積核大小 + 2*填充) / 步長 + 1
而有時會出現除不盡的情況,這個時候我們通常是進行向下取整來進行處理。
1*1卷積層
1*1卷積層存在的意義是他可以以最低成本改變通道數,示例如下
我們定義一個1x1卷積層,其卷積核的數量(即輸出通道數)為 C_out。每個卷積核的尺寸是 [1, 1, C_in]。也就是說,每個卷積核都有 C_in 個權重值(每個輸入通道對應一個權重)和一個偏置項。
這個1x1的窗口在特征圖的空間維度(高度和寬度) 上滑動。因為窗口是1x1,所以它每次只“看”一個像素點。但是,這個像素點有 C_in 個通道,因此它看到的是一個包含 C_in 個數值的向量。
對于特征圖上的每一個位置 (i, j),取出該位置所有 C_in 個通道的值,形成一個向量 [v1, v2, ..., v_Cin]。
用第一個1x1卷積核(它也有 C_in 個權重 [w1_1, w2_1, ..., w_Cin_1])與這個向量進行點積(即對應元素相乘后求和),再加上偏置,就得到了輸出特征圖在位置 (i, j) 的第一個通道的值。
輸出值_1 = (v1 * w1_1) + (v2 * w2_1) + ... + (v_Cin * w_Cin_1) + 偏置_1
用第二個卷積核(權重為 [w1_2, w2_2, ...])與同一個輸入向量再進行點積,得到輸出特征圖在 (i, j) 的第二個通道的值。
...
重復此過程,直到用完所有的 C_out 個卷積核。
經過上述操作,輸入的 C_in 個通道的信息,在每個像素點上都被混合、加權,并投影到了一個全新的 C_out 維空間。
因為計算是在每個空間位置上獨立、并行地完成的,所以輸出的空間尺寸(高度和寬度)保持不變,但通道數從 C_in 變成了 C_out。
全局平均池化層
我們知道一般遇到的是最大池化層,從n*n的區域中拿出一個最大的值,即為最大池化操作,它只關注了局部的特征。而這里的全局平均池化是什么意思呢?
實際上,他就類似我們的平均數,比如當前有十個數,它就會求和再除10,這樣就得到了全局平均池化的輸出,這個不會引入額外參數,而且有每個像素的特征。
貓狗分類實戰
import os
import random
from PIL import Image
from torchvision import transforms # ? 從 torchvision 導入
import torch
from torch import device, nn
from torch.utils.data import DataLoader, Dataset, TensorDataset
def verify_images(image_folder):
classes = ["Cat", "Dog"]
class_to_idx = {"Cat": 0, "Dog": 1}
samples = []
for cls_name in classes:
cls_dir = os.path.join(image_folder, cls_name)
for fname in os.listdir(cls_dir):
if not fname.lower().endswith(('.jpg', '.jpeg', '.png')):
continue
path = os.path.join(cls_dir, fname)
try:
with Image.open(path) as img:
img.verify()
samples.append((path, class_to_idx[cls_name]))
except Exception:
print(f"Warning: Skipping corrupted image {path}")
return samples
class ImageDataset(Dataset):
def __init__(self, samples, transform=None):
self.samples = samples
self.transform = transform
def __len__(self):
return len(self.samples)
def __getitem__(self, idx):
path, label = self.samples[idx]
with Image.open(path) as img:
img = img.convert("RGB")
if self.transform:
img = self.transform(img)
return img, label
class CNNmodel(nn.Module):
def __init__(self):
super().__init__()
self.model = nn.Sequential(
nn.Conv2d(3, 16, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2, 2),
nn.Conv2d(16, 32, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2, 2),
nn.Conv2d(32, 64, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2, 2),
nn.Conv2d(64, 128, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2, 2),
nn.Conv2d(128,1,1),
nn.AdaptiveAvgPool2d(1),
nn.Flatten(),
nn.Sigmoid()
)
def forward(self, x):
return self.model(x)
def evaluate(model,test_dataloader):
model.eval()
val_correct = 0
val_total = 0
with torch.no_grad():
for inputs,labels in test_dataloader:
inputs = inputs.to(device)
labels = labels.float().unsqueeze(1).to(device)
outputs = model(inputs)
preds = (outputs >= 0.5).float()
val_correct += (preds == labels).sum().item()
val_total += labels.size(0)
val_acc = val_correct / val_total
return val_acc
if __name__ == "__main__":
DATA_DIR = r"D:\Computer Graphic\review\examples\datasets\archive\PetImages"
BATCH_SIZE = 64
IMG_SIZE = 128
EPOCHS = 10
LR = 0.001
PRINT_STEP = 100
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
all_samples = verify_images(DATA_DIR)
random.seed(42)
random.shuffle(all_samples)
train_size = int(len(all_samples) * 0.8)
train_samples = all_samples[:train_size]
valid_samples = all_samples[train_size:]
data_transform = transforms.Compose([
transforms.Resize((IMG_SIZE,IMG_SIZE)),#統一圖片大小
transforms.ToTensor(),#轉換為張量
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) #標準化
])
train_dataset = ImageDataset(train_samples,data_transform)
valid_dataset = ImageDataset(valid_samples,data_transform)
train_dataloader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True,num_workers=4)
valid_dataloader = DataLoader(valid_dataset, batch_size=BATCH_SIZE, shuffle=False,num_workers=4)
model = CNNmodel().to(device)
criterion = nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=LR)
for epoch in range(EPOCHS):
print(f"\nEpoch {epoch+1}/{EPOCHS}")
model.train()
running_loss = 0.0
for step,(inputs,labels) in enumerate(train_dataloader):
inputs = inputs.to(device)
labels = labels.float().unsqueeze(1).to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if (step + 1) % PRINT_STEP == 0:
avg_loss = running_loss / PRINT_STEP
print(f"Step {step+1}- Loss: {avg_loss:.4f}")
running_loss = 0.0
val_acc = evaluate(model, valid_dataloader)
print(f"Validation Accuracy after epoch {epoch+1}: {val_acc:.4f}")

圖像增強
什么是圖像增強,圖像增強是在保持圖像語義不變的情況下,生成多樣化的新數據。例如旋轉,裁剪,顏色亮度等的改變,都是圖像增強的一種方式。
幾何變化
旋轉,這個是我們所熟悉的操作,將物體按照一定角度進行旋轉操作后,表達語義仍然不變。

那么如何在PyTorch里實現旋轉操作呢,具體如下。
首先我們定義一個函數,它可以對圖片應用PyTorch里Transform對象的操作,進而展示圖片。
def imshow(img_path,transform):
img = Image.open(img_path)
fig,ax = plt.subplots(1,2,figsize=(15,4))
ax[0].set_title(f"Original Image {img.size}")
ax[0].imshow(img)
img = transform(img)
ax[1].set_title(f"Transformed Image {img.size}")
ax[1].imshow(img)
plt.show()
而后通過以下代碼即可實現在-30-30度之間隨機旋轉
path = r"D:\Computer Graphic\review\examples\datasets\archive\PetImages\Cat\238.jpg"
transform = transforms.RandomRotation(degrees=30)
imshow(path,transform)

翻轉
具體有水平和垂直,水平如下
path = r"D:\Computer Graphic\review\examples\datasets\archive\PetImages\Cat\2239.jpg"
transform = transforms.RandomHorizontalFlip(p=1.0)#p代表翻轉概率
imshow(path,transform)

垂直翻轉如下
transform = transforms.RandomVerticalFlip(p=1.0)

裁剪
不難理解,隨機裁剪出圖像區域
transform = transforms.RandomCrop(size=(120, 120))

透視變換
改變圖片的形狀,進行一定的角度扭曲
transform = transforms.RandomPerspective(
distortion_scale=0.5, # 控制變形強度,0~1,越大越扭曲
p=1.0, # 應用該變換的概率
interpolation=transforms.InterpolationMode.BILINEAR
)

顏色變化
顏色具體可調的是亮度、對比度、飽和度和色調
transforms.ColorJitter(
brightness=0.5,#圖像亮暗程度,設置為x時從[1-x,1+x]隨機旋轉
contrast=0.5,#圖像對比度,范圍[1-x,1+x]
saturation=0.5,#圖像飽和度,范圍[1-x,1+x]
hue=0.1#色調,范圍[-0.5,0.5],設置為0.1則為[-0.1,0.1]
)

模糊
對圖像進行高斯模糊
# 對圖像進行高斯模糊,kernel size 為 5,sigma 可調節模糊強度
transform = transforms.GaussianBlur(kernel_size=5, sigma=(0.1, 3.0))
其中kernel_size是指高斯模糊卷積核的大小,它決定模糊的范圍,必須為奇數,設置越大則模糊效果越明顯。后面傳入的元組(0.1,3.0),表示在里面隨機旋轉一個值,sigma越大越模糊

遮罩
遮罩是指隨機遮擋一個或多個連續的方形區域,讓模型忽略局部信息更關注上下文特征,有利于提升模型魯棒性。
PyTorch里沒有直接實現,這里需要自己進行實現
from PIL import Image
import numpy as np
import random
def cutout_pil_multi(image, mask_size=50, num_masks=3):
"""
對圖像應用多個 Cutout 遮擋塊
參數:
- image: PIL.Image 對象
- mask_size: 每個遮擋塊的大?。ㄕ叫芜呴L)
- num_masks: 遮擋塊的數量
"""
image_np = np.array(image).copy()
h, w = image_np.shape[0], image_np.shape[1]
for _ in range(num_masks):
y = random.randint(0, h - 1)
x = random.randint(0, w - 1)
y1 = max(0, y - mask_size // 2)#防止變為負數
y2 = min(h, y + mask_size // 2)#防止溢出
x1 = max(0, x - mask_size // 2)
x2 = min(w, x + mask_size // 2)
# 遮擋區域設置為黑色
image_np[y1:y2, x1:x2, :] = 0
return Image.fromarray(image_np)
而后調用函數即可
imshow(path, cutout_pil_multi)

這個時候我們就可以利用已有的圖像增強技術給貓狗分類實戰加上,相當于擴充了訓練集。
train_transform = transforms.Compose([
transforms.Resize((150,150)),
transforms.RandomCrop(size=(IMG_SIZE,IMG_SIZE)),
transforms.RandomHorizontalFlip(p=0.5),
transforms.ColorJitter(
brightness=0.5,
contrast=0.3,
saturation=0.4,
hue=0.1
),
transforms.RandomRotation(degrees=15),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
valid_transform = transforms.Compose([
transforms.Resize((IMG_SIZE,IMG_SIZE)),#統一圖片大小
transforms.ToTensor(),#轉換為張量
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) #標準化
])
train_dataset = ImageDataset(train_samples,train_transform)
valid_dataset = ImageDataset(valid_samples,valid_transform)

語義分割
什么是語義分割呢,將圖片所有的像素都賦予語義信息的任務就是語義分割,如下圖所示

循環神經網絡(RNN)
循環神經網絡常用于序列問題,序列問題是指數據之間存在順序的問題,輸入、輸出或兩者都是有序的數據序列,元素上存在時間或位置上的聯系,在考慮這類問題時,我們需要考慮前面/未來的信息。
事實上,他的工作方式如下圖所示

它會將上一層的隱藏層輸出,傳給下一層的輸入,將這兩個拼接就組成了下一個的輸入,然后傳入隱藏層再到輸出層得到預測結果,之后循環往復,直至最終。
由此可得當前隱狀態的計算公式

其中,xt表示t時刻輸入,yt表示t時刻輸出,ht表示t時刻更新后記憶,即隱狀態。wh表示隱藏層權重,bh表示隱藏層偏置,wy表示輸出層權重,by表示輸出層偏置。
輸出層使用了Softmax函數作為激活函數

通過觀測可以得出,其實只有第一層隱藏層我們在循環使用,第二層是普通層,因此第一層也被我們稱為循環層。循環層的遞歸調用就是RNN的本質。每一時間步對之前所有的時間步的循環層的調用,輸出關鍵隱狀態ht。對于普通層,可以看成是每一時間步利用ht向量作為輸入,進行的額外的分類或者回歸任務。普通層不是RNN的核心,它只是為了完成每一步的特定任務添加的任務層。
LSTM
之前的RNN只能記住前一時刻的信息,只有短時記憶,而在現實生活中例如語音識別、天氣預報、股票預測這些情況下我們都需要進行長期時間的記憶,這個時候我們就引入了長短期記憶網絡(LSTM)。

我們對新輸入的信息進行存儲作為Z,而后再傳入上一時間狀態的Ht-1和這一時刻的信息Xt,經過sigmoid函數作為輸入門,即輸入信息的控制函數,將Z和這個相乘得到新的輸入,此操作將判定一部分新信息可以進入記憶,一部分則被丟棄。這個時候它就是受控新信息,接下來他就該進入長時記憶了,那么如何進入長時記憶呢,這里我們首先需要取出長時記憶,然后選擇一部分需要遺忘的記憶,以此來給出空間存儲新的記憶,這個時候依然使用sigmoid函數作為遺忘門函數進行篩選,然后將長期記憶乘上遺忘門的函數再加上tanh激活函數處理的新信息Z乘上控制函數,作為待輸出記憶,也就是要存儲的長期記憶,而后對這部分再進行tanh激活函數處理,再經過sigmoid輸出門處理,判斷那部分需要輸出,得到的輸出就是隱狀態的輸出了。多個時間步就是以Ct-1作為前一步的長時記憶,經過當前時間步處理后,生成新的長時記憶Ct和隱狀態ht,傳至下一個記憶中心。公式如下所示

不過這里的wh、wi、wo實際上都是兩個權重,以wh為例,我們的wh包含了前一時刻ht-1隱狀態輸出的權重wh,還包含了這一時刻輸入xt的權重wx
代碼實現如下
import torch
from torch import nn
from d2l import torch as d2l
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
def get_lstm_params(vocab_size, num_hiddens, device):
num_inputs = num_outputs = vocab_size
def normal(shape):
return torch.randn(size=shape, device=device)*0.01
def three():
return (normal((num_inputs, num_hiddens)),
normal((num_hiddens, num_hiddens)),
torch.zeros(num_hiddens, device=device))
W_xi, W_hi, b_i = three() # 輸入門參數
W_xf, W_hf, b_f = three() # 遺忘門參數
W_xo, W_ho, b_o = three() # 輸出門參數
W_xc, W_hc, b_c = three() # 候選記憶元參數
# 輸出層參數
W_hq = normal((num_hiddens, num_outputs))
b_q = torch.zeros(num_outputs, device=device)
# 附加梯度
params = [W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc,
b_c, W_hq, b_q]
for param in params:
param.requires_grad_(True)
return params
def init_lstm_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device),
torch.zeros((batch_size, num_hiddens), device=device))
def lstm(inputs, state, params):
[W_xi, W_hi, b_i, W_xf, W_hf, b_f, W_xo, W_ho, b_o, W_xc, W_hc, b_c,
W_hq, b_q] = params
(H, C) = state
outputs = []
for X in inputs:
I = torch.sigmoid((X @ W_xi) + (H @ W_hi) + b_i)
F = torch.sigmoid((X @ W_xf) + (H @ W_hf) + b_f)
O = torch.sigmoid((X @ W_xo) + (H @ W_ho) + b_o)
C_tilda = torch.tanh((X @ W_xc) + (H @ W_hc) + b_c)
C = F * C + I * C_tilda
H = O * torch.tanh(C)
Y = (H @ W_hq) + b_q
outputs.append(Y)
return torch.cat(outputs, dim=0), (H, C)
vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu()
num_epochs, lr = 500, 1
model = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_lstm_params,
init_lstm_state, lstm)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
GRU
GRU,即門控循環單元,他相較LSTM變得更加精簡了一些,他只有重置門和更新門,去除了LSTM的記憶細胞,也實現了記錄長時記憶和更新的效果。

這里通過重置門實現遺忘部分信息,這里的重置門實際上就是LSTM的遺忘門,他將上一時刻的記憶ht-1和當前時刻的輸入Xt進行拼接,而后經過線性層,再加上偏置進行sigmoid激活函數處理,就得到了Gr。
這個得到的Gr再乘上ht-1,就是我門經過重置后的長時記憶。重置后的長期記憶和當前輸入xt合并,然后經過一個線性層(權重為wh),加tanh激活,就得到當前層的備用輸出ht~。
此時得到的備用輸出還是無法直接輸出,因為GRU只能靠隱狀態來傳遞長時記憶,這里需要將長期保留的記憶加進來再作為當前時間步的隱狀態作為輸出。這里怎樣決定哪些維度保留長期記憶,哪些維度作為備用輸出的隱狀態呢,答案是使用更新門函數進行處理,這個函數同時決定保留多少長期記憶,更新多少當前步產生的記憶。
首先用sigmoid更新門生成一個更新向量,而后和備用輸出相乘,獲得要更新到長期記憶里的信息。然后用1減去更新向量,這樣就得到了對長期記憶的保留向量。用保留向量與長期記憶按位點乘,就得到了保留的長期記憶,在和更新信息相加,就得到了這一步輸出的長期記憶,ht。

其實現代碼如下
import torch
from torch import nn
from d2l import torch as d2l
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
def get_params(vocab_size, num_hiddens, device):
num_inputs = num_outputs = vocab_size
def normal(shape):
return torch.randn(size=shape, device=device)*0.01
def three():
return (normal((num_inputs, num_hiddens)),
normal((num_hiddens, num_hiddens)),
torch.zeros(num_hiddens, device=device))
W_xz, W_hz, b_z = three() # 更新門參數
W_xr, W_hr, b_r = three() # 重置門參數
W_xh, W_hh, b_h = three() # 候選隱狀態參數
# 輸出層參數
W_hq = normal((num_hiddens, num_outputs))
b_q = torch.zeros(num_outputs, device=device)
# 附加梯度
params = [W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q]
for param in params:
param.requires_grad_(True)
return params
def init_gru_state(batch_size, num_hiddens, device):
return (torch.zeros((batch_size, num_hiddens), device=device), )
def gru(inputs, state, params):
W_xz, W_hz, b_z, W_xr, W_hr, b_r, W_xh, W_hh, b_h, W_hq, b_q = params
H, = state
outputs = []
for X in inputs:
Z = torch.sigmoid((X @ W_xz) + (H @ W_hz) + b_z)
R = torch.sigmoid((X @ W_xr) + (H @ W_hr) + b_r)
H_tilda = torch.tanh((X @ W_xh) + ((R * H) @ W_hh) + b_h)
H = Z * H + (1 - Z) * H_tilda
Y = H @ W_hq + b_q
outputs.append(Y)
return torch.cat(outputs, dim=0), (H,)
vocab_size, num_hiddens, device = len(vocab), 256, d2l.try_gpu()
num_epochs, lr = 500, 1
model = d2l.RNNModelScratch(len(vocab), num_hiddens, device, get_params,
init_gru_state, gru)
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
深度循環神經網絡
之前的都是只有一層隱藏層,而這里則是指使用多層隱藏層,每個隱狀態都連續地傳遞到當前層的下一個時間步和下一層的當前時間步。
它與之前的LSTM代碼幾乎一致,唯一不同的是這里多加了隱藏層,之前我們得到隱狀態后即為結束,這里需要傳到下一個隱藏層。
import torch
from torch import nn
from d2l import torch as d2l
batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)
def get_multi_lstm_params(vocab_size, num_hiddens, num_layers, device):
"""初始化多層LSTM參數"""
num_inputs = vocab_size
num_outputs = vocab_size
def normal(shape):
return torch.randn(size=shape, device=device) * 0.01
def three(in_size, out_size):
"""返回門控機制的三組參數"""
return (normal((in_size, out_size)),
normal((out_size, out_size)),
torch.zeros(out_size, device=device))
params = []
# 初始化各層參數
for i in range(num_layers):
# 第一層輸入為vocab_size,其他層輸入為上一層的隱藏層大小
in_size = num_inputs if i == 0 else num_hiddens
# 輸入門、遺忘門、輸出門、候選記憶元參數
W_xi, W_hi, b_i = three(in_size, num_hiddens)
W_xf, W_hf, b_f = three(in_size, num_hiddens)
W_xo, W_ho, b_o = three(in_size, num_hiddens)
W_xc, W_hc, b_c = three(in_size, num_hiddens)
params.extend([W_xi, W_hi, b_i, W_xf, W_hf, b_f,
W_xo, W_ho, b_o, W_xc, W_hc, b_c])
# 輸出層參數(連接最后一層隱藏層到輸出)
W_hq = normal((num_hiddens, num_outputs))
b_q = torch.zeros(num_outputs, device=device)
params.extend([W_hq, b_q])
# 開啟梯度計算
for param in params:
param.requires_grad_(True)
return params
def init_multi_lstm_state(batch_size, num_hiddens, num_layers, device):
"""初始化多層LSTM的隱藏狀態和記憶元"""
return [(torch.zeros((batch_size, num_hiddens), device=device),
torch.zeros((batch_size, num_hiddens), device=device))
for _ in range(num_layers)]
def multi_lstm(inputs, state, params, num_layers):
"""多層LSTM前向傳播"""
# 解析狀態:每層包含(H, C)
layer_states = state
outputs = []
# 每層參數數量:12個參數(4個門×3組參數)
per_layer_params = 12
current_inputs = inputs
# 逐層計算
for layer in range(num_layers):
layer_params = params[layer * per_layer_params : (layer + 1) * per_layer_params]
H, C = layer_states[layer]
layer_outputs = []
# 時序步計算
for X in current_inputs:
# 輸入門
I = torch.sigmoid((X @ layer_params[0]) + (H @ layer_params[1]) + layer_params[2])
# 遺忘門
F = torch.sigmoid((X @ layer_params[3]) + (H @ layer_params[4]) + layer_params[5])
# 輸出門
O = torch.sigmoid((X @ layer_params[6]) + (H @ layer_params[7]) + layer_params[8])
# 候選記憶元
C_tilda = torch.tanh((X @ layer_params[9]) + (H @ layer_params[10]) + layer_params[11])
# 更新記憶元
C = F * C + I * C_tilda
# 更新隱藏狀態
H = O * torch.tanh(C)
layer_outputs.append(H)
# 當前層輸出作為下一層輸入
current_inputs = layer_outputs
# 更新該層狀態
layer_states[layer] = (H, C)
# 輸出層計算(使用最后一層的輸出)
W_hq, b_q = params[-2], params[-1]
final_outputs = [(H @ W_hq) + b_q for H in current_inputs]
return torch.cat(final_outputs, dim=0), layer_states
# 模型超參數
vocab_size = len(vocab)
num_hiddens = 256 # 每層隱藏單元數
num_layers = 2 # 隱藏層數(可根據需要調整)
device = d2l.try_gpu()
num_epochs, lr = 500, 1 # 多層網絡可能需要調整學習率和迭代次數
# 定義模型
def model_fn(vocab_size, num_hiddens, device, num_layers):
return d2l.RNNModelScratch(
vocab_size, num_hiddens, device,
lambda vs, nh, dev: get_multi_lstm_params(vs, nh, num_layers, dev),
lambda bs, nh, dev: init_multi_lstm_state(bs, nh, num_layers, dev),
lambda inputs, state, params: multi_lstm(inputs, state, params, num_layers)
)
model = model_fn(vocab_size, num_hiddens, device, num_layers)
# 訓練模型
d2l.train_ch8(model, train_iter, vocab, lr, num_epochs, device)
注意力機制
Self-attention
這里的自注意力機制是一對一的,即每一個向量vector輸入,就會輸出一個對應的標簽label

使用全連接層完成輸出,但是我們要考慮前后文,怎么做呢,我們可以一次選定Window大小,可以連接上下文。但是如果Sequence過長,我們的Window設置很大的話,計算量將十分巨大,所以這個時候就用到了self-attention

Self-attention一次將讀取全部的輸入,然后再進行輸出,同時這個也可以進行疊加,疊加多次再生成輸出。具體計算如下
生成b1:
- a1與其他向量的關聯性α
α計算方法:
(1)dot-product:輸入向量分別乘兩個矩陣Wq和Wk,產生的q和k點乘得到α(最常用,用在transformer里面)
(2)additive:得到的q與k相加通過激活函數activate function然后通過transform?得到α
| 方法 | 公式 | 特點 |
|---|---|---|
| Dot-product | α = q?·k |
計算簡單,效率高,最常用 |
| Additive | α = v?·tanh(W·[q;k]) |
更靈活,可處理不同維度的q,k |
這里我們采用的是第一種方法。

實際上自己也會和自己做關聯性,這樣做的意義是保留自身信息,如果不計算α1,1,輸出b1就會由其他向量決定,該操作就類似于人在做選擇時,既要傾聽他人建議,同時也要考慮自身的想法。

然后用Softmax函數對α進行處理

接下來再乘上通過a*Wv得到v,v與α相乘就得到了最終的b1

如果a1,a2關聯性很強,那么b1就可能會比較接近b2。
將輸入向量和QKV這些都視為矩陣,可以發現實際上只有三個參數是我們未知的,Wq、Wk和Wv

多頭注意力機制
多頭注意力機制與自注意機制類似,只是它有多個qkv

我們這里計算依舊從前,不過我們計算bi1就只考慮ki,1和vi,1這些部分,計算bi2就只考慮qi2,ki2,vi2這部分,最終計算的時候將bi,1和bi,2視為一個矩陣進行計算,得到最終的bi。

Transfrom
Transfrom采用的結構是Sequence to Sequence,縮寫為Seq2Seq,如下圖所示

Encoder
encoder可以用rnn/cnn/self attention,編碼的目的就是,輸入一排向量,輸出一排等同數量的向量。

Encoder結構具體可分為以下部分
1、輸入層
負責接收輸入序列,并將每個符號(如單詞)轉換為一個向量表示。這個向量表示通常被稱為詞嵌入(embedding),它是通過學習大量語料庫而得到的。
2、位置編碼層(positonal encoding)
由于Transformer模型采用自注意力機制,它不像RNN那樣天然地考慮輸入序列中詞的位置信息。因此,位置編碼層負責將每個詞的位置信息編碼為一個向量,并將其加到詞嵌入上。這樣,模型就能考慮到詞在序列中的位置。
3、編碼器層堆疊(block)
Transformer模型的Encoder部分通常由多個編碼器層堆疊而成。每個編碼器層都具有相同的結構block,并且它們之間通過殘差連接和規范化層進行連接。
4、多頭自注意力子層(multi head attention)
在每個編碼器層中,首先是一個多頭自注意力子層。這個子層負責計算輸入序列中每個詞與其他詞之間的相關性,并生成一個注意力權重分布。這個注意力權重分布用于計算一個加權和的向量表示,這個向量表示考慮了序列中其他詞對當前詞的影響。
5、前饋全連接子層(feed forward)
在每個編碼器層中,緊接著多頭自注意力子層的是一個前饋全連接子層。這個子層負責將多頭自注意力子層的輸出通過一個全連接神經網絡進行處理,以捕獲更復雜的特征表示。
6、殘差連接和規范化層
在每個子層之后,都有一個殘差連接和一個規范化層。殘差連接負責將子層的輸入與輸出相加,以避免梯度消失和表示瓶頸問題。規范化層則負責對子層的輸出進行規范化處理,以加速模型的收斂速度。
Block結構如下圖所示

其中,一個block包含了以下
(1)self-attention
(2)feed forward(在李宏毅的課件里稱為FC)
(3)residual
(4)norm
self-attention子層中的residual connection(殘差連接)被用來添加輸入數據和輸出數據之間的直接連接(輸入和輸出相加一次),作用如下
(1)避免梯度消失或梯度爆炸問題;
(2)殘差連接可以使輸入數據和輸出數據的形狀保持不變,避免信息丟失,并且可以使模型的訓練速度和精度得到提高。
得到residual后對其進行normalization(layer normalization,這個是計算一整個樣本的標準化方式)。
norm完后,在全連接層層也進行residual。
residual后,再norm,最后得到一個block的輸出。

Decoder
Decoder實際上與Encoder十分相像,遮擋中間的一部分,可以發現除了最后的Decoder走了線性層和Softmax,兩者在前面的操作幾乎無二。

需要注意的是這里的多頭注意力機制是加了Masked的,這個是什么意思呢。因為是一個一個輸入的,比如當前要計算α2,我們還不知道a3和a4,那我們就只計算a1和a2,而不再考慮a3,a4。
完整的結構

中間這部分是一段多頭注意力子層,將Encoder的內容匯合進來結合之前的再傳到全連接層,最終經過線性層和Softmax進行輸出概率。

浙公網安備 33010602011771號