PythonとPandasで大規模データセットを扱うコツ4つ

スポンサーリンク

大規模なデータセットは、今や機械学習やデータサイエンスのプロジェクトの一部になっています。

このような大規模なデータセットはRAMに収まらず、機械学習アルゴリズムを適用することが不可能になります。

システムが遅くなり、他の作業を行うことができなくなります。

そこで、この記事では、機械学習やデータサイエンスのプロジェクトで大規模なデータセットを扱う方法について学びます。

スポンサーリンク

Pandasで大規模なデータセットを扱う

Pandasモジュールは、データ操作や分析に最も広く使用されています。

強力なDataFrameを提供し、CSVやJSONなどのファイル形式を扱うことができ、重複の削除やデータのクリーニングが容易です。

しかし、大規模なデータセットを扱うことは、pandasではまだ問題になっています。

以下は、その試行錯誤です。

KaggleからNYC Yellow Taxi 2015データセットの学習データセットを様々な方法で読み込み、psutil.virtual_memory()を使ってメモリ消費量を見てみます。

1. データをチャンクする

すべてのデータを同時に必要としない場合、チャンクと呼ばれる断片にデータをロードすることができます。

チャンクとは、データセットの一部分のことです。

read_csv()を使って、パラメータchunksize` を渡します。

チャンクサイズは RAM の容量に依存します。

import pandas as pd
import psutil
 
# Loading the training dataset by chunking dataframe
memory_timestep_1 = psutil.virtual_memory()
 
data_iterator = pd.read_csv("dataset/train_2015.csv", chunksize=100000)
fare_amount_sum_chunk = 0
for data_chunk in data_iterator:
  fare_amount_sum_chunk += data_chunk['fare_amount'].sum()
 
memory_timestep_2 = psutil.virtual_memory()
 
memory_used_pd = (memory_timestep_2[3] - memory_timestep_1[3])/(1024*1024)
print("Memory acquired with chunking the dataframe: %.4f MB"%memory_used_pd)
 
 
# Loading the training dataset using pandas
memory_timestep_3 = psutil.virtual_memory()
 
training_data_pd = pd.read_csv("dataset/train_2015.csv")
fare_amount_sum_pd = training_data_pd['fare_amount'].sum()
 
memory_timestep_4 = psutil.virtual_memory()
 
memory_used_pd = (memory_timestep_4[3] - memory_timestep_3[3])/(1024*1024)
print("Memory acquired without chunking the dataframe: %.4f MB"%memory_used_pd)
Memory acquired with chunking the dataframe: 103.0469 MB
Memory acquired without chunking the dataframe: 854.8477 MB

2. カラムの削除

分析に必要なカラムは一部だけで、すべてのカラムは必要ない場合がある。

データセットには不要なカラムがたくさん存在します。

そこで、read_csv() のパラメータ usecols を用いて、有用なカラムのみをメモリにロードすることにします。

import pandas as pd
import psutil
 
# Loading the training dataset by chunking dataframe
memory_timestep_1 = psutil.virtual_memory()
 
columns = ['fare_amount', 'trip_distance']
data_1 = pd.read_csv("dataset/train_2015.csv", usecols=columns)
 
memory_timestep_2 = psutil.virtual_memory()
 
memory_used_pd = (memory_timestep_2[3] - memory_timestep_1[3])/(1024*1024)
print("Memory acquired by sampling columns: %.4f MB"%memory_used_pd)
 
 
# Loading the training dataset using pandas
memory_timestep_3 = psutil.virtual_memory()
 
data_2 = pd.read_csv("dataset/train_2015.csv")
 
memory_timestep_4 = psutil.virtual_memory()
 
memory_used_pd = (memory_timestep_4[3] - memory_timestep_3[3])/(1024*1024)
print("Memory acquired without sampling columns: %.4f MB"%memory_used_pd)
Memory acquired by sampling columns: 25.7812 MB
Memory acquired without sampling columns: 896.5195 MB

3. 正しいdatatypesの選択

pandasが値に対して使用するデフォルトのdatatypesは、最もメモリ効率が良いとは言えません。

いくつかのカラムのデータ型を、格納する値に基づいて変更することで、大きなデータセットをメモリ内にロードすることができます。

例えば、私たちのデータセットにはVendorIDというカラムがあり、これは1と2という値だけを取ります。

しかし、pandasが使用する型はint64です。

これをbooleanに変換することで、ストレージを削減することができます。

また、pickup_latitude pickup_longitude, dropoff_latitude, dropoff_longitude は float64 から float32 に、payment_type は categorical に変換します。

import pandas as pd
from sys import getsizeof
 
data = pd.read_csv("dataset/train_2015.csv")
 
size = getsizeof(data)/(1024*1024)
print("Initial Size: %.4f MB"%size)
 
# chaning VendorID to boolean
data.VendorID = data.VendorID.apply(lambda x: x==2)
 
# chaning pickup_latitude, pickup_longitude, dropoff_latitude, dropoff_longitude to float32
location_columns = ['pickup_latitude','pickup_longitude',
                    'dropoff_latitude','dropoff_longitude']
data[location_columns] = data[location_columns].astype('float32')
 
# chaning payment_type to categorical
data.payment_type = data.payment_type.astype('category')
 
size = getsizeof(data)/(1024*1024)
print("Size after reduction: %.4f MB"%size)
Initial Size: 957.8787 MB
Size after reduction: 873.8545 MB

Dask で大規模なデータセットを扱う

Dask は並列計算ライブラリで、NumPy, pandas, scikit module をスケールアップして高速計算と低メモリを実現します。

1台のマシンが複数のコアを持っていることを利用し、daskはこの事実を並列計算のために利用します。

daskのデータフレームは、pandasのデータフレームと似たようなものです。

daskのデータフレームは、複数の小さなpandasのデータフレームから構成されています。

1つのDask DataFrameに対するメソッド呼び出しは、多くのpandasメソッド呼び出しを行うことになり、Daskは結果を得るためにどのように全てを調整するかを知っています。

pandasとdaskの両方を使用してKaggleからNYC Yellow Taxi 2015 datasetのトレーニングデータセットをロードし、 psutil.virtual_memory() を使用してメモリ消費量を見てみましょう。

import pandas as pd
import dask.dataframe as ddf
import psutil
 
#Loading the training dataset using dask
memory_timestep_3 = psutil.virtual_memory()
 
training_data_ddf = ddf.read_csv("dataset/train_2015.csv")
 
memory_timestep_4 = psutil.virtual_memory()
 
memory_used_ddf = (memory_timestep_4[3] - memory_timestep_3[3])/(1024*1024)
print("Memory acquired using dask: %.4f MB"%memory_used_ddf)
 
 
# Loading the training dataset using pandas
memory_timestep_1 = psutil.virtual_memory()
 
training_data_pd = pd.read_csv("dataset/train_2015.csv")
 
memory_timestep_2 = psutil.virtual_memory()
 
memory_used_pd = (memory_timestep_2[3] - memory_timestep_1[3])/(1024*1024)
print("Memory acquired using pandas: %.4f MB"%memory_used_pd)
Memory acquired using dask: 5.1523 MB
Memory acquired using pandas: 832.1602 MB

daskとpandasのデータフレームの大きな違いの1つは、daskのデータフレーム操作は遅延型であることです。

pandasのようにすぐに演算が実行されるのではなく、daskによってタスクグラフが作成され、必要な時に値が読み込まれます。

値が使われた後はメモリから捨てられるので、メモリに収まらないデータでもdaskは動作します。

画像データジェネレータ

ディスクに膨大なメモリを消費し、同時にメモリにロードできない画像を扱う場合、ディスクから直接バッチで画像をロードしてくれるKerasの ImageDataGenerator を使うことができます。

これだけでなく、新しい画像を作成することなく、回転、拡大縮小、反転などを使って画像を変換できるImage Augmentationの機能も提供し、MLプロジェクトのための多様なデータセットを生成するのに役立ちます。

ImageDataGenerator`を使用するためのデータセットディレクトリの標準的な作り方があります。

