Files
tensorflow--tensorflow/tensorflow/lite/kernels/random_ops_test.cc
T
wehub-resource-sync 8a852e4b4e
cffconvert / validate (push) Has been skipped
License Check / license-check (push) Failing after 2s
chore: import upstream snapshot with attribution
2026-07-13 12:14:16 +08:00

462 lines
17 KiB
C++

/* Copyright 2021 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
#include <algorithm>
#include <initializer_list>
#include <vector>
#include <gtest/gtest.h>
#include "absl/strings/str_cat.h"
#include "tensorflow/lite/kernels/test_util.h"
#include "tensorflow/lite/schema/schema_generated.h"
namespace tflite {
namespace {
enum class InputType {
kConst = 0,
kDynamic = 1,
};
class RandomOpModel : public SingleOpModel {
public:
RandomOpModel(BuiltinOperator op_code, InputType input_type,
const std::initializer_list<int32_t>& shape,
int32_t seed = 0, int32_t seed2 = 0) {
bool is_input_const = (input_type == InputType::kConst);
if (is_input_const) {
input_ = AddConstInput(TensorType_INT32, shape,
{static_cast<int32_t>(shape.size())});
} else {
input_ =
AddInput({TensorType_INT32, {static_cast<int32_t>(shape.size())}});
}
output_ = AddOutput({TensorType_FLOAT32, {}});
SetBuiltinOp(op_code, BuiltinOptions_RandomOptions,
CreateRandomOptions(builder_, seed, seed2).Union());
BuildInterpreter({GetShape(input_)});
if (!is_input_const) {
PopulateTensor<int32_t>(input_, std::vector<int32_t>(shape));
}
}
int input() { return input_; }
int output() { return output_; }
std::vector<float> GetOutput() { return ExtractVector<float>(output_); }
private:
int input_;
int output_;
};
class MultinomialOpModel : public SingleOpModel {
public:
MultinomialOpModel(InputType input_type,
const std::initializer_list<float>& logits,
int num_batches, int num_classes, int num_samples,
int32_t seed = 0, int32_t seed2 = 0,
tflite::TensorType output_type = TensorType_INT64) {
bool is_input_const = (input_type == InputType::kConst);
auto logits_shape = {num_batches, num_classes};
if (is_input_const) {
logits_ = AddConstInput(TensorType_FLOAT32, logits, logits_shape);
} else {
logits_ = AddInput({TensorType_FLOAT32, logits_shape});
}
num_samples_ = AddConstInput(TensorType_INT32, {num_samples}, {});
output_ = AddOutput({output_type, {}});
SetBuiltinOp(BuiltinOperator_MULTINOMIAL, BuiltinOptions_RandomOptions,
CreateRandomOptions(builder_, seed, seed2).Union());
BuildInterpreter({GetShape(logits_), GetShape(num_samples_)});
if (!is_input_const) {
PopulateTensor<float>(logits_, std::vector<float>(logits));
}
}
int logits() { return logits_; }
int num_samples() { return num_samples_; }
int output() { return output_; }
std::vector<int64_t> GetOutput() { return ExtractVector<int64_t>(output_); }
std::vector<int32_t> GetInt32Output() {
return ExtractVector<int32_t>(output_);
}
private:
int logits_;
int num_samples_;
int output_;
};
class TestSuite : public testing::TestWithParam<std::tuple<
BuiltinOperator, InputType>> {
};
TEST_P(TestSuite, NonDeterministicOutputWithSeedsEqualToZero)
{
BuiltinOperator op_code = std::get<0>(GetParam());
InputType input_type = std::get<1>(GetParam());
RandomOpModel m1(op_code, input_type,
/*shape=*/{100, 50, 5}, /*seed=*/0, /*seed2=*/0);
ASSERT_EQ(m1.Invoke(), kTfLiteOk);
std::vector<float> output1a = m1.GetOutput();
EXPECT_EQ(output1a.size(), 100 * 50 * 5);
ASSERT_EQ(m1.Invoke(), kTfLiteOk);
std::vector<float> output1b = m1.GetOutput();
// Verify that consecutive outputs are different.
EXPECT_NE(output1a, output1b);
RandomOpModel m2(op_code, input_type,
/*shape=*/{100, 50, 5}, /*seed=*/0, /*seed2=*/0);
ASSERT_EQ(m2.Invoke(), kTfLiteOk);
std::vector<float> output2a = m2.GetOutput();
EXPECT_EQ(output2a.size(), 100 * 50 * 5);
ASSERT_EQ(m2.Invoke(), kTfLiteOk);
std::vector<float> output2b = m2.GetOutput();
// Verify that consecutive outputs are different.
EXPECT_NE(output2a, output2b);
// Verify that outputs are non-deterministic (different random sequences)
EXPECT_NE(output1a, output2a);
EXPECT_NE(output1b, output2b);
}
TEST_P(TestSuite, DeterministicOutputWithNonZeroSeeds) {
BuiltinOperator op_code = std::get<0>(GetParam());
InputType input_type = std::get<1>(GetParam());
RandomOpModel m1(op_code, input_type, /*shape=*/{100, 50, 5},
/*seed=*/1234, /*seed2=*/5678);
ASSERT_EQ(m1.Invoke(), kTfLiteOk);
std::vector<float> output1a = m1.GetOutput();
EXPECT_EQ(output1a.size(), 100 * 50 * 5);
ASSERT_EQ(m1.Invoke(), kTfLiteOk);
std::vector<float> output1b = m1.GetOutput();
// Verify that consecutive outputs are different.
EXPECT_NE(output1a, output1b);
RandomOpModel m2(op_code, input_type, /*shape=*/{100, 50, 5},
/*seed=*/1234, /*seed2=*/5678);
ASSERT_EQ(m2.Invoke(), kTfLiteOk);
std::vector<float> output2a = m2.GetOutput();
EXPECT_EQ(output2a.size(), 100 * 50 * 5);
ASSERT_EQ(m2.Invoke(), kTfLiteOk);
std::vector<float> output2b = m2.GetOutput();
// Verify that consecutive outputs are different.
EXPECT_NE(output2a, output2b);
// Verify that outputs are determinisitc (same random sequence)
EXPECT_EQ(output1a, output2a);
EXPECT_EQ(output1b, output2b);
}
INSTANTIATE_TEST_SUITE_P(
RandomOpTest, TestSuite,
testing::Combine(
testing::Values(BuiltinOperator_RANDOM_UNIFORM,
BuiltinOperator_RANDOM_STANDARD_NORMAL),
testing::Values(InputType::kConst, InputType::kDynamic)),
[](const testing::TestParamInfo<TestSuite::ParamType>& info) {
std::string name = absl::StrCat(
std::get<0>(info.param) == BuiltinOperator_RANDOM_UNIFORM ?
"_RandomUniformOp" : "_RandomStandardNormalOp",
std::get<1>(info.param) == InputType::kConst ?
"_ConstInput" : "_DynamicInput");
return name;
}
);
TEST(RandomUniformOpTest, OutputMeanAndVariance) {
RandomOpModel m(/*op_code*/BuiltinOperator_RANDOM_UNIFORM,
/*input_type=*/InputType::kConst,
/*shape=*/{100, 50, 5}, /*seed=*/1234, /*seed2=*/5678);
// Initialize output tensor to infinity to validate that all of its values are
// updated and are normally distributed after Invoke().
const std::vector<float> output_data(100 * 50 * 5,
std::numeric_limits<float>::infinity());
m.PopulateTensor(m.output(), output_data);
ASSERT_EQ(m.Invoke(), kTfLiteOk);
auto output = m.GetOutput();
EXPECT_EQ(output.size(), 100 * 50 * 5);
// For uniform distribution with min=0 and max=1:
// * Mean = (max-min)/2 = 0.5
// * Variance = 1/12 * (max-min)^2 = 1/12
// Mean should be approximately 0.5
double sum = 0;
for (const auto r : output) {
sum += r;
}
double mean = sum / output.size();
ASSERT_LT(std::abs(mean - 0.5), 0.05);
// Variance should be approximately 1/12
double sum_squared = 0;
for (const auto r : output) {
sum_squared += std::pow(r - mean, 2);
}
double var = sum_squared / output.size();
EXPECT_LT(std::abs(1. / 12 - var), 0.05);
}
TEST(RandomStandardNormalOpTest, OutputMeanAndVariance) {
RandomOpModel m(/*op_code*/BuiltinOperator_RANDOM_STANDARD_NORMAL,
/*input_type=*/InputType::kConst,
/*shape=*/{100, 50, 5}, /*seed=*/1234, /*seed2=*/5678);
// Initialize output tensor to infinity to validate that all of its values are
// updated and are normally distributed after Invoke().
const std::vector<float> output_data(100 * 50 * 5,
std::numeric_limits<float>::infinity());
m.PopulateTensor(m.output(), output_data);
ASSERT_EQ(m.Invoke(), kTfLiteOk);
auto output = m.GetOutput();
EXPECT_EQ(output.size(), 100 * 50 * 5);
// For uniform distribution with min=0 and max=1:
// * Mean = (max-min)/2 = 0.5
// * Variance = 1/12 * (max-min)^2 = 1/12
// Mean should be approximately 0.5
double sum = 0;
for (const auto r : output) {
sum += r;
}
double mean = sum / output.size();
ASSERT_LT(std::abs(mean), 0.05);
// Variance should be approximately 1/12
double sum_squared = 0;
for (const auto r : output) {
sum_squared += std::pow(r - mean, 2);
}
double var = sum_squared / output.size();
EXPECT_LT(std::abs(1.0 - var), 0.05);
}
class MultinomialOpTestSuite : public testing::TestWithParam<InputType> {};
TEST_P(MultinomialOpTestSuite, NonDeterministicOutputWithSeedsEqualToZero) {
const std::initializer_list<float> kLogits = {logf(0.3f), logf(0.7f)};
const int kNumBatches = 1;
const int kNumClasses = 2;
const int kNumSamples = 30;
MultinomialOpModel m1(GetParam(), kLogits, kNumBatches, kNumClasses,
kNumSamples, /*seed=*/0, /*seed2=*/0);
ASSERT_EQ(m1.Invoke(), kTfLiteOk);
std::vector<int64_t> output1a = m1.GetOutput();
EXPECT_EQ(output1a.size(), kNumSamples);
ASSERT_EQ(m1.Invoke(), kTfLiteOk);
std::vector<int64_t> output1b = m1.GetOutput();
// Verify that consecutive outputs are different.
EXPECT_NE(output1a, output1b);
MultinomialOpModel m2(GetParam(), kLogits, kNumBatches, kNumClasses,
kNumSamples, /*seed=*/0, /*seed2=*/0);
ASSERT_EQ(m2.Invoke(), kTfLiteOk);
std::vector<int64_t> output2a = m2.GetOutput();
EXPECT_EQ(output2a.size(), kNumSamples);
ASSERT_EQ(m2.Invoke(), kTfLiteOk);
std::vector<int64_t> output2b = m2.GetOutput();
// Verify that consecutive outputs are different.
EXPECT_NE(output2a, output2b);
// Verify that outputs are non-deterministic (different random sequences)
EXPECT_NE(output1a, output2a);
EXPECT_NE(output1b, output2b);
}
TEST_P(MultinomialOpTestSuite, DeterministicOutputWithNonZeroSeeds) {
const std::initializer_list<float> kLogits = {logf(0.3f), logf(0.7f)};
const int kNumBatches = 1;
const int kNumClasses = 2;
const int kNumSamples = 30;
MultinomialOpModel m1(GetParam(), kLogits, kNumBatches, kNumClasses,
kNumSamples, /*seed=*/123, /*seed2=*/456);
ASSERT_EQ(m1.Invoke(), kTfLiteOk);
std::vector<int64_t> output1a = m1.GetOutput();
EXPECT_EQ(output1a.size(), kNumBatches * kNumSamples);
ASSERT_EQ(m1.Invoke(), kTfLiteOk);
std::vector<int64_t> output1b = m1.GetOutput();
EXPECT_EQ(output1b.size(), kNumBatches * kNumSamples);
// Verify that consecutive outputs are different.
EXPECT_NE(output1a, output1b);
MultinomialOpModel m2(GetParam(), kLogits, kNumBatches, kNumClasses,
kNumSamples, /*seed=*/123, /*seed2=*/456);
ASSERT_EQ(m2.Invoke(), kTfLiteOk);
std::vector<int64_t> output2a = m2.GetOutput();
EXPECT_EQ(output2a.size(), kNumBatches * kNumSamples);
ASSERT_EQ(m2.Invoke(), kTfLiteOk);
std::vector<int64_t> output2b = m2.GetOutput();
EXPECT_EQ(output2b.size(), kNumBatches * kNumSamples);
// Verify that consecutive outputs are different.
EXPECT_NE(output2a, output2b);
// Verify that outputs are determinisitc (same random sequence)
EXPECT_EQ(output1a, output2a);
EXPECT_EQ(output1b, output2b);
}
INSTANTIATE_TEST_SUITE_P(
RandomOpTest2, MultinomialOpTestSuite,
testing::Values(InputType::kConst, InputType::kDynamic),
[](const testing::TestParamInfo<MultinomialOpTestSuite::ParamType>& info) {
std::string name = absl::StrCat(
"_MultinomialOp",
info.param == InputType::kConst ? "_ConstInput" : "_DynamicInput");
return name;
});
TEST(MultinomialTest, ValidateTFLiteOutputisTheSameAsTFOutput_OutputTypeInt32) {
const std::initializer_list<float> kLogits = {-1.2039728, -0.35667497};
const int kNumBatches = 1;
const int kNumClasses = 2;
const int kNumSamples = 10;
MultinomialOpModel m(/*input_type=*/InputType::kConst, kLogits, kNumBatches,
kNumClasses, kNumSamples, /*seed=*/1234, /*seed2=*/5678,
TensorType_INT32);
const std::vector<std::vector<int32_t>> expected_outputs = {
{1, 0, 1, 0, 1, 1, 1, 1, 1, 1},
{1, 1, 1, 0, 1, 1, 0, 0, 0, 1},
{0, 1, 1, 0, 1, 1, 1, 1, 0, 1},
{1, 1, 1, 0, 1, 0, 0, 0, 1, 0}};
// Validate output.
for (int i = 0; i < expected_outputs.size(); i++) {
ASSERT_EQ(m.Invoke(), kTfLiteOk);
auto output = m.GetInt32Output();
EXPECT_EQ(output.size(), kNumBatches * kNumSamples);
EXPECT_EQ(expected_outputs[i], output);
}
}
TEST(MultinomialTest, ValidateTFLiteOutputisTheSameAsTFOutput) {
const std::initializer_list<float> kLogits = {-1.609438, -1.2039728,
-0.6931472};
const int kNumBatches = 1;
const int kNumClasses = 3;
const int kNumSamples = 15;
MultinomialOpModel m(/*input_type=*/InputType::kConst, kLogits, kNumBatches,
kNumClasses, kNumSamples, /*seed=*/5678, /*seed2=*/1234);
const std::vector<std::vector<int64_t>> expected_outputs = {
{1, 2, 1, 2, 1, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2},
{1, 2, 0, 0, 2, 1, 2, 0, 1, 0, 2, 2, 0, 2, 2},
{1, 1, 2, 2, 2, 2, 1, 1, 2, 2, 0, 0, 2, 2, 2},
{0, 1, 1, 1, 2, 0, 1, 2, 1, 1, 2, 2, 1, 2, 2},
{0, 2, 2, 0, 2, 0, 2, 0, 1, 1, 2, 2, 0, 0, 1}};
// Validate output.
for (int i = 0; i < expected_outputs.size(); i++) {
ASSERT_EQ(m.Invoke(), kTfLiteOk);
auto output = m.GetOutput();
EXPECT_EQ(output.size(), kNumBatches * kNumSamples);
EXPECT_EQ(expected_outputs[i], output);
}
}
TEST(MultinomialTest,
ValidateTFLiteOutputisTheSameAsTFOutput_MultiBatchMultiInvoke) {
const std::vector<float> kProb = {0.1f, 0.2f, 0.7f, 0.2f, 0.3f,
0.5f, 0.1f, 0.1f, 0.8f};
const std::initializer_list<float> kLogits = {
logf(0.1f), logf(0.2f), logf(0.7f), logf(0.2f), logf(0.3f),
logf(0.5f), logf(0.1f), logf(0.1f), logf(0.8f)};
const int kNumBatches = 3;
const int kNumClasses = 3;
const int kNumSamples = 10;
MultinomialOpModel m(/*input_type=*/InputType::kConst, kLogits, kNumBatches,
kNumClasses, kNumSamples, /*seed=*/1234, /*seed2=*/5678);
const std::vector<std::vector<int64_t>> expected_output = {
{2, 1, 2, 1, 2, 2, 2, 2, 2, 2, 2, 2, 0, 2, 2,
2, 2, 1, 1, 2, 2, 2, 1, 2, 2, 1, 2, 2, 2, 2},
{2, 2, 2, 0, 2, 1, 0, 0, 2, 0, 2, 0, 2, 1, 2,
2, 0, 0, 2, 2, 2, 2, 2, 2, 1, 2, 1, 1, 2, 2},
{2, 0, 0, 0, 1, 2, 1, 2, 0, 0, 2, 2, 2, 2, 0,
2, 1, 2, 2, 1, 2, 2, 1, 2, 2, 2, 1, 2, 2, 2}};
// Validate output.
for (int i = 0; i < 3; i++) {
ASSERT_EQ(m.Invoke(), kTfLiteOk);
auto output = m.GetOutput();
EXPECT_EQ(output.size(), kNumBatches * kNumSamples);
EXPECT_EQ(expected_output[i], output);
}
}
TEST(MultinomialTest, ValidateClassProbabilities) {
const std::vector<float> kProb = {0.1f, 0.9f, 0.2f, 0.8f, 0.3f,
0.7f, 0.4f, 0.6f, 0.5f, 0.5f};
const std::initializer_list<float> kLogits = {
logf(0.1f), logf(0.9f), logf(0.2f), logf(0.8f), logf(0.3f),
logf(0.7f), logf(0.4f), logf(0.6f), logf(0.5f), logf(0.5f)};
const int kNumBatches = 5;
const int kNumClasses = 2;
const int kNumSamples = 10000;
MultinomialOpModel m(/*input_type=*/InputType::kConst, kLogits, kNumBatches,
kNumClasses, kNumSamples, /*seed=*/1234, /*seed2=*/5678);
ASSERT_EQ(m.Invoke(), kTfLiteOk);
auto output = m.GetOutput();
EXPECT_EQ(output.size(), kNumBatches * kNumSamples);
int total_count = 0;
// Make sure they're all sampled with the roughly expected probability.
for (int i = 0; i < kNumBatches; i++) {
for (int j = 0; j < kNumClasses; j++) {
int idx = i * kNumClasses + j;
const int expected_count = static_cast<int>(kProb[idx] * kNumSamples);
const int allowed_misses = static_cast<int>(expected_count / 20); // 5%
int actual_count = std::count(output.begin() + i * kNumSamples,
output.begin() + (i + 1) * kNumSamples, j);
EXPECT_LE(abs(actual_count - expected_count), allowed_misses);
total_count += actual_count;
}
}
// Make sure only the expected classes are sampled.
EXPECT_EQ(total_count, kNumBatches * kNumSamples);
}
TEST(MultinomialTest, ValidatePreciseOutput) {
const std::initializer_list<float> kLogits = {1000.0f, 1001.0f};
const int kNumBatches = 1;
const int kNumClasses = 2;
const int kNumSamples = 1000;
MultinomialOpModel m(/*input_type=*/InputType::kConst, kLogits, kNumBatches,
kNumClasses, kNumSamples, /*seed=*/1234, /*seed2=*/5678);
ASSERT_EQ(m.Invoke(), kTfLiteOk);
auto output = m.GetOutput();
EXPECT_EQ(output.size(), kNumBatches * kNumSamples);
int c0 = std::count(output.begin(), output.end(), 0);
int c1 = std::count(output.begin(), output.end(), 1);
double p0 = static_cast<double>(c0) / (c0 + c1);
EXPECT_LT(std::abs(p0 - 0.26894142137), 0.01);
}
} // namespace
} // namespace tflite