1、梯度计算过程以及Adam优化器细节
梯度计算
概述:梯度计算本质上就是对损失函数进行求导的结果,该结果就是本次机器学习的梯度。
Adam优化器实现细节
概述:优化器的作用是帮助权重进行更新,根据上面求解的梯度加上学习率进行权重的更新;最简单的优化器有SGD:权重=权重-学习
率*梯度,Adam优化器的更加复杂,更新效果更好的优化器,下面介绍其基本原理。
针对上面的原理解析用代码进行实现:
import torch
import torch.nn as nn
import numpy as np
# 通过numpy实现整个求导、计算梯度、更新权重的整个过程,并比较torch的结果
# 通过pytorch定义一个模型框架,
class ord_model(nn.Module):
def __init__(self, input_size, output_size):
super(ord_model, self).__init__()
self.linear = nn.Linear(input_size, output_size, bias=False)
self.activate = nn.Sigmoid()
self.loss = nn.MSELoss()
def forward(self, x, y=None):
x = self.linear(x)
pred = self.activate(x)
return self.loss(pred, y)
# 自定义模型,并手动实现其方法
class diy_model:
def __init__(self, weight, input_x, input_y):
self.weight = weight
self.input_x = input_x
self.input_y = input_y
# 线性层
def diy_linear(self):
weight = self.weight.detach().numpy()
return np.dot(self.input_x, np.transpose(weight))
#激活函数
def diy_sigmoid(self, x):
return 1/(1+np.exp(-x))
# 损失函数
def mes_loss(self, y_pred, y_true):
return (y_pred-y_true)**2
# 梯度计算
def diy_grad(self, y_pred, y, x):
grad = np.array([[0.0, 0.0], [0.0, 0.0]])
grad[0][0] = (y_pred[0]-y[0]) * y_pred[0]*(1-y_pred[0]) * x[0]
grad[0][1] = (y_pred[1]-y[1]) * y_pred[1]*(1-y_pred[1]) * x[0]
grad[1][0] = (y_pred[0]-y[0]) * y_pred[0]*(1-y_pred[0]) * x[1]
grad[1][1] = (y_pred[1]-y[1]) * y_pred[1]*(1-y_pred[1]) * x[1]
return np.transpose(grad)
# 更新权重
def update_grad(self, grad):
self.weight[0][0] = self.weight[0][0] - 1e-3 * grad[0][0]
self.weight[0][1] = self.weight[0][1] - 1e-3 * grad[0][1]
self.weight[1][0] = self.weight[1][0] - 1e-3 * grad[1][0]
self.weight[1][1] = self.weight[1][1] - 1e-3 * grad[1][1]
# 构建Adam优化器
def diy_adam(self, grad):
lr = 1e-3
beta1 = 0.9
beta2 = 0.999
eps = 1e-8
m = 0
v = 0
t = 0
t += 1
m = beta1*m + (1-beta1)*grad
v = beta2*v + (1-beta2)*grad**2
m_hat = m/(1-beta1**t)
v_hat = v/(1-beta2**t)
self.weight = torch.FloatTensor(self.weight)-lr*m_hat/(torch.sqrt(v_hat)+eps)
return self.weight
def forward(self):
self.x = self.diy_linear()
y_pred = self.diy_sigmoid(self.x)
loss = np.mean(self.mes_loss(y_pred, self.input_y))
return loss, self.diy_grad(y_pred, self.input_y, self.input_x)
if __name__ == "__main__":
x = np.array([1, 2])
y = np.array([4, 1])
model = ord_model(2, 2) #定义模型
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3) #定义SGD优化器
optimizer_adam = torch.optim.Adam(model.parameters(), lr=1e-3) # 定义Adam优化器
weight = model.state_dict()['linear.weight']
print("======================框架初始化权重======================")
for name, param in model.named_parameters():
print(f"name:{name}, weight:{param}")
# 创建自定义模型
diy_model = diy_model(weight.tolist(), x, y)
optimizer_adam.zero_grad() # 更新梯度
loss = model(torch.FloatTensor(x), torch.FloatTensor(y))
loss.backward() # 计算梯度
grads = {} # 存放模型产生的梯度
for name, param in model.named_parameters():
if param.grad is not None:
grads[name] = param.grad
print("======================Adam更新权重======================")
print(diy_model.diy_adam(grads["linear.weight"]))
optimizer_adam.step() # 权重更新
print("======================框架更新后权重======================")
for name, param in model.named_parameters():
print(f"name:{name}, weight:{param}")
diy_loss, diy_grad = diy_model.forward()
print("======================比较计算的loss值======================")
print(f"pytorch框计算的损失结果:{loss}")
print(f"手动实现结果的损失结果 :{diy_loss}")
print("======================MSE计算梯度======================")
print(diy_grad)
print(model.linear.weight.grad)
print("======================MSE更新权重======================")
diy_model.update_grad(diy_grad)
print(diy_model.weight)
比对其结果:
学习建议:对于学习到的大模型训练相关组件的知识可以通过python代码自己实现一遍,然后再用框架调取处理一样的数据,检查比对最后的结果看是否和框架的结果一致,若相同则证明自己的学习掌握的没有问题。
2、Embedding层实现细节
说明:Embedding层的主要作用就是将文本字符转换为向量,也就是将离散的数据转换为向量
import torch
import torch.nn as nn
if __name__ == "__main__":
embed = nn.Embedding(10, 4, padding_idx=0)
input = torch.tensor([[0, 2, 3, 4], [5, 6, 7, 8]])
print("=====================Embedding初始化参数=====================")
print(embed.weight)
print("=====================Embedding层转换结果=====================")
output = embed(input)
print(output)
3、Pooling层实现细节
说明:Pooling层的主要作用是将减小输入数据的大小,减轻模型训练的成本(在不影响模型训练效果的前提下)
import torch
import torch.nn as nn
if __name__ == "__main__":
embedding = nn.Embedding(10, 4, padding_idx=0)
pooling = nn.MaxPool2d((4, 1), stride=(1, 1))
pooling2 = nn.MaxPool1d(2, stride=1)
pooling1 = nn.AvgPool2d((4, 1), stride=(1, 1))
pooling3 = nn.AvgPool1d(2, stride=2)
input = torch.tensor([0, 1, 2, 3])
output = embedding(input)
print("=====================池化前结果=====================")
print(output)
print("=====================二维最大池化后结果=====================")
output = output.unsqueeze(0).unsqueeze(0)
output_pooling = pooling(output)
print(output_pooling.squeeze())
print("=====================一维最大池化后结果=====================")
output_1d = pooling2(output.squeeze(0))
print(output_1d)
print("=====================二维平均池化后结果=====================")
output_pooling1 = pooling1(output)
print(output_pooling1.squeeze())
print("=====================一维平均池化后结果=====================")
output = output.squeeze(0)
output_pooling3 = pooling3(output)
print(output_pooling3)
4、RNN
说明:将整个序列划分为多个时间步,每个时间步依次传入模型中,同时将输出结果传入下一个时间步。多用于文本自然语言处理当
中。
import torch
import torch.nn as nn
if __name__ == "__main__":
rnn = nn.RNN(5, 6, num_layers=2,batch_first=True)
# tarh(x*w1+b1+h*w2+b2)
print(rnn.weight_ih_l0.shape) # 6*5
print(rnn.bias_ih_l0.shape) # 1*6
print(rnn.weight_hh_l0.shape) # 6*6
print(rnn.bias_hh_l0.shape) # 1*6
input = torch.randn(1, 4, 5)
print(input)
output, h0 = rnn(input)
print(output.shape)
print(h0.shape)
其中需要中点注意的参数是batch_first,该参数的作用是控制数据排列的顺序,若为True则会将批次大小放在第一个维度,若为False则会 将时间步数(即序列中元素的数量)放在第一个维度,总体来讲就是输出更加符合一些数据的加载方式(例如批处理模式)。(大部分任务一般会置为True) 在框架中RNN最终会输出两个结果,其中output是含有每个时间步对应的计算结果;h0含有的是每一层RNN最终输出的结果,其中h0[-1]可以形象的代表为所输入一句话在当前模型结构的浓缩向量表示(类似于映射)。
5、CNN
说明:卷积神经网络的工作原理就是通过初始化的卷积核(一个小型矩阵),然后对输入的目标矩阵中每个卷积核大小的区域与卷积 核做加权求和,同时卷积核就按照指定步长进行滑动,最终得到输入目标的特征矩阵,对这些特征矩阵会将其放入到后面的多个全连 接层进行训练,进而得到可以对输入进行识别的模型。
对一维的卷积神经网络Conv1d()进行重要参数的相关解析:
- in_channels:输入的通道数,主要作用确定表示输入数据的维度
- out_channels:输出的通道数,代表有多少个卷积核,其中这所有的卷积核会对每一个通道中的进行卷积,进而每个通道都会产生卷 积核数量的特征图
- kernel_size:卷积核的大小,一般一维的就是一个数,二维的就是一个(m,n)【处理黑白图片】,三维的卷积神经网络是(x,y,z)【处理 彩图】
- stride:卷积核滑动的步长
卷积神经网络输出数据的形状为:
(batch_size,out_channels,(input_size-kernel_size+2*padding)/stride+1)
6、Normalization(数据归一化)
说明:归一化层的作用主要是对上层输出的数据做标准化,使得模型的训练速度加快、减轻梯度爆炸/梯度消失、防止过拟合;其中优又
会细分为 Batch Normalization、 Layear Normalization、 Instance Normalization,其中第一个主要用于图像领域,第二个
主要用于文本处理领域,最后一个主要用于图像处理中每个通道中元素的归一化。
Normalization实现细节:
import torch
import torch.nn as nn
if __name__ == "__main__":
bn = nn.BatchNorm1d(3)
input = torch.randn(5, 3)
print(input)
output = bn(input)
print(output)
相较于上面的 Batch Noramlization同一批次元素公用一个均值和方差, Layer Normalization则是对每个单独的元素序列都有一 个均值方差,形象的理解为 Batch Normalization是对整个班级的成绩做平均标准化,而 Layer Normalization则是对每个同学的 成绩做平均标准化。
7、DropOut层
说明:Dropout层的作用就是减少过拟合的情况,对输入元素进行一定概率p的丢弃然后对未丢弃的元素进行等比例的放大为元素乘以 1/(1-p)。
import torch
import torch.nn as nn
if __name__ == "__main__":
m = nn.Dropout(p=0.6)
# m.eval()
input = torch.FloatTensor([1, 2, 3, 4, 5, 6])
print(input)
output = m(input)
print(output)
8、实例模型训练代码实现
用上述pytorch相关组件完成下面的任务,
实现任务:输入一个5-6位的英文字符串,其中含有a的被分为第一类(0),含有b的被分类第二类(1),含有c的被分为第三类(2),上面是一个顺序优先级,第一类优先级最高,第二类次之,第三类最低。
import random
import torch
import torch.nn as nn
import string
import json
import numpy as np
import matplotlib.pyplot as plt
# 实现任务:输入一个5-6位的英文字符串,其中含有a的被分为第一类,含有b的被分类第二类,含有c的被分为第三类
# 构建训练数据
def structure_data(train_number):
train_input = []
train_output = []
for i in range(train_number):
length = random.choice([5, 6])
need_char = random.choice('abc')
surplus_string_list = random.choices(string.ascii_letters, k=length-1)
random_string_list = surplus_string_list+[need_char]
random.shuffle(random_string_list)
random_string = ''.join(random_string_list)
if 'a' in random_string:
train_input.append(random_string)
train_output.append(0)
elif 'b' in random_string:
train_input.append(random_string)
train_output.append(1)
elif 'c' in random_string:
train_input.append(random_string)
train_output.append(2)
return train_input, torch.LongTensor(train_output)
# 将训练数据转换为对应词表坐标
def convert_str(train_input, vocab):
result = []
for str in train_input:
str_array = list(str)
for i in range(len(str_array)):
str_array[i] = vocab.index(str_array[i])+1
if len(str_array) != 6:
str_array.append(0)
result.append(str_array)
return torch.LongTensor(result)
# 定义网络结构
class RNN_Model(nn.Module):
def __init__(self, vocab_size, embedding_size):
super(RNN_Model, self).__init__()
self.embedding = nn.Embedding(vocab_size+1, embedding_size, padding_idx=0)
self.rnn = nn.RNN(10, 20, num_layers=2, batch_first=True)
self.linear = nn.Linear(6*20, 3)
self.loss = nn.CrossEntropyLoss()
def forward(self, x, y=None):
x = self.embedding(x)
x, _ = self.rnn(x)
x = x.reshape(x.shape[0], -1)
x = self.linear(x)
if y is not None:
return self.loss(x, y)
else:
return x
# 校验模型的准确率
def verify_model(model, vocab):
verify_input_data, verify_output_data = structure_data(100)
verify_input_data = convert_str(verify_input_data, vocab)
correct, worry = 0, 0
model.eval()
with torch.no_grad():
output = model(verify_input_data)
for y_true, y_pred in zip(verify_output_data, output):
if np.argmax(y_pred) == y_true:
correct += 1
else:
worry += 1
return correct/(correct+worry)
# 训练模型
def train_model(model, vocab):
# 定义外部参数
epochs = 1000 # 模型训练轮次
batch_size = 64 # 批次大小
train_data_number = 6400 # 训练数据量大小
learning_rate = 1e-3 # 学习率
# 定义Adam优化器
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
# 准备训练数据
train_input_data, train_output_data = structure_data(train_data_number)
train_input_data = convert_str(train_input_data, vocab)
# 存放训练数据日志
log = []
# 训练模型
for epoch_number in range(epochs):
model.train()
loss_value = []
for i in range(len(train_input_data)//batch_size):
x = train_input_data[i*batch_size:(i+1)*batch_size]
y = train_output_data[i*batch_size:(i+1)*batch_size]
loss = model(x, y) # 计算损失值
loss.backward() # 计算梯度
optimizer.step() # 梯度更新
optimizer.zero_grad() # 梯度置零
loss_value.append(loss.item())
# 计算每轮损失均值以及当前模型的准确率放入日志中
loss_avg = np.mean(loss_value)
acc = verify_model(model, vocab)
log.append([loss_avg, acc])
print(f'第{epoch_number}轮训练完成!')
# 判断损失值是否达到阈值
if loss_avg <= 1e-5:
print('模型符合要求,结束训练过程!')
break
# 保存模型
torch.save(model.state_dict(), './model/rnn_class_model.bin')
# 可视化展示损失值以及模型准确率的变化趋势
plt.plot(list(range(len(log))), [loss[0] for loss in log], label='loss', color='red')
plt.plot(list(range(len(log))), [acc[1] for acc in log], label='acc', color='blue')
plt.legend()
plt.title('Change Trend')
plt.show()
# 调用模型进行预测
def prediction(model_path, vocab, predict_data):
model = RNN_Model(len(vocab), 10)
model.load_state_dict(torch.load(model_path, weights_only=True))
data = convert_str(predict_data, vocab)
model.eval()
with torch.no_grad():
output = model(data)
for i, j in zip(predict_data, output):
print(f'字符串{i},对应类别{np.argmax(j)}')
if __name__ == "__main__":
x, y = structure_data(5)
with open('./vocab/vocab.json', 'r') as f:
vocab = json.load(f)
model = RNN_Model(len(vocab), 10)
# 训练模型
# train_model(model, vocab)
# 运用模型做预测
prediction('./model/rnn_class_model.bin', vocab, x)
模型训练效果:
实际使用效果: