Skip to content

Commit 79f300c

Browse files
authored
[FEATURE] Add KaNCD (#39)
* add KaNCD model * delete useless codes in KaNCD.py * modify default learning rate in KaNCD.py * add example files for KaNCD * add doc for KaNCD * update README.md * add test files * delete blank line at the end of file
1 parent f7e4916 commit 79f300c

13 files changed

+736
-1
lines changed

EduCDM/KaNCD/KaNCD.py

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# coding: utf-8
2+
# 2023/7/3 @ WangFei
3+
4+
import logging
5+
import torch
6+
import torch.nn as nn
7+
import torch.optim as optim
8+
import torch.nn.functional as F
9+
import numpy as np
10+
from tqdm import tqdm
11+
from sklearn.metrics import roc_auc_score, accuracy_score
12+
from EduCDM import CDM
13+
14+
15+
class PosLinear(nn.Linear):
16+
def forward(self, input: torch.Tensor) -> torch.Tensor:
17+
weight = 2 * F.relu(1 * torch.neg(self.weight)) + self.weight
18+
return F.linear(input, weight, self.bias)
19+
20+
21+
class Net(nn.Module):
22+
23+
def __init__(self, exer_n, student_n, knowledge_n, mf_type, dim):
24+
self.knowledge_n = knowledge_n
25+
self.exer_n = exer_n
26+
self.student_n = student_n
27+
self.emb_dim = dim
28+
self.mf_type = mf_type
29+
self.prednet_input_len = self.knowledge_n
30+
self.prednet_len1, self.prednet_len2 = 256, 128 # changeable
31+
32+
super(Net, self).__init__()
33+
34+
# prediction sub-net
35+
self.student_emb = nn.Embedding(self.student_n, self.emb_dim)
36+
self.exercise_emb = nn.Embedding(self.exer_n, self.emb_dim)
37+
self.knowledge_emb = nn.Parameter(torch.zeros(self.knowledge_n, self.emb_dim))
38+
self.e_discrimination = nn.Embedding(self.exer_n, 1)
39+
self.prednet_full1 = PosLinear(self.prednet_input_len, self.prednet_len1)
40+
self.drop_1 = nn.Dropout(p=0.5)
41+
self.prednet_full2 = PosLinear(self.prednet_len1, self.prednet_len2)
42+
self.drop_2 = nn.Dropout(p=0.5)
43+
self.prednet_full3 = PosLinear(self.prednet_len2, 1)
44+
45+
if mf_type == 'gmf':
46+
self.k_diff_full = nn.Linear(self.emb_dim, 1)
47+
self.stat_full = nn.Linear(self.emb_dim, 1)
48+
elif mf_type == 'ncf1':
49+
self.k_diff_full = nn.Linear(2 * self.emb_dim, 1)
50+
self.stat_full = nn.Linear(2 * self.emb_dim, 1)
51+
elif mf_type == 'ncf2':
52+
self.k_diff_full1 = nn.Linear(2 * self.emb_dim, self.emb_dim)
53+
self.k_diff_full2 = nn.Linear(self.emb_dim, 1)
54+
self.stat_full1 = nn.Linear(2 * self.emb_dim, self.emb_dim)
55+
self.stat_full2 = nn.Linear(self.emb_dim, 1)
56+
57+
# initialize
58+
for name, param in self.named_parameters():
59+
if 'weight' in name:
60+
nn.init.xavier_normal_(param)
61+
nn.init.xavier_normal_(self.knowledge_emb)
62+
63+
def forward(self, stu_id, input_exercise, input_knowledge_point):
64+
# before prednet
65+
stu_emb = self.student_emb(stu_id)
66+
exer_emb = self.exercise_emb(input_exercise)
67+
# get knowledge proficiency
68+
batch, dim = stu_emb.size()
69+
stu_emb = stu_emb.view(batch, 1, dim).repeat(1, self.knowledge_n, 1)
70+
knowledge_emb = self.knowledge_emb.repeat(batch, 1).view(batch, self.knowledge_n, -1)
71+
if self.mf_type == 'mf': # simply inner product
72+
stat_emb = torch.sigmoid((stu_emb * knowledge_emb).sum(dim=-1, keepdim=False)) # batch, knowledge_n
73+
elif self.mf_type == 'gmf':
74+
stat_emb = torch.sigmoid(self.stat_full(stu_emb * knowledge_emb)).view(batch, -1)
75+
elif self.mf_type == 'ncf1':
76+
stat_emb = torch.sigmoid(self.stat_full(torch.cat((stu_emb, knowledge_emb), dim=-1))).view(batch, -1)
77+
elif self.mf_type == 'ncf2':
78+
stat_emb = torch.sigmoid(self.stat_full1(torch.cat((stu_emb, knowledge_emb), dim=-1)))
79+
stat_emb = torch.sigmoid(self.stat_full2(stat_emb)).view(batch, -1)
80+
batch, dim = exer_emb.size()
81+
exer_emb = exer_emb.view(batch, 1, dim).repeat(1, self.knowledge_n, 1)
82+
if self.mf_type == 'mf':
83+
k_difficulty = torch.sigmoid((exer_emb * knowledge_emb).sum(dim=-1, keepdim=False)) # batch, knowledge_n
84+
elif self.mf_type == 'gmf':
85+
k_difficulty = torch.sigmoid(self.k_diff_full(exer_emb * knowledge_emb)).view(batch, -1)
86+
elif self.mf_type == 'ncf1':
87+
k_difficulty = torch.sigmoid(self.k_diff_full(torch.cat((exer_emb, knowledge_emb), dim=-1))).view(batch, -1)
88+
elif self.mf_type == 'ncf2':
89+
k_difficulty = torch.sigmoid(self.k_diff_full1(torch.cat((exer_emb, knowledge_emb), dim=-1)))
90+
k_difficulty = torch.sigmoid(self.k_diff_full2(k_difficulty)).view(batch, -1)
91+
# get exercise discrimination
92+
e_discrimination = torch.sigmoid(self.e_discrimination(input_exercise))
93+
94+
# prednet
95+
input_x = e_discrimination * (stat_emb - k_difficulty) * input_knowledge_point
96+
# f = input_x[input_knowledge_point == 1]
97+
input_x = self.drop_1(torch.tanh(self.prednet_full1(input_x)))
98+
input_x = self.drop_2(torch.tanh(self.prednet_full2(input_x)))
99+
output_1 = torch.sigmoid(self.prednet_full3(input_x))
100+
101+
return output_1.view(-1)
102+
103+
104+
class KaNCD(CDM):
105+
def __init__(self, **kwargs):
106+
super(KaNCD, self).__init__()
107+
mf_type = kwargs['mf_type'] if 'mf_type' in kwargs else 'gmf'
108+
self.net = Net(kwargs['exer_n'], kwargs['student_n'], kwargs['knowledge_n'], mf_type, kwargs['dim'])
109+
110+
def train(self, train_set, valid_set, lr=0.002, device='cpu', epoch_n=15):
111+
logging.info("traing... (lr={})".format(lr))
112+
self.net = self.net.to(device)
113+
loss_function = nn.BCELoss()
114+
optimizer = optim.Adam(self.net.parameters(), lr=lr)
115+
for epoch_i in range(epoch_n):
116+
self.net.train()
117+
epoch_losses = []
118+
batch_count = 0
119+
for batch_data in tqdm(train_set, "Epoch %s" % epoch_i):
120+
batch_count += 1
121+
user_info, item_info, knowledge_emb, y = batch_data
122+
user_info: torch.Tensor = user_info.to(device)
123+
item_info: torch.Tensor = item_info.to(device)
124+
knowledge_emb: torch.Tensor = knowledge_emb.to(device)
125+
y: torch.Tensor = y.to(device)
126+
pred = self.net(user_info, item_info, knowledge_emb)
127+
loss = loss_function(pred, y)
128+
optimizer.zero_grad()
129+
loss.backward()
130+
optimizer.step()
131+
132+
epoch_losses.append(loss.mean().item())
133+
134+
print("[Epoch %d] average loss: %.6f" % (epoch_i, float(np.mean(epoch_losses))))
135+
logging.info("[Epoch %d] average loss: %.6f" % (epoch_i, float(np.mean(epoch_losses))))
136+
auc, acc = self.eval(valid_set, device)
137+
print("[Epoch %d] auc: %.6f, acc: %.6f" % (epoch_i, auc, acc))
138+
logging.info("[Epoch %d] auc: %.6f, acc: %.6f" % (epoch_i, auc, acc))
139+
140+
return auc, acc
141+
142+
def eval(self, test_data, device="cpu"):
143+
logging.info('eval ... ')
144+
self.net = self.net.to(device)
145+
self.net.eval()
146+
y_true, y_pred = [], []
147+
for batch_data in tqdm(test_data, "Evaluating"):
148+
user_id, item_id, knowledge_emb, y = batch_data
149+
user_id: torch.Tensor = user_id.to(device)
150+
item_id: torch.Tensor = item_id.to(device)
151+
knowledge_emb: torch.Tensor = knowledge_emb.to(device)
152+
pred = self.net(user_id, item_id, knowledge_emb)
153+
y_pred.extend(pred.detach().cpu().tolist())
154+
y_true.extend(y.tolist())
155+
156+
return roc_auc_score(y_true, y_pred), accuracy_score(y_true, np.array(y_pred) >= 0.5)
157+
158+
def save(self, filepath):
159+
torch.save(self.net.state_dict(), filepath)
160+
logging.info("save parameters to %s" % filepath)
161+
162+
def load(self, filepath):
163+
self.net.load_state_dict(torch.load(filepath, map_location=lambda s, loc: s))
164+
logging.info("load parameters from %s" % filepath)

EduCDM/KaNCD/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# coding: utf-8
2+
# 2021/4/1 @ WangFei
3+
4+
from .KaNCD import KaNCD

EduCDM/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
from .NCDM import NCDM
1010
from .IRT import EMIRT, GDIRT
1111
from .MIRT import MIRT
12+
from .KaNCD import KaNCD

README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
[![License](https://img.shields.io/github/license/bigdata-ustc/EduCDM)](LICENSE)
1313
[![DOI](https://zenodo.org/badge/348569904.svg)](https://zenodo.org/badge/latestdoi/348569904)
1414

15-
The Model Zoo of Cognitive Diagnosis Models, including classic Item Response Ranking (**IRT**), Multidimensional Item Response Ranking (**MIRT**), Deterministic Input, Noisy "And" model(**DINA**), and advanced Fuzzy Cognitive Diagnosis Framework (**FuzzyCDF**), Neural Cognitive Diagnosis Model (**NCDM**), Item Response Ranking framework (**IRR**) and Incremental Cognitive Diagnosis (**ICD**).
15+
The Model Zoo of Cognitive Diagnosis Models, including classic Item Response Ranking (**IRT**), Multidimensional Item Response Ranking (**MIRT**), Deterministic Input, Noisy "And" model(**DINA**), and advanced Fuzzy Cognitive Diagnosis Framework (**FuzzyCDF**), Neural Cognitive Diagnosis Model (**NCDM**), Item Response Ranking framework (**IRR**), Incremental Cognitive Diagnosis (**ICD**) and Knowledge-association baesd extension of NeuralCD (**KaNCD**).
1616

1717
## Brief introduction to CDM
1818

@@ -38,6 +38,7 @@ More recent researches about CDMs:
3838
- [NeuralCD](http://staff.ustc.edu.cn/~cheneh/paper_pdf/2020/Fei-Wang-AAAI.pdf): neural cognitive diagnosis framework, a neural-network-based general cognitive diagnosis framework. In this repository we provide the basic implementation NCDM.
3939
- [IRR](http://home.ustc.edu.cn/~tongsw/files/IRR.pdf): item response ranking framework, a pairwise cognitive diagnosis framework. In this repository we provide the several implementations for most of CDMs.
4040
- [ICD]: Incremental Cognitive Diagnosis, a framework that tailor cognitive diagnosis into the online scenario of intelligent education. In this repository we provide the several implementations for most of CDMs.
41+
- [KaNCD](https://ieeexplore.ieee.org/abstract/document/9865139): extended from the NeuralCD framework. We use high-order latent traits of students, exercises and knowledge concepts to capture latent associations among knowledge concepts.
4142

4243
## List of models
4344

@@ -57,6 +58,7 @@ More recent researches about CDMs:
5758
* [IRR-DINA](examples/IRR/DINA.ipynb)
5859
* [IRR-IRT](examples/IRR/IRT.ipynb)
5960
* [ICD](EduCDM/ICD) [[doc]](docs/ICD.md)
61+
* [KaNCD](EduCDM/KaNCD) [[doc\]](docs/KaNCD.md) [[example\]](examples/KaNCD)
6062
## Installation
6163

6264
Git and install with `pip`:
@@ -100,3 +102,5 @@ If this repository is helpful for you, please cite our work
100102
[2] Wang F, Liu Q, Chen E, et al. Neural cognitive diagnosis for intelligent education systems[C]//Proceedings of the AAAI Conference on Artificial Intelligence. 2020, 34(04): 6153-6161.
101103

102104
[3] Tong S, Liu Q, Yu R, et al. Item response ranking for cognitive diagnosis[C]. IJCAI, 2021.
105+
106+
[4] Wang F, Liu Q, Chen E, et al. NeuralCD: A General Framework for Cognitive Diagnosis. IEEE Transactions on Knowledge and Data Engineering (IEEE TKDE), accepted, 2022.

docs/KaNCD.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# KaNCD
2+
3+
The implementation of the KaNCD model in paper: [NeuralCD: A General Framework for Cognitive Diagnosis](https://ieeexplore.ieee.org/abstract/document/9865139)
4+
5+
KaNCD is an **K**nowledge-**a**ssociation based extension of the **N**eural**CD**M (alias NCDM in this package) model. In KaNCD, higher-order low dimensional latent traits of students, exercises and knowledge concepts are used respectively.
6+
7+
The knowledge difficulty vector of an exercise is calculated from the latent trait of the exercise and the latent trait of each knowledge concept.
8+
9+
![KDM_MF](F:\git_project\EduCDM\EduCDM\docs\_static\KDM_MF.png)
10+
11+
Similarly, the knowledge proficiency vector of a student is calculated from the latent trait of the student and the latent trait of each knowledge concept.
12+
13+
![KPM_MF](F:\git_project\EduCDM\EduCDM\docs\_static\KPM_MF.png)
14+
15+
Please refer to the paper for more details.

docs/_static/KDM_MF.png

57.5 KB
Loading

docs/_static/KPM_MF.png

55.2 KB
Loading

0 commit comments

Comments
 (0)