SigNet (обнаружение сходства сигнатур с использованием машинного обучения / глубокого обучения): это конец судебно-медицинской экспертизы?

Мой дедушка был знатоком почерка. Он провел всю свою жизнь, анализируя документы для CBI (Центральное бюро расследований) и других организаций. Его уникальный способ анализа документов с помощью увеличительного стекла и различных инструментов требовал огромного количества времени и терпения для анализа одного документа. Это было тогда, когда компьютеры были недостаточно быстрыми. Я хорошо помню, что он несколько раз скопировал один и тот же документ и положил его на стол, чтобы лучше рассмотреть почерк.

Анализ почерка включает всесторонний сравнительный анализ сомнительного документа и известного почерка предполагаемого автора. Конкретные привычки, характеристики и индивидуальности как сомнительного документа, так и известного образца исследуются на сходства и различия.

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

Почему и как?

Зачем? Уникальный способ моего дедушки анализировать документы с помощью увеличительного стекла и различных инструментов требовал огромного количества времени и терпения для анализа одного документа. Это было тогда, когда компьютеры были недостаточно быстрыми. Я хорошо помню, что он несколько раз сделал несколько копий одного и того же документа и разложил его на столе, чтобы лучше рассмотреть почерк. Хотя я согласен с тем, что мы не можем заменить эту работу искусственным интеллектом со 100% точностью, мы, безусловно, можем построить систему, способную помочь людям.

Как: Чтобы построить нашу сеть сигнатурного сходства, мы будем использовать чудеса глубокого обучения. Мы рассмотрим три подхода, чтобы выявить сходство между нашими собственноручными подписями. В качестве исходных данных мы будем использовать набор данных HandWritten Signatures от Kaggle.

Требования

Для этого проекта нам потребуются:

  • Python 3.8: язык программирования
  • TensorFlow 2: библиотека глубокого обучения
  • Numpy: линейная алгебра
  • Matplotlib: Построение изображений
  • Scikit-Learn: общая библиотека машинного обучения

Набор данных

Набор содержит настоящие и поддельные подписи 30 человек. У каждого человека есть 5 подлинных и 5 поддельных подписей.

Для загрузки данных я создал простую load_data (), которая выполняет итерацию по всем наборам данных и извлекает настоящие и поддельные подписи с меткой 1 и 0 соответственно.

В дополнение к этому, я также создал словарь кортежей, состоящий из изображений и меток. (Будет использовано позже в проекте).

def load_data(DATA_DIR=DATA_DIR, test_size=0.2, verbose=True, load_grayscale=True):
    """
        Loads the data into a dataframe.
        
        Arguments:
            DATA_DIR: str
            test_size: float
        Returns:
            (x_train, y_train,x_test, y_test, x_val, y_val, df)
    """
    features = []
    features_forged = []
    features_real = []
    features_dict = {}
    labels = [] # forged: 0 and real: 1
    mode = "rgb"
    if load_grayscale:
        mode = "grayscale"
    
    for folder in os.listdir(DATA_DIR):
        # forged images
        if folder == '.DS_Store' or folder == '.ipynb_checkpoints':
            continue
        print ("Searching folder {}".format(folder))
        for sub in os.listdir(DATA_DIR+"/"+folder+"/forge"):
            f = DATA_DIR+"/"+folder+"/forge/" + sub
            img = load_img(f,color_mode=mode, target_size=(150,150))
            features.append(img_to_array(img))
            features_dict[sub] = (img, 0)
            features_forged.append(img)
            if verbose:
                print ("Adding {} with label 0".format(f))
            labels.append(0) # forged
        # real images
        for sub in os.listdir(DATA_DIR+"/"+folder+"/real"):
            f = DATA_DIR+"/"+folder+"/real/" + sub
            img = load_img(f,color_mode=mode, target_size=(150,150))
            features.append(img_to_array(img))
            features_dict[sub] = (img, 1)
            features_real.append(img)
            if verbose:
                print ("Adding {} with label 1".format(f))
            labels.append(1) # real
            
    features = np.array(features)
    labels = np.array(labels)
    
    x_train, x_test, y_train, y_test = train_test_split(features, labels, test_size=test_size, random_state=42)
    x_train, x_val, y_train, y_val = train_test_split(x_train, y_train, test_size=0.25, random_state=42)
    
    print ("Generated data.")
    return features, labels,features_forged, features_real,features_dict,x_train, x_test, y_train, y_test, x_val, y_val
def convert_label_to_text(label=0):
    """
        Convert label into text
        
        Arguments:
            label: int
        Returns:
            str: The mapping
    """
    return "Forged" if label == 0 else "Real"
features, labels,features_forged, features_real, features_dict,x_train, x_test, y_train, y_test, x_val, y_val = load_data(verbose=False, load_grayscale=False)

Визуализация данных

Изображения загружаются с целевым_размером (150,150,3).

