機器學習(07)——嶺回歸算法實戰


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM