Как стать автором
Обновить

Могут ли нейросети читать чеки?

Время на прочтение 6 мин
Количество просмотров 6.8K

Вот уже почти три года я скрупулезно записываю все свои доходы и расходы в hledger. Почему именно он? Так сложилось исторически. С наступлением 2018 года я начал все записывать в гугл табличку, а в апреле поехал в Японию. Я сидел в отеле и пытался понять, как мне правильно считать цены в разных валютах, и решил написать что-нибудь на лиспе. И написал. И показал это людям в емаксовом чатике. На что получил ответ "а ведь уже есть готовое" и ссылку на hledger. После чего перетащил в hledger все свои записи из гугловой таблички.

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

И вот изучая в какой-то момент чек от Утконоса, я задумался, а сколько же я трачу на шоколад? Спойлер – много. Полез в историю заказов, нашел там старые чеки, переписал записи по категориям. Были просто "expenses:продукты", а стало "expenses:продукты:фрукты" и прочие. Заодно там же обнаружились и кое-какие бытовые товары.

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

Но ведь нейросети и прочий датасатанизм.

В голове возникла идея, что нейросеть вполне в состоянии понять, что "GL.VIL.Апельсины ОТБОР.фас.1кг" это фрукт такой, в отличие от "НИЖ.ХЛ.атон ПОДМОСКОВНЫЙ 400г" (хлеб, но для этого мне пришлось скопировать это название в гугл).

Задача, которую требуется решить, это очевидно классификация. На вход подаются строчки-названия, а на выходе ожидается категория. Изначально эти категории я буду проставлять вручную, а потом уже только править предсказания. Соответственно программа должна сначала проанализировать текст чека, выделить оттуда названия продуктов и их стоимости, для каждого названия предсказать категорию. Показать мне предсказанное, чтобы я мог поправить. Когда все в порядке, сформировать набор строчек для hledger.

Анализ текста я сделал просто с помощью разбиения на строки, а строк на части. Обычные правила, сформулированные на питоне.

Примерно так
def parse_utk(lines):
    while lines:
        if lines[0].startswith('Адрес проверки чека'):
            break
        lines.pop(0)
    else:
        return
    lines.pop(0)
    result = []
    while lines:
        data = lines[0:6]
        name, price, lines = lines[0], lines[3], lines[6:]
        if name.startswith('ИТОГ'):
            break
        assert price.startswith('= ')
        result.append((name, Decimal(price[2:]))
    return result

Не самый изящный код, но свое дело делает. Аналогичным образом у меня сразу были написаны правила для чеков Пятерочки и Магнита.

Дальше начинается более интересная часть, а именно нейросети. Я уже давно считаю, что будущее за character-level моделями, в частности мне нравится flair. Чтобы сделать нейросеть нужны два компонента – языковая модель и собственно классификатор. Языковую модель я делаю в двух экземплярах – по предсказанию последующих символов и по предсказанию предыдущих. Скрипт для обучения сети сразу написан так, чтобы можно было как обучать сеть с нуля, так и дообучать существующую.

Результат оказался даже лучше, чем я ожидал. Как только я справился со всеми тонкостями "правильно запустить", сеть сразу же стала показывать неплохие результаты. Первые три чека пришлось разметить вручную. В четвертом я исправил примерно половину предсказаний. В девятом из 23 позиций корректно были определены 21. При появлении новых категорий я переобучал сеть заново – это занимает минуты.

После этого я уже смог пересмотреть часть записей в hledger.

Я же говорил про шоколад
$ hledger bal продукты -b thisyear -% -S
             100.0 %  expenses:продукты
              20.6 %    <unsorted>
              15.3 %    шоколад
               7.9 %    овощи
               7.1 %    сыр
               6.9 %    заморозка
               6.3 %    кофе
               5.2 %    колбаса
               4.8 %    чай
               4.0 %    снеки
               3.8 %    яйца
               2.7 %    мясо
               2.7 %    вода
               2.0 %    макароны
               2.0 %    фрукты
               1.7 %    консервы
               1.6 %    алкоголь
               1.3 %    бакалея
               1.2 %    морепродукты
               0.9 %    молоко
               0.9 %    крупы
               0.5 %    соусы
               0.4 %    хлеб
               0.2 %    специи

И если уже все работает, наверно можно было бы и успокоиться. Что я вобщем и сделал и пару месяцев просто пользовался. Каждый раз радуюсь, что нейросеть способна отличить соль для ванн (бытовые товары) от обычной поваренной соли.

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

Это еще одна хотелка – я считаю, что предобработка данных для нейросетей должна быть минимальной. Мой код для парсинга чеков это некоторая rule-based предобработка. А вот сможет ли нейросеть просто из полного текста чека (или точнее копи-пасты из личного кабинета) выцепить все те данные, которые нужны классификатору? И если да, то как ей это объяснить.

Это уже другая задача – NER. Опять же я беру flair, но языковая модель будет обучена на другом наборе данных (насколько это обязательно? Может быть и классификатор смог бы корректно работать с языковой моделью полного чека). Первый пункт это разметить данные для обучения. Я потратил полдня на код, который позволяет мне в интерактивном режиме проставить IOB-теги для каждого токена в тексте чека… А потом понял, что занимался этим зря.

Если можно проанализировать чек с помощью rule-based парсеров, то ими же можно сгенерировать обучающие данные. И еще полдня я убил на это. Впрочем, "убил" это не самое подходящее слово. Все получилось, но результат был так себе. Теггер не мог адекватно понять, почему вот это число это количество товара и потому не нужно, а вот это число это рубли, а значит требуется. Во-вторых, были трудности с адекватным сопоставлением результатов rule-based парсинга и предсказанного теггером.

Еще мне в глаза бросилось некоторое различие между строчками, которые я отдавал в теггер и которые он выдавал в качестве предложений. Как оказалось, дело в токенизаторе. Мне нужен был токенизатор, которые просто разбивает по пробельным символам. В flair есть готовый SpaceTokenizer, который разбивает по пробелам, но у меня бывают также и табы и переносы строк. Пришлось реализовать свой методом "скопировать и поменять одно условие". Результат стал лучше, но все равно далек от желаемого.

Где-то когда-то мне попалась фраза: "Чтобы понять, сможет ли нейросеть решить ту или иную задачу, надо ответить на вопрос, сможет ли ее на основе этих же данных решить специалист". Действительно, из строки "1 107 99" вовсе неочевидно, что это одна бутылка Швепса стоимостью 107 рублей 99 копеек. Да и вообще, сопоставить сам товар с его ценой это тоже не очень тривиально (не во всех форматах чеков цена идет после имени товара). Но как бы читал чек живой человек? Когда я читаю чек, я сначала пытаюсь понять, какие строки относятся к одному товару.

Итак, новая модель. Точнее две. Первая модель должна сделать ровно одну вещь – все строчки разбить на "ненужную фигню", "начало блока по товару" и "продолжение блока по товару". То есть NER с ровно одним видом сущностей. Вторая модель внутри блока будет искать сущности "название товара" и "стоимость".

Для первой модели опять же нужен особый токенизатор. Токенами для нее являются строки чека, то есть токенизатор должен делить по символам новой строки (да, еще один свой токенизатор, NewlineTokenizer), а файл с данными для обучения должен допускать пробельные символы в строке с текстом. Ничего страшного, в ColumnCorpus можно задать произвольный разделитель столбцов. Сначала я выбрал \r, но нарвался на то, что питон считает его нормальным окончанием строки. Затем я взял vertical tab (\x0b), но как раз в этот момент мне подсказали о существовании символов RS и US.

Вроде бы что-то стало получаться, но не совсем. Смущало, что в выхлопе присутствует категория O, которая вроде бы и не категория вовсе. Я долго и внимательно изучал логи, пытаясь понять, почему оно анализируется как отдельная категория. А потом задался вопросом, почему при выводе меток, которые проставил теггер, степень достоверности пишется с новой строки.

И вдруг оказалось, что название категории 'entry' не равно строке 'entry'. Это могло означать только одно – имена категорий попали в теггер с символами новой строки на конце. То есть вместо тега 'O', который должен обрабатываться особым образом, был тег 'O\n', на который эти привилегии не распространяются.

Еще некоторое время ушло на изучение того, почему вообще это происходит. Как оказалось – это баг в flair.

И вот после этого уже стало все очень неплохо. Сначала первый теггер выделяет из чека сегменты по конкретным товарам – обучается с нуля быстро, но изначально делает ошибки и включает "Итого" как один из товаров. Потом второй теггер совершенно корректно выделяет продукты.

При наличии работающих rule-based парсеров не очень понятно, зачем такой "умный" парсер мог бы пригодиться. Конечно же, для таких чеков, для которых rule-based парсера еще нет. Чтобы было с чем экспериментировать, пришлось идти и оформлять для себя учетку во Вкусвилле, чтобы получить новый формат чеков, для которого rule-based парсера еще нет.

Одного единственного чека оказалось вполне достаточно, чтобы обучить теггеров. Правда сначала пришлось допилить скрипт, чтобы редактировать промежуточные результаты, а потом еще и сохранять их в правильном формате (я успел забыть про особый разделитель столбцов и час не понимал, почему ничего не получается). Осталось только прикрутить "нейросетевое" разбиение к основному парсеру.

Потом причесать код и опубликовать.

P.S. исходники

Теги:
Хабы:
+15
Комментарии 0
Комментарии Комментировать

Публикации

Истории

Работа

Data Scientist
58 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн