GPT1 – Improving Language Understanding by Generative Pre-Training

GPT1 – Improving Language Understanding by Generative Pre-Training GPT1 – Improving Language Understanding by Generative Pre-Training

Aviso: Este post foi traduzido para o português usando um modelo de tradução automática. Por favor, me avise se encontrar algum erro.

Artigolink image 31

Melhorando a Compreensão de Linguagem por Pré-Treinamento Gerativo é o paper do GPT1. Antes de ler o post, é necessário que você se situe: antes do GPT, os modelos de linguagem estavam baseados em redes recorrentes (RNN), que eram redes que funcionavam relativamente bem para tarefas específicas, mas com as quais não era possível reutilizar o pré-treinamento para fazer um fine tuning para outras tarefas. Além disso, elas não tinham muita memória, então se fossem alimentadas com frases muito longas, não lembravam muito bem o início da frase.

Arquiteturalink image 32

Antes de falar da arquitetura do GPT1, vamos lembrar como era a arquitetura dos transformers.

arquitetura do transformer

GPT1 é um modelo baseado nos decodificadores dos transformers, então, como não temos codificador, a arquitetura de um único decodificador fica da seguinte maneira

decoder architecture

O mecanismo de atenção entre a sentença do encoder e do decoder é removido.

No o artigo do GPT1 propõem a seguinte arquitetura

arquitetura do gpt1

Que corresponde ao decoder de um transformer como vimos anteriormente, executado 12 vezes

Resumo do artigolink image 33

As ideias mais interessantes do paper são:

  • O modelo é treinado em um grande corpus de texto sem supervisão. Com isso, cria-se um modelo de linguagem. Cria-se um modelo de linguagem de alta capacidade em um grande corpus de texto.
  • Em seguida, é feito um fine-tuning em tarefas de NLP supervisionadas com conjuntos de dados rotulados. Realiza-se um ajuste fino em uma tarefa objetivo com supervisão. Além disso, quando o modelo é avaliado na tarefa supervisionada, não é avaliado apenas por essa tarefa, mas também pelo quão bem ele prevê o próximo token, o que ajuda a melhorar a generalização do modelo supervisionado e faz com que o modelo convirja mais rapidamente.
  • Embora já tenhamos mencionado, no artigo diz que é utilizada a arquitetura transformer, pois até aquele momento eram usadas RNN para os modelos de linguagem. Isso resultou em uma melhoria na qual o aprendizado do primeiro treinamento (treinamento no corpus de texto sem supervisão) é mais fácil de transferir para tarefas supervisionadas. Ou seja, graças ao uso de transformers, foi possível realizar um treinamento em todo um corpus de texto e, posteriormente, ajustes finos em tarefas supervisionadas.
  • Avaliaram o modelo em quatro tipos de tarefas de compreensão da linguagem:
  • Inferência da linguagem natural* Resposta a perguntas
  • Similaridade semântica
  • Classificação de textos.
  • O modelo geral (treinado em todo o corpus de texto sem supervisão) supera os modelos RNN treinados discriminativamente que utilizam arquiteturas projetadas especificamente para cada tarefa, melhorando significativamente o estado da arte em 9 das 12 tarefas estudadas. Eles também analisaram os comportamentos de "tiro zero" do modelo pré-treinado em quatro ambientes diferentes e demonstraram que ele adquire um conhecimento linguístico útil para as tarefas subsequentes.
  • Nos últimos anos, os pesquisadores haviam demonstrado os benefícios de utilizar embeddings, que são treinados em corpora não anotados, para melhorar o desempenho em uma variedade de tarefas. No entanto, essas abordagens transferem principalmente informações a nível de palavra, enquanto o uso de transformers treinados em grandes corpora de texto sem supervisão captura a semântica de nível superior, a nível de frase.

Geração de textolink image 34

Vamos a ver como gerar texto com um GPT1 pré-treinado

Primeiro é necessário instalar ftfy e spacy através de

pip install ftfy spacy

Uma vez instaladas, você deve baixar o modelo de linguagem do spaCy que deseja usar. Por exemplo, para baixar o modelo em inglês, você pode executar:

python -m spacy download en_core_web_sm

Para gerar texto vamos utilizar o modelo do repositório GPT1 da Hugging Face.

