【深度學習 01】線性迴歸+PyTorch實現

最菜程式設計師Sxx發表於2022-03-27

1. 線性迴歸

1.1 線性模型

    當輸入包含d個特徵,預測結果表示為:

     

    記x為樣本的特徵向量,w為權重向量,上式可表示為:

    

    對於含有n個樣本的資料集,可用X來表示n個樣本的特徵集合,其中行代表樣本,列代表特徵,那麼預測值可用矩陣乘法表示為:

    

    給定訓練資料特徵X和對應的已知標籤y,線性迴歸的⽬標是找到⼀組權重向量w和偏置b:當給定從X的同分布中取樣的新樣本特徵時,這組權重向量和偏置能夠使得新樣本預測標籤的誤差儘可能小。

1.2 損失函式(loss function)

    損失函式又稱代價函式(cost function),通常用其來度量目標的實際值和預測值之間的誤差。在迴歸問題中,常用的損失函式為平方誤差函式:

    

     

    我們的目標便是求得最小化損失函式下引數w和b的值:

       

    求解上式,一般有以下兩種方式:

    1> 正規方程(解析解)

    

    2> 梯度下降(gradient descent)

    (1)初始化模型引數的值,如隨機初始化;

    (2)從資料集中隨機抽取小批量樣本且在負梯度的方向上更新引數,並不斷迭代這一步驟。

    

    上式中:n表示每個小批量中的樣本數,也稱批量大小(batch size)、α表示學習率(learning rate),n和α的值需要手動預先指定,而不是模型訓練得到的,這類引數稱為超引數(hyperparameter),選擇超引數的過程稱為調參(hyperparameter tuning)。

    梯度下降和正規方程比較:

 

1.3 向量化加速

    為了加快模型訓練速度,可以採用向量化計算的方式,這通常會帶來數量級的加速。下邊用程式碼簡單對比測試下向量化計算的加速效果。

import math
import time
import numpy as np
import torch
from d2l import torch as d2l

# a、b是全為1的10000維向量
n = 10000
a = torch.ones(n)
b = torch.ones(n)


class Timer:
    def __init__(self):
        """記錄多次執行時間"""
        self.tik = None
        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()


c = torch.zeros(n)
timer = Timer()
for i in range(n):
    c[i] = a[i] + b[i]
print(f'{timer.stop():.5f} sec')

timer.start()
d = a + b
print(f'{timer.stop():.5f} sec')  

    程式碼執行結果如下,可見向量化程式碼確實極大的提高了計算速度。

    注:這裡向量化計算d=a+b的時間不知道為什麼統計出來是0,可能是跟電腦的計時器精度有關。

2. 從零實現線性迴歸

    線性迴歸的實現過程可以簡單總結為以下幾個步驟:

    (1)讀取資料(或構造資料),轉換成需要的格式和型別,並生成標籤 ;

    (2)定義初始化模型引數、定義模型、定義損失函式、定義優化演算法;

    (3)使用優化演算法訓練模型。

import random
import torch
import numpy as np
from matplotlib import pyplot as plt
from d2l import torch as d2l


# 構造資料集
def synthetic_data(w, b, num_examples):
    """生成 y = Xw + b + 噪聲。"""
    # 均值為0,方差為1的隨機數,行數為樣本數,列數是w的長度(行代表樣本,列代表特徵)
    X = torch.normal(0, 1, (num_examples, len(w)))  # pytorch較新版本
    # X = torch.tensor(np.random.normal(0, 1, (num_examples, len(w))), dtype=torch.float32)  # pytorch1.1.0版本
    y = torch.matmul(X, w) + b
    # 均值為0,方差為1的隨機數,噪聲項。
    y += torch.normal(0, 0.01, y.shape)  # pytorch較新版本
    # y += torch.tensor(np.random.normal(0, 0.01, y.shape), dtype=torch.float32)  # pytorch1.1.0版本
    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)
print('features:', features[0], '\nlabel:', labels[0])

d2l.set_figsize()
d2l.plt.scatter(features[:, 1].detach().numpy(), labels.detach().numpy(), 1)


# 生成一個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]


batch_size = 10
for X, y in data_iter(batch_size, features, labels):
    print(X, '\n', y)
    break

# 定義初始化模型引數
w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)  # pytorch較新版本
# w = torch.autograd.Variable(torch.tensor(np.random.normal(0, 0.01, size=(2, 1)),
#                                          dtype=torch.float32), requires_grad=True)  # pytorch1.1.0版本
b = torch.zeros(1, requires_grad=True)


# 定義模型
def linreg(X, w, b):
    """線性迴歸模型。"""
    return torch.matmul(X, w) + b


# 定義損失函式
def squared_loss(y_hat, y):
    """均方損失。"""
    return (y_hat - y.reshape(y_hat.shape))**2 / 2


