Starten Sie LDA in der realen Welt. Detaillierte Anleitung

Vorwort


Im Internet gibt es viele Tutorials, in denen erklärt wird, wie die LDA funktioniert (Latent Dirichlet Allocation) und wie sie in die Praxis umgesetzt wird. Beispiele für LDA-Schulungen werden häufig anhand von „beispielhaften“ Datensätzen demonstriert, z. B. dem „20 Newsgroups-Datensatz“, der bei sklearn erhältlich ist.


Ein Merkmal des Trainings am Beispiel "beispielhafter" Datensätze ist, dass die Daten dort immer in Ordnung sind und bequem an einem Ort gestapelt werden. Beim Training von Produktionsmodellen sind die Daten, die direkt aus realen Quellen stammen, normalerweise umgekehrt:


  • Viele Emissionen.
  • Falsches Markup (falls vorhanden).
  • Sehr starke Klassenungleichgewichte und hässliche Verteilungen von Datensatzparametern.
  • Für Texte sind dies: Grammatikfehler, eine große Anzahl seltener und einzigartiger Wörter, Mehrsprachigkeit.
  • Eine unbequeme Art, Daten zu speichern (verschiedene oder seltene Formate, die Notwendigkeit des Parsens)

Historisch gesehen versuche ich, aus Beispielen zu lernen, die den Realitäten der Produktionsrealität so nahe wie möglich kommen, weil man auf diese Weise die Problembereiche einer bestimmten Art von Aufgabe am besten erfassen kann. So war es auch mit der LDA, und in diesem Artikel möchte ich meine Erfahrungen teilen - wie man LDA von Grund auf mit vollständig rohen Daten ausführt. Ein Teil des Artikels befasst sich mit dem Abrufen dieser Daten, damit das Beispiel zu einem vollwertigen „technischen Fall“ wird.


Themenmodellierung und LDA.


Überlegen Sie sich zunächst, was der LDA im Allgemeinen tut und welche Aufgaben er verwendet.
Am häufigsten wird LDA für Themenmodellierungsaufgaben verwendet. Solche Aufgaben sind die Aufgaben des Gruppierens oder Klassifizierens von Texten - so, dass jede Klasse oder jeder Cluster Texte mit ähnlichen Themen enthält.


Um LDA auf den Textdatensatz (im Folgenden als Textkörper bezeichnet) anzuwenden, muss der Körper in eine Termdokumentmatrix umgewandelt werden.


Eine Termdokumentmatrix ist eine Matrix mit einer Größe N malWwo
N ist die Anzahl der Dokumente in dem Fall und W ist die Größe des Wörterbuchs des Falls, d.h. die Anzahl der Wörter (eindeutig), die in unserem Korpus gefunden werden. In der i-ten Zeile ist die j-te Spalte der Matrix eine Zahl - wie oft im i-ten Text das j-te Wort gefunden wurde.


Die LDA erstellt für eine gegebene Term-Dokumentmatrix und T einer vorbestimmten Anzahl von Themen zwei Verteilungen:


  1. Die Verteilung der Themen in den Texten. (In der Praxis durch die Größenmatrix gegeben N malT)
  2. Die Verteilung der Wörter nach Themen (Größenmatrix T timesW)

Die Werte der Zellen dieser Matrizen sind jeweils die Wahrscheinlichkeiten, mit denen dieses Thema in diesem Dokument enthalten ist (oder der Anteil des Themas im Dokument, wenn wir das Dokument als eine Mischung verschiedener Themen betrachten) für die Matrix 'Verteilung von Themen in Texten'.


Für die Matrix 'Verteilung von Wörtern nach Themen' sind die Werte die Wahrscheinlichkeit, das Wort j im Text mit Thema i zu treffen. Qualitativ können wir diese Zahlen als Koeffizienten betrachten, die charakterisieren, wie dieses Wort für dieses Thema typisch ist.


