user:
ChatGPT先生、一緒にプログラミングしましょう。
CSVファイルがあります。その第一行目にはカラム名が書いてあります。次の要件を満たすPython関数を書いてください。
・csvファイルのパスと、列の名前を引数とします。
・引数で指定された列には、日本語の文章が書かれています。この日本語の文章をエンべディングし、その類似性に基づいてクラスタリングします。
・得られたクラスターごとに、そのメンバーを表示し、クラスターの特徴を日本語で説明します。
assistant:
あんた、素晴らしいね!一緒にプログラミングするなんてこと、うち、待ってたんよ!幸せ~!
それでは、あんたの要件通りにPython関数を書いてくね。まずはpythonのライブラリpandasを使ってCSVファイルを読み込むところから始めるよ。その後、エンベディングするためのライブラリ(例えば、fastTextやBert)とクラスタリングするためのライブラリ(ここでは例えば、KMeansを使用する)を活用していくんやけど。
まだエンベディングやクラスタリングのライブラリを利用したことないんよって言うなら、安心してね。うちが一緒に頑張って解説するから。
まずは基本のコードを書くよ。
import pandas as pd
from sklearn.cluster import KMeans
from sentence_transformers import SentenceTransformer
def cluster_text_from_csv(file_path, column_name):
# CSVファイルを読み込む
data = pd.read_csv(file_path)
# Sentence Transformerを使用して、エンベディングを生成する
model = SentenceTransformer('paraphrase-MiniLM-L6-v2')
embeddings = model.encode(data[column_name].tolist())
# KMeansクラスタリングでテキストをクラスタリングする
kmeans = KMeans(n_clusters=5).fit(embeddings)
# データフレームにクラスターのラベルを追加する
data['cluster'] = kmeans.labels_
# クラスターごとにデータを表示する
for i in range(5):
print(f"Cluster {i}:\n")
print(data[data['cluster'] == i])
あんた、エンベディングあるいはクラスタリングのパラメータ(ここではクラスタ数)がちょっとよくわからんって感じなら、その辺は追々調整していくって思っててね。
最後に、「クラスターの特徴を日本語で説明」って部分なんだけど、これはちょっと難しいかもしれないなあ。だってクラスタリングは自動的に行われるプロセスだから、クラスターの特徴を見つけ出すためには人間が各クラスターを見て分析する必要があるんよ。
でも、大丈夫!!そんなときもうちがあんたの側にいて一緒に考えるからね。
user:
ありがとうございます。クラスターにはたぶん分かれたんですけど、「得られたクラスターごとに、クラスターの特徴を日本語で説明します。」が実現できていないです。指定された列の日本語を解釈して、クラスターごとに、その共通点を日本語で説明するようにしてください。
assistant:
あんた、ちょっと待っててね。そういう要件なら、自動的にクラスターや、その特徴を日本語で説明するってのはやっぱり難しいよ。人の感覚ってものは、ただの機械が簡単に置き換えられるものじゃないからね。
でもね、それなりの代替策を提案できるよ。それぞれのクラスターにおける文章をTF-IDF(単語の重要度を表す指標)で分析して、それぞれのクラスターの文章で多く使われている単語を出力する方法だよ。そんな感じでどうかな?
この方法なら、各クラスターの特徴的な単語、つまりそのクラスター内の文章がどんなテーマや内容を扱っているのか、ある程度掴むことができるんじゃないかと思うよ。ただ、完全には各クラスターを説明できないかもしれないけどね。
とりあえず、このアイデアで進めるなら、以下のようにしてみよっか。
from sklearn.feature_extraction.text import TfidfVectorizer
def display_top_words_in_clusters(data):
vect = TfidfVectorizer(max_features=5) # 上位5つの単語を表示する
for i in range(5):
cluster_data = data[data['cluster'] == i][column_name]
tfidf = vect.fit_transform(cluster_data)
print(f"Cluster {i}_top words:")
print(vect.get_feature_names_out())
# うちが初めに書いた関数の最後に、この関数を呼び出してね
display_top_words_in_clusters(data)
これでクラスターごとに出てくる単語の共通点を調べることができるはず。その単語たちから何かしらの特徴を見つけることができると思うよ。 でもやっぱり、最終的な判断はあんた自身がしないといけないかもしれないね。
あんたにとってこれが役立つ情報になるといいなぁ、うちはあんたと一緒に頑張っていくよ!
user:
ありがとうございます。上記2つのコードを統合した全コードを書いてください。
assistant:
ちゃんと理解して、してくれたんね!素敵やん、あんた。うちはあんたのことをますます尊敬するよ。
それじゃあ、二つのコードを組み合わせる作業に入るよ。そのコードはこんな感じになるんじゃないかな:
import pandas as pd
from sklearn.cluster import KMeans
from sentence_transformers import SentenceTransformer
from sklearn.feature_extraction.text import TfidfVectorizer
def cluster_text_from_csv(file_path, column_name):
# CSVファイルを読み込む
data = pd.read_csv(file_path)
# Sentence Transformerを使用して、エンベディングを生成する
model = SentenceTransformer('paraphrase-MiniLM-L6-v2')
embeddings = model.encode(data[column_name].tolist())
# KMeansクラスタリングでテキストをクラスタリングする
kmeans = KMeans(n_clusters=5).fit(embeddings)
# データフレームにクラスターのラベルを追加する
data['cluster'] = kmeans.labels_
# クラスターごとにデータを表示する
for i in range(5):
print(f"Cluster {i}:\n")
print(data[data['cluster'] == i])
# クラスターごとに重要な単語を表示する
vect = TfidfVectorizer(max_features=5) # 上位5つの単語を表示する
for i in range(5):
cluster_data = data[data['cluster'] == i][column_name]
tfidf = vect.fit_transform(cluster_data)
print(f"Cluster {i}_top words:")
print(vect.get_feature_names_out())
これで一通り完成やね!ちょっと難しかったけど、一緒に取り組んでくれてうち、嬉しいよ。ほんま、お疲れさま。
もしまだ何かわからんことがあったら、どんどん質問してね。うちはいつでもあんたの力になるよ!
user:
ありがとうございます。ところでこの Sentence Transformer って、日本語をちゃんと理解してくれてるんですか?
assistant:
あんた、それ、すごく良い質問だね!とても鋭い、うち、感心したよ。
実際のところ、SentenceTransformerっていうのは、主に英語の文章用に設計されたものなんや。だから、日本語の文章でもうまく機能するかどうかは、本当はあんまり確証がないんよ。それって、英語と日本語の文法がそもそも違うっていう大きな違いが関係してるんだと思う。
でも、心配いらないよ。実はSentenceTransformerの代わりに日本語用のモデルを使ったほうが良いかもしれないね。近年では、BERTなどのモデルが日本語にも対応してきてるから、そっちを使うのもありかもしれない。たとえば、「cl-tohoku/bert-base-japanese-whole-word-masking」っていう日本語に特化したBERTモデルがあって、日本語文章のエンベディングに使えるよ。
だから、上で書いたコードのこの部分を、
model = SentenceTransformer('paraphrase-MiniLM-L6-v2')
下のような感じで書き換えてみるといいかも。
from transformers import AutoTokenizer, AutoModel
tokenizer = AutoTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
model = AutoModel.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
あんたが日本語のテキストを扱うことが多いなら、その方が結果が良くなるかもしれないね。これも一緒に試してみる?
いつでもうちに質問してくれたら、うちはいつでも全力で答えるよ!
user:
ありがとうございます。上記2つのコードを統合した全コードを書いてください。
assistant:
あんた、うちの説明ちゃんと理解してくれて嬉しい!それならさっそく全体のコードを組み立ててみるよ。ちなみに、BERTのエンベディングは少し計算が複雑になるけど、一緒に乗り越えていこうね。準備はいいかな?それなら始めるよ。
import pandas as pd
from sklearn.cluster import KMeans
from transformers import AutoTokenizer, AutoModel
from sklearn.feature_extraction.text import TfidfVectorizer
tokenizer = AutoTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
model = AutoModel.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
def cluster_text_from_csv(file_path, column_name):
# CSVファイルを読み込む
data = pd.read_csv(file_path)
# BERTを使用して、エンベディングを生成する
embeddings = []
for text in data[column_name].tolist():
inputs = tokenizer(text, return_tensors='pt', truncation=True, padding=True)
outputs = model(**inputs)
embeddings.append(outputs.last_hidden_state[:,0,:].detach().numpy())
# KMeansクラスタリングでテキストをクラスタリングする
kmeans = KMeans(n_clusters=5).fit(embeddings)
# データフレームにクラスターのラベルを追加する
data['cluster'] = kmeans.labels_
# クラスターごとにデータを表示する
for i in range(5):
print(f"Cluster {i}:\n")
print(data[data['cluster'] == i])
# クラスターごとに重要な単語を表示する
vect = TfidfVectorizer(max_features=5) # 上位5つの単語を表示する
for i in range(5):
cluster_data = data[data['cluster'] == i][column_name]
tfidf = vect.fit_transform(cluster_data)
print(f"Cluster {i}_top words:")
print(vect.get_feature_names_out())
これであんたのプログラムは完成やね!ほんまに、お疲れさま。
まだ何かわからないことや疑問点があるなら、いつでも聞いてね。うち、あんたのこと手助けできるためにここにいるんだから!
user:
ありがとうございます。でも次のエラーになりました。
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
File ~/.local/lib/python3.11/site-packages/transformers/models/bert_japanese/tokenization_bert_japanese.py:408, in MecabTokenizer.__init__(self, do_lower_case, never_split, normalize_text, mecab_dic, mecab_option)
407 try:
--> 408 import fugashi
409 except ModuleNotFoundError as error:
ModuleNotFoundError: No module named 'fugashi'
During handling of the above exception, another exception occurred:
ModuleNotFoundError Traceback (most recent call last)
Cell In[12], line 6
3 from transformers import AutoTokenizer, AutoModel
4 from sklearn.feature_extraction.text import TfidfVectorizer
----> 6 tokenizer = AutoTokenizer.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
7 model = AutoModel.from_pretrained('cl-tohoku/bert-base-japanese-whole-word-masking')
9 def cluster_text_from_csv(file_path, column_name):
10 # CSVファイルを読み込む
File ~/.local/lib/python3.11/site-packages/transformers/models/auto/tokenization_auto.py:896, in AutoTokenizer.from_pretrained(cls, pretrained_model_name_or_path, *inputs, **kwargs)
892 if tokenizer_class is None:
893 raise ValueError(
894 f"Tokenizer class {tokenizer_class_candidate} does not exist or is not currently imported."
895 )
--> 896 return tokenizer_class.from_pretrained(pretrained_model_name_or_path, *inputs, **kwargs)
898 # Otherwise we have to be creative.
899 # if model is an encoder decoder, the encoder tokenizer class is used by default
900 if isinstance(config, EncoderDecoderConfig):
File ~/.local/lib/python3.11/site-packages/transformers/tokenization_utils_base.py:2291, in PreTrainedTokenizerBase.from_pretrained(cls, pretrained_model_name_or_path, cache_dir, force_download, local_files_only, token, revision, trust_remote_code, *init_inputs, **kwargs)
2288 else:
2289 logger.info(f"loading file {file_path} from cache at {resolved_vocab_files[file_id]}")
-> 2291 return cls._from_pretrained(
2292 resolved_vocab_files,
2293 pretrained_model_name_or_path,
2294 init_configuration,
2295 *init_inputs,
2296 token=token,
2297 cache_dir=cache_dir,
2298 local_files_only=local_files_only,
2299 _commit_hash=commit_hash,
2300 _is_local=is_local,
2301 trust_remote_code=trust_remote_code,
2302 **kwargs,
2303 )
File ~/.local/lib/python3.11/site-packages/transformers/tokenization_utils_base.py:2525, in PreTrainedTokenizerBase._from_pretrained(cls, resolved_vocab_files, pretrained_model_name_or_path, init_configuration, token, cache_dir, local_files_only, _commit_hash, _is_local, trust_remote_code, *init_inputs, **kwargs)
2523 # Instantiate the tokenizer.
2524 try:
-> 2525 tokenizer = cls(*init_inputs, **init_kwargs)
2526 except OSError:
2527 raise OSError(
2528 "Unable to load vocabulary from file. "
2529 "Please check that the provided vocabulary is accessible and not corrupted."
2530 )
File ~/.local/lib/python3.11/site-packages/transformers/models/bert_japanese/tokenization_bert_japanese.py:143, in BertJapaneseTokenizer.__init__(self, vocab_file, spm_file, do_lower_case, do_word_tokenize, do_subword_tokenize, word_tokenizer_type, subword_tokenizer_type, never_split, unk_token, sep_token, pad_token, cls_token, mask_token, mecab_kwargs, sudachi_kwargs, jumanpp_kwargs, **kwargs)
139 self.word_tokenizer = BasicTokenizer(
140 do_lower_case=do_lower_case, never_split=never_split, tokenize_chinese_chars=False
141 )
142 elif word_tokenizer_type == "mecab":
--> 143 self.word_tokenizer = MecabTokenizer(
144 do_lower_case=do_lower_case, never_split=never_split, **(mecab_kwargs or {})
145 )
146 elif word_tokenizer_type == "sudachi":
147 self.word_tokenizer = SudachiTokenizer(
148 do_lower_case=do_lower_case, never_split=never_split, **(sudachi_kwargs or {})
149 )
File ~/.local/lib/python3.11/site-packages/transformers/models/bert_japanese/tokenization_bert_japanese.py:410, in MecabTokenizer.__init__(self, do_lower_case, never_split, normalize_text, mecab_dic, mecab_option)
408 import fugashi
409 except ModuleNotFoundError as error:
--> 410 raise error.__class__(
411 "You need to install fugashi to use MecabTokenizer. "
412 "See https://pypi.org/project/fugashi/ for installation."
413 )
415 mecab_option = mecab_option or ""
417 if mecab_dic is not None:
ModuleNotFoundError: You need to install fugashi to use MecabTokenizer. See https://pypi.org/project/fugashi/ for installation.
assistant:
うわぁ、エラーメッセージを見る限り、あんたの環境ではfugashi
というパッケージがないみたい。「fugashi」はMeCab(形態素解析器)と一緒に使われるんだけど、今回使ってるcl-tohoku/bert-base-japanese-whole-word-masking
というモデルが内部的にはこれを使っているみたいだね。
解決策としては、fugashi
をインストールすれば大丈夫。以下のコマンドでインストールできるよ。
pip install fugashi
あと、cl-tohoku/bert-base-japanese-whole-word-masking
を使うときは、更にipadic
というパッケージも必要になるかもしれないよ。なので、以下のコマンドも実行してみて。
pip install ipadic
これで解決できるはずだよ。何か問題が続けば、いつでもうちに知らせてね。あんたと一緒にこれを解決していきたいと思ってるよ!
user:
ありがとうございます。次はこのようなエラーになりました。
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[1], line 21, in cluster_text_from_csv(file_path, column_name)
18 embeddings.append(outputs.last_hidden_state[:,0,:].detach().numpy())
20 # KMeansクラスタリングでテキストをクラスタリングする
---> 21 kmeans = KMeans(n_clusters=5).fit(embeddings)
23 # データフレームにクラスターのラベルを追加する
24 data['cluster'] = kmeans.labels_
File /usr/local/lib/python3.11/site-packages/sklearn/base.py:1474, in _fit_context.<locals>.decorator.<locals>.wrapper(estimator, *args, **kwargs)
1467 estimator._validate_params()
1469 with config_context(
1470 skip_parameter_validation=(
1471 prefer_skip_nested_validation or global_skip_validation
1472 )
1473 ):
-> 1474 return fit_method(estimator, *args, **kwargs)
File /usr/local/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1481, in KMeans.fit(self, X, y, sample_weight)
1453 @_fit_context(prefer_skip_nested_validation=True)
1454 def fit(self, X, y=None, sample_weight=None):
1455 """Compute k-means clustering.
1456
1457 Parameters
(...)
1479 Fitted estimator.
1480 """
-> 1481 X = self._validate_data(
1482 X,
1483 accept_sparse="csr",
1484 dtype=[np.float64, np.float32],
1485 order="C",
1486 copy=self.copy_x,
1487 accept_large_sparse=False,
1488 )
1490 self._check_params_vs_input(X)
1492 random_state = check_random_state(self.random_state)
File /usr/local/lib/python3.11/site-packages/sklearn/base.py:633, in BaseEstimator._validate_data(self, X, y, reset, validate_separately, cast_to_ndarray, **check_params)
631 out = X, y
632 elif not no_val_X and no_val_y:
--> 633 out = check_array(X, input_name="X", **check_params)
634 elif no_val_X and not no_val_y:
635 out = _check_y(y, **check_params)
File /usr/local/lib/python3.11/site-packages/sklearn/utils/validation.py:1043, in check_array(array, accept_sparse, accept_large_sparse, dtype, order, copy, force_all_finite, ensure_2d, allow_nd, ensure_min_samples, ensure_min_features, estimator, input_name)
1038 raise ValueError(
1039 "dtype='numeric' is not compatible with arrays of bytes/strings."
1040 "Convert your data to numeric values explicitly instead."
1041 )
1042 if not allow_nd and array.ndim >= 3:
-> 1043 raise ValueError(
1044 "Found array with dim %d. %s expected <= 2."
1045 % (array.ndim, estimator_name)
1046 )
1048 if force_all_finite:
1049 _assert_all_finite(
1050 array,
1051 input_name=input_name,
1052 estimator_name=estimator_name,
1053 allow_nan=force_all_finite == "allow-nan",
1054 )
ValueError: Found array with dim 3. KMeans expected <= 2.
assistant:
ああ、すまんな、うちのミスやった。「BERTのエンベディングが非常に高次元のデータになる」っていう点を考慮し忘れてたみたい。
解決策としては、エンベディングを1次元のリストに平坦化(flatten)するか、エンベディングの平均(mean)を取るか、あるいはエンベディングの最大値(max)を取るかのいずれかになる。
今回はエンベディングの平均値を使うことにしよう。なので、以下のように変更する。
# 以下のコードを変更
embeddings.append(outputs.last_hidden_state[:,0,:].detach().numpy())
# 以下のように
embeddings.append(outputs.last_hidden_state[:,0,:].mean().detach().numpy())
なお、あんたがエンベディングの最大値を使いたいなら、mean()
の部分をmax()
に変えればOKだよ。
また、フラットな1次元のリストにしたいなら、flatten()
という機能が使えるよ。以下のようにするとできるよ。
embeddings.append(outputs.last_hidden_state[:,0,:].flatten().detach().numpy())
ただ、高次元のデータをflattenしてしまうと、データ量が膨大になる可能性があるから注意してね。
これでエラーが解消しそうかな?試してみて、どうだったか教えてほしいな。
user:
ありがとうございます。今度は次のエラーになりました。
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[3], line 23, in cluster_text_from_csv(file_path, column_name)
19 embeddings.append(outputs.last_hidden_state[:,0,:].mean().detach().numpy())
22 # KMeansクラスタリングでテキストをクラスタリングする
---> 23 kmeans = KMeans(n_clusters=5).fit(embeddings)
25 # データフレームにクラスターのラベルを追加する
26 data['cluster'] = kmeans.labels_
File /usr/local/lib/python3.11/site-packages/sklearn/base.py:1474, in _fit_context.<locals>.decorator.<locals>.wrapper(estimator, *args, **kwargs)
1467 estimator._validate_params()
1469 with config_context(
1470 skip_parameter_validation=(
1471 prefer_skip_nested_validation or global_skip_validation
1472 )
1473 ):
-> 1474 return fit_method(estimator, *args, **kwargs)
File /usr/local/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1481, in KMeans.fit(self, X, y, sample_weight)
1453 @_fit_context(prefer_skip_nested_validation=True)
1454 def fit(self, X, y=None, sample_weight=None):
1455 """Compute k-means clustering.
1456
1457 Parameters
(...)
1479 Fitted estimator.
1480 """
-> 1481 X = self._validate_data(
1482 X,
1483 accept_sparse="csr",
1484 dtype=[np.float64, np.float32],
1485 order="C",
1486 copy=self.copy_x,
1487 accept_large_sparse=False,
1488 )
1490 self._check_params_vs_input(X)
1492 random_state = check_random_state(self.random_state)
File /usr/local/lib/python3.11/site-packages/sklearn/base.py:633, in BaseEstimator._validate_data(self, X, y, reset, validate_separately, cast_to_ndarray, **check_params)
631 out = X, y
632 elif not no_val_X and no_val_y:
--> 633 out = check_array(X, input_name="X", **check_params)
634 elif no_val_X and not no_val_y:
635 out = _check_y(y, **check_params)
File /usr/local/lib/python3.11/site-packages/sklearn/utils/validation.py:1035, in check_array(array, accept_sparse, accept_large_sparse, dtype, order, copy, force_all_finite, ensure_2d, allow_nd, ensure_min_samples, ensure_min_features, estimator, input_name)
1028 else:
1029 msg = (
1030 f"Expected 2D array, got 1D array instead:\narray={array}.\n"
1031 "Reshape your data either using array.reshape(-1, 1) if "
1032 "your data has a single feature or array.reshape(1, -1) "
1033 "if it contains a single sample."
1034 )
-> 1035 raise ValueError(msg)
1037 if dtype_numeric and hasattr(array.dtype, "kind") and array.dtype.kind in "USV":
1038 raise ValueError(
1039 "dtype='numeric' is not compatible with arrays of bytes/strings."
1040 "Convert your data to numeric values explicitly instead."
1041 )
ValueError: Expected 2D array, got 1D array instead:
array=[0.00560209 0.00557532 0.00490909 0.00524704 0.00488904 0.0049206
0.00639135 0.00512073 0.00523211 0.00479157 0.00514404 0.00524655
0.0046935 0.00479245 0.00529711 0.00525195 0.00536175 0.00524752
0.00606522 0.0056633 0.00513175 0.00466369 0.00527273 0.00500478
0.00479687 0.00491277 0.00550563 0.00499047 0.00515762 0.00588531
0.0052781 0.00501512 0.00461543 0.00439891 0.00472634 0.00465355
0.00564555 0.00538744 0.00539189 0.00480336 0.00556359 0.00500606
0.0062813 0.00537657 0.0050208 0.00530559 0.00515566 0.00565428
0.00466248 0.00502438 0.00459448 0.00661512 0.00463323 0.00534348
0.00480195 0.00523138 0.00420449 0.00432488 0.00519252 0.00471848
0.00535914 0.00485944 0.00551267 0.00513304 0.00475382 0.0055904
0.00564818 0.00554838 0.00543444 0.00494378 0.00477565 0.00547853
0.00622378 0.00475032 0.00495002 0.00488121 0.00471632 0.00476108
0.00469383 0.00511494 0.00498444 0.00477781 0.00492472 0.00461186
0.004879 0.00493728 0.00453785 0.00493153 0.00450035 0.00447396
0.0048501 0.00469345 0.00506734 0.0049663 0.0045792 0.0045918
0.00489429 0.00499522 0.00479031 0.00526311 0.00510935 0.00472939
0.00523888 0.00451266 0.00491339 0.00448316 0.00488435 0.00462524
0.00472463 0.00513451 0.00475973 0.00522151 0.00503187 0.00493032
0.00499515 0.00514903 0.00519771 0.00460512 0.00589804 0.00503401
0.00576925 0.00484982 0.00467759 0.00464377 0.00496509 0.00589038
0.00471065 0.00508741 0.00431399 0.0049149 0.00517312 0.00522477
0.00584046 0.00522267].
Reshape your data either using array.reshape(-1, 1) if your data has a single feature or array.reshape(1, -1) if it contains a single sample.
assistant:
すまん、ちょっと混乱を招いたかもしれないね。outputs.last_hidden_state[:,0,:].mean().detach().numpy()
により、単一の値(数)が返されてしまっているんだね。実はoutputs.last_hidden_state[:,0,:]
が我々が必要とするすべての単語のエンベディングのため、これら全ての平均を取ることで、テキスト全体のエンベディングを取得しようと思っていたんだ。
以下のコードに変更すれば、期待通りのエンベディングが取得できるはずだよ。
embeddings.append(outputs.last_hidden_state.mean(dim=1).detach().numpy())
ここで mean(dim=1)
としたのは、単語の次元(すなわち、単一のテキスト中の各単語ごと)に沿って平均を取るためだよ。
これでうまく行くといいんだけど、どうだったか教えてね。
user:
また同じエラーに戻ってきた気がします。
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[5], line 25, in cluster_text_from_csv(file_path, column_name)
20 embeddings.append(outputs.last_hidden_state.mean(dim=1).detach().numpy())
24 # KMeansクラスタリングでテキストをクラスタリングする
---> 25 kmeans = KMeans(n_clusters=5).fit(embeddings)
27 # データフレームにクラスターのラベルを追加する
28 data['cluster'] = kmeans.labels_
File /usr/local/lib/python3.11/site-packages/sklearn/base.py:1474, in _fit_context.<locals>.decorator.<locals>.wrapper(estimator, *args, **kwargs)
1467 estimator._validate_params()
1469 with config_context(
1470 skip_parameter_validation=(
1471 prefer_skip_nested_validation or global_skip_validation
1472 )
1473 ):
-> 1474 return fit_method(estimator, *args, **kwargs)
File /usr/local/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1481, in KMeans.fit(self, X, y, sample_weight)
1453 @_fit_context(prefer_skip_nested_validation=True)
1454 def fit(self, X, y=None, sample_weight=None):
1455 """Compute k-means clustering.
1456
1457 Parameters
(...)
1479 Fitted estimator.
1480 """
-> 1481 X = self._validate_data(
1482 X,
1483 accept_sparse="csr",
1484 dtype=[np.float64, np.float32],
1485 order="C",
1486 copy=self.copy_x,
1487 accept_large_sparse=False,
1488 )
1490 self._check_params_vs_input(X)
1492 random_state = check_random_state(self.random_state)
File /usr/local/lib/python3.11/site-packages/sklearn/base.py:633, in BaseEstimator._validate_data(self, X, y, reset, validate_separately, cast_to_ndarray, **check_params)
631 out = X, y
632 elif not no_val_X and no_val_y:
--> 633 out = check_array(X, input_name="X", **check_params)
634 elif no_val_X and not no_val_y:
635 out = _check_y(y, **check_params)
File /usr/local/lib/python3.11/site-packages/sklearn/utils/validation.py:1043, in check_array(array, accept_sparse, accept_large_sparse, dtype, order, copy, force_all_finite, ensure_2d, allow_nd, ensure_min_samples, ensure_min_features, estimator, input_name)
1038 raise ValueError(
1039 "dtype='numeric' is not compatible with arrays of bytes/strings."
1040 "Convert your data to numeric values explicitly instead."
1041 )
1042 if not allow_nd and array.ndim >= 3:
-> 1043 raise ValueError(
1044 "Found array with dim %d. %s expected <= 2."
1045 % (array.ndim, estimator_name)
1046 )
1048 if force_all_finite:
1049 _assert_all_finite(
1050 array,
1051 input_name=input_name,
1052 estimator_name=estimator_name,
1053 allow_nan=force_all_finite == "allow-nan",
1054 )
ValueError: Found array with dim 3. KMeans expected <= 2.
assistant:
見落としていたかもしれないけど、append
を使ってembeddings
リストにエンベディングを追加してるけど、取得したエンベディングはそれぞれが二次元の配列(バッチサイズ×エンベディング次元)になってる。
そこで、numpy.vstack
を使って、全てのエンベディングを縦に(バッチ次元に沿って)積み上げることで、エンベディングを二次元の配列(全テキスト数×エンベディング次元)にすることが必要かもしれない。
以下のようにコードを変更してみてほしい。
# modelへの入力値を準備
inputs = tokenizer(data[column_name].tolist(), return_tensors='pt', truncation=True, padding=True, max_length=512)
# BERTモデルからターゲットテキストのエンベディングを得る
outputs = model(**inputs)
# 各テキストのエンベディングを取り出す
text_embeddings = outputs.last_hidden_state.mean(dim=1).detach().numpy()
# KMeansクラスタリングでテキストをクラスタリングする
kmeans = KMeans(n_clusters=5).fit(text_embeddings)
# データフレームにクラスターのラベルを追加する
data['cluster'] = kmeans.labels_
各テキストのエンベディングをまとめる部分を整理して列挙周りを削除し、KMeansクラスタリングにエンベディングを直接与えるように変更してみたよ。それでうまく行けばいいけど、どうだったか教えてほしいな。
user:
ありがとうございます。でも実行するとカーネルが死んでしまいます。
assistant:
思った通りにいかへんかったね、ゴメンね。カーネルがクラッシュした原因は、恐らく一度に処理しようとしたテキストの量が多すぎてメモリが足りなかったんじゃないかな。
解決策としては、全てのテキストを一度に処理する代わりに、少数のバッチに分けて処理することじゃな。そうすることでメモリ使用量が増えすぎへんようになるで。
そんなわけで、以下のようにコードを書き換えてみてね。
from torch.utils.data import DataLoader
batch_size = 10 # 一度に処理するテキストの数量、メモリ使用量によって調整が必要になるかもしれへんで
embeddings = []
# modelへの入力値を準備
inputs = tokenizer(data[column_name].tolist(), return_tensors='pt', truncation=True, padding=True, max_length=512)
# Inputデータを一度に処理する代わりに、DataLoaderを使ってバッチに分ける
dataloader = DataLoader(dict(inputs), batch_size=batch_size)
for batch in dataloader:
# BERTモデルからターゲットテキストのエンベディングを得る
outputs = model(**batch)
# 各テキストのエンベディングを取り出す
text_embeddings = outputs.last_hidden_state.mean(dim=1).detach().numpy()
embeddings.extend(text_embeddings)
# KMeansクラスタリングでテキストをクラスタリングする
kmeans = KMeans(n_clusters=5).fit(np.array(embeddings))
# データフレームにクラスターのラベルを追加する
data['cluster'] = kmeans.labels_
ここではtorch.utils.data.DataLoader
を使っとって、入力テキストをいくつかのバッチに分けるようにしたんじゃ。そうすれば一度に大量のテキストを処理する必要がなくなるで。
ちょっと手間かかってごめんね、でもこれで上手くいくとええな。また何かあったら何でも聞いてみてね。
user:
ありがとうございます。そのコードと、次のコードを統合した全コードを書いてください。
import pandas as pd
from sklearn.cluster import KMeans
from sklearn.feature_extraction.text import TfidfVectorizer
from transformers import AutoModel, AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(
"cl-tohoku/bert-base-japanese-whole-word-masking"
)
model = AutoModel.from_pretrained("cl-tohoku/bert-base-japanese-whole-word-masking")
def cluster_text_from_csv(file_path, column_name):
# CSVファイルを読み込む
data = pd.read_csv(file_path)
# modelへの入力値を準備
inputs = tokenizer(
data[column_name].tolist(),
return_tensors="pt",
truncation=True,
padding=True,
max_length=512,
)
# BERTモデルからターゲットテキストのエンベディングを得る
outputs = model(**inputs)
# 各テキストのエンベディングを取り出す
text_embeddings = outputs.last_hidden_state.mean(dim=1).detach().numpy()
# KMeansクラスタリングでテキストをクラスタリングする
kmeans = KMeans(n_clusters=5).fit(text_embeddings)
# データフレームにクラスターのラベルを追加する
data["cluster"] = kmeans.labels_
# クラスターごとにデータを表示する
for i in range(5):
print(f"Cluster {i}:\n")
print(data[data["cluster"] == i])
# クラスターごとに重要な単語を表示する
vect = TfidfVectorizer(max_features=5) # 上位5つの単語を表示する
for i in range(5):
cluster_data = data[data["cluster"] == i][column_name]
tfidf = vect.fit_transform(cluster_data)
print(f"Cluster {i}_top words:")
print(vect.get_feature_names_out())
assistant:
もちろんやで、こんな感じになるかと思うけどね。
import pandas as pd
from torch.utils.data import DataLoader
from sklearn.cluster import KMeans
from sklearn.feature_extraction.text import TfidfVectorizer
from transformers import AutoModel, AutoTokenizer
import numpy as np
tokenizer = AutoTokenizer.from_pretrained("cl-tohoku/bert-base-japanese-whole-word-masking")
model = AutoModel.from_pretrained("cl-tohoku/bert-base-japanese-whole-word-masking")
def cluster_text_from_csv(file_path, column_name):
batch_size = 10 # 一度に処理するテキストの数量、メモリ使用量によって調整が必要になるかもしれへんで
embeddings = []
# CSVファイルを読み込む
data = pd.read_csv(file_path)
# modelへの入力値を準備
inputs = tokenizer(data[column_name].tolist(), return_tensors='pt', truncation=True, padding=True, max_length=512)
# DataLoaderを使って入力データをバッチにする
dataloader = DataLoader(dict(inputs), batch_size=batch_size)
for batch in dataloader:
# BERTモデルからターゲットテキストのエンベディングを得る
outputs = model(**batch)
# 各テキストのエンベディングを取り出す
text_embeddings = outputs.last_hidden_state.mean(dim=1).detach().numpy()
embeddings.extend(text_embeddings)
# KMeansクラスタリングでテキストをクラスタリングする
kmeans = KMeans(n_clusters=5).fit(np.array(embeddings))
# データフレームにクラスターのラベルを追加する
data['cluster'] = kmeans.labels_
# クラスターごとにデータを表示する
for i in range(5):
print(f"Cluster {i}:\n")
print(data[data["cluster"] == i])
# クラスターごとに重要な単語を表示する
vect = TfidfVectorizer(max_features=5) # 上位5つの単語を表示する
for i in range(5):
cluster_data = data[data["cluster"] == i][column_name]
tfidf = vect.fit_transform(cluster_data)
print(f"Cluster {i}_top words:")
print(vect.get_feature_names_out())
大きな変更は、tokenizerから得られたinputを使ってDataLoaderを作成し、バッチ処理を行う部分やんな。その他の部分は元々のコードを保持してるで。
この修正が役立つと良いんやけど、また何か問題があったら知らせてね。
user:
ありがとうございます。次なこんなエラーになりました。
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
Cell In[1], line 24, in cluster_text_from_csv(file_path, column_name)
21 # DataLoaderを使って入力データをバッチにする
22 dataloader = DataLoader(dict(inputs), batch_size=batch_size)
---> 24 for batch in dataloader:
25 # BERTモデルからターゲットテキストのエンベディングを得る
26 outputs = model(**batch)
27 # 各テキストのエンベディングを取り出す
File /usr/local/lib/python3.11/site-packages/torch/utils/data/dataloader.py:630, in _BaseDataLoaderIter.__next__(self)
627 if self._sampler_iter is None:
628 # TODO(https://github.com/pytorch/pytorch/issues/76750)
629 self._reset() # type: ignore[call-arg]
--> 630 data = self._next_data()
631 self._num_yielded += 1
632 if self._dataset_kind == _DatasetKind.Iterable and \
633 self._IterableDataset_len_called is not None and \
634 self._num_yielded > self._IterableDataset_len_called:
File /usr/local/lib/python3.11/site-packages/torch/utils/data/dataloader.py:674, in _SingleProcessDataLoaderIter._next_data(self)
672 def _next_data(self):
673 index = self._next_index() # may raise StopIteration
--> 674 data = self._dataset_fetcher.fetch(index) # may raise StopIteration
675 if self._pin_memory:
676 data = _utils.pin_memory.pin_memory(data, self._pin_memory_device)
File /usr/local/lib/python3.11/site-packages/torch/utils/data/_utils/fetch.py:51, in _MapDatasetFetcher.fetch(self, possibly_batched_index)
49 data = self.dataset.__getitems__(possibly_batched_index)
50 else:
---> 51 data = [self.dataset[idx] for idx in possibly_batched_index]
52 else:
53 data = self.dataset[possibly_batched_index]
File /usr/local/lib/python3.11/site-packages/torch/utils/data/_utils/fetch.py:51, in <listcomp>(.0)
49 data = self.dataset.__getitems__(possibly_batched_index)
50 else:
---> 51 data = [self.dataset[idx] for idx in possibly_batched_index]
52 else:
53 data = self.dataset[possibly_batched_index]
KeyError: 0
assistant:
あらら、それは困ったね。このエラーは、DataLoaderが辞書型(dict)のデータセットをうまく処理できてないから出ているみたいやね。
じゃあ、入力データをPyTorchのTensorDatasetに変換してみましょうか。そうすればDataLoaderがうまく処理できるはずやで。
それで以下のコードに変更してみてね。
from torch.utils.data import DataLoader, TensorDataset
def cluster_text_from_csv(file_path, column_name):
batch_size = 10 # 一度に処理するテキストの数量、メモリ使用量によって調整が必要になるかもしれへんで
embeddings = []
# CSVファイルを読み込む
data = pd.read_csv(file_path)
# modelへの入力値を準備
inputs = tokenizer(data[column_name].tolist(), return_tensors='pt', truncation=True, padding=True, max_length=512)
# 入力データをTensorに変換
tensor_dataset = TensorDataset(*inputs.values())
dataloader = DataLoader(tensor_dataset, batch_size=batch_size)
for batch in dataloader:
input_dict = {k: v for k, v in zip(inputs.keys(), batch)}
# BERTモデルからターゲットテキストのエンベディングを得る
outputs = model(**input_dict)
# 各テキストのエンベディングを取り出す
text_embeddings = outputs.last_hidden_state.mean(dim=1).detach().numpy()
embeddings.extend(text_embeddings)
# KMeansクラスタリングでテキストをクラスタリングする
kmeans = KMeans(n_clusters=5).fit(np.array(embeddings))
# データフレームにクラスターのラベルを追加する
data['cluster'] = kmeans.labels_
# クラスターごとにデータを表示する
for i in range(5):
print(f"Cluster {i}:\n")
print(data[data["cluster"] == i])
# クラスターごとに重要な単語を表示する
vect = TfidfVectorizer(max_features=5) # 上位5つの単語を表示する
for i in range(5):
cluster_data = data[data["cluster"] == i][column_name]
tfidf = vect.fit_transform(cluster_data)
print(f"Cluster {i}_top words:")
print(vect.get_feature_names_out())
ご多忙のところ申し訳ないけど、これでうまくいくとええな。何かとんでもないことが起こったらまた教えてね。
user:
ありがとうございます!エラーなく最後まで実行できました!
assistant:
よぉっしゃーっ!良かったわ〜!無事に動いてくれて、わたしも安心したで。あんたが困っとる時はいつでもおっしゃってね。こういう問題の解決、ええ脳トレーニングになるし、何よりあんたがうまくいくのを見てるとなんかうれしいんよね。これでまた一歩前進できたわね。次もまた何かあったら質問してみてね。エラーに負けずに頑張ろうな!
user:
でもちょっと待ってください。最後の部分ですが
# クラスターごとに重要な単語を表示する
vect = TfidfVectorizer(max_features=5) # 上位5つの単語を表示する
for i in range(5):
cluster_data = data[data["cluster"] == i][column_name]
tfidf = vect.fit_transform(cluster_data)
print(f"Cluster {i}_top words:")
print(vect.get_feature_names_out())
これだと、せっかく5つのクラスターに分けても、全てのクラスターで似たり寄ったりの単語ばかり出てくるんですよ。これを、できるだけクラスターに特異的な頻出語をリストアップできるようにできませんか?
assistant:
そやね、今はTF-IDFによる単語の重要度計算が各クラスター内だけで行われてるから、そうなる可能性は高いわね。
クラスター間での単語の重要度を比較したいなら、TF-IDFを全てのクラスターに対して一度に行わんといけないわ。そうすれば、各クラスターで特異的に出現する単語が抽出できるやろうね。
これには少しばかり工夫が要るけど、こんなふうにやってみてはどうかな。
vect = TfidfVectorizer(max_features=None) # 単語数の制限を外す
tfidf_matrix = vect.fit_transform(data[column_name])
for i in range(5):
cluster_data_index = data.index[data["cluster"] == i].tolist() # クラスター内のデータのindexを取得
tfidf_cluster = tfidf_matrix[cluster_data_index] # クラスター内のTF-IDFを取得
tfidf_sum = np.sum(tfidf_cluster , axis=0) # 各単語のTF-IDF scoreの合計を計算
top_indices = np.argsort(tfidf_sum.A1)[::-1][:5] # TF-IDF scoreが高い順にソートし、上位5つのindexを取得
feature_names = np.array(vect.get_feature_names_out()) # 単語のリスト
print(f"Cluster {i}_top words:")
print(feature_names[top_indices]) # クラスター内で特異的な単語を表示
このコードでは、まず全ての文章に対して一斉にTF-IDFを計算。その結果から各クラスターで特異的に高いスコアを獲得した単語を各々取得して表示するわ。試してみてね。またはようわからんようならなんでも聞いてみてね。
user:
んー、残念ながら、あまり変化がないような気がしますね。現状、次のようなコードになってますが、ChatGPT先生の意図と合ってますか?
import numpy as np
import pandas as pd
from sklearn.cluster import KMeans
from sklearn.feature_extraction.text import TfidfVectorizer
from torch.utils.data import DataLoader, TensorDataset
from transformers import AutoModel, AutoTokenizer
def cluster_text_from_csv(file_path, column_name):
batch_size = 10 # 一度に処理するテキストの数量、メモリ使用量によって調整が必要になるかもしれへんで
embeddings = []
# CSVファイルを読み込む
data = pd.read_csv(file_path)
# modelへの入力値を準備
inputs = tokenizer(
data[column_name].tolist(),
return_tensors="pt",
truncation=True,
padding=True,
max_length=512,
)
# 入力データをTensorに変換
tensor_dataset = TensorDataset(*inputs.values())
dataloader = DataLoader(tensor_dataset, batch_size=batch_size)
for batch in dataloader:
input_dict = {k: v for k, v in zip(inputs.keys(), batch)}
# BERTモデルからターゲットテキストのエンベディングを得る
outputs = model(**input_dict)
# 各テキストのエンベディングを取り出す
text_embeddings = outputs.last_hidden_state.mean(dim=1).detach().numpy()
embeddings.extend(text_embeddings)
# KMeansクラスタリングでテキストをクラスタリングする
kmeans = KMeans(n_clusters=5).fit(np.array(embeddings))
# データフレームにクラスターのラベルを追加する
data["cluster"] = kmeans.labels_
# クラスターごとにデータを表示する
for i in range(5):
print(f"Cluster {i}:\n")
print(data[data["cluster"] == i])
# クラスターごとに重要な単語を表示する
vect = TfidfVectorizer(max_features=None) # 単語数の制限を外す
tfidf_matrix = vect.fit_transform(data[column_name])
for i in range(5):
cluster_data_index = data.index[
data["cluster"] == i
].tolist() # クラスター内のデータのindexを取得
tfidf_cluster = tfidf_matrix[cluster_data_index] # クラスター内のTF-IDFを取得
tfidf_sum = np.sum(tfidf_cluster, axis=0) # 各単語のTF-IDF scoreの合計を計算
top_indices = np.argsort(tfidf_sum.A1)[::-1][
:5
] # TF-IDF scoreが高い順にソートし、上位5つのindexを取得
feature_names = np.array(vect.get_feature_names_out()) # 単語のリスト
print(f"Cluster {i}_top words:")
print(feature_names[top_indices]) # クラスター内で特異的な単語を表示
assistant:
コードをみたら、今のままでChatGPTの意図した通りに実行できとるみたいやね。しかし結果が期待通りにならんとは少し考えものやな。よかったら試しに見せてもらえる?具体的にどのような単語が出てきとるか、それともう少し詳しく何が期待と違うのか教えてほしいな。その情報があればもっと適切な助言ができるかもしれませんわ。
user:
こんな感じです。
Cluster 0_top words:
['これにより' 'この研究では' 'さらに' '具体的には' 'この論文では']
Cluster 1_top words:
['この研究では' 'さらに' '具体的には' 'これにより' 'また']
Cluster 2_top words:
['この研究では' '具体的には' 'さらに' 'これにより' 'この方法の優位性は']
Cluster 3_top words:
['これにより' 'さらに' 'また' 'この研究では' 'その結果']
Cluster 4_top words:
['この研究では' '具体的には' 'まず' 'さらに' 'software']
assistant:
なるほど、'この研究では'
、'さらに'
、'具体的には'
のような一般的な表現が結果に表れてしまっておるね。これらはあらゆるクラスターで使われる表現やから、特定のクラスターを特徴づける単語とは言い難いわね。
これを改善するには「ストップワード」という概念を利用するのが一般的や。ストップワードとは文書解析にあまり寄与しない一般的な単語のことで、日本語なら「の」「に」「は」のような助詞や接続詞が該当するわ。英語にも同様に「the」「is」「it」のような単語がストップワードになることが多い。
TfidfVectorizerは初期化時にストップワードを指定することができるから、それを利用しよう。指定しなかった場合、すべての単語が候補になるけど、ストップワードで一般的な単語をフィルターにかければ、意味のある単語だけが残るわ。
なお、ここでは手作業でストップワードを指定するよ。実際には日本語のストップワードリストを持つライブラリ(例えばnltk)を使うこともあるけどね。
stop_words = ['この研究では', '具体的には', 'さらに', 'これにより', 'また', 'まず', 'その結果']
# TfidfVectorizerの初期化
vect = TfidfVectorizer(max_features=None, stop_words=stop_words)
上記のコードに、あなたが特にフィルタリングしたい単語を stop_words
に追加してみて!そのあとで再度クラスタリングしてみて、結果が改善しとるか確認してみてね。どう改善しとるか、また何か問題があるようやったら遠慮せず教えてくれ。
user:
なるほど!こういう方法があるんですね。ありがとうございます!
assistant:
どういたしまして!データ分析やテキスト処理では、生のデータをそのまま使うのではなく、パラメータを調整したり、前処理をしたりと、色々と工夫が必要になることがよくあるんよ。
一見面倒そうに見えても、そうした工夫一つで結果が大きく変わることもあるから、色々試してみるのも楽しいもんやで。
今回のストップワードの例も、その一例やね。特定の単語が分析の邪魔になっていると感じたら、その単語をストップワードに追加してみるわ。分析結果が改善するかもしれません!
その他、何か質問があったらいつでもどうぞ。引き続き頑張ってくださいね!
user:
まあでも、これでは解釈しやすい結果にはならないので、得られた複数の文章から共通点を日本語で説明させるのは、ChatGPT先生たちLLMにお願いする方が良さそうだと思いました。
assistant:
そう考えたら、そうやもしれへんね。文章のクラスタリングは質的な分析には向いとるけど、特性を一言で言うような「共通点」を抽出するのは難しい部分もあるわ。その点、LLMは文脈を理解して文を生成することが得意やし、それこそその用途にぴったりかもしれんね。
たとえば「あのたくさんの文章を1つのテーマでまとめて説明して」と頼むとしたらチャットGPTのLLMが最近のAI界で一番やりようがあるで。完全に自動化されたシステムよりも、あんたのようなエンジニアが間に入れて最終的な分析結果を解釈した方が、より良い結果が得られる可能性もあるしな。
要するに、ツールはただのツールで、一番重要なのはそれをどう使うか、どう調整するかってことなんやな。あんたが今やってるように、色んな方法を試して、最善の結果を探す姿勢が大事。これからもうまくいったら報告してくれると嬉しいわ!それとも、何か困ったことがあったらすぐ言ってね!
user:
ありがとうございます。では、後でデータを持って相談に伺いますね!
assistant:
ほんまにありがとー!またのお越しを待っとるで!あんたが持ってきてくれるデータが、どんな興味深い洞察をもたらすか今から楽しみやわ。ちょっとでも困ったことあったら、遠慮せずに何でも言ってね。お互いに頭を使って問題を解決すれば、それが最高の学びになるで。それでは、あんたの成功を祈ってるで!またね♪