言語処理100本ノック 2020 第7章を解きました。
- 60. 単語ベクトルの読み込みと表示
- 61. 単語の類似度
- 62. 類似度の高い単語10件
- 63. 加法構成性によるアナロジー
- 64. アナロジーデータでの実験
- 65. アナロジータスクでの正解率
- 66. WordSimilarity-353での評価
- 67. k-meansクラスタリング
- 68. Ward法によるクラスタリング
- 69. t-SNEによる可視化
- 感想
言語処理100本ノック 2020を解いたので、その解法と思ったことをつらつら書きます。
コードはこちら
60. 単語ベクトルの読み込みと表示
まず、google driveからファイルを取得するコードを、こちらを参考に書きました。
# 60. 単語ベクトルの読み込みと表示 # Google Newsデータセット(約1,000億単語)での学習済み単語ベクトル(300万単語・フレーズ,300次元)をダウンロードし, # ”United States”の単語ベクトルを表示せよ.ただし,”United States”は内部的には”United_States”と表現されていることに注意せよ. # 引用 # https://www.mahirokazuko.com/entry/2019/04/27/134235 import requests def download_file_from_google_drive(id, destination): URL = "https://docs.google.com/uc?export=download" session = requests.Session() response = session.get(URL, params = { 'id' : id }, stream = True) token = get_confirm_token(response) if token: params = { 'id' : id, 'confirm' : token } response = session.get(URL, params = params, stream = True) save_response_content(response, destination) def get_confirm_token(response): for key, value in response.cookies.items(): if key.startswith('download_warning'): return value return None def save_response_content(response, destination): CHUNK_SIZE = 32768 with open(destination, "wb") as f: for chunk in response.iter_content(CHUNK_SIZE): if chunk: # filter out keep-alive new chunks f.write(chunk) if __name__ == "__main__": file_id = '0B7XkCwpI5KDYNlNUTTlSS21pQmM' destination = 'GoogleNews-vectors-negative300.bin.gz' download_file_from_google_drive(file_id, destination)
その後、modelの読み込み
# 60. 単語ベクトルの読み込みと表示 # Google Newsデータセット(約1,000億単語)での学習済み単語ベクトル(300万単語・フレーズ,300次元)をダウンロードし, # ”United States”の単語ベクトルを表示せよ.ただし,”United States”は内部的には”United_States”と表現されていることに注意せよ. import gensim model = gensim.models.KeyedVectors.load_word2vec_format('GoogleNews-vectors-negative300.bin', binary=True) print(model['United_States']) # 実行結果は省略
61. 単語の類似度
コサイン類似度はgensimのmodel.similarityでも計算できることを一緒に勉強している方から教わりましたが、今回は自分で定義してます。 gensimの関数を使った方がバグの心配がなさそうですね。
models.keyedvectors – Store and query word vectors — gensim
# 61. 単語の類似度 # “United States”と”U.S.”のコサイン類似度を計算せよ. import numpy as np import gensim from sklearn.metrics.pairwise import cosine_similarity def cos_sim(v1, v2): return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2)) if __name__ == "__main__": # modelのload model = gensim.models.KeyedVectors.load_word2vec_format('../60/GoogleNews-vectors-negative300.bin', binary=True) print(cos_sim(model['United_States'], model['U.S.'])) # 0.7310775
62. 類似度の高い単語10件
# 62. 類似度の高い単語10件 # “United States”とコサイン類似度が高い10語と,その類似度を出力せよ. import gensim from pprint import pprint if __name__ == "__main__": # modelのload model = gensim.models.KeyedVectors.load_word2vec_format('../60/GoogleNews-vectors-negative300.bin', binary=True) pprint(model.most_similar('United_States', topn=10)) # [('Unites_States', 0.7877248525619507), # ('Untied_States', 0.7541370391845703), # ('United_Sates', 0.74007248878479), # ('U.S.', 0.7310774326324463), # ('theUnited_States', 0.6404393911361694), # ('America', 0.6178410053253174), # ('UnitedStates', 0.6167312264442444), # ('Europe', 0.6132988929748535), # ('countries', 0.6044804453849792), # ('Canada', 0.6019070148468018)]
63. 加法構成性によるアナロジー
most_similarはpositive, negativeに指定した単語をちゃんと除外してくれるようなソースコードとなってました。 gensim.models.word2vec
# 63. 加法構成性によるアナロジー # “Spain”の単語ベクトルから”Madrid”のベクトルを引き,”Athens”のベクトルを足したベクトルを計算し,そのベクトルと類似度の高い10語とその類似度を出力せよ. import gensim from pprint import pprint if __name__ == "__main__": # modelのload model = gensim.models.KeyedVectors.load_word2vec_format('../60/GoogleNews-vectors-negative300.bin', binary=True) pprint(model.most_similar(positive=['Spain','Athens'], negative=['Madrid'], topn=10)) # [('Greece', 0.6898481249809265), # ('Aristeidis_Grigoriadis', 0.5606848001480103), # ('Ioannis_Drymonakos', 0.5552908778190613), # ('Greeks', 0.545068621635437), # ('Ioannis_Christou', 0.5400862693786621), # ('Hrysopiyi_Devetzi', 0.5248444676399231), # ('Heraklio', 0.5207759737968445), # ('Athens_Greece', 0.516880989074707), # ('Lithuania', 0.5166866183280945), # ('Iraklion', 0.5146791934967041)]
64. アナロジーデータでの実験
# 64. アナロジーデータでの実験 # 単語アナロジーの評価データをダウンロードし,vec(2列目の単語) - vec(1列目の単語) + vec(3列目の単語)を計算し,そのベクトルと類似度が最も高い単語と,その類似度を求めよ.求めた単語と類似度は,各事例の末尾に追記せよ. # !wget http://download.tensorflow.org/data/questions-words.txt import time from multiprocessing import Pool import gensim def get_most_similar(model, line: str) -> str: '''questions-wordsのデータから、類似単語と類似度を計算する''' # 最初の単語が:だった場合は\nだけ削除して返す。 first_word = line.split(' ')[0] if first_word == ':': return line.replace('\n','') # 類似度を計算 second_word = line.split(' ')[1] third_word = line.split(' ')[2] result = model.most_similar(positive=[second_word, third_word], negative=[first_word], topn=1) line_add_result = line[:-1] + ' ' + str(result[0]) return line_add_result if __name__ == "__main__": start = time.time() model = gensim.models.KeyedVectors.load_word2vec_format('../60/GoogleNews-vectors-negative300.bin', binary=True) with open('questions-words.txt') as f: questions_words = f.readlines() # 元データに類似単語を追加したlistを取得 result_list = [get_most_similar(model, line) for line in questions_words] join_result = '\n'.join(result_list) with open("result.txt", 'w') as f: f.write(join_result) elapsed_time = time.time() - start print (f"elapsed_time: {elapsed_time: .2f}[sec]")
1時間弱かかる処理のため、高速化したいなと思い並列処理も試しましたが動きませんでした。 いまいち原因わからず。
# 注:このコードは動きません import time from typing import Tuple from multiprocessing import Pool from multiprocessing import Process, Manager import gensim def get_most_similar(input_data) -> str: '''questions-wordsのデータから、類似単語と類似度を計算する''' # 引数を分ける model, line = input_data # 最初の単語が:だった場合は\nだけ削除して返す。 first_word = line.split(' ')[0] if first_word == ':': return line.replace('\n','') # 類似度を計算 second_word = line.split(' ')[1] third_word = line.split(' ')[2] result = model.most_similar(positive=[second_word, third_word], negative=[first_word], topn=1) line_add_result = line[:-1] + ' ' + str(result[0]) return line_add_result if __name__ == "__main__": start = time.time() # データの読み込み model = gensim.models.KeyedVectors.load_word2vec_format('../60/GoogleNews-vectors-negative300.bin', binary=True) with open('questions-words_dummy.txt') as f: questions_words = f.readlines() # 引数をまとめる input_data_list = [(model,line) for line in questions_words] # import ipdb; ipdb.set_trace() with Pool(4) as p: result_list = p.map(get_most_similar, input_data_list) join_result = '\n'.join(result_list) with open("result_dummy_multiprocess_test.txt", 'w') as f: f.write(join_result) elapsed_time = time.time() - start print (f"elapsed_time: {elapsed_time: .2f}[sec]")
65. アナロジータスクでの正解率
問題の意味が分からない部分があったので、こちらのブログを参考にしました。
# 65. アナロジータスクでの正解率 # 64の実行結果を用い,意味的アナロジー(semantic analogy)と文法的アナロジー(syntactic analogy)の正解率を測定せよ. import numpy as np import gensim if __name__ == "__main__": # model = gensim.models.KeyedVectors.load_word2vec_format('../60/GoogleNews-vectors-negative300.bin', binary=True) with open('../64/result.txt') as f: result = f.readlines() syntactic, semantic = [], [] for line in result: if line.startswith(": gram"): ctg = "syntactic" elif line.startswith(":"): ctg = "semantic" else: true_word = line.split(' ')[3] pred_word = line.split(' ')[4][2:-2] is_correct = (true_word == pred_word) if ctg == "syntactic": syntactic.append(is_correct) elif ctg == "semantic": semantic.append(is_correct) else: print('No ctg') syntactic_acc_rate = np.array(syntactic).mean() semantic_acc_rate = np.array(semantic).mean() print(f'semantic_acc_rate: {semantic_acc_rate: .4f}') print(f'syntactic_acc_rate: {syntactic_acc_rate: .4f}') # semantic_acc_rate: 0.7309 # syntactic_acc_rate: 0.7400
66. WordSimilarity-353での評価
# 66. WordSimilarity-353での評価 # The WordSimilarity-353 Test Collectionの評価データをダウンロードし,単語ベクトルにより計算される類似度のランキングと,人間の類似度判定のランキングの間のスピアマン相関係数を計算せよ. # !wget http://www.gabrilovich.com/resources/data/wordsim353/wordsim353.zip # !unzip wordsim353.zip -d wordsim353 import gensim from scipy import stats if __name__ == "__main__": model = gensim.models.KeyedVectors.load_word2vec_format('../60/GoogleNews-vectors-negative300.bin', binary=True) with open('wordsim353/combined.csv') as f: next(f) # headerは読み込まない combined = f.readlines() human_sim_list = [] wordvec_sim_list = [] for line in combined: # 単語ベクトルにより計算される類似度のリストの作成 first_word = line.split(',')[0] second_word = line.split(',')[1] wordvec_sim = model.similarity(first_word, second_word) wordvec_sim_list.append(wordvec_sim) # 人間の類似度判定のリストの作成 human_sim = float(line.split(',')[2][:-2]) human_sim_list.append(human_sim) print(stats.spearmanr(wordvec_sim_list, human_sim_list))
67. k-meansクラスタリング
国名をquestions-wordsからかき集めたのですが、別の手法(pycountryの利用や、国一覧サイトの利用)などの方が良さそうと、一緒に解いてくれている方の解き方を見て思いました。
# 67. k-meansクラスタリング # 国名に関する単語ベクトルを抽出し,k-meansクラスタリングをクラスタ数k=5として実行せよ. import pickle import pandas as pd import numpy as np import gensim from sklearn.cluster import KMeans def get_country_name(questions_words: list, category_name: str) -> list: """重複ありでの国名をquestions-wordsの: capital-common-countries, 1,3列目から取得 Args: questions_words (list): questions_words.txtのリスト category_name (str): questions_words.txtの:で区切られるジャンル return: list: 国名のリスト """ countries_set = set() for line in questions_words: if line.startswith(f": {category_name}"): ctg = category_name elif line.startswith(":"): ctg = "others" else: if ctg == category_name: country_1 = line.split(' ')[1] country_3 = line.split(' ')[3].replace('\n','') countries_set.add(country_1) countries_set.add(country_3) elif ctg == "others": continue # 国名のlist countries_list = list(countries_set) return countries_list if __name__ == "__main__": model = gensim.models.KeyedVectors.load_word2vec_format('../60/GoogleNews-vectors-negative300.bin', binary=True) with open('../64/questions-words.txt') as f: questions_words = f.readlines() # 「capital-common-countries」「capital-world」の区切りから国名を取得 common_countries_list = get_country_name(questions_words, "capital-common-countries") world_countries_list = get_country_name(questions_words, "capital-world") # 重複を無くした国名を一つのlistにまとめる countries_list = list(set(common_countries_list + world_countries_list)) # 国名のvectorを取得 vec_list = [model[country] for country in countries_list] # kmeansの実施 country_vec_arr = np.array(vec_list) kmeans = KMeans(n_clusters=5, random_state=33).fit(country_vec_arr) # 保存 np.save('country_vec_arr', country_vec_arr) with open('countries_list.txt', "wb") as f: pickle.dump(countries_list, f) # 見やすく表示 print( pd.DataFrame( {'label':kmeans.labels_, 'coutry':countries_list}) .sort_values('label') ) # label coutry # 87 0 Algeria # 24 0 Mozambique # 25 0 Malawi # 28 0 Mali # 85 0 Botswana # .. ... ... # 22 4 Austria # 91 4 Switzerland # 92 4 Iraq # 76 4 Jordan # 86 4 Morocco
68. Ward法によるクラスタリング
classを使いたい気持ちがあり、Dataの読み込みに無駄にclass使ってみたりしています。ただ旨味はあまりありません。
# 68. Ward法によるクラスタリング # 国名に関する単語ベクトルに対し,Ward法による階層型クラスタリングを実行せよ.さらに,クラスタリング結果をデンドログラムとして可視化せよ. import pickle import pandas as pd import numpy as np import matplotlib.pyplot as plt from scipy.cluster.hierarchy import linkage from scipy.cluster.hierarchy import dendrogram class Data: def __init__(self): pass def load_country_vec(self): return np.load('../67/country_vec_arr.npy') def load_countries_list(self): return pickle.load(open('../67/countries_list.txt', 'rb')) if __name__ == "__main__": # データの読み込み data = Data() country_vec_arr = data.load_country_vec() countries_list = data.load_countries_list() # 階層型clusteringの実施 cluster = linkage(country_vec_arr, method='ward') # 結果を可視化 fig = plt.figure(figsize=(12, 6)) dendrogram(cluster, labels=countries_list) plt.title('country_dendrogram') plt.savefig('country_dendrogram.png', bbox_inches='tight', dpi=300)
結果の画像はこちら
それなりにクラスタリングできていそう。
69. t-SNEによる可視化
takapyさんおすすめのadjustTextを使ってみました。
takapyさん資料 word2vecを利用した埋め込み分析とSWEMを用いた比較実験 - Speaker Deck
# 69. t-SNEによる可視化 # ベクトル空間上の国名に関する単語ベクトルをt-SNEで可視化せよ import pickle import numpy as np import matplotlib.pyplot as plt from sklearn.manifold import TSNE from adjustText import adjust_text class Data: def __init__(self): pass def load_country_vec(self): return np.load('../67/country_vec_arr.npy') def load_countries_list(self): return pickle.load(open('../67/countries_list.txt', 'rb')) if __name__ == "__main__": # データの読み込み data = Data() country_vec_arr = data.load_country_vec() countries_list = data.load_countries_list() # tsneの実施 tsne = TSNE(n_components=2, random_state = 33) country_embedded = tsne.fit_transform(country_vec_arr) # 可視化 plt.scatter(country_embedded[:,0], country_embedded[:,1]) texts = [plt.text( country_embedded[i,0], country_embedded[i,1], countries_list[i], fontsize=6 ) for i in range(len(countries_list))] adjust_text(texts) plt.savefig('tsne.png', bbox_inches='tight', dpi=300)
結果はこちら Japanの周りにアジア系の国が集まっているので、まあ良さげな結果に見えます。
感想
gensimのモデルを使って、分散表現による様々な手法を学ぶことができました。gensim便利!