snownlp類庫(中文情感分析)原始碼註釋及使用

勳爵發表於2019-05-14

最近發現了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類庫的安裝:

$ pip install snownlp

使用snownlp進行情感分析:

# -*- coding:utf-8 -*-
from snownlp import SnowNLP

#建立snownlp物件,設定要測試的語句
s = SnowNLP(u'買來給家婆用來洗兒子的衣服的')

print("1",s.words)   
                #將句子分成單詞      
                # ['買', '來', '給', '家婆', '用', '來', '洗', '兒子', '的', '衣服', '的']

s.tags         
                # 例如:[(u'這個', u'r'), (u'東西', u'n'),
                #  (u'真心', u'd'), (u'很', u'd'),
                #  (u'贊', u'Vg')]

# 呼叫sentiments方法獲取積極情感概率 positive的概率
print("2",s.sentiments)

s.pinyin        # 將漢字語句轉換為Pinyin語句
                # 例如:[u'zhe', u'ge', u'dong', u'xi',
                #  u'zhen', u'xin', u'hen', u'zan']
#————————————————————————————————————————————————————————————————————————————————————————————————————————
s = SnowNLP(u'「繁體字」「繁體中文」的叫法在臺灣亦很常見。')

s.han           #將繁體字轉換為簡體字      
                # u'「繁體字」「繁體中文」的叫法
                # 在臺灣亦很常見。'
#————————————————————————————————————————————————————————————————————————————————————————————————————————
text = u'''
自然語言處理是電腦科學領域與人工智慧領域中的一個重要方向。
它研究能實現人與計算機之間用自然語言進行有效通訊的各種理論和方法。
自然語言處理是一門融語言學、電腦科學、數學於一體的科學。
因此,這一領域的研究將涉及自然語言,即人們日常使用的語言,
所以它與語言學的研究有著密切的聯絡,但又有重要的區別。
自然語言處理並不是一般地研究自然語言,
而在於研製能有效地實現自然語言通訊的計算機系統,
特別是其中的軟體系統。因而它是電腦科學的一部分。
'''

s = SnowNLP(text)

s.keywords(3)    # [u'語言', u'自然', u'計算機']

s.summary(3)    # [u'因而它是電腦科學的一部分',
                #  u'自然語言處理是一門融語言學、電腦科學、
                #     數學於一體的科學',
                #  u'自然語言處理是電腦科學領域與人工智慧
                #     領域中的一個重要方向']
s.sentences
                #分成句子
#————————————————————————————————————————————————————————————————————————————————————————————————————————
s = SnowNLP([[u'這篇', u'文章'],
             [u'那篇', u'論文'],
             [u'這個']])
print(s.tf)     #TF意思是詞頻(Term Frequency)
print(s.idf)    #IDF意思是逆文字頻率指數(Inverse Document Frequency)  
s.sim([u'文章'])# [0.3756070762985226, 0, 0]

 

實現過程:

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.得到訓練資料後,使用樸素貝葉斯分類進行分類

該方法可自行查閱。

 

相關文章