言語処理100本ノック 2020 第7章を解きました。

言語処理100本ノック 2020を解いたので、その解法と思ったことをつらつら書きます。

コードはこちら

github.com

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. アナロジータスクでの正解率

問題の意味が分からない部分があったので、こちらのブログを参考にしました。

カテゴリ名にgramが含まれるものを文法的アナロジー, そうでないものを意味的アナロジーとします。

# 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)

結果の画像はこちら

それなりにクラスタリングできていそう。

f:id:sinchir0:20210621082338p:plain
country_dendrogram

69. t-SNEによる可視化

takapyさんおすすめのadjustTextを使ってみました。

takapyさん資料 word2vecを利用した埋め込み分析とSWEMを用いた比較実験 - Speaker Deck

adjustText公式 GitHub - Phlya/adjustText: A small library for automatically adjustment of text position in matplotlib plots to minimize overlaps.

# 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の周りにアジア系の国が集まっているので、まあ良さげな結果に見えます。

f:id:sinchir0:20210621085101p:plain
tsne

感想

gensimのモデルを使って、分散表現による様々な手法を学ぶことができました。gensim便利!