Skip to content

Commit 50f4fd0

Browse files
committed
Added lesson 29 materials
1 parent ce32363 commit 50f4fd0

File tree

5 files changed

+940
-1
lines changed

5 files changed

+940
-1
lines changed

notebooks/notebooks.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,4 +257,13 @@ units:
257257
- name: "Activity"
258258
file: "Lesson_28_activity.ipynb"
259259
- name: "Activity solution"
260-
file: "Lesson_28_activity_solution.ipynb"
260+
file: "Lesson_28_activity_solution.ipynb"
261+
262+
- number: "29"
263+
title: "PyTorch"
264+
topics: "Basic deep learning with PyTorch"
265+
notebooks:
266+
- name: "In-class demo, part 1"
267+
file: "Lesson_29_demo_part1.ipynb"
268+
- name: "Activity part 1"
269+
file: "Lesson_29_activity_part1.ipynb "
Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "50f51079",
6+
"metadata": {},
7+
"source": [
8+
"# Lesson 29: PyTorch training loop activity\n",
9+
"\n",
10+
"In this activity, you will modify the basic PyTorch training loop from the lesson 29 demo to add:\n",
11+
"\n",
12+
"1. **Batching** - Process the training data in mini-batches instead of all at once\n",
13+
"2. **Validation** - Track model performance on a held-out validation set during training\n",
14+
"\n",
15+
"## Notebook set-up\n",
16+
"\n",
17+
"### Imports"
18+
]
19+
},
20+
{
21+
"cell_type": "code",
22+
"execution_count": null,
23+
"id": "13c76bb6",
24+
"metadata": {},
25+
"outputs": [],
26+
"source": [
27+
"# Third party imports\n",
28+
"import matplotlib.pyplot as plt\n",
29+
"import numpy as np\n",
30+
"import pandas as pd\n",
31+
"import torch\n",
32+
"import torch.nn as nn\n",
33+
"import torch.optim as optim\n",
34+
"\n",
35+
"# Set random seeds for reproducibility\n",
36+
"torch.manual_seed(315)\n",
37+
"np.random.seed(315)\n",
38+
"\n",
39+
"# Check for GPU availability\n",
40+
"device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
41+
"print(f'Using device: {device}')"
42+
]
43+
},
44+
{
45+
"cell_type": "markdown",
46+
"id": "aaa50a53",
47+
"metadata": {},
48+
"source": [
49+
"## 1. Load preprocessed data"
50+
]
51+
},
52+
{
53+
"cell_type": "code",
54+
"execution_count": null,
55+
"id": "9304299e",
56+
"metadata": {},
57+
"outputs": [],
58+
"source": [
59+
"data = pd.read_pickle('https://gperdrizet.github.io/FSA_devops/assets/data/unit4/preprocessed_housing_data.pkl')\n",
60+
"\n",
61+
"training_df = data['training_df']\n",
62+
"testing_df = data['testing_df']\n",
63+
"features = data['features']\n",
64+
"label = data['label']\n",
65+
"\n",
66+
"print(f'Training samples: {len(training_df)}')\n",
67+
"print(f'Testing samples: {len(testing_df)}')\n",
68+
"print(f'Features: {features}')\n",
69+
"print(f'Label: {label}')"
70+
]
71+
},
72+
{
73+
"cell_type": "markdown",
74+
"id": "3660a060",
75+
"metadata": {},
76+
"source": [
77+
"## 2. Prepare PyTorch tensors and DataLoaders\n",
78+
"\n",
79+
"### Task 1: Add batching and validation split\n",
80+
"\n",
81+
"Currently, the code below creates tensors for training and testing. Your task is to:\n",
82+
"\n",
83+
"1. **Add imports** for `TensorDataset` and `DataLoader` from `torch.utils.data`\n",
84+
"2. **Split training data** into train and validation sets (e.g., 80/20 split)\n",
85+
"3. **Create DataLoaders** for batched training\n",
86+
"\n",
87+
"**Hints:**\n",
88+
"- Use `torch.randperm()` to shuffle indices for the split\n",
89+
"- Use `TensorDataset(X, y)` to combine feature and label tensors\n",
90+
"- Use `DataLoader(dataset, batch_size=32, shuffle=True)` to create batches\n",
91+
"- Create separate DataLoaders for training and validation"
92+
]
93+
},
94+
{
95+
"cell_type": "code",
96+
"execution_count": null,
97+
"id": "a7bdc99c",
98+
"metadata": {},
99+
"outputs": [],
100+
"source": [
101+
"# Convert dataframes to PyTorch tensors and move to device\n",
102+
"X_train = torch.tensor(training_df[features].values, dtype=torch.float32).to(device)\n",
103+
"y_train = torch.tensor(training_df[label].values, dtype=torch.float32).unsqueeze(1).to(device)\n",
104+
"X_test = torch.tensor(testing_df[features].values, dtype=torch.float32).to(device)\n",
105+
"y_test = torch.tensor(testing_df[label].values, dtype=torch.float32).unsqueeze(1).to(device)\n",
106+
"\n",
107+
"print(f'X_train shape: {X_train.shape}')\n",
108+
"print(f'y_train shape: {y_train.shape}')\n",
109+
"print(f'X_test shape: {X_test.shape}')\n",
110+
"print(f'y_test shape: {y_test.shape}')"
111+
]
112+
},
113+
{
114+
"cell_type": "markdown",
115+
"id": "219abc95",
116+
"metadata": {},
117+
"source": [
118+
"## 3. Build model"
119+
]
120+
},
121+
{
122+
"cell_type": "code",
123+
"execution_count": null,
124+
"id": "cfb7fbbe",
125+
"metadata": {},
126+
"outputs": [],
127+
"source": [
128+
"model = nn.Sequential(\n",
129+
" nn.Linear(8, 64), # Fully connected layer (similar to tf.keras.layers.Dense)\n",
130+
" nn.ReLU(),\n",
131+
" nn.Dropout(0.2),\n",
132+
" nn.Linear(64, 32),\n",
133+
" nn.ReLU(),\n",
134+
" nn.Dropout(0.2),\n",
135+
" nn.Linear(32, 1)\n",
136+
").to(device)\n",
137+
"\n",
138+
"# Define loss function and optimizer\n",
139+
"criterion = nn.MSELoss()\n",
140+
"optimizer = optim.Adam(model.parameters(), lr=1e-2)\n",
141+
"\n",
142+
"print(model)"
143+
]
144+
},
145+
{
146+
"cell_type": "markdown",
147+
"id": "5823b5be",
148+
"metadata": {},
149+
"source": [
150+
"## 4. Training function\n",
151+
"\n",
152+
"### Task 2: Update training loop for batching and validation\n",
153+
"\n",
154+
"The current training loop processes all training data at once. Your task is to modify this function to:\n",
155+
"\n",
156+
"1. **Accept DataLoaders** instead of raw tensors\n",
157+
"2. **Iterate over batches** in an inner loop within each epoch\n",
158+
"3. **Compute validation metrics** after each training epoch\n",
159+
"\n",
160+
"**Hints:**\n",
161+
"- Change function signature to accept `train_loader` and `val_loader` instead of `X_train`, `y_train`\n",
162+
"- Add an inner `for X_batch, y_batch in train_loader:` loop\n",
163+
"- Accumulate loss across batches, then average for reporting\n",
164+
"- Use `model.eval()` and `torch.no_grad()` for validation\n",
165+
"- Track `val_loss` and `val_r2` in the history dictionary"
166+
]
167+
},
168+
{
169+
"cell_type": "code",
170+
"execution_count": null,
171+
"id": "d821d6cb",
172+
"metadata": {},
173+
"outputs": [],
174+
"source": [
175+
"def train_model(\n",
176+
" model: nn.Module,\n",
177+
" X_train: torch.Tensor,\n",
178+
" y_train: torch.Tensor,\n",
179+
" criterion: nn.Module,\n",
180+
" optimizer: optim.Optimizer,\n",
181+
" epochs: int = 50,\n",
182+
" print_every: int = 5\n",
183+
") -> dict[str, list[float]]:\n",
184+
" '''Basic training loop for PyTorch model.\n",
185+
" \n",
186+
" TODO: Modify this function to add:\n",
187+
" 1. Batching - process data in mini-batches\n",
188+
" 2. Validation - track performance on a validation set\n",
189+
" '''\n",
190+
" \n",
191+
" history = {'loss': [], 'r2': []}\n",
192+
" \n",
193+
" for epoch in range(epochs):\n",
194+
" # Set model to training mode\n",
195+
" model.train()\n",
196+
" \n",
197+
" # Zero the gradients\n",
198+
" optimizer.zero_grad()\n",
199+
" \n",
200+
" # Forward pass\n",
201+
" predictions = model(X_train)\n",
202+
" \n",
203+
" # Calculate loss\n",
204+
" loss = criterion(predictions, y_train)\n",
205+
" \n",
206+
" # Backward pass\n",
207+
" loss.backward()\n",
208+
" \n",
209+
" # Update weights\n",
210+
" optimizer.step()\n",
211+
" \n",
212+
" # Calculate R²\n",
213+
" with torch.no_grad():\n",
214+
" ss_res = torch.sum((y_train - predictions) ** 2)\n",
215+
" ss_tot = torch.sum((y_train - torch.mean(y_train)) ** 2)\n",
216+
" r2 = 1 - (ss_res / ss_tot)\n",
217+
" \n",
218+
" # Record metrics\n",
219+
" history['loss'].append(loss.item())\n",
220+
" history['r2'].append(r2.item())\n",
221+
" \n",
222+
" # Print progress\n",
223+
" if (epoch + 1) % print_every == 0 or epoch == 0:\n",
224+
" print(f'Epoch {epoch+1}/{epochs} - loss: {loss.item():.4f} - R²: {r2.item():.4f}')\n",
225+
" \n",
226+
" print('\\nTraining complete.')\n",
227+
" return history"
228+
]
229+
},
230+
{
231+
"cell_type": "markdown",
232+
"id": "bf715640",
233+
"metadata": {},
234+
"source": [
235+
"## 5. Train model"
236+
]
237+
},
238+
{
239+
"cell_type": "code",
240+
"execution_count": null,
241+
"id": "dee08328",
242+
"metadata": {},
243+
"outputs": [],
244+
"source": [
245+
"history = train_model(\n",
246+
" model=model,\n",
247+
" X_train=X_train,\n",
248+
" y_train=y_train,\n",
249+
" criterion=criterion,\n",
250+
" optimizer=optimizer,\n",
251+
" epochs=100,\n",
252+
" print_every=10\n",
253+
")"
254+
]
255+
},
256+
{
257+
"cell_type": "markdown",
258+
"id": "d3b088b5",
259+
"metadata": {},
260+
"source": [
261+
"## 6. Learning curves\n",
262+
"\n",
263+
"**Note:** Once you add validation, update these plots to show both training and validation metrics."
264+
]
265+
},
266+
{
267+
"cell_type": "code",
268+
"execution_count": null,
269+
"id": "abf68740",
270+
"metadata": {},
271+
"outputs": [],
272+
"source": [
273+
"fig, axes = plt.subplots(1, 2, figsize=(10, 4))\n",
274+
"\n",
275+
"axes[0].set_title('Training loss')\n",
276+
"axes[0].plot(history['loss'])\n",
277+
"axes[0].set_xlabel('Epoch')\n",
278+
"axes[0].set_ylabel('Loss (MSE)')\n",
279+
"\n",
280+
"axes[1].set_title('Training R²')\n",
281+
"axes[1].plot(history['r2'])\n",
282+
"axes[1].set_xlabel('Epoch')\n",
283+
"axes[1].set_ylabel('R²')\n",
284+
"\n",
285+
"plt.tight_layout()\n",
286+
"plt.show()"
287+
]
288+
},
289+
{
290+
"cell_type": "markdown",
291+
"id": "ffcd44a5",
292+
"metadata": {},
293+
"source": [
294+
"## 7. Test set evaluation"
295+
]
296+
},
297+
{
298+
"cell_type": "code",
299+
"execution_count": null,
300+
"id": "0b6adfed",
301+
"metadata": {},
302+
"outputs": [],
303+
"source": [
304+
"# Set model to evaluation mode\n",
305+
"model.eval()\n",
306+
"\n",
307+
"# Make predictions (no gradient calculation needed)\n",
308+
"with torch.no_grad():\n",
309+
" predictions = model(X_test).cpu().numpy().flatten()\n",
310+
"\n",
311+
"# Calculate R²\n",
312+
"ss_res = np.sum((testing_df[label].values - predictions) ** 2)\n",
313+
"ss_tot = np.sum((testing_df[label].values - np.mean(testing_df[label].values)) ** 2)\n",
314+
"rsquared = 1 - (ss_res / ss_tot)\n",
315+
"\n",
316+
"print(f'Model R² on test set: {rsquared:.4f}')"
317+
]
318+
},
319+
{
320+
"cell_type": "markdown",
321+
"id": "0bdf9c3d",
322+
"metadata": {},
323+
"source": [
324+
"## 8. Performance analysis"
325+
]
326+
},
327+
{
328+
"cell_type": "code",
329+
"execution_count": null,
330+
"id": "04a3a765",
331+
"metadata": {},
332+
"outputs": [],
333+
"source": [
334+
"fig, axes = plt.subplots(1, 2, figsize=(8, 4))\n",
335+
"\n",
336+
"axes[0].set_title('Model predictions')\n",
337+
"axes[0].scatter(\n",
338+
" testing_df[label], predictions,\n",
339+
" c='black', s=0.5, alpha=0.5\n",
340+
")\n",
341+
"axes[0].plot(\n",
342+
" [testing_df[label].min(), testing_df[label].max()],\n",
343+
" [testing_df[label].min(), testing_df[label].max()],\n",
344+
" color='red', linestyle='--'\n",
345+
")\n",
346+
"axes[0].set_xlabel('True values (standardized)')\n",
347+
"axes[0].set_ylabel('Predicted values (standardized)')\n",
348+
"\n",
349+
"axes[1].set_title('Residuals vs predicted values')\n",
350+
"axes[1].scatter(\n",
351+
" predictions, testing_df[label] - predictions,\n",
352+
" c='black', s=0.5, alpha=0.5\n",
353+
")\n",
354+
"axes[1].axhline(0, color='red', linestyle='--')\n",
355+
"axes[1].set_xlabel('Predicted values (standardized)')\n",
356+
"axes[1].set_ylabel('Residuals (standardized)')\n",
357+
"\n",
358+
"plt.tight_layout()\n",
359+
"plt.show()"
360+
]
361+
}
362+
],
363+
"metadata": {
364+
"language_info": {
365+
"name": "python"
366+
}
367+
},
368+
"nbformat": 4,
369+
"nbformat_minor": 5
370+
}

0 commit comments

Comments
 (0)