1. 程式人生 > >Python--基於樸素貝葉斯演算法的情感分類

Python--基於樸素貝葉斯演算法的情感分類

環境

win8, python3.7, jupyter notebook

正文

什麼是情感分析?(以下引用百度百科定義)

情感分析(Sentiment analysis),又稱傾向性分析,意見抽取(Opinion extraction),意見挖掘(Opinion mining),情感挖掘(Sentiment mining),主觀分析(Subjectivity analysis),它是對帶有情感色彩的主觀性文字進行分析、處理、歸納和推理的過程,如從評論文字中分析使用者對“數碼相機”的“變焦、價格、大小、重量、閃光、易用性”等屬性的情感傾向。

簡單來說, 就是從文字中總結歸納出個人對某一話題的主觀態度(褒義或貶義的兩種或者更多種型別). 對於情感分析的方法中, 目前基於監督學習(已知分類標籤)是主流, 我們選擇監督學習演算法中的樸素貝葉斯進行分析介紹.

1. 樸素貝葉斯

樸素貝葉斯中的樸素是指特徵條件獨立假設, 貝葉斯是指貝葉斯定理, 我們從貝葉斯定理開始說起吧.

1.1 貝葉斯定理

貝葉斯定理是用來描述兩個條件概率之間的關係

1). 什麼是條件概率? 

如果有兩個事件A和B, 條件概率就是指在事件B發生的條件下, 事件A發生的概率, 記作P(A|B).

若P(A)>0, 則滿足以下公式

若P(B) > 0, 同理.

通過條件概率公式我們可以直接推出概率的乘法公式.

2). 概率的乘法公式

進而通過概率的乘法公式, 可以推出貝葉斯公式.

3). 貝葉斯公式

貝葉斯公式同樣滿足條件P(A)>0, P(B)>0, 在該公式中, A和B均代表單個事件, 但是當B代表一個事件組時, 公式又是如何呢?在介紹之前, 引出全概公式

4). 全概公式

當事件組B1, B2, B3, ....Bn是完備事件組(兩兩互不相容, 其和為全集), 並且當P(Bn) >0時, 對於任意一個事件A, 滿足全概公式:

推導過程如下:

那麼, 此時的完備事件組B1, B2, B3, ...Bn對於任意事件A的貝葉斯公式可寫成:

上式中P(Bi)被稱為先驗概率, P(Bi|A)被稱為後驗概率. 

5). 先驗概率, 後驗概率分別指什麼呢? (舉例說明)

某地區10月份下大暴雨的概率為0.9. 下大暴雨時, 發洪水的概率是0.6; 不下大暴雨時, 發洪水的概率為0.02, 試求該地區已發洪水, 下暴雨的概率?

記A1=下大暴雨, B1=發洪水, A2

=不下大暴雨, B2=不發洪水, 由題意知, P(A1) = 0.9, P(B1|A1) = 0.6, P(B1|A2) = 0.02, 根據貝葉斯公式得: P(A1|B1)=0.9*0.6/[0.9*0.6 + (1-0.9)*0.02] = 0.996.

從上述例子中, 先驗概率(下大暴雨的概率)很容易從現有條件中得出, 而後驗概率(已經發洪水時下大暴雨的概率)需要根據附加資訊用貝葉斯公式去計算得出, 下面引出百度百科對於這兩者的定義.

先驗概率(prior probability)是指根據以往經驗和分析得到的概率,如全概率公式,它往往作為"由因求果"問題中的"因"出現的概率

後驗概率是指在得到“結果”的資訊後重新修正的概率,是“執果尋因”問題中的"果"。先驗概率與後驗概率有不可分割的聯絡,後驗概率的計算要以先驗概率為基礎.

事情還沒有發生,要求這件事情發生的可能性的大小,是先驗概率。事情已經發生,要求這件事情發生的原因是由某個因素引起的可能性的大小,是後驗概率。

將定義與例題相結合就能更好的理解先驗概率和後驗概率.

6). 如果A也是事件組時, 公式又是如何呢?

假設現有兩個事件A1和A2, 公式則可改寫為:

要想求滿足條件A1和A2時Bi的概率, 關鍵在於求P(A1, A2|Bi)的概率, 在這之前需要了解事件的獨立性:

通常情況下, 條件概率P(A|B)與無條件概率P(A)是不相等的, 如果P(A|B)=P(A), 則說明事件B對事件A的發生沒有任何影響, 也就是說事件A與B是相互獨立的, 又根據我們上面的概率乘法公式可以推出P(AB)=P(A|B)P(B)=P(A)P(B).

現假設事件A1和事件A2關於事件B獨立, 根據事件的獨立性, 則有P[(A1|B)(A2|B)]=P(A1|B)P(A2|B), 又由於只有在事件B這個共同條件下事件A1才會與事件A2獨立, 我們不妨將式子改寫為P(A1, A2|B) = P(A1|B)P(A2|B), 從而可以將我們的貝葉斯公式改寫為:

假設事件組Am中的所有事件關於完備事件組Bn中任意一個事件兩兩相互獨立, 進而公式可以推廣為:

要想上式成立, 則必須要滿足前提條件: 事件組Am中的所有事件關於事件組Bn的任意一個事件兩兩相互獨立, 到這裡貝葉斯定理部分就介紹完畢了,

1.2 特徵條件獨立假設

將上面的公式應用到分類問題中, Am表示m個特徵, Bn表示n個分類標籤, 而要想上式成立, 要滿足前提條件: m個特徵關於n個分類標籤中任意一個兩兩相互獨立, 也就是特徵條件獨立, 而"樸素"二字表示特徵條件獨立假設, 即假設特徵條件獨立(前提條件), 想必這也就是為什麼"樸素"二字恰好在貝葉斯之前.

1.3 樸素貝葉斯模型

樸素貝葉斯分類模型要做的事就是在先驗概率的基礎上將資料集歸為n個標籤中後驗概率最大的標籤(基於最小錯誤率貝葉斯決策原則).

由於資料集的特徵個數m和分類標籤總數n是固定不變的, 即貝葉斯定理中分母不變, 所以要求最大值, 只需求出分子中的最大值, 即下式中的最大值

又可利用argmax()函式, 表示為最大值的類別, 比如X1 = argmax(f(x))表示當f(x)中的自變數x=X1時, f(x)取最大值

即求自變數Bi的值, 當Bn = Bi時, 後驗概率最大, 上式也稱為樸素貝葉斯推導式

通過上式, 影響最終結果的有先驗概率和條件概率, 另外滿足特徵條件獨立假設時, 上式才會成立. 即當先驗概率, 條件概率和特徵條件獨立假設均成立時, 根據樸素貝葉斯推導式得出的結果具有真正最小錯誤率..

1) 先驗概率

當先驗概率已知時, 可以直接通過公式計算.

當先驗概率未知時, 基於最小最大損失準則(讓其在最糟糕的情況下, 帶來的損失最小)或者N-P(聶曼-皮爾遜)決策準則來進行分類

2) 條件概率(舉例說明)

現需對某產品評價中"質量好, 價格便宜, 顏值高"這三個詞語作出統計, 假設它們之間兩兩相互獨立. 已知某論壇上關於該產品的1000條評價中未購買使用者概率為0.2, 未購買使用者的評價中"質量好"出現0次, "價格便宜"出現100次, "顏值高"出現150次; 已購買使用者的評價中"質量好"出現600次, "價格便宜"出現500次, "顏值高"出現700次, 試問評價中同時出現"質量好, 價格便宜, 顏值高"這三個詞語的使用者是未購買使用者的概率.

首先, 特徵條件相互獨立, 且先驗概率已知, 可直接列舉出我們的公式:

事件B1表示未購買使用者, 事件B2表示已購買使用者, 事件A1, A2, A3則分別表示"質量好, 價格便宜, 顏值高".

由題知, P(A1|B1)=0, 分子為0, P(B1|A1, A2, A3)也就為0, 如果我們根據計算結果直接就下定義: 做出"質量好, 價格便宜, 顏值高"這個評價的使用者不可能是未購買使用者, 會有點以偏概全了, 比如現新有一條包含這三個詞語的新評價且為未購買使用者, 經詢問, 該使用者在體驗朋友(已購買使用者)的產品一段時間後, 進而在論壇作此評價. 對於此種情況, 重新計算嗎?假設該使用者在半個月, 一個月...後才出現, 我們要等半個月, 一個月..再計算的話, 有點不切合實際.

