ВВЕДЕНИЕ
В этой статье я представляю простую реализацию платформы PyTorch для решения задачи классификации изображений. В этом проекте используется набор данных изображений Intel. Я написал короткую статью, чтобы представить и поработать над этапами предварительной обработки изображений этого набора данных здесь. Эта статья является продолжением, посвященным этапам построения модели, обучения и логического вывода, с рефакторингом кода в правильную структуру репозитория. Вы также можете найти репозиторий GitHub этого проекта ниже.
Обратите внимание, что я могу обновить репозиторий, поэтому содержание этой статьи может не совпадать на 100 % с кодами репозитория, но общий подход должен быть таким же.
Вы можете ознакомиться со второй частью, где я улучшаю производительность этой статьи с помощью трансферного обучения.
II. ПАПКА ПРОЕКТА ОРГАНИЗАЦИЯ
Мы будем следить за организацией проекта ниже:
data
— это место, куда мы поместили, как вы уже догадались, наши данные. Необработанные данные обучения, тестирования и прогнозирования находятся в подпапке01_raw
. Другие выходные данные наших кодов, такие как файлы промежуточных аннотаций csv, обученная модель и отчеты о производительности модели, также хранятся в папкеdata
.notebooks
— это место, где мы храним нашу экспериментальную записную книжку Jupyter. Цель состоит в том, чтобы сделать этот блокнот как можно более чистым: никаких пользовательских функций/классов… Он предназначен для импорта всех необходимых библиотек и пользовательских модулей и выполнения экспериментальных шагов максимально простым способом.src
— это место, где мы определяем все наши модули.config
хранит необходимые константы для простоты повторного использования и будущих изменений в одном месте.model
хранит наш класс модели CNN и класс набора данных,preprocessing
иpostprocessing
хранитutils
файлы со вспомогательными функциями для подготовки данных для обучения, а также для получения результатов производительности и экспорта обученной модели.- И последнее, но не менее важное: виртуальная среда (созданная с помощью Python 3.9, отсюда и название) обычно размещается снаружи в корневой папке (эта папка не синхронизируется с GitHub, если вам нужна помощь в создании собственного venv, см. мою другую статью ниже).
+---data | +---01_raw | +---02_intermediate | +---03_model_input | +---04_model | +---05_model_output | \---06_reporting +---notebooks +---src | +---config | +---model | +---postprocessing | \---preprocessing \---venv39
III. ДЕТАЛИ СПЕЦИАЛЬНЫХ МОДУЛЕЙ
Теперь давайте кратко рассмотрим пользовательские модули.
1. Модуль конфигурации
Прежде всего, содержимое файла конфигурации указано ниже. Первый файл data_config.py
определяет входную форму изображений, которые позже будут загружены в модель CNN, вместе с параметрами обучения: размер пакета и количество рабочих. Любые изменения этих констант могут быть внесены в этот файл и повторно импортированы в другие модули, что упрощает процесс обслуживания. Второй файл loc_config.py
указывает другие папки для нашего эксперимента позже.
INPUT_WIDTH = 224 INPUT_HEIGHT = 224 INPUT_CHANNEL = 3 BATCH_SIZE = 32 NUM_WORKERS = 2 TRAIN_DATA_LOC = r'..\data\01_raw\seg_train' TEST_DATA_LOC = r'..\data\01_raw\seg_test' PRED_DATA_LOC = r'..\data\01_raw\seg_pred' ANNOT_LOC = r'..\data\02_intermediate' MODEL_SAVE_LOC = r'..\data\04_model' REPORT_SAVE_LOC = r'..\data\06_reporting'
2. Модуль предварительной обработки
Во-вторых, модуль preprocessing
помогает нам подготовить набор данных изображения. build_annotation_csv
начинается с записи меток столбцов в пустой CSV-файл, затем проходит через каждую папку в указанном каталоге (например, папку для обучения) и извлекает имена подпапок в качестве имен классов. Затем полные пути ко всем изображениям вместе с соответствующими именами классов (и индексами) записываются в CSV-файл построчно. Он также возвращает CSV в качестве аннотации для последующего использования в классе data.Dataset
и сохраняет этот файл в классе annot_location
(позже мы будем использовать константу ANNOT_LOC
в блокноте для экспериментов).
Функция transform_bilinear
нормализует входное изображение, применяя стандартный набор шагов обработки изображения. Вы можете настроить его по мере необходимости.
def build_annotation_dataframe(image_location, annot_location, output_csv_name): class_lst = os.listdir( image_location) # returns a LIST containing the names of the entries (folder names in this case) in the directory. class_lst.sort() # IMPORTANT with open(os.path.join(annot_location, output_csv_name), 'w', newline='') as csvfile: writer = csv.writer(csvfile, delimiter=',') writer.writerow(['file_name', 'file_path', 'class_name', 'class_index']) # create column names for class_name in class_lst: # concatenates various path components with exactly one directory separator (‘/’) except the last path component. class_path = os.path.join(image_location, class_name) # get list of files in class folder file_list = os.listdir(class_path) for file_name in file_list: # concatenate class folder dir, class name and file name file_path = os.path.join(image_location, class_name, file_name) # write the file path and class name to the csv file writer.writerow( [file_name, file_path, class_name, class_lst.index(class_name)]) return pd.read_csv(os.path.join(annot_location, output_csv_name)) def check_annot_dataframe(annot_df): class_zip = zip(annot_df['class_index'], annot_df['class_name']) my_list = list() for index, name in class_zip: my_list.append(tuple((index, name))) unique_list = list(set(my_list)) return unique_list def transform_bilinear(output_img_width, output_img_height): image_transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]), transforms.Resize((output_img_width, output_img_height), interpolation=PIL.Image.BILINEAR) ]) return image_transform
3. Модельный модуль
Далее давайте посмотрим на модуль model
, содержащий файлы dataset.py
, modelling_config.py
и cnn_model.py
.
dataset.py
— это место, где мы определяем пользовательский класс набора данных с именем IntelDataset
, который наследуется от класса набора данных PyTorch, как описано в моей предыдущей статье. Я добавил еще один метод visualize
для вывода 10 случайных изображений набора данных для иллюстрации. Наконец, функция create_validation_dataset
принимает объект набора данных и разделяет его на основной набор данных (для использования в качестве обучающего набора) и набор данных проверки с заданной пропорцией. Это нужно для того, чтобы мы могли позже увидеть, как потери при проверке поезда прогрессируют в течение тренировочных эпох, и определить точку остановки, чтобы предотвратить переобучение.
class IntelDataset(torch.utils.data.Dataset): def __init__(self, annot_df, transform=None): self.annot_df = annot_df # root directory of images, leave "" if using the image path column in the __getitem__ method self.root_dir = "" self.transform = transform def __len__(self): # return length (numer of rows) of the dataframe return len(self.annot_df) def __getitem__(self, idx): # use image path column (index = 1) in csv file image_path = self.annot_df.iloc[idx, 1] image = cv2.imread(image_path) # read image by cv2 # convert from BGR to RGB for matplotlib image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # use class name column (index = 2) in csv file class_name = self.annot_df.iloc[idx, 2] # use class index column (index = 3) in csv file class_index = self.annot_df.iloc[idx, 3] if self.transform: image = self.transform(image) # when accessing an instance via index, 3 outputs are returned - the image, class name and class index return image, class_name, class_index def visualize(self, number_of_img=10, output_width=12, output_height=6): plt.figure(figsize=(output_width, output_height)) for i in range(number_of_img): idx = random.randint(0, len(self.annot_df)) image, class_name, class_index = self.__getitem__(idx) ax = plt.subplot(2, 5, i+1) # create an axis # create a name of the axis based on the img name ax.title.set_text(class_name + '-' + str(class_index)) if self.transform == None: plt.imshow(image) else: plt.imshow(image.permute(1, 2, 0)) def create_validation_dataset(dataset, validation_proportion): if (validation_proportion > 1) or (validation_proportion < 0): return "The proportion of the validation set must be between 0 and 1" else: dataset_size = int((1 - validation_proportion) * len(dataset)) validation_size = len(dataset) - dataset_size print(dataset_size, validation_size) dataset, validation_set = torch.utils.data.random_split( dataset, [dataset_size, validation_size]) return dataset, validation_set
Второй файл modelling_config.py
используется для определения вспомогательных функций для процесса моделирования. Первые 3 функции говорят сами за себя. Следующая функция model_prep_and_summary
перемещает модель на устройство по умолчанию (GPU или CPU) и выводит сводку модели с помощью модуля torchsummary.summary
. Это для нас, чтобы проверить входные/выходные размеры различных слоев в модели.
def default_loss(): return nn.CrossEntropyLoss() def default_optimizer(model, learning_rate = 0.001): return optim.Adam(model.parameters(), lr = learning_rate) def get_default_device(): """Picking GPU if available or else CPU""" if torch.cuda.is_available(): return torch.device('cuda') else: return torch.device('cpu') def model_prep_and_summary(model, device): """ Move model to GPU and print model summary """ # Define the model and move it to GPU: model = model model = model.to(device) print('Current device: ' + str(device)) print('Is Model on CUDA: ' + str(next(model.parameters()).is_cuda)) # Display model summary: summary(model, (INPUT_CHANNEL, INPUT_WIDTH, INPUT_HEIGHT))
Далее внутри файла cnn_model.py
мы определяем модель CNN, собирая слои с классом nn.Module
. Несколько замечаний:
- Выходной размер сверточных слоев должен быть рассчитан для использования в качестве входных данных для первого слоя FC. Поскольку форма вывода self.conv7 равна 64*56*56, слой MaxPool просто уменьшает W и H наполовину, не влияя на количество каналов (глубину). Следовательно, вход на уровень FC равен 64*28*28 с
self.fc14 = nn.Linear(64*28*28, 500)
. Я приложил электронную таблицу, чтобы помочь с этим расчетом в репозитории, и некоторые расчеты в комментариях к коду. - Выходное измерение последнего слоя FC (
fc16
) должно быть равно количеству классов, которые вы хотите классифицировать в наборе данных. Здесь у нас 6 классов.
class MyCnnModel(nn.Module): def __init__(self): super(MyCnnModel, self).__init__() self.conv1 = nn.Conv2d( in_channels=3, out_channels=16, kernel_size=3, padding=1) self.conv2 = nn.Conv2d( in_channels=16, out_channels=16, kernel_size=3, padding=1) self.conv3 = nn.Conv2d( in_channels=16, out_channels=32, kernel_size=3, padding=1) self.conv4 = nn.Conv2d( in_channels=32, out_channels=32, kernel_size=3, padding=1) self.conv5 = nn.Conv2d( in_channels=32, out_channels=64, kernel_size=3, padding=1) self.conv6 = nn.Conv2d( in_channels=64, out_channels=64, kernel_size=3, padding=1) self.conv7 = nn.Conv2d( in_channels=64, out_channels=64, kernel_size=3, padding=1) # Define a max pooling layer to use repeatedly in the forward function # The role of pooling layer is to reduce the spatial dimension (H, W) of the input volume for next layers. # It only affects weight and height but not depth. self.maxpool = nn.MaxPool2d(kernel_size=2, stride=2) # output shape of maxpool3 is 64*28*28 self.fc14 = nn.Linear(64*28*28, 500) self.fc15 = nn.Linear(500, 50) # output of the final DC layer = 6 = number of classes self.fc16 = nn.Linear(50, 6) def forward(self, x): x = F.relu(self.conv1(x)) x = F.relu(self.conv2(x)) # maxpool1 output shape is 16*112*112 (112 = (224-2)/2 + 1) x = self.maxpool(x) x = F.relu(self.conv3(x)) x = F.relu(self.conv4(x)) # maxpool2 output shape is 32*56*56 (56 = (112-2)/2 + 1) x = self.maxpool(x) x = F.relu(self.conv5(x)) x = F.relu(self.conv6(x)) x = F.relu(self.conv7(x)) # maxpool3 output shape is 64*28*28 (28 = (56-2)/2 + 1) x = self.maxpool(x) x = x.reshape(x.shape[0], -1) x = F.relu(self.fc14(x)) x = F.relu(self.fc15(x)) x = F.dropout(x, 0.5) x = self.fc16(x) return x
Следующая важная функция — train_model()
. При количестве эпох по умолчанию = 5 для каждой партии в каждой эпохе он устанавливает модель в режим train
для прямого/обратного распространения/расчета потерь/обновления параметров с использованием набора обучающих данных, а затем устанавливает модель в режим eval
, чтобы зафиксировать параметры и вычисление потерь при тестировании на наборе данных проверки. Функция вернет саму обученную модель вместе с кадром данных истории потерь в наборе данных для обучения и проверки, который мы можем исследовать. Для этого я определяю функцию visualize_training
для отображения истории убытков.
def train_model(model, device, train_loader, val_loader, criterion, optimizer, num_epochs=5): # device = get_default_device() model = model.to(device) train_result_dict = {'epoch': [], 'train_loss': [], 'val_loss': [], 'accuracy': [], 'time': []} for epoch in range(num_epochs): start_time = time.time() train_loss = 0.0 correct = 0 total = 0 model.train() # set the model to training mode, parameters are updated for i, data in enumerate(train_loader, 0): image, class_name, class_index = data image = image.to(device) class_index = class_index.to(device) optimizer.zero_grad() # zero the parameter gradients outputs = model(image) # forward propagation loss = criterion(outputs, class_index) # loss calculation loss.backward() # backward propagation optimizer.step() # params update train_loss += loss.item() # loss for each minibatch _, predicted = torch.max(outputs.data, 1) total += class_index.size(0) correct += (predicted == class_index).sum().item() epoch_accuracy = round(float(correct)/float(total)*100, 2) # Here evaluation is combined together with val_loss = 0.0 model.eval() # set the model to evaluation mode, parameters are frozen for i, data in enumerate(val_loader, 0): image, class_name, class_index = data image = image.to(device) class_index = class_index.to(device) outputs = model(image) loss = criterion(outputs, class_index) val_loss += loss.item() # print statistics every 1 epoch # divide by the length of the minibatch because loss.item() returns the loss of the whole minibatch train_loss_result = round(train_loss / len(train_loader), 3) val_loss_result = round(val_loss / len(val_loader), 3) epoch_time = round(time.time() - start_time, 1) # add statistics to the dictionary: train_result_dict['epoch'].append(epoch + 1) train_result_dict['train_loss'].append(train_loss_result) train_result_dict['val_loss'].append(val_loss_result) train_result_dict['accuracy'].append(epoch_accuracy) train_result_dict['time'].append(epoch_time) print(f'Epoch {epoch+1} \t Training Loss: {train_loss_result} \t Validation Loss: {val_loss_result} \t Epoch Train Accuracy (%): {epoch_accuracy} \t Epoch Time (s): {epoch_time}') # return the trained model and the loss dictionary return model, train_result_dict def visualize_training(train_result_dictionary): # Define Data df = pd.DataFrame(train_result_dictionary) x = df['epoch'] data_1 = df['train_loss'] data_2 = df['val_loss'] data_3 = df['accuracy'] # Create Plot fig, ax1 = plt.subplots(figsize=(7, 7)) ax1.set_xlabel('Epoch') ax1.set_ylabel('Loss') ax1.plot(x, data_1, color='red', label='training loss') ax1.plot(x, data_2, color='blue', label='validation loss') # Adding Twin Axes ax2 = ax1.twinx() ax2.plot(x, data_3, color='green', label='Training Accuracy') # Add label plt.ylabel('Accuracy') lines = ax1.get_lines() + ax2.get_lines() ax1.legend(lines, [line.get_label() for line in lines], loc='upper center') # Show plot plt.show()
Наконец, для этого длинного файла у нас есть коды логического вывода. Первая функция infer()
работает с классом DataLoader и выполняет вывод для всего набора данных, а вторая функция infer_single_image
работает с одним изображением. Обратите внимание, что мы должны выполнить ту же предварительную обработку изображения, что и предыдущий метод IntelDataset
class __getitem()__
.
def infer(model, device, data_loader): ''' Calculate predicted class indices of the data_loader by the trained model ''' model = model.to(device) y_pred = [] y_true = [] with torch.no_grad(): for data in data_loader: image, class_name, class_index = data image = image.to(device) class_index = class_index.to(device) outputs = model(image) outputs = (torch.max(torch.exp(outputs), 1)[1]).data.cpu().numpy() y_pred.extend(outputs) class_index = class_index.data.cpu().numpy() y_true.extend(class_index) return y_pred, y_true def infer_single_image(model, device, image_path, transform): ''' Calculate predicted class index of the image by the trained model ''' # Prepare the Image image = cv2.imread(image_path) # read image by cv2 image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image_transformed = transform(image) plt.imshow(image_transformed.permute(1, 2, 0)) image_transformed_sq = torch.unsqueeze(image_transformed, dim=0) # Inference model.eval() with torch.no_grad(): image_transformed_sq = image_transformed_sq.to(device) output = model(image_transformed_sq) _, predicted_class_index = torch.max(output.data, 1) print(f'Predicted Class Index: {predicted_class_index}') return predicted_class_index
4. Модуль постобработки
Последний модуль содержит вспомогательные функции в файле utils.py
, помогающие нам с выходными данными модели.
Первая функция принимает выходные данные вышеприведенной функции infer()
вместе со списком имен классов и выполняет расчет метрик для точности, прецизионности, полноты и F1 для каждого из 6 классов. Он также выводит общую точность и баллы F1.
Обратите внимание, что:
- Использование
class_names
предназначено только для косметических целей на этапе представления метрик. - Мы используем инструменты библиотеки
torch
для вычисления различных показателей, таких как точность и F1, с выходными данными в тензорах. Следовательно, чтобы представить их в кадре данных, нам нужно преобразовать их в обычный формат numpy.
def calculate_model_performance(y_true, y_pred, class_names): num_classes = len(set(y_true + y_pred)) # build confusion matrix based on predictions and class_index confusion_matrix = torch.zeros(num_classes, num_classes) for i in range(len(y_pred)): # true label on row, predicted on column confusion_matrix[y_true[i], y_pred[i]] += 1 # PER-CLASS METRICS: # calculate accuracy, precision, recall, f1 for each class: accuracy = torch.zeros(num_classes) precision = torch.zeros(num_classes) recall = torch.zeros(num_classes) f1_score = torch.zeros(num_classes) for i in range(num_classes): # find TP, FP, FN, TN for each class: TP = confusion_matrix[i, i] FP = torch.sum(confusion_matrix[i, :]) - TP FN = torch.sum(confusion_matrix[:, i]) - TP TN = torch.sum(confusion_matrix) - TP - FP - FN # calculate accuracy, precision, recall, f1 for each class: accuracy[i] = (TP+TN)/(TP+FP+FN+TN) precision[i] = TP/(TP+FP) recall[i] = TP/(TP+FN) f1_score[i] = 2*precision[i]*recall[i]/(precision[i]+recall[i]) # calculate support for each class support = torch.sum(confusion_matrix, dim=0) # calculate support proportion for each class support_prop = support/torch.sum(support) # OVERALL METRICS # calculate overall accuracy: overall_acc = torch.sum(torch.diag(confusion_matrix) )/torch.sum(confusion_matrix) # calculate macro average F1 score: macro_avg_f1_score = torch.sum(f1_score)/num_classes # calculate weighted average rF1 score based on support proportion: weighted_avg_f1_score = torch.sum(f1_score*support_prop) TP = torch.diag(confusion_matrix) FP = torch.sum(confusion_matrix, dim=1) - TP FN = torch.sum(confusion_matrix, dim=0) - TP TN = torch.sum(confusion_matrix) - (TP + FP + FN) # calculate micro average f1 score based on TP, FP, FN micro_avg_f1_score = torch.sum( 2*TP)/(torch.sum(2*TP)+torch.sum(FP)+torch.sum(FN)) # METRICS PRESENTATION # performance for each class class_columns = ['accuracy', 'precision', 'recall', 'f1_score'] class_data_raw = [accuracy.numpy(), precision.numpy(), recall.numpy(), f1_score.numpy()] class_data = np.around(class_data_raw, decimals=3) df_class_raw = pd.DataFrame( class_data, index=class_columns, columns=class_names) class_metrics = df_class_raw.T # overall performance overall_columns = ['accuracy', 'f1_mirco', 'f1_macro', 'f1_weighted'] overall_data_raw = [overall_acc.numpy(), micro_avg_f1_score.numpy( ), macro_avg_f1_score.numpy(), weighted_avg_f1_score.numpy()] overall_data = np.around(overall_data_raw, decimals=3) overall_metrics = pd.DataFrame( overall_data, index=overall_columns, columns=['overall']) return confusion_matrix, class_metrics, overall_metrics
Последний набор функций просто помогает нам сохранить обученную модель и отчет об обучении с текущей отметкой времени (чтобы избежать перезаписи старых файлов).
def get_current_timestamp(): now = datetime.datetime.now() return now.strftime("%Y%m%d_%H%M%S") def save_model_with_timestamp(model, filepath = MODEL_SAVE_LOC): filename = get_current_timestamp() + '_cnn_model' + '.pt' filepath = os.path.join(filepath, filename) torch.save(model.state_dict(), filepath) return print('Saved model to: ', filepath) def save_csv_with_timestamp(train_result_dict, filepath = MODEL_SAVE_LOC): filename = get_current_timestamp() + '_training_report' + '.csv' filepath = os.path.join(filepath, filename) df = pd.DataFrame(train_result_dict) df.to_csv(filepath) return print('Saved training report to: ', filepath)
IV. ОБУЧЕНИЕ МОДЕЛИ
После определения всех необходимых модулей наш экспериментальный блокнот может быть очень простым:
1. Создание и предварительная обработка входного набора данных
Во-первых, мы создаем кадры данных аннотации и печатаем имя класса — пары индексов классов для проверки. Мы видим, что пары согласуются как в обучающем, так и в тестовом наборе данных.
train_df = build_annotation_dataframe(image_location = TRAIN_DATA_LOC, annot_location = ANNOT_LOC, output_csv_name = 'train.csv') test_df = build_annotation_dataframe(image_location = TEST_DATA_LOC, annot_location = ANNOT_LOC, output_csv_name = 'test.csv') class_names = list(train_df['class_name'].unique()) print(class_names) print(check_annot_dataframe(train_df)) print(check_annot_dataframe(test_df)) Output: ['buildings', 'forest', 'glacier', 'mountain', 'sea', 'street'] [(5, 'street'), (1, 'forest'), (4, 'sea'), (0, 'buildings'), (2, 'glacier'), (3, 'mountain')] [(5, 'street'), (1, 'forest'), (4, 'sea'), (0, 'buildings'), (2, 'glacier'), (3, 'mountain')]
Во-вторых, мы вызываем функцию преобразования изображения и применяем ее к обучающим, проверочным (соотношение 4:1 с validation_proportion = 0.2
) и тестовым наборам данных, которые создаются пользовательским классом IntelDataset
. Мы также распечатываем размер этих наборов данных для проверки.
image_transform = transform_bilinear(INPUT_WIDTH, INPUT_HEIGHT) main_dataset = IntelDataset(annot_df = train_df, transform=image_transform) train_dataset, validation_dataset = create_validation_dataset(main_dataset, validation_proportion = 0.2) print('Train set size: ', len(train_dataset)) print('Validation set size: ', len(validation_dataset)) test_dataset = IntelDataset(annot_df = test_df, transform=image_transform) print('Test set size: ', len(test_dataset)) Output: Train set size: 11227 Validation set size: 2807 Test set size: 3000
2. Настройка загрузчиков данных
Следующим важным шагом является настройка загрузчиков данных из наборов данных выше. Основная цель состоит в том, чтобы обеспечить итерацию по этому набору данных в соответствии с размером пакета. Также необходимо перетасовывать изображения в этих наборах данных, особенно в обучающем наборе, чтобы процесс обучения можно было каждый раз выполнять с разными классами изображений и улучшать способность обобщения модели. Использование num_workers
может быть хорошо объяснено в статье, указанной ниже, но это не является целью этого проекта, и вы можете оптимизировать его дальше.
train_loader = DataLoader(train_dataset, batch_size = BATCH_SIZE, shuffle=True, num_workers = NUM_WORKERS) val_loader = DataLoader(validation_dataset, batch_size = BATCH_SIZE, shuffle=True, num_workers = NUM_WORKERS) test_loader = DataLoader(test_dataset, batch_size = BATCH_SIZE, shuffle=True, num_workers = NUM_WORKERS)
3. Обучение модели и экспорт
Наконец мы подошли к тренировочной части. Мы просто инициализируем модель с архитектурой, определенной ранее, используем функцию потерь и оптимизатор по умолчанию и устанавливаем количество эпох (10 в этом испытании). Мы видим, что имеем дело с довольно крупной моделью с более чем 25 миллионами обучаемых параметров (как и ожидалось, в основном из первого слоя FC). Столбец «Форма вывода» помогает понять размеры вывода слоя.
# initiation model = cnn_model.MyCnnModel() device = modelling_config.get_default_device() modelling_config.model_prep_and_summary(model, device) criterion = modelling_config.default_loss() optimizer = modelling_config.default_optimizer(model = model) num_epochs = 10 # get training results trained_model, train_result_dict = cnn_model.train_model(model, device, train_loader, val_loader, criterion, optimizer, num_epochs) cnn_model.visualize_training(train_result_dict) Current device: cpu Is Model on CUDA: False ---------------------------------------------------------------- Layer (type) Output Shape Param # ================================================================ Conv2d-1 [-1, 16, 224, 224] 448 Conv2d-2 [-1, 16, 224, 224] 2,320 MaxPool2d-3 [-1, 16, 112, 112] 0 Conv2d-4 [-1, 32, 112, 112] 4,640 Conv2d-5 [-1, 32, 112, 112] 9,248 MaxPool2d-6 [-1, 32, 56, 56] 0 Conv2d-7 [-1, 64, 56, 56] 18,496 Conv2d-8 [-1, 64, 56, 56] 36,928 Conv2d-9 [-1, 64, 56, 56] 36,928 MaxPool2d-10 [-1, 64, 28, 28] 0 Linear-11 [-1, 500] 25,088,500 Linear-12 [-1, 50] 25,050 Linear-13 [-1, 6] 306 ================================================================ Total params: 25,222,864 Trainable params: 25,222,864 Non-trainable params: 0 ---------------------------------------------------------------- Input size (MB): 0.57 Forward/backward pass size (MB): 25.65 Params size (MB): 96.22 Estimated Total Size (MB): 122.44 ----------------------------------------------------------------
История тренировок построена следующим образом. Здесь мы можем видеть прогрессирование потерь классификации в наборах для обучения и проверки. точка переобучения возникает в эпоху 4, после чего потери при проверке начинают расти по мере уменьшения потерь при обучении. Следовательно, эпоха 5 может быть хорошей точкой для остановки обучения в нашем следующем испытании. Наконец, мы сохраняем модели, обученные на 10 и 5 эпохах, в определенное место.
save_model_with_timestamp(trained_model, MODEL_SAVE_LOC)
V. ХАРАКТЕРИСТИКИ МОДЕЛИ
Наконец, мы перейдем к импорту и проверке производительности двух моделей на тестовом наборе. Причина экспорта/импорта вместо использования непосредственно из обученных моделей в той же записной книжке является просто личным предпочтением, поскольку я могу просто запустить эту часть записной книжки, импортировать предварительно обученные модели и продолжить работу при перезапуске своей работы без переобучения, что занять очень слишком много времени на моем бедном ноутбуке.
Здесь модель, обученная 10 эпохам, является первой моделью в сохраненном месте. Мы используем встроенный метод .load_state_dict
класса модели для загрузки схемы модели с предварительно обученными параметрами. Наконец, мы вызываем пользовательский метод infer()
, описанный ранее в тестовом DataLoader, и используем функцию calculate_model_performance
для просмотра метрик.
Общая точность составляет 68,4%. Мы можем увидеть больше деталей из метрики путаницы, где диагональные значения представляют правильные прогнозы. Например, ледники (строка 3) чаще всего путают с горами (строка 4), где 136 изображений ледников классифицируются как горы.
trained_model_list = os.listdir(MODEL_SAVE_LOC) MODELutils
EPOCH_PATH = os.path.join(MODEL_SAVE_LOC, trained_model_list[0]) MODELutils
EPOCH = cnn_model.MyCnnModel() device = modelling_config.get_default_device() print(MODELutils
EPOCH_PATH) MODELutils
EPOCH.load_state_dict(torch.load(MODELutils
EPOCH_PATH)) # check accuracy on test set y_pred, y_true = cnn_model.infer(model = MODELutils
EPOCH, device = device, data_loader = test_loader) confusion_matrix, class_metrics, overall_metrics = calculate_model_performance(y_pred, y_true, class_names = class_names) print(confusion_matrix) print(class_metrics) print(overall_metrics) Output: tensor([[325., 15., 37., 47., 46., 76.], [ 14., 425., 4., 3., 7., 29.], [ 5., 0., 270., 51., 51., 7.], [ 2., 5., 136., 366., 116., 1.], [ 14., 0., 89., 55., 283., 4.], [ 77., 29., 17., 3., 7., 384.]]) accuracy precision recall f1_score buildings 0.889 0.595 0.744 0.661 forest 0.965 0.882 0.897 0.889 glacier 0.868 0.703 0.488 0.576 mountain 0.860 0.585 0.697 0.636 sea 0.870 0.636 0.555 0.593 street 0.917 0.743 0.766 0.754 overall accuracy 0.684 f1_mirco 0.684 f1_macro 0.685 f1_weighted 0.681
Давайте посмотрим на модель, обученную с 5 эпохами. Мы видим, что точность немного выше, хотя время обучения сокращается вдвое. Это ожидается, поскольку модель демонстрирует переоснащение после 4-й эпохи, поэтому ее способность к обобщению фактически ухудшается с увеличением количества эпох и будет хуже работать на тестовом наборе.
tensor([[309., 23., 16., 21., 12., 49.], [ 5., 383., 0., 3., 1., 9.], [ 8., 0., 373., 67., 63., 7.], [ 6., 3., 24., 233., 34., 0.], [ 23., 2., 130., 200., 392., 12.], [ 86., 63., 10., 1., 8., 424.]]) accuracy precision recall f1_score buildings 0.917 0.719 0.707 0.713 forest 0.964 0.955 0.808 0.875 glacier 0.892 0.720 0.675 0.697 mountain 0.880 0.777 0.444 0.565 sea 0.838 0.516 0.769 0.618 street 0.918 0.716 0.846 0.776 overall accuracy 0.705 f1_mirco 0.705 f1_macro 0.707 f1_weighted 0.704
VI. ТЕСТИРОВАНИЕ НА ОДНОМ ИЗОБРАЖЕНИИ
Наконец, давайте воспользуемся моделью с 5 эпохами, чтобы вывести одно изображение. Мы просто вызываем метод infer_single_image()
класса модели и вводим соответствующие аргументы. В приведенном ниже коде из папки PRED_DATA_LOC
выбирается случайное изображение.
image_list = os.listdir(PRED_DATA_LOC)
random_image = random.choice(image_list)
random_image_path = os.path.join(PRED_DATA_LOC, random_image)
print(random_image_path)
predicted_class_index = cnn_model.infer_single_image(
model=MODELsrc
EPOCH,
device=device,
image_path=random_image_path,
transform=image_transform)
print(class_names[predicted_class_index])
Output:
..\data\01_raw\seg_pred\11904.jpg
Predicted Class Index: tensor([0])
buildings
Выбранное изображение (11904.jpg) показано ниже, и он соответствует классу buildings
, что верно.
V. ЗАКЛЮЧЕНИЕ
Таким образом, эта статья — моя попытка реализовать PyTorch для классификации изображений более организованным способом, основанным на проектах. В целом производительность не очень хорошая, но и не ужасная. Есть способы улучшить его, наиболее практически через трансферное обучение, которое я реализую в следующей статье.
Спасибо за чтение, и я приветствую любые отзывы и советы.