Набор данных Toxic Comments Kaggle — это хорошо известный набор данных и тестовый набор для классификации текста с несколькими метками. Ознакомьтесь с моим предыдущим сообщением в блоге об изучении набора данных о токсичных комментариях. В этой работе я реализую модели глубокого обучения с использованием фреймворка Tensorflow и Pytorch, а также сравниваю другие разные методологии, чтобы подойти к задаче классификации текста с несколькими метками.

Импорт и очистка набора данных

Функцию очистки текста можно найти в моей предыдущей работе над набором данных токсичных комментариев. Я удобно импортирую эти функции, чтобы упростить предварительную обработку текста. Затем я использую scikit-learn CountVectorizer для базовых вложений. Я выбрал CountVectorizer в качестве базового встраивания текста. Определенно есть более продвинутые вложения текста, которые могут лучше работать для классификации текста.

# importing dataset from local directory
dataset_directory = ".../toxic-comments-datasets/"
train_df = pd.read_csv(dataset_directory + "train.csv.zip", usecols = ['comment_text', 'toxic', 'severe_toxic', 'obscene', 'threat', 'insult', 'identity_hate'])
train_df = train_df.astype({'toxic':'int16',
                            'severe_toxic':'int16',
                            'obscene':'int16',
                            'threat':'int16',
                            'insult':'int16',
                            'identity_hate':'int16'})

train_df = train_df.dropna()  # I found 5 nan rows in cleaned_text, so I just drop those rows altogether

# importing custom text_cleaner 
from text_cleaning_functions import text_cleaner
train_df['cleaner_text'] = train_df['comment_text'].map(lambda comments : text_cleaner(comments))
# create text embeddings with scikit-learn CountVectorizer
LABELS = ['toxic', 'severe_toxic', 'obscene', 'threat', 'insult', 'identity_hate']
train_text = train_df['cleaner_text']
train_labels = train_df[LABELS]

train_text_countvec = CountVectorizer(max_features=10000).fit_transform(train_text)

X_train, X_test, y_train, y_test = train_test_split(train_text_countvec, train_labels.values, test_size=0.2)
print(X_train.shape)
print(X_test.shape)

Модель TensorFlow MLP

Архитектура, которую я использую, представляет собой очень общий многослойный персептрон (MLP) с несколькими полностью связанными слоями, одним отсеваемым слоем и слоем пакетной нормализации. Топология этой архитектуры сужается от большого количества вложений N-векторов текста, который нужно классифицировать, к 6 бинарным классам задачи классификации с несколькими метками.

import tensorflow as tf
from tensorflow.keras import layers

l2_regularizer = tf.keras.regularizers.L2(l2=0.001) # default l2 =
glorot_initializer = tf.keras.initializers.GlorotNormal(seed=32)

model2 = tf.keras.Sequential([
    layers.Input(shape=X_train.shape[1:], name="Input_Layer"),
    
    layers.Dense(256, 
                 activation="relu", 
                 kernel_initializer=glorot_initializer,
                 kernel_regularizer=l2_regularizer, 
                 bias_regularizer=l2_regularizer, 
                 name="FC1"),
    
    layers.Dense(128, 
                 activation="relu", 
                 kernel_initializer=glorot_initializer,
                 kernel_regularizer=l2_regularizer,
                 bias_regularizer=l2_regularizer, 
                 name="FC2"),
    
    layers.Dropout(rate=0.3),
    
    layers.Dense(64, 
                 activation="relu", 
                 kernel_initializer=glorot_initializer,
                 kernel_regularizer=l2_regularizer, 
                 bias_regularizer=l2_regularizer, 
                 name="FC3"),
    
    layers.BatchNormalization(),
    
    layers.Dense(32, 
                 activation="relu", 
                 kernel_initializer=glorot_initializer,
                 kernel_regularizer=l2_regularizer, 
                 bias_regularizer=l2_regularizer, 
                 name="FC4"),
    
    layers.Dense(y_train.shape[1], 
                 activation="sigmoid", 
                 kernel_initializer=tf.keras.initializers.GlorotNormal(seed=3),
                 kernel_regularizer=l2_regularizer, 
                 bias_regularizer=l2_regularizer, name="Output")
])

#compile
model2.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
              loss='binary_crossentropy',
              metrics=["binary_accuracy"])

Объяснение гиперпараметров:

`kernel_initializer` — это параметр, используемый в нейронных сетях для определения метода инициализации весов (также называемых «ядрами») слоев. Правильная инициализация веса имеет решающее значение для обучения нейронной сети, поскольку она может повлиять на скорость сходимости и окончательную производительность модели.

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

1. Случайная инициализация: случайная инициализация весов небольшими значениями, обычно выбираемыми из равномерного или нормального распределения.

2. Инициализация Xavier/Glorot: Инициализация весов путем взятия образцов из равномерного распределения с определенным диапазоном. Этот диапазон зависит от количества входных и выходных единиц в слое. Идея инициализации Xavier состоит в том, чтобы поддерживать одинаковую дисперсию как для входных, так и для выходных активаций во время прямого прохода, что помогает при обучении глубоких сетей.

3. Инициализация He: аналогична инициализации Xavier, но использует другой диапазон для равномерного распределения, что делает его более подходящим для сетей, использующих функции активации ReLU (выпрямленная линейная единица).

`kernel_regularizer` – это параметр, используемый в нейронных сетях для применения регуляризации к весам (также называемым "ядрами") слоев. Регуляризация — это метод, используемый для предотвращения переобучения путем добавления штрафного члена к функции потерь, что препятствует изучению моделью слишком сложных или зашумленных шаблонов в обучающих данных. Основная цель регуляризации — улучшить способность модели к обобщению, чтобы она хорошо работала с невидимыми данными.

Существует два основных типа регуляризации, применяемых к весам:

1. Регуляризация L1 (лассо): добавляет абсолютные значения весов к функции потерь, масштабированные с помощью коэффициента регуляризации (обычно обозначаемого лямбда или альфа). Этот тип регуляризации способствует разреженности модели, поскольку некоторые веса могут быть сведены к нулю.

2. Регуляризация L2 (хребет): Добавляет квадраты значений весов к функции потерь, масштабированные с помощью коэффициента регуляризации. Регуляризация L2 не поощряет большие значения веса, но не обязательно сводит их к нулю. Обычно используется в нейронных сетях.

Обучение скомпилированной модели NN

# this step is necessary to convert the sparse matrix to a sparse tensor

def convert_sparse_matrix_to_sparse_tensor(X):
    coo = X.tocoo()
    indices = np.mat([coo.row, coo.col]).transpose()
    return tf.SparseTensor(indices, coo.data, coo.shape)

train_tensor = convert_sparse_matrix_to_sparse_tensor(X_train)
train_tensor = tf.sparse.reorder(train_tensor)
test_tensor = convert_sparse_matrix_to_sparse_tensor(X_test)
test_tensor= tf.sparse.reorder(test_tensor)

# train for 10 epochs
history = model2.fit(train_tensor, y_train, 
                     validation_data=(test_tensor, y_test), 
                     epochs=10)

Модель Pytorch MLP

Для сравнения и обучения я создал эквивалентную нейронную сеть с PyTorch.

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.nn import init

class CustomModel(nn.Module):
    def __init__(self, input_shape, output_shape):
        super(CustomModel, self).__init__()
        
        self.fc1 = nn.Linear(input_shape, 256)
        self.fc2 = nn.Linear(256, 128)
        self.fc3 = nn.Linear(128, 64)
        self.bn = nn.BatchNorm1d(64)
        self.fc4 = nn.Linear(64, 32)
        self.fc5 = nn.Linear(32, output_shape)
        
        self.dropout = nn.Dropout(p=0.3)
        
        self.init_weights()
        
    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.dropout(x)
        x = F.relu(self.fc3(x))
        x = self.bn(x)
        x = F.relu(self.fc4(x))
        x = torch.sigmoid(self.fc5(x))
        return x

    def init_weights(self):
        for m in self.modules():
            if isinstance(m, nn.Linear):
                init.xavier_normal_(m.weight, gain=1)
                init.constant_(m.bias, 0)
                
                # Apply L2 regularization to weights and biases
                # Note that the equivalent is handled in the optimizer in PyTorch
                # See the optimizer definition below

input_shape = X_train.shape[1]
output_shape = y_train.shape[1]

model = CustomModel(input_shape, output_shape)

# Define an optimizer with L2 regularization (weight_decay)
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=0.001)
criterion = nn.BCELoss() # Use Binary Cross Entropy Loss for binary classification
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=0.001) # Define the optimizer with L2 regularization
# convert the training and test data to usable torch tensors
X_train_dense = X_train.toarray()
X_test_dense = X_test.toarray()

X_train_tensor = torch.tensor(X_train_dense, dtype=torch.float32)
y_train_tensor = torch.tensor(y_train, dtype=torch.float32)
X_test_tensor = torch.tensor(X_test_dense, dtype=torch.float32)
y_test_tensor = torch.tensor(y_test, dtype=torch.float32)
from torch.utils.data import TensorDataset, DataLoader

batch_size = 64

train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)

test_dataset = TensorDataset(X_test_tensor, y_test_tensor)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
epochs = 10

for epoch in range(epochs):
    model.train() # Set the model to training mode
    
    # Training loop
    for i, (inputs, targets) in enumerate(train_loader):
        optimizer.zero_grad() # Reset the gradients
        outputs = model(inputs) # Forward pass
        loss = criterion(outputs, targets) # Compute the loss
        loss.backward() # Backward pass
        optimizer.step() # Update the weights

    # Validation loop
    model.eval() # Set the model to evaluation mode
    validation_loss = 0
    for inputs, targets in test_loader:
        with torch.no_grad():
            outputs = model(inputs) # Forward pass
            loss = criterion(outputs, targets) # Compute the loss
            validation_loss += loss.item()

    validation_loss /= len(test_loader)
    print(f"Epoch {epoch+1}/{epochs}, Validation Loss: {validation_loss:.4f}")

Заключение

Различия между Tensorflow и Pytorch

  • Первое отличие заключается в API и синтаксисе кода для реализации обоих пакетов. Типичный рабочий процесс для реализации моделей глубокого обучения:
    создать модель → скомпилировать → подогнать/обучить → предсказать.
    Я считаю, что Tensorflow API немного более интуитивно понятен, однако я м уверен, что это дело вкуса.
  • Pytorch был значительно быстрее при обучении по сравнению с Tensorflow.
  • В Pytorch есть больше кода для реализации той же модели NN.

Точность и потери обеих моделей были очень близки, что неудивительно, поскольку архитектуры НС были одинаковыми. Разница в первую очередь в реализации и скорости обработки.

Что дальше?

  • Реализуйте различные вложения текста, например. БЕРТ, Перчатка
  • Экспериментируйте с различными архитектурами NN