文本生成

文本生成是自然语言处理中一个重要的研究领域,具有广阔的应用前景。国内外已经有诸如Automated Insights、Narrative Science以及“小南”机器人和“小明”机器人等文本生成系统投入使用。这些系统根据格式化数据或自然语言文本生成新闻、财报或者其他解释性文本。例如,Automated Insights的WordSmith技术已经被美联社等机构使用,帮助美联社报道大学橄榄球赛事、公司财报等新闻。这使得美联社不仅新闻更新速度更快,而且在人力资源不变的情况下扩大了其在公司财报方面报道的覆盖面。

任务定义

接受非语言形式的信息作为输入,生成可读的文字表述。数据到文本的生成适用于这个定义,后续研究人员将这个概念拓展为包括了文本到文本的生成、数据到文本的生成以及图像到文本的生成的文本生成技术。

任务分类

按照输入数据的区别,可以将文本生成任务大致分为以下三类:

  • 1)文本到文本的生成;
  • 2)数据到文本的生成;
  • 3)图像到文本的生成。

1)文本到文本的生成又可根据不同的任务分为(包括但不限于):文本摘要、 古诗生成、文本复述等。文本摘要又可以分为抽取式摘要和生成式摘要。
抽取式摘要通常包含信息抽取和规划等主要步骤。近期,在这方面有许多有趣的工作:

  • 在为论文自动生成相关工作部分文本的任务上使用主题模型PLSA将句子按照主题进行聚类,使用SVR(Support Vector Regression)计算句子的相似度,最后使用线性规划生成相关工作文本。
  • 在基于短语级别为学术论文生成演示文件的研究中采用了四个步骤。首先从论文中抽取名词短语、动词短语作为候选短语, 利用人工设计的特征和基于随机森林的分类器决定短语是否应出现在演示文件中,再训练一个基于随机森林的分类器判断两个短语是否存在一级、二级标题的关系,最后使用贪心策略选择句子构成一个演示文件。Zhang[5]在根据体育赛事直播文字生成赛事报道的任务上,主要采用了Learning to Rank的方法结合人工设计的特征模版对句子进行打分,进而采用行列式点过程(DPP, Determinantal Point Process)进行句子选择。
  • 最近ACL 2017上发表了多篇生成式摘要的论文。
  • 如See等人提出了解决生成事实性错误文本和重复性文本问题的方法[6],Zhou等人加入选择门网络(selective gate network)进行摘要生成[7]。
  • 古诗生成方面,Zhang等人[8]使用循环神经网络进行生成,Wang等人[9]将古诗生成划分为规划模型和生成模型两部份。
  • Zhang等人[10]在Seq2Seq模型的基础上加入记忆模块。文本复述方面,Quirk等人[11]使用机器翻译的方法生成复述文本,Max等人
  • [12]采用基于枢轴(pivot)的复述生成方法,以另一种语言作为中间媒介,将源语言翻译成另一种语言后再翻译为原来的语言。

2)结构化数据生成文本的任务上,Reiter等人[13]将数据到文本的系统分为了信号处理(视输入数据类型可选)、数据分析、文档规划和文本实现四个步骤。Mei等人[14]基于encoder-decoder模型加入了aligner选择重要信息,基于深度学习提出了一个端到端的根据数据生成文本的模型。比如 语义解析 (Text-to-SQL)

3)图像到文本的生成方面也有不同的任务,如image-caption、故事生成、基于图像的问答等。在为图像生成解释性文本(image-caption)的任务上,Vinyals等人[15]使用类似encoder-decoder的模型进行生成。Xu等人[16]则进一步加入Attention机制。Huang等人[17]提出针对图片序列生成故事的任务,并且提供了单张图片的描述性文本、单张图片的故事以及图片序列的故事三个层级的数据集。在第三个数据集上,他们拓展之前的模型并加入一些技巧提供了该任务的一些baseline。并通过对自动化评价指标以及人工评价相关度的衡量,确定使用METEOR作为自动化评价指标。基于图像的问答任务上,Shih等人[18]提出了使用基于Attention机制的模型用VGGnet编码图片,用词向量求均值表示问题,最后经过两层网络生成答案 、Wu等人[19]提出了整合image-caption模型和外部知识库等生成答案。

文本生成方法

1 基于语言模型的自然语言生成

基于马尔可夫的语言模型在数据驱动的自然语言生成中有着重要的应用。它利用数据和文字间的对齐语料,主要采用两个步骤:内容规划和内容实现为数据生成对应的文本。Oh等人[21]在搭建面向旅行领域的对话系统时,在内容规划部分使用bigram作特征根据近期的对话历史,选取待生成文本中需要出现的属性,内容实现部分使用n-gram语言模型生成对话。Ratnaparkhi等人[22]经过实验对比发现在语言模型上加入依存语法关系有助于改善生成效果。Angeli等人[23]则将文本生成的过程分为三种决策(以生成天气报道为例):1)宏观的内容选择,如选择温度等方面进行报道。2)微观内容选择,如选择最低温度或者最高温度进行报道。3)模版选择。这三个决策步骤交替进行。每次决策的时候会考虑到历史决策信息,这有助于处理需要考虑长距离的依赖关系的情况,如语义连贯性。

2 使用深度学习方法的自然语言生成

  • 在文本到文本的生成方面,Zhang等人[8]使用RNN进行中文古诗生成,用户输入关键词后首先拓展为短语,并用来生成诗的第一行。接下来的每一行结合当时所有已生成的诗句进行生成。Wang[9]则将古诗生成分为规划模型和生成模型两部份。规划模型部分得到用户的输入,在使用TextRank进行关键词抽取和使用RNN语言模型和基于知识库的方法进行拓展后,获得一个主题词序列,作为写作大纲,每一个主题词在下一个部分生成一行诗。生成模型部分基于encoder-decoder模型,增加一个encoder为主题词获得一个向量表示。
  • 另一 个encoder编码已经生成的句子。使用attention-based的模型,decoder综合主题词和已经生成的句子,生成下一句的内容。通过这两个模型,在更好的控制每一行诗的主题的同时保持诗词的流畅性。最近,在ACL 2017上发表了多篇生成式摘要的论文。如See等人[6]为了解决生成一些与事实不符的内容,在标准的基于attention的Seq2Seq模型上结合Pointer Network,使其既可以生成词,也可以从原文中直接把一些词放入生成的文本中。为了解决重复的问题,加入coverage模型。Zhou等人[7]则通过在encoder和decoder之间加入一个选择门网络(selective gate network)作为输入句子的第二层表示,提高编码的有效性,降低decoder的负担。
  • 在数据到文本的生成方面,Mei[14]提出了encoder-aligner-decoder的端到端模型。主要特点是在标准的encoder和进行了改进的decoder之间加入用于选择将要描述的重要信息的aligner。它对每条记录生成的权重分为两个部分。第一部分是针对每条记录的向量表示单独计算一个权重。 第二部分是在decoder的第t步时,根据decoder已经生成的内容及对应记录的向量表示计算权重。 在两个数据集上取得比较好的效果提升。它的优势在于同步训练内容选择和生成部分且不需要针对任务人工设置特征,普适性较好。
  • 在图像到文本的生成方面,Vinyals[15]使用Seq2Seq的模型,首先利用深层卷积神经网络DCNN 对图像建模,然后由一个LSTM网络进行解码生成最终的文本。与传统的机器学习方法相比,无需进行图像和文本中词的对齐、调整顺序等步骤。Xu[16]则进一步提出利用Attention机制来加强词语和图像块之间的对齐,在生成文字的时候,模拟人看东西时关注点逐渐转移的过程,以生成更符合人习惯的文本。

推荐资料:

seq2seq结构:nmt_with_attention

【深度学习和自然语言处理】Seq2seq 中文文本生成

模型评价

No evaluation, no research。如何对生成的文本进行评价也是文本生成研究中重要的一环。Gkatzia[24]总结2005年到2014年间的常用的针对文本生成的评价方法,将其分为内在评价和外在评价方法。其中内在评价关注文本的正确性、流畅度和易理解性。常见的内在评价方法又可分为两类:

  • 1)采用BLEU、NIST和ROUGE等进行自动化评价,评估生成文本和参考文本间相似度来衡量生成质量。
  • 2)通过人工评价,从有用性等对文本进行打分。外在评价则关注生成文本在实际应用中的可用性。

根据他们的分析,内在评价方法是最为流行的评价方法。2012-2015年间发表的论文超半数使用自动化评价指标进行评价,但由于它需要有大量的对齐语料,且对于对齐语料的质量很敏感,所以在使用自动化评价指标的同时,研究者常常还会同时使用其它的评价方法,如直观且易于操作(与外在评价方法相比)的人工评价生成文本的正确性、流畅性方法。

基于Seq2Seq的文本生成评价指标解析

基于GPT-2进行文本生成

基于Transformers的语言模型最不可思议的特点之一是它们能够生成与人类所写的文本几乎没有区别的文本。一个著名的例子是OpenAI的GPT-2,它在给出以下提示时:

能够生成一篇关于独角兽的新闻:

这个例子之前是非常有名的,是因为它是在没有任何明确监督的情况下产生的! 通过简单地学习预测数以百万计的网页文本中的下一个词,GPT-2和它更强大的改进版,如GPT-3,能够获得文本学习和模式识别能力,以及能被不同类型的输入提示获得比较不错效果。语言模型在预训练期间有时会接触到一些任务序列,在这些任务中,它们需要仅仅根据上下文来预测下面的标记,如加法、解词和翻译。这使得它们在微调期间或(如果模型足够大)在推理时间有效地转移这些知识。这些任务不是提前选择的,而是在用于训练十亿参数语言模型的巨大语料库中自然出现的。

Transformers生成现实文本的能力导致了多样化的应用,如InferKit、Write With Transformer、AI Dungeon,以及像谷歌的Meena这样的对话代理,它甚至可以讲出老套的笑话,如图5-2所示!

在之前教程里,我们一直专注于通过预训练和监督微调的组合来处理NLP任务。正如我们所看到的,对于像序列或标记分类这样的特定任务,产生预测是相当直接的,比如预测一个文本的情感类别;模型产生一些分数(可以理解为logits),然后我们取最大值来获得预测的类别,或者应用softmax函数来获得每个类别的预测概率。相比之下,将模型的概率输出转换为文本需要一种解码方法,这就引入了一些文本生成所特有的挑战:

  • 解码是反复进行的,因此比简单地将输入通过模型的前向传递一次涉及到更多的计算。
  • 生成文本的质量和多样性取决于解码方法和相关超参数的选择。

为了理解这个解码过程是如何进行的,让我们先来看看GPT-2是如何进行预训练并随后应用于生成文本的。

像其他自回归或因果语言模型一样,给定一些初始提示或语境序列x = x1, x2, ... xk,GPT-2被预训练来估计文本中出现的文本序列y = y1, y2, ... yt的概率P(y|x)。由于训练数据比较大,来直接估计P(y|x)是不切实际的,所以通常使用概率链规则来将其分解为条件概率的乘积。

image.png

其中y<t是序列y1, ..., yt-1的文本字符。从这些条件概率中,我们可以推断出:自回归语言建模相当于在一个句子中给定前面的词来预测每个词;这正是前面方程中右边的概率所描述的。请注意,这个预训练目标与BERT的预训练目标完全不同,BERT利用过去和未来的语境来预测一个被掩盖的标记。

现在你可能已经猜到我们如何调整这个下一个标记的预测任务,以生成任意长度的文本序列。如图5-3所示,我们从 "transformers "这样的流程开始,用模型来预测下一个标记。一旦我们确定了下一个标记,我们就把它附加到提示上,然后用新的输入序列来生成另一个标记。重复这个过程,直到我们达到一个特殊的序列结束符号或预先定义的最大长度。

image.png

致谢

第十届“泰迪杯”数据挖掘挑战赛C题 -疫情背景下的周边游需求图谱分析 第2问和第3问 产品热度计算方法

第一问分享在下列链接:[post cid="161" cover="" size=""/]

[scode type="green" size="simple"]

我已在AIStudio将赛题方案上传,可以一键Fork运行。

步骤:未注册的小伙伴需要先去AI Studio进行注册,完善资料后,访问下面方案链接(觉得好的话,记得给我点个关注和小红心哦):

问题一解决方案代码

问题二、三解决方案代码

由于该赛题方案内容太多,故将1问和2、3问分开分享

竞赛地址:https://www.tipdm.org:10010/#/competition/1481159137780998144/question

[/scode]

  • 一、项目介绍

    • 1.1 赛题背景
    • 1.2 第二问拟解决的问题 周边游产品热度分析
    • 1.3 第三问拟解决的问题 本地旅游图谱构建与分析
  • 二、方案设计

    • 2.1 赛题背景
    • 2.2 第二问拟解决的问题 周边游产品热度分析
  • 三、基于SKEP的周边游产品热度评价

    • 3.1 评论观点抽取模型
    • 3.2 属性级情感分类模型
    • 3.3 全流程模型推理
  • 四、 计算产品热度

    • 4.1 方面级情感极性预测及计算情感分数
    • 4.2统计产品情感热度得分
  • 五、致谢

一、 项目介绍

1.1 赛题背景

随着互联网和自媒体的繁荣,文本形式的在线旅游(Online Travel Agency,OTA)和游客的用户生成内容(User Generated Content,UGC)数据成为了解旅游市场现状的重要信息来源。OTA和UGC数据的内容较为分散和碎片化,要使用它们对某一特定旅游目的地进行研究时,迫切需要一种能够从文本中抽取相关的旅游要素,并挖掘要素之间的相关性和隐含的高层概念的可视化分析工具。

为此本赛题提出本地旅游图谱这一概念,它在通用知识图谱的基础上加入了更多针对旅游行业的需求。本地旅游图谱采用图的形式直观全面地展示特定旅游目的地“吃住行娱购游”等旅游要素,以及它们之间的关联。图 1所示为我国西藏阿里地区的本地旅游图谱,中心位置节点为旅游目的地“阿里”,它的下层要素包括该地区的重要景点如“冈仁波齐”和“玛旁雍错”,以及“安全”、“住宿”等旅游要素。旅游要素分为多个等级,需要从文本中挖掘出面对不同要素游客所关注的下一级要素。如阿里地区的“安全”要素下包括“高反”、“天气”和“季节”等下一级要素,这个组合是西藏旅游所特有的。旅游要素之间会存在关联关系,如“冈仁波齐”和“玛旁雍错”这两个景点通过“神山圣湖”这一高层概念产生联系,在本地旅游图谱中使用连接两个节点的一条边来表示。

在近年来新冠疫情常态化防控的背景下,我国游客的旅游消费方式已经发生明显的转变。在出境游停滞,跨省游时常因为零散疫情的影响被叫停的情况下,中长程旅游受到非常大的冲击,游客更多选择短程旅游,本地周边游规模暴涨迎来了风口。疫情防控常态化背景下研究分析游客消费需求行为的变化,对于旅游企业产品供给、资源优化配置以及市场持续开拓具有长远而积极的作用。本赛题提供收集自互联网公开渠道的2018年至2021年广东省茂名市的OTA和UGC数据,期待参赛者采用自然语言处理等数据挖掘方法通过建立本地旅游图谱的方式来分析新冠疫情时期该市周边游的发展。

1.2 第二问拟解决的问题 周边游产品热度分析

从附件提供的OTA、UGC数据中提取包括景区、酒店、网红景点、民宿、特色餐饮、乡村旅游、文创等旅游产品的实例和其他有用信息,将提取出的旅游产品和所依托的语料以表2的形式保存为文件“result2-1.csv”。建立旅游产品的多维度热度评价模型,对提取出的旅游产品按年度进行热度分析,并排名。将结果以表3的形式保存为文件“result2-2.csv”。

1.3 第三问拟解决的问题 本地旅游图谱构建与分析

依据提供的OTA、UGC数据,对问题2中提取出的旅游产品进行关联分析,找出以景区、酒店、餐饮等为核心的强关联模式,结果以表4的形式保存为文件“result3.csv”。在此基础上构建本地旅游图谱并选择合适方法进行可视化分析。鼓励参赛队挖掘旅游产品间隐含的关联模式并进行解释。

二. 方案设计

C题第二问
通过构建基于 SBERT 的旅游产品提取模型,对旅游攻略及微信公众号进行产品抽取,再针对情感热度问题,构建基于迁移学习与细粒度情感分析的情感热度分析模型,得出每个产品的情感分数,最终利用情感分数以及年度的产品频次按照自定的产品热度公式进行计算,最终生成年度产品热度。
本实践的解决方案采用skep模型,大致分为两个环节,首先需要进行评论观点抽取,接下来,便可以根据该评论观点去分析相应观点的情感极性。

2.1 评论观点抽取

在本实践中,我们将采用序列标注的方式进行评论观点抽取,具体而言,会抽取评论中的属性以及属性对应的观点,为此我们基于BIO的序列标注体系进行了标签的拓展:B-Aspect, I-Aspect, B-Opinion, I-Opinion, O,其中前两者用于标注评论属性,后两者用于标注相应观点。

如图1所示,首先将文本串传入SKEP模型中,利用SKEP模型对该文本串进行语义编码后,然后基于每个位置的输出去预测相应的标签。

**

图1 评价观点抽取模型

**

2.2 属性级情感分类

在抽取完评论观点之后,便可以有针对性的对各个属性进行评论。具体来讲,本实践将抽取出的评论属性和评论观点进行拼接,然后和原始语句进行拼接作为一条独立的训练语句。

如图2所示,首先将评论属性和观点词进行拼接为"味道好",然后将"味道好"和原文进行拼接,然后传入SKEP模型,并使用"CLS"位置的向量进行细粒度情感倾向。

**

图2 属性级情感分类模型

**

三、基于SKEP的周边游产品热度评价

3.1 评论观点抽取模型

3.1.1数据预处理

(1) 数据集介绍

本实践中包含训练集、评估和测试3项数据集,以及1个标签词典。其中标签词典记录了本实践中用于抽取评论对象和观点词时使用的标签。

另外,本实践将采用序列标注的方式完成此任务,所以本数据集中需要包含两列数据:文本串和相应的序列标签数据,下面给出了一条样本。

服务好,环境好,做出来效果也不错 B-Aspect I-Aspect B-Opinion O B-Aspect I-Aspect B-Opinion O O O O B-Aspect I-Aspect O B-Opinion I-Opinion

(2)数据加载

本节我们将训练、评估和测试数据集,以及标签词典加载到内存中。相关代码如下:

import os
import zipfile

def zip_decompress(file_path, new_path):
    '''支持中文的解压缩程序
       file_path:原zip文件路径
       new_path:新文件夹路径
'''
    z = zipfile.ZipFile(f'{file_path}', 'r')
    z.extractall(path=f"{new_path}")
    
    for root,dirs,files in os.walk(new_path):
        for d in dirs:
            try:
                new_name = f.encode('cp437').decode('utf-8')
                os.rename(os.path.join(root, d), os.path.join(root, new_dname))
            except:
                new_dname = d.encode('cp437').decode('utf-8')
                os.rename(os.path.join(root, d), os.path.join(root, new_dname))  
    for root,dirs,files in os.walk(new_path):
        for f in files:
            try:
                new_name = f.encode('cp437').decode('utf-8')
                os.rename(os.path.join(root, f), os.path.join(root, new_name))
            except:
                new_name = f.encode('cp437').decode('utf-8')
                os.rename(os.path.join(root, f), os.path.join(root, new_name))   
    z.close()
    # print('完成!')
zip_decompress("./data/CEC-Corpus-master.zip","./data/CEC")
import xml.dom.minidom
import os

count = [0 for i in range(4000)]

#存放xml文件的地址
xml_file_path = r"./data/CEC/CEC-Corpus-master/CEC/地震/"
lst_dir = os.listdir(xml_file_path)

for file_name in lst_dir:
    #读入所有的xml文件
    file_path = xml_file_path + file_name
    tree = xml.dom.minidom.parse(file_path)
    #获取根节点
    root = tree.documentElement
    #接下来就可以对指定的文本元素进行操作
    size_node = root.getElementsByTagName("object")

    for o in size_node:
        count[i] = count[i] + 1
        #有一个就数量加1
!pip install lxml
import os
import argparse
from functools import partial
import paddle
import paddle.nn.functional as F
from paddlenlp.metrics import ChunkEvaluator
from paddlenlp.datasets import load_dataset
from paddlenlp.data import Pad, Stack, Tuple
from paddlenlp.transformers import SkepTokenizer, SkepModel, LinearDecayWithWarmup
from utils.utils import set_seed
from utils import data_ext, data_cls

train_path = "./data/data121190/train_ext.txt"
dev_path = "./data/data121190/dev_ext.txt"
test_path = "./data/data121190/test_ext.txt"
label_path = "./data/data121190/label_ext.dict"

# load and process data
label2id, id2label = data_ext.load_dict(label_path)
train_ds = load_dataset(data_ext.read, data_path=train_path, lazy=False)
dev_ds =  load_dataset(data_ext.read, data_path=dev_path, lazy=False)
test_ds =  load_dataset(data_ext.read, data_path=test_path, lazy=False)

# print examples
for example in train_ds[9:11]:
    print(example)

(3)将数据转换成特征形式

在将数据加载完成后,接下来,我们将各项数据集转换成适合输入模型的特征形式,即将文本字符串数据转换成字典id的形式。这里我们要加载paddleNLP中的SkepTokenizer,其将帮助我们完成这个字符串到字典id的转换。

model_name = "skep_ernie_1.0_large_ch"
batch_size = 8
max_seq_len = 512

tokenizer = SkepTokenizer.from_pretrained(model_name)
trans_func = partial(data_ext.convert_example_to_feature, tokenizer=tokenizer, label2id=label2id, max_seq_len=max_seq_len)
train_ds = train_ds.map(trans_func, lazy=False)
dev_ds = dev_ds.map(trans_func, lazy=False)
test_ds = test_ds.map(trans_func, lazy=False)

# print examples
for example in train_ds[9:11]:
    print("input_ids: ", example[0])
    print("token_type_ids: ", example[1])
    print("seq_len: ", example[2])
    print("label: ", example[3])
    print()

(4)构造DataLoader

接下来,我们需要根据加载至内存的数据构造DataLoader,该DataLoader将支持以batch的形式将数据进行划分,从而以batch的形式训练相应模型。

batchify_fn = lambda samples, fn=Tuple(
        Pad(axis=0, pad_val=tokenizer.pad_token_id),
        Pad(axis=0, pad_val=tokenizer.pad_token_type_id),
        Stack(dtype="int64"),
        Pad(axis=0, pad_val= -1)
    ): fn(samples)

train_batch_sampler = paddle.io.BatchSampler(train_ds, batch_size=batch_size, shuffle=True)
dev_batch_sampler = paddle.io.BatchSampler(dev_ds, batch_size=batch_size, shuffle=False)
test_batch_sampler = paddle.io.BatchSampler(test_ds, batch_size=batch_size, shuffle=False)

train_loader = paddle.io.DataLoader(train_ds, batch_sampler=train_batch_sampler, collate_fn=batchify_fn)
dev_loader = paddle.io.DataLoader(dev_ds, batch_sampler=dev_batch_sampler, collate_fn=batchify_fn)
test_loader = paddle.io.DataLoader(test_ds, batch_sampler=test_batch_sampler, collate_fn=batchify_fn)

3.1.2模型构建

本案例中,我们将基于SKEP模型实现图1所展示的评论观点抽取功能。具体来讲,我们将处理好的文本数据输入SKEP模型中,SKEP将会对文本的每个token进行编码,产生对应向量序列。接下来,我们将基于该向量序列进行预测每个位置上的输出标签。相应代码如下。

class SkepForTokenClassification(paddle.nn.Layer):
    def __init__(self, skep, num_classes=2, dropout=None):
        super(SkepForTokenClassification, self).__init__()
        self.num_classes = num_classes
        self.skep = skep
        self.dropout = paddle.nn.Dropout(dropout if dropout is not None else self.skep.config["hidden_dropout_prob"])
        self.classifier = paddle.nn.Linear(self.skep.config["hidden_size"], num_classes)

    def forward(self, input_ids, token_type_ids=None, position_ids=None, attention_mask=None):
        sequence_output, _ = self.skep(input_ids, token_type_ids=token_type_ids, position_ids=position_ids, attention_mask=attention_mask)

        sequence_output = self.dropout(sequence_output)
        logits = self.classifier(sequence_output)
        return logits

3.1.3 训练配置

接下来,定义情感分析模型训练时的环境,包括:配置训练参数、配置模型参数,定义模型的实例化对象,指定模型训练迭代的优化算法等,相关代码如下。

# model hyperparameter  setting
num_epoch = 20
learning_rate = 3e-5
weight_decay = 0.01
warmup_proportion = 0.1
max_grad_norm = 1.0
log_step = 20
eval_step = 100
seed = 1000
checkpoint = "./checkpoint/"

set_seed(seed)
use_gpu = True if paddle.get_device().startswith("gpu") else False
if use_gpu:
    paddle.set_device("gpu:0")
if not os.path.exists(checkpoint):
    os.mkdir(checkpoint)

skep = SkepModel.from_pretrained(model_name)
model = SkepForTokenClassification(skep, num_classes=len(label2id))

num_training_steps = len(train_loader) * num_epoch
lr_scheduler = LinearDecayWithWarmup(learning_rate=learning_rate, total_steps=num_training_steps, warmup=warmup_proportion)
decay_params = [p.name for n, p in model.named_parameters() if not any(nd in n for nd in ["bias", "norm"])]
grad_clip = paddle.nn.ClipGradByGlobalNorm(max_grad_norm)
optimizer = paddle.optimizer.AdamW(learning_rate=lr_scheduler, parameters=model.parameters(), weight_decay=weight_decay, apply_decay_param_fun=lambda x: x in decay_params, grad_clip=grad_clip)

metric = ChunkEvaluator(label2id.keys())

3.1.4 模型训练与测试

本节我们将定义一个train函数和evaluate函数,其将分别进行训练和评估模型。在训练过程中,每隔log_steps步打印一次日志,每隔eval_steps步进行评估一次模型,并始终保存验证效果最好的模型。相关代码如下:


def evaluate(model, data_loader, metric):

model.eval()
metric.reset()
for idx, batch_data in enumerate(data_loader):
input_ids, token_type_ids, seq_lens, labels = batch_data
logits = model(input_ids, token_type_ids=token_type_ids)

# count metric
predictions = logits.argmax(axis=2)
num_infer_chunks, num_label_chunks, num_correct_chunks = metric.compute(seq_lens, predictions, labels)
metric.update(num_infer_chunks.numpy(), num_label_chunks.numpy(), num_correct_chunks.numpy())

precision, recall, f1 = metric.accumulate()
return precision, recall, f1

def train():
# start to train model
global_step, best_f1 = 1, 0.
model.train()
for epoch in range(1, num_epoch+1):
for batch_data in train_loader():
input_ids, token_type_ids, _, labels = batch_data
# logits: batch_size, seql_len, num_tags
logits = model(input_ids, token_type_ids=token_type_ids)
loss = F.cross_entropy(logits.reshape([-1, len(label2id)]), labels.reshape([-1]), ignore_index=-1)

loss.backward()
lr_scheduler.step()
optimizer.step()
optimizer.clear_grad()

if global_step > 0 and global_step % log_step == 0:
print(f"epoch: {epoch} - global_step: {global_step}/{num_training_steps} - loss:{loss.numpy().item():.6f}")
if (global_step > 0 and global_step % eval_step == 0) or global_step == num_training_steps:
precision, recall, f1 = evaluate(model, dev_loader, metric)
model.train()
if f1 > best_f1:
print(f"best F1 performence has been updated: {best_f1:.5f} --> {f1:.5f}")
best_f1 = f1
paddle.save(model.state_dict(), f"{checkpoint}/best_ext.pdparams")
print(f'evalution result: precision: {precision:.5f}, recall: {recall:.5f}, F1: {f1:.5f}')

global_step += 1

paddle.save(model.state_dict(), f"{checkpoint}/final_ext.pdparams")

train()

接下来,我们将加载训练过程中评估效果最好的模型,并使用测试集进行测试。相关代码如下。

# load model
model_path = "./checkpoint/best_ext.pdparams"

loaded_state_dict = paddle.load(model_path)
skep = SkepModel.from_pretrained(model_name)
model = SkepForTokenClassification(skep, num_classes=len(label2id))  
model.load_dict(loaded_state_dict)
# evalute on test data
precision, recall, f1  = evaluate(model, test_loader,  metric)
print(f'evalution result: precision: {precision:.5f}, recall: {recall:.5f},  F1: {f1:.5f}')

3. 2属性级情感分类模型

3.2.1数据预处理

(1)数据集介绍

本实践中包含训练集、评估和测试3项数据集,以及1个标签词典。其中标签词典记录了两类情感标签:正向和负向。

另外,数据集中需要包含3列数据:文本串和相应的序列标签数据,下面给出了一条样本,其中第1列是情感标签,第2列是评论属性和观点,第3列是原文。

1 口味清淡 口味很清淡,价格也比较公道

(2)数据加载

本节我们将训练、评估和测试数据集,以及标签词典加载到内存中。相关代码如下:

import os
import argparse
from functools import partial
import paddle
import paddle.nn.functional as F
from paddlenlp.metrics import AccuracyAndF1
from paddlenlp.datasets import load_dataset
from paddlenlp.data import Pad, Stack, Tuple
from paddlenlp.transformers import SkepTokenizer, SkepModel, LinearDecayWithWarmup
from utils.utils import set_seed, decoding, is_aspect_first, concate_aspect_and_opinion, format_print
from utils import data_ext, data_cls

train_path = "./data/data121242/train_cls.txt"
dev_path = "./data/data121242/dev_cls.txt"
test_path = "./data/data121242/test_cls.txt"
label_path = "./data/data121242/label_cls.dict"

# load and process data
label2id, id2label = data_cls.load_dict(label_path)
train_ds = load_dataset(data_cls.read, data_path=train_path, lazy=False)
dev_ds =  load_dataset(data_cls.read, data_path=dev_path, lazy=False)
test_ds =  load_dataset(data_cls.read, data_path=test_path, lazy=False)

# print examples
for example in train_ds[:2]:
    print(example)
import os
import argparse
from functools import partial
import paddle
import paddle.nn.functional as F
from paddlenlp.metrics import AccuracyAndF1
from paddlenlp.datasets import load_dataset
from paddlenlp.data import Pad, Stack, Tuple
from paddlenlp.transformers import SkepTokenizer, SkepModel, LinearDecayWithWarmup
from utils.utils import set_seed, decoding, is_aspect_first, concate_aspect_and_opinion, format_print
from utils import data_ext, data_cls

(3)将数据转换成特征形式

在将数据加载完成后,接下来,我们将各项数据集转换成适合输入模型的特征形式,即将文本字符串数据转换成字典id的形式。这里我们要加载paddleNLP中的SkepTokenizer,其将帮助我们完成这个字符串到字典id的转换。

model_name = "skep_ernie_1.0_large_ch"
batch_size = 8
max_seq_len = 512

tokenizer = SkepTokenizer.from_pretrained(model_name)
trans_func = partial(data_cls.convert_example_to_feature, tokenizer=tokenizer, label2id=label2id, max_seq_len=max_seq_len)
train_ds = train_ds.map(trans_func, lazy=False)
dev_ds = dev_ds.map(trans_func, lazy=False)
test_ds = test_ds.map(trans_func, lazy=False)

# print examples
# print examples
for example in train_ds[:2]:
    print("input_ids: ", example[0])
    print("token_type_ids: ", example[1])
    print("seq_len: ", example[2])
    print("label: ", example[3])
    print()

(4)构造DataLoader

接下来,我们需要根据加载至内存的数据构造DataLoader,该DataLoader将支持以batch的形式将数据进行划分,从而以batch的形式训练相应模型。

batchify_fn = lambda samples, fn=Tuple(
        Pad(axis=0, pad_val=tokenizer.pad_token_id),
        Pad(axis=0, pad_val=tokenizer.pad_token_type_id),
        Stack(dtype="int64"),
        Stack(dtype="int64")
    ): fn(samples)

train_batch_sampler = paddle.io.BatchSampler(train_ds, batch_size=batch_size, shuffle=True)
dev_batch_sampler = paddle.io.BatchSampler(dev_ds, batch_size=batch_size, shuffle=False)
test_batch_sampler = paddle.io.BatchSampler(test_ds, batch_size=batch_size, shuffle=False)

train_loader = paddle.io.DataLoader(train_ds, batch_sampler=train_batch_sampler, collate_fn=batchify_fn)
dev_loader = paddle.io.DataLoader(dev_ds, batch_sampler=dev_batch_sampler, collate_fn=batchify_fn)
test_loader = paddle.io.DataLoader(test_ds, batch_sampler=test_batch_sampler, collate_fn=batchify_fn)

3.2.2模型构建

本案例中,我们将基于SKEP模型实现图1所展示的评论观点抽取功能。具体来讲,我们将处理好的文本数据输入SKEP模型中,SKEP将会对文本的每个token进行编码,产生对应向量序列。我们使用CLS位置对应的输出向量进行情感分类。相应代码如下。

