网站流量异常检测

目录

创建工程

数据集

时序数据中的异常检测

基于直方图的异常检测

加载数据

创建直方图

基于密度的k最近邻算法

完整代码


第二个例子中,我们将对前一个例子的反面建模。我们不会讨论典型的少欺诈案例是什么, 而要讨论系统正常的预期行为。如果有违背模型预期的事情发生,这个事情就会被识别为异常。

创建工程

接着前面的项目:

数据集

我们将使用一个Yahoo Labs公开的数据集,这有助于我们研究如何从时序数据中检测异常。 对于Yahoo而言,主要使用案例是检测Yahoo服务器上的非正常流量。

数据集由流向Yahoo服务的真实流量组成,里面同时含有一些合成数据。这个数据集总共包 含367个时间序列,每个时序包含741~1680个观察结果,它们是按固定时间间隔记录得到的。每 个序列都有自己的记录文件,每一行对应一个观察结果。每个序列都带有一个列指示器——“异常”(anomaly),观察表明这个序列是异常时,该列值为1,否则为0。实际数据中的异常由人为 判断决定,而合成数据中的异常由算法自动生成。

接下来,我们将学习如何将时序数据转换为属性,以便应用机器学习算法。

时序数据中的异常检测

在原始流式时序数据中检测异常时,需要对数据做一些转换。最明显的做法是选择一个时间 窗口,用固定长度采集时间序列。接下来,我们比较新时间序列与之前采集的序列,以检测是否 有异常发生。
可以选用的比较技术多种多样:

 预测最有可能的跟随值(following value)以及置信区间(比如霍尔特-温特指数平滑)。 若新值超出预测的置信区间,即被判定为异常。

 互相关(Cross correlation)技术比较新样本与正例样本库,查找准确匹配。若未发现匹配, 则把新样本标记为异常。

 动态时间规整与互相关类似,但它允许比较中有信号失真。

 信号离散化到频带,每个频带对应于一个字母,比如 A=[min, mean/3] 、 B=[mean/3, mean*2/3]、C=[mean*2/3, max],将信号转换为字母序列,比如aAABAACAABBA….这个 方法可以有效减少存储,并且允许我们使用文本挖掘算法。文本挖掘相关内容将在第10 章讲解。

 基于分布的方法评估一个特定时间窗口中值的分布。观察一个新样本时,可以将其分布 与之前观察的进行比较,查看是否匹配。

上述列表并不全面,这些不同方法都将重点放在检测某些异常上(比如值异常、频率异常、 分布异常)。接下来将重点讲解基于分布的方法。

基于直方图的异常检测

基于直方图的异常检测中,通过某个选定的时间窗口对信号做切分

针对每个窗口计算直方图,也就是说,针对选中的桶数,计算每个桶中落入多少个值。直方 图反映了所选时间窗口中值的分布情况(下图中间部分)。

然后,可以直接把直方图表示成实例,每个bin对应一个属性。而且,通过应用维度缩减技 术(比如主成分分析,PCA),可以减少属性数目,这样就可以使用散点图绘制降维后的直方图, 其中每个点代表一个直方图(下图右下)。

我们的示例中,主要的想法是先对网站流量观察几天,然后创建直方图,比如以4小时为窗 口创建一个正常行为库。若新时间窗口直方图与正常行为库不匹配,就将其标记为异常。

比较一个新的直方图与一组已有的直方图时,我们会使用基于密度的k最近邻算法——局部异常因子算法(LOF,Breunig等,2000)。这个算法能够处理拥有不同密度的群组(clusters), 如图所示。比如,相比于左下方小而密集的群组,右上方群组既大又广。

加载数据

第一步中,我们需要把数据从文本文件加载到一个Java对象。这些文本文件存储在一个文件 夹,每个文件中,每一行就是一个带有值的时间序列。我们将其加载到一个Double型列表:

        /*
         * 加载数据
         */
        String path = ClassUtils.getDefaultClassLoader().getResource("data/test07/ydata/A1Benchmark").getPath() + "/real_";
        List<List<Double>> rawData = new ArrayList<List<Double>>();

对直方图做正态化处理的过程中,要用到min与max值。因此数据传递时,需要先得到它们。

        double max = Double.MIN_VALUE;
        double min = Double.MAX_VALUE;
        // 读取67个CSV文件
        for (int i = 1; i <= 67; i++) {
            List<Double> sample = new ArrayList<Double>();
            BufferedReader reader = new BufferedReader(new FileReader(path + i + ".csv"));
            boolean isValid = false;
            // 跳过第一行(标题行)
            reader.readLine();
            while (reader.ready()) {
                String line = reader.readLine();
                String[] str = line.split(",");
                double value = Double.parseDouble(str[1]);
                // 添加值到sample列表
                sample.add(value);
                // 更新最大值和最小值
                if (value > max) max = value;
                if (value < min) min = value;
                // 检查是否是有效的样本
                if ("1".equals(str[2])) {
                    isValid = true;
                }
            }
            // 打印是否是有效样本
            System.out.println(isValid);
            // 关闭reader
            reader.close();
            // 将样本添加到rawData列表
            rawData.add(sample);
        }
        // 打印加载的样本数量、最大值和最小值
        System.out.println("size:" + rawData.size() + "\tmax: " + max + "\tmin: " + min);

至此,数据加载完毕,接下来开始创建直方图。

创建直方图

我们将按照WIN_SIZE宽度为选定的时间窗口创建直方图。这个直方图用于存储HIST_BINS 值桶(value buckets)。这些包含double列表的直方图会被存储到一个数组列表。
 

        /*
         * 创建直方图
         */
        // 创建delta_t直方图
        int WIN_SIZE = 500; // 窗口大小
        int HIST_BINS = 20; // 直方图的 bins 数量
        int current = 0;
        List<double[]> dataHist = new ArrayList<double[]>();
        // 遍历每个样本
        for (List<Double> sample : rawData) {
            double[] hist = new double[HIST_BINS];
            for (Double value : sample) {
                int bin = toBin(normalize(value, min, max), HIST_BINS);
                hist[bin]++;
                current++;
                // 如果当前窗口达到窗口大小,重置计数器并添加直方图
                if (current == WIN_SIZE) {
                    current = 0;
                    dataHist.add(hist);
                    hist = new double[HIST_BINS];
                }
            }
            // 添加最后一个窗口的直方图
            dataHist.add(hist);
        }

直方图已经做好,接下来要把它们转换为Weka中的Instance对象。每个直方图值对应于一 个Weka属性,代码如下:

        /*
         * 创建数据库
         */
        ArrayList<Attribute> attributes = new ArrayList<Attribute>();
        for (int i = 0; i < HIST_BINS; i++)
            attributes.add(new Attribute("HIST-" + i));
        // 创建数据集
        Instances data = new Instances("data", attributes, dataHist.size());
        for (double[] hist : dataHist) {
            data.add(new DenseInstance(1.0, hist));
        }
        // 打印数据集大小
        System.out.println("Dataset created: " + data.size());

至此,我们已经加载好数据集,接下来应用异常检测算法。

基于密度的k最近邻算法

为了演示LOF算法(即局部异常因子算法)如何计算分数,先使用testCV(int, int)函数 把数据集划分为训练集与测试集。其中,第一个参数用于指定折数,第二个参数指定要返回哪个 折。

        /*
         * 构建模型
         */
        // 将数据分为训练集和测试集
        Instances trainData = data.testCV(2, 0);
        Instances testData = data.testCV(2, 1);
        System.out.println("Train: " + trainData.size() + "\nTest:" + testData.size());

LOF算法有两个实现接口:一个用作无监督过滤器,计算LOF值(已知的未知);另一个用 作监督k-nn分类器(已知的已知)。我们的示例要计算异常分数因子(outlier-ness factor),所以将 使用无监督过滤器接口。

使用常规过滤器的初始化方式对这个过滤器进行初始化。可以指定邻居的k数(比如k=3)以 及-min与-max参数。LOF允许我们指定两个不同的k参数,在内部一个用作上界,另一个用作下 界,以便查找最小/最大数lof值:

        // 加载训练数据到 k-NN 算法
        LOF lof = new LOF();
        lof.setInputFormat(trainData);
        lof.setOptions(new String[]{"-min", "3", "-max", "3"});

接下来,将训练实例加载到过滤器,用作正例库。加载完成后,调用batchFinished()方 法对内部计算做初始化:

        for (Instance inst : trainData) {
            lof.input(inst);
        }
        lof.batchFinished();

最后,将过滤器应用于测试数据。过滤器将处理实例,并在最后添加一个包含LOF评分的属 性。我们可以在控制台简单输出分数。

        // 打印 LOF 模型加载完成
        System.out.println("LOF loaded");
        // 对测试数据进行过滤
        Instances testDataLofScore = Filter.useFilter(testData, lof);
        for (Instance inst : testDataLofScore) {
            // 打印 LOF 得分
            System.out.println(inst.value(inst.numAttributes() - 1));
        }

前面几个测试实例的LOF分数如下:

1.306740014927325 
1.318239332210458 
1.0294812291949587 
1.1715039094530768

为了理解LOF值,需要先了解LOF算法。LOF算法比较一个实例的密度与其最近邻的密度, 两个分数相除就是LOF分数。若LOF分数接近1,则表示密度近似相等。LOF值越大,表示实例密 度越低于它邻居的密度。这些情况下,实例会被标记为异常。

