Add MultiheadAttention test, fix dimension calculation

Also added some language to the comments about the sequence lengths.
This commit is contained in:
Adam Kropp
2023-11-10 14:27:00 -05:00
parent 3363707881
commit 2bdae7d438
5 changed files with 298 additions and 227 deletions
@@ -50,7 +50,12 @@ namespace mlpack {
* The input to this layer is expected to be a sequence of embedding vectors.
* The embedding size is inferred from inputDimensions[0], and the source
* sequence length is inferred from inputDimensions[1]. If there are more than
* 2 dimensions, they are flattened into the source sequence length.
* 2 dimensions, they are flattened into the source sequence length. If
* selfAttention is true, then query, key, and value are all the same, so the
* input data should be of size [embedDim * seqLen, batchSize]. Otherwise,
* the input data should be of size
* [embedDim * (2 * srcSeqLen + tgtSeqLen), batchSize]. The
* output data will always be of size (embedDim * tgtSeqLen, batchSize)
*
* @tparam MatType Type of the input/output data (arma::colvec, arma::mat,
* arma::sp_mat or arma::cube).
@@ -76,11 +81,15 @@ class MultiheadAttentionType : public Layer<MatType>
*
* @param tgtSeqLen Target sequence length.
* @param numHeads Number of parallel attention heads.
* @param attnMask Two dimensional Attention Mask. Takes the values [-Inf, 0]
* @param keyPaddingMask Key Padding Mask. Takes the values [-Inf, 0]
* @param selfAttention Use self-attention; source key, query, and value all
* come from the same inputs
*/
MultiheadAttentionType(const size_t tgtSeqLen,
const size_t numHeads,
const MatType& attnMask = MatType(),
const MatType& keyPaddingMask = MatType(),
const bool selfAttention = false);
//! Clone the MultiheadAttentionType object. This handles polymorphism
@@ -139,6 +148,11 @@ class MultiheadAttentionType : public Layer<MatType>
template<typename Archive>
void serialize(Archive& ar, const uint32_t /* version */);
//! Get the parameters.
MatType const& Parameters() const override { return weights; }
//! Modify the parameters.
MatType& Parameters() override { return weights; }
//! Get the target sequence length.
size_t TgtSeqLen() const { return tgtSeqLen; }
//! Modify the target sequence length.
@@ -183,9 +197,29 @@ class MultiheadAttentionType : public Layer<MatType>
<< " [EmbeddingDim,SequenceLen]" << std::endl;
}
embedDim = this->inputDimensions[0];
srcSeqLen = this->inputDimensions[1];
for (size_t i=2; i<this->inputDimensions.size(); i++) {
srcSeqLen *= this->inputDimensions[i];
if (selfAttention) {
// for selfAttention, query, key, and value
// all come from the same values
// also, srcSeqLen must == tgtSeqLen
srcSeqLen = this->inputDimensions[1];
for (size_t i=2; i<this->inputDimensions.size(); i++) {
srcSeqLen *= this->inputDimensions[i];
}
if (srcSeqLen != tgtSeqLen) {
Log::Fatal << "If using selfAttention, srcSeqLen must equal tgtSeqLen"
<< std::endl;
}
}
else {
// if we are not using self attention (where query == key == value),
// then the source sequence needs have
// query = tgtSeqLen
// key = srcSeqLen
// value = srcSeqLen
if ((this->inputDimensions[1] - tgtSeqLen) % 2 != 0) {
Log::Fatal << "input dimension 1 is invalid." << std::endl;
}
srcSeqLen = (this->inputDimensions[1] - tgtSeqLen) / 2;
}
if (embedDim % numHeads != 0)
{
@@ -197,9 +231,17 @@ class MultiheadAttentionType : public Layer<MatType>
// This returns the output as a 2-dimensional (embedDim * tgtSeqLen)
// matrix.
this->outputDimensions = std::vector<size_t>(2, 1);
this->outputDimensions = std::vector<size_t>(this->inputDimensions.size(), 1);
this->outputDimensions[0] = embedDim;
this->outputDimensions[1] = tgtSeqLen;
for (size_t i=2; i<this->outputDimensions.size(); i++) {
this->outputDimensions[i] = this->inputDimensions[i];
if (this->outputDimensions[1] % this->inputDimensions[i] != 0) {
Log::Fatal << "tgtSeqLen " << tgtSeqLen << " not divisible by extra "
<< "dimension " << this->inputDimensions[i] << std::endl;
}
this->outputDimensions[1] /= this->inputDimensions[i];
}
}
size_t InputShape() const
@@ -226,10 +268,11 @@ class MultiheadAttentionType : public Layer<MatType>
//! Dimensionality of each head.
size_t headDim;
//! Two dimensional Attention Mask of shape (tgtSeqLen, srcSeqLen).
//! Two dimensional Attention Mask of shape (tgtSeqLen, srcSeqLen). Takes
//! the values [-Inf, 0]
MatType attnMask;
//! Key Padding Mask.
//! Key Padding Mask. Takes the values [-Inf, 0]
MatType keyPaddingMask;
//! Whether or not self-attention is used (source key, value, and query all
@@ -38,11 +38,15 @@ MultiheadAttentionType<MatType, RegularizerType>::
MultiheadAttentionType(
const size_t tgtSeqLen,
const size_t numHeads,
const MatType& attnmask,
const MatType& keypaddingmask,
const bool selfAttention) :
tgtSeqLen(tgtSeqLen),
srcSeqLen(0),
embedDim(0),
numHeads(numHeads),
attnMask(attnmask),
keyPaddingMask(keypaddingmask),
selfAttention(selfAttention)
{
}
@@ -141,40 +145,24 @@ Forward(const MatType& input, MatType& output)
// Apply the attention mask if provided. The attention mask is used to black-
// out future sequences and generally used in Encoder-Decoder attention.
// The attention mask has elements 0 or 1.
// The attention mask has elements -inf or 0.
// The shape of the attention mask : (tgtSeqLen, srcSeqLen).
if (!attnMask.is_empty())
{
if (attnMask.n_rows != tgtSeqLen || attnMask.n_cols != srcSeqLen)
Log::Fatal << "The size of the 'attn_mask' is not correct.\n";
// not sure if there is a better way to do this. now that the mask
// is 0 or 1, we can't simply add, but converting first seems slow
const arma::uword rows = tgtSeqLen;
const arma::uword cols = srcSeqLen;
for (arma::uword i=0; i<rows; i++) {
for (arma::uword j=0; j<cols; j++) {
if (attnMask.at(i, j) == 0) {
scores.tube(i, j).fill(-INFINITY);
}
}
}
scores.each_slice() += attnMask;
}
// Apply the key padding mask when provided. It blacks-out any particular
// word in the sequence.
// The key padding mask has elements 0 or 1.
// The key padding mask has elements -inf or 0
// The shape of keyPaddingMask : (1, srcSeqLen).
if (!keyPaddingMask.is_empty())
{
if (keyPaddingMask.n_rows != 1 || keyPaddingMask.n_cols != srcSeqLen)
Log::Fatal << "The size of the 'keyPaddingMask' is not correct.\n";
// not sure if there is a better way to do this. now that the mask
// is 0 or 1, we can't simply add, but converting first seems slow
for (arma::uword pos=0; pos<keyPaddingMask.n_elem; pos++) {
if (keyPaddingMask.at(pos) == 0) {
scores.tube(0, pos, tgtSeqLen, pos).fill(-INFINITY);
}
}
scores.each_slice() += arma::repmat(keyPaddingMask, tgtSeqLen, 1);
}
for (size_t i = 0; i < numHeads * batchSize; ++i)
@@ -491,11 +479,11 @@ serialize(Archive& ar, const uint32_t /* version */)
ar(CEREAL_NVP(selfAttention));
ar(CEREAL_NVP(softmax));
ar(CEREAL_NVP(regularizer));
ar(CEREAL_NVP(attnMask));
ar(CEREAL_NVP(keyPaddingMask));
if (Archive::is_loading::value)
{
attnMask.clear();
keyPaddingMask.clear();
queryWt.clear();
keyWt.clear();
valueWt.clear();
@@ -0,0 +1,237 @@
/**
* @file tests/layer/multihead_attention.cpp
* @author Marcus Edel
* @author Praveen Ch
*
* Tests the multihead_attention layer.
*
* mlpack is free software; you may redistribute it and/or modify it under the
* terms of the 3-clause BSD license. You should have received a copy of the
* 3-clause BSD license along with mlpack. If not, see
* http://www.opensource.org/licenses/BSD-3-Clause for more information.
*/
#include <mlpack/core.hpp>
#include <mlpack/methods/ann.hpp>
#include "../../test_catch_tools.hpp"
#include "../../catch.hpp"
#include "../../serialization.hpp"
#include "../ann_test_tools.hpp"
using namespace mlpack;
/**
* Simple Multihead Attention test.
*/
TEST_CASE("SimpleMultiheadAttentionTest", "[ANNLayerTest]")
{
size_t tLen = 5;
size_t sLen = tLen;
size_t embedDim = 4;
size_t numHeads = 2;
size_t bsz = 3;
arma::mat query = 0.1 * arma::randu(embedDim * tLen, bsz);
arma::mat output;
arma::mat attnMask = arma::zeros(tLen, sLen);
for (size_t i = 0; i < tLen; ++i)
{
for (size_t j = 0; j < sLen; ++j)
{
if (i < j)
attnMask(i, j) = std::numeric_limits<double>::lowest();
}
}
arma::mat keyPaddingMask = arma::zeros(1, sLen);
keyPaddingMask(sLen - 1) = std::numeric_limits<double>::lowest();
MultiheadAttention module(tLen, numHeads);
module.InputDimensions() = std::vector<size_t>({ embedDim, 2 * sLen + tLen });
module.ComputeOutputDimensions();
arma::mat weights(module.WeightSize(), 1);
weights.randu();
module.SetWeights(weights.memptr());
module.AttentionMask() = attnMask;
module.KeyPaddingMask() = keyPaddingMask;
// Forward test.
arma::mat input = arma::join_cols(arma::join_cols(query, query), query);
module.Forward(input, output);
REQUIRE(output.n_rows == embedDim * tLen);
REQUIRE(output.n_cols == bsz);
// Backward test.
arma::mat gy = 0.01 * arma::randu(embedDim * tLen, bsz);
arma::mat g;
module.Backward(input, output, gy, g);
REQUIRE(g.n_rows == input.n_rows);
REQUIRE(g.n_cols == input.n_cols);
// Gradient test.
arma::mat error = 0.05 * arma::randu(embedDim * tLen, bsz);
arma::mat gradient;
module.Gradient(input, error, gradient);
REQUIRE(gradient.n_rows == module.Parameters().n_rows);
REQUIRE(gradient.n_cols == module.Parameters().n_cols);
}
/**
* Jacobian MultiheadAttention module test.
*/
TEST_CASE("JacobianMultiheadAttentionTest", "[ANNLayerTest]")
{
// Check when query = key = value.
for (size_t i = 0; i < 5; ++i)
{
const size_t tgtSeqLen = 2;
const size_t embedDim = 4;
const size_t nHeads = 2;
const size_t batchSize = 1;
arma::mat query = arma::randu(embedDim * tgtSeqLen, batchSize);
arma::mat input = arma::join_cols(arma::join_cols(query, query), query);
MultiheadAttention module(tgtSeqLen, nHeads);
module.InputDimensions() = std::vector<size_t>({ embedDim, 3 * tgtSeqLen });
module.ComputeOutputDimensions();
arma::mat weights(module.WeightSize(), 1);
weights.randu();
module.SetWeights(weights.memptr());
double error = CustomJacobianTest(module, input);
REQUIRE(error <= 1e-5);
}
// Check when key = value.
for (size_t i = 0; i < 5; ++i)
{
const size_t tgtSeqLen = 2;
const size_t srcSeqLen = RandInt(2, 5);
const size_t embedDim = 4;
const size_t nHeads = 2;
const size_t batchSize = 1;
arma::mat query = arma::randu(embedDim * tgtSeqLen, batchSize);
arma::mat key = 0.091 * arma::randu(embedDim * srcSeqLen, batchSize);
arma::mat input = arma::join_cols(arma::join_cols(query, key), key);
MultiheadAttention module(tgtSeqLen, nHeads);
module.InputDimensions() = std::vector<size_t>({ embedDim, 2 * srcSeqLen + tgtSeqLen });
module.ComputeOutputDimensions();
arma::mat weights(module.WeightSize(), 1);
weights.randu();
module.SetWeights(weights.memptr());
double error = CustomJacobianTest(module, input);
REQUIRE(error <= 1e-5);
}
// Check when query, key and value are not same.
for (size_t i = 0; i < 5; ++i)
{
const size_t tgtSeqLen = 2;
const size_t srcSeqLen = RandInt(2, 5);
const size_t embedDim = 4;
const size_t nHeads = 2;
const size_t batchSize = 1;
arma::mat query = arma::randu(embedDim * tgtSeqLen, batchSize);
arma::mat key = 0.091 * arma::randu(embedDim * srcSeqLen, batchSize);
arma::mat value = 0.045 * arma::randu(embedDim * srcSeqLen, batchSize);
arma::mat input = arma::join_cols(arma::join_cols(query, key), value);
MultiheadAttention module(tgtSeqLen, nHeads);
module.InputDimensions() = std::vector<size_t>({ embedDim, 2 * srcSeqLen + tgtSeqLen });
module.ComputeOutputDimensions();
arma::mat weights(module.WeightSize(), 1);
weights.randu();
module.SetWeights(weights.memptr());
double error = JacobianTest(module, input);
REQUIRE(error <= 1e-5);
}
}
/**
* Numerical gradient test for MultiheadAttention layer.
*/
TEST_CASE("GradientMultiheadAttentionTest", "[ANNLayerTest]")
{
struct GradientFunction
{
GradientFunction() :
tgtSeqLen(2),
srcSeqLen(2),
embedDim(4),
nHeads(2),
vocabSize(5),
batchSize(2)
{
input = arma::randu(embedDim * (tgtSeqLen + 2 * srcSeqLen), batchSize);
target = arma::zeros(vocabSize, batchSize);
for (size_t i = 0; i < target.n_elem; ++i)
{
const size_t label = RandInt(1, vocabSize);
target(i) = label;
}
attnMask = arma::zeros(tgtSeqLen, srcSeqLen);
for (size_t i = 0; i < tgtSeqLen; ++i)
{
for (size_t j = 0; j < srcSeqLen; ++j)
{
if (i < j)
attnMask(i, j) = std::numeric_limits<double>::lowest();
}
}
keyPaddingMask = arma::zeros(1, srcSeqLen);
keyPaddingMask(srcSeqLen - 1) = std::numeric_limits<double>::lowest();
model = new FFN<NegativeLogLikelihood, XavierInitialization>();
model->InputDimensions() = {embedDim, srcSeqLen * 2 + tgtSeqLen};
model->ResetData(input, target);
// attnModule = new MultiheadAttention(tgtSeqLen, srcSeqLen, embedDim,
// nHeads);
// attnModule->AttentionMask() = attnMask;
// attnModule->KeyPaddingMask() = keyPaddingMask;
// model->Add(attnModule);
model->Add<MultiheadAttention>(tgtSeqLen, nHeads,
attnMask, keyPaddingMask);
model->Add<Linear>(vocabSize);
model->Add<LogSoftMax>();
}
~GradientFunction()
{
delete model;
}
double Gradient(arma::mat& gradient) const
{
double error = model->Evaluate(model->Parameters(), 0, batchSize);
model->Gradient(model->Parameters(), 0, gradient, batchSize);
return error;
}
arma::mat& Parameters() { return model->Parameters(); }
FFN<NegativeLogLikelihood, XavierInitialization>* model;
// MultiheadAttention* attnModule;
arma::mat input, target, attnMask, keyPaddingMask;
const size_t tgtSeqLen;
const size_t srcSeqLen;
const size_t embedDim;
const size_t nHeads;
const size_t vocabSize;
const size_t batchSize;
} function;
REQUIRE(CheckGradient(function) <= 3e-06);
}
+1
View File
@@ -42,3 +42,4 @@
#include "layer/softmin.cpp"
#include "layer/ftswish.cpp"
#include "layer/layer_norm.cpp"
#include "layer/multihead_attention.cpp"
@@ -3980,204 +3980,6 @@ TEST_CASE("JacobianPositionalEncodingTest", "[ANNLayerTest]")
}
*/
/**
* Simple Multihead Attention test.
*/
TEST_CASE("SimpleMultiheadAttentionTest", "[ANNLayerTest]")
{
size_t tLen = 5;
size_t sLen = tLen;
size_t embedDim = 4;
size_t numHeads = 2;
size_t bsz = 3;
arma::mat query = 0.1 * arma::randu(embedDim * tLen, bsz);
arma::mat output;
arma::mat attnMask = arma::zeros(tLen, sLen);
for (size_t i = 0; i < tLen; ++i)
{
for (size_t j = 0; j < sLen; ++j)
{
if (i < j)
attnMask(i, j) = std::numeric_limits<double>::lowest();
}
}
arma::mat keyPaddingMask = arma::zeros(1, sLen);
keyPaddingMask(sLen - 1) = std::numeric_limits<double>::lowest();
MultiheadAttention module(tLen, sLen, embedDim, numHeads);
module.AttentionMask() = attnMask;
module.KeyPaddingMask() = keyPaddingMask;
module.Reset();
module.Parameters().randu();
// Forward test.
arma::mat input = arma::join_cols(arma::join_cols(query, query), query);
module.Forward(input, output);
REQUIRE(output.n_rows == embedDim * tLen);
REQUIRE(output.n_cols == bsz);
// Backward test.
arma::mat gy = 0.01 * arma::randu(embedDim * tLen, bsz);
arma::mat g;
module.Backward(input, gy, g);
REQUIRE(g.n_rows == input.n_rows);
REQUIRE(g.n_cols == input.n_cols);
// Gradient test.
arma::mat error = 0.05 * arma::randu(embedDim * tLen, bsz);
arma::mat gradient;
module.Gradient(input, error, gradient);
REQUIRE(gradient.n_rows == module.Parameters().n_rows);
REQUIRE(gradient.n_cols == module.Parameters().n_cols);
}
/**
* Jacobian MultiheadAttention module test.
*/
TEST_CASE("JacobianMultiheadAttentionTest", "[ANNLayerTest]")
{
// Check when query = key = value.
for (size_t i = 0; i < 5; ++i)
{
const size_t tgtSeqLen = 2;
const size_t embedDim = 4;
const size_t nHeads = 2;
const size_t batchSize = 1;
arma::mat query = arma::randu(embedDim * tgtSeqLen, batchSize);
arma::mat input = arma::join_cols(arma::join_cols(query, query), query);
MultiheadAttention module(tgtSeqLen, tgtSeqLen, embedDim, nHeads);
module.Parameters().randu();
double error = CustomJacobianTest(module, input);
REQUIRE(error <= 1e-5);
}
// Check when key = value.
for (size_t i = 0; i < 5; ++i)
{
const size_t tgtSeqLen = 2;
const size_t srcSeqLen = RandInt(2, 5);
const size_t embedDim = 4;
const size_t nHeads = 2;
const size_t batchSize = 1;
arma::mat query = arma::randu(embedDim * tgtSeqLen, batchSize);
arma::mat key = 0.091 * arma::randu(embedDim * srcSeqLen, batchSize);
arma::mat input = arma::join_cols(arma::join_cols(query, key), key);
MultiheadAttention module(tgtSeqLen, srcSeqLen, embedDim, nHeads);
module.Parameters().randu();
double error = CustomJacobianTest(module, input);
REQUIRE(error <= 1e-5);
}
// Check when query, key and value are not same.
for (size_t i = 0; i < 5; ++i)
{
const size_t tgtSeqLen = 2;
const size_t srcSeqLen = RandInt(2, 5);
const size_t embedDim = 4;
const size_t nHeads = 2;
const size_t batchSize = 1;
arma::mat query = arma::randu(embedDim * tgtSeqLen, batchSize);
arma::mat key = 0.091 * arma::randu(embedDim * srcSeqLen, batchSize);
arma::mat value = 0.045 * arma::randu(embedDim * srcSeqLen, batchSize);
arma::mat input = arma::join_cols(arma::join_cols(query, key), value);
MultiheadAttention module(tgtSeqLen, srcSeqLen, embedDim, nHeads);
module.Parameters().randu();
double error = JacobianTest(module, input);
REQUIRE(error <= 1e-5);
}
}
/**
* Numerical gradient test for MultiheadAttention layer.
*/
TEST_CASE("GradientMultiheadAttentionTest", "[ANNLayerTest]")
{
struct GradientFunction
{
GradientFunction() :
tgtSeqLen(2),
srcSeqLen(2),
embedDim(4),
nHeads(2),
vocabSize(5),
batchSize(2)
{
input = arma::randu(embedDim * (tgtSeqLen + 2 * srcSeqLen), batchSize);
target = arma::zeros(vocabSize, batchSize);
for (size_t i = 0; i < target.n_elem; ++i)
{
const size_t label = RandInt(1, vocabSize);
target(i) = label;
}
attnMask = arma::zeros(tgtSeqLen, srcSeqLen);
for (size_t i = 0; i < tgtSeqLen; ++i)
{
for (size_t j = 0; j < srcSeqLen; ++j)
{
if (i < j)
attnMask(i, j) = std::numeric_limits<double>::lowest();
}
}
keyPaddingMask = arma::zeros(1, srcSeqLen);
keyPaddingMask(srcSeqLen - 1) = std::numeric_limits<double>::lowest();
model = new FFN<NegativeLogLikelihood, XavierInitialization>();
model->ResetData(input, target);
// attnModule = new MultiheadAttention(tgtSeqLen, srcSeqLen, embedDim,
// nHeads);
// attnModule->AttentionMask() = attnMask;
// attnModule->KeyPaddingMask() = keyPaddingMask;
// model->Add(attnModule);
model->Add<MultiheadAttention>(tgtSeqLen, srcSeqLen, embedDim, nHeads,
attnMask, keyPaddingMask);
model->Add<Linear>(embedDim * tgtSeqLen, vocabSize);
model->Add<LogSoftMax>();
}
~GradientFunction()
{
delete model;
}
double Gradient(arma::mat& gradient) const
{
double error = model->Evaluate(model->Parameters(), 0, batchSize);
model->Gradient(model->Parameters(), 0, gradient, batchSize);
return error;
}
arma::mat& Parameters() { return model->Parameters(); }
FFN<NegativeLogLikelihood, XavierInitialization>* model;
// MultiheadAttention* attnModule;
arma::mat input, target, attnMask, keyPaddingMask;
const size_t tgtSeqLen;
const size_t srcSeqLen;
const size_t embedDim;
const size_t nHeads;
const size_t vocabSize;
const size_t batchSize;
} function;
REQUIRE(CheckGradient(function) <= 3e-06);
}
/**
* Simple tests for instance normalization layer.