GPGPUによるレコメンデーションの高速化

ネットワークシステム研究室
11EC060 田中 雄太
指導教員 坂本 直志

目次

1 はじめに
2 準備
2.1 レコメンデーションシステムとは
2.2 レコメンデーションの基本アルゴリズム
2.3 ユークリッド距離による類似度の計算
3 CUDA
3.1 CUDA
3.2 CUDAのスレッド管理
3.2.1 ウォープについて
3.3 CUDAのメモリ構造
4 作成したプログラム
4.1 源データ
4.2 ユークリッド距離を求めるアルゴリズム
4.3 カーネル関数Reco
4.4 カーネル関数Reco2及びReco3
4.4.1 総和計算の実装
4.4.2 プログラム1(手法1)
4.4.3 プログラム2(手法2)
4.4.4 プログラム3(手法3)
5 評価
5.1 測定環境
5.2 測定結果
6 まとめ
7 参考文献
8 付録

1 はじめに

レコメンドとは顧客の求めているであろう商品を勧めることである。近年インターネットショッピングサイトなどで商品の購入履歴や閲覧履歴を基にレコメンドをするレコメンデーションシステムが開発されてきた。しかし、商品のロングテール化により商品点数の莫大な増加により、レコメンドされる商品を更新するのに時間がかかるようになってきた。

さて、近年マルチコアプロセッサの発達により並列処理が高速化している。特にGPGPUはスーパーコンピューターにも用いられている。

一方、パソコン向けにコア数が2000個を超えるGPUも存在している。これは並列計算に強く、データ量が莫大になるレコメンデーションシステムにおいて効果的なのではないかと考えられる。

そこでGPUを用いてレコメンデーションシステムを開発することでよりリアルタイムなデータを用いてレコメンド出来ることが期待される。

本研究ではユークリッド距離を用いたレコメンデーションアルゴリズムをGPGPUで計算するプログラムを開発し、その性能を評価した。

2 準備

本章ではレコメンデーションシステムについて、本研究で類似度を計測するために用いたユークリッド距離について説明していく。

2.1 レコメンデーションシステムとは

レコメンデーションシステムとは特定ユーザが興味を持つと思われる情報を提示するシステムである。従来は店員が記憶力や洞察力を使い、顧客の過去の購買履歴や、現在の表情・会話から勧める商品を考えていた。ところがウォルマートが行ったデータマイニングの手法により売上が向上したのを受け、顧客の販売履歴の分析とそれを利用した広告が行われるようになってきた。特に顧客の販売履歴から顧客ごとの広告をすることをレコメンデーションと呼び、その計算する仕組みをレコメンデーションシステムという。特にインターネットでは顧客の表情などの情報を得ることが出来ないが、アクセスデータを記録することが出来る。そこで、顧客の購入履歴や、商品へのアクセスデータを用いてフィルタリングをかけることで顧客の求めているであろう商品を勧めるためにレコメンデーションシステムが用いられている。

2.2 レコメンデーションの基本アルゴリズム

レコメンデーションの基本的な考え方は顧客のモデル化である。つまり、似ている顧客は同じ購買行動をすると仮定する。そして似ている顧客AとBにおいてAが購入していてBが購入していない商品があったとき、その商品をBに勧める。

そしてこの似ているかどうかの判定に類似度を用いる。類似度を実数値で表すとする。類似度は「近い」、「遠い」などと表すように直観的に距離と同じ性質を持つと考えやすい。そのため、次のように

Cを顧客集合、d:C×C→Rを類似度とし、以下の距離空間の公理を満たすとする。

  1. (∀x,y∈C) [dx,y≧0]
  2. (∀x∈C)[dx,x=0]
  3. (∀x,y∈C) [dx,y=dy,x]
  4. (∀x,y,z∈C) [dx,y+dy,z≧dx.z]

2.3 ユークリッド距離による類似度の計算

本研究では類似度を計算するためにユークリッド距離を用いる。n個の商品に対して、n次元ベクトルを考える。そして顧客Pが商品iを購入していたら、pi=1、していないならpi=0というベクトルPを顧客ベクトルとする。そして顧客の類似度をその点の間のユークリッド距離d(P,Q)と考える(式(1))。この値自体は大きいほど距離がより離れている。つまりより似ていないユーザであると考えられる。但し、商品の種類がベクトルの次元になるため、0〜∞次元の距離となってしまい扱いづらい。そこで2.2節に関わらず本研究ではユークリッド距離dに対して、次元に依存しない1/(1+d^2 )という形で正規化を行う。これにより次元nに関わらず値が1に近い程似ているユーザであり、0に近い程似ていないユーザだと考えられるようになる。

