基于互信息和左右信息熵的短语提取识别

短语提取是从一篇文章中提取出主要的短语(短语是由几个词构成的),短语提取常用于搜索引擎与推荐系统。

下面的介绍是基于HanLP基于互信息和左右信息熵的短语提取识别开源实现。

给定文本:

"算法工程师\n" +
                "算法(Algorithm)是一系列解决问题的清晰指令,也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。" +
                "如果一个算法有缺陷,或不适合于某个问题,执行这个算法将不会解决这个问题。不同的算法可能用不同的时间、" +
                "空间或效率来完成同样的任务。一个算法的优劣可以用空间复杂度与时间复杂度来衡量。算法工程师就是利用算法处理事物的人。\n" +
                "\n" +
                "1职位简介\n" +
                "算法工程师是一个非常高端的职位;\n" +
                "专业要求:计算机、电子、通信、数学等相关专业;\n" +
                "学历要求:本科及其以上的学历,大多数是硕士学历及其以上;\n" +
                "语言要求:英语要求是熟练,基本上能阅读国外专业书刊;\n" +
                "必须掌握计算机相关知识,熟练使用仿真工具MATLAB等,必须会一门编程语言。\n" +
                "\n" +
                "2研究方向\n" +
                "视频算法工程师、图像处理算法工程师、音频算法工程师 通信基带算法工程师\n" +
                "\n" +
                "3目前国内外状况\n" +
                "目前国内从事算法研究的工程师不少,但是高级算法工程师却很少,是一个非常紧缺的专业工程师。" +
                "算法工程师根据研究领域来分主要有音频/视频算法处理、图像技术方面的二维信息算法处理和通信物理层、" +
                "雷达信号处理、生物医学信号处理等领域的一维信息算法处理。\n" +
                "在计算机音视频和图形图像技术等二维信息算法处理方面目前比较先进的视频处理算法:机器视觉成为此类算法研究的核心;" +
                "另外还有2D转3D算法(2D-to-3D conversion),去隔行算法(de-interlacing),运动估计运动补偿算法" +
                "(Motion estimation/Motion Compensation),去噪算法(Noise Reduction),缩放算法(scaling)," +
                "锐化处理算法(Sharpness),超分辨率算法(Super Resolution),手势识别(gesture recognition),人脸识别(face recognition)。\n" +
                "在通信物理层等一维信息领域目前常用的算法:无线领域的RRM、RTT,传送领域的调制解调、信道均衡、信号检测、网络优化、信号分解等。\n" +
                "另外数据挖掘、互联网搜索算法也成为当今的热门方向。\n" +
                "算法工程师逐渐往人工智能方向发展。";

尝试从文本中提取出短语。

分句,分词,去掉停用词

首先,需要对整个文本进行分句,然后分词,然后去掉停用词(处理方法在之前的文章中都有介绍过了)。得到结果如下:

sentenceList = {LinkedList@906}  size = 50
 0 = {ArrayList@909}  size = 2
  0 = {Term@960} "算法/n"
  1 = {Term@961} "工程师/nnt"
 1 = {ArrayList@910}  size = 4
  0 = {Term@965} "算法/n"
  1 = {Term@966} "解决问题/v"
  2 = {Term@967} "清晰/a"
  3 = {Term@968} "指令/n"
 2 = {ArrayList@911}  size = 1
  0 = {Term@974} "也就是说/l"
 3 = {ArrayList@912}  size = 3
  0 = {Term@977} "能够/v"
  1 = {Term@978} "规范/v"
  2 = {Term@979} "输入/v"
 4 = {ArrayList@913}  size = 5
  0 = {Term@984} "有限/a"
  1 = {Term@985} "时间/n"
  2 = {Term@986} "获得/v"
  3 = {Term@987} "要求/n"
  4 = {Term@988} "输出/vn"
 5 = {ArrayList@914}  size = 2
  0 = {Term@995} "算法/n"
  1 = {Term@996} "缺陷/n"
 6 = {ArrayList@915}  size = 2
  0 = {Term@1003} "适合于/v"
  1 = {Term@1004} "问题/n"
 7 = {ArrayList@916}  size = 5
  0 = {Term@1008} "执行/v"
  1 = {Term@1009} "算法/n"
  2 = {Term@1010} "不会/v"
  3 = {Term@1011} "解决/v"
  4 = {Term@1012} "问题/n"
  
  ......

(太长了,后面我省略了一些)

统计词频

得到所有的词语后,就进行词频统计,在这一步需要统计单个词频、二阶共现词频、三阶共现词频。

单个词频

单个词频就是每个词出现的总次数。比如算法 31表示词算法在整个文档中出现的次数是31次。

二阶共现词频

二阶共现词频是两个词语一起出现的次数。比如算法→工程师 10表示词算法→工程师在整个文档中出现的次数是10次。

三阶共现词频

三阶共现词频也就是二阶短语“算法→研究”后面的接续:“算法→研究→工程师”。同时,为了接下来计算的方便,还需要统计二阶串“算法→研究”的前面可能的接续“从事→算法→研究”;在源码中,作者使用了前缀树来储存词与词频,所以为了方便,记作“算法→研究←从事”。

比如算法→研究→工程师 1表示词算法→研究→工程师出现的次数是1次; 算法→研究←从事 1表示词算法→研究→从事出现的次数是1次。

为了理解前缀树,我画了一个图:

image

上面的图表示的是使用前缀树存储二阶共现词频(只画出了其中的一部分)。 存储的部分词:

不会→解决
不同→时间
不同→算法
算法→不会
算法→优劣
算法→可能
算法→处理
算法→工程师
算法→机器
算法→研究
算法→缺陷
算法→解决问题
算法→领域

信息熵

互信息体现了两个变量之间的相互依赖程度。二元互信息是指两个事件相关性的量(论文给出如下定义):

image

互信息值越高, 表明X和Y相关性越高, 则X和Y 组成短语的可能性越大; 反之, 互信息值越低,X 和Y之间相关性越低, 则X 和Y之间存在短语边界的可能性越大。

公式中的X和Y指的是两个相邻的单词,P值是它的出现概率。

具体到这个例子,“算法→研究”一共出现了2次,而二阶短语一共有191个,所以上式的P(X,Y)= 2 / 191。同理可以求出P(X)P(Y)。

左右熵

熵这个术语表示随机变量不确定性的量度。具体表述如下: 一般地, 设X 是取有限个值的随机变量( 或者说X 是有限个离散事件的概率场) , X 取值x 的概率为P ( x ) , 则X 的熵定义为:

image

左右熵是指多字词表达的左边界的熵和右边界的熵。左右熵的公式如下:

image

左信息熵是所有指向该词的词,比如算法→研究这个词的左信息熵相关词是:

算法→研究←从事
算法→研究←成为

右信息熵是该词指向所有的词,比如算法→研究这个词的右信息熵相关词是:

算法→研究→工程师
算法→研究→核心

最后,将每个二元共现词的互信息熵,左信息熵,右信息熵相加,得到最终的分数,取出前几个最大分数值的词,即为提取的短语。

[算法工程师, 算法处理, 一维信息, 算法研究, 信号处理]

参考文章:

TextRank算法实现自动摘要

TextRank算法是根据PageRank改进而来的,关于PageRank算法你可以参考这里PageRank算法–从原理到实现

PageRank的计算公式:

PageRank的主要思想是用一个数值(PR值)来表示一个节点的重要层度,PR值的计算方式是对每个出度节点的PR值求平均,然后加上一个平滑的值,不断迭代进行求解。

TextRank的计算公式:

TextRank公式在PageRank的公式的基础上,引入了边的权值的概念,代表两个句子的相似度。

假设我现在有一段文字,如何进行自动摘要呢?

水利部水资源司司长陈明忠9月29日在国务院新闻办举行的新闻发布会上透露,
根据刚刚完成了水资源管理制度的考核,有部分省接近了红线的指标,
 有部分省超过红线的指标。对一些超过红线的地方,陈明忠表示,
 对一些取用水项目进行区域的限批,严格地进行水资源论证和取水许可的批准。;

HanLP中的自动摘要就是基于TextRank算法的,它的主要步骤如下:

  • 句子划分
  • 句子分词,转成doc对象
  • TextRank自动摘要
  • 获取前几个句子
  /**
     * @param document 目标文档
     * @param size     需要的关键句的个数
     * @param sentence_separator 句子分隔符,正则格式, 如:[。??!!;;]
     * @return 关键句列表
     */
    public static List<String> getTopSentenceList(String document, int size, String sentence_separator)
    {
        List<String> sentenceList = splitSentence(document, sentence_separator);//句子划分
        List<List<String>> docs = convertSentenceListToDocument(sentenceList);//句子分词,转成doc对象
        TextRankSentence textRank = new TextRankSentence(docs);//TextRank自动摘要
        int[] topSentence = textRank.getTopSentence(size);//获取前几个句子
        List<String> resultList = new LinkedList<String>();
        for (int i : topSentence)
        {
            resultList.add(sentenceList.get(i));
        }
        return resultList;
    }

句子划分

句子划分直接使用制定的分割符对整个句子进行切割就好了。

/**
     * 将文章分割为句子
     *	 
     * @param document 待分割的文档
     * @param sentence_separator 句子分隔符,正则表达式,如:   [。:??!!;;]
     * @return
     */
    static List<String> splitSentence(String document, String sentence_separator)
    {
        List<String> sentences = new ArrayList<String>();
        for (String line : document.split("[\r\n]"))
        {
            line = line.trim();
            if (line.length() == 0) continue;
            for (String sent : line.split(sentence_separator))//分割句子 [,,。::“”??!!;;]
            {
                sent = sent.trim();
                if (sent.length() == 0) continue;
                sentences.add(sent);
            }
        }

        return sentences;
    }

句子划分之后得到的结果:

sentenceList = {ArrayList@529}  size = 8
 0 = "水利部水资源司司长陈明忠9月29日在国务院新闻办举行的新闻发布会上透露"
 1 = "根据刚刚完成了水资源管理制度的考核"
 2 = "有部分省接近了红线的指标"
 3 = "有部分省超过红线的指标"
 4 = "对一些超过红线的地方"
 5 = "陈明忠表示"
 6 = "对一些取用水项目进行区域的限批"
 7 = "严格地进行水资源论证和取水许可的批准"

句子分词,转成doc对象

对每个句子进行分词,会去掉一些停用词(因为这些词在句子中的作用不大,例如:的 地 得 这种词,对自动摘要的影响不大,需要去除)。

    /**
     * 将句子列表转化为文档
     *
     * @param sentenceList
     * @return
     */
    private static List<List<String>> convertSentenceListToDocument(List<String> sentenceList)
    {
        List<List<String>> docs = new ArrayList<List<String>>(sentenceList.size());
        for (String sentence : sentenceList)
        {
            List<Term> termList = StandardTokenizer.segment(sentence.toCharArray());//句子分词,源码中用的维特比分词
            List<String> wordList = new LinkedList<String>();
            for (Term term : termList)
            {
                if (CoreStopWordDictionary.shouldInclude(term))//去掉停用词(常见的停用词直接写在字典里面了)
                {
                    wordList.add(term.word);
                }
            }
            docs.add(wordList);
        }
        return docs;
    }

这一步得到的结果是:

docs = {ArrayList@539}  size = 8
 0 = "水利部" "水资源" "司" "司长" "陈明忠" "9月" "国务院新闻办" "举行" "新闻" "发布会" "透露"
 1 =  "刚刚" "完成" "水资源" "管理制度" "考核"
 2 =  "部分" "省" "接近" "红线" "指标"
 3 =  "部分" "省" "超过" "红线" "指标"
 4 =  "超过" "红线" "地方"
 5 =  "陈明忠" "表示" 
 6 =  "取" "用水" "项目" "进行" "区域" "限"
 7 =  "严格" "进行" "水资源" "论证" "取水" "许可" "批准"

TextRank自动摘要

TextRank公式中需要计算权重w_ij,它描述了两个句子的相似度。计算方法可以使用BM25算法

下面是HanLP中对BM25算法的一个实现

/*
 * <summary></summary>
 * <author>He Han</author>
 * <email>hankcs.cn@gmail.com</email>
 * <create-date>2014/8/22 14:17</create-date>
 *
 * <copyright file="BM25.java" company="上海林原信息科技有限公司">
 * Copyright (c) 2003-2014, 上海林原信息科技有限公司. All Right Reserved, http://www.linrunsoft.com/
 * This source is subject to the LinrunSpace License. Please contact 上海林原信息科技有限公司 to get more information.
 * </copyright>
 */
package com.hankcs.hanlp.summary;

import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
 * 搜索相关性评分算法
 * @author hankcs
 */
public class BM25
{
    /**
     * 文档句子的个数
     */
    int D;

    /**
     * 文档句子的平均长度
     */
    double avgdl;

    /**
     * 拆分为[句子[单词]]形式的文档
     */
    List<List<String>> docs;

    /**
     * 文档中每个句子中的每个词与词频
     */
    Map<String, Integer>[] f;

    /**
     * 文档中全部词语与出现在几个句子中
     */
    Map<String, Integer> df;

    /**
     * IDF
     */
    Map<String, Double> idf;

    /**
     * 调节因子
     */
    final static float k1 = 1.5f;

    /**
     * 调节因子
     */
    final static float b = 0.75f;

    public BM25(List<List<String>> docs)
    {
        this.docs = docs;
        D = docs.size();//文档句子的个数
        for (List<String> sentence : docs)
        {
            avgdl += sentence.size();
        }
        avgdl /= D; // 句子平均长度
        f = new Map[D]; // 保存每一个句子中的 词与词频
        df = new TreeMap<String, Integer>(); //保存所有的 词与词频
        idf = new TreeMap<String, Double>();//保存所有词的逆文档频率
        init();
    }

    /**
     * 在构造时初始化自己的所有参数
     */
    private void init()
    {
        int index = 0;
        for (List<String> sentence : docs)//依次处理每一个句子
        {
            Map<String, Integer> tf = new TreeMap<String, Integer>();//保存词与词频
            for (String word : sentence)
            {
                Integer freq = tf.get(word);
                freq = (freq == null ? 0 : freq) + 1;
                tf.put(word, freq);//保存词与词频
            }
            f[index] = tf; //保存每一个句子中的 词与词频
            for (Map.Entry<String, Integer> entry : tf.entrySet())//计算所有的 词与词频
            {
                String word = entry.getKey();
                Integer freq = df.get(word);
                freq = (freq == null ? 0 : freq) + 1;
                df.put(word, freq);//保存所有的 词与词频
            }
            ++index;
        }
        for (Map.Entry<String, Integer> entry : df.entrySet())//计算所有词的逆文档频率
        {
            String word = entry.getKey();
            Integer freq = entry.getValue();
            idf.put(word, Math.log(D - freq + 0.5) - Math.log(freq + 0.5));//保存所有词的逆文档频率
        }
    }

    //相似度计算:计算当前句子与某一个句子的相似度
    public double sim(List<String> sentence, int index)
    {
        double score = 0;
        for (String word : sentence)
        {
            if (!f[index].containsKey(word)) continue;//如果当前句子中不包含该词,则跳过
            int d = docs.get(index).size();//当前句子的大小(包含多少个词)
            Integer wf = f[index].get(word);//当前句子中的词的词频
            score += (idf.get(word) * wf * (k1 + 1)
                    / (wf + k1 * (1 - b + b * d
                                                / avgdl)));//BM25计算公式
        }

        return score;
    }

    //相似度计算:依次计算当前句子与文档中所有句子的相似度
    public double[] simAll(List<String> sentence)
    {
        double[] scores = new double[D];//相似度评分
        for (int i = 0; i < D; ++i)//依次计算当前句子与文档中所有句子的相似度
        {
            scores[i] = sim(sentence, i);
        }
        return scores;
    }
}

上面的代码只是计算出了TextRank中的w_ij,接下来就是TextRank的实现了:

    //TextRank的具体实现
    private void solve()
    {
        int cnt = 0;
        for (List<String> sentence : docs)
        {
            double[] scores = bm25.simAll(sentence);//计算一个句子和其他句子的相似度
            weight[cnt] = scores;  // 第cnt个句子与其他句子的相似度
            weight_sum[cnt] = sum(scores) - scores[cnt]; // 减掉自己,自己跟自己肯定最相似
            vertex[cnt] = 1.0;//初始TextRank值
            ++cnt;
        }
        //不断迭代,慢慢收敛
        for (int _ = 0; _ < max_iter; ++_)
        {
            double[] m = new double[D];
            double max_diff = 0;
            for (int i = 0; i < D; ++i)
            {
                m[i] = 1 - d;
                for (int j = 0; j < D; ++j)
                {
                    if (j == i || weight_sum[j] == 0) continue;
                    m[i] += (d * weight[j][i] / weight_sum[j] * vertex[j]);//TextRank计算公式
                }
                double diff = Math.abs(m[i] - vertex[i]);
                if (diff > max_diff)//记录每次变化最大的值
                {
                    max_diff = diff;
                }
            }
            vertex = m;//保存当前值,作为公式中上一次的值
            if (max_diff <= min_diff) break;
        }
        // 我们来排个序吧(从大到小),方便后面的句子获取
        for (int i = 0; i < D; ++i)
        {
            top.put(vertex[i], i);
        }
    }

TextRank算法计算结果是:

top = {TreeMap@924}  size = 8
 0 = {TreeMap$Entry@931} "1.349988008966875" -> "7"
 1 = {TreeMap$Entry@932} "1.3342420985416243" -> "0"
 2 = {TreeMap$Entry@933} "1.3197213990970156" -> "3"
 3 = {TreeMap$Entry@934} "1.0274137158613745" -> "2"
 4 = {TreeMap$Entry@935} "0.8364585720283898" -> "5"
 5 = {TreeMap$Entry@936} "0.7696043981680054" -> "6"
 6 = {TreeMap$Entry@937} "0.7097069222950998" -> "1"
 7 = {TreeMap$Entry@938} "0.6528648850416094" -> "4"

从上面我们可以看到,第7个句子的TR值最高,即句子:"严格地进行水资源论证和取水许可的批准"

获取前几个句子

TextRank算法计算结束之后,就知道了每个句子的TR值,越大越好,所以,最终获取指定的个数句子就行(这里获取前3个句子)。

[严格地进行水资源论证和取水许可的批准,
水利部水资源司司长陈明忠9月29日在国务院新闻办举行的新闻发布会上透露,
有部分省超过红线的指标]

参考资源:

TextRank算法提取关键词

TextRank算法是根据PageRank改进而来的,关于PageRank算法你可以参考这里PageRank算法–从原理到实现

PageRank的计算公式:

PageRank的主要思想是用一个数值(PR值)来表示一个节点的重要层度,PR值的计算方式是对每个出度节点的PR值求平均,然后加上一个平滑的值,不断迭代进行求解。

TextRank的计算公式:

TextRank公式在PageRank的公式的基础上,引入了边的权值的概念,代表两个句子的相似度。

在用TextRank提取关键字的时候,实际上它退化成了PageRank算法,接下来,我们看看具体的实现,下面的实现是参考HanLP的。

