创新实训(6)——有关博客的摘要抽取的算法实现(TextRank)

标题由于进行数据抽取时,必须顺便清洗数据,所以实现一个简单的抽取式摘要生成算法,来进行摘要的生成,这里我用java实现了TextRank算法。

TextRank算法思想

与PageRank一样,textrank算法给每一个句子一个权重,然后根据一个句子与其他句子的相似程度,将自己的权重按相似程度分配给其他句子,为了避免某一个句子的权重变为0,则需要加一个调和参数,进行平滑。
具体的公式为:
在这里插入图片描述
等式左边表示一个句子的权重(WS是weight_sum的缩写),右侧的求和表示每个相邻句子对本句子的贡献程度。与提取关键字的时候不同,一般认为全部句子都是相邻的,不再提取窗口。

求和的分母wji表示两个句子的相似程度,分母又是一个weight_sum,而WS(Vj)代表上次迭代j的权重。整个公式是一个迭代的过程。

相似程度的计算BM25算法

BM25算法常用来做搜索相关性的评分,对Query进行语素解析,生成语素qi;然后,对于每个搜索结果D,计算每个语素qi与D的相关性得分,最后,将qi相 对于D的相关性得分进行加权求和,从而得到Query与D的相关性得分。
在这里插入图片描述
其中,Q表示Query,qi表示Q解析之后的一个语素(对中文而言,我们可以把对Query的分词作为语素分析,每个词看成语素qi。);d表示一个搜索结果文档;Wi表示语素qi的权重; R(qi,d)表示语素qi与文档d的相关性得分。
下面用IDF来定义权重:
在这里插入图片描述
其中,N为索引中的全部文档数,n(qi)为包含了qi的文档数
根据IDF的定义可以看出,对于给定的文档集合,包含了qi的文档数越多,qi的权重则越低。也就是说,当很多文档都包含了qi时,qi的区分度就不高,因此使用qi来判断相关性时的重要度就较低。

在这里插入图片描述
通过计算即可得到一句查询与一个文本的相似程度

BM25算法的实现

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

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;//index表示现在是文档中的第几句话

        for (List<String> sentence : docs)
        {
            //对于每个句子的分词结果  计算 分词在这个句子中出现的频率 为tf   存成String int的映射形式
            Map<String, Integer> tf = new TreeMap<String, Integer>();
            for (String word : sentence)

            {
                //计算每个值出现的频数  作为tf
                Integer freq = tf.get(word);
                freq = (freq == null ? 0 : freq) + 1;
                tf.put(word, freq);
            }
            f[index] = tf;//存储每句话对应的tf值Map

            //根据tf值算df值  计算每个词出现在几个句子中
            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;
        }
        //根据df计算idf   公司为log(D - freq + 0.5) - Math.log(freq + 0.5)  D为文档中句子个数  0.5为平滑项
        for (Map.Entry<String, Integer> entry : df.entrySet())
        {
            //计算逆文档频率 idf
            String word = entry.getKey();
            Integer freq = entry.getValue();
            idf.put(word, Math.log(D - freq + 0.5) - Math.log(freq + 0.5));
        }
    }

    /**
     * 计算相似度 最终得到一个句子 与对应index句子的相关性得分
     * @param sentence
     * @param index
     * @return
     */
    public double sim(List<String> sentence, int index)
    {
        double score = 0;
        //对于一句话中的每一个单词  计算这个单词  与其他句子的相关性得分 这个得分用BM25计算出
        for (String word : sentence)
        {
            if (!f[index].containsKey(word)) {
                continue;
            }
            int d = docs.get(index).size();//index对应句子的词的个数
            Integer wf = f[index].get(word);//在index对应句子中 词word出现的次数
            //,参数b的作用是调整文档长度对相关性影响的大小。b越大,文档长度的对相关性得分的影响越大,反之越小。而文档的相对长度越长,K值将越大,则相关性得
            //分会越小。这可以理解为,当文档较长时,包含qi的机会越大,因此,同等fi的情况下,长文档与qi的相关性应该比短文档与qi的相关性弱。
            score += (idf.get(word) * wf * (k1 + 1)
                    / (wf + k1 * (1 - b + b * d
                    / avgdl)));
        }
        //最终得到一个句子  与对应index句子的相关性得分
        return score;
    }

    /**
     * 计算整体的相似度  计算每一个句子与其他所有句子的相似度
     * @param sentence
     * @return
     */
    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;
    }
}

(1)我们将一段文字,首先通过标点切分成句子,然后将句子分词,就可以得到[句子[单词]]形式的文档docs,
然后对于句子中的每一个次计算tf值和idf值(tf值为某个分词在某句话中出现的频率 df为每个词出现在几个句子中 idf为df计算得出的逆文档频率)
(2)我们计算两句话之间的相似程度,得到两个句子的相关性得分,使用BM25算法。
(3)最终我们计算一个句子与其他所有句子的相似程度。

实现TextRank算法

import com.hankcs.hanlp.HanLP;
import com.hankcs.hanlp.dictionary.stopword.CoreStopWordDictionary;
import com.hankcs.hanlp.seg.common.Term;

import java.util.*;

/**
 * TextRank 自动摘要
 */
public class TextRankSummary {
    /**
     * 阻尼系数,一般取值为0.85
     */
    final double d = 0.85f;
    /**
     * 最大迭代次数
     */
    final int maxIter = 200;
    final double min_diff = 0.001f;
    /**
     * 文档句子的个数
     */
    int docSentenceCount;
    /**
     * 拆分为[句子[单词]]形式的文档
     */
    List<List<String>> docs;
    /**
     * 排序后的最终结果 score <-> index
     * 使用treemap 因为treemap自动按key排序
     */
    TreeMap<Double, Integer> top;

    /**
     * 句子和其他句子的相关程度
     */
    double[][] weight;
    /**
     * 该句子和其他句子相关程度之和
     */
    double[] weightSum;
    /**
     * 迭代之后收敛的权重
     */
    double[] vertex;

    /**
     * BM25相似度
     */
    BM25 bm25;
    public TextRankSummary(List<List<String>> docs)
    {
        this.docs = docs;
        bm25 = new BM25(docs);
        docSentenceCount = docs.size();
        // 句子和其他句子的相关程度
        weight = new double[docSentenceCount][docSentenceCount];
        weightSum = new double[docSentenceCount];
        //该句子和其他句子相关程度之和
        vertex = new double[docSentenceCount];
        //选出排名靠前的句子
        top = new TreeMap<Double, Integer>(Collections.reverseOrder());
        solve();
    }

    /**
     * 构造矩阵计算最相似的内容
     */
    private void solve()
    {
        int cnt = 0;
        //对于文档中的每一句话 都要计算他与其他每一句话的相似程度  以及他与其他所有句子的总相似程度
        for (List<String> sentence : docs)
        {
            //对于文档中的每一个句子 计算它与文档中其他所有句子的相似程度
            double[] scores = bm25.simAll(sentence);

            //将计算结果存储在矩阵中
            weight[cnt] = scores;
            // 减掉自己,自己跟自己肯定最相似
            weightSum[cnt] = sum(scores) - scores[cnt];
            //所有句子权重初始化都为1
            vertex[cnt] = 1.0;
            ++cnt;
        }
        //开始使用textrank算法进行迭代 200轮
        for (int iter = 0; iter < maxIter; ++iter)
        {
            double[] m = new double[docSentenceCount];
            double maxDiff = 0;
            for (int i = 0; i < docSentenceCount; ++i)
            {
                m[i] = 1 - d;
                for (int j = 0; j < docSentenceCount; ++j)
                {
                    if (j == i || weightSum[j] == 0) {
                        continue;
                    }
                    m[i] += (d * weight[j][i] / weightSum[j] * vertex[j]);
                }
                double diff = Math.abs(m[i] - vertex[i]);
                if (diff > maxDiff)
                {
                    maxDiff = diff;
                }
            }
            vertex = m;
            if (maxDiff <= min_diff) {
                break;
            }
        }
        // 然后进行排序 输出权重最大的那个
        for (int i = 0; i < docSentenceCount; ++i)
        {
            top.put(vertex[i], i);
        }
    }

