前提
現在、pytorchを用いてマリオのコースをクリアするDQNのソースコードを作成し、2万回ほど学習させた時点で約3割クリアするようになっています。ここから更にクリア率を上げていきたいのですが、中断したモデルから再開しようとしても初めからになってしまっている状態です。
ですので、
- モデルを保存する際に、保存しなければならないもの
- 学習済みモデルのロードの方法
について教えていただきたいです。
なお、100epoch(episode)ごとにモデルを保存しています。
追記
モデル全体を保存するように変更したところ、学習済みモデルから再開することに成功しました。
しかし、この方法は一般的には非推奨となっており、またなぜこれで成功したのか分かっていない状態です。
ですので、成功した要因等についても教えていただきたいです。
以下のソースコードに該当部分を追記しておきます。
環境
- windows10
- python 3.10.5
- pytorch 1.12.1
実現したいこと
pytorchにおいて、中断したモデルから再度学習できるようにしたい。
セーブ・ロードをしている部分のソースコード
def save_model(self,epoch): torch.save({ 'epoch': epoch, 'model_state_dict': self.model.state_dict(), 'optimizer_state_dict': self.optimizer.state_dict(), 'loss': self.loss}, "my_model_training_state.pt")
if os.path.exists("my_model_training_state.pt"): print("Succeesfully load") PATH = "my_model_training_state.pt" checkpoint = torch.load(PATH) self.model.load_state_dict(checkpoint['model_state_dict']) self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) self.loss = checkpoint['loss'] self.epoch = checkpoint['epoch']
追記
保存 torch.save(model, 'save/to/path/model.pt') 読み込み model = torch.load('load/from/path/model.pt')
補足情報(FW/ツールのバージョンなど)
以下に該当ソースコードを載せておきます。
import sys import os from collections import namedtuple import numpy as np #--------ニューラルネットワークの実装-------- import random import copy from copy import deepcopy import torch from torch import nn from torch import optim import torch.nn.functional as F #------学習に使うハイパーパラメータ--------- GAMMA = 0.9 # 時間割引率ガンマ Transition = namedtuple('Transicion', ('state', 'action', 'next_state', 'reward')) BATCH_SIZE = 16 # バッチサイズ CAPACITY = 1000000 # Replay Memoryに保存するデータの最大量 EPSILON_LAST = 0.00025 #εの最低値 class ReplayMemory: def __init__(self, CAPACITY): self.capacity = CAPACITY #メモリの最大値 self.memory = [] # 経験を保存するリスト self.index = 0 # 保存するindexを表す変数 def push(self, state, action, state_next, reward): '''trasicion = (state, action, state_next, reward)をReplayMemoryに保存する''' if len(self.memory) < self.capacity: self.memory.append(None) # メモリが満タンじゃないときは足す # 各引数をnamuedtupleでまとめ、memoryに保存する self.memory[self.index] = Transition(state, action, state_next, reward) self.index = (self.index + 1) % self.capacity # 保存するindexを1つずらす def sample(self, batch_size): '''batch_sizeだけ、ランダムに取り出す''' return random.sample(self.memory, batch_size) def __len__(self): '''関数lenに対して、現在のmemoryの長さを返す''' return len(self.memory) #----------------------------------------------------------------------------------------- class Net(nn.Module): def __init__(self, num_states, num_actions): super(Net, self).__init__() self.fc1 = nn.Linear(num_states, 32) self.fc2 = nn.Linear(32, 32) #中間層 self.fc3 = nn.Linear(32, num_actions) def forward(self, x): h1 = F.relu(self.fc1(x)) # 活性化関数にはReLu h2 = F.relu(self.fc2(h1)) output = self.fc3(h2) return output #------------------------------------------------------------------------------------ #----行動の選択、ネットワークの更新------------------ class Brain: def __init__(self, num_states, num_actions): self.num_actions = num_actions # 行動の数を取得 self.action = np.zeros(5,int) self.action_list = [ [0, 0, 0, 0, 0],#停止 [1, 0, 0, 0, 0],#左に歩く [0, 0, 1, 0, 0],#しゃがみ [0, 1, 0, 0, 0],#右に歩く [0, 0, 0, 1, 0],#その場ジャンプ [0, 1, 0, 0, 1],#右ダッシュ [0, 1, 0, 1, 0],#右ジャンプ [0, 1, 0, 1, 1],#右ダッシュジャンプ [1, 0, 0, 0, 1],#左ダッシュ [1, 0, 0, 1, 0],#左ジャンプ [1, 0, 0, 1, 1],#左ダッシュジャンプ ] # 経験を保存するメモリオブジェクトを生成 self.memory = ReplayMemory(CAPACITY) device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu') self.model = Net(num_states, num_actions) #self.model = self.model.to(device) self.optimizer = optim.Adam(self.model.parameters(), lr=0.001) self.loss = None self.epoch = 0 if os.path.exists("my_model_training_state.pt"): print("Succeesfully load") PATH = "my_model_training_state.pt" checkpoint = torch.load(PATH) self.model.load_state_dict(checkpoint['model_state_dict']) self.optimizer.load_state_dict(checkpoint['optimizer_state_dict']) self.loss = checkpoint['loss'] self.epoch = checkpoint['epoch'] # target_net self.target_net = copy.deepcopy(self.model) self.target_net.load_state_dict(self.model.state_dict()) def replay(self,episode): '''Experience Replayでネットワークの結合パラメータを出力''' # メモリサイズがミニバッチより小さい間は何もしない if len(self.memory) < BATCH_SIZE: return self.loss # メモリからミニバッチ分のデータを取り出す transitions = self.memory.sample(BATCH_SIZE) # 各変数をミニバッチに対応する形に変形 batch = Transition(*zip(*transitions)) # 各変数の要素をミニバッチに対応する形に変形する state_batch = torch.cat(batch.state) action_batch = torch.cat(batch.action) reward_batch = torch.cat(batch.reward) non_final_next_states = torch.cat([s for s in batch.next_state if s is not None]) # 教師信号となるQ(s_t, a_t)値を求める self.model.eval() # ネットワークが出力したQ(s_t, a_t)を求める state_action_values = self.model(state_batch).gather(1, action_batch.unsqueeze(1)) # max{Q(s_t+1, a)}値を求める。ただし、次の状態があるかに注意。 # is_Finishedがdoneになっておらず、next_stateがあるかをチェックするインデックスマスクを作成 non_final_mask = torch.BoolTensor( tuple(map(lambda s: s is not None, batch.next_state))) # まずは全部0にしておく next_state_values = torch.zeros(BATCH_SIZE) # 次の状態があるindexの最大Q値を求める self.target_net.eval() next_state_values[non_final_mask] = self.target_net(non_final_next_states).max(1)[0].detach() # 3.4 教師となるQ(s_t, a_t)を求める expected_state_action_values = (next_state_values * GAMMA) + reward_batch # ネットワークを訓練モードに切り替える self.model.train() # 損失関数を計算する (smooth_l1_lossはHuberloss) self.loss = F.smooth_l1_loss(state_action_values, expected_state_action_values.unsqueeze(1)) # 結合パラメータを更新する self.optimizer.zero_grad() # 勾配をリセット self.loss.backward() # バックプロパゲーションを計算 self.optimizer.step() # 結合パラメータを更新 return self.loss def save_model(self,epoch): torch.save({ 'epoch': epoch+self.epoch, 'model_state_dict': self.model.state_dict(), 'optimizer_state_dict': self.optimizer.state_dict(), 'loss': self.loss}, "my_model_training_state9.pt") def update_target_model(self): # モデルの重みをtarget_networkにコピー self.target_net.load_state_dict(self.model.state_dict()) def decide_action(self, state, episode): '''現在の状態に応じて、行動を決定する''' epsilon = 0.5 * (1 / ((episode+self.epoch)/1000 + 1)) if epsilon < EPSILON_LAST: epsilon = EPSILON_LAST if epsilon <= random.random(): self.model.eval() with torch.no_grad(): self.action = self.action_list[self.model(state).max(1)[1]] # ネットワークの出力の最大値のindexを取り出す = max(1)[1] # .view(1, 1)は[torch.LongTensor of size 1] を size 1x1 に変換する else: action_next = random.randint(0,len(self.action_list)-1) self.action = self.action_list[action_next] return self.action,epsilon,self.epoch def brain_predict(self, state): self.model.eval() # ネットワークを推論モードに切り替える with torch.no_grad(): self.action = self.action_list[self.model(state).max(1)[1]] return self.action
質問内容とはちょっと違いますが、保存したモデルをロードした(はずの)状態でのネットワークの各層の重みが、モデルを保存した時と同じになってるか、数値で表示させて確認したらいいと思います
参考
https://tzmi.hatenablog.com/entry/2021/04/30/105227
https://wonderfuru.com/%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF%E3%81%AE%E9%87%8D%E3%81%BF%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B/
https://betashort-lab.com/%E3%83%87%E3%83%BC%E3%82%BF%E3%82%B5%E3%82%A4%E3%82%A8%E3%83%B3%E3%82%B9/%E3%83%87%E3%82%A3%E3%83%BC%E3%83%97%E3%83%A9%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0/pytorch%E3%81%A7%E9%87%8D%E3%81%BF%E3%81%AE%E7%A2%BA%E8%AA%8D%E3%81%A8%E3%80%81%E7%95%B3%E3%81%BF%E8%BE%BC%E3%81%BF%E5%B1%A4%E3%81%AE%E3%82%AB%E3%83%BC%E3%83%8D%E3%83%AB%E3%81%AE%E5%8F%AF%E8%A6%96/
回答ありがとうございます。
確認させていただいたところ、モデルを保存した時とロードした時の各層の重みは一致していました。
ということは、ロードした後に重みが初期化されてる、ってことですかね
確認したところ、モデルの重みは初期化されていないようです。
> 中断したモデルから再開しようとしても初めからになってしまっている
の、まさにその再開の時点でも
> モデルの重みは初期化されていない
ので、ネットワークの各層の重みが、モデルを保存した時と同じになってる、ということですか?
回答ありがとうございます。
まさにその通りです。学習済みモデルを実行した後の重みを見ても、その重みを引き継いで学習が行われています。しかし、エージェント(マリオ)の動きは明らかに学習済みモデルと同等の動きをしていません。
学習が進むと変化するパラメータが元に戻ってるのではないですかね
たとえば、下記のεとか
http://arduinopid.web.fc2.com/N88.html
他にも何かそういうのが無いか、確認してみてください
回答ありがとうございます。返信が遅くなってしまい申し訳ありません。
元に戻ってしまっているパラメータがないか調べていたのですが、結局見つけることができませんでした...
そこで、モデルの保存・読み込みの方法を以下のようにモデル全体を保存するように変更したところ、学習済みモデルから再開することに成功しました。
保存
torch.save(self.model, 'model.pt')
読み込み
self.model = torch.load('lmodel.pt')
しかし、なぜこれでうまくいったのか分かっていないまま成功している状態です。また、以下のサイトによるとこの方法は非推奨となっているため、益々なぜうまくいったのか分かりません。
ですので、うまくいった要因等について教えていただきたいです。
サイトのリンクを張ることを忘れていました。以下のリンクが該当のものです。
https://wandb.ai/wandb_fc/japanese/reports/PyTorch---VmlldzoxNTAyODQy
> 元に戻ってしまっているパラメータがないか調べていたのですが、結局見つけることができませんでした...
のことですが、
> def decide_action(self, state, episode):
内の
> epsilon = 0.5 * (1 / ((episode+self.epoch)/1000 + 1))
で計算されてる「epsilon」が、
>> 下記のεとか
http://arduinopid.web.fc2.com/N88.html
の「ε」のようですが、保存したモデルをロードして
> 中断したモデルから再開
の際に、モデルを保存した時点の「epsilon」の値に設定されるのでしょうか?
「decide_action」を実行してるところが質問のコードに無いので、
> 中断したモデルから再開
の際の「episode」がどうなってるか不明ですが、その時もしかしたら「episode」が初期化されていて、その結果「epsilon」が
> 初めからになってしまっている
のではありませんでしょうか?
http://den3.net/activity_diary/2022/06/18/5785/
もマリオの学習をしてて、そのコードの「self.exploration_rate」がこの質問のコードの「epsilon」に相当するようですが、モデルを保存する際に「self.net.state_dict()」の他に「self.exploration_rate」も保存してて、モデルをロードする際に「self.exploration_rate」を保存時の値に設定してます
質問者さんのコードも、同様にモデルのロード時に、「epsilon」がモデル保存時点の値に設定されるように書かれてますでしょうか?
【追記】
上記と、下記に変えたら大丈夫になることが直接関係してるのかは分かりません
> 保存
torch.save(self.model, 'model.pt')
読み込み
self.model = torch.load('lmodel.pt')
ただ、
http://den3.net/activity_diary/2022/06/18/5785/
は「self.net.state_dict()」と「self.exploration_rate」を保存してロードしてうまくいってるようなので、この質問のコードでもそのあたりを同様にすればいけるのかも、と思いまして
> epsilon = 0.5 * (1 / ((episode+self.epoch)/1000 + 1))
の「self.epoch」は、「episode」が更新される(数値が増える)際に、「self.epoch」は「0」に初期化されるのでしょうか?
もし、「episode」の更新とは無関係に「self.epoch」は累積される(「0」に初期化されない)のなら、
> epsilon = 0.5 * (1 / ((episode+self.epoch)/1000 + 1))
で「episode」を使わず
> epsilon = 0.5 * (1 / ((self.epoch)/1000 + 1))
とすれば、「epsilon」も初期化されないのではないか、と思いまして
「self.epoch」もモデルと一緒に保存・ロードされてるし
【追記】
質問の「セーブ・ロードをしている部分のソースコード」のコードを再度見たら、保存時は
> 'epoch': epoch,
と「epoch」を保存してて、ロード時は
> self.epoch = checkpoint['epoch']
と「self.epoch」に読み込んでますが、保存時の「epoch」とロード時の「self.epoch」は同じものなのでしょうか?
保存時に、他は
> 'model_state_dict': self.model.state_dict(),
'optimizer_state_dict': self.optimizer.state_dict(),
'loss': self.loss},
と「self.」が付いてるのに、「epoch」だけ「self.」が付いてないのは何でかなぁ、と思いまして
【追記2】
質問の「以下に該当ソースコードを載せておきます。」のコードは、上記と違い
> 'epoch': epoch+self.epoch,
と「epoch+self.epoch」を保存してますね
ロード時に「self.epoch」に読み込んでるのは同じですが
「epoch+self.epoch」を保存して、「self.epoch」にロードって、値の受け渡しは矛盾しないのでしょうか?
回答ありがとうございます。
このdecide_actionの「episode」は現在行っている学習のエピソード数を渡しています。
そして、「self.epoch」は学習済みモデルが保存している学習数、つまり今までどのくらい学習していたかを表しています。
例えば、今までに200回学習していたら「self.epoch」は200になり、更にそこから学習を再開すると、「episode」は0..1..2..3.....と増えていき学習を200回学習させて保存すると、「self.epoch」には400が保存されるといった感じです。
また、試しにepsilonを固定値にしたモデルを学習させ保存してみても「self.model.state.dict()」ではうまくいかずにモデル全体を保存するようにするとうまくいきました。
> epsilon = 0.5 * (1 / ((episode+self.epoch)/1000 + 1))
のすぐ下に
print(epsilon)
を追加して実行して、その結果表示で、
A:本当の初回の一番最初
B:最初の学習が一通り終わる最後(モデルを保存する直前)
C:モデルをロードしてから学習を再開させた最初
のタイミングの数値を確認したら、Cではどうなってますでしょうか?
Bの時と(だいたい)同じでしょうか?
またそれは、うまくいってないもともとの保存・ロードのやり方と、モデル全体を保存するやり方で、違いはありませんでしょうか?
> 試しにepsilonを固定値にしたモデルを学習させ保存してみても「self.model.state.dict()」ではうまくいかずにモデル全体を保存するようにするとうまくいきました。
ということなら、保存・ロードの方法でうまくいったりいかなかったりすることと、「epsilon」が継続されるかは関係無いのでしょうけど、「epsilon」の役割を考えると学習の続きで初期化されるのはよくないと思うので、もし初期化されてるのなら、それはそれで直した方がいいと思います
> 今までに200回学習していたら「self.epoch」は200になり、更にそこから学習を再開すると、「episode」は0..1..2..3.....と増えていき学習を200回学習させて保存すると、「self.epoch」には400が保存される
の説明では、「episode」が一つ増える間に「self.epoch」がいくつ増えるのかが分からないのですが、
> 100epoch(episode)ごとにモデルを保存
が、「self.epoch」が100増えたら「episode」が一つ増えるという意味なら、
> epsilon = 0.5 * (1 / ((episode+self.epoch)/1000 + 1))
の計算で、「self.epoch」の増加に比べて「episode」の増加は微々たるものなので、もし「episode」がモデルのロードの度に初期化されるとしても、「self.epoch」がちゃんと引き継がれてたら、「epsilon」もそれなりに引き継がれるので、大丈夫でしょうね
回答ありがとうございます。
今回のソースコードでは、マリオが学習を終える度に(クリアする・死亡する度に)episodeをインクリメントする、という形になっています。
また、BとCを比べてもepsilonは同一でした。
質問者さんの環境で、
http://den3.net/activity_diary/2022/06/18/5785/
で解説されてるコードが動くのなら、そちらではどうなるのかを確認してみたらいいと思います
「学習が途中で終了した場合は最新のものを読み込むことで継続学習が可能になります。」と書かれてて、コードでは「self.net.state_dict()」と「self.exploration_rate」を保存してロードしてます
もし上記のコードならうまく継続学習できるのなら、それとの差異がどこにあるのかを突き止めたらいいと思います
もし上記のコードでもうまく継続学習ができないのなら、やはり何か不足してるデータがあるのでしょう
あなたの回答
tips
プレビュー