全部產品
Search
文件中心

Artificial Intelligence Recommendation:自訂特徵運算元

更新時間:Jan 30, 2026

自訂特徵運算元能夠以外掛程式的形式被架構動態載入並執行。FG架構盡量保持輕量,僅內建少量常用的特徵運算元,以節省編譯時間、服務資源,加快服務啟動速度。

配置

{
    "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,
    ...
}

除了上述配置項,使用者可以根據需要添加其他配置項,當前配置的JSON字串會傳遞給自訂運算元。

配置項

說明

feature_type

固定為custom_feature

operator_name

特徵運算元註冊的名字,建議與實現的類名保持一致;同一operator可以在多個特徵變換中複用。

operator_lib_file

指定特徵運算元動態庫檔案的名稱,必須以.so結尾。離線任務必填,線上任務可選

  • 線上服務(Torch/EasyRecProcessor)預設會掃描模型檔案中fg.json所在目錄的custom_fg_lib子目錄下的所有動態庫檔案,並載入到記憶體中;

  • 官方提供了少量擴充的運算元,如下文的開發樣本列表中展示的,通過配置operator_lib_file的值為pyfg/lib/libxxx.so來指定;

  • 運行離線任務時需要把該動態庫檔案(非官方提供的)上傳為MaxCompute的同名資源(注意上傳後需要提交資源)。

expression

輸入運算式,支援多輸入。

value_type

特徵變換的輸出類型,只能是基礎類型stringint32int64floatdouble

default_value

特徵預設值,統一以字串格式配置。代碼中自行轉換為需要的類型。

separator

多值分隔字元,用來split配置的default_value;當輸出特徵值是多維時,可以配置多值的預設值。

stub_type

表示當前特徵運算元是否只能作為特徵變換的中間結果,設定為true表示不能用作DAG執行圖的葉子節點。

is_sequence

標記是否是序列特徵。

sequence_length

sequence的最大長度,超過該值時會截斷

sequence_delim

sequence元素之間的分隔字元,僅在輸入為string類型時才需要設定

split_sequence

當sequence輸入特徵的類型為string時,是否需要架構對sequence進行split操作,預設值為true

  • 執行split操作後,當前輸入欄位的類型變成std::vector<std::string>,即使原來是scalar的欄位也是如此

  • 當有多個輸入欄位,有些是序列,有些是標量時,需要謹慎考慮是否需要在架構層做split操作

  • 架構層的split操作會採用 CPU AVX512 指令集,一般情況下效能更好

value_dimension

輸出特徵的維度,可以用來截斷離線任務的輸出結果,影響輸出表的schema;如果是多值特徵且輸出維度不確定,可以不添加該配置。

  • 可選項,預設值為0,可以在離線任務中用來截斷輸出;

  • 值為1並且is_sequence=false時輸出表的schema類型為value_type; 配置了分箱操作時,輸出類型為bigint

  • 值為1並且is_sequence=true時輸出表的schema類型為array<value_type>; 配置了分箱操作時,輸出類型為array<bigint>;

  • 值不為1並且is_sequence=false時輸出表的schema類型為array<value_type>; 配置了分箱操作時,輸出類型為array<bigint>;

  • 值不為1並且is_sequence=true時輸出表的schema類型為array<array<value_type>>; 配置了分箱操作時,輸出類型為array<array<bigint>>;

  • 特殊情況1:按照上述規則,當輸出表的schema類型為array<array<int>>時,強制修改為array<array<bigint>>;

  • 特殊情況2:按照上述規則,當輸出表的schema類型為array<array<double>>時,強制修改為array<array<float>>;

分箱操作

支援6種類型的分箱操作,使用者無需自己實現分箱操作。詳情請參見特徵分箱(離散化)

  • hash_bucket_size:對特徵變換結果進行hash和模數。

  • vocab_list:把特徵變換結果轉化為列表的索引。

  • vocab_dict:把特徵變換結果轉化為字典的值(必須可轉化為int64類型)。

  • vocab_file: 從檔案載入vocab_listvocab_dict

  • boundaries:指定分箱邊界,把特徵變換結果轉化為對應的桶號。

  • num_buckets:直接使用特徵變換結果作為分箱桶號。

normalizer

針對數值型特徵,可以添加該配置對變換結果進一步處理,如計算一個運算式的值。

支援的操作符與函數,請參見內建特徵運算元。支援minmax、zscore、log10、expression共4種架構,配置和計算方法如下:

  • log10

    配置例子:method=log10,threshold=1e-10,default=-10

    計算公式:x = x > threshold ? log10(x) : default;

  • zscore

    配置例子:method=zscore,mean=0.0,standard_deviation=10.0

    計算公式:x = (x - mean) / standard_deviation

  • minmax

    配置例子:method=minmax,min=2.1,max=2.2

    計算公式:x = (x - min) / (max - min)

  • expression

    配置例子:method=expression,expr=sign(x)

    計算公式:可以配置任意的函數或運算式,變數名固定為x,代表運算式的輸入。

