O framework Feature Generation (FG) suporta operadores de feature personalizados como plugins de biblioteca compartilhada. Utilize-os quando os operadores nativos não atenderem à sua lógica de transformação específica de domínio — por exemplo, distância de edição entre dois campos de texto, distância esférica a partir de coordenadas GPS ou qualquer fórmula numérica aplicada elemento a elemento em uma sequência.
Como funciona
Implemente a interface C++
IFeatureOPe registre o operador comREGISTER_PLUGIN.Compile a implementação em uma biblioteca compartilhada (
.so).Referencie o operador no arquivo
fg.jsondefinindofeature_typecomocustom_featureeoperator_namecom o nome registrado.Implante o arquivo
.so: os serviços online o descobrem automaticamente no diretóriocustom_fg_lib, enquanto tarefas offline exigem o upload do arquivo como um resource do MaxCompute.
Pré-requisitos
Antes de começar, certifique-se de ter:
Um ambiente de build compatível com C++17 usando a imagem oficial do compilador (consulte Compilar um operador personalizado)
O pacote de dependências da API: fg-api.tar.gz — contém todos os arquivos de cabeçalho necessários
Familiaridade com o framework FG e o funcionamento da configuração
fg.json
Implementar um operador
Exemplo mínimo
Todo operador personalizado segue o mesmo padrão: herdar de IFeatureOP, implementar Initialize e pelo menos um método ProcessWith*, além de registrar com REGISTER_PLUGIN.
Abaixo está um operador mínimo que gera um número inteiro constante:
#include "api/base_op.h"
namespace fg {
class ConstantOp : public IFeatureOP {
public:
int Initialize(const string& feature_config) override {
// Parse JSON config if needed; return 0 on success.
return 0;
}
int ProcessWithInt32Outputs(const vector<FieldPtr>& inputs,
vector<int32>& outputs) override {
outputs.push_back(42);
return 0; // 0 = success
}
};
} // namespace fg
REGISTER_PLUGIN("ConstantOp", ConstantOp);
Requisitos principais:
Inclua um construtor sem parâmetros (o construtor padrão funciona neste caso).
O método
Initializerecebe toda a entradafg.jsoncomo uma string JSON. Faça o parse dos itens de configuração necessários a partir dela.Retorne
0para indicar sucesso em todos os métodos; valores diferentes de zero indicam falha.Chame
REGISTER_PLUGINno arquivo de implementação, não no cabeçalho.
Escolher entre ProcessWith* e BatchProcess
Duas interfaces de processamento estão disponíveis. Escolha com base nos seus requisitos de throughput e complexidade:
|
** |
** |
|
|
Granularidade |
Um registro por vez |
Um lote de registros |
|
Tipos de saída |
Apenas tipos escalares básicos ( |
Qualquer tipo válido de |
|
Quando usar |
Transformações simples, baixo custo de implementação |
Necessário alto throughput; features do lado do usuário com uma amostra por requisição (use o mecanismo de broadcast para evitar parsing redundante) |
|
Obrigatório |
Pelo menos um método correspondente ao |
Sobrescrever |
Quando BatchProcess é implementado e HasBatchProcessImpl() retorna true, o framework ignora todos os métodos ProcessWith*.
Interface C++ completa
#pragma once
#ifndef FEATURE_GENERATOR_PLUGIN_BASE_H
#define FEATURE_GENERATOR_PLUGIN_BASE_H
#include <absl/container/flat_hash_map.h>
#include <absl/strings/string_view.h>
#include <absl/types/optional.h>
#include <stdexcept>
#include <utility>
#include <vector>
#include "fsmap.h"
#include "integral_types.h"
namespace fg {
using absl::optional;
using std::string;
using std::vector;
template <typename T>
using List = std::vector<T>;
template <typename K, typename V>
using Map = absl::flat_hash_map<K, V>;
template <typename K, typename V>
using MapArray = std::vector<std::pair<K, V>>;
using Matrix = std::vector<std::vector<float>>;
using MatrixL = std::vector<std::vector<int64>>;
using MatrixS = std::vector<std::vector<string>>;
template <typename K, typename V>
using FSMap = featurestore::type::fs_map<K, V>;
using FieldPtr = absl::variant<
const optional<string>*, const optional<int32>*, const optional<int64>*,
const optional<float>*, const optional<double>*,
const optional<absl::string_view>*,
const List<string>*, const List<int32>*, const List<int64>*,
const List<float>*, const List<double>*, const List<absl::string_view>*,
const Map<string, string>*, const Map<string, int32>*,
const Map<string, int64>*, const Map<string, float>*,
const Map<string, double>*, const Map<string, absl::string_view>*,
const Map<absl::string_view, absl::string_view>*,
const Map<absl::string_view, int32>*, const Map<absl::string_view, int64>*,
const Map<absl::string_view, float>*, const Map<absl::string_view, double>*,
const Map<absl::string_view, string>*,
const Map<int32, string>*, const Map<int32, int32>*,
const Map<int32, int64>*, const Map<int32, float>*,
const Map<int32, double>*, const Map<int32, absl::string_view>*,
const Map<int64, string>*, const Map<int64, float>*,
const Map<int64, double>*, const Map<int64, int32>*,
const Map<int64, int64>*, const Map<int64, absl::string_view>*,
const FSMap<absl::string_view, absl::string_view>*,
const FSMap<absl::string_view, int32>*,
const FSMap<absl::string_view, int64>*,
const FSMap<absl::string_view, float>*,
const FSMap<absl::string_view, double>*,
const FSMap<int32, int32>*, const FSMap<int32, int64>*,
const FSMap<int32, float>*, const FSMap<int32, double>*,
const FSMap<int32, absl::string_view>*,
const FSMap<int64, float>*, const FSMap<int64, double>*,
const FSMap<int64, int32>*, const FSMap<int64, int64>*,
const FSMap<int64, absl::string_view>*,
const MapArray<string, string>*, const MapArray<string, int32>*,
const MapArray<string, int64>*, const MapArray<string, float>*,
const MapArray<string, double>*,
const MapArray<int32, string>*, const MapArray<int32, float>*,
const MapArray<int32, double>*, const MapArray<int32, int32>*,
const MapArray<int32, int64>*,
const MapArray<int64, string>*, const MapArray<int64, float>*,
const MapArray<int64, double>*, const MapArray<int64, int32>*,
const MapArray<int64, int64>*, const Matrix*, const MatrixL*,
const MatrixS*>;
// Represents a COLUMN of the feature table.
using VariantVector = absl::variant<
vector<optional<string>>, vector<optional<int32>>, vector<optional<int64>>,
vector<optional<float>>, vector<optional<double>>,
vector<optional<absl::string_view>>,
vector<List<string>>, vector<List<int32>>, vector<List<int64>>,
vector<List<float>>, vector<List<double>>, vector<List<absl::string_view>>,
vector<Map<string, string>>, vector<Map<string, int32>>,
vector<Map<string, int64>>, vector<Map<string, float>>,
vector<Map<string, double>>, vector<Map<string, absl::string_view>>,
vector<Map<absl::string_view, absl::string_view>>,
vector<Map<absl::string_view, int32>>,
vector<Map<absl::string_view, int64>>,
vector<Map<absl::string_view, float>>,
vector<Map<absl::string_view, double>>,
vector<Map<int32, string>>, vector<Map<int32, int32>>,
vector<Map<int32, int64>>, vector<Map<int32, float>>,
vector<Map<int32, double>>, vector<Map<int32, absl::string_view>>,
vector<Map<int64, string>>, vector<Map<int64, float>>,
vector<Map<int64, double>>, vector<Map<int64, int32>>,
vector<Map<int64, int64>>, vector<Map<int64, absl::string_view>>,
vector<FSMap<absl::string_view, absl::string_view>>,
vector<FSMap<absl::string_view, int32>>,
vector<FSMap<absl::string_view, int64>>,
vector<FSMap<absl::string_view, float>>,
vector<FSMap<absl::string_view, double>>,
vector<FSMap<int32, int32>>, vector<FSMap<int32, int64>>,
vector<FSMap<int32, float>>, vector<FSMap<int32, double>>,
vector<FSMap<int32, absl::string_view>>,
vector<FSMap<int64, float>>, vector<FSMap<int64, double>>,
vector<FSMap<int64, int32>>, vector<FSMap<int64, int64>>,
vector<FSMap<int64, absl::string_view>>,
vector<MapArray<string, string>>, vector<MapArray<string, int32>>,
vector<MapArray<string, int64>>, vector<MapArray<string, float>>,
vector<MapArray<string, double>>,
vector<MapArray<int32, string>>, vector<MapArray<int32, float>>,
vector<MapArray<int32, double>>, vector<MapArray<int32, int32>>,
vector<MapArray<int32, int64>>,
vector<MapArray<int64, string>>, vector<MapArray<int64, float>>,
vector<MapArray<int64, double>>, vector<MapArray<int64, int32>>,
vector<MapArray<int64, int64>>, vector<Matrix>, vector<MatrixL>,
vector<MatrixS>>;
/**
* @brief The public base class for custom feature operators.
*
* The framework checks if a subclass overrides the `BatchProcess` method. If it is overridden,
* the framework calls this method to perform the feature transformation.
* Otherwise, the framework selects one of the `ProcessWith*` methods based on the `value_type` configuration.
* Implement the method that corresponds to the required output type.
*/
class IFeatureOP {
public:
class NotOverriddenException : public std::exception {
public:
explicit NotOverriddenException(std::string msg) : msg_(std::move(msg)) {}
const char* what() const noexcept override {
if (msg_.empty()) {
return "unimplemented method called";
}
// Cache the message to a member variable to ensure that the returned pointer remains valid.
cached_ = "unimplemented method called: " + msg_;
return cached_.c_str();
}
private:
std::string msg_;
mutable std::string cached_;
};
virtual ~IFeatureOP() = default;
/**
* @brief Initialization method.
* @param feature_config The full fg.json entry for this feature, as a JSON string.
* @return 0 on success; non-zero indicates failure.
*/
virtual int Initialize(const string& feature_config) = 0;
/**
* @brief Processes one record and outputs string values.
* @param inputs A record that can contain multiple fields.
* @param outputs The outputs of the feature transformation.
* @return 0 on success.
*/
virtual int ProcessWithStrOutputs(const vector<FieldPtr>& inputs,
vector<string>& outputs) {
throw NotOverriddenException("ProcessWithStrOutputs(FieldPtr)");
}
/**
* @brief Processes one record and outputs int32 values.
*/
virtual int ProcessWithInt32Outputs(const vector<FieldPtr>& inputs,
vector<int32>& outputs) {
throw NotOverriddenException("ProcessWithInt32Outputs(FieldPtr)");
}
/**
* @brief Processes one record and outputs int64 values.
*/
virtual int ProcessWithInt64Outputs(const vector<FieldPtr>& inputs,
vector<int64>& outputs) {
throw NotOverriddenException("ProcessWithInt64Outputs(FieldPtr)");
}
/**
* @brief Processes one record and outputs float values.
*/
virtual int ProcessWithFloatOutputs(const vector<FieldPtr>& inputs,
vector<float>& outputs) {
throw NotOverriddenException("ProcessWithFloatOutputs(FieldPtr)");
}
/**
* @brief Processes one record and outputs double values.
*/
virtual int ProcessWithDoubleOutputs(const vector<FieldPtr>& inputs,
vector<double>& outputs) {
throw NotOverriddenException("ProcessWithDoubleOutputs(FieldPtr)");
}
/**
* @brief Optional batch interface that processes one batch of records.
*
* @param inputs A vector of input columns; each `VariantVector` represents one feature column.
* @param outputs The transformed features. Supports complex output types usable as inputs for downstream operators.
* @return 0 on success.
*/
virtual int BatchProcess(const vector<VariantVector>& inputs,
VariantVector& outputs) {
throw NotOverriddenException("BatchProcess");
}
/**
* @brief Declares whether the subclass implements BatchProcess.
*
* Override this to return `true` if you implement `BatchProcess`.
* This avoids exception propagation issues across dynamic library boundaries
* when the .so and the main program use different C++ ABIs.
*
* @return true if BatchProcess is implemented; false otherwise (default).
*/
virtual bool HasBatchProcessImpl() const { return false; }
};
using CreateOperatorFunc = IFeatureOP* (*)();
inline FieldPtr GetFieldPtr(const VariantVector& input, size_t i) {
return absl::visit(
[&](const auto& vec) -> FieldPtr {
if (i >= vec.size()) {
throw std::out_of_range("GetFieldPtr: index " + std::to_string(i) +
" out of range [0, " +
std::to_string(vec.size()) + ")");
}
return &vec.at(i);
},
input);
}
} // namespace fg
#if defined(__GNUC__)
#define PLUGIN_API_HIDDEN \
__attribute__((visibility("hidden"))) __attribute__((used))
#define PLUGIN_API_EXPORT \
__attribute__((visibility("default"))) __attribute__((used))
#else
#define PLUGIN_API_HIDDEN
#define PLUGIN_API_EXPORT
#endif
std::vector<std::string>& getLocalNames();
std::vector<std::pair<std::string, void*>>& getLocalRegs();
#define REGISTER_PLUGIN(OpName, OpClass) \
extern "C" PLUGIN_API_EXPORT fg::IFeatureOP* create##OpClass() { \
return new fg::OpClass(); \
} \
namespace { \
struct _Reg_##OpClass { \
_Reg_##OpClass() { \
getLocalNames().push_back(OpName); \
getLocalRegs().emplace_back(OpName, (void*)&create##OpClass); \
} \
}; \
static _Reg_##OpClass _dummy_##OpClass __attribute__((used)); \
}
#endif // FEATURE_GENERATOR_PLUGIN_BASE_H
Notas de implementação
Tipos de entrada: Implemente métodos
ProcessWith*para todos os tipos de entrada que pretende suportar. Para tipos não suportados, lance uma exceção diretamente.VariantRecorddefine todos os tipos de campos de feature que o framework pode processar. Os tiposFSMapsão necessários na integração com o Feature Store — eles melhoram significativamente o desempenho do processador online.Discretização: Implemente apenas a lógica de transformação anterior à discretização. Se
hash_bucket_size,vocab_list,boundariesou outra operação de discretização estiver configurada, o framework cuidará disso automaticamente.Segurança de threads: Por padrão (
is_op_thread_safe=true), o operador deve ser stateless ou usar apenas variáveisthread_local. Definais_op_thread_safe=falsepara que o framework crie uma réplica de objeto por thread — isso simplifica a implementação, mas consome mais memória.Entradas
string_view: Serviços online (EasyRecProcessor, TorchEasyRec Processor) passam features de string do lado do item comoabsl::string_viewpara maior eficiência. Caso seu operador não consiga lidar comstring_view, definadisable_string_view=truena configuração para que o framework as converta emstringantes de chamar seu operador. Isso reduz o desempenho.Itens de configuração personalizados: Use quaisquer nomes de chave na entrada JSON — o framework passa a string JSON completa para
Initialize. Não reutilize nomes de chave reservados pelo framework (comofeature_type,operator_name,value_type). Chaves que referenciam arquivos externos devem terminar com_filepara que o framework possa sincronizá-las em tarefas offline.Diretório de operadores: Utilize a variável de ambiente
FEATURE_OPERATOR_DIRpara especificar o diretório onde os arquivos de biblioteca de vínculo dinâmico estão localizados. Cada biblioteca de vínculo dinâmico pode conter implementações de múltiplos operadores de feature.Tipo de retorno de
BatchProcess: O tipo doVariantVectorretornado porBatchProcessdepende dos valores deis_sequence,value_dimensionevalue_type. Para mais informações, consulte o schema da tabela de saída em Referência de configuração. Quandostub_type=trueestá configurado e nenhuma operação de binning está definida,BatchProcesspode retornar qualquer tipo válido, comoMap.
Referência de configuração
Estrutura da entrada fg.json
{
"feature_name": "my_custom_fg_op",
"feature_type": "custom_feature",
"operator_name": "EditDistance",
"operator_lib_file": "libedit_distance.so",
"expression": [
"user:query",
"item:title"
],
"value_type": "string",
"separator": ",",
"default_value": "-1",
"value_dimension": 1,
"normalizer": "method=expression,expr=x>16?16:x",
"num_buckets": 10000,
"stub_type": false,
"is_sequence": false,
"is_op_thread_safe": true
}
Adicione quaisquer itens de configuração adicionais que seu operador necessite. Toda a entrada JSON é passada para Initialize.
Parâmetros de configuração
|
Parâmetro |
Obrigatório |
Descrição |
|
|
Sim |
Defina como |
|
|
Sim |
Nome usado em |
|
|
Offline: obrigatório; Online: opcional |
Nome do arquivo |
|
|
Sim |
Campos de entrada. Suporta múltiplas entradas. |
|
|
Sim |
Tipo de saída. Um dos seguintes: |
|
|
Sim |
Valor padrão como string. O framework o converte para |
|
|
Quando a saída é multidimensional |
Divide |
|
|
Não |
Se |
|
|
Não |
Indica se a saída é uma feature de sequência. Padrão: |
|
|
Quando |
Comprimento máximo da sequência. Sequências mais longas são truncadas. |
|
|
Quando a entrada é uma sequência do tipo string |
Separador entre elementos da sequência. |
|
|
Não |
Para sequências de entrada do tipo string, indica se o framework divide a string antes de passá-la ao operador. Padrão: |
|
|
Não |
Dimensão da feature de saída. Padrão: |
|
|
Não |
Normalização pós-transformação para features numéricas. Consulte Frameworks de normalização. |
|
|
Quando |
Preenche posições vazias em um elemento de sequência multivalorado. Padrão para floats: |
|
|
Não |
Converte entradas do tipo |
|
|
Não |
Indica se o operador é thread-safe. Padrão: |
Schema da tabela de saída
As configurações value_dimension e is_sequence determinam o tipo de schema da tabela de saída em tarefas offline:
|
** |
** |
Tipo de schema |
Com discretização |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Casos especiais: array<array<int>> é forçado para array<array<bigint>>; array<array<double>> é forçado para array<array<float>>.
Frameworks de normalização
Para features numéricas, adicione um normalizer para processar ainda mais o resultado da transformação:
|
Framework |
Exemplo de configuração |
Fórmula |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Qualquer expressão; a variável |
Para funções suportadas em expression, consulte Operadores de feature nativos.
Operações de discretização
Seis tipos de discretização estão disponíveis sem necessidade de implementação adicional:
|
Tipo |
Descrição |
|
|
Operação de hash e módulo no resultado da transformação. |
|
|
Converte o resultado em um índice dentro de uma lista. |
|
|
Converte o resultado em um valor dentro de um dicionário. O valor deve ser conversível para |
|
|
Carrega uma |
|
|
Converte o resultado em um número de bucket com base nos limites de intervalo especificados. |
|
|
Usa o resultado diretamente como número de bucket. |
Para mais informações, consulte Discretização de features (binning).
Exemplos de configuração
Feature de diferença de tempo em sequência
{
"feature_name": "time_diff_seq",
"feature_type": "custom_feature",
"operator_name": "SeqExpr",
"expression": ["user:cur_time", "user:clk_time_seq"],
"formula": "cur_time - clk_time_seq",
"default_value": "0",
"value_type": "int32",
"is_sequence": true,
"num_buckets": 1000,
"is_op_thread_safe": false
}
Distância esférica com normalização
{
"feature_name": "spherical_distance",
"feature_type": "custom_feature",
"operator_name": "SeqExpr",
"expression": ["item:click_id_lng", "item:click_id_lat", "user:j_lng", "user:j_lat"],
"formula": "spherical_distance",
"default_value": "0",
"value_type": "double",
"is_sequence": true,
"is_op_thread_safe": true,
"value_dimension": 1,
"normalizer": "method=expression,expr=sqrt(x)"
}
Ambos os exemplos utilizam o operador SeqExpr. O campo formula é um item de configuração específico do SeqExpr passado através de Initialize.
spherical_distance: Calcula a distância entre dois pares de coordenadas de latitude/longitude. Os parâmetros são[lng1_seq, lat1_seq, lng2, lat2]— os dois primeiros são sequências; os dois últimos são escalares.
Estes exemplos demonstram o formato tiled para features de sequência personalizadas. Para um exemplo em formato aninhado, consulte sequence_feature.
Features de sequência
Quando is_sequence=true, os requisitos de saída diferem dependendo se a feature é esparsa ou densa:
Sequências esparsas
Valor único por elemento: saída de qualquer tipo.
Múltiplos valores por elemento: saída apenas do tipo
string. Definavalue_typecomostringe usechr(29)como separador entre valores dentro de um único elemento.
Sequências densas
Defina
value_dimensioncomo a dimensão de cada elemento.Elementos escalares:
value_dimension=1.Elementos vetoriais:
value_dimension=comprimento do vetor.O número total de valores de saída deve ser um múltiplo inteiro de
value_dimension.
Exemplo para desenvolvedores
O exemplo a seguir implementa um operador de distância de edição que calcula a distância Levenshtein entre duas entradas de texto.
Arquivo de cabeçalho (edit_distance.h):
#pragma once
#include "api/base_op.h"
namespace fg {
namespace functor {
class EditDistanceFunctor;
}
using std::string;
using std::vector;
/**
* @brief EditDistance: takes two strings, outputs their edit distance.
*/
class EditDistance : public IFeatureOP {
public:
int Initialize(const string& feature_config) override;
/// @return 0 on success.
int ProcessWithStrOutputs(const vector<FieldPtr>& inputs,
vector<string>& outputs) override;
/// @return 0 on success.
int ProcessWithInt32Outputs(const vector<FieldPtr>& inputs,
vector<int32>& outputs) override;
/// @return 0 on success.
int ProcessWithInt64Outputs(const vector<FieldPtr>& inputs,
vector<int64>& outputs) override;
/// @return 0 on success.
int ProcessWithFloatOutputs(const vector<FieldPtr>& inputs,
vector<float>& outputs) override;
/// @return 0 on success.
int ProcessWithDoubleOutputs(const vector<FieldPtr>& inputs,
vector<double>& outputs) override;
private:
string feature_name_;
std::unique_ptr<functor::EditDistanceFunctor> functor_p_;
};
} // end of namespace fg
Arquivo de implementação (edit_distance.cc):
#include "edit_distance.h"
#include <absl/strings/ascii.h>
#include <absl/strings/str_join.h>
#include <nlohmann/json.hpp>
#include <numeric> // std::iota
#include <stdexcept>
#include "api/log.h"
namespace fg {
using absl::optional;
namespace functor {
template <class T>
int edit_distance(const T& s1, const T& s2) {
int l1 = s1.size();
int l2 = s2.size();
if (l1 * l2 == 0) {
return l1 + l2;
}
vector<int> prev(l2 + 1);
vector<int> curr(l2 + 1);
std::iota(prev.begin(), prev.end(), 0);
for (int i = 0; i <= l1; ++i) {
curr[0] = i;
for (int j = 1; j <= l2; ++j) {
int d = prev[j - 1];
if (s1[i - 1] == s2[j - 1]) {
curr[j] = d;
} else {
int d2 = std::min(prev[j], curr[j - 1]);
curr[j] = 1 + std::min(d, d2);
}
}
prev.swap(curr);
}
return prev[l2];
}
enum class Encoding : unsigned int { Latin = 0, UTF8 = 1 };
class EditDistanceFunctor {
public:
EditDistanceFunctor(const string& encoding) {
string enc = absl::AsciiStrToLower(encoding);
if (enc == "utf-8" || enc == "utf8") {
encoding_ = Encoding::UTF8;
} else {
encoding_ = Encoding::Latin;
}
}
int operator()(absl::string_view s1, absl::string_view s2) {
if (encoding_ == Encoding::Latin) {
return edit_distance(s1, s2);
}
if (encoding_ == Encoding::UTF8) {
return edit_distance(from_bytes(s1), from_bytes(s2));
}
LOG(ERROR) << "EditDistanceFunctor found unsupported text encoding";
assert(false);
return 0;
}
const Encoding TextEncoding() const { return encoding_; }
private:
Encoding encoding_;
std::wstring from_bytes(absl::string_view str) {
std::wstring result;
int i = 0;
int len = (int)str.length();
while (i < len) {
int char_size = 0;
int unicode = 0;
if ((str[i] & 0x80) == 0) {
unicode = str[i];
char_size = 1;
} else if ((str[i] & 0xE0) == 0xC0) {
unicode = str[i] & 0x1F;
char_size = 2;
} else if ((str[i] & 0xF0) == 0xE0) {
unicode = str[i] & 0x0F;
char_size = 3;
} else if ((str[i] & 0xF8) == 0xF0) {
unicode = str[i] & 0x07;
char_size = 4;
} else {
// Invalid UTF-8 sequence
++i;
continue;
}
for (int j = 1; j < char_size; ++j) {
unicode = (unicode << 6) | (str[i + j] & 0x3F);
}
if (unicode <= 0xFFFF) {
result += static_cast<wchar_t>(unicode);
} else {
// Handle surrogate pairs for characters outside the BMP
unicode -= 0x10000;
result += static_cast<wchar_t>((unicode >> 10) + 0xD800);
result += static_cast<wchar_t>((unicode & 0x3FF) + 0xDC00);
}
i += char_size;
}
return result;
}
};
} // namespace functor
// Overloaded helper for absl::visit (C++17).
template <class... Ts>
struct overloaded : Ts... {
using Ts::operator()...;
};
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
int EditDistance::Initialize(const string& feature_config) {
nlohmann::json cfg;
try {
cfg = nlohmann::json::parse(feature_config);
} catch (nlohmann::json::parse_error& ex) {
LOG(ERROR) << "parse error at byte " << ex.byte;
LOG(ERROR) << "config: " << feature_config;
throw std::runtime_error("parse EditDistance config failed");
}
feature_name_ = cfg.at("feature_name");
string encoding = cfg.value("encoding", "latin");
functor_p_ = std::make_unique<functor::EditDistanceFunctor>(encoding);
functor::Encoding enc = functor_p_->TextEncoding();
encoding = (enc == functor::Encoding::UTF8) ? "UTF-8" : "Latin";
LOG(INFO) << "feature <" << feature_name_ << "> with text encoding: " << encoding;
return 0;
}
int EditDistance::ProcessWithInt32Outputs(const vector<FieldPtr>& inputs,
vector<int32>& outputs) {
outputs.clear();
if (inputs.size() < 2) {
outputs.push_back(0);
return -1; // invalid inputs
}
int d = absl::visit(
overloaded{
[this](const optional<string>* s1, const optional<string>* s2) {
absl::string_view empty_view;
return functor_p_->operator()(*s1 ? **s1 : empty_view, *s2 ? **s2 : empty_view);
},
[this](const optional<absl::string_view>* s1,
const optional<absl::string_view>* s2) {
absl::string_view empty_view;
return functor_p_->operator()(*s1 ? **s1 : empty_view, *s2 ? **s2 : empty_view);
},
[this](const optional<absl::string_view>* s1,
const optional<string>* s2) {
absl::string_view empty_view;
return functor_p_->operator()(*s1 ? **s1 : empty_view, *s2 ? **s2 : empty_view);
},
[this](const optional<string>* s1,
const optional<absl::string_view>* s2) {
absl::string_view empty_view;
return functor_p_->operator()(*s1 ? **s1 : empty_view, *s2 ? **s2 : empty_view);
},
[this](const List<string>* s1, const List<string>* s2) {
string str1 = absl::StrJoin(*s1, "");
string str2 = absl::StrJoin(*s2, "");
return functor_p_->operator()(str1, str2);
},
[this](const List<absl::string_view>* s1,
const List<absl::string_view>* s2) {
string str1 = absl::StrJoin(*s1, "");
string str2 = absl::StrJoin(*s2, "");
return functor_p_->operator()(str1, str2);
},
[this](const auto* x, const auto* y) {
ERROR_EXIT(feature_name_,
"unsupported input type: ", typeid(*x).name(), " vs ",
typeid(*y).name());
return 0;
}},
inputs.at(0), inputs.at(1));
outputs.push_back(d);
return 0;
}
// int32 results are reused for int64, float, and double outputs.
int EditDistance::ProcessWithInt64Outputs(const vector<FieldPtr>& inputs,
vector<int64>& outputs) {
vector<int32> distances;
int status = ProcessWithInt32Outputs(inputs, distances);
if (0 != status) return status;
outputs.assign(distances.begin(), distances.end());
return 0;
}
int EditDistance::ProcessWithFloatOutputs(const vector<FieldPtr>& inputs,
vector<float>& outputs) {
vector<int32> distances;
int status = ProcessWithInt32Outputs(inputs, distances);
if (0 != status) return status;
outputs.assign(distances.begin(), distances.end());
return 0;
}
int EditDistance::ProcessWithDoubleOutputs(const vector<FieldPtr>& inputs,
vector<double>& outputs) {
vector<int32> distances;
int status = ProcessWithInt32Outputs(inputs, distances);
if (0 != status) return status;
outputs.assign(distances.begin(), distances.end());
return 0;
}
int EditDistance::ProcessWithStrOutputs(const vector<FieldPtr>& inputs,
vector<string>& outputs) {
vector<int32> distances;
int status = ProcessWithInt32Outputs(inputs, distances);
if (0 != status) return status;
outputs.reserve(distances.size());
std::transform(distances.begin(), distances.end(),
std::back_inserter(outputs),
[](int32& x) { return std::to_string(x); });
return 0;
}
} // end of namespace fg
REGISTER_PLUGIN("EditDistance", EditDistance);
Baixe o código-fonte da tabela em Operadores disponíveis e execute o script build.sh para compilar o operador.
Compilar um operador personalizado
Utilize o mesmo ambiente de compilação do framework FG: C++17 e a imagem oficial do compilador. Os detalhes da imagem estão no script build.sh incluído em cada exemplo.
|
Imagem |
SO Base |
Observações |
|
|
CentOS 7 |
ABI C++11 padrão (não habilitado) |
|
|
Rocky Linux 8 (compatível com CentOS 8) |
Use esta imagem se precisar da nova ABI C++11 ( |
Dependências de terceiros: Incorpore todas as dependências como código-fonte ou use vinculação estática. A vinculação dinâmica a bibliotecas de terceiros faz com que o operador falhe ao carregar em tempo de execução.
Dependência obrigatória:
abseil-cpp — use a mesma versão do framework FG.
Para detalhes da configuração CMake, consulte o arquivo CMakeLists.txt em cada exemplo para desenvolvedores.
Operadores disponíveis
Os seguintes operadores estão disponíveis como código-fonte e binários pré-compilados:
|
Operador |
Descrição |
Código-fonte |
Binário |
|
EditDistance |
Distância de edição entre duas entradas de texto |
||
|
SeqExpr |
Avaliação de expressão de sequência |
||
|
BPETokenize |
Tokenização Byte Pair Encoding (BPE) |
Incluído na tokenize_feature nativa |
Configuração do EditDistance
|
Parâmetro |
Opções |
Padrão |
|
|
|
|
Para um exemplo de BatchProcess, baixe e revise o RegexReplace.
Próximos passos
Operadores de feature nativos — operadores disponíveis e sintaxe de expressão
expr_featureUsar FG em tarefas offline — como implantar operadores personalizados para tarefas offline
Visão geral e configuração do Feature Generation — schema completo do
fg.jsone referência de discretização