传统神经网络在处理序列数据时存在根本性缺陷。想象你正在阅读一本小说——每个单词的含义都依赖于前文的语境。这种时序依赖关系正是RNN的用武之地。我在处理股票预测项目时发现,简单的前馈网络根本无法捕捉价格波动的时序模式,而RNN通过其内部状态(hidden state)实现了对历史信息的记忆。
具体来说,RNN的独特之处在于:
注意:虽然理论上RNN可以记住任意长度的历史信息,但实际训练中会面临梯度消失问题。这也是为什么LSTM和GRU后来被提出。
一个标准RNN单元的计算过程可以用以下公式表示:
python复制h_t = tanh(W_hh * h_{t-1} + W_xh * x_t + b_h)
y_t = W_hy * h_t + b_y
其中:
h_t是当前时间步的隐藏状态x_t是当前输入W_*是可训练权重矩阵b_*是偏置项我在实现第一个RNN时犯过的典型错误:
BPTT是理解RNN训练的关键。与传统反向传播不同,BPTT需要考虑时间维度上的梯度流动。具体步骤:
实际训练中的技巧:
truncated BPTT处理长序列LSTM通过三个门控机制解决了梯度消失问题:
python复制# 遗忘门
f_t = σ(W_f · [h_{t-1}, x_t] + b_f)
# 输入门
i_t = σ(W_i · [h_{t-1}, x_t] + b_i)
# 输出门
o_t = σ(W_o · [h_{t-1}, x_t] + b_o)
我在文本生成项目中对比发现:
GRU将LSTM的三个门简化为两个:
python复制# 更新门
z_t = σ(W_z · [h_{t-1}, x_t] + b_z)
# 重置门
r_t = σ(W_r · [h_{t-1}, x_t] + b_r)
实际应用建议:
python复制class SimpleRNN:
def __init__(self, input_size, hidden_size):
self.Wxh = np.random.randn(hidden_size, input_size)*0.01
self.Whh = np.random.randn(hidden_size, hidden_size)*0.01
self.Why = np.random.randn(input_size, hidden_size)*0.01
self.bh = np.zeros((hidden_size, 1))
self.by = np.zeros((input_size, 1))
def forward(self, inputs):
h = np.zeros((self.Whh.shape[0], 1))
self.last_inputs = inputs
self.last_hs = {0: h}
for i, x in enumerate(inputs):
h = np.tanh(np.dot(self.Wxh, x) + np.dot(self.Whh, h) + self.bh)
self.last_hs[i+1] = h
y = np.dot(self.Why, h) + self.by
return y, h
python复制# 标准化
scaler = MinMaxScaler(feature_range=(-1, 1))
scaled_data = scaler.fit_transform(data)
# 创建序列数据
def create_sequences(data, seq_length):
sequences = []
for i in range(len(data)-seq_length):
seq = data[i:i+seq_length]
label = data[i+seq_length]
sequences.append((seq, label))
return sequences
python复制class StockPredictor(nn.Module):
def __init__(self, input_size=1, hidden_size=50, num_layers=2):
super().__init__()
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
self.linear = nn.Linear(hidden_size, 1)
def forward(self, x):
out, _ = self.lstm(x)
out = self.linear(out[:, -1, :])
return out
使用PyTorch的pack_padded_sequence:
python复制lengths = [len(seq) for seq in sequences]
packed = pack_padded_sequence(padded, lengths, batch_first=True)
python复制nn.LSTM(..., bidirectional=True)
python复制# 计算attention权重
scores = torch.matmul(query, key.transpose(-2, -1))
weights = F.softmax(scores, dim=-1)
# 加权求和
output = torch.matmul(weights, value)
重要提示:RNN对初始化非常敏感,建议使用Xavier初始化。我在一个项目中因为忽视这点导致训练完全失败。
最后分享一个调参技巧:当验证损失波动较大时,尝试减小batch size并增加梯度裁剪阈值。这个经验帮我节省了至少20小时的调试时间。