對於此種情況, 法國數學家拉普拉斯最早提出用加1的方法估計沒有出現過的現象的概率, 所以加法平滑也叫做拉普拉斯平滑. 假定訓練樣本很大時, 每個分量計數加1造成的估計概率變化可以忽略不計,但可以方便有效的避免零概率問題。(參考自博文: https://www.cnblogs.com/bqtang/p/3693827.html)

應用在我們的示例中就是, P(A1|B1)=1/203, P(A2|B1)=101/203, P(A3|B1)=151/203, 進而求解即可

3) 特徵條件獨立假設

假設特徵條件獨立, 但是實際中往往不是真正獨立甚至有時特徵之間存在某種聯絡(比如年齡和星座), 這時就需通過特徵選擇, 主成分分析等方法儘可能讓特徵之間獨立.

到這裡, 樸素貝葉斯分類模型就介紹完了, 下面就是應用了, 決定將樸素貝葉斯應用在某電視劇的評論上進行情感分類

2. 爬取評論文字

由於我本人想對爬蟲再練習練習, 所以先從爬蟲開始介紹(哈哈, 有點自私了.., 對這部分內容不感興趣的朋友可以跳過)

2.1 目標站點分析

分析的目的是為了找出真正的url, 請求方式等資訊

2.2 新建爬蟲專案

1. 在cmd命令列中執行以下命令, 新建youku專案檔案

scrapy startproject yok

2. 進入yok專案中, 新建spider檔案

cd yok
scrapy genspider pinglun yok.com

3. 新建main.py檔案, 用來執行專案

2.3 定義要爬取的欄位

在items.py檔案中編輯如下程式碼

import scrapy
from scrapy.item import Item, Field
class YoukuItem(Item):
    content = Field()

2.4 編輯爬蟲主程式

在pinglun.py檔案中編輯

import scrapy
import json
from yok.items import YokItem

class PinglunSpider(scrapy.Spider):
    name = 'pinglun'
    allowed_domains = ['yok.com']
    episodes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
    def start_requests(self):
        for ep in self.episodes:
            url = 'https://p.Id={0}&Page=1&pageSize=30'.format(str(ep))
            yield scrapy.Request(url=url, callback=self.parse_pages, meta={'ep':ep}, dont_filter=True)
    def parse_pages(self,response):
      '''獲取總頁數'''
        ep, results= response.meta['ep'], response.text
        results = results.replace('n_commentList(', '').replace(')', '')
        results = json.loads(results, strict=False)
        if results.get('code') == 0:
            pages = results.get('data').get('totalPage')
            for page in range(1, int(pages)+1):
                url = 'https://p.Id={0}Page={1}&pageSize=30'.format(str(ep), str(page))
                yield scrapy.Request(url=url, callback=self.parse, dont_filter=True)
    def parse(self, response):
      '''解析內容'''
        results = response.text
        results= results.replace('n_commentList(', '').replace(')', '')
        #strict表示對json語法要求不嚴格
        results = json.loads(results, strict = False)
        item = YoukuItem()
        if results.get('data'):
            if results.get('data').get('comment'):
                comments = results.get('data').get('comment')
                for comment in comments:
                    item['content'] =comment.get('content').strip().replace(r'\n','')yield item

2.5 儲存到Mysql資料庫

在pipelines.py中編輯

import pymysql
class YokPipeline(object):
    def __init__(self):
        self.db = pymysql.connect(
            host='localhost',
            user='root',
            password='1234',
            db='yok',
            port=3306,
            charset='utf8'
        )
        self.cursor = self.db.cursor()
    def process_item(self, item, spider):
        sql = "INSERT INTO pinglun(content) values(%s)"
        data = (item["content"])
        try:
            self.cursor.execute(sql, data)
            self.db.commit()
        except:
            print('儲存失敗')
            self.db.rollback()
    def closed_spider(self, spider):
        self.cursor.close()
        self.db.close()

2.6 配置settings

BOT_NAME = 'yok'
SPIDER_MODULES = ['yok.spiders']
NEWSPIDER_MODULE = 'yok.spiders'
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36'
ROBOTSTXT_OBEY = False
DOWNLOAD_DELAY = 2
COOKIES_ENABLED = False
DEFAULT_REQUEST_HEADERS = {
    ':authority': 'p.comments.yok.com',
    ':method': 'GET',':scheme': 'https',
    'accept': '*/*',
    'accept-encoding': 'gzip, deflate, br',
    'accept-language': 'zh-CN,zh;q=0.9',
    'referer': 'https://www.yok.com/id.html'
}
ITEM_PIPELINES = {
   'yok.pipelines.YokPipeline': 300,
}

