引言

使用大语言模型或其输出结果来完成各类机器学习驱动的任务,包括一些在语言模型出现前就已被解决的传统预测任务,已逐渐成为一种趋势。这自然引出了时间序列预测领域的一个问题:利用大语言模型,例如将大语言模型生成的嵌入作为额外的工程特征,是否真的有助于提升时间序列预测模型的性能?

本文将通过一个鼓励批判性思考的、循序渐进的实践示例,来探讨这个关于大语言模型在预测未来等任务中实用性的紧迫问题。

利用LLM嵌入改进金融时间序列预测:实践演练

首先,我们为本示例导入必要的模块和库。在这个示例中,我们将创建两个用于训练预测模型的数据集版本:一个仅包含时间序列相关的特征,另一个则额外包含了从另一个不同(但存在一定因果关联)数据集中衍生出的、由大语言模型生成的嵌入特征。

使用的两个数据集是:

  • 道琼斯工业平均指数:30家美国大型公司(道指成分股)的每日调整后收盘价。我们将使用 yfinance 库获取此数据集。
  • Combined_News_DJIA 新闻标题:每日前25条财经新闻标题,大部分与道指成分股公司相关。
import pandas as pd
import numpy as np
import yfinance as yf
from sentence_transformers import SentenceTransformer
from sklearn.decomposition import PCA
from sklearn.metrics import accuracy_score
from lightgbm import LGBMClassifier

接下来的代码片段加载第一个数据集,并应用简单的特征工程来添加滞后特征和滚动统计量——这是时间序列预处理中的常见做法,旨在为下游预测任务更好地捕捉有意义的信号模式:

ticker = "^DJI"
df_price = yf.download(ticker, start="2008-01-01", end="2016-12-31")

df_price = df_price[["Close"]]
df_price["return"] = df_price["Close"].pct_change()
df_price["target"] = (df_price["return"].shift(-1) > 0).astype(int)

df_price = df_price.dropna()
df_price.head()

# 添加滞后特征和滚动平均特征
for lag in [1, 2, 3, 5]:
    df_price[f"lag_{lag}"] = df_price["return"].shift(lag)

df_price["roll_mean_5"] = df_price["return"].rolling(5).mean()
df_price["roll_std_5"] = df_price["return"].rolling(5).std()

df_price = df_price.dropna()

请注意这些工程特征名称中使用的前缀,我们稍后将再次引用它们。

接着,加载新闻标题数据集,并将每日的标题合并到一个单一的文本列中。这是一个为了说明问题而故意简化的方法,实际中可以做得更复杂——例如,更严格地筛选与道指相关的标题内容。

df_news = pd.read_csv(
    "https://raw.githubusercontent.com/gakudo-ai/open-datasets/refs/heads/main/Combined_News_DJIA.csv"
)

df_news.columns = df_news.columns.str.strip()
df_news["Date"] = pd.to_datetime(df_news["Date"], dayfirst=True)

headline_cols = [c for c in df_news.columns if c.startswith("Top")]

df_news[headline_cols] = df_news[headline_cols].fillna("")

df_news["combined"] = df_news[headline_cols].apply(
    lambda row: " ".join([str(x) for x in row if str(x).strip() != ""]),
    axis=1
)

df_news["combined"].head()

下面的辅助函数并非绝对必要,但对于去除因数据集中某些字符串最初为字节编码而产生的奇怪字符很有用:

import ast

def clean_bytes_text(x):
    """
    将类似字节的字符串(b'...')转换为普通文本
    """
    if isinstance(x, str):
        try:
            evaluated_value = ast.literal_eval(x)
            if isinstance(evaluated_value, bytes):
                return evaluated_value.decode('utf-8')
            else:
                return str(evaluated_value)
        except (ValueError, SyntaxError):
            if x.startswith("b"):
                return x.strip("b'\"")
            return x
    return x

df_news["combined"] = df_news["combined"].apply(clean_bytes_text)

现在我们使用一个预训练的句子转换器模型来生成嵌入,将“combined”列中合并的新闻标题作为输入:

model = SentenceTransformer("all-MiniLM-L6-v2")

embeddings = model.encode(
    df_news["combined"].tolist(),
    show_progress_bar=True
)

为了降低过拟合风险并限制嵌入空间的维度,我们应用主成分分析:

pca = PCA(n_components=20)
embeddings_reduced = pca.fit_transform(embeddings)

emb_df = pd.DataFrame(
    embeddings_reduced,
    index=df_news["Date"],
    columns=[f"emb_{i}" for i in range(20)]
)

现在进入流程中最复杂的部分:将时间序列特征与生成的嵌入维度合并到一个单一数据集中。该过程包括:通过清理列名、扁平化可能的多重索引列,以及确保’Date’表示为单层列来准备用于合并的 df_price。然后将结果与 emb_df 基于’Date’进行合并。

emb_df = emb_df.copy()
emb_df.reset_index(inplace=True)
emb_df.rename(columns={emb_df.columns[0]: "Date"}, inplace=True)

if 'level_0' in df_price.columns:
    df_price = df_price.drop(columns=['level_0'])
if 'index' in df_price.columns:
    df_price = df_price.drop(columns=['index'])

df_price_cleaned = df_price.reset_index()

if isinstance(df_price_cleaned.columns, pd.MultiIndex):
    new_cols = []
    for col in df_price_cleaned.columns:
        if col[0] == 'Date':
            new_cols.append('Date')
        elif isinstance(col, tuple) and col[1] != '':
            new_cols.append(col[1])
        else:
            new_cols.append(col[0])
    df_price_cleaned.columns = new_cols

if 'Ticker' in df_price_cleaned.columns:
    df_price_cleaned = df_price_cleaned.drop(columns=['Ticker'])

df_price_cleaned['Date'] = pd.to_datetime(df_price_cleaned['Date'])

df = pd.merge(df_price_cleaned, emb_df, on="Date", how="inner")

回顾之前用于时间序列特征的前缀。现在我们将列名分为两组:一组是与传统时间序列属性相关的特征,另一组是与大语言模型嵌入相对应的特征。

emb_features = [col for col in df.columns if "emb_" in col]

ts_features = [col for col in df.columns if "lag_" in col or "roll_" in col]

定义了这些特征组后,我们进入最后阶段:训练两个预测模型。第一个是仅使用时间序列特征的基线模型,第二个则结合了时间序列特征与基于嵌入的特征。

# 重要提示:将'Date'设置为DataFrame索引,以便正确分割时间序列
df = df.set_index('Date')

train = df[df.index < "2014-01-01"]  # 分割阈值
test  = df[df.index >= "2014-01-01"]

X_train_base = train[ts_features]
X_test_base  = test[ts_features]

X_train_full = train[ts_features + emb_features]
X_test_full  = test[ts_features + emb_features]

y_train = train["target"]
y_test  = test["target"]

最后,我们训练这两个模型并比较它们的性能。

# 基线模型:不使用LLM嵌入
model_base = LGBMClassifier(random_state=42)
model_base.fit(X_train_base, y_train)

pred_base = model_base.predict(X_test_base)
acc_base = accuracy_score(y_test, pred_base)

print("Baseline Accuracy:", acc_base)

基线准确率: 0.5.

# 完整模型:使用LLM嵌入
model_full = LGBMClassifier(random_state=42)
model_full.fit(X_train_full, y_train)

pred_full = model_full.predict(X_test_full)
acc_full = accuracy_score(y_test, pred_full)

print("With Embeddings Accuracy:", acc_full)

完整模型准确率: 0.5047619047619047.

结论:LLM能否提升时间序列预测?

有趣的是,完整模型的准确率仅略高于基线模型——实际上在实践意义上几乎相同。因此,结果是尚无定论的。读者也可以尝试稍微调整用于分割训练集和测试集的阈值日期(2014年1月1日),观察结果会如何变化。在某些情况下,性能差异甚至可能变得更加不稳定。

这自然而然地引出了对文章开头问题的细致回答。

LLM嵌入能改进时间序列预测吗?

答案也许并不令人意外:取决于具体情况。虽然通过特征工程整合LLM嵌入(有时被称为基于LLM的时间序列预测)在特定情境下可能前景可期,但它们远非纯粹基于时序特征的传统预测方法的通用替代方案。

近期的一些研究表明,尽管LLM在数据匮乏或文本丰富的场景中可能提升性能,但在高频、复杂数值或高度不稳定数据的环境中,它们带来的价值往往有限,甚至可能表现不佳。一个明智的策略是在多种实验设置和时间分割下评估嵌入增强的预测模型,评估其对明确定义的基线的改进是否一致且具有统计意义。

简而言之,LLM嵌入在某些预测场景中可能增加价值——但仔细的实验、稳健的验证和对领域背景的深刻理解仍然是至关重要的。FINISHED
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)或者 我的个人博客 https://blog.qife122.com/
对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