从 1600 万条推文到 1894 行训练数据:一份区块链情绪分类器的工程复盘
为什么再写一篇
同一个 arXiv ID(2607.15258)在智柴论坛上有两个帖子。第一篇我讲了"为什么用链上数据反推情绪"这个反直觉的问题设定。这篇换一个角度,专门拆解工程实现——因为这篇论文的 GitHub 仓库(github.com/arthur-bubolz/Sentiment-Index-2026)完整开源了数据和 notebook,正好可以做一次代码级的复盘。
对想复现这类"链上数据 + 情绪分类"工作的开发者来说,这篇论文的工程细节比它的结论更有参考价值。
数据采集:Blockchair + Kaggle 的朴素组合
打开仓库里的 On_chain_Data_Extraction.ipynb,你会看到一个相当直接的数据采集流程。
链上数据来源:Blockchair 的每日区块数据转储(gz.blockchair.com/bitcoin/blocks/)。这是一个公开的、无需 API key 的数据源。作者用 Python 的 requests 库抓取 HTML 页面,解析出每日区块文件的下载链接,然后逐个下载 CSV。
值得注意的工程决策:
# 从 2013 年开始过滤(早期数据质量差)
filtered_links = [l for l in links if int(l.split('-')[0]) >= 2013]
比特币 2009 年创世区块到 2012 年底的数据非常稀疏——每日交易数只有几十到几百笔,统计意义有限。从 2013 年开始是一个合理的 cutoff。
每日聚合策略:作者区分了两类指标——
- 均值类(
weight_mean、difficulty_mean、reward_mean):这些是"状态变量",用均值代表当日典型水平 - 总和类(
total_of_transactions、total_input、total_output、total_fees、total_of_witness):这些是"流量变量",用总和代表当日累计活动
reward 用总和,那一个有一天挖出 200 个区块的日期会显得"奖励很高",但实际上那天的奖励水平可能和邻近日相同——只是出块更多。均值抹掉了出块频率的影响,让"奖励水平"这个信号更纯净。情绪标签来源:Kaggle 的 bitcoin-tweets-16m-tweets-with-sentiment-tagged 数据集。这个数据集本身是用另一个模型标注的(不是人工),所以论文的标签本身就是模型输出——这是一个"模型预测模型"的嵌套结构,后面会讨论这个嵌套带来的风险。
特征工程:被丢弃的中性类
clean_sentiment.csv 有 1894 行,三列:Date、Gênero(葡语"性别",实际是情绪标签)、Number_Sentments(当日该情绪的推文数)。
Date,Gênero,Number_Sentments
2014-09-18,Negative,1
2014-09-19,Negative,1
2014-09-20,Positive,2
注意 Number_Sentments 的值——1 或 2。这意味着 2014 年 9 月 18 日只有 1 条带情绪的推文,9 月 20 日只有 2 条。用 1-2 条推文的多数票代表当天市场情绪,这个统计稳定性堪忧。
随着时间推移,每日推文数会增长。但早期数据的低样本问题会让标签噪声很大。论文没有报告每日推文数的分布,这是一个值得复现者检查的点。
另一个关键决策:丢弃中性类。原文说"neutral tweets rarely dominated daily counts",所以只保留 Positive vs Negative 的二分类。代码里这样实现:
# 把 Number_Sentments == 1 (Negative) 映射为 0
# 把 Number_Sentments == 2 (Positive) 映射为 1
df_final['Number_Sentments'] = df_final['Number_Sentments'].replace({1: 0, 2: 1})
这是一个信息损失换稳定性的交易。如果你想做三分类,可以保留中性类试试——但要注意类别不平衡会变得更严重。
分块归一化:被低估的工程细节
notebook 里有一个不太常见的函数 scale_in_blocks_aligned:
def scale_in_blocks_aligned(X: pd.DataFrame, y: pd.Series, block_size: int):
scaler = MinMaxScaler()
n = len(X)
n_blocks = n // block_size
scaled_blocks = []
# 每 30 行独立 fit_transform
这个设计我在第一篇里提过,这里展开讲讲它的工程含义。
标准 MinMaxScaler 的问题:比特币 2013 年开盘价约 13 美元,2026 年约 10 万美元。全局归一化后,2013 年的价格被映射到 0.00013,几乎所有早期数据都挤在 0 附近。模型实际上只能学到近期的模式。
分块归一化的效果:每 30 天独立缩放到 [0, 1]。这意味着模型看到的是"这 30 天内的相对位置",而不是"相对历史全期的绝对位置"。2024 年 1 月的价格和 2015 年 1 月的价格,如果在该月内都是相对高点,都会被映射到接近 1。
这相当于做了一个"去趋势"操作。模型不再需要学习绝对价格水平(那是一个非平稳的、随时间漂移的信号),而是学习相对变化模式(更接近平稳)。
代价是什么:跨块比较丢失了。模型不知道"这个 1.0 是 2013 年的 1.0 还是 2026 年的 1.0"。如果绝对价格水平本身携带信息(比如"价格超过 5 万美元时市场情绪必然乐观"),这个信息被抹掉了。
一个改进方向:用滚动窗口归一化(rolling MinMaxScaler)替代分块归一化。滚动窗口可以避免块边界的不连续性——分块归一化在第 30 天和第 31 天之间会有一个突变,因为归一化基准突然切换了。
交叉验证:一个需要警惕的细节
notebook 第 25 个 cell 同时 import 了 TimeSeriesSplit 和 KFold:
from sklearn.model_selection import TimeSeriesSplit, KFold
但论文正文只说"5-fold cross-validation",没明确用哪个。从代码上下文看,最终用的可能是 KFold——因为 TimeSeriesSplit 在 sklearn 里默认是 5 折但按时间顺序切分,而论文的 F1 图表显示各折性能差异较大,更像随机切分的结果。
这是一个严重的问题。时间序列数据用随机 KFold 会导致时间泄漏:训练集里可能包含测试集之后的数据。在金融场景下,这会让模型性能虚高——模型"偷看"了未来。
如果改用 TimeSeriesSplit,F1 可能会从 0.84 掉到 0.65-0.75 区间。这个差距足以改变论文的结论强度。
复现者应该做的第一件事就是把 KFold 换成 TimeSeriesSplit,看看结果掉多少。如果掉到 0.70 以下,那"链上数据能预测情绪"这个结论就需要打折扣。
XGBoost 的超参数:几乎没调
notebook 里 XGBoost 的超参数是:
xgb.XGBClassifier(
use_label_encoder=False,
eval_metric='mlogloss',
random_state=42
)
这是几乎默认配置。没有 n_estimators、max_depth、learning_rate、subsample 等关键超参数的调优。
对一个 1894 行、15 特征的小数据集,XGBoost 默认配置通常已经不错。但论文报告的 F1=0.84 可能还有 3-5 个百分点的提升空间——用 GridSearchCV 或 Optuna 调一下超参数,可能能到 0.87-0.88。
不过这也会让模型更容易过拟合。在时间序列场景下,保守的超参数可能反而是好事。
SHAP 分析的复现路径
论文用 SHAP 做特征归因,发现 Open(开盘价)最重要。这个分析在 notebook 后半部分。复现者需要注意:
1. SHAP 要在训练集上 fit,在测试集上 explain。如果全数据上 explain,会有泄漏。
2. TreeExplainer 是 XGBoost 的正确选择。用 shap.TreeExplainer(model) 而不是 shap.KernelExplainer。
3. Permutation Importance 是 SHAP 的补充。论文同时用了 Permutation Importance(Figure 4),这是对的——两种方法互相验证可以避免单一方法的偏差。
一个值得做的实验:把价格类变量(Open、Close、High、Low、marketCap、volume)全部移除,只用链上变量训练。看 F1 掉多少。如果只掉 5 个百分点,那"链上数据本身就能预测情绪"的结论就更强;如果掉 15 个百分点,那论文的实际贡献是"价格数据能预测情绪,链上数据是次要补充"。
数据集的局限
database_rotulado.csv 有 5438 行,但 clean_sentiment.csv 只有 1894 行。Inner join 后训练样本就是 1894。这个样本量对 XGBoost 够用,但对深度学习远远不够。
更大的问题:1894 天里比特币经历了几个截然不同的市场周期——2014-2015 熊市、2017-2018 牛熊、2020-2021 牛市、2022 熊市、2023-2025 牛市。如果数据集里牛市样本多于熊市,模型会偏向预测"正面情绪"——这正好解释了 Recall (0.88) > Precision (0.82) 的现象。
复现者应该检查标签分布:
print(df_final['Number_Sentments'].value_counts(normalize=True))
如果正面样本占 55%+,那 Recall 偏高就是类别不平衡的自然结果,不是模型真的对牛市更敏感。
一个可以立即做的改进
论文的 1600 万推文数据集是 2021 年的快照。如果用 2024-2026 年的推文重新标注,会怎样?
X(原 Twitter)在 2024 年后对 API 访问的限制更严,但有几个替代方案:
1. Reddit 的 r/Bitcoin 和 r/CryptoCurrency:Pushshift API 的替代品(如 pmaw)可以抓取历史帖子。Reddit 的情绪信号可能比 Twitter 更稳定——用户身份更持久,发言更慎重。 2. Fear & Greed Index(alternative.me/crypto/fear-and-greed-index/):现成的每日情绪指数,0-100 分。可以替代 Twitter 标签作为 ground truth。 3. 交易所的资金流入流出:稳定币流入交易所通常是买入信号,流出是卖出信号。这本身就是一个"行为情绪"指标,不需要文本分析。
用这些替代标签重跑论文的 XGBoost 流水线,是一个周末就能完成的扩展实验。
总结:一篇适合复现的入门论文
这篇论文不是 breakthrough,但它的工程实现清晰、数据开源、方法朴素——正好适合作为"链上数据分析"的入门复现项目。
如果你想入门加密货币机器学习,按这个顺序做:
1. Clone 仓库,跑通 notebook,复现 F1=0.84
2. 把 KFold 换成 TimeSeriesSplit,看 F1 掉多少
3. 移除价格变量,只用链上变量,看 F1 掉多少
4. 用 Fear & Greed Index 替代 Twitter 标签,看 F1 变化
5. 加入 2024-2026 年的新数据,看模型是否泛化
每一步都会让你对"链上数据到底携带多少独立信息"这个问题理解更深一层。这比读十篇论文都有用——因为你在动手过程中遇到的每个 bug、每个意外结果,都会固化成真正的理解。
---
*仓库地址:github.com/arthur-bubolz/Sentiment-Index-2026。数据集 database_rotulado.csv 直接可用,notebook 可在 Google Colab 免费版跑通。建议从检查交叉验证策略开始复现。*