С тех пор как 2 недели назад вышел ChatGPT, у меня в ленте полно скриншотов, где человек вставляет в него документ и получает обратно поля. Банковская выписка тут классический случай. Каждый банк выгружает её по-своему. Обычное решение это парсер с регуляркой под каждый банк. Он ломается в тот день, когда банк меняет формат выгрузки. Я ни разу не замерял, как модель справляется по сравнению с таким парсером. Я считал, что дату модель хотя бы скопирует правильно. Поэтому вопрос для меня был только в том, как часто она ошибается в сумме.
Стенд
Я взял 4 формата выписки и сгенерировал скриптом по 150 операций на формат, seed 2022, так что каждый верный ответ известен. Каждая операция должна превратиться в 3 поля: дату, сумму со знаком (расход с минусом) и контрагента. Форматы выглядят так:
A 14.06.2022 CARD PAYMENT MEDIA EXPERT 7734 GDANSK -248,22 18 848,37
B Date: 2022-06-20 Description: Transfer to Piotr Wisniewski, title: gift Amount: -2,327.19 PLN Balance: -5,575.15 PLN
C 12 Jun 2022 CARD PAYMENT BOOKING.COM 33.40 EUR 157.32 DR 25,180.67 CR
D 15/06/2022 Card payment 4.55 53,361.89
AUCHAN 5293 GDANSK
Последнее число в каждой строке это остаток после операции. У оплат картой за границей в описании есть ещё сумма в валюте операции. Парсер на регулярках я написал только под A и B, 34 строки на Python. Написал до того, как посмотрел на C и D. C и D играют роль банка, который сменил формат выгрузки.
Модель это Flan-T5 от Google в двух размерах, base (250 млн параметров) и large (780 млн). Обе вышли в октябре и работают у меня на машине: 4 ядра виртуалки на AMD EPYC, Python 3.9, torch 1.13, transformers 4.25.1, жадное декодирование. На каждую операцию 4 вопроса: дата, сумма, направление платежа и кто вторая сторона. В промпте есть ещё строка выписки с первым и последним днём периода. В настоящей выписке она тоже есть, по ней видно, в каком формате банк пишет даты. Оба способа получают операции уже нарезанными на блоки, так что разбиение страницы в замер не входит.
Операция считается верной, только если верны все 3 поля. Неверный результат громкий, когда он не разбирается. Это значит, что регулярка не совпала или модель выдала вместо даты что-то, что датой не является. Тихий, когда он разбирается, но неверен.
Регулярки
На A и B парсер верно разобрал 293 операции из 300. 7 неверных на моей совести. В строке перевода формата A имя и назначение платежа идут подряд капсом, без разделителя: TRANSFER FROM JAN KOWALSKI LOAN BACK. За имя я брал первые 2 слова. Для зарплаты от SOFTHOUSE SP. Z O.O. это даёт SOFTHOUSE SP., а парсер молчит. Это правило я писал, не думая про компании.
На C и D не совпала ни одна регулярка, все 300 отказов громкие. Этого я и ждал. Это же обычный довод в пользу модели.
Модель
Flan-T5 large верно разобрала 91 операцию из 600. Base разобрала 107, так что модель побольше лучше не оказалась.
Я ждал ошибок в сумме, но первой сломалась дата. Я какое-то время думал, что сломан мой разбор дат, пока не посмотрел на сырые ответы. Со строкой периода в промпте large в 360 операциях из 600 ответила первым днём периода. Для платежа от 14 июня она давала 01.06.2022. Тогда я убрал строку периода и спросил ещё раз. Large назвала дату верно в 291 операции из 600, base в 567. Часто ответ large это не дата из строки: 01.10.2022 вместо 10.01.2022, 01/26/2022 в американском порядке вместо 26/01/2022. Иногда год был 2020. Только в формате B она дала 148 верных из 150, где дата уже была записана как 2022-06-20. Когда я попросил скопировать дату ровно так, как она написана, модель так и сделала в 162 случаях из 600. 143 из них пришлись на B.
На каждый входящий перевод large отвечала “out”, кроме формата D, где в строке словами написано Incoming transfer. Ответ “in” она дала на 33 входящих перевода из 129, все 33 в D. На строках без минуса и на строках с CR large всё равно отвечала “out”. Я подумал, что виноват порядок вариантов в моём вопросе. Поэтому спросил ещё раз с “in or out” вместо “out or in”. Large не поменяла ни одного ответа. Base от порядка зависела. Когда первым стоял “out”, она отвечала “out” во всех 600 операциях, а когда первым стоял “in”, отвечала “in” в 583 из 600.
Сумма без знака была верной в 423 операциях из 600. В 114 неверных модель вернула остаток, последнее число в строке. В 22 вернула сумму в евро или долларах из описания. Для контрагента large часто возвращала не только имя, например SPOTIFY INTERNET. 61 раз она вернула только город. Если принимать любой ответ, в котором есть верное имя, выходит 526 из 600. С таким нестрогим сравнением у large было бы 160 верных операций вместо 91.
Почти все эти ошибки тихие, потому что остаток разбирается как сумма ничуть не хуже самой суммы. Из 509 неверных операций large громкими были 32.
Сначала регулярка, модель только там, где регулярка не совпала
Именно такую схему я бы и построил: парсер для знакомых форматов и модель только для строк, которые он не узнал. Она дала 335 верных из 600, больше, чем каждый из двух способов по отдельности. Но 256 операций вышли неверными, но ни одна не вызвала ошибки. Кроме моих 7 строк с зарплатой, все они пришли из C и D, тех 300 строк, на которых раньше был громкий отказ.
Потом я сделал то, что сделал бы в любом случае, когда банк меняет выгрузку: написал регулярки под C и D. Это ещё 29 строк. Парсер верно разобрал 582 из 600 и ни одной громкой ошибки. 18 неверных это тот же баг с зарплатой: я скопировал его в разбор формата C вместе с остальным кодом. Парсер тратит около 6 микросекунд на операцию. Large тратит в медиане 1,2 секунды на вопрос, то есть около 5 секунд на операцию.
Чего я не проверял
Это одна маленькая модель, которую я могу запустить сам. Это не GPT-3 из API OpenAI и не ChatGPT, у которого API пока нет. Ничего крупнее я не пробовал. На каждое поле был один промпт без примеров. Ничего я не дообучал. Заставить модель выдать все поля сразу в JSON я тоже не пробовал. Выписки синтетические, это чистый текст без PDF и OCR.
Пока я оставляю регулярку под каждый банк. Модель я бы использовал только как подсказку разбора для строки, которую парсер не знает. Подсказку проверяет человек, прежде чем она пойдёт дальше.