基於數據挖掘的抖音商用廣告視頻識別
Project:基於數據挖掘的tik tok商用廣告視頻識別
任務
- 爲了吸引觀衆的注意力,廣告視頻的長度、音頻、文本位置和畫面會有與衆不同之處。
- 我們將使用人工智能的方法構建一套商用廣告識別系統來預測抖音短視頻是否爲商用廣告,通過對Tik Tok平臺上視頻的時長、聲音頻譜、視頻光譜、文字分佈、畫面變化等特徵,進行特徵抽取、特徵過濾等方式處理後進行建模,來快速區分出投稿視頻中的商業廣告。
- 具體包括
- 瞭解這份數據
- 進行必要的數據清洗
- 自由進行特徵生成、特徵選擇、特徵降維等工作
- 建立合適的預測模型,並進行調參
- 選用合適的方式進行模型集成,優化模型
數據
- 廣告數據集包括5次採樣、總長度爲150小時的抖音視頻中提取的視頻鏡頭的標準視聽特徵,以270 fps的分辨率錄製視頻,分辨率爲720 X 576。將視頻數據處理爲視頻的時長、聲音頻譜、視頻光譜、文字分佈和畫面變化等特徵,以判斷其是否爲商用廣告。最終的數據包含1個標籤、230個特徵。
- 數據提供了129685份視頻的信息,儲存在commercial_vedio_data.csv文件中,其中labels爲標籤。
- 數據集共包230個特徵,涵蓋視頻的時長、聲音頻譜、視頻光譜、文字分佈和畫面變化等方面。
- 具體描述詳見:數據集和變量說明.pdf
A. 視覺特徵
- 視頻鏡頭長度Length 1
- 每個視頻鏡頭的屏幕文本分佈Text 92-122
- 運動分佈Move 2-3 18-58
- 幀差異分佈Frame 4-5 59-91
- 邊緣變化率Edge 4124-4125
B. 音頻特徵
- 短期能量Energe 6-7
- 零交叉率ZCR 8-9
- 光譜質心Centroid 10-11
- 光譜通量Flux 14-15
- 頻譜滾降頻率Rolloff 12-13
- 基頻BasFreq 16-17
- 音頻詞包MFCC 123-4123
問題思路
數據集
- 觀察數據的實際意義,發現所有數據都是連續型。
- 數據整體上分爲兩類,一類是期望、方差這種在意義上有高度概括性的數據;一類是第18-58、59-91、123-4123多個並列的特徵。
- 標籤分爲兩種,-1和+1,可以將-1調整爲0,成爲一個典型的分類問題
數據處理
- 數據中數字的尺度差異較小,可以不需要標準化
- 連續性變量不需要使用One-Hot編碼
- 數據中存在大量的缺失值,嚴重影響完整性;可以預先將缺失值填充爲均值,再通過特徵的重要性判斷是否有必要返回調整填充策略
- 數據中重複樣本巨大,需要剔除
- 數據中有200+個特徵,有必要進行選擇,降低樣本特徵的維度;使用隨機森林模型對特徵重要性進行排序,對重要性排序圖進行觀察後調整輸入模型的特徵數量的超參數
- PCA能基於選擇的特徵融合出新的特徵,期望與原數據合併之後提高預測能力
- 決策樹驅動的特徵分箱能在特徵維度較高的情況下有利於快速迭代和穩定性,提高準確度並降低過擬合風險
模型建立
- 將數據輸入隨機森林分類器
- 通過ROC_AUC和Accuracy得分來評價生成的模型,反饋於模型的調參
模型準備
- 導入庫
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
import seaborn as sns
%matplotlib inline
import warnings
- 導入數據集
# 導入文件依據運行的環境和平臺進行必要的更改
data = pd.read_csv("../commercial_vedio_data.csv", index_col=0)
- 獲得特徵和標籤
col_name = data.columns[:-2]
label_name = data.columns[-1]
print ('訓練集的標籤:{}\n'.format(label_name))
print ('訓練集的特徵:{}\n'.format(col_name))
print ('訓練集的形狀:{}\n'.format(data.shape))
>>output
訓練集的標籤:Label
訓練集的特徵:Index(['Length', 'Move_E', 'Move_D', 'Frame_E', 'Frame_D', 'Energe_E',
'Energe_D', 'ZCR_E', 'ZCR_D', 'Centroid_E',
...
'882', '924', '959', '1002', '1016', '1028', '1048', '1112', '1119',
'Edge_E'],
dtype='object', length=229)
訓練集的形狀:(129685, 231)
- 重命名標籤
# Rename
data.rename(columns={'1':'Length', '2':'Move_E', '3':'Move_D', '4':'Frame_E', '5':'Frame_D', '6':'Energe_E', '7':'Energe_D', '8':'ZCR_E', '9':'ZCR_D', '10':'Centroid_E', '11':'Centroid_D', '12':'Rolloff_E', '13':'Rolloff_D', '14':'Flux_E', '15':'Flux_D', '16':'BasFreq_E', '17':'BasFreq_D', '4124':'Edge_E', '4125':'Edge_D', 'labels':'Label'}, inplace=True)
數據分析
- 數據整體描述
- 整體來說數據的尺度較爲一致
- 在Length特徵中存在有異常值,應該清除
data.head()
data.describe()
Length | Move_E | Move_D | Frame_E | Frame_D | Energe_E | Energe_D | ZCR_E | ZCR_D | Centroid_E | … | 959 | 1002 | 1016 | 1028 | 1048 | 1112 | 1119 | Edge_E | Edge_D | Label | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 123 | 1.316440 | 1.516003 | 5.605905 | 5.346760 | 0.013233 | 0.010729 | 0.091743 | 0.050768 | 3808.067871 | … | 0.036017 | 0.006356 | 0.008475 | NaN | 0.002119 | NaN | NaN | 0.422334 | 0.663918 | 1 | |
1 | 124 | 0.966079 | 0.546420 | 4.046537 | 3.190973 | 0.008338 | 0.011490 | 0.075504 | 0.065841 | 3466.266113 | … | 0.117647 | 0.006303 | NaN | NaN | 0.008403 | NaN | NaN | 0.332664 | 0.766184 | 1 | |
2 | 109 | 2.035407 | 0.571643 | 9.551406 | 5.803685 | 0.015189 | 0.014294 | 0.094209 | 0.044991 | 3798.196533 | … | 0.062500 | 0.004808 | NaN | NaN | 0.009615 | NaN | NaN | 0.346674 | 0.225022 | 1 | |
3 | 86 | 3.206008 | 0.786326 | 10.092709 | 2.693058 | 0.013962 | 0.011039 | 0.092042 | 0.043756 | 3761.712402 | … | 0.046296 | 0.012346 | NaN | NaN | 0.012346 | 0.003086 | NaN | 0.993323 | 0.840083 | 1 | |
4 | 76 | 3.135861 | 0.896346 | 10.348035 | 2.651010 | 0.020914 | 0.012061 | 0.108018 | 0.052617 | 3784.488037 | … | NaN | 0.003521 | NaN | NaN | 0.045775 | 0.007042 | NaN | 0.341520 | 0.710470 | 1 |
Length | Move_E | Move_D | Frame_E | Frame_D | Energe_E | Energe_D | ZCR_E | ZCR_D | Centroid_E | … | 959 | 1002 | 1016 | 1028 | 1048 | 1112 | 1119 | Edge_E | Edge_D | Label | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 129685.000000 | 129685.000000 | 129685.000000 | 129685.000000 | 129685.000000 | 129685.000000 | 129685.000000 | 129685.000000 | 129685.000000 | 129685.000000 | … | 62719.000000 | 53351.000000 | 41920.000000 | 1860.000000 | 55528.000000 | 13108.000000 | 217.000000 | 129685.000000 | 129685.000000 | 129685.000000 | |
mean | 106.400000 | 2.587003 | 1.601049 | 11.918077 | 8.264462 | 0.015218 | 0.009762 | 0.103230 | 0.056772 | 3481.604677 | … | 0.044993 | 0.040969 | 0.055489 | 0.003688 | 0.035440 | 0.006209 | 0.036149 | 0.500648 | 0.500378 | 0.268165 | |
std | 264.814882 | 2.179930 | 1.374998 | 9.068333 | 6.847135 | 0.005434 | 0.003281 | 0.037289 | 0.021509 | 669.086147 | … | 0.056405 | 0.049836 | 0.069591 | 0.004812 | 0.043293 | 0.012654 | 0.075509 | 0.288909 | 0.288068 | 0.963377 | |
min | 25.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | … | 0.000059 | 0.000059 | 0.000073 | 0.000014 | 0.000007 | 0.000047 | 0.000271 | 0.000032 | 0.000003 | -1.000000 | |
25% | 30.000000 | 0.947497 | 0.674715 | 5.380625 | 3.244237 | 0.012589 | 0.008073 | 0.083190 | 0.045150 | 3390.920654 | … | 0.011161 | 0.008621 | 0.009770 | 0.000836 | 0.009804 | 0.001208 | 0.004464 | 0.250215 | 0.251995 | -1.000000 | |
50% | 49.000000 | 1.970185 | 1.343323 | 9.476908 | 6.584897 | 0.015709 | 0.010057 | 0.102859 | 0.054889 | 3608.322998 | … | 0.025000 | 0.023585 | 0.028226 | 0.001988 | 0.022727 | 0.002687 | 0.012500 | 0.501763 | 0.499753 | 1.000000 | |
75% | 96.000000 | 3.710244 | 2.163196 | 16.568928 | 11.572393 | 0.018552 | 0.012005 | 0.123875 | 0.066628 | 3774.575684 | … | 0.057143 | 0.056830 | 0.076539 | 0.004556 | 0.045455 | 0.006410 | 0.037500 | 0.751095 | 0.749926 | 1.000000 | |
max | 33871.000000 | 21.679216 | 37.363274 | 67.285736 | 63.396584 | 0.036905 | 0.021416 | 0.394551 | 0.246353 | 4005.922607 | … | 0.812500 | 0.637500 | 1.012500 | 0.050000 | 0.840909 | 0.223214 | 0.512500 | 0.999973 | 0.999997 | 1.000000 |
- 查看分佈
# Label分佈的直方圖
sns.distplot(data['Label'], kde=False)
# 時長Length分佈和統計
data.drop(data[data['Length'] > 10000].index.tolist(), inplace=True)
fig, axes = plt.subplots(1, 2)
sns.barplot(x='Label', y='Length', data=data, ax=axes[0])
sns.stripplot(x='Label', y='Length', data=data, ax=axes[1], jitter=True)
plt.show()
# 商業廣告和非商業廣告的長度分佈
facet = sns.FacetGrid(data[['Length', 'Label']], hue='Label', aspect=2)
facet.map(sns.kdeplot, "Length", shade=True)
facet.set(xlim=(0, 500))
facet.add_legend()
facet.set_axis_labels("Length", "Density")
圖爲商業廣告和非商業廣告的長度分佈
- 可以看出是否爲商業廣告在視頻長度上有着相似的分佈,但又存在着不同
- 通過圖表可以發現有異常數據,剔除後顯示如下
清洗數據
查看、填充缺失值
data.isnull().any()
>>output
Length False
Move_E False
Move_D False
Frame_E False
Frame_D False
...
1112 True
1119 True
Edge_E False
Edge_D False
Label False
Length: 231, dtype: bool
去除重複樣本
- 樣本中存在高達上千的重複樣本,去除後有利於模型精準度
data.drop_duplicates(inplace=True)
data.shape
>>output
(111615, 231)
填充缺失樣本
- 暫時以平均值進行填充
- 若填充的這些特徵不重要則可以保持
- 反之,應該通過無缺失的特徵對缺失特徵進行簡單預測
data = data.fillna(data.mean())
更改標籤
# Label -1 -> 0
data['Label'] = data['Label'].apply(lambda x:0 if x == -1 else x)
data['Label'].hist()
通過柱形圖可以簡單觀察到標籤修改完成
啞變量
- 無離散型變量,無需get_dummies
特徵工程
可綜合多種方法進行特徵工程
- 一是使用某些方法生成新的特徵納入模型進行預測
- 二是通過某些方法進行特徵過濾,減少納入模型的特徵數量
- 三是對連續特徵進行特徵分箱,離散特徵進行特徵組合。
- 特徵過濾
- 特徵生成
- 特徵分箱
- 分離特徵和標籤
X = data.drop(['Label'], axis=1)
Y = data['Label']
- 劃分訓練集和測試集
from sklearn.model_selection import train_test_split
xtrain, xtest, ytrain, ytest = train_test_split(X, Y, train_size=0.75)
特徵選擇
- 使用隨機森林進行特徵選擇
- 訓練集擬合隨機森林模型
- 用於獲得feature_importances_
from sklearn.ensemble import RandomForestClassifier
rfcModel = RandomForestClassifier()
rfcModel.fit(xtrain, ytrain)
特徵重要性排序
通過重要性值進行排序畫出柱狀圖
通過計算前綴和畫出階梯圖
前綴和階梯圖可以直觀地看出所選的特徵模型的累計貢獻,給特徵選擇的超參數調整參考
# 將特徵的重要性程度進行排序
N_most_important = 25
imp = np.argsort(rfcModel.feature_importances_)[::-1]
imp_slct = imp[:N_most_important]
FeaturesImportances = zip(col_name, map(lambda x:round(x,5), rfcModel.feature_importances_))
FeatureRank = pd.DataFrame(columns=['Feature', 'Imp'], data=sorted(FeaturesImportances, key=lambda x:x[1], reverse=True)[:N_most_important])
# 重新選擇X
xtrain_slct = xtrain.iloc[:,imp_slct]
xtest_slct = xtest.iloc[:,imp_slct]
# 特徵排序圖
ax1 = fig.add_subplot(111)
ax1 = sns.barplot(x='Feature', y='Imp', data=FeatureRank)
ax1.set_xticklabels(ax1.get_xticklabels(), rotation=90)
SumImp = FeatureRank
for i in SumImp.index:
if (i==0):
SumImp['Imp'][i] = FeatureRank['Imp'][i]
else:
SumImp['Imp'][i] = SumImp['Imp'][i-1] + FeatureRank['Imp'][i]
ax2 = ax1.twinx()
plt.step(x=SumImp['Feature'], y=SumImp['Imp'])
特徵排序和前綴和階梯圖
- 可以看出前3個特徵對於模型有着相對高的貢獻,但是累計的貢獻不足
- 在排序後的第25個特徵附近的特徵的重要性僅僅有重要程度最高的特徵的10%
- 因此在保證特徵充足、又簡化模型複雜度的情況下,我選擇前25個特徵進行建模
PCA
使用PCA進行特徵生成,即與選擇出的主成分與原數據合併,能夠一定程度上提高預測精準度
對訓練集使用PCA生成新特徵,根據累計貢獻率,保留前5個主成分
對測試集進行相同的操作,注意測試集上直接使用pca中的transform函數,相同方法處理訓練集和測試集
from sklearn.decomposition import PCA
pca = PCA(n_components=N_most_important)
pca.fit(xtrain)
pca.explained_variance_ratio_
>>output
>>array([6.44019353e-01, 1.91966700e-01, 1.31533910e-01, 1.94097858e-02,
7.15686452e-03, 5.11195793e-03, 6.94964416e-04, 7.23740187e-05,
2.35382455e-05, 6.08344172e-06, 3.61950934e-06, 6.11507427e-07,
9.72953450e-08, 2.07865426e-08, 2.06120726e-08, 2.01479207e-08,
1.26524838e-08, 7.37201378e-09, 5.68464881e-09, 5.41418107e-09,
4.50735308e-09, 3.67000625e-09, 3.15899020e-09, 3.00134603e-09,
2.56423139e-09])
- 對訓練集使用PCA生成新特徵,根據累計貢獻率,保留前6個主成分
pca1 = PCA(6)
pc = pd.DataFrame(pca1.fit_transform(xtrain))
pc.index = xtrain.index
xtrain_pca = xtrain.join(pc)
- 對測試集進行相同的操作,注意測試集上直接使用pca中的transform函數
pc = pd.DataFrame(pca1.fit_transform(xtest))
pd.index = xtrain.index
xtest_pca = xtest.join(pc)
特徵分箱
使用cut_bin和cut_test_bin基於決策樹進行分箱
重新獲得訓練集和測試集
- 使用決策樹進行特徵分箱
- 通過特徵分箱可以將連續性的數據轉換爲離散型數據
- 提高模型穩定性
- 降低過擬合風險
調整決策樹參數中
決策樹最大深度應在10-100之間,提高到一定值後,優化效果不顯著
最小葉子節點數與樣本量的比例在大於0.3的情況下會降低分箱效果
from sklearn.tree import DecisionTreeClassifier
train = xtrain.join(ytrain)
test = xtest.join(ytest)
new_train, train_dict_bin = cut_bin(train, 'Label', 50, 0.2)
new_test , test_dict_bin = cut_test_bin(test, 'Label', train_dict_bin)
- 分離特徵和標籤
xtrain = new_train.drop(['Label'], axis=1)
xtest = new_test.drop(['Label'] , axis=1)
ytrain = new_train['Label']
ytest = new_test['Label']
- 特徵分箱函數
# cut_bin對訓練集進行分箱
def cut_bin(df,label,max_depth,p):
df_bin = df[[label]]
df_feature = df.drop([label],axis=1)
dict_bin = {}
for col in df_feature.columns:
get_model = DecisionTreeClassifier(max_depth=max_depth,min_samples_leaf=int(p*len(df)))
get_cut_point = get_model.fit(df[col].values.reshape(-1,1),df[label].values.reshape(-1,1))
cut_point = get_cut_point.tree_.threshold[get_cut_point.tree_.threshold!=-2]
N_split = np.zeros_like(df[col])
inter_range = []
if len(cut_point)==1:
N_split[np.array(df[col]<cut_point[0])]=1
N_split[np.array(df[col]>=cut_point[0])]=2
inter_range=[[1,-100000000,cut_point[0]],[2,cut_point[0],100000000]]
elif len(cut_point)>1:
cut_point.sort()
N_split[np.array(df[col]<cut_point[0])]=1
inter_range=[[1,-100000000,cut_point[0]]]
for i in range(len(cut_point)-1):
N_split[np.array((df[col]>=cut_point[i]) & (df[col]<cut_point[i+1]))]=i+2
inter_range=inter_range+[[i+2,cut_point[i],cut_point[i+1]]]
N_split[np.array(df[col]>=cut_point[len(cut_point)-1])]=len(cut_point)+1
inter_range=inter_range+[[len(cut_point)+1,cut_point[len(cut_point)-1],100000000]]
else:
N_split=1
inter_range=np.array([1,-100000000,100000000]).reshape(1,-1)
df_bin[col] = N_split
inter_df = pd.DataFrame(inter_range)
inter_df.columns=['bin','lower','upper']
crosstable = pd.crosstab(df_bin[col],df_bin[label])
crosstable.columns = ['notCommercial','Commercial']
crosstable['all'] = crosstable['notCommercial']+crosstable['Commercial']
crosstable['percent'] = crosstable['all']/sum(crosstable['all'])
crosstable['c_rate'] = crosstable['Commercial']/crosstable['all']
inter_df = pd.merge(inter_df, crosstable, left_on='bin', right_index=True)
dict_bin[col] = inter_df
return df_bin, dict_bin
# cut_test_bin對測試集進行分箱
def cut_test_bin(df, label, train_dict_bin):
df_bin = df[[label]]
df_feature = df.drop([label],axis=1)
dict_bin = {}
for col in df_feature.columns:
train_bin = train_dict_bin[col]
splited = pd.Series([np.nan]*len(df[col]))
for i in range(len(train_bin['bin'])):
splited[((df[col]>=train_bin['lower'][i]) & (df[col]<train_bin['upper'][i])).tolist()]=train_bin['bin'][i]
df_bin[col]=splited.tolist()
crosstable = pd.crosstab(df_bin[col],df_bin[label])
crosstable.columns = ['notCommercial','Commercial']
crosstable['all'] = crosstable['notCommercial']+crosstable['Commercial']
crosstable['percent'] = crosstable['all']/sum(crosstable['all'])
crosstable['c_rate'] = crosstable['Commercial']/crosstable['all']
inter_df = pd.merge(train_bin[['bin','lower','upper']], crosstable, left_on='bin', right_index=True, how='left')
dict_bin[col] = inter_df
return df_bin, dict_bin
訓練模型
基於以上的特徵工程進行模型訓練
- 使用隨機森林分類器
- 預設參數爲
- max_features=16
- max_depth=12
- n_estimators=2048
- n_jobs=-1
- random_state=0
# 隨機森林分類器訓練模型
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(max_features=16,max_depth=12,n_estimators=2048,n_jobs=-1,random_state=0)
rf.fit(xtrain, ytrain)
模型評估
- 評價訓練集表現
- 評價測試集表現
- 隨機猜測函數對比
from sklearn.metrics import confusion_matrix
from sklearn.metrics import roc_auc_score, accuracy_score
# AUC和混淆矩陣評估
ytrain_pred_clf = rf.predict_proba(xtrain)
ytrain_pred = rf.predict(xtrain)
ytest_pred_clf = rf.predict_proba(xtest)
ytest_pred = rf.predict(xtest)
# 評估訓練集效果,直觀判斷是否過擬合
print ('分類模型訓練集表現:')
print ('ml train model auc score {:.6f}'.format(roc_auc_score(ytrain, ytrain_pred_clf[:,1])))
print ('------------------------------')
print ('ml train model accuracy score {:.6f}'.format(accuracy_score(ytrain, ytrain_pred)))
print ('------------------------------')
threshold = 0.5
print (confusion_matrix(ytrain, (ytrain_pred_clf>threshold)[:,1]))
# 評估測試集效果
print ('分類模型測試集表現:')
print ('ml model auc score {:.6f}'.format(roc_auc_score(ytest, ytest_pred_clf[:,1])))
print ('------------------------------')
print ('ml model accuracy score {:.6f}'.format(accuracy_score(ytest, ytest_pred)))
print ('------------------------------')
threshold = 0.5
print (confusion_matrix(ytest, (ytest_pred_clf>threshold)[:,1]))
# 隨機猜測函數對比
ytest_random_clf = np.random.uniform(low=0.0, high=1.0, size=len(ytest))
print ('random model auc score {:.6f}'.format(roc_auc_score(ytest, ytest_random_clf)))
print ('------------------------------')
print (confusion_matrix(ytest, (ytest_random_clf<=threshold).astype('int')))
>>output
>>分類模型訓練集表現:
ml train model auc score 0.979045
------------------------------
ml train model accuracy score 0.926605
------------------------------
[[26550 4049]
[ 2095 51017]]
分類模型測試集表現:
ml model auc score 0.957583
------------------------------
ml model accuracy score 0.895033
------------------------------
[[ 8414 2030]
[ 899 16561]]
random model auc score 0.505389
------------------------------
[[5105 5339]
[8671 8789]]
測試集上模型的表現較爲優秀,ROC_AUC和Accuracy分別達到了96%和90%的分數
- 假陽性率爲橫座標,真陽性率爲縱座標做曲線,評價模型
from sklearn.metrics import roc_curve, auc
fpr,tpr,threshold = roc_curve(ytest,ytest_pred_clf[:,1])
roc_auc = auc(fpr,tpr)
## 假陽性率爲橫座標,真陽性率爲縱座標做曲線
plt.figure()
lw = 2
plt.figure(figsize=(10,10))
plt.plot(fpr, tpr, color='darkorange',
lw=lw,
label='ROC curve (area = %0.2f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='navy', lw=lw, linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver operating characteristic curve')
plt.legend(loc="lower right")
plt.show()
項目總結
通過對於視頻提取的數據的異常處理和清洗,依據隨機森林模型的重要性排序選擇有效貢獻於標籤的特徵,用PCA融合出新的顯著特徵,在基於決策樹對離散數據分箱,輸入隨機森林分類器後評價準確率最高達到了96%,可以認爲較好的完成了對商業廣告視頻的識別。