Importamos as bibliotecas

	
import torch
from transformers import OpenAIGPTTokenizer, OpenAIGPTLMHeadModel, AutoTokenizer
Copy

Se você reparar, importamos OpenAIGPTTokenizer e AutoTokenizer. Isso é porque na model card do GPT1 indica-se que se deve usar OpenAIGPTTokenizer, mas no post da biblioteca transformers explicamos que se deve usar AutoTokenizer para carregar o tokenizador. Então, vamos testar os dois.

	
ckeckpoints = "openai-community/openai-gpt"
tokenizer = OpenAIGPTTokenizer.from_pretrained(ckeckpoints)
auto_tokenizer = AutoTokenizer.from_pretrained(ckeckpoints)
input_tokens = tokenizer("Hello, my dog is cute and", return_tensors="pt")
input_auto_tokens = auto_tokenizer("Hello, my dog is cute and", return_tensors="pt")
print(f"input tokens: {input_tokens}")
print(f"input auto tokens: {input_auto_tokens}")
Copy
	
input tokens:
{'input_ids': tensor([[3570, 240, 547, 2585, 544, 4957, 488]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1]])}
input auto tokens:
{'input_ids': tensor([[3570, 240, 547, 2585, 544, 4957, 488]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1]])}

Como pode ser visto com os dois tokenizadores, são obtidos os mesmos tokens. Então, para que o código seja mais geral, de forma que se os checkpoints forem alterados, não seja necessário alterar o código, vamos utilizar AutoTokenizer.

Criamos então o dispositivo, o tokenizador e o modelo

	
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
tokenizer = AutoTokenizer.from_pretrained(ckeckpoints)
model = OpenAIGPTLMHeadModel.from_pretrained(ckeckpoints).to(device)
Copy

Como instanciamos o modelo, vamos a ver quantos parâmetros ele tem.

	
params = sum(p.numel() for p in model.parameters())
print(f"Number of parameters: {round(params/1e6)}M")
Copy
	
Number of parameters: 117M

Na era dos trilhões de parâmetros, podemos ver que o GPT1 tinha apenas 117 milhões de parâmetros.

Criamos os tokens de entrada para o modelo

	
input_sentence = "Hello, my dog is cute and"
input_tokens = tokenizer(input_sentence, return_tensors="pt").to(device)
input_tokens
Copy
	
{'input_ids': tensor([[3570, 240, 547, 2585, 544, 4957, 488]], device='cuda:0'), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1]], device='cuda:0')}

Passamo-los ao modelo para gerar os tokens de saída

	
output_tokens = model.generate(**input_tokens)
print(f"output tokens: {output_tokens}")
Copy
	
output tokens:
tensor([[ 3570, 240, 547, 2585, 544, 4957, 488, 249, 719, 797,
485, 921, 575, 562, 246, 1671, 239, 244, 40477, 244]],
device='cuda:0')
	
