介紹
我們可以使用計算機視覺和深度學習做很多事情,例如檢測圖像中的對象,對這些對象進行分類,從電影海報中生成標籤。
這一次,我決定將注意力轉向計算機視覺中不太引人注目的方面-視頻!我們正以前所未有的速度消費視頻內容。我覺得對數據科學家來說這個計算機視覺的領域具有很大的潛力。
我很好奇將相同的計算機視覺算法應用於視頻數據。我用於構建圖像分類模型的方法是否可以推廣?
對於機器來說,視頻可能很棘手。它們的動態特性與圖像的靜態特性相反,這可能使數據科學家構建這些模型變得複雜。
但不要擔心,它與處理圖像數據沒有什麼不同。在本文中,我們將使用Python構建我們自己的視頻分類模型。這是一個非常實用的教程,所以準備好Jupyter Notebook,這將是一個非常有趣的過程。
目錄
- 視頻分類概述
- 構建視頻分類模型的步驟
- 探索視頻分類數據集
- 訓練視頻分類模型
- 評估視頻分類模型
視頻分類概述
你會如何定義視頻?
我們可以說視頻是按特定順序排列的一組圖像的集合。這些圖像也稱為幀。
這就是為什麼視頻分類問題與圖像分類問題沒有什麼不同。對於圖像分類任務,我們採用圖像,使用特徵提取器(如卷積神經網絡或CNN)從圖像中提取特徵,然後基於這些提取的特徵對該圖像進行分類。視頻分類僅涉及一個額外步驟。
我們首先從給定視頻中提取幀。然後,我們可以按照與圖像分類任務相同的步驟進行操作。這是處理視頻數據的最簡單方法。
實際上有多種其他方式來處理視頻,甚至還有視頻分析領域。我們將使用CNN從視頻幀中提取特徵。
構建視頻分類模型的步驟
建立一個能夠將視頻分類到各自類別的模型很興奮吧!我們將研究UCF101 – Action Recognition Data Set(動作識別數據集),它包含13,320種不同的視頻片段,屬於101個不同的類別。
讓我總結一下我們將構建視頻分類模型的步驟:
- 瀏覽數據集並創建訓練和驗證集。我們將使用訓練集來訓練模型和驗證集來評估模型
- 從訓練集以及驗證集中的所有視頻提取幀
- 預處理這些幀,然後使用訓練集中的幀來訓練模型。使用驗證集中的幀來評估模型
- 一旦我們對驗證集上的性能感到滿意,就可以使用訓練好的模型對新視頻進行分類
我們現在開始探索數據吧!
探索視頻分類數據集
你可以從官方UCF101站點(https://www.crcv.ucf.edu/data/UCF101.php)下載數據集。數據集採用.rar格式,因此我們首先必須從中提取視頻。創建一個新文件夾,假設為"視頻"(你也可以選擇任何其他名稱),然後使用以下命令提取所有下載的視頻:
unrar e UCF101.rar Videos/
UCF101的官方文件指出:
"在訓練和測試中,將屬於同一組的視頻分開是非常重要的。由於組內的視頻都是來自一個較長的視頻,所以在訓練集和測試集上共享來自同一組的視頻可以獲得較高的性能。"
因此,我們將按照官方文檔中的建議將數據集拆分為訓練和測試集。你可以從這裡下載訓練/測試集(https://www.crcv.ucf.edu/data/UCF101/UCF101TrainTestSplits-RecognitionTask.zip)。請記住,由於我們處理的是大型數據集,因此你可能需要較高的計算能力。
我們現在將視頻放在一個文件夾中,將訓練/測試拆分文件放在另一個文件夾中。接下來,我們將創建數據集。打開你的Jupyter Notebook,然後按照下面的代碼塊。我們將首先導入所需的庫:
import cv2 #捕獲視頻庫
import math #數學操作庫
import matplotlib.pyplot as plt #畫圖的庫
%matplotlib inline
import pandas as pd
from keras.preprocessing import image # 預處理圖像庫
import numpy as np #數學操作庫
from keras.utils import np_utils
from skimage.transform import resize #改變圖像尺寸
from sklearn.model_selection import train_test_split
from glob import glob
from tqdm import tqdm
我們現在將視頻的名稱存儲在dataframe中:
# 導入訓練集txt文件,裡面有視頻名字列表
f = open("trainlist01.txt", "r")
temp = f.read()
videos = temp.split('\\n')
# 創建含有視頻名字列表的dataframe
train = pd.DataFrame()
train['video_name'] = videos
train = train[:-1]
train.head()
這就是.txt文件中視頻名稱的方式。它沒有正確對齊,我們需要預處理它。在此之前,讓我們為測試視頻創建一個類似的dataframe:
# 導入測試集txt文件,裡面有視頻名字列表
f = open("testlist01.txt", "r")
temp = f.read()
videos = temp.split('\\n')
# 創建含有視頻名字列表的dataframe
test = pd.DataFrame()
test['video_name'] = videos
test = test[:-1]
test.head()
接下來,我們將添加每個視頻的標籤(用於訓練和測試集)。你是否注意到視頻名稱中"/"之前的整個部分代表了視頻的標籤?因此,我們將整個字符串拆分為"/"並選擇所有視頻的標籤:
# 為訓練數據集創建標籤
train_video_tag = []
for i in range(train.shape[0]):
train_video_tag.append(train['video_name'][i].split('/')[0])
train['tag'] = train_video_tag
# 為測試數據集創建標籤
test_video_tag = []
for i in range(test.shape[0]):
test_video_tag.append(test['video_name'][i].split('/')[0])
test['tag'] = test_video_tag
下一步是什麼?現在,我們將從訓練視頻中提取幀,這些視頻將用於訓練模型。我將所有幀存儲在名為train_1的文件夾中。
因此,首先,創建一個新文件夾並將其重命名為"train_1",然後按照下面給出的代碼提取幀:
# 存儲訓練集視頻的幀
for i in tqdm(range(train.shape[0])):
count = 0
videoFile = train['video_name'][i]
cap = cv2.VideoCapture('UCF/'+videoFile.split(' ')[0].split('/')[1]) # 從給定路徑獲取視頻
frameRate = cap.get(5) #幀率
x=1
while(cap.isOpened()):
frameId = cap.get(1) #當前幀編號
ret, frame = cap.read()
if (ret != True):
break
if (frameId % math.floor(frameRate) == 0):
# 存儲在train_1文件夾
filename ='train_1/' + videoFile.split('/')[1].split(' ')[0] +"_frame%d.jpg" % count;count+=1
cv2.imwrite(filename, frame)
cap.release()
這需要一些時間,因為訓練集中有超過9,500個視頻。提取幀後,我們將在.csv文件中保存這些幀的名稱及其對應的標籤。創建此文件將有助於我們讀取下一節中將要看到的幀。
# 獲取所有圖像的名字
images = glob("train_1/*.jpg")
train_image = []
train_class = []
for i in tqdm(range(len(images))):
# 創建圖像名
train_image.append(images[i].split('/')[1])
# 創建圖像類標
train_class.append(images[i].split('/')[1].split('_')[1])
# 存儲在dataframe里
train_data = pd.DataFrame()
train_data['image'] = train_image
train_data['class'] = train_class
# 轉換dataframe為csv文件
train_data.to_csv('UCF/train_new.csv',header=True, index=False)
到目前為止,我們已經從所有訓練視頻中提取了幀,並將它們與相應的標籤一起保存在.csv文件中。現在是時候訓練我們的模型,我們將用它來預測測試集中視頻的標籤。
訓練視頻分類模型
現在是時候訓練我們的視頻分類模型了!我確信這是本教程中最受期待的部分。為了便於理解,我已將此步驟劃分為子步驟:
- 讀取我們之前為訓練提取的所有幀
- 創建一個驗證集,它將幫助我們檢查模型在看不見的數據上的表現
- 定義模型的結構
- 最後,訓練模型並保存其權重
讀取所有視頻幀
那麼,讓我們開始第一步,我們將提取幀。我們將首先導入庫:
import keras
from keras.models import Sequential
from keras.applications.vgg16 import VGG16
from keras.layers import Dense, InputLayer, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D, GlobalMaxPooling2D
from keras.preprocessing import image
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from tqdm import tqdm
from sklearn.model_selection import train_test_split
我們之前創建了一個.csv文件,其中包含每個框架的名稱及其相應的標籤,我們在這裡也進行讀取:
train = pd.read_csv('UCF/train_new.csv')
train.head()
這是前五行的樣子。我們為每個幀都有相應的標籤。現在,使用此.csv文件,我們將讀取先前提取的幀,然後將這些幀存儲為NumPy數組:
# 創建空列表
train_image = []
# 循環讀取和保存幀
for i in tqdm(range(train.shape[0])):
# 載入圖片
img = image.load_img('train_1/'+train['image'][i], target_size=(224,224,3))
# 轉換為array
img = image.img_to_array(img)
# 標準化像素值
img = img/255
# 保存到train_image列表
train_image.append(img)
# 轉換為numpy數組
X = np.array(train_image)
# 輸出形狀
X.shape
輸出:(73844,224,224,3)
我們有73,844張形狀為(224,224,3)的圖片。接下來,我們將創建驗證集。
創建驗證集
要創建驗證集,我們需要確保每個類的分布在訓練集和驗證集中都相似。我們可以使用stratify參數來做到這一點:
# 分離數據集
y = train['class']
# 創建訓練與測試集
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42, test_size=0.2, stratify = y)
這裡,stratify = y(每個幀的標籤)在訓練和驗證集中保持一個類似分布。
視頻可以被分為101類。因此,我們必須在目標中創建101個不同的列,每個列對應一個類別。我們將使用get_dummies()函數:
y_train = pd.get_dummies(y_train)
y_test = pd.get_dummies(y_test)
下一步,定義視頻分類模型的結構。
定義視頻分類模型的結構
由於我們沒有非常大的數據集,因此從頭開始創建模型可能效果不佳。因此,我們將使用預先訓練的模型並利用其學習來解決我們的問題。
對於這個特定的數據集,我們將使用VGG-16預訓練模型。讓我們創建預訓練模型的基本模型:
# 創建預訓練的VGG16基本模型
base_model = VGG16(weights='imagenet', include_top=False)
該模型在具有1,000個類的數據集上進行訓練。我們將根據我們的要求對此模型進行微調。include_top = False將刪除此模型的最後一層,以便我們可以根據需要對其進行調整。
現在,我們將從這個預先訓練的模型中提取我們的訓練和驗證圖像的功能:
# 從訓練集的幀中提取特徵
X_train = base_model.predict(X_train)
X_train.shape
輸出:(59075,7,7,512)
我們在訓練集中有59,075個圖像,並且由於我們已經通過VGG16架構傳遞了這些圖像,因此形狀已更改為(7,7,512)。同樣,我們將提取驗證集的特徵:
# 從驗證集的幀中提取特徵
X_test = base_model.predict(X_test)
X_test.shape
輸出:(14769,7,7,512)
驗證集中有14,769個圖像,這些圖像的形狀也變為(7,7,512)。我們現在將使用完全連接的網絡來微調模型。這個完全連接的網絡以單一維度輸入。因此,我們將圖像重塑為一個維度:
X_train = X_train.reshape(59075, 7*7*512)
X_test = X_test.reshape(14769, 7*7*512)
始終建議對像素值進行歸一化,即將像素值保持在0和1之間。這有助於模型更快地收斂。
# 標準化像素值
max = X_train.max()
X_train = X_train/max
X_test = X_test/max
接下來,我們將創建模型的體系結構。我們必須為此定義輸入形狀。那麼,讓我們檢查一下圖像的形狀:
# 圖像形狀
X_train.shape
輸出:(59075,25088)
輸入形狀為25,088。我們現在定義結構:
#定義結構
model = Sequential()
model.add(Dense(1024, activation='relu', input_shape=(25088,)))
model.add(Dropout(0.5))
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(256, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(101, activation='softmax'))
我們有多個完全連接的全連接層。我也添加了Dropout層控制模型不會過擬合。最後一層中的神經元數量等於我們擁有的類別數量,因此這裡的神經元數量為101。
訓練視頻分類模型
我們現在將使用訓練框架訓練我們的模型,並使用驗證框架驗證模型。我們將保存模型的權重,以便我們不必一次又一次地重新訓練模型。
所以,讓我們定義一個函數來保存模型的權重:
# 保存權重函數
from keras.callbacks import ModelCheckpoint
mcp_save = ModelCheckpoint('weight.hdf5', save_best_only=True, monitor='val_loss', mode='min')
我們將根據驗證損失確定最佳模型。請注意,權重將保存為weights.hdf5。如果你願意,可以重命名該文件。在訓練模型之前,我們必須編譯它:
# 編譯模型
model.compile(loss='categorical_crossentropy',optimizer='Adam',metrics=['accuracy'])
我們使用categorical_crossentropy作為損失函數,優化器是Adam。讓我們訓練模型:
# 訓練模型
model.fit(X_train, y_train, epochs=200, validation_data=(X_test, y_test), callbacks=[mcp_save], batch_size=128)
我們現在有權重,我們將用它來預測新視頻。因此,在下一節中,我們將看到此模型在視頻分類任務中的表現如何!
評估視頻分類模型
讓我們打開一個新的Jupyter Notebook來評估模型。評估部分也可以分成多個步驟,以更清楚地理解過程:
- 定義模型結構並加載權重
- 創建測試數據
- 對測試視頻進行預測
- 最後,評估模型
定義模型結構並加載權重
導入所需的庫:
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras.preprocessing import image
import numpy as np
import pandas as pd
from tqdm import tqdm
from keras.applications.vgg16 import VGG16
import cv2
import math
import os
from glob import glob
from scipy import stats as s
接下來,我們將定義模型結構,它與我們在訓練模型時的模型結構類似:
base_model = VGG16(weights='imagenet', include_top=False)
這是預先訓練好的模型,接下來我們將對其進行微調:
model = Sequential()
model.add(Dense(1024, activation='relu', input_shape=(25088,)))
model.add(Dropout(0.5))
model.add(Dense(512, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(256, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(101, activation='softmax'))
現在,正如我們已經定義了架構,我們現在將加載訓練的權重,我們將其存儲為weights.hdf5:
#載入權重
model.load_weights("weights.hdf5")
編譯模型:
# 編譯模型
model.compile(loss='categorical_crossentropy',optimizer='Adam',metrics=['accuracy'])
確保損失函數,優化程序和指標與我們在訓練模型時使用的相同。
創建測試數據
你應該根據UCF101數據集的官方文檔下載訓練/測試集文件。在下載的文件夾中,有一個名為" testlist01.txt " 的文件,其中包含測試視頻列表。我們將利用它來創建測試數據:
# 獲取測試列表
f = open("testlist01.txt", "r")
temp = f.read()
videos = temp.split('\\n')
# 創建dataframe
test = pd.DataFrame()
test['video_name'] = videos
test = test[:-1]
test_videos = test['video_name']
test.head()
我們現在擁有存儲在數據框中的所有視頻的列表。要將預測類別與實際類別進行映射,我們將使用train_new.csv文件:
# 創建標籤
train = pd.read_csv('UCF/train_new.csv')
y = train['class']
y = pd.get_dummies(y)
現在,我們將對測試集中的視頻進行預測。
測試視頻的預測
讓我總結一下在查看代碼之前我們將在此步驟中執行的操作。以下步驟將幫助你了解預測部分:
- 首先,我們將創建兩個空列表,一個用於存儲預測標籤,另一個用於存儲實際標籤
- 然後,我們將從測試集中獲取每個視頻,提取該視頻的幀並將其存儲在一個文件夾中(在當前目錄中創建一個名為temp的文件夾來存儲幀)。我們將在每次疊代時從此文件夾中刪除所有其他文件
- 接下來,我們將讀取temp文件夾中的所有幀,使用預先訓練的模型提取這些幀的特徵,進行預測得到標籤後將其附加到第一個列表中
- 我們將在第二個列表中為每個視頻添加實際標籤
讓我們編寫這些步驟並生成預測:
# 創建兩個列表來存儲預測的和實際的標籤
predict = []
actual = []
# for循環從每個測試視頻中提取幀
for i in tqdm(range(test_videos.shape[0])):
count = 0
videoFile = test_videos[i]
cap = cv2.VideoCapture('UCF/'+videoFile.split(' ')[0].split('/')[1]) # 從給定路徑獲取視頻
frameRate = cap.get(5) #幀率
x=1
# 從臨時文件夾中刪除所有其他文件
files = glob('temp/*')
for f in files:
os.remove(f)
while(cap.isOpened()):
frameId = cap.get(1) #當前幀編號
ret, frame = cap.read()
if (ret != True):
break
if (frameId % math.floor(frameRate) == 0):
# 將此特定視頻的幀存儲在temp文件夾中
filename ='temp/' + "_frame%d.jpg" % count;count+=1
cv2.imwrite(filename, frame)
cap.release()
# 從臨時文件夾中讀取所有幀
images = glob("temp/*.jpg")
prediction_images = []
for i in range(len(images)):
img = image.load_img(images[i], target_size=(224,224,3))
img = image.img_to_array(img)
img = img/255
prediction_images.append(img)
# 將測試視頻的所有幀轉換為numpy數組
prediction_images = np.array(prediction_images)
# extracting features using pre-trained model
prediction_images = base_model.predict(prediction_images)
# 使用預訓練模型提取特徵
prediction_images = prediction_images.reshape(prediction_images.shape[0], 7*7*512)
# 轉換一維數組中的特徵
prediction = model.predict_classes(prediction_images)
# 在預測列表中添加預測模式,將標籤分配給視頻
predict.append(y.columns.values[s.mode(prediction)[0][0]])
# 添加上視頻的真實標籤
actual.append(videoFile.split('/')[1].split('_')[1])
此步驟需要一些時間,因為測試集中有大約3,800個視頻。一旦我們得到預測的結果,我們將用來計算模型的性能。
評估模型
是時候評估我們的模型了。
我們有實際的標籤以及我們的模型預測的標籤。我們將利用這些來獲得準確度分數。在UCF101的官方文檔頁面上,當前準確率為43.90%。我們的模型可以擊敗它嗎?讓我們檢查!
from sklearn.metrics import accuracy_score
accuracy_score(predict, actual)*100
輸出:44.80570975416337
大!我們的模型的準確率為44.8%,與官方文件中的相似(43.9%)。
你可能想知道為什麼我們對50%以下的準確度感到滿意。那麼,這種低精度背後的原因主要是由於缺乏數據。我們只有大約13,000個視頻,甚至持續時間很短。
結束
在本文中,我們介紹了計算機視覺最有趣的應用之一,視頻分類。我們首先了解如何處理視頻,然後我們提取幀,訓練視頻分類模型,最後在測試視頻上獲得44.8%的準確度。
我們現在可以嘗試不同的方法,旨在提高模型的性能。我能想到的一些方法是使用可以直接處理視頻的3D卷積。
由於視頻是一系列幀,我們也可以將其解決為序列問題。所以,可以有更多的解決方案,我建議你可以探索它們。