Подписывайтесь на меня"…

Pytorch — это фреймворк для машинного обучения с открытым исходным кодом, созданный в рамках стажировки Адама Пашке, который в то время был студентом Soumith Chintala (один из разработчиков Torch и работает исследователем в Meta — ранее Facebook).

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

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

Как и сегодня, в 2023 году, когда революция OpenAI-GPT пожирает цифровой рынок, они оба играют огромную роль в разработке таких моделей, как: DALL-E2, Stable Diffusion и ChatGPT.

Следующая история о том, как начать работу с PyTorch, используя одну из вещей, которую мне было труднее переварить с точки зрения кода; Feature Engineering / Transformations, и я возьму свою модель и буду использовать различные облачные инструменты для запуска своего кода и делать с его помощью некоторые прогнозы. Поскольку это первый шаг к пониманию алгоритма PyTorch, я буду использовать табличные данные с теми же старыми данными, которые я использовал для других своих средних историй, что даст вам лучшее представление о том, как это работает, а затем я начну публиковать на большом языке. Такие модели, как chatGPT и T5.

Ключевая особенность

Вот некоторые из вещей, которые будут использоваться во время этого эксперимента:

  1. Панды
  2. ПиТорч
  3. Scikit-learn
  4. Vertex AI: Custom Training Job, Custom Prediction Routines и Endpoints (онлайн-прогнозирование).

Я создам файл Python с обучающим кодом PyTorch и буду использовать Vertex AI для обучения, поэтому загружу модель в реестр, чтобы отслеживать версии и использовать ее в онлайн-прогнозировании.

Для прогнозирования, поскольку мы должны сохранить тот же процесс преобразования, который мы использовали на этапе обучения, чтобы избежать перекоса между обучением и обслуживанием (качество работы модели), мы должны создать контейнер с нуля. Для этого существует модульный механизм под названием CPR (Custom Predictions Routines), вы можете сделать это самостоятельно, но мне нравится, что CPR помогает мне просто написать ту часть, которая мне нужна, и я могу протестировать ее локально.

Итак, настроимся,

Переменные

Установите переменные с вашими собственными значениями:

PROJECT_ID = 'jchavezar-demo'
REGION = 'us-central1'
DATASET_URI = 'gs://vtx-datasets-public/ecommerce/datasets.csv'
MODEL_URI = 'gs://vtx-models/pytorch/ecommerce/script'
MODEL_DISPLAY_NAME = 'pytorch-ecommerce-script'
STAGING_URI = 'gs://vtx-staging/pytorch/ecommerce/script'
TRAIN_IMAGE_URI = 'us-docker.pkg.dev/vertex-ai/training/pytorch-gpu.1-11:latest'

Одной из наиболее важных частей рабочего процесса e2e является трансформация. Почему? поскольку данные, над которыми мы работаем, имеют числовые и категориальные характеристики, взгляните:

Есть 4 числовых столбца: last_ecommerce_progress, bounces, time_on_site и pageviews, а остальные категориальные, нейронная сеть не считывает строки сразу, я должен сначала представить их в виде чисел, для этого есть разные способы, но в целом называется кодирование (спасибо Tanvir).

Кстати, в мире машинного обучения хорошей практикой является установление границ искажения данных (нормализация числовых признаков/масштабирование их), об этом много информации, спасибо Урваши Джейтли и вашей истории. о «Почему нормализация данных необходима для моделей машинного обучения».

Подробнее здесь и здесь.

Код обучения машинному обучению

В коде есть 2 основных части: предварительная обработка (на самом деле я создал для нее функцию python) и обучение, из-за характера преобразований, происходящих с вашей нейронной сетью, код может быть трудночитаемым, поэтому я попытаюсь объяснить самые важные части:

