0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

fastTextの言語獲得②

Posted at

はじめに

こんにちは。
定期的に書くために前回の記事を短くして少しずつ書いていこうと考えていたのですが、忙しくて全然書けませんでした。
今回で書き切ります。

さっと本編にいきましょう

この記事は「fastTextの言語獲得①」の続きの記事になります。まだ①を見てない方はそちらを見てからの方が良いかと思います。

fastTextの言語獲得は以下の順番となっております。
①五十音をonehotベクトルにする
②MLP学習時の正解のfastTextの生成
③MLPの生成
④MLPの学習
⑤MLPの精度の確認

fastTextの言語獲得①では、「①五十音をonehotベクトルにする」の1つしか書いていません。
今回は
②MLP学習時の正解のfastTextの生成
③MLPの生成
④MLPの学習
⑤MLPの精度の確認
の4つをいっきに書きたいとおもいます。

②MLP学習時の正解のfastTextの生成

今回のやりたいことはMLPからの出力をfastTextの単語ベクトルと同じものを出力させるということ

まずは、fastTextのサイトに飛んで、日本語のfastTextのモデルをダウンロードしてください。
URLはこれです。
https://fasttext.cc/docs/en/crawl-vectors.html

注意
ダウンロードする言語は「Japanese(日本語)」です。「Javanese(ジャワ語)」ではないのでお気をつけてください。
一字違いなのでよく見ないと間違えちゃいます。(自分はJavaneseをダウンロードしてしまい痛い目見ました...)
ダウンロードしたファイルの名前が「cc.ja.300.bin」であれば大丈夫です。

下記コードの「model_path = "PATH/cc.ja.300.bin"#モデル」の行でダウンロードしたモデルへのパスを設定してください。

五十音をfasttextに入力し、それぞれの単語ベクトルを取得します。取得した単語ベクトルが正解ベクトルとなります。取得した正解ベクトルとonehotベクトルを対応させて学習データをシャッフルさせます。

②MLP学習時の正解のfastTextの生成
# 五十音のリスト
gojyuon = [
    'あ', 'い', 'う', 'え', 'お',
    'か', 'き', 'く', 'け', 'こ',
    'さ', 'し', 'す', 'せ', 'そ',
    'た', 'ち', 'つ', 'て', 'と',
    'な', 'に', 'ぬ', 'ね', 'の',
    'は', 'ひ', 'ふ', 'へ', 'ほ',
    'ま', 'み', 'む', 'め', 'も',
    'や', 'ゆ', 'よ',
    'ら', 'り', 'る', 'れ', 'ろ',
    'わ', 'を', 'ん'
]

# 辞書を使って、文字とインデックスの対応を作成
char_to_index = {char: i for i, char in enumerate(gojyuon)}
#print(char_to_index)

# One-Hotベクトルを生成する関数
def make_one_hot_vector(char, char_to_index):
    # ベクトルの初期化(すべて0のベクトル)
    vector = np.zeros(len(char_to_index))
    # 対応するインデックスに1を設定
    if char in char_to_index:
        vector[char_to_index[char]] = 1
    return torch.tensor(vector)

# 五十音の文字ごとにOne-Hotベクトルを生成
dict_one_hot_vectors = {char: make_one_hot_vector(char, char_to_index) for char in gojyuon}
#print(dict_one_hot_vectors[gojyuon[0]])
print(len(dict_one_hot_vectors))
one_hot_vectors=[]
for i in range(len(dict_one_hot_vectors)):
    one_hot_vectors.append(dict_one_hot_vectors[gojyuon[i]])
print(one_hot_vectors)



model_path = "PATH/cc.ja.300.bin"#モデル
model = fasttext.load_model(model_path)

#五十音をfasttextに入力、ベクトル取得
gojyuon_fast_vec=[]
for i in tqdm(range(len(gojyuon)), desc="文字数"):
    collect_V=model.get_word_vector(gojyuon[i])
    gojyuon_fast_vec.append(torch.tensor(collect_V, dtype=torch.float64))
    

paired = list(zip(one_hot_vectors, gojyuon_fast_vec))

# ペアをシャッフル
random.shuffle(paired)

# シャッフルしたペアを再び辞書に戻す
random_one_hot_vectors, random_gojyuon_fast_vec = map(list, zip(*paired))


#ペアの順番等を確認したい場合のコード↓
'''
indices = []
for i, (key, value) in enumerate(dict_one_hot_vectors.items()):
    if torch.equal(value, random_one_hot_vectors[0]):  # 値がtarget_tensorと等しいか確認
        indices.append(i)
# 結果を表示
print("Indices:", indices)
#print(random_gojyuon_fast_vec[indices])
# 結果を表示
#print(random_one_hot_vectors)
print(gojyuon_fast_vec[indices[0]])
print(one_hot_vectors[indices[0]])
print(len(random_gojyuon_fast_vec))
'''

