基于语义连贯性实现主题挖掘和分类

来源:互联网 发布:ubuntu输入法安装 编辑:程序博客网 时间:2024/05/18 14:45

约定一下文中使用的一些词的含义:

  • 文章:一般来说,一篇文章具有一个标题、一个或多个段落组成,其他的我们暂时不考虑。
  • 段落:一篇文章可以根据缩进(有些可能不存在缩进)或回车换行,将文章分成多个段,而每段是由数个句子组成。
  • 片段:片段是由一个或者多个段落组成,但是片段最多不能大于一篇文章的全部段落数,我们限定在一篇文章之内。


基础概述


对于给定的一篇文章,它到底在围绕哪一个主题(或哪几个主题)进行描述,我们在实际生活中经常会遇到。我们在读小学的时候,老师教我们给文章分段并归纳段落大意,其实,这就是在考虑基于文章段落之间的关系和语义内容对整个文章进行划分,得到的结果是确定某一些连续的段落属于某一个主题。如果一篇文章段落很多,而且根据标题可以确定具有一个主题A,那么这篇文章仍然可以在主题A的范畴内,继续提炼出多个子主题集合{A1, A2, ..., An},也就是说文章仍然是可分的。

上面讲述的是给定一篇文章,根据文章内容抽取出具有多个主题的段落,你可能很容易想到,这就是数据挖掘中的聚类分析:把具有相同特征(主题)的一些连续的段落聚集成一个类,而不去限定类别是什么,可能你根本也不知道能划分成哪些“基于一定主题”的文章片段,也可能不同的文章片段之间出现重叠的段落。在实际中,可能存在这样的应用场景。

但是,在实际中更多的是,基于给定一个关键字词典,基于词典来对一篇文章进行主题划分:抽取出文章的某些段落作为一个片段,这个片段当当且仅当这一组段落在讲述某一个主题时才成立。一个主题通常可以由一个词向量来表达,例如主题“体育运动”可以由词向量(足球, 篮球, 排球 ,网球 , 跑步, NBA)来表达,文章的某些段落中在讲述词向量中词项描述的内容时,基本可以认为在讲述给定的主题。词向量是否足够丰富地表达主题,这就要看你应用需求和你的词典中词项的分布情况。我们分析一下:存在“主题-词向量”词典,也就是说在分析文章的时候,它的类标签只限定于给定的主题词典,实际上这是分类,基于文章内容对文章的多个片段进行分类,而待分类的文章片段需要基于某些启发式信息(或经过预处理的元数据)进行抽取。

我们讨论下面两个问题:

  • 第一个:获取主题信息困难

你在看一篇字数很长的文章的时,如果你是想快速获取到你想知道的某一个主题内容,那你必须读过整篇文章之后,才知道哪些段落在讲述这个主题。很有可能一篇文章有50多段,但是描述你想要的主题的只有其中的2~3段,这是你要花费时间浏览很多不相关的信息。如果这篇文章正是你展现给用户的信息,那么这种信息太没有价值了(虽然某些段落可能是精华,但对用户来说不次于垃圾信息),用户体验相当的差:用户可能在浏览的过程中对主题的关注度已经分散了,可能因此而离开你的提供数据的网站——用户流失。

  • 第二个:主题相关信息不连贯
虽然我从文章中抽取出了这些包含指定主题词的段落,但是这些段落需要一定的连贯性,才能让用户在阅读的时候语义更加流畅,否则用户感觉自己在阅读一篇没有完整性的文章片段,而且语义的残缺造成了无法正确获取主题的实际语义。所以,我们要考虑语义连贯性——我找了一个有关语义连贯性的定义及说明,引用如下:

书面表达中句与句之间的组合与衔接问题,保持语言连贯,需要兼顾话题(有共同的话题)、语序(合理的语序)和语言的运用(语言的衔接与呼应)三个方面还要注意语境、句式的协调一致。1、话题统一指组成段落的句子间,或是组成复句的分句间,有紧密联系,围绕着一个中心,集中表现一个事实、场景、或思想观点,无关的话不掺杂在一起。 (1)陈述对象一致。(2)观点和材料的统一。2、句式前后一致(照应)能收到形势整齐,音节和谐、气势贯通的修辞效果。(1)句式要协调一致。指一组句子中几个句子的句式要大体相同,读来才琅琅上口,语气连贯或相互对应。(2)要有必要的过渡、交待、衔接、呼应。恰当的使用关联词语及比照式的词语。3、合乎事理、语境(1)意思表达要合乎客观事例,否则,上下句在事理上出现了“裂痕”,衔接不上。(2)表达要合乎语境。对于写景的复句和语段,要注意语境因素,要分析景物、情调、写法的特点。景物有远近动静不同;色彩有鲜明、暗淡之分,气氛有热烈凄清;视角有高低俯仰之异;态度或褒或贬。要保持和谐一致。4、语序合理(1)时间顺序。按陈述内容的时间先后作为排列顺序。(多用于记叙文)(2)空间顺序。按照空间的上下、左右、外内等顺序。(多用于描写、说明性文字)(3)逻辑顺序。一是人们对客观事物的认识顺序,如现象本质、个别一般、浅深、感性理性等;二是客观事物固有的内部逻辑,因果、轻重。

为了简化问题,我们考虑语义连贯性的如下几个要素:

  • 兼顾话题: 上面已经说过,通过主题及其主题相关词向量来表达,可以考虑主题相关词向量中的词项的出现频率,对片段的边缘段落加权并向两侧扩展段落;
  • 保持语序: 虽然包含主题词片段中某些段落之间不连续,例如段落A和C被抽取出来,但是B中不包含主题词,但是可以根据段落B的一些特征(如长度、包含图片)来选择,将B加入到这个片段的段落组中,使片段中个段落之间语义连贯。


基本思想


我们要实现的基于语义连贯性,对文章的片段进行挖掘(对选择的段落进行组合),主要是基于如下几点:

  • 建立自己的领域词典D:用来描述主题的词条(词向量)
  • 对文章进行分词,获取一篇文章中存在的且出现在词典中的关键词集合S
  • 根据关键词集合S中的每一个词,对整篇文章中的各个段落(有序)进行匹配,不考虑词频(TF)影响
  • 设定语义连贯性标准:如果按照文章段落顺序(序号),对序号i和序号j,若j-i<maxParagraphGap,则认为i和j两段连续(maxParagraphGap可以调整)
  • 语义连贯性扩展:如果一篇文章段落序号序列{0, 1, 2, ... , x, x+1, x+2, ..., y, y+1, ..., N},如果x-0<maxParagraphGap且N-y<maxParagraphGap,则选择整篇文章作为一个段落,亦即,整篇文章都在描述一个主题
  • 基于关键词集合S中的每一个关键词,在一篇文章抽取一组片段F={f1, f2, ... , fn},这组片段的选择标准:对F中的片段对应的每个段落集合{P1, P2, ... , Pm},统计段落数为n,一篇文章总段落数N,计算抽取出来的所有段落的覆盖率,可以设定一个阙值Coverage=n/N
当然,上述基本思想可以根据实际文章内容的特点进行个性化定制,具有很大的改进空间,优化抽取段落以及使按主题分类更加准确。


程序实现


