樸素貝葉斯算法下的情感分析——C#編程實現

分類:IT技術 時間:2016-10-13

這篇文章做了什麽

  樸素貝葉斯算法是機器學習中非常重要的分類算法,用途十分廣泛,如垃圾郵件處理等。而情感分析(Sentiment Analysis)是自然語言處理(Natural Language Progressing)中的重要問題,用以對文本進行正負面的判斷,以及情感度評分和意見挖掘。本文借助樸素貝葉斯算法,針對文本正負面進行判別,並且利用C#進行編程實現。

不先介紹點基礎?

樸素貝葉斯,真的很樸素

  樸素貝葉斯分類算法,是一種有監督學習算法,通過對訓練集的學習,基於先驗概率與貝葉斯公式,計算出特定條件下樣本屬於某一類別的概率(條件概率),從而達到分類的目的。為什麽說樸素貝葉斯樸素,其實這兒的樸素是英文翻譯過來的,樸素貝葉斯叫Naive Bayesian Classification,這裏的"Naive"被譯為了樸素,其實翻譯為天真更貼切一點,因為它假設了所有的樣本都是獨立的,這確實"Too Naive"。因此我們就有了半樸素貝葉斯和貝葉斯網絡,這些將在後面為大家呈現。

情感分析是啥

  自然語言處理(NLP)是人工智能之中非常重要的學科,與機器學習交叉甚廣。而情感分析優勢NLP中最為重要的內容之一,目前主要分為兩大流派,一派是語言學派(推薦《自然語言處理綜論》),他們從語法、句子結構、詞性等角度出發,通過解析一個句子的結構來分析情感正負面或是中性;另一派是統計學派(推薦《統計自然語言處理》),通過高質量的語料庫與訓練集,借助統計理論與算法,完成句子的情感分析。

樸素貝葉斯如何實現情感分析

1.前期準備:

  • 做好正負面標記的文本作為訓練集

  • 正負面詞庫

2.針對文本實現:

  • 分詞,推薦使用Jieba分詞引擎,無論是python, C#, 還是R語言上都有非常優秀的第三方庫,而且可以導入詞庫

  • 獲取正負面詞,此時需要用到正負面詞庫,即通過正負面詞庫的匹配,篩選出文本中能夠反映情感的詞。

  • 進入訓練集中匹配,獲取每個正面或者負面的詞在兩個訓練集的詞頻,如“好吃”這個詞在正面訓練集裏出現頻率為0.1%,而在負面訓練集裏頻率為0.001%。

  • 通過貝葉斯公式計算出在文本出現“好吃”這個詞的條件下位正面或者為負面的概率,例如:

  $p(正面|"好吃") = \frac{p(正面)p("好吃"|正面)}{p(正面)p("好吃"|正面)+ p(負面)p("好吃"|負面)} $

  得到的概率即是在好吃這個詞出現的條件下,該句子為正面的概率。

  • 然而一句話可能有多個表示情感的詞,比如"不好看"、"開心",我們最終要求得在這些詞出現的條件下該文本為正面的條件概率:p(正面|"好吃","不好看","開心")$,具體計算過程見如下推導,令上述三個詞分別為a,b,c,正面為pos,負面為neg,具體計算過程見如下推導,令上述三個詞分別為a,b,c,正面為pos,負面為neg:
\begin{eqnarray} p(pos|a,b,c) &=& \frac{p(pos)p(a,b,c|pos)}{p(pos)p(a,b,c|pos)+p(neg)p(a,b,c|neg)}\\ &=& \frac{p(pos)p(a|pos)p(b|pos)p(c|pos)}{p(pos)p(a|pos)p(b|pos)p(c|pos)+p(neg)p(a|neg)p(b|neg)p(c|neg)} \end{eqnarray}

  假設一句話為正面或是負面的先驗概率均為0.5,則\(p(pos) = p(neg) = 0.5\),那麽(2)式等於:

\begin{eqnarray} = \frac{p(a|pos)p(b|pos)p(c|pos)}{p(a|pos)p(b|pos)p(c|pos) + p(a|neg)p(b|neg)p(c|neg)} \end{eqnarray}
  • 拉普拉斯平滑:為了避免某個詞在訓練集中出現概率為0導致結果有偏,拉普拉斯平滑法講所有詞出現次數加一,在樣本量很大時,每個分量加一幾乎不會對最終估計結果造成太大影響,但這樣的好處是很好的解決了詞頻為0導致的有偏問題。

  • 其他變式

    貝葉斯推斷及其互聯網應用(二):過濾垃圾郵件 這篇文章中做了如下變形,將結果以單個詞下正面或是負面的條件概率表示,但是文章並未提及拉普拉斯校準,而是用拍腦袋想出來的1%作為校準概率。由於:

\begin{eqnarray} p(a|pos)& =& \frac{p(a|pos)p(a)}{p(a)}\\ &=& \frac{p(a|pos)p(a)}{p(a|pos)p(pos)+p(a|neg)p(neg)} (全概率公式)\\ &=& \frac{p(a|pos)p(pos)}{p(a|pos)p(pos)+p(a|neg)p(neg)}p(a)/p(pos)\\ &=& \frac{p(pos|a)p(a)}{p(pos)}\\ \end{eqnarray}

  所以有(3)式等於:

\begin{eqnarray} (3)式& =& \frac{\frac{p(pos|a)p(pos|b)p(pos|c)p(a)p(b)p(c)}{p(pos)^3}}{\frac{p(pos|a)p(pos|b)p(pos|c)p(a)p(b)p(c)}{p(pos)^3} + \frac{p(neg|a)p(neg|b)p(neg|c)p(a)p(b)p(c)}{p(neg)^3}}\\ &=& \frac{p(pos|a)p(pos|b)p(pos|c)}{p(pos|a)p(pos|b)p(pos|c)+p(neg|a)p(neg|b)p(neg|c)}\\ &=& \frac{p(pos|a)p(pos|b)p(pos|c)}{p(pos|a)p(pos|b)p(pos|c)+(1-p(pos|a))(1-p(pos|b))(1-p(pos|c))}\\ \end{eqnarray}

  以上公式均為自己推導,作者原文中並未詳細寫出,如涉及侵權問題請通知我刪除此部分。如有問題也歡迎大家指出,謝謝~。

3.聊一聊其他的

  python號稱最強大的中文自然語言處理庫SnowNLP,其中的情感分析事實上非常粗糙,首先它並沒有一個正負面的詞庫,而是把一個句子分出的所有詞都參與計算,包括各種各樣的無意義詞、符號等等,而事實上這些詞並不能反映任何情感。作者可能先驗的認為正負面訓練集中這些無情感詞頻率可以認為是近似的,但事實並非如此,所以這些詞將導致計算結果有偏。

缺點:

  • 樸素貝葉斯假設每個詞是獨立的,但是事實並非如此,尤其是針對長篇文章,或者是出現多個復雜語氣詞情況,獨立性假設失效。
  • 無法處理句子中比較的情況,無法落在句子中某一個產品或者屬性上,難以做意見挖掘。
  • 依賴於高質量的語料庫

優點:

  • 無需對句子成分進行分解,無需分析語法語義
  • 針對無法判斷正負面的詞時有效(“呵呵”,“臥槽”等)
  • 正負面詞混雜時,依舊可以根據訓練集做出較為準確的判斷

說了這麽多,我該如何實現呢?

在此小弟附上c#代碼,如果覺得不清晰,您可以移步至我的GitHub:

//首先需要加載Jieba.Net,可以通過NuGet自行安裝,然後using
//建立jieba類,用以處理訓練集和文本
    class Jieba
    {
        public string doc{get;set;}
        public string PosWords  {get;set;}
        public string NegWords {get;set;}
        public string stopwords  {get;set;}
        public List<string> JiebaCut()
        {
            JiebaSegmenter jiebaseg = new JiebaSegmenter();
            var segment = jiebaseg.Cut(doc);
            List<string> cutresult = new List<string>();
            foreach (var i in segment)
            {
                if (!stopwords.Contains(i))
                    cutresult.Add(i);
            }
            return cutresult;
        }
        public List<string> handle_sentiment(bool pos = true)
        {
            var words = JiebaCut();
            string PosOrNegWords = pos ? PosWords:NegWords;
            List<string> handle_result = new List<string>();
            foreach (var word in words)
            {
                if (PosOrNegWords.Contains(word))
                    handle_result.Add(word);
            }
            return handle_result;
        }
        public List<string> handle_words()
        {
            var words = JiebaCut();
            List<string> handle_result = new List<string>();
            foreach (var word in words)
            {
                if ((PosWords + NegWords).Contains(word))
                    handle_result.Add(word);
            }
            return handle_result;
        }
    }

  然後,我們需要定義一個處理概率的類,以及派生類

    class BaseProb
    {
        public Dictionary<string, double> d = new Dictionary<string, double>();
        public double total = 0.0;
        public int none = 0;
        public bool exists(string key) { return d.Keys.Contains(key); }
        public double getsum() { return total; }
        public void get(string key, out bool para1, out double para2)
        {
            if (!exists(key))
            {
                para1 = false; para2 = none;
            }
            else
            {
                para1 = true;para2 = d[key];
            }
        }
        public double freq(string key)
        {
            bool para1;
            double para2;
            get(key, out para1, out para2);
            return para2 / total;
        }
        //def samples(self):
        //    return self.d.keys()
    }
    class AddOneProb : BaseProb
    {
        //public Dictionary<string, double> d = new Dictionary<string, double>();
        //public double total = 0.0;
        //public int none = 1;
        public void add(string key, int value)
        {
            total += value;
            if (!exists(key))
            {
                d.Add(key, 1);
                total += 1;
            }
            d[key] += value;
        }

        public void DPrint()
        {
            Console.WriteLine(d.Count);
            Console.ReadKey();
            foreach (var key in d.Keys)
            {
                Console.WriteLine(key+" : "+d[key].ToString());
            }
        }
    }

  接下來需要將原始訓練集序列化為Json格式,方便以後調用,我使用的是LitJson,轉為["pos":{"我是一個詞":10},"neg":{"我也是一個詞":20}]的形式,以後直接讀取詞頻而非整個句子再進行分詞與統計。我們通過以下代碼讀取序列化後的數據:

        public static Dictionary<string, AddOneProb> Load(string filepath, out double total)
        {
            string json;
            using (var sr = new StreamReader(filepath, Encoding.Default))
                json = sr.ReadToEnd();

            JsonData jd = JsonMapper.ToObject(json);
            Dictionary<string, AddOneProb> d = new Dictionary<string, AddOneProb>() { { "neg", new AddOneProb() }, { "pos", new AddOneProb() } };

            foreach (var i in jd["d"]["neg"]["d"])
            {
                var arr = i.ToString().Replace("[","").Replace("]","").Split(',');
                int count = 0;
                if (int.TryParse(arr[1],out count ))
                    d["neg"].add(arr[0], count);
            }
            foreach (var i in jd["d"]["pos"]["d"])
            {
                var arr = i.ToString().Replace("[", "").Replace("]", "").Split(',');
                int count = 0;
                if (int.TryParse(arr[1], out count))
                    d["pos"].add(arr[0], count);
            }
            total = Convert.ToDouble (jd["total"].ToString());
            return d;
        }

  當然如果你不想進行序列化,也行,可以通過以下代碼訓練樣本

        public static void Train_data(string negFilePath, string negWords, string posFilePath, string posWords,
             ref Dictionary<string, AddOneProb> d, ref double total, string stopwordFilepath)
        {
            //d = new Dictionary<string, AddOneProb>() { { "pos", new AddOneProb() }, { "neg", new AddOneProb() } };
            string negfile = "", posfile = "";
            using (var sr1 = new StreamReader(negFilePath, Encoding.Default))
                negfile = sr1.ReadToEnd();
            using (var sr2 = new StreamReader(posFilePath, Encoding.Default))
                posfile = sr2.ReadToEnd();
            string stopwords = ReadTxtToEnd(stopwordFilepath);
            List<Tuple<List<string>, string>> data = http://www.cnblogs.com/yangruiGB2312/p/new List, string>>();
            var sent_cut = new Jieba();
            sent_cut.NegWords = negWords;
            sent_cut.PosWords = posWords;
            foreach (var sent in posfile.Replace("/r", "").Split('\n'))
            {
                sent_cut.doc = sent;
                sent_cut.stopwords = stopwords;
                data.Add(new Tuple<List<string>, string>(sent_cut.handle_sentiment(), "pos"));
            }
            Console.WriteLine("正面詞庫導入完畢");
            foreach (var sent in negfile.Replace("\r", "").Split('\n'))
            {
                sent_cut.doc = sent;
                sent_cut.stopwords = stopwords;
                data.Add(new Tuple<List<string>, string>(sent_cut.handle_sentiment(false), "neg"));
            }
            Console.WriteLine("負面詞庫導入完畢");

            foreach (var d_ in data)
            {
                var c = d_.Item2.ToString();
                if (d_.Item1 == null)
                    continue;
                else
                {
                    foreach (var word in d_.Item1)
                        d[c].add(word, 1);
                }
            }
            total = 0;
            foreach (var value in d.Values)
            {
                total += value.total;
            }
        }

  接下來就可以寫分類函數了,是這樣的:

