Advertisement

contex-aware knowledge tracing——AKT

阅读量:

作为目前较真实且auc较高的的知识追踪模型,来看看它的代码结构,方便你我去实现和修改。原理、创新点请看论文,本文只作代码分析:

目的

AKT在assistment15(0.782)数据上比DKT(0.731)要高很多,同样只使用技能信息的情况下:
我的任务就是看为什么auc高这么多,文章没有具体消融试验,但可以从一部分看出

在这里插入图片描述

对技能和交互先encoder一次,在assist15得到5%的提升

在这里插入图片描述

使用了衰减机制,在assist15得到5%的提升。
问题出现了,究竟是encoder起的作用,还是衰减机制起的作用。使其达到0.7228

我估计是这里的AKT-NR-pos是相对于AKT-NR-raw的基础上在去掉衰减机制,替换成position embedding。
也可以看到AKT-NR-raw为0.7332,而AKT-NR-pos为0.7271。所以就算CCF-A也不不公平地比较。不是严格按照:每个对比实验只有一个组件不同。

结论:应该是encoder起的作用大一点。

数据集

作者处理成 一个样本行数据:(id是连续)

  1. 学生id,学生真实索引
  2. 题目id
  3. 技能id
  4. 答对答错
    样本以学生为单位,分成训练集,验证集,测试集。(60%,20%,20%)
    如果嫌数据量太少,把验证集归到训练集,因为代码要跑验证部分,避免修改结构,把测试集赋给验证集。

main.py 总体

  1. 加载数据的类,分DATA和PID_DATA,有的数据集没有技能信息
  2. 设置种子数seed
  3. params.train_set,5折交叉验证,有5个训练集
  4. train_q_data, train_qa_data, train_pid = dat.load_data(train_data_path)与DKVMN一样。而DKT不需要seq_len-1(不预测第一题)
  5. best_epoch = train_one_dataset()训练模型,记录最好的epoch
  6. test_one_dataset(best_epoch )使用最好的epoch预测测试集

train_one_dataset训练步骤

  1. 加载模型,在utils.py文件里,return model = AKT()
  2. 设置optimizer,包括模型参数、学习率
  3. for idx in range(params.max_iter)里面执行train函数。参数包括模型、数据,优化器,超参数
  4. 验证集auc提升时,保存模型、优化器。设置提前终止条件。
  5. f_save_log.write()记录结果

test_one_dataset测试步骤

  1. 加载模型及最优epoch的模型参数
  2. 执行一次test函数。参数没有优化器
  3. 运行完之后又把模型参数给删除

load_data.py

class DATA(object):
其中len(Q[len(Q)-1]) == 0,这一步是怕字符串根据分隔符分割之后,列表的最后一个元素为空字符。如:"1,2,3,"这样分割就会出现4个元素。

复制代码
    for lineID, line in enumerate(f_data):
    	记录student_id,第几个学生(样本)
    	记录Q,回答的技能
    	记录A,回答的情况
    	# 处理数据
    	1.一个学生截断成多个学生(题目数多于200道就认为新的样本),这里没有限制样本最小题目数
    	2.答题情况 = 2倍技能数,前半部代表答错,后半部代表答对
    	3.q_dataArray = np.zeros((len(q_data), self.seqlen)) 固定输入数据的大小,有数据的地方就填充,没有就为0
    	4.返回 q_dataArray, qa_dataArray, np.asarray(idx_data),idx_data是学生的id,模型应该没有用到的,只是用来保证return3个数据。
    	前两个是二维【batch,seq_len】 idx_data是一维【batch】

class PID_DATA(object):
只不过多处理了一行:题目信息

复制代码
    q_data.append(question_sequence) 技能信息
    qa_data.append(answer_sequence) 答题情况
    p_data.append(problem_sequence)  问题信息
    return q_dataArray, qa_dataArray, p_dataArray

run.py

数据值域:id从1开始,0的位置为padding

train函数

  1. Shuffle the data,pid_flag是否有题目信息
  2. 数据包括input_q、input_pid、(input_qa、target)同一个,输入网络和训练目标
  3. target = (target - 1) / params.n_question,np.floor(target),使padding值为-1,非padding值为0或1
  4. loss, pred, true_ct = net(),这里的loss是用来反向传播
  5. nopadding_index = np.flatnonzero(target >= -0.9),根据索引就能筛选出要的预测值:pred[nopadding_index]
  6. 训练完全部数据后对all_pred,all_target计算auc、acc、loss(对全部数据算二值交叉熵)

