ВЪВЕДЕНИЕ
В тази статия представям проста реализация на рамката PyTorch за проблема с класификацията на изображения. В този проект се използва наборът от данни за изображения на Intel. Написах кратка статия, за да представя и да работя върху стъпките за предварителна обработка на изображения на този набор от данни тук. Тази статия е продължение за разширяване на изграждането на модела, обучението и стъпките за извод, като кодовете се преработват в подходяща структура на хранилище. Можете също да намерите хранилището на GitHub на този проект по-долу.
Имайте предвид, че може да актуализирам хранилището, така че съдържанието на тази статия може да не е 100% същото като кодовете на хранилището, но общият подход трябва да е същият.
Можете да разгледате част 2, където подобрявам ефективността на тази статия с трансфер на обучение.
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. Конфигуриране на устройствата за зареждане на данни
Следващата важна стъпка е да конфигурирате DataLoaders от наборите от данни по-горе. Основната цел е да се осигури възможност за повторение върху тези набори от данни, според размера на партидата. Също така е необходимо да се разбъркват изображения в тези набори от данни, особено набора за обучение, така че процесът на обучение да може да се извършва с различни класове изображения всеки път и подобрява способността за обобщаване на модела. Използването на 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 слой, както се очаква). Колоната Output Shape помага за разбирането на изходните размери на слоя.
# 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 за класифициране на изображения по по-организиран, базиран на проекти начин. Като цяло представянето не е добро, но не и твърде ужасно. Има начини да го подобрим, най-практично чрез трансферно обучение, което ще приложа в следващата статия.
Благодаря ви, че ме прочетохте и приветствам всякакви отзиви и съвети.