当前位置:   article > 正文

es实战-使用IK分词器进行词频统计_es分词词频统计

es分词词频统计

简介:通过IK分词器分词并生成词云。

本文主要介绍如何通过 IK 分词器进行词频统计。使用分词器对文章的词频进行统计,主要目的是实现如下图所示的词云功能,可以找到文章内的重点词汇。后续也可以对词进行词性标注,实体识别以及对实体的情感分析等功能。

词频统计服务具体模块如下:
数据输入:文本信息
数据输出:词 - 词频(TF-IDF等) - 词性等内容
使用的组件:分词器、语料库、词云展示组件等
功能点:白名单,黑名单,同义词等

现存的中文分词器有 IK、HanLP、jieba 和 NLPIR 等几种,不同分词器各有特点,本文使用 IK 实现,因为 ES 一般使用 medcl 等大佬封装的 IK 分词器插件作为中文分词器。
由于 ES 的 IK 分词器插件深度结合了 ES,仅对文本分词使用不到 ES 的内容,所以文本采用申艳超大佬版本的 IK

1. IK 分词统计代码

IK 的代码相对比较简单,东西不多,将 String 拆分为词并统计代码如下:

  1. 单纯统计词频:
  1. /**
  2. * 全文本词频统计
  3. *
  4. * @param content 文本内容
  5. * @param useSmart 是否使用 smart
  6. * @return 词,词频
  7. * @throws IOException
  8. */
  9. private static Map<String, Integer> countTermFrequency(String content, Boolean useSmart) throws IOException {
  10. // 输出结果 Map
  11. Map<String, Integer> frequencies = new HashMap<>();
  12. if (StringUtils.isBlank(content)) {
  13. return frequencies;
  14. }
  15. DefaultConfig conf = new DefaultConfig();
  16. conf.setUseSmart(useSmart);
  17. // 使用 IKSegmenter 初始化文本信息并加载词典
  18. IKSegmenter ikSegmenter = new IKSegmenter(new StringReader(content), conf);
  19. Lexeme lexeme;
  20. while ((lexeme = ikSegmenter.next()) != null) {
  21. if (lexeme.getLexemeText().length() > 1) {// 过滤单字,也可以过滤其他内容,如数字和单纯符号等内容
  22. final String term = lexeme.getLexemeText();
  23. // Map 累加操作
  24. frequencies.compute(term, (k, v) -> {
  25. if (v == null) {
  26. v = 1;
  27. } else {
  28. v += 1;
  29. }
  30. return v;
  31. });
  32. }
  33. }
  34. return frequencies;
  35. }
  1. 统计词频和文档频率:
  1. /**
  2. * 文本列表词频和词文档频率统计
  3. *
  4. * @param docs 文档列表
  5. * @param useSmart 是否使用只能分词
  6. * @return 词频列表 词-[词频,文档频率]
  7. * @throws IOException
  8. */
  9. private static Map<String, Integer[]> countTFDF(List<String> docs, boolean useSmart) throws IOException {
  10. // 输出结果 Map
  11. Map<String, Integer[]> frequencies = new HashMap<>();
  12. for (String doc : docs) {
  13. if (StringUtils.isBlank(doc)) {
  14. continue;
  15. }
  16. DefaultConfig conf = new DefaultConfig();
  17. conf.setUseSmart(useSmart);
  18. // 使用 IKSegmenter 初始化文本信息并加载词典
  19. IKSegmenter ikSegmenter = new IKSegmenter(new StringReader(doc), conf);
  20. Lexeme lexeme;
  21. // 用于文档频率统计的 Set
  22. Set<String> terms = new HashSet<>();
  23. while ((lexeme = ikSegmenter.next()) != null) {
  24. if (lexeme.getLexemeText().length() > 1) {
  25. final String text = lexeme.getLexemeText();
  26. // 进行词频统计
  27. frequencies.compute(text, (k, v) -> {
  28. if (v == null) {
  29. v = new Integer[]{1, 0};
  30. } else {
  31. v[0] += 1;
  32. }
  33. return v;
  34. });
  35. terms.add(text);
  36. }
  37. }
  38. // 进行文档频率统计:无需初始化 Map,统计词频后 Map 里面必有该词记录
  39. for (String term : terms) {
  40. frequencies.get(term)[1] += 1;
  41. }
  42. }
  43. return frequencies;
  44. }

2. 获取词云 TopN 个词

获取 TopN 个词用于词云展示有多种排序方式,可以直接根据词频、文档频率或者 TF-IDF 等算法进行排序,本文仅根据词频求取 TopN。
M 个数字获取 TopN 有以下算法:

  • M 小 N 小:快速选择算法
  • M 大 N 小:小顶堆
  • M 大 N 大:归并排序

