在当今的互联网时代,大量的数据在网络中流动。作为网站服务器的核心组件,Nginx承担着处理众多访问请求的重任。在这个过程中,监测Nginx的访问量并检测潜在异常变得至关重要,以便采取相应的措施确保服务器正常运行和保障数据安全。本文将详细介绍一个实际工作中的项目,旨在利用循环神经网络(RNN)分析Nginx访问量时间序列数据,以检测异常访问行为。我们将从数据介绍、预处理、模型构建到预测、可视化等方面,全面解析这一项目的实践过程。
循环神经网络(Recurrent Neural Networks, RNN)是一种广泛应用于时间序列分析的深度学习模型。它们可以处理具有时间依赖性的数据,因为它们具有在时间步之间传递信息的内部循环。RNN的这种特性使其非常适合处理具有顺序结构的数据,如音频、文本和时间序列。
在时间序列分析中,RNN具有广泛的应用,主要包括以下方面:
在使用RNN进行Nginx日志流量异常检测的项目中,整体思路可以分为以下几个步骤:
本项目中的数据来自原始的、经过模板初始化的Nginx服务器。这些数据是服务器在运行过程中自动生成的访问日志,详细记录了用户与服务器之间的交互信息。数据直接从Nginx服务器获取,因此具有较高的真实性和可靠性。
Nginx访问日志文件通常采用文本格式(如.log或.txt文件),每行代表一次访问请求。日志中包含多个字段,如时间戳、客户端IP地址、请求类型(GET、POST等)、请求的资源路径、响应状态码等。这些字段共同构成了访问流量的基本信息,可以用于分析访问模式、流量波动等问题。
通过上述的介绍,相信大家能更好地理解后续的数据处理和分析过程。
先使用pandas初步探索下数据集:
import pandas as pd
data = pd.read_table("power.log")
# 查看数据集前5行,因为涉及敏感信息,此处不做展示
print(df.head())
# 查看数据集基本信息
print(df.info())
包含24个列和8102076行数据。每一列的名称和数据类型如下:
Column | Dtype | Description |
---|---|---|
access_nginx_deb.eventtime | object | 请求时间。 |
access_nginx_deb.serveraddr | object | 服务器地址。 |
access_nginx_deb.userip | object | 用户IP地址。 |
access_nginx_deb.remoteuser | object | 远程用户。 |
access_nginx_deb.responsebodysize | int64 | 响应内容的大小,单位为字节。 |
access_nginx_deb.connectionsn | int64 | 连接数。 |
access_nginx_deb.connectionrequestscount | int64 | 连接请求数量。 |
access_nginx_deb.requestlength | int64 | 请求内容的大小,单位为字节。 |
access_nginx_deb.requestid | object | 请求ID。 |
access_nginx_deb.responsetime | float64 | 响应时间,单位为秒。 |
access_nginx_deb.upstreamtime | float64 | 上游服务器响应时间,单位为秒。 |
access_nginx_deb.httphost | object | HTTP主机。 |
access_nginx_deb.upstreamaddr | object | 上游服务器地址。 |
access_nginx_deb.xff | object | X-Forwarded-For头部。 |
access_nginx_deb.referer | object | HTTP Referer头部,即请求来源页面的URL。 |
access_nginx_deb.ua | object | HTTP User-Agent头部,即客户端浏览器信息。 |
access_nginx_deb.uri | object | 请求URI,即请求的资源路径。 |
access_nginx_deb.status | float64 | 响应状态码。 |
access_nginx_deb.method | object | 请求方法,如 GET、POST 等。 |
access_nginx_deb.url | object | 请求的完整URL。 |
access_nginx_deb.httpver | object | HTTP版本。 |
access_nginx_deb.day | object | 请求日期,格式为"YYYY-MM-DD"。 |
access_nginx_deb.hour | int64 | 请求小时数。 |
access_nginx_deb.source | object | 请求来源。 |
该DataFrame对象占用的内存为1.4 GB。同样,我们可以进行更多的初步数据探索:
# 查看数据集中是否存在缺失值
print(data.isnull().sum())
# 查看每个特征的唯一值数量
for column in data.columns:
print(f"{column} has {data[column].nunique()} unique values")
# 查看数据集中每个特征的值分布情况
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style="darkgrid")
for column in data.columns:
if data[column].dtype != 'object':
sns.displot(data[column])
plt.show()
数据下载:因为存在敏感数据,这里仅提供预处理好用于训练的数据集链接: 网盘
提取码:nuf8
–来自百度网盘超级会员V5的分享
要想做这个任务 ,整体思路是:从Nginx访问日志文件中提取与访问流量相关的字段,例如时间戳、请求类型(如GET、POST等)、IP地址和访问状态码等。接着,将时间戳转换为标准的时间序列格式,并根据时间戳对访问量进行汇总,得到每个时间间隔内的访问次数。可以选择合适的时间间隔,例如每分钟、每小时或每天,以捕获流量变化的粒度。本项目我们构建一个baseline版本,所以只选用时间戳这个字段。
根据时间戳计算每分钟内产生的访问量。首先,将原始Nginx访问日志中的时间戳提取出来,并将其转换为统一的时间序列格式。接着,根据时间戳对访问记录进行汇总,计算每分钟的访问量。通过这种方式,我们可以构建一个包含时间戳和对应访问量的数据集。此数据集将作为RNN时间序列模型的输入,用于学习访问流量的变化模式和预测未来一段时间内的访问量。通过这个方法,我们可以有效地利用RNN模型来分析和检测Nginx访问流量的异常情况。
df = pd.DataFrame(data["access_nginx_deb.eventtime"])
df
access_nginx_deb.eventtime | |
---|---|
0 | 2023-03-27 12:45:32 |
1 | 2023-03-27 12:45:31 |
2 | 2023-03-27 12:45:32 |
3 | 2023-03-27 12:45:31 |
4 | 2023-03-27 12:45:23 |
… | … |
8102071 | 2023-03-31 09:14:54 |
8102072 | 2023-03-31 09:14:54 |
8102073 | 2023-03-31 09:14:54 |
8102074 | 2023-03-31 09:14:54 |
8102075 | 2023-03-31 09:14:54 |
# 将字符串日期转换为日期时间
df['access_nginx_deb.eventtime'] = pd.to_datetime(df['access_nginx_deb.eventtime'])
# 将日期时间格式转换为不带秒的格式,将2023-03-31 09:14:54改成2023-03-31 09:14,把秒去掉
df['access_nginx_deb.eventtime'] = df['access_nginx_deb.eventtime'].dt.strftime('%Y-%m-%d %H:%M')
# 计算日期时间列中的值出现的次数
counts = df['access_nginx_deb.eventtime'].value_counts()
# 将计数作为新的一列添加到数据帧中
df['counts'] = df['access_nginx_deb.eventtime'].map(counts)
access_nginx_deb.eventtime | counts | |
---|---|---|
0 | 2023-03-27 12:45 | 1476 |
1 | 2023-03-27 12:45 | 1476 |
2 | 2023-03-27 12:45 | 1476 |
3 | 2023-03-27 12:45 | 1476 |
4 | 2023-03-27 12:45 | 1476 |
… | … | … |
8102071 | 2023-03-31 09:14 | 4293 |
8102072 | 2023-03-31 09:14 | 4293 |
8102073 | 2023-03-31 09:14 | 4293 |
8102074 | 2023-03-31 09:14 | 4293 |
8102075 | 2023-03-31 09:14 | 4293 |
# 去除数据帧中的重复行
df.drop_duplicates(subset='access_nginx_deb.eventtime', keep='first', inplace=True)
# 重置数据帧的索引
df.reset_index(drop=True, inplace=True)
access_nginx_deb.eventtime | counts | |
---|---|---|
0 | 2023-03-27 12:45 | 1476 |
1 | 2023-03-27 12:46 | 1300 |
2 | 2023-03-27 12:47 | 1420 |
3 | 2023-03-27 12:48 | 1269 |
4 | 2023-03-27 12:49 | 1161 |
… | … | … |
9188 | 2023-03-31 09:09 | 2778 |
9189 | 2023-03-31 09:10 | 3260 |
9190 | 2023-03-31 09:11 | 4240 |
9191 | 2023-03-31 09:12 | 4116 |
9192 | 2023-03-31 09:13 | 3797 |
到这里,我们初步完成了数据集的构建 |
做完数据后,我们可以绘制一些基本的图像,对一天的访问趋势做一个大致的了解
# 绘制基本图像
# 选择某些天的数据
dates = [("2023-03-28 00:00:00", "2023-03-28 23:59:59"),
("2023-03-30 00:00:00", "2023-03-30 23:59:59")]
fig, axs = plt.subplots(1, 2, figsize=(15, 5), sharey=True)
for i, (start_date, end_date) in enumerate(dates):
mask = (df["access_nginx_deb.eventtime"] >= start_date) & (df["access_nginx_deb.eventtime"] <= end_date)
selected_data = df.loc[mask]
# 绘制子图
axs[i].plot(selected_data["access_nginx_deb.eventtime"], selected_data["counts"])
# 设置x轴时间格式
date_format = mdates.DateFormatter("%H:%M")
axs[i].xaxis.set_major_formatter(date_format)
axs[i].set_xlabel("Eventtime")
axs[i].set_ylabel("Counts")
axs[i].set_title(f"Counts vs Eventtime on {start_date[:10]}")
axs[i].grid()
plt.tight_layout()
plt.show()
首先简单介绍一下滞后变量和滑动窗口的原理
def sliding_window(data, window_size):
"""
将时间序列数据转换为监督学习问题。
参数:
data -- 原始数据(一维数组)
window_size -- 滑动窗口大小
返回值:
X -- 特征矩阵,形状为 (n_samples, window_size)
y -- 目标向量,形状为 (n_samples,)
"""
n_samples = len(data) - window_size
X = np.zeros((n_samples, window_size))
y = np.zeros(n_samples)
for i in range(n_samples):
X[i] = data[i : i + window_size]
y[i] = data[i + window_size]
return X, y
具体来说,函数首先计算样本数量n_samples,然后为特征矩阵X和目标向量y分配空间。接下来,函数遍历所有样本,使用当前位置的滑动窗口来填充特征矩阵X,同时将窗口之后的那个数据点作为目标向量y中对应的值。最后,函数返回特征矩阵X和目标向量y。
这个函数的主要作用是将时间序列数据转换为一种适用于监督学习算法的形式,使得可以使用各种机器学习和深度学习方法进行训练和预测。
# 示例数据
data = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
# 转换为监督学习问题
window_size = 3
X, y = sliding_window(data, window_size)
print("Feature Matrix (X):")
print(X)
print("Target Vector (y):")
print(y)
接下来我们进行数据集的分割
# 计算70%索引的位置(此处也可以使用scikit-learn中的train_test_split函数实现)
split_index = int(0.8 * len(df))
# 按照索引拆分数据为训练集和测试集
train_data = df[:split_index]
test_data = df[split_index:]
# 设置时间窗口,存在问题:这个时间窗口不一定就是5分钟,如果某一分钟没出现数据,就会跨间隔做
look_back = 5
# 截断数据以使其能被时间窗口整除
train_data = train_data[: len(train_data) // look_back * look_back]
test_data = test_data[: len(test_data) // look_back * look_back]
# # 创建训练集和测试集的X和y
train_X, train_y = sliding_window(train_data["counts"].values, look_back)
test_X, test_y = sliding_window(test_data["counts"].values, look_back)
# 显示训练集和测试集的形状
print(train_X.shape, train_y.shape, test_X.shape, test_y.shape)
# 输入如下: (7345, 5) (7345,) (1830, 5) (1830,)
在使用Pytorch构建RNN循环神经网络之前,要先知道一下几个概念:
# 设置设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 将训练集和测试集转换为PyTorch张量
train_X_tensor = torch.tensor(train_X.reshape(-1, look_back, 1), dtype=torch.float32)
train_y_tensor = torch.tensor(train_y, dtype=torch.float32)
test_X_tensor = torch.tensor(test_X.reshape(-1, look_back, 1), dtype=torch.float32)
test_y_tensor = torch.tensor(test_y, dtype=torch.float32)
# 创建数据加载器
batch_size = 64
train_dataset = TensorDataset(train_X_tensor, train_y_tensor)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_dataset = TensorDataset(test_X_tensor, test_y_tensor)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
在这段代码中,我解释一下train_X_tensor的维度为什么这么定义,这个很重要 ,要理解:train_X_tensor 的维度是 (-1, look_back, 1),这是为了满足RNN模型输入的要求:
做完了数据后,我们要开始使用pytorch搭建一个RNN模型,如果不懂RNN理论的,可以去看看我的这篇文章链接: 循环神经网络(RNN)的黑科技:基于经典论文的全方位解析
# 定义RNN模型
class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size, num_layers=1):
super(SimpleRNN, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
# 创建一个 RNN 层,输入大小为 input_size,隐藏层大小为 hidden_size,层数为 num_layers
self.rnn = nn.RNN(input_size, hidden_size, num_layers, batch_first=True)
# 创建一个线性层,将 RNN 的输出(隐藏层)映射到指定输出大小
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
# 获取批量大小
batch_size = x.size(0)
# 初始化隐状态,大小为 (num_layers, batch_size, hidden_size)
h0 = torch.zeros(self.num_layers, batch_size, self.hidden_size)
# 前向传播:将输入 x 和初始隐状态 h0 传递给 RNN 层
out, _ = self.rnn(x, h0)
# 将 RNN 的输出传递给线性层,获取最后一个时间步的输出
out = self.fc(out[:, -1, :])
return out
总结一下:在Pytorch中定义RNN网络,主要组成如下:首先要继承自 nn.Module ,这将使得你可以定义自己的神经网络结构。然后要定义 init 方法,在自定义类中定义 init 方法,并调用父类(nn.Module)的 init 方法。在这个方法中,你需要定义 RNN 模型的各层。典型的 RNN 网络可能包括一个 RNN 层(如 nn.RNN、nn.LSTM 或 nn.GRU)和一个或多个全连接层(nn.Linear),最后定义 forward 方法:在自定义类中定义 forward 方法,这是网络的前向传播过程。在这个方法中,你需要按照网络结构的顺序将输入数据传递给各个层。对于 RNN 网络,通常需要初始化隐状态,将输入数据和隐状态传递给 RNN 层,然后将 RNN 层的输出传递给全连接层。
定义完网络结构后,就要进行参数的设置和模型的实例化
# 参数设置
input_size = 1 # 输入特征的数量
hidden_size = 256 # RNN层的隐藏单元数量
output_size = 1 # 输出层的大小
num_layers = 2 # RNN层数
# 初始化模型、损失函数和优化器
model = SimpleRNN(input_size, hidden_size, output_size, num_layers).to(device)
# 这里展示了一个简单的RNN模型,你也可以自定义一个更复杂的
print(model)
## SimpleRNN(
## (rnn): RNN(1, 256, num_layers=2, batch_first=True)
## (fc): Linear(in_features=256, out_features=1, bias=True)
)
损失函数和优化器在神经网络训练过程中起到关键作用。先简单的讲一下概念:
损失函数(Loss Function):用于衡量模型预测结果与真实标签之间的差距。换句话说,损失函数量化了模型在训练数据上的表现。目标是在训练过程中最小化这个损失值。常见的损失函数有均方误差(Mean Squared Error, MSE)、交叉熵损失(Cross Entropy Loss)等。损失函数的选择取决于问题类型,例如,对于回归问题,通常使用 MSE;对于分类问题,通常使用交叉熵损失。
优化器(Optimizer):用于更新模型参数以最小化损失函数的算法。在训练神经网络时,优化器根据损失函数的梯度来调整模型参数,从而使模型在每次迭代中改进。常见的优化器有随机梯度下降(Stochastic Gradient Descent, SGD)、Adam、RMSprop 等。选择合适的优化器对于网络训练的速度和收敛性能至关重要。
总结一下,损失函数用于衡量模型在训练数据上的表现,而优化器负责根据损失函数的梯度更新模型参数。在神经网络训练过程中,通过最小化损失函数来不断改进模型,使其在预测任务上具有更好的表现。
# 定义损失函数和优化器
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
在执行模型训练阶段,模型会根据输入的数据计算预测值。之后,计算损失函数(模型预测值与真实值之间的差异)。接着,使用优化器执行反向传播和参数更新,以便在下一个迭代中改进模型性能。在每个 epoch 结束时,计算训练损失并将其记录下来。
# 训练阶段
model.train()
running_train_loss = 0.0
for batch_X, batch_y in train_loader:
batch_X = batch_X.to(device)
batch_y = batch_y.to(device)
# 前向传播
outputs = model(batch_X).squeeze()
loss = criterion(outputs, batch_y)
# 反向传播和优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
running_train_loss += loss.item() * batch_X.size(0)
train_loss = running_train_loss / len(train_loader.dataset)
train_losses.append(train_loss)
在执行模型测试阶段,模型会在测试数据集上进行预测。我们会计算预测值与真实值之间的损失,并记录下测试损失。这有助于了解模型在未见过的数据上的性能。
# 测试阶段
model.eval()
running_test_loss = 0.0
with torch.no_grad():
for batch_X, batch_y in test_loader:
batch_X = batch_X.to(device)
batch_y = batch_y.to(device)
outputs = model(batch_X).squeeze()
loss = criterion(outputs, batch_y)
running_test_loss += loss.item() * batch_X.size(0)
test_loss = running_test_loss / len(test_loader.dataset)
test_losses.append(test_loss)
# 输出每个epoch的训练误差和测试误差
print(f"Epoch [{epoch+1}/{num_epochs}], Train Loss: {train_loss:.4f}, Test Loss: {test_loss:.4f}")
完整代码如下:
# 优化,添加loss曲线
# 定义损失函数和优化器
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# 训练模型并记录每个epoch的训练误差和测试误差
train_losses = []
test_losses = []
num_epochs = 1500
for epoch in range(num_epochs):
# 训练阶段
model.train()
running_train_loss = 0.0
for batch_X, batch_y in train_loader:
batch_X = batch_X.to(device)
batch_y = batch_y.to(device)
# 前向传播
outputs = model(batch_X).squeeze()
loss = criterion(outputs, batch_y)
# 反向传播和优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
running_train_loss += loss.item() * batch_X.size(0)
train_loss = running_train_loss / len(train_loader.dataset)
train_losses.append(train_loss)
# 测试阶段
model.eval()
running_test_loss = 0.0
with torch.no_grad():
for batch_X, batch_y in test_loader:
batch_X = batch_X.to(device)
batch_y = batch_y.to(device)
outputs = model(batch_X).squeeze()
loss = criterion(outputs, batch_y)
running_test_loss += loss.item() * batch_X.size(0)
test_loss = running_test_loss / len(test_loader.dataset)
test_losses.append(test_loss)
# 输出每个epoch的训练误差和测试误差
print(f"Epoch [{epoch+1}/{num_epochs}], Train Loss: {train_loss:.4f}, Test Loss: {test_loss:.4f}")
# 绘制训练误差和测试误差曲线
plt.figure(figsize=(10, 5))
plt.plot(train_losses, label="Train Loss", color="blue")
plt.plot(test_losses, label="Test Loss", color="red")
plt.xlabel("Epoch")
plt.ylabel("Loss")
plt.legend()
plt.show()
在训练过程中,优化器负责根据损失函数的梯度调整模型参数。在上面的代码中,使用了 PyTorch 提供的优化器(torch.optim.Adam)。在每个 epoch 的训练阶段,优化器会在反向传播过程中根据损失函数的梯度更新模型参数。通过多次迭代,模型会逐渐学习到一组能够最小化损失函数的参数。当然也可以尝试使用其他不同的优化器。
模型性能的评估指标是训练损失和测试损失。它们分别表示模型在训练数据集和测试数据集上的损失值。损失值越低,说明模型在对应数据集上的表现越好。通常,我们会关注测试损失,因为它反映了模型在未见过的数据上的泛化能力。当测试损失随着训练的进行而降低时,说明模型的性能在提高。如果测试损失开始上升,可能出现了过拟合现象,此时可以考虑调整模型结构、优化器设置或早停等策略来避免过拟合。
本文给出的是一个baseline版本,因为涉密原因,无法提供线上真实的优化源码,此处给一些建议,大家可以自行调整:
过拟合发生在模型在训练数据上表现很好,但在测试数据上表现较差的情况。如果出现过拟合问题,通常可以采用增加数据,尝试收集更多的 nginx 日志数据,或使用数据增强技术,其次可以适当的减小模型的层数或隐藏层神经元数量,以降低模型的容量。另外通过在损失函数中加入 L1 或 L2 正则化项或者在在 RNN 层后加入 Dropout 层,以随机丢弃部分神经元的输出。这有助于提高模型的泛化能力。最后在训练过程中监控验证损失。当验证损失连续多个 epoch 不再降低时,停止训练以避免过拟合的早停方案也值得一试
欠拟合是指模型在训练数据和测试数据上的表现都不理想。要解决欠拟合问题,可以尝试以下方法:增加模型的层数或隐藏层神经元数量,以提高模型的容量。增加训练迭代次数(即 num_epochs),以便模型有更多的机会学习数据的特征。调整学习率:尝试不同的学习率,以找到最佳的训练速度。较大的学习率可能会导致模型收敛过快,而较小的学习率可能会导致训练过程过慢。可以尝试使用学习率调度器(例如 torch.optim.lr_scheduler)来动态调整学习率。最后尝试使用不同的优化器(例如 Adam、RMSprop 等),以找到最适合当前问题的优化策略。
我下面通过一个示例,通俗的解释一下训练的过程:
当使用滑动窗口法(sliding_window)准备数据并送入 RNN 进行训练时,假设我们的原始数据是一维数组 [1, 2, 3, 4, 5, 6, 7, 8, 9],设置窗口大小(window_size)为 3。
# 使用滑动窗口法处理原始数据,得到特征矩阵 X 和目标向量 y。
X, y = sliding_window(data, window_size=3)
# 得到
X = [[1, 2, 3],
[2, 3, 4],
[3, 4, 5],
[4, 5, 6],
[5, 6, 7],
[6, 7, 8]]
y = [4, 5, 6, 7, 8, 9]
# 将 X 和 y 转换为 PyTorch 张量,调整形状以适应 RNN 的输入要求。假设已经将数据拆分为训练集和测试集,这里只演示训练集的处理过程。
train_X_tensor = torch.tensor(train_X.reshape(-1, 3, 1), dtype=torch.float32)
train_y_tensor = torch.tensor(train_y, dtype=torch.float32)
# 将处理后的数据送入 RNN 模型进行训练。在每个训练批次中,模型接收一个大小为 3 的时间窗口作为输入,输出对应的下一个时间点的预测值。训练过程中,模型通过最小化预测值与实际值(y)之间的差异来学习数据的模式。
# 例如,在一个训练批次中,模型可能接收到输入:
[[1, 2, 3],
[2, 3, 4],
[3, 4, 5]]
# 其输出的预测值:
[4.1, 4.9, 6.2]
# 而实际的目标值:
[4, 5, 6]
模型将通过调整其权重以减小预测值与实际值之间的差异来学习。在多次迭代训练之后,模型将能够更准确地预测未来的时间点。
在使用模型进行预测时,首先要做的就是使用相同的预处理方法(例如滑动窗口、标准化等)处理新的数据。确保数据符合模型训练时的输入形状和尺度,将预处理后的数据输入模型,得到预测结果。如果使用了标准化,请将预测结果逆标准化回原始尺度。
在完成训练之后,我们可以保存训练好的模型,并加载这个模型进行后续的预测
# 保存模型
torch.save(model.state_dict(), "rnn_model.pth")
loaded_model = SimpleRNN(input_size, hidden_size, output_size, num_layers).to(device)
loaded_model.load_state_dict(torch.load("rnn_model.pth"))
loaded_model.eval()
# 使用模型进行预测
n_future = 7
# 在测试集的最后一个时间窗口上进行预测
last_window = test_X_tensor[-1, :, :].unsqueeze(0).to(device)
future_preds = []
for _ in range(n_future):
pred = loaded_model(last_window)
future_preds.append(pred.item())
# 更新时间窗口以包含最新的预测值
last_window = torch.cat((last_window[:, 1:, :], pred.view(1, 1, -1)), dim=1)
# 打印预测值
print("Future predictions:")
print(future_preds)
# Future predictions: [91.52577209472656, 102.395751953125, 116.01768493652344, 124.50785064697266, 130.66241455078125, 136.9891815185547, 144.7757568359375]
根据上面的代码,首先要了解这两个维度:
# 假设有一个时间序列数据集,其观测值如下:
data = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
# 使用长度为 3 的时间窗口(即 look_back = 3)来训练 RNN 模型。测试数据集的最后一个时间窗口将如下所示:
test_X = [[80, 90, 100]]
# 这是一个 (1, 3, 1) 形状的张量,包含一个批量大小,3 个时间步长,每个时间步长包含一个特征。
# 现在,想要预测未来的 2 个时间步长。将使用 last_window 变量来存储当前时间窗口。首先,我们将其设置为测试数据集的最后一个时间窗口:
last_window = test_X_tensor[-1, :, :].unsqueeze(0).to(device)
# 接下来,进行第一次预测
# 1、使用当前时间窗口进行预测:pred = loaded_model(last_window)
# 2、添加预测值到 future_preds 列表:future_preds.append(pred.item())
# 3、从当前时间窗口中移除第一个时间步长,并将预测值添加到当前时间窗口:
last_window = torch.cat((last_window[:, 1:, :], pred.view(1, 1, -1)), dim=1)
# 假设第一次预测的值为 110,则 last_window 将更新为:
last_window = [[90, 100, 110]]
# 接下来,进行第二次预测:
# 1、使用更新后的时间窗口进行预测:pred = loaded_model(last_window)
# 2、添加预测值到 future_preds 列表:future_preds.append(pred.item())
# 3、从当前时间窗口中移除第一个时间步长,并将预测值添加到当前时间窗口:
last_window = torch.cat((last_window[:, 1:, :], pred.view(1, 1, -1)), dim=1)
# 假设第二次预测的值为 120,则 last_window 将更新为:
last_window = [[100, 110, 120]]
预测过程结束,我们获得了两个未来时间步长的预测值 110 和 120,这些值将存储在 future_preds 列表中。通过这种方式,我们可以连续预测未来的时间步长,每次更新时间窗口以包含最新的预测值。现在这个过程应该很清晰了吧。
在实际应用中,可能需要根据实际情况调整阈值,以找到最佳的平衡点,既能检测到异常,又能降低误报率。在设置告警阈值时,可以参考训练集和验证集的预测误差分布,并根据业务需求和容忍度进行调整。下面是一些可参考的方法:
可以使用测试集上的滚动预测来初步查看模型的基本效果,因为目前的项目模型仅为baseline版本,所以定义峰值点的检测还有待提高,大家可以自行调整,后面结论与未来工作部分也写了一些思路,可以进行参考
# 使用测试集上的滚动预测
rolling_preds = []
model.eval()
with torch.no_grad():
for i in range(len(test_X_tensor)):
input_window = test_X_tensor[i, :, :].unsqueeze(0).to(device)
pred = model(input_window)
rolling_preds.append(pred.item())
# 可视化预测结果与测试集
plt.figure(figsize=(12, 6))
plt.plot(test_y, label="Test Data", color="blue")
plt.plot(rolling_preds, label="Predicted Data", color="red", linestyle="--")
plt.xlabel("Time")
plt.ylabel("Counts")
plt.legend()
plt.show()
此处演示使用阈值设置为误差的 3 倍标准差作为异常标准,要根据实际情况调整策略
# 准备输入数据
input_data = test_X[-1] # 使用测试集中的最后一个窗口作为输入数据
print(input_data)
input_data = input_data.reshape(1, look_back, input_size) # 使用正确的形状调整输入数据
# 将输入数据转换为PyTorch张量
input_tensor = torch.tensor(input_data, dtype=torch.float32).to(device)
# 使用模型进行预测
n_future = 7
future_preds = []
last_window = input_tensor
for _ in range(n_future):
pred = loaded_model(last_window)
future_preds.append(pred.item())
# 更新时间窗口以包含最新的预测值
last_window = torch.cat((last_window[:, 1:, :], pred.view(1, 1, -1)), dim=1)
# 打印预测值
print("Future predictions:")
print(future_preds)
# 异常检测
test_preds = []
for window in test_X:
window = window.reshape(1, look_back, input_size)
input_tensor = torch.tensor(window, dtype=torch.float32).to(device)
pred = loaded_model(input_tensor)
test_preds.append(pred.item())
errors = np.abs(np.array(test_preds) - test_y)
threshold = 5 * errors.std()
anomalies = np.where(errors > threshold)[0]
print("Anomalies detected:")
for idx in anomalies:
print(f"Index: {idx}, Error: {errors[idx]}")
#输出结果:再次说明,此处baseline模型,如需生产使用请调优
[35. 97. 36. 4. 6. 3. 3. 2. 8. 2. 1. 3. 1. 3. 11. 2.]
Future predictions:
[91.52577209472656, 102.395751953125, 116.01768493652344, 124.50785064697266, 130.66241455078125, 136.9891815185547, 144.7757568359375]
Anomalies detected:
Index: 69, Actual Value: 5325.0, Predicted Value: 3271.231201171875, Error: 2053.768798828125
Index: 564, Actual Value: 5898.0, Predicted Value: 3271.248046875, Error: 2626.751953125
Index: 578, Actual Value: 5106.0, Predicted Value: 3271.37451171875, Error: 1834.62548828125
Index: 990, Actual Value: 784.0, Predicted Value: 3198.170654296875, Error: 2414.170654296875
Index: 1421, Actual Value: 775.0, Predicted Value: 3009.341796875, Error: 2234.341796875
Index: 1608, Actual Value: 1925.0, Predicted Value: 91.53280639648438, Error: 1833.4671936035156
Index: 1638, Actual Value: 87.0, Predicted Value: 2168.897216796875, Error: 2081.897216796875
Index: 1684, Actual Value: 2859.0, Predicted Value: 91.54193878173828, Error: 2767.4580612182617
Index: 1746, Actual Value: 31.0, Predicted Value: 2541.65625, Error: 2510.65625
本文以baseline版本实现贯穿,其实 有很多方法优化,在这里我提以下几个方面:针对这样一个以时间戳和访问量为主要字段的数据,可以进行时间序列的重采样和平滑:
import pandas as pd
data = pd.DataFrame({'timestamp': timestamps, 'visits': visits})
data['timestamp'] = pd.to_datetime(data['timestamp'])
data = data.set_index('timestamp')
data_resampled = data.resample('1H').sum() # 按小时进行重采样并求和
# 移动平均(MA)
window_size = 3
data_smoothed_ma = data_resampled.rolling(window=window_size).mean()
# 指数加权移动平均(EWMA)
alpha = 0.1
data_smoothed_ewma = data_resampled.ewm(alpha=alpha).mean()
注意:在使用平滑方法时,请根据实际情况选择合适的窗口大小和平滑系数。过大的窗口可能会导致模型对异常和突变不够敏感,而过小的窗口则可能无法有效减小噪声。
通过这些方法可以对时间序列数据进行重采样和平滑处理,使数据更适合训练RNN模型以进行异常检测。
此外,这个案例中,上文中已经提到了滞后变量和滑动窗口可以帮助捕捉时间序列数据的潜在模式和依赖关系。下面分别介绍这两种技术的实现方法:
在pandas中,可以使用shift函数创建滞后变量:
data['visits_lag1'] = data['visits'].shift(1) # 创建滞后1阶变量
data['visits_lag2'] = data['visits'].shift(2) # 创建滞后2阶变量
# ...根据需要创建更多滞后变量
data = data.dropna() # 移除包含NaN值的行
在pandas中,可以使用rolling函数创建滑动窗口特征:
window_size = 3
data['visits_mean'] = data['visits'].rolling(window=window_size).mean() # 计算窗口内的均值
data['visits_std'] = data['visits'].rolling(window=window_size).std() # 计算窗口内的标准差
# ...根据需要创建更多滑动窗口特征
data = data.dropna() # 移除包含NaN值的行
滞后变量和滑动窗口都是为了捕捉时间序列数据中的依赖关系和局部特征。滞后变量反映了不同时刻观测值之间的关联,有助于模型学习数据的序列结构。而滑动窗口特征可以提供关于数据局部波动和趋势的信息,有助于模型识别访问流量的变化规律。在训练RNN模型时,将这些特征纳入输入数据,可以提高模型的预测能力和异常检测性能。
本文中使用的sliding_window函数与之前介绍的滞后变量和滑动窗口在目的和实现上有一些区别。
sliding_window函数的主要目的是将时间序列数据转换为监督学习问题。通过使用滑动窗口的方式,将一段连续的观测值作为特征(X),并使用紧跟在这段观测值之后的单个数据点作为目标(y)。这种转换使得我们可以将时间序列问题视为一个监督学习问题,从而应用各种监督学习算法进行预测。
滞后变量用于捕捉时间序列中不同时间点之间的依赖关系。通过将过去的观测值(滞后变量)作为特征,可以帮助模型更好地理解数据中的时间相关性。
滑动窗口特征用于提取时间序列数据的局部统计特征,如滑动窗口内的均值、标准差等。这些特征可以提供关于数据局部波动和趋势的信息,有助于模型识别访问流量的变化规律。
总结一下,sliding_window函数主要用于将时间序列数据转换为监督学习问题,而滞后变量和滑动窗口特征用于从时间序列中提取有用的特征。在实际应用中,我们可以结合这三种方法来处理时间序列数据,以便为监督学习模型提供丰富的特征信息。例如,可以在sliding_window函数中使用滞后变量和滑动窗口特征作为输入特征,从而提高模型的预测性能。
另外可以看下数据集:以counts这列作为时间窗口,但是couns存在很大的不均衡问题,最大值是6700,最小值只有1,数据的不均衡可能会影响模型的训练。在这种情况下,RNN 可能会对较大值的样本产生较大的误差,而忽略较小值的样本。为了解决这个问题,可以采取以下方法之一或组合使用:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
train_data["counts"] = scaler.fit_transform(train_data["counts"].values.reshape(-1, 1))
test_data["counts"] = scaler.transform(test_data["counts"].values.reshape(-1, 1))
train_data["counts"] = np.log1p(train_data["counts"])
test_data["counts"] = np.log1p(test_data["counts"])
综合考虑,可以先尝试数据标准化和对数变换这两种方法,观察它们对模型性能的影响。如有必要,再尝试其他方法。
注意:当你使用第一种方法(数据标准化)时,需要在预测阶段将预测值反向转换回原始尺度。这意味着在模型完成预测后,使用相同的标准化器(例如 StandardScaler)对预测结果进行逆变换。这里是一个例子:
首先,在训练和测试数据上使用 StandardScaler:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
train_data["counts"] = scaler.fit_transform(train_data["counts"].values.reshape(-1, 1))
test_data["counts"] = scaler.transform(test_data["counts"].values.reshape(-1, 1))
然后,训练模型并进行预测。假设 predicted_y_tensor 是预测值的张量:
predicted_y_tensor = model(test_X_tensor).squeeze()
接下来,将预测值转换为 NumPy 数组,并使用 scaler.inverse_transform() 方法将其反向转换回原始尺度:
predicted_y = predicted_y_tensor.cpu().numpy()
predicted_y_original_scale = scaler.inverse_transform(predicted_y.reshape(-1, 1))
现在,predicted_y_original_scale 是一个 NumPy 数组,其中包含反向转换后的预测值。请注意,这个逆变换应该在预测完成后进行。
将Nginx访问日志转换为以每分钟为时间间隔的访问量时间序列数据,有助于捕捉访问流量的波动和变化趋势。然后,使用RNN时间序列模型来学习这些模式,以便进行异常检测。不过,需要注意的是,在实际操作中,可以根据实际情况调整时间间隔(如每分钟、每小时等),以便更好地捕捉访问流量的波动特征。
在这篇博客中,我们通过实战演示了如何使用循环神经网络(RNN)解决时间序列问题。RNN是一种非常强大的神经网络模型,但是在某些情况下,它可能无法捕捉到长期的依赖关系。在这种情况下,我们可以使用一种更强大的循环神经网络模型——长短期记忆网络(LSTM)。在接下来的一篇文章中,我们将探讨LSTM的综述,并介绍如何在实际应用中使用它来解决更加复杂的自然语言处理问题。敬请期待!