В рамках процесса кредитования физических и юридических лиц, банки запрашивают у клиентов оригиналы различных документов. Эти документы, очевидно, необходимо проверять по многим критериям. Из пунктов проверки документов достаточно большую значимость среди прочих несет проверка полноты пакета документов. В данной статье будет рассмотрена именно эта процедура.

Существует множество таких заявок на кредит, где заявитель мог подать в банк неполный комплект документов, или может случиться так, что некоторые из поданных заявителем документов сохранены в ненадлежащем формате либо нечитаемы. Может случиться и так, что файлы передадутся до места хранения (сервер) не в полном объеме. Все это – нежелательные явления, которые необходимо обнаружить в процессе работы над данной задачей.

Данные по задаче были предоставлены в большом объеме. Всего предстояло обработать более 400 000 файлов в различном формате. В основном, это скан-копии документов клиента, но есть также и таблицы, и текстовые документы. Всего в папках содержатся файлы с 23 разными форматами, но важны в рамках задачи только PDF файлы и файлы изображений.

Для обработки выбраны файлы за определенный период. Они разделены по папкам, в каждой из которых хранится около 50 000 файлов. Все эти файлы принадлежат к разным случаям подачи заявлений, и в зависимости от типа такого заявления, к нему должны быть приложены документы, из одного, нескольких или всех классов. Помимо файлов есть сводная таблица с принадлежностью файлов к заявлениям и другой важной информацией.

Необходимо обнаружить следующие документы:

  • заявление на кредитование;

  • паспорт субъекта;

  • согласие на обработку персональных данных.

Уже из названий классов можно понять – они отличаются друг от друга по содержанию. Чего нельзя четко определить из названия – визуальная составляющая. Но и внешне их тоже достаточно просто различить.

Первым решением задачи классификации файлов был метод OCR – Optical Character Recognition (оптическое распознавание символов). Один из самых известных и популярных модулей для распознавания текста с изображения на Python это PyTesseract:

import pytesseract
import os

from PIL import Image


def im2str(file, language='rus'):
    """
    finds text on image file and converts to string
    :param file: string path to imagefile
    :param language: language of imagefile
    :return: text from imagefile
    """

    pytesseract.pytesseract.tesseract_cmd = os.getcwd() + \
	'\\Tesseract-OCR\\tesseract.exe'
    tessdata_dir = os.getcwd() + '\\Tesseract-OCR\\tessdata'
    tessdata_dir_config = '--tessdata-dir "'+tessdata_dir+'"' 

    img = Image.open(file)
    document_text = pytesseract.image_to_string(
        img,
        lang=language,
        config=tessdata_dir_config
    )

    return document_text

Предполагалось преобразовать файлы в текст, и, используя слова-маркеры в тексте, определить, к какому классу принадлежит тот или иной файл.

Но от данного подхода нам пришлось отказаться, ведь OCR утилиты работают долго и недостаточно точно распознают текст. Еще одной причиной было то, что среди всех файлов было обнаружено слишком много разных классов, ключевые слова в них повторялись.

Исходя из вышеперечисленных недостатков, для решения поставленной задачи было решено использовать алгоритмы машинного обучения. В частности, выбор пал на ResNeXt. Это свёрточная нейронная сеть, применяемая в задачах классификации изображений.

С использованием интерфейса Jupyter Lab и таких библиотек для Python, как: PyTorch, SKLearn, Numpy, CV2, Pandas и другие – было обучено несколько моделей классификации. Обучение проходило на виртуальной машине с 128 гб оперативной памяти, 8 ядерном процессоре, GPU – Tesla V100 (32 гб ОЗУ) и CUDA версии 10.0:

Для классификации при помощи СНС необходимо было конвертировать все PDF файлы в файлы изображений. Для этого была использована утилита pdf2png.exe:

from subprocess import run
pdf_converter = 'path\to\pdf2png.exe'


def convert_pdf(file, out_f, pdf2png=pdf_converter):
    # executable string that converts first page of pdf file to png format
    exec = '"' + pdf2png + '" -f 1 -l 1 -r 250 "' + (str(file)) + '" "' + \
	str(Path(out_f).joinpath(f'picture/{file.stem}')) + '"'
    run(exec)

Первой проблемой, с которой мы столкнулись в процессе обучения классификатора, стало то, что документы слабо отличались между собой. В данной задаче это листы А4 с черными буквами на белом фоне. Визуально, для человека, они могут отличаться достаточно сильно, чтобы различить их, не вдаваясь в контекст. Но плохо обученная модель может не распознать в таких документах разницу и долгие часы обучения будут напрасны.

В обучении классификатора много важных шагов. Первый из них - определение выборки данных для обучения. Изображения, подающиеся в качестве обучающей выборки для модели должны быть четко распределены по классам. Мы выяснили: если обучать модель для обнаружения 3 классов среди множества (как уже было сказано, в одной заявке может быть очень много разных файлов) – ненужные изображения будут определяться как принадлежащие к искомым. Для решения этой проблемы мы решили создать отдельные классы для ненужных типов документов, что усложнило процесс обучения, но сильно улучшило результат.

Вторым важным этапом является предобработка данных. В нашем случае есть заявки, где приложены фото документов. Они могут быть сделаны неровно, на плохую камеру. Чтобы обнаружить и их, необходимо грамотно осуществить предобработку – поворачивать, изменять, картинки и прочее. Подобный прием существенно увеличивает объем и вариативность данных для обучения, что в конечном итоге улучшило нашу модель:

def get_train_augmentations(image_size):
    return Compose([
        Resize(image_size, image_size),
        HorizontalFlip(p=0.5),
        RandomBrightnessContrast(p=0.4, brightness_limit=0.25, \
		contrast_limit=0.3),
        RandomGamma(p=0.4),
        CoarseDropout(p=0.1, max_holes=8, max_height=8, max_width=8),
        GaussNoise(p=0.1, var_limit=(5.0, 50.0)),
        ShiftScaleRotate(shift_limit=0.1, scale_limit=0.15, \
		rotate_limit=45, p=0.8),
        ImageCompression(quality_lower=80, quality_upper=100, p=0.4),
        Normalize(
            mean=[0.485, 0.456, 0.406],
            std=[0.229, 0.224, 0.225],
        ),
        ToTensorV2(),
    ])

Процесс обучения, или в нашем случае – дообучения модели, следующий:

Определяются важные параметры для обучения, загружается предобученная модель:

def main():
    BATCH_SIZE = 8
    NUM_WORKERS = 16
    IMAGE_SIZE = 1024
    N_EPOCHS = 50
    device = torch.device("cuda:0")
    df = get_df()

    albumentations_transform = get_train_augmentations(IMAGE_SIZE)
    albumentations_transform_valid = get_val_augmentations(IMAGE_SIZE)

    model = models.resnext50_32x4d(pretrained=True)
    model.fc = nn.Linear(2048, 4)
    model.to(device)

    optimizer = torch.optim.AdamW(model.parameters(), lr=0.00001)
    criterion = nn.CrossEntropyLoss()
    scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(optimizer=optimizer, T_0=200)

Затем модель обучается определенное количество раз (N_EPOCHS):

for epoch in range(N_EPOCHS):
        train_loader, validate_loader = get_loaders(*args)
        train_len = len(train_loader)
        model.train()
        train_loss = 0
        train_acc = 0 

        for i, (imgs, labels) in train_loader:
            imgs = imgs.to(device)
            labels = labels.to(device)
            optimizer.zero_grad()
            output = model(imgs)
            loss = criterion(output, labels)
            loss.backward()
            optimizer.step()
            train_loss += loss.item()
            pred = torch.argmax(torch.softmax(output, 1), 1).cpu().detach().numpy()
            true = labels.cpu().numpy()
            train_acc += accuracy_score(true, pred)
            scheduler.step(epoch + i / train_len)

        model.eval()

Модель в каждой следующей итерации сравнивается по точности предсказаний с предыдущей. Сравнение происходит на тренировочной и валидационной выборке – в нашем случае они берутся из разбиения тренировочной выборки на две в соотношении 80/20.

for i, (imgs, labels) in validate_loader:
            with torch.no_grad():
                imgs_vaild, labels_vaild = imgs.to(device), labels.to(device)
                output_test = model(imgs_vaild)
                val_loss += criterion(output_test, labels_vaild).item()
                pred = torch.argmax(torch.softmax(output_test, 1), \
                        1).cpu().detach().numpy()
                true = labels.cpu().numpy()
                acc_val += accuracy_score(true, pred)

        avg_val_acc = acc_val / val_len
        avg_train_acc = train_acc / train_len
        
        if avg_val_acc > best_acc_val:
            best_acc_val = avg_val_acc
            torch.save(model.state_dict(), f'/path/weight_best.pth')
        elif (avg_val_acc == best_acc_val) \
               and (avg_train_acc == best_acc_train):
            best_acc_train = avg_train_acc
            torch.save(model.state_dict(), f'/path/ weight_best.pth')

        train_losses.append(train_loss / train_len)
        val_losses.append(val_loss / val_len)