class SkepForSequenceClassification(paddle.nn.Layer):
    def __init__(self, skep, num_classes=2, dropout=None):
        super(SkepForSequenceClassification, self).__init__()
        self.num_classes = num_classes
        self.skep = skep
        self.dropout = paddle.nn.Dropout(dropout if dropout is not None else self.skep.config["hidden_dropout_prob"])
        self.classifier = paddle.nn.Linear(self.skep.config["hidden_size"], num_classes)

    def forward(self, input_ids, token_type_ids=None, position_ids=None, attention_mask=None):
        _, pooled_output = self.skep(input_ids, token_type_ids=token_type_ids, position_ids=position_ids, attention_mask=attention_mask)

        pooled_output = self.dropout(pooled_output)
        logits = self.classifier(pooled_output)
        return logits

3.2.3 训练配置

接下来,定义情感分析模型训练时的环境,包括:配置训练参数、配置模型参数,定义模型的实例化对象,指定模型训练迭代的优化算法等,相关代码如下。

# model hyperparameter  setting
num_epoch = 20
learning_rate = 3e-5
weight_decay = 0.01
warmup_proportion = 0.1
max_grad_norm = 1.0
log_step = 20
eval_step = 100
seed = 1000
checkpoint = "./checkpoint/"

set_seed(seed)
use_gpu = True if paddle.get_device().startswith("gpu") else False
if use_gpu:
    paddle.set_device("gpu:0")
if not os.path.exists(checkpoint):
    os.mkdir(checkpoint)

skep = SkepModel.from_pretrained(model_name)
model = SkepForSequenceClassification(skep, num_classes=len(label2id))

num_training_steps = len(train_loader) * num_epoch
lr_scheduler = LinearDecayWithWarmup(learning_rate=learning_rate, total_steps=num_training_steps, warmup=warmup_proportion)
decay_params = [p.name for n, p in model.named_parameters() if not any(nd in n for nd in ["bias", "norm"])]
grad_clip = paddle.nn.ClipGradByGlobalNorm(max_grad_norm)
optimizer = paddle.optimizer.AdamW(learning_rate=lr_scheduler, parameters=model.parameters(), weight_decay=weight_decay, apply_decay_param_fun=lambda x: x in decay_params, grad_clip=grad_clip)

metric = AccuracyAndF1()

3.2.4 模型训练与测试

本节我们将定义一个train函数和evaluate函数,其将分别进行训练和评估模型。在训练过程中,每隔log_steps步打印一次日志,每隔eval_steps步进行评估一次模型,并始终保存验证效果最好的模型。相关代码如下:

def evaluate(model, data_loader, metric):

    model.eval()
    metric.reset()
    for batch_data in data_loader:
        input_ids, token_type_ids, _, labels = batch_data
        logits = model(input_ids, token_type_ids=token_type_ids)  
        correct = metric.compute(logits, labels)
        metric.update(correct)

    accuracy, precision, recall, f1, _ = metric.accumulate()

    return accuracy, precision, recall, f1

def train():
    # start to train model
    global_step, best_f1 = 1, 0.
    model.train()
    for epoch in range(1, num_epoch+1):
        for batch_data in train_loader():
            input_ids, token_type_ids, _, labels = batch_data
            # logits: batch_size, seql_len, num_tags
            logits = model(input_ids, token_type_ids=token_type_ids)  
            loss = F.cross_entropy(logits, labels)

            loss.backward()
            lr_scheduler.step()
            optimizer.step()
            optimizer.clear_grad()

            if global_step > 0 and global_step % log_step == 0:
                print(f"epoch: {epoch} - global_step: {global_step}/{num_training_steps} - loss:{loss.numpy().item():.6f}")
            if (global_step > 0 and global_step % eval_step == 0) or global_step == num_training_steps:
                accuracy, precision, recall, f1  = evaluate(model, dev_loader,  metric)
                model.train()
                if f1 > best_f1:
                    print(f"best F1 performence has been updated: {best_f1:.5f} --> {f1:.5f}")
                    best_f1 = f1
                    paddle.save(model.state_dict(), f"{checkpoint}/best_cls.pdparams")
                print(f'evalution result: accuracy:{accuracy:.5f} precision: {precision:.5f}, recall: {recall:.5f},  F1: {f1:.5f}')

            global_step += 1

    paddle.save(model.state_dict(), f"{checkpoint}/final_cls.pdparams")

train()

接下来,我们将加载训练过程中评估效果最好的模型,并使用测试集进行测试。相关代码如下。

# load model
model_path = "./checkpoint/best_cls.pdparams"

loaded_state_dict = paddle.load(model_path)
skep = SkepModel.from_pretrained(model_name)
model = SkepForSequenceClassification(skep, num_classes=len(label2id))  
model.load_dict(loaded_state_dict)
accuracy, precision, recall, f1  = evaluate(model, test_loader,  metric)
print(f'evalution result: accuracy:{accuracy:.5f} precision: {precision:.5f}, recall: {recall:.5f},  F1: {f1:.5f}')

3.3 全流程模型推理

paddlepaddle也公开了训练好的模型参数,可以直接下载进行推理

# 下载评论观点抽取模型
!wget https://bj.bcebos.com/paddlenlp/models/best_ext.pdparams
# 下载属性级情感分类模型
!wget https://bj.bcebos.com/paddlenlp/models/best_cls.pdparams
label_ext_path = "./data/data121190/label_ext.dict"
label_cls_path = "./data/data121242/label_cls.dict"
ext_model_path = "./best_ext.pdparams"
cls_model_path = "./best_cls.pdparams"

# load dict
model_name = "skep_ernie_1.0_large_ch"
ext_label2id, ext_id2label = data_ext.load_dict(label_ext_path)
cls_label2id, cls_id2label = data_cls.load_dict(label_cls_path)
tokenizer = SkepTokenizer.from_pretrained(model_name)
print("label dict loaded.")

# load ext model
ext_state_dict = paddle.load(ext_model_path)
ext_skep = SkepModel.from_pretrained(model_name)
ext_model = SkepForTokenClassification(ext_skep, num_classes=len(ext_label2id))  
ext_model.load_dict(ext_state_dict)
print("extraction model loaded.")

# load cls model
cls_state_dict = paddle.load(cls_model_path)
cls_skep = SkepModel.from_pretrained(model_name)
cls_model = SkepForSequenceClassification(cls_skep, num_classes=len(cls_label2id))  
cls_model.load_dict(cls_state_dict)
print("classification model loaded.")
from tqdm import tqdm
import pandas as pd
tqdm.pandas()


Hotel_reviews1 = pd.read_excel(
    './data/2018-2019茂名(含自媒体).xlsx', sheet_name=0)   # 酒店评论
Scenic_reviews1 = pd.read_excel(
    './data/2018-2019茂名(含自媒体).xlsx', sheet_name=1)  # 景区评论
Travel_tips1 = pd.read_excel(
    './data/2018-2019茂名(含自媒体).xlsx', sheet_name=2)     # 游记攻略
Dining_reviews1 = pd.read_excel(
    './data/2018-2019茂名(含自媒体).xlsx', sheet_name=3)  # 餐饮评论
Wechat_article1 = pd.read_excel(
    './data/2018-2019茂名(含自媒体).xlsx', sheet_name=4)  # 微信公众号文章


Hotel_reviews2 = pd.read_excel(
    './data/2020-2021茂名(含自媒体).xlsx', sheet_name=0)   # 酒店评论
Scenic_reviews2 = pd.read_excel(
    './data/2020-2021茂名(含自媒体).xlsx', sheet_name=1)  # 景区评论
Travel_tips2 = pd.read_excel(
    './data/2020-2021茂名(含自媒体).xlsx', sheet_name=2)     # 游记攻略
Dining_reviews2 = pd.read_excel(
    './data/2020-2021茂名(含自媒体).xlsx', sheet_name=3)  # 餐饮评论
Wechat_article2 = pd.read_excel(
    './data/2020-2021茂名(含自媒体).xlsx', sheet_name=4)  # 微信公众号文章

Hotel_reviews = pd.concat([Hotel_reviews1, Hotel_reviews2],axis=0)  # 酒店评论
Scenic_reviews = pd.concat([Scenic_reviews1, Scenic_reviews2], axis=0)  # 景区评论
Travel_tips = pd.concat([Travel_tips1, Travel_tips2], axis=0)  # 游记攻略
Dining_reviews = pd.concat([Dining_reviews1, Dining_reviews2], axis=0)  # 餐饮评论
Wechat_article = pd.concat([Wechat_article1, Wechat_article2], axis=0)  # 微信公众号文章
import pandas as pd
Wechat_article = pd.read_csv('Wechat_article_product.csv',index_col=0).astype(str)  # 微信公众号文章
Travel_tips = pd.read_csv('Travel_tips_product.csv',index_col=0).astype(str)    # 游记攻略
Wechat_article.head()
def addstr(s):
    return '游记ID-'+str(s)


Travel_tips['语料ID'] = Travel_tips['游记ID'].apply(addstr)
# Travel_tips['文本'] = Travel_tips['评论内容'] + '\n'+Dining_reviews['标题']
Travel_tips['产品名称'] = Travel_tips['mmr产品3']
Travel_tips['年份'] = pd.to_datetime(Travel_tips['发布时间']).dt.year
def addstr(s):
    return '文章ID-'+str(s)


Wechat_article['语料ID'] = Wechat_article['文章ID'].apply(addstr)
# Wechat_article['文本'] = Wechat_article['评论内容'] + '\n'+Dining_reviews['标题']
Wechat_article['产品名称'] = Wechat_article['mmr产品3']
Wechat_article['年份'] = pd.to_datetime(Wechat_article['发布时间']).dt.year
Travel_tips.head()
Scenic_reviews.head(10)
def addstr(s):
    return '景区评论-'+str(s)

Scenic_reviews['语料ID'] = Scenic_reviews['景区评论ID'].apply(addstr)
Scenic_reviews['文本'] = Scenic_reviews['评论内容']
Scenic_reviews['产品名称'] = Scenic_reviews['景区名称']
Scenic_reviews['年份'] = pd.to_datetime(Scenic_reviews['评论日期']).dt.year

Scenic_reviews.head(10)
def addstr(s):
    return '酒店评论-'+str(s)

Hotel_reviews['语料ID'] = Hotel_reviews['酒店评论ID'].apply(addstr)
Hotel_reviews['文本'] = Hotel_reviews['评论内容']
Hotel_reviews['产品名称'] = Hotel_reviews['酒店名称']
Hotel_reviews['年份'] = pd.to_datetime(Hotel_reviews['评论日期']).dt.year
Hotel_reviews.head(10)
Dining_reviews.head(10)
def addstr(s):
    return '餐饮评论-'+str(s)


