疫情微博内容情感分析

一.前言

最近,碰到一个疫情微博情感分类的任务看到挺有意思的,就试了试手,顺便记录了下整个实验的全过程,话不多说,请看下文。

二.数据集简介与预处理

2.1 数据集简介

数据集为疫情期间在微博收集的,原始数据集的存储格式为TXT,其中存储的是一个py的列表对象,列表中包含的每条元素都为一条微博内容及其对应的标签,这里摘取了训练集中的一条数据来进行直观的展示:

{"id": 26, "content": "#全国确诊新型肺炎病例# http://t.cn/RXnNTiO ??福州", "label": "neural"}

整个数据集共包含10606条数据,其中训练集有8606条,测试集有2000条数据。数据集的微博内容按情感类别分为如下的6种:

  • neural: 无情绪
  • happy: 积极
  • angry: 愤怒
  • sad: 悲伤
  • fear: 恐惧
  • surprise: 惊奇

2.2 数据集预处理

对于微博内容,我们首先需要进行数据的清洗以及分词,分词可以用jieba分词,但我在github上看到有大佬实现了专门对微博的内容的清洗以及利用pynlpir进行分析 (传送门),抱着偷懒的心思,这里就直接站在前人的肩膀上了,下面给出对微博内容的清洗源码:

def weibo_process(content):
    """
    功能:清洗微博内容并分词
    """
    processed_content = []
    # Replaces URLs with the word [URL]
    content = re.sub(r'(https?|ftp|file|www\.)[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]', '[URL]', content)
    # Replaces Email with the word [URL]
    content = re.sub(r'[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+[\.][a-zA-Z0-9_-]+', '[URL]', content)
    # Replaces user with the word FORWARD
    content = re.sub(r'(\/\/){0,1}@.*?(:|:| )', '[FORWARD]', content)
    # Replaces number  with the word [N]
    content = re.sub(r'\d+', '[N]', content)
    # Replace 2+ dots with space
    content = re.sub(r'[\.。…]{2,}', '。', content)
    # Replace 2+ ~~ 为 ~
    content = re.sub(r'~{2,}', '~', content)
    # Replace 2+ 叹号 为 一个叹号
    content = re.sub(r'[!!]{2,}', '!', content)
    # Replace 2+ 叹号 为 一个问号
    content = re.sub(r'[??]{2,}', '?', content)
    # 去掉 //
    content = re.sub(r'//', ' ', content)
    # 去掉 引号
    content = re.sub(r'["“”\'‘’]', '', content)

    pynlpir.open(encoding='utf_8', encoding_errors='ignore')
    segments = pynlpir.segment(content, pos_tagging=False)
    i = 1
    count = len(segments) - 1
    for segment in segments:
        if re.match(r'\s+', segment):  # 过滤掉空格
            i = i + 1
            continue
        segment = re.sub(r'@[\S]+', '[USER_MENTION]', segment)
        processed_content.append(segment.strip())
        if (i == count) & (segment == '[USER_MENTION]'):  # 过滤掉最后一个单独的字
            break
        i = i + 1
    pynlpir.close()
    return processed_content

通过json模型我们可以读取原始数据集进行清洗和分词后存储到下来(方便后续的加载),源码如下:

ignore_chars = ["/","@","【","】","#",":","[","]"]

def datasetProcess(org_path,save_path,stop_words):
    """
    功能:过滤出微博内容重点中文并进行分词
    """
    outcome = []
    with open(org_path,"r",encoding="utf-8") as fp:
        for idx,item in enumerate(json.load(fp)):
            print("processing item {}".format(idx))
            content = item.get("content")
            label = item.get("label")
            # content = "".join(regex.findall(chinese,content))
            seg_list = weibo_process(content)
            # seg_list = jieba.cut(content,cut_all=False)
            words = []
            for word in seg_list:
                if word in ignore_chars:
                    continue
                if word not in stop_words:
                    words.append(word)
            outcome.append({"content":words,"label":label})
    
    with open(save_path,"w",encoding="utf-8") as fp:
        json.dump(outcome,fp,ensure_ascii=False)

停用词表选用的是哈工大的,清洗完成后数据集变成了下面栗子中的样式:

{"content": ["[心]", "健康", "平安", "[FORWARD]", "致敬", "疫情", "前线", "医护", "人员", "愿", "所有", "人", "都", "健康", "平安", "白衣天使", "致敬", "[心]"], "label": "happy"}

完成数据集的清洗和预处理后,我又对清洗后的数据集进行了一波分析,其中训练集和测试机中每条数据(分词后)的平均长度分别为19和20。然后我又分析了各种标签的数据在训练集和测试中的分布:

label_distrubution

可以看出,六种标签在训练集和测试集上的分布是基本一致的,由此我们不需要对数据集进行打乱和重新划分。此外,由于神经网络输入的必须是数值类型的数据,因此我们必须得将单词映射为数值,这就需要构建一个单词词典,其对应的源码如下:

