专栏名称: 大数据挖掘DT数据分析
实战数据资源提供。数据实力派社区,手把手带你玩各种数据分析,涵盖数据分析工具使用,数据挖掘算法原理与案例,机器学习,R语言,Python编程,爬虫。如需发布广告请联系: hai299014
51好读  ›  专栏  ›  大数据挖掘DT数据分析

非主流自然语言处理:大规模语料词库自动生成

大数据挖掘DT数据分析  · 公众号  · 大数据  · 2017-07-15 20:35

正文

请到「今天看啥」查看全文




数据挖掘入门与实战  公众号: datadw


一、前言

写这篇文时,突然想到一个问题,大家的词库都是从哪来的?
之所以会这么有些意外的问,是因为从没把词库当成个事儿:平时处理微博,就用程序跑一下微博语料获得微博词库;处理新闻,程序跑一下新闻语料获得新闻词库。甚至没有把跑出来的词库存下来的习惯,谁知道过两天是不是又出什么新词,与其用可能过时的,不如随手生成个新鲜出炉的。


好吧,我承认我这是在显摆。如果你也想和我一样,想要随用随丢,任性它一把,那随我来。


如果你只想要这样一个程序,可以直奔这里下载。

回复公众号"词库"获取。
如果你想亲手写一个,那也没什么,百来行代码的事儿。


好,咱们言归正传。


二、词库生成


1、算法分析,先来考虑以下几个问题


问:目标是从文本中抽取词语,是否可以考虑使用遗忘的方法呢?

答:可以,词语具备以相对稳定周期重复再现的特征,所以可以考虑使用遗忘的方法。这意味着,我们只需要找一种适当的方法,将句子划分成若干子串,这些子串即为“候选词”。在遗忘的作用下,如果“候选词”会周期性重现,那么它就会被保留在词库中,相反如果只是偶尔或随机出现,则会逐渐被遗忘掉。


问:那用什么方法来把句子划分成子串比较合适呢?

答:考察句中任意相邻的两个字,相邻两字有两种可能:要么同属于一个共同的词,要么是两个词的边界。我们都会有这样一种感觉,属于同一个词的相邻两字的“关系”肯定比属于不同词的相邻两字的“关系”要强烈一些。

数学中并不缺少刻划“关系”的模型,这里我们选择公式简单并且参数容易统计的一种:如果两个字共现的概率大于它们随机排列在一起的概率,那么我们认为这两个字有关,反之则无关。

如果相邻两字无关,就可以将两字中间断开。逐字扫描句子,如果相邻两字满足下面的公式,则将两字断开,如此可将句子切成若干子串,从而获得“候选词”集,判断公式如下图所示:

公式中所需的参数可以通过统计获得:遍历一次语料,即可获得公式中所需的“单字的频数”、“相邻两字共现的频数”,以及“所有单字的频数总和”。


问:如何计算遗忘剩余量?

答:使用牛顿冷却公式,各参数在遗忘算法中的含义,如下图所示:

牛顿冷却公式的详情说明,可以参考阮一峰老师的博文《 基于用户投票的排名算法(四):牛顿冷却定律 》。


问:参数中时间是用现实时间吗,遗忘系数取多少合适呢?

答:a、关于时间:

可以使用现实时间,遗忘的发生与现实同步。

也可以考虑用处理语料中对象的数量来代替,这样仅当有数据处理时,才会发生遗忘。比如按处理的字数为计时单位,人阅读的速度约每秒5至7个字,当然每个人的阅读速度并不相同,这里的参数值要求并不需要特别严格。


b、遗忘系数可以参考艾宾浩斯曲线中的实验值,如下图(来自互联网)

我们取6天记忆剩余量约为25.4%这个值,按每秒阅读7个字,将其代入牛顿冷却公式可以求得遗忘系数:


注意艾宾浩斯曲线中的每组数值代入公式,所得的系数并不相同,会对词库的最大有效容量产生影响。


