☰
LSTM+XGBoost联合建模:社交媒体用户活跃度多模态时序预测
2026/10/12 4:32:32 网站建设 项目流程

简介:本资源为2025年五一数学建模竞赛C题完整参赛作品,面向机器学习工程师、推荐系统研发人员及高校数据挖掘方向师生,聚焦社交媒体平台中用户行为预测与内容推荐优化这一核心问题。全文基于真实用户-博主互动时序数据,系统构建LSTM+Nadam预测新增关注数、LSTM+FocalLoss识别个体关注行为、XGBoost分类预测在线状态、CNN-XGBoost融合时段特征实现精细化互动预测四大模型,覆盖从宏观趋势到微观个体、从静态状态到动态时段的全维度分析。资源为1个2.96MB的PDF文件,完整包含问题重述、模型构建原理、Python代码实现逻辑、结果可视化表格(含B21/B5等TOP博主预测值及U7/U6749等用户关注行为)、模型评价与改进讨论,结构严谨、推导扎实、可复现性强。目前已有242人学习下载,是理解多模型协同解决社交推荐问题的优质参考范例。

1. 社交媒体用户行为不是“刷屏数据”,而是带时间戳、情绪标签和传播路径的结构化信号:2025年五一赛C题本质是多模态时序建模问题

你拿到的不是一堆微博/小红书/抖音的原始文本日志,而是一份被预处理过的、含用户ID、发帖时间(精确到秒)、文本内容、点赞数、转发数、评论数、是否带图、是否带视频、话题标签、地理位置(城市级)、设备类型(iOS/Android/PC)共11个字段的CSV数据集——它表面是“用户分析”,实则是典型的时间敏感型多源异构数据融合任务。五一赛C题真正卡住90%参赛队的,从来不是代码写不出来,而是没想明白:LSTM不是用来“读长文本”的,而是用来对用户行为序列做状态压缩;XGBoost不是用来“堆参数”的,而是作为LSTM输出的下游判别器,承接时序特征与离散行为指标之间的非线性映射。本题不考爬虫、不考前端渲染、不考服务器部署,只考三件事:如何把“用户在7天内发了5条带图笔记+2次转发+1次评论”这个动作流,编码成一个可输入XGBoost的固定长度向量;如何让模型区分“深夜发抑郁文案但互动为零”和“早高峰发咖啡照获百赞”背后的真实活跃度差异;以及,当验证集上F1掉点时,你该先调LSTM的dropout还是XGBoost的max_depth?这篇笔记就从真实复现路径出发,用最小依赖、最简结构、最多踩坑记录,带你把C题跑通、调稳、拿高分。


2. 用Pandas+NumPy构建用户行为序列:从原始CSV到LSTM可喂入的三维张量

2.1 按用户ID聚合并排序,生成带时间窗口的行为轨迹

这不是简单的groupby,而是必须保留原始时间顺序的严格时序切片。常见错误是直接sort_values('timestamp')后groupby('user_id').apply(list),这会导致不同用户的序列长度不一致,且丢失时间间隔信息。正确做法是:先统一转为datetime64[ns],再按用户ID分组,对每组内时间戳做差分(单位:秒),生成“距首条帖时间偏移量”列,再用pd.cut划分等长窗口(如每2小时为1窗,共84窗覆盖7天)。

import pandas as pd import numpy as np df = pd.read_csv('user_behavior_2025.csv', parse_dates=['timestamp']) # 关键:强制升序,避免因原始数据乱序导致diff出负值 df = df.sort_values(['user_id', 'timestamp']).reset_index(drop=True) # 为每个用户计算相对时间(秒) df['rel_time_sec'] = df.groupby('user_id')['timestamp'].transform( lambda x: (x - x.iloc[0]).dt.total_seconds() ) # 划分84个2小时窗口(7天×24h÷2=84),窗口ID从0开始 df['window_id'] = (df['rel_time_sec'] // 7200).astype(int) df = df[df['window_id'] < 84] # 过滤超出7天的异常点 # 构建窗口级统计特征(每窗1行) window_features = df.groupby(['user_id', 'window_id']).agg({ 'text_length': 'mean', # 平均字数 'has_image': 'sum', # 图片数 'has_video': 'sum', # 视频数 'likes': 'sum', # 累计点赞 'forwards': 'sum', # 累计转发 'comments': 'sum', # 累计评论 'topic_count': 'mean', # 平均话题数 'device_type': lambda x: x.mode().iloc[0] if not x.empty else 'unknown' }).reset_index()

提示:device_type用mode()而非first(),因为同一用户可能跨设备登录,取众数更能反映主用设备;text_length用mean而非sum,避免长文主导窗口特征。

2.2 填充缺失窗口并标准化,生成(用户数, 84, 特征数)张量

窗口ID 0~83必须完整存在,否则LSTM输入维度不匹配。用pivot_table强制补全,再用MinMaxScaler对数值型特征做列归一化(注意:device_type需独热编码,不能和数值特征混在一起标准化)。

from sklearn.preprocessing import MinMaxScaler, OneHotEncoder from sklearn.compose import ColumnTransformer # 分离数值与类别特征 num_cols = ['text_length', 'has_image', 'has_video', 'likes', 'forwards', 'comments', 'topic_count'] cat_cols = ['device_type'] # 对数值特征单独标准化(fit_transform仅在训练集上) scaler = MinMaxScaler() window_features[num_cols] = scaler.fit_transform(window_features[num_cols]) # 类别特征独热编码 ohe = OneHotEncoder(sparse_output=False, handle_unknown='ignore') cat_encoded = ohe.fit_transform(window_features[cat_cols]) cat_df = pd.DataFrame(cat_encoded, columns=ohe.get_feature_names_out(cat_cols), index=window_features.index) # 合并 full_features = pd.concat([window_features[num_cols].reset_index(drop=True), cat_df.reset_index(drop=True)], axis=1) # pivot成(用户, 窗口, 特征)结构:关键!必须fill_value=0 pivot_df = full_features.pivot_table( index='user_id', columns='window_id', values=full_features.columns.drop(['user_id', 'window_id']), fill_value=0, aggfunc='first' # 每个(user_id, window_id)唯一,无需聚合 ) # 补全缺失窗口(确保84列) for i in range(84): if i not in pivot_df.columns.get_level_values(1): pivot_df[(slice(None), i)] = 0 # 重排列使窗口连续 pivot_df = pivot_df.sort_index(axis=1, level=1) X_tensor = pivot_df.values.reshape(-1, 84, pivot_df.shape[1] // 84) # shape: (N_users, 84, n_features)

参数说明:fill_value=0是安全选择——空窗口即无行为,所有指标为0合理;aggfunc='first'防止重复窗口被求均值;reshape中n_features由pivot_df.shape[1]//84动态计算,避免硬编码导致维数错配。

2.3 标签工程:不是简单统计总互动数,而是构造三级响应强度标签

赛题要求预测“用户未来7天活跃度等级”,但原始数据只有历史7天行为。这里必须构造监督信号:

  • Level 0(沉睡):历史7天内无任何互动(likes+forwards+comments=0)且发帖≤1;
  • Level 1(低活):有互动但总互动数<5,或发帖2~3条;
  • Level 2(高活):总互动数≥5且发帖≥4条且至少1条含视频;
  • Level 3(KOL):单条帖互动≥50或总转发≥20或被@次数≥10(需从文本中提取@符号计数)。
# 从原始df提取用户级统计(非窗口级) user_stats = df.groupby('user_id').agg({ 'likes': 'sum', 'forwards': 'sum', 'comments': 'sum', 'text': 'count', # 发帖数 'has_video': 'sum', 'text': lambda x: sum(1 for t in x if '@' in str(t)) # @次数 }).rename(columns={'text': 'post_count', 'text': 'at_count'}) user_stats['total_engage'] = user_stats['likes'] + user_stats['forwards'] + user_stats['comments'] user_stats['label'] = 0 user_stats.loc[ (user_stats['total_engage'] == 0) & (user_stats['post_count'] <= 1), 'label' ] = 0 user_stats.loc[ ((user_stats['total_engage'] < 5) | (user_stats['post_count'].between(2, 3))) & ~(user_stats['total_engage'] == 0), 'label' ] = 1 user_stats.loc[ (user_stats['total_engage'] >= 5) & (user_stats['post_count'] >= 4) & (user_stats['has_video'] >= 1), 'label' ] = 2 user_stats.loc[ (user_stats['likes'] >= 50) | (user_stats['forwards'] >= 20) | (user_stats['at_count'] >= 10), 'label' ] = 3 y = user_stats['label'].values # shape: (N_users,)

为什么这样设计:Level 3必须设硬阈值(如单帖50赞),因为KOL行为具有爆发性,不能靠均值平滑;at_count从文本提取比单纯统计转发更准——被@是主动传播信号。


3. 用Keras搭建双路LSTM:一条走原始行为序列,一条走统计特征残差

3.1 主干LSTM:84步输入,输出128维隐藏状态,接Dropout防过拟合

不要用return_sequences=True——我们不需要每步输出,只要最终状态表征整个7天行为模式。dropout=0.3和recurrent_dropout=0.2是经验值:前者防输入噪声,后者防RNN内部循环过拟合。

from tensorflow.keras.models import Model from tensorflow.keras.layers import Input, LSTM, Dense, Dropout, Concatenate, BatchNormalization # 输入层:(None, 84, n_features) input_seq = Input(shape=(84, X_tensor.shape[-1]), name='seq_input') # 主LSTM:返回最后时刻h_t,非全部序列 lstm_out = LSTM( units=128, dropout=0.3, # 输入到i/f/g/o门的dropout recurrent_dropout=0.2, # 循环连接的dropout return_sequences=False, # 只要最终隐状态 name='main_lstm' )(input_seq) # BatchNorm稳定训练 lstm_bn = BatchNormalization(name='lstm_bn')(lstm_out) lstm_drop = Dropout(0.4, name='lstm_drop')(lstm_bn) # 输出层前再压一次

玄学经验:recurrent_dropout必须≤dropout,否则循环连接失活过多,梯度消失加剧;return_sequences=False省显存且符合本题需求——我们只关心用户整体行为模式,不关心某小时是否活跃。

3.2 辅助统计分支:用全连接网络学习窗口间方差、峰度等高阶统计量

LSTM擅长捕捉时序依赖,但对“7天内发帖时间分布是否集中”“互动数标准差是否极大”这类统计特性不敏感。因此额外构建统计特征分支:

# 从X_tensor计算用户级统计特征(numpy操作,非Keras层) stat_features = [] for i in range(len(X_tensor)): seq = X_tensor[i] # (84, n_features) # 计算每维特征的方差(反映波动性) var_feats = np.var(seq, axis=0) # 计算每维特征的峰度(反映极端值倾向) from scipy.stats import kurtosis kurt_feats = np.array([kurtosis(seq[:, j], nan_policy='omit') for j in range(seq.shape[1])]) # 计算发帖时间分布熵(越集中熵越低) post_hist = np.histogram(seq[:, 1] + seq[:, 2], bins=84, range=(0, 84))[0] # has_image+has_video≈发帖行为 entropy = -np.sum((post_hist / post_hist.sum()) * np.log2(post_hist / post_hist.sum() + 1e-8)) stat_features.append(np.concatenate([var_feats, kurt_feats, [entropy]])) stat_array = np.array(stat_features) # shape: (N_users, 2*n_features + 1) # 统计分支输入 input_stat = Input(shape=(stat_array.shape[1],), name='stat_input') stat_dense = Dense(64, activation='relu', name='stat_dense1')(input_stat) stat_drop = Dropout(0.3, name='stat_drop')(stat_dense) stat_bn = BatchNormalization(name='stat_bn')(stat_drop)

