2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PythonAdvent Calendar 2023

Day 20

ChatGPTと作る 和歌生成AI : 1. LSTM 編

Last updated at Posted at 2023-11-15

皆さんこんにちは。今日も引き続きChatGPT先生をお迎えして、「ChatGPTとLSTMで作る和歌生成AI」というテーマで雑談したいと思います。

本日用いる和歌のデータは、国際日本文化研究センターで公開されている 「和歌データベース」 を利用します。

それではChatGPT先生、よろしくお願いします。


assistant:

はじめまして、ChatGPTです。和歌を生成するLSTMモデルについてお話ししますね。

まず基本的なことから説明します。

LSTMとは、Long Short-Term Memoryの略で、時系列データに対する深層学習の一つです。LSTMは、"ゲート"という概念を導入することで、長い時間差のある情報を取り扱うことができるという特性を持っています。

和歌作りをPythonで行う場合、LSTMを使って文字列を予測するというアプローチを取ることが多いです。

基本的な流れは以下のようになります。

  1. まず大量の和歌データを用意します。
  2. 各和歌を文字列として捉え、一定文字数ごとに区切って入力データを作ります。例えば、「春はあけぼの」を5文字ずつ区切ると、「春はあけ」「はあけぼ」のようになります。
  3. その次の文字を正解ラベルとします。上記の例では、「春はあけ」の次は「ぼ」、「はあけぼ」の次は「の」となります。
  4. これを大量の和歌データに対して行い、学習データを作ります。
  5. LSTMでこの学習データを学習させます。ここでは、前の5文字から次の文字が何かを予測するモデルを学習させることになります。
  6. 生成時には、初めの数文字を与えてやり、それ以降はモデルの予測結果を次々と入力していきます。生成された文字を連結することで和歌が完成します。

まずは実際にこの流れを元にコードを書いてみてはいかがでしょうか。何か不明な点があればいつでも質問してください。


user:

ありがとうございます。それでは、waka.txt に、1行あたり1つずつ、和歌が入ってるものとします。このデータをLSTMで学習し、学習曲線を描画するコードをPyTorchで書いてください。


assistant:

了解しました。次のようなコードを考えてみましょう:

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
from torch.utils.data import Dataset, DataLoader
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt

# 文字列を数値に変換するための辞書
with open("waka.txt", "r", encoding="utf-8") as f:
    text = f.read()
chars = sorted(list(set(text)))
char_to_int = dict((c, i) for i, c in enumerate(chars))
int_to_char = dict((i, c) for i, c in enumerate(chars))

# データセットの作成
sequence_length = 5
dataX = []
dataY = []
for i in range(0, len(text) - sequence_length, 1):
    seq_in = text[i:i + sequence_length]
    seq_out = text[i + sequence_length]
    dataX.append([char_to_int[char] for char in seq_in])
    dataY.append(char_to_int[seq_out])


class MyDataset(Dataset):
    def __init__(self, dataX, dataY):
        self.dataX = dataX
        self.dataY = dataY

    def __len__(self):
        return len(self.dataX)

    def __getitem__(self, idx):
        return self.dataX[idx], self.dataY[idx]


