2. 观点提取和聚类代码详解

1. pyhanlp介绍和简单应用

2. 观点提取和聚类代码详解

1. 前言

本文介绍如何在无监督的情况下,对文本进行简单的观点提取和聚类。

2. 观点提取

观点提取是通过依存关系的方式,根据固定的依存结构,从原文本中提取重要的结构,代表整句的主要意思。

我认为比较重要的依存关系结构是"动补结构", "动宾关系", "介宾关系"3个关系。不重要的结构是"定中关系", "状中结构", "主谓关系"。通过核心词ROOT出发,来提取观点。

观点提取的主要方法如下,完整代码请移步致github

''' 
关键词观点提取,根据关键词key,找到关键处的rootpath,寻找这个root中的观点,观点提取方式和parseSentence的基本一样。
支持提取多个root的观点。
'''
def parseSentWithKey(self, sentence, key=None):
    #key是关键字,如果关键字存在,则只分析存在关键词key的句子,如果没有key,则不判断。
    if key:
        keyIndex = 0
        if key not in sentence:
            return []
    rootList = []
    parse_result = str(self.hanlp.parseDependency(sentence)).strip().split('\n')
    # 索引-1,改正确,因为从pyhanlp出来的索引是从1开始的。
    for i in range(len(parse_result)):
        parse_result[i] = parse_result[i].split('\t')
        parse_result[i][0] = int(parse_result[i][0]) - 1
        parse_result[i][6] = int(parse_result[i][6]) - 1
        if key and parse_result[i][1] == key:
            keyIndex = i

    for i in range(len(parse_result)):
        self_index = int(parse_result[i][0])
        target_index = int(parse_result[i][6])
        relation = parse_result[i][7]
        if relation in self.main_relation:
            if self_index not in rootList:
                rootList.append(self_index)
        # 寻找多个root,和root是并列关系的也是root
        elif relation == "并列关系" and target_index in rootList:
            if self_index not in rootList:
                rootList.append(self_index)


        if len(parse_result[target_index]) == 10:
            parse_result[target_index].append([])

        #对依存关系,再加一个第11项,第11项是一个当前这个依存关系指向的其他索引
        if target_index != -1 and not (relation == "并列关系" and target_index in rootList):
            parse_result[target_index][10].append(self_index)
    
    # 寻找key在的那一条root路径
    if key:
        rootIndex = 0
        if len(rootList) > 1:
            target = keyIndex
            while True:
                if target in rootList:
                    rootIndex = rootList.index(target)
                    break
                next_item = parse_result[target]
                target = int(next_item[6])
        loopRoot = [rootList[rootIndex]]
    else:
        loopRoot = rootList

    result = {}
    related_words = set()
    for root in loopRoot:
        # 把key和root加入到result中
        if key:
            self.addToResult(parse_result, keyIndex, result, related_words)
        self.addToResult(parse_result, root, result, related_words)

    #根据'动补结构', '动宾关系', '介宾关系',选择观点
    for item in parse_result:
        relation = item[7]
        target = int(item[6])
        index = int(item[0])
        if relation in self.reverse_relation and target in result and target not in related_words:
            self.addToResult(parse_result, index, result, related_words)

    # 加入关键词
    for item in parse_result:
        word = item[1]
        if word == key:
            result[int(item[0])] = word

    #对已经在result中的词,按照在句子中原来的顺序排列
    sorted_keys = sorted(result.items(), key=operator.itemgetter(0))
    selected_words = [w[1] for w in sorted_keys]
    return selected_words

通过这个方法,我们拿到了每个句子对应的观点了。下面对所有观点进行聚类。

2.1 观点提取效果

原句 观点
这个手机是正品吗? 手机是正品
礼品是一些什么东西? 礼品是什么东西
现在都送什么礼品啊 都送什么礼品
直接付款是怎么付的啊 付款是怎么付
如果不满意也可以退货的吧 不满意可以退货

3. 观点聚类

观点聚类的方法有几种:

  1. 直接计算2个观点的聚类。(我使用的方法)
  2. 把观点转化为向量,比较余弦距离。

我的方法是用difflib对任意两个观点进行聚类。我的时间复杂度很高\(O(n^2)\),用一个小技巧优化了下。代码如下:

def extractor(self):
    de = DependencyExtraction()
    opinionList = OpinionCluster()
    for sent in self.sentences:
        keyword = ""
        if not self.keyword:
            keyword = ""
        else:
            checkSent = []
            for word in self.keyword:
                if sent not in checkSent and word in sent:
                    keyword = word
                    checkSent.append(sent)
                    break

        opinion = "".join(de.parseSentWithKey(sent, keyword))
        if self.filterOpinion(opinion):
            opinionList.addOpinion(Opinion(sent, opinion, keyword))


    '''
        这里设置两个阈值,先用小阈值把一个大数据切成小块,由于是小阈值,所以本身是一类的基本也能分到一类里面。
        由于分成了许多小块,再对每个小块做聚类,聚类速度大大提升,thresholds=[0.2, 0.6]比thresholds=[0.6]速度高30倍左右。
        但是[0.2, 0.6]和[0.6]最后的结果不是一样的,会把一些相同的观点拆开。
    '''
    thresholds = self.json_config["thresholds"]
    clusters = [opinionList]
    for threshold in thresholds:
        newClusters = []
        for cluster in clusters:
            newClusters += self.clusterOpinion(cluster, threshold)
        clusters = newClusters

    resMaxLen = {}
    for oc in clusters:
        if len(oc.getOpinions()) >= self.json_config["minClusterLen"]:
            summaryStr = oc.getSummary(self.json_config["freqStrLen"])
            resMaxLen[summaryStr] = oc.getSentences()

    return self.sortRes(resMaxLen)

3.1 观点总结

对聚类在一起的观点,提取一个比较好的代表整个聚类的观点。

我的方法是对聚类观点里面的所有观点进行字的频率统计,对高频的字组成的字符串去和所有观点计算相似度,相似度最高的那个当做整个观点聚类的总的观点。

def getSummary(self, freqStrLen):
    opinionStrs = []
    for op in self._opinions:
        opinion = op.opinion
        opinionStrs.append(opinion)

    # 统计字频率
    word_counter = collections.Counter(list("".join(opinionStrs))).most_common()

    freqStr = ""
    for item in word_counter:
        if item[1] >= freqStrLen:
            freqStr += item[0]

    maxSim = -1
    maxOpinion = ""
    for opinion in opinionStrs:
        sim = similarity(freqStr, opinion)
        if sim > maxSim:
            maxSim = sim
            maxOpinion = opinion

    return maxOpinion

3.2 观点总结效果

聚类总结 所有观点
手机是全新正品 手机是全新正品 手机是全新 手机是不是正品 保证是全新手机
能送无线充电器 能送无线充电器 人家送无线充电器 送无线充电器 买能送无线充电器
可以优惠多少 可以优惠多少 你好可优惠多少 能优惠多少 可以优惠多少
是不是翻新机 是不是翻新机 不会是翻新机 手机是还是翻新 会不会是翻新机
花呗可以分期 花呗不够可以分期 花呗分期可以 可以花呗分期 花呗可以分期
没有给发票 我没有发票 发票有开给我 没有给发票 你们有给发票

4. 总结

以上我本人做的一些简单的观点提取和聚类,可以适用一些简单的场景中。

posted @ 2019-01-16 20:35  hyc339408769  阅读(5510)  评论(1编辑  收藏  举报