В рамках процесса кредитования физических и юридических лиц, банки запрашивают у клиентов оригиналы различных документов. Эти документы, очевидно, необходимо проверять по многим критериям. Из пунктов проверки документов достаточно большую значимость среди прочих несет проверка полноты пакета документов. В данной статье будет рассмотрена именно эта процедура.
Существует множество таких заявок на кредит, где заявитель мог подать в банк неполный комплект документов, или может случиться так, что некоторые из поданных заявителем документов сохранены в ненадлежащем формате либо нечитаемы. Может случиться и так, что файлы передадутся до места хранения (сервер) не в полном объеме. Все это – нежелательные явления, которые необходимо обнаружить в процессе работы над данной задачей.
Данные по задаче были предоставлены в большом объеме. Всего предстояло обработать более 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 этапах обучения:

Также в процессе нашей работы над задачей продумывался и сам алгоритм распознавания. Все файлы, которые удалось преобразовать в формат изображений, классифицируются не единственной моделью, но каскадом моделей с несколькими классами в каждой.
Например, несмотря на добавление «мусорных» классов для ненужных документов, в класс с паспортами всё-равно попадали лишние фото. Было решено обучить модель, отсеивающую файлы, классифицированные как паспорт, но не являющиеся паспортом.
Итак, каскад моделей. В нем первый этап - обработка файлов моделью, распознающей 7 классов. Это паспорта, согласия, заявки, а также 4 класса для других документов. Файлы, распознанные как паспорт проверяются моделью с 2 классами. Файлы, попавшие в «мусорные» классы проверяются другой моделью, обученной на 4 классах для уточнения, не принадлежат ли они к классу согласий.
Достаточно просто если вдуматься. И, оказывается, работает! Например, благодаря модели для верификации паспортов удалось получить следующий результат
Результат проверки гипотезы h0 | Возможные состояния проверяемой гипотезы h0 | |
Гипотеза верна | Гипотеза не верна | |
Гипотеза принимается | 10595 | 338 |
Гипотеза отклоняется | 230 | 27802 |
Как видно из таблицы, количество ошибок первого и второго рода в задаче классификации паспортов минимально.
Работа над задачей все еще ведется. В данный момент нами дорабатывается модель определения согласий, осталось классифицировать еще много файлов, но все получится и мы обязательно об этом расскажем.
