さいとー・ま

さいとー・ま

さいとう・まの。おしごとは manoestasmanoあっとgmail.com (あっとを いれかえてください)まで。

テキストマイニングへの道06――トピックモデルとの格闘4

hunihunisaito.hatenablog.com
前回の続きです。

今回は、前回よりも元データを増やしていろいろよさそうな分析をしてみようと思いました。そのために知ったことをいろいろとメモしていこうとおもいます。最初は、google colaboratoryでやっていたと思いこんで、「記録がない!がーん!」としていたが、実際はJupiter Notebookを使っていたのでちゃんと記録はありました。よかったよかった。

はてなブログの書き方

どうやらはてなブログはてな記法においては、[]を他の文字とは独立にそのまま書いても上手く表示されないらしい。
そこで、はてな記法で用いるエスケープシーケンス(カッコやクオテーションマーク記号をそのまま表示する方法) - 持つべきものは妙なこだわりを確認するとアンド#x5bと書くと[が表示され、アンド#x5dと書くと]が表示されるらしい。これまでのブログのコードの表記を直す気はないけれど、そのためにコピペでは動かないかもしれない。こうやって不幸は繰り返していくのだなあ。

TypeError: Object of type complex is not JSON serializable

pyLDAvisというトピックモデルの可視化のためのモジュールを使っていると、TypeError: Object of type complex is not JSON serializableというエラーに遭遇しました。困って、いろいろとchatGPTに聞いた結果を載せておきます。

エラーメッセージは、JSONシリアライズできないcomplex型のオブジェクトが含まれている可能性があることを示しています。pyLDAvis.save_html()関数に渡されるデータに含まれている可能性があります。

解決策の1つは、JSONシリアライズできる形式に複素数を変換してから、データをpyLDAvis.save_html()関数に渡すことです。また、関数に渡されるデータが予期しないデータ型を含んでいないかを確認する必要があります。

より具体的な解決策を提供するためには、コードやデータについての詳細情報を提供していただく必要があります。

pyLDAvisは、可視化に使用するJavaScriptを含むため、生成される可視化オブジェクト(vis)はcomplex型になります。これは、pyLDAvisの可視化オブジェクトが複数のプログラミング言語で使用されるため、JavaScriptのオブジェクトをPythonで表現するために複雑なデータ構造が必要だからです。この複雑なデータ構造には、JSONやHTMLの要素が含まれるため、単純なPythonオブジェクトでは表現できないため、complex型になります。

JSONシリアライズできないcomplex型のオブジェクトとは、Pythonの標準ライブラリjsonで直接シリアライズできない、複素数を表すオブジェクトです。Pythonのcomplex型は、実数と虚数の値を持つため、jsonライブラリが理解できないため、シリアライズできません。例えば、以下のような複素数オブジェクトはJSONに直接変換できません。

(1+2j)

JSONに変換するためには、複素数オブジェクトを実数と虚数の値を持つ辞書オブジェクトに変換する必要があります。例えば、上記の複素数オブジェクトは以下のような辞書オブジェクトに変換することができます。

{"real": 1, "imag": 2}

ここから少しそれてしまうが、こんなことも教えてくれた。

pyLDAvisの可視化処理において、トピックモデルによって得られた単語のトピック分布や文書のトピック分布などを計算する際に、複素数が扱われることがあります。そのため、可視化処理を行う際に、visオブジェクトが複素数を含むcomplex型のオブジェクトになってしまう場合があります。このような複素数が扱われるのは、トピック分布を表す確率分布が複素数の形で表される場合があるためです。

トピック分布を表す確率分布が複素数の形で現れることは、通常のトピックモデルでは起こりません。通常、トピックモデルにおいては、トピック分布は単調増加する0から1の実数の範囲で表されます。ただし、特殊なトピックモデルや拡張版のモデルで、複素数を用いた確率分布が現れることがあります。例えば、量子トピックモデルでは、トピック分布が複素数の形で表されます。量子トピックモデルでは、量子力学の原理を応用して、単語や文書を量子ビットとして扱い、それらの状態をトピック分布として表現することができます。このようなアプローチでは、トピック分布が複素数の形で表されることがあります。

