Lanzar LDA en el mundo real. Guía detallada

Prólogo


Hay muchos tutoriales en Internet que explican cómo funciona el LDA (Asignación de Dirichlet Latente) y cómo ponerlo en práctica. Los ejemplos de capacitación en LDA a menudo se demuestran en conjuntos de datos "ejemplares", como el "conjunto de datos de 20 grupos de noticias", que está disponible en sklearn.


Una característica de la capacitación sobre el ejemplo de conjuntos de datos "ejemplares" es que los datos están siempre en orden y convenientemente apilados en un solo lugar. Al entrenar modelos de producción, los datos obtenidos directamente de fuentes reales suelen ser lo contrario:


  • Muchas emisiones.
  • Marcado incorrecto (si lo hay).
  • Desequilibrios de clase muy fuertes y distribuciones feas de cualquier parámetro del conjunto de datos.
  • Para los textos, estos son: errores gramaticales, una gran cantidad de palabras raras y únicas, multilingüismo.
  • Una forma inconveniente de almacenar datos (formatos diferentes o raros, la necesidad de analizar)

Históricamente, trato de aprender de ejemplos que están lo más cerca posible de las realidades de la realidad de producción porque es de esta manera que uno puede sentir las áreas problemáticas de un tipo particular de tarea. Así fue con la LDA, y en este artículo quiero compartir mi experiencia: cómo ejecutar LDA desde cero, en datos completamente sin procesar. Parte del artículo se dedicará a obtener estos mismos datos, de modo que el ejemplo se convierta en un "caso de ingeniería" completo.


Modelado de temas y LDA.


Para comenzar, considere qué hace el LDA en general y qué tareas usa.
Muy a menudo, LDA se utiliza para tareas de modelado de temas. Tales tareas significan las tareas de agrupar o clasificar textos, de tal manera que cada clase o grupo contenga textos con temas similares.


Para aplicar LDA al conjunto de datos de textos (en lo sucesivo, el cuerpo del texto), es necesario transformar el cuerpo en una matriz de documento de término.


Un término matriz de documento es una matriz que tiene un tamaño N vecesWdonde
N es el número de documentos en el caso, y W es el tamaño del diccionario del caso, es decir El número de palabras (únicas) que se encuentran en nuestro corpus. En la fila i-ésima, la columna j-ésima de la matriz es un número: cuántas veces en el texto i-ésimo se encontró la palabra j-ésima.


El LDA construye, para una matriz de documento de Término dado y T de un número predeterminado de temas, dos distribuciones:


  1. La distribución de temas en los textos (en la práctica, dada por la matriz de tamaño N vecesT)
  2. La distribución de palabras por tema. (Matriz de tamaño T vecesW)

Los valores de las celdas de estas matrices son, respectivamente, las probabilidades de que este tema esté contenido en este documento (o la proporción del tema en el documento, si consideramos el documento como una mezcla de diferentes temas) para la matriz 'Distribución de temas en textos'.


Para la matriz 'Distribución de palabras por temas', los valores son la probabilidad de encontrar la palabra j en el texto con el tema i, cualitativamente, podemos considerar estos números como coeficientes que caracterizan cómo esta palabra es típica para este tema.


Debe decirse que la palabra tema no es una definición "cotidiana" de esta palabra. La LDA asigna T a esos, pero se desconoce qué tipo de temas son estos y si corresponden a algún tema de texto conocido, como: 'Deporte', 'Ciencia', 'Política'. En este caso, es más apropiado hablar sobre el tema como una especie de entidad abstracta, que se define por una línea en la matriz de distribución de palabras por tema y con cierta probabilidad corresponde a este texto, si puede imaginarlo como una familia de conjuntos de palabras características que se juntan con las probabilidades correspondientes (de la tabla) en cierto conjunto de textos.