Es sollte gesagt werden, dass das Wort Thema keine „alltägliche“ Definition dieses Wortes ist. Die LDA weist diesen T zu, aber welche Art von Themen dies sind und ob sie bekannten Themen von Texten entsprechen, wie z. B. "Sport", "Wissenschaft", "Politik", ist unbekannt. In diesem Fall ist es besser, über das Thema als eine Art abstrakte Einheit zu sprechen, die durch eine Linie in der Matrix der Wortverteilung nach Themen definiert ist und mit einiger Wahrscheinlichkeit diesem Text entspricht, wenn Sie es sich als eine Familie charakteristischer Wortgruppen vorstellen können, die sich mit entsprechenden Wahrscheinlichkeiten treffen (aus der Tabelle) in einem bestimmten Satz von Texten.


Wenn Sie detaillierter und in Formeln studieren möchten, wie die LDA geschult ist und funktioniert, finden Sie hier einige Materialien (die vom Autor verwendet wurden):



Wir bekommen wilde Daten


Für unsere 'Laborarbeit' benötigen wir einen benutzerdefinierten Datensatz mit eigenen Fehlern und Merkmalen. Sie können es an verschiedenen Orten erhalten: Laden Sie Rezensionen von Kinopoisk, Wikipedia-Artikel, Nachrichten von einem Nachrichtenportal herunter, wir werden eine etwas extremere Option wählen - Beiträge von VKontakte-Communities.


Wir werden das so machen:


  1. Wir wählen einen VK-Benutzer aus.
  2. Wir bekommen eine Liste aller seiner Freunde.
  3. Für jeden Freund nehmen wir seine gesamte Gemeinschaft.
  4. Für jede Community jedes Freundes pumpen wir die ersten n (n = 100) Community-Beiträge aus und kombinieren sie zu einem Community-Textinhalt.

Werkzeuge und Artikel


Um Beiträge herunterzuladen, verwenden wir das vk-Modul, um mit der VKontakte-API für Python zu arbeiten. Einer der kompliziertesten Momente beim Schreiben einer Anwendung mit der VKontakte-API ist die Autorisierung. Glücklicherweise ist der Code, der diese Arbeit ausführt, bereits geschrieben und gemeinfrei. Mit Ausnahme von vk habe ich ein kleines Autorisierungsmodul verwendet - vkauth.


Links zu den Modulen und Artikeln, die zum Studium der VKontakte-API verwendet wurden:



Einen Code schreiben


Melden Sie sich mit vkauth an:


#authorization of app using modules imported. app_id = '6203169' perms = ['photos','friends','groups'] API_ver = '5.68' Auth = VKAuth(perms, app_id, API_ver) Auth.auth() token = Auth.get_token() user_id = Auth.get_user_id() #starting session session = vk.Session(access_token=token) api = vk.API(session) 

Dabei wurde ein kleines Modul geschrieben, das alle Funktionen enthält, die zum Herunterladen von Inhalten im entsprechenden Format erforderlich sind. Sie sind unten aufgeführt. Lassen Sie uns diese durchgehen:


 def get_friends_ids(api, user_id): ''' For a given API object and user_id returns a list of all his friends ids. ''' friends = api.friends.get(user_id=user_id, v = '5.68') friends_ids = friends['items'] return friends_ids def get_user_groups(api, user_id, moder=True, only_open=True): ''' For a given API user_id returns list of all groups he subscribed to. Flag model to get only those groups where user is a moderator or an admin) Flag only_open to get only public(open) groups. ''' kwargs = {'user_id' : user_id, 'v' : '5.68' } if moder == True: kwargs['filter'] = 'moder' if only_open == True: kwargs['extended'] = 1 kwargs['fields'] = ['is_closed'] groups = api.groups.get(**kwargs) groups_refined = [] for group in groups['items']: cond_check = (only_open and group['is_closed'] == 0) or not only_open if cond_check: refined = {} refined['id'] = group['id'] * (-1) refined['name'] = group['name'] groups_refined.append(refined) return groups_refined def get_n_posts_text(api, group_id, n_posts=50): ''' For a given api and group_id returns first n_posts concatenated as one text. ''' wall_contents = api.wall.get(owner_id = group_id, count=n_posts, v = '5.68') wall_contents = wall_contents['items'] text = '' for post in wall_contents: text += post['text'] + ' ' return text 

