简介:这套资源是一份基于深度神经网络(DNN)的社交媒体用户性格分析完整项目,面向人工智能、深度学习方向的研究者与开发者,适合用于NLP文本分类、用户画像及性格预测等场景。包内实现了从数据爬取、预处理、特征构建到模型训练、评估与推理的完整流程,包含TF-IDF、Doc2Vec、情感词典、词向量等特征处理方法,并覆盖DNN与SVM对比实验,可帮助读者理解性格多分类任务的落地思路。资源共54个文件,以Python脚本、npy数据文件为主,另有Markdown说明文档、文本配置、CSV数据及gitignore配置,压缩包整体约18.34MB。目录区分为数据处理、模型训练、评估推理等模块,结构清晰,便于按步骤复现实验。项目结合五大人格特质模型,展示如何将深度学习应用于社交媒体文本分析,对个性化推荐、广告定向等领域具有参考价值。已有136人学习下载,适合具备一定Python与深度学习基础、希望从零搭建文本性格分析系统的读者深入研究。
1. 文本性格分析从传统机器学习转向DNN,瓶颈不在模型而在特征形态
做社交媒体用户性格分析,很多团队第一版用的是SVM加TF-IDF,跑出来的准确率也在可接受范围内,但一旦把数据规模从几千条推到几万条,SVM的核函数计算和稀疏矩阵存储会先把内存压垮,调参窗口也越来越窄。这个项目把特征工程和DNN训练放到同一条流水线里,等于把“特征设计”和“分类器训练”解耦,让模型自己去组合高阶特征,而不是靠人工拼特征。它的核心价值不在于网络有多深,而在于提供了从文本清洗、特征抽取、模型训练到推理落地的完整链路。对刚入门的工程师来说,能直接看到每一层输入输出的形状变化;对做过多年的开发者,也能在特征选择和数据增强的策略上找到值得对比的细节。项目代码基于Python实现,模型部分用TensorFlow或PyTorch均可复现,数据集指向社交媒体文本标注的五大人格维度,适合NLP方向的算法工程师、数据挖掘岗面试者、以及对用户画像建模感兴趣的研究人员。
2. 数据清洗与特征工程:222维特征是如何从原始文本里挤出来的
2.1 原始语料的预处理链路
项目根目录下的process_data1.py负责把社交媒体原始文本转换成结构化数据。单条用户动态,比如“今天天气真好啊哈哈哈哈”,经过管道后变成词汇序列、词性标记和情感极性三路输出。这个多路并行设计的目的是后续可以同时构建词汇级和句子级特征,避免单一表示丢失语义层次。
# process_data1.py 核心逻辑(示意代码) import re import jieba from emotion_lexicon import load_lexicon def clean_text(raw: str) -> str: # 1. 去除URL和@提及 text = re.sub(r'http\S+|www\.\S+', '', raw) text = re.sub(r'@\w+', '', text) # 2. 统一数字和英文大小写 text = re.sub(r'\d+', ' NUM ', text) text = text.lower() return text def tokenize(text: str) -> list: # 中文按jieba精确模式分词,保留长度大于1的词 return [w for w in jieba.lcut(text) if len(w.strip()) > 1] def extract_emotion_scores(text: str, lexicon: dict) -> dict: # 统计文本中正/负情感词命中数,返回极性得分 pos_score = sum(1 for w in tokenize(text) if w in lexicon['pos']) neg_score = sum(1 for w in tokenize(text) if w in lexicon['neg']) return {'pos': pos_score, 'neg': neg_score}clean_text里的正则顺序有讲究——先删URL再处理数字,能避免把URL里的数字误归一化成NUM标记,否则下游TF-IDF会把“链接”和“数字”混成同一个语义槽。tokenize这里用len(w.strip()) > 1过滤单字,是因为社交媒体口语里单字语气词(“哈”“啊”)对人格预测的判别力极低,且会拉高特征维度,保留反而增加过拟合风险。情感词典的命中统计走的是朴素计数路线,词频信息后续会和TF-IDF特征做拼接,而不是单独建模,这样设计的原因是情感极性本身已经压缩了语义,不需要再让DNN从头学。
2.2 三类特征的无缝拼接
222 features目录下存的是已经抽取完成的特征矩阵,每行对应一个用户,每列对应一个特征维度。这222维特征不是单一来源,而是分为三组:词法统计类(token数量、平均句长、标点密度、大写比例等)、TF-IDF类(对分词结果做TfidfVectorizer降维到150维)、Doc2Vec类(用doc2vec模型把用户全部动态聚合成一条文档向量,取维度70)。
# 特征拼接逻辑(示意代码) from sklearn.feature_extraction.text import TfidfVectorizer from gensim.models.doc2vec import Doc2Vec, TaggedDocument # 1. TF-IDF特征:ngram_range=(1,2),控制维度不超过150 tfidf_vec = TfidfVectorizer(max_features=150, ngram_range=(1,2), sublinear_tf=True, min_df=3) tfidf_mat = tfidf_vec.fit_transform(all_docs) # 2. Doc2Vec特征:dm=0表示使用DBOW模式,训练更快 d2v_model = Doc2Vec(documents=[TaggedDocument(doc.split(), [i]) for i, doc in enumerate(all_docs)], vector_size=70, window=5, min_count=2, dm=0, epochs=20, workers=4) d2v_mat = [d2v_model.dv[i] for i in range(len(all_docs))] # 3. 手工特征:统计每个用户的动态条数/平均长度/情感极性 manual_feat = extract_manual_features(user_texts) # 假设返回(样本数, 2)TF-IDF的max_features=150属于结构化压缩,原本文本空间可能有几万维,截断到150维后虽然损失长尾词信息,但配合Doc2Vec的70维语义向量,恰好覆盖了“关键词命中”和“全局语义”两个互补维度。Doc2Vec使用dm=0(DBOW)而非dm=1(DM),原因是训练语料只有社交媒体短文本,DM模式对上下文窗口的平滑要求更高,短文本下DBOW的泛化能力更稳。workers=4在多核机器上能跑满,但对小数据集(低于1万条)反而会有线程切换开销,建议改为2。
2.3 特征矩阵写入与内存边界控制
特征拼接完成后,write_feature_matrix函数负责把稀疏矩阵和稠密向量统一成.npy格式落盘。这里有一个容易踩的坑:TF-IDF输出的是scipy.sparse矩阵,直接np.save会报错,必须用toarray()显式转成稠密格式,但5000用户×222维的稠密矩阵内存约9MB,不影响;如果用户量到10万级,建议只存稀疏格式并让DNN从稀疏输入层开始训练。
import numpy as np from scipy import sparse def write_feature_matrix(tfidf_mat, d2v_mat, manual_feat, output_path): # 合并三类特征:横向堆叠 dense_tfidf = tfidf_mat.toarray() if sparse.issparse(tfidf_mat) else tfidf_mat all_features = np.hstack([dense_tfidf, d2v_mat, np.array(manual_feat).reshape(-1, 2)]) # 输出形状为 (样本数, 222) np.save(output_path, all_features) print(f"Feature matrix saved with shape {all_features.shape}")output_path建议按数据集版本命名,比如feat_v3_222.npy,因为FB数据集的标签分布在不同采集批次下会有偏移,版本号能帮你快速回溯是哪一版特征跑出的指标。np.hstack拼接时要求三组数据的样本数一致,如果某用户没有Doc2Vec向量(因为分词后词数少于min_count),需要在更早环节滤掉该用户,否则拼接阶段直接报维度不匹配错误。
3. DNN模型设计与训练:从222维输入到五维输出的映射
3.1 网络结构和参数量计算
character_dnn目录下的character_train.py定义了模型主体。输入层为222个特征,经过三层全连接后,输出层为10个节点。这里输出为什么不是5而是10?因为项目把每个性格维度建模成独立的二分类问题——比如“开放性”分高低两档,而不是直接做五分类,这样每个维度的阈值可以单独校准,避免不同性格维度的分数分布差异互相干扰。
# character_train.py 核心模型定义(示意代码) import tensorflow as tf from tensorflow.keras import layers, models def build_dnn_model(input_dim=222, hidden_units=[128, 64, 32]): model = models.Sequential(name="character_dnn") model.add(layers.InputLayer(input_shape=(input_dim,), name="features")) # 隐藏层:ReLU + BatchNorm + Dropout 交替 for units in hidden_units: model.add(layers.Dense(units, activation="relu", kernel_regularizer=tf.keras.regularizers.l2(5e-4))) model.add(layers.BatchNormalization()) model.add(layers.Dropout(rate=0.3)) # 输出层:sigmoid,每个维度独立二分类 model.add(layers.Dense(10, activation="sigmoid", name="personality_output")) model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), loss="binary_crossentropy", metrics=["accuracy", tf.keras.metrics.AUC(name="auc")]) return model三层隐藏层设为128-64-32,是一个从宽到窄的漏斗结构。输入222维特征,第一层用128个神经元先做宽泛组合,第二层64个神经元压缩模式,第三层32个神经元提炼高级抽象。这种结构比直接222→10的单层网络多了非线性表达能力,又比深层ResNet更容易训练——毕竟文本统计特征不像图像像素那样有天然的空间局部性,过深的网络反而会把已经稠密的特征打散。L2正则化系数5e-4是经验值,太大(超过1e-2)会把权重压得过于稀疏,导致模型退化到线性;Dropout率0.3对222维输入来说是中等强度,社交媒体文本特征之间本身有相关性,dropout率太高会随机屏蔽整组相关特征,训练不稳定。
3.2 训练配置与早停策略
训练循环里需要特别关注的是早停和模型checkpoint的配合。character_train.py里设置了验证集上AUC不再提升连续5轮就停止训练,并保存验证集上AUC最高的权重文件。
# character_train.py 训练配置(示意代码) checkpoint = tf.keras.callbacks.ModelCheckpoint( filepath="model/best_model.weights.h5", monitor="val_auc", mode="max", save_best_only=True, verbose=1) early_stop = tf.keras.callbacks.EarlyStopping( monitor="val_auc", patience=5, restore_best_weights=True, verbose=1) history = model.fit( x_train, y_train, validation_data=(x_val, y_val), epochs=100, batch_size=64, callbacks=[checkpoint, early_stop], verbose=1 )monitor="val_auc"比val_accuracy更可靠。人格分析数据集的标签分布通常不平衡——比如“神经质”维度,低分组可能占70%,模型全部预测为低分组也能有70%准确率,但AUC会掉到0.5左右,能真实反映模型区分能力。patience=5意味着如果连续5轮验证集AUC没创新高,就回滚到最优权重,这个值适配小数据集,因为小数据集验证集噪声大,patience太小(比如2)容易被单轮的随机波动骗到提前停止。
注意:
restore_best_weights=True保证训练结束后模型加载的是历史最优权重,而不是最后一轮的权重。有些版本ModelCheckpoint和EarlyStopping同时启用时,如果EarlyStopping先触发,restore_best_weights会覆盖掉checkpoint加载的权重,需要确认TF版本行为,建议统一使用EarlyStopping的恢复机制。
3.3 与SVM基线模型的对比实验
项目里保留了character_svm目录,里面有svm_tfidf_test.py和svm_dbow_test.py两个脚本——前者只用TF-IDF特征,后者用DBOW向量。做对比实验的意义在于:DNN的收益到底来自特征组合能力还是网络结构本身,如果不跑SVM基线,这个结论说不清。
# svm_tfidf_test.py 对比实验(示意代码) from sklearn.svm import SVC from sklearn.multiclass import OneVsRestClassifier # SVM用RBF核,C=1.0,gamma='scale',每个维度独立训练 svm_model = OneVsRestClassifier( SVC(kernel='rbf', C=1.0, gamma='scale', probability=True, random_state=42), n_jobs=-1) svm_model.fit(x_train, y_train) y_pred_proba = svm_model.predict_proba(x_val)OneVsRestClassifier包了一层SVC,和DNN的10节点sigmoid输出在数学上是等价的——都是每个维度做独立二分类。C=1.0是默认值,RBF核下这个值偏保守,如果你在SVM上把C调到10以上,可能缩小和DNN的差距,但训练时间会指数级增长。gamma='scale'会根据特征维数自动计算带宽,222维特征下等价于gamma=1/222,是一个合理的初始值。跑完对比,记录每个维度的AUC,如果DNN比SVM提升不足1%,说明特征工程已经做到位,网络再深也难有突破;如果提升超过3%,说明特征之间的非线性交互确实被DNN捕获了。
4. 模型评估与推理落地:character_eval和character_inference的完整逻辑
4.1 评估指标如何细粒度拆分
character_eval.py不是简单地打印整体准确率,而是输出10个标签各自的精确率、召回率、F1和AUC,并额外计算五个维度的高低分组各自的正负样本比例。这个设计解决了机器学习里关键的“平均指标掩盖局部失败”问题——整体准确率75%可能意味着“尽责性”维度的低分组全对了,“开放性”维度的高分组全错了,平均值一拉,看起来还不错,但业务上完全没法用。
# character_eval.py 各维度评估(示意代码) from sklearn.metrics import classification_report, roc_auc_score import numpy as np label_names = ['open_low', 'open_high', 'cons_low', 'cons_high', 'extra_low', 'extra_high', 'agree_low', 'agree_high', 'neur_low', 'neur_high'] # 对每个输出节点做独立评估 for idx, name in enumerate(label_names): y_true_dim = y_val[:, idx].astype(int) y_pred_binary = (y_val_pred[:, idx] > 0.5).astype(int) auc = roc_auc_score(y_true_dim, y_val_pred[:, idx]) report = classification_report(y_true_dim, y_pred_binary, output_dict=True, zero_division=0) print(f"Dimension {name}: AUC={auc:.4f}, " f"F1={report['1']['f1-score']:.4f}, " f"Precision={report['1']['precision']:.4f}")roc_auc_score接收的是预测概率而不是二值标签,这一点比accuracy_score更严格——如果只传y_pred_binary,AUC会因离散化丢失大量概率排序信息,算出的值也偏低。zero_division=0处理极端情况,当某个维度在预测时完全没有输出正样本(比如所有预测概率都低于0.5),classification_report按定义会除零,设成0避免训练中断。如果某个维度的高分组F1明显低于低分组,需要先看该组的样本量——社交媒体文本对“开放性”这类心理特质的信号本身就比较弱,模型倾向保守预测,这种情况优先考虑给少数类加权,而不是急着改网络结构。
4.2 评估集划分中容易被忽视的泄漏源
目录里utils文件下的split_data函数承担了划分逻辑。真正容易导致评估结果虚高的泄漏源不是随机种子没设,而是同一用户的多次动态被拆到了训练集和验证集。假设一个用户发了200条推文,模型见过其中150条,验证集里剩下50条和训练集内容高度相关,预测自然准确,但换新用户就露馅——这个在代码里需要按用户ID而不是按文本条目切分。
# utils/split_data.py 按用户划分(示意代码) from sklearn.model_selection import GroupShuffleSplit def group_split(user_ids, X, y, test_size=0.2, random_state=42): gss = GroupShuffleSplit(n_splits=1, test_size=test_size, random_state=random_state) train_idx, val_idx = next(gss.split(X, y, groups=user_ids)) return X[train_idx], X[val_idx], y[train_idx], y[val_idx]GroupShuffleSplit的groups参数是关键,它保证同一个用户的样本全部落在同一侧。如果不用分组切分,评估的AUC虚高0.05到0.1是很正常的,尤其在Doc2Vec特征下,因为同用户的文本训练出的向量天然就有嵌入记忆。嵌套写法next(gss.split(...))只取第一折,因为这里只需要一个验证集,不需要K折交叉验证——K折在小数据集上会显著增加训练时间,且每个fold的模型都不一样,最后没法直接对单条新样本做推理。
4.3 推理脚本的输入输出设计
character_inference.py是新用户文本在线预测的入口。它加载保存好的权重文件,对新用户的所有动态按时间排序拼接成单个文档,经过和训练阶段完全相同的预处理、TF-IDF变换、Doc2Vec推断后,输出10个概率值。
# character_inference.py 推理逻辑(示意代码) def predict_user(model, tfidf_vectorizer, d2v_model, user_texts): # 1. 拼接用户动态并按训练时的流程清洗 merged_text = " ".join(user_texts) clean = clean_text(merged_text) # 2. TF-IDF必须用训练时拟合的vectorizer,不能重新fit tfidf_vec = tfidf_vectorizer.transform([clean]) # 3. Doc2Vec推理:用dbow_words=False避免更新词向量 doc_vec = d2v_model.infer_vector(tokenize(clean), epochs=50) # 4. 特征拼接后输入DNN features = np.hstack([tfidf_vec.toarray(), doc_vec.reshape(1, -1), extract_manual_feature(clean)]) proba = model.predict(features)[0] # 5. 输出为五维性格得分(每维取high档概率) return {"openness": proba[1], "conscientiousness": proba[3], "extraversion": proba[5], "agreeableness": proba[7], "neuroticism": proba[9]}tfidf_vectorizer必须直接复用训练时保存的对象,而不是新建一个TfidfVectorizer再fit——重新fit得到的词表和新数据分布一致,但和训练阶段的词表空间不对齐,transform出来特征的位置全错乱了。infer_vector的epochs=50是Gensim的默认值,对短文本聚合文档建议保持,过少(小于20)语义向量不够稳定,过多(大于100)会把训练阶段的向量空间拉偏。proba[1]代表openness维度high档的概率,因为输出节点索引顺序是低档在前高档在后,这个映射关系要和训练时标签编码保持一致,否则预测结果正好反向。
5. 用混淆矩阵定位特征失效的边界条件
性格分析的最后一个实战技巧,是把混淆矩阵按特征来源拆分来看——不是看四个象限数字,而是看“哪些样本被预测正确、哪些被预测错误”对应的特征模式差异。常见的定位场景:验证集上“外向性”维度F1为0.61,明显低于其他维度。直接改网络结构可能无效,正确的排查是把错误预测的样本单独抽出来,检查他们的Doc2Vec向量在空间中的分布位置。
# 特征错误模式分析(示意代码) import matplotlib.pyplot as plt from sklearn.decomposition import PCA # 取预测错误的样本 errors_idx = np.where((y_val[:, 5] > 0.5) != (y_val_pred[:, 5] > 0.5))[0] # 降维并可视化错误样本的Doc2Vec空间位置 pca = PCA(n_components=2) d2v_reduced = pca.fit_transform(d2v_val[errors_idx]) plt.figure(figsize=(8, 6)) plt.scatter(d2v_reduced[:, 0], d2v_reduced[:, 1], c=y_val[errors_idx, 5], cmap='coolwarm', alpha=0.7) plt.title("Distribution of error samples in Doc2Vec space") plt.savefig("error_analysis_extra.png", dpi=150)errors_idx获取的是预测错误样本的索引,这一行在调试阶段的价值是让你能回溯原始文本,直接看模型到底在哪些表达上犯了错。如果错误样本在Doc2Vec空间里聚成明显的两团,说明该性格维度在文档向量这个特征族上存在语义盲区,常见做法是回到process_data1.py增加一个基于句法模式的特征——比如“我经常/我从不”这类行为陈述句对“尽责性”有强信号。dpi=150足以看清散点分层,不需要更高。
如果你的业务场景不是社交媒体,而是电商评论、客服工单或问卷开放题,这套222维特征加DNN的流水线不需要改动架构,只替换数据清洗阶段的正则规则和分词词典即可。特征维度越高,DNN相对SVM的优势越显眼,那是因为DNN的参数组合能力在稠密特征空间里更容易发挥。做实验时建议固定随机种子,最多跑三轮取均值——文本数据的方差本身就大,单轮结果不具备参考意义。
本文还有配套的精品资源,点击获取