# LSTM モデル
class Net(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(Net, self).__init__()
        self.hidden_size = hidden_size
        self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
        self.out = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        x, _ = self.lstm(x)
        x = self.out(x[:, -1, :])
        return x


# 学習関数
def train(epoch, model, dataloader, criterion, optimizer):
    model.train()
    running_loss = 0.0
    for batch_idx, (data, target) in enumerate(dataloader):
        data, target = data.to(device), target.to(device)
        data = data.view(-1, sequence_length, 1).float()
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
    return running_loss / len(dataloader)

# 損失をプロットする関数
def plot_loss(loss_list):
    plt.plot(list(range(len(loss_list))), loss_list, '-b', label='loss')
    plt.legend(loc=0)
    plt.show()


device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

model = Net(1, 128, len(chars)).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

train_data, val_data, train_label, val_label = train_test_split(dataX, dataY, test_size=0.2)

train_dataset = MyDataset(train_data, train_label)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

loss_list = []
for epoch in range(100):
    loss = train(epoch, model, train_loader, criterion, optimizer)
    print('Train Epoch: {} \t Loss: {:.6f}'.format(epoch+1, loss))
    loss_list.append(loss)

# 損失の描画
plot_loss(loss_list)

このコードは、"waka.txt"に含まれる和歌を学習し、各エポックの損失を計算・描画します。和歌を一定の文字数で区切り、次の文字を予測するようにLSTMモデルを学習しています。そして、各エポックごとの学習損失をリストに保持して描画しています。

ただし、上記のコードでは和歌の生成までは行われていません。学習したモデルを使って具体的に和歌を生成する部分については、別途コードを記述する必要があります。


user:

次のエラーを修正したコードを書いてください。

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[1], line 88
     86 loss_list = []
     87 for epoch in range(100):
---> 88     loss = train(epoch, model, train_loader, criterion, optimizer)
     89     print('Train Epoch: {} \t Loss: {:.6f}'.format(epoch+1, loss))
     90     loss_list.append(loss)

Cell In[1], line 58, in train(epoch, model, dataloader, criterion, optimizer)
     56 running_loss = 0.0
     57 for batch_idx, (data, target) in enumerate(dataloader):
---> 58     data, target = data.to(device), target.to(device)
     59     data = data.view(-1, sequence_length, 1).float()
     60     optimizer.zero_grad()

AttributeError: 'list' object has no attribute 'to'

assistant:

このエラーは、DataLoaderで取得したデータがPyTorchのTensor型ではなく、Pythonのlist型になっていることが原因です。MyDatasetクラスの__getitem__メソッド内で、データをTensor型に変換することでエラーを解消できます。

以下に修正したコードを示します:

class MyDataset(Dataset):
    def __init__(self, dataX, dataY):
        self.dataX = dataX
        self.dataY = dataY

    def __len__(self):
        return len(self.dataX)

    def __getitem__(self, idx):
        return torch.tensor(self.dataX[idx]), torch.tensor(self.dataY[idx])

# ...

train_dataset = MyDataset(train_data, train_label)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

loss_list = []
for epoch in range(100):
    loss = train(epoch, model, train_loader, criterion, optimizer)
    print('Train Epoch: {} \t Loss: {:.6f}'.format(epoch+1, loss))
    loss_list.append(loss)

# 損失の描画
plot_loss(loss_list)

ここで、MyDatasetクラスの__getitem__メソッドで返ってくるデータがTensor型になっているため、'list' object has no attribute 'to'というエラーは解消されます。


user:

ありがとうございます。それでは、修正したコードをもとに、GPUが使える場合は使うように変更した全コードを書いてください。


assistant:

了解しました。GPUが使える場合はそれを利用するように、使えない場合はCPUを利用するように更新したコードは以下になります。

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
from torch.utils.data import Dataset, DataLoader
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt

with open("waka.txt", "r", encoding="utf-8") as f:
    text = f.read()
chars = sorted(list(set(text)))
char_to_int = dict((c, i) for i, c in enumerate(chars))
int_to_char = dict((i, c) for i, c in enumerate(chars))

sequence_length = 5
dataX = []
dataY = []
for i in range(0, len(text) - sequence_length, 1):
    seq_in = text[i:i + sequence_length]
    seq_out = text[i + sequence_length]
    dataX.append([char_to_int[char] for char in seq_in])
    dataY.append(char_to_int[seq_out])


class MyDataset(Dataset):
    def __init__(self, dataX, dataY):
        self.dataX = dataX
        self.dataY = dataY

    def __len__(self):
        return len(self.dataX)

    def __getitem__(self, idx):
        return torch.tensor(self.dataX[idx]), torch.tensor(self.dataY[idx])


class Net(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(Net, self).__init__()
        self.hidden_size = hidden_size
        self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
        self.out = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        x, _ = self.lstm(x)
        x = self.out(x[:, -1, :])
        return x


def train(epoch, model, dataloader, criterion, optimizer):
    model.train()
    running_loss = 0.0
    for batch_idx, (data, target) in enumerate(dataloader):
        data, target = data.to(device), target.to(device)
        data = data.view(-1, sequence_length, 1).float()
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
    return running_loss / len(dataloader)


def plot_loss(loss_list):
    plt.plot(list(range(len(loss_list))), loss_list, '-b', label='loss')
    plt.legend(loc=0)
    plt.show()


device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

model = Net(1, 128, len(chars)).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

train_data, val_data, train_label, val_label = train_test_split(dataX, dataY, test_size=0.2)

train_dataset = MyDataset(train_data, train_label)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

loss_list = []
for epoch in range(100):
    loss = train(epoch, model, train_loader, criterion, optimizer)
    print('Train Epoch: {} \t Loss: {:.6f}'.format(epoch+1, loss))
    loss_list.append(loss)

plot_loss(loss_list)

torch.device('cuda' if torch.cuda.is_available() else 'cpu') の部分が GPU が利用可能かどうかを判定し、
利用可能であれば 'cuda' を、利用可能でなければ 'cpu' を device という変数に格納しています。この device を後の処理で利用します。具体的には、model.to(device) でモデルをGPUに送ったり、 data.to(device) でデータをGPUに送ったりしています。これで、GPUが利用可能な場合はそれを使うコードになります。


user:

ありがとうございます。それでは、修正したコードをもとに、1エポックごとにスナップショットを保存するように変更した全コードを書いてください。


assistant:

それぞれの学習エポックのスナップショットを保存したい場合、PyTorchの torch.save 関数を利用することができます。エポックごとのモデルの重みをファイルに保存するため、学習が中断された場合でも途中から再開することが可能になります。

以下に修正したコードを示します:

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
from torch.utils.data import Dataset, DataLoader
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt

with open("waka.txt", "r", encoding="utf-8") as f:
    text = f.read()
chars = sorted(list(set(text)))
char_to_int = dict((c, i) for i, c in enumerate(chars))
int_to_char = dict((i, c) for i, c in enumerate(chars))

sequence_length = 5
dataX = []
dataY = []
for i in range(0, len(text) - sequence_length, 1):
    seq_in = text[i:i + sequence_length]
    seq_out = text[i + sequence_length]
    dataX.append([char_to_int[char] for char in seq_in])
    dataY.append(char_to_int[seq_out])


class MyDataset(Dataset):
    def __init__(self, dataX, dataY):
        self.dataX = dataX
        self.dataY = dataY

    def __len__(self):
        return len(self.dataX)

    def __getitem__(self, idx):
        return torch.tensor(self.dataX[idx]), torch.tensor(self.dataY[idx])


class Net(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(Net, self).__init__()
        self.hidden_size = hidden_size
        self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
        self.out = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        x, _ = self.lstm(x)
        x = self.out(x[:, -1, :])
        return x


def train(epoch, model, dataloader, criterion, optimizer):
    model.train()
    running_loss = 0.0
    for batch_idx, (data, target) in enumerate(dataloader):
        data, target = data.to(device), target.to(device)
        data = data.view(-1, sequence_length, 1).float()
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
    return running_loss / len(dataloader)


def plot_loss(loss_list):
    plt.plot(list(range(len(loss_list))), loss_list, '-b', label='loss')
    plt.legend(loc=0)
    plt.show()


device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

model = Net(1, 128, len(chars)).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

train_data, val_data, train_label, val_label = train_test_split(dataX, dataY, test_size=0.2)

train_dataset = MyDataset(train_data, train_label)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

loss_list = []
for epoch in range(100):
    loss = train(epoch, model, train_loader, criterion, optimizer)
    print('Train Epoch: {} \t Loss: {:.6f}'.format(epoch+1, loss))
    loss_list.append(loss)
    torch.save(model.state_dict(), f'model_epoch{epoch+1}.pth')

plot_loss(loss_list)

output_3_0.png

torch.save(model.state_dict(), f'model_epoch{epoch+1}.pth') とすることで、各エポックごとにモデルのスナップショットを保存しています。保存形式は ".pth" ファイルとしており、ファイル名は 'model_epoch' にエポック数を付け加えたものにしています。これにより、学習が途中で中断してもその時点から再開することが可能となります。


user:

上記のように進めた学習が中断したときに、最新のスナップショットから学習を再開するコードを書いてください。


assistant:

既存のスナップショットから学習を再開するためには、torch.load を使用してモデルの重みをロードします。

以下に修正したコードを示します:

# ...

# 中断されたエポックから再開するための値
resume_epoch = 32

model = Net(1, 128, len(chars)).to(device)
if resume_epoch > 0:
    model.load_state_dict(torch.load(f"model_epoch{resume_epoch}.pth"))
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

train_data, val_data, train_label, val_label = train_test_split(
    dataX, dataY, test_size=0.2
)

train_dataset = MyDataset(train_data, train_label)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

loss_list = []
for epoch in range(resume_epoch, 100):
    loss = train(epoch, model, train_loader, criterion, optimizer)
    print("Train Epoch: {} \t Loss: {:.6f}".format(epoch + 1, loss))
    loss_list.append(loss)
    torch.save(model.state_dict(), f"model_epoch{epoch+1}.pth")

plot_loss(loss_list)
Train Epoch: 33 	 Loss: 3.021376
Train Epoch: 34 	 Loss: 3.026941
Train Epoch: 35 	 Loss: 3.020184
Train Epoch: 36 	 Loss: 3.024213

output_5_0.png

上記のコードでは resume_epoch という変数が追加されています。この変数に再開したいエポック数を設定します。また model.load_state_dict(torch.load(f'model_epoch{resume_epoch}.pth')) といった形で指定したエポックの重みをロードしています。またエポックのループも再開したいエポックから開始するようになっています。


user:

上記のように学習させたLSTMモデルを用いて、和歌を10個生成するコードを書いてください。


assistant:

詩を生成するためのコードは以下のようになります:

def generate_poem(model, start_str, length=30):
    model.eval()
    input = torch.tensor([char_to_int[c] for c in start_str]).to(device)
    input = input.unsqueeze(0).view(-1, sequence_length, 1).float()
    predict_str = start_str

    for _ in range(length):
        output = model(input)
        _, top_index = output.topk(1)
        top_index = top_index[0].item()
        predict_str += int_to_char[top_index]
        int_seq = [char_to_int[char] for char in predict_str]
        input = torch.tensor(int_seq[-sequence_length:]).to(device)
        input = input.unsqueeze(0).view(-1, sequence_length, 1).float()

    return predict_str

# モデルのロード
model = Net(1, 128, len(chars)).to(device)
model.load_state_dict(torch.load('model_epoch100.pth')) # 最後のエポックの重みをロード

# 詩の生成
for i in range(10):
    start_str = text[np.random.randint(0, len(text)-sequence_length-1):][:sequence_length] # ランダムな開始文字列
    print(f'Poem {i+1}')
    print(generate_poem(model, start_str, length=30)) # ランダムな和歌30文字を生成する
    print('\n')
Poem 1
つはりのわ−なみたのかは−なかめれは−あきのよの−あきのよの−あきのよ


Poem 2
うみわたる−あきのよの−あきのよの−あきのよの−あきのよの−あきのよの


Poem 3
きたちにし−あきのよの−あきのよの−あきのよの−あきのよの−あきのよの


Poem 4
−にほひそめ−あきのよの−あきのよの−あきのよの−あきのよの−あきのよ


Poem 5

さみたれの−あきのよの−あきのよの−あきのよの−あきのよの−あきのよ


Poem 6
みつ−たのむしらむ−あきのよの−あきのよの−あきのよの−あきのよの−あ


Poem 7
はのそらの−あきのよの−あきのよの−あきのよの−あきのよの−あきのよの


Poem 8
く−たみゆかほ−なかめれは−あきのよの−あきのよの−あきのよの−あきの


Poem 9
つこころは−あきのよの−あきのよの−あきのよの−あきのよの−あきのよの


Poem 10
やまに−いはねのかはの−あきのよの−あきのよの−あきのよの−あきのよの

このコードではgenerate_poemという関数を定義し、その関数を用いて和歌を生成します。生成する和歌はランダムな初めの文字列から始め、その後の文字をモデルが予測する方式で生成します。この生成は10回繰り返し、それぞれの結果はprintで表示されます。


user:

同じ5文字(あきのよの)ばかり繰り返し生成してしまっていますが、どうにか直せませんか?


assistant:

モデルが同じ文字列を繰り返してしまう原因としては、学習が十分になされていない、または、モデルの設定(レイヤの数やノードの数など)が適切でない可能性が考えられます。

シーケンスを生成する際に多様性を持たせるためには、temperatureパラメータを利用して出力確率分布を操作することがあります。

Temperatureは1より小さい値をとると、モデルが一番確率の高い文字を選びやすくなります。逆にTemperatureが1より大きい値をとると、モデルは様々な文字を選ぶ可能性が高まり、出力の多様性が高まります。

また、予測時に各出力を確定的に扱わず、出力の確率分布からサンプリングを行うことでも多様性を高めることが可能です。

以下にこれらの考え方を取り入れた詩の生成方法を示します:

def generate_poem(model, start_str, length=30, temperature=1.0):
    model.eval()
    input = torch.tensor([char_to_int[c] for c in start_str]).to(device)
    input = input.unsqueeze(0).view(-1, sequence_length, 1).float()
    predict_str = start_str

    for _ in range(length):
        output = model(input)
        output_dist = output.data.view(-1).div(temperature).exp()
        top_index = torch.multinomial(output_dist, 1)[0]
        predict_str += int_to_char[top_index.item()]
        int_seq = [char_to_int[char] for char in predict_str]
        input = torch.tensor(int_seq[-sequence_length:]).to(device)
        input = input.unsqueeze(0).view(-1, sequence_length, 1).float()

    return predict_str

# モデルのロード
model = Net(1, 128, len(chars)).to(device)
model.load_state_dict(torch.load('model_epoch100.pth')) 

# 詩の生成
for i in range(10):
    start_str = text[np.random.randint(0, len(text)-sequence_length-1):][:sequence_length] 
    print(f'Poem {i+1}')
    print(generate_poem(model, start_str, length=30, temperature=0.8)) # temperature を追加
    print('\n')

ここで generate_poem では temperature をパラメータとして受け取り、確率分布からサンプリングする際にsoftmaxの出力に対してこの temperature を使用しています。また、torch.multinomial を利用し、出力の確率分布を基にサンプリングを行うようにしています。これにより、常に最も確率の高い文字を選ぶわけではなく、さまざまな文字を選び、出力の多様性を高めることができます。


user:

次のエラーを修正したコードを書いてください。

Poem 1
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[6], line 26
     24 start_str = text[np.random.randint(0, len(text)-sequence_length-1):][:sequence_length] 
     25 print(f'Poem {i+1}')
---> 26 print(generate_poem(model, start_str, length=30, temperature=0.8)) # temperature を追加
     27 print('\n')

Cell In[6], line 10, in generate_poem(model, start_str, length, temperature)
      8 output = model(input)
      9 output_dist = output.data.view(-1).div(temperature).exp()
---> 10 top_index = torch.multinomial(output_dist, 1)[0]
     11 predict_str += int_to_char[top_index.item()]
     12 int_seq = [char_to_int[char] for char in predict_str]

RuntimeError: invalid multinomial distribution (sum of probabilities <= 0)

assistant:

推論時に出現しやすさを制御するtemperatureパラメータが0または極端に小さい値になっているとこのようなエラーが起こり得ます。しかし、与えられたコードではtemperatureに0.8という値が渡されていますので、エラーの原因はそこにはないようです。

別の可能性としては、ネットワークの出力値が非常に大きい(または小さい)値になってしまい、出力値をexponential関数で処理して確率的な値に変換しようとしたときにアンダーフローやオーバーフローが発生してしまっていると考えられます。

そのため、この問題を解決するためには、expに変換する前に出力値をSoftmax関数に通すことで、出力値を確率的な範囲内に収めることが有効です。

以下に修正したコードを示します:

def generate_poem(model, start_str, length=30, temperature=1.0):
    model.eval()
    input = torch.tensor([char_to_int[c] for c in start_str]).to(device)
    input = input.unsqueeze(0).view(-1, sequence_length, 1).float()
    predict_str = start_str

    for _ in range(length):
        output = model(input)
        output = torch.nn.functional.softmax(output, dim=1)  # softmaxを追加
        output_dist = output.data.view(-1).div(temperature).exp()
        top_index = torch.multinomial(output_dist, 1)[0]
        predict_str += int_to_char[top_index.item()]
        int_seq = [char_to_int[char] for char in predict_str]
        input = torch.tensor(int_seq[-sequence_length:]).to(device)
        input = input.unsqueeze(0).view(-1, sequence_length, 1).float()

    return predict_str

# モデルのロード
model = Net(1, 128, len(chars)).to(device)
model.load_state_dict(torch.load('model_epoch100.pth')) 

# 詩の生成
for i in range(10):
    start_str = text[np.random.randint(0, len(text)-sequence_length-1):][:sequence_length] 
    print(f'Poem {i+1}')
    print(generate_poem(model, start_str, length=30, temperature=0.8)) 
    print('\n')
Poem 1
の−ころも金款岑頃尺左忘浦恋身恒桶通射厭亀立−堤佗付茅望終沢乱勅妙樋釣


Poem 2
きときの−通埋科見鳩翁物米悟稲品原雀帯き洗蔓潮振瑞本帆消梁衾苗文衾沈霜


Poem 3
り星出てに由銀貫室x類頻籬鐘瑩者詠降類汀阿峰染速淡誰柄釘興河溝席堀比底


Poem 4
かへりしほ刈礼干蔦田恒仃思浮真直悌荷幾等泊学独みせ桧峨氷曰袴楢姿沖照望


Poem 5
みちは−た宮曳剣毛得繁狭語者宮何陀稲屋短雨苫た海岑ぐ俤亀休ペ反雨幡猿髣


Poem 6
つみむ
わ貌筋柱尾潟孫涼角皆ら四匂辞杖母息駅ん川薗積沼片開指宝午髴帯栗


Poem 7
−とはぬひ荻誠憎機釣中葦如梯穂弓淺苗天煙桂言し師猿情犬蛛a齢詞高窟期国


Poem 8
−さけるさ極せ島堤祓ゑ淺類口紬菅藻槿思榎筆挿又調改織其歩物引使雁覚淺主


Poem 9
ゑたてて−え庭内物手八曇か鷺きせ布篭櫨橘掠乗晩何尋速春葎殿徒大薪棚筑負


Poem 10
に−ちよそ碁歹長涼道タ釜爰碑佐晩過榎あ鴨恋皇漕山祝林越馴杜剣彼八作庵壁

この修正により、各出力(モデルの予測)が [0, 1] の範囲に収まり、その総和が1になるようになりました。これにより torch.multinomial の引数が正しい確率分布を形成できるようになり、エラーが解消するはずです。


user:

たしかにバリエーションは出ましたが、ひらがなばかりのデータセットで、出現頻度が低いはずの漢字ばかり出力するようになってしまいました。


assistant:

そうですね。Tempertureを用いると、確率分布全体に対するサンプリングが行われるため、出現頻度が低い文字も出現し得ます。

この問題を解決するアプローチの一つは、出力確率の上位k個からのみサンプリングを行うというものです。これにより、出現頻度が低い文字が出現しすぎることを防ぎつつ、多様性は保たれます。

以下にそのように修正したコードを示します:

def generate_poem(model, start_str, length=30, top_k=3):
    model.eval()
    input = torch.tensor([char_to_int[c] for c in start_str]).to(device)
    input = input.unsqueeze(0).view(-1, sequence_length, 1).float()
    predict_str = start_str

    for _ in range(length):
        output = model(input)
        output = torch.nn.functional.softmax(output, dim=1)
        top_prob, top_index = output.topk(top_k)
        sampled_index = torch.multinomial(top_prob, num_samples=1)
        predict_str += int_to_char[top_index[0][sampled_index].item()]
        int_seq = [char_to_int[char] for char in predict_str]
        input = torch.tensor(int_seq[-sequence_length:]).to(device)
        input = input.unsqueeze(0).view(-1, sequence_length, 1).float()

    return predict_str

# モデルのロード
model = Net(1, 128, len(chars)).to(device)
model.load_state_dict(torch.load('model_epoch100.pth'))

# 詩の生成
for i in range(10):
    start_str = text[np.random.randint(0, len(text)-sequence_length-1):][:sequence_length] 
    print(f'Poem {i+1}')
    print(generate_poem(model, start_str, length=30, top_k=3)) # top_kパラメータを追加
    print('\n')
Poem 1
よとか−いろにみは−あきはしたみに−なきものから−こころのうへの−こと


Poem 2
しや我たにしのかりに−かすみのからの−あらしをかはの−あはれとる−あら


Poem 3
はかなしも−あらしのは−かすむはかりの−ことのはに−あきのよは−あきは


Poem 4
うらの−しくれのな−あらしのかせ
あさかりの−こひはかは−なみのかすの


Poem 5
うくひすのかは
いかなせむ−あきはきの−やきみるかな−あらしにも−なほ


Poem 6
ひたちぬる−かすのしらにみ−あらしのみ−あきのきれ−あさかほは−あきの


Poem 7
すきむ−ことのかは−かけとから−なみたのそら
いまひかか−こひしくこ−


Poem 8
ほらぬ浪にかりのから−あらはしき−たつたはの−ことのかは−なほたなり



Poem 9
−つらさにもと−なほたにる−こころのうちの−あきのよの−ありあけのつき


Poem 10
ひに−みたれなる
あふさとは−あらしをかせ
いまそとは−なかめれは−あ

ここで generate_poem の定義に top_k パラメータを新たに追加しました。このパラメータは予測時に最も確率の高い上位k個の文字からサンプリングを行うためのものです。また、topk 関数を使用して確率の上位k個の文字を選択しています。


user:

ありがとうございます。だいぶ良くなりました。あとは、和歌なので「5文字-7文字-5文字」という構成になってくれると嬉しいですが、さすがにそれは難しいですかね?


assistant:

見た目を整えるために、「5-7-5」の形式に和歌を整形することは可能です。ただし、ここでは「5-7-5」の形式に合わせて文字列を出力するだけで、和歌としての意味の一貫性や韻律などは考慮されません。

以下にそのように修正したコードを示します:

def generate_poem(model, start_str, top_k=3):
    model.eval()
    input = torch.tensor([char_to_int[c] for c in start_str]).to(device)
    input = input.unsqueeze(0).view(-1, sequence_length, 1).float()
    predict_str = ''

    poem_format = [5, 7, 5] # 和歌の形式
    for length in poem_format:
        for _ in range(length):
            output = model(input)
            output = torch.nn.functional.softmax(output, dim=1)
            top_prob, top_index = output.topk(top_k)
            sampled_index = torch.multinomial(top_prob, num_samples=1)
            predict_str += int_to_char[top_index[0][sampled_index].item()]
            int_seq = [char_to_int[char] for char in predict_str]
            input = torch.tensor(int_seq[-sequence_length:]).to(device)
            input = input.unsqueeze(0).view(-1, sequence_length, 1).float()
        predict_str += '\n' # 行を換える

    return predict_str

# モデルのロード
model = Net(1, 128, len(chars)).to(device)
model.load_state_dict(torch.load('model_epoch100.pth')) 

# 詩の生成
for i in range(10):
    start_str = text[np.random.randint(0, len(text)-sequence_length-1):][:sequence_length] 
    print(f'Poem {i+1}')
    print(generate_poem(model, start_str, top_k=3)) 
    print('\n')

ここで、generate_poem関数の中にpoem_format = [5, 7, 5]という新たな行を追加し、それに従って「5-7-5」という形式で和歌が生成されるようにしました。さらに、それぞれの部分が改行されるようにpredict_str += '\n'を追加しています。


user:

次のエラーを修正したコードを書いてください。

Poem 1
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[12], line 30
     28 start_str = text[np.random.randint(0, len(text)-sequence_length-1):][:sequence_length] 
     29 print(f'Poem {i+1}')
---> 30 print(generate_poem(model, start_str, top_k=3)) 
     31 print('\n')

Cell In[12], line 17, in generate_poem(model, start_str, top_k)
     15         int_seq = [char_to_int[char] for char in predict_str]
     16         input = torch.tensor(int_seq[-sequence_length:]).to(device)
---> 17         input = input.unsqueeze(0).view(-1, sequence_length, 1).float()
     18     predict_str += '\n' # 行を換える
     20 return predict_str

RuntimeError: shape '[-1, 5, 1]' is invalid for input of size 1

assistant:

このエラーは1行の詩を生成するたびに predict_str が長くなり、次のステップの入力として使用しようとすると、その長さが sequence_length を超えてしまうため発生しています。その結果、詩を生成する途中で指定された形状に合わせることができずエラーが生じています。

この問題を解決するためには、predict_str の最新の sequence_length 文字のみを入力として使用します。そして、predict_str の長さが sequence_length を超えることはありません。

以下にコードの修正箇所を示します:

def generate_poem(model, start_str, top_k=3):
    model.eval()
    predict_str = start_str
    poem_format = [5, 7, 5] 
    for length in poem_format:
        for _ in range(length):
            input = torch.tensor([char_to_int[c] for c in predict_str]).to(device)
            input = input.unsqueeze(0).view(-1, sequence_length, 1).float()
            output = model(input)
            output = torch.nn.functional.softmax(output, dim=1)
            top_prob, top_index = output.topk(top_k)
            sampled_index = torch.multinomial(top_prob, num_samples=1)
            predict_str += int_to_char[top_index[0][sampled_index].item()]
            int_seq = [char_to_int[char] for char in predict_str]
            predict_str = predict_str[-sequence_length:]  # 最新のsequence_length文字のみ保持
        predict_str += '\n'

    return predict_str

# モデルのロード
model = Net(1, 128, len(chars)).to(device)
model.load_state_dict(torch.load('model_epoch100.pth')) 

# 詩の生成
for i in range(10):
    start_str = text[np.random.randint(0, len(text)-sequence_length-1):][:sequence_length]
    print(f'Poem {i+1}')
    print(generate_poem(model, start_str, top_k=3))
    print('\n')

これにより、各ステップで入力として使用する文字列の長さは sequence_length を超えることはなくなり、上述のエラーは解消されるはずです。


user:

次のエラーを修正したコードを書いてください。

Poem 1
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[14], line 28
     26 start_str = text[np.random.randint(0, len(text)-sequence_length-1):][:sequence_length]
     27 print(f'Poem {i+1}')
---> 28 print(generate_poem(model, start_str, top_k=3))
     29 print('\n')

Cell In[14], line 8, in generate_poem(model, start_str, top_k)
      6 for _ in range(length):
      7     input = torch.tensor([char_to_int[c] for c in predict_str]).to(device)
----> 8     input = input.unsqueeze(0).view(-1, sequence_length, 1).float()
      9     output = model(input)
     10     output = torch.nn.functional.softmax(output, dim=1)

RuntimeError: shape '[-1, 5, 1]' is invalid for input of size 6

assistant:

このエラーは、input 数列の形状を (-1, sequence_length, 1) にリシェイプしようとした際に発生しています。エラーメッセージから推測するに、sequence_length が5であり、リシェイプする前の input のサイズが6であることがわかります。これは input のサイズが sequence_length と一致していないため、リシェイプが失敗しエラーが発生しています。

このエラーを修正するためには、input のサイズを sequence_length と一致するようにする必要があります。特定の sequence_length 文字の順番に予測するために,input を作成する際に最初の sequence_length 文字を使用して predict_str を初期化します。

以下のコードではエラーを修正しています。

def generate_poem(model, start_str, top_k=3):
    model.eval()
    predict_str = start_str[:sequence_length]  # predict_strをsequence_length文字で初期化
    poem_format = [5, 7, 5] 
    for length in poem_format:
        for _ in range(length):
            input = torch.tensor([char_to_int[c] for c in predict_str[-sequence_length:]]).to(device)
            input = input.unsqueeze(0).view(-1, sequence_length, 1).float()
            output = model(input)
            output = torch.nn.functional.softmax(output, dim=1)
            top_prob, top_index = output.topk(top_k)
            sampled_index = torch.multinomial(top_prob, num_samples=1)
            predict_str += int_to_char[top_index[0][sampled_index].item()]
        predict_str += '\n'

    return predict_str

# モデルのロード
model = Net(1, 128, len(chars)).to(device)
model.load_state_dict(torch.load('model_epoch100.pth')) 

# 詩の生成
for i in range(10):
    start_str = text[np.random.randint(0, len(text)-sequence_length-1):][:sequence_length]
    print(f'Poem {i+1}')
    print(generate_poem(model, start_str, top_k=3))
    print('\n')
Poem 1
つきやいては−かけろ
むつしきる−あ
わらかれし



Poem 2
もけふも−なほのうち
あきのきせらに
いまはかけ



Poem 3
てと−きこゆりし−あ
いて天のかきら
いかかしむ



Poem 4
世の中をなのかり−こ
彦はかのから−
おしたかと



Poem 5
れや−いはねとかは−
いつたきは−か
ふかてけて



Poem 6
をはきかは−かせのみ
かせみたの−あ
いむちいつ



Poem 7
きする−なかれへる−
たかのけと−つ
えたえつや



Poem 8
ははやおなと−かけろ
よそにから−あ
えらはとの



Poem 9
にしへは−あきのよに
あきかぬと−あ
えらるむく



Poem 10
まの−こころのから−
たまもくる−あ
ふхるての

ここで,inputを生成する際にはpredict_strの最新のsequence_length文字を利用するように変更しています。これにより,入力のサイズが常にsequence_lengthと一致します。


user:

あー、なるほど、やっぱりそうなりますか。句と句をつなぐ "-" がデータセットに入ったまま学習させたので、それをデータセットから取り除いて再学習させた方が良さそうですね。


assistant:

はい、その通りです。

モデルがデータセットに含まれるすべての文字を学習するため、データセット内で頻繁に出現する特殊な文字や記号 (- など)は、生成される詩にも影響を与えます。

したがって、もしモデルに特定の文字(この場合は -)を生成させたくないのであれば、その文字をデータセットから事前に除去してからモデルを訓練させると良いでしょう。

以下のようにデータセットから - を削除してからモデルの訓練を始めることができます。

# データローディングと前処理
text = text.replace('-', '')
text = text.translate(str.maketrans({chr(0x3099): None, chr(0x309A): None}))
chars = sorted(list(set(text)))
char_to_int = dict((c, i) for i, c in enumerate(chars))
int_to_char = dict((i, c) for i, c in enumerate(chars))

これにより、モデルは - を生成することなく詩を生成することが可能となります。詩の生成関数も最初のsequence_lengthの数の文字列で初期化するように変更するとより良い結果が得られるでしょう。


import matplotlib.pyplot as plt
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.model_selection import train_test_split
from torch.utils.data import DataLoader, Dataset

with open("waka.txt", "r", encoding="utf-8") as f:
    text = f.read()


# データローディングと前処理
text = text.replace("-", "")
text = text.replace(" ", "")
text = text.translate(str.maketrans({chr(0x3099): None, chr(0x309A): None}))
chars = sorted(list(set(text)))
char_to_int = dict((c, i) for i, c in enumerate(chars))
int_to_char = dict((i, c) for i, c in enumerate(chars))

sequence_length = 5
dataX = []
dataY = []
for i in range(0, len(text) - sequence_length, 1):
    seq_in = text[i : i + sequence_length]
    seq_out = text[i + sequence_length]
    dataX.append([char_to_int[char] for char in seq_in])
    dataY.append(char_to_int[seq_out])


class MyDataset(Dataset):
    def __init__(self, dataX, dataY):
        self.dataX = dataX
        self.dataY = dataY

    def __len__(self):
        return len(self.dataX)

    def __getitem__(self, idx):
        return torch.tensor(self.dataX[idx]), torch.tensor(self.dataY[idx])


class Net(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(Net, self).__init__()
        self.hidden_size = hidden_size
        self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
        self.out = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        x, _ = self.lstm(x)
        x = self.out(x[:, -1, :])
        return x


def train(epoch, model, dataloader, criterion, optimizer):
    model.train()
    running_loss = 0.0
    for batch_idx, (data, target) in enumerate(dataloader):
        data, target = data.to(device), target.to(device)
        data = data.view(-1, sequence_length, 1).float()
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
    return running_loss / len(dataloader)


def plot_loss(loss_list):
    plt.plot(list(range(len(loss_list))), loss_list, "-b", label="loss")
    plt.legend(loc=0)
    plt.show()


device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

model = Net(1, 128, len(chars)).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

train_data, val_data, train_label, val_label = train_test_split(
    dataX, dataY, test_size=0.2
)

train_dataset = MyDataset(train_data, train_label)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

loss_list = []
for epoch in range(100):
    loss = train(epoch, model, train_loader, criterion, optimizer)
    print("Train Epoch: {} \t Loss: {:.6f}".format(epoch + 1, loss))
    loss_list.append(loss)
    torch.save(model.state_dict(), f"model_epoch{epoch+1}.pth")

plot_loss(loss_list)
Train Epoch: 1 	 Loss: 3.262770
plot_loss(loss_list)

output_14_0.png

# ...

# 中断されたエポックから再開するための値
resume_epoch = 4

model = Net(1, 128, len(chars)).to(device)
if resume_epoch > 0:
    model.load_state_dict(torch.load(f"model_epoch{resume_epoch}.pth"))
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

train_data, val_data, train_label, val_label = train_test_split(
    dataX, dataY, test_size=0.2
)

train_dataset = MyDataset(train_data, train_label)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

for epoch in range(resume_epoch, 100):
    loss = train(epoch, model, train_loader, criterion, optimizer)
    print("Train Epoch: {} \t Loss: {:.6f}".format(epoch + 1, loss))
    loss_list.append(loss)
    torch.save(model.state_dict(), f"model_epoch{epoch+1}.pth")

plot_loss(loss_list)

output_16_0.png

def generate_poem(model, start_str, length=30, top_k=3):
    model.eval()
    input = torch.tensor([char_to_int[c] for c in start_str]).to(device)
    input = input.unsqueeze(0).view(-1, sequence_length, 1).float()
    predict_str = start_str

    for _ in range(length):
        output = model(input)
        output = torch.nn.functional.softmax(output, dim=1)
        top_prob, top_index = output.topk(top_k)
        sampled_index = torch.multinomial(top_prob, num_samples=1)
        predict_str += int_to_char[top_index[0][sampled_index].item()]
        int_seq = [char_to_int[char] for char in predict_str]
        input = torch.tensor(int_seq[-sequence_length:]).to(device)
        input = input.unsqueeze(0).view(-1, sequence_length, 1).float()

    return predict_str


# モデルのロード
model = Net(1, 128, len(chars)).to(device)
model.load_state_dict(torch.load("model_epoch28.pth"))

# 詩の生成
for i in range(10):
    start_str = text[np.random.randint(0, len(text) - sequence_length - 1) :][
        :sequence_length
    ]
    print(f"Poem {i+1}")
    print(generate_poem(model, start_str, length=30, top_k=3))  # top_kパラメータを追加
    print("\n")
Poem 1
みの−なかききに−あきのゆの−かけははかりに−かせめたつる−あきのゆふ


Poem 2
とりもなき−かけはのやまに−かけてとりらむ
いつかたに−あふみのかすみ


Poem 3
ひとをまつ−いつとなき−かけのかりかは−かけはのころ−あきのよは−こと


Poem 4
きつらむ
いまこさへ−あさかののみ−かすはしたにる−はるのみの−つきの


Poem 5
ちさまたけとは−かけははこれ−はるのみしきに−あふさかの−こゑもとも−


Poem 6
しののたきにの−ことのはの−あきかせに−かせめとる−かみなつき−あまの


Poem 7
のへにかすて−かすみのはかりの−こころのもまに−あふのかく−なほたまか


Poem 8
へきたより
あまのはら−かみのこそゑ−おもひしらむ−かすみのは
あさか


Poem 9
−ととまらま−あきかせの−あさかほの−かたらひし−あきのつの−つきのか


Poem 10
とたえぬ−こころのも−いはちのかせの−かすみにりけれ
あきののの−かけ

user:

ロスの履歴はこんな感じでした。

Train Epoch: 5 	 Loss: 3.047453
Train Epoch: 6 	 Loss: 3.034867
Train Epoch: 7 	 Loss: 3.027176
Train Epoch: 8 	 Loss: 3.020200
Train Epoch: 9 	 Loss: 3.014454
Train Epoch: 10 	 Loss: 3.010406
Train Epoch: 11 	 Loss: 3.006892
Train Epoch: 12 	 Loss: 3.004679
Train Epoch: 13 	 Loss: 3.004441
Train Epoch: 14 	 Loss: 3.001050
Train Epoch: 15 	 Loss: 2.998358
Train Epoch: 16 	 Loss: 2.996320
Train Epoch: 17 	 Loss: 2.994868
Train Epoch: 18 	 Loss: 2.996118
Train Epoch: 19 	 Loss: 2.992241
Train Epoch: 20 	 Loss: 2.991326
Train Epoch: 21 	 Loss: 2.991220
Train Epoch: 22 	 Loss: 2.990349
Train Epoch: 23 	 Loss: 2.990480
Train Epoch: 24 	 Loss: 2.992147
Train Epoch: 25 	 Loss: 2.990153
Train Epoch: 26 	 Loss: 2.994044
Train Epoch: 27 	 Loss: 2.990754
Train Epoch: 28 	 Loss: 2.989437
Train Epoch: 29 	 Loss: 2.989545
Train Epoch: 30 	 Loss: 2.990357
Train Epoch: 31 	 Loss: 2.991224
Train Epoch: 32 	 Loss: 2.990124
Train Epoch: 33 	 Loss: 2.997097
Train Epoch: 34 	 Loss: 2.998021
Train Epoch: 35 	 Loss: 3.000053
Train Epoch: 36 	 Loss: 2.991467
Train Epoch: 37 	 Loss: 2.993590
Train Epoch: 38 	 Loss: 3.021503
Train Epoch: 39 	 Loss: 3.001553
Train Epoch: 40 	 Loss: 3.027776
Train Epoch: 41 	 Loss: 3.004113
Train Epoch: 42 	 Loss: 3.002589
Train Epoch: 44 	 Loss: 3.003464
Train Epoch: 45 	 Loss: 3.002224
Train Epoch: 46 	 Loss: 3.002951
Train Epoch: 47 	 Loss: 3.003215
Train Epoch: 48 	 Loss: 3.008458
Train Epoch: 49 	 Loss: 3.004695
Train Epoch: 50 	 Loss: 3.008111
Train Epoch: 51 	 Loss: 3.015421
Train Epoch: 52 	 Loss: 3.041470
Train Epoch: 53 	 Loss: 3.036416
Train Epoch: 54 	 Loss: 3.010204

assistant:

ロスの履歴を見ると、一定の場所で停滞してしまっています。モデルが十分な学習ができていない可能性があるため、パラメータの調整やさまざまな技巧を試すことを検討してみてください。例えば以下のようなアプローチが考えられます。

  1. 学習率を調整する: 学習率が大きすぎると収束しない可能性がありますが、小さすぎるとローカルミニマムに引っかかってしまう可能性があります。適切な学習率を見つけることでパフォーマンスを向上させることができます。

  2. 損失関数を変更する: 用いている損失関数がタスクに最適でない場合、別の損失関数を試してみることも一つの手段です。

  3. モデルアーキテクチャを変更する: 現状のモデル設計が複雑すぎたりシンプルすぎたりしているかもしれません。畳み込み層の数、全結合層の数、ノードの数などを調整してみてください。

  4. 正則化を試す: 過学習が問題であれば、L1、L2正則化(重み減衰)、ドロップアウト、早期停止などのテクニックを導入できます。

  5. 最適化アルゴリズムを変更する: 現在の最適化手法が収束しなかったり、収束が遅すぎる場合、別の最適化手法(Adam, RMSPropなど)を試してみる価値があります。


user:

ご提案ありがとうございます。今のところモデルアーキテクチャは以下のような構成ですが、もし変更するならどう変更したら良さそうか提案していただけますか?

class Net(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(Net, self).__init__()
        self.hidden_size = hidden_size
        self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
        self.out = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        x, _ = self.lstm(x)
        x = self.out(x[:, -1, :])
        return x

assistant:

ご提供いただいた現行のモデルアーキテクチャは既に良く設計されていますが、以下にいくつかの変更の提案をします:

  1. 深層化: LSTMの層の数を増やすことで,モデルの表現力を高めることができます。ただし、層を増やすと訓練は難しくなり、過学習が起こりやすくなるかもしれません。nn.LSTMの引数num_layersを用いて層の数を指定できます。

  2. Dropoutの追加: LSTMの間や全結合層の前にDropout層を追加することで,過学習を防ぎ,モデルの汎化性能を向上させることができます。nn.Dropoutを使用し、Dropoutの割合(例えば0.5)を引数として指定します。

  3. バッチ正規化: LSTMの出力や全結合層の出力にバッチ正規化を適用することで,訓練を安定させ、学習速度を向上させることができます。nn.BatchNorm1dを使用します。

コードに適用すると以下のようになります。

class Net(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(Net, self).__init__()
        self.hidden_size = hidden_size
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers=2, batch_first=True, dropout=0.5)
        self.dropout = nn.Dropout(0.5)
        self.bn = nn.BatchNorm1d(hidden_size)
        self.out = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        x, _ = self.lstm(x)
        x = self.dropout(x)
        x = self.bn(x[:, -1, :])
        x = self.out(x)
        return x

上述の提案は一例です。モデルのアーキテクチャを選択する際には、特定の問題の具体的なニーズやリソース、およびモデルの性能を考慮する必要があります。ですから、これらの変更を一度に全て試すのではなく、一つずつ変更して、モデルの性能を確認することをお勧めします。


import matplotlib.pyplot as plt
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.model_selection import train_test_split
from torch.utils.data import DataLoader, Dataset

with open("waka.txt", "r", encoding="utf-8") as f:
    text = f.read()


# データローディングと前処理
text = text.replace("-", "")
text = text.replace(" ", "")
text = text.translate(str.maketrans({chr(0x3099): None, chr(0x309A): None}))
chars = sorted(list(set(text)))
char_to_int = dict((c, i) for i, c in enumerate(chars))
int_to_char = dict((i, c) for i, c in enumerate(chars))

sequence_length = 5
dataX = []
dataY = []
for i in range(0, len(text) - sequence_length, 1):
    seq_in = text[i : i + sequence_length]
    seq_out = text[i + sequence_length]
    dataX.append([char_to_int[char] for char in seq_in])
    dataY.append(char_to_int[seq_out])


class MyDataset(Dataset):
    def __init__(self, dataX, dataY):
        self.dataX = dataX
        self.dataY = dataY

    def __len__(self):
        return len(self.dataX)

    def __getitem__(self, idx):
        return torch.tensor(self.dataX[idx]), torch.tensor(self.dataY[idx])


def train(epoch, model, dataloader, criterion, optimizer):
    model.train()
    running_loss = 0.0
    for batch_idx, (data, target) in enumerate(dataloader):
        data, target = data.to(device), target.to(device)
        data = data.view(-1, sequence_length, 1).float()
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
        running_loss += loss.item()
    return running_loss / len(dataloader)


def plot_loss(loss_list):
    plt.plot(list(range(len(loss_list))), loss_list, "-b", label="loss")
    plt.legend(loc=0)
    plt.show()


device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

model = Net(1, 128, len(chars)).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

train_data, val_data, train_label, val_label = train_test_split(
    dataX, dataY, test_size=0.2
)

train_dataset = MyDataset(train_data, train_label)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

loss_list = []
for epoch in range(100):
    loss = train(epoch, model, train_loader, criterion, optimizer)
    print("Train Epoch: {} \t Loss: {:.6f}".format(epoch + 1, loss))
    loss_list.append(loss)
    torch.save(model.state_dict(), f"model_epoch{epoch+1}.pth")

plot_loss(loss_list)
Train Epoch: 1 	 Loss: 3.356973
Train Epoch: 2 	 Loss: 3.276574
Train Epoch: 3 	 Loss: 3.251987
Train Epoch: 4 	 Loss: 3.236578
Train Epoch: 5 	 Loss: 3.224641
Train Epoch: 6 	 Loss: 3.214640

output_20_0.png

# ...

# 中断されたエポックから再開するための値
resume_epoch = 18

model = Net(1, 128, len(chars)).to(device)
if resume_epoch > 0:
    model.load_state_dict(torch.load(f"model_epoch{resume_epoch}.pth"))
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

train_data, val_data, train_label, val_label = train_test_split(
    dataX, dataY, test_size=0.2
)

train_dataset = MyDataset(train_data, train_label)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

for epoch in range(resume_epoch, 100):
    loss = train(epoch, model, train_loader, criterion, optimizer)
    print("Train Epoch: {} \t Loss: {:.6f}".format(epoch + 1, loss))
    loss_list.append(loss)
    torch.save(model.state_dict(), f"model_epoch{epoch+1}.pth")

plot_loss(loss_list)
Train Epoch: 19 	 Loss: 3.168676
Train Epoch: 20 	 Loss: 3.165564
plot_loss(loss_list)

output_22_0.png

# ...

# 中断されたエポックから再開するための値
resume_epoch = 26

model = Net(1, 128, len(chars)).to(device)
if resume_epoch > 0:
    model.load_state_dict(torch.load(f"model_epoch{resume_epoch}.pth"))
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

train_data, val_data, train_label, val_label = train_test_split(
    dataX, dataY, test_size=0.2
)

train_dataset = MyDataset(train_data, train_label)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

for epoch in range(resume_epoch, 100):
    loss = train(epoch, model, train_loader, criterion, optimizer)
    print("Train Epoch: {} \t Loss: {:.6f}".format(epoch + 1, loss))
    loss_list.append(loss)
    torch.save(model.state_dict(), f"model_epoch{epoch+1}.pth")

plot_loss(loss_list)

output_24_0.png

def generate_poem(model, start_str, length=35, top_k=3):
    model.eval()
    input = torch.tensor([char_to_int[c] for c in start_str]).to(device)
    input = input.unsqueeze(0).view(-1, sequence_length, 1).float()
    predict_str = start_str

    for _ in range(length):
        output = model(input)
        output = torch.nn.functional.softmax(output, dim=1)
        top_prob, top_index = output.topk(top_k)
        sampled_index = torch.multinomial(top_prob, num_samples=1)
        predict_str += int_to_char[top_index[0][sampled_index].item()]
        int_seq = [char_to_int[char] for char in predict_str]
        input = torch.tensor(int_seq[-sequence_length:]).to(device)
        input = input.unsqueeze(0).view(-1, sequence_length, 1).float()

    return predict_str


# モデルのロード
model = Net(1, 128, len(chars)).to(device)
model.load_state_dict(torch.load("model_epoch50.pth"))

# 詩の生成
for i in range(10):
    start_str = text[np.random.randint(0, len(text) - sequence_length - 1) :][
        :sequence_length
    ]
    print(f"Poem {i+1}")
    print(generate_poem(model, start_str, length=30, top_k=3))  # top_kパラメータを追加
    print("\n")
Poem 1
ねられす
こころにも−あまのときの−みつのことのはな
あきのよのつき



Poem 2
るさの−つらのかけに−こひしかるけり
いつしかも−あきかせの−なかめの


Poem 3
まつかえに−なほたのは−いつるのかせに−こころをかの−あまのとも−いつ


Poem 4
ぬれは−なほひとりけり
いつかかのなみに−なほのうちの−みつのこころも


Poem 5
ひのひまよに
いろかせの−なかきこそ−なみのうへの−こゑもうて−いろに


Poem 6
ら≥なほきにも−あらあけの−こゑもかたの−かみのこころも−あきのよの−


Poem 7
かへせ−よきふるに−あらしにか−おとしきにけり
あまのはの−あきのよに


Poem 8
なれをそたて−ことははかりの−みつのこころも−あらしときの−みととしす


Poem 9
−よそにおもへとも−あきかせに−なほのことのしきのは−あきのよの−あき


Poem 10
になるみの−みつのしたの−こころのうへに−なほのおもは−なほひのやまの

user:

ロスはこんな感じです。どう思いますか?

Train Epoch: 27 	 Loss: 3.158212
Train Epoch: 28 	 Loss: 3.156639
Train Epoch: 29 	 Loss: 3.155497
Train Epoch: 30 	 Loss: 3.155636
Train Epoch: 31 	 Loss: 3.153798
Train Epoch: 32 	 Loss: 3.153307
Train Epoch: 33 	 Loss: 3.152566
Train Epoch: 34 	 Loss: 3.152786
Train Epoch: 35 	 Loss: 3.151855
Train Epoch: 36 	 Loss: 3.150640
Train Epoch: 37 	 Loss: 3.151107
Train Epoch: 38 	 Loss: 3.151620
Train Epoch: 39 	 Loss: 3.150698
Train Epoch: 40 	 Loss: 3.150682
Train Epoch: 41 	 Loss: 3.149988
Train Epoch: 42 	 Loss: 3.149883
Train Epoch: 43 	 Loss: 3.149316
Train Epoch: 44 	 Loss: 3.149541
Train Epoch: 45 	 Loss: 3.149687
Train Epoch: 46 	 Loss: 3.150298
Train Epoch: 47 	 Loss: 3.150516
Train Epoch: 48 	 Loss: 3.149681
Train Epoch: 49 	 Loss: 3.149310
Train Epoch: 50 	 Loss: 3.148600
Train Epoch: 51 	 Loss: 3.148624
Train Epoch: 52 	 Loss: 3.148844
Train Epoch: 53 	 Loss: 3.148993

assistant:

ロスの履歴を見ると、訓練が進むにつれてロスが徐々に減少していることがわかります。これはモデルが訓練データから学習し、その結果として訓練データに対する損失が小さくなっていることを��しています。

しかし、ロスがある一定の値(この場合、約3.15近辺)で停滞しているように見えます。これはモデルが特定のパターンを学習できていない、あるいは学習データに対して過学習してしまっていることを示す可能性があります。

この問題を解決するためにはいくつかの方法があります:

  1. 学習率を調整する: 学習率が高すぎると、最適な値を見逃してしまう可能性があります。また、学習率が低すぎると、十分な学習が行われない可能性があります。

  2. モデルのアーキテクチャを変更する: もしネットワークが十分に深くない、あるいは逆に深すぎると、質の高い学習が行われない可能性があります。

  3. 正則化を導入する: L1, L2正則化、またはドロップアウトのような手法を導入することで、モデルが学習データに対して適度に過学習しないようにすることができます。

  4. 学習データを増やす: 学習データが不十分な場合、ロスが一定の値で停滞することがあります。学習データを増やすことで、モデルがより一般的なパターンを学習することが可能となります。

そして最後に、評価はトレーニングデータのパフォーマンスだけでなく、検証データやテストデータに対するパフォーマンスに基づくべきです.。これにより、モデルが未知のデータに対してどの程度汎化できるかを確認します。これが良い性能評価指標となります。


user:

ありがとうございました。今日は疲れたので、ここまでにしたいと思います。また次の機会によろしくおねがいします!


assistant:

了解しました。どんな質問でもいつでもお気軽にどうぞ。これからのプロジェクトがうまく進むことを願っています。あなたの成功を祈っています!また次の機会にお会いしましょう!


2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?