实现上述基本思想的代码,程序中已经做了详细的注释,不再累述。代码如下所示:
package org.shirdrn.ie.travel;import java.io.BufferedReader;import java.io.FileReader;import java.io.IOException;import java.io.Reader;import java.io.StringReader;import java.util.ArrayList;import java.util.HashMap;import java.util.HashSet;import java.util.Iterator;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;import java.util.Map.Entry;import java.util.Set;import java.util.regex.Pattern;import org.apache.log4j.Logger;import org.apache.lucene.analysis.Analyzer;import org.apache.lucene.analysis.TokenStream;import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;import org.apache.lucene.analysis.tokenattributes.CharTermAttributeImpl;import com.chenlb.mmseg4j.Dictionary;import com.chenlb.mmseg4j.analysis.ComplexAnalyzer;import com.mongodb.BasicDBObject;import com.mongodb.DBCollection;import com.mongodb.DBCursor;import com.mongodb.DBObject;import org.shirdrn.common.DBConfig;/** * Article analysis tool. *  * @author shirdrn * @date 2011-12-13 */public class ArticleAnalyzeTool {private static final Logger LOG = Logger.getLogger(ArticleAnalyzeTool.class);private Analyzer analyzer;private String dictPath;private String wordsPath;/** 自定义关键字词典:通常是根据需要进行定向整理的 */private Set<String> wordSet = new HashSet<String>();private DBConfig articleConfig;private MongodbAccesser accesser;/** 最小段落内容长度限制 */private int globalMinParagraphLength = 3;/** 最大段落间隔数限制 */private int globalMaxParagraphGap = 3;/** 截取片段中的段落数,必须大于1 */private int globalMinFragmentParagraphsCount = 3;/** 截取最大片段数限制,必须大于1 */private int globalMinFragmentSize = 4;/** 一个word在一篇文章的片段集合,在整篇文章中的覆盖率:如果大于该值,则选择整篇文章作为一个片段,否则分别取出多个片段 */private double globalCoverage = 0.75;public ArticleAnalyzeTool(DBConfig articleConfig) {this.articleConfig = articleConfig;accesser = new MongodbAccesser(articleConfig);}/** * 初始化:初始化Lucene分析器,并加载词典 */public void initialize() {analyzer = new ComplexAnalyzer(Dictionary.getInstance(dictPath));try {BufferedReader reader = new BufferedReader(new FileReader(this.wordsPath));String line = null;while ((line = reader.readLine()) != null) {if (!line.isEmpty()) {wordSet.add(line.trim());}}reader.close();} catch (IOException e) {e.printStackTrace();}}/** * 分析程序入口主驱动方法 *  * @param conditions * @param config */public void runWork(Map<String, Object> conditions, DBConfig config) {MongodbAccesser newAccesser = new MongodbAccesser(config);DBCollection newCollection = newAccesser.getDBCollection(config.getCollectionName());DBCollection collection = accesser.getDBCollection(articleConfig.getCollectionName());DBObject q = new BasicDBObject();if (conditions != null && !conditions.isEmpty()) {q = new BasicDBObject(conditions);}DBCursor cursor = collection.find(q);StringBuffer words = new StringBuffer();while (cursor.hasNext()) {try {DBObject result = cursor.next();ParagraphsAnalyzer pa = new ParagraphsAnalyzer(result);List<LinkedHashMap<String, Object>> all = pa.analyze();if (!all.isEmpty()) {for (LinkedHashMap<String, Object> m : all) {newCollection.insert(new BasicDBObject(m));LOG.info("Insert: " + m.get("articleId") + ", " + m.get("word") + ", " + m.get("fragmentSize") + "/" + m.get("paragraphCount") + ", " + m.get("selected"));}}words.delete(0, words.length());} catch (Exception e) {e.printStackTrace();}}cursor.close();}public void setWordsPath(String wordsPath) {this.wordsPath = wordsPath;}public void setDictPath(String dictPath) {this.dictPath = dictPath;}public void setGlobalMinParagraphLength(int globalMinParagraphLength) {this.globalMinParagraphLength = globalMinParagraphLength;}public void setGlobalMaxParagraphGap(int globalMaxParagraphGap) {this.globalMaxParagraphGap = globalMaxParagraphGap;}public void setGlobalMinFragmentParagraphsCount(int globalMinFragmentParagraphsCount) {this.globalMinFragmentParagraphsCount = globalMinFragmentParagraphsCount;}public void setGlobalMinFragmentSize(int globalMinFragmentSize) {this.globalMinFragmentSize = globalMinFragmentSize;}public void setGlobalCoverage(double globalCoverage) {this.globalCoverage = globalCoverage;}class ParagraphsAnalyzer {/** 一条记录 */private DBObject result;// 下面集合中涉及到Term的,是基于给定的数据(如手工整理的景点或目的地)过滤得到的/** LinkedHashMap<文章段落序号, 文章段落内容> */private LinkedHashMap<Integer, String> paragraphMap = new LinkedHashMap<Integer, String>();/** LinkedHashMap<文章段落序号, 段落分词后Term组> */private LinkedHashMap<Integer, Set<String>> paragraphTermMap = new LinkedHashMap<Integer, Set<String>>();/** 文章全部段落分词后得到的Term及其TF集合 */private Map<String, IntCounter> allTermsCountMap = new HashMap<String, IntCounter>();/** Map<文章段落编号, 段落长度> */private Map<Integer, Integer> paragraphLenMap = new HashMap<Integer, Integer>();// 一些限制参数配置,考虑局部可以动态调整,如果外部进行了全局设置,可以根据局部来动态调整/** 最小段落内容长度限制 */private int minParagraphLength = 3;/** 最大段落间隔数限制:必须大于2 */private int maxParagraphGap = 2;/** 截取片段中的段落数,必须大于1 */private int minFragmentParagraphsCount = 3;/** 截取最大片段数限制,必须大于1 */private int minFragmentSize = 5;/** 一个word在一篇文章的片段集合,在整篇文章中的覆盖率:如果大于该值,则选择整篇文章作为一个片段,否则分别取出多个片段 */private double coverage = 0.75;/** 文章内容长度 */private int contentLength = 0;/** 计划平均片段长度,用于分割整篇文章 */private int planningFragmentAverageLength = 600;public ParagraphsAnalyzer(DBObject result) {super();this.result = result;this.minParagraphLength = globalMinParagraphLength;this.maxParagraphGap = globalMaxParagraphGap;this.minFragmentParagraphsCount = globalMinFragmentParagraphsCount;this.minFragmentSize = globalMinFragmentSize;this.coverage = globalCoverage;beforeAnalysis();}private void beforeAnalysis() {// TODO// 这里可做一些预分析工作:根据一篇文章的整体段落情况,确定全局适应的参数,如minParagraphLength、maxParagraphGap// 文章段落的连贯性可以考虑(maxParagraphGap),如果两个段落之间文字数量小于设定的阙值则认为是连贯的,可以直接作为一个相关的片段抽取出来// TODO// 可以根据预分析结果,适当调整局部minParagraphLength、maxParagraphGap、minFragmentSize、coverage}public List<LinkedHashMap<String, Object>> analyze() {String content = (String) result.get("content");String[] paragraphs = content.split("\n+");int paragraphIdCounter = 0;for (int i = 0; i < paragraphs.length; i++) {if (!paragraphs[i].isEmpty()) {int len = paragraphs[i].trim().length();contentLength += len;paragraphLenMap.put(paragraphIdCounter, len);paragraphMap.put(paragraphIdCounter, paragraphs[i].trim());if (paragraphs[i].trim().length() > minParagraphLength) {analyzeParagraph(paragraphIdCounter, paragraphs[i].trim());}paragraphIdCounter++;}}// 每一个段落分别处理完成后,综合个段落数据信息,进行综合分析// 此时,可用的数据集合如下:// 1. paragraphMap 整篇文章各个段落:Map<文章段落序号, 文章段落内容>// 2. allTermsCountMap 文章全部段落分词后得到的Term及其TF集合// 3. paragraphTermMap 整篇文章各个段落:Map<文章段落序号, 段落分词后Term组>// 4. paragraphLenMap 整篇文章各个段落:Map<文章段落序号, 段落长度>Map<String, Object> raw = compute(paragraphs.length);// 处理抽取出来的片段,组织好后存储到数据库List<LinkedHashMap<String, Object>> forStore = new ArrayList<LinkedHashMap<String, Object>>();List<Fragment> fragmentList = (List<Fragment>) raw.remove("fragment");if (fragmentList != null) {for (Fragment frag : fragmentList) {LinkedHashMap<String, Object> record = new LinkedHashMap<String, Object>();record.put("articleId", result.get("_id").toString());record.put("title", result.get("title"));record.put("url", result.get("url"));record.put("spiderName", result.get("spiderName"));record.put("publishDate", result.get("publishDate"));record.put("word", frag.word);StringBuffer selectedIdBuffer = new StringBuffer();for (int paragraphId : frag.paragraphIdList) {selectedIdBuffer.append(paragraphId).append(" ");}record.put("paragraphCount", raw.get("paragraphCount"));record.put("fragmentSize", frag.end - frag.start + 1);record.put("selectedCount", frag.paragraphIdList.size());record.put("selected", selectedIdBuffer.toString().trim());// 将连续的段落内容拼接在一起LinkedHashMap<Integer, String> selected = new LinkedHashMap<Integer, String>();int start = frag.start;int end = frag.end;for (int i = start; i <= end; i++) {selected.put(i, paragraphMap.get(i));}record.put("fragment", selected);record.put("paragraphs", raw.get("paragraphs"));// 最终需要存储的记录集合forStore.add(record);}}return forStore;}/** * 分析文章的一个段落:为分析整篇文章,准备各个段落相关的元数据 *  * @param paragraphId * @param paragraph */private void analyzeParagraph(int paragraphId, String paragraph) {// 一个段落中分词后得到的Term的TF统计,暂时没有用到Map<String, IntCounter> counterMap = new HashMap<String, IntCounter>();Set<String> set = new HashSet<String>();// 对一个段落的文本内容进行分词处理Reader reader = new StringReader(paragraph.trim());TokenStream ts = analyzer.tokenStream("", reader);ts.addAttribute(CharTermAttribute.class);try {while (ts.incrementToken()) {CharTermAttributeImpl attr = (CharTermAttributeImpl) ts.getAttribute(CharTermAttribute.class);String word = attr.toString().trim();if (word.length() > 1 && wordSet.contains(word)) {if (counterMap.containsKey(word)) {++counterMap.get(word).value;} else {counterMap.put(word, new IntCounter(1));}set.add(word);// 加入到全局Term集合allTermsCountMap中if (allTermsCountMap.containsKey(word)) {++allTermsCountMap.get(word).value;} else {allTermsCountMap.put(word, new IntCounter(1));}}}paragraphTermMap.put(paragraphId, set);} catch (IOException e) {e.printStackTrace();}}private Optimizer optimizer;private Map<String, Object> compute(int paragraphCount) {// 返回一篇文章中,每个word对应段落的集合Map<String, Object> result = new HashMap<String, Object>();result.put("paragraphs", paragraphMap);result.put("paragraphCount", paragraphCount);// 这个列表中的每个Fragment对应着最终需要存储的记录List<Fragment> fragmentList = new ArrayList<Fragment>();// 如果需要排序,可以在这里按照allTermsCountMap的Term的TF降序排序Iterator<Entry<String, IntCounter>> iter = allTermsCountMap.entrySet().iterator();while (iter.hasNext()) {Map.Entry<String, IntCounter> entry = iter.next();// 对每个Term,遍历整篇文章,对Term的分布进行分析String word = entry.getKey();IntCounter freq = entry.getValue();// 如果需要,可以进行剪枝,降低无用的计算(如根据TF,TF在一篇文章太低,可以直接过滤掉)// 这里剪枝条件设置为TF>1if (freq.value > 1) {// 获取到一个word在一篇文章哪些段落中出现过List<Integer> paragraphIdList = choose(word, paragraphCount);// 提取一个word相关的段落集合,应该考虑如下因素:// 1. 一篇文章的段落总数// 2. 一个word出现的各个段落之间间隔数// 3. 获取多少个段落能够以word为中心List<Integer> list = new ArrayList<Integer>();List<Fragment> temp = new ArrayList<Fragment>();if (paragraphIdList.size() >= Math.max(2, minFragmentParagraphsCount)) {// 包含段落太少的文章,直接过滤掉// 根据包含word的段落序号列表,计算一篇文章中有多个片段的集合int previous = paragraphIdList.get(0);list.add(previous);for (int i = 1; i < paragraphIdList.size(); i++) {int current = paragraphIdList.get(i);if (current - previous <= maxParagraphGap) {list.add(current);} else {makeFragment(word, list, temp);list = new ArrayList<Integer>();list.add(current);}previous = current;}if (!list.isEmpty()) {makeFragment(word, list, temp);}}// 一个关键词,在一篇文章中,根据分析后的结果选择抽取的片段集合if (!temp.isEmpty()) {// 这里对多个片段Fragment的集合进行综合分析、优化处理optimizer = new CoverageOptimizer();temp = optimizer.optimize(word, temp, paragraphCount);if(temp!=null && !temp.isEmpty()) {fragmentList.addAll(temp);}}}}// 返回抽取到的片段集合// 在存储到数据库之前,先要对其进行拆分、组合、优化result.put("fragment", fragmentList);return result;}private void makeFragment(String word, List<Integer> list, List<Fragment> temp) {Fragment frag = new Fragment(word, list);frag.start = list.get(0);frag.end = list.get(list.size() - 1);temp.add(frag);}/** * 处理一个Term在整篇文章中的分布情况 *  * @param word * @param paragraphCount * @return */private List<Integer> choose(String word, int paragraphCount) {// 遍历每一个段落的Term集合paragraphTermMap,获取包含该word的段落序号集合List<Integer> paragraphIdList = new ArrayList<Integer>();for (Entry<Integer, Set<String>> entry : paragraphTermMap.entrySet()) {if (entry.getValue().contains(word)) {paragraphIdList.add(entry.getKey());}}return paragraphIdList;}class CoverageOptimizer implements Optimizer {@Overridepublic List<Fragment> optimize(String word, List<Fragment> temp, int paragraphCount) {// 考虑一个word在文章中截取的片段,在整篇文章中的覆盖情况List<Fragment> selectedFragmentList = null;double sum = 0.0;for (Fragment f : temp) {sum += (f.end - f.start + 1); // 累加片段的长度}double rate = sum / (double) paragraphCount;// 满足覆盖条件// TODO 可以基于覆盖条件,再考虑抽取段落在整篇文章中的分布,如果分布均匀,酌情降低覆盖率以选取整篇文章为一个段落if (rate >= coverage) {selectedFragmentList = updateFragment(word, temp, paragraphCount);} else {// 不满足覆盖率条件,看一下全部片段的段落集合在整篇文章中的分布情况// 考虑分布:将整篇文章分成几个连续片段的集合,看我们计算关键词的得到的片段集合,是否与连续片段集合发生重叠Fragment first = temp.get(0);Fragment last = temp.get(temp.size()-1);// 这个条件有点太弱,如果一篇文章中段落很多,开始部分一段,结尾部分一段,结果就会取整篇文章// TODO 所以还要考虑覆盖率,降低覆盖率的条件,但是提高文章边界价差限制,覆盖率初步定为文章段落数量的一半if(rate >= 0.5 && first.paragraphIdList.get(0) - 0 < maxParagraphGap && paragraphCount - last.paragraphIdList.get(last.paragraphIdList.size() - 1) < maxParagraphGap) {return updateFragment(word, temp, paragraphCount);}// 用于优化:如果段落很少,很有可能这篇文章出现关键词段落数覆盖率很低,但是这篇文章确实是讲该关键词表达的主题// TODO 是否可以考虑做一个分段函数y = f(x, y, z), x:片段数 y:文章段落数 z:文章文本总长度if(temp.size()==1 && paragraphCount<10) {return updateFragment(word, temp, paragraphCount);}// TODO 如果一个片段,片段起始段落距离文章首段距离大于maxParagraphGap,可以考虑计算一下这段间隔的文字数量来进行优化// 文章末尾段落也可以类似考虑,暂时不做// do something// 需要对各个片段进行筛选for (Iterator<Fragment> iter = temp.iterator(); iter.hasNext();) {// 片段包含的段落太少if (iter.next().paragraphIdList.size() < minFragmentSize) {iter.remove();}}return temp;}// TODO 可以考虑文章段落中出现的多组图片,导致文章段落衔接隔断问题return selectedFragmentList;}private List<Fragment> updateFragment(String word, List<Fragment> temp, int paragraphCount) {List<Fragment> selectedFragmentList = new ArrayList<Fragment>();List<Integer> list = new ArrayList<Integer>();for (Fragment f : temp) {list.addAll(f.paragraphIdList);}Fragment f = new Fragment(word, list);f.start = 0;f.end = paragraphCount-1;selectedFragmentList.add(f);return selectedFragmentList;}}class DistributionOptimizer implements Optimizer {@Overridepublic List<Fragment> optimize(String word, List<Fragment> temp, int paragraphCount) {List<Fragment> selectedFragmentList = new ArrayList<Fragment>();// write your logicreturn selectedFragmentList;}}}/** * 片段选择优化器 *  * @author shirdrn * @date 2011-12-19 */interface Optimizer {public List<Fragment> optimize(String word, List<Fragment> temp, int paragraphCount);}/** * 计数器:为了对Map中数据对象计数方便 *  * @author shirdrn * @date 2011-12-15 */class IntCounter {Integer value;public IntCounter() {this(0);}public IntCounter(Integer value) {super();this.value = value;}@Overridepublic String toString() {return value == null ? "0" : value.toString();}}/** * 文章的一个片段:由一个或多个段落组成 *  * @author shirdrn * @date 2011-12-15 */class Fragment {/** 关键词 */String word;/** 经过分析后,最终确定的片段截取起始段落序号 */int start;/** 经过分析后,最终确定的片段截取终止段落序号 */int end;/** 出现word的段落的序号列表 */List<Integer> paragraphIdList;public Fragment() {super();}public Fragment(String word, List<Integer> paragraphIdList) {super();this.word = word;this.paragraphIdList = paragraphIdList;}@Overridepublic String toString() {return "[word=" + word + ", start=" + start + ", end=" + end + ", paragraphIdList=" + paragraphIdList + "]";}}}


上述代码中,相关参数的默认设定,如下所示:
  • minParagraphLength=3, 最小段落长度限制,为保证文章连贯性,满足小于该值的可有可无(可以保留)
  • maxParagraphGap=2,最大段落间隔限制
  • minFragmentSize=5,最小片段长度限制,如果某个关键词在一篇文章中抽取的片段中段落时小于该值,被过滤掉
  • minFragmentParagraphsCount=5,做小片段包含段落数限制
  • word长度限制,大于1
  • coverage=0.75,基于某个词word抽取的片段集合在整篇文章中的覆盖率
你可以根据需要进行扩展和修改,选择设置最适合你的参数值。
测试用例,代码如下所示:
package org.shirdrn.ie.travel;import java.util.HashMap;import java.util.Iterator;import java.util.Map;import junit.framework.TestCase;import org.shirdrn.common.DBConfig;public class TestArticleAnalyzeTool extends TestCase {String indexPath;ArticleAnalyzeTool tool;DBConfig dictLoaderConfig;DBConfig articleConfig;@Overrideprotected void setUp() throws Exception {articleConfig = new DBConfig("192.168.0.184", 27017, "page", "Article");tool = new ArticleAnalyzeTool(articleConfig);}public void testArticlesAnalyzeTool() {if(true) {tool.setWordsPath("E:\\words-attractions.dic");tool.setDictPath("E:\\Develop\\eclipse-jee-helios-win32\\workspace\\EasyTool\\dict");tool.initialize();tool.setGlobalMaxParagraphGap(5);tool.setGlobalMinFragmentParagraphsCount(3);tool.setGlobalMinParagraphLength(3);tool.setGlobalMinFragmentSize(3);tool.setGlobalCoverage(0.65);Map<String, Object> spiderToCollection = new HashMap<String, Object>();spiderToCollection.put("mafengwoSpider", "mafengwo");spiderToCollection.put("go2euSpider", "go2eu");spiderToCollection.put("daodaoSpider", "daodao");spiderToCollection.put("lotourSpider", "lotour");spiderToCollection.put("17uSpider", "17u");spiderToCollection.put("lvpingSpider", "lvping");spiderToCollection.put("sinaSpider", "sina");spiderToCollection.put("sohuSpider", "sohu");for (Iterator<Map.Entry<String, Object>> iterator = spiderToCollection.entrySet().iterator(); iterator.hasNext();) {Map.Entry<String, Object> entry = iterator.next();Map<String, Object> conditions = new HashMap<String, Object>();conditions.put("spiderName", entry.getKey());DBConfig config = new DBConfig("192.168.0.184", 27017, "fragment", (String) entry.getValue());tool.runWork(conditions, config);}}}}


上述测试用例中,主题词词典可以根据你的需要选择,我的都存储到了E:\\words-attractions.dic中,而E:\\Develop\\eclipse-jee-helios-win32\\workspace\\EasyTool\\dict是Lucene用于分词的词典。
测试代码执行,结果示例如下所示:
{   "_id": ObjectId("4eeb3ae7ca251f31c5b10d79"),   "articleId": "4ed31f1df776481ed002e71d",   "title": "2010杭州九溪行",   "url": "http: \/\/www.mafengwo.cn\/i\/623758.html",   "spiderName": "mafengwoSpider",   "publishDate": "2010-05-15 13: 19: 09",   "word": "西湖",   "paragraphCount": 14,   "fragmentSize": 7,   "selectedCount": 4,   "selected": "7 9 10 13",   "fragment": {     "7": "       过了九溪烟树,向着龙井村进发。一路上看到的全是茶园。西湖龙井也是有品牌的,这里都是六和塔牌的。(PS: 我怎么总想起马季那个宇宙牌香烟呢。。。) [...]",     "8": "<img src ='http: \/\/file2.mafengwo.net\/M00\/3D\/F0\/wKgBm04VGUDpGZVtAANCFqD0HjM39.groupinfo.w600.jpeg' title ='杭州旅游攻略图片' alt ='杭州旅游攻略图片' width ='600' [...]",     "9": "       和一位茶农阿婆一路聊着天就走到了龙井村。都说杭州新西湖十景里有个“龙井问茶”。那既然来到了龙井村,就不能白白错过啊。。。 [...]",     "10": "       出了龙井村,如果脚力好的话,建议可以徒步下山。林间的公路在夕阳的印衬下更显静谧的味道。一路上会穿过另一个新西湖十景——“满陇桂雨”。如果是秋天的话,一路上都可以闻到桂花的香味。很是舒服啊~~ [...]",     "11": "<img src ='http: \/\/file2.mafengwo.net\/M00\/3D\/F1\/wKgBm04VGUDw9xs8AAMnNG6MHkM33.groupinfo.w600.jpeg' title ='杭州图片' alt ='杭州图片' width ='600' height = [...]",     "12": "       等到我一步一个赏味的从满陇桂雨出来,我这天的九溪行也就可以画上句号了。",     "13": "       提起杭州,很多人反映出的只有西湖,其实在西湖的周边还有很多好玩的地方。比如九溪。如果时间允许的话,不妨到周边的景点走走看看吧。杭州,真的不是只有西湖哦~~ [...]"   },   "paragraphs": {     "0": "说好的一起去杭州聚会,结果被先后放了鸽子。5个人的聚会,在4月28日的晚上瞬间变成我独独的一个人。纠结了一个晚上要不要取消这次旅行,还是没想好。29号早上一个朋友告诉让我兴奋的消息——她陪我去杭州。然而好景不长,还没等我从兴奋的喜悦中出来,她告诉我5月1日去杭州的票全部卖光,连站票都没了。。。好吧, [...]",     "1": "      继续纠结了一个晚上,左思右想去还是不去。可是可是,这次旅行是很早之前自己承诺给自己的。别人放了我鸽子,我总不能自己对自己也不守信用吧?往返车票已经买好,酒店也已经订好,我还犹豫什么呢?不就是一个人上路么。其实一个人也有一个人的好,可以遵从自己的心,想走到哪里就走到哪里,累了可以休息,休息 [...]",     "2": "       我终于来到了向往已久的杭州九溪。乘坐Y5路车到九溪站下车。话说玩九溪有两种走法,一种是从下往上走(例如我),另一种是从上往下走(坐Y3路车到杨梅岭下车)。从实际情况来看我更倾向第一种。九溪又叫九溪十八涧,听名字就知道是个有山有水的地方,一路上左边是茶园右边是溪水,好不惬意。有些小朋友干 [...]",     "3": "<img src ='http: \/\/file4.mafengwo.net\/M00\/3D\/EF\/wKgBm04VGT-le47JAAIdz-AMEpU09.groupinfo.w600.jpeg' title ='杭州自助游图片' alt ='杭州自助游图片' width ='600' he [...]",     "4": "更有些小情侣互相依偎着坐在溪水边谈情蜜意。这么个好地方,夏天来估计更舒服。呵呵~~",     "5": "       再往前走就到了九溪的标志景点——九溪烟树。果然景如其名,这树像画儿似的非常漂亮。",     "6": "<img src ='http: \/\/file4.mafengwo.net\/M00\/3D\/EF\/wKgBm04VGUC7WuR6AAM8XvRaDaQ82.groupinfo.w600.jpeg' title ='杭州景点图片' alt ='杭州景点图片' width ='600' heig [...]",     "7": "       过了九溪烟树,向着龙井村进发。一路上看到的全是茶园。西湖龙井也是有品牌的,这里都是六和塔牌的。(PS: 我怎么总想起马季那个宇宙牌香烟呢。。。) [...]",     "8": "<img src ='http: \/\/file2.mafengwo.net\/M00\/3D\/F0\/wKgBm04VGUDpGZVtAANCFqD0HjM39.groupinfo.w600.jpeg' title ='杭州旅游攻略图片' alt ='杭州旅游攻略图片' width ='600' [...]",     "9": "       和一位茶农阿婆一路聊着天就走到了龙井村。都说杭州新西湖十景里有个“龙井问茶”。那既然来到了龙井村,就不能白白错过啊。。。 [...]",     "10": "       出了龙井村,如果脚力好的话,建议可以徒步下山。林间的公路在夕阳的印衬下更显静谧的味道。一路上会穿过另一个新西湖十景——“满陇桂雨”。如果是秋天的话,一路上都可以闻到桂花的香味。很是舒服啊~~ [...]",     "11": "<img src ='http: \/\/file2.mafengwo.net\/M00\/3D\/F1\/wKgBm04VGUDw9xs8AAMnNG6MHkM33.groupinfo.w600.jpeg' title ='杭州图片' alt ='杭州图片' width ='600' height = [...]",     "12": "       等到我一步一个赏味的从满陇桂雨出来,我这天的九溪行也就可以画上句号了。",     "13": "       提起杭州,很多人反映出的只有西湖,其实在西湖的周边还有很多好玩的地方。比如九溪。如果时间允许的话,不妨到周边的景点走走看看吧。杭州,真的不是只有西湖哦~~ [...]"   } }
上述结果中,个字段的含义如下:
  • word是出现在词典中,并且在文章中出现的关键词
  • paragraphCount是组成一篇文章的段落数
  • selected是文章中抽取出来包含关键词word的段落的段落序号列表
  • selectedCount是selected中段落序号的个数
  • fragmentSize是selected字段中指示的段落号“起始~终止”之间一共包含的段落数,例如selected=3 5 8 12,则fragmentSize=12-3+1=10
  • fragment中是抽取出来的片段内容,word是片段的主题内容,这正是我们实际想要的内容
  • paragraphs是包含段落序号的文章的段落集合
根据上述测试结果,抽取的段落还是可以说明给定关键词表达的主题的。不过,抽取的主主题还是依赖于你所选定的关键词词典,以及你所使用的选择片段段落的标准。我们在上面,没有真正考虑一个关键词,在一篇文章中的分布情况,而只是简单地根据出现某个关键词的段落数与文章全部段落数的一个比值来进行选择,而实际考虑选择的片段中的一组段落在文章中的分布情况,更能确定一篇文章的主题(如果你的需求是确定文章讲的某一个主题)。