假设我现在有一段文字,如何提取出里面的关键字呢?

程序员(英文Programmer)是从事程序开发、维护的专业人员。一般将程序员分为程序设计人员和程序编码人员,但两者的界限并不非常清楚,特别是在中国。
软件从业人员分为初级程序员、高级程序员、系统分析员和项目经理四大类。

首先对这句话分词,这里可以借助各种分词项目,比如HanLP分词,得出分词结果:

[程序员/nnt, (/w, 英文/nz, Programmer/nx, )/w, 是/vshi, 从事/vi, 程序/n, 开发/vn, 、/w, 维护/v, 的/ude1, 专业/n, 人员/n, 。/w, 一般/ad, 将/d, 程序员/nnt, 分为/v, 程序/n, 设计/vn, 人员/n, 和/cc, 程序/n, 编码/n, 人员/n, ,/w, 但/c, 两者/rzv, 的/ude1, 界限/n, 并不/d, 非常/d, 清楚/a, ,/w, 特别是在/l, 中国/ns, 。/w, 软件/n, 从业人员/nz, 分为/v, 初级/b, 程序员/nnt, 、/w, 高级/a, 程序员/nnt, 、/w, 系统分析员/nnt, 和/cc, 项目经理/nnt, 四大/n, 类/q, 。/w]

然后去掉里面的停用词,这里去掉了标点符号、常用词,最后只保留了名词、动词、形容词、副词。得出实际有用的词语:

[程序员, 英文, Programmer, 从事, 程序, 开发, 维护, 专业, 人员, 程序员, 分为, 程序, 设计, 人员, 程序, 编码, 人员, 界限, 并不, 非常, 清楚, 特别是在, 中国, 软件, 从业人员, 分为, 程序员, 高级, 程序员, 系统分析员, 项目经理, 四大]

之后建立两个大小为5的窗口,每个单词将票投给它身前身后距离5以内的单词:

{Programmer=[从事, 开发, 程序, 程序员, 维护, 英文], 
专业=[人员, 从事, 分为, 开发, 程序, 程序员, 维护], 
中国=[从业人员, 分为, 并不, 清楚, 特别是在, 程序员, 软件, 非常], 人员=[专业, 分为, 并不, 开发, 清楚, 界限, 程序, 程序员, 维护, 编码, 设计, 非常], 
从业人员=[中国, 分为, 清楚, 特别是在, 程序员, 软件, 高级], 从事=[Programmer, 专业, 开发, 程序, 程序员, 维护, 英文], 
分为=[专业, 中国, 人员, 从业人员, 特别是在, 程序, 程序员, 系统分析员, 维护, 设计, 软件, 高级], 
四大=[程序员, 系统分析员, 项目经理, 高级], 
并不=[中国, 人员, 清楚, 特别是在, 界限, 程序, 编码, 非常], 
开发=[Programmer, 专业, 人员, 从事, 程序, 程序员, 维护, 英文], 
清楚=[中国, 人员, 从业人员, 并不, 特别是在, 界限, 软件, 非常], 
特别是在=[中国, 从业人员, 分为, 并不, 清楚, 界限, 软件, 非常], 
界限=[人员, 并不, 清楚, 特别是在, 程序, 编码, 非常], 
程序=[Programmer, 专业, 人员, 从事, 分为, 并不, 开发, 界限, 程序员, 维护, 编码, 英文, 设计], 
程序员=[Programmer, 专业, 中国, 人员, 从业人员, 从事, 分为, 四大, 开发, 程序, 系统分析员, 维护, 英文, 设计, 软件, 项目经理, 高级], 系统分析员=[分为, 四大, 程序员, 项目经理, 高级], 
维护=[Programmer, 专业, 人员, 从事, 分为, 开发, 程序, 程序员], 
编码=[人员, 并不, 界限, 程序, 设计, 非常], 
英文=[Programmer, 从事, 开发, 程序, 程序员], 
设计=[人员, 分为, 程序, 程序员, 编码], 
软件=[中国, 从业人员, 分为, 清楚, 特别是在, 程序员, 非常, 高级], 非常=[中国, 人员, 并不, 清楚, 特别是在, 界限, 编码, 软件], 
项目经理=[四大, 程序员, 系统分析员, 高级], 
高级=[从业人员, 分为, 四大, 程序员, 系统分析员, 软件, 项目经理]}