Dining_reviews['语料ID'] = Dining_reviews['餐饮评论ID'].apply(addstr)
Dining_reviews['文本'] = Dining_reviews['评论内容'] + '\n'+Dining_reviews['标题']
Dining_reviews['产品名称'] = Dining_reviews['餐饮名称']
Dining_reviews['年份'] = pd.to_datetime(Dining_reviews['评论日期']).dt.year
Dining_reviews.head(10)
# Scenic_reviews.index = range(len(Scenic_reviews))
Hotel_reviews.index = range(len(Hotel_reviews))
Dining_reviews.index = range(len(Dining_reviews))
Scenic_reviews.head(10)
import re
Scenic_reviews['文本']=Scenic_reviews['文本'].apply(lambda x:''.join(filter(lambda ch: ch not in ' \t◆#%', x)))
Scenic_reviews['文本']= Scenic_reviews['文本'].apply(lambda x: re.sub('&', ' ', x))
Scenic_reviews['文本'] = Scenic_reviews['文本'].apply(lambda x: re.sub('"', ' ', x))
Scenic_reviews['文本'] = Scenic_reviews['文本'].apply(lambda x: re.sub('"', ' ', x))
Scenic_reviews['文本'] = Scenic_reviews['文本'].apply(lambda x: re.sub(' ', ' ', x))
Scenic_reviews['文本'] = Scenic_reviews['文本'].apply(lambda x: re.sub('>', ' ', x))
Scenic_reviews['文本'] = Scenic_reviews['文本'].apply(lambda x: re.sub('<', ' ', x))
strinfo = re.compile('······')
Scenic_reviews['文本'] = Scenic_reviews['文本'].apply(lambda x: re.sub('<', ' ', x))
import re
Hotel_reviews['文本']=Hotel_reviews['文本'].apply(lambda x:''.join(filter(lambda ch: ch not in ' \t◆#%', x)))
Hotel_reviews['文本']= Hotel_reviews['文本'].apply(lambda x: re.sub('&', ' ', x))
Hotel_reviews['文本'] = Hotel_reviews['文本'].apply(lambda x: re.sub('"', ' ', x))
Hotel_reviews['文本'] = Hotel_reviews['文本'].apply(lambda x: re.sub('"', ' ', x))
Hotel_reviews['文本'] = Hotel_reviews['文本'].apply(lambda x: re.sub(' ', ' ', x))
Hotel_reviews['文本'] = Hotel_reviews['文本'].apply(lambda x: re.sub('>', ' ', x))
Hotel_reviews['文本'] = Hotel_reviews['文本'].apply(lambda x: re.sub('<', ' ', x))
strinfo = re.compile('······')
Hotel_reviews['文本'] = Hotel_reviews['文本'].apply(lambda x: re.sub('<', ' ', x))
import re
Dining_reviews['文本']=Dining_reviews['文本'].apply(lambda x:''.join(filter(lambda ch: ch not in ' \t◆#%', x)))
Dining_reviews['文本']= Dining_reviews['文本'].apply(lambda x: re.sub('&', ' ', x))
Dining_reviews['文本'] = Dining_reviews['文本'].apply(lambda x: re.sub('"', ' ', x))
Dining_reviews['文本'] = Dining_reviews['文本'].apply(lambda x: re.sub('"', ' ', x))
Dining_reviews['文本'] = Dining_reviews['文本'].apply(lambda x: re.sub(' ', ' ', x))
Dining_reviews['文本'] = Dining_reviews['文本'].apply(lambda x: re.sub('>', ' ', x))
Dining_reviews['文本'] = Dining_reviews['文本'].apply(lambda x: re.sub('<', ' ', x))
strinfo = re.compile('······')
Dining_reviews['文本'] = Dining_reviews['文本'].apply(lambda x: re.sub('<', ' ', x))
import re
Travel_tips['正文']=Travel_tips['正文'].apply(lambda x:''.join(filter(lambda ch: ch not in ' \t◆#%', x)))
Travel_tips['正文']= Travel_tips['正文'].apply(lambda x: re.sub('&', ' ', x))
Travel_tips['正文'] = Travel_tips['正文'].apply(lambda x: re.sub('"', ' ', x))
Travel_tips['正文'] = Travel_tips['正文'].apply(lambda x: re.sub('"', ' ', x))
Travel_tips['正文'] = Travel_tips['正文'].apply(lambda x: re.sub(' ', ' ', x))
Travel_tips['正文'] = Travel_tips['正文'].apply(lambda x: re.sub('>', ' ', x))
Travel_tips['正文'] = Travel_tips['正文'].apply(lambda x: re.sub('<', ' ', x))
strinfo = re.compile('······')
Travel_tips['正文'] = Travel_tips['正文'].apply(lambda x: re.sub('<', ' ', x))
import re
Wechat_article['正文']=Wechat_article['正文'].apply(lambda x:''.join(filter(lambda ch: ch not in ' \t◆#%', x)))
Wechat_article['正文']= Wechat_article['正文'].apply(lambda x: re.sub('&', ' ', x))
Wechat_article['正文'] = Wechat_article['正文'].apply(lambda x: re.sub('"', ' ', x))
Wechat_article['正文'] = Wechat_article['正文'].apply(lambda x: re.sub('"', ' ', x))
Wechat_article['正文'] = Wechat_article['正文'].apply(lambda x: re.sub(' ', ' ', x))
Wechat_article['正文'] = Wechat_article['正文'].apply(lambda x: re.sub('>', ' ', x))
Wechat_article['正文'] = Wechat_article['正文'].apply(lambda x: re.sub('<', ' ', x))
strinfo = re.compile('······')
Wechat_article['正文'] = Wechat_article['正文'].apply(lambda x: re.sub('<', ' ', x))
!pip install zhconv
# 繁体转中文
from zhconv import convert
for index,line in enumerate(Scenic_reviews['文本']):
    # print(line)
    lis1=[]
    lis1=convert(str(line),'zh-cn')
    Scenic_reviews.loc[index,'文本']=str(lis1)
    # break
# 繁体转中文
from zhconv import convert
for index,line in enumerate(Hotel_reviews['文本']):
    # print(line)
    lis1=[]
    lis1=convert(str(line),'zh-cn')
    Hotel_reviews.loc[index,'文本']=str(lis1)
    # break
# 繁体转中文
from zhconv import convert
for index,line in enumerate(Dining_reviews['文本']):
    # print(line)
    lis1=[]
    lis1=convert(str(line),'zh-cn')
    Dining_reviews.loc[index,'文本']=str(lis1)
    # break
# 繁体转中文
from zhconv import convert
for index,line in enumerate(Wechat_article['正文']):
    # print(line)
    lis1=[]
    lis1=convert(str(line),'zh-cn')
    Wechat_article.loc[index,'正文']=str(lis1)
    # break
# 繁体转中文
from zhconv import convert
for index,line in enumerate(Travel_tips['正文']):
    # print(line)
    lis1=[]
    lis1=convert(str(line),'zh-cn')
    Travel_tips.loc[index,'正文']=str(lis1)
    # break
Dining_reviews.head(5)
Hotel_reviews.head(5)
# !pip install zhconv
def predict(input_text, ext_model, cls_model, tokenizer, ext_id2label, cls_id2label, max_seq_len=512):

    ext_model.eval()
    cls_model.eval()

    # processing input text
    encoded_inputs = tokenizer(list(input_text), is_split_into_words=True, max_seq_len=max_seq_len,)
    input_ids = paddle.to_tensor([encoded_inputs["input_ids"]])
    token_type_ids = paddle.to_tensor([encoded_inputs["token_type_ids"]])

    # extract aspect and opinion words
    logits = ext_model(input_ids, token_type_ids=token_type_ids)
    predictions = logits.argmax(axis=2).numpy()[0]
    tag_seq = [ext_id2label[idx] for idx in predictions][1:-1]
    aps = decoding(input_text, tag_seq)

    # predict sentiment for aspect with cls_model
    results = []
    for ap in aps:
        aspect = ap[0]
        opinion_words = list(set(ap[1:]))
        aspect_text = concate_aspect_and_opinion(input_text, aspect, opinion_words)

        encoded_inputs = tokenizer(aspect_text, text_pair=input_text, max_seq_len=max_seq_len, return_length=True)
        input_ids = paddle.to_tensor([encoded_inputs["input_ids"]])
        token_type_ids = paddle.to_tensor([encoded_inputs["token_type_ids"]])

        logits = cls_model(input_ids, token_type_ids=token_type_ids)
        prediction = logits.argmax(axis=1).numpy()[0]

        result = {"aspect": aspect, "opinions": opinion_words, "sentiment": cls_id2label[prediction]}
        results.append(result)

    # format_print(results)
    # print(result)
    return results
max_seq_len = 512
input_text = """10月1号全家自驾游去了中国第一滩,沙滩和海水都算挺干净的,停车场按次收费一次30元,停车场保安态度极差,不知他哪来的自信,他自我感觉比较优越,在沙滩旁边有冲淡水浴的店铺,冷水5元一位热水15元一位,但是很多人要排队,于是我们走远一点去沙滩入口图4这家,冷水3元一位,热水10元一位,我们两个小孩要了两个热水加一包洗发水共支付21元,店铺老板娘态度极其恶劣,只顾收钱,收了钱之后什么都不管,刚开始去到问价格,问了三四次才回答,小孩湿身上岸之后感觉比较冷,进去找不到热水间,出来找老板娘,她说我们没给钱,让我先给了,我说明明给过了她才慢吞吞的带我们进去马上又走了,结果热水用不了,我又要跑出去找她,她又慢吞吞的进来,我一个人帮两个小孩洗头冲凉,就算半小时都不为过,但帮我们冲了都还没有15分钟,有人在排队了,她们就开始赶人,后来我就两个小孩用一个花洒,浴间设施很简陋,没有门,只有一块遮不住的布,不分男女,小孩洗洗还可以,大人千万不能洗,我有一个小孩没有带衣服,我妈去车上拿衣服进去,后来我们4个人一起出来,那个老板娘还用乡下话嫌弃的说了一句:“哇,不用钱啊,那么多个人”我只说了句:两个大人帮两个小孩洗澡穿衣服,难免会身水身汗。在这里我想对这个老板娘说:这种鬼地方送给我都不会在这里冲的,生意不能这样做的,或者你在旅游区只想做一次性生意,但你这样子很快会倒闭。洗完澡去吃饭,才知道我们去的那家店铺里面都可以免费洗澡的,而且有单独的洗浴间有冷热水,餐厅出品也不错,服务态度也挺好,一天的不愉快烟消云散。"""
if len(input_text)>512:
    input_text=input_text[0:510]
len(input_text)
a=predict(input_text, ext_model, cls_model, tokenizer, ext_id2label, cls_id2label,  max_seq_len=max_seq_len)
a
max_seq_len = 512
for index,line in enumerate(Scenic_reviews['文本']):
    res1=[]
    res2=[]
    res3=[]
    if len(line)>510:
        line=line[0:510]
    lis=predict(line, ext_model, cls_model, tokenizer, ext_id2label, cls_id2label,  max_seq_len=max_seq_len)
    # print(ress)
    for item in lis:
        for name,values in item.items():
            res1.append(values.title())
            res2.append(values.title())
            res3.append(values.title())
            values.title()
    #    res1.append(list(item.values())[0])
    #    res2.append(list(item.values())[1])
    #    res3.append(list(item.values())[2])
    str1 = ",".join('%s' %a for a in  res1)
    str2 = ",".join('%s' %a for a in  res2)
    str3 = ",".join('%s' %a for a in  res3)
    Scenic_reviews.loc[index,"aspect"]=str1
    Scenic_reviews.loc[index,"opinions"]=str2
    Scenic_reviews.loc[index,"sentiment"]=str3

四、计算产品热度

4.1 方面级情感极性预测及计算情感分数

import collections
max_seq_len = 512
for index,line in enumerate(Hotel_reviews['文本']):
    aspsent=[]
    res1=[]
    res2=[]
    res3=[]
    score=[]
    if len(line)>510:
        line=line[0:510]
    lis=predict(line, ext_model, cls_model, tokenizer, ext_id2label, cls_id2label,  max_seq_len=max_seq_len)
    keys = ['aspect', 'opinions']
    asp=['aspect']
    opi=['opinions']
    sent=['sentiment']
    aspsent = [dict((k, d[k]) for k in keys) for d in lis]
    res1=[d[k] for k in asp for d in lis]
    res2=[d[k] for k in opi for d in lis]
    res3=[d[k] for k in sent for d in lis]
    # 统计情感得分
    score=collections.Counter(res3)
    # print("正向得分",m1["正向"],"负向得分",m1["负向"])
    fin_score=score["正向"]-score["负向"]
    str1 = ",".join('%s' %a for a in res1)
    str2 = ",".join('%s' %a for a in res2)
    str3 = ",".join('%s' %a for a in res3)
    Hotel_reviews.loc[index,"aspect"]=str1
    Hotel_reviews.loc[index,"opinions"]=str2
    Hotel_reviews.loc[index,"sentiment"]=str3
    Hotel_reviews.loc[index,"aspect&opinions"]=str(aspsent)
    Hotel_reviews.loc[index,"情感分数"]=str(fin_score)
import collections
max_seq_len = 512
for index,line in enumerate(Travel_tips['正文']):
    aspsent=[]
    res1=[]
    res2=[]
    res3=[]
    score=[]
    if len(line)>510:
        line=line[0:510]
    lis=predict(line, ext_model, cls_model, tokenizer, ext_id2label, cls_id2label,  max_seq_len=max_seq_len)
    keys = ['aspect', 'opinions']
    asp=['aspect']
    opi=['opinions']
    sent=['sentiment']
    aspsent = [dict((k, d[k]) for k in keys) for d in lis]
    res1=[d[k] for k in asp for d in lis]
    res2=[d[k] for k in opi for d in lis]
    res3=[d[k] for k in sent for d in lis]
    # 统计情感得分
    score=collections.Counter(res3)
    # print("正向得分",m1["正向"],"负向得分",m1["负向"])
    fin_score=score["正向"]-score["负向"]
    str1 = ",".join('%s' %a for a in res1)
    str2 = ",".join('%s' %a for a in res2)
    str3 = ",".join('%s' %a for a in res3)
    Travel_tips.loc[index,"aspect"]=str1
    Travel_tips.loc[index,"opinions"]=str2
    Travel_tips.loc[index,"sentiment"]=str3
    Travel_tips.loc[index,"aspect&opinions"]=str(aspsent)
    Travel_tips.loc[index,"情感分数"]=str(fin_score)
import collections
max_seq_len = 512
for index,line in enumerate(Wechat_article['正文']):
    aspsent=[]
    res1=[]
    res2=[]
    res3=[]
    score=[]
    if len(line)>510:
        line=line[0:510]
    lis=predict(line, ext_model, cls_model, tokenizer, ext_id2label, cls_id2label,  max_seq_len=max_seq_len)
    keys = ['aspect', 'opinions']
    asp=['aspect']
    opi=['opinions']
    sent=['sentiment']
    aspsent = [dict((k, d[k]) for k in keys) for d in lis]
    res1=[d[k] for k in asp for d in lis]
    res2=[d[k] for k in opi for d in lis]
    res3=[d[k] for k in sent for d in lis]
    # 统计情感得分
    score=collections.Counter(res3)
    # print("正向得分",m1["正向"],"负向得分",m1["负向"])
    fin_score=score["正向"]-score["负向"]
    str1 = ",".join('%s' %a for a in res1)
    str2 = ",".join('%s' %a for a in res2)
    str3 = ",".join('%s' %a for a in res3)
    Wechat_article.loc[index,"aspect"]=str1
    Wechat_article.loc[index,"opinions"]=str2
    Wechat_article.loc[index,"sentiment"]=str3
    Wechat_article.loc[index,"aspect&opinions"]=str(aspsent)
    Wechat_article.loc[index,"情感分数"]=str(fin_score)
import collections
max_seq_len = 512
for index,line in enumerate(Dining_reviews['文本']):
    aspsent=[]
    res1=[]
    res2=[]
    res3=[]
    score=[]
    if len(line)>510:
        line=line[0:510]
    lis=predict(line, ext_model, cls_model, tokenizer, ext_id2label, cls_id2label,  max_seq_len=max_seq_len)
    keys = ['aspect', 'opinions']
    asp=['aspect']
    opi=['opinions']
    sent=['sentiment']
    aspsent = [dict((k, d[k]) for k in keys) for d in lis]
    res1=[d[k] for k in asp for d in lis]
    res2=[d[k] for k in opi for d in lis]
    res3=[d[k] for k in sent for d in lis]
    # 统计情感得分
    score=collections.Counter(res3)
    # print("正向得分",m1["正向"],"负向得分",m1["负向"])
    fin_score=score["正向"]-score["负向"]
    str1 = ",".join('%s' %a for a in res1)
    str2 = ",".join('%s' %a for a in res2)
    str3 = ",".join('%s' %a for a in res3)
    Dining_reviews.loc[index,"aspect"]=str1
    Dining_reviews.loc[index,"opinions"]=str2
    Dining_reviews.loc[index,"sentiment"]=str3
    Dining_reviews.loc[index,"aspect&opinions"]=str(aspsent)
    Dining_reviews.loc[index,"情感分数"]=str(fin_score)
Hotel_reviews.to_excel("Hotel_reviews.xlsx")
Dining_reviews.to_excel("Dining_reviews.xlsx")
Wechat_article.to_excel("Wechat_article.xlsx")
Travel_tips.to_excel("Travel_tips.xlsx")
Scenic_reviews.to_excel("Scenic_reviews.xlsx")
aa=eval(Scenic_reviews['aspect&opinions'][0])
for alien in aa:
    print(alien)
    print(type(alien))
Scenic_reviews=Scenic_reviews.drop(["aspect&sentiment","sentiment","情感分数","aspect&opinions","aspect","opinions"],axis=1)
Scenic_reviews.head()
aliens = []
for alien_number in range (0,5):
    new_alien = {'color': 'green', 'points': 5, 'speed': 'slow'}
    aliens.append(new_alien)
# print("The following languages have been mentioned:")
# rt=[]
# for language in favorite_languages.values():
# print(str(favorite_languages.values()))
# ",".join('%s' %a for a in favorite_languages.values())
# for members in aliens:
    # print(members["color"], members["speed"])
    # print(list(members.items())[0]+list(members.items())[1])
    # print(members.values())
    # print(members.keys())
        # print("\tage: {}".format(members["age"]))
        # print("\tcity: {}".format(members["city"]))
keys = ['color', 'speed']
se = ['points']
new_data = [dict((k, d[k]) for k in keys) for d in aliens]
res1 = [d[x] for x in se for d in aliens]
str1 = ",".join('%s' %a for a in  res1)
# dicrs=dict(zip(favorite_languages,favorite_languages[1]))
# new_data
lrs=['正向','负向']
import collections
res1
m1=collections.Counter(lrs)
print(collections.Counter(res1))
print(collections.Counter(str1))
print(collections.Counter(lrs))
print("正向得分",m1["正向"],"负向得分",m1["负向"])
print(m1["正向"]-m1["负向"])

4.2 统计产品情感热度得分

import collections
for index,line in enumerate(Scenic_reviews['sentiment']):
    m1=[]
    m1=collections.Counter(line)
    # res1.append(m1[0])
    # m1['正向']
    # res2.append(list(item.values())[1])
    # res3.append(list(item.values())[2])
    # Scenic_reviews.loc[index,"情感得分"]=str(int(m1['正向'])-int(m1['负向']))
    print("正向得分",m1['正'],"负向得分",m1['负'],line)
    break
colors = ['red', 'blue', 'red', 'green', 'blue', 'blue']
c = collections.Counter(colors)
c
Scenic_reviews.to_excel("s.xlsx")
# max_seq_len = 512
# b=[]
# ress=[]
# df=pd.DataFrame()
# for item in a:
#     # for k,v in item.items():
#     ress.append(list(item.values())[2])
#     print(ress)
#             # print(k,v)
#         # print(v[:])
#         # ','.join(k["aspect"])
#         # abc=","+v[:]
#         # k["aspect"]
#         # print(k)
# df.loc[1,"as"]=str(ress)
# df.head()

五. 致谢

[1] H. Tian et al., “SKEP: Sentiment Knowledge Enhanced Pre-training for Sentiment Analysis,” arXiv:2005.05635 [cs], May 2020, Accessed: Nov. 11, 2021.

[2] 小余同学的博客

第十届“泰迪杯”数据挖掘挑战赛 C题:疫情背景下的周边游需求图谱分析 - 微信公众号文章无监督分类

第二、三问分享在下列链接:[post cid="162" cover="" size=""/]

[scode type="green" size="simple"]

我已在AIStudio将赛题方案上传,可以一键Fork运行。

步骤:未注册的小伙伴需要先去AI Studio进行注册,完善资料后,访问下面方案链接(觉得好的话,记得给我点个关注和小红心哦):

问题一解决方案代码

问题二、三解决方案代码

竞赛地址:https://www.tipdm.org:10010/#/competition/1481159137780998144/question

[/scode]

证书.png

  • 一、项目介绍

    • 1.1 赛题背景
    • 1.2 第一问拟解决问题
    • 1.3 数据展示
  • 二、基于LDA的微信公众号分类

    • 2.1 解决方案
    • 2.2 数据预处理
    • 2.3 LDA分析
    • 2.4 词云展示
    • 2.5 结果展示
  • 三、总结
  • 四、后续的任务

一、项目介绍

1.1 赛题背景

随着互联网和自媒体的繁荣,文本形式的在线旅游(Online Travel Agency,OTA)和游客的用户生成内容(User Generated Content,UGC)数据成为了解旅游市场现状的重要信息来源。OTA和UGC数据的内容较为分散和碎片化,要使用它们对某一特定旅游目的地进行研究时,迫切需要一种能够从文本中抽取相关的旅游要素,并挖掘要素之间的相关性和隐含的高层概念的可视化分析工具。

为此本赛题提出本地旅游图谱这一概念,它在通用知识图谱的基础上加入了更多针对旅游行业的需求。本地旅游图谱采用图的形式直观全面地展示特定旅游目的地“吃住行娱购游”等旅游要素,以及它们之间的关联。图 1所示为我国西藏阿里地区的本地旅游图谱,中心位置节点为旅游目的地“阿里”,它的下层要素包括该地区的重要景点如“冈仁波齐”和“玛旁雍错”,以及“安全”、“住宿”等旅游要素。旅游要素分为多个等级,需要从文本中挖掘出面对不同要素游客所关注的下一级要素。如阿里地区的“安全”要素下包括“高反”、“天气”和“季节”等下一级要素,这个组合是西藏旅游所特有的。旅游要素之间会存在关联关系,如“冈仁波齐”和“玛旁雍错”这两个景点通过“神山圣湖”这一高层概念产生联系,在本地旅游图谱中使用连接两个节点的一条边来表示。