式1
ユークリッド距離dを求める式

3 CUDA

本章ではCUDAについて、スレッド管理の方法やメモリ構造について説明していく。

3.1 CUDA(Compute Unified Device Architecture)

CUDAはCompute Unified Device Architectureの略である。これはNVIDIA社のGPUをGPGPUとして扱い、並列計算を行うための統合開発環境である。これはGPUを動作させるためのプログラミングモデル及びプログラミング言語、そのコンパイラとライブラリやサンプル・ソースコード、実行環境で構成される。プログラミング言語としてはC言語を用いる。

3.2 CUDAのスレッド管理

GPUには多数のコアがある。そのためGPUの性能を充分に引き出すには、これらのコアを同時にフル稼働させる必要がある。そうすることによりGPUではCPUと比べ数千スレッド、あるいは数万スレッドといったコア数よりも圧倒的に多い数のスレッドで計算することによって、GPUの高い性能が引き出すことが出来る。

CUDAの仕様ではGPUのコアに対して65535×65535×512個のスレッドの実行を命令することが出来る。CUDAではこの多数のスレッドを管理するためにグリッドとブロックという概念を導入し、その中で階層的にスレッドを管理することで、容易に管理することを実現している。全てのスレッドは同じコードを実行する。

CUDAではスレッド管理の最上位にグリッドという階層を設けている。図1にスレッド管理の概念図を示す。グリッドの中にはブロックという階層で構成されている。グリッドの中のブロックはx方向、y方向、z方向の三次元的に配置されて管理されている。グリッド内のブロック数のx方向とy方向の最大値は65535個である。これを超えた配置すると実行時エラーになる。

さらに、同じようにブロック内にはスレッドが配置されている。またブロック内のスレッドのx方向とy方向の最大値は1024である。x方向とy方向を合わせたブロック内のスレッド数の最大値は1024個となっている。これを超えて配置すると、同じように実行時エラーになる。

図1
図1 CUDAのスレッド管理の概念図

3.2.1 ウォープ(WARP)

CUDAでは32個のスレッドがウォープという単位で管理される。スレッド中のプログラムにおいてif文などの制御構文で異なる方向に分岐する可能性があるとき、ウォープ内で は全ての分岐を実行する。ウォープ内のスレッドが多くの分岐命令含むと、GPUは多くの ルートを実行することになってしまい、GPUの性能低下を招いてしまう。これをウォープ ダイバージェントと呼ぶ。

3.3 CUDAのメモリ構造

CUDAには複数のメモリが存在している。ここではメモリの種類について説明をしてい く。図2にCUDAのメモリモデル[5]を示す。これらのメモリをどのように使うかによって 実際の性能は大きく異なる。GPGPUの演算装置はSM/SMX(Streaming Multiprocessor) であり、これは8又は32個の演算器Cudacoreのクラスタになっている。

図2
図2 CUDAのメモリモデル[5]

4 作成したプログラム

本研究ではレコメンデーションシステムにユークリッド距離を用いて類似度を計算している。そこで本章ではCUDAでユークリッド距離の計算の実装、使用したサンプルデータについて説明していく。

4.1 源データ

本研究ではCollaborative filtering dataset - dating agency [3]内のrating.datをサンプルデータとして用いる。このデータはチェコスロバキアの出会い系サイトの評価データを匿名化したものである。ファイルにはユーザID、プロフィールID、評価をカンマで区切ったデータが17,358,346件格納されている。図3にデータの例を示す。ユーザIDは135359件、プロフィールIDは220970件である。プロフィールIDはユーザが他のユーザに対しての評価を表す。

これを前処理として各ユーザIDごとに集計する。y,i,jというデータがあればQyのi番目のデータはjとなり、無ければ0とする。そしてユーザIDyがベクトルQyとなる。図4にユーザIDごとの集計のイメージを示す。

さて、本研究ではあるユーザPを考える。ベクトルPとQyで類似度を計算し、Pに一番似ているユーザを見つける。

図3
図3 rating.datに格納されているデータ
図4
図4 ユーザIDごとの集計のイメージ

4.2 ユークリッド距離を求めるアルゴリズム

本研究で用いたユークリッド距離を求めるアルゴリズムをフローチャートで図5に示す。

