如何在 python 中对维基百科类别进行分组?

How to group wikipedia categories in python?

对于我的数据集的每个概念,我都存储了相应的维基百科类别。例如,考虑以下 5 个概念及其对应的维基百科类别。

如您所见,前三个概念属于医学领域(而其余两个术语不是医学术语)。

更准确地说,我想把我的概念分为医学和非医学。但是,仅使用类别来划分概念是非常困难的。例如,尽管 enzyme inhibitorbypass surgery 这两个概念属于医学领域,但它们的类别却大不相同。

所以想知道有没有办法获取类别的parent category(比如enzyme inhibitorbypass surgery的类别就属于medical 父类别)

我目前正在使用 pymediawikipywikibot。但是,我不仅限于这两个库,而且很高兴也有使用其他库的解决方案。

编辑

根据@IlmariKaronen 的建议,我也在使用 categories of categories,我得到的结果如下(Tcategory 附近的小字体是 categories of the category)。

但是,我仍然找不到使用这些类别详细信息来确定给定术语是医学术语还是非医学术语的方法。

此外,正如@IlmariKaronen 所指出的,使用 Wikiproject 细节可能是有潜力的。然而,Medicine wikiproject 似乎没有所有的医学术语。因此我们也需要检查其他维基项目。

编辑: 我目前从维基百科概念中提取类别的代码如下。这可以使用 pywikibotpymediawiki 来完成,如下所示。

  1. 使用图书馆pymediawiki

    将 mediawiki 导入为 pw

    p = wikipedia.page('enzyme inhibitor')
    print(p.categories)
    
  2. 使用库pywikibot

    import pywikibot as pw
    
    site = pw.Site('en', 'wikipedia')
    
    print([
        cat.title()
        for cat in pw.Page(site, 'support-vector machine').categories()
        if 'hidden' not in cat.categoryinfo
    ])
    

类别的分类也可以按照@IlmariKaronen 的回答中所示的相同方式完成。

如果您正在寻找更长的测试概念列表,我在下面提到了更多示例。

['juvenile chronic arthritis', 'climate', 'alexidine', 'mouthrinse', 'sialosis', 'australia', 'artificial neural network', 'ricinoleic acid', 'bromosulfophthalein', 'myelosclerosis', 'hydrochloride salt', 'cycasin', 'aldosterone antagonist', 'fungal growth', 'describe', 'liver resection', 'coffee table', 'natural language processing', 'infratemporal fossa', 'social withdrawal', 'information retrieval', 'monday', 'menthol', 'overturn', 'prevailing', 'spline function', 'acinic cell carcinoma', 'furth', 'hepatic protein', 'blistering', 'prefixation', 'january', 'cardiopulmonary receptor', 'extracorporeal membrane oxygenation', 'clinodactyly', 'melancholic', 'chlorpromazine hydrochloride', 'level of evidence', 'washington state', 'cat', 'newyork', 'year elevan', 'trituration', 'gold alloy', 'hexoprenaline', 'second molar', 'novice', 'oxygen radical', 'subscription', 'ordinate', 'approximal', 'spongiosis', 'ribothymidine', 'body of evidence', 'vpb', 'porins', 'musculocutaneous']

对于很长的列表,请查看下面的 link。 https://docs.google.com/document/d/1BYllMyDlw-Rb4uMh89VjLml2Bl9Y7oUlopM-Z4F6pN0/edit?usp=sharing

注意:我并不期望该解决方案 100% 有效(如果所提出的算法能够检测出许多对我来说足够的医学概念)

如果需要,我很乐意提供更多详细信息。

"Therefore, I would like to know if there is a way to obtain the parent category of the categories (for example, the categories of enzyme inhibitor and bypass surgery belong to medical parent category)"

MediaWiki 类别本身就是 wiki 页面。 "parent category" 只是 "child" 类别页面所属的类别。因此,您可以使用与获取任何其他 wiki 页面的类别完全相同的方式获取类别的父类别。

例如,使用pymediawiki:

p = wikipedia.page('Category:Enzyme inhibitors')
parents = p.categories

这个问题对我来说似乎有点不清楚,而且似乎不是一个可以直接解决的问题,可能需要一些 NLP 模型。此外,概念和类别这两个词可以互换使用。我的理解是,酶抑制剂、旁路手术和高甘油三酯血症等概念需要结合在一起作为医学,其余的作为非医学。这个问题需要的数据不仅仅是类别名称。需要一个语料库来训练 LDA 模型(例如),其中整个文本信息被提供给算法并且它 returns 每个概念最可能的主题。

