snownlp情感分析源码解析

最近发现了snownlp这个库,这个类库是专门针对中文文本进行文本挖掘的。

主要功能:

  • 中文分词(Character-Based Generative Model
  • 词性标注(TnT 3-gram 隐马)
  • 情感分析(现在训练数据主要是买卖东西时的评价,所以对其他的一些可能效果不是很好,待解决)
  • 文本分类(Naive Bayes)
  • 转换成拼音(Trie树实现的最大匹配)
  • 繁体转简体(Trie树实现的最大匹配)
  • 提取文本关键词(TextRank算法)
  • 提取文本摘要(TextRank算法)
  • tf,idf
  • Tokenization(分割成句子)
  • 文本相似(BM25
  • 支持python3(感谢erning

官网信息:

snownlp github:https://github.com/isnowfy/snownlp

使用及源码分析:

使用snownlp进行情感分析:

from snownlp import SnowNLP

#创建snownlp对象,设置要测试的语句
s = SnowNLP(‘这东西不错。。‘)
# 调用sentiments方法获取积极情感概率
print(s.sentiments)

实现过程:

1.首先从SnowNLP入手,看一下sentiments方法,在sentiments方法中,调用了sentiment下的分类方法。

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from . import normal
from . import seg
from . import tag
from . import sentiment
from .sim import bm25
from .summary import textrank
from .summary import words_merge

class SnowNLP(object):

    def __init__(self, doc):
        self.doc = doc
        self.bm25 = bm25.BM25(doc)

    @property
    def words(self):
        return seg.seg(self.doc)

    @property
    def sentences(self):
        return normal.get_sentences(self.doc)

    @property
    def han(self):
        return normal.zh2hans(self.doc)

    @property
    def pinyin(self):
        return normal.get_pinyin(self.doc)

    @property
    def sentiments(self):
        return sentiment.classify(self.doc)#调用了sentiment的classify分类方法

    @property
    def tags(self):
        words = self.words
        tags = tag.tag(words)
        return zip(words, tags)

    @property
    def tf(self):
        return self.bm25.f

    @property
    def idf(self):
        return self.bm25.idf

    def sim(self, doc):
        return self.bm25.simall(doc)

    def summary(self, limit=5):
        doc = []
        sents = self.sentences
        for sent in sents:
            words = seg.seg(sent)
            words = normal.filter_stop(words)
            doc.append(words)
        rank = textrank.TextRank(doc)
        rank.solve()
        ret = []
        for index in rank.top_index(limit):
            ret.append(sents[index])
        return ret

    def keywords(self, limit=5, merge=False):
        doc = []
        sents = self.sentences
        for sent in sents:
            words = seg.seg(sent)
            words = normal.filter_stop(words)
            doc.append(words)
        rank = textrank.KeywordTextRank(doc)
        rank.solve()
        ret = []
        for w in rank.top_index(limit):
            ret.append(w)
        if merge:
            wm = words_merge.SimpleMerge(self.doc, ret)
            return wm.merge()
        return ret

2.sentiment文件夹下的__init__文件

sentiment中创建了Sentiment对象

首先调用load方法加载训练好的数据字典,然后调用classify方法,在classify方法中实际调用的是Bayes对象中的classify方法。

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import os
import codecs

from .. import normal
from .. import seg
from ..classification.bayes import Bayes

#数据文件路径
data_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                         ‘sentiment.marshal‘)

class Sentiment(object):

    def __init__(self):
        #创建Bayes对象
        self.classifier = Bayes()

    #保存训练好的字典数据
    def save(self, fname, iszip=True):
        self.classifier.save(fname, iszip)

    #加载字典数据
    def load(self, fname=data_path, iszip=True):
        self.classifier.load(fname, iszip)

    #对文档分词
    def handle(self, doc):
        words = seg.seg(doc)
        words = normal.filter_stop(words)
        return words

    # 训练数据集
    def train(self, neg_docs, pos_docs):
        data = []
        #读取消极评论list,同时为每条评论加上neg标签,也放入到一个list中
        for sent in neg_docs:
            data.append([self.handle(sent), ‘neg‘])
        #读取积极评论list,为每条评论加上pos标签
        for sent in pos_docs:
            data.append([self.handle(sent), ‘pos‘])
        #调用分类器的训练数据集方法,对模型进行训练
        self.classifier.train(data)

    #分类
    def classify(self, sent):
        #调用贝叶斯分类器的分类方法,获取分类标签和概率
        ret, prob = self.classifier.classify(self.handle(sent))
        #如果分类标签是pos直接返回概率值
        if ret == ‘pos‘:
            return prob
        #如果返回的是neg,由于显示的是积极概率值,因此用1减去消极概率值
        return 1-prob

classifier = Sentiment()
classifier.load()

#训练数据
def train(neg_file, pos_file):
    #打开消极数据文件
    neg = codecs.open(neg_file, ‘r‘, ‘utf-8‘).readlines()
    pos = codecs.open(pos_file, ‘r‘, ‘utf-8‘).readlines()
    neg_docs = []
    pos_docs = []
    #遍历每一条消极评论,放入到list中
    for line in neg:
        neg_docs.append(line.rstrip("\r\n"))
    #遍历每一条积极评论,放入到list中
    for line in pos:
        pos_docs.append(line.rstrip("\r\n"))
    global classifier
    classifier = Sentiment()
    #训练数据,传入积极、消极评论list
    classifier.train(neg_docs, pos_docs)

#保存数据字典
def save(fname, iszip=True):
    classifier.save(fname, iszip)

#加载数据字典
def load(fname, iszip=True):
    classifier.load(fname, iszip)

#对语句进行分类
def classify(sent):
    return classifier.classify(sent)

sentiment中包含了训练数据集的方法,看一下是如何训练数据集的:
在sentiment文件夹下,包含了以下文件:

neg.txt和pos.txt是已经分类好的评论数据,neg.txt中都是消极评论,pos中是积极评论

sentiment.marshal和sentiment.marshal.3中存放的是序列化后的数据字典,这个也稍后再说

(1)在train()方法中,首先读取消极和积极评论txt文件,然后获取每一条评论,放入到list集合中,格式大致如下

[ ‘ 还没有收到书!!!还没有收到书 ‘ , ‘ 小熊宝宝我觉得孩子不喜欢,能换别的吗 ‘ , ......]

#训练数据
def train(neg_file, pos_file):
    #打开消极数据文件
    neg = codecs.open(neg_file, ‘r‘, ‘utf-8‘).readlines()
    pos = codecs.open(pos_file, ‘r‘, ‘utf-8‘).readlines()
    neg_docs = []
    pos_docs = []
    #遍历每一条消极评论,放入到list中
    for line in neg:
        neg_docs.append(line.rstrip("\r\n"))
    #遍历每一条积极评论,放入到list中
    for line in pos:
        pos_docs.append(line.rstrip("\r\n"))
    global classifier
    classifier = Sentiment()
    #训练数据,传入积极、消极评论list
    classifier.train(neg_docs, pos_docs)

然后调用了Sentiment对象中的train()方法:
在train方法中,遍历了传入的积极、消极评论list,为每条评论进行分词,并为加上了分类标签,此时的数据格式如下:

评论分词后的数据格式:[‘收到‘,‘没有‘...]

加上标签后的数据格式(以消极评论为例):[ [[‘收到‘,‘没有‘ ...],‘neg‘] ,  [[‘小熊‘,‘宝宝‘ ...],‘neg’] ........]]

可以看到每一条评论都是一个list,其中又包含了评论分词后的list和评论的分类标签

# 训练数据集
    def train(self, neg_docs, pos_docs):
        data = []
        #读取消极评论list,对每条评论分词,并加上neg标签,也放入到一个list中
        for sent in neg_docs:
            data.append([self.handle(sent), ‘neg‘])
        #读取积极评论list,为每条评论分词,加上pos标签
        for sent in pos_docs:
            data.append([self.handle(sent), ‘pos‘])
        #调用分类器的训练数据集方法,对模型进行训练
        self.classifier.train(data)

经过了此步骤,已经对数据处理完毕,接下来就可以对数据进行训练

3.classification下的bayes.py

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import sys
import gzip
import marshal
from math import log, exp

from ..utils.frequency import AddOneProb

class Bayes(object):

    def __init__(self):
        #标签数据对象
        self.d = {}
        #所有分类的词数之和
        self.total = 0

    #保存字典数据
    def save(self, fname, iszip=True):
        #创建对象,用来存储训练结果
        d = {}
        #添加total,也就是积极消极评论分词总词数
        d[‘total‘] = self.total
        #d为分类标签,存储每个标签的数据对象
        d[‘d‘] = {}
        for k, v in self.d.items():
            #k为分类标签,v为标签对应的所有分词数据,是一个AddOneProb对象
            d[‘d‘][k] = v.__dict__
        #这里判断python版本
        if sys.version_info[0] == 3:
            fname = fname + ‘.3‘
        #这里可有两种方法可以选择进行存储
        if not iszip:
            ##将序列化后的二进制数据直接写入文件
            marshal.dump(d, open(fname, ‘wb‘))
        else:
            #首先获取序列化后的二进制数据,然后写入文件
            f = gzip.open(fname, ‘wb‘)
            f.write(marshal.dumps(d))
            f.close()

    #加载数据字典
    def load(self, fname, iszip=True):
        #判断版本
        if sys.version_info[0] == 3:
            fname = fname + ‘.3‘
        #判断打开文件方式
        if not iszip:
            d = marshal.load(open(fname, ‘rb‘))
        else:
            try:
                f = gzip.open(fname, ‘rb‘)
                d = marshal.loads(f.read())
            except IOError:
                f = open(fname, ‘rb‘)
                d = marshal.loads(f.read())
            f.close()
        #从文件中读取数据,为total和d对象赋值
        self.total = d[‘total‘]
        self.d = {}
        for k, v in d[‘d‘].items():
            self.d[k] = AddOneProb()
            self.d[k].__dict__ = v

    #训练数据集
    def train(self, data):
        #遍历数据集
        for d in data:
            #d[1]标签-->分类类别
            c = d[1]
            #判断数据字典中是否有当前的标签
            if c not in self.d:
                #如果没有该标签,加入标签,值是一个AddOneProb对象
                self.d[c] = AddOneProb()
            #d[0]是评论的分词list,遍历分词list
            for word in d[0]:
                #调用AddOneProb中的add方法,添加单词
                self.d[c].add(word, 1)
        #计算总词数
        self.total = sum(map(lambda x: self.d[x].getsum(), self.d.keys()))

    #贝叶斯分类
    def classify(self, x):
        tmp = {}
        #遍历每个分类标签
        for k in self.d:
            #获取每个分类标签下的总词数和所有标签总词数,求对数差相当于log(某标签下的总词数/所有标签总词数)
            tmp[k] = log(self.d[k].getsum()) - log(self.total)
            for word in x:
                #获取每个单词出现的频率,log[(某标签下的总词数/所有标签总词数)*单词出现频率]
                tmp[k] += log(self.d[k].freq(word))
        #计算概率,由于直接得到的概率值比较小,这里应该使用了一种方法来转换,原理还不是很明白
        ret, prob = 0, 0
        for k in self.d:
            now = 0
            try:
                for otherk in self.d:
                    now += exp(tmp[otherk]-tmp[k])
                now = 1/now
            except OverflowError:
                now = 0
            if now > prob:
                ret, prob = k, now
        return (ret, prob)
from . import good_turing

class BaseProb(object):

    def __init__(self):
        self.d = {}
        self.total = 0.0
        self.none = 0

    def exists(self, key):
        return key in self.d

    def getsum(self):
        return self.total

    def get(self, key):
        if not self.exists(key):
            return False, self.none
        return True, self.d[key]

    def freq(self, key):
        return float(self.get(key)[1])/self.total

    def samples(self):
        return self.d.keys()

class NormalProb(BaseProb):

    def add(self, key, value):
        if not self.exists(key):
            self.d[key] = 0
        self.d[key] += value
        self.total += value

class AddOneProb(BaseProb):

    def __init__(self):
        self.d = {}
        self.total = 0.0
        self.none = 1

    #添加单词
    def add(self, key, value):
        #更新该类别下的单词总数
        self.total += value
        #如果单词未出现过
        if not self.exists(key):
            #将单词加入对应标签的数据字典中,value设为1
            self.d[key] = 1
            #更新总词数
            self.total += 1
        #如果单词出现过,对该单词的value值加1
        self.d[key] += value

在bayes对象中,有两个属性d和total,d是一个数据字典,total存储所有分类的总词数,经过train方法训练数据集后,d中存储的是每个分类标签的数据key为分类标签,value是一个AddOneProb对象。

def __init__(self):
        self.d = {}
        self.total = 0.0

在AddOneProb对象中,同样存在d和total属性,这里的total存储的是每个分类各自的单词总数,d中存储的是所有出现过的单词,单词作为key,单词出现的次数作为value.
为了下次计算概率时,不用重新训练,可以将训练得到的数据序列化到文件中,下次直接加载文件,将文件反序列为对象,从对象中获取数据即可(save和load方法)。

4.得到训练数据后,使用朴素贝叶斯分类进行分类

该方法可自行查阅。

原文地址:https://www.cnblogs.com/X-knight/p/10853699.html

时间: 2024-10-12 21:28:32

snownlp情感分析源码解析的相关文章

Sprig AOP原理及源码解析

在介绍AOP之前,想必很多人都听说AOP是基于动态代理和反射来实现的,那么在看AOP之前,你需要弄懂什么是动态代理和反射及它们又是如何实现的. 想了解JDK的动态代理及反射的实现和源码分析,请参见下面三篇文章 JDK的动态代理源码分析之一 (http://blog.csdn.net/weililansehudiefei/article/details/73655925) JDK的动态代理源码分析之二(http://blog.csdn.net/weililansehudiefei/article/

【Spring源码分析】.properties文件读取及占位符${...}替换源码解析

前言 我们在开发中常遇到一种场景,Bean里面有一些参数是比较固定的,这种时候通常会采用配置的方式,将这些参数配置在.properties文件中,然后在Bean实例化的时候通过Spring将这些.properties文件中配置的参数使用占位符"${}"替换的方式读入并设置到Bean的相应参数中. 这种做法最典型的就是JDBC的配置,本文就来研究一下.properties文件读取及占位符"${}"替换的源码,首先从代码入手,定义一个DataSource,模拟一下JDB

[java源码解析]对HashMap源码的分析(二)

上文我们讲了HashMap那骚骚的逻辑结构,这一篇我们来吹吹它的实现思想,也就是算法层面.有兴趣看下或者回顾上一篇HashMap逻辑层面的,可以看下HashMap源码解析(一).使用了哈希表得"拉链法". 我打算按这个顺序来讲HashMap:几个关键属性 -> 构造方法-> 存取元素方法 ->解决hash冲突方法->HashMap扩容问题. 4个关键属性: /** *HashMap的存储大小 */ transient int size; /** * HashMa

Mybaits 源码解析 (九)----- 全网最详细,没有之一:一级缓存和二级缓存源码分析

像Mybatis.Hibernate这样的ORM框架,封装了JDBC的大部分操作,极大的简化了我们对数据库的操作. 在实际项目中,我们发现在一个事务中查询同样的语句两次的时候,第二次没有进行数据库查询,直接返回了结果,实际这种情况我们就可以称为缓存. Mybatis的缓存级别 一级缓存 MyBatis的一级查询缓存(也叫作本地缓存)是基于org.apache.ibatis.cache.impl.PerpetualCache 类的 HashMap本地缓存,其作用域是SqlSession,myBat

Java源码解析之HashMap

一.HashMap类声明: HashMap继承于AbstractMap并且实现了接口Map,Cloneable,Serializable. public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {} 二.HashMap类层次: HashMap实现了三个接口,继承一个抽象类.除此之外我们应该知道Object是所有类的超类.之所以有一个A

Flume-ng源码解析之Channel组件

如果还没看过Flume-ng源码解析之启动流程,可以点击Flume-ng源码解析之启动流程 查看 1 接口介绍 组件的分析顺序是按照上一篇中启动顺序来分析的,首先是Channel,然后是Sink,最后是Source,在开始看组件源码之前我们先来看一下两个重要的接口,一个是LifecycleAware ,另一个是NamedComponent 1.1 LifecycleAware @[email protected] interface LifecycleAware {  public void s

gulp源码解析(二)—— vinyl-fs

在上一篇文章我们对 Stream 的特性及其接口进行了介绍,gulp 之所以在性能上好于 grunt,主要是因为有了 Stream 助力来做数据的传输和处理. 那么我们不难猜想出,在 gulp 的任务中,gulp.src 接口将匹配到的文件转化为可读(或 Duplex/Transform)流,通过 .pipe 流经各插件进行处理,最终推送给 gulp.dest 所生成的可写(或 Duplex/Transform)流并生成文件. 本文将追踪 gulp(v4.0)的源码,对上述猜想进行验证. 为了分

socketserver源码解析和协程版socketserver

来,贴上一段代码让你仰慕一下欧socketserver的魅力,看欧怎么完美实现多并发的魅力 client import socket ip_port = ('127.0.0.1',8009) sk = socket.socket() sk.connect(ip_port) sk.settimeout(5) while True: data = sk.recv(1024) print('receive:',data.decode()) inp = input('please input:') sk

Handler机制(四)---Handler源码解析

Handler的主要用途有两个:(1).在将来的某个时刻执行消息或一个runnable,(2)把消息发送到消息队列. 主要依靠post(Runnable).postAtTime(Runnable, long).postDelayed(Runnable, long).sendEmptyMessage(int).sendMessage(Message).sendMessageAtTime(Message).sendMessageDelayed(Message, long)这些方法来来完成消息调度.p