☰
MBTI人格预测系统实战:文本多分类与TF-IDF特征工程全解析
2026/10/10 14:15:31 网站建设 项目流程

简介:面向MBTI人格预测的机器学习项目资源,涵盖数据清洗、特征分析、模型训练与评估、前端界面集成等完整开发链路,适合希望从零搭建人格分类系统的数据科学学习者、算法工程师以及毕业设计开发者参考。压缩包共2000个文件,大小253.46MB,其中1894个py为算法与界面主体,39个c、29个h等C扩展文件多与底层数值计算相关,txt、docx、md等文档则包含项目开发计划、可行性报告与说明;这些源码可直接配合主程序运行。目前已有1819人学习下载,可作为同类项目实践与二次开发的参照案例。除完整代码外,还提供数据预处理、特征工程、模型调参评估等环节的实现思路,能帮助理解数据清洗、文本向量化、标签编码、模型选择和系统集成的工程化做法,对构建基于NLP或文本特征的人格预测、情感分析等应用很有参考价值。

1. 一句「你是 INFP 吧」背后不全是玄学:MBTI 人格预测系统真正要解决什么

当别人开口就是“你肯定是 ENTP”时,你未必想得到一个心理测试结果——你遇到的可能是基于机器学习的 MBTI 人格预测系统。它的本质是文本多分类:把用户的社交发言、问卷自由文本当作输入,预测出 16 型人格标签中的一种。适合三类人:刚学完分类算法、想找一个能落地的机器学习项目的初级工程师;想做轻量人格初筛的运营;在 NLP 特征工程上练手的学生。真正做完一轮你会同意一个反直觉结论:这个项目成败七八成在数据整理与评价口径,只有两三成在算法选型。当它被当作机器学习期末复习项目或毕业设计选题时,也比常见的手写数字分类更接近工业界的脏数据现实。

2. 数据清洗:把用户帖子转成不泄露的「标签桶」,三个动作直接决定模型上限

从任何渠道拿到 MBTI 语料,结构都差不多:一个user_id对应一个type标签,这个 id 名下挂着少则几条、多则几百条帖子。很多人抱着“先清词、再建模”的顺序,第一步就写停用词表,结果后面全部返工。我一般会反过来:先解决标签和样本的对应关系,再回来清文本。因为这一个动作决定了切分合不合法,也决定了你测试集上的准确率是“能信”还是“只能看看”。

2.1 为什么不能拿一条帖子当一条样本:用户、帖子、标签的依赖关系

最常见的数据流是直接把每条帖子当一行丢进训练集。从文件格式看好像每个样本都有独立性,但 MBTI 语料的独立性在用户层面,不在帖子层面。同一个人发的几百条帖子共享同一个标签,写法高度雷同。如果直接按帖子随机切 80/20,同一个用户的帖子会同时出现在训练和测试里,模型更像在背这个人的写作模板,而不是在学习人格类型。

先按用户去重,再按用户切分,基本代码长这样:

import pandas as pd from sklearn.model_selection import train_test_split # 每条原始记录里带着 user_id / type / post 三列 df = pd.read_csv("mbti_raw.csv") # 以 user_id 为粒度,拿到“一个人对应一个标签”的主表 user_labels = ( df.groupby("user_id")["type"] .first() .reset_index() ) # 按用户主表做分层切分,帖子表再跟过去 train_users, test_users = train_test_split( user_labels, test_size=0.2, stratify=user_labels["type"], random_state=42, ) train_df = df[df["user_id"].isin(train_users["user_id"])] test_df = df[df["user_id"].isin(test_users["user_id"])]

这里最要紧的是stratify=user_labels["type"]。MBTI 十六型的样本量天然不均,如果某个类型只有五六个用户,不按标签分层,很容易在测试集里一个都分不到;分层之后能保证每个类型在切出来的两份数据里占比接近。random_state看似只是随机种子,实际是整个实验的后悔药——生产环境重跑、调参前后对比全靠它对齐。

这层做完,还要做一个用户级统计:每个type下的用户数、帖子数、人均帖子数。某个类型只有 3 个用户、200 条帖子时,不要指望模型自己学会这个冷门类。提前决定是删除、合并还是给它单独一个大类标记,比事后看分数解释半天要舒服。

2.2 值得清理和不该清理的文本:链接、缩写、停用词的取舍

做文本清洗前先想清楚:这个任务的关键特征到底是“关键词”还是“表达方式”。MBTI 文本里,句长、人称代词比例、感叹号密度都比某个具体词更有辨识度。所以清洗策略不能照抄新闻分类的停用词表——I、you、am这类英文高频词在人格语料里恰恰是信号。

