跳至内容

快速入门指南¤

本指南假设读者没有pytorch或其他深度学习框架的基础知识,但具备一些神经网络的基本概念。 旨在快速概述tinygrad提供的高级API。

本指南也以教程形式组织,通过它您最终将获得一个可以识别手写数字的工作模型。

我们需要一些导入来开始:

import numpy as np
from tinygrad.helpers import Timing

张量¤

张量是tinygrad中的基础数据结构。它们可以被视为特定数据类型的多维数组。 tinygrad中的所有高级操作都作用于这些张量。

张量类可以像这样导入:

from tinygrad import Tensor

张量可以从现有的数据结构创建,比如Python列表或NumPy ndarray:

t1 = Tensor([1, 2, 3, 4, 5])
na = np.array([1, 2, 3, 4, 5])
t2 = Tensor(na)

张量也可以通过多种工厂方法之一来创建:

full = Tensor.full(shape=(2, 3), fill_value=5) # create a tensor of shape (2, 3) filled with 5
zeros = Tensor.zeros(2, 3) # create a tensor of shape (2, 3) filled with 0
ones = Tensor.ones(2, 3) # create a tensor of shape (2, 3) filled with 1

full_like = Tensor.full_like(full, fill_value=2) # create a tensor of the same shape as `full` filled with 2
zeros_like = Tensor.zeros_like(full) # create a tensor of the same shape as `full` filled with 0
ones_like = Tensor.ones_like(full) # create a tensor of the same shape as `full` filled with 1

eye = Tensor.eye(3) # create a 3x3 identity matrix
arange = Tensor.arange(start=0, stop=10, step=1) # create a tensor of shape (10,) filled with values from 0 to 9

rand = Tensor.rand(2, 3) # create a tensor of shape (2, 3) filled with random values from a uniform distribution
randn = Tensor.randn(2, 3) # create a tensor of shape (2, 3) filled with random values from a standard normal distribution
uniform = Tensor.uniform(2, 3, low=0, high=10) # create a tensor of shape (2, 3) filled with random values from a uniform distribution between 0 and 10

还有更多类似的工厂方法,您可以在Tensor Creation文件中找到它们。

所有张量创建方法都可以接受一个dtype参数来指定张量的数据类型,支持的dtype类型可在dtypes中找到。

from tinygrad import dtypes

t3 = Tensor([1, 2, 3, 4, 5], dtype=dtypes.int32)

张量允许您对它们执行如下操作:

t4 = Tensor([1, 2, 3, 4, 5])
t5 = (t4 + 1) * 2
t6 = (t5 * t4).relu().log_softmax()

所有这些操作都是惰性的,只有在您使用.realize().numpy()实现张量时才会执行。

print(t6.numpy())
# [-56. -48. -36. -20.   0.]

可以对张量执行更多操作,您可以在Tensor Ops文件中找到它们。 此外,阅读abstractions2.py将帮助您理解这些张量上的操作如何传递到您的硬件。

模型¤

在tinygrad中,神经网络实际上仅由对张量执行的操作表示。 这些操作通常被分组到类的__call__方法中,这样可以实现这些操作组的模块化和重用。 这些类不需要继承任何基类,事实上如果它们不需要任何可训练参数,甚至不需要是一个类!

一个例子是nn.Linear类,它表示神经网络中的一个线性层。

class Linear:
  def __init__(self, in_features, out_features, bias=True, initialization: str='kaiming_uniform'):
    self.weight = getattr(Tensor, initialization)(out_features, in_features)
    self.bias = Tensor.zeros(out_features) if bias else None

  def __call__(self, x):
    return x.linear(self.weight.transpose(), self.bias)

nn中已经实现了更多的神经网络模块,你也可以实现自己的模块。

我们将实现一个简单的神经网络,能够对MNIST数据集中的手写数字进行分类。 我们的分类器将是一个简单的2层神经网络,使用Leaky ReLU激活函数。 它将使用128个节点的隐藏层和10个节点的输出层(每个数字对应一个输出),且两个线性层都不使用偏置项。

class TinyNet:
  def __init__(self):
    self.l1 = Linear(784, 128, bias=False)
    self.l2 = Linear(128, 10, bias=False)

  def __call__(self, x):
    x = self.l1(x)
    x = x.leaky_relu()
    x = self.l2(x)
    return x

net = TinyNet()

我们可以看到,神经网络的前向传播只是对输入张量x执行的一系列操作。 我们还可以看到,像leaky_relu这样的函数操作并没有被定义为类,而是可以直接调用的方法。 最后,我们只需初始化神经网络的一个实例,就可以开始训练它了。

训练¤

现在我们已经定义了神经网络,可以开始训练它了。 在tinygrad中训练神经网络非常简单。 我们只需要定义神经网络、定义损失函数,然后在损失函数上调用.backward()来计算梯度。 然后可以使用这些梯度,通过众多Optimizers中的一个来更新神经网络的参数。

对于我们的损失函数,我们将使用稀疏分类交叉熵损失。下面的实现取自tensor.py,这里复制出来是为了强调tinygrad的一个重要细节。

def sparse_categorical_crossentropy(self, Y, ignore_index=-1) -> Tensor:
    loss_mask = Y != ignore_index
    y_counter = Tensor.arange(self.shape[-1], dtype=dtypes.int32, requires_grad=False, device=self.device).unsqueeze(0).expand(Y.numel(), self.shape[-1])
    y = ((y_counter == Y.flatten().reshape(-1, 1)).where(-1.0, 0) * loss_mask.reshape(-1, 1)).reshape(*Y.shape, self.shape[-1])
    return self.log_softmax().mul(y).sum() / loss_mask.sum()

