文章目錄
- 線性回歸基本概念
- 隨機梯度下降
- 矢量化加速
- 正態分布和平方損失
- 極大似然估計
- 線性回歸實現
- 從0開始
- **`torch.no_grad()`的兩種用途**
- **為什么需要 `l.sum().backward()`?**
- 調用現成庫
- softmax回歸
- 圖像數據集
- 從0開始實現softmax
- 利用框架API實現
課程學習自李牧老師B站的視頻和網站文檔
https://zh-v2.d2l.ai/chapter_preliminaries
線性回歸基本概念
線性模型可以看作單層神經網絡,由多個輸入得到一個輸出
線性回歸可以解決預測多少的問題
適用問題:
- 數值預測:
- 預測連續值,如房價(基于面積、位置等)、股票價格(基于歷史數據)、溫度預測(基于時間和天氣因素)。
- 示例:用房屋面積和臥室數預測房價。
- 趨勢分析:
- 分析變量之間的線性趨勢,如銷售額隨廣告投入的增長。
- 示例:研究學習時間對考試成績的影響。
- 因果關系探索:
- 初步評估輸入特征對輸出的影響(如廣告支出對銷售額的貢獻)。
- 示例:分析肥料用量對作物產量的關系。
- 數據標準化:
- 用線性回歸擬合基線(如去趨勢數據),用于時間序列分析。
- 示例:移除季節性影響后的銷售預測。
下面兩組參數分別表示求一個損失均值,期望可以最小化損失的w和b
解析解
隨機梯度下降
梯度下降最簡單的用法是計算損失函數(數據集中所有樣本的損失均值) 關于模型參數的導數(在這里也可以稱為梯度)。 但實際中的執行可能會非常慢:因為在每一次更新參數之前,我們必須遍歷整個數據集。 因此,我們通常會在每次需要計算更新的時候隨機抽取一小批樣本, 這種變體叫做小批量隨機梯度下降
公式表示梯度的原先的w減去均值乘上學習率
批量大小和學習率(沿著梯度走多長的方向)都是預先指定的
訓練過程中不斷更新的參數叫超參數,調參是選擇超參數的過程
矢量化加速
在訓練我們的模型時,我們經常希望能夠同時處理整個小批量的樣本。 為了實現這一點,需要我們對計算進行矢量化, 從而利用線性代數庫,而不是在Python中編寫開銷高昂的for循環。
什么是矢量化?
- 矢量化(vectorization)是指用向量或矩陣操作替代循環,特別是在計算中同時處理多個數據點。這是一種利用現代處理器(如 CPU 或 GPU)并行計算能力的技巧。
import numpy as np
import time
import torch
import math
class Timer: """記錄多次運行時間"""def __init__(self):self.times = []self.start()def start(self):"""啟動計時器"""self.tik = time.time()def stop(self):"""停止計時器并將時間記錄在列表中"""self.times.append(time.time() - self.tik)return self.times[-1]def avg(self):"""返回平均時間"""return sum(self.times) / len(self.times)def sum(self):"""返回時間總和"""return sum(self.times)def cumsum(self):"""返回累計時間"""return np.array(self.times).cumsum().tolist()n=10000
a=torch.ones([n])
b=torch.ones([n])c=torch.zeros([n])
time1=Timer()
for i in range(n):c[i]=a[i]+b[i]
print(f"{time1.stop()} sec")time2=Timer()
d=a+b
print(f"{time2.stop()} sec")
正態分布和平方損失
極大似然估計
什么是極大似然估計?
- 極大似然估計是一種統計方法,用于根據觀測數據找到最可能產生這些數據的模型參數。簡單說,就是從數據“反推”出最合理的參數值。
線性回歸實現
從0開始
因為感覺應該也不會從頭開始寫函數,所以就只分析一下背后的原理不做實現了
生成數據集沒什么好說的,注意,features
中的每一行都包含一個二維數據樣本, labels
中的每一行都包含一維標簽值(一個標量)
def synthetic_data(w, b, num_examples): #@save"""生成y=Xw+b+噪聲"""X = torch.normal(0, 1, (num_examples, len(w)))y = torch.matmul(X, w) + by += torch.normal(0, 0.01, y.shape)return X, y.reshape((-1, 1))true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
在下面的代碼中,我們定義一個data_iter
函數, 該函數接收批量大小、特征矩陣和標簽向量作為輸入,生成大小為batch_size
的小批量。 每個小批量包含一組特征和標簽。
def data_iter(batch_size, features, labels):num_examples = len(features)indices = list(range(num_examples))# 這些樣本是隨機讀取的,沒有特定的順序random.shuffle(indices)for i in range(0, num_examples, batch_size):batch_indices = torch.tensor(indices[i: min(i + batch_size, num_examples)])yield features[batch_indices], labels[batch_indices]
yield
用于實現一個 生成器(Generator),它的核心作用是:
按批次(batch)逐個返回數據和標簽,而不是一次性返回所有數據。
在我們開始用小批量隨機梯度下降優化我們的模型參數之前, 我們需要先有一些參數。 在下面的代碼中,我們通過從均值為0、標準差為0.01的正態分布中采樣隨機數來初始化權重, 并將偏置初始化為0。
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
初始化參數后每次更新參數時計算梯度,然后從梯度減小的方向更新梯度
定義線性回歸模型:
def linreg(X, w, b): #@save"""線性回歸模型"""return torch.matmul(X, w) + b
定義損失函數:
def squared_loss(y_hat, y): #@save"""均方損失"""return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2
定義優化算法:在每一步中,使用從數據集中隨機抽取的一個小批量,然后根據參數計算損失的梯度。 接下來,朝著減少損失的方向更新我們的參數。每 一步更新的大小由學習速率lr
決定
def sgd(params, lr, batch_size): #@save"""小批量隨機梯度下降"""with torch.no_grad():for param in params:param -= lr * param.grad / batch_sizeparam.grad.zero_()
params: 模型參數列表(如 [w, b]
),每個參數是張量且帶有 .grad
屬性。
with torch.no_grad():
-
作用:禁用梯度計算上下文,確保參數更新時不會記錄梯度(避免干擾后續反向傳播)
-
除以
batch_size
:梯度是批次內樣本梯度的總和,除以批次大小得到平均梯度(保證不同批次大小下的穩定性)。也叫梯度歸一化,因為pytorch默認對批次內樣本梯度求和
param.grad.zero_()
- 清空梯度:將當前參數的梯度置零,避免下一次反向傳播時梯度累加
那么到這里,數值初始化,損失計算,優化函數都搞定了,就可以開始訓練了
lr = 0.03
num_epochs = 3
net = linreg
loss = squared_lossfor epoch in range(num_epochs):for X, y in data_iter(batch_size, features, labels):l = loss(net(X, w, b), y) # X和y的小批量損失# 因為l形狀是(batch_size,1),而不是一個標量。l中的所有元素被加到一起,# 并以此計算關于[w,b]的梯度l.sum().backward()sgd([w, b], lr, batch_size) # 使用參數的梯度更新參數with torch.no_grad():train_l = loss(net(features, w, b), labels)print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')
torch.no_grad()
的兩種用途
- 參數更新時(
sgd
內):避免更新操作被記錄到計算圖中。 - 評估時:禁用梯度以節省內存和計算資源。
為什么需要 l.sum().backward()
?
-
PyTorch 的
backward()
需要標量輸入。如果l
是形狀為(batch_size, 1)
的張量,必須先求和或均值轉為標量。 -
梯度計算邏輯:
對每個樣本的損失求梯度后,PyTorch 默認對批次內梯度求和(
sum
),因此后續需手動除以batch_size
(在sgd
中實現)。
調用現成庫
接下來try一try調用框架現成的庫
同樣先生成數據
true_w=torch.tensor([3,-3.4])
true_b=3.2
x,y=synthetic_data(true_w,true_b,1000)
讀取數據可以調用現成的數據迭代器來進行,同時可以指定樣本batch的大小,以及是否打亂數據
DataLoader其實本質上就是一個迭代器
def load_array(data_arrays,batch_size,is_train=True):dataset=data.TensorDataset(*data_arrays)return data.DataLoader(dataset,batch_size,shuffle=is_train)batch_size=10
data_iter=load_array((x,y),batch_size)
print(next(iter(data_iter)))
對于標準深度學習模型,我們可以使用框架的預定義好的層。這使我們只需關注使用哪些層來構造模型,而不必關注層的實現細節。 我們首先定義一個模型變量net
,它是一個Sequential
類的實例。 Sequential
類將多個層串聯在一起。 當給定輸入數據時,Sequential
實例將數據傳入到第一層, 然后將第一層的輸出作為第二層的輸入,以此類推。
單層神經網絡就是全連接層,在PyTorch中,全連接層在Linear
類中定義。 值得注意的是,我們將兩個參數傳遞到nn.Linear
中。 第一個指定輸入特征形狀,即2,第二個指定輸出特征形狀,輸出特征形狀為單個標量,因此為1。
from torch import nn
net=nn.Sequential(nn.Linear(2,1))
指定每個權重參數應該從均值為0、標準差為0.01的正態分布中隨機采樣, 偏置參數將初始化為零。
net[0].weight.data.normal_(0,0.01)
net[0].bias.data.fill_(0)
損失函數和優化算法同樣有現成的
loss=nn.MSELoss()
trainer=torch.optim.SGD(net.parameters(),lr=0.03)
接下來開始訓練,使用模型net計算輸出,之前生成的優化算法trainer更新結果
num_epochs=3
for epoch in range(num_epochs):for x,y in data_iter:l=loss(net(x),y)trainer.zero_grad()l.backward()trainer.step()l=loss(net(X),Y)print(f'epoch {epoch + 1}, loss {l:f}')
完整代碼
from operator import ne
import numpy as np
import torch
from torch.utils import data
def synthetic_data(w, b, num_examples): #@save"""生成y=Xw+b+噪聲"""X = torch.normal(0, 1, (num_examples, len(w)))y = torch.matmul(X, w) + by += torch.normal(0, 0.01, y.shape)return X, y.reshape((-1, 1))
true_w=torch.tensor([3,-3.4])
true_b=3.2
X,Y=synthetic_data(true_w,true_b,1000)def load_array(data_arrays,batch_size,is_train=True):dataset=data.TensorDataset(*data_arrays)return data.DataLoader(dataset,batch_size,shuffle=is_train)batch_size=10
data_iter=load_array((X,Y),batch_size)from torch import nn
net=nn.Sequential(nn.Linear(2,1))net[0].weight.data.normal_(0,0.01)
net[0].bias.data.fill_(0)loss=nn.MSELoss()
trainer=torch.optim.SGD(net.parameters(),lr=0.01)num_epochs=5
for epoch in range(num_epochs):for x,y in data_iter:l=loss(net(x),y)trainer.zero_grad()l.backward()trainer.step()l=loss(net(X),Y)print(f'epoch {epoch + 1}, loss {l:f}')
softmax回歸
用于解決分類的問題,預測圖片中物品是哪一類
什么是置信度?
- 置信度(confidence)表示模型對某個預測結果的“把握程度”,通常是一個介于 0 到 1 之間的值。數值越高,模型越認為預測正確。
對分類進行編碼
softmax本質上也是一個根據輸入計算得到輸出的單層網絡
什么是全連接層?
- 全連接層(Fully Connected Layer, FC Layer)是神經網絡中的一種層級結構,每個神經元都與前一層的所有神經元相連,像一張“全網”聯系的網絡,用于整合和變換特征。
要將輸出視為概率,我們必須保證在任何數據上的輸出都是非負的且總和為1。 此外,我們需要一個訓練的目標函數,來激勵模型精準地估計概率。 例如, 在分類器輸出0.5的所有樣本中,我們希望這些樣本是剛好有一半實際上屬于預測的類別。 這個屬性叫做校準(calibration)。
接下來對預測的概率值做一個處理,使其非負且總和為1
第一個公式描述的是給定x的前提下所有標簽的連乘概率,每一次事件都會對結果產生影響,MLE(最大似然估計)就是要最大化這個概率
第二個公式是取對數,將乘法轉換為加法,最大值改為求最小值
第三個公式描述的是損失函數
圖像數據集
圖像數據集使用Fashion-MNIST數據集
-
像素值范圍:
- 在計算機中,一張圖像由像素(Pixel)組成,每個像素的顏色通常用數值表示。
- 對于 8位灰度圖像(如FashionMNIST),每個像素的取值范圍是
[0, 255]
:0
表示純黑色,255
表示純白色,- 中間值表示不同深淺的灰色。
- 對于彩色圖像(如RGB),每個通道(紅、綠、藍)的取值范圍也是
[0, 255]
。
-
歸一化(Normalization):
- 歸一化是指將數據縮放到一個固定的范圍(這里是
[0, 1]
)。 transforms.ToTensor()
會自動將像素值從[0, 255]
除以255
,轉換到[0, 1]
。
- 歸一化是指將數據縮放到一個固定的范圍(這里是
-
灰度圖像(Grayscale Image):
- 是一種單通道圖像,每個像素只有一個數值表示亮度(沒有顏色信息)。
- 數值范圍通常是
[0, 255]
:0
:純黑色,255
:純白色,- 中間值:灰色(如
128
是中灰色)。
-
FashionMNIST 數據集是灰度圖像,每張圖片大小為
28x28
像素,像素值范圍[0, 255]
。 -
通過
ToTensor()
轉換后:- 形狀從
(28, 28)
變為(1, 28, 28)
(添加了通道維度)。 - 像素值從
[0, 255]
縮放到[0, 1]
。
- 形狀從
先下載數據
trans=transforms.ToTensor()
mnist_train=torchvision.datasets.FashionMNIST(root='./data',train=True,transform=trans,download=True
)
mnist_test=torchvision.datasets.FashionMNIST(root='./data',train=False,transform=trans,download=True
)
再展示圖片
import re
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
import matplotlib.pyplot as plttrans=transforms.ToTensor()
mnist_train=torchvision.datasets.FashionMNIST(root='./data',train=True,transform=trans,download=True
)
mnist_test=torchvision.datasets.FashionMNIST(root='./data',train=False,transform=trans,download=True
)
def get_fashion_mnist_labels(labels): """返回Fashion-MNIST數據集的文本標簽"""text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat','sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']return [text_labels[int(i)] for i in labels]def show_fashion_mnist(imgs,num_rows,num_cols,title=None,scale=1.5):figsize=(num_cols * scale, num_rows* scale)_, axes = plt.subplots(num_rows, num_cols, figsize=figsize)axes = axes.flatten()for i, (ax,img) in enumerate(zip(axes, imgs)):if torch.is_tensor(img):ax.imshow(img.numpy())else:ax.imshow(img)ax.axes.get_xaxis().set_visible(False) ax.axes.get_yaxis().set_visible(False)if title:ax.set_title(title[i])plt.show()return axes
X, y = next(iter(data.DataLoader(mnist_train, batch_size=18)))
show_fashion_mnist(X.reshape(18, 28, 28), 2, 9, title=get_fashion_mnist_labels(y))
接下來通過內置數據迭代器來讀取小批量數據
batch_size = 256
train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True,num_workers=4)
綜合下載數據并返回迭代器
def load_data_fashion_mnist(batch_size, resize=None): #@save"""下載Fashion-MNIST數據集,然后將其加載到內存中"""trans = [transforms.ToTensor()]if resize:trans.insert(0, transforms.Resize(resize))trans = transforms.Compose(trans)mnist_train = torchvision.datasets.FashionMNIST(root="../data", train=True, transform=trans, download=True)mnist_test = torchvision.datasets.FashionMNIST(root="../data", train=False, transform=trans, download=True)return (data.DataLoader(mnist_train, batch_size, shuffle=True,num_workers=get_dataloader_workers()),data.DataLoader(mnist_test, batch_size, shuffle=False,num_workers=get_dataloader_workers()))
從0開始實現softmax
將28*28的圖像展平后得到784的向量,也就是一個w,而我們有十個輸出,所以有10個w
所以權重將構成一個784*10的矩陣
num_inputs = 784
num_outputs = 10W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)
def softmax(X):X_exp = torch.exp(X)partition = X_exp.sum(1, keepdim=True)return X_exp / partition # 這里應用了廣播機制
定義模型:其實就是將數據轉換為w*x+b
def net(X):return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
在分類問題中,最常用的損失函數就是交叉熵函數
- 分類任務需要模型輸出 “屬于某一類的概率”(如“這張圖是貓的概率是90%”)。
- 交叉熵直接衡量 預測概率分布 和 真實標簽分布 的差異,完美匹配這一需求。
具體實現
def cross_entropy(y_hat, y):return - torch.log(y_hat[range(len(y_hat)), y])cross_entropy(y_hat, y)
softmax求精度,就是把預測值跟真實值作比較,最后用正確數/總數,下面描述的其實是怎么得到預測值的一個過程
利用框架API實現
我們還是一樣先讀取數據
def load_data_fashion_mnist(batch_size, resize=None): """下載Fashion-MNIST數據集,然后將其加載到內存中"""trans = [transforms.ToTensor()]if resize:trans.insert(0, transforms.Resize(resize))trans = transforms.Compose(trans)mnist_train = torchvision.datasets.FashionMNIST(root="../data", train=True, transform=trans, download=True)mnist_test = torchvision.datasets.FashionMNIST(root="../data", train=False, transform=trans, download=True)return (data.DataLoader(mnist_train, batch_size, shuffle=True,num_workers=get_dataloader_workers()),data.DataLoader(mnist_test, batch_size, shuffle=False,num_workers=get_dataloader_workers()))batch_size = 256
train_iter, test_iter = load_data_fashion_mnist(batch_size)
進行w和b的初始化
net=nn.Sequential(nn.Flatten(),nn.Linear(784, 10))def init_weights(m):if type(m) == nn.Linear:nn.init.normal_(m.weight, mean=0, std=0.01)nn.init.constant_(m.bias, 0)net.apply(init_weights)
模型訓練代碼
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import transforms, datasets
import matplotlib.pyplot as plt
import numpy as np# 1. 數據加載函數
def load_data_fashion_mnist(batch_size):"""下載Fashion-MNIST數據集并返回數據加載器"""transform = transforms.ToTensor()train_data = datasets.FashionMNIST(root="./data", train=True, transform=transform, download=True)test_data = datasets.FashionMNIST(root="./data", train=False, transform=transform, download=True)return (DataLoader(train_data, batch_size, shuffle=True, num_workers=0), # Windows必須設為0DataLoader(test_data, batch_size, shuffle=False, num_workers=0))# 2. 模型定義
def init_weights(m):if type(m) == nn.Linear:nn.init.normal_(m.weight, std=0.01)nn.init.zeros_(m.bias)net = nn.Sequential(nn.Flatten(),nn.Linear(784, 10)
)
net.apply(init_weights)# 3. 訓練工具
loss = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(net.parameters(), lr=0.1)def accuracy(y_hat, y):"""計算預測正確的數量"""if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:y_hat = y_hat.argmax(axis=1)cmp = y_hat.type(y.dtype) == yreturn float(cmp.sum())class Accumulator:"""用于累加多個指標"""def __init__(self, n):self.data = [0.0] * ndef add(self, *args):self.data = [a + float(b) for a, b in zip(self.data, args)]def __getitem__(self, idx):return self.data[idx]# 4. 可視化類(適配普通Python腳本)
class Animator:def __init__(self, xlabel='epoch', legend=None, figsize=(8, 4)):self.fig, self.ax = plt.subplots(figsize=figsize)self.xlabel = xlabelself.legend = legendself.lines = Noneplt.show(block=False) # 非阻塞顯示def update(self, x, y_values):"""更新圖表數據"""if self.lines is None:# 第一次調用時創建線條self.lines = []for _ in range(len(y_values)):line, = self.ax.plot([], [])self.lines.append(line)if self.legend:self.ax.legend(self.lines, self.legend)self.ax.grid()# 更新每條線的數據for line, y in zip(self.lines, y_values):x_data = list(line.get_xdata())y_data = list(line.get_ydata())x_data.append(x)y_data.append(y)line.set_data(x_data, y_data)# 調整坐標軸范圍self.ax.relim()self.ax.autoscale_view()self.ax.set_xlabel(self.xlabel)self.fig.canvas.draw()plt.pause(0.01) # 短暫暫停讓GUI更新# 5. 訓練和評估函數
def train_epoch(net, train_iter, loss, optimizer):"""訓練一個epoch"""metric = Accumulator(3) # 訓練損失總和,訓練準確度總和,樣本數for X, y in train_iter:y_hat = net(X)l = loss(y_hat, y)optimizer.zero_grad()l.backward()optimizer.step()metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())return metric[0]/metric[2], metric[1]/metric[2] # 平均損失,準確率def evaluate_accuracy(net, data_iter):"""評估模型在數據集上的準確率"""net.eval() # 評估模式metric = Accumulator(2) # 正確預測數,總預測數with torch.no_grad():for X, y in data_iter:metric.add(accuracy(net(X), y), y.numel())return metric[0] / metric[1]def train(net, train_iter, test_iter, loss, num_epochs, optimizer):"""完整訓練過程"""animator = Animator(xlabel='epoch', legend=['train loss', 'train acc', 'test acc'])for epoch in range(num_epochs):train_loss, train_acc = train_epoch(net, train_iter, loss, optimizer)test_acc = evaluate_accuracy(net, test_iter)animator.update(epoch + 1, (train_loss, train_acc, test_acc))plt.show(block=True) # 訓練結束后保持窗口顯示# 6. 主程序
if __name__ == '__main__':# 參數設置batch_size = 256num_epochs = 10# 加載數據train_iter, test_iter = load_data_fashion_mnist(batch_size)# 開始訓練print("開始訓練...")train(net, train_iter, test_iter, loss, num_epochs, optimizer)print("訓練完成!")# 保存模型torch.save(net.state_dict(), 'fashion_mnist_model.pth')print("模型已保存到 fashion_mnist_model.pth")
后面寫的有點草率,一直學一個東西會疲勞,先調整一下再回來接著學