diff --git a/dlib/dnn/layers.h b/dlib/dnn/layers.h index 7ec8b1a956..2b0136ef91 100644 --- a/dlib/dnn/layers.h +++ b/dlib/dnn/layers.h @@ -2329,24 +2329,46 @@ namespace dlib template < unsigned long num_outputs_, - linear_bias_mode bias_mode_ + linear_bias_mode bias_mode_ = LINEAR_HAS_BIAS > class linear_ { static_assert(num_outputs_ > 0, "The number of outputs from a linear_ layer must be > 0"); public: - linear_() : + explicit linear_() : num_outputs(num_outputs_), - num_inputs(0), + num_inputs(0), learning_rate_multiplier(1), bias_mode(bias_mode_) { } + linear_(const linear_& other) : + num_outputs(other.num_outputs), + num_inputs(other.num_inputs), + learning_rate_multiplier(other.learning_rate_multiplier), + bias_mode(other.bias_mode), + params(other.params), + weights(other.weights), + biases(other.biases) { + } + + linear_& operator=(const linear_& other) { + if (this != &other) { + num_outputs = other.num_outputs; + num_inputs = other.num_inputs; + learning_rate_multiplier = other.learning_rate_multiplier; + bias_mode = other.bias_mode; + params = other.params; + weights = other.weights; + biases = other.biases; + } + return *this; + } + double get_learning_rate_multiplier() const { return learning_rate_multiplier; } void set_learning_rate_multiplier(double val) { learning_rate_multiplier = val; } - - unsigned long get_num_inputs() const { return num_inputs; } + unsigned long get_num_outputs() const { return num_outputs; } void set_num_outputs(long num) { @@ -2358,6 +2380,7 @@ namespace dlib num_outputs = num; } } + unsigned long get_num_inputs() const { return num_inputs; } linear_bias_mode get_bias_mode() const { return bias_mode; } template @@ -2503,8 +2526,8 @@ namespace dlib } private: - unsigned long num_inputs; unsigned long num_outputs; + unsigned long num_inputs; double learning_rate_multiplier; linear_bias_mode bias_mode; resizable_tensor params; @@ -2515,7 +2538,7 @@ namespace dlib unsigned long num_outputs, typename SUBNET > - using linear = add_layer, SUBNET>; + using linear = add_layer, SUBNET>; template < unsigned long num_outputs, diff --git a/dlib/tokenizer/bpe_tokenizer.h b/dlib/tokenizer/bpe_tokenizer.h index f9457b554f..06f0168cfd 100644 --- a/dlib/tokenizer/bpe_tokenizer.h +++ b/dlib/tokenizer/bpe_tokenizer.h @@ -7,12 +7,15 @@ #include #include #include -#include -#include -#include #include #include -#include +#include +#include +#include +#include +#include +#include +#include #include "../base64.h" #include "../serialize.h" @@ -20,382 +23,462 @@ namespace dlib { - constexpr size_t BPE_TOKENIZER_MAX_TOKEN_LENGTH = 8; - constexpr int BPE_TOKENIZER_BASE_VOCAB_SIZE = 256; class bpe_tokenizer { public: - bpe_tokenizer() : vocab_size(BPE_TOKENIZER_BASE_VOCAB_SIZE) + bpe_tokenizer() { - // Initialize the base vocabulary with single bytes - for (int i = 0; i < BPE_TOKENIZER_BASE_VOCAB_SIZE; ++i) - vocab[i] = std::vector{ static_cast(i) }; - - // Initialize special tokens with sequential IDs - special_tokens = - { - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 1}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 2}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 3}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 4}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 5}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 7}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 9}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 10}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 11}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 12}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 13}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 14}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 15}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 16}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 17}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 18}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 19}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 20}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 21}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 22}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 23}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 24}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 25}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 26}, - {"", BPE_TOKENIZER_BASE_VOCAB_SIZE + 27} - }; - - // Initialize the vector of special token IDs - for (const auto& token : special_tokens) - special_token_map[token.second] = token.first; + vocab_size = BPE_BASE_VOCAB_SIZE + special_token_list.size(); + + // Initialize base vocabulary (bytes 0-255) + for (int i = 0; i < BPE_BASE_VOCAB_SIZE; ++i) { + Merge m; + m.token_id = i; + m.left = i; + m.right = i; + m.pattern.push_back(static_cast(i)); + merges.push_back(m); + } + + // Initialize special tokens + initialize_special_tokens(); } - // Train the tokenizer on the given text - void train(const std::string& text, int vocab_size, bool verbose = false) + // Train the tokenizer on input data + void train(const std::string& text, size_t max_vocab_size, size_t max_bytes = 0, bool verbose = false) { - DLIB_CASSERT(vocab_size >= BPE_TOKENIZER_BASE_VOCAB_SIZE); - this->vocab_size = vocab_size; - int num_merges = vocab_size - BPE_TOKENIZER_BASE_VOCAB_SIZE; - - // Convert text to byte IDs - std::vector ids; - for (char c : text) ids.push_back(static_cast(c)); - - // Perform BPE merges - for (int i = 0; i < num_merges; ++i) { - auto stats = get_stats(ids); - if (stats.empty()) break; - - // Find the most frequent pair that does not exceed BPE_TOKENIZER_MAX_TOKEN_LENGTH - auto pair = get_most_frequent_pair(stats); - - // Check if the resulting token would exceed BPE_TOKENIZER_MAX_TOKEN_LENGTH - size_t new_token_length = vocab[pair.first].size() + vocab[pair.second].size(); - if (new_token_length > BPE_TOKENIZER_MAX_TOKEN_LENGTH) { - if (verbose) - { - std::cout << "\r" - << std::setw(100) << std::flush - << "\rskipping merge " << std::to_string(i + 1) << "/" << std::to_string(num_merges) << ": (" - << std::to_string(pair.first) << "," << std::to_string(pair.second) << ") -> new token length " - << std::to_string(new_token_length) << " exceeds limit of " << std::to_string(BPE_TOKENIZER_MAX_TOKEN_LENGTH) - << std::flush; - } - continue; // Skip this merge - } + if (text.empty()) return; + + // Convert text to bytes + std::vector data(text.begin(), text.end()); + if (max_bytes > 0 && data.size() > max_bytes) data.resize(max_bytes); - int idx = (BPE_TOKENIZER_BASE_VOCAB_SIZE + (int)special_tokens.size()) + i; - ids = merge(ids, pair, idx); - merges[pair] = idx; - vocab[idx].insert(vocab[idx].end(), vocab[pair.first].begin(), vocab[pair.first].end()); - vocab[idx].insert(vocab[idx].end(), vocab[pair.second].begin(), vocab[pair.second].end()); - - if (verbose) - { - std::cout << "\r" - << std::setw(100) << std::flush - << "\rmerge " << std::to_string(i + 1) << "/" << std::to_string(num_merges) << ": (" - << std::to_string(pair.first) << "," << std::to_string(pair.second) << ") -> " << std::to_string(idx) - << " (" << bytes_to_string(vocab[idx]) << ") had " - << std::to_string(stats[pair]) << " occurrences" - << std::endl; + // Calculate available merges (reserving space for special tokens) + size_t num_merges = max_vocab_size - BPE_BASE_VOCAB_SIZE - special_token_list.size(); + + if (num_merges <= 0) { + if (verbose) { + std::cout << "Warning: max_vocab_size too small for any merges. Need at least " + << (BPE_BASE_VOCAB_SIZE + special_token_list.size() + 1) << " tokens." << std::endl; } + return; } - } - // Encode the given text into subword tokens - std::vector encode(const std::string& text) const - { - std::vector result_ids; - std::mutex result_mutex; - - // Split the text into paragraphs based on newline characters - std::vector paragraphs; - size_t start = 0, end = text.find('\n'); - while (end != std::string::npos) { - std::string paragraph = text.substr(start, end - start); - if (!paragraph.empty()) paragraphs.push_back(paragraph); - start = end + 1; - end = text.find('\n', start); + if (verbose) { + std::cout << "Training BPE tokenizer on " << data.size() << " bytes..." << std::endl; + std::cout << "Target vocabulary size: " << max_vocab_size << std::endl; + std::cout << "Base vocabulary: " << BPE_BASE_VOCAB_SIZE << " tokens" << std::endl; + std::cout << "Special tokens: " << special_token_list.size() << " tokens" << std::endl; + std::cout << "Available merges: " << num_merges << std::endl; } - // Add the last paragraph (if any) and only if it's not empty - if (start < text.size()) { - std::string paragraph = text.substr(start); - if (!paragraph.empty()) paragraphs.push_back(paragraph); + + // Reset merges beyond base vocabulary + merges.clear(); + for (int i = 0; i < BPE_BASE_VOCAB_SIZE; ++i) { + Merge m; + m.token_id = i; + m.left = i; + m.right = i; + m.pattern.push_back(static_cast(i)); + merges.push_back(m); } - // Function to encode a single paragraph - auto encode_paragraph = [this](const std::string& paragraph) -> std::vector { - std::vector ids; - ids.reserve(paragraph.size()); - for (char c : paragraph) ids.push_back(static_cast(c)); - - auto stats = get_stats(ids); - std::priority_queue>> pq; - for (const auto& stat : stats) { - const std::pair& pair = stat.first; - if (merges.count(pair)) pq.push({ merges.at(pair), pair }); - } + // Tokenize input into segments (split on whitespace and newlines) + std::vector> segments; + std::vector segment_counts; + tokenize(data, segments, segment_counts); - while (!pq.empty()) { - const auto& top_element = pq.top(); - const std::pair& pair = top_element.second; - pq.pop(); + if (verbose) { + std::cout << "Created " << segments.size() << " segments for training" << std::endl; + } - bool pair_found = false; - for (size_t i = 0; i < ids.size() - 1; ++i) { - if (ids[i] == pair.first && ids[i + 1] == pair.second) { - pair_found = true; - break; - } - } - if (!pair_found) continue; + // Initialize pair counting + std::unordered_map>> pair_counts; + std::unordered_map> where_to_update; - int idx = merges.at(pair); - ids = merge(ids, pair, idx); + // Count initial pairs + for (uint32_t i = 0; i < segments.size(); i++) { + countInSegment(segments[i], i, segment_counts[i], pair_counts, where_to_update); + } + + // Main training loop + size_t merges_performed = 0; + + for (size_t merge_idx = 0; merge_idx < num_merges; merge_idx++) { + // Find most frequent pair + int32_t max_count = 0; + std::pair max_pair = findMaxPair(pair_counts, max_count); - stats = get_stats(ids); - for (const auto& stat : stats) { - const std::pair& new_pair = stat.first; - if (merges.count(new_pair)) pq.push({ merges.at(new_pair), new_pair }); + if (max_count <= 0) { + if (verbose) { + std::cout << "\nNo more pairs to merge at iteration " << merge_idx << std::endl; } + break; } - return ids; - }; - - // Special case: if there's only one paragraph, no need for threads - int sot_tok = get_special_token_id(""); - int eot_tok = get_special_token_id(""); - if (paragraphs.size() == 1) { - std::vector paragraph_ids = encode_paragraph(paragraphs[0]); - result_ids.push_back(sot_tok); - result_ids.insert(result_ids.end(), paragraph_ids.begin(), paragraph_ids.end()); - result_ids.push_back(eot_tok); - return result_ids; + uint16_t new_token = BPE_BASE_VOCAB_SIZE + merge_idx; + + // Create merge entry + Merge m; + m.token_id = new_token; + m.left = max_pair.first; + m.right = max_pair.second; + + // Build pattern for new token + m.pattern = merges[max_pair.first].pattern; + const auto& right_pattern = merges[max_pair.second].pattern; + m.pattern.insert(m.pattern.end(), right_pattern.begin(), right_pattern.end()); + + merges.push_back(m); + + // Store mapping for fast encoding + uint32_t pair_key = (static_cast(max_pair.first) << 16) | max_pair.second; + + if (verbose && (merge_idx % 1000 == 0 || merge_idx < 10)) { + std::cout << "Merge " << merge_idx << ": (" << max_pair.first << ", " << max_pair.second + << ") -> " << new_token << " (occurrences: " << max_count + << ", pattern length: " << m.pattern.size() << ")" << std::endl; + } + + // Apply merge to all affected segments + auto affected_segments = where_to_update[pair_key]; + applyMerge(max_pair, new_token, segments, segment_counts, affected_segments, + pair_counts, where_to_update); + + // Clear this pair's count + pair_counts[pair_key].first = 0; + + merges_performed++; } - // Launch encoding tasks in parallel for multiple paragraphs - std::vector>> futures; - for (const auto& paragraph : paragraphs) - futures.push_back(std::async(std::launch::async, encode_paragraph, paragraph)); - - // Collect results in order - for (auto& future : futures) { - std::vector paragraph_ids = future.get(); - std::lock_guard lock(result_mutex); - result_ids.push_back(sot_tok); - result_ids.insert(result_ids.end(), paragraph_ids.begin(), paragraph_ids.end()); - result_ids.push_back(eot_tok); + // Update vocabulary size: base + special tokens + actual merges performed + vocab_size = merges.size() + special_token_list.size(); + initialize_special_tokens(); + + if (verbose) { + std::cout << "\nTraining complete!" << std::endl; + std::cout << "Base vocabulary: " << BPE_BASE_VOCAB_SIZE << " tokens" << std::endl; + std::cout << "Merges performed: " << merges_performed << std::endl; + std::cout << "Special tokens: " << special_token_list.size() << " tokens" << std::endl; + std::cout << "Total vocabulary size: " << vocab_size << std::endl; } - return result_ids; } - // Decode a single token ID back into text - std::string decode(int id, bool display_special_tokens = true) const + // Encode text into tokens + std::vector encode(const std::string& text) const { - return decode(std::vector({ id }), display_special_tokens); - } + if (text.empty()) return {}; - // Decode a sequence of token IDs back into text - std::string decode(const std::vector& ids, bool display_special_tokens = true) const - { - std::vector bytes; - int vocab_size = static_cast(get_vocab_size()); - for (int id : ids) - { - if (id < vocab_size) - { - // Check if the ID is a special token - auto it = special_token_map.find(id); - if (it != special_token_map.end()) - { - // It's a special token, get the corresponding string - if (display_special_tokens) bytes.insert(bytes.end(), it->second.begin(), it->second.end()); + // Convert to initial tokens + std::vector tokens; + tokens.reserve(text.size()); + for (unsigned char byte : text) { + tokens.push_back(static_cast(byte)); + } + + // Apply all merges in order + for (size_t merge_idx = BPE_BASE_VOCAB_SIZE; merge_idx < merges.size(); merge_idx++) { + const Merge& m = merges[merge_idx]; + + std::vector new_tokens; + new_tokens.reserve(tokens.size()); + + for (size_t i = 0; i < tokens.size(); ) { + if (i < tokens.size() - 1 && tokens[i] == m.left && tokens[i + 1] == m.right) { + new_tokens.push_back(m.token_id); + i += 2; } - else - { - // It's a regular token, get the bytes from the vocabulary - auto& token = vocab.at(id); - bytes.insert(bytes.end(), token.begin(), token.end()); + else { + new_tokens.push_back(tokens[i]); + i++; } } + + tokens = std::move(new_tokens); } - return std::string(bytes.begin(), bytes.end()); + + return tokens; } - // Save the tokenizer model and vocabulary to file - friend void serialize(const bpe_tokenizer& tok, std::ostream& out) + // Decode tokens back to text + std::string decode(const std::vector& tokens, bool display_special_tokens = true) const { - serialize("bpe_tokenizer2_", out); - serialize(tok.special_tokens, out); - serialize(tok.special_token_map, out); - serialize(tok.merges, out); - serialize(tok.vocab, out); - serialize(tok.vocab_size, out); + std::vector result; + result.reserve(tokens.size() * 4); // Estimate + + for (int token : tokens) { + if (token >= 0 && token < static_cast(merges.size())) { + // Base token or merge token + const std::vector& pattern = merges[token].pattern; + result.insert(result.end(), pattern.begin(), pattern.end()); + } + else if (token >= static_cast(merges.size()) && + token < static_cast(get_vocab_size())) { + // Special token + if (display_special_tokens) { + auto it = special_token_map.find(token); + if (it != special_token_map.end()) { + const std::string& special_str = it->second; + result.insert(result.end(), special_str.begin(), special_str.end()); + } + } + } + else { + const std::string tok_unk = ""; + result.insert(result.end(), tok_unk.begin(), tok_unk.end()); + } + } + + return std::string(result.begin(), result.end()); } - // Load the tokenizer model and vocabulary from file - friend void deserialize(bpe_tokenizer& tok, std::istream& in) { - std::string version; - dlib::deserialize(version, in); - if (version != "bpe_tokenizer2_") - throw dlib::serialization_error("Unexpected version '" + version + "' found while deserializing dlib::bpe_tokenizer_."); - deserialize(tok.special_tokens, in); - deserialize(tok.special_token_map, in); - deserialize(tok.merges, in); - deserialize(tok.vocab, in); - deserialize(tok.vocab_size, in); + // Decode single token (for compatibility) + std::string decode(int token, bool display_special_tokens = true) const + { + std::vector tokens = { token }; + return decode(tokens, display_special_tokens); } - // Get the ID of a special token + // Get special token ID int get_special_token_id(const std::string& token) const { auto it = special_tokens.find(token); - if (it != special_tokens.end()) return it->second; + if (it != special_tokens.end()) { + // Special tokens come after base vocab and merges + return it->second; + } throw std::runtime_error("Special token not found: " + token); } - // Get the total vocabulary size - size_t get_vocab_size() const + // Get vocabulary size + size_t get_specials_size() const { return special_token_list.size(); } + size_t get_vocab_size() const { return vocab_size; } + size_t get_vocab_without_specials_size() const { return (vocab_size - special_token_list.size()); } + + // Serialization + friend void serialize(const bpe_tokenizer& item, std::ostream& out) + { + serialize("bpe_tokenizer_", out); + serialize(item.vocab_size, out); + + // Serialize only the merge entries beyond base vocabulary + size_t num_merges = item.merges.size() - bpe_tokenizer::BPE_BASE_VOCAB_SIZE; + serialize(num_merges, out); + + for (size_t i = bpe_tokenizer::BPE_BASE_VOCAB_SIZE; i < item.merges.size(); ++i) { + const auto& m = item.merges[i]; + serialize(m.left, out); + serialize(m.right, out); + serialize(m.pattern, out); + } + } + + // Deserialization + friend void deserialize(bpe_tokenizer& item, std::istream& in) { - return (vocab.size() + special_tokens.size()); + std::string version; + deserialize(version, in); + if (version != "bpe_tokenizer_") + throw serialization_error("Unexpected version found while deserializing dlib::bpe_tokenizer."); + deserialize(item.vocab_size, in); + + // Initialize base vocabulary + item.merges.clear(); + for (int i = 0; i < bpe_tokenizer::BPE_BASE_VOCAB_SIZE; ++i) { + Merge m; + m.token_id = i; + m.left = i; + m.right = i; + m.pattern.push_back(static_cast(i)); + item.merges.push_back(m); + } + + // Deserialize merge entries + size_t num_merges; + deserialize(num_merges, in); + for (size_t i = 0; i < num_merges; ++i) { + Merge m; + m.token_id = bpe_tokenizer::BPE_BASE_VOCAB_SIZE + i; + deserialize(m.left, in); + deserialize(m.right, in); + deserialize(m.pattern, in); + item.merges.push_back(m); + } + + // Initialize special tokens + item.initialize_special_tokens(); } private: + // Define special tokens + const std::vector special_token_list = { + "", "", "", "", + "", "", "", + "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "", + "", "", "", "" + }; + static const int BPE_BASE_VOCAB_SIZE = 256; + + // Merge structure + struct Merge { + int token_id; + int left; + int right; + std::vector pattern; + }; + + // Data members + size_t vocab_size; + std::vector merges; + + // Special tokens handling std::map special_tokens; std::unordered_map special_token_map; - std::map, int> merges; - std::map> vocab; - int vocab_size; - - // Get frequency statistics of adjacent token pairs - struct pair_hash { - template - std::size_t operator()(const std::pair& p) const - { - auto hash1 = std::hash{}(p.first); - auto hash2 = std::hash{}(p.second); - return hash1 ^ (hash2 << 1); - } - }; - std::unordered_map, int, pair_hash> get_stats(const std::vector& ids) const + + void initialize_special_tokens() { - std::unordered_map, int, pair_hash> global_stats; - std::mutex global_stats_mutex; - - auto worker = [&](size_t start, size_t end) { - std::unordered_map, int, pair_hash> local_stats; - for (size_t i = start; i < end - 1 && i + 1 < ids.size(); ++i) - local_stats[{ids[i], ids[i + 1]}]++; - - std::lock_guard lock(global_stats_mutex); - for (const auto& pair : local_stats) - global_stats[pair.first] += pair.second; - }; - - size_t num_threads = std::thread::hardware_concurrency(); - size_t segment_size = ids.size() / num_threads; - std::vector threads; - - for (size_t t = 0; t < num_threads; ++t) - { - size_t start = t * segment_size; - size_t end = (t == num_threads - 1) ? ids.size() : start + segment_size; - threads.emplace_back(worker, start, end); - } + special_tokens.clear(); + special_token_map.clear(); - for (auto& thread : threads) thread.join(); + // Initialize special tokens with sequential IDs + int next_id = get_vocab_without_specials_size(); - return global_stats; + for (const auto& token : special_token_list) { + special_tokens[token] = next_id; + special_token_map[next_id] = token; + next_id++; + } } - // Finds the most frequent pair of tokens in the given statistics map that does not exceed the maximum token length - std::pair get_most_frequent_pair(const std::unordered_map, int, pair_hash>& stats) const + // Segment-based training functions from BPETokenizer + void tokenize(const std::vector& data, + std::vector>& segments, + std::vector& segment_counts) { - std::pair best_pair = { -1, -1 }; // Initialize the best pair to an invalid value - double max_score = 0; // Initialize the maximum score to 0 - - // Iterate over all pairs in the statistics map - for (const auto& stat : stats) { - const std::pair& pair = stat.first; // Extract the token pair - int count = stat.second; // Extract the frequency count - - // Check if the new token formed by merging the pair would exceed the maximum allowed length - size_t new_token_length = vocab.at(pair.first).size() + vocab.at(pair.second).size(); - if (new_token_length > BPE_TOKENIZER_MAX_TOKEN_LENGTH) continue; // Skip this pair if it exceeds the maximum token length - - // Calculate the score for this pair (frequency * length_penalty) - double score = (size_t)count * (new_token_length > (BPE_TOKENIZER_MAX_TOKEN_LENGTH / 2) ? 1.75 : 1.0); - - // Update the best pair if the current pair has a higher score - if (score > max_score) - { - best_pair = pair; - max_score = score; + segments.clear(); + segment_counts.clear(); + + // Split on whitespace and newlines to create meaningful segments + std::list current_segment; + + for (size_t i = 0; i < data.size(); i++) { + uint8_t byte = data[i]; + + // Split on whitespace and newlines + if (byte == ' ' || byte == '\n' || byte == '\t' || byte == '\r') { + if (!current_segment.empty()) { + segments.push_back(current_segment); + segment_counts.push_back(1); + current_segment.clear(); + } + // Add the delimiter as its own segment + segments.push_back({ static_cast(byte) }); + segment_counts.push_back(1); + } + else { + current_segment.push_back(static_cast(byte)); } } - return best_pair; // Return the pair with the highest score + // Add the last segment if not empty + if (!current_segment.empty()) { + segments.push_back(current_segment); + segment_counts.push_back(1); + } } - // Merge the most frequent pair in the token sequence - std::vector merge(std::vector& ids, const std::pair& pair, int idx) const + // Count pairs in a segment and update global pair counts + void countInSegment(const std::list& segment, uint32_t segment_idx, + int32_t count_delta, + std::unordered_map>>& pair_counts, + std::unordered_map>& where_to_update) { - std::vector new_ids; - new_ids.reserve(ids.size()); // Reserve space to avoid reallocations - - for (size_t i = 0; i < ids.size(); ++i) - { - if (i < ids.size() - 1 && ids[i] == pair.first && ids[i + 1] == pair.second) - { - new_ids.push_back(idx); // Replace the pair with the new token ID - i++; // Skip the next token + if (segment.size() < 2) return; + + auto it = segment.begin(); + uint16_t prev_token = *it; + ++it; + + while (it != segment.end()) { + uint16_t curr_token = *it; + uint32_t pair_key = (static_cast(prev_token) << 16) | curr_token; + + auto& entry = pair_counts[pair_key]; + entry.first += count_delta; + entry.second = { prev_token, curr_token }; + + if (count_delta > 0) { + where_to_update[pair_key].insert(segment_idx); + } + else { + where_to_update[pair_key].erase(segment_idx); } - else new_ids.push_back(ids[i]); // Keep the current token - } - return new_ids; + prev_token = curr_token; + ++it; + } } - static std::string base64_encode(const std::string& input) { - dlib::base64 encoder; - std::istringstream sin(input); - std::ostringstream sout; - encoder.encode(sin, sout); - return sout.str(); + // Find the most frequent pair + std::pair findMaxPair( + const std::unordered_map>>& pair_counts, + int32_t& max_count) + { + max_count = 0; + std::pair max_pair = { 0, 0 }; + + for (const auto& entry : pair_counts) { + int32_t count = entry.second.first; + const auto& pair = entry.second.second; + + if (count > max_count || (count == max_count && pair < max_pair)) { + max_count = count; + max_pair = pair; + } + } + + return max_pair; } - // Convert a sequence of bytes to a readable string - static std::string bytes_to_string(const std::vector& bytes) + // Apply a merge to all affected segments + void applyMerge(const std::pair& merge_pair, uint16_t new_token_id, + std::vector>& segments, + const std::vector& segment_counts, + const std::unordered_set& affected_segments, + std::unordered_map>>& pair_counts, + std::unordered_map>& where_to_update) { - std::string data(bytes.begin(), bytes.end()); - return base64_encode(data); - } + for (uint32_t segment_idx : affected_segments) { + auto& segment = segments[segment_idx]; + int32_t count = segment_counts[segment_idx]; + + // Remove old pair counts for this segment + countInSegment(segment, segment_idx, -count, pair_counts, where_to_update); + + // Apply merge in this segment + auto it = segment.begin(); + while (it != segment.end()) { + auto next_it = std::next(it); + if (next_it != segment.end() && *it == merge_pair.first && *next_it == merge_pair.second) { + *it = new_token_id; + segment.erase(next_it); + // Don't increment it, check the same position again + } + else { + ++it; + } + } + // Add new pair counts for this segment + countInSegment(segment, segment_idx, count, pair_counts, where_to_update); + } + } }; } - -#endif // DLIB_BPE_TOKENIZER_H +#endif // DLIB_BPE_TOKENIZER_H \ No newline at end of file diff --git a/dlib/tokenizer/bpe_tokenizer_abstract.h b/dlib/tokenizer/bpe_tokenizer_abstract.h index 1fa421d13a..d1b0f6b724 100644 --- a/dlib/tokenizer/bpe_tokenizer_abstract.h +++ b/dlib/tokenizer/bpe_tokenizer_abstract.h @@ -27,24 +27,20 @@ namespace dlib handling out-of-vocabulary words and reducing the size of the vocabulary while maintaining the ability to represent any text. - The tokenizer supports special tokens, which can be used to mark specific elements - in the text (e.g., ``, ``, ``, etc.). These special tokens are - treated as atomic units during tokenization and are not subject to further splitting. - - The class provides methods for training the tokenizer on a given text corpus, encoding - text into subword tokens, and decoding tokens back into text. The tokenizer can be - serialized and deserialized to/from a file, allowing for easy storage and reuse. + Key Features: + - Base vocabulary of 256 single-byte tokens (0-255) + - 28 predefined special tokens (e.g., , , , etc.) + - Dynamic vocabulary expansion through merges during training + - Configurable training with: + * Vocabulary size control + * Byte-level truncation (via max_bytes) + * Progress reporting REFERENCES - Sennrich, R., Haddow, B., & Birch, A. (2016). Neural Machine Translation of Rare Words with Subword Units. In Proceedings of the 54th Annual Meeting of the Association for Computational Linguistics (ACL 2016). - - INITIAL VALUE - - The base vocabulary is initialized with single-byte tokens (0-255). - - Special tokens are pre-defined and assigned IDs starting from 256. - - The maximum token length is set to 8 bytes. - !*/ + */ public: bpe_tokenizer(); @@ -56,16 +52,18 @@ namespace dlib void train( const std::string& text, - int vocab_size, + size_t vocab_size, + size_t max_bytes = 0, bool verbose = false ); /*! requires - vocab_size >= 256 ensures - - Trains the tokenizer on the provided text corpus. + - Trains the tokenizer on the provided text corpus, or up to `max_bytes`. - Iteratively merges the most frequent pairs of tokens to form a subword vocabulary of size `vocab_size`. + - If max_bytes==0, uses entire text. - If `verbose` is true, progress information is printed to the standard output. !*/ @@ -90,8 +88,10 @@ namespace dlib - Returns the decoded text as a UTF-8 encoded string. !*/ - std::string decode(int id, bool display_special_tokens = true) const - { return decode(std::vector({ id }), display_special_tokens); } + std::string decode( + int id, + bool display_special_tokens = true + ) const; /*! ensures - decode a single token back into text. @@ -106,11 +106,23 @@ namespace dlib - Throws an exception if the token is not found in the special tokens map. !*/ + size_t get_specials_size() const; + /*! + ensures + - Returns count of special tokens + */ + size_t get_vocab_size() const; /*! ensures - Returns the total size of the vocabulary, including base tokens and special tokens. !*/ + + size_t get_vocab_without_specials_size() const; + /*! + ensures + - Returns vocabulary size excluding special tokens + */ }; void serialize( diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index c23067879a..1232d58b09 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -147,6 +147,7 @@ add_gui_example(dnn_dcgan_train_ex) add_gui_example(dnn_yolo_train_ex) add_gui_example(dnn_self_supervised_learning_ex) add_example(slm_basic_train_ex) +add_example(slm_advanced_train_ex) add_gui_example(3d_point_cloud_ex) add_example(bayes_net_ex) add_example(bayes_net_from_disk_ex) diff --git a/examples/slm_advanced_train_ex.cpp b/examples/slm_advanced_train_ex.cpp new file mode 100644 index 0000000000..4ed6ffec22 --- /dev/null +++ b/examples/slm_advanced_train_ex.cpp @@ -0,0 +1,1362 @@ +/*! + @file slm_advanced_train_ex.cpp + @brief Transformer-based text training/generation + + This program implements a complete training and generation pipeline for a + Transformer-based text compression system. + The model features: + + 1. Rotary Positional Embeddings (RoPE) for enhanced positional encoding + 2. Multi-head self-attention with efficient memory handling + 3. Mixture-of-Experts architecture for specialized processing + 4. BPE tokenization with custom vocabulary + 5. Full training/generation/verification workflow + + Key capabilities demonstrated: + - Perfect memorization and reproduction of training text + - Efficient autoregressive generation + - Byte-level verification of reconstructed text + + References: + [1] Vaswani et al., "Attention Is All You Need" (Transformer architecture) + arXiv:1706.03762 + [2] Su et al., "RoFormer: Enhanced Transformer with Rotary Position Embedding" + arXiv:2104.09864 + [3] Shazeer et al., "Outrageously Large Neural Networks: The Sparsely-Gated + Mixture-of-Experts Layer" (MoE architecture) arXiv:1701.06538 + + Usage modes: + --train Train model on enwiki dataset + --generate Generate text from trained model + --verify Compare generated output with original + --tokenize-only Only perform tokenization step + + Configuration: + - Adjust template parameters in transformer_config for model architecture + - Modify training parameters in main() for optimization + - Set sequence length and memory limits according to available hardware +!*/ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace dlib; + +namespace dlib +{ + /*! + @class rotary_positional_embedding_ + @brief Implements Rotary Positional Embeddings (RoPE) for transformers + + This layer applies rotary positional embeddings to queries and keys in + self-attention layers, providing relative positional information without + absolute position embeddings. + + The implementation follows the RoPE formulation from [2], where positions + are encoded through rotation matrices applied to pairs of dimensions. + !*/ + class rotary_positional_embedding_ { + public: + explicit rotary_positional_embedding_() = default; + + template + void setup(const SUBNET& sub) { + // Precompute the rotation angles and their trigonometric values + seq_len = sub.get_output().nr(); + d_head = sub.get_output().nc(); + compute_rotation_angles(); + precompute_trigonometric_values(); + } + + template + void forward(const SUBNET& sub, resizable_tensor& output) { + const tensor& input = sub.get_output(); + output.copy_size(input); + tt::copy_tensor(false, output, 0, input, 0, input.k()); + + // Apply rotary embedding to the output + apply_rotary_embedding(output); + } + + template + void backward( + const tensor& gradient_input, + SUBNET& sub, + tensor& params_grad + ) { + tensor& prev = sub.get_gradient_input(); + resizable_tensor grad_output; + grad_output.copy_size(gradient_input); + tt::copy_tensor(false, grad_output, 0, gradient_input, 0, gradient_input.k()); + + // Apply the inverse rotation to the gradient (transpose of the rotation matrix) + apply_rotary_embedding(grad_output, true); + tt::copy_tensor(true, prev, 0, grad_output, 0, grad_output.k()); + } + + const tensor& get_layer_params() const { return params; } + tensor& get_layer_params() { return params; } + + friend void serialize(const rotary_positional_embedding_& item, std::ostream& out) { + std::string version = "rotary_positional_embedding_"; + dlib::serialize(version, out); + dlib::serialize(item.seq_len, out); + dlib::serialize(item.d_head, out); + dlib::serialize(item.angles, out); + dlib::serialize(item.cos_values, out); + dlib::serialize(item.sin_values, out); + } + + friend void deserialize(rotary_positional_embedding_& item, std::istream& in) { + std::string version; + dlib::deserialize(version, in); + if (version != "rotary_positional_embedding_") + throw serialization_error("Unexpected version found while deserializing rotary_positional_embedding_."); + dlib::deserialize(item.seq_len, in); + dlib::deserialize(item.d_head, in); + dlib::deserialize(item.angles, in); + dlib::deserialize(item.cos_values, in); + dlib::deserialize(item.sin_values, in); + } + + friend std::ostream& operator<<(std::ostream& out, const rotary_positional_embedding_& item) { + out << "rotary_positional_embedding"; + out << " (d_head=" << item.d_head << ", seq_len=" << item.seq_len << ")"; + return out; + } + + friend void to_xml(const rotary_positional_embedding_& item, std::ostream& out) + { + out << "\n"; + } + + protected: + void compute_rotation_angles() { + // Following the original RoPE paper formulation + const float base = 10000.0f; + const long half_dim = d_head / 2; + angles.set_size(seq_len, half_dim); + + for (long pos = 0; pos < seq_len; ++pos) { + for (long i = 0; i < half_dim; ++i) { + float inv_freq = std::pow(base, -2.0f * (i + 0.5f) / d_head); + angles(pos, i) = pos * inv_freq; + } + } + } + + void precompute_trigonometric_values() { + // Precompute cos and sin for all angles + cos_values.set_size(angles.nr(), angles.nc()); + sin_values.set_size(angles.nr(), angles.nc()); + + for (long i = 0; i < angles.size(); ++i) { + cos_values(i) = std::cos(angles(i)); + sin_values(i) = std::sin(angles(i)); + } + } + + template + void apply_rotary_embedding( + tensor_type& x, + bool is_backward = false + ) const { + DLIB_CASSERT(x.nc() == d_head, "Input dimension must match d_head param"); + DLIB_CASSERT(x.nr() == seq_len, "Sequence length must match seq_len param"); + + const long batch_size = x.num_samples(); + const long num_heads = x.k(); + const bool is_odd = (d_head % 2 != 0); + const long rot_dim = is_odd ? d_head - 1 : d_head; + + auto* ptr = x.host(); + const long stride = seq_len * d_head; + + for (long n = 0; n < batch_size; ++n) { + for (long h = 0; h < num_heads; ++h) { + auto* x_ptr = ptr + (n * num_heads + h) * stride; + + for (long pos = 0; pos < seq_len; ++pos) { + const float* cos = &cos_values(pos, 0); + const float* sin = &sin_values(pos, 0); + + for (long i = 0; i < rot_dim; i += 2) { + const float x0 = x_ptr[pos * d_head + i]; + const float x1 = x_ptr[pos * d_head + i + 1]; + + if (!is_backward) { + x_ptr[pos * d_head + i] = x0 * cos[i / 2] - x1 * sin[i / 2]; + x_ptr[pos * d_head + i + 1] = x0 * sin[i / 2] + x1 * cos[i / 2]; + } + else { + x_ptr[pos * d_head + i] = x0 * cos[i / 2] + x1 * sin[i / 2]; + x_ptr[pos * d_head + i + 1] = -x0 * sin[i / 2] + x1 * cos[i / 2]; + } + } + } + } + } + } + + private: + long seq_len, d_head; // Sequence length and dimension of each head + matrix angles; // Precomputed rotation angles (seq_len x d_head/2) + matrix cos_values; // Precomputed cosine values + matrix sin_values; // Precomputed sine values + resizable_tensor params; // Empty tensor (no learnable parameters) + }; + + // Helper to easily add RoPE to a network + template + using rope = add_layer; + + template + class scale_weights_ : public multiply_ { + public: + explicit scale_weights_() : multiply_(1.0f / std::sqrt(static_cast(d_k_))) {} + }; + + template + using scale_weights = add_layer, SUBNET>; + + // Attention mechanism component extractors + template + using query = reshape_to>; + + template + using key = reshape_to>; + + template + using value = reshape_to>; + + /*! + This layer implements multi-head self-attention. + + Template parameters: + - ACT: Activation function type + - DO: Dropout layer type for regularization + - d_model: Model dimension (must be divisible by num_heads) + - num_heads: Number of attention heads + !*/ + template