図5
図5 ユークリッド距離を求めるアルゴリズム

全体の構成としては初めにCPU側でデータを用意し、GPUにコピーしてユークリッド距離を求めるカーネル関数を呼び出している。また、今回の実装ではカーネル関数を複数作成し、式を分割して段階的に実装していく方法を採っている。これはカーネル関数によってブロックの中のスレッドの数を変更するためである。カーネル関数Recoでは式の始めの処理として全てのデータの差と、その二乗を求めている。カーネル関数Reco2とReco3では求めた差の総和を計算している。Reco4では求めた総和に平方根をとった後、正規化を行っている。また、この動作は全てのデータにおいて計算を終えるまでループしている。また、それぞれのカーネル関数については後で説明をしていく。

4.3 カーネル関数Reco

ここではカーネル関数Recoが行うデータの差の二乗を求める計算についてCUDAによってどのように並列化されていくか説明していく。

先の2.3において説明をしたユークリッド距離を求める式(1)

式1

より初めに 式2 を計算する。

カーネル関数Recoは顧客ベクトルPとQのポインタが渡されると各スレッドで実行され、それぞれの成分の差の二乗を求める。それぞれのスレッド毎にカーネル関数をコピーして実行している。これによりスレッドで並列に実行することが出来る。つまり、スレッドidがy,xの時、rxの値としてQy-Pのx個目のデータの差の二乗 式3 を計算する。ブロックやスレッドをx方向とy方向にそれぞれ管理することでユーザIDのデータの差を全て並列に計算することが出来る。図6に動作のイメージを示す。

図6
図6 カーネル関数Recoの動作イメージ

プログラム中の各変数の意味を次に示す。

予約変数

グローバル変数

変数

ratings型の構造体のメンバ

この動作をするコードを図7に示す。


__global__ void Reco(ratings *myda, ratings *da, ratings *g_s_da)
{

 unsigned int x = blockIdx.x * blockDim.x + threadIdx.x;
 unsigned int y = blockIdx.y * blockDim.y + threadIdx.y;

 if(y < USER && x < ID){
  int idx = y*ID+x;
  g_s_da[idx].rate = myda[x].rate-da[idx].rate;
  g_s_da[idx].rate = g_s_da[idx].rate*g_s_da[idx].rate;
 }	
}

図7 カーネル関数Recoのプログラム

変数xはスレッドがx方向のどのブロックIDに属しているどのスレッドかを示している。同じように変数yはy方向のどのブロックIDに属しているどのスレッドかを示している。変数idxは一次配列を二次配列のように扱うため、変数xとy*IDを足した値が代入される。構造体ratings型の変数mydaとdaのメンバrateの差を変数g_s_dataのメンバrateに代入して二乗する。全てのスレッドで同じコードを実行し、変数xとyの値により、スレッドがどのブロックのどのスレッドに属しているかが分かり、データの差の二乗を求めるという動作を並列に実行することが出来る。

4.4 カーネル関数Reco2及びReco3

ここではカーネル関数Reco2及びReco3においてRecoで求めた差の二乗の和の計算について、どのようにして実装するか、またどのように高速化をしていくかについて、過程についても説明する。

4.4.1 総和計算の実装

ユークリッド距離を求めるためにベクトルRyの成分が持つ評価の差の二乗の列から和を計算する。つまり、ベクトルRyの大きさの二乗を求めている。この時CUDAではシェアードメモリを使うことで同一ブロック内のスレッドが同期しながら木構造的にスレッドの和を計算する。この時3つの手法を用いて高速化を図っている。カーネル関数Reco2ではブロック内のスレッドで部分和bnを求め、Reco3ではReco2で求めた部分和bnから和Syを求めている。また、プログラム1及びプログラム2ではReco3に当たる部分はCPUで動作しているため、カーネル関数ではない。図8に和の計算のイメージを示す。

なお、各スレッドが同バンクのシェアードメモリにアクセスすることを防ぐために、文献[1]のアルゴリズムを使用した。

図8
図8 GPUでの和の計算のイメージ

それぞれの手法で使われる変数の意味は次の通りである。

4.4.2 プログラム1(手法1)

ここでは総和計算をするためのプログラム1(手法1)について説明をしていく。手法1ではカーネル関数Recoで求めたデータの差のベクトルRyの中から一つのベクトルを抽出し、GPUにメモリ転送を行う。カーネル関数Reco2を呼び出すとGPUでブロック内のスレッドの部分和bnを求める。求めたブロック内の部分和bnをCPUにメモリ転送し、全ブロックの和Syを求める。この動作を全てのユーザIDの大きさを求めるまで繰り返す。