protected static Tuple<string, double> Classify(List<string> x,
            Dictionary<string, AddOneProb> d, double total)
        {
            Dictionary<string, double> temp = new Dictionary<string, double>();
            foreach (var k in d.Keys)
            {
                temp.Add(k, Math.Log(0.5));
                foreach (var word in x)
                {
                    Console.WriteLine(k+" : "+word + " : " + d[k].freq(word));
                    temp[k] += Math.Log(d[k].freq(word));
                }
                Console.ReadKey();
            }
            string ret = "";
            double prob = 0;
            double now;
            foreach (var k in d.Keys)
            {
                now = 0;
                foreach (var otherk in d.Keys)
                {
                    try
                    {
                        now += Math.Exp(temp[otherk] - temp[k]);
                    }
                    catch (Exception)
                    {
                        now = 10000;
                    }
                }
                now = 1 / now;
                if (now > prob)
                {
                    ret = k;
                    prob = now;
                }
            }
            return new Tuple<string, double>(ret, prob);
        }
        public static double classify_(string sent, Dictionary<string, AddOneProb> d,
            double total, string stopwordFilepath)
        {
            Jieba jiebaword = new Jieba();
            jiebaword.doc = sent;
            jiebaword.stopwords = ReadTxtToEnd(stopwordFilepath);
            var retprob = Classify(jiebaword.JiebaCut(), d, total);
            if (retprob.Item1 == "pos")
                return retprob.Item2;
            else
                return 1 - retprob.Item2;
        }

  如何使用:

        static void Main(string[] args)
        {
        double total = 0;
        var d = Train.Load(SentimentFilepath + "sentiment_json.txt",out total);
        var poswords = Train.ReadTxtToEnd(SentimentFilepath + "pos.csv");
        var negwords = Train.ReadTxtToEnd(SentimentFilepath + "neg.csv");
        Train.Train_data(SentimentFilepath + "neg_train.csv", negwords, SentimentFilepath + "pos_train.csv", poswords,ref d, ref total, SentimentFilepath +             "stopwords.csv");
        string testsentence = "很忽悠,不好";
        var sent = Train.classify_(testsentence, d, total, SentimentFilepath + "stopwords.csv");
        Console.WriteLine(sent);
        Console.ReadKey();
}

本博原創作品僅供品讀,歡迎評論,未經本人同意謝絕轉載。特此申明!


Tags: 英文翻譯 人工智能 Natural 統計學 語料庫

文章來源:


ads
ads

相關文章
ads

相關文章

ad