pyBKT: Bayesian Knowledge Tracing in Python

repository·master·Indexed 43 days ago

https://github.com/cahlr/pybkt

A Python implementation of Bayesian Knowledge Tracing (BKT) and its variants used to estimate student cognitive mastery from problem-solving sequences. It features a pure Python version and a high-performance C++ extension for efficient model fitting and prediction. The library includes a Model class for training and evaluation (similar to SciKit-Learn), a Roster class for simulating learning environments, and support for various BKT model variants including individual student and item priors, learn rates, guess, and slip parameters.

Tokens
13.9K
Snippets
44
Records
54
Agent score
55%

What's inside pyBKT

  1. Understand the internal Data format for pyBKT

    master

    The pyBKT model requires a specific data structure to model student mastery. Mastery is treated as a latent variable with two states: "knowing" and "not knowing".

    Required Variables

    • num_subparts: Number of unique questions used for checking understanding.
    • num_resources: Number of unique learning resources available.
    • num_fit_initialization: Number of iterations in the EM step.

    The Data Object Attributes

    • data: A matrix of sequential checkpoints. Rows = subparts, Columns = student checkpoints.
      • 0: No response or no question asked.
      • 1: Wrong response.
      • 2: Correct response.
    • starts: Defines each student's starting column index in the data matrix.
    • lengths: Defines the number of checkpoints for each student.
    • resources: Sequential IDs of resources at each checkpoint (matches the length of the data columns).
    • stateseqs: The true knowledge state (leave undefined before running the model).

    Model Output (fitmodel attributes)

    • As: Transition probabilities between states (includes learns and forgets).
    • learns: Probability of transitioning to "knowing" from "not knowing".
    • forgets: Probability of transitioning to "not knowing" from "knowing".
    • prior: Prior probability of "knowing".
    • guesses: Probability of guessing correctly while "not knowing".
    • slips: Probability of an incorrect response while "knowing".
  2. Understand TensorLayout (ColMajor vs RowMajor)

    master

    Eigen Tensors support two layouts: ColMajor (the default) and RowMajor.

    Important Constraints:

    • Only ColMajor is currently fully supported; using RowMajor is not recommended.
    • All operands in a tensor expression must use the same layout, otherwise a compilation error occurs.

    To change the layout of a tensor, use the swap_layout() method. Note that swap_layout() reverses the order of the dimensions. To swap the layout while preserving the original dimension order, combine swap_layout() with a shuffle() operation.

    // Explicitly specifying layouts
    Tensor<float, 3, ColMajor> col_major;
    TensorMap<Tensor<float, 3, RowMajor>> row_major(data, ...);
    
    // Simple layout swap (reverses dimension order)
    Tensor<float, 2, ColMajor> col_major_orig(2, 4);
    Tensor<float, 2, RowMajor> row_major_swapped = col_major_orig.swap_layout();
    // col_major_orig (2,4) becomes row_major_swapped (4,2)
    
    // Swap layout while preserving dimension order
    array<int, 2> shuffle(1, 0);
    Tensor<float, 2, RowMajor> row_major_orig(2, 4);
    Tensor<float, 2, ColMajor> col_major_preserved = row_major_orig.swap_layout().shuffle(shuffle);
    // row_major_orig (2,4) becomes col_major_preserved (2,4)
  3. Avoid using C++ 'auto' for Tensor expressions

    master

    Because Eigen uses lazy evaluation, using the auto keyword with tensor operations will capture the expression tree (the operator object) rather than the resulting tensor.

    Warning: You cannot access elements directly from an auto expression. Attempting to do so will result in a compilation error.

    To get the actual values, you must assign the expression to a concrete Tensor type.

    Tensor<float, 3> t3 = t1 + t2; 
    cout << t3(0, 0, 0);  // OK: prints the sum
    
    auto t4 = t1 + t2;     
    cout << t4(0, 0, 0);  // ERROR: Compilation error! t4 is an operator, not a tensor
  4. Understand Eigen Tensor Datatypes

    master

    When working with Eigen Tensors, the documentation uses several pseudo-types to describe dimensions and elements:

    • <Tensor-Type>::Dimensions: An array-like object of ints representing the tensor's dimensions. It has a .size attribute (the rank) and can be indexed like an array.
    • <Tensor-Type>::Index: An integer type used for indexing tensors along their dimensions.
    • <Tensor-Type>::Scalar: The underlying data type of the individual tensor elements (e.g., float for Tensor<float, 2>).
    • <Operation>: Indicates that a method returns a deferred operation rather than a concrete tensor. Operations must be evaluated (e.g., by assigning them to a tensor) before values can be accessed.
  5. Supported BKT model variants

    master

    pyBKT supports various Bayesian Knowledge Tracing (BKT) model variants from literature, allowing you to model different levels of individualization:

    • Individual student priors, learn rate, guess, and slip: Models individual characteristics for each student.
    • Individual item guess and slip: Models characteristics specific to each item/problem.
    • Individual item or resource learn rate: Models learning rates specific to items or resources.
  6. Simulate learning environments with the Roster class

    master

    The Roster class simulates a cohort of students. It tracks individual progress by storing only the current latent and observable states, making it highly efficient for simulating learning environments.

    Key Operations:

    • Roster(students=[...], skills=..., model=...): Initializes a roster for specific students and skills.
    • update_state(skill, student, responses): Updates a student's state using a single response or an array of responses (e.g., np.ones(10) for 10 correct responses).
    • get_state_type(skill, student): Returns the mastery state (e.g., StateType.MASTERED).
    • get_mastery_prob(skill, student): Returns the probability of mastery.
    • get_correct_probs(skill) / get_mastery_probs(skill): Returns aggregate statistics for the whole roster.
    • add_student(skill, name): Adds a new student to a skill.
    • remove_student(skill, name): Removes a student.
    • reset_state(skill, student): Resets a student to their initial prior state.
    from pyBKT.models import *
    import numpy as np
    
    model = Model()
    model.fit(data_path='ct.csv')
    
    # Initialize roster
    roster = Roster(students=['Jeff', 'Bob'], skills='Calculate unit rate', model=model)
    
    # Update state with 10 correct responses
    roster.update_state('Calculate unit rate', 'Jeff', np.ones(10))
    
    # Check mastery
    if roster.get_state_type('Calculate unit rate', 'Jeff') == StateType.MASTERED:
        print("Jeff has mastered the skill!")
    
    # Get aggregate probabilities
    print(roster.get_correct_probs('Calculate unit rate'))
  7. Perform lazy Tensor operations in Eigen

    master

    Eigen Tensor operations return non-evaluated Operations objects rather than immediate results. These operations can be chained together (e.g., a.constant(2.0f).pow(2.0f)). The entire chain is evaluated lazily, typically only when the result is assigned to a Tensor object. This allows the library to optimize the evaluation process.

    // Operations are lazy and can be chained
    Eigen::Tensor<float, 2> a(2, 3);
    a.setConstant(1.0f);
    // The expression below is not evaluated until assigned to 'c'
    Eigen::Tensor<float, 2> c = (a + a.constant(2.0f)) * a.constant(0.2f);
  8. Prepare input data for pyBKT

    master

    pyBKT accepts the following input formats:

    • Pandas DataFrames
    • CSV files (comma-separated)
    • TSV files (tab-separated)

    Correctness values: The correct column must use -1 for no response, 0 for incorrect, and 1 for correct.

    Column Mapping: Because column names vary by dataset, you must ensure pyBKT can map your columns to its expected internal names (e.g., order_id, skill_name, correct). For standard datasets like Cognitive Tutor or Assistments, mapping is often automatic. For custom datasets, use a defaults dictionary.

  9. Understand Lazy Evaluation in Eigen Tensors

    master

    Eigen Tensor operations use lazy evaluation. When you perform an operation like t1 + t2, the library does not immediately compute the sum. Instead, it constructs a lightweight "tensor operator" object (e.g., TensorCwiseBinaryOp) that holds references to the operands.

    Actual computation only occurs when the expression is assigned to a concrete tensor type (like Tensor, TensorFixedSize, or TensorMap) or when an element is accessed via a TensorRef.

    This mechanism allows the library to optimize complex expression trees and avoid unnecessary intermediate allocations.

    Tensor<float, 3> t1(2, 3, 4);
    Tensor<float, 3> t2(2, 3, 4);
    // t3 is the result of the actual addition performed during assignment
    Tensor<float, 3> t3 = t1 + t2;
  10. Control when Tensor expressions are evaluated

    master

    You can force the evaluation of a lazy expression using several methods:

    1. Assignment to a Tensor: Assigning to Tensor, TensorFixedSize, or TensorMap triggers evaluation.
    2. Using .eval(): Inserts an intermediate evaluation point in an expression tree. This is useful for improving performance in complex expressions or preventing aliasing issues (where the output tensor is also an input).
    3. Using TensorRef: A wrapper that allows accessing individual elements of an expression by evaluating them on-the-fly. Use this for accessing subsets of values without materializing a full tensor.

    When to use .eval():

    • To prevent redundant computations in complex trees (e.g., before a reshape() or broadcast()).
    • To avoid aliasing errors when the target of an assignment is also part of the expression (e.g., Y = Y / Y.sum()).
    // 1. Evaluation via assignment
    auto t3 = t1 + t2;
    Tensor<float, 3> result = t3; // Evaluated here
    
    // 2. Evaluation via .eval() to prevent aliasing
    Y = Y / (Y.sum(depth_dim).eval().reshape(dims2d).broadcast(bcast));
    
    // 3. Evaluation via TensorRef for on-the-fly access
    TensorRef<Tensor<float, 3>> ref = ((t1 + t2) * 0.2f).exp();
    float val = ref(0, 0, 0); // Evaluated on demand
  11. Install pyBKT

    master

    You can install pyBKT using pip. By default, pip attempts to install the optimized Python/C++ extension version. If the required C++ libraries are missing or installation fails, it will automatically fall back to the pure Python implementation.

    If pip fails, you can install manually from the repository source:

    pip install pyBKT
    # Manual installation if pip fails
    git clone https://github.com/CAHLR/pyBKT.git
    cd pyBKT
    python3 setup.py install