ВЪВЕДЕНИЕ

В тази статия представям проста реализация на рамката 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)
MODELutilsEPOCH_PATH = os.path.join(MODEL_SAVE_LOC, trained_model_list[0])
MODELutilsEPOCH = cnn_model.MyCnnModel()
device = modelling_config.get_default_device()
print(MODELutilsEPOCH_PATH)
MODELutilsEPOCH.load_state_dict(torch.load(MODELutilsEPOCH_PATH))

# check accuracy on test set
y_pred, y_true = cnn_model.infer(model = MODELutilsEPOCH, 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=MODELsrcEPOCH, 
    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 за класифициране на изображения по по-организиран, базиран на проекти начин. Като цяло представянето не е добро, но не и твърде ужасно. Има начини да го подобрим, най-практично чрез трансферно обучение, което ще приложа в следващата статия.

Благодаря ви, че ме прочетохте и приветствам всякакви отзиви и съвети.