/home/wallabot/miniconda3/envs/nlp/lib/python3.11/site-packages/transformers/generation/utils.py:1178: UserWarning: Using the model-agnostic default `max_length` (=20) to control the generation length. We recommend setting `max_new_tokens` to control the maximum length of the generation.
warnings.warn(

Decodificamos os tokens para obter a frase de saída

	
decoded_output = tokenizer.decode(output_tokens[0], skip_special_tokens=True)
print(f"decoded output: {decoded_output}")
Copy
	
decoded output:
hello, my dog is cute and i'm going to take him for a walk. "
"

Já conseguimos gerar texto com GPT1

Gerar texto token a tokenlink image 35

Busca gulosalink image 36

Nós usamos model.generate para gerar os tokens de saída de uma só vez, mas vamos ver como gerá-los um a um. Para isso, em vez de usar model.generate vamos usar model, que na verdade chama o método model.forward

	
outputs = model(**input_tokens)
outputs
Copy
	
CausalLMOutput(loss=None, logits=tensor([[[ -5.9486, -5.8697, -18.4258, ..., -9.7371, -10.4495, 0.8814],
[ -6.1212, -4.8031, -14.3970, ..., -6.5411, -9.5051, -1.2015],
[ -7.4231, -6.3615, -14.7297, ..., -10.4575, -8.4600, -1.5183],
...,
[ -5.4751, -5.8803, -13.7767, ..., -10.5048, -12.4167, -6.1584],
[ -7.2052, -6.0198, -21.5040, ..., -16.2941, -14.0494, -1.2416],
[ -7.7240, -7.3631, -17.3174, ..., -12.1546, -12.3327, -1.7169]]],
device='cuda:0', grad_fn=<UnsafeViewBackward0>), hidden_states=None, attentions=None)

Vemos que saí muitos dados, primeiro vamos ver as keys da saída

	
outputs.keys()
Copy
	
odict_keys(['logits'])

Neste caso, temos apenas os logits do modelo, vamos verificar seu tamanho.

	
logits = outputs.logits
logits.shape
Copy
	
torch.Size([1, 7, 40478])

Vamos ver quantos tokens tínhamos na entrada

	
input_tokens.input_ids.shape
Copy
	
torch.Size([1, 7])

Bem, na saída temos o mesmo número de logits que na entrada. Isso é normal.

Obtemos os logits da última posição da saída

	
nex_token_logits = logits[0,-1]
nex_token_logits.shape
Copy
	
torch.Size([40478])

Há um total de 40478 logits, ou seja, há um vocabulário de 40478 tokens e temos que ver qual é o token com a maior probabilidade. Para isso, primeiro calculamos a softmax.

	
softmax_logits = torch.softmax(nex_token_logits, dim=0)
softmax_logits.shape
Copy
	
torch.Size([40478])
	
next_token_prob, next_token_id = torch.max(softmax_logits, dim=0)
next_token_prob, next_token_id
Copy
	
(tensor(0.1898, device='cuda:0', grad_fn=<MaxBackward0>),
tensor(249, device='cuda:0'))

Obtivemos o seguinte token, agora vamos decodificá-lo.

	
tokenizer.decode(next_token_id.item())
Copy
	
'i'

Obtivemos o seguinte token através do método greedy, ou seja, o token com maior probabilidade. Mas já vimos no post da biblioteca transformers as formas de gerar textos que podem ser feitas sampling, top-k, top-p, etc.

Vamos a colocar tudo dentro de uma função e ver o que sai se gerarmos alguns tokens

	
def generate_next_greedy_token(input_sentence, tokenizer, model, device):
input_tokens = tokenizer(input_sentence, return_tensors="pt").to(device)
outputs = model(**input_tokens)
logits = outputs.logits
nex_token_logits = logits[0,-1]
softmax_logits = torch.softmax(nex_token_logits, dim=0)
next_token_prob, next_token_id = torch.max(softmax_logits, dim=0)
return next_token_prob, next_token_id
Copy
	
def generate_greedy_text(input_sentence, tokenizer, model, device, max_length=20):
generated_text = input_sentence
for _ in range(max_length):
next_token_prob, next_token_id = generate_next_greedy_token(generated_text, tokenizer, model, device)
generated_text += tokenizer.decode(next_token_id.item())
return generated_text
Copy

Agora geramos texto

	
generate_greedy_text("Hello, my dog is cute and", tokenizer, model, device)
Copy
	
'Hello, my dog is cute andi." '

A saída é bastante repetitiva, como já foi visto nas formas de gerar textos

Ajuste fino do GPTlink image 37

Cálculo da perdalink image 38

Antes de começar a fazer o fine tuning do GPT1, vamos ver uma coisa. Antes, quando obtínhamos a saída do modelo, fazíamos isso

	
outputs = model(**input_tokens)
outputs
Copy
	
CausalLMOutput(loss=None, logits=tensor([[[ -5.9486, -5.8697, -18.4258, ..., -9.7371, -10.4495, 0.8814],
[ -6.1212, -4.8031, -14.3970, ..., -6.5411, -9.5051, -1.2015],
[ -7.4231, -6.3615, -14.7297, ..., -10.4575, -8.4600, -1.5183],
...,
[ -5.4751, -5.8803, -13.7767, ..., -10.5048, -12.4167, -6.1584],
[ -7.2052, -6.0198, -21.5040, ..., -16.2941, -14.0494, -1.2416],
[ -7.7240, -7.3631, -17.3174, ..., -12.1546, -12.3327, -1.7169]]],
device='cuda:0', grad_fn=<UnsafeViewBackward0>), hidden_states=None, attentions=None)

Pode-se ver que obtemos loss=None

	
print(outputs.loss)
Copy
	
None

Como vamos a precisar da loss para fazer o fine tuning, vamos a ver como obtê-la.

Se nós formos à documentação do método forward de OpenAIGPTLMHeadModel, podemos ver que diz que na saída retorna um objeto do tipo transformers.modeling_outputs.CausalLMOutput, então se formos à documentação de transformers.modeling_outputs.CausalLMOutput, podemos ver que diz que retorna loss se for passado labels para o método forward.

Se nós formos à fonte do código do método forward, vemos este bloco de código

perda = None
se labels não for None:
# Deslocar para que os tokens < n prevejam n
shift_logits = lm_logits[..., :-1, :].contiguous()
shift_labels = labels[..., 1:].contiguous()
# Aplanar os tokens
loss_fct = CrossEntropyLoss()
loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))