    /**
     * 获取前几个关键句子
     * @param size 要几个
     * @return 关键句子的下标
     */
    public int[] getTopSentence(int size)
    {
        Collection<Integer> values = top.values();
        size = Math.min(size, values.size());
        int[] indexArray = new int[size];
        Iterator<Integer> it = values.iterator();
        for (int i = 0; i < size; ++i)
        {
            indexArray[i] = it.next();
        }
        return indexArray;
    }

    /**
     * 简单的求和
     * @param array
     * @return
     */
    private static double sum(double[] array)
    {
        double total = 0;
        for (double v : array)
        {
            total += v;
        }
        return total;
    }

    /**
     * 将文章分割为句子  分割依据为标点符号
     * @param document
     * @return
     */
    static List<String> spiltSentence(String document)
    {
        List<String> sentences = new ArrayList<String>();
        if (document == null) {
            return sentences;
        }
        String regex1 = "[\r\n]";
        String regex2 = "[,,。::“”??!!;;]";
        for (String line : document.split(regex1)) {
            line = line.trim();
            if (line.length() == 0) {continue;}

            for (String sent : line.split(regex2))
            {
                sent = sent.trim();
                if (sent.length() == 0) {
                    continue;
                }
                sentences.add(sent);
            }
        }

        return sentences;
    }

    /**
     * 是否应当将这个term纳入计算,词性属于名词、动词、副词、形容词
     * @param term
     * @return 是否应当
     */
    public static boolean shouldInclude(Term term) {
        return CoreStopWordDictionary.shouldInclude(term);
    }

    /**
     * 一句话调用接口
     * @param document 目标文档
     * @param size 需要的关键句的个数
     * @return 关键句列表
     */
    public static String getTopSentenceList(String document, int size) {
        List<String> sentenceList = spiltSentence(document);
        //存储句子分词后的结果
        List<List<String>> docs = new ArrayList<List<String>>();
        for (String sentence : sentenceList) {
            //利用HanLP的接口将句子进行分词
            List<Term> termList = HanLP.segment(sentence);
            List<String> wordList = new LinkedList<String>();
            //是否应当将这个term纳入计算,词性属于名词、动词、副词、形容词
            for (Term term : termList) {
                if (shouldInclude(term)) {
                    wordList.add(term.word);
                }
            }
            docs.add(wordList);
        }
        TextRankSummary textRankSummary = new TextRankSummary(docs);
        //然后获取排名最高的几个句子
        int[] topSentence = textRankSummary.getTopSentence(size);
        List<String> resultList = new LinkedList<String>();

        for (int i : topSentence) {
            resultList.add(sentenceList.get(i));
        }
        String result = String.join("-",resultList);
        return result;
    }




}

(1)首先,输入目标文本和需要生成的摘要个数,然后我们现将文本,通过标点符号切分成句子,然后对每句话进行分词,这里分词使用了HanLP库(https://github.com/hankcs/hanlp),最后得到文档docs。
(2)然后通过docs使用BM25算法,计算每句话相互之间的文本相似度
(3)有了每句话的相似度之后,就可以给每个句子一个初始的权重,使用textrank算法,结合句子之间的相似度进行迭代,迭代200轮之后,停止迭代,输出权重最高的句子作为这篇文本的摘要。
(与pagerank算法思想相同,将自己的权重通过文本相似度的不同按比例分给不同的其他文本,自己也获得其他文本传来的权重,然后逐渐收敛)

最终的效果为

在这里插入图片描述
在这里插入图片描述
他只是抽取了几句,在文本中权重比较大的句子,语义之间没有连贯性,效果很不好。

明天准备尝试以下深度学习的方法来进行摘要的生成。

猜你喜欢

转载自blog.csdn.net/baidu_41871794/article/details/106652899