此时此刻,2020 年的最后一个月,不管过去这一年给我们留下了怎样的记忆,时间终究自顾自地往前走,留给我们的怀念已时日无多。如果要说 2020 年的年度日剧,我想《半泽直树》实至名归,这部在时隔七年后上映的续集,豆瓣评分高达 9.4 分,一度超越 2013 年第一部的 9.3 分,是当之无愧的现象级电视剧,期间甚至因为疫情原因而推迟播出,这不能不感谢为此付出辛勤努力的演职人员们。身为一个“打工人”,主角半泽直树那种百折不挠、恩怨分明的性格,难免会引起你我这种“社畜”们的共鸣,即使做不到“以牙还牙,加倍奉还”,至少可以活得像一个活生生的人。电视剧或许大家都看过了,那么,电视剧相对于原著小说有哪些改动呢?今天,就让我们使用 Python 来抽取半泽直树原著小说中的人物关系吧!
准备工作
在开始今天的博客内容前,我们有一点准备工作要完成。考虑到小说人物关系抽取,属于自然语言处理(NLP)领域的内容,所以,除了准备好 Python 环境以外,我们需要提前准备相关的中文语料,在这里主要有:半泽直树原著小说、 半泽直树人名词典、半泽直树别名词典、中文分词停用词表。除此之外,我们需要安装结巴分词
、PyECharts
两个第三方库(注,可以通过 pip 直接安装),以及用于展示人物关系的软件Gephi
(注,这个软件依赖 Java 环境)。所以,你基本可以想到,我们会使用结巴分词对小说文本进行分词处理,而半泽直树人名列表则作为用户词典供结巴分词使用,经过一系列处理后,我们最终通过Gephi
和PyECharts
对结果进行可视化,通过分析人物间的关系,结合我们对电视剧剧情的掌握情况,我们就可以对本文所采用方法的效果进行评估,也许你认为两个人毫无联系,可最终他们以某种特殊的形式建立了联系,这就是我们要做这件事情的意义所在。本项目已托管在 Github上,供大家自由查阅。
原理说明
这篇博客主要参考了 Python 基于共现提取《釜山行》人物关系 这个课程,该项目已在 Github 上开源,可以参考:https://github.com/Forec/text-cooccurrence。这篇文章中提到了一种称之为“共现网络”的方法,它本质上是一种基于统计的信息提取方法。其基本原理是,当我们在阅读书籍或者观看影视作品时,在同一时间段内同时出现的人物,通常都会存在某种联系。所以,如果我们将小说中的每个人物都看作一个节点,将人物间的关系都看作一条连线,最终我们将会得到一个图(指数据结构中的Graph)。因为Gephi
和PyECharts
以及NetworkX
都提供了针对Graph
的可视化功能,因此,我们可以使用这种方法,对《半泽直树》原著小说中的人物关系进行抽取。当然,这种方法本身会存在一点局限性,这些我们会放在总结思考这部分来进行说明,而我们之所以需要准备人名词典,主要还是为了排除单纯的分词产生的干扰词汇的影响;准备别名词典,则是考虑到同一个人物,在不同的语境下会有不同的称谓。
过程实现
这里,我们定义一个RelationExtractor
类来实现小说人物关系的抽取。其中,extract()
方法用于抽取制定小说文本中的人物关系,exportGephi()
方法用于输出 Gephi 格式的节点和边信息, exportECharts()
方法则可以使用ECharts
对人物关系进行渲染和输出:
1import os, sys
2import jieba, codecs, math
3import jieba.posseg as pseg
4from pyecharts import options as opts
5from pyecharts.charts import Graph
6
7class RelationExtractor:
8
9 def __init__(self, fpStopWords, fpNameDicts, fpAliasNames):
10 # 人名词典
11 self.name_dicts = [line.strip().split(' ')[0] for line in open(fpNameDicts,'rt',encoding='utf-8').readlines()]
12 # 停止词表
13 self.stop_words = [line.strip() for line in open(fpStopWords,'rt',encoding='utf-8').readlines()]
14 # 别名词典
15 self.alias_names = dict([(line.split(',')[0].strip(), line.split(',')[1].strip()) for line in open(fpAliasNames,'rt',encoding='utf-8').readlines()])
16 # 加载词典
17 jieba.load_userdict(fpNameDicts)
18
19 # 提取指定小说文本中的人物关系
20 def extract(self, fpText):
21 # 人物关系
22 relationships = {}
23 # 人名频次
24 name_frequency = {}
25 # 每个段落中的人名
26 name_in_paragraph = []
27
28 # 读取小说文本,统计人名出现的频次,以及每个段落中出现的人名
29 with codecs.open(fpText, "r", "utf8") as f:
30 for line in f.readlines():
31 poss = pseg.cut(line)
32 name_in_paragraph.append([])
33 for w in poss:
34 if w.flag != "nr" or len(w.word) < 2:
35 continue
36 if (w.word in self.stop_words):
37 continue
38 if (not w.word in self.name_dicts and w.word != '半泽'):
39 continue
40 # 规范化人物姓名,例:半泽->半泽直树,大和田->大和田晓
41 word = w.word
42 if (self.alias_names.get(word)):
43 word = self.alias_names.get(word)
44 name_in_paragraph[-1].append(word)
45 if name_frequency.get(word) is None:
46 name_frequency[word] = 0
47 relationships[word] = {}
48 name_frequency[word] += 1
49
50 # 基于共现组织人物关系
51 for paragraph in name_in_paragraph:
52 for name1 in paragraph:
53 for name2 in paragraph:
54 if name1 == name2:
55 continue
56 if relationships[name1].get(name2) is None:
57 relationships[name1][name2] = 1
58 else:
59 relationships[name1][name2] += 1
60
61 # 返回节点和边
62 return name_frequency, relationships
63
64 # 输出Gephi格式的节点和边信息
65 def exportGephi(self, nodes, relationships):
66 # 输出节点
67 with codecs.open("./output/node.txt", "w", "gbk") as f:
68 f.write("Id Label Weight\r\n")
69 for name, freq in nodes.items():
70 f.write(name + " " + name + " " + str(freq) + "\r\n")
71
72 # 输出边
73 with codecs.open("./output/edge.txt", "w", "gbk") as f:
74 f.write("Source Target Weight\r\n")
75 for name, edges in relationships.items():
76 for v, w in edges.items():
77 if w > 0:
78 f.write(name + " " + v + " " + str(w) + "\r\n")
79
80 # 使用ECharts对人物关系进行渲染
81 def exportECharts(self, nodes, relationships):
82 # 总频次,用于数据的归一化
83 total = sum(list(map(lambda x:x[1], nodes.items())))
84
85 # 输出节点
86 nodes_data = []
87 for name, freq in nodes.items():
88 nodes_data.append(opts.GraphNode(
89 name = name,
90 symbol_size = round(freq / total * 100, 2),
91 value = freq,
92 )),
93
94 # 输出边
95 links_data = []
96 for name, edges in relationships.items():
97 for v, w in edges.items():
98 if w > 0:
99 links_data.append(opts.GraphLink(
100 source = v,
101 target = w,
102 value = w
103 ))
104
105 # 绘制Graph
106 c = (
107 Graph()
108 .add(
109 "",
110 nodes_data,
111 links_data,
112 gravity = 0.2,
113 repulsion = 8000,
114 is_draggable = True,
115 symbol = 'circle',
116 linestyle_opts = opts.LineStyleOpts(
117 curve = 0.3,
118 width = 0.5,
119 opacity = 0.7
120 ),
121 edge_label = opts.LabelOpts(
122 is_show = False,
123 position = "middle",
124 formatter = "{b}->{c}"
125 ),
126 )
127 .set_global_opts(
128 title_opts = opts.TitleOpts(title="半泽直树原著小说人物关系抽取")
129 )
130 .render("./docs/半泽直树原著小说人物关系抽取.html")
131 )
你可以注意到,在input
目录中,博主已经准备好了中文语料。因此,我们可以通过下面的代码来完成任务关系抽取:
1extractor = RelationExtractor('./input/停用词词典.txt',
2 './input/人名词典.txt',
3 './input/别名词典.txt'
4)
5nodes, relationships = extractor.extract('./input/半泽直树.txt')
6extractor.exportGephi(nodes, relationships)
7extractor.exportECharts(nodes, relationships)
此时,我们可以分别在output
目录和docs
目录获得Gephi
和ECharts
相关的渲染结果。
结果展示
这里,通过 Gephi 软件导入生成的节点和边信息,这两个信息默认情况下在output
目录下。如果你熟悉这个软件的使用的话,你可以得到下面的结果:
作为对比,博主这里同时提供了使用 ECharts 渲染的小说人物关系图:
或者,可以直接访问博主托管在 Github Pages 上的 在线版本 。关于 Gephi 软件的使用,请参考: Gephi 网络图极简教程。关于 PyECharts 的使用,请参考: PyECharts。
总结思考
通过生成的人物关系图,可以发现下列规律:
- 大多数人物间的关系是正确的,譬如东田->浅野匡->半泽这条线,对应的是第一季西大阪钢铁 5 亿贷款的事件,而箕部->白井->半泽这条线,显然对应的第二季议员利用“炼金术”敛财的事件。
- 我们发现渡真利拥有仅次于半泽的“连线”数量,这符合他在剧中掌握大量信息来源、职场上八面玲珑的形象设定。相比之下,同样作为半泽好友的近藤和苅田,则没有这样强大的光环。
- 关于大和田,我们都知道他在第二季属于编剧强行“加戏”,一定程度上是在顶替内藤部长的作用,大和田实际上并未参与第二季的剧情,这一点从图中人物节点的联系和大小可以看出。
- 日本人似乎更喜欢使用姓氏,由于妻子要跟随丈夫的姓氏,剧中很多女性角色譬如半泽花、浅野利惠等似乎都不太好提取出来,除非是像白井、谷川、藤泽这些重要的剧情人物。
考虑到,小说中同一个人的称呼通常会有很多种,与之相关联的领域被称为“中文指代消解问题”,使用姓氏作为关键字会造成“女性角色”的缺失,而这种基于“共现”的理论,无法解决 A 在 B 交谈的过程中提到 C 的问题,此时,C 和 A、C 和 B 可能并没有直接的联系,譬如图中的垣内,理论上与西大阪钢铁 5 亿贷款事件并无直接联系,因为剧情中参与融资的主要是新人中西,更不用说老员工角田居然“孤零零”的一个人,而主流的命名实体识别的理论基本都针对三元组,所以,在这里要心疼下角田这位老人。
在目前的人物关系抽取案例中,这种情况称为“无效的人名实体共现句”,所以,更好的做法是,采用文本分类模型,结合依存句法去识别实体间的关系,比如同事关系、朋友关系或者亲属关系等等,它有一个非常专业的名词,称为命名实体识别(NER),而这会让我们的这张图变得更加丰富。在这个方向上,我个人推荐使用哈工大的语言技术平台(LTP)作为进一步改进,因为它可以更好地识别人名。好了,以上就是这篇博客的全部内容啦,欢迎大家在博客下面留言,喜欢我的博客话,请一键三连,点赞收藏,谢谢大家!