Isso significa que a loss é calculada da seguinte maneira

  • Shift de logits e labels: A primeira parte é deslocar os logits (lm_logits) e as etiquetas (labels) para que os tokens < n prevejam n, ou seja, a partir de uma posição n se prevê o próximo token com base nos anteriores.
  • CrossEntropyLoss: Cria-se uma instância da função de perda CrossEntropyLoss().
  • Achatando tokens: A seguir, os logits e as etiquetas são aplanados utilizando view(-1, shift_logits.size(-1)) e view(-1), respectivamente. Isso é feito para que os logits e as etiquetas tenham a mesma forma para a função de perda.
  • Cálculo da perda: Finalmente, calcula-se a perda utilizando a função de perda CrossEntropyLoss() com os logits achatados e as etiquetas achatadas como entradas.

Em resumo, a loss é calculada como a perda de entropia cruzada entre os logits deslocados e achatados e as labels deslocadas e achatadas.

Portanto, se passarmos os labels para o método forward, ele retornará a loss.

	
outputs = model(**input_tokens, labels=input_tokens.input_ids)
outputs.loss
Copy
	
tensor(4.2607, device='cuda:0', grad_fn=&lt;NllLossBackward0&gt;)

Conjunto de Dadoslink image 39

Para o treinamento, vamos usar um dataset de piadas em inglês short-jokes-dataset, que é um dataset com 231 mil piadas em inglês.

Baixamos o dataset

	
from datasets import load_dataset
jokes = load_dataset("Maximofn/short-jokes-dataset")
jokes
Copy
	
DatasetDict({
train: Dataset({
features: ['ID', 'Joke'],
num_rows: 231657
})
})

Vamos vê-lo um pouco

	
jokes["train"][0]
Copy
	
