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

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


来自铭毅老哥对电影《长津湖》的影评分析

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

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

1. IK 分词统计代码

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

  1. 单纯统计词频:
/**
 * 全文本词频统计
 *
 * @param content  文本内容
 * @param useSmart 是否使用 smart
 * @return 词,词频
 * @throws IOException
 */
private static Map<String, Integer> countTermFrequency(String content, Boolean useSmart) throws IOException {
    // 输出结果 Map
    Map<String, Integer> frequencies = new HashMap<>();
    if (StringUtils.isBlank(content)) {
        return frequencies;
    }
    DefaultConfig conf = new DefaultConfig();
    conf.setUseSmart(useSmart);
    // 使用 IKSegmenter 初始化文本信息并加载词典
    IKSegmenter ikSegmenter = new IKSegmenter(new StringReader(content), conf);
    Lexeme lexeme;
    while ((lexeme = ikSegmenter.next()) != null) {
        if (lexeme.getLexemeText().length() > 1) {// 过滤单字,也可以过滤其他内容,如数字和单纯符号等内容
            final String term = lexeme.getLexemeText();
            // Map 累加操作
            frequencies.compute(term, (k, v) -> {
                if (v == null) {
                    v = 1;
                } else {
                    v += 1;
                }
                return v;
            });
        }
    }
    return frequencies;
}
  1. 统计词频和文档频率:
/**
 * 文本列表词频和词文档频率统计
 *
 * @param docs     文档列表
 * @param useSmart 是否使用只能分词
 * @return 词频列表 词-[词频,文档频率]
 * @throws IOException
 */
private static Map<String, Integer[]> countTFDF(List<String> docs, boolean useSmart) throws IOException {
    // 输出结果 Map
    Map<String, Integer[]> frequencies = new HashMap<>();
    for (String doc : docs) {
        if (StringUtils.isBlank(doc)) {
            continue;
        }
        DefaultConfig conf = new DefaultConfig();
        conf.setUseSmart(useSmart);
        // 使用 IKSegmenter 初始化文本信息并加载词典
        IKSegmenter ikSegmenter = new IKSegmenter(new StringReader(doc), conf);
        Lexeme lexeme;
        // 用于文档频率统计的 Set
        Set<String> terms = new HashSet<>();
        while ((lexeme = ikSegmenter.next()) != null) {
            if (lexeme.getLexemeText().length() > 1) {
                final String text = lexeme.getLexemeText();
                // 进行词频统计
                frequencies.compute(text, (k, v) -> {
                    if (v == null) {
                        v = new Integer[]{1, 0};
                    } else {
                        v[0] += 1;
                    }
                    return v;
                });
                terms.add(text);
            }
        } 
        // 进行文档频率统计:无需初始化 Map,统计词频后 Map 里面必有该词记录
        for (String term : terms) {
            frequencies.get(term)[1] += 1;
        }
    }
    return frequencies;
}

2. 获取词云 TopN 个词

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

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

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

/**
 * 按出现次数,从高到低排序取 TopN
 *
 * @param data 词和排序数字对应的 Map
 * @param TopN 词云展示的 TopN
 * @return 前 N 个词和排序值
 */
private static List<Map.Entry<String, Integer>> order(Map<String, Integer> data, int topN) {
    PriorityQueue<Map.Entry<String, Integer>> priorityQueue = new PriorityQueue<>(data.size(), new Comparator<Map.Entry<String, Integer>>() {
        @Override
        public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
            return o2.getValue().compareTo(o1.getValue());
        }
    });
    for (Map.Entry<String, Integer> entry : data.entrySet()) {
        priorityQueue.add(entry);
    }
    //TODO 当前100词频一致时(概率极低)的处理办法,if( list(0).value == list(99).value ){xxx}
    List<Map.Entry<String, Integer>> list = new ArrayList<>();
    //统计结果队列size和topN值取较小值列表
    int size = priorityQueue.size() <= topN ? priorityQueue.size() : topN;
    for (int i = 0; i < size; i++) {
        list.add(priorityQueue.remove());
    }
    return list;
}

3. IK 代码浅析

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

IK 代码结构

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

// IKSegmenter 类构造方法
public IKSegmenter(Reader input, Configuration cfg) {
    this.input = input;
    this.cfg = cfg;
    this.init();
}
// IKSegmenter 类初始化
private void init() {
    //初始化词典单例
    Dictionary.initial(this.cfg);
    //初始化分词上下文
    this.context = new AnalyzeContext(this.cfg);
    //加载子分词器
    this.segmenters = this.loadSegmenters();
    //加载歧义裁决器
    this.arbitrator = new IKArbitrator();
}

// Dictionary 类初始化词典
public static Dictionary initial(Configuration cfg) {
    if (singleton == null) {
        synchronized (Dictionary.class) {
            if (singleton == null) {
                singleton = new Dictionary(cfg);
                return singleton;
            }
        }
    }
    return singleton;
}

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

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

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

static int identifyCharType(char input) {
    if (input >= '0' && input <= '9') {
        return CHAR_ARABIC;
    } else if ((input >= 'a' && input <= 'z') || (input >= 'A' && input <= 'Z')) {
        return CHAR_ENGLISH;
    } else {
        Character.UnicodeBlock ub = Character.UnicodeBlock.of(input);
        //caster 增加#为中文字符
        if (ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
                || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
                || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A ||input=='#') {
            //目前已知的中文字符UTF-8集合
            return CHAR_CHINESE;

        } else if (ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS //全角数字字符和日韩字符
                //韩文字符集
                || ub == Character.UnicodeBlock.HANGUL_SYLLABLES
                || ub == Character.UnicodeBlock.HANGUL_JAMO
                || ub == Character.UnicodeBlock.HANGUL_COMPATIBILITY_JAMO
                //日文字符集
                || ub == Character.UnicodeBlock.HIRAGANA //平假名
                || ub == Character.UnicodeBlock.KATAKANA //片假名
                || ub == Character.UnicodeBlock.KATAKANA_PHONETIC_EXTENSIONS) {
            return CHAR_OTHER_CJK;

        }
    }
    //其他的不做处理的字符
    return CHAR_USELESS;
}

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

4. 进行词云展示

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

微词云使用方式

展示效果如下图所示:
微词云效果图

5. 总结

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容