Hỏi - đáp Nơi cung cấp thông tin nghề nghiệp và giải đáp những thắc mắc thường gặp của bạn

Đánh giá hiệu suất cải thiện khi sử dụng GPU cho Machine Learning với NVIDIA RAPIDS

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:

  • Import một cơ sở dữ liệu hình ảnh.
  • Chọn một hình ảnh cần truy vấn.
  • Tìm kiếm n hình ảnh tương tự nhất trong cơ sở dữ liệu.

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:

  1. Tìm feature có tính phân biệt cho tất cả các hình ảnh trong cơ sở dữ liệu.
  2. Gom chúng thành K-visual word.
  3. Véc-tơ hóa toàn bộ cơ sở dữ liệu.

Đố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:

  1.   Véc-tơ hóa hình ảnh truy vấn.
  2.   Thực hiện truy xuất theo véc-tơ.

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
Xem bài viết tiếng Anh tại đây.