学習用データセットのディレクトリには、クラスと同じ名前のサブディレクトリを作成する必要があります。

サブディレクトリの中に、同じクラスの画像を格納します。

画像ファイル名は問わない。

from tensorflow.keras.preprocessing.image import ImageDataGenerator
import matplotlib.pyplot as plt
 
# Create object of ImageDataGenerator
datagen = ImageDataGenerator(
    rotation_range=20, # randomly rotate images by 20 degrees
    horizontal_flip = True # randomly flip images
)
 
# Create generator using flow_from_directory method
data_generator = datagen.flow_from_directory(
    directory = "/content/dataset/training_set/training_set", # specify your dataset directory
    batch_size=16, # specify the no. of images you want to load at a time
)
 
# load a batch using next
images, labels = next(data_generator)
 
nrows = 4
ncols = 4
fig = plt.figure(figsize=(10,10))
for i in range(16):
  fig.add_subplot(nrows, ncols, i+1)
  plt.imshow(images[i].astype('uint8'))
  plt.axis(False)
 
plt.show()

KaggleのCats and Dogsデータセットを使って、 ImageDataGenerator を使って読み込んでみましょう。

まず、ImageDataGeneratorのオブジェクトを作成し、flow_from_directory() メソッドを使ってデータをロードします。

import tensorflow as tf
import cv2
import numpy
import os
import matplotlib.pyplot as plt
 