2.7 執行程式

在main.py中編輯

from scrapy import cmdline

cmdline.execute('scrapy crawl pinglun'.split())

最終爬取到1萬5千多條資料.

3. 樸素貝葉斯分類

通過爬蟲分析過程瞭解到我們的資料是無標籤的, 但樸素貝葉斯是有監督的學習, 因此我又爬取了一些同類型電視劇評論的標籤資料, 將問題轉化為情感分類問題, 即通過這些標籤資料進行訓練模型, 進而對無標籤資料進行分類.

3.1 從資料庫中讀取資料

import pandas as pd
import pymysql
#連線資料庫
conn = pymysql.connect(host='localhost', user='root', password='1234', port=3306, db='dn', charset='utf8')
sql = 'select * from pinglun'
#讀取資料
df = pd.read_sql(sql, conn)
#刪除重複記錄
df.drop_duplicates(inplace = True)
#設定行的最大顯示為2條
pd.set_option('display.max_rows', 2)
df

3.2 對資料進行分類

我們依據評分進行情感分類, 大於3分為積極情感, 小於3分為消極情感, 積極情感用1表示, 消極情感用0表示.

import numpy as np
#轉換資料型別
df.score = df.score.astype(int)
teleplay_0 = df[df.score <3]
#通過比對,發現消極情感的總數較少,隨機抽取同數量的積極情感
teleplay_1 = df[df.score > 3].sample(n=teleplay_0.shape[0])
#對兩者進行拼接
teleplay = pd.concat([teleplay_1, teleplay_0], axis=0)
#對情感進行分類, 0消極,1積極
teleplay['emotion'] = np.where(teleplay.score > 3, 1, 0)
teleplay

3.3 對評論進行分詞處理

我這裡採用兩種庫, 一個jieba, 一個是snownlp

1. 安裝兩個庫

pip install jieba
pip install snownlp

2. 對評論進行分詞

from snownlp import SnowNLP
import jieba
#對評論進行分詞, 並以空格隔開
teleplay['cut_jieba'] = teleplay.content.apply(lambda x: ' '.join(jieba.cut(x)))
teleplay['cut_snownlp'] = teleplay.content.apply(lambda x: ' '.join(SnowNLP(x).words))
teleplay

可以看到兩者的分詞結果還是有區別的

3.4 停用詞處理

什麼是停用詞? 我們把"看到", "和", "的", "基本"等這類可忽略的詞彙, 稱為停用詞. 它們的存在反而影響處理效率, 因此將它們除去.

def get_stopwords(path):
    '''讀取停用詞'''
    with open(path) as f:
        stopwords = [i.strip() for i in f.readlines()]
        return stopwords
    
path = r'D:\aPython\Data\stopword.txt'
stopwords = get_stopwords(path)
#分別去除cut_jieba和cut_snownlp中的停用詞
teleplay['cut_jieba'] = teleplay.cut_jieba.apply(lambda x: ' '.join([w for w in (x.split(' ')) if w not in stopwords]))
teleplay['cut_snownlp'] = teleplay.cut_snownlp.apply(lambda x: ' '.join([w for w in (x.split(' ')) if w not in stopwords]))
teleplay

3.5 特徵向量化

特徵向量化的目的就是將mX1的矩陣轉化為mXn的矩陣(其中1表示1維文字, n表示1維文字中的n個詞彙), 分別計算n個特徵詞彙在m行中出現的頻數

1. 在特徵向量化之前按照2:8的比例將資料集隨機劃分為訓練集和測試集.

from sklearn.model_selection import train_test_split
#分別對cut_jieba和cut_snownlp進行劃分
X1, X2, y = teleplay[['cut_jieba']], teleplay[['cut_snownlp']], teleplay.emotion
X1_train, X1_test, y1_train, y1_test = train_test_split(X1, y, test_size=0.2)
X2_train, X2_test, y2_train, y2_test = train_test_split(X2, y, test_size=0.2)

