示例#1
0
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
示例#2
0
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
示例#3
0
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])
示例#4
0
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
示例#5
0
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))}"
    )
示例#6
0
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)