Время прочтения: 4 мин.

В работе каждого аудитора возникает необходимость анализа неструктурированных данных, таких как: сканы, фотографии или print screen. Ручная обработка такой информации в больших объемах занимает огромное количество времени. Одним из способов оптимизации трудозатрат является классификация изображений с применением методов Machine Learning. Сегодня хотим поделиться опытом классификации на примере авансовых отчетов.

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

Для начала импортируем необходимые модули.

import zipfile
import os
import random
from keras.models import Sequential
from keras.layers import Convolution2D
from keras.layers import MaxPooling2D
from keras.layers import Flatten
from keras.layers import Dense
from keras.preprocessing.image import ImageDataGenerator
from sklearn.metrics import classification_report

Предварительно мы распределили 6 607 изображений на два класса: «билет» и «прочие». Вес архива с изображениями составил 18,5 Гб.

Распаковываем выборку в рабочую директорию.

with zipfile.ZipFile('/opt/workspace/reports/new_data_for_ML.zip', 'r') as zip_ref:
     zip_ref.extractall('/opt/workspace/reports/')

Считываем пути к выборке.

other = os.listdir('/opt/workspace/reports/other/')
tarif = os.listdir('/opt/workspace/reports/tarif/')

Разделяем выборку на обучающую – «train» и тестовую – «test».

random.shuffle(other)
random.shuffle(tarif)

train_other = other[:round(len(other) * 0.8)]
test_other = other[round(len(other) * 0.8):]

train_tarif = tarif[:round(len(tarif) * 0.8)]
test_tarif = tarif[round(len(tarif) * 0.8):]

print('Прочие документы (other): train -', len(train_other), ', test -', len(test_other), ';')
print('Билеты (tarif): train -', len(train_tarif), ', test -', len(test_tarif), '.')

Прочие документы (other): train - 4894 , test - 1223 ;
Билеты (tarif): train - 392 , test - 98 .

Раскладываем изображения в папки для обучения (train) и валидации (test).

for file in other:
    if file in train_other:
        os.rename('/opt/workspace/reports/other/' + file, 
                  '/opt/workspace/reports/training_set/other/' + file)
for file in other:
    if file in test_other:
        os.rename('/opt/workspace/reports/other/' + file, 
                  '/opt/workspace/reports/test_set/other/' + file)
for file in tarif:
    if file in train_tarif:
        os.rename('/opt/workspace/reports/tarif/' + file, 
                  '/opt/workspace/reports/training_set/tarif/' + file)
for file in tarif:
    if file in test_tarif:
        os.rename('/opt/workspace/reports/tarif/' + file, 
                  '/opt/workspace/reports/test_set/tarif/' + file)

Подготавливаем и настраиваем нашу модель для обучения.

classifier = Sequential()
classifier.add(Convolution2D(32, 3, 3, input_shape = (64, 64, 3), activation = 'relu'))
classifier.add(MaxPooling2D(pool_size = (2, 2)))
classifier.add(Convolution2D(32, 3, 3, activation = 'relu'))
classifier.add(MaxPooling2D(pool_size = (2, 2)))
classifier.add(Flatten())
classifier.add(Dense(output_dim = 128, activation = 'relu'))
classifier.add(Dense(output_dim = 1, activation = 'sigmoid'))
classifier.compile(optimizer='adam', loss = 'binary_crossentropy', metrics=['accuracy'])

train_datagen = ImageDataGenerator(
                rescale = 1./255,
                shear_range = 0.2,
                zoom_range = 0.2,
                horizontal_flip = True)

test_datagen = ImageDataGenerator(rescale = 1./255)

Определяем обучающую выборку

training_set = train_datagen.flow_from_directory(
                '/opt/workspace/reports/training_set',
                target_size=(64, 64),
                batch_size=32,
                class_mode='binary')
Found 5286 images belonging to 2 classes.

Определяем тестовую выборку

test_set = test_datagen.flow_from_directory(
                '/opt/workspace/reports/test_set',
                target_size=(64, 64),
                batch_size=32,
                class_mode='binary')
Found 1321 images belonging to 2 classes.

Запускаем обучение модели

%%time
classifier.fit_generator(
            training_set,
            steps_per_epoch=456,
            epochs=5,
            validation_data=test_set,
            validation_steps=114)
Epoch 1/5
456/456 [==============================] - 2955s - loss: 0.2356 - acc: 0.9275 - val_loss: 0.1528 - val_acc: 0.9481
Epoch 2/5
456/456 [==============================] - 2862s - loss: 0.1464 - acc: 0.9493 - val_loss: 0.1099 - val_acc: 0.9760
Epoch 3/5
456/456 [==============================] - 2841s - loss: 0.1040 - acc: 0.9659 - val_loss: 0.1002 - val_acc: 0.9631
Epoch 4/5
456/456 [==============================] - 2859s - loss: 0.0805 - acc: 0.9730 - val_loss: 0.0748 - val_acc: 0.9767
Epoch 5/5
456/456 [==============================] - 2847s - loss: 0.0601 - acc: 0.9812 - val_loss: 0.1203 - val_acc: 0.9584
CPU times: user 4h 10min 21s, sys: 9min 16s, total: 4h 19min 38s
Wall time: 3h 59min 25s

Наша модель обучилась за 4 часа.

Определяем функцию считывания изображения для предсказания

def load_image(img_path, show=True):
    img_original = image.load_img(img_path)
    img = image.load_img(img_path,target_size=(64,64))
    img_tensor = image.img_to_array(img)
    img_tensor = np.expand_dims(img_tensor,axis=0)
    img_tensor /= 255.
    return img_tensor

Определяем функцию предсказания класса где 0 это «прочие», а 1 – «билет».

def predict(img_file):
    new_image = load_image(img_file)
    pred = classifier.predict(new_image)
    if pred<.5 :
        return 0
    else:
        return 1

Подготавливаем датасет для предсказания и оценки точности предсказания нашей модели.

other = os.listdir('/opt/workspace/reports/test_set/other/')
tarif = os.listdir('/opt/workspace/reports/test_set/tarif/')

df_othet = pd.DataFrame(other)
df_othet['target'] = 0
df_othet['path'] = '/opt/workspace/reports/test_set/other/'

df_tarif = pd.DataFrame(tarif)
df_tarif['target'] = 1
df_tarif['path'] = '/opt/workspace/reports/test_set/tarif/'

df = pd.concat([df_othet, df_tarif]).reset_index()

df['path'] = df['path'] + df[0]

df['predict'] = 0

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

for ix in df.index:
    df['predict'][ix] = predict(df['path'][ix])

print(classification_report(df['target'], df['predict']))
             precision    recall  f1-score   support

          0       0.99      0.97      0.98      1223
          1       0.75      0.94      0.83        98

avg / total       0.98      0.97      0.97      1321

Таким образом, с помощью Machine Learning нам удалось выделить 6,6 тыс. билетов на проезд в общественном транспорте, необходимых для анализа, из более чем 30 тыс. авансовых отчетов. Это позволило существенно сократить трудозатраты и исключить просмотр излишней информации, которая составила более 78% общего объема документов.