Khi nhóm nghiên cứu về việc tăng tốc bằng GPU, một trong những điểm đáng chú ý nhất là về hiệu năng vượt trội mà RAPIDS có thể mang lại. NVIDIA khẳng định rằng khi phân tích các dataset trong khoảng 10TB, RAPIDS có thể thực hiện nhanh hơn lên tới 20 lần trên GPU so với CPU cùng phân khúc. Nhóm của chúng tôi rất muốn biết liệu RAPIDS có thể đạt được hiệu suất tương tự trên các dataset với quy mô nhỏ hơn không.
Mục tiêu dự án
Nhóm đã tiến hành nghiên cứu về hiệu năng RAPIDS trên GPU bằng cách thực hiện một số bài test, để đánh giá tổng thời gian build và test ba model khác nhau. Đầu tiên chúng tôi chọn build model K-means clustering, tiếp theo là trên bộ phân loại random forest, sau đó sử dụng mã hóa VLAD(VLAD encoding) kết hợp với thuật toán K-means để phân cụm hình ảnh.
Hai thử nghiệm đầu tiên sử dụng data được tạo ngẫu nhiên, trong khi thử nghiệm thứ ba sử dụng một bộ dataset chứa hình ảnh các địa điểm du lịch quốc tế nổi tiếng. Chúng tôi đã tiến hành hai bài test ban đầu với các kích thước tập dữ liệu khác nhau, từ 100 row và 5.000 data points đến 10 triệu row, 500 triệu data points. Đối với bài test ba, chúng tôi sử dụng một dataset hình ảnh có kích thước khoảng 2GB.
Nếu kết quả hiệu suất tăng đáng kể dựa trên các kích thước tăng dần dataset trong hai thử nghiệm đầu tiên, điều này sẽ cho thấy rằng RAPIDS có thể mang lại lợi ích về hiệu suất trên nhiều tập dữ liệu và nhiều trường hợp sử dụng. Trong blog này, chúng tôi cũng sẽ đề cập đến giới hạn data thấp hơn của Rapid và đưa ra những trường hợp mà GPU không thể cho ra kết quả tốt.
Kết quả của thử nghiệm được thể hiện trong bảng dưới đây:
Compute |
Spec |
Price |
K-Means Clustering |
Random Forest Classifier |
VLAD Encoding (w/ K-Means Clustering) |
CPU |
Intel Xeon (2.30GHz) (1 Core, 2 Threads) |
~$200 |
RAPIDS is up to 24x faster |
RAPIDS is up to 174x faster |
RAPIDS is over 50x faster |
GPU |
NVIDIA T4 (2560 Cores) |
~$1200 |
Team đã sử dụng các Google Colab notebooks miễn phí cho các bài test trong blog này.
Vui lòng đọc tiếp để biết thêm thông tin!
Phân cụm K-Means
Đầu tiên, chúng ta import các thư viện cần thiết và chuẩn bị để tạo dữ liệu mẫu bằng cách sử dụng hàm make_blobs từ cuML. Chúng ta sẽ thay đổi số lượng mẫu (n_samples) để đánh giá sự khác biệt về hiệu suất giữa cuML và Scikit-learn đối với các kích thước tập dữ liệu khác nhau.
import cudf
import cupy
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from cuml.datasets import make_blobs
from cuml.cluster import KMeans as cuKMeans
from sklearn.cluster import KMeans as skKMeans
%matplotlib inline
Hình 1. Import thư viện
Chúng ta sẽ bắt đầu phân tích với 10.000 mẫu và tăng kích thước mẫu trong các thử nghiệm tiếp theo. Chúng tôi tạo một tập dữ liệu với 50 feature và 4 cluster.
Code mẫu trong hình 2 dưới đây.
n_samples = 10000
n_features = 50
n_clusters = 4
cu_data, cu_labels = make_blobs(
n_samples=n_samples,
n_features=n_features,
centers=n_clusters,
cluster_std=0.1
)
Hình 2. Tạo data mẫu
Chúng tôi biến đổi sample data thành các mảng Numpy trước khi thực hiện phân cụm K-means bằng Scikit-learn. Chúng tôi sử dụng hàm timeit để đo thời gian thực thi khi phân cụm. Hàm này chạy code 1 vài lần và tính toán thời gian thực thi trung bình, cùng với độ lệch chuẩn.
Đoạn code này được thể hiện trong hình 3 dưới đây.
np_data = cu_data.get()
np_labels = cu_labels.get()
kmeans_sk = skKMeans(
init="k-means++",
n_clusters=n_clusters,
n_init = 'auto'
)
%timeit kmeans_sk.fit(np_data
Hình 3. Phân cụm K-means của SciKit-Learn
Sau đó, chúng tôi thực hiện phân cụm data tương tự bằng cuML và tính toán lại thời gian thực hiện trung bình và độ lệch chuẩn. Chúng tôi lặp lại thử nghiệm này với các tập dữ liệu có kích thước khác nhau, từ 10 triệu row đến chỉ còn 100 row. Chúng tôi ghi lại thời gian chạy khi sử dụng Scikit-learn và khi sử dụng cuML, và so sánh hai phương pháp.
Kết quả có thể được tìm thấy trong bảng dưới đây.
Rows |
Data Points |
Sklearn Runtime |
cuML Runtime |
Result |
10,000,000 |
500,000,000 |
31.9 s ± 5.83 s |
1.34 s ± 1.99 ms |
~24x faster |
1,000,000 |
50,000,000 |
1.58 s ± 383 ms |
137 ms ± 2.8 ms |
~11.5x faster |
100,000 |
5,000,000 |
163 ms ± 11.6 ms |
17.7 ms ± 636 µs |
~9x faster |
10,000 |
500,000 |
65.8 ms ± 11.5 ms |
9.59 ms ± 730 µs |
~7x faster |
1000 |
50,000 |
22.6 ms ± 16.8 ms |
14.8 ms ± 6.23 ms |
~1.5x faster* |
100 |
5000 |
1.94 ms ± 67.9 µs |
4.48 ms ± 68.6 |
~2.3x slower |
Chúng ta có thể thấy rằng cuML nhanh hơn gần như trong tất cả các trường hợp, ngoại trừ tập dữ liệu nhỏ nhất. Sức mạnh về hiệu năng của cuML tăng lên khi kích thước tập dữ liệu tăng lên. Khi sử dụng tập dữ liệu với 10 nghìn row, cuML nhanh gấp khoảng bảy lần. Khi sử dụng tập dữ liệu với 10 triệu row, cuML nhanh gấp 24 lần, một sức mạnh đáng kinh ngạc.
Hình 4. Kết quả phân cụm K-mean
Phân loại Random Forest
Tương tự như thí nghiệm phân cụm K-means, chúng tôi đã import các thư viện cần thiết và thiết lập dataset. Chúng tôi đã thay đổi số lượng mẫu bằng cách tăng hoặc giảm biến n_samples. Chúng tôi đã sử dụng hàm make_classification của scikit-learn để tạo dữ liệu ngẫu nhiên phù hợp cho thử nghiệm. Chúng tôi giữ tổng số lượng feature và số lượng feature "hữu ích" không đổi cho mỗi lần test, chỉ thay đổi số lượng row data. Sau đó, chúng tôi chia tập dữ liệu thành các tập train và test bằng cách sử dụng hàm train_test_split của scikit-learn. Đoạn code để import thư viện và thiết lập dữ liệu được thể hiện trong hình 5 dưới đây.
import cudf
import numpy as np
import pandas as pd
from cuml.ensemble import RandomForestClassifier as cuRFC
from cuml.metrics import accuracy_score
from sklearn.ensemble import RandomForestClassifier as skRFC
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import adjusted_rand_score
n_samples = 500000
n_features = 200 # total no. dimensions
n_info = 100 # no. useful dimensions
dtype = np.float32
x, y = make_classification(n_samples = n_samples,
n_features = n_features,
n_informative = n_info,
n_classes = 2)
x = pd.DataFrame(x.astype(dtype))
y = pd.Series(y.astype(np.int32))
x_train, x_test, y_train, y_test = train_test_split(x, y,
test_size = 0.2,
random_state=0)
Hình 5. Import thư viên và tạo tập Data
Đối với mô hình cuML, chúng tôi đã chuyển đổi data ban đầu thành các dataset cuDF trước khi huấn luyện mô hình. Đoạn code mẫu được thể hiện trong hình 6.
x_cuDF_train = cudf.DataFrame.from_pandas(x_train)
x_cuDF_test = cudf.DataFrame.from_pandas(x_test)
y_cuDF_train = cudf.Series(y_train.values)
Hình 6. Chuyển đổi data sang cuDF Dataframe
Để cho ngắn gọn, chúng tôi sẽ không bàn về điều chỉnh parameter trong bài đăng blog này. Đối với mỗi bài test, chúng tôi không thay đổi parameter khi huấn luyện mô hình. Chúng tôi đo thời gian huấn luyện mô hình và dự đoán kết quả khi sử dụng tập test. Thời gian huấn luyện mô hình được so sánh trong bảng dưới đây. Code để tạo mô hình Random Forest Classification cho cả Scikit-learn và cuML được thể hiện trong hình 7.
Rows |
Data Points |
Scikit-learn Runtime |
Accuracy Score |
cuML Runtime |
Accuracy Score |
Result |
100,000 |
5,000,000 |
13min 25s |
0.89 |
4.65 s |
0.9 |
~174x faster |
10,000 |
500,000 |
1min |
0.86 |
523ms |
0.85 |
~115x faster |
1000 |
50,000 |
4.48s |
0.67 |
550 ms |
0.65 |
~8x faster* |
Tương tự trong bài phân cụm K-means, chúng tôi thấy những cải tiến đáng kể khi sử dụng cuML. Một lần nữa, những cải tiến này thể hiện rõ rệt hơn đối với kích thước tập dữ liệu lớn, còn đối với các tập dữ liệu nhỏ hơn thì cuML thể hiện ít hiệu quả hơn.
#SK-learn model
sk_model = skRFC(n_estimators=40,
max_depth=16,
max_features=1.0,
random_state=10)
sk_model.fit(x_train, y_train)
#cuML model
cuml_model = cuRFC(n_estimators=40,
max_depth=16,
max_features=1.0,
random_state=10)
Hình 7. RFC Modelling
VLAD – Vector of Locally Aggregated Descriptors
Trong thử nghiệm thứ ba, chúng tôi sử dụng mã hóa VLAD cho bài toán gọi là Tìm kiếm Hình ảnh Dựa trên Nội dung (Content-Based Image Retrieval), có thể mô tả như sau: "Cho trước một hình ảnh cần truy vấn, tìm các hình ảnh tương tự về nội dung từ một cơ sở dữ liệu hình ảnh cho trước.
Nói chung, quy trình Tìm kiếm Hình ảnh Dựa trên Nội dung như sau:
Trong thực tế, mỗi hình ảnh thường được lưu trữ dưới dạng một embedding, đó là một véc-tơ được thiết kế để thu giữ những đặc trưng có tính phân biệt. Các đặc trưng có tính phân biệt được tính toán bằng các thuật toán trích xuất đặc trưng (image descriptor), được chia thành hai loại: Trích xuất cục bộ (local image descriptors) và Trích xuất toàn cục (global image descriptor). Local image descriptors lưu giữ những thông tin các vùng nhỏ trong hình ảnh, thường là các điểm đặc biệt như góc, cạnh hoặc điểm mạnh. trong khi global image descriptor lưu giữ các đặc trưng được tính toán trên toàn bộ hình ảnh. Global image descriptor hoạt động tốt nhất khi bạn muốn giữ những đặc điểm cụ thể, bất kể vị trí chúng trong hình ảnh. Trong trường hợp này, tập dữ liệu hình ảnh mà chúng tôi đang làm việc bao gồm các bức ảnh chụp các danh lam thắng cảnh nổi tiếng từ các vị trí và góc nhìn khác nhau. Vì vậy, mô tả hình ảnh toàn cục(global image descriptor) là lựa chọn phù hợp nhất.
QUY TRÌNH TIẾN HÀNH
Các bước được thực hiện trong thử nghiệm này như sau:
Đối với một hình ảnh truy vấn cụ thể, quy trình truy xuất được thực hiện như sau:
Phần lớn của việc thực thi được thực hiện trong các bước (3) và (4), có thể được giải thích như sau:
Cho một tập C={c1, c2, …, ck}
Một hình ảnh được định nghĩa là v={v1, v2, …, vk}
Trong đó, mỗi thành phần từ hình ảnh được định nghĩa như sau:
Trong đó:
x: Một đặc trưng có tính phân biêt.
ci: Một vector mô tả trực quan biến thứ i.
NN(x): biến gần nhất của x.
v được chuẩn hóa theo chuẩn L2:
Thực thi và kết quả
Toàn bộ code Python được cung cấp trong phần phụ lục ở cuối bài viết blog. Team đã sử dụng K-means của Scikit-learn, tính toán đại số bằng NumPy và thuật toán trích xuất đặc trưng phân biệt SIFT của OpenCV. Sau khi thực hiện workflow đã được mô tả ở trên với code này, Chúng tôi đã import hàm K-means của cuML và sử dụng nó để thay thế hàm từ Scikit-learn. Việc thay thế này đã cải thiện đáng kể hiệu suất. Thời gian phân nhóm giảm từ 1620 giây xuống chỉ còn 28 giây, nhanh hơn hơn 50 lần. Trong trường hợp này, chúng tôi thực hiện bài test của mình với một tập dữ liệu có kích thước cố định, tuy nhiên, vì chúng tôi đã sử dụng lại gom nhóm K-means, ta có thể giả định rằng hiệu năng sẽ lớn hơn với kích thước tập dữ liệu lớn hơn.
Replace:
from sklearn.cluster import KMeans
self.vocabs = KMeans(n_clusters = self.n_vocabs, init='k-means++').fit(X)
With:
from cuml.cluster import KMeans
self.vocabs = KMeans(n_clusters = self.n_vocabs, init='k-means||').fit(X)
Hình 8. Thay thế hàm phân cụm scikit-learn với CuML
Một số mẫu kết quả truy xuất hình ảnh trong hình 9 dưới đây. Chúng tôi đã làm nổi bật hình ảnh cần truy vấn bằng viền màu xanh lam và tập hình ảnh kết quả được truy xuất bằng viền màu tím.
Hình 9.Kết quả truy xuất
Kết luận
Đối với cả ba thử nghiệm trên, chúng tôi đã thấy rằng, sau khi tập dữ liệu vượt qua kích thước tối thiểu, cuML cung cấp một hiệu năng đáng kể so với Scikit-learn. Những lợi ích về hiệu suất này là do sử dụng GPU và hiệu năng sẽ tăng lên theo mức độ tăng kích thước tập dữ liệu. Trong mỗi thử nghiệm, chúng tôi nhận thấy việc chuyển đổi từ các phương pháp truyền thống dựa trên CPU sang cuML là rất đơn giản và yêu cầu ít tác vụ bổ sung. Quá trình chủ yếu giống nhau và việc chuyển đổi thường liên quan đến việc thay thế các hàm từ Scikit-learn bằng các hàm từ cuML. Tổng thể, chúng tôi thấy được cải thiện hiệu suất lên đến 50 lần so với phương pháp ban đầu. Trong bài viết blog cuối cùng của chúng tôi về NVIDIA RAPIDS, chúng tôi sẽ triển khai một Machine Learning Pipeline đầy đủ bằng RAPIDS và tìm hiểu về hiệu suất và độ phức tạp của việc ứng dụng này.
Phụ lục
conf={
'SIFT': {
'output': 'feats-SIFT',
'preprocessing': {
'grayscale': False,
'resize_max': 1600,
'resize_force': False,
},
},
}
class ImageDataset(torch.utils.data.Dataset):
default_conf = {
'globs': ['*.jpg', '*.png', '*.jpeg', '*.JPG', '*.PNG'],
'grayscale': False,
'interpolation': 'cv2_area'
}
def __init__(self, root, conf, paths = None):
self.conf = conf = SimpleNamespace(**{**self.default_conf, **conf})
self.root = root
paths = []
for g in conf.globs:
paths += list(Path(root).glob('**/'+g))
if len(paths) == 0:
raise ValueError(f'Could not find any image in root: {root}.')
paths = sorted(list(set(paths)))
self.names = [i.relative_to(root).as_posix() for i in paths]
def __getitem__(self, idx):
name = self.names[idx]
image = read_image(self.root/name)
size = image.shape[:2][::-1]
feature = compute_SIFT(image)
data = {
'image': image,
'feature': feature
}
return data
def __len__(self):
return len(self.names)
class VLAD:
"""
Parameters
------------------------------------------------------------------
k: int, default = 128
Dimension of each visual words (vector length of each visual words)
n_vocabs: int, default = 16
Number of visual words
Attributes
------------------------------------------------------------------
vocabs: sklearn.cluster.Kmeans(k)
The visual word coordinate system
centers: [n_vocabs, k] array
the centroid of each visual words
"""
def __init__(self, k=128, n_vocabs=16):
self.n_vocabs = n_vocabs
self.k = k
self.vocabs = None
self.centers = None
def fit(self,
conf,
img_dir:Path,
out_path: Optional[Path] = None,
overwrite:bool = False):
"""This function build a visual words dictionary and compute database VLADs,
and export them into a h5 file in 'out_path'
Args
----------------------------------------------------------------------------
conf: local descripors configuration
img_dir: database image directory
out_path:
"""
start_time = time.time()
#Setup dataset and output path
dataset = ImageDataset(img_dir,conf)
if out_path is None:
out_path = Path(img_dir, conf['vlads']+'.h5')
out_path.parent.mkdir(exist_ok=True, parents=True)
features = [data['feature'] for data in dataset]
X = np.vstack(features) #stacking local descriptor
del features #save RAM
#find visual word dictionary
cluster_time = time.time()
self.vocabs = KMeans(n_clusters = self.n_vocabs, init='k-means++').fit(X)
print('Clutering time is {} seconds'.format(time.time()-cluster_time))
self.centers = self.vocabs.cluster_centers_
del X #save RAM
# self._save_vocabs(out_path.parent / 'vocabs.joblib')
for i,data in enumerate(dataset):
name = dataset.names[i]
v = self._calculate_VLAD(data['feature'])
with h5py.File(str(out_path), 'a', libver='latest') as fd:
try:
if name in fd:
del fd[name]
#each image is saved in a different group for later
grp = fd.create_group(name)
grp.create_dataset('vlad', data=v)
except OSError as error:
if 'No space left on device' in error.args[0]:
del grp, fd[name]
raise error
print('Execution time is {} seconds'.format(time.time()-start_time))
return self
def query(self,
query_dir: Path,
vlad_features: Path,
out_path: Optional[Path] = None,
n_result=10):
#define output path
if out_path is None:
out_path = Path(query_dir, 'retrievals'+'.h5')
out_path.parent.mkdir(exist_ok=True, parents=True)
query_names = [str(ref.relative_to(query_dir)) for ref in query_dir.iterdir()]
images = [read_image(query_dir/r) for r in query_names]
query_vlads = np.zeros([len(images), self.n_vocabs*self.k])
for i, img in enumerate(images):
query_vlads[i] = self._calculate_VLAD(compute_SIFT(img))
with h5py.File(str(vlad_features), 'r', libver = 'latest') as f:
db_names = []
db_vlads = np.zeros([len(f.keys()), self.n_vocabs*self.k])
for i, key in enumerate(f.keys()):
data = f[key]
db_names.append(key)
db_vlads[i]= data['vlad'][()]
sim = np.einsum('id, jd -> ij', query_vlads, db_vlads) # can be switch out with ANN
pairs = pairs_from_similarity_matrix(sim, n_result)
pairs = [(query_names[i], db_names[j]) for i,j in pairs]
retrieved_dict = {}
for query_name, db_name in pairs:
if query_name in retrieved_dict.keys():
retrieved_dict[query_name].append(db_name)
else:
retrieved_dict[query_name] = [db_name]
with h5py.File(str(out_path), 'a', libver='latest') as f:
try:
for k,v in retrieved_dict.items():
if k in f:
del f[k]
f[k] =v
except OSError as error:
if 'No space left on device' in error.args[0]:
pass
raise error
return self
def _calculate_VLAD(self, img_des):
v = np.zeros([self.n_vocabs, self.k])
NNs = self.vocabs.predict(img_des)
for i in range(self.n_vocabs):
if np.sum(NNs==i)>0:
v[i] = np.sum(img_des[NNs==i, :]-self.centers[i], axis=0)
v = v.flatten()
v = np.sign(v)*np.sqrt(np.abs(v)) #power norm
v = v/np.sqrt(np.dot(v,v)) #L2 norm
return v