本文主要介紹神經網絡萬能逼近理論,并且通過PyTorch展示了兩個案例來說明神經網絡的函數逼近功能。
大多數人理解"函數"為高等代數中形如"f(x)=2x"的表達式,但是實際上,函數只是輸入到輸出的映射關系,其形式是多樣的。
拿個人衣服尺寸預測來說,我們用機器學習來實現這個功能,就是將個人身高、體重、年齡作為輸入,將衣服尺寸作為輸出,實現輸入-輸出映射。
具體來說,需要以下幾個步驟:
· 收集關鍵數據(大量人口的身高/體重/年齡,已經對應的實際服裝尺寸)。
· 訓練模型來實現輸入-輸出的映射逼近。
· 對未知數據進行預測來驗證模型。
如果輸出是輸入特征的線性映射,那么模型的訓練往往相對簡單,只需要一個線性回歸就可以實現;size = a*height + b*weight + c*age + d。
但是,通常假設輸出是輸入特征的線性映射是不夠合理和不完全準確的。現實情況往往很復雜,存在一定的特例和例外。常見的問題(字體識別、圖像分類等)顯然涉及到復雜的模式,需要從高維輸入特征中學習映射關系。
但是根據萬能逼近理論,帶有單隱藏的人工神經網絡就能夠逼近任意函數,因此可以被用于解決復雜問題。
人工神經網絡
本文將只研究具有輸入層、單個隱藏層和輸出層的完全連接的神經網絡。在服裝尺寸預測器的例子中,輸入層將有三個神經元(身高、體重和年齡),而輸出層只有一個(預測尺寸)。在這兩者之間,有一個隱藏層,上面有一些神經元(下圖中有5個,但實際上可能更大一些,比如1024個)。
網絡中的每個連接都有一些可調整的權重。訓練意味著找到好的權重,使給定輸入集的預測大小與實際大小之間存在微小差異。
每個神經元與下一層的每個神經元相連。這些連接都有一定的權重。每個神經元的值沿著每個連接傳遞,在那里它乘以權重。然后所有的神經元都會向前反饋到輸出層,然后輸出一個結果。訓練模型需要為所有連接找到合適的權重。萬能逼近定理的核心主張是,在有足夠多的隱藏神經元的情況下,存在著一組可以近似任何函數的連接權值,即使該函數不是像f(x)=x²那樣可以簡潔地寫下來的函數。即使是一個瘋狂的,復雜的函數,比如把一個100x100像素的圖像作為輸入,輸出"狗"或"貓"的函數也被這個定理所覆蓋。
非線性關系
神經網絡之所以能夠逼近任意函數,關鍵在于將非線性關系函數整合到了網絡中。每層都可以設置激活函數實現非線性映射,換言之,人工神經網絡不只是進行線性映射計算。常見的非線性激活函數有 ReLU, Tanh, Sigmoid等。
ReLU是一個簡單的分段線性函數-計算消耗小。另外兩個都涉及到指數運算,因此計算成本更高
為了展示人工神經網絡的萬能逼近的能力,接下來通過PyTorch實現兩個案例。
案例一:任意散點曲線擬合
神經網絡可能面臨的最基本的情況之一就是學習兩個變量之間的映射關系。例如,假設x值表示時間,y坐標表示某條街道上的交通量。在一天中的不同時間點都會出現高峰和低谷,因此這不是一種線性關系。
下面的代碼首先生成符合正態分布的隨機點,然后訓練一個網絡,該網絡將x坐標作為輸入,y坐標作為輸出。有關每個步驟的詳細信息,請參見代碼注釋:
import torch
import plotly.graph_objects as go
import numpy as np
# Batch Size, Input Neurons, Hidden Neurons, Output Neurons
N, D_in, H, D_out = 16, 1, 1024, 1
# Create random Tensors to hold inputs and outputs
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
# Use the nn package to define our model
# Linear (Input -> Hidden), ReLU (Non-linearity), Linear (Hidden-> Output)
model = torch.nn.Sequential(
torch.nn.Linear(D_in, H),
torch.nn.ReLU(),
torch.nn.Linear(H, D_out),
)
# Define the loss function: Mean Squared Error
# The sum of the squares of the differences between prediction and ground truth
loss_fn = torch.nn.MSELoss(reduction='sum')
# The optimizer does a lot of the work of actually calculating gradients and
# Applying backpropagation through the network to update weights
learning_rate = 1e-4
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
# Perform 30000 training steps
for t in range(30000):
# Forward pass: compute predicted y by passing x to the model.
y_pred = model(x)
# Compute loss and print it periodically
loss = loss_fn(y_pred, y)
if t % 100 == 0:
print(t, loss.item())
# Update the network weights using gradient of the loss
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Draw the original random points as a scatter plot
fig = go.Figure()
fig.add_trace(go.Scatter(x=x.flatten().numpy(), y=y.flatten().numpy(), mode="markers"))
# Generate predictions for evenly spaced x-values between minx and maxx
minx = min(list(x.numpy()))
maxx = max(list(x.numpy()))
c = torch.from_numpy(np.linspace(minx, maxx, num=640)).reshape(-1, 1).float()
d = model(c)
# Draw the predicted functions as a line graph
fig.add_trace(go.Scatter(x=c.flatten().numpy(), y=d.flatten().detach().numpy(), mode="lines"))
fig.show()
請注意右邊的兩點,即模型沒有完全擬合。我們可以通過運行更多的訓練步驟或增加隱藏神經元的數量來解決這個問題。
案例二:二值分類
函數不一定是在代數中看到的那種"一個數進去,另一個數出來"的函數。現在讓我們嘗試一個二進制分類任務。數據點有兩個特征,可以分為兩個標簽中的一個。也許這兩個特征是經緯度坐標,而標簽是環境污染物的存在。或者,這些特征可能是學生的數學和閱讀測試成績,并且標簽對應于他們是右撇子還是左撇子。重要的是模型必須實現兩個輸入到一個輸出(0或1)的映射。
下面的代碼與前面的代碼非常相似。唯一的差異是輸入層現在有兩個神經元,輸出層之后是一個Sigmoid激活,它將所有輸出壓縮到范圍(0,1)。
import torch
import plotly.express as px
import pandas as pd
# Batch Size, Input Neurons, Hidden Neurons, Output Neurons
N, D_in, H, D_out = 128, 2, 1024, 1
# Create random Tensors to hold inputs and outputs
x = torch.rand(N, D_in)
y = torch.randint(0, 2, (N, D_out))
# Plot randomly generated points and color by label
df = pd.DataFrame({"x": x[:, 0].flatten(), "y": x[:, 1].flatten(), "class": y.flatten()})
fig = px.scatter(df, x="x", y="y", color="class", color_continuous_scale="tealrose")
fig.show()
# define model: Linear (Input->Hidden), ReLU, Linear (Hidden->Output), Sigmoid
model = torch.nn.Sequential(
torch.nn.Linear(D_in, H),
torch.nn.ReLU(),
torch.nn.Linear(H, D_out),
torch.nn.Sigmoid()
)
# define loss function: Binary Cross Entropy Loss (good for binary classification tasks)
loss_fn = torch.nn.BCELoss()
learning_rate = 0.002
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
# Store losses over time
ts, losses = ([], [])
# run training steps
for t in range(60000):
y_pred = model(x)
loss = loss_fn(y_pred.float(), y.float())
if t % 100 == 0:
ts.append(t)
losses.append(loss.data.numpy())
optimizer.zero_grad()
loss.backward()
optimizer.step()
# generate a bunch of random points to cover the sample space, then call model
c = torch.rand(32000, D_in)
d = model(c)
# store random data and predicted classifications in a DataFrame and plot with Plotly Express
df2 = pd.DataFrame({"x": c[:, 0].flatten(),
"y": c[:, 1].flatten(),
"class": d.flatten().detach().numpy()})
fig2 = px.scatter(df2, x="x", y="y", color="class", color_continuous_scale="tealrose")
fig2.show()
# plot the loss as a function of training step
fig3 = px.scatter(x=ts, y=losses)
fig3.show()
在單位正方形中隨機均勻生成的點,隨機指定給標簽0(青色)和標簽1(粉紅色)。
首先,在單位正方形內隨機均勻生成數據點,并且隨機指點每個數據點的標簽為0/1。從圖中可以看出,顯然不存在線性關系。本案例的目的在于訓練模型使其通過坐標判斷標簽。
模型分類結果
過擬合
以上兩個案例似乎都給出了很可觀的結果,但是這是不是我們真正想要的呢?值得注意的是,這兩個案例都存在過擬合的現象。過擬合表現為模型在訓練數據集表現優秀,但是在未知數據集表現不足。
在案例一中,假設其中一個點是由于錯誤的數據收集而導致的異常值。考慮到要學習的訓練數據量如此之少,模型對這些數據的擬合度過高,只看到了一個信號,而實際上只是噪聲。一方面,令人印象深刻的是,模型能夠學習一個考慮到這個異常值的函數。另一方面,當將此模型應用于真實世界的數據時,這可能會導致不良結果,在該點附近產生錯誤的預測。
在案例二中,模型學習了一個漂亮的分類預測。但是,請注意最靠近右下角的藍綠色點。盡管這是唯一的一點,它導致模型將整個右下角標記為青色。僅僅是一些錯誤的數據點就可能嚴重扭曲模型。當我們嘗試將模型應用于測試數據時,它的工作效果可能比預期的差得多。
為了避免過度擬合,重要的是要有大量的訓練數據來代表模型預期面對的樣本。如果你正在建立一個工具來預測普通人群的衣服尺寸,不要只從你大學朋友那里收集訓練數據。此外,還有一些先進的技術可以別用于幫助減少過擬合的發生(例如:權重下降 weight decay)。
結語
總之,神經網絡是強大的機器學習工具,因為它們(理論上)能夠學習任何函數。然而,這并不能保證你很容易找到一個給定問題的最優權重!實際上,在合理的時間內訓練一個精確的模型取決于許多因素,例如優化器、模型體系結構、數據質量等等。特別是,深度學習涉及具有多個隱藏層的神經網絡,它們非常擅長學習某些困難的任務。
作者:Thomas Hikaru Clark
deephub翻譯組 Oliver Lee