def getWordDict(data_path,min_count=5):
    """
    功能:构建单词词典
    """
    word2id = {}
    # 统计词频
    with open(data_path,"r",encoding="utf-8") as fp:
        for item in json.load(fp):
            for word in item['content']:
                if word2id.get(word) == None:
                    word2id[word] = 1
                else:
                    word2id[word] += 1
    # 过滤低频词
    vocab = set()
    for word,count in word2id.items():
        if count >= min_count:
            vocab.add(word)

    # 构成单词到索引的映射词典
    word2id = {"PAD":0,"UNK":1}
    length = 2
    for word in vocab:
        word2id[word] = length
        length += 1
    with open("datasets/word2id.json",'w',encoding="utf-8") as fp:
        json.dump(word2id,fp,ensure_ascii=False)

调用该函数得到的单词词典中共包含3493个词,包括"PAD""UNK"两个额外添加的词,其中"PAD"主要是用来完成填充词的映射,因为微博句子的长度不同,但神经网络的输入必须要一致,因此就需要进行填充操作。而"UNK"的为了完成未在词典中出现的词的映射。

2.3 Wrod2Vec词嵌入的生成

对于词嵌入的生成,本实验中提供了两种策略:

  • 直接调用Pytorch中的nn.Embedding先随机初始化,然后在训练的过程中不断更新其权重参数。
  • 利用gensim库来生成word2vec词嵌入,然后用nn.Embedding加载该词嵌入,训练的过程中就可以不更新嵌入层的权重。

在本实验中,利用清洗后的训练集来生成word2vec词嵌入的源码如下所示:

train_path = "datasets/train.txt"
sents = []
with open(train_path,"r",encoding="utf-8") as fp:
    for item in json.load(fp):
        sents.append(item['content'])
model = word2vec.Word2Vec(sents, vector_size=100, window=10, min_count=5,epochs=15,sg=1) 
model.wv.save_word2vec_format('word2vec.bin',binary=False)

由源码可见生成的词嵌入维度为100,由于训练的语料库比较小,因此增大了迭代的次数,另外我发现使用Skip-Gram模式得到的词嵌入的效果要比使用CBOW的效果要好。

此外,由于"PAD""UNK"两个词在数据集中没有,参考自然语言处理这篇文章,采取的策略是"PAD"的词嵌入向量采用全零初始化,而"UNK"的词嵌入采用的是np.random.randn(embedding_dim)的初始化方式。

2.4 数据集的加载

对于数据集的加载,采用的是基础torch.utils.data.Dataset来自定义数据集,对应的源码如下:

import json
import torch
import torch.utils.data as data

word2id = json.load(open("datasets/word2id.json","r",encoding="utf-8"))

label2id = {
    'neural':0,
    'happy':1,
    'angry':2,
    'sad':3,
    'fear':4,
    'surprise':5
}

# maxlen为每条文本的平均单词数+2倍标准差
class WeiBoDataset(data.Dataset):
    def __init__(self,data_path,maxlen=84) -> None:
        super(WeiBoDataset,self).__init__()
        self.maxlen = maxlen
        self.sents,self.labels = self.loadDataset(data_path)
    
    def loadDataset(self,data_path):
        sents,labels = [],[]
        with open(data_path,"r",encoding="utf-8") as fp:
            for item in json.load(fp):
                ids = []
                for ch in item['content'][:self.maxlen]:
                    ids.append(word2id.get(ch,word2id["UNK"]))
                ids = ids[:self.maxlen] if len(ids) > self.maxlen else ids + [word2id["PAD"]] * (self.maxlen - len(ids))
                sents.append(ids)
                labels.append(label2id.get(item['label']))
        f = torch.LongTensor
        return f(sents),f(labels)
    
    def __len__(self):
        return len(self.labels)
    
    def __getitem__(self, index):
        return self.sents[index],self.labels[index]

在数据集,需要对微博内容的长度进行统一,过长的句子需要截断,过短的句子用"PAD"来填充,句子设置的最大长度maxlen为84。

三.模型设计与实现

对于情感分析,我采用的是BiGRU+Linear模型,其中BiGRU来完成句子信息的抽取,然后通过线性层来对句子进行分类,模型的结构图如下所示:

BiGRU+Linear

从图中可以看出,模型为双层双向GRU+线性层,关于模型中的词嵌入,我实现了2.3小节中说的两种策略,具体源码如下:

import torch
import torch.nn as nn
import torch.nn.functional as F
from data_loader import word2id
from gensim_word2vec import build_embdding_matrix

embedding_path="word2vec.bin"

class BiGRU(nn.Module):
    def __init__(self,embedding_dim,hidden_size,output_size,drop_prob,extra_embedding=True):
        super(BiGRU,self).__init__()
        if extra_embedding:
            embedding_matrix = build_embdding_matrix(
                word_dict=word2id,
                embedding_path=embedding_path,
                embedding_dim=embedding_dim)
            embedding_weight = torch.from_numpy(embedding_matrix).float()
            self.embeds = nn.Embedding.from_pretrained(embedding_weight)
        else:
            self.embeds = nn.Embedding(len(word2id),embedding_dim)
            nn.init.uniform_(self.embeds.weight)
        self.gru = nn.GRU(
            bidirectional=True, 
            num_layers=2, 
            input_size=embedding_dim, 
            hidden_size=hidden_size,
            batch_first=True,
            dropout=drop_prob
        )
        self.batchnorm = nn.BatchNorm1d(84)
        self.dropout = nn.Dropout(drop_prob)
        self.decoder = nn.Linear(hidden_size * 2,output_size)
        
    def forward(self,x):
        x = self.embeds(x)
        x,_ = self.gru(x)
        x = self.batchnorm(x)
        x = self.dropout(x)
        x = self.decoder(torch.mean(x,dim=1))
        return x