https://www.analyticsvidhya.com/blog/2018/10/stepwise-guide-topic-modeling-latent-semantic-analysis/

NLP 中有词向量的概念,它的基本作用是通过查看大量文本,尝试将词转换为 multi-dimensional 向量,然后缩小这些向量之间的距离,增大它们之间的相似性,好消息是许多人已经生成了这个词向量并在非常宽松的许可下提供它们,并且在你的情况下你正在使用维基百科并且这里存在它们的词向量 http://dumps.wikimedia.org/enwiki/latest/enwiki-latest-pages-articles.xml.bz2

现在这些最适合这项任务,因为它们包含维基百科语料库中的大部分单词,但如果它们不适合您,或者将来会被删除,您可以使用我将在下面列出的更多其中,话虽如此,有一种更好的方法可以做到这一点,即将它们传递给 tensorflow 的通用语言模型 embed 模块,您不必在其中完成大部分繁重的工作,您可以阅读更多关于那个 here. The reason I put it after the Wikipedia text dump is because I have heard people say that they are a bit hard to work with when working with medical samples. This paper 确实提出了一个解决方案来解决这个问题,但我从未尝试过,所以我不能确定它的准确性。

现在如何使用 tensorflow 中的词嵌入很简单,只需要做

embed = hub.Module("https://tfhub.dev/google/universal-sentence-encoder/2")
embeddings = embed(["Input Text here as"," List of strings"])
session.run(embeddings)

由于您可能不熟悉 tensorflow 并尝试 运行 只是这段代码,您可能 运行 遇到一些麻烦,Follow this link 他们已经完全提到了如何使用这个和从那里你应该能够很容易地修改它以满足你的需要。

话虽如此,我建议首先查看他的 tensorlfow 嵌入模块及其 pre-trained 词嵌入,如果它们对您不起作用,请查看维基媒体 link,如果这也不起作用' 然后继续我 link 编辑的论文的概念。由于此答案描述的是 NLP 方法,因此不会 100% 准确,因此在继续之前请记住这一点。

手套向量https://nlp.stanford.edu/projects/glove/

Facebook的快文:https://github.com/facebookresearch/fastText/blob/master/pretrained-vectors.md

或者这个http://www.statmt.org/lm-benchmark/1-billion-word-language-modeling-benchmark-r13output.tar.gz

如果您运行在遵循 colab 教程后实施这个时遇到问题,请将您的问题添加到下面的问题和评论中,我们可以从那里进一步进行。

编辑向集群主题添加代码

简要,我没有使用词向量,而是对他们的摘要句子进行编码

文件content.py

def AllTopics():
    topics = []# list all your topics, not added here for space restricitons
    for i in range(len(topics)-1):
        yield topics[i]

文件summaryGenerator.py

import wikipedia
import pickle
from content import Alltopics
summary = []
failed = []
for topic in Alltopics():
    try:
        summary.append(wikipedia.summary(tuple((topic,str(topic)))))
    except Exception as e:
        failed.append(tuple((topic,e)))
with open("summary.txt", "wb") as fp:
    pickle.dump(summary , fp)
with open('failed.txt', 'wb') as fp:
    pickle.dump('failed', fp)

文件SimilartiyCalculator.py

import tensorflow as tf
import tensorflow_hub as hub
import numpy as np
import os
import pandas as pd
import re
import pickle
import sys
from sklearn.cluster import AgglomerativeClustering
from sklearn import metrics
from scipy.cluster import hierarchy
from scipy.spatial import distance_matrix


try:
    with open("summary.txt", "rb") as fp:   # Unpickling
        summary = pickle.load(fp)
except Exception as e:
    print ('Cannot load the summary file, Please make sure that it exists, if not run Summary Generator first', e)
    sys.exit('Read the error message')

module_url = "https://tfhub.dev/google/universal-sentence-encoder-large/3"
embed = hub.Module(module_url)

tf.logging.set_verbosity(tf.logging.ERROR)
messages = [x[1] for x in summary]
labels = [x[0] for x in summary]
with tf.Session() as session:
    session.run([tf.global_variables_initializer(), tf.tables_initializer()])
    message_embeddings = session.run(embed(messages)) # In message embeddings each vector is a second (1,512 vector) and is numpy.ndarray (noOfElemnts, 512)

X = message_embeddings
agl = AgglomerativeClustering(n_clusters=5, affinity='euclidean', memory=None, connectivity=None, compute_full_tree='auto', linkage='ward', pooling_func='deprecated')
agl.fit(X)
dist_matrix = distance_matrix(X,X)
Z = hierarchy.linkage(dist_matrix, 'complete')
dendro = hierarchy.dendrogram(Z)
cluster_labels = agl.labels_

这也托管在 GitHub 的 https://github.com/anandvsingh/WikipediaSimilarity 那里你可以找到 similarity.txt 文件和其他文件,在我的情况下我不能 运行关于所有主题,但我会敦促您在完整的主题列表中 运行 它(直接克隆存储库和 运行 SummaryGenerator.py),然后 上传 similarity.txt 通过拉取请求,以防你没有得到预期的结果。如果可能,还可以将 message_embeddings 作为主题上传到 csv 文件中并嵌入。

修改 2 将 similarityGenerator 切换为基于层次结构的聚类(凝聚)我建议您将标题名称保留在树状图的底部,为此请查看 dendrogram here 的定义,我验证了查看一些示例,结果看起来相当很好,您可以更改 n_clusters 值来微调您的模型。注意:这需要您再次 运行 摘要生成器。我认为您应该可以从这里获取它,您需要做的是尝试 n_cluster 的几个值并查看所有医学术语在其中分组在一起,然后找到该集群的 cluster_label你完成了。由于这里我们按摘要分组,因此聚类会更准确。如果您 运行 遇到任何问题或不明白的地方,请在下面发表评论。

您可以尝试通过为每个类别返回的 mediawiki 链接和反向链接对维基百科类别进行分类

import re
from mediawiki import MediaWiki

#TermFind will search through a list a given term
def TermFind(term,termList):
    responce=False
    for val in termList:
        if re.match('(.*)'+term+'(.*)',val):
            responce=True
            break
    return responce

#Find if the links and backlinks lists contains a given term 
def BoundedTerm(wikiPage,term):
    aList=wikiPage.links
    bList=wikiPage.backlinks
    responce=False
    if TermFind(term,aList)==True and TermFind(term,bList)==True:
         responce=True
    return responce

container=[]
wikipedia = MediaWiki()
for val in termlist:
    cpage=wikipedia.page(val)
    if BoundedTerm(cpage,'term')==True:
        container.append('medical')
    else:
        container.append('nonmedical')

想法是尝试猜测一个被大多数类别共享的术语,我尝试了生物学、医学和疾病,结果很好。也许您可以尝试使用 BoundedTerms 的多次调用来进行分类,或者一次调用多个术语并将结果组合起来进行分类。希望对你有帮助

解决方案概述

好的,我会从多个方向来解决这个问题。这里有一些很好的建议,如果我是你,我会使用这些方法的集合(多数投票,预测标签,在你的二元案例中,超过 50% 的 classifers 同意)。

我正在考虑以下方法:

  • 主动学习(下面我提供的示例方法)
  • provided as an answer by @TavoGC
  • SPARQL @Stanislav Kralin and/or provided by @Meena Nagarajan 对您的问题的评论提供的祖先类别(根据它们的差异,这两个可能是一个整体,但为此您必须联系两位创作者并比较他们的结果)。

这样三分之二的人必须同意某个概念是医学概念,从而进一步减少出错的可能性。

当我们讨论的时候,我会争论反对@ananand_v.singh in 提出的方法,因为:

  • 距离度量不应该是欧几里得,余弦相似度是更好的度量(例如spaCy使用),因为它不考虑向量的大小(不应该,word2vec 或 GloVe 就是这样训练的)
  • 如果我理解正确的话,会创建很多人工集群,而我们只需要两个:医学和非医学一个。此外,药物的质心并不以药物本身为中心。这带来了额外的问题,比如质心远离药物,其他词,比如 computerhuman(或您认为不适合药物的任何其他词)可能会进入集群.
  • 很难评价结果,更何况这件事是完全主观的。此外,词向量很难可视化和理解(使用 PCA/TSNE/similar 将它们转换为较低维度的 [2D/3D] 如此多的词,会给我们完全没有意义的结果 [是的,我试过这样做,PCA为您的较长数据集获得大约 5% 的解释方差,真的非常低])。

基于上面突出显示的问题,我提出了使用 active learning 的解决方案,这是解决此类问题的一种被遗忘的方法。

主动学习方法

在机器学习的这个子集中,当我们很难想出一个精确的算法时(比如一个术语属于 medical 类别意味着什么),我们会问人类 "expert"(实际上不必是专家)提供一些答案。

知识编码

正如 anand_v.singh 指出的那样,词向量是最有前途的方法之一,我也会在这里使用它(虽然有所不同,但 IMO 以一种更简洁、更简单的方式)。

我不会在我的回答中重复他的观点,所以我会加上我的两分钱:

  • 不要使用上下文词嵌入作为当前可用的技术水平(例如BERT
  • 检查有多少概念没有表示(例如表示为零向量)。它应该被检查(并且在我的代码中被检查,到时候会有进一步的讨论)并且你可以使用其中大部分存在的嵌入。

使用 spaCy

测量相似性

这个 class 测量编码为 spaCy 的 GloVe 词向量的 medicine 与所有其他概念之间的相似性。

class Similarity:
    def __init__(self, centroid, nlp, n_threads: int, batch_size: int):
        # In our case it will be medicine
        self.centroid = centroid

        # spaCy's Language model (english), which will be used to return similarity to
        # centroid of each concept
        self.nlp = nlp
        self.n_threads: int = n_threads
        self.batch_size: int = batch_size

        self.missing: typing.List[int] = []

    def __call__(self, concepts):
        concepts_similarity = []
        # nlp.pipe is faster for many documents and can work in parallel (not blocked by GIL)
        for i, concept in enumerate(
            self.nlp.pipe(
                concepts, n_threads=self.n_threads, batch_size=self.batch_size
            )
        ):
            if concept.has_vector:
                concepts_similarity.append(self.centroid.similarity(concept))
            else:
                # If document has no vector, it's assumed to be totally dissimilar to centroid
                concepts_similarity.append(-1)
                self.missing.append(i)

        return np.array(concepts_similarity)

此代码将为每个概念 return 一个数字,衡量它与质心的相似程度。此外,它还记录了缺少表示的概念索引。可以这样称呼:

import json
import typing

import numpy as np
import spacy

nlp = spacy.load("en_vectors_web_lg")

centroid = nlp("medicine")

concepts = json.load(open("concepts_new.txt"))
concepts_similarity = Similarity(centroid, nlp, n_threads=-1, batch_size=4096)(
    concepts
)

您可以用您的数据代替 new_concepts.json

看看spacy.load and notice I have used en_vectors_web_lg。它由 685.000 个独特的词向量 (很多)组成,并且可能开箱即用。您必须在安装 spaCy 后单独下载它,更多信息在上面的链接中提供。

另外你可能想使用多个质心词,例如添加 diseasehealth 之类的词并平均它们的词向量。不过,我不确定这是否会对您的案件产生积极影响。

其他可能性可能是使用多个质心并计算每个概念与多个质心之间的相似度。在这种情况下我们可能有一些阈值,这可能会删除一些 false positives,但可能会遗漏一些可以认为与 medicine 相似的术语。此外,这会使情况变得更加复杂,但如果您的结果不令人满意,您应该考虑上面的两个选项(并且只有在那些选项存在的情况下,不要在没有事先考虑的情况下跳入这种方法)。

现在,我们对概念的相似性有了一个粗略的度量。但是某个概念与医学有0.1正相似度是什么意思?这是一个应该 class 确定为医学的概念吗?或许那已经太遥远了?

请教专家

要获得阈值(低于阈值的术语将被视为非医学术语),最简单的方法是请人为我们class确定一些概念(这就是主动学习的意义所在)。是的,我知道这是一种非常简单的主动学习形式,但无论如何我都会这么认为。

我写了一个 class 和 sklearn-like 界面,要求人类 class 验证概念,直到达到最佳阈值(或最大迭代次数)。

class ActiveLearner:
    def __init__(
        self,
        concepts,
        concepts_similarity,
        max_steps: int,
        samples: int,
        step: float = 0.05,
        change_multiplier: float = 0.7,
    ):
        sorting_indices = np.argsort(-concepts_similarity)
        self.concepts = concepts[sorting_indices]
        self.concepts_similarity = concepts_similarity[sorting_indices]

        self.max_steps: int = max_steps
        self.samples: int = samples
        self.step: float = step
        self.change_multiplier: float = change_multiplier

        # We don't have to ask experts for the same concepts
        self._checked_concepts: typing.Set[int] = set()
        # Minimum similarity between vectors is -1
        self._min_threshold: float = -1
        # Maximum similarity between vectors is 1
        self._max_threshold: float = 1

        # Let's start from the highest similarity to ensure minimum amount of steps
        self.threshold_: float = 1
  • samples 参数描述了在每次迭代期间将向专家展示多少示例(这是最大值,如果已经要求提供示例或没有足够的示例,它将减少 return他们展示)。
  • step 表示每次迭代中阈值的下降(我们从 1 开始,表示完全相似)。
  • change_multiplier - 如果专家回答的概念不相关(或大部分不相关,因为其中多个是 returned),步长将乘以这个浮点数。它用于查明每次迭代 step 变化之间的确切阈值。
  • 概念根据相似度排序(概念越相似,越高)

下面的函数向专家征求意见,并根据他的回答找到最佳阈值。

def _ask_expert(self, available_concepts_indices):
    # Get random concepts (the ones above the threshold)
    concepts_to_show = set(
        np.random.choice(
            available_concepts_indices, len(available_concepts_indices)
        ).tolist()
    )
    # Remove those already presented to an expert
    concepts_to_show = concepts_to_show - self._checked_concepts
    self._checked_concepts.update(concepts_to_show)
    # Print message for an expert and concepts to be classified
    if concepts_to_show:
        print("\nAre those concepts related to medicine?\n")
        print(
            "\n".join(
                f"{i}. {concept}"
                for i, concept in enumerate(
                    self.concepts[list(concepts_to_show)[: self.samples]]
                )
            ),
            "\n",
        )
        return input("[y]es / [n]o / [any]quit ")
    return "y"

示例问题如下所示:

Are those concepts related to medicine?                                                      

0. anesthetic drug                                                                                                                                                                         
1. child and adolescent psychiatry                                                                                                                                                         
2. tertiary care center                                                     
3. sex therapy                           
4. drug design                                                                                                                                                                             
5. pain disorder                                                      
6. psychiatric rehabilitation                                                                                                                                                              
7. combined oral contraceptive                                
8. family practitioner committee                           
9. cancer family syndrome                          
10. social psychology                                                                                                                                                                      
11. drug sale                                                                                                           
12. blood system                                                                        

[y]es / [n]o / [any]quit y

...正在解析专家的回答:

# True - keep asking, False - stop the algorithm
def _parse_expert_decision(self, decision) -> bool:
    if decision.lower() == "y":
        # You can't go higher as current threshold is related to medicine
        self._max_threshold = self.threshold_
        if self.threshold_ - self.step < self._min_threshold:
            return False
        # Lower the threshold
        self.threshold_ -= self.step
        return True
    if decision.lower() == "n":
        # You can't got lower than this, as current threshold is not related to medicine already
        self._min_threshold = self.threshold_
        # Multiply threshold to pinpoint exact spot
        self.step *= self.change_multiplier
        if self.threshold_ + self.step < self._max_threshold:
            return False
        # Lower the threshold
        self.threshold_ += self.step
        return True
    return False

最后是 ActiveLearner 的整个代码代码,它根据专家找到最佳相似度阈值:

class ActiveLearner:
    def __init__(
        self,
        concepts,
        concepts_similarity,
        samples: int,
        max_steps: int,
        step: float = 0.05,
        change_multiplier: float = 0.7,
    ):
        sorting_indices = np.argsort(-concepts_similarity)
        self.concepts = concepts[sorting_indices]
        self.concepts_similarity = concepts_similarity[sorting_indices]

        self.samples: int = samples
        self.max_steps: int = max_steps
        self.step: float = step
        self.change_multiplier: float = change_multiplier

        # We don't have to ask experts for the same concepts
        self._checked_concepts: typing.Set[int] = set()
        # Minimum similarity between vectors is -1
        self._min_threshold: float = -1
        # Maximum similarity between vectors is 1
        self._max_threshold: float = 1

        # Let's start from the highest similarity to ensure minimum amount of steps
        self.threshold_: float = 1

    def _ask_expert(self, available_concepts_indices):
        # Get random concepts (the ones above the threshold)
        concepts_to_show = set(
            np.random.choice(
                available_concepts_indices, len(available_concepts_indices)
            ).tolist()
        )
        # Remove those already presented to an expert
        concepts_to_show = concepts_to_show - self._checked_concepts
        self._checked_concepts.update(concepts_to_show)
        # Print message for an expert and concepts to be classified
        if concepts_to_show:
            print("\nAre those concepts related to medicine?\n")
            print(
                "\n".join(
                    f"{i}. {concept}"
                    for i, concept in enumerate(
                        self.concepts[list(concepts_to_show)[: self.samples]]
                    )
                ),
                "\n",
            )
            return input("[y]es / [n]o / [any]quit ")
        return "y"

    # True - keep asking, False - stop the algorithm
    def _parse_expert_decision(self, decision) -> bool:
        if decision.lower() == "y":
            # You can't go higher as current threshold is related to medicine
            self._max_threshold = self.threshold_
            if self.threshold_ - self.step < self._min_threshold:
                return False
            # Lower the threshold
            self.threshold_ -= self.step
            return True
        if decision.lower() == "n":
            # You can't got lower than this, as current threshold is not related to medicine already
            self._min_threshold = self.threshold_
            # Multiply threshold to pinpoint exact spot
            self.step *= self.change_multiplier
            if self.threshold_ + self.step < self._max_threshold:
                return False
            # Lower the threshold
            self.threshold_ += self.step
            return True
        return False

    def fit(self):
        for _ in range(self.max_steps):
            available_concepts_indices = np.nonzero(
                self.concepts_similarity >= self.threshold_
            )[0]
            if available_concepts_indices.size != 0:
                decision = self._ask_expert(available_concepts_indices)
                if not self._parse_expert_decision(decision):
                    break
            else:
                self.threshold_ -= self.step
        return self

总而言之,您必须手动回答一些问题,但我认为这种方法更加准确。

此外,您不必检查所有样本,只需检查一小部分即可。您可以决定有多少样本构成一个医学术语(显示的 40 个医学样本和 10 个非医学样本是否仍应被视为医学术语?),这让您可以根据自己的喜好微调此方法。如果存在异常值(例如,50 个样本中有 1 个是非医学样本),我会认为阈值仍然有效。

再一次: 这种方法应该与其他方法混合使用,以尽量减少错误 classification 的机会。

分类器

当我们从专家那里获得阈值时,class化会是瞬间的,这里有一个简单的class用于class化:

class Classifier:
    def __init__(self, centroid, threshold: float):
        self.centroid = centroid
        self.threshold: float = threshold

    def predict(self, concepts_pipe):
        predictions = []
        for concept in concepts_pipe:
            predictions.append(self.centroid.similarity(concept) > self.threshold)
        return predictions

为了简洁起见,这里是最终的源代码:

import json
import typing

import numpy as np
import spacy


class Similarity:
    def __init__(self, centroid, nlp, n_threads: int, batch_size: int):
        # In our case it will be medicine
        self.centroid = centroid

        # spaCy's Language model (english), which will be used to return similarity to
        # centroid of each concept
        self.nlp = nlp
        self.n_threads: int = n_threads
        self.batch_size: int = batch_size

        self.missing: typing.List[int] = []

    def __call__(self, concepts):
        concepts_similarity = []
        # nlp.pipe is faster for many documents and can work in parallel (not blocked by GIL)
        for i, concept in enumerate(
            self.nlp.pipe(
                concepts, n_threads=self.n_threads, batch_size=self.batch_size
            )
        ):
            if concept.has_vector:
                concepts_similarity.append(self.centroid.similarity(concept))
            else:
                # If document has no vector, it's assumed to be totally dissimilar to centroid
                concepts_similarity.append(-1)
                self.missing.append(i)

        return np.array(concepts_similarity)


class ActiveLearner:
    def __init__(
        self,
        concepts,
        concepts_similarity,
        samples: int,
        max_steps: int,
        step: float = 0.05,
        change_multiplier: float = 0.7,
    ):
        sorting_indices = np.argsort(-concepts_similarity)
        self.concepts = concepts[sorting_indices]
        self.concepts_similarity = concepts_similarity[sorting_indices]

        self.samples: int = samples
        self.max_steps: int = max_steps
        self.step: float = step
        self.change_multiplier: float = change_multiplier

        # We don't have to ask experts for the same concepts
        self._checked_concepts: typing.Set[int] = set()
        # Minimum similarity between vectors is -1
        self._min_threshold: float = -1
        # Maximum similarity between vectors is 1
        self._max_threshold: float = 1

        # Let's start from the highest similarity to ensure minimum amount of steps
        self.threshold_: float = 1

    def _ask_expert(self, available_concepts_indices):
        # Get random concepts (the ones above the threshold)
        concepts_to_show = set(
            np.random.choice(
                available_concepts_indices, len(available_concepts_indices)
            ).tolist()
        )
        # Remove those already presented to an expert
        concepts_to_show = concepts_to_show - self._checked_concepts
        self._checked_concepts.update(concepts_to_show)
        # Print message for an expert and concepts to be classified
        if concepts_to_show:
            print("\nAre those concepts related to medicine?\n")
            print(
                "\n".join(
                    f"{i}. {concept}"
                    for i, concept in enumerate(
                        self.concepts[list(concepts_to_show)[: self.samples]]
                    )
                ),
                "\n",
            )
            return input("[y]es / [n]o / [any]quit ")
        return "y"

    # True - keep asking, False - stop the algorithm
    def _parse_expert_decision(self, decision) -> bool:
        if decision.lower() == "y":
            # You can't go higher as current threshold is related to medicine
            self._max_threshold = self.threshold_
            if self.threshold_ - self.step < self._min_threshold:
                return False
            # Lower the threshold
            self.threshold_ -= self.step
            return True
        if decision.lower() == "n":
            # You can't got lower than this, as current threshold is not related to medicine already
            self._min_threshold = self.threshold_
            # Multiply threshold to pinpoint exact spot
            self.step *= self.change_multiplier
            if self.threshold_ + self.step < self._max_threshold:
                return False
            # Lower the threshold
            self.threshold_ += self.step
            return True
        return False

    def fit(self):
        for _ in range(self.max_steps):
            available_concepts_indices = np.nonzero(
                self.concepts_similarity >= self.threshold_
            )[0]
            if available_concepts_indices.size != 0:
                decision = self._ask_expert(available_concepts_indices)
                if not self._parse_expert_decision(decision):
                    break
            else:
                self.threshold_ -= self.step
        return self


class Classifier:
    def __init__(self, centroid, threshold: float):
        self.centroid = centroid
        self.threshold: float = threshold

    def predict(self, concepts_pipe):
        predictions = []
        for concept in concepts_pipe:
            predictions.append(self.centroid.similarity(concept) > self.threshold)
        return predictions


if __name__ == "__main__":
    nlp = spacy.load("en_vectors_web_lg")

    centroid = nlp("medicine")

    concepts = json.load(open("concepts_new.txt"))
    concepts_similarity = Similarity(centroid, nlp, n_threads=-1, batch_size=4096)(
        concepts
    )

    learner = ActiveLearner(
        np.array(concepts), concepts_similarity, samples=20, max_steps=50
    ).fit()
    print(f"Found threshold {learner.threshold_}\n")

    classifier = Classifier(centroid, learner.threshold_)
    pipe = nlp.pipe(concepts, n_threads=-1, batch_size=4096)
    predictions = classifier.predict(pipe)
    print(
        "\n".join(
            f"{concept}: {label}"
            for concept, label in zip(concepts[20:40], predictions[20:40])
        )
    )

在回答了一些问题后,阈值为 0.1([-1, 0.1) 之间的所有内容都被认为是非医学的,而 [0.1, 1] 被认为是医学的)我得到了以下结果:

kartagener s syndrome: True
summer season: True
taq: False
atypical neuroleptic: True
anterior cingulate: False
acute respiratory distress syndrome: True
circularity: False
mutase: False
adrenergic blocking drug: True
systematic desensitization: True
the turning point: True
9l: False
pyridazine: False
bisoprolol: False
trq: False
propylhexedrine: False
type 18: True
darpp 32: False
rickettsia conorii: False
sport shoe: True

如您所见,这种方法远非完美,所以最后一节描述了可能的改进:

可能的改进

如开头所述,将我的方法与其他答案混合使用可能会遗漏像 sport shoe 属于 medicine 的想法,而主动学习方法在以下情况下更像是决定性的投票上述两种启发式之间的平局。

我们也可以创建一个主动学习集成。我们将使用多个阈值(增加或减少)而不是一个阈值,比如 0.1,假设它们是 0.1, 0.2, 0.3, 0.4, 0.5.

假设 sport shoe 得到,对于每个阈值,它各自的 True/False 如下所示:

True True False False False,

进行多数投票后,我们将以 2 票中的 3 票将其标记为 non-medical。此外,如果低于它的阈值投票超过它,我也会减轻太严格的阈值(如果 True/False 看起来像这样:True True True False False)。

我想出的最终可能的改进:在上面的代码中,我使用了 Doc 向量,这是创建概念的词向量的平均值。假设缺少一个词(由零组成的向量),在这种情况下,它会被推离 medicine 质心更远。您可能不希望这样(因为一些小众医学术语 [gpv 或其他缩写] 可能缺少它们的表示形式),在这种情况下,您可以仅对那些不同于零的向量进行平均。

我知道这个 post 很长,所以如果您有任何问题 post 请在下面提出。

wikipedia 库也是从给定页面中提取类别的好选择,因为 wikipedia.WikipediaPage(page).categories returns 是一个简单的列表。该库还允许您搜索具有相同标题的多个页面。

在医学中似乎有很多关键词根和后缀,因此查找关键词的方法可能是查找医学术语的好方法。

import wikipedia

def categorySorter(targetCats, pagesToCheck, mainCategory):
    targetList = []
    nonTargetList = []
    targetCats = [i.lower() for i in targetCats]

    print('Sorting pages...')
    print('Sorted:', end=' ', flush=True)
    for page in pagesToCheck:

        e = openPage(page)

        def deepList(l):
            for item in l:
                if item[1] == 'SUBPAGE_ID':
                    deepList(item[2])
                else:
                    catComparator(item[0], item[1], targetCats, targetList, nonTargetList, pagesToCheck[-1])

        if e[1] == 'SUBPAGE_ID':
            deepList(e[2])
        else:
            catComparator(e[0], e[1], targetCats, targetList, nonTargetList, pagesToCheck[-1])

    print()
    print()
    print('Results:')
    print(mainCategory, ': ', targetList, sep='')
    print()
    print('Non-', mainCategory, ': ', nonTargetList, sep='')

def openPage(page):
    try:
        pageList = [page, wikipedia.WikipediaPage(page).categories]
    except wikipedia.exceptions.PageError as p:
        pageList = [page, 'NONEXIST_ID']
        return
    except wikipedia.exceptions.DisambiguationError as e:
        pageCategories = []
        for i in e.options:
            if '(disambiguation)' not in i:
                pageCategories.append(openPage(i))
        pageList = [page, 'SUBPAGE_ID', pageCategories]
        return pageList
    finally:
        return pageList

def catComparator(pageTitle, pageCategories, targetCats, targetList, nonTargetList, lastPage):

    # unhash to view the categories of each page
    #print(pageCategories)
    pageCategories = [i.lower() for i in pageCategories]

    any_in = False
    for i in targetCats:
        if i in pageTitle:
            any_in = True
    if any_in:
        print('', end = '', flush=True)
    elif compareLists(targetCats, pageCategories):
        any_in = True

    if any_in:
        targetList.append(pageTitle)
    else:
        nonTargetList.append(pageTitle)

    # Just prints a pretty list, you can comment out until next hash if desired
    if any_in:
        print(pageTitle, '(T)', end='', flush=True)
    else:
        print(pageTitle, '(F)',end='', flush=True)

    if pageTitle != lastPage:
        print(',', end=' ')
    # No more commenting

    return any_in

def compareLists (a, b):
    for i in a:
        for j in b:
            if i in j:
                return True
    return False

该代码实际上只是将关键词和后缀列表与每个页面的标题及其类别进行比较,以确定页面是否与医学相关。它还查看更大主题的相关 pages/sub 页面,并确定这些页面是否也相关。我不太精通我的医学,所以请原谅这些类别,但这里有一个标记到底部的例子:

medicalCategories = ['surgery', 'medic', 'disease', 'drugs', 'virus', 'bact', 'fung', 'pharma', 'cardio', 'pulmo', 'sensory', 'nerv', 'derma', 'protein', 'amino', 'unii', 'chlor', 'carcino', 'oxi', 'oxy', 'sis', 'disorder', 'enzyme', 'eine', 'sulf']
listOfPages = ['juvenile chronic arthritis', 'climate', 'alexidine', 'mouthrinse', 'sialosis', 'australia', 'artificial neural network', 'ricinoleic acid', 'bromosulfophthalein', 'myelosclerosis', 'hydrochloride salt', 'cycasin', 'aldosterone antagonist', 'fungal growth', 'describe', 'liver resection', 'coffee table', 'natural language processing', 'infratemporal fossa', 'social withdrawal', 'information retrieval', 'monday', 'menthol', 'overturn', 'prevailing', 'spline function', 'acinic cell carcinoma', 'furth', 'hepatic protein', 'blistering', 'prefixation', 'january', 'cardiopulmonary receptor', 'extracorporeal membrane oxygenation', 'clinodactyly', 'melancholic', 'chlorpromazine hydrochloride', 'level of evidence', 'washington state', 'cat', 'year elevan', 'trituration', 'gold alloy', 'hexoprenaline', 'second molar', 'novice', 'oxygen radical', 'subscription', 'ordinate', 'approximal', 'spongiosis', 'ribothymidine', 'body of evidence', 'vpb', 'porins', 'musculocutaneous']
categorySorter(medicalCategories, listOfPages, 'Medical')

至少据我所知,这个示例列表包含了大约 70% 的内容。