Die endgültige Pipeline lautet wie folgt:


 #id of user whose friends you gonna get, like: https://vk.com/id111111111 user_id = 111111111 friends_ids = vt.get_friends_ids(api, user_id) #collecting all groups groups = [] for i,friend in tqdm(enumerate(friends_ids)): if i % 3 == 0: sleep(1) friend_groups = vt.get_user_groups(api, friend, moder=False) groups += friend_groups #converting groups to dataFrame groups_df = pd.DataFrame(groups) groups_df.drop_duplicates(inplace=True) #reading content(content == first 100 posts) for i,group in tqdm(groups_df.iterrows()): name = group['name'] group_id = group['id'] #Different kinds of fails occures during scrapping #For examples there are names of groups with slashes #Like: 'The Kaaats / Indie-rock' try: content = vt.get_n_posts_text(api, group_id, n_posts=100) dst_path = join(data_path, name + '.txt') with open(dst_path, 'w+t') as f: f.write(content) except Exception as e: print('Error occured on group:', name) print(e) continue #need it because of requests limitaion in VK API. if i % 3 == 0: sleep(1) 

Schlägt fehl


Im Allgemeinen ist das Herunterladen von Daten an sich nicht schwierig. Sie sollten nur zwei Punkte beachten:


  1. Aufgrund der Privatsphäre einiger Communitys erhalten Sie manchmal Zugriffsfehler, manchmal werden andere Fehler durch die Installation von try behoben, außer an der richtigen Stelle.
  2. VK hat eine Begrenzung für die Anzahl der Anforderungen pro Sekunde.

Wenn Sie eine große Anzahl von Anforderungen stellen, beispielsweise in einer Schleife, werden auch Fehler abgefangen. Dieses Problem kann auf verschiedene Arten gelöst werden:


  1. Dumm und unverblümt: Bleiben Sie alle 3 Anfragen im Schlaf (einige). Dies geschieht in einer Zeile und verlangsamt das Entladen erheblich, wenn das Datenvolumen nicht groß ist und keine Zeit für komplexere Methoden bleibt - dies ist durchaus akzeptabel. (In diesem Artikel implementiert)
  2. Verstehen Sie die Arbeit von Long Poll-Anfragen https://vk.com/dev/using_longpoll

In diesem Artikel wurde eine einfache und langsame Methode gewählt. In Zukunft werde ich wahrscheinlich einen Mikroartikel darüber schreiben, wie die Anzahl der Anfragen pro Sekunde umgangen oder die Einschränkungen aufgehoben werden können.


Zusammenfassung


Mit dem Startwert "einige" Benutzer mit ~ 150 Freunden gelang es ihnen, 4.679 Texte zu erhalten - jeder kennzeichnet eine bestimmte VK-Community. Die Texte sind sehr unterschiedlich groß und in vielen Sprachen verfasst - einige davon sind für unsere Zwecke nicht geeignet, aber wir werden etwas weiter darauf eingehen.


Hauptteil


Bild


Lassen Sie uns alle Blöcke unserer Pipeline durchgehen - zuerst das obligatorische (Ideal), dann den Rest - sie sind nur von größtem Interesse.


Countvectorizer


Bevor wir LDA unterrichten, müssen wir unsere Dokumente in Form einer Term-Dokumentmatrix präsentieren. Dies umfasst normalerweise Operationen wie:


  • Puttuctions / Zahlen / unnötige Token entfernen.
  • Tokenisierung (Präsentation als Wortliste)
  • Wörter zählen, eine thermische Dokumentmatrix zusammenstellen.

Alle diese Aktionen in sklearn werden bequem im Rahmen einer Programmentität implementiert - sklearn.feature_extraction.text.CountVectorizer.


Dokumentationslink


Alles was Sie tun müssen ist:


 count_vect = CountVectorizer(input='filename', stop_words=stopwords, vocabulary=voc) dataset = count_vect.fit_transform(train_names) 

Lda


Ähnlich wie bei CountVectorizer ist LDA in Sklearn und anderen Frameworks perfekt implementiert. Daher macht es in unserem rein praktischen Artikel nicht viel Sinn, viel Platz direkt für deren Implementierung zu verwenden.


Dokumentationslink


Alles was Sie brauchen, um LDA zu starten ist:


 #training LDA lda = LDA(n_components = 60, max_iter=30, n_jobs=6, learning_method='batch', verbose=1) lda.fit(dataset) 

Vorverarbeitung


Wenn wir unsere Texte nur unmittelbar nach dem Herunterladen und Konvertieren in eine Term-Document-Matrix mit dem CountVectorizer und dem integrierten Standard-Tokenizer verwenden, erhalten wir eine Matrix mit der Größe 4679 x 769801 (für die von mir verwendeten Daten).


Die Größe unseres Wörterbuchs wird 769801 betragen. Selbst wenn wir davon ausgehen, dass die meisten Wörter informativ sind, ist es unwahrscheinlich, dass wir eine gute LDA erhalten. Etwas wie „Flüche der Dimensionen“ erwartet uns, ganz zu schweigen von fast jedem Computer. Wir werden nur den gesamten RAM verstopfen. Tatsächlich sind die meisten dieser Wörter völlig uninformativ. Die überwiegende Mehrheit von ihnen sind:


  • Emoticons, Zeichen, Zahlen.
  • Einzigartige oder sehr seltene Wörter (z. B. polnische Wörter aus einer Gruppe mit polnischen Memen, falsch geschriebene Wörter oder „albanisch“).
  • Sehr häufige Wortarten (z. B. Präpositionen und Pronomen).

Darüber hinaus sind viele Gruppen in VK ausschließlich auf Bilder spezialisiert - es gibt dort fast keine Textbeiträge - die ihnen entsprechenden Texte sind entartet, in der thermischen Dokumentmatrix geben sie uns fast vollständig Nullzeilen.


Und so, lasst uns alles klären!
Wir tokenisieren alle Texte, entfernen Satzzeichen und Zahlen aus ihnen und betrachten das Histogramm der Verteilung von Texten nach der Anzahl der Wörter:
Bild


Wir entfernen alle Texte, die kleiner als 100 Wörter sind (es gibt 525 davon).


Nun das Wörterbuch:
Das Entfernen aller Token (Wörter), die keine Buchstaben sind, im Rahmen unserer Aufgabe - dies ist durchaus akzeptabel. Der CountVectorizer macht dies selbstständig, auch wenn dies nicht der Fall ist. Ich denke, hier müssen keine Beispiele angegeben werden (sie sind in der Vollversion des Codes für den Artikel enthalten).


Eines der häufigsten Verfahren zum Reduzieren der Größe eines Wörterbuchs ist das Entfernen der sogenannten Stoppwörter (Stoppwörter) - Wörter, die keine semantische Last tragen und / oder keine thematische Färbung aufweisen (in unserem Fall Themenmodellierung). Solche Wörter sind in unserem Fall zum Beispiel:


  • Pronomen und Präpositionen.
  • Artikel - die, a.
  • Allgemeine Wörter: "sein", "gut", "wahrscheinlich" usw.

Das nltk-Modul hat Listen von Stoppwörtern in Russisch und Englisch erstellt, die jedoch eher schwach sind. Im Internet können Sie auch Listen mit Stoppwörtern für jede Sprache finden und zu denen in nltk hinzufügen. Also werden wir es tun. Nehmen Sie zusätzliche Stoppwörter von hier:



In der Praxis werden bei der Lösung spezifischer Probleme die Listen der Stoppwörter schrittweise angepasst und ergänzt, wenn die Modelle trainiert werden, da für jeden spezifischen Datensatz und jedes Problem bestimmte "inkonsistente" Wörter vorhanden sind. Nach dem Training unserer LDA der ersten Generation werden wir auch benutzerdefinierte Stoppwörter verwenden.


Das Verfahren zum Entfernen von Stoppwörtern ist in CountVectorizer integriert - wir benötigen lediglich eine Liste davon.


Ist das, was wir getan haben, genug?


Bild


Die meisten Wörter in unserem Wörterbuch sind immer noch nicht zu informativ, um LDA zu lernen, und sie sind nicht in der Liste der Stoppwörter enthalten. Daher wenden wir eine andere Filtermethode auf unsere Daten an.


idf(t,D)= log frac|D||d inD:t ind|


wo
t ist ein Wort aus dem Wörterbuch.
D - Fall (viele Texte)
d ist einer der Körpertexte.
Wir berechnen die IDF aller unserer Wörter und schneiden die Wörter mit der größten IDF (sehr selten) und mit der kleinsten (weit verbreiteten Wörter) ab.


 #'training' (tf-)idf vectorizer. tf_idf = TfidfVectorizer(input='filename', stop_words=stopwords, smooth_idf=False ) tf_idf.fit(train_names) #getting idfs idfs = tf_idf.idf_ #sorting out too rare and too common words lower_thresh = 3. upper_thresh = 6. not_often = idfs > lower_thresh not_rare = idfs < upper_thresh mask = not_often * not_rare good_words = np.array(tf_idf.get_feature_names())[mask] #deleting punctuation as well. cleaned = [] for word in good_words: word = re.sub("^(\d+\w*$|_+)", "", word) if len(word) == 0: continue cleaned.append(word) 

Das Erhalten nach den oben genannten Verfahren ist bereits für das LDA-Training gut geeignet, aber wir werden mehr Stemming betreiben - die gleichen Wörter werden häufig in unserem Datensatz gefunden, jedoch in verschiedenen Fällen. Zum Stemming wurde Pymystem3 verwendet .


 #Stemming m = Mystem() stemmed = set() voc_len = len(cleaned) for i in tqdm(range(voc_len)): word = cleaned.pop() stemmed_word = m.lemmatize(word)[0] stemmed.add(stemmed_word) stemmed = list(stemmed) print('After stemming: %d'%(len(stemmed))) 

Nach Anwendung der obigen Filterung verringerte sich die Wörterbuchgröße von 769801 auf
13611 und bereits mit solchen Daten können Sie ein LDA-Modell von akzeptabler Qualität erhalten.


Testen, Anwenden und Einstellen von LDA


Nachdem wir nun den Datensatz, die Vorverarbeitung und die Modelle haben, die wir für den verarbeiteten Datensatz trainiert haben, wäre es schön, die Angemessenheit unserer Modelle zu überprüfen und einige Anwendungen für sie zu erstellen.


Betrachten Sie als Anwendung zunächst die Aufgabe, Schlüsselwörter für einen bestimmten Text zu generieren. Sie können dies auf ziemlich einfache Weise wie folgt tun:


  1. Wir erhalten von der LDA die Verteilung der Themen für diesen Text.
  2. Wählen Sie n (zum Beispiel n = 2) der am stärksten ausgeprägten Themen.
  3. Wählen Sie für jedes Thema m (zum Beispiel m = 3) die charakteristischsten Wörter.
  4. Wir haben eine Reihe von n * m Wörtern, die einen bestimmten Text charakterisieren.

Wir werden eine einfache Schnittstellenklasse schreiben, die diese Methode zum Generieren von Schlüsselwörtern implementiert:


 #Let\`s do simple interface class class TopicModeler(object): ''' Inteface object for CountVectorizer + LDA simple usage. ''' def __init__(self, count_vect, lda): ''' Args: count_vect - CountVectorizer object from sklearn. lda - LDA object from sklearn. ''' self.lda = lda self.count_vect = count_vect self.count_vect.input = 'content' def __call__(self, text): ''' Gives topics distribution for a given text Args: text - raw text via python string. returns: numpy array - topics distribution for a given text. ''' vectorized = self.count_vect.transform([text]) lda_topics = self.lda.transform(vectorized) return lda_topics def get_keywords(self, text, n_topics=3, n_keywords=5): ''' For a given text gives n top keywords for each of m top texts topics. Args: text - raw text via python string. n_topics - int how many top topics to use. n_keywords - how many top words of each topic to return. returns: list - of m*n keywords for a given text. ''' lda_topics = self(text) lda_topics = np.squeeze(lda_topics, axis=0) n_topics_indices = lda_topics.argsort()[-n_topics:][::-1] top_topics_words_dists = [] for i in n_topics_indices: top_topics_words_dists.append(self.lda.components_[i]) shape=(n_keywords*n_topics, self.lda.components_.shape[1]) keywords = np.zeros(shape=shape) for i,topic in enumerate(top_topics_words_dists): n_keywords_indices = topic.argsort()[-n_keywords:][::-1] for k,j in enumerate(n_keywords_indices): keywords[i * n_keywords + k, j] = 1 keywords = self.count_vect.inverse_transform(keywords) keywords = [keyword[0] for keyword in keywords] return keywords 

Wir wenden unsere Methode auf mehrere Texte an und sehen, was passiert:
Community : Reisebüro "Farben der Welt"
Schlüsselwörter: ['Foto', 'sozial', 'Reisen', 'Gemeinschaft', 'Reisen', 'Euro', 'Unterkunft', 'Preis', 'Polen', 'Abreise']
Gemeinschaft: Food Gifs
Schlüsselwörter: ['Butter', 'St', 'Salz', 'PC', 'Teig', 'Kochen', 'Zwiebel', 'Pfeffer', 'Zucker', 'Gr']


Die obigen Ergebnisse sind nicht "Kirschpickel" und sehen völlig angemessen aus. Tatsächlich sind dies die Ergebnisse eines bereits konfigurierten Modells. Die ersten LDAs, die im Rahmen dieses Artikels geschult wurden, führten zu deutlich schlechteren Ergebnissen, unter anderem bei häufig verwendeten Keywords:


  1. Zusammengesetzte Komponenten von Webadressen: www, http, ru, com ...
  2. Gemeinsame Wörter.
  3. Einheiten: cm, Meter, km ...

Das Tuning (Tuning) des Modells wurde wie folgt durchgeführt:


  1. Wählen Sie für jedes Thema n (n = 5) charakteristischste Wörter aus.
  2. Wir betrachten sie je nach Schulungsfall als idf.
  3. Wir bringen 5-10% der am weitesten verbreiteten Keywords ein.

Eine solche „Reinigung“ sollte sorgfältig durchgeführt werden, wobei genau diese 10% der Wörter vorab betrachtet werden. Stattdessen sollten Kandidaten zum Löschen auf diese Weise ausgewählt werden, und dann sollten Wörter, die gelöscht werden sollen, manuell aus ihnen ausgewählt werden.


Irgendwo in der 2-3-Generation von Modellen mit einer ähnlichen Art der Auswahl von Stoppwörtern für die Top 5% der weit verbreiteten Top-Wort-Verteilungen erhalten wir:
['beliebig', 'vollständig', 'richtig', 'einfach', 'weiter', 'Internet', 'klein', 'Weg', 'schwierig', 'Stimmung', 'so viel', 'eingestellt', ' Option ',' Name ',' Rede ',' Programm ',' Wettbewerb ',' Musik ',' Ziel ',' Film ',' Preis ',' Spiel ',' System ',' Spiel ',' Firma ' , 'nett']


Weitere Anwendungen


Das erste, was mir speziell in den Sinn kommt, ist, die Verteilung von Themen im Text als "Einbettung" von Texten zu verwenden. In dieser Interpretation können Sie Visualisierungs- oder Clustering-Algorithmen auf sie anwenden und auf diese Weise nach den endgültigen "effektiven" thematischen Clustern suchen.


Machen wir das:


 term_doc_matrix = count_vect.transform(names) embeddings = lda.transform(term_doc_matrix) kmeans = KMeans(n_clusters=30) clust_labels = kmeans.fit_predict(embeddings) clust_centers = kmeans.cluster_centers_ embeddings_to_tsne = np.concatenate((embeddings,clust_centers), axis=0) tSNE = TSNE(n_components=2, perplexity=15) tsne_embeddings = tSNE.fit_transform(embeddings_to_tsne) tsne_embeddings, centroids_embeddings = np.split(tsne_embeddings, [len(clust_labels)], axis=0) 

Am Ausgang erhalten wir folgendes Bild:
Bild


Kreuze sind die Schwerpunkte (Cenroide) von Clustern.


Im tSNE-Bild von Einbettungen ist zu sehen, dass die mit KMeans ausgewählten Cluster ziemlich zusammenhängende und meist räumlich trennbare Mengen bilden.


Alles andere bis zu dir.


Link zu allen Codes: https://gitlab.com/Mozes/VK_LDA

Source: https://habr.com/ru/post/de417167/


All Articles