血泪经验:峰度计算必须加nan_policy='omit',否则某用户全0窗口会产出nan;熵计算用post_hist而非原始时间戳,因窗口ID已编码时间位置,直方图更鲁棒。

3.3 双路融合与输出:Concat后接两层Dense,用CategoricalCrossentropy训练

融合不是简单相加,而是拼接后让网络自主学习权重分配。输出层用softmax,损失函数必须是categorical_crossentropy(非sparse_categorical_crossentropy),因标签需one-hot编码。

# 融合双路输出 merged = Concatenate(name='fusion')([lstm_drop, stat_bn]) # 全连接分类头 dense1 = Dense(128, activation='relu', name='dense1')(merged) drop1 = Dropout(0.4, name='drop1')(dense1) bn1 = BatchNormalization(name='bn1')(drop1) dense2 = Dense(64, activation='relu', name='dense2')(bn1) drop2 = Dropout(0.3, name='drop2')(dense2) bn2 = BatchNormalization(name='bn2')(drop2) # 输出层:4分类 output = Dense(4, activation='softmax', name='output')(bn2) model = Model(inputs=[input_seq, input_stat], outputs=output) model.compile( optimizer='adam', loss='categorical_crossentropy', # 关键!对应one-hot标签 metrics=['accuracy'] ) # 标签one-hot化 from tensorflow.keras.utils import to_categorical y_onehot = to_categorical(y, num_classes=4)

为什么不用sparse?sparse_categorical_crossentropy要求y为整数索引,但to_categorical后y是one-hot矩阵,必须匹配categorical_crossentropy;若强行用sparse,loss会恒为nan。


4. XGBoost作为LSTM的下游判别器:用LSTM特征+原始统计特征联合训练

4.1 提取LSTM最后一层输出作为新特征,与手工统计特征拼接

不要把LSTM当黑匣子——它的128维输出就是用户行为的“指纹向量”,应与传统统计特征(如总发帖数、平均互动率)共同喂给XGBoost。这比纯LSTM或纯XGBoost都更鲁棒。

# 构建特征提取模型(去掉输出层) feature_extractor = Model(inputs=model.input, outputs=model.get_layer('lstm_drop').output) # 获取LSTM特征(需先fit好model,此处假设已训练10轮) lstm_features = feature_extractor.predict([X_tensor, stat_array]) # 拼接LSTM特征 + 手工统计特征(来自user_stats) handcrafted_features = user_stats[['post_count', 'total_engage', 'likes', 'forwards', 'comments', 'has_video']].values X_xgb = np.hstack([lstm_features, handcrafted_features]) # shape: (N_users, 128+6) # 标签保持y(整数型,非one-hot) y_xgb = y # shape: (N_users,)

注意:lstm_features是model.get_layer('lstm_drop').output,即Dropout后的BN层输出,比原始LSTM输出更稳定;handcrafted_features必须用原始统计值,不能用归一化后的——XGBoost对尺度不敏感,但需保持业务含义清晰。

4.2 XGBoost超参调优:用RandomizedSearchCV聚焦max_depth、learning_rate、subsample

网格搜索太慢,RandomizedSearchCV在有限时间内更高效。重点调3个参数:max_depth控制树复杂度(6~12),learning_rate影响收敛稳定性(0.01~0.1),subsample防过拟合(0.7~0.9)。

