PolyScribe 0.1.1__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {polyscribe-0.1.1 → polyscribe-0.2.0}/PKG-INFO +1 -1
- polyscribe-0.2.0/include/byte_pair_encoder.hpp +30 -0
- polyscribe-0.2.0/include/typedefs.hpp +18 -0
- polyscribe-0.2.0/include/word_extracter.hpp +18 -0
- {polyscribe-0.1.1 → polyscribe-0.2.0}/pyproject.toml +10 -2
- polyscribe-0.2.0/python/bindings.cpp +40 -0
- polyscribe-0.2.0/src/byte_pair_encoder.cpp +193 -0
- polyscribe-0.2.0/src/word_extracter.cpp +76 -0
- {polyscribe-0.1.1 → polyscribe-0.2.0}/.gitignore +0 -0
- {polyscribe-0.1.1 → polyscribe-0.2.0}/CMakeLists.txt +0 -0
- {polyscribe-0.1.1 → polyscribe-0.2.0}/LICENSE +0 -0
- {polyscribe-0.1.1 → polyscribe-0.2.0}/README.md +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <unordered_map>
|
|
4
|
+
#include <string>
|
|
5
|
+
#include <vector>
|
|
6
|
+
#include <cstdint>
|
|
7
|
+
|
|
8
|
+
#include <./typedefs.hpp>
|
|
9
|
+
|
|
10
|
+
namespace Scribe {
|
|
11
|
+
class BytePairEncoder {
|
|
12
|
+
private:
|
|
13
|
+
std::unordered_map<Token, std::vector<uint8_t>> vocab;
|
|
14
|
+
std::unordered_map<uint8_t, Token> utf8Lookup;
|
|
15
|
+
std::vector<std::pair<Pair, Token>> mergeForest;
|
|
16
|
+
|
|
17
|
+
std::vector<Token> getCodePoints(const std::string& str);
|
|
18
|
+
Pair getMostFrequentPair(const WordCounts& wordCounts);
|
|
19
|
+
Pair getBestRankedPair(const WordCounts& wordCounts);
|
|
20
|
+
void doMerge(WordCounts& wordCounts, const Pair& pairToMerge, const int newToken);
|
|
21
|
+
|
|
22
|
+
public:
|
|
23
|
+
BytePairEncoder();
|
|
24
|
+
~BytePairEncoder() = default;
|
|
25
|
+
|
|
26
|
+
void train(const std::string& filename, int cycles, bool normalizedRanking, bool verbose);
|
|
27
|
+
std::vector<Token> encode(const std::string& data);
|
|
28
|
+
std::vector<uint8_t> decode(const std::vector<Token>& tokens);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <unordered_map>
|
|
4
|
+
#include <vector>
|
|
5
|
+
|
|
6
|
+
namespace Scribe {
|
|
7
|
+
struct PairHash {
|
|
8
|
+
inline size_t operator()(const std::pair<int, int>& v) const {
|
|
9
|
+
return v.first * 997 + v.second;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
using Count = int;
|
|
14
|
+
using Token = int;
|
|
15
|
+
using Pair = std::pair<Token, Token>;
|
|
16
|
+
using PairCounts = std::unordered_map<Pair, Count, PairHash>;
|
|
17
|
+
using WordCounts = std::vector<std::pair<std::vector<Token>, Count>>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <string>
|
|
4
|
+
#include <unordered_map>
|
|
5
|
+
|
|
6
|
+
namespace Scribe {
|
|
7
|
+
class WordExtracter
|
|
8
|
+
{
|
|
9
|
+
private:
|
|
10
|
+
int getUTFByteLength(const char ch);
|
|
11
|
+
|
|
12
|
+
public:
|
|
13
|
+
WordExtracter() = default;
|
|
14
|
+
~WordExtracter() = default;
|
|
15
|
+
|
|
16
|
+
std::unordered_map<std::string, int> wordify(const std::string& filename);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -21,9 +21,17 @@ cmake.version = ">=3.28"
|
|
|
21
21
|
cmake.args = ["-D_BUILD_PY_MOD=ON"]
|
|
22
22
|
ninja.make-fallback = true
|
|
23
23
|
wheel.packages = []
|
|
24
|
-
|
|
24
|
+
sdist.include = [
|
|
25
|
+
"CMakeLists.txt",
|
|
26
|
+
"src/*",
|
|
27
|
+
"include/*",
|
|
28
|
+
"python/*"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[[tool.dynamic-metadata]]
|
|
32
|
+
provider = "scikit_build_core.metadata.setuptools_scm"
|
|
25
33
|
|
|
26
|
-
[
|
|
34
|
+
[tool.setuptools_scm]
|
|
27
35
|
|
|
28
36
|
[tool.scikit-build.cmake.define]
|
|
29
37
|
_BUILD_PY_MOD = "ON"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#include <nanobind/nanobind.h>
|
|
2
|
+
#include "../include/byte_pair_encoder.hpp"
|
|
3
|
+
|
|
4
|
+
#include <nanobind/stl/string.h>
|
|
5
|
+
#include <nanobind/stl/unordered_map.h>
|
|
6
|
+
#include <nanobind/stl/vector.h>
|
|
7
|
+
#include <nanobind/ndarray.h>
|
|
8
|
+
|
|
9
|
+
namespace nb = nanobind;
|
|
10
|
+
using namespace nb::literals;
|
|
11
|
+
|
|
12
|
+
NB_MODULE(Scribe, m) {
|
|
13
|
+
m.doc() = "A fast and simple module to tokenize text data.";
|
|
14
|
+
|
|
15
|
+
nb::class_<Scribe::BytePairEncoder>(m, "BytePairEncoder", "A module that applies BPE algorithm on the given text to generate word/sub-word tokens."
|
|
16
|
+
).def(nb::init<>()
|
|
17
|
+
).def("train", &Scribe::BytePairEncoder::train, nb::arg("filename"), nb::arg("cycles"), nb::arg("normalizedRanking"), nb::arg("verbose"),
|
|
18
|
+
"Trains the tokenizer on text data contained in `filename` file.\n\n"
|
|
19
|
+
"Args:\n"
|
|
20
|
+
" filename (str): Path to the training data (taken relative from the directory the function will be called in).\n"
|
|
21
|
+
" cycles (int): Number of cycles to train the tokenizer for, total vocab size will be 256 + `cycles`.\n"
|
|
22
|
+
" normalizedRanking (bool): If false, follows default BPE Scoring for token pairs, i.e. P(A|B)."
|
|
23
|
+
" If true, follows WordPiece Scoring for token pairs, i.e. P(A|B)/P(A)*P(B)."
|
|
24
|
+
" verbose (bool): If true, prints log of every merge cycle to standard output.\n"
|
|
25
|
+
"Returns:\n"
|
|
26
|
+
" None"
|
|
27
|
+
).def("encode", &Scribe::BytePairEncoder::encode, nb::arg("data"),
|
|
28
|
+
"Encoder function to convert text data into tokens.\n\n"
|
|
29
|
+
"Args:\n"
|
|
30
|
+
" data (str): Text data to encode.\n"
|
|
31
|
+
"Returns:\n"
|
|
32
|
+
" list[int]"
|
|
33
|
+
).def("decode", &Scribe::BytePairEncoder::decode, nb::arg("tokens"),
|
|
34
|
+
"Decoder function to convert tokens into string data.\n\n"
|
|
35
|
+
"Args:\n"
|
|
36
|
+
" tokens (list[int]): List of tokens to decode.\n"
|
|
37
|
+
"Returns:\n"
|
|
38
|
+
" list[int]"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#include "../include/byte_pair_encoder.hpp"
|
|
2
|
+
|
|
3
|
+
#include <string>
|
|
4
|
+
#include <unordered_map>
|
|
5
|
+
#include <vector>
|
|
6
|
+
#include <iostream>
|
|
7
|
+
#include <cstdint>
|
|
8
|
+
#include <fmt/format.h>
|
|
9
|
+
|
|
10
|
+
#include <../include/typedefs.hpp>
|
|
11
|
+
#include "../include/word_extracter.hpp"
|
|
12
|
+
|
|
13
|
+
std::vector<Scribe::Token> Scribe::BytePairEncoder::getCodePoints(const std::string& str) {
|
|
14
|
+
int len = str.length();
|
|
15
|
+
int i;
|
|
16
|
+
|
|
17
|
+
std::vector<Token> codepoints(len);
|
|
18
|
+
|
|
19
|
+
for (i = 0; i < len; i++) {
|
|
20
|
+
codepoints[i] = utf8Lookup[str[i]];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return codepoints;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Scribe::Pair Scribe::BytePairEncoder::getMostFrequentPair(const WordCounts& wordCounts) {
|
|
27
|
+
PairCounts pairCounts;
|
|
28
|
+
Count maxCount = 0;
|
|
29
|
+
Pair mostFrequentPair = { -1, -1 };
|
|
30
|
+
|
|
31
|
+
for (const auto& [word, count] : wordCounts) {
|
|
32
|
+
int len = word.size();
|
|
33
|
+
if (len < 2) continue;
|
|
34
|
+
|
|
35
|
+
int i = 0, j = 1;
|
|
36
|
+
while (j < len) {
|
|
37
|
+
Pair pair = { word[i], word[j] };
|
|
38
|
+
pairCounts[pair] += count;
|
|
39
|
+
|
|
40
|
+
if (pairCounts[pair] > maxCount) {
|
|
41
|
+
maxCount = pairCounts[pair];
|
|
42
|
+
mostFrequentPair = pair;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
i++; j++;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return mostFrequentPair;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
Scribe::Pair Scribe::BytePairEncoder::getBestRankedPair(const WordCounts& wordCounts) {
|
|
53
|
+
PairCounts pairCounts;
|
|
54
|
+
std::unordered_map<Token, Count> tokenCounts;
|
|
55
|
+
double maxScore = 0;
|
|
56
|
+
Pair bestPair = { -1, -1 };
|
|
57
|
+
|
|
58
|
+
for (const auto& [word, count] : wordCounts) {
|
|
59
|
+
int len = word.size();
|
|
60
|
+
if (len < 2) continue;
|
|
61
|
+
|
|
62
|
+
int i = 0, j = 1;
|
|
63
|
+
tokenCounts[word[0]] += count;
|
|
64
|
+
while (j < len) {
|
|
65
|
+
Pair pair = { word[i], word[j] };
|
|
66
|
+
pairCounts[pair] += count;
|
|
67
|
+
tokenCounts[word[j]] += count;
|
|
68
|
+
|
|
69
|
+
i++; j++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const auto& [word, count] : wordCounts) {
|
|
74
|
+
int len = word.size();
|
|
75
|
+
if (len < 2) continue;
|
|
76
|
+
|
|
77
|
+
int i = 0, j = 1;
|
|
78
|
+
while (j < len) {
|
|
79
|
+
Pair pair = { word[i], word[j] };
|
|
80
|
+
|
|
81
|
+
double score = (double)pairCounts[pair] / tokenCounts[word[i]];
|
|
82
|
+
score /= tokenCounts[word[j]];
|
|
83
|
+
if (score > maxScore) {
|
|
84
|
+
maxScore = score;
|
|
85
|
+
bestPair = pair;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
i++; j++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return bestPair;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
void Scribe::BytePairEncoder::doMerge(WordCounts& wordCounts, const Pair& pairToMerge, const int newToken) {
|
|
96
|
+
for (auto& [word, _] : wordCounts) {
|
|
97
|
+
int len = word.size();
|
|
98
|
+
int read_idx = 0;
|
|
99
|
+
int write_idx = 0;
|
|
100
|
+
|
|
101
|
+
while (read_idx < len) {
|
|
102
|
+
if (read_idx + 1 < len && word[read_idx] == pairToMerge.first && word[read_idx + 1] == pairToMerge.second) {
|
|
103
|
+
word[read_idx] = newToken;
|
|
104
|
+
word[write_idx++] = std::move(word[read_idx]);
|
|
105
|
+
|
|
106
|
+
read_idx += 2;
|
|
107
|
+
} else {
|
|
108
|
+
if (write_idx != read_idx) {
|
|
109
|
+
word[write_idx] = std::move(word[read_idx]);
|
|
110
|
+
}
|
|
111
|
+
write_idx++;
|
|
112
|
+
read_idx++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
word.resize(write_idx);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
Scribe::BytePairEncoder::BytePairEncoder() {
|
|
120
|
+
for (int i = 0; i < 256; i++) {
|
|
121
|
+
const uint8_t ch = static_cast<uint8_t>(i);
|
|
122
|
+
|
|
123
|
+
vocab[i] = { ch };
|
|
124
|
+
utf8Lookup[ch] = i;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
void Scribe::BytePairEncoder::train(const std::string& filename, int cycles, bool normalizedRanking, bool verbose) {
|
|
129
|
+
WordExtracter extracter;
|
|
130
|
+
|
|
131
|
+
std::unordered_map<std::string, Count> strWordCounts = extracter.wordify(filename);
|
|
132
|
+
// TODO: make it so that `strWordCounts` is destroyed after its use
|
|
133
|
+
|
|
134
|
+
WordCounts wordCounts;
|
|
135
|
+
wordCounts.reserve(strWordCounts.size());
|
|
136
|
+
|
|
137
|
+
for (const auto& [word, count] : strWordCounts) {
|
|
138
|
+
if (word.empty()) continue;
|
|
139
|
+
|
|
140
|
+
const std::vector<Token> initialTokens = getCodePoints(word);
|
|
141
|
+
wordCounts.emplace_back(std::move(initialTokens), count);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
Pair pairToMerge;
|
|
145
|
+
for (int i = 0; i < cycles; i++) {
|
|
146
|
+
if (!normalizedRanking) pairToMerge = getMostFrequentPair(wordCounts);
|
|
147
|
+
else pairToMerge = getBestRankedPair(wordCounts);
|
|
148
|
+
|
|
149
|
+
if (pairToMerge == Pair(-1, -1)) {
|
|
150
|
+
std::cerr << "\n\n[INFO]::BYTE_PAIR_ENCODER::CYCLES: Max possible vocab size reached, ";
|
|
151
|
+
std::cerr << "{Cycles: " << cycles << "} is too high for the provided dataset.";
|
|
152
|
+
std::cerr << "\nFinishing gracefully at cycle: " << i << "." << std::endl;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
int newToken = i + 256;
|
|
157
|
+
doMerge(wordCounts, pairToMerge, newToken);
|
|
158
|
+
|
|
159
|
+
mergeForest.emplace_back(pairToMerge, newToken);
|
|
160
|
+
|
|
161
|
+
std::vector<uint8_t> newBytes = vocab[pairToMerge.first];
|
|
162
|
+
newBytes.insert(newBytes.end(), vocab[pairToMerge.second].begin(), vocab[pairToMerge.second].end());
|
|
163
|
+
vocab[newToken] = newBytes;
|
|
164
|
+
|
|
165
|
+
if (!verbose) continue;
|
|
166
|
+
std::string w1 = "", w2 = "";
|
|
167
|
+
for (auto ch : vocab[pairToMerge.first]) w1 += static_cast<char>(ch);
|
|
168
|
+
for (auto ch : vocab[pairToMerge.second]) w2 += static_cast<char>(ch);
|
|
169
|
+
std::string w3 = w1 + w2;
|
|
170
|
+
std::clog << fmt::format("[INFO]::NEW_TOKEN_{:<6}: Merged Pair {{{:^6}, {:^6}}} : {{{:^15}, {:^15}}} -> {:<40}", newToken, pairToMerge.first, pairToMerge.second, w1, w2, w3) << std::endl;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
std::vector<Scribe::Token> Scribe::BytePairEncoder::encode(const std::string& data) {
|
|
175
|
+
std::vector<Token> codepoints = getCodePoints(data);
|
|
176
|
+
WordCounts encodedData = { std::pair<std::vector<Token>, Count>(codepoints, 1) };
|
|
177
|
+
|
|
178
|
+
for (const auto& [pair, token] : mergeForest) {
|
|
179
|
+
doMerge(encodedData, pair, token);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return encodedData[0].first;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
std::vector<uint8_t> Scribe::BytePairEncoder::decode(const std::vector<Token>& tokens) {
|
|
186
|
+
std::vector<uint8_t> decodedData;
|
|
187
|
+
|
|
188
|
+
for (const Token token : tokens) {
|
|
189
|
+
decodedData.insert(decodedData.end(), vocab[token].begin(), vocab[token].end());
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return decodedData;
|
|
193
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#include "../include/word_extracter.hpp"
|
|
2
|
+
|
|
3
|
+
#include <set>
|
|
4
|
+
#include <fstream>
|
|
5
|
+
#include <sstream>
|
|
6
|
+
#include <string>
|
|
7
|
+
|
|
8
|
+
int Scribe::WordExtracter::getUTFByteLength(const char ch) {
|
|
9
|
+
const unsigned char uc = static_cast<unsigned char>(ch);
|
|
10
|
+
if (uc < 0x80) { return 1; } // 0xxxxxxx, hence a normal ASCII char
|
|
11
|
+
else if ((uc & 0xE0) == 0xC0) { return 2; } // 110xxxxx: 2 bytes
|
|
12
|
+
else if ((uc & 0xF0) == 0xE0) { return 3; } // 1110xxxx: 3 bytes
|
|
13
|
+
else if ((uc & 0xF8) == 0xF0) { return 4; } // 11110xxx: 4 bytes
|
|
14
|
+
return 1; // for invalid utf-8 byte
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
std::unordered_map<std::string, int> Scribe::WordExtracter::wordify(const std::string& filename) {
|
|
18
|
+
std::ifstream file(filename, std::ios::binary);
|
|
19
|
+
if (!file.is_open()) {
|
|
20
|
+
throw std::runtime_error("[ERROR]::COULD_NOT_OPEN_FILE: " + filename);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
std::stringstream buffer;
|
|
24
|
+
buffer << file.rdbuf();
|
|
25
|
+
std::string filedata(buffer.str());
|
|
26
|
+
|
|
27
|
+
file.close();
|
|
28
|
+
|
|
29
|
+
const std::set<char> delimiters{'!', '"', '#', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '_', '.', '/', ':', ';', '<', '=', '>', '?', '[', ']', '~', '`', '\\', '^', '{', '}', '\n'};
|
|
30
|
+
|
|
31
|
+
std::unordered_map<std::string, int> words;
|
|
32
|
+
unsigned long wordStartIndex = 0, wordLen = 0;
|
|
33
|
+
|
|
34
|
+
while (wordStartIndex < filedata.length()) {
|
|
35
|
+
const char ch = filedata[wordStartIndex + wordLen];
|
|
36
|
+
const int utfByteLen = getUTFByteLength(ch);
|
|
37
|
+
|
|
38
|
+
if (utfByteLen > 1) {
|
|
39
|
+
// Inserting the word building upto the multibyte character
|
|
40
|
+
words[std::string(&filedata[wordStartIndex], wordLen)]++;
|
|
41
|
+
wordStartIndex += wordLen;
|
|
42
|
+
|
|
43
|
+
// Inserting the multibyte character
|
|
44
|
+
words[std::string(&filedata[wordStartIndex], utfByteLen)]++;
|
|
45
|
+
wordStartIndex += utfByteLen;
|
|
46
|
+
wordLen = 0;
|
|
47
|
+
|
|
48
|
+
} else if (ch == ' ' || ch == '\0') {
|
|
49
|
+
// Inserting the word building upto the whitespace (or EOF)
|
|
50
|
+
words[std::string(&filedata[wordStartIndex], wordLen)]++;
|
|
51
|
+
|
|
52
|
+
wordStartIndex += wordLen;
|
|
53
|
+
wordLen = 1;
|
|
54
|
+
|
|
55
|
+
// TODO: Decouple ' ' and '\0' and make individual cases for them; add an EOF token for file-termination.
|
|
56
|
+
} else if (delimiters.count(ch)) {
|
|
57
|
+
// Inserting the word building upto the delimiter
|
|
58
|
+
words[std::string(&filedata[wordStartIndex], wordLen)]++;
|
|
59
|
+
wordStartIndex += wordLen;
|
|
60
|
+
|
|
61
|
+
while (delimiters.count(filedata[wordStartIndex])) {
|
|
62
|
+
// Inserting all consecutive (if) occuring delimiters
|
|
63
|
+
words[std::string(1, filedata[wordStartIndex])]++;
|
|
64
|
+
wordStartIndex++;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
wordLen = 1;
|
|
68
|
+
} else {
|
|
69
|
+
wordLen++;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// TODO: Make the function work with multiple files at once.
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return words;
|
|
76
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|