# リストの結果を表示
#print(random_one_hot_vectors, random_gojyuon_fast_vec)

上記コードでonehotベクトルを入力した際のMLPより生成されるベクトルの正解ベクトルを得ることができました。

③MLPの生成

続いてonehotベクトルを入力するMLPを生成します。

MLPはGeneratorクラスを作成して、インスタンスを生成します。

今回の五十音は46単語となっているので、onehotベクトルは46次元となります。よって、入力層は46次元とします。
出力層は正解ベクトルであるfasttextより生成されたベクトルの次元数となるので、300次元となります。
下記コードでMLPを生成します。

③MLPの生成
class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()

        self.layer1 = nn.Linear(46, 96, dtype=torch.float64)
        self.activation1 = nn.ReLU()

        self.layer2 = nn.Linear(96, 146, dtype=torch.float64)
        self.activation2 = nn.ReLU()

        self.layer3 = nn.Linear(146, 196, dtype=torch.float64)
        self.activation3 = nn.ReLU()

        self.layer4 = nn.Linear(196, 246, dtype=torch.float64)
        self.activation4 = nn.ReLU()

        self.output_layer = nn.Linear(246, 300, dtype=torch.float64)

    def forward(self, x):
        x = self.activation1(self.layer1(x))
        x = self.activation2(self.layer2(x))
        x = self.activation3(self.layer3(x))
        x = self.activation4(self.layer4(x))
        x = self.output_layer(x)

        return x
        
generator = Generator()#.to(device)

print(generator)

今回は次元数が多くないので、中間層をある程度増やしても学習に時間はかからないし、なるべく情報失わせないように学習を行いたいため、中間層を多めに設定しました。
出力層以外の活性化関数には、ReLU関数を用いました。出力層は恒等関数となっています。

④MLPの学習

学習に必要なonehotベクトル、正解ベクトル、MLPの3つを揃えたので、学習を行います。

loss関数には、CosineEmbeddingLossを用います。
CosineEmbeddingLossは対象となる2つのベクトルのコサイン類似度を用いてlossを計算します。具体的には、2つのベクトルのコサイン類似度が高くなるようにlossを計算するのか、コサイン類似度が低くなるようにlossを計算するのかという2つの計算方法があります。
今回はMLPより生成されるベクトルが正解ベクトルになるように学習を行いたいので、2つのベクトルのコサイン類似度が高くなるようにlossを計算します。

下記コード部分でオプティマイザのセットアップも行います。 学習率は0.0002となっています。

optimizerG = optim.Adam(generator.parameters(),lr=0.0002, betas=(beta1, 0.999), weight_decay=1e-5)  # 生成器G用

 
下記コード部分で学習されたMLPを保存するコードとなっています。モデルを保存したいディレクトリを指定してください。

save_dir = 'PATH/modelを保存したいディレクトリ'
save_G_path = '{}/epoch-{}-model.pth'.format(save_dir, epoch)

fasttextへのモデルのパス、学習したMLPの保存したいディレクトリへのパスの部分を書き換えたら学習を行いましょう。
以下に学習を行う際のコードを記載します。
以下のコードは、
①五十音をonehotベクトルにする
②MLP学習時の正解のfastTextの生成
③MLPの生成
④MLPの学習
の4つをまとめたコードとなっています。

train.py
import numpy as np
import fasttext
from tqdm import tqdm
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import random
from gensim.models import KeyedVectors
import fasttext.util
import matplotlib.pyplot as plt

# 五十音のリスト
gojyuon = [
    'あ', 'い', 'う', 'え', 'お',
    'か', 'き', 'く', 'け', 'こ',
    'さ', 'し', 'す', 'せ', 'そ',
    'た', 'ち', 'つ', 'て', 'と',
    'な', 'に', 'ぬ', 'ね', 'の',
    'は', 'ひ', 'ふ', 'へ', 'ほ',
    'ま', 'み', 'む', 'め', 'も',
    'や', 'ゆ', 'よ',
    'ら', 'り', 'る', 'れ', 'ろ',
    'わ', 'を', 'ん'
]

# 辞書を使って、文字とインデックスの対応を作成
char_to_index = {char: i for i, char in enumerate(gojyuon)}
#print(char_to_index)

# One-Hotベクトルを生成する関数
def make_one_hot_vector(char, char_to_index):
    # ベクトルの初期化(すべて0のベクトル)
    vector = np.zeros(len(char_to_index))
    # 対応するインデックスに1を設定
    if char in char_to_index:
        vector[char_to_index[char]] = 1
    return torch.tensor(vector)