对于从GRU抽取到的句子的表示,本实验采取的做法是将各个隐藏层状态值求平均。另外,为了减缓模型的过拟合,模型中还添加了Dropout和BatchNorm策略。

四.实验与结果分析

4.1 实验配置

本次实验中的超级参数配置如下表所示:

ParameterConfiguration
learning rate1e-4
batch size64
hidden size128
weight_decay5e-4
dropout rate0.5
epochs100

实验采用的优化器为Adam,为了减缓过拟合,优化器添加了权重衰减策略。

4.2 实验结果

下面展示的是训练集和测试机的loss变化和预测准确率随epoch的变化情况,可以看出测试集上的loss在训练过程中的后期后上升的趋势,说明模型还是过拟合了。另外,在实验的过程中,博主方向学习率为0.01时,训练后期过拟合的趋势更加明显,训练集上准确率急剧上升(超过90%),但测试集的性能却会下降的比较厉害,主要原因是利用训练集作为语料库太小了,得到的词嵌入的性能一般。对此可以考虑,引入更大的语料库来进行训练,或者使用Google开源的语言预训练模型BERT。

loss

acc

五.结语

完整项目源码(有条件的支持一下,感谢!!!)
以上便是本文的全部内容,要是觉得不错的话就点个赞或关注一下博主吧,你们的支持是博主继续创作的不解动力,当然若是有任何问题也敬请批评指正!!!

# emotionAnalysis 疫情背景下,基于情感词典和机器学习对新闻和微博评论的情感分析 # Data Science Basics in SWI, NJU, 2020-Fall > ## 计算社会学:基于NLP技术的新冠疫情下的社会心态研究 Cong Jin , YDJSIR, Sugar Xu‘s project of 2020 Data Science Basic Course in SWI, NJU. 此为发布开源的版本而不是开发环境中使用的版本。 ## 文件结构 ```bash │ LICENSE │ README.md ├─Analyze # 分析数据的过程中所使用的所有代码 ├─Data # 原始数据以及处理过后的所有数据 ├─Report # 报告相关源文件以及最终报告的成品 └─Spyder # 爬虫代码 ``` 文件结构经过事后整理,并不是工作时目录的状态,因而代码中所涉及的路径需要稍加修改后运行。 原始报告数据在评分后抹掉相关关键词后后放出。 > ### `Data`目录下文件结构 > > 该目录下共有6个文件夹,分别对应`stage0` - `stage6` > > ##### stage内文件目录结构 > > ```bash > │ COVkeywords-Stage-.json # 人工筛选后的疫情相关关键词 > │ COVkeywords-Stage.json # 未经筛选的疫情关键词 > │ keywords-Stage.json # 从荔枝新闻中获取的原始结果 > │ ratioByDate.png # 该阶段内每日疫情相关重点微博占比 > │ SaveTest.png # 疫情相关度分布拟合结果图1 > │ SaveTest_Fit.png # 疫情相关度分布拟合结果图2 > │ stageCOVWeibo.json # 该阶段内疫情相关重点微博(按时间先后排序) > │ stageCOVWeiboByImportance.json # 该阶段内疫情相关重点微博(按疫情相关度排序) > | SaveTest-热度.png # 各项热度指标占比 > │ stageInfo.json # 该阶段基础信息 > │ weiboPolar.png # 疫情相关重点微博情感极性图 > | weiboEmotion.png # 当前阶段的疫情相关微博情感倾向 > ├─YYYY-MM-DD- > ├─YYYY-MM-DD- > ├─YYYY-MM-DD- > ├─YYYY-MM-DD- > ... > └─YYYY-MM-DD- > ``` > > ##### 每个日期内文件目录结构 > > ```bash > YYYY-MM-DD > | jstvRAW.csv # 疫情相关关键词检索得到的荔枝新闻原始数据 > | keywords.json # 荔枝新闻正文提取出来的关键词及其乘以100以后的TextRank权值 > | wordcloud.html # 由荔枝新闻生成的词云图 > | blog-Scored.json # 每篇微博都有一个疫情相关度 > | blog-COV.json # 筛选后的新冠疫情相关微博 > | blogInfo.json # 当日博客相关基础信息 > | weiboEmotion.png # 基于心态词典的当日疫情相关微博重点评论情感分析生成的雷达图 > └─weiboEmotion.csv # 基于心态词典的当日疫情相关微博重点评论情感分析原始数据 > ``` ======= # emotionAnalysis 疫情背景下,基于情感词典和机器学习对新闻和微博评论的情感分析
评论 68
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

斯曦巍峨

码文不易,有条件的可以支持一下

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值