Spaces:
Running
Running
abril4416 commited on
Commit ·
c745dff
1
Parent(s): 522ffd0
Add Gradio hub with linear and logistic regression interfaces
Browse files- app.py +55 -0
- linear-regression/app.js +1326 -0
- linear-regression/gradient-descent.html +116 -0
- linear-regression/gradient.js +829 -0
- linear-regression/index.html +50 -0
- linear-regression/linear-regression-steps.html +65 -0
- linear-regression/linear-regression-steps.js +387 -0
- linear-regression/numpy-lab.html +76 -0
- linear-regression/styles.css +428 -0
- logistic-regression/about.html +37 -0
- logistic-regression/app.js +381 -0
- logistic-regression/confusion-matrix.html +73 -0
- logistic-regression/confusion.js +608 -0
- logistic-regression/cost-visualization.html +79 -0
- logistic-regression/cost.js +581 -0
- logistic-regression/index.html +59 -0
- logistic-regression/sigmoid.html +72 -0
- logistic-regression/simple-sigmoid.html +56 -0
- logistic-regression/simple.js +205 -0
- logistic-regression/styles.css +293 -0
app.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pathlib import Path
|
| 2 |
+
|
| 3 |
+
import gradio as gr
|
| 4 |
+
from fastapi import FastAPI
|
| 5 |
+
from fastapi.staticfiles import StaticFiles
|
| 6 |
+
|
| 7 |
+
BASE_DIR = Path(__file__).resolve().parent
|
| 8 |
+
|
| 9 |
+
linear_dir = BASE_DIR / "linear-regression"
|
| 10 |
+
logistic_dir = BASE_DIR / "logistic-regression"
|
| 11 |
+
|
| 12 |
+
with gr.Blocks(title="DDW Machine Learning") as demo:
|
| 13 |
+
gr.Markdown(
|
| 14 |
+
"""
|
| 15 |
+
# DDW Machine Learning
|
| 16 |
+
|
| 17 |
+
Choose one interface:
|
| 18 |
+
- **Linear Regression** (`week10/interface`)
|
| 19 |
+
- **Logistic Regression** (`week11/interface`)
|
| 20 |
+
|
| 21 |
+
Each interface opens with its own home page and keeps all original sub-page navigation.
|
| 22 |
+
"""
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
with gr.Row():
|
| 26 |
+
gr.HTML(
|
| 27 |
+
"""
|
| 28 |
+
<div style="border:1px solid #ddd;border-radius:12px;padding:16px;">
|
| 29 |
+
<h3>Linear Regression</h3>
|
| 30 |
+
<p>Week10 interface with NumPy Lab, Gradient Descent Studio, and Step Trainer.</p>
|
| 31 |
+
<a href="/linear-regression/index.html" target="_self">Open Linear Regression Interface</a>
|
| 32 |
+
</div>
|
| 33 |
+
"""
|
| 34 |
+
)
|
| 35 |
+
gr.HTML(
|
| 36 |
+
"""
|
| 37 |
+
<div style="border:1px solid #ddd;border-radius:12px;padding:16px;">
|
| 38 |
+
<h3>Logistic Regression</h3>
|
| 39 |
+
<p>Week11 interface with sigmoid, confusion matrix, and cost-function pages.</p>
|
| 40 |
+
<a href="/logistic-regression/index.html" target="_self">Open Logistic Regression Interface</a>
|
| 41 |
+
</div>
|
| 42 |
+
"""
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
app = FastAPI()
|
| 46 |
+
|
| 47 |
+
app.mount("/linear-regression", StaticFiles(directory=str(linear_dir), html=True), name="linear-regression")
|
| 48 |
+
app.mount("/logistic-regression", StaticFiles(directory=str(logistic_dir), html=True), name="logistic-regression")
|
| 49 |
+
app = gr.mount_gradio_app(app, demo, path="/")
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
if __name__ == "__main__":
|
| 53 |
+
import uvicorn
|
| 54 |
+
|
| 55 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
linear-regression/app.js
ADDED
|
@@ -0,0 +1,1326 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const operations = [
|
| 2 |
+
{
|
| 3 |
+
id: "add",
|
| 4 |
+
label: "Add two matrices (element-wise)",
|
| 5 |
+
description: "A + B. Shapes of A and B should be equal or broadcast-compatible.",
|
| 6 |
+
inputs: 2,
|
| 7 |
+
code: "output = A + B",
|
| 8 |
+
match: ["add", "sum", "plus", "element-wise add"],
|
| 9 |
+
run: (A, B) => elementWise(A, B, (a, b) => a + b, "add"),
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
id: "subtract",
|
| 13 |
+
label: "Subtract two matrices (element-wise)",
|
| 14 |
+
description: "A - B. Shapes of A and B should be equal or broadcast-compatible.",
|
| 15 |
+
inputs: 2,
|
| 16 |
+
code: "output = A - B",
|
| 17 |
+
match: ["subtract", "minus", "difference"],
|
| 18 |
+
run: (A, B) => elementWise(A, B, (a, b) => a - b, "subtract"),
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
id: "multiply",
|
| 22 |
+
label: "Multiply two matrices (element-wise)",
|
| 23 |
+
description: "A * B. Shapes of A and B should be equal or broadcast-compatible.",
|
| 24 |
+
inputs: 2,
|
| 25 |
+
code: "output = A * B",
|
| 26 |
+
match: ["multiply", "times", "element-wise multiply"],
|
| 27 |
+
run: (A, B) => elementWise(A, B, (a, b) => a * b, "multiply"),
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
id: "matmul",
|
| 31 |
+
label: "Matrix multiplication (np.matmul)",
|
| 32 |
+
description: "np.matmul(A, B). Works for 2D and stacked arrays (up to 3D here).",
|
| 33 |
+
inputs: 2,
|
| 34 |
+
code: "output = np.matmul(A, B)",
|
| 35 |
+
match: ["matmul", "matrix multiplication", "dot product", "@"],
|
| 36 |
+
run: (A, B) => matmul(A, B),
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
id: "transpose",
|
| 40 |
+
label: "Transpose matrix (np.transpose)",
|
| 41 |
+
description: "Swap axes. For 2D, rows and columns are flipped.",
|
| 42 |
+
inputs: 1,
|
| 43 |
+
code: "output = np.transpose(A)",
|
| 44 |
+
match: ["transpose", "swap axes", "flip rows columns"],
|
| 45 |
+
run: (A) => transpose(A),
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
id: "reshape",
|
| 49 |
+
label: "Reshape matrix (np.reshape)",
|
| 50 |
+
description: "Reshape A into the target shape with same total number of elements.",
|
| 51 |
+
inputs: 1,
|
| 52 |
+
code: "output = np.reshape(A, new_shape)",
|
| 53 |
+
match: ["reshape", "change shape"],
|
| 54 |
+
requiresTarget: true,
|
| 55 |
+
run: (A, _unused, targetShape) => reshape(A, targetShape),
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
id: "concat",
|
| 59 |
+
label: "Concatenate two arrays (np.concatenate)",
|
| 60 |
+
description: "Join A and B along axis 0. Non-concat dimensions must match.",
|
| 61 |
+
inputs: 2,
|
| 62 |
+
code: "output = np.concatenate([A, B], axis=0)",
|
| 63 |
+
match: ["concatenate", "concat", "join arrays"],
|
| 64 |
+
run: (A, B) => concatAxis0(A, B),
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
id: "stack",
|
| 68 |
+
label: "Stack two arrays (np.stack)",
|
| 69 |
+
description: "Stack A and B along a new axis 0. A and B shapes must be identical.",
|
| 70 |
+
inputs: 2,
|
| 71 |
+
code: "output = np.stack([A, B], axis=0)",
|
| 72 |
+
match: ["stack", "new axis"],
|
| 73 |
+
run: (A, B) => stackAxis0(A, B),
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
id: "sum",
|
| 77 |
+
label: "Sum matrix values (np.sum)",
|
| 78 |
+
description: "Sum values on an optional axis with optional keepdims.",
|
| 79 |
+
inputs: 1,
|
| 80 |
+
code: "output = np.sum(A)",
|
| 81 |
+
match: ["sum all", "total", "np.sum", "sum"],
|
| 82 |
+
supportsReductionOptions: true,
|
| 83 |
+
run: (A, _unused, _unused2, options) =>
|
| 84 |
+
reduceArray(A, {
|
| 85 |
+
mode: "sum",
|
| 86 |
+
axis: options.axis,
|
| 87 |
+
keepdims: options.keepdims,
|
| 88 |
+
}),
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
id: "mean",
|
| 92 |
+
label: "Mean matrix values (np.mean)",
|
| 93 |
+
description: "Compute mean on an optional axis with optional keepdims.",
|
| 94 |
+
inputs: 1,
|
| 95 |
+
code: "output = np.mean(A)",
|
| 96 |
+
match: ["mean", "average", "np.mean"],
|
| 97 |
+
supportsReductionOptions: true,
|
| 98 |
+
run: (A, _unused, _unused2, options) =>
|
| 99 |
+
reduceArray(A, {
|
| 100 |
+
mode: "mean",
|
| 101 |
+
axis: options.axis,
|
| 102 |
+
keepdims: options.keepdims,
|
| 103 |
+
}),
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
id: "ones",
|
| 107 |
+
label: "Create ones matrix (np.ones)",
|
| 108 |
+
description: "Generate an array filled with ones for the given shape.",
|
| 109 |
+
inputs: 1,
|
| 110 |
+
code: "output = np.ones(shape)",
|
| 111 |
+
match: ["ones", "all ones", "np.ones"],
|
| 112 |
+
generatorOnly: true,
|
| 113 |
+
run: (A) => clone(A),
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
id: "zeros",
|
| 117 |
+
label: "Create zeros matrix (np.zeros)",
|
| 118 |
+
description: "Generate an array filled with zeros for the given shape.",
|
| 119 |
+
inputs: 1,
|
| 120 |
+
code: "output = np.zeros(shape)",
|
| 121 |
+
match: ["zeros", "all zeros", "np.zeros"],
|
| 122 |
+
generatorOnly: true,
|
| 123 |
+
run: (A) => clone(A),
|
| 124 |
+
},
|
| 125 |
+
];
|
| 126 |
+
|
| 127 |
+
const operationSelect = document.getElementById("operationSelect");
|
| 128 |
+
const nlInput = document.getElementById("nlInput");
|
| 129 |
+
const matchBtn = document.getElementById("matchBtn");
|
| 130 |
+
const runBtn = document.getElementById("runBtn");
|
| 131 |
+
const operationInfo = document.getElementById("operationInfo");
|
| 132 |
+
const shapeInputs = document.getElementById("shapeInputs");
|
| 133 |
+
const operationOptions = document.getElementById("operationOptions");
|
| 134 |
+
const codeOutput = document.getElementById("codeOutput");
|
| 135 |
+
const inputViz = document.getElementById("inputViz");
|
| 136 |
+
const outputViz = document.getElementById("outputViz");
|
| 137 |
+
const detailViz = document.getElementById("detailViz");
|
| 138 |
+
let lastRunMeta = null;
|
| 139 |
+
|
| 140 |
+
function init() {
|
| 141 |
+
operations.forEach((op) => {
|
| 142 |
+
const option = document.createElement("option");
|
| 143 |
+
option.value = op.id;
|
| 144 |
+
option.textContent = op.label;
|
| 145 |
+
operationSelect.appendChild(option);
|
| 146 |
+
});
|
| 147 |
+
|
| 148 |
+
operationSelect.value = "add";
|
| 149 |
+
renderShapeInputs();
|
| 150 |
+
bindEvents();
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
function bindEvents() {
|
| 154 |
+
operationSelect.addEventListener("change", renderShapeInputs);
|
| 155 |
+
|
| 156 |
+
matchBtn.addEventListener("click", () => {
|
| 157 |
+
const query = nlInput.value.trim().toLowerCase();
|
| 158 |
+
if (!query) return;
|
| 159 |
+
|
| 160 |
+
let best = operations[0];
|
| 161 |
+
let bestScore = 0;
|
| 162 |
+
|
| 163 |
+
for (const op of operations) {
|
| 164 |
+
let score = 0;
|
| 165 |
+
for (const key of op.match) {
|
| 166 |
+
if (query.includes(key)) score += key.length;
|
| 167 |
+
}
|
| 168 |
+
if (score > bestScore) {
|
| 169 |
+
best = op;
|
| 170 |
+
bestScore = score;
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
operationSelect.value = best.id;
|
| 175 |
+
renderShapeInputs();
|
| 176 |
+
});
|
| 177 |
+
|
| 178 |
+
runBtn.addEventListener("click", () => {
|
| 179 |
+
try {
|
| 180 |
+
lastRunMeta = null;
|
| 181 |
+
const op = currentOperation();
|
| 182 |
+
const parsed = parseAllShapes(op);
|
| 183 |
+
const inputArrays = buildInputArrays(op, parsed);
|
| 184 |
+
const options = parsed.options || {};
|
| 185 |
+
const output = op.run(
|
| 186 |
+
inputArrays[0],
|
| 187 |
+
inputArrays[1],
|
| 188 |
+
parsed.targetShape || null,
|
| 189 |
+
options
|
| 190 |
+
);
|
| 191 |
+
|
| 192 |
+
renderCode(op, parsed);
|
| 193 |
+
renderInputs(inputArrays, op);
|
| 194 |
+
renderOutput(output);
|
| 195 |
+
renderComputationDetails(op, inputArrays, output, parsed, lastRunMeta);
|
| 196 |
+
} catch (err) {
|
| 197 |
+
codeOutput.textContent = "Error: " + err.message;
|
| 198 |
+
inputViz.innerHTML = "";
|
| 199 |
+
outputViz.innerHTML = `<p class=\"error\">${escapeHtml(err.message)}</p>`;
|
| 200 |
+
detailViz.innerHTML = "";
|
| 201 |
+
}
|
| 202 |
+
});
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
function currentOperation() {
|
| 206 |
+
return operations.find((op) => op.id === operationSelect.value);
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
function renderShapeInputs() {
|
| 210 |
+
const op = currentOperation();
|
| 211 |
+
|
| 212 |
+
operationInfo.innerHTML = `<strong>${op.label}</strong><br/>${escapeHtml(op.description)}`;
|
| 213 |
+
|
| 214 |
+
const cards = [];
|
| 215 |
+
for (let i = 0; i < op.inputs; i += 1) {
|
| 216 |
+
cards.push(shapeCard(`inputShape${i + 1}`, `Input ${i + 1} shape`, "2,3"));
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
if (op.requiresTarget) {
|
| 220 |
+
cards.push(shapeCard("targetShape", "Target shape", "3,2"));
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
shapeInputs.innerHTML = cards.join("");
|
| 224 |
+
operationOptions.innerHTML = op.supportsReductionOptions
|
| 225 |
+
? reductionOptionsCard()
|
| 226 |
+
: "";
|
| 227 |
+
|
| 228 |
+
const s1 = document.getElementById("inputShape1");
|
| 229 |
+
const s2 = document.getElementById("inputShape2");
|
| 230 |
+
|
| 231 |
+
if (op.id === "matmul") {
|
| 232 |
+
s1.value = "2,3";
|
| 233 |
+
if (s2) s2.value = "3,2";
|
| 234 |
+
} else if (op.id === "concat" || op.id === "stack") {
|
| 235 |
+
s1.value = "2,2";
|
| 236 |
+
if (s2) s2.value = "2,2";
|
| 237 |
+
} else if (op.id === "transpose") {
|
| 238 |
+
s1.value = "2,3";
|
| 239 |
+
} else if (op.id === "reshape") {
|
| 240 |
+
s1.value = "2,3";
|
| 241 |
+
document.getElementById("targetShape").value = "3,2";
|
| 242 |
+
} else if (op.id === "ones" || op.id === "zeros") {
|
| 243 |
+
s1.value = "3,3";
|
| 244 |
+
} else if (op.id === "sum" || op.id === "mean") {
|
| 245 |
+
s1.value = "2,3";
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
if (op.supportsReductionOptions) {
|
| 249 |
+
const axisInput = document.getElementById("reduceAxis");
|
| 250 |
+
const keepdimsInput = document.getElementById("reduceKeepdims");
|
| 251 |
+
|
| 252 |
+
const setAxisHint = () => {
|
| 253 |
+
try {
|
| 254 |
+
const dims = parseShape(document.getElementById("inputShape1").value);
|
| 255 |
+
axisInput.placeholder = `axis (optional): 0 to ${dims.length - 1}`;
|
| 256 |
+
} catch (_err) {
|
| 257 |
+
axisInput.placeholder = "axis (optional): 0";
|
| 258 |
+
}
|
| 259 |
+
};
|
| 260 |
+
|
| 261 |
+
document.getElementById("inputShape1").addEventListener("input", setAxisHint);
|
| 262 |
+
setAxisHint();
|
| 263 |
+
keepdimsInput.checked = false;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
detailViz.innerHTML = "";
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
function shapeCard(inputId, label, placeholder) {
|
| 270 |
+
return `
|
| 271 |
+
<div class="shape-card">
|
| 272 |
+
<h3>${label}</h3>
|
| 273 |
+
<input id="${inputId}" type="text" placeholder="${placeholder}" />
|
| 274 |
+
<small>Use comma-separated dimensions, max 3D, e.g. 2,3 or 2,2,3</small>
|
| 275 |
+
</div>
|
| 276 |
+
`;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
function reductionOptionsCard() {
|
| 280 |
+
return `
|
| 281 |
+
<div class="shape-card options-card">
|
| 282 |
+
<h3>Reduction options</h3>
|
| 283 |
+
<div class="inline-fields">
|
| 284 |
+
<label class="mini-label" for="reduceAxis">axis</label>
|
| 285 |
+
<input id="reduceAxis" type="text" placeholder="axis (optional): 0" />
|
| 286 |
+
</div>
|
| 287 |
+
<div class="inline-fields">
|
| 288 |
+
<label class="mini-label" for="reduceKeepdims">keepdims</label>
|
| 289 |
+
<input id="reduceKeepdims" type="checkbox" />
|
| 290 |
+
</div>
|
| 291 |
+
<small>Leave axis empty to reduce all dimensions. keepdims keeps reduced axes as size 1.</small>
|
| 292 |
+
</div>
|
| 293 |
+
`;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
function parseAllShapes(op) {
|
| 297 |
+
const shapes = [];
|
| 298 |
+
|
| 299 |
+
for (let i = 0; i < op.inputs; i += 1) {
|
| 300 |
+
const input = document.getElementById(`inputShape${i + 1}`);
|
| 301 |
+
shapes.push(parseShape(input.value));
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
const parsed = { shapes };
|
| 305 |
+
|
| 306 |
+
if (op.requiresTarget) {
|
| 307 |
+
parsed.targetShape = parseShape(document.getElementById("targetShape").value);
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
if (op.supportsReductionOptions) {
|
| 311 |
+
const axisRaw = document.getElementById("reduceAxis").value.trim();
|
| 312 |
+
const keepdims = document.getElementById("reduceKeepdims").checked;
|
| 313 |
+
let axis = null;
|
| 314 |
+
|
| 315 |
+
if (axisRaw !== "") {
|
| 316 |
+
axis = Number(axisRaw);
|
| 317 |
+
if (!Number.isInteger(axis)) {
|
| 318 |
+
throw new Error("axis must be an integer or left empty.");
|
| 319 |
+
}
|
| 320 |
+
if (axis < 0 || axis >= shapes[0].length) {
|
| 321 |
+
throw new Error(`axis out of range for input rank ${shapes[0].length}.`);
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
parsed.options = { axis, keepdims };
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
return parsed;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
function parseShape(raw) {
|
| 332 |
+
if (!raw || !raw.trim()) {
|
| 333 |
+
throw new Error("Shape cannot be empty.");
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
let normalized = raw.trim();
|
| 337 |
+
if (normalized.startsWith("(") && normalized.endsWith(")")) {
|
| 338 |
+
normalized = normalized.slice(1, -1);
|
| 339 |
+
}
|
| 340 |
+
normalized = normalized.trim();
|
| 341 |
+
|
| 342 |
+
const dims = normalized
|
| 343 |
+
.split(",")
|
| 344 |
+
.map((x) => x.trim())
|
| 345 |
+
.filter((x) => x.length > 0)
|
| 346 |
+
.map((x) => Number(x));
|
| 347 |
+
|
| 348 |
+
if (dims.length < 1 || dims.length > 3) {
|
| 349 |
+
throw new Error("Each shape must have 1 to 3 dimensions.");
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
dims.forEach((d) => {
|
| 353 |
+
if (!Number.isFinite(d) || !Number.isInteger(d) || d < 1 || d > 6) {
|
| 354 |
+
throw new Error("Dimensions must be integers between 1 and 6.");
|
| 355 |
+
}
|
| 356 |
+
});
|
| 357 |
+
|
| 358 |
+
return dims;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
function buildInputArrays(op, parsed) {
|
| 362 |
+
if (op.id === "ones") {
|
| 363 |
+
return [fillArray(parsed.shapes[0], 1)];
|
| 364 |
+
}
|
| 365 |
+
if (op.id === "zeros") {
|
| 366 |
+
return [fillArray(parsed.shapes[0], 0)];
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
const [shapeA, shapeB] = parsed.shapes;
|
| 370 |
+
const A = randomArray(shapeA);
|
| 371 |
+
if (op.inputs === 1) return [A];
|
| 372 |
+
|
| 373 |
+
let B;
|
| 374 |
+
if (["add", "subtract", "multiply"].includes(op.id)) {
|
| 375 |
+
B = randomArray(shapeB || shapeA);
|
| 376 |
+
} else if (op.id === "matmul") {
|
| 377 |
+
B = randomArray(shapeB);
|
| 378 |
+
} else if (op.id === "concat" || op.id === "stack") {
|
| 379 |
+
B = randomArray(shapeB);
|
| 380 |
+
} else {
|
| 381 |
+
B = randomArray(shapeB || shapeA);
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
return [A, B];
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
function randomArray(shape) {
|
| 388 |
+
return createByShape(shape, () => Math.floor(Math.random() * 9) + 1);
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
function fillArray(shape, val) {
|
| 392 |
+
return createByShape(shape, () => val);
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
function createByShape(shape, valueFn, level = 0) {
|
| 396 |
+
const len = shape[level];
|
| 397 |
+
const arr = new Array(len);
|
| 398 |
+
|
| 399 |
+
for (let i = 0; i < len; i += 1) {
|
| 400 |
+
arr[i] =
|
| 401 |
+
level === shape.length - 1
|
| 402 |
+
? valueFn()
|
| 403 |
+
: createByShape(shape, valueFn, level + 1);
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
return arr;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
function shapeOf(arr) {
|
| 410 |
+
if (!Array.isArray(arr)) return [];
|
| 411 |
+
return [arr.length, ...shapeOf(arr[0])];
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
function formatShape(shape) {
|
| 415 |
+
return `(${shape.join(", ")})`;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
function broadcastShapes(shapeA, shapeB) {
|
| 419 |
+
const maxRank = Math.max(shapeA.length, shapeB.length);
|
| 420 |
+
const out = new Array(maxRank);
|
| 421 |
+
|
| 422 |
+
for (let i = 0; i < maxRank; i += 1) {
|
| 423 |
+
const a = shapeA[shapeA.length - 1 - i] ?? 1;
|
| 424 |
+
const b = shapeB[shapeB.length - 1 - i] ?? 1;
|
| 425 |
+
|
| 426 |
+
if (a !== b && a !== 1 && b !== 1) {
|
| 427 |
+
throw new Error(
|
| 428 |
+
`Broadcast mismatch at dimension ${maxRank - i - 1}: ${a} vs ${b}.`
|
| 429 |
+
);
|
| 430 |
+
}
|
| 431 |
+
out[maxRank - 1 - i] = Math.max(a, b);
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
return out;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
function getAtIndices(arr, indices) {
|
| 438 |
+
let cur = arr;
|
| 439 |
+
for (let i = 0; i < indices.length; i += 1) {
|
| 440 |
+
cur = cur[indices[i]];
|
| 441 |
+
}
|
| 442 |
+
return cur;
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
function createByShapeIndexed(shape, valueFn, idx = []) {
|
| 446 |
+
if (shape.length === 0) return valueFn(idx);
|
| 447 |
+
const dim = shape[idx.length];
|
| 448 |
+
const out = new Array(dim);
|
| 449 |
+
for (let i = 0; i < dim; i += 1) {
|
| 450 |
+
const nextIdx = idx.concat(i);
|
| 451 |
+
if (nextIdx.length === shape.length) {
|
| 452 |
+
out[i] = valueFn(nextIdx);
|
| 453 |
+
} else {
|
| 454 |
+
out[i] = createByShapeIndexed(shape, valueFn, nextIdx);
|
| 455 |
+
}
|
| 456 |
+
}
|
| 457 |
+
return out;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
function projectBroadcastIndices(outputIndices, sourceShape) {
|
| 461 |
+
const offset = outputIndices.length - sourceShape.length;
|
| 462 |
+
const mapped = [];
|
| 463 |
+
for (let i = 0; i < sourceShape.length; i += 1) {
|
| 464 |
+
const srcDim = sourceShape[i];
|
| 465 |
+
const outIndex = outputIndices[offset + i];
|
| 466 |
+
mapped.push(srcDim === 1 ? 0 : outIndex);
|
| 467 |
+
}
|
| 468 |
+
return mapped;
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
function elementWise(A, B, fn, opName) {
|
| 472 |
+
const shapeA = shapeOf(A);
|
| 473 |
+
const shapeB = shapeOf(B);
|
| 474 |
+
const outputShape = broadcastShapes(shapeA, shapeB);
|
| 475 |
+
|
| 476 |
+
const output = createByShapeIndexed(outputShape, (outIdx) => {
|
| 477 |
+
const idxA = projectBroadcastIndices(outIdx, shapeA);
|
| 478 |
+
const idxB = projectBroadcastIndices(outIdx, shapeB);
|
| 479 |
+
return fn(getAtIndices(A, idxA), getAtIndices(B, idxB));
|
| 480 |
+
});
|
| 481 |
+
|
| 482 |
+
const usedBroadcasting =
|
| 483 |
+
shapeA.length !== shapeB.length ||
|
| 484 |
+
shapeA.some((dim, i) => dim !== shapeB[i]) ||
|
| 485 |
+
shapeA.join(",") !== outputShape.join(",") ||
|
| 486 |
+
shapeB.join(",") !== outputShape.join(",");
|
| 487 |
+
|
| 488 |
+
lastRunMeta = {
|
| 489 |
+
kind: "elementwise",
|
| 490 |
+
opName,
|
| 491 |
+
shapeA,
|
| 492 |
+
shapeB,
|
| 493 |
+
outputShape,
|
| 494 |
+
usedBroadcasting,
|
| 495 |
+
};
|
| 496 |
+
|
| 497 |
+
return output;
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
function elementWiseDeep(A, B, fn) {
|
| 501 |
+
if (!Array.isArray(A) && !Array.isArray(B)) return fn(A, B);
|
| 502 |
+
return A.map((v, i) => elementWiseDeep(v, B[i], fn));
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
function transpose(A) {
|
| 506 |
+
const shape = shapeOf(A);
|
| 507 |
+
if (shape.length === 1) return clone(A);
|
| 508 |
+
|
| 509 |
+
if (shape.length === 2) {
|
| 510 |
+
const [rows, cols] = shape;
|
| 511 |
+
const out = [];
|
| 512 |
+
for (let c = 0; c < cols; c += 1) {
|
| 513 |
+
const row = [];
|
| 514 |
+
for (let r = 0; r < rows; r += 1) {
|
| 515 |
+
row.push(A[r][c]);
|
| 516 |
+
}
|
| 517 |
+
out.push(row);
|
| 518 |
+
}
|
| 519 |
+
return out;
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
if (shape.length === 3) {
|
| 523 |
+
const [d0, d1, d2] = shape;
|
| 524 |
+
const out = [];
|
| 525 |
+
for (let i = 0; i < d2; i += 1) {
|
| 526 |
+
const level2 = [];
|
| 527 |
+
for (let j = 0; j < d1; j += 1) {
|
| 528 |
+
const row = [];
|
| 529 |
+
for (let k = 0; k < d0; k += 1) {
|
| 530 |
+
row.push(A[k][j][i]);
|
| 531 |
+
}
|
| 532 |
+
level2.push(row);
|
| 533 |
+
}
|
| 534 |
+
out.push(level2);
|
| 535 |
+
}
|
| 536 |
+
return out;
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
throw new Error("Transpose supports up to 3D in this interface.");
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
function reshape(A, targetShape) {
|
| 543 |
+
const flat = flatten(A);
|
| 544 |
+
const totalA = flat.length;
|
| 545 |
+
const totalTarget = targetShape.reduce((x, y) => x * y, 1);
|
| 546 |
+
|
| 547 |
+
if (totalA !== totalTarget) {
|
| 548 |
+
throw new Error(
|
| 549 |
+
`reshape needs same number of elements. Got ${totalA} and ${totalTarget}.`
|
| 550 |
+
);
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
return unflatten(flat, targetShape);
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
function flatten(arr) {
|
| 557 |
+
if (!Array.isArray(arr)) return [arr];
|
| 558 |
+
return arr.flatMap((x) => flatten(x));
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
function unflatten(flat, shape) {
|
| 562 |
+
let idx = 0;
|
| 563 |
+
function build(level = 0) {
|
| 564 |
+
const len = shape[level];
|
| 565 |
+
const out = [];
|
| 566 |
+
for (let i = 0; i < len; i += 1) {
|
| 567 |
+
if (level === shape.length - 1) {
|
| 568 |
+
out.push(flat[idx]);
|
| 569 |
+
idx += 1;
|
| 570 |
+
} else {
|
| 571 |
+
out.push(build(level + 1));
|
| 572 |
+
}
|
| 573 |
+
}
|
| 574 |
+
return out;
|
| 575 |
+
}
|
| 576 |
+
return build();
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
function matmul(A, B) {
|
| 580 |
+
const sA = shapeOf(A);
|
| 581 |
+
const sB = shapeOf(B);
|
| 582 |
+
const leftVec = sA.length === 1;
|
| 583 |
+
const rightVec = sB.length === 1;
|
| 584 |
+
|
| 585 |
+
const leftBatch = sA.length === 3 ? sA[0] : 1;
|
| 586 |
+
const rightBatch = sB.length === 3 ? sB[0] : 1;
|
| 587 |
+
const outBatch = Math.max(leftBatch, rightBatch);
|
| 588 |
+
|
| 589 |
+
if (leftBatch !== rightBatch && leftBatch !== 1 && rightBatch !== 1) {
|
| 590 |
+
throw new Error(
|
| 591 |
+
`matmul batch broadcast mismatch: ${leftBatch} vs ${rightBatch}.`
|
| 592 |
+
);
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
const leftRows = leftVec ? 1 : sA[sA.length - 2];
|
| 596 |
+
const leftInner = sA[sA.length - 1];
|
| 597 |
+
const rightInner = rightVec ? sB[0] : sB[sB.length - 2];
|
| 598 |
+
const rightCols = rightVec ? 1 : sB[sB.length - 1];
|
| 599 |
+
|
| 600 |
+
if (leftInner !== rightInner) {
|
| 601 |
+
throw new Error(
|
| 602 |
+
`matmul shape mismatch on core dims: ${formatShape(sA)} @ ${formatShape(
|
| 603 |
+
sB
|
| 604 |
+
)} (inner ${leftInner} vs ${rightInner}).`
|
| 605 |
+
);
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
const getLeft = (batch, row, k) => {
|
| 609 |
+
if (leftVec) return A[k];
|
| 610 |
+
if (sA.length === 2) return A[row][k];
|
| 611 |
+
const batchIdx = leftBatch === 1 ? 0 : batch;
|
| 612 |
+
return A[batchIdx][row][k];
|
| 613 |
+
};
|
| 614 |
+
|
| 615 |
+
const getRight = (batch, k, col) => {
|
| 616 |
+
if (rightVec) return B[k];
|
| 617 |
+
if (sB.length === 2) return B[k][col];
|
| 618 |
+
const batchIdx = rightBatch === 1 ? 0 : batch;
|
| 619 |
+
return B[batchIdx][k][col];
|
| 620 |
+
};
|
| 621 |
+
|
| 622 |
+
const matrixForBatch = (batch) => {
|
| 623 |
+
const out = [];
|
| 624 |
+
for (let r = 0; r < leftRows; r += 1) {
|
| 625 |
+
const row = [];
|
| 626 |
+
for (let c = 0; c < rightCols; c += 1) {
|
| 627 |
+
let sum = 0;
|
| 628 |
+
for (let k = 0; k < leftInner; k += 1) {
|
| 629 |
+
sum += getLeft(batch, r, k) * getRight(batch, k, c);
|
| 630 |
+
}
|
| 631 |
+
row.push(sum);
|
| 632 |
+
}
|
| 633 |
+
out.push(row);
|
| 634 |
+
}
|
| 635 |
+
return out;
|
| 636 |
+
};
|
| 637 |
+
|
| 638 |
+
const hasBatchAxis = sA.length > 2 || sB.length > 2;
|
| 639 |
+
const matrices = hasBatchAxis
|
| 640 |
+
? Array.from({ length: outBatch }, (_, batch) => matrixForBatch(batch))
|
| 641 |
+
: [matrixForBatch(0)];
|
| 642 |
+
|
| 643 |
+
let output;
|
| 644 |
+
if (leftVec && rightVec) {
|
| 645 |
+
output = hasBatchAxis ? matrices.map((m) => m[0][0]) : matrices[0][0][0];
|
| 646 |
+
} else if (leftVec) {
|
| 647 |
+
output = hasBatchAxis ? matrices.map((m) => m[0].slice()) : matrices[0][0].slice();
|
| 648 |
+
} else if (rightVec) {
|
| 649 |
+
output = hasBatchAxis
|
| 650 |
+
? matrices.map((m) => m.map((row) => row[0]))
|
| 651 |
+
: matrices[0].map((row) => row[0]);
|
| 652 |
+
} else {
|
| 653 |
+
output = hasBatchAxis ? matrices : matrices[0];
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
const outputShape = shapeOf(output);
|
| 657 |
+
lastRunMeta = {
|
| 658 |
+
kind: "matmul",
|
| 659 |
+
shapeA: sA,
|
| 660 |
+
shapeB: sB,
|
| 661 |
+
outputShape,
|
| 662 |
+
leftBatch,
|
| 663 |
+
rightBatch,
|
| 664 |
+
outBatch,
|
| 665 |
+
usedBroadcasting:
|
| 666 |
+
leftBatch !== rightBatch || sA.length !== sB.length || leftVec || rightVec,
|
| 667 |
+
leftVectorPromoted: leftVec,
|
| 668 |
+
rightVectorPromoted: rightVec,
|
| 669 |
+
};
|
| 670 |
+
|
| 671 |
+
return output;
|
| 672 |
+
}
|
| 673 |
+
|
| 674 |
+
function matmul2D(A, B) {
|
| 675 |
+
const rowsA = A.length;
|
| 676 |
+
const colsA = A[0].length;
|
| 677 |
+
const rowsB = B.length;
|
| 678 |
+
const colsB = B[0].length;
|
| 679 |
+
|
| 680 |
+
if (colsA !== rowsB) {
|
| 681 |
+
throw new Error(
|
| 682 |
+
`matmul shape mismatch: (${rowsA},${colsA}) x (${rowsB},${colsB})`
|
| 683 |
+
);
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
const out = [];
|
| 687 |
+
for (let r = 0; r < rowsA; r += 1) {
|
| 688 |
+
const row = [];
|
| 689 |
+
for (let c = 0; c < colsB; c += 1) {
|
| 690 |
+
let sum = 0;
|
| 691 |
+
for (let k = 0; k < colsA; k += 1) {
|
| 692 |
+
sum += A[r][k] * B[k][c];
|
| 693 |
+
}
|
| 694 |
+
row.push(sum);
|
| 695 |
+
}
|
| 696 |
+
out.push(row);
|
| 697 |
+
}
|
| 698 |
+
return out;
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
function concatAxis0(A, B) {
|
| 702 |
+
const sA = shapeOf(A);
|
| 703 |
+
const sB = shapeOf(B);
|
| 704 |
+
if (sA.length !== sB.length) {
|
| 705 |
+
throw new Error("concatenate requires same rank.");
|
| 706 |
+
}
|
| 707 |
+
for (let i = 1; i < sA.length; i += 1) {
|
| 708 |
+
if (sA[i] !== sB[i]) {
|
| 709 |
+
throw new Error(
|
| 710 |
+
"concatenate axis=0 requires other dimensions to be identical."
|
| 711 |
+
);
|
| 712 |
+
}
|
| 713 |
+
}
|
| 714 |
+
return [...clone(A), ...clone(B)];
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
function stackAxis0(A, B) {
|
| 718 |
+
const sA = JSON.stringify(shapeOf(A));
|
| 719 |
+
const sB = JSON.stringify(shapeOf(B));
|
| 720 |
+
if (sA !== sB) {
|
| 721 |
+
throw new Error("stack requires A and B to have the same shape.");
|
| 722 |
+
}
|
| 723 |
+
return [clone(A), clone(B)];
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
function reduceArray(A, { mode, axis, keepdims }) {
|
| 727 |
+
const rank = shapeOf(A).length;
|
| 728 |
+
|
| 729 |
+
if (axis === null) {
|
| 730 |
+
const flat = flatten(A);
|
| 731 |
+
let scalar;
|
| 732 |
+
if (mode === "sum") {
|
| 733 |
+
scalar = flat.reduce((acc, x) => acc + x, 0);
|
| 734 |
+
} else {
|
| 735 |
+
scalar = Number(
|
| 736 |
+
(flat.reduce((acc, x) => acc + x, 0) / flat.length).toFixed(4)
|
| 737 |
+
);
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
if (!keepdims) return scalar;
|
| 741 |
+
|
| 742 |
+
let wrapped = scalar;
|
| 743 |
+
for (let i = 0; i < rank; i += 1) wrapped = [wrapped];
|
| 744 |
+
return wrapped;
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
const reduced = reduceAlongAxis(A, axis, mode);
|
| 748 |
+
if (!keepdims) return reduced;
|
| 749 |
+
return insertAxisDimension(reduced, axis);
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
function reduceAlongAxis(arr, axis, mode) {
|
| 753 |
+
if (axis === 0) {
|
| 754 |
+
if (arr.length === 0) throw new Error("Cannot reduce empty array.");
|
| 755 |
+
let accum = clone(arr[0]);
|
| 756 |
+
for (let i = 1; i < arr.length; i += 1) {
|
| 757 |
+
accum = elementWiseDeep(accum, arr[i], (a, b) => a + b);
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
if (mode === "sum") return accum;
|
| 761 |
+
return elementWiseDeep(accum, accum, (a) => Number((a / arr.length).toFixed(4)));
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
return arr.map((sub) => reduceAlongAxis(sub, axis - 1, mode));
|
| 765 |
+
}
|
| 766 |
+
|
| 767 |
+
function insertAxisDimension(value, axis) {
|
| 768 |
+
if (axis === 0) return [value];
|
| 769 |
+
if (!Array.isArray(value)) return [value];
|
| 770 |
+
return value.map((v) => insertAxisDimension(v, axis - 1));
|
| 771 |
+
}
|
| 772 |
+
|
| 773 |
+
function clone(value) {
|
| 774 |
+
return JSON.parse(JSON.stringify(value));
|
| 775 |
+
}
|
| 776 |
+
|
| 777 |
+
function renderCode(op, parsed) {
|
| 778 |
+
const lines = ["import numpy as np", ""];
|
| 779 |
+
|
| 780 |
+
if (op.id === "ones" || op.id === "zeros") {
|
| 781 |
+
lines.push(`shape = (${parsed.shapes[0].join(", ")})`);
|
| 782 |
+
} else {
|
| 783 |
+
lines.push(`A = np.random.randint(1, 10, size=(${parsed.shapes[0].join(", ")}))`);
|
| 784 |
+
if (op.inputs === 2) {
|
| 785 |
+
lines.push(
|
| 786 |
+
`B = np.random.randint(1, 10, size=(${parsed.shapes[1].join(", ")}))`
|
| 787 |
+
);
|
| 788 |
+
}
|
| 789 |
+
if (op.requiresTarget) {
|
| 790 |
+
lines.push(`new_shape = (${parsed.targetShape.join(", ")})`);
|
| 791 |
+
}
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
if (op.supportsReductionOptions) {
|
| 795 |
+
const axisPart = parsed.options.axis === null ? "None" : parsed.options.axis;
|
| 796 |
+
lines.push(
|
| 797 |
+
`output = np.${op.id}(A, axis=${axisPart}, keepdims=${parsed.options.keepdims})`
|
| 798 |
+
);
|
| 799 |
+
} else {
|
| 800 |
+
lines.push(op.code);
|
| 801 |
+
}
|
| 802 |
+
|
| 803 |
+
lines.push("print(output)");
|
| 804 |
+
|
| 805 |
+
codeOutput.textContent = lines.join("\n");
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
function renderInputs(arrays, op) {
|
| 809 |
+
inputViz.innerHTML = "";
|
| 810 |
+
|
| 811 |
+
if (op.id === "ones" || op.id === "zeros") {
|
| 812 |
+
inputViz.appendChild(
|
| 813 |
+
buildMatrixCard("Generated Array", arrays[0], "from requested shape")
|
| 814 |
+
);
|
| 815 |
+
return;
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
arrays.forEach((arr, i) => {
|
| 819 |
+
inputViz.appendChild(buildMatrixCard(`Input ${i + 1}`, arr));
|
| 820 |
+
});
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
function renderOutput(output) {
|
| 824 |
+
outputViz.innerHTML = "";
|
| 825 |
+
outputViz.appendChild(buildMatrixCard("Output", output));
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
function renderComputationDetails(op, inputArrays, output, parsed, meta) {
|
| 829 |
+
detailViz.innerHTML = "";
|
| 830 |
+
|
| 831 |
+
if (meta && (meta.kind === "elementwise" || meta.kind === "matmul")) {
|
| 832 |
+
detailViz.appendChild(buildBroadcastUsageCard(meta));
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
if (op.id === "matmul") {
|
| 836 |
+
detailViz.appendChild(
|
| 837 |
+
buildMatmulDetail(inputArrays[0], inputArrays[1], output, meta)
|
| 838 |
+
);
|
| 839 |
+
} else if (op.id === "reshape") {
|
| 840 |
+
detailViz.appendChild(buildReshapeDetail(inputArrays[0], parsed.targetShape));
|
| 841 |
+
} else if (op.supportsReductionOptions) {
|
| 842 |
+
detailViz.appendChild(buildReductionDetail(op, inputArrays[0], parsed.options, output));
|
| 843 |
+
} else {
|
| 844 |
+
const note = document.createElement("p");
|
| 845 |
+
note.className = "slice-label";
|
| 846 |
+
note.textContent = "No extra computation walkthrough for this operation yet.";
|
| 847 |
+
detailViz.appendChild(note);
|
| 848 |
+
}
|
| 849 |
+
}
|
| 850 |
+
|
| 851 |
+
function buildBroadcastUsageCard(meta) {
|
| 852 |
+
const card = document.createElement("article");
|
| 853 |
+
card.className = "matrix-card";
|
| 854 |
+
|
| 855 |
+
const title = document.createElement("p");
|
| 856 |
+
title.className = "matrix-title";
|
| 857 |
+
title.textContent = meta.usedBroadcasting
|
| 858 |
+
? "Broadcasting detected"
|
| 859 |
+
: "No broadcasting needed";
|
| 860 |
+
card.appendChild(title);
|
| 861 |
+
|
| 862 |
+
const line1 = document.createElement("p");
|
| 863 |
+
line1.className = "formula-line";
|
| 864 |
+
line1.textContent = `Input shapes: A${formatShape(meta.shapeA)}, B${formatShape(
|
| 865 |
+
meta.shapeB
|
| 866 |
+
)}`;
|
| 867 |
+
card.appendChild(line1);
|
| 868 |
+
|
| 869 |
+
const line2 = document.createElement("p");
|
| 870 |
+
line2.className = "formula-line";
|
| 871 |
+
line2.textContent = `Output shape: ${formatShape(meta.outputShape)}`;
|
| 872 |
+
card.appendChild(line2);
|
| 873 |
+
|
| 874 |
+
const explain = document.createElement("p");
|
| 875 |
+
explain.className = "slice-label";
|
| 876 |
+
|
| 877 |
+
if (!meta.usedBroadcasting) {
|
| 878 |
+
explain.textContent = "Inputs already align directly; operation runs without dimension expansion.";
|
| 879 |
+
} else if (meta.kind === "elementwise") {
|
| 880 |
+
explain.textContent =
|
| 881 |
+
"Element-wise broadcasting aligns dimensions from the right. Any dimension with size 1 is repeated to match the other input.";
|
| 882 |
+
} else {
|
| 883 |
+
explain.textContent =
|
| 884 |
+
"For np.matmul, only batch dimensions are broadcast; core matrix dimensions still follow (..., m, k) @ (..., k, n).";
|
| 885 |
+
}
|
| 886 |
+
card.appendChild(explain);
|
| 887 |
+
|
| 888 |
+
return card;
|
| 889 |
+
}
|
| 890 |
+
|
| 891 |
+
function buildMatmulDetail(A, B, output, meta) {
|
| 892 |
+
const wrapper = document.createElement("article");
|
| 893 |
+
wrapper.className = "matrix-card";
|
| 894 |
+
|
| 895 |
+
const title = document.createElement("p");
|
| 896 |
+
title.className = "matrix-title";
|
| 897 |
+
title.textContent = "np.matmul interactive computation breakdown";
|
| 898 |
+
wrapper.appendChild(title);
|
| 899 |
+
|
| 900 |
+
const sA = shapeOf(A);
|
| 901 |
+
const sB = shapeOf(B);
|
| 902 |
+
|
| 903 |
+
if (sA.length === 2 && sB.length === 2) {
|
| 904 |
+
wrapper.appendChild(buildMatmulInteractive2D(A, B, output));
|
| 905 |
+
return wrapper;
|
| 906 |
+
}
|
| 907 |
+
|
| 908 |
+
if (sA.length === 3 && sB.length === 3 && sA[0] === sB[0]) {
|
| 909 |
+
const help = document.createElement("p");
|
| 910 |
+
help.className = "slice-label";
|
| 911 |
+
help.textContent =
|
| 912 |
+
"Select a batch and output cell to highlight A row, B column, and formula.";
|
| 913 |
+
wrapper.appendChild(help);
|
| 914 |
+
|
| 915 |
+
const controls = document.createElement("div");
|
| 916 |
+
controls.className = "inline-fields";
|
| 917 |
+
controls.innerHTML = `
|
| 918 |
+
<label class="mini-label" for="batchSelect">batch</label>
|
| 919 |
+
<select id="batchSelect"></select>
|
| 920 |
+
`;
|
| 921 |
+
wrapper.appendChild(controls);
|
| 922 |
+
|
| 923 |
+
const batchSelect = controls.querySelector("#batchSelect");
|
| 924 |
+
for (let i = 0; i < sA[0]; i += 1) {
|
| 925 |
+
const opt = document.createElement("option");
|
| 926 |
+
opt.value = String(i);
|
| 927 |
+
opt.textContent = `batch ${i}`;
|
| 928 |
+
batchSelect.appendChild(opt);
|
| 929 |
+
}
|
| 930 |
+
|
| 931 |
+
const host = document.createElement("div");
|
| 932 |
+
wrapper.appendChild(host);
|
| 933 |
+
|
| 934 |
+
const renderBatch = () => {
|
| 935 |
+
const idx = Number(batchSelect.value);
|
| 936 |
+
host.innerHTML = "";
|
| 937 |
+
const label = document.createElement("p");
|
| 938 |
+
label.className = "slice-label";
|
| 939 |
+
label.textContent = `output[${idx}] = A[${idx}] @ B[${idx}]`;
|
| 940 |
+
host.appendChild(label);
|
| 941 |
+
host.appendChild(buildMatmulInteractive2D(A[idx], B[idx], output[idx]));
|
| 942 |
+
};
|
| 943 |
+
|
| 944 |
+
batchSelect.addEventListener("change", renderBatch);
|
| 945 |
+
renderBatch();
|
| 946 |
+
return wrapper;
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
const mixedNote = document.createElement("p");
|
| 950 |
+
mixedNote.className = "slice-label";
|
| 951 |
+
mixedNote.textContent =
|
| 952 |
+
"Mixed-rank/broadcasted matmul: each output batch uses A_batch @ B_batch after NumPy batch broadcasting.";
|
| 953 |
+
wrapper.appendChild(mixedNote);
|
| 954 |
+
|
| 955 |
+
if (meta && meta.outBatch > 1) {
|
| 956 |
+
const mapLine = document.createElement("p");
|
| 957 |
+
mapLine.className = "formula-line";
|
| 958 |
+
mapLine.textContent = `Batch mapping: output batch i uses A[${meta.leftBatch === 1 ? "0" : "i"}] and B[${
|
| 959 |
+
meta.rightBatch === 1 ? "0" : "i"
|
| 960 |
+
}].`;
|
| 961 |
+
wrapper.appendChild(mapLine);
|
| 962 |
+
}
|
| 963 |
+
|
| 964 |
+
wrapper.appendChild(buildMatrixCard("Input A", A));
|
| 965 |
+
wrapper.appendChild(buildMatrixCard("Input B", B));
|
| 966 |
+
wrapper.appendChild(buildMatrixCard("Output", output));
|
| 967 |
+
|
| 968 |
+
return wrapper;
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
function buildMatmulInteractive2D(A, B, out) {
|
| 972 |
+
const box = document.createElement("div");
|
| 973 |
+
const helper = document.createElement("p");
|
| 974 |
+
helper.className = "slice-label";
|
| 975 |
+
helper.textContent =
|
| 976 |
+
"Click a value in the output matrix. The corresponding row/column will be highlighted.";
|
| 977 |
+
box.appendChild(helper);
|
| 978 |
+
|
| 979 |
+
const state = { row: 0, col: 0 };
|
| 980 |
+
const pickerHost = document.createElement("div");
|
| 981 |
+
const matrixHost = document.createElement("div");
|
| 982 |
+
matrixHost.className = "viz-grid";
|
| 983 |
+
const formulaLine = document.createElement("p");
|
| 984 |
+
formulaLine.className = "formula-line";
|
| 985 |
+
|
| 986 |
+
function renderPicker() {
|
| 987 |
+
pickerHost.innerHTML = "";
|
| 988 |
+
const table = document.createElement("table");
|
| 989 |
+
table.className = "matrix-table";
|
| 990 |
+
|
| 991 |
+
out.forEach((rowValues, r) => {
|
| 992 |
+
const tr = document.createElement("tr");
|
| 993 |
+
rowValues.forEach((value, c) => {
|
| 994 |
+
const td = document.createElement("td");
|
| 995 |
+
const btn = document.createElement("button");
|
| 996 |
+
btn.type = "button";
|
| 997 |
+
btn.className = "matrix-pick-btn";
|
| 998 |
+
if (r === state.row && c === state.col) btn.classList.add("active");
|
| 999 |
+
btn.textContent = String(value);
|
| 1000 |
+
btn.addEventListener("click", () => {
|
| 1001 |
+
state.row = r;
|
| 1002 |
+
state.col = c;
|
| 1003 |
+
renderAll();
|
| 1004 |
+
});
|
| 1005 |
+
td.appendChild(btn);
|
| 1006 |
+
tr.appendChild(td);
|
| 1007 |
+
});
|
| 1008 |
+
table.appendChild(tr);
|
| 1009 |
+
});
|
| 1010 |
+
|
| 1011 |
+
pickerHost.appendChild(table);
|
| 1012 |
+
}
|
| 1013 |
+
|
| 1014 |
+
function build2DCard(title, matrix, subtitle, highlight) {
|
| 1015 |
+
const card = document.createElement("article");
|
| 1016 |
+
card.className = "matrix-card";
|
| 1017 |
+
|
| 1018 |
+
const shape = shapeOf(matrix);
|
| 1019 |
+
const titleEl = document.createElement("p");
|
| 1020 |
+
titleEl.className = "matrix-title";
|
| 1021 |
+
titleEl.innerHTML = `${escapeHtml(title)} <span class="shape-badge">shape: (${shape.join(
|
| 1022 |
+
", "
|
| 1023 |
+
)})</span>`;
|
| 1024 |
+
card.appendChild(titleEl);
|
| 1025 |
+
|
| 1026 |
+
const sub = document.createElement("p");
|
| 1027 |
+
sub.className = "slice-label";
|
| 1028 |
+
sub.textContent = subtitle;
|
| 1029 |
+
card.appendChild(sub);
|
| 1030 |
+
|
| 1031 |
+
card.appendChild(render2DTableWithHighlights(matrix, highlight));
|
| 1032 |
+
return card;
|
| 1033 |
+
}
|
| 1034 |
+
|
| 1035 |
+
function renderAll() {
|
| 1036 |
+
renderPicker();
|
| 1037 |
+
matrixHost.innerHTML = "";
|
| 1038 |
+
matrixHost.appendChild(
|
| 1039 |
+
build2DCard(
|
| 1040 |
+
"Input A",
|
| 1041 |
+
A,
|
| 1042 |
+
`highlighted row: ${state.row}`,
|
| 1043 |
+
{ highlightRow: state.row }
|
| 1044 |
+
)
|
| 1045 |
+
);
|
| 1046 |
+
matrixHost.appendChild(
|
| 1047 |
+
build2DCard(
|
| 1048 |
+
"Input B",
|
| 1049 |
+
B,
|
| 1050 |
+
`highlighted column: ${state.col}`,
|
| 1051 |
+
{ highlightCol: state.col }
|
| 1052 |
+
)
|
| 1053 |
+
);
|
| 1054 |
+
matrixHost.appendChild(
|
| 1055 |
+
build2DCard(
|
| 1056 |
+
"Output",
|
| 1057 |
+
out,
|
| 1058 |
+
`selected cell: [${state.row}, ${state.col}]`,
|
| 1059 |
+
{ highlightCell: { row: state.row, col: state.col } }
|
| 1060 |
+
)
|
| 1061 |
+
);
|
| 1062 |
+
|
| 1063 |
+
const terms = [];
|
| 1064 |
+
const values = [];
|
| 1065 |
+
for (let k = 0; k < A[0].length; k += 1) {
|
| 1066 |
+
terms.push(`${A[state.row][k]}*${B[k][state.col]}`);
|
| 1067 |
+
values.push(A[state.row][k] * B[k][state.col]);
|
| 1068 |
+
}
|
| 1069 |
+
formulaLine.textContent = `output[${state.row}, ${state.col}] = ${terms.join(
|
| 1070 |
+
" + "
|
| 1071 |
+
)} = ${values.join(" + ")} = ${out[state.row][state.col]}`;
|
| 1072 |
+
}
|
| 1073 |
+
|
| 1074 |
+
box.appendChild(pickerHost);
|
| 1075 |
+
box.appendChild(formulaLine);
|
| 1076 |
+
box.appendChild(matrixHost);
|
| 1077 |
+
renderAll();
|
| 1078 |
+
|
| 1079 |
+
return box;
|
| 1080 |
+
}
|
| 1081 |
+
|
| 1082 |
+
function buildReshapeDetail(A, targetShape) {
|
| 1083 |
+
const wrapper = document.createElement("article");
|
| 1084 |
+
wrapper.className = "matrix-card";
|
| 1085 |
+
|
| 1086 |
+
const title = document.createElement("p");
|
| 1087 |
+
title.className = "matrix-title";
|
| 1088 |
+
title.textContent = "np.reshape step-by-step (dynamic)";
|
| 1089 |
+
wrapper.appendChild(title);
|
| 1090 |
+
|
| 1091 |
+
const flat = flatten(A);
|
| 1092 |
+
const total = flat.length;
|
| 1093 |
+
|
| 1094 |
+
const helper = document.createElement("p");
|
| 1095 |
+
helper.className = "slice-label";
|
| 1096 |
+
helper.textContent =
|
| 1097 |
+
"Use Play to animate reshaping; slider also works for manual inspection.";
|
| 1098 |
+
wrapper.appendChild(helper);
|
| 1099 |
+
|
| 1100 |
+
const controls = document.createElement("div");
|
| 1101 |
+
controls.className = "slider-row";
|
| 1102 |
+
controls.innerHTML = `
|
| 1103 |
+
<label for="reshapeStep">step</label>
|
| 1104 |
+
<input class="reshape-step" id="reshapeStep" type="range" min="0" max="${total}" value="0" />
|
| 1105 |
+
<span id="reshapeStepLabel">0 / ${total}</span>
|
| 1106 |
+
<button type="button" id="reshapePlayBtn">Play</button>
|
| 1107 |
+
<button type="button" id="reshapeResetBtn">Reset</button>
|
| 1108 |
+
`;
|
| 1109 |
+
wrapper.appendChild(controls);
|
| 1110 |
+
|
| 1111 |
+
const flatLine = document.createElement("p");
|
| 1112 |
+
flatLine.className = "formula-line";
|
| 1113 |
+
flatLine.textContent = `flat(A) = [${flat.join(", ")}]`;
|
| 1114 |
+
wrapper.appendChild(flatLine);
|
| 1115 |
+
|
| 1116 |
+
const previewHost = document.createElement("div");
|
| 1117 |
+
wrapper.appendChild(previewHost);
|
| 1118 |
+
|
| 1119 |
+
const slider = controls.querySelector("#reshapeStep");
|
| 1120 |
+
const stepLabel = controls.querySelector("#reshapeStepLabel");
|
| 1121 |
+
const playBtn = controls.querySelector("#reshapePlayBtn");
|
| 1122 |
+
const resetBtn = controls.querySelector("#reshapeResetBtn");
|
| 1123 |
+
let timer = null;
|
| 1124 |
+
let playing = false;
|
| 1125 |
+
|
| 1126 |
+
const renderStep = () => {
|
| 1127 |
+
const step = Number(slider.value);
|
| 1128 |
+
stepLabel.textContent = `${step} / ${total}`;
|
| 1129 |
+
const partialFlat = new Array(total).fill("·");
|
| 1130 |
+
for (let i = 0; i < step; i += 1) partialFlat[i] = flat[i];
|
| 1131 |
+
const partial = unflatten(partialFlat, targetShape);
|
| 1132 |
+
|
| 1133 |
+
previewHost.innerHTML = "";
|
| 1134 |
+
previewHost.appendChild(
|
| 1135 |
+
buildMatrixCard(
|
| 1136 |
+
"Current reshaped output",
|
| 1137 |
+
partial,
|
| 1138 |
+
`first ${step} element(s) assigned in row-major order`
|
| 1139 |
+
)
|
| 1140 |
+
);
|
| 1141 |
+
};
|
| 1142 |
+
|
| 1143 |
+
const stopAnimation = () => {
|
| 1144 |
+
if (timer) {
|
| 1145 |
+
clearInterval(timer);
|
| 1146 |
+
timer = null;
|
| 1147 |
+
}
|
| 1148 |
+
playing = false;
|
| 1149 |
+
playBtn.textContent = "Play";
|
| 1150 |
+
};
|
| 1151 |
+
|
| 1152 |
+
const startAnimation = () => {
|
| 1153 |
+
stopAnimation();
|
| 1154 |
+
playing = true;
|
| 1155 |
+
playBtn.textContent = "Pause";
|
| 1156 |
+
timer = setInterval(() => {
|
| 1157 |
+
if (!document.body.contains(wrapper)) {
|
| 1158 |
+
stopAnimation();
|
| 1159 |
+
return;
|
| 1160 |
+
}
|
| 1161 |
+
const current = Number(slider.value);
|
| 1162 |
+
if (current >= total) {
|
| 1163 |
+
stopAnimation();
|
| 1164 |
+
return;
|
| 1165 |
+
}
|
| 1166 |
+
slider.value = String(current + 1);
|
| 1167 |
+
renderStep();
|
| 1168 |
+
}, 500);
|
| 1169 |
+
};
|
| 1170 |
+
|
| 1171 |
+
playBtn.addEventListener("click", () => {
|
| 1172 |
+
if (playing) {
|
| 1173 |
+
stopAnimation();
|
| 1174 |
+
} else {
|
| 1175 |
+
startAnimation();
|
| 1176 |
+
}
|
| 1177 |
+
});
|
| 1178 |
+
|
| 1179 |
+
resetBtn.addEventListener("click", () => {
|
| 1180 |
+
stopAnimation();
|
| 1181 |
+
slider.value = "0";
|
| 1182 |
+
renderStep();
|
| 1183 |
+
});
|
| 1184 |
+
|
| 1185 |
+
slider.addEventListener("input", () => {
|
| 1186 |
+
if (playing) stopAnimation();
|
| 1187 |
+
renderStep();
|
| 1188 |
+
});
|
| 1189 |
+
|
| 1190 |
+
renderStep();
|
| 1191 |
+
startAnimation();
|
| 1192 |
+
|
| 1193 |
+
return wrapper;
|
| 1194 |
+
}
|
| 1195 |
+
|
| 1196 |
+
function buildReductionDetail(op, A, options, output) {
|
| 1197 |
+
const wrapper = document.createElement("article");
|
| 1198 |
+
wrapper.className = "matrix-card";
|
| 1199 |
+
|
| 1200 |
+
const title = document.createElement("p");
|
| 1201 |
+
title.className = "matrix-title";
|
| 1202 |
+
title.textContent = `np.${op.id} options summary`;
|
| 1203 |
+
wrapper.appendChild(title);
|
| 1204 |
+
|
| 1205 |
+
const summary = document.createElement("p");
|
| 1206 |
+
summary.className = "formula-line";
|
| 1207 |
+
summary.textContent = `axis=${options.axis === null ? "None" : options.axis}, keepdims=${options.keepdims}`;
|
| 1208 |
+
wrapper.appendChild(summary);
|
| 1209 |
+
|
| 1210 |
+
const explain = document.createElement("p");
|
| 1211 |
+
explain.className = "slice-label";
|
| 1212 |
+
explain.textContent =
|
| 1213 |
+
options.axis === null
|
| 1214 |
+
? "Reducing over all dimensions."
|
| 1215 |
+
: `Reducing along axis ${options.axis}; values on that axis are aggregated.`;
|
| 1216 |
+
wrapper.appendChild(explain);
|
| 1217 |
+
|
| 1218 |
+
wrapper.appendChild(buildMatrixCard("Input A", A));
|
| 1219 |
+
wrapper.appendChild(buildMatrixCard("Reduced output", output));
|
| 1220 |
+
|
| 1221 |
+
return wrapper;
|
| 1222 |
+
}
|
| 1223 |
+
|
| 1224 |
+
function buildMatrixCard(title, arr, subtitle = "") {
|
| 1225 |
+
const card = document.createElement("article");
|
| 1226 |
+
card.className = "matrix-card";
|
| 1227 |
+
|
| 1228 |
+
const shape = shapeOf(arr);
|
| 1229 |
+
const titleEl = document.createElement("p");
|
| 1230 |
+
titleEl.className = "matrix-title";
|
| 1231 |
+
titleEl.innerHTML = `${escapeHtml(title)} <span class="shape-badge">shape: (${shape.join(
|
| 1232 |
+
", "
|
| 1233 |
+
) || "scalar"})</span>`;
|
| 1234 |
+
|
| 1235 |
+
card.appendChild(titleEl);
|
| 1236 |
+
|
| 1237 |
+
if (subtitle) {
|
| 1238 |
+
const sub = document.createElement("p");
|
| 1239 |
+
sub.className = "slice-label";
|
| 1240 |
+
sub.textContent = subtitle;
|
| 1241 |
+
card.appendChild(sub);
|
| 1242 |
+
}
|
| 1243 |
+
|
| 1244 |
+
if (!Array.isArray(arr)) {
|
| 1245 |
+
const scalar = document.createElement("p");
|
| 1246 |
+
scalar.textContent = String(arr);
|
| 1247 |
+
scalar.style.fontFamily = "Courier New, monospace";
|
| 1248 |
+
scalar.style.fontWeight = "700";
|
| 1249 |
+
card.appendChild(scalar);
|
| 1250 |
+
return card;
|
| 1251 |
+
}
|
| 1252 |
+
|
| 1253 |
+
const rank = shape.length;
|
| 1254 |
+
|
| 1255 |
+
if (rank === 1) {
|
| 1256 |
+
card.appendChild(render2DTable([arr]));
|
| 1257 |
+
} else if (rank === 2) {
|
| 1258 |
+
card.appendChild(render2DTable(arr));
|
| 1259 |
+
} else if (rank === 3) {
|
| 1260 |
+
arr.forEach((slice, idx) => {
|
| 1261 |
+
const lbl = document.createElement("p");
|
| 1262 |
+
lbl.className = "slice-label";
|
| 1263 |
+
lbl.textContent = `slice ${idx} (axis 0)`;
|
| 1264 |
+
card.appendChild(lbl);
|
| 1265 |
+
card.appendChild(render2DTable(slice));
|
| 1266 |
+
});
|
| 1267 |
+
}
|
| 1268 |
+
|
| 1269 |
+
return card;
|
| 1270 |
+
}
|
| 1271 |
+
|
| 1272 |
+
function render2DTable(matrix2d) {
|
| 1273 |
+
const table = document.createElement("table");
|
| 1274 |
+
table.className = "matrix-table";
|
| 1275 |
+
|
| 1276 |
+
matrix2d.forEach((row) => {
|
| 1277 |
+
const tr = document.createElement("tr");
|
| 1278 |
+
row.forEach((value) => {
|
| 1279 |
+
const td = document.createElement("td");
|
| 1280 |
+
td.textContent = String(value);
|
| 1281 |
+
tr.appendChild(td);
|
| 1282 |
+
});
|
| 1283 |
+
table.appendChild(tr);
|
| 1284 |
+
});
|
| 1285 |
+
|
| 1286 |
+
return table;
|
| 1287 |
+
}
|
| 1288 |
+
|
| 1289 |
+
function render2DTableWithHighlights(matrix2d, highlight = {}) {
|
| 1290 |
+
const table = document.createElement("table");
|
| 1291 |
+
table.className = "matrix-table";
|
| 1292 |
+
|
| 1293 |
+
matrix2d.forEach((row, r) => {
|
| 1294 |
+
const tr = document.createElement("tr");
|
| 1295 |
+
row.forEach((value, c) => {
|
| 1296 |
+
const td = document.createElement("td");
|
| 1297 |
+
td.textContent = String(value);
|
| 1298 |
+
|
| 1299 |
+
if (highlight.highlightRow === r) td.classList.add("hl-row");
|
| 1300 |
+
if (highlight.highlightCol === c) td.classList.add("hl-col");
|
| 1301 |
+
if (
|
| 1302 |
+
highlight.highlightCell &&
|
| 1303 |
+
highlight.highlightCell.row === r &&
|
| 1304 |
+
highlight.highlightCell.col === c
|
| 1305 |
+
) {
|
| 1306 |
+
td.classList.add("hl-cell");
|
| 1307 |
+
}
|
| 1308 |
+
|
| 1309 |
+
tr.appendChild(td);
|
| 1310 |
+
});
|
| 1311 |
+
table.appendChild(tr);
|
| 1312 |
+
});
|
| 1313 |
+
|
| 1314 |
+
return table;
|
| 1315 |
+
}
|
| 1316 |
+
|
| 1317 |
+
function escapeHtml(text) {
|
| 1318 |
+
return String(text)
|
| 1319 |
+
.replaceAll("&", "&")
|
| 1320 |
+
.replaceAll("<", "<")
|
| 1321 |
+
.replaceAll(">", ">")
|
| 1322 |
+
.replaceAll('"', """)
|
| 1323 |
+
.replaceAll("'", "'");
|
| 1324 |
+
}
|
| 1325 |
+
|
| 1326 |
+
init();
|
linear-regression/gradient-descent.html
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Gradient Descent Studio</title>
|
| 7 |
+
<link rel="stylesheet" href="styles.css" />
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div class="bg-grid"></div>
|
| 11 |
+
<main class="container">
|
| 12 |
+
<nav class="top-nav">
|
| 13 |
+
<a href="index.html">Back Home</a>
|
| 14 |
+
</nav>
|
| 15 |
+
|
| 16 |
+
<header class="hero compact">
|
| 17 |
+
<p class="eyebrow">Page 2</p>
|
| 18 |
+
<h1>Gradient Descent Studio</h1>
|
| 19 |
+
<p>
|
| 20 |
+
Explore gradient descent for one-feature linear regression. Track
|
| 21 |
+
cost, parameters, and gradients at every step.
|
| 22 |
+
</p>
|
| 23 |
+
</header>
|
| 24 |
+
|
| 25 |
+
<section class="panel controls">
|
| 26 |
+
<div class="row two-col-row">
|
| 27 |
+
<div class="control-group">
|
| 28 |
+
<label for="sampleCount">Samples</label>
|
| 29 |
+
<input id="sampleCount" type="number" min="10" max="120" value="50" />
|
| 30 |
+
</div>
|
| 31 |
+
<div class="control-group">
|
| 32 |
+
<label for="learningRate">Learning Rate (alpha)</label>
|
| 33 |
+
<input id="learningRate" type="number" min="0.001" max="0.5" step="0.001" value="0.02" />
|
| 34 |
+
</div>
|
| 35 |
+
<div class="control-group">
|
| 36 |
+
<label for="iterationCount">Iterations</label>
|
| 37 |
+
<input id="iterationCount" type="number" min="5" max="300" value="120" />
|
| 38 |
+
</div>
|
| 39 |
+
<div class="control-group">
|
| 40 |
+
<label for="noiseLevel">Noise</label>
|
| 41 |
+
<input id="noiseLevel" type="number" min="0" max="5" step="0.1" value="0.35" />
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<div class="row">
|
| 46 |
+
<button id="runGdBtn" type="button">Run Gradient Descent</button>
|
| 47 |
+
</div>
|
| 48 |
+
</section>
|
| 49 |
+
|
| 50 |
+
<section class="panel output">
|
| 51 |
+
<h2>Optimization Playback</h2>
|
| 52 |
+
<div class="slider-row gd-slider-row">
|
| 53 |
+
<label for="gdStep">step</label>
|
| 54 |
+
<input id="gdStep" type="range" min="0" max="0" value="0" />
|
| 55 |
+
<span id="gdStepLabel">0 / 0</span>
|
| 56 |
+
<button id="playGdBtn" type="button">Play</button>
|
| 57 |
+
<button id="resetGdBtn" type="button">Reset</button>
|
| 58 |
+
</div>
|
| 59 |
+
</section>
|
| 60 |
+
|
| 61 |
+
<section class="panel output">
|
| 62 |
+
<h2>Model Visualization</h2>
|
| 63 |
+
<canvas id="fitCanvas" width="980" height="450"></canvas>
|
| 64 |
+
</section>
|
| 65 |
+
|
| 66 |
+
<section class="panel output">
|
| 67 |
+
<h2>Cost Curve</h2>
|
| 68 |
+
<canvas id="costCanvas" width="980" height="260"></canvas>
|
| 69 |
+
</section>
|
| 70 |
+
|
| 71 |
+
<section class="panel output">
|
| 72 |
+
<h2>Cost Landscape (Descent Direction)</h2>
|
| 73 |
+
<div class="row">
|
| 74 |
+
<label for="landscapeMode">Landscape View</label>
|
| 75 |
+
<select id="landscapeMode">
|
| 76 |
+
<option value="2d">2D Contour-Style + Arrows</option>
|
| 77 |
+
<option value="3d">3D Surface + Trajectory</option>
|
| 78 |
+
</select>
|
| 79 |
+
</div>
|
| 80 |
+
<p class="slice-label">
|
| 81 |
+
3D axes: x=theta0, y=theta1, z=cost J(theta). Upward along z means higher cost; the trajectory goes from higher to lower cost.
|
| 82 |
+
</p>
|
| 83 |
+
<canvas id="landscapeCanvas" width="980" height="420"></canvas>
|
| 84 |
+
</section>
|
| 85 |
+
|
| 86 |
+
<section class="panel output">
|
| 87 |
+
<h2>Cost Function and Result</h2>
|
| 88 |
+
<div id="costSummary" class="viz-grid"></div>
|
| 89 |
+
</section>
|
| 90 |
+
|
| 91 |
+
<section class="panel output">
|
| 92 |
+
<h2>Step Details</h2>
|
| 93 |
+
<div id="stepSummary" class="viz-grid"></div>
|
| 94 |
+
</section>
|
| 95 |
+
|
| 96 |
+
<section class="panel output">
|
| 97 |
+
<h2>Iteration Log (Cost / Parameters / Gradients)</h2>
|
| 98 |
+
<div class="log-wrap">
|
| 99 |
+
<table class="gd-log-table" id="gdLogTable">
|
| 100 |
+
<thead>
|
| 101 |
+
<tr>
|
| 102 |
+
<th>step</th>
|
| 103 |
+
<th>cost</th>
|
| 104 |
+
<th>theta</th>
|
| 105 |
+
<th>gradient</th>
|
| 106 |
+
</tr>
|
| 107 |
+
</thead>
|
| 108 |
+
<tbody id="gdLogBody"></tbody>
|
| 109 |
+
</table>
|
| 110 |
+
</div>
|
| 111 |
+
</section>
|
| 112 |
+
</main>
|
| 113 |
+
|
| 114 |
+
<script src="gradient.js?v=20260812g"></script>
|
| 115 |
+
</body>
|
| 116 |
+
</html>
|
linear-regression/gradient.js
ADDED
|
@@ -0,0 +1,829 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const sampleCountInput = document.getElementById("sampleCount");
|
| 2 |
+
const learningRateInput = document.getElementById("learningRate");
|
| 3 |
+
const iterationCountInput = document.getElementById("iterationCount");
|
| 4 |
+
const noiseLevelInput = document.getElementById("noiseLevel");
|
| 5 |
+
const runGdBtn = document.getElementById("runGdBtn");
|
| 6 |
+
const gdStep = document.getElementById("gdStep");
|
| 7 |
+
const gdStepLabel = document.getElementById("gdStepLabel");
|
| 8 |
+
const playGdBtn = document.getElementById("playGdBtn");
|
| 9 |
+
const resetGdBtn = document.getElementById("resetGdBtn");
|
| 10 |
+
const fitCanvas = document.getElementById("fitCanvas");
|
| 11 |
+
const costCanvas = document.getElementById("costCanvas");
|
| 12 |
+
const landscapeMode = document.getElementById("landscapeMode");
|
| 13 |
+
const landscapeCanvas = document.getElementById("landscapeCanvas");
|
| 14 |
+
const costSummary = document.getElementById("costSummary");
|
| 15 |
+
const stepSummary = document.getElementById("stepSummary");
|
| 16 |
+
const gdLogBody = document.getElementById("gdLogBody");
|
| 17 |
+
|
| 18 |
+
const fitCtx = fitCanvas.getContext("2d");
|
| 19 |
+
const costCtx = costCanvas.getContext("2d");
|
| 20 |
+
const landscapeCtx = landscapeCanvas.getContext("2d");
|
| 21 |
+
|
| 22 |
+
const state = {
|
| 23 |
+
data: [],
|
| 24 |
+
steps: [],
|
| 25 |
+
currentStep: 0,
|
| 26 |
+
timer: null,
|
| 27 |
+
landscapeCache: null,
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
function init() {
|
| 31 |
+
resizeCanvases();
|
| 32 |
+
|
| 33 |
+
runGdBtn.addEventListener("click", runGradientDescent);
|
| 34 |
+
landscapeMode.addEventListener("change", renderAll);
|
| 35 |
+
window.addEventListener("resize", handleResize);
|
| 36 |
+
|
| 37 |
+
gdStep.addEventListener("input", () => {
|
| 38 |
+
stopPlayback();
|
| 39 |
+
state.currentStep = Number(gdStep.value);
|
| 40 |
+
renderAll();
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
playGdBtn.addEventListener("click", () => {
|
| 44 |
+
if (!state.steps.length) return;
|
| 45 |
+
if (state.timer) stopPlayback();
|
| 46 |
+
else startPlayback();
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
resetGdBtn.addEventListener("click", () => {
|
| 50 |
+
stopPlayback();
|
| 51 |
+
if (!state.steps.length) return;
|
| 52 |
+
state.currentStep = 0;
|
| 53 |
+
gdStep.value = "0";
|
| 54 |
+
renderAll();
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
runGradientDescent();
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
function handleResize() {
|
| 61 |
+
resizeCanvases();
|
| 62 |
+
if (state.steps.length) renderAll();
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
function resizeCanvases() {
|
| 66 |
+
resizeCanvasToContainer(fitCanvas, 0.46, 240, 480);
|
| 67 |
+
resizeCanvasToContainer(costCanvas, 0.28, 200, 320);
|
| 68 |
+
resizeCanvasToContainer(landscapeCanvas, 0.5, 280, 520);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
function resizeCanvasToContainer(canvas, ratio, minHeight, maxHeight) {
|
| 72 |
+
const parent = canvas.parentElement;
|
| 73 |
+
if (!parent) return;
|
| 74 |
+
|
| 75 |
+
const width = Math.max(320, Math.floor(parent.clientWidth - 2));
|
| 76 |
+
const targetHeight = Math.floor(width * ratio);
|
| 77 |
+
const height = Math.max(minHeight, Math.min(maxHeight, targetHeight));
|
| 78 |
+
|
| 79 |
+
if (canvas.width !== width || canvas.height !== height) {
|
| 80 |
+
canvas.width = width;
|
| 81 |
+
canvas.height = height;
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
function runGradientDescent() {
|
| 86 |
+
stopPlayback();
|
| 87 |
+
|
| 88 |
+
const config = readConfig();
|
| 89 |
+
state.data = generateData(config);
|
| 90 |
+
state.steps = computeGradientDescent(state.data, config).steps;
|
| 91 |
+
state.currentStep = 0;
|
| 92 |
+
state.landscapeCache = null;
|
| 93 |
+
|
| 94 |
+
gdStep.max = String(state.steps.length - 1);
|
| 95 |
+
gdStep.value = "0";
|
| 96 |
+
|
| 97 |
+
renderAll();
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
function readConfig() {
|
| 101 |
+
const sampleCount = clampInt(Number(sampleCountInput.value), 10, 120, 50);
|
| 102 |
+
const learningRate = clampNum(Number(learningRateInput.value), 0.001, 0.5, 0.02);
|
| 103 |
+
const iterations = clampInt(Number(iterationCountInput.value), 5, 300, 120);
|
| 104 |
+
const noise = clampNum(Number(noiseLevelInput.value), 0, 5, 0.35);
|
| 105 |
+
|
| 106 |
+
sampleCountInput.value = String(sampleCount);
|
| 107 |
+
learningRateInput.value = String(learningRate);
|
| 108 |
+
iterationCountInput.value = String(iterations);
|
| 109 |
+
noiseLevelInput.value = String(noise);
|
| 110 |
+
|
| 111 |
+
return { sampleCount, learningRate, iterations, noise };
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
function generateData(config) {
|
| 115 |
+
const rows = [];
|
| 116 |
+
for (let i = 0; i < config.sampleCount; i += 1) {
|
| 117 |
+
const x1 = rand(-5, 5);
|
| 118 |
+
const y = 8 + 4.2 * x1 + rand(-config.noise, config.noise);
|
| 119 |
+
rows.push({ x: [x1], y });
|
| 120 |
+
}
|
| 121 |
+
return rows;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
function computeGradientDescent(data, config) {
|
| 125 |
+
let theta = [-12, 12];
|
| 126 |
+
const steps = [];
|
| 127 |
+
|
| 128 |
+
for (let step = 0; step <= config.iterations; step += 1) {
|
| 129 |
+
const { cost, grad } = costAndGradient(data, theta);
|
| 130 |
+
|
| 131 |
+
steps.push({ step, theta: theta.slice(), grad: grad.slice(), cost });
|
| 132 |
+
|
| 133 |
+
if (step === config.iterations) break;
|
| 134 |
+
theta = theta.map((t, j) => t - config.learningRate * grad[j]);
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
return { steps };
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
function costAndGradient(data, theta) {
|
| 141 |
+
const m = data.length;
|
| 142 |
+
let sumSq = 0;
|
| 143 |
+
const grad = [0, 0];
|
| 144 |
+
|
| 145 |
+
for (const row of data) {
|
| 146 |
+
const yHat = theta[0] + theta[1] * row.x[0];
|
| 147 |
+
const err = yHat - row.y;
|
| 148 |
+
sumSq += err * err;
|
| 149 |
+
grad[0] += err;
|
| 150 |
+
grad[1] += err * row.x[0];
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
grad[0] /= m;
|
| 154 |
+
grad[1] /= m;
|
| 155 |
+
|
| 156 |
+
return { cost: sumSq / (2 * m), grad };
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
function renderAll() {
|
| 160 |
+
if (!state.steps.length) return;
|
| 161 |
+
|
| 162 |
+
gdStepLabel.textContent = `${state.currentStep} / ${state.steps.length - 1}`;
|
| 163 |
+
renderFitPlot();
|
| 164 |
+
renderCostPlot();
|
| 165 |
+
renderCostLandscape();
|
| 166 |
+
renderCostSummary();
|
| 167 |
+
renderStepSummary();
|
| 168 |
+
renderLogTable();
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
function renderFitPlot() {
|
| 172 |
+
const ctx = fitCtx;
|
| 173 |
+
const { width, height } = fitCanvas;
|
| 174 |
+
const pad = 52;
|
| 175 |
+
const s = state.steps[state.currentStep];
|
| 176 |
+
|
| 177 |
+
ctx.clearRect(0, 0, width, height);
|
| 178 |
+
|
| 179 |
+
const xs = state.data.map((d) => d.x[0]);
|
| 180 |
+
const ys = state.data.map((d) => d.y);
|
| 181 |
+
|
| 182 |
+
const xMin = Math.min(...xs) - 1;
|
| 183 |
+
const xMax = Math.max(...xs) + 1;
|
| 184 |
+
const yMin = Math.min(...ys) - 2;
|
| 185 |
+
const yMax = Math.max(...ys) + 2;
|
| 186 |
+
|
| 187 |
+
const toX = (x) => pad + ((x - xMin) / (xMax - xMin)) * (width - 2 * pad);
|
| 188 |
+
const toY = (y) => height - pad - ((y - yMin) / (yMax - yMin)) * (height - 2 * pad);
|
| 189 |
+
|
| 190 |
+
drawAxes2D(ctx, width, height, pad, "x", "y");
|
| 191 |
+
|
| 192 |
+
ctx.fillStyle = "#0c7b73";
|
| 193 |
+
for (const row of state.data) {
|
| 194 |
+
ctx.beginPath();
|
| 195 |
+
ctx.arc(toX(row.x[0]), toY(row.y), 4, 0, Math.PI * 2);
|
| 196 |
+
ctx.fill();
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
const yL = s.theta[0] + s.theta[1] * xMin;
|
| 200 |
+
const yR = s.theta[0] + s.theta[1] * xMax;
|
| 201 |
+
ctx.strokeStyle = "#dd5e2f";
|
| 202 |
+
ctx.lineWidth = 3;
|
| 203 |
+
ctx.beginPath();
|
| 204 |
+
ctx.moveTo(toX(xMin), toY(yL));
|
| 205 |
+
ctx.lineTo(toX(xMax), toY(yR));
|
| 206 |
+
ctx.stroke();
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
function renderCostPlot() {
|
| 210 |
+
const ctx = costCtx;
|
| 211 |
+
const { width, height } = costCanvas;
|
| 212 |
+
const pad = 40;
|
| 213 |
+
const costs = state.steps.map((s) => s.cost);
|
| 214 |
+
|
| 215 |
+
ctx.clearRect(0, 0, width, height);
|
| 216 |
+
|
| 217 |
+
const minC = Math.min(...costs);
|
| 218 |
+
const maxC = Math.max(...costs);
|
| 219 |
+
|
| 220 |
+
const toX = (i) => pad + (i / (costs.length - 1 || 1)) * (width - 2 * pad);
|
| 221 |
+
const toY = (c) =>
|
| 222 |
+
height - pad - ((c - minC) / (maxC - minC || 1)) * (height - 2 * pad);
|
| 223 |
+
|
| 224 |
+
drawAxes2D(ctx, width, height, pad, "iteration", "cost");
|
| 225 |
+
|
| 226 |
+
ctx.strokeStyle = "#0c7b73";
|
| 227 |
+
ctx.lineWidth = 2.2;
|
| 228 |
+
ctx.beginPath();
|
| 229 |
+
costs.forEach((cost, i) => {
|
| 230 |
+
const x = toX(i);
|
| 231 |
+
const y = toY(cost);
|
| 232 |
+
if (i === 0) ctx.moveTo(x, y);
|
| 233 |
+
else ctx.lineTo(x, y);
|
| 234 |
+
});
|
| 235 |
+
ctx.stroke();
|
| 236 |
+
|
| 237 |
+
ctx.fillStyle = "#dd5e2f";
|
| 238 |
+
ctx.beginPath();
|
| 239 |
+
ctx.arc(toX(state.currentStep), toY(costs[state.currentStep]), 5, 0, Math.PI * 2);
|
| 240 |
+
ctx.fill();
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
function renderCostLandscape() {
|
| 244 |
+
const data = getLandscapeData();
|
| 245 |
+
if (landscapeMode.value === "3d") renderLandscape3D(data);
|
| 246 |
+
else renderLandscape2D(data);
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
function getLandscapeData() {
|
| 250 |
+
if (state.landscapeCache) return state.landscapeCache;
|
| 251 |
+
|
| 252 |
+
const pathTheta0 = state.steps.map((s) => s.theta[0]);
|
| 253 |
+
const pathTheta1 = state.steps.map((s) => s.theta[1]);
|
| 254 |
+
|
| 255 |
+
const t0MinRaw = Math.min(...pathTheta0);
|
| 256 |
+
const t0MaxRaw = Math.max(...pathTheta0);
|
| 257 |
+
const t1MinRaw = Math.min(...pathTheta1);
|
| 258 |
+
const t1MaxRaw = Math.max(...pathTheta1);
|
| 259 |
+
|
| 260 |
+
const t0Pad = (t0MaxRaw - t0MinRaw || 1) * 0.35;
|
| 261 |
+
const t1Pad = (t1MaxRaw - t1MinRaw || 1) * 0.35;
|
| 262 |
+
|
| 263 |
+
const t0Min = t0MinRaw - t0Pad;
|
| 264 |
+
const t0Max = t0MaxRaw + t0Pad;
|
| 265 |
+
const t1Min = t1MinRaw - t1Pad;
|
| 266 |
+
const t1Max = t1MaxRaw + t1Pad;
|
| 267 |
+
|
| 268 |
+
const gridN = 44;
|
| 269 |
+
const grid = [];
|
| 270 |
+
let zMin = Infinity;
|
| 271 |
+
let zMax = -Infinity;
|
| 272 |
+
|
| 273 |
+
for (let gy = 0; gy < gridN; gy += 1) {
|
| 274 |
+
const t1 = t1Min + (gy / (gridN - 1)) * (t1Max - t1Min);
|
| 275 |
+
const row = [];
|
| 276 |
+
for (let gx = 0; gx < gridN; gx += 1) {
|
| 277 |
+
const t0 = t0Min + (gx / (gridN - 1)) * (t0Max - t0Min);
|
| 278 |
+
const cost = costAndGradient(state.data, [t0, t1]).cost;
|
| 279 |
+
row.push(cost);
|
| 280 |
+
zMin = Math.min(zMin, cost);
|
| 281 |
+
zMax = Math.max(zMax, cost);
|
| 282 |
+
}
|
| 283 |
+
grid.push(row);
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
const path = state.steps.map((s) => ({
|
| 287 |
+
t0: s.theta[0],
|
| 288 |
+
t1: s.theta[1],
|
| 289 |
+
z: s.cost,
|
| 290 |
+
g0: s.grad[0],
|
| 291 |
+
g1: s.grad[1],
|
| 292 |
+
}));
|
| 293 |
+
|
| 294 |
+
state.landscapeCache = {
|
| 295 |
+
grid,
|
| 296 |
+
gridN,
|
| 297 |
+
t0Min,
|
| 298 |
+
t0Max,
|
| 299 |
+
t1Min,
|
| 300 |
+
t1Max,
|
| 301 |
+
zMin,
|
| 302 |
+
zMax,
|
| 303 |
+
path,
|
| 304 |
+
};
|
| 305 |
+
|
| 306 |
+
return state.landscapeCache;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
function renderLandscape2D(data) {
|
| 310 |
+
const ctx = landscapeCtx;
|
| 311 |
+
const { width, height } = landscapeCanvas;
|
| 312 |
+
const pad = 54;
|
| 313 |
+
|
| 314 |
+
ctx.clearRect(0, 0, width, height);
|
| 315 |
+
|
| 316 |
+
const toX = (t0) => pad + ((t0 - data.t0Min) / (data.t0Max - data.t0Min || 1)) * (width - 2 * pad);
|
| 317 |
+
const toY = (t1) => height - pad - ((t1 - data.t1Min) / (data.t1Max - data.t1Min || 1)) * (height - 2 * pad);
|
| 318 |
+
|
| 319 |
+
const cellW = (width - 2 * pad) / (data.gridN - 1);
|
| 320 |
+
const cellH = (height - 2 * pad) / (data.gridN - 1);
|
| 321 |
+
|
| 322 |
+
for (let gy = 0; gy < data.gridN - 1; gy += 1) {
|
| 323 |
+
for (let gx = 0; gx < data.gridN - 1; gx += 1) {
|
| 324 |
+
const z = data.grid[gy][gx];
|
| 325 |
+
const ratio = (z - data.zMin) / (data.zMax - data.zMin || 1);
|
| 326 |
+
ctx.fillStyle = heatColor(ratio);
|
| 327 |
+
ctx.fillRect(pad + gx * cellW, pad + gy * cellH, cellW + 1, cellH + 1);
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
drawAxes2D(ctx, width, height, pad, "theta0", "theta1");
|
| 332 |
+
|
| 333 |
+
ctx.strokeStyle = "#f4f1ea";
|
| 334 |
+
ctx.lineWidth = 2.4;
|
| 335 |
+
ctx.beginPath();
|
| 336 |
+
data.path.forEach((p, idx) => {
|
| 337 |
+
const x = toX(p.t0);
|
| 338 |
+
const y = toY(p.t1);
|
| 339 |
+
if (idx === 0) ctx.moveTo(x, y);
|
| 340 |
+
else ctx.lineTo(x, y);
|
| 341 |
+
});
|
| 342 |
+
ctx.stroke();
|
| 343 |
+
|
| 344 |
+
for (let idx = 0; idx < data.path.length - 1; idx += Math.max(1, Math.floor(data.path.length / 14))) {
|
| 345 |
+
const p1 = data.path[idx];
|
| 346 |
+
const p2 = data.path[Math.min(idx + 1, data.path.length - 1)];
|
| 347 |
+
drawArrow(ctx, toX(p1.t0), toY(p1.t1), toX(p2.t0), toY(p2.t1), "#14212b", 8);
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
const cur = data.path[state.currentStep];
|
| 351 |
+
drawArrow(
|
| 352 |
+
ctx,
|
| 353 |
+
toX(cur.t0),
|
| 354 |
+
toY(cur.t1),
|
| 355 |
+
toX(cur.t0 - cur.g0 * 0.15),
|
| 356 |
+
toY(cur.t1 - cur.g1 * 0.15),
|
| 357 |
+
"#dd5e2f",
|
| 358 |
+
11
|
| 359 |
+
);
|
| 360 |
+
|
| 361 |
+
ctx.fillStyle = "#dd5e2f";
|
| 362 |
+
ctx.beginPath();
|
| 363 |
+
ctx.arc(toX(cur.t0), toY(cur.t1), 5.5, 0, Math.PI * 2);
|
| 364 |
+
ctx.fill();
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
function renderLandscape3D(data) {
|
| 368 |
+
const ctx = landscapeCtx;
|
| 369 |
+
const { width, height } = landscapeCanvas;
|
| 370 |
+
|
| 371 |
+
ctx.clearRect(0, 0, width, height);
|
| 372 |
+
|
| 373 |
+
const t0Mid = (data.t0Min + data.t0Max) / 2;
|
| 374 |
+
const t1Mid = (data.t1Min + data.t1Max) / 2;
|
| 375 |
+
const t0Range = data.t0Max - data.t0Min || 1;
|
| 376 |
+
const t1Range = data.t1Max - data.t1Min || 1;
|
| 377 |
+
const zRange = data.zMax - data.zMin || 1;
|
| 378 |
+
const zScale = Math.max(t0Range, t1Range) * 0.95;
|
| 379 |
+
|
| 380 |
+
const baseCamera = {
|
| 381 |
+
yaw: -1.08,
|
| 382 |
+
pitch: 0.66,
|
| 383 |
+
dist: 21,
|
| 384 |
+
lockCostUp: true,
|
| 385 |
+
groundTilt: 0.62,
|
| 386 |
+
costTilt: 1.05,
|
| 387 |
+
scale: 1,
|
| 388 |
+
cx: 0,
|
| 389 |
+
cy: 0,
|
| 390 |
+
};
|
| 391 |
+
|
| 392 |
+
const camera = fitCameraToScene(data, width, height, zScale, t0Mid, t1Mid, baseCamera);
|
| 393 |
+
|
| 394 |
+
const project = (t0, t1, z) => {
|
| 395 |
+
const x1 = t0 - t0Mid;
|
| 396 |
+
const x2 = t1 - t1Mid;
|
| 397 |
+
const y = ((z - data.zMin) / zRange) * zScale;
|
| 398 |
+
return project3D(x1, x2, y, camera);
|
| 399 |
+
};
|
| 400 |
+
|
| 401 |
+
ctx.strokeStyle = "rgba(30, 44, 53, 0.26)";
|
| 402 |
+
ctx.lineWidth = 1;
|
| 403 |
+
|
| 404 |
+
for (let gy = 0; gy < data.gridN; gy += 1) {
|
| 405 |
+
ctx.beginPath();
|
| 406 |
+
let first = true;
|
| 407 |
+
for (let gx = 0; gx < data.gridN; gx += 1) {
|
| 408 |
+
const t0 = data.t0Min + (gx / (data.gridN - 1)) * (data.t0Max - data.t0Min);
|
| 409 |
+
const t1 = data.t1Min + (gy / (data.gridN - 1)) * (data.t1Max - data.t1Min);
|
| 410 |
+
const p = project(t0, t1, data.grid[gy][gx]);
|
| 411 |
+
if (first) {
|
| 412 |
+
ctx.moveTo(p.x, p.y);
|
| 413 |
+
first = false;
|
| 414 |
+
} else {
|
| 415 |
+
ctx.lineTo(p.x, p.y);
|
| 416 |
+
}
|
| 417 |
+
}
|
| 418 |
+
ctx.stroke();
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
for (let gx = 0; gx < data.gridN; gx += 1) {
|
| 422 |
+
ctx.beginPath();
|
| 423 |
+
let first = true;
|
| 424 |
+
for (let gy = 0; gy < data.gridN; gy += 1) {
|
| 425 |
+
const t0 = data.t0Min + (gx / (data.gridN - 1)) * (data.t0Max - data.t0Min);
|
| 426 |
+
const t1 = data.t1Min + (gy / (data.gridN - 1)) * (data.t1Max - data.t1Min);
|
| 427 |
+
const p = project(t0, t1, data.grid[gy][gx]);
|
| 428 |
+
if (first) {
|
| 429 |
+
ctx.moveTo(p.x, p.y);
|
| 430 |
+
first = false;
|
| 431 |
+
} else {
|
| 432 |
+
ctx.lineTo(p.x, p.y);
|
| 433 |
+
}
|
| 434 |
+
}
|
| 435 |
+
ctx.stroke();
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
drawSurfaceAxes3D(ctx, data, project);
|
| 439 |
+
drawAxisLegend3D(ctx, width, height);
|
| 440 |
+
|
| 441 |
+
for (let idx = 0; idx < data.path.length - 1; idx += 1) {
|
| 442 |
+
const p1m = data.path[idx];
|
| 443 |
+
const p2m = data.path[idx + 1];
|
| 444 |
+
const p1 = project(p1m.t0, p1m.t1, p1m.z);
|
| 445 |
+
const p2 = project(p2m.t0, p2m.t1, p2m.z);
|
| 446 |
+
const t = idx / Math.max(1, data.path.length - 2);
|
| 447 |
+
ctx.strokeStyle = trajectoryColor(t);
|
| 448 |
+
ctx.lineWidth = 2.8;
|
| 449 |
+
ctx.beginPath();
|
| 450 |
+
ctx.moveTo(p1.x, p1.y);
|
| 451 |
+
ctx.lineTo(p2.x, p2.y);
|
| 452 |
+
ctx.stroke();
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
for (let idx = 0; idx < data.path.length - 1; idx += Math.max(1, Math.floor(data.path.length / 10))) {
|
| 456 |
+
const p1m = data.path[idx];
|
| 457 |
+
const p2m = data.path[Math.min(idx + 1, data.path.length - 1)];
|
| 458 |
+
const p1 = project(p1m.t0, p1m.t1, p1m.z);
|
| 459 |
+
const p2 = project(p2m.t0, p2m.t1, p2m.z);
|
| 460 |
+
const t = idx / Math.max(1, data.path.length - 2);
|
| 461 |
+
drawArrow(ctx, p1.x, p1.y, p2.x, p2.y, trajectoryColor(t), 9);
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
const cur = data.path[state.currentStep];
|
| 465 |
+
const curP = project(cur.t0, cur.t1, cur.z);
|
| 466 |
+
ctx.fillStyle = "#dd5e2f";
|
| 467 |
+
ctx.beginPath();
|
| 468 |
+
ctx.arc(curP.x, curP.y, 5.5, 0, Math.PI * 2);
|
| 469 |
+
ctx.fill();
|
| 470 |
+
|
| 471 |
+
const start = data.path[0];
|
| 472 |
+
const end = data.path[data.path.length - 1];
|
| 473 |
+
const startP = project(start.t0, start.t1, start.z);
|
| 474 |
+
const endP = project(end.t0, end.t1, end.z);
|
| 475 |
+
|
| 476 |
+
ctx.fillStyle = "#1e4d4b";
|
| 477 |
+
ctx.beginPath();
|
| 478 |
+
ctx.arc(startP.x, startP.y, 4.2, 0, Math.PI * 2);
|
| 479 |
+
ctx.fill();
|
| 480 |
+
draw3DLabel(ctx, startP.x + 8, startP.y - 8, `start J=${start.z.toFixed(2)}`);
|
| 481 |
+
|
| 482 |
+
ctx.fillStyle = "#dd5e2f";
|
| 483 |
+
ctx.beginPath();
|
| 484 |
+
ctx.arc(endP.x, endP.y, 4.2, 0, Math.PI * 2);
|
| 485 |
+
ctx.fill();
|
| 486 |
+
draw3DLabel(ctx, endP.x + 8, endP.y + 14, `end J=${end.z.toFixed(2)}`);
|
| 487 |
+
|
| 488 |
+
const dropPct = ((start.z - end.z) / Math.max(1e-9, start.z)) * 100;
|
| 489 |
+
draw3DLabel(
|
| 490 |
+
ctx,
|
| 491 |
+
Math.min(startP.x, endP.x) + 14,
|
| 492 |
+
Math.min(startP.y, endP.y) - 18,
|
| 493 |
+
`cost drop: ${dropPct.toFixed(1)}%`
|
| 494 |
+
);
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
function fitCameraToScene(data, width, height, zScale, t0Mid, t1Mid, baseCamera) {
|
| 498 |
+
const points = [];
|
| 499 |
+
const gridStep = Math.max(1, Math.floor(data.gridN / 18));
|
| 500 |
+
|
| 501 |
+
for (let gy = 0; gy < data.gridN; gy += gridStep) {
|
| 502 |
+
for (let gx = 0; gx < data.gridN; gx += gridStep) {
|
| 503 |
+
const t0 = data.t0Min + (gx / (data.gridN - 1)) * (data.t0Max - data.t0Min);
|
| 504 |
+
const t1 = data.t1Min + (gy / (data.gridN - 1)) * (data.t1Max - data.t1Min);
|
| 505 |
+
const z = data.grid[gy][gx];
|
| 506 |
+
points.push(toCameraPoint(t0, t1, z, data, zScale, t0Mid, t1Mid, baseCamera));
|
| 507 |
+
}
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
for (const p of data.path) {
|
| 511 |
+
points.push(toCameraPoint(p.t0, p.t1, p.z, data, zScale, t0Mid, t1Mid, baseCamera));
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
const axisPoints = [
|
| 515 |
+
[data.t0Min, data.t1Min, data.zMin], // axis origin
|
| 516 |
+
[data.t0Max, data.t1Min, data.zMin], // theta0 end
|
| 517 |
+
[data.t0Min, data.t1Max, data.zMin], // theta1 end
|
| 518 |
+
[data.t0Min, data.t1Min, data.zMax], // cost end
|
| 519 |
+
];
|
| 520 |
+
for (const [t0, t1, z] of axisPoints) {
|
| 521 |
+
points.push(toCameraPoint(t0, t1, z, data, zScale, t0Mid, t1Mid, baseCamera));
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
let minX = Infinity;
|
| 525 |
+
let maxX = -Infinity;
|
| 526 |
+
let minY = Infinity;
|
| 527 |
+
let maxY = -Infinity;
|
| 528 |
+
|
| 529 |
+
for (const p of points) {
|
| 530 |
+
if (!Number.isFinite(p.x) || !Number.isFinite(p.y)) continue;
|
| 531 |
+
minX = Math.min(minX, p.x);
|
| 532 |
+
maxX = Math.max(maxX, p.x);
|
| 533 |
+
minY = Math.min(minY, p.y);
|
| 534 |
+
maxY = Math.max(maxY, p.y);
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
if (!Number.isFinite(minX) || !Number.isFinite(maxX) || !Number.isFinite(minY) || !Number.isFinite(maxY)) {
|
| 538 |
+
return {
|
| 539 |
+
...baseCamera,
|
| 540 |
+
scale: 24,
|
| 541 |
+
cx: width * 0.5,
|
| 542 |
+
cy: height * 0.58,
|
| 543 |
+
};
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
const sceneW = Math.max(1e-6, maxX - minX);
|
| 547 |
+
const sceneH = Math.max(1e-6, maxY - minY);
|
| 548 |
+
const padX = Math.max(74, Math.floor(width * 0.2));
|
| 549 |
+
const padY = Math.max(72, Math.floor(height * 0.22));
|
| 550 |
+
const availW = Math.max(120, width - padX * 2);
|
| 551 |
+
const availH = Math.max(120, height - padY * 2);
|
| 552 |
+
const rawScale = Math.min(availW / sceneW, availH / sceneH);
|
| 553 |
+
const scale = Math.max(1.5, Math.min(90, rawScale * 0.62));
|
| 554 |
+
const midX = (minX + maxX) / 2;
|
| 555 |
+
const midY = (minY + maxY) / 2;
|
| 556 |
+
|
| 557 |
+
return {
|
| 558 |
+
...baseCamera,
|
| 559 |
+
scale,
|
| 560 |
+
cx: width / 2 - midX * scale,
|
| 561 |
+
cy: height / 2 - midY * scale,
|
| 562 |
+
};
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
function toCameraPoint(t0, t1, z, data, zScale, t0Mid, t1Mid, camera) {
|
| 566 |
+
const zRange = data.zMax - data.zMin || 1;
|
| 567 |
+
const x1 = t0 - t0Mid;
|
| 568 |
+
const x2 = t1 - t1Mid;
|
| 569 |
+
const y = ((z - data.zMin) / zRange) * zScale;
|
| 570 |
+
return project3D(x1, x2, y, camera);
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
function renderCostSummary() {
|
| 574 |
+
const first = state.steps[0];
|
| 575 |
+
const last = state.steps[state.steps.length - 1];
|
| 576 |
+
|
| 577 |
+
costSummary.innerHTML = `
|
| 578 |
+
<article class="matrix-card">
|
| 579 |
+
<p class="matrix-title">Cost Function</p>
|
| 580 |
+
<p class="formula-line">J(θ) = (1/2m) Σ(θ0 + θ1*x1 - y)^2</p>
|
| 581 |
+
</article>
|
| 582 |
+
<article class="matrix-card">
|
| 583 |
+
<p class="matrix-title">Result</p>
|
| 584 |
+
<p class="formula-line">y_hat = ${fmt(last.theta[0])} + ${fmt(last.theta[1])}*x1</p>
|
| 585 |
+
<p class="slice-label">parameters: 2 (theta0, theta1)</p>
|
| 586 |
+
<p class="slice-label">initial cost: ${first.cost.toFixed(6)}</p>
|
| 587 |
+
<p class="slice-label">final cost: ${last.cost.toFixed(6)}</p>
|
| 588 |
+
</article>
|
| 589 |
+
`;
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
function renderStepSummary() {
|
| 593 |
+
const s = state.steps[state.currentStep];
|
| 594 |
+
stepSummary.innerHTML = `
|
| 595 |
+
<article class="matrix-card">
|
| 596 |
+
<p class="matrix-title">Current Step ${s.step}</p>
|
| 597 |
+
<p class="formula-line">cost = ${s.cost.toFixed(6)}</p>
|
| 598 |
+
<p class="formula-line">theta = [${s.theta.map(fmt).join(", ")}]</p>
|
| 599 |
+
<p class="formula-line">gradient = [${s.grad.map(fmt).join(", ")}]</p>
|
| 600 |
+
</article>
|
| 601 |
+
`;
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
function renderLogTable() {
|
| 605 |
+
gdLogBody.innerHTML = "";
|
| 606 |
+
|
| 607 |
+
for (const s of state.steps) {
|
| 608 |
+
const tr = document.createElement("tr");
|
| 609 |
+
if (s.step === state.currentStep) tr.classList.add("active-log-row");
|
| 610 |
+
|
| 611 |
+
const cells = [
|
| 612 |
+
String(s.step),
|
| 613 |
+
s.cost.toFixed(6),
|
| 614 |
+
`[${s.theta.map(fmt).join(", ")}]`,
|
| 615 |
+
`[${s.grad.map(fmt).join(", ")}]`,
|
| 616 |
+
];
|
| 617 |
+
|
| 618 |
+
cells.forEach((txt) => {
|
| 619 |
+
const td = document.createElement("td");
|
| 620 |
+
td.textContent = txt;
|
| 621 |
+
tr.appendChild(td);
|
| 622 |
+
});
|
| 623 |
+
|
| 624 |
+
gdLogBody.appendChild(tr);
|
| 625 |
+
}
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
function drawAxes2D(ctx, width, height, pad, xLabel, yLabel) {
|
| 629 |
+
ctx.strokeStyle = "#8f9ba1";
|
| 630 |
+
ctx.lineWidth = 1.2;
|
| 631 |
+
|
| 632 |
+
ctx.beginPath();
|
| 633 |
+
ctx.moveTo(pad, height - pad);
|
| 634 |
+
ctx.lineTo(width - pad, height - pad);
|
| 635 |
+
ctx.stroke();
|
| 636 |
+
|
| 637 |
+
ctx.beginPath();
|
| 638 |
+
ctx.moveTo(pad, height - pad);
|
| 639 |
+
ctx.lineTo(pad, pad);
|
| 640 |
+
ctx.stroke();
|
| 641 |
+
|
| 642 |
+
ctx.fillStyle = "#4c5961";
|
| 643 |
+
ctx.font = "13px 'Courier New', monospace";
|
| 644 |
+
ctx.fillText(xLabel, width - pad - 50, height - pad + 22);
|
| 645 |
+
ctx.fillText(yLabel, pad - 30, pad - 10);
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
function drawSurfaceAxes3D(ctx, data, project) {
|
| 649 |
+
const origin = project(data.t0Min, data.t1Min, data.zMin);
|
| 650 |
+
const t0End = project(data.t0Max, data.t1Min, data.zMin);
|
| 651 |
+
const t1End = project(data.t0Min, data.t1Max, data.zMin);
|
| 652 |
+
const zEnd = project(data.t0Min, data.t1Min, data.zMax);
|
| 653 |
+
|
| 654 |
+
drawArrow(ctx, origin.x, origin.y, t0End.x, t0End.y, "#30444f", 9);
|
| 655 |
+
drawArrow(ctx, origin.x, origin.y, t1End.x, t1End.y, "#30444f", 9);
|
| 656 |
+
drawArrow(ctx, origin.x, origin.y, zEnd.x, zEnd.y, "#b23f2c", 10);
|
| 657 |
+
|
| 658 |
+
draw3DLabel(ctx, t0End.x + 7, t0End.y + 14, "theta0 (+)");
|
| 659 |
+
draw3DLabel(ctx, t1End.x + 7, t1End.y + 14, "theta1 (+)");
|
| 660 |
+
draw3DLabel(ctx, zEnd.x + 8, zEnd.y - 10, "cost J(theta) increases");
|
| 661 |
+
draw3DLabel(ctx, origin.x + 8, origin.y + 16, "cost decreases toward valley");
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
function draw3DLabel(ctx, x, y, text) {
|
| 665 |
+
ctx.fillStyle = "#23333d";
|
| 666 |
+
ctx.font = "12px 'Courier New', monospace";
|
| 667 |
+
ctx.fillText(text, x, y);
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
function drawAxisLegend3D(ctx, width, height) {
|
| 671 |
+
const boxW = Math.min(270, Math.max(200, Math.floor(width * 0.34)));
|
| 672 |
+
const boxH = Math.min(120, Math.max(92, Math.floor(height * 0.24)));
|
| 673 |
+
const boxX = 16;
|
| 674 |
+
const boxY = height - boxH - 14;
|
| 675 |
+
const ox = boxX + Math.floor(boxW * 0.36);
|
| 676 |
+
const oy = boxY + Math.floor(boxH * 0.78);
|
| 677 |
+
|
| 678 |
+
ctx.fillStyle = "rgba(245, 244, 240, 0.92)";
|
| 679 |
+
ctx.strokeStyle = "rgba(35, 51, 61, 0.4)";
|
| 680 |
+
ctx.lineWidth = 1;
|
| 681 |
+
ctx.beginPath();
|
| 682 |
+
ctx.rect(boxX, boxY, boxW, boxH);
|
| 683 |
+
ctx.fill();
|
| 684 |
+
ctx.stroke();
|
| 685 |
+
|
| 686 |
+
const xLen = Math.floor(boxW * 0.42);
|
| 687 |
+
const yLen = Math.floor(boxW * 0.24);
|
| 688 |
+
const zLen = Math.floor(boxH * 0.64);
|
| 689 |
+
|
| 690 |
+
drawArrow(ctx, ox, oy, ox + xLen, oy, "#1f5d7a", 9);
|
| 691 |
+
drawArrow(ctx, ox, oy, ox - yLen, oy - yLen, "#2f7a3a", 9);
|
| 692 |
+
drawArrow(ctx, ox, oy, ox, oy - zLen, "#b23f2c", 9);
|
| 693 |
+
|
| 694 |
+
ctx.fillStyle = "#1f5d7a";
|
| 695 |
+
ctx.font = "12px 'Courier New', monospace";
|
| 696 |
+
ctx.fillText("theta0 (+)", ox + xLen + 4, oy + 4);
|
| 697 |
+
|
| 698 |
+
ctx.fillStyle = "#2f7a3a";
|
| 699 |
+
ctx.fillText("theta1 (+)", Math.max(6, ox - yLen - 62), oy - yLen - 6);
|
| 700 |
+
|
| 701 |
+
ctx.fillStyle = "#b23f2c";
|
| 702 |
+
ctx.fillText("cost J(theta) (+)", ox + 8, oy - zLen - 8);
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
function project3D(x1, x2, y, camera) {
|
| 706 |
+
if (camera.lockCostUp) {
|
| 707 |
+
const cosY = Math.cos(camera.yaw);
|
| 708 |
+
const sinY = Math.sin(camera.yaw);
|
| 709 |
+
const u = x1 * cosY - x2 * sinY;
|
| 710 |
+
const ground = x1 * sinY + x2 * cosY;
|
| 711 |
+
const v = ground * camera.groundTilt + y * camera.costTilt;
|
| 712 |
+
|
| 713 |
+
return {
|
| 714 |
+
x: camera.cx + u * camera.scale,
|
| 715 |
+
y: camera.cy - v * camera.scale,
|
| 716 |
+
};
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
const cosY = Math.cos(camera.yaw);
|
| 720 |
+
const sinY = Math.sin(camera.yaw);
|
| 721 |
+
const cosX = Math.cos(camera.pitch);
|
| 722 |
+
const sinX = Math.sin(camera.pitch);
|
| 723 |
+
|
| 724 |
+
const u = x1 * cosY - x2 * sinY;
|
| 725 |
+
const v0 = x1 * sinY + x2 * cosY;
|
| 726 |
+
const v = v0 * cosX - y * sinX;
|
| 727 |
+
const w = v0 * sinX + y * cosX;
|
| 728 |
+
|
| 729 |
+
const perspective = camera.dist / (camera.dist + w + 0.001);
|
| 730 |
+
return {
|
| 731 |
+
x: camera.cx + u * camera.scale * perspective,
|
| 732 |
+
y: camera.cy - v * camera.scale * perspective,
|
| 733 |
+
};
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
function heatColor(t) {
|
| 737 |
+
const clamped = Math.max(0, Math.min(1, t));
|
| 738 |
+
const r = Math.round(34 + clamped * 210);
|
| 739 |
+
const g = Math.round(48 + (1 - clamped) * 140);
|
| 740 |
+
const b = Math.round(95 + (1 - clamped) * 80);
|
| 741 |
+
return `rgb(${r}, ${g}, ${b})`;
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
function trajectoryColor(t) {
|
| 745 |
+
const clamped = Math.max(0, Math.min(1, t));
|
| 746 |
+
const r = Math.round(22 + clamped * 205);
|
| 747 |
+
const g = Math.round(88 + clamped * 28);
|
| 748 |
+
const b = Math.round(120 - clamped * 68);
|
| 749 |
+
return `rgb(${r}, ${g}, ${b})`;
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
function drawArrow(ctx, x1, y1, x2, y2, color, headSize) {
|
| 753 |
+
const dx = x2 - x1;
|
| 754 |
+
const dy = y2 - y1;
|
| 755 |
+
const len = Math.hypot(dx, dy);
|
| 756 |
+
if (len < 0.0001) return;
|
| 757 |
+
|
| 758 |
+
const ux = dx / len;
|
| 759 |
+
const uy = dy / len;
|
| 760 |
+
const hx = x2 - ux * headSize;
|
| 761 |
+
const hy = y2 - uy * headSize;
|
| 762 |
+
|
| 763 |
+
ctx.strokeStyle = color;
|
| 764 |
+
ctx.fillStyle = color;
|
| 765 |
+
ctx.lineWidth = 2;
|
| 766 |
+
ctx.beginPath();
|
| 767 |
+
ctx.moveTo(x1, y1);
|
| 768 |
+
ctx.lineTo(x2, y2);
|
| 769 |
+
ctx.stroke();
|
| 770 |
+
|
| 771 |
+
ctx.beginPath();
|
| 772 |
+
ctx.moveTo(x2, y2);
|
| 773 |
+
ctx.lineTo(hx - uy * (headSize * 0.5), hy + ux * (headSize * 0.5));
|
| 774 |
+
ctx.lineTo(hx + uy * (headSize * 0.5), hy - ux * (headSize * 0.5));
|
| 775 |
+
ctx.closePath();
|
| 776 |
+
ctx.fill();
|
| 777 |
+
}
|
| 778 |
+
|
| 779 |
+
function startPlayback() {
|
| 780 |
+
if (state.timer || !state.steps.length) return;
|
| 781 |
+
playGdBtn.textContent = "Pause";
|
| 782 |
+
|
| 783 |
+
state.timer = setInterval(() => {
|
| 784 |
+
if (!document.body.contains(gdStep)) {
|
| 785 |
+
stopPlayback();
|
| 786 |
+
return;
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
if (state.currentStep >= state.steps.length - 1) {
|
| 790 |
+
stopPlayback();
|
| 791 |
+
return;
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
state.currentStep += 1;
|
| 795 |
+
gdStep.value = String(state.currentStep);
|
| 796 |
+
renderAll();
|
| 797 |
+
}, 220);
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
function stopPlayback() {
|
| 801 |
+
if (!state.timer) {
|
| 802 |
+
playGdBtn.textContent = "Play";
|
| 803 |
+
return;
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
clearInterval(state.timer);
|
| 807 |
+
state.timer = null;
|
| 808 |
+
playGdBtn.textContent = "Play";
|
| 809 |
+
}
|
| 810 |
+
|
| 811 |
+
function fmt(n) {
|
| 812 |
+
return Number(n).toFixed(4);
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
function rand(min, max) {
|
| 816 |
+
return min + Math.random() * (max - min);
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
function clampInt(v, min, max, fallback) {
|
| 820 |
+
if (!Number.isFinite(v)) return fallback;
|
| 821 |
+
return Math.max(min, Math.min(max, Math.round(v)));
|
| 822 |
+
}
|
| 823 |
+
|
| 824 |
+
function clampNum(v, min, max, fallback) {
|
| 825 |
+
if (!Number.isFinite(v)) return fallback;
|
| 826 |
+
return Math.max(min, Math.min(max, v));
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
init();
|
linear-regression/index.html
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Data Driven World Interface</title>
|
| 7 |
+
<link rel="stylesheet" href="styles.css" />
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div class="bg-grid"></div>
|
| 11 |
+
<main class="container">
|
| 12 |
+
<header class="hero">
|
| 13 |
+
<p class="eyebrow">Course Interface</p>
|
| 14 |
+
<h1>Data Driven World</h1>
|
| 15 |
+
<p>
|
| 16 |
+
Explore NumPy matrix operations through an interactive visual lab.
|
| 17 |
+
Choose operations in natural language, set matrix shapes, and inspect
|
| 18 |
+
the generated NumPy code with input/output visualizations.
|
| 19 |
+
</p>
|
| 20 |
+
</header>
|
| 21 |
+
|
| 22 |
+
<section class="cards">
|
| 23 |
+
<a class="card" href="numpy-lab.html">
|
| 24 |
+
<h2>NumPy Matrix Lab</h2>
|
| 25 |
+
<p>
|
| 26 |
+
Operations, shape-aware inputs, NumPy snippets, and visualized arrays
|
| 27 |
+
up to 3 dimensions.
|
| 28 |
+
</p>
|
| 29 |
+
<span>Open Lab</span>
|
| 30 |
+
</a>
|
| 31 |
+
<a class="card" href="gradient-descent.html">
|
| 32 |
+
<h2>Gradient Descent Studio</h2>
|
| 33 |
+
<p>
|
| 34 |
+
Dynamic optimization walkthrough for one-feature linear regression
|
| 35 |
+
with step-level gradients, costs, and descent-direction landscapes.
|
| 36 |
+
</p>
|
| 37 |
+
<span>Open Studio</span>
|
| 38 |
+
</a>
|
| 39 |
+
<a class="card" href="linear-regression-steps.html">
|
| 40 |
+
<h2>Linear Regression Step Trainer</h2>
|
| 41 |
+
<p>
|
| 42 |
+
Solve hand-calculation style gradient and one-step parameter updates
|
| 43 |
+
on two easy samples with optional z-normalization.
|
| 44 |
+
</p>
|
| 45 |
+
<span>Open Trainer</span>
|
| 46 |
+
</a>
|
| 47 |
+
</section>
|
| 48 |
+
</main>
|
| 49 |
+
</body>
|
| 50 |
+
</html>
|
linear-regression/linear-regression-steps.html
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Linear Regression Step Trainer</title>
|
| 7 |
+
<link rel="stylesheet" href="styles.css" />
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div class="bg-grid"></div>
|
| 11 |
+
<main class="container">
|
| 12 |
+
<nav class="top-nav">
|
| 13 |
+
<a href="index.html">Back Home</a>
|
| 14 |
+
</nav>
|
| 15 |
+
|
| 16 |
+
<header class="hero compact">
|
| 17 |
+
<p class="eyebrow">Page 3</p>
|
| 18 |
+
<h1>Linear Regression Step Trainer</h1>
|
| 19 |
+
<p>
|
| 20 |
+
Practice one-step gradient and parameter updates with two data samples.
|
| 21 |
+
Choose a model setup, optionally enable z-normalization, then inspect
|
| 22 |
+
every calculation in detail.
|
| 23 |
+
</p>
|
| 24 |
+
</header>
|
| 25 |
+
|
| 26 |
+
<section class="panel controls">
|
| 27 |
+
<div class="row">
|
| 28 |
+
<label for="setupSelect">Parameter Setup</label>
|
| 29 |
+
<select id="setupSelect">
|
| 30 |
+
<option value="two_weights_no_intercept">Two weights, no intercept: y_hat = w1x1 + w2x2</option>
|
| 31 |
+
<option value="two_weights_one_intercept">Two weights + one intercept: y_hat = b + w1x1 + w2x2</option>
|
| 32 |
+
<option value="one_weight_one_intercept">One weight + one intercept: y_hat = b + wx</option>
|
| 33 |
+
</select>
|
| 34 |
+
</div>
|
| 35 |
+
|
| 36 |
+
<div class="row">
|
| 37 |
+
<label for="alphaInput">Learning Rate (alpha)</label>
|
| 38 |
+
<input id="alphaInput" type="number" min="0.01" max="1" step="0.01" value="0.1" />
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<div class="row">
|
| 42 |
+
<label for="zNormToggle">Use z-normalization</label>
|
| 43 |
+
<input id="zNormToggle" type="checkbox" />
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<div class="row">
|
| 47 |
+
<button id="newQuestionBtn" type="button">Randomize New Samples</button>
|
| 48 |
+
<button id="solveBtn" type="button">Show Step-by-Step Solution</button>
|
| 49 |
+
</div>
|
| 50 |
+
</section>
|
| 51 |
+
|
| 52 |
+
<section class="panel output">
|
| 53 |
+
<h2>Question Data (Ground-Truth y Included)</h2>
|
| 54 |
+
<div id="questionPrompt" class="viz-grid"></div>
|
| 55 |
+
</section>
|
| 56 |
+
|
| 57 |
+
<section class="panel output">
|
| 58 |
+
<h2>Detailed Process, Result, and Explanation</h2>
|
| 59 |
+
<div id="solutionOutput" class="viz-grid"></div>
|
| 60 |
+
</section>
|
| 61 |
+
</main>
|
| 62 |
+
|
| 63 |
+
<script src="linear-regression-steps.js?v=20260812"></script>
|
| 64 |
+
</body>
|
| 65 |
+
</html>
|
linear-regression/linear-regression-steps.js
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const setupSelect = document.getElementById("setupSelect");
|
| 2 |
+
const alphaInput = document.getElementById("alphaInput");
|
| 3 |
+
const zNormToggle = document.getElementById("zNormToggle");
|
| 4 |
+
const newQuestionBtn = document.getElementById("newQuestionBtn");
|
| 5 |
+
const solveBtn = document.getElementById("solveBtn");
|
| 6 |
+
const questionPrompt = document.getElementById("questionPrompt");
|
| 7 |
+
const solutionOutput = document.getElementById("solutionOutput");
|
| 8 |
+
|
| 9 |
+
const setupConfig = {
|
| 10 |
+
two_weights_no_intercept: {
|
| 11 |
+
label: "Two weights, no intercept",
|
| 12 |
+
featureNames: ["x1", "x2"],
|
| 13 |
+
paramNames: ["w1", "w2"],
|
| 14 |
+
hasIntercept: false,
|
| 15 |
+
formula: "y_hat = w1*x1 + w2*x2",
|
| 16 |
+
},
|
| 17 |
+
two_weights_one_intercept: {
|
| 18 |
+
label: "Two weights + one intercept",
|
| 19 |
+
featureNames: ["x1", "x2"],
|
| 20 |
+
paramNames: ["b", "w1", "w2"],
|
| 21 |
+
hasIntercept: true,
|
| 22 |
+
formula: "y_hat = b + w1*x1 + w2*x2",
|
| 23 |
+
},
|
| 24 |
+
one_weight_one_intercept: {
|
| 25 |
+
label: "One weight + one intercept",
|
| 26 |
+
featureNames: ["x"],
|
| 27 |
+
paramNames: ["b", "w"],
|
| 28 |
+
hasIntercept: true,
|
| 29 |
+
formula: "y_hat = b + w*x",
|
| 30 |
+
},
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
const state = {
|
| 34 |
+
setupKey: "two_weights_no_intercept",
|
| 35 |
+
question: null,
|
| 36 |
+
};
|
| 37 |
+
|
| 38 |
+
function init() {
|
| 39 |
+
bindEvents();
|
| 40 |
+
generateQuestion();
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
function bindEvents() {
|
| 44 |
+
setupSelect.addEventListener("change", () => {
|
| 45 |
+
state.setupKey = setupSelect.value;
|
| 46 |
+
generateQuestion();
|
| 47 |
+
solutionOutput.innerHTML = "";
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
zNormToggle.addEventListener("change", renderQuestionPrompt);
|
| 51 |
+
|
| 52 |
+
newQuestionBtn.addEventListener("click", () => {
|
| 53 |
+
generateQuestion();
|
| 54 |
+
solutionOutput.innerHTML = "";
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
solveBtn.addEventListener("click", () => {
|
| 58 |
+
const alpha = clampNum(Number(alphaInput.value), 0.01, 1, 0.1);
|
| 59 |
+
alphaInput.value = fmt(alpha);
|
| 60 |
+
const useZNorm = zNormToggle.checked;
|
| 61 |
+
renderSolution(alpha, useZNorm);
|
| 62 |
+
});
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
function generateQuestion() {
|
| 66 |
+
const cfg = setupConfig[state.setupKey];
|
| 67 |
+
const rows = [];
|
| 68 |
+
|
| 69 |
+
if (state.setupKey === "one_weight_one_intercept") {
|
| 70 |
+
const trueB = randInt(-1, 3);
|
| 71 |
+
const trueW = randInt(1, 4);
|
| 72 |
+
const x1 = randInt(1, 4);
|
| 73 |
+
let x2 = randInt(2, 6);
|
| 74 |
+
while (x2 === x1) x2 = randInt(2, 6);
|
| 75 |
+
|
| 76 |
+
rows.push({ x: [x1], y: trueB + trueW * x1 });
|
| 77 |
+
rows.push({ x: [x2], y: trueB + trueW * x2 });
|
| 78 |
+
|
| 79 |
+
state.question = {
|
| 80 |
+
trueParams: { b: trueB, w: trueW },
|
| 81 |
+
initParams: { b: 0, w: 0 },
|
| 82 |
+
rows,
|
| 83 |
+
cfg,
|
| 84 |
+
};
|
| 85 |
+
} else if (state.setupKey === "two_weights_no_intercept") {
|
| 86 |
+
const trueW1 = randInt(1, 3);
|
| 87 |
+
const trueW2 = randInt(1, 3);
|
| 88 |
+
|
| 89 |
+
const x11 = randInt(1, 4);
|
| 90 |
+
let x12 = randInt(2, 6);
|
| 91 |
+
while (x12 === x11) x12 = randInt(2, 6);
|
| 92 |
+
|
| 93 |
+
const x21 = randInt(1, 4);
|
| 94 |
+
let x22 = randInt(2, 6);
|
| 95 |
+
while (x22 === x21) x22 = randInt(2, 6);
|
| 96 |
+
|
| 97 |
+
rows.push({ x: [x11, x21], y: trueW1 * x11 + trueW2 * x21 });
|
| 98 |
+
rows.push({ x: [x12, x22], y: trueW1 * x12 + trueW2 * x22 });
|
| 99 |
+
|
| 100 |
+
state.question = {
|
| 101 |
+
trueParams: { w1: trueW1, w2: trueW2 },
|
| 102 |
+
initParams: { w1: 0, w2: 0 },
|
| 103 |
+
rows,
|
| 104 |
+
cfg,
|
| 105 |
+
};
|
| 106 |
+
} else {
|
| 107 |
+
const trueB = randInt(-1, 3);
|
| 108 |
+
const trueW1 = randInt(1, 3);
|
| 109 |
+
const trueW2 = randInt(1, 3);
|
| 110 |
+
|
| 111 |
+
const x11 = randInt(1, 4);
|
| 112 |
+
let x12 = randInt(2, 6);
|
| 113 |
+
while (x12 === x11) x12 = randInt(2, 6);
|
| 114 |
+
|
| 115 |
+
const x21 = randInt(1, 4);
|
| 116 |
+
let x22 = randInt(2, 6);
|
| 117 |
+
while (x22 === x21) x22 = randInt(2, 6);
|
| 118 |
+
|
| 119 |
+
rows.push({ x: [x11, x21], y: trueB + trueW1 * x11 + trueW2 * x21 });
|
| 120 |
+
rows.push({ x: [x12, x22], y: trueB + trueW1 * x12 + trueW2 * x22 });
|
| 121 |
+
|
| 122 |
+
state.question = {
|
| 123 |
+
trueParams: { b: trueB, w1: trueW1, w2: trueW2 },
|
| 124 |
+
initParams: { b: 0, w1: 0, w2: 0 },
|
| 125 |
+
rows,
|
| 126 |
+
cfg,
|
| 127 |
+
};
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
renderQuestionPrompt();
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
function renderQuestionPrompt() {
|
| 134 |
+
if (!state.question) return;
|
| 135 |
+
|
| 136 |
+
const useZNorm = zNormToggle.checked;
|
| 137 |
+
const { cfg, rows } = state.question;
|
| 138 |
+
|
| 139 |
+
questionPrompt.innerHTML = `
|
| 140 |
+
<div class="matrix-card">
|
| 141 |
+
<p class="matrix-title">Setup</p>
|
| 142 |
+
<p class="formula-line">Model: ${escapeHtml(cfg.label)}</p>
|
| 143 |
+
<p class="formula-line">Formula: ${escapeHtml(cfg.formula)}</p>
|
| 144 |
+
<p class="formula-line">Initial parameters: ${renderParamInline(state.question.initParams)}</p>
|
| 145 |
+
<p class="formula-line">Normalization for update step: ${useZNorm ? "z-normalization enabled" : "raw features"}</p>
|
| 146 |
+
<p class="formula-line">Task: Compute gradients and one update step for all parameters.</p>
|
| 147 |
+
</div>
|
| 148 |
+
<div class="matrix-card">
|
| 149 |
+
<p class="matrix-title">Two Data Samples</p>
|
| 150 |
+
${renderDataTable(rows, cfg.featureNames, null)}
|
| 151 |
+
<p class="slice-label">Ground-truth y is given in the table.</p>
|
| 152 |
+
</div>
|
| 153 |
+
`;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
function renderSolution(alpha, useZNorm) {
|
| 157 |
+
if (!state.question) return;
|
| 158 |
+
|
| 159 |
+
const { cfg, rows, initParams } = state.question;
|
| 160 |
+
const m = rows.length;
|
| 161 |
+
const featureStats = computeFeatureStats(rows);
|
| 162 |
+
const rowsUsed = rows.map((row) => ({
|
| 163 |
+
y: row.y,
|
| 164 |
+
x: row.x.map((value, idx) => (useZNorm ? zNorm(value, featureStats[idx]) : value)),
|
| 165 |
+
xRaw: row.x.slice(),
|
| 166 |
+
}));
|
| 167 |
+
|
| 168 |
+
const orderedParams = cfg.paramNames.slice();
|
| 169 |
+
const grads = {};
|
| 170 |
+
const update = {};
|
| 171 |
+
const predRows = [];
|
| 172 |
+
|
| 173 |
+
for (const name of orderedParams) grads[name] = 0;
|
| 174 |
+
|
| 175 |
+
for (let i = 0; i < m; i += 1) {
|
| 176 |
+
const row = rowsUsed[i];
|
| 177 |
+
const yHat = predict(cfg, initParams, row.x);
|
| 178 |
+
const err = yHat - row.y;
|
| 179 |
+
|
| 180 |
+
predRows.push({ index: i + 1, x: row.x, xRaw: row.xRaw, y: row.y, yHat, err });
|
| 181 |
+
|
| 182 |
+
if (cfg.hasIntercept) grads.b += err;
|
| 183 |
+
|
| 184 |
+
if (state.setupKey === "one_weight_one_intercept") {
|
| 185 |
+
grads.w += err * row.x[0];
|
| 186 |
+
} else {
|
| 187 |
+
grads.w1 += err * row.x[0];
|
| 188 |
+
grads.w2 += err * row.x[1];
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
for (const name of orderedParams) {
|
| 193 |
+
grads[name] /= m;
|
| 194 |
+
update[name] = initParams[name] - alpha * grads[name];
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
const cost = predRows.reduce((acc, r) => acc + r.err * r.err, 0) / (2 * m);
|
| 198 |
+
|
| 199 |
+
const normBlock = useZNorm
|
| 200 |
+
? `
|
| 201 |
+
<div class="matrix-card">
|
| 202 |
+
<p class="matrix-title">Step 0: z-Normalization</p>
|
| 203 |
+
${renderNormSummary(cfg.featureNames, featureStats)}
|
| 204 |
+
${renderDataTable(rows, cfg.featureNames, rowsUsed.map((r) => r.x))}
|
| 205 |
+
</div>
|
| 206 |
+
`
|
| 207 |
+
: "";
|
| 208 |
+
|
| 209 |
+
solutionOutput.innerHTML = `
|
| 210 |
+
${normBlock}
|
| 211 |
+
<div class="matrix-card">
|
| 212 |
+
<p class="matrix-title">Step 1: Predictions and Errors</p>
|
| 213 |
+
${renderPredictionSteps(cfg, predRows, initParams, useZNorm)}
|
| 214 |
+
<p class="formula-line">Cost: J = (1/(2m)) * sum((y_hat - y)^2) = ${fmt(cost)}</p>
|
| 215 |
+
</div>
|
| 216 |
+
|
| 217 |
+
<div class="matrix-card">
|
| 218 |
+
<p class="matrix-title">Step 2: Gradient Calculation</p>
|
| 219 |
+
${renderGradientSteps(cfg, predRows, grads, m)}
|
| 220 |
+
</div>
|
| 221 |
+
|
| 222 |
+
<div class="matrix-card">
|
| 223 |
+
<p class="matrix-title">Step 3: Parameter Update (One Gradient Step)</p>
|
| 224 |
+
${renderUpdateSteps(cfg, initParams, grads, update, alpha)}
|
| 225 |
+
</div>
|
| 226 |
+
|
| 227 |
+
<div class="matrix-card">
|
| 228 |
+
<p class="matrix-title">Explanation</p>
|
| 229 |
+
<p class="formula-line">A negative gradient means increasing that parameter will reduce cost, so the update adds value in that direction.</p>
|
| 230 |
+
<p class="formula-line">A positive gradient means decreasing that parameter will reduce cost.</p>
|
| 231 |
+
<p class="formula-line">${useZNorm ? "Using z-normalized features keeps scales consistent, so gradient magnitudes across features are easier to compare." : "Raw features are used directly, so larger-scale features can produce larger gradient terms."}</p>
|
| 232 |
+
<p class="formula-line">Use <strong>Randomize New Samples</strong> to practice again with fresh numbers.</p>
|
| 233 |
+
</div>
|
| 234 |
+
`;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
function renderDataTable(rows, featureNames, zRows) {
|
| 238 |
+
const headers = featureNames.map((f) => `<th>${f}</th>`).join("");
|
| 239 |
+
const zHeaders = zRows ? featureNames.map((f) => `<th>z(${f})</th>`).join("") : "";
|
| 240 |
+
|
| 241 |
+
const body = rows
|
| 242 |
+
.map((row, i) => {
|
| 243 |
+
const xs = row.x.map((v) => `<td>${fmt(v)}</td>`).join("");
|
| 244 |
+
const zs = zRows
|
| 245 |
+
? zRows[i].map((v) => `<td>${fmt(v)}</td>`).join("")
|
| 246 |
+
: "";
|
| 247 |
+
return `<tr><td>${i + 1}</td>${xs}${zs}<td>${fmt(row.y)}</td></tr>`;
|
| 248 |
+
})
|
| 249 |
+
.join("");
|
| 250 |
+
|
| 251 |
+
return `
|
| 252 |
+
<table class="matrix-table">
|
| 253 |
+
<thead>
|
| 254 |
+
<tr>
|
| 255 |
+
<th>sample</th>
|
| 256 |
+
${headers}
|
| 257 |
+
${zHeaders}
|
| 258 |
+
<th>y (ground truth)</th>
|
| 259 |
+
</tr>
|
| 260 |
+
</thead>
|
| 261 |
+
<tbody>${body}</tbody>
|
| 262 |
+
</table>
|
| 263 |
+
`;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
function renderNormSummary(featureNames, stats) {
|
| 267 |
+
return featureNames
|
| 268 |
+
.map((name, idx) => {
|
| 269 |
+
const s = stats[idx];
|
| 270 |
+
return `<p class="formula-line">${name}: mean=${fmt(s.mean)}, std=${fmt(s.std)} so z = (x - ${fmt(s.mean)}) / ${fmt(s.std)}</p>`;
|
| 271 |
+
})
|
| 272 |
+
.join("");
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
function renderPredictionSteps(cfg, predRows, initParams, useZNorm) {
|
| 276 |
+
const pLine = renderParamInline(initParams);
|
| 277 |
+
const head = `<p class="formula-line">Start with ${pLine}. ${useZNorm ? "Use normalized x values." : "Use raw x values."}</p>`;
|
| 278 |
+
|
| 279 |
+
const lines = predRows
|
| 280 |
+
.map((row) => {
|
| 281 |
+
if (state.setupKey === "one_weight_one_intercept") {
|
| 282 |
+
return `<p class="formula-line">sample ${row.index}: y_hat = b + w*x = ${fmt(initParams.b)} + ${fmt(initParams.w)}*${fmt(row.x[0])} = ${fmt(row.yHat)}, error = y_hat - y = ${fmt(row.yHat)} - ${fmt(row.y)} = ${fmt(row.err)}</p>`;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
if (cfg.hasIntercept) {
|
| 286 |
+
return `<p class="formula-line">sample ${row.index}: y_hat = b + w1*x1 + w2*x2 = ${fmt(initParams.b)} + ${fmt(initParams.w1)}*${fmt(row.x[0])} + ${fmt(initParams.w2)}*${fmt(row.x[1])} = ${fmt(row.yHat)}, error = ${fmt(row.err)}</p>`;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
return `<p class="formula-line">sample ${row.index}: y_hat = w1*x1 + w2*x2 = ${fmt(initParams.w1)}*${fmt(row.x[0])} + ${fmt(initParams.w2)}*${fmt(row.x[1])} = ${fmt(row.yHat)}, error = ${fmt(row.err)}</p>`;
|
| 290 |
+
})
|
| 291 |
+
.join("");
|
| 292 |
+
|
| 293 |
+
return head + lines;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
function renderGradientSteps(cfg, predRows, grads, m) {
|
| 297 |
+
const errTerms = predRows.map((r) => fmt(r.err)).join(" + ");
|
| 298 |
+
let out = "";
|
| 299 |
+
|
| 300 |
+
if (cfg.hasIntercept) {
|
| 301 |
+
out += `<p class="formula-line">grad_b = (1/m) * sum(error) = (1/${m}) * (${errTerms}) = ${fmt(grads.b)}</p>`;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
if (state.setupKey === "one_weight_one_intercept") {
|
| 305 |
+
const terms = predRows.map((r) => `${fmt(r.err)}*${fmt(r.x[0])}`).join(" + ");
|
| 306 |
+
out += `<p class="formula-line">grad_w = (1/m) * sum(error*x) = (1/${m}) * (${terms}) = ${fmt(grads.w)}</p>`;
|
| 307 |
+
return out;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
const terms1 = predRows.map((r) => `${fmt(r.err)}*${fmt(r.x[0])}`).join(" + ");
|
| 311 |
+
const terms2 = predRows.map((r) => `${fmt(r.err)}*${fmt(r.x[1])}`).join(" + ");
|
| 312 |
+
out += `<p class="formula-line">grad_w1 = (1/m) * sum(error*x1) = (1/${m}) * (${terms1}) = ${fmt(grads.w1)}</p>`;
|
| 313 |
+
out += `<p class="formula-line">grad_w2 = (1/m) * sum(error*x2) = (1/${m}) * (${terms2}) = ${fmt(grads.w2)}</p>`;
|
| 314 |
+
|
| 315 |
+
return out;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
function renderUpdateSteps(cfg, initParams, grads, update, alpha) {
|
| 319 |
+
return cfg.paramNames
|
| 320 |
+
.map((name) => {
|
| 321 |
+
return `<p class="formula-line">${name}_new = ${name} - alpha*grad_${name} = ${fmt(initParams[name])} - ${fmt(alpha)}*${fmt(grads[name])} = ${fmt(update[name])}</p>`;
|
| 322 |
+
})
|
| 323 |
+
.join("");
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
function predict(cfg, params, xRow) {
|
| 327 |
+
let yHat = cfg.hasIntercept ? params.b : 0;
|
| 328 |
+
|
| 329 |
+
if (state.setupKey === "one_weight_one_intercept") {
|
| 330 |
+
yHat += params.w * xRow[0];
|
| 331 |
+
return yHat;
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
yHat += params.w1 * xRow[0] + params.w2 * xRow[1];
|
| 335 |
+
return yHat;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
function computeFeatureStats(rows) {
|
| 339 |
+
const d = rows[0].x.length;
|
| 340 |
+
const stats = [];
|
| 341 |
+
|
| 342 |
+
for (let j = 0; j < d; j += 1) {
|
| 343 |
+
const values = rows.map((r) => r.x[j]);
|
| 344 |
+
const mean = values.reduce((acc, v) => acc + v, 0) / values.length;
|
| 345 |
+
const variance =
|
| 346 |
+
values.reduce((acc, v) => acc + (v - mean) * (v - mean), 0) / values.length;
|
| 347 |
+
const std = Math.sqrt(variance) || 1;
|
| 348 |
+
stats.push({ mean, std });
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
return stats;
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
function zNorm(value, stat) {
|
| 355 |
+
return (value - stat.mean) / stat.std;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
function renderParamInline(params) {
|
| 359 |
+
return Object.entries(params)
|
| 360 |
+
.map(([k, v]) => `${k}=${fmt(v)}`)
|
| 361 |
+
.join(", ");
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
function clampNum(v, min, max, fallback) {
|
| 365 |
+
if (!Number.isFinite(v)) return fallback;
|
| 366 |
+
return Math.max(min, Math.min(max, v));
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
function randInt(min, max) {
|
| 370 |
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
function fmt(v) {
|
| 374 |
+
if (!Number.isFinite(v)) return String(v);
|
| 375 |
+
return Number(v.toFixed(4)).toString();
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
function escapeHtml(text) {
|
| 379 |
+
return String(text)
|
| 380 |
+
.replaceAll("&", "&")
|
| 381 |
+
.replaceAll("<", "<")
|
| 382 |
+
.replaceAll(">", ">")
|
| 383 |
+
.replaceAll('"', """)
|
| 384 |
+
.replaceAll("'", "'");
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
init();
|
linear-regression/numpy-lab.html
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>NumPy Matrix Lab</title>
|
| 7 |
+
<link rel="stylesheet" href="styles.css" />
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div class="bg-grid"></div>
|
| 11 |
+
<main class="container">
|
| 12 |
+
<nav class="top-nav">
|
| 13 |
+
<a href="index.html">Back Home</a>
|
| 14 |
+
</nav>
|
| 15 |
+
|
| 16 |
+
<header class="hero compact">
|
| 17 |
+
<p class="eyebrow">Page 1</p>
|
| 18 |
+
<h1>NumPy Matrix Operations</h1>
|
| 19 |
+
<p>
|
| 20 |
+
Pick a natural-language operation, define matrix shapes (max 3D), and
|
| 21 |
+
inspect input/output arrays plus executable NumPy code.
|
| 22 |
+
</p>
|
| 23 |
+
<p class="slice-label">
|
| 24 |
+
Broadcasting is enabled. Example element-wise valid pair: (2,3) with (1,3). Example matmul valid pair: (2,3) with (4,3,2).
|
| 25 |
+
</p>
|
| 26 |
+
</header>
|
| 27 |
+
|
| 28 |
+
<section class="panel controls">
|
| 29 |
+
<div class="row">
|
| 30 |
+
<label for="operationSelect">Operation</label>
|
| 31 |
+
<select id="operationSelect"></select>
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
<div class="row">
|
| 35 |
+
<label for="nlInput">Natural language query</label>
|
| 36 |
+
<input
|
| 37 |
+
id="nlInput"
|
| 38 |
+
type="text"
|
| 39 |
+
placeholder="Example: multiply matrix A and B"
|
| 40 |
+
/>
|
| 41 |
+
<button id="matchBtn" type="button">Match Query</button>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<div id="operationInfo" class="operation-info"></div>
|
| 45 |
+
<div id="shapeInputs" class="shape-grid"></div>
|
| 46 |
+
<div id="operationOptions" class="shape-grid"></div>
|
| 47 |
+
|
| 48 |
+
<div class="row">
|
| 49 |
+
<button id="runBtn" type="button">Generate and Compute</button>
|
| 50 |
+
</div>
|
| 51 |
+
</section>
|
| 52 |
+
|
| 53 |
+
<section class="panel output">
|
| 54 |
+
<h2>Generated NumPy Code</h2>
|
| 55 |
+
<pre id="codeOutput" class="code"></pre>
|
| 56 |
+
</section>
|
| 57 |
+
|
| 58 |
+
<section class="panel output">
|
| 59 |
+
<h2>Input Matrices</h2>
|
| 60 |
+
<div id="inputViz" class="viz-grid"></div>
|
| 61 |
+
</section>
|
| 62 |
+
|
| 63 |
+
<section class="panel output">
|
| 64 |
+
<h2>Output Matrix</h2>
|
| 65 |
+
<div id="outputViz" class="viz-grid"></div>
|
| 66 |
+
</section>
|
| 67 |
+
|
| 68 |
+
<section class="panel output">
|
| 69 |
+
<h2>Computation Details</h2>
|
| 70 |
+
<div id="detailViz" class="viz-grid"></div>
|
| 71 |
+
</section>
|
| 72 |
+
</main>
|
| 73 |
+
|
| 74 |
+
<script src="app.js?v=20260812"></script>
|
| 75 |
+
</body>
|
| 76 |
+
</html>
|
linear-regression/styles.css
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--bg: #f4f1ea;
|
| 3 |
+
--card: #fffef9;
|
| 4 |
+
--ink: #18242d;
|
| 5 |
+
--muted: #57646d;
|
| 6 |
+
--accent: #dd5e2f;
|
| 7 |
+
--accent-2: #0c7b73;
|
| 8 |
+
--line: #d8d2c4;
|
| 9 |
+
--shadow: 0 12px 30px rgba(0, 0, 0, 0.08);
|
| 10 |
+
--radius: 14px;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
* {
|
| 14 |
+
box-sizing: border-box;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
body {
|
| 18 |
+
margin: 0;
|
| 19 |
+
font-family: "Avenir Next", "Gill Sans", "Trebuchet MS", sans-serif;
|
| 20 |
+
color: var(--ink);
|
| 21 |
+
background: radial-gradient(circle at 20% 10%, #fff6df, #f4f1ea 45%),
|
| 22 |
+
linear-gradient(120deg, #f4f1ea, #ebf3ef);
|
| 23 |
+
min-height: 100vh;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.bg-grid {
|
| 27 |
+
position: fixed;
|
| 28 |
+
inset: 0;
|
| 29 |
+
background-image: linear-gradient(rgba(12, 123, 115, 0.07) 1px, transparent 1px),
|
| 30 |
+
linear-gradient(90deg, rgba(12, 123, 115, 0.07) 1px, transparent 1px);
|
| 31 |
+
background-size: 36px 36px;
|
| 32 |
+
pointer-events: none;
|
| 33 |
+
z-index: -1;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.container {
|
| 37 |
+
width: min(1050px, 92vw);
|
| 38 |
+
margin: 0 auto;
|
| 39 |
+
padding: 28px 0 50px;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.top-nav {
|
| 43 |
+
margin-bottom: 12px;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.top-nav a {
|
| 47 |
+
text-decoration: none;
|
| 48 |
+
color: var(--accent-2);
|
| 49 |
+
font-weight: 700;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.hero {
|
| 53 |
+
background: var(--card);
|
| 54 |
+
border: 1px solid var(--line);
|
| 55 |
+
border-radius: var(--radius);
|
| 56 |
+
padding: 28px;
|
| 57 |
+
box-shadow: var(--shadow);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.hero.compact {
|
| 61 |
+
padding: 22px 24px;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.eyebrow {
|
| 65 |
+
margin: 0;
|
| 66 |
+
text-transform: uppercase;
|
| 67 |
+
letter-spacing: 1.8px;
|
| 68 |
+
color: var(--accent);
|
| 69 |
+
font-weight: 800;
|
| 70 |
+
font-size: 12px;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
h1 {
|
| 74 |
+
margin: 10px 0 8px;
|
| 75 |
+
font-size: clamp(1.8rem, 3vw, 2.8rem);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
h2 {
|
| 79 |
+
margin-top: 0;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
p {
|
| 83 |
+
margin: 0;
|
| 84 |
+
line-height: 1.6;
|
| 85 |
+
color: var(--muted);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.cards {
|
| 89 |
+
margin-top: 20px;
|
| 90 |
+
display: grid;
|
| 91 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 92 |
+
gap: 16px;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.card {
|
| 96 |
+
display: block;
|
| 97 |
+
background: var(--card);
|
| 98 |
+
border: 1px solid var(--line);
|
| 99 |
+
border-radius: var(--radius);
|
| 100 |
+
padding: 20px;
|
| 101 |
+
color: inherit;
|
| 102 |
+
text-decoration: none;
|
| 103 |
+
box-shadow: var(--shadow);
|
| 104 |
+
transition: transform 0.2s ease;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.card:hover {
|
| 108 |
+
transform: translateY(-4px);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.card span {
|
| 112 |
+
display: inline-block;
|
| 113 |
+
margin-top: 14px;
|
| 114 |
+
color: var(--accent-2);
|
| 115 |
+
font-weight: 700;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.panel {
|
| 119 |
+
margin-top: 16px;
|
| 120 |
+
background: var(--card);
|
| 121 |
+
border: 1px solid var(--line);
|
| 122 |
+
border-radius: var(--radius);
|
| 123 |
+
padding: 20px;
|
| 124 |
+
box-shadow: var(--shadow);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.row {
|
| 128 |
+
display: flex;
|
| 129 |
+
flex-wrap: wrap;
|
| 130 |
+
gap: 10px;
|
| 131 |
+
align-items: center;
|
| 132 |
+
margin-bottom: 14px;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
label {
|
| 136 |
+
font-weight: 700;
|
| 137 |
+
min-width: 170px;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
select,
|
| 141 |
+
input,
|
| 142 |
+
button {
|
| 143 |
+
border-radius: 10px;
|
| 144 |
+
border: 1px solid #c7c0b2;
|
| 145 |
+
padding: 10px 12px;
|
| 146 |
+
font-size: 0.96rem;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
select,
|
| 150 |
+
input {
|
| 151 |
+
background: #fff;
|
| 152 |
+
flex: 1;
|
| 153 |
+
min-width: 220px;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
button {
|
| 157 |
+
background: var(--accent);
|
| 158 |
+
color: #fff;
|
| 159 |
+
border: none;
|
| 160 |
+
font-weight: 700;
|
| 161 |
+
cursor: pointer;
|
| 162 |
+
transition: filter 0.15s ease;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
button:hover {
|
| 166 |
+
filter: brightness(0.94);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.operation-info {
|
| 170 |
+
margin: 8px 0 14px;
|
| 171 |
+
color: var(--muted);
|
| 172 |
+
font-size: 0.95rem;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.shape-grid {
|
| 176 |
+
display: grid;
|
| 177 |
+
gap: 10px;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.shape-card {
|
| 181 |
+
padding: 12px;
|
| 182 |
+
border-radius: 12px;
|
| 183 |
+
border: 1px solid var(--line);
|
| 184 |
+
background: #fff;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.shape-card h3 {
|
| 188 |
+
margin: 0 0 8px;
|
| 189 |
+
font-size: 1rem;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.shape-card input {
|
| 193 |
+
width: 100%;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.options-card {
|
| 197 |
+
background: #fffaf1;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.inline-fields {
|
| 201 |
+
display: flex;
|
| 202 |
+
align-items: center;
|
| 203 |
+
gap: 10px;
|
| 204 |
+
margin-bottom: 8px;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.mini-label {
|
| 208 |
+
min-width: 70px;
|
| 209 |
+
font-weight: 700;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
input[type="checkbox"] {
|
| 213 |
+
width: 18px;
|
| 214 |
+
height: 18px;
|
| 215 |
+
min-width: 18px;
|
| 216 |
+
flex: 0;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.code {
|
| 220 |
+
margin: 0;
|
| 221 |
+
padding: 14px;
|
| 222 |
+
border-radius: 12px;
|
| 223 |
+
background: #1c252e;
|
| 224 |
+
color: #f3f0e6;
|
| 225 |
+
overflow-x: auto;
|
| 226 |
+
min-height: 120px;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.viz-grid {
|
| 230 |
+
display: grid;
|
| 231 |
+
gap: 14px;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.matrix-card {
|
| 235 |
+
border: 1px solid var(--line);
|
| 236 |
+
border-radius: 12px;
|
| 237 |
+
padding: 12px;
|
| 238 |
+
background: #fff;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.matrix-title {
|
| 242 |
+
margin: 0 0 8px;
|
| 243 |
+
font-weight: 700;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.shape-badge {
|
| 247 |
+
display: inline-block;
|
| 248 |
+
margin-left: 8px;
|
| 249 |
+
color: #fff;
|
| 250 |
+
background: var(--accent-2);
|
| 251 |
+
border-radius: 999px;
|
| 252 |
+
font-size: 0.8rem;
|
| 253 |
+
padding: 2px 8px;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.matrix-table {
|
| 257 |
+
border-collapse: collapse;
|
| 258 |
+
width: max-content;
|
| 259 |
+
max-width: 100%;
|
| 260 |
+
margin-bottom: 10px;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
.matrix-table td {
|
| 264 |
+
border: 1px solid #ddd3c0;
|
| 265 |
+
padding: 6px 9px;
|
| 266 |
+
text-align: right;
|
| 267 |
+
font-family: "Courier New", monospace;
|
| 268 |
+
font-size: 0.9rem;
|
| 269 |
+
background: #fdfbf5;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.matrix-table th {
|
| 273 |
+
border: 1px solid #ddd3c0;
|
| 274 |
+
padding: 6px 9px;
|
| 275 |
+
text-align: right;
|
| 276 |
+
font-family: "Courier New", monospace;
|
| 277 |
+
font-size: 0.86rem;
|
| 278 |
+
background: #f6efdf;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.matrix-pick-btn {
|
| 282 |
+
width: 100%;
|
| 283 |
+
border: 1px solid #ddd3c0;
|
| 284 |
+
border-radius: 6px;
|
| 285 |
+
padding: 5px 7px;
|
| 286 |
+
background: #fff;
|
| 287 |
+
color: var(--ink);
|
| 288 |
+
font-family: "Courier New", monospace;
|
| 289 |
+
font-weight: 700;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
.matrix-pick-btn.active {
|
| 293 |
+
background: var(--accent-2);
|
| 294 |
+
color: #fff;
|
| 295 |
+
border-color: var(--accent-2);
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.hl-row {
|
| 299 |
+
background: #fff1dc !important;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
.hl-col {
|
| 303 |
+
background: #e7f7f4 !important;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.hl-cell {
|
| 307 |
+
background: #ddeafc !important;
|
| 308 |
+
font-weight: 800;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.slice-label {
|
| 312 |
+
font-size: 0.85rem;
|
| 313 |
+
color: var(--muted);
|
| 314 |
+
margin: 6px 0;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
.formula-line {
|
| 318 |
+
margin: 4px 0;
|
| 319 |
+
font-family: "Courier New", monospace;
|
| 320 |
+
font-size: 0.9rem;
|
| 321 |
+
color: #29333a;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.slider-row {
|
| 325 |
+
display: grid;
|
| 326 |
+
grid-template-columns: auto 1fr auto auto auto;
|
| 327 |
+
gap: 10px;
|
| 328 |
+
align-items: center;
|
| 329 |
+
margin: 8px 0 10px;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
.slider-row label {
|
| 333 |
+
min-width: 36px;
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
.error {
|
| 337 |
+
color: #a11634;
|
| 338 |
+
font-weight: 700;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.two-col-row {
|
| 342 |
+
align-items: flex-end;
|
| 343 |
+
display: grid;
|
| 344 |
+
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
| 345 |
+
gap: 12px;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.control-group {
|
| 349 |
+
display: grid;
|
| 350 |
+
gap: 6px;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
.control-group label {
|
| 354 |
+
min-width: 0;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.gd-slider-row {
|
| 358 |
+
grid-template-columns: auto 1fr auto auto auto;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
#fitCanvas,
|
| 362 |
+
#costCanvas,
|
| 363 |
+
#landscapeCanvas {
|
| 364 |
+
width: 100%;
|
| 365 |
+
border: 1px solid var(--line);
|
| 366 |
+
border-radius: 12px;
|
| 367 |
+
background: #fff;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.log-wrap {
|
| 371 |
+
overflow: auto;
|
| 372 |
+
border: 1px solid var(--line);
|
| 373 |
+
border-radius: 12px;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
.gd-log-table {
|
| 377 |
+
border-collapse: collapse;
|
| 378 |
+
width: 100%;
|
| 379 |
+
min-width: 800px;
|
| 380 |
+
font-family: "Courier New", monospace;
|
| 381 |
+
font-size: 0.88rem;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
.gd-log-table th,
|
| 385 |
+
.gd-log-table td {
|
| 386 |
+
border-bottom: 1px solid #e6dfd2;
|
| 387 |
+
padding: 8px 10px;
|
| 388 |
+
text-align: left;
|
| 389 |
+
white-space: nowrap;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
.gd-log-table th {
|
| 393 |
+
position: sticky;
|
| 394 |
+
top: 0;
|
| 395 |
+
background: #faf6eb;
|
| 396 |
+
z-index: 1;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
.active-log-row td {
|
| 400 |
+
background: #eef8f6;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
@media (max-width: 680px) {
|
| 404 |
+
.container {
|
| 405 |
+
width: 94vw;
|
| 406 |
+
padding-top: 18px;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
label {
|
| 410 |
+
min-width: 100%;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
.row {
|
| 414 |
+
margin-bottom: 10px;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
.slider-row {
|
| 418 |
+
grid-template-columns: 1fr;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.slider-row label {
|
| 422 |
+
min-width: 0;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
.gd-slider-row {
|
| 426 |
+
grid-template-columns: 1fr;
|
| 427 |
+
}
|
| 428 |
+
}
|
logistic-regression/about.html
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>DDW Interface - Notes</title>
|
| 7 |
+
<link rel="stylesheet" href="styles.css" />
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<header class="site-header">
|
| 11 |
+
<div>
|
| 12 |
+
<p class="kicker">Data Driven World</p>
|
| 13 |
+
<h1>Interface Notes</h1>
|
| 14 |
+
</div>
|
| 15 |
+
<nav>
|
| 16 |
+
<a href="index.html">Home</a>
|
| 17 |
+
<a href="simple-sigmoid.html">Simple Sigmoid</a>
|
| 18 |
+
<a href="confusion-matrix.html">Confusion Matrix (Page 3)</a>
|
| 19 |
+
<a href="sigmoid.html">Sigmoid Function</a>
|
| 20 |
+
<a href="cost-visualization.html">Cost Function</a>
|
| 21 |
+
<a class="active" href="about.html">Notes</a>
|
| 22 |
+
</nav>
|
| 23 |
+
</header>
|
| 24 |
+
|
| 25 |
+
<main class="panel page-copy">
|
| 26 |
+
<h2>How to use the sigmoid page</h2>
|
| 27 |
+
<p>
|
| 28 |
+
The sigmoid page visualizes logistic regression probability:
|
| 29 |
+
<code>p = 1 / (1 + exp(-(b0 + b1x)))</code>.
|
| 30 |
+
</p>
|
| 31 |
+
<p>
|
| 32 |
+
Adjust <code>b0</code>, <code>b1</code>, and threshold <code>t</code> to see how decision boundaries and
|
| 33 |
+
probabilities change. You can also click the chart to add sample points and inspect classification outcomes.
|
| 34 |
+
</p>
|
| 35 |
+
</main>
|
| 36 |
+
</body>
|
| 37 |
+
</html>
|
logistic-regression/app.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const canvas = document.getElementById("plot");
|
| 2 |
+
const ctx = canvas.getContext("2d");
|
| 3 |
+
|
| 4 |
+
const controls = {
|
| 5 |
+
b0: document.getElementById("b0"),
|
| 6 |
+
b1: document.getElementById("b1"),
|
| 7 |
+
t: document.getElementById("t"),
|
| 8 |
+
probeX: document.getElementById("probeX"),
|
| 9 |
+
showGrid: document.getElementById("showGrid"),
|
| 10 |
+
showShade: document.getElementById("showShade"),
|
| 11 |
+
showDerivative: document.getElementById("showDerivative"),
|
| 12 |
+
preset: document.getElementById("preset"),
|
| 13 |
+
resetBtn: document.getElementById("resetBtn"),
|
| 14 |
+
animateBtn: document.getElementById("animateBtn"),
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
const valueLabels = {
|
| 18 |
+
b0: document.getElementById("b0Value"),
|
| 19 |
+
b1: document.getElementById("b1Value"),
|
| 20 |
+
t: document.getElementById("tValue"),
|
| 21 |
+
probeX: document.getElementById("probeValue"),
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
const stats = document.getElementById("stats");
|
| 25 |
+
const points = [];
|
| 26 |
+
|
| 27 |
+
let animationTimer = null;
|
| 28 |
+
let animationDirection = 1;
|
| 29 |
+
|
| 30 |
+
const MARGIN = { left: 70, right: 24, top: 24, bottom: 58 };
|
| 31 |
+
const X_MIN = -10;
|
| 32 |
+
const X_MAX = 10;
|
| 33 |
+
|
| 34 |
+
function logistic(z) {
|
| 35 |
+
return 1 / (1 + Math.exp(-z));
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
function modelProbability(x, b0, b1) {
|
| 39 |
+
return logistic(b0 + b1 * x);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
function xToCanvas(x) {
|
| 43 |
+
const w = canvas.width - MARGIN.left - MARGIN.right;
|
| 44 |
+
return MARGIN.left + ((x - X_MIN) / (X_MAX - X_MIN)) * w;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
function yToCanvas(y) {
|
| 48 |
+
const h = canvas.height - MARGIN.top - MARGIN.bottom;
|
| 49 |
+
return canvas.height - MARGIN.bottom - y * h;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
function canvasToX(pixelX) {
|
| 53 |
+
const w = canvas.width - MARGIN.left - MARGIN.right;
|
| 54 |
+
return X_MIN + ((pixelX - MARGIN.left) / w) * (X_MAX - X_MIN);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
function format(num, digits = 3) {
|
| 58 |
+
return Number(num).toFixed(digits);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
function drawAxes(showGrid) {
|
| 62 |
+
const plotLeft = MARGIN.left;
|
| 63 |
+
const plotRight = canvas.width - MARGIN.right;
|
| 64 |
+
const plotTop = MARGIN.top;
|
| 65 |
+
const plotBottom = canvas.height - MARGIN.bottom;
|
| 66 |
+
|
| 67 |
+
ctx.strokeStyle = "#c9d9df";
|
| 68 |
+
ctx.lineWidth = 1;
|
| 69 |
+
|
| 70 |
+
if (showGrid) {
|
| 71 |
+
for (let x = -10; x <= 10; x += 2) {
|
| 72 |
+
const px = xToCanvas(x);
|
| 73 |
+
ctx.beginPath();
|
| 74 |
+
ctx.moveTo(px, plotTop);
|
| 75 |
+
ctx.lineTo(px, plotBottom);
|
| 76 |
+
ctx.stroke();
|
| 77 |
+
}
|
| 78 |
+
for (let y = 0; y <= 1.001; y += 0.1) {
|
| 79 |
+
const py = yToCanvas(y);
|
| 80 |
+
ctx.beginPath();
|
| 81 |
+
ctx.moveTo(plotLeft, py);
|
| 82 |
+
ctx.lineTo(plotRight, py);
|
| 83 |
+
ctx.stroke();
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
ctx.strokeStyle = "#14343f";
|
| 88 |
+
ctx.lineWidth = 1.4;
|
| 89 |
+
|
| 90 |
+
ctx.beginPath();
|
| 91 |
+
ctx.moveTo(plotLeft, yToCanvas(0));
|
| 92 |
+
ctx.lineTo(plotRight, yToCanvas(0));
|
| 93 |
+
ctx.stroke();
|
| 94 |
+
|
| 95 |
+
ctx.beginPath();
|
| 96 |
+
ctx.moveTo(xToCanvas(0), plotTop);
|
| 97 |
+
ctx.lineTo(xToCanvas(0), plotBottom);
|
| 98 |
+
ctx.stroke();
|
| 99 |
+
|
| 100 |
+
ctx.fillStyle = "#14343f";
|
| 101 |
+
ctx.font = "13px 'Avenir Next', sans-serif";
|
| 102 |
+
|
| 103 |
+
for (let x = -10; x <= 10; x += 2) {
|
| 104 |
+
const px = xToCanvas(x);
|
| 105 |
+
ctx.fillText(String(x), px - 8, yToCanvas(0) + 19);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
for (let y = 0; y <= 1.001; y += 0.2) {
|
| 109 |
+
const py = yToCanvas(y);
|
| 110 |
+
ctx.fillText(format(y, 1), plotLeft - 40, py + 4);
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
ctx.fillText("x", plotRight - 12, yToCanvas(0) + 40);
|
| 114 |
+
ctx.fillText("p", xToCanvas(0) + 12, plotTop + 12);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
function drawThresholdLine(t) {
|
| 118 |
+
ctx.strokeStyle = "#d8534f";
|
| 119 |
+
ctx.lineWidth = 1.3;
|
| 120 |
+
ctx.setLineDash([7, 5]);
|
| 121 |
+
ctx.beginPath();
|
| 122 |
+
ctx.moveTo(MARGIN.left, yToCanvas(t));
|
| 123 |
+
ctx.lineTo(canvas.width - MARGIN.right, yToCanvas(t));
|
| 124 |
+
ctx.stroke();
|
| 125 |
+
ctx.setLineDash([]);
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
function drawRegionShade(b0, b1, t) {
|
| 129 |
+
if (Math.abs(b1) < 1e-9) {
|
| 130 |
+
return;
|
| 131 |
+
}
|
| 132 |
+
const logit = Math.log(t / (1 - t));
|
| 133 |
+
const boundaryX = (logit - b0) / b1;
|
| 134 |
+
|
| 135 |
+
const left = MARGIN.left;
|
| 136 |
+
const right = canvas.width - MARGIN.right;
|
| 137 |
+
const top = MARGIN.top;
|
| 138 |
+
const bottom = canvas.height - MARGIN.bottom;
|
| 139 |
+
const boundaryPx = xToCanvas(Math.max(X_MIN, Math.min(X_MAX, boundaryX)));
|
| 140 |
+
|
| 141 |
+
ctx.globalAlpha = 0.15;
|
| 142 |
+
ctx.fillStyle = "#0a8f7b";
|
| 143 |
+
|
| 144 |
+
if (b1 > 0) {
|
| 145 |
+
ctx.fillRect(boundaryPx, top, right - boundaryPx, bottom - top);
|
| 146 |
+
} else {
|
| 147 |
+
ctx.fillRect(left, top, boundaryPx - left, bottom - top);
|
| 148 |
+
}
|
| 149 |
+
ctx.globalAlpha = 1;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
function drawCurve(b0, b1) {
|
| 153 |
+
ctx.strokeStyle = "#0a8f7b";
|
| 154 |
+
ctx.lineWidth = 3;
|
| 155 |
+
ctx.beginPath();
|
| 156 |
+
|
| 157 |
+
const steps = 600;
|
| 158 |
+
for (let i = 0; i <= steps; i += 1) {
|
| 159 |
+
const x = X_MIN + (i / steps) * (X_MAX - X_MIN);
|
| 160 |
+
const y = modelProbability(x, b0, b1);
|
| 161 |
+
const px = xToCanvas(x);
|
| 162 |
+
const py = yToCanvas(y);
|
| 163 |
+
|
| 164 |
+
if (i === 0) {
|
| 165 |
+
ctx.moveTo(px, py);
|
| 166 |
+
} else {
|
| 167 |
+
ctx.lineTo(px, py);
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
ctx.stroke();
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
function drawDerivative(b0, b1) {
|
| 175 |
+
ctx.strokeStyle = "#f2b84b";
|
| 176 |
+
ctx.lineWidth = 2;
|
| 177 |
+
ctx.setLineDash([6, 4]);
|
| 178 |
+
ctx.beginPath();
|
| 179 |
+
|
| 180 |
+
const steps = 600;
|
| 181 |
+
for (let i = 0; i <= steps; i += 1) {
|
| 182 |
+
const x = X_MIN + (i / steps) * (X_MAX - X_MIN);
|
| 183 |
+
const p = modelProbability(x, b0, b1);
|
| 184 |
+
const derivative = Math.abs(b1 * p * (1 - p));
|
| 185 |
+
const y = Math.min(1, derivative * 4);
|
| 186 |
+
|
| 187 |
+
const px = xToCanvas(x);
|
| 188 |
+
const py = yToCanvas(y);
|
| 189 |
+
|
| 190 |
+
if (i === 0) {
|
| 191 |
+
ctx.moveTo(px, py);
|
| 192 |
+
} else {
|
| 193 |
+
ctx.lineTo(px, py);
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
ctx.stroke();
|
| 198 |
+
ctx.setLineDash([]);
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
function drawProbeAndSamples(b0, b1, t, probeX) {
|
| 202 |
+
const probeP = modelProbability(probeX, b0, b1);
|
| 203 |
+
|
| 204 |
+
for (const sampleX of points) {
|
| 205 |
+
const p = modelProbability(sampleX, b0, b1);
|
| 206 |
+
const positive = p >= t;
|
| 207 |
+
ctx.fillStyle = positive ? "#0a8f7b" : "#d8534f";
|
| 208 |
+
ctx.beginPath();
|
| 209 |
+
ctx.arc(xToCanvas(sampleX), yToCanvas(p), 5, 0, Math.PI * 2);
|
| 210 |
+
ctx.fill();
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
ctx.strokeStyle = "#14343f";
|
| 214 |
+
ctx.setLineDash([4, 4]);
|
| 215 |
+
ctx.beginPath();
|
| 216 |
+
ctx.moveTo(xToCanvas(probeX), yToCanvas(0));
|
| 217 |
+
ctx.lineTo(xToCanvas(probeX), yToCanvas(probeP));
|
| 218 |
+
ctx.stroke();
|
| 219 |
+
ctx.setLineDash([]);
|
| 220 |
+
|
| 221 |
+
ctx.fillStyle = "#14343f";
|
| 222 |
+
ctx.beginPath();
|
| 223 |
+
ctx.arc(xToCanvas(probeX), yToCanvas(probeP), 6, 0, Math.PI * 2);
|
| 224 |
+
ctx.fill();
|
| 225 |
+
|
| 226 |
+
ctx.font = "12px 'Avenir Next', sans-serif";
|
| 227 |
+
ctx.fillText(`probe p=${format(probeP)}`, xToCanvas(probeX) + 8, yToCanvas(probeP) - 8);
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
function renderStats(b0, b1, t, probeX) {
|
| 231 |
+
const probeP = modelProbability(probeX, b0, b1);
|
| 232 |
+
|
| 233 |
+
let boundaryText = "No finite boundary (b1≈0)";
|
| 234 |
+
if (Math.abs(b1) > 1e-9) {
|
| 235 |
+
const logit = Math.log(t / (1 - t));
|
| 236 |
+
const boundaryX = (logit - b0) / b1;
|
| 237 |
+
boundaryText = `Decision boundary x*: ${format(boundaryX)}`;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
const positives = points.filter((x) => modelProbability(x, b0, b1) >= t).length;
|
| 241 |
+
const negatives = points.length - positives;
|
| 242 |
+
|
| 243 |
+
stats.innerHTML = `
|
| 244 |
+
<span>Equation: p = 1/(1+exp(-(${format(b0)} + ${format(b1)}x)))</span>
|
| 245 |
+
<span>Threshold t = ${format(t, 2)}</span>
|
| 246 |
+
<span>Probe: x = ${format(probeX, 2)}, p = ${format(probeP)}</span>
|
| 247 |
+
<span>${boundaryText}</span>
|
| 248 |
+
<span>Sample points: ${points.length} total</span>
|
| 249 |
+
<span>Class counts by t: ${positives} positive, ${negatives} negative</span>
|
| 250 |
+
`;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
function updateLabelValues() {
|
| 254 |
+
valueLabels.b0.textContent = format(controls.b0.value, 2);
|
| 255 |
+
valueLabels.b1.textContent = format(controls.b1.value, 2);
|
| 256 |
+
valueLabels.t.textContent = format(controls.t.value, 2);
|
| 257 |
+
valueLabels.probeX.textContent = format(controls.probeX.value, 2);
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
function render() {
|
| 261 |
+
const b0 = Number(controls.b0.value);
|
| 262 |
+
const b1 = Number(controls.b1.value);
|
| 263 |
+
const t = Number(controls.t.value);
|
| 264 |
+
const probeX = Number(controls.probeX.value);
|
| 265 |
+
|
| 266 |
+
updateLabelValues();
|
| 267 |
+
|
| 268 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 269 |
+
drawAxes(controls.showGrid.checked);
|
| 270 |
+
|
| 271 |
+
if (controls.showShade.checked) {
|
| 272 |
+
drawRegionShade(b0, b1, t);
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
drawThresholdLine(t);
|
| 276 |
+
drawCurve(b0, b1);
|
| 277 |
+
|
| 278 |
+
if (controls.showDerivative.checked) {
|
| 279 |
+
drawDerivative(b0, b1);
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
drawProbeAndSamples(b0, b1, t, probeX);
|
| 283 |
+
renderStats(b0, b1, t, probeX);
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
function reset() {
|
| 287 |
+
controls.b0.value = 0;
|
| 288 |
+
controls.b1.value = 1;
|
| 289 |
+
controls.t.value = 0.5;
|
| 290 |
+
controls.probeX.value = 0;
|
| 291 |
+
controls.showGrid.checked = true;
|
| 292 |
+
controls.showShade.checked = true;
|
| 293 |
+
controls.showDerivative.checked = false;
|
| 294 |
+
controls.preset.value = "default";
|
| 295 |
+
points.length = 0;
|
| 296 |
+
stopAnimation();
|
| 297 |
+
render();
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
function applyPreset(presetKey) {
|
| 301 |
+
const presetMap = {
|
| 302 |
+
default: { b0: 0, b1: 1, t: 0.5 },
|
| 303 |
+
steep: { b0: -1, b1: 3, t: 0.5 },
|
| 304 |
+
reversed: { b0: 0.5, b1: -2.2, t: 0.5 },
|
| 305 |
+
"high-threshold": { b0: 0, b1: 1, t: 0.8 },
|
| 306 |
+
};
|
| 307 |
+
|
| 308 |
+
const preset = presetMap[presetKey] || presetMap.default;
|
| 309 |
+
controls.b0.value = preset.b0;
|
| 310 |
+
controls.b1.value = preset.b1;
|
| 311 |
+
controls.t.value = preset.t;
|
| 312 |
+
render();
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
function startAnimation() {
|
| 316 |
+
if (animationTimer) return;
|
| 317 |
+
|
| 318 |
+
controls.animateBtn.textContent = "Stop Animation";
|
| 319 |
+
|
| 320 |
+
animationTimer = setInterval(() => {
|
| 321 |
+
const current = Number(controls.probeX.value);
|
| 322 |
+
let next = current + animationDirection * 0.18;
|
| 323 |
+
|
| 324 |
+
if (next > X_MAX) {
|
| 325 |
+
next = X_MAX;
|
| 326 |
+
animationDirection = -1;
|
| 327 |
+
}
|
| 328 |
+
if (next < X_MIN) {
|
| 329 |
+
next = X_MIN;
|
| 330 |
+
animationDirection = 1;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
controls.probeX.value = next;
|
| 334 |
+
render();
|
| 335 |
+
}, 30);
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
function stopAnimation() {
|
| 339 |
+
if (animationTimer) {
|
| 340 |
+
clearInterval(animationTimer);
|
| 341 |
+
animationTimer = null;
|
| 342 |
+
}
|
| 343 |
+
controls.animateBtn.textContent = "Animate Probe";
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
for (const key of ["b0", "b1", "t", "probeX", "showGrid", "showShade", "showDerivative"]) {
|
| 347 |
+
controls[key].addEventListener("input", render);
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
controls.resetBtn.addEventListener("click", reset);
|
| 351 |
+
|
| 352 |
+
controls.preset.addEventListener("change", (event) => {
|
| 353 |
+
applyPreset(event.target.value);
|
| 354 |
+
});
|
| 355 |
+
|
| 356 |
+
controls.animateBtn.addEventListener("click", () => {
|
| 357 |
+
if (animationTimer) {
|
| 358 |
+
stopAnimation();
|
| 359 |
+
} else {
|
| 360 |
+
startAnimation();
|
| 361 |
+
}
|
| 362 |
+
});
|
| 363 |
+
|
| 364 |
+
canvas.addEventListener("click", (event) => {
|
| 365 |
+
const rect = canvas.getBoundingClientRect();
|
| 366 |
+
const clickX = event.clientX - rect.left;
|
| 367 |
+
const x = canvasToX((clickX / rect.width) * canvas.width);
|
| 368 |
+
|
| 369 |
+
if (x < X_MIN || x > X_MAX) {
|
| 370 |
+
return;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
points.push(Number(x.toFixed(2)));
|
| 374 |
+
if (points.length > 30) {
|
| 375 |
+
points.shift();
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
render();
|
| 379 |
+
});
|
| 380 |
+
|
| 381 |
+
reset();
|
logistic-regression/confusion-matrix.html
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>DDW Interface - Confusion Matrix Practice</title>
|
| 7 |
+
<link rel="stylesheet" href="styles.css" />
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<header class="site-header">
|
| 11 |
+
<div>
|
| 12 |
+
<p class="kicker">Data Driven World</p>
|
| 13 |
+
<h1>Confusion Matrix Practice (Page 3)</h1>
|
| 14 |
+
</div>
|
| 15 |
+
<nav>
|
| 16 |
+
<a href="index.html">Home</a>
|
| 17 |
+
<a href="simple-sigmoid.html">Simple Sigmoid</a>
|
| 18 |
+
<a class="active" href="confusion-matrix.html">Confusion Matrix (Page 3)</a>
|
| 19 |
+
<a href="sigmoid.html">Sigmoid Function</a>
|
| 20 |
+
<a href="cost-visualization.html">Cost Function</a>
|
| 21 |
+
<a href="about.html">Notes</a>
|
| 22 |
+
</nav>
|
| 23 |
+
</header>
|
| 24 |
+
|
| 25 |
+
<main class="layout">
|
| 26 |
+
<section class="panel controls">
|
| 27 |
+
<h2>Exercise Setup</h2>
|
| 28 |
+
|
| 29 |
+
<label for="classMode">Classification type</label>
|
| 30 |
+
<select id="classMode">
|
| 31 |
+
<option value="binary">2 classes (Positive / Negative)</option>
|
| 32 |
+
<option value="three">3 classes (A / B / C)</option>
|
| 33 |
+
</select>
|
| 34 |
+
|
| 35 |
+
<label for="threshold">Threshold t: <span id="thresholdValue">0.50</span></label>
|
| 36 |
+
<input id="threshold" type="range" min="0.30" max="0.80" step="0.01" value="0.50" />
|
| 37 |
+
|
| 38 |
+
<p class="hint" id="ruleText"></p>
|
| 39 |
+
|
| 40 |
+
<div class="button-row">
|
| 41 |
+
<button id="checkBtn" type="button">Check My Answers</button>
|
| 42 |
+
<button id="showSolutionBtn" type="button">Show Full Solution</button>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<div class="button-row">
|
| 46 |
+
<button id="newSamplesBtn" type="button">Generate New Samples</button>
|
| 47 |
+
<button id="resetBtn" type="button">Reset Inputs</button>
|
| 48 |
+
</div>
|
| 49 |
+
|
| 50 |
+
<div class="stats" id="feedbackStats"></div>
|
| 51 |
+
</section>
|
| 52 |
+
|
| 53 |
+
<section class="panel chart-panel confusion-panel">
|
| 54 |
+
<h2>Student Task</h2>
|
| 55 |
+
<p class="hint">
|
| 56 |
+
Six samples are given. Prediction for Sample 1 and 2 are worked examples. You calculate Sample 3 to 6, then
|
| 57 |
+
fill confusion matrix and metrics.
|
| 58 |
+
</p>
|
| 59 |
+
|
| 60 |
+
<div id="sampleArea"></div>
|
| 61 |
+
<div id="matrixArea"></div>
|
| 62 |
+
<div id="metricArea"></div>
|
| 63 |
+
|
| 64 |
+
<section class="solution-block" id="solutionBlock" hidden>
|
| 65 |
+
<h3>Detailed Process and Solutions</h3>
|
| 66 |
+
<div id="solutionText"></div>
|
| 67 |
+
</section>
|
| 68 |
+
</section>
|
| 69 |
+
</main>
|
| 70 |
+
|
| 71 |
+
<script src="confusion.js"></script>
|
| 72 |
+
</body>
|
| 73 |
+
</html>
|
logistic-regression/confusion.js
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const controls = {
|
| 2 |
+
classMode: document.getElementById("classMode"),
|
| 3 |
+
threshold: document.getElementById("threshold"),
|
| 4 |
+
thresholdValue: document.getElementById("thresholdValue"),
|
| 5 |
+
ruleText: document.getElementById("ruleText"),
|
| 6 |
+
checkBtn: document.getElementById("checkBtn"),
|
| 7 |
+
showSolutionBtn: document.getElementById("showSolutionBtn"),
|
| 8 |
+
newSamplesBtn: document.getElementById("newSamplesBtn"),
|
| 9 |
+
resetBtn: document.getElementById("resetBtn"),
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
const sampleArea = document.getElementById("sampleArea");
|
| 13 |
+
const matrixArea = document.getElementById("matrixArea");
|
| 14 |
+
const metricArea = document.getElementById("metricArea");
|
| 15 |
+
const feedbackStats = document.getElementById("feedbackStats");
|
| 16 |
+
const solutionBlock = document.getElementById("solutionBlock");
|
| 17 |
+
const solutionText = document.getElementById("solutionText");
|
| 18 |
+
|
| 19 |
+
let binarySamples = [];
|
| 20 |
+
let threeClassSamples = [];
|
| 21 |
+
|
| 22 |
+
function fmt(num, digits = 2) {
|
| 23 |
+
return Number(num).toFixed(digits);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
function round3(num) {
|
| 27 |
+
return Number(num).toFixed(3);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
function round2(num) {
|
| 31 |
+
return Math.round(num * 100) / 100;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
function rand(min, max) {
|
| 35 |
+
return min + Math.random() * (max - min);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
function generateBinarySamples() {
|
| 39 |
+
const actualPattern = ["Positive", "Negative", "Positive", "Negative", "Positive", "Negative"];
|
| 40 |
+
const samples = [];
|
| 41 |
+
|
| 42 |
+
for (let i = 0; i < 6; i += 1) {
|
| 43 |
+
const actual = actualPattern[i];
|
| 44 |
+
const feature = round2(rand(0.2, 2.6));
|
| 45 |
+
const pPos = actual === "Positive" ? round2(rand(0.38, 0.92)) : round2(rand(0.12, 0.78));
|
| 46 |
+
samples.push({ id: `S${i + 1}`, feature, actual, pPos });
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
binarySamples = samples;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
function generateThreeProbabilities(actual) {
|
| 53 |
+
const labels = ["A", "B", "C"];
|
| 54 |
+
const weights = {
|
| 55 |
+
A: rand(0.1, 0.45),
|
| 56 |
+
B: rand(0.1, 0.45),
|
| 57 |
+
C: rand(0.1, 0.45),
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
weights[actual] += rand(0.18, 0.5);
|
| 61 |
+
const total = labels.reduce((sum, label) => sum + weights[label], 0);
|
| 62 |
+
|
| 63 |
+
let pA = round2(weights.A / total);
|
| 64 |
+
let pB = round2(weights.B / total);
|
| 65 |
+
if (pA + pB > 0.98) {
|
| 66 |
+
const scale = 0.98 / (pA + pB);
|
| 67 |
+
pA = round2(pA * scale);
|
| 68 |
+
pB = round2(pB * scale);
|
| 69 |
+
}
|
| 70 |
+
const pC = round2(1 - pA - pB);
|
| 71 |
+
|
| 72 |
+
return { pA, pB, pC };
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
function generateThreeClassSamples() {
|
| 76 |
+
const actualPattern = ["A", "B", "C", "B", "C", "A"];
|
| 77 |
+
const samples = [];
|
| 78 |
+
|
| 79 |
+
for (let i = 0; i < 6; i += 1) {
|
| 80 |
+
const actual = actualPattern[i];
|
| 81 |
+
const feature = round2(rand(0.5, 2.4));
|
| 82 |
+
const probs = generateThreeProbabilities(actual);
|
| 83 |
+
samples.push({ id: `S${i + 1}`, feature, actual, ...probs });
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
threeClassSamples = samples;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
function generateAllSamples() {
|
| 90 |
+
generateBinarySamples();
|
| 91 |
+
generateThreeClassSamples();
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
function computeBinaryPrediction(sample, threshold) {
|
| 95 |
+
return sample.pPos >= threshold ? "Positive" : "Negative";
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
function computeThreePrediction(sample, threshold) {
|
| 99 |
+
if (sample.pA >= threshold) return "A";
|
| 100 |
+
if (sample.pB >= threshold) return "B";
|
| 101 |
+
return "C";
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
function computeBinaryResults(threshold) {
|
| 105 |
+
const rows = binarySamples.map((sample) => {
|
| 106 |
+
const predicted = computeBinaryPrediction(sample, threshold);
|
| 107 |
+
return { ...sample, predicted };
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
let tp = 0;
|
| 111 |
+
let fp = 0;
|
| 112 |
+
let tn = 0;
|
| 113 |
+
let fn = 0;
|
| 114 |
+
|
| 115 |
+
for (const row of rows) {
|
| 116 |
+
if (row.actual === "Positive" && row.predicted === "Positive") tp += 1;
|
| 117 |
+
if (row.actual === "Negative" && row.predicted === "Positive") fp += 1;
|
| 118 |
+
if (row.actual === "Negative" && row.predicted === "Negative") tn += 1;
|
| 119 |
+
if (row.actual === "Positive" && row.predicted === "Negative") fn += 1;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
const precision = tp + fp === 0 ? 0 : tp / (tp + fp);
|
| 123 |
+
const recall = tp + fn === 0 ? 0 : tp / (tp + fn);
|
| 124 |
+
const sensitivity = recall;
|
| 125 |
+
const specificity = tn + fp === 0 ? 0 : tn / (tn + fp);
|
| 126 |
+
|
| 127 |
+
return {
|
| 128 |
+
rows,
|
| 129 |
+
matrix: {
|
| 130 |
+
Positive: { Positive: tp, Negative: fn },
|
| 131 |
+
Negative: { Positive: fp, Negative: tn },
|
| 132 |
+
},
|
| 133 |
+
metrics: { precision, recall, sensitivity, specificity },
|
| 134 |
+
};
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
function getOneVsRestMetrics(matrix, className) {
|
| 138 |
+
const classes = ["A", "B", "C"];
|
| 139 |
+
const tp = matrix[className][className];
|
| 140 |
+
|
| 141 |
+
let fp = 0;
|
| 142 |
+
let fn = 0;
|
| 143 |
+
for (const c of classes) {
|
| 144 |
+
if (c !== className) {
|
| 145 |
+
fp += matrix[c][className];
|
| 146 |
+
fn += matrix[className][c];
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
const total = classes.reduce((sum, r) => sum + classes.reduce((acc, c) => acc + matrix[r][c], 0), 0);
|
| 151 |
+
const tn = total - tp - fp - fn;
|
| 152 |
+
|
| 153 |
+
const precision = tp + fp === 0 ? 0 : tp / (tp + fp);
|
| 154 |
+
const recall = tp + fn === 0 ? 0 : tp / (tp + fn);
|
| 155 |
+
const specificity = tn + fp === 0 ? 0 : tn / (tn + fp);
|
| 156 |
+
|
| 157 |
+
return { precision, recall, sensitivity: recall, specificity };
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
function computeThreeResults(threshold) {
|
| 161 |
+
const rows = threeClassSamples.map((sample) => {
|
| 162 |
+
const predicted = computeThreePrediction(sample, threshold);
|
| 163 |
+
return { ...sample, predicted };
|
| 164 |
+
});
|
| 165 |
+
|
| 166 |
+
const classes = ["A", "B", "C"];
|
| 167 |
+
const matrix = { A: { A: 0, B: 0, C: 0 }, B: { A: 0, B: 0, C: 0 }, C: { A: 0, B: 0, C: 0 } };
|
| 168 |
+
|
| 169 |
+
for (const row of rows) {
|
| 170 |
+
matrix[row.actual][row.predicted] += 1;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
const perClass = classes.map((className) => getOneVsRestMetrics(matrix, className));
|
| 174 |
+
const macro = {
|
| 175 |
+
precision: perClass.reduce((s, m) => s + m.precision, 0) / classes.length,
|
| 176 |
+
recall: perClass.reduce((s, m) => s + m.recall, 0) / classes.length,
|
| 177 |
+
sensitivity: perClass.reduce((s, m) => s + m.sensitivity, 0) / classes.length,
|
| 178 |
+
specificity: perClass.reduce((s, m) => s + m.specificity, 0) / classes.length,
|
| 179 |
+
};
|
| 180 |
+
|
| 181 |
+
return { rows, matrix, metrics: macro, perClass };
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
function getCurrentResults() {
|
| 185 |
+
const mode = controls.classMode.value;
|
| 186 |
+
const t = Number(controls.threshold.value);
|
| 187 |
+
return mode === "binary" ? computeBinaryResults(t) : computeThreeResults(t);
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
function samplePredictionSelect(rowId, options) {
|
| 191 |
+
return `<select data-role="sample-pred" data-id="${rowId}">${options
|
| 192 |
+
.map((opt) => `<option value="${opt}">${opt}</option>`)
|
| 193 |
+
.join("")}</select>`;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
function renderSamples(results) {
|
| 197 |
+
const mode = controls.classMode.value;
|
| 198 |
+
const rows = results.rows;
|
| 199 |
+
|
| 200 |
+
if (mode === "binary") {
|
| 201 |
+
sampleArea.innerHTML = `
|
| 202 |
+
<h3>1) Predict the class for each sample</h3>
|
| 203 |
+
<table class="exercise-table">
|
| 204 |
+
<thead>
|
| 205 |
+
<tr>
|
| 206 |
+
<th>Sample</th>
|
| 207 |
+
<th>Feature x</th>
|
| 208 |
+
<th>Actual Class</th>
|
| 209 |
+
<th>p(Positive)</th>
|
| 210 |
+
<th>Your Predicted Class</th>
|
| 211 |
+
</tr>
|
| 212 |
+
</thead>
|
| 213 |
+
<tbody>
|
| 214 |
+
${rows
|
| 215 |
+
.map((row, index) => {
|
| 216 |
+
if (index < 2) {
|
| 217 |
+
return `
|
| 218 |
+
<tr>
|
| 219 |
+
<td>${row.id}</td>
|
| 220 |
+
<td>${fmt(row.feature)}</td>
|
| 221 |
+
<td>${row.actual}</td>
|
| 222 |
+
<td>${fmt(row.pPos)}</td>
|
| 223 |
+
<td><strong>${row.predicted}</strong> <span class="worked-tag">(worked example)</span></td>
|
| 224 |
+
</tr>
|
| 225 |
+
`;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
return `
|
| 229 |
+
<tr>
|
| 230 |
+
<td>${row.id}</td>
|
| 231 |
+
<td>${fmt(row.feature)}</td>
|
| 232 |
+
<td>${row.actual}</td>
|
| 233 |
+
<td>${fmt(row.pPos)}</td>
|
| 234 |
+
<td>${samplePredictionSelect(row.id, ["Positive", "Negative"])}</td>
|
| 235 |
+
</tr>
|
| 236 |
+
`;
|
| 237 |
+
})
|
| 238 |
+
.join("")}
|
| 239 |
+
</tbody>
|
| 240 |
+
</table>
|
| 241 |
+
`;
|
| 242 |
+
return;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
sampleArea.innerHTML = `
|
| 246 |
+
<h3>1) Predict the class for each sample</h3>
|
| 247 |
+
<table class="exercise-table">
|
| 248 |
+
<thead>
|
| 249 |
+
<tr>
|
| 250 |
+
<th>Sample</th>
|
| 251 |
+
<th>Feature x</th>
|
| 252 |
+
<th>Actual Class</th>
|
| 253 |
+
<th>p(A)</th>
|
| 254 |
+
<th>p(B)</th>
|
| 255 |
+
<th>p(C)</th>
|
| 256 |
+
<th>Your Predicted Class</th>
|
| 257 |
+
</tr>
|
| 258 |
+
</thead>
|
| 259 |
+
<tbody>
|
| 260 |
+
${rows
|
| 261 |
+
.map((row, index) => {
|
| 262 |
+
if (index < 2) {
|
| 263 |
+
return `
|
| 264 |
+
<tr>
|
| 265 |
+
<td>${row.id}</td>
|
| 266 |
+
<td>${fmt(row.feature)}</td>
|
| 267 |
+
<td>${row.actual}</td>
|
| 268 |
+
<td>${fmt(row.pA)}</td>
|
| 269 |
+
<td>${fmt(row.pB)}</td>
|
| 270 |
+
<td>${fmt(row.pC)}</td>
|
| 271 |
+
<td><strong>${row.predicted}</strong> <span class="worked-tag">(worked example)</span></td>
|
| 272 |
+
</tr>
|
| 273 |
+
`;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
return `
|
| 277 |
+
<tr>
|
| 278 |
+
<td>${row.id}</td>
|
| 279 |
+
<td>${fmt(row.feature)}</td>
|
| 280 |
+
<td>${row.actual}</td>
|
| 281 |
+
<td>${fmt(row.pA)}</td>
|
| 282 |
+
<td>${fmt(row.pB)}</td>
|
| 283 |
+
<td>${fmt(row.pC)}</td>
|
| 284 |
+
<td>${samplePredictionSelect(row.id, ["A", "B", "C"])}</td>
|
| 285 |
+
</tr>
|
| 286 |
+
`;
|
| 287 |
+
})
|
| 288 |
+
.join("")}
|
| 289 |
+
</tbody>
|
| 290 |
+
</table>
|
| 291 |
+
`;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
function renderMatrixInputs() {
|
| 295 |
+
const mode = controls.classMode.value;
|
| 296 |
+
|
| 297 |
+
if (mode === "binary") {
|
| 298 |
+
matrixArea.innerHTML = `
|
| 299 |
+
<h3>2) Fill the confusion matrix</h3>
|
| 300 |
+
<p class="hint">Rows = Actual class, Columns = Predicted class.</p>
|
| 301 |
+
<table class="exercise-table matrix-table">
|
| 302 |
+
<thead>
|
| 303 |
+
<tr><th>Actual \ Predicted</th><th>Positive</th><th>Negative</th></tr>
|
| 304 |
+
</thead>
|
| 305 |
+
<tbody>
|
| 306 |
+
<tr>
|
| 307 |
+
<th>Positive</th>
|
| 308 |
+
<td><input type="number" min="0" data-role="matrix" data-key="Positive-Positive" /></td>
|
| 309 |
+
<td><input type="number" min="0" data-role="matrix" data-key="Positive-Negative" /></td>
|
| 310 |
+
</tr>
|
| 311 |
+
<tr>
|
| 312 |
+
<th>Negative</th>
|
| 313 |
+
<td><input type="number" min="0" data-role="matrix" data-key="Negative-Positive" /></td>
|
| 314 |
+
<td><input type="number" min="0" data-role="matrix" data-key="Negative-Negative" /></td>
|
| 315 |
+
</tr>
|
| 316 |
+
</tbody>
|
| 317 |
+
</table>
|
| 318 |
+
`;
|
| 319 |
+
return;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
matrixArea.innerHTML = `
|
| 323 |
+
<h3>2) Fill the confusion matrix</h3>
|
| 324 |
+
<p class="hint">Rows = Actual class, Columns = Predicted class.</p>
|
| 325 |
+
<table class="exercise-table matrix-table">
|
| 326 |
+
<thead>
|
| 327 |
+
<tr><th>Actual \ Predicted</th><th>A</th><th>B</th><th>C</th></tr>
|
| 328 |
+
</thead>
|
| 329 |
+
<tbody>
|
| 330 |
+
<tr>
|
| 331 |
+
<th>A</th>
|
| 332 |
+
<td><input type="number" min="0" data-role="matrix" data-key="A-A" /></td>
|
| 333 |
+
<td><input type="number" min="0" data-role="matrix" data-key="A-B" /></td>
|
| 334 |
+
<td><input type="number" min="0" data-role="matrix" data-key="A-C" /></td>
|
| 335 |
+
</tr>
|
| 336 |
+
<tr>
|
| 337 |
+
<th>B</th>
|
| 338 |
+
<td><input type="number" min="0" data-role="matrix" data-key="B-A" /></td>
|
| 339 |
+
<td><input type="number" min="0" data-role="matrix" data-key="B-B" /></td>
|
| 340 |
+
<td><input type="number" min="0" data-role="matrix" data-key="B-C" /></td>
|
| 341 |
+
</tr>
|
| 342 |
+
<tr>
|
| 343 |
+
<th>C</th>
|
| 344 |
+
<td><input type="number" min="0" data-role="matrix" data-key="C-A" /></td>
|
| 345 |
+
<td><input type="number" min="0" data-role="matrix" data-key="C-B" /></td>
|
| 346 |
+
<td><input type="number" min="0" data-role="matrix" data-key="C-C" /></td>
|
| 347 |
+
</tr>
|
| 348 |
+
</tbody>
|
| 349 |
+
</table>
|
| 350 |
+
`;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
function renderMetricInputs() {
|
| 354 |
+
const mode = controls.classMode.value;
|
| 355 |
+
const detail = mode === "binary" ? "(for Positive class)" : "(macro-average across A, B, C)";
|
| 356 |
+
metricArea.innerHTML = `
|
| 357 |
+
<h3>3) Calculate metrics ${detail}</h3>
|
| 358 |
+
<p class="hint">Enter decimal values (e.g., 0.667).</p>
|
| 359 |
+
<table class="exercise-table metric-table">
|
| 360 |
+
<thead>
|
| 361 |
+
<tr><th>Metric</th><th>Your value</th></tr>
|
| 362 |
+
</thead>
|
| 363 |
+
<tbody>
|
| 364 |
+
<tr><th>Precision</th><td><input type="number" step="0.001" data-role="metric" data-key="precision" /></td></tr>
|
| 365 |
+
<tr><th>Recall</th><td><input type="number" step="0.001" data-role="metric" data-key="recall" /></td></tr>
|
| 366 |
+
<tr><th>Sensitivity</th><td><input type="number" step="0.001" data-role="metric" data-key="sensitivity" /></td></tr>
|
| 367 |
+
<tr><th>Specificity</th><td><input type="number" step="0.001" data-role="metric" data-key="specificity" /></td></tr>
|
| 368 |
+
</tbody>
|
| 369 |
+
</table>
|
| 370 |
+
`;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
function updateRuleText() {
|
| 374 |
+
const mode = controls.classMode.value;
|
| 375 |
+
const threshold = Number(controls.threshold.value);
|
| 376 |
+
|
| 377 |
+
controls.thresholdValue.textContent = fmt(threshold);
|
| 378 |
+
|
| 379 |
+
if (mode === "binary") {
|
| 380 |
+
controls.ruleText.textContent =
|
| 381 |
+
"Rule: predict Positive if p(Positive) >= t, otherwise predict Negative. (Default t = 0.50)";
|
| 382 |
+
} else {
|
| 383 |
+
controls.ruleText.textContent =
|
| 384 |
+
"Rule: check A first, then B. If p(A) >= t predict A; else if p(B) >= t predict B; otherwise predict C.";
|
| 385 |
+
}
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
function renderPage() {
|
| 389 |
+
const results = getCurrentResults();
|
| 390 |
+
updateRuleText();
|
| 391 |
+
renderSamples(results);
|
| 392 |
+
renderMatrixInputs();
|
| 393 |
+
renderMetricInputs();
|
| 394 |
+
feedbackStats.innerHTML = "";
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
function readUserSampleAnswers() {
|
| 398 |
+
const selects = [...document.querySelectorAll('select[data-role="sample-pred"]')];
|
| 399 |
+
const map = {};
|
| 400 |
+
for (const select of selects) {
|
| 401 |
+
map[select.dataset.id] = select.value;
|
| 402 |
+
}
|
| 403 |
+
return map;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
function readUserMatrix() {
|
| 407 |
+
const inputs = [...document.querySelectorAll('input[data-role="matrix"]')];
|
| 408 |
+
const matrix = {};
|
| 409 |
+
for (const input of inputs) {
|
| 410 |
+
matrix[input.dataset.key] = input.value === "" ? NaN : Number(input.value);
|
| 411 |
+
}
|
| 412 |
+
return matrix;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
function readUserMetrics() {
|
| 416 |
+
const inputs = [...document.querySelectorAll('input[data-role="metric"]')];
|
| 417 |
+
const metrics = {};
|
| 418 |
+
for (const input of inputs) {
|
| 419 |
+
metrics[input.dataset.key] = input.value === "" ? NaN : Number(input.value);
|
| 420 |
+
}
|
| 421 |
+
return metrics;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
function expectedMatrixFlat(results) {
|
| 425 |
+
const mode = controls.classMode.value;
|
| 426 |
+
if (mode === "binary") {
|
| 427 |
+
return {
|
| 428 |
+
"Positive-Positive": results.matrix.Positive.Positive,
|
| 429 |
+
"Positive-Negative": results.matrix.Positive.Negative,
|
| 430 |
+
"Negative-Positive": results.matrix.Negative.Positive,
|
| 431 |
+
"Negative-Negative": results.matrix.Negative.Negative,
|
| 432 |
+
};
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
return {
|
| 436 |
+
"A-A": results.matrix.A.A,
|
| 437 |
+
"A-B": results.matrix.A.B,
|
| 438 |
+
"A-C": results.matrix.A.C,
|
| 439 |
+
"B-A": results.matrix.B.A,
|
| 440 |
+
"B-B": results.matrix.B.B,
|
| 441 |
+
"B-C": results.matrix.B.C,
|
| 442 |
+
"C-A": results.matrix.C.A,
|
| 443 |
+
"C-B": results.matrix.C.B,
|
| 444 |
+
"C-C": results.matrix.C.C,
|
| 445 |
+
};
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
function approximatelyEqual(a, b, eps = 0.02) {
|
| 449 |
+
return Math.abs(a - b) <= eps;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
function checkAnswers() {
|
| 453 |
+
const results = getCurrentResults();
|
| 454 |
+
|
| 455 |
+
const expectedPreds = {};
|
| 456 |
+
for (const row of results.rows.slice(2)) {
|
| 457 |
+
expectedPreds[row.id] = row.predicted;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
const userPreds = readUserSampleAnswers();
|
| 461 |
+
let predCorrect = 0;
|
| 462 |
+
const predTotal = Object.keys(expectedPreds).length;
|
| 463 |
+
for (const id of Object.keys(expectedPreds)) {
|
| 464 |
+
if (userPreds[id] === expectedPreds[id]) {
|
| 465 |
+
predCorrect += 1;
|
| 466 |
+
}
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
const userMatrix = readUserMatrix();
|
| 470 |
+
const expectedMatrix = expectedMatrixFlat(results);
|
| 471 |
+
let matrixCorrect = 0;
|
| 472 |
+
const matrixTotal = Object.keys(expectedMatrix).length;
|
| 473 |
+
for (const key of Object.keys(expectedMatrix)) {
|
| 474 |
+
if (userMatrix[key] === expectedMatrix[key]) {
|
| 475 |
+
matrixCorrect += 1;
|
| 476 |
+
}
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
const userMetrics = readUserMetrics();
|
| 480 |
+
const expectedMetrics = results.metrics;
|
| 481 |
+
let metricCorrect = 0;
|
| 482 |
+
const metricTotal = 4;
|
| 483 |
+
for (const metricName of ["precision", "recall", "sensitivity", "specificity"]) {
|
| 484 |
+
if (approximatelyEqual(userMetrics[metricName], expectedMetrics[metricName])) {
|
| 485 |
+
metricCorrect += 1;
|
| 486 |
+
}
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
feedbackStats.innerHTML = `
|
| 490 |
+
<span>Prediction check: ${predCorrect}/${predTotal} correct</span>
|
| 491 |
+
<span>Confusion matrix check: ${matrixCorrect}/${matrixTotal} correct</span>
|
| 492 |
+
<span>Metric check: ${metricCorrect}/${metricTotal} correct (tolerance +/- 0.02)</span>
|
| 493 |
+
<span>Expected metrics: Precision=${round3(expectedMetrics.precision)}, Recall=${round3(
|
| 494 |
+
expectedMetrics.recall,
|
| 495 |
+
)}, Sensitivity=${round3(expectedMetrics.sensitivity)}, Specificity=${round3(expectedMetrics.specificity)}</span>
|
| 496 |
+
`;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
function renderBinarySolution(results) {
|
| 500 |
+
const steps = results.rows
|
| 501 |
+
.map((row) => {
|
| 502 |
+
return `<li>${row.id}: p(Positive)=${fmt(row.pPos)}; compare with t=${fmt(
|
| 503 |
+
controls.threshold.value,
|
| 504 |
+
)} => predicted <strong>${row.predicted}</strong>; actual ${row.actual}.</li>`;
|
| 505 |
+
})
|
| 506 |
+
.join("");
|
| 507 |
+
|
| 508 |
+
solutionText.innerHTML = `
|
| 509 |
+
<h4>Step A: Classify all 6 samples</h4>
|
| 510 |
+
<ol>${steps}</ol>
|
| 511 |
+
|
| 512 |
+
<h4>Step B: Build confusion matrix (rows=actual, columns=predicted)</h4>
|
| 513 |
+
<p>TP=${results.matrix.Positive.Positive}, FN=${results.matrix.Positive.Negative}, FP=${results.matrix.Negative.Positive}, TN=${results.matrix.Negative.Negative}</p>
|
| 514 |
+
|
| 515 |
+
<table class="exercise-table matrix-table">
|
| 516 |
+
<thead><tr><th>Actual \\ Predicted</th><th>Positive</th><th>Negative</th></tr></thead>
|
| 517 |
+
<tbody>
|
| 518 |
+
<tr><th>Positive</th><td>${results.matrix.Positive.Positive}</td><td>${results.matrix.Positive.Negative}</td></tr>
|
| 519 |
+
<tr><th>Negative</th><td>${results.matrix.Negative.Positive}</td><td>${results.matrix.Negative.Negative}</td></tr>
|
| 520 |
+
</tbody>
|
| 521 |
+
</table>
|
| 522 |
+
|
| 523 |
+
<h4>Step C: Metrics</h4>
|
| 524 |
+
<p>Precision = TP/(TP+FP) = ${results.matrix.Positive.Positive}/(${results.matrix.Positive.Positive}+${results.matrix.Negative.Positive}) = ${round3(
|
| 525 |
+
results.metrics.precision,
|
| 526 |
+
)}</p>
|
| 527 |
+
<p>Recall = TP/(TP+FN) = ${results.matrix.Positive.Positive}/(${results.matrix.Positive.Positive}+${results.matrix.Positive.Negative}) = ${round3(
|
| 528 |
+
results.metrics.recall,
|
| 529 |
+
)}</p>
|
| 530 |
+
<p>Sensitivity = Recall = ${round3(results.metrics.sensitivity)}</p>
|
| 531 |
+
<p>Specificity = TN/(TN+FP) = ${results.matrix.Negative.Negative}/(${results.matrix.Negative.Negative}+${results.matrix.Negative.Positive}) = ${round3(
|
| 532 |
+
results.metrics.specificity,
|
| 533 |
+
)}</p>
|
| 534 |
+
`;
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
function renderThreeSolution(results) {
|
| 538 |
+
const threshold = Number(controls.threshold.value);
|
| 539 |
+
const steps = results.rows
|
| 540 |
+
.map((row) => {
|
| 541 |
+
return `<li>${row.id}: p(A)=${fmt(row.pA)}, p(B)=${fmt(row.pB)}, p(C)=${fmt(
|
| 542 |
+
row.pC,
|
| 543 |
+
)}. Since t=${fmt(threshold)}, prediction is <strong>${row.predicted}</strong>; actual ${row.actual}.</li>`;
|
| 544 |
+
})
|
| 545 |
+
.join("");
|
| 546 |
+
|
| 547 |
+
const classes = ["A", "B", "C"];
|
| 548 |
+
const perClass = classes
|
| 549 |
+
.map((c, idx) => {
|
| 550 |
+
return `<li>Class ${c}: Precision=${round3(results.perClass[idx].precision)}, Recall/Sensitivity=${round3(
|
| 551 |
+
results.perClass[idx].recall,
|
| 552 |
+
)}, Specificity=${round3(results.perClass[idx].specificity)}</li>`;
|
| 553 |
+
})
|
| 554 |
+
.join("");
|
| 555 |
+
|
| 556 |
+
solutionText.innerHTML = `
|
| 557 |
+
<h4>Step A: Classify all 6 samples</h4>
|
| 558 |
+
<ol>${steps}</ol>
|
| 559 |
+
|
| 560 |
+
<h4>Step B: Build 3x3 confusion matrix (rows=actual, columns=predicted)</h4>
|
| 561 |
+
<table class="exercise-table matrix-table">
|
| 562 |
+
<thead><tr><th>Actual \\ Predicted</th><th>A</th><th>B</th><th>C</th></tr></thead>
|
| 563 |
+
<tbody>
|
| 564 |
+
<tr><th>A</th><td>${results.matrix.A.A}</td><td>${results.matrix.A.B}</td><td>${results.matrix.A.C}</td></tr>
|
| 565 |
+
<tr><th>B</th><td>${results.matrix.B.A}</td><td>${results.matrix.B.B}</td><td>${results.matrix.B.C}</td></tr>
|
| 566 |
+
<tr><th>C</th><td>${results.matrix.C.A}</td><td>${results.matrix.C.B}</td><td>${results.matrix.C.C}</td></tr>
|
| 567 |
+
</tbody>
|
| 568 |
+
</table>
|
| 569 |
+
|
| 570 |
+
<h4>Step C: One-vs-rest metrics for each class, then macro-average</h4>
|
| 571 |
+
<ol>${perClass}</ol>
|
| 572 |
+
|
| 573 |
+
<p><strong>Macro Precision</strong> = ${round3(results.metrics.precision)}</p>
|
| 574 |
+
<p><strong>Macro Recall</strong> = ${round3(results.metrics.recall)}</p>
|
| 575 |
+
<p><strong>Macro Sensitivity</strong> = ${round3(results.metrics.sensitivity)}</p>
|
| 576 |
+
<p><strong>Macro Specificity</strong> = ${round3(results.metrics.specificity)}</p>
|
| 577 |
+
`;
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
function showSolution() {
|
| 581 |
+
const results = getCurrentResults();
|
| 582 |
+
if (controls.classMode.value === "binary") {
|
| 583 |
+
renderBinarySolution(results);
|
| 584 |
+
} else {
|
| 585 |
+
renderThreeSolution(results);
|
| 586 |
+
}
|
| 587 |
+
solutionBlock.hidden = false;
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
function resetInputs() {
|
| 591 |
+
renderPage();
|
| 592 |
+
solutionBlock.hidden = true;
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
function regenerateSamples() {
|
| 596 |
+
generateAllSamples();
|
| 597 |
+
resetInputs();
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
controls.classMode.addEventListener("change", resetInputs);
|
| 601 |
+
controls.threshold.addEventListener("input", resetInputs);
|
| 602 |
+
controls.checkBtn.addEventListener("click", checkAnswers);
|
| 603 |
+
controls.showSolutionBtn.addEventListener("click", showSolution);
|
| 604 |
+
controls.newSamplesBtn.addEventListener("click", regenerateSamples);
|
| 605 |
+
controls.resetBtn.addEventListener("click", resetInputs);
|
| 606 |
+
|
| 607 |
+
generateAllSamples();
|
| 608 |
+
renderPage();
|
logistic-regression/cost-visualization.html
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>DDW Interface - Cost Function Visualization</title>
|
| 7 |
+
<link rel="stylesheet" href="styles.css" />
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<header class="site-header">
|
| 11 |
+
<div>
|
| 12 |
+
<p class="kicker">Data Driven World</p>
|
| 13 |
+
<h1>Visualization for Cost Function</h1>
|
| 14 |
+
</div>
|
| 15 |
+
<nav>
|
| 16 |
+
<a href="index.html">Home</a>
|
| 17 |
+
<a href="simple-sigmoid.html">Simple Sigmoid</a>
|
| 18 |
+
<a href="confusion-matrix.html">Confusion Matrix (Page 3)</a>
|
| 19 |
+
<a href="sigmoid.html">Sigmoid Function</a>
|
| 20 |
+
<a class="active" href="cost-visualization.html">Cost Function</a>
|
| 21 |
+
<a href="about.html">Notes</a>
|
| 22 |
+
</nav>
|
| 23 |
+
</header>
|
| 24 |
+
|
| 25 |
+
<main class="layout">
|
| 26 |
+
<section class="panel controls">
|
| 27 |
+
<h2>Gradient Descent Setup</h2>
|
| 28 |
+
|
| 29 |
+
<label for="caseSelect">Select model case</label>
|
| 30 |
+
<select id="caseSelect">
|
| 31 |
+
<option value="case1">Case 1: one intercept + one feature</option>
|
| 32 |
+
<option value="case2">Case 2: two features, no intercept</option>
|
| 33 |
+
</select>
|
| 34 |
+
|
| 35 |
+
<p class="hint" id="formulaText"></p>
|
| 36 |
+
|
| 37 |
+
<label for="lr">Learning rate: <span id="lrValue">0.20</span></label>
|
| 38 |
+
<input id="lr" type="range" min="0.001" max="1.000" step="0.001" value="0.200" />
|
| 39 |
+
<label for="lrInput">Learning rate input</label>
|
| 40 |
+
<input id="lrInput" type="number" min="0.001" max="1.000" step="0.001" value="0.200" />
|
| 41 |
+
|
| 42 |
+
<label for="iters">Iterations: <span id="iterValue">80</span></label>
|
| 43 |
+
<input id="iters" type="range" min="5" max="220" step="1" value="80" />
|
| 44 |
+
|
| 45 |
+
<label for="startP1">Start parameter 1: <span id="p1Value">-4.00</span></label>
|
| 46 |
+
<input id="startP1" type="range" min="-6" max="6" step="0.1" value="-4" />
|
| 47 |
+
|
| 48 |
+
<label for="startP2">Start parameter 2: <span id="p2Value">4.00</span></label>
|
| 49 |
+
<input id="startP2" type="range" min="-6" max="6" step="0.1" value="4" />
|
| 50 |
+
|
| 51 |
+
<label for="speed">Automatic run speed (ms): <span id="speedValue">90</span></label>
|
| 52 |
+
<input id="speed" type="range" min="20" max="260" step="5" value="90" />
|
| 53 |
+
|
| 54 |
+
<div class="button-row">
|
| 55 |
+
<button id="runBtn" type="button">Run Gradient Descent</button>
|
| 56 |
+
<button id="stepBtn" type="button">Next Step</button>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
<div class="button-row">
|
| 60 |
+
<button id="autoBtn" type="button">Automatic Run</button>
|
| 61 |
+
<button id="restartBtn" type="button">Restart Path</button>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
<div class="button-row">
|
| 65 |
+
<button id="regenBtn" type="button">Regenerate Synthetic Data</button>
|
| 66 |
+
<button id="clearBtn" type="button">Clear</button>
|
| 67 |
+
</div>
|
| 68 |
+
</section>
|
| 69 |
+
|
| 70 |
+
<section class="panel chart-panel">
|
| 71 |
+
<h2>3D Cost Surface and Gradient Descent Trajectory</h2>
|
| 72 |
+
<canvas id="costCanvas" width="900" height="520" aria-label="3D cost function and gradient descent"></canvas>
|
| 73 |
+
<div class="stats" id="costStats"></div>
|
| 74 |
+
</section>
|
| 75 |
+
</main>
|
| 76 |
+
|
| 77 |
+
<script src="cost.js"></script>
|
| 78 |
+
</body>
|
| 79 |
+
</html>
|
logistic-regression/cost.js
ADDED
|
@@ -0,0 +1,581 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const canvas = document.getElementById("costCanvas");
|
| 2 |
+
const ctx = canvas.getContext("2d");
|
| 3 |
+
|
| 4 |
+
const controls = {
|
| 5 |
+
caseSelect: document.getElementById("caseSelect"),
|
| 6 |
+
lr: document.getElementById("lr"),
|
| 7 |
+
lrInput: document.getElementById("lrInput"),
|
| 8 |
+
iters: document.getElementById("iters"),
|
| 9 |
+
p1: document.getElementById("startP1"),
|
| 10 |
+
p2: document.getElementById("startP2"),
|
| 11 |
+
speed: document.getElementById("speed"),
|
| 12 |
+
runBtn: document.getElementById("runBtn"),
|
| 13 |
+
stepBtn: document.getElementById("stepBtn"),
|
| 14 |
+
autoBtn: document.getElementById("autoBtn"),
|
| 15 |
+
restartBtn: document.getElementById("restartBtn"),
|
| 16 |
+
regenBtn: document.getElementById("regenBtn"),
|
| 17 |
+
clearBtn: document.getElementById("clearBtn"),
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
const labels = {
|
| 21 |
+
formula: document.getElementById("formulaText"),
|
| 22 |
+
lr: document.getElementById("lrValue"),
|
| 23 |
+
iters: document.getElementById("iterValue"),
|
| 24 |
+
p1: document.getElementById("p1Value"),
|
| 25 |
+
p2: document.getElementById("p2Value"),
|
| 26 |
+
speed: document.getElementById("speedValue"),
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
const stats = document.getElementById("costStats");
|
| 30 |
+
|
| 31 |
+
const P_MIN = -6;
|
| 32 |
+
const P_MAX = 6;
|
| 33 |
+
const GRID_N = 26;
|
| 34 |
+
|
| 35 |
+
const caseMeta = {
|
| 36 |
+
case1: {
|
| 37 |
+
p1Name: "b0",
|
| 38 |
+
p2Name: "b1",
|
| 39 |
+
logistic: "Case 1 logistic: p(y=1|x) = 1 / (1 + exp(-(b0 + b1x)))",
|
| 40 |
+
},
|
| 41 |
+
case2: {
|
| 42 |
+
p1Name: "w1",
|
| 43 |
+
p2Name: "w2",
|
| 44 |
+
logistic: "Case 2 logistic: p(y=1|x1,x2) = 1 / (1 + exp(-(w1x1 + w2x2))) (no intercept)",
|
| 45 |
+
},
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
let datasets = { case1: [], case2: [] };
|
| 49 |
+
let gridCache = { case1: null, case2: null };
|
| 50 |
+
|
| 51 |
+
let trajectory = [];
|
| 52 |
+
let stepIndex = 0;
|
| 53 |
+
let animationTimer = null;
|
| 54 |
+
|
| 55 |
+
function sigmoid(z) {
|
| 56 |
+
return 1 / (1 + Math.exp(-z));
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
function clampProb(p) {
|
| 60 |
+
return Math.min(1 - 1e-9, Math.max(1e-9, p));
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
function randRange(min, max) {
|
| 64 |
+
return min + Math.random() * (max - min);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
function randNormal(mean = 0, std = 1) {
|
| 68 |
+
const u1 = Math.max(1e-12, Math.random());
|
| 69 |
+
const u2 = Math.random();
|
| 70 |
+
const z0 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
| 71 |
+
return mean + z0 * std;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
function generateSyntheticData() {
|
| 75 |
+
const case1 = [];
|
| 76 |
+
const case2 = [];
|
| 77 |
+
|
| 78 |
+
const trueB0 = -0.2;
|
| 79 |
+
const trueB1 = 2.35;
|
| 80 |
+
for (let i = 0; i < 160; i += 1) {
|
| 81 |
+
const center = i < 80 ? -3.0 : 3.0;
|
| 82 |
+
const x = randNormal(center, 1.3);
|
| 83 |
+
const p = sigmoid(trueB0 + trueB1 * x);
|
| 84 |
+
const y = Math.random() < p ? 1 : 0;
|
| 85 |
+
case1.push({ x, y });
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
const trueW1 = 2.15;
|
| 89 |
+
const trueW2 = 1.85;
|
| 90 |
+
for (let i = 0; i < 190; i += 1) {
|
| 91 |
+
const center1 = i < 95 ? -2.4 : 2.4;
|
| 92 |
+
const center2 = i < 95 ? -1.9 : 1.9;
|
| 93 |
+
const x1 = randNormal(center1, 1.25);
|
| 94 |
+
const x2 = randNormal(center2 + 0.45 * (x1 - center1), 1.1);
|
| 95 |
+
const p = sigmoid(trueW1 * x1 + trueW2 * x2);
|
| 96 |
+
const y = Math.random() < p ? 1 : 0;
|
| 97 |
+
case2.push({ x1, x2, y });
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
datasets = { case1, case2 };
|
| 101 |
+
gridCache = { case1: null, case2: null };
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
function costFor(caseKey, params) {
|
| 105 |
+
const data = datasets[caseKey];
|
| 106 |
+
let total = 0;
|
| 107 |
+
|
| 108 |
+
if (caseKey === "case1") {
|
| 109 |
+
const [b0, b1] = params;
|
| 110 |
+
for (const row of data) {
|
| 111 |
+
const p = clampProb(sigmoid(b0 + b1 * row.x));
|
| 112 |
+
total += -(row.y * Math.log(p) + (1 - row.y) * Math.log(1 - p));
|
| 113 |
+
}
|
| 114 |
+
} else {
|
| 115 |
+
const [w1, w2] = params;
|
| 116 |
+
for (const row of data) {
|
| 117 |
+
const p = clampProb(sigmoid(w1 * row.x1 + w2 * row.x2));
|
| 118 |
+
total += -(row.y * Math.log(p) + (1 - row.y) * Math.log(1 - p));
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
return total / data.length;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
function gradFor(caseKey, params) {
|
| 126 |
+
const data = datasets[caseKey];
|
| 127 |
+
let g1 = 0;
|
| 128 |
+
let g2 = 0;
|
| 129 |
+
|
| 130 |
+
if (caseKey === "case1") {
|
| 131 |
+
const [b0, b1] = params;
|
| 132 |
+
for (const row of data) {
|
| 133 |
+
const p = sigmoid(b0 + b1 * row.x);
|
| 134 |
+
const diff = p - row.y;
|
| 135 |
+
g1 += diff;
|
| 136 |
+
g2 += diff * row.x;
|
| 137 |
+
}
|
| 138 |
+
} else {
|
| 139 |
+
const [w1, w2] = params;
|
| 140 |
+
for (const row of data) {
|
| 141 |
+
const p = sigmoid(w1 * row.x1 + w2 * row.x2);
|
| 142 |
+
const diff = p - row.y;
|
| 143 |
+
g1 += diff * row.x1;
|
| 144 |
+
g2 += diff * row.x2;
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
const m = data.length;
|
| 149 |
+
return [g1 / m, g2 / m];
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
function buildTrajectory(caseKey, start, lr, iters) {
|
| 153 |
+
const path = [];
|
| 154 |
+
let params = [start[0], start[1]];
|
| 155 |
+
|
| 156 |
+
path.push({ p1: params[0], p2: params[1], cost: costFor(caseKey, params), gradNorm: 0 });
|
| 157 |
+
|
| 158 |
+
for (let i = 0; i < iters; i += 1) {
|
| 159 |
+
const grad = gradFor(caseKey, params);
|
| 160 |
+
const gradNorm = Math.hypot(grad[0], grad[1]);
|
| 161 |
+
params = [params[0] - lr * grad[0], params[1] - lr * grad[1]];
|
| 162 |
+
path.push({ p1: params[0], p2: params[1], cost: costFor(caseKey, params), gradNorm });
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
return path;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
function getGrid(caseKey) {
|
| 169 |
+
if (gridCache[caseKey]) {
|
| 170 |
+
return gridCache[caseKey];
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
const vals = [];
|
| 174 |
+
let min = Infinity;
|
| 175 |
+
let max = -Infinity;
|
| 176 |
+
|
| 177 |
+
for (let j = 0; j < GRID_N; j += 1) {
|
| 178 |
+
const row = [];
|
| 179 |
+
const p2 = P_MIN + (j / (GRID_N - 1)) * (P_MAX - P_MIN);
|
| 180 |
+
for (let i = 0; i < GRID_N; i += 1) {
|
| 181 |
+
const p1 = P_MIN + (i / (GRID_N - 1)) * (P_MAX - P_MIN);
|
| 182 |
+
const c = costFor(caseKey, [p1, p2]);
|
| 183 |
+
row.push(c);
|
| 184 |
+
if (c < min) min = c;
|
| 185 |
+
if (c > max) max = c;
|
| 186 |
+
}
|
| 187 |
+
vals.push(row);
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
gridCache[caseKey] = { vals, min, max };
|
| 191 |
+
return gridCache[caseKey];
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
function colorScale(t) {
|
| 195 |
+
const clamped = Math.max(0, Math.min(1, t));
|
| 196 |
+
const r = Math.round(35 + 220 * clamped);
|
| 197 |
+
const g = Math.round(90 + 160 * (1 - Math.abs(clamped - 0.5) * 2));
|
| 198 |
+
const b = Math.round(240 - 220 * clamped);
|
| 199 |
+
return `rgb(${r},${g},${b})`;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
function project3D(x, y, z) {
|
| 203 |
+
const yaw = -0.75;
|
| 204 |
+
const pitch = 0.75;
|
| 205 |
+
|
| 206 |
+
const cy = Math.cos(yaw);
|
| 207 |
+
const sy = Math.sin(yaw);
|
| 208 |
+
const cp = Math.cos(pitch);
|
| 209 |
+
const sp = Math.sin(pitch);
|
| 210 |
+
|
| 211 |
+
const xr = x * cy - y * sy;
|
| 212 |
+
const yr = x * sy + y * cy;
|
| 213 |
+
|
| 214 |
+
const y2 = yr * cp - z * sp;
|
| 215 |
+
const z2 = yr * sp + z * cp;
|
| 216 |
+
|
| 217 |
+
const perspective = 1 / (1 + z2 * 0.09);
|
| 218 |
+
const scale = 56;
|
| 219 |
+
|
| 220 |
+
return {
|
| 221 |
+
sx: canvas.width * 0.49 + xr * scale * perspective,
|
| 222 |
+
sy: canvas.height * 0.62 - y2 * scale * perspective,
|
| 223 |
+
depth: z2,
|
| 224 |
+
};
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
function mapParam(v) {
|
| 228 |
+
return ((v - P_MIN) / (P_MAX - P_MIN)) * 8 - 4;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
function mapCost(cost, minCost, maxCost) {
|
| 232 |
+
const t = (cost - minCost) / Math.max(1e-9, maxCost - minCost);
|
| 233 |
+
return t * 5.0;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
function drawSurface(caseKey) {
|
| 237 |
+
const grid = getGrid(caseKey);
|
| 238 |
+
const { vals, min, max } = grid;
|
| 239 |
+
|
| 240 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 241 |
+
ctx.fillStyle = "#ffffff";
|
| 242 |
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
| 243 |
+
|
| 244 |
+
for (let j = 0; j < GRID_N - 1; j += 1) {
|
| 245 |
+
for (let i = 0; i < GRID_N - 1; i += 1) {
|
| 246 |
+
const p1a = P_MIN + (i / (GRID_N - 1)) * (P_MAX - P_MIN);
|
| 247 |
+
const p2a = P_MIN + (j / (GRID_N - 1)) * (P_MAX - P_MIN);
|
| 248 |
+
const p1b = P_MIN + ((i + 1) / (GRID_N - 1)) * (P_MAX - P_MIN);
|
| 249 |
+
const p2b = P_MIN + ((j + 1) / (GRID_N - 1)) * (P_MAX - P_MIN);
|
| 250 |
+
|
| 251 |
+
const z00 = mapCost(vals[j][i], min, max);
|
| 252 |
+
const z10 = mapCost(vals[j][i + 1], min, max);
|
| 253 |
+
const z11 = mapCost(vals[j + 1][i + 1], min, max);
|
| 254 |
+
const z01 = mapCost(vals[j + 1][i], min, max);
|
| 255 |
+
|
| 256 |
+
const q00 = project3D(mapParam(p1a), mapParam(p2a), z00);
|
| 257 |
+
const q10 = project3D(mapParam(p1b), mapParam(p2a), z10);
|
| 258 |
+
const q11 = project3D(mapParam(p1b), mapParam(p2b), z11);
|
| 259 |
+
const q01 = project3D(mapParam(p1a), mapParam(p2b), z01);
|
| 260 |
+
|
| 261 |
+
const t = ((vals[j][i] + vals[j][i + 1] + vals[j + 1][i + 1] + vals[j + 1][i]) / 4 - min) / Math.max(1e-9, max - min);
|
| 262 |
+
ctx.fillStyle = colorScale(t);
|
| 263 |
+
ctx.globalAlpha = 0.48;
|
| 264 |
+
ctx.beginPath();
|
| 265 |
+
ctx.moveTo(q00.sx, q00.sy);
|
| 266 |
+
ctx.lineTo(q10.sx, q10.sy);
|
| 267 |
+
ctx.lineTo(q11.sx, q11.sy);
|
| 268 |
+
ctx.lineTo(q01.sx, q01.sy);
|
| 269 |
+
ctx.closePath();
|
| 270 |
+
ctx.fill();
|
| 271 |
+
}
|
| 272 |
+
}
|
| 273 |
+
ctx.globalAlpha = 1;
|
| 274 |
+
|
| 275 |
+
for (let j = 0; j < GRID_N; j += 1) {
|
| 276 |
+
ctx.beginPath();
|
| 277 |
+
for (let i = 0; i < GRID_N; i += 1) {
|
| 278 |
+
const p1 = P_MIN + (i / (GRID_N - 1)) * (P_MAX - P_MIN);
|
| 279 |
+
const p2 = P_MIN + (j / (GRID_N - 1)) * (P_MAX - P_MIN);
|
| 280 |
+
const x = mapParam(p1);
|
| 281 |
+
const y = mapParam(p2);
|
| 282 |
+
const z = mapCost(vals[j][i], min, max);
|
| 283 |
+
const pr = project3D(x, y, z);
|
| 284 |
+
if (i === 0) ctx.moveTo(pr.sx, pr.sy);
|
| 285 |
+
else ctx.lineTo(pr.sx, pr.sy);
|
| 286 |
+
}
|
| 287 |
+
ctx.strokeStyle = "rgba(13, 66, 83, 0.34)";
|
| 288 |
+
ctx.lineWidth = 1;
|
| 289 |
+
ctx.stroke();
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
for (let i = 0; i < GRID_N; i += 1) {
|
| 293 |
+
ctx.beginPath();
|
| 294 |
+
for (let j = 0; j < GRID_N; j += 1) {
|
| 295 |
+
const p1 = P_MIN + (i / (GRID_N - 1)) * (P_MAX - P_MIN);
|
| 296 |
+
const p2 = P_MIN + (j / (GRID_N - 1)) * (P_MAX - P_MIN);
|
| 297 |
+
const x = mapParam(p1);
|
| 298 |
+
const y = mapParam(p2);
|
| 299 |
+
const z = mapCost(vals[j][i], min, max);
|
| 300 |
+
const pr = project3D(x, y, z);
|
| 301 |
+
if (j === 0) ctx.moveTo(pr.sx, pr.sy);
|
| 302 |
+
else ctx.lineTo(pr.sx, pr.sy);
|
| 303 |
+
}
|
| 304 |
+
ctx.strokeStyle = "rgba(13, 66, 83, 0.26)";
|
| 305 |
+
ctx.lineWidth = 1;
|
| 306 |
+
ctx.stroke();
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
drawAxes3D(caseKey, min, max);
|
| 310 |
+
drawHeatLegend(min, max);
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
function drawAxes3D(caseKey, minCost, maxCost) {
|
| 314 |
+
const meta = caseMeta[caseKey];
|
| 315 |
+
|
| 316 |
+
const o = project3D(-4.3, -4.3, 0);
|
| 317 |
+
const xA = project3D(4.4, -4.3, 0);
|
| 318 |
+
const yA = project3D(-4.3, 4.4, 0);
|
| 319 |
+
const zA = project3D(-4.3, -4.3, 3.4);
|
| 320 |
+
|
| 321 |
+
ctx.strokeStyle = "#13323d";
|
| 322 |
+
ctx.lineWidth = 1.4;
|
| 323 |
+
|
| 324 |
+
ctx.beginPath();
|
| 325 |
+
ctx.moveTo(o.sx, o.sy);
|
| 326 |
+
ctx.lineTo(xA.sx, xA.sy);
|
| 327 |
+
ctx.stroke();
|
| 328 |
+
|
| 329 |
+
ctx.beginPath();
|
| 330 |
+
ctx.moveTo(o.sx, o.sy);
|
| 331 |
+
ctx.lineTo(yA.sx, yA.sy);
|
| 332 |
+
ctx.stroke();
|
| 333 |
+
|
| 334 |
+
ctx.beginPath();
|
| 335 |
+
ctx.moveTo(o.sx, o.sy);
|
| 336 |
+
ctx.lineTo(zA.sx, zA.sy);
|
| 337 |
+
ctx.stroke();
|
| 338 |
+
|
| 339 |
+
ctx.fillStyle = "#13323d";
|
| 340 |
+
ctx.font = "12px 'Avenir Next', sans-serif";
|
| 341 |
+
ctx.fillText(meta.p1Name, xA.sx + 6, xA.sy + 2);
|
| 342 |
+
ctx.fillText(meta.p2Name, yA.sx + 6, yA.sy + 2);
|
| 343 |
+
ctx.fillText("J", zA.sx + 6, zA.sy + 2);
|
| 344 |
+
|
| 345 |
+
ctx.fillText(`J min=${minCost.toFixed(3)}`, 18, 24);
|
| 346 |
+
ctx.fillText(`J max=${maxCost.toFixed(3)}`, 18, 42);
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
function drawHeatLegend(minCost, maxCost) {
|
| 350 |
+
const x = canvas.width - 54;
|
| 351 |
+
const y = 80;
|
| 352 |
+
const w = 18;
|
| 353 |
+
const h = 180;
|
| 354 |
+
|
| 355 |
+
for (let i = 0; i < h; i += 1) {
|
| 356 |
+
const t = 1 - i / h;
|
| 357 |
+
ctx.strokeStyle = colorScale(t);
|
| 358 |
+
ctx.beginPath();
|
| 359 |
+
ctx.moveTo(x, y + i);
|
| 360 |
+
ctx.lineTo(x + w, y + i);
|
| 361 |
+
ctx.stroke();
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
ctx.strokeStyle = "#14343f";
|
| 365 |
+
ctx.strokeRect(x, y, w, h);
|
| 366 |
+
|
| 367 |
+
ctx.fillStyle = "#14343f";
|
| 368 |
+
ctx.font = "11px 'Avenir Next', sans-serif";
|
| 369 |
+
ctx.fillText("Cost", x - 4, y - 8);
|
| 370 |
+
ctx.fillText(maxCost.toFixed(2), x - 8, y - 6);
|
| 371 |
+
ctx.fillText(minCost.toFixed(2), x - 6, y + h + 16);
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
function drawTrajectory3D(caseKey) {
|
| 375 |
+
if (trajectory.length === 0) return;
|
| 376 |
+
|
| 377 |
+
const grid = getGrid(caseKey);
|
| 378 |
+
const current = Math.min(stepIndex, trajectory.length - 1);
|
| 379 |
+
|
| 380 |
+
ctx.strokeStyle = "#d8534f";
|
| 381 |
+
ctx.lineWidth = 2.6;
|
| 382 |
+
ctx.beginPath();
|
| 383 |
+
|
| 384 |
+
for (let i = 0; i <= current; i += 1) {
|
| 385 |
+
const node = trajectory[i];
|
| 386 |
+
const p = project3D(
|
| 387 |
+
mapParam(node.p1),
|
| 388 |
+
mapParam(node.p2),
|
| 389 |
+
mapCost(node.cost, grid.min, grid.max)
|
| 390 |
+
);
|
| 391 |
+
if (i === 0) ctx.moveTo(p.sx, p.sy);
|
| 392 |
+
else ctx.lineTo(p.sx, p.sy);
|
| 393 |
+
}
|
| 394 |
+
ctx.stroke();
|
| 395 |
+
|
| 396 |
+
for (let i = 0; i <= current; i += 1) {
|
| 397 |
+
const node = trajectory[i];
|
| 398 |
+
const p = project3D(
|
| 399 |
+
mapParam(node.p1),
|
| 400 |
+
mapParam(node.p2),
|
| 401 |
+
mapCost(node.cost, grid.min, grid.max)
|
| 402 |
+
);
|
| 403 |
+
const isNow = i === current;
|
| 404 |
+
ctx.fillStyle = isNow ? "#102a32" : "#d8534f";
|
| 405 |
+
ctx.beginPath();
|
| 406 |
+
ctx.arc(p.sx, p.sy, isNow ? 5.4 : 3.2, 0, Math.PI * 2);
|
| 407 |
+
ctx.fill();
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
const node = trajectory[current];
|
| 411 |
+
const p = project3D(mapParam(node.p1), mapParam(node.p2), mapCost(node.cost, grid.min, grid.max));
|
| 412 |
+
ctx.fillStyle = "#102a32";
|
| 413 |
+
ctx.font = "12px 'Avenir Next', sans-serif";
|
| 414 |
+
ctx.fillText(`step ${current}`, p.sx + 8, p.sy - 8);
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
function renderStats(caseKey) {
|
| 418 |
+
const meta = caseMeta[caseKey];
|
| 419 |
+
const dataCount = datasets[caseKey].length;
|
| 420 |
+
|
| 421 |
+
if (trajectory.length === 0) {
|
| 422 |
+
stats.innerHTML = `
|
| 423 |
+
<span>Model case: ${meta.p1Name}, ${meta.p2Name}</span>
|
| 424 |
+
<span>Synthetic samples: ${dataCount}</span>
|
| 425 |
+
<span>No trajectory yet. Click Run Gradient Descent.</span>
|
| 426 |
+
<span>Use Next Step or Automatic Run to show the path.</span>
|
| 427 |
+
`;
|
| 428 |
+
return;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
const idx = Math.min(stepIndex, trajectory.length - 1);
|
| 432 |
+
const curr = trajectory[idx];
|
| 433 |
+
const start = trajectory[0];
|
| 434 |
+
const last = trajectory[trajectory.length - 1];
|
| 435 |
+
|
| 436 |
+
stats.innerHTML = `
|
| 437 |
+
<span>Model case: ${meta.p1Name}, ${meta.p2Name}</span>
|
| 438 |
+
<span>Synthetic samples: ${dataCount}</span>
|
| 439 |
+
<span>Current step: ${idx} / ${trajectory.length - 1}</span>
|
| 440 |
+
<span>Current params: ${meta.p1Name}=${curr.p1.toFixed(3)}, ${meta.p2Name}=${curr.p2.toFixed(3)}</span>
|
| 441 |
+
<span>Current cost J: ${curr.cost.toFixed(5)}</span>
|
| 442 |
+
<span>Start J: ${start.cost.toFixed(5)} -> Final J: ${last.cost.toFixed(5)}</span>
|
| 443 |
+
`;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
function render() {
|
| 447 |
+
const caseKey = controls.caseSelect.value;
|
| 448 |
+
|
| 449 |
+
labels.formula.textContent = caseMeta[caseKey].logistic;
|
| 450 |
+
labels.lr.textContent = Number(controls.lr.value).toFixed(3);
|
| 451 |
+
labels.iters.textContent = controls.iters.value;
|
| 452 |
+
labels.p1.textContent = Number(controls.p1.value).toFixed(2);
|
| 453 |
+
labels.p2.textContent = Number(controls.p2.value).toFixed(2);
|
| 454 |
+
labels.speed.textContent = controls.speed.value;
|
| 455 |
+
|
| 456 |
+
drawSurface(caseKey);
|
| 457 |
+
drawTrajectory3D(caseKey);
|
| 458 |
+
renderStats(caseKey);
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
function stopAnimation() {
|
| 462 |
+
if (animationTimer) {
|
| 463 |
+
clearInterval(animationTimer);
|
| 464 |
+
animationTimer = null;
|
| 465 |
+
controls.autoBtn.textContent = "Automatic Run";
|
| 466 |
+
}
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
function runTrajectory() {
|
| 470 |
+
stopAnimation();
|
| 471 |
+
|
| 472 |
+
const caseKey = controls.caseSelect.value;
|
| 473 |
+
const start = [Number(controls.p1.value), Number(controls.p2.value)];
|
| 474 |
+
const lr = Number(controls.lr.value);
|
| 475 |
+
const iters = Number(controls.iters.value);
|
| 476 |
+
|
| 477 |
+
trajectory = buildTrajectory(caseKey, start, lr, iters);
|
| 478 |
+
stepIndex = 0;
|
| 479 |
+
render();
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
function nextStep() {
|
| 483 |
+
if (trajectory.length === 0) {
|
| 484 |
+
runTrajectory();
|
| 485 |
+
return;
|
| 486 |
+
}
|
| 487 |
+
if (stepIndex < trajectory.length - 1) {
|
| 488 |
+
stepIndex += 1;
|
| 489 |
+
render();
|
| 490 |
+
}
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
function automaticRun() {
|
| 494 |
+
if (animationTimer) {
|
| 495 |
+
stopAnimation();
|
| 496 |
+
return;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
if (trajectory.length === 0) {
|
| 500 |
+
runTrajectory();
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
controls.autoBtn.textContent = "Stop Auto";
|
| 504 |
+
|
| 505 |
+
animationTimer = setInterval(() => {
|
| 506 |
+
if (stepIndex >= trajectory.length - 1) {
|
| 507 |
+
stopAnimation();
|
| 508 |
+
return;
|
| 509 |
+
}
|
| 510 |
+
stepIndex += 1;
|
| 511 |
+
render();
|
| 512 |
+
}, Number(controls.speed.value));
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
function restartPath() {
|
| 516 |
+
stopAnimation();
|
| 517 |
+
if (trajectory.length > 0) {
|
| 518 |
+
stepIndex = 0;
|
| 519 |
+
}
|
| 520 |
+
render();
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
function clearPath() {
|
| 524 |
+
stopAnimation();
|
| 525 |
+
trajectory = [];
|
| 526 |
+
stepIndex = 0;
|
| 527 |
+
render();
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
function syncLrFromSlider() {
|
| 531 |
+
controls.lrInput.value = Number(controls.lr.value).toFixed(3);
|
| 532 |
+
render();
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
function syncLrFromInput() {
|
| 536 |
+
let v = Number(controls.lrInput.value);
|
| 537 |
+
if (!Number.isFinite(v)) v = 0.2;
|
| 538 |
+
v = Math.max(0.001, Math.min(1.0, v));
|
| 539 |
+
controls.lr.value = v;
|
| 540 |
+
controls.lrInput.value = v.toFixed(3);
|
| 541 |
+
render();
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
function applyCaseDefaults(caseKey) {
|
| 545 |
+
if (caseKey === "case1") {
|
| 546 |
+
controls.p1.value = -4.0;
|
| 547 |
+
controls.p2.value = 4.0;
|
| 548 |
+
controls.lr.value = 0.20;
|
| 549 |
+
} else {
|
| 550 |
+
controls.p1.value = -2.2;
|
| 551 |
+
controls.p2.value = -2.0;
|
| 552 |
+
controls.lr.value = 0.14;
|
| 553 |
+
}
|
| 554 |
+
controls.lrInput.value = Number(controls.lr.value).toFixed(3);
|
| 555 |
+
controls.iters.value = 80;
|
| 556 |
+
controls.speed.value = 90;
|
| 557 |
+
clearPath();
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
for (const el of [controls.iters, controls.p1, controls.p2, controls.speed]) {
|
| 561 |
+
el.addEventListener("input", render);
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
controls.lr.addEventListener("input", syncLrFromSlider);
|
| 565 |
+
controls.lrInput.addEventListener("change", syncLrFromInput);
|
| 566 |
+
controls.caseSelect.addEventListener("change", () => {
|
| 567 |
+
applyCaseDefaults(controls.caseSelect.value);
|
| 568 |
+
});
|
| 569 |
+
|
| 570 |
+
controls.runBtn.addEventListener("click", runTrajectory);
|
| 571 |
+
controls.stepBtn.addEventListener("click", nextStep);
|
| 572 |
+
controls.autoBtn.addEventListener("click", automaticRun);
|
| 573 |
+
controls.restartBtn.addEventListener("click", restartPath);
|
| 574 |
+
controls.regenBtn.addEventListener("click", () => {
|
| 575 |
+
generateSyntheticData();
|
| 576 |
+
clearPath();
|
| 577 |
+
});
|
| 578 |
+
controls.clearBtn.addEventListener("click", clearPath);
|
| 579 |
+
|
| 580 |
+
generateSyntheticData();
|
| 581 |
+
applyCaseDefaults("case1");
|
logistic-regression/index.html
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>DDW Interface - Home</title>
|
| 7 |
+
<link rel="stylesheet" href="styles.css" />
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<header class="site-header">
|
| 11 |
+
<div>
|
| 12 |
+
<p class="kicker">Data Driven World</p>
|
| 13 |
+
<h1>Classification Categorical Data</h1>
|
| 14 |
+
</div>
|
| 15 |
+
<nav>
|
| 16 |
+
<a class="active" href="index.html">Home</a>
|
| 17 |
+
<a href="simple-sigmoid.html">Simple Sigmoid</a>
|
| 18 |
+
<a href="confusion-matrix.html">Confusion Matrix (Page 3)</a>
|
| 19 |
+
<a href="sigmoid.html">Sigmoid Function</a>
|
| 20 |
+
<a href="cost-visualization.html">Cost Function</a>
|
| 21 |
+
<a href="about.html">Notes</a>
|
| 22 |
+
</nav>
|
| 23 |
+
</header>
|
| 24 |
+
|
| 25 |
+
<main class="panel page-copy home-page">
|
| 26 |
+
<h2>Lecture Objective</h2>
|
| 27 |
+
<p>
|
| 28 |
+
The objective of this lecture is classification of categorical data. Use these interactive pages to understand
|
| 29 |
+
how logistic regression and threshold-based decisions classify observations into categories.
|
| 30 |
+
</p>
|
| 31 |
+
|
| 32 |
+
<section class="home-grid">
|
| 33 |
+
<article class="home-card">
|
| 34 |
+
<h3>Simple Sigmoid (Page 1)</h3>
|
| 35 |
+
<p>Basic illustration of the function 1/(1+np.exp(-z)) with a single z control.</p>
|
| 36 |
+
<a class="cta-button" href="simple-sigmoid.html">Simple Sigmoid</a>
|
| 37 |
+
</article>
|
| 38 |
+
|
| 39 |
+
<article class="home-card">
|
| 40 |
+
<h3>Confusion Matrix Practice (Page 3)</h3>
|
| 41 |
+
<p>Practice threshold-based 2-class and 3-class predictions, then build confusion matrix and key metrics.</p>
|
| 42 |
+
<a class="cta-button secondary" href="confusion-matrix.html">Confusion Matrix</a>
|
| 43 |
+
</article>
|
| 44 |
+
|
| 45 |
+
<article class="home-card">
|
| 46 |
+
<h3>Sigmoid Function</h3>
|
| 47 |
+
<p>Interactive plot of p = 1 / (1 + exp(-(b0 + b1x))) with sliders and chart actions.</p>
|
| 48 |
+
<a class="cta-button secondary" href="sigmoid.html">Sigmoid Function</a>
|
| 49 |
+
</article>
|
| 50 |
+
|
| 51 |
+
<article class="home-card">
|
| 52 |
+
<h3>Visualization for Cost Function</h3>
|
| 53 |
+
<p>Compare two logistic-model cases and track gradient descent trajectory on the cost surface.</p>
|
| 54 |
+
<a class="cta-button secondary" href="cost-visualization.html">Cost Function</a>
|
| 55 |
+
</article>
|
| 56 |
+
</section>
|
| 57 |
+
</main>
|
| 58 |
+
</body>
|
| 59 |
+
</html>
|
logistic-regression/sigmoid.html
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>DDW Interface - Sigmoid Function</title>
|
| 7 |
+
<link rel="stylesheet" href="styles.css" />
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<header class="site-header">
|
| 11 |
+
<div>
|
| 12 |
+
<p class="kicker">Data Driven World</p>
|
| 13 |
+
<h1>Sigmoid Function Playground</h1>
|
| 14 |
+
</div>
|
| 15 |
+
<nav>
|
| 16 |
+
<a href="index.html">Home</a>
|
| 17 |
+
<a href="simple-sigmoid.html">Simple Sigmoid</a>
|
| 18 |
+
<a href="confusion-matrix.html">Confusion Matrix (Page 3)</a>
|
| 19 |
+
<a class="active" href="sigmoid.html">Sigmoid Function</a>
|
| 20 |
+
<a href="cost-visualization.html">Cost Function</a>
|
| 21 |
+
<a href="about.html">Notes</a>
|
| 22 |
+
</nav>
|
| 23 |
+
</header>
|
| 24 |
+
|
| 25 |
+
<main class="layout">
|
| 26 |
+
<section class="panel controls">
|
| 27 |
+
<h2>Model Controls</h2>
|
| 28 |
+
|
| 29 |
+
<label for="b0">b0 (intercept): <span id="b0Value">0.00</span></label>
|
| 30 |
+
<input id="b0" type="range" min="-10" max="10" step="0.1" value="0" />
|
| 31 |
+
|
| 32 |
+
<label for="b1">b1 (slope): <span id="b1Value">1.00</span></label>
|
| 33 |
+
<input id="b1" type="range" min="-5" max="5" step="0.1" value="1" />
|
| 34 |
+
|
| 35 |
+
<label for="t">threshold t: <span id="tValue">0.50</span></label>
|
| 36 |
+
<input id="t" type="range" min="0.05" max="0.95" step="0.01" value="0.5" />
|
| 37 |
+
|
| 38 |
+
<label for="probeX">probe x: <span id="probeValue">0.00</span></label>
|
| 39 |
+
<input id="probeX" type="range" min="-10" max="10" step="0.1" value="0" />
|
| 40 |
+
|
| 41 |
+
<div class="button-row">
|
| 42 |
+
<button id="resetBtn" type="button">Reset</button>
|
| 43 |
+
<button id="animateBtn" type="button">Animate Probe</button>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<label for="preset">Preset examples</label>
|
| 47 |
+
<select id="preset">
|
| 48 |
+
<option value="default">Default (b0=0, b1=1, t=0.5)</option>
|
| 49 |
+
<option value="steep">Steep classifier</option>
|
| 50 |
+
<option value="reversed">Reversed slope</option>
|
| 51 |
+
<option value="high-threshold">High threshold</option>
|
| 52 |
+
</select>
|
| 53 |
+
|
| 54 |
+
<div class="checkboxes">
|
| 55 |
+
<label><input id="showGrid" type="checkbox" checked /> Show grid</label>
|
| 56 |
+
<label><input id="showShade" type="checkbox" checked /> Shade positive region</label>
|
| 57 |
+
<label><input id="showDerivative" type="checkbox" /> Show derivative curve</label>
|
| 58 |
+
</div>
|
| 59 |
+
</section>
|
| 60 |
+
|
| 61 |
+
<section class="panel chart-panel">
|
| 62 |
+
<h2>p(x) = 1 / (1 + exp(-(b0 + b1x)))</h2>
|
| 63 |
+
<canvas id="plot" width="900" height="520" aria-label="Logistic curve chart"></canvas>
|
| 64 |
+
|
| 65 |
+
<div class="stats" id="stats"></div>
|
| 66 |
+
<p class="hint">Interactive option: click anywhere on the chart to add sample x points and see class labels using threshold t.</p>
|
| 67 |
+
</section>
|
| 68 |
+
</main>
|
| 69 |
+
|
| 70 |
+
<script src="app.js"></script>
|
| 71 |
+
</body>
|
| 72 |
+
</html>
|
logistic-regression/simple-sigmoid.html
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>DDW Interface - Simple Sigmoid</title>
|
| 7 |
+
<link rel="stylesheet" href="styles.css" />
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<header class="site-header">
|
| 11 |
+
<div>
|
| 12 |
+
<p class="kicker">Data Driven World</p>
|
| 13 |
+
<h1>Simple Sigmoid (Page 1)</h1>
|
| 14 |
+
</div>
|
| 15 |
+
<nav>
|
| 16 |
+
<a href="index.html">Home</a>
|
| 17 |
+
<a class="active" href="simple-sigmoid.html">Simple Sigmoid</a>
|
| 18 |
+
<a href="confusion-matrix.html">Confusion Matrix (Page 3)</a>
|
| 19 |
+
<a href="sigmoid.html">Sigmoid Function</a>
|
| 20 |
+
<a href="cost-visualization.html">Cost Function</a>
|
| 21 |
+
<a href="about.html">Notes</a>
|
| 22 |
+
</nav>
|
| 23 |
+
</header>
|
| 24 |
+
|
| 25 |
+
<main class="layout">
|
| 26 |
+
<section class="panel controls">
|
| 27 |
+
<h2>Single-Variable Function</h2>
|
| 28 |
+
|
| 29 |
+
<p class="hint">
|
| 30 |
+
Illustrating: <code>1 / (1 + np.exp(-z))</code>
|
| 31 |
+
</p>
|
| 32 |
+
|
| 33 |
+
<label for="z">z value: <span id="zValue">0.00</span></label>
|
| 34 |
+
<input id="z" type="range" min="-10" max="10" step="0.1" value="0" />
|
| 35 |
+
|
| 36 |
+
<div class="button-row">
|
| 37 |
+
<button id="resetSimple" type="button">Reset</button>
|
| 38 |
+
<button id="animateSimple" type="button">Animate z</button>
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<div class="checkboxes">
|
| 42 |
+
<label><input id="showHalf" type="checkbox" checked /> Show y = 0.5 line</label>
|
| 43 |
+
<label><input id="showFormula" type="checkbox" checked /> Show formula annotation</label>
|
| 44 |
+
</div>
|
| 45 |
+
</section>
|
| 46 |
+
|
| 47 |
+
<section class="panel chart-panel">
|
| 48 |
+
<h2>sigmoid(z) = 1 / (1 + np.exp(-z))</h2>
|
| 49 |
+
<canvas id="simplePlot" width="900" height="520" aria-label="Simple sigmoid chart"></canvas>
|
| 50 |
+
<div class="stats" id="simpleStats"></div>
|
| 51 |
+
</section>
|
| 52 |
+
</main>
|
| 53 |
+
|
| 54 |
+
<script src="simple.js"></script>
|
| 55 |
+
</body>
|
| 56 |
+
</html>
|
logistic-regression/simple.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const canvas = document.getElementById("simplePlot");
|
| 2 |
+
const ctx = canvas.getContext("2d");
|
| 3 |
+
|
| 4 |
+
const zSlider = document.getElementById("z");
|
| 5 |
+
const zValue = document.getElementById("zValue");
|
| 6 |
+
const showHalf = document.getElementById("showHalf");
|
| 7 |
+
const showFormula = document.getElementById("showFormula");
|
| 8 |
+
const resetBtn = document.getElementById("resetSimple");
|
| 9 |
+
const animateBtn = document.getElementById("animateSimple");
|
| 10 |
+
const stats = document.getElementById("simpleStats");
|
| 11 |
+
|
| 12 |
+
const MARGIN = { left: 72, right: 24, top: 24, bottom: 58 };
|
| 13 |
+
const Z_MIN = -10;
|
| 14 |
+
const Z_MAX = 10;
|
| 15 |
+
|
| 16 |
+
let timer = null;
|
| 17 |
+
let direction = 1;
|
| 18 |
+
|
| 19 |
+
function sigmoid(z) {
|
| 20 |
+
return 1 / (1 + Math.exp(-z));
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
function zToX(z) {
|
| 24 |
+
const width = canvas.width - MARGIN.left - MARGIN.right;
|
| 25 |
+
return MARGIN.left + ((z - Z_MIN) / (Z_MAX - Z_MIN)) * width;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
function yToCanvas(y) {
|
| 29 |
+
const height = canvas.height - MARGIN.top - MARGIN.bottom;
|
| 30 |
+
return canvas.height - MARGIN.bottom - y * height;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
function drawAxes() {
|
| 34 |
+
const left = MARGIN.left;
|
| 35 |
+
const right = canvas.width - MARGIN.right;
|
| 36 |
+
const top = MARGIN.top;
|
| 37 |
+
const bottom = canvas.height - MARGIN.bottom;
|
| 38 |
+
|
| 39 |
+
ctx.strokeStyle = "#c9d9df";
|
| 40 |
+
ctx.lineWidth = 1;
|
| 41 |
+
|
| 42 |
+
for (let z = -10; z <= 10; z += 2) {
|
| 43 |
+
const x = zToX(z);
|
| 44 |
+
ctx.beginPath();
|
| 45 |
+
ctx.moveTo(x, top);
|
| 46 |
+
ctx.lineTo(x, bottom);
|
| 47 |
+
ctx.stroke();
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
for (let y = 0; y <= 1.001; y += 0.1) {
|
| 51 |
+
const py = yToCanvas(y);
|
| 52 |
+
ctx.beginPath();
|
| 53 |
+
ctx.moveTo(left, py);
|
| 54 |
+
ctx.lineTo(right, py);
|
| 55 |
+
ctx.stroke();
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
ctx.strokeStyle = "#14343f";
|
| 59 |
+
ctx.lineWidth = 1.4;
|
| 60 |
+
|
| 61 |
+
ctx.beginPath();
|
| 62 |
+
ctx.moveTo(left, yToCanvas(0));
|
| 63 |
+
ctx.lineTo(right, yToCanvas(0));
|
| 64 |
+
ctx.stroke();
|
| 65 |
+
|
| 66 |
+
ctx.beginPath();
|
| 67 |
+
ctx.moveTo(zToX(0), top);
|
| 68 |
+
ctx.lineTo(zToX(0), bottom);
|
| 69 |
+
ctx.stroke();
|
| 70 |
+
|
| 71 |
+
ctx.fillStyle = "#14343f";
|
| 72 |
+
ctx.font = "13px 'Avenir Next', sans-serif";
|
| 73 |
+
|
| 74 |
+
for (let z = -10; z <= 10; z += 2) {
|
| 75 |
+
ctx.fillText(String(z), zToX(z) - 8, yToCanvas(0) + 19);
|
| 76 |
+
}
|
| 77 |
+
for (let y = 0; y <= 1.001; y += 0.2) {
|
| 78 |
+
ctx.fillText(y.toFixed(1), left - 40, yToCanvas(y) + 4);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
ctx.fillText("z", right - 12, yToCanvas(0) + 38);
|
| 82 |
+
ctx.fillText("y", zToX(0) + 12, top + 12);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
function drawSigmoid() {
|
| 86 |
+
ctx.strokeStyle = "#0a8f7b";
|
| 87 |
+
ctx.lineWidth = 3;
|
| 88 |
+
ctx.beginPath();
|
| 89 |
+
|
| 90 |
+
const steps = 600;
|
| 91 |
+
for (let i = 0; i <= steps; i += 1) {
|
| 92 |
+
const z = Z_MIN + (i / steps) * (Z_MAX - Z_MIN);
|
| 93 |
+
const y = sigmoid(z);
|
| 94 |
+
const x = zToX(z);
|
| 95 |
+
const py = yToCanvas(y);
|
| 96 |
+
if (i === 0) ctx.moveTo(x, py);
|
| 97 |
+
else ctx.lineTo(x, py);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
ctx.stroke();
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
function drawHalfLine() {
|
| 104 |
+
ctx.strokeStyle = "#d8534f";
|
| 105 |
+
ctx.lineWidth = 1.3;
|
| 106 |
+
ctx.setLineDash([7, 5]);
|
| 107 |
+
ctx.beginPath();
|
| 108 |
+
ctx.moveTo(MARGIN.left, yToCanvas(0.5));
|
| 109 |
+
ctx.lineTo(canvas.width - MARGIN.right, yToCanvas(0.5));
|
| 110 |
+
ctx.stroke();
|
| 111 |
+
ctx.setLineDash([]);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
function drawProbe(z) {
|
| 115 |
+
const y = sigmoid(z);
|
| 116 |
+
const px = zToX(z);
|
| 117 |
+
const py = yToCanvas(y);
|
| 118 |
+
|
| 119 |
+
ctx.strokeStyle = "#14343f";
|
| 120 |
+
ctx.setLineDash([4, 4]);
|
| 121 |
+
ctx.beginPath();
|
| 122 |
+
ctx.moveTo(px, yToCanvas(0));
|
| 123 |
+
ctx.lineTo(px, py);
|
| 124 |
+
ctx.stroke();
|
| 125 |
+
ctx.setLineDash([]);
|
| 126 |
+
|
| 127 |
+
ctx.fillStyle = "#14343f";
|
| 128 |
+
ctx.beginPath();
|
| 129 |
+
ctx.arc(px, py, 6, 0, Math.PI * 2);
|
| 130 |
+
ctx.fill();
|
| 131 |
+
|
| 132 |
+
if (showFormula.checked) {
|
| 133 |
+
ctx.font = "13px 'Avenir Next', sans-serif";
|
| 134 |
+
ctx.fillText(`z=${z.toFixed(2)}, y=${y.toFixed(4)}`, px + 8, py - 8);
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
function renderStats(z) {
|
| 139 |
+
const y = sigmoid(z);
|
| 140 |
+
const inverse = Math.log(y / (1 - y));
|
| 141 |
+
|
| 142 |
+
stats.innerHTML = `
|
| 143 |
+
<span>Function: y = 1 / (1 + np.exp(-z))</span>
|
| 144 |
+
<span>Current z: ${z.toFixed(2)}</span>
|
| 145 |
+
<span>Output y: ${y.toFixed(4)}</span>
|
| 146 |
+
<span>Cross-check logit(y): ${inverse.toFixed(4)}</span>
|
| 147 |
+
`;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
function render() {
|
| 151 |
+
const z = Number(zSlider.value);
|
| 152 |
+
zValue.textContent = z.toFixed(2);
|
| 153 |
+
|
| 154 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 155 |
+
drawAxes();
|
| 156 |
+
drawSigmoid();
|
| 157 |
+
if (showHalf.checked) drawHalfLine();
|
| 158 |
+
drawProbe(z);
|
| 159 |
+
renderStats(z);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
function stopAnimation() {
|
| 163 |
+
if (timer) {
|
| 164 |
+
clearInterval(timer);
|
| 165 |
+
timer = null;
|
| 166 |
+
}
|
| 167 |
+
animateBtn.textContent = "Animate z";
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
function startAnimation() {
|
| 171 |
+
if (timer) return;
|
| 172 |
+
animateBtn.textContent = "Stop Animation";
|
| 173 |
+
|
| 174 |
+
timer = setInterval(() => {
|
| 175 |
+
let next = Number(zSlider.value) + direction * 0.18;
|
| 176 |
+
if (next >= Z_MAX) {
|
| 177 |
+
next = Z_MAX;
|
| 178 |
+
direction = -1;
|
| 179 |
+
} else if (next <= Z_MIN) {
|
| 180 |
+
next = Z_MIN;
|
| 181 |
+
direction = 1;
|
| 182 |
+
}
|
| 183 |
+
zSlider.value = next;
|
| 184 |
+
render();
|
| 185 |
+
}, 30);
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
function reset() {
|
| 189 |
+
zSlider.value = 0;
|
| 190 |
+
showHalf.checked = true;
|
| 191 |
+
showFormula.checked = true;
|
| 192 |
+
stopAnimation();
|
| 193 |
+
render();
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
zSlider.addEventListener("input", render);
|
| 197 |
+
showHalf.addEventListener("input", render);
|
| 198 |
+
showFormula.addEventListener("input", render);
|
| 199 |
+
resetBtn.addEventListener("click", reset);
|
| 200 |
+
animateBtn.addEventListener("click", () => {
|
| 201 |
+
if (timer) stopAnimation();
|
| 202 |
+
else startAnimation();
|
| 203 |
+
});
|
| 204 |
+
|
| 205 |
+
reset();
|
logistic-regression/styles.css
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--bg: #f4f7f8;
|
| 3 |
+
--panel: #ffffff;
|
| 4 |
+
--ink: #13272f;
|
| 5 |
+
--muted: #597179;
|
| 6 |
+
--accent: #0a8f7b;
|
| 7 |
+
--accent-2: #f2b84b;
|
| 8 |
+
--danger: #d8534f;
|
| 9 |
+
--line: #e1ebee;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
* {
|
| 13 |
+
box-sizing: border-box;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
body {
|
| 17 |
+
margin: 0;
|
| 18 |
+
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
| 19 |
+
color: var(--ink);
|
| 20 |
+
background:
|
| 21 |
+
radial-gradient(circle at 20% 10%, #d8f4ef 0%, transparent 35%),
|
| 22 |
+
radial-gradient(circle at 90% 90%, #fff4da 0%, transparent 30%),
|
| 23 |
+
var(--bg);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.site-header {
|
| 27 |
+
padding: 1.2rem 1.6rem;
|
| 28 |
+
border-bottom: 1px solid var(--line);
|
| 29 |
+
background: rgba(255, 255, 255, 0.9);
|
| 30 |
+
backdrop-filter: blur(4px);
|
| 31 |
+
display: flex;
|
| 32 |
+
gap: 1rem;
|
| 33 |
+
justify-content: space-between;
|
| 34 |
+
align-items: end;
|
| 35 |
+
flex-wrap: wrap;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.kicker {
|
| 39 |
+
margin: 0;
|
| 40 |
+
text-transform: uppercase;
|
| 41 |
+
letter-spacing: 0.06em;
|
| 42 |
+
font-size: 0.72rem;
|
| 43 |
+
color: var(--muted);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
h1 {
|
| 47 |
+
margin: 0.1rem 0;
|
| 48 |
+
font-size: 1.4rem;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
h2 {
|
| 52 |
+
margin-top: 0;
|
| 53 |
+
font-size: 1.1rem;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
nav {
|
| 57 |
+
display: flex;
|
| 58 |
+
gap: 0.5rem;
|
| 59 |
+
flex-wrap: wrap;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
nav a {
|
| 63 |
+
text-decoration: none;
|
| 64 |
+
color: var(--ink);
|
| 65 |
+
border: 1px solid var(--line);
|
| 66 |
+
padding: 0.45rem 0.7rem;
|
| 67 |
+
border-radius: 999px;
|
| 68 |
+
font-size: 0.9rem;
|
| 69 |
+
background: #fff;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
nav a.active {
|
| 73 |
+
border-color: var(--accent);
|
| 74 |
+
color: var(--accent);
|
| 75 |
+
font-weight: 600;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.layout {
|
| 79 |
+
display: grid;
|
| 80 |
+
grid-template-columns: minmax(260px, 340px) 1fr;
|
| 81 |
+
gap: 1rem;
|
| 82 |
+
padding: 1rem;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.panel {
|
| 86 |
+
background: var(--panel);
|
| 87 |
+
border: 1px solid var(--line);
|
| 88 |
+
border-radius: 14px;
|
| 89 |
+
padding: 1rem;
|
| 90 |
+
box-shadow: 0 12px 20px rgba(14, 33, 41, 0.06);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.controls label,
|
| 94 |
+
.controls select,
|
| 95 |
+
.controls input,
|
| 96 |
+
.controls button {
|
| 97 |
+
display: block;
|
| 98 |
+
width: 100%;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.controls label {
|
| 102 |
+
margin-top: 0.7rem;
|
| 103 |
+
margin-bottom: 0.25rem;
|
| 104 |
+
font-size: 0.9rem;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.controls input[type="range"] {
|
| 108 |
+
accent-color: var(--accent);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.button-row {
|
| 112 |
+
margin-top: 0.8rem;
|
| 113 |
+
display: flex;
|
| 114 |
+
gap: 0.6rem;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
button {
|
| 118 |
+
border: 1px solid var(--line);
|
| 119 |
+
background: #fff;
|
| 120 |
+
color: var(--ink);
|
| 121 |
+
border-radius: 9px;
|
| 122 |
+
padding: 0.5rem;
|
| 123 |
+
font-weight: 600;
|
| 124 |
+
cursor: pointer;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
button:hover {
|
| 128 |
+
border-color: var(--accent);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
select {
|
| 132 |
+
padding: 0.45rem;
|
| 133 |
+
border-radius: 8px;
|
| 134 |
+
border: 1px solid var(--line);
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.checkboxes {
|
| 138 |
+
margin-top: 0.8rem;
|
| 139 |
+
display: grid;
|
| 140 |
+
gap: 0.3rem;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.checkboxes label {
|
| 144 |
+
display: flex;
|
| 145 |
+
align-items: center;
|
| 146 |
+
gap: 0.5rem;
|
| 147 |
+
margin: 0;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.chart-panel canvas {
|
| 151 |
+
width: 100%;
|
| 152 |
+
height: auto;
|
| 153 |
+
border: 1px solid var(--line);
|
| 154 |
+
border-radius: 10px;
|
| 155 |
+
background: #fff;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.stats {
|
| 159 |
+
margin-top: 0.8rem;
|
| 160 |
+
display: grid;
|
| 161 |
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
| 162 |
+
gap: 0.55rem;
|
| 163 |
+
font-size: 0.9rem;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.stats span {
|
| 167 |
+
display: block;
|
| 168 |
+
border: 1px solid var(--line);
|
| 169 |
+
border-radius: 8px;
|
| 170 |
+
padding: 0.45rem 0.6rem;
|
| 171 |
+
background: #fcfeff;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.hint {
|
| 175 |
+
margin: 0.8rem 0 0;
|
| 176 |
+
color: var(--muted);
|
| 177 |
+
font-size: 0.9rem;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.page-copy {
|
| 181 |
+
max-width: 760px;
|
| 182 |
+
margin: 1.2rem auto;
|
| 183 |
+
line-height: 1.6;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
@media (max-width: 980px) {
|
| 187 |
+
.layout {
|
| 188 |
+
grid-template-columns: 1fr;
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.home-page {
|
| 193 |
+
margin-top: 1rem;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.home-grid {
|
| 197 |
+
margin-top: 1rem;
|
| 198 |
+
display: grid;
|
| 199 |
+
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
|
| 200 |
+
gap: 0.9rem;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.home-card {
|
| 204 |
+
border: 1px solid var(--line);
|
| 205 |
+
border-radius: 12px;
|
| 206 |
+
padding: 0.9rem;
|
| 207 |
+
background: #fcfeff;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.home-card h3 {
|
| 211 |
+
margin: 0;
|
| 212 |
+
font-size: 1.05rem;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.home-card p {
|
| 216 |
+
margin: 0.45rem 0 0.9rem;
|
| 217 |
+
color: var(--muted);
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.cta-button {
|
| 221 |
+
display: inline-block;
|
| 222 |
+
text-decoration: none;
|
| 223 |
+
border: 1px solid var(--accent);
|
| 224 |
+
color: #fff;
|
| 225 |
+
background: var(--accent);
|
| 226 |
+
border-radius: 9px;
|
| 227 |
+
padding: 0.5rem 0.7rem;
|
| 228 |
+
font-weight: 600;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.cta-button.secondary {
|
| 232 |
+
background: #fff;
|
| 233 |
+
color: var(--ink);
|
| 234 |
+
border-color: var(--line);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.home-card.muted {
|
| 238 |
+
background: #f8fafb;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.coming-soon {
|
| 242 |
+
color: var(--muted);
|
| 243 |
+
font-size: 0.9rem;
|
| 244 |
+
font-weight: 600;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.confusion-panel h3 {
|
| 248 |
+
margin: 1rem 0 0.45rem;
|
| 249 |
+
font-size: 1rem;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.exercise-table {
|
| 253 |
+
width: 100%;
|
| 254 |
+
border-collapse: collapse;
|
| 255 |
+
margin: 0.45rem 0 1rem;
|
| 256 |
+
font-size: 0.9rem;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.exercise-table th,
|
| 260 |
+
.exercise-table td {
|
| 261 |
+
border: 1px solid var(--line);
|
| 262 |
+
padding: 0.45rem 0.5rem;
|
| 263 |
+
text-align: left;
|
| 264 |
+
vertical-align: middle;
|
| 265 |
+
background: #fff;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.exercise-table th {
|
| 269 |
+
background: #f6fbfc;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.exercise-table input,
|
| 273 |
+
.exercise-table select {
|
| 274 |
+
width: 100%;
|
| 275 |
+
padding: 0.4rem;
|
| 276 |
+
border: 1px solid var(--line);
|
| 277 |
+
border-radius: 6px;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
.worked-tag {
|
| 281 |
+
color: var(--muted);
|
| 282 |
+
font-size: 0.82rem;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.solution-block {
|
| 286 |
+
margin-top: 1rem;
|
| 287 |
+
border-top: 1px dashed var(--line);
|
| 288 |
+
padding-top: 0.8rem;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.solution-block h4 {
|
| 292 |
+
margin: 0.8rem 0 0.35rem;
|
| 293 |
+
}
|