2、算法的代码实现(C#版)


呼,算法分析完成,相比于上面的文档,还是写代码要容易的多。


2.1、候选词生成

[csharp] view plain copy

  1. ///

  2. /// 从文本中生成候选词

  3. ///

  4. /// 文本行

  5. /// 相邻字典

  6. /// 词库

  7. /// 是否更新相邻字典

  8. /// 是否更新词库

  9. public static void UpdateKeyWordColl( string text, MemoryBondColl< string > objCharBondColl, MemoryItemColl< string > objKeyWordColl, bool bUpdateCharBondColl = true , bool bUpdateKeyWordColl = true )

  10. {

  11. if (String.IsNullOrEmpty(text)) return ;

  12. StringBuilder buffer = new StringBuilder(); //用于存放连续的子串

  13. string keyHead = text[0].ToString(); //keyHead、keyTail分别存放相邻的两个字符

  14. buffer.Append(keyHead);

  15. for ( int k = 1; k //遍历句子中的每一个字符

  16. {

  17. //从句子中取一个字作为相邻两字的尾字

  18. string keyTail = text[k].ToString();

  19. if (bUpdateCharBondColl)

  20. {

  21. //更新相邻字典

  22. DictionaryDAL.UpdateMemoryBondColl< string >(keyHead, keyTail, objCharBondColl);

  23. }

  24. if (bUpdateKeyWordColl)

  25. {

  26. //判断相邻两字是否有关

  27. if (!DictionaryDAL.IsBondValid< string >(keyHead, keyTail, objCharBondColl ) )

  28. {

  29. //两字无关,则将绥中的字串取出,此即为候选词

  30. string keyword = buffer.ToString();

  31. //将候选词添加到词库中

  32. DictionaryDAL.UpdateMemoryItemColl< string >(keyword, objKeyWordColl);

  33. //清空缓冲

  34. buffer.Clear();

  35. //并开始下一个子串

  36. buffer.Append(keyTail);

  37. }

  38. else

  39. {

  40. //两个字有关,则将当前字追加至串缓冲中

  41. buffer.Append(keyTail);

  42. }

  43. }

  44. //将当前的字作为相邻的首字

  45. keyHead = keyTail;

  46. }

  47. }


2.2、相邻字统计

[csharp] view plain copy

  1. ///

  2. /// 相邻字统计

  3. ///

  4. /// 文本行

  5. /// 存放相邻结果的字典

  6. /// 遍历句中相邻的字,将结果存放到字典中

  7. public static void UpdateCharBondColl( string text, MemoryBondColl< string > objCharBondColl)

  8. {

  9. if (String.IsNullOrEmpty(text)) return ;

  10. string keyHead = text[0].ToString();

  11. for ( int k = 1; k

  12. {

  13. string keyTail = text[k].ToString();

  14. //存入相邻字典中

  15. DictionaryDAL.UpdateMemoryBondColl< string >(keyHead, keyTail, objCharBondColl);

  16. keyHead = keyTail;

  17. }

  18. }



2.3、切片算法

[csharp] view plain copy

  1. ///

  2. /// 判断键是否为有效关联键

  3. ///

  4. /// C#中的范型,具体类型由调用者传入

  5. /// 相邻键中首项

  6. /// 相邻键中尾项

  7. /// 相邻字典

  8. /// 返回是否判断的结果:true、相邻项有关;false、相邻项无关

  9. /// 判断标准:共享键概率 > 单字概率之积

  10. public static bool IsBondValid (T keyHead, T keyTail, MemoryBondColl objMemoryBondColl )

  11. {

  12. //如果相邻项任何一个不在相邻字典中,则返回false 。

  13. if (!objMemoryBondColl.Contains(keyHead) || !objMemoryBondColl.Contains(keyTail)) return false ;

  14. //分别获得相邻单项的频次

  15. double dHeadValidCount = CalcRemeberValue (keyHead, objMemoryBondColl);

  16. double dTailValidCount = CalcRemeberValue (keyTail, objMemoryBondColl);

  17. //获得相邻字典全库的总词频

  18. double dTotalValidCount = objMemoryBondColl.MinuteOffsetSize;

  19. if (dTotalValidCount <= 0) return false ;

  20. //获得相邻项共现的频次

  21. MemoryItemColl objLinkColl = objMemoryBondColl[keyHead].LinkColl;

  22. if (!objLinkColl.Contains(keyTail)) return false ;

  23. double dShareValidCount = CalcRemeberValue (keyTail, objLinkColl);

  24. //返回计算的结果

  25. return dShareValidCount / dHeadValidCount > dTailValidCount / dTotalValidCount;

  26. }



2.4、牛顿冷却公式

[csharp] view plain copy

  1. ///

  2. /// 牛顿冷却公式

  3. ///

  4. /// 冷却系数

  5. /// 时间间隔

  6. ///

  7. ///

  8. /// 建议遗忘系数:-Math.Log(0.254, Math.E) / (6天 * 24小时 * 60分钟 *60秒 *7每秒阅读字数);

  9. ///

  10. public static double CalcNetonCooling( double parameter, double interval)

  11. {

  12. return Math.Exp(-1 * parameter * interval);

  13. }


2.5、遗忘是在词入库的时候计算的(其实算法核心仅此一行)

[csharp] view plain copy

  1. ///

  2. /// 将候选项添加到词典中

  3. ///

  4. /// C#中的泛型,具体类型由调用者传入

  5. /// 候选项

  6. /// 候选项词典

  7. public static void UpdateMemoryItemColl (T keyItem, MemoryItemColl objMemoryItemColl)

  8. {

  9. if (!objMemoryItemColl.Contains(keyItem))

  10. {

  11. //如果词典中不存在该候选项

  12. //声明数据对象,用于存放候选项及其相关数据

  13. MemoryItemMDL mdl = new MemoryItemMDL ();

  14. mdl.Key = keyItem; //候选项

  15. mdl.TotalCount = 1; //候选项出现的物理次数

  16. mdl.ValidCount = 1; //边遗忘边累加共同作用下的有效次数

  17. mdl.ValidDegree = 1; //该词的成熟度

  18. objMemoryItemColl.Add(mdl); //添加至词典中

  19. }

  20. else

  21. {

  22. //如果词典中已包含该候选项

  23. //从词典中取出该候选项

  24. MemoryItemMDL mdl = objMemoryItemColl[keyItem];

  25. //计算从最后一次入库至现在这段时间剩余量系数

  26. double dRemeberValue =  MemoryDAL.CalcRemeberValue(objMemoryItemColl.OffsetTotalCount - mdl.UpdateOffsetCount, objMemoryItemColl.MinuteOffsetSize);

  27. mdl.TotalCount +=  1; //累加总计数

  28. //计算成熟度,嗯,这个公式写的比较乱,我自己现在看都有点头晕,看不懂算了,改天琢磨换个公式

  29. mdl.ValidDegree = mdl.ValidDegree * ((mdl.ValidCount * dRemeberValue) / (mdl.ValidCount * dRemeberValue + 1)) + (1 - mdl.ValidCount * (1 - dRemeberValue)) * (1 / (mdl.ValidCount * dRemeberValue + 1));

  30. mdl.ValidCount =mdl.ValidCount* dRemeberValue+ 1; // 遗忘累频=记忆保留量+1

  31. mdl.UpdateOffsetCount = objMemoryItemColl.OffsetTotalCount; //更新时的偏移量(相当于记录本次入库的时间)

  32. }

  33. objMemoryItemColl.OffsetTotalCount += 1; //处理过的数据总量(相当于一个全局的计时器)

  34. }



2.6、按词权重排序显示词库(词的权重将在后面的文章中详解)

[csharp] view plain copy

  1. ///

  2. /// 按权重排序输出词库

  3. ///

  4. /// 词库

  5. /// 输出词的数量

  6. /// 是否仅输出词

  7. /// 输出的结果

  8. public static string ShowKeyWordWeightColl(MemoryItemColl< string > objMemoryItemColl, int nKeyWordTopCount, bool bIsOnlyWord = true )

  9. {

  10. StringBuilder sb = new StringBuilder();

  11. sb.AppendLine(String.Format( " 【{0}】 | {1} | {2} | {3}" , "主词" , "遗忘词频" , "累计词频" , "词权值" ));

  12. var tbuffer = from x in objMemoryItemColl

  13. //如果只显示词,则要求:长度大于1,且不包含符号

  14. where !bIsOnlyWord || x.Key.Length > 1 && !Regex.IsMatch(x.Key,@ "\p{P}" )

  15. //按权重排序

  16. orderby x.ValidCount <= 0 ? 0 : (x.ValidCount  )* (Math.Log(objMemoryItemColl.MinuteOffsetSize) - Math.Log(x.ValidCount)) descending

  17. select x;

  18. var buffer = (tbuffer).Take(nKeyWordTopCount);

  19. sb.AppendLine(String.Format( "================ 共{0} 个 ================== " , buffer.Count()));

  20. //逐词输出,每个词一行

  21. foreach (var x in buffer)

  22. {

  23. sb.AppendLine(String.Format( " 【{0}】 | {1} | {2} | {3}" , x.Key, Math.Round(DictionaryDAL.CalcRemeberValue< string >(x.Key, objMemoryItemColl), 2), x.TotalCount, Math.Round((x.ValidCount <= 0 ? 0 : (x.ValidCount  ) * (Math.Log(objMemoryItemColl.MinuteOffsetSize) - Math.Log(x.ValidCount))), 4)));

  24. }

  25. return sb.ToString();

  26. }


2.7、清理词频低于阀值的词

[csharp] view plain copy

  1. ///

  2. /// 清理集合,移除低于阈值的项

  3. ///

  4. /// C#中的泛型,具体类型由调用者传入

  5. /// 词库

  6. /// 阈值,默认值相当于至少1个记忆周期时间内,未曾再次出现

  7. public static void ClearMemoryItemColl (MemoryItemColl objMemoryItemColl, double dMinValidValue=1.25)

  8. {

  9. for ( int k = objMemoryItemColl.Count - 1; k >= 0; k--)

  10. {

  11. //此处使用最后一次的计数即可,无需计算当前剩余值

  12. //double dValidValue = CalcRemeberValue (objMemoryItemColl[k].Key, objMemoryItemColl);

  13. //if (dValidValue

  14. if (objMemoryItemColl[k].ValidCount

  15. }

  16. }



3、该算法生成词库的特点

3.1、无监督学习

3.2、O(N)级时间复杂度

3.3、训练、执行为同一过程,可无缝处理流式数据

3.4、未登录词、新词、登录词没有区别

3.5、领域自适应:领域变化时,词条、词频自适应的随之调整

3.6、算法中仅使用到频数这一语言的共性特征,无需对任何字符做特别处理,因此原理上跨语种。


三、词库成熟度

由于每个词都具备一个相对稳定的重现周期,不难证明,当训练语料达到一定规模后,在遗忘的作用下,每个词的词频在衰减和累加会达到平衡,也即衰减的速度与增加的速度基本一致。成熟的词库,词频的波动就会比较小,利用这个特征,我们可以衡量词库的成熟程度。

词频的这种波动也有一些妙用,比如: 微博中重复发的广告、 突然暴发的热点事件等情况,都会因词频的非正常变化而导致这些词的成熟度变低。

鉴于目前所用的模型,属于顺手写的,算不上满意,也没在这块花精力,这里就不再细讲。源代码中包含具体的方法,算是抛砖引玉,哪位兄弟找到更好的模型公式,记得跟俺说一声。

修改说明,上传的代码中,成熟度公式修改为:

[csharp] view plain copy

  1. ///

  2. /// 计算当前关键词的成熟度

  3. ///

  4. /// 泛型,具体类别由调用者传入

  5. /// 待计算的对象

  6. /// 记忆剩余量系数

  7. /// 当前成熟度

  8. ///

  9. /// 1、成熟度这里用对象遗忘与增加的量的残差累和来表征;

  10. /// 2、已经累计的残差之和会随时间衰减;

  11. /// 3、公式的意思是: 成熟度 = 成熟度衰减剩余量 + 本次遗忘与增加量的残差的绝对值

  12. ///

  13. public static double CalcValidDegree (MemoryItemMDL mdl, double dRemeberValue)

  14. {

  15. return mdl.ValidDegree * dRemeberValue + Math.Abs(1 - mdl.ValidCount * (1 - dRemeberValue));

  16. }




四、演示程序下载

回复公众号"词库"获取。

使用内附语料(在“可直接运行的演示程序”下可以找到)生成词库效果图如下:


数据挖掘入门与实战

搜索添加微信公众号:datadw


教你机器学习,教你数据挖掘


长按图片,识别二维码,点关注



公众号: weic2c
据分析入门与实战

长按图片,识别二维码,点关注








请到「今天看啥」查看全文