一个项目的基本组成框架
基本步骤:Load Data , Training , Validation ,Testing
Dataset
数据内容
HW01的数据读入
class COVID19Dataset(Dataset):
'''
x: Features.
y: Targets, if none, do prediction.
'''
def __init__(self, x, y=None):
if y is None:
self.y = y
else:
self.y = torch.FloatTensor(y)
self.x = torch.FloatTensor(x)
def __getitem__(self, idx):
if self.y is None:
return self.x[idx]
else:
return self.x[idx], self.y[idx]
def __len__(self):
return len(self.x)
DKT模型中的数据读入部分
import csv
import random
import sys
def load_data(fileName): # 提示参数都要小写
rows = []
max_skill_num = 0
max_num_problems = 0
# 将数据集存储到列表中
with open(fileName, "r") as csvfile:
reader = csv.reader(csvfile, delimiter=',')
for row in reader:
rows.append(row)
#rows = [['3'], ['4', '5', '6'], ['1', '0', '1'],['2'],['14','12'],['0','0']]
#rows中的每一个元素就是原来的每一行数据
index = 0
print("the number of rows is " + str(len(rows)), file=sys.stderr)
tuple_rows = []
# turn list to tuple 每3个row作为1个tuple
while index < len(rows)-1:
problems_num = int(rows[index][0])
# 找出每个tuple第2行的最大数值作为最大技能编号
tmp_max_skill = max(map(int, rows[index+1]))
#直接用max函数提取第2行的最大技能编号
if tmp_max_skill > max_skill_num:
max_skill_num = tmp_max_skill
# 去除问题回答数量小于2的学生数据
if problems_num <= 2:
index += 3
else:
# 找出所有学生回答问题数量的最大值
if problems_num > max_num_problems:
max_num_problems = problems_num
# tup即每个学生的三元组
tup = (rows[index], rows[index+1], rows[index+2])
# tuple_rows即所有学生的三元组
tuple_rows.append(tup)
index += 3
# shuffle the tuple
random.shuffle(tuple_rows) # 打乱原元祖的顺序
# 也就是输出过滤后的学生数量
print("The number of students is ", len(tuple_rows), file=sys.stderr)
print("Finish reading data", file=sys.stderr)
# 技能编号从0开始 故返回技能个数需要+1
return tuple_rows, max_num_problems, max_skill_num+1
#返回整理后的数据,最大问题数量和最大技能编号
#tuple_rows[ [['3'],['12','14','9'],['1','0','0']], [['2'],['6','7'],['1','0']], [['4'],['13','2','25','35'],['0','1','1','0']] ]
Dataloader会呼叫dataset(batch_size)次,得到(batch_size)笔资料后,将这些资料汇总成一个batch,再去做后续的训练
def run_epoch(m, optimizer, students, batch_size, num_steps, num_skills, training=True, epoch=1)
#m: 表示一个模型.
#optimizer: 表示优化器,用于更新模型的参数以最小化损失函数。优化器采用模型的梯度信息来进行参数更新,常见的优化器包括随机梯度下降(SGD)、Adam、Adagrad等。
#students: 表示学生(或学习者)的数据集。这个参数可能是一个包含学生数据的列表、数组或其他数据结构。
#batch_size: 表示批量大小,用于确定每个训练批次中包含的样本数量。批量大小影响模型参数的更新频率和内存消耗。
#num_steps: 表示每个训练样本的序列长度或步数。在序列数据(如文本、时间序列等)的训练中,每个样本可以被划分为多个步骤或时间步骤。num_steps决定了每个样本被划分的步骤数量。
#num_skills: 表示技能数量,表示学生的技能或知识点的总数。这个参数可能用于确定模型输出的分类数量或用于其他与技能相关的计算。
#training: 表示是否在训练模式下。如果为True,表示当前是训练阶段,模型将进行参数更新;如果为False,表示当前是评估或测试阶段,模型只做前向传播计算。
#epoch: 表示当前的训练周期或迭代次数。一个训练周期表示模型对整个训练数据集进行一次完整的训练。
Tensor是参数的常见形式,是多维的数据,.shape()函数可以判断参数的维度
创建张量
Pytorch支持tensor的基本运算,x=x.transpose(0,1)#互换x张量的第0个维度和第1个维度
data:要用于创建张量的数据。可以是Python列表、NumPy数组、标量值或其他支持的数据类型。
dtype(可选):指定张量的数据类型。默认情况下,PyTorch会根据输入数据自动确定数据类型。常见的数据类型包括torch.float32(默认的浮点型)、torch.int64(默认的整型)等。可以参考官方文档以获取完整的数据类型列表。
device(可选):指定张量所在的设备。可以是’cpu’表示CPU、’cuda’表示CUDA设备,也可以是特定的设备对象。默认情况下,张量将被放置在CPU上。
requires_grad(可选):指定是否计算张量的梯度。默认为False,表示不计算梯度。当需要进行自动微分和反向传播时,可以将其设置为True,以便在张量上跟踪梯度信息。
pin_memory(可选):指定是否将张量数据固定在内存中,以便更快地从CPU上传输到GPU。默认为False。这对于使用GPU加速训练的情况下,处理大型数据集时可能会提高性能。
张量运算是pytorch处理数据的基本方式
def run_epoch(m, optimizer, students, batch_size, num_steps, num_skills, training=True, epoch=1):
"""Runs the model on the given data."""
# lr = args.learning_rate # learning rate
total_loss = 0
input_size = num_skills * 2
# start_time = time.time()
index = 0
actual_labels = [] # 真实值
pred_labels = [] # 预测值
hidden = m.init_hidden(batch_size)
count = 0
# 计算需要进行多少次batch
batch_num = len(students) // batch_size
# 最后一组的学生数量可能不到batch_size的大小,用小于号防止越界
all_loss, all_auc, all_acc, all_rmse, all_r2 = [], [], [], [], []
while index+batch_size < len(students):
x = np.zeros((batch_size, num_steps))
# 比如32*128,初始化预测用的数组,x是一个二维张量,batch_size是一个初始化的量,表示的是一个batch中的学生数量,而num_steps是学生所能回答的最多的问题数量,num_skills是学生回答的最大问题编号
target_id: List[int] = []
target_correctness = []
# 对每一组batch进行操作
for i in range(batch_size): #它一次循环32个元组中的基本元素,即32个学生
# 可以看作是锁定每个3元组
# students[ [['3'],['12','14','9'],['1','0','0']], [['2'],['6','7'],['1','0']], [['4'],['13','2','25','35'],['0','1','1','0']] ]
student = students[index+i]
# 每个3元组的第二行-回答的技能编号
problem_ids = student[1]#这里的problem_ids是包含了该学生写过的所有题目编号的数组
# 每个3元组的第三行-回答的正误
correctness = student[2]#这里的correctness也是数组
# a = torch.range(1, 6) a的tensor中是存在6的,也就是右区间是闭的
# 每一个学生
for j in range(len(problem_ids)-1):#这里的j就是学生i回答的第j道题目
problem_id = int(problem_ids[j])
# label_index = 0
if int(correctness[j]) == 0:#错题
label_index = problem_id
#这个problem_index是错题的编号
else:
label_index = problem_id + num_skills
#正确的话题目下标会加上一个偏移值,
#input_size维度是num_skills*2的原因,为什么一定要用这种加上偏移值的方式来区别题目的对错呢,1/0不就可以表示对错了吗?可能是因为把值设为0的话,张量的某些维度是初始为0的,这些维度根本没有训练过题目,如果说通过加上偏移量来表示答题情况的话,1所在的位置一定是训练过的。
x[i, j] = label_index #把x第i行,第j列的值设置成label_index
target_id.append(i*num_steps*num_skills+j*num_skills+int(problem_ids[j+1]))
#对于第i位同学写的第j道题生成一个地址
target_correctness.append(int(correctness[j+1]))#这里是获得下一个问题的回答情况
actual_labels.append(int(correctness[j+1]))
# index指向下一组batch
index += batch_size #步长为32
count += 1
# 将得到的这个batch中学生的所有所答题目以及对这些题目的回答情况做一个张量的转化
target_id = torch.tensor(target_id, dtype=torch.int64)
target_correctness = torch.tensor(target_correctness, dtype=torch.float)
# One Hot encoding input data [batch_size, num_steps, input_size]
# x代表每个学生回答每个问题的正误情况
x = torch.tensor(x, dtype=torch.int64)
# 对于每一个学生回答的
# 把二维数组转化成张量形式
# unsqueeze:扩充数据维度,在0起的指定位置N加上维数为一的维度,这里的x本来是两维,现在变为3维
x = torch.unsqueeze(x, 2)
input_data = torch.FloatTensor(batch_size, num_steps, input_size)
# 创建一个每一维度分别是batch_size,num_steps,input_size的三维张量
input_data.zero_()
# 在input_data的第三维找到x中所记录的位置,并将之修改为1
input_data.scatter_(2, x, 1)
#input_data前两维和x一样,最后一维是前两维下标(第i个学生做的第j道题目的编号(正确的话是编号+偏移量,错误的话就是编号本身))
#input_data[i][j][x[i][j][k]]=1
#可以从这里的one-hot编码值中信息是:是i同学做的第j道题目,这道题目的编号可以从x[i][j][k]得知,还可以知道此题有没有写对
HW01中对数据的处理
def select_feat(train_data, valid_data, test_data, select_all=True):
'''Selects useful features to perform regression'''
y_train, y_valid = train_data[:,-1], valid_data[:,-1]
raw_x_train, raw_x_valid, raw_x_test = train_data[:,:-1], valid_data[:,:-1], test_data
if select_all:
feat_idx = list(range(raw_x_train.shape[1]))
else:
feat_idx = [0,1,2,3,4] # TODO: Select suitable feature columns.
return raw_x_train[:,feat_idx], raw_x_valid[:,feat_idx], raw_x_test[:,feat_idx], y_train, y_valid
same_seed(config['seed'])
# train_data size: 2699 x 118 (id + 37 states + 16 features x 5 days)
# test_data size: 1078 x 117 (without last day's positive rate)
train_data, test_data = pd.read_csv('./covid.train.csv').values, pd.read_csv('./covid.test.csv').values
train_data, valid_data = train_valid_split(train_data, config['valid_ratio'], config['seed'])
# Print out the data size.
print(f"""train_data size: {train_data.shape}
valid_data size: {valid_data.shape}
test_data size: {test_data.shape}""")
# Select features
x_train, x_valid, x_test, y_train, y_valid = select_feat(train_data, valid_data, test_data, config['select_all'])
# Print out the number of features.
print(f'number of features: {x_train.shape[1]}')
train_dataset, valid_dataset, test_dataset = COVID19Dataset(x_train, y_train), \
COVID19Dataset(x_valid, y_valid), \
COVID19Dataset(x_test)
# Pytorch data loader loads pytorch dataset into batches.
train_loader = DataLoader(train_dataset, batch_size=config['batch_size'], shuffle=True, pin_memory=True)
valid_loader = DataLoader(valid_dataset, batch_size=config['batch_size'], shuffle=True, pin_memory=True)
test_loader = DataLoader(test_dataset, batch_size=config['batch_size'], shuffle=False, pin_memory=True)
训练神经网络的三个步骤:Define Neural Network ,Loss Function , Optimization Algorithm
x是输入,y是输出
HW01的神经网络模型
class My_Model(nn.Module):
def __init__(self, input_dim):
super(My_Model, self).__init__()
# TODO: modify model's structure, be aware of dimensions.
self.layers = nn.Sequential(
#定义了四个linear model
nn.Linear(input_dim, 64),
nn.ReLU(),
nn.Linear(64, 16),
nn.ReLU(),
nn.Linear(16, 4),
nn.ReLU(),
nn.Linear(4, 1),
)
def forward(self, x):
x = self.layers(x) # 将x放到上文定义好的model中去训练
x = x.squeeze(1) # (B, 1) -> (B)
return x
3层网络
4层网络
128的batch_size
DKT的神经网络模型
import torch.nn as nn
class DeepKnowledgeTracing(nn.Module):
"""Deep Knowledge tracing model"""
# 构造方法 tie_weights是参数共享的意思
def __init__(self, rnn_type, input_size, hidden_size, num_skills, nlayers, dropout=0.6, tie_weights=False):
# 重构8个字典属性
#self的基本成员变量是rnn_type,nhid,nlayers,其他的是自定义初始化的参数
super(DeepKnowledgeTracing, self).__init__()
self.decoder = nn.Linear(hidden_size, num_skills)
# 创建一个线性层,将输入的特征维度hidden_size映射到输出的特征维度num_skills,hidden_size是RNN的隐藏状态维度,num_skills是最终输出的特征维度
self.init_weights()
self.rnn_type = rnn_type
self.nhid = hidden_size
self.nlayers = nlayers
def forward(self, inputs, hidden):
# 对每一个batch做运算
output, hidden = self.rnn(inputs, hidden)
# output维度(seq_size, batch_size, hidden_size)
# output的最后一维是hidden_size,经过nn.Linear的计算可以将decoded最后一个维度变成num_skills
decoded = self.decoder(output.contiguous().view(output.size(0) * output.size(1), output.size(2)))
#最终返回的张量的最后一个维度是num_skills
return decoded, hidden
# 这里只初始化了每个cell中的h和c
def init_hidden(self, bsz):
# 每一隐层参数
weight = next(self.parameters())
# 如果是LSTM的话还需要c
if self.rnn_type == 'LSTM':
return (weight.new_zeros(self.nlayers, bsz, self.nhid),
weight.new_zeros(self.nlayers, bsz, self.nhid))
else:
return weight.new_zeros(self.nlayers, bsz, self.nhid)
# 这里只初始化了线性层decoder的参数
def init_weights(self):
initrange = 0.05
self.decoder.bias.data.zero_()
self.decoder.weight.data.uniform_(-initrange, initrange)
常见的计算loss的函数
criterion = nn.BCEWithLogitsLoss()
loss = criterion(logits, target_correctness)
loss.backward()
# clip_grad_norm` helps prevent the exploding gradient problem in RNNs / LSTMs.
# max_norm越小,裁剪的梯度越大,得到的梯度就越小,防止梯度爆炸的效果越明显
torch.nn.utils.clip_grad_norm_(m.parameters(), args.max_grad_norm)
optimizer.step()
训练模块的实际代码结构
HW01
def trainer(train_loader, valid_loader, model, config, device):
criterion = nn.MSELoss(reduction='mean') # Define your loss function, do not modify this.
# Define your optimization algorithm.
# TODO: Please check https://pytorch.org/docs/stable/optim.html to get more available algorithms.
# TODO: L2 regularization (optimizer(weight decay...) or implement by your self).
optimizer = torch.optim.SGD(model.parameters(), lr=config['learning_rate'], momentum=0.9)
writer = SummaryWriter() # Writer of tensoboard.
if not os.path.isdir('./models'):
os.mkdir('./models') # Create directory of saving models.
n_epochs, best_loss, step, early_stop_count = config['n_epochs'], math.inf, 0, 0
for epoch in range(n_epochs):
model.train() # Set your model to train mode.
loss_record = []
# tqdm is a package to visualize your training progress.
train_pbar = tqdm(train_loader, position=0, leave=True)
for x, y in train_pbar:
optimizer.zero_grad() # Set gradient to zero.
x, y = x.to(device), y.to(device) # Move your data to device.
pred = model(x)
loss = criterion(pred, y)
loss.backward() # Compute gradient(backpropagation).
optimizer.step() # Update parameters.
step += 1
loss_record.append(loss.detach().item())
# Display current epoch number and loss on tqdm progress bar.
train_pbar.set_description(f'Epoch [{epoch+1}/{n_epochs}]')
train_pbar.set_postfix({'loss': loss.detach().item()})
mean_train_loss = sum(loss_record)/len(loss_record)
writer.add_scalar('Loss/train', mean_train_loss, step)
model.eval() # Set your model to evaluation mode.
loss_record = []
for x, y in valid_loader:
x, y = x.to(device), y.to(device)
with torch.no_grad():
pred = model(x)
loss = criterion(pred, y)
loss_record.append(loss.item())
mean_valid_loss = sum(loss_record)/len(loss_record)
print(f'Epoch [{epoch+1}/{n_epochs}]: Train loss: {mean_train_loss:.4f}, Valid loss: {mean_valid_loss:.4f}')
writer.add_scalar('Loss/valid', mean_valid_loss, step)
if mean_valid_loss < best_loss:
best_loss = mean_valid_loss
torch.save(model.state_dict(), config['save_path']) # Save your best model
print('Saving model with loss {:.3f}...'.format(best_loss))
early_stop_count = 0
else:
early_stop_count += 1
if early_stop_count >= config['early_stop']:
print('\nModel is not improving, so we halt the training session.')
return
DKT
if training:
# 是run_epoch中的一部分
#将隐藏状态与计算图的历史分离,防止梯度的累计
hidden = repackage_hidden(hidden)
# 将梯度初始化为零(因为一个batch的loss关于weight的导数是所有sample的loss关于weight的导数的累加和)
optimizer.zero_grad()
# 调用了forward函数
output, hidden = m(input_data, hidden)
# Get prediction results from output
# 将output转成1维
output = output.contiguous().view(-1)
# 从output的特定位置取值,具体位置由target_id给出,得到对于做过题目的预测值
logits = torch.gather(output, 0, target_id)
# preds 使用sigmod函数
preds = torch.sigmoid(logits)
# 在浮点数结果上使用 .item() 函数可以提高显示精度
for p in preds:
pred_labels.append(p.item())
# 记录预测结果
# criterion = nn.CrossEntropyLoss()
# sigmoid+BCEloss
# softmax+CrossEntropyLoss
criterion = nn.BCEWithLogitsLoss()
loss = criterion(logits, target_correctness)
#比较logits中的预测值和真实值间的差别,得到评估性能的loss
loss.backward()
# 将梯度计算的结果传回模型的每一个参数,以便更新参数时根据梯度的方向进行调整
# `clip_grad_norm` helps prevent the exploding gradient problem in RNNs / LSTMs.
# max_norm越小,裁剪的梯度越大,得到的梯度就越小,防止梯度爆炸的效果越明显
torch.nn.utils.clip_grad_norm_(m.parameters(), args.max_grad_norm)
#用optimizer调整模型参数
optimizer.step()
# total_loss += loss.item()
all_loss.append(loss.item())
测试框架关闭了梯度计算,有些神经网络模型的训练和测试模式差别很大
else:
# 表明当前计算不需要反向传播,使用之后,强制后边的内容不进行计算图的构建
with torch.no_grad():
m.eval()
output, hidden = m(input_data, hidden)
output = output.contiguous().view(-1)
logits = torch.gather(output, 0, target_id)
# preds
preds = torch.sigmoid(logits)
# pred_labels用于后面计算rmse
for p in preds:
pred_labels.append(p.item())
# criterion = nn.CrossEntropyLoss()
criterion = nn.BCEWithLogitsLoss()
loss = criterion(logits, target_correctness)
# total_loss += loss.item()
all_loss.append(loss.item())
hidden = repackage_hidden(hidden)
#对于目标数据(输出指标)的计算
rmse = sqrt(mean_squared_error(actual_labels, pred_labels))
all_rmse.append(rmse)
# 计算ROC
fpr, tpr, thresholds = metrics.roc_curve(actual_labels, pred_labels, pos_label=1)
# 计算AUC
auc = metrics.auc(fpr, tpr)
all_auc.append(auc)
# count指当前在第几个batch的计算,batch_num指的是一共有几个batch
# print("Epoch: {}, Batch {}/{} AUC: {}".format(epoch, count, batch_num, auc))
# calculate r^2
r2 = r2_score(actual_labels, pred_labels)
all_r2.append(r2)
acc = compute_acc(actual_labels, pred_labels)
all_acc.append(acc)
loss = sum(all_loss) / len(all_loss)
auc = sum(all_auc) / len(all_auc)
acc = sum(all_acc) / len(all_acc)
rmse = sum(all_rmse) / len(all_rmse)
r2 = sum(all_r2) / len(all_r2)
# return rmse, auc, r2, acc
return loss, auc, acc, rmse, r2
真正的测试,不给y,得出的预测概率