Luncurkan LDA di dunia nyata. Panduan lengkap

Kata Pengantar


Ada banyak tutorial di Internet yang menjelaskan cara kerja LDA (Alokasi Dirichlet Laten) dan cara menerapkannya. Contoh-contoh pelatihan LDA sering ditunjukkan pada set data "contoh", seperti "20 newsgroup dataset," yang tersedia di sklearn.


Fitur pelatihan tentang contoh dataset "contoh" adalah bahwa data selalu ada dalam urutan dan mudah ditumpuk di satu tempat. Saat melatih model produksi, data yang diperoleh langsung dari sumber nyata biasanya sebaliknya:


  • Banyak emisi.
  • Markup salah (jika ada).
  • Ketidakseimbangan kelas yang sangat kuat dan distribusi jelek dari setiap parameter dataset.
  • Untuk teks, ini adalah: kesalahan tata bahasa, sejumlah besar kata langka dan unik, multibahasa.
  • Cara penyimpanan data yang tidak nyaman (format berbeda atau jarang, perlunya penguraian)

Secara historis, saya mencoba belajar dari contoh-contoh yang sedekat mungkin dengan realitas-realitas produksi karena dengan cara inilah orang dapat sepenuhnya merasakan area masalah dari jenis tugas tertentu. Begitu juga dengan LDA, dan dalam artikel ini saya ingin berbagi pengalaman saya - bagaimana menjalankan LDA dari awal, pada data yang sepenuhnya mentah. Beberapa bagian dari artikel akan dikhususkan untuk memperoleh data ini, sehingga contohnya menjadi 'kasus rekayasa' lengkap.


Pemodelan topik dan LDA.


Untuk memulai, pertimbangkan apa yang LDA lakukan secara umum dan tugas apa yang digunakannya.
Paling sering, LDA digunakan untuk tugas Pemodelan Topik. Tugas semacam itu berarti tugas pengelompokan atau pengelompokan teks - sedemikian rupa sehingga setiap kelas atau kelompok berisi teks dengan topik yang sama.


Untuk menerapkan LDA ke dataset teks (selanjutnya disebut sebagai badan teks), perlu untuk mengubah tubuh menjadi matriks dokumen-istilah.


Matriks dokumen istilah adalah matriks yang memiliki ukuran N kaliWdimana
N adalah jumlah dokumen dalam case, dan W adalah ukuran dari kamus case i.e. jumlah kata (unik) yang ditemukan di korpus kami. Di baris ke-i, kolom ke-j dari matriks adalah angka - berapa kali dalam teks ke-i, kata ke-j ditemukan.


LDA membangun, untuk matriks dokumen Term yang diberikan dan T dari sejumlah topik yang telah ditentukan, dua distribusi:


  1. Distribusi topik dalam teks. (Dalam praktiknya, diberikan oleh ukuran matriks N kaliT)
  2. Distribusi kata berdasarkan topik (Ukuran matriks T kaliW)

Nilai-nilai sel-sel matriks ini, masing-masing, adalah probabilitas bahwa topik ini terkandung dalam dokumen ini (atau proporsi topik dalam dokumen, jika kita menganggap dokumen sebagai campuran dari berbagai topik) untuk matriks 'Distribusi topik dalam teks'.


Untuk matriks 'Distribusi kata berdasarkan tema', nilainya adalah probabilitas bertemu kata j dalam teks dengan topik i, secara kualitatif, kita dapat menganggap angka-angka ini sebagai koefisien yang mengkarakterisasi bagaimana kata ini khas untuk topik ini.


Harus dikatakan bahwa kata topik bukanlah definisi "sehari-hari" dari kata ini. LDA mengalokasikan T untuk itu, tetapi topik macam apa ini dan apakah mereka sesuai dengan topik teks yang terkenal, seperti: 'Olahraga', 'Sains', 'Politik' tidak diketahui. Dalam hal ini, lebih tepat untuk membicarakan topik sebagai semacam entitas abstrak, yang didefinisikan oleh garis dalam matriks distribusi kata berdasarkan topik dan dengan beberapa probabilitas yang sesuai dengan teks ini, jika Anda dapat membayangkannya sebagai kumpulan kumpulan kata karakteristik yang bertemu bersama dengan probabilitas yang sesuai. (dari tabel) dalam satu set teks tertentu.


Jika Anda tertarik untuk mempelajari lebih detail dan 'dalam formula' bagaimana LDA dilatih dan bekerja, berikut adalah beberapa bahan (yang digunakan oleh penulis):



Kami mendapatkan data liar


Untuk 'pekerjaan laboratorium' kami, kami membutuhkan set data khusus dengan kekurangan dan fiturnya sendiri. Anda bisa mendapatkannya di tempat yang berbeda: unduh ulasan dari Kinopoisk, artikel Wikipedia, berita dari beberapa portal berita, kami akan mengambil opsi yang sedikit lebih ekstrem - posting dari komunitas VKontakte.


Kami akan melakukan ini seperti ini:


  1. Kami memilih beberapa pengguna VK.
  2. Kami mendapatkan daftar semua teman-temannya.
  3. Untuk setiap teman, kami mengambil semua komunitasnya.
  4. Untuk setiap komunitas dari setiap teman, kami memompa keluar posting komunitas pertama n (n = 100) dan menggabungkannya menjadi satu konten teks komunitas.

Alat dan artikel


Untuk mengunduh posting kita akan menggunakan modul vk untuk bekerja dengan VKontakte API, untuk Python. Salah satu momen paling rumit ketika menulis aplikasi menggunakan VKontakte API adalah otorisasi, untungnya, kode yang melakukan pekerjaan ini sudah ditulis dan berada dalam domain publik, kecuali untuk vk, saya menggunakan modul otorisasi kecil - vkauth.


Tautan ke modul dan artikel yang digunakan untuk mempelajari VKontakte API:



Menulis kode


Jadi, menggunakan vkauth, masuk:


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

Dalam prosesnya, modul kecil ditulis berisi semua fungsi yang diperlukan untuk mengunduh konten dalam format yang sesuai, yang tercantum di bawah ini, mari kita bahas:


 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 

Pipa terakhir adalah sebagai berikut:


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

Gagal


Secara umum, proses pengunduhan data tidak sulit dengan sendirinya, Anda harus memperhatikan hanya dua hal:


  1. Terkadang, karena privasi beberapa komunitas, Anda akan menerima kesalahan akses, kadang-kadang kesalahan lain akan diselesaikan dengan menginstal coba, kecuali di tempat yang tepat.
  2. VK memiliki batasan jumlah permintaan per detik.

Saat membuat sejumlah besar permintaan, misalnya dalam satu lingkaran, kami juga akan menemukan kesalahan. Masalah ini dapat diselesaikan dengan beberapa cara:


  1. Bodoh dan blak-blakan: Tetaplah tidur (beberapa) setiap 3 permintaan. Ini dilakukan dalam satu baris dan sangat memperlambat pembongkaran, dalam situasi di mana volume data tidak besar, dan tidak ada waktu untuk metode yang lebih canggih - ini cukup dapat diterima. (Diterapkan dalam artikel ini)
  2. Memahami pekerjaan permintaan Polling Panjang https://vk.com/dev/using_longpoll

Dalam tulisan ini, metode yang sederhana dan lambat dipilih, di masa depan, saya mungkin akan menulis artikel mikro tentang cara untuk memotong atau mengurangi pembatasan jumlah permintaan per detik.


Ringkasan


Dengan "beberapa" pengguna memiliki ~ 150 teman, mereka berhasil mendapatkan 4.679 teks - masing-masing mencirikan komunitas VK tertentu. Ukuran teks sangat bervariasi dan ditulis dalam banyak bahasa - beberapa di antaranya tidak cocok untuk tujuan kita, tetapi kita akan membicarakannya sedikit lebih jauh.


Tubuh utama


gambar


Mari kita pergi melalui semua blok pipa kita - pertama, pada mandatory (Ideal), kemudian pada sisanya - mereka hanya dari kepentingan terbesar.


Countvectorizer


Sebelum mengajar LDA, kita perlu mempresentasikan dokumen kita dalam bentuk matriks dokumen Jangka. Ini biasanya termasuk operasi seperti:


  • Menghapus puttuctions / angka / token yang tidak perlu.
  • Tokenisasi (presentasi sebagai daftar kata)
  • Menghitung kata-kata, menyusun matriks dokumen termal.

Semua tindakan ini di sklearn diimplementasikan dengan mudah dalam kerangka satu entitas program - sklearn.feature_extraction.text.CountVectorizer.


Tautan Dokumentasi


Yang perlu Anda lakukan adalah:


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

Lda


