PyTorch — это мощный и гибкий фреймворк для машинного обучения, широко используемый для создания нейронных сетей. Он особенно популярен благодаря простоте использования, динамическим вычислительным графам и богатой экосистеме инструментов для обучения моделей. Для использования этого фреймворка, часто достаточно поверхностно понимать работу алгоритмов машинного обучения.
Но Андрей Карпаты, известный исследователь в области ИИ, считает, что реализация алгоритмов с нуля позволяет понять их суть и детали работы, что сложно осознать, используя только готовые библиотеки. Это помогает развить интуицию для дальнейшего применения и улучшения методов. Андрей посвящает много собственного времени, чтобы объяснять ключевые принципы работы нейросетей в своих блогах и на своём ютуб-канале. Он также не раз подчеркивал, что на его курсе в Cтэнфорде есть задачи по реализации различных алгоритмов, например, обратное распространение.
Я хотел бы посвятить данную статью этой идеи, потому что мне самому особенно интересно копаться в алгоритмах глубокого обучения. Эта статья продолжение третьей статьи
Итак, как я и сказал в предыдущей статье, у нас достаточно знаний, чтобы собрать их в целую библиотеку. Так я и сделал, реализовав pycandle.
Там находиться всё то, что мы изучали на протяжении 3 частей. В этой части мы будем писать инференс код для GPT2 на собственной библиотеке!
Начнём с импорта библиотек
import torch
import torch.nn as nn
Ой, не то. Я имел в виду
import candle
import candle.nn as nn
Также воспользуемся токенизатором от OpenAI, который они используют в своих моделях. Сделаем все необходимые импорты
import tiktoken
from candle import Tensor
from dataclasses import dataclass
Определим конфигурацию нашей модели с помощью dataclasses
@dataclass
class GPTConfig:
block_size: int = 1024 # размер окна вниманиә
vocab_size: int = 50257 # размер словаря BPE
n_layer: int = 12 # количество слоёв
n_head: int = 12 # количество голов в механизме внимания
n_embd: int = 768 # размерность вектора эмбеддингов
Определим линейный слой модели
class MLP(nn.Module):
def __init__(self, config):
super().__init__()
self.c_fc = nn.Linear(config.n_embd, 4 * config.n_embd)
self.gelu = nn.GeLU()
self.c_proj = nn.Linear(4 * config.n_embd, config.n_embd)
def forward(self, x):
x = self.c_fc(x)
x = self.gelu(x)
return self.c_proj(x)
Обратите внимание тут candle.nn
, а не torch.nn
. С линейными слоями мы уже знакомы, а GeLU()
, это что такое?
Исследователи из OpenAI
, выяснили, что в их моделях такая функция работает лучше всего, посмотрим на её реализацию
class GeLU:
def __init__(self):
Parameter([self,[]])
def __call__(self, x):
return Tensor.gelu(x)
#return 0.5 * x * (1 + Tensor.tanh(0.79788456 * (x + 0.044715 * (x ** 3))))
Я закомментировал вторую строчку и вместо этого использую встроенный метод Tensor.gelu()
, но никто не запрещает и честно считать по формуле. Заглянем в этот метод
from scipy.stats import norm
@classmethod
def gelu(cls, x):
return x * norm.cdf(x.value) # формула из картинки
Теперь в линейном слое нам все понятно, идём дальше!
class CausalSelfAttetion(nn.Module):
def __init__(self, config):
super().__init__()
self.config = config
self.c_attn = nn.Linear(config.n_embd, 3 * config.n_embd, bias=True)
self.c_proj = nn.Linear(config.n_embd, config.n_embd, bias=True)
def forward(self, x):
B, T, C = x.shape
qkv = self.c_attn(x)
q, k, v = Tensor.split(qkv, 3, axis=2)
n_head = self.config.n_head
q = q.reshape(B, T, n_head, C // n_head).transpose(0, 2, 1, 3)
k = k.reshape(B, T, n_head, C // n_head).transpose(0, 2, 1, 3)
v = v.reshape(B, T, n_head, C // n_head).transpose(0, 2, 1, 3)
att = q @ k.transpose(0, -3, -1, -2) * (k.shape[-1] ** -0.5)
lg = att.local_gradients
att = Tensor.tril(att)
att[att == 0] = float('-inf')
att.local_gradients = lg
probs = Tensor.softmax(att, axis=-1)
y = probs @ v
y = y.transpose(0, 2, 1, 3).reshape(B, T, C)
y = self.c_proj(y)
return y
Слой внимания, я предполагаю, что уже видели подобную реализацию, поэтому много уделять внимания не буду. Видим, незнакомые нам методы Tensor.split(), Tensor.tril
, они делают тоже самое, что и их аналоги в pytorch
@classmethod
def tril(cls, input, diagonal=0):
value = np.tril(input.value, k=diagonal)
local_gradients = (
('tril', input, lambda x: x * np.tril(np.ones_like(input.value), k=diagonal)),
)
return cls(value, local_gradients=local_gradients)
@classmethod
def split(cls, array, split_size_or_sections, axis=0):
value = np.split(array.value, split_size_or_sections, axis=axis)
return cls(value, requires_grad=False)
Обратите внимание, я не определил вычисление градиента для второй операции. Значит, либо мне придётся его определить, либо я просто не смогу обучать модели!
class Block(nn.Module):
def __init__(self, config):
super().__init__()
self.ln_1 = nn.LayerNorm(config.n_embd)
self.attn = CausalSelfAttetion(config)
self.ln_2 = nn.LayerNorm(config.n_embd)
self.mlp = MLP(config)
def forward(self, x):
x = x + self.attn(self.ln_1(x))
x = x + self.mlp(self.ln_2(x))
return x
Тут я использую готовый слой candle.nn.LayerNorm()
, заглянем в него!
class LayerNorm:
def __init__(self, dim, eps=1e-5):
super().__init__()
self.param = None
self.gamma = Tensor.ones(dim, requires_grad=True)
self.beta = Tensor.zeros(dim, requires_grad=True)
self.eps = eps
self.all_layers = [self.gamma, self.beta]
self.grad = None
def __call__(self, x):
xmean = Tensor.mean(x, axis=2, keepdims=True)
xstd = Tensor.std(x, axis=2, keepdims=True)
x = (x - xmean) / (xstd + self.eps)
return self.gamma * x + self.beta
В целом ничего сложного, если вы идейно знакомы с методами нормализации. Тут я использую Tensor.mean(), Tensor.std()
. Посмотрим на их реализацию
@classmethod
def mean(cls, array, axis=None, keepdims=False):
if axis == None:
return Tensor.sum(array, axis=None, keepdims=keepdims) / np.size(array.value)
else:
delimeter = 1
if not isinstance(axis, int):
for ax in axis:
delimeter = delimeter * array.shape[ax]
else:
delimeter = array.shape[axis]
return Tensor.sum(array, axis=axis, keepdims=keepdims) / delimeter
@classmethod
def std(cls, array, axis=None, keepdims=False):
if axis == None or axis == 0:
mean = Tensor.mean(array, axis=axis, keepdims=False)
sub = array - mean
squared = sub ** 2
scaled_sum = Tensor.mean(squared, axis=axis, keepdims=keepdims)
std = Tensor.sqrt(scaled_sum)
return std
elif axis >= 1:
mean = Tensor.mean(array, axis=axis, keepdims=True)
sub = array - mean
squared = sub ** 2
scaled_sum = Tensor.mean(squared, axis=axis, keepdims=keepdims)
std = Tensor.sqrt(scaled_sum)
out = Tensor(std, local_gradients=None)
out.local_gradients = (('std', std, lambda x: x * array.shape[axis] / (array.shape[axis] - 1)),) # array.shape[axis] / (array.shape[axis] - 1) additional multiplier due to dissimilarity
return out
class GPT(nn.Module):
def __init__(self, config):
super().__init__()
self.config = config
self.wte = nn.Embedding(config.vocab_size, config.n_embd)
self.wpe = nn.Embedding(1024, config.n_embd)
self.h = nn.ModuleList([Block(config) for _ in range(config.n_layer)])
self.ln_f = nn.LayerNorm(config.n_embd)
self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
def forward(self, x):
B, T = x.shape
assert T >= self.config.block_size, f"Cannot forward the sequence of length {T}, block_size is smaller"
pos = Tensor.arange(T).reshape(1, -1)
pos_embd = self.wpe(pos)
tok_embd = self.wte(x)
x = pos_embd + tok_embd
for block in self.h:
x = block(x)
x = self.ln_f(x)
logits = self.lm_head(x)
return logits
Разберемся как работают candle.nn.Embedding() и candle.nn.ModuleList()
.
class Embedding:
def __init__(self, num_emb, emb_dim):
self.w = Tensor.randn((num_emb, emb_dim), requires_grad=True)
self.w.local_gradients = None
self.num_embd = num_emb
self.emb_dim = emb_dim
self.param = None
self.grad = None
self.all_layers = [self.w]
def __call__(self, x):
if self.param:
global Parameter
Parameter = self.param
def multiply_by_locgrad(path_value):
temp = np.zeros_like(self.w.value)
np.add.at(np.zeros_like(self.w.value), x.value, path_value)
return temp
x.value = x.value.astype(int)
local_gradients = (('embd', self.w, multiply_by_locgrad),)
return Tensor(self.w.value[x.value], local_gradients=local_gradients)
В целом ничего сложного, просто ожидаем на входе тензор из целых значений и рассматриваем эти значения как индексы для матрицы, хранящей эмбеддинги.
class ModuleList:
def __init__(self, layers):
self.layers = layers
self.index = 0
def __call__(self, x):
for layer in self.layers:
x = layer(x)
return x
def __len__(self):
return len(self.layers)
def __iter__(self):
self.index = 0
return self
def __next__(self):
if self.index < len(self.layers):
result = self.layers[self.index]
self.index += 1
return result
else:
raise StopIteration
def __getitem__(self, index):
return self.layers[index]
Оказывается ModuleList
это просто итератор!
Note! Для корректной работы, нам нужно определить метод Module.__setattr__()
, иначе слои которые находятся внутри ModuleList
, просто не будут видны нашей глобальной переменной Parameter
, я не буду на этом останавливаться, но при желании можно заглянуть в код и разобраться!
class GPT(nn.Module):
@classmethod
def from_pretrained(cls, model_type):
assert model_type in ('gpt2', 'gpt2-medium', 'gpt2-large', 'gpt2-xl')
config_args = {
'gpt2': dict(n_layer=12, n_head=12, n_embd=768),
'gpt2-medium': dict(n_layer=24, n_head=16, n_embd=1024),
'gpt2-large': dict(n_layer=36, n_head=20, n_embd=1280),
'gpt2-xl': dict(n_layer=48, n_head=25, n_embd=1600)
}[model_type]
config_args['vocab_size'] = 50257
config_args['block_size'] = 1024
config = GPTConfig(**config_args)
model = GPT(config)
model = GPT.get_params(model, model_type)
return model
Здесь мы определяем модель из библиотеки hf.transformers
, это самый простой путь для доступа к весам GPT2.
@staticmethod
def get_params(model, model_type):
from transformers import GPT2LMHeadModel, logging
logging.set_verbosity_error()
model_hf = GPT2LMHeadModel.from_pretrained(model_type)
sd_hf = model_hf.state_dict()
...
Это часть кода, отвечающего за перенос весов с модели huggingface
на нашу модель. Там ничего умного, кроме аккуратной работы.
Генерация
model = GPT.from_pretrained(model_type=model_type)
max_length = 100
number_of_examples = 3
starting_sentence = "Hello, I'm a language model,"
enc = tiktoken.get_encoding('gpt2')
tokens = enc.encode(starting_sentence)
tokens = Tensor(tokens, dtype=int)
tokens = Tensor.unsqueeze(tokens, 0)
tokens = Tensor.repeat(tokens, number_of_examples, 0)
x = tokens
topk = 50 # responsible for "creativity" or "adequacy"
for i in tqdm(range(max_length)):
logits = model(x)
logits = logits[:, -1, :]
probs = Tensor.softmax(logits, axis=-1)
topk_indices, topk_probs = Tensor.topk(probs, topk)
new_token = Tensor.multinomial_from_array(topk_indices, topk_probs, num_samples=1).reshape(-1, 1)
x = Tensor.cat([x, new_token], axis=1)
for sample in x:
sample = sample.value.astype(int).tolist()
print(enc.decode(sample), end='\n')
print('---------------')
Hello, I'm a language model, not a coding model, but a compiler model.
I am thinking of what it means to be a developer and a language model
and to be able to write in some simple yet expressive way code that makes it
possible to use it with our software in any meaningful way to make
applications more readable, better, more readable.
I can think of more words to talk about this.
In this post, I'm trying to explain some of the main differences
---------------
Hello, I'm a language model, based on my personal experience with code and development using C#.
This tutorial will help you create my own virtual language that you can use to implement things like an HTML page in your own app.
Now, what if I were going to learn how to program a website in an Objective-C program and create a web app out of it,
but didn't know how to do that and want to do some extra work to try it out? First I
---------------
Hello, I'm a language model, I'm my own language. Well, a language model has two parts:
a semantic construct and a structural construct.
To understand the relationship with the semantic construct, let's see how all of the variables on a graph come together.
Now I know that some of the variables on a graph represent numbers. But when you see the graphs, they're graphs.
In other words, you see the connections and they are networks. We have an example
Вы можете поиграться с этой моделью на каггле или скачав к себе из репозитория smolGPT
Вот и подошла к концу моя мини-серия по созданию собственной библиотеки на NumPy.
Благодарю за внимание!
Надеюсь вы нашли для себя этот цикл статей познавательным!
Если вам понравилось, пожалуйста, поделитесь ими, поставьте upvote и поставьте звёздочки моим реализациям на гитхабе.
Первая версия библиотеки