一、前饋神經網路
前饋神經網路(Feedforward Neural Networks, FNN)是人工神經網路中的一種,它的資訊流動是單向的,從輸入層到隱藏層,再到輸入層,沒有反向的連線。其中,隱藏層可以有多個,用於處理輸入層的資料,且每一個隱藏層通常配合一個非線性的啟用函式來進行訓練。
前饋神經網路的架構如下:
我們透過構建上圖的網路模型來進行我們今天的氣溫預測任務。
首先,我們需要去下載對應的資料集,我們可以透過下面的網址下載到世界上基本所有國家的歷史以及實時氣溫資料:https://rp5.ru
以廣州(機場)
氣象站的氣溫資料為例,下載 2005.2.1 - 2023.12.31 期間的資料作為訓練集,2024.1.1-2024.11.02 期間的資料作為驗證集,最後預測 2024.11.03 的溫度。
在上述網址下載了資料後,對資料進行簡單地手工處理,結果如下:
下載完資料集後,我們就可以開始搭建網路進行訓練了!
二、搭建模型並訓練
1 匯入資料並對資料進行預處理
我們首先在程式碼中假如如下語句,如有 GPU 則用 GPU 訓練,這樣能大大提高訓練的速度:
# 如有 GPU 則用 GPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
然後我們利用 pandas 庫中的函式 read_excel
對 excel
中的資料進行讀取:
# 匯入訓練集和驗證集
train_file_name = './data/FNN/temperature_history.xls'
valid_file_name = './data/FNN/temperature_2024.xls'
train_features = pd.read_excel(train_file_name, skiprows=6) # 跳過前 6 行無效資料
valid_features = pd.read_excel(valid_file_name, skiprows=6)
由於我們下載的 excel
資料中含有年份時間資料,我們要將其拆分為月、日、時,並捨棄年份,併入到原來的資料中:
# 定義一個函式來拆分日期和時間
def split_datetime(date_str):
date_part, time_part = date_str.split(' ')
date_parts = date_part.split('.')
time_parts = time_part.split(':')
return pd.Series([ int(date_parts[1]), int(date_parts[0]), int(time_parts[0])])
# 拆分日期和時間,但不保留年份
train_features[['月', '日', '時']] = train_features['時間'].apply(split_datetime)
valid_features[['月', '日', '時']] = valid_features['時間'].apply(split_datetime)
# 刪除原始的 '時間' 列
train_features.drop('時間', axis=1, inplace=True)
valid_features.drop('時間', axis=1, inplace=True)
檢視處理後的資料我們會發現,其中有一些行的特徵是空值,我們需要將其剔除,以免影響訓練效果:
# 刪除包含 NaN 的行(若有特徵不存在則刪除改行)
train_features.dropna(inplace=True)
valid_features.dropna(inplace=True)
然後,我們透過 print
語句來列印一下,看看我們處理後的資料:
接下來,我們需要將標籤
從資料中剝離出來,並將資料格式轉換為 ndarry
(即 numpy 格式):
# 從 features 中取出標籤列並轉換為 numpy 資料格式
train_labels = np.array(train_features['溫度'], dtype=np.float32).reshape(-1, 1)
valid_labels = np.array(valid_features['溫度'], dtype=np.float32).reshape(-1, 1)
# 從 features 中剔除 ‘溫度' 標籤列
train_features = train_features.drop('溫度', axis=1)
valid_features = valid_features.drop('溫度', axis=1)
# 將 features 轉化為 numpy 資料格式
train_features = np.array(train_features, dtype=np.float32)
valid_features = np.array(valid_features, dtype=np.float32)
在將資料傳入到 GPU 訓練之前,我們還有一步重要的操作要做,那就是對資料進行標準化操作(以原點為中心對稱,縮小取值範圍和維度差異)。這是由於,在人工神經網路中,模型並不知道我們傳入的特徵哪些是重要的,哪些是不重要的,模型會潛在的認為特徵值越大就表示該特徵越重要,從而會把它所謂的重要的特徵學的越大,而實際上,不同維度的取值範圍可能會差異很大,所以不同維度的取值範圍並不具備參考意義,為了降低模型的誤解,需要對資料進行標準化操作:
input_train_features = preprocessing.StandardScaler().fit_transform(train_features)
input_valid_features = preprocessing.StandardScaler().fit_transform(valid_features)
然後我們就可以將 ndarry
格式的資料轉為 tensor
格式,傳入到 GPU 或 CPU 中:
# 將輸入資料從 ndarray 轉換成 tensor 並傳入 GPU 或 CPU
train_inputs = torch.tensor(input_train_features, dtype=torch.float32).to(device)
valid_inputs = torch.tensor(input_valid_features, dtype=torch.float32).to(device)
train_labels = torch.tensor(train_labels, dtype=torch.float32).to(device)
valid_labels = torch.tensor(valid_labels, dtype=torch.float32).to(device)
2 構建模型
首先我們按照開篇給的網路模型來進行構建:
# 定義模型結構
class FNN(nn.Module):
def __init__(self): # 定義模型的建構函式
super(FNN, self).__init__() # 呼叫父類的建構函式
self.linear1 = nn.Linear(13, 50) # 輸入層到隱藏層1
self.linear2 = nn.Linear(50, 100) # 隱藏層1到隱藏層2
self.linear3 = nn.Linear(100, 1) # 隱藏層2到輸出層
self.relu = nn.ReLU() # 非線性啟用函式
def forward(self, x): # 定義模型的前向傳播函式
x = self.relu(self.linear1(x)) # 透過第一個隱藏層並啟用
x = self.relu(self.linear2(x)) # 透過第二個隱藏層並啟用
x = self.linear3(x) # 透過輸出層
return x
在這個模型中,我們定義了兩個隱藏層(全連線層),每層後面加入了一個非線性啟用函式,最後透過輸出層進行輸出,得到一個預測值。定義好模型結構後,我們將模型進行例項化,並輸入到 GPU 或 CPU:
# 初始化模型並把模型輸入到 GPU 或 CPU
model = FNN().to(device)
3 設定模型的超引數
接下來我們需要為模型設定超引數,分別是迭代次數
、學習率
、最佳化器
和損失函式
:
epochs = 50000 # 迭代次數
learning_rate = 0.0001 # 學習率
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate) # 最佳化器
criterion = nn.MSELoss() # 損失函式(均方誤差)
4 訓練模型並儲存引數
設定好超引數之後,我們就可以進行模型的訓練了:
# 訓練模型
for epoch in range(epochs):
epoch += 1
# 每次迭代之前將梯度清零
optimizer.zero_grad()
# 進行前向傳播
train_outputs = model(train_inputs)
# 計算損失
train_loss = criterion(train_outputs, train_labels)
# 反向傳播
train_loss.backward()
# 更新權重引數
optimizer.step()
# 每 1000 個 epoch 列印一次損失,跑一次驗證集
if epoch % 1000 == 0:
# 列印損失
print('epoch: {0}, train loss: {1}'.format(epoch, train_loss.item()))
# 在驗證集上評估模型
model.eval() # 將模型設定為評估模式
with torch.no_grad(): # 關閉梯度計算
# 將驗證集輸入模型,並計算損失
valid_outputs = model(valid_inputs)
valid_loss = criterion(valid_outputs, valid_labels)
# 計算準確率
correct = (torch.abs(valid_outputs - valid_labels) < 1.5).sum().item() # 溫度誤差上下 1.5° 為準確
total = valid_labels.size(0)
accuracy = correct / total * 100
print('epoch: {0}, valid loss: {1}, accuracy: {2:.4f}%\n'.format(epoch, valid_loss.item(), accuracy))
model.train() # 將模型轉回訓練模式
在訓練過程中,我們設定每隔 1000 次就去驗證集上跑一下損失和準確率,當驗證集上跑出的預測溫度和實際溫度誤差在 1.5° 以內,我們就定義為預測正確,然後計算出正確率。
當模型訓練完後,我們需要將模型訓練好的引數進行儲存:
# 訓練好後對模型引數進行儲存
torch.save(model.state_dict(), './data/FNN/model.pk1')
5 載入模型和訓練好的引數用於預測
我們的模型引數訓練完畢後已經儲存到了本地,之後就可以隨時利用本地已經訓練好的引數來進行實際溫度的預測:
# 如有 GPU 則用 GPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 載入模型引數
pred_model = FNN().to(device) # 載入模型到 GPU 或 CPU
pred_model.load_state_dict(torch.load('./data/FNN/model.pk1')) # 載入模型引數
pred_model.eval() # 將模型設定為評估模式
# 建立單個資料的輸入張量,並新增批次維度,輸入資料特徵對應如下:
# Po(氣象站水平大氣壓)、P(海平面大氣壓)、Pa(氣壓趨勢)、U(相對溼度)、Ff(風速)
# ff3(5) 、Tn(最低氣溫)、Tx(最高氣溫)、VV(水平能見度)、Td(露點溫度)、月、日、時
# 當日上午 8 點的實際溫度為:22.3
single_data = [757.7, 764.0, 1.0, 69, 2, 4, 19.7, 27.5, 30, 16.3, 11, 3, 8]
actual_temp = 22.3
# 將 list 資料格式轉為 numpy 資料格式,並將資料從一維提升到二維
single_data = np.array(single_data, dtype=np.float32).reshape(1, -1)
# 對資料進行標準化處理
single_data = preprocessing.StandardScaler().fit_transform(single_data)
# 將 numpy 資料格式轉為 tensor 資料格式並傳入 GPU 或 CPU
inputs = torch.tensor(single_data, dtype=torch.float32).to(device)
# 進行預測
with torch.no_grad():
prediction = pred_model(inputs)
# 輸出預測結果
print('2024-11-03 8:00 的實際溫度為:{},模型預測溫度為:{}'.format(actual_temp, prediction.item()))
輸出的預測結果如下:
可以從上圖看到,我們將學習率設定為 0.0001,並迭代了 50000 次後,能夠在驗證集上跑出 84.6398% 的準確率,並且我們載入訓練好的引數,對 2024-11-03 8:00 時的氣溫進行了預測,和實際溫度基本一致,說明模型訓練的效果還算可以。