这篇文章做了什么

  朴素贝叶斯算法是机器学习中非常重要的分类算法,用途十分广泛,如垃圾邮件处理等。而情感分析(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:

\[\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

  1. //首先需要加载Jieba.Net,可以通过NuGet自行安装,然后using
  2. //建立jieba类,用以处理训练集和文本
  3. class Jieba
  4. {
  5. public string doc{get;set;}
  6. public string PosWords {get;set;}
  7. public string NegWords {get;set;}
  8. public string stopwords {get;set;}
  9. public List<string> JiebaCut()
  10. {
  11. JiebaSegmenter jiebaseg = new JiebaSegmenter();
  12. var segment = jiebaseg.Cut(doc);
  13. List<string> cutresult = new List<string>();
  14. foreach (var i in segment)
  15. {
  16. if (!stopwords.Contains(i))
  17. cutresult.Add(i);
  18. }
  19. return cutresult;
  20. }
  21. public List<string> handle_sentiment(bool pos = true)
  22. {
  23. var words = JiebaCut();
  24. string PosOrNegWords = pos ? PosWords:NegWords;
  25. List<string> handle_result = new List<string>();
  26. foreach (var word in words)
  27. {
  28. if (PosOrNegWords.Contains(word))
  29. handle_result.Add(word);
  30. }
  31. return handle_result;
  32. }
  33. public List<string> handle_words()
  34. {
  35. var words = JiebaCut();
  36. List<string> handle_result = new List<string>();
  37. foreach (var word in words)
  38. {
  39. if ((PosWords + NegWords).Contains(word))
  40. handle_result.Add(word);
  41. }
  42. return handle_result;
  43. }
  44. }

  然后,我们需要定义一个处理概率的类,以及派生类

  1. class BaseProb
  2. {
  3. public Dictionary<string, double> d = new Dictionary<string, double>();
  4. public double total = 0.0;
  5. public int none = 0;
  6. public bool exists(string key) { return d.Keys.Contains(key); }
  7. public double getsum() { return total; }
  8. public void get(string key, out bool para1, out double para2)
  9. {
  10. if (!exists(key))
  11. {
  12. para1 = false; para2 = none;
  13. }
  14. else
  15. {
  16. para1 = true;para2 = d[key];
  17. }
  18. }
  19. public double freq(string key)
  20. {
  21. bool para1;
  22. double para2;
  23. get(key, out para1, out para2);
  24. return para2 / total;
  25. }
  26. //def samples(self):
  27. // return self.d.keys()
  28. }
  29. class AddOneProb : BaseProb
  30. {
  31. //public Dictionary<string, double> d = new Dictionary<string, double>();
  32. //public double total = 0.0;
  33. //public int none = 1;
  34. public void add(string key, int value)
  35. {
  36. total += value;
  37. if (!exists(key))
  38. {
  39. d.Add(key, 1);
  40. total += 1;
  41. }
  42. d[key] += value;
  43. }
  44. public void DPrint()
  45. {
  46. Console.WriteLine(d.Count);
  47. Console.ReadKey();
  48. foreach (var key in d.Keys)
  49. {
  50. Console.WriteLine(key+" : "+d[key].ToString());
  51. }
  52. }
  53. }

  接下来需要将原始训练集序列化为Json格式,方便以后调用,我使用的是LitJson,转为["pos":{"我是一个词":10},"neg":{"我也是一个词":20}]的形式,以后直接读取词频而非整个句子再进行分词与统计。我们通过以下代码读取序列化后的数据:

  1. public static Dictionary<string, AddOneProb> Load(string filepath, out double total)
  2. {
  3. string json;
  4. using (var sr = new StreamReader(filepath, Encoding.Default))
  5. json = sr.ReadToEnd();
  6. JsonData jd = JsonMapper.ToObject(json);
  7. Dictionary<string, AddOneProb> d = new Dictionary<string, AddOneProb>() { { "neg", new AddOneProb() }, { "pos", new AddOneProb() } };
  8. foreach (var i in jd["d"]["neg"]["d"])
  9. {
  10. var arr = i.ToString().Replace("[","").Replace("]","").Split(',');
  11. int count = 0;
  12. if (int.TryParse(arr[1],out count ))
  13. d["neg"].add(arr[0], count);
  14. }
  15. foreach (var i in jd["d"]["pos"]["d"])
  16. {
  17. var arr = i.ToString().Replace("[", "").Replace("]", "").Split(',');
  18. int count = 0;
  19. if (int.TryParse(arr[1], out count))
  20. d["pos"].add(arr[0], count);
  21. }
  22. total = Convert.ToDouble (jd["total"].ToString());
  23. return d;
  24. }

  当然如果你不想进行序列化,也行,可以通过以下代码训练样本

  1. public static void Train_data(string negFilePath, string negWords, string posFilePath, string posWords,
  2. ref Dictionary<string, AddOneProb> d, ref double total, string stopwordFilepath)
  3. {
  4. //d = new Dictionary<string, AddOneProb>() { { "pos", new AddOneProb() }, { "neg", new AddOneProb() } };
  5. string negfile = "", posfile = "";
  6. using (var sr1 = new StreamReader(negFilePath, Encoding.Default))
  7. negfile = sr1.ReadToEnd();
  8. using (var sr2 = new StreamReader(posFilePath, Encoding.Default))
  9. posfile = sr2.ReadToEnd();
  10. string stopwords = ReadTxtToEnd(stopwordFilepath);
  11. List<Tuple<List<string>, string>> data = new List<Tuple<List<string>, string>>();
  12. var sent_cut = new Jieba();
  13. sent_cut.NegWords = negWords;
  14. sent_cut.PosWords = posWords;
  15. foreach (var sent in posfile.Replace("\r", "").Split('\n'))
  16. {
  17. sent_cut.doc = sent;
  18. sent_cut.stopwords = stopwords;
  19. data.Add(new Tuple<List<string>, string>(sent_cut.handle_sentiment(), "pos"));
  20. }
  21. Console.WriteLine("正面词库导入完毕");
  22. foreach (var sent in negfile.Replace("\r", "").Split('\n'))
  23. {
  24. sent_cut.doc = sent;
  25. sent_cut.stopwords = stopwords;
  26. data.Add(new Tuple<List<string>, string>(sent_cut.handle_sentiment(false), "neg"));
  27. }
  28. Console.WriteLine("负面词库导入完毕");
  29. foreach (var d_ in data)
  30. {
  31. var c = d_.Item2.ToString();
  32. if (d_.Item1 == null)
  33. continue;
  34. else
  35. {
  36. foreach (var word in d_.Item1)
  37. d[c].add(word, 1);
  38. }
  39. }
  40. total = 0;
  41. foreach (var value in d.Values)
  42. {
  43. total += value.total;
  44. }
  45. }

  接下来就可以写分类函数了,是这样的:

  1. protected static Tuple<string, double> Classify(List<string> x,
  2. Dictionary<string, AddOneProb> d, double total)
  3. {
  4. Dictionary<string, double> temp = new Dictionary<string, double>();
  5. foreach (var k in d.Keys)
  6. {
  7. temp.Add(k, Math.Log(0.5));
  8. foreach (var word in x)
  9. {
  10. Console.WriteLine(k+" : "+word + " : " + d[k].freq(word));
  11. temp[k] += Math.Log(d[k].freq(word));
  12. }
  13. Console.ReadKey();
  14. }
  15. string ret = "";
  16. double prob = 0;
  17. double now;
  18. foreach (var k in d.Keys)
  19. {
  20. now = 0;
  21. foreach (var otherk in d.Keys)
  22. {
  23. try
  24. {
  25. now += Math.Exp(temp[otherk] - temp[k]);
  26. }
  27. catch (Exception)
  28. {
  29. now = 10000;
  30. }
  31. }
  32. now = 1 / now;
  33. if (now > prob)
  34. {
  35. ret = k;
  36. prob = now;
  37. }
  38. }
  39. return new Tuple<string, double>(ret, prob);
  40. }
  41. public static double classify_(string sent, Dictionary<string, AddOneProb> d,
  42. double total, string stopwordFilepath)
  43. {
  44. Jieba jiebaword = new Jieba();
  45. jiebaword.doc = sent;
  46. jiebaword.stopwords = ReadTxtToEnd(stopwordFilepath);
  47. var retprob = Classify(jiebaword.JiebaCut(), d, total);
  48. if (retprob.Item1 == "pos")
  49. return retprob.Item2;
  50. else
  51. return 1 - retprob.Item2;
  52. }

  如何使用:

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

本博原创作品仅供品读,欢迎评论,未经本人同意谢绝转载。特此申明!

朴素贝叶斯算法下的情感分析——C#编程实现的更多相关文章

  1. C#编程实现朴素贝叶斯算法下的情感分析

    C#编程实现 这篇文章做了什么 朴素贝叶斯算法是机器学习中非常重要的分类算法,用途十分广泛,如垃圾邮件处理等.而情感分析(Sentiment Analysis)是自然语言处理(Natural Lang ...

  2. Python机器学习笔记:朴素贝叶斯算法

    朴素贝叶斯是经典的机器学习算法之一,也是为数不多的基于概率论的分类算法.对于大多数的分类算法,在所有的机器学习分类算法中,朴素贝叶斯和其他绝大多数的分类算法都不同.比如决策树,KNN,逻辑回归,支持向 ...

  3. Python机器学习算法 — 朴素贝叶斯算法(Naive Bayes)

    朴素贝叶斯算法 -- 简介 朴素贝叶斯法是基于贝叶斯定理与特征条件独立假设的分类方法.最为广泛的两种分类模型是决策树模型(Decision Tree Model)和朴素贝叶斯模型(Naive Baye ...

  4. 朴素贝叶斯算法java实现(多项式模型)

    网上有很多对朴素贝叶斯算法的说明的文章,在对算法实现前,参考了一下几篇文章: NLP系列(2)_用朴素贝叶斯进行文本分类(上) NLP系列(3)_用朴素贝叶斯进行文本分类(下) 带你搞懂朴素贝叶斯分类 ...

  5. 什么是机器学习的分类算法?【K-近邻算法(KNN)、交叉验证、朴素贝叶斯算法、决策树、随机森林】

    1.K-近邻算法(KNN) 1.1 定义 (KNN,K-NearestNeighbor) 如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类 ...

  6. 朴素贝叶斯算法的python实现

    朴素贝叶斯 算法优缺点 优点:在数据较少的情况下依然有效,可以处理多类别问题 缺点:对输入数据的准备方式敏感 适用数据类型:标称型数据 算法思想: 朴素贝叶斯比如我们想判断一个邮件是不是垃圾邮件,那么 ...

  7. 朴素贝叶斯算法 & 应用实例

    转载请注明出处:http://www.cnblogs.com/marc01in/p/4775440.html 引 和师弟师妹聊天时经常提及,若有志于从事数据挖掘.机器学习方面的工作,在大学阶段就要把基 ...

  8. [ML学习笔记] 朴素贝叶斯算法(Naive Bayesian)

    [ML学习笔记] 朴素贝叶斯算法(Naive Bayesian) 贝叶斯公式 \[P(A\mid B) = \frac{P(B\mid A)P(A)}{P(B)}\] 我们把P(A)称为"先 ...

  9. 【十大算法实现之naive bayes】朴素贝叶斯算法之文本分类算法的理解与实现

    关于bayes的基础知识,请参考: 基于朴素贝叶斯分类器的文本聚类算法 (上) http://www.cnblogs.com/phinecos/archive/2008/10/21/1315948.h ...

随机推荐

  1. BZOJ4591——[Shoi2015]超能粒子炮·改

    1.题意:求 2.分析:公式恐惧症的同学不要跑啊QAQ 根据lucas定理-- 这一步大家都能懂吧,这是浅而易见的lucas定理转化过程,将每一项拆分成两项 那么下一步,我们将同类项合并 我们观察可以 ...

  2. zepto之tap事件点透问题分析及解决方案

    点透现象出现的场景: 当A/B两个层上下z轴重叠,上层的A点击后消失或移开(这一点很重要),并且B元素本身有默认click事件(如a标签)或绑定了click事件.在这种情况下,点击A/B重叠的部分,就 ...

  3. Sicily 1051: 魔板(BFS+排重)

    相对1150题来说,这道题的N可能超过10,所以需要进行排重,即相同状态的魔板不要重复压倒队列里,这里我用map储存操作过的状态,也可以用康托编码来储存状态,这样时间缩短为0.03秒.关于康托展开可以 ...

  4. PHP 文件下载 显示进度条

    前台调用:js调用: function downloadfile(id,name,price,curcount_pricelimit){ Date.prototype.Format = functio ...

  5. Eclipse导出插件工程

    一.Feature Projecties工程设置 1. 新建一个Feature Projecties 2. 选择我们的插件工程,finish 3. 在目录下新建一个Category definitio ...

  6. JetBrains PyCharm 2016.2.3注册码

    43B4A73YYJ-eyJsaWNlbnNlSWQiOiI0M0I0QTczWVlKIiwibGljZW5zZWVOYW1lIjoibGFuIHl1IiwiYXNzaWduZWVOYW1lIjoiI ...

  7. [iOS]创建一像素的线

    float sortaPixel = 1.0/[UIScreen mainScreen].scale; UIView* line = [[UIView alloc]initWithFrame:CGRe ...

  8. git 删除分支

    1,删除本地分支 git branch -D 本地分支名 2,删除远程分支 git push origin --delete 远程分支名 注意:如果只删除了本地分支而没有删除远程分支,再新建一个相同名 ...

  9. October 31st Week 45th Monday 2016

    While there is life there is hope. 一息若存,希望不灭. Go on living even if there is no hope. Knowing is not ...

  10. pygame学习笔记

    pygame参考文档pdf版:pygame API html版 pygame API 石头剪子布的简单小游戏,待改进的地方,自适应大小.感兴趣的小伙伴可以依据get_surface()返回值(即当前窗 ...