Demikian pula dengan CountVectorizer, LDA diimplementasikan dengan sempurna di Sklearn dan kerangka kerja lainnya, oleh karena itu, tidak ada gunanya mencurahkan banyak ruang secara langsung untuk implementasinya, dalam artikel kami yang sepenuhnya praktis.


Tautan Dokumentasi


Yang Anda butuhkan untuk memulai LDA adalah:


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

Preprocessing


Jika kami hanya mengambil teks kami segera setelah mengunduhnya dan mengubahnya menjadi matriks dokumen-Term menggunakan CountVectorizer, dengan tokenizer bawaan bawaan, kami akan mendapatkan matriks ukuran 4679x769801 (pada data yang saya gunakan).


Ukuran kamus kami adalah 769801. Bahkan jika kami berasumsi bahwa sebagian besar kata-kata informatif, kami masih tidak mungkin untuk mendapatkan LDA yang baik, sesuatu seperti "Kutukan Dimensi" menunggu kami, belum lagi bahwa untuk hampir semua komputer, kami hanya akan menyumbat semua RAM. Sebenarnya, sebagian besar kata-kata ini sama sekali tidak informatif. Sebagian besar dari mereka adalah:


  • Emoticon, karakter, angka.
  • Kata-kata unik atau sangat langka (misalnya, kata-kata Polandia dari grup dengan meme Polandia, kata-kata dieja salah atau dalam 'Albania').
  • Bagian bicara yang sangat sering (mis. Preposisi dan kata ganti).

Selain itu, banyak grup di VK yang berspesialisasi secara khusus dalam gambar - hampir tidak ada tulisan di sana - teks yang terkait dengannya merosot, dalam matriks dokumen Thermal, mereka akan memberi kita hampir nol garis.


Jadi, mari kita selesaikan semuanya!
Kami tokenize semua teks, hapus tanda baca dan angka dari mereka, lihat histogram distribusi teks dengan jumlah kata:
gambar


Kami menghapus semua teks yang lebih kecil dari 100 kata (ada 525 di antaranya)


Sekarang kamus:
Menghapus semua token (kata) yang bukan huruf, dalam kerangka tugas kita - ini cukup dapat diterima. CountVectorizer melakukan ini sendiri, bahkan jika tidak, maka saya pikir tidak perlu memberikan contoh di sini (mereka berada dalam versi lengkap dari kode untuk artikel).


Salah satu prosedur paling umum untuk mengurangi ukuran kamus adalah dengan menghapus apa yang disebut stopwords (stopwords) - kata-kata yang tidak membawa muatan semantik dan / atau tidak memiliki pewarnaan tematik (dalam kasus kami, Pemodelan Topik). Kata-kata seperti itu dalam kasus kami adalah, misalnya:


  • Ucapan dan preposisi.
  • Artikel -,, a.
  • Kata-kata umum: 'menjadi', 'baik', 'mungkin', dll.

Modul nltk telah membentuk daftar stopword dalam bahasa Rusia dan Inggris, tetapi mereka agak lemah. Di Internet, Anda juga dapat menemukan daftar stopword untuk bahasa apa pun dan menambahkannya ke yang ada di nltk. Jadi kita akan lakukan. Ambil stopwords tambahan dari sini:



Dalam praktiknya, ketika memecahkan masalah tertentu, daftar kata kunci secara bertahap disesuaikan dan ditambah sebagai model dilatih, karena untuk setiap dataset tertentu dan masalah ada kata-kata "tidak konsisten" spesifik. Kami juga akan mengambil kata kunci khusus setelah melatih LDA generasi pertama kami.


Dengan sendirinya, prosedur untuk menghapus stopword dibangun ke dalam CountVectorizer - kita hanya perlu daftar mereka.


Apakah yang sudah kita lakukan cukup?


gambar


Sebagian besar kata-kata yang ada di kamus kami masih tidak terlalu informatif untuk mempelajari LDA pada mereka dan tidak ada dalam daftar kata kunci. Oleh karena itu, kami menerapkan metode penyaringan lain ke data kami.


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


dimana
t adalah kata dari kamus.
Kasing D (banyak teks)
d adalah salah satu teks tubuh.
Kami menghitung IDF dari semua kata kami, dan memotong kata-kata dengan idf terbesar (sangat jarang) dan dengan yang terkecil (kata-kata luas).


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

Diperoleh setelah prosedur di atas sudah cukup cocok untuk pelatihan LDA, tetapi kami akan melakukan lebih banyak stemming - kata-kata yang sama sering ditemukan dalam dataset kami, tetapi dalam kasus yang berbeda. Untuk membendung, pymystem3 digunakan .


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

Setelah menerapkan pemfilteran di atas, ukuran kamus menurun dari 769801 ke
13611 dan sudah dengan data seperti itu, Anda bisa mendapatkan model LDA dengan kualitas yang dapat diterima.


Menguji, menerapkan, dan menyetel LDA


Sekarang kita memiliki dataset, preprocessing dan model yang kita latih pada dataset yang diproses, akan lebih baik untuk memeriksa kecukupan model kita, serta membangun beberapa aplikasi untuk mereka.


Sebagai aplikasi, sebagai permulaan, pertimbangkan tugas menghasilkan kata kunci untuk teks yang diberikan. Anda dapat melakukan ini dengan cara yang cukup sederhana sebagai berikut:


  1. Kami mendapatkan dari LDA distribusi topik untuk teks ini.
  2. Pilih n (misalnya, n = 2) dari topik yang paling jelas.
  3. Untuk setiap topik, pilih m (misalnya m = 3) kata yang paling khas.
  4. Kami memiliki serangkaian kata-kata n * m yang mengkarakterisasi teks yang diberikan.

Kami akan menulis kelas antarmuka sederhana yang akan menerapkan metode menghasilkan kata kunci ini:


 #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 

Kami menerapkan metode kami pada beberapa teks dan melihat apa yang terjadi:
Komunitas : Agen Perjalanan "Warna Dunia"
Kata kunci: ['foto', 'sosial', 'perjalanan', 'komunitas', 'perjalanan', 'euro', 'akomodasi', 'harga', 'Polandia', 'keberangkatan']
Komunitas: Gif Makanan
Kata kunci: ['mentega', 'st', 'garam', 'pc', 'adonan', 'memasak', 'bawang', 'lada', 'gula', 'gr']


Hasil di atas bukan 'cherry pick' dan terlihat cukup memadai. Bahkan, ini adalah hasil dari model yang sudah dikonfigurasi. LDA pertama yang dilatih sebagai bagian dari artikel ini menghasilkan hasil yang jauh lebih buruk, di antara kata kunci yang sering Anda lihat, misalnya:


  1. Komponen komposit alamat web: www, http, ru, com ...
  2. Kata-kata umum
  3. unit: cm, meter, km ...

Penyetelan (tuning) model dilakukan sebagai berikut:


  1. Untuk setiap topik, pilih n (n = 5) kata paling khas.
  2. Kami menganggap mereka idf, sesuai dengan kasus pelatihan.
  3. Kami mendatangkan kata kunci 5-10% yang paling luas.

"Pembersihan" seperti itu harus dilakukan dengan hati-hati, sebelum melihat 10% dari kata-kata itu. Sebaliknya, kandidat untuk dihapus harus dipilih dengan cara ini, dan kemudian kata-kata yang harus dihapus dari mereka harus dipilih secara manual.


Di suatu tempat dalam generasi 2-3 model, dengan cara yang sama memilih stopwords, untuk 5% teratas dari distribusi kata-top yang tersebar luas, kita mendapatkan:
['any', 'sepenuhnya', 'benar', 'mudah', 'berikutnya', 'internet', 'kecil', 'cara', 'sulit', 'suasana hati', 'sangat banyak', 'set', ' opsi ',' nama ',' pidato ',' program ',' kompetisi ',' musik ',' target ',' film ',' harga ',' permainan ',' sistem ',' permainan ',' permainan ',' perusahaan ' , 'bagus']


Lebih banyak aplikasi


Hal pertama yang muncul di benak saya secara khusus adalah menggunakan distribusi topik dalam teks sebagai 'embeddings' teks, dalam interpretasi ini Anda dapat menerapkan algoritma visualisasi atau pengelompokan untuk mereka, dan mencari cluster tematik 'efektif' terakhir dengan cara ini.


Mari kita lakukan ini:


 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) 

Pada output, kita mendapatkan gambar berikut:
gambar


Persilangan adalah pusat gravitasi (cenroid) dari kelompok.


Pada gambar tSNE dari embeddings, dapat dilihat bahwa cluster yang dipilih menggunakan KMeans cukup terhubung dan paling sering set terpisah secara spasial.


Yang lainnya, terserah Anda.


Tautan ke semua kode: https://gitlab.com/Mozes/VK_LDA

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


All Articles