blob: 1dce851264f10388bc2ea039ac7f8f43b93bb8ee [file] [log] [blame]
#include <gtest/gtest.h>
#include <torch/torch.h>
#include <test/cpp/api/support.h>
using namespace torch::nn;
using namespace torch::test;
template <typename R, typename Func>
bool test_RNN_xor(Func&& model_maker, bool cuda = false) {
torch::manual_seed(0);
auto nhid = 32;
auto model = std::make_shared<SimpleContainer>();
auto l1 = model->add(Linear(1, nhid), "l1");
auto rnn = model->add(model_maker(nhid), "rnn");
auto lo = model->add(Linear(nhid, 1), "lo");
torch::optim::Adam optimizer(model->parameters(), 1e-2);
auto forward_op = [&](torch::Tensor x) {
auto T = x.size(0);
auto B = x.size(1);
x = x.view({T * B, 1});
x = l1->forward(x).view({T, B, nhid}).tanh_();
x = rnn->forward(x).output[T - 1];
x = lo->forward(x);
return x;
};
if (cuda) {
model->to(torch::kCUDA);
}
float running_loss = 1;
int epoch = 0;
auto max_epoch = 1500;
while (running_loss > 1e-2) {
auto bs = 16U;
auto nlen = 5U;
const auto backend = cuda ? torch::kCUDA : torch::kCPU;
auto inputs =
torch::rand({nlen, bs, 1}, backend).round().to(torch::kFloat32);
auto labels = inputs.sum(0).detach();
inputs.set_requires_grad(true);
auto outputs = forward_op(inputs);
torch::Tensor loss = torch::mse_loss(outputs, labels);
optimizer.zero_grad();
loss.backward();
optimizer.step();
running_loss = running_loss * 0.99 + loss.item<float>() * 0.01;
if (epoch > max_epoch) {
return false;
}
epoch++;
}
return true;
};
void check_lstm_sizes(RNNOutput output) {
// Expect the LSTM to have 64 outputs and 3 layers, with an input of batch
// 10 and 16 time steps (10 x 16 x n)
ASSERT_EQ(output.output.ndimension(), 3);
ASSERT_EQ(output.output.size(0), 10);
ASSERT_EQ(output.output.size(1), 16);
ASSERT_EQ(output.output.size(2), 64);
ASSERT_EQ(output.state.ndimension(), 4);
ASSERT_EQ(output.state.size(0), 2); // (hx, cx)
ASSERT_EQ(output.state.size(1), 3); // layers
ASSERT_EQ(output.state.size(2), 16); // Batchsize
ASSERT_EQ(output.state.size(3), 64); // 64 hidden dims
// Something is in the hiddens
ASSERT_GT(output.state.norm().item<float>(), 0);
}
struct RNNTest : torch::test::SeedingFixture {};
TEST_F(RNNTest, CheckOutputSizes) {
LSTM model(LSTMOptions(128, 64).layers(3).dropout(0.2));
// Input size is: sequence length, batch size, input size
auto x = torch::randn({10, 16, 128}, torch::requires_grad());
auto output = model->forward(x);
auto y = x.mean();
y.backward();
check_lstm_sizes(output);
auto next = model->forward(x, output.state);
check_lstm_sizes(next);
torch::Tensor diff = next.state - output.state;
// Hiddens changed
ASSERT_GT(diff.abs().sum().item<float>(), 1e-3);
}
TEST_F(RNNTest, CheckOutputValuesMatchPyTorch) {
torch::manual_seed(0);
// Make sure the outputs match pytorch outputs
LSTM model(2, 2);
for (auto& v : model->parameters()) {
float size = v.numel();
auto p = static_cast<float*>(v.storage().data());
for (size_t i = 0; i < size; i++) {
p[i] = i / size;
}
}
auto x = torch::empty({3, 4, 2}, torch::requires_grad());
float size = x.numel();
auto p = static_cast<float*>(x.storage().data());
for (size_t i = 0; i < size; i++) {
p[i] = (size - i) / size;
}
auto out = model->forward(x);
ASSERT_EQ(out.output.ndimension(), 3);
ASSERT_EQ(out.output.size(0), 3);
ASSERT_EQ(out.output.size(1), 4);
ASSERT_EQ(out.output.size(2), 2);
auto flat = out.output.view(3 * 4 * 2);
float c_out[] = {0.4391, 0.5402, 0.4330, 0.5324, 0.4261, 0.5239,
0.4183, 0.5147, 0.6822, 0.8064, 0.6726, 0.7968,
0.6620, 0.7860, 0.6501, 0.7741, 0.7889, 0.9003,
0.7769, 0.8905, 0.7635, 0.8794, 0.7484, 0.8666};
for (size_t i = 0; i < 3 * 4 * 2; i++) {
ASSERT_LT(std::abs(flat[i].item<float>() - c_out[i]), 1e-3);
}
ASSERT_EQ(out.state.ndimension(), 4); // (hx, cx) x layers x B x 2
ASSERT_EQ(out.state.size(0), 2);
ASSERT_EQ(out.state.size(1), 1);
ASSERT_EQ(out.state.size(2), 4);
ASSERT_EQ(out.state.size(3), 2);
flat = out.state.view(16);
float h_out[] = {0.7889,
0.9003,
0.7769,
0.8905,
0.7635,
0.8794,
0.7484,
0.8666,
1.1647,
1.6106,
1.1425,
1.5726,
1.1187,
1.5329,
1.0931,
1.4911};
for (size_t i = 0; i < 16; i++) {
ASSERT_LT(std::abs(flat[i].item<float>() - h_out[i]), 1e-3);
}
}
TEST_F(RNNTest, EndToEndLSTM) {
ASSERT_TRUE(test_RNN_xor<LSTM>(
[](int s) { return LSTM(LSTMOptions(s, s).layers(2)); }));
}
TEST_F(RNNTest, EndToEndGRU) {
ASSERT_TRUE(
test_RNN_xor<GRU>([](int s) { return GRU(GRUOptions(s, s).layers(2)); }));
}
TEST_F(RNNTest, EndToEndRNNRelu) {
ASSERT_TRUE(test_RNN_xor<RNN>(
[](int s) { return RNN(RNNOptions(s, s).relu().layers(2)); }));
}
TEST_F(RNNTest, EndToEndRNNTanh) {
ASSERT_TRUE(test_RNN_xor<RNN>(
[](int s) { return RNN(RNNOptions(s, s).tanh().layers(2)); }));
}
TEST_F(RNNTest, Sizes_CUDA) {
torch::manual_seed(0);
LSTM model(LSTMOptions(128, 64).layers(3).dropout(0.2));
model->to(torch::kCUDA);
auto x =
torch::randn({10, 16, 128}, torch::requires_grad().device(torch::kCUDA));
auto output = model->forward(x);
auto y = x.mean();
y.backward();
check_lstm_sizes(output);
auto next = model->forward(x, output.state);
check_lstm_sizes(next);
torch::Tensor diff = next.state - output.state;
// Hiddens changed
ASSERT_GT(diff.abs().sum().item<float>(), 1e-3);
}
TEST_F(RNNTest, EndToEndLSTM_CUDA) {
ASSERT_TRUE(test_RNN_xor<LSTM>(
[](int s) { return LSTM(LSTMOptions(s, s).layers(2)); }, true));
}
TEST_F(RNNTest, EndToEndGRU_CUDA) {
ASSERT_TRUE(test_RNN_xor<GRU>(
[](int s) { return GRU(GRUOptions(s, s).layers(2)); }, true));
}
TEST_F(RNNTest, EndToEndRNNRelu_CUDA) {
ASSERT_TRUE(test_RNN_xor<RNN>(
[](int s) { return RNN(RNNOptions(s, s).relu().layers(2)); }, true));
}
TEST_F(RNNTest, EndToEndRNNTanh_CUDA) {
ASSERT_TRUE(test_RNN_xor<RNN>(
[](int s) { return RNN(RNNOptions(s, s).tanh().layers(2)); }, true));
}
TEST_F(RNNTest, PrettyPrintRNNs) {
ASSERT_EQ(
c10::str(LSTM(LSTMOptions(128, 64).layers(3).dropout(0.2))),
"torch::nn::LSTM(input_size=128, hidden_size=64, layers=3, dropout=0.2)");
ASSERT_EQ(
c10::str(GRU(GRUOptions(128, 64).layers(3).dropout(0.5))),
"torch::nn::GRU(input_size=128, hidden_size=64, layers=3, dropout=0.5)");
ASSERT_EQ(
c10::str(RNN(RNNOptions(128, 64).layers(3).dropout(0.2).tanh())),
"torch::nn::RNN(input_size=128, hidden_size=64, layers=3, dropout=0.2, activation=tanh)");
}
// This test assures that flatten_parameters does not crash,
// when bidirectional is set to true
// https://github.com/pytorch/pytorch/issues/19545
TEST_F(RNNTest, BidirectionalFlattenParameters) {
GRU gru(GRUOptions(100, 256).layers(2).bidirectional(true));
gru->flatten_parameters();
}
template <typename Impl>
void copyParameters(torch::nn::ModuleHolder<Impl>& target, size_t t_i,
const torch::nn::ModuleHolder<Impl>& source, size_t s_i) {
at::NoGradGuard guard;
target->w_ih[t_i].copy_(source->w_ih[s_i]);
target->w_hh[t_i].copy_(source->w_hh[s_i]);
target->b_ih[t_i].copy_(source->b_ih[s_i]);
target->b_hh[t_i].copy_(source->b_hh[s_i]);
}
// This test is a port of python code introduced here:
// https://towardsdatascience.com/understanding-bidirectional-rnn-in-pytorch-5bd25a5dd66
// Reverse forward of bidrectional GRU should act
// as regular forward of unidirectional GRU
void BidirectionalGRUReverseForward(bool cuda) {
auto opt = torch::TensorOptions().dtype(torch::kFloat32).requires_grad(false)
.device(cuda ? torch::kCUDA : torch::kCPU);
auto input = torch::tensor({1, 2, 3, 4, 5}, opt).reshape({5, 1, 1});
auto input_reversed = torch::tensor({5, 4, 3, 2, 1}, opt).reshape({5, 1, 1});
auto gru_options = GRUOptions(1, 1).layers(1).batch_first(false);
GRU bi_grus {gru_options.bidirectional(true)};
GRU reverse_gru {gru_options.bidirectional(false)};
if (cuda) {
bi_grus->to(torch::kCUDA);
reverse_gru->to(torch::kCUDA);
}
// Now make sure the weights of the reverse gru layer match
// ones of the (reversed) bidirectional's:
copyParameters(reverse_gru, 0, bi_grus, 1);
auto bi_output = bi_grus->forward(input);
auto reverse_output = reverse_gru->forward(input_reversed);
if (cuda) {
bi_output.output = bi_output.output.to(torch::kCPU);
bi_output.state = bi_output.state.to(torch::kCPU);
reverse_output.output = reverse_output.output.to(torch::kCPU);
reverse_output.state = reverse_output.state.to(torch::kCPU);
}
ASSERT_EQ(bi_output.output.size(0), reverse_output.output.size(0));
auto size = bi_output.output.size(0);
for (int i = 0; i < size; i++) {
ASSERT_EQ(bi_output.output[i][0][1].item<float>(),
reverse_output.output[size - 1 - i][0][0].item<float>());
}
// The hidden states of the reversed GRUs sits
// in the odd indices in the first dimension.
ASSERT_EQ(bi_output.state[1][0][0].item<float>(),
reverse_output.state[0][0][0].item<float>());
}
TEST_F(RNNTest, BidirectionalGRUReverseForward) {
BidirectionalGRUReverseForward(false);
}
TEST_F(RNNTest, BidirectionalGRUReverseForward_CUDA) {
BidirectionalGRUReverseForward(true);
}
// Reverse forward of bidrectional LSTM should act
// as regular forward of unidirectional LSTM
void BidirectionalLSTMReverseForwardTest(bool cuda) {
auto opt = torch::TensorOptions().dtype(torch::kFloat32).requires_grad(false)
.device(cuda ? torch::kCUDA : torch::kCPU);
auto input = torch::tensor({1, 2, 3, 4, 5}, opt).reshape({5, 1, 1});
auto input_reversed = torch::tensor({5, 4, 3, 2, 1}, opt).reshape({5, 1, 1});
auto lstm_opt = GRUOptions(1, 1).layers(1).batch_first(false);
LSTM bi_lstm {lstm_opt.bidirectional(true)};
LSTM reverse_lstm {lstm_opt.bidirectional(false)};
if (cuda) {
bi_lstm->to(torch::kCUDA);
reverse_lstm->to(torch::kCUDA);
}
// Now make sure the weights of the reverse lstm layer match
// ones of the (reversed) bidirectional's:
copyParameters(reverse_lstm, 0, bi_lstm, 1);
auto bi_output = bi_lstm->forward(input);
auto reverse_output = reverse_lstm->forward(input_reversed);
if (cuda) {
bi_output.output = bi_output.output.to(torch::kCPU);
bi_output.state = bi_output.state.to(torch::kCPU);
reverse_output.output = reverse_output.output.to(torch::kCPU);
reverse_output.state = reverse_output.state.to(torch::kCPU);
}
ASSERT_EQ(bi_output.output.size(0), reverse_output.output.size(0));
auto size = bi_output.output.size(0);
for (int i = 0; i < size; i++) {
ASSERT_EQ(bi_output.output[i][0][1].item<float>(),
reverse_output.output[size - 1 - i][0][0].item<float>());
}
// The hidden states of the reversed LSTM sits
// in the odd indices in the first dimension.
ASSERT_EQ(bi_output.state[0][1][0][0].item<float>(),
reverse_output.state[0][0][0][0].item<float>());
ASSERT_EQ(bi_output.state[1][1][0][0].item<float>(),
reverse_output.state[1][0][0][0].item<float>());
}
TEST_F(RNNTest, BidirectionalLSTMReverseForward) {
BidirectionalLSTMReverseForwardTest(false);
}
TEST_F(RNNTest, BidirectionalLSTMReverseForward_CUDA) {
BidirectionalLSTMReverseForwardTest(true);
}
TEST_F(RNNTest, BidirectionalMultilayerGRU_CPU_vs_CUDA) {
// Create two GRUs with the same options
auto opt = GRUOptions(2, 4).layers(3).batch_first(false).bidirectional(true);
GRU gru_cpu {opt};
GRU gru_cuda {opt};
// Copy weights and biases from CPU GRU to CUDA GRU
{
at::NoGradGuard guard;
const auto num_directions = gru_cpu->options.bidirectional() ? 2 : 1;
for (int64_t layer = 0; layer < gru_cpu->options.layers(); layer++) {
for (auto direction = 0; direction < num_directions; direction++) {
const auto layer_idx = (layer * num_directions) + direction;
copyParameters(gru_cuda, layer_idx, gru_cpu, layer_idx);
}
}
}
gru_cpu->flatten_parameters();
gru_cuda->flatten_parameters();
// Move GRU to CUDA
gru_cuda->to(torch::kCUDA);
// Create the same inputs
auto input_opt = torch::TensorOptions()
.dtype(torch::kFloat32).requires_grad(false);
auto input_cpu = torch::tensor({1, 2, 3, 4, 5, 6}, input_opt)
.reshape({3, 1, 2});
auto input_cuda = torch::tensor({1, 2, 3, 4, 5, 6}, input_opt)
.reshape({3, 1, 2}).to(torch::kCUDA);
// Call forward on both GRUs
auto output_cpu = gru_cpu->forward(input_cpu);
auto output_cuda = gru_cuda->forward(input_cuda);
output_cpu.output = output_cpu.output.to(torch::kCPU);
output_cpu.state = output_cpu.state.to(torch::kCPU);
// Assert that the output and state are equal on CPU and CUDA
ASSERT_EQ(output_cpu.output.dim(), output_cuda.output.dim());
for (int i = 0; i < output_cpu.output.dim(); i++) {
ASSERT_EQ(output_cpu.output.size(i), output_cuda.output.size(i));
}
for (int i = 0; i < output_cpu.output.size(0); i++) {
for (int j = 0; j < output_cpu.output.size(1); j++) {
for (int k = 0; k < output_cpu.output.size(2); k++) {
ASSERT_NEAR(
output_cpu.output[i][j][k].item<float>(),
output_cuda.output[i][j][k].item<float>(), 1e-5);
}
}
}
}
TEST_F(RNNTest, BidirectionalMultilayerLSTM_CPU_vs_CUDA) {
// Create two LSTMs with the same options
auto opt = LSTMOptions(2, 4).layers(3).batch_first(false).bidirectional(true);
LSTM lstm_cpu {opt};
LSTM lstm_cuda {opt};
// Copy weights and biases from CPU LSTM to CUDA LSTM
{
at::NoGradGuard guard;
const auto num_directions = lstm_cpu->options.bidirectional() ? 2 : 1;
for (int64_t layer = 0; layer < lstm_cpu->options.layers(); layer++) {
for (auto direction = 0; direction < num_directions; direction++) {
const auto layer_idx = (layer * num_directions) + direction;
copyParameters(lstm_cuda, layer_idx, lstm_cpu, layer_idx);
}
}
}
lstm_cpu->flatten_parameters();
lstm_cuda->flatten_parameters();
// Move LSTM to CUDA
lstm_cuda->to(torch::kCUDA);
auto options = torch::TensorOptions()
.dtype(torch::kFloat32).requires_grad(false);
auto input_cpu = torch::tensor({1, 2, 3, 4, 5, 6}, options)
.reshape({3, 1, 2});
auto input_cuda = torch::tensor({1, 2, 3, 4, 5, 6}, options)
.reshape({3, 1, 2}).to(torch::kCUDA);
// Call forward on both LSTMs
auto output_cpu = lstm_cpu->forward(input_cpu);
auto output_cuda = lstm_cuda->forward(input_cuda);
output_cpu.output = output_cpu.output.to(torch::kCPU);
output_cpu.state = output_cpu.state.to(torch::kCPU);
// Assert that the output and state are equal on CPU and CUDA
ASSERT_EQ(output_cpu.output.dim(), output_cuda.output.dim());
for (int i = 0; i < output_cpu.output.dim(); i++) {
ASSERT_EQ(output_cpu.output.size(i), output_cuda.output.size(i));
}
for (int i = 0; i < output_cpu.output.size(0); i++) {
for (int j = 0; j < output_cpu.output.size(1); j++) {
for (int k = 0; k < output_cpu.output.size(2); k++) {
ASSERT_NEAR(
output_cpu.output[i][j][k].item<float>(),
output_cuda.output[i][j][k].item<float>(), 1e-5);
}
}
}
}