import re def clean_post(text: str) -> str: # 去掉 [link]、[removed]、[表情] 这类角括号标记 text = re.sub(r"\[.*?\]", " ", text) # URL 替换成占位 token,而不是直接删掉 text = re.sub(r"http\S+", " <url> ", text, flags=re.I) # 连续标点压缩成单标点 text = re.sub(r"[.!?]{2,}", " . ", text) # 统一空白,最后再小写;缩写不展开,大写信息并入词形 text = " ".join(text.split()).lower() return text

两个经验点。第一,[link]这类标签与发帖平台强绑定,删除前要确认训练语料的来源;如果原始数据里链接本身就是某类用户习惯大段引用的表现,全删会丢一个弱特征,所以我宁可替换成<url>占位符,既避免超长 URL 干扰 TF-IDF 的词频,又保留它出现过的位置信息。第二,不要做词干化,也别开英文停用词表。词干化把thinking和think合并,看着省维度,实则把“思维方式”的时态差异洗没了;而停用词表会把第一人称I直接滤掉,等于把最有性格辨识度的代词全扔了。

2.3 切分之后必须做的三类统计检查

切完不要急着建模,先做三分钟检查。第一,验证集和测试集的user_id交集必须为 0;第二,训练集和测试集的type分布要和全量分布接近;第三,随机抽几条清洗后的文本,确认不是空串或只剩一个 URL。

# 检查一:用户集合不能有交集 assert not set(train_df["user_id"]) & set(test_df["user_id"]) # 检查二:训练集标签分布与全量分布比例差距 dist_all = df["type"].value_counts(normalize=True) dist_train = train_df["type"].value_counts(normalize=True) print((dist_all - dist_train).abs().max()) # 检查三:清洗后空文本数量占比 blank_ratio = df["cleaned"].str.strip().eq("").mean() print(f"blank ratio: {blank_ratio:.4f}")

第二项检查很多时候比模型指标更早暴露问题。如果训练集里 F 型比例被抽得偏高,模型自然会对 F 型更友好,线上回访发现准确率结构性下降,溯源时往往就是这一步没看。建议把这组检查写成一个validate_split()函数,每次切分完都跑,不要靠肉眼看。

2.4 不均衡样本的三条处理路径

第一个选择是降采样:按用户主表把多数类用户随机抽到和少数类接近的数量。第二个选择是过采样,但文本分类里复制原帖会让模型背诵重复样本,只在少数类实在少时兜底。第三个选择是给损失函数加权,在 sklearn 模型里就是class_weight="balanced"。三者取舍非常直接:16 类里少数类用户低于 20 时,任何重采样都会引入明显噪声,我一般优先选class_weight,而不是造新句子。

3. TF-IDF 与线性基线:先让朴素贝叶斯跑通,再谈机器学习模型选型

许多机器学习入门教程会把第一个模型直接扔给神经网络,但 MBTI 预测这套文本分类,统计特征基线必须先立住。原因有二:一是 16 类多分类对数据量的要求比二分类高得多,小语料上复杂模型容易过拟合;二是 TF-IDF 加线性模型推理极快,一分钟可以跑完一次全量训练,方便反复验证清洗和切分的正确性。等统计基线稳定,再往上叠机器学习模型才有可比性。

3.1 人格预测的判别特征不是关键词,而是表达方式

在 MBTI 语料上,两类用户的用词有差别,但更稳定的差别在“怎么说”。内向型更常以I think、I feel开头,外向型更常出现we should、let's这类集体行动句式;高判断倾向的人句子里的感叹号和祈使句占比更高。这些特征藏在词序和人称代词里,是与内容主题无关的稳定信号。

通常会从清洗后的文本抽出五个语言结构特征:

from nltk import word_tokenize def extract_style_feats(text: str) -> dict: tokens = word_tokenize(text.lower()) if not tokens: return {} return { "token_len": len(tokens), # 句长均值代理 "char_len": len(text), "i_ratio": tokens.count("i") / len(tokens), # 第一人称单数占比 "we_ratio": tokens.count("we") / len(tokens), # 第一人称复数占比 "exclaim_ratio": text.count("!") / max(1, len(text)), "question_ratio": text.count("?") / max(1, len(text)), "paren_ratio": text.count("(") / max(1, len(text)), }

这些特征要直接横向拼接在 TF-IDF 矩阵后面,或者单独作为另一组输入喂给线性模型。关键点是,人称比例特征都基于每条文本自身统计,不依赖全量语料,因此天然不容易泄露;但如果你用 sklearn 的StandardScaler做归一化,就必须先 fit 训练集再 transform 测试集,否则均值和方差又被测试集污染了。

3.2 n_gram 和 TF-IDF 的五个必调参数

TfidfVectorizer不是开箱即用,它在 MBTI 这类短文本上的默认参数表现一般。下面是我常用的一组配置和每条参数的理由:

from sklearn.feature_extraction.text import TfidfVectorizer vectorizer = TfidfVectorizer( max_features=15000, # 特征维度上限,控制训练内存 ngram_range=(1, 2), # 只看单词和双词词组 min_df=3, # 至少在3条帖子中出现 max_df=0.7, # 去掉超过70%帖子都出现的词 sublinear_tf=True, # tf 改 1+log(tf),压制长文 strip_accents="ascii", # 统一重音字符 ) train_vec = vectorizer.fit_transform(train_df["cleaned"]) test_vec = vectorizer.transform(test_df["cleaned"])

测试集用的是transform而不是fit_transform,因为测试集只能用训练集拟合出的词表转换,否则 IDF 统计会带入测试信息。max_features=15000是我在万级帖子语料上的经验值;如果语料小,可以降到 8000 加快实验;如果语料更大,把上限提到 30000 并不会立刻让模型变好,因为 2-gram 基本都是低频长尾。

ngram_range=(1,2)是与任务绑定最强的选择。3-gram 在 MBTI 帖子上的边际收益极小,却让矩阵稀疏度迅速升高,模型训练时间成倍拉长。如果你确实要抽i think that这类固定搭配,建议单独做一组短语特征,而不是整体把 ngram 上限提到 3。

3.3 逻辑回归和朴素贝叶斯对比:基线分数怎么读

基线实验至少跑两个模型:MultinomialNB和LogisticRegression,两个都看准确率与宏平均 F1。朴素贝叶斯在短文本上通常能给出一个不差的起点,逻辑回归则更适合做特征权重解释。

from sklearn.naive_bayes import MultinomialNB from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, f1_score def evaluate(clf, X_train, y_train, X_test, y_test): clf.fit(X_train, y_train) pred = clf.predict(X_test) return { "acc": accuracy_score(y_test, pred), "macro_f1": f1_score(y_test, pred, average="macro", zero_division=0), } for name, model in [ ("nb", MultinomialNB(alpha=0.1)), ("lr", LogisticRegression(C=1.0, max_iter=2000)), ]: score = evaluate(model, train_vec, y_train, test_vec, y_test) print(name, score)

看结果时有一个很实用的判断标准:如果LogisticRegression和MultinomialNB的差距小于两个百分点,说明当前特征基本被模型潜力摸透了,加模型容量未必有用;如果 LR 明显输给 NB,先怀疑数据清洗,再怀疑特征,最后才怀疑模型结构。原因是朴素贝叶斯对“不相关特征”有天然鲁棒性,而 LR 会给每个特征分配权重,特征质量差时反而容易被噪声带着跑。

3.4 升级到线性 SVC:什么时候该赢,什么时候只是在浪费时间

接着再用一版LinearSVC对比,是常规操作。SVC 没有predict_proba,所以后面如果要做 Top-3 概率排序,通常直接用 LR 的概率输出。

from sklearn.svm import LinearSVC svc = LinearSVC( C=0.5, max_iter=5000, class_weight="balanced", random_state=42, ) svc.fit(train_vec, y_train) print("svc acc:", accuracy_score(y_test, svc.predict(test_vec)))

如果 SVC 和 LR 的成绩落在一两个点以内,说明当前数据与特征下线性模型已接近天花板,赶紧收手。继续调 C 只是在浪费时间。很多机器学习实战项目会把 SVC 换成 RBF 核,但在 TF-IDF 稀疏矩阵上 RBF 核训练极慢,而且效果基本不会超过线性核,不必为了“算法听起来更高级”去换。

4. 避坑与排查:五个把测试集分数“捧高”的操作,我在实战里全踩过

以下五条坑,每一条都按“现象 → 原因 → 解决”的顺序写。如果你最后模型的指标好得不像话,先回来看一遍这份清单,大概率能对上一个。

4.1 按帖子切分:模型学到了“人设”而不是人格类型

现象:随机按帖子做训练/测试切分时,测试集 F1 比按用户切分凭空高出 15 到 20 个百分点,看起来性能很好,换了一组全新用户后指标立刻崩掉一半。

原因:同一用户的多条帖子被同时分进训练和测试两个集合,训练时模型已经把该用户的写作模板背下来,预测时直接套用这套模板,而不是判断人格类型。MBTI 帖子的模板化程度比普通新闻文本高得多,整段文字的口癖重复率极高,因此这种泄露造成的虚高幅度很明显。

解决:统一按user_id切分,训练、验证、测试三份的用户集合互斥。如果一篇文章由多帖拼接而成,拼之前就先把同 user 的帖子合并成文档,再按 user 划分,不要在合并后误以为它们是互不相关的独立样本。

4.2 不带 stratify 的随机切分:少数类在测试集里直接消失

现象:训练完打印测试集标签分布,16 个类型里只出现了 10 个左右,模型遇到未出现的类型时全部预测成多数类。

原因:默认train_test_split是无放回随机抽样,不控制标签分布,稀疏类型能否出现在测试集完全取决于它样本量。在只有几帖的冷门类型上,随机一抽就可能抽没了。

解决:切分时始终加stratify=y;按用户切分时,要先对用户主表做分层。注意一个细节:分层函数的输入应该是一个用户一个标签,而不是一条帖子一个标签。若某类型总用户数实在不足,删除该类型或临时合并为“其他”,并在实验记录里写明,不要默默处理。

4.3 先在全量语料上 fit TF-IDF,再做切分:测试信息已经进训练集

现象:交叉验证和最终测试集指标都极高,可换一批数据表现骤降;换用只在训练集上 fit 的向量化,分数立刻回落。

原因:TfidfVectorizer的词表、IDF、文档频率都由全量语料统计而来。切分前对全量做一次fit_transform,等于测试集里每个词在训练阶段都被看到过。这不算标签泄露,却是典型的特征泄露。

解决:切分后严格按“训练集 fit_transform,测试集 transform”的顺序执行。整个流程放进Pipeline可以避免误操作;如果已经踩了,重新生成向量化器并保存成独立文件,训练和推理共用这一份。

4.4 反复用同一份验证集调参:验证集被“用脏了”

现象:调参过程中验证集指标一路微涨,最终测试集反而掉点;继续加大轮次,验证集仍然上涨,测试集却不反弹。

原因:验证集上每调一次参数就多一次观察,模型选择过程本身在朝验证集过拟合,验证集不再代表未知数据。小样本场景下,这种污染在第四五次实验后就会显现。

解决:固定验证集只做终审,训练时用 K 折交叉验证做参数寻优。把每次实验的随机种子、参数、指标写进一张实验清单,避免同一参数组合反复跑。在几千条样本量级上,必要时要限定调参次数,比如主参数不超过三轮,防止把交叉验证也调到过拟合。

4.5 推理端没有同步预处理:标签和特征两头对不上

现象:离线评估很好,线上服务拿到用户提交文本后却预测成一堆同一标签;排查后是线上代码少了小写或角括号清理。

原因:离线训练和在线推理走了两条清洗流程,能分词的内容不一样。模型输入是i feel,线上输入是I FEEL,线性模型的特征权重对不上,等于换了一个词表。

解决:把清洗函数封装成同一个独立模块,训练和预测共用;标签也统一规范化,人写 “INTJ”,模型输出 “intj” 时在接口层做一个映射。上线前用三组线上真实文本走一遍推理脚本,直接对比离线预处理后的同一文本,两步输出应该完全一致。

5. 把“单选预测”改成 Top-3 候选排序:交付 MBTI 模型的更好方式

模型做出来不是终点,能让人信服才是。MBTI 有 16 类,在文本长度有限、用户行为复杂的条件下,单点预测的准确率很难高。与其给产品方一个孤零零的标签,不如把任务定义成“给用户一个预测候选列表”,系统可信度会明显上升。

5.1 从 predict 到 predict_proba:拿置信度排序代替硬分类

直接输出标签,相当于掩盖了模型对其他 15 类的判断。我给演示系统写过一个函数,把逻辑回归的预测概率转成 Top-3 排序:

import numpy as np def predict_topk(model, vectorizer, label_encoder, texts, k=3): vec = vectorizer.transform(texts) proba = model.predict_proba(vec) top_idx = np.argsort(-proba, axis=1)[:, :k] result = [] for i, row in enumerate(top_idx): result.append( [ (label_encoder.classes_[idx], proba[i, idx]) for idx in row ] ) return result

np.argsort(-proba)按概率从高到低排列,label_encoder.classes_把下标还原成四字母人格缩写。Top-3 呈现给运营方时,他们看到的不是“模型说你一定是 INFP”,而是“第一可能是 INFP,第二可能是 ISFP,第三可能是 ENFP”,这更符合心理测评的倾向度概念。我一般会把 k 设成 3 或 5,并在交付文档里写明:只把 Top-1 当强信号,Top-2 和 Top-3 当作需要人工复核的模糊区。

5.2 交付前做一张量化对照表

在这个任务里我最想保留的习惯是:每次跑完模型,都从测试集随机抽 30 条样本,连同真实标签、Top-1 预测、Top-3 是否命中整理成一张 CSV 对照表。你很快会从表里发现问题:某个真实类型从不进前三、某几条文本所有候选类型概率都接近,说明模型在学普遍模板而不是人格差异,或者这批文本信息量太低。

刚接手 MBTI 预测项目时,我也爱直接看准确率,后来发现按人切分后准确率只有三成左右的数据集,硬推一个答案并不合适。把输出改成候选排序、把验证改成“Top-3 命中率”,这类系统才更能被使用者接受。这个改动手工写起来不到二十分钟,却是我在多个文本分类项目里返工最少的一次改变。希望这个习惯对你也有效。

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

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

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

立即咨询