forked from rasbt/python-machine-learning-book-3rd-edition
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
2 changed files
with
646 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,444 @@ | ||
{ | ||
"cells": [ | ||
{ | ||
"cell_type": "markdown", | ||
"id": "b65f2a05", | ||
"metadata": {}, | ||
"source": [ | ||
"# Chapter 13: Going Deeper -- the Mechanics of PyTorch (Part 3/3)" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"id": "ddec26f4", | ||
"metadata": {}, | ||
"source": [ | ||
"## Higher-level PyTorch APIs: a short introduction to PyTorch-Ignite " | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"id": "7722db5f", | ||
"metadata": {}, | ||
"source": [ | ||
"### Setting up the PyTorch model" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 1, | ||
"id": "220a5ea0", | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"import torch \n", | ||
"import torch.nn as nn \n", | ||
"from torch.utils.data import DataLoader \n", | ||
" \n", | ||
"from torchvision.datasets import MNIST \n", | ||
"from torchvision import transforms\n", | ||
" \n", | ||
" \n", | ||
"image_path = './' \n", | ||
"torch.manual_seed(1) \n", | ||
" \n", | ||
"transform = transforms.Compose([ \n", | ||
" transforms.ToTensor() \n", | ||
"]) \n", | ||
" \n", | ||
" \n", | ||
"mnist_train_dataset = MNIST( \n", | ||
" root=image_path, \n", | ||
" train=True,\n", | ||
" transform=transform, \n", | ||
" download=True\n", | ||
") \n", | ||
" \n", | ||
"mnist_val_dataset = MNIST( \n", | ||
" root=image_path, \n", | ||
" train=False, \n", | ||
" transform=transform, \n", | ||
" download=False \n", | ||
") \n", | ||
" \n", | ||
"batch_size = 64\n", | ||
"train_loader = DataLoader( \n", | ||
" mnist_train_dataset, batch_size, shuffle=True \n", | ||
") \n", | ||
" \n", | ||
"val_loader = DataLoader( \n", | ||
" mnist_val_dataset, batch_size, shuffle=False \n", | ||
") \n", | ||
" \n", | ||
" \n", | ||
"def get_model(image_shape=(1, 28, 28), hidden_units=(32, 16)): \n", | ||
" input_size = image_shape[0] * image_shape[1] * image_shape[2] \n", | ||
" all_layers = [nn.Flatten()]\n", | ||
" for hidden_unit in hidden_units: \n", | ||
" layer = nn.Linear(input_size, hidden_unit) \n", | ||
" all_layers.append(layer) \n", | ||
" all_layers.append(nn.ReLU()) \n", | ||
" input_size = hidden_unit \n", | ||
" \n", | ||
" all_layers.append(nn.Linear(hidden_units[-1], 10)) \n", | ||
" all_layers.append(nn.Softmax(dim=1)) \n", | ||
" model = nn.Sequential(*all_layers)\n", | ||
" return model \n", | ||
" \n", | ||
" \n", | ||
"device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n", | ||
" \n", | ||
"model = get_model().to(device)\n", | ||
"loss_fn = nn.CrossEntropyLoss()\n", | ||
"optimizer = torch.optim.Adam(model.parameters(), lr=0.001)" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"id": "99dcabd0", | ||
"metadata": {}, | ||
"source": [ | ||
"### Setting up training and validation engines with PyTorch-Ignite" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 2, | ||
"id": "c972bfa2", | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"from ignite.engine import Events, create_supervised_trainer, create_supervised_evaluator\n", | ||
"from ignite.metrics import Accuracy, Loss\n", | ||
" \n", | ||
" \n", | ||
"trainer = create_supervised_trainer(\n", | ||
" model, optimizer, loss_fn, device=device\n", | ||
")\n", | ||
" \n", | ||
"val_metrics = {\n", | ||
" \"accuracy\": Accuracy(),\n", | ||
" \"loss\": Loss(loss_fn)\n", | ||
"}\n", | ||
" \n", | ||
"evaluator = create_supervised_evaluator(\n", | ||
" model, metrics=val_metrics, device=device\n", | ||
")\n" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"id": "aa17c608", | ||
"metadata": {}, | ||
"source": [ | ||
"### Creating event handlers for logging and validation" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 3, | ||
"id": "0edafc78", | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"# How many batches to wait before logging training status\n", | ||
"log_interval = 100\n", | ||
" \n", | ||
"@trainer.on(Events.ITERATION_COMPLETED(every=log_interval))\n", | ||
"def log_training_loss():\n", | ||
" e = trainer.state.epoch\n", | ||
" max_e = trainer.state.max_epochs\n", | ||
" i = trainer.state.iteration\n", | ||
" batch_loss = trainer.state.output\n", | ||
" print(f\"Epoch[{e}/{max_e}], Iter[{i}] Loss: {batch_loss:.2f}\")\n" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 4, | ||
"id": "1944b444", | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"@trainer.on(Events.EPOCH_COMPLETED)\n", | ||
"def log_validation_results():\n", | ||
" eval_state = evaluator.run(val_loader)\n", | ||
" metrics = eval_state.metrics\n", | ||
" e = trainer.state.epoch\n", | ||
" max_e = trainer.state.max_epochs\n", | ||
" acc = metrics['accuracy']\n", | ||
" avg_loss = metrics['loss']\n", | ||
" print(f\"Validation Results - Epoch[{e}/{max_e}] Avg Accuracy: {acc:.2f} Avg Loss: {avg_loss:.2f}\")" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"id": "11b8cdfa", | ||
"metadata": {}, | ||
"source": [ | ||
"### Setting up training checkpoints and saving the best model" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 5, | ||
"id": "e451c03b", | ||
"metadata": {}, | ||
"outputs": [ | ||
{ | ||
"data": { | ||
"text/plain": [ | ||
"<ignite.engine.events.RemovableEventHandle at 0x1060993d0>" | ||
] | ||
}, | ||
"execution_count": 5, | ||
"metadata": {}, | ||
"output_type": "execute_result" | ||
} | ||
], | ||
"source": [ | ||
"from ignite.handlers import Checkpoint, DiskSaver\n", | ||
" \n", | ||
"# We will save in the checkpoint the following:\n", | ||
"to_save = {\"model\": model, \"optimizer\": optimizer, \"trainer\": trainer}\n", | ||
" \n", | ||
"# We will save checkpoints to the local disk\n", | ||
"output_path = \"./output\"\n", | ||
"save_handler = DiskSaver(dirname=output_path, require_empty=False)\n", | ||
" \n", | ||
"# Set up the handler:\n", | ||
"checkpoint_handler = Checkpoint(\n", | ||
" to_save, save_handler, filename_prefix=\"training\")\n", | ||
"\n", | ||
"# Attach the handler to the trainer\n", | ||
"trainer.add_event_handler(Events.EPOCH_COMPLETED, checkpoint_handler)" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 6, | ||
"id": "e3c8def7", | ||
"metadata": {}, | ||
"outputs": [ | ||
{ | ||
"data": { | ||
"text/plain": [ | ||
"<ignite.engine.events.RemovableEventHandle at 0x106088430>" | ||
] | ||
}, | ||
"execution_count": 6, | ||
"metadata": {}, | ||
"output_type": "execute_result" | ||
} | ||
], | ||
"source": [ | ||
"# Store best model by validation accuracy\n", | ||
"best_model_handler = Checkpoint(\n", | ||
" {\"model\": model},\n", | ||
" save_handler,\n", | ||
" filename_prefix=\"best\",\n", | ||
" n_saved=1,\n", | ||
" score_name=\"accuracy\",\n", | ||
" score_function=Checkpoint.get_default_score_fn(\"accuracy\"),\n", | ||
")\n", | ||
" \n", | ||
"evaluator.add_event_handler(Events.COMPLETED, best_model_handler)\n" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"id": "7092b069", | ||
"metadata": {}, | ||
"source": [ | ||
"### Setting up TensorBoard as an experiment tracking system" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 7, | ||
"id": "9dc0368b", | ||
"metadata": {}, | ||
"outputs": [ | ||
{ | ||
"data": { | ||
"text/plain": [ | ||
"<ignite.engine.events.RemovableEventHandle at 0x16605ff70>" | ||
] | ||
}, | ||
"execution_count": 7, | ||
"metadata": {}, | ||
"output_type": "execute_result" | ||
} | ||
], | ||
"source": [ | ||
"from ignite.contrib.handlers import TensorboardLogger, global_step_from_engine\n", | ||
" \n", | ||
" \n", | ||
"tb_logger = TensorboardLogger(log_dir=output_path)\n", | ||
" \n", | ||
"# Attach handler to plot trainer's loss every 100 iterations\n", | ||
"tb_logger.attach_output_handler(\n", | ||
" trainer,\n", | ||
" event_name=Events.ITERATION_COMPLETED(every=100),\n", | ||
" tag=\"training\",\n", | ||
" output_transform=lambda loss: {\"batch_loss\": loss},\n", | ||
")\n", | ||
" \n", | ||
"# Attach handler for plotting both evaluators' metrics after every epoch completes\n", | ||
"tb_logger.attach_output_handler(\n", | ||
" evaluator,\n", | ||
" event_name=Events.EPOCH_COMPLETED,\n", | ||
" tag=\"validation\",\n", | ||
" metric_names=\"all\",\n", | ||
" global_step_transform=global_step_from_engine(trainer),\n", | ||
")" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"id": "ad26fb6b", | ||
"metadata": {}, | ||
"source": [ | ||
"### Executing the PyTorch-Ignite model training code" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 8, | ||
"id": "7f4d38cf", | ||
"metadata": {}, | ||
"outputs": [ | ||
{ | ||
"name": "stdout", | ||
"output_type": "stream", | ||
"text": [ | ||
"Epoch[1/5], Iter[100] Loss: 1.87\n", | ||
"Epoch[1/5], Iter[200] Loss: 1.82\n", | ||
"Epoch[1/5], Iter[300] Loss: 1.67\n", | ||
"Epoch[1/5], Iter[400] Loss: 1.55\n", | ||
"Epoch[1/5], Iter[500] Loss: 1.65\n", | ||
"Epoch[1/5], Iter[600] Loss: 1.59\n", | ||
"Epoch[1/5], Iter[700] Loss: 1.59\n", | ||
"Epoch[1/5], Iter[800] Loss: 1.56\n", | ||
"Epoch[1/5], Iter[900] Loss: 1.63\n", | ||
"Validation Results - Epoch[1/5] Avg Accuracy: 0.91 Avg Loss: 1.56\n", | ||
"Epoch[2/5], Iter[1000] Loss: 1.61\n", | ||
"Epoch[2/5], Iter[1100] Loss: 1.56\n", | ||
"Epoch[2/5], Iter[1200] Loss: 1.54\n", | ||
"Epoch[2/5], Iter[1300] Loss: 1.54\n", | ||
"Epoch[2/5], Iter[1400] Loss: 1.51\n", | ||
"Epoch[2/5], Iter[1500] Loss: 1.53\n", | ||
"Epoch[2/5], Iter[1600] Loss: 1.50\n", | ||
"Epoch[2/5], Iter[1700] Loss: 1.50\n", | ||
"Epoch[2/5], Iter[1800] Loss: 1.52\n", | ||
"Validation Results - Epoch[2/5] Avg Accuracy: 0.92 Avg Loss: 1.54\n", | ||
"Epoch[3/5], Iter[1900] Loss: 1.61\n", | ||
"Epoch[3/5], Iter[2000] Loss: 1.60\n", | ||
"Epoch[3/5], Iter[2100] Loss: 1.54\n", | ||
"Epoch[3/5], Iter[2200] Loss: 1.51\n", | ||
"Epoch[3/5], Iter[2300] Loss: 1.48\n", | ||
"Epoch[3/5], Iter[2400] Loss: 1.56\n", | ||
"Epoch[3/5], Iter[2500] Loss: 1.57\n", | ||
"Epoch[3/5], Iter[2600] Loss: 1.52\n", | ||
"Epoch[3/5], Iter[2700] Loss: 1.54\n", | ||
"Epoch[3/5], Iter[2800] Loss: 1.54\n", | ||
"Validation Results - Epoch[3/5] Avg Accuracy: 0.93 Avg Loss: 1.53\n", | ||
"Epoch[4/5], Iter[2900] Loss: 1.53\n", | ||
"Epoch[4/5], Iter[3000] Loss: 1.49\n", | ||
"Epoch[4/5], Iter[3100] Loss: 1.51\n", | ||
"Epoch[4/5], Iter[3200] Loss: 1.51\n", | ||
"Epoch[4/5], Iter[3300] Loss: 1.54\n", | ||
"Epoch[4/5], Iter[3400] Loss: 1.50\n", | ||
"Epoch[4/5], Iter[3500] Loss: 1.58\n", | ||
"Epoch[4/5], Iter[3600] Loss: 1.59\n", | ||
"Epoch[4/5], Iter[3700] Loss: 1.50\n", | ||
"Validation Results - Epoch[4/5] Avg Accuracy: 0.94 Avg Loss: 1.53\n", | ||
"Epoch[5/5], Iter[3800] Loss: 1.52\n", | ||
"Epoch[5/5], Iter[3900] Loss: 1.60\n", | ||
"Epoch[5/5], Iter[4000] Loss: 1.50\n", | ||
"Epoch[5/5], Iter[4100] Loss: 1.50\n", | ||
"Epoch[5/5], Iter[4200] Loss: 1.51\n", | ||
"Epoch[5/5], Iter[4300] Loss: 1.57\n", | ||
"Epoch[5/5], Iter[4400] Loss: 1.56\n", | ||
"Epoch[5/5], Iter[4500] Loss: 1.55\n", | ||
"Epoch[5/5], Iter[4600] Loss: 1.50\n", | ||
"Validation Results - Epoch[5/5] Avg Accuracy: 0.94 Avg Loss: 1.52\n" | ||
] | ||
}, | ||
{ | ||
"data": { | ||
"text/plain": [ | ||
"State:\n", | ||
"\titeration: 4690\n", | ||
"\tepoch: 5\n", | ||
"\tepoch_length: 938\n", | ||
"\tmax_epochs: 5\n", | ||
"\toutput: 1.5042390823364258\n", | ||
"\tbatch: <class 'list'>\n", | ||
"\tmetrics: <class 'dict'>\n", | ||
"\tdataloader: <class 'torch.utils.data.dataloader.DataLoader'>\n", | ||
"\tseed: <class 'NoneType'>\n", | ||
"\ttimes: <class 'dict'>" | ||
] | ||
}, | ||
"execution_count": 8, | ||
"metadata": {}, | ||
"output_type": "execute_result" | ||
} | ||
], | ||
"source": [ | ||
"trainer.run(train_loader, max_epochs=5)" | ||
] | ||
}, | ||
{ | ||
"cell_type": "markdown", | ||
"id": "523acf34", | ||
"metadata": {}, | ||
"source": [ | ||
"---\n", | ||
"\n", | ||
"Readers may ignore the next cell." | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": 10, | ||
"id": "29befadd", | ||
"metadata": {}, | ||
"outputs": [ | ||
{ | ||
"name": "stdout", | ||
"output_type": "stream", | ||
"text": [ | ||
"[NbConvertApp] Converting notebook ch13_part3.ipynb to script\n", | ||
"[NbConvertApp] Writing 4864 bytes to ch13_part3.py\n" | ||
] | ||
} | ||
], | ||
"source": [ | ||
"! python ../.convert_notebook_to_script.py --input ch13_part3.ipynb --output ch13_part3.py" | ||
] | ||
} | ||
], | ||
"metadata": { | ||
"kernelspec": { | ||
"display_name": "Python 3", | ||
"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.9.2" | ||
} | ||
}, | ||
"nbformat": 4, | ||
"nbformat_minor": 5 | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
# coding: utf-8 | ||
|
||
|
||
import torch | ||
import torch.nn as nn | ||
from torch.utils.data import DataLoader | ||
from torchvision.datasets import MNIST | ||
from torchvision import transforms | ||
from ignite.engine import Events, create_supervised_trainer, create_supervised_evaluator | ||
from ignite.metrics import Accuracy, Loss | ||
from ignite.handlers import Checkpoint, DiskSaver | ||
from ignite.contrib.handlers import TensorboardLogger, global_step_from_engine | ||
|
||
# # Chapter 13: Going Deeper -- the Mechanics of PyTorch (Part 3/3) | ||
|
||
# ## Higher-level PyTorch APIs: a short introduction to PyTorch-Ignite | ||
|
||
# ### Setting up the PyTorch model | ||
|
||
|
||
|
||
|
||
|
||
|
||
image_path = './' | ||
torch.manual_seed(1) | ||
|
||
transform = transforms.Compose([ | ||
transforms.ToTensor() | ||
]) | ||
|
||
|
||
mnist_train_dataset = MNIST( | ||
root=image_path, | ||
train=True, | ||
transform=transform, | ||
download=True | ||
) | ||
|
||
mnist_val_dataset = MNIST( | ||
root=image_path, | ||
train=False, | ||
transform=transform, | ||
download=False | ||
) | ||
|
||
batch_size = 64 | ||
train_loader = DataLoader( | ||
mnist_train_dataset, batch_size, shuffle=True | ||
) | ||
|
||
val_loader = DataLoader( | ||
mnist_val_dataset, batch_size, shuffle=False | ||
) | ||
|
||
|
||
def get_model(image_shape=(1, 28, 28), hidden_units=(32, 16)): | ||
input_size = image_shape[0] * image_shape[1] * image_shape[2] | ||
all_layers = [nn.Flatten()] | ||
for hidden_unit in hidden_units: | ||
layer = nn.Linear(input_size, hidden_unit) | ||
all_layers.append(layer) | ||
all_layers.append(nn.ReLU()) | ||
input_size = hidden_unit | ||
|
||
all_layers.append(nn.Linear(hidden_units[-1], 10)) | ||
all_layers.append(nn.Softmax(dim=1)) | ||
model = nn.Sequential(*all_layers) | ||
return model | ||
|
||
|
||
device = "cuda" if torch.cuda.is_available() else "cpu" | ||
|
||
model = get_model().to(device) | ||
loss_fn = nn.CrossEntropyLoss() | ||
optimizer = torch.optim.Adam(model.parameters(), lr=0.001) | ||
|
||
|
||
# ### Setting up training and validation engines with PyTorch-Ignite | ||
|
||
|
||
|
||
|
||
|
||
trainer = create_supervised_trainer( | ||
model, optimizer, loss_fn, device=device | ||
) | ||
|
||
val_metrics = { | ||
"accuracy": Accuracy(), | ||
"loss": Loss(loss_fn) | ||
} | ||
|
||
evaluator = create_supervised_evaluator( | ||
model, metrics=val_metrics, device=device | ||
) | ||
|
||
|
||
# ### Creating event handlers for logging and validation | ||
|
||
|
||
|
||
# How many batches to wait before logging training status | ||
log_interval = 100 | ||
|
||
@trainer.on(Events.ITERATION_COMPLETED(every=log_interval)) | ||
def log_training_loss(): | ||
e = trainer.state.epoch | ||
max_e = trainer.state.max_epochs | ||
i = trainer.state.iteration | ||
batch_loss = trainer.state.output | ||
print(f"Epoch[{e}/{max_e}], Iter[{i}] Loss: {batch_loss:.2f}") | ||
|
||
|
||
|
||
|
||
@trainer.on(Events.EPOCH_COMPLETED) | ||
def log_validation_results(): | ||
eval_state = evaluator.run(val_loader) | ||
metrics = eval_state.metrics | ||
e = trainer.state.epoch | ||
max_e = trainer.state.max_epochs | ||
acc = metrics['accuracy'] | ||
avg_loss = metrics['loss'] | ||
print(f"Validation Results - Epoch[{e}/{max_e}] Avg Accuracy: {acc:.2f} Avg Loss: {avg_loss:.2f}") | ||
|
||
|
||
# ### Setting up training checkpoints and saving the best model | ||
|
||
|
||
|
||
|
||
# We will save in the checkpoint the following: | ||
to_save = {"model": model, "optimizer": optimizer, "trainer": trainer} | ||
|
||
# We will save checkpoints to the local disk | ||
output_path = "./output" | ||
save_handler = DiskSaver(dirname=output_path, require_empty=False) | ||
|
||
# Set up the handler: | ||
checkpoint_handler = Checkpoint( | ||
to_save, save_handler, filename_prefix="training") | ||
|
||
# Attach the handler to the trainer | ||
trainer.add_event_handler(Events.EPOCH_COMPLETED, checkpoint_handler) | ||
|
||
|
||
|
||
|
||
# Store best model by validation accuracy | ||
best_model_handler = Checkpoint( | ||
{"model": model}, | ||
save_handler, | ||
filename_prefix="best", | ||
n_saved=1, | ||
score_name="accuracy", | ||
score_function=Checkpoint.get_default_score_fn("accuracy"), | ||
) | ||
|
||
evaluator.add_event_handler(Events.COMPLETED, best_model_handler) | ||
|
||
|
||
# ### Setting up TensorBoard as an experiment tracking system | ||
|
||
|
||
|
||
|
||
|
||
tb_logger = TensorboardLogger(log_dir=output_path) | ||
|
||
# Attach handler to plot trainer's loss every 100 iterations | ||
tb_logger.attach_output_handler( | ||
trainer, | ||
event_name=Events.ITERATION_COMPLETED(every=100), | ||
tag="training", | ||
output_transform=lambda loss: {"batch_loss": loss}, | ||
) | ||
|
||
# Attach handler for plotting both evaluators' metrics after every epoch completes | ||
tb_logger.attach_output_handler( | ||
evaluator, | ||
event_name=Events.EPOCH_COMPLETED, | ||
tag="validation", | ||
metric_names="all", | ||
global_step_transform=global_step_from_engine(trainer), | ||
) | ||
|
||
|
||
# ### Executing the PyTorch-Ignite model training code | ||
|
||
|
||
|
||
trainer.run(train_loader, max_epochs=5) | ||
|
||
|
||
# --- | ||
# | ||
# Readers may ignore the next cell. | ||
|
||
|
||
|
||
|