目录
- 序列数据
- 统计工具
- (方案一)马尔可夫假设
- (方案二)潜变量模型
- 总结
- 序列模型
- 基于马尔可夫假设方式
- 该部分总代码
- 单步预测
- 多步预测
- k步预测
- 该部分总代码
序列数据
实际中数据是有时序结构的。
统计工具
在时间t观察带 x t x_t xt,(假设有T个时间的话)那么得到T个不独立的随机变量。( x 1 x_1 x1,… x T x_T xT)~ p(x)
使用条件概率展开
先算p(
x
1
x_1
x1),然后
x
2
x_2
x2是依赖于
x
1
x_1
x1的,
x
2
x_2
x2出现的概率是需要知道
x
1
x_1
x1是多少的。依次类推
x
3
x_3
x3依赖于
x
1
x_1
x1和
x
2
x_2
x2。…
对条件概率建模(正向)
训练f的话,其实就是训练一个模型,用
x
t
−
1
x_{t-1}
xt−1个数据训练一个模型来预测下一个模型。
所谓的自回归:就是给出一些数据,然后去预测这个数据的时候,不是用的另一些数据而是用这个数据前面的一些样本,所以叫自回归。
(方案一)马尔可夫假设
例:
在原始的模型中,这个数据点与前面三个相关
当τ=2的时候:
这个数据点与前τ个相关,即该数据点前2个。
(方案二)潜变量模型
例:算x1的话,就是跟前面的x相关,也与潜变量h1相关
一旦引入了潜变量概念的话,那么可以不断更新h。h1是根据前一个时间的潜变量和前一个时间的x相关。
等价于是说:建立两个模型。
第一个模型:根据前面一个事件的h和前面时间的x来算新的潜变量。
第二个模型:给定新的潜变量的状态和前一个时间的x怎么算新的x(这里指x1)。
反过来写:
总结
①时序模型中,当前数据跟之前观察到的数据相关
②自回归模型使用自身过去数据来预测未来
③马尔可夫模型假设当前只跟最近少数数据相关,从而简化模型。
④潜变量模型使用潜变量来概括历史信息。
序列模型
使用正弦函数和一些可加性噪声来生成序列数据,时间步为1,2,3…1000
import torch
from d2l import torch as d2l
T = 1000 # 总共产生1000个点
time = torch.arange(1, T + 1, dtype=torch.float32)
x = torch.sin(0.01 * time) + torch.normal(0, 0.2, (T,))
d2l.plot(time, [x], 'time', 'x', xlim=[1, 1000], figsize=(6, 3))
d2l.plt.tight_layout()
d2l.plt.show()
任务:给定任意一个时间点,预测接下来的时间的点长什么样子?
基于马尔可夫假设方式
使用马尔可夫假设:将数据映射为数据对
y t y_t yt就表示预测第T个事件的数据。
解释:
①T - tau 为样本数,tau 为特征数,创建的是一个二维张量。
②表示有T-tau个元素(996),每个元素是一个长度为tau(即:向量中元素的数量为4)的向量,形状是[996,4]。
③这个矩阵的每一行将存储一个特征向量,每个特征向量包含tau个连续的时间点数据(即:每个特征向量的长度为4)。
④每个特征向量包含tau个连续的时间点数据。由于每个特征向量需要包含x中的前tau个时间点,所以最多只能构造出T - tau个这样的特征向量。
features = torch.zeros((T - tau, tau))
解释为什么这么设置:用一个例子举例推断说明
x = [x0, x1, x2, x3, x4, x5, x6, x7, x8, x9]
# 延迟时间步长
tau = 4
# 1000个点中最多构造出996个特征向量,且特征向量的长度为4
# 一个特征向量就是一个样本
features = torch.zeros((T - tau, tau))
# 每四个数据作为特征,第五个作为标签,不断构造这样的数据形成数据集
for i in range(tau):
# x[i:T - tau + i]是切片操作,确保了每次都能从x中取出长度为tau的连续片段。
# 当i=0时,x[0:996]右边不包含
# 当i=1时,x[1:997]
# 当i=2时,x[2:998]
# 当i=3时,x[3:999]
# features[:, i]是996行,4列,然而x是一个向量,且长度是996
# 0 1 2 3
# 1 2 3 4
# 2 3 4 5
# ...
# 995 996 997 998
features[:, i] = x[i:T - tau + i]
# 提取标签数据并进行形状变换,从1行996列变成了996行1列 (即:从一维变成二维)
# x[tau:]从第五列开始到最后一列,转变为1列996行
labels = x[tau:].reshape((-1, 1))
# # 批量大小和训练样本数量
batch_size, n_train = 16, 600
# 使用 features 和 labels 的前 n_train 个样本创建一个可迭代的训练集
# 第一个参数(features[:n_train], labels[:n_train])是一个元组
# 这行代码的作用是从完整的数据集(features和labels)中分割出训练集(前n_train个样本)
train_iter = d2l.load_array((features[:n_train], labels[:n_train]), batch_size, is_train=True)
为什么写这句代码?
features[:, i] = x[i:T - tau + i]
在循环中的形状数据是这样的。(是不是很迷?这到底是在干什么?这里就是巧妙之处。)
这句代码设置的原因:
这是将i=0、1、2、3时,对应的1维的x的向量,填入features矩阵的第一列、第二列、第三列、第四列。
这里四个数据,也就是每一行作为数据特征。
第五个作为标签,第五个与前四个数据相关。
labels = x[tau:].reshape((-1, 1))
使用一个相当简单的结构:只是一个拥有两个全连接层的多层感知机。
# 初始化网络权重的函数
def init_weights(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
# 一个简单的多层感知机
def get_net():
net = nn.Sequential(nn.Linear(4, 10),
nn.ReLU(),
nn.Linear(10, 1))
net.apply(init_weights)
return net
# 平方损失。注意:MSELoss计算平方误差时不带系数1/2
loss = nn.MSELoss(reduction='none')
训练模型
def train(net, train_iter, loss, epochs, lr):
# 定义优化器
trainer = torch.optim.Adam(net.parameters(), lr)
# 迭代训练指定的轮数
for epoch in range(epochs):
# 遍历训练集中的每个批次
for X, y in train_iter:
# 梯度清零
trainer.zero_grad()
# 前向传播计算损失
l = loss(net(X), y)
# 反向传播求梯度
l.backward()
# 更新模型参数
trainer.step()
# 打印当前轮次的损失
print(f'epoch {epoch + 1}, '
f'loss: {d2l.evaluate_loss(net, train_iter, loss):f}')
# 创建神经网络模型
net = get_net()
# 训练模型
train(net, train_iter, loss, 5, 0.01)
该部分总代码
import torch
from torch import nn
from d2l import torch as d2l
def init_weights(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
def get_net():
net = nn.Sequential(nn.Linear(4, 10),
nn.ReLU(),
nn.Linear(10, 1))
net.apply(init_weights)
return net
def train(net, train_iter, loss, epochs, lr):
# 定义优化器
trainer = torch.optim.Adam(net.parameters(), lr)
# 迭代训练指定的轮数
for epoch in range(epochs):
# 遍历训练集中的每个批次
for X, y in train_iter:
# 梯度清零
trainer.zero_grad()
# 前向传播计算损失
l = loss(net(X), y)
# 反向传播求梯度
l.backward()
# 更新模型参数
trainer.step()
# 打印当前轮次的损失
print(f'epoch {epoch + 1}, '
f'loss: {d2l.evaluate_loss(net, train_iter, loss):f}')
T = 1000
time = torch.arange(1, T + 1, dtype=torch.float32)
x = torch.sin(0.01 * time) + torch.normal(0, 0.2, (T,))
tau = 4
features = torch.zeros((T - tau, tau))
for i in range(tau):
features[:, i] = x[i:T - tau + i]
labels = x[tau:].reshape((-1, 1))
batch_size, n_train = 16, 600
train_iter = d2l.load_array((features[:n_train], labels[:n_train]), batch_size, is_train=True)
loss = nn.MSELoss()
# 创建神经网络模型
net = get_net()
# 训练模型
train(net, train_iter, loss, 5, 0.01)
单步预测
模型预测下一个时间步的能力(即:单步预测)
import torch
from torch import nn
from d2l import torch as d2l
def init_weights(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
def get_net():
net = nn.Sequential(nn.Linear(4, 10),
nn.ReLU(),
nn.Linear(10, 1))
net.apply(init_weights)
return net
# net(要训练的神经网络模型)、train_iter(训练数据的迭代器,用于批量地提供训练数据)、loss(损失函数,用于评估模型的预测与实际值之间的差异)
def train(net, train_iter, loss, epochs, lr):
# 定义优化器,目标是调整模型的参数,以最小化损失函数。Adam是一种基于梯度下降的优化算法,具有自适应学习率的特点。
trainer = torch.optim.Adam(net.parameters(), lr)
# 迭代训练指定的轮数
for epoch in range(epochs):
# 遍历训练集中的每个批次
# 下面的y相当于合并元组中的labels[:n_train]
for X, y in train_iter:
# 梯度清零,PyTorch 会累积梯度,而我们需要的是每个批次独立计算的梯度
trainer.zero_grad()
# 通过模型 net 进行前向传播,计算模型对于当前批次数据的预测,然后使用损失函数 loss 计算预测值与真实值之间的损失。
l = loss(net(X), y)
# 反向传播求梯度
l.backward()
# 更新模型参数
trainer.step()
# 打印当前轮次的损失
print(f'epoch {epoch + 1}, '
f'loss: {d2l.evaluate_loss(net, train_iter, loss):f}')
T = 1000
time = torch.arange(1, T + 1, dtype=torch.float32)
x = torch.sin(0.01 * time) + torch.normal(0, 0.2, (T,))
tau = 4
features = torch.zeros((T - tau, tau))
for i in range(tau):
features[:, i] = x[i:T - tau + i]
labels = x[tau:].reshape((-1, 1))
batch_size, n_train = 16, 600
# features[:n_train] 并不是取一维的,而是取 features 张量的前 n_train(这里是 600)行。
# 表示从整个特征集中选择了前 600 个特征向量作为训练集。
# 合并成一个元组是为了将数据分为特征和标签,并将它们作为训练数据集传递给数据加载器
train_iter = d2l.load_array((features[:n_train], labels[:n_train]), batch_size, is_train=True)
# 只是定义还没使用
loss = nn.MSELoss()
# 创建神经网络模型
net = get_net()
# 训练模型,使用 features 和 labels 的前 n_train(即:600) 个样本进行训练。总共996个特征向量(features)
train(net, train_iter, loss, 5, 0.01)
# 将特征数据输入到模型net中,并获取模型的一步预测结果onestep_preds。net是一个已经训练好的神经网络,用于根据给定的特征预测未来的某个值。
# 使用训练好的模型,对所有特征向量进行一个预测
# onestep_preds形状为(T - tau, 1),对应于features中每个特征向量的预测结果。每个预测值都是基于其对应的特征向量通过整个模型的前向传播过程计算得到的。
onestep_preds = net(features)
# 进行数据可视化,将真实数据和一步预测结果绘制在同一个图中进行比较
d2l.plot(
# 是两个x轴的数据系列,下面x.detach().numpy() 表示模型在原始时间轴 time 上的观测数据
# onestep_preds.detach().numpy() 表示的是模型的预测数据。
[time, time[tau:]],
# x是在时间序列中真实观测到的数据。detach()用于从计算图中分离变量,防止其梯度被追踪
# 将模型预测的一步结果从PyTorch张量转换为NumPy数组,以便绘图。
[x.detach().numpy(), onestep_preds.detach().numpy()], 'time', 'x',
legend=['data', 'l-step preds'], xlim=[1, 1000], figsize=(6, 3))
d2l.plt.show()
前向传播:
当net(features)被调用时,features中的每个特征向量(即每一行)都将被逐一传递给模型的第一个线性层。线性层会对这些特征向量进行线性变换(即矩阵乘法加上偏置),产生新的特征表示。然后,这些新的特征表示会经过ReLU激活函数,进行非线性变换。最后,它们被传递给第二个线性层,该层将输出最终的预测结果。由于第二个线性层的输出维度为1,因此每个特征向量都会得到一个单一的预测值。
正如我们所料,单步预测效果不错。 即使这些预测的时间步超过了600+4(n_train + tau), 其结果看起来仍然是可信的。 然而有一个小问题:如果数据观察序列的时间步只到604, 我们需要一步一步地向前迈进:
多步预测
tau是每个特征向量包含的数量
为了进行多步预测,我们需要从当前点回溯 tau 个点来获取输入特征。
初始化多步预测:
开始多步预测时,您希望从训练数据的最后一个时间点之后立即开始,最后一个特征向量的最后一个时间点实际上与下一个时间点(即第一个未用于训练的时间点)是连续的,因此您只需要额外考虑 tau - 1 个时间点来确保时间窗口的连续性。然而,由于我们实际上想要包括训练数据中的最后一个完整特征向量,所以我们需要包括这个特征向量的所有 tau 个时间点。
通常,对于直到 x t x_t xt的观测序列,其在时间步t+k处的预测输出称为k步预测(k-step-ahead-prediction)。由于我们的观察已经到了 x 604 x_{604} x604,它的k步预测是。换句话说,我们必须使用我们自己的预测(而不是原始数据)来进行多步预测。让我们看看效果如何。
import torch
from torch import nn
from d2l import torch as d2l
def init_weights(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
def get_net():
net = nn.Sequential(nn.Linear(4, 10),
nn.ReLU(),
nn.Linear(10, 1))
net.apply(init_weights)
return net
# net(要训练的神经网络模型)、train_iter(训练数据的迭代器,用于批量地提供训练数据)、loss(损失函数,用于评估模型的预测与实际值之间的差异)
def train(net, train_iter, loss, epochs, lr):
# 定义优化器,目标是调整模型的参数,以最小化损失函数。Adam是一种基于梯度下降的优化算法,具有自适应学习率的特点。
trainer = torch.optim.Adam(net.parameters(), lr)
# 迭代训练指定的轮数
for epoch in range(epochs):
# 遍历训练集中的每个批次
# 下面的y相当于合并元组中的labels[:n_train]
for X, y in train_iter:
# 梯度清零,PyTorch 会累积梯度,而我们需要的是每个批次独立计算的梯度
trainer.zero_grad()
# 通过模型 net 进行前向传播,计算模型对于当前批次数据的预测,然后使用损失函数 loss 计算预测值与真实值之间的损失。
l = loss(net(X), y)
# 反向传播求梯度
l.backward()
# 更新模型参数
trainer.step()
# 打印当前轮次的损失
print(f'epoch {epoch + 1}, '
f'loss: {d2l.evaluate_loss(net, train_iter, loss):f}')
T = 1000
time = torch.arange(1, T + 1, dtype=torch.float32)
x = torch.sin(0.01 * time) + torch.normal(0, 0.2, (T,))
tau = 4
features = torch.zeros((T - tau, tau))
for i in range(tau):
features[:, i] = x[i:T - tau + i]
labels = x[tau:].reshape((-1, 1))
batch_size, n_train = 16, 600
# features[:n_train] 并不是取一维的,而是取 features 张量的前 n_train(这里是 600)行。
# 表示从整个特征集中选择了前 600 个特征向量作为训练集。
# 合并成一个元组是为了将数据分为特征和标签,并将它们作为训练数据集传递给数据加载器
train_iter = d2l.load_array((features[:n_train], labels[:n_train]), batch_size, is_train=True)
# 只是定义还没使用
loss = nn.MSELoss()
# 创建神经网络模型
net = get_net()
# 训练模型,使用 features 和 labels 的前 n_train(即:600) 个样本进行训练。总共996个特征向量(features)
train(net, train_iter, loss, 5, 0.01)
# 将特征数据输入到模型net中,并获取模型的一步预测结果onestep_preds。net是一个已经训练好的神经网络,用于根据给定的特征预测未来的某个值。
# 使用训练好的模型,对所有特征向量进行一个预测
# onestep_preds形状为(T - tau, 1),对应于features中每个特征向量的预测结果。每个预测值都是基于其对应的特征向量通过整个模型的前向传播过程计算得到的。
onestep_preds = net(features)
# 初始化多步预测结果的张量
multistep_preds = torch.zeros(T)
# 将已知的真实数据赋值给多步预测结果,0到604(604不包含)
# x[:n_train + tau]为已经拥有的真实的历史数据,可以直接使用这些数据作为多步预测的起点。
# 实际上是在告诉模型:“对于前 n_train + tau 个时间点,我们已经知道了真实的值,因此不需要预测它们。”
# 为什么是n_train + tau个呢?额外包含了 tau 个时间点确保在多步预测开始时有一个完整的时间窗口可用
multistep_preds[:n_train + tau] = x[:n_train + tau]
# 对剩余时间步进行多步预测
for i in range(n_train + tau, T):
# 获得多步预测结果
# multistep_preds[i - tau:i]是从n_train到n_train+3四个,然后n_train+1,...n_train+tau+1四个,依次类推
multistep_preds[i] = net(multistep_preds[i - tau:i].reshape((1, -1)))
# 进行数据可视化
d2l.plot(
[time, time[tau:], time[n_train + tau:]],
[x.detach().numpy(), onestep_preds.detach().numpy(), multistep_preds[n_train + tau:].detach().numpy()],
'time',
'x',
legend=['data', '1-step preds', 'multistep preds'],
xlim=[1, 1000],
figsize=(6, 3))
d2l.plt.show()
前600个数据点做训练,后面600-1000做预测
所以从这个600开始就往后预测400个点,也就是那个绿色,发现相差很离谱。
绿色的上面的紫红色线是每一次去预测下一个点,然后在下一个数据点用真正观测到的数据给你。而绿色则是从600这里,蓝色的点就不在给出了,然后需要用自己的预测的值去预测,然后一直下去。
所以为什么会相差这么多呢?因为每一次预测的时候都有一点误差,然后误差不断叠加。就是说预测有误差,然后误差进入到数据,然后误差又会增加,然后就会不断累积误差。
如上面的例子所示,绿线的预测显然并不理想。 经过几个预测步骤之后,预测的结果很快就会衰减到一个常数。为什么这个算法效果这么差呢?事实是由于错误的累积:假设在步骤1之后,我们积累了一些错误。 于是,步骤2的输入被扰动了,结果积累的误差是依照次序的,其中 c 为某个常数,后面的预测误差依此类推。因此误差可能会相当快地偏离真实的观测结果。
例如,未来24小时的天气预报往往相当准确,但超过这一点,精度就会迅速下降。
k步预测
基于k = 1, 4, 16, 64,通过对整个序列预测的计算,让我们更仔细地看一下k步预测的困难。
# 最大步长
max_steps = 64
# 初始化特征张量
features = torch.zeros((T - tau - max_steps + 1, tau + max_steps))
# 从 0 到 tau-1进行遍历
for i in range(tau):
# 构造特征矩阵
features[:, i] = x[i:i + T - tau - max_steps + 1]
# 从 tau 到 tau + max_steps - 1,通过 net(features[:, i - tau:i]) 进行多步预测
for i in range(tau, tau + max_steps):
# 进行多步预测并更新特征矩阵
features[:,i] = net(features[:, i - tau:i]).reshape(-1)
# 预测的步长
steps = (1, 4, 16, 64)
# 进行数据可视化
d2l.plot([time[tau + i - 1:T - max_steps + i] for i in steps],
[features[:, (tau + i - 1)].detach().numpy() for i in steps],
'time',
'x',
legend = [f'{i}-step preds' for i in steps],
xlim = [5,1000],
figsize=(6,3) )
该部分总代码
import torch
from torch import nn
from d2l import torch as d2l
def init_weights(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
def get_net():
net = nn.Sequential(nn.Linear(4, 10),
nn.ReLU(),
nn.Linear(10, 1))
net.apply(init_weights)
return net
# net(要训练的神经网络模型)、train_iter(训练数据的迭代器,用于批量地提供训练数据)、loss(损失函数,用于评估模型的预测与实际值之间的差异)
def train(net, train_iter, loss, epochs, lr):
# 定义优化器,目标是调整模型的参数,以最小化损失函数。Adam是一种基于梯度下降的优化算法,具有自适应学习率的特点。
trainer = torch.optim.Adam(net.parameters(), lr)
# 迭代训练指定的轮数
for epoch in range(epochs):
# 遍历训练集中的每个批次
# 下面的y相当于合并元组中的labels[:n_train]
for X, y in train_iter:
# 梯度清零,PyTorch 会累积梯度,而我们需要的是每个批次独立计算的梯度
trainer.zero_grad()
# 通过模型 net 进行前向传播,计算模型对于当前批次数据的预测,然后使用损失函数 loss 计算预测值与真实值之间的损失。
l = loss(net(X), y)
# 反向传播求梯度
l.backward()
# 更新模型参数
trainer.step()
# 打印当前轮次的损失
print(f'epoch {epoch + 1}, '
f'loss: {d2l.evaluate_loss(net, train_iter, loss):f}')
T = 1000
time = torch.arange(1, T + 1, dtype=torch.float32)
x = torch.sin(0.01 * time) + torch.normal(0, 0.2, (T,))
tau = 4
features = torch.zeros((T - tau, tau))
for i in range(tau):
features[:, i] = x[i:T - tau + i]
labels = x[tau:].reshape((-1, 1))
batch_size, n_train = 16, 600
# features[:n_train] 并不是取一维的,而是取 features 张量的前 n_train(这里是 600)行。
# 表示从整个特征集中选择了前 600 个特征向量作为训练集。
# 合并成一个元组是为了将数据分为特征和标签,并将它们作为训练数据集传递给数据加载器
train_iter = d2l.load_array((features[:n_train], labels[:n_train]), batch_size, is_train=True)
# 只是定义还没使用
loss = nn.MSELoss()
# 创建神经网络模型
net = get_net()
# 训练模型,使用 features 和 labels 的前 n_train(即:600) 个样本进行训练。总共996个特征向量(features)
train(net, train_iter, loss, 5, 0.01)
max_steps = 64
# 933行68列
features = torch.zeros((T - tau - max_steps + 1, tau + max_steps))
# 列i(i<tau)是来自x的观测,其时间步从(i)到(i+T-tau-max_steps+1)
for i in range(tau):
# 填充features矩阵的前tau列
# i的取值0、1、2、3
# 所有行的前四列,使用原始时间序列x的前tau个时间步的数据复制到features矩阵的前tau列中。
# 0到933、1到934、2到935、3到936,x是一维的
# 0 1 2 3
# 1 2 3 4
# 2 3 4 5
# ...
# 933 934 935 936
# 每列代表一个时间步的观测数据,时间范围从i到i + T - tau - max_steps + 1。
features[:, i] = x[i: i + T - tau - max_steps + 1]
# 列i(i>=tau)是来自(i-tau+1)步的预测,其时间步从(i)到(i+T-tau-max_steps+1)
# 从4开始(第五个点)到67列
for i in range(tau, tau + max_steps):
# 所有行的4到68列,features的所有行,0到4列
# 1到5列
# 2到6列
# ...
# 63列到67列
# 对于每一列(从tau(4列)到tau + max_steps - 1(67列)),它使用前tau(4)个时间步的观测数据(即features[:, i - tau:i])作为输入,
# 通过前tau(4)个时间步(即观测数据)来预测第i个时间步的值。
# 例如前4列预测第5列
# 通过.reshape(-1)调整为与输入数据相同的形状,并存储在features矩阵的相应列中。
features[:, i] = net(features[:, i - tau:i]).reshape(-1)
steps = (1, 4, 16, 64)
# steps=1时,time从4到937(937不包含)
# steps=4时,time从7到940(940不包含)
# steps=16时,time从19到952(952不包含)
# steps=64时,time从67到1000(1000不包含)
d2l.plot([time[tau + i - 1: T - max_steps + i] for i in steps],
# 从features矩阵中提取了每个预测步长对应的预测结果列
# 所有行的4(标号)列,所有行的7(标号)列,所有行的19(标号)列,所有行的67(标号)列
[features[:, tau + i - 1].detach().numpy() for i in steps],
'time', 'x',
legend=[f'{i}-step preds' for i in steps], xlim=[5, 1000],
figsize=(6, 3))
d2l.plt.show()
蓝色点:
每次给出4个真实的数据点预测下一个
紫红色:
每次给出4个真实的数据点预测接下来4个(这4个不会给出真实数据)
绿色:
每次给出4个真实的数据点预测接下来16个(这16个不会给出真实数据)
红色:
每次给出4个真实的数据点预测接下来64个(这64个不会给出真实数据)
其中:
[time[tau + i - 1: T - max_steps + i] for i in steps]