Created
March 10, 2024 00:21
-
-
Save iczero/b778d0a77573d19d4f4e61b3b9a01b84 to your computer and use it in GitHub Desktop.
FizzBuzz in PyTorch
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"cells": [ | |
{ | |
"cell_type": "code", | |
"execution_count": 1, | |
"id": "3cc970e3-a4e2-40ae-a4da-db15abcd550c", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"import torch\n", | |
"from torch import nn\n", | |
"import numpy as np\n", | |
"from tqdm.notebook import trange\n", | |
"import matplotlib.pyplot as plt" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 2, | |
"id": "ea5f1ab4", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"torch.set_default_device('cuda')\n", | |
"model_dtype = torch.float32\n", | |
"\n", | |
"FIZZ = 3\n", | |
"BUZZ = 5" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 3, | |
"id": "7b1f6362-de4b-4e09-84dc-7ccff11cd2be", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"class Network(nn.Module):\n", | |
" def __init__(self):\n", | |
" super().__init__()\n", | |
" self.stack = nn.Sequential(\n", | |
" nn.Linear(32, 1024),\n", | |
" nn.Tanh(),\n", | |
" nn.Linear(1024, 256),\n", | |
" nn.Tanh(),\n", | |
" nn.Linear(256, 3),\n", | |
" nn.Sigmoid(),\n", | |
" )\n", | |
" \n", | |
" def forward(self, x):\n", | |
" return self.stack(x)\n", | |
"\n", | |
"# currently unused\n", | |
"def log_loss_old(out: torch.Tensor, expected: torch.Tensor):\n", | |
" eps = 1e-7\n", | |
" out = torch.clip(out, eps, 1 - eps)\n", | |
" errors = -expected * torch.log(out) - (1 - expected) * torch.log(1 - out)\n", | |
" # sum horizontally (each data row)\n", | |
" return torch.mean(torch.sum(errors, axis=1))\n", | |
"\n", | |
"loss_fn = torch.nn.BCELoss()" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 4, | |
"id": "cb24b1e4", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"shift_by = torch.arange(32, dtype=torch.int64)\n", | |
"mask = 1\n", | |
"\n", | |
"def split_nums(nums: torch.Tensor):\n", | |
" \"split u32 into bits\"\n", | |
" return nums.unsqueeze(-1).bitwise_right_shift(shift_by).bitwise_and(mask).type(model_dtype) * 20 - 10\n", | |
"\n", | |
"def make_data_set(min: int, max: int, count: int | None) -> tuple[torch.Tensor, torch.Tensor]:\n", | |
" if count is not None:\n", | |
" nums = torch.randint(min, max, (count,), dtype=torch.int64)\n", | |
" else:\n", | |
" nums = torch.arange(min, max, dtype=torch.int64)\n", | |
" inputs = split_nums(nums)\n", | |
" out_fizz = nums.remainder(FIZZ).eq(0)\n", | |
" out_buzz = nums.remainder(BUZZ).eq(0)\n", | |
" out_none = torch.logical_and(out_fizz.logical_not(), out_buzz.logical_not())\n", | |
" outputs = torch.stack((out_fizz, out_buzz, out_none), axis=1).type(model_dtype)\n", | |
" return inputs, outputs" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 5, | |
"id": "f4f49008", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"data": { | |
"text/plain": [ | |
"(tensor([[-10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10.],\n", | |
" [ 10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10.],\n", | |
" [-10., 10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10.],\n", | |
" [ 10., 10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10.],\n", | |
" [-10., -10., 10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10.],\n", | |
" [ 10., -10., 10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10.],\n", | |
" [-10., 10., 10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10.],\n", | |
" [ 10., 10., 10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10.],\n", | |
" [-10., -10., -10., 10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10.],\n", | |
" [ 10., -10., -10., 10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10., -10.,\n", | |
" -10., -10., -10., -10., -10., -10., -10., -10.]], device='cuda:0'),\n", | |
" tensor([[1., 1., 0.],\n", | |
" [0., 0., 1.],\n", | |
" [0., 0., 1.],\n", | |
" [1., 0., 0.],\n", | |
" [0., 0., 1.],\n", | |
" [0., 1., 0.],\n", | |
" [1., 0., 0.],\n", | |
" [0., 0., 1.],\n", | |
" [0., 0., 1.],\n", | |
" [1., 0., 0.]], device='cuda:0'))" | |
] | |
}, | |
"execution_count": 5, | |
"metadata": {}, | |
"output_type": "execute_result" | |
} | |
], | |
"source": [ | |
"make_data_set(0, 10, None)" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 6, | |
"id": "f7b3ff70", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"def make_step(optimizer, batch_size, min, max):\n", | |
" def step(model):\n", | |
" train_in, train_out = make_data_set(min, max, batch_size)\n", | |
" prediction = model(train_in)\n", | |
" loss = loss_fn(prediction, train_out)\n", | |
"\n", | |
" loss.backward()\n", | |
" optimizer.step()\n", | |
" optimizer.zero_grad()\n", | |
"\n", | |
" return loss\n", | |
" \n", | |
" return step" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 7, | |
"id": "e64deb5e", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"def model_eval(model, input):\n", | |
" model.eval()\n", | |
" with torch.no_grad():\n", | |
" return model.forward(input)\n", | |
"\n", | |
"test_threshold = 0.75\n", | |
"def test_fizzbuzz(model, input, quiet=False):\n", | |
" out = model_eval(model, split_nums(input))\n", | |
" for i, val in zip(input, out):\n", | |
" fizz, buzz, neither = val\n", | |
" fizzmod = i % FIZZ\n", | |
" buzzmod = i % BUZZ\n", | |
" if not quiet:\n", | |
" print(\n", | |
" f'{i} fizz: {fizz:.4f}, fizzmod: {fizzmod}, ' +\n", | |
" f'buzz: {buzz:.4f}, buzzmod: {buzzmod}, ' +\n", | |
" f'neither: {neither:.4f}')\n", | |
" if fizzmod == 0 and fizz < test_threshold:\n", | |
" print(i, 'fizz wrong: expect fizz got', fizz)\n", | |
" if fizz >= test_threshold and fizzmod != 0:\n", | |
" print(i, 'fizz wrong: expect not fizz got', fizz)\n", | |
" if buzzmod == 0 and buzz < test_threshold:\n", | |
" print(i, 'buzz wrong: expect buzz got', buzz)\n", | |
" if buzz >= test_threshold and buzzmod != 0:\n", | |
" print(i, 'buzz wrong: expect not buzz got', buzz)\n", | |
" if fizzmod != 0 and buzzmod != 0 and neither < test_threshold:\n", | |
" print(i, 'neither wrong: expect neither got', neither)\n", | |
" if (fizzmod == 0 or buzzmod == 0) and neither >= test_threshold:\n", | |
" print(i, 'neither wrong: expect not neither got', neither)" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 8, | |
"id": "178e7313", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"def train_stage(model, optimizer, batch_size, iterations, min_val, max_val):\n", | |
" model.train()\n", | |
" step = make_step(optimizer, batch_size, min_val, max_val)\n", | |
" progress = trange(iterations)\n", | |
" lossplot_v = np.zeros(len(progress))\n", | |
" for i in progress:\n", | |
" loss_v = step(model)\n", | |
" if i % 100 == 0:\n", | |
" progress.set_postfix({ 'loss': loss_v })\n", | |
" lossplot_v[i] = float(loss_v)\n", | |
" if loss_v < 0.001:\n", | |
" break\n", | |
"\n", | |
" lossplot_t = np.arange(progress.n)\n", | |
" plt.plot(lossplot_t, lossplot_v[:progress.n])" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 9, | |
"id": "dc6facba", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"data": { | |
"text/plain": [ | |
"Network(\n", | |
" (stack): Sequential(\n", | |
" (0): Linear(in_features=32, out_features=1024, bias=True)\n", | |
" (1): Tanh()\n", | |
" (2): Linear(in_features=1024, out_features=256, bias=True)\n", | |
" (3): Tanh()\n", | |
" (4): Linear(in_features=256, out_features=3, bias=True)\n", | |
" (5): Sigmoid()\n", | |
" )\n", | |
")" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"model = Network()\n", | |
"display(model)\n", | |
"def make_optimizer(model):\n", | |
" return torch.optim.AdamW(model.parameters(), lr=0.0005, weight_decay=0.001)\n", | |
"batch_size = 1024" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 10, | |
"id": "ac8334dd", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"# stage 1: train to 16 bits\n", | |
"#optimizer = make_optimizer(model)\n", | |
"#train_stage(model, optimizer, batch_size,\n", | |
"# iterations=16384, min_val=0, max_val=1 << 16)\n", | |
"#test_fizzbuzz(model, torch.arange(25))" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 11, | |
"id": "b109fb31", | |
"metadata": {}, | |
"outputs": [], | |
"source": [ | |
"# stage 2: train to 24 bits\n", | |
"#optimizer = make_optimizer(model)\n", | |
"#train_stage(model, optimizer, batch_size,\n", | |
"# iterations=32768, min_val=0, max_val=(1 << 24) - 1)\n", | |
"#test_fizzbuzz(model, torch.tensor([i for i in range(int(1e2), int(1e2) + 25)]))\n", | |
"#test_fizzbuzz(model, torch.tensor([i for i in range(int(1e6), int(1e6) + 25)]))" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 12, | |
"id": "63728fdc", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"data": { | |
"application/vnd.jupyter.widget-view+json": { | |
"model_id": "a8a8b4b487c74908af2f572170236611", | |
"version_major": 2, | |
"version_minor": 0 | |
}, | |
"text/plain": [ | |
" 0%| | 0/65536 [00:00<?, ?it/s]" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
}, | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"200 fizz: 0.0001, fizzmod: 2, buzz: 1.0000, buzzmod: 0, neither: 0.0000\n", | |
"201 fizz: 1.0000, fizzmod: 0, buzz: 0.0000, buzzmod: 1, neither: 0.0000\n", | |
"202 fizz: 0.0000, fizzmod: 1, buzz: 0.0000, buzzmod: 2, neither: 0.9999\n", | |
"203 fizz: 0.0000, fizzmod: 2, buzz: 0.0000, buzzmod: 3, neither: 0.9998\n", | |
"204 fizz: 1.0000, fizzmod: 0, buzz: 0.0001, buzzmod: 4, neither: 0.0000\n", | |
"205 fizz: 0.0000, fizzmod: 1, buzz: 1.0000, buzzmod: 0, neither: 0.0005\n", | |
"206 fizz: 0.0000, fizzmod: 2, buzz: 0.0000, buzzmod: 1, neither: 1.0000\n", | |
"207 fizz: 1.0000, fizzmod: 0, buzz: 0.0002, buzzmod: 2, neither: 0.0000\n", | |
"208 fizz: 0.0000, fizzmod: 1, buzz: 0.0000, buzzmod: 3, neither: 0.9999\n", | |
"209 fizz: 0.0000, fizzmod: 2, buzz: 0.0000, buzzmod: 4, neither: 1.0000\n", | |
"210 fizz: 1.0000, fizzmod: 0, buzz: 1.0000, buzzmod: 0, neither: 0.0000\n", | |
"211 fizz: 0.0000, fizzmod: 1, buzz: 0.0000, buzzmod: 1, neither: 1.0000\n", | |
"212 fizz: 0.0000, fizzmod: 2, buzz: 0.0000, buzzmod: 2, neither: 1.0000\n", | |
"213 fizz: 1.0000, fizzmod: 0, buzz: 0.0001, buzzmod: 3, neither: 0.0000\n", | |
"214 fizz: 0.0000, fizzmod: 1, buzz: 0.0000, buzzmod: 4, neither: 1.0000\n", | |
"215 fizz: 0.0000, fizzmod: 2, buzz: 1.0000, buzzmod: 0, neither: 0.0002\n", | |
"216 fizz: 1.0000, fizzmod: 0, buzz: 0.0000, buzzmod: 1, neither: 0.0000\n", | |
"217 fizz: 0.0000, fizzmod: 1, buzz: 0.0001, buzzmod: 2, neither: 1.0000\n", | |
"218 fizz: 0.0000, fizzmod: 2, buzz: 0.0000, buzzmod: 3, neither: 0.9999\n", | |
"219 fizz: 1.0000, fizzmod: 0, buzz: 0.0001, buzzmod: 4, neither: 0.0000\n", | |
"220 fizz: 0.0000, fizzmod: 1, buzz: 1.0000, buzzmod: 0, neither: 0.0004\n", | |
"221 fizz: 0.0000, fizzmod: 2, buzz: 0.0000, buzzmod: 1, neither: 0.9999\n", | |
"222 fizz: 1.0000, fizzmod: 0, buzz: 0.0002, buzzmod: 2, neither: 0.0000\n", | |
"223 fizz: 0.0000, fizzmod: 1, buzz: 0.0000, buzzmod: 3, neither: 1.0000\n", | |
"224 fizz: 0.0000, fizzmod: 2, buzz: 0.0000, buzzmod: 4, neither: 0.9999\n", | |
"1000000000 fizz: 0.0000, fizzmod: 1, buzz: 1.0000, buzzmod: 0, neither: 0.0003\n", | |
"1000000001 fizz: 0.0000, fizzmod: 2, buzz: 0.0000, buzzmod: 1, neither: 1.0000\n", | |
"1000000002 fizz: 1.0000, fizzmod: 0, buzz: 0.0000, buzzmod: 2, neither: 0.0000\n", | |
"1000000003 fizz: 0.0000, fizzmod: 1, buzz: 0.0000, buzzmod: 3, neither: 1.0000\n", | |
"1000000004 fizz: 0.0000, fizzmod: 2, buzz: 0.0000, buzzmod: 4, neither: 1.0000\n", | |
"1000000005 fizz: 1.0000, fizzmod: 0, buzz: 0.9999, buzzmod: 0, neither: 0.0000\n", | |
"1000000006 fizz: 0.0000, fizzmod: 1, buzz: 0.0000, buzzmod: 1, neither: 1.0000\n", | |
"1000000007 fizz: 0.0000, fizzmod: 2, buzz: 0.0000, buzzmod: 2, neither: 1.0000\n", | |
"1000000008 fizz: 1.0000, fizzmod: 0, buzz: 0.0001, buzzmod: 3, neither: 0.0000\n", | |
"1000000009 fizz: 0.0000, fizzmod: 1, buzz: 0.0000, buzzmod: 4, neither: 0.9999\n", | |
"1000000010 fizz: 0.0000, fizzmod: 2, buzz: 0.9999, buzzmod: 0, neither: 0.0004\n", | |
"1000000011 fizz: 1.0000, fizzmod: 0, buzz: 0.0000, buzzmod: 1, neither: 0.0000\n", | |
"1000000012 fizz: 0.0000, fizzmod: 1, buzz: 0.0001, buzzmod: 2, neither: 0.9991\n", | |
"1000000013 fizz: 0.0000, fizzmod: 2, buzz: 0.0002, buzzmod: 3, neither: 0.9992\n", | |
"1000000014 fizz: 1.0000, fizzmod: 0, buzz: 0.0000, buzzmod: 4, neither: 0.0000\n", | |
"1000000015 fizz: 0.0000, fizzmod: 1, buzz: 0.9998, buzzmod: 0, neither: 0.0023\n", | |
"1000000016 fizz: 0.0000, fizzmod: 2, buzz: 0.0000, buzzmod: 1, neither: 1.0000\n", | |
"1000000017 fizz: 1.0000, fizzmod: 0, buzz: 0.0000, buzzmod: 2, neither: 0.0001\n", | |
"1000000018 fizz: 0.0000, fizzmod: 1, buzz: 0.0000, buzzmod: 3, neither: 1.0000\n", | |
"1000000019 fizz: 0.0000, fizzmod: 2, buzz: 0.0000, buzzmod: 4, neither: 1.0000\n", | |
"1000000020 fizz: 1.0000, fizzmod: 0, buzz: 1.0000, buzzmod: 0, neither: 0.0000\n", | |
"1000000021 fizz: 0.0001, fizzmod: 1, buzz: 0.0001, buzzmod: 1, neither: 0.9996\n", | |
"1000000022 fizz: 0.0000, fizzmod: 2, buzz: 0.0000, buzzmod: 2, neither: 1.0000\n", | |
"1000000023 fizz: 1.0000, fizzmod: 0, buzz: 0.0001, buzzmod: 3, neither: 0.0001\n", | |
"1000000024 fizz: 0.0000, fizzmod: 1, buzz: 0.0000, buzzmod: 4, neither: 0.9999\n" | |
] | |
}, | |
{ | |
"data": { | |
"image/png": "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", | |
"text/plain": [ | |
"<Figure size 640x480 with 1 Axes>" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
} | |
], | |
"source": [ | |
"# stage 3: train to 32 bits\n", | |
"optimizer = make_optimizer(model)\n", | |
"train_stage(model, optimizer, batch_size,\n", | |
" iterations=65536, min_val=0, max_val=(1 << 32) - 1)\n", | |
"test_fizzbuzz(model, torch.tensor([i for i in range(int(2e2), int(2e2) + 25)]))\n", | |
"test_fizzbuzz(model, torch.tensor([i for i in range(int(1e9), int(1e9) + 25)]))" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 13, | |
"id": "cb69a399", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"data": { | |
"application/vnd.jupyter.widget-view+json": { | |
"model_id": "6275b40a60df4d25b321500c3f43bc7d", | |
"version_major": 2, | |
"version_minor": 0 | |
}, | |
"text/plain": [ | |
" 0%| | 0/16 [00:00<?, ?it/s]" | |
] | |
}, | |
"metadata": {}, | |
"output_type": "display_data" | |
}, | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"tensor(28229346, device='cuda:0') fizz wrong: expect fizz got tensor(0.1587, device='cuda:0')\n", | |
"tensor(53023808, device='cuda:0') neither wrong: expect neither got tensor(0.7494, device='cuda:0')\n", | |
"tensor(53023810, device='cuda:0') buzz wrong: expect buzz got tensor(0.2398, device='cuda:0')\n", | |
"tensor(53023840, device='cuda:0') buzz wrong: expect buzz got tensor(0.2424, device='cuda:0')\n", | |
"tensor(53023850, device='cuda:0') buzz wrong: expect buzz got tensor(0.4570, device='cuda:0')\n", | |
"tensor(53023970, device='cuda:0') buzz wrong: expect buzz got tensor(0.1307, device='cuda:0')\n", | |
"tensor(53024320, device='cuda:0') buzz wrong: expect buzz got tensor(0.2518, device='cuda:0')\n", | |
"tensor(53024330, device='cuda:0') buzz wrong: expect buzz got tensor(0.4427, device='cuda:0')\n", | |
"tensor(53024354, device='cuda:0') neither wrong: expect neither got tensor(0.7040, device='cuda:0')\n", | |
"tensor(53024355, device='cuda:0') buzz wrong: expect buzz got tensor(0.2424, device='cuda:0')\n", | |
"tensor(53024360, device='cuda:0') buzz wrong: expect buzz got tensor(0.4293, device='cuda:0')\n", | |
"tensor(53024362, device='cuda:0') neither wrong: expect neither got tensor(0.4302, device='cuda:0')\n", | |
"tensor(53024370, device='cuda:0') buzz wrong: expect buzz got tensor(0.2436, device='cuda:0')\n", | |
"tensor(53024450, device='cuda:0') buzz wrong: expect buzz got tensor(0.1235, device='cuda:0')\n", | |
"tensor(53024458, device='cuda:0') neither wrong: expect neither got tensor(0.5055, device='cuda:0')\n", | |
"tensor(53024480, device='cuda:0') buzz wrong: expect buzz got tensor(0.1266, device='cuda:0')\n", | |
"tensor(53024482, device='cuda:0') neither wrong: expect neither got tensor(0.5953, device='cuda:0')\n", | |
"tensor(53024483, device='cuda:0') neither wrong: expect neither got tensor(0.6983, device='cuda:0')\n", | |
"tensor(53024488, device='cuda:0') neither wrong: expect neither got tensor(0.6188, device='cuda:0')\n", | |
"tensor(53024490, device='cuda:0') buzz wrong: expect buzz got tensor(0.2725, device='cuda:0')\n", | |
"tensor(53024498, device='cuda:0') neither wrong: expect neither got tensor(0.7045, device='cuda:0')\n", | |
"tensor(53024610, device='cuda:0') buzz wrong: expect buzz got tensor(0.2425, device='cuda:0')\n" | |
] | |
} | |
], | |
"source": [ | |
"test_fizzbuzz(model, torch.arange(0, 101), quiet=True)\n", | |
"for i in trange(0, int(64e6), int(4e6)):\n", | |
" start = np.random.randint(i, i + 1048576 - 1024)\n", | |
" test_fizzbuzz(model, torch.arange(start, start + 1024), quiet=True)" | |
] | |
}, | |
{ | |
"cell_type": "code", | |
"execution_count": 14, | |
"id": "f4ddb9bd", | |
"metadata": {}, | |
"outputs": [ | |
{ | |
"name": "stdout", | |
"output_type": "stream", | |
"text": [ | |
"tensor(1, device='cuda:0')\n", | |
"tensor(2, device='cuda:0')\n", | |
"fizz\n", | |
"tensor(4, device='cuda:0')\n", | |
"buzz\n", | |
"fizz\n", | |
"tensor(7, device='cuda:0')\n", | |
"tensor(8, device='cuda:0')\n", | |
"fizz\n", | |
"buzz\n", | |
"tensor(11, device='cuda:0')\n", | |
"fizz\n", | |
"tensor(13, device='cuda:0')\n", | |
"tensor(14, device='cuda:0')\n", | |
"fizzbuzz\n", | |
"tensor(16, device='cuda:0')\n", | |
"tensor(17, device='cuda:0')\n", | |
"fizz\n", | |
"tensor(19, device='cuda:0')\n", | |
"buzz\n", | |
"fizz\n", | |
"tensor(22, device='cuda:0')\n", | |
"tensor(23, device='cuda:0')\n", | |
"fizz\n", | |
"buzz\n", | |
"tensor(26, device='cuda:0')\n", | |
"fizz\n", | |
"tensor(28, device='cuda:0')\n", | |
"tensor(29, device='cuda:0')\n", | |
"fizzbuzz\n", | |
"tensor(31, device='cuda:0')\n", | |
"tensor(32, device='cuda:0')\n", | |
"fizz\n", | |
"tensor(34, device='cuda:0')\n", | |
"buzz\n", | |
"fizz\n", | |
"tensor(37, device='cuda:0')\n", | |
"tensor(38, device='cuda:0')\n", | |
"fizz\n", | |
"buzz\n", | |
"tensor(41, device='cuda:0')\n", | |
"fizz\n", | |
"tensor(43, device='cuda:0')\n", | |
"tensor(44, device='cuda:0')\n", | |
"fizzbuzz\n", | |
"tensor(46, device='cuda:0')\n", | |
"tensor(47, device='cuda:0')\n", | |
"fizz\n", | |
"tensor(49, device='cuda:0')\n", | |
"buzz\n", | |
"fizz\n", | |
"tensor(52, device='cuda:0')\n", | |
"tensor(53, device='cuda:0')\n", | |
"fizz\n", | |
"buzz\n", | |
"tensor(56, device='cuda:0')\n", | |
"fizz\n", | |
"tensor(58, device='cuda:0')\n", | |
"tensor(59, device='cuda:0')\n", | |
"fizzbuzz\n", | |
"tensor(61, device='cuda:0')\n", | |
"tensor(62, device='cuda:0')\n", | |
"fizz\n", | |
"tensor(64, device='cuda:0')\n", | |
"buzz\n", | |
"fizz\n", | |
"tensor(67, device='cuda:0')\n", | |
"tensor(68, device='cuda:0')\n", | |
"fizz\n", | |
"buzz\n", | |
"tensor(71, device='cuda:0')\n", | |
"fizz\n", | |
"tensor(73, device='cuda:0')\n", | |
"tensor(74, device='cuda:0')\n", | |
"fizzbuzz\n", | |
"tensor(76, device='cuda:0')\n", | |
"tensor(77, device='cuda:0')\n", | |
"fizz\n", | |
"tensor(79, device='cuda:0')\n", | |
"buzz\n", | |
"fizz\n", | |
"tensor(82, device='cuda:0')\n", | |
"tensor(83, device='cuda:0')\n", | |
"fizz\n", | |
"buzz\n", | |
"tensor(86, device='cuda:0')\n", | |
"fizz\n", | |
"tensor(88, device='cuda:0')\n", | |
"tensor(89, device='cuda:0')\n", | |
"fizzbuzz\n", | |
"tensor(91, device='cuda:0')\n", | |
"tensor(92, device='cuda:0')\n", | |
"fizz\n", | |
"tensor(94, device='cuda:0')\n", | |
"buzz\n", | |
"fizz\n", | |
"tensor(97, device='cuda:0')\n", | |
"tensor(98, device='cuda:0')\n", | |
"fizz\n", | |
"buzz\n" | |
] | |
} | |
], | |
"source": [ | |
"infer_threshold = 0.75\n", | |
"def fizzbuzz(model, min, max, quiet=False):\n", | |
" nums = torch.arange(min, max)\n", | |
" inputs = split_nums(nums)\n", | |
" outputs = model.forward(inputs)\n", | |
" collected = []\n", | |
" for num, (fizz, buzz, neither) in zip(nums, outputs):\n", | |
" out = ''\n", | |
" if fizz > infer_threshold:\n", | |
" out += 'fizz'\n", | |
" if buzz > infer_threshold:\n", | |
" out += 'buzz'\n", | |
" if neither > infer_threshold:\n", | |
" out += str(num)\n", | |
" collected.append(out)\n", | |
" if not quiet:\n", | |
" print(out)\n", | |
" \n", | |
" return collected\n", | |
"\n", | |
"fizzbuzz(model, 1, 101);" | |
] | |
} | |
], | |
"metadata": { | |
"kernelspec": { | |
"display_name": "Python 3 (ipykernel)", | |
"language": "python", | |
"name": "python3" | |
}, | |
"language_info": { | |
"codemirror_mode": { | |
"name": "ipython", | |
"version": 3 | |
}, | |
"file_extension": ".py", | |
"mimetype": "text/x-python", | |
"name": "python", | |
"nbconvert_exporter": "python", | |
"pygments_lexer": "ipython3", | |
"version": "3.11.8" | |
} | |
}, | |
"nbformat": 4, | |
"nbformat_minor": 5 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment