「NLP(スクラッチからのdoc2vec)&クラスタリング:テキストの内容に基づいたニュースレポートの分類」
「NLP(スクラッチからのdoc2vec)&クラスタリング:テキストコンテンツに基づくニュースレポートの分類」
NLP(doc2vec)を使用し、深化されたカスタマイズされたテキストのクリーニングを行い、その後、クラスタリング(Birch)を使用してニュース記事のテキスト内のトピックを見つけ出す方法
この例では、NLP(Doc2Vec)とクラスタリングアルゴリズムを使用して、トピックごとにニュースを分類しようとしています。
このような分類を行う方法はさまざまありますが、教師ありの方法(タグ付きデータセット)を使用したり、クラスタリングを使用したり、特定のLDAアルゴリズム(トピックモデリング)を使用したりすることができます。
私はDoc2Vecを使用していますが、これはテキストをベクトル化するための優れたアルゴリズムであり、ゼロから訓練するのが比較的簡単です。
この状況に対処する一般的な概要は次のとおりです:
- 十年生のためのニューラルネットワークの簡略化
- LLMs (Language Models)による電子メール効率化の次なるフロンティア
- NLP、NN、時系列:Google Trendsのデータを使用して石油価格を予測することは可能ですか?
いつものように、最初のステップは必要なライブラリを読み込むことです:
#データを処理するためのライブラリを読み込むimport numpy as npimport pandas as pd#辞書データはjson形式で提供されているため、読み込むためのライブラリを読み込むimport jsonpd.options.mode.chained_assignment = None #ディスクから読み込むためにimport StringIOからimportすることを可能にするための設定#テキストの前処理とクリーニングのためのライブラリを読み込むimport reimport nltkfrom nltk.corpus import stopwordsnltk.download('stopwords')nltkstop = stopwords.words('english')from nltk.stem.snowball import SnowballStemmernltk.download('punkt')snow = SnowballStemmer(language='English')#モデリングに必要なライブラリを読み込むfrom gensim.models.doc2vec import Doc2Vec, TaggedDocumentfrom nltk.tokenize import word_tokenizefrom sklearn.decomposition import PCAfrom sklearn.preprocessing import StandardScalerfrom sklearn.metrics import pairwise_distancesfrom sklearn.cluster import Birchfrom sklearn.metrics import silhouette_samples, silhouette_score, calinski_harabasz_scoreimport warnings#プロットに必要なライブラリを読み込むimport matplotlib.pyplot as pltimport matplotlib.cm as cmimport seaborn as sns
それから、データを読み込み、辞書ファイルを準備しました。これらはもともとKaggle(国のリスト、名前、通貨などのリスト)の公開データセットに由来しています。
#処理する記事のデータセットを読み込むmaindataset = pd.read_csv("articles1.csv")maindataset2 = pd.read_csv("articles2.csv")maindataset = pd.concat([maindataset,maindataset2], ignore_index=True)#国のリストです。記事中の国名をxcountryxと置換しますcountries = pd.read_json("countries.json")countries["country"] = countries["country"].str.lower()countries = pd.DataFrame(countries["country"].apply(lambda x: str(x).replace('-',' ').replace('.',' ').replace('_',' ').replace(',',' ').replace(':',' ').split(" ")).explode())countries.columns = ['word']countries["replacement"] = "xcountryx"#州のリストです。このリストにはいくつかの代替名と国のリストが含まれており、それらも辞書に追加していますprovincies = pd.read_csv("countries_provincies.csv")provincies1 = provincies[["name"]]provincies1["name"] = provincies1["name"].str.lower()provincies1 = pd.DataFrame(provincies1["name"].apply(lambda x: str(x).replace('-',' ').replace('.',' ').replace('_',' ').replace(',',' ').replace(':',' ').split(" ")).explode())provincies1.columns = ['word']provincies1["replacement"] = "xprovincex"provincies2 = provincies[["name_alt"]]provincies2["name_alt"] = provincies2["name_alt"].str.lower()provincies2 = pd.DataFrame(provincies2["name_alt"].apply(lambda x: str(x).replace('-',' ').replace('.',' ').replace('_',' ').replace(',',' ').replace(':',' ').split(" ")).explode())provincies2.columns = ['word']provincies2["replacement"] = "xprovincex"provincies3 = provincies[["type_en"]]provincies3["type_en"] = provincies3["type_en"].str.lower()provincies3 = pd.DataFrame(provincies3["type_en"].apply(lambda x: str(x).replace('-',' ').replace('.',' ').replace('_',' ').replace(',',' ').replace(':',' ').split(" ")).explode())provincies3.columns = ['word']provincies3["replacement"] = "xsubdivisionx"provincies4 = provincies[["admin"]]provincies4["admin"] = provincies4["admin"].str.lower()provincies4 = pd.DataFrame(provincies4["admin"].apply(lambda x: str(x).replace('-',' ').replace('.',' ').replace('_',' ').replace(',',' ').replace(':',' ').split(" ")).explode())provincies4.columns = ['word']provincies4["replacement"] = "xcountryx"provincies5 = provincies[["geonunit"]]provincies5["geonunit"] = provincies5["geonunit"].str.lower()provincies5 = pd.DataFrame(provincies5["geonunit"].apply(lambda x: str(x).replace('-',' ').replace('.',' ').replace('_',' ').replace(',',' ').replace(':',' ').split(" ")).explode())provincies5.columns = ['word']provincies5["replacement"] = "xcountryx"provincies6 = provincies[["gn_name"]]provincies6["gn_name"] = provincies6["gn_name"].str.lower()provincies6 = pd.DataFrame(provincies6["gn_name"].apply(lambda x: str(x).replace('-',' ').replace('.',' ').replace('_',' ').replace(',',' ').replace(':',' ').split(" ")).explode())provincies6.columns = ['word']provincies6["replacement"] = "xcountryx"provincies = pd.concat([provincies1,provincies2,provincies3,provincies4,provincies5,provincies6], axis=0, ignore_index=True)#通貨のリストですcurrencies = pd.read_json("country-by-currency-name.json")currencies1 = currencies[["country"]]currencies1["country"] = currencies1["country"].str.lower()currencies1 = pd.DataFrame(currencies1["country"].apply(lambda x: str(x).replace('-',' ').replace('.',' ').replace('_',' ').replace(',',' ').replace(':',' ').split(" ")).explode())currencies1.columns = ['word']currencies1["replacement"] = "xcountryx"currencies2 = currencies[["currency_name"]]currencies2["currency_name"] = currencies2["currency_name"].str.lower()currencies2 = pd.DataFrame(currencies2["currency_name"].apply(lambda x: str(x).replace('-',' ').replace('.',' ').replace('_',' ').replace(',',' ').replace(':',' ').split(" ")).explode())currencies2.columns = ['word']currencies2["replacement"] = "xcurrencyx"currencies = pd.concat([currencies1,currencies2], axis=0, ignore_index=True)#名前のリストですfirstnames = pd.read_csv("interall.csv", header=None)firstnames = firstnames[firstnames[1]>=10000]firstnames = firstnames[[0]]firstnames[0] = firstnames[0].str.lower()firstnames = pd.DataFrame(firstnames[0].apply(lambda x: str(x).replace('-',' ').replace('.',' ').replace('_',' ').replace(',',' ').replace(':',' ').split(" ")).explode())firstnames.columns = ['word']firstnames["replacement"] = "xfirstnamex"#姓のリストですlastnames = pd.read_csv("intersurnames.csv", header=None)lastnames = lastnames[lastnames[1]>=10000]lastnames = lastnames[[0]]lastnames[0] = lastnames[0].str.lower()lastnames = pd.DataFrame(lastnames[0].apply(lambda x: str(x).replace('-',' ').replace('.',' ').replace('_',' ').replace(',',' ').replace(':',' ').split(" ")).explode())lastnames.columns = ['word']lastnames["replacement"] = "xlastnamex"#月、日、その他の時間情報ですtempこれは元のデータセットのプレビューです
maindataset
次の関数の目的は次のとおりです:
- 上記で作成した辞書を使用して単語を置き換える
- 句読点、二重スペースなどを削除する
def replace_words(tt, lookp_dict): temp = tt.split() res = [] for wrd in temp: res.append(lookp_dict.get(wrd, wrd)) res = ' '.join(res) return resdef preprepare(eingang): ausgang = eingang.lower() ausgang = ausgang.replace(u'\xa0', u' ') ausgang = re.sub(r'^\s*$',' ',str(ausgang)) ausgang = ausgang.replace('|', ' ') ausgang = ausgang.replace('ï', ' ') ausgang = ausgang.replace('»', ' ') ausgang = ausgang.replace('¿', '. ') ausgang = ausgang.replace('', ' ') ausgang = ausgang.replace('"', ' ') ausgang = ausgang.replace("'", " ") ausgang = ausgang.replace('?', ' ') ausgang = ausgang.replace('!', ' ') ausgang = ausgang.replace(',', ' ') ausgang = ausgang.replace(';', ' ') ausgang = ausgang.replace('.', ' ') ausgang = ausgang.replace("(", " ") ausgang = ausgang.replace(")", " ") ausgang = ausgang.replace("{", " ") ausgang = ausgang.replace("}", " ") ausgang = ausgang.replace("[", " ") ausgang = ausgang.replace("]", " ") ausgang = ausgang.replace("~", " ") ausgang = ausgang.replace("@", " ") ausgang = ausgang.replace("#", " ") ausgang = ausgang.replace("$", " ") ausgang = ausgang.replace("%", " ") ausgang = ausgang.replace("^", " ") ausgang = ausgang.replace("&", " ") ausgang = ausgang.replace("*", " ") ausgang = ausgang.replace("<", " ") ausgang = ausgang.replace(">", " ") ausgang = ausgang.replace("/", " ") ausgang = ausgang.replace("\\", " ") ausgang = ausgang.replace("`", " ") ausgang = ausgang.replace("+", " ") ausgang = ausgang.replace("=", " ") ausgang = ausgang.replace("_", " ") ausgang = ausgang.replace("-", " ") ausgang = ausgang.replace(':', ' ') ausgang = ausgang.replace('\n', ' ').replace('\r', ' ') ausgang = ausgang.replace(" +", " ") ausgang = ausgang.replace(" +", " ") ausgang = ausgang.replace('?', ' ') ausgang = re.sub('[^a-zA-Z]', ' ', ausgang) ausgang = re.sub(' +', ' ', ausgang) ausgang = re.sub('\ +', ' ', ausgang) ausgang = re.sub(r'\s([?.!"](?:\s|$))', r'\1', ausgang) return ausgang
辞書データを整理する
dictionary["word"] = dictionary["word"].apply(lambda x: preprepare(x))dictionary = dictionary[dictionary["word"] != " "]dictionary = dictionary[dictionary["word"] != ""]dictionary = {row['word']: row['replacement'] for index, row in dictionary.iterrows()}
テキストデータの準備:タイトル(4回)と概要を連結した新しい列を作成します。これがベクトルに変換されます。なぜなら、この方法で、記事の実際の内容よりもタイトルにより多くの価値を与えるからです。
その後、ストップワードと辞書内の単語を置き換えます
maindataset["NLPtext"] = maindataset["title"] + maindataset["title"] + maindataset["content"] + maindataset["title"] + maindataset["title"]maindataset["NLPtext"] = maindataset["NLPtext"].str.lower()maindataset["NLPtext"] = maindataset["NLPtext"].apply(lambda x: preprepare(str(x)))maindataset["NLPtext"] = maindataset["NLPtext"].apply(lambda x: ' '.join([word for word in x.split() if word not in (nltkstop)]))maindataset["NLPtext"] = maindataset["NLPtext"].apply(lambda x: replace_words(str(x), dictionary))
テキストを準備する最後の部分はステミングです。この場合はモデルのトレーニングをゼロから行っているため、ステミングを行います。
ステミングするかどうかは使用するモデルによって異なります。BERTのような事前学習済みモデルを使用する場合は、ライブラリ内の単語と一致しないため、ステミングは推奨されません。
def steming(sentence): words = word_tokenize(sentence) stems = [snow.stem(whole) for whole in words] oup = ' '.join(stems) return oupmaindataset["NLPtext"] = maindataset["NLPtext"].apply(lambda x: steming(x))maindataset['lentitle'] = maindataset["title"].apply(lambda x: len(str(x).split(' ')))maindataset['lendesc'] = maindataset["content"].apply(lambda x: len(str(x).split(' ')))maindataset['lentext'] = maindataset["NLPtext"].apply(lambda x: len(str(x).split(' ')))maindataset = maindataset[maindataset['NLPtext'].notna()]maindataset = maindataset[maindataset['lentitle']>=4]maindataset = maindataset[maindataset['lendesc']>=4]maindataset = maindataset[maindataset['lentext']>=4]maindataset = maindataset.reset_index(drop=False)maindataset
最後に、doc2vecモデルをトレーニングする時間です。
#データセットをランダムにするtrainset = maindataset.sample(frac=1).reset_index(drop=True)#長さが短すぎるテキストを排除するtrainset = trainset[(trainset['NLPtext'].str.len() >= 5)]#テキスト列を選択するtrainset = trainset[["NLPtext"]]#トークン化してトレーニングセットを生成するtagged_data = []for index, row in trainset.iterrows(): part = TaggedDocument(words=word_tokenize(row[0]), tags=[str(index)]) tagged_data.append(part)#モデルを定義するmodel = Doc2Vec(vector_size=250, min_count=3, epochs=20, dm=1)model.build_vocab(tagged_data)#トレーニングして保存するmodel.train(tagged_data, total_examples=model.corpus_count, epochs=model.epochs)model.save("d2v.model")print("モデルを保存しました")
データと時間のサイズを制限するために、1つのニュースソースにフィルタリングします。
maindataset.groupby('publication').count()['index']
maindatasetF = maindataset[maindataset["publication"]=="Guardian"]
次に、選択した出版物のテキスト情報をベクトル化します。
a = []for index, row in maindatasetF.iterrows(): nlptext = row['NLPtext'] ids = row['index'] vector = model.infer_vector(word_tokenize(nlptext)) vector = pd.DataFrame(vector).T vector.index = [ids] a.append(vector)textvectors = pd.concat(a)textvectors
埋め込みを標準化し、PCA(次元数を削減)
def properscaler(simio): scaler = StandardScaler() resultsWordstrans = scaler.fit_transform(simio) resultsWordstrans = pd.DataFrame(resultsWordstrans) resultsWordstrans.index = simio.index resultsWordstrans.columns = simio.columns return resultsWordstransdatasetR = properscaler(textvectors)def varred(simio): scaler = PCA(n_components=0.8, svd_solver='full') resultsWordstrans = simio.copy() resultsWordstrans = scaler.fit_transform(resultsWordstrans) resultsWordstrans = pd.DataFrame(resultsWordstrans) resultsWordstrans.index = simio.index resultsWordstrans.columns = resultsWordstrans.columns.astype(str) return resultsWordstransdatasetR = varred(datasetR)
今やりたい最初のエクササイズは、類似の記事を探すことです。提供された例に類似する記事を見つけてください。
#インデックスで検索し、元の検索オブジェクトをプリントする
index = 95133
texttofind = maindatasetF[maindatasetF["index"]==index]["title"]
print(str(texttofind))
id = index
print(str(id))
cat = maindatasetF[maindatasetF["index"]==index]["publication"]
print(str(cat))
embdfind = datasetR[datasetR.index==id]
#ユークリッド相互距離を計算し、提供された例に最も類似したものを抽出する
distances = pairwise_distances(X=embdfind, Y=datasetR, metric='euclidean')
distances = pd.DataFrame(distances).T
distances.index = datasetR.index
distances = distances.sort_values(0)
distances = distances.reset_index(drop=False)
distances = pd.merge(distances, maindatasetF[["index","title","publication","content"]], left_on=["index"], right_on=["index"])
pd.options.display.max_colwidth = 100
distances.head(100)[['index',0,'publication','title']]
#抽出されたテキストは意味を成し、提供された例と同様の性質を持つことが分かります。
#クラスタリングには、クラスタの理想的な数を見つけることから始めます。
#この段階では、シルエットとCalinski Harabaszスコアを最大化し、同時に論理的なクラスタの数(解釈が困難すぎないように低すぎず、詳細すぎても適切ではないように高すぎない)を保ちたいと思います。
#モデルとクラスタを試すループ
a = []
X = datasetR.to_numpy(dtype='float')
for ncl in np.arange(2, int(20), 1):
clusterer = Birch(n_clusters=int(ncl))
#出力を混乱させる警告をキャッチする
with warnings.catch_warnings():
warnings.simplefilter("ignore")
cluster_labels2 = clusterer.fit_predict(X)
silhouette_avg2 = silhouette_score(X, cluster_labels2)
calinski2 = calinski_harabasz_score(X, cluster_labels2)
row = pd.DataFrame({"ncl": [ncl], "silKMeans": [silhouette_avg2], "c_hKMeans": [calinski2]})
a.append(row)
scores = pd.concat(a, ignore_index=True)
#結果をプロット
plt.style.use('bmh')
fig, [ax_sil, ax_ch] = plt.subplots(1,2,figsize=(15,7))
ax_sil.plot(scores["ncl"], scores["silKMeans"], 'b-')
ax_ch.plot(scores["ncl"], scores["c_hKMeans"], 'b-')
ax_sil.set_title("シルエット曲線")
ax_ch.set_title("Calinski Harabasz曲線")
ax_sil.set_xlabel('クラスタ')
ax_sil.set_ylabel('平均シルエット')
ax_ch.set_xlabel('クラスタ')
ax_ch.set_ylabel('Calinski Harabasz')
ax_ch.legend(loc="upper right")
plt.show()
#私は5つのクラスタを選び、アルゴリズムを実行します。
ncl_birch = 5
with warnings.catch_warnings():
warnings.simplefilter("ignore")
clusterer2 = Birch(n_clusters=int(ncl_birch))
cluster_labels2 = clusterer2.fit_predict(X)
n_clusters2 = max(cluster_labels2)
silhouette_avg2 = silhouette_score(X, cluster_labels2)
sample_silhouette_values2 = silhouette_samples(X, cluster_labels2)
finalDF = datasetR.copy()
finalDF["cluster"] = cluster_labels2
finalDF["silhouette"] = sample_silhouette_values2
#シルエットスコアをプロット
fig, ax2 = plt.subplots()
ax2.set_xlim([-0.1, 1])
ax2.set_ylim([0, len(X) + (n_clusters2 + 1) * 10])
y_lower = 10
for i in range(min(cluster_labels2),max(cluster_labels2)+1):
ith_cluster_silhouette_values = sample_silhouette_values2[cluster_labels2 == i]
ith_cluster_silhouette_values.sort()
size_cluster_i = ith_cluster_silhouette_values.shape[0]
y_upper = y_lower + size_cluster_i
color = cm.nipy_spectral(float(i) / n_clusters2)
ax2.fill_betweenx(np.arange(y_lower, y_upper), 0, ith_cluster_silhouette_values, facecolor=color, edgecolor=color, alpha=0.7)
ax2.text(-0.05, y_lower + 0.5 * size_cluster_i, str(i))
y_lower = y_upper + 10
ax2.set_title("Birchのシルエットプロット")
ax2.set_xlabel("シルエット係数の値")
ax2.set_ylabel("クラスタラベル")
ax2.axvline(x=silhouette_avg2, color="red", linestyle="--")
ax2.set_yticks([])
ax2.set_xticks([-0.1, 0, 0.2, 0.4, 0.6, 0.8, 1])
これらの結果から、クラスター番号4は他のクラスターよりも少なく「一貫性のある」出現がある可能性があります。これに対して、クラスター番号3と1は明確に定義されています。これは結果の一部です。
showDF = finalDF.sort_values(['cluster','silhouette'], ascending=[False,False]).groupby('cluster').head(3)showDF = pd.merge(showDF[['cluster','silhouette']],maindatasetF[["index",'title']], left_index=True ,right_on=["index"])showDF
クラスター4はテクノロジーに関連するニュース、クラスター3は戦争/国際イベントに関するもの、クラスター2はエンターテイメント、クラスター1はスポーツ、そして通常通り0は「その他」と考えられる場所です。
We will continue to update VoAGI; if you have any questions or suggestions, please contact us!
Was this article helpful?
93 out of 132 found this helpful
Related articles