В рамках процесса кредитования физических и юридических лиц, банки запрашивают у клиентов оригиналы различных документов. Эти документы, очевидно, необходимо проверять по многим критериям. Из пунктов проверки документов достаточно большую значимость среди прочих несет проверка полноты пакета документов. В данной статье будет рассмотрена именно эта процедура.
Существует множество таких заявок на кредит, где заявитель мог подать в банк неполный комплект документов, или может случиться так, что некоторые из поданных заявителем документов сохранены в ненадлежащем формате либо нечитаемы. Может случиться и так, что файлы передадутся до места хранения (сервер) не в полном объеме. Все это – нежелательные явления, которые необходимо обнаружить в процессе работы над данной задачей.
Данные по задаче были предоставлены в большом объеме. Всего предстояло обработать более 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 |
Как видно из таблицы, количество ошибок первого и второго рода в задаче классификации паспортов минимально.
Работа над задачей все еще ведется. В данный момент нами дорабатывается модель определения согласий, осталось классифицировать еще много файлов, но все получится и мы обязательно об этом расскажем.
jonie
400к документов и такие приседания для одноразовой работы? Я в своё время решал похожую задачу (только документов было побольше) - так просто нарисовал мини-приложение что представила UI для оператора вида "обработка такой-то персоны - справа картинка документа и пара кнопок "паспорт-заявление-анкета.." и, наняв 20 человек на месяц (студенты) просто классифицировал все документы "человеческими руками" (+еще и кросс-проверку организовали). Итоговый бюджет по текущим меркам выйдёт на ваши 400к документов ну, тысяч этак в 600 (руб)/месяц (а-то и дешевле). А ваши текущие приседания уже стоили больше, а результата "всё нет"...
amarao
Вы включили в бюджет процесса обеспечение конфиденциальности данных? 20 студентов - это отличный источник "база паспортов из заявок на кредит банка СовПодход".
jonie
конечно - всех загоняют на территорию режимную - обычно не проблема же обеспечить 20 мест рабочих на территории работодателя временных. И конфиденциальность там точно такая же как и в целом в учреждении.
amarao
Я не знаю как в РФ, а в Европе им нужно будет как минимум GDPR-трейнинг с экзаменом делать. Даже я, казалось бы, к клиентским данным отношения не имею от слова "совсем" его проходил, потому что компании обязаны соответствовать.
agarius
Думаю, что их 'текущие приседания' не составили и 5 процентов от суммарных ваших и студенческих усилий в прошлом) Не говоря уже о денежных тратах!) Кроме всего, ваше решение нельзя воспроизвести без затрат, а данное в статье - можно!
jonie
Да не было там никаких усилий. Две переговорки и три дня организации - вот и и все усилия. По-деньгам я вам ответил сколько это будет стоит сейчас +/- с прогнозируемым результатом и бюджетом. Воспроизводить-то зачем? Задача разовая.
"Настоящий программист на UNIX это тот, кто два дня пишет скрипт, что делает работу, которую можно было сделать руками за 3 часа всего лишь за 1 секунду".