🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
PyTorch

PyTorchは、オープンソースのPython向けの機械学習ライブラリ。Facebookの人工知能研究グループが開発を主導しています。強力なGPUサポートを備えたテンソル計算、テープベースの自動微分による柔軟なニューラルネットワークの記述が可能です。

NumPy

NumPyはPythonのプログラミング言語の科学的と数学的なコンピューティングに関する拡張モジュールです。

OpenCV

OpenCV(オープンソースコンピュータービジョン)は、1999年にインテルが開発・公開したオープンソースのコンピュータビジョン向けのクロスプラットフォームライブラリです。

並列処理

複数の計算が同時に実行される手法

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

2回答

1568閲覧

動画に対する推論の高速化について

daikooooooon

総合スコア9

PyTorch

PyTorchは、オープンソースのPython向けの機械学習ライブラリ。Facebookの人工知能研究グループが開発を主導しています。強力なGPUサポートを備えたテンソル計算、テープベースの自動微分による柔軟なニューラルネットワークの記述が可能です。

NumPy

NumPyはPythonのプログラミング言語の科学的と数学的なコンピューティングに関する拡張モジュールです。

OpenCV

OpenCV(オープンソースコンピュータービジョン)は、1999年にインテルが開発・公開したオープンソースのコンピュータビジョン向けのクロスプラットフォームライブラリです。

並列処理

複数の計算が同時に実行される手法

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

3グッド

1クリップ

投稿2020/12/30 11:28

編集2021/01/18 13:20

動画に対して、1024×1024のフレーム画像を取得し、256×256の領域64個(縦8×横8、オーバーラップあり)に対して識別器を適用し、出力した確率を格納した(フレーム数,8,8)のnumpy配列を作成したいのですが、現在の実装では
①OpenCVでフレーム画像読み込み
②PIL画像に変換
③識別する領域をクロップして変数に格納
④識別
⑤出力した確率を、配列の対応する場所に格納
⑥③へ
という単純な実装で、処理時間が1フレームあたり約1秒かかり、とても遅いです。
処理したい動画は30000フレームちょっとあり、10時間ほどかかります。
0.1秒ほどで処理するのが理想で、高速化のためいくつか思いついたことを試したのですが上手くいきません。
高速化する良い方法がありましたら教えていただきたいです

Python

1 #推論 2 for i in tqdm(range(num_frame), position=0): 3 cap.set(cv2.CAP_PROP_POS_FRAMES, i+1) 4 ret, frame = cap.read() 5 6 if not ret: 7 break 8 else: 9 pil_frame = cv2pil(frame) 10 11 with torch.no_grad(): 12 for j in range(len(x)): 13 for k in range(len(y)): 14 cropped_img = pil_frame.crop((int(x[j]), int(y[k]), int(x[j])+256, int(y[k])+256)) 15 cropped_img_tensor = transform(cropped_img, phase="test").unsqueeze(0) #transformしたあと、推論するためバッチの次元を追加 16 17 output = model(cropped_img_tensor) 18 prob = softmax(output)[0][1].item() #softmaxを計算 19 prob_array[i][k][j] = prob

↓実行時間計測

Python

1 for i in tqdm(range(num_frame), position=0): 2 a = time.time() ############################### 3 cap.set(cv2.CAP_PROP_POS_FRAMES, i+1) 4 ret, frame = cap.read() 5 b = time.time()############################ 6 7 if not ret: 8 break 9 else: 10 pil_frame = cv2pil(frame) 11 12 c = time.time() ####################### 13 14 with torch.no_grad(): 15 for j in range(len(x)): 16 for k in range(len(y)): 17 cropped_img = pil_frame.crop((int(x[j]), int(y[k]), int(x[j])+256, int(y[k])+256)) 18 cropped_img_tensor = transform(cropped_img, phase="test").unsqueeze(0) #transformしたあと、推論するためバッチの次元を追加 19 20 output = model(cropped_img_tensor) 21 prob = softmax(output)[0][1].item() #softmaxを計算 22 prob_array[i][k][j] = prob 23 24 d = time.time() ############################## 25 print(b-a) 26 print(c-b) 27 print(d-c)

0%| | 0/32762 [00:00<?, ?it/s]
0.05161237716674805
0.017586469650268555
2.1721038818359375
0%| | 1/32762 [00:02<20:23:48, 2.24s/it]
0.044759511947631836
0.016994237899780273
0.6606061458587646
0%| | 2/32762 [00:02<16:14:59, 1.79s/it]
0.056168556213378906
0.016913175582885742
0.6579580307006836
0%| | 3/32762 [00:03<13:22:14, 1.47s/it]
0.06777358055114746
0.01434183120727539
0.6527915000915527
0%| | 4/32762 [00:04<11:21:56, 1.25s/it]
0.07939982414245605
0.014380693435668945
0.6701173782348633
0%| | 5/32762 [00:05<10:02:29, 1.10s/it]
0.09820103645324707
0.014620542526245117
0.6635544300079346
0%| | 6/32762 [00:05<9:08:54, 1.01s/it]
0.10292172431945801
0.015182971954345703
0.6512439250946045
0%| | 7/32762 [00:06<8:30:15, 1.07it/s]
0.11549758911132812
0.01836109161376953
0.649014949798584
0%| | 8/32762 [00:07<8:05:24, 1.12it/s]
0.13360857963562012
0.016347885131835938
0.6655523777008057

これはVGG16で実行したときの時間です
32762はフレーム数です
一番時間がかかるのは推論だと思うのですが、なぜかモデルをMobileNet_v2にしてもほとんど実行時間は変わりませんでした

#追記
・1/4
cap.read()すると勝手に次のフレームに行くみたいなので、余計なcap.setをコメントアウトしたところ、実行時間が約1.3倍になり、7時間ほどで処理できるようになりました。
30000フレームちょっとの動画の
read()に7分
cv2pilに8分
cropとtransformに30分
output = model(cropped_img_tensor)に約6時間
かかるようです。

Python

1 for i in tqdm(range(num_frame), position=0): 2 #cap.set(cv2.CAP_PROP_POS_FRAMES, i+1) 3 ret, frame = cap.read()

・1/5
dataloaderを使用し、64枚一気に推論するように変更したところ、1秒で2.6フレームほど処理可能で、3時間ちょっとで処理できるようになりました。
しかし、以前の実装では大丈夫だったのですが、なぜか実行毎に推論結果が同じにならず、原因を調査中です。

Python

1getitemを変更 2 def __getitem__(self, index): 3 i = index // len(self.x) 4 j = index % len(self.y) 5 cropped_img = self.frame.crop((int(self.x[j]), int(self.y[i]), int(self.x[j])+256, int(self.y[i])+256)) 6 cropped_img = self.transform(cropped_img) 7 return cropped_img 8 9推論部分を変更 10 for i in tqdm(range(num_frame), position=0): 11 ret, frame = cap.read() 12 13 if not ret: 14 break 15 else: 16 pil_frame = cv2pil(frame) 17 18 train_dataset = Dataset(x, y, pil_frame, transform=Transform(resize, mean, std), phase="test") 19 train_dataloader = torch.utils.data.DataLoader(train_dataset, batch_size=64, num_workers=2, pin_memory=True) 20 21 with torch.no_grad(): 22 for inputs in train_dataloader: 23 if use_cuda: 24 inputs = inputs.cuda() 25 outputs = model(inputs) 26 prob_array[i] = softmax(outputs)[:, 1].to('cpu').detach().numpy().copy().reshape(len(x), len(y)) #8×8にreshape

・1/8
torch.manual_seed(1)とrandom.seed(1)を行うと推論結果が実行毎に同じになるようになりましたが、以前のように1枚ずつ推論していたときとは少し異なる結果となっています
1枚ずつ推論する方では、実行結果が毎回同じになっています
・1/17
解決しました!!
私の初歩的なミスでした。申し訳ありません。
データ拡張のTransformを

Python