2. 特徵向量化(只對訓練集進行向量化)

from sklearn.feature_extraction.text import CountVectorizer
#初始化
vect = CountVectorizer()
#分別對cut_jieba和cut_snownlp進行向量化, 並轉化為dataframe.
vect_matrix_1 = pd.DataFrame(vect.fit_transform(X1_train.cut_jieba).toarray(), columns = vect.get_feature_names())
vect_matrix_2 = pd.DataFrame(vect.fit_transform(X2_train.cut_snownlp).toarray(), columns = vect.get_feature_names())
vect_matrix_1
vect_matrix_2

 

可以看到向量化的結果中存在05, 07, 08...等數字, 而且這些數字對於分類的結果無太大作用, 需要剔除, 可以選擇在向量化之前藉助正則表示式進行剔除, 下面直接在CountVectorizer中篩選

#max_df的型別若為int, 則表示過濾掉文件中出現次數大於max_df的詞彙, 若為float時, 則是百分比; min_df類似, token_pattern表示token的正則表示式
vect = CountVectorizer(max_df = 0.8, 
                     min_df = 2,
                     token_pattern = r"(?u)\b[^\d\W]\w+\b")
vect_matrix_1 = pd.DataFrame(vect.fit_transform(X1_train.cut_jieba).toarray(), columns = vect.get_feature_names())
vect_matrix_2 = pd.DataFrame(vect.fit_transform(X2_train.cut_snownlp).toarray(), columns = vect.get_feature_names())
vect_matrix_1
vect_matrix_2

兩種分詞方式經過同樣的處理之後均過濾掉一半以上.

3.6  構建模型

1. 構建模型與交叉驗證

from sklearn.pipeline import make_pipeline
from sklearn.naive_bayes import MultinomialNB
from sklearn.model_selection import cross_val_score
#初始化樸素貝葉斯模型
nb = MultinomialNB()
#利用pipeline管道進行特徵向量化
pipe = make_pipeline(vect, nb)
#交叉驗證,將訓練集分成10份,即計算10次,在每次計算中用10份中的一份當作驗證集,對應的另外9份用作訓練集,最後對10次計算出的準確率求平均值
score1 = cross_val_score(pipe, X1_train.cut_jieba, y1_train, cv=10, scoring='accuracy').mean()
score2 = cross_val_score(pipe, X2_train.cut_snownlp, y2_train, cv=10, scoring='accuracy').mean()
score1
score2
0.8002292107510046
0.7559003695080964

在訓練集上, jieba分詞後的模型的準確率比snownlp能高些

2. 模型評估

from sklearn import metrics
#訓練出模型
pipe1= pipe.fit(X1_train.cut_jieba, y1_train)
pipe2= pipe.fit(X2_train.cut_jieba, y2_train)
#用訓練的模型分別預測cut_jieba和cut_snownlp的測試集 y1_pre = pipe1.predict(X1_test.cut_jieba) y2_pre = pipe2.predict(X2_test.cut_snownlp) #評價模型預測結果 metrics.accuracy_score(y1_test, y1_pre) metrics.accuracy_score(y2_test, y2_pre)
0.8071278825995807
0.7631027253668763

 在測試集上, jieba同樣比snownlp的準確率高, 對於分類問題通常用混淆矩陣中的精準度和召回率進行評價.

1) 混淆矩陣

1和0表示真實值(1和0代表兩個類別, 非數字意義), P和N表示預測值, T表示預測正確, F則錯誤

準確率: (TP+TN)/(1+0)

靈敏度: TP/1

精準度: TP/(TP+FP)

召回率: TP/(TP+FN)等價於靈敏度 

2) 在樸素貝葉斯怎麼才能得到混淆矩陣?

#分別求得cut_jieba和cut_snownlp的混淆矩陣
con_matrix1 = metrics.confusion_matrix(y1_test, y1_pre)
con_matrix2 = metrics.confusion_matrix(y2_test, y2_pre)
#分別計算精準率和召回率
accu_rate1, accu_rate2= con_matrix1[0][0]/(con_matrix1[1][0]+con_matrix1[0][0]), con_matrix2[0][0]/(con_matrix2[1][0]+con_matrix2[0][0])
recall_rate1, recall_rate2 = con_matrix1[0][0]/(con_matrix1[0][1]+con_matrix1[0][0]), con_matrix2[0][0]/(con_matrix2[0][1]+con_matrix2[0][0])
print('jieba_confused_matrix:')
print(con_matrix1)
print('accurate_rate:{0}, recall_rate:{1}'.format(accu_rate1, recall_rate1))
print()
print('snownlp_confused_matrix:')
print(con_matrix2)
print('accurate_rate:{0}, recall_rate:{1}'.format(accu_rate2, recall_rate2))
jieba_confused_matrix:
[[351 130]
 [ 54 419]]
