I am working on porting an effective model from TensorFlow to PyTorch but have been unable to get the network to learn effectively in PyTorch. I suspect there is a simple misunderstanding on my end of how PyTorch operates. I have been working on this port too long now and am finally willing to admit I could use a little help
The problem I am experiencing is the train accuracy seems to peak at ~25% which is barely better than guessing. I have tried playing with the optimizer (various optimizers, learning rates, and other hyperparams) but, again, the tensorflow version does very well (so I don’t believe retuning is necessary). This leads me to believe the way I am using the optimizer is erroneous. I did confirm the model weights change on optimizer.step() by inspecting the LSTM layer’s parameters (see the training logic below).
The goal is to classify single-channel (one feature) time-series data into one of five classes.
Here is my module implementation
class Discriminator(nn.Module):
def __init__(self, hidden_size: int, target_seq_len: int, num_classes: int):#, batch_size: int):
super(Discriminator, self).__init__()
#self.batch_size = batch_size
self.hidden_size = hidden_size
self.num_layers = 2
self.n_units = 8
self.bidirectional = True
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
drop_out_pct = 0.1
self.lstm = nn.LSTM(input_size=target_seq_len, num_layers=self.num_layers, hidden_size=hidden_size,
batch_first=True, bidirectional=self.bidirectional, dropout=0.0).to(self.device)
self.drop_out = nn.Dropout(drop_out_pct).to(self.device)
self.linear = nn.Linear(hidden_size * (2 if self.bidirectional else 1), self.n_units).to(self.device)
self.relu = nn.ReLU().to(self.device)
self.linear_out = nn.Linear(self.n_units, num_classes).to(self.device)
#self.softmax = nn.LogSoftmax(dim=1).to(self.device)
#self.init_lstm_memory() # dont do this here - we now do it every epoch
def init_lstm_memory(self, batch_size: int) -> torch.Tensor:
hidden = (
torch.zeros(((2 if self.bidirectional else 1) * self.num_layers, batch_size, self.hidden_size), dtype=torch.float).to(self.device),
torch.zeros(((2 if self.bidirectional else 1) * self.num_layers, batch_size, self.hidden_size), dtype=torch.float).to(self.device)
)
#for i, tensor in enumerate(hidden):
# nn.init.normal_(tensor, mean=0, std=1)
return hidden
def forward(self, input: torch.Tensor, hidden: torch.Tensor) -> (torch.Tensor, torch.Tensor):
x = input.to(self.device)
x, hidden = self.lstm(x, hidden)
x = x[:, -1, :] # we're interested in the final output of the lstm
#if self.bidirectional:
# x = torch.cat((hidden[0][-2, :, :], hidden[1][-1, :, :]), dim=1)
#else:
# x = hidden[0][-1, :, :]
#print(f'shape after dropout {x.shape}')
x = self.linear(x)
x = self.relu(x)
x = self.drop_out(x)
#print(f'shape after linear {x.shape}')
x = self.linear_out(x)
#print(f'shape after linear_out {x.shape}')
#x = self.softmax(x)
x = torch.nn.functional.softmax(x, dim=1)
return x, hidden
Here is my training logic
def repackage_hidden(h):
""" Wraps hidden states in new Tensors, to detach them from their history.
Copied from https://github.com/pytorch/examples/blob/master/word_language_model/main.py """
if isinstance(h, torch.Tensor):
return h.detach()
else:
return tuple(repackage_hidden(v) for v in h)
def trainLSTM(data: np.array, lbls: np.array):
data = np.expand_dims(data, axis=2)
print(f'data shape {data.shape}')
num_epochs = 1600
batch_size = 128
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
label_encoder = LabelEncoder()
lbls = label_encoder.fit_transform(lbls)
x_train, x_test, y_train, y_test = train_test_split(data, lbls, stratify=lbls, test_size=0.15)
x_train = torch.from_numpy(x_train).float()#.to(device) # we dont want to put all of this on the GPU at once - at least I dont think so..
x_test = torch.from_numpy(x_test).float()#.to(device)
y_train = torch.from_numpy(y_train)#.int()#.to(device)
y_test = torch.from_numpy(y_test)#.int()#.to(device)
#print(y_test)
model = Discriminator(hidden_size=32, target_seq_len=1, num_classes=5)#, batch_size)
#optimizer = Adam(model.parameters(), lr=LR)
optimizer = RMSprop(model.parameters(), lr=LR, alpha=ALPHA) # pretty sure alpha is equivalent to TFs rho
#optimizer = SGD(model.parameters(), lr=LR, momentum=MOMENTUM)
scheduler = LambdaLR(optimizer, lambda epoch: 1.0 if epoch < 10 else math.exp(-0.1))
loss_fn = nn.CrossEntropyLoss() #nn.NLLLoss()
print(f'model params: {model.parameters()}')
train_dataset = MyDataset(x_train, y_train) # Note: MyDataset provides defs for __len__ and__getitem__ (it is trivial and the definition has been intentionally omitted in this post)
test_dataset = MyDataset(x_test, y_test)
train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)#, drop_last=True) # drop last so we can use the same hidden state
test_dataloader = DataLoader(test_dataset, batch_size=batch_size, shuffle=True)
model.train()
#print(torchinfo.summary(model))
# plot sanity visualizations - Note: this yielded the expected results
#rand_indices = random.sample(range(x_train.shape[0]), 5)
#print(rand_indices)
#fig, axs = plt.subplots(nrows=5)
#fig.suptitle('None')
#x_vals = np.arange(longest_seq_len)
#for i in range(len(rand_indices)):
# #print(x_train[rand_indices[i]].shape)
# sns.lineplot(x=x_vals, y=x_train[rand_indices[i]].squeeze().numpy(), ax=axs[i])
#plt.show()
#return
for i in range(num_epochs):
print(f'epoch: {i}')
epoch_loss = epoch_acc = 0
#lstm_hidden = model.init_lstm_memory(batch_size)
for train_batch, train_lbls in train_dataloader:
#print(train_batch.shape, train_lbls.shape)
train_batch, train_lbls = train_batch.to(device), train_lbls.to(device)
#lstm_hidden = repackage_hidden(lstm_hidden)
lstm_hidden = model.init_lstm_memory(train_batch.shape[0])
optimizer.zero_grad()
preds, lstm_hidden = model(train_batch, lstm_hidden)
loss = loss_fn(preds, train_lbls)
#print(f'loss shape {loss.shape}')
loss.backward()
# check lstm weights
#print(next(model.lstm.parameters()))
#old_lstm_weights = list(model.lstm.parameters())[0].clone()
#torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # tried grad clipping - doesnt seem to help
optimizer.step()
#new_lstm_weights = list(model.lstm.parameters())[0].clone()
#for param in model.parameters():
# print(param.grad)
#return
#print(f'weights unchanged: {torch.equal(old_lstm_weights.data, new_lstm_weights.data)}')
#if torch.equal(old_lstm_weights.data, new_lstm_weights.data):
# any_param_grad_not_none = False
# grad = None
# for param in model.parameters():
# if param.grad is not None:
# any_param_grad_not_none = True
# grad = param.grad
# break
# print(f'new lstm weights grad: {new_lstm_weights.grad}')
# print(f'any model weights grad not none: {any_param_grad_not_none}')
# print(f'not none grad: {grad}')
#preds = torch.nn.functional.softmax(preds, dim=1)
#print(f'first pred: {preds[0]}')
_, preds = torch.max(preds, dim=1) # extract the predicted labels (index of max arg)
epoch_loss += loss.detach().item() # update loss
epoch_acc += (preds == train_lbls).float().sum() # update correct count
scheduler.step()
epoch_acc /= len(train_dataset) # TODO: this is an incorrect divisor if drop_last is true in the train dataloader
print(f'epoch loss: {epoch_loss:.3f}')
print(f'epoch acc: {epoch_acc:.3f}')
Does anyone see anything wrong with my training loop or model which may explain the horrible results I am receiving? Any help would be greatly appreciated. Thank you for your time.