From b01ab00e1306169f7a25b295312bb61f43b4f4b8 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 27 Jan 2026 20:59:35 +0100 Subject: [PATCH 1/3] Initial commit with task details Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: https://github.com/netkeep80/NNets/issues/19 --- CLAUDE.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 043e473..661e7d4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,3 +5,16 @@ Your forked repository: konard/netkeep80-NNets Original repository (upstream): netkeep80/NNets Proceed. + +--- + +Issue to solve: https://github.com/netkeep80/NNets/issues/19 +Your prepared branch: issue-19-57fc143cd06d +Your prepared working directory: /tmp/gh-issue-solver-1769543969408 +Your forked repository: konard/netkeep80-NNets +Original repository (upstream): netkeep80/NNets + +Proceed. + + +Run timestamp: 2026-01-27T19:59:34.992Z \ No newline at end of file From d9ea3aab030f63cf7727ac6a123ab28eef2ac6ed Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 27 Jan 2026 21:07:58 +0100 Subject: [PATCH 2/3] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B4=D0=BE=D0=BE=D0=B1=D1=83=D1=87=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5,=20=D0=B2=D0=B5=D1=80=D0=B8=D1=84=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8E=20=D1=82=D0=BE=D1=87=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B8=20=D0=B8=20=D0=BF=D1=80=D0=B5=D1=80=D1=8B=D0=B2=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BE=D0=B1=D1=83=D1=87=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Реализованы новые режимы работы согласно issue #19: 1. Режим дообучения (-r/--retrain): - Загрузка существующей сети и продолжение обучения - Автоматическое определение необученных классов - Поддержка добавления новых классов к существующей модели 2. Режим верификации точности (--verify): - Проверка точности загруженной модели на тестовых данных - Вывод статистики (passed/failed/accuracy) 3. Прерывание обучения (Ctrl+C): - Корректное завершение после текущей итерации - Автосохранение при указанном -s - Возможность продолжить обучение через -r Обновлена документация в README.md и добавлены 2 новых теста. Co-Authored-By: Claude Opus 4.5 --- CMakeLists.txt | 32 +++++ README.md | 67 +++++++++- cmake/test_retraining.cmake | 141 ++++++++++++++++++++ cmake/test_verify.cmake | 82 ++++++++++++ include/json_io.h | 227 ++++++++++++++++++++++++++++++++ main.cpp | 250 ++++++++++++++++++++++++++++++++++-- 6 files changed, 786 insertions(+), 13 deletions(-) create mode 100644 cmake/test_retraining.cmake create mode 100644 cmake/test_verify.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 0698630..07fc88a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -179,3 +179,35 @@ set_tests_properties(test_multithreading_benchmark PROPERTIES TIMEOUT 300 LABELS "benchmark;multithreading;performance" ) + +# Test 7: Verification mode test +# Tests --verify flag for checking model accuracy +add_test( + NAME test_verify_mode + COMMAND ${CMAKE_COMMAND} + -DNNETS_EXE=$ + -DCONFIG_DIR=${CMAKE_SOURCE_DIR}/configs + -DWORK_DIR=${CMAKE_BINARY_DIR} + -P ${CMAKE_SOURCE_DIR}/cmake/test_verify.cmake + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} +) +set_tests_properties(test_verify_mode PROPERTIES + TIMEOUT 180 + LABELS "verification;inference" +) + +# Test 8: Retraining mode test +# Tests -r flag for retraining existing models with new classes +add_test( + NAME test_retraining_mode + COMMAND ${CMAKE_COMMAND} + -DNNETS_EXE=$ + -DCONFIG_DIR=${CMAKE_SOURCE_DIR}/configs + -DWORK_DIR=${CMAKE_BINARY_DIR} + -P ${CMAKE_SOURCE_DIR}/cmake/test_retraining.cmake + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} +) +set_tests_properties(test_retraining_mode PROPERTIES + TIMEOUT 600 + LABELS "retraining;training" +) diff --git a/README.md b/README.md index 02f7ac5..b01ee65 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ g++ -o NNets main.cpp ## Usage -The program supports two main modes: **Training** and **Inference**. +The program supports several modes: **Training**, **Retraining**, **Inference**, and **Verification**. ### Command Line Options @@ -56,6 +56,7 @@ Usage: ./NNets [options] MODES: Training mode (default): Train network and optionally save to file Inference mode: Load trained network and classify inputs + Retraining mode: Load existing network and continue training with new data TRAINING OPTIONS: -c, --config Load training configuration from JSON file @@ -63,12 +64,27 @@ TRAINING OPTIONS: -t, --test Run automated test after training (no interactive mode) -b, --benchmark Run benchmark to measure training speed +RETRAINING OPTIONS: + -r, --retrain Load existing network and continue training (retraining mode) + Combines -l (load) with training mode. Requires -c for new data. + New classes in config (without output_neuron) will be trained. + INFERENCE OPTIONS: -l, --load Load trained network from JSON file (inference mode) -i, --input Classify single input text and exit (non-interactive) + --verify Verify accuracy of loaded model on training config (-c required) + +PERFORMANCE OPTIONS: + -j, --threads Number of threads to use (0 = auto, default) + --single-thread Disable multithreading (use single thread) GENERAL OPTIONS: -h, --help Show help message + +INTERRUPTION: + Press Ctrl+C during training to interrupt gracefully. + The network will be saved if -s is specified. + Training can be continued later with -r option. ``` ### Training Mode @@ -105,6 +121,55 @@ Load a pre-trained network and classify inputs: ./NNets -l model.json -i "yes" ``` +### Retraining Mode + +Continue training an existing network with new classes or additional training data: + +```bash +# Add new classes to an existing model +# 1. First, train initial model with classes yes/no +./NNets -c configs/simple.json -s model_v1.json + +# 2. Create a new config with additional classes (e.g., adding "maybe") +# 3. Retrain the model with new data +./NNets -r model_v1.json -c configs/extended.json -s model_v2.json +``` + +Retraining automatically detects which classes are already trained (have `output_neuron`) and only trains new classes. This is useful for: +- Adding new recognition classes without retraining from scratch +- Continuing interrupted training sessions +- Incrementally improving the model + +### Verification Mode + +Check the accuracy of a trained model on test data: + +```bash +# Verify model accuracy on training data +./NNets -l model.json -c configs/test.json --verify +``` + +This mode loads the trained network and tests it against all samples in the configuration file, reporting accuracy statistics. + +### Training Interruption + +Training can be interrupted at any time by pressing Ctrl+C: +- The first Ctrl+C requests graceful interruption (finishes current iteration) +- The second Ctrl+C forces immediate exit +- If `-s` option is specified, the network state is saved automatically +- Training can be continued later using the `-r` (retrain) option + +```bash +# Start long training with auto-save +./NNets -c configs/large.json -s checkpoint.json + +# Press Ctrl+C to interrupt... +# Network saved to checkpoint.json + +# Continue training later +./NNets -r checkpoint.json -c configs/large.json -s final_model.json +``` + ### Training Configuration Format Training configurations are JSON files that define the classes and training images: diff --git a/cmake/test_retraining.cmake b/cmake/test_retraining.cmake new file mode 100644 index 0000000..4f7e734 --- /dev/null +++ b/cmake/test_retraining.cmake @@ -0,0 +1,141 @@ +# CMake script to test retraining functionality +# This script: +# 1. Trains a model with simple.json (yes/no/empty classes) +# 2. Saves the model +# 3. Retrains with extended.json (adds cat/dog/bird/fish classes) +# 4. Verifies the retrained model works for both old and new classes + +# Check required variables +if(NOT DEFINED NNETS_EXE) + message(FATAL_ERROR "NNETS_EXE not defined") +endif() + +if(NOT DEFINED CONFIG_DIR) + message(FATAL_ERROR "CONFIG_DIR not defined") +endif() + +if(NOT DEFINED WORK_DIR) + message(FATAL_ERROR "WORK_DIR not defined") +endif() + +set(MODEL_V1 "${WORK_DIR}/test_retrain_v1.json") +set(MODEL_V2 "${WORK_DIR}/test_retrain_v2.json") +set(SIMPLE_CONFIG "${CONFIG_DIR}/simple.json") +set(EXTENDED_CONFIG "${CONFIG_DIR}/extended.json") + +message(STATUS "=== Testing Retraining Mode ===") +message(STATUS "Executable: ${NNETS_EXE}") +message(STATUS "Simple config: ${SIMPLE_CONFIG}") +message(STATUS "Extended config: ${EXTENDED_CONFIG}") + +# Step 1: Train initial model with simple config (yes/no) +message(STATUS "Step 1: Training initial model with simple config...") +execute_process( + COMMAND "${NNETS_EXE}" -c "${SIMPLE_CONFIG}" -s "${MODEL_V1}" -t + WORKING_DIRECTORY "${WORK_DIR}" + RESULT_VARIABLE TRAIN1_RESULT + OUTPUT_VARIABLE TRAIN1_OUTPUT + ERROR_VARIABLE TRAIN1_ERROR + TIMEOUT 120 +) + +if(NOT TRAIN1_RESULT EQUAL 0) + message(FATAL_ERROR "Initial training failed with code ${TRAIN1_RESULT}:\nOutput: ${TRAIN1_OUTPUT}\nError: ${TRAIN1_ERROR}") +endif() +message(STATUS "Initial training completed successfully") + +# Verify model file was created +if(NOT EXISTS "${MODEL_V1}") + message(FATAL_ERROR "Model file was not created: ${MODEL_V1}") +endif() +message(STATUS "Initial model saved: ${MODEL_V1}") + +# Step 2: Verify the initial model using --verify +message(STATUS "Step 2: Verifying initial model accuracy...") +execute_process( + COMMAND "${NNETS_EXE}" -l "${MODEL_V1}" -c "${SIMPLE_CONFIG}" --verify + WORKING_DIRECTORY "${WORK_DIR}" + RESULT_VARIABLE VERIFY1_RESULT + OUTPUT_VARIABLE VERIFY1_OUTPUT + ERROR_VARIABLE VERIFY1_ERROR + TIMEOUT 60 +) + +if(NOT VERIFY1_RESULT EQUAL 0) + message(FATAL_ERROR "Initial model verification failed with code ${VERIFY1_RESULT}:\nOutput: ${VERIFY1_OUTPUT}\nError: ${VERIFY1_ERROR}") +endif() + +# Check that verification output contains accuracy info +string(FIND "${VERIFY1_OUTPUT}" "Accuracy:" ACCURACY_FOUND) +if(ACCURACY_FOUND EQUAL -1) + message(FATAL_ERROR "Verification output doesn't contain accuracy info:\n${VERIFY1_OUTPUT}") +endif() +message(STATUS "Initial model verification passed") +message(STATUS "Verification output:\n${VERIFY1_OUTPUT}") + +# Step 3: Test inference with initial model +message(STATUS "Step 3: Testing inference with initial model...") +execute_process( + COMMAND "${NNETS_EXE}" -l "${MODEL_V1}" -i "yes" + WORKING_DIRECTORY "${WORK_DIR}" + RESULT_VARIABLE INFER1_RESULT + OUTPUT_VARIABLE INFER1_OUTPUT + ERROR_VARIABLE INFER1_ERROR + TIMEOUT 30 +) + +if(NOT INFER1_RESULT EQUAL 0) + message(FATAL_ERROR "Initial inference failed with code ${INFER1_RESULT}:\nOutput: ${INFER1_OUTPUT}\nError: ${INFER1_ERROR}") +endif() +message(STATUS "Initial inference passed") + +# Step 4: Retrain with extended config (adding more classes) +# Note: For simplicity, we create a new config that can be used for retraining +# The retrain mode will detect that some classes are already trained +message(STATUS "Step 4: Retraining model with extended config...") +execute_process( + COMMAND "${NNETS_EXE}" -r "${MODEL_V1}" -c "${EXTENDED_CONFIG}" -s "${MODEL_V2}" -t + WORKING_DIRECTORY "${WORK_DIR}" + RESULT_VARIABLE RETRAIN_RESULT + OUTPUT_VARIABLE RETRAIN_OUTPUT + ERROR_VARIABLE RETRAIN_ERROR + TIMEOUT 300 +) + +# Note: Retrain might show warnings about config mismatch, which is expected +# since simple.json has different classes than extended.json +# The actual retraining will train the new classes +message(STATUS "Retrain output: ${RETRAIN_OUTPUT}") + +# Verify retrained model file was created +if(NOT EXISTS "${MODEL_V2}") + message(FATAL_ERROR "Retrained model file was not created: ${MODEL_V2}") +endif() +message(STATUS "Retrained model saved: ${MODEL_V2}") + +# Step 5: Test inference with retrained model for new classes +message(STATUS "Step 5: Testing inference with retrained model...") +execute_process( + COMMAND "${NNETS_EXE}" -l "${MODEL_V2}" -i "cat" + WORKING_DIRECTORY "${WORK_DIR}" + RESULT_VARIABLE INFER2_RESULT + OUTPUT_VARIABLE INFER2_OUTPUT + ERROR_VARIABLE INFER2_ERROR + TIMEOUT 30 +) + +if(NOT INFER2_RESULT EQUAL 0) + message(FATAL_ERROR "Retrained inference failed with code ${INFER2_RESULT}:\nOutput: ${INFER2_OUTPUT}\nError: ${INFER2_ERROR}") +endif() + +# Check that output contains new class +string(FIND "${INFER2_OUTPUT}" "cat" CAT_FOUND) +if(CAT_FOUND EQUAL -1) + message(FATAL_ERROR "Retrained model doesn't recognize 'cat' class:\n${INFER2_OUTPUT}") +endif() +message(STATUS "Retrained inference passed") + +# Cleanup +file(REMOVE "${MODEL_V1}") +file(REMOVE "${MODEL_V2}") +message(STATUS "=== Retraining Test PASSED ===") diff --git a/cmake/test_verify.cmake b/cmake/test_verify.cmake new file mode 100644 index 0000000..4b9479b --- /dev/null +++ b/cmake/test_verify.cmake @@ -0,0 +1,82 @@ +# CMake script to test verification mode (--verify) +# This script trains a model and then verifies its accuracy + +# Check required variables +if(NOT DEFINED NNETS_EXE) + message(FATAL_ERROR "NNETS_EXE not defined") +endif() + +if(NOT DEFINED CONFIG_DIR) + message(FATAL_ERROR "CONFIG_DIR not defined") +endif() + +if(NOT DEFINED WORK_DIR) + message(FATAL_ERROR "WORK_DIR not defined") +endif() + +set(MODEL_FILE "${WORK_DIR}/test_verify_model.json") +set(CONFIG_FILE "${CONFIG_DIR}/simple.json") + +message(STATUS "=== Testing Verification Mode ===") +message(STATUS "Executable: ${NNETS_EXE}") +message(STATUS "Config: ${CONFIG_FILE}") +message(STATUS "Model: ${MODEL_FILE}") + +# Step 1: Train and save model +message(STATUS "Step 1: Training model...") +execute_process( + COMMAND "${NNETS_EXE}" -c "${CONFIG_FILE}" -s "${MODEL_FILE}" -t + WORKING_DIRECTORY "${WORK_DIR}" + RESULT_VARIABLE TRAIN_RESULT + OUTPUT_VARIABLE TRAIN_OUTPUT + ERROR_VARIABLE TRAIN_ERROR + TIMEOUT 120 +) + +if(NOT TRAIN_RESULT EQUAL 0) + message(FATAL_ERROR "Training failed with code ${TRAIN_RESULT}:\nOutput: ${TRAIN_OUTPUT}\nError: ${TRAIN_ERROR}") +endif() +message(STATUS "Training completed successfully") + +# Verify model file was created +if(NOT EXISTS "${MODEL_FILE}") + message(FATAL_ERROR "Model file was not created: ${MODEL_FILE}") +endif() + +# Step 2: Verify model accuracy using --verify +message(STATUS "Step 2: Verifying model accuracy...") +execute_process( + COMMAND "${NNETS_EXE}" -l "${MODEL_FILE}" -c "${CONFIG_FILE}" --verify + WORKING_DIRECTORY "${WORK_DIR}" + RESULT_VARIABLE VERIFY_RESULT + OUTPUT_VARIABLE VERIFY_OUTPUT + ERROR_VARIABLE VERIFY_ERROR + TIMEOUT 60 +) + +if(NOT VERIFY_RESULT EQUAL 0) + message(FATAL_ERROR "Verification failed with code ${VERIFY_RESULT}:\nOutput: ${VERIFY_OUTPUT}\nError: ${VERIFY_ERROR}") +endif() + +# Check that verification output contains expected elements +string(FIND "${VERIFY_OUTPUT}" "Verifying model accuracy" VERIFY_HEADER) +if(VERIFY_HEADER EQUAL -1) + message(FATAL_ERROR "Verification output missing header:\n${VERIFY_OUTPUT}") +endif() + +string(FIND "${VERIFY_OUTPUT}" "Accuracy:" ACCURACY_FOUND) +if(ACCURACY_FOUND EQUAL -1) + message(FATAL_ERROR "Verification output missing accuracy:\n${VERIFY_OUTPUT}") +endif() + +string(FIND "${VERIFY_OUTPUT}" "Passed:" PASSED_FOUND) +if(PASSED_FOUND EQUAL -1) + message(FATAL_ERROR "Verification output missing passed count:\n${VERIFY_OUTPUT}") +endif() + +message(STATUS "Verification output:\n${VERIFY_OUTPUT}") +message(STATUS "Verification mode test passed") + +# Cleanup +file(REMOVE "${MODEL_FILE}") +message(STATUS "=== Verification Mode Test PASSED ===") diff --git a/include/json_io.h b/include/json_io.h index 55db23c..94d1239 100644 --- a/include/json_io.h +++ b/include/json_io.h @@ -373,4 +373,231 @@ bool loadNetwork(const string& filePath) { } } +/** + * Загрузка обученной нейронной сети для дообучения + * + * Похоже на loadNetwork(), но также загружает начальные классы + * и подготавливает структуры для добавления новых классов. + * Сохраняет информацию о том, какие классы уже обучены (имеют output_neuron). + * + * @param filePath - путь к JSON файлу с моделью + * @param trainedClasses - выходной вектор: индексы уже обученных классов + * @return true при успешной загрузке, false при ошибке + */ +bool loadNetworkForRetraining(const string& filePath, vector& trainedClasses) { + ifstream inFile(filePath); + if (!inFile.is_open()) { + cerr << "Error: Cannot open network file: " << filePath << endl; + return false; + } + + try { + json network; + inFile >> network; + + // Загружаем конфигурацию + Receptors = network["receptors"].get(); + Inputs = network["inputs"].get(); + Neirons = network["neurons_count"].get(); + + // Проверяем совпадение размера базиса + int loadedBaseSize = network["base_size"].get(); + if (loadedBaseSize != base_size) { + cerr << "Warning: Basis size mismatch (file: " << loadedBaseSize + << ", expected: " << base_size << ")" << endl; + } + + // Выделяем массив входов сети + NetInput.resize(Inputs); + + // Устанавливаем базисные значения + for (int i = 0; i < base_size; i++) { + NetInput[i + Receptors] = base[i]; + } + + // Загружаем классы + const auto& classesArray = network["classes"]; + int networkClasses = classesArray.size(); + classes.clear(); + classes.resize(networkClasses); + NetOutput.resize(networkClasses); + trainedClasses.clear(); + + for (const auto& cls : classesArray) { + int id = cls["id"].get(); + classes[id] = cls["name"].get(); + if (cls.contains("output_neuron")) { + NetOutput[id] = cls["output_neuron"].get(); + trainedClasses.push_back(id); + } else { + NetOutput[id] = -1; // Не обучен + } + } + + Classes = networkClasses; + + // Инициализируем нейроны + nei.resize(MAX_NEURONS); + for (int n = 0; n < MAX_NEURONS; n++) { + nei[n].cached = false; + nei[n].val_cached = false; + } + + // Загружаем структуру нейронов + const auto& neuronsArray = network["neurons"]; + int neuronIndex = Inputs; + for (const auto& neuron : neuronsArray) { + nei[neuronIndex].i = neuron["i"].get(); + nei[neuronIndex].j = neuron["j"].get(); + int opIndex = neuron["op"].get(); + if (opIndex >= 0 && opIndex < op_count) { + nei[neuronIndex].op = op[opIndex]; + } else { + nei[neuronIndex].op = op[0]; + } + neuronIndex++; + } + + cout << "Network loaded for retraining from: " << filePath << endl; + cout << " Receptors: " << Receptors << endl; + cout << " Classes: " << Classes << endl; + cout << " Trained classes: " << trainedClasses.size() << endl; + for (int c : trainedClasses) { + cout << " " << c << ": " << classes[c] << " (neuron " << NetOutput[c] << ")" << endl; + } + cout << " Neurons: " << (Neirons - Inputs) << endl; + + return true; + } + catch (const json::exception& e) { + cerr << "JSON parsing error: " << e.what() << endl; + return false; + } + catch (const exception& e) { + cerr << "Error loading network: " << e.what() << endl; + return false; + } +} + +/** + * Объединение загруженной сети с новой конфигурацией для дообучения + * + * Добавляет новые классы из конфигурации к уже загруженной сети. + * Проверяет совместимость receptors. + * + * @param configPath - путь к JSON файлу конфигурации с новыми данными + * @param trainedClasses - индексы уже обученных классов + * @param newClassIds - выходной вектор: индексы новых классов для обучения + * @return true при успешном объединении, false при ошибке + */ +bool mergeConfigForRetraining(const string& configPath, const vector& trainedClasses, vector& newClassIds) { + ifstream configFile(configPath); + if (!configFile.is_open()) { + cerr << "Error: Cannot open config file: " << configPath << endl; + return false; + } + + try { + json config; + configFile >> config; + + // Проверяем совместимость receptors + int configReceptors = Receptors; + if (config.contains("receptors")) { + configReceptors = config["receptors"].get(); + } + if (configReceptors != Receptors) { + cerr << "Error: Config receptors (" << configReceptors << ") don't match model (" << Receptors << ")" << endl; + return false; + } + + newClassIds.clear(); + + // Загружаем образы из конфигурации + if (config.contains("images")) { + // Режим прямых образов + int maxClassId = Classes - 1; + for (const auto& img : config["images"]) { + string word = img["word"].get(); + int id = img["id"].get(); + const_words.push_back({word, id}); + if (id > maxClassId) { + maxClassId = id; + } + } + + // Расширяем классы если нужно + if (maxClassId >= Classes) { + classes.resize(maxClassId + 1); + NetOutput.resize(maxClassId + 1, -1); + Classes = maxClassId + 1; + } + + // Определяем какие классы нужно обучить + for (int c = 0; c < Classes; c++) { + bool isTrained = (std::find(trainedClasses.begin(), trainedClasses.end(), c) != trainedClasses.end()); + if (!isTrained) { + newClassIds.push_back(c); + } + } + } + else if (config.contains("classes")) { + bool generateShifts = true; + if (config.contains("generate_shifts")) { + generateShifts = config["generate_shifts"].get(); + } + + for (const auto& cls : config["classes"]) { + string word = cls["word"].get(); + int id = cls["id"].get(); + + // Проверяем, есть ли это новый класс + bool isTrained = (std::find(trainedClasses.begin(), trainedClasses.end(), id) != trainedClasses.end()); + + // Расширяем массив классов если нужно + if (id >= Classes) { + classes.resize(id + 1); + NetOutput.resize(id + 1, -1); + Classes = id + 1; + } + + // Сохраняем имя класса + if (classes[id].empty()) { + classes[id] = word; + } + + // Генерируем образы для класса + if (generateShifts && word.length() > 0) { + generateShiftedImages(word, id, Receptors); + } else { + string padded = word; + while ((int)padded.length() < Receptors) { + padded += ' '; + } + const_words.push_back({padded.substr(0, Receptors), id}); + } + + // Добавляем в список для обучения если не обучен + if (!isTrained && std::find(newClassIds.begin(), newClassIds.end(), id) == newClassIds.end()) { + newClassIds.push_back(id); + } + } + } + + cout << "Config merged for retraining: " << configPath << endl; + cout << " Total classes: " << Classes << endl; + cout << " New classes to train: " << newClassIds.size() << endl; + for (int c : newClassIds) { + cout << " " << c << ": " << classes[c] << endl; + } + cout << " Total images: " << const_words.size() << endl; + + return true; + } + catch (const json::exception& e) { + cerr << "JSON parsing error: " << e.what() << endl; + return false; + } +} + #endif // JSON_IO_H diff --git a/main.cpp b/main.cpp index 80a8b35..a355088 100644 --- a/main.cpp +++ b/main.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include using namespace std; @@ -89,6 +90,13 @@ vector vz; // Ожидаемые выхо vector NetOutput; // Выходные нейроны для классов char InputStr[StringSize], word_buf[StringSize]; // Буферы для ввода +// ============================================================================ +// Глобальные переменные для прерывания обучения +// ============================================================================ + +volatile std::sig_atomic_t g_interruptRequested = 0; // Флаг запроса прерывания +string g_autoSavePath = ""; // Путь для автосохранения при прерывании + // ============================================================================ // Операции нейронов - элементарные математические действия // ============================================================================ @@ -201,6 +209,24 @@ const int MAX_NEURONS = 64000; // Максимальное к // Вспомогательные функции // ============================================================================ +/** + * Обработчик сигнала прерывания (Ctrl+C) + * + * Устанавливает флаг прерывания для корректного завершения обучения. + */ +void interruptHandler(int signal) { + if (signal == SIGINT) { + if (g_interruptRequested == 0) { + g_interruptRequested = 1; + cout << "\n[INTERRUPT] Ctrl+C detected. Training will stop after current iteration..." << endl; + cout << "[INTERRUPT] Press Ctrl+C again to force exit (may lose progress)." << endl; + } else { + cout << "\n[INTERRUPT] Force exit requested." << endl; + exit(1); + } + } +} + /** * Вывод справки по использованию программы */ @@ -210,6 +236,7 @@ void printUsage(const char* programName) { cout << "MODES:" << endl; cout << " Training mode (default): Train network and optionally save to file" << endl; cout << " Inference mode: Load trained network and classify inputs" << endl; + cout << " Retraining mode: Load existing network and continue training with new data" << endl; cout << endl; cout << "TRAINING OPTIONS:" << endl; cout << " -c, --config Load training configuration from JSON file" << endl; @@ -217,9 +244,15 @@ void printUsage(const char* programName) { cout << " -t, --test Run automated test after training (no interactive mode)" << endl; cout << " -b, --benchmark Run benchmark to measure training speed" << endl; cout << endl; + cout << "RETRAINING OPTIONS:" << endl; + cout << " -r, --retrain Load existing network and continue training (retraining mode)" << endl; + cout << " Combines -l (load) with training mode. Requires -c for new data." << endl; + cout << " New classes in config (without output_neuron) will be trained." << endl; + cout << endl; cout << "INFERENCE OPTIONS:" << endl; cout << " -l, --load Load trained network from JSON file (inference mode)" << endl; cout << " -i, --input Classify single input text and exit (non-interactive)" << endl; + cout << " --verify Verify accuracy of loaded model on training config (-c required)" << endl; cout << endl; cout << "PERFORMANCE OPTIONS:" << endl; cout << " -j, --threads Number of threads to use (0 = auto, default)" << endl; @@ -228,10 +261,17 @@ void printUsage(const char* programName) { cout << "GENERAL OPTIONS:" << endl; cout << " -h, --help Show this help message" << endl; cout << endl; + cout << "INTERRUPTION:" << endl; + cout << " Press Ctrl+C during training to interrupt gracefully." << endl; + cout << " The network will be saved if -s is specified." << endl; + cout << " Training can be continued later with -r option." << endl; + cout << endl; cout << "EXAMPLES:" << endl; cout << " " << programName << " -c configs/default.json -s model.json # Train and save" << endl; cout << " " << programName << " -l model.json # Interactive inference" << endl; cout << " " << programName << " -l model.json -i \"time\" # Single classification" << endl; + cout << " " << programName << " -r model.json -c configs/new.json -s model_v2.json # Retrain" << endl; + cout << " " << programName << " -l model.json -c configs/test.json --verify # Verify accuracy" << endl; cout << endl; cout << "JSON config format (training):" << endl; cout << " {" << endl; @@ -331,10 +371,13 @@ int main(int argc, char* argv[]) string configPath = ""; string savePath = ""; string loadPath = ""; + string retrainPath = ""; string inputText = ""; bool testMode = false; bool benchmarkMode = false; bool inferenceMode = false; + bool retrainMode = false; + bool verifyMode = false; for (int i = 1; i < argc; i++) { string arg = argv[i]; @@ -345,12 +388,17 @@ int main(int argc, char* argv[]) } else if ((arg == "-l" || arg == "--load") && i + 1 < argc) { loadPath = argv[++i]; inferenceMode = true; + } else if ((arg == "-r" || arg == "--retrain") && i + 1 < argc) { + retrainPath = argv[++i]; + retrainMode = true; } else if ((arg == "-i" || arg == "--input") && i + 1 < argc) { inputText = argv[++i]; } else if (arg == "-t" || arg == "--test") { testMode = true; } else if (arg == "-b" || arg == "--benchmark") { benchmarkMode = true; + } else if (arg == "--verify") { + verifyMode = true; } else if ((arg == "-j" || arg == "--threads") && i + 1 < argc) { NumThreads = atoi(argv[++i]); } else if (arg == "--single-thread") { @@ -361,9 +409,96 @@ int main(int argc, char* argv[]) } } + // Установка обработчика сигнала Ctrl+C + std::signal(SIGINT, interruptHandler); + g_autoSavePath = savePath; + + // ===== РЕЖИМ ВЕРИФИКАЦИИ ТОЧНОСТИ ===== + // Загружаем обученную сеть и тестируем на данных из конфига + if (verifyMode) { + if (loadPath.empty()) { + cerr << "Error: --verify requires -l " << endl; + return 1; + } + if (configPath.empty()) { + cerr << "Error: --verify requires -c for test data" << endl; + return 1; + } + + // Загружаем сеть + if (!loadNetwork(loadPath)) { + return 1; + } + + // Загружаем конфиг для тестовых данных (сохраняем текущие Receptors для проверки) + int savedReceptors = Receptors; + if (!loadConfig(configPath, Receptors)) { + return 1; + } + + // Проверяем совместимость + if (Receptors != savedReceptors) { + cerr << "Error: Config receptors (" << Receptors << ") don't match model (" << savedReceptors << ")" << endl; + return 1; + } + + cout << "\n=== Verifying model accuracy ===" << endl; + + int passed = 0; + int failed = 0; + int total = const_words.size(); + + for (int img = 0; img < total; img++) { + // Устанавливаем входы сети из образа + for (int d = 0; d < Receptors; d++) { + if (d < (int)const_words[img].word.length() && const_words[img].word[d] != 0) { + NetInput[d] = float((unsigned char)const_words[img].word[d]) / float(max_num); + } else { + NetInput[d] = float((unsigned char)' ') / float(max_num); + } + } + clear_val_cache(nei, MAX_NEURONS); + + // Находим класс с максимальным выходом + int predictedClass = -1; + float maxOutput = -big; + for (int c = 0; c < Classes; c++) { + float output = GetNeironVal(NetOutput[c]); + if (output > maxOutput) { + maxOutput = output; + predictedClass = c; + } + } + + int expectedClass = const_words[img].id; + float expectedOutput = (expectedClass < Classes) ? GetNeironVal(NetOutput[expectedClass]) : 0.0f; + + // Проверяем корректность + bool testPassed = (predictedClass == expectedClass) || (expectedOutput >= 0.5f); + + if (testPassed) { + passed++; + } else { + failed++; + string shortWord = const_words[img].word.substr(0, 10); + cout << "[FAIL] \"" << shortWord << "...\" expected class " << expectedClass + << ", predicted " << predictedClass << " (conf=" << (int)(expectedOutput*100) << "%)" << endl; + } + } + + cout << "\n=== Verification Summary ===" << endl; + cout << "Total samples: " << total << endl; + cout << "Passed: " << passed << endl; + cout << "Failed: " << failed << endl; + float accuracy = (float)passed / (float)total * 100.0f; + cout << "Accuracy: " << accuracy << "%" << endl; + + return (failed == 0) ? 0 : 1; + } + // ===== РЕЖИМ ИНФЕРЕНСА ===== // Загружаем обученную сеть и переходим к классификации - if (inferenceMode) { + if (inferenceMode && !retrainMode) { if (!loadNetwork(loadPath)) { return 1; } @@ -396,15 +531,47 @@ int main(int argc, char* argv[]) } while (true); } - // ===== РЕЖИМ ОБУЧЕНИЯ ===== + // ===== РЕЖИМ ДООБУЧЕНИЯ ===== + // Загружаем существующую сеть и дообучаем на новых данных + vector trainedClasses; // Индексы уже обученных классов + vector newClassIds; // Индексы новых классов для обучения - // Загружаем конфигурацию или используем значения по умолчанию - if (!configPath.empty()) { - if (!loadConfig(configPath, Receptors)) { + if (retrainMode) { + if (configPath.empty()) { + cerr << "Error: Retraining mode requires -c for new training data" << endl; return 1; } - } else { - initDefaultConfig(Receptors); + + // Загружаем сеть для дообучения + if (!loadNetworkForRetraining(retrainPath, trainedClasses)) { + return 1; + } + + // Объединяем с новой конфигурацией + if (!mergeConfigForRetraining(configPath, trainedClasses, newClassIds)) { + return 1; + } + + if (newClassIds.empty()) { + cout << "\nAll classes are already trained. Nothing to do." << endl; + cout << "Use --verify to check accuracy or -l for inference mode." << endl; + return 0; + } + + cout << "\nRetraining mode: will train " << newClassIds.size() << " new class(es)" << endl; + } + + // ===== РЕЖИМ ОБУЧЕНИЯ ===== + + // Загружаем конфигурацию или используем значения по умолчанию (если не дообучение) + if (!retrainMode) { + if (!configPath.empty()) { + if (!loadConfig(configPath, Receptors)) { + return 1; + } + } else { + initDefaultConfig(Receptors); + } } cout << "Random seed: " << rand() << endl; @@ -424,7 +591,11 @@ int main(int argc, char* argv[]) // Вычисляем производные значения после загрузки конфигурации Images = const_words.size(); Inputs = Receptors + base_size; - Neirons = Inputs; + + // В режиме дообучения Neirons уже установлен из загруженной сети + if (!retrainMode) { + Neirons = Inputs; + } // Выделяем динамические массивы NetInput.resize(Inputs); @@ -436,7 +607,16 @@ int main(int argc, char* argv[]) NetOutput.resize(Classes); // Инициализируем массив нейронов - initNeurons(); + // В режиме дообучения нейроны уже инициализированы, но нужно расширить кэши под новые образы + if (retrainMode) { + // Расширяем кэши под новое количество образов + for (int n = 0; n < MAX_NEURONS; n++) { + nei[n].c.resize(Images); + nei[n].cached = false; // Сбрасываем кэши, т.к. теперь другое количество образов + } + } else { + initNeurons(); + } // Задаём базисные значения for (int i = 0; i < base_size; i++) @@ -471,13 +651,32 @@ int main(int argc, char* argv[]) vector class_er(Classes, big); float er = .01f; // Допустимая ошибка + // В режиме дообучения устанавливаем ошибку 0 для уже обученных классов + if (retrainMode) { + for (int c : trainedClasses) { + class_er[c] = 0.0f; + } + // Начинаем с первого нового класса + if (!newClassIds.empty()) { + classIndex = newClassIds[0]; + } + } + // Засекаем время обучения auto trainingStartTime = chrono::high_resolution_clock::now(); int trainingIterations = 0; + bool trainingInterrupted = false; // Цикл обучения do { + // Проверяем запрос на прерывание + if (g_interruptRequested) { + cout << "\n[INTERRUPT] Training interrupted by user." << endl; + trainingInterrupted = true; + break; + } + trainingIterations++; cout << "train class:" << classes[classIndex] << " (id=" << classIndex << ")"; @@ -513,17 +712,44 @@ int main(int argc, char* argv[]) auto trainingEndTime = chrono::high_resolution_clock::now(); auto trainingDuration = chrono::duration_cast(trainingEndTime - trainingStartTime); - cout << "\nTraining completed!" << endl; - cout << "Final errors per class:" << endl; + if (trainingInterrupted) { + cout << "\nTraining interrupted after " << trainingIterations << " iterations." << endl; + } else { + cout << "\nTraining completed!" << endl; + } + + cout << "Errors per class:" << endl; + int trainedCount = 0; + int untrainedCount = 0; for (int c = 0; c < Classes; c++) { - cout << " Class " << c << " (" << classes[c] << "): error = " << class_er[c] << endl; + bool isTrained = (class_er[c] <= er); + cout << " Class " << c << " (" << classes[c] << "): error = " << class_er[c]; + if (isTrained) { + cout << " [trained]"; + trainedCount++; + } else { + cout << " [not trained]"; + untrainedCount++; + } + cout << endl; + } + + if (trainingInterrupted && untrainedCount > 0) { + cout << "\nWarning: " << untrainedCount << " class(es) are not fully trained." << endl; + cout << "Use -r option to continue training later." << endl; } // Сохраняем сеть если указан путь if (!savePath.empty()) { if (!saveNetwork(savePath)) { cerr << "Warning: Failed to save network to " << savePath << endl; + } else if (trainingInterrupted) { + cout << "\nNetwork state saved. To continue training, use:" << endl; + cout << " " << argv[0] << " -r " << savePath << " -c -s " << endl; } + } else if (trainingInterrupted) { + cout << "\nWarning: Network state not saved (no -s option specified)." << endl; + cout << "Progress will be lost. Use -s to save network for later continuation." << endl; } // Режим бенчмарка: выводим метрики скорости обучения From 51d55eddec87d400d25c7645cde2de6407ac98ce Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 27 Jan 2026 21:12:10 +0100 Subject: [PATCH 3/3] Restore CLAUDE.md to original state Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 661e7d4..043e473 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,16 +5,3 @@ Your forked repository: konard/netkeep80-NNets Original repository (upstream): netkeep80/NNets Proceed. - ---- - -Issue to solve: https://github.com/netkeep80/NNets/issues/19 -Your prepared branch: issue-19-57fc143cd06d -Your prepared working directory: /tmp/gh-issue-solver-1769543969408 -Your forked repository: konard/netkeep80-NNets -Original repository (upstream): netkeep80/NNets - -Proceed. - - -Run timestamp: 2026-01-27T19:59:34.992Z \ No newline at end of file