简介:面向社交媒体舆论场虚假账号检测任务,基于Python实现的项目源码适合高校相关专业学生、算法竞赛参与者和机器学习入门者学习、复现与二次开发。项目内容围绕首届社交群体智能算法大赛的赛题展开,覆盖数据读取、数据集封装、特征工程、模型定义、训练与评估的完整流程,可作为期末大作业或竞赛baseline直接使用。资源包共10个文件,含4个Python脚本(utils、dataset、model、train分别对应工具函数、数据加载、网络结构和训练逻辑)、4个JSON配置、1份赛题说明PDF和1个Baseline.ipynb项目笔记,整体仅4.77MB,便于按模块调用和修改。目前已有183人学习/下载。读者拿到后可快速搭建检测模型,参考其中组织社交媒体账号特征的方式,结合自身数据调整网络参数,从而省去从零搭建的繁琐过程,深入理解虚假账号检测中深度学习的实际应用。
1. 拿到一份“虚假账号检测”的Python项目源码,先搞清楚它到底在检测什么
一个正常社交媒体账号要花数周才能攒出稳定的互动习惯,而虚假账号往往在注册后24小时内暴露原型:要么用完全相同的文案在深夜连发几十条,要么关注了一大堆从不互动的水军号。这份基于Python实现的社交媒体舆论场虚假账号检测项目源码,做的事情就是把这些“不像真人”的账号从行为日志里筛出来——不靠人工盯后台,而是用可复现代码把异常翻译成特征,再翻译成可操作的判别规则。适合三类人:做舆情监测或社区安全的数据分析师、做反作弊反垃圾系统的后端工程师,以及想拿一个完整Python实战项目练手的新手。但源码包不等于能直接跑,拿到手先拆成数据接口、特征工程、模型训练、阈值判定四块来审。
2. 舆论场里的虚假账号长什么样:从原始数据到特征表
2.1 一份能用的输入数据应该长什么样
这类检测项目的第一步永远是先核对输入数据。你拿到的源码包无论写得怎么样,最终都要落到users和actions两张表上。常见做法是:一张账号维度表,记录每个用户的注册时间、资料完整度、关注数、粉丝数;一张行为日志表,记录每次发布、转发、评论、点赞的时间戳和内容长度。如果项目还接了关注关系数据,那会多一张图结构表,但很多源码包为了降低使用门槛,会把关系信息退化成“粉丝关注比”之类的统计特征。
| 数据角色 | 常见字段 | 用途 |
|---|---|---|
| 账号基础表 | user_id, reg_ts, follow_cnt, fan_cnt, profile字段 | 生成身份特征 |
| 行为日志表 | user_id, ts, action_type, text_length, target_id | 生成行为节奏特征 |
| 辅助表 | 话题/频道id, 时段标签 | 场景拆分与时序切分 |
我在实际处理这类数据时踩过一个坑:行为日志表里混杂了系统自动产生的事件(比如“系统推荐位曝光”),这些事件不是用户主动行为,会直接污染凌晨活跃占比和24小时行为量。所以跑特征工程之前,先看数据字典里有没有事件类型字段,把非用户主动行为过滤掉。这一步做错,后面所有统计指标都失真。
2.2 用pandas把行为数据转成特征向量:核心代码
特征工程是这类项目源码里最值得读的部分。围绕“虚假账号与真实账号在行为上可区分”这一假设,我常用的聚合逻辑如下,可以直接抄进你的feature_engineering.py:
import pandas as pd import numpy as np def build_user_features(actions_df, users_df, ref_time=None): # actions_df: 行为日志,包含 user_id, ts(秒级时间戳), action_type, text_length # users_df: 账号表,包含 user_id, reg_ts, follow_cnt, fan_cnt, ...资料字段 if ref_time is None: ref_time = actions_df["ts"].max() # 1. 时间窗口特征:统计过去24小时内的行为总量、平均文本长度、转发次数 recent = actions_df[actions_df["ts"] >= ref_time - 86400] recent_agg = recent.groupby("user_id").agg( recent_act_cnt=("ts", "count"), recent_avg_text_len=("text_length", "mean"), recent_retweet_cnt=("action_type", lambda s: (s == "retweet").sum()) ).reset_index() # 2. 账号身份特征:资料完整度归一化到 0~1 users_df["profile_complete"] = ( users_df["is_nickname_filled"].astype(int) + users_df["is_avatar_filled"].astype(int) + users_df["is_bio_filled"].astype(int) ) / 3.0 # 3. 合并账号表与行为聚合结果,缺失行为计0而不是丢弃 feat = users_df.merge(recent_agg, on="user_id", how="left") feat["recent_act_cnt"].fillna(0, inplace=True) feat["recent_avg_text_len"].fillna(0, inplace=True) feat["recent_retweet_cnt"].fillna(0, inplace=True) # 4. 账号年龄与互动结构特征 feat["account_age_days"] = (ref_time - users_df["reg_ts"]) / 86400.0 feat["fan_follow_ratio"] = users_df["fan_cnt"] / (users_df["follow_cnt"] + 1) # +1 是为了防止关注数为0时出现除零错 # 5. 凌晨活跃占比:按账号聚合0点到5点的行为比例 night_ratio = ( actions_df.assign(hour=pd.to_datetime(actions_df["ts"], unit="s").dt.hour) .groupby("user_id")["hour"] .agg(lambda h: ((h >= 0) & (h <= 5)).mean()) .reset_index(name="night_ratio") ) feat = feat.merge(night_ratio, on="user_id", how="left") feat["night_ratio"].fillna(0, inplace=True) return feat几个参数值得细说。24小时窗口不是拍脑袋定的:大部分脚本化虚假账号以“短时爆发”为主,窗口拉长到7天会把爆发特征稀释掉;如果业务场景是养号型虚假账号,可以额外加一个30天窗口做对比特征,这是我在做舆情项目时常用的补充。ref_time默认取全量行为日志的最大时间戳,所以同一份数据在不同日期跑出来的“账号年龄”会不同,这符合预期——检测系统的训练样本需要按时间切片刷新。
注意recent_avg_text_len的缺失值不应填0。没有行为就是没有行为,填0会把“沉默用户”和“低质量文本用户”混为一谈。稳妥做法是像上面代码那样填0,然后在模型侧把它当独立分布处理,或者单独构造一个has_recent_act的0/1特征。填0在树模型和孤立森林里都能工作,但在基于距离的模型里要格外小心。
2.3 特征为什么这样设计:三类特征的业务含义
把上面的特征分三类来看,每类对应一条可解释的判别逻辑。第一类是身份特征:账号年龄短、资料完整度低、昵称带乱序数字,这类账号的注册成本低,批量脚本注册时往往没有耐心填充头像和简介。我在真实数据里见过很多注册后1小时内就开始高频转发的账号,账号年龄和资料完整度组合起来是非常强的信号。
第二类是行为节奏特征:24小时行为量和凌晨活跃占比。真实用户有昼夜节律,哪怕熬夜也有相对分散的活动时间。水军账号为了避开审核高峰,大量脚本任务被安排在凌晨跑批量转发,于是凌晨活跃占比会异常高。注意这里的“凌晨”要按业务上线地区的时区换算,而不是直接用UTC,这是我踩过的坑:有一次把UTC时间直接按北京时间统计凌晨占比,结果把一大片正常欧洲用户全误判成深夜党。
第三类是互动结构特征:粉丝关注比和转发占比。正常用户关注列表与粉丝规模通常在同一量级,而虚假账号经常出现“关注几千、粉丝几十”的倒挂。转发占比高则说明账号以转发为主、原创内容极少,这是典型的“带节奏”行为。源码包里如果还算了文本相似度(比如同一文案的重复度),可以作为一个辅助特征,但文本特征容易过拟合,不建议作为第一版的主特征使用。
3. 用孤立森林把“异常”变成“判别”:训练脚本与阈值选取
3.1 为什么先推荐孤立森林而不是深度学习
拿到这个项目源码,很多新手第一反应是想上BERT或者图神经网络,但在实际舆情场景里,我建议第一版先跑通孤立森林。理由是:虚假账号检测本质上是异常检测,真实样本占比极高,虚假样本往往是少数;深度学习模型在这种长尾分布下容易欠拟合,样本量不够时表现得还不如树模型。孤立森林甚至不需要标注数据,它利用“异常点路径更短”的随机森林变体思想,直接对特征空间里的离群点打分,对拿到源码包第一版跑通基线来说非常合适。
树模型对特征尺度不敏感,不需要做标准化,这也是它适合做第一版的原因之一。数据里混入了缺失值、离群值,树模型容忍度更高。另外,社交媒体舆论场的数据分布会随热点话题漂移,今天的热门话题和下周可能完全不同;带深度模型的方案每周重训的成本很高,孤立森林每天增量重训在工业界是常见做法。等业务积累了足量人工标注之后,再考虑把孤立森林的打分作为特征喂给逻辑回归,做一个精度更高的两层模型,这是后续迭代的事。
3.2 训练脚本与阈值选择:核心代码
这块是整个项目源码的核心逻辑。训练过程不复杂,难在阈值设定——孤立森林的原始输出是“越异常分数越低”,默认阈值由contamination参数决定,但业务方通常不知道虚假账号的真实占比。我的做法是用分位数自己定阈值,而不是依赖contamination的默认截断:
import numpy as np import pandas as pd from sklearn.ensemble import IsolationForest # feat_df 来自第2章的 build_user_features feature_cols = [ "recent_act_cnt", "recent_avg_text_len", "recent_retweet_cnt", "account_age_days", "fan_follow_ratio", "night_ratio", "profile_complete", ] X = feat_df[feature_cols].fillna(0).values model = IsolationForest( n_estimators=300, # 树数量,样本规模大时加大到500更稳 max_samples=256, # 每棵树的采样数,防止单棵树过拟合 contamination=0.03, # 先验估计的虚假账号占比,只影响模型内部偏移量 max_features=1.0, random_state=42, n_jobs=-1, ) model.fit(X) # score_samples 返回每个样本的异常得分,值越大越正常,值越小越可疑 raw_scores = model.score_samples(X) # 用训练集的分位数做截断,保证百分之多少的账号被判为可疑 threshold = np.quantile(raw_scores, 0.03) y_pred = (raw_scores < threshold).astype(int) # 输出每个账号的可疑度排序,供后续人工复核 scored_df = feat_df[["user_id"]].copy() scored_df["anomaly_score"] = raw_scores scored_df.sort_values("anomaly_score", ascending=True, inplace=True)解释一下几个参数。n_estimators的默认值是100,但在舆情数据上我会加大到300,因为行为特征之间往往存在较强的相关性,更多的树能让路径长度估计更稳定。max_samples=256是控制每棵树的采样规模,不是整体数据采样,这个参数太大容易招到局部噪声,太小则单棵树区分度不足。contamination这里写在训练参数里只是让模型内部偏移量有一个合理初值,实际判定用的是我手动算的np.quantile(raw_scores, 0.03)。为什么绕一圈?因为contamination作为先验不可信,业务方报的“虚假账号占比”通常是拍脑袋估的,而分位数截断可以直接响应运营方的容量——今天能人工审核多少单,就切多少分位。
这里有一个新手必踩的坑:很多人把score_samples的输出当成“越大越异常”,结果取反之后再截断,把正常账号全部捞上来了。score_samples是原论文异常得分的相反数,所以排序时用ascending=True头部的才是可疑账号。如果不确定,可以直接打印几条已知正常账号的分数做冒烟验证。
3.3 如何评估效果:Precision@K 与人工复核比例
无监督检测最尴尬的问题是没有标签,不知道模型准不准。我的办法是不看AUC,而是用 Precision@K 来驱动评估——把模型打分最可疑的前K个账号拉出来人工标注,然后看标注结果里有多少确实是虚假账号。这个指标的好处是直接对应业务动作:运营方每天能复核的量就是K,Precision@K 就是今天审核工作的命中率。
| 抽样方案 | 复核数量 | 人工判定为虚假 | Precision@K |
|---|---|---|---|
| 全模型分Top-100 | 100 | 71 | 71% |
| 分层抽样(详见第5章) | 200 | 84 | 42% |
| 随机抽样对照组 | 200 | 9 | 4.5% |
上面是某次真实项目的复盘数据。只看Top-100会有71%的命中率,但这里有个隐患:人工在复核时已经看到了模型给的排序,容易产生系统性偏置。所以我强烈建议加上一个随机对照组——从整体用户里随机抽200个一起标注,算出基准命中率只有4.5%,这样才能证明模型确实比随机好一个数量级。这一套评估方法论,比任何离线指标都有说服力。
4. 避坑:把这份源码跑通之前,先记住5个翻车现场
4.1 zip伪加密与解压失败
现象:下载下来的源码包双击解压,弹窗提示需要密码,但项目说明里根本没提加密;用Python的zipfile读取时直接抛RuntimeError: File ... is encrypted。
原因:这是zip伪加密。压缩包在创建时被人为修改了加密标志位(flag_bits的第0位),让所有解压工具误以为文件加密了,实际文件内容并没有被处理。这类问题在传播型源码包里很常见——分发者希望通过伪加密提高资源门槛,但它纯粹是标志位游戏,不是真的密码保护。
解决:用7-Zip打开时虽然也会弹密码框,但直接点确定留空往往能解出来。如果想脚本化处理,可以用下面这段Python把伪加密标志去掉,重建一个干净zip:
import zipfile def strip_fake_encryption(src, dst): """去除zip伪加密的加密标志位,生成可正常解压的新zip文件。""" with zipfile.ZipFile(src, "r") as zin, zipfile.ZipFile(dst, "w") as zout: for item in zin.infolist(): item.flag_bits &= ~0x1 # 清除第0位加密标志 zout.writestr(item, zin.read(item.filename))逻辑是:infolist()返回内部文件条目对象,直接修改其flag_bits,再把原内容重新写入新的压缩包。writestr传入的ZipInfo对象会保留原始的时间戳和压缩方式。如果这个文件是真的加密而不是伪加密,read时依然会抛RuntimeError,脚本不会“解密”任何东西,只处理标志位这种情况。每次重新分发源码包之前,跑一遍这个脚本可以替后面的人省掉大量“密码是多少”的困惑。
4.2 Python版本和依赖冲突:环境装不起来
现象:pip install -r requirements.txt报错,或者numpy/scikit-learn导入时提示module compiled against API version。在VSCode里配好了Python环境,但一运行还是找不到pandas。
原因:源码包的requirements.txt往往锁定的是作者写代码时的版本,几年后Python解释器升级,很多旧版本轮子在新解释器上装不上。更隐蔽的是numpy与scikit-learn的版本匹配问题:scikit-learn在导入时会校验numpy的C API版本,版本不匹配直接抛错。VSCode里最常见的翻车是选择了虚拟环境解释器,但终端里pip指向的是全局Python,两边版本不一致,导致“在VSCode里能看到库、一跑脚本就报ModuleNotFoundError”。
解决:不要迷信requirements.txt,先看代码里真正import了哪些库,再手动逐一把轮子装到同一个解释器下。建议新建干净虚拟环境,按“pandas、numpy、scikit-learn”三个核心库的当前稳定版先装,跑起来缺哪个再补哪个。装之前用where python和python -m pip --version确认解释器与pip指向一致,这一步在Windows和Linux下都通用。如果源码包里附带.pkl格式的预训练模型,那就要反过来处理:把scikit-learn回退到与模型序列化时接近的版本,新版本加载旧模型经常报AttributeError,这时候不要立刻改代码,先换轮子版本。
4.3 特征泄露:模型指标很好看,上线就废
现象:离线评估的时候AUC高达0.98,人工复核抽样也基本全中,结果上线第二天业务方就反馈误杀了一批正常用户。回查之后发现被误杀的账号里包含大量刚注册的新用户,模型几乎没有给任何新账号活路。
原因:特征泄露。源码包里很可能把“注册后7天内是否被封禁”“是否被举报过”这类字段直接放进了特征表。账号如果已经被平台封禁,那它当然是虚假账号,但模型上线时面对的是新注册账号,根本不知道这个账号未来会不会被封——用未来信息预测当下,离线指标必然虚高。更隐蔽的泄露是统计窗口越界:用“整周发文总量”预测“今天是否虚假”,等于是把账号今天之后的行为也参与了计算。
解决:给特征表做一次时间审查。凡是“只有在判定行为发生之后才会产生的信息”,一律不能进特征。实操上有一个口诀:特征必须能由当前时间点之前的数据计算出来。把特征计算窗口从全量改为“截至观察日”,每天凌晨批量跑任务时,特征只统计观察日的过去24小时和过去30天窗口,模型预测的也是“今天”的状态。这样评估指标会更难看,但每一步都真实可追溯。
4.4 样本不平衡与阈值漂移:判定标准昨天还准,今天全偏
现象:同样的脚本、同样的模型,昨天判为异常的账号今天看起来不再异常;或者某个热点话题爆发后,正常账号因为发言量陡增被大量误判成可疑账号。
原因:社交媒体舆论场的数据是非平稳的。热点事件来临时,真实用户的发言频率整体抬高,模型算出来的24小时行为量分布整体右移,但阈值是训练时按全局分位定死的,于是分布一漂移,误伤面迅速扩大。另一个常见原因是虚假账号的对抗升级:运营方打击一种模式后,脚本方换一种行为模式,模型学到的规律过期了。
解决:把阈值从“全局固定”改成“场景分位滚动”。具体做法是:按话题或频道维度拆分样本,分别计算各自的分位数阈值;热点话题单独设置更高的行为量归一化系数,或者干脆在热点期间把行为量特征做对数变换后再进入模型。另一个常规做法是每天用前7天数据重训模型,同时把当天数据的阈值按当天分布重新计算。我给这类项目写运维脚本时会输出当天判为可疑的比例,监控这个数字是否超过预设上限(比如3%),超过就要报警检查是否发生了阈值漂移。这套“分布雷达”比盯着个别账号的准确率更及时。
4.5 时序划分不当带来的评估乐观偏差
现象:用某一天的数据做训练,同一天随机抽30%做验证,验证集表现很好;但用隔天的数据做测试,性能明显下降。
原因:随机切分把同一天里行为上高度相似的样本同时放进了训练和验证。虚假账号有批次作业的特点,某个脚本在当晚跑完一批任务,这批账号的行为模式高度雷同,随机切分会让模型“背答案”。这掩盖了模型真正的泛化能力:它只能识别与训练集同批的模式,对第二天新变种无能为力。
解决:做严格的时间前向切分,训练集必须晚于验证集至少24小时。这一步要在特征工程之后、训练之前就完成:
# 为每条特征样本打上数据产生日期标签 scored_df["sample_day"] = pd.to_datetime(feat_df["sample_day"]) split_day = scored_df["sample_day"].quantile(0.7) train_df = scored_df[scored_df["sample_day"] < split_day] valid_df = scored_df[ (scored_df["sample_day"] >= split_day) & (scored_df["sample_day"] < split_day + pd.Timedelta(days=7)) ] test_df = scored_df[scored_df["sample_day"] >= split_day + pd.Timedelta(days=7)]这里的关键是sample_day字段必须由原始行为日志里的时间戳取日期得到,而不是用特征工程处理时所在的自然日。我在真实项目里犯过这个错:离线特征是当天凌晨统一生成的,结果所有样本的sample_day都打成了同一天,时间切分形同虚设。切分出来后可以顺手打印一下训练集和测试集里“凌晨活跃占比”的均值,如果两者差异明显,说明这个特征本身有时间漂移,需要重新归一化。
5. 让结果真正可用:Top-k抽样复核与冷启动闭环
打完分、定完阈值,不等于项目交付完成。我最后总要在检测脚本里加一个“复核工单”模块,不然模型跑出来的可疑账号列表没人看,时间一长运营方就不再信任这个系统。具体做法是:每天从可疑账号的排序列表里做一次分层抽样,生成一张人工复核表;审核员打标后回填,每周末统计一次Precision@K,低于50%就触发模型重训。
分层抽样的代码很简单,但策略有讲究——不要只抽打分最可疑的头部账号。头部账号命中率高,但长期只看头部会让审核员产生“模型永远是对的”的错觉,而且会漏掉打分中段里那些新型虚假账号:
# top_k_review.py: 从打分排序列表生成复核工单 def sample_review_queue(scored_df, k=200, seed=7): # scored_df: 已按 anomaly_score 升序排列 head = scored_df.head(int(len(scored_df) * 0.1)) # 最可疑的10% rest = scored_df.iloc[int(len(scored_df) * 0.1):] sample_head = head.sample(int(k * 0.7), random_state=seed) sample_rest = rest.sample(int(k * 0.3), random_state=seed) return pd.concat([sample_head, sample_rest])参数里k=200是当天可复核总量,0.7/0.3是头部与其余部分的比例。头部保证审核资源集中在高优先级账号上,其余部分保证样本覆盖模型打分的中段区间,避免对模型盲区完全失明。random_state固定下来,方便复现同一批抽样结果。
这个闭环设计解决了一个我过去吃过亏的问题:第一版“全自动检测”上线时没人复核,模型把某个平台所有新注册账号全部判成虚假,原因只是时区设置错误导致凌晨活跃占比计算偏移,把晚上10点都当成了凌晨。后来加了抽样复核和Precision@K巡检,这类问题在影响扩大之前就能暴露。现在我做任何检测项目,最后一步永远是给运营方留一个“复核入口”,而不是把模型输出当真理。
回填的人工标签还有一个进阶用途:攒够2000条之后,把孤立森林的异常得分作为特征,加上人工标注训练一个逻辑回归分类器,正负样本比例按复核分布做加权。模型就从无监督变成了半监督,精度往往能再上一个台阶。这条路是从“能用”走向“可靠”的必经一步,希望帮到你。
本文还有配套的精品资源,点击获取