文本挖掘实践再回顾:基于游记的事件演化序列抽取与图谱可视化展示

文摘   科技   2024-09-15 10:55   北京  

叙事性文本中,隐藏着十分丰富的事件先后演化知识,而对这个知识进行结构化的挖掘,可以形成一些很有意思的结论。

例如,我们学生时代写记叙文,比如去外婆家,整篇读下来,我们就能知道去外婆家这次活动都经历了哪些事情。

而具备典型代表性的游记,也成为了众多旅游爱好十分热衷写的一种题材,这为我们进行游记的挖掘提供了很好的素材。


进一步的,如果我们将游记文本挖掘和图谱进行结合,将游记中的一些关键步骤形式化成一个图节点,并连成线,做成有向图,也能得到十分有趣的信息

本文围绕游记文本结构化这一主题,介绍一个基于游记的结构化挖掘实现,通过借助句法依存分析,将文本中的信息表示为主谓宾、主谓或者动宾的形式,调用vis插件,进行可视化展示。

实现代码地址:

https://github.com/liuhuanyong/SequentialEventExtration

一、出行游记文本语料的获取

现在有许多游记的垂类网站提供了相应的文本,通过使用scrapy进行文本采集,可以得到相关游记的文本。

例如,针对《库布齐老牛湾之户外行走》一文,

https://m.ctrip.com/webapp/you/travels/KubuqiDesert120472/1494526.html

通过采集【项目中提供了news-spider实现】,可以直接得到以下非结构化纯文本数据。

从德胜门出发,同往常活动一样,开篇需做self-intro,籍贯、年龄、星座、感情状况等等一应俱全。
22:30:熄灯;“夜生活”真正开始了。伸展、托腮、睡10分钟醒10分钟、在座位上无比频繁的变换着睡姿,试图能找到快速入睡的方法。好巧不巧前排座椅放倒,膝盖顿时有了压迫感。颈椎疼了腰酸了膝盖顶了,是岁月不饶人还是空间太过狭小?
03:00:车子在一高速服务区开始为时两个钟头的停靠,原因是凌晨2点至5点是交通事故高发时段,为避免疲劳驾驶和抑制事故的发生大多数车辆都停在服务区附近。凌晨温差挺大,加之网络信号不好,手机难以获取定位,于是乎再次昏昏沉沉的睡去~~~
04:30:睡觉睡到手抽筋;一觉醒来发现车窗上布满了哈气,下车后有种醍醐灌顶之清醒。
05:00:车子启动继续前行;此时手机定位显示位于呼和浩特最近的一个服务区。5年后,内蒙我又来啦!
08:00:历经3小时高速行驶继续迷迷瞪瞪的半睡半醒般假寐,终于抵达了曾经令我魂牵梦绕的地方——响沙湾。响沙湾是库不齐沙漠的一部分,以风吹过后沙子顺势滑下沙沙作响而得名。2010年曾计划自由行,无奈中途搁置;2012年曾期盼来此地徒步,但心里始终无法克服夜间乘车恐惧症和负重行走。
今年我终于说服了自己,始终如一保持一种独立、不畏艰难以及昂扬斗志的户外精神去尝试在沙海中徜徉。幻想自己老的那一天,卧在某个海滩的躺椅上,听着不远处的阵阵海浪,翻看着年轻时的足迹,追忆往事,该是多么的惬意与回味。
08:10:开始洗漱时间;
还记得2008年第一次自由行爬泰山,当时就背了个极其普通的双肩背,踏了一双板鞋,没有什么啰哩啰嗦的其他装备就这样夜间登顶,然后四处寻找住宿。
随着出行次数的增多,清单也越列越长,随身携带的物品也随之增多。书包和箱子越来越大。
本次出发负重20斤,其中有7斤水。从家前往集合的道路上异常不适应如此沉重而又累赘的背囊,但有时繁琐也不见得是坏事。响沙湾景区外的洗手间还算干净,但是上水需要很长一段时间,只有洗手间外的一个水龙头可以上水,因此100来人只能等着一个水龙头轮流洗漱。。。有人干脆放弃
这时候农夫山泉1.5L上场了,它很快速的帮我解决了洗漱难题。
08:40:向沙漠进军!

二、事件演化序列抽取与图谱生成

我们需要基于游记挖掘事件序列,其中包括两个重点,一个是事件,一个是序列。

其中的事件,与我们事件抽取中的事件框架,以及一个动作词的事件不同,下图总结了当前遇到的几种工业界事件表示方式及其优缺点。

在这里,我们将事件表示为借助句法依存分析,将文本中的信息表示为主谓宾、主谓或者动宾的形式。

对于事件序列,我们认为两个事件之间如果在一个上下文【一个长句】中出现,那么根据两个事件之间出现的先后顺序,所形成的序列就是事件序列。

下面就事件序列的抽取以及可视化的实现方法进行解读?

1、事件演化序列抽取

事件序列的抽取,可以分成以下9个步骤:
1)输入游记文本。即对采集到的文本作为输入。

2)对游记进行长句切分。一般我们会按照??!!。;;::\n\r进行切分。

def seg_long_sents(self, content):
        return [sentence for sentence in re.split(r'[??!!。;;::\n\r….·]', content.replace(' ','').replace('\u3000','')) if len(sentence) > 5]

3)基于构造的先后关系模板【例如A紧接着B,】进行前后部分提取,转入4)

self.pattern = re.compile(r'(.*)(其次|然后|接着|随后|接下来)(.*)')
result = self.pattern.findall(sent)
if result:
    event_seqs = []
    for tmp in result:
        pre = tmp[0]
        post = tmp[2]

4)对3)得到的部分进行短句处理短句中以、,和与及且跟分隔符进行切分。转入5)

    def process_subsent(self, content):
        return [s for s in re.split(r'[,、,和与及且跟()~▲.]', content) if len(s)>1]

5)对4)得到的短句进行谓词性短语提取,此处可以使用LTP工具进行处理,替代的有百度的DDparser,Hanlp等。

'''将一个长句中的句子进行分解,提取出其中的vob短语'''
    def extract_phrase(self, event_seqs):
        events = []
        for event in event_seqs:
            vobs = self.vob_exract(event)
            if vobs:
                events += vobs
        return events
    '''提取VOB关系'''
    def vob_exract(self, content):
        vobs = []
        words = list(jieba.cut(content))
        if len(words) >= 300:
            return []
        postags = self.parse_handler.get_postag(words)
        tuples, child_dict_list = self.parse_handler.parser_main(words, postags)
        for tuple in tuples:
            rel = tuple[-1]
            pos_verb= tuple[4][0]
            pos_object = tuple[2][0]
            if rel == 'VOB' and (pos_verb, pos_object) in [('v''n'), ('v''i')]:
                phrase = ''.join([tuple[3], '#', tuple[1]])
                vobs.append(phrase)
        return vobs

6)对5)得到的谓词性短语向上汇聚,得到一个长句的谓词性短语有序集合 。

7)对6)步骤得到的谓词性短语集合,以滑窗方式构造先后关系事件对 。

8)对步骤7)得到的事件对进行汇总,最终得到事件序列。

9)对8)进行事件进行整合,去除过低频次的事件,并构造标准的事件序列数据。

2、事件演化序列图谱生成

以结构化存储的数据并不利于分析和展示,因此,我们可以与图谱可视化插件进行结合,对其进行展示。例如,使用VIS插件进行顺承关系图谱构建与展示

class EventGraph:
    def __init__(self):
        self.event_path = './seq_events.txt'

    '''统计事件频次'''
    def collect_events(self):
        event_dict = {}
        node_dict = {}
        for line in open('seq_events.txt'):
            event = line.strip()
            if not event:
                continue
            nodes = event.split('->')
            for node in nodes:
                if node not in node_dict:
                    node_dict[node] = 1
                else:
                    node_dict[node] += 1
            if event not in event_dict:
                event_dict[event] = 1
            else:
                event_dict[event] += 1
        return event_dict, node_dict

    '''过滤低频事件,构建事件图谱'''
    def filter_events(self, event_dict, node_dict):
        edges = []
        nodes = []
        for event in sorted(event_dict.items(), key=lambda asd: asd[1], reverse=True)[:500]:
            e1 = event[0].split('->')[0]
            e2 = event[0].split('->')[1]
            if e1 in node_dict and e2 in node_dict:
                nodes.append(e1)
                nodes.append(e2)
                edges.append([e1, e2])
            else:
                continue
        return edges, nodes

    '''调用VIS插件,进行事件图谱展示'''
    def show_graph(self, edges, nodes):
        handler = CreatePage()
        data_nodes, data_edges = handler.collect_data(nodes, edges)
        handler.create_html(data_nodes, data_edges)

不过,由于VIS作为一个封装的JS库,而且图中的节点很多,容易造成图谱爆炸,因此可以做下限制,暂时设置到500。

三、事件演化序列图谱展示