# 定義優化演算法
def sgd(params, lr, batch_size):
    """小批量隨機梯度下降"""
    with torch.no_grad():
        for param in params:
            param -= lr * param.grad / batch_size
            param.grad.zero_()


# 訓練過程
lr = 0.03
num_epochs = 3
net = linreg
loss = squared_loss

for 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}')

print(f'w的估計誤差:{true_w - w.reshape(true_w.shape)}')
print(f'b的估計誤差:{true_b - b}') 

3. 使用深度學習框架(PyTorch)實現線性迴歸

    使用PyTorch封裝的高階API可以快速高效的實現線性迴歸

import numpy as np
import torch
from torch import nn  # 'nn'是神經網路的縮寫
from torch.utils import data
from d2l import torch as d2l


# 構造資料集
def synthetic_data(w, b, num_examples):
    """生成 y = Xw + b + 噪聲。"""
    # 均值為0,方差為1的隨機數,行數為樣本數,列數是w的長度(行代表樣本,列代表特徵)
    X = torch.normal(0, 1, (num_examples, len(w)))  # pytorch較新版本
    # X = torch.tensor(np.random.normal(0, 1, (num_examples, len(w))), dtype=torch.float32)  # pytorch1.1.0版本
    y = torch.matmul(X, w) + b
    # 均值為0,方差為1的隨機數,噪聲項。
    y += torch.normal(0, 0.01, y.shape)  # pytorch較新版本
    # y += torch.tensor(np.random.normal(0, 0.01, y.shape), dtype=torch.float32)  # pytorch1.1.0版本
    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)

d2l.set_figsize()
d2l.plt.scatter(features[:, 1].detach().numpy(), labels.detach().numpy(), 1)


# 呼叫框架中現有的API來讀取資料
def load_array(data_arrays, batch_size, is_train=True):
    """構造一個PyTorch資料迭代器"""
    dataset = data.TensorDataset(*data_arrays)
    return data.DataLoader(dataset, batch_size, shuffle=is_train)


batch_size = 10
data_iter = load_array((features, labels), batch_size)

print(next(iter(data_iter)))

# 使用框架預定義好的層
net = nn.Sequential(nn.Linear(2, 1))

# 初始化模型引數(等價於前邊手動實現w、b以及network的方式)
net[0].weight.data.normal_(0, 0.01)  # 使用正態分佈替換掉w的值
net[0].bias.data.fill_(0)

# 計算均方誤差使用MSELoss類,也稱為平方L2範數
loss = nn.MSELoss()

# 例項化SGD例項
trainer = torch.optim.SGD(net.parameters(), lr=0.03)

# 訓練
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(features), labels)
    print(f'epoch {epoch + 1}, loss {l:f}')

w = net[0].weight.data
print('w的估計誤差:', true_w - w.reshape(true_w.shape))
b = net[0].bias.data
print('b的估計誤差:', true_b - b)

4. 報錯總結

1. torch.normal()報錯,這個是由於PyTorch版本問題,torch.normal()函式的引數形式和用法有所變化。

    要生成均值為0且方差為1的隨機數,pytorch1.1.0和pytorch1.9.0可以分別採用以下形式:

# pytorch1.9.0 
X = torch.normal(0, 1, (num_examples, len(w)))    
# pytorch1.1.0(也適用於高版本) 
X = torch.tensor(np.random.normal(0, 1, (num_examples, len(w))), dtype=torch.float32)

2. d2l庫安裝報錯。這個我在公司電腦上直接一行pip install d2l成功安裝,回家換自己電腦,各種報錯。解決之後發現大多都是找不到安裝源、缺少相關庫或者庫版本不相容的問題。

    安裝方式:conda install d2l 或 pip install d2l。網速太慢下不下來可以選擇國內源映象:

pip install d2l -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com

    國內常用源映象:

# 清華:https://pypi.tuna.tsinghua.edu.cn/simple
# 阿里雲:http://mirrors.aliyun.com/pypi/simple/
# 中國科技大學 https://pypi.mirrors.ustc.edu.cn/simple/
# 華中理工大學:http://pypi.hustunique.com/
# 山東理工大學:http://pypi.sdutlinux.org/
# 豆瓣:http://pypi.douban.com/simple/ 

    需要注意的是:有時候使用conda install d2l命令無法下載,改為pip 命令後即可下載成功。這是因為有些包只能通過pip安裝。Anaconda提供超過1,500個軟體包,包括最流行的資料科學、機器學習和AI框架,這與PyPI上提供的150,000多個軟體包相比,只是一小部分。

    Python官方安裝whl包和tar.gz包安裝方法:

    安裝whl包:pip install wheel,pip install xxx.whl

    安裝tar.gz包:cd到解壓後路徑,python setup.py install

 

參考資料

[1] Python錯誤筆記(2)之Pytorch的torch.normal()函式

[2] 動手學深度學習 李沐

相關文章