mirror of
https://github.com/opencv/opencv.git
synced 2026-07-21 19:33:03 +04:00
Merge pull request #28837 from varun-jaiswal17:gemma3-tokenizer
Add Gemma3 tokenizer support for dnn #28837 - Adds Gemma3 tokenizer support - Implements character-level BPE - Adds 6 tests covering English, phrase, mixed case, numbers, special tokens, and encode/decode - add gemma3_inference.py Merge with: - **Companion PR** : https://github.com/opencv/opencv_extra/pull/1346 - forward pass bug in gemma3_inference.py : https://github.com/opencv/opencv/pull/28836 ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [x] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [x] The feature is well documented and sample code can be built with the project CMake
This commit is contained in:
@@ -527,7 +527,33 @@ public:
|
||||
case 't' : { buf[i++] = '\t'; break; }
|
||||
case 'b' : { buf[i++] = '\b'; break; }
|
||||
case 'f' : { buf[i++] = '\f'; break; }
|
||||
case 'u' : { CV_PARSE_ERROR_CPP( "'\\uXXXX' currently not supported" ); break; }
|
||||
case 'u' : {
|
||||
if (i + 4 >= CV_FS_MAX_LEN)
|
||||
CV_PARSE_ERROR_CPP("string is too long");
|
||||
ptr++;
|
||||
uint32_t codepoint = 0;
|
||||
for (int k = 0; k < 4; k++, ptr++) {
|
||||
char hex = *ptr;
|
||||
uint32_t digit = 0;
|
||||
if (hex >= '0' && hex <= '9') digit = (uint32_t)(hex - '0');
|
||||
else if (hex >= 'a' && hex <= 'f') digit = (uint32_t)(hex - 'a') + 10u;
|
||||
else if (hex >= 'A' && hex <= 'F') digit = (uint32_t)(hex - 'A') + 10u;
|
||||
else CV_PARSE_ERROR_CPP("invalid \\uXXXX escape sequence");
|
||||
codepoint = (codepoint << 4) | digit;
|
||||
}
|
||||
if (codepoint < 0x80) {
|
||||
buf[i++] = (char)codepoint;
|
||||
} else if (codepoint < 0x800) {
|
||||
buf[i++] = (char)(0xC0 | (codepoint >> 6));
|
||||
buf[i++] = (char)(0x80 | (codepoint & 0x3F));
|
||||
} else {
|
||||
buf[i++] = (char)(0xE0 | (codepoint >> 12));
|
||||
buf[i++] = (char)(0x80 | ((codepoint >> 6) & 0x3F));
|
||||
buf[i++] = (char)(0x80 | (codepoint & 0x3F));
|
||||
}
|
||||
beg = ptr;
|
||||
continue;
|
||||
}
|
||||
default : { CV_PARSE_ERROR_CPP( "Invalid escape character" ); }
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1685,6 +1685,53 @@ TEST(Core_InputOutput, FileStorage_json_bool)
|
||||
fs.release();
|
||||
}
|
||||
|
||||
TEST(Core_InputOutput, FileStorage_json_unicode_escape)
|
||||
{
|
||||
// Test \uXXXX Unicode escape sequences in JSON strings
|
||||
std::string test = R"({
|
||||
"ascii": "\u0041\u0042\u0043",
|
||||
"copyright": "\u00A9",
|
||||
"chinese": "\u4E2D",
|
||||
"emoji_base": "\u263A",
|
||||
"mixed": "Hello \u4E16\u754C"
|
||||
})";
|
||||
FileStorage fs(test, FileStorage::READ | FileStorage::MEMORY);
|
||||
|
||||
// Test ASCII characters (\u0041=A, \u0042=B, \u0043=C)
|
||||
ASSERT_TRUE(fs["ascii"].isString());
|
||||
ASSERT_EQ((std::string)fs["ascii"], "ABC");
|
||||
|
||||
// Test 2-byte UTF-8 character (\u00A9=©)
|
||||
ASSERT_TRUE(fs["copyright"].isString());
|
||||
std::string copyright_str = (std::string)fs["copyright"];
|
||||
ASSERT_EQ(copyright_str.size(), 2u); // © is 2 bytes in UTF-8
|
||||
ASSERT_EQ((unsigned char)copyright_str[0], 0xC2);
|
||||
ASSERT_EQ((unsigned char)copyright_str[1], 0xA9);
|
||||
|
||||
// Test 3-byte UTF-8 character (\u4E2D=中)
|
||||
ASSERT_TRUE(fs["chinese"].isString());
|
||||
std::string chinese_str = (std::string)fs["chinese"];
|
||||
ASSERT_EQ(chinese_str.size(), 3u); // 中 is 3 bytes in UTF-8
|
||||
ASSERT_EQ((unsigned char)chinese_str[0], 0xE4);
|
||||
ASSERT_EQ((unsigned char)chinese_str[1], 0xB8);
|
||||
ASSERT_EQ((unsigned char)chinese_str[2], 0xAD);
|
||||
|
||||
// Test another 3-byte character (\u263A=☺)
|
||||
ASSERT_TRUE(fs["emoji_base"].isString());
|
||||
std::string emoji_str = (std::string)fs["emoji_base"];
|
||||
ASSERT_EQ(emoji_str.size(), 3u);
|
||||
ASSERT_EQ((unsigned char)emoji_str[0], 0xE2);
|
||||
ASSERT_EQ((unsigned char)emoji_str[1], 0x98);
|
||||
ASSERT_EQ((unsigned char)emoji_str[2], 0xBA);
|
||||
|
||||
// Test mixed ASCII and Unicode
|
||||
ASSERT_TRUE(fs["mixed"].isString());
|
||||
std::string mixed = (std::string)fs["mixed"];
|
||||
ASSERT_EQ(mixed.substr(0, 6), "Hello "); // First 6 chars are "Hello "
|
||||
|
||||
fs.release();
|
||||
}
|
||||
|
||||
TEST(Core_InputOutput, FileStorage_free_file_after_exception)
|
||||
{
|
||||
const std::string fileName = cv::tempfile("FileStorage_free_file_after_exception_test.yml");
|
||||
|
||||
@@ -65,8 +65,9 @@ public:
|
||||
class ONNXGraphWrapper : public ImportGraphWrapper
|
||||
{
|
||||
public:
|
||||
ONNXGraphWrapper(opencv_onnx::GraphProto& _net) : net(_net)
|
||||
ONNXGraphWrapper(opencv_onnx::GraphProto& _net, const std::string& _basePath = "") : net(_net)
|
||||
{
|
||||
basePath = _basePath;
|
||||
// Add a fake initializer with empty name.
|
||||
// Some ONNX models skip their inputs. For example,
|
||||
// Resize which has 4 inputs but 2 of them have empty names.
|
||||
@@ -129,7 +130,7 @@ public:
|
||||
Mat getMatFromInitializer(int idx)
|
||||
{
|
||||
const opencv_onnx::TensorProto& tensor_proto = net.initializer(idx);
|
||||
return getMatFromTensor(tensor_proto);
|
||||
return getMatFromTensor(tensor_proto, false, basePath);
|
||||
}
|
||||
|
||||
std::string getNameOfInitializer(int idx) const
|
||||
@@ -176,6 +177,9 @@ public:
|
||||
private:
|
||||
int numInputs, numInitializers;
|
||||
opencv_onnx::GraphProto& net;
|
||||
|
||||
public:
|
||||
std::string basePath;
|
||||
};
|
||||
|
||||
static Mat extractConstant(const Ptr<ImportGraphWrapper>& net, int node_id, int input_id)
|
||||
@@ -193,7 +197,7 @@ static Mat extractConstant(const Ptr<ImportGraphWrapper>& net, int node_id, int
|
||||
Ptr<ImportNodeWrapper> constant_ptr = net->getNode(constant_id);
|
||||
opencv_onnx::NodeProto* constant_node = constant_ptr.dynamicCast<ONNXNodeWrapper>()->node;
|
||||
opencv_onnx::TensorProto constant_proto = constant_node->attribute(0).t();
|
||||
return getMatFromTensor(constant_proto);
|
||||
return getMatFromTensor(constant_proto, false, onnx_net->basePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1725,7 +1729,7 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
void simplifySubgraphs(opencv_onnx::GraphProto& net)
|
||||
void simplifySubgraphs(opencv_onnx::GraphProto& net, const std::string& basePath)
|
||||
{
|
||||
std::vector<Ptr<Subgraph> > subgraphs;
|
||||
subgraphs.push_back(makePtr<BiasedMatmulSubgraph>());
|
||||
@@ -1762,7 +1766,7 @@ void simplifySubgraphs(opencv_onnx::GraphProto& net)
|
||||
subgraphs.push_back(makePtr<AttentionSingleHeadSubGraph>());
|
||||
}
|
||||
|
||||
simplifySubgraphs(Ptr<ImportGraphWrapper>(new ONNXGraphWrapper(net)), subgraphs);
|
||||
simplifySubgraphs(Ptr<ImportGraphWrapper>(new ONNXGraphWrapper(net, basePath)), subgraphs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
namespace cv { namespace dnn {
|
||||
CV__DNN_INLINE_NS_BEGIN
|
||||
|
||||
void simplifySubgraphs(opencv_onnx::GraphProto& net);
|
||||
void simplifySubgraphs(opencv_onnx::GraphProto& net, const std::string& basePath = "");
|
||||
|
||||
template<typename T1, typename T2>
|
||||
void convertInt64ToInt32(const T1& src, T2& dst, int size)
|
||||
|
||||
@@ -520,7 +520,7 @@ LayerParams ONNXImporter2::getLayerParams(const opencv_onnx::NodeProto& node_pro
|
||||
else if (attribute_proto.has_t())
|
||||
{
|
||||
opencv_onnx::TensorProto tensor = attribute_proto.t();
|
||||
Mat blob = getMatFromTensor2(tensor);
|
||||
Mat blob = parseTensor(tensor);
|
||||
lp.blobs.push_back(blob);
|
||||
lp.set("original_dims_of_mat", tensor.dims_size());
|
||||
}
|
||||
@@ -774,7 +774,7 @@ Mat ONNXImporter2::parseTensor(const opencv_onnx::TensorProto& tensor_proto)
|
||||
Ptr<Graph> ONNXImporter2::parseGraph(opencv_onnx::GraphProto* graph_proto, bool mainGraph_)
|
||||
{
|
||||
CV_LOG_DEBUG(NULL, "DNN/ONNX: parsing graph '" << graph_proto->name() << "' of " << graph_proto->node_size() << " nodes");
|
||||
simplifySubgraphs(*graph_proto);
|
||||
simplifySubgraphs(*graph_proto, onnxBasePath);
|
||||
int n_nodes = graph_proto->node_size();
|
||||
CV_LOG_DEBUG(NULL, "DNN/ONNX: simplified the graph to " << n_nodes << " nodes");
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ std::vector<std::pair<std::size_t, std::uint32_t>> bytePairMerge(const ByteVecRa
|
||||
std::vector<std::uint32_t> bytePairEncode(const std::vector<std::uint8_t>& piece,
|
||||
const ByteVecRankMap& ranks) {
|
||||
|
||||
if (piece.size() == 1) {
|
||||
if (piece.size() == 1u) {
|
||||
auto it = ranks.find(piece);
|
||||
return it == ranks.end() ? std::vector<std::uint32_t>{} : std::vector<std::uint32_t>{it->second};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
// This file is part of OpenCV project.
|
||||
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
// of this distribution and at http://opencv.org/license.html.
|
||||
// Copyright (C) 2025, BigVision LLC, all rights reserved.
|
||||
// Third party copyrights are property of their respective owners.
|
||||
|
||||
#ifndef __OPENCV_DNN_TOKENIZER_CORE_GEMMA_HPP__
|
||||
#define __OPENCV_DNN_TOKENIZER_CORE_GEMMA_HPP__
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace cv { namespace dnn {
|
||||
|
||||
static const std::string GEMMA_METASPACE = "\xe2\x96\x81"; // UTF-8 for ▁ (U+2581)
|
||||
|
||||
struct CoreGemmaBPE {
|
||||
std::unordered_map<std::string, int> pieceToId;
|
||||
std::vector<std::string> idToPiece;
|
||||
std::unordered_map<std::string, uint32_t> mergeRanks; // key = piece_a + '\0' + piece_b
|
||||
std::unordered_map<std::string, int> specialToId;
|
||||
std::unordered_map<int, std::string> idToSpecial;
|
||||
|
||||
void addMerge(const std::string& a, const std::string& b, uint32_t rank) {
|
||||
mergeRanks[a + '\0' + b] = rank;
|
||||
}
|
||||
|
||||
static std::string normalize(const std::string& text) {
|
||||
std::string out;
|
||||
out.reserve(text.size() + text.size() / 4);
|
||||
for (char c : text) {
|
||||
if (c == ' ')
|
||||
out += GEMMA_METASPACE;
|
||||
else
|
||||
out += c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::vector<std::string> splitUtf8Chars(const std::string& text) {
|
||||
std::vector<std::string> out;
|
||||
out.reserve(text.size());
|
||||
size_t i = 0;
|
||||
while (i < text.size()) {
|
||||
unsigned char c = static_cast<unsigned char>(text[i]);
|
||||
int len;
|
||||
if (c < 0x80) len = 1;
|
||||
else if (c < 0xE0) len = 2;
|
||||
else if (c < 0xF0) len = 3;
|
||||
else len = 4;
|
||||
if (i + static_cast<size_t>(len) > text.size()) len = 1;
|
||||
out.push_back(text.substr(i, len));
|
||||
i += static_cast<size_t>(len);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::vector<std::string> byteFallback(const std::string& ch) {
|
||||
std::vector<std::string> out;
|
||||
out.reserve(ch.size());
|
||||
char buf[8];
|
||||
for (unsigned char b : ch) {
|
||||
std::snprintf(buf, sizeof(buf), "<0x%02X>", static_cast<unsigned>(b));
|
||||
out.push_back(buf);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<int> encodePiece(const std::string& text) const {
|
||||
if (text.empty()) return {};
|
||||
|
||||
std::vector<std::string> pieces;
|
||||
for (const std::string& ch : splitUtf8Chars(text)) {
|
||||
if (pieceToId.count(ch)) {
|
||||
pieces.push_back(ch);
|
||||
} else {
|
||||
for (const std::string& fb : byteFallback(ch))
|
||||
pieces.push_back(fb);
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr uint32_t RANK_INF = std::numeric_limits<uint32_t>::max();
|
||||
while (pieces.size() > 1) {
|
||||
uint32_t best_rank = RANK_INF;
|
||||
int best_i = -1;
|
||||
|
||||
for (int i = 0; i + 1 < static_cast<int>(pieces.size()); ++i) {
|
||||
std::string key;
|
||||
key.reserve(pieces[i].size() + 1 + pieces[i + 1].size());
|
||||
key = pieces[i];
|
||||
key += '\0';
|
||||
key += pieces[i + 1];
|
||||
|
||||
auto it = mergeRanks.find(key);
|
||||
if (it != mergeRanks.end() && it->second < best_rank) {
|
||||
best_rank = it->second;
|
||||
best_i = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (best_i == -1) break;
|
||||
|
||||
pieces[best_i] += pieces[best_i + 1];
|
||||
pieces.erase(pieces.begin() + best_i + 1);
|
||||
}
|
||||
|
||||
std::vector<int> ids;
|
||||
ids.reserve(pieces.size());
|
||||
for (const std::string& p : pieces) {
|
||||
auto it = pieceToId.find(p);
|
||||
if (it != pieceToId.end()) {
|
||||
ids.push_back(it->second);
|
||||
} else {
|
||||
for (const std::string& fb : byteFallback(p)) {
|
||||
auto it2 = pieceToId.find(fb);
|
||||
if (it2 != pieceToId.end())
|
||||
ids.push_back(it2->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
std::vector<int> encode(const std::string& text,
|
||||
const std::unordered_set<std::string>& allowedSpecial = {}) const {
|
||||
std::vector<int> result;
|
||||
size_t pos = 0;
|
||||
|
||||
while (pos < text.size()) {
|
||||
bool foundSpecial = false;
|
||||
for (const auto& kv : specialToId) {
|
||||
const std::string& sp = kv.first;
|
||||
if (!allowedSpecial.count(sp)) continue;
|
||||
if (text.compare(pos, sp.size(), sp) == 0) {
|
||||
result.push_back(kv.second);
|
||||
pos += sp.size();
|
||||
foundSpecial = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (foundSpecial) continue;
|
||||
|
||||
size_t chunkStart = pos;
|
||||
while (pos < text.size()) {
|
||||
bool atSpecial = false;
|
||||
for (const auto& kv : specialToId) {
|
||||
if (allowedSpecial.count(kv.first) &&
|
||||
text.compare(pos, kv.first.size(), kv.first) == 0) {
|
||||
atSpecial = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (atSpecial) break;
|
||||
++pos;
|
||||
}
|
||||
|
||||
std::string chunk = text.substr(chunkStart, pos - chunkStart);
|
||||
std::string norm = normalize(chunk);
|
||||
auto ids = encodePiece(norm);
|
||||
result.insert(result.end(), ids.begin(), ids.end());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string decode(const std::vector<int>& ids) const {
|
||||
std::string raw;
|
||||
raw.reserve(ids.size() * 4);
|
||||
for (int id : ids) {
|
||||
auto sp = idToSpecial.find(id);
|
||||
if (sp != idToSpecial.end()) {
|
||||
raw += sp->second;
|
||||
continue;
|
||||
}
|
||||
if (id >= 0 && id < static_cast<int>(idToPiece.size()))
|
||||
raw += idToPiece[id];
|
||||
}
|
||||
|
||||
std::string out;
|
||||
out.reserve(raw.size());
|
||||
size_t i = 0;
|
||||
while (i < raw.size()) {
|
||||
if (raw[i] == '<' && i + 5 < raw.size() &&
|
||||
raw[i + 1] == '0' && raw[i + 2] == 'x' &&
|
||||
raw[i + 5] == '>') {
|
||||
char hi = raw[i + 3], lo = raw[i + 4];
|
||||
auto hexVal = [](char c) -> int {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
return -1;
|
||||
};
|
||||
int hv = hexVal(hi), lv = hexVal(lo);
|
||||
if (hv >= 0 && lv >= 0) {
|
||||
out += static_cast<char>((hv << 4) | lv);
|
||||
i += 6;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (i + 2 < raw.size() &&
|
||||
static_cast<unsigned char>(raw[i]) == 0xE2 &&
|
||||
static_cast<unsigned char>(raw[i + 1]) == 0x96 &&
|
||||
static_cast<unsigned char>(raw[i + 2]) == 0x81) {
|
||||
out += ' ';
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
out += raw[i++];
|
||||
}
|
||||
|
||||
if (!out.empty() && out[0] == ' ')
|
||||
out.erase(0, 1);
|
||||
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
}} // namespace cv::dnn
|
||||
#endif // __OPENCV_DNN_TOKENIZER_CORE_GEMMA_HPP__
|
||||
@@ -6,8 +6,10 @@
|
||||
#include "utils.hpp"
|
||||
#include "unicode.hpp"
|
||||
#include "core_bpe.hpp"
|
||||
#include "core_gemma.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace cv { namespace dnn {
|
||||
CV__DNN_INLINE_NS_BEGIN
|
||||
@@ -54,6 +56,97 @@ struct BpeTokenizerImpl : public Tokenizer::Impl {
|
||||
}
|
||||
};
|
||||
|
||||
struct GemmaBpeTokenizerImpl : public Tokenizer::Impl {
|
||||
CoreGemmaBPE model;
|
||||
std::unordered_set<std::string> allowedSpecial;
|
||||
|
||||
explicit GemmaBpeTokenizerImpl(CoreGemmaBPE m,
|
||||
std::unordered_set<std::string> special = {})
|
||||
: model(std::move(m)), allowedSpecial(std::move(special)) {}
|
||||
|
||||
std::vector<int> encode(const std::string& text) override {
|
||||
return model.encode(text, allowedSpecial);
|
||||
}
|
||||
|
||||
std::string decode(const std::vector<int>& tokens) override {
|
||||
return model.decode(tokens);
|
||||
}
|
||||
};
|
||||
|
||||
static Ptr<GemmaBpeTokenizerImpl> buildGemmaFromJson(
|
||||
const std::string& json_path,
|
||||
std::unordered_set<std::string>* outSpecial = nullptr) {
|
||||
|
||||
cv::FileStorage fs(json_path, cv::FileStorage::READ | cv::FileStorage::FORMAT_JSON);
|
||||
if (!fs.isOpened())
|
||||
CV_Error(cv::Error::StsError, "Failed to open tokenizer.json: " + json_path);
|
||||
|
||||
cv::FileNode model_node = fs["model"];
|
||||
CV_CheckFalse(model_node.empty(), "tokenizer.json missing 'model'");
|
||||
|
||||
std::string model_type;
|
||||
model_node["type"] >> model_type;
|
||||
if (model_type != "BPE")
|
||||
CV_Error(cv::Error::StsError,
|
||||
"Expected BPE model in tokenizer.json for Gemma3, got: " + model_type);
|
||||
|
||||
CoreGemmaBPE gemma;
|
||||
|
||||
cv::FileNode vocab_node = model_node["vocab"];
|
||||
CV_CheckFalse(vocab_node.empty(), "tokenizer.json model missing 'vocab'");
|
||||
|
||||
int maxId = -1;
|
||||
for (auto it = vocab_node.begin(); it != vocab_node.end(); ++it) {
|
||||
cv::FileNode entry = *it;
|
||||
std::string piece = entry.name();
|
||||
int id = (int)entry;
|
||||
if (id > maxId) maxId = id;
|
||||
gemma.pieceToId[piece] = id;
|
||||
}
|
||||
|
||||
gemma.idToPiece.resize(maxId + 1);
|
||||
for (const auto& kv : gemma.pieceToId)
|
||||
gemma.idToPiece[kv.second] = kv.first;
|
||||
|
||||
cv::FileNode merges_node = model_node["merges"];
|
||||
if (!merges_node.empty()) {
|
||||
uint32_t rank = 0;
|
||||
for (auto it = merges_node.begin(); it != merges_node.end(); ++it) {
|
||||
cv::FileNode entry = *it;
|
||||
if (static_cast<int>(entry.size()) != 2) {
|
||||
++rank;
|
||||
continue;
|
||||
}
|
||||
std::string a, b;
|
||||
entry[0] >> a;
|
||||
entry[1] >> b;
|
||||
gemma.addMerge(a, b, rank);
|
||||
++rank;
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> special;
|
||||
cv::FileNode added = fs["added_tokens"];
|
||||
if (!added.empty()) {
|
||||
for (auto it = added.begin(); it != added.end(); ++it) {
|
||||
cv::FileNode t = *it;
|
||||
int id = -1; t["id"] >> id;
|
||||
std::string content; t["content"] >> content;
|
||||
bool is_special = false; t["special"] >> is_special;
|
||||
if (id >= 0 && !content.empty()) {
|
||||
gemma.specialToId[content] = id;
|
||||
gemma.idToSpecial[id] = content;
|
||||
// All added tokens bypass BPE (matching HuggingFace behavior),
|
||||
// not just those marked special.
|
||||
special.insert(content);
|
||||
if (outSpecial) outSpecial->insert(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return makePtr<GemmaBpeTokenizerImpl>(std::move(gemma), std::move(special));
|
||||
}
|
||||
|
||||
static void registerDefaultTokenizers() {
|
||||
auto& reg = tokenizerRegistry();
|
||||
if (reg.find("BPE") == reg.end()) {
|
||||
@@ -74,6 +167,14 @@ static void registerDefaultTokenizers() {
|
||||
return makePtr<BpeTokenizerImpl>(std::move(core), std::move(special));
|
||||
};
|
||||
}
|
||||
|
||||
if (reg.find("Gemma") == reg.end()) {
|
||||
reg["Gemma"] = [](const FileStorage& /*cfg*/, const std::string& dir) -> Ptr<Tokenizer::Impl> {
|
||||
std::string tok_json = dir + "tokenizer.json";
|
||||
std::unordered_set<std::string> special;
|
||||
return buildGemmaFromJson(tok_json, &special);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Tokenizer::Tokenizer() : impl_(nullptr) {}
|
||||
@@ -174,7 +275,7 @@ Tokenizer Tokenizer::load(const std::string& model_config) {
|
||||
auto it = reg.find(methodType);
|
||||
if (it == reg.end())
|
||||
CV_Error(cv::Error::StsError,
|
||||
"Unsupported tokenizer method: '" + methodType + "'. Supported: BPE");
|
||||
"Unsupported tokenizer method: '" + methodType + "'. Supported: BPE, Gemma");
|
||||
|
||||
Tokenizer tok;
|
||||
tok.impl_ = it->second(cfg, dir);
|
||||
|
||||
@@ -151,4 +151,50 @@ TEST(Tokenizer_BPE, Tokenizer_Qwen2_5_Roundtrip) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST(Tokenizer_Gemma, Tokenizer_Gemma3_English) {
|
||||
std::string model = _tf("gemma3/config.json");
|
||||
Tokenizer tok = Tokenizer::load(model);
|
||||
EXPECT_EQ(tok.encode("Hello world"), (std::vector<int>{9259, 1902}));
|
||||
}
|
||||
|
||||
TEST(Tokenizer_Gemma, Tokenizer_Gemma3_Phrase) {
|
||||
std::string model = _tf("gemma3/config.json");
|
||||
Tokenizer tok = Tokenizer::load(model);
|
||||
EXPECT_EQ(tok.encode("the quick brown fox"),
|
||||
(std::vector<int>{1437, 3823, 8864, 37423}));
|
||||
}
|
||||
|
||||
TEST(Tokenizer_Gemma, Tokenizer_Gemma3_Mixed) {
|
||||
std::string model = _tf("gemma3/config.json");
|
||||
Tokenizer tok = Tokenizer::load(model);
|
||||
EXPECT_EQ(tok.encode("OpenCV"), (std::vector<int>{7084, 20741}));
|
||||
}
|
||||
|
||||
TEST(Tokenizer_Gemma, Tokenizer_Gemma3_Numbers) {
|
||||
std::string model = _tf("gemma3/config.json");
|
||||
Tokenizer tok = Tokenizer::load(model);
|
||||
EXPECT_EQ(tok.encode("2024"), (std::vector<int>{236778, 236771, 236778, 236812}));
|
||||
}
|
||||
|
||||
TEST(Tokenizer_Gemma, Tokenizer_Gemma3_SpecialTokens) {
|
||||
std::string model = _tf("gemma3/config.json");
|
||||
Tokenizer tok = Tokenizer::load(model);
|
||||
EXPECT_EQ(tok.encode("<bos>Hello<eos>"), (std::vector<int>{2, 9259, 1}));
|
||||
}
|
||||
|
||||
TEST(Tokenizer_Gemma, Tokenizer_Gemma3_Roundtrip) {
|
||||
std::string model = _tf("gemma3/config.json");
|
||||
Tokenizer tok = Tokenizer::load(model);
|
||||
std::vector<std::string> cases = {
|
||||
"Hello world",
|
||||
"the quick brown fox",
|
||||
"OpenCV",
|
||||
"hello world",
|
||||
};
|
||||
for (const auto& text : cases) {
|
||||
EXPECT_EQ(tok.decode(tok.encode(text)), text);
|
||||
}
|
||||
}
|
||||
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# This file is part of OpenCV project.
|
||||
# It is subject to the license terms in the LICENSE file found in the top-level directory
|
||||
# of this distribution and at http://opencv.org/license.html.
|
||||
# Copyright (C) 2025, BigVision LLC, all rights reserved.
|
||||
# Third party copyrights are property of their respective owners.
|
||||
|
||||
'''
|
||||
This is a sample script to run Gemma3 inference in OpenCV using ONNX model.
|
||||
The script loads the Gemma3 model and runs inference on a given prompt using
|
||||
the Gemma3 chat format (<start_of_turn> / <end_of_turn> special tokens).
|
||||
|
||||
Model: https://huggingface.co/google/gemma-3-1b-it
|
||||
|
||||
Exporting Gemma3 model to ONNX:
|
||||
|
||||
1. Install the required dependencies:
|
||||
|
||||
pip install optimum[exporters] torch transformers
|
||||
|
||||
2. Export the model to ONNX:
|
||||
|
||||
optimum-cli export onnx --model google/gemma-3-1b-it --task causal-lm gemma3_instruct_onnx/
|
||||
|
||||
|
||||
Run the script:
|
||||
1. Install the required dependencies:
|
||||
|
||||
pip install numpy
|
||||
|
||||
2. Run the script:
|
||||
|
||||
python gemma3_inference.py --model=<path-to-onnx-model> \
|
||||
--tokenizer_path=<path-to-opencv-tokenizer-config.json> \
|
||||
--prompt="What is OpenCV?"
|
||||
|
||||
The tokenizer_path should point to an OpenCV-format config.json (e.g., from
|
||||
opencv_extra/testdata/dnn/llm/gemma3/config.json), NOT the HuggingFace tokenizer_config.json.
|
||||
'''
|
||||
|
||||
import numpy as np
|
||||
import argparse
|
||||
import cv2 as cv
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description='Use this script to run Gemma3 inference in OpenCV',
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser.add_argument('--model', type=str, required=True, help='Path to Gemma3 ONNX model file.')
|
||||
parser.add_argument('--tokenizer_path', type=str, required=True, help='Path to Gemma3 tokenizer config.json.')
|
||||
parser.add_argument('--prompt', type=str, default='What is OpenCV?', help='User prompt.')
|
||||
parser.add_argument('--max_new_tokens', type=int, default=64, help='Maximum number of new tokens to generate.')
|
||||
parser.add_argument('--seed', type=int, default=0, help='Random seed.')
|
||||
return parser.parse_args()
|
||||
|
||||
def build_gemma3_prompt(user_prompt):
|
||||
'''Wrap user prompt in Gemma3 chat format.'''
|
||||
return '<start_of_turn>user\n' + user_prompt + '<end_of_turn>\n<start_of_turn>model\n'
|
||||
|
||||
def gemma3_inference(net, prompt, max_new_tokens, tokenizer):
|
||||
|
||||
print("Inferencing Gemma3 model...")
|
||||
|
||||
tokens = tokenizer.encode(prompt)
|
||||
# Prepend BOS token (id=2) as required by Gemma3
|
||||
tokens = [2] + list(tokens)
|
||||
tokens = np.array(tokens, dtype=np.int64).reshape(1, -1)
|
||||
|
||||
# Gemma3 special token IDs
|
||||
eos_id = 1 # <eos>
|
||||
eot_id = 106 # <end_of_turn>
|
||||
stop_ids = (eos_id, eot_id)
|
||||
|
||||
for _ in range(max_new_tokens):
|
||||
seq_len = tokens.shape[1]
|
||||
attention_mask = np.ones((1, seq_len), dtype=np.int64)
|
||||
|
||||
net.setInput(tokens, 'input_ids')
|
||||
net.setInput(attention_mask, 'attention_mask')
|
||||
logits = net.forward() # (1, seq_len, vocab_size)
|
||||
logits = logits[:, -1, :] # take last token logits
|
||||
|
||||
new_id = int(np.argmax(logits.reshape(-1)))
|
||||
tokens = np.concatenate((tokens, np.array([[new_id]], dtype=np.int64)), axis=1)
|
||||
|
||||
if new_id in stop_ids:
|
||||
break
|
||||
|
||||
return tokens
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
args = parse_args()
|
||||
np.random.seed(args.seed)
|
||||
|
||||
print("Preparing Gemma3 model...")
|
||||
tokenizer = cv.dnn.Tokenizer.load(args.tokenizer_path)
|
||||
|
||||
net = cv.dnn.readNetFromONNX(args.model, cv.dnn.ENGINE_NEW)
|
||||
|
||||
gemma3_prompt = build_gemma3_prompt(args.prompt)
|
||||
print(f"Prompt:\n{gemma3_prompt}")
|
||||
|
||||
prompt_len = len(tokenizer.encode(gemma3_prompt)) + 1 # +1 for BOS token
|
||||
tokens = gemma3_inference(net, gemma3_prompt, args.max_new_tokens, tokenizer)
|
||||
response = tokenizer.decode(tokens[0][prompt_len:].tolist())
|
||||
print(f"Response:\n{response}")
|
||||
Reference in New Issue
Block a user