之前所学的全连接神经网络(DNN)和卷积神经网络(CNN),他们的前一个输入和后一个输入是没有关系的(从输入层到隐含层再到输出层,层与层之间是全连接的,每层之间的节点是无连接的)。如下面这样,输出层X,经过隐藏层,到输出层Y,通过调节权重Win和Wout就可以实现学习的效果。
但是当我们处理序列信息的时候,某些前面的输入和后面的输入是有关系的,比如:当我们在理解一句话意思时,孤立的理解这句话的每个词是不够的,我们需要处理这些词连接起来的整个序列;这个时候我们就需要使用到循环神经网络(Recurrent Neural Network)。比如:手机坏了,我要买一个256g苹果,结合前面的手机坏了,这个苹果含义是一台手机,而不是不是吃的苹果。
1 基本循环神经网络RNN
X是输入向量,O是输出,S是隐藏层,U是输入到隐藏层的权重矩阵,V是隐藏层到输出层的权重矩阵,循环神经网络的隐藏层的值s不仅仅取决于当前这次的输入x,还取决于上一次隐藏层的值s。权重矩阵w就是隐藏层上一次的值作为这一次的输入的权重。上面的图可以在时间维度进行展开成一个链式的结构。
这个网络在t时刻接收到输入Xt之后,隐藏层的值是St,输出值是Ot。关键一点是,St的值不仅仅取决于Xt,还取决于St-1。
隐藏层计算公式为:St=f(UXt + W * St-1),其中f为激活函数。
输出层计算公式为:Ot = g(VSt),其中g为激活函数,V是权重矩阵。
结合前面的例子,“手机坏了,我要买一个256g苹果”,被分词之后,成一组向量[X1,X2,...,X6]
循环神经网络从左到右阅读这个句子,不断调用相同的RNN CELL来处理。但是上面方法有一个明显的缺陷,当阅读的句子很长的时候,网络会变得复杂甚至无效。当前面的信息在传递到后面的同时,信息的权重会下降(梯度爆炸和梯度消失),导致预测不准。比如下面两句话,was和were要根据前面的student的单复数来确定。句子过长的情况下,就难以判定了,因此RNN这种网络被称为短时记忆网络(Short Term Memory)。
he student,who got A+ in the exam,was excellent.
The students,who got A+ in the exam,were excellent.
2 LSTM循环神经网络
为了解决上面记忆信息不足的问题,引入一种长短时记忆网络(Long Short Term Memory,LSTM)。原始RNN的隐藏层只有一个状态,即h,它对于短期的输入非常敏感。那么如果我们再增加一个门(gate)机制用于控制特征的流通和损失,即c,让它来保存长期的状态。
左边是不同时刻的X,中间黄色球是隐藏层H,右边绿色是输出Y,和基本RNN相比,除了黄色的链条(Short Term Memory),LSTM增加了一个新的红色链条,用C来表示,叫LongTerm Memory,且两个链条相互作用,相关更新,将这两条线“拍平”后如下:
我们在学习的时候,会经常看到下面这幅图,比较难以理解,主要是二维的图难以想象成三维的结构。理解上面三维结构的两条线,就知道下面的二维图两条线是怎么进行数据更新的。
现在正式介绍LSTM中三个重要的门结构。
2.1 遗忘门
函数f1是sigmoid函数,可以把矩阵的值压缩到0-1之间,矩阵元素相乘的时候,因为任何数乘以 0 都得 0,这部分信息就会剔除掉。同样的,任何数乘以 1 都得到它本身,这部分信息就会完美地保存下来。这样网络就能了解哪些数据是需要遗忘,哪些数据是需要保存。
数据更新公式:
与基本RNN的内部结构计算非常相似,首先将当前时间步输入x(t)与上一个时间步隐含状态h(t—1)拼接,得到[x(t),h(t—1)],然后通过一个全连接层做变换,最后通过sigmoid函数进行激活得到f1(t),我们可以将f1(t)看作是门值,好比一扇门开合的大小程度,门值都将作用在通过该扇门的张量,遗忘门门值将作用的上一层的细胞状态上,代表遗忘过去的多少信息,又因为遗忘门门值是由x(t),h(t—1)计算得来的,因此整个公式意味着根据当前时间步输入和上一个时间步隐含状态h(t—1)来决定遗忘多少上一层的细胞状态所携带的过往信息。
2.2 输入门
我们看到输入门的计算公式有两个,第一个就是产生输入门门值的公式,它和遗忘门公式几乎相同,区别只是在于它们之后要作用的目标上,这个公式意味着输入信息有多少需要进行过滤。输入门的第二个公式是与传统RNN的内部结构计算相同。对于LSTM来讲,它得到的是当前的细胞状态,而不是像经典RNN一样得到的是隐含状态。最后,第一个公式f1与上一次的Ct-1做全连接然后,加上f2之后的结果,更新给当前的Ct,这个过程被称为LSTM的细胞状态更新。
2.3 输出门
输出门部分的公式也是两个,第一个即是计算输出门的门值,它和遗忘门,输入门计算方式相同。第二个即是使用这个门值产生隐含状态h(t),他将作用在更新后的细胞状态C(t)上,并做tanh激活,最终得到h(t)作为下一时间步输入的一部分.整个输出门的过程,就是为了产生隐含状态h(t)。
上面的过程用代码表示就是
# 定义一个LSTM模型
class LSTM(nn.Module):
def __init__(self, input_size, hidden_size, num_layers, output_size):
super(LSTM, self).__init__()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
# 初始化隐藏状态h0, c0为全0向量
h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device)
# 将输入x和隐藏状态(h0, c0)传入LSTM网络
out, _ = self.lstm(x, (h0, c0))
# 取最后一个时间步的输出作为LSTM网络的输出
out = self.fc(out[:, -1, :])
return out
3 BiLSTM神经网络
双向循环神经网络(Bi-Directional Long Short-Term Memory,BiLSTM)是一种特殊的循环神经网络(RNN)架构,它包含一个正向LSTM 层和一个反向LSTM层。这两个LSTM层分别对序列中的元素进行正向和反向传递,并在最后的隐藏层中进行合并。这样,BiLSTM可以同时考虑序列中的历史信息和未来信息,使得它在处理序列数据任务中(如文本分类和序列标注)有着良好的表现。
前向的LSTML依次输入“我”,“爱”,“你”得到三个向量{h0,h1,h2}。后向的LSTMR依次输入“你”,“爱”,“我”得到三个向量{h5,h4,h3}。最后将前向和后向的隐向量进行拼接得到{[h0,h5],[h1,h4],[h2,h3]},即{A,B,C},对于情感分类任务来说,我们采用的句子表示往往是[h2,h5],因为这其中包含了前向和后向的所有信息。
4 电影评价的极性分析实践
4.1 划分训练集、测试集
import torch
import torch.nn.functional as F
from torchtext import data
from torchtext import datasets
from torchtext.legacy import data, datasets
import time
import random
torch.backends.cudnn.deterministic = True
# 定义超参数
RANDOM_SEED = 123
torch.manual_seed(RANDOM_SEED)
VOCABULARY_SIZE = 20000
LEARNING_RATE = 1e-3
BATCH_SIZE = 128
NUM_EPOCHS = 15
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
if __name__ == '__main__':
# 注意:由于 RNN 只能处理序列中的非 padded 元素(即非0数据)
# 对于任何 padded 元素输出都是 0 。所以在准备数据的时候将include_length设置为True
# 以获得句子的实际长度。
TEXT = data.Field(tokenize='spacy', include_lengths=True, tokenizer_language='en_core_web_sm')
LABEL = data.LabelField(dtype=torch.float)
train_data, test_data = datasets.IMDB.splits(TEXT, LABEL)
datasets.IMDB.splits(TEXT,LABEL)
# 从训练集中选取部分做验证集
train_data, valid_data = train_data.split(random_state=random.seed(RANDOM_SEED), split_ratio=0.8)
print(f'Num Train: {len(train_data)}')
print(f'Num Valid: {len(valid_data)}')
print(f'Num Test: {len(test_data)}')
print("train_data[0:200]", test_data.examples[0].text[0:100])
Num Train: 20000
Num Valid: 5000
Num Test: 25000
train_data[0:200] ['Based', 'on', 'an', 'actual', 'story', ',', 'John', 'Boorman', 'shows', 'the', 'struggle', 'of', 'an', 'American', 'doctor', ',', 'whose', 'husband', 'and', 'son', 'were', 'murdered', 'and', 'she', 'was', 'continually', 'plagued', 'with', 'her', 'loss', '.', 'A', 'holiday', 'to', 'Burma', 'with', 'her', 'sister', 'seemed', 'like', 'a', 'good', 'idea', 'to', 'get', 'away', 'from', 'it', 'all', ',', 'but', 'when', 'her', 'passport', 'was', 'stolen', 'in', 'Rangoon', ',', 'she', 'could', 'not', 'leave', 'the', 'country', 'with', 'her', 'sister', ',', 'and', 'was', 'forced', 'to', 'stay', 'back', 'until', 'she', 'could', 'get', 'I.D.', 'papers', 'from', 'the', 'American', 'embassy', '.', 'To', 'fill', 'in', 'a', 'day', 'before', 'she', 'could', 'fly', 'out', ',', 'she', 'took', 'a']
4.2 创建词向量
TEXT.build_vocab(train_data, max_size=VOCABULARY_SIZE)
LABEL.build_vocab(train_data)
print(f'Vocabulary size: {len(TEXT.vocab)}')
print(f'Number of classes: {len(LABEL.vocab)}')
输出:
Vocabulary size: 20002
Number of classes: 2
TEXT.build_vocab表示从预训练的词向量中,将当前训练数据中的词汇的词向量抽取出来,构成当前训练集的 Vocab(词汇表)。对于当前词向量语料库中没有出现的单词(记为UNK)。
4.3 创建数据迭代器
BATCH_SIZE = 64
# 根据当前环境选择是否调用GPU进行训练
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 创建数据迭代器
train_loader, valid_loader, test_loader = data.BucketIterator.splits(
(train_data, valid_data, test_data),
batch_size=BATCH_SIZE,
sort_within_batch=True, # 为了 packed_padded_sequence
device=device)
4.4 定义RNN模型
class RNN(nn.Module):
def __init__(self, input_dim, embedding_dim, hidden_dim, output_dim):
super().__init__()
self.embedding = nn.Embedding(input_dim, embedding_dim)
self.rnn = nn.RNN(embedding_dim, hidden_dim)
self.fc = nn.Linear(hidden_dim, output_dim)
def forward(self, text, text_length):
embedded = self.embedding(text)
# pack_padded_sequence 技术的应用
packed = torch.nn.utils.rnn.pack_padded_sequence(embedded, text_length)
output, hidden = self.rnn(packed)
#squeeze(0)的作用是将张量中维度大小为1的维度进行压缩,减少张量的维度数量和大小。
#如果张量中没有维度大小为1的维度,那么squeeze(0)函数不会对张量进行任何修改,它会返回与原始张量相同的张量
# view(-1)将张量重塑为一维形状
return self.fc(hidden.squeeze(0)).view(-1)
关于pack_padded_sequence(处理Pad问题)的解释:
"Pad问题"是指填充操作中的一个常见问题,即如何处理填充元素(通常用特殊的占位符,如<pad>)对模型训练和推理的影响。我们需要对电影评论进行情感分类,这些评论往往具有不同长度的单词数量。当我们将这些评论句子作为输入传递给循环神经网络(RNN)进行处理时,由于RNN的输入需要是固定长度的张量,我们需要对序列进行填充(padding)操作,使得每个评论都具有相同的长度。
假设我们有三个电影评论,分别是"这是一部很好看的电影","这个电影一般般"和"我不喜欢这部电影"。我们可以将这些评论编码为以下张量:
评论1: [这, 是, 一部, 很, 好看, 的, 电影]
评论2: [这个, 电影, 一般般]
评论3: [我, 不喜欢, 这部, 电影]
在这个例子中,我们有3个电影评论。它们的长度分别是7、3和4。我们需要将它们填充到相同的长度,以便能够将它们作为一个批次输入到模型中。填充后的序列是:
评论1: [这, 是, 一部, 很, 好看, 的, 电影]
评论2: [这个, 电影, 一般般, <pad>, <pad>, <pad>, <pad>]
评论3: [我, 不喜欢, 这部, 电影, <pad>, <pad>, <pad>]
4.5 RNN模型训练
INPUT_DIM = len(TEXT.vocab)
EMBEDDING_DIM = 128
HIDDEN_DIM = 256
OUTPUT_DIM = 1
torch.manual_seed(RANDOM_SEED)
model = RNN(INPUT_DIM, EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM)
model = model.to(DEVICE)
#选择Adam优化器
optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)
start_time = time.time()
for epoch in range(NUM_EPOCHS):
model.train()
for batch_idx, batch_data in enumerate(train_loader):
text, text_lengths = batch_data.text
logits = model(text, text_lengths)
cost = F.binary_cross_entropy_with_logits(logits, batch_data.label)
optimizer.zero_grad()
cost.backward()
optimizer.step()
if not batch_idx % 50:
print(f'Epoch: {epoch + 1:03d}/{NUM_EPOCHS:03d} | '
f'Batch {batch_idx:03d}/{len(train_loader):03d} | '
f'Cost: {cost:.4f}')
4.6 RNN模型评估
def compute_binary_accuracy(model, data_loader, device):
model.eval()
correct_pred, num_examples = 0, 0
with torch.no_grad():
for batch_idx, batch_data in enumerate(data_loader):
text, text_lengths = batch_data.text
logits = model(text, text_lengths)
predicted_labels = (torch.sigmoid(logits) > 0.5).long()
num_examples += batch_data.label.size(0)
correct_pred += (predicted_labels == batch_data.label.long()).sum()
return correct_pred.float() / num_examples * 100
def predict_sentiment(model, sentence):
model.eval()
tokenized = [tok.text for tok in nlp.tokenizer(sentence)]
indexed = [TEXT.vocab.stoi[t] for t in tokenized]
length = [len(indexed)]
tensor = torch.LongTensor(indexed).to(DEVICE)
tensor = tensor.unsqueeze(1)
length_tensor = torch.LongTensor(length)
prediction = torch.sigmoid(model(tensor, length_tensor))
return prediction.item()
with torch.set_grad_enabled(False):
print(f'training accuracy: '
f'{compute_binary_accuracy(model, train_loader, DEVICE):.2f}%'
f'\nvalid accuracy: '
f'{compute_binary_accuracy(model, valid_loader, DEVICE):.2f}%')
print(f'Time elapsed: {(time.time() - start_time) / 60:.2f} min')
print(f'Total Training Time: {(time.time() - start_time) / 60:.2f} min')
print(f'Test accuracy: {compute_binary_accuracy(model, test_loader, DEVICE):.2f}%')
nlp = spacy.load('en_core_web_sm')
ret = predict_sentiment(model, "I really love this movie. This movie is so great!")
print("ret=", ret)
输出:
Num Train: 20000
Num Valid: 5000
Num Test: 25000
train_data[0:200] ['Based', 'on', 'an', 'actual', 'story', ',', 'John', 'Boorman', 'shows', 'the', 'struggle', 'of', 'an', 'American', 'doctor', ',', 'whose', 'husband', 'and', 'son', 'were', 'murdered', 'and', 'she', 'was', 'continually', 'plagued', 'with', 'her', 'loss', '.', 'A', 'holiday', 'to', 'Burma', 'with', 'her', 'sister', 'seemed', 'like', 'a', 'good', 'idea', 'to', 'get', 'away', 'from', 'it', 'all', ',', 'but', 'when', 'her', 'passport', 'was', 'stolen', 'in', 'Rangoon', ',', 'she', 'could', 'not', 'leave', 'the', 'country', 'with', 'her', 'sister', ',', 'and', 'was', 'forced', 'to', 'stay', 'back', 'until', 'she', 'could', 'get', 'I.D.', 'papers', 'from', 'the', 'American', 'embassy', '.', 'To', 'fill', 'in', 'a', 'day', 'before', 'she', 'could', 'fly', 'out', ',', 'she', 'took', 'a']
Vocabulary size: 20002
Number of classes: 2
Epoch: 001/004 | Batch 000/313 | Cost: 0.7078
Epoch: 001/004 | Batch 050/313 | Cost: 0.6911
Epoch: 001/004 | Batch 100/313 | Cost: 0.6901
Epoch: 001/004 | Batch 150/313 | Cost: 0.6965
Epoch: 001/004 | Batch 200/313 | Cost: 0.6274
Epoch: 001/004 | Batch 250/313 | Cost: 0.6855
Epoch: 001/004 | Batch 300/313 | Cost: 0.6413
training accuracy: 66.27%
valid accuracy: 65.26%
Time elapsed: 6.34 min
Epoch: 002/004 | Batch 000/313 | Cost: 0.6546
Epoch: 002/004 | Batch 050/313 | Cost: 0.6024
Epoch: 002/004 | Batch 100/313 | Cost: 0.6676
Epoch: 002/004 | Batch 150/313 | Cost: 0.6437
Epoch: 002/004 | Batch 200/313 | Cost: 0.6236
Epoch: 002/004 | Batch 250/313 | Cost: 0.6862
Epoch: 002/004 | Batch 300/313 | Cost: 0.5634
training accuracy: 54.29%
valid accuracy: 52.32%
Time elapsed: 12.72 min
Epoch: 003/004 | Batch 000/313 | Cost: 0.6892
Epoch: 003/004 | Batch 050/313 | Cost: 0.6420
Epoch: 003/004 | Batch 100/313 | Cost: 0.6250
Epoch: 003/004 | Batch 150/313 | Cost: 0.6815
Epoch: 003/004 | Batch 200/313 | Cost: 0.5970
Epoch: 003/004 | Batch 250/313 | Cost: 0.6502
Epoch: 003/004 | Batch 300/313 | Cost: 0.5945
training accuracy: 68.32%
valid accuracy: 61.98%
Time elapsed: 19.41 min
Epoch: 004/004 | Batch 000/313 | Cost: 0.5901
Epoch: 004/004 | Batch 050/313 | Cost: 0.3887
Epoch: 004/004 | Batch 100/313 | Cost: 0.6483
Epoch: 004/004 | Batch 150/313 | Cost: 0.5912
Epoch: 004/004 | Batch 200/313 | Cost: 0.5973
Epoch: 004/004 | Batch 250/313 | Cost: 0.4288
Epoch: 004/004 | Batch 300/313 | Cost: 0.4574
training accuracy: 75.49%
valid accuracy: 65.98%
Time elapsed: 25.52 min
Total Training Time: 25.52 min
Test accuracy: 65.84%
ret= 0.9203115105628967
0.9大于0.5 代表是积极的观点,但是测试集上准确率只有65.84%。
4.7 定义LSTM模型
class LSTM(nn.Module):
def __init__(self, input_dim, embedding_dim, hidden_dim, output_dim):
super().__init__()
self.embedding = nn.Embedding(input_dim, embedding_dim)
self.lstm = nn.LSTM(embedding_dim, hidden_dim)
self.fc = nn.Linear(hidden_dim, output_dim)
def forward(self, text, text_length):
embedded = self.embedding(text)
packed = torch.nn.utils.rnn.pack_padded_sequence(embedded, text_length)
packed_output, (hidden, cell) = self.lstm(packed)
return self.fc(hidden.unsqueeze(0)).view(-1)
4.8 LSTM 模型训练和评估
model2 = LSTM(INPUT_DIM, EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM).to(DEVICE)
optimizer2 = torch.optim.Adam(model2.parameters(), lr=LEARNING_RATE)
start_time = time.time()
for epoch in range(NUM_EPOCHS):
model2.train()
for batch_idx, batch_data in enumerate(train_loader):
text, text_lengths = batch_data.text
logits = model2(text, text_lengths)
cost2 = F.binary_cross_entropy_with_logits(logits, batch_data.label)
optimizer2.zero_grad()
cost2.backward()
optimizer2.step()
if not batch_idx % 50:
print (f'Epoch: {epoch+1:03d}/{NUM_EPOCHS:03d} | '
f'Batch {batch_idx:03d}/{len(train_loader):03d} | '
f'Cost: {cost2:.4f}')
with torch.set_grad_enabled(False):
print(f'training accuracy: '
f'{compute_binary_accuracy(model2, train_loader, DEVICE):.2f}%'
f'\nvalid accuracy: '
f'{compute_binary_accuracy(model2, valid_loader, DEVICE):.2f}%')
print(f'Time elapsed: {(time.time() - start_time)/60:.2f} min')
print(f'Total Training Time: {(time.time() - start_time)/60:.2f} min')
print(f'Test accuracy: {compute_binary_accuracy(model2, test_loader, DEVICE):.2f}%')
输出:
Num Train: 20000
Num Valid: 5000
Num Test: 25000
train_data[0:200] ['Based', 'on', 'an', 'actual', 'story', ',', 'John', 'Boorman', 'shows', 'the', 'struggle', 'of', 'an', 'American', 'doctor', ',', 'whose', 'husband', 'and', 'son', 'were', 'murdered', 'and', 'she', 'was', 'continually', 'plagued', 'with', 'her', 'loss', '.', 'A', 'holiday', 'to', 'Burma', 'with', 'her', 'sister', 'seemed', 'like', 'a', 'good', 'idea', 'to', 'get', 'away', 'from', 'it', 'all', ',', 'but', 'when', 'her', 'passport', 'was', 'stolen', 'in', 'Rangoon', ',', 'she', 'could', 'not', 'leave', 'the', 'country', 'with', 'her', 'sister', ',', 'and', 'was', 'forced', 'to', 'stay', 'back', 'until', 'she', 'could', 'get', 'I.D.', 'papers', 'from', 'the', 'American', 'embassy', '.', 'To', 'fill', 'in', 'a', 'day', 'before', 'she', 'could', 'fly', 'out', ',', 'she', 'took', 'a']
Vocabulary size: 20002
Number of classes: 2
Epoch: 001/010 | Batch 000/313 | Cost: 0.6930
Epoch: 001/010 | Batch 050/313 | Cost: 0.6436
Epoch: 001/010 | Batch 100/313 | Cost: 0.6402
Epoch: 001/010 | Batch 150/313 | Cost: 0.5405
Epoch: 001/010 | Batch 200/313 | Cost: 0.6803
Epoch: 001/010 | Batch 250/313 | Cost: 0.6905
Epoch: 001/010 | Batch 300/313 | Cost: 0.6695
training accuracy: 56.28%
valid accuracy: 56.62%
Time elapsed: 73.09 min
Epoch: 002/010 | Batch 000/313 | Cost: 0.6772
Epoch: 002/010 | Batch 050/313 | Cost: 0.6866
Epoch: 002/010 | Batch 100/313 | Cost: 0.6674
Epoch: 002/010 | Batch 150/313 | Cost: 0.6037
Epoch: 002/010 | Batch 200/313 | Cost: 0.6808
Epoch: 002/010 | Batch 250/313 | Cost: 0.6685
Epoch: 002/010 | Batch 300/313 | Cost: 0.6927
training accuracy: 50.33%
valid accuracy: 50.40%
Time elapsed: 148.10 min
Epoch: 003/010 | Batch 000/313 | Cost: 0.7443
Epoch: 003/010 | Batch 050/313 | Cost: 0.6509
Epoch: 003/010 | Batch 100/313 | Cost: 0.6160
Epoch: 003/010 | Batch 150/313 | Cost: 0.6501
Epoch: 003/010 | Batch 200/313 | Cost: 0.5341
Epoch: 003/010 | Batch 250/313 | Cost: 0.4378
Epoch: 003/010 | Batch 300/313 | Cost: 0.4366
training accuracy: 84.29%
valid accuracy: 81.76%
Time elapsed: 218.03 min
Epoch: 004/010 | Batch 000/313 | Cost: 0.3864
Epoch: 004/010 | Batch 050/313 | Cost: 0.2678
Epoch: 004/010 | Batch 100/313 | Cost: 0.2225
Epoch: 004/010 | Batch 150/313 | Cost: 0.3614
Epoch: 004/010 | Batch 200/313 | Cost: 0.2415
Epoch: 004/010 | Batch 250/313 | Cost: 0.1816
Epoch: 004/010 | Batch 300/313 | Cost: 0.2577
training accuracy: 91.74%
valid accuracy: 86.48%
Time elapsed: 285.13 min
Test accuracy: 90.72%
ret= 0.9601113557815552