<jupyter_start><jupyter_text>PyTorch Tutorial> In this post, We will cover the basic tutorial while we use PyTorch. This is the summary of lecture CS285 "Deep Reinforcement Learning" from Berkeley.- toc: true - badges: true- comments: true- author: <NAME>- categories: [Python, PyTorch, Berkeley]- image:   IntroThis is a PyTorch Tutorial for UC Berkeley's CS285.There's already a bunch of great tutorials that you might want to check out, and in particular [this tutorial](https://pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html).This tutorial covers a lot of the same material. If you're familiar with PyTorch basics, you might want to skip ahead to the PyTorch Advanced section.First, let's import some things and define a useful plotting function<jupyter_code>import matplotlib.pyplot as plt
import torch
import numpy as np

def plot(xs, ys, xlim=(-3, 3), ylim=(-3, 3)):
    fig, ax = plt.subplots()
    ax.plot(xs, ys, linewidth=5)
    # ax.set_aspect('equal')
    ax.grid(True, which='both')

    ax.axhline(y=0, color='k')
    ax.axvline(x=0, color='k')
    ax.set_xlim(*xlim)
    ax.set_ylim(*ylim)<jupyter_output><empty_output><jupyter_text>PyTorch Basic  Tensors Numpy arrays are objects that allow you to store and manipulation matrices.<jupyter_code>shape = (2, 3)
x = np.zeros(shape)
y = np.ones(shape)
z = x + y
print(x)
print("+")
print(y)
print("=")
print(z)
print(z.sum())
print(z[0, 1:])<jupyter_output>[1. 1.]<jupyter_text>PyTorch is build around _tensors_, which play a similar role as numpy arrays. You can do many of the same operations in PyTorch:<jupyter_code>x = torch.zeros(shape)
y = torch.ones(shape)
z = x + y

print(x)
print("+")
print(y)
print("=")
print(z)<jupyter_output>tensor([[0., 0., 0.],
        [0., 0., 0.]])
+
tensor([[1., 1., 1.],
        [1., 1., 1.]])
=
tensor([[1., 1., 1.],
        [1., 1., 1.]])<jupyter_text>Many functions have alternate syntax that accomplish the same thing<jupyter_code>torch.add(x, y)
z.min()
z[1:, 0]
torch.sum(z)<jupyter_output><empty_output><jupyter_text>Function that reduce dimenions will by default reduce all dimensions unless a dimension is specified<jupyter_code>torch.sum(z, dim=1)
torch.sum(z, dim=0)<jupyter_output><empty_output><jupyter_text>Like numpy, pytorch will try to broadcast operations<jupyter_code>x = torch.ones((3, 1))
y = torch.ones((1, 3))
z = x + y

print(x)
print("+")
print(y)
print("=")
print(z)<jupyter_output>tensor([[1.],
        [1.],
        [1.]])
+
tensor([[1., 1., 1.]])
=
tensor([[2., 2., 2.],
        [2., 2., 2.],
        [2., 2., 2.]])<jupyter_text>Operations that end with an underscore denote in-place functions. Use these sparingly as they easily lead to bugs.<jupyter_code>print(z)
z.zero_()
print(z)
z.add_(5)
print(z)<jupyter_output>tensor([[5., 5., 5.],
        [5., 5., 5.],
        [5., 5., 5.]])<jupyter_text>Moving between numpy and PyTorch<jupyter_code>x_np = np.random.randn(*shape)
print(x_np)<jupyter_output>[[-0.13298262  0.72862306  0.95932965]
 [ 2.24109395  0.53208287 -0.42554932]]<jupyter_text>numpy -> pytorch is easy<jupyter_code>x = torch.from_numpy(x_np)
print(x)<jupyter_output>tensor([[-0.1330,  0.7286,  0.9593],
        [ 2.2411,  0.5321, -0.4255]], dtype=torch.float64)<jupyter_text>By default, numpy arrays are float64. You'll probably want to convert arrays to float32, as most tensors in pytorch are float32.<jupyter_code>x = torch.from_numpy(x_np).to(torch.float32)
print(x)<jupyter_output>tensor([[-0.1330,  0.7286,  0.9593],
        [ 2.2411,  0.5321, -0.4255]])<jupyter_text>pytorch -> numpy is also easy<jupyter_code>print(x.numpy())<jupyter_output>[[-0.13298263  0.7286231   0.95932966]
 [ 2.2410939   0.53208286 -0.42554933]]<jupyter_text>GPU support<jupyter_code>torch.cuda.is_available()<jupyter_output><empty_output><jupyter_text>The code below errors out because both tensors need to be on the same device.<jupyter_code>device = torch.device("cuda")
x = torch.zeros(shape)
y = torch.ones(shape, device=device)
z = x + y<jupyter_output><empty_output><jupyter_text>You can move a tensor to the GPU by using the `to` function.<jupyter_code>x = x.to(device)
z = x + y
print(z)<jupyter_output><empty_output><jupyter_text>This code also errors out, because you can't convert tensors on a GPU into numpy arrays directly.<jupyter_code>z.numpy()<jupyter_output><empty_output><jupyter_text>First you need to move them to the CPU.<jupyter_code>z_cpu = z.to('cpu')
z_cpu.numpy()<jupyter_output><empty_output><jupyter_text>(homework aside)<jupyter_code>from cs285.infrastructure import pytorch_util as ptu
ptu.from_numpy(x_np)
ptu.to_numpy(x)<jupyter_output><empty_output><jupyter_text>Neural-Network specific functionsPyTorch has a bunch of built-in funcitons.See [the docs](https://pytorch.org/docs/stable/torch.html) for a full list.<jupyter_code>xs = torch.linspace(-3, 3, 100)
ys = torch.relu(xs)
plot(xs.numpy(), ys.numpy())
xs = torch.linspace(-3, 3, 100)
ys = torch.tanh(xs)
plot(xs.numpy(), ys.numpy())
xs = torch.linspace(-3, 3, 100)
ys = torch.selu(xs)
plot(xs.numpy(), ys.numpy())<jupyter_output><empty_output><jupyter_text>Automatic differentiation Given some loss function$$L(\vec x, \vec y) = ||2 \vec x + \vec y||_2^2$$we want to evaluate$$\frac{\partial L}{\partial \vec x}$$and$$\frac{\partial L}{\partial \vec y}$$<jupyter_code>shape = (3, )
x = torch.tensor([1., 2, 3], requires_grad=True)
y = torch.ones(shape, requires_grad=True)<jupyter_output><empty_output><jupyter_text>PyTorch makes this easy by having tensors keep track of their data...<jupyter_code>x.data<jupyter_output><empty_output><jupyter_text>...and their gradient:<jupyter_code>print(x.grad)<jupyter_output>None<jupyter_text>However, right now `x` has no gradient because it does not know what loss it must be differentiated with respect to.Below, we define the loss.<jupyter_code>loss = ((2 * x + y)**2).sum()
print(loss)<jupyter_output>tensor(83., grad_fn=<SumBackward0>)<jupyter_text>And we perform back-propagation by calling `backward` on it.<jupyter_code>loss.backward()<jupyter_output><empty_output><jupyter_text>Now we see that the gradients are populated!<jupyter_code>print(x.grad)
print(y.grad)<jupyter_output>tensor([12., 20., 28.])
tensor([ 6., 10., 14.])<jupyter_text>gradients accumulateGradients accumulate, os if you call backwards twice...<jupyter_code>loss = ((2 * x + y)**2).sum()
loss.backward()<jupyter_output><empty_output><jupyter_text>...you'll get twice the gradient.<jupyter_code>print(x.grad)
print(y.grad)<jupyter_output>tensor([24., 40., 56.])
tensor([12., 20., 28.])<jupyter_text>multiple losses This accumulation makes it easy to add gradients from different losses, which might not even use the same parameters. For example, this loss is only a function of `x`....<jupyter_code>other_loss = (x**2).sum()
other_loss.backward()<jupyter_output><empty_output><jupyter_text>...and so only `x.grad` changes.<jupyter_code>print(x.grad)
print(y.grad)<jupyter_output>tensor([26., 44., 62.])
tensor([12., 20., 28.])<jupyter_text>stopping and starting gradients If you don't specify `required_grad=True`, the gradient will always be `None`.<jupyter_code>shape = (3, )
x = torch.tensor([1., 2, 3], requires_grad=True)
y = torch.ones(shape)
loss = ((2 * x + y)**2).sum()
loss.backward()
print(x.grad)
print(y.grad)<jupyter_output>tensor([12., 20., 28.])
None<jupyter_text>You can turn `required_grad` back on after initializing a tensor.<jupyter_code>shape = (3, )
x = torch.tensor([1., 2, 3], requires_grad=True)
y = torch.ones(shape)
y.requires_grad = True
loss = ((2 * x + y)**2).sum()
loss.backward()
print(x.grad)
print(y.grad)<jupyter_output>tensor([12., 20., 28.])
tensor([ 6., 10., 14.])<jupyter_text>You can cut a gradient by calling `y.detach()`, which will return a new tensor with `required_grad=False`. Note that `detach` is not an in-place operation!<jupyter_code>shape = (3, )
x = torch.tensor([1., 2, 3], requires_grad=True)
y = torch.ones(shape, requires_grad=True)
y_detached = y.detach()
loss = ((2 * x + y)**2).sum()
loss.backward()
print(x.grad)
print(y.grad)
print(y_detached)
shape = (3, )
x = torch.tensor([1., 2, 3], requires_grad=True)
y = torch.ones(shape, requires_grad=True)
z = 2 * x
z.required_grad = True
loss = ((2 * z + y)**2).sum()
loss.backward()
print(x.grad)
print(y.grad)
z.grad
torch.from_numpy(np.array([1,2]))<jupyter_output><empty_output><jupyter_text>Any difference between p.data.add_(-0.001 + p.grad) and p.data+= -0.001 + p.grad?  Modules![tmp.png](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA1IAAAFVCAMAAADSYi6kAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAMAUExURf///+Grqf39//79/dHp8dfN4XKRt8Xl7a23manByf3//63Dy9flvdjlvsPl7c3r8WSlubK/mdvnxafAyYp5nqXR3ZSFp8vp8c3dq6vCyfz+/dnR42+QuGCgsf79+cnn8M/frmmourK6l2+svWSkt3mxwtvv9mKhtG2qvMXVpcDSoNfju22js46/zbPU3F6dsNbt9K/P2dnlv9Pr9HKTt9Xjt93V54G5yWalt6vDy97px7O/l4u9zc/p8dbo72ant8jn79PhtHSvwJzH1J+7xWihsczo76S+yEd8vIS4x322xbJlZL/Z4fr7/WOitdjU4Ozq8L7f6NHL2XCls418oaWXt5XF09fP4418o72zx7LGzWyltm+PtdXL4WyNtW<KEY>2ZeJrY3Bz+jx9E+BvK+5mZ/L2K7Fy4CcroO3xfT47F6JtnWVsvPo54OgvOLs8YGuvYm5x/b7/O30+JGtwrzY3//9/XSUuH2sufP3+rd0dN3q8MLj7K/Fy+Hqy////pe9zbjG<KEY>) `nn.Modules` represent the building blocks of a computation graph.For example, in typical pytorch code, each convolution block above is its own module, each fully connected block is a module, and the whole network itself is also a module.Modules can contain modules within them.All the classes inside of `torch.nn` are instances `nn.Modules`.Below is an example definition of a module:<jupyter_code>import pdb
import torch
import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):

    def __init__(self, input_size, output_size):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(input_size, 32)
        self.fc2 = nn.Linear(32, 32)
        self.fc3 = nn.Linear(32, output_size)

    def forward(self, x):
        pdb.set_trace()
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x
    
    def hook(self, gradient):
        return 2*gradient<jupyter_output><empty_output><jupyter_text>The main function that you need to implement is the `forward` function.Otherwise, it's a normal Python object:<jupyter_code>net = Net(input_size=1, output_size=1)
print(net)<jupyter_output>Net(
  (fc1): Linear(in_features=1, out_features=32, bias=True)
  (fc2): Linear(in_features=32, out_features=32, bias=True)
  (fc3): Linear(in_features=32, out_features=1, bias=True)
)<jupyter_text>Here we create some dummy input. The first dimension will be the batch dimension.<jupyter_code>x = torch.linspace(-5, 5, 100).view(100, 1)
print(x.shape)<jupyter_output>torch.Size([100, 1])<jupyter_text>To evaluate a neural network on some input, you pass an input through a module by calling it directly. In particular, don't call `net.forward(x)`.<jupyter_code>y = net(x)
print(y.shape)<jupyter_output>> <ipython-input-45-7c9b43d6b070>(16)forward()
-> x = F.relu(self.fc1(x))<jupyter_text>Let's visualize what the networks looks like.<jupyter_code>plot(x.detach().numpy(), y.detach().numpy(), ylim=(-1, 1), xlim=(-5, 5))<jupyter_output><empty_output><jupyter_text>The network keeps track of all the parameters and gradients!<jupyter_code>print(net.fc1.bias.grad)<jupyter_output>None<jupyter_text>In the `__init__` function, any variable that you assign to `self` that is also a module will be automatically added as a sub-module. The parameters of a module (and all sub-modules) can be accessed through the `parameters()` function:<jupyter_code>for p in net.parameters():
    print(p.shape)<jupyter_output>torch.Size([32, 1])
torch.Size([32])
torch.Size([32, 32])
torch.Size([32])
torch.Size([1, 32])
torch.Size([1])<jupyter_text>WARNING: if you want to have a list of modules use```def __init__(self, network1, network2):  self.list = nn.ModuleList([network1, network 2])```and **not** ```def __init__(self, network1, network2):  self.list = [network1, network 2]```In the later case, `network1` and `network2` won't be added as sub-modules. The output of the module is just a tensor. We can perform operations on the tensor like before to automatically compute derivatives.For example, below, we minimize the sum-of-squares of the output.<jupyter_code>loss = (y**2).sum()
loss.backward()<jupyter_output><empty_output><jupyter_text>We can manually update the parameters by adding the gradient (times a negative learning rate) and zero'ing out the gradients to prevent gradient accumulation.<jupyter_code>for p in net.parameters():
    p.data.add_(-0.001 * p.grad)
    p.grad.data.zero_()<jupyter_output><empty_output><jupyter_text>And we can do this in a loop to train our network!<jupyter_code>class Net2(nn.Module):

    def __init__(self, input_size, output_size):
        super(Net2, self).__init__()
        self.fc1 = nn.Linear(input_size, 32)
        self.fc2 = nn.Linear(32, 32)
        self.fc3 = nn.Linear(32, output_size)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x
    
    def hook(self, gradient):
        return 2*gradient
    
net = Net2(input_size=1, output_size=1)
for _ in range(100):
    y = net(x)
    loss = (y**2).sum()
    loss.backward()
    for p in net.parameters():
        p.data.add_(- 0.001 * p.grad)
        p.grad.data.zero_()<jupyter_output><empty_output><jupyter_text>Sure enough, our network learns to set everything to zero.<jupyter_code>plot(x.detach().numpy(), y.detach().numpy(), ylim=(-1, 1), xlim=(-5, 5))<jupyter_output><empty_output><jupyter_text>Loss FunctionsPyTorch has a bunch of built in loss functions, which are just other modules that you can pass your data through.<jupyter_code>y_target = torch.sin(x)
loss_fn = nn.SmoothL1Loss()
for _ in range(1000):
    y = net(x)
    loss = loss_fn(y, y_target)
    loss.backward()
    for p in net.parameters():
        p.data.add_(- 0.001 * p.grad)
        p.grad.data.zero_()
plot(x.detach().numpy(), y.detach().numpy(), ylim=(-1, 1), xlim=(-5, 5))<jupyter_output><empty_output><jupyter_text>Optimizers We can use more fancy optimizers with the `optim` package.<jupyter_code>from torch import optim

net = Net2(input_size=1, output_size=1)

optimizer = optim.Adam(net.parameters(), lr=1e-3)

x = torch.linspace(-5, 5, 100).view(-1, 1)
y = net(x)
y_target = torch.sin(x)
loss_fn = nn.MSELoss()<jupyter_output><empty_output><jupyter_text>Here's the network before training<jupyter_code>plot(x.detach().numpy(), y.detach().numpy(), ylim=(-1, 1), xlim=(-5, 5))<jupyter_output><empty_output><jupyter_text>and here's how you can use the optimize to train the network.Note that we call `zero_grad` _before_ calling `loss.backward()`, and then we just call `optimizer.step()`. This `step` function will take care of updating all the parameters that were passed to that optimizer's constructor.<jupyter_code>for _ in range(100):
    y = net(x)
    loss = loss_fn(y, y_target)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()<jupyter_output><empty_output><jupyter_text>And we see that this trained a network quite well<jupyter_code>plot(x.detach().numpy(), y.detach().numpy(), ylim=(-1, 1), xlim=(-5, 5))<jupyter_output><empty_output><jupyter_text>Regression example We're going to train two separate neural networks to solve a prediction task:$$f_\theta(x) \approx y$$First we generate the data<jupyter_code>N = 100
d = 1
X = np.random.randn(N, 1)
Y = X * 2 + 3 + np.random.randn(N, 1)
plt.scatter(X, Y)
plt.show()
print(X.shape, Y.shape)<jupyter_output>(100, 1) (100, 1)<jupyter_text>Next we convert that data to pytorch<jupyter_code>X_pt = torch.from_numpy(X).to(torch.float32)
Y_pt = torch.from_numpy(Y).to(torch.float32)

loss_fn = nn.MSELoss()<jupyter_output><empty_output><jupyter_text>Define the training loop<jupyter_code>def train(net: nn.Module):
    optimizer = optim.SGD(net.parameters(), lr=1e-2)
    losses = []
    for _  in range(100):
        Y_hat_pt = net(X_pt)
        loss = loss_fn(Y_hat_pt, Y_pt) 
  
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
  
        losses.append(loss.detach().numpy())
    return np.array(losses)<jupyter_output><empty_output><jupyter_text>Let's test two different networks<jupyter_code>linear_network = nn.Linear(1, 1)
linear_losses = train(linear_network)

non_linear_network = Net2(1, 1)
non_linear_losses = train(non_linear_network)<jupyter_output><empty_output><jupyter_text>and plot the losses and predictions.<jupyter_code>plt.figure()
t = np.arange(len(linear_losses))
plt.plot(t, linear_losses, t, non_linear_losses)
plt.legend(['linear', 'non_linear'])
plt.show()
x_probe = torch.linspace(X.min(), X.max(), 300).reshape(-1, 1)
y_lin = linear_network(x_probe).detach().numpy()
y_non_lin = non_linear_network(x_probe).detach().numpy()

plt.scatter(x_probe.numpy(), y_lin, s=3)
plt.scatter(x_probe.numpy(), y_non_lin, s=3)
plt.scatter(X, Y)
plt.legend(['linear', 'non_linear', 'data'])
plt.show()<jupyter_output><empty_output><jupyter_text>PyTorch - Advanced  DistributionsPyTorch has a very convenient [distributions](https://pytorch.org/docs/stable/distributions.html) package.<jupyter_code>from torch import distributions<jupyter_output><empty_output><jupyter_text>You create distributions by passing the parameters of the distribution.<jupyter_code>mean = torch.zeros(1, requires_grad=True)
std = torch.ones(1, requires_grad=True)
gaussian = distributions.Normal(mean, std)<jupyter_output><empty_output><jupyter_text>These distributions are instances of the more general `Distribution` class, which you can read more about [here](https://pytorch.org/docs/stable/distributions.htmldistribution).<jupyter_code>print(gaussian)
print(isinstance(gaussian, distributions.Distribution))
sample = gaussian.sample((1,))
print(sample)
gaussian.log_prob(sample)<jupyter_output><empty_output><jupyter_text>The log probability depends on the the parameters of the distribution. So, calling `backward` on a loss that depends on `log_prob` will back-propagate gradients into the parmaeters of the distribution.NOTE: this won't back-propagate through the samples (the "reparatermization trick''), unless you use `rsample`, which is only implemented for some distributions. at 5:18, we can set loss to -log_prob to maximize the probability of an event. That makes sense to me because the higher prob is, the smaller loss would be. Now if we want to incorporate reward into this loss function, i think usually people do loss=-log_prob*reward? but that means the higher the reward is, the higher the loss is. Is this because we want to push the prob of the event to be higher with a higher loss when we have a higher reward? or should we have lower loss with higher reward (loss=log_prob*reward)? $$s_1, a_1, s_2, a_2, ...$$<jupyter_code>loss = - gaussian.log_prob(sample).sum()
loss.backward()
mean.grad<jupyter_output><empty_output><jupyter_text>Batch-Wise distribution The distributions also support batch-operations. In this case, all the operations (`sample`, `log_prob`, etc.) are batch-wise.<jupyter_code>mean = torch.zeros(10)
std = torch.ones(10)
gaussian = distributions.Normal(mean, std)
gaussian
sample = gaussian.sample((1,))
print(sample)
gaussian.log_prob(sample)<jupyter_output><empty_output><jupyter_text>Multivariate Normal There are other distributions<jupyter_code>mean = torch.zeros(2)
covariance = torch.tensor(
    [[1, 0.8],
     [0.8, 1]]
)
gaussian = distributions.MultivariateNormal(mean, covariance)
gaussian.sample((1,))
samples = gaussian.sample((500,))
plt.scatter(samples[:, 0].numpy(), samples[:, 1].numpy())<jupyter_output><empty_output><jupyter_text>NOTE: if you want to use a batch of `MultivariateNormal` distributions, you'll need to construct a batch of covariance matrices (i.e. shape `[BATCH_SIZE, DIM, DIM]`).  Categorical Distribution<jupyter_code>from torch import distributions<jupyter_output><empty_output><jupyter_text>Another useful distribution is the categorical distribution.<jupyter_code>probs = torch.tensor([0.1, 0.2, 0.7])
dist = distributions.Categorical(probs=probs)
sample = dist.sample([20])
print(sample)
dist.log_prob(sample)<jupyter_output><empty_output><jupyter_text>Distributions and Modules Typically, your network will output parameters of a distribution<jupyter_code>import torch
import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):

    def __init__(self, input_size, output_size):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(input_size, 32)
        self.fc2 = nn.Linear(32, 32)
        self.fc3 = nn.Linear(32, output_size)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x
mean_network = Net(1, 1)
x = torch.randn(100, 1)
mean = mean_network(x)
distribution = distributions.Normal(x, scale=1)<jupyter_output><empty_output><jupyter_text>If you want, your nn.Module can return a distribution in the `forward` function!<jupyter_code>import torch
import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):

    def __init__(self, input_size, output_size):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(input_size, 32)
        self.fc2 = nn.Linear(32, 32)
        self.fc3 = nn.Linear(32, output_size)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return distributions.Normal(x, scale=1)
distribution_network = Net(1, 1)
x = torch.randn(100, 1)
distribution = distribution_network(x)<jupyter_output><empty_output><jupyter_text>`gather`This function will be useful for the DQN assignment.It allows you to index into arrays in a batch.<jupyter_code>x = torch.arange(6).reshape(2, 3)
y = torch.tensor([0, 1]).reshape(2, 1)
print(x)
print(y)
print(torch.gather(x, 0, y))
x = torch.arange(6).reshape(2, 3)
y = torch.tensor([0, 1, 0]).reshape(1, 3)
print(x)
print(y)
print(torch.gather(x, 0, y))<jupyter_output>tensor([[0, 1, 2],
        [3, 4, 5]])
tensor([[0, 1, 0]])
tensor([[0, 4, 2]])
{'fraction_non_alphanumeric': 0.14083291702620465, 'fraction_numerical': 0.036105181888369135, 'mean_word_length': 5.262798634812286, 'pattern_counts': {'":': 0, '<': 236, '<?xml version=': 0, '>': 241, 'https://': 4, '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': '63', 'dir': 'jupyter-structured-clean-dedup', 'id': '410034', 'license': 'Apache-2.0', 'n_tokens_mistral': 8707, 'n_tokens_neox': 8182, 'n_words': 1942, 'path': '_notebooks/2021-01-06-pytorch-tutorial.ipynb', 'repo_name': 'uplandr/uplandr.github.io', 'size': '74158'}