上篇传送门:基于OpenHarmony的智能相册应用开发(1)

有了Tokenizer后,我们即可以对用户输入的文本进行编码:

torch::Tensor ClipPreprocessor::preprocessText(const std::string &text) {
    std::vector<size_t> all_tokens;

    // tokenize the text
    all_tokens.emplace_back(tokenizer_->getVocabId(L"[CLS]"));
    auto ids = tokenizer_->convertTokensToIds(tokenizer_->tokenize(text));
    if (ids.size() > text_length - 2) {
        ids.resize(text_length - 2);
    }
    all_tokens.insert(all_tokens.end(), ids.begin(), ids.end());
    all_tokens.emplace_back(tokenizer_->getVocabId(L"[SEP]"));

    // convert all_tokens to tensor
    auto tensor_text = torch::zeros({1, text_length}, torch::kInt32);
    for (auto i = 0; i < all_tokens.size(); i++) {
        tensor_text[0][i] = all_tokens[i];
    }

    // std::cout << "tensor: " << tensor_text << std::endl;

    return tensor_text;
}

这段代码定义了一个名为 preprocessText 的函数,它属于 ClipPreprocessor 类。这个函数的目的是将输入的文本字符串转换为一个 PyTorch 张量(Tensor),该张量可以用于后续的模型输入中。让我们详细解释一下这段代码的每一步:

函数签名

torch::Tensor ClipPreprocessor::preprocessText(const std::string &text)
  • torch::Tensor:函数返回一个 PyTorch 张量。
  • ClipPreprocessor:函数属于 ClipPreprocessor 类。
  • preprocessText:函数名称。
  • const std::string &text:函数接受一个常量引用的字符串作为输入。

函数内部逻辑

  1. 定义一个存储所有 token ID 的向量
       cpp    std::vector<size_t> all_tokens;    

  2. 添加特殊的起始 token [CLS]
       cpp    all_tokens.emplace_back(tokenizer_->getVocabId(L"[CLS]"));    
       - 使用 tokenizer_ 获取 [CLS] token 的词汇表 ID,并将其添加到 all_tokens 中。

  3. 将输入文本进行分词并转换为 token ID
       cpp    auto ids = tokenizer_->convertTokensToIds(tokenizer_->tokenize(text));    
       - tokenizer_->tokenize(text) 将输入文本 text 分词。
       - tokenizer_->convertTokensToIds(...) 将这些分词转换为对应的词汇表 ID。

  4. 截断 token ID 列表以适应最大长度
       cpp    if (ids.size() > text_length - 2) {        ids.resize(text_length - 2);    }    
       - 如果分词后的 token ID 数量超过了 text_length - 2(因为要预留给 [CLS][SEP]),则截断到合适长度。

  5. 添加分词后的 token ID 到 all_tokens
       cpp    all_tokens.insert(all_tokens.end(), ids.begin(), ids.end());    

  6. 添加特殊的结束 token [SEP]
       cpp    all_tokens.emplace_back(tokenizer_->getVocabId(L"[SEP]"));    

  7. 将 token ID 转换为 PyTorch 张量
       cpp    auto tensor_text = torch::zeros({1, text_length}, torch::kInt32);    for (auto i = 0; i < all_tokens.size(); i++) {        tensor_text[0][i] = all_tokens[i];    }    
       - 创建一个大小为 [1, text_length] 的张量 tensor_text,初始值为 0,数据类型为 torch::kInt32
       - 将 all_tokens 中的值逐个赋值给 tensor_text

  8. 返回处理后的张量
       cpp    return tensor_text;    

处理完token后,即可以调用模型进行文本编码,如getTextFeature函数所示:

torch::Tensor ClipInferEngine::getTextFeature(const std::string& text) {
    auto image_tensor = torch::zeros({1, 3, ClipPreprocessor::image_size, ClipPreprocessor::image_size});
    auto text_tensor = preprocessor_.preprocessText(text);
    
    std::vector<torch::jit::IValue> inputs;
    inputs.push_back(image_tensor);
    inputs.push_back(text_tensor);
    auto outputs = model_.forward(inputs).toTuple()->elements();
    assert(outputs.size() == 3);
    return outputs[1].toTensor();
}

为了能在ArkTs代码内调用模型相关的推理、Tokenizer接口,我们需要定义napi(Node-API)接口来进行ArkTs和C++的交互:

// 文字转换成Feature
static napi_value GetTextFeature(napi_env env, napi_callback_info info) {
    size_t requireArgc = 1;
    size_t argc = 1;
    napi_value args[1] = {nullptr};
    
    napi_get_cb_info(env, info, &argc, args , nullptr, nullptr);
    
    napi_valuetype valuetype0;
    napi_typeof(env, args[0], &valuetype0);
    // 保证valuetype0 是字符串类型,进行相应的操作
    assert(valuetype0 == napi_string);
    const size_t MAX_SIZE = 256;
    char buf[MAX_SIZE];
    size_t size = 0;
    napi_get_value_string_utf8(env, args[0], buf, MAX_SIZE, &size);
    std::string text(buf, size);
    auto text_feature = CLIP::ClipEngineHelper::GetTextFeature(text);
    
    return ConvertVectorToJSArray(env, text_feature);
}

这段代码的主要功能是:从 JavaScript 获取一个字符串参数,再将这个字符串转换为 C++ 字符串。调用 CLIP::ClipEngineHelper::GetTextFeature 获取该字符串的特征向量,将特征向量转换为 ArkTs数组并返回。

之后通过napi_module_register函数注册C++模块

EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
    napi_property_descriptor desc[] = {
        {"initialize", nullptr, Initialize, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"getTextFeature", nullptr, GetTextFeature, nullptr, nullptr, nullptr, napi_default, nullptr}
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END
static napi_module clipNativeModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "clipnative",
    .nm_priv = ((void*)0),
    .reserved = { 0 },
};

extern "C" __attribute__((constructor)) void RegisterEntryModule(void)
{
    napi_module_register(&clipNativeModule);
}

(未完待续)

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