プログラム1のフローチャートを図9、動作のイメージを図10に示す。

図9
図9 プログラム1のフローチャート
図10
図10 プログラム1の動作イメージ

この動作をするコードを図11に示す。


for(int i = 0;i < USER; i++){
 for(int j = 0;j < ID; j++){
  onenear[j] = s_data[i*ID+j].rate;
 }
 cudaMemcpy( g_onenear, onenear, sizeof(int)*ID, cudaMemcpyHostToDevice);
 Reco2<<< grid2, block2, BLOCKSIZE3*sizeof(int) >>>(g_onenear,g_souwa_y);
 cudaThreadSynchronize();
 cudaMemcpy(souwa_y, g_souwa_y, sizeof(int)*((ID/BLOCKSIZE3)), cudaMemcpyDeviceToHost);
 a = 0.0;
 for(int j = 0;j < (ID/BLOCKSIZE3);j++){
  a += souwa_y[j];
 }
 sq_souwa_y[i] = a;
}

図11 プログラム1のmain関数の和の計算部分のプログラム

変数s_data.rateより変数onenearへユーザID一つ分のデータをfor文を使い代入する。よって変数onenearにはユーザID一つ分のベクトルcの成分が格納されている。その後cudaMemcpy関数でCPU側の変数onenearからGPU側の変数g_onenearへメモリ転送を行い、カーネル関数Reco2を呼び出す。Reco2ではブロック内のスレッドの部分和bnを求めている。そしてGPU側の変数g_souwa_yからCPU側の変数souwa_yへと再びメモリ転送を行い、Reco3に当たる部分で求めた部分和から和Syを求めている。この時for文の条件にはj<(ID/BLOCKSIZE3)となっている。IDはプロフィールIDのデータの数となっており、BLOCKSIZE3はブロック内のスレッドの数となっている。ブロック内で部分和を求めているためIDをスレッド数で割っている。この動作を変数USERの値まで繰り返し行なっている。

4.4.3 プログラム2(手法2)

ここでは総和計算をするためのプログラム2(手法2)について説明していく。手法2ではカーネル関数Recoで求めたデータの差のベクトルRyをGPUにメモリ転送を行う。そしてReco2を呼び出し、ブロック内のスレッドの部分和bnを求める。その後求めたブロック内の部分和bnをCPUにメモリ転送し、CPUで全ブロックの和Syを求める。

プログラム2のフローチャートを図12、動作のイメージを図13に示す。

図12
図12 プログラム2のフローチャート
図13
図13 プログラム2の動作イメージ

この動作をするコードを図14に示す。


Reco2<<< grid2, block2, BLOCKSIZE3*sizeof(int) >>>(g_s_data,g_souwa_y);
cudaThreadSynchronize();

cudaMemcpy( souwa_y, g_souwa_y, sizeof(int)*(((USER*ID)/BLOCKSIZE3)), cudaMemcpyDeviceToHost);

n = 0;
j = 0;

for(i = 0; i < USER; i++){
 a = 0.0;
 for(j = n;j < n+(ID/BLOCKSIZE3);j++){
  a += souwa_y[j];
 }
 sq_souwa_y[i] = a;
 n = j;
}

図14 プログラム2のmain関数の和の計算部分のプログラム

この手法では手法1と比べ、ユーザID一つ分のベクトルを抽出する必要が無いため、初めにカーネル関数Reco2を呼び出し、ブロック内のスレッドで部分和bnを計算している。その後GPU側の変数g_souwa_yからCPU側の変数souwa_yへメモリ転送を行い、Reco3に当たる部分でユーザID毎にReco2で求めた部分和から和SyをCPU上でfor文を使い求め、計算した値を変数sq_souwa_yに代入している。この手法では全てのユーザIDのデータを全て一度のカーネル関数呼び出しで実行するためにブロック内のスレッド数で割り切れる値になるようにデータの最後にダミーのデータを置くことで割り切れるようにデータの数を揃えている。手法1と比べるとデータの抽出するという動作が無くなり、メモリ転送を行う回数が減っている。

4.4.4 プログラム3(手法3)

ここでは総和計算をするためのプログラム3(手法3)について説明していく。手法3ではカーネル関数Recoで求めたデータの差のベクトルRyをGPUにメモリ転送し、Reco2でGPUを使ってブロック内のスレッドの部分和bnを求める。その後求めたブロック内の部分和bnをReco3に引数として渡し、Reco3のスレッドに割り当て、和Syを求める。

プログラム3のフローチャートを図15、動作のイメージを図16に示す。

図15
図15 プログラム3のフローチャート
図16
図16 プログラム3の動作イメージ

この動作をするコードを図17に示す。


Reco2<<< grid2, block2, BLOCKSIZE3*sizeof(int) >>>(g_s_data,g_souwa_y);
cudaThreadSynchronize();

Reco3<<< grid3, block3, BLOCKSIZE4*sizeof(int) >>>(g_souwa_y,g_sq_souwa_y);
cudaThreadSynchronize();

図17 プログラム3のmain関数の和の計算部分のプログラム

この手法では手法2と同じようにデータの抽出の必要が無いため、初めにカーネル関数Reco2を呼び出し、ブロック内の部分和bnを求めている。次に求めた部分和から和を求めるが、この手法ではそれぞれのブロックで求めた部分和をCPUで計算せずにカーネル関数Reco3を呼び出しReco2で求めた各ブロックで求めた部分和をReco3のスレッドに割り当てて和Syを計算している。この手法では手法2でデータの後ろにダミーデータを置いて割り切れるように揃えていたが、更にカーネル関数Reco3で和を計算をするために更に割り切れるようにダミーデータを置くことで数を揃えている。このためダミーデータの数を増えてしまうのが問題になる可能性が考えられる。またCPUで計算を行うことが無いため、メモリ転送を行う必要が無いため高速化出来ると考えられる。

5 評価

5.1 測定環境

今回の計測に使用したPCの環境を表1に示す。

表1 測定環境
OSWindows7 Professional 64bit
CPUIntel Core i7-4820k CPU 3.70GHz
GPUNVIDIA GeForce GTXTITAN×2
CUDAVer5.5
RAM32GB
源データ参考文献[3]サイト内のrating.dat

5.2 測定結果

それぞれのプログラムを各5回ずつ実行した平均の結果を表2及び図18に示す。CPUのみで動作するプログラム0と比べ、プログラム1(手法1)では1.22倍、プログラム2(手法2)では9.65倍、プログラム3(手法3)では10.02倍高速化出来た。プログラム1ではベクトルRyの中からユーザID一つ分のデータを抽出をしてGPUに転送しているためカーネル関数の呼び出し回数とメモリ転送の回数が増えたことがロスになっているのではないかと考える。プログラム2とプログラム3ではどちらもCPUの実行時間と比べ約10倍以上の高速化出来た。またプログラム2とプログラム3を比べた場合プログラム3の実行時間のほうが短くなった。これはプログラム2ではReco3に当たる部分をCPUで動作させるためにメモリ転送をしていたがプログラム3ではGPUで動作させているため、その分メモリ転送が無くなったため、高速化に繋がったのではないかと考えられる。

表2 実行時間
プログラム0[s]プログラム1[s]プログラム2[s]プログラム3[s]
1152.568122.62415.77815.179
2150.453125.63215.77515.15
3152.58124.6915.74615.129
4151.574124.65915.67615.12
5151.022122.17215.61815.138
平均151.6394123.955415.718615.1432
図18
図18 実行時間

6 まとめ

本研究ではレコメンデーションシステムをGPGPUで高速化することを目標に研究を行った。結果としてCPUのみで動作するプログラムに比べ、最大で約10倍高速化出来た。3つの手法を用いて高速化したが、速い手法とあまり速くない手法があった。これはメモリ転送の回数やカーネル関数の呼び出し回数が多い手法だったためあまり速くならない手法になってしまったのだと考えられる。また約10倍高速化出来たが、より高速化出来る手法を見つけていきたい。

7 参考文献

  1. 青木尊之、額田彰 “はじめてのCUDAプログラミング”工学社
  2. Sean Owen、Robin Anil、Ted Dunning、Ellen Friedman“Mahoutイン・アクション”オライリー・ジャパン
  3. Collaborative filtering dataset - dating agency http://www.occamslab.com/petricek/data/
  4. レコメンドウェアハウス® http://www.activecore.jp/column/recommend_01_01.html
  5. CUDAテクニカルトレーニング Vol. I:CUDAプログラミング入門 http://www.nvidia.co.jp/docs/IO/59373/VolumeI.pdf

8 付録