Xem trên TensorFlow.org | Chạy trong Google Colab | Xem nguồn trên GitHub | Tải xuống sổ ghi chép |
Hướng dẫn này trình bày cách phân loại dữ liệu có cấu trúc (ví dụ: dữ liệu dạng bảng trong CSV). Chúng tôi sẽ sử dụng Keras để xác định mô hình và tf.feature_column
làm cầu nối để ánh xạ từ các cột trong CSV tới các đối tượng địa lý được sử dụng để đào tạo mô hình. Hướng dẫn này chứa mã hoàn chỉnh để:
- Tải tệp CSV bằng Pandas .
- Xây dựng một đường dẫn đầu vào để xử lý hàng loạt và xáo trộn các hàng bằng cách sử dụng tf.data .
- Ánh xạ từ các cột trong CSV đến các đối tượng địa lý được sử dụng để đào tạo mô hình bằng cách sử dụng các cột đối tượng địa lý.
- Xây dựng, đào tạo và đánh giá một mô hình bằng Keras.
Tập dữ liệu
Chúng tôi sẽ sử dụng phiên bản đơn giản hóa của bộ dữ liệu PetFinder. Có vài nghìn hàng trong CSV. Mỗi hàng mô tả một con vật cưng và mỗi cột mô tả một thuộc tính. Chúng tôi sẽ sử dụng thông tin này để dự đoán tốc độ vật nuôi sẽ được nhận nuôi.
Sau đây là mô tả của tập dữ liệu này. Lưu ý rằng có cả cột số và cột phân loại. Có một cột văn bản miễn phí mà chúng tôi sẽ không sử dụng trong hướng dẫn này.
Cột | Sự miêu tả | Loại tính năng | Loại dữ liệu |
---|---|---|---|
Loại | Loại động vật (Chó, Mèo) | Phân loại | chuỗi |
Già đi | Tuổi của vật nuôi | Số | số nguyên |
Giống1 | Giống vật nuôi chính | Phân loại | chuỗi |
Màu1 | Màu 1 của thú cưng | Phân loại | chuỗi |
Màu 2 | Màu 2 của thú cưng | Phân loại | chuỗi |
MaturitySize | Kích thước khi trưởng thành | Phân loại | chuỗi |
FurLength | Chiều dài lông | Phân loại | chuỗi |
Đã tiêm phòng | Thú cưng đã được tiêm phòng | Phân loại | chuỗi |
Tiệt trùng | Thú cưng đã được triệt sản | Phân loại | chuỗi |
Sức khỏe | Tình trạng sức khỏe | Phân loại | chuỗi |
Học phí | Phí nhận con nuôi | Số | số nguyên |
Sự miêu tả | Hồ sơ viết lên cho con vật cưng này | Chữ | chuỗi |
PhotoAmt | Tổng số ảnh đã tải lên cho con vật cưng này | Số | số nguyên |
AdoptionSpeed | Tốc độ chấp nhận | Phân loại | số nguyên |
Nhập TensorFlow và các thư viện khác
pip install sklearn
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import feature_column
from tensorflow.keras import layers
from sklearn.model_selection import train_test_split
Sử dụng Pandas để tạo khung dữ liệu
Pandas là một thư viện Python với nhiều tiện ích hữu ích để tải và làm việc với dữ liệu có cấu trúc. Chúng tôi sẽ sử dụng Pandas để tải xuống tập dữ liệu từ một URL và tải nó vào khung dữ liệu.
import pathlib
dataset_url = 'http://storage.googleapis.com/download.tensorflow.org/data/petfinder-mini.zip'
csv_file = 'datasets/petfinder-mini/petfinder-mini.csv'
tf.keras.utils.get_file('petfinder_mini.zip', dataset_url,
extract=True, cache_dir='.')
dataframe = pd.read_csv(csv_file)
Downloading data from http://storage.googleapis.com/download.tensorflow.org/data/petfinder-mini.zip 1671168/1668792 [==============================] - 0s 0us/step 1679360/1668792 [==============================] - 0s 0us/step
dataframe.head()
Tạo biến mục tiêu
Nhiệm vụ trong tập dữ liệu ban đầu là dự đoán tốc độ vật nuôi sẽ được nhận nuôi (ví dụ: trong tuần đầu tiên, tháng đầu tiên, ba tháng đầu tiên, v.v.). Hãy đơn giản hóa điều này cho hướng dẫn của chúng tôi. Ở đây, chúng tôi sẽ chuyển nó thành một bài toán phân loại nhị phân và chỉ đơn giản là dự đoán liệu vật nuôi có được nhận nuôi hay không.
Sau khi sửa đổi cột nhãn, 0 sẽ cho biết vật nuôi không được nhận nuôi và 1 sẽ cho biết nó đã được nhận.
# In the original dataset "4" indicates the pet was not adopted.
dataframe['target'] = np.where(dataframe['AdoptionSpeed']==4, 0, 1)
# Drop un-used columns.
dataframe = dataframe.drop(columns=['AdoptionSpeed', 'Description'])
Chia khung dữ liệu thành đào tạo, xác thực và kiểm tra
Tập dữ liệu chúng tôi đã tải xuống là một tệp CSV. Chúng tôi sẽ chia điều này thành các tập huấn luyện, xác nhận và thử nghiệm.
train, test = train_test_split(dataframe, test_size=0.2)
train, val = train_test_split(train, test_size=0.2)
print(len(train), 'train examples')
print(len(val), 'validation examples')
print(len(test), 'test examples')
7383 train examples 1846 validation examples 2308 test examples
Tạo một đường dẫn đầu vào bằng tf.data
Tiếp theo, chúng tôi sẽ bọc các khung dữ liệu bằng tf.data . Điều này sẽ cho phép chúng tôi sử dụng các cột đặc điểm làm cầu nối để ánh xạ từ các cột trong khung dữ liệu Pandas tới các đối tượng địa lý được sử dụng để đào tạo mô hình. Nếu chúng tôi đang làm việc với một tệp CSV rất lớn (lớn đến mức nó không vừa với bộ nhớ), chúng tôi sẽ sử dụng tf.data để đọc trực tiếp từ đĩa. Điều đó không được đề cập trong hướng dẫn này.
# A utility method to create a tf.data dataset from a Pandas Dataframe
def df_to_dataset(dataframe, shuffle=True, batch_size=32):
dataframe = dataframe.copy()
labels = dataframe.pop('target')
ds = tf.data.Dataset.from_tensor_slices((dict(dataframe), labels))
if shuffle:
ds = ds.shuffle(buffer_size=len(dataframe))
ds = ds.batch(batch_size)
return ds
batch_size = 5 # A small batch sized is used for demonstration purposes
train_ds = df_to_dataset(train, batch_size=batch_size)
val_ds = df_to_dataset(val, shuffle=False, batch_size=batch_size)
test_ds = df_to_dataset(test, shuffle=False, batch_size=batch_size)
Hiểu đường dẫn đầu vào
Bây giờ chúng ta đã tạo xong đường dẫn đầu vào, hãy gọi nó để xem định dạng dữ liệu mà nó trả về. Chúng tôi đã sử dụng kích thước lô nhỏ để giữ cho đầu ra có thể đọc được.
for feature_batch, label_batch in train_ds.take(1):
print('Every feature:', list(feature_batch.keys()))
print('A batch of ages:', feature_batch['Age'])
print('A batch of targets:', label_batch )
Every feature: ['Type', 'Age', 'Breed1', 'Gender', 'Color1', 'Color2', 'MaturitySize', 'FurLength', 'Vaccinated', 'Sterilized', 'Health', 'Fee', 'PhotoAmt'] A batch of ages: tf.Tensor([ 6 2 36 2 2], shape=(5,), dtype=int64) A batch of targets: tf.Tensor([1 1 1 1 1], shape=(5,), dtype=int64)
Chúng ta có thể thấy rằng tập dữ liệu trả về một từ điển tên cột (từ khung dữ liệu) ánh xạ tới các giá trị cột từ các hàng trong khung dữ liệu.
Thể hiện một số loại cột tính năng
TensorFlow cung cấp nhiều loại cột tính năng. Trong phần này, chúng tôi sẽ tạo một số loại cột tính năng và trình bày cách chúng biến đổi một cột từ khung dữ liệu.
# We will use this batch to demonstrate several types of feature columns
example_batch = next(iter(train_ds))[0]
# A utility method to create a feature column
# and to transform a batch of data
def demo(feature_column):
feature_layer = layers.DenseFeatures(feature_column)
print(feature_layer(example_batch).numpy())
Cột số
Đầu ra của một cột tính năng trở thành đầu vào cho mô hình (sử dụng chức năng demo được định nghĩa ở trên, chúng ta sẽ có thể thấy chính xác từng cột từ khung dữ liệu được chuyển đổi như thế nào). Cột số là loại cột đơn giản nhất. Nó được sử dụng để đại diện cho các tính năng có giá trị thực. Khi sử dụng cột này, mô hình của bạn sẽ nhận được giá trị cột từ khung dữ liệu không thay đổi.
photo_count = feature_column.numeric_column('PhotoAmt')
demo(photo_count)
[[2.] [4.] [4.] [1.] [2.]]
Trong tập dữ liệu PetFinder, hầu hết các cột từ khung dữ liệu đều được phân loại.
Cột biketized
Thông thường, bạn không muốn đưa một số trực tiếp vào mô hình mà thay vào đó, hãy chia giá trị của nó thành các danh mục khác nhau dựa trên phạm vi số. Xem xét dữ liệu thô đại diện cho tuổi của một người. Thay vì thể hiện độ tuổi dưới dạng cột số, chúng tôi có thể chia độ tuổi thành nhiều nhóm bằng cách sử dụng cột được bucketized . Lưu ý các giá trị duy nhất bên dưới mô tả độ tuổi mà mỗi hàng phù hợp.
age = feature_column.numeric_column('Age')
age_buckets = feature_column.bucketized_column(age, boundaries=[1, 3, 5])
demo(age_buckets)
[[0. 0. 0. 1.] [0. 1. 0. 0.] [0. 0. 0. 1.] [0. 0. 1. 0.] [0. 1. 0. 0.]]
Cột phân loại
Trong tập dữ liệu này, Kiểu được biểu diễn dưới dạng một chuỗi (ví dụ: 'Chó' hoặc 'Mèo'). Chúng tôi không thể cấp chuỗi trực tiếp cho một mô hình. Thay vào đó, trước tiên chúng ta phải ánh xạ chúng thành các giá trị số. Các cột từ vựng phân loại cung cấp một cách để biểu diễn các chuỗi dưới dạng một vectơ duy nhất (giống như bạn đã thấy ở trên với các nhóm tuổi). Từ vựng có thể được chuyển dưới dạng danh sách bằng categorical_column_with_vocabulary_list hoặc tải từ tệp bằng categorical_column_with_vocabulary_file .
animal_type = feature_column.categorical_column_with_vocabulary_list(
'Type', ['Cat', 'Dog'])
animal_type_one_hot = feature_column.indicator_column(animal_type)
demo(animal_type_one_hot)
[[1. 0.] [1. 0.] [1. 0.] [1. 0.] [0. 1.]]
Nhúng cột
Giả sử thay vì chỉ có một vài chuỗi khả thi, chúng ta có hàng nghìn (hoặc nhiều hơn) giá trị cho mỗi danh mục. Vì một số lý do, khi số lượng danh mục ngày càng lớn, việc đào tạo mạng nơ-ron bằng cách sử dụng mã hóa một nóng trở nên không khả thi. Chúng ta có thể sử dụng một cột nhúng để khắc phục hạn chế này. Thay vì biểu thị dữ liệu dưới dạng vectơ một nóng của nhiều thứ nguyên, cột nhúng biểu thị dữ liệu đó dưới dạng vectơ dày đặc, chiều thấp hơn, trong đó mỗi ô có thể chứa bất kỳ số nào, không chỉ 0 hoặc 1. Kích thước của nhúng ( 8, trong ví dụ bên dưới) là một tham số phải được điều chỉnh.
# Notice the input to the embedding column is the categorical column
# we previously created
breed1 = feature_column.categorical_column_with_vocabulary_list(
'Breed1', dataframe.Breed1.unique())
breed1_embedding = feature_column.embedding_column(breed1, dimension=8)
demo(breed1_embedding)
[[-0.22380038 -0.09379731 0.21349265 0.33451992 -0.49730566 0.05174963 0.2668497 0.27391028] [-0.5484653 -0.03492585 0.05648395 -0.09792244 0.02530896 -0.15477926 -0.10695003 -0.45474145] [-0.22380038 -0.09379731 0.21349265 0.33451992 -0.49730566 0.05174963 0.2668497 0.27391028] [ 0.10050306 0.43513173 0.375823 0.5652766 0.40925583 -0.03928828 0.4901914 0.20637617] [-0.2319875 -0.21874283 0.12272807 0.33345345 -0.4563055 0.21609035 -0.2410521 0.4736915 ]]
Các cột tính năng băm
Một cách khác để biểu diễn cột phân loại với một số lượng lớn giá trị là sử dụng cột phân loại_with_hash_bucket . Cột tính năng này tính toán giá trị băm của đầu vào, sau đó chọn một trong các nhóm hash_bucket_size
để mã hóa một chuỗi. Khi sử dụng cột này, bạn không cần cung cấp từ vựng và bạn có thể chọn đặt số lượng băm nhỏ hơn đáng kể so với số danh mục thực tế để tiết kiệm dung lượng.
breed1_hashed = feature_column.categorical_column_with_hash_bucket(
'Breed1', hash_bucket_size=10)
demo(feature_column.indicator_column(breed1_hashed))
[[0. 0. 0. 0. 0. 0. 0. 0. 0. 1.] [0. 0. 0. 0. 0. 0. 0. 1. 0. 0.] [0. 0. 0. 0. 0. 0. 0. 0. 0. 1.] [0. 0. 0. 0. 0. 0. 1. 0. 0. 0.] [0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]]
Các cột tính năng bị gạch chéo
Việc kết hợp các tính năng thành một đối tượng địa lý, hay còn được gọi là điểm giao nhau giữa các đối tượng địa lý, cho phép một mô hình tìm hiểu các trọng số riêng biệt cho từng kết hợp các đối tượng địa lý. Ở đây, chúng tôi sẽ tạo ra một tính năng mới đó là sự kết hợp giữa Tuổi và Loại. Lưu ý rằng crossed_column
không xây dựng bảng đầy đủ của tất cả các kết hợp có thể có (có thể rất lớn). Thay vào đó, nó được hỗ trợ bởi hashed_column
, vì vậy bạn có thể chọn kích thước của bảng.
crossed_feature = feature_column.crossed_column([age_buckets, animal_type], hash_bucket_size=10)
demo(feature_column.indicator_column(crossed_feature))
[[1. 0. 0. 0. 0. 0. 0. 0. 0. 0.] [0. 0. 0. 1. 0. 0. 0. 0. 0. 0.] [1. 0. 0. 0. 0. 0. 0. 0. 0. 0.] [0. 1. 0. 0. 0. 0. 0. 0. 0. 0.] [0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]]
Chọn cột để sử dụng
Chúng tôi đã thấy cách sử dụng một số loại cột tính năng. Bây giờ chúng ta sẽ sử dụng chúng để đào tạo một người mẫu. Mục tiêu của hướng dẫn này là hiển thị cho bạn mã hoàn chỉnh (ví dụ: cơ học) cần thiết để làm việc với các cột tính năng. Chúng tôi đã chọn một vài cột để đào tạo mô hình của chúng tôi bên dưới một cách tùy ý.
feature_columns = []
# numeric cols
for header in ['PhotoAmt', 'Fee', 'Age']:
feature_columns.append(feature_column.numeric_column(header))
# bucketized cols
age = feature_column.numeric_column('Age')
age_buckets = feature_column.bucketized_column(age, boundaries=[1, 2, 3, 4, 5])
feature_columns.append(age_buckets)
# indicator_columns
indicator_column_names = ['Type', 'Color1', 'Color2', 'Gender', 'MaturitySize',
'FurLength', 'Vaccinated', 'Sterilized', 'Health']
for col_name in indicator_column_names:
categorical_column = feature_column.categorical_column_with_vocabulary_list(
col_name, dataframe[col_name].unique())
indicator_column = feature_column.indicator_column(categorical_column)
feature_columns.append(indicator_column)
# embedding columns
breed1 = feature_column.categorical_column_with_vocabulary_list(
'Breed1', dataframe.Breed1.unique())
breed1_embedding = feature_column.embedding_column(breed1, dimension=8)
feature_columns.append(breed1_embedding)
# crossed columns
age_type_feature = feature_column.crossed_column([age_buckets, animal_type], hash_bucket_size=100)
feature_columns.append(feature_column.indicator_column(age_type_feature))
Tạo một lớp tính năng
Bây giờ chúng ta đã xác định các cột tính năng của mình, chúng ta sẽ sử dụng lớp DenseFeatures để nhập chúng vào mô hình Keras của chúng ta.
feature_layer = tf.keras.layers.DenseFeatures(feature_columns)
Trước đó, chúng tôi đã sử dụng kích thước lô nhỏ để chứng minh các cột tính năng hoạt động như thế nào. Chúng tôi tạo một đường dẫn đầu vào mới với quy mô lô lớn hơn.
batch_size = 32
train_ds = df_to_dataset(train, batch_size=batch_size)
val_ds = df_to_dataset(val, shuffle=False, batch_size=batch_size)
test_ds = df_to_dataset(test, shuffle=False, batch_size=batch_size)
Tạo, biên dịch và đào tạo mô hình
model = tf.keras.Sequential([
feature_layer,
layers.Dense(128, activation='relu'),
layers.Dense(128, activation='relu'),
layers.Dropout(.1),
layers.Dense(1)
])
model.compile(optimizer='adam',
loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
metrics=['accuracy'])
model.fit(train_ds,
validation_data=val_ds,
epochs=10)
Epoch 1/10 WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'Type': <tf.Tensor 'IteratorGetNext:11' shape=(None,) dtype=string>, 'Age': <tf.Tensor 'IteratorGetNext:0' shape=(None,) dtype=int64>, 'Breed1': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=string>, 'Gender': <tf.Tensor 'IteratorGetNext:6' shape=(None,) dtype=string>, 'Color1': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'Color2': <tf.Tensor 'IteratorGetNext:3' shape=(None,) dtype=string>, 'MaturitySize': <tf.Tensor 'IteratorGetNext:8' shape=(None,) dtype=string>, 'FurLength': <tf.Tensor 'IteratorGetNext:5' shape=(None,) dtype=string>, 'Vaccinated': <tf.Tensor 'IteratorGetNext:12' shape=(None,) dtype=string>, 'Sterilized': <tf.Tensor 'IteratorGetNext:10' shape=(None,) dtype=string>, 'Health': <tf.Tensor 'IteratorGetNext:7' shape=(None,) dtype=string>, 'Fee': <tf.Tensor 'IteratorGetNext:4' shape=(None,) dtype=int64>, 'PhotoAmt': <tf.Tensor 'IteratorGetNext:9' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API. WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'Type': <tf.Tensor 'IteratorGetNext:11' shape=(None,) dtype=string>, 'Age': <tf.Tensor 'IteratorGetNext:0' shape=(None,) dtype=int64>, 'Breed1': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=string>, 'Gender': <tf.Tensor 'IteratorGetNext:6' shape=(None,) dtype=string>, 'Color1': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'Color2': <tf.Tensor 'IteratorGetNext:3' shape=(None,) dtype=string>, 'MaturitySize': <tf.Tensor 'IteratorGetNext:8' shape=(None,) dtype=string>, 'FurLength': <tf.Tensor 'IteratorGetNext:5' shape=(None,) dtype=string>, 'Vaccinated': <tf.Tensor 'IteratorGetNext:12' shape=(None,) dtype=string>, 'Sterilized': <tf.Tensor 'IteratorGetNext:10' shape=(None,) dtype=string>, 'Health': <tf.Tensor 'IteratorGetNext:7' shape=(None,) dtype=string>, 'Fee': <tf.Tensor 'IteratorGetNext:4' shape=(None,) dtype=int64>, 'PhotoAmt': <tf.Tensor 'IteratorGetNext:9' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API. 231/231 [==============================] - ETA: 0s - loss: 0.6759 - accuracy: 0.6802WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'Type': <tf.Tensor 'IteratorGetNext:11' shape=(None,) dtype=string>, 'Age': <tf.Tensor 'IteratorGetNext:0' shape=(None,) dtype=int64>, 'Breed1': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=string>, 'Gender': <tf.Tensor 'IteratorGetNext:6' shape=(None,) dtype=string>, 'Color1': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'Color2': <tf.Tensor 'IteratorGetNext:3' shape=(None,) dtype=string>, 'MaturitySize': <tf.Tensor 'IteratorGetNext:8' shape=(None,) dtype=string>, 'FurLength': <tf.Tensor 'IteratorGetNext:5' shape=(None,) dtype=string>, 'Vaccinated': <tf.Tensor 'IteratorGetNext:12' shape=(None,) dtype=string>, 'Sterilized': <tf.Tensor 'IteratorGetNext:10' shape=(None,) dtype=string>, 'Health': <tf.Tensor 'IteratorGetNext:7' shape=(None,) dtype=string>, 'Fee': <tf.Tensor 'IteratorGetNext:4' shape=(None,) dtype=int64>, 'PhotoAmt': <tf.Tensor 'IteratorGetNext:9' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API. 231/231 [==============================] - 4s 10ms/step - loss: 0.6759 - accuracy: 0.6802 - val_loss: 0.5361 - val_accuracy: 0.7351 Epoch 2/10 231/231 [==============================] - 2s 9ms/step - loss: 0.5742 - accuracy: 0.7054 - val_loss: 0.5178 - val_accuracy: 0.7411 Epoch 3/10 231/231 [==============================] - 2s 9ms/step - loss: 0.5369 - accuracy: 0.7231 - val_loss: 0.5031 - val_accuracy: 0.7438 Epoch 4/10 231/231 [==============================] - 2s 9ms/step - loss: 0.5161 - accuracy: 0.7214 - val_loss: 0.5115 - val_accuracy: 0.7259 Epoch 5/10 231/231 [==============================] - 2s 9ms/step - loss: 0.5034 - accuracy: 0.7296 - val_loss: 0.5173 - val_accuracy: 0.7237 Epoch 6/10 231/231 [==============================] - 2s 8ms/step - loss: 0.4983 - accuracy: 0.7301 - val_loss: 0.5153 - val_accuracy: 0.7254 Epoch 7/10 231/231 [==============================] - 2s 9ms/step - loss: 0.4912 - accuracy: 0.7412 - val_loss: 0.5258 - val_accuracy: 0.7010 Epoch 8/10 231/231 [==============================] - 2s 9ms/step - loss: 0.4890 - accuracy: 0.7360 - val_loss: 0.5066 - val_accuracy: 0.7221 Epoch 9/10 231/231 [==============================] - 2s 9ms/step - loss: 0.4824 - accuracy: 0.7443 - val_loss: 0.5091 - val_accuracy: 0.7481 Epoch 10/10 231/231 [==============================] - 2s 9ms/step - loss: 0.4758 - accuracy: 0.7466 - val_loss: 0.5159 - val_accuracy: 0.7492 <keras.callbacks.History at 0x7f06b52a1810>
loss, accuracy = model.evaluate(test_ds)
print("Accuracy", accuracy)
73/73 [==============================] - 0s 6ms/step - loss: 0.4812 - accuracy: 0.7543 Accuracy 0.7543327808380127
Bước tiếp theo
Cách tốt nhất để tìm hiểu thêm về phân loại dữ liệu có cấu trúc là tự mình thử. Chúng tôi khuyên bạn nên tìm một tập dữ liệu khác để làm việc và đào tạo một mô hình để phân loại nó bằng cách sử dụng mã tương tự như trên. Để cải thiện độ chính xác, hãy suy nghĩ cẩn thận về những tính năng nào cần đưa vào mô hình của bạn và cách chúng được thể hiện.