test函数,区别在于
net.eval()
with torch.no_grad():

akt.py

模型部分不解释,就是self-attention、注意力衰减,只分析输入部分

参数部分

复制代码
    parser.add_argument('--d_model', type=int, default=256,
                        help='Transformer d_model shape')
    parser.add_argument('--d_ff', type=int, default=1024,
                        help='Transformer d_ff shape')
    parser.add_argument('--dropout', type=float,
                        default=0.05, help='Dropout rate')
    parser.add_argument('--n_block', type=int, default=1,
                        help='number of blocks')
    parser.add_argument('--n_head', type=int, default=8,
                        help='number of heads in multihead attention')
    parser.add_argument('--kq_same', type=int, default=1) # 
    模型参数:
    	final_fc_dim=512
    	self.separate_qa 默认False,即交互信息=问题信息 拼接 对错信息(0或1)
    	self.n_pid 题目数 = 16891
    	self.n_question 技能数 = 110

嵌入部分

如果带有题目信息,如果看作者注释会有点杂乱

复制代码
    self.difficult_param = nn.Embedding(self.n_pid+1, 1) # 问题难度
    self.q_embed_diff = nn.Embedding(self.n_question+1, embed_l) # 技能变体
    self.qa_embed_diff = nn.Embedding(2 * self.n_question + 1, embed_l) # 交互变体
    
    self.q_embed = nn.Embedding(self.n_question+1, embed_l) # 技能嵌入
    self.qa_embed = nn.Embedding(2, embed_l) # 对错嵌入
    
    def forward(self, q_data, qa_data, target, pid_data=None):
    	q_embed_data = self.q_embed(q_data) # 技能嵌入 ct
    	qa_data = (qa_data-q_data)//self.n_question # 0 或 1
    	qa_embed_data = self.qa_embed(qa_data)+q_embed_data # 对错嵌入 rt + ct => 交互嵌入 at
    	
    	# 如果没有题目信息,上面q_embed_data、qa_embed_data 就是模型所要的
    	# 加入题目信息后,q_embed_data、qa_embed_data要更新
    	q_embed_diff_data = self.q_embed_diff(q_data) # 技能变体 d_ct
    	pid_embed_data = self.difficult_param(pid_data) # 问题难度 u 标量
    	# ct + d_ct * u ==> X(题目信息)= 技能嵌入 + 技能变体 * 问题难度
    	q_embed_data = q_embed_data + pid_embed_data * q_embed_diff_data
    	
    	# 不符合:因为上面定义该embedding层为2倍技能数,而qa_data属于0或1 ————>应该修改self.qa_embed_diff(2, embed_l)
    	qa_embed_diff_data = self.qa_embed_diff(qa_data) # 对错嵌入 rt*
    	
    	# at + u * ( rt* + d_ct) ==> Y(交互信息) = 交互嵌入 + 问题难度 * ( 对错嵌入 + 技能变体)
    	qa_embed_data = qa_embed_data + pid_embed_data * (qa_embed_diff_data+q_embed_diff_data)
    	c_reg_loss = (pid_embed_data ** 2.).sum() * self.l2 # 对 难度系数 正则化

最后目的还是为得到q_embed_data,qa_embed_data 。

模型部分

self.model = Architecture()
—Architecture:3个TransformerLayer,(mask=1,qkv=Y,apply_pos=True)(mask=1,qkv=X,apply_pos=False) (mask=0,qk=X,v=Y,apply_pos=True)apply_pos表示有FFN,mask=1,可以看当前,zero_pad=False;mask=0,只能看过去,zero_pad=True。
------TransformerLayer:MultiHeadAttention、LN、Linear
-----------MultiHeadAttention:(kq_same=1,所以QK共用一个Linear映射)self-attention(多头拼接后在通过一层全连接)
---------------self-attention( Monotonic Attention Mechanism)

self.out = nn.Sequential(3个全连接)
—output拼接question:2*256——>final_fc_dim:512——>256——>1

