HW#1 Imitation Learning

HW #1

Behavioral Cloning

DAgger 를 적용해보기 전에는 replay buffer 에서 sampling 하는 것과 MLP policy 네트워크 코드만 작성하면 된다.

policy 에서는 observation 과 action 을 mapping 하는 MLP 구조로 되어 있어, forward() 부분에는 코랩 튜토리얼을 바탕으로 gaussian distribution 을 output 으로 만듦.

class GaussianPolicy(nn.Module):
    def __init__(self, input_size, output_size):
        super(GaussianPolicy, self).__init__()
 
        # Mean will be a function of input; std will be a fixed (learned) value
        self.mean_fc1 = nn.Linear(input_size, 32)
        self.mean_fc2 = nn.Linear(32, 32)
        self.mean_fc3 = nn.Linear(32, output_size)
        self.log_std = nn.Parameter(torch.randn(output_size))
 
    def forward(self, x):
        mean = F.relu(self.mean_fc1(x))
        mean = F.relu(self.mean_fc2(mean))
        mean = self.mean_fc3(mean)
        return distributions.Normal(mean, self.log_std.exp())

처음에는 mean_net, log_std 가 왜 있는지, nn.Sequential 로 MLP 를 만들고도 왜 forward() 를 따로 구현해야 하는지 잘 몰라서 (보통 튜토리얼 예제들과 다르다보니) 헤매다가 가 distribution 인 내용을 이해하고 변수들의 존재 이유를 알 수 있었음.

update() 함수에서는 확률분포로부터 sampling 한 observation 을 얻고, 이를 MSE(square loss) 나 NLL(Negative log-likelihood) 로 Loss 를 계산한다.

https://wensun.github.io/CS4789_data/Imitation_Learning_April_8_annotated.pdf

위 자료에서 많이 사용되는 것이 NLL 이나 MSE 라고 하였고, quadratic error 증명 파트에서 MSE Loss 를 사용해서 이를 그대로 코드에 사용함.

https://pytorch.org/docs/stable/generated/torch.nn.MSELoss.html

def update(self, observations, actions):
        """
        Updates/trains the policy
 
        :param observations: observation(s) to query the policy
        :param actions: actions we want the policy to imitate
        :return:
            dict: 'Training Loss': supervised learning loss
        """
        observations = ptu.from_numpy(observations)
        observations.requires_grad = True
        actions = ptu.from_numpy(actions)
        actions.requires_grad = True
        
        loss_fn = nn.MSELoss() # nn.NLLLoss()
        sample_obs = self(observations).sample()
        loss = loss_fn(sample_obs, actions)
        
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()
        return {
            'Training Loss': ptu.to_numpy(loss),
        }

궁금한 점

1. 보통 PyTorch 예제를 보면 모델을 정의하고 forward() 대신 모델의 클래스 변수에 넣어 y = net(x) 와 같이 사용한다. 아마도 이렇게 하면 nn.Module 상위 클래스의 forward() 함수가 wrapping 되어 input_dim 으로 넣어주면 feed foward 가 자동으로 되는 것으로 보이는데, 여기 코드를 짤 때는 update() 가 클래스 내부에 있어서 클래스 변수명을 사용할 수 없어 forward() 를 직접 호출하였는데 맞는지는 모르겠다.expert_policy 에서 self(observation) 으로 작성되어 있음!!

2. NLLLoss 로 짜는 경우에는 코랩 예제처럼 loss = -self.forward.log_prob(actions).sum() 로 하거나 loss = -self.forward.log_prob(actions).mean() 으로 하는 것 같다. (어떤 것이 맞는 방법인지?) https://pytorch.org/docs/stable/generated/torch.nn.NLLLoss.html

DAgger

Behavioral cloning 에서는 n_iter 가 1 이지만 DAgger 에서는 여러 번 학습하며 expert 의 데이터를 가져와야 한다.

environment 에서 policy 를 수행해서 얻은 결과를 라벨링을 통해 데이터셋을 수정하고 이를 가지고 policy 를 다시 학습시킨다.

코드에서는 샘플링한 action 을 expert 의 action 으로 바꾸는 방식으로 DAgger 를 적용하였다.

앞서 Behavioral cloning 코드를 돌려보려면 sample_trajectory 함수도 작성했어야 하므로, 여기 부분은 지시사항에 맞게 작성만 해주면 의외로 쉽게 마무리된다.

궁금한 점

  1. *def* sample_trajectories(*env*, *policy*, *min_timesteps_per_batch*, *max_path_length*, *render*=False): 에서 max_path_length 는 어떤 변수를 의미하는지?

DAgger 에서 sampling 할 때 total_envstepsenvsteps_this_batch 중 어떤 변수를 넣어야 맞는지 모르겠음.

  1. 몇몇 코드를 맞게 짠 것인지? 현재는 DAgger 를 실행하면 index out of range 에러가 나타남.
	paths[itr]["action"] = expert_policy.get_action(paths[itr]["observation"])
IndexError: list index out of range

아마도 sample_trajectory 구현 부분에서 문제가 있어보임. (1번과 연관이 있거나)


디버깅 및 제출

Important

docs 폴더에 Analysis 와 결과 제출 pdf 파일 업로드.

코드에서 잘못된 부분은 위 궁금한 점에서 max_path_length 에 잘못된 변수를 넣었던 것이다.

paths, envsteps_this_batch = utils.sample_trajectories(
                env, actor, params['batch_size'], params['ep_len'])

여기에 total_envstepsenvsteps_this_batch 가 아닌 episode length 가 들어가야 한다.

두 번째로 random.permutation 으로 무작위 학습 데이터를 가져오는 부분에서 잘못되었다.

rand_buffer = np.random.permutation(replay_buffer.__len__())
ob_batch, ac_batch = replay_buffer.obs[rand_buffer[0: params['train_batch_size']]], \
                     replay_buffer.acs[rand_buffer[0: params['train_batch_size']]]

위와 같이 전체 데이터 중에서 batch_size 만큼 무작위로 가져와야 한다.

이전에는 아래와 같이 작성했다.

ri = np.random.permutation(params['train_batch_size'])
ob_batch, ac_batch = replay_buffer.obs[ri], replay_buffer.acs[ri]

세 번째로 rsample()sample() 의 차이이다.

https://stackoverflow.com/questions/60533150/what-is-the-difference-between-sample-and-rsample

forward 에서 distributions.Normal(self.mean_net(observation), self.logstd.exp()) 을 return 하도록 하였고, action 을 가져올 때마다 .sample() 을 사용하였다. 근데 성능이 매우 저조하였고 가장 높은게 expert policy의 약 0.3% 였다. 여러 param 들을 바꿔서 테스트해보아도 정상적인 연관성이 나타나지 않았다.

어떤 환경은 evaluation 에서의 episode length 가 1000 으로 끝까지 수행하였음에도 평균 return 이 작았다.

그래서 문제를 찾아서 아래와 같이 수정하였다.

loss_fn = nn.MSELoss() # nn.NLLLoss()
sample_ac = self(observations).rsample()
loss = loss_fn(sample_ac, actions)
 
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()

loss 를 계산하여 신경망을 학습하는 경우에는 .rsample() 을 사용해서 넣어주었다.

ob = ptu.from_numpy(ob.astype(np.float32)).requires_grad_()
ac = policy(ob).sample() # HINT: this is a numpy array
ob = ptu.to_numpy(ob)
ac = ptu.to_numpy(ac)

sample_trajectory() 에서 action 을 한번 가져올 때는 .sample() 을 사용하였다.

그랬더니 성능들이 정상적으로 나왔고 제출 파일까지 작성 완료.