В процессе обучения, сохраняется модель с наилучшими показателями. Ее можно использовать дальше для классификации. Вот так, например, менялось среднее значение неправильно распознанных данных на 50 этапах обучения:

Параметр loss для тренировочной и тестовой выборок.
Параметр loss для тренировочной и тестовой выборок.

Также в процессе нашей работы над задачей продумывался и сам алгоритм распознавания. Все файлы, которые удалось преобразовать в формат изображений, классифицируются не единственной моделью, но каскадом моделей с несколькими классами в каждой.

Например, несмотря на добавление «мусорных» классов для ненужных документов, в класс с паспортами всё-равно попадали лишние фото. Было решено обучить модель, отсеивающую файлы, классифицированные как паспорт, но не являющиеся паспортом.

Итак, каскад моделей. В нем первый этап - обработка файлов моделью, распознающей 7 классов. Это паспорта, согласия, заявки, а также 4 класса для других документов. Файлы, распознанные как паспорт проверяются моделью с 2 классами. Файлы, попавшие в «мусорные» классы проверяются другой моделью, обученной на 4 классах для уточнения, не принадлежат ли они к классу согласий.

Достаточно просто если вдуматься. И, оказывается, работает! Например, благодаря модели для верификации паспортов удалось получить следующий результат

Результат проверки гипотезы h0

Возможные состояния проверяемой гипотезы h0

Гипотеза верна

Гипотеза не верна

Гипотеза принимается

10595

338

Гипотеза отклоняется

230

27802

 

Как видно из таблицы, количество ошибок первого и второго рода в задаче классификации паспортов минимально.

Работа над задачей все еще ведется. В данный момент нами дорабатывается модель определения согласий, осталось классифицировать еще много файлов, но все получится и мы обязательно об этом расскажем.

Комментарии (6)


  1. jonie
    29.11.2021 14:27

    400к документов и такие приседания для одноразовой работы? Я в своё время решал похожую задачу (только документов было побольше) - так просто нарисовал мини-приложение что представила UI для оператора вида "обработка такой-то персоны - справа картинка документа и пара кнопок "паспорт-заявление-анкета.." и, наняв 20 человек на месяц (студенты) просто классифицировал все документы "человеческими руками" (+еще и кросс-проверку организовали). Итоговый бюджет по текущим меркам выйдёт на ваши 400к документов ну, тысяч этак в 600 (руб)/месяц (а-то и дешевле). А ваши текущие приседания уже стоили больше, а результата "всё нет"...


    1. amarao
      29.11.2021 16:02
      +2

      Вы включили в бюджет процесса обеспечение конфиденциальности данных? 20 студентов - это отличный источник "база паспортов из заявок на кредит банка СовПодход".


      1. jonie
        30.11.2021 10:47

        конечно - всех загоняют на территорию режимную - обычно не проблема же обеспечить 20 мест рабочих на территории работодателя временных. И конфиденциальность там точно такая же как и в целом в учреждении.


        1. amarao
          30.11.2021 13:21

          Я не знаю как в РФ, а в Европе им нужно будет как минимум GDPR-трейнинг с экзаменом делать. Даже я, казалось бы, к клиентским данным отношения не имею от слова "совсем" его проходил, потому что компании обязаны соответствовать.


    1. agarius
      30.11.2021 06:36

      Думаю, что их 'текущие приседания' не составили и 5 процентов от суммарных ваших и студенческих усилий в прошлом) Не говоря уже о денежных тратах!) Кроме всего, ваше решение нельзя воспроизвести без затрат, а данное в статье - можно!


      1. jonie
        30.11.2021 10:54
        +1

        Да не было там никаких усилий. Две переговорки и три дня организации - вот и и все усилия. По-деньгам я вам ответил сколько это будет стоит сейчас +/- с прогнозируемым результатом и бюджетом. Воспроизводить-то зачем? Задача разовая.

        "Настоящий программист на UNIX это тот, кто два дня пишет скрипт, что делает работу, которую можно было сделать руками за 3 часа всего лишь за 1 секунду".