# coding=utf-8
from itertools import product
import numpy as np
import progressbar
from psy.exceptions import ConvergenceError
from psy.utils import cached_property, get_log_beta_pd, get_log_normal_pd, get_log_lognormal_pd
[docs]class Dina(object):
def __init__(self, attrs, score=None):
self._attrs = attrs
self._score = score
@cached_property
def _people_size(self):
return self._score.shape[0]
@cached_property
def item_size(self):
# 题量
return self._attrs.shape[1]
@cached_property
def _skills_size(self):
# 被试技能数量,也是试题属性数量
return self._attrs.shape[0]
[docs] def get_yita(self, skills):
# dina模型下的yita值
_yita = np.dot(skills, self._attrs)
_aa = np.sum(self._attrs * self._attrs, axis=0)
_yita[_yita < _aa] = 0
_yita[_yita == _aa] = 1
return _yita
@staticmethod
def _get_p(yita, no_slip, guess):
# dina模型下的答题正确的概率值
return no_slip ** yita * guess ** (1 - yita)
[docs] def get_p(self, yita, no_slip, guess):
# 答对的概率值
p_val = self._get_p(yita, no_slip, guess)
p_val[p_val <= 0] = 1e-10
p_val[p_val >= 1] = 1 - 1e-10
return p_val
[docs]class BaseEmDina(Dina):
def _loglik(self, p_val):
# dina模型的对数似然函数
log_p_val = np.log(p_val)
log_q_val = np.log(1 - p_val)
score = self._score
return np.dot(log_p_val, score.transpose()) + np.dot(log_q_val, (1 - score).transpose())
def _get_all_skills(self):
# 获得所有可能被试技能的排列组合
size = self._skills_size
return np.array(list(product([0, 1], repeat=size)))
[docs]class MlDina(BaseEmDina):
def __init__(self, guess, no_slip, *args, **kwargs):
super(MlDina, self).__init__(*args, **kwargs)
self._guess = guess
self._no_slip = no_slip
[docs] def solve(self):
# 已知项目参数下的被试技能极大似然估计求解
skills = self._get_all_skills()
yita = self.get_yita(skills)
p_val = self.get_p(yita, self._no_slip, self._guess)
loglik = self._loglik(p_val)
return skills[loglik.argmax(axis=0)]
[docs]class EmDina(BaseEmDina):
def __init__(self, guess_init=None, no_slip_init=None, max_iter=100, tol=1e-5, *args, **kwargs):
super(EmDina, self).__init__(*args, **kwargs)
self._skills = self._get_all_skills()
self._no_slip_init = np.zeros((1, self.item_size)) + 0.7 if no_slip_init is None else no_slip_init
self._guess_init = np.zeros((1, self.item_size)) + 0.3 if guess_init is None else guess_init
self._max_iter = max_iter
self._tol = tol
def _posterior(self, p_val):
# 后验似然函数
return np.exp(self._loglik(p_val) + 1.0 / p_val.shape[0])
def _posterior_normalize(self, p_val):
# 这个主要是起归一化的作用
posterior = self._posterior(p_val)
return posterior / np.sum(posterior, axis=0)
@staticmethod
def _skill_dis(posterior_normalize):
# 每种技能组合的人数分布
return np.sum(posterior_normalize, axis=1)
def _get_init_yita_item_dis(self, posterior_normalize):
# 每道题都搞个人数分布,1是0的复制,0用于yita为0情况,1个用于yita为1情况
yita_item_dis_0 = np.repeat(self._skill_dis(posterior_normalize), self.item_size)
yita_item_dis_0.shape = posterior_normalize.shape[0], self.item_size
yita_item_dis_1 = yita_item_dis_0.copy()
return yita_item_dis_0, yita_item_dis_1
[docs] def em(self):
skills = self._get_all_skills()
yita_val = self.get_yita(skills)
score = self._score
max_iter = self._max_iter
tol = self._tol
guess = self._guess_init
no_slip = self._no_slip_init
for i in range(max_iter):
p_val = self.get_p(yita_val, no_slip, guess)
post_normalize = self._posterior_normalize(p_val)
yita_item_dis_0, yita_item_dis_1 = self._get_init_yita_item_dis(post_normalize)
# 回答正确的归一化数, 1是0的复制,0用于yita为0情况,1用于yita为1情况
yita_item1_post_normalize_0 = np.dot(post_normalize, score)
yita_item1_post_normalize_1 = yita_item1_post_normalize_0.copy()
yita0_item1_dis, yita0_item_dis, yita1_item1_dis, yita1_item_dis = self._get_yita_item_dis(
yita_item_dis_0, yita_item1_post_normalize_0, yita_item_dis_1, yita_item1_post_normalize_1, yita_val
)
guess_temp = self._get_est_guess(yita0_item1_dis, yita0_item_dis)
no_slip_temp = self._get_est_no_slip(yita1_item1_dis, yita1_item_dis)
if max(np.max(np.abs(guess - guess_temp)), np.max(np.abs(no_slip - no_slip_temp))) < tol:
return no_slip_temp, guess_temp
no_slip = no_slip_temp
guess = guess_temp
raise ConvergenceError('no Convergence')
@staticmethod
def _get_yita_item_dis(yita_item_dis_0, yita_item1_post_normalize_0, yita_item_dis_1,
yita_item1_post_normalize_1, yita_val):
yita_item_dis_0[yita_val == 1] = 0
# yita值为0的人数分布
yita0_item_dis = np.sum(yita_item_dis_0, axis=0)
yita0_item_dis[yita0_item_dis <= 0] = 1e-10
yita_item1_post_normalize_0[yita_val == 1] = 0
# yita值为0回答正确的人数分布
yita0_item1_dis = np.sum(yita_item1_post_normalize_0, axis=0)
yita_item_dis_1[yita_val == 0] = 0
# yita值为1的人数分布
yita1_item_dis = np.sum(yita_item_dis_1, axis=0)
yita_item1_post_normalize_1[yita_val == 0] = 0
# yita值为1回答正确的人数分布
yita1_item1_dis = np.sum(yita_item1_post_normalize_1, axis=0)
return yita0_item1_dis, yita0_item_dis, yita1_item1_dis, yita1_item_dis
@staticmethod
def _get_est_guess(yita0_item1_dis, yita0_item_dis):
guess = yita0_item1_dis / yita0_item_dis
guess[guess <= 0] = 1e-10
return guess
@staticmethod
def _get_est_no_slip(yita1_item1_dis, yita1_item_dis):
no_slip = 1 - (yita1_item_dis - yita1_item1_dis) / yita1_item_dis
no_slip[no_slip >= 1] = 1 - 1e-10
return no_slip
[docs]class BaseMcmcDina(Dina):
def __init__(self, thin=1, burn=3000, max_iter=10000, *args, **kwargs):
super(BaseMcmcDina, self).__init__(*args, **kwargs)
self.max_iter = max_iter * thin
self.burn = burn
self.thin = thin
def _get_item_params_tran(self, skills, no_slip, guess, next_no_slip, next_guess):
# 项目参数转移概率函数
yita_val = self.get_yita(skills)
pre = self._get_loglik(yita_val, no_slip, guess, axis=0) + get_log_beta_pd(no_slip, guess)
nxt = self._get_loglik(yita_val, next_no_slip, next_guess, axis=0) + get_log_beta_pd(next_no_slip, next_guess)
res = np.exp(nxt - pre)
res[res > 1] = 1
return res
def _get_loglik(self, yita, no_slip, guess, axis):
score = self._score
p_val = self.get_p(yita, no_slip, guess)
return np.sum(score * np.log(p_val) + (1 - score) * np.log(1 - p_val), axis)
def _get_item_params_init(self, size):
# 初始值
skills = np.ones((self._people_size, self._skills_size))
skills_list = np.zeros((size, self._people_size, self._skills_size))
no_slip = np.zeros((1, self.item_size)) + 0.7
guess = np.zeros((1, self.item_size)) + 0.3
no_slip_list = np.zeros((size, self.item_size))
guess_list = np.zeros((size, self.item_size))
return guess, guess_list, no_slip, no_slip_list, skills, skills_list
def _get_item_params_tran_res(self, skills, no_slip, guess):
# 项目参数转移的结果
next_no_slip = np.random.uniform(no_slip - 0.1, no_slip + 0.1)
next_no_slip[next_no_slip <= 0.4] = 0.4 + 1e-10
next_no_slip[next_no_slip >= 1] = 1 - 1e-10
next_guess = np.random.uniform(guess - 0.1, guess + 0.1)
next_guess[next_guess <= 0] = 1e-10
next_guess[next_guess >= 0.6] = 0.6 - 1e-10
tran_param = self._get_item_params_tran(skills, no_slip, guess, next_no_slip, next_guess)
param_r = np.random.uniform(0, 1, tran_param.shape)
no_slip[tran_param >= param_r] = next_no_slip[tran_param >= param_r]
guess[tran_param >= param_r] = next_guess[tran_param >= param_r]
return no_slip, guess
[docs]class McmcDina(BaseMcmcDina):
def _get_skills_tran(self, skills, no_slip, guess, next_skills):
# 被试技能参数转移概率函数
yita_val = self.get_yita(skills)
pre = self._get_loglik(yita_val, no_slip, guess, axis=1)
next_yita_val = self.get_yita(next_skills)
nxt = self._get_loglik(next_yita_val, no_slip, guess, axis=1)
res = np.exp(nxt - pre)
res[res > 1] = 1
return res
[docs] def mcmc(self):
size = self.max_iter
bar = progressbar.ProgressBar()
guess, guess_list, no_slip, no_slip_list, skills, skills_list = self._get_item_params_init(size)
for i in bar(range(size)):
skills = self._get_skills_tran_res(skills, no_slip, guess)
no_slip, guess = self._get_item_params_tran_res(skills, no_slip, guess)
skills_list[i] = skills
no_slip_list[i] = no_slip
guess_list[i] = guess
est_skills = np.mean(skills_list[self.burn::self.thin], axis=0)
est_no_slip = np.mean(no_slip_list[self.burn::self.thin], axis=0)
est_guess = np.mean(guess_list[self.burn::self.thin], axis=0)
return est_skills, est_no_slip, est_guess
def _get_skills_tran_res(self, skills, no_slip, guess):
# 被试技能参数转移结果
next_skills = np.random.binomial(1, 0.5, skills.shape)
tran_skills = self._get_skills_tran(skills, no_slip, guess, next_skills)
skills_r = np.random.uniform(0, 1, tran_skills.shape)
skills[tran_skills >= skills_r] = next_skills[tran_skills >= skills_r]
return skills
[docs]class McmcHoDina(BaseMcmcDina):
[docs] @staticmethod
def get_skills_p(lam0, lam1, theta):
# 高阶能力
exp_z = np.exp(theta * lam1 + lam0)
p_val = exp_z / (1.0 + exp_z)
return p_val
def _get_skills_pd(self, skills, theta, lam0, lam1, axis):
# 高阶能力的概率密度函数
p_val = self.get_skills_p(lam0, lam1, theta)
p_val[p_val <= 0] = 1e-10
p_val[p_val >= 1] = 1 - 1e-10
return np.sum(skills * np.log(p_val) + (1 - skills) * np.log(1 - p_val), axis)
def _get_skills_tran(self, skills, no_slip, guess, theta, lam0, lam1, next_skills):
# 被试技能转移概率
yita_val = self.get_yita(skills)
pre = self._get_loglik(yita_val, no_slip, guess, axis=1) + \
self._get_skills_pd(skills, theta, lam0, lam1, 1)
next_yita_val = self.get_yita(next_skills)
nxt = self._get_loglik(next_yita_val, no_slip, guess, axis=1) + \
self._get_skills_pd(next_skills, theta, lam0, lam1, 1)
res = np.exp(nxt - pre)
res[res > 1] = 1
return res
def _get_theta_tran(self, skills, theta, lam0, lam1, next_theta):
# 高阶能力转移概率
pre = self._get_skills_pd(skills, theta, lam0, lam1, 1) + get_log_normal_pd(theta)[:, 0]
nxt = self._get_skills_pd(skills, next_theta, lam0, lam1, 1) + get_log_normal_pd(next_theta)[:, 0]
res = np.exp(nxt - pre)
res[res > 1] = 1
return res
def _get_lam_tran(self, skills, theta, lam0, lam1, next_lam0, next_lam1):
# 高阶参数转移概率
pre = self._get_skills_pd(skills, theta, lam0, lam1, 0) + get_log_normal_pd(lam0) + \
get_log_lognormal_pd(lam1)
nxt = self._get_skills_pd(skills, theta, next_lam0, next_lam1, 0) + get_log_normal_pd(next_lam0) + \
get_log_lognormal_pd(next_lam1)
res = np.exp(nxt - pre)
res[res > 1] = 1
return res
def _get_skills_tran_res(self, skills, no_slip, guess, theta, lam0, lam1):
# 被试技能转移结果
next_skills = np.random.binomial(1, 0.5, skills.shape)
tran_skills = self._get_skills_tran(skills, no_slip, guess, theta, lam0, lam1, next_skills)
skills_r = np.random.uniform(0, 1, tran_skills.shape)
skills[tran_skills >= skills_r] = next_skills[tran_skills >= skills_r]
return skills
[docs] def mcmc(self):
size = self.max_iter
bar = progressbar.ProgressBar()
guess, guess_list, no_slip, no_slip_list, skills, skills_list = self._get_item_params_init(size)
theta = np.zeros((self._people_size, 1))
theta_list = np.zeros((size, self._people_size, 1))
lam0 = np.zeros(5)
lam0_list = np.zeros((size, 5))
lam1 = np.ones(5)
lam1_list = np.zeros((size, 5))
for i in bar(range(size)):
next_lam0 = np.random.uniform(lam0 - 0.3, lam0 + 0.3)
next_lam1 = np.random.uniform(lam1 - 0.3, lam1 + 0.3)
next_lam1[next_lam1 <= 0] = 1e-10
next_lam1[next_lam1 > 4] = 4
tran_lam = self. _get_lam_tran(skills, theta, lam0, lam1, next_lam0, next_lam1)
lam_r = np.random.uniform(0, 1, tran_lam.shape)
lam0[tran_lam >= lam_r] = next_lam0[tran_lam >= lam_r]
lam1[tran_lam >= lam_r] = next_lam1[tran_lam >= lam_r]
lam0_list[i] = lam0
lam1_list[i] = lam1
next_theta = np.random.normal(theta, 0.1)
tran_theta = self._get_theta_tran(skills, theta, lam0, lam1, next_theta)
theta_r = np.random.uniform(0, 1, tran_theta.shape)
theta[tran_theta >= theta_r] = next_theta[tran_theta >= theta_r]
theta_list[i] = theta
skills = self._get_skills_tran_res(skills, no_slip, guess, theta, lam0, lam1)
no_slip, guess = self._get_item_params_tran_res(skills, no_slip, guess)
skills_list[i] = skills
no_slip_list[i] = no_slip
guess_list[i] = guess
est_lam0 = np.mean(lam0_list[self.burn::self.thin], axis=0)
est_lam1 = np.mean(lam1_list[self.burn::self.thin], axis=0)
est_theta = np.mean(theta_list[self.burn::self.thin], axis=0)
est_skills = np.mean(skills_list[self.burn::self.thin], axis=0)
est_no_slip = np.mean(no_slip_list[self.burn::self.thin], axis=0)
est_guess = np.mean(guess_list[self.burn::self.thin], axis=0)
return est_lam0, est_lam1, est_theta, est_skills, est_no_slip, est_guess