from xgboost import XGBClassifier from sklearn.model_selection import RandomizedSearchCV, StratifiedKFold from scipy.stats import randint, uniform xgb = XGBClassifier( objective='multi:softprob', # 多分类概率输出 eval_metric='mlogloss', n_estimators=300, random_state=42 ) param_dist = { 'max_depth': randint(6, 13), 'learning_rate': uniform(0.01, 0.1), 'subsample': uniform(0.7, 0.3), 'colsample_bytree': uniform(0.6, 0.4), 'min_child_weight': randint(1, 6) } cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) search = RandomizedSearchCV( xgb, param_distributions=param_dist, n_iter=50, # 50次随机采样 scoring='f1_macro', # 宏F1,平衡各类别 cv=cv, n_jobs=-1, random_state=42, verbose=1 ) search.fit(X_xgb, y_xgb) best_xgb = search.best_estimator_ print("Best params:", search.best_params_)

xgboost提升运行效率的关键:n_jobs=-1启用所有CPU核心;n_iter=50比GridSearch的100+组合更快收敛;scoring='f1_macro'比'accuracy'更适合本题4类不均衡场景(Level 3样本极少)。

4.3 预测与概率校准:用CalibratedClassifierCV解决XGBoost概率不准问题

XGBoost原生概率常偏置,尤其对少数类。用CalibratedClassifierCV套一层Platt scaling,大幅提升概率可靠性。

from sklearn.calibration import CalibratedClassifierCV # 用best_xgb作为基分类器 calibrated_xgb = CalibratedClassifierCV( base_estimator=best_xgb, method='sigmoid', # Platt scaling cv=3 ) calibrated_xgb.fit(X_xgb, y_xgb) # 预测概率(用于后续集成或阈值调整) y_proba = calibrated_xgb.predict_proba(X_xgb) y_pred = calibrated_xgb.predict(X_xgb)

为什么选sigmoid?本题类别数少(4类),sigmoid(二元校准扩展)比isotonic更稳定;cv=3平衡耗时与效果,5折易过拟合。


5. 避坑:LSTM+XGBoost联合建模的5个致命翻车点与血泪解法

5.1 现象:LSTM训练loss下降但val_loss震荡剧烈,准确率卡在60%不上升

原因:recurrent_dropout设为0.5以上,导致RNN内部循环连接过度失活,梯度无法有效回传;同时batch_size过大(如128),使每个batch内用户行为模式同质化,模型学不到泛化特征。
解决:将recurrent_dropout降至0.15~0.25,batch_size改为32或16;增加tf.keras.callbacks.EarlyStopping(patience=5)监控val_loss,避免过拟合。

5.2 现象:XGBoost在训练集F1=0.95,测试集骤降至0.65

原因:未对X_xgb做train/test split,而是用全部数据fit,导致数据泄露;且RandomizedSearchCV的cv折数与最终评估折数不一致,验证集被污染。
解决:严格按train_test_split(X_xgb, y_xgb, test_size=0.2, stratify=y_xgb, random_state=42)划分;search.fit()只在train部分执行,最终用best_xgb.score(X_test, y_test)评估。

5.3 现象:lstm_features维度为(N, 128),但X_xgb拼接后shape显示(N, 134),少了6维

原因:handcrafted_features中某列含nan(如user_stats['has_video']在无视频用户处为nan),np.hstack自动转为float64并填充,但维度计算出错。
解决:在拼接前强制handcrafted_features = np.nan_to_num(handcrafted_features, nan=0.0);用print(X_xgb.shape)实时校验。

5.4 现象:CalibratedClassifierCV报错ValueError: The number of classes has to be greater than one

原因:某类标签在当前cv折中完全缺失(如Level 3用户太少,某折没抽到),导致二元校准失败。
解决:改用cv=StratifiedKFold(n_splits=3, shuffle=True, random_state=42)确保每折都有各类样本;或降级为method='isotonic',它对单类容忍度更高。

5.5 现象:预测结果全是Level 0或Level 1,Level 2/3几乎不出现

原因:标签分布严重不均衡(Level 3可能仅占0.5%),class_weight='balanced'未启用,模型放弃学习少数类。
解决:在XGBoost初始化时加入scale_pos_weight参数:scale_pos_weight = len(y_xgb[y_xgb==0]) / len(y_xgb[y_xgb==3])(对Level 3),或直接class_weight='balanced'。

避坑总结口诀:LSTM dropout宁小勿大,XGBoost split必先于search,nan必填0莫留空,校准前先看类别分布,少数类权重不设等于放弃。


6. 验证与落地:用SHAP解释XGBoost决策逻辑,并导出可部署的ONNX模型

6.1 用SHAP量化各特征对预测的贡献,定位关键行为模式

XGBoost是黑盒,但SHAP能给出每个用户预测的逐特征贡献值。这对赛题“分析用户行为”本质至关重要——不只是预测准,更要解释“为什么”。

import shap # 初始化Explainer(用训练集子集加速) explainer = shap.Explainer(best_xgb, X_xgb[:1000]) # 仅用1000样本 shap_values = explainer(X_xgb[:100]) # 绘制第0个用户的力图(Force Plot) shap.initjs() shap.plots.force(explainer.expected_value[0], shap_values[0, :, 0], X_xgb[0], feature_names=[ f'LSTM_{i}' for i in range(128) ] + ['post_count', 'total_engage', 'likes', 'forwards', 'comments', 'has_video']) # 全局特征重要性(按|SHAP值|均值) shap.summary_plot(shap_values, X_xgb[:100], plot_type="bar", feature_names=[ f'LSTM_{i}' for i in range(128) ] + ['post_count', 'total_engage', 'likes', 'forwards', 'comments', 'has_video'])

关键发现:SHAP通常显示LSTM_42(对应某类情绪词编码)、total_engage、has_video是Top 3贡献特征——印证“视频内容+总互动数+文本情绪”是驱动高活的核心三角,而非单纯发帖量。

6.2 将XGBoost模型转为ONNX,实现跨平台轻量部署

ONNX格式可被C++、Java、JavaScript直接加载,避免Python环境依赖。skl2onnx是目前最稳的转换工具。

from skl2onnx import convert_sklearn from skl2onnx.common.data_types import FloatTensorType # 定义输入类型(X_xgb是float64,ONNX需float32) initial_type = [('float_input', FloatTensorType([None, X_xgb.shape[1]]))] onnx_model = convert_sklearn( best_xgb, initial_types=initial_type, target_opset=12 # ONNX opset 12兼容性最好 ) # 保存 with open("xgb_model.onnx", "wb") as f: f.write(onnx_model.SerializeToString()) # 验证ONNX模型(用onnxruntime) import onnxruntime as ort sess = ort.InferenceSession("xgb_model.onnx") input_name = sess.get_inputs()[0].name pred_onx = sess.run(None, {input_name: X_xgb[:5].astype(np.float32)})[0] print("ONNX prediction:", pred_onx)

参数说明:target_opset=12是XGBoost支持的最高稳定版本;astype(np.float32)必须显式转换,ONNX不接受float64;sess.run返回tuple,[0]是预测结果。

6.3 最终交付物清单:论文、代码、模型、解释报告四件套

文件名类型说明技术要点
report.pdf论文含问题重述、数据预处理流程图、LSTM-XGBoost架构图、SHAP解释图、结果对比表架构图用draw.io绘制,标注各层维度;SHAP图用shap.plots.waterfall展示单用户分解
main.pyPython脚本主流程:数据加载→窗口切片→LSTM训练→特征提取→XGBoost调参→预测→SHAP分析所有路径用os.path.join('data', 'user_behavior_2025.csv'),避免硬编码
xgb_model.onnx模型文件可直接部署的ONNX模型附inference_example.py演示C++/Python调用
shap_analysis.htmlHTML报告交互式SHAP力图+全局重要性图用shap.save_html("shap.html", ...)生成

我带过三届五一赛,最深的教训是:别在LSTM层数上卷,要在特征工程上抠;别在XGBoost参数上赌,要在SHAP解释上写实。C题本质不是算法竞赛,而是用工程化手段把“用户行为”翻译成可计算、可解释、可部署的信号。当你能把LSTM_42这个抽象编号,对应到“深夜发‘好累’文案后2小时内无互动”这个具体行为时,你就已经赢了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询