มาสร้าง LeNet5 โดยใช้ PyTorch กัน
บทนำ
วิธีการหนึ่งในการเรียนรู้เกี่ยวกับ CNN หรือ Convolutional Neural Network คือ ทดลองสร้างโมเดล CNN ที่มีชื่อเสียงในอดีต ในบทความนี้เราจะศึกษาแนวคิดของ Convolutional Neural Networks แรกๆ ที่มีผู้เสนอ ซึ่งโด่งดังอย่างมาก เรียกได้ว่าหากมีการสอนเกี่ยวกับ CNN แล้ว เกือบทุกคนจะต้องยกตัวอย่างโมเดลนี้ โมเดลนี้มีชื่อว่า LeNet5 (paper) โดยในบทความนี้เราจะเขียนโปรแกรม LeNet5 โดยใช้ภาษา Python และใช้ PyTorch เป็นLibrary
การทดลองใน paper LeNet5 ดั้งเดิมจะใช้ dataset MNIST ซึ่งเป็นเป็นภาพลายมือเขียนของเลข 0–9 ซึ่ง LeNet5 ให้ความถูกต้องสูงมาก คือมีความถูกต้องถึง 99% แต่ในบทความนี้เราจะทดลองว่า หากนำ LeNet5 มาใช้กับชุดข้อมูลที่ยากขึ้นจะให้ผลอย่างไร โดยชุดข้อมูลที่จะนำมาใช้ในบทความนี้มีชื่อว่า CIFAR-10 ซึ่งเป็นชุดข้อมูลที่อยู่ใน Library TorchVision (ใน TorchVision มีชุดข้อมูลจำนวนมาก)
สำหรับหัวข้อที่จะกล่าวถึงในบทความนี้ จะมีดังนี้
- บทนำ
- Convolutional Neural Networks คืออะไร
- PyTorch
- Data Loading
- LeNet5
- การกำหนด Hyperparameters
- การสอน Model (Training)
- การทดสอบ Model (Testing)
Convolutional Neural Networks
การทำงานของ CNN จะเริ่มจากการรับข้อมูลภาพเป็น input และผ่านขั้นตอนต่างๆ เพื่อบอกว่าภาพที่เข้ามาเป็นภาพอะไร (Image Classification) ที่ output โดยแต่ละภาพที่เข้าสู่ Network จะส่งผ่านชั้นต่างๆ ใน Network โดยแบ่งคร่าวๆ ออกเป็น 2 ขั้นตอน คือ ขั้นตอนการหา feature หรือลักษณะเด่นของภาพ (feature learning) และขั้นตอนการจำแนก (classification)
สำหรับขั้นตอนการหา feature จะประกอบด้วยขั้นตอนย่อยๆ คือ convolutional layers, pooling layers ซึ่งอาจวางซ้อนๆ กันหลายๆ ชั้นก็ได้ โดยมีเป้าหมายเพื่อหา หรือ สกัด feature ออกมาจากภาพให้ได้มากที่สุด จากนั้นจึงส่ง feature เป็น input ของขั้นตอน classification ต่อไป โดยในชั้น classification จะประกอบด้วย flatten (การแปลงข้อมูลให้เป็นมิติเดียว เพราะ Neural Network จะรับข้อมูลแค่ 1 มิติ) โครงข่าย Neural Network ที่เรียกว่า Fully Connected Layer และชั้น output ซึ่งใช้อัลกอริทึมที่ชื่อว่า softmax ในการทำงาน ซึ่งจะให้ผลเป็นคำตอบว่าภาพที่เข้ามาเป็นภาพอะไร
Convolutional Layer
ชั้น convolutional layer จะใช้ในการสกัด feature จากภาพที่เข้ามา โดยจะทำ operation ทางคณิตศาสตร์ระหว่างภาพที่เข้ามากับ kernel (filter) โดยจะนำ kernel มาคำนวณกับตำแหน่งบนภาพ (ตามรูป) ซึ่งจะทำให้ได้ข้อมูล 1 ค่า (จากรูปคือค่า 16) จากนั้นจะทำการเลื่อน kernel ไปทำงานเช่นนี้กับตำแหน่งอื่นๆ ของภาพไปเรื่อยๆ จนครบหมดทั้งภาพ โดยการทำเช่นนี้จะทำให้ได้สิ่งที่เรียกว่าภาพ feature map ซึ่งจะมีขนาดลดลง (มีเทคนิคที่ทำให้ขนาดไม่ลดลงด้วย มีชื่อว่า padding)
ในแต่ละ kernel จะทำให้ได้ข้อมูลแต่ละด้านของภาพ เช่น อาจได้ข้อมูลของขอบ (edge detection) หรืออื่นๆ
Pooling Layers
ชั้นนี้มีหน้าที่ลดขนาดภาพที่เป็น feature map แต่ยังพยายามรักษา feature ที่สำคัญเอาไว้ โดยรูปแบบการทำงานที่มักใช้กันจะมี 2 แบบ คือ max pooling (เลือกค่ามากที่สุดในบริเวณนั้น) หรือ average pooling (นำค่าในบริเวณนั้นมาเฉลี่ยกัน) จากรูปเป็นการทำ max pooling โดยใช้ filter ชนาด 2 x 2 ซึ่งหมายความว่าในบริเวณ 2 x 2 (ระบายสีไว้แต่ละสี) จะเลือกค่าที่มากที่สุดในบริเวณนั้นออกมาก จะเห็นได้ว่า ข้อมูลที่เด่นยังคงอยู่ใน output แม้ภาพที่ได้จะมีขนาดเล็กลงก็ตาม
PyTorch
Pytorch เป็น library ด้าน deep learning ที่นิยมใช้มากที่สุดในโลก มีความสามารถครอบคลุมการทำงานอย่างกว้างขวาง รูปแบบการใช้งานง่าย นอกจากนั้นยังเป็น opensource อีกด้วย ในบทความนี้จะใช้ Pytorch ในการสร้าง CNN
Data Loading
Dataset
เราจะเริ่มจากการโหลดข้อมูลภาพ โดยบทความนี้จะใช้ชุดข้อมูล CIFAR-10 ซึ่งประกอบด้วยข้อมูลภาพสี 60,000 ภาพ (RGB) ที่มีขนาด 32x32 จุด โดยแบ่งออกเป็น 10 ประเภท (class) ประเภทละ 6,000 ภาพ โดยจะแบ่งข้อมูลเป็น 2 ส่วน คือ ข้อมูลสำหรับสอนโมเดล จำนวน 50,000 ภาพ และ ข้อมูลสำหรับทดสอบ จำนวน 10,000 ภาพ
ตัวอย่างของภาพ เป็นไปตามภาพด้านล่างนี้
Importing the Libraries
เราจะเริ่มต้นด้วยการ import library ที่จำเป็น ดังต่อไปนี้
# Load in relevant libraries, and alias where appropriate
import torch
import torch.nn as nn
from torchvision import datasets, transforms
from torch.utils.data import Dataset, DataLoader, random_split
import matplotlib.pyplot as plt
# Define relevant variables for the ML task
batch_size = 32
num_classes = 10
learning_rate = 0.001
num_epochs = 20
# Device will determine whether to run the training on GPU or CPU.
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
ใน library torch และ torch.nn จะเป็น library เกี่ยวกับ neural network ทั้งหมด สำหรับ torchvision จะเป็น library เกี่ยวกับการประมวลผลภาพ
จากนั้นจะกำหนด hyperparameter ได้แก่
- batch_size เป็นขนาด 32 หมายความว่าจะนำภาพเข้าไปในโมเดลครั้งละ 32 ภาพโดยภาพมีขนาด 32x32x3 แสดงว่าใน 1 batch จะใช้พื้นที่ 98,304 ไบต์
- num_class = 10 หมายความว่าภาพที่จะให้ทำนายมี 10 ประเภท หรือ 10 class
- learning_rate = 0.01 กำหนดอัตราการเรียนรู้เท่ากับ 0.01
- num_epochs = 20 กำหนดจำนวนรอบของการสอนโมเดล 20 รอบ
- สำหรับบรรทัดสุดท้าย คือ ถ้ามี cuda หรือ GPU ก็ให้ไปทำงานบน gpu
Dataset Loading
ต่อไปเราจะโหลดข้อมูลเพื่อนำมา train โดยจะใช้โมดูล dataset ที่อยู่ใน torchvision โดยโมดูลนี้จะช่วยในการโหลด dataset ให้มาอยู่ในเครื่องคอมพิวเตอร์ (./data) โดยในระหว่างโหลดสามารถแปลงรูปภาพ (transformation) ได้ด้วย จากโปรแกรม คือ แปลงให้เป็น Tensor และ Normalize ระดับสีของแต่ละสีให้เหมาะสม
เราจะสร้าง dataset เป็น 2 ชุด โดยชุดหนึ่งสำหรับฝึกสอนโมเดล และอีกชุดหนึ่งสำหรับทดสอบ จากนั้นส่ง dataset ต่อไปยัง dataloader ซึ่งจะทำหน้าที่จัดข้อมูลเป็น batch เพื่อส่งเข้าไปในโมเดลครั้งละ 1 batch
class CIFAR10Dataset(Dataset):
def __init__(self, root='./data', train=True, transform=None):
self.cifar10 = datasets.CIFAR10(root=root, train=train, download=True, transform=transform)
def __len__(self):
return len(self.cifar10)
def __getitem__(self, idx):
return self.cifar10[idx]
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465),(0.2023, 0.1994, 0.2010))
])
full_dataset = CIFAR10Dataset(transform=transform)
train_size = int(0.8 * len(full_dataset))
test_size = len(full_dataset) - train_size
train_dataset, test_dataset = random_split(full_dataset, [train_size, test_size])
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
LeNet5
เอาละคราวนี้มาดูโครงสร้างของ LeNet5 กัน โมเดลนี้เสนอโดย Yann LeCun และคณะในปี 1998 โดยเลข 5 หมายถึงมี 5 layer ซึ่งถือว่าน้อยสำหรับยุคนี้ ประกอบด้วยชั้น convolutional จำนวน 2 ชั้น และชั้น fully connected จำนวน 3 ชั้น ในชั้น input โมเดล LeNet5 จะรับภาพขนาด 32x32 (โมเดล LeNet5 เดิมรับเพียง grayscale แต่ในบทความนี้ปรับโมเดลให้รับภาพแบบ RGB ได้)
ในชั้น convolutional แรก จะใช้ filter ขนาด 5x5 จำนวน 6 filter โดยไม่มี padding ดังนั้นจะลดขนาดภาพจาก 32x32 เหลือ 28x28 โดยมีจำนวน channel เป็น 6 channel ตามจำนวน filter ดังนั้น output ของชั้น convolutional แรกจึงมีขนาด 28x28x6 จากนั้นก็จะเข้าสู่ชั้น pooling แบบ average โดยมีขนาด 2x2 และมี stride เป็น 2 จึงทำให้ขนาดของ feature map ลดลงครึ่งหนึ่ง คือ 14x14x6
ในชั้น convolutional ที่ 2 ก็ทำเหมือนเดิม คือ ใช้ filter ขนาด 5x5 แต่เพิ่มเป็น 16 filter (ลดลงเหลือ 10x10x16) และตามด้วยชั้น pooling ดังนั้นจะลดขนาดของ feature map ลงเหลือ 5x5x16
จากนั้นนำข้อมูล 5x5x16 มาทำ flatten โดยใช้ชั้น convolutional ขนาด 5x5 (เท่ากับขนาดของข้อมูล) จำนวน 120 filter และจากขนาดที่เท่ากันจึงทำให้การทำ convolution แต่ละครั้งจะได้ข้อมูลเพียง 1 ไบต์ เมื่อทำ 120 filter ก็จะได้ 120 ไบต์ แล้วจึงนำมาเรียงเป็นอาเรย์ 1 มิติ ซึ่งจะเป็น input ของชั้น fully connected ชั้นแรก ซึ่งมีจำนวน neuron 84 นิวรอน และเข้าสู่ชั้น fully connected ชั้นที่ 2 ซึ่งเป็น output layer ซึ่งจะเป็นข้อมูล 10 class
CNN from Scratch
ก่อนจะอธิบาย code ในส่วนของโมเดลขออธิบายการกำหนดโมเดล neural network ใน PyTorch กันก่อน
- การสร้างโมเดลจะเริ่มจากการสร้าง class ที่ extend มาจาก nn.Module ซึ่งเป็นคลาสที่สร้าง neural network ของ PyTorch
- หลังจากนั้นจะต้องกำหนดโครงสร้างของ neural network ว่ามีกี่ชั้น แต่ละชั้นจะมีกี่โหนด โดยจะต้องกำหนดให้เสร็จภายใน method
__init__
ของคลาส เราจะตั้งชื่อ layer ซึ่งมักจะเป็นชื่อง่ายๆ เช่น conv หมายถึงชั้น convolution , relu หมายถึงชั้นที่เป็น activation function LeRU หรือ pool จะหมายถึงชั้นที่เป็น pooling เป็นต้น - สุดท้ายจะต้องกำหนด
forward
method ของคลาส โดยใน method นี้จะทำหน้าที่กำหนดลำดับการทำงานของ คลาส ว่าเมื่อข้อมูลเข้ามาในคลาสแล้วจะต้องผ่าน layer ต่างๆ ตามลำดับอย่างไรบ้าง
โดยมี code ของการกำหนดโมเดลดังนี้ (หมายเหตุ code ชุดนี้อาจมีส่วนซ้ำ เพราะเจตนาเขียนให้อ่านง่ายมากกว่าจะเขียนให้สั้น
class LeNet5(nn.Module):
def __init__(self,input_size=(3,32,32)):
super(CustomLeNet5, self).__init__()
self.conv1 = nn.Conv2d(in_channels=input_size[0], out_channels=6, kernel_size=5, stride=1, padding=2)
self.relu1 = nn.ReLU()
self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2)
self.conv2 = nn.Conv2d(in_channels=6, out_channels=16, kernel_size=5, stride=1, padding=0)
self.relu2 = nn.ReLU()
self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2)
self.flatten = nn.Flatten()
self.fc1 = nn.Linear(self._get_input_size_fc1(input_size), 120)
self.relu3 = nn.ReLU()
self.fc2 = nn.Linear(120, 84)
self.relu4 = nn.ReLU()
self.fc3 = nn.Linear(84, 10)
def _get_input_size_fc1(self, input_shape):
with torch.no_grad():
x = torch.zeros(1, *input_shape)
x = self.conv1(x)
x = self.relu1(x)
x = self.pool1(x)
x = self.conv2(x)
x = self.relu2(x)
x = self.pool2(x)
x = self.flatten(x)
return x.size(1)
def forward(self, x):
x = self.conv1(x)
x = self.relu1(x)
x = self.pool1(x)
x = self.conv2(x)
x = self.relu2(x)
x = self.pool2(x)
x = self.flatten(x)
x = self.fc1(x)
x = self.relu3(x)
x = self.fc2(x)
x = self.relu4(x)
x = self.fc3(x)
return x
เราจะเริ่มจากสร้างคลาสที่ inherit จาก nn.Module
จากนั้นก็สร้าง layer ต่างๆ ตามที่ได้กล่าวไปข้างต้น ใน __init__
และ forward
ตามลำดับ
มีข้อสังเกตเพิ่มเติมดังนี้
nn.Conv2d
จะใช้ในการกำหนดชั้น convolutional โดยกำหนดจำนวน channel ที่รับเข้ามา และจำนวน channel ที่ส่งออกไปตาม kernel size ที่กำหนดnn.AvgPool2d
เป็นชั้นสำหรับ average-pooling layer ดังนั้นจึงต้องการข้อมูลเพียง kernel size และ stridenn.Linear
เป็นชั้น fully connected และnn.ReLU
เป็นชั้นสำหรับ activation function- ใน
forward
method เราจะกำหนดลำดับการทำงานและก่อนจะเข้าสู่ชั้น fully connected layers จะมีการ reshape หรือ flatten ขนาดของข้อมูลในเหมาะกับขนาดของ input ของชั้น fully connected
Setting Hyperparameters
คราวนี้เราจะเขียนฟังก์ชันสำหรับ train model กัน โดยมีฟังก์ชันดังนี้
def train_model(model, train_loader, test_loader, criterion, optimizer, epochs=20):
training_logs = {"train_loss": [], "train_acc": [], "validate_loss": [], "validate_acc": []}
print("-" * 80)
for epoch in range(epochs):
model.train()
train_loss, correct = 0, 0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
output = model(images)
loss = criterion(output, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.item()
correct += (output.argmax(1) == labels).float().sum().item()
# save training logs
training_logs["train_loss"].append(train_loss / len(train_loader))
training_logs["train_acc"].append(correct / len(train_loader.dataset))
# Evaluation
model.eval()
test_loss, correct = 0, 0
for test_images, test_labels in test_loader:
test_images, test_labels = test_images.to(device), test_labels.to(device)
output = model(test_images)
test_loss += criterion(output, test_labels).item()
correct += (output.argmax(1) == test_labels).float().sum().item()
# save validation logs
training_logs["validate_loss"].append(test_loss / len(test_loader))
training_logs["validate_acc"].append(correct / len(test_loader.dataset))
if epoch % 5 == 0:
print(f"Epoch {epoch}".ljust(10),
f"train loss {training_logs['train_loss'][-1]:.5f}",
f"train acc {training_logs['train_acc'][-1]:.5f}",
f"validate loss {training_logs['validate_loss'][-1]:.5f}",
f"validate acc {training_logs['validate_acc'][-1]:.5f}")
print("-" * 80)
return model, training_logs
ขออธิบาย code ในส่วนของฟังก์ชัน train_model
- ฟังก์ชันจะรับพารามิเตอร์ model , train_loader, test_loader, criterion, optimizer และ epochs
- model คือ ตัว instance ของโมเดลที่สร้างขึ้นจาก class
- train_loader และ test_loader คือ ตัวโหลดข้อมูลเข้ามาทีละ batch เพื่อเข้ามา train ในแต่ละรอบ
- criterion คือ loss function ที่ใช้คำนวณว่าการทำงานมีผลลัพธ์ใกล้กับสิ่งที่ต้องการมากน้อยเพียงใด
- optimizer คือ ฟังก์ชันที่ทำการคำนวณค่าพารามิเตอร์ที่จะต้องปรับในโมเดล
- epochs คือ จำนวนรอบในการทำงาน
- ในบรรทัดแรกจะมีการเตรียมพื้นที่สำหรับเก็บผลการทำงานในแต่ละรอบ โดยเก็บใน training_logs
- จากนั้นจะเข้าสู่ loop แรก ซึ่งจะทำงานตามจำนวน epochs และภายใน loop นอกนี้จะแบ่งการทำงานเป็น 2 ส่วน คือ ส่วนของ training loop และ evaluation loop
- ใน loop ในที่ 1 เป็น training loop จะรับข้อมูลภาพและ label มาจาก train loader จากนั้นจะนำเข้าสู่ device ซึ่งอาจเป็น GPU หรือ CPU
- จากนั้นจะเป็นการทำ forward pass โดยให้ model ทำการทำนายตามข้อมูลใน model โดยจะได้ข้อมูลกลับมาใน output ซึ่งจะนำไปคำนวณค่า loss ในบรรทัด
loss=criterion(output, labels)
- จากนั้นจึงทำ backward pass โดยเริ่มจากเคลียรข้อมูล gradient จากการทำงานในรอบก่อนหน้าออก
optimizer.zero_grad()
- จากนั้นนำค่า loss ไปคำนวณค่า gradient ใหม่สำหรับปรับพารามิเตอร์ โดยใช้ฟังก์ชัน
loss.backward()
- และสุดท้ายนำไปปรับ weights ในฟังก์ชัน
optimizer.step()
- ใน loop ในที่ 2 จะเป็น loop สำหรับนำข้อมูลที่ใช้ประเมิน evaluation data มาทดสอบกับโมเดล จะได้เห็นภาพว่าในแต่ละ epoch โมเดลมีการปรับตัวดีขึ้นมากน้อยเพียงใด โดยการทำงานจะคล้ายกับ training loop ทุกประการ จะต่างกันตรงที่จะไม่มีการปรับ weights
- ส่วนสุดท้ายจะเป็นการเก็บข้อมูลของแต่ละ epoch ลงใน training_logs และในทุก 5 epoch จะแสดงผลการทำงานออกมาที่จอภาพ
การสอน Model (Training)
หลังจากที่สร้างฟังก์ชันสำหรับ train แล้ว ก็มาทำการสอนโมเดลกัน โดยกำหนด loss function ซึ่งในที่นี้จะใช้ CrossEntropyLoss เนื่องจากเป็นการทำงานแบบ multiple classification และเลือก optimization algorithm ซึ่งจะเลือกฟังก์ชัน SDG (Stochastic Gradient Descent) ในการทำงาน เมื่อพร้อมแล้วก็เรียกฟังก์ชัน train model มาทำงาน
torch.manual_seed(42)
model = CustomLeNet5().to(device)
# Loss function and optimizer
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, momentum=0.9)
model, training_logs = train_model(model, train_loader, test_loader, criterion, optimizer, epochs=num_epochs)
ซึ่งจะแสดงผลลัพธ์ดังนี้
--------------------------------------------------------------------------------
Epoch 0 train loss 2.28083 train acc 0.14900 validate loss 2.17080 validate acc 0.22540
--------------------------------------------------------------------------------
Epoch 5 train loss 1.49460 train acc 0.47032 validate loss 1.48394 validate acc 0.47360
--------------------------------------------------------------------------------
Epoch 10 train loss 1.26145 train acc 0.55073 validate loss 1.29170 validate acc 0.53870
--------------------------------------------------------------------------------
Epoch 15 train loss 1.11355 train acc 0.60847 validate loss 1.22339 validate acc 0.57040
--------------------------------------------------------------------------------
Epoch 20 train loss 0.99272 train acc 0.65192 validate loss 1.17267 validate acc 0.58920
--------------------------------------------------------------------------------
Epoch 25 train loss 0.88659 train acc 0.68883 validate loss 1.12197 validate acc 0.61550
--------------------------------------------------------------------------------
Epoch 30 train loss 0.79246 train acc 0.72162 validate loss 1.11890 validate acc 0.62330
--------------------------------------------------------------------------------
จะเห็นได้ว่าค่า loss ลดลงในทุก epoch ซึ่งแสดงให้เห็นว่า model สามารถเรียนรู้ และทำงานได้ดีขึ้นเรื่อยๆ ตามรอบการทำงาน อย่างไรก็ตามจะเห็นได้ว่า ในรอบหลังๆ ค่า loss มีการลดลงในอัตราที่น้อยลง และ validation accuracy ถูกต้องในอัตราที่ลดลง และมีค่าที่ต่างจาก training accuracy ซึ่งแสดงให้เห็นว่าเกิด overfitting ขึ้น ซึ่งมีหลายวิธีที่จะแก้ไข เช่น regularization, data augmentation และอื่นๆ แต่เราจะไม่กล่าวถึงในบทความนี้
การทดสอบ Model (Testing)
ในการดูผลการทำงานของโมเดลที่ง่าย คือ แสดงด้วยกราฟ ดังนั้นเราจะเขียนโปรแกรมสำหรับ plot
def plot_graph(history):
fig, (ax1, ax2) = plt.subplots(1, 2)
fig.set_figwidth(10)
fig.suptitle("Train vs Validation")
ax1.plot(history["train_acc"], label="Train")
ax1.plot(history["validate_acc"], label="Validation")
ax1.legend()
ax1.set_title("Accuracy")
ax2.plot(history["train_loss"], label="Train")
ax2.plot(history["validate_loss"], label="Validation")
ax2.legend()
ax2.set_title("Loss")
plt.show()
plot_graph(training_logs)
โดยมีผลการทำงานดังนี้
สรุป
จะเห็นว่าความถูกต้องในการทำงานจะอยู่ที่ประมาณ 62% ซึ่งถือว่าไม่ค่อยน่าพึงพอใจ ทั้งนี้เนื่องจากโมเดลที่ใช้เป็นโมเดลที่มีขนาดเล็ก มีจำนวนพารามิเตอร์เพียง 61,706 ตัวเท่านั้น ซึ่งจะลองหาโมเดลที่น่าสนใจมาทดลองกับ dataset นี้ต่อไป