基本概念
通過深度學習進行視頻處理是一個非常複雜的領域,因為它需要處理空間和時間兩個方面。 為了總結現代比較流行的方法是什麼,在這篇文章中我們將對視頻回歸任務的深度學習方法進行一些研究。
處理視頻意味著處理圖像,所以這裡需要cnn。但是,有不同的方法來處理時態組件。我試著概括出主要的幾點:
現有的方法
1、只使用CNN的回歸
史丹福大學的一篇非常有趣的論文(http://vision.stanford.edu/pdf/karpathy14.pdf)討論了視頻分類任務中可能遇到的挑戰,並提供了處理這些挑戰的方法(這些方法也可以應用於有輕微變化的回歸問題)。簡而言之,作者嘗試了各種CNNs架構來跨時間組件融合信息,並學習時空特徵。
2、3d CNN
這種方法背後的邏輯非常直觀,因為卷積中的第三維可以對應於時間域,從而可以直接從一個網絡學習時空特徵。
3、長期循環卷積網絡(LRCN)
2016年,一組作者提出了用於視覺識別和描述的端到端可訓練類架構。其主要思想是使用CNNs從視頻幀學習視覺特徵,使用LSTMs將圖像嵌入序列轉換為類標籤、句子、概率或任何您需要的東西。因此,我們用CNN對原始的視覺輸入進行處理,CNN的輸出被輸入到一堆遞歸序列模型中。
在我看來,LRCN架構在實現方面似乎比其他架構更有吸引力,因為您必須同時練習卷積和循環網絡。
我是個電影迷,已經看了大約一千部電影。基於此,我決定對電影預告片進行深度學習,預測預告片的收視率。不幸的是,收集訓練數據集非常困難,因為除了下載視頻之外,我還需要對它們進行標記。為了擴展數據集,我們可以應用一些數據增強,對於視頻可以旋轉,添加不同種類的噪聲,改變速度等。然而,最初的相機角度、圖像質量和電影速度都是未來評級的一部分,所以保留分數而改變這些特徵似乎是錯誤的。我決定從一個視頻中取幾個不重疊的部分然後將每個部分分割成幀最終得到n個訓練樣本
def preprocess(movie_name, train=True, n_subclips=3, subclip_duration=30, frequency=45, verbose=False):""" Preprocesses a movie trailer making subclips and then extracting sequences of frames :param movie_name: movie name :train: boolean flag to determine whether preprocessing is performed for training videos or not :param n_subclips: number of subclips to make from the trailer :param subclip_duration: duration of a subclip :param frequency: frequency of extracting frames from subclips :param verbose: increase verbosity if True """ name = '.'.join(movie_name.split('.')[:-1]) format_ = movie_name.split('.')[-1] if not format_ok(format_): print('Skipping file, which is not a video...') return if train == True: if not os.path.isdir(TRAINING_PATH): os.mkdir(TRAINING_PATH) DEST = TRAINING_PATH else: if not os.path.isdir(PREDICTION_PATH): os.mkdir(PREDICTION_PATH) DEST = PREDICTION_PATH if format_ == 'flv': #Decord does not work with flv format format_ = 'mov' #Extracting subclip from trailer base = 10 os.makedirs(f"{FPATH}/{name}", exist_ok=True) for i in range(n_subclips): if verbose: print(f"{i} iteration...") print("....Making subclip....") try: ffmpeg_extract_subclip(f"{FPATH}/{movie_name}", base, base+subclip_duration, targetname=f"{FPATH}/{name}/{i}.{format_}") base = base + subclip_duration except BaseException: print(f"Some error occured during {i+1} extraction") continue #Check if all subclips were correctly created try: video = moviepy.editor.VideoFileClip(f"{FPATH}/{name}/{i}.{format_}") if int(video.duration) <= subclip_duration//2: raise DurationError except: print(f"The {i} subclip was not correctly created, deleting...") os.remove(f"{FPATH}/{name}/{i}.{format_}") continue #Creating frames if verbose: print("....Extracting frames....") os.makedirs(f"{DEST}/{name+'_'+str(i)}", exist_ok=True) #Creating directory for Train dataset try: video_to_frames(f"{FPATH}/{name}/{i}.{format_}", f"{DEST}/{name+'_'+str(i)}", overwrite=False, every=frequency) except: print("Error occured while executing VIDEO_TO_FRAMES") os.rmdir(f"{DEST}/{name+'_'+str(i)}/{i}") os.rmdir(f"{DEST}/{name+'_'+str(i)}") continue #Delete directory with subclips if name in os.listdir(f"{FPATH}"): shutil.rmtree(f"{FPATH}/{name}")
現在我們可以開始構建網絡架構並訓練模型。原論文中的CNN base是對CaffeNet的修改,但為了簡單和更快的訓練,我只創建了兩個卷積層(batch normalization) dropout和max pooling作為編碼器,兩個稠密層作為解碼器。雖然該技術通過平均LSTM輸出用於視覺識別,但我們只要將結果使用softmax函數去掉就可以使用相同的方法進行視頻回歸,。
class CNN(torch.nn.Module):def __init__(self, channels1=10, channels2=20, embedding_size=15, activation='relu'): super(CNN, self).__init__() self.layer1 = torch.nn.Sequential(torch.nn.Conv2d(3, channels1, kernel_size=5, padding=2), activation_func(activation), torch.nn.BatchNorm2d(num_features=channels1), torch.nn.MaxPool2d(2, 2)) self.layer2 = torch.nn.Sequential(torch.nn.Conv2d(channels1, channels2, kernel_size=3, padding=1), torch.nn.Dropout2d(p=0.2), activation_func(activation), torch.nn.BatchNorm2d(num_features=channels2), torch.nn.MaxPool2d(2, 2)) self.encoder = torch.nn.Sequential(self.layer1, self.layer2) self.decoder = torch.nn.Sequential(torch.nn.Linear(56*56*channels2, 6000), activation_func(activation), torch.nn.Linear(6000, 500), activation_func(activation), torch.nn.Linear(500, embedding_size)) def forward(self, x): x = self.encoder(x) #flatten heatmap before utilizing dense layers x = x.view(x.size(0), x.size(1) * x.size(2) * x.size(3)) out = self.decoder(x) return outclass LRCN(torch.nn.Module): def __init__(self, channels1=10, channels2=20, embedding_size=15, LSTM_size=64, LSTM_layers=1, activation='relu'): super(LRCN, self).__init__() self.cnn = CNN(channels1, channels2, embedding_size, activation) #batch first: data formatted in (batch, seq, feature) self.rnn = torch.nn.LSTM(input_size=embedding_size, hidden_size=LSTM_size, num_layers=LSTM_layers, batch_first=True) self.linear = torch.nn.Linear(LSTM_size, 1) def forward(self, x): heatmaps = [] for seq in x: heatmaps.append(self.cnn.forward(seq)) heatmaps = torch.stack(heatmaps) out, (_, _) = self.rnn(heatmaps) out = self.linear(out) return out[:,-1,:]
一旦我們準備好了數據和模型,就該開始訓練了。我選擇的默認參數不會導致高質量但快速的訓練。
def fit(self, dir_names, X_test, y_test, lr=3e-4, loss_name='mse', n_epoch=5, batch_size=10, device='cpu', saving_results=False, use_tensorb=False, verbose=False):optimizer = torch.optim.Adam(self.parameters(), lr=lr) loss = loss_choice(loss_name) #Specifying loss function dir_names = list(filter(lambda x: os.path.isdir(f"{TRAINING_PATH}/{x}"), dir_names)) #Filtering waste files random.shuffle(dir_names) train_loss_history = [] test_loss_history = [] learning_dir_names = dir_names.copy() #Training model print('---------------TRAINING----------------') for epoch in range(n_epoch): dir_names = learning_dir_names.copy() train_loss = 0 for i in range(0, len(learning_dir_names), batch_size): optimizer.zero_grad() print(dir_names) X_batch, y_batch, dir_names = load_data(dir_names, train=True, verbose=verbose, batch_size=batch_size) X_batch = X_batch.to(device).float() y_batch = y_batch.to(device).float() preds = self.forward(X_batch).view(y_batch.size()[0]) loss_value = loss(preds, y_batch) loss_value.backward() train_loss += loss_value.data.cpu() optimizer.step() train_loss_history.append(train_loss) with torch.no_grad(): test_preds = self.forward(X_test).view(y_test.size()[0]) test_loss_history.append(loss(test_preds, y_test).data.cpu()) print(f"{epoch+1}: {loss_name} = {test_loss_history[-1]}") if saving_results==True: torch.save(self.state_dict(), MODEL_WEIGHTS) print('---------------------------------------') return [train_loss_history, test_loss_history]
即使訓練一個簡單的端到端LRCN模型也需要大量的計算能力和時間,因為我們的任務包括圖像和序列。此外,訓練數據集必須相當大,z這樣模型可以很好地推廣。從下圖可以看出,經過訓練後的模型存在明顯的擬合不足。
總結
LRCN是一種用於處理視覺和時間輸入的模型,它提供了很大的靈活性,可應用於計算機視覺的各種任務,並可合併到CV處理管道中。然後這種方法可用於各種時變視覺輸入或序列輸出的問題。LRCN提供了一個易於實現和訓練的端到端模型體系結構。
本文代碼: github/DJAlexJ/LRCN-for-Video-Regression
作者:Alexander Golubev
deephub翻譯組