{'ID': 1,
'Joke': '[me narrating a documentary about narrators] "I can't hear what they're saying cuz I'm talking"'}

Treinamento com Pytorchlink image 40

Primeiro vamos ver como seria o treinamento com puro Pytorch

Reiniciamos o notebook para que não haja problemas com a memória da GPU

	
import torch
from transformers import OpenAIGPTLMHeadModel, AutoTokenizer
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
ckeckpoints = "openai-community/openai-gpt"
tokenizer = AutoTokenizer.from_pretrained(ckeckpoints)
model = OpenAIGPTLMHeadModel.from_pretrained(ckeckpoints)
model = model.to(device)
Copy

Conjunto de dados do Pytorchlink image 41

Criamos uma classe Dataset do Pytorch

	
from torch.utils.data import Dataset
class JokesDataset(Dataset):
def __init__(self, dataset, tokenizer):
self.dataset = dataset
self.joke = "JOKE: "
self.end_of_text_token = "&lt;|endoftext|&gt;"
self.tokenizer = tokenizer
def __len__(self):
return len(self.dataset["train"])
def __getitem__(self, item):
sentence = self.joke + self.dataset["train"][item]["Joke"] + self.end_of_text_token
tokens = self.tokenizer(sentence, return_tensors="pt")
return sentence, tokens
Copy

A instanciamos

	
dataset = JokesDataset(jokes, tokenizer=tokenizer)
Copy

Vamos ver um exemplo

	
sentence, tokens = dataset[5]
print(sentence)
tokens.input_ids.shape, tokens.attention_mask.shape
Copy
	
JOKE: Why can't Barbie get pregnant? Because Ken comes in a different box. Heyooooooo&lt;|endoftext|&gt;
	
(torch.Size([1, 30]), torch.Size([1, 30]))

Dataloaderlink image 42

Criamos agora um dataloader do Pytorch

	
from torch.utils.data import DataLoader
BS = 1
joke_dataloader = DataLoader(dataset, batch_size=BS, shuffle=True)
Copy

Vemos um lote

	
sentences, tokens = next(iter(joke_dataloader))
len(sentences), tokens.input_ids.shape, tokens.attention_mask.shape
Copy
	
(1, torch.Size([1, 1, 29]), torch.Size([1, 1, 29]))

Treinamentolink image 43

	
from transformers import AdamW, get_linear_schedule_with_warmup
import tqdm
BATCH_SIZE = 32
EPOCHS = 5
LEARNING_RATE = 3e-5
WARMUP_STEPS = 5000
MAX_SEQ_LEN = 500
model.train()
optimizer = AdamW(model.parameters(), lr=LEARNING_RATE)
scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=WARMUP_STEPS, num_training_steps=-1)
proc_seq_count = 0
batch_count = 0
tmp_jokes_tens = None
for epoch in range(EPOCHS):
print(f"EPOCH {epoch} started" + '=' * 30)
progress_bar = tqdm.tqdm(joke_dataloader, desc="Training")
for sample in progress_bar:
sentence, tokens = sample
#################### "Fit as many joke sequences into MAX_SEQ_LEN sequence as possible" logic start ####
joke_tens = tokens.input_ids[0].to(device)
# Skip sample from dataset if it is longer than MAX_SEQ_LEN
if joke_tens.size()[1] &gt; MAX_SEQ_LEN:
continue
# The first joke sequence in the sequence
if not torch.is_tensor(tmp_jokes_tens):
tmp_jokes_tens = joke_tens
continue
else:
# The next joke does not fit in so we process the sequence and leave the last joke
# as the start for next sequence
if tmp_jokes_tens.size()[1] + joke_tens.size()[1] &gt; MAX_SEQ_LEN:
work_jokes_tens = tmp_jokes_tens
tmp_jokes_tens = joke_tens
else:
#Add the joke to sequence, continue and try to add more
tmp_jokes_tens = torch.cat([tmp_jokes_tens, joke_tens[:,1:]], dim=1)
continue
################## Sequence ready, process it trough the model ##################
outputs = model(work_jokes_tens, labels=work_jokes_tens)
loss = outputs.loss
loss.backward()
proc_seq_count = proc_seq_count + 1
if proc_seq_count == BATCH_SIZE:
proc_seq_count = 0
batch_count += 1
optimizer.step()
scheduler.step()
optimizer.zero_grad()
model.zero_grad()
progress_bar.set_postfix({'loss': loss.item(), 'lr': scheduler.get_last_lr()[0]})
if batch_count == 10:
batch_count = 0
Copy
	
/home/wallabot/miniconda3/envs/nlp/lib/python3.11/site-packages/transformers/optimization.py:429: FutureWarning: This implementation of AdamW is deprecated and will be removed in a future version. Use the PyTorch implementation torch.optim.AdamW instead, or set `no_deprecation_warning=True` to disable this warning
warnings.warn(
	
EPOCH 0 started==============================
	
Training: 100%|██████████| 231657/231657 [11:31&lt;00:00, 334.88it/s, loss=2.88, lr=2.93e-6]
	
EPOCH 1 started==============================
	
Training: 100%|██████████| 231657/231657 [11:30&lt;00:00, 335.27it/s, loss=2.49, lr=5.87e-6]
	
EPOCH 2 started==============================
	
Training: 100%|██████████| 231657/231657 [11:17&lt;00:00, 341.75it/s, loss=2.57, lr=8.81e-6]
	
EPOCH 3 started==============================
	
Training: 100%|██████████| 231657/231657 [11:18&lt;00:00, 341.27it/s, loss=2.41, lr=1.18e-5]
	
EPOCH 4 started==============================
	
Training: 100%|██████████| 231657/231657 [11:19&lt;00:00, 341.04it/s, loss=2.49, lr=1.47e-5]

Inferêncialink image 44

Vamos a ver como o modelo faz piadas

	
sentence_joke = "JOKE:"
input_tokens_joke = tokenizer(sentence_joke, return_tensors="pt").to(device)
output_tokens_joke = model.generate(**input_tokens_joke)
decoded_output_joke = tokenizer.decode(output_tokens_joke[0], skip_special_tokens=True)
print(f"decoded joke: {decoded_output_joke}")
Copy
	
decoded joke:
joke : what do you call a group of people who are not afraid of the dark? a group

Pode-se ver que você passa uma sequência com a palavra joke e ele retorna uma piada. Mas se você retornar outra sequência não

	
sentence_joke = "My dog is cute and"
input_tokens_joke = tokenizer(sentence_joke, return_tensors="pt").to(device)
output_tokens_joke = model.generate(**input_tokens_joke)
decoded_output_joke = tokenizer.decode(output_tokens_joke[0], skip_special_tokens=True)
print(f"decoded joke: {decoded_output_joke}")
Copy
	
decoded joke:
my dog is cute and i'm not sure if i should be offended or not. "

Continuar lendo

MCP: Guia Completa para Criar Servidores e Clientes MCP (Model Context Protocol) com FastMCP

MCP: Guia Completa para Criar Servidores e Clientes MCP (Model Context Protocol) com FastMCP

Aprenda o que é o Model Context Protocol (MCP), o padrão de código aberto desenvolvido pela Anthropic que revoluciona como os modelos de IA interagem com ferramentas externas. Nesta guia prática e detalhada, eu te levo passo a passo na criação de um servidor e cliente MCP do zero usando a biblioteca fastmcp. Você construirá um agente de IA "inteligente" com Claude Sonnet, capaz de interagir com a API do GitHub para consultar issues e informações de repositórios. Vamos cobrir desde conceitos básicos até recursos avançados como filtragem de ferramentas por tags, composição de servidores, recursos estáticos e plantillas dinâmicas (resource templates), geração de prompts e autenticação segura. Descubra como MCP pode padronizar e simplificar a integração de ferramentas em suas aplicações de IA, de forma análoga ao como o USB unificou periféricos!

Últimos posts -->

Você viu esses projetos?

Horeca chatbot

Horeca chatbot Horeca chatbot
Python
LangChain
PostgreSQL
PGVector
React
Kubernetes
Docker
GitHub Actions

Chatbot conversacional para cozinheiros de hotéis e restaurantes. Um cozinheiro, gerente de cozinha ou serviço de quarto de um hotel ou restaurante pode falar com o chatbot para obter informações sobre receitas e menus. Mas também implementa agentes, com os quais pode editar ou criar novas receitas ou menus

Naviground

Naviground Naviground

Subtify

Subtify Subtify
Python
Whisper
Spaces

Gerador de legendas para vídeos no idioma que você desejar. Além disso, coloca uma legenda de cor diferente para cada pessoa

Ver todos os projetos -->

Quer aplicar IA no seu projeto? Entre em contato!

Quer melhorar com essas dicas?

Últimos tips -->

Use isso localmente

Os espaços do Hugging Face nos permitem executar modelos com demos muito simples, mas e se a demo quebrar? Ou se o usuário a deletar? Por isso, criei contêineres docker com alguns espaços interessantes, para poder usá-los localmente, aconteça o que acontecer. Na verdade, se você clicar em qualquer botão de visualização de projeto, ele pode levá-lo a um espaço que não funciona.

Flow edit

Flow edit Flow edit

Edite imagens com este modelo de Flow. Baseado em SD3 ou FLUX, você pode editar qualquer imagem e gerar novas

FLUX.1-RealismLora

FLUX.1-RealismLora FLUX.1-RealismLora
Ver todos os contêineres -->

Quer aplicar IA no seu projeto? Entre em contato!

Você quer treinar seu modelo com esses datasets?

short-jokes-dataset

Dataset com piadas em inglês

opus100

Dataset com traduções de inglês para espanhol

netflix_titles

Dataset com filmes e séries da Netflix

Ver mais datasets -->