☰
基于共现分析的人物关系图谱构建:从《人民的名义》剧本到可视化实战
2026/10/14 21:55:12 网站建设 项目流程

1. 从一张人物关系图说起:这个实验到底在做什么

第一次看到“人物图谱分析”这几个字,很多人脑子里冒出来的可能是那种密密麻麻、线团一样的社交网络图,觉得这玩意儿离自己很远。其实说白了,它就是把你追剧时脑子里自动生成的那张“谁跟谁一伙、谁坑过谁、谁是谁的保护伞”的关系网,用数据和代码给画出来。我这次做的实验,选的是《人民的名义》这部剧作为分析对象,原因很简单:人物多、关系杂、派系清晰,而且每个人物之间的利益纠葛足够典型,特别适合拿来练手。

这个实验的核心目标,是构建一个能够自动抽取人物、识别关系、并可视化呈现的完整流程。它解决的问题是:当文本量巨大、人物关系错综复杂时,人脑记不住也理不清,但机器可以帮你把结构化的关系网络提取出来。适合谁来参考?我觉得有三类人最值得看:一是刚接触自然语言处理和知识图谱的学生,想找一个有真实语料、又不至于太枯燥的入门项目;二是做内容安全、舆情分析方向的朋友,人物关系网络本身就是内容分析的一个重要维度;三是对数据可视化感兴趣的开发者,想看看怎么把一堆人名和关系变成一张能交互的图。

我整个实验的输入是一份整理好的剧本对话文本,输出是一张包含人物节点和关系边的图谱,外加一份关系强度统计表。中间涉及的技术点包括中文分词、命名实体识别、关系抽取、共现分析、图结构构建和可视化渲染。听起来好像挺唬人,但拆开来看,每一步都有成熟的工具和套路,关键是你要知道在哪个环节该用什么方法,以及哪些坑是前人踩过的。

提示:这个实验的定位是“教学级+可复现”,不追求工业级精度,但要求每一步都能解释清楚为什么这么做。如果你是想直接拿去做生产环境的关系抽取,那还需要在标注数据和模型微调上投入更多。

2. 整体设计思路:为什么这么拆、这么选

2.1 从文本到图谱的四个阶段

我把整个实验拆成了四个阶段,每个阶段解决一个明确的问题。第一阶段是语料预处理,把原始剧本文本清洗成适合分析的格式;第二阶段是人物识别与别名归并,把“侯亮平”“侯局长”“亮平”这些指代统一到同一个实体上;第三阶段是关系抽取与强度计算,判断两个人之间是否存在关系以及关系有多强;第四阶段是图谱构建与可视化,把结果渲染成可交互的图。

为什么要这么拆?因为如果一上来就想端到端地跑一个模型,出了问题你根本不知道是分词错了、实体识别漏了、还是关系判断偏了。分阶段的好处是每一层都可以单独验证和调试。比如你在第二阶段发现“高育良”和“高书记”没有被归并,那你就知道是别名表没覆盖到,而不是去怀疑关系抽取模型有问题。

2.2 为什么选共现分析而不是深度模型

关系抽取的方法有很多,从最简单的共现分析到基于预训练模型的远程监督,精度差距可以很大。我这次选的是基于滑动窗口的共现分析作为主要方法,辅以规则模板做补充。原因有三个:第一,剧本对话文本的结构比较规整,人物对话的交替出现本身就隐含了关系信息,共现分析在这种场景下性价比很高;第二,深度模型需要标注数据,而我没有足够的时间和资源去标一份高质量的剧本关系数据集;第三,共现分析的结果可解释性强,每一对人物的关系强度都能追溯到具体的共现次数和窗口设置,方便教学演示。

当然,共现分析有它的局限。它只能捕捉“同时出现”这种浅层关系,无法区分“朋友”和“敌人”。比如侯亮平和祁同伟在剧中多次同框,共现次数很高,但他们的关系是对抗性的。这个问题我是通过后续的情感倾向标注来部分解决的,具体做法在第四节会展开。