# 五十音の文字ごとにOne-Hotベクトルを生成
dict_one_hot_vectors = {char: make_one_hot_vector(char, char_to_index) for char in gojyuon}
#print(dict_one_hot_vectors[gojyuon[0]])
print(len(dict_one_hot_vectors))
one_hot_vectors=[]
for i in range(len(dict_one_hot_vectors)):
    one_hot_vectors.append(dict_one_hot_vectors[gojyuon[i]])
#print(one_hot_vectors)


model_path = "PATH/cc.ja.300.bin"#モデル
model = fasttext.load_model(model_path)

#五十音をfasttextに入力、ベクトル取得
gojyuon_fast_vec=[]
for i in tqdm(range(len(gojyuon)), desc="文字数"):
    collect_V=model.get_word_vector(gojyuon[i])
    gojyuon_fast_vec.append(torch.tensor(collect_V, dtype=torch.float64))

paired = list(zip(one_hot_vectors, gojyuon_fast_vec))

# ペアをシャッフル
random.shuffle(paired)

# シャッフルしたペアを再び辞書に戻す
random_one_hot_vectors, random_gojyuon_fast_vec = map(list, zip(*paired))


class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()

        self.layer1 = nn.Linear(46, 96, dtype=torch.float64)
        self.activation1 = nn.ReLU()

        self.layer2 = nn.Linear(96, 146, dtype=torch.float64)
        self.activation2 = nn.ReLU()

        self.layer3 = nn.Linear(146, 196, dtype=torch.float64)
        self.activation3 = nn.ReLU()

        self.layer4 = nn.Linear(196, 246, dtype=torch.float64)
        self.activation4 = nn.ReLU()

        self.output_layer = nn.Linear(246, 300, dtype=torch.float64)

    def forward(self, x):
        x = self.activation1(self.layer1(x))
        x = self.activation2(self.layer2(x))
        x = self.activation3(self.layer3(x))
        x = self.activation4(self.layer4(x))
        x = self.output_layer(x)

        return x
        
generator = Generator()#.to(device)

print(generator)

G_criterion = nn.CosineEmbeddingLoss()

# オプティマイザ−のセットアップ
beta1 = 0.9
optimizerG = optim.Adam(generator.parameters(), lr=0.0002, betas=(beta1, 0.999), weight_decay=1e-5)  # 生成器G用

train_loss=[]
epoch=2000
for j in tqdm(range(epoch), desc=" epoch"):

    for i in tqdm(range(len(gojyuon)), desc="文字数"):

        optimizerG.zero_grad()
        output_Generator = generator(random_one_hot_vectors[i])
        errG = G_criterion(output_Generator.unsqueeze(0), random_gojyuon_fast_vec[i].unsqueeze(0),torch.ones(1))
        print(errG)
        errG.backward()
        optimizerG.step()
        train_loss.append(errG.item())

save_dir = 'PATH/modelを保存したいディレクトリ'
save_G_path = '{}/epoch-{}-model.pth'.format(save_dir, epoch)
torch.save({
    'model_state_dict': generator.state_dict(),
    'optimizer_state_dict': optimizerG.state_dict()
}, save_G_path)
plt.figure()
plt.plot(range(len(train_loss)), train_loss, label='Training Loss')
plt.xlabel('Number of documents studied')
plt.ylabel('Loss')
plt.title('Training Loss over Epochs (Epoch {})'.format(epoch))
# plt.legend()
plt.savefig('training_loss(Epoch {}).png'.format(epoch))

エポックは特に気になかったので、2000となっています(やりすぎ都市伝説)。

学習の際のlossのグラフは以下のようになりました。
training_loss(Epoch 2000).png

やはり2000エポックも必要ないですね。
まあ、学習している途中のlossを見ててもいらないなと思っていましたが、コードの都合上設定したエポック回数学習させないとグラフが得られないので、学習させました。

⑤MLPの精度の確認

学習したMLPを生成したので、精度を確認します。

精度の確認には、MLPより生成されるベクトルがfasttextのベクトル空間内のどの単語に最も近いのかをコサイン類似度を用いて計算します。
その結果、fasttextのベクトル空間内で最もコサイン類似度が高い単語がMLPに入力した単語(ここではonehotベクトルに変換したもの)であれば、MLPはfasttextのベクトルを学習しており、fasttextの言語を獲得したこととなる。学習を行った五十音である46単語について精度を確認する。

注意
下記のコードの部分で使用するfasttextのモデルはMLPを学習する際に用いたfasttextのモデルとは違うモデルとなっています。
学習する際のモデルは「cc.ja.300.bin」ですが、精度を調べるためのモデルは「cc.ja.300.vec」となっています。fasttextのサイトに行き、japaneseのtextという部分をクリックするとダウンロードできます(binの隣にあるやつです)。

model = KeyedVectors.load_word2vec_format("PATH/cc.ja.300.vec", binary=False)

以下に学習したMLPを用いて精度を確認するためのコードを記載する。