Si está interesado en estudiar con más detalle y 'en fórmulas' cómo se capacita y trabaja el LDA, aquí hay algunos materiales (que fueron utilizados por el autor):



Obtenemos datos salvajes


Para nuestro 'trabajo de laboratorio', necesitamos un conjunto de datos personalizado con sus propios defectos y características. Puede obtenerlo en diferentes lugares: descargue reseñas de Kinopoisk, artículos de Wikipedia, noticias de algún portal de noticias, tomaremos una opción un poco más extrema: publicaciones de las comunidades VKontakte.


Haremos esto así:


  1. Seleccionamos algún usuario de VK.
  2. Tenemos una lista de todos sus amigos.
  3. Para cada amigo, tomamos toda su comunidad.
  4. Para cada comunidad de cada amigo, extraemos las primeras n (n = 100) publicaciones de la comunidad y las combinamos en un contenido de texto de la comunidad.

Herramientas y articulos


Para descargar publicaciones, utilizaremos el módulo vk para trabajar con la API VKontakte, para Python. Uno de los momentos más intrincados al escribir una aplicación usando la API de VKontakte es la autorización, afortunadamente, el código que realiza este trabajo ya ha sido escrito y está en el dominio público, excepto vk, utilicé un pequeño módulo para autorización: vkauth.


Enlaces a los módulos y artículos utilizados para estudiar la API de VKontakte:



Escribir un código


Y así, usando vkauth, inicie sesión:


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

En el proceso, se escribió un pequeño módulo que contiene todas las funciones necesarias para descargar contenido en el formato apropiado, se enumeran a continuación, veamos:


 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 

La tubería final es la siguiente:


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

Falla


En general, el proceso de descarga de datos no es difícil en sí mismo; debe prestar atención solo a dos puntos:


  1. A veces, debido a la privacidad de algunas comunidades, recibirá errores de acceso, a veces otros errores se resolverán instalando try, excepto en el lugar correcto.
  2. VK tiene un límite en el número de solicitudes por segundo.

Al realizar una gran cantidad de solicitudes, por ejemplo en un bucle, también detectaremos errores. Este problema se puede resolver de varias maneras:


  1. Estúpidamente y sin rodeos: Stick sleep (algunos) cada 3 solicitudes. Se realiza en una línea y ralentiza enormemente la descarga, en situaciones en las que los volúmenes de datos no son grandes y no hay tiempo para métodos más sofisticados, esto es bastante aceptable (implementado en este artículo)
  2. Comprender el trabajo de las solicitudes de Long Poll https://vk.com/dev/using_longpoll

En este documento, se eligió un método simple y lento, en el futuro, probablemente escribiré un micro artículo sobre formas de evitar o aliviar las restricciones en el número de solicitudes por segundo.


Resumen


Con el usuario inicial "algunos" que tiene ~ 150 amigos, lograron obtener 4,679 textos, cada uno de los cuales caracteriza una determinada comunidad de VK. Los textos varían mucho en tamaño y están escritos en muchos idiomas; algunos de ellos no son adecuados para nuestros propósitos, pero hablaremos un poco más sobre esto.


Cuerpo principal


imagen


Repasemos todos los bloques de nuestra tubería, primero, en el obligatorio (Ideal), luego en el resto, son de gran interés.


Countvectorizer


Antes de enseñar LDA, debemos presentar nuestros documentos en forma de matriz de documentos a plazo. Esto generalmente incluye operaciones como:


  • Eliminar puttucciones / números / fichas innecesarias.
  • Tokenización (presentación como una lista de palabras)
  • Contando palabras, compilando una matriz de documentos térmicos.

Todas estas acciones en sklearn se implementan convenientemente en el marco de una entidad de programa: sklearn.feature_extraction.text.CountVectorizer.


Enlace de documentación


Todo lo que necesitas hacer es:


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

Lda


De manera similar con CountVectorizer, LDA se implementa perfectamente en Sklearn y otros marcos, por lo tanto, no tiene mucho sentido dedicar mucho espacio directamente a sus implementaciones, en nuestro artículo puramente práctico.


Enlace de documentación


Todo lo que necesitas para iniciar LDA es:


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

Preprocesamiento


Si solo tomamos nuestros textos inmediatamente después de descargarlos y convertirlos en una matriz de documentos a plazo utilizando el CountVectorizer, con el tokenizador predeterminado incorporado, obtendremos una matriz de tamaño 4679x769801 (en los datos que uso).


El tamaño de nuestro diccionario será de 769801. Incluso si suponemos que la mayoría de las palabras son informativas, es poco probable que obtengamos una buena LDA, encontraremos algo así como 'Maldiciones de dimensiones', sin mencionar que para casi cualquier computadora, simplemente obstruiremos toda la RAM. De hecho, la mayoría de estas palabras son completamente informativas. La gran mayoría de ellos son:


  • Emoticones, personajes, números.
  • Palabras únicas o muy raras (por ejemplo, palabras polacas de un grupo con memes polacos, palabras escritas incorrectamente o en 'albanés').
  • Partes del habla muy frecuentes (por ejemplo, preposiciones y pronombres).

Además, muchos grupos en VK se especializan exclusivamente en imágenes (casi no hay mensajes de texto allí), los textos correspondientes a ellos son degenerados, en la matriz de documentos térmicos nos darán casi cero líneas.


Y así, ¡solucionémoslo todo!
Tokenizamos todos los textos, eliminamos la puntuación y los números de ellos, miramos el histograma de la distribución de textos por el número de palabras:
imagen


Eliminamos todos los textos de menos de 100 palabras (hay 525 de ellos)


Ahora el diccionario:
Eliminar todos los tokens (palabras) que no son letras, en el marco de nuestra tarea, esto es bastante aceptable. CountVectorizer hace esto por sí solo, incluso si no es así, creo que no hay necesidad de dar ejemplos (están en la versión completa del código del artículo).


Uno de los procedimientos más comunes para reducir el tamaño de un diccionario es eliminar las llamadas palabras vacías (palabras vacías), palabras que no llevan una carga semántica y / o no tienen color temático (en nuestro caso, Modelado de temas). Tales palabras en nuestro caso son, por ejemplo:


  • Pronombres y preposiciones.
  • Artículos - el, a.
  • Palabras comunes: 'ser', 'bueno', 'probablemente', etc.

El módulo nltk ha formado listas de palabras vacías en ruso e inglés, pero son bastante débiles. En Internet, también puede encontrar listas de palabras vacías para cualquier idioma y agregarlas a las de nltk. Entonces lo haremos. Tome pausas adicionales desde aquí:



En la práctica, al resolver problemas específicos, las listas de palabras vacías se ajustan gradualmente y se complementan a medida que se entrena a los modelos, ya que para cada conjunto de datos y problema específicos hay palabras específicas "inconsistentes". También recogeremos palabras clave personalizadas después de entrenar a nuestra LDA de primera generación.


Por sí solo, el procedimiento para eliminar las palabras vacías está integrado en CountVectorizer; solo necesitamos una lista de ellas.


¿Es suficiente lo que hemos hecho?


imagen


La mayoría de las palabras que están en nuestro diccionario todavía no son demasiado informativas para aprender LDA sobre ellas y no están en la lista de palabras clave. Por lo tanto, aplicamos otro método de filtrado a nuestros datos.


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


donde
t es una palabra del diccionario.
D - caso (muchos textos)
d es uno de los textos del cuerpo.
Calculamos el IDF de todas nuestras palabras, y cortamos las palabras con el idf más grande (muy raro) y con el más pequeño (palabras extendidas).


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

Obtenido después de los procedimientos anteriores, ya es bastante adecuado para la capacitación de LDA, pero haremos más derivaciones: las mismas palabras se encuentran a menudo en nuestro conjunto de datos, pero en diferentes casos. Para la derivación, se utilizó pymystem3 .


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