最后用PageRank算法进行计算,得到每个词的PR值,最后的结果是:

{特别是在=1.0015739, 程序员=2.0620303, 编码=0.78676623, 四大=0.6312981, 英文=0.6835063, 非常=1.0018439, 界限=0.88890904, 系统分析员=0.74232763, 从业人员=0.8993066, 程序=1.554001, 专业=0.88107216, 项目经理=0.6312981, 设计=0.6702926, 从事=0.9027207, Programmer=0.7930236, 软件=1.0078223, 人员=1.4288887, 清楚=0.9998723, 中国=0.99726284, 开发=1.0065585, 并不=0.9968608, 高级=0.9673803, 分为=1.4548829, 维护=0.9946941}

最后获取里面前5个最高的值作为关键字,即:[程序员, 程序, 分为, 人员, 软件]

rank算法的部分源码:


    /**
     * 使用已经分好的词来计算rank
     *
     * @param termList
     * @return
     */
    public Map<String, Float> getRank(List<Term> termList)
    {
        List<String> wordList = new ArrayList<String>(termList.size());
        for (Term t : termList)
        {
            if (shouldInclude(t))//只保留 名词、动词、副词、形容词
            {
                wordList.add(t.word);
            }
        }

        Map<String, Set<String>> words = new TreeMap<String, Set<String>>();//保存每个词的相邻词(邻接表)
        Queue<String> que = new LinkedList<String>();//使用队列作为滑动窗口
        for (String w : wordList)//获取各个词的窗口词,可以理解为图的邻接表
        {
            if (!words.containsKey(w))
            {
                words.put(w, new TreeSet<String>());
            }
            // 复杂度O(n-1)
            if (que.size() >= 5)//使用队列作为滑动窗口
            {
                que.poll();
            }
            for (String qWord : que)
            {
                if (w.equals(qWord))//碰到一样的词,就不需要再放了
                {
                    continue;
                }
                //既然是邻居,那么关系是相互的,遍历一遍即可
                words.get(w).add(qWord);
                words.get(qWord).add(w);
            }
            que.offer(w);
        }
//        System.out.println(words);
        Map<String, Float> score = new HashMap<String, Float>();
        //依据TF来设置初值
        for (Map.Entry<String, Set<String>> entry : words.entrySet()){ 
        	score.put(entry.getKey(),sigMoid(entry.getValue().size()));
        }
        //开始迭代,不断收敛
        for (int i = 0; i < max_iter; ++i)
        {
            Map<String, Float> m = new HashMap<String, Float>();//保存每次结果值
            float max_diff = 0;
            for (Map.Entry<String, Set<String>> entry : words.entrySet())
            {
                String key = entry.getKey();
                Set<String> value = entry.getValue();
                m.put(key, 1 - d);
                for (String element : value)//根据公式进行计算(这里是抽取关键字,TextRank退化为PageRake)
                {
                    int size = words.get(element).size();
                    if (key.equals(element) || size == 0) continue;
                    m.put(key, m.get(key) + d / size * (score.get(element) == null ? 0 : score.get(element)));
                }
                max_diff = Math.max(max_diff, Math.abs(m.get(key) - (score.get(key) == null ? 0 : score.get(key))));//出现迭代前后差值最大的值 都小于阈值就停止迭代
            }
            score = m;//作为下一次的初始值
            if (max_diff <= min_diff) break; // 出现迭代前后差值最大的值 都小于阈值就停止迭代
        }

        return score;//最终的分数值
    }

参考资料: