<jupyter_start><jupyter_text>Домашнее задание. Transformers.Привет! Это очередное домашнее задание, но теперь ты познакомишься с моделя трансформеров и с библиотекой `HuggingFace🤗`. В этом задании будет предложено решить с помощью модели `GPT2` простую задачу (анализ сентимента) и сделать небольшое исследование карт внимания. Приступим!<jupyter_code>import numpy as np
import matplotlib.pyplot as plt
from matplotlib import ticker

import torch
import torch.nn as nn

from transformers import GPT2ForSequenceClassification, GPT2TokenizerFast, GPT2Config
from datasets import load_dataset
device = "cuda" if torch.cuda.is_available else "cpu"<jupyter_output><empty_output><jupyter_text>Датасет, который мы будем использовать сегодня – тексты из английского твиттера. Они уже почищенны от никнеймов, знаков препинания и прочего.<jupyter_code>emotion_dataset = load_dataset("emotion")<jupyter_output><empty_output><jupyter_text>Посмотри, из чего состоит `emotion_dataset`:<jupyter_code>emotion_dataset
emotion_dataset["train"]
emotion_dataset["train"]["text"][0]
emotion_dataset["train"]["label"][0]
len(emotion_dataset["train"])<jupyter_output><empty_output><jupyter_text>Для перевода текста в токены мы будем использовать предобученный BPE-токенайзер.<jupyter_code>tokenizer = GPT2TokenizerFast.from_pretrained("distilgpt2")
tokenizer.pad_token = tokenizer.eos_token # У gpt2 нет pad токенов. Вместо них воспользуемся токенами конца текста.<jupyter_output><empty_output><jupyter_text>Подготовь класс, который принимает датасет, токенайзер и имя используемой части (`train`, `validation`, `test`). Используй его для получения данных для обучения.P.S. Посмотри, как работает токенайзер ([docs](https://huggingface.co/transformers/main_classes/tokenizer.html)) и подумай, как его надо добавить в датасет. Немного примеров, как работает токенайзер. Это поможет с написанием датасета.<jupyter_code>tokenizer.tokenize(emotion_dataset["train"]["text"][0])
tokenizer.encode(emotion_dataset["train"]["text"][0])
tokenizer.encode_plus(emotion_dataset["train"]["text"][0])
tokenizer.encode_plus(emotion_dataset["train"]["text"][0], return_tensors="pt")
tokenizer.encode_plus(
    emotion_dataset["train"]["text"][0], 
    max_length=128, # максимальная длина текста
    padding="max_length", # надо ли добавлять паддинг в конце?
    return_tensors="pt", # возвращает pytorch тензоры
)
# Если надо, попрактикуйся работать с токенайзером здесь
class TweetDataset(torch.utils.data.Dataset):
    def __init__(self, part, dataset=emotion_dataset, tokenizer=tokenizer, max_length=128):
        self.part = part
        self.dataset = dataset
        self.tokenizer = tokenizer
        self.max_length = max_length
        
        self.labels = np.unique(dataset[part]["label"])
        self.label2num = {l: num for num, l in enumerate(self.labels)}
        
    def __getitem__(self, idx):
        """
        Return dict with tokens, attention_mask and label
        """
        text = self.dataset[self.part]["text"][idx]
        label = self.dataset[self.part]["label"][idx]
        
        tokenizer_output = self.tokenizer.encode_plus(text, max_length=128,padding="max_length", return_tensors="pt")
        target = self.label2num[label]
        return {
            "input_ids": tokenizer_output["input_ids"], 
            "mask":  tokenizer_output["attention_mask"],
            "target": target
        }
        
    def __len__(self):
        """
        Return length of dataset
        """
        return len(self.dataset[self.part])<jupyter_output><empty_output><jupyter_text>Создай `train`, `validation` и `test` части датасета. Загрузи их в `DataLoaders`.<jupyter_code>train_dataset = TweetDataset('train')
valid_dataset = TweetDataset('validation') # validation
test_dataset = TweetDataset('test')
batch_size = 64 # Задай batch_size
num_workers=4
train_loader = torch.utils.data.DataLoader(
    train_dataset,batch_size=batch_size,
)
valid_loader = torch.utils.data.DataLoader(
    valid_dataset,batch_size=batch_size
)
test_loader = torch.utils.data.DataLoader(
    test_dataset,batch_size=batch_size
)<jupyter_output><empty_output><jupyter_text>Начнем с нуля.Попробуем обучить модель трансформер с нуля решать данную задачу.<jupyter_code>config = GPT2Config.from_pretrained(
    "distilgpt2", # distilgpt2 – уменьшенная версия модели gpt2
    output_attentions=True,
    pad_token_id=tokenizer.eos_token_id,
    num_labels=6
)
model_0 = GPT2ForSequenceClassification(config=config).to(device) # GPT2 для классификации текста<jupyter_output><empty_output><jupyter_text>Подготовь оптимайзер и критерий:<jupyter_code>lr = 1e-5 # Предполагаемый learning rate. Он может быть больше или меньше :)

optimizer = torch.optim.Adam(model_0.parameters(),lr=lr)
criterion = nn.CrossEntropyLoss()
# scheduler = ... # Можно добавить шедулер для обучения моделей. Это на твое усмотрение<jupyter_output><empty_output><jupyter_text>Посмотри, что возвращает модель ([docs](https://huggingface.co/transformers/model_doc/gpt2.htmlgpt2forsequenceclassification)), если в неё подать данные:<jupyter_code>tokens = train_dataset.__getitem__(0)['input_ids'].to(device) # Получи токены из датасета
mask = train_dataset.__getitem__(0)['mask'].to(device) # Получи маску из датасета


model_0(input_ids=tokens,attention_mask=mask)[0] # Посмотри на аутпут<jupyter_output><empty_output><jupyter_text>Обучи модель с помощью `train_dataset`, проверяй во время обучения качество с помощью `valid_dataset` и получи финальное качество с помощью `test_dataset`.<jupyter_code>from tqdm.notebook import tqdm
from sklearn.metrics import accuracy_score

num_epochs = 10

# Train loop
for e in range(num_epochs):
    model_0.train()
    train_loss = 0
    for batch in tqdm(train_loader):
        src_ids=batch.get('input_ids').squeeze(1).to(device)
        src_mask=batch.get("mask").squeeze(1).to(device)
        trg=batch.get('target').unsqueeze(0).to(device)
        optimizer.zero_grad()
        output=model_0(input_ids=src_ids,attention_mask=src_mask,labels=trg)[1]
        loss=criterion(output,trg.squeeze(0))
        loss.backward()
        optimizer.step()
        train_loss+=loss
    valid_loss = 0
    valid_acc = 0
    model_0.eval()
    with torch.no_grad():
        for batch in valid_loader:
            src_ids=batch['input_ids'].squeeze(1).to(device)
            src_mask=batch["mask"].squeeze(1).to(device)
            trg=batch['target'].unsqueeze(0).to(device)
            output=model_0(input_ids=src_ids,attention_mask=src_mask,labels=trg)[1]
            loss=criterion(output,trg.squeeze(0))
            valid_loss+=loss
            valid_acc+=accuracy_score(trg.permute(1,0).cpu().data.numpy(),output.cpu().argmax(1).data.numpy())
    print(f"Train Loss: {train_loss / len(train_loader)},"
          f"Valid Loss: {valid_loss / len(valid_loader)},"
          f"Valid Acc: {valid_acc / len(valid_loader)}")
        
# Testing
test_acc = 0
model_0.eval()
with torch.no_grad():
    for batch in test_loader:
        src_ids=batch['input_ids'].squeeze(1).to(device)
        src_mask=batch["mask"].squeeze(1).to(device)
        trg=batch['target'].unsqueeze(0).to(device)
        output=model_0(input_ids=src_ids,attention_mask=src_mask,labels=trg)[1]
        test_acc+=accuracy_score(trg.permute(1,0).cpu().data.numpy(),output.cpu().argmax(1).data.numpy())
print(f"Test Acc: {test_acc / len(test_loader)}")<jupyter_output><empty_output><jupyter_text>После того, как получил лучшую модель для решения этой задачи, посмотри на карты внимания. Нашел ли что-нибудь интересное в них?<jupyter_code>labels=['anger','fear','joy','love','sadness','surprize']
def get_attention_matrixes(model, tokenizer, text, device=device):
    inp = list(filter(lambda x: x != tokenizer.sep_token_id, tokenizer.encode(text)))
    inp = torch.tensor(inp, dtype=torch.long, device=device).unsqueeze(0)
    attn_tensors = model(inp)[-1]
    pred=labels[model(inp)[0].argmax(1)]
    seq = [tokenizer.decode(x) for x in inp[0].tolist()]
    attn = []
    for i in range(len(attn_tensors)):
        attn_layer = []
        for j in range(attn_tensors[i].size(1)):
            attn_layer.append(attn_tensors[i][0, j].cpu().detach().numpy())
        attn.append(np.array(attn_layer))
    
    return np.array(attn), seq, pred
def show_attention(seq, attentions,displayFirst):
    # Set up figure with colorbar
    
    if(displayFirst):
        fig = plt.figure(figsize=(20,20))
        ax = fig.add_subplot(111)
        cax = ax.matshow(attentions[0])
        fig.colorbar(cax)

        # Set up axes
        ax.set_xticklabels(['']+seq, rotation=90, fontsize=16)
        ax.set_yticklabels(['']+seq, fontsize=16)

        # Show label at every tick
        ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
        ax.yaxis.set_major_locator(ticker.MultipleLocator(1))

        plt.show()
    else:
        for i in attentions:
            fig = plt.figure(figsize=(20,20))
            ax = fig.add_subplot(111)
            cax = ax.matshow(i)
            fig.colorbar(cax)

            # Set up axes
            ax.set_xticklabels(['']+seq, rotation=90, fontsize=16)
            ax.set_yticklabels(['']+seq, fontsize=16)

            # Show label at every tick
            ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
            ax.yaxis.set_major_locator(ticker.MultipleLocator(1))

            plt.show()
    
text = emotion_dataset["test"]["text"][15] # Выбери текст из датасета
tokens = tokenizer.tokenize(text)
attn,seq,pred = get_attention_matrixes(model_0, tokenizer, text)
print(text)
print(pred)
show_attention(seq, attn[-1],True)# You can check more attentions by choosing False<jupyter_output>i was feeling as heartbroken as im sure katniss was
sadness<jupyter_text>Fine-tuningТеперь другой подход: загрузим модель, которая обучалась решать задачу Language Modeling. Посмотрим, получим ли мы прирост в качестве.<jupyter_code>model_1 = GPT2ForSequenceClassification.from_pretrained(
    "distilgpt2", 
    output_attentions=True,
    pad_token_id=tokenizer.eos_token_id,
    num_labels=6
).to(device)
lr = 1e-5 # Предполагаемый learning rate. Он может быть больше или меньше :)

optimizer = torch.optim.Adam(model_1.parameters(),lr=lr)
criterion = nn.CrossEntropyLoss()
# scheduler = ... # Можно добавить шедулер для обучения моделей. Это на твое усмотрение<jupyter_output><empty_output><jupyter_text>Вывод модели ничем не отличается от предыдущего случая, поэтому сразу приступаем к обучению:<jupyter_code>from tqdm.notebook import tqdm


num_epochs = 5

# Train loop
for e in range(num_epochs):
    model_1.train()
    train_loss = 0
    for batch in tqdm(train_loader):
        src_ids=batch.get('input_ids').squeeze(1).to(device)
        src_mask=batch.get("mask").squeeze(1).to(device)
        trg=batch.get('target').unsqueeze(0).to(device)
        optimizer.zero_grad()
        output=model_1(input_ids=src_ids,attention_mask=src_mask,labels=trg)[1]
        loss=criterion(output,trg.squeeze(0))
        loss.backward()
        optimizer.step()
        train_loss+=loss
    valid_loss = 0
    valid_acc = 0
    model_1.eval()
    with torch.no_grad():
        for batch in valid_loader:
            src_ids=batch['input_ids'].squeeze(1).to(device)
            src_mask=batch["mask"].squeeze(1).to(device)
            trg=batch['target'].unsqueeze(0).to(device)
            output=model_1(input_ids=src_ids,attention_mask=src_mask,labels=trg)[1]
            loss=criterion(output,trg.squeeze(0))
            valid_loss+=loss
            valid_acc+=accuracy_score(trg.permute(1,0).cpu().data.numpy(),output.cpu().argmax(1).data.numpy())
    print(f"Train Loss: {train_loss / len(train_loader)},"
          f"Valid Loss: {valid_loss / len(valid_loader)},"
          f"Valid Acc: {valid_acc / len(valid_loader)}")
        
# Testing
test_acc = 0
model_1.eval()
with torch.no_grad():
    for batch in test_loader:
        src_ids=batch['input_ids'].squeeze(1).to(device)
        src_mask=batch["mask"].squeeze(1).to(device)
        trg=batch['target'].unsqueeze(0).to(device)
        output=model_1(input_ids=src_ids,attention_mask=src_mask,labels=trg)[1]
        test_acc+=accuracy_score(trg.permute(1,0).cpu().data.numpy(),output.cpu().argmax(1).data.numpy())
print(f"Test Acc: {test_acc / len(test_loader)}")<jupyter_output><empty_output><jupyter_text>Есть ли прирост качества или скорости обучения?Посмотри на карты внимания. Есть ли отличие от предыдущего случая?<jupyter_code>text = emotion_dataset["test"]["text"][15] # Выбери текст из датасета
tokens = tokenizer.tokenize(text)
attn,seq,pred = get_attention_matrixes(model_1, tokenizer, text)
print(text)
print(pred)
show_attention(seq, attn[-1],True)# You can check more attentions by choosing False<jupyter_output>i was feeling as heartbroken as im sure katniss was
sadness<jupyter_text>ОтчетПокажи здесь, что ты выполнил по этой работе. Ответь на несколько вопросов:- Какой подход оказался лучше? - На какие слова модель большего всего обращала внимание?- На каких слоях/головах модель обращала внимание? 1) Лучше оказался второй подход, поскольку быстрее приближался к лучшему качеству и способен добится лучших его значений.2) Больше всего она обращает внимания на имена, местоимения и прилагательные, особенно если они связаны с первым словом в предложении.3) Не совсем понятна суть вопроса, однако при просмотре всех голов последнего слоя в обеих моделях явно заметно, что модель уделяет большее внимание первому слову и связанным с ним.<jupyter_code><jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.11782968657599054, 'fraction_numerical': 0.01212300413956239, 'mean_word_length': 4.492894843686561, 'pattern_counts': {'":': 3, '<': 59, '<?xml version=': 0, '>': 59, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '536350', 'license': 'MIT', 'n_tokens_mistral': 4992, 'n_tokens_neox': 5234, 'n_words': 895, 'path': '2sem/Transformers/[homework]Transformers.ipynb', 'repo_name': 'Temprain-ops/Deep_Learning_School_MIPT', 'size': '13528'}