Después de aplicar el filtro anterior, el tamaño del diccionario disminuyó de 769801 a
13611 y ya con dichos datos, puede obtener un modelo LDA de calidad aceptable.


Probar, aplicar y ajustar LDA


Ahora que tenemos el conjunto de datos, el preprocesamiento y los modelos que capacitamos en el conjunto de datos procesados, sería bueno verificar la idoneidad de nuestros modelos, así como crear algunas aplicaciones para ellos.


Como aplicación, para empezar, considere la tarea de generar palabras clave para un texto dado. Puede hacer esto de una manera bastante simple de la siguiente manera:


  1. Obtenemos de LDA la distribución de temas para este texto.
  2. Elija n (por ejemplo, n = 2) de los temas más pronunciados.
  3. Para cada tema, elija m (por ejemplo m = 3) las palabras más características.
  4. Tenemos un conjunto de n * m palabras que caracterizan un texto dado.

Escribiremos una clase de interfaz simple que implementará este método de generación de palabras clave:


 #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 

Aplicamos nuestro método a varios textos y vemos qué sucede:
Comunidad : Agencia de viajes "Colores del mundo"
Palabras clave: ['foto', 'social', 'viaje', 'comunidad', 'viaje', 'euro', 'alojamiento', 'precio', 'Polonia', 'salida']
Comunidad: Gifs de comida
Palabras clave: ['mantequilla', 'st', 'sal', 'pc', 'masa', 'cocción', 'cebolla', 'pimienta', 'azúcar', 'gr']


Los resultados anteriores no son 'selección de cereza' y parecen bastante adecuados. De hecho, estos son los resultados de un modelo ya configurado. Los primeros LDA que se formaron como parte de este artículo produjeron resultados significativamente peores, entre las palabras clave que a menudo se pueden ver, por ejemplo:


  1. Componentes compuestos de direcciones web: www, http, ru, com ...
  2. Palabras comunes
  3. unidades: cm, metro, km ...

El ajuste (ajuste) del modelo se realizó de la siguiente manera:


  1. Para cada tema, seleccione n (n = 5) palabras más características.
  2. Los consideramos idf, según el caso de capacitación.
  3. Traemos palabras clave del 5 al 10% de las más difundidas.

Tal "limpieza" debe llevarse a cabo con cuidado, visualizando previamente el 10% de las palabras. Por el contrario, los candidatos para la eliminación deben elegirse de esta manera, y luego las palabras que deben eliminarse deben seleccionarse manualmente de ellos.


En algún lugar de la generación 2-3 de modelos, con una forma similar de seleccionar palabras vacías, para el 5% superior de las distribuciones de palabras principales generalizadas, obtenemos:
['any', 'completamente', 'right', 'easy', 'next', 'internet', 'small', 'way', 'difficult', 'mood', 'so much', 'set', ' opción ',' nombre ',' discurso ',' programa ',' competencia ',' música ',' objetivo ',' película ',' precio ',' juego ',' sistema ',' juego ',' compañía ' "agradable"]


Más aplicaciones


Lo primero que me viene a la mente específicamente es usar la distribución de temas en el texto como 'incrustaciones' de textos, en esta interpretación puede aplicarles algoritmos de visualización o agrupación, y buscar los grupos temáticos 'efectivos' finales de esta manera.


Hagamos esto:


 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) 

En la salida, obtenemos la siguiente imagen:
imagen


Las cruces son los centros de gravedad (cenroides) de los grupos.


En la imagen tSNE de incrustaciones, se puede ver que los clústeres seleccionados usando KMeans forman conjuntos bastante conectados y más a menudo separables espacialmente.


Todo lo demás, depende de ti.


Enlace a todo el código: https://gitlab.com/Mozes/VK_LDA

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


All Articles