predict.py
import numpy as np
import fasttext
from tqdm import tqdm
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import random
from gensim.models import KeyedVectors
import fasttext.util
import matplotlib.pyplot as plt
import fasttext
from gensim.models import KeyedVectors

# 五十音のリスト
gojyuon = [
    'あ', 'い', 'う', 'え', 'お',
    'か', 'き', 'く', 'け', 'こ',
    'さ', 'し', 'す', 'せ', 'そ',
    'た', 'ち', 'つ', 'て', 'と',
    'な', 'に', 'ぬ', 'ね', 'の',
    'は', 'ひ', 'ふ', 'へ', 'ほ',
    'ま', 'み', 'む', 'め', 'も',
    'や', 'ゆ', 'よ',
    'ら', 'り', 'る', 'れ', 'ろ',
    'わ', 'を', 'ん'
]

# 辞書を使って、文字とインデックスの対応を作成
char_to_index = {char: i for i, char in enumerate(gojyuon)}
#print(char_to_index)

# One-Hotベクトルを生成する関数
def make_one_hot_vector(char, char_to_index):
    # ベクトルの初期化(すべて0のベクトル)
    vector = np.zeros(len(char_to_index))
    # 対応するインデックスに1を設定
    if char in char_to_index:
        vector[char_to_index[char]] = 1
    return torch.tensor(vector)

# 五十音の文字ごとにOne-Hotベクトルを生成
dict_one_hot_vectors = {char: make_one_hot_vector(char, char_to_index) for char in gojyuon}

one_hot_vectors=[]
for i in range(len(dict_one_hot_vectors)):
    one_hot_vectors.append(dict_one_hot_vectors[gojyuon[i]])
#print(one_hot_vectors)


# GensimでFastTextモデルをロード
model = KeyedVectors.load_word2vec_format("PATH/cc.ja.300.vec", binary=False)

class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()

        self.layer1 = nn.Linear(46, 96, dtype=torch.float64)
        self.activation1 = nn.ReLU()

        self.layer2 = nn.Linear(96, 146, dtype=torch.float64)
        self.activation2 = nn.ReLU()

        self.layer3 = nn.Linear(146, 196, dtype=torch.float64)
        self.activation3 = nn.ReLU()

        self.layer4 = nn.Linear(196, 246, dtype=torch.float64)
        self.activation4 = nn.ReLU()

        self.output_layer = nn.Linear(246, 300, dtype=torch.float64)

    def forward(self, x):
        x = self.activation1(self.layer1(x))
        x = self.activation2(self.layer2(x))
        x = self.activation3(self.layer3(x))
        x = self.activation4(self.layer4(x))
        x = self.output_layer(x)

        return x


generator = Generator()#.to(device)

checkpoint = torch.load("mdoel_PATH/epoch-2000-model.pth")
generator.load_state_dict(checkpoint['model_state_dict'])


for j in range(len(one_hot_vectors)):
    #検索する単語
    print("「",gojyuon[j],"」のonehotベクトルを入力")

    # 近似単語を検索
    vector = generator(one_hot_vectors[j]).tolist()
    vector=np.array(vector)# <class 'numpy.ndarray'>
    # ベクトルに最も近い単語を検索
    similar_words = model.most_similar([vector], topn=1)
    # 結果を表示
    for word, similarity in similar_words:
        print(f"最も類似しているベクトルの単語: {word}, 類似度: {similarity:.4f}")
    print()

コード中の以下の2つのコードの部分のパスを適宜変えてください。

model = KeyedVectors.load_word2vec_format("PATH/cc.ja.300.vec", binary=False)
checkpoint = torch.load("mdoel_PATH/epoch-2000-model.pth")

結果

結果
  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9987

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9988

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9987

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9996

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9989

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9989

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9988

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9996

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9995

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9994

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9991

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9986

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9989

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9995

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9999

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9987

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9996

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9981

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9986

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9984

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9990

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9984

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9998

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9997

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9995

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9984

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9990

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9994

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9989

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9997

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9999

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9986

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9990

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9968

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9988

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9989

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9999

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9991

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9997

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9990

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9985

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9998

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9985

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9999

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9993

  のonehotベクトルを入力
最も類似しているベクトルの単語: , 類似度: 0.9995

精度は46/46となり、100%の精度である。
また、そのコサイン類似度はすべての0.99以上である。

終わりに

すみません。全て書き切る予定でしたが、時間の都合でここで終わりにさせてください。
あとは考察と感想を次の記事に書きたいと思います。
今からは「アイドルのコンセプトは歌詞にも現れているのか?」この記事をもう一度やった結果の記事を作成します。

分かりにくいところや疑問点があれば、気軽にコメントなどで教えてください。
そこについて詳しい説明などをしたいと思います。

ここまで読んでいただきありがとうございます。
また書こうと思います。
また逢う日まで。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?