accurate_rate:0.8666666666666667, recall_rate:0.7297297297297297

snownlp_confused_matrix:
[[353 146]
 [ 80 375]]
accurate_rate:0.815242494226328, recall_rate:0.7074148296593187

在精準度和召回率上jieba同樣表現更佳, 也從另一個層面上說明了特徵的重要性, snownlp還有情感分類功能, 不妨看看結果如何

3) snownlp情感分類

y_pred_snow = X1_test.cut_jieba.apply(lambda x: SnowNLP(x).sentiments)
y_pred_snow= np.where(y_pred_snow > 0.5, 1, 0)
metrics.accuracy_score(y1_test, y_pred_snow)
metrics.confusion_matrix(y1_test, y_pred_snow)
0.640461215932914
array([[179, 299],
       [ 44, 432]], dtype=int64)

可以看到在電視劇的評論上準確率不高, 但是精確率還是很高的: 0.80, 召回率: 0.37

下面將利用訓練出的模型對新資料集進行情感分類

4. 情感分類

通過以上可以看到通過jieba分詞後訓練的樸素貝葉斯模型還是相當不錯的, 那麼現在就用該模型對新資料集進行分類

#讀取資料, 刪除重複項以及jieba分詞
conn = pymysql.connect(host = 'localhost', user='root', password = '1234', port=3306,db='yu', charset='utf8')
sql = 'select * from pinglun'
yk = pd.read_sql(sql, conn)
yk.drop_duplicates(inplace=True)
yk['cut_content'] = yk.content.apply(lambda x: ' '.join(jieba.cut(x)))
#用模型進行情感分類
yk['emotion'] = pipe1.predict(yk.cut_content)
#計算積極情感佔比
yk[yk.emotion_nb == 1].shape[0]/yk.shape[0]
0.5522761760242793

另外根據模型在測試集的精準度0.87(在預測的結果為1中有87%真實值也是1), 因此在0.55的基礎上再乘0.87, 結果為0.48, 這僅僅是預測為1且真實為1所佔總體的參考比例, 不妨再利用snownlp對情感進行分類

#利用snownlp進行情感分類
emotion_pred = yk.cut_content.apply(lambda x: SnowNLP(x).sentiments)
yk['emotion_snow'] = np.where(emotion_pred > 0.5, 1, 0)
#計算積極情感佔比
yk[yk.emotion_snow == 1].shape[0]/yk.shape[0]
0.6330804248861912

兩者的差距也是挺大的, 相差8個百分點

#隨機抽取1個
yk.sample(n=1)

表情就能表示出情感, 沒能預測正確看來還是訓練資料量太少, 要擴充啊, 另外當訓練資料達到某種程度, 會遠超人類的判斷力

5. 生成詞雲圖

import matplotlib.pyplot as plt
from wordcloud import WordCloud
bg_image = plt.imread(r'tig.jpg')                
wc = WordCloud(width=1080,                       #設定寬度
               height=840,                       #設定高度
               background_color='white',         #背景顏色
               mask=bg_image,                    #背景圖
               font_path='STKAITI.TTF',          #中文字型
               stopwords=stopwords,              #停用詞
               max_font_size=400,                #字型最大值
               random_state=50                   #隨機配色方案
              )            
wc.generate_from_text(' '.join(yk.cut_content))  #生成詞雲
plt.imshow(wc)                                   # 繪製圖像
plt.axis('off')                                  # 關閉座標軸顯示
wc.to_file('pig.jpg')
plt.show()

 

以上便是對學習過程的總結, 若出現錯誤, 還望指正, 謝謝!

參考: 

http://www.cnblogs.com/mxp-neu/articles/5316989.html

https://www.jianshu.com/p/29aa3ad63f9d

宣告: 本文僅用作學習交流