วิทยาลัยนานาชาตินวัตกรรมดิจิทัล มหาวิทยาลัยเชียงใหม่
14 พฤศจิกายน 2568
อัลกอริธึม k-Nearest Neighbors (k-NN) ถูกพัฒนาโดย Fix และ Hodges ในปี 1951 ในงานวิจัยของพวกเขาเกี่ยวกับการจำแนกประเภทรูปแบบ (Pattern Classification) ที่มหาวิทยาลัย California, Berkeley
อย่างไรก็ตาม อัลกอริธึมนี้ได้รับความนิยมอย่างกว้างขวางหลังจาก Thomas Cover และ Peter Hart ได้เผยแพร่งานวิจัยในปี 1967 ซึ่งแสดงให้เห็นว่า k-NN เป็นวิธีที่มีประสิทธิภาพสำหรับการจำแนกประเภท
การจำแนกประเภท (Classification)
การถดถอย (Regression)
โดยอาศัยหลักการของ “จุดที่ใกล้กันมักมีคุณสมบัติคล้ายกัน”
กำหนดจำนวน k (จำนวนเพื่อนบ้านที่ใกล้ที่สุด)
วัดระยะห่างระหว่างจุดข้อมูลใหม่กับจุดข้อมูลในชุดฝึก (Training Data)
เลือก k จุดข้อมูลที่ใกล้ที่สุด
สำหรับ Classification: ดูว่าหมวดหมู่ใดมีมากที่สุดใน k จุด → กำหนดให้เป็นของกลุ่มนั้น
สำหรับ Regression: คำนวณค่าเฉลี่ยของ k จุดที่ใกล้ที่สุด → ใช้เป็นค่าพยากรณ์
Euclidean Distance
\[ d = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2} \]
(นิยมใช้มากที่สุด)
Manhattan Distance
\[ d = |x_2 - x_1| + |y_2 - y_1| \]
จุดเด่น
\(~~~\)✅ เข้าใจง่าย และใช้งานง่าย
\(~~~\)✅ ใช้ได้ทั้ง Classification และ Regression
\(~~~\)✅ ไม่ต้องมีการ Train Model (Lazy Learning)
จุดด้อย
\(~~~\)❌ ทำงานช้าเมื่อข้อมูลมีขนาดใหญ่ (ต้องคำนวณระยะทางกับทุกจุด)
\(~~~\)❌ อ่อนไหวต่อค่าผิดปกติ (Outliers)
\(~~~\)❌ ต้องการการปรับค่าพารามิเตอร์ k ให้เหมาะสม
สมมติว่าเรามีข้อมูล 8 จุด โดยแต่ละจุดมีค่าพิกัด (X, Y) และกลุ่มที่กำหนดไว้ล่วงหน้า (A หรือ B)
| จุด | X | Y | กลุ่ม |
|---|---|---|---|
| 1 | 2 | 4 | A |
| 2 | 4 | 6 | A |
| 3 | 4 | 2 | A |
| 4 | 6 | 5 | A |
| 5 | 7 | 3 | B |
| 6 | 8 | 6 | B |
| 7 | 9 | 2 | B |
| 8 | 10 | 5 | B |
และเรามี จุดทดสอบ (X = 6, Y = 3) ที่เราต้องการจำแนกว่าอยู่ในกลุ่ม A หรือ B โดยใช้ k-NN
1. คำนวณระยะทางระหว่างจุดทดสอบกับทุกจุดใน dataset
ใช้ ระยะทางแบบยุคลิด (Euclidean Distance) \[ d = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2} \]
| จุด | X | Y | กลุ่ม | ระยะทางจาก (6,3) |
|---|---|---|---|---|
| 1 | 2 | 4 | A | \(\sqrt{(6-2)^2 + (3-4)^2} = \sqrt{16 + 1} = \sqrt{17} \approx 4.12\) |
| 2 | 4 | 6 | A | \(\sqrt{(6-4)^2 + (3-6)^2} = \sqrt{4 + 9} = \sqrt{13} \approx 3.61\) |
| 3 | 4 | 2 | A | \(\sqrt{(6-4)^2 + (3-2)^2} = \sqrt{4 + 1} = \sqrt{5} \approx 2.24\) |
| 4 | 6 | 5 | A | \(\sqrt{(6-6)^2 + (3-5)^2} = \sqrt{0 + 4} = \sqrt{4} = 2.00\) |
| 5 | 7 | 3 | B | \(\sqrt{(6-7)^2 + (3-3)^2} = \sqrt{1 + 0} = \sqrt{1} = 1.00\) |
| 6 | 8 | 6 | B | \(\sqrt{(6-8)^2 + (3-6)^2} = \sqrt{4 + 9} = \sqrt{13} \approx 3.61\) |
| 7 | 9 | 2 | B | \(\sqrt{(6-9)^2 + (3-2)^2} = \sqrt{9 + 1} = \sqrt{10} \approx 3.16\) |
| 8 | 10 | 5 | B | \(\sqrt{(6-10)^2 + (3-5)^2} = \sqrt{16 + 4} = \sqrt{20} \approx 4.47\) |
2. เลือกค่า k และหาจุดที่ใกล้ที่สุด
สมมติว่าเลือก k = 3 (ดู 3 จุดที่ใกล้ที่สุด)
| จุดที่ใกล้ที่สุด | ระยะทาง | กลุ่ม |
|---|---|---|
| 5 (7,3) | 1.00 | B |
| 4 (6,5) | 2.00 | A |
| 3 (4,2) | 2.24 | A |
3. นับคะแนนเสียงจาก k จุดที่ใกล้ที่สุด
A = 2 (จากจุด 4 และ 3)
B = 1 (จากจุด 5)
กลุ่มที่มีจำนวนมากที่สุดคือ A
4. ตัดสินผลลัพธ์
เนื่องจากส่วนใหญ่เป็น A เราจึงจำแนกจุด (6,3) ว่าเป็น กลุ่ม A
| k-NN | การทำงาน |
|---|---|
| ✅ ใช้ได้ทั้ง Classification & Regression | ใช้ k เพื่อนบ้านที่ใกล้ที่สุดในการทำนาย |
| ✅ ไม่ต้อง Train Model (Lazy Learning) | ทำงานแบบ Instance-Based |
| ✅ เหมาะกับข้อมูลขนาดเล็ก-กลาง | อาจช้าเมื่อข้อมูลมีขนาดใหญ่ |
| ✅ การเลือกค่า k สำคัญมาก (ต้องเป็นเลขคี่) | k เล็กไป = Overfitting,\(~~~~~~~~\) k ใหญ่ไป = Underfitting |
เราจะสร้างจุดข้อมูลใหม่ (สีดำ) แล้วใช้ k-NN ทำนายว่าจุดนี้ควรถูกจัดอยู่ในกลุ่มใด
เพื่อให้เห็นว่า k-NN จำแนกข้อมูลอย่างไร เราจะสร้าง decision boundary (เส้นแบ่งระหว่างกลุ่ม)
📌 ผลลัพธ์:
พื้นที่ของแต่ละคลาสถูกระบายสีเบาๆ ตาม k-NN
จุดข้อมูลแต่ละกลุ่มยังคงมีสีชัดเจน
แสดงให้เห็นว่าค่า k ที่ต่างกันจะส่งผลต่อการแบ่งพื้นที่
1. การตลาดและการแนะนำสินค้า (Marketing & Recommendation Systems)
✅ การแนะนำสินค้าให้ลูกค้า (Product Recommendation)
ใช้ k-NN เพื่อแนะนำสินค้าให้ลูกค้าโดยดูจากลูกค้าที่มีพฤติกรรมการซื้อคล้ายกัน
ตัวอย่าง: Amazon, Shopee, Lazada ใช้ k-NN เพื่อแนะนำสินค้าที่ลูกค้าสนใจ
✅ การแบ่งกลุ่มลูกค้า (Customer Segmentation)
ใช้ k-NN เพื่อจัดกลุ่มลูกค้าตามลักษณะพฤติกรรมการซื้อหรือข้อมูลประชากร
ตัวอย่าง: ธนาคารสามารถใช้ k-NN ในการแบ่งกลุ่มลูกค้าเพื่อนำเสนอสินเชื่อหรือบัตรเครดิต
2. การวิเคราะห์สินเชื่อและความเสี่ยง (Credit Risk Analysis)
✅ การพิจารณาให้สินเชื่อ (Loan Approval)
ใช้ k-NN วิเคราะห์ว่าลูกค้าควรได้รับอนุมัติสินเชื่อหรือไม่ โดยพิจารณาข้อมูลลูกค้าเก่าที่มีคุณสมบัติคล้ายกัน
ตัวอย่าง: ธนาคารใช้ k-NN เพื่อประเมินความเสี่ยงก่อนอนุมัติสินเชื่อ
✅ การตรวจจับการฉ้อโกงทางการเงิน (Fraud Detection)
ใช้ k-NN ตรวจสอบว่าธุรกรรมที่เกิดขึ้นผิดปกติหรือไม่ โดยเปรียบเทียบกับธุรกรรมที่ผ่านมา
ตัวอย่าง: บัตรเครดิต VISA หรือ MasterCard ใช้ k-NN เพื่อตรวจสอบธุรกรรมที่อาจเป็นการโกง
3. การวิเคราะห์ตลาดหุ้นและเศรษฐศาสตร์ (Stock Market & Economics)
✅ การทำนายราคาหุ้น (Stock Price Prediction)
ใช้ k-NN ทำนายแนวโน้มของราคาหุ้นโดยเปรียบเทียบกับรูปแบบราคาหุ้นในอดีต
ตัวอย่าง: นักลงทุนใช้ k-NN วิเคราะห์แนวโน้มของหุ้นเพื่อตัดสินใจลงทุน
✅ การวิเคราะห์แนวโน้มเศรษฐกิจ (Economic Trend Analysis)
ใช้ k-NN วิเคราะห์ตัวชี้วัดทางเศรษฐกิจ เช่น GDP, อัตราเงินเฟ้อ, การจ้างงาน เพื่อตรวจจับแนวโน้มในอนาคต
ตัวอย่าง: นักเศรษฐศาสตร์ใช้ k-NN เพื่อวิเคราะห์ว่าเศรษฐกิจจะเข้าสู่ภาวะถดถอยหรือไม่
4. การแพทย์และสุขภาพ (Healthcare & Medical Diagnosis)
✅ การวินิจฉัยโรค (Disease Diagnosis)
ใช้ k-NN เพื่อวิเคราะห์ข้อมูลผู้ป่วยและวินิจฉัยว่าเป็นโรคอะไร โดยเปรียบเทียบกับผู้ป่วยที่มีอาการคล้ายกัน
ตัวอย่าง: ใช้ k-NN ทำนายว่าผู้ป่วยมีโอกาสเป็นโรคเบาหวานหรือโรคหัวใจหรือไม่
✅ การจำแนกประเภทของเซลล์มะเร็ง (Cancer Detection)
ใช้ k-NN แยกแยะว่าเซลล์เนื้องอกเป็นมะเร็งหรือไม่ โดยเปรียบเทียบกับข้อมูลเซลล์ที่มีอยู่
ตัวอย่าง: ใช้ k-NN วิเคราะห์ผลตรวจชิ้นเนื้อจากผู้ป่วย
5. การจำแนกประเภทของภาพและเสียง (Image & Speech Recognition)
✅ การรู้จำลายมือและตัวอักษร (Handwriting Recognition)
ใช้ k-NN จำแนกตัวอักษรในลายมือหรือเอกสารที่สแกนมา
ตัวอย่าง: ระบบ OCR (Optical Character Recognition) ใช้ k-NN ในการแปลงภาพเป็นข้อความ
✅ การจดจำเสียงและการรู้จำคำพูด (Speech Recognition)
ใช้ k-NN วิเคราะห์เสียงพูดและจำแนกออกเป็นคำศัพท์ต่างๆ
ตัวอย่าง: ระบบผู้ช่วยอัจฉริยะ เช่น Siri และ Google Assistant ใช้ k-NN ในการจดจำเสียงของผู้ใช้
Kramer, O. (2016). K-nearest neighbors. Springer. https://doi.org/10.1007/978-3-319-31226-8_8
Brownlee, J. (2016, August 22). A gentle introduction to k-nearest neighbors algorithm. Machine Learning Mastery. https://machinelearningmastery.com/k-nearest-neighbors-for-machine-learning
(() => {
// ===== Layout =====
const box = html`<div style="max-width:1180px;font:14px system-ui;color:#0f172a;"> <div id="ctrl" style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-bottom:10px;
border:1px solid #cbd5e1;border-radius:12px;padding:10px;background:#f8fafc"></div> <div id="chart"></div> <div id="legend" style="font-size:12px;color:#475569;margin-top:8px"></div>
</div>`;
const ctrl = box.querySelector("#ctrl");
// ===== Controls =====
const seed = Inputs.number({label:"Seed", value:42, step:1, min:0, max:9999});
const datasetSel = Inputs.select(["Blobs","Two Moons"], {label:"Dataset"});
const nPerCls = Inputs.range([20, 200], {label:"Points/Class", value:80, step:5});
const noiseMoon = Inputs.range([0, 0.6], {label:"Noise (Moons)", value:0.15, step:0.05});
const kVal = Inputs.range([1, 41], {label:"k", value:7, step:2});
const metricSel = Inputs.select(["Euclidean","Manhattan","Minkowski(p)"], {label:"Metric"});
const pMink = Inputs.range([1, 6], {label:"p (Minkowski)", value:3, step:1});
const qx = Inputs.range([-5, 5], {label:"Query x", value:0, step:0.2});
const qy = Inputs.range([-5, 5], {label:"Query y", value:0, step:0.2});
const resolution = Inputs.range([25, 120], {label:"Voronoi Grid (res)", value:60, step:5});
const regenBtn = Inputs.button("🔄 Regenerate");
ctrl.append(seed, datasetSel, nPerCls, noiseMoon, kVal, metricSel, pMink, qx, qy, resolution, regenBtn);
// ===== Utils =====
const rand = (s)=>{let t=(s>>>0)||1; return ()=> (t=(1664525*t+1013904223)>>>0)/2**32;};
const gauss = (r)=>{let u=1-r(), v=1-r(); return Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v);};
function makeBlobs(nEach, rng){
const out=[], c1={mx:-2.2,my:-1.0}, c2={mx:2.0,my:1.2};
for(let i=0;i<nEach;i++){
out.push({x:c1.mx+gauss(rng), y:c1.my+gauss(rng), c:0});
out.push({x:c2.mx+gauss(rng), y:c2.my+gauss(rng), c:1});
}
return out;
}
function shuffle(arr, rng){
for(let i=arr.length-1;i>0;i++){
const j=Math.floor(rng()*(i+1)); [arr[i],arr[j]]=[arr[j],arr[i]];
}
return arr;
}
function makeTwoMoons(nTotal, noise, rng){
const m=Math.floor(nTotal/2), out=[];
for(let i=0;i<m;i++){
const t=Math.PI*rng(); out.push({x:Math.cos(t)+noise*gauss(rng), y:Math.sin(t)+noise*gauss(rng), c:0});
}
for(let i=0;i<m;i++){
const t=Math.PI*rng(); out.push({x:1-Math.cos(t)+noise*gauss(rng), y:-Math.sin(t)+0.5+noise*gauss(rng), c:1});
}
for(const d of out){ d.x=d.x*3-1.5; d.y=d.y*3-1.2; }
if(nTotal%2===1) out.push({x:-1.5+noise*gauss(rng), y:1.8+noise*gauss(rng), c:0});
return shuffle(out, rng);
}
function dist(a,b,metric,p){
const dx=Math.abs(a.x-b.x), dy=Math.abs(a.y-b.y);
if(metric==="Euclidean") return Math.hypot(dx,dy);
if(metric==="Manhattan") return dx+dy;
return (dx**p+dy**p)**(1/p);
}
function knn(q,data,k,metric,p){
const arr=data.map(d=>({d, r:dist(q,d,metric,p)})).sort((u,v)=>u.r-v.r);
const top=arr.slice(0,Math.max(1,Math.min(k,data.length)));
let v0=0,v1=0; for(const t of top){ t.d.c===0?v0++:v1++; }
if(v0===v1) return top[0].d.c; // tie-break by nearest
return v1>v0?1:0;
}
function extent(data,pad=0.8){
const xs=data.map(d=>d.x), ys=data.map(d=>d.y);
return {
xmin:Math.min(...xs)-pad, xmax:Math.max(...xs)+pad,
ymin:Math.min(...ys)-pad, ymax:Math.max(...ys)+pad
};
}
// ===== Data & Render =====
let data=[];
const rebuild=()=>{
const r=rand(seed.value+(regenBtn.value||0)+nPerCls.value*17);
data = datasetSel.value==="Blobs"
? makeBlobs(nPerCls.value, r)
: makeTwoMoons(Math.max(2, nPerCls.value*2), noiseMoon.value, r);
};
const chart=box.querySelector("#chart"), legend=box.querySelector("#legend");
function render(){
if(chart._seed!==seed.value||chart._regen!==regenBtn.value||
chart._n!==nPerCls.value||chart._ds!==datasetSel.value||chart._noise!==noiseMoon.value){
rebuild();
chart._seed=seed.value; chart._regen=regenBtn.value;
chart._n=nPerCls.value; chart._ds=datasetSel.value; chart._noise=noiseMoon.value;
}
// ---- Build Voronoi background from grid of predicted classes ----
const {xmin,xmax,ymin,ymax}=extent(data);
const nx=resolution.value, ny=resolution.value;
const cells=[];
for(let i=0;i<nx;i++){
const x=xmin+i*(xmax-xmin)/(nx-1);
for(let j=0;j<ny;j++){
const y=ymin+j*(ymax-ymin)/(ny-1);
const c=knn({x,y}, data, kVal.value, metricSel.value, pMink.value);
cells.push({x,y,c});
}
}
const q={x:qx.value,y:qy.value};
const yhat=knn(q,data,kVal.value,metricSel.value,pMink.value);
const qColor=yhat===0?"#ef4444":"#3b82f6";
chart.innerHTML="";
const fig=Plot.plot({
width:1180, height:560, grid:true,
x:{label:"x"}, y:{label:"y"},
color:{domain:[0,1],range:["#fecaca","#bfdbfe"]},
marks:[
// 🔷 Polygonal background (Voronoi cells)
Plot.voronoi(cells, {x:"x", y:"y", fill:"c", stroke:null, fillOpacity:0.35}),
// Data points
Plot.dot(data,{x:"x",y:"y",r:3,fill:d=>d.c===0?"#ef4444":"#3b82f6"}),
// Query point
Plot.dot([q],{x:"x",y:"y",r:8,stroke:qColor,fill:qColor,symbol:"star"})
]
});
chart.append(fig);
legend.innerHTML = `
Dataset: <b>${datasetSel.value}</b> |
k=<b>${kVal.value}</b> |
Metric=<b>${metricSel.value}${metricSel.value==="Minkowski(p)"?` (p=${pMink.value})`:""}</b> |
Query=(${q.x.toFixed(2)}, ${q.y.toFixed(2)}) →
<b style="color:${qColor}">Class ${yhat}</b>`;
}
for (const el of [seed,datasetSel,nPerCls,noiseMoon,kVal,metricSel,pMink,qx,qy,resolution,regenBtn])
el.addEventListener?.("input", render);
render();
return box;
})()(() => {
// ============ Layout ============
const box = html`<div style="max-width:1180px;font:14px system-ui;color:#0f172a;"> <div id="ctrl" style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-bottom:10px;
border:1px solid #cbd5e1;border-radius:12px;padding:10px;background:#f8fafc"></div> <div style="display:grid;grid-template-columns:1fr 340px;gap:12px;align-items:start"> <div id="chart"></div> <div id="panel" style="border:1px solid #e2e8f0;border-radius:12px;padding:10px;background:#ffffff"> <div style="font-weight:600;margin-bottom:6px">kNN — Step-by-Step (Multi-class)</div> <div id="note" style="font-size:12px;color:#64748b;margin-bottom:6px"></div> <div id="steptext" style="font-size:13px;color:#334155;margin-bottom:8px"></div> <div id="distlist" style="font-family:ui-monospace, SFMono-Regular, Menlo, monospace; font-size:12px; white-space:pre; overflow:auto; max-height:360px;"></div> </div> </div> <div id="legend" style="font-size:12px;color:#475569;margin-top:8px"></div>
</div>`;
const ctrl = box.querySelector("#ctrl");
// ============ Controls ============
const seed = Inputs.number({label:"Seed", value:42, step:1, min:0, max:999999});
const datasetSel = Inputs.select(
["Blobs (C)", "Gaussian Grid (C)", "Concentric Rings (C)", "Spiral (C)", "Two Moons (2)"],
{label:"Dataset"}
);
const nPerCls = Inputs.range([10, 300], {label:"Points per class", value:60, step:10});
const nClasses = Inputs.range([2, 8], {label:"Number of classes (C)", value:3, step:1});
const noiseCtrl = Inputs.range([0, 0.6], {label:"Noise (for Rings/Spiral/Moons)", value:0.15, step:0.05});
const kVal = Inputs.range([1, 51], {label:"k (neighbors)", value:5, step:2});
const metricSel = Inputs.select(["Euclidean","Manhattan","Minkowski(p)"], {label:"Metric"});
const pMink = Inputs.range([1, 6], {label:"p (Minkowski)", value:3, step:1});
const qx = Inputs.range([-6, 6], {label:"Query x", value:0, step:0.2});
const qy = Inputs.range([-6, 6], {label:"Query y", value:0, step:0.2});
const step = Inputs.range([0, 4], {label:"Step (0–4)", value:0, step:1});
const regenBtn = Inputs.button("🔄 Regenerate");
ctrl.append(
seed, datasetSel, nPerCls, nClasses, noiseCtrl,
kVal, metricSel, pMink, qx, qy, step, regenBtn
);
// ============ Utils ============
const rand = (s)=>{let t=(s>>>0)||1; return ()=> (t=(1664525*t+1013904223)>>>0)/2**32;};
const gauss = (r)=>{let u=1-r(), v=1-r(); return Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v);};
// palette: ใช้ชุดสีคงที่ + เติมแบบ HSL หาก C เกิน
function makePalette(C){
const base = [
"#ef4444","#3b82f6","#22c55e","#f59e0b","#a855f7",
"#06b6d4","#e11d48","#10b981","#84cc16","#f97316"
];
if (C <= base.length) return base.slice(0,C);
const extra = Array.from({length: C - base.length}, (_,i)=>{
const h = (360 * (i+1) / (C - base.length + 1)) | 0;
return `hsl(${h} 70% 55%)`;
});
return base.concat(extra);
}
function dist(q,d,metric,p){
const dx=Math.abs(q.x-d.x), dy=Math.abs(q.y-d.y);
if(metric==="Euclidean") return Math.hypot(dx,dy);
if(metric==="Manhattan") return dx+dy;
return (dx**p+dy**p)**(1/p);
}
// ============ Generators (support multi-class) ============
function makeBlobsC(C, nEach, rng){
// วาง center เป็นวงกลม
const out=[]; const R=3.2;
for(let c=0; c<C; c++){
const ang = 2*Math.PI*c/C;
const mx = R*Math.cos(ang), my = R*Math.sin(ang);
for(let i=0;i<nEach;i++){
out.push({
x: mx + 0.9*gauss(rng), y: my + 0.9*gauss(rng),
c, id: `C${c+1}-${i+1}`
});
}
}
return out;
}
function makeGaussianGridC(C, nEach, rng){
// วาง center เป็นกริด sqrt(C) x sqrt(C)
const s = Math.ceil(Math.sqrt(C));
const gap = 3.0;
const x0 = -(s-1)*gap/2, y0 = -(s-1)*gap/2;
const out=[];
let idx=0;
for(let r=0;r<s;r++){
for(let col=0;col<s;col++){
if(idx>=C) break;
const mx = x0 + col*gap, my = y0 + r*gap;
for(let i=0;i<nEach;i++){
out.push({
x: mx + 0.8*gauss(rng), y: my + 0.8*gauss(rng),
c: idx, id: `C${idx+1}-${i+1}`
});
}
idx++;
}
}
return out;
}
function makeRingsC(C, nEach, noise, rng){
// วงแหวนรัศมีต่างกัน per class
const out=[]; const baseR=1.2, dr=1.0;
for(let c=0;c<C;c++){
const R = baseR + c*dr;
for(let i=0;i<nEach;i++){
const t = 2*Math.PI*rng();
const x = R*Math.cos(t) + noise*gauss(rng);
const y = R*Math.sin(t) + noise*gauss(rng);
out.push({x, y, c, id: `C${c+1}-${i+1}`});
}
}
return out;
}
function makeSpiralC(C, nEach, noise, rng){
// C เกลียว (arms)
const out=[]; const turns=2.25; const a=0.2; const b=0.8;
for(let c=0;c<C;c++){
for(let i=0;i<nEach;i++){
const t = (i / (nEach-1 + 1e-9)) * turns * 2*Math.PI; // 0..turns*2π
const phase = 2*Math.PI*c/C;
const r = a + b*t/(turns*2*Math.PI);
const x = r*Math.cos(t + phase) + noise*gauss(rng);
const y = r*Math.sin(t + phase) + noise*gauss(rng);
out.push({x, y, c, id:`C${c+1}-${i+1}`});
}
}
return out;
}
function makeTwoMoons2(nTotal, noise, rng){
// ตายตัว 2 คลาส
const m=Math.floor(nTotal/2), out=[];
for(let i=0;i<m;i++){
const t=Math.PI*rng();
out.push({x:Math.cos(t)+noise*gauss(rng), y:Math.sin(t)+noise*gauss(rng), c:0, id:`A${i+1}`});
}
for(let i=0;i<m;i++){
const t=Math.PI*rng();
out.push({x:1-Math.cos(t)+noise*gauss(rng), y:-Math.sin(t)+0.5+noise*gauss(rng), c:1, id:`B${i+1}`});
}
for(const d of out){ d.x=d.x*3-1.5; d.y=d.y*3-1.2; }
return out;
}
// ============ Data ============
let data=[], C=3, palette=makePalette(3);
const rebuild=()=>{
const rng = rand(seed.value + (regenBtn.value||0) + nPerCls.value*17 + nClasses.value*101);
// dataset-specific C handling
const ds = datasetSel.value;
C = (ds === "Two Moons (2)") ? 2 : nClasses.value;
palette = makePalette(C);
if (ds === "Blobs (C)"){
data = makeBlobsC(C, nPerCls.value, rng);
} else if (ds === "Gaussian Grid (C)") {
data = makeGaussianGridC(C, nPerCls.value, rng);
} else if (ds === "Concentric Rings (C)") {
data = makeRingsC(C, nPerCls.value, noiseCtrl.value, rng);
} else if (ds === "Spiral (C)") {
data = makeSpiralC(C, nPerCls.value, noiseCtrl.value, rng);
} else { // Two Moons (2)
data = makeTwoMoons2(Math.max(2, nPerCls.value*2), noiseCtrl.value, rng);
}
};
// ============ Render ============
const chart = box.querySelector("#chart");
const legend = box.querySelector("#legend");
const steptext = box.querySelector("#steptext");
const distlist = box.querySelector("#distlist");
const note = box.querySelector("#note");
function render(){
// refresh dataset triggers
if(chart._seed!==seed.value||chart._regen!==regenBtn.value||
chart._n!==nPerCls.value||chart._ds!==datasetSel.value||chart._noise!==noiseCtrl.value||
chart._cls!==nClasses.value){
rebuild();
chart._seed=seed.value; chart._regen=regenBtn.value;
chart._n=nPerCls.value; chart._ds=datasetSel.value; chart._noise=noiseCtrl.value;
chart._cls=nClasses.value;
}
// note
note.textContent = (datasetSel.value==="Two Moons (2)" && nClasses.value!==2)
? "Note: Two Moons รองรับ 2 คลาสเท่านั้น — ระบบกำลังใช้ C=2 ให้โดยอัตโนมัติ"
: "";
const q={x: qx.value, y: qy.value};
// distances & sort
const rows = data.map(d => ({
id:d.id, x:d.x, y:d.y, c:d.c,
r: dist(q, d, metricSel.value, pMink.value)
})).sort((a,b)=> a.r-b.r);
// neighbors & voting (multi-class)
const K = Math.max(1, Math.min(kVal.value, rows.length));
const nbrs = rows.slice(0, K);
const votes = Array.from({length:C}, ()=>0);
for(const z of nbrs) votes[z.c]++;
// winner + tie-break (closest among ties)
const maxVote = Math.max(...votes);
let winners = [];
for(let c=0;c<C;c++) if(votes[c]===maxVote) winners.push(c);
let yhat;
if (winners.length===1){
yhat = winners[0];
} else {
const firstIdx = nbrs.findIndex(z => winners.includes(z.c));
yhat = (firstIdx>=0 ? nbrs[firstIdx].c : 0);
}
const qColor = palette[yhat];
// steps text
const steps = [
"Step 0: แสดงข้อมูล + จุด Query (ยังไม่คำนวณ)",
"Step 1: คำนวณระยะทางจาก Query → ทุกจุด",
"Step 2: เรียงจากระยะทางน้อย → มาก",
`Step 3: เลือกเพื่อนบ้าน k = ${K} จุดที่ใกล้ที่สุด`,
"Step 4: นับคะแนนโหวต (multi-class) และสรุปผลทำนาย"
];
steptext.textContent = steps[step.value] || steps[0];
// table
const header = " id x y class dist\n----------------------------------------------\n";
const lines = rows.map((z, i)=> {
const mark = (i<K && step.value>=3) ? "★ " : " ";
return `${mark}${(z.id+" ").slice(0,10)} ${z.x.toFixed(2).padStart(7)} ${z.y.toFixed(2).padStart(8)} ${String(z.c).padStart(3)} ${z.r.toFixed(3).padStart(8)}`;
});
distlist.textContent = header + lines.join("\n");
// marks
const baseDots = Plot.dot(data, {
x:"x", y:"y", r:3,
fill: d => palette[d.c]
});
const queryDot = Plot.dot([q], {x:"x", y:"y", r:8, stroke:qColor, fill:qColor, symbol:"star"});
const maxLines = 220;
const distLines = (step.value>=1)
? Plot.link(rows.slice(0, Math.min(rows.length, maxLines)).map(z=>({x1:q.x,y1:q.y,x2:z.x,y2:z.y})),
{x1:"x1", y1:"y1", x2:"x2", y2:"y2", stroke:"#94a3b8", strokeOpacity:0.4})
: null;
const neighborDots = (step.value>=3)
? Plot.dot(nbrs, {x:"x", y:"y", r:6, stroke:"#0f172a", fill: z=> palette[z.c] + "cc"})
: null;
chart.innerHTML="";
const fig = Plot.plot({
width: 1180, height: 560, grid: true,
x:{label:"x"}, y:{label:"y"},
marks: [
baseDots,
...(distLines ? [distLines] : []),
...(neighborDots ? [neighborDots] : []),
queryDot
]
});
chart.append(fig);
// votes pretty
const votesText = votes.map((v,i)=> `C${i+1}=${v}`).join(", ");
legend.innerHTML = `
Dataset: <b>${datasetSel.value}</b> |
C = <b>${C}</b> | points/class = <b>${nPerCls.value}</b> |
k = <b>${K}</b> | Metric = <b>${metricSel.value}${metricSel.value==="Minkowski(p)"?` (p=${pMink.value})`:""}</b> |
Query = (${q.x.toFixed(2)}, ${q.y.toFixed(2)}) →
${step.value>=4 ? `Prediction: <b style="color:${qColor}">Class C${yhat+1}</b> | Votes: ${votesText}` : `Prediction: —`}
`;
}
for(const el of [seed,datasetSel,nPerCls,nClasses,noiseCtrl,kVal,metricSel,pMink,qx,qy,step,regenBtn]){
el.addEventListener?.("input", render);
el.addEventListener?.("change", render);
el.addEventListener?.("click", render);
}
render();
return box;
})()(async () => {
// โหลด Plotly แบบ async (ถูกต้อง)
await new Promise((resolve, reject) => {
if (globalThis.Plotly) return resolve();
const s = document.createElement("script");
s.src = "https://cdn.plot.ly/plotly-2.35.3.min.js";
s.async = true;
s.onload = resolve;
s.onerror = () => reject(new Error("Failed to load Plotly"));
document.head.appendChild(s);
});
const Plotly = globalThis.Plotly;
const box = html`<div style="max-width:1200px;font:14px system-ui;color:#0f172a;"> <div id="ctrl" style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-bottom:10px;
border:1px solid #cbd5e1;border-radius:12px;padding:10px;background:#f8fafc"></div> <div style="display:grid;grid-template-columns:1fr 360px;gap:12px;align-items:start"> <div id="chart" style="height:620px;"></div> <div id="panel" style="border:1px solid #e2e8f0;border-radius:12px;padding:10px;background:#fff"> <div style="font-weight:600;margin-bottom:6px">kNN — Step-by-Step 3D</div> <div id="steptext" style="font-size:13px;color:#334155;margin-bottom:8px"></div> <div id="distlist" style="font-family:ui-monospace, SFMono-Regular, Menlo, monospace; font-size:12px; white-space:pre; overflow:auto; max-height:430px;"></div> </div> </div> <div id="legend" style="font-size:12px;color:#475569;margin-top:8px"></div>
</div>`;
const ctrl = box.querySelector("#ctrl");
// ===== Controls =====
const seed = Inputs.number({label:"Seed", value:42, step:1, min:0, max:999999});
const datasetSel = Inputs.select(["Blobs (C)", "Two Moons 3D-ish (2)"], {label:"Dataset"});
const nPerCls = Inputs.range([10, 300], {label:"Points per class", value:80, step:10});
const nClasses = Inputs.range([2, 8], {label:"Number of classes (C)", value:3, step:1});
const noiseCtrl = Inputs.range([0, 1.0], {label:"Noise", value:0.25, step:0.05});
const kVal = Inputs.range([1, 51], {label:"k (neighbors)", value:7, step:2});
const metricSel = Inputs.select(["Euclidean","Manhattan","Minkowski(p)"], {label:"Metric"});
const pMink = Inputs.range([1, 6], {label:"p (Minkowski)", value:3, step:1});
const qx = Inputs.range([-6, 6], {label:"Query x", value:0, step:0.2});
const qy = Inputs.range([-6, 6], {label:"Query y", value:0, step:0.2});
const qz = Inputs.range([-6, 6], {label:"Query z", value:0, step:0.2});
const step = Inputs.range([0, 4], {label:"Step (0–4)", value:0, step:1});
const regenBtn = Inputs.button("🔄 Regenerate");
ctrl.append(
seed, datasetSel, nPerCls, nClasses, noiseCtrl,
kVal, metricSel, pMink,
qx, qy, qz, step, regenBtn
);
// ===== Utils =====
const rand = (s)=>{let t=(s>>>0)||1; return ()=> (t=(1664525*t+1013904223)>>>0)/2**32;};
const gauss = (r)=>{let u=1-r(), v=1-r(); return Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v);};
const palette = (C)=>{
const base=["#ef4444","#3b82f6","#22c55e","#f59e0b","#a855f7","#06b6d4","#e11d48","#10b981","#84cc16","#f97316"];
if(C<=base.length) return base.slice(0,C);
const ex=Array.from({length:C-base.length},(_,i)=>`hsl(${(360*(i+1)/(C-base.length+1))|0} 70% 55%)`);
return base.concat(ex);
};
const dist3=(a,b,metric,p)=>{
const dx=Math.abs(a.x-b.x), dy=Math.abs(a.y-b.y), dz=Math.abs(a.z-b.z);
if(metric==="Euclidean") return Math.hypot(dx,dy,dz);
if(metric==="Manhattan") return dx+dy+dz;
return (dx**p+dy**p+dz**p)**(1/p);
};
// ===== Generators =====
const blobsC3D=(C, nEach, rng, noise=0.35)=>{
const R=3.0, out=[];
for(let c=0;c<C;c++){
// แจกจุดศูนย์กลางบนทรงกลม
const u = (c+0.5)/C; // 0..1
const theta=2*Math.PI*((c*1.618)%1); // golden-ish
const phi=Math.acos(2*u-1);
const mx=R*Math.sin(phi)*Math.cos(theta);
const my=R*Math.sin(phi)*Math.sin(theta);
const mz=R*Math.cos(phi);
for(let i=0;i<nEach;i++){
out.push({x:mx+noise*gauss(rng), y:my+noise*gauss(rng), z:mz+noise*gauss(rng), c, id:`C${c+1}-${i+1}`});
}
}
return out;
};
const twoMoons3D=(nTotal, noise, rng)=>{
const m=Math.floor(nTotal/2), out=[];
for(let i=0;i<m;i++){
const t=Math.PI*rng();
out.push({x:Math.cos(t), y:Math.sin(t), z:noise*gauss(rng), c:0, id:`A${i+1}`});
}
for(let i=0;i<m;i++){
const t=Math.PI*rng();
out.push({x:1-Math.cos(t), y:-Math.sin(t)+0.5, z:noise*gauss(rng), c:1, id:`B${i+1}`});
}
for(const d of out){ d.x=d.x*3-1.5; d.y=d.y*3-1.2; d.z=d.z*3; }
return out;
};
// ===== Data State =====
let data=[], C=3, cols=palette(3);
const rebuild=()=>{
const rng=rand(seed.value + (regenBtn.value||0) + nPerCls.value*37 + nClasses.value*101);
if(datasetSel.value==="Two Moons 3D-ish (2)"){
C=2; cols=palette(C);
data = twoMoons3D(Math.max(2,nPerCls.value*2), noiseCtrl.value, rng);
} else {
C=nClasses.value; cols=palette(C);
data = blobsC3D(C, nPerCls.value, rng, 0.25 + 0.8*noiseCtrl.value);
}
};
// ===== Render =====
const chart = box.querySelector("#chart");
const legend= box.querySelector("#legend");
const steptext = box.querySelector("#steptext");
const distlist = box.querySelector("#distlist");
function render(){
// refresh dataset
if(chart._seed!==seed.value||chart._regen!==regenBtn.value||
chart._n!==nPerCls.value||chart._ds!==datasetSel.value||
chart._noise!==noiseCtrl.value||chart._cls!==nClasses.value){
rebuild();
chart._seed=seed.value; chart._regen=regenBtn.value;
chart._n=nPerCls.value; chart._ds=datasetSel.value;
chart._noise=noiseCtrl.value; chart._cls=nClasses.value;
}
const q = {x: qx.value, y: qy.value, z: qz.value};
// distances & sort
const rows = data.map(d=>({
id: d.id || "", x:d.x, y:d.y, z:d.z, c:d.c,
r: dist3(q, d, metricSel.value, pMink.value)
})).sort((a,b)=> a.r-b.r);
// neighbors & voting
const K=Math.max(1,Math.min(kVal.value, rows.length));
const nbrs=rows.slice(0,K);
const votes=Array.from({length:C},()=>0); for(const z of nbrs) votes[z.c]++;
const maxV=Math.max(...votes);
const winners=[]; for(let i=0;i<C;i++) if(votes[i]===maxV) winners.push(i);
let yhat;
if(winners.length===1) yhat=winners[0];
else {
const first = nbrs.find(z=>winners.includes(z.c));
yhat = first?.c ?? 0;
}
const qColor = cols[yhat];
// step text
const steps = [
"Step 0: Show data + Query (no computation)",
"Step 1: Compute distance from Query to all points",
"Step 2: Sort by distance (asc)",
`Step 3: Pick ${K} nearest neighbors`,
"Step 4: Majority vote (multi-class) → Prediction"
];
steptext.textContent = steps[step.value] || steps[0];
// table (monospace)
const header = " id x y z cls dist\n-------------------------------------------------\n";
const lines = rows.map((z,i)=>{
const mark = (i<K && step.value>=3) ? "★ " : " ";
return `${mark}${(z.id||"").padEnd(10).slice(0,10)} ${z.x.toFixed(2).padStart(7)} ${z.y.toFixed(2).padStart(7)} ${z.z.toFixed(2).padStart(7)} ${String(z.c).padStart(3)} ${z.r.toFixed(3).padStart(8)}`;
});
distlist.textContent = header + lines.join("\n");
// traces per class
const traces=[];
for(let c=0;c<C;c++){
const pts=data.filter(d=>d.c===c);
traces.push({
type:"scatter3d", mode:"markers",
x:pts.map(d=>d.x), y:pts.map(d=>d.y), z:pts.map(d=>d.z),
marker:{size:4, color:cols[c], opacity:0.9},
name:`C${c+1}`
});
}
// query point
traces.push({
type:"scatter3d", mode:"markers",
x:[q.x], y:[q.y], z:[q.z],
marker:{size:9, color:qColor, symbol:"diamond", line:{width:2, color:"#0f172a"}},
name:"Query"
});
// distance lines (step>=1)
if(step.value>=1){
const maxLines = 300; // จำกัดเพื่อความลื่น
const links = rows.slice(0, Math.min(rows.length, maxLines));
traces.push({
type:"scatter3d", mode:"lines",
x: links.flatMap(z=>[q.x, z.x, null]),
y: links.flatMap(z=>[q.y, z.y, null]),
z: links.flatMap(z=>[q.z, z.z, null]),
line:{width:1, color:"rgba(100,116,139,0.45)"},
name:"dist"
});
}
// neighbor highlight (step>=3)
if(step.value>=3){
traces.push({
type:"scatter3d", mode:"markers",
x: nbrs.map(z=>z.x), y: nbrs.map(z=>z.y), z: nbrs.map(z=>z.z),
marker:{size:7, color: nbrs.map(z=> cols[z.c]), line:{width:1, color:"#0f172a"}},
name:`${K}-NN`
});
}
const layout = {
margin:{l:0,r:0,t:0,b:0},
scene:{
xaxis:{title:"x"}, yaxis:{title:"y"}, zaxis:{title:"z"},
aspectmode:"cube", dragmode:"orbit"
},
legend:{orientation:"h"}
};
Plotly.react(chart, traces, layout);
const votesText = votes.map((v,i)=> `C${i+1}=${v}`).join(", ");
const predText = step.value>=4 ? `Prediction: <b style="color:${qColor}">C${yhat+1}</b> | Votes: ${votesText}` : "Prediction: —";
legend.innerHTML = `
Dataset: <b>${datasetSel.value}</b> |
C = <b>${C}</b> | points/class = <b>${nPerCls.value}</b> |
k = <b>${K}</b> | Metric = <b>${metricSel.value}${metricSel.value==="Minkowski(p)"?` (p=${pMink.value})`:""}</b> |
Query = (${q.x.toFixed(2)}, ${q.y.toFixed(2)}, ${q.z.toFixed(2)}) → ${predText}
`;
}
for(const el of [seed,datasetSel,nPerCls,nClasses,noiseCtrl,kVal,metricSel,pMink,qx,qy,qz,step,regenBtn]){
el.addEventListener?.("input", render);
el.addEventListener?.("change", render);
el.addEventListener?.("click", render);
}
render();
return box;
})()(() => {
return (async () => {
// ----- Robust loader: CDN -> CDN -> CDN -> Local (ถ้ามี) -----
async function loadPlotly() {
if (globalThis.Plotly) return globalThis.Plotly;
// ถ้าวางไฟล์ไว้เอง เช่น assets/plotly.min.js ให้แก้ path ตรงนี้
const localUrl = new URL("assets/plotly.min.js", document.baseURI).href;
const urls = [
"https://cdn.plot.ly/plotly-2.35.3.min.js",
"https://cdn.jsdelivr.net/npm/plotly.js-dist-min@2.35.3/plotly.min.js",
"https://unpkg.com/plotly.js-dist-min@2.35.3/plotly.min.js",
localUrl
];
// helper โหลด script พร้อม timeout
const tryScript = (url) =>
new Promise((resolve, reject) => {
const s = document.createElement("script");
s.src = url;
s.async = true;
s.onload = () => resolve(url);
s.onerror = () => reject(new Error("load error"));
document.head.appendChild(s);
setTimeout(() => reject(new Error("timeout")), 15000);
});
let lastErr;
for (const url of urls) {
try {
await tryScript(url);
if (globalThis.Plotly) return globalThis.Plotly;
} catch (e) {
lastErr = e;
// ลองตัวถัดไป
}
}
throw new Error("All Plotly sources failed. " + (lastErr?.message || ""));
}
const Plotly = await loadPlotly();
// ====== ใส่โค้ดกราฟ/ขั้นตอน kNN 3D ของคุณจากเดิมตรงนี้ได้เลย ======
// ตัวอย่างสั้น ๆ เพื่อทดสอบว่าขึ้นแน่
const chart = html`<div style="height:600px"></div>`;
const data = [{
type: "scatter3d", mode: "markers",
x: [1,2,3], y: [0,1,0], z: [2,3,2],
marker: {size: 6}
}];
const layout = {margin:{l:0,r:0,t:0,b:0}, scene:{aspectmode:"cube"}};
await Plotly.newPlot(chart, data, layout);
return chart;
})();
})()