本文介绍如何在无监督的情况下,对文本进行简单的观点提取和聚类。
观点提取是通过依存关系的方式,根据固定的依存结构,从原文本中提取重要的结构,代表整句的主要意思。
我认为比较重要的依存关系结构是"动补结构", "动宾关系", "介宾关系"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通过这个方法,我们拿到了每个句子对应的观点了。下面对所有观点进行聚类。
| 原句 | 观点 | 
|---|---|
| 这个手机是正品吗? | 手机是正品 | 
| 礼品是一些什么东西? | 礼品是什么东西 | 
| 现在都送什么礼品啊 | 都送什么礼品 | 
| 直接付款是怎么付的啊 | 付款是怎么付 | 
| 如果不满意也可以退货的吧 | 不满意可以退货 | 
观点聚类的方法有几种:
我的方法是用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)对聚类在一起的观点,提取一个比较好的代表整个聚类的观点。
我的方法是对聚类观点里面的所有观点进行字的频率统计,对高频的字组成的字符串去和所有观点计算相似度,相似度最高的那个当做整个观点聚类的总的观点。
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| 聚类总结 | 所有观点 | 
|---|---|
| 手机是全新正品 | 手机是全新正品 手机是全新 手机是不是正品 保证是全新手机 | 
| 能送无线充电器 | 能送无线充电器 人家送无线充电器 送无线充电器 买能送无线充电器 | 
| 可以优惠多少 | 可以优惠多少 你好可优惠多少 能优惠多少 可以优惠多少 | 
| 是不是翻新机 | 是不是翻新机 不会是翻新机 手机是还是翻新 会不会是翻新机 | 
| 花呗可以分期 | 花呗不够可以分期 花呗分期可以 可以花呗分期 花呗可以分期 | 
| 没有给发票 | 我没有发票 发票有开给我 没有给发票 你们有给发票 | 
以上我本人做的一些简单的观点提取和聚类,可以适用一些简单的场景中。
原文:https://www.cnblogs.com/huangyc/p/10279254.html