正如我们在这个交叉熵损失函数的实现中所看到的,tinygrad原生不支持某些操作。 tinygrad原生不支持加载/存储操作,因为它们在尝试移植到不同后端时会增加复杂性,90%的模型不需要使用它们,而且可以像上面那样通过arange掩码来实现。

对于我们的优化器,我们将使用传统的随机梯度下降优化器,学习率为3e-4。

from tinygrad.nn.optim import SGD

opt = SGD([net.l1.weight, net.l2.weight], lr=3e-4)

我们可以看到,我们正在将神经网络的参数传递给优化器。 这是因为优化器需要知道要更新哪些参数。 有一种更简单的方法,只需使用get_parameters(net)tinygrad.nn.state中获取,它将返回神经网络中所有参数的列表。 这里明确列出参数是为了清晰起见。

现在我们已经定义了网络、损失函数和优化器,唯一缺少的就是训练数据! tinygrad中有几个数据集加载器,位于/extra/datasets。 我们将使用MNIST数据集加载器。

from extra.datasets import fetch_mnist

现在我们已经准备好开始训练我们的神经网络了。 我们将以64的批量大小进行1000步的训练。

我们使用with Tensor.train()在训练期间将内部标志Tensor.training设置为True。 退出时,上下文管理器会将标志恢复为其先前的值。

X_train, Y_train, X_test, Y_test = fetch_mnist()

with Tensor.train():
  for step in range(1000):
    # random sample a batch
    samp = np.random.randint(0, X_train.shape[0], size=(64))
    batch = Tensor(X_train[samp], requires_grad=False)
    # get the corresponding labels
    labels = Tensor(Y_train[samp])

    # forward pass
    out = net(batch)

    # compute loss
    loss = sparse_categorical_crossentropy(out, labels)

    # zero gradients
    opt.zero_grad()

    # backward pass
    loss.backward()

    # update parameters
    opt.step()

    # calculate accuracy
    pred = out.argmax(axis=-1)
    acc = (pred == labels).mean()

    if step % 100 == 0:
      print(f"Step {step+1} | Loss: {loss.numpy()} | Accuracy: {acc.numpy()}")

评估¤

现在我们已经训练好了神经网络,可以在测试集上对其进行评估。我们将使用相同的64批次大小,并评估其中的1000个批次。

with Timing("Time: "):
  avg_acc = 0
  for step in range(1000):
    # random sample a batch
    samp = np.random.randint(0, X_test.shape[0], size=(64))
    batch = Tensor(X_test[samp], requires_grad=False)
    # get the corresponding labels
    labels = Y_test[samp]

    # forward pass
    out = net(batch)

    # calculate accuracy
    pred = out.argmax(axis=-1).numpy()
    avg_acc += (pred == labels).mean()
  print(f"Test Accuracy: {avg_acc / 1000}")

就这样了¤

强烈建议您查看examples/文件夹以获取更多使用tinygrad的示例。 阅读tinygrad的源代码也是了解其工作原理的好方法。 特别是test/中的测试用例,是学习如何使用不同操作及其语义的绝佳场所。 在models/中还实现了一系列模型,您可以用作参考。

此外,欢迎在discord#learn-tinygrad频道中提问。不要问能不能问,直接问就行!

附加功能¤

JIT¤

此外,通过使用JIT可以加速某些神经网络的计算。 目前,这不支持输入尺寸变化的模型和非tinygrad操作。

要使用JIT,我们只需在神经网络的前向传播函数上添加一个装饰器,并确保输入和输出是已实现的张量。 或者在本例中,我们将创建一个包装函数并对该包装函数进行装饰,以加速神经网络的评估。

from tinygrad import TinyJit

@TinyJit
def jit(x):
  return net(x).realize()

with Timing("Time: "):
  avg_acc = 0
  for step in range(1000):
    # random sample a batch
    samp = np.random.randint(0, X_test.shape[0], size=(64))
    batch = Tensor(X_test[samp], requires_grad=False)
    # get the corresponding labels
    labels = Y_test[samp]

    # forward pass with jit
    out = jit(batch)

    # calculate accuracy
    pred = out.argmax(axis=-1).numpy()
    avg_acc += (pred == labels).mean()
  print(f"Test Accuracy: {avg_acc / 1000}")

你会发现评估时间比以前快得多,而且加速器利用率也大幅提高。

保存和加载模型¤

tinygrad的标准权重格式是safetensors。这意味着您可以将任何使用safetensors格式的模型权重加载到tinygrad中。 在state.py中有函数可以保存和加载这种格式的模型。

from tinygrad.nn.state import safe_save, safe_load, get_state_dict, load_state_dict

# first we need the state dict of our model
state_dict = get_state_dict(net)

# then we can just save it to a file
safe_save(state_dict, "model.safetensors")

# and load it back in
state_dict = safe_load("model.safetensors")
load_state_dict(net, state_dict)

models/ 文件夹中的许多模型都有一个 load_from_pretrained 方法,可以为您下载并加载权重。这些通常是 PyTorch 权重,意味着您需要安装 PyTorch 才能加载它们。

环境变量¤

存在一系列环境变量可以控制tinygrad的运行时行为。 其中一些常见的包括DEBUG和不同的后端启用变量。

您可以在env_vars.md中找到完整列表及其描述。

可视化计算图¤

可以使用VIZ=1来可视化神经网络的计算图。