openGauss-server/src/gausskernel/dbmind/db4ai/executor/kmeans/kmeans.cpp

2015 lines
91 KiB
C++

/**
Copyright (c) 2021 Huawei Technologies Co.,Ltd.
openGauss is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.
---------------------------------------------------------------------------------------
kmeans.cpp
k-means operations
IDENTIFICATION
src/gausskernel/dbmind/db4ai/executor/kmeans/kmeans.cpp
---------------------------------------------------------------------------------------
* */
#include <random>
#include "nodes/execnodes.h"
#include "nodes/pg_list.h"
#include "postgres_ext.h"
#include "db4ai/kmeans.h"
#include "db4ai/aifuncs.h"
#include "db4ai/fp_ops.h"
#include "db4ai/distance_functions.h"
#include "db4ai/model_warehouse.h"
#include "db4ai/predict_by.h"
#include "db4ai/db4ai_cpu.h"
#include "db4ai/db4ai_common.h"
/*
* parameters that affect k-means (hyper-parameters)
*/
typedef struct HyperparametersKMeans {
ModelHyperparameters mhp; // place-holder
SeedingFunction seeding = KMEANS_RANDOM_SEED;
DistanceFunction distance = KMEANS_L2_SQUARED;
Verbosity verbosity = NO_OUTPUT;
uint32_t num_centroids = 1U;
uint32_t num_iterations = 10U;
uint32_t n_features = 0U;
uint32_t batch_size = 1000U;
uint64_t external_seed = 0ULL;
double tolerance = 0.00001;
} HyperparametersKMeans;
/*
* optimized for prediction
*/
typedef struct PCentroid {
double objective_function = DBL_MAX;
double avg_distance = DBL_MAX;
double min_distance = DBL_MAX;
double max_distance = DBL_MAX;
double std_dev_distance = DBL_MAX;
uint64_t cluster_size = 0ULL;
uint32_t id = 0U;
double *coordinates = nullptr;
} PCentroid;
typedef struct ModelKMeansV01 {
/*
* the following fields are put here for convenience
* (currently used for prediction, and explain)
*/
uint32_t actual_num_centroids = 0U;
uint32_t num_actual_iterations = 0U;
uint32_t dimension = 0U;
uint32_t distance_function_id = 0U;
uint32_t coordinates_offset = 0U;
uint32_t padding = 0U;
PCentroid *centroids = nullptr;
} ModelKMeansV01;
/*
* current state of the k-means algorithm
*/
typedef struct KMeansStateDescription {
Centroid *centroids[2] = {nullptr};
ArrayType *bbox_min = nullptr;
ArrayType *bbox_max = nullptr;
GSPoint *batch = nullptr;
double (*distance)(double const *, double const *, uint32_t const dimension) = nullptr;
double execution_time = 0.;
double seeding_time = 0.;
IncrementalStatistics solution_statistics[2];
uint64_t num_good_points = 0UL;
uint64_t num_dead_points = 0UL;
uint32_t current_iteration = 0U;
uint32_t current_centroid = 0U;
uint32_t dimension = 0U;
uint32_t num_centroids = 0U;
uint32_t actual_num_iterations = 0U;
uint32_t size_centroids_bytes = 0U;
bool initialized = false;
bool late_initialized = false;
} KMeansStateDescription;
typedef struct KMeansState {
TrainModelState tms;
KMeansStateDescription description;
bool done = false;
} KMeansState;
/*
* kmeans|| does at most 10 iterations
*/
uint32_t constexpr NUM_ITERATIONS_KMEANSBB = 10U;
/*
* internally, the operator works in stages, this enum identifies each one of them
*/
typedef enum : uint32_t {
KMEANS_INIT = 0,
KMEANS_INITIAL_CENTROIDS_RANDOM_SAMPLE,
KMEANS_INITIAL_CENTROIDS_BB_SAMPLE,
KMEANS_INITIAL_CENTROIDS_BB_COMPUTE_COST,
KMEANS_LLOYD
} AlgoStage;
/*
* this initializes all relevant fields once the dimension of the points (as specified
* by the user or as obtained from the very first tuple read) is known
*/
force_inline static void initialize_fields(KMeansStateDescription *kmeans_state_description,
uint32_t const num_centroids, uint32_t const dimension)
{
auto datums_tmp = reinterpret_cast<Datum *>(palloc0(sizeof(Datum) * dimension));
for (uint32_t c = 0; c < num_centroids; ++c) {
kmeans_state_description->centroids[0][c].id = c + 1;
kmeans_state_description->centroids[1][c].id = c + 1;
/*
* this is an internal array that will eventually hold the coordinates of a centroid
* its representation as a PG Array is legacy. It will be better to represent it as
* an array of double directly (in a manner that can be returned to the client directly)
*/
kmeans_state_description->centroids[0][c].coordinates =
construct_array(datums_tmp, dimension, FLOAT8OID, sizeof(float8), FLOAT8PASSBYVAL, 'd');
kmeans_state_description->centroids[1][c].coordinates =
construct_array(datums_tmp, dimension, FLOAT8OID, sizeof(float8), FLOAT8PASSBYVAL, 'd');
}
/*
* general running time information of the operator
*/
kmeans_state_description->bbox_max =
construct_array(datums_tmp, dimension, FLOAT8OID, sizeof(float8), FLOAT8PASSBYVAL, 'd');
kmeans_state_description->bbox_min =
construct_array(datums_tmp, dimension, FLOAT8OID, sizeof(float8), FLOAT8PASSBYVAL, 'd');
kmeans_state_description->num_good_points = 0;
kmeans_state_description->num_dead_points = 0;
kmeans_state_description->current_iteration = 0;
kmeans_state_description->current_centroid = 0;
kmeans_state_description->dimension = dimension;
kmeans_state_description->initialized = true;
kmeans_state_description->size_centroids_bytes = sizeof(float8) * dimension;
pfree(datums_tmp);
}
/*
* to verify that the pg array we get complies with what we expect
* we discard entries that do not pass the test because we cannot start
* much (consistent) with them anyway
*/
static bool verify_pgarray(ArrayType const *pg_array, int32_t n)
{
/*
* We expect the input to be an n-element array of doubles; verify that. We
* don't need to use deconstruct_array() since the array data is just
* going to look like a C array of n double values.
*/
if (unlikely((ARR_NDIM(pg_array) != 1) || (ARR_DIMS(pg_array)[0] != n) || ARR_HASNULL(pg_array) ||
(ARR_ELEMTYPE(pg_array) != FLOAT8OID)))
return false;
return true;
}
/*
* this copies the coordinates found inside a slot onto an array we own
*/
static bool copy_slot_coordinates_to_array(GSPoint *coordinates, ModelTuple const *slot, uint32_t const dimension)
{
if (unlikely((slot == nullptr) or (coordinates == nullptr)))
return false;
/*
* we obtain the coordinates of the current point (function call incurs in detoasting
* and thus memory is allocated and we have to free it once we don't need it
*/
ArrayType *current_point_pgarray = DatumGetArrayTypePCopy(slot->values[0]);
bool const valid_point = verify_pgarray(current_point_pgarray, dimension);
bool release_point = PointerGetDatum(current_point_pgarray) != slot->values[0];
/*
* if the point is not valid and it was originally toasted, then we release the copy
*/
if (unlikely(!valid_point && release_point)) {
pfree(current_point_pgarray);
release_point = false;
}
coordinates->pg_coordinates = current_point_pgarray;
coordinates->should_free = release_point;
return valid_point;
}
/*
* given a set of centroids (as a PG list) and a point, this function compute the distance to the closest
* centroid
*/
static bool closest_centroid(List const *centroids, GSPoint const *point, uint32_t const dimension, double *distance)
{
ListCell const *current_centroid_cell = centroids ? centroids->head : nullptr;
GSPoint *centroid = nullptr;
GSPoint *closest_centroid_ptr = nullptr;
bool result = false;
bool min_distance_changed = false;
double local_distance = 0.;
auto min_distance = DBL_MAX;
auto const *point_coordinates = reinterpret_cast<double const *>(ARR_DATA_PTR(point->pg_coordinates));
for (; current_centroid_cell != nullptr; current_centroid_cell = lnext(current_centroid_cell)) {
/*
* low temporal locality for a prefetch for read
*/
prefetch(lnext(current_centroid_cell), 0, 1);
centroid = reinterpret_cast<GSPoint *>(lfirst(current_centroid_cell));
local_distance = l2_squared(point_coordinates,
reinterpret_cast<double const *>(ARR_DATA_PTR(centroid->pg_coordinates)),
dimension);
min_distance_changed = local_distance < min_distance;
min_distance = min_distance_changed ? local_distance : min_distance;
closest_centroid_ptr = min_distance_changed ? centroid : closest_centroid_ptr;
}
if (closest_centroid_ptr) {
result = true;
++closest_centroid_ptr->weight;
}
if (likely(distance != nullptr))
*distance = min_distance;
return result;
}
static bool deal_sample(bool const sample, std::mt19937_64 *prng, GSPoint *batch, uint32_t current_slot,
ModelTuple const *scan_slot, uint32_t const dimension, bool first_candidate,
List *centroid_candidates, double local_sample_probability, AlgoStage stage)
{
double coin = 0.;
bool result = false;
bool distance_computed = false;
double distance_to_centroid = 0.;
double op_error = 0.;
std::uniform_real_distribution<double> unit_sampler(0., 1.);
/*
* toss the coin to see if we have to
*/
if (sample) {
if (unlikely(prng == nullptr))
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("k-means exec: prng must be set (non-null)")));
coin = unit_sampler(*prng);
/*
* random++ and kmeans|| sample with different probabilities
* (the latter is much more complicated)
*
* when sampling we will pay the price of unpacking every single
* tuple (which has to be done for kmeans|| anyway) for code
* simplicity
*/
result = copy_slot_coordinates_to_array(batch + current_slot, scan_slot, dimension);
if (result && (stage == KMEANS_INITIAL_CENTROIDS_BB_SAMPLE) && !first_candidate) {
distance_computed = closest_centroid(centroid_candidates, batch + current_slot,
dimension, &distance_to_centroid);
twoMult(local_sample_probability, distance_computed ? distance_to_centroid : 1.,
&local_sample_probability, &op_error);
local_sample_probability += op_error;
}
/*
* if the data point is not valid or did not pass the test
* we release the memory it occupies and ask for the next data point
*/
if (!result || coin >= local_sample_probability) {
if (likely(batch[current_slot].should_free)) {
pfree(batch[current_slot].pg_coordinates);
batch[current_slot].pg_coordinates = nullptr;
batch[current_slot].should_free = false;
}
return true;
}
}
return false;
}
/*
* given a set of centroids (as a PG list) and a set of points, this function computes
* the cost of the set of centroids as well as their weights (number of points assigned
* to each centroid)
* we assumed that all weights have been reset already
*/
static bool compute_cost_and_weights(List const *centroids, GSPoint const *points, uint32_t dimension,
uint32_t const num_slots, double *cost)
{
GSPoint const *point = nullptr;
double local_distance = 0.;
double cost_of_batch = 0.;
double local_op_error = 0.;
double total_op_error = 0.;
uint32_t current_slot = 0U;
bool const result = current_slot < num_slots;
// for every point we compute the closest centroid and increase the weight of the corresponding centroid
while (current_slot < num_slots) {
point = points + current_slot;
closest_centroid(centroids, point, dimension, &local_distance);
twoSum(cost_of_batch, local_distance, &cost_of_batch, &local_op_error);
total_op_error += local_op_error;
++current_slot;
}
cost_of_batch += total_op_error;
*cost = cost_of_batch;
return result;
}
/*
* a batch is used only once and thus the data it points to is
* released after it has been used
*/
force_inline static void release_batch(GSPoint *batch, uint32_t const num_slots)
{
GSPoint *point = nullptr;
for (uint32_t current_slot = 0; current_slot < num_slots; ++current_slot) {
point = batch + current_slot;
if (likely(point->should_free)) {
pfree(point->pg_coordinates);
point->pg_coordinates = nullptr;
point->should_free = false;
}
}
}
/*
* given the running mean of a centroid and a new point, this adds the new point to the aggregate
* using a sum that provides higher precision (we could provide much higher precision at the cost
* of allocating yet another array to keep correction terms for every dimension
*/
force_inline static void aggregate_point(double *centroid_aggregation, double const *new_point,
uint32_t const dimension)
{
double local_correction = 0;
for (uint32_t d = 0; d < dimension; ++d) {
twoSum(centroid_aggregation[d], new_point[d], centroid_aggregation + d, &local_correction);
centroid_aggregation[d] += local_correction;
}
}
/*
* we assume that all slots in the batch are non-null (guaranteed by the upper call)
* also, that the next set of centroids has been reset previous to the very first call
*/
static void update_centroids(KMeansStateDescription *description, GSPoint *slots, uint32_t const num_slots,
uint32_t const idx_current_centroids, uint32_t const idx_next_centroids)
{
uint32_t const dimension = description->dimension;
uint32_t current_slot = 0U;
uint32_t current_centroid = 0U;
uint32_t const num_centroids = description->num_centroids;
uint32_t closest_centroid = 0U;
GSPoint const *current_point = nullptr;
double const *current_point_coordinates = nullptr;
double *current_centroid_coordinates = nullptr;
double *next_centroid_coordinates = nullptr;
double dist = 0.;
auto min_dist = DBL_MAX;
Centroid *current_centroids = description->centroids[idx_current_centroids];
Centroid *next_centroids = description->centroids[idx_next_centroids];
Centroid *closest_centroid_ptr = nullptr;
Centroid *closest_centroid_next_ptr = nullptr;
bool min_dist_change = false;
IncrementalStatistics local_statistics;
/*
* just in case, but this should not happen as we control the parent call
*/
if (unlikely(num_slots == 0))
return;
do {
current_centroid = 0U;
min_dist = DBL_MAX;
/*
* we obtain the coordinates of the current point
*/
current_point = slots + current_slot;
/*
* this loops obtains the distance of the current point to all centroids and keeps the closest one
*/
while (likely(current_centroid < num_centroids)) {
current_centroid_coordinates =
reinterpret_cast<double *>(ARR_DATA_PTR(current_centroids[current_centroid].coordinates));
current_point_coordinates = reinterpret_cast<double *>(ARR_DATA_PTR(current_point->pg_coordinates));
dist = description->distance(current_point_coordinates, current_centroid_coordinates, dimension);
min_dist_change = dist < min_dist;
closest_centroid = min_dist_change ? current_centroid : closest_centroid;
min_dist = min_dist_change ? dist : min_dist;
++current_centroid;
}
/*
* once the closest centroid has been detected we proceed with the aggregation and update
* of statistics
*/
local_statistics.setTotal(min_dist);
closest_centroid_ptr = current_centroids + closest_centroid;
closest_centroid_next_ptr = next_centroids + closest_centroid;
closest_centroid_ptr->statistics += local_statistics;
/*
* for the next iteration (if there is any) we have to obtain a new centroid, which will be
* the average of the points that we aggregate here
*/
next_centroid_coordinates = reinterpret_cast<double *>(ARR_DATA_PTR(closest_centroid_next_ptr->coordinates));
aggregate_point(next_centroid_coordinates, current_point_coordinates, dimension);
} while (likely(++current_slot < num_slots));
}
/*
* updates the minimum bounding box to contain the new given point
*/
force_inline static void update_bbox(double *const bbox_min, double *const bbox_max, double const *point,
uint32_t const dimension)
{
uint32_t current_dimension = 0;
double min = 0.;
double max = 0.;
double p = 0.;
while (current_dimension < dimension) {
min = bbox_min[current_dimension];
max = bbox_max[current_dimension];
p = point[current_dimension];
/*
* we do cmovs instead of ifs (we could spare a comparison from time to time
* by using ifs, but we increase branch misprediction as well. thus we settle
* for the branchless option (more expensive than a hit, but cheaper than a miss)
*/
bbox_min[current_dimension] = p < min ? p : min;
bbox_max[current_dimension] = p > max ? p : max;
++current_dimension;
}
}
static bool init_kmeans(KMeansStateDescription *description, double *bbox_min, double *bbox_max, GSPoint const *batch,
uint32_t const num_slots)
{
uint32_t const dimension = description->dimension;
uint32_t const size_centroid_bytes = description->size_centroids_bytes;
bool const first_run = description->current_iteration == 0;
uint32_t current_slot = 0;
GSPoint const *current_point = nullptr;
double const *current_point_coordinates = nullptr;
if (unlikely(num_slots == 0))
return false;
/*
* the very first slot of the batch is a bit special. observe that we have got here
* after the point has passed validity checks and thus it is safe to access its information
* directly
*/
current_point = batch + current_slot;
current_point_coordinates = reinterpret_cast<double const *>(ARR_DATA_PTR(current_point->pg_coordinates));
/*
* in the very first run we set the coordinates of the bounding box as the ones
* of the very first point (we improve from there)
*/
if (unlikely(first_run)) {
/*
* no need to memset to zero because in the very first run they are freshly allocated
* with palloc0
*/
errno_t rc = memcpy_s(bbox_min, size_centroid_bytes, current_point_coordinates, size_centroid_bytes);
securec_check(rc, "\0", "\0");
rc = memcpy_s(bbox_max, size_centroid_bytes, current_point_coordinates, size_centroid_bytes);
securec_check(rc, "\0", "\0");
description->current_iteration = 1;
} else {
update_bbox(bbox_min, bbox_max, current_point_coordinates, dimension);
}
++description->num_good_points;
/*
* let's consider the rest of the batch
*/
while (likely(++current_slot < num_slots)) {
current_point = batch + current_slot;
current_point_coordinates = reinterpret_cast<double const *>(ARR_DATA_PTR(current_point->pg_coordinates));
update_bbox(bbox_min, bbox_max, current_point_coordinates, dimension);
++description->num_good_points;
}
/*
* done with the batch
*/
return true;
}
/*
* this is the work horse of the whole algorithm. in here we do a lot of things depending on the stage
* of the algorithm. this function does a complete (single) pass over the data. the upper layer
* calls this function multiple times depending the stage of the algorithm
*/
static List *one_data_pass(TrainModelState *pstate, KMeansStateDescription *state_description,
uint32_t const batch_size, uint32_t const idx_current_centroids,
uint32_t const idx_next_centroids, bool const sample, double const sample_probability,
AlgoStage stage, List *centroid_candidates, double *cost_centroid_candidates,
std::mt19937_64 *prng)
{
bool plan_exhausted = false;
bool result = false;
bool first_candidate = centroid_candidates == nullptr;
bool initialized = state_description->initialized;
uint32_t current_slot = 0;
uint32_t dimension = state_description->dimension;
uint32_t const num_centroids = state_description->num_centroids;
uint32_t num_elements_round = 0;
uint32_t slot_number = 0;
uint32_t valid_row = 0;
ModelTuple const *scan_slot = nullptr;
double *bbox_min = nullptr;
double *bbox_max = nullptr;
double local_sample_probability = sample_probability;
double cost_of_batch = 0.;
GSPoint *centroid_candidate = nullptr;
GSPoint *batch = state_description->batch;
while (!plan_exhausted) {
current_slot = 0;
/* we produce a batch of slots to be passed to the algorithm */
while (current_slot < batch_size) {
scan_slot = pstate->fetch(pstate->callback_data, &pstate->tuple)
? &pstate->tuple : nullptr;
++slot_number;
// every slot will have its own chances
local_sample_probability = sample_probability;
/*
* we get out of the whole thing if we have exhausted the relation or
* we have found our k centroids.
* if we were not able to sample the k centroids, the upper call
* will perform runs until we have done so
*/
if (unlikely(!scan_slot || state_description->current_centroid == num_centroids)) {
plan_exhausted = true;
break;
}
/*
* we jump over rows with empty coordinates
*/
if (unlikely(scan_slot->isnull[0])) {
continue;
}
/*
* if we have not initialized the node state because the user didn't provide the dimension, we set it
* here and initialized with what we found. this is very optimistic because if the very first row
* is not "correct", then all rows not matching this dimension will be ignored.
*
* observe that this branch will be executed at most once and after it all fields are allocated
* and "properly" initialized
*/
if (unlikely(stage == KMEANS_INIT && !initialized)) {
ArrayType *first_poing_pgarray = DatumGetArrayTypeP(scan_slot->values[0]);
dimension = ARR_DIMS(first_poing_pgarray)[0];
initialize_fields(state_description, num_centroids, dimension);
if (unlikely(PointerGetDatum(first_poing_pgarray) != scan_slot->values[0]))
pfree(first_poing_pgarray);
batch = state_description->batch;
initialized = true;
}
if (deal_sample(sample, prng, batch, current_slot, scan_slot, dimension, first_candidate,
centroid_candidates, local_sample_probability, stage))
continue;
if ((stage == KMEANS_INITIAL_CENTROIDS_RANDOM_SAMPLE) || (stage == KMEANS_INITIAL_CENTROIDS_BB_SAMPLE)) {
/*
* we only know the expected number of centroid candidates that we will produce
* but not the exact number. thus we allocate each one of them on demand
* (observe that we cannot use the batch structure because the number of
* candidates we generate can be much larger than the size of a batch)
*/
centroid_candidate = reinterpret_cast<GSPoint *>(palloc0(sizeof(GSPoint)));
/*
* observe that the scan_slot was already copied above (when sampling) and thus
* we just move the memory reference from the batch slot to the newly allocated
* GSPoint
*/
*centroid_candidate = batch[current_slot];
centroid_candidate->distance_to_closest_centroid = DBL_MAX;
centroid_candidate->id = slot_number;
batch[current_slot].id = 0;
batch[current_slot].distance_to_closest_centroid = 0.;
batch[current_slot].weight = 0.;
batch[current_slot].pg_coordinates = nullptr;
batch[current_slot].should_free = false;
/*
* this stores the reference to the current selected candidate and thus we
* can forget about it
*/
centroid_candidates = lappend(centroid_candidates, centroid_candidate);
/*
* memory should be allocated in the next iteration
*/
centroid_candidate = nullptr;
++num_elements_round;
/*
* for kmeans|| we produce a single candidate the very first time.
* for random++ we produce a number of candidates in a single pass
* (thus we consume the whole relation since we do not update current_slot)
*/
if (unlikely(first_candidate && (stage == KMEANS_INITIAL_CENTROIDS_BB_SAMPLE))) {
plan_exhausted = true;
break;
} else if (unlikely(first_candidate)) {
first_candidate = false;
}
} else {
/*
* the element's coordinates are copied to be processed
*/
result = copy_slot_coordinates_to_array(batch + current_slot, scan_slot, dimension);
batch[current_slot].id = slot_number;
valid_row = result ? 1U : 0U;
current_slot += valid_row;
state_description->num_dead_points += 1 - valid_row;
}
}
/* we process the batch
* each stage happens in a batch and thus branch misprediction should not be a problem
* also, except for KMEANS_LLOYD, the other two stages require exactly one data pass
*/
switch (stage) {
case KMEANS_INIT:
/*
* this run is to obtain initial statistics about the data (like the number of valid tuples)
* and the coordinates of the bounding box
*/
bbox_min = reinterpret_cast<double *>(state_description->bbox_min);
bbox_max = reinterpret_cast<double *>(state_description->bbox_max);
init_kmeans(state_description, bbox_min, bbox_max, batch, current_slot);
/*
* we are done with the batch and thus we release the allocated memory (corresponding
* to the points of the batch)
*/
release_batch(batch, current_slot);
break;
case KMEANS_INITIAL_CENTROIDS_RANDOM_SAMPLE:
case KMEANS_INITIAL_CENTROIDS_BB_SAMPLE:
/*
* when sampling for random++ and kmeans|| we do no computations other than the sample
* the upper call will run kmeans++ after the candidates have been sampled
*/
break;
case KMEANS_INITIAL_CENTROIDS_BB_COMPUTE_COST:
/*
* when computing the cost of a solution, we do it in a batched manner as the other
* non-sampling cases
*/
compute_cost_and_weights(centroid_candidates, batch, dimension, current_slot, &cost_of_batch);
if (unlikely(cost_centroid_candidates == nullptr))
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("k-means exec: cost variable must be non-null")));
*cost_centroid_candidates += cost_of_batch;
/*
* we are done with the batch and thus we release the allocated memory (corresponding
* to the points of the batch)
*/
release_batch(batch, current_slot);
break;
case KMEANS_LLOYD:
/*
* let's find out which centroid is the closest and aggregate the corresponding statistics
*/
update_centroids(state_description, batch, current_slot, idx_current_centroids, idx_next_centroids);
/*
* we are done with the batch and thus we release the allocated memory (corresponding
* to the points of the batch)
*/
release_batch(batch, current_slot);
break;
default:
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("k-means exec: no known algorithm stage")));
}
}
return centroid_candidates;
}
/*
* this sets the weights of a set of candidates to 1 (every point is the centroid of itself)
*/
void reset_weights(List const *centroids)
{
ListCell const *current_centroid_cell = centroids ? centroids->head : nullptr;
GSPoint *centroid = nullptr;
for (; current_centroid_cell != nullptr; current_centroid_cell = lnext(current_centroid_cell)) {
centroid = reinterpret_cast<GSPoint *>(lfirst(current_centroid_cell));
centroid->weight = 1U;
}
}
/*
* this runs kmeans++ on a super-set of centroids to obtain the k centroids we want as seeding
*/
List *kmeanspp(KMeansStateDescription *description, List *centroids_candidates, uint32_t const idx_current_centroids,
std::mt19937_64 *prng)
{
Centroid *centroids = description->centroids[idx_current_centroids];
uint32_t const num_centroids_needed = description->num_centroids;
uint32_t const size_centroid_bytes = description->size_centroids_bytes;
uint32_t num_candidates = centroids_candidates ? centroids_candidates->length : 0;
uint32_t const dimension = description->dimension;
ListCell *current_candidate_cell = nullptr;
ListCell *prev_candidate_cell = nullptr;
ListCell *tmp_cell = nullptr;
std::uniform_real_distribution<double> unit_sampler(0., 1.);
// if there are less candidates than centroids needed, then all of them become centroids
double sample_probability = num_candidates <= num_centroids_needed ? 1. : 1. / static_cast<double>(num_candidates);
double sample_probability_correction = 0.;
double candidate_probability = 0.;
double distance = 0.;
double sum_distances = 0.;
double sum_distances_local_correction = 0.;
double sum_distances_correction = 0.;
ArrayType *current_candidate_pgarray = nullptr;
ArrayType *current_centroid_pgarray = nullptr;
GSPoint *current_candidate = nullptr;
GSPoint *tmp_candidate = nullptr;
uint32_t current_centroid_idx = description->current_centroid;
uint32_t tries_until_next_centroid = 0;
bool no_more_candidates = false;
/*
* we expect to produce all centroids in one go and to be able to produce them because
* we have enough candidates
*/
if ((current_centroid_idx > 0) || (num_centroids_needed == 0))
ereport(ERROR,
(errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("k-means: not able to run k-means++")));
/*
* we produce the very first centroid uniformly at random (not weighted)
* the rest of the centroids are found by sampling w.r.t. the (weighted) distance to their closest centroid
* (elements that are far from their centroids are more likely to become centroids themselves,
* but heavier points also have better chances)
*/
while (likely((current_centroid_idx < num_centroids_needed) && !no_more_candidates)) {
++tries_until_next_centroid;
/*
* the probability of no choosing a centroid in a round is > 0
* so we loop until we have found all our k centroids
* in each round one centroid will be found at most
*/
prev_candidate_cell = nullptr;
no_more_candidates = true;
current_candidate_cell = centroids_candidates ? centroids_candidates->head : nullptr;
for (; current_candidate_cell != nullptr; current_candidate_cell = lnext(current_candidate_cell)) {
current_candidate = reinterpret_cast<GSPoint *>(lfirst(current_candidate_cell));
current_candidate_pgarray = current_candidate->pg_coordinates;
no_more_candidates = false;
candidate_probability = unit_sampler(*prng);
/*
* the very first candidate will be sampled uniformly, the rest will be sampled
* w.r.t. the distance to their closest centroid (the farther the more probable)
*/
if (likely(current_centroid_idx > 0)) {
// this distance is already weighted and thus requires no weighting again
distance = current_candidate->distance_to_closest_centroid;
twoDiv(distance, sum_distances, &sample_probability, &sample_probability_correction);
sample_probability += sample_probability_correction;
}
/*
* this is weighted sampling (taking into consideration the weight of the point)
*/
if (candidate_probability >= sample_probability) {
prev_candidate_cell = current_candidate_cell;
continue;
}
/*
* this candidate becomes a centroid
*/
current_centroid_pgarray = centroids[current_centroid_idx].coordinates;
/*
* we copy the coordinates of the centroid to the official set of centroids
*/
if (unlikely((current_centroid_pgarray != nullptr) and (current_candidate_pgarray != nullptr))) {
auto point_coordinates_to = reinterpret_cast<double *>(ARR_DATA_PTR(current_centroid_pgarray));
auto point_coordinates_from = reinterpret_cast<double *>(ARR_DATA_PTR(current_candidate_pgarray));
memset_s(point_coordinates_to, size_centroid_bytes, 0, size_centroid_bytes);
errno_t rc = memcpy_s(point_coordinates_to,
size_centroid_bytes, point_coordinates_from, size_centroid_bytes);
securec_check(rc, "\0", "\0");
}
/*
* we delete the element that just became centroid from the list of candidates (along all its information)
*/
centroids_candidates = list_delete_cell(centroids_candidates, current_candidate_cell, prev_candidate_cell);
pfree(current_candidate->pg_coordinates);
current_candidate_cell =
prev_candidate_cell ? prev_candidate_cell : centroids_candidates ? centroids_candidates->head
: nullptr;
/*
* we can reset sum_distances because it will be overwritten below anyway
*/
sum_distances = 0.;
/*
* we update the distance to the closest centroid depending on the presence of the new
* centroid
*/
tmp_cell = centroids_candidates ? centroids_candidates->head : nullptr;
for (; tmp_cell != nullptr; tmp_cell = lnext(tmp_cell)) {
tmp_candidate = reinterpret_cast<GSPoint *>(lfirst(tmp_cell));
distance = l2_squared(reinterpret_cast<double *>(ARR_DATA_PTR(tmp_candidate->pg_coordinates)),
reinterpret_cast<double *>(ARR_DATA_PTR(current_centroid_pgarray)), dimension);
/*
* medium temporal locality for a prefetch for write
*/
prefetch(lnext(tmp_cell), 1, 2);
/*
* since we are dealing with weighted points, the overall sum of distances has
* to consider the weight of each point
*/
twoMult(distance, tmp_candidate->weight, &distance, &sum_distances_correction);
distance += sum_distances_correction;
/*
* we store the weighted distance at the point to save the multiplication later on
* when sampling
* only if the new centroid becomes the closest one to a candidate we update
*/
if ((current_centroid_idx == 0) || (distance < tmp_candidate->distance_to_closest_centroid))
tmp_candidate->distance_to_closest_centroid = distance;
/*
* every distance appears as many times as the weight of the point
*/
twoSum(sum_distances, tmp_candidate->distance_to_closest_centroid, &sum_distances,
&sum_distances_local_correction);
sum_distances_correction += sum_distances_local_correction;
}
/*
* high temporal locality for a prefetch for read
*/
if (likely(current_candidate_cell != nullptr))
prefetch(lnext(current_candidate_cell), 0, 3);
sum_distances += sum_distances_correction;
++current_centroid_idx;
tries_until_next_centroid = 0;
break;
}
}
/*
* we get rid of the linked list of candidates in case some candidates are left
*/
while (centroids_candidates) {
current_candidate_cell = centroids_candidates->head;
current_candidate = reinterpret_cast<GSPoint *>(lfirst(current_candidate_cell));
/*
* low temporal locality for a prefetch for read
*/
prefetch(lnext(current_candidate_cell), 0, 1);
/*
* this frees the list cell
*/
centroids_candidates = list_delete_cell(centroids_candidates, current_candidate_cell, nullptr);
/*
* this frees the PG array inside the GSPoint
*/
pfree(current_candidate->pg_coordinates);
/*
* and finally, this free ths GSPoint
*/
pfree(current_candidate);
}
description->current_centroid = current_centroid_idx;
return centroids_candidates;
}
/*
* this aggregates the value of the function over all centroids
*/
void compute_cost(KMeansStateDescription *description, uint32_t const idx_current_centroids)
{
Centroid *centroids = description->centroids[idx_current_centroids];
uint32_t const num_centroids = description->num_centroids;
IncrementalStatistics *output_statistics = description->solution_statistics + idx_current_centroids;
output_statistics->reset();
for (uint32_t c = 0; c < num_centroids; ++c)
*output_statistics += centroids[c].statistics;
}
/*
* every iteration there is a set of centroids (the next one) that is reset
*/
void reset_centroids(KMeansStateDescription *description, uint32_t const idx_centroids)
{
uint32_t const num_centroids = description->num_centroids;
uint32_t const size_centroid_bytes = description->size_centroids_bytes;
Centroid *centroids = description->centroids[idx_centroids];
Centroid *centroid = nullptr;
double *centroid_coordinates = nullptr;
for (uint32_t c = 0; c < num_centroids; ++c) {
centroid = centroids + c;
centroid->statistics.reset();
centroid_coordinates = reinterpret_cast<double *>(ARR_DATA_PTR(centroid->coordinates));
memset_s(centroid_coordinates, size_centroid_bytes, 0, size_centroid_bytes);
}
}
/*
* this produces the centroid by dividing the aggregate by the amount of points it got assigned
* we assumed that population > 0
*/
force_inline void finish_centroid(double *centroid_aggregation, uint32_t const dimension, double const population)
{
double local_correction = 0.;
for (uint32_t d = 0; d < dimension; ++d) {
twoDiv(centroid_aggregation[d], population, centroid_aggregation + d, &local_correction);
centroid_aggregation[d] += local_correction;
}
}
void merge_centroids(KMeansStateDescription *description, uint32_t const idx_current_centroids,
uint32_t const idx_next_centroids)
{
uint32_t const num_centroids = description->num_centroids;
uint32_t const size_centroid_bytes = description->size_centroids_bytes;
uint32_t const dimension = description->dimension;
Centroid *const current_centroids = description->centroids[idx_current_centroids];
Centroid *const next_centroids = description->centroids[idx_next_centroids];
Centroid *current_centroid = nullptr;
Centroid *next_centroid = nullptr;
double *current_centroid_coordinates = nullptr;
double *next_centroid_coordinates = nullptr;
for (uint32_t c = 0; c < num_centroids; ++c) {
next_centroid = next_centroids + c;
current_centroid = current_centroids + c;
next_centroid_coordinates = reinterpret_cast<double *>(ARR_DATA_PTR(next_centroid->coordinates));
/*
* if the cluster of a centroid is empty we copy it from the previous iteration verbatim
* to not loose it
*/
if (unlikely(current_centroid->statistics.getPopulation() == 0)) {
current_centroid_coordinates = reinterpret_cast<double *>(ARR_DATA_PTR(current_centroid->coordinates));
/*
* observe that we do not have to memset to zero because current_centroid was reset before the run
* and since no point was assigned to it, it has remained reset
*/
error_t rc = memcpy_s(next_centroid_coordinates, size_centroid_bytes, current_centroid_coordinates,
size_centroid_bytes);
securec_check(rc, "\0", "\0");
} else {
finish_centroid(next_centroid_coordinates, dimension, current_centroid->statistics.getPopulation());
}
}
}
/*
* new API follows
* functions are defined in the order they are required during execution
*/
HyperparameterDefinition kmeans_hyperparameter_definitions[] = {
HYPERPARAMETER_ENUM("seeding_function", "Random++", kmeans_seeding_str, kmeans_seeding_str_size,
kmeans_seeding_getter, kmeans_seeding_setter, HyperparametersKMeans, seeding, HP_NO_AUTOML()),
HYPERPARAMETER_ENUM("distance_function", "L2_Squared", kmeans_distance_functions_str,
kmeans_distance_functions_str_size, kmeans_distance_function_getter,
kmeans_distance_function_setter, HyperparametersKMeans, distance, HP_NO_AUTOML()),
HYPERPARAMETER_INT4("verbose", 0, 0, true, 2, true, HyperparametersKMeans, verbosity, HP_NO_AUTOML()),
HYPERPARAMETER_INT4("num_centroids", 1, 1, true, 1000000, true, HyperparametersKMeans, num_centroids,
HP_NO_AUTOML()),
HYPERPARAMETER_INT4("max_iterations", 10, 1, true, ITER_MAX, true, HyperparametersKMeans, num_iterations,
HP_NO_AUTOML()),
HYPERPARAMETER_INT4("num_features", 0, 1, true, INT32_MAX, true, HyperparametersKMeans, n_features, HP_NO_AUTOML()),
HYPERPARAMETER_INT4("batch_size", 1000, 1, true, MAX_BATCH_SIZE, true, HyperparametersKMeans, batch_size,
HP_NO_AUTOML()),
HYPERPARAMETER_INT4("seed", 0, 0, true, INT32_MAX, true, HyperparametersKMeans, external_seed,
HP_AUTOML_INT(1, INT32_MAX, 1, ProbabilityDistribution::UNIFORM_RANGE)),
HYPERPARAMETER_FLOAT8("tolerance", 0.00001, 0.0, false, 1.0, true, HyperparametersKMeans, tolerance,
HP_NO_AUTOML()),
};
force_inline static ModelHyperparameters *kmeans_make_hyperparameters(AlgorithmAPI *self)
{
auto kmeans_hyperp = reinterpret_cast<HyperparametersKMeans *>(palloc0(sizeof(HyperparametersKMeans)));
return &kmeans_hyperp->mhp;
}
force_inline static HyperparameterDefinition const *kmeans_get_hyperparameters(AlgorithmAPI *self,
int32_t *definitions_size)
{
Assert(definitions_size != nullptr);
*definitions_size = sizeof(kmeans_hyperparameter_definitions) / sizeof(HyperparameterDefinition);
return kmeans_hyperparameter_definitions;
}
force_inline static void kmeans_update_hyperparameters(AlgorithmAPI *self, ModelHyperparameters *hyperp)
{
auto kmeans_hyperp = reinterpret_cast<HyperparametersKMeans *>(hyperp);
/* if the user-provided seed is 0 we take the current time but reset
* the higher order bits to be able to return this seed to the user
* as an int32_t so that the user can reproduce the run
* (observe that epoch 2^31 is around year 2038 and the shifts are
* until then useless)
*/
if (kmeans_hyperp->external_seed == 0ULL)
kmeans_hyperp->external_seed = (get_time_ms() << 33U) >> 33U;
}
/*
* this function initializes the algorithm
*/
static TrainModelState *kmeans_create(AlgorithmAPI *self, const TrainModel *pnode)
{
if (pnode->configurations != 1)
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("multiple hyper-parameter configurations for k-means are not yet supported")));
auto kmeans_hyperp = reinterpret_cast<HyperparametersKMeans const *>(pnode->hyperparameters[0]);
uint32_t const num_centroids = kmeans_hyperp->num_centroids;
uint32_t const dimension = kmeans_hyperp->n_features;
uint32_t const batch_size = kmeans_hyperp->batch_size;
// create state structure
auto kmeans_state = reinterpret_cast<KMeansState *>(makeNodeWithSize(TrainModelState,
sizeof(KMeansState)));
KMeansStateDescription *kmeans_state_description = &kmeans_state->description;
kmeans_state->done = false;
kmeans_state->description.centroids[0] =
reinterpret_cast<Centroid *>(palloc0(sizeof(Centroid) * num_centroids));
kmeans_state->description.centroids[1] =
reinterpret_cast<Centroid *>(palloc0(sizeof(Centroid) * num_centroids));
/*
* if the dimension was provided by the user we use that one as a reference
* to initialize all fields that depend on it
* if later on the dimension of the tuple disagrees and error is reported
* if the dimension is not provided by the user then it will be set on the very
* first tuple we read (optimistically)
*/
if (dimension > 0) {
initialize_fields(kmeans_state_description, num_centroids, dimension);
kmeans_state_description->late_initialized = false;
}
/*
* the following fields do not depend on the dimension and thus they are initialized
* here already
*/
kmeans_state_description->num_centroids = num_centroids;
switch (kmeans_hyperp->distance) {
case KMEANS_L1:
kmeans_state_description->distance = l1;
break;
case KMEANS_L2:
kmeans_state_description->distance = l2;
break;
case KMEANS_LINF:
kmeans_state_description->distance = linf;
break;
case KMEANS_L2_SQUARED:
kmeans_state_description->distance = l2_squared;
break;
default:
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("k-means init: no known distance function: %u", kmeans_hyperp->distance)));
}
kmeans_state_description->batch = reinterpret_cast<GSPoint *>(palloc0(sizeof(GSPoint) * batch_size));
return &kmeans_state->tms;
}
force_inline static void output_kmeans_state(Verbosity const verbosity, uint32_t const dimension,
uint64_t const num_good_points, uint64_t const num_dead_points,
double const local_elapsed_time)
{
if (verbosity > NO_OUTPUT) {
ereport(NOTICE, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("*** Initial statistics gathered:")));
ereport(NOTICE, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("*** Dimension used for computations: %u", dimension)));
ereport(NOTICE, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("*** Number of valid points: %lu", num_good_points)));
ereport(NOTICE, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("*** Number of dead points: %lu", num_dead_points)));
if (verbosity == VERBOSE_OUTPUT)
ereport(NOTICE, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("*** Duration (s): %0.6lf", local_elapsed_time)));
}
}
static void kmeans_deal_seeding_function(SeedingFunction seeding_function, uint32_t num_centroids, uint64_t num_points,
double sample_factor, uint32_t num_centroids_orig, Verbosity verbosity,
List **centroid_candidates_kmeans_bb, TrainModelState *pstate,
KMeansStateDescription *state_description, uint32_t const batch_size,
uint32_t idx_current_centroids,
uint32_t idx_next_centroids, std::mt19937_64 *prng)
{
double sample_probability = 0.;
double oversampling = 0.;
struct timespec start_kmeans_round, finish_kmeans_round;
uint64_t num_candidates = 0ULL;
uint64_t prev_num_candidates = 0ULL;
double cost_kmeans_bb = 0.;
double op_error = 0.;
double local_elapsed_time = 0.;
uint32_t current_iteration_kmeans_bb = 0U;
switch (seeding_function) {
case KMEANS_BB:
sample_probability = num_centroids >= num_points ? 1.0 : sample_factor / num_points;
/*
* the number of iterations of kmeans|| depends on the value of the initial solution
* if the solution is bad, we will iterate for longer time
* (one iteration consists of two data passes (one to sample candidates, and another
* to compute the cost of the (partial) solution
*/
oversampling = sample_factor * static_cast<double>(num_centroids_orig);
if (verbosity > NO_OUTPUT)
ereport(NOTICE, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("*** k-means|| oversampling factor: %lf, "
"expected number of candidates per round: %lu", sample_factor,
static_cast<uint64_t>(oversampling))));
do {
clock_gettime(CLOCK_MONOTONIC, &start_kmeans_round);
/*
* this data pass will sample candidates
* (the probability of not choosing a single element is > 0 and thus we have to
* try until at least one candidate is found)
*/
do {
prev_num_candidates = num_candidates;
pstate->rescan(pstate->callback_data);
*centroid_candidates_kmeans_bb = one_data_pass(pstate, state_description, batch_size,
idx_current_centroids,
idx_next_centroids,
true, sample_probability,
KMEANS_INITIAL_CENTROIDS_BB_SAMPLE,
*centroid_candidates_kmeans_bb, nullptr, prng);
num_candidates = *centroid_candidates_kmeans_bb ? (*centroid_candidates_kmeans_bb)->length : 0ULL;
} while (num_candidates <= prev_num_candidates);
/*
* this data pass will compute the cost of the (partial) solution
*/
reset_weights(*centroid_candidates_kmeans_bb);
cost_kmeans_bb = 0.;
pstate->rescan(pstate->callback_data);
*centroid_candidates_kmeans_bb = one_data_pass(pstate, state_description, batch_size,
idx_current_centroids,
idx_next_centroids,
false, 0., KMEANS_INITIAL_CENTROIDS_BB_COMPUTE_COST,
*centroid_candidates_kmeans_bb, &cost_kmeans_bb,
nullptr);
/*
* for the next iteration, sample probability changes according to the cost of the current
* solution
*/
twoDiv(oversampling, cost_kmeans_bb, &sample_probability, &op_error);
sample_probability += op_error;
clock_gettime(CLOCK_MONOTONIC, &finish_kmeans_round);
local_elapsed_time = static_cast<double>(finish_kmeans_round.tv_sec - start_kmeans_round.tv_sec);
local_elapsed_time +=
static_cast<double>(finish_kmeans_round.tv_nsec - start_kmeans_round.tv_nsec) /
1000000000.0;
if (verbosity == VERBOSE_OUTPUT)
ereport(NOTICE, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("*** k-means|| round %u stats: cost: %lf, total number of candidates: %u, "
"duration (s): %0.06lf", current_iteration_kmeans_bb + 1,
cost_kmeans_bb, (*centroid_candidates_kmeans_bb)->length, local_elapsed_time)));
} while (likely((++current_iteration_kmeans_bb < NUM_ITERATIONS_KMEANSBB) &&
(num_candidates < num_points)));
break;
case KMEANS_RANDOM_SEED:
/*
* the expected number of points to sample
*/
oversampling = sample_factor * static_cast<double>(num_centroids_orig);
/*
* if the number of centroids is larger than the number of data points (corner case)
* each data point becomes a centroid. otherwise we over sample (all data points could
* be sampled)
*/
sample_probability = num_centroids >= num_points ? 1.0 : oversampling / num_points;
if (verbosity > NO_OUTPUT)
ereport(NOTICE, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("*** random++ oversampling factor: %lf, expected number of candidates: %lu",
sample_factor, static_cast<uint64_t>(oversampling))));
do {
pstate->rescan(pstate->callback_data);
*centroid_candidates_kmeans_bb = one_data_pass(pstate, state_description, batch_size,
idx_current_centroids, idx_next_centroids,
true, sample_probability,
KMEANS_INITIAL_CENTROIDS_RANDOM_SAMPLE,
*centroid_candidates_kmeans_bb, nullptr, prng);
num_candidates = *centroid_candidates_kmeans_bb ? (*centroid_candidates_kmeans_bb)->length : 0ULL;
} while ((num_candidates < num_points) && (num_candidates < oversampling));
reset_weights(*centroid_candidates_kmeans_bb);
break;
default:
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("k-means exec: not known seeding function: %u", seeding_function)));
}
}
void kmeans_create_model(KMeansState *kmeans_state, Model *model)
{
auto kmeans_hyperp =
reinterpret_cast<HyperparametersKMeans const *>(kmeans_state->tms.config->hyperparameters[0]);
uint32_t const num_centroids = kmeans_state->description.num_centroids;
uint32_t const num_points = kmeans_state->description.num_good_points;
uint32_t const actual_num_centroids = num_centroids > num_points ? num_points : num_centroids;
uint32_t const dimension = kmeans_state->description.dimension;
uint32_t idx_current_centroids = 1U - (kmeans_state->description.current_iteration & 1U);
bool const late_initialization = kmeans_state->description.late_initialized;
uint64_t cluster_size = 0ULL;
auto *score = reinterpret_cast<TrainingScore *>(palloc0(sizeof(TrainingScore)));
Centroid *current_centroid = nullptr;
PCentroid *current_wh_centroid = nullptr;
double *centroid_coordinates_data = nullptr;
double objective_function = 0.;
double local_error = 0.;
double total_error = 0.;
uint32_t const size_centroid_bytes = kmeans_state->description.size_centroids_bytes;
errno_t errorno = EOK;
/*
* this is generic information about the model. specific information (about k-means) is set down below
*/
model->return_type = INT4OID;
model->pre_time_secs = kmeans_state->description.seeding_time;
model->exec_time_secs = kmeans_state->description.execution_time;
model->processed_tuples = kmeans_state->description.num_good_points;
model->discarded_tuples = kmeans_state->description.num_dead_points;
/*
* in order to serialize the model, it has to be stored in a contiguous memory chunk,
* thus we compute the required space as follows:
* sizeof(ModelKMeans) + (sizeof(WHCentroid) * actual_num_centroids) +
* (sizeof(double) * dimension * actual_num_centroids)
*/
Size const coordinates_array_offset = sizeof(ModelKMeansV01) + (sizeof(PCentroid) * actual_num_centroids);
Size const model_kmeans_size =
coordinates_array_offset + (kmeans_state->description.size_centroids_bytes * actual_num_centroids);
auto model_kmeans = reinterpret_cast<ModelKMeansV01 *>(palloc0(model_kmeans_size));
/*
* filling in the raw model data (to be serialized)
*/
model->data.size = model_kmeans_size;
model->data.raw_data = model_kmeans;
model->data.version = DB4AI_MODEL_V01;
/*
* this is particular information about k-means (centroids are set down below)
*/
model_kmeans->actual_num_centroids = actual_num_centroids;
model_kmeans->dimension = dimension;
model_kmeans->distance_function_id = kmeans_hyperp->distance;
model_kmeans->coordinates_offset = coordinates_array_offset;
model_kmeans->num_actual_iterations = kmeans_state->description.actual_num_iterations;
/*
* this fields are deprecated and will disappear in the future
*/
model->train_info = nullptr;
model->num_actual_iterations = 0U;
model->weights = 0U;
model->model_data = 0U;
/*
* on late initialization, the dimension was set at runtime and thus the corresponding
* hyper-parameter has to be updated
*/
if (late_initialization)
update_model_hyperparameter(model->memory_context, model->hyperparameters, "num_features", INT4OID,
Int32GetDatum(dimension));
/*
* we fill in the information of every centroid
* the array of centroids start right after the last element of model_kmeans
*
* all coordinates are stored at the end of ModelKMeans (after the array of WHCentroid)
*/
model_kmeans->centroids =
reinterpret_cast<PCentroid *>(reinterpret_cast<char *>(model_kmeans) + sizeof(ModelKMeansV01));
centroid_coordinates_data =
reinterpret_cast<double *>(reinterpret_cast<char *>(model_kmeans) + coordinates_array_offset);
uint32_t centroid_coordinates_offset = 0U;
for (uint32_t current_centroid_idx = 0; current_centroid_idx < actual_num_centroids; ++current_centroid_idx) {
current_centroid = kmeans_state->description.centroids[idx_current_centroids] + current_centroid_idx;
cluster_size = current_centroid->statistics.getPopulation();
current_wh_centroid = model_kmeans->centroids + current_centroid_idx;
current_wh_centroid->id = current_centroid->id;
current_wh_centroid->objective_function = current_centroid->statistics.getTotal();
current_wh_centroid->avg_distance = current_centroid->statistics.getEmpiricalMean();
current_wh_centroid->min_distance = cluster_size > 0 ? current_centroid->statistics.getMin() : 0.;
current_wh_centroid->max_distance = current_centroid->statistics.getMax();
current_wh_centroid->std_dev_distance = current_centroid->statistics.getEmpiricalStdDev();
current_wh_centroid->cluster_size = cluster_size;
current_wh_centroid->coordinates = centroid_coordinates_data + centroid_coordinates_offset;
errorno = memcpy_s(current_wh_centroid->coordinates, size_centroid_bytes,
ARR_DATA_PTR(current_centroid->coordinates), size_centroid_bytes);
securec_check(errorno, "\0", "\0");
centroid_coordinates_offset += dimension;
twoSum(objective_function, current_wh_centroid->objective_function, &objective_function, &local_error);
total_error += local_error;
}
/*
* finally, the loss function (objective function)
* observe that hyper-parameters are set for all models the same
*/
score->value = objective_function + total_error;
score->name = kmeans_distance_to_string(kmeans_hyperp->distance);
model->scores = lappend(model->scores, score);
}
/*
* this executes the k-means "training" by executing multiple scans through the data
* until convergence can be declared.
* the operator works in stages:
* 1) obtain general statistics about the input data (one data pass),
* 2) execute a seeding method (random++ or kmeans||) (at least one data pass but not more than 10),
* 3) run Lloyd's algorithm (at least one data pass)
*/
static void kmeans_run(AlgorithmAPI *self, TrainModelState *pstate, Model **models)
{
/*
* get information from the node
*/
Assert(pstate->finished == 0);
Assert(pstate->config->configurations == 1);
auto kmeans_state = reinterpret_cast<KMeansState *>(pstate);
auto kmeans_hyperp =
const_cast<HyperparametersKMeans *>(
reinterpret_cast<HyperparametersKMeans const *>(pstate->config->hyperparameters[0]));
KMeansStateDescription& state_description = kmeans_state->description;
uint32_t const max_num_iterations = kmeans_hyperp->num_iterations;
uint32_t const batch_size = kmeans_hyperp->batch_size;
uint32_t idx_current_centroids = 0U;
uint32_t idx_next_centroids = 0U;
uint32_t num_centroids = state_description.num_centroids; // this field is always initialized
uint32_t const num_centroids_orig = num_centroids;
Verbosity const verbosity = kmeans_hyperp->verbosity;
bool const late_initialization = !state_description.initialized;
SeedingFunction const seeding_function = kmeans_hyperp->seeding;
IncrementalStatistics *prev_solution_statistics = nullptr;
IncrementalStatistics *current_solution_statistics = nullptr;
double cost_fraction = 0.;
double cost_fraction_correction = 0.;
double local_elapsed_time = 0.;
double total_time = 0.;
double const sample_factor = num_centroids < 1000000 ? 4. : 2.;
double const tolerance = kmeans_hyperp->tolerance;
List *centroid_candidates_kmeans_bb = nullptr;
struct timespec start_time, finish_time, start_kmeans_round, finish_kmeans_round;
uint64_t num_points = 0ULL;
uint64_t seed = kmeans_hyperp->external_seed;
Assert(seed != 0);
uint64_t const external_seed = seed;
/*
* internal seed obtained from random.org. do not change it because
* it will (most probably) change the overall seed used and results
* will not be reproducible
*/
uint64_t const internal_seed = 0x274066DB9441E851ULL;
seed ^= internal_seed;
/*
* high-quality prng (based Mersenne primes)
* nothing of this sort is currently available in the system
*/
std::mt19937_64 prng(seed);
// check if training is already finished, if so we return the next centroid
if (!kmeans_state->done) {
/*
* we have to see that the column we are passed on is of type array
*/
Oid oidtype = pstate->tuple.typid[0];
if (unlikely((oidtype != FLOAT8ARRAYOID)))
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("k-means exec: data is not of type float8 (double precision) array")));
if (pstate->tuple.ncolumns != 1)
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("k-means exec: relation should contain only a single attribute "
"(point coordinates in a double precision array)")));
/*
* on late initialization we propagate the dimension properly
*/
if (late_initialization) {
kmeans_hyperp->n_features = state_description.dimension;
kmeans_state->description.late_initialized = true;
}
/*
* Before iterating we have to find an initial set of centroids (seeds)
* either random seeds or using kmeans||, this is one table scan on which we
* can also obtain the diameter of the bounding box (for normalization for example)
* from this scan we can also obtain the coordinates of the bounding box
*/
clock_gettime(CLOCK_MONOTONIC, &start_time);
one_data_pass(pstate, &state_description, batch_size,
0, 1, false, 0., KMEANS_INIT, nullptr, nullptr, nullptr);
clock_gettime(CLOCK_MONOTONIC, &finish_time);
local_elapsed_time = static_cast<double>(finish_time.tv_sec - start_time.tv_sec);
local_elapsed_time += static_cast<double>(finish_time.tv_nsec - start_time.tv_nsec) / 1000000000.0;
total_time += local_elapsed_time;
output_kmeans_state(verbosity, state_description.dimension, state_description.num_good_points,
state_description.num_dead_points, local_elapsed_time);
num_points = state_description.num_good_points;
/*
* if the number of centroids is larger than the number of data points we save useless computations
* by keeping track of the actual number of centroids that can be realized, in the very end
* we restore the original number of centroids
*/
state_description.num_centroids = num_centroids_orig > num_points ? num_points : num_centroids_orig;
num_centroids = state_description.num_centroids;
if (unlikely(num_points == 0))
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("k-means exec: no valid point found (no input array seems to be a one-dimensional array, "
"or no point seems to be fully dimensional (perhaps all points have a null dimension?))")));
/*
* it is time to produce an initial set of centroids to start with
*/
clock_gettime(CLOCK_MONOTONIC, &start_time);
idx_current_centroids = state_description.current_iteration & 1U;
idx_next_centroids = 1U - idx_current_centroids;
/*
* if the number of centers is larger than the number of data points we choose
* all data points (we have no other option). this is a non-sense corner case.
* for the time being we restrict to k's that fit in memory, later we can
* lift that restriction by spooling centroids to a file
*
* for random production we sample with probability (sample_factor * k)/n
*
* for kmeans|| we sample the very first points with probability sample_factor / n and
* later with probability (sample_factor * k * d(x))/sum(d(y))
*/
kmeans_deal_seeding_function(seeding_function, num_centroids, num_points, sample_factor, num_centroids_orig,
verbosity, &centroid_candidates_kmeans_bb, pstate, &state_description,
batch_size, idx_current_centroids, idx_next_centroids, &prng);
/*
* once the set of candidates (> k) has been gathered, we produce k initial centroids using
* (weighted) kmeans++
* observe that the output of this function are the k centroids stored in their place
* and the list of candidates is freed up inside the function, thus accessing the list
* is illegal
*/
if (verbosity > NO_OUTPUT)
ereport(NOTICE, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("*** k-means++ begin: consolidating %u candidates to %u centroid(s)",
centroid_candidates_kmeans_bb->length, num_centroids)));
clock_gettime(CLOCK_MONOTONIC, &start_kmeans_round);
centroid_candidates_kmeans_bb = kmeanspp(&state_description, centroid_candidates_kmeans_bb,
idx_current_centroids, &prng);
clock_gettime(CLOCK_MONOTONIC, &finish_kmeans_round);
local_elapsed_time = static_cast<double>(finish_kmeans_round.tv_sec - start_kmeans_round.tv_sec);
local_elapsed_time +=
static_cast<double>(finish_kmeans_round.tv_nsec - start_kmeans_round.tv_nsec) /
1000000000.0;
if (verbosity == VERBOSE_OUTPUT)
ereport(NOTICE, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("*** k-means++ ended: duration (s): %0.06lf", local_elapsed_time)));
Assert(!centroid_candidates_kmeans_bb);
clock_gettime(CLOCK_MONOTONIC, &finish_time);
local_elapsed_time = static_cast<double>(finish_time.tv_sec - start_time.tv_sec);
local_elapsed_time += static_cast<double>(finish_time.tv_nsec - start_time.tv_nsec) / 1000000000.0;
total_time += local_elapsed_time;
state_description.seeding_time = local_elapsed_time;
if (verbosity == VERBOSE_OUTPUT)
ereport(NOTICE, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("*** Seed centroids constructed (%lu, %u): duration (s): %0.6lf",
num_points, state_description.current_centroid,
state_description.seeding_time)));
// we reset current_centroid so that later each centroid can be output
state_description.current_centroid = 0;
/*
* Scan the sub-plan and feed all the tuples to kmeans (every single iteration).
* for these scans we will do batching
*/
do {
clock_gettime(CLOCK_MONOTONIC, &start_time);
idx_next_centroids = 1U - idx_current_centroids;
prev_solution_statistics = state_description.solution_statistics + idx_next_centroids;
/*
* in this iteration we reset the set of next centroids start with a clean set for aggregation
*/
reset_centroids(&state_description, idx_next_centroids);
/*
* every iteration we have to reset the sub-plan to be able to re-scan it
*/
pstate->rescan(pstate->callback_data);
one_data_pass(pstate, &state_description, batch_size, idx_current_centroids,
idx_next_centroids, false, 0., KMEANS_LLOYD, nullptr, nullptr, nullptr);
/*
* let's produce the new set of centroids for the next iteration
* (this should be executed at the coordinator in a distributed environment
*/
merge_centroids(&state_description, idx_current_centroids, idx_next_centroids);
compute_cost(&state_description, idx_current_centroids);
current_solution_statistics = state_description.solution_statistics + idx_current_centroids;
if (unlikely(state_description.current_iteration == 1)) {
cost_fraction = 1.;
} else {
twoDiv(prev_solution_statistics->getTotal(), current_solution_statistics->getTotal(),
&cost_fraction, &cost_fraction_correction);
cost_fraction += cost_fraction_correction;
twoDiff(cost_fraction, 1.0, &cost_fraction, &cost_fraction_correction);
cost_fraction += cost_fraction_correction;
}
idx_current_centroids = idx_next_centroids;
clock_gettime(CLOCK_MONOTONIC, &finish_time);
local_elapsed_time = static_cast<double>(finish_time.tv_sec - start_time.tv_sec);
local_elapsed_time += static_cast<double>(finish_time.tv_nsec - start_time.tv_nsec) / 1000000000.0;
total_time += local_elapsed_time;
if (verbosity == VERBOSE_OUTPUT)
ereport(NOTICE, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("*** iteration %u: duration (s): %0.6lf, "
"total relevant population %lu, "
"total cost %0.6lf, "
"average distance %0.6lf, "
"min distance %0.6lf, "
"max distance %0.6lf, "
"standard deviation of distances %0.6lf, "
"cost delta: %0.6lf", state_description.current_iteration, local_elapsed_time,
current_solution_statistics->getPopulation(),
current_solution_statistics->getTotal(), current_solution_statistics->getEmpiricalMean(),
current_solution_statistics->getMin(), current_solution_statistics->getMax(),
current_solution_statistics->getEmpiricalStdDev(), cost_fraction)));
} while ((++state_description.current_iteration <= max_num_iterations)
&& (cost_fraction >= tolerance) && (num_points > num_centroids));
state_description.execution_time = total_time;
state_description.actual_num_iterations = state_description.current_iteration - 1;
if (verbosity > NO_OUTPUT) {
ereport(NOTICE, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("*** Number of centroids constructed: %u", num_centroids)));
ereport(NOTICE, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("*** Value of global objective function: %0.6lf", current_solution_statistics->getTotal())));
ereport(NOTICE, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("*** Seed: %lu", external_seed)));
if (verbosity == VERBOSE_OUTPUT)
ereport(NOTICE, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("*** Total duration time (s): %0.6lf", state_description.execution_time)));
}
/*
* finally set the finished flag to true
*/
kmeans_state->done = true;
/*
* we restore the original number of centroids
*/
state_description.num_centroids = num_centroids_orig;
}
// number of configurations already run
++pstate->finished;
Model *model = models[0];
MemoryContext oldcxt = MemoryContextSwitchTo(model->memory_context);
kmeans_create_model(kmeans_state, model);
model->status = ERRCODE_SUCCESSFUL_COMPLETION;
MemoryContextSwitchTo(oldcxt);
}
/*
* this function frees up all the storage required by the algorithm.
* at this point the upper layer has made persistent the model and
* this information is not required anymore
*/
static void kmeans_end(AlgorithmAPI *self, TrainModelState *pstate)
{
auto kmeans_state_node = reinterpret_cast<KMeansState *>(pstate);
if (kmeans_state_node->description.initialized) {
uint32_t const num_centroids = kmeans_state_node->description.num_centroids;
for (uint32_t c = 0; c < num_centroids; ++c) {
pfree(kmeans_state_node->description.centroids[0][c].coordinates);
pfree(kmeans_state_node->description.centroids[1][c].coordinates);
}
pfree(kmeans_state_node->description.centroids[0]);
pfree(kmeans_state_node->description.centroids[1]);
pfree(kmeans_state_node->description.batch);
}
}
force_inline static MetricML *kmeans_metrics_loss(AlgorithmAPI *self, int *num_metrics)
{
elog(ERROR, "kmeans_metrics_loss");
return nullptr;
}
void kmeans_set_references(ModelKMeansV01 *model_kmeans)
{
uint32_t const actual_num_centroids = model_kmeans->actual_num_centroids;
/*
* we have to fix the internal pointers to point to the right locations (as memory has changed)
* observe that this pointers are pretty much offsets into the same memory chunk
*
* the array of centroids start right after the last element of model_kmeans (we fix the address)
*/
model_kmeans->centroids =
reinterpret_cast<PCentroid *>(reinterpret_cast<char *>(model_kmeans) + sizeof(ModelKMeansV01));
/*
* the address of the coordinates of each centroid must be fixed as well
*/
PCentroid *current_centroid = nullptr;
double *centroid_coordinates_data =
reinterpret_cast<double *>(reinterpret_cast<char *>(model_kmeans) + model_kmeans->coordinates_offset);
uint32_t centroid_coordinates_offset = 0U;
for (uint32_t c = 0; c < actual_num_centroids; ++c) {
current_centroid = model_kmeans->centroids + c;
current_centroid->coordinates = centroid_coordinates_data + centroid_coordinates_offset;
centroid_coordinates_offset += model_kmeans->dimension;
}
}
ModelPredictor kmeans_predict_prepare(AlgorithmAPI *self, SerializedModel const *model, Oid return_type)
{
if (unlikely(!model))
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("k-means predict prepare: model cannot be null")));
if (unlikely(model->version != DB4AI_MODEL_V01))
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("k-means predict prepare: currently only model V01 is supported")));
auto model_kmeans = reinterpret_cast<ModelKMeansV01 *>(model->raw_data);
kmeans_set_references(model_kmeans);
return reinterpret_cast<ModelPredictor>(model_kmeans);
}
static Datum kmeans_predict(AlgorithmAPI *, ModelPredictor model, Datum *data, bool *nulls, Oid *types, int32_t nargs)
{
auto kmeans_model = reinterpret_cast<ModelKMeansV01 *>(model);
/*
* sanity checks
*/
if (unlikely(nargs != 1))
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("k-means predict: only a single attribute containing the coordinates is accepted")));
if (unlikely(types[0] != FLOAT8ARRAYOID))
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("k-means predict: only double precision array of coordinates is accepted")));
if (unlikely(nulls[0]))
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("k-means predict: array of coordinates cannot be null")));
uint32_t const num_centroids = kmeans_model->actual_num_centroids;
uint32_t const dimension = kmeans_model->dimension;
double (*distance)(double const *, double const *, uint32_t const) = nullptr;
if (unlikely(num_centroids == 0))
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("k-means predict: number of centroids must be positive")));
switch (kmeans_model->distance_function_id) {
case KMEANS_L1:
distance = l1;
break;
case KMEANS_L2:
distance = l2;
break;
case KMEANS_L2_SQUARED:
distance = l2_squared;
break;
case KMEANS_LINF:
distance = linf;
break;
default:
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("k-means predict: distance function id %u not recognized",
kmeans_model->distance_function_id)));
}
PCentroid *current_centroid = nullptr;
PCentroid *closest_centroid = nullptr;
auto input_point_pg_array = DatumGetArrayTypeP(data[0]);
auto min_distance = DBL_MAX;
double local_distance = 0.;
double const *input_point_coordinates = nullptr;
int32_t closest_centroid_id = -1;
bool const valid_input = verify_pgarray(input_point_pg_array, dimension);
bool min_distance_changed = false;
if (unlikely(!valid_input))
return Int32GetDatum(closest_centroid_id);
input_point_coordinates = reinterpret_cast<double const *>(ARR_DATA_PTR(input_point_pg_array));
for (uint32_t c = 0; c < num_centroids; ++c) {
current_centroid = kmeans_model->centroids + c;
local_distance = distance(input_point_coordinates, current_centroid->coordinates, dimension);
min_distance_changed = local_distance < min_distance;
min_distance = min_distance_changed ? local_distance : min_distance;
closest_centroid = min_distance_changed ? current_centroid : closest_centroid;
}
closest_centroid_id = closest_centroid->id;
/*
* for the time being there is no other way to get to the computed distance other than by a log
*/
return Int32GetDatum(closest_centroid_id);
}
/*
* this is a faster version of construct_md_array in which we use knowledge we have
* to speed up computations
*/
ArrayType *construct_empty_md_array(uint32_t const num_centroids, uint32_t const dimension)
{
ArrayType *result = NULL;
int32_t ndims = 0;
uint32_t dims[2] = {0U};
uint32_t const lbs[2] = {1U, 1U};
if (num_centroids == 0) {
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("construct_empty_md_array: first dimension must be larger than 0")));
} else if (num_centroids == 1) {
ndims = 1;
dims[0] = {dimension};
} else {
ndims = 2;
dims[0] = num_centroids;
dims[1] = dimension;
}
check_hyper_bounds(num_centroids, dimension, "num_centroids");
check_hyper_bounds(num_centroids * dimension, sizeof(float8), "num_centroids");
int32_t nbytes = num_centroids * dimension * sizeof(float8);
nbytes += ARR_OVERHEAD_NONULLS(ndims);
result = reinterpret_cast<ArrayType *>(palloc0(nbytes));
SET_VARSIZE(result, nbytes);
result->ndim = ndims;
result->dataoffset = 0; /* marker for no null bitmap */
result->elemtype = FLOAT8OID;
errno_t errorno = EOK;
errorno = memcpy_s(ARR_DIMS(result), ndims * sizeof(int32_t), dims, ndims * sizeof(int32_t));
securec_check(errorno, "\0", "\0");
errorno = memcpy_s(ARR_LBOUND(result), ndims * sizeof(int32_t), lbs, ndims * sizeof(int32_t));
securec_check(errorno, "\0", "\0");
return result;
}
/*
* used in EXPLAIN MODEL
*/
List *kmeans_explain(AlgorithmAPI *self, SerializedModel const *model, Oid return_type)
{
if (unlikely(!model))
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("k-means explain: model cannot be null")));
if (unlikely(model->version != DB4AI_MODEL_V01))
ereport(ERROR, (errmodule(MOD_DB4AI), errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("k-means explain: currently only model V01 is supported")));
List *model_info = nullptr;
auto model_kmeans = reinterpret_cast<ModelKMeansV01 *>(model->raw_data);
kmeans_set_references(model_kmeans);
uint32_t const actual_num_centroids = model_kmeans->actual_num_centroids;
uint32_t const dimension = model_kmeans->dimension;
TrainingInfo *actual_num_centroids_info = (TrainingInfo *)palloc0(sizeof(TrainingInfo));
TrainingInfo *num_iterations_info = (TrainingInfo *)palloc0(sizeof(TrainingInfo));
actual_num_centroids_info->name = "Actual number of centroids";
num_iterations_info->name = "Number of iterations";
actual_num_centroids_info->type = INT4OID;
num_iterations_info->type = INT4OID;
actual_num_centroids_info->value = Int32GetDatum(model_kmeans->actual_num_centroids);
num_iterations_info->value = Int32GetDatum(model_kmeans->num_actual_iterations);
model_info = lappend(model_info, actual_num_centroids_info);
model_info = lappend(model_info, num_iterations_info);
PCentroid *current_wh_centroid = nullptr;
ArrayType *centroid_coordinates_array = nullptr;
double *centroid_coordinates_data = nullptr;
uint32_t const size_centroid_bytes = sizeof(double) * dimension;
error_t errorno = EOK;
for (uint32_t current_centroid_idx = 0; current_centroid_idx < actual_num_centroids; ++current_centroid_idx) {
current_wh_centroid = model_kmeans->centroids + current_centroid_idx;
/*
* opening and closing the eigenvector group
* if both open_group and close_group are set, they will be ignored)
*/
TrainingInfo *centroid_open_group = (TrainingInfo *)palloc0(sizeof(TrainingInfo));
centroid_open_group->open_group = true;
TrainingInfo *centroid_close_group = (TrainingInfo *)palloc0(sizeof(TrainingInfo));
centroid_close_group->close_group = true;
// these define the properties of a centroid
TrainingInfo *centroid_id = (TrainingInfo *)palloc0(sizeof(TrainingInfo));
TrainingInfo *centroid_coordinates = (TrainingInfo *)palloc0(sizeof(TrainingInfo));
TrainingInfo *centroid_loss_function = (TrainingInfo *)palloc0(sizeof(TrainingInfo));
TrainingInfo *centroid_max_distance = (TrainingInfo *)palloc0(sizeof(TrainingInfo));
TrainingInfo *centroid_min_distance = (TrainingInfo *)palloc0(sizeof(TrainingInfo));
TrainingInfo *centroid_avg_distance = (TrainingInfo *)palloc0(sizeof(TrainingInfo));
TrainingInfo *centroid_std_dev_distance = (TrainingInfo *)palloc0(sizeof(TrainingInfo));
TrainingInfo *centroid_cluster_size = (TrainingInfo *)palloc0(sizeof(TrainingInfo));
// opening and closing a group must have exactly the same name (otherwise fire!)
centroid_open_group->name = "Centroid";
centroid_close_group->name = "Centroid";
// the name of each property
centroid_id->name = "ID";
centroid_coordinates->name = "Coordinates";
centroid_loss_function->name = "Objective function";
centroid_max_distance->name = "Maximum cluster distance";
centroid_min_distance->name = "Minimum cluster distance";
centroid_avg_distance->name = "Average cluster distance";
centroid_std_dev_distance->name = "Standard deviation of cluster distances";
centroid_cluster_size->name = "Cluster size";
// the type of each property
centroid_id->type = INT4OID;
centroid_coordinates->type = FLOAT8ARRAYOID;
centroid_loss_function->type = FLOAT8OID;
centroid_max_distance->type = FLOAT8OID;
centroid_min_distance->type = FLOAT8OID;
centroid_avg_distance->type = FLOAT8OID;
centroid_std_dev_distance->type = FLOAT8OID;
centroid_cluster_size->type = INT8OID;
// the coordinates of a centroid
centroid_coordinates_array = construct_empty_md_array(1, dimension);
centroid_coordinates_data = reinterpret_cast<double *>(ARR_DATA_PTR(centroid_coordinates_array));
// filling in the data
centroid_id->value = Int32GetDatum(current_wh_centroid->id);
centroid_coordinates->value = PointerGetDatum(centroid_coordinates_array);
centroid_loss_function->value = Float8GetDatumFast(current_wh_centroid->objective_function);
centroid_max_distance->value = Float8GetDatumFast(current_wh_centroid->max_distance);
centroid_min_distance->value = Float8GetDatumFast(current_wh_centroid->min_distance);
centroid_avg_distance->value = Float8GetDatumFast(current_wh_centroid->avg_distance);
centroid_std_dev_distance->value = Float8GetDatumFast(current_wh_centroid->std_dev_distance);
centroid_cluster_size->value = Int64GetDatumFast(current_wh_centroid->cluster_size);
// filling in the coordinates
errorno = memcpy_s(centroid_coordinates_data, size_centroid_bytes, current_wh_centroid->coordinates,
size_centroid_bytes);
securec_check(errorno, "\0", "\0");
/*
* appending the properties to the list of properties
* OBSERVE that open and close elements must be well positioned! (at the beginning and end of the information)
*/
model_info = lappend(model_info, centroid_open_group);
model_info = lappend(model_info, centroid_id);
model_info = lappend(model_info, centroid_cluster_size);
model_info = lappend(model_info, centroid_coordinates);
model_info = lappend(model_info, centroid_loss_function);
model_info = lappend(model_info, centroid_max_distance);
model_info = lappend(model_info, centroid_min_distance);
model_info = lappend(model_info, centroid_avg_distance);
model_info = lappend(model_info, centroid_std_dev_distance);
model_info = lappend(model_info, centroid_close_group);
}
return model_info;
}
KMeans kmeans = {
// AlgorithmAPI
{KMEANS, "kmeans", ALGORITHM_ML_UNSUPERVISED | ALGORITHM_ML_RESCANS_DATA, kmeans_metrics_loss,
kmeans_get_hyperparameters, kmeans_make_hyperparameters, kmeans_update_hyperparameters, kmeans_create, kmeans_run,
kmeans_end, kmeans_predict_prepare, kmeans_predict, kmeans_explain},
};