本文采用小顶堆方式实现,对应JAVA中的优先队列数据结构 PriorityQueue:

  1. /**
  2. * 按出现次数,从高到低排序取 TopN
  3. *
  4. * @param data 词和排序数字对应的 Map
  5. * @param TopN 词云展示的 TopN
  6. * @return 前 N 个词和排序值
  7. */
  8. private static List<Map.Entry<String, Integer>> order(Map<String, Integer> data, int topN) {
  9. PriorityQueue<Map.Entry<String, Integer>> priorityQueue = new PriorityQueue<>(data.size(), new Comparator<Map.Entry<String, Integer>>() {
  10. @Override
  11. public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
  12. return o2.getValue().compareTo(o1.getValue());
  13. }
  14. });
  15. for (Map.Entry<String, Integer> entry : data.entrySet()) {
  16. priorityQueue.add(entry);
  17. }
  18. //TODO 当前100词频一致时(概率极低)的处理办法,if( list(0).value == list(99).value ){xxx}
  19. List<Map.Entry<String, Integer>> list = new ArrayList<>();
  20. //统计结果队列size和topN值取较小值列表
  21. int size = priorityQueue.size() <= topN ? priorityQueue.size() : topN;
  22. for (int i = 0; i < size; i++) {
  23. list.add(priorityQueue.remove());
  24. }
  25. return list;
  26. }

3. IK 代码浅析

核心主类为IKSegmenter,需要关注的点有dic包也就是词典相关内容以及字符处理工具类CharacterUtilidentifyCharType()方法,目录结构如下:


IKSegmenter类结构如下图,其中 init() 为私有方法,初始化加载词典采用非懒加载模式,在第一次初始化IKSegmenter实例时会调用并加载词典,代码位于结构图下方。

  1. // IKSegmenter 类构造方法
  2. public IKSegmenter(Reader input, Configuration cfg) {
  3. this.input = input;
  4. this.cfg = cfg;
  5. this.init();
  6. }
  7. // IKSegmenter 类初始化
  8. private void init() {
  9. //初始化词典单例
  10. Dictionary.initial(this.cfg);
  11. //初始化分词上下文
  12. this.context = new AnalyzeContext(this.cfg);
  13. //加载子分词器
  14. this.segmenters = this.loadSegmenters();
  15. //加载歧义裁决器
  16. this.arbitrator = new IKArbitrator();
  17. }
  18. // Dictionary 类初始化词典
  19. public static Dictionary initial(Configuration cfg) {
  20. if (singleton == null) {
  21. synchronized (Dictionary.class) {
  22. if (singleton == null) {
  23. singleton = new Dictionary(cfg);
  24. return singleton;
  25. }
  26. }
  27. }
  28. return singleton;
  29. }

词典私有构造方法Dictionary()内会加载 IK 自带的词典以及扩展词典,我们也可以把自己线上不变的词典放到这里这样IKAnalyzer.cfg.xml中就只需要配置经常变更词典即可。

  1. private Dictionary(Configuration cfg) {
  2. this.cfg = cfg;
  3. this.loadMainDict();// 主词典以及扩展词典
  4. this.loadmiaozhenDict();// 自定义词典加载,仿照其他方法即可
  5. this.loadStopWordDict();// 扩展停词词典
  6. this.loadQuantifierDict();// 量词词典
  7. }

IKSegmenter类调用next()方法获取下一个词元时,会调用CharacterUtil类中的identifyCharType()方法识别字符种类,这里我们也可以自定义一些字符种类针对处理新兴的网络语言,如@、##等内容:

  1. static int identifyCharType(char input) {
  2. if (input >= '0' && input <= '9') {
  3. return CHAR_ARABIC;
  4. } else if ((input >= 'a' && input <= 'z') || (input >= 'A' && input <= 'Z')) {
  5. return CHAR_ENGLISH;
  6. } else {
  7. Character.UnicodeBlock ub = Character.UnicodeBlock.of(input);
  8. //caster 增加#为中文字符
  9. if (ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
  10. || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
  11. || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A ||input=='#') {
  12. //目前已知的中文字符UTF-8集合
  13. return CHAR_CHINESE;
  14. } else if (ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS //全角数字字符和日韩字符
  15. //韩文字符集
  16. || ub == Character.UnicodeBlock.HANGUL_SYLLABLES
  17. || ub == Character.UnicodeBlock.HANGUL_JAMO
  18. || ub == Character.UnicodeBlock.HANGUL_COMPATIBILITY_JAMO
  19. //日文字符集
  20. || ub == Character.UnicodeBlock.HIRAGANA //平假名
  21. || ub == Character.UnicodeBlock.KATAKANA //片假名
  22. || ub == Character.UnicodeBlock.KATAKANA_PHONETIC_EXTENSIONS) {
  23. return CHAR_OTHER_CJK;
  24. }
  25. }
  26. //其他的不做处理的字符
  27. return CHAR_USELESS;
  28. }

由于 IK 内容不多,建议大家可以从头捋一遍,包括各个实现ISegmenter接口的各个自分词器等内容。

4. 进行词云展示

词云展示可以使用 Kibana 自带的词云 Dashboard,或者比较热门的 WordCloud。自己测试可以使用线上的微词云快速便捷查看词云效果:导入两列的 XLS 文件即可,左侧控制栏也可以对形状字体等进行配置美化。


展示效果如下图所示:

5. 总结

本文主要通过 IK 分词器实现了词频统计功能,用于词云的展示,不仅仅适用于 ES,任何数据源文档都可以进行词频统计。但是功能比较基础,感兴趣的同学可以实现一下词排序方式变更(tf/idf)、词性标注、实体识别和情感分析等功能;IK 分词器较为局限,需要使用 HanLP(自带词性标注)等更高级的分词器以及 NLP 相关知识来辅助,也可以参考百度 AI 的词法分析模块。

原文链接

本文为阿里云原创内容,未经允许不得转载。 

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/黑客灵魂/article/detail/966533
推荐阅读
相关标签
  

闽ICP备14008679号