テキストマイニングへの道04――トピックモデルとの格闘2
はじめに
前回の続き。
前回に引き続きジュピターノートブックで作業している。
今回はwindowsであるからこその間違い(バックラッシュか円マークかスラッシュかなど)があるかと思ったが、そんなことはなく実行するディレクトリを変える必要があったことが問題だった。
MeCad
まず、MeCad自身はトピックモデルとは関係なく、形態素分析をするためのものである。
今回はMeCadと辞書について格闘したのでその記録を残す。
MeCadの辞書として、固有名詞などを含むNEologdという辞書がいいとみんないっているので、それをダウンロードしたい。
最終的には、次の二つのサイトを参考にした。
resanaplaza.com
上のサイトは、辞書の作成の前までを特に参考にした。
qd-suriken.com
ここはUFT-8形式辞書のコンパイルを特に参考にした。
一番苦戦したのは、辞書の作成という段階。
最終的には次のような手順で行った。
1. 実行するディレクトリを変える
実行するためのディレクトリを mecab-dict-index.exeが存在する、"C:\Program Files (x86)\MeCab\bin”まで移動する必要があることが長いこと分からなくて、悪戦苦闘していた。ディレクトリの移動はosを使う。例えば、pythonでこんな感じ。
ちなみに、Winodowsのパスの表記は基本的にはバックスラッシュ=円マークらしいが、スラッシュでも機能するらしいので、スラッシュで書いている。バックスラッシュはpythonでは何か別の意味に読み取られてしまうらしいので注意が必要だそう。
2. 辞書の作成
"C:/Program Files (x86)/MeCab/bin"の中にあるmecab-dict-index.exeを使うことで、辞書をcsvファイルからMeCabが使えるファイルに変えることができるようだ。これを起動させるには、下のスクリプトのように、subprocessというものを使う必要があるみたいだ。
- dの後は、どこに辞書がありますか?という話だと思われる。ここを下のように"C:/Program Files (x86)/MeCab/dic/ipadic"と変えることで上手くいった。-uの後にどういう名前で作るか、-tの後でどこからとってくるかだと思われる。自分はデスクトップにcsvファイルを解凍した。
import subprocess
command = 'mecab-dict-index -d "C:\Program Files (x86)\MeCab\dic\ipadic-UTF8" -u NEologd.20200910-u.dic -f utf-8 -t utf-8 mecab-user-dict-seed.20200910.csv'
subprocess.run(command, shell=True)
”***”はユーザー名。
WindowsでMeCab+NEologdをインストールする | すーりけんでは、mecab-ipadic-neologdのseedに移動することを勧めているが、自分はMeCabのbinで行った。それは、mecab-dict-indexがそこにあるから。逆に、解凍してでき上がったmecab-user-dict-seed.20200910.csvをbinに移した。そうすると、よくわからないところにデータができたので、それをNEologd.20200910-u.dicという名前で検索して、探し出して、MeCabのdicにNeologedのフォルダを作って移した。
辞書フォルダの設定(UTF-8)において、ファイルのセキュリティの設定が大変だったので、記録しておく。
ちなみに、自分のmecabrc-uの中身はこんな感じ。
;
; Configuration file of MeCab
;
; $Id: mecabrc.in,v 1.3 2006/05/29 15:36:08 taku-ku Exp $;
;
dicdir = $(rcpath)\..\dic\ipadic; userdic = /home/foo/bar/user.dic
; output-format-type = wakati
; input-buffer-size = 8192; node-format = %m\n
; bos-format = %S\n
; eos-format = EOS\ndicdir = C:\Program Files (x86)\MeCab\dic\ipadic-UTF8
userdic = C:\Program Files (x86)\MeCab\dic\NEologd\Neologd.20200910-u.dic
ファイルetc→プロパティ→セキュリティ→Users→フルコントロールにチェックをいれる
とできた。しかし、その前にいろいろいじったから、再現性は低いかも…?
最終的にこれをスペースがないところに入れた。
ここまで完成すると、ジュピターノートブックで次のスクリプトを実行することができる。
import MeCab
m = MeCab.Tagger("-r C:/MeCab/mecabrc-u")
print(m.parse("これは例文です。好きな文章を入れて見て下さい。"))
トピックモデル
やっとトピックモデル本体のスクリプトをいじることができた。
今回は、次の記事を参考にした。
zenn.dev
変更箇所とかいたところはそれぞれの環境によって変更する必要がある箇所であると思う。
stopword1.txtというファイルは、事前に http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txtをテキストファイルにコピペして色々編集した。特にジェンダーに関する言葉をストップワード(よく使われて、ほとんど意味がない単語のこと)としないように編集した。
最初にimportするライブラリについては、pip installで準備が必要である。
変更するべき箇所については、下でまとめておいた。
ちなみに、それぞれのトップの単語を表示する機能はまだ使えていない。
#pandasはデータフレーム操作、MeCabは日本語形態素解析ライブラリ、reは正規表現ライブラリ、CountVectorizerは文書の単語の出現頻度をカウントするためのツール、gensimはトピックモデリングライブラリ、tqdmは進捗バー表示用ライブラリ、numpyは数値計算用ライブラリです。
import pandas as pd
import MeCab
import re
import pyLDAvis
import pyLDAvis.gensim_models as gensimvis
from sklearn.feature_extraction.text import CountVectorizer
from gensim.corpora.dictionary import Dictionary
from gensim.models import LdaModel
from tqdm import tqdm
import numpy as np#csvファイルを読み込んで、pandasのデータフレームとして扱っています。ファイル名は"transexcluded20221028test.csv"で、最初の列をインデックスとして扱っています。reset_index()を用いて、インデックスを再設定しています
#変更箇所
df = pd.read_csv("transexcluded20221028test.csv",index_col=0).reset_index()
#stopwordsの指定
with open("stopword1.txt","r", encoding="utf-8" ) as f:
stopwords = f.read().split("\n")#Neologdによるトーカナイザー(リストで返す関数・名詞のみ)
def mecab_tokenizer(text):replaced_text = text.lower()
replaced_text = re.sub(r'[【】]', ' ', replaced_text) # 【】の除去
replaced_text = re.sub(r'[()()]', ' ', replaced_text) # ()の除去
replaced_text = re.sub(r'[[]\[\]]', ' ', replaced_text) # []の除去
replaced_text = re.sub(r'\d+\.*\d*', '', replaced_text) #数字を0にするpath = "-r C:/MeCab/mecabrc-u"
mecab = MeCab.Tagger(path)
parsed_lines = mecab.parse(replaced_text).split("\n")[:-2]
# #表層形を取得
# surfaces = [l.split('\t')[0] for l in parsed_lines]
#原形を取得 変更箇所
token_list = [l.split("\t")[3] for l in parsed_lines]
#品詞を取得 変更箇所
pos = [l.split("\t")[4].split("-")[0] for l in parsed_lines]
# 名詞,動詞,形容詞のみに絞り込み 変更箇所
target_pos = ["名詞", "動詞", "形容詞"]
token_list = [t for t, p in zip(token_list, pos) if p in target_pos]
# stopwordsの除去
token_list = [t for t in token_list if t not in stopwords]
# ひらがなのみの単語を除く
kana_re = re.compile("^[ぁ-ゖ]+$")
token_list = [t for t in token_list if not kana_re.match(t)]
return token_list
#df全体に対してmecab_tokenizerを適用し、形態素解析を行なったリストを返す関数
def make_docs(df,column_number):
docs=
for i in range(len(df)):
text = df.iloc[i,column_number]
docs.append(mecab_tokenizer(text))
return docs#形態素解析の実行 変更箇所 数字で何列目を分析するか選ぶ。今回は一列目がテキストだったので、0、変更箇所
d = make_docs(df,0)#辞書の作成 gensimの「Dictionary」クラスを用いて、形態素解析された文書群「docs_keiei_2203_lda」から、単語の辞書を作成します。
dictionary = Dictionary(d)
#出現がx文書に満たない単語と、y%以上の文書に出現する単語を極端と見做し削除する 上記で作成された単語の辞書から、「no_below」で指定した文書数より出現頻度の低い単語、「no_above」で指定した割合以上の文書に出現する高頻度の単語を除外します。
#変更箇所
x =5
y =0.5
dictionary.filter_extremes(no_below=x,no_above=y)
# LdaModelが読み込めるBoW形式に変換 各文書を単語の出現回数を表すBoW(Bag of Words)形式に変換し、「corpus」というリストに格納します。
corpus = [dictionary.doc2bow(text) for text in d]print(f"Number of unique tokens: {len(dictionary)}")
print(f"Number of documents: {len(corpus)}")#gensimの「LdaModel」クラスを用いて、トピック数「num_topics」を指定し、BoW形式の文書群「corpus」と辞書「dictionary」からトピックモデルを作成します。「alpha」はディリクレ事前分布のハイパーパラメータで、デフォルト値は「1.0/num_topics」となっています。
#変更箇所
num_topics =8
lda = LdaModel(corpus, id2word =dictionary, num_topics=num_topics, alpha=0.01)#作成されたトピックモデルから、各トピックに属する上位の単語を取り出して、Pandasのデータフレームにまとめます。「get_topic_terms()」関数で各トピックの単語の確率を取得し、「topn=15」で確率の高い上位15単語を抽出します。「id2token」で単語IDを単語に変換し、「append()」でリスト「word」に格納します。それをPandasのデータフレームに追加し、「df.T」で転置したデータフレームを出力します。
df =pd.DataFrame() #からのデータフレーム
for t in range(num_topics): #トピック数だけ繰り返せ
word=
for i, prob in lda.get_topic_terms(t, topn=15): #ここで、lda.get_topic_terms(t, topn=15)は、t番目のトピックに属する上位15単語とその単語の確率を返す関数です。取得された単語と確率は、リストwordに格納されます。
# lda.get_topic_terms(t, topn=15)は、トピックtに属する上位15単語とその単語の出現確率を返す関数です。for i, prob in lda.get_topic_terms(t, topn=15):で、単語のidとその単語の出現確率が順番に取り出されます。ここで、iは単語のidを表し、int(i)で整数型に変換しています。
word.append(dictionary.id2token[int(i)]) #dictionary.id2tokenは、単語のidから単語そのものへと変換する辞書です。そのため、dictionary.id2token[int(i)]は、idがiである単語を表します。この単語をwordリストに追加することで、トピックtに属する上位15単語がwordリストに格納されます。
_ = pd.DataFrame([word],index=[f'topic{t+1}']) #リストwordを1行のデータフレームに変換し、インデックスに'topic{t+1}'を指定します。
df = df.append(_) # データフレーム_を、空のデータフレームdfに追加します。
df.T #最後に、列と行を入れ替えたデータフレームを出力します。
#PyLDAvisの実装
pyLDAvis.enable_notebook()vis = gensimvis.prepare(
lda, corpus, dictionary, n_jobs = 1, sort_topics = False
)pyLDAvis.display(vis)
それぞれの環境で変更すべき箇所については以下の通り。
1. df = pd.read_csv("transexcluded20221028test.csv",index_col=0).reset_index()
df = pd.read_ファイルの形式("ファイルの名前.csv",index_col=一行目が項目名の場合は0).reset_index()
2. with open("stopword1.txt","r", encoding="utf-8" ) as f:
with open("ストップワードのテキストのファイルの名前.txt","r", encoding="utf-8" ) as f:
ただし、ストップワードのテキストは、実行するフォルダにいれておくことを前提としている。違うファイルに入れているならば、パスを入力すること。
3. replaced_textまわり。
自分はメンションを残したいのでメンションの除去をしていないので、メンションの除去をしたい場合は、下のものに置き換えるべき。
replaced_text = re.sub(r'[【】]', ' ', replaced_text) # 【】の除去
replaced_text = re.sub(r'[()()]', ' ', replaced_text) # ()の除去
replaced_text = re.sub(r'[[]\[\]]', ' ', replaced_text) # []の除去
replaced_text = re.sub(r'[@@]\w+', '', replaced_text) # メンションの除去
replaced_text = re.sub(r'\d+\.*\d*', '', replaced_text) #数字を0にする
4. path = "-r C:/MeCab/mecabrc-u"
path = "-r 自分の設定ファイルがどこにあるか"
5. token_list = [l.split("\t")[3] for l in parsed_lines]
原形の取得のところ。 token_list = [l.split("\t")[使っている辞書によって、何番目に原形が入っているかを確認し、そこから1を引いた数] for l in parsed_lines]
ちなみに、自分は最終的に次のスクリプトを試して、原形が入るかどうかを確認した。
import re
import MeCabtext = 'なんでもいいので、ここに例文を書きこんでいろいろ試してみてください。除去が成功しているか試すためには、【26】とか(abd)とか試すといいです。'
replaced_text = text.lower()
replaced_text = re.sub(r'[【】]', '', replaced_text) # 【】の除去
replaced_text = re.sub(r'[()()]', '', replaced_text) # ()の除去
replaced_text = re.sub(r'[[]\[\]]', '', replaced_text) # []の除去
replaced_text = re.sub(r'\d+\.*\d*', '', replaced_text) #数字を0にするpath = "-r C:/MeCab/mecabrc-u"
mecab = MeCab.Tagger(path)
parsed_lines = mecab.parse(replaced_text).split("\n")[:-2]for line in parsed_lines:
token_list = [l.split("\t")[3] for l in parsed_lines]print(token_list)
6. pos = [l.split("\t")[4].split("-")[0] for l in parsed_lines]
品詞の取得のところ。「名詞-普通名詞-一般」などの形を取り出すのがsplit("\t")[4]で、そこから一番目の要素の「名詞」を取り出すために.split("-")[0]をかけている。ここもそれぞれの出力結果を見て調整が必要そう。
7. target_pos = ["名詞", "動詞", "形容詞"]
# 名詞,動詞,形容詞のみに絞り込み するために、三つを指定。名詞だけにしてもいいかも?
8. 結果を見て調整するところ。大事
#変更箇所
x =5
y =0.5
num_topics =8
xは全体の文章数を見て決めないといけない。yはどれくらいがいいのかよくわらかない。num_topicsも分からない。いま試してみている。
結果
結果としてはこんな感じのものがでてくる。