class CustomDataGenerator(tf.keras.utils.Sequence):
 
  def __init__(self, batch_size, dataset_directory):
    self.batch_size = batch_size
    self.directory = dataset_directory
    self.list_IDs = os.listdir(self.directory)
   
  # Returns the number of batches to generate
  def __len__(self):
    return len(self.list_IDs) // self.batch_size
   
  # Return a batch of given index
  # Create your logic how you want to load your data
  def __getitem__(self, index):
    batch_IDs = self.list_IDs[index*self.batch_size : (index+1)*self.batch_size]
    images = []
    for id in batch_IDs:
      path = os.path.join(self.directory, id)
      image = cv2.imread(path)
      image = cv2.resize(image, (100,100))
      images.append(image)
     
    return images
 
 
dog_data_generator = CustomDataGenerator(
    batch_size = 16,
    dataset_directory = "/content/dataset/training_set/training_set/dogs"
)
 
# get a batch of images
images = next(iter(dog_data_generator))
 
nrows = 4
ncols = 4
fig = plt.figure(figsize=(10,10))
for i in range(16):
  fig.add_subplot(nrows, ncols, i+1)
  plt.imshow(images[i].astype('uint8'))
  plt.axis(False)
 
plt.show()
ImageDataGenerator Directory
ImageDataGenerator Directory

カスタムデータジェネレータ

上記のどの方法もうまくいかず、まだ何かマジックが起きないかと探しているのであれば、このような方法があります。

tf.keras.utils.Sequenceクラスを継承することで、完全に制御可能な独自のデータジェネレータを定義することができます。

作成したクラスはgetitemlenメソッドを実装する必要があります。

もし、エポック間でデータセットを変更したい場合は、on_epoch_end` を実装することができる。

この方法では、ディレクトリから直接データセットを読み込むことができ、必要な分の RAM だけを使用することができる。

これは model.fit() でデータセットを提供する際に使用することができます。

ImageDataGenerator Results
ImageDataGenerator Results
Custom Data Generator
Custom Data Generator

まとめ

これで、大規模なデータセットを処理するさまざまな方法を知ることができました。

これで、データサイエンスや機械学習のプロジェクトでそれらを使うことができ、メモリの少なさも問題にならなくなります。

お読みいただきありがとうございました。

タイトルとURLをコピーしました