在近年来新冠疫情常态化防控的背景下,我国游客的旅游消费方式已经发生明显的转变。在出境游停滞,跨省游时常因为零散疫情的影响被叫停的情况下,中长程旅游受到非常大的冲击,游客更多选择短程旅游,本地周边游规模暴涨迎来了风口。疫情防控常态化背景下研究分析游客消费需求行为的变化,对于旅游企业产品供给、资源优化配置以及市场持续开拓具有长远而积极的作用。本赛题提供收集自互联网公开渠道的2018年至2021年广东省茂名市的OTA和UGC数据,期待参赛者采用自然语言处理等数据挖掘方法通过建立本地旅游图谱的方式来分析新冠疫情时期该市周边游的发展。

1.2 第一问拟解决问题

构建文本分类模型,对附件1提供的微信公众号的推送文章根据其内容与文旅的相关性分为“相关”和“不相关”两类,并将分类结果以表1的形式保存为文件“result1.csv”。与文旅相关性较强的主题有旅游、活动、节庆、特产、交通、酒店、景区、景点、文创、文化、乡村旅游、民宿、假日、假期、游客、采摘、赏花、春游、踏青、康养、公园、滨海游、度假、农家乐、剧本杀、旅行、徒步、工业旅游、线路、自驾游、团队游、攻略、游记、包车、玻璃栈道、游艇、高尔夫、温泉等等。

  • 该数据文本长度长,且没有任何标签

1.3 数据展示

共有8000多条微信公众号的文章,字数在4000+

import pandas as pd
wechat=pd.read_excel('./data/data175696/_5_微信公众号文章.xlsx')
wechat

二、基于LDA的微信公众号分类

2.1 解决方案

首先对疫情前发布的394篇和疫情后发布的5665篇公众号文章,以文旅这一大主题为目标,建立模型判断公众号文章与文旅的相关性。先对原始数据集进行数据清洗,避免因为存在较大的噪声干扰模型的判断。然后用TF-IDF算法进行特征关键词提取,并将处理好的数据输入LDA模型中进行主题词和主题生成。考虑到细粒度的问题,最终对疫情前的公众号文章生成16个主题,疫情后的公众号文章生成40个主题,通过人工标注的方法对这56个主题进行相关性标注,最后生成公众号文章与文旅相关性表格。

# 安装pyLDAvis和词云库
"""
    TypeError: import_optional_dependency() got an unexpected keyword argument 'errors',
    感谢桨友发现在执行分词遇到错误,原因是因为pandas库版本太低和pyLDAvis不匹配,将pip更新安装最新的pandas即可,安装完后记得重启内核。
"""
!pip install pyLDAvis
!pip install wordcloud
!pip install --upgrade pip
!pip install --upgrade pandas
import os
import pandas as pd
import numpy as np
import re
import jieba
import jieba.posseg as psg
from tqdm import tqdm
# 画图
import warnings
warnings.filterwarnings(action='ignore')

import plotly.express as px
from plotly.subplots import make_subplots
import plotly.graph_objects as go

import seaborn as sns
import matplotlib.pyplot as plt
from matplotlib import rcParams
plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签(动态设置)
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号

warnings.filterwarnings("ignore", category=DeprecationWarning) #忽略警告信息
warnings.simplefilter("ignore")
import os
print (os.path.abspath('.'))

2.2 数据预处理

# 加载停用词表
# dic_file = "./lda_t/stop_dic/dict.txt"
stop_file = "./lda_t/stop_dic/stopword.txt"

2.2.1 采用停词表过滤,并且从深蓝字典以及搜狗字典找了相关的一些字典,导入jieba进行分词

def chinese_word_cut(mytext):
    jieba.load_userdict('./lda_t/字典/常用水果.txt')
    jieba.load_userdict('./lda_t/字典/旅游常用词汇.txt')
    jieba.load_userdict('./lda_t/字典/茂名.txt')
    jieba.load_userdict('./lda_t/字典/茂名市.txt')
    jieba.load_userdict('./lda_t/字典/茂名市城市信息精选.txt')
    jieba.load_userdict('./lda_t/字典/茂名市信息大全.txt')
    jieba.load_userdict('./lda_t/字典/甜品.txt')
    jieba.initialize()
    try:
        stopword_list = open(stop_file,encoding ='utf-8')
    except:
        stopword_list = []
        print("error in stop_file")
    stop_list = []
    flag_list = ['n','nz','vn']
    for line in stopword_list:
        line = re.sub(u'\n|\\r', '', line)
        stop_list.append(line)
    
    word_list = []
    #jieba分词
    seg_list = psg.cut(mytext)
    for seg_word in seg_list:
        word = re.sub(u'[^\u4e00-\u9fa5]','',seg_word.word)
        #word = seg_word.word  #如果想要分析英语文本,注释这行代码,启动下行代码
        find = 0
        for stop_word in stop_list:
            if stop_word == word or len(word)<2:     #this word is stopword
                    find = 1
                    break
        if find == 0 and seg_word.flag in flag_list:
            word_list.append(word)      
    return (" ").join(word_list)
import jieba
import jieba.analyse

def jiebacut(content):
    content_S = []
    for line in content:
        # jieba分词 精确模式。返回一个列表类型,建议使用
        current_segment = jieba.lcut(line) #每一行都做分词
        if len(current_segment) > 1 and current_segment != '\r\n':
            content_S.append(current_segment)
    return content_S

这里的2个文件,是按照疫情前后进行分开的
2018-2019微信公众号.xlsx;2020-2021微信公众号.xlsx

data = pd.read_excel('./lda_t/data/2018-2019微信公众号.xlsx',sheet_name=0,header=0).astype(str)
data.columns=['id','title','time','content']
data.head()

2.2.2 数据清洗

data['title_cut'] = data['title'].apply(lambda x:''.join(filter(lambda ch: ch not in ' \t◆#%', x)))
data['content_cut'] = data['content'].apply(lambda x:''.join(filter(lambda ch: ch not in ' \t◆#%', x)))
data['title_cut'] = data['title_cut'].apply(lambda x: re.sub('&', ' ', x))
data['title_cut'] = data['title_cut'].apply(lambda x: re.sub('"', ' ', x))
data['title_cut'] = data['title_cut'].apply(lambda x: re.sub('"', ' ', x))

data['content_cut'] = data['content_cut'].apply(lambda x: re.sub('&', ' ', x))
data['content_cut'] = data['content_cut'].apply(lambda x: re.sub('"', ' ', x))
data['content_cut'] = data['content_cut'].apply(lambda x: re.sub('"', ' ', x))

data['content_cut'] = data['content_cut'].apply(lambda x: re.sub(' ', ' ', x))
data['title_cut'] = data['title_cut'].apply(lambda x: re.sub(' ', ' ', x))

data['content_cut'] = data['content_cut'].apply(lambda x: re.sub('>', ' ', x))
data['title_cut'] = data['title_cut'].apply(lambda x: re.sub('>', ' ', x))

data['content_cut'] = data['content_cut'].apply(lambda x: re.sub('<', ' ', x))
data['title_cut'] = data['title_cut'].apply(lambda x: re.sub('<', ' ', x))
data['content_cut'] = data['content_cut'].apply(lambda x: re.sub('hr/', ' ', x))
data['title_cut'] = data['title_cut'].apply(lambda x: re.sub('hr/', ' ', x))

strinfo = re.compile('······')

data['content_cut'] = data['content_cut'].apply(lambda x: re.sub(strinfo, ' ', x))
data['title_cut'] = data['title_cut'].apply(lambda x: re.sub(strinfo, ' ', x))
data.head()
data['content_cutted'] = data['title_cut'] + data['content_cut']

content_cutted_list = []
for content in tqdm(data.content):
    content_cutted_list.append(chinese_word_cut(content))

data["content_cutted"] = content_cutted_list
data.head()
from tqdm import tqdm

content_cutted_list = []
for content in tqdm(data.content):
    content_cutted_list.append(chinese_word_cut(content))

data["content_cutted"] = content_cutted_list
data["content_cutted"] = ""
for index, row in tqdm(data.iterrows(), total=data.shape[0]):
    data.at[index, "content_cutted"] = chinese_word_cut_with_progress(row["content"])
data.head()

2.3 LDA分析

我们用LDA将上述处理好的content_cut进行LDA分析,生成Topk个主题。

from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation
def print_top_words(model, feature_names, n_top_words):
    tword = []
    for topic_idx, topic in enumerate(model.components_):
        print("Topic #%d:" % topic_idx)
        topic_w = " ".join([feature_names[i] for i in topic.argsort()[:-n_top_words - 1:-1]])
        tword.append(topic_w)
        print(topic_w)
    return tword
n_features = 1000 #提取1000个特征词语
tf_vectorizer = CountVectorizer(strip_accents = 'unicode',
                                max_features=n_features,
                                stop_words='english',
                                max_df = 0.5,
                                min_df = 10)
tf = tf_vectorizer.fit_transform(data.content_cutted)
# print((tf_vectorizer.get_feature_names()))
# print(tf_vectorizer.vocabulary_)  # 索引
n_topics = 16 #主题的数量
lda = LatentDirichletAllocation(n_components=n_topics
                                , max_iter=50
                                ,learning_method='batch',
                                learning_offset=50,
                                # doc_topic_prior=0.1, #α 若不指定,模型会自动选择
                                # topic_word_prior=0.01,#β
                               random_state=0)
lda.fit(tf)
n_top_words = 25
tf_feature_names = tf_vectorizer.get_feature_names()
topic_word = print_top_words(lda, tf_feature_names, n_top_words)

设置主题词的个数

n_top_words = 25 

2.3.1 每个topic对应的关键词所占的比例图

def plot_top_words(model, feature_names, n_top_words, title):
    fig, axes = plt.subplots(4, 4, figsize=(60, 50), sharex=True)
    axes = axes.flatten()
    for topic_idx, topic in enumerate(model.components_):
        top_features_ind = topic.argsort()[: -n_top_words - 1 : -1]
        top_features = [feature_names[i] for i in top_features_ind]
        weights = topic[top_features_ind]

        ax = axes[topic_idx]
        ax.barh(top_features, weights, height=0.7)
        ax.set_title(f"Topic {topic_idx +1}", fontdict={"fontsize": 30})
        ax.invert_yaxis()
        ax.tick_params(axis="both", which="major", labelsize=20)
        for i in "top right left".split():
            ax.spines[i].set_visible(False)
        fig.suptitle(title, fontsize=40)

    plt.subplots_adjust(top=0.90, bottom=0.05, wspace=0.90, hspace=0.3)
    plt.show()


feature_names = tf_vectorizer.get_feature_names()
plot_top_words(lda, feature_names, n_top_words, "Topics in LDA model")
topics=lda.transform(tf) #获得每个文章对应的主题概率
labels = topics.argmax(-1)
# vect = CountVectorizer(max_df=0.95, min_df=2, max_features=n_features, stop_words="english")
# X-tf = vect.fit_transform(df.text.tolist())
data['topic'] = labels
data.head()
lda.components_[0].shape

2.3.2 输出主题对应的example