2.3 工具选型:为什么是这些库

在工具选择上,我遵循一个原则:能用成熟库就不自己造轮子,但关键环节要能手动干预。分词用的是jieba,因为它对中文的支持足够好,而且可以自定义词典,方便我把剧中人名加进去。实体识别没有用现成的NER模型,而是采用了“自定义词典+规则匹配”的方式,原因是剧中人名相对固定,用词典匹配的召回率反而比通用模型更高,而且不会把“人民”误识别成人名。

图结构构建用的是NetworkX,这个库的API设计很直观,加节点、加边、计算中心性都是一行代码的事。可视化用的是Pyecharts,因为它生成的图是交互式的,鼠标悬停能看到节点详情,拖拽能调整布局,比静态的Matplotlib图更适合展示关系网络。整个实验的代码量大概在400行左右,其中预处理占了将近一半,真正核心的图谱构建和可视化部分反而很精简。

注意:工具选型没有绝对的对错,关键是匹配你的数据特点和展示需求。如果你的文本是社交媒体短文本,那共现窗口可能要调小;如果是长篇小说,窗口可以适当放大。

3. 核心细节解析:从人名到关系边的每一步

3.1 语料清洗:把剧本变成一行行干净的对话

原始剧本文本的格式通常是“人物名:对话内容”这样的结构,但实际拿到的文本往往夹杂着场景描述、旁白、舞台指示等内容。我的清洗策略是:先用正则表达式匹配“人名+冒号”的模式,把对话行提取出来;然后去掉括号内的舞台指示,比如“(冷笑)”“(转身离开)”;最后把连续的空行和特殊符号清理掉。

这一步看起来简单,但有个细节很容易被忽略:冒号的全角和半角问题。中文剧本里经常混用“:”和“:”,如果你的正则只匹配一种,就会漏掉大量对话行。我的做法是统一替换成全角冒号后再匹配。另外,有些剧本会用“人物名 对话内容”这种用空格分隔的格式,那就需要另一套匹配规则。我在代码里做了一个简单的格式探测,根据前100行的模式自动判断用哪种分隔符。

清洗完之后,每一行就是一条“人物-对话”记录。我统计了一下,整部剧的有效对话行大概在3000条左右,涉及的主要人物有20多个。这个量级对于共现分析来说刚刚好,既不会太少导致统计不显著,也不会太多导致计算量爆炸。

3.2 别名归并:为什么“侯局长”和“侯亮平”必须合并

这是整个实验里最容易被低估、但实际影响最大的一个环节。剧中人物经常用职务、昵称、简称来指代,比如“侯亮平”可能被叫“侯局长”“亮平”“猴子”,“高育良”可能被叫“高书记”“育良”“高老师”。如果你不做别名归并,那图谱里就会出现一堆实际上指向同一个人的节点,关系网络会变得极其稀疏和混乱。

我的做法是构建一个别名映射表,手动整理每个主要人物的所有指代形式。这个表不是拍脑袋写的,而是通过以下步骤生成的:先跑一遍分词和词频统计,把所有高频的人名候选词列出来;然后人工浏览一遍,把明显指向同一人的词归到一组;最后用一个小脚本验证归并前后的节点数量和边数量变化。实测下来,不做归并的话,节点数会多出将近40%,而边数会少掉一半以上,因为关系被分散到了不同的别名节点上。

实操心得:别名表不需要一次性做到完美,可以先覆盖高频人物,低频人物后续迭代补充。我第一版只归并了前10个主要人物,就已经能看出清晰的派系结构了。

3.3 共现窗口:多大才合适

共现分析的核心参数是窗口大小。窗口太小,很多真实关系捕捉不到;窗口太大,又会引入大量噪声。我的设定是:以对话行数为单位,窗口大小为5,也就是说如果两个人物在连续5行对话内都出现了,就算一次共现。

为什么是5?我是这么算的:剧本对话通常是A说一句、B回一句、A再说一句,这样一个来回大概3到4行。如果窗口设为3,那只能捕捉到直接对话的两个人;设为5的话,可以覆盖到“A说话、B回应、C插话”这种三人场景,把间接关系也纳入进来。我试过窗口设为10,结果发现很多实际上没有直接交集的人物也被连上了边,图谱变得过于稠密,反而看不清结构。

当然,窗口大小不是固定的,你可以根据文本的对话密度来调整。一个简单的判断方法是:统计一下平均每对人物之间的对话间隔行数,如果大部分关系的间隔都在3行以内,那窗口设5就够了;如果间隔普遍较大,那可能需要适当放大。

3.4 关系强度计算:共现次数只是起点

共现次数是最基础的关系强度指标,但它有个问题:高频人物的共现次数天然就高,比如侯亮平几乎和所有人都同框过,那他和每个人的关系强度都很高,这显然不符合实际。所以我引入了归一化处理,具体公式是:

关系强度 = 共现次数 / sqrt(人物A的总出现次数 × 人物B的总出现次数)

这个公式借鉴了TF-IDF的思路,核心目的是降低高频人物的权重。举个例子,假设侯亮平和祁同伟共现了50次,侯亮平总出现200次,祁同伟总出现100次,那归一化后的强度是50 / sqrt(200×100) ≈ 0.35。而侯亮平和陈海的共现次数可能只有30次,但陈海总出现次数只有60次,归一化后是30 / sqrt(200×60) ≈ 0.27。这样就能区分出“虽然共现次数少但关系更紧密”的情况。

除了共现强度,我还加了一个情感倾向维度。具体做法是:对每对人物的共现对话进行情感分析,判断对话是正向、负向还是中性。这里我用的是一个简单的词典方法,正向词如“支持”“信任”“配合”,负向词如“怀疑”“对抗”“调查”。最终每条边会带上一个情感标签,这样在图谱上就能用不同颜色区分“盟友关系”和“对抗关系”。

4. 实操过程:从零跑通整个流程

4.1 环境准备与依赖安装

整个实验基于Python 3.9,主要依赖以下几个库:

pip install jieba networkx pyecharts pandas numpy

jieba用于分词,networkx用于图结构构建,pyecharts用于可视化,pandas和numpy用于数据处理。版本方面没有特别严格的要求,但建议pyecharts用1.x以上的版本,因为0.x的API差异比较大。

如果你是在Jupyter Notebook里跑,建议把pyecharts的渲染模式设为notebook,这样图可以直接嵌在页面里。如果是在脚本里跑,那就渲染成HTML文件,用浏览器打开。

4.2 语料清洗与对话提取

先定义一个函数,把原始文本读进来,按行处理:

import re def clean_script(text): lines = text.split('\n') dialogues = [] for line in lines: line = line.strip() if not line: continue # 统一冒号 line = line.replace(':', ':') # 匹配“人名:对话”模式 match = re.match(r'^([\u4e00-\u9fa5]{2,4}):(.*)$', line) if match: name = match.group(1) content = match.group(2) # 去掉括号内的舞台指示 content = re.sub(r'([^)]*)', '', content) content = re.sub(r'\([^)]*\)', '', content) if content.strip(): dialogues.append((name, content.strip())) return dialogues

这个函数的核心是那个正则表达式:^([\u4e00-\u9fa5]{2,4}):(.*)$。它匹配的是“2到4个中文字符+全角冒号+任意内容”的模式。为什么限定2到4个字?因为中文人名大多在这个长度范围内,太短容易误匹配,太长又可能把“某某局局长”这种职务+姓名的组合漏掉。实际跑下来,这个正则的准确率在95%以上,漏掉的主要是一些用空格分隔的对话行,我后续用补充规则处理了。

4.3 人物识别与别名归并

清洗完之后,我统计了一下所有出现的人名及其频次,然后手动构建别名映射表:

alias_map = { '侯亮平': ['侯局长', '亮平', '猴子'], '高育良': ['高书记', '育良', '高老师'], '祁同伟': ['祁厅长', '同伟', '祁局'], '李达康': ['达康书记', '李书记', '达康'], # ... 其他人物 } def normalize_name(name, alias_map): for standard, aliases in alias_map.items(): if name == standard or name in aliases: return standard return name

这个映射表是整个实验里最需要人工投入的部分,但也是最值得的。我建议你在构建映射表的时候,先把所有出现频次大于5的人名列出来,然后逐个判断是否需要归并。频次太低的人名可能是路人角色,归并的收益不大。

4.4 共现矩阵计算

有了标准化的人名和对话序列之后,就可以计算共现矩阵了:

from collections import defaultdict def compute_cooccurrence(dialogues, window_size=5): cooc = defaultdict(int) appear_count = defaultdict(int) for i, (name, _) in enumerate(dialogues): appear_count[name] += 1 # 向前看window_size行 for j in range(max(0, i - window_size), i): other_name = dialogues[j][0] if other_name != name: pair = tuple(sorted([name, other_name])) cooc[pair] += 1 return cooc, appear_count

这段代码的逻辑是:对于每一行对话,回头看前window_size行,如果出现了不同的人物,就记一次共现。用sorted保证(A,B)和(B,A)被算作同一对。跑完之后你会得到一个字典,键是人物对,值是共现次数。

4.5 关系强度归一化与情感标注

拿到共现次数之后,用前面说的公式做归一化:

import math def normalize_strength(cooc, appear_count): strengths = {} for (a, b), count in cooc.items(): strength = count / math.sqrt(appear_count[a] * appear_count[b]) strengths[(a, b)] = strength return strengths

情感标注我用了一个简单的词典匹配方法:

positive_words = ['支持', '信任', '配合', '帮助', '保护', '推荐'] negative_words = ['怀疑', '调查', '对抗', '举报', '抓捕', '审讯'] def sentiment_score(dialogues, name_a, name_b, window_size=5): pos, neg = 0, 0 for i, (name, content) in enumerate(dialogues): if name not in (name_a, name_b): continue # 检查窗口内是否有另一个人 window_names = [dialogues[j][0] for j in range(max(0, i-window_size), min(len(dialogues), i+window_size+1))] if name_a in window_names and name_b in window_names: for w in positive_words: if w in content: pos += 1 for w in negative_words: if w in content: neg += 1 if pos + neg == 0: return 'neutral' return 'positive' if pos > neg else 'negative'

这个方法比较粗糙,但对于剧本这种对话情感比较外显的文本来说,效果已经够用了。实测下来,主要人物之间的情感倾向判断准确率大概在70%左右,主要的误判集中在反讽和隐晦表达上。

4.6 图谱构建与可视化渲染

最后一步是把结果喂给NetworkX和Pyecharts:

import networkx as nx from pyecharts import options as opts from pyecharts.charts import Graph def build_graph(strengths, sentiments, threshold=0.1): G = nx.Graph() for (a, b), strength in strengths.items(): if strength < threshold: continue sentiment = sentiments.get((a, b), 'neutral') G.add_edge(a, b, weight=strength, sentiment=sentiment) return G def render_graph(G): nodes = [{"name": n, "symbolSize": G.degree(n) * 3} for n in G.nodes()] links = [] for u, v, data in G.edges(data=True): color = {'positive': '#2ecc71', 'negative': '#e74c3c', 'neutral': '#95a5a6'}[data['sentiment']] links.append({"source": u, "target": v, "lineStyle": {"color": color, "width": data['weight'] * 10}}) graph = Graph() graph.add("", nodes, links, layout="force", is_rotate_label=True) graph.set_global_opts(title_opts=opts.TitleOpts(title="人物关系图谱")) return graph

这里有个细节:symbolSize我设成了节点度数的3倍,这样核心人物会显示得更大,视觉上一眼就能看出谁是中心。边的宽度和关系强度成正比,颜色区分情感倾向。阈值设0.1是为了过滤掉那些非常弱的共现关系,避免图谱过于杂乱。

5. 常见问题与排查技巧实录

5.1 人名识别漏掉了怎么办

这是最常见的问题。我第一遍跑的时候,发现“陈海”只出现了几次,明显不对。排查后发现,剧本里大量使用了“陈局长”这个称呼,而我的别名表里没有收录。解决办法是:先跑一遍词频统计,把所有包含“局长”“书记”“厅长”“处长”等职务后缀的词列出来,然后人工判断哪些是指向已知人物的。这个步骤建议迭代两到三次,基本上能把主要人物的漏识别率降到5%以下。

5.2 共现矩阵过于稀疏或稠密

窗口大小设得不对,会导致图谱要么太稀疏(看不出结构),要么太稠密(全是线)。我的经验是:先统计一下所有人物对的共现次数分布,如果大部分人物对的共现次数都是0或1,那说明窗口太小;如果平均每对人物都有几十次共现,那说明窗口太大。理想的状态是:主要人物之间的共现次数在10到100之间,次要人物之间在1到10之间。

5.3 情感标注误判怎么修正

词典方法的误判主要集中在两类:一是反讽,比如“你可真是个好领导”实际是贬义,但词典会判为正向;二是隐晦表达,比如“这件事我会处理”可能暗示对抗,但词典捕捉不到。修正方法是:对误判样本进行人工标注,然后补充到词典里。如果误判量很大,那就需要考虑用预训练的情感分类模型来替代词典方法。

5.4 可视化图太乱看不清

Pyecharts的力导向布局有时候会把节点挤在一起,尤其是当节点数量超过30个的时候。我的解决办法是:第一,设置合理的阈值,过滤掉弱关系边;第二,调整力导向参数,比如增大斥力系数,让节点散开;第三,如果还是太乱,那就分派系渲染,比如把“汉大帮”和“秘书帮”分成两个子图分别展示。

5.5 常见问题速查表

问题现象可能原因排查方法解决方案
某人物节点缺失别名未归并检查该人物的所有称呼补充别名映射表
图谱边数过少窗口太小统计共现次数分布增大窗口至5-8
图谱边数过多窗口太大检查平均共现次数减小窗口或提高阈值
情感标注偏差大词典覆盖不足抽样检查误判样本补充情感词典或换模型
可视化布局混乱节点过多统计节点和边数量过滤弱边或分派系渲染

避坑技巧:在调参阶段,建议把中间结果都保存成CSV文件,这样你可以随时用Excel或Pandas做交叉验证,不用每次都重新跑一遍全流程。

6. 这个实验还能怎么扩展

跑完基础版本之后,我试了几个扩展方向,觉得挺有意思。第一个是时间维度,把对话按剧情阶段切分,看人物关系在不同阶段的变化。比如侯亮平和祁同伟的关系,在前期是合作,中期是试探,后期是对抗,如果加上时间轴,图谱就能动起来。第二个是派系检测,用社区发现算法自动识别图谱中的紧密群体,看看机器划分的派系和人工理解的派系是否一致。第三个是对话主题标注,把每段对话归类到“案件调查”“人事任命”“家庭生活”等主题上,然后看不同主题下的人物关系差异。

这些扩展不需要推翻现有的流程,只需要在现有结果上叠加分析维度。比如时间维度,就是在共现计算的时候加一个时间窗口的过滤条件;派系检测,就是调一下NetworkX的社区发现函数。如果你把这个实验当作一个教学项目来练手,我建议先把基础版本跑通,然后再逐个尝试扩展,这样每一步都有明确的对比基准。

我个人在实际操作中的体会是,人物图谱分析这个方向,技术门槛其实不高,真正的难点在于对文本的理解和对关系的判断。工具和代码都是现成的,但哪些关系值得关注、哪些别名需要归并、哪些情感倾向需要修正,这些都需要你对分析对象有足够的了解。换句话说,机器帮你画图,但图背后的故事,还是得你自己去读。

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

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

立即咨询