通过对抽取的图谱数据进行可视化,调用visjs插件,可以对该数据进行可视化。

1、事件演化序列图谱全貌

以500个顺承事件, 进行顺承事件图谱展示,结果是一张事件网络,这是一个大的顺承关系图谱,由众多小子图谱构成,并且有的还比较稀疏。

2、去丽江事件演化序列子图

该子图谱围绕"去丽江旅游"这一出行事件为核心形成的事件群:

从中我们看到,围绕着“去丽江”这一事件所产生的顺承逻辑,如“拿#身份证”->“去#售票口”-> “去#买票”->“遇上#旺季”->“去#丽江”这一演化序列。

3、飞机路线演化序列子图谱 

该子图谱显示了选择飞机进行出行形成的事件序列。

旅游前,先打开百度,然后搜索航班,预定机票,有的时候还需要抢特价机票。

4、火车路线演化序列子图

该子图谱显示了选择火车进行出行形成的事件序列。

与飞机不同,坐火车通常比较漫长,需要先拿身份证,去售票口,去买票。买到卧铺票后,先睡一觉,然后到达丽江,再到昆明。

5、订酒店事件演化序列

该子图谱描述了一个"预定酒店不愉快事件",从预定到失望到总结,在这条顺承事件链表现出来。

“去#丽江”-> “预订#客栈”->“看过#攻略”->“结合#眼光”->“没有#价值”->“擦亮#眼睛”这一顺承逻辑结构。

6、做饭事件演化序列 

该子图谱表示了一个"做饭"场景下的顺承事件,感觉也很有意思。

“去#庄园”->“去#竹林”->“挖#冬笋”->“切成#块”->“配上#鳊鱼”-> “匀以#薯粉”->“成#棒状”->“入#油锅”->“炸成#小块”这一顺承事件链形象地描述了“烹饪”这一事件的时序关系。

四、总结

游记文本是一个很有意思的文本,本文围绕游记文本结构化这一主题,介绍了一个基于游记的结构化挖掘实现,通过借助句法依存分析,将文本中的信息表示为主谓宾、主谓或者动宾的形式,调用vis插件,进行可视化展示。

基于这种朴素的思想,本文基于50W文章领域语料,形成了事件节点为326781个,事件演化序列对为543580条。

不过,需要注意的是:

其一,这种方法只对游记这种特殊文体有效,不具备普适性。更有效的方式实际上是预先定义好一些事件,然后做事件识别、事件关系识别,这样更可控。

其二,对于谓词性短语进行事件表示是事件表示的一种方式,本方法只采用VOB关系进行提取,这种方式还有待改进。以这种方式得到的结果中,还存在大量噪声,这一方面准确率受依存句法的准确性限制,另一方面该依存关系可能还相对单一,不够准确。

其三,在构造顺承事件序列的方法,本文采用的是长句为单位下的滑窗方式进行构造,这个方式还有待改进,未能考虑之前的语义关系。

最后,谈谈这个结果可能可以进一步支撑的场景,例如,针对火车以及飞机出行的子图为例,可以为现在的C端用户进行一些有意思的消费推荐。

例如,当用户发出“丽江是个好地方,我想去看看”的状态时,通过分析该用户的消费意图,将消费意图识别为一个出行事件时,通过游走以“丽江出行”这一个顺承图谱可以推出多种消费行为。例如“出机场、看到接待点”这个子事件可以推出“机票预订与推荐”与“接送机”服务;

“预订#客栈”这一子事件可以引出“酒店预订”服务,“买卧铺票”这一子事件可引出“火车票预订”这项需求。

全局的来看,一个足够准确以及丰富的出行事件演化序列图谱可以作为一个整体的出行指南提供给用户,充当用户规划的“探路者”与“规划师”

最后,大家可以在一个基础上换上自己的文本数据进行实验。也可以对其中的一些方法进行完善,做进一步的探索。

源码地址:

https://github.com/liuhuanyong/SequentialEventExtration

关于我们

老刘,刘焕勇,NLP开源爱好者与践行者,主页:https://liuhuanyong.github.io。

对大模型&知识图谱&RAG&文档理解感兴趣,并对每日早报、老刘说NLP历史线上分享、心得交流等感兴趣的,欢迎加入社区,社区持续纳新。

加入会员方式:关注公众号,在后台菜单栏中点击会员社区->会员入群加入


老刘说NLP
老刘,NLP开源爱好者与践行者。主页:https://liuhuanyong.github.io。老刘说NLP,将定期发布语言资源、工程实践、技术总结等内容,欢迎关注。
 最新文章