for topic_idx in range(n_topics):
    topic = lda.components_[topic_idx]
    top_features_ind = topic.argsort()[: -n_top_words - 1 : -1]
    top_features = [feature_names[i] for i in top_features_ind]
    samples = data[data['topic'] == topic_idx].sample(5)
    print("=========================================================")
    print(f"TOPIC {topic_idx + 1}")
    print(f"  Top words: {top_features}")
    print("=========================================================")
    for sample_idx, sample in enumerate(samples['content'].tolist(), 1):
        print(f"Example {sample_idx}:")
        print(sample)
        print()
        print('---------------------')
        print()
    print()
    print()
    print()

2.3.3 输出每篇文章对应主题

topic = []
for t in topics:
    topic.append("Topic #"+str(list(t).index(np.max(t))))
data['概率最大的主题序号']=topic
data['每个主题对应概率']=list(topics)
data.to_excel("data_topic_2018_2019.xlsx",index=False)

2.3.4 可视化

import pyLDAvis
import pyLDAvis.sklearn
pyLDAvis.enable_notebook()
pic = pyLDAvis.sklearn.prepare(lda, tf, tf_vectorizer)
pyLDAvis.display(pic)
pyLDAvis.save_html(pic, 'lda_pass'+str(n_topics)+'.html')
# pyLDAvis.display(pic)
# pyLDAvis.show(pic)

生成后在目录中的html文件,打开可以对每个主题进行拖拽查看

生成的可视化LDA]

2.3.5 困惑度计算调优

import matplotlib.pyplot as plt
plexs = []
scores = []
n_max_topics = 20
for i in range(1,n_max_topics):
    print(i)
    lda = LatentDirichletAllocation(n_components=i, max_iter=50,
                                    learning_method='batch',
                                    learning_offset=50,random_state=0)
    lda.fit(tf)
    plexs.append(lda.perplexity(tf))
    scores.append(lda.score(tf))
n_t=19#区间最右侧的值。注意:不能大于n_max_topics
x=list(range(1,n_t+1))
plt.plot(x,plexs[0:n_t])
plt.xlabel("number of topics")
plt.ylabel("perplexity")
plt.show()

2.4 词云展示

word_name=tf_vectorizer.get_feature_names()
word_freq=tf.toarray().sum(axis=0)#每个词对应的词频
dic=dict(zip(word_name,word_freq))
from wordcloud import WordCloud
#开始画图
font_paths = './lda_t/SimHei.ttf' #词云词的字体目录
# picture_path = 'a.jpg' #画词云的背景图
# mask = plt.imread(picture_path)
# wc = WordCloud(font_path=font_paths, mask=mask, background_color='white') #这里换上字体的路径
wc = WordCloud(font_path=font_paths,background_color='white') #这里换上字体的路径
wc.fit_words(dic)
plt.imshow(wc)   
wc.to_file('微信公众号文章的词云图.png')  #保存词云图

2.5 结果展示

2.5.1生成分类结果

我们先将topk的主题,这里我们生成了16个主题,通过主题词判断是否与文旅相关;
然后将对应的主题进行替换

gx = pd.read_excel('./lda_t/18-19主题对应关系.xlsx',header=None)
gx.head()
relation=dict(zip(gx[0],gx[1]))
result=pd.DataFrame(data.loc[:,'id'])
result['文章id']=pd.DataFrame(data.loc[:,'id'])
result=result.drop('id',axis=1)
result.head()
result=result.drop('文章id',axis=1)
result.head()
def to_result():
    for index in range(len(data["概率最大的主题序号"])):
        # print(i)
        data.loc[index,'分类标签']=relation[data.loc[index,"概率最大的主题序号"]]
    result['文章ID']=pd.DataFrame(data.loc[:,'id'])
    result['分类标签']=pd.DataFrame(data.loc[:,'分类标签'])
    result.to_excel("result1_2018-2019.xlsx",index=False)
data=to_result()

2.5.2 输出总结果

最后分别将疫情前和疫情后的数据进行合并生成最终的结果

result1=pd.read_excel('result1_2018-2019.xlsx',header=0)
result2=pd.read_excel('result1_2020-2021.xlsx',header=0)
result_1=pd.concat((result1,result2),axis=0,join='inner')
result_1.head()
result_1.info()
result_1.to_excel("result1.xlsx",index=False)

三、总结

在微信公众号分类这个问题上,我们通过在网上查阅资料和对历年赛题的优秀作品的研究,了解了许多主题模型算法,比如LDA、BERTopic、Author-Topic。通过对各个主题模型分析和对比,最后选择了使用LDA模型,相比之下LDA模型能够更好的解决长文本的主题分类问题,并且LDA能够很好的对接LDAvis可视化工具。首先对文章进行分词特征抽取,之后进行主题词生成和主题生成,最后对生产的关键词进行了分析并进行了可视化。

四、后续的任务

由于篇幅有限,后续2个任务写在了另外一篇
第二问拟解决的问题 周边游产品热度分析
第三问拟解决的问题 本地旅游图谱构建与分析

测试地址:http://101.43.3.117:8501/

演示网页

注册账户并申请API

由于ChatGPT平台并没有对中国开放,因此我们无法按照正常流程通过OpenAI官网来注册OpenAI账号,但是可以通过一些变通的方法(你懂的)或者请国外的亲朋好友代你注册账号和申请api_key,一旦我们拿到了api_key就不再受网络的限制,可以正常通过api来和ChatGPT机器人聊天了。

安装相关包

pip install openai
pip install streamlit
pip install streamlit_chat

聊天机器人API接口参数说明

model:模型名词

prompt:您对机器人提出的问题

temperature:温度参数,该参数控制生成文本的随机性级别。较高的温度参数会导致更多变化且可能不太连贯的响应,而较低的t温度参数会产生更可预测且可能更连贯的响应。

max_tokens:应答语句的长度

代码

import openai
import streamlit as st
from streamlit_chat import message
# 设置标题, 功能介绍
st.title("和ChatGPT聊天")
st.sidebar.header("功能介绍")
st.sidebar.info(
    '''这是一个web应用程序,通过OpenAI的API和ChatGPT模型实现交互(聊天)功能。在文本框中输入问题,然后按Enter键查询,接收ChatGPT的回答'''
)

#申请的api_key
openai.api_key = "xxxxxxxxxxxxxxxxx" 
def generate_response(prompt):
    completion=openai.Completion.create(
        model='text-davinci-003',
        prompt=prompt,
        max_tokens=1024,
        temperature=0.6
    )
    message=completion.choices[0].text
    return message
 
st.markdown("#### 我是ChatGPT聊天机器人,我可以回答您的任何问题!")
if 'generated' not in st.session_state:
    st.session_state['generated'] = []
if 'past' not in st.session_state:
    st.session_state['past'] = []
user_input=st.text_input("请输入您的问题:",key='input')
if user_input:
    output=generate_response(user_input)
    st.session_state['past'].append(user_input)
    st.session_state['generated'].append(output)
if st.session_state['generated']:
    for i in range(len(st.session_state['generated'])-1, -1, -1):
        message(st.session_state["generated"][i], key=str(i))
        message(st.session_state['past'][i], 
                is_user=True, 
                key=str(i)+'_user')

Docker安装

Dockerfile制作

# 从python3.8镜像基础上创建
FROM python:3.8
ADD ./ /data
WORKDIR /data

# 设置镜像源,提高pip install 速度
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple \
        && pip install streamlit\
        && pip install openai\
        && pip install streamlit_chat
# streamlit hello创建一个演示页面,映射80端口以便网页访问
#CMD ["streamlit","hello", "--server.port","8501"]
ENTRYPOINT nohup streamlit run index.py
  1. 从项目创建镜像:docker build -t steamlit:1.0 .
  2. 从镜像创建容器:docker run -itd -p 8501:8501 --name steamlit steamlit :1.0
  3. 启动命令 nohup streamlit main.py >steamlit.log 2>&1 &

一、Self-Attention

1.1. 为什么要使用Self-Attention

假设现在一有个词性标注(POS Tags)的任务,例如:输入I saw a saw(我看到了一个锯子)这句话,目标是将每个单词的词性标注出来,最终输出为N, V, DET, N(名词、动词、定冠词、名词)。

这句话中,第一个saw为动词,第二个saw(锯子)为名词。如果想做到这一点,就需要保证机器在看到一个向量(单词)时,要同时考虑其上下文,并且,要能判断出上下文中每一个元素应该考虑多少。例如,对于第一个saw,要更多的关注I,而第二个saw,就应该多关注a。

这个时候,就要Attention机制来提取这种关系:如果一个任务的输入是一个Sequence(一排向量),而且各向量之间有一定关系,那么就要利用Attention机制来提取这种关系。

1.2. 直观的感受下Self-Attention

该图描述了Self-Attention的使用。Self-Attention接受一个Sequence(一排向量,可以是输入,也可以是前面隐层的输出),然后Self-Attention输出一个长度相同的Sequence,该Sequence的每个向量都充分考虑了上下文。 举个例子,输入是I、saw、a、saw,对应向量为:

在经过Self-Attention层之后,实际上计算了Softmax就会变成了这样:

对于第一个saw,它除了自身外,还要考虑 0.34 0.340.34个I;对于第二个saw,它要考虑0.49 0.490.49个a

1.3. Self-Attenion是如何考虑上下文的

如图所示,每个输入都会和其他输入计算一个相关性分数,然后基于该分数,输出包含上下文信息的新向量。

同理,对于b2,也是计算权重然后进行加权求和。

如果按照上面这个式子做,还有两个问题:

  1. α之和不为1,这样会将输入向量放大或缩小
  2. 直接用输入向量去乘的话,拟合能力不够好

对于问题1,通常的做法是将α过一个Softmax(当然也可以选择其他的方式)

对于问题2,通常是将乘个矩阵(该矩阵是训练出来的),然后生成,然后用去乘α。

1.4. 如何计算相关性分数 α

首先,复习下向量相乘。两个向量相乘(做内积),公式为:a ⋅ b = ∣a∣∣b∣cos⁡ θ,通过公式可以很容易得出结论:

  • 两个向量夹角越小(越接近),其内积越大,相关性越高 。反之,两个向量夹角越大,相关性越差,如果夹角为90°,两向量垂直,内积为0,无相关性

通过上面的结论,很容易想到,要计算 a^1a^2的相关性,直接做内积即可,即
α_{1,2} =a1⋅a2。 但如果直接这样,显然不好,例如,句子I saw a saw的saw和saw相关性一定很高(两个一样的向量夹角为0),这样不就错了嘛。

为了解决上面这个问题,Self-Attention又额外“训练”了两个矩阵WqWk

  • Wq 负责对“主角”进行线性变化,将其变换为q,称为query,
  • Wk 负责对“配角”进行线性变化,将其变换为q,称为key

有了WqWk,我们就可以计算 a^1a^2的相关分数α_{1,2},即:

上面这些内容可以汇总成如下图:

上图没有把K1画出来,但实际计算的时候,需要计算K1,即需要计算a1和其自身的相关性分类。

1.5. 将α归一化

还记得上面提到的,α之和不为1,所以,在上面得到了α{1,*}后,还需要过一下Softmax,将α进行归一化。如下图:

计算自注意力机制

最终,会将归一化后的α'{1,*}作为a1与其他向量的相关分数,同理,a2...an向量与其他向量的相关分数也这么求。

  • 不一定非要用Softmax,你开心想用什么都行,说不定效果还不错,也不一定非要归一化。 只是通常是这么做的

致谢