1. 回歸算法概念
回歸分析是一種預測性的建模技術,它研究的是因變量(目標)和自變量(預測器)之間的關系。這種技術通常用於預測分析、時間序列模型以及發現變量之間的因果關系。
回歸算法通過對特征數據的計算,從數據中尋找規律,找出數據與規律之間的因果關系,並根據其關系預測后續發展變化的規律以及結果。
常用回歸算法有:線性回歸算法、逐步回歸算法、嶺回歸算法、lasso回歸算法、支持向量機回歸等。
2. 嶺回歸算法
嶺回歸(英文名:ridge regression, Tikhonov regularization)是一種專用於共線性數據分析的有偏估計回歸方法,實質上是一種改良的最小二乘估計法,通過放棄最小二乘法的無偏性,以損失部分信息、降低精度為代價獲得回歸系數更為符合實際、更可靠的回歸方法,對病態數據的擬合要強於最小二乘法。
通常嶺回歸方程的R平方值會稍低於普通回歸分析,但回歸系數的顯著性往往明顯高於普通回歸,在存在共線性問題和病態數據偏多的研究中有較大的實用價值。
適用情況:
1.可以用來處理特征數多於樣本數的情況
2.可適用於“病態矩陣”的分析(對於有些矩陣,矩陣中某個元素的一個很小的變動,會引起最后計算結果誤差很大,這類矩陣稱為“病態矩陣”)
3.可作為一種縮減算法,通過找出預測誤差最小化的λ,篩選出不重要的特征或參數,從而幫助我們更好地理解數據,取得更好的預測效果
3. 使用嶺回歸算法預測防火牆日志中,每小時總體請求數的變化
1)項目說明
防火牆日志會記錄所有的外網對內網或內網對外網的訪問請求,根據不同日期、時間段以及使用情況,請求數與ip數都在不停的變化,通過機器算法的學習,掌握其變化的規律,預測出當天的變化規律。
2)數據信息
已通過前期的數據處理,已經完成了請求統計記錄與效果展示。
日志請求統計匯總表--小時
表名 | 字段名稱 | 字段類型 | 主鍵 | 是否允許空 | 默認值 | 字段說明 |
---|---|---|---|---|---|---|
request_report_for_hour | id | serial | PK | 0 | 主鍵Id | |
request_report_for_hour | date | timestamp | IX | 日期 | ||
request_report_for_hour | hour | integer | IX | 0 | 小時 | |
request_report_for_hour | tag | text | IX | 分類標簽:total=匯總統計;device=設備名稱 | ||
request_report_for_hour | devname | text | IX | 防火牆設備名稱 | ||
request_report_for_hour | request_for_total | integer | IX | 0 | 總請求數 | |
request_report_for_hour | ip_for_total | integer | IX | 0 | 總IP數 |
日志請求統計匯總表數據
日志請求統計匯總表效果圖
3)設計思路
根據這些已有數據,我們需要做的是,將數據和數據中所包含的特征,轉換成機器學習可以計算的數值數據,然后使用回歸算法對這些數據進行運算,找出這些數據的變化規律,然后根據這些規律,預測其未來的變化值。
業務問題思考
對於已記錄的數據,我們需要思考的問題有:
- 對於這種數值結果的預測,可以使用回歸算法來處理,而請求數變化這種類型,應該使用什么回歸算法比較合適?
- 使用的回歸算法,需要提供什么數據來進行學習和預測?
- 根據“日志請求統計匯總表”的字段設計,我們能用於分享的特征有日期、小時、匯總統計和防火牆設備名稱。而可用於機器學習的標簽(答案)有總請求數和總ip數,怎么將已有的這些內容轉換成機器學習可以使用的數據?
- 這些已有數據是否夠用?需要新增哪些字段來幫助機器學習,提升預測的准確率?
- 對於字符串類型的特征,要怎么轉換?設計什么值比較合理?不同的設計方案會有什么樣的區別?對預測結果有什么樣的影響?
- 對於這些學習的數據集,所有數據混雜在一起學習?還是需要做隔離操作(即對不同的分類與設備,各自獨立學習與預測)?它們會對預測有什么樣的影響?
- 工作日與節假日對請求數值變化有什么影響?工作時間與休息時間對請求數值變化有什么影響?需要區分嗎?而工作日與工作日大家的操作是否也會有所不同呢?如果只將日期分為工作日與節假日兩種類型,那么對於所有工作日的預測結果是否都是一樣的呢?
- 實際請求突然爆發式增長,而預測結果在正常值范圍時,如何及時進行調節適應?將預測取值隨爆發量變化處於合適水平?
- 對於請求數忽高忽低的非平滑曲線變化,如何能預測到合理范圍?
- 對於諸多的特征參數,這些值應該如果設置?每個值對預測結果有什么影響?怎么進行調配?如何找到合適的參數設置搭配?
- 對於預測結果,需要有獨立的字段用來記錄。預測效果的展示,也需要進行對應的處理,將實際結果與預測結果進行區別。
對於這些問題,我們可以做如下處理:
- 由於我們要預測的是請求數的變化,而這個變化它可能是忽高忽低的,非線性的,所以我們可以選擇嶺回歸算法來進行預測
- 對於屬於監督類型的回歸算法,我們需要提供的是可以計算的數值類型的學習數據,以及這些數據對應的標簽值
- 雖然“日志請求統計匯總表”已經有不少特征字段存在了,但實際上它們的數據類型包括日期、數值與字符類型,並不能直接用於計算,需要根據需要對它們進行轉換操作。
- 對於日期型數據是不能直接使用的,因為日期只不過代表時間的變化,而實際上不同的日期卻有着不一樣的意義,比如節假日與調休,大家放假了請求數自然就會與工作日不一樣,為了方便數據導出計算需要增加周工作日字段(weekdays),用來存儲對應的星期幾數值,區分節假日與工作日。
- 對於周工作日字段(weekdays)這個特征參數,這個值的變化范圍為0~6之間,是否直接使用這個值?直接使用會帶來什么影響?這是需要認真思考的問題。因為直接使用0至6的數值,這樣的數值模型的變化,實際結果會導致各個數據之間的權重關系的不同,一般來說值越大權重也越大,最終會直接影響預測結果。而在實際項目中,周一至周日,他們在權重上應該都是持平的一致的,只是各自標識不同日期時間而已。所以在轉為機器學習數據時,可以轉化為[0, 0, 0, 0, 0, 0]這樣的特征碼(節假日變化並不大,可以合並為1個標識,當然也可以分開設置,這個大家根據自己的設計思路進行修改即可),根據星期幾的不同,在對應的位置標識為1,即周一為[1, 0, 0, 0, 0, 0],周二為[0, 1, 0, 0, 0, 0],以此類推,而節假日、調休,則為[0, 0, 0, 0, 0, 1]。
- 為了壓縮單次學習數據的數量,隔離不同設置的請求量變化的相互影響,在生成學習數據時,可以將匯總統計和防火牆設備分離出來,各自獨立學習與預測。
- 對於預測結果,需要新增預測總請求數(calculate_request_for_total)、預測總IP數(calculate_ip_for_total)兩個字段
- 對於其他的問題思考解答,會在下面的實操部分分開講解。
當然,除了這些,實際在開發中,還可能會遇到很多其他的各種問題或難點,需要機器學習算法設計人員更深入的了解業務,了解各種機器學習算法,了解各算法在實際項目中怎么靈活應用,熟練掌握特征的各種處理辦法與轉換方法,熟悉各參數的調配與測試,從中找出最優的解決方案。
4)編碼實現
由於數據已經有了,所以只需要根據日期同步更新對應的周工作日字段(weekdays)即可,直接跳過數據清洗階段
數據加工
數據加工主要是數據從數據庫中讀取出來,然后根據嶺回歸算法所要求的數據格式進行處理,組合成學習數據集與標簽集,來進行學習訓練。同時准備好預測數據,利用訓練結果,預測出目標值。具體代碼如下:\
def get_ml_weekdays(weekdays, value): """ 初始化周工作日字段特征標識 :param weekdays: 星期幾,周一至周五值為0~4,節假日值為7 :param value: 默認標識值 """ # 初始化周工作日特征標識數組 week = [0, 0, 0, 0, 0, 0] # 為了避免對象引用問題,使用對象復制出一個副本來設置 _week = week.copy() # 如果是節假日,則設置數組索引為最后一個標識 if weekdays == 7: weekdays = 5 # 設置周工作日特征標識值,該參數可以用來調節預測值的匹配程度 _week[weekdays] = value return _week def calculate(now, tag, devname, is_all_day=False): """ 預測防火牆每小時請求數與Ip數 :param now: 預測日期 :param tag: 預測標簽類型(total=匯總數據,device=指定各防火牆設備分類) :param devname: 防火牆設備名稱 :param is_all_day: 是否預測全天各時間段的變化結果 """ # 設置查詢起始時間,即學習數據集的時間范圍為1個月內的記錄 start_date = datetime_helper.timedelta('d', now, -31).date() # 獲取當前預測日期為星期幾(節假日值為7) weekdays = datetime_helper.get_weekdays(now) # 循環遍歷1天24小時 for i in range(24): # 判斷是否需要預測整天所有時間段的數據,如果為否,則直接跳過已過的時間,只對未到來的時間進行預測 if not is_all_day and i < now.hour: continue # 限制查詢數據范圍,只查詢當前預測時間前后1小時內的數據,即對0點做預測時,只使用23點到凌晨1點的數據,以此類推 # 主要用於對學習數據進行隔離,增加預測數據的變化,不然會撓亂預測判斷,導致最終預測出的結果是一個線性值 if i == 0: hour = '23,0,1' elif i == 23: hour = '22, 23, 0' else: hour = '{},{},{}'.format(i - 1, i, i + 1) # 設置sql查詢語句 sql = """ select * from firewall_log_request_report_for_hour where date>='{}' and tag='{}' and devname='{}' and hour in ({}) order by date, hour """.format(start_date, tag, devname, hour) # 從數據庫中獲取學習數據集 flrr = firewall_log_request_report_for_hour_logic.FirewallLogLogic() result = flrr.select(sql) if not result: continue # 初始化機器學習特征集和標簽集 ml_data = [] ml_label_request = [] ml_label_ip = [] # 遍歷數據,設置周工作日特征標識,添加學習特征集 for model in result: # 因為查詢出來的學習數據集,包含當前未發生的數據,這些數據的請求數為0,需要直接過濾掉,不然會干擾預測結果 if model.get('date').date() == now.date() and now.hour - 1 <= model.get('hour') and model.get('request_for_total') == 0: continue # 判斷當前記錄是否是當前需要預測日期,是的話將其周工作日字段值設置為1 if model.get('date').date() == now.date(): _week = get_ml_weekdays(model.get('weekdays'), 1) # 非當前預測日期的所有歷史數據,都設置為0.5,即將其權重調低, # 用於弱化歷史數據對預測日期的影響,只抽取歷史日期中數據的變化規律, # 加強預測日期當天的數值強度,使其能應對請求數突發性爆發式增長或降低時,縮小預測值與實際發生數值的差距 else: _week = get_ml_weekdays(model.get('weekdays'), 0.5) # 將當前時間(小時)與周工作日特征參數組合成機器學習數據 # 例如周二凌晨1點的數據為:[1, 0, 1, 0, 0, 0, 0] _arr = [model.get('hour')] _arr.extend(_week) # 將機器學習數據添加到學習數據集中 # [[0, 0, 1, 0, 0, 0, 0] # [1, 0, 1, 0, 0, 0, 0] # [2, 0, 1, 0, 0, 0, 0] # ...] ml_data.append(_arr) # 將總請求數與總ip數添加到標簽(答案)集中 ml_label_request.append(model.get('request_for_total')) ml_label_ip.append(model.get('ip_for_total')) # 設置預測數據 # 預測2020年1月15日早上8點的請求數,測試數據格式為:[8, 0, 0, 1, 0, 0, 0] calculate_data = [i] calculate_data.extend(get_ml_weekdays(weekdays, 1))
上面代碼有幾個關鍵地方需要留意的
1.查詢數據范圍限制代碼
if i == 0: hour = '23,0,1' elif i == 23: hour = '22, 23, 0' else: hour = '{},{},{}'.format(i - 1, i, i + 1)
在代碼注釋中已經詳細說明了限制的目的,主要用於對學習數據進行隔離,增加預測數據的變化,如果去掉這一段代碼,將所有時間段內的數據加載出來提供給算法進行學習,預測結果就會出現下圖的狀態,各時間段內的數據會撓亂預測判斷,導致最終預測出的結果是一個線性值。
2.數據增加權重配置
# 判斷當前記錄是否是當前需要預測日期,是的話將其周工作日字段值設置為1 if model.get('date').date() == now.date(): _week = get_ml_weekdays(model.get('weekdays'), 1) # 非當前預測日期的所有歷史數據,都設置為0.5,即將其權重調低, # 用於弱化歷史數據對預測日期的影響,只抽取歷史日期中數據的變化規律, # 加強預測日期當天的數值強度,使其能應對請求數突發性爆發式增長或降低時,縮小預測值與實際發生數值的差距 else: _week = get_ml_weekdays(model.get('weekdays'), 0.5)
未加權重配置時,算法訓練會在全部數據集中尋找規律,然后根據歷史數據來預測當前的數據變化,然后實際項目中會存在很多意外的事情發生,可能在某個時間段因為某些特定的原因,請求數爆增或爆跌,這時預測值與實際值之間就會存在很大的差距,有時這個差距會擴大到幾倍、甚至十幾倍都有可能,而實時查看圖表時,實際值與預測值之間會有及大的落差。
而通過給當天的數據配置更高的權重,會讓這些數據從算法運算中脫穎而出,讓實際發生的數值與預測值在量上處於同一級別,而歷史的大量數據則用來給算法訓練出其歷史變化規律,從而讓預測結果更加趨向真實值,從而提高預測准確率。
使用嶺回歸算法,對目標進行預測
前面已將訓練數據集、訓練標簽集和預測數據加工處理好了,接下來就是調用回歸算法函數,對訓練數據集進行學習,然后預測目標結果。最后將結果更新到數據庫中。
# 調用回歸算法操作類的預測函數,預測總請求數 request_value = regression_helper.calculate(ml_data, ml_label_request, calculate_data) # 判斷返回值是否正常(不為nan),並做非負值判斷 if not numpy.isnan(request_value) and request_value.A[0][0] > 0: # 記錄預測結果 request_value = request_value.A[0][0] else: request_value = 0 # 調用回歸算法操作類的預測函數,預測總ip數 ip_value = regression_helper.calculate(ml_data, ml_label_ip, calculate_data) # 判斷返回值是否正常(不為nan),並做非負值判斷 if not numpy.isnan(ip_value) and ip_value.A[0][0] > 0: # 記錄預測結果 ip_value = ip_value.A[0][0] else: ip_value = 0 # 同步更新數據庫,記錄當前預測結果 _flrr = firewall_log_request_report_for_hour_logic.FirewallLogLogic() fields = { 'date': string(now.date()), 'hour': i, 'tag': string(tag), 'devname': string(devname), 'weekdays': weekdays, 'calculate_request_for_total': request_value, 'calculate_ip_for_total': ip_value } wheres = 'date=\'{}\' and hour={} and tag=\'{}\' and devname=\'{}\''.format(now.date(), i, tag, devname) model = _flrr.get_model_for_cache_of_where(wheres) # 判斷當前記錄是否存在,存在則更新,不存在則新增 if model: _flrr.edit_model(model.get('id'), fields) else: _flrr.add_model(fields)
機器學習嶺回歸算法操作類代碼(regression_helper.py)
嶺回歸算法函數直接根據《機器學習實戰》書中的代碼改造來的,詳細請看注釋。
def calculate(ml_data, ml_label, calculate_data): """ 嶺回歸算法預測函數 :param ml_data: 訓練數據特征集(樣本的特征數據) :param ml_label: 訓練數據特征標簽集,即每個樣本對應的類別標簽,目標變量,實際值 :param calculate_data: 預測數據 :return: 預測結果值 """ ws = ridge_regres(ml_data, ml_label) if (isinstance(ws, float) or isinstance(ws, numpy.float64)) and (numpy.isnan(ws) or numpy.isnan(ws[0][0])): return numpy.nan # 將預測數據轉為矩陣 calculateMat = numpy.mat(calculate_data) # 將訓練數據集轉為矩陣 xMat = numpy.mat(ml_data) # 計算 xMat 平均值 xMeans = numpy.mean(xMat, 0) # 計算 X 的方差 xVar = numpy.var(xMat, 0) # 預測特征減去xMat的均值並除以方差 calculateMat = (calculateMat - xMeans) / xVar # 計算預測值 calculate_result = calculateMat * numpy.mat(ws).T + numpy.mean(ml_label) return calculate_result def ridge_regres(ml_data, ml_label): """ 嶺回歸求解函數,計算回歸系數 :param ml_data: 訓練數據特征集(樣本的特征數據) :param ml_label: 訓練數據特征標簽集,即每個樣本對應的類別標簽,目標變量,實際值 :return: 經過嶺回歸公式計算得到的回歸系數矩陣 """ try: # 將訓練數據集轉為矩陣 xMat = numpy.mat(ml_data) # 將標簽集轉為行向量 yMat = numpy.mat(ml_label).T # 計算Y的均值 yMean = numpy.mean(yMat, 0) # Y的所有特征減去均值 yMat = yMat - yMean # 計算 xMat 平均值 xMeans = numpy.mean(xMat, 0) # 計算 X 的方差 xVar = numpy.var(xMat, 0) # 所有特征都減去各自的均值並除以方差 xMat = (xMat - xMeans) / xVar # 計算x的平方值 xTx = xMat.T * xMat # 嶺回歸就是在矩陣 xTx 上加一個 λI 從而使得矩陣非奇異,進而能對 xTx + λI 求逆 denom = xTx + numpy.eye(numpy.shape(xMat)[1]) * numpy.exp(-9) # 檢查行列式是否為零,即矩陣是否可逆,行列式為0的話就不可逆,不為0的話就是可逆。 if numpy.linalg.det(denom) == 0.0: print("This matrix is singular, cannot do inverse") return # 求解取得回歸系數 ws = denom.I * (xMat.T * yMat) return ws.T except Exception as e: # 當訓練數據集中,某一列的值全部相同時,這一列求解會得出0值,而對這個值進行運算就會出現異常 return numpy.nan
預測結果展示
從下面兩圖預測結果曲線圖的對比上看,總體預測結果與實際結果相差並不大,應對突發性的數量變化,預測會有偏差,這需要后續對算法再做進一步的優化調整。經過處理,當前算法也會根據上一小時的結果及時做出調整,調整下一小時的預測數值
嶺回歸算法參數調優
下圖是在做嶺回歸算法調優時,不同參數下測試出來的預測結果
從圖中可以看到,通過不同數據量、參數值大小、權重調整等參數的設置,預測結果曲線與實際結果曲線的偏差,再根據結果來設置最優參數值
最終實現效果
4. 參考資料
https://github.com/apachecn/AiLearning/blob/master/docs/ml/8.回歸.md