复制代码
    x=np.triu(np.ones((1, 1, 5, 5)), k=0).astype('uint8')
    [[[[1 1 1 1 1]
       [0 1 1 1 1]
       [0 0 1 1 1]
       [0 0 0 1 1]
       [0 0 0 0 1]]]]
    x=np.triu(np.ones((1, 1, 5, 5)), k=1).astype('uint8')
    [[[[0 1 1 1 1]
       [0 0 1 1 1]
       [0 0 0 1 1]
       [0 0 0 0 1]
       [0 0 0 0 0]]]]

utils.py

get_file_name_identifier函数,模型不同,需要的参数不同,存储的文件名不同。比如:params.model = ‘akt_pid’

load_model函数,根据params.model = ‘akt_pid’,是否带有题目信息,模型根据params.n_pid,是否为题目信息创建Embedding,和对题目信息与技能信息进行拼接。

try_makedirs函数,创建目录os.makedirs(path_)

Monotonic Attention Mechanism

最后,也是最详细地讲一下最有数学功底的部分,怎么修改/调整注意力机制的,因为self-attention的注意力矩阵就是两个矩阵相乘,怎么在这个基础上做文章。

在这里插入图片描述
复制代码
    def attention(q, k, v, d_k, mask, dropout, zero_pad, gamma=None):
    """
    This is called by Multi-head atention object to find the values.
    """
    # 这里q,k共享全连接,所以是相同的
    scores = torch.matmul(q, k.transpose(-2, -1)) / \
        math.sqrt(d_k)  # BS, 8, seqlen, seqlen
    bs, head, seqlen = scores.size(0), scores.size(1), scores.size(2)
    # 还没mask的注意力矩阵
    
    x1 = torch.arange(seqlen).expand(seqlen, -1).to(device)
    x2 = x1.transpose(0, 1).contiguous()
    # x2 [[0 0 0]  x1-x2[[0   1 2]
    #     [1 1 1]        [-1  0 1]  
    #     [2 2 2]]       [-2 -1 0]]
    with torch.no_grad():
    	# False的地方(未来信息),赋无穷小
        scores_ = scores.masked_fill(mask == 0, -1e32)
        scores_ = F.softmax(scores_, dim=-1)  # BS,8,seqlen,seqlen
        scores_ = scores_ * mask.float().to(device)
        # scores_ 未来信息已为0
        
        
        distcum_scores = torch.cumsum(scores_, dim=-1)  # bs, 8, sl, sl #cumsum:按维度dim进行累加
        # 红色部分,当前个step,对角线位置,全部权重之和
        disttotal_scores = torch.sum(
            scores_, dim=-1, keepdim=True)  # bs, 8, sl, 1
        # 绝对距离,|t-θ| 蓝色部分
        position_effect = torch.abs(
            x1-x2)[None, None, :, :].type(torch.FloatTensor).to(device)  # 1, 1, seqlen, seqlen
        # bs, 8, sl, sl positive distance
        # disttotal_scores-distcum_scores,绿色部分
        # 假设step=5,观察第2个词时,总和减去前2个词,剩下就是第2个词之后的权重和
        # 语义:观察第2个词,要考虑它之后的词是否重要
        dist_scores = torch.clamp(
            (disttotal_scores-distcum_scores)*position_effect, min=0.)
        # dist_scores 距离计算完成
        dist_scores = dist_scores.sqrt().detach() #切断反向传播
    m = nn.Softplus() #激活函数log(1+e^x)
    gamma = -1. * m(gamma).unsqueeze(0)  # 1,8,1,1
    # Now after do exp(gamma*distance) and then clamp to 1e-5 to 1e5
    total_effect = torch.clamp(torch.clamp(
        (dist_scores*gamma).exp(), min=1e-5), max=1e5)
    # e的-x次方小于1 衰减机制
    scores = scores * total_effect
    	# 怕泄露 在一次mask未来信息
    scores.masked_fill_(mask == 0, -1e32)
    scores = F.softmax(scores, dim=-1)  # BS,8,seqlen,seqlen
    if zero_pad:
    	# 该情况,包括对角线以上为False/0。
    	# 第一行softmax之后就是平均值,非0,要把第一行变为全0
        pad_zero = torch.zeros(bs, head, 1, seqlen).to(device)
        scores = torch.cat([pad_zero, scores[:, :, 1:, :]], dim=2)
    scores = dropout(scores) # dropout在乘以values
    output = torch.matmul(scores, v)
    return output

全部评论 (0)

还没有任何评论哟~