placeholder

在序列特徵中,當序列的每個元素有多個值時(value_dimension != 1),自訂運算元開發人員用來填充空位、補齊維度特殊值;

  • 浮點數預設值為NaN;整型預設為對應類型最小的值;

  • FG架構最終會過濾掉這些特殊佔位值;配置分箱操作(稀疏特徵)後輸出鋸齒狀jagged特徵值,不做分箱操作(稠密特徵)時佔位值最終會被替換為特徵預設值(default_value)。

disable_string_view

是否禁用string_view類型的特徵值,預設值為false

  • 當FG被整合到模型推理服務(EasyRecProcessor/TorchEasyRec Processor)中時,item側的string類型的特徵會轉換為string_view類型輸入給FG(效能更好);

  • 為了方便運算元開發人員,可以開啟這個配置開關,FG架構會把string_view類型的特徵值轉換為string類型,傳給自訂OP;

  • 注意:Map類型的資料,當key或者value是string_view類型時無法被轉換,開發人員依然需要處理string_view類型;

  • 注意:開啟這個配置開關後,效能會有損耗。

is_op_thread_safe

表示當前特徵運算元是否是安全執行緒的,設定為true表示安全執行緒,設定為false表示線程不安全

  • 預設為true,表示運算元的開發人員需要保證運算元是安全執行緒的(無狀態,或者只有thread_local的變數)

    • 【推薦】提倡開發人員提供原生的安全執行緒的運算元

  • 如果設為false,架構會為每個線程建立一個對象副本;

    • 相對於原生的安全執行緒的運算元,這種方式會佔用更多的記憶體

補充說明:

  • 使用者自訂的配置項不能與架構使用的配置項同名

    • 自訂運算元可以讀取並使用架構定義的配置項,但試圖改變其語義的行為可能會導致未定義的結果

  • 依賴外部資源檔的配置項需要以_file結尾

    • 在離線任務中使用FG依賴該標記來同步資源檔

配置樣本

{
    "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
},
{
    "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)"
}
  • formula: 運算式,支援的運算式參考expr_feature

    • spherical_distance: 計算兩個經緯度座標的距離,參數為[lng1_seq, lat1_seq, lng2, lat2],前兩個參數是序列,後兩個參數是標量值。

    這是平鋪形式的自訂Sequence特徵樣本,如果需要嵌套格式的自訂Sequence特徵樣本請參考sequence_feature

C++介面

#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 自訂特徵運算元的公用基類
 *
 * 架構會檢測子類有沒有override批量介面`BatchProcess`方法,如果有實現則會調用該方法完成特徵變換;
 * 否則,架構根據`value_type`的配置從一下`ProcessWith*`方法中選擇一個執行,使用者必須實現其中一個對應類型的介面
 */
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";
      }
      // 緩衝到成員裡,保證返回指標有效
      cached_ = "unimplemented method called: " + msg_;
      return cached_.c_str();
    }

   private:
    std::string msg_;
    mutable std::string cached_;
  };

  virtual ~IFeatureOP() = default;

  /**
   * @brief 初始化方法
   * @param feature_config is a json string,
   * @return 如果為0,則表示模型載入成功,否則表示模型載入失敗。
   */
  virtual int Initialize(const string& feature_config) = 0;

  /**
   * @brief 特徵變換,輸出為string類型
   * @param inputs 表示一條記錄,可以有多個欄位(field)
   * @param outputs 特徵變換的輸出
   * @return 狀態代碼,如果為0表示執行成功
   */
  virtual int ProcessWithStrOutputs(const vector<FieldPtr>& inputs,
                                    vector<string>& outputs) {
    throw NotOverriddenException("ProcessWithStrOutputs(FieldPtr)");
  }

  /**
   * @brief 特徵變換,輸出為int32類型
   * @param inputs 表示一條記錄,可以有多個欄位(field)
   * @param outputs 特徵變換的輸出
   * @return 狀態代碼,如果為0表示執行成功
   */
  virtual int ProcessWithInt32Outputs(const vector<FieldPtr>& inputs,
                                      vector<int32>& outputs) {
    throw NotOverriddenException("ProcessWithInt32Outputs(FieldPtr)");
  }

  /**
   * @brief 特徵變換,輸出為int64類型
   * @param inputs 表示一條記錄,可以有多個欄位(field)
   * @param outputs 特徵變換的輸出
   * @return 狀態代碼,如果為0表示執行成功
   */
  virtual int ProcessWithInt64Outputs(const vector<FieldPtr>& inputs,
                                      vector<int64>& outputs) {
    throw NotOverriddenException("ProcessWithInt64Outputs(FieldPtr)");
  }

  /**
   * @brief 特徵變換,輸出為float類型
   * @param inputs 表示一條記錄,可以有多個欄位(field)
   * @param outputs 特徵變換的輸出
   * @return 狀態代碼,如果為0表示執行成功
   */
  virtual int ProcessWithFloatOutputs(const vector<FieldPtr>& inputs,
                                      vector<float>& outputs) {
    throw NotOverriddenException("ProcessWithFloatOutputs(FieldPtr)");
  }

  /**
   * @brief 特徵變換,輸出為double類型
   * @param inputs 表示一條記錄,可以有多個欄位(field)
   * @param outputs 特徵變換的輸出
   * @return 狀態代碼,如果為0表示執行成功
   */
  virtual int ProcessWithDoubleOutputs(const vector<FieldPtr>& inputs,
                                       vector<double>& outputs) {
    throw NotOverriddenException("ProcessWithDoubleOutputs(FieldPtr)");
  }

  /**
   * @brief 可選,處理多個records的批量介面
   *
   * @param inputs 輸入column的vector,VariantVector表示一個特徵column
   * @param outputs
   * 輸出,變換後的特徵;支援輸出複雜類型,可作為其他的特徵變換的輸入
   * @return 狀態代碼,如果為0表示執行成功
   */
  virtual int BatchProcess(const vector<VariantVector>& inputs,
                           VariantVector& outputs) {
    throw NotOverriddenException("BatchProcess");
  }

  /**
   * @brief 用於顯式聲明子類是否實現了BatchProcess方法
   *
   * 架構會優先調用此方法來檢測BatchProcess是否被重寫。
   * 如果子類實現了BatchProcess,應該override此方法並返回true。
   * 預設返回false表示未實現BatchProcess。
   *
   * 注意:此方法用於避免跨動態庫邊界的異常傳播問題。
   * 當自訂運算元(.so檔案)與主程式使用不同的C++ ABI或編譯選項時,
   * 通過調用BatchProcess並捕獲異常的方式來檢測可能會失敗。
   *
   * @return true 表示子類實現了BatchProcess
   * @return false 表示子類未實現BatchProcess(預設)
   */
  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

開發指南

  • 下載依賴API代碼檔案 fg-api.tar.gz,包含必要的標頭檔等。

  • 您需要繼承基類IFeatureOP,實現Initialize方法,同時至少實現一個ProcessWith*方法。

  • 您的實作類別必須包含一個無參建構函式。

  • 架構會把配置的JSON字串傳遞給Initialize方法,您自行解析需要的配置項。

  • 架構會根據value_type配置項調用對應的ProcessWith*方法,如果您未實現對應類型的方法,會拋出運行時異常。

    • ProcessWith*方法僅需要處理一條記錄,可以有多個輸入欄位,也可有多維輸出(比如多值特徵)。

    • VariantRecord定義了所有可以被架構處理的特徵field類型。

    • 您的代碼需要儘可能支援每種類型,即對每種可能的輸入類型實現相應的特徵變換操作,除非確定某些類型確實不需要用到,這種情況可直接拋異常。

    • FSMAP是使用featurestore時需要支援的類型,可大幅提高Processor的效能。

  • 您僅需要實現分箱操作前的特徵變換操作,如果有配置分箱操作,架構會自動執行分箱操作。

  • 您需要使用REGISTER_PLUGIN宏註冊新開發的特徵OP,否則架構無法使用。

    • REGISTER_PLUGIN("OperatorName", OperatorClass);兩個宏參數根據需要替換,建議保持一致

    • 配置項中的operator_name就是這裡的"OperatorName",需要保持一致。

    • 在實現檔案而不是標頭檔中註冊OP

  • 架構會掃描一個指定目錄下的所有動態庫,並在必要時嘗試載入其中需要用到的特徵運算元。

    • 通過環境變數FEATURE_OPERATOR_DIR指定動態庫檔案所在的目錄。

    • 每個動態庫裡可以包含多個特徵運算元的實現。

  • 批處理介面BatchProcess支援一次處理一個批量的資料

    • 可選介面,如果實現了該介面,則FG架構不會再調用樣本粒度的介面ProcessWith*

    • 當實現了該介面後,需要同步override bool HasBatchProcessImpl() const這個函數,並返回true,告訴主程式調用該介面;

    • 實現該介面有機會提供更高的效能,比如,user-side的特徵在一次請求中只有1條樣本,對於交叉特徵,可通過廣播機制避免重複解析user側特徵;

    • 當配置了stub_type=true並且沒有配置分箱操作時,該介面可返回所有合法的類型,比如Map類型;

    • BatchProcess函數返回的VariantVector的具體類型需要根據is_sequencevalue_dimensionvalue_type的值來決定;具體參考上文中value_dimension配置項的描述;

    • 批處理介面的樣本 RegexReplace,請下載後查閱。

  • 依賴的三方庫

    • abseil-cpp(推薦使用與FG架構相同的版本)

    • 自訂運算元依賴的三方庫只能以嵌入原始碼或者靜態連結的方式編譯,不能依賴任何動態連結程式庫(會導致運算元載入失敗)

序列特徵

如果配置項is_sequence設定為true,有以下注意事項:

  • 稀疏特徵序列

    • 當運算元產生的是一個稀疏特徵序列,如歷史訪問過的item_id的序列,並且序列的每個元素都是單值,此時可以輸出任意類型。

    • 當運算元產生的是一個稀疏特徵序列,並且序列的每個元素可能是多值時,只能輸出string類型(value_type必須設為string),多值使用分隔字元chr(29)隔開。

  • 稠密特徵序列

    • 當運算元產生的是一個稀疏特徵序列,如歷史訪問過的物品的embedding向量,此時需要配置value_dimension,值為序列的每個元素的維度。

    • 序列的元素是標量(scalar)時,value_dimension設定為1。

    • 序列的元素是向量(vector)時,value_dimension設定為向量的長度。

    • 運算元輸出的特徵值數量必須是value_dimension的整數倍。

自訂運算元列表

運算元名稱

運算元功能

源碼下載連結

二進位包下載連結

EditDistance

編輯距離

下載連結

點擊下載

SeqExpr

序列運算式

下載連結

點擊下載

BPETokenize

BPE分詞

下載連結

已包含在內建tokenize_feature

配置項

  • EditDistance

    • encoding: 輸入文本的編碼,可選:utf-8latin,預設值為latin

開發樣本

下面以計算兩個輸入文本的編輯距離為例,標頭檔為edit_distance.h

#pragma once
#include "api/base_op.h"

namespace fg {
namespace functor {
  class EditDistanceFunctor;
}

using std::string;
using std::vector;


/**
 * @brief 編輯距離:輸入兩個字串,輸出是它們的文本編輯距離
 */
class EditDistance : public IFeatureOP {
 public:
  int Initialize(const string& feature_config) override;

  /// @return 狀態代碼,如果為0表示執行成功
  int ProcessWithStrOutputs(const vector<FieldPtr>& inputs,
                            vector<string>& outputs) override;

  /// @return 狀態代碼,如果為0表示執行成功
  int ProcessWithInt32Outputs(const vector<FieldPtr>& inputs,
                              vector<int32>& outputs) override;

  /// @return 狀態代碼,如果為0表示執行成功
  int ProcessWithInt64Outputs(const vector<FieldPtr>& inputs,
                              vector<int64>& outputs) override;

  /// @return 狀態代碼,如果為0表示執行成功
  int ProcessWithFloatOutputs(const vector<FieldPtr>& inputs,
                              vector<float>& outputs) override;

  /// @return 狀態代碼,如果為0表示執行成功
  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

實現檔案為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 unsupport 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 類
template <class... Ts>
struct overloaded : Ts... {
  using Ts::operator()...;
};
// 類模板參數推導指引(C++17)
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;
}

int EditDistance::ProcessWithInt64Outputs(const vector<FieldPtr>& inputs,
                                          vector<int64>& outputs) {
  vector<int32> distances;
  int status = ProcessWithInt32Outputs(inputs, distances);
  if (0 != status) {
    return status;
  }
  outputs.clear();
  outputs.insert(outputs.end(), 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.clear();
  outputs.insert(outputs.end(), 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.clear();
  outputs.insert(outputs.end(), 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.clear();
  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);

下載上述表格中的原始碼,執行build.sh指令碼編譯產生FG運算元。

編譯自訂運算元

需要與FG架構保持相同的編譯環境,比如語言標準(C++17)、編譯選項等。推薦使用官方提供的編譯鏡像,可以在build.sh指令碼中查看。

  • 編譯環境鏡像(CentOS7):mybigpai-public-registry.cn-beijing.cr.aliyuncs.com/easyrec/feature_generator:centos7-0.1.1

  • 編譯環境鏡像(rockylinux:8,相容CentOS8):mybigpai-public-registry.cn-beijing.cr.aliyuncs.com/easyrec/feature_generator:0.1.1

  • 預設情況下不使用C++11的ABI,如果需要使用新版ABI,即設定_GLIBCXX_USE_CXX11_ABI=1,則只能使用以rockylinux:8為基礎鏡像的第二個鏡像(tag: 0.1.1

  • 務必注意自訂運算元不可以用動態連結的方式連結三方庫,可以使用靜態連結或者拷貝源碼到專案中的方式編譯。

具體可以查看開發樣本中的CMakeLists.txt檔案。