量子トピックモデル!ワクワクする!
しかし、今回使っている、pyLDAvisは、その名の通りLDA(Latent Dirichlet Allocation、潜在ディレクトリ配分法)を使っており、量子トピックモデルではない。
そこで、もう少し聞いてみると、

通常、LDAによって推定されるトピック分布は実数値の確率分布で表されます。しかし、LDAのモデルにおいて、トピック数を増やし過ぎた場合や、文書内の単語数が非常に少ない場合など、稀に複素数値の確率分布が現れることがあります。これは、ギブスサンプリングによる数値計算の精度不足が原因となって生じることがあります。このような場合、複素数値のトピック分布は適切な表現ではなく、数値計算の誤差を抑えるためにトピック数を減らすなどの対策が必要となります。

なんと!トピック数を増やしすぎた場合や、単語数が少なすぎると、こういうことが起きるそうだ。確かに調子にのってトピック数を20に設定していたことがわるかったのかもしれない。もとに戻して10に変えた。
また、

一般的に、LDAのトピック分布を表す確率分布は、各トピックに割り当てられた単語の出現回数から計算されます。したがって、単語の出現回数が整数である限り、トピック分布は複素数になることはありません。

ただし、トピックモデルを適用する文書の数が非常に多い場合、単語の出現回数が非常に小さくなり、計算されたトピック分布が数値的に不安定になることがあります。このような場合、複素数が現れる可能性があります。ただし、これはLDAが想定するような状況ではなく、過剰な計算コストを引き起こす可能性があるため、注意が必要です。

とのこと。量の多すぎ、少なすぎには注意が必要なようだ。

複数のファイルの読み込み

前回は一つのファイルだけを読み込んで作業したが、今回は複数のファイルを読み込んで作業した。
従って、複数のファイルを読み込んでいって、データフレームに入れる必要がでてきた。
またもやchatGPTさんに頼んで、コードを作ってもらった。

import pandas as pd

# ファイルのリストを定義する
files = ['file1.csv', 'file2.csv', 'file3.csv']

# データフレームを格納するための空のリストを作成する
df_list = []

# ファイルを1つずつ読み込み、データフレームをリストに追加する
for file in files:
df = pd.read_csv(file)
df_list.append(df)

# リスト内のすべてのデータフレームを結合する
merged_df = pd.concat(df_list)

# 重複する行を削除する
deduplicated_df = merged_df.drop_duplicates()

# 結果を表示する
print(deduplicated_df)

しかし、これでは上手くいかなかったので、最終的には次の形に落ち着いた。

import pandas as pd
import codecs

files = ["data/***.csv",
"data/***.csv"]

# データフレームを格納するための空のリストを作成する
df_list = []

# ファイルを1つずつ読み込み、データフレームをリストに追加する
for file in files:
with codecs.open(file, "r", "utf-8", "ignore") as file:
df2 = pd.read_table(file, header=None, delimiter=",")
df_list.append(df2)

# リスト内のすべてのデータフレームを結合する
merged_df = pd.concat(df_list)

# 重複する行を削除する
df = merged_df.drop_duplicates()
df.to_csv("***.csv")

まず起きた問題は、ファイルの文字コードのせいでエンコードがうまくできなかったことである。そこで、もう怖くない!!Pandasデータ読み込みにおける文字コードの指定 – GeoSpatial Computing LAB Noteを参考にして、codecsというモジュールを使って、"ignore"という引数(ひきすう)(丸括弧のなかにいれるもの)を指定することでエラーを無視することにした。
そして、pd.read_tableを使った。pandasでcsv/tsvファイル読み込み(read_csv, read_table) | note.nkmk.meによると、pd.read_csvでもあまり変わらない(引数(ひきすう)はdelimiter=","を無くしてよいかも?)ようだ。また、同じサイトを参照して、header=Noneを引数(ひきすう)に指定して一行目からデータとして読み込んでもらうようにした。
df2とかいろいろ適当な名前がついているのは、コードの他の部分と合わせてdfを一つにするため。

