Skip to content

Comment regrouper les catégories de wikipedia en python ?

Cette équipe de spécialistes, après quelques jours de recherche et de collecte d'informations, a obtenu les données nécessaires, nous espérons que vous les trouverez utiles dans votre travail.

Solution :

" Par conséquent, j'aimerais savoir s'il existe un moyen d'obtenir les parent category des catégories (par exemple, les catégories de enzyme inhibitor et bypass surgery appartiennent à medical catégorie parentale)"

Les catégories de MediaWiki sont elles-mêmes des pages wiki. Une "catégorie parente" est juste une catégorie à laquelle la page de catégorie "enfant" appartient. Vous pouvez donc obtenir les catégories parentes d'une catégorie exactement de la même manière que vous obtiendriez les catégories de n'importe quelle autre page wiki.

Par exemple, en utilisant pymediawiki :

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

Aperçu de la solution

Ok, j'aborderais le problème sous plusieurs angles. Il y a quelques grandes suggestions ici et si j'étais vous, j'utiliserais un ensemble de ces approches (vote majoritaire, prédiction de l'étiquette qui est convenue par plus de 50% des classificateurs dans votre cas binaire).

Je pense aux approches suivantes :

  • Apprentissage actif (exemple d'approche fourni par moi ci-dessous)
  • Backlinks de MediaWiki fourni en réponse par @TavoGC
  • SPARQL catégories ancestrales fournies comme commentaire à votre question par @Stanislav Kralin et/ou catégories parentales fournies par @Meena Nagarajan (ces deux-là pourraient constituer un ensemble à part entière en fonction de leurs différences, mais pour cela il faudrait contacter les deux créateurs et comparer leurs résultats).

De cette façon, 2 sur trois devraient convenir qu'un certain concept est un concept médical, ce qui minimise encore plus les chances d'une erreur.

Pendant qu'on y est, je soutiendrais que... contre approche présentée par @ananand_v.singh dans cette réponse, car :

  • métrique de la distance ne devrait pas être euclidienne, la similarité en cosinus est une bien meilleure métrique (utilisée par, par exemple, spaCy) car elle ne tient pas compte de la magnitude des vecteurs (et elle ne devrait pas, c'est ainsi que word2vec ou GloVe ont été formés).
  • de nombreux clusters artificiels seraient créés si j'ai bien compris, alors que nous n'en avons besoin que de deux : celui de médecine et celui de non-médecine. De plus, le centroïde de la médecine n'est pas centré sur le médicament lui-même. Cela pose des problèmes supplémentaires, disons que le centroïde est éloigné du médicament et d'autres mots comme, par exemple, computer ou human (ou tout autre mot ne correspondant pas à votre avis au médicament) pourraient entrer dans le cluster.
  • il est difficile d'évaluer les résultats, d'autant plus que la question est strictement subjective. En outre, les vecteurs de mots sont difficiles à visualiser et à comprendre (les couler dans des dimensions inférieures. [2D/3D] en utilisant PCA/TSNE/similaire pour autant de mots, nous donnerait des résultats totalement non-sens. [yeah, I have tried to do it, PCA gets around 5% explained variance for your longer dataset, really, really low]).

Sur la base des problèmes soulignés ci-dessus, j'ai trouvé une solution en utilisant l'apprentissage actif, qui est une approche assez oubliée pour de tels problèmes.

Approche de l'apprentissage actif

Dans ce sous-ensemble de l'apprentissage automatique, lorsque nous avons du mal à trouver un algorithme exact (par exemple, qu'est-ce que cela signifie pour un terme de faire partie de... medical catégorie), nous demandons à un "expert" humain (qui n'a pas besoin d'être réellement un expert) de fournir des réponses.

Encodage des connaissances

Comme anand_v.singh l'a souligné, les vecteurs de mots sont l'une des approches les plus prometteuses et je vais l'utiliser ici aussi (différemment cependant, et IMO d'une manière beaucoup plus propre et plus facile).

Je ne vais pas répéter ses points dans ma réponse, alors je vais ajouter mes deux cents :

  • Ne pas utiliser les mots-embeddings contextualisés comme état de l'art actuellement disponible (par exemple, BERT).
  • Vérifiez combien de vos concepts ont aucune représentation (par exemple, est représenté comme un vecteur de zéros). Il devrait être vérifié (et est vérifié dans mon code,, il y aura une discussion plus approfondie le moment venu) et vous pouvez utiliser l'incorporation qui a la plupart d'entre eux présents.

Mesure de la similarité en utilisant spaCy

Cette classe mesure la similarité entre medicine encodé comme vecteur de mots GloVe de spaCy et tout autre concept.

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)

Ce code renvoie un nombre pour chaque concept mesurant son degré de similarité avec le centroïde. De plus, il enregistre les indices des concepts manquant leur représentation. Il pourrait être appelé comme suit :

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
)

Vous pouvez substituer vos données à la place de new_concepts.json.

Regardez spacy.load et remarquez que j'ai utilisé en_vectors_web_lg. Il est composé de 685.000 vecteurs de mots uniques (ce qui est beaucoup), et peut fonctionner d'emblée pour votre cas. Vous devez le télécharger séparément après avoir installé spaCy, plus d'informations fournies dans les liens ci-dessus.

En outre, vous pouvez vouloir utiliser des mots centroïdes multiples par exemple, ajoutez des mots comme disease ou health et faire la moyenne de leurs vecteurs de mots. Je ne suis pas sûr que cela affecte positivement votre cas cependant.

Autre possibilité pourrait être d'utiliser plusieurs centroïdes et de calculer la similitude entre chaque concept et plusieurs centroïdes. Nous pouvons avoir quelques seuils dans ce cas, cela est susceptible de supprimer certains faux positifs, mais peut manquer certains termes que l'on pourrait considérer comme similaires à.... medicine. En outre, cela compliquerait beaucoup plus le cas, mais si vos résultats ne sont pas satisfaisants, vous devriez envisager les deux options ci-dessus (et seulement si celles-ci le sont, ne vous lancez pas dans cette approche sans réflexion préalable).

Maintenant, nous avons une mesure approximative de la similarité des concepts. Mais qu'est-ce que cela signifie qu'un certain concept a 0.1 similarité positive avec la médecine ? Est-ce un concept que l'on devrait classer comme médical ? Ou peut-être que c'est déjà trop loin ?

Demander à un expert

Pour obtenir un seuil (en dessous duquel les termes seront considérés comme non médicaux), le plus simple est de demander à un humain de classer certains des concepts pour nous (et c'est à cela que sert l'apprentissage actif). Oui, je sais que c'est une forme très simple d'apprentissage actif, mais je le considérerais comme tel quand même.

J'ai écrit une classe avec sklearn-like interface demandant à l'humain de classer des concepts jusqu'à ce que le seuil optimal (ou le nombre maximum d'itérations) soit atteint.

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 argument décrit combien d'exemples seront montrés à un expert lors de chaque itération (c'est le maximum, il retournera moins si des échantillons ont déjà été demandés ou s'il n'y en a pas assez à montrer).
  • step représente la baisse du seuil (on commence à 1 signifiant une similarité parfaite) à chaque itération.
  • change_multiplier - si un expert répond que les concepts ne sont pas liés (ou pour la plupart non liés, car plusieurs d'entre eux sont retournés), le pas est multiplié par ce nombre à virgule flottante. Il est utilisé pour déterminer le seuil exact entre step changements à chaque itération.
  • Les concepts sont triés en fonction de leur similarité (plus un concept est similaire, plus il est élevé).

La fonction ci-dessous demande un avis à un expert et trouve le seuil optimal en fonction de ses réponses.

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"

Un exemple de question ressemble à ceci :

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

... analyse d'une réponse de l'expert :

# 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

Et enfin tout le code de ActiveLearnerqui trouve le seuil optimal de similitude en fonction de l'expert :

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

Au final, il faudrait répondre à certaines questions manuellement mais cette approche est... beaucoup plus précise à mon avis.

En outre, vous n'êtes pas obligé de passer par tous les échantillons, juste un petit sous-ensemble de celui-ci. Vous pouvez décider combien d'échantillons constituent un terme médical (si 40 échantillons médicaux et 10 échantillons non médicaux montrés, devraient encore être considérés comme médicaux ?), ce qui vous permet d'affiner cette approche selon vos préférences. S'il y a une valeur aberrante (disons qu'un échantillon sur 50 est non-médical), je considérerais que le seuil est toujours valable.

Encore une fois : Cette approche devrait être mélangée avec d'autres afin de minimiser les chances de classification erronée.

Classificateur

Lorsque nous obtenons le seuil de l'expert, la classification serait instantanée, voici une classe simple pour la classification :

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

Et pour la brièveté, voici le code source final :

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])
        )
    )

Après avoir répondu à quelques questions, avec un seuil de 0,1 (tout ce qui se situe entre... [-1, 0.1) est considéré comme non-médical, tandis que [0.1, 1] est considéré comme médical), j'ai obtenu les résultats suivants :

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

Comme vous pouvez le constater, cette approche est loin d'être parfaite, aussi la dernière section a décrit les améliorations possibles :

Améliorations possibles

Comme mentionné au début, utiliser mon approche mélangée à d'autres réponses laisserait probablement de côté des idées comme... sport shoe l'appartenance à medicine out et l'approche de l'apprentissage actif serait plutôt un vote décisif en cas d'égalité entre deux heuristiques mentionnées ci-dessus.

Nous pourrions également créer un ensemble d'apprentissage actif. Au lieu d'un seul seuil, disons 0,1, nous en utiliserions plusieurs (soit croissants, soit décroissants), disons que ce sont les suivants . 0.1, 0.2, 0.3, 0.4, 0.5.

Disons que sport shoe obtient, pour chaque seuil, il est respectif True/False comme ceci :

True True False False False,

En faisant un vote majoritaire, nous le marquerions non-medical par 3 voix sur 2. De plus, un seuil trop strict serait également atténué si les seuils inférieurs le surpassent (cas si True/False ressemblerait à ceci : True True True False False).

Dernière amélioration possible que j'ai trouvée: Dans le code ci-dessus, j'utilise Doc vector, qui est une moyenne des vecteurs de mots créant le concept. Disons qu'un mot est manquant (vecteurs constitués de zéros), dans ce cas, il serait poussé plus loin du vecteur Doc. medicine centroïde. Vous pouvez ne pas vouloir cela (comme certains termes médicaux de niche [abbreviations like gpv or others] pourraient ne pas être représentés), dans ce cas vous pourriez faire la moyenne uniquement des vecteurs qui sont différents de zéro.

Je sais que ce post est assez long, donc si vous avez des questions posez-les ci-dessous.

Vous pourriez essayer de classer les catégories wikipedia par les liens mediawiki et les rétroliens retournés pour chaque catégorie.

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')

L'idée est d'essayer de deviner un terme qui est partagé par la plupart des catégories, j'ai essayé biologie, médecine et maladie avec de bons résultats. Vous pouvez peut-être essayer d'utiliser plusieurs appels de BoundedTerms pour effectuer la classification, ou un appel unique pour plusieurs termes et combiner le résultat pour la classification. J'espère que cela vous aidera.

Section des critiques et des évaluations

Vous pouvez aider notre travail en laissant un commentaire et en le notant, nous vous en remercions.



Utilisez notre moteur de recherche

Ricerca
Generic filters

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.