Это логика под таблицей:

  • Разделите данные на категориальные и числовые.
  • Преобразуйте категориальные в числа и нормализуйте числовые признаки.
  • Соедините оба.
  • Постройте нейронную сеть, используя измерение после разработки признаков (в: 114) и используя 32 нейронные сети в середине, отсев и нормализацию полезны для сокращения пространства обучения, чтобы модель могла учиться быстрее.
  • Вывод: 2, потому что мы предсказываем, купит клиент или нет (двоичный код).

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

Прежде всего, мы классифицируем признаки по их типу: категориальные/числовые:

target = 'will_buy_on_return_visit'
cat_columns = [i for i in df.columns if df[i].dtypes == 'object']
num_columns = [i for i in df.columns if df[i].dtypes == 'int64' or df[i].dtypes == 'float']
num_columns.remove(target)

cat_train_df = df[cat_columns]
num_train_df = df[num_columns]
label = df[target].to_numpy()

Мы используем LabelEncoder из scikit-learn для преобразования текста в значения:

labelencoder = defaultdict(LabelEncoder)
cat_train_df[cat_columns] = cat_train_df[cat_columns].apply(lambda x: labelencoder[x.name].fit_transform(x))
cat_train_df[cat_columns] = cat_train_df[cat_columns].astype('category')

Стандартизируем числовые значения:

scaler = StandardScaler()
    X_train[num_columns] = scaler.fit_transform(X_train[num_columns])
    X_val[num_columns] = scaler.transform(X_val[num_columns])

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

embedded_cols = {n: len(col.cat.categories) for n,col in X_train[cat_columns].items() if len(col.cat.categories) > 2}
embedded_col_names = embedded_cols.keys()
embedding_sizes = [(n_categories, min(50, (n_categories+1)//2)) for _,n_categories in embedded_cols.items()]
embedding_sizes = nn.ModuleList([nn.Embedding(categories, size) for categories,size in embedding_sizes])
pickle.dump(labelencoder, open('label.pkl', 'wb'))
pickle.dump(scaler, open('std_scaler.pkl', 'wb'))
pickle.dump(embedding_sizes, open('emb.pkl', 'wb'))

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

class ShelterOutcomeDataset(Dataset):
    def __init__(self, X, Y, embedded_col_names):
        X = X.copy()
        self.X1 = X.loc[:,embedded_col_names].copy().values.astype(np.int64) #categorical columns
        self.X2 = X.drop(columns=embedded_col_names).copy().values.astype(np.float32) #numerical columns
        self.y = Y
        
    def __len__(self):
        return len(self.y)
    
    def __getitem__(self, idx):
        return self.X1[idx], self.X2[idx], self.y[idx]

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

Обучение

Теперь самое интересное, я создам класс для обучения под названием ShelterOutcomeModel, этот класс вызовет nn.Module, чтобы собрать все части вместе. У нас в классе 2 части:

__init__ — это место, где мы инициализируем значения каркаса нейронной сети, например; количество нейронов, слои нормализации и отсева:

forward, где мы вызываем значения из __init__ и создаем последовательность во время обучения:

-› входы/эмбеддинги -> Drop - > Нормализация - > Concat - > Функция активации (relu) - > Drop - > Normalization - > Функция активации (relu) - > Drop - > Normalization - > Output

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

class ShelterOutcomeModel(nn.Module):
    def __init__(self, embedding_sizes, n_cont):
        super().__init__()
        self.embeddings = embedding_sizes
        n_emb = sum(e.embedding_dim for e in self.embeddings) #length of all embeddings combined
        self.n_emb, self.n_cont = n_emb, n_cont
        self.lin1 = nn.Linear(self.n_emb + self.n_cont, 200)
        self.lin2 = nn.Linear(200, 70)
        self.lin3 = nn.Linear(70, 2)
        self.bn1 = nn.BatchNorm1d(self.n_cont)
        self.bn2 = nn.BatchNorm1d(200)
        self.bn3 = nn.BatchNorm1d(70)
        self.emb_drop = nn.Dropout(0.6)
        self.drops = nn.Dropout(0.3)
        

    def forward(self, x_cat, x_cont):
        x = [e(x_cat[:,i]) for i,e in enumerate(self.embeddings)]
        x = torch.cat(x, 1)
        x = self.emb_drop(x)
        x2 = self.bn1(x_cont)
        x = torch.cat([x, x2], 1)
        x = F.relu(self.lin1(x))
        x = self.drops(x)
        x = self.bn2(x)
        x = F.relu(self.lin2(x))
        x = self.drops(x)
        x = self.bn3(x)
        x = self.lin3(x)
        return x

Последняя часть кода — это обучение в стиле python с использованием циклов и функций и вызовом класса нейронной сети:

def train_model(model, optim, train_dl):
    model.train()
    total = 0
    sum_loss = 0
    for x1, x2, y in train_dl:
        batch = y.shape[0]
        output = model(x1, x2)
        loss = F.cross_entropy(output, y)   
        optim.zero_grad()
        loss.backward()
        optim.step()
        total += batch
        sum_loss += batch*(loss.item())
    return sum_loss/total

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

def val_loss(model, valid_dl):
    model.eval()
    total = 0
    sum_loss = 0
    correct = 0
    for x1, x2, y in valid_dl:
        current_batch_size = y.shape[0]
        out = model(x1, x2)
        loss = F.cross_entropy(out, y)
        sum_loss += current_batch_size*(loss.item())
        total += current_batch_size
        pred = torch.max(out, 1)[1]
        correct += (pred == y).float().sum().item()
    print("valid loss %.3f and accuracy %.3f" % (sum_loss/total, correct/total))
    return sum_loss/total, correct/total
def train_loop(model, epochs, lr=0.01, wd=0.0):
    optim = get_optimizer(model, lr = lr, wd = wd)
    for i in range(epochs): 
        loss = train_model(model, optim, train_dl)
        print("training loss: ", loss)
        val_loss(model, valid_dl)

PyTorch не читает данные, поскольку они должны проходить через свою библиотеку:

batch_size = 1000
train_dl = DataLoader(train_ds, batch_size=batch_size,shuffle=True)
valid_dl = DataLoader(valid_ds, batch_size=batch_size,shuffle=True)

train_dl = DeviceDataLoader(train_dl, device)
valid_dl = DeviceDataLoader(valid_dl, device)

В конце мы используем вызов функции, созданной ранее для обучения:

train_loop(model, epochs=8, lr=0.05, wd=0.00001)
torch.save(model.state_dict(), "state_model.pt")

И все, именно так создается нейросеть на PyTorch, очень полезная информация есть здесь, документация PyTorch легко читается.

Я использую виртуальную машину на GCP с T4 GPU, на которой установлены драйверы nvidia с последним deep-learning linux image (debian) и vscode в качестве IDE, это работает, это идеально , он работает без сбоев.

Как вы можете видеть, потеря уменьшится после некоторых итераций, от 0,06 до 0,05, ненамного, и мы можем улучшить ее, изменив элементы нейронной сети или, возможно, добавив больше данных или избавившись от признаков без сильной корреляции по зависимой переменной ( результат/метка/цель), но я оставлю часть оптимизации для другой истории.

Это был код; фрагменты, вставленные из Aakanksha NS (большое спасибо!) здесь. Набор данных другой, предварительная обработка тоже, а инфраструктура была Vertex AI от Google.

Время для вершины

Как только наш обучающий файл создан: /tmp/train.py, мы можем вызвать обучение вершин с помощью aiplatform.CustomJob.from_local_script (время написания версии: 1.22.0) следующим образом:

from google.cloud import aiplatform as aip

customJob = aip.CustomJob.from_local_script(
    display_name = 'pytorch_tab_sa_ecommerce',
    script_path = 'source/trainer/train.py',
    container_uri = TRAIN_IMAGE_URI,
    requirements = ['scikit-learn'],
    args = [
        '--dataset_uri', 
        DATASET_URI,
        '--project',
        PROJECT_ID
    ],
    replica_count = 1,
    machine_type = 'n1-standard-4',
    accelerator_type = 'NVIDIA_TESLA_T4',
    accelerator_count = 1,
    staging_bucket = STAGING_URI,
    base_output_dir = MODEL_URI
)

customJob.run()

Существуют разные способы работы с библиотекой aiplatform: вы можете сделать это локально со своего ноутбука (только не забудьте сохранить свои учетные данные gcloud), из Vertex Workbench (управляемая служба Jupyter Notebook), используя Colab (самый простой способ) или любая другая IDE, которая вам нравится, не имеет значения, если вы прошли аутентификацию в Google.

Самый простой способ: зайти в мои репозитории и нажать colab.

Измените ваши переменные и раскомментируйте первые 2 поля, это должно выглядеть так:

Затем проведите обучение:

Вы можете изучить свою работу, перейдя в облачную консоль Google. › Vertex AI › Обучение › Пользовательские задания

Нажмите на свою работу и журналы:

Ваше окно должно выглядеть так:

Отличная работа! вы обучили свою первую нейронную сеть PyTorch за пару минут.

Теперь мы можем закрыть наш ноутбук, пойти выпить пива и отпраздновать.

Но если вам интересно, как развернуть модель и начать делать прогнозы, выполните следующие шаги в своей записной книжке:

Прогнозы с использованием пользовательских процедур прогнозирования.

А вот с PyTorch, как вы могли заметить, все на python (мы никогда не используем малоизвестные библиотеки, которые творят чудеса за кулисами), единственная проблема: это может быть сложно. Поэтому, если вы хотите развернуть модель для прогнозов, вам нужно заново создать нейронную сеть со значениями, которые вы использовали в процессе преобразования, поэтому CPR поможет нам создать веб-сервер с необходимым кодом ML.

Прежде всего: нам нужно создать файл с требованиями к библиотеке для этого нового блестящего контейнера:

%%writefile $USER_SRC_DIR/requirements.txt
fastapi
uvicorn==0.17.6
pandas
torch
scikit-learn
google-cloud-storage>=1.26.0,<2.0.0dev
google-cloud-aiplatform[prediction]>=1.16.0

А затем следуйте процессу создания деталей для CPR (подробнее здесь).

он имеет 3 уровня: загрузка (как извлечь данные для прогнозирования), предварительная обработка (вся разработка функций со значениями из обучения), постобработка (что делать в конце с предсказанием)

Вот как выглядит препроцесс:

def preprocess(self, prediction_input: Dict) -> torch.utils.data.dataloader.DataLoader:
        instances = prediction_input["instances"]
        data = pd.DataFrame(instances, columns = self.columns)
        ## Prepare Data        
        embedded_col_names = ['source', 'medium', 'channelGrouping', 'deviceCategory', 'country']
        
        def preprocessing_data(df):
            import pickle
            
            standarization = pickle.load(open("std_scaler.pkl", "rb"))
            labelencoder = pickle.load(open("label.pkl", "rb"))
    
            target = 'will_buy_on_return_visit'
            cat_columns = [i for i in df.columns if df[i].dtypes == 'object']
            num_columns = [i for i in df.columns if df[i].dtypes == 'int64' or df[i].dtypes == 'float']

            cat_df = df[cat_columns]
            num_df = df[num_columns]
    
            cat_df = cat_df.apply(lambda x: labelencoder[x.name].transform(x))
            cat_df = cat_df.astype('category')
    
            df = pd.concat([cat_df, num_df], axis=1)
            df[num_columns] = standarization.transform(df[num_columns])
            
            return df

        class PredictData(Dataset):
            def __init__(self, X):
                embedded_col_names = ['source', 'medium', 'channelGrouping', 'deviceCategory', 'country']
                self.X1 = X.loc[:,embedded_col_names].copy().values.astype(np.int64)
                self.X2 = X.drop(columns=embedded_col_names).copy().values.astype(np.float32)

            def __getitem__(self, index):
                return self.X1[index], self.X2[index]

            def __len__ (self):
                return len(self.X1)
        
        prep_df = DataLoader(PredictData(preprocessing_data(data)))
        return prep_df

Подождите, это тот же код, который мы использовали выше!, да, мы загружаем файлы *.pkl с другим, потому что в процессе стандартизации мы применили уравнение:

Стандартное отклонение и среднее значение являются значениями, полученными в результате обучения (ЭТО ОЧЕНЬ ВАЖНО), без которого не может быть идеальной модели.

Загрузочная часть:

Во время этой функции я снова объявил свою нейронную сеть следующим образом:

def load(self, artifacts_uri: str):
        """Loads the model artifacts."""
        prediction_utils.download_model_artifacts(artifacts_uri)
        self.embeddings = pickle.load(open('emb.pkl', 'rb'))
        class ShelterOutcomeModel(nn.Module):
            def __init__(self, embedding_sizes, n_cont):
                super().__init__()
                self.embeddings = embedding_sizes
                n_emb = sum(e.embedding_dim for e in self.embeddings) #length of all embeddings combined
                self.n_emb, self.n_cont = n_emb, 4
                self.lin1 = nn.Linear(self.n_emb + self.n_cont, 200)
                self.lin2 = nn.Linear(200, 70)
                self.lin3 = nn.Linear(70, 2)
                self.bn1 = nn.BatchNorm1d(self.n_cont)
                self.bn2 = nn.BatchNorm1d(200)
                self.bn3 = nn.BatchNorm1d(70)
                self.emb_drop = nn.Dropout(0.6)
                self.drops = nn.Dropout(0.3)


            def forward(self, x_cat, x_cont):
                x = [e(x_cat[:,i]) for i,e in enumerate(self.embeddings)]
                x = torch.cat(x, 1)
                x = self.emb_drop(x)
                x2 = self.bn1(x_cont)
                x = torch.cat([x, x2], 1)
                x = F.relu(self.lin1(x))
                x = self.drops(x)
                x = self.bn2(x)
                x = F.relu(self.lin2(x))
                x = self.drops(x)
                x = self.bn3(x)
                x = self.lin3(x)
                return x
            
        device = torch.device('cpu')
        self._model = ShelterOutcomeModel(self.embeddings, 4)
        self._model.load_state_dict(torch.load("state_model.pt", map_location=device))

Вывод довольно прост:

@torch.inference_mode()
    def predict(self, instances: torch.utils.data.dataloader.DataLoader) -> list:
        """Performs prediction."""
        preds = []
        self._model.eval()
        with torch.no_grad():
            for x1,x2 in instances:
                out = self._model(x1,x2)
                prob = F.softmax(out, dim=1)
                preds.append(prob)
        final_probs = [item for sublist in preds for item in sublist]
        predicted = [0 if t[0] > 0.5 else 1 for t in final_probs]
        print(predicted)
        return predicted

    def postprocess(self, prediction_results: list) -> Dict:
        return {"predictions": prediction_results}

Не забудьте пройти аутентификацию (не беспокойтесь, моя записная книжка все объясняет).

Вы можете протестировать контейнер локально:

Рекомендую зайти в терминал вашего блокнота и посмотреть логи последнего созданного контейнера, там вы увидите прогнозы.

Если все работает нормально, вы можете отправить контейнер в Google Cloud Container Registry:

local_model.push_image()

model = aip.Model.upload(
    local_model=local_model,
    display_name=MODEL_DISPLAY_NAME,
    artifact_uri=f"{MODEL_URI}/model",
)

Разверните модель и протестируйте ее:

endpoint = model.deploy(machine_type="n1-standard-4")

Мы сделали это!

Немного! много шагов в строке (я знаю, но что-то, что нравится разработчикам PyTorch, заключается в том, что он прозрачен, а код выставлен таким образом, что вы можете легко настроить его).

Надеюсь, вам понравилась эта история.

Удачного кодирования!

Полный код здесь.