循環神經網絡(RNN)是一種專門用于處理序列數據的神經網絡架構。與處理空間數據的卷積神經網絡(Conv2D)不同,RNN通過引入循環連接使網絡具有"記憶"能力,能夠利用之前的信息來影響當前的輸出,非常適合處理音頻波形、頻譜。
一、RNN介紹
1.1 結構
- 輸入層:序列數據,通常為形狀為
(batch_size, seq_len, input_size)
的張量
示例:音頻頻譜處理:將音頻轉換為頻譜圖后,seq_len對應時間幀數,input_size對應每個時間幀的頻率維度(如梅爾頻帶數),梅爾頻譜圖特征形狀為 (batch_size, 128, 100) 表示:
-128個時間幀(seq_len=128)
-每個時間幀有100個梅爾頻帶特征(input_size=100)原始波形處理:直接處理音頻波形時,seq_len對應采樣點數,input_size對應特征維度(如單聲道為1,立體聲為2),16kHz音頻的2秒片段形狀為 (batch_size, 32000, 1) 表示:
-32000個采樣點(seq_len=32000)
-單聲道音頻(input_size=1)
-
RNN層
- 循環單元:包含一個或多個RNN單元,每個單元包含以下可學習參數:
- 輸入到隱藏的權重:WxhW_{xh}Wxh?,形狀:
(hidden_size, input_size)
- 隱藏到隱藏的權重:WhhW_{hh}Whh?,形狀:
(hidden_size, hidden_size)
- 偏置:bhb_hbh?,形狀:
(hidden_size,)
- 輸入到隱藏的權重:WxhW_{xh}Wxh?,形狀:
- 隱藏狀態:hth_tht?,形狀:
(batch_size, hidden_size)
:存儲網絡的狀態信息,在時間步之間傳遞
- 循環單元:包含一個或多個RNN單元,每個單元包含以下可學習參數:
-
激活函數:
- 隱藏層激活:通常使用
Tanh
或ReLU
;tanh
將輸出壓縮到[-1,1]范圍,有助于緩解梯度爆炸;ReLU
計算高效,但可能導致梯度消失。 - 輸出層激活:根據任務選擇(Softmax用于分類,線性激活用于回歸)
- 隱藏層激活:通常使用
1.2 參數
- input_size:每個時間步輸入的特征數量。對于音頻頻譜,通常是頻率維度(如梅爾頻帶數)
- hidden_size:隱藏狀態的維度,決定RNN的記憶容量和表征能力
- num_layers:堆疊的RNN層數,增加層數可提高模型復雜度但也會增加計算量
- nonlinearity:激活函數選擇,
Tanh
適合大多數情況,RELU
在某些場景可能表現更好 - bias:是否在計算中添加可學習的偏置項
- batch_first:輸入張量的維度順序。
True: (batch, seq, feature)
,False: (seq, batch, feature)
- dropout:在多層RNN中應用dropout防止過擬合,0表示不使用dropout
- bidirectional:是否使用雙向RNN,True時會同時考慮前向和后向序列信息
1.3 輸入輸出維度
- 輸入數據維度:
(batch_size, seq_len, input_size)
(當batch_first=True
時) - 輸出序列維度:
(batch_size, seq_len, hidden_size * num_directions)
(當batch_first=True
時) - 最終隱藏狀態:
(num_layers * num_directions, batch_size, hidden_size)
1.4 計算過程
ht=tanh?(Wxh×xt+Whh×ht?1+bh)h_t = \tanh(W_{xh} \times x_t + W_{hh} \times h_{t-1} + b_h)ht?=tanh(Wxh?×xt?+Whh?×ht?1?+bh?)
其中:
- hth_tht?:當前時間步的隱藏狀態(也是該時間步的輸出)
- ht?1h_{t-1}ht?1?:上一時間步的隱藏狀態
- xtx_txt?:當前時間步的輸入
- WxhW_{xh}Wxh?:輸入到隱藏的權重矩陣
- WhhW_{hh}Whh?:隱藏到隱藏的權重矩陣
- bhb_hbh?:偏置項
- tanh?\tanhtanh:激活函數
對于多層RNN(num_layers > 1):
ht(l)=tanh?(Wxh(l)ht(l?1)+Whh(l)ht?1(l)+bh(l))h_t^{(l)} = \tanh(W_{xh}^{(l)} h_t^{(l-1)} + W_{hh}^{(l)} h_{t-1}^{(l)} + b_h^{(l)})ht(l)?=tanh(Wxh(l)?ht(l?1)?+Whh(l)?ht?1(l)?+bh(l)?)
其中
ht(0)=xth_t^{(0)} = x_tht(0)?=xt?
1.5 計算過程可視化
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.patches import Circle, Arrow# 創建畫布
fig, ax = plt.subplots(figsize=(12, 6))
ax.set_xlim(0, 10)
ax.set_ylim(0, 5)
ax.axis('off')
plt.title('RNN Computation Process', fontsize=16, pad=20)# 顏色定義
input_color = '#FFD700' # 金色
hidden_color = '#1E90FF' # 道奇藍
active_color = '#FF4500' # 橙紅色
arrow_color = '#8B0000' # 深紅色# 初始化節點
time_steps = 3
input_nodes = []
hidden_nodes = []
input_texts = []
hidden_texts = []# 初始隱藏狀態
h_init = Circle((0.5, 2.5), 0.3, facecolor='lightgray', edgecolor='black')
ax.add_patch(h_init)
ax.text(0.5, 2.5, 'h_{-1}', ha='center', va='center', fontsize=10)# 創建節點
for t in range(time_steps):# 輸入節點input_circle = Circle((2.5 + t * 2.5, 4), 0.3, facecolor=input_color, edgecolor='black', alpha=0.7)ax.add_patch(input_circle)input_nodes.append(input_circle)input_text = ax.text(2.5 + t * 2.5, 4, f'x_{t}', ha='center', va='center', fontsize=10)input_texts.append(input_text)# 隱藏狀態節點hidden_circle = Circle((2.5 + t * 2.5, 2.5), 0.3, facecolor=hidden_color, edgecolor='black', alpha=0.7)ax.add_patch(hidden_circle)hidden_nodes.append(hidden_circle)hidden_text = ax.text(2.5 + t * 2.5, 2.5, f'h_{t}', ha='center', va='center', fontsize=10)hidden_texts.append(hidden_text)# 時間步標簽ax.text(2.5 + t * 2.5, 4.8, f'Time Step {t}', ha='center', fontsize=10)# 繪制連接線
arrows = []
arrow_labels = []# 輸入到隱藏的連接
for t in range(time_steps):arrow = Arrow(2.5 + t * 2.5, 3.7, 0, -0.9, width=0.1, color='gray', alpha=0.3)ax.add_patch(arrow)arrows.append(arrow)label = ax.text(2.7 + t * 2.5, 3.2, '$W_{xh}$', fontsize=10, alpha=0.3)arrow_labels.append(label)# 隱藏到隱藏的連接
for t in range(time_steps):if t == 0:# 初始隱藏狀態到第一個隱藏狀態arrow = Arrow(0.8, 2.5, 1.5, 0, width=0.1, color='gray', alpha=0.3)ax.add_patch(arrow)arrows.append(arrow)label = ax.text(1.5, 2.7, '$W_{hh}$', fontsize=10, alpha=0.3)arrow_labels.append(label)else:# 隱藏狀態之間的連接arrow = Arrow(2.5 + (t - 1) * 2.5, 2.5, 2.5, 0, width=0.1, color='gray', alpha=0.3)ax.add_patch(arrow)arrows.append(arrow)label = ax.text(2.5 + (t - 1) * 2.5 + 1.25, 2.7, '$W_{hh}$', fontsize=10, alpha=0.3)arrow_labels.append(label)# 添加公式
formula_text = ax.text(5, 1, '', fontsize=14, ha='center')# 動畫更新函數
def update(frame):# 重置所有顏色for node in input_nodes + hidden_nodes:node.set_alpha(0.7)if node.get_facecolor() != active_color:node.set_facecolor(input_color if node in input_nodes else hidden_color)for arrow in arrows:arrow.set_alpha(0.3)arrow.set_color('gray')for label in arrow_labels:label.set_alpha(0.3)# 根據幀數更新if frame == 0:# 初始狀態formula_text.set_text('Initialization: $h_{-1} = 0$')h_init.set_facecolor(active_color)h_init.set_alpha(1.0)elif frame <= time_steps:t = frame - 1# 激活當前輸入input_nodes[t].set_facecolor(active_color)input_nodes[t].set_alpha(1.0)# 激活輸入到隱藏的連接arrows[t].set_alpha(1.0)arrows[t].set_color(arrow_color)arrow_labels[t].set_alpha(1.0)# 激活隱藏狀態hidden_nodes[t].set_facecolor(active_color)hidden_nodes[t].set_alpha(1.0)# 激活隱藏到隱藏的連接if t == 0:arrows[time_steps].set_alpha(1.0)arrows[time_steps].set_color(arrow_color)arrow_labels[time_steps].set_alpha(1.0)else:arrows[time_steps + t].set_alpha(1.0)arrows[time_steps + t].set_color(arrow_color)arrow_labels[time_steps + t].set_alpha(1.0)# 顯示公式formula_text.set_text(f'Compute $h_{t}$: $h_{t} = \\tanh(W_{{xh}} x_{t} + W_{{hh}} h_{t - 1} + b_h)$')return input_nodes + hidden_nodes + arrows + arrow_labels + [formula_text, h_init]# 創建動畫
animation = FuncAnimation(fig, update, frames=range(time_steps + 1),interval=1500, blit=True)plt.tight_layout()
animation.save('rnn_core_animation.gif', writer='pillow', fps=1, dpi=100)
plt.show()
二、代碼示例
通過兩層RNN處理一段音頻頻譜,打印每層的輸出形狀、參數形狀,并可視化特征圖。
import torch
import torch.nn as nn
import matplotlib.pyplot as plt
import librosa
import numpy as np# 定義 RNN 模型
class RNNModel(nn.Module):def __init__(self, input_size):super(RNNModel, self).__init__()self.rnn1 = nn.RNN(input_size, 128, batch_first=True)self.rnn2 = nn.RNN(128, 64, batch_first=True)def forward(self, x):h_out1, _ = self.rnn1(x)h_out2, _ = self.rnn2(h_out1)return h_out1, h_out2 # 返回兩層的輸出# 讀取音頻文件并處理
file_path = 'test.wav'
waveform, sample_rate = librosa.load(file_path, sr=16000, mono=True)# 選取 3 秒的數據
start_sample = int(1.5 * sample_rate)
end_sample = int(4.5 * sample_rate)
audio_segment = waveform[start_sample:end_sample]# 轉換為頻譜
n_fft = 512
hop_length = 256
spectrogram = librosa.stft(audio_segment, n_fft=n_fft, hop_length=hop_length)
spectrogram_db = librosa.amplitude_to_db(np.abs(spectrogram))
spectrogram_tensor = torch.tensor(spectrogram_db, dtype=torch.float32).unsqueeze(0)
spectrogram_tensor = spectrogram_tensor.permute(0, 2, 1) # 調整為 (batch_size, seq_len, input_size)
print(f"Spectrogram tensor shape: {spectrogram_tensor.shape}")# 創建 RNN 模型實例
input_size = spectrogram_tensor.shape[2]
model = RNNModel(input_size)# 前向傳播
rnn_output1, rnn_output2 = model(spectrogram_tensor)
print(f"RNN Layer 1 output shape: {rnn_output1.shape}")
print(f"RNN Layer 2 output shape: {rnn_output2.shape}")# 打印每層的參數形狀
print(f"RNN Layer 1 weights shape: {model.rnn1.weight_ih_l0.shape}")
print(f"RNN Layer 1 hidden weights shape: {model.rnn1.weight_hh_l0.shape}")
print(f"RNN Layer 1 bias shape: {model.rnn1.bias_ih_l0.shape}")print(f"RNN Layer 2 weights shape: {model.rnn2.weight_ih_l0.shape}")
print(f"RNN Layer 2 hidden weights shape: {model.rnn2.weight_hh_l0.shape}")
print(f"RNN Layer 2 bias shape: {model.rnn2.bias_ih_l0.shape}")# 可視化原始頻譜
plt.figure(figsize=(10, 4))
plt.imshow(spectrogram_db, aspect='auto', origin='lower', cmap='inferno')
plt.title("Original Spectrogram")
plt.xlabel("Time Frames")
plt.ylabel("Frequency Bins")
plt.colorbar(format='%+2.0f dB')
plt.tight_layout()# 可視化 RNN 輸出的特征圖
plt.figure(figsize=(10, 4))# 繪制第一層 RNN 輸出的特征圖
plt.subplot(2, 1, 1)
plt.imshow(rnn_output1[0].detach().numpy().T, aspect='auto', origin='lower', cmap='inferno') # 轉置
plt.title("RNN Layer 1 Output Feature Map")
plt.xlabel("Time Steps")
plt.ylabel("Hidden State Dimensions")
plt.colorbar(label='Hidden State Value')# 繪制第二層 RNN 輸出的特征圖
plt.subplot(2, 1, 2)
plt.imshow(rnn_output2[0].detach().numpy().T, aspect='auto', origin='lower', cmap='inferno') # 轉置
plt.title("RNN Layer 2 Output Feature Map")
plt.xlabel("Time Steps")
plt.ylabel("Hidden State Dimensions")
plt.colorbar(label='Hidden State Value')plt.tight_layout()
plt.show()
Spectrogram tensor shape: torch.Size([1, 188, 257])
RNN Layer 1 output shape: torch.Size([1, 188, 128])
RNN Layer 2 output shape: torch.Size([1, 188, 64])
RNN Layer 1 weights shape: torch.Size([128, 257])
RNN Layer 1 hidden weights shape: torch.Size([128, 128])
RNN Layer 1 bias shape: torch.Size([128])
RNN Layer 2 weights shape: torch.Size([64, 128])
RNN Layer 2 hidden weights shape: torch.Size([64, 64])
RNN Layer 2 bias shape: torch.Size([64])
三、RNN的梯度消失與長期依賴問題
循環神經網絡(RNN)在處理序列數據時面臨兩個核心問題:梯度消失問題和長期依賴問題。這些問題的根源在于RNN的結構和訓練機制。
3.1 梯度消失問題
RNN通過時間反向傳播(BPTT)算法進行訓練,梯度需要沿著時間步反向傳播。當序列較長時,梯度在反向傳播過程中會指數級地減小或增大。
考慮RNN的計算公式:
ht=tanh?(Wxhxt+Whhht?1+bh)h_t = \tanh(W_{xh}x_t + W_{hh}h_{t-1} + b_h)ht?=tanh(Wxh?xt?+Whh?ht?1?+bh?)
在反向傳播時,需要計算損失函數LLL對參數WhhW_{hh}Whh?的梯度:
?L?Whh=∑t=1T?L?hT?hT?ht?ht?Whh\frac{\partial L}{\partial W_{hh}} = \sum_{t=1}^T \frac{\partial L}{\partial h_T} \frac{\partial h_T}{\partial h_t} \frac{\partial h_t}{\partial W_{hh}}?Whh??L?=t=1∑T??hT??L??ht??hT???Whh??ht??
關鍵項是?hT?ht\frac{\partial h_T}{\partial h_t}?ht??hT??,它可以通過鏈式法則展開:
?hT?ht=∏k=tT?1?hk+1?hk=∏k=tT?1Whh??diag(tanh?′(zk))\frac{\partial h_T}{\partial h_t} = \prod_{k=t}^{T-1} \frac{\partial h_{k+1}}{\partial h_k} = \prod_{k=t}^{T-1} W_{hh}^\top \cdot \text{diag}(\tanh'(z_k))?ht??hT??=k=t∏T?1??hk??hk+1??=k=t∏T?1?Whh???diag(tanh′(zk?))
其中
zk=Wxhxk+Whhhk?1+bhz_k = W_{xh}x_k + W_{hh}h_{k-1} + b_hzk?=Wxh?xk?+Whh?hk?1?+bh?
由于tanh?\tanhtanh的導數tanh?′(z)=1?tanh?2(z)\tanh'(z) = 1 - \tanh^2(z)tanh′(z)=1?tanh2(z)的值域為(0,1](0, 1](0,1],且WhhW_{hh}Whh?通常初始化為小隨機數,這個連乘積會指數級衰減:
∣∏k=tT?1?hk+1?hk∣≤∣Whh∣T?t?(max?tanh?′)T?t\left| \prod_{k=t}^{T-1} \frac{\partial h_{k+1}}{\partial h_k} \right| \leq \left| W_{hh} \right|^{T-t} \cdot (\max \tanh')^{T-t}?k=t∏T?1??hk??hk+1???≤∣Whh?∣T?t?(maxtanh′)T?t
當T?tT-tT?t很大時,這個值趨近于0,導致早期時間步的梯度消失。
影響
- 早期時間步的參數無法有效更新:網絡難以學習長序列中早期時間步的重要信息
- 訓練過程緩慢且不穩定:梯度太小導致參數更新幅度極小
- 模型無法捕捉長期模式:只能記住短期信息,難以學習長序列中的依賴關系
3.2 長期依賴問題
即使沒有梯度消失問題,RNN也難以有效利用序列中相距較遠的信息。這是因為隱藏狀態的表示能力有限,信息在多次變換中逐漸"稀釋"。
考慮信息從時間步t傳遞到時間步T的過程:
hT=f(hT?1,xT)=f(f(hT?2,xT?1),xT)=?=f(?f(ht,xt+1)??,xT)h_T = f(h_{T-1}, x_T) = f(f(h_{T-2}, x_{T-1}), x_T) = \cdots = f(\cdots f(h_t, x_{t+1}) \cdots, x_T)hT?=f(hT?1?,xT?)=f(f(hT?2?,xT?1?),xT?)=?=f(?f(ht?,xt+1?)?,xT?)
每次變換fff都會對信息進行非線性轉換和壓縮,經過多次變換后,早期信息hth_tht?對hTh_ThT?的影響變得微弱且難以區分。
示例在語言建模中,考慮句子:"The clouds in the sky are [...] color." 要預測最后一個詞"blue",需要記住開頭的"clouds"信息。標準RNN很難保持這種長距離依賴。
3.3 梯度爆炸問題
與梯度消失相反,當權重矩陣WhhW_{hh}Whh?的特征值大于 1 時,梯度在反向傳播過程中會指數級增長。這種現象被稱為梯度爆炸。
在 RNN 的反向傳播過程中,梯度的計算可以表示為:
∣∏k=tT?1?hk+1?hk∣≤∣Whh∣T?t\left| \prod_{k=t}^{T-1} \frac{\partial h_{k+1}}{\partial h_k} \right| \leq \left| W_{hh} \right|^{T-t}?k=t∏T?1??hk??hk+1???≤∣Whh?∣T?t
如果∥Whh∥>1\| W_{hh} \| > 1∥Whh?∥>1,則梯度的范數會指數增長,導致以下問題:
- 參數更新過大:由于梯度過大,參數更新可能會超出合理范圍,導致模型無法收斂。
- 訓練不穩定:模型的訓練過程可能變得不穩定,導致損失函數波動較大。
- 可能產生 NaN 值:在極端情況下,過大的參數更新可能導致數值溢出,從而產生 NaN 值。
為了解決梯度爆炸問題,可以采取以下措施:
- 梯度裁剪:通過限制梯度的大小,確保參數更新不會過大。常用的方法是將梯度的 L2 范數限制在一個預設的閾值之內。例如,如果梯度的范數超過閾值,則按比例縮放梯度。
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)
- 權重正則化:通過約束權重矩陣的范數,防止權重過大。常見的正則化方法包括 L2 正則化(權重衰減)和 L1 正則化。