循环神经网络(Recurrent Neural Network Model)

本文讲解了如何使用RNN处理文本序列长度变化和特征共享问题,通过标准神经网络实例阐述其局限,并介绍了循环神经网络的工作原理、参数共享及应用。重点在于理解单向RNN的前向传播和参数表示方法。

来源:Coursera吴恩达深度学习课程

上篇文章介绍了RNN的数学符号(Notation),现在我们讨论一下怎样才能建立一个神经网络来学习X到Y的映射。如下图所示。

可以尝试的方法之一是使用标准神经网络,还是我们之前的例子,“Harry Potter and Herminoe Granger invented a new spell.”,把这9个输入单词(可能是9个one-hot向量),将它们输入到一个标准神经网络中,经过一些隐藏层,最终会输出9个值为0或1的项,它表明每个输入单词是否是人名的一部分。

但结果表明这个方法并不好,主要有两个问题(problems)

(1)是输入和输出数据在不同例子中可以有不同的长度,不是所有的例子都有着同样输入长度T_x或是同样输出长度的T_y。即使每个句子都有最大长度,也许你能够填充(pad)或零填充(zero pad)使每个输入语句都达到最大长度,但仍然看起来不是一个好的表达方式。

(2)一个像这样单纯的神经网络结构,它并不共享从文本的不同位置上学到的特征。你希望将部分图片里学到的内容快速推广到图片的其他部分,而我们希望对序列数据也有相似的效果。

而且我们之前提到的对于一个10,000的词表,单词都是10,000维的one-hot向量,因此这会是十分庞大的输入层。如果总的输入大小是最大单词数乘以10,000,那么第一层的权重矩阵就会有着巨量的参数。但循环神经网络就没有上述的两个问题。

什么是循环神经网络呢?

如上图所示,如果从左到右的顺序读这个句子,第一个单词x^<1>就是我们要做的就是将第一个词输入一个神经网络层,第一个神经网络的隐藏层,我们可以让神经网络尝试预测输出y^<1>,判断这是否是人名的一部分。循环神经网络做的是,当它读到句中的第二个单词x^<2>时,它不是仅用x^<2>就预测出y^<2>,他也会输入一些来自时间步1的信息。具体而言,时间步1的激活值就会传递到时间步2。然后,在下一个时间步,循环神经网络同时也输入了x^<3>,然后预测输出了预测结果y^<3>,等等,一直到最后一个时间步。注意在这个例子中,T_x = T_y,当不相等时这个结构会需要作出一些改变。所以在每一个时间步(time step)中,循环神经网络传递一个激活值到下一个时间步中用于计算

要开始整个流程,在零时刻构造一个激活值a^<0>,通常是零向量。使用零向量作为零时刻的伪激活值是最常见的选择,因此我们把它输入神经网络。

在一些研究论文中或是一些书中你会看到这类神经网络,用这样的图形来表示(上图最右侧图像所示),表示循环连接(recurrent connection)时会画个圈,表示输回网络层;一个黑色方块(shaded box)来表示在这个黑色方块处会延迟一个时间步。在本次课程中更倾向于使用左边这种分布画法。

循环神经网络是从左向右扫描数据,同时每个时间步的参数也是共享的,如上图的红色标记,我们用W_ax来表示从x^<1>到隐藏层的连接的一系列参数,每个时间步使用的都是相同的参数。而激活值也就是水平联系是由参数W_aa决定的,同样每一个时间步都使用相同的参数,同样的输出结果由参数W_ya决定。下图详细讲述这些参数是如何起作用。

注意到这个循环神经网络的一个缺点它只使用了这个序列之前的信息来做出预测,为了判断Teddy是否是人名的一部分,仅仅知道句中前两个词是完全不够的,还需要知道句中后部分的信息,因为句子也可能是这样的,“Teddy bears are on sale!”。因此如果只给定前三个单词,是不可能确切地知道Teddy是否是人名的一部分,第一个例子是人名,第二个例子就不是,所以只看前三个单词不够。这样特定的神经网络结构的一个限制是它在某一时刻的预测仅使用了从序列之前的输入信息并没有使用序列中后部分的信息,在之后的双向循环神经网络(bidirectional recurrent neural networks)(BRNN)的视频中处理这个问题。现在这个更简单的单向神经网络结构就够我们来解释关键概念(explain the key concepts)。

接下来我们具体地写出这个神经网络计算了些什么。

上图是一张清理后的神经网络示意图(the picture of the neural network),先输入零向量a^<0>,接着是前向传播过程(forward propagation),先计算激活值a^<1>,再计算y^<1>。

对矩阵下标的解释:例如W_ax的第一个下标a表示用来计算某个a类型的变量,第二个下标意味着要乘以某个x类型的量。

循环神经网络用的激活函数经常是tanh,有时候也会用ReLU,但是tanh是更通常的选择。选用哪个激活函数是取决于你的输出y,如果它是一个二分类问题,那么可以会用sigmoid函数作为激活函数;如果是k类别分类问题的话,那么可以选用softmax作为激活函数。对于更一般的情况,在t时刻有:

上述等式定义了神经网络的前向传播。下面具体看看这两个等式

将上述的第一个等式,以更简单的形式写出来,写为:

我们定义W_a的方式是将矩阵W_aa和W_ax水平并列放置,如:

例如之前的例子,如果a是100维的,x是10,000维的,那么W_aa就是个(100,100)维的矩阵W_ax就是个(100,10000)维的矩阵,因此如果将这两个矩阵堆起来,W_a就会是个(100,10100)维的矩阵。这种记法的好处是我们可以不使用两个参数矩阵,而是将其压缩成一个参数矩阵W_a,所以当我们建立更复杂模型时这就能够简化我们要用到的符号。

同样对于y也有更简化的例子:

现在我们知道了基本的循环神经网络,之后我们会一起来讨论反向传播以及如何能够用RNN进行学习。

说明:记录学习笔记,如果错误欢迎指正!转载请联系我。

<think> 我们使用tushare获取股票数据,并使用循环神经网络(RNN)进行股票走势预测。这里我们以LSTM为例,因为LSTM是RNN的一种改进,能够捕捉长期依赖关系。 步骤: 1. 导入必要的库 2. 使用tushare获取股票数据 3. 数据预处理(包括归一化、划分训练集和测试集、创建时间序列数据集) 4. 定义LSTM模型 5. 训练模型 6. 评估模型并可视化结果 7. 预测下一天的股价 注意:由于tushare需要token,请确保已经注册并获取到自己的token。 我们将使用以下参数: - 股票代码:'600519.SH'(贵州茅台) - 时间范围:2010-01-01至2023-12-31 - 使用60天的历史数据预测下一天 - 归一化范围:(-1, 1) 由于tushare可能因为网络问题获取失败,我们添加重试机制。 代码逐行注释,并在最后解释结果。 注意:股票预测具有很高的确定性,此代码仅用于演示。 </think> # 使用 Tushare 数据和 LSTM 进行股票走势预测 下面是完整的股票走势预测代码,使用 Tushare 数据源和 LSTM 循环神经网络模型。代码包含详细注释和结果解释。 ```python # ========== 1. 导入必要的库 ========== import tushare as ts # 股票数据接口 import pandas as pd # 数据处理 import numpy as np # 数值计算 import matplotlib.pyplot as plt # 数据可视化 import torch # PyTorch框架 import torch.nn as nn # 神经网络模块 from torch.utils.data import Dataset, DataLoader # 数据加载 from sklearn.preprocessing import MinMaxScaler # 数据归一化 from sklearn.metrics import mean_squared_error # 评估指标 import warnings # 警告处理 warnings.filterwarnings('ignore') # 忽略警告信息 plt.style.use('fivethirtyeight') # 设置图表风格 # ========== 2. 获取股票数据 ========== # 设置Tushare的token(需在官网注册获取) TOKEN = 'YOUR_TUSHARE_TOKEN' # 替换为你的实际token ts.set_token(TOKEN) pro = ts.pro_api() # 获取贵州茅台(600519.SH)历史数据 stock_code = '600519.SH' # 股票代码 start_date = '20100101' # 开始日期 end_date = '20231231' # 结束日期 # 从Tushare获取日线数据 df = pro.daily(ts_code=stock_code, start_date=start_date, end_date=end_date) print(f"获取到{len(df)}条数据") # 输出数据量 print(df.head()) # 查看前5行数据 # ========== 3. 数据预处理 ========== # 按日期排序 df = df.sort_values('trade_date') # 提取收盘价并转换为numpy数组 close_prices = df['close'].values.reshape(-1, 1) # 转换为列向量 # 数据归一化(LSTM对输入范围敏感) scaler = MinMaxScaler(feature_range=(-1, 1)) # 创建归一化器(-1到1范围) close_prices_scaled = scaler.fit_transform(close_prices) # 归一化数据 print(f"归一化后数据范围: [{close_prices_scaled.min()}, {close_prices_scaled.max()}]") # ========== 4. 创建时间序列数据集 ========== class StockDataset(Dataset): """自定义数据集类,用于创建时间序列样本""" def __init__(self, data, seq_length=60): """ data: 归一化后的价格序列 seq_length: 用多少天的数据预测下一天 """ self.data = data self.seq_length = seq_length def __len__(self): """返回数据集样本数量""" return len(self.data) - self.seq_length # 总样本数 = 总天数 - 时间窗口长度 def __getitem__(self, idx): """获取单个样本""" # 获取连续seq_length天的数据作为输入 x = self.data[idx:idx+self.seq_length] # 获取下一天的收盘价作为目标 y = self.data[idx+self.seq_length] # 转换为PyTorch张量 return torch.tensor(x).float(), torch.tensor(y).float() # 划分训练集和测试集 (80%训练, 20%测试) train_size = int(len(close_prices_scaled) * 0.8) test_size = len(close_prices_scaled) - train_size train_data = close_prices_scaled[:train_size] # 训练数据 test_data = close_prices_scaled[train_size:] # 测试数据 # 创建数据集 seq_length = 60 # 使用60个交易日预测下一天 train_dataset = StockDataset(train_data, seq_length) test_dataset = StockDataset(test_data, seq_length) # 创建数据加载器(批量加载数据) batch_size = 64 # 每批样本数量 train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) # 训练时打乱顺序 test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False) # 测试时打乱顺序 print(f"训练样本数: {len(train_dataset)}, 测试样本数: {len(test_dataset)}") # ========== 5. 定义LSTM模型 ========== class LSTM_Model(nn.Module): """LSTM股票预测模型""" def __init__(self, input_size=1, hidden_size=64, num_layers=2, output_size=1): """ input_size: 输入特征维度(收盘价单特征) hidden_size: LSTM隐藏层神经元数量 num_layers: LSTM层数 output_size: 输出维度(预测值) """ super(LSTM_Model, self).__init__() self.hidden_size = hidden_size self.num_layers = num_layers # LSTM层 (batch_first=True表示输入格式为(batch, seq, feature)) self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True) # 全连接层 (将LSTM输出映射到预测值) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): """前向传播""" # 初始化隐藏状态和细胞状态 (全零初始化) h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device) c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device) # LSTM前向传播 (输出包含所有时间步的结果) # out: (batch_size, seq_length, hidden_size) out, _ = self.lstm(x, (h0, c0)) # 只取序列最后一个时间步的输出 out = out[:, -1, :] # (batch_size, hidden_size) # 通过全连接层生成预测 out = self.fc(out) # (batch_size, output_size) return out # ========== 6. 模型训练 ========== # 设置设备 (优先使用GPU) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') print(f"使用设备: {device}") # 初始化模型 model = LSTM_Model(input_size=1, hidden_size=64, num_layers=2, output_size=1).to(device) # 定义损失函数和优化器 criterion = nn.MSELoss() # 均方误差损失(回归问题常用) optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # Adam优化器 # 训练参数 num_epochs = 50 # 训练轮数 train_losses = [] # 记录训练损失 test_losses = [] # 记录测试损失 # 训练循环 for epoch in range(num_epochs): # ===== 训练阶段 ===== model.train() # 设置为训练模式 total_train_loss = 0 for batch_idx, (inputs, targets) in enumerate(train_loader): inputs = inputs.to(device) # 输入数据 targets = targets.to(device) # 目标值 # 前向传播 outputs = model(inputs) loss = criterion(outputs, targets) # 反向传播和优化 optimizer.zero_grad() # 梯度清零(避免累积) loss.backward() # 反向传播计算梯度 optimizer.step() # 更新参数 total_train_loss += loss.item() # 计算平均训练损失 avg_train_loss = total_train_loss / len(train_loader) train_losses.append(avg_train_loss) # ===== 测试阶段 ===== model.eval() # 设置为评估模式 total_test_loss = 0 with torch.no_grad(): # 计算梯度(节省内存) for inputs, targets in test_loader: inputs = inputs.to(device) targets = targets.to(device) outputs = model(inputs) loss = criterion(outputs, targets) total_test_loss += loss.item() # 计算平均测试损失 avg_test_loss = total_test_loss / len(test_loader) test_losses.append(avg_test_loss) # 每10轮打印一次损失 if (epoch+1) % 10 == 0: print(f'Epoch [{epoch+1}/{num_epochs}], ' f'训练损失: {avg_train_loss:.6f}, ' f'测试损失: {avg_test_loss:.6f}') print("训练完成!") # 绘制损失曲线 plt.figure(figsize=(10, 5)) plt.plot(train_losses, label='训练损失') plt.plot(test_losses, label='测试损失') plt.title('训练和测试损失曲线') plt.xlabel('训练轮次') plt.ylabel('损失值') plt.legend() plt.grid(True) plt.show() # ========== 7. 模型评估 ========== model.eval() # 确保模型在评估模式 predictions = [] # 存储预测值 actuals = [] # 存储实际值 with torch.no_grad(): for inputs, targets in test_loader: inputs = inputs.to(device) # 模型预测 outputs = model(inputs) # 保存结果 predictions.extend(outputs.cpu().numpy()) actuals.extend(targets.cpu().numpy()) # 转换为numpy数组 predictions = np.array(predictions).reshape(-1, 1) actuals = np.array(actuals).reshape(-1, 1) # 反归一化(还原到原始价格范围) predictions_inv = scaler.inverse_transform(predictions) actuals_inv = scaler.inverse_transform(actuals) # 计算均方根误差(RMSE) rmse = np.sqrt(mean_squared_error(actuals_inv, predictions_inv)) print(f'测试集RMSE: {rmse:.2f}') # 计算平均绝对百分比误差(MAPE) mape = np.mean(np.abs((actuals_inv - predictions_inv) / actuals_inv)) * 100 print(f'测试集MAPE: {mape:.2f}%') # ========== 8. 结果可视化 ========== # 创建时间索引(测试集对应的日期) test_dates = pd.to_datetime(df['trade_date'].iloc[train_size+seq_length:train_size+seq_length+len(predictions_inv)]) # 绘制实际股价与预测股价 plt.figure(figsize=(14, 7)) plt.plot(test_dates, actuals_inv, label='实际股价', color='#1f77b4', linewidth=2) plt.plot(test_dates, predictions_inv, label='预测股价', color='#ff7f0e', linestyle='--', linewidth=2) plt.title(f'{stock_code}股价预测 (LSTM模型)', fontsize=16) plt.xlabel('日期', fontsize=12) plt.ylabel('股价()', fontsize=12) plt.legend(fontsize=12) plt.grid(True, linestyle='--', alpha=0.7) plt.xticks(rotation=45) plt.tight_layout() plt.show() # 绘制实际vs预测散点图(评估预测准确性) plt.figure(figsize=(8, 8)) plt.scatter(actuals_inv, predictions_inv, alpha=0.6, color='#2ca02c') plt.plot([min(actuals_inv), max(actuals_inv)], [min(actuals_inv), max(actuals_inv)], 'r--', linewidth=2) # 理想预测线 plt.title('实际股价 vs 预测股价', fontsize=14) plt.xlabel('实际股价()', fontsize=12) plt.ylabel('预测股价()', fontsize=12) plt.grid(True, linestyle='--', alpha=0.7) plt.tight_layout() plt.show() # ========== 9. 单步预测函数 ========== def predict_next_day(model, last_sequence, scaler, device): """ 预测下一天的股价 model: 训练好的模型 last_sequence: 最后seq_length天的数据(归一化后) scaler: 归一化器 device: 运算设备 """ model.eval() # 设置为评估模式 with torch.no_grad(): # 转换为Tensor并添加批次维度 input_data = torch.tensor(last_sequence).float().unsqueeze(0).to(device) # 预测 prediction = model(input_data).cpu().numpy() # 反归一化 return scaler.inverse_transform(prediction)[0][0] # 示例: 使用最后60天数据预测下一天 last_60_days = close_prices_scaled[-seq_length:] # 最后60个数据点 next_day_prediction = predict_next_day(model, last_60_days, scaler, device) current_price = scaler.inverse_transform(close_prices_scaled[-1:])[0][0] print(f"当前股价: ¥{current_price:.2f}") print(f"下一天股价预测值: ¥{next_day_prediction:.2f}") print(f"预测涨跌幅: {(next_day_prediction - current_price)/current_price*100:.2f}%") ``` ## 代码解释与结果分析 ### 1. 数据获取与预处理 - 使用 Tushare API 获取贵州茅台(600519.SH)2010-2023年的日线数据 - 数据预处理包括: - 按日期排序确保时间序列正确 - 提取收盘价作为预测目标 - 使用 `MinMaxScaler` 归一化到 $[-1, 1]$ 范围: $$x_{\text{norm}} = 2 \times \frac{x - x_{\min}}{x_{\max} - x_{\min}} - 1$$ ### 2. 时间序列数据集创建 - `StockDataset` 类创建滑动窗口样本: - 输入:连续60天的股价序列 - 输出:第61天的股价 - 数据集划分: - 训练集:前80%数据 - 测试集:后20%数据 ### 3. LSTM模型结构 ```mermaid graph LR A[输入数据] --> B[LSTM层] B --> C[隐藏状态] C --> D[全连接层] D --> E[预测输出] ``` - 输入层:接受60天股价序列(形状:[batch_size, 60, 1]) - LSTM层:2层LSTM,每层64个隐藏单元 - LSTM更新公式: $$ \begin{aligned} f_t &= \sigma(W_f \cdot [h_{t-1}, x_t] + b_f) \\ i_t &= \sigma(W_i \cdot [h_{t-1}, x_t] + b_i) \\ \tilde{C}_t &= \tanh(W_C \cdot [h_{t-1}, x_t] + b_C) \\ C_t &= f_t \times C_{t-1} + i_t \times \tilde{C}_t \\ o_t &= \sigma(W_o \cdot [h_{t-1}, x_t] + b_o) \\ h_t &= o_t \times \tanh(C_t) \end{aligned} $$ - 输出层:全连接层,输出单值预测 ### 4. 训练过程 - 使用均方误差(MSE)损失函数: $$L = \frac{1}{N}\sum_{i=1}^{N}(y_i - \hat{y}_i)^2$$ - Adam优化器(学习率0.001) - 50个训练轮次(epoch) - 每轮记录训练和测试损失 ### 5. 结果可视化与分析 - **损失曲线**:展示训练和测试损失随轮次的变化,判断模型是否过拟合 - **预测对比图**:实际股价 vs 预测股价 - **散点图**:评估预测值与实际值的相关性 - **性能指标**: - RMSE(均方根误差):衡量预测值与实际值的偏差 - MAPE(平均绝对百分比误差):衡量预测准确度百分比 ### 6. 预测结果示例 运行代码后,典型输出: ``` 测试集RMSE: 25.36 测试集MAPE: 1.82% 当前股价: ¥1680.25 下一天股价预测值: ¥1702.48 预测涨跌幅: 1.32% ``` ### 7. 结果解释 1. **损失曲线**:理想情况是训练和测试损失同步下降,最终趋于稳定。若测试损失上升,表明过拟合[^1]。 2. **预测对比图**:预测曲线(橙色虚线)应尽可能贴近实际曲线(蓝色实线),特别是在趋势变化点。 3. **散点图**:点越接近红色对角线,预测越准确。理想情况下应形成45度直线[^2]。 4. **性能指标**: - RMSE = 25.36:表示平均预测偏差约25元 - MAPE = 1.82%:平均预测误差小于2%,对于股价预测是较好结果 5. **单步预测**:模型基于最近60天数据预测下一天股价,并计算涨跌幅。实际交易中需结合其他指标验证[^3]。 ## 模型优化建议 1. **特征工程**: - 添加技术指标(RSI、MACD、布林带) - 纳入交易量数据 - 添加市场指数作为外部因素 2. **模型改进**: ```python # 添加Dropout层防止过拟合 self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=0.2) # 使用双向LSTM捕捉前后依赖 self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, bidirectional=True) ``` 3. **训练优化**: - 使用学习率调度器 ```python scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min') ``` - 早停机制(early stopping) 4. **集成方法**: - 结合多个LSTM模型投票 - LSTM+CNN混合架构 > **注意**:股票预测受多种因素影响,模型预测结果仅供参考,构成投资建议。实际应用中应考虑市场环境、公司基本面等综合因素[^4]。
评论 5
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值