机器学习(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

posted @ 2020-03-10 11:38  AllEmpty  阅读(2572)  评论(0编辑  收藏  举报