Мы будем строить модель CNN для обнаружения дорожных знаков.

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

Если вы не знакомы с основами ConvNet, вы можете изучить его здесь.

Мы будем использовать пакет keras для построения модели CNN.

Получить набор данных

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

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

Предварительная обработка данных

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

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

Нам нужно сжать или интерполировать изображения до одного измерения. Нет, чтобы сжать большую часть данных и не растягивать изображение слишком сильно, нам нужно определить размер, который находится между ними, и сохранить данные изображения в основном точными. Я решил использовать размер 64 * 64 * 3.

Мы преобразуем изображение в заданное измерение с помощью пакета opencv.

import cv2
def resize_cv(img):
    return cv2.resize(img, (64, 64), interpolation = cv2.INTER_AREA)

cv2 - это пакет opencv. Метод resize преобразует изображение в заданное измерение. Здесь мы преобразуем изображение в размер 64 * 64. Интерполяция определит, какой тип техники вы хотите использовать для растяжения или сжатия изображений. Opencv предоставляет 5 типов методов интерполяции в зависимости от метода, который они используют для оценки значений пикселей полученного изображения. Техника INTER_AREA, INTER_NEAREST, INTER_LINEAR, INTER_CUBIC, INTER_LANCZOS4. Мы будем использовать INTER_AREA метод интерполяции, он более предпочтителен для прореживания изображения, но для метода экстраполяции он аналогичен INTER_NEAREST. Мы могли бы использовать INTER_CUBIC, но для этого требуется большая вычислительная мощность, поэтому мы не будем его использовать.

Загрузка данных

Выше мы узнали, как предварительно обработать изображения. Теперь мы загрузим набор данных и преобразуем их в заданное измерение.

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

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

import seaborn as sns
fig = sns.distplot(output, kde=False, bins = 43, hist = True, hist_kws=dict(edgecolor="black", linewidth=2))
fig.set(title = "Traffic signs frequency graph",
        xlabel = "ClassId",
        ylabel = "Frequency")

ClassId - это уникальный идентификатор, присвоенный каждому уникальному дорожному знаку.

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

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

Поскольку набор данных разделен на несколько папок и названия изображений не совпадают, мы загрузим все изображения, преобразовав их в размерности (64 * 64 * 3) в один список list_image , а дорожный знак, на который он похож, - в другой список output. Мы будем читать изображения, используя imread.

list_images = []
output = []
for dir in os.listdir(data_dir):
    if dir == '.DS_Store' :
        continue
    
    inner_dir = os.path.join(data_dir, dir)
    csv_file = pd.read_csv(os.path.join(inner_dir,"GT-" + dir + '.csv'), sep=';')
    for row in csv_file.iterrows() :
        img_path = os.path.join(inner_dir, row[1].Filename)
        img = imread(img_path)
        img = img[row[1]['Roi.X1']:row[1]['Roi.X2'],row[1]['Roi.Y1']:row[1]['Roi.Y2'],:]
        img = resize_cv(img)
        list_images.append(img)
        output.append(row[1].ClassId)

data_dir - это путь к каталогу, в котором находится набор данных.

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

input_array = np.stack(list_images)
import keras
train_y = keras.utils.np_utils.to_categorical(output)
randomize = np.arange(len(input_array))
np.random.shuffle(randomize)
x = input_array[randomize]
y = train_y[randomize]

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

Теперь разделим набор данных. Мы разделим набор данных в соотношении 60:20:20 на набор данных для обучения, проверки и тестирования соответственно.

split_size = int(x.shape[0]*0.6)
train_x, val_x = x[:split_size], x[split_size:]
train1_y, val_y = y[:split_size], y[split_size:]
split_size = int(val_x.shape[0]*0.5)
val_x, test_x = val_x[:split_size], val_x[split_size:]
val_y, test_y = val_y[:split_size], val_y[split_size:]

Обучение модели

from keras.layers import Dense, Dropout, Flatten, Input
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import BatchNormalization
from keras.optimizers import Adam
from keras.models import Sequential
hidden_num_units = 2048
hidden_num_units1 = 1024
hidden_num_units2 = 128
output_num_units = 43
epochs = 10
batch_size = 16
pool_size = (2, 2)
#list_images /= 255.0
input_shape = Input(shape=(32, 32,3))
model = Sequential([
Conv2D(16, (3, 3), activation='relu', input_shape=(64,64,3), padding='same'),
 BatchNormalization(),
Conv2D(16, (3, 3), activation='relu', padding='same'),
 BatchNormalization(),
 MaxPooling2D(pool_size=pool_size),
 Dropout(0.2),
    
 Conv2D(32, (3, 3), activation='relu', padding='same'),
 BatchNormalization(),
    
 Conv2D(32, (3, 3), activation='relu', padding='same'),
 BatchNormalization(),
 MaxPooling2D(pool_size=pool_size),
 Dropout(0.2),
    
 Conv2D(64, (3, 3), activation='relu', padding='same'),
 BatchNormalization(),
    
 Conv2D(64, (3, 3), activation='relu', padding='same'),
 BatchNormalization(),
 MaxPooling2D(pool_size=pool_size),
 Dropout(0.2),
Flatten(),
Dense(units=hidden_num_units, activation='relu'),
 Dropout(0.3),
 Dense(units=hidden_num_units1, activation='relu'),
 Dropout(0.3),
 Dense(units=hidden_num_units2, activation='relu'),
 Dropout(0.3),
 Dense(units=output_num_units, input_dim=hidden_num_units, activation='softmax'),
])
model.compile(loss='categorical_crossentropy', optimizer=Adam(lr=1e-4), metrics=['accuracy'])
trained_model_conv = model.fit(train_x.reshape(-1,64,64,3), train1_y, epochs=epochs, batch_size=batch_size, validation_data=(val_x, val_y))

Мы использовали keras пакет.

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

Оценка модели

model.evaluate(test_x, test_y)

Модель оценивается, и вы можете определить точность 99%.

Прогнозирование результата

pred = model.predict_classes(test_x)

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

Вы можете найти весь рабочий код здесь.