完整代码

public class Anomaly {
    public static void main(String[] args) throws Exception {
        /*
         * 加载数据
         */
        String path = ClassUtils.getDefaultClassLoader().getResource("data/test07/ydata/A1Benchmark").getPath() + "/real_";
        List<List<Double>> rawData = new ArrayList<List<Double>>();
        double max = Double.MIN_VALUE;
        double min = Double.MAX_VALUE;
        // 读取67个CSV文件
        for (int i = 1; i <= 67; i++) {
            List<Double> sample = new ArrayList<Double>();
            BufferedReader reader = new BufferedReader(new FileReader(path + i + ".csv"));
            boolean isValid = false;
            // 跳过第一行(标题行)
            reader.readLine();
            while (reader.ready()) {
                String line = reader.readLine();
                String[] str = line.split(",");
                double value = Double.parseDouble(str[1]);
                // 添加值到sample列表
                sample.add(value);
                // 更新最大值和最小值
                if (value > max) max = value;
                if (value < min) min = value;
                // 检查是否是有效的样本
                if ("1".equals(str[2])) {
                    isValid = true;
                }
            }
            // 打印是否是有效样本
            System.out.println(isValid);
            // 关闭reader
            reader.close();
            // 将样本添加到rawData列表
            rawData.add(sample);
        }
        // 打印加载的样本数量、最大值和最小值
        System.out.println("size:" + rawData.size() + "\tmax: " + max + "\tmin: " + min);
        /*
         * 创建直方图
         */
        // 创建delta_t直方图
        int WIN_SIZE = 500; // 窗口大小
        int HIST_BINS = 20; // 直方图的 bins 数量
        int current = 0;
        List<double[]> dataHist = new ArrayList<double[]>();
        // 遍历每个样本
        for (List<Double> sample : rawData) {
            double[] hist = new double[HIST_BINS];
            for (Double value : sample) {
                int bin = toBin(normalize(value, min, max), HIST_BINS);
                hist[bin]++;
                current++;
                // 如果当前窗口达到窗口大小,重置计数器并添加直方图
                if (current == WIN_SIZE) {
                    current = 0;
                    dataHist.add(hist);
                    hist = new double[HIST_BINS];
                }
            }
            // 添加最后一个窗口的直方图
            dataHist.add(hist);
        }
        // 归一化直方图
        for (double[] hist : dataHist) {
            double sum = 0;
            for (double d : hist) {
                sum += d;
            }
            for (int i = 0; i < hist.length; i++) {
                hist[i] /= sum;
            }
        }
        // 打印总数直方图
        System.out.println("Total histograms:" + dataHist.size());
        /*
         * 创建数据库
         */
        ArrayList<Attribute> attributes = new ArrayList<Attribute>();
        for (int i = 0; i < HIST_BINS; i++)
            attributes.add(new Attribute("HIST-" + i));
        // 创建数据集
        Instances data = new Instances("data", attributes, dataHist.size());
        for (double[] hist : dataHist) {
            data.add(new DenseInstance(1.0, hist));
        }
        // 打印数据集大小
        System.out.println("Dataset created: " + data.size());
        /*
         * 构建模型
         */
        // 将数据分为训练集和测试集
        Instances trainData = data.testCV(2, 0);
        Instances testData = data.testCV(2, 1);
        System.out.println("Train: " + trainData.size() + "\nTest:" + testData.size());
        // 加载训练数据到 k-NN 算法
        LOF lof = new LOF();
        lof.setInputFormat(trainData);
        lof.setOptions(new String[]{"-min", "3", "-max", "3"});
        for (Instance inst : trainData) {
            lof.input(inst);
        }
        lof.batchFinished();
        // 打印 LOF 模型加载完成
        System.out.println("LOF loaded");
        // 对测试数据进行过滤
        Instances testDataLofScore = Filter.useFilter(testData, lof);
        for (Instance inst : testDataLofScore) {
            // 打印 LOF 得分
            System.out.println(inst.value(inst.numAttributes() - 1));
        }
    }

    /**
     * 将值归一化到 [0, 1] 区间
     *
     * @param value 值
     * @param min   最小值
     * @param max   最大值
     * @return 归一化后的值
     */
    static double normalize(double value, double min, double max) {
        return (value - min) / (max - min);
    }

    /**
     * 返回一个 bin,范围在 [0, bins)。假设值已归一化到 [0, 1] 区间
     *
     * @param normalizedValue 归一化后的值
     * @param bins            bins 数量
     * @return bin 编号
     */
    static int toBin(double normalizedValue, int bins) {
        if (normalizedValue == 1.0) return bins - 1;
        return (int) (normalizedValue * bins);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

顾北辰20

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值