From 6dd9ae15c3e5b3bdc7f458977404a91b401845eb Mon Sep 17 00:00:00 2001
From: nnnyt <793313994@qq.com>
Date: Mon, 1 Mar 2021 21:26:22 +0800
Subject: [PATCH] add scripts
---
scripts/dataset/assistment.ipynb | 745 +++++++++++++++++++++++++++++++
scripts/test.ipynb | 367 +++++++++++++++
scripts/train.ipynb | 406 +++++++++++++++++
3 files changed, 1518 insertions(+)
create mode 100644 scripts/dataset/assistment.ipynb
create mode 100644 scripts/test.ipynb
create mode 100644 scripts/train.ipynb
diff --git a/scripts/dataset/assistment.ipynb b/scripts/dataset/assistment.ipynb
new file mode 100644
index 0000000..acfec2d
--- /dev/null
+++ b/scripts/dataset/assistment.ipynb
@@ -0,0 +1,745 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import csv\n",
+ "import json\n",
+ "import random\n",
+ "import numpy as np\n",
+ "import pandas as pd\n",
+ "import matplotlib.pyplot as plt\n",
+ "from collections import defaultdict"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def stat_unique(data: pd.DataFrame, key):\n",
+ " if key is None:\n",
+ " print('Total length: {}'.format(len(data)))\n",
+ " elif isinstance(key, str):\n",
+ " print('Number of unique {}: {}'.format(key, len(data[key].unique())))\n",
+ " elif isinstance(key, list):\n",
+ " print('Number of unique [{}]: {}'.format(','.join(key), len(data.drop_duplicates(key, keep='first'))))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/Users/yutingning/opt/anaconda3/envs/cat/lib/python3.7/site-packages/IPython/core/interactiveshell.py:3147: DtypeWarning: Columns (18) have mixed types.Specify dtype option on import or set low_memory=False.\n",
+ " interactivity=interactivity, compiler=compiler, result=result)\n"
+ ]
+ },
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " Unnamed: 0 | \n",
+ " order_id | \n",
+ " assignment_id | \n",
+ " user_id | \n",
+ " assistment_id | \n",
+ " problem_id | \n",
+ " original | \n",
+ " correct | \n",
+ " attempt_count | \n",
+ " ms_first_response | \n",
+ " ... | \n",
+ " hint_count | \n",
+ " hint_total | \n",
+ " overlap_time | \n",
+ " template_id | \n",
+ " answer_id | \n",
+ " answer_text | \n",
+ " first_action | \n",
+ " bottom_hint | \n",
+ " opportunity | \n",
+ " opportunity_original | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " 1 | \n",
+ " 33022537 | \n",
+ " 277618 | \n",
+ " 64525 | \n",
+ " 33139 | \n",
+ " 51424 | \n",
+ " 1 | \n",
+ " 1 | \n",
+ " 1 | \n",
+ " 32454 | \n",
+ " ... | \n",
+ " 0 | \n",
+ " 3 | \n",
+ " 32454 | \n",
+ " 30799 | \n",
+ " NaN | \n",
+ " 26 | \n",
+ " 0 | \n",
+ " NaN | \n",
+ " 1 | \n",
+ " 1.0 | \n",
+ "
\n",
+ " \n",
+ " 1 | \n",
+ " 2 | \n",
+ " 33022709 | \n",
+ " 277618 | \n",
+ " 64525 | \n",
+ " 33150 | \n",
+ " 51435 | \n",
+ " 1 | \n",
+ " 1 | \n",
+ " 1 | \n",
+ " 4922 | \n",
+ " ... | \n",
+ " 0 | \n",
+ " 3 | \n",
+ " 4922 | \n",
+ " 30799 | \n",
+ " NaN | \n",
+ " 55 | \n",
+ " 0 | \n",
+ " NaN | \n",
+ " 2 | \n",
+ " 2.0 | \n",
+ "
\n",
+ " \n",
+ " 2 | \n",
+ " 3 | \n",
+ " 35450204 | \n",
+ " 220674 | \n",
+ " 70363 | \n",
+ " 33159 | \n",
+ " 51444 | \n",
+ " 1 | \n",
+ " 0 | \n",
+ " 2 | \n",
+ " 25390 | \n",
+ " ... | \n",
+ " 0 | \n",
+ " 3 | \n",
+ " 42000 | \n",
+ " 30799 | \n",
+ " NaN | \n",
+ " 88 | \n",
+ " 0 | \n",
+ " NaN | \n",
+ " 1 | \n",
+ " 1.0 | \n",
+ "
\n",
+ " \n",
+ " 3 | \n",
+ " 4 | \n",
+ " 35450295 | \n",
+ " 220674 | \n",
+ " 70363 | \n",
+ " 33110 | \n",
+ " 51395 | \n",
+ " 1 | \n",
+ " 1 | \n",
+ " 1 | \n",
+ " 4859 | \n",
+ " ... | \n",
+ " 0 | \n",
+ " 3 | \n",
+ " 4859 | \n",
+ " 30059 | \n",
+ " NaN | \n",
+ " 41 | \n",
+ " 0 | \n",
+ " NaN | \n",
+ " 2 | \n",
+ " 2.0 | \n",
+ "
\n",
+ " \n",
+ " 4 | \n",
+ " 5 | \n",
+ " 35450311 | \n",
+ " 220674 | \n",
+ " 70363 | \n",
+ " 33196 | \n",
+ " 51481 | \n",
+ " 1 | \n",
+ " 0 | \n",
+ " 14 | \n",
+ " 19813 | \n",
+ " ... | \n",
+ " 3 | \n",
+ " 4 | \n",
+ " 124564 | \n",
+ " 30060 | \n",
+ " NaN | \n",
+ " 65 | \n",
+ " 0 | \n",
+ " 0.0 | \n",
+ " 3 | \n",
+ " 3.0 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
5 rows × 31 columns
\n",
+ "
"
+ ],
+ "text/plain": [
+ " Unnamed: 0 order_id assignment_id user_id assistment_id problem_id \\\n",
+ "0 1 33022537 277618 64525 33139 51424 \n",
+ "1 2 33022709 277618 64525 33150 51435 \n",
+ "2 3 35450204 220674 70363 33159 51444 \n",
+ "3 4 35450295 220674 70363 33110 51395 \n",
+ "4 5 35450311 220674 70363 33196 51481 \n",
+ "\n",
+ " original correct attempt_count ms_first_response ... hint_count \\\n",
+ "0 1 1 1 32454 ... 0 \n",
+ "1 1 1 1 4922 ... 0 \n",
+ "2 1 0 2 25390 ... 0 \n",
+ "3 1 1 1 4859 ... 0 \n",
+ "4 1 0 14 19813 ... 3 \n",
+ "\n",
+ " hint_total overlap_time template_id answer_id answer_text first_action \\\n",
+ "0 3 32454 30799 NaN 26 0 \n",
+ "1 3 4922 30799 NaN 55 0 \n",
+ "2 3 42000 30799 NaN 88 0 \n",
+ "3 3 4859 30059 NaN 41 0 \n",
+ "4 4 124564 30060 NaN 65 0 \n",
+ "\n",
+ " bottom_hint opportunity opportunity_original \n",
+ "0 NaN 1 1.0 \n",
+ "1 NaN 2 2.0 \n",
+ "2 NaN 1 1.0 \n",
+ "3 NaN 2 2.0 \n",
+ "4 0.0 3 3.0 \n",
+ "\n",
+ "[5 rows x 31 columns]"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "raw_data = pd.read_csv('../../data/assistment.csv', encoding = 'utf-8', dtype={'skill_id': str})\n",
+ "raw_data.head()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "raw_data = raw_data.rename(columns={'user_id': 'student_id',\n",
+ " 'problem_id': 'question_id',\n",
+ " 'skill_id': 'knowledge_id',\n",
+ " 'skill_name': 'knowledge_name',\n",
+ " })\n",
+ "all_data = raw_data.loc[:, ['student_id', 'question_id', 'knowledge_id', 'knowledge_name', 'correct']].dropna()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Total length: 274590\n",
+ "Number of unique [student_id,question_id]: 270478\n",
+ "Number of unique student_id: 4151\n",
+ "Number of unique question_id: 16891\n",
+ "Number of unique knowledge_id: 138\n"
+ ]
+ }
+ ],
+ "source": [
+ "stat_unique(all_data, None)\n",
+ "stat_unique(all_data, ['student_id', 'question_id'])\n",
+ "stat_unique(all_data, 'student_id')\n",
+ "stat_unique(all_data, 'question_id')\n",
+ "stat_unique(all_data, 'knowledge_id')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Filter data"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "selected_data = all_data"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "filter 15924 questions\n"
+ ]
+ }
+ ],
+ "source": [
+ "# filter questions\n",
+ "n_students = selected_data.groupby('question_id')['student_id'].count()\n",
+ "question_filter = n_students[n_students < 50].index.tolist()\n",
+ "print(f'filter {len(question_filter)} questions')\n",
+ "selected_data = selected_data[~selected_data['question_id'].isin(question_filter)]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "filter 1749 students\n"
+ ]
+ }
+ ],
+ "source": [
+ "# filter students\n",
+ "n_questions = selected_data.groupby('student_id')['question_id'].count()\n",
+ "student_filter = n_questions[n_questions < 10].index.tolist()\n",
+ "print(f'filter {len(student_filter)} students')\n",
+ "selected_data = selected_data[~selected_data['student_id'].isin(student_filter)]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# get question to knowledge map\n",
+ "q2k = {}\n",
+ "table = selected_data.loc[:, ['question_id', 'knowledge_id']].drop_duplicates()\n",
+ "for i, row in table.iterrows():\n",
+ " q = row['question_id']\n",
+ " q2k[q] = set(map(int, str(row['knowledge_id']).split('_')))\n",
+ " \n",
+ "# get knowledge to question map\n",
+ "k2q = {}\n",
+ "for q, ks in q2k.items():\n",
+ " for k in ks:\n",
+ " k2q.setdefault(k, set())\n",
+ " k2q[k].add(q)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "filter 10 knowledges\n"
+ ]
+ }
+ ],
+ "source": [
+ "# filter knowledges\n",
+ "selected_knowledges = { k for k, q in k2q.items() if len(q) >= 10}\n",
+ "print(f'filter {len(k2q) - len(selected_knowledges)} knowledges')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# update maps\n",
+ "q2k = {q : ks for q, ks in q2k.items() if ks & selected_knowledges}\n",
+ "k2q = {}\n",
+ "for q, ks in q2k.items():\n",
+ " for k in ks:\n",
+ " k2q.setdefault(k, set())\n",
+ " k2q[k].add(q)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# update data\n",
+ "selected_data = selected_data[selected_data.apply(lambda x: x['question_id'] in q2k, axis=1)]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# renumber the students\n",
+ "s2n = {}\n",
+ "cnt = 0\n",
+ "for i, row in selected_data.iterrows():\n",
+ " if row.student_id not in s2n:\n",
+ " s2n[row.student_id] = cnt\n",
+ " cnt += 1\n",
+ "selected_data.loc[:, 'student_id'] = selected_data.loc[:, 'student_id'].apply(lambda x: s2n[x])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# renumber the questions\n",
+ "q2n = {}\n",
+ "cnt = 0\n",
+ "for i, row in selected_data.iterrows():\n",
+ " if row.question_id not in q2n:\n",
+ " q2n[row.question_id] = cnt\n",
+ " cnt += 1\n",
+ "selected_data.loc[:, 'question_id'] = selected_data.loc[:, 'question_id'].apply(lambda x: q2n[x])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# renumber the knowledges\n",
+ "k2n = {}\n",
+ "cnt = 0\n",
+ "for i, row in selected_data.iterrows():\n",
+ " for k in str(row.knowledge_id).split('_'):\n",
+ " if int(k) not in k2n:\n",
+ " k2n[int(k)] = cnt\n",
+ " cnt += 1\n",
+ "selected_data.loc[:, 'knowledge_id'] = selected_data.loc[:, 'knowledge_id'].apply(lambda x: '_'.join(map(lambda y: str(k2n[int(y)]), str(x).split('_'))))\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Total length: 61860\n",
+ "Number of unique [student_id,question_id]: 59500\n",
+ "Number of unique student_id: 1505\n",
+ "Number of unique question_id: 932\n",
+ "Number of unique knowledge_id: 22\n",
+ "Average #questions per knowledge: 44.38095238095238\n"
+ ]
+ }
+ ],
+ "source": [
+ "stat_unique(selected_data, None)\n",
+ "stat_unique(selected_data, ['student_id', 'question_id'])\n",
+ "stat_unique(selected_data, 'student_id')\n",
+ "stat_unique(selected_data, 'question_id')\n",
+ "stat_unique(selected_data, 'knowledge_id')\n",
+ "print('Average #questions per knowledge: {}'.format((len(q2k) / len(k2q))))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# save selected data\n",
+ "selected_data.to_csv('selected_data.csv', index=False)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# save concept map\n",
+ "q2k = {}\n",
+ "table = selected_data.loc[:, ['question_id', 'knowledge_id']].drop_duplicates()\n",
+ "for i, row in table.iterrows():\n",
+ " q = str(row['question_id'])\n",
+ " q2k[q] = list(map(int, str(row['knowledge_id']).split('_')))\n",
+ "with open('concept_map.json', 'w') as f:\n",
+ " json.dump(q2k, f)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## parse data"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def parse_data(data):\n",
+ " \"\"\" \n",
+ "\n",
+ " Args:\n",
+ " data: list of triplets (sid, qid, score)\n",
+ " \n",
+ " Returns:\n",
+ " student based datasets: defaultdict {sid: {qid: score}}\n",
+ " question based datasets: defaultdict {qid: {sid: score}}\n",
+ " \"\"\"\n",
+ " stu_data = defaultdict(lambda: defaultdict(dict))\n",
+ " ques_data = defaultdict(lambda: defaultdict(dict))\n",
+ " for i, row in data.iterrows():\n",
+ " sid = row.student_id\n",
+ " qid = row.question_id\n",
+ " correct = row.correct\n",
+ " stu_data[sid][qid] = correct\n",
+ " ques_data[qid][sid] = correct\n",
+ " return stu_data, ques_data"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "data = []\n",
+ "for i, row in selected_data.iterrows():\n",
+ " data.append([row.student_id, row.question_id, row.correct])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "stu_data, ques_data = parse_data(selected_data)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "test_size = 0.2\n",
+ "least_test_length=150"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "n_students = len(stu_data)\n",
+ "if isinstance(test_size, float):\n",
+ " test_size = int(n_students * test_size)\n",
+ "train_size = n_students - test_size\n",
+ "assert(train_size > 0 and test_size > 0)\n",
+ "\n",
+ "students = list(range(n_students))\n",
+ "random.shuffle(students)\n",
+ "if least_test_length is not None:\n",
+ " student_lens = defaultdict(int)\n",
+ " for t in data:\n",
+ " student_lens[t[0]] += 1\n",
+ " students = [student for student in students\n",
+ " if student_lens[student] >= least_test_length]\n",
+ "test_students = set(students[:test_size])\n",
+ "\n",
+ "train_data = [record for record in data if record[0] not in test_students]\n",
+ "test_data = [record for record in data if record[0] in test_students]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def renumber_student_id(data):\n",
+ " \"\"\"\n",
+ "\n",
+ " Args:\n",
+ " data: list of triplets (sid, qid, score)\n",
+ " \n",
+ " Returns:\n",
+ " renumbered datasets: list of triplets (sid, qid, score)\n",
+ " \"\"\"\n",
+ " student_ids = sorted(set(t[0] for t in data))\n",
+ " renumber_map = {sid: i for i, sid in enumerate(student_ids)}\n",
+ " data = [(renumber_map[t[0]], t[1], t[2]) for t in data]\n",
+ " return data"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 26,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "train_data = renumber_student_id(train_data)\n",
+ "test_data = renumber_student_id(test_data)\n",
+ "all_data = renumber_student_id(data)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 27,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "train records length: 51010\n",
+ "test records length: 10850\n",
+ "all records length: 61860\n"
+ ]
+ }
+ ],
+ "source": [
+ "print(f'train records length: {len(train_data)}')\n",
+ "print(f'test records length: {len(test_data)}')\n",
+ "print(f'all records length: {len(all_data)}')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## save data"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 28,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def save_to_csv(data, path):\n",
+ " \"\"\"\n",
+ "\n",
+ " Args:\n",
+ " data: list of triplets (sid, qid, correct)\n",
+ " path: str representing saving path\n",
+ " \"\"\"\n",
+ " pd.DataFrame.from_records(sorted(data), columns=['student_id', 'question_id', 'correct']).to_csv(path, index=False)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 29,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "save_to_csv(train_data, 'train_triples.csv')\n",
+ "save_to_csv(test_data, 'test_triples.csv')\n",
+ "save_to_csv(all_data, 'triples.csv')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 30,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "metadata = {\"num_students\": 1505, \n",
+ " \"num_questions\": 932,\n",
+ " \"num_concepts\": 22, \n",
+ " \"num_records\": 61860, \n",
+ " \"num_train_students\": 1444, \n",
+ " \"num_test_students\": 61}"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 31,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "with open('metadata.json', 'w') as f:\n",
+ " json.dump(metadata, f)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.7.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/scripts/test.ipynb b/scripts/test.ipynb
new file mode 100644
index 0000000..6877dcf
--- /dev/null
+++ b/scripts/test.ipynb
@@ -0,0 +1,367 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import sys\n",
+ "sys.path.append('..')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import CAT\n",
+ "import json\n",
+ "import torch\n",
+ "import logging\n",
+ "import datetime\n",
+ "import numpy as np\n",
+ "import pandas as pd\n",
+ "import matplotlib.pyplot as plt\n",
+ "from tensorboardX import SummaryWriter"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def setuplogger():\n",
+ " root = logging.getLogger()\n",
+ " root.setLevel(logging.INFO)\n",
+ " handler = logging.StreamHandler(sys.stdout)\n",
+ " handler.setLevel(logging.INFO)\n",
+ " formatter = logging.Formatter(\"[%(levelname)s %(asctime)s] %(message)s\")\n",
+ " handler.setFormatter(formatter)\n",
+ " root.addHandler(handler)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "setuplogger()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "seed = 0\n",
+ "np.random.seed(seed)\n",
+ "torch.manual_seed(seed)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "../logs/2021-03-01-21:19/\n"
+ ]
+ }
+ ],
+ "source": [
+ "# tensorboard\n",
+ "log_dir = f\"../logs/{datetime.datetime.now().strftime('%Y-%m-%d-%H:%M')}/\"\n",
+ "print(log_dir)\n",
+ "writer = SummaryWriter(log_dir)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# choose dataset here\n",
+ "dataset = 'assistment'\n",
+ "# modify config here\n",
+ "config = {\n",
+ " 'learning_rate': 0.0025,\n",
+ " 'batch_size': 2048,\n",
+ " 'num_epochs': 8,\n",
+ " 'num_dim': 1,\n",
+ " 'device': 'cpu',\n",
+ "}\n",
+ "# fixed test length\n",
+ "test_length = 10\n",
+ "# choose strategies here\n",
+ "strategies = [CAT.strategy.MFIStrategy(), CAT.strategy.KLIStrategy()]\n",
+ "# modify checkpoint path here\n",
+ "ckpt_path = '../ckpt/checkpoint.pt'"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# read datasets\n",
+ "test_triplets = pd.read_csv(f'../data/{dataset}/test_triples.csv', encoding='utf-8').to_records(index=False)\n",
+ "concept_map = json.load(open(f'../data/{dataset}/concept_map.json', 'r'))\n",
+ "concept_map = {int(k):v for k,v in concept_map.items()}\n",
+ "metadata = json.load(open(f'../data/{dataset}/metadata.json', 'r'))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "test_data = CAT.dataset.AdapTestDataset(test_triplets, concept_map,\n",
+ " metadata['num_test_students'], \n",
+ " metadata['num_questions'], \n",
+ " metadata['num_concepts'])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import warnings\n",
+ "warnings.filterwarnings(\"ignore\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[INFO 2021-03-01 21:20:10,230] -----------\n",
+ "[INFO 2021-03-01 21:20:10,231] start adaptive testing with Maximum Fisher Information Strategy strategy\n",
+ "[INFO 2021-03-01 21:20:10,231] Iteration 0\n",
+ "[INFO 2021-03-01 21:20:10,260] auc:0.6484533447389293\n",
+ "[INFO 2021-03-01 21:20:10,262] cov:0.0\n",
+ "[INFO 2021-03-01 21:20:10,262] Iteration 1\n",
+ "[INFO 2021-03-01 21:20:11,768] auc:0.6492874695641851\n",
+ "[INFO 2021-03-01 21:20:11,770] cov:0.0503951833541452\n",
+ "[INFO 2021-03-01 21:20:11,773] Iteration 2\n",
+ "[INFO 2021-03-01 21:20:13,255] auc:0.6504904035191303\n",
+ "[INFO 2021-03-01 21:20:13,255] cov:0.09758199115948935\n",
+ "[INFO 2021-03-01 21:20:13,256] Iteration 3\n",
+ "[INFO 2021-03-01 21:20:14,841] auc:0.6521533779951826\n",
+ "[INFO 2021-03-01 21:20:14,843] cov:0.13904934331124966\n",
+ "[INFO 2021-03-01 21:20:14,845] Iteration 4\n",
+ "[INFO 2021-03-01 21:20:16,413] auc:0.6533600336989795\n",
+ "[INFO 2021-03-01 21:20:16,414] cov:0.1810611882609775\n",
+ "[INFO 2021-03-01 21:20:16,415] Iteration 5\n",
+ "[INFO 2021-03-01 21:20:17,927] auc:0.6548746560294985\n",
+ "[INFO 2021-03-01 21:20:17,928] cov:0.2138639398207279\n",
+ "[INFO 2021-03-01 21:20:17,929] Iteration 6\n",
+ "[INFO 2021-03-01 21:20:19,460] auc:0.6557371632351279\n",
+ "[INFO 2021-03-01 21:20:19,461] cov:0.24498131873469464\n",
+ "[INFO 2021-03-01 21:20:19,462] Iteration 7\n",
+ "[INFO 2021-03-01 21:20:20,909] auc:0.6569063425461397\n",
+ "[INFO 2021-03-01 21:20:20,910] cov:0.2693855756697466\n",
+ "[INFO 2021-03-01 21:20:20,911] Iteration 8\n",
+ "[INFO 2021-03-01 21:20:22,342] auc:0.6578221516679326\n",
+ "[INFO 2021-03-01 21:20:22,343] cov:0.2909745397361497\n",
+ "[INFO 2021-03-01 21:20:22,344] Iteration 9\n",
+ "[INFO 2021-03-01 21:20:23,781] auc:0.6591130483479124\n",
+ "[INFO 2021-03-01 21:20:23,781] cov:0.313530251083772\n",
+ "[INFO 2021-03-01 21:20:23,782] Iteration 10\n",
+ "[INFO 2021-03-01 21:20:25,208] auc:0.6602825512892591\n",
+ "[INFO 2021-03-01 21:20:25,209] cov:0.3378566690173305\n",
+ "[INFO 2021-03-01 21:20:25,213] -----------\n",
+ "[INFO 2021-03-01 21:20:25,214] start adaptive testing with KL Information Strategy strategy\n",
+ "[INFO 2021-03-01 21:20:25,215] Iteration 0\n",
+ "[INFO 2021-03-01 21:20:25,233] auc:0.6434585636017105\n",
+ "[INFO 2021-03-01 21:20:25,233] cov:0.0\n",
+ "[INFO 2021-03-01 21:20:25,233] Iteration 1\n",
+ "[INFO 2021-03-01 21:20:31,442] auc:0.6445390034748836\n",
+ "[INFO 2021-03-01 21:20:31,445] cov:0.0503951833541452\n",
+ "[INFO 2021-03-01 21:20:31,448] Iteration 2\n",
+ "[INFO 2021-03-01 21:21:31,010] auc:0.6466906273936509\n",
+ "[INFO 2021-03-01 21:21:31,010] cov:0.09758821238631525\n",
+ "[INFO 2021-03-01 21:21:31,011] Iteration 3\n",
+ "[INFO 2021-03-01 21:22:13,886] auc:0.647910293036912\n",
+ "[INFO 2021-03-01 21:22:13,887] cov:0.1480900453431908\n",
+ "[INFO 2021-03-01 21:22:13,888] Iteration 4\n",
+ "[INFO 2021-03-01 21:22:47,299] auc:0.6495854036505243\n",
+ "[INFO 2021-03-01 21:22:47,300] cov:0.18953852469760513\n",
+ "[INFO 2021-03-01 21:22:47,301] Iteration 5\n",
+ "[INFO 2021-03-01 21:23:16,619] auc:0.6505211807639825\n",
+ "[INFO 2021-03-01 21:23:16,620] cov:0.22356989488930729\n",
+ "[INFO 2021-03-01 21:23:16,622] Iteration 6\n",
+ "[INFO 2021-03-01 21:23:40,716] auc:0.6521429894614312\n",
+ "[INFO 2021-03-01 21:23:40,717] cov:0.2498769584864044\n",
+ "[INFO 2021-03-01 21:23:40,718] Iteration 7\n",
+ "[INFO 2021-03-01 21:24:01,997] auc:0.6528805429947431\n",
+ "[INFO 2021-03-01 21:24:01,997] cov:0.2755841655947051\n",
+ "[INFO 2021-03-01 21:24:01,999] Iteration 8\n",
+ "[INFO 2021-03-01 21:24:22,699] auc:0.653538321650494\n",
+ "[INFO 2021-03-01 21:24:22,700] cov:0.3012489219787817\n",
+ "[INFO 2021-03-01 21:24:22,701] Iteration 9\n",
+ "[INFO 2021-03-01 21:24:42,715] auc:0.6544837753109667\n",
+ "[INFO 2021-03-01 21:24:42,716] cov:0.3182659639579475\n",
+ "[INFO 2021-03-01 21:24:42,717] Iteration 10\n",
+ "[INFO 2021-03-01 21:25:01,542] auc:0.6558215983895119\n",
+ "[INFO 2021-03-01 21:25:01,542] cov:0.3443018600089941\n"
+ ]
+ }
+ ],
+ "source": [
+ "auc_history = {}\n",
+ "cov_history = {}\n",
+ "iters = {}\n",
+ "for strategy in strategies:\n",
+ " model = CAT.model.IRTModel(**config)\n",
+ " model.init_model(test_data)\n",
+ " model.adaptest_load(ckpt_path)\n",
+ " test_data.reset()\n",
+ " auc_history[strategy.name] = []\n",
+ " cov_history[strategy.name] = []\n",
+ " iters[strategy.name] = []\n",
+ " \n",
+ " logging.info('-----------')\n",
+ " logging.info(f'start adaptive testing with {strategy.name} strategy')\n",
+ "\n",
+ " logging.info(f'Iteration 0')\n",
+ " iters[strategy.name].append(0)\n",
+ " # evaluate models\n",
+ " results = model.evaluate(test_data)\n",
+ " auc_history[strategy.name].append(results['auc'])\n",
+ " cov_history[strategy.name].append(results['cov'])\n",
+ " for name, value in results.items():\n",
+ " logging.info(f'{name}:{value}')\n",
+ " \n",
+ " for it in range(1, test_length + 1):\n",
+ " logging.info(f'Iteration {it}')\n",
+ " # select question\n",
+ " selected_questions = strategy.adaptest_select(model, test_data)\n",
+ " for student, question in selected_questions.items():\n",
+ " test_data.apply_selection(student, question)\n",
+ " # update models\n",
+ " model.adaptest_update(test_data)\n",
+ " # evaluate models\n",
+ " results = model.evaluate(test_data)\n",
+ " # log results\n",
+ " iters[strategy.name].append(it)\n",
+ " auc_history[strategy.name].append(results['auc'])\n",
+ " cov_history[strategy.name].append(results['cov'])\n",
+ " for name, value in results.items():\n",
+ " logging.info(f'{name}:{value}')\n",
+ " writer.add_scalars(name, {strategy.name: value}, it)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZUAAAEWCAYAAACufwpNAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/Il7ecAAAACXBIWXMAAAsTAAALEwEAmpwYAABDnklEQVR4nO3dd3iV5fnA8e+dhL0hYa8ACSuBAAEZIiAiCAgqDsQBWEBtFbXVOmqrdVSq/qq2dYEgalVQVECg4GIqAgFZCTMhQMIKAUJCErLu3x/vSXqIENY5ORn357pycc7zrvtNSO7zvM8SVcUYY4zxBD9fB2CMMabssKRijDHGYyypGGOM8RhLKsYYYzzGkooxxhiPsaRijDHGYyypGGOM8RhLKsYUMxFZJiLHRaRSobIJhfbrLyIJbu9FRCaLyFYROSUiCSLyuYiEF2f8xhTFkooxxUhEWgJ9AQVGXOThbwAPAZOBukAoMBcY5rkIjbk8Ab4OwJhy5m7gZ2ANMBb4/EIOEpEQ4HdAL1Vd67bpY49HaMxlsKRiTPG6G/gHTlL5WUQaqOrhCzhuIJBQKKEYU+LY4y9jiomIXAm0AD5T1fVALDDmAg+vBxz0VmzGeIolFWOKz1jgG1U96nr/iasMIAeoUGj/CkC263Uy0MjrERpzmezxlzHFQESqALcC/iJyyFVcCagtIp2BfUDLQocFA3tdr78H3hSRSFWNKoaQjbkkVlMxpnjcAOQCHYAI11d7YCVOO8tsYLyI9HB1HQ4FHgFmAajqLuAt4FNXV+OKIlJZREaLyBPFfTPGnIvYeirGeJ+ILAaiVfUPhcpvBf4JNMVJLn8AmgFHgPeAl1U1z7Wv4HQnnoRTizkOrAKeU9XoYroVY4pkScUYY4zH2OMvY4wxHmNJxRhjjMdYUjHGGOMxllSMMcZ4TLkepxIYGKgtW7b0dRjGGFOqrF+//qiqBp1tW7lOKi1btiQqysaRGWPMxRCRvefaZo+/jDHGeIwlFWOMMR5jScUYY4zHlOs2lbPJzs4mISGBzMxMX4diTLlTuXJlmjZtSoUKhSdsNqWFJZVCEhISqFGjBi1btsSZaskYUxxUleTkZBISEggODvZ1OOYS2eOvQjIzM6lXr54lFGOKmYhQr149e0pQyllSOQtLKMb4hv3ulX6WVIwxphw5mZnNu8tjWbvnmFfO79WkIiJDRGSHiOw+10JCInKriMSISLSIfOJW3lxEvhGRba7tLV3lwSKyxnXO2SJS0VVeyfV+t2t7S2/emzeJCHfeeWfB+5ycHIKCghg+fPglnW/+/PlMmTLFU+FdtP79+9O2bVsiIiKIiIhgzpw5TJgwgZiYmCKP8eTA1JYtW3L06NEi99m+fTsRERF06dKF2NhYj127KBs3bmTRokUF7z35s3rxxRfp2LEjnTp1IiIigjVr1gDw+uuvk56eftHnmzlzJgcOHPBIbKb4JRxP5/kFMfT62/e89N/tLN1xxDsXUlWvfAH+QCzQCqgIbAI6FNonBPgFqON6X99t2zJgkOt1daCq6/VnwGjX63eA+12vfwu843o9Gph9vhi7deumhcXExPyqrLhVq1ZNO3furOnp6aqqumjRIu3cubMOGzbMx5Fdmn79+um6deu8fky+7OzsX5W1aNFCk5KSijzupZde0ueff/6Cr5OXl6e5ubkXHZ+7999/X3/3u99d1jnO5qefftKePXtqZmamqqomJSVpYmKiqhb9vcjJyTnnOS/nZ3IxSsLvYFmyaf9xfeCTDdrqyYXa6smF+tCnG3RLwonLOicQpef4u+rNmkoPYLeqxqlqFs6yqCML7TMReFNVjwOo6hEAEekABKjqt67yNFVNd618dzUwx3X8BzjLtOI69weu13OAgVKKH9AOHTqUhQsXAvDpp59y++23F2xbu3YtvXr1okuXLvTu3ZsdO3YA8Nprr3HPPfcAsGXLFsLCwkhPT2fmzJk88MADAIwbN47777+fnj170qpVK5YtW8Y999xD+/btGTduXME1qlevXvB6zpw5Bdsu9Pjzya+J5ObmMm7cOMLCwggPD+e1114r2Ofzzz+nR48ehIaGsnLlSgByc3N57LHH6N69O506deLdd98FYNmyZfTt25cRI0bQoUOHc143Pj6e9u3bM3HiRDp27Mi1115LRkYGixYt4vXXX+ftt99mwIABAPzjH/8gLCyMsLAwXn/99YLj27Zty913301YWBgrV66kXbt2jBs3jtDQUO644w6+++47+vTpQ0hICGvXrj3nzywrK4u//OUvzJ49m4iICGbPnn3Gzyo+Pp6rr76aTp06MXDgQPbt21fwM5g8eTK9e/emVatWzJkz51f3efDgQQIDA6lUqRIAgYGBNG7cmH/+858cOHCAAQMGFNxn9erV+cMf/kDnzp1ZvXo1zz33HN27dycsLIxJkyahqsyZM4eoqCjuuOMOIiIiyMjIYP369fTr149u3boxePBgDh48CMC6desKakePPfYYYWFhAFx11VVs3LixIMYrr7ySTZs2XfD/GXPh8vKU77cdZvTU1Yz4948s3X6Ee/q0ZMUfB/D66C6ENanlvYufK9tc7hdwM/Ce2/u7gH8X2mcu8DLwI/AzMMRVfgOwAPgSpybzCk7NJxAnUeUf3wzY6nq9FWjqti0WCCwqxvPVVJ6dv1Vvfecnj349O3/reT8FVKtWTTdt2qSjRo3SjIwM7dy5sy5durSgppKSklLwafzbb7/Vm266SVVVc3NztW/fvvrll19qt27ddNWqVap65qfhsWPH6m233aZ5eXk6d+5crVGjhm7evFlzc3O1a9eu+ssvvxTEkO/zzz/XsWPHXtTx7vr166ehoaHauXNn7dy5sx49erTgU29UVJRec801BfseP3684Jjf//73qqq6cOFCHThwoKqqvvvuuwW1iczMTO3WrZvGxcXp0qVLtWrVqhoXF3fW72n+p/M9e/aov79/QZy33HKLfvTRR6qq+swzz+grr7yiqqpRUVEaFhamaWlpmpqaqh06dNANGzbonj17VER09erVqqoF53P/HowfP77g+zNy5Mgif2aFayru74cPH64zZ85UVdXp06cXnGvs2LF68803a25urkZHR2vr1q1/db+pqanauXNnDQkJ0fvvv1+XLVv2q+9FPkBnz55d8D45Obng9Z133qnz588v+Jnk11SysrK0V69eeuTIEVVVnTVrlo4fP15VVTt27Kg//fSTqqo+/vjj2rFjR1VVnTlzpj700EOqqrpjxw492++fqtVULkdGVo5+umavXv3qUm3x+ALt9bfvdOryWE3JyPLodSiipuLrcSoBOI/A+uOs0b1CRMJd5X2BLsA+YDYwDph3uRcUkUk4a3zTvHnzyz2d13Tq1In4+Hg+/fRThg4desa2lJQUxo4dy65duxARsrOzAfDz82PmzJl06tSJe++9lz59+pz13Ndffz0iQnh4OA0aNCA8PByAjh07Eh8fT0RERJGxXcrxH3/8MZGRkb8qb9WqFXFxcTz44IMMGzaMa6+9tmDbTTfdBEC3bt2Ij48H4JtvvmHz5s0Fn85TUlLYtWsXFStWpEePHhc0viE4OLggRvdz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\n",
+ "text/plain": [
+ "