def pp_from_raw_generator(raw_file: str, chunk_size: int, **pp_settings) -> pd.DataFrame: """ Генератор предобработанных текстов :param raw_file: путь к файлу исходных текстов :param chunk_size: размер чанка для предобработки :param pp_settings: настройки препроцессора """ pp = Preprocessor() for n, chunk in enumerate( pd.read_csv(raw_file, encoding="cp1251", quoting=3, sep="\t", chunksize=chunk_size)): try: pp_chunk = pp.preprocess_dataframe(df=chunk, **pp_settings) except Exception as e: logger = get_logger("ecs.data_tools.pp_from_raw_generator") error_ps( logger, f"Error occurred during preprocessing chunk {n} of file {raw_file}: {e}" ) exit(1) else: yield pp_chunk
def aggregate_full_dataset(vector_gen) -> pd.DataFrame: logger = get_logger("ecs.data_tools.aggregate_full_dataset") rubricators = ["subj", "ipv", "rgnti"] full_df = pd.DataFrame(columns=["vectors", *rubricators]) try: for chunk in vector_gen: chunk[rubricators] = chunk[rubricators].astype(str) full_df = pd.concat([full_df, chunk], ignore_index=True) except Exception as e: error_ps(logger, f"Error occurred during loading the dataset in memory: {e}") exit(1) return full_df
def create_report(model, x_test: np.ndarray, y_test: list, ignore_rubrics=()): # Для обратной совместимости используется старый код logger = get_logger("ecs.model_tool.create_report") pred = [] try: for p in model.predict_proba(x_test): all_prob = pd.Series(p, index=model.classes_) for code in ignore_rubrics: try: all_prob.drop(code) except KeyError: pass pred.append(list(all_prob.sort_values(ascending=False).index)) except Exception as e: error_ps(logger, f"Error occurred during model testing: {e}") exit(1) else: return count_stats(predicts=pred, y_test=y_test, amounts=[1, 2, 3, 4, 5, -1])
def create_w2v(pp_sources: list, vector_dim: int, window_size: int, train_alg: str) -> Word2Vec: """ Создать модель Word2Vec для предобработанных текстов. :param window_size: размер окна контекста :param vector_dim: размерность векторной модели :param train_alg: тип алгоритма обучения word2vec (cbow или skip-gram) :param pp_sources: список инстансов генератора предобработанных данных (pp_generator или caching_pp_generator) :returns обученная модель Word2Vec """ train_algoritm = { "cbow": 0, "skip-gram": 1 } # to transform into word2vec param logger = get_logger("ecs.data_tools.create_w2v") w2v = Word2Vec(size=vector_dim, min_count=3, workers=3, window=window_size, sg=train_algoritm[train_alg]) init = False for n_source, pp_source in enumerate(pp_sources, start=1): info_ps(logger, f"Training W2V on source {n_source}/{len(pp_sources)}") for n_chunk, pp_chunk in enumerate(pp_source, start=1): try: if n_chunk % N_CHUNKS_INTERVAL == 0: info_ps(logger, f"\nChunk {n_chunk}") sentence = [text.split() for text in pp_chunk["text"].values] w2v.build_vocab(sentence, update=init) # TODO: вынести количество эпох в параметры w2v.train(sentence, epochs=20, total_examples=len(sentence)) init = True except Exception as e: error_ps( logger, f"An error occurred during training W2V: {e}. Source {n_source}, chunk {n_chunk} " f"(~{(n_chunk - 1) * len(pp_chunk) + 1}{n_chunk * len(pp_chunk)} " f"lines in clear file)") exit(1) return w2v
def main(): # Получаем аргументы командной строки argparser = ArgumentParser() add_args(argparser) args = argparser.parse_args() exp_path = args.exp_path settings_path = os.path.join(exp_path, "settings.ini") # Создаем логгер logger = create_logger(os.path.join(exp_path, f"{timestamp()}.log"), "ecs.main") if not os.path.exists(settings_path): logger.info(f"No settings.ini file found in {exp_path}") exit(0) # Загружаем и проверяем конфиг config = ValidConfig() config.read(settings_path, encoding="cp1251") logger.info("Validating experiment settings...") config.validate_all() logger.info("\n" + "=" * 20 + "Validation OK" + "=" * 20 + "\n") # Находим датасет training_file = config.get("TrainingData", "dataset") test_file = config.get("TrainingData", "test_file") dataset_folder = os.path.dirname(training_file) # Определяем язык language = config.get("Preprocessing", "language") if language == "auto": # Для распознавания языка грузим 10 первых строк language = recognize_language(training_file, encoding="cp1251", n_lines=10) # Потом мы будем копировать файл настроек в кэш # Поэтому поддерживаем его актуальным config.set("Preprocessing", "language", language) # Получаем параметры exp_title = config.get_primitive("Experiment", "experiment_title") if exp_title == "": exp_title = timestamp() remove_stopwords = config.get_primitive("Preprocessing", "remove_stopwords") normalization = config.get_primitive("Preprocessing", "normalization") chunk_size = config.get_primitive("Preprocessing", "batch_size") use_model = config.get("WordEmbedding", "use_model") w2v_exists = os.path.exists(use_model) vector_dim = config.getint("WordEmbedding", "vector_dim") pooling = config.get("WordEmbedding", "pooling") window = config.getint("WordEmbedding", "window") train_algorithm = config.get("WordEmbedding", "train_algorithm") binary = config.get_primitive("Experiment", "binary") rubricators = config.get_as_list("Experiment", "rubricator") n_jobs = config.getint("Experiment", "threads") n_folds = config.getint("Experiment", "n_folds") grnti_level = config.getint("rgnti", "level", fallback=2) # Заплатка # TODO: починить # *дьявольский голос из-за плеча*: # - Оно работает, не трогай! test_percent = 0 if not test_file: test_percent = config.getint("TrainingData", "test_percent") test_percent = test_percent / 100 # Готовим фильтры настроек для поиска кэша clear_metadata_filter = { "language": language, "remove_stopwords": remove_stopwords, "normalization": normalization } vector_metadata_filter = { **clear_metadata_filter, } for key in ["vector_dim", "window", "pooling", "train_algorithm"]: vector_metadata_filter[key] = config.get_primitive( "WordEmbedding", key) # Создаем источники векторов согласно схеме total_timer = time() cached_vectors = find_cached_vectors( base_dir=dataset_folder, metadata_filter=vector_metadata_filter) train_test_vec_exist = False cached_w2v_path = "" vector_cache_path = "" train_vec_cache = "" test_vec_cache = "" vector_gens = {} # Проводим разведку if len(cached_vectors) > 0: vector_cache_folder, vector_cache_files = cached_vectors.popitem() train_vec_cache = find_cache_for_source(vector_cache_files, training_file) test_vec_cache = None if test_file != "": test_vec_cache = find_cache_for_source(vector_cache_files, test_file) train_test_vec_exist = train_vec_cache and test_vec_cache cached_w2v_path = find_cached_w2v(vector_cache_folder) if train_test_vec_exist and cached_w2v_path and use_model != "": logger.info("Cached vectors found") w2v_model, language = load_w2v(cached_w2v_path) config.set("Preprocessing", "language", language) vector_gens["train"] = create_reading_vector_gen( train_vec_cache, chunk_size) vector_gens["test"] = create_reading_vector_gen( test_vec_cache, chunk_size) else: # Либо нет готовых обучающих векторов, # либо в кэше нет модели W2V, # либо нужно создать их заново при помощи указанной модели # Получаем источники чистых текстов # Словарь вида {'train': генератор, ['test': генератор]} pp_gens = create_clear_generators( base_dir=dataset_folder, clear_filter=clear_metadata_filter, chunk_size=chunk_size, training_fpath=training_file, test_fpath=test_file, experiment_title=exp_title, pp_params=extract_pp_settings(config)) if w2v_exists: # Используем ее # И обновляем язык logger.info(f"Using Word2Vec model: {os.path.basename(use_model)}") w2v_model, language = load_w2v(use_model) config.set("Preprocessing", "language", language) else: # Создаем новую # Для совместимости преобразуем в список logger.info("Creating new Word2Vec model") w2v_model = create_w2v(pp_sources=list(pp_gens.values()), vector_dim=vector_dim, window_size=window, train_alg=train_algorithm) # Увы, генераторы - это одноразовые итераторы # Придется создать их заново # Должны гарантированно получиться # читающие,а не кэширующие pp_gens = create_clear_generators( base_dir=dataset_folder, clear_filter=clear_metadata_filter, chunk_size=chunk_size, training_fpath=training_file, test_fpath=test_file, experiment_title=exp_title, pp_params=extract_pp_settings(config)) vector_cache_path = generate_vector_cache_path(raw_path=training_file, exp_name=exp_title) try: train_vec_gen = caching_vector_generator( pp_source=pp_gens["train"], w2v_file=w2v_model, cache_path=vector_cache_path, conv_type=pooling, pp_metadata=clear_metadata_filter) except Exception as e: error_ps( logger, f"Error occurred during creation caching vector generator (training): {e}" ) else: vector_gens["train"] = train_vec_gen if test_file: vector_cache_path = generate_vector_cache_path(raw_path=test_file, exp_name=exp_title) test_vec_gen = caching_vector_generator( pp_source=pp_gens["test"], w2v_file=w2v_model, cache_path=vector_cache_path, conv_type=pooling, pp_metadata=clear_metadata_filter) vector_gens["test"] = test_vec_gen # Время хорошенько загрузить память # Собираем обучающие и тестировочные выборки training_gen = vector_gens["train"] logger.info("Collecting the training dataset in memory") training_df = aggregate_full_dataset(training_gen) test_df = None if test_file != "": test_gen = vector_gens["test"] logger.info("Collecting the test dataset in memory") test_df = aggregate_full_dataset(test_gen) # На этом этапе уже должны быть созданы папки кэша, # так как мы гарантированно прогнали все генераторы # Копируем файл настроек и W2V # По недоразумению мы не храним путь к чистому кэшу, # поэтому создаем его заново. TODO: Исправить clear_cache_folder = os.path.dirname( generate_clear_cache_path(training_file, exp_title)) vector_cache_folder = os.path.dirname(vector_cache_path) with open(os.path.join(clear_cache_folder, "settings.ini"), "w") as clear_copy_file: config.write(clear_copy_file) with open(os.path.join(vector_cache_folder, "settings.ini"), "w") as vector_copy_file: config.write(vector_copy_file) w2v_fname = generate_w2v_fname(vector_dim=w2v_model.vector_size, language=language) w2v_cache_path = os.path.join(vector_cache_folder, w2v_fname) w2v_save_path = os.path.join(exp_path, w2v_fname) w2v_model.save(w2v_cache_path) logger.info(f"Saving W2V model to {w2v_save_path}") w2v_model.save(w2v_save_path) # Обучаем и тестируем модели model_names = config.get_as_list("Classification", "models") mapping_config = ValidConfig() mapping_config.read( os.path.join(os.path.dirname(__file__), "interface", "map.ini")) model_import_mapping = mapping_config.get_as_dict("SupportedModels") # Для каждого рубрикатора создается свой датасет, т.к. каждый текст # обладает разным количеством разных кодов # и если размножить векторы сразу для всех рубрикаторов, # память может быстро закончиться for rubricator in rubricators: if test_df is None: x_train, x_test, y_train, y_test = create_labeled_tt_split( full_df=training_df, test_percent=test_percent, rubricator=rubricator, grnti_level=grnti_level) else: x_train, y_train = df_to_labeled_dataset(full_df=training_df, rubricator=rubricator, grnti_level=grnti_level) x_test, y_test = df_to_labeled_dataset(full_df=test_df, rubricator=rubricator, grnti_level=grnti_level) min_training_rubr = config.get_primitive( rubricator, "min_training_rubric", fallback="0") or 1 min_test_rubr = config.get_primitive( rubricator, "min_validation_rubric", fallback="0") or 1 max_training_rubr = config.get_primitive( rubricator, "max_training_rubric", fallback="0") or -1 max_test_rubr = config.get_primitive( rubricator, "max_validation_rubric", fallback="0") or -1 train_filter_res = {} if min_training_rubr > 1 or max_training_rubr > -1: train_filter_res = inplace_rubric_filter( x_train, y_train, limits=(min_training_rubr, max_training_rubr)) log_str = f"Dropped rubrics from training dataset for {rubricator}:\n" + \ "\n".join([f"{k}\t({v} texts)" for k, v in train_filter_res.items()]) logger.info(log_str) if min_test_rubr > 1 or max_test_rubr > -1: y_test_cntr = Counter(y_test) test_filter_res = inplace_rubric_filter(x_test, y_test, limits=(min_test_rubr, max_test_rubr)) # В датасете тексты с множественными метками дублируются, # поэтому можно просто дропнуть записи с удаленными из train рубриками, # чтобы не учитывать их при тестировании inplace_drop_rubrics(x_test, y_test, rubrics=train_filter_res.keys()) total_test_drop = test_filter_res.copy() for key in train_filter_res: total_test_drop[key] = y_test_cntr[key] log_str = "Dropped rubrics from test dataset:\n" + \ "\n".join([f"{k}\t({v} texts)" for k, v in total_test_drop.items()]) logger.info(log_str) # Оставляем пересечение рубрик logger.info("Building training/test rubric intersection") intersect_rubrics = { k: v for k, v in Counter(y_test).items() if k in Counter(y_train) } inplace_keep_rubrics(x_train, y_train, intersect_rubrics) inplace_keep_rubrics(x_test, y_test, intersect_rubrics) # Пост-валидация for desc, ds in { "Training dataset": y_train, "Test dataset": y_test }.items(): # Проверка на слишком строгие пороги if len(ds) == 0: logger.error( desc + " is empty! All the texts were removed due to the threshold" ) exit(0) # Проверка на неприятный баг. Все по непонятной причине ломается, # когда остаются только тексты любой одной рубрики if len(set(ds)) == 1: logger.error( f"{desc} contains only 1 rubric '{ds[1]}'. " f"This will cause invalid behavior and ECS crash. Finishing execution" ) exit(0) for model_name in model_names: hypers = config.get_hyperparameters(model_name) if model_name == "svm": hypers["probability"] = [True] try: model_type = load_class(model_import_mapping[model_name]) except ImportError as ie: logger.warning( f"\n>>> Unable to import model {model_name}, it will be skipped." ) logger.warning(f">>> ({ie})\n") continue logger.info( f"Fitting parameters for model {model_name} by {rubricator}") model_instance = model_type() timer = time() try: best_params = run_grid_search(model_instance=model_instance, hyperparameters=hypers, x_train=x_train, y_train=y_train, binary=binary, n_folds=n_folds, n_jobs=n_jobs) except ValueError as ve: logger.warning( f"\n>>> Detected incorrect hyperparameters ({ve}) for model '{model_name}'." f" It will be skipped.") continue except OSError as ose: state_str = f"(model: {model_name}, rubricator: {rubricator})" error_ps( logger, f"OS has interrupted the grid search process: {ose} {state_str}" ) exit(1) except AttributeError as ae: logger.error(f"Unsupported data type: {ae}") exit(1) else: try: best_model = refit_model(model_instance=model_type(), best_params=best_params, x_train=x_train, y_train=y_train, binary=binary) except OSError as ose: state_str = f"(model: {model_name}, rubricator: {rubricator})" error_ps( logger, f"OS has interrupted the refitting process: {ose} {state_str}" ) exit(1) else: time_elapsed = int(time() - timer) # Сохраняем модель model_fname = create_model_fname( model_name=model_name, language=language, rubricator=rubricator, pooling=pooling, vector_dim=w2v_model.vector_size) model_path = os.path.join(exp_path, model_fname) # Пока эта информация не используется, но в будущем может пригодиться model_metadata = { **vector_metadata_filter, **best_params, "settings": { section: config.get_as_dict(section) for section in config.sections() } } logger.info(f"Saving model to {model_path}") save_model(model=best_model, path=model_path, metadata=model_metadata) # Создаем и сохраняем отчеты logger.info("Testing model and creating report") excel_report = create_report( model=best_model, x_test=x_test, y_test=y_test, ) text_report = create_description( model_name=model_name, hyper_grid=hypers, best_params=best_params, train_file=training_file, test_file=test_file, train_size=len(x_train), test_size=len(x_test), stats=excel_report, training_secs=time_elapsed) report_fname = create_report_fname( model_name=model_name, language=language, rubricator=rubricator, pooling=pooling, vector_dim=w2v_model.vector_size) excel_path = os.path.join(exp_path, f"{report_fname}.xlsx") txt_path = os.path.join(exp_path, f"{report_fname}.txt") logger.info( f"Saving reports to {excel_path} and {txt_path}") save_excel_report(path=excel_path, report=excel_report, rubricator=rubricator) save_txt_report(path=txt_path, report=text_report) # Печатаем мини-отчет mini_report = short_report(excel_report, time_elapsed) logger.info(f"\n{mini_report}") logger.info("Done") logger.info( f"Total time elapsed: {seconds_to_duration(int(time() - total_timer))}" )
def create_clear_generators(base_dir: str, clear_filter: dict, chunk_size: int, training_fpath: str, test_fpath: str, experiment_title: str, pp_params: dict) -> dict: """ Создает подходящий генератор чистых текстов. Если найден кэш, то создает читающий генератор, если нет - предобрабатывает сырые данные :param base_dir: базовая директория для поиска кэша :param clear_filter: словарь с настройками препроцессора для поиска подходящего кэша :param chunk_size: размер чанка :param training_fpath: путь к сырому обучающему файлу :param test_fpath: путь к сырому тест-файлу (может быть равен '') :param experiment_title: название эксперимента :param pp_params: словарь с полными настройками препроцессора :return: словарь вида {'train': генератор, ['test': генератор]} """ try: pp_sources = {} cached_clear = find_cached_clear(base_dir=base_dir, metadata_filter=clear_filter) clear_file_list = [] # Если мы нашли кэш, список станет непустым # А если нет, то мы не зайдем в условие if len(cached_clear) > 0: # Если нашли больше, чем одну папку с чистыми текстами, # берем случайную _, clear_file_list = cached_clear.popitem() # Если список был пустым, то find_cache_for source # вернет пустую строку и будет создан кэширующий генератор train_pp_cache = find_cache_for_source(clear_file_list, training_fpath) if train_pp_cache: pp_sources[train_pp_cache] = pp_from_csv_generator( train_pp_cache, chunk_size) else: pp_sources[training_fpath] = create_caching_pp_gen( raw_path=training_fpath, exp_name=experiment_title, chunk_size=chunk_size, pp_params=pp_params) # Тут возможно 2 ситуации: # 1. Тестового файла не предусмотрено (ничего не делать) # 2. Тестовый кэш не найден (создать кэширующий генератор) if test_fpath: test_pp_cache = find_cache_for_source(clear_file_list, test_fpath) if test_pp_cache: pp_sources[test_pp_cache] = pp_from_csv_generator( test_pp_cache, chunk_size) else: pp_sources[test_fpath] = create_caching_pp_gen( raw_path=test_fpath, exp_name=experiment_title, chunk_size=chunk_size, pp_params=pp_params) return train_test_match(pp_sources, train_raw_path=training_fpath, test_raw_path=test_fpath) except Exception as e: logger = get_logger("ecs.create_clear_generators") error_ps(logger, f"Error occurred during creation of clear generators: {e}") exit(1)