結果の記録

今回は複数のファイルを読み込んでデータの数を増やすだけでなく、出てきた結果を保存したいと思った。
そこで、こんな風にコードを変えてみた。

#可視化2

#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
import codecs
from collections import defaultdict


#csvファイルを読み込んで、pandasのデータフレームとして扱っています。ファイル名は"transexcluded20221028test.csv"で、最初の列をインデックスとして扱っています。reset_index()を用いて、インデックスを再設定しています
#変更箇所
# ファイルのリストを定義する
files = ["data/***.csv",
"data/***.csv"]

# データフレームを格納するための空のリストを作成する
df_list = []

# ファイルを1つずつ読み込み、データフレームをリストに追加する
for file in files:
with codecs.open(file, "r", "utf-8", "ignore") as file:
df2 = pd.read_table(file, header=None, delimiter=",")
df_list.append(df2)

# リスト内のすべてのデータフレームを結合する
merged_df = pd.concat(df_list)

# 重複する行を削除する
df = merged_df.drop_duplicates()

df.to_csv("result01.csv") #結果の出力1。分析した元データ。


#stopwordsの指定
with open("stopword2.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]
if isinstance(text, str): # 文字列かどうかをチェック
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 =10
lda = LdaModel(corpus, id2word =dictionary, num_topics=num_topics, alpha=0.01)



#PyLDAvisの実装
pyLDAvis.enable_notebook()

vis = gensimvis.prepare(
lda, corpus, dictionary, n_jobs = 2, sort_topics = True
)

pyLDAvis.save_html(vis, 'result01.html') #結果の出力2。可視化のグラフ。

#作成されたトピックモデルから、各トピックに属する上位の単語を取り出して、Pandasのデータフレームにまとめます。「get_topic_terms()」関数で各トピックの単語の確率を取得し、「topn=15」で確率の高い上位15単語を抽出します。「id2token」で単語IDを単語に変換し、「append()」でリスト「word」に格納します。それをPandasのデータフレームに追加し、「df.T」で転置したデータフレームを出力します。
df3 =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}'を指定します。
df3 = df3.append(_) # データフレーム_を、空のデータフレームdfに追加します。
df3.to_csv("topic_result07.csv")

# クラスタリング結果を出力
score_by_topic = defaultdict(int)
raw_test_texts = []
raw_test_texts = df.iloc[:, 0] #一行目がテキストなので、それを取り出す。

for unseen_doc, raw_train_text in zip(corpus, raw_test_texts):
for topic, score in lda[unseen_doc]:
score_by_topic[int(topic)] = float(score)
with codecs.open('result07.txt', 'a', "ignore") as f: # ファイルを追記モードで開く
for i in range(num_topics):
f.write('{:.2f}\t'.format(score_by_topic[i]))
f.write(raw_train_text + '\n') # 改行も追記する
#結果の出力3。トピックモデルを応用した結果をテキストファイルに記録。

関係ない文章の除外について

さて、いろいろいじっていると、うまく可視化もできて、トピックも分けられたが、自分が欲しいものとは全然違う文章も検索して沢山収集していたことが分かった。これらを前処理で駆逐すれば、もっとデータの特徴が描きやすくなるだろう。

import pandas as pd

#除外ワードを読み込む
with open("excludedwords1.txt","r", encoding="utf-8" ) as f:
excludedwords = f.read().split("\n")

# 条件に合致する行をフィルタリングする
pattern = '|'.join(excludedwords)
filtered = df[~df.iloc[:, 0].str.contains(pattern)]

df = filtered