25、股票数据建模与神经网络分析

股票数据建模与神经网络分析

1. 股票价格检查

在处理文本数据后,我们将注意力转向股票价格。之前,我们使用相关函数从 CSV 文件中加载股票数据。现在,我们重新加载数据:

user=> (def stock (csvd/read-stock-prices "d/d-1995-2001.csv"))
user=> (count stock)
1263

我们可以通过图表查看多年来收盘价的变化情况。价格最初在 30 美元出头,有一定波动,最终收于 20 美元出头。期间,有一些价格快速攀升的时期,我们希望能够捕捉并预测这些变化。

2. 合并文本和股票特征

在训练神经网络之前,我们需要确定如何表示数据以及神经网络需要哪些信息。具体来说,对于每篇文档,我们要基于之前的股票价格和文档中的标记,预测未来股票价格的走势。
- 特征选择
- 一组特征是文档中的标记,我们之前已经识别出来。
- 其他特征表示股票价格。由于我们关注未来价格的走势,因此可以使用过去某一时刻和文章发布当天的股票价格差异。我们不确定哪些时间范围会有帮助,所以选择多个时间范围并全部包含。
- 输出 :输出是另一个股票价格差异。同样,我们不确定在什么时间差异下能得到好的结果,所以尝试在不同的未来时间距离进行观察。
- 时间范围 :我们使用一些标准的时间周期,逐渐扩大范围:1 天、2 天、3 天、4 天、5 天、2 周、3 周、1 个月、2 个月、6 个月和 1 年。周末的日期采用前一个工作日的值,1 个月按 30 天计算,1 年按 365 天计算。在 Clojure 中,我们可以使用 clj-time 库来表示这些周期:

(def periods [(time/days 1)
              (time/days 2)
              (time/days 3)
              (time/days 4)
              (time/days 5)
              (time/days (* 7 2))
              (time/days (* 7 3))
              (time/days 30)
              (time/days (* 30 2))
              (time/days (* 30 6))
              (time/days 365)])
  • 特征提取函数
    • index-by 函数用于将集合索引为一个映射:
(defn index-by [key-fn coll]
  (into {} (map #(vector (key-fn %) %) coll)))
- `get-stock-date` 函数尝试从索引中获取 `StockData` 实例,如果找不到则尝试前一天,若追溯到 1990 年之前则返回 `nil`:
(defn get-stock-date [stock-index date]
  (if-let [price (stock-index date)]
    price
    (if (<= (time/year date) 1990)
      nil
      (get-stock-date
        stock-index (time/minus date (time/days 1))))))
  • 向量生成函数
    • make-price-vector 函数根据文章发布日期和股票索引生成价格向量:
(defn make-price-vector [stock-index article date-op]
  (let [pub-date (:pub-date article)
        base-price (:close (get-stock-date stock-index pub-date))
        price-feature
        (fn [period]
          (let [date-key (date-op pub-date period)]
            (if-let [stock (get-stock-date stock-index date-key)]
              (/ (price-diff base-price (:close stock))
                 base-price)
              0.0)))]
    (vec (remove nil? (map price-feature periods)))))
- `make-feature-vector` 函数生成输入特征向量,包含标记向量和价格向量:
(defn make-feature-vector [stock-index vocab article]
  (let [freqs (:text article)
        token-features (map #(freqs % 0.0) (sort vocab))
        price-features (make-price-vector
                         stock-index article time/minus)]
    (vec (concat token-features price-features))))
- `make-training-vector` 函数生成训练向量,用于获取未来股票价格:
(defn make-training-vector [stock-index article]
  (vec (make-price-vector stock-index article time/plus)))
- `make-training-set` 函数生成训练集,返回包含输入向量和预期输出向量的哈希映射:
(defn make-training-set [stock-index vocab articles]
  (let [make-pair
        (fn [article]
          {:input (make-feature-vector stock-index vocab article)
           :outputs (zipmap periods
                            (make-training-vector
                              stock-index article))})]
    (map make-pair articles)))
3. 神经网络分析文本和股票特征

现在我们已经准备好所有数据,接下来尝试训练一个人工神经网络来学习输入数据的未来价格变化方向,即创建一个基于过去价格变化和文章文本的简单二元分类器。

3.1 理解神经网络

人工神经网络是模仿人类大脑中神经元的结构和行为的机器学习结构。我们使用最古老和最常见的三层前馈网络。
- 网络结构 :网络有三层,即输入层、隐藏层和输出层。每层由一个或多个神经元组成,每个神经元接收一个或多个输入并产生一个输出,输入会被加权,所有输入相加后通过激活函数进行归一化和缩放。
- 前馈激活过程
1. 输入向量被输入到网络的输入层,根据网络设置,可能会通过每个神经元的激活函数,确定每个神经元的激活程度。
2. 输入层和隐藏层之间的加权连接被激活,通过输入神经元与进入每个隐藏节点的权重的点积来激发隐藏层节点,这些值再通过隐藏神经元的激活函数。
3. 前向传播过程在隐藏层和输出层之间再次重复。
4. 输出层神经元的激活值就是网络的输出。
- 权重训练 :初始时,权重通常是随机选择的。然后使用各种技术训练权重,常见的是反向传播算法,通过计算输出神经元和期望输出之间的误差,将误差反馈到网络中,调整权重使网络输出更接近目标。但要注意避免网络“记住”训练集,导致对新输入的泛化能力差。

以下是前馈激活过程的 mermaid 流程图:

graph LR
    A[输入向量] --> B[输入层]
    B --> C{是否通过激活函数}
    C -- 是 --> D[确定神经元激活程度]
    C -- 否 --> D
    D --> E[加权连接]
    E --> F[隐藏层]
    F --> G[隐藏层激活函数]
    G --> H[前向传播到输出层]
    H --> I[输出层激活函数]
    I --> J[网络输出]
3.2 设置神经网络

我们使用 Encog 机器学习框架和 Clojure 包装库 Enclog 来实现神经网络。
- 创建神经网络 make-network 函数根据词汇表大小和隐藏节点数量创建神经网络:

(defn make-network [vocab-size hidden-nodes]
  (nnets/network (nnets/neural-pattern :feed-forward)
                 :activation :sigmoid
                 :input (+ vocab-size (count periods))
                 :hidden [hidden-nodes]
                 :output 1))
  • 输出处理 activated 函数将预期输出通过与网络输出相同的激活函数,以便直接比较:
(defn activated [act-fn output]
  (let [a (double-array 1 [output]))]
    (.activationFunction act-fn a 0 1)
    a)
  • 数据准备 build-data 函数将训练集数据转换为 Encog 可以处理的数据结构:
(defn build-data [nnet period training-set]
  (let [act (.getActivation nnet (dec (.getLayerCount nnet)))
        output (mapv #(activated act (get (:outputs %) period))
                     training-set)]
    (training/data :basic-dataset
                   (mapv :input training-set)
                   output)))
3.3 训练神经网络

创建的神经网络初始权重是随机的,需要立即进行训练。

(defn train-for
  ([nnet period training-set]
    (train-for nnet period training-set 0.01 500 []))
  ([nnet period training-set error-tolerance
    iterations strategies]
    (let [data (build-data nnet period training-set)
          trainer (training/trainer :back-prop
                                    :network nnet
                                    :training-set data)]
      (training/train
        trainer error-tolerance iterations strategies)
      nnet)))

为了方便验证网络,我们可以将创建和训练网络合并为一个函数:

(defn make-train [vocab-size hidden-count period coll]
  (let [nn (make-network vocab-size hidden-count)]
    (train-for nn period coll 0.01 100 [])
    nn))
3.4 运行神经网络

训练好的网络可以对新输入进行预测:

(defn run-network [nnet input]
  (let [input (double-array (count input) input)
        output (double-array (.getOutputCount nnet))]
    (.compute nnet input output)
    output))

我们可以用两种方式使用这个函数:
- 传入未知输出的数据,查看网络如何分类。
- 传入已知输出的输入数据,评估网络对未见过数据的性能。

3.5 验证神经网络

test-on 函数用于计算网络在测试集上的平方误差之和(SSE):

(defn test-on [nnet period test-set]
  "Runs the net on the test set and calculates the SSE."
  (let [act (.getActivation nnet (dec (.getLayerCount nnet)))
        sqr (fn [x] (* x x))
        error (fn [{:keys [input outputs]}]
                (- (first (activated act (get outputs period)))
                   (first (run-network nnet input))))]
    (reduce + 0.0 (map sqr (map error test-set)))))

为了更好地评估网络性能,我们可以使用 K 折交叉验证。例如,当 K = 4 时,将训练输入分为四组 A、B、C 和 D,我们将训练四个不同的分类器:
| 测试集 | 训练集 |
| ---- | ---- |
| A | B、C、D 合并 |
| B | A、C、D |
| C | A、B、D |
| D | A、B、C |

通过平均 test-on 返回的误差,我们可以更好地了解网络在这些参数下的性能。

股票数据建模与神经网络分析(续)

4. 深入分析 K 折交叉验证

K 折交叉验证是一种非常有效的评估机器学习模型性能的方法。在股票价格预测的场景中,它能帮助我们更准确地了解神经网络在不同参数设置下的表现。

4.1 K 折交叉验证的原理

K 折交叉验证的核心思想是将数据集分成 K 个大小相等的子集。然后进行 K 次训练和测试的循环,每次循环中,选取其中一个子集作为测试集,其余 K - 1 个子集合并作为训练集。通过多次这样的操作,我们可以得到多个不同训练和测试组合下的模型性能指标,最后将这些指标进行平均,得到一个更稳定、更具代表性的性能评估结果。

以下是 K 折交叉验证的 mermaid 流程图:

graph LR
    A[数据集] --> B[分成 K 个子集]
    B --> C{循环 K 次}
    C -- 是 --> D[选择一个子集作为测试集]
    D --> E[其余子集合并作为训练集]
    E --> F[训练模型]
    F --> G[在测试集上测试模型]
    G --> H[记录性能指标]
    H --> C
    C -- 否 --> I[平均性能指标]
4.2 在股票价格预测中的应用

在股票价格预测中,我们可以使用 K 折交叉验证来评估神经网络在不同时间周期下的性能。具体步骤如下:
1. 划分数据集 :将包含股票数据和新闻文章的数据集按照时间顺序或随机方式分成 K 个大小相等的子集。
2. 循环训练和测试 :进行 K 次循环,每次循环中:
- 选择一个子集作为测试集,其余 K - 1 个子集合并作为训练集。
- 使用训练集训练神经网络,使用 make-train 函数创建并训练网络。
- 使用 test-on 函数在测试集上测试网络,计算平方误差之和(SSE)。
3. 计算平均性能 :将 K 次循环得到的 SSE 进行平均,得到一个更稳定的性能评估指标。

以下是一个简单的示例代码,展示如何实现 K 折交叉验证:

(defn k-fold-cross-validation [k vocab-size hidden-count period data]
  (let [subset-size (int (/ (count data) k))
        folds (partition-all subset-size data)]
    (letfn [(train-and-test [test-fold]
              (let [train-data (apply concat (remove #(= % test-fold) folds))
                    test-data test-fold
                    nn (make-train vocab-size hidden-count period train-data)]
                (test-on nn period test-data)))]
      (/ (reduce + (map train-and-test folds)) k))))
5. 优化神经网络参数

在使用神经网络进行股票价格预测时,选择合适的参数非常重要。参数的选择会直接影响网络的性能和泛化能力。以下是一些需要考虑的参数及其优化方法。

5.1 隐藏节点数量

隐藏节点数量是神经网络中一个非常重要的参数。隐藏节点数量过少,网络可能无法学习到数据中的复杂模式,导致欠拟合;隐藏节点数量过多,网络可能会过度拟合训练数据,对新数据的泛化能力变差。

我们可以通过实验的方法来选择合适的隐藏节点数量。例如,我们可以尝试不同的隐藏节点数量,使用 K 折交叉验证评估每个设置下的网络性能,选择性能最好的隐藏节点数量。

以下是一个简单的示例代码,展示如何尝试不同的隐藏节点数量:

(defn find-best-hidden-nodes [vocab-size periods data]
  (let [hidden-node-options [5 10 15 20 25]
        k 4]
    (letfn [(evaluate-hidden-nodes [hidden-nodes]
              (k-fold-cross-validation k vocab-size hidden-nodes periods data))]
      (let [errors (map evaluate-hidden-nodes hidden-node-options)]
        (nth hidden-node-options (argmin errors))))))
5.2 时间周期选择

在前面的分析中,我们选择了多个时间周期来提取股票价格特征。不同的时间周期可能对预测结果有不同的影响。我们可以通过实验的方法来选择最有效的时间周期。

例如,我们可以尝试不同的时间周期组合,使用 K 折交叉验证评估每个组合下的网络性能,选择性能最好的时间周期组合。

以下是一个简单的示例代码,展示如何尝试不同的时间周期组合:

(defn find-best-periods [vocab-size hidden-nodes data]
  (let [period-options [[(time/days 1) (time/days 2) (time/days 3)]
                        [(time/days 1) (time/days 5) (time/days 10)]
                        [(time/days 1) (time/days 3) (time/days 7)]]
        k 4]
    (letfn [(evaluate-periods [periods]
              (k-fold-cross-validation k vocab-size hidden-nodes periods data))]
      (let [errors (map evaluate-periods period-options)]
        (nth period-options (argmin errors))))))
6. 总结与展望

通过对股票数据和新闻文章的建模,以及使用神经网络进行分析,我们可以尝试预测股票价格的未来走势。在这个过程中,我们使用了多种技术和方法,包括数据预处理、特征提取、神经网络的创建和训练、以及 K 折交叉验证等。

然而,股票市场是一个非常复杂和不确定的系统,受到多种因素的影响,包括宏观经济环境、政策变化、公司业绩等。因此,我们的预测结果可能存在一定的误差。为了提高预测的准确性,我们可以考虑以下几点:
1. 增加数据维度 :除了股票价格和新闻文章,我们可以考虑引入更多的相关数据,如宏观经济指标、行业数据等,以提供更丰富的信息。
2. 改进模型结构 :尝试使用更复杂的神经网络结构,如循环神经网络(RNN)、长短期记忆网络(LSTM)等,以更好地处理时间序列数据。
3. 结合其他模型 :可以将神经网络与其他机器学习模型或传统的统计方法相结合,发挥各自的优势,提高预测性能。

总之,股票价格预测是一个具有挑战性但又非常有意义的研究领域。通过不断地探索和改进,我们有望提高预测的准确性,为投资者提供更有价值的决策支持。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值