Views
No views yet
| Component | Description |
|---|---|
| Base Encoder | ViT-L14 from OpenCLIP |
| Scorer (Regression Head) | A fully connected neural network (3 layers with ReLU activation and dropout) |
1def build_prompt(r: pd.Series)->str:
2 return (f"1.{str(r.get('skill_name',''))[:50]} "
3 f"2.{str(r.get('grade level',''))[:20]} "
4 f"3.{str(r.get('problem_body',''))[:300]} "
5 f"4.{str(r.get('student_response',''))[:200]}")Li, H., Xing, W., Zhu, W., Li, C., Lyu, B., Liu, Z., & Heffernan, N. (2025). Leveraging multi-modality and collaborative filtering for supporting automatic scoring in mathematics education. Proceedings of the 26th International Conference on Artificial Intelligence in Education.
1import torch, open_clip
2from PIL import Image
3
4# ---------- 0. Paths ----------
5root_dir = "./"
6clip_ckpt = f"{root_dir}/full_clip.pt"
7reg_ts = f"{root_dir}/reg_head.ts"
8pp_ckpt = f"{root_dir}/preprocess.pt"
9
10image_paths = ["path_to_image1", "path_to_image2"]
11text_prompts = ["prompt_q+ans_1", "prompt_q+ans_2"]
12device = "cuda" if torch.cuda.is_available() else "cpu"
13
14# ---------- 1. CLIP Backbone ----------
15clip_name = "ViT-L-14"
16clip = open_clip.create_model(clip_name, device=device, pretrained=None)
17clip.load_state_dict(torch.load(clip_ckpt, map_location=device), strict=False)
18clip.eval(); clip.requires_grad_(False)
19
20# ---------- 2. Regression Head ----------
21reg_head = torch.jit.load(reg_ts, map_location=device)
22reg_head.eval()
23
24# ---------- 3. Pre-processing ----------
25preprocess = torch.load(pp_ckpt, weights_only=False)
26
27# ---------- 4. Inference (batch size = 2) ----------
28# 4-1. Build image & text batches
29imgs = torch.stack(
30 [preprocess(Image.open(p).convert("RGB")) for p in image_paths]
31).to(device) # (2, 3, H, W)
32
33toks = open_clip.tokenize(
34 text_prompts, context_length=clip.context_length
35).to(device) # (2, ctx_len)
36
37with torch.no_grad():
38 #----- Encode -----
39 img_emb = torch.nn.functional.normalize(clip.encode_image(imgs), dim=1) # (2, D)
40 txt_emb = torch.nn.functional.normalize(clip.encode_text(toks), dim=1) # (2, D)
41
42 #----- Fuse image & text into one embedding per sample -----
43 # Here we simply average the two L2-normalised vectors, then renormalise.
44 fused_emb = torch.nn.functional.normalize(img_emb + txt_emb, dim=1) # (2, D)
45
46 #----- Similarity between the two fused samples -----
47 # fused_emb[0] · fused_emb[1] (equivalent to (fused_emb @ fused_emb.T)[0,1])
48 pair_sim = (fused_emb[0] * fused_emb[1]).sum().item()
49
50 #----- Regression scores (unchanged) -----
51 scores = reg_head(img_emb).squeeze(-1) # (2,)
52
53# ---------- 5. Output ----------
54print(f"Cosine similarity between sample-1 and sample-2: {pair_sim:.4f}\n")
55print(f"[Sample 1] Regression score: {scores[0]:.4f}")
56print(f"[Sample 2] Regression score: {scores[1]:.4f}")