Подход №1: Сходство изображений (подписей) с использованием MSE и SSIM.

Для этого подхода мы будем вычислять сходство между изображениями, используя MSE (среднеквадратичная ошибка) или SSIM (структурное сходство). Как видите, формулы довольно просты, и, к счастью, Scikit-Learn предоставляет реализацию для SSIM.

def mse(A, B):
    """
        Computes Mean Squared Error between two images. (A and B)
        
        Arguments:
            A: numpy array
            B: numpy array
        Returns:
            err: float
    """
    
    # sigma(1, n-1)(a-b)^2)
    err = np.sum((A - B) ** 2)
    
    # mean of the sum (r,c) => total elements: r*c
    err /= float(A.shape[0] * B.shape[1])
    
    return err
def ssim(A, B):
    """
        Computes SSIM between two images.
        
        Arguments:
            A: numpy array
            B: numpy array
            
        Returns:
            score: float
    """
    
    return structural_similarity(A, B)

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

Как видите, ошибка MSE не имеет фиксированной границы, тогда как SSIM имеет фиксированную границу между -1 и 1.

Нижняя MSE представляет похожие изображения, тогда как нижняя SSIM представляет похожие изображения.

Подход № 2: построение классификатора с использованием CNN, способных обнаруживать поддельные или настоящие подписи.

При таком подходе мы попытаемся придумать классификатор (с использованием CNN) для обнаружения поддельных или настоящих подписей
Так как CNN, как известно, обнаруживают сложные особенности среди изображений, мы будем экспериментировать с этот классификатор.

Мы неизбежно столкнемся с переобучением, поскольку у нас недостаточно данных.
Мы, вероятно, будем использовать Image Augmentation для генерации большего количества обучающих данных.

При обучении нашей модели мы неизбежно столкнемся с переобучением, и после применения методов для преодоления проблемы модель не улучшилась.

Подход №2.1: Перенос обучения с использованием начала

Чтобы улучшить нашу модель, мы воспользуемся трансферным обучением и настроим модель для этой конкретной проблемы.

Модель InceptionV3

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

# loading Inception
model2 = tf.keras.applications.InceptionV3(include_top=False, input_shape=(150,150,3))
# freezing layers
for layer in model2.layers:
    layer.trainable=False
# getting mixed7 layer
l = model2.get_layer("mixed7")
x = tf.keras.layers.Flatten()(l.output)
x = tf.keras.layers.Dense(1024, activation='relu')(x)
x = tf.keras.layers.Dropout(.5)(x)                  
x = tf.keras.layers.Dense(1, activation='sigmoid')(x)           
net = tf.keras.Model(model2.input, x)
net.compile(optimizer='adam', loss=tf.keras.losses.binary_crossentropy, metrics=['acc'])
h2 = net.fit(x_train, y_train, validation_data=(x_val, y_val), epochs=5)

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

Есть еще много способов улучшить нашу модель, один - путем увеличения данных.

Подход № 3: сиамские сети для определения сходства изображений

С помощью нашего третьего подхода мы попытаемся изучить функцию подобия. Мы будем использовать так называемые сиамские сети (из-за характера наших данных, т. Е. Меньшего количества обучающих примеров).

В этом подходе мы будем использовать сиамские сети, чтобы изучить функцию подобия. Сиамский означает «близнецы», и самая большая разница между обычными NN заключается в том, что эти сети пытаются изучить функцию подобия вместо того, чтобы пытаться классифицировать (соответствовать функции).

  • Сначала мы создаем общий вектор признаков для наших изображений. Мы передадим два изображения (положительное и отрицательное) и воспользуемся функцией контрастных потерь (метрика расстояния (расстояние L1)) и, в конце концов, сожмем вывод между 1 и 0 (сигмоид), чтобы получить окончательный результат.

# creating the siamese network
im_a = tf.keras.layers.Input(shape=(150,150,3))
im_b = tf.keras.layers.Input(shape=(150,150,3))
encoded_a = feature_vector(im_a)
encoded_b = feature_vector(im_b)
combined = tf.keras.layers.concatenate([encoded_a, encoded_b])
combine = tf.keras.layers.BatchNormalization()(combined)
combined = tf.keras.layers.Dense(4, activation = 'linear')(combined)
combined = tf.keras.layers.BatchNormalization()(combined)
combined = tf.keras.layers.Activation('relu')(combined)
combined = tf.keras.layers.Dense(1, activation = 'sigmoid')(combined)
sm = tf.keras.Model(inputs=[im_a, im_b], outputs=[combined])
sm.summary()

Создание набора данных

Чтобы создать требуемый набор данных, мы попробуем два подхода. Сначала сгенерируем данные на основе ярлыков. Если два изображения имеют одинаковую метку (1 или 0), они похожи. Мы будем генерировать данные попарно в форме (im_a, im_b, label). Во-вторых, мы будем генерировать данные на основе номера человека. Согласно набору данных, 02104021.png представляет собой подпись, созданную человеком 21 (т.е. настоящую).

Подход №1 генерации данных:

Здесь мы предполагаем подобие на основе ярлыков. Если два изображения имеют одинаковую метку (например, 1 или 0), они похожи.

def generate_data_first_approach(features, labels, test_size=0.25):
    """
        Generate data in pairs according to labels.
        Arguments:
            features: numpy
            labels: numpy
    """
    im_a = [] # images a
    im_b = [] # images b
    pair_labels = []
    for i in range(0, len(features)-1):
        j = i + 1
        if labels[i] == labels[j]:
            im_a.append(features[i])
            im_b.append(features[j])
            pair_labels.append(1) # similar
        else:
            im_a.append(features[i])
            im_b.append(features[j])
            pair_labels.append(0) # not similar
            
    pairs = np.stack([im_a, im_b], axis=1)
    pair_labels = np.array(pair_labels)
    x_train, x_test, y_train, y_test = train_test_split(pairs, pair_labels, test_size=test_size, random_state=42)
    x_train, x_val, y_train, y_val = train_test_split(x_train, y_train, test_size=0.25, random_state=42)
    return x_train, y_train, x_test, y_test, x_val, y_val, pairs, pair_labels
x_train, y_train, x_test, y_test, x_val, y_val, pairs, pair_labels = generate_data_first_approach(features, labels)
# show data
plt.imshow(pairs[:,0][0]/255.)
plt.show()
plt.imshow(pairs[:,1][0]/255.)
plt.show()
print("Label: ",pair_labels[0])

Обучение набора данных с помощью Dataset Generation # 1

Теперь обучим сеть. Из-за вычислительных ограничений мы обучаем модель только для одной эпохи.

# x_train[:,0] => axis=1 (all 150,150,3) x_train[:,1] => axis=1 (second column)
sm.fit([x_train[:,0], x_train[:,1]], y_train, validation_data=([x_val[:,0],x_val[:,1]], y_val),epochs=1)

  • Эта метрика вычисляет расстояние L1 (MAE) между y_hat и y.
  • Из-за вычислительных ограничений мы обучаем его только для одной эпохи
  • Это представляет собой очень простую сиамскую сеть, способную обучаться функции подобия.

Подход к генерации данных # 2

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

def generate_data(person_number="001"):
    x = list(features_dict.keys())
    im_r = []
    im_f = []
    labels = [] # represents 1 if signature is real else 0
    for i in x:
        if i.startswith(person_number):
            if i.endswith("{}.png".format(person_number)):
                im_r.append(i)
                labels.append(1)
            else:
                im_f.append(i)
                labels.append(0)
    return im_r, im_f, labels
def generate_dataset_approach_two(size=100, test_size=0.25):
    """
        Generate data using the second approach.
        Remember input and output must be the same size!
        
        Arguments:
            features: numpy array
            labels: numpy array
            size: the target size (length of the array)
        Returns:
            x_train, y_train
    """
    im_r = []
    im_f = []
    ls = []
ids = ["001","002","003",'004','005','006','007','008','009','010','011','012','013','014','015','016','017','018','019','020','021','022',
           '023','024','025','026','027','028','029','030']
    
    for i in ids:
        imr, imf, labels = generate_data(i)
        
        # similar batch
        for i in imr:
            for j in imr:
                im_r.append(img_to_array(features_dict[i][0]))
                im_f.append(img_to_array(features_dict[j][0]))
                ls.append(1) # they are similar
        
        # not similar batch
        for k in imf:
            for l in imf:
                im_r.append(img_to_array(features_dict[k][0]))
                im_f.append(img_to_array(features_dict[l][0]))
                ls.append(0) # they are not similar
    
    print(len(im_r), len(im_f))
    pairs = np.stack([im_r, im_f], axis=1)
    ls = np.array(ls)
    
    x_train, x_test, y_train, y_test = train_test_split(pairs, ls, test_size=test_size, random_state=42)
    x_train, x_val, y_train, y_val = train_test_split(x_train, y_train, test_size=0.25, random_state=42)
    return x_train, y_train, x_test, y_test, x_val, y_val, pairs, ls
x_train, y_train, x_test, y_test, x_val, y_val, pairs, ls = generate_dataset_approach_two()
# show data
plt.imshow(x_train[:,0][0]/255.)
plt.show()
plt.imshow(x_train[:,0][1]/255.)
print("Label: ",y_train[0])

Обучение сети с помощью генерации набора данных №2

Самая большая разница между поколениями наборов данных №1 и №2 заключается в способе организации входных данных. В наборе данных №1 мы выбираем случайные подписи в соответствии с их метками, а в №2 мы выбираем подписи от одного и того же человека.

Заключение

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

использованная литература

[1] https://arxiv.org/pdf/1709.08761.pdf

Github: https://github.com/aaditkapoor/SigNet