1class Transform(): 2 def __init__(self, resize, mean, std): 3 self.data_transform = { 4 "train": transforms.Compose([ 5 #Histogram_Equalization(), 6 Luminance_Histogram_Equalization(), 7 transforms.RandomRotation((-20,20)), 8 transforms.RandomVerticalFlip(), 9 transforms.RandomHorizontalFlip(), 10 transforms.Resize(resize), 11 transforms.ToTensor(), 12 transforms.Normalize(mean, std) 13 ]), 14 "test": transforms.Compose([ 15 #Histogram_Equalization(), 16 # Luminance_Histogram_Equalization(), 17 transforms.Resize(resize), 18 transforms.ToTensor(), 19 transforms.Normalize(mean, std) 20 ]) 21 } 22 23 def __call__(self, img, phase="train"): 24 return self.data_transform[phase](img)

というふうに、Transformのtrainとtestを辞書型にしていて、デフォルトでtrainの方を呼び出すようにしていたのが原因でした。
datasetのgetitemを変更し

Python

1✖ cropped_img = self.transform(cropped_img) 2〇 cropped_img = self.transform(cropped_img, self.phase)

とすることで、ちゃんとtest用のtransformをするようになり、最初に書いていた1枚ずつ推論するコードとまったく同じ結果になりました
seedを固定しないと結果が同じにならなかったのは、train用のtransformのRandom~等の影響だと思います
また、test時には不要なtransformの処理もなくなり、少し早くなって1秒当たり3.9フレームほど処理できるようになり、2時間20分で推論できるようになりましたが、まだ1時間を切れていないので、さらに高速化できればまた追記します
A_kirisakiさん、様々な助言を下さり、本当にありがとうございました。
・1/18
画像の読み込み等をデータローダーで並列化し、1秒で6.5フレームほど処理でき、1時間20分で終わるようになりました

Python

1キャッシュを使用してフレーム画像を取得するクラスを追加 2class Frame: 3 def __init__(self, cap): 4 self.cap = cap 5 6 @lru_cache(maxsize=1) # キャッシュ 7 def __call__(self, frame_num): 8 # self.cap.set(cv2.CAP_PROP_POS_FRAMES, frame_num) 9 ret, frame = self.cap.read() 10 pil_frame = cv2pil(frame) 11 return pil_frame 12 13Frameクラスに合わせて__getitem__を変更 14class Dataset(data.Dataset): 15 def __init__(self, x, y, cap, num_frame, transform=None, phase="test"): 16 self.x = x 17 self.y = y 18 self.num_frame = num_frame 19 self.transform = transform 20 self.phase = phase 21 self.get_frame = Frame(cap) 22 23 def __len__(self): 24 return len(self.x) * len(self.y) * self.num_frame 25 26 def __getitem__(self, index): 27 frame_num = index // (len(self.x) * len(self.y)) # index // 8 * 8 28 i = (index - (frame_num * len(self.x) * len(self.y))) // len(self.x) 29 j = (index - (frame_num * len(self.x) * len(self.y))) % len(self.y) 30 self.frame = self.get_frame(frame_num) 31 cropped_img = self.frame.crop((int(self.x[i]), int(self.y[j]), int(self.x[i])+256, int(self.y[j])+256)) 32 cropped_img = self.frame.crop((int(self.x[j]), int(self.y[i]), int(self.x[j])+256, int(self.y[i])+256)) 33 cropped_img = self.transform(cropped_img, self.phase) 34 return cropped_img 35 36Frameクラスのおかげで、1フレームずつ読み込んでそのたびにデータローダーを宣言する必要が無くなったので、推論部分を変更 37 train_dataset = Dataset(x, y, cap, num_frame, transform=Transform(resize, mean, std), phase="test") 38 train_dataloader = torch.utils.data.DataLoader(train_dataset, batch_size=64, num_workers=1, pin_memory=True) # num_workersは2にすると変なメッセージが出る上に、1と速度が大差ないので1 39 40 model.eval() 41 print("-------------") 42 print("推論開始") 43 44 #推論 45 with torch.no_grad(): 46 for i, inputs in enumerate(tqdm(train_dataloader)): 47 if use_cuda: 48 inputs = inputs.cuda() 49 outputs = model(inputs) 50 prob_array[i] = softmax(outputs)[:, 1].to('cpu').detach().numpy().copy().reshape(len(x), len(y))

目標の50分まではまだ届いていませんが思いつくことは大体やったので、解決にしようと思います。
ありがとうございました。

hoshi-takanori, A_kirisaki👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

hoshi-takanori

2020/12/30 12:08

まずは各ステップの処理時間を計測することから始めましょう。
daikooooooon

2020/12/30 14:59

アドバイスをしていただきありがとうございます。 計測結果を追記しました。
A_kirisaki

2020/12/30 15:10

PyTorch っていい感じに並列化してくれたりしてくんないんですか(エアプ)?for 文中のそれぞれの処理時間が気になるところですね。意外と PIL がヤバそう
daikooooooon

2020/12/30 15:31

256×256の画像を1枚ずつ推論するのではなく、PyTorchのデータローダーを用いれば並列化できそうな気がしますね。明日やって見たいと思います。 動画の読み込みにOpenCVを使わないといけないと思うのですが、PyTorchの学習と推論時の画像読み込みやデータ拡張にPILを用いているので、cv2pilというqiitaからとってきた関数で変換させています。 自分のノートPCで、1024*1024の200kbぐらいの画像を10000回cv2pilするとだいたい10分ほどでした。 推論したい動画は30000フレームほどなので約30分ほどですが、フレーム読み込みとPIL変換を並列化できれば問題ないような気もしますし、今30000フレームちょっとの動画に36000秒(10時間)ほどかかるので、もっと改善できる点がある気がします。 ありがとうございます。
退会済みユーザー

退会済みユーザー

2021/01/05 22:00

> output = model(cropped_img_tensor)に約6時間 ご察しの通り、恐らく30000フレーム(30000バッチ)に約6時間です。並列処理で128枚同時にバッチ(128,255,255,3)に突っ込めれば230倍くらいは早くなり約2.8分で捌けそうです。512枚同時なら43秒くらいでしょうか。
daikooooooon

2021/01/06 14:50

ありがとうございます 128はちょっと実行できるか分かりませんが、実行毎に推論結果が同じにならない問題を解決してからやって見ようと思います
A_kirisaki

2021/01/07 04:08

機械学習の推論結果って基本完全一致はしないんじゃなかったでしたっけ(学生時代のうろ覚え)?ましてや並列ともなるともともとのアルゴリズムに順序依存がなかったか考える必要がありそう。
daikooooooon

2021/01/08 09:08

学習済みのモデルで推論を行っているだけなので演算結果が同じだと思うのですが、 torch.manual_seed(1) np.random.seed(1) random.seed(1) について調べた結果、 torch.manual_seed(1) random.seed(1) を行うと推論結果が実行毎に同じになるようになりました。 torch.manual_seed(1)だけ、もしくは random.seed(1)だけだと同じにならないので、この2つが関係があるようです ですが、以前のように1枚ずつ推論していたときとは少し異なる結果となっているので、それについても調べてみます。 ありがとうございます。
daikooooooon

2021/01/12 17:38

記事まで報告して頂いてありがとうございます????‍♂️ 申し訳ないのですが、他に実装しないといけないものがあり、少し忙しいので、後日試したいと思います。
退会済みユーザー

退会済みユーザー

2021/01/17 02:13

解決できたようで何よりです。解答欄に解決策をのせてCloseするようにお願いします。今後同じ問題に当たった人が助かりますので…。
daikooooooon

2021/01/17 02:24

fourteenlengthさん 助言をいただきありがとうございました 推論結果が同じにならない問題は、元々の高速化の問題解決の中で生じたもので、高速化についてはまだ理想が達成できていないので、もうしばらくしてからそうしたいと思います
guest

回答2

0

自己解決

追記に記載-----

######・1/21
さらに1.5倍ほど高速化し、1つの動画に約55分という理想に近い速度で処理できるようになりました!
PILのクロップをしているあたりに時間がかかっていたので、
https://blog.shikoan.com/pytorch-extract-patches/
を参考に、フレーム画像をTensorに変換し、それからunfoldを使って一気にクロップするように変更しました

Python

1Transformでunfoldするクラス 2int(x[1])109で、フレーム画像から画像をcropするときの画像間のstep幅 3class Make_Patch: 4 def __init__(self, x): 5 self.step = int(x[1] * (224 / 256)) # resize後のstepなので、* (224 / 256) 6 7 def __call__(self, img): 8 imgs = img.unsqueeze(0).unfold(2, 224, self.step).unfold(3, 224, self.step).permute([0, 2, 3, 1, 4, 5]).reshape(-1, 3, 224, 224) 9 return imgs 10 11Transformを変更 12 "test": transforms.Compose([ 13 transforms.Resize(resize), 14 transforms.ToTensor(), 15 transforms.Normalize(mean, std), 16 Make_Patch(x), 17 ]) 18 19Datasetを変更 20class Dataset(data.Dataset): 21 def __init__(self, cap, num_frame, transform=None, phase="test"): 22 self.cap = cap 23 self.num_frame = num_frame 24 self.transform = transform 25 self.phase = phase 26 27 def __len__(self): 28 return self.num_frame ################################# 29 30 def __getitem__(self, index): 31 ret, self.frame = self.cap.read() 32 self.frame = cv2pil(self.frame) 33 cropped_img = self.transform(self.frame, self.phase)####################### 34 return cropped_img 35 36Transformのresizeは256224にしていたが、今回の実装ではフレーム画像をリサイズしてTensorにしてunfoldするので、1024896にリサイズ 37resize = 896 # 224/256 = 0.875, 1024*0.875 = 896 38 39推論部分を変更 40 #推論 41 with torch.no_grad(): 42 for i, inputs in enumerate(tqdm(dataloader)): 43 inputs = inputs.reshape(-1, 3, 224, 224) # 先頭に余計な次元1があるので消す 44 if use_cuda: 45 inputs = inputs.cuda() 46 outputs = model(inputs) 47 if inputs.size()[0] == batch_size * len(x) * len(y): # フレーム数がバッチサイズで割り切れないときは、最後のイテレーションの代入はインデックス指定を変更 48 prob_array[i*batch_size:i*batch_size+batch_size] = softmax(outputs)[:, 1].to('cpu').detach().numpy().copy().reshape(batch_size, len(x), len(y)) 49 else: 50 prob_array[i*batch_size:] = softmax(outputs)[:, 1].to('cpu').detach().numpy().copy().reshape(-1, len(x), len(y))

投稿2021/01/18 13:21

編集2021/01/21 12:22
daikooooooon

総合スコア9

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

↓↓↓↓↓↓↓↓↓↓

投稿2021/01/18 13:23

編集2021/01/22 09:29
daikooooooon

総合スコア9

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.36%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問